From 2cccb2f8ba55037d0b3ddd8797b2b39251d89c0c Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:07:32 +1000 Subject: [PATCH 01/63] feat(protocols/pir): add PIR protocol + skeleton mirror MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR (Plan → Implement → Review) is a new sibling protocol for GitHub-issue-driven work that needs human review at two points: the approach (before coding) and the implementation (before a PR). Three phases: plan → implement → review. Two human gates: plan-approval, code-review. Artifacts: codev/plans/-.md and codev/reviews/-.md on the builder branch (ship to main with the merged PR). Structurally SPIR minus the specify phase, with the code-review gate firing pre-PR rather than post-PR. Consult footprint matches BUGFIX/AIR (one CMAP-2 at PR creation; human gates do the heavy review). Closes part of #691. --- .../protocols/pir/builder-prompt.md | 92 +++++++ .../pir/consult-types/impl-review.md | 78 ++++++ .../protocols/pir/consult-types/pr-review.md | 65 +++++ .../protocols/pir/prompts/implement.md | 154 ++++++++++++ codev-skeleton/protocols/pir/prompts/plan.md | 133 +++++++++++ .../protocols/pir/prompts/review.md | 226 ++++++++++++++++++ codev-skeleton/protocols/pir/protocol.json | 124 ++++++++++ codev-skeleton/protocols/pir/protocol.md | 198 +++++++++++++++ codev/protocols/pir/builder-prompt.md | 92 +++++++ .../pir/consult-types/impl-review.md | 78 ++++++ .../protocols/pir/consult-types/pr-review.md | 65 +++++ codev/protocols/pir/prompts/implement.md | 154 ++++++++++++ codev/protocols/pir/prompts/plan.md | 133 +++++++++++ codev/protocols/pir/prompts/review.md | 226 ++++++++++++++++++ codev/protocols/pir/protocol.json | 124 ++++++++++ codev/protocols/pir/protocol.md | 198 +++++++++++++++ 16 files changed, 2140 insertions(+) create mode 100644 codev-skeleton/protocols/pir/builder-prompt.md create mode 100644 codev-skeleton/protocols/pir/consult-types/impl-review.md create mode 100644 codev-skeleton/protocols/pir/consult-types/pr-review.md create mode 100644 codev-skeleton/protocols/pir/prompts/implement.md create mode 100644 codev-skeleton/protocols/pir/prompts/plan.md create mode 100644 codev-skeleton/protocols/pir/prompts/review.md create mode 100644 codev-skeleton/protocols/pir/protocol.json create mode 100644 codev-skeleton/protocols/pir/protocol.md create mode 100644 codev/protocols/pir/builder-prompt.md create mode 100644 codev/protocols/pir/consult-types/impl-review.md create mode 100644 codev/protocols/pir/consult-types/pr-review.md create mode 100644 codev/protocols/pir/prompts/implement.md create mode 100644 codev/protocols/pir/prompts/plan.md create mode 100644 codev/protocols/pir/prompts/review.md create mode 100644 codev/protocols/pir/protocol.json create mode 100644 codev/protocols/pir/protocol.md diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md new file mode 100644 index 000000000..933c94776 --- /dev/null +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -0,0 +1,92 @@ +# {{protocol_name}} Builder ({{mode}} mode) + +You are implementing a fix or feature driven by a GitHub issue, using the PIR protocol. + +{{#if mode_soft}} +## Mode: SOFT +You are running in SOFT mode. This means: +- You follow the PIR protocol document yourself (no porch orchestration) +- The architect monitors your work and verifies you're adhering to the protocol +- Run consultations manually when the protocol calls for them +- You have flexibility in execution, but must stay compliant with the protocol +{{/if}} + +{{#if mode_strict}} +## Mode: STRICT +You are running in STRICT mode. This means: +- Porch orchestrates your work +- Run: `porch next` to get your next tasks +- Follow porch signals and gate approvals +- Do not deviate from the porch-driven workflow + +### ABSOLUTE RESTRICTIONS (STRICT MODE) +- **NEVER edit `status.yaml` directly** — only porch commands may modify project state +- **NEVER call `porch approve` without explicit human approval** — only run it after the architect says to +- **NEVER skip consultations** — porch handles them via the verify step +- **NEVER advance phases manually** — porch handles phase transitions on gate approval +{{/if}} + +## Protocol +Follow the PIR protocol: `codev/protocols/pir/protocol.md` +Read and internalize the protocol before starting any work. + +PIR has three phases: +1. **plan** (gated by `plan-approval`) — write `codev/plans/pir-{{project_id}}-.md`, await human review +2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) +3. **review** — write `codev/reviews/pir-{{project_id}}-.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction + +{{#if issue}} +## Issue #{{issue.number}} +**Title**: {{issue.title}} + +**Description**: +{{issue.body}} +{{/if}} + +## Sitting at Gates + +PIR has two human gates. When you reach one: + +1. Finish your phase work and run `porch done ` +2. Run `porch next ` — you'll get a `gate_pending` response +3. End your turn with a short prose summary: what file you wrote, where it lives, how to approve +4. **Stay in the interactive session**. Do NOT exit. Wait for the user's next message. + +The reviewer can give feedback by: +- Editing the plan file (at the plan-approval gate) or the code itself (at the code-review gate) in the worktree directly — you'll see changes via `git diff` +- Typing into your PTY pane (this reaches you live) +- `afx send ""` (queued; check on next turn) +- Commenting on the GitHub issue (re-fetch with `gh issue view --comments` if asked) + +When the user provides feedback, revise the artifact, recommit, and ask if there's more to address. The gate remains pending until the user runs `porch approve` — do NOT call `porch approve` yourself. + +## Notifications +Use `afx send architect "..."` at key moments: +- **PR ready**: `afx send architect "PR # ready for review (PIR #{{issue.number}})"` +- **PR merged**: `afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup."` +- **Blocked**: `afx send architect "Blocked on PIR #{{issue.number}}: [reason]"` + +Gate-pending notifications are sent automatically by porch — you do not need to send them yourself. + +## Handling Flaky Tests + +If you encounter **pre-existing flaky tests** (intermittent failures unrelated to your changes): +1. **DO NOT** edit `status.yaml` to bypass checks +2. **DO NOT** skip porch checks or use any workaround to avoid the failure +3. **DO** mark the test as skipped with a clear annotation (e.g., `it.skip('...') // FLAKY: skipped pending investigation`) +4. **DO** document each skipped flaky test in the review file under a `## Flaky Tests` section +5. Commit the skip and continue with your work + +## Resumption After Crash + +If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch you with the same prompt. On startup: + +1. Run `porch next {{project_id}}` to learn what phase you're in +2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (code-review) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. +3. Otherwise: pick up where you left off + +## Getting Started + +1. Read the PIR protocol document (`codev/protocols/pir/protocol.md`) +2. Run `porch next {{project_id}}` to see what to do next +3. Begin work diff --git a/codev-skeleton/protocols/pir/consult-types/impl-review.md b/codev-skeleton/protocols/pir/consult-types/impl-review.md new file mode 100644 index 000000000..ed8c9bfc9 --- /dev/null +++ b/codev-skeleton/protocols/pir/consult-types/impl-review.md @@ -0,0 +1,78 @@ +# Implementation Review Prompt (PIR) + +## Context + +You are reviewing the implementation of a PIR protocol project before it reaches the `code-review` human gate. A builder has implemented the approved plan and written a code-review summary. Your job is to verify the implementation matches the plan and is ready for human review. + +## CRITICAL: Verify Before Flagging + +Before requesting changes for missing configuration, incorrect patterns, or framework issues: +1. **Check `package.json`** for actual dependency versions — framework conventions change between major versions +2. **Read the actual config files** (or confirm their deliberate absence) before flagging missing configs +3. **Do not assume** your training data reflects the version in use — verify against project files +4. If "Previous Iteration Context" is provided, read it carefully before re-raising concerns that were already disputed + +## Focus Areas + +1. **Plan Adherence** + - Does the implementation fulfill the approved plan? + - Are all "Files to Change" actually changed? + - Are the changes scoped to what the plan described, or has scope crept? + +2. **Code Quality** + - Is the code readable and maintainable? + - Are there obvious bugs? + - Are error cases handled appropriately? + - Is the change minimal — no unnecessary refactoring or unrelated tidy-ups? + +3. **Test Coverage** + - Are the tests adequate for the changes? + - Do tests cover both the main path and the edge cases the plan called out? + - For a bug fix: is there a regression test that would fail without the fix? + +4. **Review File Quality** + - Does `codev/reviews/pir--.md` exist and follow the template? + - Does it accurately describe what changed? + - Is "Things to Look At" honest about tricky spots? + - Is "How to Test Locally" specific enough that the human reviewer can act on it? + +5. **PIR-Specific Concerns** + - For UI / mobile / cross-platform changes: does the review file explain platform-specific behavior the human should verify? + - For changes with external integrations: are the integration points documented? + +## Verdict Format + +After your review, provide your verdict in exactly this format: + +``` +--- +VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT] +SUMMARY: [One-line summary of your assessment] +CONFIDENCE: [HIGH | MEDIUM | LOW] +--- +KEY_ISSUES: +- [Issue 1 or "None"] +- [Issue 2] +... +``` + +**Verdict meanings:** +- `APPROVE`: Ready for human at the `code-review` gate +- `REQUEST_CHANGES`: Issues that must be fixed before reaching the human +- `COMMENT`: Minor suggestions, can proceed but note feedback + +## Scope + +- **DO** review the implementation against the approved plan +- **DO** flag missing regression tests for bug fixes +- **DO** flag obvious bugs, code smells, security issues +- **DO NOT** redesign the approach — that was settled at `plan-approval` +- **DO NOT** demand changes outside the plan's scope +- **DO NOT** request architecture-level refactors unless the change introduces a clear new problem + +## Notes + +- This is a pre-gate review; the human is the final authority +- Focus on "is this ready for someone else to test in a browser / simulator" +- If referencing line numbers, use `file:line` format +- The builder needs actionable feedback to iterate diff --git a/codev-skeleton/protocols/pir/consult-types/pr-review.md b/codev-skeleton/protocols/pir/consult-types/pr-review.md new file mode 100644 index 000000000..790b5c3db --- /dev/null +++ b/codev-skeleton/protocols/pir/consult-types/pr-review.md @@ -0,0 +1,65 @@ +# PR Review Prompt (PIR) + +## Context + +You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `code-review` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. + +## Focus Areas + +1. **Completeness** + - Is the PR body the review file content + `Fixes #`? + - Are all commits properly formatted (`[PIR #] ...`)? + - Does the diff match what the review file describes? + +2. **Test Status** + - Do all tests pass on the branch? + - Is test coverage adequate for the change? + - Are there skipped or flaky tests documented? + +3. **Code Quality** + - Any debug code left in? + - Any TODO comments that should be resolved? + - Any `// REVIEW:` markers that weren't addressed? + +4. **Branch Hygiene** + - Is the branch up to date with main? (If not, suggest a rebase.) + - Are commits atomic and well-described? + - Is the change diff a reasonable size for the issue scope? + +5. **Issue Linkage** + - Does the PR body contain `Fixes #` (or `Refs #` for partial fixes)? + - Without this, GitHub won't auto-close the issue on merge + +## Verdict Format + +After your review, provide your verdict in exactly this format: + +``` +--- +VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT] +SUMMARY: [One-line summary of your assessment] +CONFIDENCE: [HIGH | MEDIUM | LOW] +--- +KEY_ISSUES: +- [Issue 1 or "None"] +- [Issue 2] +... +``` + +**Verdict meanings:** +- `APPROVE`: Ready to merge +- `REQUEST_CHANGES`: Issues to fix before merging +- `COMMENT`: Minor items, can merge but note feedback + +## Scope + +- **DO** flag missing `Fixes #` lines +- **DO** flag obvious problems the human reviewer at the gate might have missed +- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `code-review` +- **DO NOT** demand changes the human reviewer already accepted at the `code-review` gate (the human ran the code and approved it; you didn't) + +## Notes + +- The human at the `code-review` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases +- Focus on "what would an integration reviewer catch that the gate reviewer missed" +- If referencing line numbers, use `file:line` format diff --git a/codev-skeleton/protocols/pir/prompts/implement.md b/codev-skeleton/protocols/pir/prompts/implement.md new file mode 100644 index 000000000..c499611db --- /dev/null +++ b/codev-skeleton/protocols/pir/prompts/implement.md @@ -0,0 +1,154 @@ +# IMPLEMENT Phase Prompt + +You are executing the **IMPLEMENT** phase of the PIR protocol. + +## Your Goal + +Implement the approved plan, write tests, and pause at the `code-review` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Issue Title**: {{issue.title}} +- **Plan File**: `codev/plans/{{artifact_name}}.md` (already approved) + +## Resumption Check (do this FIRST) + +Run `porch next {{project_id}}`. If the response is `gate_pending` on `code-review`, the code is already written and you're awaiting review. In that case: + +1. Check for feedback: + - `git diff main` — has the reviewer made any direct edits to your code? + - `gh issue view {{issue.number}} --comments` + - `afx send` queue messages +2. If feedback requires code changes: make them, re-run build + tests, recommit. +3. If feedback is a discussion question: respond and stay in the session. + +Otherwise (`tasks` response — first run), continue below. + +## Process + +### 1. Re-Read the Plan + +```bash +cat codev/plans/{{artifact_name}}.md +``` + +The plan is your authoritative scope. Stick to it. If you discover the plan is wrong while implementing, stop and signal `BLOCKED` rather than silently deviating. + +### 2. Implement the Code + +Follow the plan's "Files to Change" section. Apply the changes. + +**Code quality standards:** +- Self-documenting code (clear names, obvious structure) +- No commented-out code +- No debug prints +- Explicit error handling +- Follow project style guide + +**Commit cadence:** commit each logical unit separately. Use the format: + +``` +[PIR #{{issue.number}}] +``` + +**Never use `git add .` or `git add -A`.** Stage files explicitly: + +```bash +git add path/to/changed-file.ts +git commit -m "[PIR #{{issue.number}}] ..." +``` + +### 3. Write Tests + +The plan's "Test Plan" section lists what to verify. Write the corresponding tests: + +- Unit tests for new functions +- Integration tests for cross-component flows +- Regression tests for any bug fixes + +**Test quality:** +- Test behavior, not implementation +- Avoid overmocking — only mock external dependencies (APIs, databases, file systems) +- Use real implementations for internal module boundaries + +### 4. Verify Everything Works + +```bash +npm run build # or project equivalent +npm test # or project equivalent +``` + +Both MUST pass before signaling phase complete. If a test is flaky (intermittent failure unrelated to your changes), skip it with annotation — you'll document each skipped test in the review file in the next phase. + +### 5. Push Your Branch + +```bash +git push -u origin "$(git branch --show-current)" +``` + +So the reviewer can pull / inspect from elsewhere if they want. + +### 6. Signal Phase Complete + +```bash +porch done {{project_id}} +porch next {{project_id}} +``` + +PIR's `implement` phase has no AI consult — the `code-review` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) + +### 7. End Your Turn With a Code-Review Summary (Prose, Not a File) + +When the gate goes pending, output a short prose summary in the pane to orient the human reviewer. This is **not** a committed file — it's a transient message to help them inspect the change. Structure: + +> **What changed**: 2–3 sentence summary. +> +> **Files**: `git diff --stat main` style list — paths and +/-. +> +> **Test results**: `npm run build` ✓, `npm test` ✓ (X tests, Y new). +> +> **Things to look at**: tricky spots, platform-specific behavior, anything you want the reviewer to focus on. +> +> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev {{project_id}}`. View diff via `git -C .builders/pir-{{project_id}} diff main` or VSCode → **View Diff**. +> +> Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). + +Then **stay in the interactive session**. Do not exit. Wait for the user's next message. + +(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient code-review note.) + +## Signals + +``` +PHASE_COMPLETE # Implementation + tests done; code-review gate becomes pending +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- **Don't write `codev/reviews/-.md` in this phase** — it's the next phase's artifact, with a different shape (retrospective with arch + lessons updates) +- Don't add features not in the plan — scope creep is a `BLOCKED` signal, not a free expansion +- Don't run `porch approve` yourself +- Don't push to main — only to your builder branch +- Don't squash commits — let the merge commit preserve history +- Don't use `git add .` or `git add -A` +- Don't open the PR yet — that's the `review` phase +- Don't exit the interactive session at the gate + +## Handling Feedback at the Gate + +The reviewer will run your code via `afx dev` and test it. They may: + +- Approve immediately → porch advances to the `review` phase +- Type feedback in the pane / send via `afx send` / edit code in the worktree / comment on the issue +- Ask clarifying questions about specific files or behaviors + +When they provide feedback: + +1. If it's code feedback: make the change, run build + tests, recommit +2. If it's a discussion question: answer it in the pane +3. Don't re-run `porch done` unless porch's `verify` block needs to re-validate — porch will tell you + +The gate stays pending until the human approves. diff --git a/codev-skeleton/protocols/pir/prompts/plan.md b/codev-skeleton/protocols/pir/prompts/plan.md new file mode 100644 index 000000000..0e2948013 --- /dev/null +++ b/codev-skeleton/protocols/pir/prompts/plan.md @@ -0,0 +1,133 @@ +# PLAN Phase Prompt + +You are executing the **PLAN** phase of the PIR protocol. + +## Your Goal + +Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/pir-{{project_id}}-.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Issue Title**: {{issue.title}} +- **Artifact**: `codev/plans/{{artifact_name}}.md` + +## Resumption Check (do this FIRST) + +Run `porch next {{project_id}}`. If the response is `gate_pending`, you have already drafted the plan and are awaiting review. In that case: + +1. Read your current plan file: `cat codev/plans/{{artifact_name}}.md` +2. Check for new feedback that may have arrived while you were idle: + - `git diff HEAD~1 codev/plans/{{artifact_name}}.md` — the reviewer may have edited the file directly + - `gh issue view {{issue.number}} --comments` — check for new comments + - Read any `afx send` queue messages +3. If feedback exists, revise the plan and recommit. If not, end the turn with a short "still awaiting review" message and stay in the interactive session. + +Otherwise (`tasks` response — this is your first run), continue with the steps below. + +## Process + +### 1. Read the Issue + +```bash +gh issue view {{issue.number}} +``` + +Understand what's being asked. For a bug, identify the symptom. For a feature, identify the desired outcome. + +### 2. Investigate the Codebase + +- Use Glob / Grep / Read to find the relevant code +- For a bug: trace the failure path, identify the root cause +- For a feature: find the existing patterns and integration points +- Note any existing utilities, components, or conventions you should reuse + +### 3. Write the Plan + +Create `codev/plans/pir-{{project_id}}-.md` where `` is a short kebab-case description of the change. Use this structure: + +```markdown +# PIR Plan: + +## Understanding + +What the issue is asking for, in your own words. For a bug, include the root cause you identified — back it up with file:line references. + +## Proposed Change + +The approach you intend to take. Be specific. If there are multiple valid approaches, pick one and explain why. + +## Files to Change + +Concrete file paths. Use `file:line` format for specific edits where possible. + +- `path/to/file.ts:42-55` — what changes +- `path/to/new-file.ts` — new file, what it does + +## Risks & Alternatives Considered + +- Risk: what could go wrong; mitigation +- Alternative: ; why rejected + +## Test Plan + +How to verify this works once implemented. The reviewer will use this at the `code-review` gate to test the running worktree. + +- Unit test: +- Manual: +- Cross-platform: +``` + +### 4. Commit and Push + +```bash +git add codev/plans/pir-{{project_id}}-.md +git commit -m "[PIR #{{issue.number}}] Plan draft" +git push -u origin "$(git branch --show-current)" +``` + +**Never use `git add .` or `git add -A`.** + +### 5. Signal Phase Complete + +```bash +porch done {{project_id}} +porch next {{project_id}} +``` + +`porch next` will respond with `gate_pending` on the `plan-approval` gate. Porch automatically notifies the architect. + +### 6. End Your Turn With a Prose Summary + +Output something like: + +> Plan written to `codev/plans/pir-{{project_id}}-.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). + +Then **stay in the interactive session**. Do not exit. Wait for the user's next message. + +## Signals + +``` +PHASE_COMPLETE # Plan drafted (informational; the real signal is porch done) +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- Don't write code — that's the implement phase +- Don't run `porch approve` yourself — only the human can approve the gate +- Don't post the plan content as a GitHub issue comment — the plan lives in the file, not the issue thread. A one-line pointer comment on the issue is fine if you think it helps the discussion. +- Don't use `git add .` or `git add -A` +- Don't exit the interactive session at the gate + +## Handling Feedback + +When the reviewer provides feedback (typed in pane, file-edit, `afx send`, or issue comment): + +1. Re-read the plan file (the user may have edited it) +2. Apply the requested changes to your plan +3. Recommit: `git add codev/plans/pir-{{project_id}}-.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` +4. Push +5. Output a short "Revised — see commit X" message +6. Wait for next input — the gate remains pending until the human approves diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md new file mode 100644 index 000000000..9a17826c8 --- /dev/null +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -0,0 +1,226 @@ +# REVIEW Phase Prompt + +You are executing the **REVIEW** phase of the PIR protocol. + +## Your Goal + +Write a retrospective at `codev/reviews/pir-{{project_id}}-.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. + +The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Plan File**: `codev/plans/{{artifact_name}}.md` +- **Review File**: `codev/reviews/{{artifact_name}}.md` (you will write this) + +## Prerequisites + +- The `code-review` gate has been approved (you're here because `porch next` advanced you) +- Your branch contains the implementation commits +- Build and tests pass + +## Process + +### 1. Write the Review File + +Create `codev/reviews/pir-{{project_id}}-.md` with these sections: + +```markdown +# PIR Review: + +Fixes #{{issue.number}} + +## Summary + +2–3 sentence overview of what was implemented and why. The reader is someone scanning `codev/reviews/` six months from now trying to understand what this PR did. + +## Files Changed + +Output of `git diff --stat main`, formatted as a list: + +- `path/to/file.ts` (+12 / -3) +- `path/to/new-file.ts` (+45 / -0) + +## Commits + +Output of `git log main..HEAD --oneline`: + +- `` [PIR #{{issue.number}}] First change +- `` [PIR #{{issue.number}}] Second change + +## Test Results + +- `npm run build`: ✓ pass +- `npm test`: ✓ pass (X tests, Y new) +- Manual verification: + +## Architecture Updates + +What changed in `codev/resources/arch.md` (or why no changes were needed). If this PR introduced or modified an architectural pattern, document it here AND update `arch.md` in this same commit. If no architectural updates are needed (typical for small fixes), write a single line explaining why: "No arch changes — this PR fixes a typo without affecting module boundaries." + +Use the `update-arch-docs` skill if available (`.claude/skills/update-arch-docs/SKILL.md`) — it encodes the discipline for what NOT to include in arch docs. + +## Lessons Learned Updates + +What durable engineering wisdom emerged from this PR (or why no lessons were captured). Same pattern as Architecture Updates: if something is worth recording in `codev/resources/lessons-learned.md`, update both files in this commit. If not, explain why: "No lessons captured — change was mechanical." + +## Things to Look At During PR Review + +Tricky spots the PR reviewer should focus on. Honest — if a section was hard to get right, flag it. + +## How to Test Locally + +For reviewers pulling the branch: + +- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff**, or `git -C .builders/pir-{{project_id}} diff main` +- **Run dev server**: VSCode sidebar → **Run Dev Server**, or `afx dev pir-{{project_id}}` +- **What to verify**: + +## Flaky Tests (if any) + +List any tests you skipped due to pre-existing flakiness, with file:line refs and a one-line rationale each. Omit this section if none. +``` + +### 2. Update Architecture / Lessons Docs (if applicable) + +If your "Architecture Updates" or "Lessons Learned Updates" section calls out real changes, update `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` accordingly. Use the `update-arch-docs` skill if it's available. + +If neither doc needs updating, your review file's sections still need to explain why — the porch `checks` block enforces section presence. + +### 3. Commit the Review File (and arch / lessons updates) + +```bash +git add codev/reviews/pir-{{project_id}}-.md +# Add arch.md / lessons-learned.md only if you changed them +git add codev/resources/arch.md # only if changed +git add codev/resources/lessons-learned.md # only if changed +git commit -m "[PIR #{{issue.number}}] Review + retrospective" +git push +``` + +### 4. Open the PR + +```bash +PR_TITLE="" +BRANCH="$(git branch --show-current)" + +gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "$PR_TITLE" \ + --body-file codev/reviews/pir-{{project_id}}-.md +``` + +**Verify the PR body contains `Fixes #{{issue.number}}`** (it should — the review file has it at the top). If somehow missing, edit and re-apply: + +```bash +gh pr edit --body-file codev/reviews/pir-{{project_id}}-.md +``` + +**Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. + +### 5. Run CMAP-2 Review + +Run 2-way parallel consultation on the PR (type=impl — same consult type BUGFIX and AIR use at their PR-creation phase): + +```bash +consult -m gemini --protocol pir --type impl & +consult -m codex --protocol pir --type impl & +``` + +Both should run in the background (`run_in_background: true`). **DO NOT proceed until both return verdicts.** + +Wait for each consultation. Use `TaskOutput` to retrieve results. Record each verdict (APPROVE / REQUEST_CHANGES / COMMENT). + +> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. + +### 6. Address Any REQUEST_CHANGES + +If any reviewer requested changes: + +1. Read the specific issues +2. Fix them in code +3. Run build + tests +4. Push the updates +5. Re-run CMAP only if substantial changes were made + +End with concrete verdicts from both models before continuing. + +### 7. Append CMAP Outcome to PR Body + +Once you have both verdicts, append them to the PR body: + +```markdown +## CMAP Review + +- **Gemini**: APPROVE / REQUEST_CHANGES (one-line summary) +- **Codex**: APPROVE / REQUEST_CHANGES (one-line summary) +``` + +Use `gh pr edit --body-file ` to apply. + +### 8. Notify the Architect + +```bash +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=, codex=" +``` + +This is the only notification you send. + +### 9. Wait for Merge Instruction + +The architect reviews the PR. They will either: + +- Tell you to merge → run the merge command (step 10) +- Request more changes → address them (loop back to step 5) +- Tell you to close without merging → `gh pr close ` and stop + +### 10. Merge the PR + +```bash +gh pr merge --merge +``` + +**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. + +The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. + +### 11. Final Notification + Signal Phase Complete + +```bash +afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." +porch done {{project_id}} +``` + +## Signals + +``` +PHASE_COMPLETE # PR merged, project complete +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- Don't squash-merge (`--squash`) — use `--merge` +- Don't merge without architect instruction +- Don't run `porch approve` for any gate yourself +- Don't push to main — only merge via PR +- Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) + +## Handling Problems + +**If the PR cannot be created (e.g., merge conflicts with main):** +- Rebase on main: `git fetch origin main && git rebase origin/main` +- Resolve conflicts (do NOT use destructive shortcuts) +- Force-push with lease: `git push --force-with-lease` +- Re-run `gh pr create` + +**If CMAP consults fail (e.g., model unavailable):** +- Retry once +- If still failing, notify the architect and ask whether to proceed without that model's verdict + +**If the architect doesn't respond within a reasonable window:** +- Send one follow-up via `afx send architect "..."` after a few hours +- Do not auto-merge diff --git a/codev-skeleton/protocols/pir/protocol.json b/codev-skeleton/protocols/pir/protocol.json new file mode 100644 index 000000000..fbeddc54b --- /dev/null +++ b/codev-skeleton/protocols/pir/protocol.json @@ -0,0 +1,124 @@ +{ + "$schema": "../../protocol-schema.json", + "name": "pir", + "alias": "plan-implement-review", + "version": "1.0.0", + "description": "PIR: Plan → Implement → Review for GitHub-issue-driven work with two human gates (plan-approval, code-review). Lighter than SPIR (no specify phase, no review-with-arch-updates ceremony), with a code-review gate that fires before the PR is opened.", + "input": { + "type": "github-issue", + "required": true, + "default_for": ["--issue", "-i"] + }, + "hooks": { + "pre-spawn": { + "collision-check": true, + "comment-on-issue": "On it! Working on this with the PIR protocol (plan + code-review gates before PR)." + } + }, + "phases": [ + { + "id": "plan", + "name": "Plan", + "description": "Read the issue, investigate the codebase, write codev/plans/-.md, await plan-approval", + "type": "build_verify", + "build": { + "prompt": "plan.md", + "artifact": "codev/plans/${PROJECT_TITLE}.md" + }, + "verify": null, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md" + }, + "gate": "plan-approval", + "next": "implement" + }, + { + "id": "implement", + "name": "Implement", + "description": "Write code + tests, run build/test checks, await code-review on the running worktree", + "type": "build_verify", + "build": { + "prompt": "implement.md", + "artifact": "src/**/*.{ts,tsx,js,jsx,py,go,rs}" + }, + "verify": null, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "build": { + "command": "npm run build", + "on_fail": "retry", + "max_retries": 2 + }, + "tests": { + "command": "npm test", + "on_fail": "retry", + "max_retries": 2 + } + }, + "gate": "code-review", + "next": "review" + }, + { + "id": "review", + "name": "Review", + "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, merge on instruction", + "type": "build_verify", + "build": { + "prompt": "review.md", + "artifact": "codev/reviews/${PROJECT_TITLE}.md" + }, + "verify": { + "type": "impl", + "models": ["gemini", "codex"], + "parallel": true + }, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "pr_exists": { + "command": "gh pr list --state all --head \"$(git branch --show-current)\" --json number --jq 'length' | grep -q '^[1-9]'", + "description": "PR must be created before signaling completion" + }, + "review_has_arch_updates": { + "command": "grep -q '## Architecture Updates' codev/reviews/${PROJECT_TITLE}.md", + "description": "Review must include '## Architecture Updates' section (what changed in arch.md, or why no changes needed)" + }, + "review_has_lessons_updates": { + "command": "grep -q '## Lessons Learned Updates' codev/reviews/${PROJECT_TITLE}.md", + "description": "Review must include '## Lessons Learned Updates' section (what changed in lessons-learned.md, or why no changes needed)" + } + }, + "next": null + } + ], + "signals": { + "PHASE_COMPLETE": { + "description": "Signal current build phase is complete", + "transitions_to": "next_phase" + }, + "BLOCKED": { + "description": "Signal implementation is blocked", + "requires": "reason" + } + }, + "defaults": { + "mode": "strict", + "max_iterations": 1, + "consultation": { + "enabled": true, + "parallel": true + } + } +} diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md new file mode 100644 index 000000000..d253aef08 --- /dev/null +++ b/codev-skeleton/protocols/pir/protocol.md @@ -0,0 +1,198 @@ +# PIR Protocol + +> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human code-review moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). + +## When to Use PIR + +Pick PIR when working from a GitHub Issue and ONE or BOTH of the following apply — based on the *nature* of the change, not its size: + +### 1. The approach needs review before coding starts +- Root cause is ambiguous; multiple valid fixes exist +- Area is unfamiliar or high-blast-radius (shared utilities, auth, migrations, public APIs) +- Design-sensitive (affects conventions, patterns, architecture) +- Cheaper to redirect at plan time than at PR time + +### 2. The implementation needs to be tested before a PR is created +The PR diff alone is insufficient; the reviewer must *run* the code: +- Mobile app changes (needs device testing on Android, iOS, possibly web) +- UI / UX changes (visual inspection, interaction flow, accessibility) +- Hardware-adjacent behavior (sensors, camera, permissions, notifications) +- Integration with external services that don't mock cleanly (OAuth, payments, analytics) +- User-journey changes that need a full-flow exercise +- Performance-sensitive changes that need profiling on the running app + +### Use SPIR / ASPIR / BUGFIX / AIR instead when +- **SPIR / ASPIR**: the change is complex enough to warrant careful specification, multi-agent consultation at every phase, and the full spec → plan → implement → review ceremony with file artifacts. The driving issue is incidental — what matters is that the design work deserves a formal spec and the implementation deserves consult-driven review at each phase +- **BUGFIX**: small bug fix, no design review needed, diff-on-PR review is enough +- **AIR**: small feature from an issue, autonomous, diff-on-PR review is enough + +## How PIR Differs from SPIR + +PIR is structurally *SPIR minus the `specify` phase*, with the human code-review moved earlier (pre-PR instead of post-PR). + +| Aspect | SPIR | PIR | +|---|---|---| +| Phases | specify → plan → implement → review → verify | plan → implement → review | +| Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | +| Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | +| Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review | +| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | + +The review file shape is intentionally identical to SPIR's, so `codev/reviews/` stays semantically consistent across protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. + +The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. + +## Phases + +``` +plan → implement → review +``` + +### Plan (gated by `plan-approval`) + +The builder: +1. Reads the GitHub issue and investigates the codebase +2. Writes `codev/plans/-.md` with: Understanding / Proposed change / Files to change / Risks & alternatives / Test plan +3. Commits the plan on the builder branch and pushes +4. Runs `porch done` and `porch next` — the `plan-approval` gate becomes pending +5. Sits at the interactive prompt waiting for review + +**Reviewer paths** (all equivalent): +- Open `codev/plans/-.md` in the worktree, read and / or edit directly, save +- Type feedback into the builder's PTY pane — the builder is alive in interactive mode +- `afx send ""` +- Comment on the GitHub issue (sidecar discussion) + +When satisfied, approve via VSCode's "Approve Gate" command (Cmd+K G) or: + +```bash +porch approve plan-approval --a-human-explicitly-approved-this +``` + +### Implement (gated by `code-review`) + +The builder: +1. Reads the approved plan file +2. Writes code and tests; runs build + tests via the `checks` block +3. *No AI consult on this phase* — the human at the `code-review` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". +4. Pushes the branch +5. Runs `porch done` and `porch next` — the `code-review` gate becomes pending +6. Outputs a **prose** code-review summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. +7. Sits at the interactive prompt + +**The reviewer's killer move**: run the worktree locally. + +- VSCode: right-click the builder in the Codev sidebar → **Run Dev Server** (spawns `afx dev ` via Tower) +- CLI: `afx dev ` + +The dev server uses **the same ports and URLs as main** intentionally (OAuth callbacks, CORS, cookie scoping all depend on consistent origins). Only one dev env runs at a time; stop main's `pnpm dev` before starting the worktree's, or use VSCode's **Stop Dev Server** to swap. + +Reviewer tests the change on real devices / browsers / simulators. When satisfied, approves via Cmd+K G or: + +```bash +porch approve code-review --a-human-explicitly-approved-this +``` + +### Review (no human gate — proceeds autonomously through PR creation; merge gated by architect instruction) + +The builder: +1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). Same shape as SPIR's review file. +2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). +3. Commits the review file (and arch / lessons updates if any) and pushes +4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #` +5. Porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) — same pattern as BUGFIX / AIR at PR creation. Outcome is appended to the PR body. +6. Notifies the architect: `afx send architect "PR # ready for review (PIR #)"` +7. Waits for the architect's merge instruction; merges with `gh pr merge --merge` (no squash — preserves history per project convention) + +## Gates + +PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. + +- **`plan-approval`** — same name as SPIR's plan gate. Reused safely (gates are keyed by `(project_id, gate_name)`). +- **`code-review`** — new gate name; works without any porch-side allowlist. + +When a gate becomes pending, porch automatically fires `notifyArchitect()` which sends an `afx send architect "GATE: (Builder )..."` message. The VSCode "Needs Attention" tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. + +## Rejection / Feedback Model + +There is no formal `porch reject` command. Rejection works via the feedback-iterate pattern: + +1. Reviewer provides feedback (edit the plan file in VSCode, type in the builder pane, `afx send`, or issue comment) +2. Builder reads the feedback on its next turn, revises the artifact, recommits +3. The gate remains pending — porch doesn't advance until the human runs `porch approve` + +The same pattern works at both gates. + +## Builder Session Lifetime + +The builder is a long-running interactive Claude Code session in a PTY pane managed by Tower. The session is launched as `claude ""` (no `--print`) inside a `while true` restart loop. That form starts an interactive Claude REPL with the prompt as the first user message; after Claude finishes the prompted work it sits at the input prompt awaiting next user input. The outer `while true` loop only fires if Claude crashes — it is a crash-recovery safety net, not the gate-wait mechanism. + +This means typed input in the builder pane reaches the live Claude session immediately, exactly like any other interactive Claude Code conversation. There is no "session ended at gate" state to worry about under normal operation. + +## Configuration + +PIR uses the same `.codev/config.json` configuration as other protocols. The `worktree` block (from Issue 689) enables the at-gate dev-server review flow: + +```json +{ + "worktree": { + "symlinks": [".env.local", "packages/*/.env"], + "postSpawn": ["pnpm install --frozen-lockfile"], + "devCommand": "pnpm dev" + } +} +``` + +Without `worktree.devCommand`, `afx dev` won't work and the `code-review` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. + +## Multi-Agent Consultation + +- **plan**: human-only review. No AI consultation. +- **implement**: no AI consult — the human at the `code-review` gate is the sole reviewer of the running code. +- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. + +Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `code-review`), not AI-consult density. + +To disable consultation entirely, say "without multi-agent consultation" when starting work. + +## Signals + +PIR uses the standard porch signal vocabulary: + +``` +PHASE_COMPLETE # Current phase build complete +BLOCKED:reason # Cannot proceed +``` + +Signals are informational for log readability. The state machine is driven by `porch done` and `porch next` CLI calls inside the builder turn. + +## Commit Messages + +Commits during PIR phases use the issue-driven format: + +``` +[PIR #] Plan draft +[PIR #] Implement avatar masking +[PIR #] Add Android-side regression test +``` + +The PR title follows the project's existing PR convention. + +## Branch Naming + +``` +builder/pir- +``` + +Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. + +## File Locations + +``` +codev/plans/pir--.md # written in plan phase, on builder branch +codev/reviews/pir--.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/projects/pir-/status.yaml # porch state, managed automatically +``` + +The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md new file mode 100644 index 000000000..933c94776 --- /dev/null +++ b/codev/protocols/pir/builder-prompt.md @@ -0,0 +1,92 @@ +# {{protocol_name}} Builder ({{mode}} mode) + +You are implementing a fix or feature driven by a GitHub issue, using the PIR protocol. + +{{#if mode_soft}} +## Mode: SOFT +You are running in SOFT mode. This means: +- You follow the PIR protocol document yourself (no porch orchestration) +- The architect monitors your work and verifies you're adhering to the protocol +- Run consultations manually when the protocol calls for them +- You have flexibility in execution, but must stay compliant with the protocol +{{/if}} + +{{#if mode_strict}} +## Mode: STRICT +You are running in STRICT mode. This means: +- Porch orchestrates your work +- Run: `porch next` to get your next tasks +- Follow porch signals and gate approvals +- Do not deviate from the porch-driven workflow + +### ABSOLUTE RESTRICTIONS (STRICT MODE) +- **NEVER edit `status.yaml` directly** — only porch commands may modify project state +- **NEVER call `porch approve` without explicit human approval** — only run it after the architect says to +- **NEVER skip consultations** — porch handles them via the verify step +- **NEVER advance phases manually** — porch handles phase transitions on gate approval +{{/if}} + +## Protocol +Follow the PIR protocol: `codev/protocols/pir/protocol.md` +Read and internalize the protocol before starting any work. + +PIR has three phases: +1. **plan** (gated by `plan-approval`) — write `codev/plans/pir-{{project_id}}-.md`, await human review +2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) +3. **review** — write `codev/reviews/pir-{{project_id}}-.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction + +{{#if issue}} +## Issue #{{issue.number}} +**Title**: {{issue.title}} + +**Description**: +{{issue.body}} +{{/if}} + +## Sitting at Gates + +PIR has two human gates. When you reach one: + +1. Finish your phase work and run `porch done ` +2. Run `porch next ` — you'll get a `gate_pending` response +3. End your turn with a short prose summary: what file you wrote, where it lives, how to approve +4. **Stay in the interactive session**. Do NOT exit. Wait for the user's next message. + +The reviewer can give feedback by: +- Editing the plan file (at the plan-approval gate) or the code itself (at the code-review gate) in the worktree directly — you'll see changes via `git diff` +- Typing into your PTY pane (this reaches you live) +- `afx send ""` (queued; check on next turn) +- Commenting on the GitHub issue (re-fetch with `gh issue view --comments` if asked) + +When the user provides feedback, revise the artifact, recommit, and ask if there's more to address. The gate remains pending until the user runs `porch approve` — do NOT call `porch approve` yourself. + +## Notifications +Use `afx send architect "..."` at key moments: +- **PR ready**: `afx send architect "PR # ready for review (PIR #{{issue.number}})"` +- **PR merged**: `afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup."` +- **Blocked**: `afx send architect "Blocked on PIR #{{issue.number}}: [reason]"` + +Gate-pending notifications are sent automatically by porch — you do not need to send them yourself. + +## Handling Flaky Tests + +If you encounter **pre-existing flaky tests** (intermittent failures unrelated to your changes): +1. **DO NOT** edit `status.yaml` to bypass checks +2. **DO NOT** skip porch checks or use any workaround to avoid the failure +3. **DO** mark the test as skipped with a clear annotation (e.g., `it.skip('...') // FLAKY: skipped pending investigation`) +4. **DO** document each skipped flaky test in the review file under a `## Flaky Tests` section +5. Commit the skip and continue with your work + +## Resumption After Crash + +If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch you with the same prompt. On startup: + +1. Run `porch next {{project_id}}` to learn what phase you're in +2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (code-review) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. +3. Otherwise: pick up where you left off + +## Getting Started + +1. Read the PIR protocol document (`codev/protocols/pir/protocol.md`) +2. Run `porch next {{project_id}}` to see what to do next +3. Begin work diff --git a/codev/protocols/pir/consult-types/impl-review.md b/codev/protocols/pir/consult-types/impl-review.md new file mode 100644 index 000000000..ed8c9bfc9 --- /dev/null +++ b/codev/protocols/pir/consult-types/impl-review.md @@ -0,0 +1,78 @@ +# Implementation Review Prompt (PIR) + +## Context + +You are reviewing the implementation of a PIR protocol project before it reaches the `code-review` human gate. A builder has implemented the approved plan and written a code-review summary. Your job is to verify the implementation matches the plan and is ready for human review. + +## CRITICAL: Verify Before Flagging + +Before requesting changes for missing configuration, incorrect patterns, or framework issues: +1. **Check `package.json`** for actual dependency versions — framework conventions change between major versions +2. **Read the actual config files** (or confirm their deliberate absence) before flagging missing configs +3. **Do not assume** your training data reflects the version in use — verify against project files +4. If "Previous Iteration Context" is provided, read it carefully before re-raising concerns that were already disputed + +## Focus Areas + +1. **Plan Adherence** + - Does the implementation fulfill the approved plan? + - Are all "Files to Change" actually changed? + - Are the changes scoped to what the plan described, or has scope crept? + +2. **Code Quality** + - Is the code readable and maintainable? + - Are there obvious bugs? + - Are error cases handled appropriately? + - Is the change minimal — no unnecessary refactoring or unrelated tidy-ups? + +3. **Test Coverage** + - Are the tests adequate for the changes? + - Do tests cover both the main path and the edge cases the plan called out? + - For a bug fix: is there a regression test that would fail without the fix? + +4. **Review File Quality** + - Does `codev/reviews/pir--.md` exist and follow the template? + - Does it accurately describe what changed? + - Is "Things to Look At" honest about tricky spots? + - Is "How to Test Locally" specific enough that the human reviewer can act on it? + +5. **PIR-Specific Concerns** + - For UI / mobile / cross-platform changes: does the review file explain platform-specific behavior the human should verify? + - For changes with external integrations: are the integration points documented? + +## Verdict Format + +After your review, provide your verdict in exactly this format: + +``` +--- +VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT] +SUMMARY: [One-line summary of your assessment] +CONFIDENCE: [HIGH | MEDIUM | LOW] +--- +KEY_ISSUES: +- [Issue 1 or "None"] +- [Issue 2] +... +``` + +**Verdict meanings:** +- `APPROVE`: Ready for human at the `code-review` gate +- `REQUEST_CHANGES`: Issues that must be fixed before reaching the human +- `COMMENT`: Minor suggestions, can proceed but note feedback + +## Scope + +- **DO** review the implementation against the approved plan +- **DO** flag missing regression tests for bug fixes +- **DO** flag obvious bugs, code smells, security issues +- **DO NOT** redesign the approach — that was settled at `plan-approval` +- **DO NOT** demand changes outside the plan's scope +- **DO NOT** request architecture-level refactors unless the change introduces a clear new problem + +## Notes + +- This is a pre-gate review; the human is the final authority +- Focus on "is this ready for someone else to test in a browser / simulator" +- If referencing line numbers, use `file:line` format +- The builder needs actionable feedback to iterate diff --git a/codev/protocols/pir/consult-types/pr-review.md b/codev/protocols/pir/consult-types/pr-review.md new file mode 100644 index 000000000..790b5c3db --- /dev/null +++ b/codev/protocols/pir/consult-types/pr-review.md @@ -0,0 +1,65 @@ +# PR Review Prompt (PIR) + +## Context + +You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `code-review` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. + +## Focus Areas + +1. **Completeness** + - Is the PR body the review file content + `Fixes #`? + - Are all commits properly formatted (`[PIR #] ...`)? + - Does the diff match what the review file describes? + +2. **Test Status** + - Do all tests pass on the branch? + - Is test coverage adequate for the change? + - Are there skipped or flaky tests documented? + +3. **Code Quality** + - Any debug code left in? + - Any TODO comments that should be resolved? + - Any `// REVIEW:` markers that weren't addressed? + +4. **Branch Hygiene** + - Is the branch up to date with main? (If not, suggest a rebase.) + - Are commits atomic and well-described? + - Is the change diff a reasonable size for the issue scope? + +5. **Issue Linkage** + - Does the PR body contain `Fixes #` (or `Refs #` for partial fixes)? + - Without this, GitHub won't auto-close the issue on merge + +## Verdict Format + +After your review, provide your verdict in exactly this format: + +``` +--- +VERDICT: [APPROVE | REQUEST_CHANGES | COMMENT] +SUMMARY: [One-line summary of your assessment] +CONFIDENCE: [HIGH | MEDIUM | LOW] +--- +KEY_ISSUES: +- [Issue 1 or "None"] +- [Issue 2] +... +``` + +**Verdict meanings:** +- `APPROVE`: Ready to merge +- `REQUEST_CHANGES`: Issues to fix before merging +- `COMMENT`: Minor items, can merge but note feedback + +## Scope + +- **DO** flag missing `Fixes #` lines +- **DO** flag obvious problems the human reviewer at the gate might have missed +- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `code-review` +- **DO NOT** demand changes the human reviewer already accepted at the `code-review` gate (the human ran the code and approved it; you didn't) + +## Notes + +- The human at the `code-review` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases +- Focus on "what would an integration reviewer catch that the gate reviewer missed" +- If referencing line numbers, use `file:line` format diff --git a/codev/protocols/pir/prompts/implement.md b/codev/protocols/pir/prompts/implement.md new file mode 100644 index 000000000..c499611db --- /dev/null +++ b/codev/protocols/pir/prompts/implement.md @@ -0,0 +1,154 @@ +# IMPLEMENT Phase Prompt + +You are executing the **IMPLEMENT** phase of the PIR protocol. + +## Your Goal + +Implement the approved plan, write tests, and pause at the `code-review` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Issue Title**: {{issue.title}} +- **Plan File**: `codev/plans/{{artifact_name}}.md` (already approved) + +## Resumption Check (do this FIRST) + +Run `porch next {{project_id}}`. If the response is `gate_pending` on `code-review`, the code is already written and you're awaiting review. In that case: + +1. Check for feedback: + - `git diff main` — has the reviewer made any direct edits to your code? + - `gh issue view {{issue.number}} --comments` + - `afx send` queue messages +2. If feedback requires code changes: make them, re-run build + tests, recommit. +3. If feedback is a discussion question: respond and stay in the session. + +Otherwise (`tasks` response — first run), continue below. + +## Process + +### 1. Re-Read the Plan + +```bash +cat codev/plans/{{artifact_name}}.md +``` + +The plan is your authoritative scope. Stick to it. If you discover the plan is wrong while implementing, stop and signal `BLOCKED` rather than silently deviating. + +### 2. Implement the Code + +Follow the plan's "Files to Change" section. Apply the changes. + +**Code quality standards:** +- Self-documenting code (clear names, obvious structure) +- No commented-out code +- No debug prints +- Explicit error handling +- Follow project style guide + +**Commit cadence:** commit each logical unit separately. Use the format: + +``` +[PIR #{{issue.number}}] +``` + +**Never use `git add .` or `git add -A`.** Stage files explicitly: + +```bash +git add path/to/changed-file.ts +git commit -m "[PIR #{{issue.number}}] ..." +``` + +### 3. Write Tests + +The plan's "Test Plan" section lists what to verify. Write the corresponding tests: + +- Unit tests for new functions +- Integration tests for cross-component flows +- Regression tests for any bug fixes + +**Test quality:** +- Test behavior, not implementation +- Avoid overmocking — only mock external dependencies (APIs, databases, file systems) +- Use real implementations for internal module boundaries + +### 4. Verify Everything Works + +```bash +npm run build # or project equivalent +npm test # or project equivalent +``` + +Both MUST pass before signaling phase complete. If a test is flaky (intermittent failure unrelated to your changes), skip it with annotation — you'll document each skipped test in the review file in the next phase. + +### 5. Push Your Branch + +```bash +git push -u origin "$(git branch --show-current)" +``` + +So the reviewer can pull / inspect from elsewhere if they want. + +### 6. Signal Phase Complete + +```bash +porch done {{project_id}} +porch next {{project_id}} +``` + +PIR's `implement` phase has no AI consult — the `code-review` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) + +### 7. End Your Turn With a Code-Review Summary (Prose, Not a File) + +When the gate goes pending, output a short prose summary in the pane to orient the human reviewer. This is **not** a committed file — it's a transient message to help them inspect the change. Structure: + +> **What changed**: 2–3 sentence summary. +> +> **Files**: `git diff --stat main` style list — paths and +/-. +> +> **Test results**: `npm run build` ✓, `npm test` ✓ (X tests, Y new). +> +> **Things to look at**: tricky spots, platform-specific behavior, anything you want the reviewer to focus on. +> +> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev {{project_id}}`. View diff via `git -C .builders/pir-{{project_id}} diff main` or VSCode → **View Diff**. +> +> Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). + +Then **stay in the interactive session**. Do not exit. Wait for the user's next message. + +(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient code-review note.) + +## Signals + +``` +PHASE_COMPLETE # Implementation + tests done; code-review gate becomes pending +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- **Don't write `codev/reviews/-.md` in this phase** — it's the next phase's artifact, with a different shape (retrospective with arch + lessons updates) +- Don't add features not in the plan — scope creep is a `BLOCKED` signal, not a free expansion +- Don't run `porch approve` yourself +- Don't push to main — only to your builder branch +- Don't squash commits — let the merge commit preserve history +- Don't use `git add .` or `git add -A` +- Don't open the PR yet — that's the `review` phase +- Don't exit the interactive session at the gate + +## Handling Feedback at the Gate + +The reviewer will run your code via `afx dev` and test it. They may: + +- Approve immediately → porch advances to the `review` phase +- Type feedback in the pane / send via `afx send` / edit code in the worktree / comment on the issue +- Ask clarifying questions about specific files or behaviors + +When they provide feedback: + +1. If it's code feedback: make the change, run build + tests, recommit +2. If it's a discussion question: answer it in the pane +3. Don't re-run `porch done` unless porch's `verify` block needs to re-validate — porch will tell you + +The gate stays pending until the human approves. diff --git a/codev/protocols/pir/prompts/plan.md b/codev/protocols/pir/prompts/plan.md new file mode 100644 index 000000000..0e2948013 --- /dev/null +++ b/codev/protocols/pir/prompts/plan.md @@ -0,0 +1,133 @@ +# PLAN Phase Prompt + +You are executing the **PLAN** phase of the PIR protocol. + +## Your Goal + +Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/pir-{{project_id}}-.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Issue Title**: {{issue.title}} +- **Artifact**: `codev/plans/{{artifact_name}}.md` + +## Resumption Check (do this FIRST) + +Run `porch next {{project_id}}`. If the response is `gate_pending`, you have already drafted the plan and are awaiting review. In that case: + +1. Read your current plan file: `cat codev/plans/{{artifact_name}}.md` +2. Check for new feedback that may have arrived while you were idle: + - `git diff HEAD~1 codev/plans/{{artifact_name}}.md` — the reviewer may have edited the file directly + - `gh issue view {{issue.number}} --comments` — check for new comments + - Read any `afx send` queue messages +3. If feedback exists, revise the plan and recommit. If not, end the turn with a short "still awaiting review" message and stay in the interactive session. + +Otherwise (`tasks` response — this is your first run), continue with the steps below. + +## Process + +### 1. Read the Issue + +```bash +gh issue view {{issue.number}} +``` + +Understand what's being asked. For a bug, identify the symptom. For a feature, identify the desired outcome. + +### 2. Investigate the Codebase + +- Use Glob / Grep / Read to find the relevant code +- For a bug: trace the failure path, identify the root cause +- For a feature: find the existing patterns and integration points +- Note any existing utilities, components, or conventions you should reuse + +### 3. Write the Plan + +Create `codev/plans/pir-{{project_id}}-.md` where `` is a short kebab-case description of the change. Use this structure: + +```markdown +# PIR Plan: + +## Understanding + +What the issue is asking for, in your own words. For a bug, include the root cause you identified — back it up with file:line references. + +## Proposed Change + +The approach you intend to take. Be specific. If there are multiple valid approaches, pick one and explain why. + +## Files to Change + +Concrete file paths. Use `file:line` format for specific edits where possible. + +- `path/to/file.ts:42-55` — what changes +- `path/to/new-file.ts` — new file, what it does + +## Risks & Alternatives Considered + +- Risk: what could go wrong; mitigation +- Alternative: ; why rejected + +## Test Plan + +How to verify this works once implemented. The reviewer will use this at the `code-review` gate to test the running worktree. + +- Unit test: +- Manual: +- Cross-platform: +``` + +### 4. Commit and Push + +```bash +git add codev/plans/pir-{{project_id}}-.md +git commit -m "[PIR #{{issue.number}}] Plan draft" +git push -u origin "$(git branch --show-current)" +``` + +**Never use `git add .` or `git add -A`.** + +### 5. Signal Phase Complete + +```bash +porch done {{project_id}} +porch next {{project_id}} +``` + +`porch next` will respond with `gate_pending` on the `plan-approval` gate. Porch automatically notifies the architect. + +### 6. End Your Turn With a Prose Summary + +Output something like: + +> Plan written to `codev/plans/pir-{{project_id}}-.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). + +Then **stay in the interactive session**. Do not exit. Wait for the user's next message. + +## Signals + +``` +PHASE_COMPLETE # Plan drafted (informational; the real signal is porch done) +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- Don't write code — that's the implement phase +- Don't run `porch approve` yourself — only the human can approve the gate +- Don't post the plan content as a GitHub issue comment — the plan lives in the file, not the issue thread. A one-line pointer comment on the issue is fine if you think it helps the discussion. +- Don't use `git add .` or `git add -A` +- Don't exit the interactive session at the gate + +## Handling Feedback + +When the reviewer provides feedback (typed in pane, file-edit, `afx send`, or issue comment): + +1. Re-read the plan file (the user may have edited it) +2. Apply the requested changes to your plan +3. Recommit: `git add codev/plans/pir-{{project_id}}-.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` +4. Push +5. Output a short "Revised — see commit X" message +6. Wait for next input — the gate remains pending until the human approves diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md new file mode 100644 index 000000000..9a17826c8 --- /dev/null +++ b/codev/protocols/pir/prompts/review.md @@ -0,0 +1,226 @@ +# REVIEW Phase Prompt + +You are executing the **REVIEW** phase of the PIR protocol. + +## Your Goal + +Write a retrospective at `codev/reviews/pir-{{project_id}}-.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. + +The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. + +## Context + +- **Project ID**: {{project_id}} +- **Issue Number**: #{{issue.number}} +- **Plan File**: `codev/plans/{{artifact_name}}.md` +- **Review File**: `codev/reviews/{{artifact_name}}.md` (you will write this) + +## Prerequisites + +- The `code-review` gate has been approved (you're here because `porch next` advanced you) +- Your branch contains the implementation commits +- Build and tests pass + +## Process + +### 1. Write the Review File + +Create `codev/reviews/pir-{{project_id}}-.md` with these sections: + +```markdown +# PIR Review: + +Fixes #{{issue.number}} + +## Summary + +2–3 sentence overview of what was implemented and why. The reader is someone scanning `codev/reviews/` six months from now trying to understand what this PR did. + +## Files Changed + +Output of `git diff --stat main`, formatted as a list: + +- `path/to/file.ts` (+12 / -3) +- `path/to/new-file.ts` (+45 / -0) + +## Commits + +Output of `git log main..HEAD --oneline`: + +- `` [PIR #{{issue.number}}] First change +- `` [PIR #{{issue.number}}] Second change + +## Test Results + +- `npm run build`: ✓ pass +- `npm test`: ✓ pass (X tests, Y new) +- Manual verification: + +## Architecture Updates + +What changed in `codev/resources/arch.md` (or why no changes were needed). If this PR introduced or modified an architectural pattern, document it here AND update `arch.md` in this same commit. If no architectural updates are needed (typical for small fixes), write a single line explaining why: "No arch changes — this PR fixes a typo without affecting module boundaries." + +Use the `update-arch-docs` skill if available (`.claude/skills/update-arch-docs/SKILL.md`) — it encodes the discipline for what NOT to include in arch docs. + +## Lessons Learned Updates + +What durable engineering wisdom emerged from this PR (or why no lessons were captured). Same pattern as Architecture Updates: if something is worth recording in `codev/resources/lessons-learned.md`, update both files in this commit. If not, explain why: "No lessons captured — change was mechanical." + +## Things to Look At During PR Review + +Tricky spots the PR reviewer should focus on. Honest — if a section was hard to get right, flag it. + +## How to Test Locally + +For reviewers pulling the branch: + +- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff**, or `git -C .builders/pir-{{project_id}} diff main` +- **Run dev server**: VSCode sidebar → **Run Dev Server**, or `afx dev pir-{{project_id}}` +- **What to verify**: + +## Flaky Tests (if any) + +List any tests you skipped due to pre-existing flakiness, with file:line refs and a one-line rationale each. Omit this section if none. +``` + +### 2. Update Architecture / Lessons Docs (if applicable) + +If your "Architecture Updates" or "Lessons Learned Updates" section calls out real changes, update `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` accordingly. Use the `update-arch-docs` skill if it's available. + +If neither doc needs updating, your review file's sections still need to explain why — the porch `checks` block enforces section presence. + +### 3. Commit the Review File (and arch / lessons updates) + +```bash +git add codev/reviews/pir-{{project_id}}-.md +# Add arch.md / lessons-learned.md only if you changed them +git add codev/resources/arch.md # only if changed +git add codev/resources/lessons-learned.md # only if changed +git commit -m "[PIR #{{issue.number}}] Review + retrospective" +git push +``` + +### 4. Open the PR + +```bash +PR_TITLE="" +BRANCH="$(git branch --show-current)" + +gh pr create \ + --base main \ + --head "$BRANCH" \ + --title "$PR_TITLE" \ + --body-file codev/reviews/pir-{{project_id}}-.md +``` + +**Verify the PR body contains `Fixes #{{issue.number}}`** (it should — the review file has it at the top). If somehow missing, edit and re-apply: + +```bash +gh pr edit --body-file codev/reviews/pir-{{project_id}}-.md +``` + +**Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. + +### 5. Run CMAP-2 Review + +Run 2-way parallel consultation on the PR (type=impl — same consult type BUGFIX and AIR use at their PR-creation phase): + +```bash +consult -m gemini --protocol pir --type impl & +consult -m codex --protocol pir --type impl & +``` + +Both should run in the background (`run_in_background: true`). **DO NOT proceed until both return verdicts.** + +Wait for each consultation. Use `TaskOutput` to retrieve results. Record each verdict (APPROVE / REQUEST_CHANGES / COMMENT). + +> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. + +### 6. Address Any REQUEST_CHANGES + +If any reviewer requested changes: + +1. Read the specific issues +2. Fix them in code +3. Run build + tests +4. Push the updates +5. Re-run CMAP only if substantial changes were made + +End with concrete verdicts from both models before continuing. + +### 7. Append CMAP Outcome to PR Body + +Once you have both verdicts, append them to the PR body: + +```markdown +## CMAP Review + +- **Gemini**: APPROVE / REQUEST_CHANGES (one-line summary) +- **Codex**: APPROVE / REQUEST_CHANGES (one-line summary) +``` + +Use `gh pr edit --body-file ` to apply. + +### 8. Notify the Architect + +```bash +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=, codex=" +``` + +This is the only notification you send. + +### 9. Wait for Merge Instruction + +The architect reviews the PR. They will either: + +- Tell you to merge → run the merge command (step 10) +- Request more changes → address them (loop back to step 5) +- Tell you to close without merging → `gh pr close ` and stop + +### 10. Merge the PR + +```bash +gh pr merge --merge +``` + +**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. + +The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. + +### 11. Final Notification + Signal Phase Complete + +```bash +afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." +porch done {{project_id}} +``` + +## Signals + +``` +PHASE_COMPLETE # PR merged, project complete +BLOCKED:reason # Cannot proceed +``` + +## What NOT to Do + +- Don't squash-merge (`--squash`) — use `--merge` +- Don't merge without architect instruction +- Don't run `porch approve` for any gate yourself +- Don't push to main — only merge via PR +- Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) + +## Handling Problems + +**If the PR cannot be created (e.g., merge conflicts with main):** +- Rebase on main: `git fetch origin main && git rebase origin/main` +- Resolve conflicts (do NOT use destructive shortcuts) +- Force-push with lease: `git push --force-with-lease` +- Re-run `gh pr create` + +**If CMAP consults fail (e.g., model unavailable):** +- Retry once +- If still failing, notify the architect and ask whether to proceed without that model's verdict + +**If the architect doesn't respond within a reasonable window:** +- Send one follow-up via `afx send architect "..."` after a few hours +- Do not auto-merge diff --git a/codev/protocols/pir/protocol.json b/codev/protocols/pir/protocol.json new file mode 100644 index 000000000..fbeddc54b --- /dev/null +++ b/codev/protocols/pir/protocol.json @@ -0,0 +1,124 @@ +{ + "$schema": "../../protocol-schema.json", + "name": "pir", + "alias": "plan-implement-review", + "version": "1.0.0", + "description": "PIR: Plan → Implement → Review for GitHub-issue-driven work with two human gates (plan-approval, code-review). Lighter than SPIR (no specify phase, no review-with-arch-updates ceremony), with a code-review gate that fires before the PR is opened.", + "input": { + "type": "github-issue", + "required": true, + "default_for": ["--issue", "-i"] + }, + "hooks": { + "pre-spawn": { + "collision-check": true, + "comment-on-issue": "On it! Working on this with the PIR protocol (plan + code-review gates before PR)." + } + }, + "phases": [ + { + "id": "plan", + "name": "Plan", + "description": "Read the issue, investigate the codebase, write codev/plans/-.md, await plan-approval", + "type": "build_verify", + "build": { + "prompt": "plan.md", + "artifact": "codev/plans/${PROJECT_TITLE}.md" + }, + "verify": null, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "plan_exists": "test -f codev/plans/${PROJECT_TITLE}.md" + }, + "gate": "plan-approval", + "next": "implement" + }, + { + "id": "implement", + "name": "Implement", + "description": "Write code + tests, run build/test checks, await code-review on the running worktree", + "type": "build_verify", + "build": { + "prompt": "implement.md", + "artifact": "src/**/*.{ts,tsx,js,jsx,py,go,rs}" + }, + "verify": null, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "build": { + "command": "npm run build", + "on_fail": "retry", + "max_retries": 2 + }, + "tests": { + "command": "npm test", + "on_fail": "retry", + "max_retries": 2 + } + }, + "gate": "code-review", + "next": "review" + }, + { + "id": "review", + "name": "Review", + "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, merge on instruction", + "type": "build_verify", + "build": { + "prompt": "review.md", + "artifact": "codev/reviews/${PROJECT_TITLE}.md" + }, + "verify": { + "type": "impl", + "models": ["gemini", "codex"], + "parallel": true + }, + "max_iterations": 1, + "on_complete": { + "commit": true, + "push": true + }, + "checks": { + "pr_exists": { + "command": "gh pr list --state all --head \"$(git branch --show-current)\" --json number --jq 'length' | grep -q '^[1-9]'", + "description": "PR must be created before signaling completion" + }, + "review_has_arch_updates": { + "command": "grep -q '## Architecture Updates' codev/reviews/${PROJECT_TITLE}.md", + "description": "Review must include '## Architecture Updates' section (what changed in arch.md, or why no changes needed)" + }, + "review_has_lessons_updates": { + "command": "grep -q '## Lessons Learned Updates' codev/reviews/${PROJECT_TITLE}.md", + "description": "Review must include '## Lessons Learned Updates' section (what changed in lessons-learned.md, or why no changes needed)" + } + }, + "next": null + } + ], + "signals": { + "PHASE_COMPLETE": { + "description": "Signal current build phase is complete", + "transitions_to": "next_phase" + }, + "BLOCKED": { + "description": "Signal implementation is blocked", + "requires": "reason" + } + }, + "defaults": { + "mode": "strict", + "max_iterations": 1, + "consultation": { + "enabled": true, + "parallel": true + } + } +} diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md new file mode 100644 index 000000000..d253aef08 --- /dev/null +++ b/codev/protocols/pir/protocol.md @@ -0,0 +1,198 @@ +# PIR Protocol + +> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human code-review moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). + +## When to Use PIR + +Pick PIR when working from a GitHub Issue and ONE or BOTH of the following apply — based on the *nature* of the change, not its size: + +### 1. The approach needs review before coding starts +- Root cause is ambiguous; multiple valid fixes exist +- Area is unfamiliar or high-blast-radius (shared utilities, auth, migrations, public APIs) +- Design-sensitive (affects conventions, patterns, architecture) +- Cheaper to redirect at plan time than at PR time + +### 2. The implementation needs to be tested before a PR is created +The PR diff alone is insufficient; the reviewer must *run* the code: +- Mobile app changes (needs device testing on Android, iOS, possibly web) +- UI / UX changes (visual inspection, interaction flow, accessibility) +- Hardware-adjacent behavior (sensors, camera, permissions, notifications) +- Integration with external services that don't mock cleanly (OAuth, payments, analytics) +- User-journey changes that need a full-flow exercise +- Performance-sensitive changes that need profiling on the running app + +### Use SPIR / ASPIR / BUGFIX / AIR instead when +- **SPIR / ASPIR**: the change is complex enough to warrant careful specification, multi-agent consultation at every phase, and the full spec → plan → implement → review ceremony with file artifacts. The driving issue is incidental — what matters is that the design work deserves a formal spec and the implementation deserves consult-driven review at each phase +- **BUGFIX**: small bug fix, no design review needed, diff-on-PR review is enough +- **AIR**: small feature from an issue, autonomous, diff-on-PR review is enough + +## How PIR Differs from SPIR + +PIR is structurally *SPIR minus the `specify` phase*, with the human code-review moved earlier (pre-PR instead of post-PR). + +| Aspect | SPIR | PIR | +|---|---|---| +| Phases | specify → plan → implement → review → verify | plan → implement → review | +| Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | +| Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | +| Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review | +| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | + +The review file shape is intentionally identical to SPIR's, so `codev/reviews/` stays semantically consistent across protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. + +The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. + +## Phases + +``` +plan → implement → review +``` + +### Plan (gated by `plan-approval`) + +The builder: +1. Reads the GitHub issue and investigates the codebase +2. Writes `codev/plans/-.md` with: Understanding / Proposed change / Files to change / Risks & alternatives / Test plan +3. Commits the plan on the builder branch and pushes +4. Runs `porch done` and `porch next` — the `plan-approval` gate becomes pending +5. Sits at the interactive prompt waiting for review + +**Reviewer paths** (all equivalent): +- Open `codev/plans/-.md` in the worktree, read and / or edit directly, save +- Type feedback into the builder's PTY pane — the builder is alive in interactive mode +- `afx send ""` +- Comment on the GitHub issue (sidecar discussion) + +When satisfied, approve via VSCode's "Approve Gate" command (Cmd+K G) or: + +```bash +porch approve plan-approval --a-human-explicitly-approved-this +``` + +### Implement (gated by `code-review`) + +The builder: +1. Reads the approved plan file +2. Writes code and tests; runs build + tests via the `checks` block +3. *No AI consult on this phase* — the human at the `code-review` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". +4. Pushes the branch +5. Runs `porch done` and `porch next` — the `code-review` gate becomes pending +6. Outputs a **prose** code-review summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. +7. Sits at the interactive prompt + +**The reviewer's killer move**: run the worktree locally. + +- VSCode: right-click the builder in the Codev sidebar → **Run Dev Server** (spawns `afx dev ` via Tower) +- CLI: `afx dev ` + +The dev server uses **the same ports and URLs as main** intentionally (OAuth callbacks, CORS, cookie scoping all depend on consistent origins). Only one dev env runs at a time; stop main's `pnpm dev` before starting the worktree's, or use VSCode's **Stop Dev Server** to swap. + +Reviewer tests the change on real devices / browsers / simulators. When satisfied, approves via Cmd+K G or: + +```bash +porch approve code-review --a-human-explicitly-approved-this +``` + +### Review (no human gate — proceeds autonomously through PR creation; merge gated by architect instruction) + +The builder: +1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). Same shape as SPIR's review file. +2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). +3. Commits the review file (and arch / lessons updates if any) and pushes +4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #` +5. Porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) — same pattern as BUGFIX / AIR at PR creation. Outcome is appended to the PR body. +6. Notifies the architect: `afx send architect "PR # ready for review (PIR #)"` +7. Waits for the architect's merge instruction; merges with `gh pr merge --merge` (no squash — preserves history per project convention) + +## Gates + +PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. + +- **`plan-approval`** — same name as SPIR's plan gate. Reused safely (gates are keyed by `(project_id, gate_name)`). +- **`code-review`** — new gate name; works without any porch-side allowlist. + +When a gate becomes pending, porch automatically fires `notifyArchitect()` which sends an `afx send architect "GATE: (Builder )..."` message. The VSCode "Needs Attention" tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. + +## Rejection / Feedback Model + +There is no formal `porch reject` command. Rejection works via the feedback-iterate pattern: + +1. Reviewer provides feedback (edit the plan file in VSCode, type in the builder pane, `afx send`, or issue comment) +2. Builder reads the feedback on its next turn, revises the artifact, recommits +3. The gate remains pending — porch doesn't advance until the human runs `porch approve` + +The same pattern works at both gates. + +## Builder Session Lifetime + +The builder is a long-running interactive Claude Code session in a PTY pane managed by Tower. The session is launched as `claude ""` (no `--print`) inside a `while true` restart loop. That form starts an interactive Claude REPL with the prompt as the first user message; after Claude finishes the prompted work it sits at the input prompt awaiting next user input. The outer `while true` loop only fires if Claude crashes — it is a crash-recovery safety net, not the gate-wait mechanism. + +This means typed input in the builder pane reaches the live Claude session immediately, exactly like any other interactive Claude Code conversation. There is no "session ended at gate" state to worry about under normal operation. + +## Configuration + +PIR uses the same `.codev/config.json` configuration as other protocols. The `worktree` block (from Issue 689) enables the at-gate dev-server review flow: + +```json +{ + "worktree": { + "symlinks": [".env.local", "packages/*/.env"], + "postSpawn": ["pnpm install --frozen-lockfile"], + "devCommand": "pnpm dev" + } +} +``` + +Without `worktree.devCommand`, `afx dev` won't work and the `code-review` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. + +## Multi-Agent Consultation + +- **plan**: human-only review. No AI consultation. +- **implement**: no AI consult — the human at the `code-review` gate is the sole reviewer of the running code. +- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. + +Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `code-review`), not AI-consult density. + +To disable consultation entirely, say "without multi-agent consultation" when starting work. + +## Signals + +PIR uses the standard porch signal vocabulary: + +``` +PHASE_COMPLETE # Current phase build complete +BLOCKED:reason # Cannot proceed +``` + +Signals are informational for log readability. The state machine is driven by `porch done` and `porch next` CLI calls inside the builder turn. + +## Commit Messages + +Commits during PIR phases use the issue-driven format: + +``` +[PIR #] Plan draft +[PIR #] Implement avatar masking +[PIR #] Add Android-side regression test +``` + +The PR title follows the project's existing PR convention. + +## Branch Naming + +``` +builder/pir- +``` + +Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. + +## File Locations + +``` +codev/plans/pir--.md # written in plan phase, on builder branch +codev/reviews/pir--.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/projects/pir-/status.yaml # porch state, managed automatically +``` + +The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. From 4222c17eb78456b9bfffb46a5b26bb31841266e4 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:35:46 +1000 Subject: [PATCH 02/63] fix(porch/artifacts): resolve artifacts for prefix-N project IDs The artifact resolver matched files by extracting leading digits and comparing against `projectId.replace(/^0+/, '')`. This worked for numeric project IDs (SPIR/ASPIR/AIR: "0073") but silently failed for prefix-N IDs (BUGFIX: "bugfix-237", PIR: "pir-1099") because no file's leading digits can equal a string starting with letters. PIR was the first issue-driven protocol with a plan-resolution check (plan_exists), so it surfaced this latent bug. BUGFIX has the same project-ID format but no plan/review-resolution checks in its protocol.json, so it never hit the broken path. Replaces six instances of the broken regex match with a single matchesProjectId helper that handles both formats: - "-" matches files starting with "--" or equal to it - "" matches files whose leading digits (zero-stripped) equal it Also fixes hasPreApproval's path-to-projectId regex (it only matched numeric path components; now extracts both formats). Adds 40 tests covering the helper directly and the LocalResolver paths with PIR / BUGFIX project IDs, plus regression guards for the numeric-ID path that SPIR / ASPIR / AIR depend on. Refs #691. --- .../porch/__tests__/artifacts.test.ts | 145 ++++++++++++++++++ .../codev/src/commands/porch/artifacts.ts | 104 ++++++------- 2 files changed, 195 insertions(+), 54 deletions(-) diff --git a/packages/codev/src/commands/porch/__tests__/artifacts.test.ts b/packages/codev/src/commands/porch/__tests__/artifacts.test.ts index 585343403..bc960236b 100644 --- a/packages/codev/src/commands/porch/__tests__/artifacts.test.ts +++ b/packages/codev/src/commands/porch/__tests__/artifacts.test.ts @@ -12,6 +12,7 @@ import { CliResolver, getResolver, isPreApprovedContent, + matchesProjectId, } from '../artifacts.js'; // --------------------------------------------------------------------------- @@ -232,3 +233,147 @@ describe('getResolver', () => { expect(() => getResolver(tmpDir)).toThrow('af-config.json is no longer supported'); }); }); + +// --------------------------------------------------------------------------- +// matchesProjectId (Issue 691 — fix prefix-N project ID resolution) +// --------------------------------------------------------------------------- + +describe('matchesProjectId', () => { + describe('numeric project IDs (SPIR / ASPIR / AIR)', () => { + it('matches a file whose leading digits equal the (zero-stripped) ID', () => { + expect(matchesProjectId('0073-feature-name.md', '0073')).toBe(true); + expect(matchesProjectId('0073-feature-name.md', '73')).toBe(true); + expect(matchesProjectId('73-feature-name.md', '0073')).toBe(true); + }); + + it('matches a directory name (no .md suffix)', () => { + expect(matchesProjectId('0073-feature-name', '0073')).toBe(true); + }); + + it('rejects a different numeric ID', () => { + expect(matchesProjectId('0073-feature-name.md', '0074')).toBe(false); + }); + + it('rejects a filename without leading digits', () => { + expect(matchesProjectId('feature-name.md', '0073')).toBe(false); + }); + }); + + describe('prefix-N project IDs (BUGFIX / PIR / future issue-driven)', () => { + it('matches a file whose name starts with --', () => { + expect(matchesProjectId('pir-1099-fix-avatar.md', 'pir-1099')).toBe(true); + expect(matchesProjectId('bugfix-237-stale-cache.md', 'bugfix-237')).toBe(true); + }); + + it('matches a directory name (no .md suffix)', () => { + expect(matchesProjectId('pir-1099-fix-avatar', 'pir-1099')).toBe(true); + }); + + it('matches when filename equals the project ID exactly (no slug)', () => { + expect(matchesProjectId('pir-1099.md', 'pir-1099')).toBe(true); + expect(matchesProjectId('pir-1099', 'pir-1099')).toBe(true); + }); + + it('rejects a different prefix', () => { + expect(matchesProjectId('bugfix-1099-foo.md', 'pir-1099')).toBe(false); + }); + + it('rejects a different number', () => { + expect(matchesProjectId('pir-1099-foo.md', 'pir-1100')).toBe(false); + }); + + it('rejects a filename where the ID is a prefix but not delimited by -', () => { + // "pir-1099foo.md" should NOT match "pir-1099" — the next char must be "-" or end. + expect(matchesProjectId('pir-1099foo.md', 'pir-1099')).toBe(false); + }); + + it('does not confuse numeric and prefixed matching', () => { + // A numeric ID should not match a prefix-N filename. + expect(matchesProjectId('pir-1099-foo.md', '1099')).toBe(false); + expect(matchesProjectId('pir-1099-foo.md', '0073')).toBe(false); + }); + }); +}); + +// --------------------------------------------------------------------------- +// LocalResolver — prefix-N project ID support (Issue 691) +// --------------------------------------------------------------------------- + +describe('LocalResolver — prefix-N project IDs', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = mkTmp(); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('getPlanContent finds a PIR plan file by pir-N project ID', () => { + writeFile( + tmpDir, + 'codev/plans/pir-1099-fix-avatar-crop.md', + '# Plan: fix avatar crop\n', + ); + + const resolver = new LocalResolver(tmpDir); + const content = resolver.getPlanContent('pir-1099', 'fix-avatar-crop'); + expect(content).toContain('# Plan: fix avatar crop'); + }); + + it('getReviewContent finds a PIR review file by pir-N project ID', () => { + writeFile( + tmpDir, + 'codev/reviews/pir-1099-fix-avatar-crop.md', + '# Review: fix avatar crop\n', + ); + + const resolver = new LocalResolver(tmpDir); + const content = resolver.getReviewContent('pir-1099', 'fix-avatar-crop'); + expect(content).toContain('# Review: fix avatar crop'); + }); + + it('getPlanContent finds a BUGFIX plan file by bugfix-N project ID', () => { + writeFile( + tmpDir, + 'codev/plans/bugfix-237-stale-cache.md', + '# Plan: stale cache\n', + ); + + const resolver = new LocalResolver(tmpDir); + const content = resolver.getPlanContent('bugfix-237', 'stale-cache'); + expect(content).toContain('# Plan: stale cache'); + }); + + it('returns null for a missing PIR plan', () => { + fs.mkdirSync(path.join(tmpDir, 'codev', 'plans'), { recursive: true }); + + const resolver = new LocalResolver(tmpDir); + expect(resolver.getPlanContent('pir-9999', 'nothing-here')).toBeNull(); + }); + + it('does not match PIR plan when looking up a numeric ID with the same digits', () => { + // Regression guard: "pir-1099-foo.md" must NOT be returned for projectId="1099". + writeFile( + tmpDir, + 'codev/plans/pir-1099-fix-avatar.md', + '# Plan: PIR avatar fix\n', + ); + + const resolver = new LocalResolver(tmpDir); + expect(resolver.getPlanContent('1099', '')).toBeNull(); + }); + + it('still works for numeric project IDs (regression guard for SPIR/ASPIR/AIR)', () => { + writeFile( + tmpDir, + 'codev/plans/0073-user-auth.md', + '# Plan: user auth\n', + ); + + const resolver = new LocalResolver(tmpDir); + expect(resolver.getPlanContent('0073', 'user-auth')).toContain('# Plan: user auth'); + expect(resolver.getPlanContent('73', 'user-auth')).toContain('# Plan: user auth'); + }); +}); diff --git a/packages/codev/src/commands/porch/artifacts.ts b/packages/codev/src/commands/porch/artifacts.ts index 9844ecd14..b94191969 100644 --- a/packages/codev/src/commands/porch/artifacts.ts +++ b/packages/codev/src/commands/porch/artifacts.ts @@ -39,6 +39,42 @@ export interface ArtifactResolver { // Shared Helpers // ============================================================================= +/** + * Match a file basename (or directory name) against a porch project ID. + * + * Supports two project-ID formats used in this codebase: + * + * 1. **Numeric** (SPIR, ASPIR, AIR): e.g. `"0073"`. Matches filenames whose + * leading digits, zero-stripped, equal the project ID (also zero-stripped). + * Example: `"0073"` matches `"0073-feature.md"` and `"73-feature.md"`. + * + * 2. **Prefix-N** (BUGFIX, PIR, any issue-driven protocol): e.g. `"pir-1099"` + * or `"bugfix-237"`. Matches filenames that equal `-` or start + * with `--`. Example: `"pir-1099"` matches + * `"pir-1099-fix-avatar.md"` but NOT `"pir-1099fix.md"`. + * + * Without this distinction, the previous regex `name.match(/^(\d+)/)` silently + * failed to find any file for prefix-N IDs, breaking `plan_exists` and other + * artifact-resolution checks for BUGFIX and PIR projects. PIR exposed the bug + * because it was the first issue-driven protocol with `plan_exists` configured. + */ +export function matchesProjectId(name: string, projectId: string): boolean { + const base = name.replace(/\.md$/, ''); + + // Prefix-N format: `-` (one or more hyphen-separated letter + // segments followed by a numeric segment). Catches "pir-1099", "bugfix-237", + // and hypothetical future "issue-driven-237". + if (/^[a-z]+(?:-[a-z]+)*-\d+$/i.test(projectId)) { + return base === projectId || base.startsWith(`${projectId}-`); + } + + // Numeric format: zero-stripped equality on leading digits. + const normalizedId = projectId.replace(/^0+/, '') || '0'; + const numMatch = base.match(/^(\d+)/); + if (!numMatch) return false; + return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; +} + /** * Check if artifact content has pre-approval frontmatter. * Looks for YAML frontmatter with `approved:` and `validated:` fields. @@ -66,15 +102,9 @@ export class LocalResolver implements ArtifactResolver { const specsDir = path.join(this.workspaceRoot, 'codev', 'specs'); if (!fs.existsSync(specsDir)) return null; - const normalizedId = projectId.replace(/^0+/, '') || '0'; try { const files = fs.readdirSync(specsDir); - const specFile = files.find(f => { - if (!f.endsWith('.md')) return false; - const numMatch = f.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); + const specFile = files.find(f => f.endsWith('.md') && matchesProjectId(f, projectId)); return specFile ? specFile.replace(/\.md$/, '') : null; } catch { return null; @@ -92,18 +122,13 @@ export class LocalResolver implements ArtifactResolver { } } - getPlanContent(projectId: string, title: string): string | null { + getPlanContent(projectId: string, _title: string): string | null { // Try new location first: codev/projects/-/plan.md const projectsDir = path.join(this.workspaceRoot, 'codev', 'projects'); if (fs.existsSync(projectsDir)) { - const normalizedId = projectId.replace(/^0+/, '') || '0'; try { const dirs = fs.readdirSync(projectsDir); - const projDir = dirs.find(d => { - const numMatch = d.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); + const projDir = dirs.find(d => matchesProjectId(d, projectId)); if (projDir) { const planPath = path.join(projectsDir, projDir, 'plan.md'); if (fs.existsSync(planPath)) { @@ -117,15 +142,9 @@ export class LocalResolver implements ArtifactResolver { const plansDir = path.join(this.workspaceRoot, 'codev', 'plans'); if (!fs.existsSync(plansDir)) return null; - const normalizedId = projectId.replace(/^0+/, '') || '0'; try { const files = fs.readdirSync(plansDir); - const planFile = files.find(f => { - if (!f.endsWith('.md')) return false; - const numMatch = f.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); + const planFile = files.find(f => f.endsWith('.md') && matchesProjectId(f, projectId)); if (planFile) { return fs.readFileSync(path.join(plansDir, planFile), 'utf-8'); } @@ -138,15 +157,9 @@ export class LocalResolver implements ArtifactResolver { const reviewsDir = path.join(this.workspaceRoot, 'codev', 'reviews'); if (!fs.existsSync(reviewsDir)) return null; - const normalizedId = projectId.replace(/^0+/, '') || '0'; try { const files = fs.readdirSync(reviewsDir); - const reviewFile = files.find(f => { - if (!f.endsWith('.md')) return false; - const numMatch = f.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); + const reviewFile = files.find(f => f.endsWith('.md') && matchesProjectId(f, projectId)); if (reviewFile) { return fs.readFileSync(path.join(reviewsDir, reviewFile), 'utf-8'); } @@ -206,12 +219,7 @@ export class CliResolver implements ArtifactResolver { const children = this.listChildren('specs'); if (!children) return null; - const normalizedId = projectId.replace(/^0+/, '') || '0'; - const match = children.find(name => { - const numMatch = name.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); + const match = children.find(name => matchesProjectId(name, projectId)); return match || null; } @@ -234,12 +242,14 @@ export class CliResolver implements ArtifactResolver { } hasPreApproval(artifactGlob: string): boolean { - // Determine artifact type from glob path (e.g., "codev/specs/0559-*.md" or "codev/plans/0559-*.md") + // Determine artifact type from glob path (e.g., "codev/specs/0559-*.md" or "codev/plans/pir-1099-*.md") const typeMatch = artifactGlob.match(/\b(specs|plans|reviews)\b/); - const idMatch = artifactGlob.match(/(?:specs|plans|reviews)\/0*(\d+)/); - if (!idMatch) return false; + // Extract project ID — supports both numeric ("0073") and prefix-N ("pir-1099", "bugfix-237") formats. + const prefixedMatch = artifactGlob.match(/(?:specs|plans|reviews)\/([a-z]+(?:-[a-z]+)*-\d+)/i); + const numericMatch = artifactGlob.match(/(?:specs|plans|reviews)\/0*(\d+)/); + const projectId = prefixedMatch?.[1] ?? numericMatch?.[1]; + if (!projectId) return false; - const projectId = idMatch[1]; let content: string | null = null; const artifactType = typeMatch?.[1] || 'specs'; @@ -262,27 +272,13 @@ export class CliResolver implements ArtifactResolver { private findPlanBaseName(projectId: string): string | null { const children = this.listChildren('plans'); if (!children) return null; - - const normalizedId = projectId.replace(/^0+/, '') || '0'; - const match = children.find(name => { - const numMatch = name.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); - return match || null; + return children.find(name => matchesProjectId(name, projectId)) || null; } private findReviewBaseName(projectId: string): string | null { const children = this.listChildren('reviews'); if (!children) return null; - - const normalizedId = projectId.replace(/^0+/, '') || '0'; - const match = children.find(name => { - const numMatch = name.match(/^(\d+)/); - if (!numMatch) return false; - return (numMatch[1].replace(/^0+/, '') || '0') === normalizedId; - }); - return match || null; + return children.find(name => matchesProjectId(name, projectId)) || null; } private listChildren(subPath: string): string[] | null { From 2d835efa595a650bb49f22758d2be7d4cf78204d Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:46:18 +1000 Subject: [PATCH 03/63] feat(agent-farm): add spawnPir, dispatch pir + bugfix separately, DB migration v9 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routing (proper separation, not flag-flavored bugfix): - Extract spawnIssueDrivenBuilder as the shared helper for any issue-driven protocol (fetch issue, derive worktree path, init porch, render prompt, …). Takes just the prefix as a parameter; derives the human-readable label internally. - spawnBugfix and spawnPir are one-line wrappers that pass their prefix. - getSpawnMode returns 'bugfix' or 'pir' honestly — no more lumping. - handlers Record has reachable entries for both: bugfix → spawnBugfix, pir → spawnPir. No dead code. - findExistingBugfixWorktree generalized to findExistingIssueWorktree with a protocolPrefix arg; old name kept as a thin backward-compat wrapper for the existing test fixture. DB: - BuilderType union includes 'pir' - builders.type CHECK constraint includes 'pir' for fresh installs - Migration v9 recreates the existing table with the new CHECK constraint (SQLite can't ALTER CHECK; idempotent — no-op if 'pir' already present) CLI help text in both cli.ts files updated to list pir. The pattern generalizes — any future issue-driven protocol gets a one-line spawn wrapper and a new dispatch entry, sharing the implementation through spawnIssueDrivenBuilder. Refs #691. --- packages/codev/src/agent-farm/cli.ts | 2 +- .../src/agent-farm/commands/spawn-worktree.ts | 28 +++++++-- .../codev/src/agent-farm/commands/spawn.ts | 63 ++++++++++++++----- packages/codev/src/agent-farm/db/index.ts | 47 ++++++++++++++ packages/codev/src/agent-farm/db/schema.ts | 2 +- packages/codev/src/agent-farm/types.ts | 2 +- packages/codev/src/cli.ts | 2 +- 7 files changed, 119 insertions(+), 27 deletions(-) diff --git a/packages/codev/src/agent-farm/cli.ts b/packages/codev/src/agent-farm/cli.ts index 220c4bbcc..55eae3e02 100644 --- a/packages/codev/src/agent-farm/cli.ts +++ b/packages/codev/src/agent-farm/cli.ts @@ -222,7 +222,7 @@ export async function runAgentFarm(args: string[]): Promise { .command('spawn') .description('Spawn a new builder') .argument('[identifier]', 'Issue identifier (positional, e.g. 315 or ENG-123)') - .option('--protocol ', 'Protocol to use (spir, aspir, air, bugfix, maintain, experiment)') + .option('--protocol ', 'Protocol to use (spir, aspir, air, bugfix, pir, maintain, experiment)') .option('--task ', 'Spawn builder with a task description') .option('--shell', 'Spawn a bare Claude session') .option('--worktree', 'Spawn worktree session') diff --git a/packages/codev/src/agent-farm/commands/spawn-worktree.ts b/packages/codev/src/agent-farm/commands/spawn-worktree.ts index 0d91054f0..d7a6d96c7 100644 --- a/packages/codev/src/agent-farm/commands/spawn-worktree.ts +++ b/packages/codev/src/agent-farm/commands/spawn-worktree.ts @@ -407,21 +407,37 @@ export function slugify(title: string): string { } /** - * Find an existing bugfix worktree directory for a given issue number. - * Scans the builders directory for directories matching `bugfix-{issueNumber}-*`. - * Returns the directory name if found, or null if no match exists. + * Find an existing issue-driven worktree directory for a given protocol prefix + * and issue number. Scans the builders directory for directories matching + * `--*`. Returns the directory name if found, or + * null if no match exists. + * + * Used by the bugfix / PIR resume path to locate a worktree whose suffix + * (slug) may have changed since the original spawn. */ -export function findExistingBugfixWorktree(buildersDir: string, issueNumber: number | string): string | null { - const prefix = `bugfix-${issueNumber}-`; +export function findExistingIssueWorktree( + buildersDir: string, + protocolPrefix: string, + issueNumber: number | string, +): string | null { + const dirPrefix = `${protocolPrefix}-${issueNumber}-`; try { const entries = readdirSync(buildersDir, { withFileTypes: true }); - const match = entries.find(e => e.isDirectory() && e.name.startsWith(prefix)); + const match = entries.find(e => e.isDirectory() && e.name.startsWith(dirPrefix)); return match ? match.name : null; } catch { return null; } } +/** + * @deprecated Use `findExistingIssueWorktree(buildersDir, 'bugfix', issueNumber)` directly. + * Kept as a thin wrapper for backwards compatibility with existing tests. + */ +export function findExistingBugfixWorktree(buildersDir: string, issueNumber: number | string): string | null { + return findExistingIssueWorktree(buildersDir, 'bugfix', issueNumber); +} + /** * Fetch a GitHub issue via the `issue-view` concept command (fatal on failure). * Delegates to shared github utility but wraps with fatal() for spawn context. diff --git a/packages/codev/src/agent-farm/commands/spawn.ts b/packages/codev/src/agent-farm/commands/spawn.ts index f73d35dcd..d99c07f0b 100644 --- a/packages/codev/src/agent-farm/commands/spawn.ts +++ b/packages/codev/src/agent-farm/commands/spawn.ts @@ -44,7 +44,7 @@ import { fetchGitHubIssue, executePreSpawnHooks, slugify, - findExistingBugfixWorktree, + findExistingIssueWorktree, validateResumeWorktree, createPtySession, startBuilderSession, @@ -193,8 +193,12 @@ function getSpawnMode(options: SpawnOptions): BuilderType { if (options.worktree) return 'worktree'; if (options.issueNumber) { - // Protocol drives mode for issue-based spawns + // Protocol drives the mode for issue-based spawns. Each issue-driven + // protocol has its own mode value and its own dispatch entry — they share + // an implementation through `spawnIssueDrivenBuilder`, not through the + // dispatcher. if (options.protocol === 'bugfix') return 'bugfix'; + if (options.protocol === 'pir') return 'pir'; return 'spec'; } @@ -650,20 +654,34 @@ async function spawnWorktree(options: SpawnOptions, config: Config): Promise { +async function spawnIssueDrivenBuilder( + options: SpawnOptions, + config: Config, + prefix: 'bugfix' | 'pir', +): Promise { + const protocolLabel = prefix === 'pir' ? 'PIR' : 'Bugfix'; const issueNumber = options.issueNumber!; const protocol = await resolveIssueProtocol(options, config); const forgeConfig = loadForgeConfig(config.workspaceRoot); - logger.header(`${options.resume ? 'Resuming' : 'Spawning'} Bugfix Builder for Issue #${issueNumber}`); + logger.header(`${options.resume ? 'Resuming' : 'Spawning'} ${protocolLabel} Builder for Issue #${issueNumber}`); // Fetch issue from GitHub logger.info('Fetching issue from GitHub...'); const issue = await fetchGitHubIssue(issueNumber, { cwd: config.workspaceRoot, forgeConfig }); - const builderId = buildAgentName('bugfix', String(issueNumber)); + const builderId = buildAgentName(prefix, String(issueNumber)); // When resuming, find the existing worktree by issue number pattern // instead of recomputing from the current title (which may have changed). @@ -672,15 +690,15 @@ async function spawnBugfix(options: SpawnOptions, config: Config): Promise let branchName: string; if (options.branch) { branchName = options.branch; - worktreeName = `bugfix-${issueNumber}`; + worktreeName = `${prefix}-${issueNumber}`; } else if (options.resume) { // Migration: try ID-only path first, fall back to old title-based path - const idOnlyName = `bugfix-${issueNumber}`; + const idOnlyName = `${prefix}-${issueNumber}`; const idOnlyPath = resolve(config.buildersDir, idOnlyName); if (existsSync(idOnlyPath)) { worktreeName = idOnlyName; } else { - const existing = findExistingBugfixWorktree(config.buildersDir, issueNumber); + const existing = findExistingIssueWorktree(config.buildersDir, prefix, issueNumber); if (existing) { worktreeName = existing; } else { @@ -689,7 +707,7 @@ async function spawnBugfix(options: SpawnOptions, config: Config): Promise } branchName = `builder/${worktreeName}`; } else { - worktreeName = `bugfix-${issueNumber}`; + worktreeName = `${prefix}-${issueNumber}`; branchName = `builder/${worktreeName}`; } @@ -741,16 +759,16 @@ async function spawnBugfix(options: SpawnOptions, config: Config): Promise } else if (options.branch) { await createWorktreeFromBranch(config, branchName, worktreePath, { remote: options.remote }); // Pre-initialize porch for --branch mode too - const porchProjectId = `bugfix-${issueNumber}`; + const porchProjectId = `${prefix}-${issueNumber}`; const slug = slugify(issue.title); await initPorchInWorktree(worktreePath, protocol, porchProjectId, slug); } else { await createWorktree(config, branchName, worktreePath); // Pre-initialize porch so the builder doesn't need to figure out project ID. - // Use bugfix-{N} as the porch project ID (not the builder agent name). + // Use -{N} as the porch project ID (not the builder agent name). // This aligns with porch's CWD-based detection from worktree paths. - const porchProjectId = `bugfix-${issueNumber}`; + const porchProjectId = `${prefix}-${issueNumber}`; const slug = slugify(issue.title); await initPorchInWorktree(worktreePath, protocol, porchProjectId, slug); } @@ -759,7 +777,7 @@ async function spawnBugfix(options: SpawnOptions, config: Config): Promise protocol_name: protocol.toUpperCase(), mode, mode_soft: mode === 'soft', mode_strict: mode === 'strict', project_id: builderId, - input_description: `a fix for GitHub Issue #${issueNumber}`, + input_description: `work for GitHub Issue #${issueNumber}`, issue: { number: issueNumber, title: issue.title, body: issue.body || '(No description provided)' }, }; if (options.branch) { @@ -781,12 +799,22 @@ async function spawnBugfix(options: SpawnOptions, config: Config): Promise upsertBuilder({ id: builderId, - name: `Bugfix #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`, + name: `${protocolLabel} #${issueNumber}: ${issue.title.substring(0, 40)}${issue.title.length > 40 ? '...' : ''}`, status: 'implementing', phase: 'init', - worktree: worktreePath, branch: branchName, type: 'bugfix', issueNumber, terminalId, + worktree: worktreePath, branch: branchName, type: prefix, issueNumber, terminalId, }); - logSpawnSuccess(`Bugfix builder for issue #${issueNumber}`, terminalId, mode); + logSpawnSuccess(`${protocolLabel} builder for issue #${issueNumber}`, terminalId, mode); +} + +/** Spawn a BUGFIX builder via the shared issue-driven helper. */ +async function spawnBugfix(options: SpawnOptions, config: Config): Promise { + return spawnIssueDrivenBuilder(options, config, 'bugfix'); +} + +/** Spawn a PIR builder via the shared issue-driven helper. */ +async function spawnPir(options: SpawnOptions, config: Config): Promise { + return spawnIssueDrivenBuilder(options, config, 'pir'); } // ============================================================================= @@ -837,6 +865,7 @@ export async function spawn(options: SpawnOptions): Promise { const handlers: Record Promise> = { spec: () => spawnSpec(options, config), bugfix: () => spawnBugfix(options, config), + pir: () => spawnPir(options, config), task: () => spawnTask(options, config), protocol: () => spawnProtocol(options, config), shell: () => spawnShell(options, config), diff --git a/packages/codev/src/agent-farm/db/index.ts b/packages/codev/src/agent-farm/db/index.ts index df75d9a6a..2c953b17d 100644 --- a/packages/codev/src/agent-farm/db/index.ts +++ b/packages/codev/src/agent-farm/db/index.ts @@ -391,6 +391,53 @@ function ensureLocalDatabase(): Database.Database { db.prepare('INSERT INTO _migrations (version) VALUES (8)').run(); } + // Migration v9: Add 'pir' to builders.type CHECK constraint (Issue 691) + const v9 = db.prepare('SELECT version FROM _migrations WHERE version = 9').get(); + if (!v9) { + const tableInfo = db + .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'") + .get() as { sql: string } | undefined; + + // Only recreate if the constraint doesn't already include 'pir' (idempotent). + if (tableInfo?.sql && !tableInfo.sql.includes("'pir'")) { + // SQLite can't alter CHECK constraints, so recreate the table. + db.exec(` + CREATE TABLE builders_new ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + port INTEGER NOT NULL DEFAULT 0, + pid INTEGER NOT NULL DEFAULT 0, + status TEXT NOT NULL DEFAULT 'spawning' + CHECK(status IN ('spawning', 'implementing', 'blocked', 'pr', 'complete')), + phase TEXT NOT NULL DEFAULT '', + worktree TEXT NOT NULL, + branch TEXT NOT NULL, + type TEXT NOT NULL DEFAULT 'spec' + CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix', 'pir')), + task_text TEXT, + protocol_name TEXT, + issue_number TEXT, + terminal_id TEXT, + started_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + INSERT INTO builders_new SELECT * FROM builders; + DROP TABLE builders; + ALTER TABLE builders_new RENAME TO builders; + CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status); + CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port); + CREATE TRIGGER IF NOT EXISTS builders_updated_at + AFTER UPDATE ON builders + FOR EACH ROW + BEGIN + UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id; + END; + `); + console.log("[info] Migrated builders table: added 'pir' to type CHECK constraint"); + } + db.prepare('INSERT INTO _migrations (version) VALUES (9)').run(); + } + return db; } diff --git a/packages/codev/src/agent-farm/db/schema.ts b/packages/codev/src/agent-farm/db/schema.ts index 8623633d2..cd320c8f3 100644 --- a/packages/codev/src/agent-farm/db/schema.ts +++ b/packages/codev/src/agent-farm/db/schema.ts @@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS builders ( worktree TEXT NOT NULL, branch TEXT NOT NULL, type TEXT NOT NULL DEFAULT 'spec' - CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix')), + CHECK(type IN ('spec', 'task', 'protocol', 'shell', 'worktree', 'bugfix', 'pir')), task_text TEXT, protocol_name TEXT, issue_number TEXT, diff --git a/packages/codev/src/agent-farm/types.ts b/packages/codev/src/agent-farm/types.ts index 0931a7888..bf232858a 100644 --- a/packages/codev/src/agent-farm/types.ts +++ b/packages/codev/src/agent-farm/types.ts @@ -2,7 +2,7 @@ * Core types for Agent Farm */ -export type BuilderType = 'spec' | 'task' | 'protocol' | 'shell' | 'worktree' | 'bugfix'; +export type BuilderType = 'spec' | 'task' | 'protocol' | 'shell' | 'worktree' | 'bugfix' | 'pir'; export interface Builder { id: string; diff --git a/packages/codev/src/cli.ts b/packages/codev/src/cli.ts index cffbda6ad..3fa456639 100644 --- a/packages/codev/src/cli.ts +++ b/packages/codev/src/cli.ts @@ -173,7 +173,7 @@ program .option('-m, --model ', 'Model to use (gemini, codex, claude, hermes, or aliases: pro, gpt, opus)') .option('--prompt ', 'Inline prompt (general mode)') .option('--prompt-file ', 'Prompt file path (general mode)') - .option('--protocol ', 'Protocol name: spir, aspir, air, bugfix, maintain') + .option('--protocol ', 'Protocol name: spir, aspir, air, bugfix, pir, maintain') .option('-t, --type ', 'Review type: spec, plan, impl, pr, phase, integration') .option('--issue ', 'Issue number (required from architect context)') .option('--output ', 'Write consultation output to file (used by porch)') From 1b67cb00cf04f8f4da44eeaca9b37b623c80a827 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:46:59 +1000 Subject: [PATCH 04/63] feat(porch/state): recognize pir-* worktrees in detectProjectIdFromCwd MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the worktree-path regex to match `.builders/pir-[-]/` paths and return `pir-` as the porch project ID — same pattern as bugfix worktrees use today. Without this, running `porch status` (or any other porch command that relies on CWD-based project detection) from inside a PIR worktree falls through to filesystem scanning instead of detecting the project directly. Refs #691. --- packages/codev/src/commands/porch/state.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/codev/src/commands/porch/state.ts b/packages/codev/src/commands/porch/state.ts index 13661bb6f..7b6800a21 100644 --- a/packages/codev/src/commands/porch/state.ts +++ b/packages/codev/src/commands/porch/state.ts @@ -309,19 +309,21 @@ export function findStatusPath(workspaceRoot: string, projectId: string): string */ export function detectProjectIdFromCwd(cwd: string): string | null { const normalized = path.resolve(cwd).split(path.sep).join('/'); - // Bugfix worktrees: .builders/bugfix-{N}-{slug} (slug is optional for legacy paths) - // Protocol worktrees: .builders/{protocol}-{N}-{slug} (aspir, spir, air) + // Issue-driven worktrees: .builders/{bugfix|pir}-{N}-{slug} (slug optional for legacy paths) + // bugfix and pir use a "{prefix}-{N}" porch project ID. + // Protocol worktrees: .builders/{aspir|spir|air}-{N}-{slug} (slug optional) + // These use the bare numeric ID as the porch project ID. // Spec worktrees (legacy): .builders/{NNNN} (bare 4-digit ID, no slug) const match = normalized.match( - /\/\.builders\/(bugfix-(\d+)(?:-[^/]*)?|(?:aspir|spir|air)-(\d+)(?:-[^/]*)?|(\d{4}))(\/|$)/, + /\/\.builders\/((bugfix|pir)-(\d+)(?:-[^/]*)?|(?:aspir|spir|air)-(\d+)(?:-[^/]*)?|(\d{4}))(\/|$)/, ); if (!match) return null; - // Bugfix worktrees use "bugfix-N" as the porch project ID - if (match[2]) return `bugfix-${match[2]}`; + // Issue-driven worktrees (bugfix, pir) use "{prefix}-N" as the porch project ID + if (match[2] && match[3]) return `${match[2]}-${match[3]}`; // Protocol worktrees (aspir, spir, air) use the bare numeric ID - if (match[3]) return match[3]; + if (match[4]) return match[4]; // Spec worktrees use zero-padded numeric IDs - return match[4]; + return match[5]; } export type ResolvedProjectId = { id: string; source: 'explicit' | 'cwd' | 'filesystem' }; From ae50b8b919e7259007c82a396a2a536aa7681c71 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:48:22 +1000 Subject: [PATCH 05/63] feat(porch/status): --json flag for structured gate state Add a --json flag to `porch status` that emits a single-line JSON object suitable for consumption by the VSCode Needs Attention view (and any other tooling needing structured access to gate state). Object shape: { "id": string, "title": string, "protocol": string, "phase": string, "iteration": number, "build_complete": boolean, "gate": string | null, "gate_status": "pending" | "approved" | null, "gate_requested_at": string | null, "gate_approved_at": string | null } Suppresses all chalk/console output when --json is set; writes the single JSON line to stdout via process.stdout.write so the consumer gets parseable output even if other code paths log warnings. Tests cover: object shape, pending-gate case, approved-gate case, ungated-phase case (gate: null), code-review gate at implement phase, suppression of human-readable console output. Refs #691. --- .../porch/__tests__/status-json.test.ts | 194 ++++++++++++++++++ packages/codev/src/commands/porch/index.ts | 58 +++++- 2 files changed, 248 insertions(+), 4 deletions(-) create mode 100644 packages/codev/src/commands/porch/__tests__/status-json.test.ts diff --git a/packages/codev/src/commands/porch/__tests__/status-json.test.ts b/packages/codev/src/commands/porch/__tests__/status-json.test.ts new file mode 100644 index 000000000..161d5a778 --- /dev/null +++ b/packages/codev/src/commands/porch/__tests__/status-json.test.ts @@ -0,0 +1,194 @@ +/** + * Tests for `porch status --json` (Issue 691). + * + * The JSON flag produces a single-line JSON object on stdout suitable for + * consumption by the VSCode Needs Attention view (and any other tooling). + * All human-readable console output is suppressed. + * + * Asserted shape (per the function's JSDoc in commands/porch/index.ts): + * + * { + * "id": string, + * "title": string, + * "protocol": string, + * "phase": string, + * "iteration": number, + * "build_complete": boolean, + * "gate": string | null, + * "gate_status": "pending" | "approved" | null, + * "gate_requested_at": string | null, + * "gate_approved_at": string | null + * } + */ + +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { tmpdir } from 'node:os'; +import { status } from '../index.js'; +import { writeState, getStatusPath } from '../state.js'; +import type { ProjectState } from '../types.js'; + +function createTestDir(): string { + return path.join( + tmpdir(), + `porch-status-json-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + ); +} + +function setupPirProtocol(testDir: string): void { + const protocolDir = path.join(testDir, 'codev', 'protocols', 'pir'); + fs.mkdirSync(protocolDir, { recursive: true }); + fs.writeFileSync( + path.join(protocolDir, 'protocol.json'), + JSON.stringify({ + name: 'pir', + version: '1.0.0', + phases: [ + { id: 'plan', name: 'Plan', type: 'build_verify', gate: 'plan-approval', next: 'implement' }, + { id: 'implement', name: 'Implement', type: 'build_verify', gate: 'code-review', next: 'review' }, + { id: 'review', name: 'Review', type: 'build_verify', next: null }, + ], + }), + ); +} + +function makeState(overrides: Partial = {}): ProjectState { + return { + id: 'pir-842', + title: 'fix-avatar-crop', + protocol: 'pir', + phase: 'plan', + plan_phases: [], + current_plan_phase: null, + gates: {}, + iteration: 1, + build_complete: false, + history: [], + started_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + ...overrides, + }; +} + +describe('porch status --json', () => { + let testDir: string; + let stdoutSpy: ReturnType; + let logSpy: ReturnType; + + beforeEach(() => { + testDir = createTestDir(); + fs.mkdirSync(testDir, { recursive: true }); + setupPirProtocol(testDir); + // Capture process.stdout.write to inspect the JSON output and prevent + // it from leaking into the test runner's output. + stdoutSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + // Suppress chalk/console output from any non-JSON code paths so tests + // don't print noise even if the suppression path regresses. + logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + fs.rmSync(testDir, { recursive: true, force: true }); + vi.restoreAllMocks(); + }); + + function parseStdoutJson(): Record { + // The last write should be the JSON line; previous writes may be empty. + const calls = stdoutSpy.mock.calls + .map(c => (typeof c[0] === 'string' ? c[0] : String(c[0]))) + .filter(s => s.trim().length > 0); + expect(calls.length).toBeGreaterThan(0); + return JSON.parse(calls[calls.length - 1]); + } + + it('emits JSON object with all required fields when gate is pending', async () => { + const state = makeState({ + gates: { + 'plan-approval': { + status: 'pending', + requested_at: '2026-05-12T14:23:00.000Z', + }, + }, + build_complete: true, + }); + writeState(getStatusPath(testDir, 'pir-842', 'fix-avatar-crop'), state); + + await status(testDir, 'pir-842', undefined, { json: true }); + + const out = parseStdoutJson(); + expect(out.id).toBe('pir-842'); + expect(out.title).toBe('fix-avatar-crop'); + expect(out.protocol).toBe('pir'); + expect(out.phase).toBe('plan'); + expect(out.iteration).toBe(1); + expect(out.build_complete).toBe(true); + expect(out.gate).toBe('plan-approval'); + expect(out.gate_status).toBe('pending'); + expect(out.gate_requested_at).toBe('2026-05-12T14:23:00.000Z'); + expect(out.gate_approved_at).toBeNull(); + }); + + it('emits gate_status approved with approved_at timestamp', async () => { + const state = makeState({ + gates: { + 'plan-approval': { + status: 'approved', + requested_at: '2026-05-12T14:23:00.000Z', + approved_at: '2026-05-12T14:35:00.000Z', + }, + }, + }); + writeState(getStatusPath(testDir, 'pir-842', 'fix-avatar-crop'), state); + + await status(testDir, 'pir-842', undefined, { json: true }); + + const out = parseStdoutJson(); + expect(out.gate_status).toBe('approved'); + expect(out.gate_requested_at).toBe('2026-05-12T14:23:00.000Z'); + expect(out.gate_approved_at).toBe('2026-05-12T14:35:00.000Z'); + }); + + it('emits gate null for an ungated phase', async () => { + const state = makeState({ phase: 'review' }); + writeState(getStatusPath(testDir, 'pir-842', 'fix-avatar-crop'), state); + + await status(testDir, 'pir-842', undefined, { json: true }); + + const out = parseStdoutJson(); + expect(out.phase).toBe('review'); + expect(out.gate).toBeNull(); + expect(out.gate_status).toBeNull(); + expect(out.gate_requested_at).toBeNull(); + expect(out.gate_approved_at).toBeNull(); + }); + + it('suppresses human-readable console output in json mode', async () => { + const state = makeState(); + writeState(getStatusPath(testDir, 'pir-842', 'fix-avatar-crop'), state); + + await status(testDir, 'pir-842', undefined, { json: true }); + + // No console.log calls should fire in JSON mode — all output goes via + // process.stdout.write. + expect(logSpy).not.toHaveBeenCalled(); + }); + + it('reports the code-review gate correctly when in the implement phase', async () => { + const state = makeState({ + phase: 'implement', + gates: { + 'code-review': { status: 'pending', requested_at: '2026-05-12T15:00:00.000Z' }, + }, + build_complete: true, + }); + writeState(getStatusPath(testDir, 'pir-842', 'fix-avatar-crop'), state); + + await status(testDir, 'pir-842', undefined, { json: true }); + + const out = parseStdoutJson(); + expect(out.phase).toBe('implement'); + expect(out.gate).toBe('code-review'); + expect(out.gate_status).toBe('pending'); + }); +}); diff --git a/packages/codev/src/commands/porch/index.ts b/packages/codev/src/commands/porch/index.ts index e448776e7..57d8d5a63 100644 --- a/packages/codev/src/commands/porch/index.ts +++ b/packages/codev/src/commands/porch/index.ts @@ -121,12 +121,40 @@ function logCheckOverrides( // ============================================================================ /** - * porch status + * porch status [--json] * Shows current state and prescriptive next steps. + * + * `--json`: emits a single-line JSON object with the project's current state + * and gate status, suppressing all human-readable output. Consumed by the + * VSCode Needs Attention view (Issue 691) and any other tooling that needs + * structured access to gate state. + * + * JSON shape: + * { + * "id": string, + * "title": string, + * "protocol": string, + * "phase": string, + * "iteration": number, + * "build_complete": boolean, + * "gate": string | null, + * "gate_status": "pending" | "approved" | null, + * "gate_requested_at": string | null, // ISO timestamp + * "gate_approved_at": string | null // ISO timestamp + * } */ -export async function status(workspaceRoot: string, projectId: string, resolver?: ArtifactResolver): Promise { +export async function status( + workspaceRoot: string, + projectId: string, + resolver?: ArtifactResolver, + options?: { json?: boolean }, +): Promise { const statusPath = findStatusPath(workspaceRoot, projectId); if (!statusPath) { + if (options?.json) { + console.error(`Project ${projectId} not found.`); + process.exit(1); + } throw new Error(`Project ${projectId} not found.\nRun 'porch init' to create a new project.`); } @@ -134,6 +162,25 @@ export async function status(workspaceRoot: string, projectId: string, resolver? const protocol = loadProtocol(workspaceRoot, state.protocol); const phaseConfig = getPhaseConfig(protocol, state.phase); + if (options?.json) { + const gateName = getPhaseGate(protocol, state.phase); + const gateStatus = gateName ? state.gates[gateName] : undefined; + const out = { + id: state.id, + title: state.title, + protocol: state.protocol, + phase: state.phase, + iteration: state.iteration, + build_complete: state.build_complete, + gate: gateName ?? null, + gate_status: gateStatus?.status ?? null, + gate_requested_at: gateStatus?.requested_at ?? null, + gate_approved_at: gateStatus?.approved_at ?? null, + }; + process.stdout.write(JSON.stringify(out) + '\n'); + return; + } + // Header console.log(''); console.log(header(`PROJECT: ${state.id} - ${state.title}`)); @@ -1025,9 +1072,12 @@ export async function cli(args: string[]): Promise { process.exit(1); break; - case 'status': - await status(workspaceRoot, getProjectId(rest[0]), resolver); + case 'status': { + const json = rest.includes('--json'); + const positional = rest.find(a => !a.startsWith('--')); + await status(workspaceRoot, getProjectId(positional), resolver, { json }); break; + } case 'check': await check(workspaceRoot, getProjectId(rest[0]), resolver); From 3487b727b9299d36b6bada9aca598d4f3f280ee5 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 13:55:34 +1000 Subject: [PATCH 06/63] feat(overview): surface PIR code-review gate in blocked allowlist + dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The overview API's detectBlocked() and detectBlockedSince() functions had a hardcoded gate-name allowlist (spec-approval, plan-approval, pr) that didn't include PIR's code-review gate. Result: when a PIR builder reached code-review, OverviewBuilder.blocked stayed null — making the builder invisible to the VSCode Needs Attention tree, the VSCode toast, the dashboard NeedsAttentionList, and the status bar counter. Adds 'code-review' to both gate-name allowlists in overview.ts. Dashboard CSS routing: - NeedsAttentionList previously had hardcoded kindClass routing (spec review → spec class; everything else → plan class). Extracted to a gateKindClass() helper that maps known kinds to dedicated classes and falls back to plan styling for unknowns. - Added .attention-kind--code-review CSS using --status-waiting (same semantic as PR review — "waiting on a human", not an error). Refs #691. --- .../codev/src/agent-farm/servers/overview.ts | 11 ++++++++++- .../src/components/NeedsAttentionList.tsx | 18 +++++++++++++++--- packages/dashboard/src/index.css | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index e2061dc57..8badd1439 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -331,11 +331,18 @@ function loadProtocolPhases(workspaceRoot: string, protocolName: string): string /** * Detect if a builder is blocked on a gate (requested but not approved). * Returns a human-readable label or null. + * + * The allowlist mirrors the gates emitted by the bundled protocols (SPIR, + * ASPIR, BUGFIX, AIR, PIR). New protocols that introduce new gate names must + * register them here, otherwise their gate-pending state is invisible to + * `OverviewBuilder.blocked` and downstream UIs (VSCode Needs Attention tree, + * VSCode toast, dashboard NeedsAttentionList, status bar counter). */ export function detectBlocked(parsed: ParsedStatus): string | null { const gateLabels: Record = { 'spec-approval': 'spec review', 'plan-approval': 'plan review', + 'code-review': 'code review', 'pr': 'PR review', }; @@ -350,9 +357,11 @@ export function detectBlocked(parsed: ParsedStatus): string | null { /** * Detect when the current blocked gate was first requested. * Returns the ISO timestamp string or null if not blocked. + * + * Keep this list in sync with `detectBlocked`'s `gateLabels` keys. */ export function detectBlockedSince(parsed: ParsedStatus): string | null { - const gateNames = ['spec-approval', 'plan-approval', 'pr']; + const gateNames = ['spec-approval', 'plan-approval', 'code-review', 'pr']; for (const gate of gateNames) { if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) { return parsed.gateRequestedAt[gate]; diff --git a/packages/dashboard/src/components/NeedsAttentionList.tsx b/packages/dashboard/src/components/NeedsAttentionList.tsx index 8f669a630..558db9db5 100644 --- a/packages/dashboard/src/components/NeedsAttentionList.tsx +++ b/packages/dashboard/src/components/NeedsAttentionList.tsx @@ -15,6 +15,20 @@ interface AttentionItem { url?: string; } +/** + * Map an OverviewBuilder.blocked label to a CSS class. The labels come from + * `detectBlocked` in packages/codev/src/agent-farm/servers/overview.ts. + * Unknown kinds fall back to the plan styling so the row still renders. + */ +function gateKindClass(blocked: string): string { + switch (blocked) { + case 'spec review': return 'attention-kind--spec'; + case 'plan review': return 'attention-kind--plan'; + case 'code review': return 'attention-kind--code-review'; + default: return 'attention-kind--plan'; + } +} + function timeAgo(dateStr: string): string { const ms = Date.now() - new Date(dateStr).getTime(); const mins = Math.floor(ms / 60000); @@ -52,9 +66,7 @@ function buildItems(prs: OverviewPR[], builders: OverviewBuilder[]): AttentionIt issueOrPR: label, title: b.issueTitle || b.id, kind: b.blocked, - kindClass: b.blocked === 'spec review' - ? 'attention-kind--spec' - : 'attention-kind--plan', + kindClass: gateKindClass(b.blocked), waitingSince: b.blockedSince, }); } diff --git a/packages/dashboard/src/index.css b/packages/dashboard/src/index.css index 9bba21f76..4648fe13f 100644 --- a/packages/dashboard/src/index.css +++ b/packages/dashboard/src/index.css @@ -1282,6 +1282,10 @@ a.attention-row { color: var(--status-error); } +.attention-kind--code-review { + color: var(--status-waiting); +} + .attention-row-age { font-size: 13px; color: var(--text-muted); From 48e672aa5586927d8db98dcd81ca0ff7738c5894 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 17:22:04 +1000 Subject: [PATCH 07/63] fix(agent-farm/utils): add 'pir' to buildAgentName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BuilderType gained a 'pir' variant in 2d835efa (commit 3 of #691) but buildAgentName's switch never grew a corresponding case — PIR builder IDs were falling through to the default. Add the 'pir' branch so PIR builder IDs are constructed correctly (e.g., 'builder-pir-737' for issue 737). Adds one test case mirroring the existing bugfix coverage. Refs #691. --- packages/codev/src/agent-farm/__tests__/agent-names.test.ts | 5 +++++ packages/codev/src/agent-farm/utils/agent-names.ts | 3 +++ 2 files changed, 8 insertions(+) diff --git a/packages/codev/src/agent-farm/__tests__/agent-names.test.ts b/packages/codev/src/agent-farm/__tests__/agent-names.test.ts index 82d4898e1..ebcbeb832 100644 --- a/packages/codev/src/agent-farm/__tests__/agent-names.test.ts +++ b/packages/codev/src/agent-farm/__tests__/agent-names.test.ts @@ -42,6 +42,11 @@ describe('buildAgentName', () => { expect(buildAgentName('bugfix', '269')).toBe('builder-bugfix-269'); }); + it('generates pir builder names', () => { + expect(buildAgentName('pir', '737')).toBe('builder-pir-737'); + expect(buildAgentName('pir', '0042')).toBe('builder-pir-42'); + }); + it('generates task builder names', () => { expect(buildAgentName('task', 'AbCd')).toBe('builder-task-abcd'); expect(buildAgentName('task', 'XyZ1')).toBe('builder-task-xyz1'); diff --git a/packages/codev/src/agent-farm/utils/agent-names.ts b/packages/codev/src/agent-farm/utils/agent-names.ts index 5947adbde..cb7e294fb 100644 --- a/packages/codev/src/agent-farm/utils/agent-names.ts +++ b/packages/codev/src/agent-farm/utils/agent-names.ts @@ -43,6 +43,9 @@ export function buildAgentName(type: BuilderType, id: string, protocol?: string) case 'bugfix': protocolSegment = 'bugfix'; break; + case 'pir': + protocolSegment = 'pir'; + break; case 'task': protocolSegment = 'task'; break; From 8956e152d3708b6465dc458dab509aec81bb2454 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:20:34 +1000 Subject: [PATCH 08/63] feat(porch): wire up gate notifications via generic notifyTerminal primitive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fills two long-standing UX gaps in porch's gate handling, both visible across all gated protocols (SPIR, ASPIR, PIR) — not PIR-specific: 1. notifyArchitect (added in Spec 0108) was orphaned: the function existed but no porch site ever called it. None of the four gate-pending sites in index.ts / next.ts notified the architect, so the architect pane only learned of gate state by user narration. Now wired up at every site that marks a gate pending. 2. After `porch approve`, the builder's interactive Claude session sat idle at the gate with nothing to prompt `porch next` — users had to type into the builder pane to make it advance. Now porch sends a wake-up message to the builder PTY immediately after approval so the builder reads it as its next user input and advances on its own. Implementation: collapsed `notifyArchitect` and a new builder-wake-up function into a single `notifyTerminal({ target, message, worktreeDir, draft? })` primitive. `draft: true` appends `--no-enter` for architect notifications (typed into the input buffer, not submitted) so the human sees the text but reviews before acting. `draft: false` submits immediately so the receiver's Claude session processes the message on its next turn (used for builder wake-ups). Message formatting lives in two pure helpers — `gatePendingMessage` and `gateApprovedMessage` — that the call sites compose with notifyTerminal. Tests cover target routing, draft vs submit, error swallowing, cwd / timeout, and both message helpers. --- .../commands/porch/__tests__/notify.test.ts | 119 ++++++++++++------ packages/codev/src/commands/porch/index.ts | 30 +++++ packages/codev/src/commands/porch/next.ts | 7 ++ packages/codev/src/commands/porch/notify.ts | 57 +++++++-- 4 files changed, 160 insertions(+), 53 deletions(-) diff --git a/packages/codev/src/commands/porch/__tests__/notify.test.ts b/packages/codev/src/commands/porch/__tests__/notify.test.ts index 57f57e8b1..dcd831456 100644 --- a/packages/codev/src/commands/porch/__tests__/notify.test.ts +++ b/packages/codev/src/commands/porch/__tests__/notify.test.ts @@ -1,8 +1,9 @@ /** - * Tests for notifyArchitect (Spec 0108) + * Tests for notifyTerminal (Spec 0108 generalized) * - * Verifies that porch sends gate notifications via afx send - * and that failures are swallowed (fire-and-forget). + * Verifies that porch sends gate notifications via `afx send` and that + * failures are swallowed (fire-and-forget). Also covers the message-builder + * helpers used by both architect-pending and builder-approval flows. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; @@ -18,14 +19,13 @@ vi.mock('node:child_process', () => ({ })); import { execFile } from 'node:child_process'; -import { notifyArchitect } from '../notify.js'; +import { notifyTerminal, gatePendingMessage, gateApprovedMessage } from '../notify.js'; const mockExecFile = vi.mocked(execFile); -describe('notifyArchitect', () => { +describe('notifyTerminal', () => { beforeEach(() => { mockExecFile.mockReset(); - // Restore default success-callback implementation after each test mockExecFile.mockImplementation( (_cmd: any, _args: any, _opts: any, cb: any) => { cb(null); @@ -34,37 +34,74 @@ describe('notifyArchitect', () => { ); }); - it('calls execFile with correct arguments', () => { - notifyArchitect('0108', 'spec-approval', '/projects/test'); + it('routes message to the named target via afx send', () => { + notifyTerminal({ + target: 'architect', + message: 'hello', + worktreeDir: '/projects/test', + }); expect(mockExecFile).toHaveBeenCalledTimes(1); const [cmd, args, opts] = mockExecFile.mock.calls[0]; expect(cmd).toBe(process.execPath); expect(args).toContain('send'); expect(args).toContain('architect'); + expect(args).toContain('hello'); expect(args).toContain('--raw'); - expect(args).toContain('--no-enter'); expect((opts as { cwd: string }).cwd).toBe('/projects/test'); }); - it('formats message with gate name and project id', () => { - notifyArchitect('0108', 'plan-approval', '/projects/test'); + it('appends --no-enter when draft is true', () => { + notifyTerminal({ + target: 'architect', + message: 'pending', + worktreeDir: '/projects/test', + draft: true, + }); + + const args = mockExecFile.mock.calls[0][1]!; + expect(args).toContain('--no-enter'); + }); + + it('omits --no-enter when draft is false/undefined', () => { + notifyTerminal({ + target: 'pir-0108', + message: 'wake up', + worktreeDir: '/projects/test', + }); + + const args = mockExecFile.mock.calls[0][1]!; + expect(args).not.toContain('--no-enter'); + }); + + it('uses the builder id as target for wake-ups', () => { + notifyTerminal({ + target: 'pir-0108', + message: 'approved', + worktreeDir: '/projects/test', + }); - const message = mockExecFile.mock.calls[0][1]![3]; - expect(message).toContain('GATE: plan-approval (Builder 0108)'); - expect(message).toContain('Builder 0108 is waiting for approval.'); - expect(message).toContain('Run: porch approve 0108 plan-approval'); + const args = mockExecFile.mock.calls[0][1]!; + expect(args).toContain('pir-0108'); }); it('sets timeout to 10 seconds', () => { - notifyArchitect('0108', 'spec-approval', '/projects/test'); + notifyTerminal({ + target: 'architect', + message: 'x', + worktreeDir: '/projects/test', + }); const opts = mockExecFile.mock.calls[0][2] as { timeout: number }; expect(opts.timeout).toBe(10_000); }); it('sets cwd to worktreeDir', () => { - notifyArchitect('0108', 'spec-approval', '/my/worktree'); + notifyTerminal({ + target: 'architect', + message: 'x', + worktreeDir: '/my/worktree', + }); const opts = mockExecFile.mock.calls[0][2] as { cwd: string }; expect(opts.cwd).toBe('/my/worktree'); @@ -80,40 +117,40 @@ describe('notifyArchitect', () => { } ); - // Should not throw - expect(() => notifyArchitect('0108', 'spec-approval', '/projects/test')).not.toThrow(); + expect(() => + notifyTerminal({ target: 'architect', message: 'x', worktreeDir: '/p' }) + ).not.toThrow(); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('Gate notification failed') - ); - - consoleSpy.mockRestore(); - }); - - it('logs error message on failure', () => { - const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - mockExecFile.mockImplementation( - (_cmd: any, _args: any, _opts: any, cb: any) => { - cb(new Error('connection refused')); - return undefined as any; - } - ); - - notifyArchitect('0108', 'spec-approval', '/projects/test'); - - expect(consoleSpy).toHaveBeenCalledWith( - '[porch] Gate notification failed: connection refused' + expect.stringContaining('notifyTerminal(architect) failed') ); consoleSpy.mockRestore(); }); it('uses afx binary path ending with bin/afx.js', () => { - notifyArchitect('0108', 'spec-approval', '/projects/test'); + notifyTerminal({ + target: 'architect', + message: 'x', + worktreeDir: '/projects/test', + }); const args = mockExecFile.mock.calls[0][1]!; - // The afx binary should be a resolved path ending with bin/afx.js expect(args[0]).toMatch(/bin\/afx\.js$/); }); }); + +describe('message helpers', () => { + it('gatePendingMessage formats with project id and gate name', () => { + const msg = gatePendingMessage('0108', 'plan-approval'); + expect(msg).toContain('GATE: plan-approval (Builder 0108)'); + expect(msg).toContain('Builder 0108 is waiting for approval.'); + expect(msg).toContain('Run: porch approve 0108 plan-approval'); + }); + + it('gateApprovedMessage references the gate and porch next', () => { + const msg = gateApprovedMessage('code-review'); + expect(msg).toContain('code-review'); + expect(msg).toContain('porch next'); + }); +}); diff --git a/packages/codev/src/commands/porch/index.ts b/packages/codev/src/commands/porch/index.ts index 57d8d5a63..b41359302 100644 --- a/packages/codev/src/commands/porch/index.ts +++ b/packages/codev/src/commands/porch/index.ts @@ -48,6 +48,7 @@ import { type CheckEnv, } from './checks.js'; import { loadCheckOverrides } from './config.js'; +import { notifyTerminal, gatePendingMessage, gateApprovedMessage } from './notify.js'; import { loadConfig } from '../../lib/config.js'; import { version } from '../../version.js'; @@ -483,6 +484,12 @@ export async function done(workspaceRoot: string, projectId: string, resolver?: if (!state.gates[gate].requested_at) { state.gates[gate].requested_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gate} gate-requested`); + notifyTerminal({ + target: 'architect', + message: gatePendingMessage(state.id, gate), + worktreeDir: workspaceRoot, + draft: true, + }); } console.log(''); console.log(chalk.yellow(`GATE REQUIRED: ${gate}`)); @@ -603,6 +610,12 @@ export async function gate(workspaceRoot: string, projectId: string, resolver?: if (!state.gates[gateName].requested_at) { state.gates[gateName].requested_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-requested`); + notifyTerminal({ + target: 'architect', + message: gatePendingMessage(state.id, gateName), + worktreeDir: workspaceRoot, + draft: true, + }); } console.log(''); @@ -673,6 +686,12 @@ export async function approve( // Gate belongs to the current phase — initialize it state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() }; await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-created (upgrade)`); + notifyTerminal({ + target: 'architect', + message: gatePendingMessage(state.id, gateName), + worktreeDir: workspaceRoot, + draft: true, + }); } else { const knownGates = Object.keys(state.gates).join(', '); throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`); @@ -729,6 +748,17 @@ export async function approve( state.gates[gateName].approved_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-approved`); + // Wake the builder. The builder's interactive Claude session sits idle at + // the gate; without an input event nothing prompts it to call porch next + // and advance. Symmetric counterpart to the architect notification that + // fires when a gate becomes pending. Submitted immediately (draft:false) + // so Claude's next turn processes it. + notifyTerminal({ + target: state.id, + message: gateApprovedMessage(gateName), + worktreeDir: workspaceRoot, + }); + console.log(''); console.log(chalk.green(`Gate ${gateName} approved.`)); diff --git a/packages/codev/src/commands/porch/next.ts b/packages/codev/src/commands/porch/next.ts index f3008da2e..aadb60664 100644 --- a/packages/codev/src/commands/porch/next.ts +++ b/packages/codev/src/commands/porch/next.ts @@ -34,6 +34,7 @@ import { import { buildPhasePrompt } from './prompts.js'; import { parseVerdict, allApprove } from './verdict.js'; import { loadCheckOverrides } from './config.js'; +import { notifyTerminal, gatePendingMessage } from './notify.js'; import { loadConfig } from '../../lib/config.js'; import { getResolver, type ArtifactResolver } from './artifacts.js'; @@ -710,6 +711,12 @@ async function handleVerifyApproved( state.iteration = 1; state.history = []; await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-requested`); + notifyTerminal({ + target: 'architect', + message: gatePendingMessage(state.id, gateName), + worktreeDir: workspaceRoot, + draft: true, + }); return { status: 'gate_pending', diff --git a/packages/codev/src/commands/porch/notify.ts b/packages/codev/src/commands/porch/notify.ts index 500df9b97..3a9207b4f 100644 --- a/packages/codev/src/commands/porch/notify.ts +++ b/packages/codev/src/commands/porch/notify.ts @@ -1,6 +1,9 @@ /** - * Porch gate notification — sends `afx send architect` when a gate transitions to pending. - * Spec 0108: Push-based gate notifications, replacing the poll-based gate watcher. + * Porch terminal notifications — sends `afx send ` to deliver messages + * into a target terminal (architect or builder) as PTY input. + * + * Used by porch's state machine to notify the architect when a gate becomes + * pending (Spec 0108) and to wake the builder when a gate is approved. */ import { execFile } from 'node:child_process'; @@ -12,27 +15,57 @@ function resolveAfxBinary(): string { return resolve(thisDir, '../../../bin/afx.js'); } -/** - * Fire-and-forget notification to the architect terminal when a gate becomes pending. - * Uses `afx send architect` via execFile (no shell, no injection risk). - * Errors are logged but never thrown — notification is best-effort. - */ -export function notifyArchitect(projectId: string, gateName: string, worktreeDir: string): void { - const message = [ +export interface NotifyTerminalOptions { + /** Target terminal: 'architect' or a builder ID (e.g., 'pir-1298'). */ + target: string; + /** Message text to deliver. */ + message: string; + /** Working directory — used by afx to resolve the workspace. */ + worktreeDir: string; + /** + * When true, deliver the message as a draft (typed into the input buffer + * but Enter is NOT pressed). The receiver sees the text but won't act on + * it until they manually submit. Used for the architect convention. + * + * When false / omitted, the message is submitted immediately so the + * receiver Claude session processes it on its next turn. Used for builder + * wake-ups after gate approval. + */ + draft?: boolean; +} + +/** Architect-bound notification when a gate becomes pending. */ +export function gatePendingMessage(projectId: string, gateName: string): string { + return [ `GATE: ${gateName} (Builder ${projectId})`, `Builder ${projectId} is waiting for approval.`, `Run: porch approve ${projectId} ${gateName}`, ].join('\n'); +} + +/** Builder-bound wake-up after a gate is approved. */ +export function gateApprovedMessage(gateName: string): string { + return `Gate ${gateName} approved — please run \`porch next\` to advance.`; +} + +/** + * Fire-and-forget notification to a terminal (architect or builder). + * Uses `afx send ` via execFile (no shell, no injection risk). + * Errors are logged but never thrown — notification is best-effort. + */ +export function notifyTerminal(opts: NotifyTerminalOptions): void { + const args = ['send', opts.target, opts.message, '--raw']; + if (opts.draft) args.push('--no-enter'); const afBinary = resolveAfxBinary(); execFile( process.execPath, - [afBinary, 'send', 'architect', message, '--raw', '--no-enter'], - { cwd: worktreeDir, timeout: 10_000 }, + [afBinary, ...args], + { cwd: opts.worktreeDir, timeout: 10_000 }, (error) => { if (error) { - console.error(`[porch] Gate notification failed: ${error.message}`); + console.error(`[porch] notifyTerminal(${opts.target}) failed: ${error.message}`); } } ); From fffb628ac366b76612e31f71edfbbcde61bd56de Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:23:59 +1000 Subject: [PATCH 09/63] feat(vscode): encode protocol in tree contextValue for menu scoping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tree items in the Builders / Needs Attention views previously used a flat contextValue (`builder`, `blocked-builder`). That gave VSCode no signal about which protocol the builder runs, so every right-click menu had to apply to all builders or none. Now contextValue carries the protocol: `builder-pir`, `builder-bugfix`, `blocked-builder-spir`, etc. The existing six menu items (Open Terminal, Open Worktree, Review Diff, Run Setup, Run Dev, Stop Dev) widen their when-clause regex from `^(builder|blocked-builder)$` to `^(builder|blocked-builder)-` so they continue matching every builder. This unlocks per-protocol menu items (e.g., PIR-only View Plan / View Review) in a follow-up commit — they can scope on `viewItem =~ /^(builder|blocked-builder)-pir$/`. --- packages/vscode/package.json | 12 ++++++------ packages/vscode/src/views/builders.ts | 4 +++- packages/vscode/src/views/needs-attention.ts | 3 ++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 60afb6fc6..8830c8785 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -143,32 +143,32 @@ "view/item/context": [ { "command": "codev.openBuilderById", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "1_terminal@1" }, { "command": "codev.openWorktreeFolder", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "2_files@1" }, { "command": "codev.reviewDiff", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "3_review@1" }, { "command": "codev.runWorktreeSetup", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@1" }, { "command": "codev.runWorktreeDev", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@2" }, { "command": "codev.stopWorktreeDev", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)$/", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@3" } ], diff --git a/packages/vscode/src/views/builders.ts b/packages/vscode/src/views/builders.ts index bb69163d5..d47847d95 100644 --- a/packages/vscode/src/views/builders.ts +++ b/packages/vscode/src/views/builders.ts @@ -22,7 +22,9 @@ export class BuildersProvider implements vscode.TreeDataProvider Date: Wed, 13 May 2026 18:29:33 +1000 Subject: [PATCH 10/63] feat(vscode): View Plan / View Review File for PIR builders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR's plan-approval and code-review gates produce file artifacts on the builder's branch (codev/plans/pir--.md and codev/reviews/pir--.md). At a gate the reviewer needs to read those files, but the worktree dir isn't open in VSCode by default, so finding them was awkward. Adds two commands surfaced as right-click items on PIR builder rows in the Builders / Needs Attention views: - Codev: View Plan File - Codev: View Review File Scoped to PIR via the when-clause `viewItem =~ /^(builder|blocked- builder)-pir$/` (relying on the contextValue from the previous commit). Other protocols don't ship plan/review files at gates so the menu items don't apply to them. The implementation reads /codev/plans|reviews/ and filters to files prefixed with the builder ID. Without that filter the worktree's inherited history surfaces every other builder's plan/review file in the quick-pick. One match → open directly; multiple → quick-pick sorted by mtime; none → friendly toast. --- packages/vscode/package.json | 18 +++ packages/vscode/src/commands/view-artifact.ts | 147 ++++++++++++++++++ packages/vscode/src/extension.ts | 5 + 3 files changed, 170 insertions(+) create mode 100644 packages/vscode/src/commands/view-artifact.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 8830c8785..39477d441 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -127,6 +127,14 @@ { "command": "codev.runWorktreeSetup", "title": "Codev: Run Worktree Setup" + }, + { + "command": "codev.viewPlanFile", + "title": "Codev: View Plan File" + }, + { + "command": "codev.viewReviewFile", + "title": "Codev: View Review File" } ], "menus": { @@ -170,6 +178,16 @@ "command": "codev.stopWorktreeDev", "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@3" + }, + { + "command": "codev.viewPlanFile", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-pir$/", + "group": "3_review@2" + }, + { + "command": "codev.viewReviewFile", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-pir$/", + "group": "3_review@3" } ], "view/title": [ diff --git a/packages/vscode/src/commands/view-artifact.ts b/packages/vscode/src/commands/view-artifact.ts new file mode 100644 index 000000000..f4de5cf3d --- /dev/null +++ b/packages/vscode/src/commands/view-artifact.ts @@ -0,0 +1,147 @@ +/** + * Codev: View Plan File / View Review File — open the markdown artifact a + * gated builder is waiting on (or has just written) directly in a VSCode + * editor tab. + * + * Right-click a builder row → "View Plan File" or "View Review File". + * + * Strategy: locate `/codev/plans/` or `/codev/reviews/`, + * list the `.md` files inside, and: + * - 0 files → friendly message ("no file yet — the builder hasn't written one") + * - 1 file → open it + * - 2+ files → quick-pick (newer files float to the top) + */ + +import * as vscode from 'vscode'; +import { resolve } from 'node:path'; +import { existsSync, readdirSync, statSync } from 'node:fs'; +import type { ConnectionManager } from '../connection-manager.js'; + +type ArtifactKind = 'plan' | 'review'; + +const ARTIFACT_SUBDIR: Record = { + plan: 'codev/plans', + review: 'codev/reviews', +}; + +export function viewPlanFile(connectionManager: ConnectionManager, builderIdArg: string | undefined) { + return viewArtifact(connectionManager, builderIdArg, 'plan'); +} + +export function viewReviewFile(connectionManager: ConnectionManager, builderIdArg: string | undefined) { + return viewArtifact(connectionManager, builderIdArg, 'review'); +} + +async function viewArtifact( + connectionManager: ConnectionManager, + builderIdArg: string | undefined, + kind: ArtifactKind, +): Promise { + const client = connectionManager.getClient(); + const workspacePath = connectionManager.getWorkspacePath(); + if (!client || !workspacePath || connectionManager.getState() !== 'connected') { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + + const overview = await client.getOverview(workspacePath); + const builders = overview?.builders ?? []; + if (builders.length === 0) { + vscode.window.showInformationMessage('Codev: No builders available'); + return; + } + + const builder = builderIdArg + ? builders.find(b => b.id === builderIdArg) + : await pickBuilder(builders, kind); + if (!builder) { + if (builderIdArg) { + vscode.window.showErrorMessage(`Codev: No builder found for "${builderIdArg}"`); + } + return; + } + if (!builder.worktreePath) { + vscode.window.showErrorMessage(`Codev: Builder ${builder.id} has no worktree on record`); + return; + } + + const artifactDir = resolve(builder.worktreePath, ARTIFACT_SUBDIR[kind]); + if (!existsSync(artifactDir)) { + vscode.window.showInformationMessage( + `Codev: No ${kind} file yet — ${ARTIFACT_SUBDIR[kind]}/ doesn't exist in the worktree.`, + ); + return; + } + + // Filter to files belonging to THIS builder. Plan/review files written by + // PIR builders are named `-.md` (e.g. `pir-1298-fix-foo.md`), + // so the builder ID prefix is the natural filter. Other builders' files for + // the same protocol live in the same dir and would otherwise show up in the + // quick-pick. + const builderPrefix = `${builder.id}-`; + const files = readdirSync(artifactDir) + .filter(f => + f.endsWith('.md') && + (f.startsWith(builderPrefix) || f === `${builder.id}.md`) + ) + .map(f => ({ name: f, path: resolve(artifactDir, f), mtime: safeMtime(resolve(artifactDir, f)) })) + .sort((a, b) => b.mtime - a.mtime); + + if (files.length === 0) { + vscode.window.showInformationMessage( + `Codev: No ${kind} file for builder ${builder.id} yet — the builder hasn't written one.`, + ); + return; + } + + let target: string; + if (files.length === 1) { + target = files[0].path; + } else { + const picked = await vscode.window.showQuickPick( + files.map(f => ({ label: f.name, description: relativeTime(f.mtime), path: f.path })), + { placeHolder: `Select ${kind} file to open` }, + ); + if (!picked) { + return; + } + target = picked.path; + } + + const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(target)); + await vscode.window.showTextDocument(doc, { preview: false }); +} + +function safeMtime(path: string): number { + try { + return statSync(path).mtimeMs; + } catch { + return 0; + } +} + +function relativeTime(mtime: number): string { + if (mtime === 0) { return ''; } + const seconds = Math.floor((Date.now() - mtime) / 1000); + if (seconds < 60) { return `${seconds}s ago`; } + if (seconds < 3600) { return `${Math.floor(seconds / 60)}m ago`; } + if (seconds < 86400) { return `${Math.floor(seconds / 3600)}h ago`; } + return `${Math.floor(seconds / 86400)}d ago`; +} + +interface BuilderLike { + id: string; + issueId: string | null; + issueTitle: string | null; +} + +async function pickBuilder(builders: T[], kind: ArtifactKind): Promise { + const picked = await vscode.window.showQuickPick( + builders.map(b => ({ + label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, + builder: b, + })), + { placeHolder: `Select builder whose ${kind} file to open` }, + ); + return picked?.builder; +} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index a5122bca5..a0a11e0dd 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -11,6 +11,7 @@ import { runWorktreeDev } from './commands/run-worktree-dev.js'; import { stopWorktreeDev } from './commands/stop-worktree-dev.js'; import { openWorktreeFolder } from './commands/open-worktree-folder.js'; import { runWorktreeSetup } from './commands/run-worktree-setup.js'; +import { viewPlanFile, viewReviewFile } from './commands/view-artifact.js'; import { connectTunnel, disconnectTunnel } from './commands/tunnel.js'; import { listCronTasks } from './commands/cron.js'; import { addReviewComment } from './commands/review.js'; @@ -218,6 +219,10 @@ export async function activate(context: vscode.ExtensionContext) { openWorktreeFolder(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.runWorktreeSetup', (arg: vscode.TreeItem | string | undefined) => runWorktreeSetup(connectionManager!, extractBuilderId(arg))), + vscode.commands.registerCommand('codev.viewPlanFile', (arg: vscode.TreeItem | string | undefined) => + viewPlanFile(connectionManager!, extractBuilderId(arg))), + vscode.commands.registerCommand('codev.viewReviewFile', (arg: vscode.TreeItem | string | undefined) => + viewReviewFile(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.refreshOverview', () => overviewCache.refresh()), vscode.commands.registerCommand('codev.reconnect', () => connectionManager?.reconnect()), vscode.commands.registerCommand('codev.connectTunnel', () => connectTunnel(connectionManager!)), From 44f1298f39f23264f989b44dab4b7afbe8b949ee Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:32:35 +1000 Subject: [PATCH 11/63] feat(vscode): make state-change commands await results and update sidebar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three state-changing tree commands (approve, cleanup, send-message) previously fired their CLI work as detached child processes with `stdio: ignore`. Three consequences: 1. Errors were silently swallowed. `porch approve` could fail (bad gate name, missing flag) and the user would see only "Approving..." with no failure feedback. 2. The sidebar didn't refresh after approve / cleanup. The just- approved gate kept the builder in the Needs Attention tree until the next SSE tick (looked like the action didn't take); a cleaned-up builder lingered in the Builders tree for several seconds — that was the bug you noticed earlier. 3. The architect's conversation history drifted out of sync. Porch orchestrates through the architect, but user-initiated VSCode actions (approve, cleanup, send) weren't reaching it, so the architect's mental model of the workspace lagged behind reality. Now each command: - awaits the CLI via `execFile` (promisified) and surfaces errors via showErrorMessage, - calls `OverviewCache.refresh()` so the tree updates immediately, - fires a one-line `afx send architect` breadcrumb describing what the user just did. The architect-bound `afx send` calls are best-effort (`.catch(noop)`) — the architect terminal may not be live in all workflows. `approveGate` and `cleanupBuilder` take the OverviewCache as an optional second arg so the call sites can opt in to refresh; existing test setups that constructed these commands without a cache continue to work. --- packages/vscode/src/commands/approve.ts | 61 ++++++++++++++++++++++--- packages/vscode/src/commands/cleanup.ts | 51 ++++++++++++++++++--- packages/vscode/src/commands/send.ts | 27 +++++++++-- packages/vscode/src/extension.ts | 4 +- 4 files changed, 125 insertions(+), 18 deletions(-) diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index dc4c15623..5ac8496ae 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -1,11 +1,31 @@ import * as vscode from 'vscode'; -import { spawn } from 'node:child_process'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; import type { ConnectionManager } from '../connection-manager.js'; +import type { OverviewCache } from '../views/overview-data.js'; + +const execFileAsync = promisify(execFile); /** * Codev: Approve Gate — show blocked builders, pick one, approve via porch CLI. + * + * After a successful `porch approve`, two follow-ups fire: + * + * 1. **Wake-up nudge** — `afx send "Gate approved..."` typed + * into the builder's PTY. The builder is alive in interactive mode and + * reads this as its next user input; on its next turn it calls + * `porch next`, sees the gate is approved, and advances. Without this + * nudge the builder would sit idle until the user typed something into + * the pane themselves. + * + * 2. **Cache refresh** — `OverviewCache.refresh()` invalidates the Needs + * Attention tree immediately rather than waiting for the next SSE-driven + * tick. Eliminates the brief "still blocked" flash after approving. */ -export async function approveGate(connectionManager: ConnectionManager): Promise { +export async function approveGate( + connectionManager: ConnectionManager, + cache?: OverviewCache, +): Promise { const client = connectionManager.getClient(); const workspacePath = connectionManager.getWorkspacePath(); if (!client || !workspacePath || connectionManager.getState() !== 'connected') { @@ -31,10 +51,37 @@ export async function approveGate(connectionManager: ConnectionManager): Promise ); if (!picked) { return; } - const child = spawn('porch', ['approve', picked.id, picked.gate, '--a-human-explicitly-approved-this'], { - detached: true, - stdio: 'ignore', + try { + await execFileAsync('porch', [ + 'approve', + picked.id, + picked.gate, + '--a-human-explicitly-approved-this', + ]); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Codev: porch approve failed — ${msg}`); + return; + } + + vscode.window.showInformationMessage(`Codev: Approved ${picked.gate} for #${picked.id}`); + + // Note: porch approve itself fires a notifyTerminal wake-up to the builder; + // no need to duplicate that here. + + // Notify the architect. Porch fires notifyTerminal(architect) when a gate + // becomes pending, but not when it transitions to approved — so without this + // breadcrumb, the architect's view of the protocol state goes stale. + // This `afx send architect` line keeps the architect's conversation + // history in sync with what the user did via VSCode. + execFileAsync('afx', [ + 'send', + 'architect', + `User approved ${picked.gate} for ${picked.id} via VSCode.`, + ]).catch(() => { + // Best-effort — architect may not be running in some workflows. }); - child.unref(); - vscode.window.showInformationMessage(`Codev: Approving ${picked.gate} for #${picked.id}`); + + // Refresh the cache so Needs Attention updates without waiting for SSE. + cache?.refresh(); } diff --git a/packages/vscode/src/commands/cleanup.ts b/packages/vscode/src/commands/cleanup.ts index 2f697e14f..faba9914b 100644 --- a/packages/vscode/src/commands/cleanup.ts +++ b/packages/vscode/src/commands/cleanup.ts @@ -1,11 +1,31 @@ import * as vscode from 'vscode'; -import { spawn } from 'node:child_process'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; import type { ConnectionManager } from '../connection-manager.js'; +import type { OverviewCache } from '../views/overview-data.js'; + +const execFileAsync = promisify(execFile); /** - * Codev: Cleanup Builder — pick builder, run afx cleanup. + * Codev: Cleanup Builder — pick builder, run `afx cleanup`, then refresh the + * sidebar so the removed builder disappears immediately. + * + * After successful cleanup, fires three side effects in order: + * + * 1. Show success / error toast based on the actual exit status (the old + * fire-and-forget spawn silently swallowed errors). + * 2. Notify the architect — cleanup removes a builder from porch's view, + * which is a state change worth recording in the architect's + * conversation history. + * 3. Refresh OverviewCache so the Needs Attention and Builders trees + * drop the removed entry without waiting for the next SSE tick. This + * fixes the user-visible bug where a cleaned-up builder lingered in + * the sidebar. */ -export async function cleanupBuilder(connectionManager: ConnectionManager): Promise { +export async function cleanupBuilder( + connectionManager: ConnectionManager, + cache?: OverviewCache, +): Promise { const client = connectionManager.getClient(); const workspacePath = connectionManager.getWorkspacePath(); if (!client || !workspacePath || connectionManager.getState() !== 'connected') { @@ -30,7 +50,26 @@ export async function cleanupBuilder(connectionManager: ConnectionManager): Prom ); if (!picked) { return; } - const child = spawn('afx', ['cleanup', '-p', picked.id], { detached: true, stdio: 'ignore' }); - child.unref(); - vscode.window.showInformationMessage(`Codev: Cleaning up builder #${picked.id}`); + try { + await execFileAsync('afx', ['cleanup', '-p', picked.id]); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + vscode.window.showErrorMessage(`Codev: afx cleanup failed — ${msg}`); + return; + } + + vscode.window.showInformationMessage(`Codev: Cleaned up builder #${picked.id}`); + + // Architect breadcrumb — cleanup removes a builder from porch's view; + // the architect should know. + execFileAsync('afx', [ + 'send', + 'architect', + `User cleaned up builder ${picked.id} via VSCode.`, + ]).catch(() => { + // Best-effort. + }); + + // Refresh the cache so the sidebar drops the removed builder immediately. + cache?.refresh(); } diff --git a/packages/vscode/src/commands/send.ts b/packages/vscode/src/commands/send.ts index 850ddb245..de86799c4 100644 --- a/packages/vscode/src/commands/send.ts +++ b/packages/vscode/src/commands/send.ts @@ -1,8 +1,17 @@ import * as vscode from 'vscode'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; import type { ConnectionManager } from '../connection-manager.js'; +const execFileAsync = promisify(execFile); + /** * Codev: Send Message — pick builder, type message, send via TowerClient. + * + * After successful send, fires a breadcrumb to the architect so its view of + * the protocol state stays in sync. Porch orchestrates through the architect; + * a builder's behavior changing in response to user feedback is a fact the + * architect should know about without having to poll porch state. */ export async function sendMessage(connectionManager: ConnectionManager): Promise { const client = connectionManager.getClient(); @@ -32,9 +41,21 @@ export async function sendMessage(connectionManager: ConnectionManager): Promise if (!message) { return; } const result = await client.sendMessage(picked.id, message, { workspace: workspacePath }); - if (result.ok) { - vscode.window.showInformationMessage(`Codev: Message sent to ${picked.label}`); - } else { + if (!result.ok) { vscode.window.showErrorMessage(`Codev: Failed to send — ${result.error}`); + return; } + + vscode.window.showInformationMessage(`Codev: Message sent to ${picked.label}`); + + // Architect breadcrumb. Short preview (first 80 chars) — full text lives + // in the builder's pane; architect only needs the gist + target builder. + const preview = message.length > 80 ? `${message.slice(0, 77)}...` : message; + execFileAsync('afx', [ + 'send', + 'architect', + `User sent feedback to ${picked.id} via VSCode: "${preview}"`, + ]).catch(() => { + // Best-effort. + }); } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index a0a11e0dd..6e0a8f4a1 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -207,8 +207,8 @@ export async function activate(context: vscode.ExtensionContext) { }), vscode.commands.registerCommand('codev.spawnBuilder', () => spawnBuilder()), vscode.commands.registerCommand('codev.sendMessage', () => sendMessage(connectionManager!)), - vscode.commands.registerCommand('codev.approveGate', () => approveGate(connectionManager!)), - vscode.commands.registerCommand('codev.cleanupBuilder', () => cleanupBuilder(connectionManager!)), + vscode.commands.registerCommand('codev.approveGate', () => approveGate(connectionManager!, overviewCache)), + vscode.commands.registerCommand('codev.cleanupBuilder', () => cleanupBuilder(connectionManager!, overviewCache)), vscode.commands.registerCommand('codev.reviewDiff', (arg: vscode.TreeItem | string | undefined) => reviewDiff(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.runWorktreeDev', (arg: vscode.TreeItem | string | undefined) => From c59bb0b05217b2f45aa019587fd1cb0b19f62bb9 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:35:40 +1000 Subject: [PATCH 12/63] feat(vscode): toast when a builder reaches a human-approval gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a passive notification path for gated protocols (SPIR, ASPIR, PIR). Without it, the only signal a builder needs review is the Needs Attention tree updating — which requires the user to be looking at the sidebar. The toast surfaces the gate without forcing attention there. Behavior: - Subscribes to OverviewCache changes. On each tick, diffs the blocked-builder set against a module-local seen-set keyed by (builderId, gateName). - New entries fire showInformationMessage with a "Review" action that opens the architect terminal — porch orchestrates through the architect, so the human-driven response starts there. (Artifact- specific entry points — View Diff, View Plan File, Run Dev Server — remain on right-click of the builder row.) - Entries leaving the blocked set are pruned so re-blocking later on a different gate re-toasts. - Respects the new `codev.gateToasts.enabled` setting (default true). No-op for BUGFIX / AIR builders — they have no gates, so they never enter the blocked set and never trigger toasts. --- packages/vscode/package.json | 5 + packages/vscode/src/extension.ts | 5 + .../vscode/src/notifications/gate-toast.ts | 94 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 packages/vscode/src/notifications/gate-toast.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 39477d441..53e461c4d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -311,6 +311,11 @@ ], "default": "notify", "description": "Behavior when Tower reports a new builder spawn" + }, + "codev.gateToasts.enabled": { + "type": "boolean", + "default": true, + "description": "Show a VSCode notification toast when a builder reaches a human-approval gate (plan-approval, code-review, etc.)" } } } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 6e0a8f4a1..d1a444c8c 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -15,6 +15,7 @@ import { viewPlanFile, viewReviewFile } from './commands/view-artifact.js'; import { connectTunnel, disconnectTunnel } from './commands/tunnel.js'; import { listCronTasks } from './commands/cron.js'; import { addReviewComment } from './commands/review.js'; +import { activateGateToasts } from './notifications/gate-toast.js'; import { activateReviewDecorations } from './review-decorations.js'; import { BuilderSpawnHandler } from './builder-spawn-handler.js'; import { BuilderTerminalLinkProvider } from './terminal-link-provider.js'; @@ -234,6 +235,10 @@ export async function activate(context: vscode.ExtensionContext) { // Review comment decorations activateReviewDecorations(context); + // Toast on new gate-pending — surfaces blocked builders without forcing the + // user to watch the Needs Attention tree. Respects `codev.gateToasts.enabled`. + activateGateToasts(context, overviewCache); + // Auto-open builder terminals on Tower spawn events const builderSpawnHandler = new BuilderSpawnHandler(connectionManager, terminalManager, outputChannel); context.subscriptions.push( diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts new file mode 100644 index 000000000..1be76972a --- /dev/null +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -0,0 +1,94 @@ +import * as vscode from 'vscode'; +import type { OverviewCache } from '../views/overview-data.js'; + +/** + * Toast notifications for newly blocked builders. + * + * Subscribes to OverviewCache changes. Whenever a builder appears in the + * blocked-set for the first time (or its gate name changes), fires an + * `showInformationMessage` toast with a single "Review" action that opens + * the **architect** terminal — porch orchestrates through the architect, + * so user-driven review starts there. + * + * (Direct artifact access — View Diff, View Plan File, View Review File, + * Run Dev Server — is available via right-click on builder rows in the + * sidebar. The toast intentionally does not duplicate those entry points.) + * + * A `(builderId, gateName)` seen-set is kept in module state so we never + * re-toast the same blocked state on subsequent cache ticks. The seen-set + * is pruned when an entry leaves the blocked set (gate approved or builder + * advances) so that re-blocking later (on a different gate) will re-toast. + * + * Respects the `codev.gateToasts.enabled` setting (default: true). Set to + * false to silence; status bar counters and the Needs Attention tree + * remain unaffected. + */ +export function activateGateToasts( + context: vscode.ExtensionContext, + cache: OverviewCache, +): void { + // Track (builderId, gateName) pairs we've already toasted for. + const seen = new Set(); + + const onChange = () => { + const enabled = vscode.workspace + .getConfiguration('codev') + .get('gateToasts.enabled', true); + if (!enabled) { + return; + } + + const data = cache.getData(); + if (!data) { + return; + } + + const currentBlocked = new Set(); + for (const b of data.builders) { + if (!b.blocked) { + continue; + } + const key = `${b.id}::${b.blocked}`; + currentBlocked.add(key); + if (!seen.has(key)) { + seen.add(key); + showGateToast(b.id, b.blocked, b.issueId, b.issueTitle); + } + } + + // Prune entries that are no longer blocked so we re-toast on future blocks. + for (const key of [...seen]) { + if (!currentBlocked.has(key)) { + seen.delete(key); + } + } + }; + + context.subscriptions.push(cache.onDidChange(onChange)); +} + +function showGateToast( + builderId: string, + gateName: string, + issueId?: string | number | null, + issueTitle?: string | null, +): void { + const label = issueId ? `#${issueId}` : builderId; + const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; + const message = `Codev: ${label} blocked on ${gateName}${titleSuffix}`; + + // Fire and forget. "Review" opens the architect terminal — porch already + // notified the architect terminal about this gate, so the architect pane + // has the context. User talks to the architect from there. + vscode.window + .showInformationMessage(message, 'Review') + .then((selection) => { + if (selection === 'Review') { + vscode.commands.executeCommand('codev.openArchitectTerminal'); + } + }); +} + +function truncate(s: string, max: number): string { + return s.length > max ? `${s.slice(0, max - 1)}…` : s; +} From d372c0bc479fb60b6283970a44cc4c940f880553 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:36:09 +1000 Subject: [PATCH 13/63] test(porch): lock in PIR protocol shape Three phases (plan / implement / review), two gates (plan-approval on plan, code-review on implement), review phase terminal (next: null). Synthesizes a minimal PIR-shaped protocol on disk and runs it through loadProtocol so the contract is enforced by `pnpm test` regardless of working-tree state of codev/protocols/pir/. --- .../commands/porch/__tests__/protocol.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/packages/codev/src/commands/porch/__tests__/protocol.test.ts b/packages/codev/src/commands/porch/__tests__/protocol.test.ts index 5bfe9a7e1..ef2fcacec 100644 --- a/packages/codev/src/commands/porch/__tests__/protocol.test.ts +++ b/packages/codev/src/commands/porch/__tests__/protocol.test.ts @@ -254,3 +254,103 @@ describe('porch protocol loading', () => { }); }); }); + +/** + * PIR-specific protocol shape tests (Issue 691). + * + * PIR's contract is meaningful enough that we lock it in here: + * plan → gated by 'plan-approval' → next 'implement' + * implement → gated by 'code-review' → next 'review' + * review → no gate, terminal (next: null) + * + * The PIR protocol.json itself lives at codev/protocols/pir/ in the repo; + * to keep these tests independent of working-tree state, we synthesize a + * minimal PIR-shaped protocol and verify loadProtocol parses it correctly. + */ +describe('PIR protocol shape', () => { + const testDir = path.join(tmpdir(), `porch-pir-test-${Date.now()}`); + const pirDir = path.join(testDir, 'codev/protocols/pir'); + + const pirProtocol = { + name: 'pir', + alias: 'plan-implement-review', + version: '1.0.0', + description: 'PIR: Plan → Implement → Review for GitHub-issue-driven work with two human gates', + input: { type: 'github-issue', required: true }, + phases: [ + { + id: 'plan', + name: 'Plan', + type: 'build_verify', + gate: 'plan-approval', + next: 'implement', + }, + { + id: 'implement', + name: 'Implement', + type: 'build_verify', + gate: 'code-review', + next: 'review', + }, + { + id: 'review', + name: 'Review', + type: 'build_verify', + next: null, + }, + ], + }; + + beforeEach(() => { + fs.mkdirSync(pirDir, { recursive: true }); + fs.writeFileSync( + path.join(pirDir, 'protocol.json'), + JSON.stringify(pirProtocol, null, 2), + ); + }); + + afterEach(() => { + if (fs.existsSync(testDir)) { + fs.rmSync(testDir, { recursive: true }); + } + }); + + it('loads and normalizes the three PIR phases', () => { + const protocol = loadProtocol(testDir, 'pir'); + + expect(protocol.name).toBe('pir'); + expect(protocol.phases).toHaveLength(3); + expect(protocol.phases.map(p => p.id)).toEqual(['plan', 'implement', 'review']); + }); + + it('gates the plan phase on plan-approval and transitions to implement', () => { + const protocol = loadProtocol(testDir, 'pir'); + expect(getPhaseGate(protocol, 'plan')).toBe('plan-approval'); + expect(getNextPhase(protocol, 'plan')?.id).toBe('implement'); + }); + + it('gates the implement phase on code-review and transitions to review', () => { + const protocol = loadProtocol(testDir, 'pir'); + expect(getPhaseGate(protocol, 'implement')).toBe('code-review'); + expect(getNextPhase(protocol, 'implement')?.id).toBe('review'); + }); + + it('leaves the review phase ungated and terminal', () => { + const protocol = loadProtocol(testDir, 'pir'); + expect(getPhaseGate(protocol, 'review')).toBeNull(); + expect(getNextPhase(protocol, 'review')).toBeNull(); + }); + + it('resolves the plan-implement-review alias', () => { + const protocol = loadProtocol(testDir, 'plan-implement-review'); + expect(protocol.name).toBe('pir'); + }); + + it('treats code-review as a valid gate name (no whitelist)', () => { + // Sanity check: porch must accept new gate names purely from data. If a + // whitelist were ever added, this test would break before PIR ships. + const protocol = loadProtocol(testDir, 'pir'); + const gate = getPhaseGate(protocol, 'implement'); + expect(gate).toBe('code-review'); + }); +}); From d367df8df7e8f4cab6572a8af7a961937a3b6877 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 18:38:55 +1000 Subject: [PATCH 14/63] docs: add PIR to CLAUDE.md / AGENTS.md + file-resolution guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three pieces to the source-repo CLAUDE.md / AGENTS.md and the skeleton templates that ship to consumer projects: 1. PIR entry in the Available Protocols list — one-line description that disambiguates it from SPIR (lighter, two gates instead of three) and BUGFIX/AIR (stronger, has gates). 2. "Use PIR for" selection guide — paired criteria. Either the approach needs design review before coding (high blast radius, ambiguous root cause) OR the implementation needs to be tested before a PR exists (mobile, UI, hardware-adjacent, OAuth). One trigger is enough. 3. File Resolution section — documents the four-tier lookup chain (.codev → codev → cache → installed skeleton) and warns against the recurring failure mode during `codev update` merges, where AI agents drop a protocol reference because `codev/protocols//` doesn't exist locally. It resolves from the package skeleton; the reference must stay. 4. Protocol Verification section — when in doubt about a protocol name, `afx spawn --protocol --help` is the source of truth (it resolves via the same four-tier chain). Skeleton template additions (codev-skeleton/templates/) are a subset of the source-repo additions — no source-repo-specific paragraphs. --- AGENTS.md | 44 ++++++++++++++++++++++++++++++ CLAUDE.md | 44 ++++++++++++++++++++++++++++++ codev-skeleton/templates/AGENTS.md | 24 ++++++++++++++++ codev-skeleton/templates/CLAUDE.md | 24 ++++++++++++++++ 4 files changed, 136 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 061453e2b..78da848dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,10 +70,34 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` +### File Resolution (How Codev Finds Protocols and Templates) + +Codev resolves protocol files, prompts, agent definitions, and roles through a four-tier lookup (highest priority first): + +1. `.codev/` — user override (project-local customization) +2. `codev/` — project-local copy (customized and checked in) +3. Runtime cache +4. **Installed package skeleton** — ships with `@cluesmith/codev` (the default for every standard protocol) + +**The absence of `codev/protocols//` on disk is not a missing reference** — it's the normal case for any protocol you haven't customized. The protocol resolves from the installed package's skeleton at runtime. Only protocols you want to customize need to live in your repo's `codev/protocols/`. + +**Implication for `codev update` and CLAUDE.md / AGENTS.md merges:** when an updated template references a protocol (e.g., PIR), do NOT drop the reference because `codev/protocols//` is absent locally. The protocol resolves via the package skeleton, and dropping the reference removes the protocol from the user's available-protocol list while it's still callable from the CLI. + +### Protocol Verification (When You Don't Recognize a Protocol Name) + +If the user mentions a protocol name you don't immediately recognize, verify against the CLI before responding: + +```bash +afx spawn --protocol --help +``` + +This succeeds if the protocol is registered (including via the skeleton fallback in tier 4 of the resolution chain) and errors helpfully otherwise. The CLI is the source of truth — defer to it when in doubt. + Key locations: - Protocol details: `codev/protocols/` (Choose appropriate protocol) - **Project tracking**: GitHub Issues (source of truth for all projects) @@ -144,6 +168,26 @@ validated: [gemini, codex, claude] **AIR uses GitHub Issues as source of truth.** Two phases: Implement → Review. See `codev/protocols/air/protocol.md`. +### Use PIR for (engineer-judged — based on the nature of the work, not its size): + +Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change: + +**1. The approach needs review before coding starts**: +- Root cause is ambiguous; multiple valid fixes exist +- Area is unfamiliar or high-blast-radius (shared utilities, auth, migrations, public APIs) +- Design-sensitive (affects conventions, patterns, architecture) +- Cheaper to redirect at plan time than at PR time + +**2. The implementation needs to be TESTED before a PR is created** (PR diff alone is insufficient): +- Mobile app changes (needs device testing on Android, iOS, possibly web) +- UI / UX changes (visual inspection, interaction flow, accessibility) +- Hardware-adjacent behavior (sensors, camera, permissions, notifications) +- Integration with external services that don't mock cleanly (OAuth, payments, analytics) +- User-journey changes that need a full-flow exercise +- Performance-sensitive changes that need profiling on the running app + +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `code-review`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `code-review` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. + ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) - New protocols or protocol variants diff --git a/CLAUDE.md b/CLAUDE.md index 061453e2b..78da848dc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,10 +70,34 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` +### File Resolution (How Codev Finds Protocols and Templates) + +Codev resolves protocol files, prompts, agent definitions, and roles through a four-tier lookup (highest priority first): + +1. `.codev/` — user override (project-local customization) +2. `codev/` — project-local copy (customized and checked in) +3. Runtime cache +4. **Installed package skeleton** — ships with `@cluesmith/codev` (the default for every standard protocol) + +**The absence of `codev/protocols//` on disk is not a missing reference** — it's the normal case for any protocol you haven't customized. The protocol resolves from the installed package's skeleton at runtime. Only protocols you want to customize need to live in your repo's `codev/protocols/`. + +**Implication for `codev update` and CLAUDE.md / AGENTS.md merges:** when an updated template references a protocol (e.g., PIR), do NOT drop the reference because `codev/protocols//` is absent locally. The protocol resolves via the package skeleton, and dropping the reference removes the protocol from the user's available-protocol list while it's still callable from the CLI. + +### Protocol Verification (When You Don't Recognize a Protocol Name) + +If the user mentions a protocol name you don't immediately recognize, verify against the CLI before responding: + +```bash +afx spawn --protocol --help +``` + +This succeeds if the protocol is registered (including via the skeleton fallback in tier 4 of the resolution chain) and errors helpfully otherwise. The CLI is the source of truth — defer to it when in doubt. + Key locations: - Protocol details: `codev/protocols/` (Choose appropriate protocol) - **Project tracking**: GitHub Issues (source of truth for all projects) @@ -144,6 +168,26 @@ validated: [gemini, codex, claude] **AIR uses GitHub Issues as source of truth.** Two phases: Implement → Review. See `codev/protocols/air/protocol.md`. +### Use PIR for (engineer-judged — based on the nature of the work, not its size): + +Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change: + +**1. The approach needs review before coding starts**: +- Root cause is ambiguous; multiple valid fixes exist +- Area is unfamiliar or high-blast-radius (shared utilities, auth, migrations, public APIs) +- Design-sensitive (affects conventions, patterns, architecture) +- Cheaper to redirect at plan time than at PR time + +**2. The implementation needs to be TESTED before a PR is created** (PR diff alone is insufficient): +- Mobile app changes (needs device testing on Android, iOS, possibly web) +- UI / UX changes (visual inspection, interaction flow, accessibility) +- Hardware-adjacent behavior (sensors, camera, permissions, notifications) +- Integration with external services that don't mock cleanly (OAuth, payments, analytics) +- User-journey changes that need a full-flow exercise +- Performance-sensitive changes that need profiling on the running app + +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `code-review`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `code-review` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. + ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) - New protocols or protocol variants diff --git a/codev-skeleton/templates/AGENTS.md b/codev-skeleton/templates/AGENTS.md index d9dbe7953..abd781a6e 100644 --- a/codev-skeleton/templates/AGENTS.md +++ b/codev-skeleton/templates/AGENTS.md @@ -12,10 +12,34 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) +## File Resolution (How Codev Finds Protocols and Templates) + +Codev resolves protocol files, prompts, agent definitions, and roles through a four-tier lookup (highest priority first): + +1. `.codev/` — user override (project-local customization) +2. `codev/` — project-local copy (customized and checked in) +3. Runtime cache +4. **Installed package skeleton** — ships with `@cluesmith/codev` (the default for every standard protocol) + +**The absence of `codev/protocols//` on disk is not a missing reference** — it's the normal case for any protocol you haven't customized. The protocol resolves from the installed package's skeleton at runtime. Only protocols you want to customize need to live in your repo's `codev/protocols/`. + +**Implication for `codev update` and CLAUDE.md / AGENTS.md merges:** when an updated template references a protocol, do NOT drop the reference because `codev/protocols//` is absent locally. The protocol resolves via the package skeleton, and dropping the reference removes it from your available-protocol list while it's still callable from the CLI. + +## Protocol Verification (When You Don't Recognize a Protocol Name) + +If the user mentions a protocol name you don't immediately recognize, verify against the CLI before responding: + +```bash +afx spawn --protocol --help +``` + +This succeeds if the protocol is registered (including via the skeleton fallback in tier 4 of the resolution chain) and errors helpfully otherwise. The CLI is the source of truth — defer to it when in doubt. + ## Key Locations - **Specs**: `codev/specs/` - Feature specifications (WHAT to build) diff --git a/codev-skeleton/templates/CLAUDE.md b/codev-skeleton/templates/CLAUDE.md index 3aa624b09..2a39cf736 100644 --- a/codev-skeleton/templates/CLAUDE.md +++ b/codev-skeleton/templates/CLAUDE.md @@ -10,10 +10,34 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) +## File Resolution (How Codev Finds Protocols and Templates) + +Codev resolves protocol files, prompts, agent definitions, and roles through a four-tier lookup (highest priority first): + +1. `.codev/` — user override (project-local customization) +2. `codev/` — project-local copy (customized and checked in) +3. Runtime cache +4. **Installed package skeleton** — ships with `@cluesmith/codev` (the default for every standard protocol) + +**The absence of `codev/protocols//` on disk is not a missing reference** — it's the normal case for any protocol you haven't customized. The protocol resolves from the installed package's skeleton at runtime. Only protocols you want to customize need to live in your repo's `codev/protocols/`. + +**Implication for `codev update` and CLAUDE.md / AGENTS.md merges:** when an updated template references a protocol, do NOT drop the reference because `codev/protocols//` is absent locally. The protocol resolves via the package skeleton, and dropping the reference removes it from your available-protocol list while it's still callable from the CLI. + +## Protocol Verification (When You Don't Recognize a Protocol Name) + +If the user mentions a protocol name you don't immediately recognize, verify against the CLI before responding: + +```bash +afx spawn --protocol --help +``` + +This succeeds if the protocol is registered (including via the skeleton fallback in tier 4 of the resolution chain) and errors helpfully otherwise. The CLI is the source of truth — defer to it when in doubt. + ## Key Locations - **Specs**: `codev/specs/` - Feature specifications (WHAT to build) From ff053152125f4f9d9d67e4c93ac4b8645d8ffca0 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 20:29:04 +1000 Subject: [PATCH 15/63] refactor(porch, vscode): drop architect-bound gate notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR / SPIR / ASPIR gates are explicit human-decision points: the user reads the plan file at plan-approval, runs the worktree at code-review, checks production at verify-approval. The architect can't act on any of these autonomously — approval requires the human `--a-human-explicitly-approved-this` flag — so pushing gate state into its conversation history only added noise. The toast (commit c59bb0b0) and the Needs Attention tree already cover the "user needs to know" path natively. Symmetrically, the VSCode-side breadcrumbs that fired on every user approve / cleanup / send (commit 44f1298f) were adding noise of the same shape: the user is already at the keyboard when these fire, and the architect can pull state via `porch status` / `porch pending` when asked. Push wasn't earning its slot in the architect's context window. Net effect: - notifyTerminal kept, but only the builder wake-up after approval uses it (genuinely needed — the builder's idle Claude session has no other input event to advance on). - gatePendingMessage helper removed entirely (dead code). - Architect notification call sites removed from porch/index.ts (3 sites) and porch/next.ts (1 site). - afx-send-architect breadcrumbs removed from approve.ts / cleanup.ts / send.ts; OverviewCache.refresh() retained (different concern — sidebar responsiveness, not architect awareness). Side effect: the `code-review`-vs-PR semantic clash dissolves. The architect never sees `code-review` so it never goes hunting for a PR that doesn't exist yet. --- .../commands/porch/__tests__/notify.test.ts | 66 +++++-------------- packages/codev/src/commands/porch/index.ts | 25 +------ packages/codev/src/commands/porch/next.ts | 7 -- packages/codev/src/commands/porch/notify.ts | 45 +++++-------- packages/vscode/src/commands/approve.ts | 33 ++-------- packages/vscode/src/commands/cleanup.ts | 22 ++----- packages/vscode/src/commands/send.ts | 20 ------ .../vscode/src/notifications/gate-toast.ts | 6 +- 8 files changed, 49 insertions(+), 175 deletions(-) diff --git a/packages/codev/src/commands/porch/__tests__/notify.test.ts b/packages/codev/src/commands/porch/__tests__/notify.test.ts index dcd831456..4694ce455 100644 --- a/packages/codev/src/commands/porch/__tests__/notify.test.ts +++ b/packages/codev/src/commands/porch/__tests__/notify.test.ts @@ -1,9 +1,9 @@ /** - * Tests for notifyTerminal (Spec 0108 generalized) + * Tests for notifyTerminal — the builder wake-up after gate approval. * - * Verifies that porch sends gate notifications via `afx send` and that - * failures are swallowed (fire-and-forget). Also covers the message-builder - * helpers used by both architect-pending and builder-approval flows. + * Architect-bound notifications were removed deliberately; the only caller + * of notifyTerminal today is the gate-approve path, which wakes the builder + * so its idle Claude session advances on the next turn. */ import { describe, it, expect, beforeEach, vi } from 'vitest'; @@ -19,7 +19,7 @@ vi.mock('node:child_process', () => ({ })); import { execFile } from 'node:child_process'; -import { notifyTerminal, gatePendingMessage, gateApprovedMessage } from '../notify.js'; +import { notifyTerminal, gateApprovedMessage } from '../notify.js'; const mockExecFile = vi.mocked(execFile); @@ -36,8 +36,8 @@ describe('notifyTerminal', () => { it('routes message to the named target via afx send', () => { notifyTerminal({ - target: 'architect', - message: 'hello', + target: 'pir-0108', + message: 'wake up', worktreeDir: '/projects/test', }); @@ -45,36 +45,13 @@ describe('notifyTerminal', () => { const [cmd, args, opts] = mockExecFile.mock.calls[0]; expect(cmd).toBe(process.execPath); expect(args).toContain('send'); - expect(args).toContain('architect'); - expect(args).toContain('hello'); + expect(args).toContain('pir-0108'); + expect(args).toContain('wake up'); expect(args).toContain('--raw'); expect((opts as { cwd: string }).cwd).toBe('/projects/test'); }); - it('appends --no-enter when draft is true', () => { - notifyTerminal({ - target: 'architect', - message: 'pending', - worktreeDir: '/projects/test', - draft: true, - }); - - const args = mockExecFile.mock.calls[0][1]!; - expect(args).toContain('--no-enter'); - }); - - it('omits --no-enter when draft is false/undefined', () => { - notifyTerminal({ - target: 'pir-0108', - message: 'wake up', - worktreeDir: '/projects/test', - }); - - const args = mockExecFile.mock.calls[0][1]!; - expect(args).not.toContain('--no-enter'); - }); - - it('uses the builder id as target for wake-ups', () => { + it('submits as a regular message (no --no-enter)', () => { notifyTerminal({ target: 'pir-0108', message: 'approved', @@ -82,12 +59,12 @@ describe('notifyTerminal', () => { }); const args = mockExecFile.mock.calls[0][1]!; - expect(args).toContain('pir-0108'); + expect(args).not.toContain('--no-enter'); }); it('sets timeout to 10 seconds', () => { notifyTerminal({ - target: 'architect', + target: 'pir-0108', message: 'x', worktreeDir: '/projects/test', }); @@ -98,7 +75,7 @@ describe('notifyTerminal', () => { it('sets cwd to worktreeDir', () => { notifyTerminal({ - target: 'architect', + target: 'pir-0108', message: 'x', worktreeDir: '/my/worktree', }); @@ -118,11 +95,11 @@ describe('notifyTerminal', () => { ); expect(() => - notifyTerminal({ target: 'architect', message: 'x', worktreeDir: '/p' }) + notifyTerminal({ target: 'pir-0108', message: 'x', worktreeDir: '/p' }) ).not.toThrow(); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('notifyTerminal(architect) failed') + expect.stringContaining('notifyTerminal(pir-0108) failed') ); consoleSpy.mockRestore(); @@ -130,7 +107,7 @@ describe('notifyTerminal', () => { it('uses afx binary path ending with bin/afx.js', () => { notifyTerminal({ - target: 'architect', + target: 'pir-0108', message: 'x', worktreeDir: '/projects/test', }); @@ -140,15 +117,8 @@ describe('notifyTerminal', () => { }); }); -describe('message helpers', () => { - it('gatePendingMessage formats with project id and gate name', () => { - const msg = gatePendingMessage('0108', 'plan-approval'); - expect(msg).toContain('GATE: plan-approval (Builder 0108)'); - expect(msg).toContain('Builder 0108 is waiting for approval.'); - expect(msg).toContain('Run: porch approve 0108 plan-approval'); - }); - - it('gateApprovedMessage references the gate and porch next', () => { +describe('gateApprovedMessage', () => { + it('references the gate and porch next', () => { const msg = gateApprovedMessage('code-review'); expect(msg).toContain('code-review'); expect(msg).toContain('porch next'); diff --git a/packages/codev/src/commands/porch/index.ts b/packages/codev/src/commands/porch/index.ts index b41359302..55871a824 100644 --- a/packages/codev/src/commands/porch/index.ts +++ b/packages/codev/src/commands/porch/index.ts @@ -48,7 +48,7 @@ import { type CheckEnv, } from './checks.js'; import { loadCheckOverrides } from './config.js'; -import { notifyTerminal, gatePendingMessage, gateApprovedMessage } from './notify.js'; +import { notifyTerminal, gateApprovedMessage } from './notify.js'; import { loadConfig } from '../../lib/config.js'; import { version } from '../../version.js'; @@ -484,12 +484,6 @@ export async function done(workspaceRoot: string, projectId: string, resolver?: if (!state.gates[gate].requested_at) { state.gates[gate].requested_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gate} gate-requested`); - notifyTerminal({ - target: 'architect', - message: gatePendingMessage(state.id, gate), - worktreeDir: workspaceRoot, - draft: true, - }); } console.log(''); console.log(chalk.yellow(`GATE REQUIRED: ${gate}`)); @@ -610,12 +604,6 @@ export async function gate(workspaceRoot: string, projectId: string, resolver?: if (!state.gates[gateName].requested_at) { state.gates[gateName].requested_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-requested`); - notifyTerminal({ - target: 'architect', - message: gatePendingMessage(state.id, gateName), - worktreeDir: workspaceRoot, - draft: true, - }); } console.log(''); @@ -686,12 +674,6 @@ export async function approve( // Gate belongs to the current phase — initialize it state.gates[gateName] = { status: 'pending', requested_at: new Date().toISOString() }; await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-created (upgrade)`); - notifyTerminal({ - target: 'architect', - message: gatePendingMessage(state.id, gateName), - worktreeDir: workspaceRoot, - draft: true, - }); } else { const knownGates = Object.keys(state.gates).join(', '); throw new Error(`Unknown gate: ${gateName}\nKnown gates: ${knownGates || 'none'}`); @@ -750,9 +732,8 @@ export async function approve( // Wake the builder. The builder's interactive Claude session sits idle at // the gate; without an input event nothing prompts it to call porch next - // and advance. Symmetric counterpart to the architect notification that - // fires when a gate becomes pending. Submitted immediately (draft:false) - // so Claude's next turn processes it. + // and advance. Submitted as a regular message so Claude's next turn + // processes it. notifyTerminal({ target: state.id, message: gateApprovedMessage(gateName), diff --git a/packages/codev/src/commands/porch/next.ts b/packages/codev/src/commands/porch/next.ts index aadb60664..f3008da2e 100644 --- a/packages/codev/src/commands/porch/next.ts +++ b/packages/codev/src/commands/porch/next.ts @@ -34,7 +34,6 @@ import { import { buildPhasePrompt } from './prompts.js'; import { parseVerdict, allApprove } from './verdict.js'; import { loadCheckOverrides } from './config.js'; -import { notifyTerminal, gatePendingMessage } from './notify.js'; import { loadConfig } from '../../lib/config.js'; import { getResolver, type ArtifactResolver } from './artifacts.js'; @@ -711,12 +710,6 @@ async function handleVerifyApproved( state.iteration = 1; state.history = []; await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-requested`); - notifyTerminal({ - target: 'architect', - message: gatePendingMessage(state.id, gateName), - worktreeDir: workspaceRoot, - draft: true, - }); return { status: 'gate_pending', diff --git a/packages/codev/src/commands/porch/notify.ts b/packages/codev/src/commands/porch/notify.ts index 3a9207b4f..80cbd5f86 100644 --- a/packages/codev/src/commands/porch/notify.ts +++ b/packages/codev/src/commands/porch/notify.ts @@ -1,9 +1,18 @@ /** - * Porch terminal notifications — sends `afx send ` to deliver messages - * into a target terminal (architect or builder) as PTY input. + * Porch terminal notifications — sends `afx send ` to deliver + * messages into a target terminal as PTY input. * - * Used by porch's state machine to notify the architect when a gate becomes - * pending (Spec 0108) and to wake the builder when a gate is approved. + * Currently used only to wake the builder after a gate is approved. The + * builder's interactive Claude session sits idle at the gate until it + * receives an input event; without this wake-up it would not call + * `porch next` and advance. + * + * Architect-bound gate notifications were removed deliberately: PIR/SPIR + * gates are explicit human-decision points (review the plan / review the + * worktree), surfaced via the VSCode sidebar tree and toast. The + * architect cannot act on a gate autonomously (approval requires a human + * `--a-human-explicitly-approved-this` flag), so pushing gate state into + * its conversation history only adds noise. */ import { execFile } from 'node:child_process'; @@ -16,31 +25,12 @@ function resolveAfxBinary(): string { } export interface NotifyTerminalOptions { - /** Target terminal: 'architect' or a builder ID (e.g., 'pir-1298'). */ + /** Target terminal — currently always a builder ID (e.g., 'pir-1298'). */ target: string; /** Message text to deliver. */ message: string; /** Working directory — used by afx to resolve the workspace. */ worktreeDir: string; - /** - * When true, deliver the message as a draft (typed into the input buffer - * but Enter is NOT pressed). The receiver sees the text but won't act on - * it until they manually submit. Used for the architect convention. - * - * When false / omitted, the message is submitted immediately so the - * receiver Claude session processes it on its next turn. Used for builder - * wake-ups after gate approval. - */ - draft?: boolean; -} - -/** Architect-bound notification when a gate becomes pending. */ -export function gatePendingMessage(projectId: string, gateName: string): string { - return [ - `GATE: ${gateName} (Builder ${projectId})`, - `Builder ${projectId} is waiting for approval.`, - `Run: porch approve ${projectId} ${gateName}`, - ].join('\n'); } /** Builder-bound wake-up after a gate is approved. */ @@ -49,19 +39,16 @@ export function gateApprovedMessage(gateName: string): string { } /** - * Fire-and-forget notification to a terminal (architect or builder). + * Fire-and-forget notification to a terminal. * Uses `afx send ` via execFile (no shell, no injection risk). * Errors are logged but never thrown — notification is best-effort. */ export function notifyTerminal(opts: NotifyTerminalOptions): void { - const args = ['send', opts.target, opts.message, '--raw']; - if (opts.draft) args.push('--no-enter'); - const afBinary = resolveAfxBinary(); execFile( process.execPath, - [afBinary, ...args], + [afBinary, 'send', opts.target, opts.message, '--raw'], { cwd: opts.worktreeDir, timeout: 10_000 }, (error) => { if (error) { diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index 5ac8496ae..7bc170e07 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -9,18 +9,11 @@ const execFileAsync = promisify(execFile); /** * Codev: Approve Gate — show blocked builders, pick one, approve via porch CLI. * - * After a successful `porch approve`, two follow-ups fire: - * - * 1. **Wake-up nudge** — `afx send "Gate approved..."` typed - * into the builder's PTY. The builder is alive in interactive mode and - * reads this as its next user input; on its next turn it calls - * `porch next`, sees the gate is approved, and advances. Without this - * nudge the builder would sit idle until the user typed something into - * the pane themselves. - * - * 2. **Cache refresh** — `OverviewCache.refresh()` invalidates the Needs - * Attention tree immediately rather than waiting for the next SSE-driven - * tick. Eliminates the brief "still blocked" flash after approving. + * After `porch approve` succeeds, refresh the OverviewCache so the Needs + * Attention tree drops the just-approved builder immediately rather than + * waiting for the next SSE-driven tick. (The builder wake-up itself is + * fired by porch's notifyTerminal — see packages/codev/src/commands/porch/ + * notify.ts.) */ export async function approveGate( connectionManager: ConnectionManager, @@ -66,22 +59,6 @@ export async function approveGate( vscode.window.showInformationMessage(`Codev: Approved ${picked.gate} for #${picked.id}`); - // Note: porch approve itself fires a notifyTerminal wake-up to the builder; - // no need to duplicate that here. - - // Notify the architect. Porch fires notifyTerminal(architect) when a gate - // becomes pending, but not when it transitions to approved — so without this - // breadcrumb, the architect's view of the protocol state goes stale. - // This `afx send architect` line keeps the architect's conversation - // history in sync with what the user did via VSCode. - execFileAsync('afx', [ - 'send', - 'architect', - `User approved ${picked.gate} for ${picked.id} via VSCode.`, - ]).catch(() => { - // Best-effort — architect may not be running in some workflows. - }); - // Refresh the cache so Needs Attention updates without waiting for SSE. cache?.refresh(); } diff --git a/packages/vscode/src/commands/cleanup.ts b/packages/vscode/src/commands/cleanup.ts index faba9914b..58998e6ce 100644 --- a/packages/vscode/src/commands/cleanup.ts +++ b/packages/vscode/src/commands/cleanup.ts @@ -10,17 +10,13 @@ const execFileAsync = promisify(execFile); * Codev: Cleanup Builder — pick builder, run `afx cleanup`, then refresh the * sidebar so the removed builder disappears immediately. * - * After successful cleanup, fires three side effects in order: + * Two follow-ups after a successful cleanup: * * 1. Show success / error toast based on the actual exit status (the old * fire-and-forget spawn silently swallowed errors). - * 2. Notify the architect — cleanup removes a builder from porch's view, - * which is a state change worth recording in the architect's - * conversation history. - * 3. Refresh OverviewCache so the Needs Attention and Builders trees - * drop the removed entry without waiting for the next SSE tick. This - * fixes the user-visible bug where a cleaned-up builder lingered in - * the sidebar. + * 2. Refresh OverviewCache so the Needs Attention and Builders trees + * drop the removed entry without waiting for the next SSE tick. + * This fixes the user-visible bug where a cleaned-up builder lingered. */ export async function cleanupBuilder( connectionManager: ConnectionManager, @@ -60,16 +56,6 @@ export async function cleanupBuilder( vscode.window.showInformationMessage(`Codev: Cleaned up builder #${picked.id}`); - // Architect breadcrumb — cleanup removes a builder from porch's view; - // the architect should know. - execFileAsync('afx', [ - 'send', - 'architect', - `User cleaned up builder ${picked.id} via VSCode.`, - ]).catch(() => { - // Best-effort. - }); - // Refresh the cache so the sidebar drops the removed builder immediately. cache?.refresh(); } diff --git a/packages/vscode/src/commands/send.ts b/packages/vscode/src/commands/send.ts index de86799c4..4bb0fb4e6 100644 --- a/packages/vscode/src/commands/send.ts +++ b/packages/vscode/src/commands/send.ts @@ -1,17 +1,8 @@ import * as vscode from 'vscode'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; import type { ConnectionManager } from '../connection-manager.js'; -const execFileAsync = promisify(execFile); - /** * Codev: Send Message — pick builder, type message, send via TowerClient. - * - * After successful send, fires a breadcrumb to the architect so its view of - * the protocol state stays in sync. Porch orchestrates through the architect; - * a builder's behavior changing in response to user feedback is a fact the - * architect should know about without having to poll porch state. */ export async function sendMessage(connectionManager: ConnectionManager): Promise { const client = connectionManager.getClient(); @@ -47,15 +38,4 @@ export async function sendMessage(connectionManager: ConnectionManager): Promise } vscode.window.showInformationMessage(`Codev: Message sent to ${picked.label}`); - - // Architect breadcrumb. Short preview (first 80 chars) — full text lives - // in the builder's pane; architect only needs the gist + target builder. - const preview = message.length > 80 ? `${message.slice(0, 77)}...` : message; - execFileAsync('afx', [ - 'send', - 'architect', - `User sent feedback to ${picked.id} via VSCode: "${preview}"`, - ]).catch(() => { - // Best-effort. - }); } diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 1be76972a..490c0923e 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -77,9 +77,9 @@ function showGateToast( const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; const message = `Codev: ${label} blocked on ${gateName}${titleSuffix}`; - // Fire and forget. "Review" opens the architect terminal — porch already - // notified the architect terminal about this gate, so the architect pane - // has the context. User talks to the architect from there. + // Fire and forget. "Review" opens the architect terminal so the user can + // talk about the gate from there if they want; the architect itself is + // not pre-notified about gate state. vscode.window .showInformationMessage(message, 'Review') .then((selection) => { From 419c1f3b52b15a52965d7415bad6a2aa44f1c6b5 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 20:34:57 +1000 Subject: [PATCH 16/63] fix(agent-farm/cleanup): invalidate Tower overview after removing builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `afx cleanup` ran from a shell, the architect, or any path outside VSCode's Cleanup command, the removed builder lingered in the sidebar indefinitely. The OverviewCache in VSCode refreshes on any SSE event from Tower, but cleanup itself wasn't firing one — the stale entry only disappeared if some unrelated event happened to trigger an incidental re-fetch. Adds `TowerClient.refreshOverview()` (POST /api/overview/refresh, which already exists server-side — Bugfix #388) and calls it at the end of `cleanup()`. Tower invalidates its in-memory overview cache and broadcasts an `overview-changed` SSE event; subscribed clients pick it up and re-fetch /api/overview. Best-effort: try/catch wraps the call so cleanup still succeeds when Tower isn't running (e.g., headless CI cleanup). The previously-added `cache.refresh()` in VSCode's Cleanup command becomes a no-op once Tower fires the SSE — but it's retained as a local-latency optimization (skips the round-trip wait). --- packages/codev/src/agent-farm/commands/cleanup.ts | 13 +++++++++++++ packages/core/src/tower-client.ts | 14 ++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/packages/codev/src/agent-farm/commands/cleanup.ts b/packages/codev/src/agent-farm/commands/cleanup.ts index ddfd7f755..b0056ccac 100644 --- a/packages/codev/src/agent-farm/commands/cleanup.ts +++ b/packages/codev/src/agent-farm/commands/cleanup.ts @@ -393,6 +393,19 @@ async function cleanupBuilder(builder: Builder, force?: boolean, issueNumber?: n } } + // Invalidate Tower's overview cache + broadcast an `overview-changed` + // SSE event. Connected clients (VSCode sidebar, dashboard) subscribe + // to SSE and re-fetch on any event — without this, the just-removed + // builder would linger in their UI until some unrelated SSE event + // happened to trigger an incidental refresh. Best-effort — silently + // no-ops if Tower isn't running. + try { + const client = new TowerClient(); + await client.refreshOverview(); + } catch { + // Tower not running or unreachable — non-fatal. + } + logger.blank(); logger.success(`Builder ${builder.id} cleaned up!`); } diff --git a/packages/core/src/tower-client.ts b/packages/core/src/tower-client.ts index 6eb01ed95..c25837cd4 100644 --- a/packages/core/src/tower-client.ts +++ b/packages/core/src/tower-client.ts @@ -222,6 +222,20 @@ export class TowerClient { return result.ok ? result.data! : null; } + /** + * Invalidate Tower's in-memory overview cache and broadcast an + * `overview-changed` SSE event. Subscribed clients (VSCode sidebar, + * dashboard) re-fetch /api/overview on any SSE event, so this is + * what makes them notice out-of-band mutations to builder state — + * e.g., `afx cleanup` invoked from a shell or the architect. Without + * it, the change is invisible to clients until some other SSE event + * happens to fire. Best-effort: returns false if Tower isn't running. + */ + async refreshOverview(): Promise { + const result = await this.request<{ ok: boolean }>('/api/overview/refresh', { method: 'POST' }); + return result.ok; + } + async getWorkspaceState(workspacePath: string): Promise { const encoded = encodeWorkspacePath(workspacePath); const result = await this.request(`/workspace/${encoded}/api/state`); From 7f3d2043d635f9cfb4345aa58f99bf21f446e238 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 20:42:32 +1000 Subject: [PATCH 17/63] fix(agent-farm/spawn): pass porch project ID (not builder agent name) to prompt template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR builders launch and immediately fail with: Project builder-pir-1298 not found. Run 'porch init' to create a new project. …because the prompt template's `{{project_id}}` was substituted with the builder agent name (`builder-pir-1298`) instead of the porch project ID (`pir-1298`). PIR's prompts pass `{{project_id}}` directly to `porch next` / `porch done` / `porch approve`, so the mismatch breaks every CLI call. Porch stores state under `-{N}` (`pir-1298`), matching `initPorchInWorktree`, the worktree dir, the branch name, and `detectProjectIdFromCwd`'s regex — but the template variable diverged. BUGFIX has had the same bug all along but didn't surface it because its builder-prompt.md uses bare `porch next` (no arg), relying on porch's cwd-based detection. PIR prompts hardcode `{{project_id}}` and exposed the latent issue. Fix: hoist `porchProjectId = ${prefix}-${issueNumber}` outside the if/else (was defined twice, then dropped from the template context) and pass it as `project_id` to the template. `buildResumeNotice` still takes `builderId` — that's fine, it ignores the arg and instructs the builder to run bare `porch next`. --- packages/codev/src/agent-farm/commands/spawn.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/codev/src/agent-farm/commands/spawn.ts b/packages/codev/src/agent-farm/commands/spawn.ts index d99c07f0b..72abb84da 100644 --- a/packages/codev/src/agent-farm/commands/spawn.ts +++ b/packages/codev/src/agent-farm/commands/spawn.ts @@ -754,21 +754,20 @@ async function spawnIssueDrivenBuilder( await ensureDirectories(config); await checkDependencies(); + // -{N} is the porch project ID, distinct from the builder agent + // name (builder--{N}). Used as the project key in + // codev/projects//status.yaml and what `porch next/done/approve` + // expect as their argument. Also matches detectProjectIdFromCwd's regex. + const porchProjectId = `${prefix}-${issueNumber}`; + if (options.resume) { validateResumeWorktree(worktreePath); } else if (options.branch) { await createWorktreeFromBranch(config, branchName, worktreePath, { remote: options.remote }); - // Pre-initialize porch for --branch mode too - const porchProjectId = `${prefix}-${issueNumber}`; const slug = slugify(issue.title); await initPorchInWorktree(worktreePath, protocol, porchProjectId, slug); } else { await createWorktree(config, branchName, worktreePath); - - // Pre-initialize porch so the builder doesn't need to figure out project ID. - // Use -{N} as the porch project ID (not the builder agent name). - // This aligns with porch's CWD-based detection from worktree paths. - const porchProjectId = `${prefix}-${issueNumber}`; const slug = slugify(issue.title); await initPorchInWorktree(worktreePath, protocol, porchProjectId, slug); } @@ -776,7 +775,9 @@ async function spawnIssueDrivenBuilder( const templateContext: TemplateContext = { protocol_name: protocol.toUpperCase(), mode, mode_soft: mode === 'soft', mode_strict: mode === 'strict', - project_id: builderId, + // The porch project ID — what `porch next/done/approve` expects. + // NOT the builder agent name (which includes the `builder-` prefix). + project_id: porchProjectId, input_description: `work for GitHub Issue #${issueNumber}`, issue: { number: issueNumber, title: issue.title, body: issue.body || '(No description provided)' }, }; From 7a22531339b11764609ac633b9cb943bc06b5c0a Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 20:48:21 +1000 Subject: [PATCH 18/63] fix(agent-farm/overview): recognize PIR worktrees + branches Tower's discovery functions hardcoded prefix-aware regex per protocol (spir, air, aspir, bugfix). PIR was missing, so: - `extractProjectIdFromWorktreeName('pir-1298-slug')` returned null, which made discoverBuilders push the entry as soft-mode with empty protocol and no status.yaml lookup. The VSCode sidebar then rendered the row with contextValue `builder-` (no protocol) and the PIR-scoped menu items (View Plan File / View Review File) never matched their when-clause `^(builder|blocked-builder)-pir$`. - `worktreeNameToRoleId` fell through to a generic match that happened to produce the right ID but only as a fallback. Made explicit for consistency with the other protocols. - `analytics.ts` BRANCH_PROTOCOL_PATTERNS missed PIR, so analytics reporting attributed PIR PRs to no protocol. Adds explicit PIR cases in all three places. PIR worktrees now follow bugfix's convention: project ID is `pir-` (mirrors what initPorchInWorktree writes and what `porch next/done/approve` expect). --- packages/codev/src/agent-farm/servers/analytics.ts | 1 + packages/codev/src/agent-farm/servers/overview.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/codev/src/agent-farm/servers/analytics.ts b/packages/codev/src/agent-farm/servers/analytics.ts index 6499534af..d34e64968 100644 --- a/packages/codev/src/agent-farm/servers/analytics.ts +++ b/packages/codev/src/agent-farm/servers/analytics.ts @@ -253,6 +253,7 @@ function computeConsultationMetrics(days: number | undefined, workspacePath: str */ const BRANCH_PROTOCOL_PATTERNS: Array<{ pattern: RegExp; protocol: string }> = [ { pattern: /^builder\/bugfix-/, protocol: 'bugfix' }, + { pattern: /^builder\/pir-/, protocol: 'pir' }, { pattern: /^builder\/spir-/, protocol: 'spir' }, { pattern: /^spir\//, protocol: 'spir' }, { pattern: /^builder\/aspir-/, protocol: 'aspir' }, diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 8badd1439..e14eecbfb 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -433,6 +433,10 @@ export function worktreeNameToRoleId(dirName: string): string | null { const bugfixMatch = lower.match(/^bugfix-(\d+)/); if (bugfixMatch) return `builder-bugfix-${Number(bugfixMatch[1])}`; + // PIR: pir-1298-slug → builder-pir-1298 + const pirMatch = lower.match(/^pir-(\d+)/); + if (pirMatch) return `builder-pir-${Number(pirMatch[1])}`; + // Task: task-NAvW → builder-task-navw const taskMatch = lower.match(/^task-([a-z0-9]+)/); if (taskMatch) return `builder-task-${taskMatch[1]}`; @@ -479,6 +483,10 @@ export function extractProjectIdFromWorktreeName(dirName: string): string | null const bugfixMatch = dirName.match(/^bugfix-(\d+)/); if (bugfixMatch) return `bugfix-${bugfixMatch[1]}`; + // PIR: pir-1298-slug → "pir-1298" (porch uses this, mirroring bugfix's convention) + const pirMatch = dirName.match(/^pir-(\d+)/); + if (pirMatch) return `pir-${pirMatch[1]}`; + // Legacy numeric: 0110 or 0110-slug → "0110" const numericMatch = dirName.match(/^(\d+)(?:-|$)/); if (numericMatch) return numericMatch[1]; From 622d7e3e974119ff3a7ed25005bd99b7e418a72a Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:14:08 +1000 Subject: [PATCH 19/63] fix(porch/approve): skip builder wake-up when caller is the builder itself MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user types "approve" into the builder's pane, the builder's Claude session runs `porch approve` as a Bash tool call. The previous implementation fired notifyTerminal unconditionally — the wake-up message was delivered to the same builder PTY that just issued the command, surfacing as the builder's next input ("Gate code-review approved — please run `porch next` to advance"). Claude then responded to its own approval message. The wake-up only serves a purpose when the builder is genuinely idle — i.e., when porch is invoked from something other than the builder itself: VSCode's execFile, the architect's Bash tool, a separate shell. In all three of those cases, cwd is OUTSIDE the builder's worktree. The builder calling porch on itself has cwd = the worktree. Compare `process.cwd()` (workspaceRoot) against `artifactRoot` (derived from the status.yaml path, always resolving to the worktree). Match → skip wake-up; differ → fire as before. No behavior change for external approvers (VSCode, architect, separate shell): wake-up still nudges the idle builder forward. --- packages/codev/src/commands/porch/index.ts | 26 ++++++++++++++-------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/packages/codev/src/commands/porch/index.ts b/packages/codev/src/commands/porch/index.ts index 55871a824..855920ec5 100644 --- a/packages/codev/src/commands/porch/index.ts +++ b/packages/codev/src/commands/porch/index.ts @@ -730,15 +730,23 @@ export async function approve( state.gates[gateName].approved_at = new Date().toISOString(); await writeStateAndCommit(statusPath, state, `chore(porch): ${state.id} ${gateName} gate-approved`); - // Wake the builder. The builder's interactive Claude session sits idle at - // the gate; without an input event nothing prompts it to call porch next - // and advance. Submitted as a regular message so Claude's next turn - // processes it. - notifyTerminal({ - target: state.id, - message: gateApprovedMessage(gateName), - worktreeDir: workspaceRoot, - }); + // Wake the builder iff porch was invoked from OUTSIDE the builder's + // worktree. The wake-up wakes an *idle* builder; when the builder is + // the one running `porch approve` (the user typed feedback into the + // builder's pane and the builder ran the command itself), it's already + // active and the message would be echoed back as the builder's next + // input — Claude then "responds" to its own approval message. + // + // workspaceRoot is process.cwd() at CLI invocation. When called from + // inside the worktree it resolves to the same path as artifactRoot. + const calledFromBuilderWorktree = path.resolve(workspaceRoot) === path.resolve(artifactRoot); + if (!calledFromBuilderWorktree) { + notifyTerminal({ + target: state.id, + message: gateApprovedMessage(gateName), + worktreeDir: workspaceRoot, + }); + } console.log(''); console.log(chalk.green(`Gate ${gateName} approved.`)); From 567887e5d6485c690f2f9a09895ece0d7ec73bb3 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:14:41 +1000 Subject: [PATCH 20/63] fix(porch/cli): broadcast overview-changed after mutating commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The VSCode sidebar (and dashboard) didn't reflect builder state changes without a manual refresh — a builder that reached a gate stayed "running" in the Builders tree until something unrelated happened to fire an SSE event and cause an incidental re-fetch. Symptom: builders that just hit plan-approval / code-review didn't move into Needs Attention; approved builders stayed blocked; freshly spawned builders appeared late. Mirrors the cleanup fix (419c1f3b). After porch dispatches a mutating command (next / done / gate / approve / rollback / verify / init), fire TowerClient.refreshOverview() which invalidates Tower's overview cache and broadcasts overview-changed via SSE. VSCode's OverviewCache picks that up and re-fetches /api/overview, which re-renders the sidebar. Best-effort: try/catch wraps the Tower call so porch commands still succeed when Tower isn't running (CI, headless usage). Read-only commands (pending / status / check) intentionally don't fire — builders run `porch status` frequently and we don't want to hammer Tower on every read. --- packages/codev/src/commands/porch/index.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/packages/codev/src/commands/porch/index.ts b/packages/codev/src/commands/porch/index.ts index 855920ec5..fc71aff88 100644 --- a/packages/codev/src/commands/porch/index.ts +++ b/packages/codev/src/commands/porch/index.ts @@ -1072,6 +1072,12 @@ export async function cli(args: string[]): Promise { return id; } + // Commands that mutate state. After dispatching, we fire a Tower + // overview-refresh so subscribed clients (VSCode sidebar, dashboard) + // pick up the change immediately instead of waiting for an unrelated + // SSE event to incidentally cause a re-fetch. + const MUTATING_COMMANDS = new Set(['next', 'done', 'gate', 'approve', 'rollback', 'verify', 'init']); + try { switch (command) { case 'pending': @@ -1199,6 +1205,18 @@ export async function cli(args: string[]): Promise { console.log(''); process.exit(command && command !== '--help' && command !== '-h' ? 1 : 0); } + + // After a successful mutating command, broadcast `overview-changed` + // via Tower so VSCode / dashboard refresh without a manual reload. + // Best-effort — silently no-ops if Tower isn't running. + if (command && MUTATING_COMMANDS.has(command)) { + try { + const { TowerClient } = await import('../../agent-farm/lib/tower-client.js'); + await new TowerClient().refreshOverview(); + } catch { + // Tower not running / unreachable — non-fatal. + } + } } catch (err) { console.error(chalk.red(`Error: ${(err as Error).message}`)); process.exit(1); From 17b784a451b7843ff5fdd3a3fbded28543baf6fd Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:20:44 +1000 Subject: [PATCH 21/63] fix(vscode/review-diff): detect default branch per worktree, don't hardcode 'main' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The View Diff command compared against a hardcoded `main`. Repos using `master`, `develop`, `trunk`, or any other default name would either silently produce a wrong diff (if a local `main` happens to exist as a non-default branch) or fail with "bad revision" (if `main` doesn't exist at all). Detect the default branch via `git symbolic-ref --short refs/remotes/origin/HEAD` which resolves to e.g. `origin/main` or `origin/master`. Strip the `origin/` prefix and use that as the diff base. Falls back to `main` if `origin/HEAD` isn't set locally — same behavior as before for repos that work today. Diff title now shows the actual branch name ("Reviewing pir-1298 (master ↔ HEAD)") so reviewers know what they're looking at. --- packages/vscode/src/commands/review-diff.ts | 47 +++++++++++++++------ 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/packages/vscode/src/commands/review-diff.ts b/packages/vscode/src/commands/review-diff.ts index 74b2c74df..bcc34f948 100644 --- a/packages/vscode/src/commands/review-diff.ts +++ b/packages/vscode/src/commands/review-diff.ts @@ -1,18 +1,21 @@ /** - * Codev: View Diff — open `main...HEAD` for a builder's worktree as a - * single multi-file diff editor (matches VSCode's built-in "Working Tree" - * view in the Source Control panel). + * Codev: View Diff — open `...HEAD` for a builder's worktree + * as a single multi-file diff editor (matches VSCode's built-in "Working + * Tree" view in the Source Control panel). * * Right-click a builder row → "View Diff". Opens ONE tab with a file * list on the left and a diff that updates as the reviewer clicks each * file. Handles added / modified / deleted files uniformly via VSCode's * `vscode.changes` command. * + * Default branch is detected per-worktree via `git symbolic-ref + * refs/remotes/origin/HEAD`. Repos using `master`, `develop`, `trunk`, + * etc. work without configuration. Falls back to `main` if the symbolic + * ref isn't set. + * * Why this works across worktrees: each `.builders//` is a real git * worktree linked to the parent repo's `.git`, so all branches live in the - * shared object database. `git -C rev-parse main` resolves the same - * SHA as from the parent repo, and `git:?ref=main` reads from that object - * database regardless of which repo VSCode attributes the file to. + * shared object database. */ import * as vscode from 'vscode'; @@ -56,13 +59,31 @@ export async function reviewDiff( return; } + // Detect the repo's default branch from origin/HEAD. Resolves to a + // branch name (e.g. `main`, `master`, `develop`). Falls back to `main` + // if origin/HEAD isn't set locally. + let defaultBranch = 'main'; + try { + const { stdout } = await execFileAsync('git', [ + '-C', builder.worktreePath, + 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD', + ]); + // Strip the `origin/` prefix to get just the branch name. + const ref = stdout.trim().replace(/^origin\//, ''); + if (ref) { defaultBranch = ref; } + } catch { + // origin/HEAD not set — keep the `main` default. The diff will still + // run; if `main` doesn't exist locally either, the next git call + // surfaces a clear error. + } + // Enumerate changed files with status letters so we can handle // added (A) / deleted (D) files distinctly from modified (M). let changes: Array<{ status: string; path: string }>; try { const { stdout } = await execFileAsync('git', [ '-C', builder.worktreePath, - 'diff', '--name-status', 'main...HEAD', + 'diff', '--name-status', `${defaultBranch}...HEAD`, ]); changes = stdout .split('\n') @@ -96,24 +117,24 @@ export async function reviewDiff( const resources: Array<[vscode.Uri, vscode.Uri, vscode.Uri]> = changes.map(({ status, path: rel }) => { const abs = path.join(builder.worktreePath, rel); const resourceUri = vscode.Uri.file(abs); - const mainUri = toGitUri(abs, rel, 'main'); + const baseUri = toGitUri(abs, rel, defaultBranch); const headUri = vscode.Uri.file(abs); if (status === 'A') { - // Added: no main version. Left = empty, right = file. + // Added: no base version. Left = empty, right = file. return [resourceUri, emptyGitUri(abs, rel), headUri]; } if (status === 'D') { - // Deleted: no worktree version. Left = main, right = empty. - return [resourceUri, mainUri, emptyGitUri(abs, rel)]; + // Deleted: no worktree version. Left = base, right = empty. + return [resourceUri, baseUri, emptyGitUri(abs, rel)]; } // Modified / renamed / copied / unmerged → side-by-side diff - return [resourceUri, mainUri, headUri]; + return [resourceUri, baseUri, headUri]; }); await vscode.commands.executeCommand( 'vscode.changes', - `Reviewing ${builder.id} (main ↔ HEAD)`, + `Reviewing ${builder.id} (${defaultBranch} ↔ HEAD)`, resources, ); } From e61c39d017735f7866bbb4daa1c70719f66bfaf6 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:32:06 +1000 Subject: [PATCH 22/63] fix(vscode/approve): use canonical gate name + correct cwd for porch approve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs broke the Approve Gate command: 1. **Gate name mismatch.** Tower's overview returned `blocked` as a *display label* ("plan review", "code review") via detectBlocked. VSCode passed that string straight to `porch approve`, which looks up state.gates by the *canonical* name ("plan-approval", "code-review"). Porch couldn't find a gate named "plan review" and the call failed with "Project pir-1298 not found" (the error path is misleading — the project IS found but the unknown gate aborts later). Fix: add `blockedGate` to OverviewBuilder (canonical name like "plan-approval") alongside the existing `blocked` (display label). detectBlockedGate populates it. VSCode's approve command uses blockedGate when calling porch; sidebar / dashboard keep using `blocked` for display. 2. **Wrong cwd.** approveGate (and cleanupBuilder) called execFile without setting cwd, so porch ran from VSCode's launch directory (usually `/` on macOS) and couldn't find `.builders//` to load the project state. Added `{ cwd: workspacePath }` to both. Pre-existing display labels stay unchanged — `blocked` is consumed across the sidebar tree, status bar, dashboard, and toast. --- .../codev/src/agent-farm/servers/overview.ts | 40 ++++++++--- packages/types/src/api.ts | 7 ++ packages/vscode/src/commands/approve.ts | 70 ++++++++++++++----- packages/vscode/src/commands/cleanup.ts | 2 +- 4 files changed, 91 insertions(+), 28 deletions(-) diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index e14eecbfb..3687a01c0 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -42,7 +42,14 @@ export interface BuilderOverview { protocol: string; planPhases: PlanPhase[]; progress: number; + /** Human-readable label for the gate the builder is blocked on (e.g. "plan review"). */ blocked: string | null; + /** + * Canonical gate name (e.g. "plan-approval") for the gate the builder is + * blocked on. Use this when calling `porch approve` — `blocked` is a + * display label and won't match porch's gate keys. + */ + blockedGate: string | null; blockedSince: string | null; startedAt: string | null; idleMs: number; @@ -338,15 +345,15 @@ function loadProtocolPhases(workspaceRoot: string, protocolName: string): string * `OverviewBuilder.blocked` and downstream UIs (VSCode Needs Attention tree, * VSCode toast, dashboard NeedsAttentionList, status bar counter). */ -export function detectBlocked(parsed: ParsedStatus): string | null { - const gateLabels: Record = { - 'spec-approval': 'spec review', - 'plan-approval': 'plan review', - 'code-review': 'code review', - 'pr': 'PR review', - }; +const GATE_LABELS: Record = { + 'spec-approval': 'spec review', + 'plan-approval': 'plan review', + 'code-review': 'code review', + 'pr': 'PR review', +}; - for (const [gate, label] of Object.entries(gateLabels)) { +export function detectBlocked(parsed: ParsedStatus): string | null { + for (const [gate, label] of Object.entries(GATE_LABELS)) { if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) { return label; } @@ -354,6 +361,20 @@ export function detectBlocked(parsed: ParsedStatus): string | null { return null; } +/** + * Canonical gate name (e.g. "plan-approval") for the gate the builder is + * blocked on. Sibling to `detectBlocked` which returns the display label. + * Returns null if the builder isn't blocked. + */ +export function detectBlockedGate(parsed: ParsedStatus): string | null { + for (const gate of Object.keys(GATE_LABELS)) { + if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) { + return gate; + } + } + return null; +} + /** * Detect when the current blocked gate was first requested. * Returns the ISO timestamp string or null if not blocked. @@ -531,6 +552,7 @@ export function discoverBuilders(workspaceRoot: string): BuilderOverview[] { planPhases: [], progress: 0, blocked: null, + blockedGate: null, blockedSince: null, startedAt: null, idleMs: 0, @@ -582,6 +604,7 @@ export function discoverBuilders(workspaceRoot: string): BuilderOverview[] { planPhases: parsed.planPhases, progress: calculateProgress(parsed, workspaceRoot), blocked: detectBlocked(parsed), + blockedGate: detectBlockedGate(parsed), blockedSince: detectBlockedSince(parsed), startedAt: parsed.startedAt || null, idleMs: computeIdleMs(parsed), @@ -610,6 +633,7 @@ export function discoverBuilders(workspaceRoot: string): BuilderOverview[] { planPhases: [], progress: 0, blocked: null, + blockedGate: null, blockedSince: null, startedAt: null, idleMs: 0, diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 678d05208..71f2571ed 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -82,7 +82,14 @@ export interface OverviewBuilder { protocol: string; planPhases: Array<{ id: string; title: string; status: string }>; progress: number; + /** Human-readable label for the gate the builder is blocked on (e.g. "plan review"). */ blocked: string | null; + /** + * Canonical gate name (e.g. "plan-approval") for the gate the builder is + * blocked on. Use this when calling `porch approve` — `blocked` is a + * display label and won't match porch's gate keys. + */ + blockedGate: string | null; blockedSince: string | null; startedAt: string | null; idleMs: number; diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index 7bc170e07..ee959a8ed 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -7,17 +7,24 @@ import type { OverviewCache } from '../views/overview-data.js'; const execFileAsync = promisify(execFile); /** - * Codev: Approve Gate — show blocked builders, pick one, approve via porch CLI. + * Codev: Approve Gate. * - * After `porch approve` succeeds, refresh the OverviewCache so the Needs - * Attention tree drops the just-approved builder immediately rather than - * waiting for the next SSE-driven tick. (The builder wake-up itself is - * fired by porch's notifyTerminal — see packages/codev/src/commands/porch/ - * notify.ts.) + * Two invocation paths: + * + * 1. Right-click a blocked-builder row → pass the builder ID directly. + * Skips the quick-pick; auto-detects the gate from b.blocked. + * + * 2. Command palette / Cmd+K G → no builder ID → show quick-pick of all + * blocked builders. + * + * After `porch approve` succeeds, refresh the OverviewCache so the + * sidebar updates immediately rather than waiting for the SSE round-trip + * triggered by porch's overview-refresh broadcast. */ export async function approveGate( connectionManager: ConnectionManager, cache?: OverviewCache, + builderIdArg?: string, ): Promise { const client = connectionManager.getClient(); const workspacePath = connectionManager.getWorkspacePath(); @@ -33,31 +40,56 @@ export async function approveGate( return; } - const picked = await vscode.window.showQuickPick( - blocked.map(b => ({ - label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, - description: `blocked on ${b.blocked}`, - id: b.id, - gate: b.blocked!, - })), - { placeHolder: 'Select gate to approve' }, + // We need blockedGate (canonical name like "plan-approval"), not blocked + // (display label like "plan review"). Porch's gate keys are the canonical + // names; the display label is for the sidebar only. + let id: string; + let gate: string; + if (builderIdArg) { + const direct = blocked.find(b => b.id === builderIdArg); + if (!direct || !direct.blockedGate) { + vscode.window.showWarningMessage(`Codev: Builder ${builderIdArg} is not blocked at a gate`); + return; + } + id = direct.id; + gate = direct.blockedGate; + } else { + const candidates = blocked.filter(b => b.blockedGate); + const picked = await vscode.window.showQuickPick( + candidates.map(b => ({ + label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, + description: `blocked on ${b.blocked}`, + id: b.id, + gate: b.blockedGate!, + })), + { placeHolder: 'Select gate to approve' }, + ); + if (!picked) { return; } + id = picked.id; + gate = picked.gate; + } + + const confirmed = await vscode.window.showInformationMessage( + `Approve ${gate} for ${id}?`, + { modal: true }, + 'Approve', ); - if (!picked) { return; } + if (confirmed !== 'Approve') { return; } try { await execFileAsync('porch', [ 'approve', - picked.id, - picked.gate, + id, + gate, '--a-human-explicitly-approved-this', - ]); + ], { cwd: workspacePath }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); vscode.window.showErrorMessage(`Codev: porch approve failed — ${msg}`); return; } - vscode.window.showInformationMessage(`Codev: Approved ${picked.gate} for #${picked.id}`); + vscode.window.showInformationMessage(`Codev: Approved ${gate} for ${id}`); // Refresh the cache so Needs Attention updates without waiting for SSE. cache?.refresh(); diff --git a/packages/vscode/src/commands/cleanup.ts b/packages/vscode/src/commands/cleanup.ts index 58998e6ce..aa298f1fb 100644 --- a/packages/vscode/src/commands/cleanup.ts +++ b/packages/vscode/src/commands/cleanup.ts @@ -47,7 +47,7 @@ export async function cleanupBuilder( if (!picked) { return; } try { - await execFileAsync('afx', ['cleanup', '-p', picked.id]); + await execFileAsync('afx', ['cleanup', '-p', picked.id], { cwd: workspacePath }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); vscode.window.showErrorMessage(`Codev: afx cleanup failed — ${msg}`); From 57f3ecab5681dbc1793b2ae9d9b1ea8f441a44aa Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:32:43 +1000 Subject: [PATCH 23/63] feat(vscode): inline Approve Gate button + drop View Review File MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sidebar UX changes for blocked builders: 1. **Inline Approve Gate button.** Each blocked-builder row in the Builders / Needs Attention views now shows a check icon. Hover (or pin always-visible) → click → modal confirmation → approve. One-click approval without navigating the command palette. The same command is still on the right-click context menu and via Cmd+K G (which now also accepts a builder ID if invoked from a tree row). 2. **Drop View Review File menu item.** The review markdown is the PR body in PIR's review phase — reviewers read it on GitHub, not from a local file. The menu item only had value in a narrow window between code-review approval and PR creation, which isn't worth the contextValue scoping and the file-picker UX. Dropped command, menu entry, declaration, and the unused export in view-artifact.ts. --- packages/vscode/package.json | 22 ++++++++++--------- packages/vscode/src/commands/view-artifact.ts | 22 +++++++++---------- packages/vscode/src/extension.ts | 7 +++--- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 53e461c4d..4714d348f 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -72,7 +72,8 @@ }, { "command": "codev.approveGate", - "title": "Codev: Approve Gate" + "title": "Codev: Approve Gate", + "icon": "$(check)" }, { "command": "codev.cleanupBuilder", @@ -131,10 +132,6 @@ { "command": "codev.viewPlanFile", "title": "Codev: View Plan File" - }, - { - "command": "codev.viewReviewFile", - "title": "Codev: View Review File" } ], "menus": { @@ -180,14 +177,19 @@ "group": "4_worktree@3" }, { - "command": "codev.viewPlanFile", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-pir$/", - "group": "3_review@2" + "command": "codev.approveGate", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^blocked-builder-/", + "group": "inline@1" }, { - "command": "codev.viewReviewFile", + "command": "codev.approveGate", + "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^blocked-builder-/", + "group": "0_gate@1" + }, + { + "command": "codev.viewPlanFile", "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-pir$/", - "group": "3_review@3" + "group": "3_review@2" } ], "view/title": [ diff --git a/packages/vscode/src/commands/view-artifact.ts b/packages/vscode/src/commands/view-artifact.ts index f4de5cf3d..a9b397c87 100644 --- a/packages/vscode/src/commands/view-artifact.ts +++ b/packages/vscode/src/commands/view-artifact.ts @@ -1,15 +1,18 @@ /** - * Codev: View Plan File / View Review File — open the markdown artifact a - * gated builder is waiting on (or has just written) directly in a VSCode - * editor tab. + * Codev: View Plan File — open the plan markdown a gated PIR builder is + * waiting on directly in a VSCode editor tab. * - * Right-click a builder row → "View Plan File" or "View Review File". + * Right-click a builder row → "View Plan File". * - * Strategy: locate `/codev/plans/` or `/codev/reviews/`, - * list the `.md` files inside, and: + * Strategy: locate `/codev/plans/`, filter to files prefixed + * with the builder ID, and: * - 0 files → friendly message ("no file yet — the builder hasn't written one") * - 1 file → open it * - 2+ files → quick-pick (newer files float to the top) + * + * View Review File was intentionally not added: the review file is the + * PR body in PIR's review phase, so reviewers read it on GitHub when + * it matters. */ import * as vscode from 'vscode'; @@ -17,21 +20,16 @@ import { resolve } from 'node:path'; import { existsSync, readdirSync, statSync } from 'node:fs'; import type { ConnectionManager } from '../connection-manager.js'; -type ArtifactKind = 'plan' | 'review'; +type ArtifactKind = 'plan'; const ARTIFACT_SUBDIR: Record = { plan: 'codev/plans', - review: 'codev/reviews', }; export function viewPlanFile(connectionManager: ConnectionManager, builderIdArg: string | undefined) { return viewArtifact(connectionManager, builderIdArg, 'plan'); } -export function viewReviewFile(connectionManager: ConnectionManager, builderIdArg: string | undefined) { - return viewArtifact(connectionManager, builderIdArg, 'review'); -} - async function viewArtifact( connectionManager: ConnectionManager, builderIdArg: string | undefined, diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index d1a444c8c..6e7406c96 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -11,7 +11,7 @@ import { runWorktreeDev } from './commands/run-worktree-dev.js'; import { stopWorktreeDev } from './commands/stop-worktree-dev.js'; import { openWorktreeFolder } from './commands/open-worktree-folder.js'; import { runWorktreeSetup } from './commands/run-worktree-setup.js'; -import { viewPlanFile, viewReviewFile } from './commands/view-artifact.js'; +import { viewPlanFile } from './commands/view-artifact.js'; import { connectTunnel, disconnectTunnel } from './commands/tunnel.js'; import { listCronTasks } from './commands/cron.js'; import { addReviewComment } from './commands/review.js'; @@ -208,7 +208,8 @@ export async function activate(context: vscode.ExtensionContext) { }), vscode.commands.registerCommand('codev.spawnBuilder', () => spawnBuilder()), vscode.commands.registerCommand('codev.sendMessage', () => sendMessage(connectionManager!)), - vscode.commands.registerCommand('codev.approveGate', () => approveGate(connectionManager!, overviewCache)), + vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined) => + approveGate(connectionManager!, overviewCache, extractBuilderId(arg))), vscode.commands.registerCommand('codev.cleanupBuilder', () => cleanupBuilder(connectionManager!, overviewCache)), vscode.commands.registerCommand('codev.reviewDiff', (arg: vscode.TreeItem | string | undefined) => reviewDiff(connectionManager!, extractBuilderId(arg))), @@ -222,8 +223,6 @@ export async function activate(context: vscode.ExtensionContext) { runWorktreeSetup(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.viewPlanFile', (arg: vscode.TreeItem | string | undefined) => viewPlanFile(connectionManager!, extractBuilderId(arg))), - vscode.commands.registerCommand('codev.viewReviewFile', (arg: vscode.TreeItem | string | undefined) => - viewReviewFile(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.refreshOverview', () => overviewCache.refresh()), vscode.commands.registerCommand('codev.reconnect', () => connectionManager?.reconnect()), vscode.commands.registerCommand('codev.connectTunnel', () => connectTunnel(connectionManager!)), From 7d377326905e4aa84304cae9e9eae32910109baa Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 21:47:51 +1000 Subject: [PATCH 24/63] refactor(vscode): merge Needs Attention into Builders + redirect toast to builder pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related sidebar changes: 1. Merge Needs Attention into Builders. Both views displayed the same builders — Needs Attention as a subset filtered to blocked, Builders as the full list. With N total builders and K blocked, the sidebar showed N+K rows with K duplicates. Now a single Builders tree: - Blocked builders sort to the top, oldest-blocked first. - Bell icon + `[2m]` wait-time suffix for blocked rows. - Play icon + `[]` for active rows. - Inline Approve button still only renders on blocked-builder-* contextValues (unchanged scoping). Ordering logic lives in a module-level `orderForDisplay()` helper so getChildren stays a three-line "fetch, order, map" flow. PRs that previously appeared in Needs Attention are already covered by the dedicated Pull Requests view — no functional loss. Status bar counter still reads `N builders · K blocked` for at-a-glance triage. Removed: codev.needsAttention view declaration, NeedsAttentionProvider registration, the provider file itself, and the now-redundant `(builders|needsAttention)` regex in every menu when-clause. 2. Toast "Review" button opens the builder pane (was: architect terminal). The architect was deliberately taken out of the gate loop in ff053152 — opening its terminal from the toast landed users in a pane with no gate context. Now Review opens the builder's own pane via `codev.openBuilderById`, which is where the gate-reached message + interactive Claude live. --- packages/vscode/package.json | 24 +++----- packages/vscode/src/commands/approve.ts | 2 +- packages/vscode/src/commands/cleanup.ts | 6 +- packages/vscode/src/extension.ts | 4 +- .../vscode/src/notifications/gate-toast.ts | 14 +++-- packages/vscode/src/views/builders.ts | 55 ++++++++++++++--- packages/vscode/src/views/needs-attention.ts | 59 ------------------- 7 files changed, 69 insertions(+), 95 deletions(-) delete mode 100644 packages/vscode/src/views/needs-attention.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 4714d348f..e154926b8 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -148,56 +148,51 @@ "view/item/context": [ { "command": "codev.openBuilderById", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "1_terminal@1" }, { "command": "codev.openWorktreeFolder", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "2_files@1" }, { "command": "codev.reviewDiff", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "3_review@1" }, { "command": "codev.runWorktreeSetup", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@1" }, { "command": "codev.runWorktreeDev", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@2" }, { "command": "codev.stopWorktreeDev", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "4_worktree@3" }, { "command": "codev.approveGate", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^blocked-builder-/", + "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", "group": "inline@1" }, { "command": "codev.approveGate", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^blocked-builder-/", + "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", "group": "0_gate@1" }, { "command": "codev.viewPlanFile", - "when": "view =~ /^codev\\.(builders|needsAttention)$/ && viewItem =~ /^(builder|blocked-builder)-pir$/", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-pir$/", "group": "3_review@2" } ], "view/title": [ - { - "command": "codev.refreshOverview", - "when": "view == codev.needsAttention", - "group": "navigation" - }, { "command": "codev.refreshOverview", "when": "view == codev.builders", @@ -255,7 +250,6 @@ "views": { "codev": [ { "id": "codev.workspace", "name": "Workspace" }, - { "id": "codev.needsAttention", "name": "Needs Attention" }, { "id": "codev.builders", "name": "Builders" }, { "id": "codev.pullRequests", "name": "Pull Requests" }, { "id": "codev.backlog", "name": "Backlog" }, diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index ee959a8ed..f6d343403 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -91,6 +91,6 @@ export async function approveGate( vscode.window.showInformationMessage(`Codev: Approved ${gate} for ${id}`); - // Refresh the cache so Needs Attention updates without waiting for SSE. + // Refresh the cache so the Builders tree updates without waiting for SSE. cache?.refresh(); } diff --git a/packages/vscode/src/commands/cleanup.ts b/packages/vscode/src/commands/cleanup.ts index aa298f1fb..00984530a 100644 --- a/packages/vscode/src/commands/cleanup.ts +++ b/packages/vscode/src/commands/cleanup.ts @@ -14,9 +14,9 @@ const execFileAsync = promisify(execFile); * * 1. Show success / error toast based on the actual exit status (the old * fire-and-forget spawn silently swallowed errors). - * 2. Refresh OverviewCache so the Needs Attention and Builders trees - * drop the removed entry without waiting for the next SSE tick. - * This fixes the user-visible bug where a cleaned-up builder lingered. + * 2. Refresh OverviewCache so the Builders tree drops the removed + * entry without waiting for the next SSE tick. This fixes the + * user-visible bug where a cleaned-up builder lingered. */ export async function cleanupBuilder( connectionManager: ConnectionManager, diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 6e7406c96..829b2da61 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -19,7 +19,6 @@ import { activateGateToasts } from './notifications/gate-toast.js'; import { activateReviewDecorations } from './review-decorations.js'; import { BuilderSpawnHandler } from './builder-spawn-handler.js'; import { BuilderTerminalLinkProvider } from './terminal-link-provider.js'; -import { NeedsAttentionProvider } from './views/needs-attention.js'; import { BuildersProvider } from './views/builders.js'; import { PullRequestsProvider } from './views/pull-requests.js'; import { BacklogProvider } from './views/backlog.js'; @@ -109,7 +108,6 @@ export async function activate(context: vscode.ExtensionContext) { context.subscriptions.push( vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager)), - vscode.window.registerTreeDataProvider('codev.needsAttention', new NeedsAttentionProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.builders', new BuildersProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.pullRequests', new PullRequestsProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.backlog', new BacklogProvider(overviewCache)), @@ -235,7 +233,7 @@ export async function activate(context: vscode.ExtensionContext) { activateReviewDecorations(context); // Toast on new gate-pending — surfaces blocked builders without forcing the - // user to watch the Needs Attention tree. Respects `codev.gateToasts.enabled`. + // user to watch the Builders tree. Respects `codev.gateToasts.enabled`. activateGateToasts(context, overviewCache); // Auto-open builder terminals on Tower spawn events diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 490c0923e..a08fb57fe 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -20,8 +20,8 @@ import type { OverviewCache } from '../views/overview-data.js'; * advances) so that re-blocking later (on a different gate) will re-toast. * * Respects the `codev.gateToasts.enabled` setting (default: true). Set to - * false to silence; status bar counters and the Needs Attention tree - * remain unaffected. + * false to silence; status bar counters and the Builders tree remain + * unaffected. */ export function activateGateToasts( context: vscode.ExtensionContext, @@ -77,14 +77,16 @@ function showGateToast( const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; const message = `Codev: ${label} blocked on ${gateName}${titleSuffix}`; - // Fire and forget. "Review" opens the architect terminal so the user can - // talk about the gate from there if they want; the architect itself is - // not pre-notified about gate state. + // Fire and forget. "Review" opens the builder's own pane — the gate- + // reached message and the builder's interactive Claude live there. From + // that pane the user can read the plan/diff, type feedback, edit files + // in VSCode, or approve via Cmd+K G (or the inline Approve button on + // the sidebar row). vscode.window .showInformationMessage(message, 'Review') .then((selection) => { if (selection === 'Review') { - vscode.commands.executeCommand('codev.openArchitectTerminal'); + vscode.commands.executeCommand('codev.openBuilderById', builderId); } }); } diff --git a/packages/vscode/src/views/builders.ts b/packages/vscode/src/views/builders.ts index d47847d95..28c4f5ca3 100644 --- a/packages/vscode/src/views/builders.ts +++ b/packages/vscode/src/views/builders.ts @@ -1,7 +1,30 @@ import * as vscode from 'vscode'; +import type { OverviewBuilder } from '@cluesmith/codev-types'; import type { OverviewCache } from './overview-data.js'; import { BuilderTreeItem } from './builder-tree-item.js'; +/** + * Order builders for the Builders tree: blocked first with the longest- + * waiting at the top, then active builders in overview order. Builders + * with no recorded `blockedSince` sort last within the blocked group + * (we don't pretend to know their wait time). + */ +function orderForDisplay(builders: OverviewBuilder[]): OverviewBuilder[] { + const ms = (iso: string | null) => iso ? new Date(iso).getTime() : Infinity; + const blocked = builders + .filter(b => b.blocked) + .sort((a, b) => ms(a.blockedSince) - ms(b.blockedSince)); + const active = builders.filter(b => !b.blocked); + return [...blocked, ...active]; +} + +/** + * Unified Builders view. Blocked builders sort to the top with a bell icon + * and a wait-time suffix; active builders sit below with a play icon. + * Replaces the previous split between a Needs Attention tree (blocked only) + * and a Builders tree (everything) — the duplication caused more noise than + * the at-a-glance triage was worth. + */ export class BuildersProvider implements vscode.TreeDataProvider { private readonly changeEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.changeEmitter.event; @@ -18,15 +41,21 @@ export class BuildersProvider implements vscode.TreeDataProvider { - const phase = b.blocked ? `[${b.blocked}] blocked` : `[${b.phase}]`; - const item = new BuilderTreeItem(b.id, `#${b.issueId ?? b.id} ${b.issueTitle ?? ''} ${phase}`); + return orderForDisplay(data.builders).map(b => { + const isBlocked = !!b.blocked; + const waitTime = isBlocked && b.blockedSince ? ` [${timeSince(b.blockedSince)}]` : ''; + const phaseLabel = isBlocked + ? `blocked on ${b.blocked}${waitTime}` + : `[${b.phase}]`; + const item = new BuilderTreeItem(b.id, `#${b.issueId ?? b.id} ${b.issueTitle ?? ''} ${phaseLabel}`); item.tooltip = `Protocol: ${b.protocol} | Mode: ${b.mode} | Progress: ${b.progress}%`; - // contextValue encodes protocol so menus can scope by it - // (e.g., codev.viewPlanFile only shows on `builder-pir`). - item.contextValue = `builder-${b.protocol || 'unknown'}`; - item.iconPath = b.blocked - ? new vscode.ThemeIcon('debug-pause', new vscode.ThemeColor('testing.iconFailed')) + // contextValue encodes both blocked-state and protocol so menus can + // scope by either (e.g., inline Approve only on blocked-builder-*). + item.contextValue = isBlocked + ? `blocked-builder-${b.protocol || 'unknown'}` + : `builder-${b.protocol || 'unknown'}`; + item.iconPath = isBlocked + ? new vscode.ThemeIcon('bell', new vscode.ThemeColor('notificationsWarningIcon.foreground')) : new vscode.ThemeIcon('play', new vscode.ThemeColor('testing.iconPassed')); item.command = { command: 'codev.openBuilderById', @@ -37,3 +66,13 @@ export class BuildersProvider implements vscode.TreeDataProvider { - private readonly changeEmitter = new vscode.EventEmitter(); - readonly onDidChangeTreeData = this.changeEmitter.event; - - constructor(private cache: OverviewCache) { - cache.onDidChange(() => this.changeEmitter.fire()); - } - - getTreeItem(element: vscode.TreeItem): vscode.TreeItem { - return element; - } - - getChildren(): vscode.TreeItem[] { - const data = this.cache.getData(); - if (!data) { return []; } - - const items: vscode.TreeItem[] = []; - - // Blocked builders - for (const b of data.builders.filter(b => b.blocked)) { - const waitTime = b.blockedSince - ? `(${timeSince(b.blockedSince)})` - : ''; - const item = new BuilderTreeItem(b.id, `#${b.issueId ?? b.id} — blocked on ${b.blocked} ${waitTime}`); - item.iconPath = new vscode.ThemeIcon('bell', new vscode.ThemeColor('notificationsWarningIcon.foreground')); - // contextValue encodes protocol so menus can scope by it. - item.contextValue = `blocked-builder-${b.protocol || 'unknown'}`; - item.command = { - command: 'codev.openBuilderById', - title: 'Open Builder Terminal', - arguments: [b.id], - }; - items.push(item); - } - - // PRs needing review - for (const pr of data.pendingPRs.filter(p => p.reviewStatus === 'review_required')) { - const item = new vscode.TreeItem(`PR #${pr.id} — ready for review`); - item.iconPath = new vscode.ThemeIcon('git-pull-request'); - item.contextValue = 'pr-needs-review'; - items.push(item); - } - - return items; - } -} - -function timeSince(isoDate: string): string { - const ms = Date.now() - new Date(isoDate).getTime(); - const minutes = Math.floor(ms / 60000); - if (minutes < 60) { return `${minutes}m`; } - const hours = Math.floor(minutes / 60); - if (hours < 24) { return `${hours}h`; } - return `${Math.floor(hours / 24)}d`; -} From 4873c48bb87446569a467b512d1e71379dfa2a65 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 22:02:05 +1000 Subject: [PATCH 25/63] [Bugfix #737] Fix: sync Tower PTY dimensions on terminal open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodevPseudoterminal.open() ignored its initialDimensions argument, so Tower's PTY stayed at node-pty's 80x24 default until the user manually resized the panel. Claude Code's TUI computed its input-box anchor row against that wrong height and rendered it mid-screen, overlapping the streaming response — until a manual resize triggered SIGWINCH and made claude-code redraw at the real dimensions. Cache the latest dimensions (seeded from open(), refreshed on every setDimensions()) and re-send them after every WebSocket auth. Also covers reconnect-after-resize: the new PTY isn't stuck at 80x24 even if a resize happened while the connection was down. --- packages/vscode/src/terminal-adapter.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/vscode/src/terminal-adapter.ts b/packages/vscode/src/terminal-adapter.ts index 09bfb0e7a..5f703dc8c 100644 --- a/packages/vscode/src/terminal-adapter.ts +++ b/packages/vscode/src/terminal-adapter.ts @@ -25,6 +25,11 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal { private lastSeq = 0; private replaying = false; private pendingResize: { cols: number; rows: number } | null = null; + // Latest dimensions VSCode has told us about. Seeded from open()'s + // initialDimensions and refreshed on every setDimensions(). Re-sent after + // every WS auth so Tower's PTY isn't stuck at node-pty's 80×24 default + // until the user happens to manually resize the panel (Bugfix #737). + private lastDimensions: { cols: number; rows: number } | null = null; private queuedBytes = 0; private disposed = false; @@ -34,7 +39,10 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal { private outputChannel: vscode.OutputChannel, ) {} - open(_initialDimensions: vscode.TerminalDimensions | undefined): void { + open(initialDimensions: vscode.TerminalDimensions | undefined): void { + if (initialDimensions) { + this.lastDimensions = { cols: initialDimensions.columns, rows: initialDimensions.rows }; + } // Prime the renderer synchronously inside open() — VS Code drops or // mis-orders writes that arrive purely asynchronously after open() // when the terminal becomes the active editor (microsoft/vscode#108298), @@ -64,6 +72,7 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal { } setDimensions(dimensions: vscode.TerminalDimensions): void { + this.lastDimensions = { cols: dimensions.columns, rows: dimensions.rows }; if (this.replaying) { // Defer resize during replay to prevent garbled rendering (Bugfix #625) this.pendingResize = { cols: dimensions.columns, rows: dimensions.rows }; @@ -87,6 +96,14 @@ export class CodevPseudoterminal implements vscode.Pseudoterminal { if (this.authKey) { this.sendControl({ type: 'ping', payload: { auth: this.authKey } }); } + // Sync Tower's PTY to the dimensions VSCode reported. Without this, + // the PTY stays at node-pty's 80×24 default until a manual resize, + // which makes Claude Code's TUI render its input box mid-screen and + // overlap streaming content. Pause/replay messages arrive *after* + // this outbound resize, so the order is auth → resize → replay → resume. + if (this.lastDimensions) { + this.sendResize(this.lastDimensions.cols, this.lastDimensions.rows); + } }); this.ws.on('message', (raw: ArrayBuffer) => { From dc177c830f33a29493f9d8b142a4060d4f300c1f Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 22:23:16 +1000 Subject: [PATCH 26/63] =?UTF-8?q?refactor(pir):=20align=20PIR=20project=20?= =?UTF-8?q?ID=20with=20SPIR=20=E2=80=94=20drop=20protocol=20prefix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR previously used `pir-` as its porch project ID, which propagated into artifact filenames (`codev/plans/pir-1298-fix-foo.md`, `codev/reviews/pir-1298-fix-foo.md`). That broke consistency with the long-established SPIR / ASPIR / AIR convention of `-.md`. The prefix was an unintentional side effect — I inherited it when generalizing `spawnBugfix` into `spawnIssueDrivenBuilder` (2d835efa) without noticing SPIR did it differently. Protocol identity is already encoded in the worktree dir, branch name, and Tower agent name; the project ID doesn't need to carry it. Changes: - `spawnIssueDrivenBuilder`: for `prefix === 'pir'`, porch project ID is now `String(issueNumber)`. BUGFIX is untouched (`bugfix-` kept for back-compat with on-disk artifacts). - `extractProjectIdFromWorktreeName` (Tower overview): PIR worktrees resolve to the bare numeric ID, matching SPIR / ASPIR / AIR. - `detectProjectIdFromCwd` (porch cwd auto-detect): same — PIR moves from the bugfix group (`-`) to the protocol-numeric group (``). - PIR prompts: artifact paths now use `{{artifact_name}}` (the SPIR idiom that resolves to `-` via porch's prompt template engine — defined in commands/porch/prompts.ts:97). Worktree and `afx dev` references keep the `pir-` prefix since those point at the worktree dir / agent name, both of which still carry it. - Tests: added PIR cases to overview and state. Net effect: PIR now produces `codev/plans/1298-fix-foo.md` and `codev/reviews/1298-fix-foo.md`, matching SPIR. Worktree dir (`.builders/pir-1298-foo/`), branch (`builder/pir-1298`), and Tower agent (`builder-pir-1298`) still carry the protocol prefix for namespace separation. Existing PIR builders need cleanup + respawn to pick up the new project ID (their on-disk status.yaml has the old `pir-N` ID). BUGFIX is untouched and its on-disk artifacts remain valid. --- .../protocols/pir/builder-prompt.md | 4 ++-- .../protocols/pir/prompts/implement.md | 2 +- codev-skeleton/protocols/pir/prompts/plan.md | 10 ++++----- .../protocols/pir/prompts/review.md | 12 +++++----- codev/protocols/pir/builder-prompt.md | 4 ++-- codev/protocols/pir/prompts/implement.md | 2 +- codev/protocols/pir/prompts/plan.md | 10 ++++----- codev/protocols/pir/prompts/review.md | 12 +++++----- .../src/agent-farm/__tests__/overview.test.ts | 8 +++++++ .../codev/src/agent-farm/commands/spawn.ts | 16 +++++++++----- .../codev/src/agent-farm/servers/overview.ts | 7 ++++-- .../commands/porch/__tests__/state.test.ts | 12 ++++++++++ packages/codev/src/commands/porch/state.ts | 22 +++++++++---------- 13 files changed, 75 insertions(+), 46 deletions(-) diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 933c94776..5cbcdef17 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -31,9 +31,9 @@ Follow the PIR protocol: `codev/protocols/pir/protocol.md` Read and internalize the protocol before starting any work. PIR has three phases: -1. **plan** (gated by `plan-approval`) — write `codev/plans/pir-{{project_id}}-.md`, await human review +1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) -3. **review** — write `codev/reviews/pir-{{project_id}}-.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction +3. **review** — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev-skeleton/protocols/pir/prompts/implement.md b/codev-skeleton/protocols/pir/prompts/implement.md index c499611db..9eccc1fc4 100644 --- a/codev-skeleton/protocols/pir/prompts/implement.md +++ b/codev-skeleton/protocols/pir/prompts/implement.md @@ -111,7 +111,7 @@ When the gate goes pending, output a short prose summary in the pane to orient t > > **Things to look at**: tricky spots, platform-specific behavior, anything you want the reviewer to focus on. > -> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev {{project_id}}`. View diff via `git -C .builders/pir-{{project_id}} diff main` or VSCode → **View Diff**. +> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev pir-{{project_id}}`. View diff via VSCode → **View Diff** (auto-detects the repo's default branch). > > Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). diff --git a/codev-skeleton/protocols/pir/prompts/plan.md b/codev-skeleton/protocols/pir/prompts/plan.md index 0e2948013..06468cd0d 100644 --- a/codev-skeleton/protocols/pir/prompts/plan.md +++ b/codev-skeleton/protocols/pir/prompts/plan.md @@ -4,7 +4,7 @@ You are executing the **PLAN** phase of the PIR protocol. ## Your Goal -Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/pir-{{project_id}}-.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. +Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/{{artifact_name}}.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. ## Context @@ -45,7 +45,7 @@ Understand what's being asked. For a bug, identify the symptom. For a feature, i ### 3. Write the Plan -Create `codev/plans/pir-{{project_id}}-.md` where `` is a short kebab-case description of the change. Use this structure: +Create `codev/plans/{{artifact_name}}.md` where `` is a short kebab-case description of the change. Use this structure: ```markdown # PIR Plan: @@ -82,7 +82,7 @@ How to verify this works once implemented. The reviewer will use this at the `co ### 4. Commit and Push ```bash -git add codev/plans/pir-{{project_id}}-.md +git add codev/plans/{{artifact_name}}.md git commit -m "[PIR #{{issue.number}}] Plan draft" git push -u origin "$(git branch --show-current)" ``` @@ -102,7 +102,7 @@ porch next {{project_id}} Output something like: -> Plan written to `codev/plans/pir-{{project_id}}-.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). +> Plan written to `codev/plans/{{artifact_name}}.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). Then **stay in the interactive session**. Do not exit. Wait for the user's next message. @@ -127,7 +127,7 @@ When the reviewer provides feedback (typed in pane, file-edit, `afx send`, or is 1. Re-read the plan file (the user may have edited it) 2. Apply the requested changes to your plan -3. Recommit: `git add codev/plans/pir-{{project_id}}-.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` +3. Recommit: `git add codev/plans/{{artifact_name}}.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` 4. Push 5. Output a short "Revised — see commit X" message 6. Wait for next input — the gate remains pending until the human approves diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index 9a17826c8..dddd981a6 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/pir-{{project_id}}-.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -25,7 +25,7 @@ The retrospective ships with the merged PR — it's durable team knowledge, sear ### 1. Write the Review File -Create `codev/reviews/pir-{{project_id}}-.md` with these sections: +Create `codev/reviews/{{artifact_name}}.md` with these sections: ```markdown # PIR Review: @@ -74,7 +74,7 @@ Tricky spots the PR reviewer should focus on. Honest — if a section was hard t For reviewers pulling the branch: -- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff**, or `git -C .builders/pir-{{project_id}} diff main` +- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff** (auto-detects the repo's default branch) - **Run dev server**: VSCode sidebar → **Run Dev Server**, or `afx dev pir-{{project_id}}` - **What to verify**: @@ -92,7 +92,7 @@ If neither doc needs updating, your review file's sections still need to explain ### 3. Commit the Review File (and arch / lessons updates) ```bash -git add codev/reviews/pir-{{project_id}}-.md +git add codev/reviews/{{artifact_name}}.md # Add arch.md / lessons-learned.md only if you changed them git add codev/resources/arch.md # only if changed git add codev/resources/lessons-learned.md # only if changed @@ -110,13 +110,13 @@ gh pr create \ --base main \ --head "$BRANCH" \ --title "$PR_TITLE" \ - --body-file codev/reviews/pir-{{project_id}}-.md + --body-file codev/reviews/{{artifact_name}}.md ``` **Verify the PR body contains `Fixes #{{issue.number}}`** (it should — the review file has it at the top). If somehow missing, edit and re-apply: ```bash -gh pr edit --body-file codev/reviews/pir-{{project_id}}-.md +gh pr edit --body-file codev/reviews/{{artifact_name}}.md ``` **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 933c94776..5cbcdef17 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -31,9 +31,9 @@ Follow the PIR protocol: `codev/protocols/pir/protocol.md` Read and internalize the protocol before starting any work. PIR has three phases: -1. **plan** (gated by `plan-approval`) — write `codev/plans/pir-{{project_id}}-.md`, await human review +1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) -3. **review** — write `codev/reviews/pir-{{project_id}}-.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction +3. **review** — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev/protocols/pir/prompts/implement.md b/codev/protocols/pir/prompts/implement.md index c499611db..9eccc1fc4 100644 --- a/codev/protocols/pir/prompts/implement.md +++ b/codev/protocols/pir/prompts/implement.md @@ -111,7 +111,7 @@ When the gate goes pending, output a short prose summary in the pane to orient t > > **Things to look at**: tricky spots, platform-specific behavior, anything you want the reviewer to focus on. > -> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev {{project_id}}`. View diff via `git -C .builders/pir-{{project_id}} diff main` or VSCode → **View Diff**. +> **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev pir-{{project_id}}`. View diff via VSCode → **View Diff** (auto-detects the repo's default branch). > > Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). diff --git a/codev/protocols/pir/prompts/plan.md b/codev/protocols/pir/prompts/plan.md index 0e2948013..06468cd0d 100644 --- a/codev/protocols/pir/prompts/plan.md +++ b/codev/protocols/pir/prompts/plan.md @@ -4,7 +4,7 @@ You are executing the **PLAN** phase of the PIR protocol. ## Your Goal -Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/pir-{{project_id}}-.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. +Read the GitHub issue, investigate the codebase, and write a plan to `codev/plans/{{artifact_name}}.md`. The plan is reviewed by a human at the `plan-approval` gate before any code is written. ## Context @@ -45,7 +45,7 @@ Understand what's being asked. For a bug, identify the symptom. For a feature, i ### 3. Write the Plan -Create `codev/plans/pir-{{project_id}}-.md` where `` is a short kebab-case description of the change. Use this structure: +Create `codev/plans/{{artifact_name}}.md` where `` is a short kebab-case description of the change. Use this structure: ```markdown # PIR Plan: @@ -82,7 +82,7 @@ How to verify this works once implemented. The reviewer will use this at the `co ### 4. Commit and Push ```bash -git add codev/plans/pir-{{project_id}}-.md +git add codev/plans/{{artifact_name}}.md git commit -m "[PIR #{{issue.number}}] Plan draft" git push -u origin "$(git branch --show-current)" ``` @@ -102,7 +102,7 @@ porch next {{project_id}} Output something like: -> Plan written to `codev/plans/pir-{{project_id}}-.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). +> Plan written to `codev/plans/{{artifact_name}}.md` and committed. Ready for review — type any feedback here, edit the plan file directly in VSCode, or approve with `porch approve {{project_id}} plan-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). Then **stay in the interactive session**. Do not exit. Wait for the user's next message. @@ -127,7 +127,7 @@ When the reviewer provides feedback (typed in pane, file-edit, `afx send`, or is 1. Re-read the plan file (the user may have edited it) 2. Apply the requested changes to your plan -3. Recommit: `git add codev/plans/pir-{{project_id}}-.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` +3. Recommit: `git add codev/plans/{{artifact_name}}.md && git commit -m "[PIR #{{issue.number}}] Plan revised"` 4. Push 5. Output a short "Revised — see commit X" message 6. Wait for next input — the gate remains pending until the human approves diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index 9a17826c8..dddd981a6 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/pir-{{project_id}}-.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -25,7 +25,7 @@ The retrospective ships with the merged PR — it's durable team knowledge, sear ### 1. Write the Review File -Create `codev/reviews/pir-{{project_id}}-.md` with these sections: +Create `codev/reviews/{{artifact_name}}.md` with these sections: ```markdown # PIR Review: @@ -74,7 +74,7 @@ Tricky spots the PR reviewer should focus on. Honest — if a section was hard t For reviewers pulling the branch: -- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff**, or `git -C .builders/pir-{{project_id}} diff main` +- **View diff**: VSCode sidebar → right-click builder pir-{{project_id}} → **Review Diff** (auto-detects the repo's default branch) - **Run dev server**: VSCode sidebar → **Run Dev Server**, or `afx dev pir-{{project_id}}` - **What to verify**: @@ -92,7 +92,7 @@ If neither doc needs updating, your review file's sections still need to explain ### 3. Commit the Review File (and arch / lessons updates) ```bash -git add codev/reviews/pir-{{project_id}}-.md +git add codev/reviews/{{artifact_name}}.md # Add arch.md / lessons-learned.md only if you changed them git add codev/resources/arch.md # only if changed git add codev/resources/lessons-learned.md # only if changed @@ -110,13 +110,13 @@ gh pr create \ --base main \ --head "$BRANCH" \ --title "$PR_TITLE" \ - --body-file codev/reviews/pir-{{project_id}}-.md + --body-file codev/reviews/{{artifact_name}}.md ``` **Verify the PR body contains `Fixes #{{issue.number}}`** (it should — the review file has it at the top). If somehow missing, edit and re-apply: ```bash -gh pr edit --body-file codev/reviews/pir-{{project_id}}-.md +gh pr edit --body-file codev/reviews/{{artifact_name}}.md ``` **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index 076327d9d..1c783046d 100644 --- a/packages/codev/src/agent-farm/__tests__/overview.test.ts +++ b/packages/codev/src/agent-farm/__tests__/overview.test.ts @@ -781,6 +781,14 @@ describe('overview', () => { expect(extractProjectIdFromWorktreeName('bugfix-296-slug')).toBe('bugfix-296'); }); + it('extracts bare numeric ID from PIR worktree (aligns with SPIR convention)', () => { + expect(extractProjectIdFromWorktreeName('pir-1298-fix-foo')).toBe('1298'); + }); + + it('extracts bare numeric ID from PIR worktree with no slug', () => { + expect(extractProjectIdFromWorktreeName('pir-1298')).toBe('1298'); + }); + it('extracts legacy numeric ID', () => { expect(extractProjectIdFromWorktreeName('0110')).toBe('0110'); }); diff --git a/packages/codev/src/agent-farm/commands/spawn.ts b/packages/codev/src/agent-farm/commands/spawn.ts index 72abb84da..4dfdac4e7 100644 --- a/packages/codev/src/agent-farm/commands/spawn.ts +++ b/packages/codev/src/agent-farm/commands/spawn.ts @@ -754,11 +754,17 @@ async function spawnIssueDrivenBuilder( await ensureDirectories(config); await checkDependencies(); - // -{N} is the porch project ID, distinct from the builder agent - // name (builder--{N}). Used as the project key in - // codev/projects//status.yaml and what `porch next/done/approve` - // expect as their argument. Also matches detectProjectIdFromCwd's regex. - const porchProjectId = `${prefix}-${issueNumber}`; + // Porch project ID: + // - PIR uses the bare issue number (matches SPIR's convention, so + // artifacts land at codev/{plans,reviews}/-.md). + // - BUGFIX uses - (historical, kept untouched). + // + // Distinct from the worktree dir (.builders/-/), the branch + // (builder/-), and the Tower agent name (builder--). + // All four are namespaced differently — porch state ID lines up with + // artifacts; the other three encode protocol for collision-free worktree + // / branch / agent management. + const porchProjectId = prefix === 'pir' ? String(issueNumber) : `${prefix}-${issueNumber}`; if (options.resume) { validateResumeWorktree(worktreePath); diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 3687a01c0..5743c79b6 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -504,9 +504,12 @@ export function extractProjectIdFromWorktreeName(dirName: string): string | null const bugfixMatch = dirName.match(/^bugfix-(\d+)/); if (bugfixMatch) return `bugfix-${bugfixMatch[1]}`; - // PIR: pir-1298-slug → "pir-1298" (porch uses this, mirroring bugfix's convention) + // PIR: pir-1298-slug → "1298" (porch project ID is just the issue + // number, aligning with SPIR's convention so artifacts land in + // codev/{plans,reviews}/-.md without a protocol prefix). + // Worktree dir keeps the `pir-` prefix for namespace separation. const pirMatch = dirName.match(/^pir-(\d+)/); - if (pirMatch) return `pir-${pirMatch[1]}`; + if (pirMatch) return pirMatch[1]; // Legacy numeric: 0110 or 0110-slug → "0110" const numericMatch = dirName.match(/^(\d+)(?:-|$)/); diff --git a/packages/codev/src/commands/porch/__tests__/state.test.ts b/packages/codev/src/commands/porch/__tests__/state.test.ts index d7a61e17a..34598d373 100644 --- a/packages/codev/src/commands/porch/__tests__/state.test.ts +++ b/packages/codev/src/commands/porch/__tests__/state.test.ts @@ -420,6 +420,18 @@ updated_at: "${state.updated_at}" expect(detectProjectIdFromCwd('/repo/.builders/spir-042-feature-name')).toBe('042'); }); + it('should detect numeric ID from pir worktree (aligns with SPIR convention)', () => { + expect(detectProjectIdFromCwd('/repo/.builders/pir-1298-fix-foo')).toBe('1298'); + }); + + it('should detect numeric ID from pir worktree without slug', () => { + expect(detectProjectIdFromCwd('/repo/.builders/pir-1298')).toBe('1298'); + }); + + it('should detect numeric ID from pir worktree subdirectory', () => { + expect(detectProjectIdFromCwd('/repo/.builders/pir-1298-fix-foo/src/file.ts')).toBe('1298'); + }); + it('should detect numeric ID from air worktree', () => { expect(detectProjectIdFromCwd('/repo/.builders/air-100-small-feature')).toBe('100'); }); diff --git a/packages/codev/src/commands/porch/state.ts b/packages/codev/src/commands/porch/state.ts index 7b6800a21..9b97c9109 100644 --- a/packages/codev/src/commands/porch/state.ts +++ b/packages/codev/src/commands/porch/state.ts @@ -305,25 +305,25 @@ export function findStatusPath(workspaceRoot: string, projectId: string): string /** * Detect project ID from the current working directory if inside a builder worktree. * Works from any subdirectory within the worktree. - * Returns the porch project ID (e.g. "bugfix-237" or "0073"), or null if not in a recognized worktree. + * Returns the porch project ID (e.g. "bugfix-237", "1298", or "0073"), or null if not in a recognized worktree. */ export function detectProjectIdFromCwd(cwd: string): string | null { const normalized = path.resolve(cwd).split(path.sep).join('/'); - // Issue-driven worktrees: .builders/{bugfix|pir}-{N}-{slug} (slug optional for legacy paths) - // bugfix and pir use a "{prefix}-{N}" porch project ID. - // Protocol worktrees: .builders/{aspir|spir|air}-{N}-{slug} (slug optional) - // These use the bare numeric ID as the porch project ID. + // bugfix worktrees: .builders/bugfix-{N}-{slug} (slug optional) + // porch project ID is "bugfix-{N}" — historical convention, kept untouched. + // PIR / SPIR / ASPIR / AIR worktrees: .builders/{prefix}-{N}-{slug} (slug optional) + // porch project ID is the bare numeric ID. // Spec worktrees (legacy): .builders/{NNNN} (bare 4-digit ID, no slug) const match = normalized.match( - /\/\.builders\/((bugfix|pir)-(\d+)(?:-[^/]*)?|(?:aspir|spir|air)-(\d+)(?:-[^/]*)?|(\d{4}))(\/|$)/, + /\/\.builders\/(bugfix-(\d+)(?:-[^/]*)?|(?:aspir|spir|air|pir)-(\d+)(?:-[^/]*)?|(\d{4}))(\/|$)/, ); if (!match) return null; - // Issue-driven worktrees (bugfix, pir) use "{prefix}-N" as the porch project ID - if (match[2] && match[3]) return `${match[2]}-${match[3]}`; - // Protocol worktrees (aspir, spir, air) use the bare numeric ID - if (match[4]) return match[4]; + // bugfix uses "bugfix-N" as the porch project ID + if (match[2]) return `bugfix-${match[2]}`; + // Protocol worktrees (aspir, spir, air, pir) use the bare numeric ID + if (match[3]) return match[3]; // Spec worktrees use zero-padded numeric IDs - return match[5]; + return match[4]; } export type ResolvedProjectId = { id: string; source: 'explicit' | 'cwd' | 'filesystem' }; From 8efe11567c51e91bd2d64a48327ce71f2cff7d3d Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Wed, 13 May 2026 22:30:34 +1000 Subject: [PATCH 27/63] docs(pir): scrub stale pir-- filename references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to dc177c83 (PIR project ID aligned with SPIR). The code paths produce the new shape correctly; this commit fixes the docs, comments, and tests that still described the old `pir--` layout. Files updated: - codev/protocols/pir/protocol.md (+ skeleton mirror): "File Locations" block drops the `pir-` prefix on plans/, reviews/, projects/ paths. - codev/protocols/pir/consult-types/impl-review.md (+ skeleton mirror): one stale review-file path. - packages/vscode/src/commands/view-artifact.ts: comment example pir-1298-fix-foo.md → 1298-fix-foo.md (filter logic itself was already correct because builder.id is the porch project ID). - packages/codev/src/commands/porch/artifacts.ts: matchesProjectId docstring moves PIR from the prefix-N group to the numeric group; examples and inline comment updated. - packages/codev/src/commands/porch/__tests__/artifacts.test.ts: - Renamed the "prefix-N project IDs" describe block to make clear the path is now bugfix-only. - Replaced PIR-tagged prefix-N tests with bugfix-style equivalents (the resolver path is unchanged, only the protocol label is). - Added two new tests asserting LocalResolver finds PIR plan/review files under the new `-.md` convention. Migration for in-flight PIR builders spawned before dc177c83: # Option A — clean slate (loses any uncommitted plan draft): afx cleanup -p pir- -f afx spawn --protocol pir # Option B — in-place rename (preserves the plan): cd /.builders/pir- mv codev/projects/pir-- codev/projects/- mv codev/plans/pir--.md codev/plans/-.md # then edit codev/projects/-/status.yaml — change `id: pir-` to `id: ` git add codev/projects codev/plans git commit -m "chore(porch): rename PIR artifacts to SPIR-aligned convention" No grep matches remain for `codev/plans/pir-`, `codev/reviews/pir-`, or `pir--` outside skeleton/protocols (which is the authoritative copy of these same docs). --- .../pir/consult-types/impl-review.md | 2 +- codev-skeleton/protocols/pir/protocol.md | 6 +- .../pir/consult-types/impl-review.md | 2 +- codev/protocols/pir/protocol.md | 6 +- .../porch/__tests__/artifacts.test.ts | 74 +++++++++++-------- .../codev/src/commands/porch/artifacts.ts | 33 +++++---- packages/vscode/src/commands/view-artifact.ts | 10 +-- 7 files changed, 76 insertions(+), 57 deletions(-) diff --git a/codev-skeleton/protocols/pir/consult-types/impl-review.md b/codev-skeleton/protocols/pir/consult-types/impl-review.md index ed8c9bfc9..e05776d87 100644 --- a/codev-skeleton/protocols/pir/consult-types/impl-review.md +++ b/codev-skeleton/protocols/pir/consult-types/impl-review.md @@ -31,7 +31,7 @@ Before requesting changes for missing configuration, incorrect patterns, or fram - For a bug fix: is there a regression test that would fail without the fix? 4. **Review File Quality** - - Does `codev/reviews/pir--.md` exist and follow the template? + - Does `codev/reviews/-.md` exist and follow the template? - Does it accurately describe what changed? - Is "Things to Look At" honest about tricky spots? - Is "How to Test Locally" specific enough that the human reviewer can act on it? diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md index d253aef08..78147be32 100644 --- a/codev-skeleton/protocols/pir/protocol.md +++ b/codev-skeleton/protocols/pir/protocol.md @@ -190,9 +190,9 @@ Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. ## File Locations ``` -codev/plans/pir--.md # written in plan phase, on builder branch -codev/reviews/pir--.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body -codev/projects/pir-/status.yaml # porch state, managed automatically +codev/plans/-.md # written in plan phase, on builder branch +codev/reviews/-.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/projects/-/status.yaml # porch state, managed automatically ``` The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. diff --git a/codev/protocols/pir/consult-types/impl-review.md b/codev/protocols/pir/consult-types/impl-review.md index ed8c9bfc9..e05776d87 100644 --- a/codev/protocols/pir/consult-types/impl-review.md +++ b/codev/protocols/pir/consult-types/impl-review.md @@ -31,7 +31,7 @@ Before requesting changes for missing configuration, incorrect patterns, or fram - For a bug fix: is there a regression test that would fail without the fix? 4. **Review File Quality** - - Does `codev/reviews/pir--.md` exist and follow the template? + - Does `codev/reviews/-.md` exist and follow the template? - Does it accurately describe what changed? - Is "Things to Look At" honest about tricky spots? - Is "How to Test Locally" specific enough that the human reviewer can act on it? diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md index d253aef08..78147be32 100644 --- a/codev/protocols/pir/protocol.md +++ b/codev/protocols/pir/protocol.md @@ -190,9 +190,9 @@ Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. ## File Locations ``` -codev/plans/pir--.md # written in plan phase, on builder branch -codev/reviews/pir--.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body -codev/projects/pir-/status.yaml # porch state, managed automatically +codev/plans/-.md # written in plan phase, on builder branch +codev/reviews/-.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/projects/-/status.yaml # porch state, managed automatically ``` The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. diff --git a/packages/codev/src/commands/porch/__tests__/artifacts.test.ts b/packages/codev/src/commands/porch/__tests__/artifacts.test.ts index bc960236b..9d6b12b37 100644 --- a/packages/codev/src/commands/porch/__tests__/artifacts.test.ts +++ b/packages/codev/src/commands/porch/__tests__/artifacts.test.ts @@ -299,7 +299,11 @@ describe('matchesProjectId', () => { // LocalResolver — prefix-N project ID support (Issue 691) // --------------------------------------------------------------------------- -describe('LocalResolver — prefix-N project IDs', () => { +describe('LocalResolver — prefix-N project IDs (bugfix)', () => { + // PIR was historically in this group but aligned with SPIR's numeric + // convention in commit dc177c83. These tests exercise the prefix-N path + // that is still load-bearing for BUGFIX (and any future issue-driven + // protocol that opts for a prefix-N ID). let tmpDir: string; beforeEach(() => { @@ -310,70 +314,82 @@ describe('LocalResolver — prefix-N project IDs', () => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); - it('getPlanContent finds a PIR plan file by pir-N project ID', () => { + it('getPlanContent finds a prefix-N plan file (bugfix-style)', () => { writeFile( tmpDir, - 'codev/plans/pir-1099-fix-avatar-crop.md', - '# Plan: fix avatar crop\n', + 'codev/plans/bugfix-237-stale-cache.md', + '# Plan: stale cache\n', ); const resolver = new LocalResolver(tmpDir); - const content = resolver.getPlanContent('pir-1099', 'fix-avatar-crop'); - expect(content).toContain('# Plan: fix avatar crop'); + const content = resolver.getPlanContent('bugfix-237', 'stale-cache'); + expect(content).toContain('# Plan: stale cache'); }); - it('getReviewContent finds a PIR review file by pir-N project ID', () => { + it('getReviewContent finds a prefix-N review file (bugfix-style)', () => { writeFile( tmpDir, - 'codev/reviews/pir-1099-fix-avatar-crop.md', - '# Review: fix avatar crop\n', + 'codev/reviews/bugfix-237-stale-cache.md', + '# Review: stale cache\n', ); const resolver = new LocalResolver(tmpDir); - const content = resolver.getReviewContent('pir-1099', 'fix-avatar-crop'); - expect(content).toContain('# Review: fix avatar crop'); + const content = resolver.getReviewContent('bugfix-237', 'stale-cache'); + expect(content).toContain('# Review: stale cache'); }); - it('getPlanContent finds a BUGFIX plan file by bugfix-N project ID', () => { + it('returns null for a missing prefix-N plan', () => { + fs.mkdirSync(path.join(tmpDir, 'codev', 'plans'), { recursive: true }); + + const resolver = new LocalResolver(tmpDir); + expect(resolver.getPlanContent('bugfix-9999', 'nothing-here')).toBeNull(); + }); + + it('does not match a prefix-N plan when looking up a numeric ID with the same digits', () => { + // Regression guard: "bugfix-237-foo.md" must NOT be returned for projectId="237". writeFile( tmpDir, - 'codev/plans/bugfix-237-stale-cache.md', - '# Plan: stale cache\n', + 'codev/plans/bugfix-237-stale.md', + '# Plan: bugfix stale cache\n', ); const resolver = new LocalResolver(tmpDir); - const content = resolver.getPlanContent('bugfix-237', 'stale-cache'); - expect(content).toContain('# Plan: stale cache'); + expect(resolver.getPlanContent('237', '')).toBeNull(); }); - it('returns null for a missing PIR plan', () => { - fs.mkdirSync(path.join(tmpDir, 'codev', 'plans'), { recursive: true }); + it('still works for numeric project IDs (SPIR / ASPIR / AIR / PIR)', () => { + writeFile( + tmpDir, + 'codev/plans/0073-user-auth.md', + '# Plan: user auth\n', + ); const resolver = new LocalResolver(tmpDir); - expect(resolver.getPlanContent('pir-9999', 'nothing-here')).toBeNull(); + expect(resolver.getPlanContent('0073', 'user-auth')).toContain('# Plan: user auth'); + expect(resolver.getPlanContent('73', 'user-auth')).toContain('# Plan: user auth'); }); - it('does not match PIR plan when looking up a numeric ID with the same digits', () => { - // Regression guard: "pir-1099-foo.md" must NOT be returned for projectId="1099". + it('finds a PIR plan file by bare numeric ID (post-dc177c83 convention)', () => { writeFile( tmpDir, - 'codev/plans/pir-1099-fix-avatar.md', - '# Plan: PIR avatar fix\n', + 'codev/plans/1298-fix-native-social-login.md', + '# Plan: fix social login\n', ); const resolver = new LocalResolver(tmpDir); - expect(resolver.getPlanContent('1099', '')).toBeNull(); + const content = resolver.getPlanContent('1298', 'fix-native-social-login'); + expect(content).toContain('# Plan: fix social login'); }); - it('still works for numeric project IDs (regression guard for SPIR/ASPIR/AIR)', () => { + it('finds a PIR review file by bare numeric ID (post-dc177c83 convention)', () => { writeFile( tmpDir, - 'codev/plans/0073-user-auth.md', - '# Plan: user auth\n', + 'codev/reviews/1298-fix-native-social-login.md', + '# Review: fix social login\n', ); const resolver = new LocalResolver(tmpDir); - expect(resolver.getPlanContent('0073', 'user-auth')).toContain('# Plan: user auth'); - expect(resolver.getPlanContent('73', 'user-auth')).toContain('# Plan: user auth'); + const content = resolver.getReviewContent('1298', 'fix-native-social-login'); + expect(content).toContain('# Review: fix social login'); }); }); diff --git a/packages/codev/src/commands/porch/artifacts.ts b/packages/codev/src/commands/porch/artifacts.ts index b94191969..7145ec593 100644 --- a/packages/codev/src/commands/porch/artifacts.ts +++ b/packages/codev/src/commands/porch/artifacts.ts @@ -44,26 +44,29 @@ export interface ArtifactResolver { * * Supports two project-ID formats used in this codebase: * - * 1. **Numeric** (SPIR, ASPIR, AIR): e.g. `"0073"`. Matches filenames whose - * leading digits, zero-stripped, equal the project ID (also zero-stripped). - * Example: `"0073"` matches `"0073-feature.md"` and `"73-feature.md"`. + * 1. **Numeric** (SPIR, ASPIR, AIR, PIR): e.g. `"0073"` or `"1298"`. Matches + * filenames whose leading digits, zero-stripped, equal the project ID + * (also zero-stripped). Example: `"0073"` matches `"0073-feature.md"` + * and `"73-feature.md"`. * - * 2. **Prefix-N** (BUGFIX, PIR, any issue-driven protocol): e.g. `"pir-1099"` - * or `"bugfix-237"`. Matches filenames that equal `-` or start - * with `--`. Example: `"pir-1099"` matches - * `"pir-1099-fix-avatar.md"` but NOT `"pir-1099fix.md"`. + * 2. **Prefix-N** (BUGFIX only — historical, kept for back-compat with + * existing on-disk artifacts): e.g. `"bugfix-237"`. Matches filenames + * that equal `-` or start with `--`. Example: + * `"bugfix-237"` matches `"bugfix-237-stale-cache.md"`. * - * Without this distinction, the previous regex `name.match(/^(\d+)/)` silently - * failed to find any file for prefix-N IDs, breaking `plan_exists` and other - * artifact-resolution checks for BUGFIX and PIR projects. PIR exposed the bug - * because it was the first issue-driven protocol with `plan_exists` configured. + * Without this distinction, the previous regex `name.match(/^(\d+)/)` + * silently failed for prefix-N IDs, breaking `plan_exists` and other + * artifact-resolution checks for BUGFIX projects. (PIR exposed the bug + * historically when it also used prefix-N IDs; PIR has since been aligned + * with SPIR's numeric convention — see commit dc177c83.) */ export function matchesProjectId(name: string, projectId: string): boolean { const base = name.replace(/\.md$/, ''); // Prefix-N format: `-` (one or more hyphen-separated letter - // segments followed by a numeric segment). Catches "pir-1099", "bugfix-237", - // and hypothetical future "issue-driven-237". + // segments followed by a numeric segment). Today this catches "bugfix-237"; + // the shape is kept general for any future issue-driven protocol that opts + // for a prefix-N ID. if (/^[a-z]+(?:-[a-z]+)*-\d+$/i.test(projectId)) { return base === projectId || base.startsWith(`${projectId}-`); } @@ -242,9 +245,9 @@ export class CliResolver implements ArtifactResolver { } hasPreApproval(artifactGlob: string): boolean { - // Determine artifact type from glob path (e.g., "codev/specs/0559-*.md" or "codev/plans/pir-1099-*.md") + // Determine artifact type from glob path (e.g., "codev/specs/0559-*.md" or "codev/plans/bugfix-237-*.md") const typeMatch = artifactGlob.match(/\b(specs|plans|reviews)\b/); - // Extract project ID — supports both numeric ("0073") and prefix-N ("pir-1099", "bugfix-237") formats. + // Extract project ID — supports both numeric ("0073", "1298") and prefix-N ("bugfix-237") formats. const prefixedMatch = artifactGlob.match(/(?:specs|plans|reviews)\/([a-z]+(?:-[a-z]+)*-\d+)/i); const numericMatch = artifactGlob.match(/(?:specs|plans|reviews)\/0*(\d+)/); const projectId = prefixedMatch?.[1] ?? numericMatch?.[1]; diff --git a/packages/vscode/src/commands/view-artifact.ts b/packages/vscode/src/commands/view-artifact.ts index a9b397c87..5be34539e 100644 --- a/packages/vscode/src/commands/view-artifact.ts +++ b/packages/vscode/src/commands/view-artifact.ts @@ -71,11 +71,11 @@ async function viewArtifact( return; } - // Filter to files belonging to THIS builder. Plan/review files written by - // PIR builders are named `-.md` (e.g. `pir-1298-fix-foo.md`), - // so the builder ID prefix is the natural filter. Other builders' files for - // the same protocol live in the same dir and would otherwise show up in the - // quick-pick. + // Filter to files belonging to THIS builder. Plan/review files are named + // `-.md` (e.g. `1298-fix-foo.md`) where `` matches the + // builder's porch project ID, so the builder ID prefix is the natural + // filter. Other builders' files for the same protocol live in the same + // dir and would otherwise show up in the quick-pick. const builderPrefix = `${builder.id}-`; const files = readdirSync(artifactDir) .filter(f => From ac934b0cd7d886b0b3818d34e10607da3c47c13c Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 11:33:43 +1000 Subject: [PATCH 28/63] refactor(vscode): replace View Diff with Open Worktree in New Window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous View Diff command tried to render a multi-file diff in the current VSCode window via `vscode.changes` + `git:` URIs. It silently failed — title showed "(2 files)" but the body said "No Changed Files" — because VSCode's Git extension doesn't auto-discover worktrees that live inside the host workspace's gitignore. Tried fixing URI shape (absolute path in query, ref-based instead of empty-ref); didn't help because the Git extension wasn't looking at the worktree's git database in the first place. The right shape is a custom TextDocumentContentProvider that resolves content via `git -C show :` ourselves — bypassing the Git extension entirely. That's a fair chunk of code and a separate concern; meanwhile reviewers need something that works. Replace View Diff with **Open Worktree in New Window**: `vscode.commands.executeCommand('vscode.openFolder', uri, true)` opens a new editor window rooted at `.builders//`. In that window the Git extension treats the worktree as a primary checkout — SCM panel, working-tree changes, inline diffs against the branch base all work natively. `vscode.openFolder` is editor-agnostic — VSCode, Cursor, Windsurf, and other VSCode-family editors all expose it identically. No CLI detection needed. Trade-off: review happens in a separate window instead of inline. Acceptable for now given that (a) the inline path didn't work, and (b) the new-window UX is closer to how reviewers actually work (focused on the worktree, full SCM panel available). Removed: - packages/vscode/src/commands/review-diff.ts (~180 LOC) - `codev.reviewDiff` command declaration and menu entry Added: - packages/vscode/src/commands/open-worktree-window.ts (~75 LOC) - `codev.openWorktreeWindow` declaration + menu entry in slot 3_review@1 (replaces View Diff's slot) --- packages/vscode/package.json | 6 +- .../src/commands/open-worktree-window.ts | 75 +++++++ packages/vscode/src/commands/review-diff.ts | 184 ------------------ packages/vscode/src/extension.ts | 6 +- .../vscode/src/notifications/gate-toast.ts | 2 +- .../vscode/src/views/builder-tree-item.ts | 7 +- 6 files changed, 86 insertions(+), 194 deletions(-) create mode 100644 packages/vscode/src/commands/open-worktree-window.ts delete mode 100644 packages/vscode/src/commands/review-diff.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index e154926b8..b030d41ef 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -106,8 +106,8 @@ "title": "Codev: Add Review Comment" }, { - "command": "codev.reviewDiff", - "title": "Codev: View Diff" + "command": "codev.openWorktreeWindow", + "title": "Codev: Open Worktree in New Window" }, { "command": "codev.runWorktreeDev", @@ -157,7 +157,7 @@ "group": "2_files@1" }, { - "command": "codev.reviewDiff", + "command": "codev.openWorktreeWindow", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "3_review@1" }, diff --git a/packages/vscode/src/commands/open-worktree-window.ts b/packages/vscode/src/commands/open-worktree-window.ts new file mode 100644 index 000000000..a296ddd5b --- /dev/null +++ b/packages/vscode/src/commands/open-worktree-window.ts @@ -0,0 +1,75 @@ +/** + * Codev: Open Worktree as Workspace — opens a builder's worktree in a new + * editor window. The new window is rooted at `.builders//`, so VSCode's + * built-in Git extension treats it as a primary checkout: the SCM panel + * shows working-tree changes, "View Working Tree Changes" shows committed + * diff vs the default branch, and inline-editor diffs work natively. + * + * Replaces the previous "View Diff" command, which tried to render a + * multi-file diff in the current window via `vscode.changes` + `git:` URIs + * but couldn't, because VSCode's Git extension doesn't auto-discover + * worktrees inside `.gitignore`d subdirectories of the host workspace. + * + * Editor-agnostic: `vscode.openFolder` is a built-in command exposed by + * VSCode, Cursor, Windsurf, and other VSCode-family editors. No CLI + * detection needed. + */ + +import * as vscode from 'vscode'; +import type { ConnectionManager } from '../connection-manager.js'; + +export async function openWorktreeWindow( + connectionManager: ConnectionManager, + builderIdArg: string | undefined, +): Promise { + const client = connectionManager.getClient(); + const workspacePath = connectionManager.getWorkspacePath(); + if (!client || !workspacePath || connectionManager.getState() !== 'connected') { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + + const overview = await client.getOverview(workspacePath); + const builders = overview?.builders ?? []; + if (builders.length === 0) { + vscode.window.showInformationMessage('Codev: No builders available'); + return; + } + + const builder = builderIdArg + ? builders.find(b => b.id === builderIdArg) + : await pickBuilder(builders); + if (!builder) { + if (builderIdArg) { + vscode.window.showErrorMessage(`Codev: No builder found for "${builderIdArg}"`); + } + return; + } + if (!builder.worktreePath) { + vscode.window.showErrorMessage(`Codev: Builder ${builder.id} has no worktree on record`); + return; + } + + await vscode.commands.executeCommand( + 'vscode.openFolder', + vscode.Uri.file(builder.worktreePath), + /* forceNewWindow */ true, + ); +} + +interface BuilderLike { + id: string; + issueId: string | null; + issueTitle: string | null; +} + +async function pickBuilder(builders: T[]): Promise { + const picked = await vscode.window.showQuickPick( + builders.map(b => ({ + label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, + builder: b, + })), + { placeHolder: 'Select builder to open in new window' }, + ); + return picked?.builder; +} diff --git a/packages/vscode/src/commands/review-diff.ts b/packages/vscode/src/commands/review-diff.ts deleted file mode 100644 index bcc34f948..000000000 --- a/packages/vscode/src/commands/review-diff.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * Codev: View Diff — open `...HEAD` for a builder's worktree - * as a single multi-file diff editor (matches VSCode's built-in "Working - * Tree" view in the Source Control panel). - * - * Right-click a builder row → "View Diff". Opens ONE tab with a file - * list on the left and a diff that updates as the reviewer clicks each - * file. Handles added / modified / deleted files uniformly via VSCode's - * `vscode.changes` command. - * - * Default branch is detected per-worktree via `git symbolic-ref - * refs/remotes/origin/HEAD`. Repos using `master`, `develop`, `trunk`, - * etc. work without configuration. Falls back to `main` if the symbolic - * ref isn't set. - * - * Why this works across worktrees: each `.builders//` is a real git - * worktree linked to the parent repo's `.git`, so all branches live in the - * shared object database. - */ - -import * as vscode from 'vscode'; -import { execFile } from 'node:child_process'; -import { promisify } from 'node:util'; -import * as path from 'node:path'; -import type { ConnectionManager } from '../connection-manager.js'; - -const execFileAsync = promisify(execFile); - -export async function reviewDiff( - connectionManager: ConnectionManager, - builderIdArg: string | undefined, -): Promise { - const client = connectionManager.getClient(); - const workspacePath = connectionManager.getWorkspacePath(); - if (!client || !workspacePath || connectionManager.getState() !== 'connected') { - vscode.window.showErrorMessage('Codev: Not connected to Tower'); - return; - } - - // Resolve the builder. Quick-pick fallback for command-palette invocation. - const overview = await client.getOverview(workspacePath); - const builders = overview?.builders ?? []; - if (builders.length === 0) { - vscode.window.showInformationMessage('Codev: No builders to diff'); - return; - } - - const builder = builderIdArg - ? builders.find(b => b.id === builderIdArg) - : await pickBuilder(builders); - if (!builder) { - if (builderIdArg) { - vscode.window.showErrorMessage(`Codev: No builder found for "${builderIdArg}"`); - } - return; - } - if (!builder.worktreePath) { - vscode.window.showErrorMessage(`Codev: Builder ${builder.id} has no worktree on record`); - return; - } - - // Detect the repo's default branch from origin/HEAD. Resolves to a - // branch name (e.g. `main`, `master`, `develop`). Falls back to `main` - // if origin/HEAD isn't set locally. - let defaultBranch = 'main'; - try { - const { stdout } = await execFileAsync('git', [ - '-C', builder.worktreePath, - 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD', - ]); - // Strip the `origin/` prefix to get just the branch name. - const ref = stdout.trim().replace(/^origin\//, ''); - if (ref) { defaultBranch = ref; } - } catch { - // origin/HEAD not set — keep the `main` default. The diff will still - // run; if `main` doesn't exist locally either, the next git call - // surfaces a clear error. - } - - // Enumerate changed files with status letters so we can handle - // added (A) / deleted (D) files distinctly from modified (M). - let changes: Array<{ status: string; path: string }>; - try { - const { stdout } = await execFileAsync('git', [ - '-C', builder.worktreePath, - 'diff', '--name-status', `${defaultBranch}...HEAD`, - ]); - changes = stdout - .split('\n') - .map(s => s.trim()) - .filter(Boolean) - .map(line => { - // Status letter, then tab(s), then the path. For renames/copies the - // line is "R100\told\tnew" — take the new (last) path. - const parts = line.split('\t'); - return { status: parts[0]![0]!, path: parts[parts.length - 1]! }; - }); - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - vscode.window.showErrorMessage(`Codev: git diff failed — ${message}`); - return; - } - - if (changes.length === 0) { - vscode.window.showInformationMessage(`Codev: No changes to review yet for ${builder.id}`); - return; - } - - // Build the resources array for vscode.changes. Each entry is a tuple of - // [resourceUri, leftUri, rightUri]: - // - resourceUri: the file as it lives in the worktree (used for icon / - // file-list display) - // - leftUri: main's version (empty for added files) - // - rightUri: worktree's version (empty for deleted files) - // Empty sides are signalled with the `git:` URI at a non-existent ref — - // VSCode's diff editor renders that as a one-sided view. - const resources: Array<[vscode.Uri, vscode.Uri, vscode.Uri]> = changes.map(({ status, path: rel }) => { - const abs = path.join(builder.worktreePath, rel); - const resourceUri = vscode.Uri.file(abs); - const baseUri = toGitUri(abs, rel, defaultBranch); - const headUri = vscode.Uri.file(abs); - - if (status === 'A') { - // Added: no base version. Left = empty, right = file. - return [resourceUri, emptyGitUri(abs, rel), headUri]; - } - if (status === 'D') { - // Deleted: no worktree version. Left = base, right = empty. - return [resourceUri, baseUri, emptyGitUri(abs, rel)]; - } - // Modified / renamed / copied / unmerged → side-by-side diff - return [resourceUri, baseUri, headUri]; - }); - - await vscode.commands.executeCommand( - 'vscode.changes', - `Reviewing ${builder.id} (${defaultBranch} ↔ HEAD)`, - resources, - ); -} - -/** - * Build a `git:` URI VSCode's built-in Git extension resolves against the - * worktree's shared object database. Matches the Git extension's canonical - * `toGitUri` shape: scheme=git, path=fsPath, query=JSON of `{ path, ref }`. - */ -function toGitUri(absPath: string, relPath: string, ref: string): vscode.Uri { - return vscode.Uri.file(absPath).with({ - scheme: 'git', - query: JSON.stringify({ path: relPath, ref }), - }); -} - -/** - * URI that renders as empty content in the diff editor. Used for the - * "missing" side of added/deleted files. We use a `git:` URI at a known - * empty ref (`HEAD~0` resolves but the file doesn't exist for added files; - * the Git extension returns empty content for unresolved paths at a valid - * ref). If that ever becomes flaky, the alternative is a `data:` URI or - * an untitled scheme. - */ -function emptyGitUri(absPath: string, relPath: string): vscode.Uri { - return vscode.Uri.file(absPath).with({ - scheme: 'git', - query: JSON.stringify({ path: relPath, ref: '' }), - }); -} - -interface BuilderLike { - id: string; - issueId: string | null; - issueTitle: string | null; -} - -async function pickBuilder(builders: T[]): Promise { - const picked = await vscode.window.showQuickPick( - builders.map(b => ({ - label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, - builder: b, - })), - { placeHolder: 'Select builder to diff against main' }, - ); - return picked?.builder; -} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 829b2da61..a07c8447d 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -6,7 +6,7 @@ import { spawnBuilder } from './commands/spawn.js'; import { sendMessage } from './commands/send.js'; import { approveGate } from './commands/approve.js'; import { cleanupBuilder } from './commands/cleanup.js'; -import { reviewDiff } from './commands/review-diff.js'; +import { openWorktreeWindow } from './commands/open-worktree-window.js'; import { runWorktreeDev } from './commands/run-worktree-dev.js'; import { stopWorktreeDev } from './commands/stop-worktree-dev.js'; import { openWorktreeFolder } from './commands/open-worktree-folder.js'; @@ -209,8 +209,8 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined) => approveGate(connectionManager!, overviewCache, extractBuilderId(arg))), vscode.commands.registerCommand('codev.cleanupBuilder', () => cleanupBuilder(connectionManager!, overviewCache)), - vscode.commands.registerCommand('codev.reviewDiff', (arg: vscode.TreeItem | string | undefined) => - reviewDiff(connectionManager!, extractBuilderId(arg))), + vscode.commands.registerCommand('codev.openWorktreeWindow', (arg: vscode.TreeItem | string | undefined) => + openWorktreeWindow(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.runWorktreeDev', (arg: vscode.TreeItem | string | undefined) => runWorktreeDev(connectionManager!, terminalManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.stopWorktreeDev', () => diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index a08fb57fe..26c31b7f1 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -10,7 +10,7 @@ import type { OverviewCache } from '../views/overview-data.js'; * the **architect** terminal — porch orchestrates through the architect, * so user-driven review starts there. * - * (Direct artifact access — View Diff, View Plan File, View Review File, + * (Direct artifact access — Open Worktree in New Window, View Plan File, * Run Dev Server — is available via right-click on builder rows in the * sidebar. The toast intentionally does not duplicate those entry points.) * diff --git a/packages/vscode/src/views/builder-tree-item.ts b/packages/vscode/src/views/builder-tree-item.ts index d8affe0d3..a83105d47 100644 --- a/packages/vscode/src/views/builder-tree-item.ts +++ b/packages/vscode/src/views/builder-tree-item.ts @@ -4,13 +4,14 @@ import * as vscode from 'vscode'; * TreeItem subclass that carries a builder id as a typed field. * * Why: VSCode passes the tree item itself (not its `command.arguments`) - * to commands invoked from `view/item/context` menus. The new #690 - * commands (codev.reviewDiff, codev.runWorktreeDev, codev.stopWorktreeDev) + * to commands invoked from `view/item/context` menus. Builder-scoped + * commands (codev.openWorktreeWindow, codev.runWorktreeDev, + * codev.stopWorktreeDev, codev.viewPlanFile, codev.approveGate, etc.) * need to know which builder was right-clicked, so the views construct * builder rows with this class and the command handlers narrow via * `instanceof BuilderTreeItem` to read `.builderId` safely. * - * Used by views/builders.ts and views/needs-attention.ts. + * Used by views/builders.ts. */ export class BuilderTreeItem extends vscode.TreeItem { constructor( From 3056f70cf0fe1ac7603445bceabc3349efe941a3 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 14:40:32 +1000 Subject: [PATCH 29/63] =?UTF-8?q?fix(pir):=20align=20CMAP=20execution=20wi?= =?UTF-8?q?th=20SPIR=20=E2=80=94=20porch-driven,=20no=20double-run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR was running CMAP-2 twice: 1. Builder followed review.md step 5 ("Run CMAP-2 Review") which shelled out to `consult -m gemini` + `consult -m codex` directly — copied from the BUGFIX/AIR prompt pattern. 2. Porch's `verify` block in protocol.json then re-ran the same CMAP-2 when `porch done` was called — the SPIR pattern. The two paths don't coordinate. Builder's manual consults never updated porch's verify state, so porch saw verify-incomplete and fired the consults itself. Visible symptom: after merge + cleanup, a second consult cycle kicks off (caught in the field on pir-1298). Pick a single pattern: align with SPIR (porch-driven). SPIR's prompts explicitly forbid manual consults — "Don't run consult commands yourself (porch handles consultations)" — precisely to prevent this duplication mode. Changes to review.md: - Goal: clarify that porch runs CMAP, not the builder. - Step 5 (was "Run CMAP-2 Review"): replaced with "Signal Completion to Porch" — calls `porch done` which triggers the verify block. - Step 6 (was "Address Any REQUEST_CHANGES"): replaced with "Handle Reviewer Feedback" — uses SPIR's pattern of reading `porch next` output for feedback baked into the task description. - Step 7 ("Append CMAP Outcome to PR Body"): DELETED. CMAP outputs live in `codev/projects/-*/-.txt` (porch state), not in the PR body — matches SPIR's behavior for its review-phase CMAP-pr verdicts. - Step 8 (notify architect): builder now reads verdicts from porch state files (grep) to compose the architect message, rather than capturing from its own consult invocations. - Step 11 (was "porch done after merge"): merged into step 10 (final notification only). The merge is a GitHub action, not a porch transition — porch already completed the review phase at the successful step-5 `porch done` (whichever iteration got all-APPROVE). - "What NOT to Do" gains: "Don't run consult commands yourself — porch handles consultations via the verify block." - "Handling Problems" updated: model-unavailable failures are now porch's problem, not the builder's manual retry loop. CMAP outputs are NOT lost. Porch writes them to codev/projects/-/-{gemini,codex}.txt — same persistence as SPIR. Visible to the builder, the architect, and anyone with the worktree. The only thing not auto-surfaced is the PR body for GitHub reviewers — that gap exists in SPIR's review-phase CMAP-pr too, and is acceptable for PIR specifically because human approval already happened at the pre-PR code-review gate. Mirror in codev-skeleton/protocols/pir/prompts/review.md. --- .../protocols/pir/prompts/review.md | 75 +++++++++---------- codev/protocols/pir/prompts/review.md | 75 +++++++++---------- 2 files changed, 70 insertions(+), 80 deletions(-) diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index dddd981a6..cf301e12e 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, then signal completion to porch — **porch runs CMAP-2 (Gemini + Codex) automatically**. After porch confirms approval, notify the architect and merge on instruction. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -121,63 +121,55 @@ gh pr edit --body-file codev/reviews/{{artifact_name}}.md **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. -### 5. Run CMAP-2 Review - -Run 2-way parallel consultation on the PR (type=impl — same consult type BUGFIX and AIR use at their PR-creation phase): +### 5. Signal Completion to Porch (porch runs CMAP-2) ```bash -consult -m gemini --protocol pir --type impl & -consult -m codex --protocol pir --type impl & +porch done {{project_id}} ``` -Both should run in the background (`run_in_background: true`). **DO NOT proceed until both return verdicts.** - -Wait for each consultation. Use `TaskOutput` to retrieve results. Record each verdict (APPROVE / REQUEST_CHANGES / COMMENT). +Porch will: +1. Run the `pr_exists` / `review_has_arch_updates` / `review_has_lessons_updates` checks. +2. **Execute CMAP-2 (Gemini + Codex) automatically** via the protocol's `verify` block. Outputs land in `codev/projects/{{project_id}}-/{{project_id}}-gemini.txt` and `-codex.txt`. +3. Evaluate verdicts: + - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). + - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. > **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. -### 6. Address Any REQUEST_CHANGES - -If any reviewer requested changes: - -1. Read the specific issues -2. Fix them in code -3. Run build + tests -4. Push the updates -5. Re-run CMAP only if substantial changes were made - -End with concrete verdicts from both models before continuing. +### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) -### 7. Append CMAP Outcome to PR Body +If `porch done` reports any reviewer requested changes, run `porch next {{project_id}}` — it returns `status: tasks` with the reviewer feedback baked into the task description. Then: -Once you have both verdicts, append them to the PR body: +1. Read the specific issues from the task output (and from `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for full context). +2. Fix them in code. +3. Run build + tests. +4. Commit + push the updates (the PR updates automatically — no new `gh pr create`). +5. Run `porch done {{project_id}}` again. Porch re-runs CMAP-2 against the updated diff. -```markdown -## CMAP Review - -- **Gemini**: APPROVE / REQUEST_CHANGES (one-line summary) -- **Codex**: APPROVE / REQUEST_CHANGES (one-line summary) -``` +Loop until porch reports all reviewers APPROVE. -Use `gh pr edit --body-file ` to apply. +### 7. Notify the Architect (after porch approves) -### 8. Notify the Architect +After `porch done` reports success (all reviewers APPROVE + checks pass), read the verdicts from porch state and notify the architect: ```bash -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=, codex=" +GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) +CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) + +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Full verdicts in codev/projects/{{project_id}}-*/." ``` This is the only notification you send. -### 9. Wait for Merge Instruction +### 8. Wait for Merge Instruction The architect reviews the PR. They will either: -- Tell you to merge → run the merge command (step 10) -- Request more changes → address them (loop back to step 5) +- Tell you to merge → run the merge command (step 9) +- Request more changes → push fixes and re-run `porch done {{project_id}}` (loops back to step 6) - Tell you to close without merging → `gh pr close ` and stop -### 10. Merge the PR +### 9. Merge the PR ```bash gh pr merge --merge @@ -187,13 +179,14 @@ gh pr merge --merge The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. -### 11. Final Notification + Signal Phase Complete +### 10. Final Notification ```bash afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." -porch done {{project_id}} ``` +Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). No additional `porch done` call is needed — the merge is a GitHub action, not a porch state transition. + ## Signals ``` @@ -208,6 +201,7 @@ porch done {{project_id}} - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) +- **Don't run `consult` commands yourself** — porch handles consultations via the `verify` block. Manually invoking `consult` causes CMAP to run twice. ## Handling Problems @@ -217,9 +211,10 @@ porch done {{project_id}} - Force-push with lease: `git push --force-with-lease` - Re-run `gh pr create` -**If CMAP consults fail (e.g., model unavailable):** -- Retry once -- If still failing, notify the architect and ask whether to proceed without that model's verdict +**If porch's CMAP consults fail (e.g., model unavailable):** +- `porch done` will report the failure. Inspect `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for the failure details. +- Re-run `porch done {{project_id}}` once — porch will retry the consult. +- If the model is persistently unavailable, notify the architect and ask whether to proceed without that model's verdict. They may direct you to skip via a manual override. **If the architect doesn't respond within a reasonable window:** - Send one follow-up via `afx send architect "..."` after a few hours diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index dddd981a6..cf301e12e 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Then push, open a PR using the review file as the PR body, run CMAP-2 (Gemini + Codex), notify the architect, and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, then signal completion to porch — **porch runs CMAP-2 (Gemini + Codex) automatically**. After porch confirms approval, notify the architect and merge on instruction. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -121,63 +121,55 @@ gh pr edit --body-file codev/reviews/{{artifact_name}}.md **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. -### 5. Run CMAP-2 Review - -Run 2-way parallel consultation on the PR (type=impl — same consult type BUGFIX and AIR use at their PR-creation phase): +### 5. Signal Completion to Porch (porch runs CMAP-2) ```bash -consult -m gemini --protocol pir --type impl & -consult -m codex --protocol pir --type impl & +porch done {{project_id}} ``` -Both should run in the background (`run_in_background: true`). **DO NOT proceed until both return verdicts.** - -Wait for each consultation. Use `TaskOutput` to retrieve results. Record each verdict (APPROVE / REQUEST_CHANGES / COMMENT). +Porch will: +1. Run the `pr_exists` / `review_has_arch_updates` / `review_has_lessons_updates` checks. +2. **Execute CMAP-2 (Gemini + Codex) automatically** via the protocol's `verify` block. Outputs land in `codev/projects/{{project_id}}-/{{project_id}}-gemini.txt` and `-codex.txt`. +3. Evaluate verdicts: + - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). + - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. > **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. -### 6. Address Any REQUEST_CHANGES - -If any reviewer requested changes: - -1. Read the specific issues -2. Fix them in code -3. Run build + tests -4. Push the updates -5. Re-run CMAP only if substantial changes were made - -End with concrete verdicts from both models before continuing. +### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) -### 7. Append CMAP Outcome to PR Body +If `porch done` reports any reviewer requested changes, run `porch next {{project_id}}` — it returns `status: tasks` with the reviewer feedback baked into the task description. Then: -Once you have both verdicts, append them to the PR body: +1. Read the specific issues from the task output (and from `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for full context). +2. Fix them in code. +3. Run build + tests. +4. Commit + push the updates (the PR updates automatically — no new `gh pr create`). +5. Run `porch done {{project_id}}` again. Porch re-runs CMAP-2 against the updated diff. -```markdown -## CMAP Review - -- **Gemini**: APPROVE / REQUEST_CHANGES (one-line summary) -- **Codex**: APPROVE / REQUEST_CHANGES (one-line summary) -``` +Loop until porch reports all reviewers APPROVE. -Use `gh pr edit --body-file ` to apply. +### 7. Notify the Architect (after porch approves) -### 8. Notify the Architect +After `porch done` reports success (all reviewers APPROVE + checks pass), read the verdicts from porch state and notify the architect: ```bash -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=, codex=" +GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) +CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) + +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Full verdicts in codev/projects/{{project_id}}-*/." ``` This is the only notification you send. -### 9. Wait for Merge Instruction +### 8. Wait for Merge Instruction The architect reviews the PR. They will either: -- Tell you to merge → run the merge command (step 10) -- Request more changes → address them (loop back to step 5) +- Tell you to merge → run the merge command (step 9) +- Request more changes → push fixes and re-run `porch done {{project_id}}` (loops back to step 6) - Tell you to close without merging → `gh pr close ` and stop -### 10. Merge the PR +### 9. Merge the PR ```bash gh pr merge --merge @@ -187,13 +179,14 @@ gh pr merge --merge The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. -### 11. Final Notification + Signal Phase Complete +### 10. Final Notification ```bash afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." -porch done {{project_id}} ``` +Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). No additional `porch done` call is needed — the merge is a GitHub action, not a porch state transition. + ## Signals ``` @@ -208,6 +201,7 @@ porch done {{project_id}} - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) +- **Don't run `consult` commands yourself** — porch handles consultations via the `verify` block. Manually invoking `consult` causes CMAP to run twice. ## Handling Problems @@ -217,9 +211,10 @@ porch done {{project_id}} - Force-push with lease: `git push --force-with-lease` - Re-run `gh pr create` -**If CMAP consults fail (e.g., model unavailable):** -- Retry once -- If still failing, notify the architect and ask whether to proceed without that model's verdict +**If porch's CMAP consults fail (e.g., model unavailable):** +- `porch done` will report the failure. Inspect `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for the failure details. +- Re-run `porch done {{project_id}}` once — porch will retry the consult. +- If the model is persistently unavailable, notify the architect and ask whether to proceed without that model's verdict. They may direct you to skip via a manual override. **If the architect doesn't respond within a reasonable window:** - Send one follow-up via `afx send architect "..."` after a few hours From 6928942fbe011b8a320439dbe667995db4b621e4 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 14:43:34 +1000 Subject: [PATCH 30/63] feat(vscode): inline review comments on plan/spec files via Comments API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds gutter-based review commenting to markdown files in codev/plans/ and codev/specs/. Hover any line → "+" appears in the gutter → click → comment input opens inline → submit writes the comment to the file as on the next line. Same on-disk format as the existing Codev: Add Review Comment palette command (commands/review.ts) and the review.json snippet: a single REVIEW(@architect): marker that review-decorations.ts highlights. The three entry points are independent code paths that converge on the same persisted form — reviewers can use whichever fits the moment (palette, gutter "+", or tab-trigger snippet). Existing inline markers in a file render as collapsed comment threads on open: refreshDoc() scans the document for the canonical regex (), creates a CommentThread per match. Threads are read-only with a trash icon in the header — clicking removes the line from the file. The onDidChangeTextDocument listener keeps threads in sync with the underlying source as the user edits. Scope is limited to codev/plans/-.md and codev/specs/-.md via the ELIGIBLE_PATH_REGEX check in isEligibleDocument() — no UI clutter in other markdown files (READMEs, etc.) or source code. Author is hardcoded to "architect" to match the existing palette command exactly — zero divergence between entry points. The codev.submitReviewComment / codev.deleteReviewComment commands are hidden from the command palette (when: false) since they only make sense when invoked from a comment thread's UI. --- packages/vscode/package.json | 32 ++++ packages/vscode/src/comments/plan-review.ts | 182 ++++++++++++++++++++ packages/vscode/src/extension.ts | 7 + 3 files changed, 221 insertions(+) create mode 100644 packages/vscode/src/comments/plan-review.ts diff --git a/packages/vscode/package.json b/packages/vscode/package.json index b030d41ef..e4c0056d5 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -132,6 +132,16 @@ { "command": "codev.viewPlanFile", "title": "Codev: View Plan File" + }, + { + "command": "codev.submitReviewComment", + "title": "Submit review comment", + "enablement": "!commentIsEmpty" + }, + { + "command": "codev.deleteReviewComment", + "title": "Delete review comment", + "icon": "$(trash)" } ], "menus": { @@ -143,6 +153,14 @@ { "command": "codev.addReviewComment", "when": "editorLangId == 'markdown'" + }, + { + "command": "codev.submitReviewComment", + "when": "false" + }, + { + "command": "codev.deleteReviewComment", + "when": "false" } ], "view/item/context": [ @@ -213,6 +231,20 @@ "when": "view == codev.status", "group": "navigation" } + ], + "comments/commentThread/context": [ + { + "command": "codev.submitReviewComment", + "group": "inline", + "when": "commentController == codev-review && commentThreadIsEmpty" + } + ], + "comments/commentThread/title": [ + { + "command": "codev.deleteReviewComment", + "group": "inline@1", + "when": "commentController == codev-review && commentThread == inline-review" + } ] }, "snippets": [ diff --git a/packages/vscode/src/comments/plan-review.ts b/packages/vscode/src/comments/plan-review.ts new file mode 100644 index 000000000..3030237e8 --- /dev/null +++ b/packages/vscode/src/comments/plan-review.ts @@ -0,0 +1,182 @@ +/** + * Codev Plan Review — inline comments on plan/spec files via VSCode's + * Comments API. + * + * Hover any line in `codev/plans/*.md` or `codev/specs/*.md` → a "+" appears + * in the gutter → click → comment input field opens inline → submit → the + * comment is written into the file as `` + * on the next line. + * + * The HTML-comment serialization re-uses the existing REVIEW convention + * (codev/protocols/.../review.md, review-decorations.ts highlighting, + * Codev: Add Review Comment palette command). Builders read REVIEW + * markers when they re-read the plan and address them inline — no new + * storage layer, no separate protocol surface to teach. + * + * Existing inline REVIEW comments in a file render as collapsed comment + * threads when the file opens, so reviewers see prior notes as comment + * UI rather than raw HTML. + */ + +import * as vscode from 'vscode'; + +/** + * Matches the canonical inline form. Capture groups: + * [1] — author (the name inside @...) + * [2] — comment body text + * + * Tolerant of whitespace; mirrors the regex shape used elsewhere + * (review-decorations.ts, snippets/review.json). + */ +const REVIEW_COMMENT_PATTERN = //g; + +const ELIGIBLE_PATH_REGEX = /\/codev\/(plans|specs)\//; + +const CONTROLLER_ID = 'codev-review'; + +/** Tracks threads we created per document URI so we can refresh on edit. */ +const threadsByDoc = new Map(); + +export function activateReviewComments(context: vscode.ExtensionContext): void { + const controller = vscode.comments.createCommentController( + CONTROLLER_ID, + 'Codev Plan Review', + ); + context.subscriptions.push(controller); + + // Where the "+" appears. We accept any line in eligible files. + controller.commentingRangeProvider = { + provideCommentingRanges(document) { + if (!isEligibleDocument(document)) { return []; } + const lastLine = Math.max(0, document.lineCount - 1); + return [new vscode.Range(0, 0, lastLine, 0)]; + }, + }; + + function refreshDoc(document: vscode.TextDocument): void { + if (!isEligibleDocument(document)) { return; } + + // Tear down stale threads so we re-create from the current text. + const key = document.uri.toString(); + const existing = threadsByDoc.get(key) ?? []; + for (const t of existing) { t.dispose(); } + + const text = document.getText(); + const fresh: vscode.CommentThread[] = []; + let match: RegExpExecArray | null; + REVIEW_COMMENT_PATTERN.lastIndex = 0; + while ((match = REVIEW_COMMENT_PATTERN.exec(text)) !== null) { + const startPos = document.positionAt(match.index); + const thread = controller.createCommentThread( + document.uri, + new vscode.Range(startPos, startPos), + [ + { + body: new vscode.MarkdownString(match[2]), + mode: vscode.CommentMode.Preview, + author: { name: match[1] }, + // contextValue is matched in the comment-context menu's `when` + // clause so the delete button only shows on inline-sourced + // comments (not on new in-progress threads). + contextValue: 'inline-review', + }, + ], + ); + thread.collapsibleState = vscode.CommentThreadCollapsibleState.Expanded; + thread.canReply = false; + thread.contextValue = 'inline-review'; + fresh.push(thread); + } + threadsByDoc.set(key, fresh); + } + + // Render for already-open documents. + for (const doc of vscode.workspace.textDocuments) { refreshDoc(doc); } + + // Refresh on open/change/close. + context.subscriptions.push( + vscode.workspace.onDidOpenTextDocument(refreshDoc), + vscode.workspace.onDidChangeTextDocument(e => refreshDoc(e.document)), + vscode.workspace.onDidCloseTextDocument(doc => { + const key = doc.uri.toString(); + const threads = threadsByDoc.get(key); + if (threads) { + for (const t of threads) { t.dispose(); } + threadsByDoc.delete(key); + } + }), + ); + + // Command: submit a new review comment from an empty thread. + context.subscriptions.push( + vscode.commands.registerCommand( + 'codev.submitReviewComment', + async (reply: vscode.CommentReply) => { + await submitReviewComment(reply); + }, + ), + ); + + // Command: delete an inline review comment (removes the `` + // line from the file). + context.subscriptions.push( + vscode.commands.registerCommand( + 'codev.deleteReviewComment', + async (thread: vscode.CommentThread) => { + await deleteReviewCommentByThread(thread); + }, + ), + ); +} + +async function submitReviewComment(reply: vscode.CommentReply): Promise { + const thread = reply.thread; + if (!thread.range) { return; } + const document = await vscode.workspace.openTextDocument(thread.uri); + const line = thread.range.start.line; + const indent = document.lineAt(line).text.match(/^\s*/)?.[0] ?? ''; + // Normalize whitespace — review markers are single-line by convention, + // and review-decorations.ts assumes the marker fits one line. + const body = reply.text.replace(/\s+/g, ' ').trim(); + // Author hardcoded to "architect" to match the existing + // `codev.addReviewComment` palette command (commands/review.ts) and the + // review.json snippet — same on-disk format from every entry point. + const commentLine = `${indent}`; + + const edit = new vscode.WorkspaceEdit(); + edit.insert(thread.uri, new vscode.Position(line + 1, 0), commentLine + '\n'); + await vscode.workspace.applyEdit(edit); + await document.save(); + + // The change event fires refreshDoc, which disposes the in-progress + // thread (it's currently in threadsByDoc as a placeholder) and re-creates + // the canonical thread from the new inline marker. + thread.dispose(); +} + +async function deleteReviewCommentByThread(thread: vscode.CommentThread): Promise { + if (!thread.range) { return; } + const document = await vscode.workspace.openTextDocument(thread.uri); + const line = thread.range.start.line; + if (line >= document.lineCount) { return; } + + // Re-confirm the line is a REVIEW marker (it could have been edited + // out manually between the user clicking the menu and this handler). + const lineText = document.lineAt(line).text; + if (!/^\s*/.test(lineText)) { return; } + + const edit = new vscode.WorkspaceEdit(); + // Delete the whole line including its trailing newline. + const end = line + 1 < document.lineCount + ? new vscode.Position(line + 1, 0) + : new vscode.Position(line, lineText.length); + edit.delete(thread.uri, new vscode.Range(new vscode.Position(line, 0), end)); + await vscode.workspace.applyEdit(edit); + await document.save(); +} + +function isEligibleDocument(doc: vscode.TextDocument): boolean { + if (doc.languageId !== 'markdown') { return false; } + const fsPath = doc.uri.fsPath.replace(/\\/g, '/'); + return ELIGIBLE_PATH_REGEX.test(fsPath); +} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index a07c8447d..228f600f3 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -17,6 +17,7 @@ import { listCronTasks } from './commands/cron.js'; import { addReviewComment } from './commands/review.js'; import { activateGateToasts } from './notifications/gate-toast.js'; import { activateReviewDecorations } from './review-decorations.js'; +import { activateReviewComments } from './comments/plan-review.js'; import { BuilderSpawnHandler } from './builder-spawn-handler.js'; import { BuilderTerminalLinkProvider } from './terminal-link-provider.js'; import { BuildersProvider } from './views/builders.js'; @@ -232,6 +233,12 @@ export async function activate(context: vscode.ExtensionContext) { // Review comment decorations activateReviewDecorations(context); + // Inline plan-review comments via VSCode Comments API. Gutter "+" on + // any line in codev/plans/*.md or codev/specs/*.md; submit writes + // `` inline, matching the format + // produced by `codev.addReviewComment` and review.json snippet. + activateReviewComments(context); + // Toast on new gate-pending — surfaces blocked builders without forcing the // user to watch the Builders tree. Respects `codev.gateToasts.enabled`. activateGateToasts(context, overviewCache); From ffaef44301fd08c8091cc8c84ff0790ae98e4b95 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 14:45:19 +1000 Subject: [PATCH 31/63] feat(vscode): add Spawn Builder and New Shell to workspace sidebar Surfaces the two highest-traffic workspace-scoped commands as top-level sidebar entries instead of palette-only. Spawn Builder runs the existing afx-spawn quick-pick flow; New Shell asks Tower for a generic PTY rooted at the workspace and opens it as a VSCode terminal tab. --- packages/vscode/src/views/workspace.ts | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/vscode/src/views/workspace.ts b/packages/vscode/src/views/workspace.ts index a41f04812..285d90acc 100644 --- a/packages/vscode/src/views/workspace.ts +++ b/packages/vscode/src/views/workspace.ts @@ -4,9 +4,9 @@ import type { ConnectionManager } from '../connection-manager.js'; import { getTowerAddress } from '../workspace-detector.js'; /** - * Workspace-level entry points: architect terminal + Tower web dashboard. - * Sits at the top of the Codev sidebar so users can launch either with - * one click instead of hunting in the command palette. + * Workspace-level entry points: architect terminal, Tower web dashboard, + * spawn builder, and new shell. Sits at the top of the Codev sidebar so + * common workspace actions are one click away, not buried in the palette. */ export class WorkspaceProvider implements vscode.TreeDataProvider { private readonly changeEmitter = new vscode.EventEmitter(); @@ -47,6 +47,26 @@ export class WorkspaceProvider implements vscode.TreeDataProvider Date: Fri, 15 May 2026 14:45:38 +1000 Subject: [PATCH 32/63] fix(vscode): include PIR in spawn protocol picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR was promoted to a first-class protocol in CLAUDE.md and is registered in the afx CLI, but the VSCode Spawn Builder quick-pick still listed only spir/aspir/air/bugfix/tick — users had to drop to the terminal to spawn a PIR builder. Inserted between aspir and air to mirror the ceremony ordering documented in CLAUDE.md (lighter than SPIR, stronger than AIR). --- packages/vscode/src/commands/spawn.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vscode/src/commands/spawn.ts b/packages/vscode/src/commands/spawn.ts index 46115af97..05cc29b4c 100644 --- a/packages/vscode/src/commands/spawn.ts +++ b/packages/vscode/src/commands/spawn.ts @@ -12,7 +12,7 @@ export async function spawnBuilder(): Promise { if (!issueNumber) { return; } const protocol = await vscode.window.showQuickPick( - ['spir', 'aspir', 'air', 'bugfix', 'tick'], + ['spir', 'aspir', 'pir', 'air', 'bugfix', 'tick'], { placeHolder: 'Select protocol' }, ); if (!protocol) { return; } From 04006b77b0f989d12eb6a4c22e207f10d707cb25 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 14:55:24 +1000 Subject: [PATCH 33/63] docs(pir): tighten protocol description to match SPIR's style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous description claimed "no review-with-arch-updates ceremony" — inaccurate: protocol.json's review-phase checks enforce both Architecture Updates and Lessons Learned sections in the review file, exactly like SPIR. Rewritten to match SPIR's terse, factual style: SPIR: "Specify → Plan → Implement → Review with build-verify cycles" PIR: "Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, code-review), for GitHub-issue-driven work." Same structure, same brevity. Editorial comparisons to SPIR (which flagged "no specify phase, no review-with-arch-updates ceremony", etc.) are dropped — the phase list itself communicates lightness vs. SPIR, and the "no X" framing was load-bearing for some of the inaccuracies. Mirror in codev-skeleton/protocols/pir/protocol.json. --- codev-skeleton/protocols/pir/protocol.json | 2 +- codev/protocols/pir/protocol.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/codev-skeleton/protocols/pir/protocol.json b/codev-skeleton/protocols/pir/protocol.json index fbeddc54b..4f068b82e 100644 --- a/codev-skeleton/protocols/pir/protocol.json +++ b/codev-skeleton/protocols/pir/protocol.json @@ -3,7 +3,7 @@ "name": "pir", "alias": "plan-implement-review", "version": "1.0.0", - "description": "PIR: Plan → Implement → Review for GitHub-issue-driven work with two human gates (plan-approval, code-review). Lighter than SPIR (no specify phase, no review-with-arch-updates ceremony), with a code-review gate that fires before the PR is opened.", + "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, code-review), for GitHub-issue-driven work.", "input": { "type": "github-issue", "required": true, diff --git a/codev/protocols/pir/protocol.json b/codev/protocols/pir/protocol.json index fbeddc54b..4f068b82e 100644 --- a/codev/protocols/pir/protocol.json +++ b/codev/protocols/pir/protocol.json @@ -3,7 +3,7 @@ "name": "pir", "alias": "plan-implement-review", "version": "1.0.0", - "description": "PIR: Plan → Implement → Review for GitHub-issue-driven work with two human gates (plan-approval, code-review). Lighter than SPIR (no specify phase, no review-with-arch-updates ceremony), with a code-review gate that fires before the PR is opened.", + "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, code-review), for GitHub-issue-driven work.", "input": { "type": "github-issue", "required": true, From dedb09250889113d42ced4e3caf84945037591ef Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 15:02:52 +1000 Subject: [PATCH 34/63] fix(vscode): close builder terminal tabs when builder is cleaned up The AI terminal (and any companion dev-server tab) survived `afx cleanup`, leaving dead "Process exited" tabs once Tower killed the underlying PTYs with the worktree. The sidebar refreshed correctly; only the tabs lingered. Add TerminalManager.closeBuilderTerminal (canonical roleId in, both builder- and dev- tabs out) and a pruneClosedBuilderTerminals helper folded into the existing overviewCache.onDidChange handler. Covers cleanup triggered from VSCode, the afx cleanup CLI, or any other path that removes a builder. A present->absent diff against Tower's workspace state avoids closing freshly-spawned builders whose first state refresh hasn't landed yet; an inFlight guard drops overlapping state fetches so a stale response can't overwrite a fresher prevBuilderIds. No new Tower-side event needed -- piggybacks on the SSE-driven overview refresh that's already firing. --- packages/vscode/src/extension.ts | 42 ++++++++++++++++++++++++- packages/vscode/src/terminal-manager.ts | 18 +++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 228f600f3..fe06445b8 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -102,10 +102,50 @@ export async function activate(context: vscode.ExtensionContext) { : `$(server) Codev: ${builderCount} builders`; }; + // Close builder/dev terminal tabs when their builder disappears from Tower + // state. Covers cleanup triggered from the VSCode "Cleanup Builder" command, + // `afx cleanup` on the CLI, or any other removal path — otherwise Tower + // kills the PTY but the VSCode tab lingers as a dead "Process exited" entry. + // Uses a present→absent diff so freshly-spawned builders whose first state + // refresh hasn't landed aren't pre-emptively closed; the inFlight guard + // drops overlapping state fetches so a stale response can't overwrite a + // fresher prevBuilderIds. + let prevBuilderIds: Set | null = null; + let pruneInFlight = false; + const pruneClosedBuilderTerminals = async () => { + if (pruneInFlight) { return; } + if (connectionManager?.getState() !== 'connected') { return; } + const client = connectionManager.getClient(); + const workspacePath = connectionManager.getWorkspacePath(); + if (!client || !workspacePath) { return; } + pruneInFlight = true; + try { + const state = await client.getWorkspaceState(workspacePath); + if (!state?.builders) { return; } + const currIds = new Set(state.builders.map(b => b.id)); + if (prevBuilderIds !== null) { + for (const prev of prevBuilderIds) { + if (!currIds.has(prev)) { + terminalManager?.closeBuilderTerminal(prev); + } + } + } + prevBuilderIds = currIds; + } catch { + // Transient state-fetch failures must not drop prevBuilderIds — + // next successful tick will resync. + } finally { + pruneInFlight = false; + } + }; + // Sidebar TreeViews const overviewCache = new OverviewCache(connectionManager); context.subscriptions.push({ dispose: () => overviewCache.dispose() }); - overviewCache.onDidChange(updateStatusBarCounts); + overviewCache.onDidChange(() => { + updateStatusBarCounts(); + pruneClosedBuilderTerminals(); + }); context.subscriptions.push( vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager)), diff --git a/packages/vscode/src/terminal-manager.ts b/packages/vscode/src/terminal-manager.ts index cd8181185..a6d54ecb2 100644 --- a/packages/vscode/src/terminal-manager.ts +++ b/packages/vscode/src/terminal-manager.ts @@ -158,6 +158,24 @@ export class TerminalManager { this.terminals.delete(key); } + /** + * Dispose the VSCode terminal tabs for a builder — both the AI terminal + * and any companion dev-server terminal — after the builder has been + * cleaned up. Tower kills the PTYs as part of cleanup, so without this + * the user sees a stale "Process exited" tab until they close it + * manually. Accepts the canonical builder roleId (e.g. `builder-spir-109`), + * matching the value passed to `openBuilder`. + */ + closeBuilderTerminal(builderId: string): void { + for (const key of [`builder-${builderId}`, `dev-${builderId}`]) { + const existing = this.terminals.get(key); + if (!existing) { continue; } + existing.pty.close(); + existing.terminal.dispose(); + this.terminals.delete(key); + } + } + /** * Return { builderId, terminalId } for every dev terminal this VSCode * instance has open. Used by `codev.stopWorktreeDev` as the source of From 014f74593d1b163cff2ff6447e6952ae43efe242 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 16:37:33 +1000 Subject: [PATCH 35/63] =?UTF-8?q?refactor(vscode):=20reorder=20builder=20c?= =?UTF-8?q?ontext=20menu=20=E2=80=94=20primary=20actions=20first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous ordering interleaved file-access and worktree-management actions: Open Terminal | Open Folder | Open Worktree Window | Run Setup | Run Dev | Stop Dev, with Approve Gate and View Plan File inserted as ad-hoc entries that broke visual grouping. New three-group structure with VSCode dividers between groups: Group 1 — Primary actions on the builder: 1. Codev: Open Builder Terminal 2. Codev: Approve Gate (only on blocked-builder-*) 3. Codev: View Plan File (only on builder-pir / blocked-builder-pir) ─ divider ─ Group 2 — Worktree access: 4. Codev: Open Worktree in New Window 5. Codev: Open Worktree Folder ─ divider ─ Group 3 — Dev server: 6. Codev: Run Worktree Setup 7. Codev: Run Dev Server 8. Codev: Stop Dev Server Implementation: groups renamed to `1_primary`, `2_worktree`, `3_dev` so VSCode's gutter dividers render between them. The inline ✓ icon for Approve Gate stays on the separate `inline` group (unchanged). Per-protocol when-clauses preserved (Approve Gate only on blocked, View Plan File only on PIR). --- packages/vscode/package.json | 46 ++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index e4c0056d5..57ec4efef 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -164,50 +164,50 @@ } ], "view/item/context": [ + { + "command": "codev.approveGate", + "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", + "group": "inline@1" + }, { "command": "codev.openBuilderById", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "1_terminal@1" + "group": "1_primary@1" }, { - "command": "codev.openWorktreeFolder", - "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "2_files@1" + "command": "codev.approveGate", + "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", + "group": "1_primary@2" + }, + { + "command": "codev.viewPlanFile", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-pir$/", + "group": "1_primary@3" }, { "command": "codev.openWorktreeWindow", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "3_review@1" + "group": "2_worktree@1" + }, + { + "command": "codev.openWorktreeFolder", + "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", + "group": "2_worktree@2" }, { "command": "codev.runWorktreeSetup", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "4_worktree@1" + "group": "3_dev@1" }, { "command": "codev.runWorktreeDev", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "4_worktree@2" + "group": "3_dev@2" }, { "command": "codev.stopWorktreeDev", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", - "group": "4_worktree@3" - }, - { - "command": "codev.approveGate", - "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", - "group": "inline@1" - }, - { - "command": "codev.approveGate", - "when": "view == codev.builders && viewItem =~ /^blocked-builder-/", - "group": "0_gate@1" - }, - { - "command": "codev.viewPlanFile", - "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-pir$/", - "group": "3_review@2" + "group": "3_dev@3" } ], "view/title": [ From b198f861db39442fcd9b7089feaeaa81b7f4a143 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 17:22:49 +1000 Subject: [PATCH 36/63] =?UTF-8?q?feat(pir):=20add=20pr=20gate=20=E2=80=94?= =?UTF-8?q?=20builder=20no=20longer=20runs=20gh=20pr=20merge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes merge capability from the PIR builder. Replaces it with a post-CMAP `pr` gate that the human approves after merging on GitHub. Closes the self-merge bug class observed on pir-1326 (builder merged 3 seconds after PR-ready notification, bypassing the "wait for merge instruction" guard). PIR was the only protocol giving the builder `gh pr merge` capability. SPIR, ASPIR, AIR, BUGFIX all keep merge in the human's hands (GitHub or `gh pr merge` from their own shell); PIR was the outlier. With a loosely-worded "wait for merge instruction" guard, the builder generalized prior user-in-pane authorizations (e.g., "skip codex") to include merge approval — too easy to misread. Capability removal beats instruction adherence. The pr gate is the porch-level synchronization point; the builder waits there until the human merges externally and approves the gate. Changes: - protocol.json: review phase gains `"gate": "pr"`. `next: null` is unchanged — there's no post-merge verify phase. The `pr` gate name is already in Tower's overview blocked-allowlist (no overview changes needed). - review.md: step 7 reframed as "Notify the Architect (after porch approves CMAP)" — gate-pending notification. Step 8 reframed from "Wait for Merge Instruction" to "Wait at the pr Gate" — explicit that the builder doesn't merge. Step 9 (Merge the PR) deleted. Step 9a (Record the Merge) replaced with a post-wake-up record triggered by gate approval. "What NOT to Do" gains a hard rule against running `gh pr merge`. - builder-prompt.md: review phase brief description updated to reference the pr gate and the no-merge constraint. - protocol.md: Phases section gains the pr gate description; Review phase walkthrough updated; comparison table reflects the new gate set (plan-approval, code-review, pr). PR/merge lifecycle is now tracked end-to-end in porch state via the existing `porch done --pr` (step 4a) and `porch done --merged` (after gate wake-up) flags — closing the `history: []` gap also seen on pir-1326. SPIR cross-references in the canonical-position language were trimmed to dedicated comparison sections. All 1803 porch + agent-farm tests still pass. --- .../protocols/pir/builder-prompt.md | 4 +- .../protocols/pir/prompts/review.md | 57 +++++++++++++------ codev-skeleton/protocols/pir/protocol.json | 3 +- codev-skeleton/protocols/pir/protocol.md | 26 +++++---- codev/protocols/pir/builder-prompt.md | 4 +- codev/protocols/pir/prompts/review.md | 57 +++++++++++++------ codev/protocols/pir/protocol.json | 3 +- codev/protocols/pir/protocol.md | 26 +++++---- 8 files changed, 114 insertions(+), 66 deletions(-) diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 5cbcdef17..a2ddaad00 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) -3. **review** — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** {{#if issue}} ## Issue #{{issue.number}} @@ -66,7 +66,7 @@ Use `afx send architect "..."` at key moments: - **PR merged**: `afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup."` - **Blocked**: `afx send architect "Blocked on PIR #{{issue.number}}: [reason]"` -Gate-pending notifications are sent automatically by porch — you do not need to send them yourself. +**Gates are not architect-notified.** When porch transitions a gate to `pending`, the gate-reached message (including the `porch approve --a-human-explicitly-approved-this` invocation) appears in YOUR pane as part of your normal output. That's the universal notification surface — visible whether the user is in VSCode, tmux, plain Terminal, or any other host. The user reads it directly from your pane (or runs `porch pending` from a shell) and approves themselves; the architect can't approve gates, so notifying it would be informational noise. ## Handling Flaky Tests diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index cf301e12e..977518226 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, then signal completion to porch — **porch runs CMAP-2 (Gemini + Codex) automatically**. After porch confirms approval, notify the architect and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) automatically** via the verify block. After CMAP approves, the `pr` gate fires; you notify the architect and wait at the gate while the human merges on GitHub. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -121,6 +121,16 @@ gh pr edit --body-file codev/reviews/{{artifact_name}}.md **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. +### 4a. Record the PR with Porch + +Immediately after creating the PR, tell porch about it so `status.yaml` carries the PR number and branch. This is a metadata-only call — it does NOT advance the phase or trigger CMAP: + +```bash +porch done {{project_id}} --pr --branch "$(git branch --show-current)" +``` + +Without this, porch's `history:` for the project stays empty and downstream tooling (status views, analytics, audit trails) can't link the porch project to its GitHub PR. + ### 5. Signal Completion to Porch (porch runs CMAP-2) ```bash @@ -148,36 +158,47 @@ If `porch done` reports any reviewer requested changes, run `porch next {{projec Loop until porch reports all reviewers APPROVE. -### 7. Notify the Architect (after porch approves) +### 7. Notify the Architect (after porch approves CMAP) -After `porch done` reports success (all reviewers APPROVE + checks pass), read the verdicts from porch state and notify the architect: +After `porch done` reports all reviewers APPROVE + checks pass, porch fires the **`pr` gate** (pending). Read the verdicts from porch state and notify: ```bash GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Full verdicts in codev/projects/{{project_id}}-*/." +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." ``` -This is the only notification you send. +This is the only notification you send at the gate. -### 8. Wait for Merge Instruction +### 8. Wait at the `pr` Gate -The architect reviews the PR. They will either: +Your active work is done. **You do NOT run `gh pr merge`.** Capability is intentionally not in this protocol — the human owns the merge step on GitHub. -- Tell you to merge → run the merge command (step 9) -- Request more changes → push fixes and re-run `porch done {{project_id}}` (loops back to step 6) -- Tell you to close without merging → `gh pr close ` and stop +The human will: -### 9. Merge the PR +1. Review the PR on GitHub (or by running the worktree via `afx dev pir-{{project_id}}` again) +2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool +3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell + +Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and code-review gates). + +If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. + +### 9. After `pr` Gate Approval — Record the Merge + +When porch wakes you with "Gate pr approved", the human has merged the PR. Record the merge so porch's `status.yaml` reflects the completed lifecycle: ```bash -gh pr merge --merge -``` +# Read the PR number that was recorded at step 4a +PR=$(yq '.history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) +# Or just: PR= -**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. +porch done {{project_id}} --merged "$PR" +porch next {{project_id}} # confirms protocol is complete (next: null) +``` -The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. +Together with the `--pr` record from step 4a, this gives porch a complete view of the PR lifecycle (created → merged) for analytics, status displays, and audit trails. ### 10. Final Notification @@ -185,7 +206,7 @@ The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." ``` -Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). No additional `porch done` call is needed — the merge is a GitHub action, not a porch state transition. +Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). The merge is a GitHub action, not a porch phase transition — but the `--merged` record in step 9a keeps porch's history complete. ## Signals @@ -196,8 +217,8 @@ Porch already marked the review phase complete at step 5 (or whichever iteration ## What NOT to Do -- Don't squash-merge (`--squash`) — use `--merge` -- Don't merge without architect instruction +- **Don't run `gh pr merge` ever.** PIR intentionally does not give the builder merge capability — the human merges on GitHub (or via their own `gh pr merge`). If you find yourself reaching for the merge tool, you've misread the protocol. The `pr` gate is the porch-level synchronization point; you wait at it, you don't bypass it. +- Don't skip porch's PR/merge records (steps 4a, 9). The `--pr` record (step 4a) lets the gate-pending state link to the actual PR; the `--merged` record (step 9) closes the lifecycle in porch state. Skipping either leaves `history:` empty and downstream tooling blind. - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) diff --git a/codev-skeleton/protocols/pir/protocol.json b/codev-skeleton/protocols/pir/protocol.json index 4f068b82e..743973a02 100644 --- a/codev-skeleton/protocols/pir/protocol.json +++ b/codev-skeleton/protocols/pir/protocol.json @@ -70,7 +70,7 @@ { "id": "review", "name": "Review", - "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, merge on instruction", + "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, then wait at pr gate while the human merges on GitHub", "type": "build_verify", "build": { "prompt": "review.md", @@ -100,6 +100,7 @@ "description": "Review must include '## Lessons Learned Updates' section (what changed in lessons-learned.md, or why no changes needed)" } }, + "gate": "pr", "next": null } ], diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md index 78147be32..6eceb2ee6 100644 --- a/codev-skeleton/protocols/pir/protocol.md +++ b/codev-skeleton/protocols/pir/protocol.md @@ -36,10 +36,10 @@ PIR is structurally *SPIR minus the `specify` phase*, with the human code-review | Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | | Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | | Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | -| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review, pr | | Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | -The review file shape is intentionally identical to SPIR's, so `codev/reviews/` stays semantically consistent across protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. +The review file always includes Summary, Architecture Updates, and Lessons Learned sections so `codev/reviews/` stays semantically consistent across all protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. @@ -94,25 +94,27 @@ Reviewer tests the change on real devices / browsers / simulators. When satisfie porch approve code-review --a-human-explicitly-approved-this ``` -### Review (no human gate — proceeds autonomously through PR creation; merge gated by architect instruction) +### Review (gated by `pr`) The builder: -1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). Same shape as SPIR's review file. +1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). 2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). 3. Commits the review file (and arch / lessons updates if any) and pushes -4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #` -5. Porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) — same pattern as BUGFIX / AIR at PR creation. Outcome is appended to the PR body. -6. Notifies the architect: `afx send architect "PR # ready for review (PIR #)"` -7. Waits for the architect's merge instruction; merges with `gh pr merge --merge` (no squash — preserves history per project convention) +4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. +5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. +6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` +7. Builder waits at the `pr` gate. **Does not run `gh pr merge`** — capability is intentionally not in the protocol. The human merges via GitHub (or their own `gh pr merge`), then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). +8. After `pr` gate approval, porch wakes the builder; builder records the merge with `porch done --merged ` and sends the final cleanup-ready notification. Protocol is complete (`next: null`). ## Gates PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. -- **`plan-approval`** — same name as SPIR's plan gate. Reused safely (gates are keyed by `(project_id, gate_name)`). -- **`code-review`** — new gate name; works without any porch-side allowlist. +- **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. +- **`code-review`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. +- **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. -When a gate becomes pending, porch automatically fires `notifyArchitect()` which sends an `afx send architect "GATE: (Builder )..."` message. The VSCode "Needs Attention" tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. +When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). ## Rejection / Feedback Model @@ -195,4 +197,4 @@ codev/reviews/-.md # written in review phase (post-code-revi codev/projects/-/status.yaml # porch state, managed automatically ``` -The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. +The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file includes Summary + Architecture Updates + Lessons Learned + supporting sections, so `codev/reviews/` stays semantically consistent across protocols. diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 5cbcdef17..a2ddaad00 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) -3. **review** — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates + Lessons Learned, same shape as SPIR's review file), open PR with the review as body, run CMAP, notify architect, merge on instruction +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** {{#if issue}} ## Issue #{{issue.number}} @@ -66,7 +66,7 @@ Use `afx send architect "..."` at key moments: - **PR merged**: `afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup."` - **Blocked**: `afx send architect "Blocked on PIR #{{issue.number}}: [reason]"` -Gate-pending notifications are sent automatically by porch — you do not need to send them yourself. +**Gates are not architect-notified.** When porch transitions a gate to `pending`, the gate-reached message (including the `porch approve --a-human-explicitly-approved-this` invocation) appears in YOUR pane as part of your normal output. That's the universal notification surface — visible whether the user is in VSCode, tmux, plain Terminal, or any other host. The user reads it directly from your pane (or runs `porch pending` from a shell) and approves themselves; the architect can't approve gates, so notifying it would be informational noise. ## Handling Flaky Tests diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index cf301e12e..977518226 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` — same shape as SPIR's review file, including **Architecture Updates** and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, then signal completion to porch — **porch runs CMAP-2 (Gemini + Codex) automatically**. After porch confirms approval, notify the architect and merge on instruction. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) automatically** via the verify block. After CMAP approves, the `pr` gate fires; you notify the architect and wait at the gate while the human merges on GitHub. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -121,6 +121,16 @@ gh pr edit --body-file codev/reviews/{{artifact_name}}.md **Exception**: if this PR only partially addresses the issue, use `Refs #{{issue.number}}` instead — the issue stays open until a follow-up PR closes it. +### 4a. Record the PR with Porch + +Immediately after creating the PR, tell porch about it so `status.yaml` carries the PR number and branch. This is a metadata-only call — it does NOT advance the phase or trigger CMAP: + +```bash +porch done {{project_id}} --pr --branch "$(git branch --show-current)" +``` + +Without this, porch's `history:` for the project stays empty and downstream tooling (status views, analytics, audit trails) can't link the porch project to its GitHub PR. + ### 5. Signal Completion to Porch (porch runs CMAP-2) ```bash @@ -148,36 +158,47 @@ If `porch done` reports any reviewer requested changes, run `porch next {{projec Loop until porch reports all reviewers APPROVE. -### 7. Notify the Architect (after porch approves) +### 7. Notify the Architect (after porch approves CMAP) -After `porch done` reports success (all reviewers APPROVE + checks pass), read the verdicts from porch state and notify the architect: +After `porch done` reports all reviewers APPROVE + checks pass, porch fires the **`pr` gate** (pending). Read the verdicts from porch state and notify: ```bash GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Full verdicts in codev/projects/{{project_id}}-*/." +afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." ``` -This is the only notification you send. +This is the only notification you send at the gate. -### 8. Wait for Merge Instruction +### 8. Wait at the `pr` Gate -The architect reviews the PR. They will either: +Your active work is done. **You do NOT run `gh pr merge`.** Capability is intentionally not in this protocol — the human owns the merge step on GitHub. -- Tell you to merge → run the merge command (step 9) -- Request more changes → push fixes and re-run `porch done {{project_id}}` (loops back to step 6) -- Tell you to close without merging → `gh pr close ` and stop +The human will: -### 9. Merge the PR +1. Review the PR on GitHub (or by running the worktree via `afx dev pir-{{project_id}}` again) +2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool +3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell + +Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and code-review gates). + +If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. + +### 9. After `pr` Gate Approval — Record the Merge + +When porch wakes you with "Gate pr approved", the human has merged the PR. Record the merge so porch's `status.yaml` reflects the completed lifecycle: ```bash -gh pr merge --merge -``` +# Read the PR number that was recorded at step 4a +PR=$(yq '.history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) +# Or just: PR= -**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. +porch done {{project_id}} --merged "$PR" +porch next {{project_id}} # confirms protocol is complete (next: null) +``` -The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. +Together with the `--pr` record from step 4a, this gives porch a complete view of the PR lifecycle (created → merged) for analytics, status displays, and audit trails. ### 10. Final Notification @@ -185,7 +206,7 @@ The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." ``` -Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). No additional `porch done` call is needed — the merge is a GitHub action, not a porch state transition. +Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). The merge is a GitHub action, not a porch phase transition — but the `--merged` record in step 9a keeps porch's history complete. ## Signals @@ -196,8 +217,8 @@ Porch already marked the review phase complete at step 5 (or whichever iteration ## What NOT to Do -- Don't squash-merge (`--squash`) — use `--merge` -- Don't merge without architect instruction +- **Don't run `gh pr merge` ever.** PIR intentionally does not give the builder merge capability — the human merges on GitHub (or via their own `gh pr merge`). If you find yourself reaching for the merge tool, you've misread the protocol. The `pr` gate is the porch-level synchronization point; you wait at it, you don't bypass it. +- Don't skip porch's PR/merge records (steps 4a, 9). The `--pr` record (step 4a) lets the gate-pending state link to the actual PR; the `--merged` record (step 9) closes the lifecycle in porch state. Skipping either leaves `history:` empty and downstream tooling blind. - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) diff --git a/codev/protocols/pir/protocol.json b/codev/protocols/pir/protocol.json index 4f068b82e..743973a02 100644 --- a/codev/protocols/pir/protocol.json +++ b/codev/protocols/pir/protocol.json @@ -70,7 +70,7 @@ { "id": "review", "name": "Review", - "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, merge on instruction", + "description": "Write retrospective (arch + lessons), push, open PR with review as body, run CMAP, then wait at pr gate while the human merges on GitHub", "type": "build_verify", "build": { "prompt": "review.md", @@ -100,6 +100,7 @@ "description": "Review must include '## Lessons Learned Updates' section (what changed in lessons-learned.md, or why no changes needed)" } }, + "gate": "pr", "next": null } ], diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md index 78147be32..6eceb2ee6 100644 --- a/codev/protocols/pir/protocol.md +++ b/codev/protocols/pir/protocol.md @@ -36,10 +36,10 @@ PIR is structurally *SPIR minus the `specify` phase*, with the human code-review | Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | | Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | | Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | -| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review, pr | | Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | -The review file shape is intentionally identical to SPIR's, so `codev/reviews/` stays semantically consistent across protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. +The review file always includes Summary, Architecture Updates, and Lessons Learned sections so `codev/reviews/` stays semantically consistent across all protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. @@ -94,25 +94,27 @@ Reviewer tests the change on real devices / browsers / simulators. When satisfie porch approve code-review --a-human-explicitly-approved-this ``` -### Review (no human gate — proceeds autonomously through PR creation; merge gated by architect instruction) +### Review (gated by `pr`) The builder: -1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). Same shape as SPIR's review file. +1. Writes `codev/reviews/-.md` with **Summary**, **Architecture Updates**, **Lessons Learned Updates**, plus the supporting sections (Files Changed, Commits, Test Results, Things to Look At, How to Test Locally). 2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). 3. Commits the review file (and arch / lessons updates if any) and pushes -4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #` -5. Porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) — same pattern as BUGFIX / AIR at PR creation. Outcome is appended to the PR body. -6. Notifies the architect: `afx send architect "PR # ready for review (PIR #)"` -7. Waits for the architect's merge instruction; merges with `gh pr merge --merge` (no squash — preserves history per project convention) +4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. +5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. +6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` +7. Builder waits at the `pr` gate. **Does not run `gh pr merge`** — capability is intentionally not in the protocol. The human merges via GitHub (or their own `gh pr merge`), then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). +8. After `pr` gate approval, porch wakes the builder; builder records the merge with `porch done --merged ` and sends the final cleanup-ready notification. Protocol is complete (`next: null`). ## Gates PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. -- **`plan-approval`** — same name as SPIR's plan gate. Reused safely (gates are keyed by `(project_id, gate_name)`). -- **`code-review`** — new gate name; works without any porch-side allowlist. +- **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. +- **`code-review`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. +- **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. -When a gate becomes pending, porch automatically fires `notifyArchitect()` which sends an `afx send architect "GATE: (Builder )..."` message. The VSCode "Needs Attention" tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. +When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). ## Rejection / Feedback Model @@ -195,4 +197,4 @@ codev/reviews/-.md # written in review phase (post-code-revi codev/projects/-/status.yaml # porch state, managed automatically ``` -The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file is shaped identically to SPIR's review file (Summary + Architecture Updates + Lessons Learned + supporting sections), so `codev/reviews/` stays semantically consistent across protocols. +The plan and review files ship to `main` with the merged PR — durable, searchable, git-versioned. The review file includes Summary + Architecture Updates + Lessons Learned + supporting sections, so `codev/reviews/` stays semantically consistent across protocols. From 481c50388639ef18381c80777c0a119fe4c4d2ed Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 17:24:51 +1000 Subject: [PATCH 37/63] fix(vscode): use last-write-wins in OverviewCache to survive SSE bursts The cache gated concurrent refreshes with a `loading` flag, which dropped every refresh that arrived while one was in flight. Rapid SSE bursts (e.g. `porch done --pr` -> `porch done --merged` -> `afx cleanup`, all firing `overview-changed` within ~100ms) hit this consistently: refresh #1 captured a mid-transition state, refreshes #2 and #3 were dropped, and the sidebar froze on stale data until the user manually clicked refresh or some unrelated SSE event triggered another fetch. Replace the gate with a sequence counter. Each call increments latestSeq; each fetch only commits if its seq is still current. Stale in-flight responses are discarded. The cache always reflects the most- recently-requested state, so the final fetch in any burst wins. Cost: N rapid events -> N parallel /api/overview fetches. Tower runs on localhost and the endpoint is cheap (mostly SQLite + filesystem + 30s-TTL PR/issue cache), so the amplification is negligible. In return we get an invariant the cache previously didn't have: the data is always at-or-newer-than the latest refresh request. --- packages/vscode/src/views/overview-data.ts | 43 ++++++++++++++-------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/vscode/src/views/overview-data.ts b/packages/vscode/src/views/overview-data.ts index cb530ddea..dc100bccd 100644 --- a/packages/vscode/src/views/overview-data.ts +++ b/packages/vscode/src/views/overview-data.ts @@ -8,7 +8,7 @@ import type { ConnectionManager } from '../connection-manager.js'; */ export class OverviewCache { private data: OverviewData | null = null; - private loading = false; + private latestSeq = 0; private readonly changeEmitter = new vscode.EventEmitter(); readonly onDidChange = this.changeEmitter.event; @@ -24,24 +24,35 @@ export class OverviewCache { return this.data; } + /** + * Fetch the latest overview from Tower and notify subscribers. + * + * Last-write-wins via a sequence counter rather than a load gate: every + * call increments `latestSeq` and only commits its result if the seq is + * still current. This guarantees the cache reflects the most-recently + * requested state even when SSE bursts (e.g. `porch done --pr` → + * `porch done --merged` → `afx cleanup`) trigger several refreshes back- + * to-back. A naive `if (loading) return` gate drops requests #2 and #3 + * and freezes the cache on the mid-transition state from request #1 + * until something else triggers an SSE event — the bug this fixes. + * Cost: N rapid events → N parallel `/api/overview` requests; on + * localhost-Tower that's negligible. + */ async refresh(): Promise { - if (this.loading) { return; } - this.loading = true; - - try { - const client = this.connectionManager.getClient(); - if (!client || this.connectionManager.getState() !== 'connected') { - this.data = null; - this.changeEmitter.fire(); - return; - } - - const workspacePath = this.connectionManager.getWorkspacePath(); - this.data = await client.getOverview(workspacePath ?? undefined) ?? null; - } finally { - this.loading = false; + const mySeq = ++this.latestSeq; + const client = this.connectionManager.getClient(); + if (!client || this.connectionManager.getState() !== 'connected') { + if (mySeq !== this.latestSeq) { return; } + this.data = null; this.changeEmitter.fire(); + return; } + + const workspacePath = this.connectionManager.getWorkspacePath(); + const result = await client.getOverview(workspacePath ?? undefined) ?? null; + if (mySeq !== this.latestSeq) { return; } + this.data = result; + this.changeEmitter.fire(); } dispose(): void { From aa99b9b54fc48348279151d8bd96915a9671c05b Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 20:27:40 +1000 Subject: [PATCH 38/63] refactor(pir): rename code-review gate to dev-approval for naming consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `code-review` gate name overloaded GitHub's PR-time "code review" convention and obscured what makes the gate distinctive — the human runs the worktree via `afx dev` and reviews the *running* implementation before any PR exists. It caused the architect to literally go looking for a PR when the gate fired (the incident upstream of this change). It also broke gate-naming consistency within PIR and across protocols. Other gates use `-approval`: | Before | After | |---------------------------|---------------------------| | plan-approval | plan-approval (unchanged) | | code-review | **dev-approval** | | pr | pr (unchanged) | The new name pairs with the `afx dev` tool used at this gate ("approve the dev environment after running it"), matches SPIR/ASPIR's `-approval` convention, and removes the GitHub-PR-review pattern match that misled the architect. Display label in Tower's overview moves from "code review" to "dev review" — preserves the consistent " review" label pattern visible to humans across all gates. Renamed across: - codev/protocols/pir/protocol.json (gate field + description) - codev/protocols/pir/{builder-prompt,protocol}.md - codev/protocols/pir/prompts/{plan,implement,review}.md - codev/protocols/pir/consult-types/{impl-review,pr-review}.md - codev/{CLAUDE,AGENTS}.md PIR-related lines - codev-skeleton/ mirrors of the above - packages/codev/src/agent-farm/servers/overview.ts (GATE_LABELS + gateNames) - packages/codev/src/commands/porch/__tests__/{notify,protocol,status-json}.test.ts Shannon's PIR templates mirror the canonical versions (already in sync). In-flight PIR builders spawned with the old gate name still have `code-review` baked into their porch state; new spawns use the new name. No automatic migration — accepted because PIR has only just landed and no in-flight projects exist on main. All 1803 porch + agent-farm tests pass. --- AGENTS.md | 4 +-- CLAUDE.md | 4 +-- .../protocols/pir/builder-prompt.md | 6 ++-- .../pir/consult-types/impl-review.md | 4 +-- .../protocols/pir/consult-types/pr-review.md | 8 ++--- .../protocols/pir/prompts/implement.md | 12 ++++---- codev-skeleton/protocols/pir/prompts/plan.md | 2 +- .../protocols/pir/prompts/review.md | 8 ++--- codev-skeleton/protocols/pir/protocol.json | 8 ++--- codev-skeleton/protocols/pir/protocol.md | 30 +++++++++---------- codev-skeleton/templates/AGENTS.md | 2 +- codev-skeleton/templates/CLAUDE.md | 2 +- codev/protocols/pir/builder-prompt.md | 6 ++-- .../pir/consult-types/impl-review.md | 4 +-- .../protocols/pir/consult-types/pr-review.md | 8 ++--- codev/protocols/pir/prompts/implement.md | 12 ++++---- codev/protocols/pir/prompts/plan.md | 2 +- codev/protocols/pir/prompts/review.md | 8 ++--- codev/protocols/pir/protocol.json | 8 ++--- codev/protocols/pir/protocol.md | 30 +++++++++---------- .../codev/src/agent-farm/servers/overview.ts | 4 +-- .../commands/porch/__tests__/notify.test.ts | 4 +-- .../commands/porch/__tests__/protocol.test.ts | 12 ++++---- .../porch/__tests__/status-json.test.ts | 8 ++--- 24 files changed, 98 insertions(+), 98 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 78da848dc..c27364650 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `code-review`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `code-review` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/CLAUDE.md b/CLAUDE.md index 78da848dc..c27364650 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `code-review`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `code-review` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index a2ddaad00..40ff40547 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -32,7 +32,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review -2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) +2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) 3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** {{#if issue}} @@ -53,7 +53,7 @@ PIR has two human gates. When you reach one: 4. **Stay in the interactive session**. Do NOT exit. Wait for the user's next message. The reviewer can give feedback by: -- Editing the plan file (at the plan-approval gate) or the code itself (at the code-review gate) in the worktree directly — you'll see changes via `git diff` +- Editing the plan file (at the plan-approval gate) or the code itself (at the dev-approval gate) in the worktree directly — you'll see changes via `git diff` - Typing into your PTY pane (this reaches you live) - `afx send ""` (queued; check on next turn) - Commenting on the GitHub issue (re-fetch with `gh issue view --comments` if asked) @@ -82,7 +82,7 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch you with the same prompt. On startup: 1. Run `porch next {{project_id}}` to learn what phase you're in -2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (code-review) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. +2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (dev-approval) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. 3. Otherwise: pick up where you left off ## Getting Started diff --git a/codev-skeleton/protocols/pir/consult-types/impl-review.md b/codev-skeleton/protocols/pir/consult-types/impl-review.md index e05776d87..380bceee9 100644 --- a/codev-skeleton/protocols/pir/consult-types/impl-review.md +++ b/codev-skeleton/protocols/pir/consult-types/impl-review.md @@ -2,7 +2,7 @@ ## Context -You are reviewing the implementation of a PIR protocol project before it reaches the `code-review` human gate. A builder has implemented the approved plan and written a code-review summary. Your job is to verify the implementation matches the plan and is ready for human review. +You are reviewing the implementation of a PIR protocol project before it reaches the `dev-approval` human gate. A builder has implemented the approved plan and written a dev-approval summary. Your job is to verify the implementation matches the plan and is ready for human review. ## CRITICAL: Verify Before Flagging @@ -57,7 +57,7 @@ KEY_ISSUES: ``` **Verdict meanings:** -- `APPROVE`: Ready for human at the `code-review` gate +- `APPROVE`: Ready for human at the `dev-approval` gate - `REQUEST_CHANGES`: Issues that must be fixed before reaching the human - `COMMENT`: Minor suggestions, can proceed but note feedback diff --git a/codev-skeleton/protocols/pir/consult-types/pr-review.md b/codev-skeleton/protocols/pir/consult-types/pr-review.md index 790b5c3db..9d723b02a 100644 --- a/codev-skeleton/protocols/pir/consult-types/pr-review.md +++ b/codev-skeleton/protocols/pir/consult-types/pr-review.md @@ -2,7 +2,7 @@ ## Context -You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `code-review` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. +You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. ## Focus Areas @@ -55,11 +55,11 @@ KEY_ISSUES: - **DO** flag missing `Fixes #` lines - **DO** flag obvious problems the human reviewer at the gate might have missed -- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `code-review` -- **DO NOT** demand changes the human reviewer already accepted at the `code-review` gate (the human ran the code and approved it; you didn't) +- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `dev-approval` +- **DO NOT** demand changes the human reviewer already accepted at the `dev-approval` gate (the human ran the code and approved it; you didn't) ## Notes -- The human at the `code-review` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases +- The human at the `dev-approval` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases - Focus on "what would an integration reviewer catch that the gate reviewer missed" - If referencing line numbers, use `file:line` format diff --git a/codev-skeleton/protocols/pir/prompts/implement.md b/codev-skeleton/protocols/pir/prompts/implement.md index 9eccc1fc4..383f391d4 100644 --- a/codev-skeleton/protocols/pir/prompts/implement.md +++ b/codev-skeleton/protocols/pir/prompts/implement.md @@ -4,7 +4,7 @@ You are executing the **IMPLEMENT** phase of the PIR protocol. ## Your Goal -Implement the approved plan, write tests, and pause at the `code-review` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. +Implement the approved plan, write tests, and pause at the `dev-approval` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. ## Context @@ -15,7 +15,7 @@ Implement the approved plan, write tests, and pause at the `code-review` gate so ## Resumption Check (do this FIRST) -Run `porch next {{project_id}}`. If the response is `gate_pending` on `code-review`, the code is already written and you're awaiting review. In that case: +Run `porch next {{project_id}}`. If the response is `gate_pending` on `dev-approval`, the code is already written and you're awaiting review. In that case: 1. Check for feedback: - `git diff main` — has the reviewer made any direct edits to your code? @@ -97,7 +97,7 @@ porch done {{project_id}} porch next {{project_id}} ``` -PIR's `implement` phase has no AI consult — the `code-review` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) +PIR's `implement` phase has no AI consult — the `dev-approval` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) ### 7. End Your Turn With a Code-Review Summary (Prose, Not a File) @@ -113,16 +113,16 @@ When the gate goes pending, output a short prose summary in the pane to orient t > > **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev pir-{{project_id}}`. View diff via VSCode → **View Diff** (auto-detects the repo's default branch). > -> Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). +> Ready for review — type feedback here, or approve with `porch approve {{project_id}} dev-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). Then **stay in the interactive session**. Do not exit. Wait for the user's next message. -(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient code-review note.) +(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient dev-approval note.) ## Signals ``` -PHASE_COMPLETE # Implementation + tests done; code-review gate becomes pending +PHASE_COMPLETE # Implementation + tests done; dev-approval gate becomes pending BLOCKED:reason # Cannot proceed ``` diff --git a/codev-skeleton/protocols/pir/prompts/plan.md b/codev-skeleton/protocols/pir/prompts/plan.md index 06468cd0d..a40cdee2d 100644 --- a/codev-skeleton/protocols/pir/prompts/plan.md +++ b/codev-skeleton/protocols/pir/prompts/plan.md @@ -72,7 +72,7 @@ Concrete file paths. Use `file:line` format for specific edits where possible. ## Test Plan -How to verify this works once implemented. The reviewer will use this at the `code-review` gate to test the running worktree. +How to verify this works once implemented. The reviewer will use this at the `dev-approval` gate to test the running worktree. - Unit test: - Manual: diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index 977518226..3e9934ca2 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -17,7 +17,7 @@ The retrospective ships with the merged PR — it's durable team knowledge, sear ## Prerequisites -- The `code-review` gate has been approved (you're here because `porch next` advanced you) +- The `dev-approval` gate has been approved (you're here because `porch next` advanced you) - Your branch contains the implementation commits - Build and tests pass @@ -54,7 +54,7 @@ Output of `git log main..HEAD --oneline`: - `npm run build`: ✓ pass - `npm test`: ✓ pass (X tests, Y new) -- Manual verification: +- Manual verification: ## Architecture Updates @@ -144,7 +144,7 @@ Porch will: - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. -> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. +> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `dev-approval` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. ### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) @@ -181,7 +181,7 @@ The human will: 2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool 3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell -Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and code-review gates). +Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and dev-approval gates). If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. diff --git a/codev-skeleton/protocols/pir/protocol.json b/codev-skeleton/protocols/pir/protocol.json index 743973a02..e2812d036 100644 --- a/codev-skeleton/protocols/pir/protocol.json +++ b/codev-skeleton/protocols/pir/protocol.json @@ -3,7 +3,7 @@ "name": "pir", "alias": "plan-implement-review", "version": "1.0.0", - "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, code-review), for GitHub-issue-driven work.", + "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, dev-approval), for GitHub-issue-driven work.", "input": { "type": "github-issue", "required": true, @@ -12,7 +12,7 @@ "hooks": { "pre-spawn": { "collision-check": true, - "comment-on-issue": "On it! Working on this with the PIR protocol (plan + code-review gates before PR)." + "comment-on-issue": "On it! Working on this with the PIR protocol (plan + dev-approval gates before PR)." } }, "phases": [ @@ -40,7 +40,7 @@ { "id": "implement", "name": "Implement", - "description": "Write code + tests, run build/test checks, await code-review on the running worktree", + "description": "Write code + tests, run build/test checks, await dev-approval on the running worktree", "type": "build_verify", "build": { "prompt": "implement.md", @@ -64,7 +64,7 @@ "max_retries": 2 } }, - "gate": "code-review", + "gate": "dev-approval", "next": "review" }, { diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md index 6eceb2ee6..0a3b7f1e7 100644 --- a/codev-skeleton/protocols/pir/protocol.md +++ b/codev-skeleton/protocols/pir/protocol.md @@ -1,6 +1,6 @@ # PIR Protocol -> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human code-review moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). +> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human dev-approval moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). ## When to Use PIR @@ -28,7 +28,7 @@ The PR diff alone is insufficient; the reviewer must *run* the code: ## How PIR Differs from SPIR -PIR is structurally *SPIR minus the `specify` phase*, with the human code-review moved earlier (pre-PR instead of post-PR). +PIR is structurally *SPIR minus the `specify` phase*, with the human dev-approval moved earlier (pre-PR instead of post-PR). | Aspect | SPIR | PIR | |---|---|---| @@ -36,12 +36,12 @@ PIR is structurally *SPIR minus the `specify` phase*, with the human code-review | Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | | Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | | Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | -| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review, pr | -| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, dev-approval, pr | +| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `dev-approval` gate) — read the diff **and run the worktree locally** | The review file always includes Summary, Architecture Updates, and Lessons Learned sections so `codev/reviews/` stays semantically consistent across all protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. -The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. +The `dev-approval` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. ## Phases @@ -70,15 +70,15 @@ When satisfied, approve via VSCode's "Approve Gate" command (Cmd+K G) or: porch approve plan-approval --a-human-explicitly-approved-this ``` -### Implement (gated by `code-review`) +### Implement (gated by `dev-approval`) The builder: 1. Reads the approved plan file 2. Writes code and tests; runs build + tests via the `checks` block -3. *No AI consult on this phase* — the human at the `code-review` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". +3. *No AI consult on this phase* — the human at the `dev-approval` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". 4. Pushes the branch -5. Runs `porch done` and `porch next` — the `code-review` gate becomes pending -6. Outputs a **prose** code-review summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. +5. Runs `porch done` and `porch next` — the `dev-approval` gate becomes pending +6. Outputs a **prose** dev-approval summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. 7. Sits at the interactive prompt **The reviewer's killer move**: run the worktree locally. @@ -91,7 +91,7 @@ The dev server uses **the same ports and URLs as main** intentionally (OAuth cal Reviewer tests the change on real devices / browsers / simulators. When satisfied, approves via Cmd+K G or: ```bash -porch approve code-review --a-human-explicitly-approved-this +porch approve dev-approval --a-human-explicitly-approved-this ``` ### Review (gated by `pr`) @@ -111,7 +111,7 @@ The builder: PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. - **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. -- **`code-review`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. +- **`dev-approval`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. - **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). @@ -146,15 +146,15 @@ PIR uses the same `.codev/config.json` configuration as other protocols. The `wo } ``` -Without `worktree.devCommand`, `afx dev` won't work and the `code-review` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. +Without `worktree.devCommand`, `afx dev` won't work and the `dev-approval` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. ## Multi-Agent Consultation - **plan**: human-only review. No AI consultation. -- **implement**: no AI consult — the human at the `code-review` gate is the sole reviewer of the running code. +- **implement**: no AI consult — the human at the `dev-approval` gate is the sole reviewer of the running code. - **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. -Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `code-review`), not AI-consult density. +Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `dev-approval`), not AI-consult density. To disable consultation entirely, say "without multi-agent consultation" when starting work. @@ -193,7 +193,7 @@ Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. ``` codev/plans/-.md # written in plan phase, on builder branch -codev/reviews/-.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/reviews/-.md # written in review phase (post-dev-approval-approval), on builder branch; becomes PR body codev/projects/-/status.yaml # porch state, managed automatically ``` diff --git a/codev-skeleton/templates/AGENTS.md b/codev-skeleton/templates/AGENTS.md index abd781a6e..6c4a4b8de 100644 --- a/codev-skeleton/templates/AGENTS.md +++ b/codev-skeleton/templates/AGENTS.md @@ -12,7 +12,7 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review) (`codev/protocols/pir/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) diff --git a/codev-skeleton/templates/CLAUDE.md b/codev-skeleton/templates/CLAUDE.md index 2a39cf736..35229ccaf 100644 --- a/codev-skeleton/templates/CLAUDE.md +++ b/codev-skeleton/templates/CLAUDE.md @@ -10,7 +10,7 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, code-review) (`codev/protocols/pir/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index a2ddaad00..40ff40547 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -32,7 +32,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review -2. **implement** (gated by `code-review`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — code-review summary is prose-in-pane) +2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) 3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** {{#if issue}} @@ -53,7 +53,7 @@ PIR has two human gates. When you reach one: 4. **Stay in the interactive session**. Do NOT exit. Wait for the user's next message. The reviewer can give feedback by: -- Editing the plan file (at the plan-approval gate) or the code itself (at the code-review gate) in the worktree directly — you'll see changes via `git diff` +- Editing the plan file (at the plan-approval gate) or the code itself (at the dev-approval gate) in the worktree directly — you'll see changes via `git diff` - Typing into your PTY pane (this reaches you live) - `afx send ""` (queued; check on next turn) - Commenting on the GitHub issue (re-fetch with `gh issue view --comments` if asked) @@ -82,7 +82,7 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch you with the same prompt. On startup: 1. Run `porch next {{project_id}}` to learn what phase you're in -2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (code-review) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. +2. If `gate_pending`: read the latest plan file (plan-approval) or `git diff main` (dev-approval) plus any new GitHub issue comments; check `afx send` queue. Decide whether to revise or just announce you're back. 3. Otherwise: pick up where you left off ## Getting Started diff --git a/codev/protocols/pir/consult-types/impl-review.md b/codev/protocols/pir/consult-types/impl-review.md index e05776d87..380bceee9 100644 --- a/codev/protocols/pir/consult-types/impl-review.md +++ b/codev/protocols/pir/consult-types/impl-review.md @@ -2,7 +2,7 @@ ## Context -You are reviewing the implementation of a PIR protocol project before it reaches the `code-review` human gate. A builder has implemented the approved plan and written a code-review summary. Your job is to verify the implementation matches the plan and is ready for human review. +You are reviewing the implementation of a PIR protocol project before it reaches the `dev-approval` human gate. A builder has implemented the approved plan and written a dev-approval summary. Your job is to verify the implementation matches the plan and is ready for human review. ## CRITICAL: Verify Before Flagging @@ -57,7 +57,7 @@ KEY_ISSUES: ``` **Verdict meanings:** -- `APPROVE`: Ready for human at the `code-review` gate +- `APPROVE`: Ready for human at the `dev-approval` gate - `REQUEST_CHANGES`: Issues that must be fixed before reaching the human - `COMMENT`: Minor suggestions, can proceed but note feedback diff --git a/codev/protocols/pir/consult-types/pr-review.md b/codev/protocols/pir/consult-types/pr-review.md index 790b5c3db..9d723b02a 100644 --- a/codev/protocols/pir/consult-types/pr-review.md +++ b/codev/protocols/pir/consult-types/pr-review.md @@ -2,7 +2,7 @@ ## Context -You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `code-review` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. +You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. ## Focus Areas @@ -55,11 +55,11 @@ KEY_ISSUES: - **DO** flag missing `Fixes #` lines - **DO** flag obvious problems the human reviewer at the gate might have missed -- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `code-review` -- **DO NOT** demand changes the human reviewer already accepted at the `code-review` gate (the human ran the code and approved it; you didn't) +- **DO NOT** redesign the approach — that was settled at `plan-approval` and validated at `dev-approval` +- **DO NOT** demand changes the human reviewer already accepted at the `dev-approval` gate (the human ran the code and approved it; you didn't) ## Notes -- The human at the `code-review` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases +- The human at the `dev-approval` gate is the primary reviewer for behavior; you are the secondary reviewer for hygiene and edge cases - Focus on "what would an integration reviewer catch that the gate reviewer missed" - If referencing line numbers, use `file:line` format diff --git a/codev/protocols/pir/prompts/implement.md b/codev/protocols/pir/prompts/implement.md index 9eccc1fc4..383f391d4 100644 --- a/codev/protocols/pir/prompts/implement.md +++ b/codev/protocols/pir/prompts/implement.md @@ -4,7 +4,7 @@ You are executing the **IMPLEMENT** phase of the PIR protocol. ## Your Goal -Implement the approved plan, write tests, and pause at the `code-review` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. +Implement the approved plan, write tests, and pause at the `dev-approval` gate so the human can verify behavior by running the worktree locally. No file artifact is produced in this phase — the retrospective (review file) is written in the next phase, after the human has approved the running code. ## Context @@ -15,7 +15,7 @@ Implement the approved plan, write tests, and pause at the `code-review` gate so ## Resumption Check (do this FIRST) -Run `porch next {{project_id}}`. If the response is `gate_pending` on `code-review`, the code is already written and you're awaiting review. In that case: +Run `porch next {{project_id}}`. If the response is `gate_pending` on `dev-approval`, the code is already written and you're awaiting review. In that case: 1. Check for feedback: - `git diff main` — has the reviewer made any direct edits to your code? @@ -97,7 +97,7 @@ porch done {{project_id}} porch next {{project_id}} ``` -PIR's `implement` phase has no AI consult — the `code-review` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) +PIR's `implement` phase has no AI consult — the `dev-approval` gate becomes pending immediately, and the human is the sole reviewer of the running code. (CMAP-2 runs later, in the `review` phase, after the human approves the gate and the PR is opened.) ### 7. End Your Turn With a Code-Review Summary (Prose, Not a File) @@ -113,16 +113,16 @@ When the gate goes pending, output a short prose summary in the pane to orient t > > **How to test locally**: VSCode → right-click builder → **Run Dev Server**, or `afx dev pir-{{project_id}}`. View diff via VSCode → **View Diff** (auto-detects the repo's default branch). > -> Ready for review — type feedback here, or approve with `porch approve {{project_id}} code-review --a-human-explicitly-approved-this` (Cmd+K G in VSCode). +> Ready for review — type feedback here, or approve with `porch approve {{project_id}} dev-approval --a-human-explicitly-approved-this` (Cmd+K G in VSCode). Then **stay in the interactive session**. Do not exit. Wait for the user's next message. -(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient code-review note.) +(Optional: if your team prefers an issue-thread record, you can also post a one-line comment on the GitHub issue pointing reviewers at the worktree branch. The summary itself stays in the pane — don't duplicate it as a committed file. That's the next phase's job, and that file will be a proper retrospective with arch + lessons updates, not a transient dev-approval note.) ## Signals ``` -PHASE_COMPLETE # Implementation + tests done; code-review gate becomes pending +PHASE_COMPLETE # Implementation + tests done; dev-approval gate becomes pending BLOCKED:reason # Cannot proceed ``` diff --git a/codev/protocols/pir/prompts/plan.md b/codev/protocols/pir/prompts/plan.md index 06468cd0d..a40cdee2d 100644 --- a/codev/protocols/pir/prompts/plan.md +++ b/codev/protocols/pir/prompts/plan.md @@ -72,7 +72,7 @@ Concrete file paths. Use `file:line` format for specific edits where possible. ## Test Plan -How to verify this works once implemented. The reviewer will use this at the `code-review` gate to test the running worktree. +How to verify this works once implemented. The reviewer will use this at the `dev-approval` gate to test the running worktree. - Unit test: - Manual: diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index 977518226..3e9934ca2 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -17,7 +17,7 @@ The retrospective ships with the merged PR — it's durable team knowledge, sear ## Prerequisites -- The `code-review` gate has been approved (you're here because `porch next` advanced you) +- The `dev-approval` gate has been approved (you're here because `porch next` advanced you) - Your branch contains the implementation commits - Build and tests pass @@ -54,7 +54,7 @@ Output of `git log main..HEAD --oneline`: - `npm run build`: ✓ pass - `npm test`: ✓ pass (X tests, Y new) -- Manual verification: +- Manual verification: ## Architecture Updates @@ -144,7 +144,7 @@ Porch will: - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. -> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `code-review` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. +> **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `dev-approval` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. ### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) @@ -181,7 +181,7 @@ The human will: 2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool 3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell -Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and code-review gates). +Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and dev-approval gates). If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. diff --git a/codev/protocols/pir/protocol.json b/codev/protocols/pir/protocol.json index 743973a02..e2812d036 100644 --- a/codev/protocols/pir/protocol.json +++ b/codev/protocols/pir/protocol.json @@ -3,7 +3,7 @@ "name": "pir", "alias": "plan-implement-review", "version": "1.0.0", - "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, code-review), for GitHub-issue-driven work.", + "description": "PIR: Plan → Implement → Review with build-verify cycles and two pre-PR human gates (plan-approval, dev-approval), for GitHub-issue-driven work.", "input": { "type": "github-issue", "required": true, @@ -12,7 +12,7 @@ "hooks": { "pre-spawn": { "collision-check": true, - "comment-on-issue": "On it! Working on this with the PIR protocol (plan + code-review gates before PR)." + "comment-on-issue": "On it! Working on this with the PIR protocol (plan + dev-approval gates before PR)." } }, "phases": [ @@ -40,7 +40,7 @@ { "id": "implement", "name": "Implement", - "description": "Write code + tests, run build/test checks, await code-review on the running worktree", + "description": "Write code + tests, run build/test checks, await dev-approval on the running worktree", "type": "build_verify", "build": { "prompt": "implement.md", @@ -64,7 +64,7 @@ "max_retries": 2 } }, - "gate": "code-review", + "gate": "dev-approval", "next": "review" }, { diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md index 6eceb2ee6..0a3b7f1e7 100644 --- a/codev/protocols/pir/protocol.md +++ b/codev/protocols/pir/protocol.md @@ -1,6 +1,6 @@ # PIR Protocol -> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human code-review moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). +> **Plan → Implement → Review** for GitHub-issue-driven work that needs human review of *either* the approach (before code is written) *or* the implementation (before a PR exists), or both. Lighter than SPIR/ASPIR (no `specify` phase — the GitHub issue is the implicit spec) with the human dev-approval moved earlier (pre-PR instead of post-PR). Stronger than BUGFIX/AIR (two human gates before the PR). ## When to Use PIR @@ -28,7 +28,7 @@ The PR diff alone is insufficient; the reviewer must *run* the code: ## How PIR Differs from SPIR -PIR is structurally *SPIR minus the `specify` phase*, with the human code-review moved earlier (pre-PR instead of post-PR). +PIR is structurally *SPIR minus the `specify` phase*, with the human dev-approval moved earlier (pre-PR instead of post-PR). | Aspect | SPIR | PIR | |---|---|---| @@ -36,12 +36,12 @@ PIR is structurally *SPIR minus the `specify` phase*, with the human code-review | Spec artifact | `codev/specs/-.md` | GitHub Issue body (implicit spec) | | Plan artifact | `codev/plans/-.md` | Same — committed on builder branch | | Review artifact | `codev/reviews/-.md` (Summary + Architecture Updates + Lessons Learned, becomes PR body) | **Same shape** — `codev/reviews/-.md` with the same sections, also becomes PR body | -| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, code-review, pr | -| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `code-review` gate) — read the diff **and run the worktree locally** | +| Human gates | spec-approval, plan-approval, pr, verify-approval | plan-approval, dev-approval, pr | +| Where code is reviewed by the human | On the PR (post-creation) — read the diff | Pre-PR (at the `dev-approval` gate) — read the diff **and run the worktree locally** | The review file always includes Summary, Architecture Updates, and Lessons Learned sections so `codev/reviews/` stays semantically consistent across all protocols. PIR's lightness comes from skipping the `specify` phase (the issue body is the spec), not from cutting corners on the retrospective. -The `code-review` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. +The `dev-approval` gate is what makes PIR genuinely different: the human gates the *running implementation* via the worktree before the PR exists, instead of gating the PR after creation. ## Phases @@ -70,15 +70,15 @@ When satisfied, approve via VSCode's "Approve Gate" command (Cmd+K G) or: porch approve plan-approval --a-human-explicitly-approved-this ``` -### Implement (gated by `code-review`) +### Implement (gated by `dev-approval`) The builder: 1. Reads the approved plan file 2. Writes code and tests; runs build + tests via the `checks` block -3. *No AI consult on this phase* — the human at the `code-review` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". +3. *No AI consult on this phase* — the human at the `dev-approval` gate is the sole reviewer of the running code. Matches BUGFIX / AIR's pattern of "no consult on implementation, one consult at PR creation". 4. Pushes the branch -5. Runs `porch done` and `porch next` — the `code-review` gate becomes pending -6. Outputs a **prose** code-review summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. +5. Runs `porch done` and `porch next` — the `dev-approval` gate becomes pending +6. Outputs a **prose** dev-approval summary in the PTY pane (Summary / Files / Test results / Things to look at / How to test locally). This is a transient message to orient the human reviewer — **not a committed file**. The retrospective file is written in the next phase, after the human approves the running code. 7. Sits at the interactive prompt **The reviewer's killer move**: run the worktree locally. @@ -91,7 +91,7 @@ The dev server uses **the same ports and URLs as main** intentionally (OAuth cal Reviewer tests the change on real devices / browsers / simulators. When satisfied, approves via Cmd+K G or: ```bash -porch approve code-review --a-human-explicitly-approved-this +porch approve dev-approval --a-human-explicitly-approved-this ``` ### Review (gated by `pr`) @@ -111,7 +111,7 @@ The builder: PIR uses porch's existing gate machinery. Gate names are opaque strings; no porch engine changes are needed. - **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. -- **`code-review`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. +- **`dev-approval`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. - **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). @@ -146,15 +146,15 @@ PIR uses the same `.codev/config.json` configuration as other protocols. The `wo } ``` -Without `worktree.devCommand`, `afx dev` won't work and the `code-review` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. +Without `worktree.devCommand`, `afx dev` won't work and the `dev-approval` gate degenerates to a diff-read — at which point you should probably use AIR or BUGFIX instead. ## Multi-Agent Consultation - **plan**: human-only review. No AI consultation. -- **implement**: no AI consult — the human at the `code-review` gate is the sole reviewer of the running code. +- **implement**: no AI consult — the human at the `dev-approval` gate is the sole reviewer of the running code. - **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. -Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `code-review`), not AI-consult density. +Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `dev-approval`), not AI-consult density. To disable consultation entirely, say "without multi-agent consultation" when starting work. @@ -193,7 +193,7 @@ Example: `builder/pir-842` for a PIR spawn against GitHub issue #842. ``` codev/plans/-.md # written in plan phase, on builder branch -codev/reviews/-.md # written in review phase (post-code-review-approval), on builder branch; becomes PR body +codev/reviews/-.md # written in review phase (post-dev-approval-approval), on builder branch; becomes PR body codev/projects/-/status.yaml # porch state, managed automatically ``` diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 5743c79b6..213e8d30e 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -348,7 +348,7 @@ function loadProtocolPhases(workspaceRoot: string, protocolName: string): string const GATE_LABELS: Record = { 'spec-approval': 'spec review', 'plan-approval': 'plan review', - 'code-review': 'code review', + 'dev-approval': 'dev review', 'pr': 'PR review', }; @@ -382,7 +382,7 @@ export function detectBlockedGate(parsed: ParsedStatus): string | null { * Keep this list in sync with `detectBlocked`'s `gateLabels` keys. */ export function detectBlockedSince(parsed: ParsedStatus): string | null { - const gateNames = ['spec-approval', 'plan-approval', 'code-review', 'pr']; + const gateNames = ['spec-approval', 'plan-approval', 'dev-approval', 'pr']; for (const gate of gateNames) { if (parsed.gates[gate] === 'pending' && parsed.gateRequestedAt[gate]) { return parsed.gateRequestedAt[gate]; diff --git a/packages/codev/src/commands/porch/__tests__/notify.test.ts b/packages/codev/src/commands/porch/__tests__/notify.test.ts index 4694ce455..109f61cf5 100644 --- a/packages/codev/src/commands/porch/__tests__/notify.test.ts +++ b/packages/codev/src/commands/porch/__tests__/notify.test.ts @@ -119,8 +119,8 @@ describe('notifyTerminal', () => { describe('gateApprovedMessage', () => { it('references the gate and porch next', () => { - const msg = gateApprovedMessage('code-review'); - expect(msg).toContain('code-review'); + const msg = gateApprovedMessage('dev-approval'); + expect(msg).toContain('dev-approval'); expect(msg).toContain('porch next'); }); }); diff --git a/packages/codev/src/commands/porch/__tests__/protocol.test.ts b/packages/codev/src/commands/porch/__tests__/protocol.test.ts index ef2fcacec..2dd02f489 100644 --- a/packages/codev/src/commands/porch/__tests__/protocol.test.ts +++ b/packages/codev/src/commands/porch/__tests__/protocol.test.ts @@ -260,7 +260,7 @@ describe('porch protocol loading', () => { * * PIR's contract is meaningful enough that we lock it in here: * plan → gated by 'plan-approval' → next 'implement' - * implement → gated by 'code-review' → next 'review' + * implement → gated by 'dev-approval' → next 'review' * review → no gate, terminal (next: null) * * The PIR protocol.json itself lives at codev/protocols/pir/ in the repo; @@ -289,7 +289,7 @@ describe('PIR protocol shape', () => { id: 'implement', name: 'Implement', type: 'build_verify', - gate: 'code-review', + gate: 'dev-approval', next: 'review', }, { @@ -329,9 +329,9 @@ describe('PIR protocol shape', () => { expect(getNextPhase(protocol, 'plan')?.id).toBe('implement'); }); - it('gates the implement phase on code-review and transitions to review', () => { + it('gates the implement phase on dev-approval and transitions to review', () => { const protocol = loadProtocol(testDir, 'pir'); - expect(getPhaseGate(protocol, 'implement')).toBe('code-review'); + expect(getPhaseGate(protocol, 'implement')).toBe('dev-approval'); expect(getNextPhase(protocol, 'implement')?.id).toBe('review'); }); @@ -346,11 +346,11 @@ describe('PIR protocol shape', () => { expect(protocol.name).toBe('pir'); }); - it('treats code-review as a valid gate name (no whitelist)', () => { + it('treats dev-approval as a valid gate name (no whitelist)', () => { // Sanity check: porch must accept new gate names purely from data. If a // whitelist were ever added, this test would break before PIR ships. const protocol = loadProtocol(testDir, 'pir'); const gate = getPhaseGate(protocol, 'implement'); - expect(gate).toBe('code-review'); + expect(gate).toBe('dev-approval'); }); }); diff --git a/packages/codev/src/commands/porch/__tests__/status-json.test.ts b/packages/codev/src/commands/porch/__tests__/status-json.test.ts index 161d5a778..e89c47167 100644 --- a/packages/codev/src/commands/porch/__tests__/status-json.test.ts +++ b/packages/codev/src/commands/porch/__tests__/status-json.test.ts @@ -46,7 +46,7 @@ function setupPirProtocol(testDir: string): void { version: '1.0.0', phases: [ { id: 'plan', name: 'Plan', type: 'build_verify', gate: 'plan-approval', next: 'implement' }, - { id: 'implement', name: 'Implement', type: 'build_verify', gate: 'code-review', next: 'review' }, + { id: 'implement', name: 'Implement', type: 'build_verify', gate: 'dev-approval', next: 'review' }, { id: 'review', name: 'Review', type: 'build_verify', next: null }, ], }), @@ -174,11 +174,11 @@ describe('porch status --json', () => { expect(logSpy).not.toHaveBeenCalled(); }); - it('reports the code-review gate correctly when in the implement phase', async () => { + it('reports the dev-approval gate correctly when in the implement phase', async () => { const state = makeState({ phase: 'implement', gates: { - 'code-review': { status: 'pending', requested_at: '2026-05-12T15:00:00.000Z' }, + 'dev-approval': { status: 'pending', requested_at: '2026-05-12T15:00:00.000Z' }, }, build_complete: true, }); @@ -188,7 +188,7 @@ describe('porch status --json', () => { const out = parseStdoutJson(); expect(out.phase).toBe('implement'); - expect(out.gate).toBe('code-review'); + expect(out.gate).toBe('dev-approval'); expect(out.gate_status).toBe('pending'); }); }); From 0b23e133c9b6fb28f766e757e3a8f2427a3c5835 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 20:34:33 +1000 Subject: [PATCH 39/63] docs(pir): reflect three-gate set in CLAUDE.md / AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIR's gate count was stated as "two human gates (plan-approval, dev-approval)" in both the canonical source-repo docs and the codev-skeleton templates. Stale from before the `pr` gate landed in commit b198f861 — PIR now has three human gates. Updated phrasings: - Source-repo (codev/{CLAUDE,AGENTS}.md): short PIR line shifts to "two pre-PR human gates (plan-approval, dev-approval) plus a post-PR `pr` gate" — preserves the "two pre-PR gates" framing that expresses what makes PIR distinctive vs BUGFIX/AIR. Deeper paragraph adds the merge-synchronization `pr` gate to the three- phases description. - Skeleton templates (codev-skeleton/templates/{CLAUDE,AGENTS}.md): short form goes to "three human gates (plan-approval, dev-approval, pr)" — concise enough for the protocol list. Shannon's downstream copies need the same edits; they have both the stale gate count *and* the stale `code-review` name (didn't get the sed pass that hit the canonical files in aa99b9b5). Updated locally in /Users/amrmohamed/repos/cluesmith/shannon; not committed in this repo since shannon is a separate codebase. --- AGENTS.md | 4 ++-- CLAUDE.md | 4 ++-- codev-skeleton/templates/AGENTS.md | 2 +- codev-skeleton/templates/CLAUDE.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c27364650..04d32b489 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -70,7 +70,7 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. +- **PIR**: Plan / Implement / Review — issue-driven with two pre-PR human gates (plan-approval, dev-approval) plus a post-PR `pr` gate. Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/CLAUDE.md b/CLAUDE.md index c27364650..04d32b489 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -70,7 +70,7 @@ You are working in the Codev project itself, with multiple development protocols - **ASPIR**: Autonomous SPIR (no human gates on spec/plan) - `codev/protocols/aspir/protocol.md` - **AIR**: Autonomous Implement & Review for small features - `codev/protocols/air/protocol.md` - **BUGFIX**: Bug fixes from GitHub issues - `codev/protocols/bugfix/protocol.md` -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval). Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. +- **PIR**: Plan / Implement / Review — issue-driven with two pre-PR human gates (plan-approval, dev-approval) plus a post-PR `pr` gate. Lighter than SPIR; stronger than BUGFIX/AIR. Useful when a change needs design review before coding OR pre-PR testing of running code (e.g., mobile / UI / cross-platform). See `codev/protocols/pir/protocol.md`. - **EXPERIMENT**: Disciplined experimentation - `codev/protocols/experiment/protocol.md` - **MAINTAIN**: Codebase maintenance (code hygiene + documentation sync) - `codev/protocols/maintain/protocol.md` - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique - `codev/protocols/research/protocol.md` @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, matching BUGFIX / AIR). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/codev-skeleton/templates/AGENTS.md b/codev-skeleton/templates/AGENTS.md index 6c4a4b8de..037ff7eb8 100644 --- a/codev-skeleton/templates/AGENTS.md +++ b/codev-skeleton/templates/AGENTS.md @@ -12,7 +12,7 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval) (`codev/protocols/pir/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with three human gates (plan-approval, dev-approval, pr) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) diff --git a/codev-skeleton/templates/CLAUDE.md b/codev-skeleton/templates/CLAUDE.md index 35229ccaf..cb1f25a5a 100644 --- a/codev-skeleton/templates/CLAUDE.md +++ b/codev-skeleton/templates/CLAUDE.md @@ -10,7 +10,7 @@ This project uses **Codev** for AI-assisted development. - **ASPIR**: Autonomous SPIR — no human gates on spec/plan (`codev/protocols/aspir/protocol.md`) - **AIR**: Autonomous Implement & Review for small features (`codev/protocols/air/protocol.md`) - **BUGFIX**: Bug fixes from GitHub issues (`codev/protocols/bugfix/protocol.md`) -- **PIR**: Plan / Implement / Review — issue-driven with two human gates (plan-approval, dev-approval) (`codev/protocols/pir/protocol.md`) +- **PIR**: Plan / Implement / Review — issue-driven with three human gates (plan-approval, dev-approval, pr) (`codev/protocols/pir/protocol.md`) - **EXPERIMENT**: Disciplined experimentation (`codev/protocols/experiment/protocol.md`) - **MAINTAIN**: Codebase maintenance (`codev/protocols/maintain/protocol.md`) - **RESEARCH**: Multi-agent research with 3-way investigation, synthesis, and critique (`codev/protocols/research/protocol.md`) From 7d580390b888df9dbd6190dac48cf323d770c2e3 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 20:51:12 +1000 Subject: [PATCH 40/63] feat(vscode): gate-toast action picks the most useful artifact per gate The toast's single button used to always open the builder's terminal pane regardless of which gate the builder was blocked on. Two PIR gates have a single obvious "best artifact" the user wants right away once the toast fires: plan-approval -> "View Plan" runs codev.viewPlanFile (opens codev/plans/-*.md, same as the right-click menu entry). dev-approval -> "Run Dev" runs codev.runWorktreeDev (starts the worktree's dev PTY, matches the gate's purpose of testing the running code before PR). Anything else (spec-approval, code-review, pr, future gates) keeps the existing "Review" -> builder terminal behavior, where the interactive Claude that just announced the gate can be talked to directly. Implementation is a small per-gate map; new specializations just add an entry to GATE_ACTIONS. --- .../vscode/src/notifications/gate-toast.ts | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 26c31b7f1..1d98ee447 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -6,13 +6,17 @@ import type { OverviewCache } from '../views/overview-data.js'; * * Subscribes to OverviewCache changes. Whenever a builder appears in the * blocked-set for the first time (or its gate name changes), fires an - * `showInformationMessage` toast with a single "Review" action that opens - * the **architect** terminal — porch orchestrates through the architect, - * so user-driven review starts there. + * `showInformationMessage` toast with a single action button whose label + * and target are picked from `GATE_ACTIONS` (see below): * - * (Direct artifact access — Open Worktree in New Window, View Plan File, - * Run Dev Server — is available via right-click on builder rows in the - * sidebar. The toast intentionally does not duplicate those entry points.) + * plan-approval → "View Plan" — opens codev/plans/-*.md + * dev-approval → "Run Dev" — starts the worktree's dev PTY + * other gates → "Review" — opens the builder's terminal pane + * + * The fallback hands review off to the interactive Claude that just + * announced the gate-reached message, where the user can read the diff, + * type feedback, or approve via Cmd+K G (or the inline Approve button on + * the sidebar row). * * A `(builderId, gateName)` seen-set is kept in module state so we never * re-toast the same blocked state on subsequent cache ticks. The seen-set @@ -67,6 +71,24 @@ export function activateGateToasts( context.subscriptions.push(cache.onDidChange(onChange)); } +/** + * Per-gate action mapping for the toast's single button. + * + * plan-approval → "View Plan" opens the plan markdown directly + * dev-approval → "Run Dev" starts the dev PTY for the worktree + * other gates → "Review" opens the builder's terminal pane + * + * The plan/dev mappings match the gate's most-useful artifact (plan-approval + * reviews the plan file; dev-approval tests the running code). Anything + * else falls back to the builder pane — gates without a single obvious + * artifact (spec-approval, code-review, pr) are best handled by typing + * feedback to the interactive Claude that just announced the gate. + */ +const GATE_ACTIONS: Record = { + 'plan-approval': { label: 'View Plan', command: 'codev.viewPlanFile' }, + 'dev-approval': { label: 'Run Dev', command: 'codev.runWorktreeDev' }, +}; + function showGateToast( builderId: string, gateName: string, @@ -77,16 +99,13 @@ function showGateToast( const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; const message = `Codev: ${label} blocked on ${gateName}${titleSuffix}`; - // Fire and forget. "Review" opens the builder's own pane — the gate- - // reached message and the builder's interactive Claude live there. From - // that pane the user can read the plan/diff, type feedback, edit files - // in VSCode, or approve via Cmd+K G (or the inline Approve button on - // the sidebar row). + const action = GATE_ACTIONS[gateName] ?? { label: 'Review', command: 'codev.openBuilderById' }; + vscode.window - .showInformationMessage(message, 'Review') + .showInformationMessage(message, action.label) .then((selection) => { - if (selection === 'Review') { - vscode.commands.executeCommand('codev.openBuilderById', builderId); + if (selection === action.label) { + vscode.commands.executeCommand(action.command, builderId); } }); } From a8964944b113993d6869fb7516c4fac11a4e3388 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 21:07:15 +1000 Subject: [PATCH 41/63] feat(vscode): richer approve dialog + one-click Approve from gate-pending toast MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two surfaces for gate approval, each tuned to its trigger: **Gate-pending toast (bottom-right, passive notification)** Adds an [Approve] button alongside the existing per-gate inspection button (View Plan / Run Dev / Review). Clicking [Approve] invokes `codev.approveGate` with `{ skipConfirmation: true }` — bypasses the modal because the toast itself is the context. Fast path: see the toast → review via the inspection button → approve, all without leaving the bottom-right corner. **Approve modal (centered, deliberate action)** Triggered from the sidebar ✓ icon, Cmd+K G, or command palette. Keeps modal blocking — spatial proximity to where the user clicked matters more than non-modal politeness for a deliberate, once-per- gate action. Toast-style notifications in the bottom-right would force a diagonal cursor traversal from the left sidebar. Improvements to the modal: - Display label: `plan review` / `dev review` (from builder.blocked) instead of the kebab-case canonical name (`plan-approval`, `dev-approval`). Matches what the sidebar already shows. - Issue context: `#1334 — fix avatar crop` instead of bare `1334`, truncated to 60 chars to keep the dialog compact. - Per-gate side button via GATE_SIDE_ACTIONS: plan-approval → [View Plan] (opens codev/plans/-*.md) dev-approval → [Open Worktree](opens worktree in new window) others → just [Approve] Clicking the side button dismisses the dialog and runs the linked command; user can re-trigger Approve afterward. Before: Approve plan-approval for 1334? [Approve] [Cancel] After: Approve plan review for #1334 — fix avatar crop? [View Plan] [Approve] [Cancel] `approve.ts` gains an `ApproveGateOptions` type and a fourth parameter `options?: { skipConfirmation?: boolean }`. The extension.ts command registration plumbs the second positional arg through. The `runPorchApprove` shell-out is extracted so both the confirmed-and-skip-confirmation paths share the same code. --- packages/vscode/src/commands/approve.ts | 112 +++++++++++++++--- packages/vscode/src/extension.ts | 4 +- .../vscode/src/notifications/gate-toast.ts | 28 +++-- 3 files changed, 112 insertions(+), 32 deletions(-) diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index f6d343403..696eb205d 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -6,16 +6,46 @@ import type { OverviewCache } from '../views/overview-data.js'; const execFileAsync = promisify(execFile); +/** + * Per-gate side-button mapping for the approval-confirmation dialog. + * + * Lets the reviewer pop open the natural artifact for one final look + * before committing to approval — without first dismissing the dialog + * and re-triggering the command. + * + * Mirror of `gate-toast.ts`'s GATE_ACTIONS — kept parallel rather than + * shared because each surface has different ergonomics (toast at gate- + * pending fires once; this confirmation fires every approval click). + */ +const GATE_SIDE_ACTIONS: Record = { + 'plan-approval': { label: 'View Plan', command: 'codev.viewPlanFile' }, + 'dev-approval': { label: 'Open Worktree', command: 'codev.openWorktreeWindow' }, +}; + +export interface ApproveGateOptions { + /** + * When true, skip the confirmation dialog and approve directly. Used + * by the gate-pending toast (gate-toast.ts), which is itself the + * context — surfacing a second confirmation would be redundant. + */ + skipConfirmation?: boolean; +} + /** * Codev: Approve Gate. * - * Two invocation paths: + * Three invocation paths: * * 1. Right-click a blocked-builder row → pass the builder ID directly. - * Skips the quick-pick; auto-detects the gate from b.blocked. + * Skips the quick-pick; auto-detects the gate from b.blockedGate. + * Shows the rich confirmation dialog. * * 2. Command palette / Cmd+K G → no builder ID → show quick-pick of all - * blocked builders. + * blocked builders. Then the rich confirmation dialog. + * + * 3. Gate-pending toast's [Approve] button → builder ID + options + * { skipConfirmation: true }. The toast was the context; approving + * from there commits directly with no second confirmation. * * After `porch approve` succeeds, refresh the OverviewCache so the * sidebar updates immediately rather than waiting for the SSE round-trip @@ -25,6 +55,7 @@ export async function approveGate( connectionManager: ConnectionManager, cache?: OverviewCache, builderIdArg?: string, + options?: ApproveGateOptions, ): Promise { const client = connectionManager.getClient(); const workspacePath = connectionManager.getWorkspacePath(); @@ -42,40 +73,83 @@ export async function approveGate( // We need blockedGate (canonical name like "plan-approval"), not blocked // (display label like "plan review"). Porch's gate keys are the canonical - // names; the display label is for the sidebar only. - let id: string; + // names; the display label is for the human-facing prompts. + let builder: typeof blocked[number] | undefined; let gate: string; if (builderIdArg) { - const direct = blocked.find(b => b.id === builderIdArg); - if (!direct || !direct.blockedGate) { + builder = blocked.find(b => b.id === builderIdArg); + if (!builder || !builder.blockedGate) { vscode.window.showWarningMessage(`Codev: Builder ${builderIdArg} is not blocked at a gate`); return; } - id = direct.id; - gate = direct.blockedGate; + gate = builder.blockedGate; } else { const candidates = blocked.filter(b => b.blockedGate); const picked = await vscode.window.showQuickPick( candidates.map(b => ({ label: `#${b.issueId ?? b.id} ${b.issueTitle ?? ''}`, description: `blocked on ${b.blocked}`, - id: b.id, + builder: b, gate: b.blockedGate!, })), { placeHolder: 'Select gate to approve' }, ); if (!picked) { return; } - id = picked.id; + builder = picked.builder; gate = picked.gate; } - const confirmed = await vscode.window.showInformationMessage( - `Approve ${gate} for ${id}?`, + const id = builder.id; + const issueRef = builder.issueId ? `#${builder.issueId}` : id; + const titlePart = builder.issueTitle ? ` — ${truncate(builder.issueTitle, 60)}` : ''; + // Display label e.g. "plan review" from overview; falls back to the + // canonical gate name if the display label isn't set. + const gateLabel = builder.blocked ?? gate; + + // Fast path: caller already has context (gate-pending toast). Skip the + // confirmation dialog and go straight to porch approve. + if (options?.skipConfirmation) { + await runPorchApprove(workspacePath, id, gate, gateLabel, issueRef); + cache?.refresh(); + return; + } + + // Rich confirmation: modal (centered, blocking) keeps the dialog close + // to where the user just clicked — the ✓ icon in the left sidebar or + // Cmd+K G near the editor — instead of a bottom-right toast that + // forces a diagonal cursor traversal. Approval is a deliberate, + // once-per-gate action; the modal interrupt is appropriate. + const sideAction = GATE_SIDE_ACTIONS[gate]; + const buttons = sideAction ? [sideAction.label, 'Approve'] : ['Approve']; + + const selection = await vscode.window.showInformationMessage( + `Approve ${gateLabel} for ${issueRef}${titlePart}?`, { modal: true }, - 'Approve', + ...buttons, ); - if (confirmed !== 'Approve') { return; } + if (!selection) { return; } + + if (selection === 'Approve') { + await runPorchApprove(workspacePath, id, gate, gateLabel, issueRef); + cache?.refresh(); + return; + } + + // Side-button clicked. Invoke the corresponding command with the + // builder ID; the user can re-trigger Approve afterward. + if (sideAction && selection === sideAction.label) { + await vscode.commands.executeCommand(sideAction.command, id); + } +} + +async function runPorchApprove( + workspacePath: string, + id: string, + gate: string, + gateLabel: string, + issueRef: string, +): Promise { try { await execFileAsync('porch', [ 'approve', @@ -88,9 +162,9 @@ export async function approveGate( vscode.window.showErrorMessage(`Codev: porch approve failed — ${msg}`); return; } + vscode.window.showInformationMessage(`Codev: Approved ${gateLabel} for ${issueRef}`); +} - vscode.window.showInformationMessage(`Codev: Approved ${gate} for ${id}`); - - // Refresh the cache so the Builders tree updates without waiting for SSE. - cache?.refresh(); +function truncate(s: string, max: number): string { + return s.length > max ? `${s.slice(0, max - 1)}…` : s; } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index fe06445b8..fa418a596 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -247,8 +247,8 @@ export async function activate(context: vscode.ExtensionContext) { }), vscode.commands.registerCommand('codev.spawnBuilder', () => spawnBuilder()), vscode.commands.registerCommand('codev.sendMessage', () => sendMessage(connectionManager!)), - vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined) => - approveGate(connectionManager!, overviewCache, extractBuilderId(arg))), + vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined, options?: { skipConfirmation?: boolean }) => + approveGate(connectionManager!, overviewCache, extractBuilderId(arg), options)), vscode.commands.registerCommand('codev.cleanupBuilder', () => cleanupBuilder(connectionManager!, overviewCache)), vscode.commands.registerCommand('codev.openWorktreeWindow', (arg: vscode.TreeItem | string | undefined) => openWorktreeWindow(connectionManager!, extractBuilderId(arg))), diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 1d98ee447..052a334f5 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -6,17 +6,16 @@ import type { OverviewCache } from '../views/overview-data.js'; * * Subscribes to OverviewCache changes. Whenever a builder appears in the * blocked-set for the first time (or its gate name changes), fires an - * `showInformationMessage` toast with a single action button whose label - * and target are picked from `GATE_ACTIONS` (see below): + * `showInformationMessage` toast with two action buttons: * - * plan-approval → "View Plan" — opens codev/plans/-*.md - * dev-approval → "Run Dev" — starts the worktree's dev PTY - * other gates → "Review" — opens the builder's terminal pane - * - * The fallback hands review off to the interactive Claude that just - * announced the gate-reached message, where the user can read the diff, - * type feedback, or approve via Cmd+K G (or the inline Approve button on - * the sidebar row). + * 1. A per-gate "inspection" button from `GATE_ACTIONS`: + * plan-approval → "View Plan" — opens codev/plans/-*.md + * dev-approval → "Run Dev" — starts the worktree's dev PTY + * other gates → "Review" — opens the builder's terminal pane + * 2. "Approve" — commits to porch approve directly via + * `codev.approveGate` with `{ skipConfirmation: true }`. The toast + * is the context; surfacing a second confirmation here would be + * friction without value. * * A `(builderId, gateName)` seen-set is kept in module state so we never * re-toast the same blocked state on subsequent cache ticks. The seen-set @@ -101,11 +100,18 @@ function showGateToast( const action = GATE_ACTIONS[gateName] ?? { label: 'Review', command: 'codev.openBuilderById' }; + // Two-button toast: [] [Approve]. + // The toast itself is the context — clicking Approve here skips the + // rich confirmation dialog (approve.ts's normal path). The reviewer + // chose to act on the toast directly; surfacing a second confirmation + // would be friction without value. vscode.window - .showInformationMessage(message, action.label) + .showInformationMessage(message, action.label, 'Approve') .then((selection) => { if (selection === action.label) { vscode.commands.executeCommand(action.command, builderId); + } else if (selection === 'Approve') { + vscode.commands.executeCommand('codev.approveGate', builderId, { skipConfirmation: true }); } }); } From 7862be1b69872108926d680cc248d8d940eba4b8 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 21:15:02 +1000 Subject: [PATCH 42/63] =?UTF-8?q?fix(vscode):=20per-gate=20toast=20actions?= =?UTF-8?q?=20=E2=80=94=20use=20canonical=20gate=20key=20for=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two issues stacking on the per-gate toast/modal action mapping: **1. gate-toast.ts has always missed for per-gate actions.** GATE_ACTIONS is keyed by canonical gate names ('plan-approval', 'dev-approval', 'pr'). But the lookup passed `b.blocked` — the human-facing display label ('plan review', 'dev review', 'PR review'). 'dev review' is not in the map → falls through to the default `{ label: 'Review', command: 'codev.openBuilderById' }`. Net effect: since dedicated per-gate actions were introduced, every gate toast has shown `[Review] [Approve]` instead of the intended `[View Plan] [Approve]` / `[Run Dev] [Approve]` etc. Fix: pass both `b.blockedGate` (canonical key, for the GATE_ACTIONS lookup and the seen-set key) and `b.blocked` (display label, for the user-facing message text). Adds the `gateLabel` parameter to `showGateToast`; the existing `gateName` parameter is now strictly the canonical key. The seen-set's key now uses the canonical gate name too — incidentally more stable across display-label changes (display labels could be re-worded; canonical names are part of the protocol contract). **2. dev-approval action drifted between toast and modal.** approve.ts's GATE_SIDE_ACTIONS had 'dev-approval' → 'Open Worktree' (open the worktree in a new VSCode window); gate-toast.ts's GATE_ACTIONS had 'dev-approval' → 'Run Dev' (start the dev server). Two different labels for the same gate is jarring — a reviewer who learns one might wonder what they're missing on the other surface. Unified to **Run Dev** in both places. PIR's distinctive feature is that the human reviews the *running implementation*, so `afx dev` is the action that matches the gate's intent. New-window view stays reachable via the right-click "Open Worktree in New Window" item on the sidebar. The two GATE_ACTIONS / GATE_SIDE_ACTIONS maps are now byte-equivalent for their overlapping keys. Updated approve.ts's docstring to note they must stay in sync. --- packages/vscode/src/commands/approve.ts | 10 ++++++---- packages/vscode/src/notifications/gate-toast.ts | 15 ++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/vscode/src/commands/approve.ts b/packages/vscode/src/commands/approve.ts index 696eb205d..1eb524fe9 100644 --- a/packages/vscode/src/commands/approve.ts +++ b/packages/vscode/src/commands/approve.ts @@ -13,13 +13,15 @@ const execFileAsync = promisify(execFile); * before committing to approval — without first dismissing the dialog * and re-triggering the command. * - * Mirror of `gate-toast.ts`'s GATE_ACTIONS — kept parallel rather than - * shared because each surface has different ergonomics (toast at gate- - * pending fires once; this confirmation fires every approval click). + * Mirrors `gate-toast.ts`'s GATE_ACTIONS one-for-one so a given gate + * surfaces the same inspection action from either entry point. The maps + * are kept in separate files because the two surfaces have different + * ergonomics (toast at gate-pending fires once; this confirmation fires + * every approval click), but their *contents* must stay in sync. */ const GATE_SIDE_ACTIONS: Record = { 'plan-approval': { label: 'View Plan', command: 'codev.viewPlanFile' }, - 'dev-approval': { label: 'Open Worktree', command: 'codev.openWorktreeWindow' }, + 'dev-approval': { label: 'Run Dev', command: 'codev.runWorktreeDev' }, }; export interface ApproveGateOptions { diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 052a334f5..277076bb6 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -48,14 +48,18 @@ export function activateGateToasts( const currentBlocked = new Set(); for (const b of data.builders) { - if (!b.blocked) { + if (!b.blocked || !b.blockedGate) { continue; } - const key = `${b.id}::${b.blocked}`; + // Track and lookup keys use the CANONICAL gate name (b.blockedGate, + // e.g. "dev-approval"). The display label (b.blocked, e.g. "dev + // review") is for human-facing text only — using it as a lookup key + // breaks GATE_ACTIONS since the map's keys are canonical names. + const key = `${b.id}::${b.blockedGate}`; currentBlocked.add(key); if (!seen.has(key)) { seen.add(key); - showGateToast(b.id, b.blocked, b.issueId, b.issueTitle); + showGateToast(b.id, b.blockedGate, b.blocked, b.issueId, b.issueTitle); } } @@ -90,13 +94,14 @@ const GATE_ACTIONS: Record = { function showGateToast( builderId: string, - gateName: string, + gateName: string, // canonical key for GATE_ACTIONS lookup, e.g. "dev-approval" + gateLabel: string, // human-facing label for the toast text, e.g. "dev review" issueId?: string | number | null, issueTitle?: string | null, ): void { const label = issueId ? `#${issueId}` : builderId; const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; - const message = `Codev: ${label} blocked on ${gateName}${titleSuffix}`; + const message = `Codev: ${label} blocked on ${gateLabel}${titleSuffix}`; const action = GATE_ACTIONS[gateName] ?? { label: 'Review', command: 'codev.openBuilderById' }; From ca75e30a94394d544a7ddc151f9075587fd1918b Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Fri, 15 May 2026 21:55:16 +1000 Subject: [PATCH 43/63] refactor(pir): builder runs merge after pr gate approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous pr-gate design removed `gh pr merge` from the builder entirely. Combined with the architect role's "DO NOT merge PRs yourself" rule, that left the user with a manual GitHub trip — the pr gate was a wall, not a handoff. Restored merge to the builder, but with a structurally safer trigger: porch gate-approved state, not free-text "merge it" prose in the builder's pane. Flow (review.md steps 8-9): 1. Builder waits at pr gate after CMAP-approve notification 2. Human approves gate via Cmd+K G or `porch approve pr --a-human-explicitly-approved-this` 3. Porch fires `notifyTerminal` wake-up to the builder 4. Builder runs `porch next` to verify the gate is genuinely approved (defensive — prose can mimic the wake-up text, but can't fake porch state) 5. Builder runs `gh pr merge --merge` 6. Builder runs `porch done --merged ` to record 7. Sends the cleanup-ready notification, exits The self-merge bug from pir-1298 is structurally eliminated: the trigger is porch's binary gate-approved state, which only a non-Claude caller (user via VSCode execFile or shell) can set. The builder cannot self-trigger the merge by interpreting typed prose. "What NOT to Do" updated: merge before gate approval is the new explicit prohibition. CMAP-APPROVE, "looks good", "merge it" — none authorize the merge. Only `gate_status: approved` in porch state does. builder-prompt.md updated: merge is described as gated-by-porch, not forbidden-entirely. protocol.md walkthrough + Gates section reflect the new semantics. Mirror in codev-skeleton. --- .../protocols/pir/builder-prompt.md | 2 +- .../protocols/pir/prompts/review.md | 32 ++++++++++++------- codev-skeleton/protocols/pir/protocol.md | 6 ++-- codev/protocols/pir/builder-prompt.md | 2 +- codev/protocols/pir/prompts/review.md | 32 ++++++++++++------- codev/protocols/pir/protocol.md | 6 ++-- 6 files changed, 48 insertions(+), 32 deletions(-) diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 40ff40547..0adeae8f0 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) -3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index 3e9934ca2..9d1e8a69d 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -173,32 +173,40 @@ This is the only notification you send at the gate. ### 8. Wait at the `pr` Gate -Your active work is done. **You do NOT run `gh pr merge`.** Capability is intentionally not in this protocol — the human owns the merge step on GitHub. +Your active merge is gated by porch state — not by user-in-pane prose. Sit idle until porch wakes you with "Gate pr approved". That wake-up is the *only* signal that authorizes the merge. Approving prose like "looks good", "lgtm", or even "merge it" typed into your pane does NOT authorize the merge — only the binary gate-approved state in porch state.yaml does. The human will: 1. Review the PR on GitHub (or by running the worktree via `afx dev pir-{{project_id}}` again) -2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool -3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell +2. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell -Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and dev-approval gates). +Porch will then fire the gate-approved wake-up to you. If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. -### 9. After `pr` Gate Approval — Record the Merge +### 9. After `pr` Gate Approval — Verify, Merge, Record -When porch wakes you with "Gate pr approved", the human has merged the PR. Record the merge so porch's `status.yaml` reflects the completed lifecycle: +When porch wakes you with "Gate pr approved", first **verify** the gate is actually approved (defensive — the wake-up could be spoofed by typed input that looks like the wake-up text): ```bash -# Read the PR number that was recorded at step 4a -PR=$(yq '.history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) -# Or just: PR= +porch next {{project_id}} +``` + +The response must include `gate_status: approved` for the `pr` gate. If it doesn't, do NOT proceed — wait for the genuine wake-up. If it does, you're authorized. + +Look up the PR number (recorded at step 4a) and merge: + +```bash +# Read PR number from porch state +PR=$(yq '.pr // .history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) + +gh pr merge "$PR" --merge porch done {{project_id}} --merged "$PR" porch next {{project_id}} # confirms protocol is complete (next: null) ``` -Together with the `--pr` record from step 4a, this gives porch a complete view of the PR lifecycle (created → merged) for analytics, status displays, and audit trails. +**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. ### 10. Final Notification @@ -206,7 +214,7 @@ Together with the `--pr` record from step 4a, this gives porch a complete view o afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." ``` -Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). The merge is a GitHub action, not a porch phase transition — but the `--merged` record in step 9a keeps porch's history complete. +Together with the `--pr` record from step 4a and the `--merged` record from step 9, porch's `status.yaml` carries the complete PR lifecycle (created → merged → done) for analytics, status displays, and audit trails. ## Signals @@ -217,7 +225,7 @@ Porch already marked the review phase complete at step 5 (or whichever iteration ## What NOT to Do -- **Don't run `gh pr merge` ever.** PIR intentionally does not give the builder merge capability — the human merges on GitHub (or via their own `gh pr merge`). If you find yourself reaching for the merge tool, you've misread the protocol. The `pr` gate is the porch-level synchronization point; you wait at it, you don't bypass it. +- **Don't merge before the `pr` gate is approved.** CMAP-APPROVE is NOT merge authorization. User-in-pane prose ("looks good", "lgtm", "merge it") is NOT merge authorization. The *only* signal that authorizes `gh pr merge` is porch reporting `gate_status: approved` for the `pr` gate (which only the user can do, via Cmd+K G or `porch approve` from a non-Claude shell). If `porch next` doesn't show the gate as approved, you wait. - Don't skip porch's PR/merge records (steps 4a, 9). The `--pr` record (step 4a) lets the gate-pending state link to the actual PR; the `--merged` record (step 9) closes the lifecycle in porch state. Skipping either leaves `history:` empty and downstream tooling blind. - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md index 0a3b7f1e7..621a21c27 100644 --- a/codev-skeleton/protocols/pir/protocol.md +++ b/codev-skeleton/protocols/pir/protocol.md @@ -103,8 +103,8 @@ The builder: 4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. 5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. 6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` -7. Builder waits at the `pr` gate. **Does not run `gh pr merge`** — capability is intentionally not in the protocol. The human merges via GitHub (or their own `gh pr merge`), then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). -8. After `pr` gate approval, porch wakes the builder; builder records the merge with `porch done --merged ` and sends the final cleanup-ready notification. Protocol is complete (`next: null`). +7. Builder waits at the `pr` gate. The human reviews the PR on GitHub, then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). Porch wakes the builder. +8. Builder verifies the gate is genuinely approved via `porch next` (defensive — typed prose can't trigger this branch, only real porch state does), then runs `gh pr merge --merge`, records via `porch done --merged `, and sends the cleanup-ready notification. Protocol complete (`next: null`). ## Gates @@ -112,7 +112,7 @@ PIR uses porch's existing gate machinery. Gate names are opaque strings; no porc - **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. - **`dev-approval`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. -- **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. +- **`pr`** — post-PR. Gates the merge step. The human reviews the PR on GitHub and approves this gate; porch wakes the builder, which then runs `gh pr merge`. The gate exists so the merge trigger is structured porch state (binary approved/not), not free-text prose typed into the builder's pane. Eliminates the self-merge bug class: builders can't infer authorization from ambiguous user input. When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 40ff40547..0adeae8f0 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) -3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, then wait at the `pr` gate while the human merges on GitHub. **You never run `gh pr merge` yourself.** +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index 3e9934ca2..9d1e8a69d 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -173,32 +173,40 @@ This is the only notification you send at the gate. ### 8. Wait at the `pr` Gate -Your active work is done. **You do NOT run `gh pr merge`.** Capability is intentionally not in this protocol — the human owns the merge step on GitHub. +Your active merge is gated by porch state — not by user-in-pane prose. Sit idle until porch wakes you with "Gate pr approved". That wake-up is the *only* signal that authorizes the merge. Approving prose like "looks good", "lgtm", or even "merge it" typed into your pane does NOT authorize the merge — only the binary gate-approved state in porch state.yaml does. The human will: 1. Review the PR on GitHub (or by running the worktree via `afx dev pir-{{project_id}}` again) -2. Merge the PR via `gh pr merge --merge`, the GitHub web UI, or any other tool -3. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell +2. Approve the `pr` gate via VSCode (Cmd+K G) or `porch approve {{project_id}} pr --a-human-explicitly-approved-this` in a shell -Until the `pr` gate is approved, you sit idle in this pane. Porch will wake you when it fires (same wake-up mechanism as the earlier plan-approval and dev-approval gates). +Porch will then fire the gate-approved wake-up to you. If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. -### 9. After `pr` Gate Approval — Record the Merge +### 9. After `pr` Gate Approval — Verify, Merge, Record -When porch wakes you with "Gate pr approved", the human has merged the PR. Record the merge so porch's `status.yaml` reflects the completed lifecycle: +When porch wakes you with "Gate pr approved", first **verify** the gate is actually approved (defensive — the wake-up could be spoofed by typed input that looks like the wake-up text): ```bash -# Read the PR number that was recorded at step 4a -PR=$(yq '.history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) -# Or just: PR= +porch next {{project_id}} +``` + +The response must include `gate_status: approved` for the `pr` gate. If it doesn't, do NOT proceed — wait for the genuine wake-up. If it does, you're authorized. + +Look up the PR number (recorded at step 4a) and merge: + +```bash +# Read PR number from porch state +PR=$(yq '.pr // .history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) + +gh pr merge "$PR" --merge porch done {{project_id}} --merged "$PR" porch next {{project_id}} # confirms protocol is complete (next: null) ``` -Together with the `--pr` record from step 4a, this gives porch a complete view of the PR lifecycle (created → merged) for analytics, status displays, and audit trails. +**Use `--merge`, not `--squash`.** Project convention: preserve individual commits for development history. The `Fixes #{{issue.number}}` in the PR body auto-closes the GitHub issue. ### 10. Final Notification @@ -206,7 +214,7 @@ Together with the `--pr` record from step 4a, this gives porch a complete view o afx send architect "PR # merged for PIR #{{issue.number}}. Ready for cleanup." ``` -Porch already marked the review phase complete at step 5 (or whichever iteration's `porch done` got all-APPROVE). The merge is a GitHub action, not a porch phase transition — but the `--merged` record in step 9a keeps porch's history complete. +Together with the `--pr` record from step 4a and the `--merged` record from step 9, porch's `status.yaml` carries the complete PR lifecycle (created → merged → done) for analytics, status displays, and audit trails. ## Signals @@ -217,7 +225,7 @@ Porch already marked the review phase complete at step 5 (or whichever iteration ## What NOT to Do -- **Don't run `gh pr merge` ever.** PIR intentionally does not give the builder merge capability — the human merges on GitHub (or via their own `gh pr merge`). If you find yourself reaching for the merge tool, you've misread the protocol. The `pr` gate is the porch-level synchronization point; you wait at it, you don't bypass it. +- **Don't merge before the `pr` gate is approved.** CMAP-APPROVE is NOT merge authorization. User-in-pane prose ("looks good", "lgtm", "merge it") is NOT merge authorization. The *only* signal that authorizes `gh pr merge` is porch reporting `gate_status: approved` for the `pr` gate (which only the user can do, via Cmd+K G or `porch approve` from a non-Claude shell). If `porch next` doesn't show the gate as approved, you wait. - Don't skip porch's PR/merge records (steps 4a, 9). The `--pr` record (step 4a) lets the gate-pending state link to the actual PR; the `--merged` record (step 9) closes the lifecycle in porch state. Skipping either leaves `history:` empty and downstream tooling blind. - Don't run `porch approve` for any gate yourself - Don't push to main — only merge via PR diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md index 0a3b7f1e7..621a21c27 100644 --- a/codev/protocols/pir/protocol.md +++ b/codev/protocols/pir/protocol.md @@ -103,8 +103,8 @@ The builder: 4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. 5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. 6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` -7. Builder waits at the `pr` gate. **Does not run `gh pr merge`** — capability is intentionally not in the protocol. The human merges via GitHub (or their own `gh pr merge`), then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). -8. After `pr` gate approval, porch wakes the builder; builder records the merge with `porch done --merged ` and sends the final cleanup-ready notification. Protocol is complete (`next: null`). +7. Builder waits at the `pr` gate. The human reviews the PR on GitHub, then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). Porch wakes the builder. +8. Builder verifies the gate is genuinely approved via `porch next` (defensive — typed prose can't trigger this branch, only real porch state does), then runs `gh pr merge --merge`, records via `porch done --merged `, and sends the cleanup-ready notification. Protocol complete (`next: null`). ## Gates @@ -112,7 +112,7 @@ PIR uses porch's existing gate machinery. Gate names are opaque strings; no porc - **`plan-approval`** — pre-PR. Human reads the plan file (committed on the builder branch) and approves before any code is written. Gates are keyed by `(project_id, gate_name)` so the name is safe to share with other protocols. - **`dev-approval`** — pre-PR. The human reviews the *running* worktree (via `afx dev`) before any PR exists. This is PIR's distinctive gate. -- **`pr`** — post-PR. Gates the merge step. The human merges on GitHub (or via `gh pr merge` from their own shell) and approves this gate to signal "merge done". This gate exists to keep the merge step out of the builder's hands — the builder never runs `gh pr merge` itself. +- **`pr`** — post-PR. Gates the merge step. The human reviews the PR on GitHub and approves this gate; porch wakes the builder, which then runs `gh pr merge`. The gate exists so the merge trigger is structured porch state (binary approved/not), not free-text prose typed into the builder's pane. Eliminates the self-merge bug class: builders can't infer authorization from ambiguous user input. When a gate becomes pending, porch broadcasts `overview-changed` via SSE. The VSCode Builders tree picks up the blocked state and renders it with a bell icon; a toast surfaces the new gate-pending event. Architect notification is *not* automatic — gates surface via the toast/sidebar (for IDE users) or by checking the builder pane / `porch pending` (for CLI users). The builder's job at any gate is to write the artifact, commit, signal completion, and wait — never to invoke `porch approve` itself (Claude refuses the `--a-human-explicitly-approved-this` flag by design). From 11c98ec50bf895403e2d0d0734d607f3b5349a00 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 09:00:40 +1000 Subject: [PATCH 44/63] feat(vscode): surface assigned-to-me backlog issues + click-to-spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backlog sidebar improvements so the architect can find and start their own work without leaving VSCode: - Issues assigned to the current GitHub user sort to the top of the Backlog view with an `account` icon + "assigned to you" description; the rest keep the `issues` icon. Single view, no separator rows — mirrors how BuildersProvider sorts blocked-first above active. - Row click now starts work: invokes codev.spawnBuilder with the issue number pre-filled, jumping straight to the protocol quick-pick (branch prompt skipped on this path). The palette flow is unchanged. - Right-click context menu (consistent with the Builders view's group/when/title conventions): Spawn Builder, Open Issue in Browser, Copy Issue Number. Current-user detection is resolved Tower-side and rides the existing /api/overview payload (new optional OverviewData.currentUser), so the VSCode client needs no new fetch path and it stays tunnel-safe. Resolution goes through a new typed fetchCurrentUser() wrapper in lib/github.ts (user-identity concept, raw output), keeping overview.ts consistent with how it fetches PRs/issues/closed/merged — same parallel Promise.all batch, same per-cwd cache pattern (1h TTL since identity is session-stable). currentUser added to both OverviewData interfaces — the server-local one in overview.ts (what Tower serializes) and the shared one in @cluesmith/codev-types (what the VSCode client deserializes). --- .../src/agent-farm/__tests__/overview.test.ts | 35 +++++++++++++++- .../codev/src/agent-farm/servers/overview.ts | 35 +++++++++++++++- packages/codev/src/lib/github.ts | 19 +++++++++ packages/types/src/api.ts | 2 + packages/vscode/package.json | 31 ++++++++++++++ packages/vscode/src/commands/spawn.ts | 40 ++++++++++++------- packages/vscode/src/extension.ts | 29 +++++++++++++- .../vscode/src/views/backlog-tree-item.ts | 24 +++++++++++ packages/vscode/src/views/backlog.ts | 34 +++++++++++++--- 9 files changed, 225 insertions(+), 24 deletions(-) create mode 100644 packages/vscode/src/views/backlog-tree-item.ts diff --git a/packages/codev/src/agent-farm/__tests__/overview.test.ts b/packages/codev/src/agent-farm/__tests__/overview.test.ts index 1c783046d..8010d69de 100644 --- a/packages/codev/src/agent-farm/__tests__/overview.test.ts +++ b/packages/codev/src/agent-farm/__tests__/overview.test.ts @@ -28,12 +28,13 @@ import { // Mocks // ============================================================================ -const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol } = vi.hoisted(() => ({ +const { mockFetchPRList, mockFetchIssueList, mockFetchRecentlyClosed, mockFetchMergedPRs, mockLoadProtocol, mockFetchCurrentUser } = vi.hoisted(() => ({ mockFetchPRList: vi.fn(), mockFetchIssueList: vi.fn(), mockFetchRecentlyClosed: vi.fn(), mockFetchMergedPRs: vi.fn(), mockLoadProtocol: vi.fn(), + mockFetchCurrentUser: vi.fn(), })); vi.mock('../../lib/github.js', async (importOriginal) => { @@ -44,6 +45,7 @@ vi.mock('../../lib/github.js', async (importOriginal) => { fetchIssueList: mockFetchIssueList, fetchRecentlyClosed: mockFetchRecentlyClosed, fetchRecentMergedPRs: mockFetchMergedPRs, + fetchCurrentUser: mockFetchCurrentUser, }; }); @@ -126,6 +128,7 @@ describe('overview', () => { mockFetchIssueList.mockResolvedValue([]); mockFetchRecentlyClosed.mockResolvedValue([]); mockFetchMergedPRs.mockResolvedValue([]); + mockFetchCurrentUser.mockResolvedValue('octocat'); }); afterEach(() => { @@ -1393,6 +1396,36 @@ describe('overview', () => { expect(mockFetchPRList).toHaveBeenCalledTimes(1); }); + it('resolves currentUser from the user-identity concept', async () => { + mockFetchCurrentUser.mockResolvedValue('octocat'); + + const cache = new OverviewCache(); + const data = await cache.getOverview(tmpDir); + + expect(data.currentUser).toBe('octocat'); + expect(mockFetchCurrentUser).toHaveBeenCalledWith(tmpDir); + }); + + it('omits currentUser when user-identity resolution fails', async () => { + mockFetchCurrentUser.mockResolvedValue(null); + + const cache = new OverviewCache(); + const data = await cache.getOverview(tmpDir); + + expect(data.currentUser).toBeUndefined(); + expect(data.backlog).toEqual([]); + }); + + it('caches currentUser across getOverview calls', async () => { + mockFetchCurrentUser.mockResolvedValue('octocat'); + + const cache = new OverviewCache(); + await cache.getOverview(tmpDir); + await cache.getOverview(tmpDir); + + expect(mockFetchCurrentUser).toHaveBeenCalledTimes(1); + }); + it('invalidates cache on refresh', async () => { mockFetchPRList.mockResolvedValue([]); mockFetchIssueList.mockResolvedValue([]); diff --git a/packages/codev/src/agent-farm/servers/overview.ts b/packages/codev/src/agent-farm/servers/overview.ts index 213e8d30e..1ca1c41c7 100644 --- a/packages/codev/src/agent-farm/servers/overview.ts +++ b/packages/codev/src/agent-farm/servers/overview.ts @@ -14,6 +14,7 @@ import { fetchIssueList, fetchRecentlyClosed, fetchRecentMergedPRs, + fetchCurrentUser, parseLinkedIssue, parseLabelDefaults, } from '../../lib/github.js'; @@ -100,6 +101,8 @@ export interface OverviewData { pendingPRs: PROverview[]; backlog: BacklogItem[]; recentlyClosed: RecentlyClosedItem[]; + /** Auto-detected forge login of the current user (via the user-identity concept). */ + currentUser?: string; errors?: { prs?: string; issues?: string }; } @@ -722,7 +725,9 @@ export class OverviewCache { private issueCache = new Map(); private closedCache = new Map(); private mergedPRCache = new Map(); + private currentUserCache = new Map(); private readonly TTL = 30_000; + private readonly USER_TTL = 3_600_000; // 1h — GitHub identity is session-stable /** * Build the overview response. Aggregates builder state, PRs, and backlog. @@ -775,12 +780,14 @@ export class OverviewCache { .filter((id): id is string => id !== null), ); - // 2. Fetch PRs, issues, recently closed, and merged PRs in parallel (each is independently cached) - const [prs, issues, closed, mergedPRs] = await Promise.all([ + // 2. Fetch PRs, issues, recently closed, merged PRs, and current user in + // parallel (each is independently cached) + const [prs, issues, closed, mergedPRs, currentUser] = await Promise.all([ this.fetchPRsCached(workspaceRoot), this.fetchIssuesCached(workspaceRoot), this.fetchRecentlyClosedCached(workspaceRoot), this.fetchMergedPRsCached(workspaceRoot), + this.fetchCurrentUserCached(workspaceRoot), ]); // 3. Process PRs @@ -863,6 +870,9 @@ export class OverviewCache { } const result: OverviewData = { builders, pendingPRs, backlog, recentlyClosed }; + if (currentUser) { + result.currentUser = currentUser; + } if (Object.keys(errors).length > 0) { result.errors = errors; } @@ -877,6 +887,7 @@ export class OverviewCache { this.issueCache.clear(); this.closedCache.clear(); this.mergedPRCache.clear(); + this.currentUserCache.clear(); } // =========================================================================== @@ -911,6 +922,26 @@ export class OverviewCache { return data; } + /** + * Resolve the current user's forge login via the `user-identity` concept. + * Long TTL — identity is stable for the lifetime of a Tower session. + * Only successful resolutions are cached, so a transient failure (gh + * logged out, offline) self-heals on the next overview poll. + */ + private async fetchCurrentUserCached(cwd: string): Promise { + const now = Date.now(); + const cached = this.currentUserCache.get(cwd); + if (cached && (now - cached.fetchedAt) < this.USER_TTL) { + return cached.data; + } + + const login = await fetchCurrentUser(cwd); + if (login !== null) { + this.currentUserCache.set(cwd, { data: login, fetchedAt: now }); + } + return login; + } + private async fetchRecentlyClosedCached(cwd: string): Promise { const now = Date.now(); const cached = this.closedCache.get(cwd); diff --git a/packages/codev/src/lib/github.ts b/packages/codev/src/lib/github.ts index 5b913d5ae..edf9ab7d6 100644 --- a/packages/codev/src/lib/github.ts +++ b/packages/codev/src/lib/github.ts @@ -115,6 +115,25 @@ export async function fetchIssueList( return result as ForgeIssueListItem[] | null; } +/** + * Resolve the current user's forge login. + * Routes through the `user-identity` concept command (default: + * `gh api user --jq .login`). The concept emits a bare string, not JSON, + * so `raw: true` is required. Returns null on failure (e.g. `gh` + * unauthenticated) so callers can degrade gracefully. + */ +export async function fetchCurrentUser( + cwd?: string, + forgeConfig?: ForgeConfig | null, +): Promise { + const result = await executeForgeCommand('user-identity', {}, { + cwd, + forgeConfig, + raw: true, + }); + return typeof result === 'string' && result.trim() ? result.trim() : null; +} + /** * Fetch recently closed issues (last 24 hours). * Routes through the `recently-closed` concept command. diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 71f2571ed..e20b844d7 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -140,6 +140,8 @@ export interface OverviewData { pendingPRs: OverviewPR[]; backlog: OverviewBacklogItem[]; recentlyClosed: OverviewRecentlyClosed[]; + /** Auto-detected GitHub login of the current user (via the user-identity forge concept). */ + currentUser?: string; errors?: { prs?: string; issues?: string }; } diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 76aadddba..8c480a54f 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -133,6 +133,14 @@ "command": "codev.viewPlanFile", "title": "Codev: View Plan File" }, + { + "command": "codev.openBacklogIssue", + "title": "Codev: Open Issue in Browser" + }, + { + "command": "codev.copyBacklogIssueNumber", + "title": "Codev: Copy Issue Number" + }, { "command": "codev.submitReviewComment", "title": "Submit review comment", @@ -150,6 +158,14 @@ "command": "codev.openBuilderById", "when": "false" }, + { + "command": "codev.openBacklogIssue", + "when": "false" + }, + { + "command": "codev.copyBacklogIssueNumber", + "when": "false" + }, { "command": "codev.addReviewComment", "when": "editorLangId == 'markdown'" @@ -208,6 +224,21 @@ "command": "codev.stopWorktreeDev", "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "3_dev@3" + }, + { + "command": "codev.spawnBuilder", + "when": "view == codev.backlog && viewItem == backlog-item", + "group": "1_primary@1" + }, + { + "command": "codev.openBacklogIssue", + "when": "view == codev.backlog && viewItem == backlog-item", + "group": "1_primary@2" + }, + { + "command": "codev.copyBacklogIssueNumber", + "when": "view == codev.backlog && viewItem == backlog-item", + "group": "1_primary@3" } ], "view/title": [ diff --git a/packages/vscode/src/commands/spawn.ts b/packages/vscode/src/commands/spawn.ts index 05cc29b4c..987ca60bf 100644 --- a/packages/vscode/src/commands/spawn.ts +++ b/packages/vscode/src/commands/spawn.ts @@ -2,14 +2,24 @@ import * as vscode from 'vscode'; import { spawn } from 'node:child_process'; /** - * Codev: Spawn Builder — quick-pick flow for issue + protocol + optional branch. + * Codev: Spawn Builder. + * + * Two entry points: + * - No arg (command palette): full flow — issue input → protocol → optional branch. + * - `issueArg` provided (Backlog row-click / context menu): the issue is + * already known, so jump straight to the protocol pick and spawn. The + * branch prompt is skipped too — starting work on a backlog issue means + * a fresh branch. */ -export async function spawnBuilder(): Promise { - const issueNumber = await vscode.window.showInputBox({ - prompt: 'Issue number', - placeHolder: '42', - }); - if (!issueNumber) { return; } +export async function spawnBuilder(issueArg?: string): Promise { + let issueNumber = issueArg; + if (!issueNumber) { + issueNumber = await vscode.window.showInputBox({ + prompt: 'Issue number', + placeHolder: '42', + }); + if (!issueNumber) { return; } + } const protocol = await vscode.window.showQuickPick( ['spir', 'aspir', 'pir', 'air', 'bugfix', 'tick'], @@ -17,14 +27,16 @@ export async function spawnBuilder(): Promise { ); if (!protocol) { return; } - const branch = await vscode.window.showInputBox({ - prompt: 'Branch name (optional — leave empty for new branch)', - placeHolder: 'feature/my-branch', - }); - const args = ['spawn', issueNumber, '--protocol', protocol]; - if (branch) { - args.push('--branch', branch); + + if (!issueArg) { + const branch = await vscode.window.showInputBox({ + prompt: 'Branch name (optional — leave empty for new branch)', + placeHolder: 'feature/my-branch', + }); + if (branch) { + args.push('--branch', branch); + } } runAfxCommand(args); diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index fa418a596..4d9dabb52 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -28,6 +28,7 @@ import { TeamProvider } from './views/team.js'; import { StatusProvider } from './views/status.js'; import { WorkspaceProvider } from './views/workspace.js'; import { BuilderTreeItem } from './views/builder-tree-item.js'; +import { BacklogTreeItem } from './views/backlog-tree-item.js'; let connectionManager: ConnectionManager | null = null; let terminalManager: TerminalManager | null = null; @@ -47,6 +48,20 @@ function extractBuilderId(arg: vscode.TreeItem | string | undefined): string | u return undefined; } +/** + * Resolve an issue number from a command argument. + * + * Backlog row-click passes the issue id as a string via + * `item.command.arguments`; right-click context-menu invocations pass the + * BacklogTreeItem itself; command-palette invocations pass nothing → + * undefined → spawnBuilder falls back to its full quick-pick flow. + */ +function extractIssueId(arg: vscode.TreeItem | string | undefined): string | undefined { + if (typeof arg === 'string') { return arg; } + if (arg instanceof BacklogTreeItem) { return arg.issueId; } + return undefined; +} + export async function activate(context: vscode.ExtensionContext) { // Output Channel for diagnostics outputChannel = vscode.window.createOutputChannel('Codev'); @@ -245,7 +260,19 @@ export async function activate(context: vscode.ExtensionContext) { if (!roleOrId) { return; } await terminalManager?.openBuilderByRoleOrId(roleOrId, true); }), - vscode.commands.registerCommand('codev.spawnBuilder', () => spawnBuilder()), + vscode.commands.registerCommand('codev.spawnBuilder', (arg: vscode.TreeItem | string | undefined) => + spawnBuilder(extractIssueId(arg))), + vscode.commands.registerCommand('codev.openBacklogIssue', (arg: vscode.TreeItem | undefined) => { + if (arg instanceof BacklogTreeItem) { + void vscode.env.openExternal(vscode.Uri.parse(arg.issueUrl)); + } + }), + vscode.commands.registerCommand('codev.copyBacklogIssueNumber', async (arg: vscode.TreeItem | undefined) => { + if (arg instanceof BacklogTreeItem) { + await vscode.env.clipboard.writeText(`#${arg.issueId}`); + vscode.window.showInformationMessage(`Codev: Copied #${arg.issueId}`); + } + }), vscode.commands.registerCommand('codev.sendMessage', () => sendMessage(connectionManager!)), vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined, options?: { skipConfirmation?: boolean }) => approveGate(connectionManager!, overviewCache, extractBuilderId(arg), options)), diff --git a/packages/vscode/src/views/backlog-tree-item.ts b/packages/vscode/src/views/backlog-tree-item.ts new file mode 100644 index 000000000..7f378c3eb --- /dev/null +++ b/packages/vscode/src/views/backlog-tree-item.ts @@ -0,0 +1,24 @@ +import * as vscode from 'vscode'; + +/** + * TreeItem subclass that carries a backlog issue's id and URL as typed fields. + * + * Why: VSCode passes the tree item itself (not its `command.arguments`) + * to commands invoked from `view/item/context` menus. The backlog + * context-menu commands (codev.spawnBuilder, codev.openBacklogIssue, + * codev.copyBacklogIssueNumber) need to know which issue was + * right-clicked, so BacklogProvider constructs rows with this class and + * the command handlers narrow via `instanceof BacklogTreeItem` to read + * `.issueId` / `.issueUrl` safely. + * + * Used by views/backlog.ts. + */ +export class BacklogTreeItem extends vscode.TreeItem { + constructor( + public readonly issueId: string, + public readonly issueUrl: string, + label: string, + ) { + super(label); + } +} diff --git a/packages/vscode/src/views/backlog.ts b/packages/vscode/src/views/backlog.ts index 57d3051e0..751f08cfd 100644 --- a/packages/vscode/src/views/backlog.ts +++ b/packages/vscode/src/views/backlog.ts @@ -1,6 +1,19 @@ import * as vscode from 'vscode'; +import type { OverviewBacklogItem } from '@cluesmith/codev-types'; import type { OverviewCache } from './overview-data.js'; +import { BacklogTreeItem } from './backlog-tree-item.js'; +/** + * Backlog view: open GitHub issues with no PR yet. Issues assigned to the + * current user (auto-detected via OverviewData.currentUser) sort to the + * top with an `account` icon; the rest keep `issues`. Order within each + * group preserves Tower's order. Mirrors how BuildersProvider sorts + * blocked-first above active within a single view — no separator rows. + * + * Row click starts work: it invokes codev.spawnBuilder with the issue + * number pre-filled (protocol-pick only). Browser / copy actions live in + * the right-click context menu (see package.json view/item/context). + */ export class BacklogProvider implements vscode.TreeDataProvider { private readonly changeEmitter = new vscode.EventEmitter(); readonly onDidChangeTreeData = this.changeEmitter.event; @@ -17,16 +30,25 @@ export class BacklogProvider implements vscode.TreeDataProvider const data = this.cache.getData(); if (!data) { return []; } - return data.backlog.map(item => { + const me = data.currentUser?.toLowerCase(); + const isMine = (item: OverviewBacklogItem) => + !!me && !!item.assignees?.some(a => a.toLowerCase() === me); + + const mine = data.backlog.filter(isMine); + const rest = data.backlog.filter(item => !isMine(item)); + + return [...mine, ...rest].map(item => { + const assigned = mine.includes(item); const author = item.author ? ` @${item.author}` : ''; - const ti = new vscode.TreeItem(`#${item.id} ${item.title}${author}`); + const ti = new BacklogTreeItem(item.id, item.url, `#${item.id} ${item.title}${author}`); ti.tooltip = item.url; ti.contextValue = 'backlog-item'; - ti.iconPath = new vscode.ThemeIcon('issues'); + ti.iconPath = new vscode.ThemeIcon(assigned ? 'account' : 'issues'); + if (assigned) { ti.description = 'assigned to you'; } ti.command = { - command: 'vscode.open', - title: 'Open in Browser', - arguments: [vscode.Uri.parse(item.url)], + command: 'codev.spawnBuilder', + title: 'Spawn Builder', + arguments: [item.id], }; return ti; }); From e2749a636db67ee40bb3df6cd0479318bc43ef39 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 09:23:29 +1000 Subject: [PATCH 45/63] feat(vscode): View Issue backlog action; default click opens issue in right pane MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Read a backlog issue's body + comments inside VSCode instead of leaving for a browser. Backlog row click now opens the issue (was: spawn); spawn moves to a deliberate right-click choice. - Tower: new forge-backed GET /api/issue → fetchIssue (issue-view concept). Stays forge-agnostic and tunnel-safe — the extension never shells out to gh; resolution happens Tower-side like every other data fetch. - core: TowerClient.getIssue(number, workspace) mirroring getOverview. - types: shared IssueView interface (title/body/state/comments), mirrors server-side IssueViewResult; exported from the package index. - vscode: new view-issue.ts — a read-only `codev-issue:` content provider renders the issue as markdown; opens via markdown.showPreviewToSide so it lands in the right editor column, the same placement model builder terminals use (ViewColumn.Two). - Backlog context menu reordered so the click-action is first (Builders-consistency rule): View Issue @1, Spawn Builder @2, Open Issue in Browser @3, Copy Issue Number @4. The three context-only commands are hidden from the command palette. Click = read the issue; right-click = spawn / open in browser / copy. Making spawn an explicit choice also avoids accidental-click spawns that the architect AI wouldn't be aware of. --- .../src/agent-farm/servers/tower-routes.ts | 30 ++++++ packages/core/src/tower-client.ts | 14 ++- packages/types/src/api.ts | 18 ++++ packages/types/src/index.ts | 1 + packages/vscode/package.json | 19 +++- packages/vscode/src/commands/view-issue.ts | 92 +++++++++++++++++++ packages/vscode/src/extension.ts | 7 ++ packages/vscode/src/views/backlog.ts | 4 +- 8 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 packages/vscode/src/commands/view-issue.ts diff --git a/packages/codev/src/agent-farm/servers/tower-routes.ts b/packages/codev/src/agent-farm/servers/tower-routes.ts index 0ceefb576..f84c6b51d 100644 --- a/packages/codev/src/agent-farm/servers/tower-routes.ts +++ b/packages/codev/src/agent-farm/servers/tower-routes.ts @@ -56,6 +56,7 @@ import { stopInstance, } from './tower-instances.js'; import { OverviewCache } from './overview.js'; +import { fetchIssue } from '../../lib/github.js'; import { computeAnalytics } from './analytics.js'; import { getAllTasks, executeTask, getTaskId } from './tower-cron.js'; import { getGlobalDb } from '../db/index.js'; @@ -144,6 +145,7 @@ const ROUTES: Record = { 'GET /api/terminals': (_req, res) => handleTerminalList(res), 'GET /api/status': (_req, res) => handleStatus(res), 'GET /api/overview': (_req, res, url) => handleOverview(res, url), + 'GET /api/issue': (_req, res, url) => handleIssueView(res, url), 'GET /api/analytics': (_req, res, url) => handleAnalytics(res, url), 'POST /api/overview/refresh': (_req, res, _url, ctx) => handleOverviewRefresh(res, ctx), 'GET /api/events': (req, res, _url, ctx) => handleSSEEvents(req, res, ctx), @@ -706,6 +708,34 @@ async function handleOverview(res: http.ServerResponse, url: URL, workspaceOverr res.end(JSON.stringify(data)); } +async function handleIssueView(res: http.ServerResponse, url: URL): Promise { + // Workspace resolution mirrors handleOverview: ?workspace= param, else + // the first known non-builder workspace path. + let workspaceRoot = url.searchParams.get('workspace'); + if (!workspaceRoot) { + const knownPaths = getKnownWorkspacePaths(); + workspaceRoot = knownPaths.find(p => !p.includes('/.builders/')) || null; + } + + const number = url.searchParams.get('number'); + if (!workspaceRoot || !number) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'Missing workspace or number' })); + return; + } + + // Routes through the `issue-view` forge concept (forge-agnostic). + const issue = await fetchIssue(number, { cwd: workspaceRoot }); + if (!issue) { + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: `Issue #${number} not found or forge unavailable` })); + return; + } + + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(issue)); +} + function handleOverviewRefresh(res: http.ServerResponse, ctx?: RouteContext): void { overviewCache.invalidate(); // Bugfix #388: Broadcast SSE event so all connected dashboard clients diff --git a/packages/core/src/tower-client.ts b/packages/core/src/tower-client.ts index c25837cd4..20ccba227 100644 --- a/packages/core/src/tower-client.ts +++ b/packages/core/src/tower-client.ts @@ -7,7 +7,7 @@ * Extracted from packages/codev/src/agent-farm/lib/tower-client.ts */ -import type { DashboardState, OverviewData } from '@cluesmith/codev-types'; +import type { DashboardState, OverviewData, IssueView } from '@cluesmith/codev-types'; import { DEFAULT_TOWER_PORT } from './constants.js'; import { ensureLocalKey } from './auth.js'; @@ -222,6 +222,18 @@ export class TowerClient { return result.ok ? result.data! : null; } + /** + * Fetch a single issue's title/body/state/comments via Tower's + * forge-backed GET /api/issue. Returns null if the issue can't be + * resolved (forge unavailable, bad number) so callers can degrade. + */ + async getIssue(issueNumber: string, workspacePath?: string): Promise { + const params = new URLSearchParams({ number: issueNumber }); + if (workspacePath) { params.set('workspace', workspacePath); } + const result = await this.request(`/api/issue?${params.toString()}`); + return result.ok ? result.data! : null; + } + /** * Invalidate Tower's in-memory overview cache and broadcast an * `overview-changed` SSE event. Subscribed clients (VSCode sidebar, diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index e20b844d7..099c61597 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -145,6 +145,24 @@ export interface OverviewData { errors?: { prs?: string; issues?: string }; } +// --- Issue view (GET /api/issue) --- + +/** + * A single issue as returned by the `issue-view` forge concept and + * surfaced verbatim by Tower's GET /api/issue. Mirrors the server-side + * IssueViewResult (packages/codev/src/lib/forge-contracts.ts). + */ +export interface IssueView { + title: string; + body: string; + state: string; + comments: Array<{ + body: string; + createdAt: string; + author: { login: string }; + }>; +} + // --- Team (GET /workspace/:path/api/team) --- export interface ReviewBlockingEntry { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0cc5e500a..ef2b6a5ba 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -23,6 +23,7 @@ export { type OverviewBacklogItem, type OverviewRecentlyClosed, type OverviewData, + type IssueView, type TeamMemberGitHubData, type ReviewBlockingEntry, type TeamApiMember, diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 8c480a54f..8f074a75d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -133,6 +133,10 @@ "command": "codev.viewPlanFile", "title": "Codev: View Plan File" }, + { + "command": "codev.viewBacklogIssue", + "title": "Codev: View Issue" + }, { "command": "codev.openBacklogIssue", "title": "Codev: Open Issue in Browser" @@ -158,6 +162,10 @@ "command": "codev.openBuilderById", "when": "false" }, + { + "command": "codev.viewBacklogIssue", + "when": "false" + }, { "command": "codev.openBacklogIssue", "when": "false" @@ -226,19 +234,24 @@ "group": "3_dev@3" }, { - "command": "codev.spawnBuilder", + "command": "codev.viewBacklogIssue", "when": "view == codev.backlog && viewItem == backlog-item", "group": "1_primary@1" }, { - "command": "codev.openBacklogIssue", + "command": "codev.spawnBuilder", "when": "view == codev.backlog && viewItem == backlog-item", "group": "1_primary@2" }, { - "command": "codev.copyBacklogIssueNumber", + "command": "codev.openBacklogIssue", "when": "view == codev.backlog && viewItem == backlog-item", "group": "1_primary@3" + }, + { + "command": "codev.copyBacklogIssueNumber", + "when": "view == codev.backlog && viewItem == backlog-item", + "group": "1_primary@4" } ], "view/title": [ diff --git a/packages/vscode/src/commands/view-issue.ts b/packages/vscode/src/commands/view-issue.ts new file mode 100644 index 000000000..ccd5b287e --- /dev/null +++ b/packages/vscode/src/commands/view-issue.ts @@ -0,0 +1,92 @@ +/** + * Codev: View Issue — read the body + comments of a backlog issue inside + * VSCode instead of opening a browser. + * + * Right-click a backlog row → "View Issue". Fetches via Tower's + * forge-backed GET /api/issue (so it stays forge-agnostic and + * tunnel-safe — the extension never shells out to `gh`), renders the + * issue as markdown behind a read-only `codev-issue:` document, and + * opens VSCode's built-in markdown preview. + * + * A TextDocumentContentProvider scheme is read-only by construction, so + * there's no editable scratch buffer left behind (unlike opening an + * untitled document). + */ + +import * as vscode from 'vscode'; +import type { IssueView } from '@cluesmith/codev-types'; +import type { ConnectionManager } from '../connection-manager.js'; + +const SCHEME = 'codev-issue'; + +class IssueContentProvider implements vscode.TextDocumentContentProvider { + private readonly contents = new Map(); + + /** Key is the issue id; value is the rendered markdown. */ + set(issueId: string, markdown: string): void { + this.contents.set(issueId, markdown); + } + + provideTextDocumentContent(uri: vscode.Uri): string { + // URI form: codev-issue:.md → authority/path is `.md` + const issueId = uri.path.replace(/\.md$/, ''); + return this.contents.get(issueId) ?? `# Issue #${issueId}\n\n_Content unavailable._`; + } +} + +const provider = new IssueContentProvider(); + +export function activateIssueView(context: vscode.ExtensionContext): void { + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider(SCHEME, provider), + ); +} + +function renderIssue(issueId: string, issue: IssueView): string { + const lines: string[] = [ + `# #${issueId} ${issue.title}`, + '', + `**State:** ${issue.state}`, + '', + issue.body?.trim() ? issue.body : '_No description._', + ]; + + if (issue.comments.length > 0) { + lines.push('', '---', '', `## Comments (${issue.comments.length})`, ''); + for (const c of issue.comments) { + lines.push(`### @${c.author.login} — ${c.createdAt}`, '', c.body, ''); + } + } + + return lines.join('\n'); +} + +export async function viewBacklogIssue( + connectionManager: ConnectionManager, + issueId: string | undefined, +): Promise { + if (!issueId) { return; } + + const client = connectionManager.getClient(); + const workspacePath = connectionManager.getWorkspacePath(); + if (!client || !workspacePath || connectionManager.getState() !== 'connected') { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + + const issue = await client.getIssue(issueId, workspacePath); + if (!issue) { + vscode.window.showWarningMessage( + `Codev: Could not load issue #${issueId} (forge unavailable?)`, + ); + return; + } + + provider.set(issueId, renderIssue(issueId, issue)); + const uri = vscode.Uri.parse(`${SCHEME}:${issueId}.md`); + // Render as a read-only markdown preview in the side (right) editor + // column — same placement model as builder terminals (which target + // ViewColumn.Two). `showPreviewToSide` opens in ViewColumn.Beside, + // which from a sidebar click lands in the right pane. + await vscode.commands.executeCommand('markdown.showPreviewToSide', uri); +} diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 4d9dabb52..76560714a 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -12,6 +12,7 @@ import { stopWorktreeDev } from './commands/stop-worktree-dev.js'; import { openWorktreeFolder } from './commands/open-worktree-folder.js'; import { runWorktreeSetup } from './commands/run-worktree-setup.js'; import { viewPlanFile } from './commands/view-artifact.js'; +import { activateIssueView, viewBacklogIssue } from './commands/view-issue.js'; import { connectTunnel, disconnectTunnel } from './commands/tunnel.js'; import { listCronTasks } from './commands/cron.js'; import { addReviewComment } from './commands/review.js'; @@ -273,6 +274,8 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showInformationMessage(`Codev: Copied #${arg.issueId}`); } }), + vscode.commands.registerCommand('codev.viewBacklogIssue', (arg: vscode.TreeItem | string | undefined) => + viewBacklogIssue(connectionManager!, extractIssueId(arg))), vscode.commands.registerCommand('codev.sendMessage', () => sendMessage(connectionManager!)), vscode.commands.registerCommand('codev.approveGate', (arg: vscode.TreeItem | string | undefined, options?: { skipConfirmation?: boolean }) => approveGate(connectionManager!, overviewCache, extractBuilderId(arg), options)), @@ -297,6 +300,10 @@ export async function activate(context: vscode.ExtensionContext) { vscode.commands.registerCommand('codev.addReviewComment', () => addReviewComment()), ); + // Read-only `codev-issue:` content provider backing the "View Issue" + // backlog action — renders issue body + comments as markdown preview. + activateIssueView(context); + // Review comment decorations activateReviewDecorations(context); diff --git a/packages/vscode/src/views/backlog.ts b/packages/vscode/src/views/backlog.ts index 751f08cfd..217620a34 100644 --- a/packages/vscode/src/views/backlog.ts +++ b/packages/vscode/src/views/backlog.ts @@ -46,8 +46,8 @@ export class BacklogProvider implements vscode.TreeDataProvider ti.iconPath = new vscode.ThemeIcon(assigned ? 'account' : 'issues'); if (assigned) { ti.description = 'assigned to you'; } ti.command = { - command: 'codev.spawnBuilder', - title: 'Spawn Builder', + command: 'codev.viewBacklogIssue', + title: 'View Issue', arguments: [item.id], }; return ti; From 1511ed744fbd47648ded2f62be704d0fcdab4048 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 09:33:29 +1000 Subject: [PATCH 46/63] feat(vscode): show live item counts in Builders/Pull Requests/Backlog titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch the three active-work list views from registerTreeDataProvider to createTreeView so their titles can carry a live count — "Builders (3)", "Pull Requests (2)", "Backlog (17)". Count is recomputed from the overview cache in the existing combined onDidChange handler (alongside status-bar + terminal-prune — one subscription, named functions), so it updates as builders spawn/finish, PRs open/merge, issues enter/leave. No-data state (disconnected/loading) falls back to the plain base name — no misleading "(0)"; a genuinely empty list still shows "(0)" once data has loaded. Recently Closed, Team, Workspace, and Status are unchanged (still registerTreeDataProvider). Context menus/commands are unaffected since they key off the view id, not the registration method; the three TreeView handles are disposed via context.subscriptions. --- packages/vscode/src/extension.ts | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 76560714a..7d14ecde5 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -118,6 +118,22 @@ export async function activate(context: vscode.ExtensionContext) { : `$(server) Codev: ${builderCount} builders`; }; + // List views show their item count in the title: "Builders (3)". + // createTreeView (not registerTreeDataProvider) is required to get a + // settable .title. When there's no data yet (disconnected/loading) the + // title falls back to the plain base name — no misleading "(0)". + let buildersView: vscode.TreeView | undefined; + let pullRequestsView: vscode.TreeView | undefined; + let backlogView: vscode.TreeView | undefined; + const updateListViewTitles = () => { + const data = overviewCache.getData(); + const withCount = (base: string, n: number | undefined) => + typeof n === 'number' ? `${base} (${n})` : base; + if (buildersView) { buildersView.title = withCount('Builders', data?.builders.length); } + if (pullRequestsView) { pullRequestsView.title = withCount('Pull Requests', data?.pendingPRs.length); } + if (backlogView) { backlogView.title = withCount('Backlog', data?.backlog.length); } + }; + // Close builder/dev terminal tabs when their builder disappears from Tower // state. Covers cleanup triggered from the VSCode "Cleanup Builder" command, // `afx cleanup` on the CLI, or any other removal path — otherwise Tower @@ -161,13 +177,19 @@ export async function activate(context: vscode.ExtensionContext) { overviewCache.onDidChange(() => { updateStatusBarCounts(); pruneClosedBuilderTerminals(); + updateListViewTitles(); }); + // Active-work list views use createTreeView so their title can carry a + // live item count; the rest stay on registerTreeDataProvider. + buildersView = vscode.window.createTreeView('codev.builders', { treeDataProvider: new BuildersProvider(overviewCache) }); + pullRequestsView = vscode.window.createTreeView('codev.pullRequests', { treeDataProvider: new PullRequestsProvider(overviewCache) }); + backlogView = vscode.window.createTreeView('codev.backlog', { treeDataProvider: new BacklogProvider(overviewCache) }); context.subscriptions.push( + buildersView, + pullRequestsView, + backlogView, vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager)), - vscode.window.registerTreeDataProvider('codev.builders', new BuildersProvider(overviewCache)), - vscode.window.registerTreeDataProvider('codev.pullRequests', new PullRequestsProvider(overviewCache)), - vscode.window.registerTreeDataProvider('codev.backlog', new BacklogProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.recentlyClosed', new RecentlyClosedProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.team', new TeamProvider(connectionManager)), vscode.window.registerTreeDataProvider('codev.status', new StatusProvider(connectionManager)), From ead62cd3eab921987ffca8c2a105c16022e5195e Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 11:13:42 +1000 Subject: [PATCH 47/63] feat(vscode): periodic overview refresh while the sidebar is visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VSCode had no timer-based overview refresh — it only refetched on SSE events, connect, or the manual button. On an idle workspace (no porch activity) an externally-merged PR or new issue stayed invisible indefinitely. The web dashboard already polls (useOverview), so this brings VSCode to parity. New setting codev.overviewRefreshSeconds (default 60, 0 = disabled). A single named controller in extension.ts refreshes on that cadence only while a Codev list view is visible (gated on the existing createTreeView .visible/onDidChangeVisibility handles), pauses when the sidebar is hidden, and does an immediate refresh on becoming visible again. Live-reconfigurable via onDidChangeConfiguration; ticks self-skip while disconnected so a transient blip doesn't empty the views. No Tower change — the shared server-side 30s cache throttles gh cost across all windows, and OverviewCache.refresh() is already last-write-wins so periodic + event-driven refreshes don't flicker. --- packages/vscode/package.json | 6 +++++ packages/vscode/src/extension.ts | 41 ++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 8f074a75d..fedba569d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -388,6 +388,12 @@ "type": "boolean", "default": true, "description": "Show a VSCode notification toast when a builder reaches a human-approval gate (plan-approval, code-review, etc.)" + }, + "codev.overviewRefreshSeconds": { + "type": "number", + "default": 60, + "minimum": 0, + "markdownDescription": "Auto-refresh the Builders / Pull Requests / Backlog / Recently Closed views every N seconds while the Codev sidebar is visible. `0` disables periodic refresh (event-only, the previous behavior). A shared Tower-side 30s cache throttles GitHub calls across all windows." } } } diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 7d14ecde5..1f6f164b6 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -195,6 +195,47 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.registerTreeDataProvider('codev.status', new StatusProvider(connectionManager)), ); + // Periodic overview refresh. VSCode has no timer-based refresh (event-only), + // so an idle workspace never sees externally-merged PRs / new issues. Mirror + // the dashboard's poll idiom: refresh on a cadence while the Codev sidebar is + // visible, paused when it isn't. The shared Tower-side 30s cache throttles gh + // cost across windows; refresh() is last-write-wins so periodic + event-driven + // refreshes coexist without flicker. + const setupPeriodicOverviewRefresh = () => { + let timer: ReturnType | undefined; + const readIntervalSeconds = (): number => { + const s = vscode.workspace.getConfiguration('codev').get('overviewRefreshSeconds', 60); + return typeof s === 'number' && Number.isFinite(s) && s > 0 ? s : 0; + }; + const anyVisible = (): boolean => + !!buildersView?.visible || !!pullRequestsView?.visible || !!backlogView?.visible; + const stop = () => { + if (timer) { clearInterval(timer); timer = undefined; } + }; + const reconcile = () => { + const seconds = readIntervalSeconds(); + if (seconds === 0 || !anyVisible()) { stop(); return; } + if (!timer) { + timer = setInterval(() => { + if (connectionManager?.getState() === 'connected') { void overviewCache.refresh(); } + }, seconds * 1000); + void overviewCache.refresh(); // resume → immediate refresh + } + }; + + context.subscriptions.push( + buildersView!.onDidChangeVisibility(reconcile), + pullRequestsView!.onDidChangeVisibility(reconcile), + backlogView!.onDidChangeVisibility(reconcile), + vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('codev.overviewRefreshSeconds')) { stop(); reconcile(); } + }), + { dispose: stop }, + ); + reconcile(); + }; + setupPeriodicOverviewRefresh(); + // Refresh overview on connect + set team visibility connectionManager.onStateChange(async (state) => { if (state === 'connected') { From 3c23e16fc04f23ac97b529c0fcf2905ee05079e9 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 11:27:24 +1000 Subject: [PATCH 48/63] feat(vscode): Recently Closed gets refresh button + live count MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recently Closed had no title-bar refresh action (only Builders/Pull Requests/Backlog did — an omission, not intentional) and no item count. Add the codev.refreshOverview view/title entry for codev.recentlyClosed, switch it from registerTreeDataProvider to createTreeView so its title carries a live "Recently Closed (N)" count via the existing updateListViewTitles, and include its visibility handle in the periodic-refresh controller's anyVisible() + onDidChangeVisibility wiring so the timer treats it like the other list views. Brings it to full parity with the other active-work views. --- packages/vscode/package.json | 5 +++++ packages/vscode/src/extension.ts | 13 +++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index fedba569d..81b4206f3 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -270,6 +270,11 @@ "when": "view == codev.backlog", "group": "navigation" }, + { + "command": "codev.refreshOverview", + "when": "view == codev.recentlyClosed", + "group": "navigation" + }, { "command": "codev.reconnect", "when": "view == codev.status", diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 1f6f164b6..2ffa5c564 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -125,6 +125,7 @@ export async function activate(context: vscode.ExtensionContext) { let buildersView: vscode.TreeView | undefined; let pullRequestsView: vscode.TreeView | undefined; let backlogView: vscode.TreeView | undefined; + let recentlyClosedView: vscode.TreeView | undefined; const updateListViewTitles = () => { const data = overviewCache.getData(); const withCount = (base: string, n: number | undefined) => @@ -132,6 +133,7 @@ export async function activate(context: vscode.ExtensionContext) { if (buildersView) { buildersView.title = withCount('Builders', data?.builders.length); } if (pullRequestsView) { pullRequestsView.title = withCount('Pull Requests', data?.pendingPRs.length); } if (backlogView) { backlogView.title = withCount('Backlog', data?.backlog.length); } + if (recentlyClosedView) { recentlyClosedView.title = withCount('Recently Closed', data?.recentlyClosed.length); } }; // Close builder/dev terminal tabs when their builder disappears from Tower @@ -180,17 +182,18 @@ export async function activate(context: vscode.ExtensionContext) { updateListViewTitles(); }); - // Active-work list views use createTreeView so their title can carry a - // live item count; the rest stay on registerTreeDataProvider. + // List views use createTreeView so their title can carry a live item + // count; the rest stay on registerTreeDataProvider. buildersView = vscode.window.createTreeView('codev.builders', { treeDataProvider: new BuildersProvider(overviewCache) }); pullRequestsView = vscode.window.createTreeView('codev.pullRequests', { treeDataProvider: new PullRequestsProvider(overviewCache) }); backlogView = vscode.window.createTreeView('codev.backlog', { treeDataProvider: new BacklogProvider(overviewCache) }); + recentlyClosedView = vscode.window.createTreeView('codev.recentlyClosed', { treeDataProvider: new RecentlyClosedProvider(overviewCache) }); context.subscriptions.push( buildersView, pullRequestsView, backlogView, + recentlyClosedView, vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager)), - vscode.window.registerTreeDataProvider('codev.recentlyClosed', new RecentlyClosedProvider(overviewCache)), vscode.window.registerTreeDataProvider('codev.team', new TeamProvider(connectionManager)), vscode.window.registerTreeDataProvider('codev.status', new StatusProvider(connectionManager)), ); @@ -208,7 +211,8 @@ export async function activate(context: vscode.ExtensionContext) { return typeof s === 'number' && Number.isFinite(s) && s > 0 ? s : 0; }; const anyVisible = (): boolean => - !!buildersView?.visible || !!pullRequestsView?.visible || !!backlogView?.visible; + !!buildersView?.visible || !!pullRequestsView?.visible + || !!backlogView?.visible || !!recentlyClosedView?.visible; const stop = () => { if (timer) { clearInterval(timer); timer = undefined; } }; @@ -227,6 +231,7 @@ export async function activate(context: vscode.ExtensionContext) { buildersView!.onDidChangeVisibility(reconcile), pullRequestsView!.onDidChangeVisibility(reconcile), backlogView!.onDidChangeVisibility(reconcile), + recentlyClosedView!.onDidChangeVisibility(reconcile), vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration('codev.overviewRefreshSeconds')) { stop(); reconcile(); } }), From 0dc96520329e02564f2af20d95fd5bb3bc2c770c Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sat, 16 May 2026 11:39:12 +1000 Subject: [PATCH 49/63] fix(forge): recently-closed/merged dropped genuinely-recent items (bare-date query) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fetchRecentlyClosed / fetchRecentMergedPRs truncated the 24h-ago timestamp to a bare date (.split('T')[0]) and the GitHub concept queries `closed:>$CODEV_SINCE_DATE` / `merged:>$CODEV_SINCE_DATE`. GitHub's `>` against a bare YYYY-MM-DD excludes the *entire* sinceDate day, so the effective window collapsed to "since the most recent UTC midnight" (0–24h wide, ~0 just after 00:00Z). Result: issues/PRs closed earlier the same UTC day were silently missing from Recently Closed even though well within 24h (reproduced: an issue closed 3.5h ago returned []). Send a full ISO-8601 timestamp (seconds precision, retains the Z UTC designator) instead of a bare date. GitHub search supports second-precision datetime qualifiers; `>` against a precise timestamp is exact (verified end-to-end: now returns exactly the items closed within 24h). Milliseconds are stripped to match GitHub's documented qualifier format rather than rely on undocumented fractional-second leniency — important because a rejected qualifier fails silently (null → empty view), the very symptom being fixed. The precise client-side 24h filter is unchanged and remains authoritative. Verified GitHub-only: gitlab/gitea scripts don't pass the date arg (JS-filter only); linear uses GraphQL `gte` (inclusive, never had the day-exclusion bug) — the shared-caller change is a no-op for gitlab/ gitea and a precision improvement for linear, no regression. --- packages/codev/src/lib/github.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/codev/src/lib/github.ts b/packages/codev/src/lib/github.ts index edf9ab7d6..2b539e30a 100644 --- a/packages/codev/src/lib/github.ts +++ b/packages/codev/src/lib/github.ts @@ -143,7 +143,15 @@ export async function fetchRecentlyClosed( cwd?: string, forgeConfig?: ForgeConfig | null, ): Promise { - const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + // Full ISO-8601 timestamp, NOT a bare date. The concept query is + // `closed:>$CODEV_SINCE_DATE`; GitHub search supports second-precision + // datetime qualifiers and `>` against a precise timestamp is exact + // (verified). A bare `YYYY-MM-DD` was the bug: GitHub's `>` excludes the + // entire sinceDate day, collapsing the 24h window to "since UTC midnight" + // (≈0h just after 00:00Z) and silently hiding genuinely-recent closures. + // Seconds precision (no millis) matches GitHub's documented format. + const since = new Date(Date.now() - 24 * 60 * 60 * 1000) + .toISOString().replace(/\.\d{3}Z$/, 'Z'); const result = await executeForgeCommand('recently-closed', { CODEV_SINCE_DATE: since, }, { @@ -168,7 +176,11 @@ export async function fetchRecentMergedPRs( cwd?: string, forgeConfig?: ForgeConfig | null, ): Promise { - const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + // Full ISO-8601 timestamp (seconds precision), not a bare date — same + // GitHub bare-date `>` day-exclusion bug as fetchRecentlyClosed. + // `merged:>$CODEV_SINCE_DATE` against a precise timestamp is exact. + const since = new Date(Date.now() - 24 * 60 * 60 * 1000) + .toISOString().replace(/\.\d{3}Z$/, 'Z'); const result = await executeForgeCommand('recently-merged', { CODEV_SINCE_DATE: since, }, { From f0f2fe7d4a81f58eeefe0774cc6c99d95ff7416e Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sun, 17 May 2026 09:50:51 +1000 Subject: [PATCH 50/63] fix(vscode): quote gate-toast issue title; persist seen-set across reloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes to the gate-pending toast: 1. The issue title is now wrapped in quotes. A title that reads like an error (e.g. "agent/reset returns 504 GATEWAY_TIMEOUT…") was appended bare after an em-dash and looked like the toast was reporting a failure rather than identifying the blocked issue. 2. The (builderId::gate) seen-set is persisted to workspaceState instead of being in-memory closure state. It previously reset on every window reload / extension reactivation / Tower reconnect, so a builder still blocked on the same gate re-toasted once per reactivation. Hydrated on activation, re-saved only when the set changes; prune semantics unchanged so a genuine re-block still toasts. --- .../vscode/src/notifications/gate-toast.ts | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/vscode/src/notifications/gate-toast.ts b/packages/vscode/src/notifications/gate-toast.ts index 277076bb6..48e94f62b 100644 --- a/packages/vscode/src/notifications/gate-toast.ts +++ b/packages/vscode/src/notifications/gate-toast.ts @@ -30,8 +30,15 @@ export function activateGateToasts( context: vscode.ExtensionContext, cache: OverviewCache, ): void { - // Track (builderId, gateName) pairs we've already toasted for. - const seen = new Set(); + // Track (builderId, gateName) pairs we've already toasted for. Persisted to + // workspaceState so a builder that stays blocked on the same gate doesn't + // re-toast on every window reload / extension reactivation / Tower reconnect + // (the set is otherwise closure state that resets each activation). Pruning + // still removes keys once a builder leaves the blocked set, so a genuine + // re-block later re-toasts. + const SEEN_KEY = 'codev.gateToasts.seen'; + const seen = new Set(context.workspaceState.get(SEEN_KEY, [])); + const persist = () => context.workspaceState.update(SEEN_KEY, [...seen]); const onChange = () => { const enabled = vscode.workspace @@ -47,6 +54,7 @@ export function activateGateToasts( } const currentBlocked = new Set(); + let changed = false; for (const b of data.builders) { if (!b.blocked || !b.blockedGate) { continue; @@ -59,6 +67,7 @@ export function activateGateToasts( currentBlocked.add(key); if (!seen.has(key)) { seen.add(key); + changed = true; showGateToast(b.id, b.blockedGate, b.blocked, b.issueId, b.issueTitle); } } @@ -67,8 +76,13 @@ export function activateGateToasts( for (const key of [...seen]) { if (!currentBlocked.has(key)) { seen.delete(key); + changed = true; } } + + if (changed) { + persist(); + } }; context.subscriptions.push(cache.onDidChange(onChange)); @@ -100,7 +114,10 @@ function showGateToast( issueTitle?: string | null, ): void { const label = issueId ? `#${issueId}` : builderId; - const titleSuffix = issueTitle ? ` — ${truncate(issueTitle, 50)}` : ''; + // Quote the issue title so a title that reads like an error + // (e.g. "agent/reset returns 504 GATEWAY_TIMEOUT…") is visibly a subject, + // not a failure the toast is reporting. + const titleSuffix = issueTitle ? ` — “${truncate(issueTitle, 50)}”` : ''; const message = `Codev: ${label} blocked on ${gateLabel}${titleSuffix}`; const action = GATE_ACTIONS[gateName] ?? { label: 'Review', command: 'codev.openBuilderById' }; From 90a87604ded1a80bc963c51fd219273c67fff267 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sun, 17 May 2026 10:50:51 +1000 Subject: [PATCH 51/63] fix(pir): align review phase with max_iterations:1 single-pass design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The review prompt promised an iterate-until-APPROVE CMAP loop that protocol.json (max_iterations: 1) structurally cannot deliver — a builder hit this: codex returned a substantive REQUEST_CHANGES, the builder fixed + rebutted it, and porch advanced to the pr gate at iteration 1 with no independent re-review and no escalation. - review.md: rewrote Goal/step 5.3/step 6/step 7/step 8 — CMAP is one advisory pass; a REQUEST_CHANGES is fixed-or-rebutted (+ regression test) and *escalated* to the human at the pr gate via a conditional notification, since PIR will not re-review it. - protocol.md + builder-prompt.md: state single-pass / no loop / escalate. - review.md step 9: detect a PR already merged on GitHub by the human (don't blind re-merge — record only). - implement.md + review.md: scope discipline — porch checks are narrow structural gates, not a full-suite proof; pre-existing unrelated failures are out of scope, not to be quarantined. - consult-types/pr-review.md: "CMAP-3" -> "CMAP-2" (was also orphaned). - protocol.md + CLAUDE.md + AGENTS.md: document the CMAP-2 invariant — porch model precedence is config > protocol, so a SPIR-tuned global porch.consultation.models silently inflates PIR. codev-skeleton mirror kept byte-identical; CLAUDE.md == AGENTS.md. --- AGENTS.md | 2 +- CLAUDE.md | 2 +- .../protocols/pir/builder-prompt.md | 2 +- .../protocols/pir/consult-types/pr-review.md | 2 +- .../protocols/pir/prompts/implement.md | 2 + .../protocols/pir/prompts/review.md | 51 +++++++++++-------- codev-skeleton/protocols/pir/protocol.md | 10 ++-- codev/protocols/pir/builder-prompt.md | 2 +- .../protocols/pir/consult-types/pr-review.md | 2 +- codev/protocols/pir/prompts/implement.md | 2 + codev/protocols/pir/prompts/review.md | 51 +++++++++++-------- codev/protocols/pir/protocol.md | 10 ++-- 12 files changed, 86 insertions(+), 52 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 04d32b489..ce50376b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). CMAP at the PR is a **single advisory pass** (`max_iterations: 1`) — no iterate-until-APPROVE loop; a `REQUEST_CHANGES` is escalated to the human at the `pr` gate, not auto-re-reviewed. The CMAP-2 footprint is a design invariant: porch's model precedence is *config > protocol*, so a project-wide `porch.consultation.models` (e.g. a SPIR-tuned 3-model list) silently inflates PIR — leave it unset or scope it per-protocol to preserve the BUGFIX/AIR-parity cost. See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/CLAUDE.md b/CLAUDE.md index 04d32b489..ce50376b0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -186,7 +186,7 @@ Pick PIR when ONE or BOTH of the following apply to a GitHub-issue-driven change - User-journey changes that need a full-flow exercise - Performance-sensitive changes that need profiling on the running app -**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). See `codev/protocols/pir/protocol.md`. +**PIR uses GitHub Issues as source of truth.** Three phases: Plan (gated by `plan-approval`) → Implement (gated by `dev-approval`) → Review (PR + CMAP-2 at PR, then gated by `pr` for merge synchronization — matching SPIR's pr-gate pattern but with no post-merge verify phase). Plan and review artifacts live in `codev/plans/` and `codev/reviews/` on the builder branch, ship to main with the merge. Review file is shaped identically to SPIR's (Summary + Architecture Updates + Lessons Learned + supporting sections) so `codev/reviews/` stays semantically consistent across protocols. Lighter than SPIR (no spec phase — the issue body is the implicit spec; consult footprint matches BUGFIX/AIR's "one consult at PR" pattern). Stronger than BUGFIX/AIR (two human gates pre-PR — the human reviews the running worktree at the `dev-approval` gate, not the PR diff post-creation). CMAP at the PR is a **single advisory pass** (`max_iterations: 1`) — no iterate-until-APPROVE loop; a `REQUEST_CHANGES` is escalated to the human at the `pr` gate, not auto-re-reviewed. The CMAP-2 footprint is a design invariant: porch's model precedence is *config > protocol*, so a project-wide `porch.consultation.models` (e.g. a SPIR-tuned 3-model list) silently inflates PIR — leave it unset or scope it per-protocol to preserve the BUGFIX/AIR-parity cost. See `codev/protocols/pir/protocol.md`. ### Use SPIR for (new features): - Creating a **new feature from scratch** (no existing spec to amend) diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 0adeae8f0..81a926977 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) -3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block (a **single advisory pass** — `max_iterations: 1`, no iterate-until-APPROVE loop; address or rebut any `REQUEST_CHANGES`, add a regression test if it's a real defect, and escalate it in the architect notification since PIR will not re-review it), notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev-skeleton/protocols/pir/consult-types/pr-review.md b/codev-skeleton/protocols/pir/consult-types/pr-review.md index 9d723b02a..0eb5fb1d0 100644 --- a/codev-skeleton/protocols/pir/consult-types/pr-review.md +++ b/codev-skeleton/protocols/pir/consult-types/pr-review.md @@ -2,7 +2,7 @@ ## Context -You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. +You are performing the CMAP-2 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is a single advisory pass (`max_iterations: 1`) — your verdict is surfaced to the human at the `pr` gate, who is the sole remaining reviewer; it is not auto-re-reviewed. ## Focus Areas diff --git a/codev-skeleton/protocols/pir/prompts/implement.md b/codev-skeleton/protocols/pir/prompts/implement.md index 383f391d4..0d8ff9e5b 100644 --- a/codev-skeleton/protocols/pir/prompts/implement.md +++ b/codev-skeleton/protocols/pir/prompts/implement.md @@ -82,6 +82,8 @@ npm test # or project equivalent Both MUST pass before signaling phase complete. If a test is flaky (intermittent failure unrelated to your changes), skip it with annotation — you'll document each skipped test in the review file in the next phase. +**Flaky ≠ pre-existing unrelated failure.** If the suite surfaces a *deterministic* failure your diff did not cause (e.g., a stale test broken by another team's earlier refactor, or a type error in an unrelated package), that is **out of scope** — do not fix it, skip it, or quarantine it to force a green. Note it for the review file's Lessons Learned and proceed. Porch's gate `checks` are narrow structural assertions, not a full-suite proof; making an unrelated red go green is scope creep, not diligence. + ### 5. Push Your Branch ```bash diff --git a/codev-skeleton/protocols/pir/prompts/review.md b/codev-skeleton/protocols/pir/prompts/review.md index 9d1e8a69d..a5153902b 100644 --- a/codev-skeleton/protocols/pir/prompts/review.md +++ b/codev-skeleton/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) automatically** via the verify block. After CMAP approves, the `pr` gate fires; you notify the architect and wait at the gate while the human merges on GitHub. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) once** via the verify block. CMAP is a *single advisory pass* (`max_iterations: 1`): its verdicts are surfaced to the human at the `pr` gate, **not** an iterate-until-APPROVE loop. After the single pass the `pr` gate fires regardless of verdict; you notify the architect (leading with any REQUEST_CHANGES) and wait at the gate while the human merges on GitHub. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -140,36 +140,40 @@ porch done {{project_id}} Porch will: 1. Run the `pr_exists` / `review_has_arch_updates` / `review_has_lessons_updates` checks. 2. **Execute CMAP-2 (Gemini + Codex) automatically** via the protocol's `verify` block. Outputs land in `codev/projects/{{project_id}}-/{{project_id}}-gemini.txt` and `-codex.txt`. -3. Evaluate verdicts: - - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). - - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. +3. CMAP runs **exactly once** (`max_iterations: 1`). Whatever the verdicts, porch records them in `status.yaml` and advances to the `pr` gate — there is **no automated re-review pass and no "stays in the review phase" loop**. `APPROVE` and `REQUEST_CHANGES` differ only in what you must surface to the human (steps 6–7), not in whether the gate fires. The output of `porch done` surfaces the verdicts. > **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `dev-approval` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. -### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) +### 6. Handle a REQUEST_CHANGES Verdict (single-pass — no automated re-review) -If `porch done` reports any reviewer requested changes, run `porch next {{project_id}}` — it returns `status: tasks` with the reviewer feedback baked into the task description. Then: +PIR's CMAP is one advisory pass (`max_iterations: 1`). If a reviewer returns `REQUEST_CHANGES`, porch does **not** loop or re-run CMAP — it records the verdict and proceeds to the `pr` gate. There is no iter-2. The correctness backstop for a CMAP-flagged issue is therefore **(a)** your fix + a regression test and **(b)** the human's `pr`-gate review — *not* an independent model re-review. Treat that as load-bearing: a substantive finding you "address and rebut" gets no second AI opinion. -1. Read the specific issues from the task output (and from `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for full context). -2. Fix them in code. -3. Run build + tests. -4. Commit + push the updates (the PR updates automatically — no new `gh pr create`). -5. Run `porch done {{project_id}}` again. Porch re-runs CMAP-2 against the updated diff. +For any `REQUEST_CHANGES`: -Loop until porch reports all reviewers APPROVE. +1. Read the finding in full (`codev/projects/{{project_id}}-*/{{project_id}}-.txt`). +2. **Assess it honestly:** + - **Real defect** (correctness / cancellation / security / data-loss): fix it in code, add a regression test that fails without the fix, commit + push (the PR updates automatically — no new `gh pr create`). Then document the finding, your fix, and the pinning test in the review file's **"Things to Look At During PR Review"** section. + - **False positive / out of scope**: write a brief rebuttal in that same section explaining why no change is warranted. +3. Do **not** re-run `porch done` expecting another CMAP — `max_iterations: 1` means it will not re-review. Proceed to step 7. -### 7. Notify the Architect (after porch approves CMAP) +Whether you fixed it or rebutted it, a `REQUEST_CHANGES` that PIR will never re-check **must be escalated to the human at the `pr` gate** (step 7) — they are the only remaining reviewer of that decision. -After `porch done` reports all reviewers APPROVE + checks pass, porch fires the **`pr` gate** (pending). Read the verdicts from porch state and notify: +### 7. Notify the Architect (after the single CMAP pass — gate is now pending) + +After the one CMAP pass + structural checks, porch fires the **`pr` gate** (pending) **regardless of the verdicts**. Read the verdicts from porch state and notify — and if any verdict is `REQUEST_CHANGES`, **lead with it and state the disposition**, because PIR will not re-review it and the human at the `pr` gate is the only remaining check: ```bash GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." +if echo "$GEMINI_VERDICT $CODEX_VERDICT" | grep -qi request_changes; then + afx send architect "⚠️ PR # (PIR #{{issue.number}}): CMAP returned REQUEST_CHANGES (gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT). Disposition: + regression test | rebutted, see review 'Things to Look At'>. PIR is single-pass — this was NOT independently re-reviewed; please verify the fix/rebuttal at the pr gate before approving. Full verdicts in codev/projects/{{project_id}}-*/." +else + afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP all clear: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." +fi ``` -This is the only notification you send at the gate. +This is the only notification you send at the gate. A `REQUEST_CHANGES` must never reach the human as an undifferentiated status line — it is the one verdict PIR's single-pass design cannot re-check. ### 8. Wait at the `pr` Gate @@ -182,7 +186,7 @@ The human will: Porch will then fire the gate-approved wake-up to you. -If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. +If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` — this runs a fresh **single** CMAP pass on the updated diff and re-fires the `pr` gate (handle any new verdict per steps 6–7). This human-driven iteration is the only way CMAP re-runs in PIR; it is not automatic. If they close the PR without merging, `gh pr close ` and stop. ### 9. After `pr` Gate Approval — Verify, Merge, Record @@ -194,15 +198,21 @@ porch next {{project_id}} The response must include `gate_status: approved` for the `pr` gate. If it doesn't, do NOT proceed — wait for the genuine wake-up. If it does, you're authorized. -Look up the PR number (recorded at step 4a) and merge: +Look up the PR number (recorded at step 4a). **Check whether the human already merged it before merging** — approving the `pr` gate and merging via the GitHub UI is a common combined action; never blind-merge: ```bash # Read PR number from porch state PR=$(yq '.pr // .history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) +STATE=$(gh pr view "$PR" --json state --jq .state) -gh pr merge "$PR" --merge +if [ "$STATE" = "MERGED" ]; then + # Human merged via the GitHub UI — do NOT re-merge. Detect and record only. + porch done {{project_id}} --merged "$PR" +else + gh pr merge "$PR" --merge + porch done {{project_id}} --merged "$PR" +fi -porch done {{project_id}} --merged "$PR" porch next {{project_id}} # confirms protocol is complete (next: null) ``` @@ -231,6 +241,7 @@ Together with the `--pr` record from step 4a and the `--merged` record from step - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) - **Don't run `consult` commands yourself** — porch handles consultations via the `verify` block. Manually invoking `consult` causes CMAP to run twice. +- **Don't fix, skip, or quarantine pre-existing failures unrelated to your change.** Porch's `checks` for this phase are narrow *structural* gates (`pr_exists`, review-section presence) — a green gate does **not** certify the wider build/test suite. If the broader suite surfaces failures your diff did not cause, they are out of scope: note them in the review's Lessons Learned / Things to Look At and proceed. Touching another team's tests to make an unrelated red go green is scope creep, not diligence. ## Handling Problems diff --git a/codev-skeleton/protocols/pir/protocol.md b/codev-skeleton/protocols/pir/protocol.md index 621a21c27..537a1de9c 100644 --- a/codev-skeleton/protocols/pir/protocol.md +++ b/codev-skeleton/protocols/pir/protocol.md @@ -101,8 +101,8 @@ The builder: 2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). 3. Commits the review file (and arch / lessons updates if any) and pushes 4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. -5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. -6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` +5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) as a **single advisory pass** (`max_iterations: 1`); CMAP outputs land in `codev/projects/-*/`. There is no iterate-until-APPROVE loop: whatever the verdicts, porch records them and advances to the `pr` gate. A `REQUEST_CHANGES` is not auto-re-reviewed — the builder addresses or rebuts it, adds a regression test if it's a real defect, and escalates it in the architect notification so the human verifies it at the `pr` gate. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. +6. The `pr` gate fires (pending) regardless of verdict. Builder notifies the architect once — leading with any `REQUEST_CHANGES` and its disposition (since PIR will not re-review it) rather than burying it in a flat status line. 7. Builder waits at the `pr` gate. The human reviews the PR on GitHub, then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). Porch wakes the builder. 8. Builder verifies the gate is genuinely approved via `porch next` (defensive — typed prose can't trigger this branch, only real porch state does), then runs `gh pr merge --merge`, records via `porch done --merged `, and sends the cleanup-ready notification. Protocol complete (`next: null`). @@ -152,10 +152,14 @@ Without `worktree.devCommand`, `afx dev` won't work and the `dev-approval` gate - **plan**: human-only review. No AI consultation. - **implement**: no AI consult — the human at the `dev-approval` gate is the sole reviewer of the running code. -- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. +- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened, as a **single advisory pass** (`max_iterations: 1`). Same pattern as BUGFIX / AIR's PR-creation consult. + +CMAP at the PR is a single pass — there is **no iterate-until-APPROVE loop**. A `REQUEST_CHANGES` does not block or re-trigger CMAP; the builder addresses or rebuts it and escalates it to the human at the `pr` gate, who is the sole remaining reviewer of any resulting fix (CMAP does not re-check it). Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `dev-approval`), not AI-consult density. +**The 2-model footprint is a design invariant.** porch's model precedence is *config > protocol* (`porch.consultation.models` in `.codev/config.json` overrides a protocol's declared `verify.models`). A project-wide setting tuned for SPIR (gemini + codex + claude) will silently inflate PIR to a 3-model pass, breaking the BUGFIX/AIR-parity cost story. To keep PIR at CMAP-2, leave `porch.consultation.models` unset or scope it per-protocol. + To disable consultation entirely, say "without multi-agent consultation" when starting work. ## Signals diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 0adeae8f0..81a926977 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -33,7 +33,7 @@ Read and internalize the protocol before starting any work. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review 2. **implement** (gated by `dev-approval`) — write code + tests, run build/tests, push branch; await the human's review of the *running worktree* (no file artifact in this phase — dev-approval summary is prose-in-pane) -3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block, notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** +3. **review** (gated by `pr`) — write `codev/reviews/{{artifact_name}}.md` (retrospective with Architecture Updates and Lessons Learned sections), open PR with the review as body, record the PR with porch, run CMAP via porch's verify block (a **single advisory pass** — `max_iterations: 1`, no iterate-until-APPROVE loop; address or rebut any `REQUEST_CHANGES`, add a regression test if it's a real defect, and escalate it in the architect notification since PIR will not re-review it), notify architect, and wait at the `pr` gate. After the human approves the gate (porch wakes you with "Gate pr approved"), run `gh pr merge --merge` and record the merge with `porch done --merged `. **Merge is gated by porch state — never by typed prose in your pane.** {{#if issue}} ## Issue #{{issue.number}} diff --git a/codev/protocols/pir/consult-types/pr-review.md b/codev/protocols/pir/consult-types/pr-review.md index 9d723b02a..0eb5fb1d0 100644 --- a/codev/protocols/pir/consult-types/pr-review.md +++ b/codev/protocols/pir/consult-types/pr-review.md @@ -2,7 +2,7 @@ ## Context -You are performing the CMAP-3 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is the final AI review before the architect merges. +You are performing the CMAP-2 review of a PIR protocol PR. The builder has implemented an approved plan, the human has approved the `dev-approval` gate (meaning a human has run the code locally and tested it), and the PR has been opened. This is a single advisory pass (`max_iterations: 1`) — your verdict is surfaced to the human at the `pr` gate, who is the sole remaining reviewer; it is not auto-re-reviewed. ## Focus Areas diff --git a/codev/protocols/pir/prompts/implement.md b/codev/protocols/pir/prompts/implement.md index 383f391d4..0d8ff9e5b 100644 --- a/codev/protocols/pir/prompts/implement.md +++ b/codev/protocols/pir/prompts/implement.md @@ -82,6 +82,8 @@ npm test # or project equivalent Both MUST pass before signaling phase complete. If a test is flaky (intermittent failure unrelated to your changes), skip it with annotation — you'll document each skipped test in the review file in the next phase. +**Flaky ≠ pre-existing unrelated failure.** If the suite surfaces a *deterministic* failure your diff did not cause (e.g., a stale test broken by another team's earlier refactor, or a type error in an unrelated package), that is **out of scope** — do not fix it, skip it, or quarantine it to force a green. Note it for the review file's Lessons Learned and proceed. Porch's gate `checks` are narrow structural assertions, not a full-suite proof; making an unrelated red go green is scope creep, not diligence. + ### 5. Push Your Branch ```bash diff --git a/codev/protocols/pir/prompts/review.md b/codev/protocols/pir/prompts/review.md index 9d1e8a69d..a5153902b 100644 --- a/codev/protocols/pir/prompts/review.md +++ b/codev/protocols/pir/prompts/review.md @@ -4,7 +4,7 @@ You are executing the **REVIEW** phase of the PIR protocol. ## Your Goal -Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) automatically** via the verify block. After CMAP approves, the `pr` gate fires; you notify the architect and wait at the gate while the human merges on GitHub. +Write a retrospective at `codev/reviews/{{artifact_name}}.md` including **Summary**, **Architecture Updates**, and **Lessons Learned Updates** sections. Push, open a PR using the review file as the PR body, record the PR with porch, then signal completion — **porch runs CMAP-2 (Gemini + Codex) once** via the verify block. CMAP is a *single advisory pass* (`max_iterations: 1`): its verdicts are surfaced to the human at the `pr` gate, **not** an iterate-until-APPROVE loop. After the single pass the `pr` gate fires regardless of verdict; you notify the architect (leading with any REQUEST_CHANGES) and wait at the gate while the human merges on GitHub. The retrospective ships with the merged PR — it's durable team knowledge, searchable in `codev/reviews/` on `main`. @@ -140,36 +140,40 @@ porch done {{project_id}} Porch will: 1. Run the `pr_exists` / `review_has_arch_updates` / `review_has_lessons_updates` checks. 2. **Execute CMAP-2 (Gemini + Codex) automatically** via the protocol's `verify` block. Outputs land in `codev/projects/{{project_id}}-/{{project_id}}-gemini.txt` and `-codex.txt`. -3. Evaluate verdicts: - - **All APPROVE + checks pass** → review phase complete (the protocol is done from porch's perspective). - - **Any REQUEST_CHANGES** → porch records the feedback in `status.yaml` and stays in the review phase. The output of `porch done` will surface the verdicts. +3. CMAP runs **exactly once** (`max_iterations: 1`). Whatever the verdicts, porch records them in `status.yaml` and advances to the `pr` gate — there is **no automated re-review pass and no "stays in the review phase" loop**. `APPROVE` and `REQUEST_CHANGES` differ only in what you must surface to the human (steps 6–7), not in whether the gate fires. The output of `porch done` surfaces the verdicts. > **Why CMAP-2, not CMAP-3?** PIR's design parallels BUGFIX/AIR's consult footprint. The human already approved the *running* implementation at the `dev-approval` gate; CMAP at PR is a pre-merge hygiene + code-quality pass, not a functional review. -### 6. Handle Reviewer Feedback (if porch reports REQUEST_CHANGES) +### 6. Handle a REQUEST_CHANGES Verdict (single-pass — no automated re-review) -If `porch done` reports any reviewer requested changes, run `porch next {{project_id}}` — it returns `status: tasks` with the reviewer feedback baked into the task description. Then: +PIR's CMAP is one advisory pass (`max_iterations: 1`). If a reviewer returns `REQUEST_CHANGES`, porch does **not** loop or re-run CMAP — it records the verdict and proceeds to the `pr` gate. There is no iter-2. The correctness backstop for a CMAP-flagged issue is therefore **(a)** your fix + a regression test and **(b)** the human's `pr`-gate review — *not* an independent model re-review. Treat that as load-bearing: a substantive finding you "address and rebut" gets no second AI opinion. -1. Read the specific issues from the task output (and from `codev/projects/{{project_id}}-*/{{project_id}}-.txt` for full context). -2. Fix them in code. -3. Run build + tests. -4. Commit + push the updates (the PR updates automatically — no new `gh pr create`). -5. Run `porch done {{project_id}}` again. Porch re-runs CMAP-2 against the updated diff. +For any `REQUEST_CHANGES`: -Loop until porch reports all reviewers APPROVE. +1. Read the finding in full (`codev/projects/{{project_id}}-*/{{project_id}}-.txt`). +2. **Assess it honestly:** + - **Real defect** (correctness / cancellation / security / data-loss): fix it in code, add a regression test that fails without the fix, commit + push (the PR updates automatically — no new `gh pr create`). Then document the finding, your fix, and the pinning test in the review file's **"Things to Look At During PR Review"** section. + - **False positive / out of scope**: write a brief rebuttal in that same section explaining why no change is warranted. +3. Do **not** re-run `porch done` expecting another CMAP — `max_iterations: 1` means it will not re-review. Proceed to step 7. -### 7. Notify the Architect (after porch approves CMAP) +Whether you fixed it or rebutted it, a `REQUEST_CHANGES` that PIR will never re-check **must be escalated to the human at the `pr` gate** (step 7) — they are the only remaining reviewer of that decision. -After `porch done` reports all reviewers APPROVE + checks pass, porch fires the **`pr` gate** (pending). Read the verdicts from porch state and notify: +### 7. Notify the Architect (after the single CMAP pass — gate is now pending) + +After the one CMAP pass + structural checks, porch fires the **`pr` gate** (pending) **regardless of the verdicts**. Read the verdicts from porch state and notify — and if any verdict is `REQUEST_CHANGES`, **lead with it and state the disposition**, because PIR will not re-review it and the human at the `pr` gate is the only remaining check: ```bash GEMINI_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-gemini.txt" || echo UNKNOWN) CODEX_VERDICT=$(grep -m1 -i '^\(approve\|request_changes\|comment\)' "codev/projects/{{project_id}}-"*/"{{project_id}}-codex.txt" || echo UNKNOWN) -afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." +if echo "$GEMINI_VERDICT $CODEX_VERDICT" | grep -qi request_changes; then + afx send architect "⚠️ PR # (PIR #{{issue.number}}): CMAP returned REQUEST_CHANGES (gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT). Disposition: + regression test | rebutted, see review 'Things to Look At'>. PIR is single-pass — this was NOT independently re-reviewed; please verify the fix/rebuttal at the pr gate before approving. Full verdicts in codev/projects/{{project_id}}-*/." +else + afx send architect "PR # ready for review (PIR #{{issue.number}}). CMAP all clear: gemini=$GEMINI_VERDICT, codex=$CODEX_VERDICT. Awaiting human merge + pr gate approval. Full verdicts in codev/projects/{{project_id}}-*/." +fi ``` -This is the only notification you send at the gate. +This is the only notification you send at the gate. A `REQUEST_CHANGES` must never reach the human as an undifferentiated status line — it is the one verdict PIR's single-pass design cannot re-check. ### 8. Wait at the `pr` Gate @@ -182,7 +186,7 @@ The human will: Porch will then fire the gate-approved wake-up to you. -If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` (loops back to step 6). If they close the PR without merging, `gh pr close ` and stop. +If the human requests more changes instead of approving, push fixes and re-run `porch done {{project_id}}` — this runs a fresh **single** CMAP pass on the updated diff and re-fires the `pr` gate (handle any new verdict per steps 6–7). This human-driven iteration is the only way CMAP re-runs in PIR; it is not automatic. If they close the PR without merging, `gh pr close ` and stop. ### 9. After `pr` Gate Approval — Verify, Merge, Record @@ -194,15 +198,21 @@ porch next {{project_id}} The response must include `gate_status: approved` for the `pr` gate. If it doesn't, do NOT proceed — wait for the genuine wake-up. If it does, you're authorized. -Look up the PR number (recorded at step 4a) and merge: +Look up the PR number (recorded at step 4a). **Check whether the human already merged it before merging** — approving the `pr` gate and merging via the GitHub UI is a common combined action; never blind-merge: ```bash # Read PR number from porch state PR=$(yq '.pr // .history[] | select(.event == "pr_recorded") | .pr' codev/projects/{{project_id}}-*/status.yaml | head -1) +STATE=$(gh pr view "$PR" --json state --jq .state) -gh pr merge "$PR" --merge +if [ "$STATE" = "MERGED" ]; then + # Human merged via the GitHub UI — do NOT re-merge. Detect and record only. + porch done {{project_id}} --merged "$PR" +else + gh pr merge "$PR" --merge + porch done {{project_id}} --merged "$PR" +fi -porch done {{project_id}} --merged "$PR" porch next {{project_id}} # confirms protocol is complete (next: null) ``` @@ -231,6 +241,7 @@ Together with the `--pr` record from step 4a and the `--merged` record from step - Don't push to main — only merge via PR - Don't skip the Architecture Updates / Lessons Learned sections — porch checks enforce their presence (the section must exist; explaining "no changes needed" in one line is fine) - **Don't run `consult` commands yourself** — porch handles consultations via the `verify` block. Manually invoking `consult` causes CMAP to run twice. +- **Don't fix, skip, or quarantine pre-existing failures unrelated to your change.** Porch's `checks` for this phase are narrow *structural* gates (`pr_exists`, review-section presence) — a green gate does **not** certify the wider build/test suite. If the broader suite surfaces failures your diff did not cause, they are out of scope: note them in the review's Lessons Learned / Things to Look At and proceed. Touching another team's tests to make an unrelated red go green is scope creep, not diligence. ## Handling Problems diff --git a/codev/protocols/pir/protocol.md b/codev/protocols/pir/protocol.md index 621a21c27..537a1de9c 100644 --- a/codev/protocols/pir/protocol.md +++ b/codev/protocols/pir/protocol.md @@ -101,8 +101,8 @@ The builder: 2. Updates `codev/resources/arch.md` and/or `codev/resources/lessons-learned.md` if real changes need recording. If not, the review file's sections state "no changes needed" with a one-line explanation (the porch `checks` block enforces section presence, not content). 3. Commits the review file (and arch / lessons updates if any) and pushes 4. Opens a PR with `gh pr create`; PR body is the review file content + `Fixes #`. Records the PR with `porch done --pr --branch `. -5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl); CMAP outputs land in `codev/projects/-*/`. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. -6. The `pr` gate fires (pending). Builder notifies the architect once: `afx send architect "PR # ready for review (PIR #), CMAP: gemini=, codex=. Awaiting human merge + pr gate approval."` +5. Runs `porch done ` — porch's `verify` block runs CMAP-2 (Gemini + Codex, type=impl) as a **single advisory pass** (`max_iterations: 1`); CMAP outputs land in `codev/projects/-*/`. There is no iterate-until-APPROVE loop: whatever the verdicts, porch records them and advances to the `pr` gate. A `REQUEST_CHANGES` is not auto-re-reviewed — the builder addresses or rebuts it, adds a regression test if it's a real defect, and escalates it in the architect notification so the human verifies it at the `pr` gate. Outcomes are not auto-appended to the PR body; reviewers with the worktree read them from the projects dir. +6. The `pr` gate fires (pending) regardless of verdict. Builder notifies the architect once — leading with any `REQUEST_CHANGES` and its disposition (since PIR will not re-review it) rather than burying it in a flat status line. 7. Builder waits at the `pr` gate. The human reviews the PR on GitHub, then approves the `pr` gate (Cmd+K G or `porch approve pr --a-human-explicitly-approved-this`). Porch wakes the builder. 8. Builder verifies the gate is genuinely approved via `porch next` (defensive — typed prose can't trigger this branch, only real porch state does), then runs `gh pr merge --merge`, records via `porch done --merged `, and sends the cleanup-ready notification. Protocol complete (`next: null`). @@ -152,10 +152,14 @@ Without `worktree.devCommand`, `afx dev` won't work and the `dev-approval` gate - **plan**: human-only review. No AI consultation. - **implement**: no AI consult — the human at the `dev-approval` gate is the sole reviewer of the running code. -- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened; appended to PR body. Same pattern as BUGFIX / AIR's PR-creation consult. +- **review**: CMAP-2 (Gemini + Codex, type=impl) after the PR is opened, as a **single advisory pass** (`max_iterations: 1`). Same pattern as BUGFIX / AIR's PR-creation consult. + +CMAP at the PR is a single pass — there is **no iterate-until-APPROVE loop**. A `REQUEST_CHANGES` does not block or re-trigger CMAP; the builder addresses or rebuts it and escalates it to the human at the `pr` gate, who is the sole remaining reviewer of any resulting fix (CMAP does not re-check it). Net: PIR runs **two model calls per protocol run**, matching its BUGFIX/AIR peers — its distinguishing features are the two human gates (`plan-approval`, `dev-approval`), not AI-consult density. +**The 2-model footprint is a design invariant.** porch's model precedence is *config > protocol* (`porch.consultation.models` in `.codev/config.json` overrides a protocol's declared `verify.models`). A project-wide setting tuned for SPIR (gemini + codex + claude) will silently inflate PIR to a 3-model pass, breaking the BUGFIX/AIR-parity cost story. To keep PIR at CMAP-2, leave `porch.consultation.models` unset or scope it per-protocol. + To disable consultation entirely, say "without multi-agent consultation" when starting work. ## Signals From f0d3e4d14c150b7dbc678bd85da362ef4ea78778 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sun, 17 May 2026 20:33:44 +1000 Subject: [PATCH 52/63] feat(vscode): Codev-managed dev server for the current workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a context-aware "Start/Stop Dev Server" to the sidebar Workspace view and an `afx dev main` CLI target. The dev target is resolved from the folder the VSCode window is rooted at — the main checkout -> `main`, a .builders// worktree -> that builder — so the existing single-slot swap machinery now covers main<->worktree, closing the silent EADDRINUSE / wrong-target-review footgun when main dev is launched through Codev. A manually-run `pnpm dev` stays unmanaged by deliberate policy. - dev.ts: reserved `main` target (local synthetic; builder-lookup deliberately untouched so `main` never leaks into send/cleanup/status) - dev-shared.ts: resolveWorkspaceDevTarget + shared startDevForTarget/ stopDevForTarget core (one implementation, target-resolution differs) - run-workspace-dev.ts: context-aware front-end (path-sniffs the open folder against the fixed .builders// layout) - run-worktree-dev.ts: refactored to delegate to the shared core (behavior unchanged) - terminal-manager.ts: onDidChangeDevTerminals event so the conditional Stop row stays correct across start/stop/swap/cleanup - workspace.ts: generic "Start/Stop Dev Server" rows, target-aware tooltip, Stop shown only while this workspace's dev runs - extension.ts/package.json: command + menu wiring - CLAUDE.md/AGENTS.md: docs (synced) --- AGENTS.md | 16 +- CLAUDE.md | 16 +- packages/codev/src/agent-farm/commands/dev.ts | 14 +- packages/vscode/package.json | 18 ++ packages/vscode/src/commands/dev-shared.ts | 173 ++++++++++++++++++ .../vscode/src/commands/run-workspace-dev.ts | 42 +++++ .../vscode/src/commands/run-worktree-dev.ts | 115 ++---------- packages/vscode/src/extension.ts | 7 +- packages/vscode/src/terminal-manager.ts | 9 + packages/vscode/src/views/workspace.ts | 45 ++++- 10 files changed, 341 insertions(+), 114 deletions(-) create mode 100644 packages/vscode/src/commands/dev-shared.ts create mode 100644 packages/vscode/src/commands/run-workspace-dev.ts diff --git a/AGENTS.md b/AGENTS.md index ce50376b0..00216f50e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -330,7 +330,7 @@ When configured, each builder worktree (`.builders//`) becomes runnable — "worktree": { "symlinks": ["..."], // glob patterns of files to symlink from root into each new worktree "postSpawn": ["..."], // shell commands run inside each new worktree after createWorktree - "devCommand": "..." // consumed by `afx dev ` + "devCommand": "..." // consumed by `afx dev ` } } ``` @@ -345,10 +345,13 @@ When configured, each builder worktree (`.builders//`) becomes runnable — ```bash afx dev # start the dev server in 's worktree -afx dev --stop # stop the currently running dev PTY +afx dev main # start the dev server in the MAIN workspace (Codev-managed) +afx dev --stop # stop the currently running dev PTY (builder or main) ``` -Only one dev PTY runs at a time (by design — see "URLs are load-bearing" below). Running `afx dev` while another builder's dev is up prompts for swap. Same-builder requests print the existing terminal URL and exit. +Only one dev PTY runs at a time (by design — see "URLs are load-bearing" below), across **{main + all builders}**. `main` is a reserved target: it runs `worktree.devCommand` in the main checkout as a Codev-managed, swappable PTY, symmetric with builders. Starting any target while another is up prompts for swap (`afx dev ` while `main` runs, or vice-versa); same-target requests print the existing terminal URL and exit. Like builder dev, main dev is a **non-persistent** PTY — a Tower restart (`pnpm -w run local-install`, crash) kills it; re-run to restart. + +**Launch main dev via `afx dev main`, not a bare `pnpm dev`.** A manually-run `pnpm dev` at the repo root is invisible to Codev (the deliberate "never kill what it didn't spawn" policy) — start a builder dev while it holds the ports and the builder dev silently fails to bind, or worse serves main's code under the worktree URL. `afx dev main` makes it a managed PTY that swap-detection can cleanly stop first. This only helps if you use it *consistently*; a hand-started `pnpm dev` stays unmanaged. ### VSCode @@ -361,11 +364,16 @@ The same actions are available via right-click on any builder row in the Codev s - **Codev: Run Dev Server** — reads `worktree.devCommand` from `.codev/config.json`, asks Tower to spawn a dev PTY in the builder's worktree, and opens it as a VSCode terminal tab named `Codev: (dev)`. If another builder's dev is already running, you get a modal asking whether to swap. - **Codev: Stop Dev Server** — kills the running dev PTY and closes its tab. +The Codev sidebar's **Workspace** view also carries a dev server for *whatever folder this VSCode window is rooted at* (it is not "main"-specific): + +- **Start Dev Server** — runs `worktree.devCommand` for the current workspace. Target is resolved from the open folder: the main checkout → `main`; a `.builders//` worktree opened as its own window (e.g. via *Open Worktree as Workspace*) → that builder. Same single-slot swap model as builder dev (prompts if another dev is running). The row tooltip names the resolved target. +- **Stop Dev Server** — stops this workspace's dev server; the row appears only while it is running. Scoped to the resolved target — it does not touch other devs. + The three commands are also available from the command palette (Cmd+Shift+P). No default keybindings; bind via `keybindings.json` if you use them often. ### URLs are load-bearing -The dev PTY uses **the same ports and URLs as main** intentionally. OAuth callbacks, CORS allowlists, cookie scoping, CSP `connect-src`, webhook URLs are all keyed off origin — running the worktree on a different port would break them. Consequence: stop main's `pnpm dev` before `afx dev`. If you don't, the spawned dev fails at bind time with its own `EADDRINUSE`. +The dev PTY uses **the same ports and URLs as main** intentionally. OAuth callbacks, CORS allowlists, cookie scoping, CSP `connect-src`, webhook URLs are all keyed off origin — running the worktree on a different port would break them. Consequence: stop main's `pnpm dev` before `afx dev`. If you don't, the spawned dev fails at bind time with its own `EADDRINUSE`. Prefer `afx dev main` (or the Workspace view's *Start Dev Server* row) over a hand-run `pnpm dev` so Codev owns the PTY and swap-detection can stop it for you automatically. ### Cleanup semantics diff --git a/CLAUDE.md b/CLAUDE.md index ce50376b0..00216f50e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -330,7 +330,7 @@ When configured, each builder worktree (`.builders//`) becomes runnable — "worktree": { "symlinks": ["..."], // glob patterns of files to symlink from root into each new worktree "postSpawn": ["..."], // shell commands run inside each new worktree after createWorktree - "devCommand": "..." // consumed by `afx dev ` + "devCommand": "..." // consumed by `afx dev ` } } ``` @@ -345,10 +345,13 @@ When configured, each builder worktree (`.builders//`) becomes runnable — ```bash afx dev # start the dev server in 's worktree -afx dev --stop # stop the currently running dev PTY +afx dev main # start the dev server in the MAIN workspace (Codev-managed) +afx dev --stop # stop the currently running dev PTY (builder or main) ``` -Only one dev PTY runs at a time (by design — see "URLs are load-bearing" below). Running `afx dev` while another builder's dev is up prompts for swap. Same-builder requests print the existing terminal URL and exit. +Only one dev PTY runs at a time (by design — see "URLs are load-bearing" below), across **{main + all builders}**. `main` is a reserved target: it runs `worktree.devCommand` in the main checkout as a Codev-managed, swappable PTY, symmetric with builders. Starting any target while another is up prompts for swap (`afx dev ` while `main` runs, or vice-versa); same-target requests print the existing terminal URL and exit. Like builder dev, main dev is a **non-persistent** PTY — a Tower restart (`pnpm -w run local-install`, crash) kills it; re-run to restart. + +**Launch main dev via `afx dev main`, not a bare `pnpm dev`.** A manually-run `pnpm dev` at the repo root is invisible to Codev (the deliberate "never kill what it didn't spawn" policy) — start a builder dev while it holds the ports and the builder dev silently fails to bind, or worse serves main's code under the worktree URL. `afx dev main` makes it a managed PTY that swap-detection can cleanly stop first. This only helps if you use it *consistently*; a hand-started `pnpm dev` stays unmanaged. ### VSCode @@ -361,11 +364,16 @@ The same actions are available via right-click on any builder row in the Codev s - **Codev: Run Dev Server** — reads `worktree.devCommand` from `.codev/config.json`, asks Tower to spawn a dev PTY in the builder's worktree, and opens it as a VSCode terminal tab named `Codev: (dev)`. If another builder's dev is already running, you get a modal asking whether to swap. - **Codev: Stop Dev Server** — kills the running dev PTY and closes its tab. +The Codev sidebar's **Workspace** view also carries a dev server for *whatever folder this VSCode window is rooted at* (it is not "main"-specific): + +- **Start Dev Server** — runs `worktree.devCommand` for the current workspace. Target is resolved from the open folder: the main checkout → `main`; a `.builders//` worktree opened as its own window (e.g. via *Open Worktree as Workspace*) → that builder. Same single-slot swap model as builder dev (prompts if another dev is running). The row tooltip names the resolved target. +- **Stop Dev Server** — stops this workspace's dev server; the row appears only while it is running. Scoped to the resolved target — it does not touch other devs. + The three commands are also available from the command palette (Cmd+Shift+P). No default keybindings; bind via `keybindings.json` if you use them often. ### URLs are load-bearing -The dev PTY uses **the same ports and URLs as main** intentionally. OAuth callbacks, CORS allowlists, cookie scoping, CSP `connect-src`, webhook URLs are all keyed off origin — running the worktree on a different port would break them. Consequence: stop main's `pnpm dev` before `afx dev`. If you don't, the spawned dev fails at bind time with its own `EADDRINUSE`. +The dev PTY uses **the same ports and URLs as main** intentionally. OAuth callbacks, CORS allowlists, cookie scoping, CSP `connect-src`, webhook URLs are all keyed off origin — running the worktree on a different port would break them. Consequence: stop main's `pnpm dev` before `afx dev`. If you don't, the spawned dev fails at bind time with its own `EADDRINUSE`. Prefer `afx dev main` (or the Workspace view's *Start Dev Server* row) over a hand-run `pnpm dev` so Codev owns the PTY and swap-detection can stop it for you automatically. ### Cleanup semantics diff --git a/packages/codev/src/agent-farm/commands/dev.ts b/packages/codev/src/agent-farm/commands/dev.ts index deb0e1442..db85c7c5d 100644 --- a/packages/codev/src/agent-farm/commands/dev.ts +++ b/packages/codev/src/agent-farm/commands/dev.ts @@ -47,10 +47,19 @@ export async function dev(options: DevOptions): Promise { } if (!options.builderId) { - throw new Error('Usage: afx dev (or --stop)'); + throw new Error('Usage: afx dev (or --stop)'); } - const builder = findBuilderById(options.builderId); + const config = getConfig(); + + // Reserved target: `main` runs the dev server in the main workspace (the + // default checkout), making it a Codev-managed, swappable PTY exactly like + // a builder worktree. Resolved locally — deliberately NOT via + // findBuilderById — so `main` never leaks into afx send/cleanup/status. + const isMain = options.builderId.toLowerCase() === 'main'; + const builder: { id: string; worktree?: string } | null = isMain + ? { id: 'main', worktree: config.workspaceRoot } + : findBuilderById(options.builderId); if (!builder) { throw new Error(`No builder found matching "${options.builderId}". Try \`afx status\`.`); } @@ -58,7 +67,6 @@ export async function dev(options: DevOptions): Promise { throw new Error(`Builder ${builder.id} has no worktree path on record — cannot start dev.`); } - const config = getConfig(); const { devCommand } = getWorktreeConfig(config.workspaceRoot); if (!devCommand) { throw new Error( diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 81b4206f3..24d70a140 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -117,6 +117,14 @@ "command": "codev.stopWorktreeDev", "title": "Codev: Stop Dev Server" }, + { + "command": "codev.runWorkspaceDev", + "title": "Codev: Start Dev Server (this workspace)" + }, + { + "command": "codev.stopWorkspaceDev", + "title": "Codev: Stop Dev Server (this workspace)" + }, { "command": "codev.openBuilderById", "title": "Codev: Open Builder Terminal" @@ -233,6 +241,16 @@ "when": "view == codev.builders && viewItem =~ /^(builder|blocked-builder)-/", "group": "3_dev@3" }, + { + "command": "codev.runWorkspaceDev", + "when": "view == codev.workspace && viewItem == workspace-dev-start", + "group": "1_primary@1" + }, + { + "command": "codev.stopWorkspaceDev", + "when": "view == codev.workspace && viewItem == workspace-dev-stop", + "group": "1_primary@1" + }, { "command": "codev.viewBacklogIssue", "when": "view == codev.backlog && viewItem == backlog-item", diff --git a/packages/vscode/src/commands/dev-shared.ts b/packages/vscode/src/commands/dev-shared.ts new file mode 100644 index 000000000..35d4dca08 --- /dev/null +++ b/packages/vscode/src/commands/dev-shared.ts @@ -0,0 +1,173 @@ +/** + * Shared core for Codev dev-server commands. There is exactly one underlying + * action — "spawn a Tower dev PTY for a {id, cwd} target and open its tab" — + * with two front-ends that differ only in how they resolve the target: + * + * - run-worktree-dev.ts : target picked from Tower's builder overview + * (right-click a Builders row / palette quick-pick). + * - run-workspace-dev.ts: target = whatever folder *this VSCode window* is + * rooted at (main checkout → `main`; a `.builders//` worktree → that + * builder), via resolveWorkspaceDevTarget(). + * + * Swap-sequencing constants mirror the CLI's `afx dev` + * (packages/codev/src/agent-farm/commands/dev.ts). + */ + +import * as vscode from 'vscode'; +import { readFile } from 'node:fs/promises'; +import * as path from 'node:path'; +import type { ConnectionManager } from '../connection-manager.js'; +import type { TerminalManager } from '../terminal-manager.js'; + +export const KILL_WAIT_TIMEOUT_MS = 7000; +export const KILL_POLL_INTERVAL_MS = 200; +export const SWAP_GRACE_MS = 250; + +/** A resolved dev target: which id to run under, where, and how to label it. */ +export interface DevTarget { + /** roleId + swap key + `Dev: ` label suffix (e.g. `main`, `pir-1467`). */ + id: string; + /** Working directory the dev command runs in. */ + cwd: string; + /** Human-facing name for toasts / the terminal tab (e.g. `Workspace`). */ + name: string; +} + +/** + * Resolve the dev target from the folder *this VSCode window* is rooted at. + * + * Path-sniffing against the fixed `/.builders//` layout that + * `afx spawn` creates (the same invariant findProjectRoot / openWorktreeWindow + * already rely on) — deterministic, offline, no Tower round-trip: + * - parent dir is `.builders` → builder worktree, id = basename + * - otherwise → the main checkout, id = `main` + */ +export function resolveWorkspaceDevTarget(workspacePath: string): DevTarget { + if (path.basename(path.dirname(workspacePath)) === '.builders') { + const id = path.basename(workspacePath); + return { id, cwd: workspacePath, name: id }; + } + return { id: 'main', cwd: workspacePath, name: 'Workspace' }; +} + +/** Read `worktree.devCommand` from `.codev/config.json`; null if unset/unreadable. */ +export async function readDevCommand(workspacePath: string): Promise { + const configPath = path.join(workspacePath, '.codev', 'config.json'); + try { + const raw = await readFile(configPath, 'utf-8'); + const parsed = JSON.parse(raw) as { worktree?: { devCommand?: unknown } }; + const cmd = parsed.worktree?.devCommand; + return typeof cmd === 'string' && cmd.length > 0 ? cmd : null; + } catch { + return null; + } +} + +/** Poll `listTerminals` until the killed terminal disappears (or timeout). */ +export async function waitForTerminalGone( + client: NonNullable>, + terminalId: string, +): Promise { + const deadline = Date.now() + KILL_WAIT_TIMEOUT_MS; + while (Date.now() < deadline) { + const terminals = await client.listTerminals(); + if (!terminals.some(t => t.id === terminalId)) { return; } + await new Promise((r) => setTimeout(r, KILL_POLL_INTERVAL_MS)); + } + throw new Error(`Dev terminal ${terminalId} did not exit within ${KILL_WAIT_TIMEOUT_MS}ms`); +} + +/** + * Start (or focus) the dev PTY for a resolved target. Single-dev-slot model: + * one dev at a time across {main + all builders} because they all bind main's + * ports — a different running target prompts for swap. Identical for every + * front-end; only target resolution differs. + */ +export async function startDevForTarget( + connectionManager: ConnectionManager, + terminalManager: TerminalManager, + target: DevTarget, +): Promise { + const client = connectionManager.getClient(); + const workspacePath = connectionManager.getWorkspacePath(); + if (!client || !workspacePath || connectionManager.getState() !== 'connected') { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + + const devCommand = await readDevCommand(workspacePath); + if (!devCommand) { + vscode.window.showErrorMessage( + 'Codev: Configure worktree.devCommand in .codev/config.json to use this action. ' + + 'See "Runnable Worktrees" in CLAUDE.md for stack-specific recipes.', + ); + return; + } + + // Swap detection (per-VSCode-instance; cross-instance is a #690 non-goal). + const existing = terminalManager.listDevTerminals(); + const same = existing.find(d => d.builderId === target.id); + if (same) { + vscode.window.showInformationMessage(`Codev: Dev server is already running for ${target.name}`); + await terminalManager.openDevTerminal(same.terminalId, target.id, target.name, true); + return; + } + if (existing.length > 0) { + const other = existing[0]!; + const choice = await vscode.window.showWarningMessage( + `Stop dev for ${other.builderId} and start ${target.name}?`, + { modal: true }, + 'Yes', 'No', + ); + if (choice !== 'Yes') { return; } + await client.killTerminal(other.terminalId); + terminalManager.closeDevTerminal(other.builderId); + try { + await waitForTerminalGone(client, other.terminalId); + } catch (err) { + vscode.window.showErrorMessage(`Codev: ${(err as Error).message}`); + return; + } + await new Promise((r) => setTimeout(r, SWAP_GRACE_MS)); + } + + const terminal = await client.createTerminal({ + command: '/bin/sh', + args: ['-lc', devCommand], + cwd: target.cwd, + workspacePath, + type: 'dev', + roleId: target.id, + label: `Dev: ${target.id}`, + persistent: false, + }); + if (!terminal) { + vscode.window.showErrorMessage(`Codev: Failed to spawn dev terminal for ${target.name}`); + return; + } + + await terminalManager.openDevTerminal(terminal.id, target.id, target.name, true); + vscode.window.showInformationMessage(`Codev: Dev server started for ${target.name}`); +} + +/** Stop the dev PTY for a single target id (scoped — does not touch others). */ +export async function stopDevForTarget( + connectionManager: ConnectionManager, + terminalManager: TerminalManager, + targetId: string, + name: string, +): Promise { + const client = connectionManager.getClient(); + if (!client || connectionManager.getState() !== 'connected') { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + const found = terminalManager.listDevTerminals().find(d => d.builderId === targetId); + if (!found) { + vscode.window.showInformationMessage(`Codev: No dev server is running for ${name}`); + return; + } + await client.killTerminal(found.terminalId); + terminalManager.closeDevTerminal(targetId); + vscode.window.showInformationMessage(`Codev: Dev server stopped for ${name}`); +} diff --git a/packages/vscode/src/commands/run-workspace-dev.ts b/packages/vscode/src/commands/run-workspace-dev.ts new file mode 100644 index 000000000..486abad96 --- /dev/null +++ b/packages/vscode/src/commands/run-workspace-dev.ts @@ -0,0 +1,42 @@ +/** + * Codev: Start / Stop Dev Server (Workspace view) — runs the dev server for + * whatever folder *this VSCode window is rooted at*. Not "main"-specific: + * + * - window opened on the main checkout → target `main`, cwd = root + * - window opened on a `.builders//` → target ``, cwd = worktree + * worktree (e.g. via "Open Worktree as Workspace") + * + * Target resolution is the *only* difference from the builder-row command; + * the actual spawn/swap/stop is the shared core in dev-shared.ts. + */ + +import * as vscode from 'vscode'; +import type { ConnectionManager } from '../connection-manager.js'; +import type { TerminalManager } from '../terminal-manager.js'; +import { resolveWorkspaceDevTarget, startDevForTarget, stopDevForTarget } from './dev-shared.js'; + +export async function runWorkspaceDev( + connectionManager: ConnectionManager, + terminalManager: TerminalManager, +): Promise { + const workspacePath = connectionManager.getWorkspacePath(); + if (!workspacePath) { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + const target = resolveWorkspaceDevTarget(workspacePath); + await startDevForTarget(connectionManager, terminalManager, target); +} + +export async function stopWorkspaceDev( + connectionManager: ConnectionManager, + terminalManager: TerminalManager, +): Promise { + const workspacePath = connectionManager.getWorkspacePath(); + if (!workspacePath) { + vscode.window.showErrorMessage('Codev: Not connected to Tower'); + return; + } + const target = resolveWorkspaceDevTarget(workspacePath); + await stopDevForTarget(connectionManager, terminalManager, target.id, target.name); +} diff --git a/packages/vscode/src/commands/run-worktree-dev.ts b/packages/vscode/src/commands/run-worktree-dev.ts index a4ce9fcf9..9a9d5098e 100644 --- a/packages/vscode/src/commands/run-worktree-dev.ts +++ b/packages/vscode/src/commands/run-worktree-dev.ts @@ -1,25 +1,17 @@ /** - * Codev: Run Dev Server — start a Tower-managed dev PTY for a builder's worktree. - * - * Right-click a builder row → "Run Dev Server". Reads `worktree.devCommand` - * from `.codev/config.json`, asks Tower to spawn the PTY (type='dev', label - * 'Dev: '), and auto-opens it as a VSCode terminal tab. - * - * Mirrors the swap-detection logic from the CLI's `afx dev` (commands/dev.ts) - * but uses a VSCode modal instead of a readline prompt. + * Codev: Run Dev Server — start a Tower-managed dev PTY for a builder's + * worktree. Right-click a builder row (or palette quick-pick) resolves the + * builder from Tower's overview, then delegates to the shared dev core + * (dev-shared.ts), which handles the devCommand read, swap, spawn, and tab + * open — identical to every other dev front-end; only target resolution + * differs (see run-workspace-dev.ts). */ import * as vscode from 'vscode'; -import { readFile } from 'node:fs/promises'; -import * as path from 'node:path'; import { resolveAgentName } from '@cluesmith/codev-core/agent-names'; import type { ConnectionManager } from '../connection-manager.js'; import type { TerminalManager } from '../terminal-manager.js'; - -/** Match the CLI's swap-sequencing values from packages/codev/src/agent-farm/commands/dev.ts */ -const KILL_WAIT_TIMEOUT_MS = 7000; -const KILL_POLL_INTERVAL_MS = 200; -const SWAP_GRACE_MS = 250; +import { startDevForTarget } from './dev-shared.js'; export async function runWorktreeDev( connectionManager: ConnectionManager, @@ -54,102 +46,23 @@ export async function runWorktreeDev( return; } - // Read worktree.devCommand from .codev/config.json - const devCommand = await readDevCommand(workspacePath); - if (!devCommand) { - vscode.window.showErrorMessage( - 'Codev: Configure worktree.devCommand in .codev/config.json to use this action. ' + - 'See "Runnable Worktrees" in CLAUDE.md for stack-specific recipes.', - ); - return; - } - - // Look up the human-friendly builder name from workspace state — same - // source and matching strategy the builder tab uses (openBuilderByRoleOrId - // in terminal-manager.ts). OverviewBuilder.id and Builder.id can differ in - // shape (one may include the worktree slug, the other not), so we use - // resolveAgentName for the tail-match fallback rather than strict ===. + // Human-friendly builder name — same source and matching strategy the + // builder tab uses (openBuilderByRoleOrId in terminal-manager.ts). + // OverviewBuilder.id and Builder.id can differ in shape, so resolveAgentName + // does the tail-match fallback rather than strict ===. const workspaceState = await client.getWorkspaceState(workspacePath); const { builder: namedBuilder } = resolveAgentName(builder.id, workspaceState?.builders ?? []); const builderName = namedBuilder?.name ?? builder.id; - // Swap detection. Source of truth for "what dev terminals exist" is - // TerminalManager's local map — Tower's label filter would be brittle - // and wouldn't catch terminals across VSCode instances anyway (a #690 - // non-goal). For now we assume one VSCode instance per workspace. - const existing = terminalManager.listDevTerminals(); - const sameBuilder = existing.find(d => d.builderId === builder.id); - if (sameBuilder) { - vscode.window.showInformationMessage(`Codev: Dev server is already running for ${builderName}`); - await terminalManager.openDevTerminal(sameBuilder.terminalId, builder.id, builderName, true); - return; - } - if (existing.length > 0) { - const other = existing[0]!; - const choice = await vscode.window.showWarningMessage( - `Stop dev for ${other.builderId} and start for ${builderName}?`, - { modal: true }, - 'Yes', 'No', - ); - if (choice !== 'Yes') { return; } - await client.killTerminal(other.terminalId); - terminalManager.closeDevTerminal(other.builderId); - try { - await waitForTerminalGone(client, other.terminalId); - } catch (err) { - vscode.window.showErrorMessage(`Codev: ${(err as Error).message}`); - return; - } - await new Promise((r) => setTimeout(r, SWAP_GRACE_MS)); - } - - // Spawn the new dev PTY - const terminal = await client.createTerminal({ - command: '/bin/sh', - args: ['-lc', devCommand], + await startDevForTarget(connectionManager, terminalManager, { + id: builder.id, cwd: builder.worktreePath, - workspacePath, - type: 'dev', - roleId: builder.id, - label: `Dev: ${builder.id}`, - persistent: false, + name: builderName, }); - if (!terminal) { - vscode.window.showErrorMessage(`Codev: Failed to spawn dev terminal for ${builderName}`); - return; - } - - await terminalManager.openDevTerminal(terminal.id, builder.id, builderName, true); - vscode.window.showInformationMessage(`Codev: Dev server started for ${builderName}`); } // ─── Helpers ──────────────────────────────────────────────────────────── -async function readDevCommand(workspacePath: string): Promise { - const configPath = path.join(workspacePath, '.codev', 'config.json'); - try { - const raw = await readFile(configPath, 'utf-8'); - const parsed = JSON.parse(raw) as { worktree?: { devCommand?: unknown } }; - const cmd = parsed.worktree?.devCommand; - return typeof cmd === 'string' && cmd.length > 0 ? cmd : null; - } catch { - return null; - } -} - -async function waitForTerminalGone( - client: NonNullable>, - terminalId: string, -): Promise { - const deadline = Date.now() + KILL_WAIT_TIMEOUT_MS; - while (Date.now() < deadline) { - const terminals = await client.listTerminals(); - if (!terminals.some(t => t.id === terminalId)) { return; } - await new Promise((r) => setTimeout(r, KILL_POLL_INTERVAL_MS)); - } - throw new Error(`Dev terminal ${terminalId} did not exit within ${KILL_WAIT_TIMEOUT_MS}ms`); -} - interface BuilderLike { id: string; issueId: string | null; diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 2ffa5c564..627eef290 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -9,6 +9,7 @@ import { cleanupBuilder } from './commands/cleanup.js'; import { openWorktreeWindow } from './commands/open-worktree-window.js'; import { runWorktreeDev } from './commands/run-worktree-dev.js'; import { stopWorktreeDev } from './commands/stop-worktree-dev.js'; +import { runWorkspaceDev, stopWorkspaceDev } from './commands/run-workspace-dev.js'; import { openWorktreeFolder } from './commands/open-worktree-folder.js'; import { runWorktreeSetup } from './commands/run-worktree-setup.js'; import { viewPlanFile } from './commands/view-artifact.js'; @@ -193,7 +194,7 @@ export async function activate(context: vscode.ExtensionContext) { pullRequestsView, backlogView, recentlyClosedView, - vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager)), + vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager, terminalManager!)), vscode.window.registerTreeDataProvider('codev.team', new TeamProvider(connectionManager)), vscode.window.registerTreeDataProvider('codev.status', new StatusProvider(connectionManager)), ); @@ -354,6 +355,10 @@ export async function activate(context: vscode.ExtensionContext) { runWorktreeDev(connectionManager!, terminalManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.stopWorktreeDev', () => stopWorktreeDev(connectionManager!, terminalManager!)), + vscode.commands.registerCommand('codev.runWorkspaceDev', () => + runWorkspaceDev(connectionManager!, terminalManager!)), + vscode.commands.registerCommand('codev.stopWorkspaceDev', () => + stopWorkspaceDev(connectionManager!, terminalManager!)), vscode.commands.registerCommand('codev.openWorktreeFolder', (arg: vscode.TreeItem | string | undefined) => openWorktreeFolder(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.runWorktreeSetup', (arg: vscode.TreeItem | string | undefined) => diff --git a/packages/vscode/src/terminal-manager.ts b/packages/vscode/src/terminal-manager.ts index a6d54ecb2..ed7ec30c9 100644 --- a/packages/vscode/src/terminal-manager.ts +++ b/packages/vscode/src/terminal-manager.ts @@ -23,6 +23,9 @@ export class TerminalManager { private outputChannel: vscode.OutputChannel; private connectionManager: ConnectionManager; private readonly iconPath: { light: vscode.Uri; dark: vscode.Uri }; + private readonly _onDidChangeDevTerminals = new vscode.EventEmitter(); + /** Fires whenever the set of open dev terminals changes (start/stop/swap/cleanup). */ + readonly onDidChangeDevTerminals = this._onDidChangeDevTerminals.event; constructor( connectionManager: ConnectionManager, @@ -142,6 +145,7 @@ export class TerminalManager { // Tab title matches the builder-tab format (`Codev: `) with a // `(dev)` suffix so the pairing is obvious in the tab strip. await this.openTerminal(terminalId, 'dev', `Codev: ${builderName} (dev)`, key, focus); + this._onDidChangeDevTerminals.fire(); } /** @@ -156,6 +160,7 @@ export class TerminalManager { existing.pty.close(); existing.terminal.dispose(); this.terminals.delete(key); + this._onDidChangeDevTerminals.fire(); } /** @@ -167,13 +172,16 @@ export class TerminalManager { * matching the value passed to `openBuilder`. */ closeBuilderTerminal(builderId: string): void { + let devClosed = false; for (const key of [`builder-${builderId}`, `dev-${builderId}`]) { const existing = this.terminals.get(key); if (!existing) { continue; } existing.pty.close(); existing.terminal.dispose(); this.terminals.delete(key); + if (key.startsWith('dev-')) { devClosed = true; } } + if (devClosed) { this._onDidChangeDevTerminals.fire(); } } /** @@ -287,5 +295,6 @@ export class TerminalManager { managed.terminal.dispose(); } this.terminals.clear(); + this._onDidChangeDevTerminals.dispose(); } } diff --git a/packages/vscode/src/views/workspace.ts b/packages/vscode/src/views/workspace.ts index 285d90acc..6bb04a00f 100644 --- a/packages/vscode/src/views/workspace.ts +++ b/packages/vscode/src/views/workspace.ts @@ -1,7 +1,9 @@ import * as vscode from 'vscode'; import { encodeWorkspacePath } from '@cluesmith/codev-core/workspace'; import type { ConnectionManager } from '../connection-manager.js'; +import type { TerminalManager } from '../terminal-manager.js'; import { getTowerAddress } from '../workspace-detector.js'; +import { resolveWorkspaceDevTarget } from '../commands/dev-shared.js'; /** * Workspace-level entry points: architect terminal, Tower web dashboard, @@ -12,8 +14,15 @@ export class WorkspaceProvider implements vscode.TreeDataProvider(); readonly onDidChangeTreeData = this.changeEmitter.event; - constructor(private connectionManager: ConnectionManager) { + constructor( + private connectionManager: ConnectionManager, + private terminalManager: TerminalManager, + ) { connectionManager.onStateChange(() => this.changeEmitter.fire()); + // Re-render when the dev-terminal set changes (start/stop, a swap that + // killed this workspace's dev, or cleanup) so the conditional "Stop Dev + // Server" row reflects reality across every path. + terminalManager.onDidChangeDevTerminals(() => this.changeEmitter.fire()); } getTreeItem(element: vscode.TreeItem): vscode.TreeItem { @@ -67,6 +76,40 @@ export class WorkspaceProvider implements vscode.TreeDataProvider/` worktree → that builder. The + // label stays generic; the tooltip names the resolved target. + const workspacePath = this.connectionManager.getWorkspacePath(); + const devTarget = workspacePath ? resolveWorkspaceDevTarget(workspacePath) : null; + + const startDev = new vscode.TreeItem('Start Dev Server'); + startDev.iconPath = new vscode.ThemeIcon('play'); + startDev.tooltip = devTarget + ? `Run worktree.devCommand for this workspace (target: ${devTarget.id})` + : 'Run worktree.devCommand for this workspace'; + startDev.contextValue = 'workspace-dev-start'; + startDev.command = { + command: 'codev.runWorkspaceDev', + title: 'Start Dev Server', + }; + items.push(startDev); + + // Stop row only while a dev PTY for THIS workspace's target is running. + const targetDevRunning = !!devTarget && this.terminalManager + .listDevTerminals() + .some(d => d.builderId === devTarget.id); + if (targetDevRunning) { + const stopDev = new vscode.TreeItem('Stop Dev Server'); + stopDev.iconPath = new vscode.ThemeIcon('debug-stop'); + stopDev.tooltip = `Stop the dev server for this workspace (target: ${devTarget!.id})`; + stopDev.contextValue = 'workspace-dev-stop'; + stopDev.command = { + command: 'codev.stopWorkspaceDev', + title: 'Stop Dev Server', + }; + items.push(stopDev); + } + return items; } From 2c7b7e29697b4fe17d7d9df8a2663e0c56f0eaff Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sun, 17 May 2026 20:41:26 +1000 Subject: [PATCH 53/63] feat(vscode): dev server terminals open in the bottom panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dev server is a long-running background log, not an interactive surface you tab between. Route type==='dev' to vscode.TerminalLocation.Panel always, independent of the codev.terminalPosition setting. Architect (editor group 1), builder/shell (group 2), and the global setting are unchanged. Flat if/else — no nested ternaries. --- packages/vscode/src/terminal-manager.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/vscode/src/terminal-manager.ts b/packages/vscode/src/terminal-manager.ts index ed7ec30c9..a084bf007 100644 --- a/packages/vscode/src/terminal-manager.ts +++ b/packages/vscode/src/terminal-manager.ts @@ -240,9 +240,19 @@ export class TerminalManager { const authKey = await this.getAuthKey(); const pty = new CodevPseudoterminal(wsUrl, authKey, this.outputChannel); const position = vscode.workspace.getConfiguration('codev').get('terminalPosition', 'editor'); - const location = position === 'editor' - ? { viewColumn: type === 'architect' ? vscode.ViewColumn.One : vscode.ViewColumn.Two } - : vscode.TerminalLocation.Panel; + + // Dev servers are long-running background logs — always the bottom panel, + // regardless of the `codev.terminalPosition` setting (which governs the + // architect/builder/shell terminals: architect → editor group 1, the + // rest → group 2). + let location: vscode.TerminalLocation | vscode.TerminalEditorLocationOptions; + if (type === 'dev' || position !== 'editor') { + location = vscode.TerminalLocation.Panel; + } else if (type === 'architect') { + location = { viewColumn: vscode.ViewColumn.One }; + } else { + location = { viewColumn: vscode.ViewColumn.Two }; + } const terminal = vscode.window.createTerminal({ name, pty, location, iconPath: this.iconPath }); From 8e2bb604519ffc798408e601dbe9e57811ec093c Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Sun, 17 May 2026 21:17:35 +1000 Subject: [PATCH 54/63] =?UTF-8?q?feat(vscode):=20Team=20view=20=E2=80=94?= =?UTF-8?q?=20clickable=20issue/PR=20rows,=20Refresh,=20Assigned=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Assigned / Open-PR rows: single click opens the item on GitHub (vscode.open the issue/PR URL; no command set when URL absent). - Team title-bar Refresh button (codev.refreshTeam → provider.refresh): Team otherwise only refetches as a side effect of `overview-changed` SSE events, so on a quiet workspace it could sit stale/empty. - "Working on:" → "Assigned:" — the label now matches the data (open issues where the member is the GitHub assignee, not an activity signal). - No context menus / right-click — single click = action. --- packages/vscode/package.json | 10 ++++++++++ packages/vscode/src/extension.ts | 4 +++- packages/vscode/src/views/team.ts | 8 +++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 24d70a140..7596158b5 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -84,6 +84,11 @@ "title": "Codev: Refresh Overview", "icon": "$(refresh)" }, + { + "command": "codev.refreshTeam", + "title": "Codev: Refresh Team", + "icon": "$(refresh)" + }, { "command": "codev.reconnect", "title": "Codev: Reconnect to Tower", @@ -293,6 +298,11 @@ "when": "view == codev.recentlyClosed", "group": "navigation" }, + { + "command": "codev.refreshTeam", + "when": "view == codev.team", + "group": "navigation" + }, { "command": "codev.reconnect", "when": "view == codev.status", diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index 627eef290..a4cc856c8 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -189,13 +189,14 @@ export async function activate(context: vscode.ExtensionContext) { pullRequestsView = vscode.window.createTreeView('codev.pullRequests', { treeDataProvider: new PullRequestsProvider(overviewCache) }); backlogView = vscode.window.createTreeView('codev.backlog', { treeDataProvider: new BacklogProvider(overviewCache) }); recentlyClosedView = vscode.window.createTreeView('codev.recentlyClosed', { treeDataProvider: new RecentlyClosedProvider(overviewCache) }); + const teamProvider = new TeamProvider(connectionManager); context.subscriptions.push( buildersView, pullRequestsView, backlogView, recentlyClosedView, vscode.window.registerTreeDataProvider('codev.workspace', new WorkspaceProvider(connectionManager, terminalManager!)), - vscode.window.registerTreeDataProvider('codev.team', new TeamProvider(connectionManager)), + vscode.window.registerTreeDataProvider('codev.team', teamProvider), vscode.window.registerTreeDataProvider('codev.status', new StatusProvider(connectionManager)), ); @@ -359,6 +360,7 @@ export async function activate(context: vscode.ExtensionContext) { runWorkspaceDev(connectionManager!, terminalManager!)), vscode.commands.registerCommand('codev.stopWorkspaceDev', () => stopWorkspaceDev(connectionManager!, terminalManager!)), + vscode.commands.registerCommand('codev.refreshTeam', () => teamProvider.refresh()), vscode.commands.registerCommand('codev.openWorktreeFolder', (arg: vscode.TreeItem | string | undefined) => openWorktreeFolder(connectionManager!, extractBuilderId(arg))), vscode.commands.registerCommand('codev.runWorktreeSetup', (arg: vscode.TreeItem | string | undefined) => diff --git a/packages/vscode/src/views/team.ts b/packages/vscode/src/views/team.ts index 185d80ef2..77c771cf5 100644 --- a/packages/vscode/src/views/team.ts +++ b/packages/vscode/src/views/team.ts @@ -45,8 +45,11 @@ export class TeamProvider implements vscode.TreeDataProvider { if (ghd.assignedIssues?.length) { items.push(...ghd.assignedIssues.map((i: any) => { - const ti = new vscode.TreeItem(`Working on: #${i.number} ${i.title}`); + const ti = new vscode.TreeItem(`Assigned: #${i.number} ${i.title}`); ti.iconPath = new vscode.ThemeIcon('issues'); + if (i.url) { + ti.command = { command: 'vscode.open', title: 'Open Issue on GitHub', arguments: [vscode.Uri.parse(i.url)] }; + } return ti; })); } @@ -55,6 +58,9 @@ export class TeamProvider implements vscode.TreeDataProvider { items.push(...ghd.openPRs.map((p: any) => { const ti = new vscode.TreeItem(`Open PR: #${p.number} ${p.title}`); ti.iconPath = new vscode.ThemeIcon('git-pull-request'); + if (p.url) { + ti.command = { command: 'vscode.open', title: 'Open Pull Request on GitHub', arguments: [vscode.Uri.parse(p.url)] }; + } return ti; })); } From 1ff16f25879ffba85e2192380ca4894496f91601 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Mon, 18 May 2026 09:18:05 +1000 Subject: [PATCH 55/63] =?UTF-8?q?feat(vscode):=20Team=20=E2=80=94=20collap?= =?UTF-8?q?se=20assigned-issues=20and=20open-PRs=20to=20counts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-member issue/PR lists duplicated the Backlog / Pull Requests views. Replace both with single "Assigned: N" / "Open PRs: N" summary rows, mirroring the "Last 7d:" stat row (informational, no command, shown only when > 0). Supersedes the per-item click-through from 8e2bb604; Team Refresh button retained. --- packages/vscode/src/views/team.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/vscode/src/views/team.ts b/packages/vscode/src/views/team.ts index 77c771cf5..ad3cafcd2 100644 --- a/packages/vscode/src/views/team.ts +++ b/packages/vscode/src/views/team.ts @@ -43,26 +43,21 @@ export class TeamProvider implements vscode.TreeDataProvider { const items: vscode.TreeItem[] = []; const ghd = member.github_data; + // Assigned issues are a count-only summary — the actual issue list lives + // in the Backlog view; duplicating it per-member here is noise. Mirrors + // the "Last 7d:" stat row (single informational row, no command). if (ghd.assignedIssues?.length) { - items.push(...ghd.assignedIssues.map((i: any) => { - const ti = new vscode.TreeItem(`Assigned: #${i.number} ${i.title}`); - ti.iconPath = new vscode.ThemeIcon('issues'); - if (i.url) { - ti.command = { command: 'vscode.open', title: 'Open Issue on GitHub', arguments: [vscode.Uri.parse(i.url)] }; - } - return ti; - })); + const ti = new vscode.TreeItem(`Assigned: ${ghd.assignedIssues.length}`); + ti.iconPath = new vscode.ThemeIcon('issues'); + items.push(ti); } + // Open PRs: count-only summary — the PR list lives in the Pull Requests + // view. Mirrors the "Assigned:" / "Last 7d:" stat rows. if (ghd.openPRs?.length) { - items.push(...ghd.openPRs.map((p: any) => { - const ti = new vscode.TreeItem(`Open PR: #${p.number} ${p.title}`); - ti.iconPath = new vscode.ThemeIcon('git-pull-request'); - if (p.url) { - ti.command = { command: 'vscode.open', title: 'Open Pull Request on GitHub', arguments: [vscode.Uri.parse(p.url)] }; - } - return ti; - })); + const ti = new vscode.TreeItem(`Open PRs: ${ghd.openPRs.length}`); + ti.iconPath = new vscode.ThemeIcon('git-pull-request'); + items.push(ti); } const merged = ghd.recentActivity?.mergedPRs?.length ?? 0; From deff725b9156e1dfde96f4c5f3fbd331ffdf714c Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Mon, 18 May 2026 09:20:45 +1000 Subject: [PATCH 56/63] feat(vscode): toggle Codev sidebar via Cmd+Alt+C (mac) / Ctrl+K C Adds a true open/close toggle for the Codev view container using two complementary when-clause keybindings (sideBarVisible + activeViewlet), so the same key reveals Codev when hidden/inactive and closes the sidebar when Codev is the active container. No extension code needed. Best-per-platform keys: single-stroke Cmd+Alt+C on macOS; the safe Ctrl+K C chord on Windows/Linux to avoid the Ctrl+Alt=AltGr clash on international keyboard layouts. --- packages/vscode/package.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index 7596158b5..cf872915d 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -345,6 +345,18 @@ "command": "codev.approveGate", "key": "ctrl+k g", "mac": "cmd+k g" + }, + { + "command": "workbench.view.extension.codev", + "key": "ctrl+k c", + "mac": "cmd+alt+c", + "when": "!sideBarVisible || activeViewlet != 'workbench.view.extension.codev'" + }, + { + "command": "workbench.action.closeSidebar", + "key": "ctrl+k c", + "mac": "cmd+alt+c", + "when": "sideBarVisible && activeViewlet == 'workbench.view.extension.codev'" } ], "viewsContainers": { From 9464eb00a1caeea879c8c563d5ca5667d58a1924 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Mon, 18 May 2026 09:22:58 +1000 Subject: [PATCH 57/63] fix(vscode): Workspace Start/Stop Dev Server are mutually exclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Show exactly one row — Start when this workspace's dev is stopped, Stop when it's running (play/stop model; the visible control is the state indicator). Removes the always-Start + additive-Stop behavior that was mock-driven inertia. Flips live via onDidChangeDevTerminals. --- packages/vscode/src/views/workspace.ts | 29 ++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/vscode/src/views/workspace.ts b/packages/vscode/src/views/workspace.ts index 6bb04a00f..2622966d0 100644 --- a/packages/vscode/src/views/workspace.ts +++ b/packages/vscode/src/views/workspace.ts @@ -82,22 +82,13 @@ export class WorkspaceProvider implements vscode.TreeDataProvider d.builderId === devTarget.id); + if (targetDevRunning) { const stopDev = new vscode.TreeItem('Stop Dev Server'); stopDev.iconPath = new vscode.ThemeIcon('debug-stop'); @@ -108,6 +99,18 @@ export class WorkspaceProvider implements vscode.TreeDataProvider Date: Mon, 18 May 2026 10:46:58 +1000 Subject: [PATCH 58/63] feat(vscode): symmetric Cmd+Alt / Ctrl+Alt keybindings for Codev actions Unifies the Codev action shortcut family onto one cross-platform model (Cmd+Alt+ on macOS, Ctrl+Alt+ on Windows/Linux): C -> toggle Codev sidebar (was asymmetric: Ctrl+K C chord on Win/Linux) R -> run workspace dev server (codev.runWorkspaceDev, new) S -> stop workspace dev server (codev.stopWorkspaceDev, new) Sidebar Win/Linux key moved Ctrl+K C -> Ctrl+Alt+C so all three follow the same pattern. Verified clear of the macOS system Cmd+Alt set, VSCode core, and all installed extensions; the only overlap is a narrow when-scoped Quarto binding (cmd/ctrl+alt+r = quarto.runAllCells, active only inside Quarto editor contexts) - accepted edge case. C/R/S are not common AltGr characters, keeping the symmetric Ctrl+Alt form low-risk on international keyboards. Sidebar toggle keeps its two when-clause entries (open/close); only the key field changed. --- packages/vscode/package.json | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/vscode/package.json b/packages/vscode/package.json index cf872915d..11bafda78 100644 --- a/packages/vscode/package.json +++ b/packages/vscode/package.json @@ -348,15 +348,25 @@ }, { "command": "workbench.view.extension.codev", - "key": "ctrl+k c", + "key": "ctrl+alt+c", "mac": "cmd+alt+c", "when": "!sideBarVisible || activeViewlet != 'workbench.view.extension.codev'" }, { "command": "workbench.action.closeSidebar", - "key": "ctrl+k c", + "key": "ctrl+alt+c", "mac": "cmd+alt+c", "when": "sideBarVisible && activeViewlet == 'workbench.view.extension.codev'" + }, + { + "command": "codev.runWorkspaceDev", + "key": "ctrl+alt+r", + "mac": "cmd+alt+r" + }, + { + "command": "codev.stopWorkspaceDev", + "key": "ctrl+alt+s", + "mac": "cmd+alt+s" } ], "viewsContainers": { From 9e72ec9fd0c4e011841d57acbc1dd050e58c0808 Mon Sep 17 00:00:00 2001 From: Amr Elsayed Date: Mon, 18 May 2026 22:02:49 +1000 Subject: [PATCH 59/63] feat(vscode): friendly builder tab titles (Codev: # ) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builder terminal tabs mirror their sidebar row — `Codev: #<issueId> <issueTitle>` — instead of the internal `builder-<protocol>-<id>` agent name. - TerminalManager.friendlyBuilderLabel resolves the builder in OverviewCache (injected via the constructor — proper DI, no setter). - Matches on the trailing id token so the canonical roleId open paths pass lines up with OverviewCache's short ids (resolveAgentName expects a short target; passing the full roleId never matched). - Issue title capped at 25 chars on a whole-word boundary (hard-cut fallback for a single over-long word); `#<id>` kept whole — VSCode's default `tabSizing: 'fit'` doesn't ellipsize a lone wide tab. - Falls back to the agent name when overview data / a match is unavailable. Display-only: identity, cleanup, and click-to-focus key off the `builder-<id>` map key and Terminal object, never the title. --- packages/vscode/src/extension.ts | 9 +++-- packages/vscode/src/terminal-manager.ts | 46 ++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/packages/vscode/src/extension.ts b/packages/vscode/src/extension.ts index a4cc856c8..902fdce09 100644 --- a/packages/vscode/src/extension.ts +++ b/packages/vscode/src/extension.ts @@ -103,8 +103,12 @@ export async function activate(context: vscode.ExtensionContext) { } }); + // OverviewCache is created before TerminalManager so it can be injected — + // TerminalManager uses it for friendly builder tab titles (`Codev: #<id> <title>`). + const overviewCache = new OverviewCache(connectionManager); + // Terminal Manager - terminalManager = new TerminalManager(connectionManager, outputChannel, context.extensionUri); + terminalManager = new TerminalManager(connectionManager, outputChannel, context.extensionUri, overviewCache); context.subscriptions.push({ dispose: () => terminalManager?.dispose() }); // Update status bar with builder/gate counts @@ -174,8 +178,7 @@ export async function activate(context: vscode.ExtensionContext) { } }; - // Sidebar TreeViews - const overviewCache = new OverviewCache(connectionManager); + // Sidebar TreeViews (overviewCache created above, before TerminalManager) context.subscriptions.push({ dispose: () => overviewCache.dispose() }); overviewCache.onDidChange(() => { updateStatusBarCounts(); diff --git a/packages/vscode/src/terminal-manager.ts b/packages/vscode/src/terminal-manager.ts index a084bf007..1b0a120e1 100644 --- a/packages/vscode/src/terminal-manager.ts +++ b/packages/vscode/src/terminal-manager.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { CodevPseudoterminal } from './terminal-adapter.js'; import type { ConnectionManager } from './connection-manager.js'; +import type { OverviewCache } from './views/overview-data.js'; import { encodeWorkspacePath } from '@cluesmith/codev-core/workspace'; import { resolveAgentName } from '@cluesmith/codev-core/agent-names'; import type { TerminalType } from '@cluesmith/codev-core/tower-client'; @@ -26,14 +27,17 @@ export class TerminalManager { private readonly _onDidChangeDevTerminals = new vscode.EventEmitter<void>(); /** Fires whenever the set of open dev terminals changes (start/stop/swap/cleanup). */ readonly onDidChangeDevTerminals = this._onDidChangeDevTerminals.event; + private readonly overviewCache: OverviewCache; constructor( connectionManager: ConnectionManager, outputChannel: vscode.OutputChannel, extensionUri: vscode.Uri, + overviewCache: OverviewCache, ) { this.connectionManager = connectionManager; this.outputChannel = outputChannel; + this.overviewCache = overviewCache; // Theme-aware icon pair. The single-Uri form rendered as solid black on // dark themes (VSCode doesn't resolve currentColor on terminal-tab icons). // codev-light.svg has dark fill (visible on light themes), codev-dark.svg @@ -44,6 +48,46 @@ export class TerminalManager { }; } + /** + * Friendly builder tab title — mirrors the sidebar's `#<issueId> <issueTitle>`, + * prefixed `Codev: `. Falls back to `fallback` (the canonical + * `Codev: <agent-name>`) whenever overview data, a builder match, or an + * issue number is unavailable, so the title is never broken or empty. + * + * Display-only: terminal identity, cleanup, and click-to-focus all key off + * the `builder-<id>` map key and Terminal object — never the tab title — so + * changing this string has no functional side effects. + */ + private friendlyBuilderLabel(builderId: string, fallback: string): string { + const data = this.overviewCache.getData(); + if (!data?.builders?.length) { return fallback; } + // Open paths pass the canonical roleId (`builder-<protocol>-<id>`), but + // OverviewCache builders carry the short id the sidebar uses. resolveAgentName + // matches a *short* target against canonical candidates, so feed it the + // trailing id token, not the full roleId (otherwise it never matches and + // every builder falls back to the agent name). + const shortId = builderId.split('-').pop() ?? builderId; + const { builder } = resolveAgentName(shortId, data.builders); + // Mirror the sidebar exactly: `#${issueId ?? id} ${issueTitle ?? ''}`. + const num = builder?.issueId ?? builder?.id; + if (num === undefined || num === null || num === '') { return fallback; } + // Bound the tab name: `#<id>` stays whole (short, identifying), but the + // issue title is capped — VSCode's default `tabSizing: 'fit'` does not + // ellipsize a lone wide tab, so an unbounded title spans the whole group. + const MAX_TITLE = 25; + const raw = (builder?.issueTitle ?? '').trim(); + let title = raw; + if (raw.length > MAX_TITLE) { + const slice = raw.slice(0, MAX_TITLE); + const lastSpace = slice.lastIndexOf(' '); + // Cut at the last whole word; fall back to a hard cut when the first + // word alone already exceeds the cap (no usable space boundary). + const cut = lastSpace > 0 ? slice.slice(0, lastSpace) : slice.slice(0, MAX_TITLE - 1); + title = `${cut.trimEnd()}…`; + } + return `Codev: #${num}${title ? ` ${title}` : ''}`; + } + /** * Open the architect terminal. `focus` defaults to false so background * paths don't steal focus; click paths pass true. @@ -81,7 +125,7 @@ export class TerminalManager { existing.terminal.dispose(); this.terminals.delete(key); } - await this.openTerminal(terminalId, 'builder', label, key, focus); + await this.openTerminal(terminalId, 'builder', this.friendlyBuilderLabel(builderId, label), key, focus); } /** From 986f56cd33a1181da8311a69f8874b942747f529 Mon Sep 17 00:00:00 2001 From: Amr Elsayed <amrmelsayed@users.noreply.github.com> Date: Mon, 18 May 2026 22:03:18 +1000 Subject: [PATCH 60/63] fix(vscode): backlog issue preview reuses one editor group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a backlog issue called markdown.showPreviewToSide, which opens in ViewColumn.Beside — relative to the *active* editor group. After the first preview opened (and became the active editor), each further click resolved Beside to a new group (3, 4, …) instead of reusing one, unlike builder terminals which target the absolute ViewColumn.Two. Anchor focus to editor group 1 before showing the preview so Beside resolves to group 2 every time, giving issue previews the same single-group reuse as builder terminals. Keeps the proven virtual-doc preview path (no custom-editor swap). --- packages/vscode/src/commands/view-issue.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/vscode/src/commands/view-issue.ts b/packages/vscode/src/commands/view-issue.ts index ccd5b287e..c3efd84d4 100644 --- a/packages/vscode/src/commands/view-issue.ts +++ b/packages/vscode/src/commands/view-issue.ts @@ -84,9 +84,16 @@ export async function viewBacklogIssue( provider.set(issueId, renderIssue(issueId, issue)); const uri = vscode.Uri.parse(`${SCHEME}:${issueId}.md`); - // Render as a read-only markdown preview in the side (right) editor - // column — same placement model as builder terminals (which target - // ViewColumn.Two). `showPreviewToSide` opens in ViewColumn.Beside, - // which from a sidebar click lands in the right pane. + // Render as a read-only markdown preview in editor group 2 — the same + // placement model as builder terminals (which target ViewColumn.Two). + // + // `markdown.showPreviewToSide` opens in ViewColumn.Beside, which is + // RELATIVE to the active editor group, not an absolute column. Without + // anchoring, the first click (sidebar focused → group 1 active) lands + // in group 2, but a second click while that preview is focused makes + // "Beside" resolve to group 3, then 4, … — a brand-new group per click. + // Focusing group 1 first pins "Beside" to group 2 every time, so issue + // previews consistently reuse a single group like the builder terminals. + await vscode.commands.executeCommand('workbench.action.focusFirstEditorGroup'); await vscode.commands.executeCommand('markdown.showPreviewToSide', uri); } From 00d1345733dca40eccbe303f22689d52e63379cf Mon Sep 17 00:00:00 2001 From: Amr Elsayed <amrmelsayed@users.noreply.github.com> Date: Tue, 19 May 2026 07:37:18 +1000 Subject: [PATCH 61/63] fix(team): use GitHub search.issueCount for true per-member counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Team view's per-member counts (Assigned / Open PRs / 7-day merged & closed) were nodes.length of search(..., first: 20) results, so they silently capped at 20. Add `issueCount` to all four per-member searches in buildTeamGraphQLQuery and expose true totals as new *Count fields on TeamMemberGitHubData (canonical @cluesmith/codev-types + the codev mirror); the parser falls back to node length if the field is absent. The Team view renders the *Count fields. Node arrays are unchanged (still 20-capped, still feed review-blocking / lists) — purely additive. --- packages/codev/src/lib/team-github.ts | 31 +++++++++++++++++++++------ packages/types/src/api.ts | 7 ++++++ packages/vscode/src/views/team.ts | 21 +++++++++--------- 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/packages/codev/src/lib/team-github.ts b/packages/codev/src/lib/team-github.ts index 63c307e55..027d57da8 100644 --- a/packages/codev/src/lib/team-github.ts +++ b/packages/codev/src/lib/team-github.ts @@ -33,11 +33,17 @@ export interface ReviewBlockingEntry { // Keep in sync with the canonical definition in @cluesmith/codev-types. export interface TeamMemberGitHubData { + // node arrays are capped at GitHub search `first` (20) and feed lists / + // review-blocking; the *Count fields are the true totals (search.issueCount). assignedIssues: { number: number; title: string; url: string }[]; + assignedIssuesCount: number; openPRs: { number: number; title: string; url: string }[]; + openPRsCount: number; recentActivity: { mergedPRs: { number: number; title: string; url: string; mergedAt: string }[]; + mergedPRsCount: number; closedIssues: { number: number; title: string; url: string; closedAt: string }[]; + closedIssuesCount: number; }; reviewBlocking: ReviewBlockingEntry[]; } @@ -100,9 +106,11 @@ export function buildTeamGraphQLQuery(members: TeamMember[], owner: string, name const alias = toAlias(m.github); return ` ${alias}_assigned: search(query: "repo:${repo} assignee:${m.github} is:issue is:open", type: ISSUE, first: 20) { + issueCount nodes { ... on Issue { number title url } } } ${alias}_prs: search(query: "repo:${repo} author:${m.github} is:pr is:open", type: ISSUE, first: 20) { + issueCount nodes { ... on PullRequest { number @@ -118,9 +126,11 @@ export function buildTeamGraphQLQuery(members: TeamMember[], owner: string, name } } ${alias}_merged: search(query: "repo:${repo} author:${m.github} is:pr is:merged merged:>=${since}", type: ISSUE, first: 20) { + issueCount nodes { ... on PullRequest { number title url mergedAt } } } ${alias}_closed: search(query: "repo:${repo} assignee:${m.github} is:issue is:closed closed:>=${since}", type: ISSUE, first: 20) { + issueCount nodes { ... on Issue { number title url closedAt } } }`; }) @@ -257,20 +267,27 @@ export function parseTeamGraphQLResponse( if (!isValidGitHubHandle(member.github)) continue; const alias = toAlias(member.github); - const assigned = data[`${alias}_assigned`] as { nodes?: Array<{ number: number; title: string; url: string }> } | undefined; - const prs = data[`${alias}_prs`] as { nodes?: OpenPrNode[] } | undefined; - const merged = data[`${alias}_merged`] as { nodes?: Array<{ number: number; title: string; url: string; mergedAt: string }> } | undefined; - const closed = data[`${alias}_closed`] as { nodes?: Array<{ number: number; title: string; url: string; closedAt: string }> } | undefined; + const assigned = data[`${alias}_assigned`] as { issueCount?: number; nodes?: Array<{ number: number; title: string; url: string }> } | undefined; + const prs = data[`${alias}_prs`] as { issueCount?: number; nodes?: OpenPrNode[] } | undefined; + const merged = data[`${alias}_merged`] as { issueCount?: number; nodes?: Array<{ number: number; title: string; url: string; mergedAt: string }> } | undefined; + const closed = data[`${alias}_closed`] as { issueCount?: number; nodes?: Array<{ number: number; title: string; url: string; closedAt: string }> } | undefined; + const assignedNodes = assigned?.nodes ?? []; const openPrNodes = prs?.nodes ?? []; + const mergedNodes = merged?.nodes ?? []; + const closedNodes = closed?.nodes ?? []; prsByAuthor.set(member.github, openPrNodes); result.set(member.github, { - assignedIssues: (assigned?.nodes ?? []).map(n => ({ number: n.number, title: n.title, url: n.url })), + assignedIssues: assignedNodes.map(n => ({ number: n.number, title: n.title, url: n.url })), + assignedIssuesCount: assigned?.issueCount ?? assignedNodes.length, openPRs: openPrNodes.map(n => ({ number: n.number, title: n.title, url: n.url })), + openPRsCount: prs?.issueCount ?? openPrNodes.length, recentActivity: { - mergedPRs: (merged?.nodes ?? []).map(n => ({ number: n.number, title: n.title, url: n.url, mergedAt: n.mergedAt })), - closedIssues: (closed?.nodes ?? []).map(n => ({ number: n.number, title: n.title, url: n.url, closedAt: n.closedAt })), + mergedPRs: mergedNodes.map(n => ({ number: n.number, title: n.title, url: n.url, mergedAt: n.mergedAt })), + mergedPRsCount: merged?.issueCount ?? mergedNodes.length, + closedIssues: closedNodes.map(n => ({ number: n.number, title: n.title, url: n.url, closedAt: n.closedAt })), + closedIssuesCount: closed?.issueCount ?? closedNodes.length, }, reviewBlocking: [], }); diff --git a/packages/types/src/api.ts b/packages/types/src/api.ts index 099c61597..6230d8ce8 100644 --- a/packages/types/src/api.ts +++ b/packages/types/src/api.ts @@ -178,11 +178,18 @@ export interface ReviewBlockingEntry { } export interface TeamMemberGitHubData { + // node arrays are capped at GitHub search `first` (20) and feed lists / + // review-blocking; the *Count fields are the true totals (search.issueCount) + // and must be used for any "N assigned / N open" display. assignedIssues: { number: number; title: string; url: string }[]; + assignedIssuesCount: number; openPRs: { number: number; title: string; url: string }[]; + openPRsCount: number; recentActivity: { mergedPRs: { number: number; title: string; url: string; mergedAt: string }[]; + mergedPRsCount: number; closedIssues: { number: number; title: string; url: string; closedAt: string }[]; + closedIssuesCount: number; }; reviewBlocking: ReviewBlockingEntry[]; } diff --git a/packages/vscode/src/views/team.ts b/packages/vscode/src/views/team.ts index ad3cafcd2..d39e49bf1 100644 --- a/packages/vscode/src/views/team.ts +++ b/packages/vscode/src/views/team.ts @@ -43,25 +43,24 @@ export class TeamProvider implements vscode.TreeDataProvider<vscode.TreeItem> { const items: vscode.TreeItem[] = []; const ghd = member.github_data; - // Assigned issues are a count-only summary — the actual issue list lives - // in the Backlog view; duplicating it per-member here is noise. Mirrors - // the "Last 7d:" stat row (single informational row, no command). - if (ghd.assignedIssues?.length) { - const ti = new vscode.TreeItem(`Assigned: ${ghd.assignedIssues.length}`); + // Count-only summaries — the issue/PR lists live in the Backlog / Pull + // Requests views. Use the *Count fields (true totals via GitHub + // search.issueCount); the node arrays are capped at 20 so `.length` + // would silently max out. + if ((ghd.assignedIssuesCount ?? 0) > 0) { + const ti = new vscode.TreeItem(`Assigned: ${ghd.assignedIssuesCount}`); ti.iconPath = new vscode.ThemeIcon('issues'); items.push(ti); } - // Open PRs: count-only summary — the PR list lives in the Pull Requests - // view. Mirrors the "Assigned:" / "Last 7d:" stat rows. - if (ghd.openPRs?.length) { - const ti = new vscode.TreeItem(`Open PRs: ${ghd.openPRs.length}`); + if ((ghd.openPRsCount ?? 0) > 0) { + const ti = new vscode.TreeItem(`Open PRs: ${ghd.openPRsCount}`); ti.iconPath = new vscode.ThemeIcon('git-pull-request'); items.push(ti); } - const merged = ghd.recentActivity?.mergedPRs?.length ?? 0; - const closed = ghd.recentActivity?.closedIssues?.length ?? 0; + const merged = ghd.recentActivity?.mergedPRsCount ?? 0; + const closed = ghd.recentActivity?.closedIssuesCount ?? 0; if (merged || closed) { const ti = new vscode.TreeItem(`Last 7d: ${merged} merged, ${closed} closed`); ti.iconPath = new vscode.ThemeIcon('graph'); From 3da673ba7ec87741602b038232a705be413082ad Mon Sep 17 00:00:00 2001 From: Amr Elsayed <amrmelsayed@users.noreply.github.com> Date: Tue, 19 May 2026 07:38:19 +1000 Subject: [PATCH 62/63] docs(vscode): changelog [Unreleased] + README refresh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CHANGELOG: add an [Unreleased] section covering the user-facing extension changes since 3.0.4 (managed dev server, Team snapshot + true issueCount-based counts, friendly builder tab titles w/ 25-char word-boundary truncation, symmetric Cmd/Ctrl+Alt shortcuts, gate toasts, PIR support, Open Worktree window, backlog actions, …) plus the backlog-preview single-group fix. Released-version history left intact; superseded items (View Diff -> Open Worktree, Needs Attention -> Builders) corrected forward. README: fix stale Needs-Attention / editor-area claims; refresh the Features, Commands (+ shortcuts), and Settings tables. --- packages/vscode/CHANGELOG.md | 31 +++++++++++++++++++++++++++++++ packages/vscode/README.md | 20 ++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/packages/vscode/CHANGELOG.md b/packages/vscode/CHANGELOG.md index dc2fb22cb..ca6dd9a6c 100644 --- a/packages/vscode/CHANGELOG.md +++ b/packages/vscode/CHANGELOG.md @@ -2,6 +2,37 @@ What's changed in the Codev VS Code extension, version by version, written for the developers who use it. +## [Unreleased] + +### What's new + +- **Codev-managed dev server for the current workspace.** New **Start Dev Server** / **Stop Dev Server** rows in the sidebar's Workspace view run `worktree.devCommand` for whatever folder the window is rooted at — the main checkout (CLI: `afx dev main`) or, if you opened a `.builders/<id>/` worktree as its own window, that builder. One dev runs at a time across {main + all builders}; starting another prompts to swap. The two rows are mutually exclusive: **Start** when stopped, **Stop** when running. +- **Dev server terminals open in the bottom panel** (not an editor group). A dev server is a long-running log, not a tab you switch between; `type: 'dev'` terminals now always use the panel regardless of `codev.terminalPosition`. Architect (left group) and builder/shell (right) are unchanged. +- **Builder terminal tabs are labeled by issue** — a builder's tab now reads `Codev: #<issueId> <issueTitle>` (matching its sidebar row) instead of the internal `builder-<protocol>-<id>` agent name. The title is capped at 25 characters on a whole-word boundary (with `…`); the `#<id>` prefix is kept whole. Falls back to the agent name when overview data or a builder match is unavailable. Architect / Shell / dev tab names are unchanged. +- **Team view is a per-member snapshot.** Expanding a teammate shows `Assigned: N` and `Open PRs: N` count summaries plus `Last 7d: X merged, Y closed` — the full issue/PR lists already live in the Backlog and Pull Requests views. A Refresh button in the Team title bar forces a re-fetch (Team otherwise only updates as a side effect of other Tower activity). +- **A symmetric keyboard-shortcut family** — `Cmd+Alt+C` / `Ctrl+Alt+C` toggles the Codev sidebar (opens & focuses it if hidden, closes it if it's the active view); `Cmd/Ctrl+Alt+R` and `Cmd/Ctrl+Alt+S` start / stop the current workspace's dev server. The existing `Cmd/Ctrl+K` A / D / G (open architect, send message, approve gate) are unchanged. +- **Workspace sidebar gained Spawn Builder and New Shell** rows, next to Open Architect and Open Web Interface. +- **Gate-pending toasts with one-click Approve.** When a builder reaches a human-approval gate, a toast surfaces it with a per-gate action (View Plan / Run Dev / Review) and an **Approve** button. The approval dialog now shows the issue + gate context. Silence with the new `codev.gateToasts.enabled` setting. +- **Inline review comments on plan/spec files** via VSCode's Comments API — leave `REVIEW(@architect):` threads in the editor, not only the text snippet. +- **PIR protocol support** — PIR in the Spawn Builder picker, a View Plan action for PIR builders, and context-menu actions scoped per protocol. +- **Open Worktree in New Window** — opens `.builders/<id>/` as its own VSCode window. Replaces the former "Codev: View Diff" (3.0.3), which couldn't reliably render multi-file worktree diffs; a real worktree window gives native SCM + diffs. +- **Builder right-click menu reordered** — primary actions first (open terminal, approve), then worktree actions, then dev actions, grouped so the common ones are at the top. +- **"Needs Attention" is merged into Builders.** Blocked builders are flagged inline in the Builders view (bell icon) instead of a separate section, and gate toasts point at the builder's pane. The standalone Needs Attention view is gone. +- **Live item counts** in the Builders / Pull Requests / Backlog / Recently Closed view titles; Recently Closed gained a refresh button. +- **Backlog is actionable** — "Codev: View Issue" opens an issue in the right pane, assigned-to-me issues are surfaced, and you can click-to-spawn a builder from a backlog issue. +- **Periodic sidebar refresh while visible**, every `codev.overviewRefreshSeconds` (default 60; `0` = event-only, the previous behavior). A shared 30s Tower-side cache throttles GitHub calls across windows. + +### Bug fixes + +- The sidebar survives SSE event bursts without losing state (last-write-wins in the overview cache). +- Builder terminal tabs close automatically on cleanup instead of lingering as dead "Process exited" tabs. +- Tower PTY dimensions sync on terminal open, fixing wrapped/truncated output. +- Gate-pending toast: the issue title is quoted (no longer reads like an error), and the seen-set persists across reloads so a still-blocked builder doesn't re-toast on every window reload. +- State-change actions (Approve Gate, Cleanup Builder, …) now wait for Tower and refresh the sidebar immediately, instead of leaving it briefly stale. +- Approving a gate from the sidebar targets the correct (canonical) gate name and runs `porch approve` in the right working directory — previously it could mis-resolve for renamed/shared gates or non-root worktrees. +- Clicking a backlog issue reuses a single editor group for the preview instead of opening a new split group on every click after the first. +- Team per-member counts (Assigned / Open PRs / the 7-day merged & closed) now show the true totals instead of silently capping at 20 — they read GitHub's search `issueCount` rather than the length of the (20-node-limited) result list. + ## [3.0.4] - 2026-05-13 ### Bug fixes diff --git a/packages/vscode/README.md b/packages/vscode/README.md index 9a9a9c10a..680b66389 100644 --- a/packages/vscode/README.md +++ b/packages/vscode/README.md @@ -4,11 +4,13 @@ Bring Codev's Agent Farm into VS Code — monitor builders, open terminals, appr ## Features -- **Unified Sidebar** — Needs Attention, Builders, Pull Requests, Backlog, Team, and Status in a single pane -- **Native Terminals** — Architect and builder terminals in the editor area with full vertical height +- **Unified Sidebar** — Workspace, Builders, Pull Requests, Backlog, Recently Closed, Team, and Status in a single pane (blocked builders are flagged inline in Builders, with live item counts in the view titles) +- **Native Terminals** — Architect / builder / shell terminals in the editor area with full vertical height; dev servers run in the bottom panel +- **Managed dev servers** — start/stop the dev server for the current workspace or any builder worktree from the sidebar; one runs at a time and swaps on demand +- **Gate review** — a toast with one-click **Approve** when a builder reaches a human-approval gate, plus inline `REVIEW(@architect):` comment threads on plan/spec files - **Live Spawn Notifications** — Get notified (or auto-open a terminal) the moment a new builder starts - **Status Bar** — Connection state, builder count, blocked gates at a glance -- **Command Palette** — Open terminals, send messages, approve gates via keyboard +- **Command Palette & shortcuts** — Open terminals, send messages, approve gates, toggle the sidebar - **Auto-Connect** — Detects Codev workspaces and connects to Tower automatically - **Auto-Start Tower** — Starts Tower if not running (configurable) @@ -32,10 +34,11 @@ Bring Codev's Agent Farm into VS Code — monitor builders, open terminals, appr | Codev | Architect | [#42] [#43] | | (sidebar) | (terminal) | Builder #42 | | | | (terminal) | -| - Attention| | | +| - Workspace| | | | - Builders | Left editor | Right editor | | - PRs | group | group | | - Backlog | | | +| - Recent | | | | - Team | | | | - Status | | | +------------+----------------+----------------+ @@ -48,11 +51,17 @@ Bring Codev's Agent Farm into VS Code — monitor builders, open terminals, appr | Codev: Open Architect Terminal | `Cmd+K, A` | Open the architect terminal in the left editor group | | Codev: Send Message | `Cmd+K, D` | Pick a builder, type a message, send via Tower | | Codev: Approve Gate | `Cmd+K, G` | Approve a blocked builder's gate | +| Toggle Codev Sidebar | `Cmd+Alt+C` / `Ctrl+Alt+C` | Show & focus the Codev sidebar, or close it if it's the active view | | Codev: Open Builder Terminal | | Pick a builder and open its terminal | | Codev: New Shell | | Create a new persistent shell terminal | | Codev: Spawn Builder | | Issue number + protocol + optional branch | | Codev: Cleanup Builder | | Remove a completed builder's worktree | +| Codev: Start / Stop Dev Server (this workspace) | `Cmd/Ctrl+Alt+R` / `Cmd/Ctrl+Alt+S` | Run/stop the dev server for the current workspace (sidebar Workspace rows) | +| Codev: Run / Stop Dev Server | | Run/stop a builder worktree's dev server (builder right-click) | +| Codev: Open Worktree in New Window | | Open `.builders/<id>/` as its own VSCode window | +| Codev: Run Worktree Setup | | Re-apply `worktree.symlinks` + `postSpawn` to an existing worktree | | Codev: Refresh Overview | | Manually refresh sidebar data | +| Codev: Refresh Team | | Re-fetch the Team view's GitHub data | | Codev: Connect Tunnel | | Connect cloud tunnel for remote access | | Codev: Disconnect Tunnel | | Disconnect cloud tunnel | | Codev: Cron Tasks | | List, run, enable, or disable cron tasks | @@ -83,3 +92,6 @@ Whenever a new builder starts (e.g. you ran `afx spawn 42`), the extension can o | `codev.autoConnect` | `true` | Connect to Tower on activation | | `codev.autoStartTower` | `true` | Auto-start Tower if not running | | `codev.autoOpenBuilderTerminal` | `notify` | Behavior on builder-spawned events (`off` / `notify` / `auto`) | +| `codev.overviewRefreshSeconds` | `60` | Auto-refresh Builders/PRs/Backlog/Recently Closed every N seconds while the sidebar is visible (`0` = event-only) | +| `codev.gateToasts.enabled` | `true` | Show a toast when a builder reaches a human-approval gate | +| `codev.telemetry` | `false` | No telemetry collected | From 0988eff7c0fe42a2cee63b236ed7b3780a213583 Mon Sep 17 00:00:00 2001 From: Amr Elsayed <amrmelsayed@users.noreply.github.com> Date: Tue, 19 May 2026 08:20:48 +1000 Subject: [PATCH 63/63] fix(db): reintroduce 'pir' builders.type migration as v10 (local DB) The branch's original "add 'pir' to builders.type CHECK" was v9 in ensureLocalDatabase and collided with main's v9 (Spec 755 multi- architect); the merge took main's v9 and dropped ours. Reintroduce it as v10 in the same local (builders/architect) runner. Drift-robust: derives builders_new from the LIVE builders DDL (rewrites only the table name + injects 'pir' into the type CHECK) so SELECT * cannot column-mismatch regardless of columns earlier/Spec-755 migrations added (e.g. spawned_by_architect). Idempotent: skips the recreate when 'pir' is already present (fresh installs; DBs that ran the old branch v9). Verified: build + 2964 unit tests; synthetic post-v9/pre-pir upgrade test (drift columns/rows/indexes/trigger preserved, pir accepted, bogus rejected, idempotent). --- packages/codev/src/agent-farm/db/index.ts | 36 +++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/codev/src/agent-farm/db/index.ts b/packages/codev/src/agent-farm/db/index.ts index 3ef9fb78e..6689309be 100644 --- a/packages/codev/src/agent-farm/db/index.ts +++ b/packages/codev/src/agent-farm/db/index.ts @@ -431,6 +431,42 @@ function ensureLocalDatabase(): Database.Database { db.prepare('INSERT INTO _migrations (version) VALUES (9)').run(); } + // Migration v10: Add 'pir' to builders.type CHECK constraint (Issue 691). + // Reintroduced post main-merge (the original collided with main's v9). + // Drift-robust: derive builders_new from the LIVE builders DDL (only the + // table name + the type CHECK literal are rewritten), so `SELECT *` can + // never column-mismatch regardless of columns earlier migrations added + // (e.g. v9's spawned_by_architect). Idempotent: skips if 'pir' is already + // in the constraint (covers fresh installs and DBs that ran the old v9). + const v10 = db.prepare('SELECT version FROM _migrations WHERE version = 10').get(); + if (!v10) { + const builders = db + .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='builders'") + .get() as { sql: string } | undefined; + + if (builders?.sql && !builders.sql.includes("'pir'")) { + const newSql = builders.sql + .replace(/^CREATE TABLE\s+(?:IF NOT EXISTS\s+)?["'`]?builders["'`]?/i, 'CREATE TABLE builders_new') + .replace(/(CHECK\s*\(\s*type\s+IN\s*\([^)]*)\)/i, "$1, 'pir')"); + db.exec(` + ${newSql}; + INSERT INTO builders_new SELECT * FROM builders; + DROP TABLE builders; + ALTER TABLE builders_new RENAME TO builders; + CREATE INDEX IF NOT EXISTS idx_builders_status ON builders(status); + CREATE INDEX IF NOT EXISTS idx_builders_port ON builders(port); + CREATE TRIGGER IF NOT EXISTS builders_updated_at + AFTER UPDATE ON builders + FOR EACH ROW + BEGIN + UPDATE builders SET updated_at = datetime('now') WHERE id = NEW.id; + END; + `); + console.log("[info] Migrated builders table: added 'pir' to type CHECK constraint (v10)"); + } + db.prepare('INSERT INTO _migrations (version) VALUES (10)').run(); + } + return db; }