From ce64fcad48d859e4e68c091c1b32b9dbd41dc97e Mon Sep 17 00:00:00 2001 From: Personal Stack Agent Date: Sat, 6 Jun 2026 10:33:08 +0000 Subject: [PATCH] council: run task verify from the worktree root, not the host repo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fan-out verify commands run in each task's isolated worktree, but a consolidator that emitted an absolute repo-root path (e.g. `cd /workspace/services/foo && npm test`) would silently check the host tree instead — so a correct change could be reported verify-failed while a stale host tree passed. Two fixes. localize_verify rewrites any occurrence of the repo root in a verify command to the worktree it actually runs in, so even an absolute path resolves against the worker's files; run_verify applies it before executing. The consolidator prompt now states that verify runs from the worktree root and must use repo-relative paths only (never `cd /abs/...`). The kit is re-rendered (consolidator.md is bundled into the installer) and the manifest installer sha256 updated; council --self-test covers localize_verify. --- platform/agents/council/council.py | 20 +++++++ .../agents/council/prompts/consolidator.md | 12 ++++ platform/agents/kit/manifest.yaml | 56 +++++++++---------- .../src/main/resources/installer/install.sh | 32 +++++++++++ 4 files changed, 92 insertions(+), 28 deletions(-) diff --git a/platform/agents/council/council.py b/platform/agents/council/council.py index 57aed44c..04957660 100644 --- a/platform/agents/council/council.py +++ b/platform/agents/council/council.py @@ -505,9 +505,21 @@ def parallel_bounded(thunks: list[Callable[[], T]], cap: int) -> list[T]: return list(ex.map(lambda t: t(), thunks)) +def localize_verify(cmd: str, repo_root: str, cwd: str) -> str: + """Point a verify command at the worktree it runs in. The consolidator is + told to write repo-relative commands, but a stray absolute repo-root path + (e.g. `cd /workspace/services/foo`) would otherwise check the host tree, not + the worker's worktree. Rewrite the repo root to the worktree so such a + command still verifies the right files. Pure (covered by --self-test).""" + if repo_root and repo_root != cwd and repo_root in cmd: + return cmd.replace(repo_root, cwd) + return cmd + + def run_verify(cmd: str, cwd: Path) -> tuple[Optional[int], str]: if not cmd.strip(): return None, "(no verify command)" + cmd = localize_verify(cmd, str(REPO_ROOT), str(cwd)) try: proc = subprocess.run(["bash", "-lc", cmd], cwd=str(cwd), capture_output=True, text=True, env=child_env(), @@ -1237,6 +1249,14 @@ def check(name: str, cond: bool) -> None: check("_split_dest_url canonical ssh remote", _split_dest_url("o", "n") == "git@github.com:o/n.git") + # localize_verify (verify runs in the worktree, not the host repo root) + check("localize_verify rewrites repo root to the worktree", + localize_verify("cd /workspace/services/foo && npm test", + "/workspace", "/tmp/wt/T1") + == "cd /tmp/wt/T1/services/foo && npm test") + check("localize_verify leaves relative commands untouched", + localize_verify("npm test", "/workspace", "/tmp/wt/T1") == "npm test") + print(f"\n{'PASS' if not failures else 'FAIL: ' + ', '.join(failures)}") return 1 if failures else 0 diff --git a/platform/agents/council/prompts/consolidator.md b/platform/agents/council/prompts/consolidator.md index e95696b3..c3abbb90 100644 --- a/platform/agents/council/prompts/consolidator.md +++ b/platform/agents/council/prompts/consolidator.md @@ -5,23 +5,30 @@ each. Do not merely pick one and discard the other; the best ideas are often split across both. # Task brief + {{brief}} # Plan A (final) + {{plan_a}} # Plan B (final) + {{plan_b}} # Critique history (rounds 1-2, both directions) + {{history}} # Repository + Ground every task in the real codebase at {{repo_root}}. Do not invent paths. # Your job + Produce (1) a clear consolidated plan in Markdown, and (2) a task DAG for parallel execution. Each task must: + - be independently executable by a cheap worker agent given only its own fields, - touch a NON-OVERLAPPING set of files from every task it does not depend on (overlapping files across parallel tasks cause merge conflicts — partition the @@ -32,6 +39,10 @@ parallel execution. Each task must: no markdown, no parenthetical asides. Chain steps with `&&`. Right: `python3 foo.py --version && python3 -m pytest -q`. Wrong: `run the script (expect "ok") and check it passes`. + The command runs from the ROOT of the worker's isolated worktree (a fresh + checkout of this repo), so use REPO-RELATIVE paths only — never an absolute + path and never `cd /abs/...`. Right: `cd services/foo && npm test`. Wrong: + `cd /workspace/services/foo && npm test`. - be tagged with a difficulty and a worker model (haiku for trivial/moderate, sonnet for hard). @@ -42,6 +53,7 @@ should be a single task with a clear ordering, not forced into false parallelism {{baseline}} # Output + Return ONLY a JSON object — no prose, no code fences — matching this schema: {{schema}} diff --git a/platform/agents/kit/manifest.yaml b/platform/agents/kit/manifest.yaml index 074d80b6..fef9118c 100644 --- a/platform/agents/kit/manifest.yaml +++ b/platform/agents/kit/manifest.yaml @@ -1,8 +1,8 @@ version: 1 generated: partial notes: - - "This manifest pins the current repo and installer agent surfaces. Repo skills, hooks, hook settings, and the installer entrypoint are rendered from agent-kit templates; installer heredoc bodies, including hooks, skills, settings, and allowlists, are rendered from partial templates." - - "Repo skill bodies are intentionally agent-specific today; parity means every shared skill has both Claude and Codex targets unless an explicit unsupported reason is present." + - 'This manifest pins the current repo and installer agent surfaces. Repo skills, hooks, hook settings, and the installer entrypoint are rendered from agent-kit templates; installer heredoc bodies, including hooks, skills, settings, and allowlists, are rendered from partial templates.' + - 'Repo skill bodies are intentionally agent-specific today; parity means every shared skill has both Claude and Codex targets unless an explicit unsupported reason is present.' renderer: script_path: platform/agents/kit/render-agent-kit.py @@ -77,8 +77,8 @@ skills: sha256: d9c050e9c9dc1714129e545559f8f42ead5f0b6bc4cb28f4af74b3610542e596 installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/agent-session-bootstrap/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/agent-session-bootstrap/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/agent-session-bootstrap/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/agent-session-bootstrap/SKILL.md' - name: council supported_agents: [codex, claude] targets: @@ -90,8 +90,8 @@ skills: sha256: 0453a7fe2373cc46cf6e17122788ef7a99a5d8c7f2e0387ff7962265c088500a installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/council/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/council/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/council/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/council/SKILL.md' - name: fleet-change supported_agents: [codex, claude] targets: @@ -112,8 +112,8 @@ skills: sha256: 99f677b9e491be60b35dc72d6abbf87a064ce1f6db83b492af990a467f63881c installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/kb-first/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/kb-first/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/kb-first/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/kb-first/SKILL.md' - name: open-pr supported_agents: [codex, claude] targets: @@ -143,25 +143,25 @@ skills: sha256: 6823d195dbfb99e2bab3bdeed760445d1d0db78600a46405ae605c12fca4467e installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/token-economy/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/token-economy/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/token-economy/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/token-economy/SKILL.md' - name: topics supported_agents: [codex, claude] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/topics/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/topics/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/topics/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/topics/SKILL.md' - name: audit supported_agents: [codex, claude] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/skills/audit/SKILL.md" - codex_target_path: "${CODEX_HOME}/skills/audit/SKILL.md" + target_path: '${CLAUDE_HOME}/skills/audit/SKILL.md' + codex_target_path: '${CODEX_HOME}/skills/audit/SKILL.md' hooks: - name: kb-user-prompt-recall event: UserPromptSubmit - purpose: "Inject bounded knowledge.recall snippets before the agent sees the prompt." + purpose: 'Inject bounded knowledge.recall snippets before the agent sees the prompt.' supported_agents: [claude, codex] mcp_tools: [knowledge.recall] targets: @@ -175,11 +175,11 @@ hooks: sha256: 50d4579b3e774675326cc76113afebcfe5251c46ea11059cf0a567557e1a6911 installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/hooks/user-prompt-submit-recall.sh" - codex_target_path: "${CODEX_HOME}/hooks/kb-user-prompt-recall.sh" + target_path: '${CLAUDE_HOME}/hooks/user-prompt-submit-recall.sh' + codex_target_path: '${CODEX_HOME}/hooks/kb-user-prompt-recall.sh' - name: kb-stop-digest event: Stop - purpose: "Digest the transcript into capped lesson candidates and capture survivors." + purpose: 'Digest the transcript into capped lesson candidates and capture survivors.' supported_agents: [claude, codex] mcp_tools: [knowledge.digest_transcript, knowledge.recall, knowledge.capture_lesson] targets: @@ -193,12 +193,12 @@ hooks: sha256: 193522faf697ca08bc18b0b00d670195efe54d88be4dfca95324dce6910b6e01 installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/hooks/stop-session-digest.sh" - codex_target_path: "${CODEX_HOME}/hooks/kb-stop-digest.sh" + target_path: '${CLAUDE_HOME}/hooks/stop-session-digest.sh' + codex_target_path: '${CODEX_HOME}/hooks/kb-stop-digest.sh' - name: pre-tool-use-edit-recall event: PreToolUse - matcher: "Edit|Write|MultiEdit|apply_patch" - purpose: "Recall file/module lessons before edits." + matcher: 'Edit|Write|MultiEdit|apply_patch' + purpose: 'Recall file/module lessons before edits.' supported_agents: [claude, codex] targets: - agent: claude @@ -212,12 +212,12 @@ hooks: mcp_tools: [knowledge.recall] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/hooks/pre-tool-use-edit-recall.sh" - codex_target_path: "${CODEX_HOME}/hooks/pre-tool-use-edit-recall.sh" + target_path: '${CLAUDE_HOME}/hooks/pre-tool-use-edit-recall.sh' + codex_target_path: '${CODEX_HOME}/hooks/pre-tool-use-edit-recall.sh' - name: pre-tool-use-git-commit-capture event: PreToolUse matcher: Bash - purpose: "Capture non-WIP git commit messages as candidate decisions." + purpose: 'Capture non-WIP git commit messages as candidate decisions.' supported_agents: [claude, codex] targets: - agent: claude @@ -231,8 +231,8 @@ hooks: mcp_tools: [knowledge.capture_decision] installer: source_path: services/knowledge-api/src/main/resources/installer/install.sh - target_path: "${CLAUDE_HOME}/hooks/pre-tool-use-git-commit-capture.sh" - codex_target_path: "${CODEX_HOME}/hooks/pre-tool-use-git-commit-capture.sh" + target_path: '${CLAUDE_HOME}/hooks/pre-tool-use-git-commit-capture.sh' + codex_target_path: '${CODEX_HOME}/hooks/pre-tool-use-git-commit-capture.sh' settings: - agent: claude @@ -244,5 +244,5 @@ settings: installer: path: services/knowledge-api/src/main/resources/installer/install.sh - sha256: a3356d48c5b98b4f21632717b785c14cf08c7ee11106274a0bef40992d72bf77 + sha256: 68d38c29f3e755f6dae9f916379867893f4ad0d4307f71f2cadf33e6662ccd0a supported_agents: [codex, claude] diff --git a/services/knowledge-api/src/main/resources/installer/install.sh b/services/knowledge-api/src/main/resources/installer/install.sh index d7ca4c03..cb087be7 100644 --- a/services/knowledge-api/src/main/resources/installer/install.sh +++ b/services/knowledge-api/src/main/resources/installer/install.sh @@ -1082,9 +1082,21 @@ def parallel_bounded(thunks: list[Callable[[], T]], cap: int) -> list[T]: return list(ex.map(lambda t: t(), thunks)) +def localize_verify(cmd: str, repo_root: str, cwd: str) -> str: + """Point a verify command at the worktree it runs in. The consolidator is + told to write repo-relative commands, but a stray absolute repo-root path + (e.g. `cd /workspace/services/foo`) would otherwise check the host tree, not + the worker's worktree. Rewrite the repo root to the worktree so such a + command still verifies the right files. Pure (covered by --self-test).""" + if repo_root and repo_root != cwd and repo_root in cmd: + return cmd.replace(repo_root, cwd) + return cmd + + def run_verify(cmd: str, cwd: Path) -> tuple[Optional[int], str]: if not cmd.strip(): return None, "(no verify command)" + cmd = localize_verify(cmd, str(REPO_ROOT), str(cwd)) try: proc = subprocess.run(["bash", "-lc", cmd], cwd=str(cwd), capture_output=True, text=True, env=child_env(), @@ -1814,6 +1826,14 @@ def cmd_self_test(_args: argparse.Namespace) -> int: check("_split_dest_url canonical ssh remote", _split_dest_url("o", "n") == "git@github.com:o/n.git") + # localize_verify (verify runs in the worktree, not the host repo root) + check("localize_verify rewrites repo root to the worktree", + localize_verify("cd /workspace/services/foo && npm test", + "/workspace", "/tmp/wt/T1") + == "cd /tmp/wt/T1/services/foo && npm test") + check("localize_verify leaves relative commands untouched", + localize_verify("npm test", "/workspace", "/tmp/wt/T1") == "npm test") + print(f"\n{'PASS' if not failures else 'FAIL: ' + ', '.join(failures)}") return 1 if failures else 0 @@ -1970,23 +1990,30 @@ each. Do not merely pick one and discard the other; the best ideas are often split across both. # Task brief + {{brief}} # Plan A (final) + {{plan_a}} # Plan B (final) + {{plan_b}} # Critique history (rounds 1-2, both directions) + {{history}} # Repository + Ground every task in the real codebase at {{repo_root}}. Do not invent paths. # Your job + Produce (1) a clear consolidated plan in Markdown, and (2) a task DAG for parallel execution. Each task must: + - be independently executable by a cheap worker agent given only its own fields, - touch a NON-OVERLAPPING set of files from every task it does not depend on (overlapping files across parallel tasks cause merge conflicts — partition the @@ -1997,6 +2024,10 @@ parallel execution. Each task must: no markdown, no parenthetical asides. Chain steps with `&&`. Right: `python3 foo.py --version && python3 -m pytest -q`. Wrong: `run the script (expect "ok") and check it passes`. + The command runs from the ROOT of the worker's isolated worktree (a fresh + checkout of this repo), so use REPO-RELATIVE paths only — never an absolute + path and never `cd /abs/...`. Right: `cd services/foo && npm test`. Wrong: + `cd /workspace/services/foo && npm test`. - be tagged with a difficulty and a worker model (haiku for trivial/moderate, sonnet for hard). @@ -2007,6 +2038,7 @@ should be a single task with a clear ordering, not forced into false parallelism {{baseline}} # Output + Return ONLY a JSON object — no prose, no code fences — matching this schema: {{schema}}