Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 71 additions & 4 deletions skills/shadow-frog-dream/dream-reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,67 @@ def _resolve_tip_commit(repo_root, branch):
return 'unknown'


def _manifest_lineage_values(manifest, key):
"""Return non-empty string values for one manifest lineage key."""
value = manifest.get(key) or ''
values = value if isinstance(value, list) else [value]
return [item.strip() for item in values
if isinstance(item, str) and item.strip()]


def _resolve_remote_parent_branch(repo_root, parent_ref):
"""Resolve a dream ID or branch reference to one remote dream branch."""
dream_id = parent_ref.rsplit('/', 1)[-1]
raw = git(
'for-each-ref',
'--format=%(refname:strip=3)',
f'refs/remotes/origin/dream/*/{dream_id}',
cwd=repo_root,
check=False,
)
matches = [line.strip() for line in raw.splitlines() if line.strip()]
if parent_ref.startswith('dream/') and parent_ref in matches:
return parent_ref
return matches[0] if len(matches) == 1 else ''


def _report_parent_branch(report):
"""Read parent_branch from report frontmatter."""
if not report:
return ''
frontmatter = re.match(r'^\ufeff?\s*---\r?\n(.*?)\r?\n---', report, re.S)
if not frontmatter:
return ''
match = re.search(
r'^parent_branch:\s*["\']?([^"\'\r\n]+)',
frontmatter.group(1),
re.M,
)
parent = match.group(1).strip() if match else ''
return '' if parent.lower() in ('null', '~') else parent


def _resolve_parent_branch(repo_root, branch, dream_id, manifest):
"""Resolve manifest/report lineage to the canonical parent branch."""
for key in ('parent_branch', 'base_branch'):
values = _manifest_lineage_values(manifest, key)
if values:
return values[0]

for key in ('parent_dream_id', 'builds_on'):
for parent_ref in _manifest_lineage_values(manifest, key):
parent = _resolve_remote_parent_branch(repo_root, parent_ref)
if parent:
return parent

report = git_show(
f'origin/{branch}',
f'.shadow/_dreams/{dream_id}/report.md',
cwd=repo_root,
)
return _report_parent_branch(report) or 'main'


def update_index(repo_root, manifests, dry_run=False):
"""Add entries to _dreams/_index.md for reconciled branches."""
index_path = os.path.join(repo_root, '.shadow', '_dreams', '_index.md')
Expand All @@ -845,12 +906,18 @@ def update_index(repo_root, manifests, dry_run=False):
with open(index_path, 'a', encoding="utf-8") as f:
for branch, dream_id, manifest in manifests:
tip = _resolve_tip_commit(repo_root, branch)
cat = re.sub(r'\s*\(.*\)\s*$', '', manifest.get('category', 'unknown').lower().strip())
verdict = manifest.get('verdict', 'unknown').lower().strip()
parent = manifest.get('parent_branch', 'main').strip()
cat = re.sub(
r'\s*\(.*\)\s*$',
'',
(manifest.get('category') or 'unknown').lower().strip(),
)
verdict = (manifest.get('verdict') or 'unknown').lower().strip()
parent = _resolve_parent_branch(
repo_root, branch, dream_id, manifest
)

# Get title from manifest or report heading
title = manifest.get('title', '')
title = manifest.get('title') or ''
if not title:
report = git_show(f'origin/{branch}',
f'.shadow/_dreams/{dream_id}/report.md', cwd=repo_root)
Expand Down
70 changes: 51 additions & 19 deletions skills/shadow-frog-meditate/meditate-repair.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@
signals so 'not useful' doesn't match 'useful'.
4. Title repair: replace generic titles (raw slug, "Dream Report: <slug>",
"Dream t##: <slug>") with the first H1 or first ## Summary line.
5. Parent linkage from manifest: for rows where parent is 'main', check
manifest.json and report.md frontmatter for a different parent_branch.
Validate the parent exists in the index (match by slug if timestamps
differ).
5. Parent linkage from metadata: for rows where parent is 'main', check
manifest parent_branch, base_branch, parent_dream_id/builds_on, then
report.md frontmatter parent_branch. Validate the parent exists in the
index (match by slug if timestamps differ).
6. Parent linkage from slug heuristics: for rows still parented to 'main'
with no manifest info, infer from compounding suffixes (-extend, -fix,
-deeper, -improve, -integration, -cleanup, -metrics, -remaining).
Expand Down Expand Up @@ -178,6 +178,28 @@ def resolve_parent_in_index(
return None


def manifest_lineage_values(manifest: dict, key: str) -> list[str]:
"""Return non-empty string values for one manifest lineage key."""
value = manifest.get(key) or ''
values = value if isinstance(value, list) else [value]
return [item.strip() for item in values
if isinstance(item, str) and item.strip()]


def report_parent_branch(content: str) -> str:
"""Read parent_branch from report frontmatter."""
frontmatter = re.match(r'^\ufeff?\s*---\r?\n(.*?)\r?\n---', content, re.S)
if not frontmatter:
return ''
match = re.search(
r'^parent_branch:\s*["\']?([^"\'\r\n]+)',
frontmatter.group(1),
re.M,
)
parent = match.group(1).strip() if match else ''
return '' if parent.lower() in ('null', '~') else parent


def repair_parent(
parts: list[str], dreams_dir: str, all_dream_ids: list[str],
branch_by_dream_id: dict[str, str],
Expand All @@ -201,33 +223,43 @@ def repair_parent(

# Step 10: manifest.json lookup
manifest_path = os.path.join(dream_dir, 'manifest.json')
parent_branch_raw: str | None = None
if os.path.exists(manifest_path):
try:
with open(manifest_path, encoding="utf-8") as mf:
mdata = json.load(mf)
pb = mdata.get('parent_branch', '').strip()
if pb and pb != 'main':
parent_branch_raw = pb
if isinstance(mdata, dict):
for key in (
'parent_branch', 'base_branch',
'parent_dream_id', 'builds_on',
):
for parent_ref in manifest_lineage_values(mdata, key):
if parent_ref == 'main':
return False
candidate_id = parent_ref.rsplit('/', 1)[-1]
resolved_did = resolve_parent_in_index(
candidate_id, all_dream_ids
)
if resolved_did is not None:
break
if resolved_did is not None:
break
except (OSError, json.JSONDecodeError):
pass

# Fallback: report.md frontmatter
if parent_branch_raw is None:
if resolved_did is None:
report_path = os.path.join(dream_dir, 'report.md')
if os.path.exists(report_path):
with open(report_path, encoding="utf-8") as rf:
rcontent = rf.read()
fm_match = re.search(r'parent_branch:\s*["\']?([^"\'\n]+)', rcontent)
if fm_match:
pb = fm_match.group(1).strip()
if pb and pb != 'main':
parent_branch_raw = pb

if parent_branch_raw:
# Extract dream_id from branch path (last /-separated segment)
candidate_id = parent_branch_raw.split('/')[-1]
resolved_did = resolve_parent_in_index(candidate_id, all_dream_ids)
parent_ref = report_parent_branch(rcontent)
if parent_ref == 'main':
return False
if parent_ref:
candidate_id = parent_ref.rsplit('/', 1)[-1]
resolved_did = resolve_parent_in_index(
candidate_id, all_dream_ids
)

# Step 11: slug heuristic (only if step 10 didn't resolve anything)
if resolved_did is None:
Expand Down
117 changes: 117 additions & 0 deletions tests/skills/shadow_frog_dream/test_dream_reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1898,6 +1898,123 @@ def test_update_index_normalizes_category_with_parens(
assert "(notes" not in row


@pytest.mark.slow
def test_update_index_uses_base_branch_when_parent_branch_is_null(
dream_reconcile, tmp_git_repo
):
env = _seed_repo(tmp_git_repo)
_add_bare_remote(tmp_git_repo, env)
parent_id = "20260420-030800Z-parent"
child_id = "20260420-030900Z-child"
parent_branch = make_dream_branch(
tmp_git_repo, env, "proj", parent_id, _default_manifest(parent_id)
)
child_manifest = _default_manifest(child_id)
child_manifest["parent_branch"] = None
child_manifest["base_branch"] = parent_branch
child_branch = make_dream_branch(
tmp_git_repo, env, "proj", child_id, child_manifest
)

dream_reconcile.update_index(
str(tmp_git_repo), [(child_branch, child_id, child_manifest)]
)

body = (tmp_git_repo / ".shadow" / "_dreams" / "_index.md").read_text(
encoding="utf-8"
)
row = [line for line in body.splitlines() if child_id in line][0]
assert f"| {child_branch} | {parent_branch} |" in row


@pytest.mark.slow
@pytest.mark.parametrize(
("lineage_key", "as_list"),
[("parent_dream_id", False), ("builds_on", True)],
)
def test_update_index_resolves_parent_id_from_remote_branch(
dream_reconcile, tmp_git_repo, lineage_key, as_list
):
env = _seed_repo(tmp_git_repo)
_add_bare_remote(tmp_git_repo, env)
parent_id = "20260420-031000Z-parent"
child_id = "20260420-031100Z-child"
parent_branch = make_dream_branch(
tmp_git_repo, env, "proj", parent_id, _default_manifest(parent_id)
)
child_manifest = _default_manifest(child_id)
del child_manifest["parent_branch"]
child_manifest[lineage_key] = [parent_id] if as_list else parent_id
child_branch = make_dream_branch(
tmp_git_repo, env, "proj", child_id, child_manifest
)

dream_reconcile.update_index(
str(tmp_git_repo), [(child_branch, child_id, child_manifest)]
)

body = (tmp_git_repo / ".shadow" / "_dreams" / "_index.md").read_text(
encoding="utf-8"
)
row = [line for line in body.splitlines() if child_id in line][0]
assert f"| {child_branch} | {parent_branch} |" in row


@pytest.mark.slow
def test_update_index_falls_back_to_report_parent(
dream_reconcile, tmp_git_repo
):
env = _seed_repo(tmp_git_repo)
_add_bare_remote(tmp_git_repo, env)
parent_id = "20260420-031200Z-parent"
child_id = "20260420-031300Z-child"
parent_branch = make_dream_branch(
tmp_git_repo, env, "proj", parent_id, _default_manifest(parent_id)
)
child_manifest = _default_manifest(child_id)
del child_manifest["parent_branch"]
report = _default_report(child_id).replace(
"---\n\n", f'parent_branch: "{parent_branch}"\n---\n\n'
)
child_branch = make_dream_branch(
tmp_git_repo, env, "proj", child_id, child_manifest, report=report
)

dream_reconcile.update_index(
str(tmp_git_repo), [(child_branch, child_id, child_manifest)]
)

body = (tmp_git_repo / ".shadow" / "_dreams" / "_index.md").read_text(
encoding="utf-8"
)
row = [line for line in body.splitlines() if child_id in line][0]
assert f"| {child_branch} | {parent_branch} |" in row


@pytest.mark.slow
def test_update_index_without_lineage_records_main(
dream_reconcile, tmp_git_repo
):
env = _seed_repo(tmp_git_repo)
_add_bare_remote(tmp_git_repo, env)
dream_id = "20260420-031400Z-root"
manifest = _default_manifest(dream_id)
del manifest["parent_branch"]
branch = make_dream_branch(
tmp_git_repo, env, "proj", dream_id, manifest, report=None
)

dream_reconcile.update_index(
str(tmp_git_repo), [(branch, dream_id, manifest)]
)

body = (tmp_git_repo / ".shadow" / "_dreams" / "_index.md").read_text(
encoding="utf-8"
)
row = [line for line in body.splitlines() if dream_id in line][0]
assert f"| {branch} | main |" in row


# ===========================================================================
# update_state
# ===========================================================================
Expand Down
Loading