fix(kanban): decompose children inherit root workspace instead of forcing scratch (#37172)

decompose_triage_task hardcoded every fan-out child to workspace_kind
'scratch', ignoring the root task's workspace. A code-gen task created
with a dir:/worktree: workspace would fan out into throwaway scratch tmp
dirs (GC'd on archive), so generated code never landed in the project.

Children now inherit the root's workspace_kind + workspace_path. A child
dict may still override with its own workspace_kind/workspace_path; the
path only carries over when kinds match. Scratch roots are unchanged.
This commit is contained in:
Teknium 2026-06-01 20:26:57 -07:00 committed by GitHub
parent fa3b06b035
commit 72e82f88c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 87 additions and 3 deletions

View file

@ -4353,13 +4353,21 @@ def decompose_triage_task(
child_ids: list[str] = []
with write_txn(conn):
root_row = conn.execute(
"SELECT id, status, tenant FROM tasks WHERE id = ?", (task_id,)
"SELECT id, status, tenant, workspace_kind, workspace_path "
"FROM tasks WHERE id = ?",
(task_id,),
).fetchone()
if root_row is None:
return None
if root_row["status"] != "triage":
return None
tenant = root_row["tenant"]
# Children inherit the root's workspace by default so a fan-out
# of a code-gen task lands in the parent's project dir/worktree
# rather than throwaway scratch tmp dirs. A child dict can still
# override with its own 'workspace_kind' / 'workspace_path'.
root_ws_kind = root_row["workspace_kind"] or "scratch"
root_ws_path = root_row["workspace_path"]
# Create children. Status is 'todo' regardless of parents — we
# link them under the root AFTER creation so the dispatcher
@ -4370,16 +4378,30 @@ def decompose_triage_task(
title = child["title"].strip()
body = child.get("body")
assignee = _canonical_assignee(child.get("assignee"))
# Per-child override wins; otherwise inherit the root's
# workspace. A child that sets workspace_kind without a path
# falls back to the root path only when kinds match (so a
# child can't accidentally point a 'dir' at the root's
# worktree path or vice versa).
child_ws_kind = child.get("workspace_kind") or root_ws_kind
if child.get("workspace_path"):
child_ws_path = child.get("workspace_path")
elif child_ws_kind == root_ws_kind:
child_ws_path = root_ws_path
else:
child_ws_path = None
conn.execute(
"INSERT INTO tasks "
"(id, title, body, assignee, status, workspace_kind, "
" tenant, created_at, created_by) "
"VALUES (?, ?, ?, ?, 'todo', 'scratch', ?, ?, ?)",
" workspace_path, tenant, created_at, created_by) "
"VALUES (?, ?, ?, ?, 'todo', ?, ?, ?, ?, ?)",
(
new_id,
title,
body if isinstance(body, str) else None,
assignee,
child_ws_kind,
child_ws_path,
tenant,
now,
(author or "decomposer"),

View file

@ -166,3 +166,65 @@ def test_decompose_records_audit_comment_and_event(kanban_home):
assert any("Decomposed into" in (c.body or "") for c in comments)
assert any(ev.kind == "decomposed" for ev in events)
def test_decompose_children_inherit_dir_workspace(kanban_home):
"""Fan-out children inherit the root's dir workspace, not scratch."""
proj = "/home/teknium/myproject"
with kb.connect() as conn:
tid = kb.create_task(
conn, title="codegen root", assignee="worker",
workspace_kind="dir", workspace_path=proj, triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[{"title": "part A"}, {"title": "part B", "parents": [0]}],
author="decomposer",
)
assert child_ids and len(child_ids) == 2
with kb.connect() as conn:
for cid in child_ids:
t = kb.get_task(conn, cid)
assert t.workspace_kind == "dir"
assert t.workspace_path == proj
def test_decompose_children_stay_scratch_when_root_scratch(kanban_home):
"""No regression: a scratch root still fans out into scratch children."""
with kb.connect() as conn:
tid = kb.create_task(
conn, title="scratch root", assignee="worker",
workspace_kind="scratch", triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[{"title": "s1"}], author="decomposer",
)
with kb.connect() as conn:
t = kb.get_task(conn, child_ids[0])
assert t.workspace_kind == "scratch"
assert t.workspace_path is None
def test_decompose_per_child_workspace_override(kanban_home):
"""An explicit per-child workspace beats inheritance."""
proj = "/home/teknium/myproject"
with kb.connect() as conn:
tid = kb.create_task(
conn, title="root", assignee="worker",
workspace_kind="dir", workspace_path=proj, triage=True,
)
child_ids = kb.decompose_triage_task(
conn, tid, root_assignee="orchestrator",
children=[
{"title": "override", "workspace_kind": "dir",
"workspace_path": "/other/repo"},
{"title": "inherit"},
],
author="decomposer",
)
with kb.connect() as conn:
over = kb.get_task(conn, child_ids[0])
inh = kb.get_task(conn, child_ids[1])
assert over.workspace_path == "/other/repo"
assert inh.workspace_path == proj