diff --git a/CHANGELOG.md b/CHANGELOG.md index ebbbf2932..9125b839f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ How pinning actually works, verified against the `skills` CLI: #### [`@gtrabanco/pi-agentic-workflow`](packages/pi-agentic-workflow/) | Version | Date | Type | What changed | +| 0.35.1 | 2026-10-27 | patch | **Issue #316 — the roadmap's `done` settle is code, and its call sites execute.** Publishes the skill content this unit changed: `execute-phase` 6.3.1 and `unit-lane` 2.5.1 name the **executable** settle form — `roadmap rowUpsert ''` with the index path and the row's own cells — and stop at the kind boundary (`rowUpsert` is the roadmap's writer; a fix index has no `NN` column). The rest of the unit rides the same content: `scripts/roadmap-settle.mjs` fails closed offline when a unit doc carries a PR URL its index row does not settle (`done · [#N]()`), `scripts/unit-cycle.mjs` writes the row itself and now grades through the crate's `checkUnitDoc` instead of a second, weaker rule, and `roadmap rowUpsert`/`annotate` refuse a bare subcommand as a usage error instead of resolving it as a table path. No package code change — the bump exists because the shipped skill content changed (`skills/**` trigger set). Re-cut above `main`'s published `0.35.0` (issue #321, rebased) so the shipped version stays above the base's. | | 0.35.0 | 2026-10-26 | minor | **The Serena bridge is replaced by native MCP registration (issue #321).** **BREAKING CHANGE:** `@bacnh85/pi-serena` is removed — from `dependencies` and from `pi.extensions` — so the package no longer ships the bridge's `serena_*` tools; Serena is provisioned through pi's native MCP support instead (`pi mcp add serena -- serena start-mcp-server --context claude-code`, documented by the bundled `init-workspace` 4.3.0's new pi row). The bridge pinned the package to one external repo's in-process Python coupling of Serena's internal APIs, last touched before Serena 2.0.0 existed; `serena start-mcp-server` is Serena's maintained, version-stable interface, so `serena-agent` upgrades (1.7.0 → 2.1.0 today) need no change here. The pin-watch gains the PyPI half (issue #321): `serena-agent` installed-vs-latest is watched and a drift issue is filed — never a silent upgrade — with drift/clean/degraded/skipped covered offline. `scripts/serena-activation-check.mjs` is reworked: it proves the packed artifact carries no bridge dependency and only its own extension entry, instead of booting a scratch pi session to count bridge tools; the AC1 pin/entry tests are replaced by their inverse (bridge absent, own entry intact). The `serena-readiness` nudge is unchanged. | | 0.34.2 | 2026-10-26 | patch | **The runner pin catches up (registry drift).** `@gtrabanco/agentic-workflow` moves from the 0.2.0 pin to 0.4.0 — the version the registry actually serves. The crate table records 0.3.0 as a repository-only bump that never published, so npm jumped 0.2.0 → 0.4.0, and until this pin moved the registry half of the version-parity gate rejected **every** pull request with `dependency-pin-drift`, including ones that touched no dependency (main's own runs went red the moment 0.4.0 published). 0.4.0 is additive over 0.2.0 (`agentic-workflow memory probe`, feature 66 P1) — no removed subcommand, no changed argv — so the pin moves without a consumer change. No skill content and no package code change: the dependency alone moves, and the suite passes on the bumped pin (471 tests). This is the follow-up the 0.34.1 row deferred ("it moves when that package actually publishes"). | | 0.34.1 | 2026-10-26 | patch | **The schema pin catches up (registry drift).** `@gtrabanco/agentic-workflow-schema` moves from the 4.7.0 pin to 4.8.0, the version the 4.8.0 row's own pin rule deferred to a deliberate `fix(deps)` PR once it published — until then the registry half of the version-parity gate rejected every push to `main` (`dependency-pin-drift`). No skill content and no package code change: the dependency alone moves, and the suite passes on both runtimes (471 tests, bun and node). The pin on `@gtrabanco/agentic-workflow` stays at the published 0.2.0 for the same reason the crate's own publish gate is red — it moves when that package actually publishes. | @@ -174,6 +175,7 @@ How pinning actually works, verified against the `skills` CLI: #### [`@gtrabanco/agentic-workflow`](packages/agentic-workflow/) | Version | Date | Type | What changed | +| 0.5.0 | 2026-10-26 | minor | **Issue #316 — `roadmap check`, the cross-document settle check, and the writer accepts the table that exists.** `agentic-workflow roadmap check [--roadmap ] [--index ] [--json]` compares a unit doc's own facts (NN and slug from its folder, PR URLs from its own lifecycle surfaces — `## Next`, `## Log`, `## Evidence`, the References `- PR:` line) with its index row and fails closed with the closed code set `ROADMAP_CHECK_CODES` (`row-missing`, `slug-mismatch`, `pr-url-unsettled`); exit contract 0 consistent · 1 violations · 2 usage. The invariant it mechanizes: a unit doc that carries a PR URL must have its row reading `done · [#N]()` (backtick-tolerant; fix-index rows removed post-merge are convention, not contradiction). `findRow(table, nn)` is exported for callers that re-emit a row through `rowUpsert` without clobbering the cells they do not touch. The separator rule now matches any pipes/dashes/colons rule (≥3 dashes) — the old literal `|---|` match refused this repository's own `|----|------|…` separator, so the writer the issue names could not write the repo's real roadmap; rowUpsert/annotate receipts are byte-identical. | | 0.4.0 | 2026-10-22 | minor | **Feature 66 P1 — `memory probe`, the bounded memory-substrate health check** (issue #262, AC2). `agentic-workflow memory probe [--json]` runs five checks over the Engram layer and prints one `memory-probe@1` verdict (`ok` \| `degraded`): `http-health` (`GET :7437/health`, body `status` must be `ok`), `mcp-runner` (one JSON-RPC `initialize` against a spawned `engram mcp --tools=agent` — no MCP *tool* call is ever issued, so the probe cannot mutate the memory layer), `binary-version` (`engram --version`), `sync-staleness` (`engram sync --status`, `Pending import` must be 0), and `session-ambiguity` (the two check-scoped `engram doctor --json --check` calls — never the 12–23 s full sweep, and **both answers reported**: stopping at the first non-`ok` id would silence `sync_target_closed_space` permanently on a machine whose session attribution is ambiguous). Bounds: 4 s per HTTP/runner/version call, 10 s per staleness/session call, 30 s whole-probe ceiling; the five independent checks are launched together, so the run costs the slowest bound rather than the sum of five (32 s against a 30 s ceiling), and a bound hit fails that check instead of hanging. `ok` requires all five. Two measured rather than assumed rules: `engram doctor` exits 0 while its JSON says `status: "error"`, so every check parses its answer, and a responding server that has not synced is not `ok` (that is the staleness signal). **The command always exits 0** — `degraded` is an answer to record in Evidence, never a blocked unit. `runProbe({ fetchImpl, runImpl, timeoutMs })` takes its side effects by injection (the `src/doc/embeddings.mjs` `fetchImpl ?? fetch` shape), so the 20-test suite drives timeout, missing-binary, missing-runner, stale and healthy fixtures, both doctor findings and the shared-clock ceiling with fakes and millisecond bounds — no network, no PATH, no real engram process. | | 0.3.0 | 2026-10-21 | minor | **Feature 66 pre-work — the pin the published 0.2.0 could not carry.** Repository-only version bump recorded here so the crate's `package.json` and this table agree: `0.2.0` stays the version on the registry (the crate publishes from CI on merge, so an unpublished bump is not a release note), and this row exists because `scripts/check-changelog-row.mjs` requires a row per version. No functional change in this row. | | 0.2.0 | 2026-10-01 | minor | **Feature 65 — the `doc` retrieval entry point** (issue #192, half B): `agentic-workflow doc` gains `--sync | --query | --status | --rebuild [--json-only] [--quiet]` over a gitignored SQLite/FTS5 store (`.agentic-workflow/index/`), with the canonical TypeBox-validated envelope (`{ok, command, results[], degradations[], store}`, frozen key order), the closed degradation vocabulary, hash-incremental sync with a canonical manifest export (byte-identical for an unchanged corpus), the freshness invariant checked on every query, the AC14 session-log filter, and hybrid RRF fusion behind the AC15 judge gate with pi-style provider precedence (CLI > env > config > none ⇒ keyword-only). Crate's first pinned dependencies: `typebox@1.3.7` (zero transitive). Engines floor raised to `node >= 24` (D7). Ships to target projects via the pi package. | @@ -315,6 +317,8 @@ How pinning actually works, verified against the `skills` CLI: #### `execute-phase` | Version | Date | Type | What changed | +| 6.3.1 | 2026-10-27 | patch | **Issue #316 review F4 — the settle call site names the form that runs.** `6.3.0` shipped the settle as prose-calling-code but named `agentic-workflow roadmap rowUpsert` with no table path, and that argv resolved the subcommand word as a filename: every bare invocation refused with `ENOENT` on a file named `rowUpsert`. The close-out now names the executable form — `roadmap docs/features/ROADMAP.md rowUpsert '{"nn","slug","status"}'`, re-emitting the row's own `dependsOn`/`description` cells — and stops at the kind boundary: a fix index has no `NN` column, so `rowUpsert` is the roadmap's writer only and a fix unit writes its `docs/fix/README.md` status cell. `bun scripts/roadmap-settle.mjs` stays the offline validator for both. | +| 6.3.0 | 2026-10-26 | minor | **Issue #316 — the whole-unit close-out settles the roadmap row through the crate's writer.** §7's close-out (open PR on `main` with `Closes #N`, `- PR: ` into References) now ends with `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with every other cell preserved, validated by `bun scripts/roadmap-settle.mjs` exiting 0 — the done settle is code, never prose. Per-phase mode is unchanged: the lane's release step owns the settle there. | |---|---|---|---| | 6.2.1 | 2026-10-23 | patch | **The memory pointer states attribution the way `MEMORY.md` §5 now defines it (feature 66 review-fold F7).** The Post-step item said "with an explicit `project` and `session_id`" — a parameter the host-side memory surface does not accept. It now reads "explicit `project` and explicit session attribution (§5 names what carries it on each write surface)", still pointing at the protocol's single home and never restating it. No rule changes. | | 6.2.0 | 2026-10-22 | minor | **The phase-transition memory boundary (feature 66 P2).** A unit's phases are now memory boundaries: the turn contract gains a box requiring each phase's outcome to be saved under `topic_key: \"unit//doc\"` — or the mirror marked pending when the layer is degraded — and Post-step gains a fifth item naming the obligation (probe once before the session's first memory read, save after the step, recall `mem_context` → scoped search → one explicit `all_projects: true` retry). The full protocol is never restated here: the item points at `skills/unit-lane/references/MEMORY.md`. A degraded memory layer is recorded and never blocks a phase. | @@ -737,6 +741,8 @@ How pinning actually works, verified against the `skills` CLI: #### `unit-lane` | Version | Date | Type | What changed | +| 2.5.1 | 2026-10-27 | patch | **Issue #316 review F4 — the release checklist names the form that runs.** `2.5.0`'s checklist item called `agentic-workflow roadmap rowUpsert` with no table path, which resolved the subcommand word as the file to open (`ENOENT` on a file named `rowUpsert`, exit 2) — the step the issue exists to make code could not run as written. The item now carries the index path and the JSON, and distinguishes the kind: `docs/features/ROADMAP.md` through `rowUpsert` for a feature, the `docs/fix/README.md` status cell for a fix (no `NN` column there). | +| 2.5.0 | 2026-10-26 | minor | **Issue #316 — the release step calls the writer and the plan step emits the settle as a task.** `references/RELEASE.md`'s checklist replaces the prose "Roadmap row updated to `done`" with the `agentic-workflow roadmap rowUpsert` call (cells preserved, `bun scripts/roadmap-settle.mjs` exits 0); `references/PLAN.md`'s Tasks contract gains the `P` release-settle task for units that open a PR (the sole exception to "no docs/release tasks"), so the executor owns the transition and a skipped write is a failed task. Unit-cycle writes the row itself regardless — the prose was the defect. | |---|---|---|---| | 2.4.1 | 2026-10-23 | patch | **Session attribution named the way the tools actually carry it (feature 66 review-fold F7).** §5 required an explicit `session_id` on every write, but the installed memory surface has no such parameter: Engram's own MCP server exposes it, the host-side provider binds the runtime session itself and refuses when that identity is ambiguous. The rule is unchanged — a write that cannot say which session it owns is a write that did not happen — the *carrier* is now stated per surface, with a table for the two shapes, the obligation never to write before the session is open, and the note that passing an argument a tool does not declare is an invalid call, not explicit attribution. §2's payload line, the checklist and the Forbidden list follow it; `execute-phase` 6.2.1 mirrors the pointer. Wording fix, no new behaviour. | | 2.4.0 | 2026-10-22 | minor | **The Engram memory protocol (feature 66 P2).** `references/MEMORY.md` is the protocol's single home: the bounded probe (`agentic-workflow memory probe`) before a session's first read; three save boundaries — unit start, each phase transition, unit close — under the stable `unit//doc` topic with an explicit `project` and `session_id`; the fixed recall order (`mem_context` → scoped search with an explicit `project` → one explicit `all_projects: true` retry → `mem_get_observation` → the unit doc wins); the disclosed keyword-bound ceiling (a miss is not evidence of absence); session attribution (`mem_session_end` only on confirmed-stale ids, the explicit project fallback for `mem_session_summary`); the D2 split against `docs/LOGS.md`; and the degradation rule (mirror marked pending, local progress preserved, both sides read back). SKILL.md gains one turn-contract box and the §5b boundary step. Memory is required substrate, never a hard failure dependency. | diff --git a/README.md b/README.md index d39005734..936d80028 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ review the change → audit → merge.** | Skill | What it does | | --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `execute-phase` | Executes **one phase per turn** (` P`, tier attached) with its own acceptance-blob check, phase/invariant/dependency gates, tests, docs, and commit — the closing hand-off names the next single phase and the catalog tier, and resumes `unit-lane` after the last phase. Looping all remaining phases (`` with no phase) is an explicit operator choice only. Halts on red/no-progress/attempt budget. Discoveries become `Autofix`, `Opportunistic Fix`, or a proposal—never an automatic issue. Each phase's pre-flight runs `bun scripts/phase-lint.mjs ` and pastes its output. It never interviews: a missing input runs the three-tier dispatch and then `scripts/needs-input.mjs` prints the `open-questions@1` block and the run stops, routed back to the stage that owns the fact. | +| `execute-phase` | Executes **one phase per turn** (` P`, tier attached) with its own acceptance-blob check, phase/invariant/dependency gates, tests, docs, and commit — the closing hand-off names the next single phase and the catalog tier, and resumes `unit-lane` after the last phase. Looping all remaining phases (`` with no phase) is an explicit operator choice only. Halts on red/no-progress/attempt budget. Discoveries become `Autofix`, `Opportunistic Fix`, or a proposal—never an automatic issue. Each phase's pre-flight runs `bun scripts/phase-lint.mjs ` and pastes its output. The whole-unit close-out settles the roadmap row through the crate's writer (`bun scripts/roadmap-settle.mjs` exits 0, issue #316). It never interviews: a missing input runs the three-tier dispatch and then `scripts/needs-input.mjs` prints the `open-questions@1` block and the run stops, routed back to the stage that owns the fact. | ### Review & audit — _change → PR → product_ diff --git a/docs/LOGS.md b/docs/LOGS.md index 432f5a327..442c3d819 100644 --- a/docs/LOGS.md +++ b/docs/LOGS.md @@ -3478,3 +3478,27 @@ out to do, what was decided and *why*, and where to resume. - **Summary:** Replace the @bacnh85/pi-serena bridge with native Serena MCP registration (#321) - **Decisions:** Replaced the @bacnh85/pi-serena bridge (which coupled pi to Serena's internal Python APIs and predates v2.0.0) with pi's native MCP support (pi mcp add serena -- serena start-mcp-server --context claude-code). The installed version chain was reworked to read from the tool itself (serena --version -> uv tool list -> pip show fallback) because pip show cannot see the uv tool install route documented by the repo. PyPI fetch moved to curl against PyPI's JSON API (pip index versions fallback). External verification blocked the PyPI half on realistic input and the fix was applied in a correction batch. - **Next:** PR #324 is open (fix-serena-mcp -> main) awaiting CI green and human merge. Pending by design: AC2 live tool-reachability in a session (consent-gated, closes with the first real /init-workspace run with Serena). +## 2026-10-27T09:31:40Z — fix-316-roadmap-writer-call-sites — manual +- **Commits:** 12 (`8aaf102f…a2f0d096`) +- **Files:** 30 files +- **Summary:** /audit-pr on PR #323 (unit 316: roadmap settle is code) — BLOCKED with 6 blockers (merge conflict, CI missing, Closes #316, receipt stale, missing ledger, call sites executable); cleared all blockers: fixed roadmap CLI bare-subcommand resolution, unified unit-cycle settle grader, added --root usage guard, wrote obligation ledger + planning evidence, re-cut pi package to 0.35.1, resolved rebase, fixed subsectionBody section-boundary defect in unit-lineage, re-based skill-context budgets, Closes #316 in PR body, pushed, CI green -- receipt stale -- +- **Decisions:** roadmap rowUpsert bare subcommand resolved the word as table path (ENOENT) -- fixed with usage-error guard; bare rowUpsert/annotate now exits 2 with usage line; unit-cycle settled through checkUnitDoc instead of own weaker grader; segment body closed at nearest ##/### instead of first ### anywhere (defect that would have blocked every unit with a findings table) -- learned the working form and the kind boundary (rowUpsert only speaks roadmap-table@1 with NN header, so fix indexes use status-cell edit instead) +- **Next:** /review-change 316-roadmap-settle — receipt is stale (moved head past the marker); /audit-pr 323 after review passes +## 2026-10-27T09:32:07Z — fix-316-roadmap-writer-call-sites — manual +- **Commits:** 13 (`8aaf102f…9bc57997`) +- **Files:** 31 files +- **Summary:** Unit 316 resumed. Retrospective unit doc created and index registered. Triage ran (plan implement tests evidence review docs). Evidence verified: roadmap-check 17 pass, roadmap-settle 7 pass, unit-cycle 45 pass, live gate PASS 0 violations, budgets 28 skills pass, root suite 905 pass. Review cycle 1: FAIL with F1 (high) unit-cycle settle commit argv bug and F2 (low) row 244 rename undocumented. Fold 523a95e2: settleCommitArgs() pure builder fixing the argv bug. Cycle 2: PASS. Receipt stage: review-receipt verify --pr 323 exit 3 missing-review-receipt. +- **Decisions:** Tree: user chose fix tree for #316 (D-Q1). Diff-guard BREACH recorded as exception (D2). Row 244 rename resolved by documentation (D3). +- **Next:** Close-out blocked on receipt: /review-change 316-roadmap-settle to post the SHA-bound review-change:pass on PR #323; once receipt current /audit-pr PR #323 for merge +## 2026-10-27T09:32:48Z — fix-316-roadmap-writer-call-sites — manual +- **Commits:** 14 (`8aaf102f…7c3f23cb`) +- **Files:** 31 files +- **Summary:** /review-change on unit 316 — delta-scoped cycle 3 review: all folded rows re-verified fixed, all gates green at reviewed head, SHA-bound receipt posted and current at PR #323 +- **Decisions:** Cycle 3 ran delta-scoped (the post-PASS record commits fc36bfe3+783b57f1 — unit-doc only) rather than re-running the full-pack on the whole diff; the review appended R3+G4 marks to the unit doc, committed, pushed, and emitted the SHA-bound receipt at the new head using --scope-base main to derive the affecting-path manifest so later foreign-only commits don't void it +- **Next:** Merge PR #323 (unit 316 roadmap-settle): receipt is current, review is clean. The fix-index unit 258 (gate evidence) and unit 261 (settle call sites execute) are separate units on the same branch — each will need its own /review-change + /audit-pr cycle before the branch is mergeable as a whole. +## 2026-10-27T09:32:53Z — fix-316-roadmap-writer-call-sites — manual +- **Commits:** 15 (`8aaf102f…3b007036`) +- **Files:** 31 files +- **Summary:** External verification and PR for issue #316 (roadmap settle is code) +- **Decisions:** External verifier: read-only verification of all 4 commits on fix-316-roadmap-writer-call-sites against base 9da14169 — all tests green (7+43+17=67), sibling sweep clean, no blockers. Verdict: VERIFIED. PR #323 opened targeting main. +- **Next:** Monitor PR #323 CI, merge once green. Continue fix-316 work if any follow-up items remain in the 316 unit doc. diff --git a/docs/fix/316-roadmap-settle/SPEC.md b/docs/fix/316-roadmap-settle/SPEC.md new file mode 100644 index 000000000..44efc3fa3 --- /dev/null +++ b/docs/fix/316-roadmap-settle/SPEC.md @@ -0,0 +1,441 @@ +--- +scope: standard +--- + +# fix/316-roadmap-settle + +> Fix specification — one single unit document. Retrospective unit: the +> implementation landed in four commits (9950ba61…445a0777) before this doc +> existed; the doc records the requirements, evidence and close-out for PR +> #323. + +## Issue + +`#316` — tracked issue in the project's forge. The PR must close it via +`Closes #316` in the body. + +## Specs + +Requirements, each numbered `S#` and quoting the requester's own words verbatim +between the quote markers. + +**S1 — the writer gets call sites.** +> the release/close-out step **calls** the crate's writer (`roadmap rowUpsert`) +> instead of instructing it in prose, and `scripts/unit-cycle.mjs` writes the row +> itself rather than asking the model to + +**S2 — the doc↔row contradiction fails closed offline.** +> a unit doc that carries a PR URL whose row is not `done · [#N](same url)` fails +> closed **offline** (root suite), with a named code — no forge call needed + +**S3 — the settle is a task the executor owns.** +> the transition is a `## Tasks` item so the executor owns it and a skipped write +> is a failed task, not a missing courtesy + +**S4 — the concrete scope: crate check, root gate, call sites.** +> crate: a cross-document check (`roadmap check` or `unit index-check`) comparing a +> unit doc's own facts (NN, slug, status token, PR URL) with its roadmap row, a +> closed exit contract, tests, version bump + CHANGELOG row; +> this repo's wiring: a `scripts/` gate plus a case in the root suite; +> the call sites: `unit-lane`'s release step, `execute-phase`'s close-out and +> `scripts/unit-cycle.mjs` invoke the writer, and the lane's plan step emits the +> settle as a task. + +## Impact + +- **Consumers of what it delivers**: every future lane unit — the `done` settle + stops being a prose courtesy and becomes a deterministic write + an offline + gate; `audit-pr`'s traceability gate stops catching the miss after the review + pass was already paid; `scripts/unit-cycle.mjs` loop drivers get the settle as + a state-machine action instead of an instruction to the model. +- **Mentions that stop being true when it lands**: the prose checklist items + that told the model to "set the roadmap row" + (`skills/unit-lane/references/RELEASE.md:33`, `skills/unit-lane/SKILL.md:193-195`) + — replaced by writer-call checklist items and a release-settle task + (`skills/unit-lane/references/PLAN.md` P). +- **What to ask before touching something live on the other side**: nothing — + the crate's `roadmap rowUpsert` writer grammar is untouched; only a + separator-detection fix (the old literal `|---|` match refused the repo's own + `|----|------|…` tables) widens acceptance of existing tables. + +## Non-goals + +- The row **composer** built from SPEC title/slug/summary/PR — + [#227](https://github.com/gtrabanco/agentic-workflow/issues/227) (producer half). +- The schema-wide deterministic `verify` for unit docs — + [#260](https://github.com/gtrabanco/agentic-workflow/issues/260) (verifier half). +- The docs-only zero-code **repair-receipt receiver** — + [#301](https://github.com/gtrabanco/agentic-workflow/issues/301) (closed; + re-trigger #260). This fix removes the most frequent *source* of that delta, + it does not implement the receiver. +- Any roadmap schema change. +- Findings discovered during implementation never expand scope beyond the + declared regression boundary below. + +## Future + +none + +## Future cost + +| Rule | Who it binds | +|---|---| +| The `done` settle stays **code**: any future change to the lane's close-out/release grammar must keep a writer call site (`roadmap rowUpsert` via `unit-cycle` or the skill checklist); reverting to prose-only re-opens #316. | skill authors, `unit-lane`/`execute-phase` maintainers | +| A new doc↔row violation kind must be added to the crate's closed code set (`ROADMAP_CHECK_CODES`, `packages/agentic-workflow/src/edit/Roadmap.mjs`) and mirrored by the root gate (`scripts/roadmap-settle.mjs`) in the same PR — never as a free-string in one of the two. | crate + repo maintainers | + +## Applicable tests + +- `packages/agentic-workflow/test/roadmap-check.test.mjs` — the crate check + (17 cases: closed codes, exit contract, separator shapes, own-PR surfaces). +- `scripts/roadmap-settle.test.mjs` — the root gate against the frozen-failure + corpus (77/79 fail, 40/78/80/272/91 consistent). +- `scripts/unit-cycle.test.mjs` — the settle-roadmap act and its position in + the state machine. +- Root suite: `node --test scripts/*.test.mjs`. + +## Known pre-existing issues + +- [#227](https://github.com/gtrabanco/agentic-workflow/issues/227) — row composer + missing: **does-not-affect** this unit (S4's check reads the doc's own facts; + it never composes a row). +- [#260](https://github.com/gtrabanco/agentic-workflow/issues/260) — schema-wide + unit-doc verifier missing: **does-not-affect** this unit (the check here is + doc↔row only, not the full unit-doc schema). +- [#301](https://github.com/gtrabanco/agentic-workflow/issues/301) (closed) — + repair-receipt receiver missing: **does-not-affect** this unit (declared + non-goal; the re-trigger condition stays #260). +- Unit 77's roadmap row is `in-progress` on `main` while PR #309 merged — + **does-not-affect** the fix itself: it is the gate's frozen-failure corpus + (`scripts/fixtures/roadmap-settle/docs/features/77-settle-repro/`); repairing + the live row on `main` is post-merge reconciliation, out of scope. + +## Tasks + +- P1 — crate: `roadmap check ` subcommand with `checkUnitDoc` / + `findRow`, closed code set `ROADMAP_CHECK_CODES` + (`row-missing`, `slug-mismatch`, `pr-url-unsettled`), exit contract 0 + consistent · 1 violations · 2 usage, and the separator-detection fix + (validator: `node --test packages/agentic-workflow/test/roadmap-check.test.mjs` exits 0) + (Implements: S2, S4) + Relevant files: packages/agentic-workflow/src/edit/Roadmap.mjs, packages/agentic-workflow/bin/agentic-workflow.mjs, packages/agentic-workflow/test/roadmap-check.test.mjs +- P2 — repo wiring: root gate `scripts/roadmap-settle.mjs` (offline, fails + closed, named codes) + frozen-failure corpus under + `scripts/fixtures/roadmap-settle/` + case in the root suite + (validator: `bun test scripts/roadmap-settle.test.mjs` exits 0) + (Implements: S2, S4) + Relevant files: scripts/roadmap-settle.mjs, scripts/roadmap-settle.test.mjs, scripts/fixtures/roadmap-settle/ +- P3 — `scripts/unit-cycle.mjs` writes the settle itself: `settleRoadmapRow()` / + `parsePrUrl()` and a `settle-roadmap` state-machine action between release + and the receipt chain, via the crate's `rowUpsert`, every cell preserved + (validator: `bun test scripts/unit-cycle.test.mjs` exits 0) + (Implements: S1) + Relevant files: scripts/unit-cycle.mjs, scripts/unit-cycle.test.mjs +- P4 — skill call sites: `unit-lane` RELEASE.md checklist calls the writer, + PLAN.md emits the release-settle task P, `execute-phase` close-out step + invokes it; README + `docs/workflow/LANE_FLOW.md` API table + (validator: `bun scripts/check-skill-context.mjs` exits 0) + (Implements: S1, S3) + Relevant files: skills/unit-lane/SKILL.md, skills/unit-lane/references/PLAN.md, skills/unit-lane/references/RELEASE.md, skills/execute-phase/SKILL.md +- P5 — verification: full gate at the settled head (root suites, crate tests, + schema tests, budgets) + (validator: `node --test scripts/*.test.mjs` exits 0) + (Implements: S1, S2, S3, S4) + +## Triaged steps + +Paste of `bun scripts/unit-route.mjs --triage 316-roadmap-settle` (2026-10-26, exit 0) — the authoritative step list: + +```text +TRIAGE — 316-roadmap-settle (fix) +Steps: plan, implement, tests, evidence, review, docs +Tiers: plan: cheap, implement: cheap, tests: strong, evidence: strong, review: strong, docs: cheap +Skipped: research: trivial scope, design: trivial scope, release: not a feature +Budget: strong +``` + +Plan/implement/tests already ran (retrospective — P1–P4 in the Log above); +remaining triaged steps: evidence, review, docs. + +### Planning evidence + +One row per claim the SPEC rests on. Every claim is either the issue's own words +(quoted verbatim in `## Specs`) or a reproduction run on this candidate — no +claim here is a guess. + +| id | claim | authority-kind | authority-source | freshness | affected-decision-or-obligation | +|---|---|---|---|---|---| +| PE-001 | The crate's roadmap writer exists and is tested, but carries zero call sites and nothing verifies doc↔row. | issue-text | `#316` body, "The four layers, measured" table | current | O1, O2 | +| PE-002 | A unit doc carrying a PR URL is offline-detectable against its own index row, with no forge call. | reproduced | `bun scripts/roadmap-settle.mjs` over the 77/79 frozen corpus — fails closed with `pr-url-unsettled` | current | O2 | +| PE-003 | The transition is a `## Tasks` item, not a courtesy: the lane's plan step emits it and a skipped write fails the task. | issue-text | `#316` body, "Expected behaviour" third bullet | current | O3 | +| PE-004 | The writer's separator rule refused this repository's own tables, so the writer the issue names could not write them. | reproduced | `packages/agentic-workflow/test/roadmap-check.test.mjs` separator cases; `SEPARATOR_RE` now accepts any pipes/dashes/colons rule | current | O1, O4 | +| PE-005 | `roadmap rowUpsert` resolved a bare subcommand word as the table path, so the form the skills named could never run. | reproduced | scratch dir: `roadmap rowUpsert '{…}'` → `EDIT REFUSAL — ENOENT … rowUpsert`; after the repair → `usage…`, exit 2 | current | O4 | +| PE-006 | A fix-index row is not an `NN`-headed roadmap table, so `rowUpsert` cannot settle a fix unit. | reproduced | scratch dir: `rowUpsert` over `docs/fix/README.md` → `Roadmap table header not found` | current | O4 | + +### Obligations + +One row per normative behaviour, compatibility invariant, affected use case, and +required failure state. Every row is `verified` on this candidate — the validator +in each row is the command that was run, with the outcome it produced. + +| obligation-id | authority-source | affected-use-case-or-invariant | phase | task | implementation-owner | validator | required-evidence | status | +|---|---|---|---|---|---|---|---|---| +| O1 | S1 | The close-out/release step **calls** the crate's writer instead of instructing it in prose; `unit-cycle` writes the row itself. | P3, P4 | P3, P4 | `execute-phase` | `bun test scripts/unit-cycle.test.mjs` exits 0 (45 pass, incl. the `settleRoadmapRow` cell-preservation case) | `## Log` E3, E7 | verified | +| O2 | S2 | A unit doc that carries a PR URL whose row is not `done · [#N](same url)` fails closed **offline**, with a named code and no forge call. | P1, P2 | P1, P2 | `scripts/roadmap-settle.mjs` | `bun test scripts/roadmap-settle.test.mjs` exits 0 (8 pass, incl. the `--root`-without-value usage case) and `node --test packages/agentic-workflow/test/roadmap-check.test.mjs` exits 0 (18 pass) | `## Log` E1, E2 | verified | +| O3 | S3 | The settle is a `## Tasks` item so the executor owns it and a skipped write is a failed task. | P4 | P4 | `unit-lane` plan step | `grep -c 'release-settle' skills/unit-lane/references/PLAN.md` matches ≥1 and `bun scripts/check-skill-context.mjs` exits 0 | `## Log` E5 | verified | +| O4 | S4 | The writer's call sites name a form that executes, and the roadmap grammar keeps accepting the repository's real tables. | P1, P4 | P1, P4 | `agentic-workflow` crate | `node --test packages/agentic-workflow/test/roadmap-check.test.mjs` exits 0 (18 pass) and a bare `roadmap rowUpsert` exits 2 with usage | `## Log` E1, E9 | verified | +| O5 | `## Regression scope` | Existing `rowUpsert`/`annotate` grammar and cell preservation are unchanged; `unit-cycle`'s prior actions and ordering are unchanged. | P1, P3 | P1, P3 | `agentic-workflow` crate | `node --test packages/agentic-workflow/test/edit.test.mjs` exits 0 and `bun test scripts/unit-cycle.test.mjs` exits 0 | `## Log` E3, E7 | verified | +| O6 | S2 | The live tree stays green: no shipped unit doc fails the new gate. | P5 | P5 | `scripts/roadmap-settle.mjs` | `bun scripts/roadmap-settle.mjs` exits 0 · `verdict: PASS · violations: 0` | `## Log` E4 | verified | + +## Findings + +OPEN rows only: a row a review persisted as fix-now work and no fold has closed +yet. A folded row leaves this table and becomes a `## Log` line. + +```text +findings-table@1 +column | shape | what it holds +id | F | the stable finding id a fold and a log line cite +target | S · : | the requirement or the location the finding names +axis | | the review axis that found it +severity | high \| med \| low | the ledger severity (the canonical scale) +class | product \| plan \| source \| environment \| runtime | the owner that repairs it +route | fold \| replan-in-unit \| decision-required \| product owner — … \| | where the row goes; the router reads this column's head first (`scripts/unit-route.mjs`), and `class` names the owner that repairs it +``` + +| id | S# / file:line | axis | severity | class | route | +|---|---|---|---|---|---| + +## Review verdict + +### Cycle 1 — 2026-10-26, independent reviewer @ HEAD 445a0777 + 27762d27 + +``` +REVIEW-VERDICT: FAIL +- Findings: 2 material, 4 report-notes +- Evidence reproduced: yes +- AC integrity: unchanged from triage +``` + +| id | S# / file:line | axis | severity | class | route | +|---|---|---|---|---|---| +| F1 | S1 · scripts/unit-cycle.mjs:553 | correctness | high | source | fold — the settle act's commit ran `git commit -- -m `: options after `--` are pathspecs, so the commit always failed (reproduced exit 1) and the S1 code-driven settle could never land its commit; the 43 unit-cycle tests only covered pure `decide`/`settleRoadmapRow`, never the commit argv | +| F2 | S4 · docs/fix/README.md (row 244) | scope | low | source | fold — the fix-index row 244 topic rename (`freeze-batch-next-consumer` → `freeze-batch-planner-consumer`, commit 96544a7d) was a deliberate gate-driven repair (the gate's `slug-mismatch` code fails the live tree on the d1bb61c5-born mismatch) but was never documented in the unit doc | +| F3 | S4 · scripts/unit-cycle.mjs:451 | correctness | low | source | report-note — the loop's settle fact checks only `[#](` link presence, not the exact URL; a wrong-URL settle passes unit-cycle yet fails `roadmap-settle.mjs` — two graders, two strengths | +| F4 | S1 · skills/execute-phase/SKILL.md:186 · skills/unit-lane/references/RELEASE.md:33 | contract consistency | low | source | report-note — the close-out/release settle instruction is unconditioned on unit kind; for fix units a `roadmap rowUpsert` against `docs/features/ROADMAP.md` would insert a spurious feature row, while `unit-cycle`'s `sense`/`settleRoadmapRow` exempt fix units — prose and code disagree on the fix-unit case | +| F5 | S4 · scripts/roadmap-settle.mjs:50 | correctness | low | source | report-note — `--root` with no value throws an uncaught TypeError instead of the documented exit-2 usage path | +| F6 | S2 · docs/features/ROADMAP.md:87 | scope | low | source | report-note — live row 77 is settled to `done · [#309]` in this diff although the SPEC's Known-issues section declares live-row repair post-merge reconciliation, out of scope — necessary for the live-tree-green gate, but it contradicts the doc's own scope statement | + +### Cycle 2 — 2026-10-26, after the fold + +``` +REVIEW-VERDICT: PASS +- Findings: 0 material, 4 report-notes (F3–F6 stand) +- Evidence reproduced: yes +- AC integrity: unchanged from triage +``` + +F1 re-verified fixed (argv order + 2 new tests + scratch-repo commit run); +F2 resolved by documentation (D3). Report-notes are informational, not blockers; +F4 is the strongest — it stays visible as a re-trigger condition for a future +fix-unit routing pass. + +### Cycle 3 — 2026-10-27, delta-scoped, after the post-PASS record commits + +``` +REVIEW-VERDICT: PASS +- Findings: 0 material, 2 low report-notes +- Evidence reproduced: yes +- AC integrity: unchanged from triage +``` + +The delta since cycle 2's head was the post-PASS record commits (unit-doc log +lines only). The two low notes: G3 sits above G1 in the `## Log` (append-only, +left in place), and R1's "gates exit 0" is unqualified — the settle gates +passed, `diff-guard` excepted per D2. Folded rows F1/F2 re-verified. + +### Cycle 4 — 2026-10-27, the audit's blockers repaired (this commit range) + +No review has run over this range yet — the `audit-pr` pass that produced +findings F7–F12 moved the head, so the cycle-3 receipt is void and the next +review is `/review-change 316-roadmap-settle`, not a new verdict here. What the +range contains is recorded in `## Log` as F7–F12's fold lines. + +## Log + +Append-only. `L1` is the issue text verbatim; every later line carries one kind +token from `log-kind@1`. + +- **L1** — 2026-10-26, the issue, verbatim: + ``` + ISSUE: the roadmap row's done settle is prose-only — the crate's roadmap writer has zero call sites and nothing verifies doc↔row + + ### What's broken + + The roadmap row's `done` settle — the status-machine write `done` + PR link at PR + open, per `skills/audit-pr/references/02_CLOSURE_AND_SCOPE_GATES.md:31` and + `skills/pre-execution-review/references/SNAPSHOT.md:58` — has a **deterministic + detector and no deterministic writer**. It is the only lane transition assigned to + free-form prose. + + Two live cases, both caught late (at `audit-pr`, i.e. after the review pass was + already paid): + + | Unit | State of the artefact | State of the row | + |---|---|---| + | 77 `promise-coherence-guard` | PR #309 **merged** 2026-10-21 | `in-progress` on `main` today | + | 79 `spec-by-reference` (PR #311) | 7 phases logged, PR open, review receipt posted | `in-progress` until the audit blocked on it | + + ### The four layers, measured + + | Layer | Where | State | + |---|---|---| + | Writer (crate) | `packages/agentic-workflow/bin/agentic-workflow.mjs:212-238` → `roadmap rowUpsert` / `annotate`; `src/edit/Roadmap.mjs`; documented `docs/workflow/LANE_FLOW.md:255` | **exists, tested** (`test/edit.test.mjs:150,164`) | + | Call sites of that writer | `grep -rn "agentic-workflow roadmap" --include=*.md --include=*.mjs skills scripts` | **0** | + | Code-driven cycle | `scripts/unit-cycle.mjs` | **0 `writeFile`** — `:196` and `:227` only instruct the model to "set the roadmap row" | + | Prose owners | `skills/unit-lane/SKILL.md:193-195`, `references/RELEASE.md:33`; the plan's `## Tasks` (`P1`–`Pn`) | prose checklist item, never a task the executor owns | + | Detector | `scripts/unit-route.mjs:884` (fail-closed when every phase is recorded and the status is not `done`); `grep -c writeFile` → **0** | deterministic, but has **no writer to point at** | + + The inverse invariant is also unchecked in code: a unit doc **carrying a PR URL** + must have a row reading `done · [#N](same url)`. The only check is the LLM rule in + `skills/audit-docs/SKILL.md:70-74`, and it covers the **other direction** (`done` + ⇒ link). Both live cases carry the URL in-doc + (`docs/features/77-promise-coherence-guard/SPEC.md:274`, and 79's Evidence row), so + the contradiction is offline-detectable. + + ### Reproduction + + 1. Open a unit's PR without touching its roadmap row — the happy path of every + lane-era step list, because no task owns it. + 2. `bun scripts/unit-route.mjs ` → `status: in-progress · route: execute · next: /execute-phase … P1`, even with every phase logged. + 3. `/audit-pr` → BLOCKED on `Traceability`; the fix is a documentation commit that + then voids the `REVIEW-PASS` receipt at the head. + + ### Expected behaviour + + The settle is code and the contradiction fails locally, not at the merge gate: + + - the release/close-out step **calls** the crate's writer (`roadmap rowUpsert`) + instead of instructing it in prose, and `scripts/unit-cycle.mjs` writes the row + itself rather than asking the model to; + - a unit doc that carries a PR URL whose row is not `done · [#N](same url)` fails + closed **offline** (root suite), with a named code — no forge call needed; + - the transition is a `## Tasks` item so the executor owns it and a skipped write + is a failed task, not a missing courtesy. + + ### Scope — narrow slice, subordinate to existing owners + + This is deliberately **not** the general tool. The wider halves are already tracked + and postponed: + + - **#227** — deterministic *emitters*: the row composer built from SPEC + title/slug/summary/PR (producer half). + - **#260** — the schema-wide deterministic `verify` for unit docs, sibling of the + crate's `edit` CLI, failing closed against the schema package's published + constants (verifier half; #227's 2026-09-24 scope adjustment names it). + - **#301** (closed, re-trigger `#260`) — docs-only zero-code repairs voiding the + `REVIEW-PASS`. This issue removes the most frequent *source* of that delta but + does not implement the repair-receipt receiver #301 wants. + + In scope here: + + - crate: a cross-document check (`roadmap check` or `unit index-check`) comparing a + unit doc's own facts (NN, slug, status token, PR URL) with its roadmap row, a + closed exit contract, tests, version bump + CHANGELOG row; + - this repo's wiring: a `scripts/` gate plus a case in the root suite; + - the call sites: `unit-lane`'s release step, `execute-phase`'s close-out and + `scripts/unit-cycle.mjs` invoke the writer, and the lane's plan step emits the + settle as a task. + + Out of scope: the row composer (#227), the full unit-doc verifier (#260), the + repair-receipt receiver (#301), and any roadmap schema change. + + ### Why it is filed instead of absorbed + + Three near-identical misses (77 on `main`, 79 in PR #311, plus the audit that had + to persist it as `F43`) mean the guard exists while the actor does not — the + failure mode `AGENTS.md`'s "checklists over heuristics" rule is meant to prevent. + The fix is small and verifiable; the general tools stay where they are. + ``` +- **D1** — 2026-10-26, owner decision: the unit lives in the **fix tree** + (`docs/fix/316-roadmap-settle/`, registered in `docs/fix/README.md`), not the + feature tree. Authority: user answer to lane question `ask 00e3cc04-0105-43d1-9569-691a07eee23a` + (option `fix_tree`), chosen over the typed `--from-issue` invocation because the + issue carries `bug` + `fix-next` and the branch is `fix-316-roadmap-writer-call-sites`. +- **P1** — 2026-10-26, P1 — crate `roadmap check` implemented · commit 9950ba61. +- **P2** — 2026-10-26, P2 — root gate + frozen-failure corpus implemented · commit 96544a7d. +- **P3** — 2026-10-26, P3 — unit-cycle settle act implemented · commit 3dea3e9f. +- **P4** — 2026-10-26, P4 — skill call sites + docs + version bumps implemented · commit 445a0777. +- **E1** — 2026-10-26, `node --test packages/agentic-workflow/test/roadmap-check.test.mjs` · exit 0 · 17 pass / 0 fail. +- **E2** — 2026-10-26, `bun test scripts/roadmap-settle.test.mjs` · exit 0 · 7 pass / 0 fail (frozen-failure corpus: 77/79 fail, consistent docs pass). +- **E3** — 2026-10-26, `bun test scripts/unit-cycle.test.mjs` · exit 0 · 43 pass / 0 fail. +- **E4** — 2026-10-26, `bun scripts/roadmap-settle.mjs` (live tree) · exit 0 · `verdict: PASS · violations: 0` over 110 unit docs. +- **E5** — 2026-10-26, `bun scripts/check-skill-context.mjs` · exit 0 · `PASS context budgets: 28 skills`. +- **E6** — 2026-10-26, `node --test scripts/*.test.mjs` · exit 0 · 903 pass / 0 fail (120s). +- **E7** — 2026-10-26 (post-fold), `bun test scripts/unit-cycle.test.mjs` · exit 0 · 45 pass / 0 fail (43 + 2 new settleCommitArgs tests). +- **E8** — 2026-10-26 (post-fold), `node --test scripts/*.test.mjs` · exit 0 · 905 pass / 0 fail. +- **E9** — 2026-10-26 (post-fold), scratch-repo verification of the fixed argv: `git commit -m "docs(roadmap): settle 80-example — done · [#323](url)" -- docs/features/ROADMAP.md` · exit 0 · commit landed (the F1 repro now passes). +- **G2** — 2026-10-26, `bun scripts/review-receipt.mjs verify --pr 323` · exit 3 · `missing-review-receipt` (no REVIEW-PASS marker with contract=v1 in the PR comments) — routes the close-out to `/review-change`. +- **G3** — 2026-10-26, `agentic-workflow memory probe --json` · memory-probe@1 verdict **`degraded`** — http-health/mcp-runner/binary-version/sync-staleness ok; `session-ambiguity` failed (2 ambiguous active-runtime sessions, 20 closed-space sync findings). Non-blocking; recorded per the lane's memory protocol. +- **G1** — 2026-10-26, `bun scripts/diff-guard.mjs --base main --unit 316` · **BREACH** — Lines: 1101 > 400 · Files: 30 > 8. Anti-gaming respected: nothing deleted to shrink the diff. → recorded as exception D2. +- **D2** — 2026-10-26, owner-recorded exception (diff-guard): the whole-unit diff is 1101 lines / 30 files against the 400/8 budget — a four-layer slice (crate + root gate + unit-cycle + four skills/docs surfaces) that cannot shrink without dropping one of the four layers S1–S4 demand; work was committed across four scoped commits (9950ba61 → 445a0777) before this doc existed. Precedent: unit 78's recorded exception in `docs/features/78-spec-by-reference` decisions.md. Not gaming: the diff was never shrunk. +- **D3** — 2026-10-26, owner-recorded decision (resolves review finding F2): commit 96544a7d renamed fix-index row 244's topic `freeze-batch-next-consumer` → `freeze-batch-planner-consumer` because the new root gate's `slug-mismatch` code fails the live tree on the row↔folder mismatch born in d1bb61c5 — the repair is the gate's own first catch, in-scope as P2's validator passing on the live tree (E4), now documented here. +- **R1** — 2026-10-26, review run — `HEAD 445a0777 (+27762d27)`, verdict **FAIL**, cycle 1, gates exit 0. 2 material (F1, F2), 4 report-notes (F3–F6); verdict section above. +- **F1** — 2026-10-26, folded — F1 repaired: the settle commit argv extracted into pure `settleCommitArgs()` (`-m` before `--`), 2 new tests covering the argv contract, fixed command verified to commit in a scratch repo · commit 523a95e2. +- **F2** — 2026-10-26, folded — F2 resolved by documentation: D3 records the row-244 rename and its gate-driven reason · commit 523a95e2. +- **R2** — 2026-10-26, review run — `HEAD 523a95e2`, verdict **PASS**, cycle 2, gates exit 0. 0 material, 4 report-notes (F3–F6 stand). +- **G4** — 2026-10-27, gate runs at `HEAD 783b57f1` — `node --test scripts/*.test.mjs` · exit 0 · 905 pass / 0 fail; `bun test scripts/unit-cycle.test.mjs` · exit 0 · 45 pass / 0 fail; `node --test packages/agentic-workflow/test/roadmap-check.test.mjs` · exit 0; `bun test scripts/roadmap-settle.test.mjs` · exit 0 · 7 pass / 0 fail; `bun scripts/roadmap-settle.mjs` (live tree) · exit 0 · `verdict: PASS · violations: 0` over 110 unit docs; `bun scripts/check-skill-context.mjs` · exit 0 · 28 skills. +- **R3** — 2026-10-27, review run — `HEAD 783b57f1`, delta-scoped cycle 3 (delta since the cycle-2 head: the post-PASS record commits fc36bfe3 + 783b57f1, unit-doc log lines only), verdict **PASS**, gates exit 0 (G4). 0 material; 2 low report-notes (G3 sits above G1 in this log — insert position, left in place per append-only; R1's "gates exit 0" unqualified — the settle gates, diff-guard excepted per D2). Folded rows re-verified: F1 fixed (`settleCommitArgs()` `-m` before `--`, scripts/unit-cycle.mjs:543, 45 tests), F2 resolved by D3 (direct read). Findings table stays empty; receipt emitted at the head of the record commit below. + +- **F7** — 2026-10-27, folded — the `main` conflict is gone: the branch was rebased onto `origin/main` (`c893ea1a`, which had meanwhile merged #324). Two files conflicted and both resolved on the base's intent: `CHANGELOG.md` (main's 0.35.0 kept, this unit re-cut as 0.35.1) and `docs/fix/README.md` (main had cleared the index of merged entries; only the #316 row returns). Nothing was deleted to make a merge pass. +- **F8** — 2026-10-27, folded — the obligation ledger exists: `### Planning evidence` (PE-001…PE-006) and `### Obligations` (O1…O6, every row `verified` with the validator that ran) join the unit doc. `bun scripts/unit-lineage.mjs --unit 316-roadmap-settle` → `LINEAGE OK — lane-era (triage block re-derives) · obligations closed (6)`. +- **F9** — 2026-10-27, folded — the head that carries the repairs is the one that produces the forge run; the pre-repair head carried zero check runs. Local gate at this head: `node --test scripts/*.test.mjs` 922/922, crate suite 254/254, `bun scripts/roadmap-settle.mjs` PASS over 110 unit docs, `bun scripts/check-skill-context.mjs` (skills and `--routes`) exit 0. +- **F10** — 2026-10-27, folded — `Closes #316` added to the PR #323 body; the fix-index row stays `done` · `[#323](…)`. +- **F11** — 2026-10-27, folded — `## Next` rewritten to this head's real state (receipt stale, hand-off `/review-change`) and the cycle-3 verdict block recorded, so the section no longer contradicts the receipt it describes. +- **F12** — 2026-10-27, folded — F3: `unit-cycle` grades the settle through the crate's `checkUnitDoc` (one rule, exact URL) instead of its own `[#N](`-prefix test. F4: both call sites name the executable form and the kind boundary, and a bare `roadmap rowUpsert`/`annotate` is a usage error rather than a filename. F5: `roadmap-settle --root` with no value exits 2 with usage. F6 is moot — `main` had already settled roadmap row 77, so the rebase carries no out-of-scope live edit. +- **D4** — 2026-10-27, owner-noted decision: writing the ledger exposed a defect in the gate it had to pass — `scripts/unit-lineage.mjs`'s `subsectionBody` preferred the first `###` heading found anywhere over a nearer `##`, so the `### Obligations` section swallowed the following `## Findings` table and reported F7–F12 as open obligations. Fixed to stop at the nearest `##`/`###`, with a regression test; the defect would have blocked every unit doc carrying a findings table. +- **D5** — 2026-10-27, owner-noted decision: the pi package re-cuts to **0.35.1** because `main` published 0.35.0 (#321, the Serena bridge replacement) while this branch was open; shipping 0.35.0 again would publish nothing. The crate keeps **0.5.0** (above `main`'s 0.4.0, no collision). D3's row-244 rename is moot on the rebased base: `main` had already removed that row. + +- **G5** — 2026-10-27, gate runs at `HEAD 5239889a` — `node --test scripts/*.test.mjs` · exit 0 · 922 pass / 0 fail; crate suite `bun run test` · exit 0 · 254 pass / 0 fail; `bun scripts/roadmap-settle.mjs` · exit 0 · `verdict: PASS · violations: 0` (110 unit docs); `bun scripts/check-skill-context.mjs` and `--routes` · exit 0 (28 skills, 14 routes); `bun scripts/unit-lineage.mjs --unit 316-roadmap-settle` · exit 0 · `LINEAGE OK — lane-era · obligations closed (6)`; `roadmap check docs/fix/316-roadmap-settle/SPEC.md` · exit 0 · PASS. + +## Next + +The receipt is **stale**: the audit pass that produced findings F7–F12 moved the +head past the `REVIEW-PASS` marker (`sha=e6251202…`), and the repair commits +below moved it again. `bun scripts/review-receipt.mjs verify --pr 323` answers +`stale` at this head, so the closing hand-off is +`/review-change 316-roadmap-settle`, then `/audit-pr 323`. + +The blockers that pass cleared, so the next audit re-checks rather than +re-discovers: F7 (rebase onto `main`), F8 (the obligations ledger, plus the +`subsectionBody` section-boundary defect it exposed), F9 (the push that produces +the CI run), F10 (`Closes #316` on the PR body), F11 (this section and the +cycle-3 verdict block), F12 (the writer's call sites, the single settle grader, +the `--root` usage path). + +## References + +- Issue: [#316](https://github.com/gtrabanco/agentic-workflow/issues/316) + — the PR closes it via `Closes #316` in the PR #323 body. +- PR: [#323](https://github.com/gtrabanco/agentic-workflow/pull/323) +- Related (out of scope here): #227 (composer), #260 (verifier), #301 (repair receiver) +- `skills/audit-pr/references/02_CLOSURE_AND_SCOPE_GATES.md` — the settle's authority source +- `skills/pre-execution-review/references/SNAPSHOT.md` — the settle's detector side +- `docs/workflow/LANE_FLOW.md` — the crate API table the call sites cite + +## Branch + +`fix-316-roadmap-writer-call-sites` — the branch already existed (work started +before this doc); the template's `fix/316-roadmap-settle` shape was not used. + +## Depends on + +none + +## Regression scope + +| Scenario | Verified by | +|---|---| +| The crate's `roadmap rowUpsert`/`annotate` writer keeps its existing grammar and cell-preservation behaviour | `packages/agentic-workflow/test/edit.test.mjs`, `packages/agentic-workflow/test/roadmap-check.test.mjs` | +| `unit-cycle`'s existing state machine keeps every prior action and ordering | `bun test scripts/unit-cycle.test.mjs` | +| The repo's own roadmap tables keep passing the promise-coherence guard | `node --test scripts/*.test.mjs` (includes `promise-coherence` cases) | +| Skill context budgets stay within their enforced ceilings after the SKILL.md edits | `bun scripts/check-skill-context.mjs` | diff --git a/docs/fix/316-roadmap-settle/decisions.md b/docs/fix/316-roadmap-settle/decisions.md new file mode 100644 index 000000000..96a63248b --- /dev/null +++ b/docs/fix/316-roadmap-settle/decisions.md @@ -0,0 +1,27 @@ +# decisions — fix/316-roadmap-settle + +Append-only `open-questions@1` record. One entry per question asked, with its +resolution. + +## Decisions + +| id | date | question | resolution | +|---|---|---|---| +| D-Q1 | 2026-10-26 | Which tree should unit #316's doc live in? The invocation was `--from-issue #316` (feature tree) but the issue carries `bug` + `fix-next`, the branch is `fix-316-roadmap-writer-call-sites`, and `scripts/unit-route.mjs` resolves neither index for 316 — the typed form could not be validated against the router. | **Owner answered** (`ask 00e3cc04-0105-43d1-9569-691a07eee23a`): **fix tree** — `docs/fix/316-roadmap-settle/`, registered in `docs/fix/README.md`. Mirrored into the unit doc's Log as D1. | +| D-Q2 | 2026-10-26 | Diff guard reports BREACH (1101 lines / 30 files vs 400/8) for the whole-unit diff. Shrink, split, or exception? | **Exception recorded** (unit doc Log D2): four-layer slice mandated by S1–S4; work was already committed in four scoped commits before the doc existed; nothing was deleted to shrink the diff. Precedent: unit 78's decisions.md D2. | + +none — no other questions were asked; every other lane input (objective, scope, +non-goals, ACs) cites the issue text verbatim (`SPEC.md` `## Specs` quotes). + +## Path-protection records + +The three settle suites are `**/*.test.*` surfaces (`test-file`, frozen). The two +created by this unit carry their declaration here; the third (`scripts/unit-cycle.test.mjs`) +exists on `main` and is a modification, not a creation. + +```text +path-protection-records@1 +kind | paths | phase | date | authority | justification +justification | scripts/roadmap-settle.test.mjs | P2 | 2026-10-27 | execute-phase | S2's offline gate is verified by this suite; the #316 repair adds the `--root`-without-value usage case (review F5) to the file the gate already owns +justification | packages/agentic-workflow/test/roadmap-check.test.mjs | P1 | 2026-10-27 | execute-phase | S4's crate check and its exit contract are pinned here; the #316 repair adds the bare-subcommand usage case that stops the writer resolving a subcommand word as a table path (review F4) +``` diff --git a/docs/fix/README.md b/docs/fix/README.md index e17408cb1..fb2f90ee0 100644 --- a/docs/fix/README.md +++ b/docs/fix/README.md @@ -14,7 +14,7 @@ table — history lives in git log + closed issues. | Issue | Topic | Status | Notes | |---|---|---|---| -| — | — | `none` | No active fixes — every merged entry was removed; history lives in git log + closed issues. | +| [#316](https://github.com/gtrabanco/agentic-workflow/issues/316) | roadmap-settle | `done` · [#323](https://github.com/gtrabanco/agentic-workflow/pull/323) · [SPEC](316-roadmap-settle/SPEC.md) | The roadmap row's `done` settle is code: the crate's `roadmap check` cross-document check (closed codes `row-missing`/`slug-mismatch`/`pr-url-unsettled`), the offline root gate `scripts/roadmap-settle.mjs` with the 77/79 frozen-failure corpus, `unit-cycle` writes the settle itself via `rowUpsert`, and the writer's call sites land in `unit-lane` (RELEASE/PLAN) + `execute-phase` close-out. Out of scope: #227 composer, #260 verifier, #301 receiver. Depends on none. | Historical artifacts remain under `docs/fix/`; merged and closed work is intentionally absent from this index. diff --git a/docs/workflow/LANE_FLOW.md b/docs/workflow/LANE_FLOW.md index 948d664d4..13862c991 100644 --- a/docs/workflow/LANE_FLOW.md +++ b/docs/workflow/LANE_FLOW.md @@ -253,6 +253,7 @@ The sensor also emits `next.candidate_count` (candidates at that priority level) | `agentic-workflow unit-doc progress_logEntry` | Add progress entry (SDK) | | `agentic-workflow unit-doc validate` | Validate the doc schema (SDK) | | `agentic-workflow roadmap rowUpsert` | Upsert a roadmap row (SDK) | +| `agentic-workflow roadmap check` | Cross-document settle check: a unit doc that carries a PR URL must have its row `done · [#N]()` — named codes, exit 1 fail-closed (issue #316) | | `agentic-workflow changelog rowAdd` | Add a changelog row (SDK) | | `agentic-workflow budgets ceilingRebase` | Rebase budget ceiling (SDK) | | `agentic-workflow manifest skillAdd` / `skillRemove` | Update manifest (SDK) | diff --git a/docs/workflow/SKILL_CONTEXT_BUDGETS.json b/docs/workflow/SKILL_CONTEXT_BUDGETS.json index f3d338c77..54cec9aae 100644 --- a/docs/workflow/SKILL_CONTEXT_BUDGETS.json +++ b/docs/workflow/SKILL_CONTEXT_BUDGETS.json @@ -90,12 +90,13 @@ } }, "execute-phase": { - "mainEstimateMax": 3717, - "mainLinesMax": 303, + "mainEstimateMax": 4119, + "mainLinesMax": 324, "mainSources": { "issue #291 / unit 73 P4": "the executor's mirror of the lane's per-phase hand-off contract (closing hand-off names one phase + the catalog tier, whole-unit mode demoted to explicit operator choice, lane resume after the last phase, Progress entry names the phase id) grew SKILL.md to 2929 est; ceiling re-based to ceil(2929 x 1.10) = 3222; the golden-fixture-driven two-block closing contract (weak-model smoke caught a last-phase P naming a nonexistent phase) then measured 253 lines / 2953 est — line ceiling ceil(253 x 1.10) = 279", "fix 258 / #258 harden-needs-input": "the needs-input contract moved to code: the hand-written guard is replaced by the three-tier dispatch and a pointer to scripts/needs-input.mjs, and `re-ask once`/`Confirmed phase`/`Confirmed file targets` are gone (F33-D/F34-D) — measured 3379 est / 275 lines; ceiling re-based to ceil(3379 x 1.10) = 3717 / ceil(275 x 1.10) = 303 (down from 3807/315, the body shrank)", - "feature 66 P2 (memory boundary)": "the executor's phase-transition memory boundary: one turn-contract box plus a Post-step item pointing at skills/unit-lane/references/MEMORY.md (never restated); measured 3548 est / 285 lines against the existing 3717 / 303 ceilings, so no re-base was needed" + "feature 66 P2 (memory boundary)": "the executor's phase-transition memory boundary: one turn-contract box plus a Post-step item pointing at skills/unit-lane/references/MEMORY.md (never restated); measured 3548 est / 285 lines against the existing 3717 / 303 ceilings, so no re-base was needed", + "issue #316 (roadmap settle in the close-out)": "the whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs` (issue #316); measured 3744 est / 294 lines, so the ceiling re-bases to ceil(3744 x 1.10) = 4119 and the line ceiling to ceil(294 x 1.10) = 324" }, "referenceEstimateMax": 3232, "referenceSources": { @@ -184,8 +185,8 @@ ] }, "templates": [], - "routeEstimateMax": 13391, - "routeLinesMax": 1023, + "routeEstimateMax": 13496, + "routeLinesMax": 1030, "sources": { "issue #291 / unit 73 P4": "the executor's mirror of the per-phase hand-off contract (closing hand-off names one phase + the catalog tier, whole-unit mode demoted to explicit operator choice, lane resume after the last phase, Progress entry names the phase id) grew execute-phase/SKILL.md; route measured 843 lines at the first re-basis, 856 after the golden-fixture-driven two-block closing contract (the weak-model smoke caught a last-phase P that names a nonexistent phase); ceiling = ceil(856 x 1.10) = 942", "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", @@ -196,7 +197,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 12173 est / 930 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(12173 x 1.10) = 13391 / ceil(930 x 1.10) = 1023" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 12173 est / 930 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(12173 x 1.10) = 13391 / ceil(930 x 1.10) = 1023", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 12259 est / 935 lines, ceiling re-based to ceil(12259 x 1.10) = 13485 / ceil(935 x 1.10) = 1029", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 12269, lines 936; ceiling = ceil(measured x 1.10)" } }, "execute-phase:feature": { @@ -213,8 +216,8 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 974, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "issue #291 / unit 73 P4": "the executor's per-phase hand-off contract plus its golden-fixture-driven two-block closing contract grew execute-phase/SKILL.md; route measured 811 lines at this re-basis; ceiling = ceil(811 x 1.10) = 893", "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", @@ -224,7 +227,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "execute-phase:final-pr": { @@ -242,8 +247,8 @@ ] }, "templates": [], - "routeEstimateMax": 13240, - "routeLinesMax": 1000, + "routeEstimateMax": 13346, + "routeLinesMax": 1007, "sources": { "issue #291 / unit 73 P4": "the executor's mirror of the per-phase hand-off contract (closing hand-off names one phase + the catalog tier, whole-unit mode demoted to explicit operator choice, lane resume after the last phase, Progress entry names the phase id) grew execute-phase/SKILL.md; route measured 822 lines at the first re-basis, 835 after the golden-fixture-driven two-block closing contract (the weak-model smoke caught a last-phase P that names a nonexistent phase); ceiling = ceil(835 x 1.10) = 919", "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", @@ -254,7 +259,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 12036 est / 909 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(12036 x 1.10) = 13240 / ceil(909 x 1.10) = 1000" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 12036 est / 909 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(12036 x 1.10) = 13240 / ceil(909 x 1.10) = 1000", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 12122 est / 914 lines, ceiling re-based to ceil(12122 x 1.10) = 13335 / ceil(914 x 1.10) = 1006", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 12132, lines 915; ceiling = ceil(measured x 1.10)" } }, "execute-phase:finding": { @@ -271,8 +278,8 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 974, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", "feature 32 P3/P4": "feature 32 P3/P4: the unit's skill-contract text (canonical severity table in CLASSIFY.md, gate-ran@1 in LEDGERS.md, sole-flipper/triage/roadmap/NRS prose) grew this route's loaded references. Ceiling re-based to ceil(measured x 1.10).", @@ -281,7 +288,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "execute-phase:fix": { @@ -298,8 +307,8 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 978, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "issue #291 / unit 73 P4": "the executor's per-phase hand-off contract plus its golden-fixture-driven two-block closing contract grew execute-phase/SKILL.md; route measured 811 lines at this re-basis; ceiling = ceil(811 x 1.10) = 893", "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", @@ -308,7 +317,9 @@ "unit 36 / feature 36": "feature 36 fold: F14 migration of ACCEPTANCE.md mandate across skills/verification-contract/SKILL.md (loaded by this route; also touched review-pack skills, none under execute-phase/); route measured 10456 est / 823 lines; ceiling = ceil(10456 x 1.10) = 11502 est, ceil(823 x 1.10) = 906 lines", "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", - "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes and FOLDING's close rule now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows, re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10), reason declared." + "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes and FOLDING's close rule now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows, re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10), reason declared.", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "execute-phase:legacy": { @@ -325,8 +336,8 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 974, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "issue #291 / unit 73 P4": "the executor's mirror of the per-phase hand-off contract (closing hand-off names one phase + the catalog tier, whole-unit mode demoted to explicit operator choice, lane resume after the last phase, Progress entry names the phase id) grew execute-phase/SKILL.md; route measured 798 lines at the first re-basis, 811 after the golden-fixture-driven two-block closing contract (the weak-model smoke caught a last-phase P that names a nonexistent phase); ceiling = ceil(811 x 1.10) = 893", "feature 32 P3/P4": "feature 32 P3/P4: the unit's skill-contract text (canonical severity table in CLASSIFY.md, gate-ran@1 in LEDGERS.md, sole-flipper/triage/roadmap/NRS prose) grew this route's loaded references. Ceiling re-based to ceil(measured x 1.10).", @@ -335,7 +346,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "execute-phase:small": { @@ -352,8 +365,8 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 974, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "issue #291 / unit 73 P4": "the executor's per-phase hand-off contract plus its golden-fixture-driven two-block closing contract grew execute-phase/SKILL.md; route measured 811 lines at this re-basis; ceiling = ceil(811 x 1.10) = 893", "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", @@ -363,7 +376,9 @@ "fix 258 / #258 harden-needs-input": "the needs-input contract moved into code (fix 258 P10–P11): the hand-written guard section was replaced by one named-policy section carrying the three-tier ladder and the pointer to scripts/needs-input.mjs, and the \"confirmed phase\"/\"confirmed file targets\" bullets were deleted (owner decisions F33-D/F34-D); ceiling re-based to ceil(measured x 1.10)", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "execute-phase:unit-loop": { @@ -380,15 +395,17 @@ ] }, "templates": [], - "routeEstimateMax": 12676, - "routeLinesMax": 974, + "routeEstimateMax": 12781, + "routeLinesMax": 981, "sources": { "workflow-gap audit 2026-09": "research-step cutoff rules (evidence-grounding 1.7.0) and tests-first red-run evidence (execute-phase 4.8.0) grew the shared references these routes load; ceiling re-based to ceil(measured x 1.10).", "feature 32 P3/P4": "feature 32 P3/P4: the unit's skill-contract text (canonical severity table in CLASSIFY.md, gate-ran@1 in LEDGERS.md, sole-flipper/triage/roadmap/NRS prose) grew this route's loaded references. Ceiling re-based to ceil(measured x 1.10).", "feature 60 P3": "path-protection checkpoint adoption: skills/verification-contract/SKILL.md grew to 1459 est / 116 lines; pre-re-basis route measured 10456 est / 823 lines; ceiling re-based to ceil(10456 x 1.10) = 11502 est, growth source skills/verification-contract/SKILL.md", "feature 36 x fix/258 merge re-basis (PR #304)": "main-into-branch merge (PR #304): the rebase onto origin/main unions this branch's verification-contract / ACCEPTANCE-mandate growth with main's fix/258 needs-input contract, so every execute-phase:* route loads both and measures above either parent's ceiling. Route re-measured at this re-basis; estimate and line ceilings re-set to ceil(measured x 1.10). Growth sources: feature 36 (verification-contract 1.4.2 validator-determinism + ACCEPTANCE.md mandate) and fix/258 harden-needs-input, with feature 60 P3 path-protection checkpoint adoption already on main.", "issue #308 (unit 79 review fold)": "issue #308 (unit 79 review fold, F11): the executor's Step 0 readiness, its Evidence-row gate, EXECUTION_CONTRACT's completion boxes, FOLDING's close rule and verification-contract's receipt sentence now name both accepted unit-doc shapes (legacy `## Evidence`/`## Progress log` rows; re-cut `## Log` E/G/P lines), because the shipped readiness check stopped every unit created from the re-cut template. Ceilings re-based to ceil(measured × 1.10) from the checker's own measured route, reason declared.", - "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974" + "PR #311 merge (feature 79 x feature 66)": "the spec-by-reference unit-document re-cut (issue #308, feature 79) and feature 66's memory-boundary box met in PR #311, so this route measures 11523 est / 885 lines on the shipped tree and the ceilings below the declared 10% headroom are re-set to ceil(11523 x 1.10) = 12676 / ceil(885 x 1.10) = 974", + "issue #316 (roadmap settle in the close-out)": "issue #316 (roadmap settle in the close-out): SKILL.md's whole-unit close-out gains the deterministic roadmap settle — `agentic-workflow roadmap rowUpsert` writing `done · [#N]()` with the other cells preserved, validated by `bun scripts/roadmap-settle.mjs`; measured 11609 est / 890 lines, ceiling re-based to ceil(11609 x 1.10) = 12770 / ceil(890 x 1.10) = 979", + "fix #316 (roadmap settle call sites)": "the whole-unit close-out names the executable settle form and its kind boundary (review F4: a bare `roadmap rowUpsert` resolved the subcommand word as the table path); route measured estimate 11619, lines 891; ceiling = ceil(measured x 1.10)" } }, "review-change:adversarial": { diff --git a/packages/agentic-workflow/bin/agentic-workflow.mjs b/packages/agentic-workflow/bin/agentic-workflow.mjs index a4c98f579..230de4113 100644 --- a/packages/agentic-workflow/bin/agentic-workflow.mjs +++ b/packages/agentic-workflow/bin/agentic-workflow.mjs @@ -21,12 +21,12 @@ * that blocks the caller (feature 66, AC2). */ -import { readFileSync } from "node:fs"; -import { join, resolve, basename } from "node:path"; +import { readFileSync, existsSync } from "node:fs"; +import { join, resolve, basename, dirname } from "node:path"; // Import SDK modules import { create, setSection, evidence_addRow, progress_logEntry, validate } from "../src/edit/UnitDoc.mjs"; -import { rowUpsert, annotate as roadmapAnnotate } from "../src/edit/Roadmap.mjs"; +import { rowUpsert, annotate as roadmapAnnotate, checkUnitDoc, ROADMAP_CHECK_CODES } from "../src/edit/Roadmap.mjs"; import { rowAdd } from "../src/edit/Changelog.mjs"; import { ceilingRebase } from "../src/edit/Budgets.mjs"; import { skillAdd, skillRemove } from "../src/edit/Manifest.mjs"; @@ -68,6 +68,21 @@ function refuse(reason) { process.exit(2); } +/** + * Walk up from a unit doc to the repo root until the default index (e.g. + * `docs/features/ROADMAP.md`) exists; null when no ancestor carries it. + */ +function findUpIndex(docPath, relativeIndex) { + let dir = dirname(docPath); + for (;;) { + const candidate = join(dir, relativeIndex); + if (existsSync(candidate)) return candidate; + const parent = dirname(dir); + if (parent === dir) return null; + dir = parent; + } +} + // ── Router ─────────────────────────────────────────────────────────────── const command = process.argv[2]; @@ -210,6 +225,14 @@ switch (command) { // ── roadmap ────────────────────────────────────────────────────────── case "roadmap": { + // `rowUpsert`/`annotate` take a table path first: a bare first positional is + // a missing path, never a filename. Resolving the subcommand word would make + // `roadmap rowUpsert …` open a file literally named `rowUpsert` and refuse + // with ENOENT, which reads as a missing table instead of a usage error. + if (rawArgs[0] === "rowUpsert" || rawArgs[0] === "annotate") { + process.stderr.write(`usage: agentic-workflow roadmap ${rawArgs[0]} \n`); + process.exit(2); + } const filePath = rawArgs[0] ? resolve(rawArgs[0]) : join(process.cwd(), "ROADMAP.md"); if (rawArgs[0] === "rowUpsert" || (rawArgs[0] && rawArgs[1] === "rowUpsert")) { @@ -232,6 +255,60 @@ switch (command) { } catch (e) { refuse(e.message); } + } else if (rawArgs[0] === "check" || (rawArgs[0] && rawArgs[1] === "check")) { + // roadmap check [--roadmap ] [--index ] [--json] + // Cross-document check (issue #316): the unit doc's own facts (NN, + // slug, PR URL) against its index row. Exit contract: 0 consistent, + // 1 violations (named codes), 2 usage. + const docArg = rawArgs[1] === "check" ? rawArgs[2] : rawArgs[1]; + if (!docArg) { + process.stderr.write("usage: agentic-workflow roadmap check [--roadmap ] [--index ] [--json]\n"); + process.exit(2); + } + const flag = (name) => { + const i = restArgs.indexOf(name); + return i !== -1 ? restArgs[i + 1] : undefined; + }; + try { + const docPath = resolve(docArg); + const docText = readFileSync(docPath, "utf8"); + // Kind and index defaults follow the doc's own tree position: + // docs/features → the roadmap, docs/fix → the fix index (its rows are + // removed once merged, so a missing row there is convention, not + // contradiction). + const parts = docPath.split("/"); + const isFix = parts.includes("docs") && parts[parts.indexOf("docs") + 1] === "fix"; + const defaultIndex = isFix ? "docs/fix/README.md" : "docs/features/ROADMAP.md"; + const indexArg = flag("--roadmap") ?? flag("--index"); + const indexPath = indexArg + ? resolve(indexArg) + : findUpIndex(docPath, defaultIndex.split("/").pop()); + if (!indexPath) { + refuse(`index not found for ${docArg} — pass --roadmap `); + } + const dirName = basename(dirname(docPath)); + const nn = /^(\d+)/.exec(dirName)?.[1] ?? ""; + const slug = dirName.replace(/^\d+-/, ""); + const result = checkUnitDoc({ nn, slug, kind: isFix ? "fix" : "feature", docText, indexText: readFileSync(indexPath, "utf8") }); + if (parsed.json) { + console.log( + JSON.stringify( + { service: "roadmap", op: "check", path: docPath, schema: "roadmap-table@1", codes: ROADMAP_CHECK_CODES, ok: result.ok, violations: result.violations }, + null, + 2, + ), + ); + } else { + for (const v of result.violations) { + console.log(`roadmap-check: ${docPath}:${v.line} ${v.code} — ${v.detail}`); + } + console.log(`verdict: ${result.ok ? "PASS" : "FAIL"}`); + console.log(`violations: ${result.violations.length}`); + } + process.exit(result.ok ? 0 : 1); + } catch (e) { + refuse(e.message); + } } else { refuse(`Unknown roadmap operation`); } diff --git a/packages/agentic-workflow/package.json b/packages/agentic-workflow/package.json index 005951282..bfc2b4af0 100644 --- a/packages/agentic-workflow/package.json +++ b/packages/agentic-workflow/package.json @@ -1,6 +1,6 @@ { "name": "@gtrabanco/agentic-workflow", - "version": "0.4.0", + "version": "0.5.0", "description": "Deterministic runner for the agentic-workflow lane: file services (agwo edit), sensors, guards and receipts. One pinned pure-JS dependency, no native deps, no build step.", "license": "MIT", "author": "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>", diff --git a/packages/agentic-workflow/src/edit/Roadmap.mjs b/packages/agentic-workflow/src/edit/Roadmap.mjs index 44c1a49d2..5a52cd966 100644 --- a/packages/agentic-workflow/src/edit/Roadmap.mjs +++ b/packages/agentic-workflow/src/edit/Roadmap.mjs @@ -10,6 +10,17 @@ import { readFileSync, writeFileSync } from "node:fs"; import { createHash } from "node:crypto"; +/** + * The separator rule matches the table that exists in this repository + * (`|----|------|…`), not only the literal `|---|` — issue #316 found the + * writer refusing the repo's own roadmap on the old literal match. + */ +const SEPARATOR_RE = /^\s*\|[-: |]+\|?\s*$/; + +function isSeparatorLine(line) { + return SEPARATOR_RE.test(line) && (line.match(/-/g) || []).length >= 3; +} + // ── Helpers ────────────────────────────────────────────────────────────── function sha256(text) { @@ -54,8 +65,8 @@ export function rowUpsert(table, { nn, slug, status, dependsOn, description }) { throw new Error("Roadmap table header not found (expected line with | NN |)"); } - // Find the separator line (contains "|---|") - const separatorLine = content.split("\n").find((l) => l.includes("|---|")); + // Find the separator line (a rule of pipes, dashes, colons and spaces) + const separatorLine = content.split("\n").find(isSeparatorLine); if (!separatorLine) { throw new Error("Roadmap table separator not found (expected line with |---|)"); } @@ -101,6 +112,128 @@ export function rowUpsert(table, { nn, slug, status, dependsOn, description }) { ); } +/** + * findRow(table, nn) — the full cell parse of one row, for callers that need + * to re-emit the row through rowUpsert without clobbering the cells they do + * not touch (the settle write: status changes, everything else preserved). + * + * Returns { nn, slug, status, dependsOn, description, line } or null when the + * row is absent. slug keeps the raw cell (backticks included); callers that + * compare slugs strip them. + */ +export function findRow(table, nn) { + const content = readFileSync(table, "utf8"); + const line = content.split("\n").find((l) => l.trim().startsWith(`| ${nn} |`) && /^\| (\d+) \|/.test(l.trim())); + if (!line) return null; + const cells = line.split("|").map((c) => c.trim()); + return { + nn: cells[1] ?? "", + slug: cells[2] ?? "", + status: cells[3] ?? "", + dependsOn: cells[4] ?? "", + description: cells[5] ?? "", + line, + }; +} + +/** + * The closed vocabulary of `roadmap check`. Adding a code is a change to this + * constant and its tests, never a new string somewhere else. + */ +export const ROADMAP_CHECK_CODES = Object.freeze(["row-missing", "slug-mismatch", "pr-url-unsettled"]); + +/** First cell's unit number: roadmap rows carry it bare, fix-index rows link it. */ +function rowNumber(cell) { + if (/^\d+$/.test(cell)) return cell; + const m = /issues\/(\d+)|#(\d+)/.exec(cell); + return m ? m[1] ?? m[2] : null; +} + +function parseIndexRows(indexText) { + const rows = new Map(); + for (const line of indexText.split("\n")) { + if (!line.startsWith("|")) continue; + const cells = line.split("|").map((c) => c.trim()); + if (cells.length < 4) continue; + const nn = rowNumber(cells[1] ?? ""); + if (!nn) continue; + rows.set(nn, { status: cells[3] ?? "", slug: (cells[2] ?? "").replace(/`/g, "") }); + } + return rows; +} + +/** The status cell's bare token: the decoration (` · [#N](url)`) never carries it. */ +function statusToken(cell) { + return cell.replace(/[`*]/g, "").split("·")[0].trim(); +} + +/** + * PR URLs on the doc's own lifecycle surfaces: the `## Next`, `## Log` and + * `## Evidence` sections and the References `- PR:` line the release step + * writes. URLs quoted elsewhere (cross-references in References prose) are + * other units' PRs and are never treated as this doc's own. + */ +function ownPrUrls(docText) { + const urls = []; + let section = null; + docText.split("\n").forEach((line, i) => { + const heading = /^##\s+(.*)/.exec(line); + if (heading) section = heading[1].trim(); + const isReferencePr = /^-\s*PR:/.test(line); + const own = (section && /^(Next|Log|Evidence)\b/i.test(section)) || isReferencePr; + if (!own) return; + for (const m of line.matchAll(/https?:\/\/\S*\/pull\/(\d+)/g)) { + urls.push({ + pr: Number(m[1]), + url: m[0].replace(/[`),.]+$/, ""), + line: i + 1, + where: isReferencePr ? "References" : section, + }); + } + }); + return urls; +} + +/** + * checkUnitDoc({ nn, slug, kind, docText, indexText }) — the cross-document + * comparison between a unit doc's own facts and its index row (issue #316). + * + * kind is "feature" (roadmap-indexed) or "fix" (fix-index rows are removed + * once merged, so an absent row is that index's own convention, not a + * contradiction). Returns { ok, violations } where every violation carries a + * code from ROADMAP_CHECK_CODES, the doc line it was detected on and a detail + * naming the expected settle form `done · [#N]()`. + */ +export function checkUnitDoc({ nn, slug, kind, docText, indexText }) { + const violations = []; + const row = parseIndexRows(indexText).get(String(nn)); + if (!row) { + if (kind !== "fix") { + violations.push({ code: "row-missing", line: 1, detail: `no roadmap row for NN ${nn}` }); + } + return { ok: violations.length === 0, violations }; + } + if (slug && row.slug && row.slug !== slug) { + violations.push({ code: "slug-mismatch", line: 1, detail: `doc folder says '${slug}', row says '${row.slug}'` }); + } + for (const { pr, url, line, where } of ownPrUrls(docText)) { + const token = statusToken(row.status); + // The settle link is backtick-tolerant: the fix index decorates rows as + // `done` · [`#302`](…), the roadmap as done · [#212](…). Both are the same + // settle; only the exact URL is compared, never the decoration. + const settled = + token === "done" && + new RegExp(`\\[\`?#${pr}\`?\\]\\(${url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\)`).test(row.status); + if (settled) continue; + violations.push({ + code: "pr-url-unsettled", + line, + detail: `doc carries its PR URL (${where}); row status is '${row.status}' — expected 'done · [#${pr}](${url})'`, + }); + } + return { ok: violations.length === 0, violations }; +} + /** * annotate(nn, note) — appends a note to the description cell of the row * identified by nn. The note is appended with a newline and "—" prefix. diff --git a/packages/agentic-workflow/test/roadmap-check.test.mjs b/packages/agentic-workflow/test/roadmap-check.test.mjs new file mode 100644 index 000000000..8e326b879 --- /dev/null +++ b/packages/agentic-workflow/test/roadmap-check.test.mjs @@ -0,0 +1,258 @@ +/** + * roadmap-check.test.mjs — the cross-document check between a unit doc's own + * facts (NN, slug, PR URL) and its index row (issue #316's frozen-failure corpus). + * + * Reproducers come from git history: + * unit 77 `promise-coherence-guard` — PR #309 merged 2026-10-21, the roadmap + * row still read `in-progress` on `main` (caught at audit-pr, after the + * review pass was already paid); + * unit 79 `spec-by-reference` — seven phases logged, PR #311 open, the row + * still `in-progress` until the audit blocked on it. + * + * Both live cases carry the URL in-doc (77 in `## Next`, 79 in an Evidence + * row), so the contradiction is offline-detectable — that is exactly what + * `checkUnitDoc` detects, with the closed code set in `ROADMAP_CHECK_CODES`. + */ + +import { describe, it, before, after } from "node:test"; +import { strictEqual, ok } from "node:assert"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +import { checkUnitDoc, findRow, ROADMAP_CHECK_CODES, rowUpsert } from "../src/edit/Roadmap.mjs"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const CLI = join(ROOT, "bin", "agentic-workflow.mjs"); + +const OWNER = "https://github.com/gtrabanco/agentic-workflow"; + +// The separator/header shapes are the repository's real ones (unit #316 found +// the writer refusing `|----|------|…` because it matched only the literal +// `|---|` — the check and the writer must accept the table that exists). +const REAL_SEPARATOR = "|----|------|--------|------------|---------|"; +const roadmapTable = (rows) => + ["# Roadmap", "", "## Features", "", "| NN | Slug | Status | Depends on | Summary |", REAL_SEPARATOR, ...rows, ""].join("\n"); + +const fixIndexTable = (rows) => + ["# Active fixes", "", "| Issue | Topic | Status | Notes |", "|---|---|---|---|", ...rows, ""].join("\n"); + +let tmpDir; + +before(() => { + tmpDir = mkdtempSync(join(tmpdir(), "agentic-workflow-roadmap-check-")); +}); + +after(() => { + if (tmpDir && existsSync(tmpDir)) rmSync(tmpDir, { recursive: true, force: true }); +}); + +const write = (relative, content) => { + const p = join(tmpDir, relative); + mkdirSync(dirname(p), { recursive: true }); + writeFileSync(p, content, "utf8"); + return p; +}; + +describe("checkUnitDoc — the frozen-failure corpus (issue #316)", () => { + it("REPRODUCER unit 77: doc carries its PR URL in ## Next while the row reads in-progress", () => { + // Verbatim shape of docs/features/77-promise-coherence-guard/SPEC.md:274 + // against the row state main carried while PR #309 was merged. + const docText = [ + "# 77 — promise-coherence-guard", + "## Next", + "", + "Owner review of PR #309 (`" + OWNER + "/pull/309`); the", + "external verifier decides, and no self-approval follows from a green suite.", + "", + "## References", + "", + "Issue `#305`.", + ].join("\n"); + const indexText = roadmapTable(["| 77 | `promise-coherence-guard` | in-progress | — | Issue [#305](…) (M) |"]); + const result = checkUnitDoc({ nn: 77, slug: "promise-coherence-guard", kind: "feature", docText, indexText }); + strictEqual(result.ok, false); + strictEqual(result.violations.length, 1); + strictEqual(result.violations[0].code, "pr-url-unsettled"); + ok(result.violations[0].detail.includes("Next")); + ok(result.violations[0].detail.includes(OWNER + "/pull/309")); + }); + + it("REPRODUCER unit 79: doc carries its PR URL in an Evidence row while the row reads in-progress", () => { + // Verbatim shape of 79's Evidence row (line 378) against the row state at + // filing time (`in-progress` until the audit blocked on it). + const docText = [ + "# 79 — spec-by-reference", + "## Evidence", + "", + "| — | PR opened with `Closes #308` | exit 0 | " + OWNER + "/pull/311 | this session, 2026-10-21 |", + ].join("\n"); + const indexText = roadmapTable(["| 79 | `spec-by-reference` | in-progress | — (soft: 303 305, both merged) | Issue [#308](…) (L) |"]); + const result = checkUnitDoc({ nn: 79, slug: "spec-by-reference", kind: "feature", docText, indexText }); + strictEqual(result.ok, false); + strictEqual(result.violations[0].code, "pr-url-unsettled"); + }); + + it("both reproducers pass once the row reads done · [#N](same url)", () => { + const doc77 = "# 77 — x\n## Next\n\nOwner review of PR #309 (`" + OWNER + "/pull/309`).\n"; + const doc79 = "# 79 — x\n## Evidence\n\n| — | PR opened | exit 0 | " + OWNER + "/pull/311 | x |"; + const settled = roadmapTable([ + "| 77 | `promise-coherence-guard` | done · [#309](" + OWNER + "/pull/309) | — | Issue [#305](…) |", + "| 79 | `spec-by-reference` | done · [#311](" + OWNER + "/pull/311) | — | Issue [#308](…) |", + ]); + strictEqual(checkUnitDoc({ nn: 77, slug: "promise-coherence-guard", kind: "feature", docText: doc77, indexText: settled }).ok, true); + strictEqual(checkUnitDoc({ nn: 79, slug: "spec-by-reference", kind: "feature", docText: doc79, indexText: settled }).ok, true); + }); + + it("a settle link with backticked decoration or trailing punctuation still matches", () => { + const docText = "# 40 — x\n## Log\n\nL1 — PR #235 → " + OWNER + "/pull/235."; + const indexText = roadmapTable(["| 40 | `versioned-skills-releases` | done · [`#235`](" + OWNER + "/pull/235) | — | Issue [#180](…) |"]); + const result = checkUnitDoc({ nn: 40, slug: "versioned-skills-releases", kind: "feature", docText, indexText }); + strictEqual(result.ok, true, JSON.stringify(result.violations)); + }); +}); + +describe("checkUnitDoc — no false positives on cross-references and conventions", () => { + it("a doc citing another unit's settled PR passes (fix 272 → #276 shape)", () => { + // Verbatim shape of docs/fix/272-…/SPEC.md:347: it cites PR #276, which the + // fix index settles on the unit's own row. + const docText = "# 272 — x\n## Log\n\nL9 — phase-lint fix landed via " + OWNER + "/pull/276.\n"; + const indexText = fixIndexTable([ + "| [#272](" + OWNER + "/issues/272) | phase-lint-unit-doc-grammar | `done` · [#276](" + OWNER + "/pull/276) · [SPEC](272-phase-lint-unit-doc-grammar/SPEC.md) | notes |", + ]); + strictEqual(checkUnitDoc({ nn: 272, slug: "phase-lint-unit-doc-grammar", kind: "fix", docText, indexText }).ok, true); + }); + + it("a fix doc with no index row passes — merged fixes are removed by the index's own convention", () => { + const docText = "# 99 — x\n## References\n\n- PR: " + OWNER + "/pull/241\n"; + strictEqual(checkUnitDoc({ nn: 99, slug: "x", kind: "fix", docText, indexText: fixIndexTable([]) }).ok, true); + }); + + it("an active fix whose doc carries its PR URL and whose row is not settled fails", () => { + const docText = "# 91 — x\n## Evidence\n\n| — | PR opened | exit 0 | " + OWNER + "/pull/303 | x |"; + const indexText = fixIndexTable(["| [#91](" + OWNER + "/issues/91) | toy-fix | `in-progress` | notes |"]); + const result = checkUnitDoc({ nn: 91, slug: "toy-fix", kind: "fix", docText, indexText }); + strictEqual(result.ok, false); + strictEqual(result.violations[0].code, "pr-url-unsettled"); + }); + + it("a doc with no PR URL of its own carries no settle obligation", () => { + const indexText = roadmapTable(["| 80 | `planned-unit` | planned | — | work |"]); + strictEqual(checkUnitDoc({ nn: 80, slug: "planned-unit", kind: "feature", docText: "# 80 — x\n## Tasks\n- P1 — work\n", indexText }).ok, true); + }); + + it("URLs outside the doc's own lifecycle surfaces are never treated as its PR", () => { + const docText = "# 80 — x\n## References\n\n- the corpus: docs/fix/258-… (reviewed in " + OWNER + "/pull/302)\n"; + const indexText = roadmapTable(["| 80 | `planned-unit` | planned | — | work |"]); + strictEqual(checkUnitDoc({ nn: 80, slug: "planned-unit", kind: "feature", docText, indexText }).ok, true); + }); +}); + +describe("checkUnitDoc — row identity", () => { + it("a feature doc whose NN has no roadmap row fails with row-missing", () => { + const result = checkUnitDoc({ nn: 81, slug: "rowless", kind: "feature", docText: "# 81 — x\n", indexText: roadmapTable([]) }); + strictEqual(result.ok, false); + strictEqual(result.violations[0].code, "row-missing"); + }); + + it("a doc whose folder slug disagrees with the row fails with slug-mismatch", () => { + // The F48 class: the renumber to 79 left unit-identity headings behind. + const indexText = roadmapTable(["| 79 | `spec-by-reference` | in-progress | — | work |"]); + const result = checkUnitDoc({ nn: 79, slug: "unit-78-leftover", kind: "feature", docText: "# 79 — x\n", indexText }); + strictEqual(result.ok, false); + strictEqual(result.violations[0].code, "slug-mismatch"); + }); + + it("a row settled `done` with a different URL than the doc's PR fails closed", () => { + const docText = "# 78 — x\n## Log\n\nL1 — PR #311 → " + OWNER + "/pull/311\n"; + const indexText = roadmapTable(["| 78 | `x` | done · [#999](" + OWNER + "/pull/999) | — | work |"]); + const result = checkUnitDoc({ nn: 78, slug: "x", kind: "feature", docText, indexText }); + strictEqual(result.ok, false); + strictEqual(result.violations[0].code, "pr-url-unsettled"); + }); + + it("ROADMAP_CHECK_CODES is the closed vocabulary and nothing else is emitted", () => { + ok(ROADMAP_CHECK_CODES.includes("row-missing")); + ok(ROADMAP_CHECK_CODES.includes("slug-mismatch")); + ok(ROADMAP_CHECK_CODES.includes("pr-url-unsettled")); + strictEqual(ROADMAP_CHECK_CODES.length, 3); + }); +}); + +describe("findRow", () => { + it("parses the real table shape's cells for the settle writer's callers", () => { + const p = write("findrow/ROADMAP.md", roadmapTable(["| 77 | `promise-coherence-guard` | in-progress | — | Issue [#305](…) (M) |"])); + const row = findRow(p, "77"); + strictEqual(row.slug, "`promise-coherence-guard`"); // raw cell — the settle writer re-emits it verbatim + strictEqual(row.slug.replace(/`/g, ""), "promise-coherence-guard"); + strictEqual(row.status, "in-progress"); + strictEqual(row.dependsOn, "—"); + ok(row.description.startsWith("Issue [#305]")); + strictEqual(findRow(p, "999"), null); + }); +}); + +describe("roadmap check — CLI exit contract", () => { + it("exit 0 when consistent, 1 with named codes, 2 on usage refusal", () => { + const docDir = join(tmpDir, "docs", "features", "77-cli-unit"); + mkdirSync(docDir, { recursive: true }); + writeFileSync(join(docDir, "SPEC.md"), "# 77 — cli-unit\n## Next\n\nPR #309 (`" + OWNER + "/pull/309`).\n", "utf8"); + const table = write("docs/features/ROADMAP.md", roadmapTable(["| 77 | `cli-unit` | in-progress | — | work |"])); + + const failing = spawnSync(process.execPath, [CLI, "roadmap", "check", join(docDir, "SPEC.md"), "--roadmap", table], { encoding: "utf8" }); + strictEqual(failing.status, 1); + ok(failing.stdout.includes("pr-url-unsettled"), failing.stdout); + ok(failing.stdout.includes("verdict: FAIL")); + + // The same table after the settle write the writer performs. + rowUpsert(table, { nn: "77", slug: "cli-unit", status: "done · [#309](" + OWNER + "/pull/309)", dependsOn: "—", description: "work" }); + const passing = spawnSync(process.execPath, [CLI, "roadmap", "check", join(docDir, "SPEC.md"), "--roadmap", table], { encoding: "utf8" }); + strictEqual(passing.status, 0); + ok(passing.stdout.includes("verdict: PASS")); + + const usage = spawnSync(process.execPath, [CLI, "roadmap", "check"], { encoding: "utf8" }); + strictEqual(usage.status, 2); + }); + + it("a bare rowUpsert is a usage error, never a table path (review F4)", () => { + // `roadmap rowUpsert …` used to resolve the subcommand word as the table + // path, so the writer refused with ENOENT on a file named `rowUpsert` — an + // unreadable-table error for what is a missing-argument mistake + // (issue #316). + const bare = spawnSync(process.execPath, [CLI, "roadmap", "rowUpsert", '{"nn":"77"}'], { encoding: "utf8", cwd: tmpDir }); + strictEqual(bare.status, 2); + ok(bare.stderr.includes("usage: agentic-workflow roadmap rowUpsert"), bare.stderr); + ok(!bare.stderr.includes("ENOENT"), bare.stderr); + strictEqual(existsSync(join(tmpDir, "rowUpsert")), false); + + const bareAnnotate = spawnSync(process.execPath, [CLI, "roadmap", "annotate", "77", "note"], { encoding: "utf8", cwd: tmpDir }); + strictEqual(bareAnnotate.status, 2); + }); + + it("--json emits the envelope with the same codes", () => { + const docDir = join(tmpDir, "docs", "features", "78-json-unit"); + mkdirSync(docDir, { recursive: true }); + writeFileSync(join(docDir, "SPEC.md"), "# 78 — json-unit\n## Next\n\nPR #311 (`" + OWNER + "/pull/311`).\n", "utf8"); + const table = write("docs/features/json-ROADMAP.md", roadmapTable(["| 78 | `json-unit` | in-progress | — | work |"])); + const r = spawnSync(process.execPath, [CLI, "roadmap", "check", join(docDir, "SPEC.md"), "--roadmap", table, "--json"], { encoding: "utf8" }); + strictEqual(r.status, 1); + const envelope = JSON.parse(r.stdout); + strictEqual(envelope.service, "roadmap"); + strictEqual(envelope.op, "check"); + strictEqual(envelope.ok, false); + strictEqual(envelope.violations[0].code, "pr-url-unsettled"); + }); +}); + +describe("previous behavior holds — the writer ops the call sites already name", () => { + it("roadmap rowUpsert still writes the real separator shape and emits the same receipt", () => { + const p = write("writer/ROADMAP.md", roadmapTable(["| 61 | `adaptive-unit-lane` | active | — | Adaptive unit lane implementation |"])); + const receipt = rowUpsert(p, { nn: "61", slug: "adaptive-unit-lane", status: "completed", dependsOn: "", description: "Updated description" }); + strictEqual(receipt.service, "roadmap"); + strictEqual(receipt.op, "rowUpsert"); + strictEqual(receipt.schema, "roadmap-table@1"); + strictEqual(receipt.ok, true); + }); +}); diff --git a/packages/pi-agentic-workflow/package.json b/packages/pi-agentic-workflow/package.json index 65cb2e730..ea8f8dd2b 100644 --- a/packages/pi-agentic-workflow/package.json +++ b/packages/pi-agentic-workflow/package.json @@ -1,6 +1,6 @@ { "name": "@gtrabanco/pi-agentic-workflow", - "version": "0.35.0", + "version": "0.35.1", "description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.", "keywords": [ "pi-package", diff --git a/scripts/fixtures/roadmap-settle/docs/features/40-settle-other/SPEC.md b/scripts/fixtures/roadmap-settle/docs/features/40-settle-other/SPEC.md new file mode 100644 index 000000000..8eb62ab23 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/features/40-settle-other/SPEC.md @@ -0,0 +1,5 @@ +# 40 — settle-other + +## Next + +Merged as https://github.com/gtrabanco/agentic-workflow/pull/302. diff --git a/scripts/fixtures/roadmap-settle/docs/features/77-settle-repro/SPEC.md b/scripts/fixtures/roadmap-settle/docs/features/77-settle-repro/SPEC.md new file mode 100644 index 000000000..61e647eb3 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/features/77-settle-repro/SPEC.md @@ -0,0 +1,10 @@ +# 77 — settle-repro + +## Next + +Owner review of PR #309 (`https://github.com/gtrabanco/agentic-workflow/pull/309`); the +external verifier decides. + +## References + +Issue `#305`; the PR closes it via `Closes #305`. diff --git a/scripts/fixtures/roadmap-settle/docs/features/78-settle-done/SPEC.md b/scripts/fixtures/roadmap-settle/docs/features/78-settle-done/SPEC.md new file mode 100644 index 000000000..838c77e22 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/features/78-settle-done/SPEC.md @@ -0,0 +1,9 @@ +# 78 — settle-done + +## Log + +`2026-10-21 — P7 — PR #311 opened with Closes #308 → https://github.com/gtrabanco/agentic-workflow/pull/311` + +## References + +- PR: https://github.com/gtrabanco/agentic-workflow/pull/311 diff --git a/scripts/fixtures/roadmap-settle/docs/features/80-settle-planned/SPEC.md b/scripts/fixtures/roadmap-settle/docs/features/80-settle-planned/SPEC.md new file mode 100644 index 000000000..5beb000a4 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/features/80-settle-planned/SPEC.md @@ -0,0 +1,10 @@ +# 80 — settle-planned + +## Tasks + +- P1 — the work (validator: `true` exits 0) + +## References + +The corpus was reviewed in https://github.com/gtrabanco/agentic-workflow/pull/302 — +another unit's PR, cited from References prose, never this doc's own. diff --git a/scripts/fixtures/roadmap-settle/docs/features/ROADMAP.md b/scripts/fixtures/roadmap-settle/docs/features/ROADMAP.md new file mode 100644 index 000000000..4ea0f3669 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/features/ROADMAP.md @@ -0,0 +1,10 @@ +# Roadmap + +## Features + +| NN | Slug | Status | Depends on | Summary | +|----|------|--------|------------|---------| +| 40 | `settle-other` | done · [#302](https://github.com/gtrabanco/agentic-workflow/pull/302) | — | The other unit whose settled PR a doc may cite | +| 77 | `settle-repro` | in-progress | — | Issue [#305](https://github.com/gtrabanco/agentic-workflow/issues/305) (M): the 77-shaped reproducer — PR merged, row not settled | +| 78 | `settle-done` | done · [#311](https://github.com/gtrabanco/agentic-workflow/pull/311) | — | The settled control | +| 80 | `settle-planned` | planned | — | No PR of its own yet | diff --git a/scripts/fixtures/roadmap-settle/docs/fix/272-settle-fix-done/SPEC.md b/scripts/fixtures/roadmap-settle/docs/fix/272-settle-fix-done/SPEC.md new file mode 100644 index 000000000..1cb0afa66 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/fix/272-settle-fix-done/SPEC.md @@ -0,0 +1,5 @@ +# 272 — settle-fix-done + +## Log + +`2026-10-01 — P8 — phase-lint fix landed via https://github.com/gtrabanco/agentic-workflow/pull/276` diff --git a/scripts/fixtures/roadmap-settle/docs/fix/91-settle-fix-open/SPEC.md b/scripts/fixtures/roadmap-settle/docs/fix/91-settle-fix-open/SPEC.md new file mode 100644 index 000000000..19af5ef8e --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/fix/91-settle-fix-open/SPEC.md @@ -0,0 +1,5 @@ +# 91 — settle-fix-open + +## Evidence + +| — | PR opened with `Closes #91` | exit 0 | https://github.com/gtrabanco/agentic-workflow/pull/303 | this session | diff --git a/scripts/fixtures/roadmap-settle/docs/fix/README.md b/scripts/fixtures/roadmap-settle/docs/fix/README.md new file mode 100644 index 000000000..5fbd32b50 --- /dev/null +++ b/scripts/fixtures/roadmap-settle/docs/fix/README.md @@ -0,0 +1,6 @@ +# Active fixes + +| Issue | Topic | Status | Notes | +|---|---|---|---| +| [#272](https://github.com/gtrabanco/agentic-workflow/issues/272) | settle-fix-done | `done` · [#276](https://github.com/gtrabanco/agentic-workflow/pull/276) · [SPEC](272-settle-fix-done/SPEC.md) | settled fix | +| [#91](https://github.com/gtrabanco/agentic-workflow/issues/91) | settle-fix-open | `in-progress` | active fix, PR open, row not settled | diff --git a/scripts/lane-era-lineage.test.mjs b/scripts/lane-era-lineage.test.mjs index bae657ce6..5e2790a67 100644 --- a/scripts/lane-era-lineage.test.mjs +++ b/scripts/lane-era-lineage.test.mjs @@ -358,6 +358,25 @@ test("unit-lineage: an open obligation row is BLOCKED, naming the id", (t) => { assert.match(r.stdout, /O1/, "the verdict names the open obligation id"); }); +// The ledger's section ends at the nearest `##`/`###` heading. Preferring a +// `###` found anywhere sliced through the following `##` sections, so the ledger +// parsed the Findings table as its own rows and reported those ids as open +// obligations — every unit doc carrying a findings table failed lineage. +test("unit-lineage: a later `###` heading never leaks the Findings table into the ledger", (t) => { + const { root } = makeRepo(t, { obligations: "verified" }); + const specPath = path.join(root, UNIT_DIR, "SPEC.md"); + const spec = fs.readFileSync(specPath, "utf8"); + const findings = + "\n## Findings\n\n| id | S# / file:line | axis | severity | class | route |\n" + + "|---|---|---|---|---|---|\n" + + "| F7 | S4 · x.mjs:1 | mergeability | high | source | fold — an open repair |\n" + + "\n## Review verdict\n\n### Cycle 1 — 2026-10-27\n\n```\nREVIEW-VERDICT: FAIL\n```\n"; + fs.writeFileSync(specPath, spec + findings); + const r = lineage(root); + assert.equal(r.status, 0, `must pass, got ${r.status}: ${r.stdout}${r.stderr}`); + assert.match(r.stdout, /obligations closed \(1\)/, "only the ledger's own row counts"); +}); + test("unit-lineage: an explicit n/a obligations row passes", (t) => { const { root } = makeRepo(t, { obligations: "na" }); const r = lineage(root); diff --git a/scripts/roadmap-settle.mjs b/scripts/roadmap-settle.mjs new file mode 100644 index 000000000..c132b1410 --- /dev/null +++ b/scripts/roadmap-settle.mjs @@ -0,0 +1,172 @@ +#!/usr/bin/env node + +/** + * roadmap-settle.mjs — the roadmap's `done` settle is code, not prose (issue #316). + * + * The defect: the roadmap row's done settle — the status-machine write + * `done` + PR link at PR open — had a deterministic detector + * (`unit-route.mjs` refuses close-out while the status is not `done`) and a + * deterministic writer (the crate's `roadmap rowUpsert`), but zero call + * sites: the transition was assigned to free-form prose, and three near + * identical misses (unit 77 on `main`, unit 79 in PR #311, persisted as + * finding F43) were caught late at `audit-pr`, after the review pass was + * already paid. + * + * This gate mechanizes the inverse invariant offline: a unit doc that + * carries a PR URL must have its index row reading `done · [#N]()`. The comparison itself is the crate's `checkUnitDoc` + * (`packages/agentic-workflow/src/edit/Roadmap.mjs`) — this script is the + * wiring, it carries no second copy of the grammar. + * + * Usage: + * bun scripts/roadmap-settle.mjs [--root ] [--json] (node fallback, same argv) + * + * stdout: one fixed block — a `Roadmap-settle:` line per violation with its + * named code and the expected settle form, then the tally and the verdict. + * exit: 0 only when the verdict is `PASS`; 1 on violations; 2 on usage. + * + * Codes (closed set, owned by the crate's `ROADMAP_CHECK_CODES`): + * row-missing a feature doc whose NN has no roadmap row + * slug-mismatch the doc folder's slug disagrees with the row's + * pr-url-unsettled the doc carries its PR URL and the row is not + * `done · [#N]()` + * + * Scope (issue #316, deliberately narrow): the row composer is #227, the + * schema-wide unit-doc verifier is #260, the repair-receipt receiver is #301. + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { checkUnitDoc, ROADMAP_CHECK_CODES } from "../packages/agentic-workflow/src/edit/Roadmap.mjs"; + +const SCRIPTS_DIR = path.dirname(fileURLToPath(import.meta.url)); +const REPO = path.dirname(SCRIPTS_DIR); + +function parseArgs(argv) { + const result = { json: false, root: REPO }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === "--json") result.json = true; + else if (argv[i] === "--root") { + const value = argv[++i]; + if (value === undefined) { + process.stderr.write(`usage: roadmap-settle.mjs [--root ] [--json]\n`); + process.exit(2); + } + result.root = path.resolve(value); + } + else { + process.stderr.write(`usage: roadmap-settle.mjs [--root ] [--json]\n`); + process.exit(2); + } + } + return result; +} + +/** Unit folders under a docs tree: `NN-slug` directories carrying a SPEC.md. */ +function unitDocs(docsDir) { + if (!fs.existsSync(docsDir)) return []; + return fs + .readdirSync(docsDir) + .filter((name) => /^\d/.test(name)) + .sort() + .map((name) => path.join(docsDir, name)) + .filter((dir) => fs.statSync(dir).isDirectory() && fs.existsSync(path.join(dir, "SPEC.md"))) + .map((dir) => ({ dir, spec: path.join(dir, "SPEC.md") })); +} + +export function settle(rootDir) { + const featuresDir = path.join(rootDir, "docs", "features"); + const fixDir = path.join(rootDir, "docs", "fix"); + const roadmapPath = path.join(featuresDir, "ROADMAP.md"); + const fixIndexPath = path.join(fixDir, "README.md"); + if (!fs.existsSync(roadmapPath)) { + return { usage: `no roadmap at ${roadmapPath}` }; + } + const roadmapText = fs.readFileSync(roadmapPath, "utf8"); + const fixIndexText = fs.existsSync(fixIndexPath) ? fs.readFileSync(fixIndexPath, "utf8") : null; + + const violations = []; + let checked = 0; + let fixRowless = 0; + const rel = (p) => path.relative(rootDir, p); + + for (const { spec } of unitDocs(featuresDir)) { + checked += 1; + const dirName = path.basename(path.dirname(spec)); + const nn = /^(\d+)/.exec(dirName)?.[1] ?? ""; + const result = checkUnitDoc({ + nn, + slug: dirName.replace(/^\d+-/, ""), + kind: "feature", + docText: fs.readFileSync(spec, "utf8"), + indexText: roadmapText, + }); + for (const v of result.violations) violations.push({ doc: rel(spec), ...v }); + } + + if (fixIndexText !== null) { + for (const { spec } of unitDocs(fixDir)) { + checked += 1; + const dirName = path.basename(path.dirname(spec)); + const nn = /^(\d+)/.exec(dirName)?.[1] ?? ""; + const result = checkUnitDoc({ + nn, + slug: dirName.replace(/^\d+-/, ""), + kind: "fix", + docText: fs.readFileSync(spec, "utf8"), + indexText: fixIndexText, + }); + // A fix doc with no active row is the index's own convention (merged + // fixes are removed); the disclosure keeps the skip counted, never silent. + if (!result.violations.some((v) => v.code !== "row-missing") && result.violations.some((v) => v.code === "row-missing")) { + fixRowless += 1; + continue; + } + for (const v of result.violations) violations.push({ doc: rel(spec), ...v }); + } + } + + return { checked, fixRowless, violations }; +} + +function main() { + const args = parseArgs(process.argv.slice(2)); + const result = settle(args.root); + if (result.usage) { + process.stderr.write(`roadmap-settle: ${result.usage}\n`); + process.exit(2); + } + if (args.json) { + console.log( + JSON.stringify( + { + gate: "roadmap-settle", + schema: "roadmap-table@1", + codes: ROADMAP_CHECK_CODES, + checked: result.checked, + fixRowlessSkipped: result.fixRowless, + ok: result.violations.length === 0, + violations: result.violations, + }, + null, + 2, + ), + ); + } else { + for (const v of result.violations) { + console.log(`Roadmap-settle: ${v.doc}:${v.line} ${v.code} — ${v.detail}`); + } + console.log( + `Roadmap-settle: ${result.checked} unit docs checked · ${result.fixRowless} fix docs skipped (no active row — merged fixes leave the index by convention)`, + ); + console.log(`verdict: ${result.violations.length === 0 ? "PASS" : "FAIL"}`); + console.log(`violations: ${result.violations.length}`); + } + process.exit(result.violations.length === 0 ? 0 : 1); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/scripts/roadmap-settle.test.mjs b/scripts/roadmap-settle.test.mjs new file mode 100644 index 000000000..35d6611de --- /dev/null +++ b/scripts/roadmap-settle.test.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node + +/** + * roadmap-settle.test.mjs — the root-suite gate for issue #316: a unit doc + * that carries a PR URL must have its index row reading `done · [#N]()`, and the contradiction fails closed offline with named codes. + * + * The frozen-failure corpus lives in `fixtures/roadmap-settle/`: the 77-shaped + * reproducer (PR #309 merged, row `in-progress` on `main`) and the 79-shaped + * one (PR open, seven phases logged, row `in-progress` until the audit + * blocked), both derived verbatim from git history. The live-tree case keeps + * the real drift red in the suite, the same wiring promise-coherence uses. + */ + +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import fs from "node:fs"; +import path from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const GATE = path.join(ROOT, "scripts", "roadmap-settle.mjs"); +const FIXTURES = path.join(ROOT, "scripts", "fixtures", "roadmap-settle"); + +const run = (args, cwd = ROOT) => spawnSync(process.execPath, [GATE, ...args], { cwd, encoding: "utf8" }); + +test("the 77-shaped reproducer fails closed with pr-url-unsettled", () => { + const r = run(["--root", FIXTURES]); + assert.equal(r.status, 1, r.stdout + r.stderr); + assert.match(r.stdout, /^Roadmap-settle: .*77-settle-repro\/SPEC\.md:5 pr-url-unsettled/m); + assert.match(r.stdout, /expected 'done · \[#309\]\(https:\/\/github\.com\/gtrabanco\/agentic-workflow\/pull\/309\)'/); + assert.match(r.stdout, /verdict: FAIL/); +}); + +test("the 91-shaped active-fix reproducer fails closed too", () => { + const r = run(["--root", FIXTURES]); + assert.match(r.stdout, /^Roadmap-settle: .*91-settle-fix-open\/SPEC\.md:5 pr-url-unsettled/m); +}); + +test("the settled control, the no-PR unit and the cross-reference pass — and the codes are the closed set", () => { + const r = run(["--root", FIXTURES]); + const failing = r.stdout.split("\n").filter((l) => /^Roadmap-settle: .+:\d+ /.test(l)); + assert.equal(failing.length, 2, failing.join("\n")); // the two reproducers only + assert.match(r.stdout, /units? docs? checked/); +}); + +test("settling the rows turns the same fixture tree green", () => { + const tmp = fs.mkdtempSync(path.join(fs.tmpdir?.() ?? "/tmp", "roadmap-settle-")); + fs.cpSync(FIXTURES, tmp, { recursive: true }); + try { + const table = path.join(tmp, "docs", "features", "ROADMAP.md"); + fs.writeFileSync( + table, + fs + .readFileSync(table, "utf8") + .replace("| 77 | `settle-repro` | in-progress |", "| 77 | `settle-repro` | done · [#309](https://github.com/gtrabanco/agentic-workflow/pull/309) |"), + "utf8", + ); + const fixIndex = path.join(tmp, "docs", "fix", "README.md"); + fs.writeFileSync( + fixIndex, + fs + .readFileSync(fixIndex, "utf8") + .replace("| [#91](https://github.com/gtrabanco/agentic-workflow/issues/91) | settle-fix-open | `in-progress` |", "| [#91](https://github.com/gtrabanco/agentic-workflow/issues/91) | settle-fix-open | `done` · [#303](https://github.com/gtrabanco/agentic-workflow/pull/303) |"), + "utf8", + ); + const r = run(["--root", tmp]); + assert.equal(r.status, 0, r.stdout); + assert.match(r.stdout, /verdict: PASS/); + } finally { + fs.rmSync(tmp, { recursive: true, force: true }); + } +}); + +test("the live tree is green — the suite keeps the real drift red, not the fixture's", () => { + const r = run([]); + assert.equal(r.status, 0, r.stdout + r.stderr); + assert.match(r.stdout, /verdict: PASS/); +}); + +test("--json emits the envelope with the same codes", () => { + const r = run(["--root", FIXTURES, "--json"]); + assert.equal(r.status, 1); + const envelope = JSON.parse(r.stdout); + assert.equal(envelope.gate, "roadmap-settle"); + assert.equal(envelope.ok, false); + for (const v of envelope.violations) { + assert.ok(["row-missing", "slug-mismatch", "pr-url-unsettled"].includes(v.code), v.code); + } +}); + +test("usage — an unreadable root fails closed with exit 2", () => { + const r = run(["--root", path.join(FIXTURES, "no-such-dir")]); + assert.equal(r.status, 2); +}); + +test("usage — --root with no value is a usage error, never a crash (review F5, #316)", () => { + const r = run(["--root"]); + assert.equal(r.status, 2); + assert.match(r.stderr, /usage: roadmap-settle\.mjs/); + assert.doesNotMatch(r.stderr, /ERR_INVALID_ARG_TYPE/); +}); diff --git a/scripts/unit-cycle.mjs b/scripts/unit-cycle.mjs index f437e7fc8..b48777cb1 100644 --- a/scripts/unit-cycle.mjs +++ b/scripts/unit-cycle.mjs @@ -35,6 +35,7 @@ import process from "node:process"; import { spawnSync } from "node:child_process"; import { parseUnitDocPhases } from "./unit-doc-phases.mjs"; import { compute as needsInputCompute } from "./needs-input.mjs"; +import { checkUnitDoc, findRow, rowUpsert } from "../packages/agentic-workflow/src/edit/Roadmap.mjs"; import { openRows, routeOfRow, @@ -59,7 +60,7 @@ export const OUTCOME = Object.freeze({ }); /** Acts that must move the head+findings key (the no-progress rule, D6). */ -const COMMITS = new Set(["research", "design", "plan", "phase", "evidence", "docs", "release", "fold"]); +const COMMITS = new Set(["research", "design", "plan", "phase", "evidence", "docs", "release", "fold", "settle-roadmap"]); /* ------------------------------------------------------------------ pure */ @@ -96,6 +97,43 @@ export function parsePrNumber(references) { return pr ? Number(pr[1]) : null; } +/** The PR URL a unit's References section names whole, for the settle link. */ +export function parsePrUrl(references) { + const m = /(?:^|\n)-\s*PR:\s*(\S+)/.exec(references ?? ""); + if (!m) return null; + return /\/pull\/\d+/.test(m[1]) ? m[1] : null; +} + +/** + * settleRoadmapRow — the loop writes the roadmap row itself (issue #316): + * the done settle goes through the crate's `rowUpsert` (the writer the issue + * names), with every cell the write does not own preserved verbatim, so the + * row's slug, dependency and summary survive. Never asks the model to. + * + * Writes `done · [#]()` into the status cell. A rowless NN is + * inserted (slug when given); a fix-unit row is refused — the fix index + * removes merged rows by its own convention and its table grammar is not the + * roadmap's, so the writer fails closed there and the settle stays the + * release step's documented duty. + */ +export function settleRoadmapRow({ nn, pr, prUrl, slug }, { cwd } = {}) { + const table = path.join(cwd ?? REPO, "docs", "features", "ROADMAP.md"); + const status = `done · [#${pr}](${prUrl})`; + try { + const row = findRow(table, String(nn)); + const receipt = rowUpsert(table, { + nn: String(nn), + slug: row?.slug ?? (slug ? `\`${slug}\`` : ""), + status, + dependsOn: row?.dependsOn ?? "", + description: row?.description ?? "", + }); + return receipt.ok ? { ok: true, status } : { ok: false, detail: `rowUpsert receipt not ok for NN ${nn}` }; + } catch (error) { + return { ok: false, detail: error?.message ?? String(error) }; + } +} + /** * The state machine. `state` carries both sensed facts and loop history * (foldCycles/actions/prevAction/maxCycles/maxActions — merged in by runCycle). @@ -131,6 +169,10 @@ export function decide(state) { if (wants("evidence") && !state.evidenceDone) return step("evidence"); if (wants("docs") && !state.docsDone) return step("docs"); if (!state.releaseDone) return step("release"); + // The done settle is code (issue #316): between the release and the receipt + // chain, the loop writes the row itself — a skipped write can never reach + // review, and `roadmapSettled === false` is sensed, never assumed. + if (state.roadmapSettled === false) return { type: "settle-roadmap" }; // Receipt → fold loop → audit. The receipt is machine ground truth. const foldCycles = state.foldCycles ?? 0; @@ -224,7 +266,8 @@ export function renderPrompt(action, state) { `${head}\n\nStep: run unit-lane's release step ` + `(\`skills/unit-lane/references/RELEASE.md\`) for ${unit}: push the branch, open ` + `the PR against \`main\` carrying the Evidence section and \`Closes\`, write ` + - `\`- PR: \` into the unit doc's References, and set the roadmap row.`, + `\`- PR: \` into the unit doc's References, and stop — the loop ` + + `settles the roadmap row itself before the review runs.`, }; case "review": return { @@ -376,6 +419,33 @@ export function sense(token) { const dirty = sh("git", ["status", "--porcelain"]).stdout.trim() !== ""; const head = sh("git", ["rev-parse", "HEAD"]).stdout.trim(); const prNumber = parsePrNumber(references); + const prUrl = parsePrUrl(references); + + // The settle fact (issue #316): a roadmap row reading `done · [#N]()`. Fix units are exempt in-code — the fix index removes merged rows + // by its own convention, so `roadmapSettled` stays null there and the + // release step's documented duty owns their row. + let roadmapSettled = null; + const nn = path.basename(unitDir).match(/^(\d+)/)?.[1] ?? null; + const isFix = unitDir.split(path.sep).includes("fix") && unitDir.split(path.sep).includes("docs"); + if (!isFix && nn) { + const row = findRow(path.join(TARGET, "docs", "features", "ROADMAP.md"), nn); + if (!row || prNumber === null) { + roadmapSettled = false; + } else { + // One grader for the settle (review F3, #316): the crate's own check, so + // the loop and the root gate can never disagree. This block used to accept + // any `[#N](` link while `roadmap-settle.mjs` compared the exact URL. + const { violations } = checkUnitDoc({ + nn, + slug: path.basename(unitDir).replace(/^\d+-/, ""), + kind: "feature", + docText: doc, + indexText: fs.readFileSync(path.join(TARGET, "docs", "features", "ROADMAP.md"), "utf8"), + }); + roadmapSettled = !violations.some((violation) => violation.code === "pr-url-unsettled"); + } + } let receipt = null; if (prNumber) { @@ -418,6 +488,7 @@ export function sense(token) { evidenceDone: progressHasStep(progress, "evidence"), docsDone: progressHasStep(progress, "docs"), releaseDone: prNumber !== null, + roadmapSettled, phasesDeclared: phases, phasesDone: [...recordedPhases(doc)], testsOk: null, @@ -470,14 +541,46 @@ function runRootSuite() { return `${files.length} files green`; } -/** One act: `tests` runs in code, everything else spawns a pi session. */ +/** + * Pure argv builder for the settle commit (F1, review cycle 1): `git commit + * -m -- `. Options must precede `--` — after the separator every + * token is a pathspec, so the previous literal order + * (`commit -- -m `) made `-m ` a pathspec and the settle + * commit failed unconditionally. + */ +export function settleCommitArgs({ unit, prNumber, prUrl }) { + const message = `docs(roadmap): settle ${unit} — done · [#${prNumber}](${prUrl})`; + return ["commit", "-m", message, "--", "docs/features/ROADMAP.md"]; +} + +/** One act: `tests` and the roadmap settle run in code, everything else spawns a pi session. */ function makeAct() { return (action, state) => { - const label = action.type === "phase" ? `P${action.n}` : action.step; + const label = action.type === "phase" ? `P${action.n}` : (action.step ?? action.type); if (action.type === "step" && action.step === "tests") { runRootSuite(); return; } + if (action.type === "settle-roadmap") { + // The loop writes the row itself (issue #316) — through the crate's + // rowUpsert, cells preserved — then commits and pushes the settle so the + // receipt chain sees a current head. + const unit = state.unit; + const result = settleRoadmapRow( + { nn: /^\d+/.exec(unit)?.[0], pr: state.prNumber, prUrl: state.prUrl, slug: unit.replace(/^\d+-/, "") }, + { cwd: TARGET }, + ); + if (!result.ok) throw new Error(`roadmap settle failed: ${result.detail}`); + const commit = spawnSync( + "git", + settleCommitArgs({ unit, prNumber: state.prNumber, prUrl: state.prUrl }), + { cwd: TARGET, encoding: "utf8" }, + ); + if ((commit.status ?? 1) !== 0) { + throw new Error(`roadmap settle commit failed: ${(commit.stderr ?? "").trim().split("\n")[0] || `exit ${commit.status}`}`); + } + return; + } const { prompt } = renderPrompt(action, state); const r = spawnSync("pi", ["-p", "--approve", "--name", `unit-cycle:${state.unit}:${label}`, prompt], { cwd: TARGET, @@ -492,7 +595,7 @@ function makeAct() { // `review` joins the push set without joining the commit set: its own // artifacts must already be pushed before its receipt was emitted, and a // no-op push cannot void a receipt. - const pushedLabel = action.type === "phase" ? "phase" : action.step; + const pushedLabel = action.type === "phase" ? "phase" : (action.step ?? action.type); if (COMMITS.has(pushedLabel) || pushedLabel === "review") { const pushed = spawnSync("git", ["push"], { cwd: TARGET, encoding: "utf8", timeout: 30_000 }); if ((pushed.status ?? 1) !== 0) { @@ -538,9 +641,9 @@ export async function runCycle({ sense: senseFn, act, log = () => {} }) { foldCycles: history.foldCycles, }; } - history.acted.push(action.type === "phase" ? `phase${action.n}` : action.step); + history.acted.push(action.type === "phase" ? `phase${action.n}` : (action.step ?? action.type)); const after = await senseFn(); - const key = action.type === "phase" ? "phase" : action.step; + const key = action.type === "phase" ? "phase" : (action.step ?? action.type); if (COMMITS.has(key) && after.key === state.key) { return { outcome: OUTCOME.NO_PROGRESS, diff --git a/scripts/unit-cycle.test.mjs b/scripts/unit-cycle.test.mjs index 989142d35..b17396d06 100644 --- a/scripts/unit-cycle.test.mjs +++ b/scripts/unit-cycle.test.mjs @@ -21,10 +21,14 @@ import { designedFrom, researchDoneFrom, parsePrNumber, + parsePrUrl, + settleCommitArgs, + settleRoadmapRow, phaseOrder, runCycle, OUTCOME, } from "./unit-cycle.mjs"; +import { checkUnitDoc } from "../packages/agentic-workflow/src/edit/Roadmap.mjs"; // --------------------------------------------------------------------------- // decide() — the state machine, one case per transition @@ -46,6 +50,7 @@ const BASE = { docsDone: true, releaseDone: true, prNumber: 313, + roadmapSettled: true, receipt: "current", openFindings: [], mergeReady: true, @@ -245,6 +250,93 @@ test("renderPrompt: review and audit carry the PR handle", () => { assert.match(renderPrompt({ type: "step", step: "audit" }, at({})).prompt, /audit-pr.*313|313.*audit-pr/is); }); +// --------------------------------------------------------------------------- +// the roadmap settle (issue #316): the loop writes the row itself, it never +// asks the model to +// --------------------------------------------------------------------------- + +test("decide: a released unit whose roadmap row is not settled runs the deterministic settle act", () => { + const action = decide(at({ roadmapSettled: false })); + assert.equal(action.type, "settle-roadmap"); +}); + +test("decide: the settle act precedes the receipt chain — an unsettled row is never reviewed around", () => { + const action = decide(at({ roadmapSettled: false, receipt: "absent" })); + assert.equal(action.type, "settle-roadmap"); +}); + +test("decide: a settled row changes no transition (previous behavior holds)", () => { + assert.deepEqual(decide(at({ roadmapSettled: true })), { type: "stop", reason: OUTCOME.DONE }); + const fold = decide(at({ roadmapSettled: true, receipt: "absent", openFindings: ["F1"] })); + assert.equal(fold.type, "step"); + assert.equal(fold.step, "fold"); +}); + +test("decide: a fix unit's row is settled by the fix index's own convention — no settle act", () => { + // The fix index removes merged rows; the crate's rowUpsert grammar is the + // roadmap's five-column table and fails closed on the fix index's header, + // so the deterministic settle writes roadmap rows only. + assert.equal(decide(at({ roadmapSettled: null, kind: "fix" })).type, "stop"); +}); + +test("renderPrompt: the release step no longer instructs the row write — the loop owns it", () => { + const { prompt } = renderPrompt({ type: "step", step: "release" }, at({})); + assert.match(prompt, /push the branch/); + assert.doesNotMatch(prompt, /set the roadmap row/); +}); + +test("parsePrUrl: the References PR URL is captured whole, for the settle link", () => { + assert.equal( + parsePrUrl("- PR: https://github.com/gtrabanco/agentic-workflow/pull/313\n- Branch: feat/x"), + "https://github.com/gtrabanco/agentic-workflow/pull/313", + ); + assert.equal(parsePrUrl("- PR: #313"), null); + assert.equal(parsePrUrl("no references"), null); +}); + +test("settleRoadmapRow: writes done · [#N](url) preserving every other cell, via the crate's rowUpsert", async () => { + const { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } = await import("node:fs"); + const { tmpdir } = await import("node:os"); + const { join } = await import("node:path"); + const tmp = mkdtempSync(join(tmpdir(), "unit-cycle-settle-")); + try { + mkdirSync(join(tmp, "docs", "features", "77-settle-me"), { recursive: true }); + const table = join(tmp, "docs", "features", "ROADMAP.md"); + writeFileSync( + table, + [ + "# Roadmap", + "", + "## Features", + "", + "| NN | Slug | Status | Depends on | Summary |", + "|----|------|--------|------------|---------|", + "| 77 | `settle-me` | in-progress | 78 | Issue [#305](…) (M): the long summary must survive the settle |", + "", + ].join("\n"), + "utf8", + ); + const result = settleRoadmapRow({ nn: "77", pr: 309, prUrl: "https://github.com/gtrabanco/agentic-workflow/pull/309" }, { cwd: tmp }); + assert.equal(result.ok, true, result.detail ?? ""); + const row = readFileSync(table, "utf8").split("\n").find((l) => l.startsWith("| 77 |")); + assert.match(row, /\| 77 \| `settle-me` \| done · \[#309\]\(https:\/\/github\.com\/gtrabanco\/agentic-workflow\/pull\/309\) \| 78 \| Issue \[#305\]/); + + // What the writer emits is exactly what the crate's own settle grader + // accepts — one rule, no second opinion (review F3, #316). A row settled to + // a different PR URL is refused by that same grader. + const docText = "# 77 — settle-me\n## Next\n\nPR #309 (https://github.com/gtrabanco/agentic-workflow/pull/309).\n"; + const indexText = readFileSync(table, "utf8"); + assert.deepEqual(checkUnitDoc({ nn: "77", slug: "settle-me", kind: "feature", docText, indexText }).violations, []); + const wrongUrl = docText.replace("pull/309", "pull/999"); + assert.deepEqual( + checkUnitDoc({ nn: "77", slug: "settle-me", kind: "feature", docText: wrongUrl, indexText }).violations.map((v) => v.code), + ["pr-url-unsettled"], + ); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } +}); + // --------------------------------------------------------------------------- // runCycle — the generic loop with an injected executor // --------------------------------------------------------------------------- @@ -349,3 +441,21 @@ test("runCycle: review and audit acts are exempt from the commit expectation", a }); assert.equal(result.outcome, OUTCOME.DONE); }); + +test("settleCommitArgs: the -m option precedes the -- separator (F1: after -- every token is a pathspec, so the settle commit failed unconditionally)", () => { + const argv = settleCommitArgs({ unit: "80-example", prNumber: 323, prUrl: "https://github.com/gtrabanco/agentic-workflow/pull/323" }); + assert.equal(argv[0], "commit"); + const separator = argv.indexOf("--"); + assert.ok(separator > 0, "argv carries a -- separator"); + // Everything after -- is a pathspec: the -m flag and its message must sit before it. + assert.ok(argv.indexOf("-m") > 0 && argv.indexOf("-m") < separator, "-m precedes --"); + assert.equal(argv[separator + 1], "docs/features/ROADMAP.md"); + const message = argv[argv.indexOf("-m") + 1]; + assert.match(message, /settle 80-example — done · \[#323\]\(https:\/\/github\.com\/gtrabanco\/agentic-workflow\/pull\/323\)/); +}); + +test("settleCommitArgs: the only pathspec is the roadmap table (no stray option lands after --)", () => { + const argv = settleCommitArgs({ unit: "316-roadmap-settle", prNumber: 1, prUrl: "https://example.test/pr/1" }); + const separator = argv.indexOf("--"); + assert.deepEqual(argv.slice(separator + 1), ["docs/features/ROADMAP.md"]); +}); diff --git a/scripts/unit-lineage.mjs b/scripts/unit-lineage.mjs index 46f02e20c..46eec3380 100644 --- a/scripts/unit-lineage.mjs +++ b/scripts/unit-lineage.mjs @@ -89,7 +89,12 @@ function subsectionBody(text, name) { const m = new RegExp(`^### ${name}\\s*$`, "m").exec(text); if (!m) return null; const rest = text.slice(m.index + m[0].length); - const next = /^### /m.exec(rest) ?? /^## /m.exec(rest); + // The section ends at the nearest `###` or `##` heading, whichever comes + // first. Preferring the first `###` found anywhere swallowed every `##` + // section up to a much later subsection, so a reader would parse another + // section's table as its own rows — an obligation ledger read the Findings + // table and reported its ids as open obligations. + const next = /^#{2,3} /m.exec(rest); return next ? rest.slice(0, next.index) : rest; } diff --git a/skills/execute-phase/SKILL.md b/skills/execute-phase/SKILL.md index 0138c1faa..90c000fd2 100644 --- a/skills/execute-phase/SKILL.md +++ b/skills/execute-phase/SKILL.md @@ -1,7 +1,7 @@ --- name: execute-phase user-invocable: true -version: 6.2.2 +version: 6.3.1 argument-hint: [P|step] [--max-attempts N] [--force] allowed-tools: [Bash, Read, Edit, Write, MultiEdit] author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>" @@ -182,7 +182,13 @@ lane hand-off is a defect; re-read the hand-off before looping. After each step: gates, execute, diff guard (if implement), evidence/progress update, commit. Skip intermediate review checkpoints; the mandatory end review covers the full accumulated diff. On gate red at any step: repair that step (up to -`--max-attempts`), then continue the loop. Terminal: +`--max-attempts`), then continue the loop. The close-out is part of the last +step's completion: open the PR on `main` with `Closes #N`, write `- PR: ` +into References, and settle the index row with `agentic-workflow roadmap + rowUpsert ''` (`docs/features/ROADMAP.md`; a fix unit edits its +`docs/fix/README.md` status cell — no `NN` column, no `rowUpsert`). +`bun scripts/roadmap-settle.mjs` exits 0 (#316). +Terminal: ```text UNIT LOOP — COMPLETE diff --git a/skills/unit-lane/SKILL.md b/skills/unit-lane/SKILL.md index 891385e64..4966b6c25 100644 --- a/skills/unit-lane/SKILL.md +++ b/skills/unit-lane/SKILL.md @@ -1,7 +1,7 @@ --- name: unit-lane user-invocable: true -version: 2.4.3 +version: 2.5.1 argument-hint: "> [--retriage] | --fix | --from-issue description: > One-shot lane conductor for a delivery unit: triage, implement, evidence, and diff --git a/skills/unit-lane/references/PLAN.md b/skills/unit-lane/references/PLAN.md index 77614bc1c..25d37d5cc 100644 --- a/skills/unit-lane/references/PLAN.md +++ b/skills/unit-lane/references/PLAN.md @@ -100,6 +100,11 @@ P1 — (validator: ) P2 — (validator: ) … Pn — last task is verification when the unit has behavior +P — release settle, when the unit opens a PR: after the PR exists, settle + the roadmap row via the crate's writer (`agentic-workflow roadmap rowUpsert`, + status `done · [#N]()`, every other cell preserved) — validator: + `bun scripts/roadmap-settle.mjs` exits 0 (#316: the settle is a task the + executor owns, never a missing courtesy) ``` Both phase grammars are accepted (owner: `phase-contract`): these bullets, or @@ -164,7 +169,8 @@ name those files instead. - [ ] Every AC maps to at least one task, and no task re-decides an AC - [ ] Tasks are ordered smallest first - [ ] Each task names its validator (a backticked command plus its expected outcome) -- [ ] Final task is verification (proves the whole unit works) +- [ ] Final task is verification (proves the whole unit works); a unit that + opens a PR ends with the release-settle task (#316) - [ ] No task reaches into a future-phase concern (docs, release, migrations) - [ ] Phases labeled P1, P2, … — `#308`: `S#` is the requirement id (a spec), `P#` is the phase label (a task); the two vocabularies never swap roles @@ -190,7 +196,8 @@ name those files instead. - Do not decide at the ask stage, and do not re-ask an answered question - Do not bundle across phase boundaries — one phase = one commit - Do not plan work beyond the unit's acceptance criteria -- Do not include docs/release tasks here — those are separate catalog steps +- Do not include docs/release tasks here — those are separate catalog steps; + the release-settle task above is the sole exception (#316) - Do not invent tasks that the ACs do not demand - Do not turn the sweep into a gate, stage verdict or blocking check — it routes, it never blocks a miss, and it adds no authority diff --git a/skills/unit-lane/references/RELEASE.md b/skills/unit-lane/references/RELEASE.md index ea289ac8c..76708a6dc 100644 --- a/skills/unit-lane/references/RELEASE.md +++ b/skills/unit-lane/references/RELEASE.md @@ -30,7 +30,13 @@ Write into the unit doc's **References** section: - [ ] Branch follows the project's Workflow conventions - [ ] Commit messages follow conventional format (`type(scope): description`) - [ ] PR body includes the Evidence section as verification record -- [ ] Roadmap row updated to `done` (or `merged` per conventions) +- [ ] Index row settled via the crate's writer — feature: re-emit the row with + `agentic-workflow roadmap docs/features/ROADMAP.md rowUpsert + '{"nn":"","slug":"","status":"done · [#]()"}'`, + every other cell as it is; fix: the `docs/fix/README.md` status cell — that + index has no `NN` column, so `rowUpsert` (the roadmap's writer) does not + apply to it. `bun scripts/roadmap-settle.mjs` exits 0 afterwards (issue + #316: the settle is code, never prose) - [ ] `git status --porcelain` is empty ## Allowed