codecartographer-pi 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.codecarto/GUIDE.md +3 -3
  2. package/.codecarto/findings/architecture/SKILL.md +1 -0
  3. package/.codecarto/findings/contracts/SKILL.md +1 -0
  4. package/.codecarto/findings/defect-scan/SKILL.md +15 -1
  5. package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
  6. package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
  7. package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
  8. package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
  9. package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
  10. package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
  11. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
  12. package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
  13. package/.codecarto/findings/porting/SKILL.md +2 -1
  14. package/.codecarto/findings/protocols/SKILL.md +1 -0
  15. package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
  16. package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
  17. package/.codecarto/templates/amendment.yaml +3 -3
  18. package/.codecarto/templates/architecture-map.md +1 -1
  19. package/.codecarto/templates/defect-report.md +23 -0
  20. package/.codecarto/templates/mechanical-defects.md +22 -0
  21. package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
  22. package/.codecarto/templates/semantic-defects.md +26 -0
  23. package/.codecarto/templates/spike-report.md +2 -2
  24. package/.codecarto/workflow/VALIDATE.md +1 -1
  25. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  26. package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
  27. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
  28. package/.codecarto/workflow/pipeline-scout-first.yaml +5 -1
  29. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  30. package/README.md +14 -10
  31. package/agent-skill/codecartographer/SKILL.md +1 -1
  32. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
  33. package/agent-skill/codecartographer/references/library.md +3 -3
  34. package/agent-skill/codecartographer/references/orchestration.md +1 -1
  35. package/agent-skill/codecartographer/references/phase-recovery.md +1 -1
  36. package/dist/core/amendment.d.ts +5 -0
  37. package/dist/core/amendment.js +23 -4
  38. package/dist/core/completion.d.ts +5 -0
  39. package/dist/core/completion.js +18 -2
  40. package/dist/core/dashboard.js +5 -3
  41. package/dist/core/findings.d.ts +59 -0
  42. package/dist/core/findings.js +145 -0
  43. package/dist/core/index.d.ts +1 -0
  44. package/dist/core/index.js +1 -0
  45. package/dist/core/library.d.ts +139 -1
  46. package/dist/core/library.js +291 -40
  47. package/dist/core/orchestrator-config.d.ts +6 -0
  48. package/dist/core/orchestrator-config.js +2 -0
  49. package/dist/core/pipeline.js +15 -0
  50. package/dist/core/prompts.js +1 -1
  51. package/dist/core/status.d.ts +2 -1
  52. package/dist/core/status.js +33 -11
  53. package/dist/core/types.d.ts +6 -0
  54. package/dist/core/utils.d.ts +14 -0
  55. package/dist/core/utils.js +30 -0
  56. package/dist/core/workspace.d.ts +16 -0
  57. package/dist/core/workspace.js +42 -22
  58. package/dist/core/yaml.js +19 -4
  59. package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
  60. package/dist/extensions/codecarto/broadside-flags.d.ts +7 -2
  61. package/dist/extensions/codecarto/broadside-flags.js +22 -9
  62. package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
  63. package/dist/extensions/codecarto/index.js +379 -20
  64. package/dist/extensions/codecarto/phase-compaction.js +4 -0
  65. package/dist/mcp-server/server.js +127 -10
  66. package/package.json +2 -2
package/README.md CHANGED
@@ -35,7 +35,7 @@ Asking an LLM to "analyze this repo" loses context halfway through, hallucinates
35
35
 
36
36
  3. **The output is a spec, not a chat log.** The final `reimplementation-spec.md` is language-agnostic, module-inventoried, and carries acceptance scenarios plus known unknowns. Hand it to another agent to rebuild from.
37
37
 
38
- Every finding is tagged with an evidence level: `observed fact`, `strong inference`, `portability hazard`, or `open question`.
38
+ Every finding is tagged with an evidence level: `observed fact`, `strong inference`, `portability hazard`, `external-behavior claim`, or `open question`.
39
39
 
40
40
  ---
41
41
 
@@ -92,7 +92,7 @@ Use this when your coding agent isn't Pi — Claude Code, Codex, opencode, Curso
92
92
 
93
93
  > **30-second setup for Claude Code, Cursor, Codex, and Claude Desktop: see the [MCP quickstart](docs/mcp-quickstart.md).**
94
94
 
95
- > **Teaching an agent to drive it:** call the `codecarto_guide` tool — the server returns the full drive loop, the phase-handoff contract, executor selection, and recovery patterns, with nothing to install. The same content ships as an installable skill at `agent-skill/codecartographer/` for agents that load skills from disk.
95
+ > **Teaching an agent to drive it:** call the `codecarto_guide` tool — the server returns the full drive loop, the phase-handoff contract, executor selection, and recovery patterns, with nothing to install. The same content ships as an installable skill at `agent-skill/codecartographer/` for agents that load skills from disk, and `/codecarto-guide [topic]` reads it into a Pi session.
96
96
 
97
97
  ```bash
98
98
  npm install --global codecartographer-pi
@@ -137,7 +137,7 @@ Analysis turns repositories into reusable specifications. Synthesis runs the oth
137
137
  library:
138
138
  path: /absolute/path/to/codecarto-library
139
139
  namespace: your-namespace # omit for a single-tenant library
140
- publish_confirm: true
140
+ publish_confirm: true # Pi asks before writing; MCP refuses a publish that lacks confirm: true
141
141
  ```
142
142
 
143
143
  2. Initialize a clean planning workspace and fill in its brief:
@@ -228,7 +228,7 @@ The porting bundle is the final intentional compression boundary. It carries a s
228
228
  | **Porting bundle** | Everything synthesized into a porting-oriented view with priority rankings |
229
229
  | **Reimplementation spec** | Language-agnostic build plan with modules, acceptance scenarios, and known unknowns |
230
230
 
231
- Every finding is tagged with an evidence level: `observed fact`, `strong inference`, `portability hazard`, or `open question`. Every phase output is validated against explicit completion criteria before the pipeline advances.
231
+ Every finding is tagged with an evidence level: `observed fact`, `strong inference`, `portability hazard`, `external-behavior claim`, or `open question`. Every phase output is validated against explicit completion criteria before the pipeline advances.
232
232
 
233
233
  ---
234
234
 
@@ -321,12 +321,16 @@ Beyond the slash commands, the Pi extension layers on:
321
321
  | `/codecarto-validate [phase]` | Validate a phase output against completion criteria |
322
322
  | `/codecarto-complete [phase]` | Validate and atomically apply the phase handoff, canonical status, closeout, and log entry |
323
323
  | `/codecarto-skill <name>` | Run a post-pipeline skill once all phases are complete (or `broadside` any time, for the scout reading guide) |
324
+ | `/codecarto-list-skills` | List the installed post-pipeline skills and the ungated `broadside` reading guide, and say when the gated ones unlock |
325
+ | `/codecarto-guide [topic]` | Read the packaged agent guide — drive loop, handoff contract, executors, recovery, Broad-Side — into the session; tab-completes topics; needs no workspace |
324
326
  | `/codecarto-broadside [action] [lenses…]` | Batch reconnaissance (Broad-Side). Actions: `submit`, `collect`, `status`, `models`. Prices the run and asks before spending; works with or without a workspace |
325
327
  | `/codecarto-publish` | Publish the reimplementation spec to the configured library after reviewing an explicit confirmation preview |
326
328
  | `/codecarto-library-init <path> [--namespace <name>]` | Create a library directory with marker and write the config — fixes the first-publish dead end |
327
329
  | `/codecarto-config` | Show the effective merged configuration (global + workspace) and library marker status |
328
330
  | `/codecarto-usage` | Cumulative + per-phase token usage |
329
331
  | `/codecarto-dashboard [--narrate]` | Regenerate `.codecarto/dashboard.html`; `--narrate` for the LLM executive summary |
332
+ | `/codecarto-refresh-scaffold` | Refresh the framework-owned `.codecarto/` files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) from the packaged template after a confirmation that lists the exact file set; project state, config, findings outputs, scratch, closeouts, and `broadside/` are never touched |
333
+ | `/codecarto-amend <name>` | Apply a post-pipeline amendment from `scratch/amendments/<name>.yaml` after a confirmation that previews which open questions and post-pipeline items it closes; refused while the pipeline is incomplete |
330
334
 
331
335
  ### End-to-end auto mode (0.8.0+)
332
336
 
@@ -360,7 +364,7 @@ Implements MCP spec revision [`2025-11-25`](https://modelcontextprotocol.io/spec
360
364
  | `codecarto_validate` | `/codecarto-validate` |
361
365
  | `codecarto_complete` | `/codecarto-complete` |
362
366
  | `codecarto_skill` | `/codecarto-skill` |
363
- | `codecarto_list_skills` | MCP-only ([#161](https://github.com/HuginnIndustries/CodeCartographer/issues/161)); Pi lists skills when `/codecarto-skill` runs with no argument |
367
+ | `codecarto_list_skills` | `/codecarto-list-skills` |
364
368
  | `codecarto_publish` | `/codecarto-publish` |
365
369
  | `codecarto_library_init` | `/codecarto-library-init` |
366
370
  | `codecarto_library_list` | MCP-only library listing |
@@ -368,12 +372,12 @@ Implements MCP spec revision [`2025-11-25`](https://modelcontextprotocol.io/spec
368
372
  | `codecarto_config` | `/codecarto-config` |
369
373
  | `codecarto_usage` | `/codecarto-usage` |
370
374
  | `codecarto_dashboard` | `/codecarto-dashboard` |
371
- | `codecarto_guide` | MCP-only ([#160](https://github.com/HuginnIndustries/CodeCartographer/issues/160)) |
372
- | `codecarto_amend` | MCP-only ([#157](https://github.com/HuginnIndustries/CodeCartographer/issues/157)) |
373
- | `codecarto_refresh_scaffold` | MCP-only ([#159](https://github.com/HuginnIndustries/CodeCartographer/issues/159)) |
375
+ | `codecarto_guide` | `/codecarto-guide` |
376
+ | `codecarto_amend` | `/codecarto-amend` (Pi previews the closures and asks first) |
377
+ | `codecarto_refresh_scaffold` | `/codecarto-refresh-scaffold` (Pi lists the file set and asks first) |
374
378
  | `codecarto_broadside` | `/codecarto-broadside` |
375
379
 
376
- Each workflow tool accepts an absolute `cwd` for the target repository. `codecarto_init` requires `force: true` to overwrite an existing `.codecarto/` (instead of Pi's interactive confirmation). The library tools accept an explicit absolute `library_path` or resolve `library.path` from `.codecarto/workflow/config.yaml` / `~/.codecarto/config.yaml`. The library schema is experimental and may break before v2.
380
+ Each workflow tool accepts an absolute `cwd` for the target repository. `codecarto_init` requires `force: true` to overwrite an existing `.codecarto/` (instead of Pi's interactive confirmation). The library tools accept an explicit absolute `library_path` or resolve `library.path` from `.codecarto/workflow/config.yaml` / `~/.codecarto/config.yaml`. `codecarto_library_reindex` and `codecarto_library_list` also report entries whose versions disagree about `source_repo` — the shape a slug collision left behind before v0.17.0's publish guard — and leave the repair manual, since splitting an entry changes paths the library format treats as ABI. The library schema is experimental and may break before v2.
377
381
 
378
382
  ---
379
383
 
@@ -584,7 +588,7 @@ The MCP server does steps 1–3 directly; the Pi extension wraps them as slash c
584
588
  - **LLM-agnostic** — works with any model that can read and write files.
585
589
  - **Phase-gated** — one phase per session, validated before advancing.
586
590
  - **Single source of truth** — `status.yaml` tracks progress; no duplicated state.
587
- - **Evidence-classified** — every finding tagged as observed fact, strong inference, portability hazard, or open question.
591
+ - **Evidence-classified** — every finding tagged as observed fact, strong inference, portability hazard, external-behavior claim, or open question.
588
592
  - **Template-driven** — consistent output structure across projects and sessions.
589
593
  - **Drop-in** — lives inside your repo as `.codecarto/`. No symlinks, no copying source code, no runtime daemon.
590
594
 
@@ -117,7 +117,7 @@ A PARTIAL row's evidence must name what is missing and which `open_questions` or
117
117
  - `codecarto_next` returns a prompt. Something still has to *do* the phase.
118
118
  - Never hand-edit `workflow/status.yaml`, append `THREAD_LOG.md`, or write a second closeout. Propose through the handoff.
119
119
  - Do not force phases out of DAG order unless the user asked.
120
- - If `codecarto_status` reports a scaffold-staleness warning, refresh the workspace's framework-owned files before trusting anything written inside `.codecarto/`; a stale scaffold's `GUIDE.md` can contradict this contract.
120
+ - If `codecarto_status` reports a scaffold-staleness warning, refresh the workspace's framework-owned files (`codecarto_refresh_scaffold`; `/codecarto-refresh-scaffold` on the Pi extension) before trusting anything written inside `.codecarto/`; a stale scaffold's `GUIDE.md` can contradict this contract. The refresh never touches project state, findings outputs, or session directories.
121
121
  - A delegated run that times out may still have written its artifact. Check for the file and validate before retrying.
122
122
  - The drop-in `.codecarto/` template works without MCP, but the server is preferred: it owns atomic state updates, validation parsing, and the completion gate.
123
123
 
@@ -13,8 +13,11 @@ The common failure is treating defect reports as a separate document that the po
13
13
  | `fix before porting` | the defect would be reproduced by a faithful port | design it out; the spec states the correct behavior |
14
14
  | `port differently` | the behavior is needed but the mechanism is wrong | spec the intent, not the implementation |
15
15
  | `leave behind` | dead, vestigial, or actively harmful | name it explicitly so a later reader doesn't "restore" it |
16
+ | `verify at runtime` | the diagnosis is an `external-behavior claim` or `open question` — about a server, engine, driver, or API this source only calls | a spike in the spec's Spike List and a `post_pipeline` `kind: spike` entry; never a design consequence, because the claim is unconfirmed |
16
17
 
17
- Add the acceptance-test implication alongside each row. A hazard with no test in the spec will be reintroduced by whoever implements it.
18
+ Add the acceptance-test implication alongside each row.
19
+
20
+ The fourth disposition exists because of a real run: a defect scan asserted, as `strong inference` / `fix before porting`, that an inference server expected a different `logit_bias` payload shape, and recommended a one-line reshape. Runtime testing against the pinned engine inverted it — the shape the code already sent worked, and the recommended one was silently ignored. The evidence level bounds the action: `open question` or `external-behavior claim` evidence never pairs with `fix before porting`, and validation now fails a defect report that does so. A hazard with no test in the spec will be reintroduced by whoever implements it.
18
21
 
19
22
  Close a carry-forward item only once its guidance is represented in an artifact a later phase actually consumes — not merely mentioned in the phase that raised it.
20
23
 
@@ -13,9 +13,9 @@ A CodeCartographer **library** is a directory of published reimplementation-spec
13
13
  | Tool | Does | Notes |
14
14
  |---|---|---|
15
15
  | `codecarto_library_init` | Create the directory, write the marker, record `library.path` in user-global config | Idempotent; pass `namespace` to create a namespaced library |
16
- | `codecarto_publish` | Publish a spec as a library entry | Required: `source_repo`, `headline`, and `spec` (inline) or `spec_path` (absolute). Content-hash idempotent: identical bytes update metadata in place, no version bump. `slug` derives from `source_repo` if omitted; namespaced libraries require `namespace` (or inherit via `cwd`). Provenance (`source_commit`, `source_branch`, `source_dirty`, `analyzed_at`, `pipeline`, `model_metadata`) is recorded; omitted generation fields default to `unknown` |
16
+ | `codecarto_publish` | Publish a spec as a library entry | Required: `source_repo`, `headline`, and `spec` (inline) or `spec_path` (absolute). Content-hash idempotent: identical bytes update metadata in place, no version bump. `slug` derives from `source_repo` if omitted; namespaced libraries require `namespace` (or inherit via `cwd`). Provenance (`source_commit`, `source_branch`, `source_dirty`, `analyzed_at`, `pipeline`, `model_metadata`) is recorded; omitted generation fields default to `unknown`. `confirm: true` acknowledges the `publish_confirm` gate (below) |
17
17
  | `codecarto_library_list` | List entries | Filter by `namespace`, `tag`, `slug`, or `source_repo` |
18
- | `codecarto_library_reindex` | Regenerate `index.yaml` + `INDEX.md` from filesystem state | For manual edits and index merge conflicts |
18
+ | `codecarto_library_reindex` | Regenerate `index.yaml` + `INDEX.md` from filesystem state | For manual edits and index merge conflicts. Also reports entries whose versions disagree about `source_repo` (merged by a slug collision before publish refused cross-project appends; `codecarto_library_list` flags them too) — repair is manual, split the entry by hand |
19
19
 
20
20
  ## When to publish
21
21
 
@@ -25,7 +25,7 @@ The moment `reimplementation-spec` completes and validates is the publish moment
25
25
  codecarto_publish cwd:<workspace repo> source_repo:<repo URL or path> headline:"<one line>" spec_path:<abs path to reimplementation-spec.md>
26
26
  ```
27
27
 
28
- Set `publish_confirm` in config if you want an explicit confirmation gate before writes. **Pi-only today:** the Pi extension asks for interactive confirmation before `/codecarto-publish` writes; the MCP `codecarto_publish` tool does not act on the key (an MCP host has no one to ask — [#162](https://github.com/HuginnIndustries/CodeCartographer/issues/162) tracks whether it should refuse-unless-forced instead), so on MCP treat it as advisory.
28
+ Set `publish_confirm` in config if you want an explicit confirmation gate before writes. It means something on both executable surfaces, in the only way each can ask. The Pi extension shows a preview and asks interactively before `/codecarto-publish` writes. The MCP `codecarto_publish` tool has no one to ask, so when the key is set — in `~/.codecarto/config.yaml`, or in the workspace's `.codecarto/workflow/config.yaml` when `cwd` is passed — it refuses a call that lacks `confirm: true` and returns the preview instead: library, entry, whether this would be a new version or a metadata-only update, `source_repo`, headline, confidentiality. Nothing is written by the refusal. Show the preview, then re-invoke with the same arguments plus `confirm: true` to publish. The gate applies only when the key is actually set in a config file (`codecarto_library_init` writes `publish_confirm: true`, so a library initialized through the tool has it on); a host that never configured the key is not gated, and `publish_confirm: false` drops the gate.
29
29
 
30
30
  ## What this is not
31
31
 
@@ -8,7 +8,7 @@ All of them happen at the **phase boundary**: after one phase completes, before
8
8
 
9
9
  1. **Promote conventions and append decisions.** Phase closeouts carry "Proposed Conventions" and "Decisions Beyond Prompt" sections; handoffs carry a `decisions` array. Promotion into `CONVENTIONS.md` (when a pattern recurs or clearly generalizes) and `DECISIONS.md` (every cross-cutting decision, numbered, append-only) is your call to make at the boundary. Proposals left in closeout prose are proposals lost — a real seven-phase run stranded ~12 proposed conventions and 23 decisions this way, because nobody held the duty.
10
10
 
11
- 2. **Re-triage open-question labels.** An `open_questions` entry's `kind` is itself a claim that needs evidence. Before accepting `needs-maintainer-decision` or `needs-runtime-test` into the next phase, re-test: *has this become answerable by reading?* Labels are sticky — the routing machinery faithfully carries a question forward, but nothing re-examines whether the label was right, so a mislabel suppresses verification for the rest of the pipeline.
11
+ 2. **Re-triage open-question labels.** An `open_questions` entry's `kind` is itself a claim that needs evidence. Before accepting `needs-maintainer-decision` or `needs-runtime-test` into the next phase, re-test: *has this become answerable by reading?* Labels are sticky — the routing machinery faithfully carries a question forward, but nothing re-examines whether the label was right, so a mislabel suppresses verification for the rest of the pipeline. The duty cuts both ways: when the answer is genuinely *no, this still needs a runtime test*, no finding in the next phase may assert one of the question's candidate answers with a settled action (`fix before porting`, `fix now`). The finding inherits the question's uncertainty — evidence `external-behavior claim` or `open question`, action `verify at runtime` — until runtime evidence closes the question. A real `full-with-deep-audit` run broke this: the mechanical phase wrote "source alone cannot determine which" and routed one candidate onward; the semantic phase closed it as `strong inference` / `fix before porting`; runtime testing showed the recommended fix would have converted working behavior into the one shape the engine silently ignores.
12
12
 
13
13
  3. **Sweep for contradictions.** Compare the incoming phase's required reads against earlier phases' `owner_notes`. A measured fact that contradicts a summarized claim (a line count that belies "this layer is pure configuration", a schema that admits a value a doc says is impossible) is a gap to route — into the next phase's work, an `open_questions` entry, or a correction — not a nuance to smooth over.
14
14
 
@@ -39,7 +39,7 @@ If two reduced-scope attempts fail, the problem is usually scope, not the execut
39
39
 
40
40
  - Split the reading. Use scoped pre-passes over individual subsystems, save the notes under `.codecarto/scratch/`, and give the retry those notes as evidence.
41
41
  - Consider whether the pipeline variant is right. A repository too large for one `architecture` pass may want `architecture-only` first, reviewed, then a switch.
42
- - Check for a scaffold-staleness warning in `codecarto_status`. A workspace whose framework-owned files predate the running version can carry instructions that contradict the current contract, which produces artifacts that fail validation for reasons the executor cannot see.
42
+ - Check for a scaffold-staleness warning in `codecarto_status`. A workspace whose framework-owned files predate the running version can carry instructions that contradict the current contract, which produces artifacts that fail validation for reasons the executor cannot see. `codecarto_refresh_scaffold` (`/codecarto-refresh-scaffold` on the Pi extension) refreshes those files without touching project state.
43
43
 
44
44
  ## What not to do
45
45
 
@@ -27,6 +27,11 @@ export type AmendmentResult = {
27
27
  };
28
28
  /** Same charset rule as phase ids: the slug becomes file names, so path shapes are refused. */
29
29
  export declare function assertSafeAmendmentSlug(slug: string): void;
30
+ /**
31
+ * Slugs of the amendment files staged under scratch/amendments/, sorted.
32
+ * Discovery only — nothing here is validated; {@link loadAmendmentFile} does that.
33
+ */
34
+ export declare function listAmendmentNames(workspaceDir: string): Promise<string[]>;
30
35
  /**
31
36
  * Load and validate one amendment file.
32
37
  * @param name - the amendment slug, with or without a `.yaml` suffix.
@@ -5,11 +5,11 @@
5
5
  // carry_forward_closures); an amendment is the post-pipeline counterpart, so
6
6
  // spec-delta sessions, spikes, and maintainer rulings no longer end with
7
7
  // "record for a later explicit amendment" that nothing can perform.
8
- import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
9
- import { join } from "node:path";
8
+ import { appendFile, mkdir, readdir, readFile, writeFile } from "node:fs/promises";
9
+ import { basename, join } from "node:path";
10
10
  import { getNextEligiblePhase } from "./pipeline.js";
11
11
  import { buildTerminalNextActions, ensureArray, normalizeStatus } from "./status.js";
12
- import { dateOnly, pathExists } from "./utils.js";
12
+ import { dateOnly, newlineIfUnterminated, pathExists } from "./utils.js";
13
13
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
14
14
  import { loadYamlFile } from "./yaml.js";
15
15
  /** Same charset rule as phase ids: the slug becomes file names, so path shapes are refused. */
@@ -18,6 +18,25 @@ export function assertSafeAmendmentSlug(slug) {
18
18
  throw new Error(`Invalid amendment name: ${slug}`);
19
19
  }
20
20
  }
21
+ /**
22
+ * Slugs of the amendment files staged under scratch/amendments/, sorted.
23
+ * Discovery only — nothing here is validated; {@link loadAmendmentFile} does that.
24
+ */
25
+ export async function listAmendmentNames(workspaceDir) {
26
+ const amendmentsDir = join(workspaceDir, "scratch", "amendments");
27
+ if (!(await pathExists(amendmentsDir)))
28
+ return [];
29
+ try {
30
+ const entries = await readdir(amendmentsDir, { withFileTypes: true });
31
+ return entries
32
+ .filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name))
33
+ .map((entry) => basename(entry.name).replace(/\.ya?ml$/i, ""))
34
+ .sort();
35
+ }
36
+ catch {
37
+ return [];
38
+ }
39
+ }
21
40
  /**
22
41
  * Load and validate one amendment file.
23
42
  * @param name - the amendment slug, with or without a `.yaml` suffix.
@@ -134,7 +153,7 @@ export async function applyAmendment(cwd, name) {
134
153
  // Created below when absent.
135
154
  }
136
155
  if (!current.split(/\r?\n/).some((line) => line.includes(`[closeout](closeouts/${closeoutFile})`))) {
137
- await appendFile(threadLogPath, `${entry}\n`, "utf8");
156
+ await appendFile(threadLogPath, `${newlineIfUnterminated(current)}${entry}\n`, "utf8");
138
157
  }
139
158
  closeoutNotice = `Closeout: .codecarto/closeouts/${closeoutFile}`;
140
159
  return { state: { ...lockedState, status: nextStatus } };
@@ -2,6 +2,11 @@ import type { ValidationResult, WorkspaceState } from "./types.ts";
2
2
  export type CompletionResult = {
3
3
  updatedState: WorkspaceState;
4
4
  closeoutNotice?: string;
5
+ /**
6
+ * Non-gating closure-integrity observations (#122): closures the handoff
7
+ * claims that the primary output never mentions. Empty when clean.
8
+ */
9
+ warnings: string[];
5
10
  /**
6
11
  * One-line phase-boundary reminder covering what completion just mechanized
7
12
  * (decisions appended, proposals staged) and what still needs orchestrator
@@ -2,7 +2,7 @@ import { appendFile, copyFile, mkdir, readFile, readdir, writeFile } from "node:
2
2
  import { join } from "node:path";
3
3
  import { getNextEligiblePhase, resolvePhase, validatePhaseOutput } from "./pipeline.js";
4
4
  import { applyHandoff, autoAssignIds, buildTerminalNextActions, loadHandoffFile, normalizeStatus } from "./status.js";
5
- import { dateOnly, pathExists, uniqueStrings } from "./utils.js";
5
+ import { dateOnly, newlineIfUnterminated, pathExists, uniqueStrings } from "./utils.js";
6
6
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
7
7
  /**
8
8
  * The Markdown a reader sees: content inside `<!-- -->` blocks removed by a
@@ -223,7 +223,7 @@ async function writeCompletionArtifacts(workspaceDir, phaseId, validation, times
223
223
  }
224
224
  const link = `[closeout](closeouts/${closeoutFile})`;
225
225
  if (!current.split(/\r?\n/).some((line) => line.includes(link))) {
226
- await appendFile(threadLogPath, `${entry}\n`, "utf8");
226
+ await appendFile(threadLogPath, `${newlineIfUnterminated(current)}${entry}\n`, "utf8");
227
227
  }
228
228
  // Mechanize the orchestrator loop's bookkeeping half (issue #98): decisions
229
229
  // reach DECISIONS.md and proposals reach CONVENTIONS.md at completion, so a
@@ -268,6 +268,21 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
268
268
  throw new Error("Invalid handoff: post_pipeline entries require a canonical id");
269
269
  }
270
270
  }
271
+ // Closure integrity (#122, warning only): a handoff can close a carry-forward
272
+ // or open question the report never addressed — "closed in the handoff,
273
+ // resolved nowhere." The id of every claimed closure should appear somewhere
274
+ // in the primary output that claims to resolve it.
275
+ const warnings = [];
276
+ if (handoff && validation.outputPath) {
277
+ const closures = [...handoff.carry_forward_closures, ...handoff.open_question_closures].filter((id) => id?.trim());
278
+ if (closures.length > 0) {
279
+ const output = await readFile(validation.outputPath, "utf8").catch(() => "");
280
+ const unmentioned = closures.filter((id) => !output.includes(id));
281
+ if (unmentioned.length > 0) {
282
+ warnings.push(`The handoff closes ${unmentioned.join(", ")} but .codecarto/${validation.primaryOutput} never mentions ${unmentioned.length === 1 ? "that id" : "those ids"} — a closure should be visible in the report that claims to resolve it, not only in the handoff.`);
283
+ }
284
+ }
285
+ }
271
286
  const completionTimestamp = new Date().toISOString();
272
287
  let closeoutPath;
273
288
  let orchestratorCheckpoint;
@@ -343,5 +358,6 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
343
358
  updatedState,
344
359
  closeoutNotice: closeoutPath ? `Closeout: ${closeoutPath}` : undefined,
345
360
  orchestratorCheckpoint,
361
+ warnings,
346
362
  };
347
363
  }
@@ -452,7 +452,9 @@ function usagePhaseNote(phaseId, status) {
452
452
  function renderActivityTimeline(runs) {
453
453
  if (runs.length === 0)
454
454
  return "";
455
- const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1));
455
+ // Newest first. The comparator must return 0 for equal keys: returning -1
456
+ // in both directions left same-timestamp order implementation-defined (#134).
457
+ const sorted = [...runs].sort((a, b) => (a.timestamp < b.timestamp ? 1 : a.timestamp > b.timestamp ? -1 : 0));
456
458
  const visible = sorted.slice(0, TIMELINE_VISIBLE_COUNT);
457
459
  const overflow = sorted.slice(TIMELINE_VISIBLE_COUNT);
458
460
  const hasSessionLinks = sorted.some((run) => Boolean(run.session_file && safeRelativeHref(run.session_file)));
@@ -507,7 +509,7 @@ function renderCloseoutsList(inputs) {
507
509
  const closeouts = inputs.closeouts;
508
510
  if (closeouts.length === 0)
509
511
  return [`<section class="cc-card cc-closeouts" id="closeouts" aria-label="Closeouts" data-section>`, `<h2>Closeouts</h2>`, `<p class="cc-empty">No closeouts yet.</p>`, `</section>`].join("\n");
510
- const sorted = [...closeouts].sort((a, b) => (a.date < b.date ? 1 : -1));
512
+ const sorted = [...closeouts].sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
511
513
  const rows = sorted.map((c) => {
512
514
  const phase = getPhase(inputs.pipeline, c.phaseOrModule);
513
515
  const outputs = inputs.outputsPresent.get(c.phaseOrModule);
@@ -646,7 +648,7 @@ function getPhase(pipeline, phaseId) {
646
648
  return pipeline.phases.find((p) => p.id === phaseId);
647
649
  }
648
650
  function closeoutForPhase(closeouts, phaseId) {
649
- return [...closeouts].filter((c) => c.phaseOrModule === phaseId).sort((a, b) => (a.date < b.date ? 1 : -1))[0];
651
+ return [...closeouts].filter((c) => c.phaseOrModule === phaseId).sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0))[0];
650
652
  }
651
653
  function phaseAnchor(phaseId) {
652
654
  return `phase-${phaseId.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
@@ -0,0 +1,59 @@
1
+ /**
2
+ * First scaffold version whose defect templates offer `verify at runtime`.
3
+ * A workspace scaffolded before it had no honest action for an unsettled
4
+ * finding, so the pairing violation is reported as a warning there instead
5
+ * of failing a phase mid-run.
6
+ */
7
+ export declare const FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION = "0.17.1";
8
+ /** Evidence levels that mean "not settled by reading this source". */
9
+ export declare const UNSETTLED_EVIDENCE_LEVELS: ReadonlySet<string>;
10
+ /** Actions that assert the diagnosis is settled enough to act on. */
11
+ export declare const SETTLED_FIX_ACTIONS: ReadonlySet<string>;
12
+ /** The pre-porting action an unsettled finding takes. */
13
+ export declare const RUNTIME_VERIFY_ACTION = "verify at runtime";
14
+ export type FindingRow = {
15
+ /** The `## Pass N` heading the table sits under, when there is one. */
16
+ pass: string | null;
17
+ /** The `#` cell, verbatim. */
18
+ number: string;
19
+ /** Normalized Evidence Level cell (lowercase, markup stripped). */
20
+ evidence: string;
21
+ /** Normalized Action cell. */
22
+ action: string;
23
+ /** 1-based line of the row in the document. */
24
+ line: number;
25
+ };
26
+ export type FindingsCrossCheck = {
27
+ /** Violations that fail validation on a current scaffold. */
28
+ errors: string[];
29
+ /** Non-gating observations, rendered as NOTE lines. */
30
+ warnings: string[];
31
+ findings: FindingRow[];
32
+ };
33
+ /**
34
+ * Every data row of every table whose header carries both an Evidence Level
35
+ * and an Action column. Placeholder rows (both cells empty) are skipped so an
36
+ * untouched template section contributes nothing.
37
+ */
38
+ export declare function parseFindingsTables(content: string): FindingRow[];
39
+ /** Whether the report has an `## Open Questions` table, and how many filled rows it holds. */
40
+ export declare function parseOpenQuestionsTable(content: string): {
41
+ present: boolean;
42
+ rows: number;
43
+ };
44
+ /**
45
+ * Whether the pairing violation fails validation for this workspace. True from
46
+ * the scaffold version that introduced `verify at runtime`; an unversioned or
47
+ * older scaffold only warns.
48
+ */
49
+ export declare function findingsPairingGateActive(scaffoldVersion: string | undefined | null): boolean;
50
+ /**
51
+ * Run the cross-checks over a phase output. Returns empty results for any
52
+ * document without findings tables, so non-defect phases are untouched.
53
+ *
54
+ * @param content - The primary output's markdown.
55
+ * @param opts.gate - Whether the pairing violation is an error (current scaffold) or a warning.
56
+ */
57
+ export declare function crossCheckFindings(content: string, opts: {
58
+ gate: boolean;
59
+ }): FindingsCrossCheck;
@@ -0,0 +1,145 @@
1
+ // Mechanical cross-checks over a defect report's findings tables (issue #122).
2
+ //
3
+ // A run can register an open question saying "source alone cannot determine
4
+ // which" and, in the same run, ship one of that question's candidates as
5
+ // `strong inference` / `fix before porting`. Both artifacts validate on their
6
+ // own criteria because nothing reads what the evidence and action cells SAY.
7
+ // The checks here do: they are deterministic reads of two cells the model
8
+ // wrote itself, so the gating one cannot wedge an --auto run on a heuristic.
9
+ //
10
+ // The findings tables are header-identical across the three defect templates
11
+ // (`| # | Location | Defect | Severity | Evidence Level | Action |`, with an
12
+ // optional trailing Spec Reference), which is what makes a header-driven parse
13
+ // a parse and not a guess. Any table without both an Evidence Level and an
14
+ // Action column is left alone.
15
+ import { compareDottedVersions } from "./utils.js";
16
+ /**
17
+ * First scaffold version whose defect templates offer `verify at runtime`.
18
+ * A workspace scaffolded before it had no honest action for an unsettled
19
+ * finding, so the pairing violation is reported as a warning there instead
20
+ * of failing a phase mid-run.
21
+ */
22
+ export const FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION = "0.17.1";
23
+ /** Evidence levels that mean "not settled by reading this source". */
24
+ export const UNSETTLED_EVIDENCE_LEVELS = new Set(["open question", "external-behavior claim"]);
25
+ /** Actions that assert the diagnosis is settled enough to act on. */
26
+ export const SETTLED_FIX_ACTIONS = new Set(["fix before porting", "fix now"]);
27
+ /** The pre-porting action an unsettled finding takes. */
28
+ export const RUNTIME_VERIFY_ACTION = "verify at runtime";
29
+ function normalizeCell(cell) {
30
+ return cell.replace(/[`*_]/g, "").replace(/\s+/g, " ").trim().toLowerCase();
31
+ }
32
+ function isTableRow(line) {
33
+ return line.trim().startsWith("|");
34
+ }
35
+ function isSeparatorRow(line) {
36
+ return /^\s*\|?\s*:?-{3,}/.test(line);
37
+ }
38
+ function splitRow(line) {
39
+ const trimmed = line.trim();
40
+ const inner = trimmed.slice(1, trimmed.endsWith("|") ? -1 : undefined);
41
+ return inner.split("|").map((cell) => cell.trim());
42
+ }
43
+ /**
44
+ * Every data row of every table whose header carries both an Evidence Level
45
+ * and an Action column. Placeholder rows (both cells empty) are skipped so an
46
+ * untouched template section contributes nothing.
47
+ */
48
+ export function parseFindingsTables(content) {
49
+ const lines = content.split(/\r?\n/);
50
+ const rows = [];
51
+ let pass = null;
52
+ for (let i = 0; i < lines.length; i++) {
53
+ const heading = /^##\s+Pass\s+(\d+)\b/i.exec(lines[i]);
54
+ if (heading) {
55
+ pass = heading[1];
56
+ continue;
57
+ }
58
+ if (!isTableRow(lines[i]) || !isSeparatorRow(lines[i + 1] ?? ""))
59
+ continue;
60
+ const header = splitRow(lines[i]).map(normalizeCell);
61
+ const evidenceIdx = header.indexOf("evidence level");
62
+ const actionIdx = header.indexOf("action");
63
+ const numberIdx = header.indexOf("#");
64
+ if (evidenceIdx < 0 || actionIdx < 0)
65
+ continue;
66
+ for (let j = i + 2; j < lines.length && isTableRow(lines[j]); j++) {
67
+ const cells = splitRow(lines[j]);
68
+ const evidence = normalizeCell(cells[evidenceIdx] ?? "");
69
+ const action = normalizeCell(cells[actionIdx] ?? "");
70
+ if (!evidence && !action)
71
+ continue;
72
+ rows.push({ pass, number: numberIdx >= 0 ? (cells[numberIdx] ?? "").trim() : "", evidence, action, line: j + 1 });
73
+ i = j;
74
+ }
75
+ }
76
+ return rows;
77
+ }
78
+ /** Whether the report has an `## Open Questions` table, and how many filled rows it holds. */
79
+ export function parseOpenQuestionsTable(content) {
80
+ const lines = content.split(/\r?\n/);
81
+ const start = lines.findIndex((line) => /^##\s+Open Questions\s*$/i.test(line));
82
+ if (start < 0)
83
+ return { present: false, rows: 0 };
84
+ for (let i = start + 1; i < lines.length; i++) {
85
+ if (/^##\s/.test(lines[i]))
86
+ break; // next section, no table
87
+ if (!isTableRow(lines[i]) || !isSeparatorRow(lines[i + 1] ?? ""))
88
+ continue;
89
+ let rows = 0;
90
+ for (let j = i + 2; j < lines.length && isTableRow(lines[j]); j++) {
91
+ if (splitRow(lines[j]).some((cell) => cell.length > 0))
92
+ rows++;
93
+ }
94
+ return { present: true, rows };
95
+ }
96
+ return { present: true, rows: 0 };
97
+ }
98
+ /**
99
+ * Whether the pairing violation fails validation for this workspace. True from
100
+ * the scaffold version that introduced `verify at runtime`; an unversioned or
101
+ * older scaffold only warns.
102
+ */
103
+ export function findingsPairingGateActive(scaffoldVersion) {
104
+ if (!scaffoldVersion)
105
+ return false;
106
+ const comparison = compareDottedVersions(scaffoldVersion, FINDINGS_PAIRING_GATE_SCAFFOLD_VERSION);
107
+ return comparison !== null && comparison >= 0;
108
+ }
109
+ function describe(row) {
110
+ const where = row.pass ? `Pass ${row.pass} finding #${row.number || "?"}` : `finding #${row.number || "?"}`;
111
+ return `${where} (line ${row.line})`;
112
+ }
113
+ /**
114
+ * Run the cross-checks over a phase output. Returns empty results for any
115
+ * document without findings tables, so non-defect phases are untouched.
116
+ *
117
+ * @param content - The primary output's markdown.
118
+ * @param opts.gate - Whether the pairing violation is an error (current scaffold) or a warning.
119
+ */
120
+ export function crossCheckFindings(content, opts) {
121
+ const findings = parseFindingsTables(content);
122
+ const errors = [];
123
+ const warnings = [];
124
+ if (findings.length === 0)
125
+ return { errors, warnings, findings };
126
+ for (const row of findings) {
127
+ if (UNSETTLED_EVIDENCE_LEVELS.has(row.evidence) && SETTLED_FIX_ACTIONS.has(row.action)) {
128
+ const message = `${describe(row)}: evidence level "${row.evidence}" cannot carry the settled action "${row.action}" — ` +
129
+ `use "${RUNTIME_VERIFY_ACTION}" or "port differently" ("investigate" on maintenance pipelines), and list the finding under ## Open Questions.`;
130
+ if (opts.gate)
131
+ errors.push(message);
132
+ else
133
+ warnings.push(`${message} Warning only: this workspace's scaffold predates the verify-at-runtime vocabulary — refresh it to make this gating.`);
134
+ }
135
+ if (row.evidence === "observed fact" && row.action === RUNTIME_VERIFY_ACTION) {
136
+ warnings.push(`${describe(row)}: "observed fact" paired with "${RUNTIME_VERIFY_ACTION}" contradicts itself — a settled label with an unsettled action. Pick the one that is true.`);
137
+ }
138
+ }
139
+ const unsettled = findings.filter((row) => UNSETTLED_EVIDENCE_LEVELS.has(row.evidence) || row.action === RUNTIME_VERIFY_ACTION);
140
+ const openQuestions = parseOpenQuestionsTable(content);
141
+ if (unsettled.length > 0 && openQuestions.present && openQuestions.rows === 0) {
142
+ warnings.push(`${unsettled.length} unsettled finding(s) but the ## Open Questions table is empty — each open question / external-behavior claim finding needs a row there so the hedge travels with the finding, not only with the handoff.`);
143
+ }
144
+ return { errors, warnings, findings };
145
+ }
@@ -4,6 +4,7 @@ export * from "./yaml.ts";
4
4
  export * from "./status.ts";
5
5
  export * from "./amendment.ts";
6
6
  export * from "./pipeline.ts";
7
+ export * from "./findings.ts";
7
8
  export * from "./prompts.ts";
8
9
  export * from "./workspace.ts";
9
10
  export * from "./completion.ts";
@@ -7,6 +7,7 @@ export * from "./yaml.js";
7
7
  export * from "./status.js";
8
8
  export * from "./amendment.js";
9
9
  export * from "./pipeline.js";
10
+ export * from "./findings.js";
10
11
  export * from "./prompts.js";
11
12
  export * from "./workspace.js";
12
13
  export * from "./completion.js";