diff --git a/AGENTS.md b/AGENTS.md index b7b8bda03..55d82b31d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,10 @@ Codev resolves protocol files, prompts, agent definitions, and roles through a f **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. +### Framework files in prompts: deliver them, don't make the builder read them by path + +Framework files (protocol/role docs, the shipped `codev/resources/` reference docs) default to the package skeleton (see File Resolution above) and aren't guaranteed on disk in a fresh project. So when authoring any builder-facing prompt, role doc, or instruction, don't tell the builder to read a framework file by literal `codev/...` path — that bypasses the resolver and fails in fresh installs. Deliver the content instead (`protocol.md` is inlined into the spawn prompt; per-phase prompts and their templates arrive via porch). Mentioning a `codev/...` path in prose for orientation is fine — the rule is about *fetching*, not *referencing*. (`codev/resources/arch.md` and `codev/resources/lessons-learned.md` are user-evolved files, not framework files, so referencing those by path is correct.) + ### 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: diff --git a/CLAUDE.md b/CLAUDE.md index b7b8bda03..55d82b31d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,10 @@ Codev resolves protocol files, prompts, agent definitions, and roles through a f **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. +### Framework files in prompts: deliver them, don't make the builder read them by path + +Framework files (protocol/role docs, the shipped `codev/resources/` reference docs) default to the package skeleton (see File Resolution above) and aren't guaranteed on disk in a fresh project. So when authoring any builder-facing prompt, role doc, or instruction, don't tell the builder to read a framework file by literal `codev/...` path — that bypasses the resolver and fails in fresh installs. Deliver the content instead (`protocol.md` is inlined into the spawn prompt; per-phase prompts and their templates arrive via porch). Mentioning a `codev/...` path in prose for orientation is fine — the rule is about *fetching*, not *referencing*. (`codev/resources/arch.md` and `codev/resources/lessons-learned.md` are user-evolved files, not framework files, so referencing those by path is correct.) + ### 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: diff --git a/codev-skeleton/protocols/air/builder-prompt.md b/codev-skeleton/protocols/air/builder-prompt.md index 963d5e5fe..f17ab40ad 100644 --- a/codev-skeleton/protocols/air/builder-prompt.md +++ b/codev-skeleton/protocols/air/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the AIR protocol: `codev/protocols/air/protocol.md` +Follow the AIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -73,3 +73,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the AIR protocol 2. Review the issue details 3. Implement the feature + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/aspir/builder-prompt.md b/codev-skeleton/protocols/aspir/builder-prompt.md index 4af70ce4f..d303da59e 100644 --- a/codev-skeleton/protocols/aspir/builder-prompt.md +++ b/codev-skeleton/protocols/aspir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the ASPIR protocol: `codev/protocols/aspir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the ASPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -111,3 +110,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the protocol document thoroughly 2. Review the spec and plan (if available) 3. Begin implementation following the protocol phases + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/aspir/prompts/plan.md b/codev-skeleton/protocols/aspir/prompts/plan.md index 3549654c5..2c12250dd 100644 --- a/codev-skeleton/protocols/aspir/prompts/plan.md +++ b/codev-skeleton/protocols/aspir/prompts/plan.md @@ -74,41 +74,9 @@ After completing the plan draft, signal completion. Porch will run 3-way consult ## Output -Create the plan file at `codev/plans/{{artifact_name}}.md`. +Create the plan file at `codev/plans/{{artifact_name}}.md`, following the template below: -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. - -### Plan Structure - -```markdown -# Implementation Plan: {{title}} - -## Overview -Brief summary of what will be implemented. - -## Phases - -### Phase 1: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files to create/modify] -- **Dependencies**: None -- **Success Criteria**: [How to verify completion] -- **Tests**: [What tests will be written] - -### Phase 2: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files] -- **Dependencies**: Phase 1 -- **Success Criteria**: [Verification method] -- **Tests**: [Test approach] - -[Continue for all phases...] - -## Risk Assessment -- [Risk 1]: [Mitigation] -- [Risk 2]: [Mitigation] - -``` +{{> protocols/spir/templates/plan.md}} ## Signals diff --git a/codev-skeleton/protocols/bugfix/builder-prompt.md b/codev-skeleton/protocols/bugfix/builder-prompt.md index 3f56828bb..aefa35a25 100644 --- a/codev-skeleton/protocols/bugfix/builder-prompt.md +++ b/codev-skeleton/protocols/bugfix/builder-prompt.md @@ -25,7 +25,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the BUGFIX protocol: `codev/protocols/bugfix/protocol.md` +Follow the BUGFIX protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if issue}} ## Issue #{{issue.number}} @@ -67,3 +67,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the BUGFIX protocol 2. Review the issue details 3. Reproduce the bug before fixing + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/bugfix/protocol.md b/codev-skeleton/protocols/bugfix/protocol.md new file mode 100644 index 000000000..29fb7ed5b --- /dev/null +++ b/codev-skeleton/protocols/bugfix/protocol.md @@ -0,0 +1,78 @@ +# BUGFIX Protocol + +> Lightweight, issue-driven protocol for minor bug fixes. **Investigate → Fix → PR**, with a single `pr` gate before merge. No spec or plan artifacts: the GitHub issue is the spec, and the review goes in the PR body. + +## When to Use + +Use BUGFIX when a bug is reported as a GitHub Issue and: + +- The reproduction is clear (or inferable) and the root cause is isolated +- The fix is small (guideline: < 300 LOC net diff) and contained to one area +- No architectural changes or new design decisions are needed + +Escalate to **SPIR** (or another heavier protocol) instead when: + +- It is actually a feature request, not a bug +- The root cause reveals a deeper architectural issue +- The fix needs design review, spans multiple components, or clearly exceeds ~300 LOC + +## Phases + +``` +investigate → fix → pr +``` + +### Investigate + +Read the issue, reproduce the bug, and identify the root cause. Confirm the fix fits BUGFIX scope. If it does not, signal `BLOCKED` and recommend escalation to the architect (`afx send architect "..."`). No code in this phase. + +### Fix + +Apply the minimal change that resolves the root cause, and add a regression test that fails without the fix and passes with it. Keep it focused: do not refactor surrounding code, do not fix unrelated bugs (file separate issues), do not add features. Run the build and tests (porch's `checks` block runs `npm run build` and `npm test`). + +Commit with the issue-driven format: + +``` +[Bugfix #] Fix: +[Bugfix #] Test: +``` + +### PR (gated by `pr`) + +1. Push the branch and open a PR with `gh pr create`. The body includes Summary, Root Cause, Fix, and Test Plan, plus `Fixes #` so the issue auto-closes on merge. +2. Run a multi-agent CMAP review on the PR (Gemini, Codex, Claude) and record each verdict. Address or rebut any `REQUEST_CHANGES`; add a regression test if a real defect surfaced. +3. Notify the architect: `afx send architect "PR # ready for review (fixes #). CMAP: gemini=..., codex=..., claude=..."`. +4. Run `porch done ` to request the `pr` gate, then wait. **The merge is gated by porch state, never by typed prose in your pane.** +5. The human reviews the PR and the CMAP results on GitHub, then approves the gate: `porch approve pr --a-human-explicitly-approved-this`. +6. porch wakes the builder with a merge task. Merge with `gh pr merge --merge` (do **not** pass `--delete-branch`: the builder is checked out on this branch in a worktree), then run `porch done ` and notify the architect that it is merged and ready for cleanup. + +## Gate + +BUGFIX has one human gate, `pr`, on the merge step. It exists so the merge trigger is structured porch state (approved or not), not free-text typed into the builder's pane. This eliminates the self-merge bug class: a builder cannot infer authorization from ambiguous input. + +## Multi-Agent Consultation + +A single CMAP pass at the PR (Gemini, Codex, Claude). There is no per-phase consultation: the issue is the spec and the fix is small, so review effort concentrates on the final PR. + +## Scope + +The < 300 LOC threshold is a **guideline**, measured as net diff (additions + deletions) anchored at the merge-base with the default branch. A well-contained 350-LOC fix is fine; a 200-LOC fix smeared across ten files may warrant escalation. + +## Escalation + +If, mid-fix, the change outgrows BUGFIX (architectural impact, multiple components, unclear root cause after investigation, or more than ~300 LOC), notify the architect with specifics and recommend escalating to SPIR. Do not silently expand scope. + +## Branch Naming + +``` +builder/bugfix-- +``` + +## Edge Cases + +| Scenario | Action | +|---|---| +| Cannot reproduce | Document the attempts in an issue comment, ask the reporter for detail, notify the architect | +| Fix outgrows scope (architectural / multi-component / > ~300 LOC) | Notify the architect, recommend escalation; do not proceed | +| Unrelated test failures | Out of scope: note them for the architect, do not fix them here | +| Multiple bugs in one issue | Fix only the primary bug; file separate issues for the rest | diff --git a/codev-skeleton/protocols/experiment/builder-prompt.md b/codev-skeleton/protocols/experiment/builder-prompt.md index 23f378975..31c5581e5 100644 --- a/codev-skeleton/protocols/experiment/builder-prompt.md +++ b/codev-skeleton/protocols/experiment/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the EXPERIMENT protocol: `codev/protocols/experiment/protocol.md` +Follow the EXPERIMENT protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## EXPERIMENT Overview The EXPERIMENT protocol ensures disciplined experimentation: @@ -74,3 +74,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the EXPERIMENT protocol document 2. Define your hypothesis clearly 3. Follow the phases in order + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/experiment/protocol.md b/codev-skeleton/protocols/experiment/protocol.md index 5dd71b271..2e53487d4 100644 --- a/codev-skeleton/protocols/experiment/protocol.md +++ b/codev-skeleton/protocols/experiment/protocol.md @@ -37,7 +37,7 @@ mkdir -p experiments/1_experiment_name cd experiments/1_experiment_name # Initialize notes.md from template -cp codev/protocols/experiment/templates/notes.md notes.md +touch notes.md # then fill it from the embedded template at the end of this protocol ``` Or ask your AI assistant: "Create a new experiment for [goal]" @@ -85,33 +85,6 @@ git add experiments/1_experiment_name/ git commit -m "[Experiment 1] Brief description of findings" ``` -## notes.md Template - -See `templates/notes.md` for the full template. Key sections: - -```markdown -# Experiment ####: Name - -**Status**: In Progress | Complete | Disproved | Aborted - -**Date**: YYYY-MM-DD - -## Goal -What are you trying to learn? - -## Time Investment -Wall clock time vs active developer time - -## Code -- [experiment.py](experiment.py) - Brief description - -## Results -What happened? What did you learn? - -## Next Steps -What should be done based on findings? -``` - ## Best Practices ### Keep It Simple @@ -222,3 +195,9 @@ Determine if Redis caching improves API response times for repeated queries. ## Next Steps Create SPIR spec for production caching implementation. ``` + +## Template: notes.md + +Create `notes.md` with the following content: + +{{> protocols/experiment/templates/notes.md}} diff --git a/codev-skeleton/protocols/maintain/builder-prompt.md b/codev-skeleton/protocols/maintain/builder-prompt.md index 72394e849..6a2deda0d 100644 --- a/codev-skeleton/protocols/maintain/builder-prompt.md +++ b/codev-skeleton/protocols/maintain/builder-prompt.md @@ -23,7 +23,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the MAINTAIN protocol: `codev/protocols/maintain/protocol.md` +Follow the MAINTAIN protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## MAINTAIN Overview @@ -54,3 +54,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 2. Run `porch next` to get your first task 3. Work through audit → clean → sync → verify in a single pass 4. Document everything in the maintenance run file + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 0edd1d992..86016f310 100644 --- a/codev-skeleton/protocols/pir/builder-prompt.md +++ b/codev-skeleton/protocols/pir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the PIR protocol: `codev/protocols/pir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the PIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review @@ -87,6 +86,12 @@ If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch ## Getting Started -1. Read the PIR protocol document (`codev/protocols/pir/protocol.md`) +1. Read the PIR protocol (provided inline in this prompt). 2. Run `porch next {{project_id}}` to see what to do next 3. Begin work + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/research/builder-prompt.md b/codev-skeleton/protocols/research/builder-prompt.md index ae501326e..088262853 100644 --- a/codev-skeleton/protocols/research/builder-prompt.md +++ b/codev-skeleton/protocols/research/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the RESEARCH protocol: `codev/protocols/research/protocol.md` +Follow the RESEARCH protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## RESEARCH Overview @@ -83,3 +83,9 @@ wait 2. Understand the research question from the architect 3. Write the research brief (Phase 1) 4. Wait for scope-approval before proceeding to investigation + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/spike/builder-prompt.md b/codev-skeleton/protocols/spike/builder-prompt.md index 8d8a97dfb..6d30dc649 100644 --- a/codev-skeleton/protocols/spike/builder-prompt.md +++ b/codev-skeleton/protocols/spike/builder-prompt.md @@ -11,7 +11,7 @@ You are running in SOFT mode. This means: {{/if}} ## Protocol -Follow the SPIKE protocol: `codev/protocols/spike/protocol.md` +Follow the SPIKE protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if task}} ## Spike Question @@ -60,3 +60,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the SPIKE protocol document 2. Understand the question you're investigating 3. Start with research — don't jump straight to code + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/spike/protocol.md b/codev-skeleton/protocols/spike/protocol.md index b8c1ec0f6..764a0bea6 100644 --- a/codev-skeleton/protocols/spike/protocol.md +++ b/codev-skeleton/protocols/spike/protocol.md @@ -52,7 +52,7 @@ The following 3-step workflow is **guidance only** — not enforced by porch. Fo ### Step 3: Findings - Write the findings document at `codev/spikes/-.md` -- Use the template: `codev/protocols/spike/templates/findings.md` +- Use the embedded template at the end of this protocol - Provide a clear feasibility verdict - Commit and notify the architect @@ -120,3 +120,9 @@ When a spike finds something is not feasible: 1. Document clearly in findings 2. Close the related GitHub issue with a link to findings 3. The findings become institutional knowledge + +## Template: findings.md + +Write the findings document using the following template: + +{{> protocols/spike/templates/findings.md}} diff --git a/codev-skeleton/protocols/spir/builder-prompt.md b/codev-skeleton/protocols/spir/builder-prompt.md index 149c531e5..437287968 100644 --- a/codev-skeleton/protocols/spir/builder-prompt.md +++ b/codev-skeleton/protocols/spir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the SPIR protocol: `codev/protocols/spir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the SPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -112,3 +111,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the protocol document thoroughly 2. Review the spec and plan (if available) 3. Begin implementation following the protocol phases + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev-skeleton/protocols/spir/prompts/plan.md b/codev-skeleton/protocols/spir/prompts/plan.md index 3549654c5..2c12250dd 100644 --- a/codev-skeleton/protocols/spir/prompts/plan.md +++ b/codev-skeleton/protocols/spir/prompts/plan.md @@ -74,41 +74,9 @@ After completing the plan draft, signal completion. Porch will run 3-way consult ## Output -Create the plan file at `codev/plans/{{artifact_name}}.md`. +Create the plan file at `codev/plans/{{artifact_name}}.md`, following the template below: -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. - -### Plan Structure - -```markdown -# Implementation Plan: {{title}} - -## Overview -Brief summary of what will be implemented. - -## Phases - -### Phase 1: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files to create/modify] -- **Dependencies**: None -- **Success Criteria**: [How to verify completion] -- **Tests**: [What tests will be written] - -### Phase 2: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files] -- **Dependencies**: Phase 1 -- **Success Criteria**: [Verification method] -- **Tests**: [Test approach] - -[Continue for all phases...] - -## Risk Assessment -- [Risk 1]: [Mitigation] -- [Risk 2]: [Mitigation] - -``` +{{> protocols/spir/templates/plan.md}} ## Signals diff --git a/codev-skeleton/protocols/spir/protocol.md b/codev-skeleton/protocols/spir/protocol.md index 188c2bda1..c4b6643fd 100644 --- a/codev-skeleton/protocols/spir/protocol.md +++ b/codev-skeleton/protocols/spir/protocol.md @@ -4,7 +4,6 @@ > > Each phase has one build-verify cycle with 3-way consultation. -> **Quick Reference**: See `codev/resources/workflow-reference.md` for stage diagrams and common commands. ## Prerequisites @@ -164,7 +163,7 @@ When filing an issue for SPIR, you can pin architectural decisions you don't wan - All consultation feedback incorporated directly into this document - Include a "Consultation Log" section summarizing key feedback and changes - Version control captures evolution through commits -**Template**: `templates/spec.md` +**Structure**: developed through the specify phase **Review Required**: Yes - Human approval AFTER consultations ### P - Plan (Structured Decomposition) @@ -243,7 +242,7 @@ Each phase should be: - Include phase status tracking within this document - **DO NOT include time estimates** - Focus on deliverables and dependencies, not hours/days - Version control captures evolution through commits -**Template**: `templates/plan.md` +**Structure**: follows the plan template provided by the plan phase **Review Required**: Yes - Technical lead approval AFTER consultations ### I - Implement (Per Plan Phase) @@ -642,7 +641,7 @@ spir/1-user-authentication/database-schema ## Templates -Templates for each phase are available in the `templates/` directory: +Each phase has a template that ships in the package skeleton; the phase prompts deliver the structure you need, so you do not fetch these files directly: - `spec.md` - Specification template - `plan.md` - Planning template (includes phase status tracking) - `review.md` - Review and lessons learned template diff --git a/codev-skeleton/roles/builder.md b/codev-skeleton/roles/builder.md index aa01346ec..4f49ded80 100644 --- a/codev-skeleton/roles/builder.md +++ b/codev-skeleton/roles/builder.md @@ -79,8 +79,8 @@ In soft mode, you follow the protocol document yourself. The architect monitors cat codev/specs/XXXX-*.md cat codev/plans/XXXX-*.md -# Read the protocol -cat codev/protocols/spir/protocol.md +# (The full protocol text is inlined in your spawn prompt under the +# "## Protocol Reference (full text)" heading; no need to fetch it.) # Start implementing ``` diff --git a/codev/plans/1011-agent-farm-inline-protocol-md-.md b/codev/plans/1011-agent-farm-inline-protocol-md-.md new file mode 100644 index 000000000..110e9e278 --- /dev/null +++ b/codev/plans/1011-agent-farm-inline-protocol-md-.md @@ -0,0 +1,215 @@ +# PIR Plan: Deliver framework files via resolver-aware channels (fresh-install class fix) + +> **Scope history (2026-06-08):** #1011 grew from "inline protocol.md at spawn" → a +> framework-file class fix → its current **three-layer** form (Delivery / Cleanup / +> Enforcement). This plan tracks the three-layer body. Patch 1 (Layer 1 / A.1) is already +> implemented and at the `dev-approval` gate; this revision folds in the rest. `codev read` +> CLI is **explicitly rejected** — not proposed. Project bootstrap of `codev/resources/` is +> **#1012** (out of scope here). + +## Understanding + +Spec 618 moved framework files into the package skeleton (resolver tier 4). `resolveCodevFile` +(`skeleton.ts:63`) reaches them. The bug is **consumer-side**: prompts, role docs, and protocol +docs reference framework files by **literal path**, which a raw shell `cat`/`cp` can't resolve +when the file lives only in the embedded skeleton (fresh post-618 installs). The builder hits +"No such file" and wastes turns. Three sub-instances: A.1 (protocol.md from 9 builder-prompts + +`roles/builder.md:83` cat), A.2 (4 template refs), A.3 (workflow-reference inside `spir/protocol.md`). + +### Investigation findings that drive the decisions + +1. **A.2 references are heterogeneous** — and the `spir`/`aspir` plan case was initially + mis-read. The plan prompt's inline `### Plan Structure` was a *simpler, JSON-less* layout, so + I first treated the `templates/plan.md` pointer as redundant chrome and dropped it. **That was + wrong**: porch's plan gate requires the **machine-readable phases JSON** (`has_phases_json` + + `min_two_phases`), which lives *only* in `templates/plan.md` — so the pointer was load-bearing. + Corrected by delivering `templates/plan.md` via a `{{> }}` include (see decision #2). The + `experiment` (`notes.md`, 97L) and `spike` (`findings.md`, 67L) templates are likewise genuine + content delivered via include. +2. **`bugfix` ships no `protocol.md` in the skeleton** (only `protocol.json` + prompts), yet its + `builder-prompt.md:28` references one, and the local `codev/` tree carries a real **548-line** + `bugfix/protocol.md` that was *never tracked in the skeleton*. So Patch 1 cannot inline for + bugfix, and after Layer 2 drops the pointer, bugfix would have **no meta-doc at all** in fresh + installs. See "Open sub-decision" below — this needs an explicit call. +3. **Embedded `notes.md`/`findings.md` contain zero `{{`**, so inlining them through + `renderTemplate` (they ride inside `experiment`/`spike` `protocol.md`) is collision-safe. +4. **`experiment`/`spike` default to `mode: soft`** (verified in `protocol.json`: every phase is + `prompt: None`, `prompts/` is empty). Soft mode has no porch phase orchestration — the builder + follows `protocol.md` directly — so `protocol.md` is their *only* guidance channel. This is the + reason templates are injected into `protocol.md` for these two (and not for the strict, + phase-prompt-driven protocols). It also means "no phase prompts" is by-design, not a defect. + +## Locked Decisions (the 5 plan-gate decisions) + +**1 — Delivery: fresh-at-delivery placeholder substitution (NOT a committed copy).** +*(Revised at the dev-approval gate: the earlier static-embed committed a duplicate that would go +stale; the reviewer rejected it.)* +- protocol.md: the builder-prompt `## Protocol` section keeps "Follow the X protocol. Read and + internalize the protocol before starting any work." and references the full text below. A + `{{protocol_reference}}` placeholder near the end of the prompt is filled **fresh at spawn** by + reading `protocol.md` through the resolver, so nothing is committed into `builder-prompt.md` (no + stale copy). The reference is **unconditional** (no `{{#if}}` guard): every shipped protocol + ships a `protocol.md` (bugfix got one in #1013), and a unit test enforces that invariant so a + future `protocol.json`-only protocol fails CI rather than rendering an empty section. (An earlier + iteration wrapped it in `{{#if protocol_reference}}`; removed once the completeness invariant was + made explicit — the guard was always-true for the shipped set and read as dead code.) +- Templates (experiment/spike): the `## Template:` section in `protocol.md` carries a + `{{> protocols//templates/}}` include directive, resolved fresh (recursively, while + building `{{protocol_reference}}`). The canonical `templates/*.md` stays single-source. + +**2 — A.2 mechanism: fresh-at-delivery `{{> }}` include, on BOTH delivery channels.** +Every genuine template is delivered via a `{{> path}}` include resolved fresh by the shared +`resolveCodevIncludes` (in `lib/skeleton.ts`), which now runs in **both** channels: +- **spawn** — `resolveProtocolReference` resolves includes inside `protocol.md` (experiment/spike + `## Template:` sections → `notes.md` / `findings.md`). +- **porch phase prompts** — `loadPromptFile` now resolves includes too, so `spir`/`aspir` + `prompts/plan.md` use `{{> protocols/spir/templates/plan.md}}` to deliver the canonical plan + template (with its required machine-readable phases JSON) fresh at the plan phase. + +The earlier "drop the spir/aspir plan pointer; the inline `### Plan Structure` is self-contained" +call was **reverted** — that inline block lacked the porch-required JSON (finding #1). The +divergent inline `### Plan Structure` is removed; the canonical `templates/plan.md` is the single +source, delivered via the include. Auto-detect (blanket scan) is still rejected; the include is +explicit (the author marks exactly what to inline), single-source, and never a committed copy. + +**Why inject for experiment/spike but not spir/aspir — the distinction is `mode`, not ad-hoc +(finding #4):** `experiment` and `spike` default to **`mode: soft`** (verified in their +`protocol.json`; every phase is `prompt: None`, `prompts/` is empty). Soft mode has **no porch +phase-by-phase orchestration** — the builder follows `protocol.md` directly — so `protocol.md` +is the *only* guidance channel and the template structure must ride it. The `{{> }}` include is +therefore the correct delivery for a soft-mode protocol (fresh, no stale committed copy). By +contrast `spir`/`aspir`/`bugfix` are **strict** (porch-orchestrated): their phase prompts carry +the structure, so injecting a template would double-deliver — hence we drop the dead pointer +instead. Net: strict → phase prompts carry structure (don't inject); soft → `protocol.md` is +everything (inject). This also retires the earlier "give experiment/spike phase prompts" idea: +phase prompts are a strict-mode concept; soft-mode protocols correctly live in `protocol.md`, so +no such follow-up is needed. + +**Bug impact for experiment/spike (was real, now fixed):** before this work, a soft-mode +experiment/spike builder following `protocol.md` in a fresh install hit a dead template path +(`cp codev/protocols/experiment/templates/notes.md …` / "use the template at `…/findings.md`") — +the file isn't on disk. The `{{> }}` injection delivers the template content inline at spawn, so +the dead path / failed `cp` is gone. These protocols are fixed by this PR, not left broken. + +**3 — A.3 disposition: Option 2 (strip). [agree]** +Remove the `> Quick Reference: See codev/resources/workflow-reference.md …` line from +`spir/protocol.md`. Informational chrome; zero-risk removal. `roles/architect.md:5` stays out. + +**4 — Missing-framework-file behavior: silently skip + `logger.debug` (no stderr warn).** +The skip is a **routine, by-design state**: `bugfix` has no skeleton `protocol.md`, so Patch 1's +resolve returns null and skips on every bugfix spawn (`validateProtocol` only `fatal()`s when +*both* json and md are absent). A stderr warn would fire on every bugfix spawn about an +intentionally-absent file. A.2 = embed (no runtime resolution to fail). Skip + debug it is. + +**5 — Doctor check: warn-not-error, scans the WORKSPACE `codev/` overrides. [revised — was +"skeleton-only"]** +`codev doctor` is an end-user tool, so its check targets what the *user* controls and could +break: their local `codev/protocols` and `codev/roles` overrides. It scans the workspace `codev/` +dir (not the global package skeleton) for shell-fetch verbs reading `codev/(protocols|roles)/…md`, +reports `status: 'warn'` (never `'fail'`), and is a no-op when the project has no overrides. +*Correction:* the original "skeleton-only" framing was wrong — auditing the shipped package +skeleton from an end-user tool is pointless (the user can't fix it, and it's already guaranteed +clean by the framework's own CI). The shipped-skeleton guard lives in the **unit test** +(`framework-ref-audit.test.ts` scans `codev-skeleton/`, runs in CI); `codev doctor` guards the +user's overrides. This matches the architect's original Layer 3 intent ("grep … and any local +`codev/` directories"). Relative-path refs and markdown-link cross-references (`templates/x.md`, +the intentional `aspir → spir` link) are a lower-severity class **not** targeted by the +shell-fetch grep — handled by manual audit where they mattered, left where intentional. + +## bugfix + experiment doc hygiene — RESOLVED in this PR (was #1013, now folded in) + +`bugfix` was the one protocol whose `protocol.md` wasn't in the skeleton (`codev/` had a tracked +548-line copy, never shipped). Worse, that copy was **stale** vs porch orchestration (a manual +`afx send "Merge it"` merge handshake, a manual architect CMAP, and the deprecated projectlist +section) — and Layer 1 now reliably **inlines** `protocol.md` into the builder's prompt wherever +it resolves, so the stale content would land in bugfix builders' context (in this repo, and in +any project that has a bugfix `protocol.md`). That is a correctness risk, not just hygiene. + +Resolution (folded in per "implement #1013 so the whole bug ships together"): +- **Rewrote `bugfix/protocol.md`** as a concise (~78-line), porch-accurate doc grounded in the + real flow (`protocol.json` phases + the `pr` gate; `prompts/{investigate,fix,pr}.md`): the merge + is gated by the `pr` gate (builder runs CMAP → `porch done` → human approves the gate → porch + emits the merge task), not a manual handshake. Dropped the projectlist section and fixed the + branch-naming inconsistency. **Shipped it to the skeleton**, so fresh-install bugfix builders + get a correct meta-doc (consistent with the other protocols) via Layer 1. +- **Removed `experiment/protocol.md`'s `## notes.md Template` partial copy** (a relative-ref + duplicate of `notes.md`); the `## Template: notes.md` fresh-include the delivery layer already + resolves makes it redundant. No committed template copy remains. + +(This also retires the latent mixed-audience concern: every protocol now ships a `protocol.md`, +and bugfix's is concise/builder-relevant rather than a stale architect-facing dump.) + +## Proposed Change (three layers) + +**Layer 1 — Delivery.** +- *Patch 1 (A.1, done):* `loadBuilderPromptTemplate()` inlines `protocol.md` under the delimiter. +- *Patch 2 (A.2):* Option B embeds (per decision 2) — markdown only, 0 LOC code. + +**Layer 2 — Cleanup (skeleton sweep).** +- Drop the `Follow the protocol: \`…protocol.md\`` line from all 9 `builder-prompt.md` + (PIR has two refs: `:30` and the `:90` getting-started line — both go; keep PIR's 3-phase + breakdown). The protocol text is already in context via Patch 1. +- `roles/builder.md:83`: replace the `cat codev/protocols/spir/protocol.md` example with a note + that the protocol is inlined in the spawn prompt under `## Protocol Reference`. +- A.2 literal paths leave the four prompts by construction (decision 2). +- A.3: strip per decision 3. + +**Layer 3 — Enforcement.** +- *Convention doc:* add a "Framework files: never reference by literal `codev/…` path" section to + **both** `AGENTS.md` and `CLAUDE.md` (kept in sync, per repo policy). +- *Doctor audit:* add a check to `doctor()` (`packages/codev/src/commands/doctor.ts:539`) mirroring + the existing `CheckResult`/`printStatus` pattern (decision 5 semantics). + +### Tree scope +Edit **both** `codev-skeleton/` (shipped source; required for the fix + repro test) and the +local `codev/` copies (this repo dogfoods; its `codev/` shadows the skeleton). Patch 1 needs no +markdown edits in either tree. + +## Files to Change + +- `packages/codev/src/agent-farm/commands/spawn-roles.ts` (+ `__tests__/spawn-roles.test.ts`) — Patch 1 (done). +- `{codev-skeleton,codev}/protocols/{spir,aspir}/prompts/plan.md` — drop redundant template pointer (Patch 2 + Layer 2). +- `{codev-skeleton,codev}/protocols/{experiment,spike}/protocol.md` — `{{> ...}}` template include, reword (Patch 2). +- `{codev-skeleton,codev}/protocols/*/builder-prompt.md` (all 9) — drop protocol.md pointer; restore "Read and internalize"; add `{{protocol_reference}}` placeholder (Layer 1/2). +- `{codev-skeleton,codev}/roles/builder.md` — fix the cat example (Layer 2). +- `{codev-skeleton,codev}/protocols/spir/protocol.md` — strip A.3 line (Layer 2). +- `AGENTS.md`, `CLAUDE.md` — convention section (Layer 3). +- `packages/codev/src/commands/doctor.ts` (+ `src/__tests__/doctor.test.ts`) — audit check (Layer 3). +- `{codev-skeleton,codev}/protocols/bugfix/protocol.md` — **new in skeleton**; rewritten concise + porch-accurate (was #1013, folded in). +- `{codev-skeleton,codev}/protocols/experiment/protocol.md` — remove the redundant `## notes.md Template` partial copy (was #1013, folded in). + +## Risks & Alternatives Considered + +- **Drift:** eliminated by design — templates and protocol.md are delivered via fresh-at-spawn + placeholder substitution (`{{protocol_reference}}` / `{{> ...}}`), so no duplicate copy is + committed to drift. A test asserts the include placeholder is present and no static embed + remains. (Pre-existing, out-of-scope: `experiment/protocol.md` already has a `## notes.md + Template` section quoting part of `notes.md` via a relative ref — noted, not touched.) +- **Doctor false positives:** scoping the grep to absolute `codev/…` paths (decision 5) keeps it + aligned with the sweep; relative refs intentionally not matched. +- **Auto-detect (Patch 2 = A):** rejected — double-delivers a conflicting plan layout (finding #1). +- **`codev read` CLI / restore copying / per-`next` injection:** rejected per the issue's table. +- **Residual cat:** eliminated for protocol.md — Layer 2 removes the pointer outright (the earlier + "leave the instruction" stance is reversed by Layer 2). + +## Test Plan + +**Automated (`npm test` → `@cluesmith/codev`):** +- Delivery (new): `buildPromptFromTemplate` fills `{{protocol_reference}}` from protocol.md fresh; + omits cleanly when absent; resolves `{{> ...}}` template includes recursively. +- A.2 guard (new): `spir`/`aspir` `plan.md` no longer reference the template path and still carry + `### Plan Structure`; `experiment`/`spike` `protocol.md` carry the `{{> ...}}` include and no + static embed. +- Layer 2 guard (new): no `builder-prompt.md` references `protocol.md`; `roles/builder.md` has no + `cat codev/protocols/…`; `spir/protocol.md` has no `workflow-reference.md`. +- Layer 3 audit (new, in `framework-ref-audit.test.ts`): flags a shell `cat`/`cp` of a framework + path (positive), ignores doc references / `codev/resources` / user files (negative), the real + `codev-skeleton/` source is clean (CI/source guard), and a codev root with no protocol/role + overrides is a no-op (the `codev doctor` workspace case). + +**Build:** `npm run build` from worktree root. + +**Manual (dev-approval, load-bearing — issue repro):** fresh `codev init` in a tmp dir, spawn a +test builder, run plan + implement + review; confirm (a) no file-hunting for protocol.md OR +templates, (b) per-phase prompts still followed (inlined material not "louder"), (c) templates +land in the right phase, (d) `codev doctor` catches a deliberately-introduced literal-path ref. diff --git a/codev/projects/1011-agent-farm-inline-protocol-md-/status.yaml b/codev/projects/1011-agent-farm-inline-protocol-md-/status.yaml new file mode 100644 index 000000000..38582fac1 --- /dev/null +++ b/codev/projects/1011-agent-farm-inline-protocol-md-/status.yaml @@ -0,0 +1,29 @@ +id: '1011' +title: agent-farm-inline-protocol-md- +protocol: pir +phase: review +plan_phases: [] +current_plan_phase: null +gates: + plan-approval: + status: approved + requested_at: '2026-06-08T02:31:06.250Z' + approved_at: '2026-06-08T02:52:55.657Z' + dev-approval: + status: approved + requested_at: '2026-06-08T02:56:40.545Z' + approved_at: '2026-06-09T04:53:41.378Z' + pr: + status: pending + requested_at: '2026-06-09T05:24:51.567Z' +iteration: 1 +build_complete: true +history: [] +started_at: '2026-06-08T02:27:23.467Z' +updated_at: '2026-06-09T05:24:51.569Z' +pr_history: + - phase: review + pr_number: 1015 + branch: builder/pir-1011 + created_at: '2026-06-09T05:01:33.143Z' +pr_ready_for_human: true diff --git a/codev/protocols/air/builder-prompt.md b/codev/protocols/air/builder-prompt.md index 963d5e5fe..f17ab40ad 100644 --- a/codev/protocols/air/builder-prompt.md +++ b/codev/protocols/air/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the AIR protocol: `codev/protocols/air/protocol.md` +Follow the AIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -73,3 +73,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the AIR protocol 2. Review the issue details 3. Implement the feature + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/aspir/builder-prompt.md b/codev/protocols/aspir/builder-prompt.md index 475721098..07d02b701 100644 --- a/codev/protocols/aspir/builder-prompt.md +++ b/codev/protocols/aspir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the ASPIR protocol: `codev/protocols/aspir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the ASPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -89,3 +88,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the protocol document thoroughly 2. Review the spec and plan (if available) 3. Begin implementation following the protocol phases + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/aspir/prompts/plan.md b/codev/protocols/aspir/prompts/plan.md index 3549654c5..2c12250dd 100644 --- a/codev/protocols/aspir/prompts/plan.md +++ b/codev/protocols/aspir/prompts/plan.md @@ -74,41 +74,9 @@ After completing the plan draft, signal completion. Porch will run 3-way consult ## Output -Create the plan file at `codev/plans/{{artifact_name}}.md`. +Create the plan file at `codev/plans/{{artifact_name}}.md`, following the template below: -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. - -### Plan Structure - -```markdown -# Implementation Plan: {{title}} - -## Overview -Brief summary of what will be implemented. - -## Phases - -### Phase 1: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files to create/modify] -- **Dependencies**: None -- **Success Criteria**: [How to verify completion] -- **Tests**: [What tests will be written] - -### Phase 2: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files] -- **Dependencies**: Phase 1 -- **Success Criteria**: [Verification method] -- **Tests**: [Test approach] - -[Continue for all phases...] - -## Risk Assessment -- [Risk 1]: [Mitigation] -- [Risk 2]: [Mitigation] - -``` +{{> protocols/spir/templates/plan.md}} ## Signals diff --git a/codev/protocols/bugfix/builder-prompt.md b/codev/protocols/bugfix/builder-prompt.md index ef6834110..bb950cfe4 100644 --- a/codev/protocols/bugfix/builder-prompt.md +++ b/codev/protocols/bugfix/builder-prompt.md @@ -25,7 +25,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the BUGFIX protocol: `codev/protocols/bugfix/protocol.md` +Follow the BUGFIX protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if issue}} ## Issue #{{issue.number}} @@ -67,3 +67,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the BUGFIX protocol 2. Review the issue details 3. Reproduce the bug before fixing + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/bugfix/protocol.md b/codev/protocols/bugfix/protocol.md index f9d4b4bbc..29fb7ed5b 100644 --- a/codev/protocols/bugfix/protocol.md +++ b/codev/protocols/bugfix/protocol.md @@ -1,548 +1,78 @@ # BUGFIX Protocol -**Lightweight protocol for minor bugfixes using GitHub Issues** +> Lightweight, issue-driven protocol for minor bug fixes. **Investigate → Fix → PR**, with a single `pr` gate before merge. No spec or plan artifacts: the GitHub issue is the spec, and the review goes in the PR body. -## Overview +## When to Use -BUGFIX is a streamlined protocol for addressing minor bugs reported as GitHub Issues. It uses the Architect-Builder pattern with isolated worktrees but skips the heavyweight specification and planning phases of SPIR. +Use BUGFIX when a bug is reported as a GitHub Issue and: -**Core Principle**: Bug → Builder → Fix → Review → Merge → Cleanup +- The reproduction is clear (or inferable) and the root cause is isolated +- The fix is small (guideline: < 300 LOC net diff) and contained to one area +- No architectural changes or new design decisions are needed -**When to Use**: -- Minor bugs with clear reproduction steps -- Issues that can be fixed in < 300 lines of code -- No architectural changes required -- Fix is straightforward once the bug is understood +Escalate to **SPIR** (or another heavier protocol) instead when: -**When NOT to Use** (escalate to SPIR or TICK): -- It's a new feature (not a bug) -- Bug reveals deeper architectural issues -- Fix requires > 300 lines of code -- Multiple components need coordinated changes -- Root cause is unclear after investigation +- It is actually a feature request, not a bug +- The root cause reveals a deeper architectural issue +- The fix needs design review, spans multiple components, or clearly exceeds ~300 LOC -## Workflow Overview +## Phases ``` -┌─────────────────────────────────────────────────────────────────────────────┐ -│ BUGFIX PROTOCOL │ -├─────────────────────────────────────────────────────────────────────────────┤ -│ │ -│ ARCHITECT BUILDER │ -│ ───────── ─────── │ -│ │ -│ 1. Identify issue #N │ -│ │ │ -│ ▼ │ -│ 2. afx spawn --task "Fix #N" ──────► 3. Comment "On it..." │ -│ │ │ │ -│ │ ▼ │ -│ │ 4. Investigate & Fix │ -│ │ │ │ -│ │ ┌───────┴───────┐ │ -│ │ │ │ │ -│ │ Too Complex? Simple Fix │ -│ │ │ │ │ -│ │◄─── afx send "Complex" ◄──────┘ │ │ -│ │ ▼ │ -│ │ 5. Create PR + CMAP review │ -│ │ │ │ -│ │◄─────────────────── afx send "PR #M ready" ◄──┘ │ -│ │ │ -│ ▼ │ -│ 6. Review PR + CMAP integration │ -│ │ │ -│ ├───── gh pr comment (feedback) ─────► 7. Address feedback │ -│ │ │ │ -│ │◄─────────────────── afx send "Fixed" ◄────────┘ │ -│ │ │ -│ ▼ │ -│ 8. afx send "Merge it" ──────────────────────► 9. gh pr merge --merge │ -│ │ │ │ -│ │◄─────────────────── afx send "Merged" ◄───────┘ │ -│ │ │ -│ ▼ │ -│ 10. git pull && verify │ -│ │ │ -│ ▼ │ -│ 11. afx cleanup && close issue │ -│ │ -└─────────────────────────────────────────────────────────────────────────────┘ +investigate → fix → pr ``` -## Phase Details +### Investigate -### Phase 1: Issue Identification (Architect) +Read the issue, reproduce the bug, and identify the root cause. Confirm the fix fits BUGFIX scope. If it does not, signal `BLOCKED` and recommend escalation to the architect (`afx send architect "..."`). No code in this phase. -**Input**: GitHub Issue number +### Fix -**Architect Actions**: -1. Read the issue: `gh issue view ` -2. Verify it's a valid bug (not a feature request) -3. Assess complexity - is it suitable for BUGFIX protocol? -4. If too complex, escalate to SPIR instead +Apply the minimal change that resolves the root cause, and add a regression test that fails without the fix and passes with it. Keep it focused: do not refactor surrounding code, do not fix unrelated bugs (file separate issues), do not add features. Run the build and tests (porch's `checks` block runs `npm run build` and `npm test`). -**Example**: -```bash -gh issue view 42 -# Assess: Clear reproduction, simple fix expected → BUGFIX -# Assess: Unclear cause, architectural implications → SPIR -``` - -### Phase 2: Spawn Builder (Architect) - -**Architect Actions**: -1. Spawn a builder for the issue: - ```bash - afx spawn --issue 42 - # or - afx spawn -i 42 - ``` -2. **Continue working immediately** - Do NOT wait for the builder. The spawn is non-blocking. Move on to other work. - -3. The command automatically: - - Fetches issue content via `gh issue view` - - Creates branch `builder/bugfix-42-` - - Creates worktree at `.builders/bugfix-42/` - - Comments "On it!" on the issue (unless `--no-comment`) - - Spawns builder with issue context - -**Key Principle**: Spawn and forget. The builder will notify you via `afx send` when the PR is ready. Don't poll, don't watch, don't block. - -**Branch Naming**: `builder/bugfix--` - -The `builder/` prefix maintains consistency with Agent Farm tooling. - -### Phase 3: Acknowledge Issue (Builder) - -**Builder Actions**: -1. Comment on the issue to signal work has started: - ```bash - gh issue comment --body "On it! Working on a fix now." - ``` -2. Read the full issue and any related discussion -3. Begin investigation - -### Phase 4: Investigate and Fix (Builder) - -**Builder Actions**: -1. Reproduce the bug locally -2. Identify root cause -3. Implement the fix -4. Write/update tests to cover the bug -5. Verify the fix resolves the issue - -**Complexity Check**: -If during investigation the builder determines the fix is too complex: -```bash -afx send architect "Issue #N is more complex than expected. [Reason]. Recommend escalating to SPIR/TICK." -``` - -The Architect will then decide whether to: -- Continue with BUGFIX (provide guidance) -- Escalate to SPIR protocol (new feature complexity) -- Escalate to TICK protocol (amends existing spec) -- Abandon and close the issue with explanation - -**Commits**: -```bash -git add -git commit -m "[Bugfix #N] Fix: " -git commit -m "[Bugfix #N] Test: Add regression test" -``` - -### Phase 5: PR Creation and CMAP Review (Builder) - -**Builder Actions**: -1. Push the branch: - ```bash - git push -u origin bugfix/-description - ``` - -2. Create the PR: - ```bash - gh pr create --title "[Bugfix #N] " --body "$(cat <<'EOF' - ## Summary - Fixes #N - - ## Root Cause - - - ## Fix - - - ## Test Plan - - [ ] Added regression test - - [ ] Verified fix locally - - [ ] Existing tests pass - - ## CMAP Review - - EOF - )" - ``` - -3. Run 3-way CMAP review: - ```bash - consult -m gemini --protocol bugfix --type pr & - consult -m codex --protocol bugfix --type pr & - consult -m claude --protocol bugfix --type pr & - wait - ``` - -4. Address any REQUEST_CHANGES from the review - -5. Update PR description with CMAP review summary - -6. Notify Architect: - ```bash - afx send architect "PR # ready for review (fixes issue #)" - ``` - -### Phase 6: Integration Review (Architect) - -**MANDATORY CHECKLIST** (do not approve until all checked): -- [ ] CMAP 3-way review completed (Gemini, Codex, Claude) -- [ ] All REQUEST_CHANGES from CMAP addressed -- [ ] PR has only the intended changes (no stale commits) -- [ ] Tests pass - -**Architect Actions**: -1. Review the PR: `gh pr view ` - -2. Run 3-way CMAP integration review (**NON-NEGOTIABLE**): - ```bash - consult -m gemini --type integration & - consult -m codex --type integration & - consult -m claude --type integration & - wait - ``` - **DO NOT SKIP THIS STEP.** Manual review is not a substitute for CMAP. - -3. If changes needed, post feedback as PR comment: - ```bash - gh pr comment --body "## Integration Review - - **Verdict: REQUEST_CHANGES** - - ### Issues - - [Issue 1] - - [Issue 2] - - --- - Architect integration review" - ``` - -4. Notify builder: - ```bash - afx send "Check PR # comments" - ``` - -5. Once satisfied, approve and instruct merge: - ```bash - gh pr review --approve - afx send "LGTM. Merge it." - ``` - -### Phase 7: Address Feedback (Builder, if needed) - -**Builder Actions** (if feedback received): -1. Read the feedback: `gh pr view --comments` -2. Make requested changes -3. Push updates to the same branch -4. Notify Architect: - ```bash - afx send architect "Fixed feedback on PR #" - ``` - -### Phase 8: Merge (Builder) - -**Builder Actions**: -1. Merge the PR (do NOT delete branch - worktree limitation): - ```bash - gh pr merge --merge - ``` - - **Important**: Do NOT use `--delete-branch`. The builder is on this branch in a worktree, so branch deletion will fail. - -2. Notify Architect: - ```bash - afx send architect "PR # merged. Ready for cleanup." - ``` - -### Phase 9: Cleanup (Architect) - -**Architect Actions**: -1. Pull the changes: - ```bash - git pull - ``` +Commit with the issue-driven format: -2. Verify the fix is on the integration branch: - ```bash - git log --oneline -5 # Should see the bugfix commits - ``` - -3. Clean up the builder's worktree and remote branch: - ```bash - afx cleanup --issue 42 - ``` - This removes the worktree at `.builders/bugfix-42/` and deletes the remote branch. - -4. Close the issue (if not auto-closed by PR): - ```bash - gh issue close --comment "Fixed in PR #" - ``` - Note: If PR body contains "Fixes #N", the issue is auto-closed on merge. - -## Communication Summary - -| From | To | Method | Example | -|------|-----|--------|---------| -| Architect | Builder | `afx spawn` | `afx spawn --task "Fix #42"` | -| Architect | Builder | `afx send` | `afx send builder "Check PR comments"` | -| Builder | Architect | `afx send` | `afx send architect "PR #50 ready"` | -| Builder | Issue | `gh issue comment` | `gh issue comment 42 --body "On it"` | -| Architect | PR | `gh pr comment` | `gh pr comment 50 --body "LGTM"` | - -## Success Criteria Checklist - -Before marking PR ready, the Builder must verify: - -- [ ] Bug is reproduced locally -- [ ] Root cause is identified and documented in PR -- [ ] Fix is implemented (< 300 LOC net diff - see scope definition below) -- [ ] **Regression test added** (MANDATORY - prevents future recurrence) -- [ ] All existing tests pass -- [ ] CMAP review completed (3-way: Gemini, Codex, Claude) -- [ ] Any REQUEST_CHANGES from CMAP addressed -- [ ] PR body includes "Fixes #N" (for auto-close) -- [ ] PR description includes: Summary, Root Cause, Fix, Test Plan - -### Scope Definition: 300 LOC - -The "< 300 LOC" threshold is measured as **net diff** (additions + deletions): - -```bash -DEFAULT_BRANCH=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||') -DEFAULT_BRANCH=${DEFAULT_BRANCH:-main} -# Anchor at the merge-base so commits the base branch picked up after you -# branched aren't counted toward this PR's LOC budget. -git diff --stat "$(git merge-base "$DEFAULT_BRANCH" HEAD)" | tail -1 -# Example: "3 files changed, 145 insertions(+), 52 deletions(-)" -# Net diff = 145 + 52 = 197 LOC ✓ (under 300) ``` - -- **Includes**: All source files (code, tests, configs) -- **Excludes**: Generated files, lock files, vendored code -- **Guideline, not hard rule**: The 300 LOC threshold is a heuristic. Use judgment - a 350 LOC fix that's well-contained is fine; a 200 LOC fix that touches 10 files may warrant escalation. - -## Edge Case Handling - -| Scenario | Builder Action | -|----------|----------------| -| Cannot reproduce bug | Document reproduction attempts in issue comment, ask reporter for more details, notify Architect via `afx send` | -| Issue already closed | Check with Architect before starting work (may be duplicate or already fixed) | -| Fix too complex (> 300 LOC) | Notify Architect with complexity details, recommend escalation to SPIR | -| Architectural changes needed | Notify Architect immediately, do not proceed with BUGFIX | -| Unrelated test failures | Do NOT fix (out of scope), notify Architect to handle separately | -| Documentation-only bug | Valid for BUGFIX - fix the docs, add test if applicable | -| Multiple bugs in one issue | Fix only the primary bug, note others for separate issues | - -## Escalation Criteria - -**Builder should escalate to Architect when**: -- Fix requires architectural changes -- Multiple services/modules affected -- Root cause is unclear after 30 minutes -- Fix would be > 300 lines of code -- Tests reveal deeper problems - -**Architect escalation options**: -1. **Continue BUGFIX** - Provide guidance, builder continues -2. **Escalate to SPIR** - Create proper spec for complex fix -3. **Close as won't fix** - Document reasoning on issue - -## Git Commit Convention - +[Bugfix #] Fix: +[Bugfix #] Test: ``` -[Bugfix #N] Fix: -[Bugfix #N] Test: -[Bugfix #N] Docs: -``` - -**Note**: This differs from SPIR's `[Spec XXXX][Phase]` format intentionally: -- Issue numbers are shorter (no leading zeros) -- No phase names (BUGFIX is single-phase conceptually) -- Aligns with GitHub's `Fixes #N` convention in PR bodies - -## Comparison with Other Protocols -| Aspect | BUGFIX | TICK | SPIR | -|--------|--------|------|--------| -| Trigger | GitHub Issue | Amendment need | New feature | -| Spec required | No | Existing spec | New spec | -| Plan required | No | Update existing | New plan | -| Review doc | No | Yes | Yes | -| Builder worktree | Yes | Yes | Yes | -| CMAP reviews | PR only | End only | Throughout | -| Typical duration | 30 min - 2 hours | 1-4 hours | Days | +### PR (gated by `pr`) -## CMAP Review Strategy +1. Push the branch and open a PR with `gh pr create`. The body includes Summary, Root Cause, Fix, and Test Plan, plus `Fixes #` so the issue auto-closes on merge. +2. Run a multi-agent CMAP review on the PR (Gemini, Codex, Claude) and record each verdict. Address or rebut any `REQUEST_CHANGES`; add a regression test if a real defect surfaced. +3. Notify the architect: `afx send architect "PR # ready for review (fixes #). CMAP: gemini=..., codex=..., claude=..."`. +4. Run `porch done ` to request the `pr` gate, then wait. **The merge is gated by porch state, never by typed prose in your pane.** +5. The human reviews the PR and the CMAP results on GitHub, then approves the gate: `porch approve pr --a-human-explicitly-approved-this`. +6. porch wakes the builder with a merge task. Merge with `gh pr merge --merge` (do **not** pass `--delete-branch`: the builder is checked out on this branch in a worktree), then run `porch done ` and notify the architect that it is merged and ready for cleanup. -BUGFIX uses **PR-only CMAP reviews**, which is intentionally lighter than SPIR's throughout-consultation approach. +## Gate -**Why PR-only?** -- BUGFIX scope is small (< 300 LOC) - mid-implementation review adds overhead without benefit -- The issue itself serves as the "spec" - no spec review needed -- No plan document exists - no plan review needed -- All review effort is concentrated where it matters: the final PR +BUGFIX has one human gate, `pr`, on the merge step. It exists so the merge trigger is structured porch state (approved or not), not free-text typed into the builder's pane. This eliminates the self-merge bug class: a builder cannot infer authorization from ambiguous input. -**Review Types**: -- Builder: `consult -m X --protocol bugfix --type pr` -- Architect: `consult -m X --type integration` - -**3-Way Review Pattern**: -```bash -# Run all three in parallel, in background -consult -m gemini --protocol bugfix --type pr & -consult -m codex --protocol bugfix --type pr & -consult -m claude --protocol bugfix --type pr & -wait -``` +## Multi-Agent Consultation -## Example Walkthrough +A single CMAP pass at the PR (Gemini, Codex, Claude). There is no per-phase consultation: the issue is the spec and the fix is small, so review effort concentrates on the final PR. -**Issue #42**: "Login fails when username contains spaces" +## Scope -```bash -# 1. Architect identifies the issue -gh issue view 42 -# → Clear bug report, simple fix expected → BUGFIX appropriate +The < 300 LOC threshold is a **guideline**, measured as net diff (additions + deletions) anchored at the merge-base with the default branch. A well-contained 350-LOC fix is fine; a 200-LOC fix smeared across ten files may warrant escalation. -# 2. Architect spawns builder (NON-BLOCKING - architect continues other work) -afx spawn --issue 42 -# → Creates .builders/bugfix-42/ -# → Creates branch builder/bugfix-42-login-fails-when-userna -# → Comments "On it!" on issue #42 -# → Spawns builder with issue context -# -# Architect immediately moves on to other tasks. Does NOT wait. -# Will be notified via "afx send" when PR is ready. +## Escalation -# ───────────────────────────────────────────────────────────────── -# Meanwhile, in the builder's worktree (.builders/bugfix-42): -# ───────────────────────────────────────────────────────────────── +If, mid-fix, the change outgrows BUGFIX (architectural impact, multiple components, unclear root cause after investigation, or more than ~300 LOC), notify the architect with specifics and recommend escalating to SPIR. Do not silently expand scope. -# 3. Builder investigates (in worktree) -cd .builders/bugfix-42 -# → Finds encoding bug in src/auth.ts line 47 +## Branch Naming -# 4. Builder fixes and tests -git add src/auth.ts tests/auth.test.ts -git commit -m "[Bugfix #42] Fix: URL-encode username before API call" -git commit -m "[Bugfix #42] Test: Add regression test for spaces in username" - -# 5. Builder creates PR -git push -u origin builder/bugfix-42-login-fails-when-userna -gh pr create --title "[Bugfix #42] Fix login for usernames with spaces" \ - --body "Fixes #42 - -## Root Cause -Username was passed to API without URL encoding. - -## Fix -Added encodeURIComponent() call in auth.ts:47. - -## Test Plan -- [x] Added regression test -- [x] Verified fix locally" - -# 6. Builder runs CMAP review -consult -m gemini --protocol bugfix --type pr & -consult -m codex --protocol bugfix --type pr & -consult -m claude --protocol bugfix --type pr & -wait -# → All APPROVE - -# 7. Builder notifies Architect -afx send architect "PR #50 ready (fixes issue #42)" - -# 8. Architect reviews + CMAP integration review -consult -m gemini --type integration & -consult -m codex --type integration & -consult -m claude --type integration & -wait - -# 9. Architect approves -gh pr review 50 --approve -afx send bugfix-42 "LGTM. Merge it." - -# 10. Builder merges (no --delete-branch due to worktree) -gh pr merge 50 --merge -afx send architect "Merged. Ready for cleanup." - -# 11. Architect cleans up -git pull -git log --oneline -3 # Verify fix is on the integration branch -afx cleanup --issue 42 -# → Removes .builders/bugfix-42/ -# → Deletes origin/builder/bugfix-42-login-fails-when-userna - -# Issue #42 auto-closed by PR (via "Fixes #42") ``` - -**Total time**: ~45 minutes - -## Best Practices - -1. **Spawn and forget** - Architect spawns builder and immediately continues other work. Never block waiting for builder progress. -2. **Comment early** - Let the reporter know someone is working on it -3. **Keep fixes minimal** - Fix the bug, don't refactor surrounding code -4. **Add regression tests** - Prevent the bug from recurring -5. **Reference the issue** - Use "Fixes #N" in PR to auto-close -6. **Escalate promptly** - Don't waste time on complex issues -7. **Clean up promptly** - Merge and cleanup shouldn't linger - -## Projectlist Integration - -**BUGFIX issues are NOT tracked in `codev/projectlist.md`.** - -Rationale: -- BUGFIX work is tracked in GitHub Issues (the source of truth) -- Projectlist is for feature work that requires specs and plans -- Adding bugfixes would clutter projectlist with transient work -- The PR and closed issue serve as the permanent record - -To find past bugfixes, search GitHub: -```bash -gh issue list --state closed --label bug -gh pr list --state merged --search "Bugfix" +builder/bugfix-- ``` -## Triage Guidelines - -Use these guidelines to determine whether an issue is appropriate for BUGFIX: - -### Use BUGFIX when: -- Clear reproduction steps provided -- Bug is isolated to a single module/component -- No architectural implications -- Fix is straightforward once root cause is understood -- < 300 LOC expected (net diff) - -### Escalate to SPIR when: -- "Feature request disguised as bug" (e.g., "bug: should support dark mode") -- Requires new specs or design discussion -- Affects multiple systems or services -- Root cause suggests deeper architectural issues -- Fix would require > 300 LOC -- Multiple stakeholders need to weigh in - -## Limitations +## Edge Cases -1. **No spec/plan artifacts** - Less documentation than SPIR -2. **Limited scope** - Only for truly minor fixes -3. **Single builder** - No parallel work on same issue -4. **Issue dependency** - Requires GitHub Issue to exist first -5. **No projectlist tracking** - Must use GitHub Issues for history +| Scenario | Action | +|---|---| +| Cannot reproduce | Document the attempts in an issue comment, ask the reporter for detail, notify the architect | +| Fix outgrows scope (architectural / multi-component / > ~300 LOC) | Notify the architect, recommend escalation; do not proceed | +| Unrelated test failures | Out of scope: note them for the architect, do not fix them here | +| Multiple bugs in one issue | Fix only the primary bug; file separate issues for the rest | diff --git a/codev/protocols/experiment/builder-prompt.md b/codev/protocols/experiment/builder-prompt.md index 23f378975..31c5581e5 100644 --- a/codev/protocols/experiment/builder-prompt.md +++ b/codev/protocols/experiment/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the EXPERIMENT protocol: `codev/protocols/experiment/protocol.md` +Follow the EXPERIMENT protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## EXPERIMENT Overview The EXPERIMENT protocol ensures disciplined experimentation: @@ -74,3 +74,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the EXPERIMENT protocol document 2. Define your hypothesis clearly 3. Follow the phases in order + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/experiment/protocol.md b/codev/protocols/experiment/protocol.md index 7bac432a5..b9eabaac3 100644 --- a/codev/protocols/experiment/protocol.md +++ b/codev/protocols/experiment/protocol.md @@ -37,7 +37,7 @@ mkdir -p experiments/1_experiment_name cd experiments/1_experiment_name # Initialize notes.md from template -cp codev/protocols/experiment/templates/notes.md notes.md +touch notes.md # then fill it from the embedded template at the end of this protocol ``` Or ask your AI assistant: "Create a new experiment for [goal]" @@ -85,33 +85,6 @@ git add experiments/1_experiment_name/ git commit -m "[Experiment 1] Brief description of findings" ``` -## notes.md Template - -See `templates/notes.md` for the full template. Key sections: - -```markdown -# Experiment ####: Name - -**Status**: In Progress | Complete | Disproved | Aborted - -**Date**: YYYY-MM-DD - -## Goal -What are you trying to learn? - -## Time Investment -Wall clock time vs active developer time - -## Code -- [experiment.py](experiment.py) - Brief description - -## Results -What happened? What did you learn? - -## Next Steps -What should be done based on findings? -``` - ## Best Practices ### Keep It Simple @@ -227,3 +200,9 @@ Determine if Redis caching improves API response times for repeated queries. ## Next Steps Create SPIR spec for production caching implementation. ``` + +## Template: notes.md + +Create `notes.md` with the following content: + +{{> protocols/experiment/templates/notes.md}} diff --git a/codev/protocols/maintain/builder-prompt.md b/codev/protocols/maintain/builder-prompt.md index 72394e849..6a2deda0d 100644 --- a/codev/protocols/maintain/builder-prompt.md +++ b/codev/protocols/maintain/builder-prompt.md @@ -23,7 +23,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the MAINTAIN protocol: `codev/protocols/maintain/protocol.md` +Follow the MAINTAIN protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## MAINTAIN Overview @@ -54,3 +54,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 2. Run `porch next` to get your first task 3. Work through audit → clean → sync → verify in a single pass 4. Document everything in the maintenance run file + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 0edd1d992..86016f310 100644 --- a/codev/protocols/pir/builder-prompt.md +++ b/codev/protocols/pir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the PIR protocol: `codev/protocols/pir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the PIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review @@ -87,6 +86,12 @@ If your Claude session crashes mid-flow, Tower's `while true` loop will relaunch ## Getting Started -1. Read the PIR protocol document (`codev/protocols/pir/protocol.md`) +1. Read the PIR protocol (provided inline in this prompt). 2. Run `porch next {{project_id}}` to see what to do next 3. Begin work + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/research/builder-prompt.md b/codev/protocols/research/builder-prompt.md index ae501326e..088262853 100644 --- a/codev/protocols/research/builder-prompt.md +++ b/codev/protocols/research/builder-prompt.md @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the RESEARCH protocol: `codev/protocols/research/protocol.md` +Follow the RESEARCH protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## RESEARCH Overview @@ -83,3 +83,9 @@ wait 2. Understand the research question from the architect 3. Write the research brief (Phase 1) 4. Wait for scope-approval before proceeding to investigation + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/spike/builder-prompt.md b/codev/protocols/spike/builder-prompt.md index 8d8a97dfb..6d30dc649 100644 --- a/codev/protocols/spike/builder-prompt.md +++ b/codev/protocols/spike/builder-prompt.md @@ -11,7 +11,7 @@ You are running in SOFT mode. This means: {{/if}} ## Protocol -Follow the SPIKE protocol: `codev/protocols/spike/protocol.md` +Follow the SPIKE protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if task}} ## Spike Question @@ -60,3 +60,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the SPIKE protocol document 2. Understand the question you're investigating 3. Start with research — don't jump straight to code + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/spike/protocol.md b/codev/protocols/spike/protocol.md index e874252df..9b121d4cc 100644 --- a/codev/protocols/spike/protocol.md +++ b/codev/protocols/spike/protocol.md @@ -52,7 +52,7 @@ The following 3-step workflow is **guidance only** — not enforced by porch. Fo ### Step 3: Findings - Write the findings document at `codev/spikes/-.md` -- Use the template: `codev/protocols/spike/templates/findings.md` +- Use the embedded template at the end of this protocol - Provide a clear feasibility verdict - Commit and notify the architect @@ -120,3 +120,9 @@ When a spike finds something is not feasible: 1. Document clearly in findings 2. Close the related GitHub issue with a link to findings 3. The findings become institutional knowledge + +## Template: findings.md + +Write the findings document using the following template: + +{{> protocols/spike/templates/findings.md}} diff --git a/codev/protocols/spir/builder-prompt.md b/codev/protocols/spir/builder-prompt.md index 0c1a6647d..580e269c5 100644 --- a/codev/protocols/spir/builder-prompt.md +++ b/codev/protocols/spir/builder-prompt.md @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the SPIR protocol: `codev/protocols/spir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the SPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. ## Baked Decisions @@ -89,3 +88,9 @@ If you encounter **pre-existing flaky tests** (intermittent failures unrelated t 1. Read the protocol document thoroughly 2. Review the spec and plan (if available) 3. Begin implementation following the protocol phases + +--- + +## Protocol Reference (full text) + +{{protocol_reference}} diff --git a/codev/protocols/spir/prompts/plan.md b/codev/protocols/spir/prompts/plan.md index 3549654c5..2c12250dd 100644 --- a/codev/protocols/spir/prompts/plan.md +++ b/codev/protocols/spir/prompts/plan.md @@ -74,41 +74,9 @@ After completing the plan draft, signal completion. Porch will run 3-way consult ## Output -Create the plan file at `codev/plans/{{artifact_name}}.md`. +Create the plan file at `codev/plans/{{artifact_name}}.md`, following the template below: -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. - -### Plan Structure - -```markdown -# Implementation Plan: {{title}} - -## Overview -Brief summary of what will be implemented. - -## Phases - -### Phase 1: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files to create/modify] -- **Dependencies**: None -- **Success Criteria**: [How to verify completion] -- **Tests**: [What tests will be written] - -### Phase 2: [Name] -- **Objective**: [Single clear goal] -- **Files**: [List of files] -- **Dependencies**: Phase 1 -- **Success Criteria**: [Verification method] -- **Tests**: [Test approach] - -[Continue for all phases...] - -## Risk Assessment -- [Risk 1]: [Mitigation] -- [Risk 2]: [Mitigation] - -``` +{{> protocols/spir/templates/plan.md}} ## Signals diff --git a/codev/protocols/spir/protocol.md b/codev/protocols/spir/protocol.md index 9cf392379..23fda7317 100644 --- a/codev/protocols/spir/protocol.md +++ b/codev/protocols/spir/protocol.md @@ -1,6 +1,5 @@ # SPIR Protocol -> **Quick Reference**: See `codev/resources/workflow-reference.md` for stage diagrams and common commands. ## Prerequisites @@ -212,7 +211,7 @@ When filing an issue for SPIR, you can pin architectural decisions you don't wan - All consultation feedback incorporated directly into this document - Include a "Consultation Log" section summarizing key feedback and changes - Version control captures evolution through commits -**Template**: `templates/spec.md` +**Structure**: developed through the specify phase **Review Required**: Yes - Human approval AFTER consultations ### P - Plan (Structured Decomposition) @@ -298,7 +297,7 @@ Each phase should be: - Include phase status tracking within this document - **DO NOT include time estimates** - Focus on deliverables and dependencies, not hours/days - Version control captures evolution through commits -**Template**: `templates/plan.md` +**Structure**: follows the plan template provided by the plan phase **Review Required**: Yes - Technical lead approval AFTER consultations ### (IDE) - Implementation Loop @@ -710,7 +709,7 @@ spir/0001-user-authentication/database-schema ## Templates -Templates for each phase are available in the `templates/` directory: +Each phase has a template that ships in the package skeleton; the phase prompts deliver the structure you need, so you do not fetch these files directly: - `spec.md` - Specification template - `plan.md` - Planning template (includes phase status tracking) - `review.md` - Review and lessons learned template diff --git a/codev/resources/arch.md b/codev/resources/arch.md index 7291c2e09..beb38f3d4 100644 --- a/codev/resources/arch.md +++ b/codev/resources/arch.md @@ -254,6 +254,15 @@ All architect sessions (at all 3 creation points) receive a role prompt injected - `tower-terminals.ts` → `reconcileTerminalSessions()` (startup reconnection with auto-restart options) - `tower-terminals.ts` → `getTerminalsForWorkspace()` (on-the-fly shellper reconnection) +#### Builder Prompt: Protocol & Template Delivery (#1011) + +Framework files (protocol docs, templates) live in the package skeleton (resolver tier 4) and are not guaranteed on disk in a fresh install, so builder-facing prompts must not fetch them by literal `codev/...` path. They are *delivered* through resolver-aware channels instead: + +- **`{{protocol_reference}}`** — `buildPromptFromTemplate` (`agent-farm/commands/spawn-roles.ts`) fills this context variable by reading the protocol's `protocol.md` fresh through the four-tier resolver at spawn and inlining it under the prompt's "Protocol Reference (full text)" heading. Nothing is committed into the prompt, so nothing goes stale. +- **`{{> }}`** — a Handlebars-style include directive resolved by the shared `resolveCodevIncludes()` (`lib/skeleton.ts`): it pulls the referenced framework file fresh through the resolver (recursive, depth-guarded, unresolved includes drop to empty). It runs on two channels: at spawn (inside `protocol.md`, e.g. experiment/spike templates) and in porch's `loadPromptFile` (`commands/porch/prompts.ts`) for phase prompts (e.g. the spir/aspir plan template, which carries the machine-readable phases JSON the plan gate requires). + +A `codev doctor` audit (`lib/framework-ref-audit.ts`) flags shell-fetch of framework files in a project's local `codev/` overrides; the shipped skeleton is guarded by a CI unit test. + #### Multi-Architect Support (Spec 755 / Spec 786) A workspace can host more than one architect terminal. Each architect has a stable name (`main` for the workspace's default; siblings via `afx workspace add-architect`). The primary use case is letting a sibling architect drive a focused workflow without monopolising `main`. diff --git a/codev/resources/lessons-learned.md b/codev/resources/lessons-learned.md index 40dde6466..d92cfb355 100644 --- a/codev/resources/lessons-learned.md +++ b/codev/resources/lessons-learned.md @@ -383,6 +383,9 @@ Generalizable wisdom extracted from review documents, ordered by impact. Updated - [From 0076] Three-layer mental models matter: when debugging process lifecycle issues, identify all layers and understand which layer owns which state. - [From 0082] Before splitting a monolith into packages, verify the dependency graph is unidirectional. The Codev -> AgentFarm -> Porch flow has no circular dependencies, making extraction feasible. Start with the component that has the cleanest boundaries (porch). - [From 0106] Porch check normalization bug: `normalizeProtocol()` merges all phases' checks into a flat `Record`. If the review phase defines a `"tests"` check, it overrides the implement phase's modified test command. Phase-scoped check definitions would prevent override collisions. +- [From 1011] Framework files (protocol docs, templates, resource docs) default to the package skeleton (resolver tier 4) and are not on disk in a fresh install. Builder-facing prompts and docs must *deliver* them through resolver-aware channels (the `{{protocol_reference}}` context var; the `{{> path}}` include resolved by `resolveCodevIncludes`), never fetch them by literal `codev/...` path (`cat`/`cp`). A path fetch bypasses the four-tier resolver and fails in fresh installs. Referencing a path in prose for orientation is fine; the rule is about fetching, not referencing. +- [From 1011] Soft-mode protocols (experiment/spike) have no porch phase prompts, so `protocol.md` is their only guidance channel and templates must be injected into it; strict-mode protocols carry structure in phase prompts. A delivery fix has to cover both channels. +- [From 1011] Don't drop a "use the template at ``" pointer as a dead reference. The spir/aspir plan template carries the machine-readable phases JSON porch's plan gate requires (`has_phases_json`), so it must be *delivered* via a porch-resolved `{{> }}` include, not removed. ## Debugging and Root Cause Analysis diff --git a/codev/reviews/1011-agent-farm-inline-protocol-md-.md b/codev/reviews/1011-agent-farm-inline-protocol-md-.md new file mode 100644 index 000000000..c9e371191 --- /dev/null +++ b/codev/reviews/1011-agent-farm-inline-protocol-md-.md @@ -0,0 +1,77 @@ +# PIR Review: Deliver framework files via resolver-aware channels (fresh-install class fix) + +Fixes #1011. +Fixes #1013. + +## Summary + +Post-Spec-618, framework files (protocol docs, role docs, templates, framework resource docs) live only in the package skeleton (resolver tier 4) and are not on disk in fresh installs, yet several builder-facing consumers referenced them by literal `codev/...` path (`cat`/`cp`/"read this file"), which bypasses the four-tier resolver and fails in a fresh install. This PR is the class fix: it *delivers* framework files through resolver-aware channels (a `{{protocol_reference}}` context var that inlines `protocol.md` fresh at spawn, and a shared `{{> path}}` include directive resolved on both the spawn and porch phase-prompt channels), sweeps the literal-path references, and adds a `codev doctor` audit plus a principle-level convention note as enforcement. It also folds in #1013 (de-stale `bugfix/protocol.md` and ship it to the skeleton; drop the `experiment/protocol.md` partial template copy). + +## Files Changed + +48 files, +1113 / -774 (the large deletions are the stale 548-line `bugfix/protocol.md` and the divergent inline plan-structure block). Core code: + +- `packages/codev/src/lib/skeleton.ts` (+24) — shared `resolveCodevIncludes()` +- `packages/codev/src/agent-farm/commands/spawn-roles.ts` (+45/-) — `{{protocol_reference}}` fill via fresh resolver read +- `packages/codev/src/commands/porch/prompts.ts` (+8) — phase-prompt include resolution +- `packages/codev/src/lib/framework-ref-audit.ts` (+85, new) — shell-fetch audit +- `packages/codev/src/commands/doctor.ts` (+23) — wire audit against workspace `codev/` overrides + +Tests: `framework-ref-audit.test.ts` (+164, new), `spawn-roles.test.ts` (+134), `bugfix-619-aspir-prompt.test.ts` (+10/-), 3 Spec-746 baselines re-swept. + +Markdown (skeleton + mirrored `codev/`): 9 builder-prompts, spir/aspir `prompts/plan.md`, experiment/spike `protocol.md`, spir `protocol.md`, `roles/builder.md`, new skeleton `bugfix/protocol.md` (rewritten from the stale `codev/` copy), `CLAUDE.md`/`AGENTS.md` convention note. Plus the plan + thread artifacts. + +## Commits + +Grouped by what landed (full per-commit history is on the PR): + +- **Delivery:** inline `protocol.md` at spawn via `{{protocol_reference}}`; shared `{{> path}}` include resolution on both the spawn and porch phase-prompt channels (incl. the spir/aspir plan template with its phases JSON). +- **Cleanup sweep:** removed literal `codev/protocols/...` references across the 9 builder-prompts, `roles/builder.md`, and `spir/protocol.md`. +- **Enforcement:** `codev doctor` framework-ref audit (scoped to workspace overrides, warn-not-error) + a principle-level convention note in CLAUDE.md / AGENTS.md. +- **#1013 fold-in:** de-staled `bugfix/protocol.md` (shipped to the skeleton); dropped the `experiment` partial template copy. +- **Consultation fixes:** fail-fast on a missing `builder-prompt.md` (Codex iter-1); `doctor` true no-op for projects with no overrides (Codex iter-2). +- Plan draft, review/retrospective, and porch phase-transition chore commits. + +## Test Results + +- `pnpm build`: ✓ pass (core then codev, including dashboard + copy-skeleton) +- `pnpm test`: ✓ 3281 passed / 13 pre-existing skips on a clean run. New tests cover the `{{protocol_reference}}` fill, `{{> }}` include resolution on both spawn and porch channels, the plan template's phases JSON reaching the builder, the doctor audit (positive/negative/no-op), the protocol.md completeness check, the `validateProtocol` json-without-md warning, the skeleton sweep, the fail-fast behaviour when a protocol ships no `builder-prompt.md` (Codex iter-1 disposition), and the `hasFrameworkOverrides` true-no-op predicate (Codex iter-2 disposition). +- Manual verification: the human reviewed the running worktree at the `dev-approval` gate and approved. End-to-end against the real skeleton: `protocol.md` inlines fresh under "Protocol Reference (full text)", "Read and internalize" restored, `{{> }}` expands, zero handlebar residue. + +## Architecture Updates + +Added a "Builder Prompt: Protocol & Template Delivery (#1011)" subsection to `codev/resources/arch.md` (under Agent Farm Internals, next to the role-prompt-injection description). It documents the two new delivery channels (`{{protocol_reference}}` and the `{{> path}}` include resolved by `resolveCodevIncludes` on both the spawn and porch phase-prompt paths) and the `codev doctor` audit, because this PR introduced a genuinely new mechanism for how framework files reach builders. + +## Lessons Learned Updates + +Added three bullets to `codev/resources/lessons-learned.md` (Protocol Orchestration): deliver framework files via resolver-aware channels rather than fetching them by literal path; soft-mode protocols have only `protocol.md` as a guidance channel so a delivery fix must cover both channels; and don't drop a plan-template pointer as a dead reference because the spir/aspir plan template carries the phases JSON porch's plan gate requires. + +## Things to Look At During PR Review + +- **Consultation disposition (Codex REQUEST_CHANGES, iter-1, addressed).** Codex flagged that the no-`builder-prompt.md` fallback in `spawn-roles.ts` still pointed the builder at `codev/protocols/...` by literal path, re-introducing the bug class for custom protocols. Real finding. Disposition: rather than harden the fallback (which would duplicate template wording and drift), the fallback was **removed** in favour of fail-fast. `validateProtocol` now hard-errors when a protocol resolves no `builder-prompt.md`, with a defensive backstop in `buildPromptFromTemplate`. Every shipped protocol ships a `builder-prompt.md`, so this only affects a malformed custom protocol, which now gets a clear error instead of a silently degraded prompt. Pinned by two tests (`buildPromptFromTemplate` throws; `validateProtocol` fails fast on a json-only protocol). PIR is single-pass, so this was not independently re-reviewed: flagged for your attention at the `pr` gate. Claude APPROVE'd; Gemini was skipped non-blockingly (agy not authenticated). +- **Consultation disposition (iter-2, architect-requested re-review).** Because the iter-1 fix landed after that consult ran, a fresh single-pass 3-way was run against the final state: Claude APPROVE (HIGH, no issues); Codex REQUEST_CHANGES on one minor item — `codev doctor` printed a `✓` line even for projects with no `codev/` overrides, contradicting the documented "no-op". Addressed (true no-op): the audit is now gated on a tested `hasFrameworkOverrides` predicate, so a project with no overrides produces no output. Gemini skipped non-blockingly (agy authenticated but timed out producing the review in the builder env, twice — environmental, not auth). Verdicts: `codev/projects/1011-*/1011-review-iter2-*.txt`. +- **The `{{> path}}` include directive is new surface.** It's a Handlebars-style partial resolved by a shared `resolveCodevIncludes()` (recursive, depth-guarded at 5, unresolved → empty), run on two channels (spawn-time inside `protocol.md`, and porch's `loadPromptFile` for phase prompts). Worth confirming the mechanism choice and the dual-channel wiring. +- **The porch `loadPromptFile` change** (`commands/porch/prompts.ts`) is what makes the spir/aspir plan template resolve fresh at the plan phase. The plan gate needs the machine-readable phases JSON (`has_phases_json`), so this is load-bearing, not cosmetic. +- **Doctor scope.** The audit flags only shell-fetch verbs (`cat`/`cp`/…) against `codev/(protocols|roles)/...`, runs against the project's local `codev/` overrides (end-user-controlled), warn-not-error; the shipped skeleton is guarded by a CI unit test instead. +- **bugfix rewrite (#1013).** A stale 548-line manual-flow doc was rewritten to a ~78-line porch-accurate doc and shipped to the skeleton (it was absent). Worth a careful read that the porch-era flow (pr gate, CMAP, gate-driven merge) is described correctly. +- **Two-tree parity.** Every protocol/role change was hand-applied to both `codev-skeleton/` and the self-hosted `codev/` tree. Filed #1016 to add a CI parity guard. + +## How to Test Locally + +For reviewers pulling the branch: + +- **View diff**: VSCode sidebar → right-click builder pir-1011 → **View Diff** +- **Run dev server**: `afx dev pir-1011` (or the VSCode Run Dev Server action) +- **What to verify**: + - `porch next ` for a spir/aspir project at the plan phase contains the plan template's `"phases"` JSON and no literal `{{>`. + - A fresh `codev init` scratch project, then `afx spawn --task ... --protocol spir`: the spawned builder's opening prompt contains the inlined `protocol.md` under "Protocol Reference (full text)" with zero `{{` residue (tier-4 skeleton fallback path). + - `codev doctor` in a project with a `codev/` override containing a `cat codev/protocols/...` line flags it (warn), and is a no-op with no overrides. + +## Flaky Tests + +Two pre-existing tests flaked once under full-suite parallel load (5s timeouts), then passed on a clean re-run of the same code: + +- `packages/codev/src/__tests__/team-cli.test.ts:140` ("auto-detects author when not provided") +- `packages/codev/src/__tests__/team-github.test.ts:241` ("returns gracefully when gh CLI succeeds or fails") + +Both contend on the `gh` CLI / git-author subprocess under parallelism and are unrelated to this diff (which touches no `team-*` code). Not fixed or quarantined per protocol scope rules. diff --git a/codev/roles/builder.md b/codev/roles/builder.md index aa01346ec..4f49ded80 100644 --- a/codev/roles/builder.md +++ b/codev/roles/builder.md @@ -79,8 +79,8 @@ In soft mode, you follow the protocol document yourself. The architect monitors cat codev/specs/XXXX-*.md cat codev/plans/XXXX-*.md -# Read the protocol -cat codev/protocols/spir/protocol.md +# (The full protocol text is inlined in your spawn prompt under the +# "## Protocol Reference (full text)" heading; no need to fetch it.) # Start implementing ``` diff --git a/codev/state/pir-1011_thread.md b/codev/state/pir-1011_thread.md new file mode 100644 index 000000000..8d51143b4 --- /dev/null +++ b/codev/state/pir-1011_thread.md @@ -0,0 +1,121 @@ +# PIR #1011 — inline protocol.md into builder prompt at spawn + +## Plan phase (2026-06-08) + +Read issue #1011, PIR protocol, and the target code. Investigation findings: + +- Target: `loadBuilderPromptTemplate()` in `packages/codev/src/agent-farm/commands/spawn-roles.ts:99-108`. +- The returned template flows through `renderTemplate()` in `buildPromptFromTemplate()` (handlebars substitution + `\n{3,}→\n\n` collapse + trim). +- Verified: **zero `{{` occurrences across all 8 `codev-skeleton/protocols/*/protocol.md`** files, so appending protocol.md before render is safe today (no accidental substitution). +- `validateProtocol()` already fatals earlier in the spawn flow if BOTH `protocol.json` and `protocol.md` are missing — so by the time we inline, a missing `protocol.md` means the json exists; silently skipping is correct. +- Tests live in `packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts`; the skeleton-fallback `describe` block (issue #706) is the natural home for the new inline-behavior test. +- Plan-gate decisions locked: (1) `---` + `## Protocol Reference (full text)` delimiter, (2) silently skip + debug log when protocol.md absent, (3) unconditional (no config flag). + +Plan written to `codev/plans/1011-agent-farm-inline-protocol-md-.md`. Awaiting plan-approval gate. + +## Implement phase (2026-06-08) + +plan-approval approved. Implemented per plan: + +- `spawn-roles.ts`: `loadBuilderPromptTemplate()` now resolves `protocol.md` via `resolveCodevFile()` and appends it under `\n\n---\n\n## Protocol Reference (full text)\n\n`. Missing protocol.md → `logger.debug` + skip (validateProtocol already fatals earlier if both json+md absent). +- `spawn-roles.test.ts`: 2 new tests in the skeleton-fallback block — (1) inlines protocol.md under the delimiter with a sentinel body, (2) builds without error and omits the heading when protocol.md is absent. + +Build ✓ (root `npm run build`), full suite ✓ (3260 passed, 13 pre-existing skips — none mine). Committed + pushed. At dev-approval gate. + +## Scope expansion (2026-06-08, architect-driven) + +Architect expanded #1011 twice: first to a framework-file class fix (A.1/A.2/A.3), then to a three-layer structure (Delivery / Cleanup / Enforcement). Plan rewritten accordingly. 5 plan-gate decisions locked (delimiter; Patch2=B explicit-embed; A.3=strip; missing-file=silent-skip+debug; doctor=warn-not-error skeleton-only). Patch 1 (Layer 1/A.1) already done + at dev-approval; Patch 2 + Layer 2 sweep + Layer 3 (doctor check + AGENTS/CLAUDE convention) still to implement. + +bugfix sub-decision RESOLVED → drop its dead pointer, embed nothing. Evidence: bugfix builder-relevant guidance (300-LOC/escalation/regression/Fixes#) already in its phase prompts (investigate/fix/pr); the 548-line codev/ protocol.md is mostly architect-facing + partly stale (manual merge flow, deprecated projectlist) and absent from the skeleton. That doc-hygiene gap filed separately as **#1013** (area/protocols), kept out of #1011's plumbing scope. + +## Three-layer implementation complete (2026-06-08) + +- **Layer 1**: Patch 1 (protocol.md inline at spawn) already done. Patch 2 (A.2) = explicit-embed: dropped redundant template pointers in spir/aspir plan prompts; embedded notes.md/findings.md into experiment/spike protocol.md under `## Template:` + `BEGIN/END EMBEDDED TEMPLATE` sentinels (drift-guarded). +- **Layer 2**: swept the protocol.md pointer from all 9 builder-prompts (path-free `Follow the X protocol.`), fixed roles/builder.md cat example, stripped A.3 workflow-reference line from spir/protocol.md. Both trees. +- **Layer 3**: added the "Framework files: never shell-fetch by literal path" convention to root AGENTS.md + CLAUDE.md; added `lib/framework-ref-audit.ts` + a `doctor()` section (warn-not-error, skeleton-only). + +**Key finding that reshaped Layer 3**: the skeleton has ~60 legitimate `codev/...` mentions (user files arch.md/lessons-learned.md; the documented protocol list in CLAUDE/AGENTS templates; cross-refs). A blanket grep would be a noise cannon and contradict documented convention. So the doctor check is deliberately NARROW: only shell-fetch verbs (cat/cp/...) reading `codev/(protocols|roles)/...`. resources/ excluded (mixed framework+user). Documented this scope in the lib + convention. **Flag for architect: the doctor check guards the shell-fetch regression specifically, not the backtick-instruction form (which can't be flagged without false-positiving the legit protocol list).** + +Also found + left out-of-scope: `codev/protocols/release/protocol.md:43` cats maintain/protocol.md (architect-run, codev/-only, not in skeleton). And relative-path template refs inside protocol.md (spir:215/301, experiment:90) — relative, informational. + +Two pre-existing tests updated (legit staleness from the sweep): baked-decisions.test.ts baselines (3 fixtures swept to match) + bugfix-619 assertion (now asserts no protocol.md path at all, preserving #619 intent). Build ✓, full suite ✓ (3270 passed, 13 pre-existing skips). Still at dev-approval gate. + +## Reworked at dev-approval gate: static embed/append → fresh-at-delivery placeholders (2026-06-08) + +Reviewer feedback: (a) "Follow the X protocol." is too terse + don't drop "Read and internalize..."; want a reference + a literal placeholder that's replaced; (b) embedding notes.md/findings.md inline causes staleness. + +Reworked the whole delivery mechanism: +- spawn-roles.ts: removed the append; added `resolveIncludes()` (recursive `{{> path}}` resolution) + `resolveProtocolReference()`; buildPromptFromTemplate now sets a `{{protocol_reference}}` context var (protocol.md read FRESH at spawn, with its `{{> ...}}` template includes resolved). No committed copy anywhere → no staleness. +- builder-prompts (9, both trees): `## Protocol` restored to "Follow the X protocol. Read and internalize the protocol before starting any work.{{#if protocol_reference}} ...included below...{{/if}}" + appended `{{#if protocol_reference}} ## Protocol Reference (full text) {{protocol_reference}}{{/if}}`. +- experiment/spike protocol.md: replaced my static embed with `{{> protocols//templates/}}` include (resolved fresh). Found + left a PRE-EXISTING out-of-scope partial copy of notes.md in experiment's `## notes.md Template` section (relative ref). +- Tests: spawn-roles placeholder + include tests; framework-ref-audit drift test now asserts include-present + no static embed; baked-decisions baselines re-swept to the new `## Protocol` line. + +Build ✓, full suite ✓ (3271 passed, 13 skips). Still at dev-approval gate. + +## #1013 folded into this PR (2026-06-09) + +Architect: "implement 1013 as part of this session so we have a complete implementation of the whole bug." Rationale: #1011's Layer 1 now reliably inlines protocol.md into builders, so the stale bugfix doc reaching builders is a correctness risk, not just hygiene. + +- **bugfix**: rewrote `protocol.md` from the stale 548-line manual-flow doc to a concise ~78-line porch-accurate one, grounded in `protocol.json` (investigate/fix/pr + `pr` gate) and `prompts/pr.md` (builder CMAP → `porch done` → `pr` gate → human approves → porch merge task). Dropped the projectlist section + manual "Merge it" handshake + manual architect CMAP; fixed branch-naming. **Shipped to the skeleton** (was absent) so fresh-install bugfix builders get a correct meta-doc via Layer 1. Verified doctor-clean (no shell-fetch). +- **experiment**: removed the redundant `## notes.md Template` partial-copy section (relative-ref duplicate of notes.md); the `## Template: notes.md` fresh-include #1011 added makes it redundant. No committed template copy remains. + +Build ✓, full suite ✓ (3271 passed, 13 skips). PR #1015 (draft) to be updated to also Fixes #1013. Still at dev-approval gate. + +## Follow-up review: spir dead template pointers + soft-mode finding (2026-06-09) + +Reviewer asked whether other protocols need compliance changes, then probed the experiment/spike template injection. + +- **spir/protocol.md dead pointers (fixed, commit 926168a5)**: the two `**Template**: templates/spec.md|plan.md` annotations + the `## Templates` section pointed builders (via inlined protocol.md) at template paths absent in fresh installs. Reworded to point at the phase prompts / describe templates as skeleton-shipped reference. (`aspir/protocol.md` `└── templates/` and `consult-types/` are ASCII directory-tree diagrams — benign, left.) +- **Unused files**: confirmed `maintain/templates/*` are all orphaned (referenced by nothing; run-file structure is inline in maintain protocol.md). Per reviewer, NOT deleting them — added a "Unused framework files (kept for reference)" note to the PR body instead. (Reverted an earlier deletion of maintenance-run.md.) +- **Soft-mode finding (resolves the injection inconsistency)**: `experiment`/`spike` default to **`mode: soft`** (`prompt: None` on every phase, empty `prompts/`). Soft mode = builder follows `protocol.md` directly, no porch phase prompts — so `protocol.md` is their only guidance channel, which is *why* templates are injected into it via `{{> }}` (and why strict protocols spir/aspir/bugfix, whose phase prompts carry structure, are NOT injected). So: strict → phase prompts carry structure (don't inject); soft → protocol.md is everything (inject). The `{{> }}` injection FIXES experiment/spike (their old `cp`/template path was dead in fresh installs). Retracted the earlier "give experiment/spike phase prompts" follow-up — phase prompts are a strict-mode concept; soft-mode protocols correctly live in protocol.md, so no follow-up needed. + +Build ✓, full suite ✓ (3271 passed, 13 skips). Still at dev-approval gate. + +## Doctor check repointed: workspace overrides, not global skeleton (2026-06-09) + +Reviewer: "codev doctor is an end-user tool — why is its check focused on the global package skeleton?" Correct catch, and a real flaw (not just framing). Auditing the shipped skeleton from an end-user tool is pointless — the user can't fix it, and it's already CI-guaranteed clean. Repointed the `doctor()` check from `getSkeletonDir()` (global package) to the **workspace `codev/` overrides** (`resolve(workspaceRoot, 'codev')`), moved it inside the "in a codev project" block, no-op when there are no local protocol/role overrides. Shipped-skeleton guard stays in the unit test (`framework-ref-audit.test.ts` scans `codev-skeleton/`, runs in CI). Renamed lib param `skeletonDir`→`rootDir`; updated the CLAUDE.md/AGENTS.md convention line ("audits your project's local codev/ overrides"); revised plan decision #5 (was wrongly "skeleton-only" — this matches the architect's original Layer 3 intent of grepping local codev/ dirs). Build ✓, suite ✓ (3272 passed, 13 skips). + +## Removed the {{#if protocol_reference}} guard; enforce completeness instead (2026-06-09) + +Reviewer: since all protocols now have a protocol.md (bugfix got one in #1013), the `{{#if protocol_reference}}` guard is always-true / dead code — remove it. Done, safely: +- Stripped `{{#if protocol_reference}}`/`{{/if}}` from both spots (the `## Protocol` line + the EOF `## Protocol Reference (full text)` block) in all 9 builder-prompts, both trees → the protocol reference is now **unconditional**. +- Added a **completeness unit test** (`framework-ref-audit.test.ts`): every shipped skeleton protocol with a `protocol.json` must also ship a `protocol.md`. This makes the "every protocol has a meta-doc" invariant true-by-construction (a future json-only protocol fails CI rather than rendering an empty `## Protocol Reference` section), which is what justifies dropping the guard. +- Updated the spawn-roles "absent" test (the omit-behavior is gone; now asserts the build still succeeds and the placeholder resolves) + the mock template (unconditional). Re-swept the 3 Spec-746 baselines to the unconditional `## Protocol` line (pure-addition diff). +- Residual (now handled): a user's OWN `codev/protocols/` json-only override (protocol.json, no protocol.md) would render an empty `## Protocol Reference` heading. Added a **non-fatal `validateProtocol` warning** for that case — "protocol X has a protocol.json but no protocol.md; builders will spawn with an empty Protocol Reference section." Non-fatal so json-only protocols stay valid; flags the omission at spawn without bringing back the render-time `{{#if}}` guard. +2 tests (warns when md absent; silent when md present). + +Build ✓, suite ✓ (3275 passed, 13 skips). Still at dev-approval gate. + +## BUG FOUND + FIXED: spir/aspir plan template was load-bearing (2026-06-09) + +Reviewer probed how the builder gets the plan template now. Found a real bug I introduced: porch's spir/aspir **plan gate requires machine-readable phases JSON** (`has_phases_json` + `min_two_phases`), which lives ONLY in `templates/plan.md`. My earlier "drop the redundant plan-template pointer; the inline `### Plan Structure` is self-contained" was WRONG — that inline block is JSON-less, and neither it nor the inlined protocol.md guides the JSON. So a spir/aspir builder would have written a markdown-only plan and FAILED the plan gate. + +Also surfaced a mechanism constraint: the `{{> }}` include only ran in `resolveProtocolReference` (spawn, on protocol.md). porch's `loadPromptFile` (phase-prompt delivery) did NOT resolve includes — so a `{{> }}` in a phase prompt would be left literal. + +Fix (Option B): +- Extracted the include logic to a shared `resolveCodevIncludes` in `lib/skeleton.ts` (one impl). +- `loadPromptFile` (porch) now resolves includes → phase prompts can pull framework files fresh. +- spir/aspir `prompts/plan.md`: replaced the divergent inline `### Plan Structure` with `{{> protocols/spir/templates/plan.md}}` — the builder now follows the real canonical template (with the phases JSON), delivered fresh, single-source. +- spawn-roles `resolveProtocolReference` now calls the shared `resolveCodevIncludes`. +- Tests: spawn-roles skeleton mock provides `resolveCodevIncludes`; framework-ref-audit asserts the include is used (not inline structure), the template carries the JSON, and the resolved plan prompt contains the JSON (porch delivery). Build ✓, suite ✓ (3277 passed, 13 skips). + +spec/review re-check: **review is self-contained** — its prompt explicitly mandates `## Architecture Updates` + `## Lessons Learned Updates` (the porch-checked sections), so it's gate-satisfied (no template delivery needed). **specify has no porch check** and guides the spec via a process (no structural template delivered); not broken, but `templates/spec.md` isn't delivered and the spir/protocol.md "Structure: provided in the specify-phase prompt" note slightly overstates (the prompt gives a process, not the template). Flagged to the architect for a decision (deliver templates/spec.md via include for consistency, or leave process-only + correct the note). + +## REVIEW phase — PR #1015 at pr gate (2026-06-09) + +dev-approval approved → advanced to review. Wrote `codev/reviews/1011-agent-farm-inline-protocol-md-.md` (Summary / Files Changed / Commits / Test Results / Architecture Updates / Lessons Learned Updates / Things to Look At / How to Test Locally / Flaky Tests). Added an arch.md subsection (Builder Prompt: Protocol & Template Delivery) + 3 lessons-learned bullets. Reused the existing draft PR #1015 (body = review file), marked ready-for-review, recorded with porch. + +3-way consult (single-pass, max_iterations:1): **claude=APPROVE** (no key issues), **gemini=COMMENT** (lane skipped — agy installed at ~/.local/bin/agy v1.0.6 but NOT authenticated; non-blocking; the earlier "skip" line the architect saw was test output from consult.test.ts), **codex=REQUEST_CHANGES** (HIGH). + +Codex finding (real, accepted): `buildFallbackPrompt` (used when a protocol has no builder-prompt.md) still pointed builders at `codev/protocols/...` by literal path → re-introduced the fresh-install bug class for custom protocols. I first attempted a resolver-aware fallback, but architect flagged it hard-coded the template heading/wording in TS (drift — the very anti-pattern this PR fights). Architect then asked "why have a fallback at all" — correct: all 9 shipped protocols ship a builder-prompt.md, so the fallback is dead code for shipped protocols + degraded for custom, and was the sole home of the bug. Fix (approved direction = fail-fast): removed `buildFallbackPrompt`; `validateProtocol` hard-errors on a missing builder-prompt.md (defensive backstop in `buildPromptFromTemplate`); +2 regression tests. Net -13 LOC. Build ✓, suite 3278 passed / 13 skips. Commit 79786392. Rebuttal: `codev/projects/1011-*/1011-review-iter1-rebuttals.md`. + +Now WAITING at the pr gate. Single-pass means the Codex fix was NOT independently re-reviewed; escalated to the architect (the remaining reviewer) via afx send, leading with the REQUEST_CHANGES + disposition. Merge is gated by porch state (`gate_status: approved`), never by pane prose. + +## REVIEW iter-2 — architect-requested re-CMAP (2026-06-09) + +Architect asked for a fresh single-pass 3-way against the FINAL state, since the iter-1 fallback fix (79786392) landed after iter-1's consult and was unreviewed. Verified agy auth in the builder env first (`agy -p "say hi"` → real response, exit 0; the iter-1 skip was unauth, now resolved by the architect signing in). Used `--type impl` to match iter-1 (architect wrote `--type pr`; flagged the discrepancy — iter-1 actually ran impl, which is the lens PIR's pr-gate consult uses). + +iter-2 verdicts: **claude=APPROVE** (HIGH, no issues), **codex=REQUEST_CHANGES** (HIGH, NEW minor finding — iter-1 fallback issue resolved and not re-raised), **gemini=COMMENT skipped** (agy authenticated but timed out producing the review — twice: the 5m print-timeout then the 6m hard cap; environmental perf in the builder env, not auth; consult's timeouts are hard-coded constants with no override). + +Codex iter-2 (verified real, cosmetic): `doctor`'s framework-ref audit printed a green ✓ line even for projects with NO codev/ overrides, contradicting the documented "no-op". Fix (architect approved, right-sized): gated the audit on a new tested `hasFrameworkOverrides` predicate → true no-op (no output) when no protocols/roles overrides. +3 predicate unit tests. Deliberately skipped a full `doctor()` integration test (fragile temp-workspace); the regressed behavior is the predicate, unit-tested directly. Build ✓, suite 3281 passed / 13 skips. Commit 6eb1fb50. + +Both post-review fixes (iter-1 fallback, iter-2 doctor) are on the branch; architect reviewing at the pr gate. No iter-3 unless requested. Gemini remains an env-skip (documented, non-blocking). diff --git a/packages/codev/src/__tests__/framework-ref-audit.test.ts b/packages/codev/src/__tests__/framework-ref-audit.test.ts new file mode 100644 index 000000000..16e2ec8e9 --- /dev/null +++ b/packages/codev/src/__tests__/framework-ref-audit.test.ts @@ -0,0 +1,185 @@ +/** + * Tests for issue #1011 — framework-file delivery via resolver-aware channels. + * + * Two concerns: + * 1. The `auditFrameworkRefs` lib (Layer 3 regression guard) flags shell-fetch + * violations and ignores legitimate references. + * 2. The skeleton sweep + template embeds (Layers 1/2 / Patch 2) actually + * landed: builder-prompts no longer point at protocol.md, the cat example + * and workflow-reference pointer are gone, the redundant plan-template + * pointers are dropped, and the embedded templates byte-match canonical. + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve, join } from 'node:path'; +import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, readdirSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { auditFrameworkRefs, hasFrameworkOverrides } from '../lib/framework-ref-audit.js'; +import { resolveCodevIncludes } from '../lib/skeleton.js'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +// src/__tests__ → repo root is four levels up. +const REPO_ROOT = resolve(__dirname, '../../../..'); +const SKELETON = join(REPO_ROOT, 'codev-skeleton'); + +describe('auditFrameworkRefs (issue #1011 Layer 3)', () => { + let dir: string; + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'fw-ref-audit-')); + mkdirSync(join(dir, 'protocols', 'demo'), { recursive: true }); + mkdirSync(join(dir, 'roles'), { recursive: true }); + mkdirSync(join(dir, 'resources'), { recursive: true }); + }); + + it('flags a shell cat of a framework file by literal path', () => { + writeFileSync(join(dir, 'protocols', 'demo', 'protocol.md'), + '# Demo\n\ncat codev/protocols/demo/protocol.md\n'); + const findings = auditFrameworkRefs(dir); + expect(findings).toHaveLength(1); + expect(findings[0].file).toContain('protocol.md'); + expect(findings[0].line).toBe(3); + }); + + it('flags a shell cp of a framework template by literal path', () => { + writeFileSync(join(dir, 'protocols', 'demo', 'protocol.md'), + 'cp codev/protocols/demo/templates/notes.md notes.md\n'); + expect(auditFrameworkRefs(dir)).toHaveLength(1); + }); + + it('does NOT flag documentation references or user-file paths', () => { + writeFileSync(join(dir, 'protocols', 'demo', 'protocol.md'), + [ + 'See `codev/protocols/demo/protocol.md` for details.', // backtick doc ref + 'Update `codev/resources/arch.md` after the change.', // user file + 'git add codev/resources/lessons-learned.md', // user file write + 'Follow the DEMO protocol.', // swept form + ].join('\n') + '\n'); + expect(auditFrameworkRefs(dir)).toHaveLength(0); + }); + + it('does NOT scan codev/resources (mixed framework + user files)', () => { + writeFileSync(join(dir, 'resources', 'guide.md'), + 'cat codev/resources/workflow-reference.md\n'); + expect(auditFrameworkRefs(dir)).toHaveLength(0); + }); + + it('the real swept skeleton is clean (no shell-fetch violations) — CI/source guard', () => { + if (!existsSync(SKELETON)) return; // resilient if layout shifts + expect(auditFrameworkRefs(SKELETON)).toEqual([]); + }); + + it('is a no-op for a codev root with no protocol/role overrides (the doctor case)', () => { + // A typical end-user project resolves framework files from the package and + // has no local codev/protocols or codev/roles; doctor scans that root and + // should find nothing rather than error. + const bare = mkdtempSync(join(tmpdir(), 'fw-ref-bare-')); + mkdirSync(join(bare, 'specs'), { recursive: true }); // user dirs, no protocols/roles + expect(auditFrameworkRefs(bare)).toEqual([]); + }); + + // #1011 (Codex iter-2): doctor must be a *true* no-op (print nothing) for a + // project with no overrides, not merely return an empty audit. That decision is + // hasFrameworkOverrides — an empty audit result alone can't distinguish + // "nothing to scan" from "scanned, clean", which is what made doctor print a + // spurious success line for projects with nothing to audit. + it('hasFrameworkOverrides: true when protocols/ (or roles/) exists', () => { + expect(hasFrameworkOverrides(dir)).toBe(true); // beforeEach created protocols/ + roles/ + }); + + it('hasFrameworkOverrides: true when only roles/ exists', () => { + const rolesOnly = mkdtempSync(join(tmpdir(), 'fw-ref-roles-')); + mkdirSync(join(rolesOnly, 'roles'), { recursive: true }); + expect(hasFrameworkOverrides(rolesOnly)).toBe(true); + }); + + it('hasFrameworkOverrides: false when neither protocols/ nor roles/ exists (true doctor no-op)', () => { + const noOverrides = mkdtempSync(join(tmpdir(), 'fw-ref-noov-')); + mkdirSync(join(noOverrides, 'specs'), { recursive: true }); + expect(hasFrameworkOverrides(noOverrides)).toBe(false); + }); +}); + +describe('shipped protocol completeness (issue #1011)', () => { + // The builder-prompts inline `{{protocol_reference}}` UNCONDITIONALLY (no {{#if}} + // guard) — that is safe only because every shipped protocol ships a protocol.md + // for it to resolve. This test enforces that invariant: a contributor adding a + // protocol.json-only protocol (the shape bugfix had pre-#1013) fails CI here, + // rather than shipping a builder prompt with an empty `## Protocol Reference`. + it('every shipped skeleton protocol with a protocol.json also ships a protocol.md', () => { + const protocolsDir = join(SKELETON, 'protocols'); + if (!existsSync(protocolsDir)) return; // resilient if layout shifts + const missing = readdirSync(protocolsDir, { withFileTypes: true }) + .filter((d) => d.isDirectory()) + .map((d) => d.name) + .filter((name) => existsSync(join(protocolsDir, name, 'protocol.json'))) + .filter((name) => !existsSync(join(protocolsDir, name, 'protocol.md'))); + expect(missing).toEqual([]); + }); +}); + +describe('skeleton sweep + embeds (issue #1011 Layers 1/2, Patch 2)', () => { + const protocols = ['air', 'aspir', 'bugfix', 'experiment', 'maintain', 'pir', 'research', 'spike', 'spir']; + + it('no builder-prompt.md references protocol.md by path', () => { + for (const p of protocols) { + const f = join(SKELETON, 'protocols', p, 'builder-prompt.md'); + if (!existsSync(f)) continue; + expect(readFileSync(f, 'utf-8')).not.toMatch(/protocol\.md/); + } + }); + + it('roles/builder.md no longer cats the protocol by path', () => { + const f = join(SKELETON, 'roles', 'builder.md'); + expect(readFileSync(f, 'utf-8')).not.toMatch(/cat codev\/protocols\//); + }); + + it('spir/protocol.md no longer points at workflow-reference.md (A.3)', () => { + const f = join(SKELETON, 'protocols', 'spir', 'protocol.md'); + expect(readFileSync(f, 'utf-8')).not.toMatch(/workflow-reference\.md/); + }); + + it('spir/aspir plan prompts deliver the canonical plan template via include (not a divergent inline copy)', () => { + for (const p of ['spir', 'aspir']) { + const md = readFileSync(join(SKELETON, 'protocols', p, 'prompts', 'plan.md'), 'utf-8'); + // Uses the fresh-at-delivery include of the real template (which carries the + // machine-readable phases JSON porch requires), not a hand-rolled inline copy. + expect(md).toContain('{{> protocols/spir/templates/plan.md}}'); + expect(md).not.toContain('### Plan Structure'); // the divergent JSON-less inline block is gone + // No literal-path / shell-fetch reference to the template (the include is resolver-mediated). + expect(md).not.toMatch(/codev\/protocols\/spir\/templates\/plan\.md/); + } + }); + + it('the plan template (delivered via the include) carries the porch-required phases JSON', () => { + const tmpl = readFileSync(join(SKELETON, 'protocols', 'spir', 'templates', 'plan.md'), 'utf-8'); + expect(tmpl).toMatch(/"phases"\s*:/); // has_phases_json + expect((tmpl.match(/"id"\s*:/g) || []).length).toBeGreaterThanOrEqual(2); // min_two_phases + }); + + it('the plan phase prompt, once its includes resolve (porch delivery), carries the phases JSON', () => { + // Mirrors what porch's loadPromptFile now does: read the phase prompt and + // resolve {{> ...}} includes. The resolved plan prompt must contain the JSON + // the plan gate requires — otherwise the builder writes a plan that fails it. + const planPrompt = readFileSync(join(SKELETON, 'protocols', 'spir', 'prompts', 'plan.md'), 'utf-8'); + const resolved = resolveCodevIncludes(planPrompt, REPO_ROOT); + expect(resolved).not.toContain('{{> protocols/spir/templates/plan.md}}'); // include expanded + expect(resolved).toMatch(/"phases"\s*:/); // JSON now present + }); + + it('experiment/spike reference templates via fresh-at-delivery include, not a static embed', () => { + const cases = [ + { protocol: 'experiment', tmpl: 'notes.md' }, + { protocol: 'spike', tmpl: 'findings.md' }, + ]; + for (const { protocol, tmpl } of cases) { + const md = readFileSync(join(SKELETON, 'protocols', protocol, 'protocol.md'), 'utf-8'); + // Uses the include placeholder, so the canonical template stays single-source + // and is read fresh at spawn — it cannot drift. + expect(md).toContain(`{{> protocols/${protocol}/templates/${tmpl}}}`); + // The static embed (BEGIN/END markers) is gone — that was the drift risk. + expect(md).not.toContain('EMBEDDED TEMPLATE'); + // The canonical template still exists for the include to resolve against. + expect(existsSync(join(SKELETON, 'protocols', protocol, 'templates', tmpl))).toBe(true); + } + }); +}); diff --git a/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts b/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts index a0e07c221..63fc0cead 100644 --- a/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts +++ b/packages/codev/src/agent-farm/__tests__/bugfix-619-aspir-prompt.test.ts @@ -19,10 +19,16 @@ describe('ASPIR builder-prompt protocol reference', () => { '../../../../../codev-skeleton/protocols/aspir/builder-prompt.md', ); - it('references aspir/protocol.md, not spir/protocol.md', () => { + it('does not reference any protocol.md by literal path (#1011 sweep; never spir per #619)', () => { + // #619 originally required the ASPIR prompt to reference aspir/protocol.md + // rather than spir/protocol.md. #1011 (Layer 2) swept the literal protocol.md + // pointer out of every builder-prompt entirely — protocol.md is now inlined + // at spawn. So the #619 intent ("never point at spir's protocol.md") is now + // satisfied by referencing no protocol.md path at all. const content = fs.readFileSync(skeletonPrompt, 'utf-8'); expect(content).not.toContain('codev/protocols/spir/protocol.md'); - expect(content).toContain('codev/protocols/aspir/protocol.md'); + expect(content).not.toContain('codev/protocols/aspir/protocol.md'); + expect(content).not.toMatch(/protocol\.md/); }); it('says "ASPIR protocol", not "SPIR protocol"', () => { diff --git a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/air-builder-prompt.md.baseline b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/air-builder-prompt.md.baseline index 8a44aede1..5836e8036 100644 --- a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/air-builder-prompt.md.baseline +++ b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/air-builder-prompt.md.baseline @@ -24,7 +24,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the AIR protocol: `codev/protocols/air/protocol.md` +Follow the AIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if issue}} ## Issue #{{issue.number}} diff --git a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/aspir-builder-prompt.md.baseline b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/aspir-builder-prompt.md.baseline index 2715ed877..a50bdd6a8 100644 --- a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/aspir-builder-prompt.md.baseline +++ b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/aspir-builder-prompt.md.baseline @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the ASPIR protocol: `codev/protocols/aspir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the ASPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if spec}} ## Spec diff --git a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/spir-builder-prompt.md.baseline b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/spir-builder-prompt.md.baseline index c905b4696..f56f68082 100644 --- a/packages/codev/src/agent-farm/__tests__/fixtures/baselines/spir-builder-prompt.md.baseline +++ b/packages/codev/src/agent-farm/__tests__/fixtures/baselines/spir-builder-prompt.md.baseline @@ -27,8 +27,7 @@ You are running in STRICT mode. This means: {{/if}} ## Protocol -Follow the SPIR protocol: `codev/protocols/spir/protocol.md` -Read and internalize the protocol before starting any work. +Follow the SPIR protocol. Read and internalize the protocol before starting any work. The full protocol text is included below under **## Protocol Reference (full text)**. {{#if spec}} ## Spec diff --git a/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts b/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts index e955d222f..1f9b4ea04 100644 --- a/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts +++ b/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts @@ -17,6 +17,7 @@ import { loadProtocolRole, } from '../commands/spawn-roles.js'; import type { TemplateContext } from '../commands/spawn-roles.js'; +import { logger } from '../utils/logger.js'; // mocked below; vi.fn()s for assertions // Mock dependencies vi.mock('../utils/logger.js', () => ({ @@ -39,21 +40,32 @@ const skeletonMock = vi.hoisted(() => ({ root: '' as string })); vi.mock('../../lib/skeleton.js', async () => { const fs = await import('node:fs'); const path = await import('node:path'); + // Mirrors the real four-tier resolver: .codev/ → codev/ → cache → skeleton. + // The cache tier is omitted (irrelevant to these tests). + const resolveCodevFile = (relativePath: string, workspaceRoot?: string): string | null => { + const root = workspaceRoot || process.cwd(); + const overridePath = path.join(root, '.codev', relativePath); + if (fs.existsSync(overridePath)) return overridePath; + const localPath = path.join(root, 'codev', relativePath); + if (fs.existsSync(localPath)) return localPath; + if (skeletonMock.root) { + const skeletonPath = path.join(skeletonMock.root, relativePath); + if (fs.existsSync(skeletonPath)) return skeletonPath; + } + return null; + }; + // Mirror of the real resolveCodevIncludes, using the mocked resolver. + const resolveCodevIncludes = (content: string, workspaceRoot?: string, depth = 0): string => { + if (depth > 5) return content; + return content.replace(/\{\{>\s*([^}\s]+)\s*\}\}/g, (_m, rel: string) => { + const resolved = resolveCodevFile(rel, workspaceRoot); + if (!resolved) return ''; + return resolveCodevIncludes(fs.readFileSync(resolved, 'utf-8'), workspaceRoot, depth + 1); + }); + }; return { - // Mirrors the real four-tier resolver: .codev/ → codev/ → cache → skeleton. - // The cache tier is omitted (irrelevant to these tests). - resolveCodevFile: (relativePath: string, workspaceRoot?: string): string | null => { - const root = workspaceRoot || process.cwd(); - const overridePath = path.join(root, '.codev', relativePath); - if (fs.existsSync(overridePath)) return overridePath; - const localPath = path.join(root, 'codev', relativePath); - if (fs.existsSync(localPath)) return localPath; - if (skeletonMock.root) { - const skeletonPath = path.join(skeletonMock.root, relativePath); - if (fs.existsSync(skeletonPath)) return skeletonPath; - } - return null; - }, + resolveCodevFile, + resolveCodevIncludes, getSkeletonDir: (): string => skeletonMock.root, }; }); @@ -145,8 +157,8 @@ describe('spawn-roles', () => { // ========================================================================= describe('buildPromptFromTemplate', () => { - it('falls back to inline prompt when no template file exists', () => { - // Config with non-existent protocols dir + it('throws (no silent fallback) when a protocol has no builder-prompt.md (#1011)', () => { + // Non-existent protocols dir → no builder-prompt.md resolves anywhere. const config = { codevDir: '/nonexistent/codev', workspaceRoot: '/workspace', @@ -160,10 +172,9 @@ describe('spawn-roles', () => { mode_strict: true, input_description: 'a feature', }; - const result = buildPromptFromTemplate(config, 'spir', context); - expect(result).toContain('SPIR Builder (strict mode)'); - expect(result).toContain('a feature'); - expect(result).toContain('STRICT'); + // Fail fast rather than synthesize a degraded prompt that points the builder + // at codev/protocols/... by literal path (bypasses the resolver, breaks fresh installs). + expect(() => buildPromptFromTemplate(config, 'spir', context)).toThrow(/no builder-prompt\.md/); }); }); @@ -319,7 +330,12 @@ describe('spawn-roles', () => { ); fs.writeFileSync( path.join(skeletonRoot, 'protocols', 'spir', 'builder-prompt.md'), - '# {{protocol_name}} prompt for {{input_description}}', + // #1011: the {{protocol_reference}} placeholder is filled fresh at spawn + // from protocol.md (and its {{> ...}} includes). Unconditional — every + // shipped protocol ships a protocol.md (guarded by the completeness test + // below), so there is no {{#if}} guard. + '# {{protocol_name}} prompt for {{input_description}}\n' + + '## Protocol Reference (full text)\n{{protocol_reference}}\n', ); fs.mkdirSync(path.join(skeletonRoot, 'protocols', 'bugfix'), { recursive: true }); fs.writeFileSync( @@ -341,6 +357,27 @@ describe('spawn-roles', () => { ); }); + it('validateProtocol warns (non-fatally) when a protocol has protocol.json but no protocol.md (issue #1011)', () => { + // The beforeEach creates spir/ with protocol.json but no protocol.md. + expect(() => validateProtocol(makeConfig(), 'spir')).not.toThrow(); // non-fatal + expect(logger.warn).toHaveBeenCalledWith(expect.stringContaining('no protocol.md')); + }); + + it('validateProtocol does NOT warn when protocol.md is present', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + fs.writeFileSync(path.join(skeletonRoot, 'protocols', 'spir', 'protocol.md'), '# SPIR'); + validateProtocol(makeConfig(), 'spir'); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('validateProtocol fails fast when a protocol has no builder-prompt.md (#1011)', () => { + // The skeleton's bugfix/ ships protocol.json but no builder-prompt.md. Rather + // than fall back to a prompt that points at codev/protocols/... by literal + // path, spawning must fail fast. + expect(() => validateProtocol(makeConfig(), 'bugfix')).toThrow(/no builder-prompt\.md/); + }); + it('loadProtocol falls back to skeleton', () => { const protocol = loadProtocol(makeConfig(), 'spir'); expect(protocol).toEqual({ name: 'spir', version: '1', phases: [] }); @@ -363,6 +400,79 @@ describe('spawn-roles', () => { expect(prompt).toContain('SPIR prompt for a v3 feature'); }); + it('fills {{protocol_reference}} with protocol.md content fresh at delivery (issue #1011)', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + fs.writeFileSync( + path.join(skeletonRoot, 'protocols', 'spir', 'protocol.md'), + '# SPIR Protocol\n\nMETA_DOC_SENTINEL: gate semantics and when-to-use guidance.', + ); + + const ctx: TemplateContext = { + protocol_name: 'SPIR', + mode: 'strict', + mode_soft: false, + mode_strict: true, + input_description: 'a v3 feature', + }; + const prompt = buildPromptFromTemplate(makeConfig(), 'spir', ctx); + + // Still carries the rendered builder-prompt template... + expect(prompt).toContain('SPIR prompt for a v3 feature'); + // ...plus the protocol meta-doc, substituted into the {{protocol_reference}} + // placeholder under the template's delimiter heading (read fresh, not committed). + expect(prompt).toContain('## Protocol Reference (full text)'); + expect(prompt).toContain('META_DOC_SENTINEL: gate semantics and when-to-use guidance.'); + }); + + it('resolves {{> ...}} template includes inside protocol.md fresh at delivery (issue #1011)', async () => { + const fs = await import('node:fs'); + const path = await import('node:path'); + // protocol.md references a template via an include directive rather than a + // committed copy — the resolver reads the template fresh, so it can't drift. + fs.mkdirSync(path.join(skeletonRoot, 'protocols', 'spir', 'templates'), { recursive: true }); + fs.writeFileSync( + path.join(skeletonRoot, 'protocols', 'spir', 'templates', 'plan.md'), + 'TEMPLATE_SENTINEL: the canonical plan template body.', + ); + fs.writeFileSync( + path.join(skeletonRoot, 'protocols', 'spir', 'protocol.md'), + '# SPIR Protocol\n\nUse the template below:\n\n{{> protocols/spir/templates/plan.md}}\n', + ); + + const ctx: TemplateContext = { + protocol_name: 'SPIR', + mode: 'strict', + mode_soft: false, + mode_strict: true, + input_description: 'a v3 feature', + }; + const prompt = buildPromptFromTemplate(makeConfig(), 'spir', ctx); + + // The include directive is gone (resolved), replaced by the template body. + expect(prompt).not.toContain('{{> protocols/spir/templates/plan.md}}'); + expect(prompt).toContain('TEMPLATE_SENTINEL: the canonical plan template body.'); + }); + + it('builds the prompt without error when protocol.md is absent (issue #1011)', () => { + // The skeleton-fallback beforeEach creates spir/ with no protocol.md. The + // reference is now unconditional (no {{#if}} guard), so {{protocol_reference}} + // resolves to empty rather than the prompt omitting the section — the build + // must still succeed without error. (Shipped protocols can't hit this: the + // completeness test guarantees every shipped protocol has a protocol.md.) + const ctx: TemplateContext = { + protocol_name: 'SPIR', + mode: 'strict', + mode_soft: false, + mode_strict: true, + input_description: 'a v3 feature', + }; + const prompt = buildPromptFromTemplate(makeConfig(), 'spir', ctx); + + expect(prompt).toContain('SPIR prompt for a v3 feature'); + expect(prompt).not.toContain('{{protocol_reference}}'); // placeholder resolved (to empty), not left raw + }); + it('local codev/protocols/ takes precedence over skeleton', async () => { const fs = await import('node:fs'); const path = await import('node:path'); diff --git a/packages/codev/src/agent-farm/commands/spawn-roles.ts b/packages/codev/src/agent-farm/commands/spawn-roles.ts index 807aff34d..66189c7c4 100644 --- a/packages/codev/src/agent-farm/commands/spawn-roles.ts +++ b/packages/codev/src/agent-farm/commands/spawn-roles.ts @@ -13,7 +13,7 @@ import type { SpawnOptions, Config, ProtocolDefinition } from '../types.js'; import { logger, fatal } from '../utils/logger.js'; import { loadRolePrompt } from '../utils/roles.js'; import { stripLeadingZeros } from '../utils/agent-names.js'; -import { resolveCodevFile, getSkeletonDir } from '../../lib/skeleton.js'; +import { resolveCodevFile, getSkeletonDir, resolveCodevIncludes } from '../../lib/skeleton.js'; // ============================================================================= // Template Rendering @@ -45,6 +45,7 @@ export interface TemplateContext { task_text?: string; spec_missing?: boolean; existing_branch?: string; // Spec 609: when --branch is used, the name of the existing branch + protocol_reference?: string; // #1011: protocol.md text, resolved fresh at spawn and inlined via the {{protocol_reference}} placeholder } /** @@ -101,59 +102,37 @@ function loadBuilderPromptTemplate(config: Config, protocolName: string): string `protocols/${protocolName}/builder-prompt.md`, config.workspaceRoot, ); - if (templatePath) { - return readFileSync(templatePath, 'utf-8'); + if (!templatePath) { + return null; } - return null; + return readFileSync(templatePath, 'utf-8'); } /** - * Build a fallback prompt when no template exists + * Compute the protocol meta-doc text to inline into the spawn prompt via the + * `{{protocol_reference}}` placeholder. Reads `protocol.md` fresh through the + * resolver (tier 4 reaches the embedded skeleton in fresh installs) and resolves + * any `{{> ...}}` template includes inside it (via the shared resolver helper). + * Returns '' when the protocol ships no `protocol.md`. */ -function buildFallbackPrompt(protocolName: string, context: TemplateContext): string { - const modeInstructions = context.mode === 'strict' - ? `## Mode: STRICT -Porch orchestrates your work. Run: \`porch next\` to get your next tasks.` - : `## Mode: SOFT -You follow the protocol yourself. The architect monitors your work and verifies compliance.`; - - let prompt = `# ${protocolName.toUpperCase()} Builder (${context.mode} mode) - -You are implementing ${context.input_description}. - -${modeInstructions} - -## Protocol -Follow the ${protocolName.toUpperCase()} protocol in \`codev/protocols/${protocolName}/\`. -Read and internalize the protocol before starting any work. -`; - - if (context.spec) { - prompt += `\n## Spec\nRead the specification at: \`${context.spec.path}\`\n`; - } - - if (context.plan) { - prompt += `\n## Plan\nFollow the implementation plan at: \`${context.plan.path}\`\n`; - } - - if (context.issue) { - prompt += `\n## Issue #${context.issue.number} -**Title**: ${context.issue.title} - -**Description**: -${context.issue.body || '(No description provided)'} -`; - } - - if (context.task_text) { - prompt += `\n## Task\n${context.task_text}\n`; +function resolveProtocolReference(config: Config, protocolName: string): string { + const protocolDocPath = resolveCodevFile( + `protocols/${protocolName}/protocol.md`, + config.workspaceRoot, + ); + if (!protocolDocPath) { + logger.debug(`No protocol.md for ${protocolName}; spawning without inlined reference`); + return ''; } - - return prompt; + return resolveCodevIncludes(readFileSync(protocolDocPath, 'utf-8'), config.workspaceRoot); } /** - * Build the prompt using protocol template or fallback to inline prompt + * Build the spawn prompt from the protocol's builder-prompt.md template. + * Fails fast if the protocol ships no builder-prompt.md (no silent fallback): + * a synthesized fallback would have to point the builder at codev/protocols/... + * by literal path, which bypasses the four-tier resolver and breaks fresh + * installs — the bug class this work exists to fix (#1011). */ export function buildPromptFromTemplate( config: Config, @@ -161,13 +140,19 @@ export function buildPromptFromTemplate( context: TemplateContext ): string { const template = loadBuilderPromptTemplate(config, protocolName); - if (template) { - logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`); - return renderTemplate(template, context); + if (!template) { + // validateProtocol already fails fast on a missing builder-prompt.md at spawn; + // this is the defensive backstop for any path that reaches here without it. + fatal( + `Protocol "${protocolName}" has no builder-prompt.md; cannot build a spawn prompt. ` + + `Add protocols/${protocolName}/builder-prompt.md.`, + ); } - // Fallback: no template found, return a basic prompt - logger.debug(`No template found for ${protocolName}, using inline prompt`); - return buildFallbackPrompt(protocolName, context); + logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`); + // Deliver the protocol meta-doc (and any templates it includes) fresh at + // spawn via the {{protocol_reference}} placeholder, never a committed copy. + const protocol_reference = resolveProtocolReference(config, protocolName); + return renderTemplate(template, { ...context, protocol_reference }); } // ============================================================================= @@ -292,6 +277,34 @@ export function validateProtocol(config: Config, protocolName: string): void { const available = dirs.length > 0 ? `\n\nAvailable protocols: ${dirs.join(', ')}` : ''; fatal(`Protocol not found: ${protocolName}${available}`); } + + // #1011: a builder-prompt.md is required to spawn. Fail fast rather than + // synthesize a degraded fallback prompt that points the builder at + // codev/protocols/... by literal path (which bypasses the four-tier resolver + // and breaks fresh installs). Every shipped protocol ships one, so this only + // fires for a custom/override protocol that omitted it. + const builderPrompt = resolveCodevFile( + `protocols/${protocolName}/builder-prompt.md`, + config.workspaceRoot, + ); + if (!builderPrompt) { + fatal( + `Protocol "${protocolName}" has no builder-prompt.md; cannot build a spawn prompt. ` + + `Add protocols/${protocolName}/builder-prompt.md.`, + ); + } + + // #1011: a protocol.json without a protocol.md is permitted, but the builder + // prompt inlines protocol.md unconditionally — so it would spawn with an empty + // "## Protocol Reference (full text)" section. Shipped protocols can't hit this + // (a completeness test enforces every shipped protocol has a protocol.md); this + // warns (non-fatally) when a project's own custom/override protocol omits it. + if (protocolJson && !protocolMd) { + logger.warn( + `Protocol "${protocolName}" has a protocol.json but no protocol.md; builders will ` + + `spawn with an empty Protocol Reference section. Add protocols/${protocolName}/protocol.md.`, + ); + } } /** diff --git a/packages/codev/src/commands/doctor.ts b/packages/codev/src/commands/doctor.ts index 968d742de..8e3c434c5 100644 --- a/packages/codev/src/commands/doctor.ts +++ b/packages/codev/src/commands/doctor.ts @@ -13,6 +13,7 @@ import { query as claudeQuery } from '@anthropic-ai/claude-agent-sdk'; import { executeForgeCommandSync, loadForgeConfig, validateForgeConfig, resolveAllConcepts, type ConceptResolution } from '../lib/forge.js'; import { detectHarnessFromCommand } from '../agent-farm/utils/harness.js'; import { auditPrGates, formatPrGateWarning } from '../lib/pr-gate-audit.js'; +import { auditFrameworkRefs, formatFrameworkRefFinding, hasFrameworkOverrides } from '../lib/framework-ref-audit.js'; import { resolveAgyBin, AGY_OAUTH_MARKERS } from './consult/index.js'; const __filename = fileURLToPath(import.meta.url); @@ -723,6 +724,31 @@ export async function doctor(): Promise { } console.log(''); + // Framework-file reference audit (#1011): the user's OWN codev/ overrides + // (codev/protocols, codev/roles) must not instruct shell reads of framework + // docs by literal path, which bypass the resolver and fail in fresh installs. + // Scans the project's local overrides only (the shipped skeleton is the + // framework's responsibility, guarded by its own CI). Warn-not-error, and a + // true no-op (no output) when the project ships no protocol/role overrides. + const codevRoot = resolve(workspaceRoot, 'codev'); + if (hasFrameworkOverrides(codevRoot)) { + const frameworkRefs = auditFrameworkRefs(codevRoot); + if (frameworkRefs.length === 0) { + console.log(` ${chalk.green('✓')} No literal-path shell reads of framework files in codev/ overrides`); + } else { + for (const finding of frameworkRefs) { + console.log(` ${chalk.yellow('⚠')} ${formatFrameworkRefFinding(finding)}`); + warnings++; + warningDetails.push({ + name: 'Framework refs', + issue: `codev/${formatFrameworkRefFinding(finding)}`, + recommendation: 'Deliver framework content via spawn/porch inline; see the "Framework files" convention in CLAUDE.md / AGENTS.md', + }); + } + } + console.log(''); + } + // Protocol PR-gate audit (#943): a PR-producing protocol whose resolved // definition (local override included) lost its `pr` gate will silently // stop surfacing PRs in Needs Attention (post-#927). Warn loudly; non-fatal. diff --git a/packages/codev/src/commands/porch/prompts.ts b/packages/codev/src/commands/porch/prompts.ts index 0636bffd3..d1e8c8329 100644 --- a/packages/codev/src/commands/porch/prompts.ts +++ b/packages/codev/src/commands/porch/prompts.ts @@ -16,7 +16,7 @@ import { findPlanFile, getCurrentPlanPhase, getPhaseContent } from './plan.js'; import { getProjectDir, resolveArtifactBaseName } from './state.js'; import type { ArtifactResolver } from './artifacts.js'; import { fetchIssue } from '../../lib/github.js'; -import { resolveCodevFile } from '../../lib/skeleton.js'; +import { resolveCodevFile, resolveCodevIncludes } from '../../lib/skeleton.js'; /** * Get project summary from GitHub Issues, with spec-file fallback. @@ -78,7 +78,11 @@ function loadPromptFile(workspaceRoot: string, protocolName: string, promptFile: const relativePath = `protocols/${protocolName}/prompts/${promptFile}`; const resolved = resolveCodevFile(relativePath, workspaceRoot); if (!resolved) return null; - return fs.readFileSync(resolved, 'utf-8'); + // Resolve `{{> ...}}` includes (e.g. a phase's template) fresh through the + // resolver, so phase prompts can pull in framework files (like the plan + // template, with its required machine-readable phases JSON) without a + // committed copy. Mirrors the spawn-side protocol.md inlining. #1011. + return resolveCodevIncludes(fs.readFileSync(resolved, 'utf-8'), workspaceRoot); } /** diff --git a/packages/codev/src/lib/framework-ref-audit.ts b/packages/codev/src/lib/framework-ref-audit.ts new file mode 100644 index 000000000..bb49557a3 --- /dev/null +++ b/packages/codev/src/lib/framework-ref-audit.ts @@ -0,0 +1,95 @@ +/** + * Audit a codev root for resolver-bypassing shell reads of framework files by + * literal path (issue #1011, Layer 3 — regression guard). + * + * Used against two roots, for two purposes: + * - the framework's `codev-skeleton/` source, in the framework's own CI/unit + * test (guards what the package *ships*); + * - a project's local `codev/` overrides, by `codev doctor` (guards what the + * *user* customizes — the shipped skeleton is the framework's responsibility, + * not the end user's). + * + * The bug class: a builder-side consumer instructs `cat`/`cp` of a framework + * doc by literal `codev/...` path. Shell commands bypass the four-tier resolver + * (`resolveCodevFile`), so the read fails in fresh installs where the file + * lives only in the embedded skeleton. Framework content is delivered to the + * builder via the spawn prompt and porch JSON instead — never fetched by shell. + * + * Scope is deliberately NARROW to avoid false positives (the skeleton is full + * of legitimate `codev/...` mentions): + * - Only shell-fetch verbs (cat/cp/less/more/head/tail/source) reading a + * `codev/protocols/...` or `codev/roles/...` path are flagged. + * - `codev/resources/` is NOT scanned: it mixes framework docs with + * user-evolved files (`arch.md`, `lessons-learned.md`) that builders and + * architects reference and write by path legitimately. + * - Plain documentation references (backtick mentions, "see codev/…") are + * NOT flagged. The rule is about *fetching*, not *referencing* — the + * protocol list in CLAUDE.md/AGENTS.md, for example, is intentional. + */ +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { join, relative } from 'node:path'; + +export interface FrameworkRefFinding { + /** Path relative to the scanned codev root. */ + file: string; + line: number; + /** The offending line, trimmed. */ + text: string; +} + +/** Framework subdirectories whose docs ship in the skeleton and resolve via the resolver. */ +const FRAMEWORK_DIRS = ['protocols', 'roles'] as const; + +/** + * A shell command that reads/copies a framework file by literal `codev/` path. + * Matches e.g. `cat codev/protocols/spir/protocol.md`, `cp codev/roles/builder.md x`. + */ +const SHELL_FETCH_RE = + /(?:^|[\s|;&(])(?:cat|cp|less|more|head|tail|source)\s+codev\/(?:protocols|roles)\/\S+\.md/; + +function collectMarkdown(dir: string, out: string[]): void { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = join(dir, entry.name); + if (entry.isDirectory()) collectMarkdown(full, out); + else if (entry.isFile() && entry.name.endsWith('.md')) out.push(full); + } +} + +/** + * Scan `/{protocols,roles}` for shell-fetch-by-literal-path violations. + * `rootDir` is a codev root: the framework's `codev-skeleton/` (CI) or a project's + * `codev/` (doctor). Returns one finding per offending line; empty when the + * protocols/roles dirs don't exist (e.g. a project with no local overrides). + */ +export function auditFrameworkRefs(rootDir: string): FrameworkRefFinding[] { + const findings: FrameworkRefFinding[] = []; + for (const sub of FRAMEWORK_DIRS) { + const base = join(rootDir, sub); + if (!existsSync(base)) continue; + const files: string[] = []; + collectMarkdown(base, files); + for (const file of files) { + const lines = readFileSync(file, 'utf-8').split('\n'); + lines.forEach((text, i) => { + if (SHELL_FETCH_RE.test(text)) { + findings.push({ file: relative(rootDir, file), line: i + 1, text: text.trim() }); + } + }); + } + } + return findings; +} + +/** + * Whether the project has any local framework overrides to scan, i.e. a + * `codev/protocols` or `codev/roles` directory exists. doctor uses this to stay + * a true no-op (no output) for projects that ship no overrides: an empty audit + * result alone cannot distinguish "nothing to scan" from "scanned, all clean". + */ +export function hasFrameworkOverrides(codevRoot: string): boolean { + return FRAMEWORK_DIRS.some((sub) => existsSync(join(codevRoot, sub))); +} + +export function formatFrameworkRefFinding(f: FrameworkRefFinding): string { + return `${f.file}:${f.line} shell-fetches a framework file by literal path: ${f.text}`; +} diff --git a/packages/codev/src/lib/skeleton.ts b/packages/codev/src/lib/skeleton.ts index bb9be7636..3a87830a4 100644 --- a/packages/codev/src/lib/skeleton.ts +++ b/packages/codev/src/lib/skeleton.ts @@ -94,6 +94,30 @@ export function resolveCodevFile(relativePath: string, workspaceRoot?: string): return null; } +/** + * Resolve `{{> }}` include directives by reading the + * referenced framework file fresh through the four-tier resolver and + * substituting its content in place (recursively). This is how framework files + * (a protocol's `protocol.md` at spawn, a phase's template in a porch prompt) are + * delivered to the builder without committing a copy — the canonical file stays + * single-source and is read at delivery time, so it cannot drift. + * + * An include that does not resolve collapses to '' (never an error — the file + * genuinely isn't shipped). Depth-guarded against include cycles. + */ +export function resolveCodevIncludes( + content: string, + workspaceRoot?: string, + depth = 0, +): string { + if (depth > 5) return content; // cycle / runaway guard + return content.replace(/\{\{>\s*([^}\s]+)\s*\}\}/g, (_match, relPath: string) => { + const resolved = resolveCodevFile(relPath, workspaceRoot); + if (!resolved) return ''; + return resolveCodevIncludes(fs.readFileSync(resolved, 'utf-8'), workspaceRoot, depth + 1); + }); +} + /** * Framework cache directory management. *