diff --git a/AGENTS.md b/AGENTS.md index b7b8bda03..d63df37e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,14 @@ 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, don't shell-fetch + +Protocol docs, role docs, and the shipped `codev/resources/` reference docs (`workflow-reference.md`, `risk-triage.md`, `commands/`) resolve through the four-tier lookup above and **default to the package skeleton**. A project may override any of them by checking a copy into `codev/...` (tier 2), but a fresh project won't have them on disk, so don't rely on it. + +So in any prompt, role doc, or instruction that drives a builder, don't `cat`/`cp` a framework file by literal `codev/...` path: a shell read 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 project files, not framework files, so referencing those by path is correct too. + +`codev doctor` audits the skeleton for this. + ### 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..d63df37e2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -88,6 +88,14 @@ 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, don't shell-fetch + +Protocol docs, role docs, and the shipped `codev/resources/` reference docs (`workflow-reference.md`, `risk-triage.md`, `commands/`) resolve through the four-tier lookup above and **default to the package skeleton**. A project may override any of them by checking a copy into `codev/...` (tier 2), but a fresh project won't have them on disk, so don't rely on it. + +So in any prompt, role doc, or instruction that drives a builder, don't `cat`/`cp` a framework file by literal `codev/...` path: a shell read 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 project files, not framework files, so referencing those by path is correct too. + +`codev doctor` audits the skeleton for this. + ### 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..4e786b07e 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -73,3 +73,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/aspir/builder-prompt.md b/codev-skeleton/protocols/aspir/builder-prompt.md index 4af70ce4f..049149c53 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -111,3 +110,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/aspir/prompts/plan.md b/codev-skeleton/protocols/aspir/prompts/plan.md index 3549654c5..c20cbd1dc 100644 --- a/codev-skeleton/protocols/aspir/prompts/plan.md +++ b/codev-skeleton/protocols/aspir/prompts/plan.md @@ -76,7 +76,6 @@ After completing the plan draft, signal completion. Porch will run 3-way consult Create the plan file at `codev/plans/{{artifact_name}}.md`. -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. ### Plan Structure diff --git a/codev-skeleton/protocols/bugfix/builder-prompt.md b/codev-skeleton/protocols/bugfix/builder-prompt.md index 3f56828bb..011c749a8 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#if issue}} ## Issue #{{issue.number}} @@ -67,3 +67,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/experiment/builder-prompt.md b/codev-skeleton/protocols/experiment/builder-prompt.md index 23f378975..2a8d86923 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## EXPERIMENT Overview The EXPERIMENT protocol ensures disciplined experimentation: @@ -74,3 +74,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/experiment/protocol.md b/codev-skeleton/protocols/experiment/protocol.md index 5dd71b271..ded112f73 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]" @@ -222,3 +222,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..0eff31647 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## MAINTAIN Overview @@ -54,3 +54,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/pir/builder-prompt.md b/codev-skeleton/protocols/pir/builder-prompt.md index 0edd1d992..bbc6ae307 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review @@ -87,6 +86,14 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/research/builder-prompt.md b/codev-skeleton/protocols/research/builder-prompt.md index ae501326e..c01220055 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## RESEARCH Overview @@ -83,3 +83,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/spike/builder-prompt.md b/codev-skeleton/protocols/spike/builder-prompt.md index 8d8a97dfb..67ea3a0e8 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#if task}} ## Spike Question @@ -60,3 +60,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} 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..d8d264320 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -112,3 +111,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev-skeleton/protocols/spir/prompts/plan.md b/codev-skeleton/protocols/spir/prompts/plan.md index 3549654c5..c20cbd1dc 100644 --- a/codev-skeleton/protocols/spir/prompts/plan.md +++ b/codev-skeleton/protocols/spir/prompts/plan.md @@ -76,7 +76,6 @@ After completing the plan draft, signal completion. Porch will run 3-way consult Create the plan file at `codev/plans/{{artifact_name}}.md`. -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. ### Plan Structure diff --git a/codev-skeleton/protocols/spir/protocol.md b/codev-skeleton/protocols/spir/protocol.md index 188c2bda1..c531ef5e8 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 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..13c22858e --- /dev/null +++ b/codev/plans/1011-agent-farm-inline-protocol-md-.md @@ -0,0 +1,171 @@ +# 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.** `spir`/`aspir` `prompts/plan.md` already embed a + self-contained `### Plan Structure` block (a *simpler, different* layout than the 184-line + canonical `templates/plan.md`) — the template pointer is **redundant chrome**. Only + `experiment` (`notes.md`, 97L) and `spike` (`findings.md`, 67L) are **genuine content**. +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. + +## 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, under `{{#if protocol_reference}}`, + 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). `{{#if}}` makes bugfix (no protocol.md) + render cleanly with neither the reference sentence nor the section. +- 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 placeholder. [revised from explicit-embed]** +Sub-handled by reference kind (finding #1): drop the redundant pointer in `spir`/`aspir` plan +prompts (already self-contained via `### Plan Structure`); in `experiment`/`spike` `protocol.md` +reword the cp/use-template step to point at the `## Template:` section, which holds a `{{> ...}}` +include resolved fresh at delivery. Auto-detect (blanket scan) is still rejected — it would +double-deliver a conflicting plan layout for spir/aspir. The include directive is explicit (the +author marks exactly what to inline) so it keeps that discrimination without committing a copy. + +**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 semantics: warn-not-error, skeleton-only initially. [agree]** +`codev doctor` greps for resolver-bypassing **absolute** literal paths only — +`cat codev/(protocols|roles|resources)/…` and backtick-wrapped `` `codev/(protocols|roles|resources)/…` `` — +and reports `status: 'warn'` (never `'fail'`) with a pointer to the convention. Skeleton dir only +for now (user `codev/` customizations are theirs; flagging them risks noise). Scope matches +Layer 2's sweep so a post-sweep skeleton is clean. Relative-path refs (`templates/x.md`, +`spir/protocol.md:215/301`, `experiment/protocol.md:90`) are a lower-severity class **not** +targeted (kept out of both the sweep and the grep) — noted as observed-out-of-scope. + +## bugfix sub-decision — RESOLVED: drop the dead pointer, embed nothing (finding #2) + +`bugfix` is the one protocol whose `protocol.md` isn't in the skeleton (`codev/` has a tracked +548-line copy that was never shipped). Layer 2 drops its `builder-prompt.md:28` pointer like the +other 8 — and for bugfix that is correct on its own merits, **not** a content loss, because: + +- The pointer is a **dead path** in fresh installs regardless (no skeleton `protocol.md`). +- The builder's actionable guidance is **already delivered** via bugfix's phase prompts + (`investigate.md` / `fix.md` / `pr.md`, 264 lines, porch-delivered): grep confirms the + 300-LOC scope, escalation criteria, mandatory regression test, and `Fixes #N` / PR-body + structure are all present there. +- The content **unique** to `protocol.md` is overwhelmingly *not* builder-actionable — + architect-facing phases (spawn / integration-review / cleanup), the ASCII workflow diagram, + comparison tables, plus material that's gone **stale** vs porch orchestration (manual + `afx send "Merge it"` merge flow, manual architect CMAP, the deprecated projectlist section). + Inlining it would add noise, not signal. + +So: **no skeleton `bugfix/protocol.md` is added here, and nothing is embedded.** The doc's +skeleton-absence + staleness is a real but orthogonal content-hygiene gap, filed as **#1013** +(not absorbed into this plumbing fix). This also makes visible a latent property of Layer 1 — +protocol docs are mixed-audience (builder + architect) yet Layer 1 inlines the whole doc; for +the 8 protocols that have one we inline as-is per the architect's design, and bugfix simply has +nothing to inline. + +## 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` — embed template, reword (Patch 2). +- `{codev-skeleton,codev}/protocols/*/builder-prompt.md` (all 9) — drop protocol.md pointer (Layer 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). + +(bugfix gets only the Layer 2 pointer-drop — no skeleton `protocol.md` added; see the resolved +sub-decision above. Doc hygiene tracked in #1013.) + +## 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 doctor (new, in `doctor.test.ts`): clean skeleton → check `ok`; a fixture with a + deliberate literal path → check `warn` (negative test). + +**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..a413abd24 --- /dev/null +++ b/codev/projects/1011-agent-farm-inline-protocol-md-/status.yaml @@ -0,0 +1,21 @@ +id: '1011' +title: agent-farm-inline-protocol-md- +protocol: pir +phase: implement +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: pending + requested_at: '2026-06-08T02:56:40.545Z' + pr: + status: pending +iteration: 1 +build_complete: false +history: [] +started_at: '2026-06-08T02:27:23.467Z' +updated_at: '2026-06-08T02:56:40.546Z' diff --git a/codev/protocols/air/builder-prompt.md b/codev/protocols/air/builder-prompt.md index 963d5e5fe..4e786b07e 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -73,3 +73,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/aspir/builder-prompt.md b/codev/protocols/aspir/builder-prompt.md index 475721098..44214aa32 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -89,3 +88,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/aspir/prompts/plan.md b/codev/protocols/aspir/prompts/plan.md index 3549654c5..c20cbd1dc 100644 --- a/codev/protocols/aspir/prompts/plan.md +++ b/codev/protocols/aspir/prompts/plan.md @@ -76,7 +76,6 @@ After completing the plan draft, signal completion. Porch will run 3-way consult Create the plan file at `codev/plans/{{artifact_name}}.md`. -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. ### Plan Structure diff --git a/codev/protocols/bugfix/builder-prompt.md b/codev/protocols/bugfix/builder-prompt.md index ef6834110..d2cf0a01c 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#if issue}} ## Issue #{{issue.number}} @@ -67,3 +67,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/experiment/builder-prompt.md b/codev/protocols/experiment/builder-prompt.md index 23f378975..2a8d86923 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## EXPERIMENT Overview The EXPERIMENT protocol ensures disciplined experimentation: @@ -74,3 +74,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/experiment/protocol.md b/codev/protocols/experiment/protocol.md index 7bac432a5..b97dfb09a 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]" @@ -227,3 +227,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..0eff31647 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## MAINTAIN Overview @@ -54,3 +54,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/pir/builder-prompt.md b/codev/protocols/pir/builder-prompt.md index 0edd1d992..bbc6ae307 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} PIR has three phases: 1. **plan** (gated by `plan-approval`) — write `codev/plans/{{artifact_name}}.md`, await human review @@ -87,6 +86,14 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/research/builder-prompt.md b/codev/protocols/research/builder-prompt.md index ae501326e..c01220055 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## RESEARCH Overview @@ -83,3 +83,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/spike/builder-prompt.md b/codev/protocols/spike/builder-prompt.md index 8d8a97dfb..67ea3a0e8 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#if task}} ## Spike Question @@ -60,3 +60,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} 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..5ced4019d 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} ## Baked Decisions @@ -89,3 +88,11 @@ 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 + +{{#if protocol_reference}} + +--- + +## Protocol Reference (full text) + +{{protocol_reference}}{{/if}} diff --git a/codev/protocols/spir/prompts/plan.md b/codev/protocols/spir/prompts/plan.md index 3549654c5..c20cbd1dc 100644 --- a/codev/protocols/spir/prompts/plan.md +++ b/codev/protocols/spir/prompts/plan.md @@ -76,7 +76,6 @@ After completing the plan draft, signal completion. Porch will run 3-way consult Create the plan file at `codev/plans/{{artifact_name}}.md`. -Use the plan template from `codev/protocols/spir/templates/plan.md` if available. ### Plan Structure diff --git a/codev/protocols/spir/protocol.md b/codev/protocols/spir/protocol.md index 9cf392379..73e34d4e1 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 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..aeaf719ea --- /dev/null +++ b/codev/state/pir-1011_thread.md @@ -0,0 +1,53 @@ +# 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. 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..5bd65a8a1 --- /dev/null +++ b/packages/codev/src/__tests__/framework-ref-audit.test.ts @@ -0,0 +1,116 @@ +/** + * 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 } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { auditFrameworkRefs } from '../lib/framework-ref-audit.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)', () => { + if (!existsSync(SKELETON)) return; // resilient if layout shifts + expect(auditFrameworkRefs(SKELETON)).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 drop the redundant template pointer but keep Plan Structure', () => { + for (const p of ['spir', 'aspir']) { + const md = readFileSync(join(SKELETON, 'protocols', p, 'prompts', 'plan.md'), 'utf-8'); + expect(md).not.toMatch(/codev\/protocols\/spir\/templates\/plan\.md/); + expect(md).toContain('### Plan Structure'); + } + }); + + 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..9cc3822fe 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#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..c0a563ce0 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#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..aeb21ce37 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.{{#if protocol_reference}} The full protocol text is included below under **## Protocol Reference (full text)**.{{/if}} {{#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..00faaa5fb 100644 --- a/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts +++ b/packages/codev/src/agent-farm/__tests__/spawn-roles.test.ts @@ -319,7 +319,10 @@ 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); {{#if}} guards it. + '# {{protocol_name}} prompt for {{input_description}}\n' + + '{{#if protocol_reference}}\n## Protocol Reference (full text)\n{{protocol_reference}}{{/if}}\n', ); fs.mkdirSync(path.join(skeletonRoot, 'protocols', 'bugfix'), { recursive: true }); fs.writeFileSync( @@ -363,6 +366,75 @@ 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 and omits the reference when protocol.md is absent (issue #1011)', () => { + // The skeleton-fallback beforeEach creates spir/ with no 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 (full text)'); + }); + 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..3484d5bcc 100644 --- a/packages/codev/src/agent-farm/commands/spawn-roles.ts +++ b/packages/codev/src/agent-farm/commands/spawn-roles.ts @@ -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,10 +102,53 @@ 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'); +} + +/** + * Resolve `{{> }}` include directives by reading the + * referenced framework file fresh through the resolver and substituting its + * content in place (recursively, so an included file may itself include). + * + * This is how framework files are delivered to the builder without committing + * a duplicated copy anywhere: the canonical file (e.g. a protocol's template) + * stays the single source of truth and is read at spawn time, so it can never + * go stale. Unresolvable includes collapse to empty (the file genuinely isn't + * shipped — e.g. an optional template), never an error. + */ +function resolveIncludes(content: string, config: Config, 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, config.workspaceRoot); + if (!resolved) { + logger.debug(`Include not resolved: ${relPath} (skipped)`); + return ''; + } + return resolveIncludes(readFileSync(resolved, 'utf-8'), config, depth + 1); + }); +} + +/** + * 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. Returns '' when the + * protocol ships no `protocol.md` (e.g. bugfix) — the builder-prompt's + * `{{#if protocol_reference}}` guard then renders cleanly with no reference. + */ +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 resolveIncludes(readFileSync(protocolDocPath, 'utf-8'), config); } /** @@ -163,7 +207,10 @@ export function buildPromptFromTemplate( const template = loadBuilderPromptTemplate(config, protocolName); if (template) { logger.info(`Using template: protocols/${protocolName}/builder-prompt.md`); - return renderTemplate(template, context); + // 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 }); } // Fallback: no template found, return a basic prompt logger.debug(`No template found for ${protocolName}, using inline prompt`); diff --git a/packages/codev/src/commands/doctor.ts b/packages/codev/src/commands/doctor.ts index 968d742de..fb8f43b09 100644 --- a/packages/codev/src/commands/doctor.ts +++ b/packages/codev/src/commands/doctor.ts @@ -13,6 +13,8 @@ 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 } from '../lib/framework-ref-audit.js'; +import { getSkeletonDir } from '../lib/skeleton.js'; import { resolveAgyBin, AGY_OAUTH_MARKERS } from './consult/index.js'; const __filename = fileURLToPath(import.meta.url); @@ -702,6 +704,31 @@ export async function doctor(): Promise { console.log(''); + // Framework-file reference audit (#1011): the skeleton must not instruct + // shell reads of framework docs by literal path — those bypass the resolver + // and fail in fresh installs. Warn-not-error; scans the skeleton only. + console.log(chalk.bold('Framework File References') + ' (resolver-safe delivery)'); + console.log(''); + try { + const frameworkRefs = auditFrameworkRefs(getSkeletonDir()); + if (frameworkRefs.length === 0) { + console.log(` ${chalk.green('✓')} No literal-path shell reads of framework files`); + } else { + for (const finding of frameworkRefs) { + console.log(` ${chalk.yellow('⚠')} ${formatFrameworkRefFinding(finding)}`); + warnings++; + warningDetails.push({ + name: 'Framework refs', + issue: formatFrameworkRefFinding(finding), + recommendation: 'Deliver framework content via spawn/porch inline; see the "Framework files" convention in CLAUDE.md / AGENTS.md', + }); + } + } + } catch { + console.log(` ${chalk.dim('○')} skeleton not found — skipped`); + } + console.log(''); + // Check codev directory structure (only if we're in a codev project) const workspaceRoot = findWorkspaceRoot(); if (workspaceRoot && existsSync(resolve(workspaceRoot, 'codev'))) { 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..d81983a07 --- /dev/null +++ b/packages/codev/src/lib/framework-ref-audit.ts @@ -0,0 +1,76 @@ +/** + * Audit the package skeleton for resolver-bypassing shell reads of framework + * files by literal path (issue #1011, Layer 3 — regression guard). + * + * 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 skeleton dir. */ + 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. Returns one finding per offending line. + */ +export function auditFrameworkRefs(skeletonDir: string): FrameworkRefFinding[] { + const findings: FrameworkRefFinding[] = []; + for (const sub of FRAMEWORK_DIRS) { + const base = join(skeletonDir, 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(skeletonDir, file), line: i + 1, text: text.trim() }); + } + }); + } + } + return findings; +} + +export function formatFrameworkRefFinding(f: FrameworkRefFinding): string { + return `${f.file}:${f.line} shell-fetches a framework file by literal path: ${f.text}`; +}