From b6571edae5859253232f612e352e82aad963ccb0 Mon Sep 17 00:00:00 2001 From: James Sesler Date: Mon, 31 Aug 2026 05:03:00 -0400 Subject: [PATCH] fix: a new workspace no longer inherits CodeCartographer's own project state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repository's .codecarto/ is two things at once: the template copied into a user's repo on init, and CodeCartographer's own live workspace. Init copied it wholesale, so every new workspace started with ~40 KB of another project's history presented as its own — a 15 KB backlog of framework deferrals, a thread log carrying the framework's entries, and a closeout from a session where CodeCartographer analyzed itself. The damage was not only clutter. GUIDE.md keys First-Time Project Setup on "no closeouts in closeouts/", so the shipped closeout told every new session it was not the first to touch the project — suppressing the orchestrator role that #97/#98 made the default. Verified against a real init before and after: closeouts/ went from one inherited file to empty. Init now copies through a filter that skips the four orchestrator files and the contents of closeouts/, then seeds BACKLOG.md and THREAD_LOG.md from new templates alongside CONVENTIONS.md and DECISIONS.md. closeouts/ is created explicitly rather than by the copy, because npm tarballs carry no empty directories and a missing directory reads differently to anything listing it. package.json negates the same paths so the tarball stops transporting them either. The one-off CHANGELOG-2026-05-02-feedback-pass.md moves to docs/, where framework history belongs. Existing workspaces are untouched: refresh already treated all four files as user-owned. Template backlog items B16 and B17 ship with it, since both were symptoms of this bug seen from the inside: - B16: templates/backlog-project.md carries the entry shape it specified (rationale, raised-by, preconditions, smallest viable form), and GUIDE.md distinguishes the project backlog from the framework's under Trust Boundaries. - B17: the spec-delta SKILL states that DEFER goes to BACKLOG.md with no D number, that a refinement made while applying is a decision recorded in the audit file, and that an existing D entry's disposition is updated in place rather than superseded. Tests: a fresh workspace has an empty closeouts/, no orchestrator file carries real entries, the seeded backlog is the template rather than the framework's list, every orchestrator file has a template to seed from, and package.json negates each shipped-by-accident path. 408 pass. Co-Authored-By: Claude Opus 5 --- .codecarto/BACKLOG.md | 22 ++++- .codecarto/GUIDE.md | 8 +- .../skills/spec-delta-application/SKILL.md | 4 +- .codecarto/templates/backlog-project.md | 51 ++++++++++ .codecarto/templates/thread-log.md | 36 +++++++ CHANGELOG.md | 1 + core/workspace.ts | 53 +++++++++- .../2026-05-02-framework-feedback-pass.md | 0 extensions/codecarto/index.ts | 3 +- mcp-server/server.ts | 5 +- package.json | 5 + tests/init-workspace-isolation.test.mjs | 96 +++++++++++++++++++ tests/orchestrator-promotion.test.mjs | 6 +- 13 files changed, 281 insertions(+), 9 deletions(-) create mode 100644 .codecarto/templates/backlog-project.md create mode 100644 .codecarto/templates/thread-log.md rename .codecarto/CHANGELOG-2026-05-02-feedback-pass.md => docs/2026-05-02-framework-feedback-pass.md (100%) create mode 100644 tests/init-workspace-isolation.test.mjs diff --git a/.codecarto/BACKLOG.md b/.codecarto/BACKLOG.md index 84198fb..5ff160f 100644 --- a/.codecarto/BACKLOG.md +++ b/.codecarto/BACKLOG.md @@ -151,7 +151,17 @@ Landed as `codecarto_amend` over `scratch/amendments/.yaml` (see `template --- -## B16. Project-level BACKLOG.md template + GUIDE.md project-vs-framework-level clarification +## B16. Project-level BACKLOG.md template + GUIDE.md project-vs-framework-level clarification — SHIPPED + +Landed with the fix for the larger bug it was a symptom of: init copied this repository's +`.codecarto/` wholesale, so every new workspace inherited *this* backlog, thread log, and a +closeout from a CodeCartographer self-analysis session. `templates/backlog-project.md` now +ships with the entry shape B16 specified (rationale, raised-by, preconditions, smallest +viable form), init seeds `BACKLOG.md` and `THREAD_LOG.md` from templates alongside +CONVENTIONS/DECISIONS, and GUIDE.md carries the project-vs-framework distinction under +Trust Boundaries. Original entry below. + + **Raised by:** `Spec-delta-3 application - 2026-05-03.txt` (1 agent — first-run-of-SKILL friction) @@ -164,7 +174,15 @@ Landed as `codecarto_amend` over `scratch/amendments/.yaml` (see `template --- -## B17. SKILL clarification — DECISIONS.md vs BACKLOG.md semantics for DEFER +## B17. SKILL clarification — DECISIONS.md vs BACKLOG.md semantics for DEFER — SHIPPED + +Landed as specified: the spec-delta-application SKILL's triage section now states that DEFER +goes to BACKLOG.md with no D number, that a refinement made while applying is a decision +recorded in the audit file's Decisions Beyond Triage section (lifted to DECISIONS.md only if +cross-cutting), and that an existing D entry's disposition is updated in place rather than +superseded. Original entry below. + + **Raised by:** `Spec-delta-3 application - 2026-05-03.txt` (1 agent — first-run-of-SKILL semantic ambiguity) diff --git a/.codecarto/GUIDE.md b/.codecarto/GUIDE.md index 9194c78..c872742 100644 --- a/.codecarto/GUIDE.md +++ b/.codecarto/GUIDE.md @@ -84,11 +84,17 @@ Some files in this workspace are **read-only instructions** and must not be modi | Closeouts (framework-owned) | `closeouts/-.md`, `THREAD_LOG.md` | Completion writes or updates one canonical closeout and one idempotent index entry. | | Conventions (orchestrator-maintained) | `CONVENTIONS.md` | Cross-cutting patterns promoted to project-wide invariants. Phase executors propose; the orchestrator promotes at the phase boundary — in inline runs, the same chat changing hats. | | Decisions (orchestrator-maintained, append-only) | `DECISIONS.md` | Numbered log of decisions that diverge from spec, prompt, or obvious-default. Completion appends each handoff's `decisions` under `## Completion log`; the orchestrator may re-file entries into categories. | -| Backlog (read-write) | `BACKLOG.md` | Deferred items with rationale. | +| Backlog (orchestrator-maintained) | `BACKLOG.md` | Work this project decided to **defer**, with the reasoning, the preconditions for revisiting, and the smallest viable form. Seeded from `templates/backlog-project.md` at init. | | Scratch (read-write) | `scratch/*` | Working notes; `scratch/checkpoints/.md` is the durable in-phase continuation checkpoint until the phase validates; `scratch/spikes//.md` holds spike reports (`templates/spike-report.md`); `scratch/amendments/.yaml` holds post-pipeline amendments (`templates/amendment.yaml`). | If you are uncertain whether a file should be modified, treat it as read-only. +### Two things named "backlog", and neither is the other + +- **`BACKLOG.md` in this workspace** is *this project's* deferrals: work the project chose not to do yet. `DECISIONS.md` records what the project decided to **do**; `BACKLOG.md` records what it decided to **defer**. Deferrals get no `D` number. When a deferred item is later picked up, remove its entry here and record the decision in `DECISIONS.md`. +- **`status.yaml`'s `post_pipeline` list** is framework-owned lifecycle state, not this file. Items there are retired by `codecarto_amend`, never by hand. +- **CodeCartographer's own backlog** — deferred improvements to the *framework* — lives in the CodeCartographer repository, not in your workspace. If a phase prompt misled you or a validation criterion did not fit, that is feedback to the framework; it does not belong in this file. + ## Pipeline Selection Seven pipeline variants are available. Check the `pipeline` field in `workflow/status.yaml` to see which is active. diff --git a/.codecarto/skills/spec-delta-application/SKILL.md b/.codecarto/skills/spec-delta-application/SKILL.md index c101c1f..e0ebdd4 100644 --- a/.codecarto/skills/spec-delta-application/SKILL.md +++ b/.codecarto/skills/spec-delta-application/SKILL.md @@ -26,11 +26,13 @@ Triage every delta into one of four buckets: |---|---|---| | **APPLY** | Delta is a real correction or required addition; the spec is wrong without it. | Edit the spec body. Add a `[revised per §]` marker at the changed section. Record in DELTAS-APPLIED.md. | | **CLARIFY** | Delta proposes wording change; the spec's *meaning* is correct but the language is ambiguous. | Edit the spec wording (not the rule). Record in DELTAS-APPLIED.md as a clarification. | -| **DEFER** | Delta is a real improvement but not load-bearing for the next implementation step. | Add to BACKLOG.md with rationale and a back-reference. Do NOT edit the spec. | +| **DEFER** | Delta is a real improvement but not load-bearing for the next implementation step. | Add to `BACKLOG.md` with rationale and a back-reference (`templates/backlog-project.md` gives the entry shape). Do NOT edit the spec. | | **REJECT** | Delta is wrong on close reading (premise was incorrect, scope was misread, the rule it proposes already exists, etc.). | Document in DELTAS-APPLIED.md with a one-line rationale. Do NOT edit the spec. | The previous wisdom: any delta you can't decisively bucket should default to DEFER. The cost of a missed correction is one re-application pass; the cost of a bad correction is shipped. +**DEFER goes to `BACKLOG.md`, not `DECISIONS.md`, and gets no `D` number.** `DECISIONS.md` is for what the project decided to *do*; `BACKLOG.md` is for what it decided to *defer*. A refinement you make while applying a delta — the applied text going beyond the literal proposal — is a decision: record it in the audit file's Decisions Beyond Triage section, and lift it into `DECISIONS.md` only if it is cross-cutting. An existing `D` entry for a proposed delta has its disposition updated in place (`APPLIED 2026-05-03 round-3`); it is never superseded by a new entry when applied. + ## Citation convention Every applied or clarified delta leaves a citation marker in the spec body so a future reader can trace the change back to its source. diff --git a/.codecarto/templates/backlog-project.md b/.codecarto/templates/backlog-project.md new file mode 100644 index 0000000..1814793 --- /dev/null +++ b/.codecarto/templates/backlog-project.md @@ -0,0 +1,51 @@ +# Backlog + +Project-level deferrals: work this project decided **not** to do yet, with the reasoning +that made deferring the right call. One entry per deferral. + +This is the project's backlog, not CodeCartographer's. Items about the framework itself — +a phase prompt that misled you, a validation criterion that does not fit — belong in +feedback to the framework, not here. + +**BACKLOG vs DECISIONS.** `DECISIONS.md` records what the project decided to **do**; +this file records what it decided to **defer**. Deferrals get no `D` number. If a deferred +item is later picked up, remove its entry here and record the decision in `DECISIONS.md`. + +## Format + +``` +## . + +**Raised by:** + +**Why deferred:** + +**Preconditions:** + +**Smallest viable form:** +``` + +## Entries + + diff --git a/.codecarto/templates/thread-log.md b/.codecarto/templates/thread-log.md new file mode 100644 index 0000000..1c34dc9 --- /dev/null +++ b/.codecarto/templates/thread-log.md @@ -0,0 +1,36 @@ +# Thread Log — Index + +This file is an **index** of per-session closeouts. Each session writes a full closeout to +`closeouts/-.md` using `templates/closeout-template.md`, and +appends one line here pointing to it. + +The body of each session lives in the closeout file, not in this index. This pattern scales +forever: per-session files are individually small and read-budget-cheap, and avoid the +heredoc-vs-edit sync risks that bite append-to-large-file workflows once the file grows past +~50 KB. + +## Format + +``` +- YYYY-MM-DD — — [closeout](closeouts/YYYY-MM-DD-phase-or-module.md) +``` + +## De-dup discipline + +Before appending, scan the bottom 5 entries. If you see a line with the same date AND same +phase-or-module AND same summary, do not append — the prior session already wrote it. The +framework has no programmatic dedup gate; this is human-discipline. + +A one-liner to surface duplicates from the shell: + +```bash +grep -E '^- [0-9]{4}-[0-9]{2}-[0-9]{2}' .codecarto/THREAD_LOG.md | sort | uniq -d +``` + +## Entries + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 485ea2c..cbfc09a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to this project are documented here. The format is based on ### Fixed +- **A new workspace no longer inherits CodeCartographer's own project state.** This repository's `.codecarto/` is two things at once: the template copied into a user's repo, and CodeCartographer's live workspace. Init copied it wholesale, so every new workspace started with ~40 KB of another project's history presented as its own — a 15 KB backlog of framework deferrals, a thread log with the framework's entries, and a closeout from a session where CodeCartographer analyzed itself. The damage was not only clutter: GUIDE.md keys First-Time Project Setup on `closeouts/` being empty, so the shipped closeout told every new session it was *not* the first to touch the project, suppressing the orchestrator role that #97/#98 made the default. Init now copies through a filter that skips the four orchestrator files and the contents of `closeouts/` (the directory is still created, empty), then seeds `BACKLOG.md` and `THREAD_LOG.md` from new templates alongside `CONVENTIONS.md` and `DECISIONS.md`. The one-off `CHANGELOG-2026-05-02-feedback-pass.md` moved to `docs/`, where framework history belongs. Existing workspaces are untouched — refresh already treated all four files as user-owned. Template backlog items B16 and B17 ship with it: `templates/backlog-project.md` carries the entry shape (rationale, raised-by, preconditions, smallest viable form), GUIDE.md distinguishes the project backlog from the framework's, and the spec-delta SKILL states that DEFER goes to BACKLOG.md with no `D` number while a refinement made during APPLY is a decision. - **Windows: `writeLibraryConfig` no longer ENOENTs on parentless paths** (#128). The hand-rolled `includes("/")` separator check treated every Windows path as a bare filename, leaving `mkdir` a no-op before the write failed. Now derives the parent directory with `dirname()`. - **`isWithinPath` accepts subpaths of filesystem roots** (#130). Appending a separator to an already-terminated root (`/` → `//`, `C:\` → `C:\\`) produced a prefix no real path starts with, rejecting every legitimate subpath. Trailing separators are now respected before prefixing. - **`acquireLock` closes the lock descriptor when `writeFile` throws** (#131). A non-EEXIST write failure previously leaked the file handle until GC. diff --git a/core/workspace.ts b/core/workspace.ts index 9f31fe1..9f62569 100644 --- a/core/workspace.ts +++ b/core/workspace.ts @@ -4,7 +4,7 @@ // atomic status-update primitive used by /codecarto-complete. import { existsSync, readFileSync } from "node:fs"; -import { appendFile, copyFile, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; +import { appendFile, copyFile, cp, mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises"; import { basename, dirname, join, relative } from "node:path"; import { fileURLToPath } from "node:url"; import { acquireLock, applyHandoff, createEmptyStatus, normalizeStatus, parseHandoff } from "./status.ts"; @@ -112,8 +112,59 @@ export async function getWorkspaceState(cwd: string): Promise { + await cp(packagedWorkspaceDir, targetWorkspaceDir, { + recursive: true, + filter: (source) => { + const relativePath = relative(packagedWorkspaceDir, source); + if (!relativePath) return true; // the workspace root itself + const segments = relativePath.split(/[\\/]/); + if (segments.length === 1) return !INIT_EXCLUDED_TOP_LEVEL.has(segments[0]); + // Keep the directory, drop what this repository wrote inside it. + return !INIT_EXCLUDED_DIR_CONTENTS.has(segments[0]); + }, + }); + // The published tarball carries no empty directories, so an excluded-contents + // directory may not exist to be copied at all. Create them either way: a + // workspace whose closeouts/ is missing rather than empty reads differently + // to anything that lists it. + for (const name of INIT_EXCLUDED_DIR_CONTENTS) { + await mkdir(join(targetWorkspaceDir, name), { recursive: true }); + } +} + /** * Seed the orchestrator-maintained files from the workspace's templates * (issue #98): orchestration is on by default, so a fresh workspace starts diff --git a/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md b/docs/2026-05-02-framework-feedback-pass.md similarity index 100% rename from .codecarto/CHANGELOG-2026-05-02-feedback-pass.md rename to docs/2026-05-02-framework-feedback-pass.md diff --git a/extensions/codecarto/index.ts b/extensions/codecarto/index.ts index 1188516..f0d4ea0 100644 --- a/extensions/codecarto/index.ts +++ b/extensions/codecarto/index.ts @@ -16,6 +16,7 @@ import { buildSkillPrompt, buildValidationSummary, canonicalPath, + copyPackagedWorkspace, computePerPhaseTotals, computeTotals, createEmptyStatus, @@ -333,7 +334,7 @@ export default function codeCartographerExtension(pi: ExtensionAPI) { if (!(await pathExists(targetWorkspaceDir))) { await mkdir(ctx.cwd, { recursive: true }); - await cp(sourceWorkspaceDir, targetWorkspaceDir, { recursive: true }); + await copyPackagedWorkspace(targetWorkspaceDir); } const rawStatusPath = join(targetWorkspaceDir, "workflow", "status.yaml"); diff --git a/mcp-server/server.ts b/mcp-server/server.ts index f505d16..66cb7e6 100644 --- a/mcp-server/server.ts +++ b/mcp-server/server.ts @@ -31,6 +31,7 @@ import { BROADSIDE_LENS_IDS, type BroadsideLensId, canonicalPath, + copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, @@ -197,11 +198,11 @@ export async function handleInit(args: { cwd: string; pipeline?: string; force?: if (!(await pathExists(targetWorkspaceDir))) { await mkdir(cwd, { recursive: true }); - await cp(packagedWorkspaceDir, targetWorkspaceDir, { recursive: true }); + await copyPackagedWorkspace(targetWorkspaceDir); } else if (broadsideOnly) { // Merge the template into the scout-only .codecarto/, preserving the // broadside state and results already on disk. - await cp(packagedWorkspaceDir, targetWorkspaceDir, { recursive: true }); + await copyPackagedWorkspace(targetWorkspaceDir); } const statusPath = join(targetWorkspaceDir, "workflow", "status.yaml"); diff --git a/package.json b/package.json index 2cf794f..e5685b7 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,11 @@ }, "files": [ ".codecarto/**/*", + "!.codecarto/BACKLOG.md", + "!.codecarto/THREAD_LOG.md", + "!.codecarto/CONVENTIONS.md", + "!.codecarto/DECISIONS.md", + "!.codecarto/closeouts/**", "agent-skill/**/*", "dist/**/*", "assets/logo.svg", diff --git a/tests/init-workspace-isolation.test.mjs b/tests/init-workspace-isolation.test.mjs new file mode 100644 index 0000000..5334d38 --- /dev/null +++ b/tests/init-workspace-isolation.test.mjs @@ -0,0 +1,96 @@ +// A new workspace must contain this project's state and nothing of anyone +// else's. The repository's own .codecarto/ is both the shipped template and +// CodeCartographer's live workspace, so a wholesale copy handed every user a +// backlog, a thread log, and a closeout belonging to the framework itself. + +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const CODECARTO = join(REPO_ROOT, ".codecarto"); +const { handleInit } = await import(pathToFileURL(`${REPO_ROOT}/mcp-server/server.ts`).href); +const core = await import(pathToFileURL(`${REPO_ROOT}/core/index.ts`).href); + +async function freshWorkspace(fn) { + const cwd = await mkdtemp(join(tmpdir(), "cc-isolation-")); + try { + await handleInit({ cwd, pipeline: "architecture-only" }); + await fn(join(cwd, ".codecarto")); + } finally { + await rm(cwd, { recursive: true, force: true }); + } +} + +test("a fresh workspace starts with an empty closeouts/", async () => { + // GUIDE.md keys First-Time Project Setup on "no closeouts in closeouts/". + // Shipping one told every new session it was not the first to touch the + // project, suppressing the orchestrator role that is meant to be default. + // The directory must exist rather than be absent: npm tarballs carry no + // empty directories, so init creates it instead of relying on the copy. + await freshWorkspace(async (ws) => { + assert.deepEqual(await readdir(join(ws, "closeouts")), [], "closeouts/ must exist and be empty"); + }); +}); + +test("no orchestrator file carries the framework's own project history", async () => { + await freshWorkspace(async (ws) => { + for (const file of ["BACKLOG.md", "THREAD_LOG.md", "CONVENTIONS.md", "DECISIONS.md"]) { + const content = await readFile(join(ws, file), "utf8"); + assert.ok(content.length > 0, `${file} must be seeded`); + // Real entries from this repository's own sessions, as opposed to the + // illustrative examples a template is allowed to carry. + const entries = content + .split(/\n/) + .filter((line) => /^- \d{4}-\d{2}-\d{2} —/.test(line) || /^## B\d+\./.test(line)); + assert.deepEqual(entries, [], `${file} ships with real entries: ${entries.join(" | ")}`); + } + }); +}); + +test("the seeded backlog is the project template, not the framework's deferral list", async () => { + const framework = await readFile(join(CODECARTO, "BACKLOG.md"), "utf8"); + assert.match(framework, /^## B\d+\./m, "the repository's own backlog should still hold its entries"); + + await freshWorkspace(async (ws) => { + const seeded = await readFile(join(ws, "BACKLOG.md"), "utf8"); + assert.notEqual(seeded, framework, "a new workspace must not inherit the framework's backlog"); + assert.match(seeded, /DECISIONS\.md.*decided to \*\*do\*\*/s, "the template must state the BACKLOG/DECISIONS split"); + assert.match(seeded, /Preconditions:/, "entries must ask what has to land first"); + assert.match(seeded, /Smallest viable form:/, "entries must record the smallest viable form"); + }); +}); + +test("every orchestrator file has a template to be seeded from", async () => { + // A seeded file whose template is missing is silently skipped, which is how + // a workspace ends up with no BACKLOG.md and a SKILL telling it to write one. + for (const { file, template } of core.ORCHESTRATOR_FILES) { + const path = join(CODECARTO, "templates", template); + const content = await readFile(path, "utf8").catch(() => null); + assert.ok(content, `${file} names templates/${template}, which does not exist`); + assert.ok(content.trim().length > 100, `templates/${template} is too short to be a real skeleton`); + } +}); + +test("the framework's own workspace state is not published to npm", async () => { + // package.json ships .codecarto/**/*, so anything this repository writes into + // its own workspace rides along into the tarball unless it is negated out. + // Init filters the same paths, but the tarball should not transport another + // project's history either. + const pkg = JSON.parse(await readFile(join(REPO_ROOT, "package.json"), "utf8")); + assert.ok(pkg.files.includes(".codecarto/**/*"), "assumption: the whole template ships"); + for (const { file } of core.ORCHESTRATOR_FILES) { + assert.ok( + pkg.files.includes(`!.codecarto/${file}`), + `package.json must exclude .codecarto/${file} — it is seeded from a template, never shipped`, + ); + } + assert.ok(pkg.files.includes("!.codecarto/closeouts/**"), "package.json must exclude this repository's closeouts"); + + // The stray one-off changelog belongs to the framework's history, not the template. + const stray = (await readdir(CODECARTO)).filter((name) => /^CHANGELOG/.test(name)); + assert.deepEqual(stray, [], `framework history left in the template: ${stray.join(", ")}`); +}); diff --git a/tests/orchestrator-promotion.test.mjs b/tests/orchestrator-promotion.test.mjs index 6830491..15d6762 100644 --- a/tests/orchestrator-promotion.test.mjs +++ b/tests/orchestrator-promotion.test.mjs @@ -70,7 +70,11 @@ test("setup: init seeds the orchestrator files", async () => { CODECARTO = join(WORKSPACE, ".codecarto"); assert.ok(await pathExists(join(CODECARTO, "CONVENTIONS.md")), "init must seed CONVENTIONS.md"); assert.ok(await pathExists(join(CODECARTO, "DECISIONS.md")), "init must seed DECISIONS.md"); - assert.deepEqual(result.structuredContent.seededOrchestratorFiles, ["CONVENTIONS.md", "DECISIONS.md"]); + assert.deepEqual( + result.structuredContent.seededOrchestratorFiles, + ["CONVENTIONS.md", "DECISIONS.md", "BACKLOG.md", "THREAD_LOG.md"], + "all four orchestrator files are seeded from templates, not copied from the framework's own workspace", + ); const conventions = await readFile(join(CODECARTO, "CONVENTIONS.md"), "utf8"); assert.match(conventions, /^# Conventions/, "seeded file carries the template body"); });