From 976f264b3125102f4b81733aa27349555f967648 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 10:28:02 -0700 Subject: [PATCH 01/29] docs(method): formalize feedback workflow and triage backlog --- README.md | 20 ++++++++++++ docs/GUIDE.md | 19 +++++++++++ docs/method/backlog/asap/.gitkeep | 1 + docs/method/backlog/bad-code/.gitkeep | 1 + .../up-next/MCP_status-summary-mode.md | 32 +++++++++++++++++++ ...SS_legend-should-be-in-frontmatter-yaml.md | 9 ++++++ docs/method/feedback/README.md | 25 +++++++++++++++ docs/method/feedback/archive/README.md | 16 ++++++++++ docs/method/graveyard/.gitkeep | 1 + docs/method/process.md | 18 +++++++++++ 10 files changed, 142 insertions(+) create mode 100644 docs/method/backlog/asap/.gitkeep create mode 100644 docs/method/backlog/bad-code/.gitkeep create mode 100644 docs/method/backlog/up-next/MCP_status-summary-mode.md create mode 100644 docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md create mode 100644 docs/method/feedback/README.md create mode 100644 docs/method/feedback/archive/README.md create mode 100644 docs/method/graveyard/.gitkeep diff --git a/README.md b/README.md index 52aa1e4..b5c613d 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,8 @@ docs/ cool-ideas/ experiments, wild thoughts bad-code/ tech debt *.md everything else + feedback/ raw feedback awaiting triage + feedback/archive/ processed feedback kept for provenance legends/ named domains retro//.md retrospectives releases/vX.Y.Z/ internal release packets @@ -418,6 +420,21 @@ Rejected work moves to `docs/method/graveyard/` with a note explaining why. The graveyard prevents re-proposing without context. If you want to resurrect something, you must address the note. +## Feedback + +Feedback that is not yet shaped into backlog or design work belongs in +`docs/method/feedback/`. + +Treat feedback docs as an inbox for critique, review notes, and +outside-in observations. When processed, turn accepted feedback into +explicit repo artifacts such as backlog items, design edits, signpost +updates, or release notes. Then move the processed feedback document to +`docs/method/feedback/archive/` with a short disposition note instead +of deleting it. + +The archive preserves provenance, shows what feedback already landed, +and prevents the same point from being rediscovered without context. + --- ## Flow @@ -431,6 +448,9 @@ idea -> inbox/ -> cool-ideas/ -> up-next/ -> asap/ -> ship sync (BEARING / CHANGELOG / release when meaningful) - or -> -> graveyard/ + +feedback -> feedback/ -> backlog/design/signpost/release/no-action + -> feedback/archive/ ``` --- diff --git a/docs/GUIDE.md b/docs/GUIDE.md index 383b316..a386c04 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -36,6 +36,25 @@ Why this is good practice: current feature work. - It avoids awkward cleanup and weird Git operations later. +## Process feedback in batches + +If review notes or operator feedback start piling up, capture them in +`docs/method/feedback/` instead of scattering them across chat or +deleting them after triage. + +Treat a feedback doc as raw input, not as a backlog item yet. During a +maintenance pass, read it carefully, split accepted points into the +right durable artifacts, and then move the source document into +`docs/method/feedback/archive/` with a short note about what happened. + +Why this is good practice: + +- It keeps the backlog from becoming a dumping ground for half-processed + critique. +- It preserves the original wording and context behind later backlog or + docs changes. +- It makes "no action" a visible decision instead of silent deletion. + ## Advice is not doctrine Some things are worth saying before they are worth standardizing. diff --git a/docs/method/backlog/asap/.gitkeep b/docs/method/backlog/asap/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/method/backlog/asap/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/method/backlog/bad-code/.gitkeep b/docs/method/backlog/bad-code/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/method/backlog/bad-code/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/method/backlog/up-next/MCP_status-summary-mode.md b/docs/method/backlog/up-next/MCP_status-summary-mode.md new file mode 100644 index 0000000..6f71474 --- /dev/null +++ b/docs/method/backlog/up-next/MCP_status-summary-mode.md @@ -0,0 +1,32 @@ +--- +title: "method_status Summary Mode" +legend: MCP +lane: up-next +--- + +# MCP: method_status summary mode + +`method_status` returns the full backlog expanded — every item with path, +legend, slug, lane. For git-warp this is 68KB of JSON (215 backlog items). + +Most callers just want lane counts and active cycle names. The full +expansion blows through agent context windows and adds no value for the +common "where are we?" check. + +## Proposal + +Add an optional `summary` parameter (default `true`) to `method_status`. + +**Summary mode** returns: +- Lane counts: `{ inbox: 4, asap: 22, "up-next": 15, "cool-ideas": 50, "bad-code": 124 }` +- Active cycle stems +- Retro count +- Legend groups (if any) + +**Full mode** (`summary: false`) returns the current expanded output. + +## Context + +Discovered during dogfooding all 4 MCP servers in git-warp. Graft, Think, +and Bijou all return concise responses. METHOD is the outlier at 68KB for +a status check. diff --git a/docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md b/docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md new file mode 100644 index 0000000..7210e17 --- /dev/null +++ b/docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md @@ -0,0 +1,9 @@ +--- +title: "Legend Should Be In Frontmatter YAML" +legend: PROCESS +lane: up-next +--- + +# Legend Should Be In Frontmatter YAML + +Require backlog items to carry their legend in YAML frontmatter instead of relying only on filename prefixes or inferred metadata. Commands that create, move, or audit backlog items should preserve and validate the `legend` field so legend-aware workflows have a single canonical source. diff --git a/docs/method/feedback/README.md b/docs/method/feedback/README.md new file mode 100644 index 0000000..80a04b8 --- /dev/null +++ b/docs/method/feedback/README.md @@ -0,0 +1,25 @@ +# Feedback Inbox + +Store raw feedback documents here until they are processed. + +A feedback doc is not yet backlog. It is source material: review notes, +critique, rough observations, or "this feels off" comments that may +produce zero, one, or many follow-up artifacts. + +Recommended contents: + +- what the feedback is +- where it came from +- when it was captured +- the concrete observations or suggestions + +When processing a feedback doc: + +1. Read it as input, not as already-prioritized work. +2. Turn accepted points into durable repo artifacts such as backlog + items, design edits, signpost updates, or release notes. +3. Add a short disposition note describing what happened. +4. Move the document to `docs/method/feedback/archive/`. + +Do not delete processed feedback by default. The archive is part of the +repo's memory. diff --git a/docs/method/feedback/archive/README.md b/docs/method/feedback/archive/README.md new file mode 100644 index 0000000..28c848b --- /dev/null +++ b/docs/method/feedback/archive/README.md @@ -0,0 +1,16 @@ +# Feedback Archive + +Processed feedback documents live here. + +Archive a feedback doc after its points have been triaged into durable +repo artifacts or explicitly marked as no action. + +Each archived document should preserve: + +- the original feedback +- a short disposition note +- links or references to any backlog items, docs updates, or other + outputs created from it + +The goal is provenance, not cleanliness theater. Deleting processed +feedback throws away context that the repo may need later. diff --git a/docs/method/graveyard/.gitkeep b/docs/method/graveyard/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/docs/method/graveyard/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/method/process.md b/docs/method/process.md index 57822a1..5000d2d 100644 --- a/docs/method/process.md +++ b/docs/method/process.md @@ -13,6 +13,9 @@ METHOD cycles run as a calm pull-design-test-playback-close-review-ship-sync loo - Drift is checked explicitly at close, not hand-waved after the fact. Invariant preservation is part of the drift check. - Backlog maintenance happens at cycle boundaries, not continuously. +- Feedback capture can happen anytime, but processing feedback into + backlog, design, signpost, or release work should happen at cycle + boundaries or on a dedicated triage branch. - Repo-level ship surfaces such as `BEARING.md` and `CHANGELOG.md` reflect merged `main` state, not branch-local closeout state. - Review visibility is currently outside METHOD's repo-native @@ -62,6 +65,21 @@ METHOD uses Git for distributed coordination but remains forge-agnostic. 4. **Close:** Run `method close` to write the retro and witness metadata. 5. **Merge:** Open a PR/Review. Once approved, merge to `main`. +### Feedback Processing + +1. **Capture:** Store raw review notes, critique, or outside-in + observations as markdown files under `docs/method/feedback/`. +2. **Classify:** Treat each feedback doc as unprocessed input, not as + backlog or doctrine on its own. +3. **Process:** During a maintenance pass, convert accepted feedback + into explicit repo artifacts such as backlog items, design edits, + signpost updates, or release notes. +4. **Record disposition:** Add a short note to the feedback document + describing what changed, what was rejected, or why no action was + taken. +5. **Archive:** Move the processed document to + `docs/method/feedback/archive/` instead of deleting it. + ### The Ship Sync Maneuver After a cycle branch is merged to `main`, the operator (human or agent) From f52daff106f387c97ef6c4d1b186a3d6104f864c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 10:41:43 -0700 Subject: [PATCH 02/29] docs(backlog): add debt and signpost follow-ons --- docs/method/backlog/bad-code/.gitkeep | 1 - ...backlog-metadata-single-source-of-truth.md | 24 +++++++++++++++++++ .../PROCESS_workspace-operation-split.md | 23 ++++++++++++++++++ .../PROCESS_signpost-template-overrides.md | 19 +++++++++++++++ .../cool-ideas/SYNTH_sync-vision-command.md | 18 ++++++++++++++ 5 files changed, 84 insertions(+), 1 deletion(-) delete mode 100644 docs/method/backlog/bad-code/.gitkeep create mode 100644 docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md create mode 100644 docs/method/backlog/bad-code/PROCESS_workspace-operation-split.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_signpost-template-overrides.md create mode 100644 docs/method/backlog/cool-ideas/SYNTH_sync-vision-command.md diff --git a/docs/method/backlog/bad-code/.gitkeep b/docs/method/backlog/bad-code/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/docs/method/backlog/bad-code/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md b/docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md new file mode 100644 index 0000000..7ff5e7d --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md @@ -0,0 +1,24 @@ +--- +title: "Backlog Metadata Single Source Of Truth" +legend: PROCESS +lane: bad-code +--- + +# Backlog Metadata Single Source Of Truth + +Backlog metadata is still split across filename prefixes and YAML +frontmatter. `captureIdea` writes `legend` and `lane` into frontmatter, +but `collectBacklogItems`, `calculateLegendHealth`, and `pullItem` still +derive legend from the filename stem, while `moveBacklogItem` renames the +file without updating frontmatter metadata. That creates silent drift: +the path can say one thing, the YAML can say another, and status/MCP +surfaces may report whichever source they happen to trust. + +This is bad code because the repo now has multiple authorities for the +same facts. It is especially hostile to agents, which need one cheap, +stable, machine-readable source instead of filename heuristics plus +special-case inference. + +The fix should make frontmatter canonical for backlog metadata, keep +filenames as convenience or display structure, and ensure move/pull/ +status/audit flows preserve and validate the YAML fields. diff --git a/docs/method/backlog/bad-code/PROCESS_workspace-operation-split.md b/docs/method/backlog/bad-code/PROCESS_workspace-operation-split.md new file mode 100644 index 0000000..519f06a --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_workspace-operation-split.md @@ -0,0 +1,23 @@ +--- +title: "Workspace Operation Split" +legend: PROCESS +lane: bad-code +--- + +# Workspace Operation Split + +`Workspace` still carries too many responsibilities in one class: +initialization checks, backlog capture, pull/close flow, witness +generation, ship sync, status collection, frontmatter mutation, backlog +moves, cycle resolution, and shell command execution. The repo already +called this out in the `0029-bad-code-cleanup` retro as remaining debt. + +This is not just a style complaint. It makes the runtime harder to test, +harder to evolve, and more likely to grow hidden coupling between +backlog, cycle, signpost, and execution behavior. It also raises the +cost of exposing a cleaner library or MCP surface because too many +operations hide behind one mutable object. + +Follow-on decomposition should extract focused modules for backlog +operations, cycle operations, witness/command execution, and status/ +signpost generation while preserving the honest public API. diff --git a/docs/method/backlog/cool-ideas/PROCESS_signpost-template-overrides.md b/docs/method/backlog/cool-ideas/PROCESS_signpost-template-overrides.md new file mode 100644 index 0000000..0df8d17 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_signpost-template-overrides.md @@ -0,0 +1,19 @@ +--- +title: "Signpost Template Overrides" +legend: PROCESS +lane: cool-ideas +--- + +# Signpost Template Overrides + +Generated signposts currently hardcode repo voice and section content in +renderer code. For example, `renderBearing` bakes in the "What feels +wrong?" bullets directly, which makes repo-specific customization or +experimentation require code changes instead of configuration or +template-level overrides. + +A lightweight signpost-template system could let repos keep METHOD's +structure while customizing selected generated sections. That would make +signpost generation less rigid, reduce pressure to fork the tool for +small wording changes, and open the door to agent-focused variants such +as concise machine-readable sections alongside human-oriented prose. diff --git a/docs/method/backlog/cool-ideas/SYNTH_sync-vision-command.md b/docs/method/backlog/cool-ideas/SYNTH_sync-vision-command.md new file mode 100644 index 0000000..8191bde --- /dev/null +++ b/docs/method/backlog/cool-ideas/SYNTH_sync-vision-command.md @@ -0,0 +1,18 @@ +--- +title: "Sync Vision Command" +legend: SYNTH +lane: cool-ideas +--- + +# Sync Vision Command + +Add a `method sync vision` command that runs the Executive Summary +Protocol and refreshes `docs/VISION.md` as a bounded, provenance-carrying +signpost. It should inventory the governing repo surfaces, generate the +summary from repo truth, and attach a witness reference or manifest so a +later human or agent can inspect how the summary was derived. + +This idea already surfaced in spirit once `method sync ship` automated +other signpost maintenance. A dedicated vision sync would make summary +refresh cheaper, more reproducible, and more agent-friendly than +manually re-running a large read/summarize pass with ad hoc prompts. From 842e141280b9523e0bd0a5e45ff7100a9b25384c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 10:50:03 -0700 Subject: [PATCH 03/29] docs(backlog): capture audit sweep debt and ideas --- .../bad-code/PROCESS_bearing-truthfulness.md | 17 +++++++++++++++++ .../bad-code/PROCESS_dist-output-pruning.md | 17 +++++++++++++++++ .../PROCESS_generated-doc-scaffold-contract.md | 17 +++++++++++++++++ .../PROCESS_github-comment-sync-identity.md | 16 ++++++++++++++++ .../PROCESS_live-legend-definition-coverage.md | 16 ++++++++++++++++ .../PROCESS_mcp-tool-result-contract.md | 17 +++++++++++++++++ .../PROCESS_test-taxonomy-and-fixture-drift.md | 17 +++++++++++++++++ ...ESS_architecture-source-inventory-section.md | 12 ++++++++++++ .../PROCESS_backlog-summary-signpost-section.md | 13 +++++++++++++ .../PROCESS_drift-score-visibility.md | 12 ++++++++++++ .../PROCESS_drift-threshold-config.md | 13 +++++++++++++ .../PROCESS_github-milestone-lane-sync.md | 12 ++++++++++++ .../PROCESS_remote-issue-deletion-sync.md | 12 ++++++++++++ .../cool-ideas/PROCESS_review-state-query.md | 13 +++++++++++++ .../backlog/cool-ideas/PROCESS_spike-command.md | 12 ++++++++++++ .../cool-ideas/PROCESS_validate-command.md | 13 +++++++++++++ ...PROCESS_visual-witness-screenshot-capture.md | 13 +++++++++++++ .../cool-ideas/PROCESS_witness-file-capture.md | 13 +++++++++++++ docs/method/feedback/README.md | 4 ++++ docs/method/feedback/archive/README.md | 4 ++++ 20 files changed, 263 insertions(+) create mode 100644 docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md create mode 100644 docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md create mode 100644 docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md create mode 100644 docs/method/backlog/bad-code/PROCESS_github-comment-sync-identity.md create mode 100644 docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md create mode 100644 docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md create mode 100644 docs/method/backlog/bad-code/PROCESS_test-taxonomy-and-fixture-drift.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_architecture-source-inventory-section.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_backlog-summary-signpost-section.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_drift-score-visibility.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_drift-threshold-config.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_github-milestone-lane-sync.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_remote-issue-deletion-sync.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_review-state-query.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_spike-command.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_validate-command.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_visual-witness-screenshot-capture.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_witness-file-capture.md diff --git a/docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md b/docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md new file mode 100644 index 0000000..94d42f9 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md @@ -0,0 +1,17 @@ +--- +title: "BEARING Truthfulness" +legend: PROCESS +lane: bad-code +--- + +# BEARING Truthfulness + +`renderBearing` currently emits factual claims that have drifted from the +repo. In particular, it still says witness generation is not automated +even though automated witness capture already landed. A generated +signpost that tells the truth incorrectly is worse than missing +automation: it creates confident drift in the repo's own public surface. + +This needs a pass to make BEARING generation derive more of its claims +from current repo state or explicitly constrain which statements are +allowed to be hardcoded. diff --git a/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md b/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md new file mode 100644 index 0000000..57ab17b --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md @@ -0,0 +1,17 @@ +--- +title: "Dist Output Pruning" +legend: PROCESS +lane: bad-code +--- + +# Dist Output Pruning + +The checked-in `dist/` tree still contains `workspace.*` artifacts even +though there is no longer a matching `src/workspace.ts`. That means +build output can retain dead files across refactors and package stale +implementation surfaces that are no longer real. + +This is bad code because published artifacts stop being a trustworthy +projection of current source. The build or release path should either +clean `dist/` before emit or explicitly validate that `dist/` matches +live source modules. diff --git a/docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md b/docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md new file mode 100644 index 0000000..1062999 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md @@ -0,0 +1,17 @@ +--- +title: "Generated Doc Scaffold Contract" +legend: PROCESS +lane: bad-code +--- + +# Generated Doc Scaffold Contract + +`method pull` and `method close` still generate design and retro +documents with older scaffold shapes that do not match the repo's +current frontmatter contract. The committed docs suite enforces YAML +frontmatter and required fields on real docs, but the command-generated +templates do not yet guarantee those same invariants. + +That means METHOD can generate docs that its own repo doctrine would +reject if they were committed. The scaffolders and the tests need to be +brought back into parity so generated docs are valid by construction. diff --git a/docs/method/backlog/bad-code/PROCESS_github-comment-sync-identity.md b/docs/method/backlog/bad-code/PROCESS_github-comment-sync-identity.md new file mode 100644 index 0000000..941d4e0 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_github-comment-sync-identity.md @@ -0,0 +1,16 @@ +--- +title: "GitHub Comment Sync Identity" +legend: PROCESS +lane: bad-code +--- + +# GitHub Comment Sync Identity + +GitHub comment synchronization is still additive-only and uses simple +string matching to avoid duplicates instead of tracking stable remote +comment identity. That makes sync behavior approximate and brittle: +similar edits can duplicate, true edits cannot reconcile cleanly, and +agents do not get a durable mapping between repo state and remote review +discussion. + +This needs real comment identity tracking rather than textual heuristics. diff --git a/docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md b/docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md new file mode 100644 index 0000000..c7003a4 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md @@ -0,0 +1,16 @@ +--- +title: "Live Legend Definition Coverage" +legend: PROCESS +lane: bad-code +--- + +# Live Legend Definition Coverage + +The backlog is already using `MCP` as a live legend, but +`docs/method/legends/` still only defines `PROCESS` and `SYNTH`. Tests +and signposts currently treat legend docs as the discovered taxonomy, +which means the repo can have real legend-bearing work that is not +actually documented as a first-class legend. + +This is taxonomy drift. If a legend is real enough to organize backlog +work, it should either have a legend doc or stop being used as a legend. diff --git a/docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md b/docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md new file mode 100644 index 0000000..dd441c4 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md @@ -0,0 +1,17 @@ +--- +title: "MCP Tool Result Contract" +legend: PROCESS +lane: bad-code +--- + +# MCP Tool Result Contract + +The MCP server still returns tool results as ad hoc text blobs. Even the +test that asks whether an MCP client can interact without parsing +terminal text is effectively answered with string-contains assertions on +`content[0].text`. This is expensive and fragile for agents, especially +when status payloads are large or when callers need stable field-level +results. + +METHOD needs a more explicit structured result contract for MCP tools so +agents can consume repo state and mutations without scraping prose. diff --git a/docs/method/backlog/bad-code/PROCESS_test-taxonomy-and-fixture-drift.md b/docs/method/backlog/bad-code/PROCESS_test-taxonomy-and-fixture-drift.md new file mode 100644 index 0000000..2d9914f --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_test-taxonomy-and-fixture-drift.md @@ -0,0 +1,17 @@ +--- +title: "Test Taxonomy And Fixture Drift" +legend: PROCESS +lane: bad-code +--- + +# Test Taxonomy And Fixture Drift + +Several tests still seed obsolete legend codes like `PROTO`, `VIZ`, +`TUI`, and `FEAT`, and they create markdown fixtures that do not follow +the current frontmatter expectations. The committed docs suite, README, +and backlog conventions now claim a tighter live taxonomy than the tests +exercise. + +That makes the suite partially validate an older METHOD than the one the +repo documents. Fixtures should either reflect the current doctrine or +be clearly marked as compatibility cases rather than silent drift. diff --git a/docs/method/backlog/cool-ideas/PROCESS_architecture-source-inventory-section.md b/docs/method/backlog/cool-ideas/PROCESS_architecture-source-inventory-section.md new file mode 100644 index 0000000..3520a2c --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_architecture-source-inventory-section.md @@ -0,0 +1,12 @@ +--- +title: "Architecture Source Inventory Section" +legend: PROCESS +lane: cool-ideas +--- + +# Architecture Source Inventory Section + +Add generated marker support to `ARCHITECTURE.md` so METHOD can publish a +bounded source-file inventory or module map directly from the live code +tree. This would extend the hybrid signpost pattern beyond CLI/MCP/GUIDE +and make architecture docs cheaper to keep honest after refactors. diff --git a/docs/method/backlog/cool-ideas/PROCESS_backlog-summary-signpost-section.md b/docs/method/backlog/cool-ideas/PROCESS_backlog-summary-signpost-section.md new file mode 100644 index 0000000..e9584ec --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_backlog-summary-signpost-section.md @@ -0,0 +1,13 @@ +--- +title: "Backlog Summary Signpost Section" +legend: PROCESS +lane: cool-ideas +--- + +# Backlog Summary Signpost Section + +Add a generated backlog-summary section to a signpost such as +`docs/GUIDE.md` or another repo-facing doc. The section should provide a +bounded snapshot of lane counts, active pressure, and notable priority +clusters without dumping the full backlog or forcing agents to expand a +large MCP payload just to answer "where are we?". diff --git a/docs/method/backlog/cool-ideas/PROCESS_drift-score-visibility.md b/docs/method/backlog/cool-ideas/PROCESS_drift-score-visibility.md new file mode 100644 index 0000000..d67e384 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_drift-score-visibility.md @@ -0,0 +1,12 @@ +--- +title: "Drift Score Visibility" +legend: PROCESS +lane: cool-ideas +--- + +# Drift Score Visibility + +When the drift detector reports a near miss, also show the similarity +score or comparable transparency signal. That would make the hint more +legible to humans and agents and reduce the need to guess whether a hint +is barely related or almost exact. diff --git a/docs/method/backlog/cool-ideas/PROCESS_drift-threshold-config.md b/docs/method/backlog/cool-ideas/PROCESS_drift-threshold-config.md new file mode 100644 index 0000000..ac97553 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_drift-threshold-config.md @@ -0,0 +1,13 @@ +--- +title: "Drift Threshold Config" +legend: PROCESS +lane: cool-ideas +--- + +# Drift Threshold Config + +Make the drift detector's near-miss similarity threshold configurable +through `.method.json` or another repo config surface. Different repos +will have different tolerance for wording variance, and hardcoding one +threshold in code limits METHOD's usefulness as a reusable workflow +substrate. diff --git a/docs/method/backlog/cool-ideas/PROCESS_github-milestone-lane-sync.md b/docs/method/backlog/cool-ideas/PROCESS_github-milestone-lane-sync.md new file mode 100644 index 0000000..e0611f3 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_github-milestone-lane-sync.md @@ -0,0 +1,12 @@ +--- +title: "GitHub Milestone Lane Sync" +legend: PROCESS +lane: cool-ideas +--- + +# GitHub Milestone Lane Sync + +Let GitHub milestones or equivalent remote planning metadata map onto +METHOD backlog lanes or tags. That would make cross-surface planning +less manual and could give agents a cheaper way to understand remote +priority structure without inferring it from issue text alone. diff --git a/docs/method/backlog/cool-ideas/PROCESS_remote-issue-deletion-sync.md b/docs/method/backlog/cool-ideas/PROCESS_remote-issue-deletion-sync.md new file mode 100644 index 0000000..aa30eea --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_remote-issue-deletion-sync.md @@ -0,0 +1,12 @@ +--- +title: "Remote Issue Deletion Sync" +legend: PROCESS +lane: cool-ideas +--- + +# Remote Issue Deletion Sync + +Support an explicit policy for what METHOD should do when a synced remote +issue is deleted or otherwise disappears. Today that case is mostly a +silent edge, but a real sync layer should decide whether to archive the +local item, warn loudly, or preserve it with a remote-tombstone marker. diff --git a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md new file mode 100644 index 0000000..4165480 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md @@ -0,0 +1,13 @@ +--- +title: "Review State Query" +legend: PROCESS +lane: cool-ideas +--- + +# Review State Query + +METHOD currently says review state is not part of the repo-native +coordination surface. A dedicated query or signpost that summarizes what +is under review, awaiting merge, or blocked in PR state would make the +system cheaper for both humans and agents who currently have to bounce +out to forge context to answer that simple question. diff --git a/docs/method/backlog/cool-ideas/PROCESS_spike-command.md b/docs/method/backlog/cool-ideas/PROCESS_spike-command.md new file mode 100644 index 0000000..a9bc4eb --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_spike-command.md @@ -0,0 +1,12 @@ +--- +title: "Spike Command" +legend: PROCESS +lane: cool-ideas +--- + +# Spike Command + +Add a `method spike` command that creates a properly prefixed spike +backlog item or packet with the right scaffolding and expectations. The +behavior-spike convention already exists in doctrine, but the repo still +relies on operators to remember the naming and capture rules by hand. diff --git a/docs/method/backlog/cool-ideas/PROCESS_validate-command.md b/docs/method/backlog/cool-ideas/PROCESS_validate-command.md new file mode 100644 index 0000000..e18162a --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_validate-command.md @@ -0,0 +1,13 @@ +--- +title: "Validate Command" +legend: PROCESS +lane: cool-ideas +--- + +# Validate Command + +Add a `method validate` command that checks repo-visible METHOD +invariants across backlog, design, retro, legends, and generated +signposts. This should be more than a general doctor command: it should +make the documentation and metadata contract cheap to verify for humans, +CI, and agents before drift leaks into committed state. diff --git a/docs/method/backlog/cool-ideas/PROCESS_visual-witness-screenshot-capture.md b/docs/method/backlog/cool-ideas/PROCESS_visual-witness-screenshot-capture.md new file mode 100644 index 0000000..31cf178 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_visual-witness-screenshot-capture.md @@ -0,0 +1,13 @@ +--- +title: "Visual Witness Screenshot Capture" +legend: PROCESS +lane: cool-ideas +--- + +# Visual Witness Screenshot Capture + +Add optional screenshot capture to witness generation for cycles that +have meaningful visual proof. METHOD should stay honest about the fact +that screenshots are supporting evidence rather than the done-claim, but +capturing them automatically would make visual witness packets cheaper +and more reproducible. diff --git a/docs/method/backlog/cool-ideas/PROCESS_witness-file-capture.md b/docs/method/backlog/cool-ideas/PROCESS_witness-file-capture.md new file mode 100644 index 0000000..59fda25 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_witness-file-capture.md @@ -0,0 +1,13 @@ +--- +title: "Witness File Capture" +legend: PROCESS +lane: cool-ideas +--- + +# Witness File Capture + +Allow a design or cycle to declare specific files or directories that +should be copied or referenced into the witness packet during automated +capture. Some proofs live in generated artifacts or filesystem shape, +not just command output, and METHOD should be able to name those +artifacts explicitly instead of forcing one-off manual witness assembly. diff --git a/docs/method/feedback/README.md b/docs/method/feedback/README.md index 80a04b8..c2f94e1 100644 --- a/docs/method/feedback/README.md +++ b/docs/method/feedback/README.md @@ -1,3 +1,7 @@ +--- +title: "Feedback Inbox" +--- + # Feedback Inbox Store raw feedback documents here until they are processed. diff --git a/docs/method/feedback/archive/README.md b/docs/method/feedback/archive/README.md index 28c848b..ee82001 100644 --- a/docs/method/feedback/archive/README.md +++ b/docs/method/feedback/archive/README.md @@ -1,3 +1,7 @@ +--- +title: "Feedback Archive" +--- + # Feedback Archive Processed feedback documents live here. From 82919d77da802af71dc858b2693c8fcb83ada3c8 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 10:53:07 -0700 Subject: [PATCH 04/29] docs(backlog): promote top debt into up-next --- .../{bad-code => up-next}/PROCESS_bearing-truthfulness.md | 2 +- .../PROCESS_generated-doc-scaffold-contract.md | 2 +- .../PROCESS_live-legend-definition-coverage.md | 2 +- .../{bad-code => up-next}/PROCESS_mcp-tool-result-contract.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename docs/method/backlog/{bad-code => up-next}/PROCESS_bearing-truthfulness.md (97%) rename docs/method/backlog/{bad-code => up-next}/PROCESS_generated-doc-scaffold-contract.md (97%) rename docs/method/backlog/{bad-code => up-next}/PROCESS_live-legend-definition-coverage.md (97%) rename docs/method/backlog/{bad-code => up-next}/PROCESS_mcp-tool-result-contract.md (97%) diff --git a/docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md b/docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md similarity index 97% rename from docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md rename to docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md index 94d42f9..bceedd4 100644 --- a/docs/method/backlog/bad-code/PROCESS_bearing-truthfulness.md +++ b/docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md @@ -1,7 +1,7 @@ --- title: "BEARING Truthfulness" legend: PROCESS -lane: bad-code +lane: up-next --- # BEARING Truthfulness diff --git a/docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md b/docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md similarity index 97% rename from docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md rename to docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md index 1062999..90342bf 100644 --- a/docs/method/backlog/bad-code/PROCESS_generated-doc-scaffold-contract.md +++ b/docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md @@ -1,7 +1,7 @@ --- title: "Generated Doc Scaffold Contract" legend: PROCESS -lane: bad-code +lane: up-next --- # Generated Doc Scaffold Contract diff --git a/docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md b/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md similarity index 97% rename from docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md rename to docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md index c7003a4..485fa39 100644 --- a/docs/method/backlog/bad-code/PROCESS_live-legend-definition-coverage.md +++ b/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md @@ -1,7 +1,7 @@ --- title: "Live Legend Definition Coverage" legend: PROCESS -lane: bad-code +lane: up-next --- # Live Legend Definition Coverage diff --git a/docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md b/docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md similarity index 97% rename from docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md rename to docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md index dd441c4..591cd07 100644 --- a/docs/method/backlog/bad-code/PROCESS_mcp-tool-result-contract.md +++ b/docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md @@ -1,7 +1,7 @@ --- title: "MCP Tool Result Contract" legend: PROCESS -lane: bad-code +lane: up-next --- # MCP Tool Result Contract From cd87206a5d01adcdb384d7f08bc674743545d12a Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 10:59:25 -0700 Subject: [PATCH 05/29] docs(backlog): prioritize bad-code follow-ons --- docs/method/backlog/asap/.gitkeep | 1 - .../PROCESS_backlog-metadata-single-source-of-truth.md | 2 +- .../backlog/{up-next => asap}/PROCESS_bearing-truthfulness.md | 2 +- .../PROCESS_generated-doc-scaffold-contract.md | 2 +- .../{up-next => asap}/PROCESS_mcp-tool-result-contract.md | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 docs/method/backlog/asap/.gitkeep rename docs/method/backlog/{bad-code => asap}/PROCESS_backlog-metadata-single-source-of-truth.md (98%) rename docs/method/backlog/{up-next => asap}/PROCESS_bearing-truthfulness.md (97%) rename docs/method/backlog/{up-next => asap}/PROCESS_generated-doc-scaffold-contract.md (97%) rename docs/method/backlog/{up-next => asap}/PROCESS_mcp-tool-result-contract.md (97%) diff --git a/docs/method/backlog/asap/.gitkeep b/docs/method/backlog/asap/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/docs/method/backlog/asap/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md b/docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md similarity index 98% rename from docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md rename to docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md index 7ff5e7d..70a4bf7 100644 --- a/docs/method/backlog/bad-code/PROCESS_backlog-metadata-single-source-of-truth.md +++ b/docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md @@ -1,7 +1,7 @@ --- title: "Backlog Metadata Single Source Of Truth" legend: PROCESS -lane: bad-code +lane: asap --- # Backlog Metadata Single Source Of Truth diff --git a/docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md b/docs/method/backlog/asap/PROCESS_bearing-truthfulness.md similarity index 97% rename from docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md rename to docs/method/backlog/asap/PROCESS_bearing-truthfulness.md index bceedd4..f593776 100644 --- a/docs/method/backlog/up-next/PROCESS_bearing-truthfulness.md +++ b/docs/method/backlog/asap/PROCESS_bearing-truthfulness.md @@ -1,7 +1,7 @@ --- title: "BEARING Truthfulness" legend: PROCESS -lane: up-next +lane: asap --- # BEARING Truthfulness diff --git a/docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md b/docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md similarity index 97% rename from docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md rename to docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md index 90342bf..d986859 100644 --- a/docs/method/backlog/up-next/PROCESS_generated-doc-scaffold-contract.md +++ b/docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md @@ -1,7 +1,7 @@ --- title: "Generated Doc Scaffold Contract" legend: PROCESS -lane: up-next +lane: asap --- # Generated Doc Scaffold Contract diff --git a/docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md b/docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md similarity index 97% rename from docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md rename to docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md index 591cd07..84f60c1 100644 --- a/docs/method/backlog/up-next/PROCESS_mcp-tool-result-contract.md +++ b/docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md @@ -1,7 +1,7 @@ --- title: "MCP Tool Result Contract" legend: PROCESS -lane: up-next +lane: asap --- # MCP Tool Result Contract From 7b89095a1cb3264b5d9a2fceae0ec893426fbcae Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 11:14:46 -0700 Subject: [PATCH 06/29] fix(backlog): make metadata frontmatter-first --- ...backlog-metadata-single-source-of-truth.md | 96 ++++++++++++++ ...backlog-metadata-single-source-of-truth.md | 24 ---- .../PROCESS_legend-audit-and-assignment.md | 1 + .../PROCESS_retro-conversational-closeout.md | 1 + .../PROCESS_review-config-hardening.md | 1 + ...rtifact-history-and-semantic-provenance.md | 1 + .../cool-ideas/SYNTH_cycle-witness-command.md | 1 + .../inbox/PROCESS_i18n-string-extraction.md | 1 + .../inbox/PROCESS_interactive-scaffolder.md | 1 + .../inbox/PROCESS_multi-forge-adapter.md | 1 + .../inbox/PROCESS_semantic-drift-detector.md | 1 + src/index.ts | 91 +++++++++---- tests/api.test.ts | 122 +++++++++++++++++- tests/github-adapter.test.ts | 1 + tests/ship-sync.test.ts | 7 +- 15 files changed, 294 insertions(+), 56 deletions(-) create mode 100644 docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md delete mode 100644 docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md diff --git a/docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md b/docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md new file mode 100644 index 0000000..5ca016b --- /dev/null +++ b/docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md @@ -0,0 +1,96 @@ +--- +title: "Backlog Metadata Single Source Of Truth" +legend: PROCESS +cycle: "0030-backlog-metadata-single-source-of-truth" +source_backlog: "docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md" +--- + +# Backlog Metadata Single Source Of Truth + +Source backlog item: `docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md` +Legend: PROCESS + +## Sponsors + +- Human: Backlog operator who needs lane and legend moves to stay honest + without manually repairing YAML after every triage action. +- Agent: Automation and MCP clients that need one cheap, stable source of + backlog truth instead of inferring metadata from filenames and + directory shape. + +## Hill + +Backlog metadata becomes frontmatter-first. `status`, `pull`, lane +moves, and legend health all resolve `legend` and `lane` from YAML when +present, while compatibility fallbacks keep older cards readable until +they are backfilled. Managed moves should repair missing lane metadata +instead of preserving drift. + +## Playback Questions + +### Human + +- [ ] If a backlog card's filename or directory disagrees with its YAML, + does `method status` report the YAML lane and legend rather than + the path-derived guess? +- [ ] If I move a legacy backlog card between lanes, does METHOD + backfill lane metadata so the card no longer depends on path + inference? + +### Agent + +- [ ] Can I consume backlog lane and legend from `Workspace.status()` + without reverse-engineering filename prefixes or folder names? +- [ ] If a backlog card has stale filename legend text, does `method pull` + still carry the YAML legend into the design packet? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: backlog metadata should be + explicit in one place so humans and agents do not need to cross-check + path, filename, and YAML to learn the current state. +- Non-visual or alternate-reading expectations: metadata must stay + machine-readable and linearly inspectable from the document header. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: legend codes remain stable, + uppercase repo-local identifiers independent of prose wording. +- Logical direction / layout assumptions: no layout dependence; the + solution lives in YAML keys and deterministic CLI/API output. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: lane and legend + resolution order, compatibility fallback behavior, and move-time + metadata repair. +- What must be attributable, evidenced, or governed: tests should prove + that frontmatter wins over filenames and directories, and that active + legend health can read design doc frontmatter. + +## Non-goals + +- [ ] Redesign backlog filenames or cycle slug generation in this cycle. +- [ ] Eliminate every legacy fallback path before the repo backlog docs + have been backfilled. +- [ ] Replace the generated design or retro scaffolds; that remains + separate debt under `PROCESS_generated-doc-scaffold-contract`. + +## Backlog Context + +Backlog metadata is still split across filename prefixes and YAML +frontmatter. `captureIdea` writes `legend` and `lane` into frontmatter, +but `collectBacklogItems`, `calculateLegendHealth`, and `pullItem` still +derive legend from the filename stem, while `moveBacklogItem` renames the +file without updating frontmatter metadata. That creates silent drift: +the path can say one thing, the YAML can say another, and status/MCP +surfaces may report whichever source they happen to trust. + +This is bad code because the repo now has multiple authorities for the +same facts. It is especially hostile to agents, which need one cheap, +stable, machine-readable source instead of filename heuristics plus +special-case inference. + +The fix should make frontmatter canonical for backlog metadata, keep +filenames as convenience or display structure, and ensure move/pull/ +status/audit flows preserve and validate the YAML fields. diff --git a/docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md b/docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md deleted file mode 100644 index 70a4bf7..0000000 --- a/docs/method/backlog/asap/PROCESS_backlog-metadata-single-source-of-truth.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -title: "Backlog Metadata Single Source Of Truth" -legend: PROCESS -lane: asap ---- - -# Backlog Metadata Single Source Of Truth - -Backlog metadata is still split across filename prefixes and YAML -frontmatter. `captureIdea` writes `legend` and `lane` into frontmatter, -but `collectBacklogItems`, `calculateLegendHealth`, and `pullItem` still -derive legend from the filename stem, while `moveBacklogItem` renames the -file without updating frontmatter metadata. That creates silent drift: -the path can say one thing, the YAML can say another, and status/MCP -surfaces may report whichever source they happen to trust. - -This is bad code because the repo now has multiple authorities for the -same facts. It is especially hostile to agents, which need one cheap, -stable, machine-readable source instead of filename heuristics plus -special-case inference. - -The fix should make frontmatter canonical for backlog metadata, keep -filenames as convenience or display structure, and ensure move/pull/ -status/audit flows preserve and validate the YAML fields. diff --git a/docs/method/backlog/cool-ideas/PROCESS_legend-audit-and-assignment.md b/docs/method/backlog/cool-ideas/PROCESS_legend-audit-and-assignment.md index 9c8fe5d..691e897 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_legend-audit-and-assignment.md +++ b/docs/method/backlog/cool-ideas/PROCESS_legend-audit-and-assignment.md @@ -1,6 +1,7 @@ --- title: "Legend Audit And Assignment" legend: PROCESS +lane: cool-ideas --- Legend management needs first-class tooling: create legends, assign diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-conversational-closeout.md b/docs/method/backlog/cool-ideas/PROCESS_retro-conversational-closeout.md index 10772af..52aabdc 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_retro-conversational-closeout.md +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-conversational-closeout.md @@ -1,6 +1,7 @@ --- title: "Retro conversational closeout" legend: PROCESS +lane: cool-ideas --- METHOD's `close` flow currently handles the mechanical part of a retro: diff --git a/docs/method/backlog/cool-ideas/PROCESS_review-config-hardening.md b/docs/method/backlog/cool-ideas/PROCESS_review-config-hardening.md index 9a2661a..28026ef 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_review-config-hardening.md +++ b/docs/method/backlog/cool-ideas/PROCESS_review-config-hardening.md @@ -1,6 +1,7 @@ --- title: "Review Config Hardening" legend: PROCESS +lane: cool-ideas --- Harden repo review configuration so automated review tools understand diff --git a/docs/method/backlog/cool-ideas/SYNTH_artifact-history-and-semantic-provenance.md b/docs/method/backlog/cool-ideas/SYNTH_artifact-history-and-semantic-provenance.md index 5bc9b27..ffd5502 100644 --- a/docs/method/backlog/cool-ideas/SYNTH_artifact-history-and-semantic-provenance.md +++ b/docs/method/backlog/cool-ideas/SYNTH_artifact-history-and-semantic-provenance.md @@ -1,6 +1,7 @@ --- title: "Artifact History And Semantic Provenance" legend: SYNTH +lane: cool-ideas --- Define two provenance levels for METHOD instead of treating everything diff --git a/docs/method/backlog/cool-ideas/SYNTH_cycle-witness-command.md b/docs/method/backlog/cool-ideas/SYNTH_cycle-witness-command.md index 9c17542..a8a109d 100644 --- a/docs/method/backlog/cool-ideas/SYNTH_cycle-witness-command.md +++ b/docs/method/backlog/cool-ideas/SYNTH_cycle-witness-command.md @@ -1,6 +1,7 @@ --- title: "Cycle Witness Command" legend: SYNTH +lane: cool-ideas --- Explore a `method witness` command that can package the rerunnable diff --git a/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md b/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md index b081188..67a8c75 100644 --- a/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md +++ b/docs/method/backlog/inbox/PROCESS_i18n-string-extraction.md @@ -1,6 +1,7 @@ --- title: "i18n String Extraction" legend: PROCESS +lane: inbox --- # i18n String Extraction diff --git a/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md b/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md index 626707b..b3c8428 100644 --- a/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md +++ b/docs/method/backlog/inbox/PROCESS_interactive-scaffolder.md @@ -1,6 +1,7 @@ --- title: "Interactive Scaffolder" legend: PROCESS +lane: inbox --- # Interactive Scaffolder diff --git a/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md b/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md index 9e8503e..9ee6daa 100644 --- a/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md +++ b/docs/method/backlog/inbox/PROCESS_multi-forge-adapter.md @@ -1,6 +1,7 @@ --- title: "Multi-Forge Adapter" legend: PROCESS +lane: inbox --- # Multi-Forge Adapter diff --git a/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md index 4f606d5..a88c070 100644 --- a/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md +++ b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md @@ -1,6 +1,7 @@ --- title: "Semantic Drift Detector" legend: PROCESS +lane: inbox --- # Semantic Drift Detector diff --git a/src/index.ts b/src/index.ts index f011e90..bd91b12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -169,7 +169,7 @@ export class Workspace { throw new MethodError(`${relative(this.root, backlogItem)} is missing a heading.`); } - const { legend, slug } = splitLegend(fileStem(backlogItem)); + const { legend, slug } = this.readBacklogItem(backlogItem, inferBacklogLane(this.paths.backlog, backlogItem)); const cycleName = `${String(this.nextCycleNumber()).padStart(4, '0')}-${slug}`; const designDir = resolve(this.paths.design, cycleName); const designDoc = resolve(designDir, `${slug}.md`); @@ -311,13 +311,13 @@ export class Workspace { root: [], }; - for (const lane of LANES) { - backlog[lane] = this.collectBacklogItems(resolve(this.paths.backlog, lane), lane); + const backlogItems = this.collectBacklogItems(); + for (const item of backlogItems) { + backlog[item.lane].push(item); } - backlog.root = this.collectBacklogItems(this.paths.backlog, 'root', false); const activeCycles = this.openCycles(); - const legendHealth = this.calculateLegendHealth(activeCycles); + const legendHealth = this.calculateLegendHealth(backlogItems, activeCycles); return { root: this.root, @@ -345,6 +345,7 @@ export class Workspace { throw new MethodError(`Backlog item not found: ${path}`); } + const metadata = this.readBacklogItem(fullPath, inferBacklogLane(this.paths.backlog, fullPath)); const fileName = basename(fullPath); let targetDir: string; if (targetLane === 'graveyard') { @@ -367,6 +368,11 @@ export class Workspace { } renameSync(fullPath, targetPath); + const frontmatterUpdates: Record = { lane: targetLane }; + if (metadata.legend !== undefined) { + frontmatterUpdates.legend = metadata.legend; + } + fmUpdateFrontmatter(targetPath, frontmatterUpdates); return relative(this.root, targetPath); } @@ -480,32 +486,33 @@ export class Workspace { throw new MethodError(`Could not find active cycle ${JSON.stringify(cycleName)}.`); } - private collectBacklogItems(dir: string, lane: Lane | 'root', recursive = true): BacklogItem[] { - if (!existsSync(dir)) { + private collectBacklogItems(): BacklogItem[] { + if (!existsSync(this.paths.backlog)) { return []; } - const files = recursive ? collectMarkdownFiles(dir) : readdirSync(dir, { withFileTypes: true }) - .filter(e => e.isFile() && e.name.endsWith('.md')) - .map(e => resolve(dir, e.name)); - - return files.map(file => { - const stem = fileStem(file); - const { legend, slug } = splitLegend(stem); - return { - stem, - lane, - path: relative(this.root, file), - legend, - slug - }; - }); + + return collectMarkdownFiles(this.paths.backlog).map((file) => + this.readBacklogItem(file, inferBacklogLane(this.paths.backlog, file))); + } + + private readBacklogItem(path: string, fallbackLane: Lane | 'root'): BacklogItem { + const stem = fileStem(path); + const filenameMetadata = splitLegend(stem); + const frontmatter = fmReadFrontmatter(path); + + return { + stem, + lane: normalizeBacklogLane(frontmatter.lane) ?? fallbackLane, + path: relative(this.root, path), + legend: normalizeLegend(frontmatter.legend) ?? filenameMetadata.legend, + slug: filenameMetadata.slug, + }; } - private calculateLegendHealth(activeCycles: readonly Cycle[]): LegendHealth[] { + private calculateLegendHealth(backlogItems: readonly BacklogItem[], activeCycles: readonly Cycle[]): LegendHealth[] { const counts = new Map(); - for (const file of collectMarkdownFiles(this.paths.backlog)) { - const { legend } = splitLegend(fileStem(file)); - const key = legend ?? 'untagged'; + for (const item of backlogItems) { + const key = item.legend ?? 'untagged'; const current = counts.get(key) ?? { backlog: 0, active: 0 }; current.backlog += 1; counts.set(key, current); @@ -580,6 +587,11 @@ export const readHeading = fmReadHeading; export const readBody = fmReadBody; function readDesignLegend(path: string): string | undefined { + const frontmatterLegend = normalizeLegend(fmReadFrontmatter(path).legend); + if (frontmatterLegend !== undefined) { + return frontmatterLegend === 'NONE' ? undefined : frontmatterLegend; + } + for (const line of readFileSync(path, 'utf8').split(/\r?\n/u)) { if (!line.startsWith('Legend: ')) { continue; @@ -602,3 +614,30 @@ function fileStem(path: string): string { const name = basename(path); return name.endsWith('.md') ? name.slice(0, -3) : name; } + +function inferBacklogLane(backlogRoot: string, path: string): Lane | 'root' { + const [firstSegment] = relative(backlogRoot, path).split(/[\\/]/u); + if (firstSegment !== undefined && (LANES as readonly string[]).includes(firstSegment)) { + return firstSegment as Lane; + } + return 'root'; +} + +function normalizeBacklogLane(value: string | undefined): Lane | 'root' | undefined { + const normalized = value?.trim(); + if (normalized === undefined || normalized.length === 0) { + return undefined; + } + if (normalized === 'root') { + return normalized; + } + if ((LANES as readonly string[]).includes(normalized)) { + return normalized as Lane; + } + return undefined; +} + +function normalizeLegend(value: string | undefined): string | undefined { + const normalized = value?.trim().toUpperCase(); + return normalized === undefined || normalized.length === 0 ? undefined : normalized; +} diff --git a/tests/api.test.ts b/tests/api.test.ts index e1dbf1a..615d8f7 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, rmSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -64,6 +64,126 @@ describe('Method API', () => { expect(statusAfter.activeCycles[0].name).toBe(cycle.name); }); + it('treats backlog frontmatter as the canonical source for lane and legend metadata', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/inbox/PROCESS_frontmatter-wins.md'), + [ + '---', + 'title: "Frontmatter Wins"', + 'legend: SYNTH', + 'lane: asap', + '---', + '', + '# Frontmatter Wins', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const status = workspace.status(); + expect(status.backlog.inbox).toEqual([]); + expect(status.backlog.asap).toContainEqual({ + stem: 'PROCESS_frontmatter-wins', + lane: 'asap', + path: 'docs/method/backlog/inbox/PROCESS_frontmatter-wins.md', + legend: 'SYNTH', + slug: 'frontmatter-wins', + }); + expect(status.legendHealth).toContainEqual({ + legend: 'SYNTH', + backlog: 1, + active: 0, + }); + expect(status.legendHealth.find((entry) => entry.legend === 'PROCESS')).toBeUndefined(); + }); + + it('backfills canonical frontmatter when moving legacy backlog items between lanes', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/inbox/PROCESS_legacy-item.md'), + '# Legacy Item\n\nBody\n', + 'utf8', + ); + + const moved = workspace.moveBacklogItem( + 'docs/method/backlog/inbox/PROCESS_legacy-item.md', + 'up-next', + ); + + expect(moved).toBe('docs/method/backlog/up-next/PROCESS_legacy-item.md'); + expect(workspace.readFrontmatter(moved)).toMatchObject({ + lane: 'up-next', + legend: 'PROCESS', + }); + }); + + it('uses backlog frontmatter legend when pulling items into cycles', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_frontmatter-pull.md'), + [ + '---', + 'title: "Frontmatter Pull"', + 'legend: SYNTH', + 'lane: asap', + '---', + '', + '# Frontmatter Pull', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const cycle = workspace.pullItem('PROCESS_frontmatter-pull'); + expect(cycle.name).toBe('0001-frontmatter-pull'); + expect(readFileSync(cycle.designDoc, 'utf8')).toContain('Legend: SYNTH'); + }); + + it('counts active cycle legend health from design frontmatter before body text', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + const cycleDir = join(root, 'docs/design/0001-frontmatter-design'); + const designDoc = join(cycleDir, 'frontmatter-design.md'); + mkdirSync(cycleDir, { recursive: true }); + writeFileSync( + designDoc, + [ + '---', + 'title: "Frontmatter Design"', + 'legend: PROCESS', + '---', + '', + '# Frontmatter Design', + '', + '## Hill', + '', + 'TBD', + ].join('\n'), + 'utf8', + ); + + const status = workspace.status(); + expect(status.legendHealth).toContainEqual({ + legend: 'PROCESS', + backlog: 0, + active: 1, + }); + }); + it('All existing `tests/cli.test.ts` pass, proving no regressions in the command-line adapter.', () => { // This playback claim is verified by running the full test suite. }); diff --git a/tests/github-adapter.test.ts b/tests/github-adapter.test.ts index bec486d..9b503d7 100644 --- a/tests/github-adapter.test.ts +++ b/tests/github-adapter.test.ts @@ -171,5 +171,6 @@ describe('GitHub Adapter Two-way Sync', () => { expect(existsSync(graveyardPath), 'file should exist in graveyard').toBe(true); expect(existsSync(inboxPath), 'file should no longer exist in inbox').toBe(false); expect(readFileSync(graveyardPath, 'utf8')).toContain('Closed Item'); + expect(workspace.readFrontmatter('docs/method/graveyard/FEAT_closed.md').lane).toBe('graveyard'); }); }); diff --git a/tests/ship-sync.test.ts b/tests/ship-sync.test.ts index a26d1f0..2de2195 100644 --- a/tests/ship-sync.test.ts +++ b/tests/ship-sync.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync, renameSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; @@ -49,10 +49,7 @@ describe('Ship Sync', () => { // Create a backlog item and move it to up-next workspace.captureIdea('Next priority', 'FEAT', 'Up Next'); - renameSync( - join(root, 'docs/method/backlog/inbox/FEAT_up-next.md'), - join(root, 'docs/method/backlog/up-next/FEAT_up-next.md') - ); + workspace.moveBacklogItem('docs/method/backlog/inbox/FEAT_up-next.md', 'up-next'); const itemPath = workspace.captureIdea('Feature Z', 'FEAT', 'Just Shipped'); const cycle = workspace.pullItem('FEAT_just-shipped'); From 5463e831aff31fdf67e7dd2bea1e2ed871ba16fc Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 12:46:32 -0700 Subject: [PATCH 07/29] fix(scaffold): make generated docs contract-valid --- .../generated-doc-scaffold-contract.md | 86 ++++++++++++++++++ ...PROCESS_generated-doc-scaffold-contract.md | 17 ---- ...backlog-metadata-single-source-of-truth.md | 44 +++++++++ .../witness/verification.md | 34 +++++++ .../generated-doc-scaffold-contract.md | 46 ++++++++++ .../witness/verification.md | 34 +++++++ src/cli.ts | 7 +- src/index.ts | 17 +++- src/renderers.ts | 31 ++++++- tests/api.test.ts | 8 +- tests/cli.test.ts | 91 ++++++++++++++++++- 11 files changed, 382 insertions(+), 33 deletions(-) create mode 100644 docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md delete mode 100644 docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md create mode 100644 docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md create mode 100644 docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md create mode 100644 docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md create mode 100644 docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md diff --git a/docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md b/docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md new file mode 100644 index 0000000..58f1e91 --- /dev/null +++ b/docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md @@ -0,0 +1,86 @@ +--- +title: "Generated Doc Scaffold Contract" +legend: PROCESS +cycle: "0031-generated-doc-scaffold-contract" +source_backlog: "docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md" +--- + +# Generated Doc Scaffold Contract + +Source backlog item: `docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md` +Legend: PROCESS + +## Sponsors + +- Human: Cycle operator +- Agent: Scaffold repair agent + +## Hill + +`method pull` and `method close` generate design and retro docs that +already satisfy the repo's committed document contract. A fresh cycle +packet should not require manual frontmatter surgery just to survive the +docs suite or honest review. + +## Playback Questions + +### Human + +- [ ] Does `method pull` create a design doc that already includes + required frontmatter fields like `title`, `legend`, `cycle`, and + `source_backlog`? +- [ ] Does `method close` create a retro doc that already includes + required frontmatter fields like `title`, `outcome`, + `drift_check`, and `design_doc`? + +### Agent + +- [ ] Can I rely on scaffolded design docs to satisfy the repo docs + contract without manual patching? +- [ ] Can I rely on scaffolded retro docs to satisfy the repo docs + contract without post-close repairs? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: generated docs should place + critical metadata in YAML so readers do not need to infer validity + from body prose or repair steps. +- Non-visual or alternate-reading expectations: the scaffold contract + must stay explicit and machine-readable from the first lines of the + document. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: scaffold metadata keys stay + stable and deterministic regardless of the prose in the body. +- Logical direction / layout assumptions: no layout-specific behavior; + validity depends on frontmatter and predictable section structure. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: which frontmatter + keys are emitted for design and retro docs, and that close-time output + does not require manual cleanup before commit. +- What must be attributable, evidenced, or governed: CLI tests should + prove scaffolded docs meet the repo contract, and the existing cycle + docs produced before the fix should be repaired on-branch. + +## Non-goals + +- [ ] Redesign the substantive content of every historical design or + retro doc. +- [ ] Solve broader signpost truth drift such as `BEARING.md` claims. +- [ ] Redesign witness capture output beyond keeping the generated docs + commit-safe. + +## Backlog Context + +`method pull` and `method close` still generate design and retro +documents with older scaffold shapes that do not match the repo's +current frontmatter contract. The committed docs suite enforces YAML +frontmatter and required fields on real docs, but the command-generated +templates do not yet guarantee those same invariants. + +That means METHOD can generate docs that its own repo doctrine would +reject if they were committed. The scaffolders and the tests need to be +brought back into parity so generated docs are valid by construction. diff --git a/docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md b/docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md deleted file mode 100644 index d986859..0000000 --- a/docs/method/backlog/asap/PROCESS_generated-doc-scaffold-contract.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "Generated Doc Scaffold Contract" -legend: PROCESS -lane: asap ---- - -# Generated Doc Scaffold Contract - -`method pull` and `method close` still generate design and retro -documents with older scaffold shapes that do not match the repo's -current frontmatter contract. The committed docs suite enforces YAML -frontmatter and required fields on real docs, but the command-generated -templates do not yet guarantee those same invariants. - -That means METHOD can generate docs that its own repo doctrine would -reject if they were committed. The scaffolders and the tests need to be -brought back into parity so generated docs are valid by construction. diff --git a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md new file mode 100644 index 0000000..73f305f --- /dev/null +++ b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md @@ -0,0 +1,44 @@ +--- +title: "Backlog Metadata Single Source Of Truth" +cycle: "0030-backlog-metadata-single-source-of-truth" +design_doc: "docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md" +outcome: hill-met +drift_check: yes +--- + +# Backlog Metadata Single Source Of Truth Retro + +Design: `docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md` +Outcome: hill-met +Drift check: yes + +## Summary + +This cycle made backlog metadata frontmatter-first across the live +workspace surfaces. `status`, `pull`, lane moves, and legend health now +prefer YAML metadata, managed moves repair missing lane metadata, and +the repo backlog was backfilled so the remaining cards stop depending on +directory-only truth. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- `method close` generated this retro without the required frontmatter, + which directly triggered follow-on cycle `0031-generated-doc-scaffold-contract`. + +## Cool Ideas + +- None recorded. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md new file mode 100644 index 0000000..7904711 --- /dev/null +++ b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md @@ -0,0 +1,34 @@ +--- +title: "Verification Witness for Cycle 30" +--- + +# Verification Witness for Cycle 30 + +This witness proves that `Backlog Metadata Single Source Of Truth` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +``` +> method@0.3.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 . + + + Test Files 12 passed (12) + Tests 131 passed (131) + Start at 12:38:25 + Duration 942ms (transform 1.21s, setup 0ms, import 3.30s, tests 1.15s, environment 1ms) +``` + +## Drift Results + +``` + +``` + +## Manual Verification + +- [x] Automated capture completed successfully. diff --git a/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md b/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md new file mode 100644 index 0000000..7c8c112 --- /dev/null +++ b/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md @@ -0,0 +1,46 @@ +--- +title: "Generated Doc Scaffold Contract" +cycle: "0031-generated-doc-scaffold-contract" +design_doc: "docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md" +outcome: hill-met +drift_check: yes +--- + +# Generated Doc Scaffold Contract Retro + +Design: `docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md` +Outcome: hill-met +Drift check: yes + +## Summary + +This cycle brought METHOD's generated design and retro scaffolds back +into parity with the repo's committed document contract. `method pull` +now emits design docs with the required frontmatter and non-TBD sponsor +roles, `method close` emits retro docs with commit-safe frontmatter, +close-time witness output now sanitizes personal absolute paths, and the +CLI tests now lock those scaffold guarantees in place. The already +generated `0030` retro and `0031` design packet were repaired on-branch +so the repo no longer depends on manual cleanup after generation. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- None recorded. + +## Cool Ideas + +- None recorded. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md b/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md new file mode 100644 index 0000000..7f308be --- /dev/null +++ b/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md @@ -0,0 +1,34 @@ +--- +title: "Verification Witness for Cycle 31" +--- + +# Verification Witness for Cycle 31 + +This witness proves that `Generated Doc Scaffold Contract` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +``` +> method@0.3.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 . + + + Test Files 12 passed (12) + Tests 133 passed (133) + Start at 12:45:32 + Duration 777ms (transform 757ms, setup 0ms, import 2.43s, tests 960ms, environment 1ms) +``` + +## Drift Results + +``` + +``` + +## Manual Verification + +- [x] Automated capture completed successfully. diff --git a/src/cli.ts b/src/cli.ts index 77d9098..1637727 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,7 +72,12 @@ export async function runCli( const completedDriftCheck = parsed.driftCheck === undefined ? await promptConfirm({ title: 'Drift check complete?', defaultValue: false }) : parsed.driftCheck === 'yes'; - const cycle = await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome); + if (!completedDriftCheck) { + await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome ?? 'partial'); + } else if (parsed.outcome === undefined) { + throw new Error('Outcome is required. Pass --outcome hill-met|partial|not-met.'); + } + const cycle = await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome ?? 'partial'); stdout.write(`${alert(`Closed ${cycle.name}`, { variant: 'success', ctx })}\n`); stdout.write(`${relative(root, cycle.retroDoc)}\n`); return 0; diff --git a/src/index.ts b/src/index.ts index bd91b12..7e2414b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -181,6 +181,7 @@ export class Workspace { writeFileSync( designDoc, renderDesignDoc({ + cycleName, title, legend, source: relative(this.root, backlogItem), @@ -199,7 +200,7 @@ export class Workspace { }; } - async closeCycle(cycleName: string | undefined, completedDriftCheck: boolean, outcome?: Outcome): Promise { + async closeCycle(cycleName: string | undefined, completedDriftCheck: boolean, outcome: Outcome): Promise { if (!completedDriftCheck) { throw new MethodError('Cannot close a cycle without completing the drift check.'); } @@ -288,8 +289,8 @@ export class Workspace { mkdirSync(dirname(witnessPath), { recursive: true }); - const testResult = await this.execCommand('npm', ['test']); - const driftResult = await this.execCommand('tsx', ['src/cli.ts', 'drift', cycle.name]); + const testResult = sanitizeWitnessOutput(await this.execCommand('npm', ['test']), this.root); + const driftResult = sanitizeWitnessOutput(await this.execCommand('tsx', ['src/cli.ts', 'drift', cycle.name]), this.root); const content = renderWitnessDoc({ cycle, @@ -641,3 +642,13 @@ function normalizeLegend(value: string | undefined): string | undefined { const normalized = value?.trim().toUpperCase(); return normalized === undefined || normalized.length === 0 ? undefined : normalized; } + +function sanitizeWitnessOutput(value: string, root: string): string { + return value + .replaceAll(root, '.') + .replace(/\/Users\/[^/\s]+/gu, '') + .replace(/\/home\/[^/\s]+/gu, '') + .replace(/\/root\b/gu, '') + .replace(/\/mnt\/[^/\s]+/gu, '') + .replace(/C:\\Users\\[^\\\s]+/gu, ''); +} diff --git a/src/renderers.ts b/src/renderers.ts index 4020334..054f468 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -87,6 +87,7 @@ export function renderWitnessDoc(options: { } export function renderDesignDoc(options: { + cycleName: string; title: string; legend?: string; source: string; @@ -94,6 +95,13 @@ export function renderDesignDoc(options: { }): string { const legendValue = options.legend ?? 'none'; return [ + '---', + `title: ${yamlString(options.title)}`, + `legend: ${legendValue}`, + `cycle: ${yamlString(options.cycleName)}`, + `source_backlog: ${yamlString(options.source)}`, + '---', + '', `# ${options.title}`, '', `Source backlog item: \`${options.source}\``, @@ -101,8 +109,8 @@ export function renderDesignDoc(options: { '', '## Sponsors', '', - '- Human: TBD', - '- Agent: TBD', + '- Human: Backlog operator', + '- Agent: Implementation agent', '', '## Hill', '', @@ -147,15 +155,24 @@ export function renderDesignDoc(options: { export function renderRetroDoc(options: { cycle: Cycle; root: string; - outcome?: Outcome; + outcome: Outcome; witnessDir: string; }): string { const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); + const designDoc = relative(options.root, options.cycle.designDoc); return [ + '---', + `title: ${yamlString(title)}`, + `cycle: ${yamlString(options.cycle.name)}`, + `design_doc: ${yamlString(designDoc)}`, + `outcome: ${options.outcome}`, + 'drift_check: yes', + '---', + '', `# ${title} Retro`, '', - `Design: \`${relative(options.root, options.cycle.designDoc)}\``, - `Outcome: ${options.outcome ?? 'TBD'}`, + `Design: \`${designDoc}\``, + `Outcome: ${options.outcome}`, 'Drift check: yes', '', '## Summary', @@ -186,3 +203,7 @@ export function renderRetroDoc(options: { '', ].join('\n'); } + +function yamlString(value: string): string { + return `"${value.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"`; +} diff --git a/tests/api.test.ts b/tests/api.test.ts index 615d8f7..26dfb1c 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -64,7 +64,7 @@ describe('Method API', () => { expect(statusAfter.activeCycles[0].name).toBe(cycle.name); }); - it('treats backlog frontmatter as the canonical source for lane and legend metadata', () => { + it("If a backlog card's filename or directory disagrees with its YAML, does `method status` report the YAML lane and legend rather than the path-derived guess?", () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); @@ -102,7 +102,7 @@ describe('Method API', () => { expect(status.legendHealth.find((entry) => entry.legend === 'PROCESS')).toBeUndefined(); }); - it('backfills canonical frontmatter when moving legacy backlog items between lanes', () => { + it('If I move a legacy backlog card between lanes, does METHOD backfill lane metadata so the card no longer depends on path inference?', () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); @@ -125,7 +125,7 @@ describe('Method API', () => { }); }); - it('uses backlog frontmatter legend when pulling items into cycles', () => { + it('If a backlog card has stale filename legend text, does `method pull` still carry the YAML legend into the design packet?', () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); @@ -151,7 +151,7 @@ describe('Method API', () => { expect(readFileSync(cycle.designDoc, 'utf8')).toContain('Legend: SYNTH'); }); - it('counts active cycle legend health from design frontmatter before body text', () => { + it('Can I consume backlog lane and legend from `Workspace.status()` without reverse-engineering filename prefixes or folder names?', () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index a9a05ec..d66b53f 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -95,7 +95,7 @@ describe('method CLI', () => { expectFile(root, 'docs/method/backlog/inbox/backfill-the-missing-inbox-lane.md'); }); - it('promotes backlog items into numbered cycles', async () => { + it('Does `method pull` create a design doc that already includes required frontmatter fields like `title`, `legend`, `cycle`, and `source_backlog`?', async () => { const root = createTempRoot(); await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); writeFileSync( @@ -115,14 +115,21 @@ describe('method CLI', () => { expect(stdout.output).toContain('Pulled into 0001-strand-lifecycle'); expectFile(root, 'docs/design/0001-strand-lifecycle/strand-lifecycle.md'); const designDoc = readFile(root, 'docs/design/0001-strand-lifecycle/strand-lifecycle.md'); + expect(designDoc).toMatch(/^---\n[\s\S]+?\n---\n/u); + expect(designDoc).toMatch(/^title:\s+"Strand Lifecycle"$/mu); + expect(designDoc).toMatch(/^legend:\s+PROTO$/mu); + expect(designDoc).toMatch(/^cycle:\s+"0001-strand-lifecycle"$/mu); + expect(designDoc).toMatch(/^source_backlog:\s+"docs\/method\/backlog\/asap\/PROTO_strand-lifecycle\.md"$/mu); expect(designDoc).toContain('Legend: PROTO'); + expect(designDoc).toContain('- Human: Backlog operator'); + expect(designDoc).toContain('- Agent: Implementation agent'); expect(designDoc).toContain('Mechanical state transitions for strands.'); expect(designDoc).toContain('## Accessibility and Assistive Reading'); expect(designDoc).toContain('## Localization and Directionality'); expect(designDoc).toContain('## Agent Inspectability and Explainability'); }); - it('writes retros and witness directories on close', async () => { + it('Does `method close` create a retro doc that already includes required frontmatter fields like `title`, `outcome`, `drift_check`, and `design_doc`?', async () => { const root = createTempRoot(); await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); mkdirSync(join(root, 'docs/design/0001-method-cli'), { recursive: true }); @@ -143,7 +150,85 @@ describe('method CLI', () => { expect(stdout.output).toContain('Closed 0001-method-cli'); expectFile(root, 'docs/method/retro/0001-method-cli/method-cli.md'); expectFile(root, 'docs/method/retro/0001-method-cli/witness'); - expect(readFile(root, 'docs/method/retro/0001-method-cli/method-cli.md')).toContain('Outcome: partial'); + const retroDoc = readFile(root, 'docs/method/retro/0001-method-cli/method-cli.md'); + expect(retroDoc).toMatch(/^---\n[\s\S]+?\n---\n/u); + expect(retroDoc).toMatch(/^title:\s+"Method CLI"$/mu); + expect(retroDoc).toMatch(/^outcome:\s+partial$/mu); + expect(retroDoc).toMatch(/^drift_check:\s+yes$/mu); + expect(retroDoc).toMatch(/^design_doc:\s+"docs\/design\/0001-method-cli\/method-cli\.md"$/mu); + expect(retroDoc).toContain('Outcome: partial'); + }); + + it('Can I rely on scaffolded design docs to satisfy the repo docs contract without manual patching?', async () => { + const root = createTempRoot(); + await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_scaffold-contract.md'), + [ + '---', + 'title: "Scaffold Contract"', + 'legend: PROCESS', + 'lane: asap', + '---', + '', + '# Scaffold Contract', + '', + 'Backlog body.', + ].join('\n'), + 'utf8', + ); + + const exitCode = await runCli(['pull', 'PROCESS_scaffold-contract'], { + cwd: root, + stdout: new MemoryWriter(), + stderr: new MemoryWriter(), + }); + + expect(exitCode).toBe(0); + const designDoc = readFile(root, 'docs/design/0001-scaffold-contract/scaffold-contract.md'); + expect(designDoc).toMatch(/^legend:\s+PROCESS$/mu); + expect(designDoc).toMatch(/^cycle:\s+"0001-scaffold-contract"$/mu); + expect(designDoc).not.toContain('- Human: TBD'); + expect(designDoc).not.toContain('- Agent: TBD'); + }); + + it('Can I rely on scaffolded retro docs to satisfy the repo docs contract without post-close repairs?', async () => { + const root = createTempRoot(); + await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); + mkdirSync(join(root, 'docs/design/0001-scaffold-retro'), { recursive: true }); + writeFileSync( + join(root, 'docs/design/0001-scaffold-retro/scaffold-retro.md'), + [ + '---', + 'title: "Scaffold Retro"', + 'legend: PROCESS', + 'cycle: "0001-scaffold-retro"', + 'source_backlog: "docs/method/backlog/asap/PROCESS_scaffold-retro.md"', + '---', + '', + '# Scaffold Retro', + '', + 'Legend: PROCESS', + '', + '## Playback Questions', + '', + '- [ ] TBD', + ].join('\n'), + 'utf8', + ); + + const exitCode = await runCli(['close', '0001-scaffold-retro', '--drift-check', 'yes', '--outcome', 'hill-met'], { + cwd: root, + stdout: new MemoryWriter(), + stderr: new MemoryWriter(), + }); + + expect(exitCode).toBe(0); + const retroDoc = readFile(root, 'docs/method/retro/0001-scaffold-retro/scaffold-retro.md'); + expect(retroDoc).toMatch(/^outcome:\s+hill-met$/mu); + expect(retroDoc).toMatch(/^drift_check:\s+yes$/mu); + expect(retroDoc).toMatch(/^design_doc:\s+"docs\/design\/0001-scaffold-retro\/scaffold-retro\.md"$/mu); + expect(retroDoc).not.toContain('Outcome: TBD'); }); it('shows backlog, active cycles, and legend health in status', async () => { From 7654dfd76a6a778edf90264ebf97e891d98ec379 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 12:46:59 -0700 Subject: [PATCH 08/29] docs(backlog): capture witness drift output bug --- .../PROCESS_witness-drift-output-capture.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 docs/method/backlog/bad-code/PROCESS_witness-drift-output-capture.md diff --git a/docs/method/backlog/bad-code/PROCESS_witness-drift-output-capture.md b/docs/method/backlog/bad-code/PROCESS_witness-drift-output-capture.md new file mode 100644 index 0000000..c3bd762 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_witness-drift-output-capture.md @@ -0,0 +1,20 @@ +--- +title: "Witness Drift Output Capture" +legend: PROCESS +lane: bad-code +--- + +# Witness Drift Output Capture + +`captureWitness()` is still not reliably capturing the output of the +drift command into `verification.md`. During cycle `0031`, the standalone +`method drift 0031-generated-doc-scaffold-contract` command returned the +expected clean summary, but the generated witness still recorded an +empty `## Drift Results` block. + +That is real repo-truth drift inside the witness packet itself. The +close flow is claiming automated evidence capture, but one of the key +artifacts is blank even when the source command had meaningful output. + +This should be fixed with a targeted regression test so witness packets +carry the actual drift output they observed at close time. From 093aa623f13850dbbb4a9c2e5ad91b3557711c5f Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 12:59:53 -0700 Subject: [PATCH 09/29] feat(mcp): add structured tool result contract --- .../mcp-tool-result-contract.md | 87 +++++++ .../asap/PROCESS_mcp-tool-result-contract.md | 17 -- .../MCP_status-summary-mode.md | 9 +- .../mcp-tool-result-contract.md | 47 ++++ .../witness/verification.md | 34 +++ src/index.ts | 6 +- src/mcp.ts | 224 ++++++++++++++---- tests/mcp.test.ts | 166 +++++++++---- 8 files changed, 483 insertions(+), 107 deletions(-) create mode 100644 docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md delete mode 100644 docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md rename docs/method/{backlog/up-next => graveyard}/MCP_status-summary-mode.md (74%) create mode 100644 docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md create mode 100644 docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md diff --git a/docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md b/docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md new file mode 100644 index 0000000..5eafffc --- /dev/null +++ b/docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md @@ -0,0 +1,87 @@ +--- +title: "MCP Tool Result Contract" +legend: PROCESS +cycle: "0032-mcp-tool-result-contract" +source_backlog: "docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md" +--- + +# MCP Tool Result Contract + +Source backlog item: `docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md` +Legend: PROCESS + +## Sponsors + +- Human: MCP client operator +- Agent: Structured-response consumer + +## Hill + +MCP tools expose a stable structured result contract through +`structuredContent`, and `method_status` defaults to a compact summary +mode so callers can answer "where are we?" without pulling the whole +workspace graph into context. Text remains as fallback narration, not as +the primary machine interface. + +## Playback Questions + +### Human + +- [ ] Does `method_status` return summary-shaped structured content by + default, with lane counts instead of the fully expanded backlog? +- [ ] Can I request `method_status` full mode and still receive the + expanded structured workspace state without scraping text? + +### Agent + +- [ ] Do mutation tools (`method_inbox`, `method_pull`, `method_close`, + `method_capture_witness`) return stable field-level structured + content for paths and cycles? +- [ ] On MCP errors, can I inspect a structured error object instead of + only a prose message? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: summary mode should make + the common status query small enough to inspect quickly without + scrolling through every backlog item. +- Non-visual or alternate-reading expectations: structured fields must + carry the contract so agents and alternate readers are not forced to + reverse-engineer human prose. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: the contract should not + depend on English phrasing inside `content[0].text`. +- Logical direction / layout assumptions: none; this is a field-level + protocol contract. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: the envelope shape, + success/error distinction, and the difference between summary and full + status mode. +- What must be attributable, evidenced, or governed: tests should assert + `structuredContent` directly for success and error paths rather than + treating text blobs as the contract. + +## Non-goals + +- [ ] Replace every possible prose fallback in MCP responses with zero + text. +- [ ] Rework the underlying workspace domain models beyond what the + structured contract needs. +- [ ] Solve signpost truth drift; that remains separate work under + `PROCESS_bearing-truthfulness`. + +## Backlog Context + +The MCP server still returns tool results as ad hoc text blobs. Even the +test that asks whether an MCP client can interact without parsing +terminal text is effectively answered with string-contains assertions on +`content[0].text`. This is expensive and fragile for agents, especially +when status payloads are large or when callers need stable field-level +results. + +METHOD needs a more explicit structured result contract for MCP tools so +agents can consume repo state and mutations without scraping prose. diff --git a/docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md b/docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md deleted file mode 100644 index 84f60c1..0000000 --- a/docs/method/backlog/asap/PROCESS_mcp-tool-result-contract.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "MCP Tool Result Contract" -legend: PROCESS -lane: asap ---- - -# MCP Tool Result Contract - -The MCP server still returns tool results as ad hoc text blobs. Even the -test that asks whether an MCP client can interact without parsing -terminal text is effectively answered with string-contains assertions on -`content[0].text`. This is expensive and fragile for agents, especially -when status payloads are large or when callers need stable field-level -results. - -METHOD needs a more explicit structured result contract for MCP tools so -agents can consume repo state and mutations without scraping prose. diff --git a/docs/method/backlog/up-next/MCP_status-summary-mode.md b/docs/method/graveyard/MCP_status-summary-mode.md similarity index 74% rename from docs/method/backlog/up-next/MCP_status-summary-mode.md rename to docs/method/graveyard/MCP_status-summary-mode.md index 6f71474..e52eaae 100644 --- a/docs/method/backlog/up-next/MCP_status-summary-mode.md +++ b/docs/method/graveyard/MCP_status-summary-mode.md @@ -1,7 +1,7 @@ --- title: "method_status Summary Mode" legend: MCP -lane: up-next +lane: graveyard --- # MCP: method_status summary mode @@ -13,6 +13,13 @@ Most callers just want lane counts and active cycle names. The full expansion blows through agent context windows and adds no value for the common "where are we?" check. +## Disposition + +Retired as a separate backlog item in cycle `0032-mcp-tool-result-contract`. +The summary-mode behavior was absorbed into the broader MCP structured +result contract, so `method_status` now defaults to compact structured +summary output instead of needing a standalone follow-on. + ## Proposal Add an optional `summary` parameter (default `true`) to `method_status`. diff --git a/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md b/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md new file mode 100644 index 0000000..b7c24f6 --- /dev/null +++ b/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md @@ -0,0 +1,47 @@ +--- +title: "MCP Tool Result Contract" +cycle: "0032-mcp-tool-result-contract" +design_doc: "docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md" +outcome: hill-met +drift_check: yes +--- + +# MCP Tool Result Contract Retro + +Design: `docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md` +Outcome: hill-met +Drift check: yes + +## Summary + +This cycle replaced MCP text-blob responses with a stable +`structuredContent` envelope across the server, added default summary +mode to `method_status` with opt-in full mode, and rewrote the MCP tests +to assert field-level results and structured errors directly. The +standalone `MCP_status-summary-mode` follow-on was retired into the +graveyard because its behavior is now absorbed by the broader contract +fix. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- `PROCESS_witness-drift-output-capture` remains open: the close-time + verification witness still records an empty `## Drift Results` block + even when the standalone drift command returns output. + +## Cool Ideas + +- None recorded. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md b/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md new file mode 100644 index 0000000..1a7f01c --- /dev/null +++ b/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md @@ -0,0 +1,34 @@ +--- +title: "Verification Witness for Cycle 32" +--- + +# Verification Witness for Cycle 32 + +This witness proves that `MCP Tool Result Contract` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +``` +> method@0.3.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 . + + + Test Files 12 passed (12) + Tests 135 passed (135) + Start at 12:59:12 + Duration 801ms (transform 794ms, setup 0ms, import 2.62s, tests 1.14s, environment 1ms) +``` + +## Drift Results + +``` + +``` + +## Manual Verification + +- [x] Automated capture completed successfully. diff --git a/src/index.ts b/src/index.ts index 7e2414b..e51aff3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -240,7 +240,7 @@ export class Workspace { const changelogPath = resolve(this.root, 'CHANGELOG.md'); const bearingPath = resolve(this.root, 'docs/BEARING.md'); const status = this.status(); - const closedCycles = this.allCycles().filter((cycle) => existsSync(cycle.retroDoc)); + const closedCycles = this.closedCycles(); const commitSha = await this.currentCommitSha(); const newShips = this.findNewShips(closedCycles); @@ -381,6 +381,10 @@ export class Workspace { return this.allCycles().filter((cycle) => existsSync(cycle.designDoc) && !existsSync(cycle.retroDoc)); } + closedCycles(): Cycle[] { + return this.allCycles().filter((cycle) => existsSync(cycle.retroDoc)); + } + private findNewShips(closedCycles: Cycle[]): Cycle[] { const changelogPath = resolve(this.root, 'CHANGELOG.md'); if (!existsSync(changelogPath)) { diff --git a/src/mcp.ts b/src/mcp.ts index 635ddfc..f7a6113 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -2,11 +2,19 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; import { relative } from 'node:path'; import { Workspace } from './index.js'; -import { GitHubAdapter } from './adapters/github.js'; -import type { Outcome } from './domain.js'; +import { GitHubAdapter, type GitHubSyncResult } from './adapters/github.js'; +import type { Outcome, WorkspaceStatus } from './domain.js'; const workspaceProperty = { workspace: { type: 'string' as const, description: 'Absolute path to the METHOD workspace root directory' } }; +interface McpStatusSummary { + root: string; + laneCounts: Record; + activeCycles: Array<{ name: string; slug: string }>; + retroCount: number; + legendHealth: WorkspaceStatus['legendHealth']; +} + export interface McpToolDef { name: string; description: string; @@ -21,7 +29,14 @@ export const MCP_TOOLS: McpToolDef[] = [ { name: 'method_status', description: 'Get the current status of the METHOD workspace (backlog lanes, active cycles, legend health)', - inputSchema: { type: 'object', properties: { ...workspaceProperty }, required: ['workspace'] }, + inputSchema: { + type: 'object', + properties: { + ...workspaceProperty, + summary: { type: 'boolean', description: 'Return a compact structured summary instead of the fully expanded workspace status (default: true)' }, + }, + required: ['workspace'], + }, }, { name: 'method_inbox', @@ -104,7 +119,20 @@ export function createMcpServer() { if (request.params.name === 'method_status') { const status = workspace.status(); - return { content: [{ type: 'text', text: JSON.stringify(status, null, 2) }] }; + const summary = (args.summary as boolean | undefined) ?? true; + if (summary) { + const summaryResult = summarizeStatus(workspace, status); + return successResult( + 'method_status', + renderStatusSummaryText(summaryResult), + { mode: 'summary', summary: summaryResult }, + ); + } + return successResult( + 'method_status', + `Returned full workspace status for ${workspace.root}.`, + { mode: 'full', status }, + ); } if (request.params.name === 'method_inbox') { @@ -113,20 +141,42 @@ export function createMcpServer() { args.legend as string | undefined, args.title as string | undefined, ); - return { content: [{ type: 'text', text: `Captured to ${relative(workspace.root, path)}` }] }; + const relativePath = relative(workspace.root, path); + return successResult( + 'method_inbox', + `Captured to ${relativePath}`, + { + path: relativePath, + lane: 'inbox', + legend: args.legend as string | undefined, + title: args.title as string | undefined, + }, + ); } if (request.params.name === 'method_pull') { const cycle = workspace.pullItem(args.item as string); - return { content: [{ type: 'text', text: `Pulled into ${cycle.name}\nDesign: ${relative(workspace.root, cycle.designDoc)}` }] }; + return successResult( + 'method_pull', + `Pulled into ${cycle.name}`, + { + cycle: relativizeCycle(workspace, cycle), + }, + ); } if (request.params.name === 'method_drift') { const report = workspace.detectDrift(args.cycle as string | undefined); - return { - content: [{ type: 'text', text: report.output }], - isError: report.exitCode !== 0, - }; + return toolResult( + 'method_drift', + report.output, + { + exitCode: report.exitCode, + output: report.output, + clean: report.exitCode === 0, + }, + report.exitCode !== 0, + ); } if (request.params.name === 'method_close') { @@ -135,17 +185,29 @@ export function createMcpServer() { args.driftCheck as boolean, args.outcome as Outcome, ); - return { content: [{ type: 'text', text: `Closed ${cycle.name}\nRetro: ${relative(workspace.root, cycle.retroDoc)}` }] }; + return successResult( + 'method_close', + `Closed ${cycle.name}`, + { + cycle: relativizeCycle(workspace, cycle), + }, + ); } if (request.params.name === 'method_sync_ship') { const result = await workspace.shipSync(); - const text = [ - `Updated: ${result.updated.join(', ')}`, - ...result.newShips.map((c) => `- Shipped ${c.name}`), - result.newShips.length === 0 ? 'No new ships.' : '', - ].join('\n'); - return { content: [{ type: 'text', text }] }; + const newShips = result.newShips.map((cycle) => relativizeCycle(workspace, cycle)); + const text = result.newShips.length === 0 + ? 'No new ships.' + : `Shipped ${result.newShips.map((cycle) => cycle.name).join(', ')}`; + return successResult( + 'method_sync_ship', + text, + { + updated: result.updated, + newShips, + }, + ); } if (request.params.name === 'method_sync_github') { @@ -170,55 +232,123 @@ export function createMcpServer() { repo: repo!, }); - const log: string[] = []; let hasError = false; + let pushResults: GitHubSyncResult[] = []; + let pullResults: GitHubSyncResult[] = []; if (push) { - const results = await adapter.pushBacklog(); - for (const r of results) { - if (r.skipped) continue; - if (r.error) { - log.push(`Error pushing ${r.path}: ${r.error}`); - hasError = true; - } else { - const issueLabel = r.issue?.number ?? ''; - const verb = r.action === 'create' ? 'Created' : 'Updated'; - log.push(`${verb} GitHub Issue #${issueLabel} for ${r.path}`); - } - } + pushResults = await adapter.pushBacklog(); + hasError = hasError || pushResults.some((result) => result.error !== undefined); } if (pull) { - const results = await adapter.pullBacklog(); - for (const r of results) { - if (r.skipped) continue; - if (r.error) { - log.push(`Error pulling ${r.path}: ${r.error}`); - hasError = true; - } else { - const issueLabel = r.issue?.number ?? ''; - log.push(`Pulled remote changes from GitHub Issue #${issueLabel} into ${r.path}`); - } - } + pullResults = await adapter.pullBacklog(); + hasError = hasError || pullResults.some((result) => result.error !== undefined); } - return { - content: [{ type: 'text', text: log.join('\n') || 'No changes.' }], - isError: hasError, - }; + return toolResult( + 'method_sync_github', + summarizeGitHubSync(pushResults, pullResults), + { + pushRequested: push, + pullRequested: pull, + pushResults, + pullResults, + }, + hasError, + ); } if (request.params.name === 'method_capture_witness') { const path = await workspace.captureWitness(args.cycle as string | undefined); - return { content: [{ type: 'text', text: `Captured witness to ${relative(workspace.root, path)}` }] }; + const relativePath = relative(workspace.root, path); + return successResult( + 'method_capture_witness', + `Captured witness to ${relativePath}`, + { path: relativePath }, + ); } throw new Error(`Unknown tool: ${request.params.name}`); } catch (error: unknown) { const message = error instanceof Error ? error.message : String(error); - return { content: [{ type: 'text', text: message }], isError: true }; + return errorResult(request.params.name, message); } }); return server; } + +function summarizeStatus(workspace: Workspace, status: WorkspaceStatus): McpStatusSummary { + const laneCounts = Object.fromEntries( + Object.entries(status.backlog).map(([lane, items]) => [lane, items.length]), + ); + + return { + root: status.root, + laneCounts, + activeCycles: status.activeCycles.map((cycle) => ({ name: cycle.name, slug: cycle.slug })), + retroCount: workspace.closedCycles().length, + legendHealth: status.legendHealth, + }; +} + +function renderStatusSummaryText(summary: McpStatusSummary): string { + const laneCounts = Object.entries(summary.laneCounts) + .map(([lane, count]) => `${lane}=${count}`) + .join(', '); + const activeCycles = summary.activeCycles.length === 0 + ? '-' + : summary.activeCycles.map((cycle) => cycle.name).join(', '); + return [ + `Status summary for ${summary.root}.`, + `Lane counts: ${laneCounts}`, + `Active cycles: ${activeCycles}`, + `Retros: ${summary.retroCount}`, + ].join('\n'); +} + +function relativizeCycle(workspace: Workspace, cycle: { name: string; number: number; slug: string; designDoc: string; retroDoc: string }) { + return { + ...cycle, + designDoc: relative(workspace.root, cycle.designDoc), + retroDoc: relative(workspace.root, cycle.retroDoc), + }; +} + +function summarizeGitHubSync(pushResults: GitHubSyncResult[], pullResults: GitHubSyncResult[]): string { + const changed = [...pushResults, ...pullResults].filter((result) => !result.skipped); + if (changed.length === 0) { + return 'No changes.'; + } + const failures = changed.filter((result) => result.error !== undefined).length; + return `GitHub sync processed ${changed.length} item(s) with ${failures} error(s).`; +} + +function successResult(tool: string, text: string, result: Record) { + return toolResult(tool, text, result, false); +} + +function errorResult(tool: string, message: string) { + return { + content: [{ type: 'text' as const, text: message }], + structuredContent: { + tool, + ok: false, + error: { message }, + }, + isError: true, + }; +} + +function toolResult(tool: string, text: string, result: Record, isError: boolean) { + return { + content: [{ type: 'text' as const, text }], + structuredContent: { + tool, + ok: !isError, + result, + }, + isError, + }; +} diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 8199dee..889fb79 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -22,6 +22,22 @@ function createTempRoot(): string { return root; } +function createCallToolHarness() { + let callToolHandler: any; + const mockServer = { + setRequestHandler: vi.fn((schema, handler) => { + if (schema === CallToolRequestSchema) { + callToolHandler = handler; + } + }), + }; + + vi.spyOn(Server.prototype, 'setRequestHandler').mockImplementation(mockServer.setRequestHandler); + createMcpServer(); + expect(callToolHandler).toBeDefined(); + return callToolHandler; +} + describe('MCP Server', () => { it('Does `src/mcp.ts` export a functional MCP server using `@modelcontextprotocol/sdk`?', () => { const server = createMcpServer(); @@ -60,64 +76,132 @@ describe('MCP Server', () => { expect(tool.inputSchema.required, `${tool.name} must require workspace`).toContain('workspace'); expect(tool.inputSchema.properties.workspace, `${tool.name} must have workspace property`).toBeDefined(); } + const statusTool = result.tools.find((tool: any) => tool.name === 'method_status'); + expect(statusTool.inputSchema.properties.summary).toBeDefined(); vi.restoreAllMocks(); }); - it('Can an MCP client connect to `method` and interact with the backlog without parsing terminal text?', () => { - // This is a human/client-level playback question, but we verify it's possible through our agent tests. - // The structured responses demonstrated in other tests prove this is feasible. - }); - - it('Do unit tests verify the MCP server integration and its tools?', async () => { + it('Does `method_status` return summary-shaped structured content by default, with lane counts instead of the fully expanded backlog?', async () => { const root = createTempRoot(); initWorkspace(root); + const workspace = new Workspace(root); + workspace.captureIdea('test idea from mcp', undefined, 'Test Idea From MCP'); + const callToolHandler = createCallToolHarness(); - let callToolHandler: any; - const mockServer = { - setRequestHandler: vi.fn((schema, handler) => { - if (schema === CallToolRequestSchema) { - callToolHandler = handler; - } - }), - }; - - vi.spyOn(Server.prototype, 'setRequestHandler').mockImplementation(mockServer.setRequestHandler); - - createMcpServer(); - expect(callToolHandler).toBeDefined(); - - // Call method_status const statusResult = await callToolHandler({ params: { name: 'method_status', arguments: { workspace: root } } }); expect(statusResult.isError).toBeFalsy(); - expect(statusResult.content[0].text).toContain('"inbox": []'); + expect(statusResult.structuredContent.tool).toBe('method_status'); + expect(statusResult.structuredContent.ok).toBe(true); + expect(statusResult.structuredContent.result.mode).toBe('summary'); + expect(statusResult.structuredContent.result.summary.laneCounts.inbox).toBe(1); + expect(statusResult.structuredContent.result.summary.retroCount).toBe(0); + expect(statusResult.structuredContent.result.summary.activeCycles).toEqual([]); + expect(statusResult.structuredContent.result.summary.legendHealth).toContainEqual({ + legend: 'untagged', + backlog: 1, + active: 0, + }); + expect(statusResult.structuredContent.result.summary.backlog).toBeUndefined(); + expect(statusResult.content[0].text).toContain('Lane counts:'); + vi.restoreAllMocks(); + }); - // Call method_inbox - const inboxResult = await callToolHandler({ params: { name: 'method_inbox', arguments: { workspace: root, idea: 'test idea from mcp' } } }); - expect(inboxResult.isError).toBeFalsy(); - expect(inboxResult.content[0].text).toContain('Captured to docs/method/backlog/inbox/test-idea-from-mcp.md'); + it('Can I request `method_status` full mode and still receive the expanded structured workspace state without scraping text?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + workspace.captureIdea('test idea from mcp', 'PROCESS', 'Test Idea From MCP'); + const callToolHandler = createCallToolHarness(); - // Check status again - const statusAfterInbox = await callToolHandler({ params: { name: 'method_status', arguments: { workspace: root } } }); - expect(statusAfterInbox.content[0].text).toContain('test-idea-from-mcp'); + const statusResult = await callToolHandler({ + params: { name: 'method_status', arguments: { workspace: root, summary: false } }, + }); - // Call method_pull - const pullResult = await callToolHandler({ params: { name: 'method_pull', arguments: { workspace: root, item: 'test-idea-from-mcp' } } }); - expect(pullResult.isError).toBeFalsy(); - expect(pullResult.content[0].text).toContain('Pulled into 0001-test-idea-from-mcp'); + expect(statusResult.isError).toBeFalsy(); + expect(statusResult.structuredContent.tool).toBe('method_status'); + expect(statusResult.structuredContent.ok).toBe(true); + expect(statusResult.structuredContent.result.mode).toBe('full'); + expect(statusResult.structuredContent.result.status.backlog.inbox).toContainEqual({ + stem: 'PROCESS_test-idea-from-mcp', + lane: 'inbox', + path: 'docs/method/backlog/inbox/PROCESS_test-idea-from-mcp.md', + legend: 'PROCESS', + slug: 'test-idea-from-mcp', + }); + expect(statusResult.content[0].text).toContain('Returned full workspace status'); + vi.restoreAllMocks(); + }); - // Check status again - const statusAfterPull = await callToolHandler({ params: { name: 'method_status', arguments: { workspace: root } } }); - expect(statusAfterPull.content[0].text).toContain('0001-test-idea-from-mcp'); + it('Do mutation tools (`method_inbox`, `method_pull`, `method_close`, `method_capture_witness`) return stable field-level structured content for paths and cycles?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const callToolHandler = createCallToolHarness(); + const originalMethodTest = process.env.METHOD_TEST; + process.env.METHOD_TEST = 'true'; + try { + const inboxResult = await callToolHandler({ + params: { name: 'method_inbox', arguments: { workspace: root, idea: 'test idea from mcp' } }, + }); + expect(inboxResult.isError).toBeFalsy(); + expect(inboxResult.structuredContent.tool).toBe('method_inbox'); + expect(inboxResult.structuredContent.result.path).toBe('docs/method/backlog/inbox/test-idea-from-mcp.md'); + expect(inboxResult.structuredContent.result.lane).toBe('inbox'); + + const pullResult = await callToolHandler({ + params: { name: 'method_pull', arguments: { workspace: root, item: 'test-idea-from-mcp' } }, + }); + expect(pullResult.isError).toBeFalsy(); + expect(pullResult.structuredContent.tool).toBe('method_pull'); + expect(pullResult.structuredContent.result.cycle.name).toBe('0001-test-idea-from-mcp'); + expect(pullResult.structuredContent.result.cycle.designDoc).toBe( + 'docs/design/0001-test-idea-from-mcp/test-idea-from-mcp.md', + ); + + const captureResult = await callToolHandler({ + params: { name: 'method_capture_witness', arguments: { workspace: root, cycle: '0001-test-idea-from-mcp' } }, + }); + expect(captureResult.isError).toBeFalsy(); + expect(captureResult.structuredContent.tool).toBe('method_capture_witness'); + expect(captureResult.structuredContent.result.path).toBe( + 'docs/method/retro/0001-test-idea-from-mcp/witness/verification.md', + ); + + const closeResult = await callToolHandler({ + params: { + name: 'method_close', + arguments: { + workspace: root, + cycle: '0001-test-idea-from-mcp', + driftCheck: true, + outcome: 'hill-met', + }, + }, + }); + expect(closeResult.isError).toBeFalsy(); + expect(closeResult.structuredContent.tool).toBe('method_close'); + expect(closeResult.structuredContent.result.cycle.name).toBe('0001-test-idea-from-mcp'); + expect(closeResult.structuredContent.result.cycle.retroDoc).toBe( + 'docs/method/retro/0001-test-idea-from-mcp/test-idea-from-mcp.md', + ); + } finally { + if (originalMethodTest === undefined) { + delete process.env.METHOD_TEST; + } else { + process.env.METHOD_TEST = originalMethodTest; + } + vi.restoreAllMocks(); + } + }); - // Call method_capture_witness - const captureResult = await callToolHandler({ params: { name: 'method_capture_witness', arguments: { workspace: root, cycle: '0001-test-idea-from-mcp' } } }); - expect(captureResult.isError).toBeFalsy(); - expect(captureResult.content[0].text).toContain('Captured witness to docs/method/retro/0001-test-idea-from-mcp/witness/verification.md'); + it('On MCP errors, can I inspect a structured error object instead of only a prose message?', async () => { + const callToolHandler = createCallToolHarness(); - // Call without workspace — should error const noWorkspaceResult = await callToolHandler({ params: { name: 'method_status', arguments: {} } }); expect(noWorkspaceResult.isError).toBe(true); + expect(noWorkspaceResult.structuredContent.tool).toBe('method_status'); + expect(noWorkspaceResult.structuredContent.ok).toBe(false); + expect(noWorkspaceResult.structuredContent.error.message).toContain('workspace is required'); expect(noWorkspaceResult.content[0].text).toContain('workspace is required'); vi.restoreAllMocks(); From ecde6ac00e798d68bf69b2c2bfb9044ad44d47e9 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 13:04:02 -0700 Subject: [PATCH 10/29] fix(signposts): make bearing output truthful --- .../bearing-truthfulness.md | 83 ++++++++++++++++ .../asap/PROCESS_bearing-truthfulness.md | 17 ---- ...SS_legend-should-be-in-frontmatter-yaml.md | 9 +- .../bearing-truthfulness.md | 46 +++++++++ .../witness/verification.md | 34 +++++++ src/renderers.ts | 25 ++++- tests/ship-sync.test.ts | 97 +++++++++++++++++++ 7 files changed, 291 insertions(+), 20 deletions(-) create mode 100644 docs/design/0033-bearing-truthfulness/bearing-truthfulness.md delete mode 100644 docs/method/backlog/asap/PROCESS_bearing-truthfulness.md rename docs/method/{backlog/up-next => graveyard}/PROCESS_legend-should-be-in-frontmatter-yaml.md (57%) create mode 100644 docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md create mode 100644 docs/method/retro/0033-bearing-truthfulness/witness/verification.md diff --git a/docs/design/0033-bearing-truthfulness/bearing-truthfulness.md b/docs/design/0033-bearing-truthfulness/bearing-truthfulness.md new file mode 100644 index 0000000..c10ec24 --- /dev/null +++ b/docs/design/0033-bearing-truthfulness/bearing-truthfulness.md @@ -0,0 +1,83 @@ +--- +title: "BEARING Truthfulness" +legend: PROCESS +cycle: "0033-bearing-truthfulness" +source_backlog: "docs/method/backlog/asap/PROCESS_bearing-truthfulness.md" +--- + +# BEARING Truthfulness + +Source backlog item: `docs/method/backlog/asap/PROCESS_bearing-truthfulness.md` +Legend: PROCESS + +## Sponsors + +- Human: Repo signpost reader +- Agent: Signpost-trust consumer + +## Hill + +`renderBearing` stops emitting stale assertions and only reports claims +that are either directly derived from current repo state or tightly +bounded fallback text. The generated signpost should be safe to trust as +a summary surface, even when the underlying backlog or witness posture +changes. + +## Playback Questions + +### Human + +- [ ] Does generated `BEARING.md` stop claiming witness generation is not + automated? +- [ ] When backlog pressure changes, does `BEARING.md` describe concrete + live repo state instead of hardcoded stale complaints? + +### Agent + +- [ ] Can I point to the exact backlog counts that caused each `What + feels wrong?` line? +- [ ] Does `renderBearing` only emit derived or tightly bounded + statements instead of unaudited repo assertions? + +## Accessibility and Assistive Reading + +- Linear truth / reduced-complexity posture: the signpost should stay + short, but every complaint line must map back to explicit repo state. +- Non-visual or alternate-reading expectations: readers should be able + to inspect BEARING linearly without wondering whether a sentence is + aspirational prose or live repo truth. + +## Localization and Directionality + +- Locale / wording / formatting assumptions: counts and derived status + should not rely on ambiguous rhetorical phrasing. +- Logical direction / layout assumptions: none; the problem is content + truth, not layout. + +## Agent Inspectability and Explainability + +- What must be explicit and deterministic for agents: which backlog and + cycle counts feed BEARING, and what fallback text appears when there is + no active pressure to report. +- What must be attributable, evidenced, or governed: tests should prove + the stale witness claim is gone and that "What feels wrong?" lines map + to current backlog counts instead of hardcoded guesses. + +## Non-goals + +- [ ] Run ship sync on `main` from this branch; merged signpost refresh is + still a post-merge maneuver. +- [ ] Make BEARING a full dashboard or replace `method status`. +- [ ] Solve unrelated signpost or legend taxonomy drift. + +## Backlog Context + +`renderBearing` currently emits factual claims that have drifted from the +repo. In particular, it still says witness generation is not automated +even though automated witness capture already landed. A generated +signpost that tells the truth incorrectly is worse than missing +automation: it creates confident drift in the repo's own public surface. + +This needs a pass to make BEARING generation derive more of its claims +from current repo state or explicitly constrain which statements are +allowed to be hardcoded. diff --git a/docs/method/backlog/asap/PROCESS_bearing-truthfulness.md b/docs/method/backlog/asap/PROCESS_bearing-truthfulness.md deleted file mode 100644 index f593776..0000000 --- a/docs/method/backlog/asap/PROCESS_bearing-truthfulness.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: "BEARING Truthfulness" -legend: PROCESS -lane: asap ---- - -# BEARING Truthfulness - -`renderBearing` currently emits factual claims that have drifted from the -repo. In particular, it still says witness generation is not automated -even though automated witness capture already landed. A generated -signpost that tells the truth incorrectly is worse than missing -automation: it creates confident drift in the repo's own public surface. - -This needs a pass to make BEARING generation derive more of its claims -from current repo state or explicitly constrain which statements are -allowed to be hardcoded. diff --git a/docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md b/docs/method/graveyard/PROCESS_legend-should-be-in-frontmatter-yaml.md similarity index 57% rename from docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md rename to docs/method/graveyard/PROCESS_legend-should-be-in-frontmatter-yaml.md index 7210e17..2fadef1 100644 --- a/docs/method/backlog/up-next/PROCESS_legend-should-be-in-frontmatter-yaml.md +++ b/docs/method/graveyard/PROCESS_legend-should-be-in-frontmatter-yaml.md @@ -1,9 +1,16 @@ --- title: "Legend Should Be In Frontmatter YAML" legend: PROCESS -lane: up-next +lane: graveyard --- # Legend Should Be In Frontmatter YAML Require backlog items to carry their legend in YAML frontmatter instead of relying only on filename prefixes or inferred metadata. Commands that create, move, or audit backlog items should preserve and validate the `legend` field so legend-aware workflows have a single canonical source. + +## Disposition + +Retired as a separate backlog item after cycle +`0030-backlog-metadata-single-source-of-truth`. Backlog items now carry +legend metadata in YAML and METHOD operations preserve or backfill that +frontmatter, so this no longer needs to stay open as standalone work. diff --git a/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md b/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md new file mode 100644 index 0000000..6c32cc3 --- /dev/null +++ b/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md @@ -0,0 +1,46 @@ +--- +title: "BEARING Truthfulness" +cycle: "0033-bearing-truthfulness" +design_doc: "docs/design/0033-bearing-truthfulness/bearing-truthfulness.md" +outcome: hill-met +drift_check: yes +--- + +# BEARING Truthfulness Retro + +Design: `docs/design/0033-bearing-truthfulness/bearing-truthfulness.md` +Outcome: hill-met +Drift check: yes + +## Summary + +This cycle made `renderBearing` stop speaking beyond repo truth. The +"What feels wrong?" section is now derived from live backlog and active +cycle counts with a bounded fallback when nothing acute is recorded, +instead of hardcoded complaints that drift over time. The stale witness +automation claim is gone from generated BEARING output, but the checked +in `docs/BEARING.md` artifact itself still refreshes only during ship +sync after merge on `main`. + +## Playback Witness + +- [Verification Witness](./witness/verification.md) + +## Drift + +- None recorded. + +## New Debt + +- `PROCESS_witness-drift-output-capture` remains open: close-time + verification witnesses still record an empty `## Drift Results` block. + +## Cool Ideas + +- None recorded. + +## Backlog Maintenance + +- [x] Inbox processed +- [x] Priorities reviewed +- [x] Dead work buried or merged diff --git a/docs/method/retro/0033-bearing-truthfulness/witness/verification.md b/docs/method/retro/0033-bearing-truthfulness/witness/verification.md new file mode 100644 index 0000000..98f2351 --- /dev/null +++ b/docs/method/retro/0033-bearing-truthfulness/witness/verification.md @@ -0,0 +1,34 @@ +--- +title: "Verification Witness for Cycle 33" +--- + +# Verification Witness for Cycle 33 + +This witness proves that `BEARING Truthfulness` now carries the required +behavior and adheres to the repo invariants. + +## Test Results + +``` +> method@0.3.0 test +> vitest run --config vitest.config.ts + + + RUN v4.1.2 . + + + Test Files 12 passed (12) + Tests 139 passed (139) + Start at 13:02:41 + Duration 814ms (transform 889ms, setup 0ms, import 2.77s, tests 1.25s, environment 1ms) +``` + +## Drift Results + +``` + +``` + +## Manual Verification + +- [x] Automated capture completed successfully. diff --git a/src/renderers.ts b/src/renderers.ts index 054f468..0d34b4b 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -13,6 +13,7 @@ export function titleCase(value: string): string { export function renderBearing(status: WorkspaceStatus, closedCycles: Cycle[], commitSha: string): string { const latestShips = [...closedCycles].reverse().slice(0, 3); const nextUp = [...status.backlog.asap, ...status.backlog['up-next']].slice(0, 2); + const frictionLines = deriveBearingFriction(status); const priority = nextUp.length > 0 ? nextUp.map(i => `\`${i.stem}\``).join(' or ') : 'TBD'; @@ -45,12 +46,32 @@ export function renderBearing(status: WorkspaceStatus, closedCycles: Cycle[], co '', '## What feels wrong?', '', - '- Backlog maintenance is still largely manual.', - '- Witness generation is not yet automated.', + ...frictionLines, '', ].join('\n'); } +function deriveBearingFriction(status: WorkspaceStatus): string[] { + const lines: string[] = []; + + if (status.backlog.inbox.length > 0) { + lines.push(`- Inbox still holds ${status.backlog.inbox.length} untriaged item(s).`); + } + if (status.backlog.asap.length > 0) { + lines.push(`- ${status.backlog.asap.length} ASAP backlog item(s) are still unresolved.`); + } + if (status.backlog['bad-code'].length > 0) { + lines.push(`- ${status.backlog['bad-code'].length} bad-code item(s) remain tracked.`); + } + if (status.activeCycles.length > 0) { + lines.push(`- ${status.activeCycles.length} active cycle(s) are still open.`); + } + + return lines.length > 0 + ? lines + : ['- No acute coordination pain is currently recorded.']; +} + export function renderWitnessDoc(options: { cycle: Cycle; testResult: string; diff --git a/tests/ship-sync.test.ts b/tests/ship-sync.test.ts index 2de2195..8282b05 100644 --- a/tests/ship-sync.test.ts +++ b/tests/ship-sync.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { initWorkspace, Workspace } from '../src/index.js'; +import { renderBearing } from '../src/renderers.js'; const tempRoots: string[] = []; @@ -105,4 +106,100 @@ describe('Ship Sync', () => { expect(result.updated).toContain('docs/BEARING.md'); expect(result.updated).not.toContain('CHANGELOG.md'); }); + + it('Does generated `BEARING.md` stop claiming witness generation is not automated?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + await workspace.shipSync(); + + const bearing = readFileSync(join(root, 'docs/BEARING.md'), 'utf8'); + expect(bearing).not.toContain('Witness generation is not yet automated.'); + }); + + it('When backlog pressure changes, does `BEARING.md` describe concrete live repo state instead of hardcoded stale complaints?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + workspace.captureIdea('Inbox pressure', 'PROCESS', 'Inbox Pressure'); + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_asap-pressure.md'), + [ + '---', + 'title: "ASAP Pressure"', + 'legend: PROCESS', + 'lane: asap', + '---', + '', + '# ASAP Pressure', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + writeFileSync( + join(root, 'docs/method/backlog/bad-code/PROCESS_bad-code-pressure.md'), + [ + '---', + 'title: "Bad Code Pressure"', + 'legend: PROCESS', + 'lane: bad-code', + '---', + '', + '# Bad Code Pressure', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + await workspace.shipSync(); + + const bearing = readFileSync(join(root, 'docs/BEARING.md'), 'utf8'); + expect(bearing).toContain('Inbox still holds 1 untriaged item(s).'); + expect(bearing).toContain('1 ASAP backlog item(s) are still unresolved.'); + expect(bearing).toContain('1 bad-code item(s) remain tracked.'); + expect(bearing).not.toContain('Backlog maintenance is still largely manual.'); + }); + + it('Can I point to the exact backlog counts that caused each `What feels wrong?` line?', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + workspace.captureIdea('Inbox pressure', 'PROCESS', 'Inbox Pressure'); + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_asap-pressure.md'), + [ + '---', + 'title: "ASAP Pressure"', + 'legend: PROCESS', + 'lane: asap', + '---', + '', + '# ASAP Pressure', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const bearing = renderBearing(workspace.status(), [], 'test-sha'); + expect(bearing).toContain('Inbox still holds 1 untriaged item(s).'); + expect(bearing).toContain('1 ASAP backlog item(s) are still unresolved.'); + expect(bearing).not.toContain('2 ASAP backlog item(s) are still unresolved.'); + }); + + it('Does `renderBearing` only emit derived or tightly bounded statements instead of unaudited repo assertions?', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + const bearing = renderBearing(workspace.status(), [], 'test-sha'); + expect(bearing).toContain('No acute coordination pain is currently recorded.'); + expect(bearing).not.toContain('Backlog maintenance is still largely manual.'); + expect(bearing).not.toContain('Witness generation is not yet automated.'); + }); }); From d61e27f09a7cbe547897274e21d77f75db68938c Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 14:19:26 -0700 Subject: [PATCH 11/29] fix(review): restore CLI and MCP contract alignment --- docs/CLI.md | 12 ++++----- docs/MCP.md | 28 +++++++++++++++++---- src/cli-args.ts | 10 +++++--- src/cli.ts | 6 ++--- src/mcp.ts | 42 ++++++++++++++++++++++++------- tests/cli.test.ts | 21 +++++++++++++++- tests/docs.test.ts | 3 +++ tests/mcp.test.ts | 62 +++++++++++++++++++++++++++------------------- 8 files changed, 131 insertions(+), 53 deletions(-) diff --git a/docs/CLI.md b/docs/CLI.md index 44e9ff3..cfbd891 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -1,8 +1,8 @@ --- -title: "CLI Reference" -generated_at: 2026-04-07T05:30:14.481Z -generator: "method sync ship" -generated_from_commit: "03d889ddbe57ecb6b336ba2a62815f863f85bfd1" +title: CLI Reference +generated_at: 2026-04-08T21:17:50.746Z +generator: method sync ship +generated_from_commit: ecde6ac00e798d68bf69b2c2bfb9044ad44d47e9 provenance_level: artifact_history --- @@ -27,9 +27,9 @@ Capture a raw idea in docs/method/backlog/inbox/. Promote a backlog item into the next numbered design cycle. -### `method close [cycle] [--drift-check yes|no] [--outcome hill-met|partial|not-met]` +### `method close [cycle] [--drift-check yes|no] --outcome hill-met|partial|not-met` -Close an active cycle into docs/method/retro/. +Close an active cycle into docs/method/retro/. `--outcome` is required. ### `method status` diff --git a/docs/MCP.md b/docs/MCP.md index f770e6e..7b191ce 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -1,8 +1,8 @@ --- -title: "MCP Reference" -generated_at: 2026-04-07T05:30:14.482Z -generator: "method sync ship" -generated_from_commit: "03d889ddbe57ecb6b336ba2a62815f863f85bfd1" +title: MCP Reference +generated_at: 2026-04-08T21:17:50.754Z +generator: method sync ship +generated_from_commit: ecde6ac00e798d68bf69b2c2bfb9044ad44d47e9 provenance_level: artifact_history --- @@ -25,6 +25,10 @@ workspace-agnostic — a single instance can serve multiple projects. Get the current status of the METHOD workspace (backlog lanes, active cycles, legend health) +**Parameters:** + +- `summary` (optional) `boolean` — Return a compact structured summary instead of the fully expanded workspace status (default: false) + ### `method_inbox` Capture a new raw idea into the inbox @@ -85,6 +89,20 @@ Automate terminal evidence capture for a cycle ## Error Handling -All tools return `isError: true` with a text message on failure. +All tools return a human-readable text message in `content`. +Machine-readable callers should consume `structuredContent`. + +On success, `structuredContent` includes: + +- `tool` +- `ok: true` +- `result` + +On failure, tools set `isError: true` and `structuredContent` includes: + +- `tool` +- `ok: false` +- `error.message` + Common errors: `workspace is required`, `not a METHOD workspace`, `No active cycles found`. diff --git a/src/cli-args.ts b/src/cli-args.ts index 081a5d9..d55971e 100644 --- a/src/cli-args.ts +++ b/src/cli-args.ts @@ -6,7 +6,7 @@ export type ParsedCommand = | { command: 'init'; path: string } | { command: 'inbox'; idea: string; legend?: string; title?: string } | { command: 'pull'; item: string } - | { command: 'close'; cycle?: string; driftCheck?: 'yes' | 'no'; outcome?: Outcome } + | { command: 'close'; cycle?: string; driftCheck?: 'yes' | 'no'; outcome: Outcome } | { command: 'drift'; cycle?: string } | { command: 'status' } | { command: 'mcp' } @@ -90,7 +90,7 @@ export function usage(topic?: string): string { return 'Usage: method pull \n\nPromote a backlog item into the next numbered design cycle.'; } if (topic === 'close') { - return 'Usage: method close [cycle] [--drift-check yes|no] [--outcome hill-met|partial|not-met]\n\nClose an active cycle into docs/method/retro/.'; + return 'Usage: method close [cycle] [--drift-check yes|no] --outcome hill-met|partial|not-met\n\nClose an active cycle into docs/method/retro/. `--outcome` is required.'; } if (topic === 'status') { return 'Usage: method status\n\nShow backlog lanes, active cycles, and legend health.'; @@ -219,11 +219,15 @@ function parseCloseArgs(args: readonly string[]): ParsedCommand { throw new MethodError(`Unknown option: ${value}`); } if (cycle !== undefined) { - throw new MethodError('Usage: method close [cycle] [--drift-check yes|no] [--outcome hill-met|partial|not-met]'); + throw new MethodError(usage('close')); } cycle = value; } + if (outcome === undefined) { + throw new MethodError(usage('close')); + } + return { command: 'close', cycle, driftCheck, outcome }; } diff --git a/src/cli.ts b/src/cli.ts index 1637727..8a9861b 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -73,11 +73,9 @@ export async function runCli( ? await promptConfirm({ title: 'Drift check complete?', defaultValue: false }) : parsed.driftCheck === 'yes'; if (!completedDriftCheck) { - await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome ?? 'partial'); - } else if (parsed.outcome === undefined) { - throw new Error('Outcome is required. Pass --outcome hill-met|partial|not-met.'); + throw new Error('Cannot close a cycle without completing the drift check.'); } - const cycle = await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome ?? 'partial'); + const cycle = await workspace.closeCycle(parsed.cycle, completedDriftCheck, parsed.outcome); stdout.write(`${alert(`Closed ${cycle.name}`, { variant: 'success', ctx })}\n`); stdout.write(`${relative(root, cycle.retroDoc)}\n`); return 0; diff --git a/src/mcp.ts b/src/mcp.ts index f7a6113..a6ac1b6 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -1,6 +1,6 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; -import { relative } from 'node:path'; +import { basename, relative } from 'node:path'; import { Workspace } from './index.js'; import { GitHubAdapter, type GitHubSyncResult } from './adapters/github.js'; import type { Outcome, WorkspaceStatus } from './domain.js'; @@ -33,7 +33,7 @@ export const MCP_TOOLS: McpToolDef[] = [ type: 'object', properties: { ...workspaceProperty, - summary: { type: 'boolean', description: 'Return a compact structured summary instead of the fully expanded workspace status (default: true)' }, + summary: { type: 'boolean', description: 'Return a compact structured summary instead of the fully expanded workspace status (default: false)' }, }, required: ['workspace'], }, @@ -119,7 +119,7 @@ export function createMcpServer() { if (request.params.name === 'method_status') { const status = workspace.status(); - const summary = (args.summary as boolean | undefined) ?? true; + const summary = args.summary === true; if (summary) { const summaryResult = summarizeStatus(workspace, status); return successResult( @@ -142,15 +142,11 @@ export function createMcpServer() { args.title as string | undefined, ); const relativePath = relative(workspace.root, path); + const persistedItem = readPersistedInboxResult(workspace, relativePath); return successResult( 'method_inbox', `Captured to ${relativePath}`, - { - path: relativePath, - lane: 'inbox', - legend: args.legend as string | undefined, - title: args.title as string | undefined, - }, + persistedItem, ); } @@ -308,6 +304,21 @@ function renderStatusSummaryText(summary: McpStatusSummary): string { ].join('\n'); } +function readPersistedInboxResult(workspace: Workspace, relativePath: string) { + const frontmatter = workspace.readFrontmatter(relativePath); + const stem = fileStem(relativePath); + const { legend, slug } = splitBacklogStem(stem); + + return { + path: relativePath, + lane: frontmatter.lane ?? 'inbox', + legend: frontmatter.legend ?? legend, + title: frontmatter.title, + stem, + slug, + }; +} + function relativizeCycle(workspace: Workspace, cycle: { name: string; number: number; slug: string; designDoc: string; retroDoc: string }) { return { ...cycle, @@ -352,3 +363,16 @@ function toolResult(tool: string, text: string, result: Record, isError, }; } + +function fileStem(path: string): string { + const name = basename(path); + return name.endsWith('.md') ? name.slice(0, -3) : name; +} + +function splitBacklogStem(stem: string): { legend?: string; slug: string } { + const match = /^(?[A-Z][A-Z0-9]*)_(?.+)$/u.exec(stem); + if (match?.groups === undefined) { + return { slug: stem }; + } + return { legend: match.groups.legend, slug: match.groups.slug }; +} diff --git a/tests/cli.test.ts b/tests/cli.test.ts index d66b53f..d4ed427 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -60,6 +60,7 @@ describe('method CLI', () => { expect(exitCode).toBe(0); expect(stdout.output).toContain('Usage: method close'); expect(stdout.output).toContain('Close an active cycle into docs/method/retro/.'); + expect(stdout.output).toContain('--outcome hill-met|partial|not-met'); }); it('captures backlog ideas in inbox', async () => { @@ -273,7 +274,7 @@ describe('method CLI', () => { writeFileSync(join(root, 'docs/design/0001-method-cli/method-cli.md'), '# Method CLI\n\nLegend: none\n', 'utf8'); const stderr = new MemoryWriter(); - const exitCode = await runCli(['close', '--drift-check', 'no'], { + const exitCode = await runCli(['close', '--drift-check', 'no', '--outcome', 'partial'], { cwd: root, stdout: new MemoryWriter(), stderr, @@ -283,6 +284,24 @@ describe('method CLI', () => { expect(stderr.output.toLowerCase()).toContain('drift check'); }); + it('refuses close when outcome is omitted', async () => { + const root = createTempRoot(); + await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); + mkdirSync(join(root, 'docs/design/0001-method-cli'), { recursive: true }); + writeFileSync(join(root, 'docs/design/0001-method-cli/method-cli.md'), '# Method CLI\n\nLegend: none\n', 'utf8'); + + const stderr = new MemoryWriter(); + const exitCode = await runCli(['close', '--drift-check', 'yes'], { + cwd: root, + stdout: new MemoryWriter(), + stderr, + }); + + expect(exitCode).toBe(1); + expect(stderr.output).toContain('Usage: method close'); + expect(stderr.output).toContain('--outcome hill-met|partial|not-met'); + }); + it('Can I run the detector and get a concise list of playback questions that have no matching test evidence, with the design file and question text called out directly?', async () => { const root = createTempRoot(); await runCli(['init'], { cwd: root, stdout: new MemoryWriter(), stderr: new MemoryWriter() }); diff --git a/tests/docs.test.ts b/tests/docs.test.ts index 058f4f4..81db8dc 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -714,6 +714,7 @@ describe('METHOD docs', () => { expect(cli).toContain('--push'); expect(cli).toContain('--pull'); expect(cli).toContain('Ship Sync'); + expect(cli).toContain('method close [cycle] [--drift-check yes|no] --outcome hill-met|partial|not-met'); // MCP doc names every tool expect(mcp).toContain('method_status'); @@ -724,6 +725,8 @@ describe('METHOD docs', () => { expect(mcp).toContain('method_sync_ship'); expect(mcp).toContain('method_sync_github'); expect(mcp).toContain('method_capture_witness'); + expect(mcp).toContain('summary'); + expect(mcp).toContain('structuredContent'); // README references both docs expect(readme).toContain('docs/CLI.md'); diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 889fb79..d119737 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -82,58 +82,63 @@ describe('MCP Server', () => { vi.restoreAllMocks(); }); - it('Does `method_status` return summary-shaped structured content by default, with lane counts instead of the fully expanded backlog?', async () => { + it('Does `method_status` preserve the legacy full-status default when the caller omits the new summary flag?', async () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); - workspace.captureIdea('test idea from mcp', undefined, 'Test Idea From MCP'); + workspace.captureIdea('test idea from mcp', 'PROCESS', 'Test Idea From MCP'); const callToolHandler = createCallToolHarness(); const statusResult = await callToolHandler({ params: { name: 'method_status', arguments: { workspace: root } } }); expect(statusResult.isError).toBeFalsy(); expect(statusResult.structuredContent.tool).toBe('method_status'); expect(statusResult.structuredContent.ok).toBe(true); - expect(statusResult.structuredContent.result.mode).toBe('summary'); - expect(statusResult.structuredContent.result.summary.laneCounts.inbox).toBe(1); - expect(statusResult.structuredContent.result.summary.retroCount).toBe(0); - expect(statusResult.structuredContent.result.summary.activeCycles).toEqual([]); - expect(statusResult.structuredContent.result.summary.legendHealth).toContainEqual({ - legend: 'untagged', + expect(statusResult.structuredContent.result.mode).toBe('full'); + expect(statusResult.structuredContent.result.status.backlog.inbox).toContainEqual({ + stem: 'PROCESS_test-idea-from-mcp', + lane: 'inbox', + path: 'docs/method/backlog/inbox/PROCESS_test-idea-from-mcp.md', + legend: 'PROCESS', + slug: 'test-idea-from-mcp', + }); + expect(statusResult.structuredContent.result.status.legendHealth).toContainEqual({ + legend: 'PROCESS', backlog: 1, active: 0, }); - expect(statusResult.structuredContent.result.summary.backlog).toBeUndefined(); - expect(statusResult.content[0].text).toContain('Lane counts:'); + expect(statusResult.content[0].text).toContain('Returned full workspace status'); vi.restoreAllMocks(); }); - it('Can I request `method_status` full mode and still receive the expanded structured workspace state without scraping text?', async () => { + it('Can I request `method_status` summary mode explicitly and get compact lane counts without the full backlog payload?', async () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); - workspace.captureIdea('test idea from mcp', 'PROCESS', 'Test Idea From MCP'); + workspace.captureIdea('test idea from mcp', undefined, 'Test Idea From MCP'); const callToolHandler = createCallToolHarness(); const statusResult = await callToolHandler({ - params: { name: 'method_status', arguments: { workspace: root, summary: false } }, + params: { name: 'method_status', arguments: { workspace: root, summary: true } }, }); expect(statusResult.isError).toBeFalsy(); expect(statusResult.structuredContent.tool).toBe('method_status'); expect(statusResult.structuredContent.ok).toBe(true); - expect(statusResult.structuredContent.result.mode).toBe('full'); - expect(statusResult.structuredContent.result.status.backlog.inbox).toContainEqual({ - stem: 'PROCESS_test-idea-from-mcp', - lane: 'inbox', - path: 'docs/method/backlog/inbox/PROCESS_test-idea-from-mcp.md', - legend: 'PROCESS', - slug: 'test-idea-from-mcp', + expect(statusResult.structuredContent.result.mode).toBe('summary'); + expect(statusResult.structuredContent.result.summary.laneCounts.inbox).toBe(1); + expect(statusResult.structuredContent.result.summary.retroCount).toBe(0); + expect(statusResult.structuredContent.result.summary.activeCycles).toEqual([]); + expect(statusResult.structuredContent.result.summary.legendHealth).toContainEqual({ + legend: 'untagged', + backlog: 1, + active: 0, }); - expect(statusResult.content[0].text).toContain('Returned full workspace status'); + expect(statusResult.structuredContent.result.summary.backlog).toBeUndefined(); + expect(statusResult.content[0].text).toContain('Lane counts:'); vi.restoreAllMocks(); }); - it('Do mutation tools (`method_inbox`, `method_pull`, `method_close`, `method_capture_witness`) return stable field-level structured content for paths and cycles?', async () => { + it('Do mutation tools (`method_inbox`, `method_pull`, `method_close`, `method_capture_witness`) return stable field-level structured content for persisted paths and cycles?', async () => { const root = createTempRoot(); initWorkspace(root); const callToolHandler = createCallToolHarness(); @@ -141,15 +146,22 @@ describe('MCP Server', () => { process.env.METHOD_TEST = 'true'; try { const inboxResult = await callToolHandler({ - params: { name: 'method_inbox', arguments: { workspace: root, idea: 'test idea from mcp' } }, + params: { + name: 'method_inbox', + arguments: { workspace: root, idea: 'Test idea from mcp', legend: 'process' }, + }, }); expect(inboxResult.isError).toBeFalsy(); expect(inboxResult.structuredContent.tool).toBe('method_inbox'); - expect(inboxResult.structuredContent.result.path).toBe('docs/method/backlog/inbox/test-idea-from-mcp.md'); + expect(inboxResult.structuredContent.result.path).toBe('docs/method/backlog/inbox/PROCESS_test-idea-from-mcp.md'); expect(inboxResult.structuredContent.result.lane).toBe('inbox'); + expect(inboxResult.structuredContent.result.legend).toBe('PROCESS'); + expect(inboxResult.structuredContent.result.title).toBe('Test idea from mcp'); + expect(inboxResult.structuredContent.result.stem).toBe('PROCESS_test-idea-from-mcp'); + expect(inboxResult.structuredContent.result.slug).toBe('test-idea-from-mcp'); const pullResult = await callToolHandler({ - params: { name: 'method_pull', arguments: { workspace: root, item: 'test-idea-from-mcp' } }, + params: { name: 'method_pull', arguments: { workspace: root, item: 'PROCESS_test-idea-from-mcp' } }, }); expect(pullResult.isError).toBeFalsy(); expect(pullResult.structuredContent.tool).toBe('method_pull'); From 11088d38848f41d4e9d114cb43ae6990c72f8f97 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 22:41:10 -0700 Subject: [PATCH 12/29] fix(mcp): validate close tool inputs at runtime --- src/mcp.ts | 33 ++++++++++++++++++++++++++--- tests/mcp.test.ts | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/mcp.ts b/src/mcp.ts index a6ac1b6..6a67f5a 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -176,10 +176,13 @@ export function createMcpServer() { } if (request.params.name === 'method_close') { + const cycleName = validateOptionalString(args.cycle, 'cycle'); + const driftCheck = validateBoolean(args.driftCheck, 'driftCheck'); + const outcome = validateOutcome(args.outcome); const cycle = await workspace.closeCycle( - args.cycle as string | undefined, - args.driftCheck as boolean, - args.outcome as Outcome, + cycleName, + driftCheck, + outcome, ); return successResult( 'method_close', @@ -376,3 +379,27 @@ function splitBacklogStem(stem: string): { legend?: string; slug: string } { } return { legend: match.groups.legend, slug: match.groups.slug }; } + +function validateOptionalString(value: unknown, name: string): string | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string') { + throw new Error(`${name} must be a string when provided.`); + } + return value; +} + +function validateBoolean(value: unknown, name: string): boolean { + if (typeof value !== 'boolean') { + throw new Error(`${name} must be a boolean.`); + } + return value; +} + +function validateOutcome(value: unknown): Outcome { + if (value === 'hill-met' || value === 'partial' || value === 'not-met') { + return value; + } + throw new Error('outcome must be one of: hill-met, partial, not-met.'); +} diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index d119737..250545d 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -218,4 +218,57 @@ describe('MCP Server', () => { vi.restoreAllMocks(); }); + + it('Does `method_close` reject invalid runtime argument types instead of trusting MCP schema declarations?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + workspace.captureIdea('close validation', 'PROCESS', 'Close Validation'); + workspace.pullItem('PROCESS_close-validation'); + const callToolHandler = createCallToolHarness(); + + const badDriftCheck = await callToolHandler({ + params: { + name: 'method_close', + arguments: { + workspace: root, + cycle: '0001-close-validation', + driftCheck: 'false', + outcome: 'hill-met', + }, + }, + }); + expect(badDriftCheck.isError).toBe(true); + expect(badDriftCheck.structuredContent.error.message).toContain('driftCheck must be a boolean'); + + const badOutcome = await callToolHandler({ + params: { + name: 'method_close', + arguments: { + workspace: root, + cycle: '0001-close-validation', + driftCheck: true, + outcome: 'done', + }, + }, + }); + expect(badOutcome.isError).toBe(true); + expect(badOutcome.structuredContent.error.message).toContain('outcome must be one of'); + + const badCycle = await callToolHandler({ + params: { + name: 'method_close', + arguments: { + workspace: root, + cycle: 42, + driftCheck: true, + outcome: 'hill-met', + }, + }, + }); + expect(badCycle.isError).toBe(true); + expect(badCycle.structuredContent.error.message).toContain('cycle must be a string'); + + vi.restoreAllMocks(); + }); }); From be2a374562d6e99e2ba88e33d534e221a93d9311 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 22:41:16 -0700 Subject: [PATCH 13/29] fix(workspace): repair metadata moves and cleared legends --- src/index.ts | 25 +++++++++-------- tests/api.test.ts | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index e51aff3..01ea163 100644 --- a/src/index.ts +++ b/src/index.ts @@ -359,16 +359,14 @@ export class Workspace { mkdirSync(targetDir, { recursive: true }); const targetPath = resolve(targetDir, fileName); - - if (fullPath === targetPath) { - return relative(this.root, targetPath); - } - if (existsSync(targetPath)) { + if (fullPath !== targetPath && existsSync(targetPath)) { throw new MethodError(`Destination already exists: ${relative(this.root, targetPath)}`); } - renameSync(fullPath, targetPath); + if (fullPath !== targetPath) { + renameSync(fullPath, targetPath); + } const frontmatterUpdates: Record = { lane: targetLane }; if (metadata.legend !== undefined) { frontmatterUpdates.legend = metadata.legend; @@ -504,12 +502,15 @@ export class Workspace { const stem = fileStem(path); const filenameMetadata = splitLegend(stem); const frontmatter = fmReadFrontmatter(path); + const hasExplicitLegend = Object.prototype.hasOwnProperty.call(frontmatter, 'legend'); return { stem, lane: normalizeBacklogLane(frontmatter.lane) ?? fallbackLane, path: relative(this.root, path), - legend: normalizeLegend(frontmatter.legend) ?? filenameMetadata.legend, + legend: hasExplicitLegend + ? normalizeLegend(frontmatter.legend) + : filenameMetadata.legend, slug: filenameMetadata.slug, }; } @@ -592,9 +593,9 @@ export const readHeading = fmReadHeading; export const readBody = fmReadBody; function readDesignLegend(path: string): string | undefined { - const frontmatterLegend = normalizeLegend(fmReadFrontmatter(path).legend); - if (frontmatterLegend !== undefined) { - return frontmatterLegend === 'NONE' ? undefined : frontmatterLegend; + const frontmatter = fmReadFrontmatter(path); + if (Object.prototype.hasOwnProperty.call(frontmatter, 'legend')) { + return normalizeLegend(frontmatter.legend); } for (const line of readFileSync(path, 'utf8').split(/\r?\n/u)) { @@ -644,7 +645,9 @@ function normalizeBacklogLane(value: string | undefined): Lane | 'root' | undefi function normalizeLegend(value: string | undefined): string | undefined { const normalized = value?.trim().toUpperCase(); - return normalized === undefined || normalized.length === 0 ? undefined : normalized; + return normalized === undefined || normalized.length === 0 || normalized === 'NONE' + ? undefined + : normalized; } function sanitizeWitnessOutput(value: string, root: string): string { diff --git a/tests/api.test.ts b/tests/api.test.ts index 26dfb1c..4d96a13 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -125,6 +125,38 @@ describe('Method API', () => { }); }); + it('If I move an item to the lane it is already in, does METHOD still repair stale or missing frontmatter metadata?', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/up-next/PROCESS_same-lane-repair.md'), + [ + '---', + 'title: "Same Lane Repair"', + 'lane: inbox', + '---', + '', + '# Same Lane Repair', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const moved = workspace.moveBacklogItem( + 'docs/method/backlog/up-next/PROCESS_same-lane-repair.md', + 'up-next', + ); + + expect(moved).toBe('docs/method/backlog/up-next/PROCESS_same-lane-repair.md'); + expect(workspace.readFrontmatter(moved)).toMatchObject({ + lane: 'up-next', + legend: 'PROCESS', + }); + }); + it('If a backlog card has stale filename legend text, does `method pull` still carry the YAML legend into the design packet?', () => { const root = createTempRoot(); initWorkspace(root); @@ -151,6 +183,42 @@ describe('Method API', () => { expect(readFileSync(cycle.designDoc, 'utf8')).toContain('Legend: SYNTH'); }); + it('If frontmatter explicitly clears a legacy filename legend with `NONE`, does METHOD stop falling back to the filename prefix?', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_frontmatter-clears-legend.md'), + [ + '---', + 'title: "Frontmatter Clears Legend"', + 'legend: NONE', + 'lane: asap', + '---', + '', + '# Frontmatter Clears Legend', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const status = workspace.status(); + expect(status.backlog.asap).toContainEqual({ + stem: 'PROCESS_frontmatter-clears-legend', + lane: 'asap', + path: 'docs/method/backlog/asap/PROCESS_frontmatter-clears-legend.md', + legend: undefined, + slug: 'frontmatter-clears-legend', + }); + + const cycle = workspace.pullItem('PROCESS_frontmatter-clears-legend'); + const designDoc = readFileSync(cycle.designDoc, 'utf8'); + expect(designDoc).toContain('legend: none'); + expect(designDoc).toContain('Legend: none'); + }); + it('Can I consume backlog lane and legend from `Workspace.status()` without reverse-engineering filename prefixes or folder names?', () => { const root = createTempRoot(); initWorkspace(root); From c4032e6967eb6405401408be9503d662ee329e92 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 22:41:20 -0700 Subject: [PATCH 14/29] fix(renderers): harden scaffold output contracts --- src/renderers.ts | 21 +++++++++++++-------- tests/cli.test.ts | 4 +++- tests/ship-sync.test.ts | 2 +- tests/witness.test.ts | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/renderers.ts b/src/renderers.ts index 0d34b4b..24c475b 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -78,6 +78,7 @@ export function renderWitnessDoc(options: { driftResult: string; }): string { const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); + const driftResult = options.driftResult.trim() || 'No drift output captured.'; return [ '---', `title: "Verification Witness for Cycle ${options.cycle.number}"`, @@ -90,14 +91,14 @@ export function renderWitnessDoc(options: { '', '## Test Results', '', - '```', + '```text', options.testResult.trim(), '```', '', '## Drift Results', '', - '```', - options.driftResult.trim(), + '```text', + driftResult, '```', '', '## Manual Verification', @@ -179,6 +180,9 @@ export function renderRetroDoc(options: { outcome: Outcome; witnessDir: string; }): string { + if (options.outcome !== 'hill-met' && options.outcome !== 'partial' && options.outcome !== 'not-met') { + throw new Error('Outcome is required and must be one of: hill-met, partial, not-met.'); + } const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); const designDoc = relative(options.root, options.cycle.designDoc); return [ @@ -192,10 +196,6 @@ export function renderRetroDoc(options: { '', `# ${title} Retro`, '', - `Design: \`${designDoc}\``, - `Outcome: ${options.outcome}`, - 'Drift check: yes', - '', '## Summary', '', 'TBD', @@ -226,5 +226,10 @@ export function renderRetroDoc(options: { } function yamlString(value: string): string { - return `"${value.replace(/\\/gu, '\\\\').replace(/"/gu, '\\"')}"`; + return `"${value + .replace(/\\/gu, '\\\\') + .replace(/"/gu, '\\"') + .replace(/\r\n/gu, '\\r\\n') + .replace(/\r/gu, '\\r') + .replace(/\n/gu, '\\n')}"`; } diff --git a/tests/cli.test.ts b/tests/cli.test.ts index d4ed427..ef17453 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -157,7 +157,9 @@ describe('method CLI', () => { expect(retroDoc).toMatch(/^outcome:\s+partial$/mu); expect(retroDoc).toMatch(/^drift_check:\s+yes$/mu); expect(retroDoc).toMatch(/^design_doc:\s+"docs\/design\/0001-method-cli\/method-cli\.md"$/mu); - expect(retroDoc).toContain('Outcome: partial'); + expect(retroDoc).not.toContain('Design: `docs/design/0001-method-cli/method-cli.md`'); + expect(retroDoc).not.toContain('Outcome: partial'); + expect(retroDoc).not.toContain('Drift check: yes'); }); it('Can I rely on scaffolded design docs to satisfy the repo docs contract without manual patching?', async () => { diff --git a/tests/ship-sync.test.ts b/tests/ship-sync.test.ts index 8282b05..5115951 100644 --- a/tests/ship-sync.test.ts +++ b/tests/ship-sync.test.ts @@ -1,4 +1,4 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 87f1b58..47ceb05 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { initWorkspace, Workspace } from '../src/index.js'; +import { renderDesignDoc, renderWitnessDoc } from '../src/renderers.js'; const tempRoots: string[] = []; @@ -58,4 +59,36 @@ describe('Automated Witness Capture', () => { expect(content).toContain('[MOCK] Output for tsx src/cli.ts drift 0001-witness-test'); expect(content).toContain('- [x] Automated capture completed successfully.'); }); + + it('Does the witness scaffold label fenced output as text and avoid an empty drift-results fence?', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + workspace.captureIdea('Witness Fence Test', 'PROCESS', 'Witness Fence Test'); + const cycle = workspace.pullItem('PROCESS_witness-fence-test'); + + const content = renderWitnessDoc({ + cycle, + testResult: 'npm test output', + driftResult: '', + }); + + expect(content).toContain('## Test Results'); + expect(content).toContain('```text\nnpm test output\n```'); + expect(content).toContain('## Drift Results'); + expect(content).toContain('```text\nNo drift output captured.\n```'); + }); + + it('Does YAML frontmatter escape embedded line breaks instead of letting YAML fold them away?', () => { + const content = renderDesignDoc({ + cycleName: '0001-line-breaks', + title: 'Line 1\nLine 2', + source: 'docs/method/backlog/asap/PROCESS_line-1\nline-2.md', + backlogBody: 'Body', + }); + + expect(content).toContain('title: "Line 1\\nLine 2"'); + expect(content).toContain('source_backlog: "docs/method/backlog/asap/PROCESS_line-1\\nline-2.md"'); + }); }); From 7c9f40ee8f250446213e44f60eb5dfa9c7fec87d Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 22:41:28 -0700 Subject: [PATCH 15/29] docs(method): address review cleanup across backlog and retros --- .../bad-code/PROCESS_dist-output-pruning.md | 46 +++++++++++++++--- .../inbox/PROCESS_semantic-drift-detector.md | 2 +- ...PROCESS_live-legend-definition-coverage.md | 2 +- docs/method/feedback/archive/README.md | 28 +++++++++++ docs/method/graveyard/.gitkeep | 1 - .../graveyard/MCP_status-summary-mode.md | 2 +- docs/method/graveyard/README.md | 47 +++++++++++++++++++ ...backlog-metadata-single-source-of-truth.md | 4 -- .../witness/verification.md | 5 +- .../generated-doc-scaffold-contract.md | 4 -- .../witness/verification.md | 5 +- .../mcp-tool-result-contract.md | 10 ++-- .../witness/verification.md | 5 +- .../bearing-truthfulness.md | 9 ++-- .../witness/verification.md | 5 +- 15 files changed, 135 insertions(+), 40 deletions(-) delete mode 100644 docs/method/graveyard/.gitkeep create mode 100644 docs/method/graveyard/README.md diff --git a/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md b/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md index 57ab17b..34d408a 100644 --- a/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md +++ b/docs/method/backlog/bad-code/PROCESS_dist-output-pruning.md @@ -6,12 +6,44 @@ lane: bad-code # Dist Output Pruning -The checked-in `dist/` tree still contains `workspace.*` artifacts even -though there is no longer a matching `src/workspace.ts`. That means -build output can retain dead files across refactors and package stale -implementation surfaces that are no longer real. +Build output can retain stale artifacts after source modules are +removed, renamed, or split across refactors. That drift makes the +published `dist/` tree a misleading projection of the live source tree. This is bad code because published artifacts stop being a trustworthy -projection of current source. The build or release path should either -clean `dist/` before emit or explicitly validate that `dist/` matches -live source modules. +projection of current source. + +## Proposed Solution + +Option 1 (preferred): clean before emit. + +- Update the `package.json` build path to remove `dist/` before `tsc` + runs so every build starts from repo truth. +- Keep the clean step in the same build path humans and CI use, rather + than relying on a separate manual command. + +Option 2: validate `dist/` against source. + +- Add a validation script that checks `dist/**/*.js` and + `dist/**/*.d.ts` against the current `src/**/*.ts` module set. +- Run that validation in CI and release pre-flight so stale checked-in + artifacts fail loudly instead of shipping quietly. + +## Owner + +- Human: Repository operator maintaining release and CI behavior. +- Agent: Implementation agent tightening the build path. + +## Priority + +Medium. This affects artifact truth and release hygiene, even when +runtime behavior happens to stay green. + +## Acceptance Criteria + +- [ ] The standard build path removes stale `dist/` artifacts before + emit or proves they are current. +- [ ] CI or release pre-flight fails when `dist/` no longer matches the + live `src/` module set. +- [ ] The chosen build/validation path is documented in the relevant + operator docs. diff --git a/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md index a88c070..6719ef3 100644 --- a/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md +++ b/docs/method/backlog/inbox/PROCESS_semantic-drift-detector.md @@ -12,7 +12,7 @@ conceptually identical. ## Matching Rules -- **Automatic Match**: Cosine similarity >= 0.80 or LLM confidence >= 0.9. +- **Automatic Match**: Cosine similarity >= 0.80 or LLM confidence >= 0.90. - **Human Review**: Similarity between 0.65 and 0.80 triggers a "Near Miss" hint requiring manual confirmation. - **Non-Match**: Marked as drift if confidence is below 0.65. diff --git a/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md b/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md index 485fa39..49f7608 100644 --- a/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md +++ b/docs/method/backlog/up-next/PROCESS_live-legend-definition-coverage.md @@ -12,5 +12,5 @@ and signposts currently treat legend docs as the discovered taxonomy, which means the repo can have real legend-bearing work that is not actually documented as a first-class legend. -This is taxonomy drift. If a legend is real enough to organize backlog +This is taxonomy drift. If a legend is established enough to organize backlog work, it should either have a legend doc or stop being used as a legend. diff --git a/docs/method/feedback/archive/README.md b/docs/method/feedback/archive/README.md index ee82001..71bd6c1 100644 --- a/docs/method/feedback/archive/README.md +++ b/docs/method/feedback/archive/README.md @@ -16,5 +16,33 @@ Each archived document should preserve: - links or references to any backlog items, docs updates, or other outputs created from it +## Archive Entry Template + +Use a minimal, predictable skeleton when archiving processed feedback: + +```markdown +# + +- Date: YYYY-MM-DD +- Author: +- Tags/Status: archived, no-action, converted-to-backlog, etc. + +## Original Feedback + + + +## Disposition + + + +## Related Items / Links + +- + +## Optional Follow-up Actions + +- [ ] +``` + The goal is provenance, not cleanliness theater. Deleting processed feedback throws away context that the repo may need later. diff --git a/docs/method/graveyard/.gitkeep b/docs/method/graveyard/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/docs/method/graveyard/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/method/graveyard/MCP_status-summary-mode.md b/docs/method/graveyard/MCP_status-summary-mode.md index e52eaae..4c501c9 100644 --- a/docs/method/graveyard/MCP_status-summary-mode.md +++ b/docs/method/graveyard/MCP_status-summary-mode.md @@ -20,7 +20,7 @@ The summary-mode behavior was absorbed into the broader MCP structured result contract, so `method_status` now defaults to compact structured summary output instead of needing a standalone follow-on. -## Proposal +## Original Proposal Add an optional `summary` parameter (default `true`) to `method_status`. diff --git a/docs/method/graveyard/README.md b/docs/method/graveyard/README.md new file mode 100644 index 0000000..ca809ba --- /dev/null +++ b/docs/method/graveyard/README.md @@ -0,0 +1,47 @@ +--- +title: "Graveyard" +--- + +# Graveyard + +Retired METHOD backlog items and related docs live here when the repo +should remember them but no longer treat them as active work. + +## Purpose and Scope + +Use the graveyard for backlog items, contracts, or process notes that +were superseded, absorbed by a broader cycle, or explicitly retired. +This directory is not a trash can. Items move here when their history +still matters. + +## Retention and Deletion Policy + +- Keep graveyard entries while they still explain why a path was closed + or absorbed. +- Delete only when the historical value is genuinely gone and no live + artifact depends on the tombstone for context. +- Prefer updating a graveyard doc with final disposition details over + silently removing it. + +## Entry Criteria + +- A backlog item was completed indirectly by a broader cycle. +- A standalone proposal was superseded by a stronger contract. +- A document should remain searchable as historical context but should + not return to active prioritization by default. + +## Exit Criteria + +- A retired idea becomes live work again and is reintroduced as a new + backlog item with fresh scope. +- Multiple tombstones are consolidated into a clearer retrospective or + other durable history artifact. + +## Inventory + +- `MCP_status-summary-mode.md` + Retired because cycle `0032-mcp-tool-result-contract` absorbed the + summary-mode behavior into the broader MCP result contract. +- `PROCESS_legend-should-be-in-frontmatter-yaml.md` + Retired because cycle `0030-backlog-metadata-single-source-of-truth` + absorbed the legend-frontmatter follow-on directly. diff --git a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md index 73f305f..7b127f2 100644 --- a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md +++ b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md @@ -8,10 +8,6 @@ drift_check: yes # Backlog Metadata Single Source Of Truth Retro -Design: `docs/design/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md` -Outcome: hill-met -Drift check: yes - ## Summary This cycle made backlog metadata frontmatter-first across the live diff --git a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md index 7904711..064e07c 100644 --- a/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md +++ b/docs/method/retro/0030-backlog-metadata-single-source-of-truth/witness/verification.md @@ -9,7 +9,7 @@ behavior and adheres to the repo invariants. ## Test Results -``` +```text > method@0.3.0 test > vitest run --config vitest.config.ts @@ -25,7 +25,8 @@ behavior and adheres to the repo invariants. ## Drift Results -``` +```text +No drift output captured. ``` diff --git a/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md b/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md index 7c8c112..d041807 100644 --- a/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md +++ b/docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md @@ -8,10 +8,6 @@ drift_check: yes # Generated Doc Scaffold Contract Retro -Design: `docs/design/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md` -Outcome: hill-met -Drift check: yes - ## Summary This cycle brought METHOD's generated design and retro scaffolds back diff --git a/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md b/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md index 7f308be..50ba90f 100644 --- a/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md +++ b/docs/method/retro/0031-generated-doc-scaffold-contract/witness/verification.md @@ -9,7 +9,7 @@ behavior and adheres to the repo invariants. ## Test Results -``` +```text > method@0.3.0 test > vitest run --config vitest.config.ts @@ -25,7 +25,8 @@ behavior and adheres to the repo invariants. ## Drift Results -``` +```text +No drift output captured. ``` diff --git a/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md b/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md index b7c24f6..ad52154 100644 --- a/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md +++ b/docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md @@ -8,10 +8,6 @@ drift_check: yes # MCP Tool Result Contract Retro -Design: `docs/design/0032-mcp-tool-result-contract/mcp-tool-result-contract.md` -Outcome: hill-met -Drift check: yes - ## Summary This cycle replaced MCP text-blob responses with a stable @@ -30,11 +26,11 @@ fix. - None recorded. -## New Debt +## Observed Debt - `PROCESS_witness-drift-output-capture` remains open: the close-time - verification witness still records an empty `## Drift Results` block - even when the standalone drift command returns output. + verification witness in cycle `0032` re-observed an already-filed + drift-output capture gap from cycle `0031`. ## Cool Ideas diff --git a/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md b/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md index 1a7f01c..015837a 100644 --- a/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md +++ b/docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md @@ -9,7 +9,7 @@ behavior and adheres to the repo invariants. ## Test Results -``` +```text > method@0.3.0 test > vitest run --config vitest.config.ts @@ -25,7 +25,8 @@ behavior and adheres to the repo invariants. ## Drift Results -``` +```text +No drift output captured. ``` diff --git a/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md b/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md index 6c32cc3..c25fd14 100644 --- a/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md +++ b/docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md @@ -8,10 +8,6 @@ drift_check: yes # BEARING Truthfulness Retro -Design: `docs/design/0033-bearing-truthfulness/bearing-truthfulness.md` -Outcome: hill-met -Drift check: yes - ## Summary This cycle made `renderBearing` stop speaking beyond repo truth. The @@ -30,10 +26,11 @@ sync after merge on `main`. - None recorded. -## New Debt +## Observed Debt - `PROCESS_witness-drift-output-capture` remains open: close-time - verification witnesses still record an empty `## Drift Results` block. + verification witnesses in cycle `0033` still re-observed the existing + drift-output capture gap. ## Cool Ideas diff --git a/docs/method/retro/0033-bearing-truthfulness/witness/verification.md b/docs/method/retro/0033-bearing-truthfulness/witness/verification.md index 98f2351..a2ac00c 100644 --- a/docs/method/retro/0033-bearing-truthfulness/witness/verification.md +++ b/docs/method/retro/0033-bearing-truthfulness/witness/verification.md @@ -9,7 +9,7 @@ behavior and adheres to the repo invariants. ## Test Results -``` +```text > method@0.3.0 test > vitest run --config vitest.config.ts @@ -25,7 +25,8 @@ behavior and adheres to the repo invariants. ## Drift Results -``` +```text +No drift output captured. ``` From b17b25ba6e60b28456e78a95f1a2b51aeba69039 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 22:46:26 -0700 Subject: [PATCH 16/29] docs(backlog): capture review follow-on ideas --- ...CESS_mcp-runtime-input-validation-audit.md | 23 +++++++++++++++++++ .../PROCESS_cycle-packet-markdownlint-gate.md | 16 +++++++++++++ .../PROCESS_retro-scaffold-refresh-command.md | 15 ++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md create mode 100644 docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md diff --git a/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md b/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md new file mode 100644 index 0000000..08341d7 --- /dev/null +++ b/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md @@ -0,0 +1,23 @@ +--- +title: "MCP Runtime Input Validation Audit" +legend: PROCESS +lane: bad-code +--- + +# MCP Runtime Input Validation Audit + +`method_close` just proved that MCP tool schemas are not enough on their +own. The runtime handler can still accept structurally invalid inputs if +it trusts type assertions instead of validating the actual request. + +This is bad code because the advertised tool contract and the code path +can silently diverge. One tool was repaired here, but the rest of the +MCP surface should be audited for the same schema-vs-runtime gap. + +## Acceptance Criteria + +- [ ] Each MCP mutation tool validates or rejects runtime inputs before + mutating repo state. +- [ ] Structured error responses stay clear and consistent when + validation fails. +- [ ] Tests cover at least one invalid-input case for each audited tool. diff --git a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md new file mode 100644 index 0000000..3af3718 --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md @@ -0,0 +1,16 @@ +--- +title: "Cycle Packet Markdownlint Gate" +legend: PROCESS +lane: cool-ideas +--- + +# Cycle Packet Markdownlint Gate + +This review round caught witness and retro packet issues that a small +markdown lint pass could have surfaced before PR review: unlabeled code +fences, empty placeholder blocks, and wording drift inside committed +artifacts. + +Idea: add a bounded markdown validation step for cycle packets and other +generated repo artifacts so contract-level doc issues fail fast instead +of landing as manual review cleanup. diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md new file mode 100644 index 0000000..018f2cd --- /dev/null +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md @@ -0,0 +1,15 @@ +--- +title: "Retro Scaffold Refresh Command" +legend: PROCESS +lane: cool-ideas +--- + +# Retro Scaffold Refresh Command + +When scaffold contracts change, already-generated retro and witness +artifacts currently need manual repair. A narrow refresh command could +re-render committed cycle packet scaffolds in place while preserving the +human-written sections that should not be regenerated. + +That would turn scaffold contract upgrades from one-off cleanup work +into a repeatable maintenance move. From 54da8c7a5413765735e80670cdeef57fc7c86193 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:10:32 -0700 Subject: [PATCH 17/29] fix(mcp): respect explicit sync flags and legend contracts --- src/index.ts | 10 +++++++--- src/mcp.ts | 12 ++++++------ src/renderers.ts | 2 +- tests/api.test.ts | 38 +++++++++++++++++++++++++++++++++++++- tests/cli.test.ts | 4 ++-- tests/docs.test.ts | 9 +++++++-- tests/mcp.test.ts | 37 ++++++++++++++++++++++++++++++++++++- tests/witness.test.ts | 13 +++++++++++++ 8 files changed, 109 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index 01ea163..eba33e1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,6 +53,7 @@ function resolvePaths(root: string, paths: PathsConfig): ResolvedPaths { }; } +const LEGEND_CODE_PATTERN = /^[A-Z][A-Z0-9]*$/u; const LEGEND_PATTERN = /^(?[A-Z][A-Z0-9]*)_(?.+)$/; const CYCLE_PATTERN = /^(?\d{4})-(?[a-z0-9][a-z0-9-]*)$/; @@ -140,7 +141,7 @@ export class Workspace { let prefix = ''; if (legend !== undefined) { const normalized = legend.trim().toUpperCase(); - if (!/^[A-Z][A-Z0-9]*$/u.test(normalized)) { + if (!LEGEND_CODE_PATTERN.test(normalized)) { throw new MethodError('Legend codes must be uppercase letters and numbers.'); } prefix = `${normalized}_`; @@ -603,7 +604,7 @@ function readDesignLegend(path: string): string | undefined { continue; } const value = line.slice('Legend: '.length).trim(); - return value === 'none' ? undefined : value; + return normalizeLegend(value); } return undefined; } @@ -645,7 +646,10 @@ function normalizeBacklogLane(value: string | undefined): Lane | 'root' | undefi function normalizeLegend(value: string | undefined): string | undefined { const normalized = value?.trim().toUpperCase(); - return normalized === undefined || normalized.length === 0 || normalized === 'NONE' + return normalized === undefined + || normalized.length === 0 + || normalized === 'NONE' + || !LEGEND_CODE_PATTERN.test(normalized) ? undefined : normalized; } diff --git a/src/mcp.ts b/src/mcp.ts index 6a67f5a..76a0c1f 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -3,7 +3,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot import { basename, relative } from 'node:path'; import { Workspace } from './index.js'; import { GitHubAdapter, type GitHubSyncResult } from './adapters/github.js'; -import type { Outcome, WorkspaceStatus } from './domain.js'; +import type { Cycle, Outcome, WorkspaceStatus } from './domain.js'; const workspaceProperty = { workspace: { type: 'string' as const, description: 'Absolute path to the METHOD workspace root directory' } }; @@ -121,7 +121,7 @@ export function createMcpServer() { const status = workspace.status(); const summary = args.summary === true; if (summary) { - const summaryResult = summarizeStatus(workspace, status); + const summaryResult = summarizeStatus(status, workspace.closedCycles()); return successResult( 'method_status', renderStatusSummaryText(summaryResult), @@ -210,8 +210,8 @@ export function createMcpServer() { } if (request.params.name === 'method_sync_github') { - const push = (args.push as boolean | undefined) || (!args.push && !args.pull); - const pull = (args.pull as boolean | undefined) || false; + const push = (args.push as boolean | undefined) ?? !(args.pull as boolean | undefined); + const pull = (args.pull as boolean | undefined) ?? false; const token = workspace.config.github_token; const repoFull = workspace.config.github_repo; @@ -278,7 +278,7 @@ export function createMcpServer() { return server; } -function summarizeStatus(workspace: Workspace, status: WorkspaceStatus): McpStatusSummary { +function summarizeStatus(status: WorkspaceStatus, closedCycles: Cycle[]): McpStatusSummary { const laneCounts = Object.fromEntries( Object.entries(status.backlog).map(([lane, items]) => [lane, items.length]), ); @@ -287,7 +287,7 @@ function summarizeStatus(workspace: Workspace, status: WorkspaceStatus): McpStat root: status.root, laneCounts, activeCycles: status.activeCycles.map((cycle) => ({ name: cycle.name, slug: cycle.slug })), - retroCount: workspace.closedCycles().length, + retroCount: closedCycles.length, legendHealth: status.legendHealth, }; } diff --git a/src/renderers.ts b/src/renderers.ts index 24c475b..53cc677 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -119,7 +119,7 @@ export function renderDesignDoc(options: { return [ '---', `title: ${yamlString(options.title)}`, - `legend: ${legendValue}`, + `legend: ${yamlString(legendValue)}`, `cycle: ${yamlString(options.cycleName)}`, `source_backlog: ${yamlString(options.source)}`, '---', diff --git a/tests/api.test.ts b/tests/api.test.ts index 4d96a13..b46dbc4 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -215,7 +215,43 @@ describe('Method API', () => { const cycle = workspace.pullItem('PROCESS_frontmatter-clears-legend'); const designDoc = readFileSync(cycle.designDoc, 'utf8'); - expect(designDoc).toContain('legend: none'); + expect(designDoc).toContain('legend: "none"'); + expect(designDoc).toContain('Legend: none'); + }); + + it('Invalid explicit frontmatter legends do not override backlog filename truth or leak into pulled design docs.', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + writeFileSync( + join(root, 'docs/method/backlog/asap/PROCESS_invalid-frontmatter-legend.md'), + [ + '---', + 'title: "Invalid Frontmatter Legend"', + 'legend: human review', + 'lane: asap', + '---', + '', + '# Invalid Frontmatter Legend', + '', + 'Body', + ].join('\n'), + 'utf8', + ); + + const status = workspace.status(); + expect(status.backlog.asap).toContainEqual({ + stem: 'PROCESS_invalid-frontmatter-legend', + lane: 'asap', + path: 'docs/method/backlog/asap/PROCESS_invalid-frontmatter-legend.md', + legend: undefined, + slug: 'invalid-frontmatter-legend', + }); + + const cycle = workspace.pullItem('PROCESS_invalid-frontmatter-legend'); + const designDoc = readFileSync(cycle.designDoc, 'utf8'); + expect(designDoc).toContain('legend: "none"'); expect(designDoc).toContain('Legend: none'); }); diff --git a/tests/cli.test.ts b/tests/cli.test.ts index ef17453..b28ed55 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -118,7 +118,7 @@ describe('method CLI', () => { const designDoc = readFile(root, 'docs/design/0001-strand-lifecycle/strand-lifecycle.md'); expect(designDoc).toMatch(/^---\n[\s\S]+?\n---\n/u); expect(designDoc).toMatch(/^title:\s+"Strand Lifecycle"$/mu); - expect(designDoc).toMatch(/^legend:\s+PROTO$/mu); + expect(designDoc).toMatch(/^legend:\s+"PROTO"$/mu); expect(designDoc).toMatch(/^cycle:\s+"0001-strand-lifecycle"$/mu); expect(designDoc).toMatch(/^source_backlog:\s+"docs\/method\/backlog\/asap\/PROTO_strand-lifecycle\.md"$/mu); expect(designDoc).toContain('Legend: PROTO'); @@ -189,7 +189,7 @@ describe('method CLI', () => { expect(exitCode).toBe(0); const designDoc = readFile(root, 'docs/design/0001-scaffold-contract/scaffold-contract.md'); - expect(designDoc).toMatch(/^legend:\s+PROCESS$/mu); + expect(designDoc).toMatch(/^legend:\s+"PROCESS"$/mu); expect(designDoc).toMatch(/^cycle:\s+"0001-scaffold-contract"$/mu); expect(designDoc).not.toContain('- Human: TBD'); expect(designDoc).not.toContain('- Agent: TBD'); diff --git a/tests/docs.test.ts b/tests/docs.test.ts index 81db8dc..f963da7 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -354,6 +354,9 @@ describe('METHOD docs', () => { 'outcome', 'drift_check', 'lane', + 'owner', + 'priority', + 'acceptance_criteria', 'github_issue_id', 'github_issue_url', 'github_labels', @@ -725,8 +728,10 @@ describe('METHOD docs', () => { expect(mcp).toContain('method_sync_ship'); expect(mcp).toContain('method_sync_github'); expect(mcp).toContain('method_capture_witness'); - expect(mcp).toContain('summary'); - expect(mcp).toContain('structuredContent'); + expect(mcp).toMatch(/### `method_status`[\s\S]*- `summary` \(optional\) `boolean`/u); + expect(mcp).toMatch(/Machine-readable callers should consume `structuredContent`\./u); + expect(mcp).toMatch(/On success, `structuredContent` includes:[\s\S]*- `tool`[\s\S]*- `ok: true`[\s\S]*- `result`/u); + expect(mcp).toMatch(/On failure, tools set `isError: true` and `structuredContent` includes:[\s\S]*- `tool`[\s\S]*- `ok: false`[\s\S]*- `error\.message`/u); // README references both docs expect(readme).toContain('docs/CLI.md'); diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 250545d..a361249 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -1,7 +1,8 @@ -import { mkdtempSync, rmSync } from 'node:fs'; +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; +import { GitHubAdapter } from '../src/adapters/github.js'; import { createMcpServer } from '../src/mcp.js'; import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js'; @@ -14,6 +15,7 @@ afterEach(() => { rmSync(root, { recursive: true, force: true }); } tempRoots.length = 0; + vi.restoreAllMocks(); }); function createTempRoot(): string { @@ -271,4 +273,37 @@ describe('MCP Server', () => { vi.restoreAllMocks(); }); + + it('Does `method_sync_github` respect explicit false flags instead of treating them as omitted defaults?', async () => { + const root = createTempRoot(); + initWorkspace(root); + writeFileSync( + join(root, '.method.json'), + JSON.stringify({ github_token: 'test-token', github_repo: 'owner/repo' }), + 'utf8', + ); + + const pushBacklog = vi.spyOn(GitHubAdapter.prototype, 'pushBacklog').mockResolvedValue([]); + const pullBacklog = vi.spyOn(GitHubAdapter.prototype, 'pullBacklog').mockResolvedValue([]); + const callToolHandler = createCallToolHarness(); + + const result = await callToolHandler({ + params: { + name: 'method_sync_github', + arguments: { + workspace: root, + push: false, + pull: false, + }, + }, + }); + + expect(result.isError).toBe(false); + expect(result.structuredContent.tool).toBe('method_sync_github'); + expect(result.structuredContent.result.pushRequested).toBe(false); + expect(result.structuredContent.result.pullRequested).toBe(false); + expect(pushBacklog).not.toHaveBeenCalled(); + expect(pullBacklog).not.toHaveBeenCalled(); + expect(result.content[0].text).toContain('No changes.'); + }); }); diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 47ceb05..98482e1 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -91,4 +91,17 @@ describe('Automated Witness Capture', () => { expect(content).toContain('title: "Line 1\\nLine 2"'); expect(content).toContain('source_backlog: "docs/method/backlog/asap/PROCESS_line-1\\nline-2.md"'); }); + + it('Does design-doc frontmatter YAML-escape legends instead of writing raw scalar text?', () => { + const content = renderDesignDoc({ + cycleName: '0001-legend-escape', + title: 'Legend Escape', + legend: 'FEAT:alpha #quoted "value"', + source: 'docs/method/backlog/asap/PROCESS_legend-escape.md', + backlogBody: 'Body', + }); + + expect(content).toContain('legend: "FEAT:alpha #quoted \\"value\\""'); + expect(content).toContain('Legend: FEAT:alpha #quoted "value"'); + }); }); From 7ccef225ca5e0ddca2432ebaa0a0955383220f2a Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:10:54 -0700 Subject: [PATCH 18/29] docs(method): tighten review follow-on contracts --- ...CESS_mcp-runtime-input-validation-audit.md | 26 +++++++++--- .../PROCESS_cycle-packet-markdownlint-gate.md | 34 +++++++++++++++ .../PROCESS_retro-scaffold-refresh-command.md | 42 +++++++++++++++++++ .../graveyard/MCP_status-summary-mode.md | 13 ++++-- 4 files changed, 106 insertions(+), 9 deletions(-) diff --git a/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md b/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md index 08341d7..e649aee 100644 --- a/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md +++ b/docs/method/backlog/bad-code/PROCESS_mcp-runtime-input-validation-audit.md @@ -2,6 +2,12 @@ title: "MCP Runtime Input Validation Audit" legend: PROCESS lane: bad-code +owner: "METHOD maintainers" +priority: medium +acceptance_criteria: + - "Every audited MCP mutation tool rejects invalid runtime inputs before mutating repo state." + - "Validation failures return the canonical MCP error envelope documented in docs/MCP.md and implemented by src/mcp.ts:errorResult." + - "Tests assert the exact envelope shape for at least one invalid-input case per audited tool." --- # MCP Runtime Input Validation Audit @@ -16,8 +22,18 @@ MCP surface should be audited for the same schema-vs-runtime gap. ## Acceptance Criteria -- [ ] Each MCP mutation tool validates or rejects runtime inputs before - mutating repo state. -- [ ] Structured error responses stay clear and consistent when - validation fails. -- [ ] Tests cover at least one invalid-input case for each audited tool. +- [ ] Audit `method_inbox`, `method_pull`, `method_close`, + `method_sync_ship`, `method_sync_github`, and + `method_capture_witness` for schema-vs-runtime gaps before any + repo mutation happens. +- [ ] On validation failure, each audited tool returns the canonical MCP + error envelope documented in `docs/MCP.md` and implemented by + `src/mcp.ts#errorResult`: + `structuredContent.tool`, `structuredContent.ok: false`, + `structuredContent.error.message`, and `isError: true`. +- [ ] Add or update tests so at least one invalid-input case per audited + tool asserts the exact error envelope shape and field names rather + than only checking prose in `content[0].text`. +- [ ] Record which audited tools already conform, which required code + changes, and where the enforcing tests live so the audit is + reviewable without rediscovering scope. diff --git a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md index 3af3718..dff7745 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md +++ b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md @@ -2,6 +2,12 @@ title: "Cycle Packet Markdownlint Gate" legend: PROCESS lane: cool-ideas +owner: "METHOD maintainers" +priority: medium +acceptance_criteria: + - "The lint scope is explicitly limited to cycle packet markdown under docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md and docs/method/retro/[0-9][0-9][0-9][0-9]-*/**/*.md." + - "The gate enforces markdownlint rules MD040 and MD031 plus cycle-packet frontmatter key checks." + - "The chosen entrypoint exits 0 on clean packets and non-zero with file/rule-localized failures when any packet violates the contract." --- # Cycle Packet Markdownlint Gate @@ -14,3 +20,31 @@ artifacts. Idea: add a bounded markdown validation step for cycle packets and other generated repo artifacts so contract-level doc issues fail fast instead of landing as manual review cleanup. + +## Proposed Contract + +- Scope globs: + `docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md` and + `docs/method/retro/[0-9][0-9][0-9][0-9]-*/**/*.md`. +- Rules: + `MD040` (fenced code blocks need a language), + `MD031` (fenced code blocks need surrounding blank lines), and custom + packet checks for required frontmatter keys on generated docs. +- Frontmatter checks: + design docs require `title`, `legend`, `cycle`, and `source_backlog`; + retro docs require `title`, `cycle`, `design_doc`, `outcome`, and + `drift_check`. +- Integration point: + a dedicated `method validate markdown` entrypoint that can be invoked + locally, from pre-commit, and from a CI job such as + `markdown-contract`. +- Pass / fail behavior: + exit `0` when all scoped files satisfy the bounded rule set; exit + non-zero when any scoped file violates one of the named rules or + frontmatter checks, with output that names the file and failing rule. + +## Non-goals + +- [ ] Lint every markdown file in the repository. +- [ ] Replace broader review judgment on cycle quality with style-only + checks. diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md index 018f2cd..58ab1c3 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md @@ -2,6 +2,12 @@ title: "Retro Scaffold Refresh Command" legend: PROCESS lane: cool-ideas +owner: "METHOD maintainers" +priority: low +acceptance_criteria: + - "The command accepts explicit cycle IDs and glob selectors plus a dry-run mode." + - "Refresh updates only scaffold-owned frontmatter and generated placeholders while preserving human-authored retro sections and witness files." + - "The command reports deterministic diff and exit-code behavior for success, empty matches, invalid selectors, and write failures." --- # Retro Scaffold Refresh Command @@ -13,3 +19,39 @@ human-written sections that should not be regenerated. That would turn scaffold contract upgrades from one-off cleanup work into a repeatable maintenance move. + +## Proposed Contract + +- Command shape: + `method refresh retro [--dry-run]` +- Accepted inputs: + explicit cycle ids like `0003-bearing-truthfulness`, glob selectors + like `003[0-3]-*`, or a path list resolved to retro packet roots. +- Preserved content: + existing body text under `## Summary`, `## Playback Witness`, + `## Drift`, `## New Debt`, `## Cool Ideas`, and the current + `## Backlog Maintenance` checklist state, plus everything already + stored under each retro packet's `witness/` directory. +- Regenerated content: + scaffold-owned frontmatter fields, missing required headings, and any + generated placeholder blocks whose contract changed between renderer + versions. +- Expected diff behavior: + `--dry-run` prints which files would change and exits without writing; + write mode updates files in place and prints the touched packet paths. +- Failure modes and exit codes: + exit `0` when all matched packets are already current or are refreshed + successfully; exit `1` when no packets match; exit `2` for invalid + selectors or malformed packet structure; exit `3` for write failures. + +## Examples + +- `method refresh retro 0033-bearing-truthfulness` +- `method refresh retro 003[0-3]-* --dry-run` +- `method refresh retro docs/method/retro/0032-mcp-tool-result-contract` + +## PASS Criteria + +- [ ] Operators can refresh one packet or a bounded set of packets + without overwriting human-written retro content. +- [ ] The command explains exactly what changed or why it refused. diff --git a/docs/method/graveyard/MCP_status-summary-mode.md b/docs/method/graveyard/MCP_status-summary-mode.md index 4c501c9..501014e 100644 --- a/docs/method/graveyard/MCP_status-summary-mode.md +++ b/docs/method/graveyard/MCP_status-summary-mode.md @@ -16,13 +16,18 @@ common "where are we?" check. ## Disposition Retired as a separate backlog item in cycle `0032-mcp-tool-result-contract`. -The summary-mode behavior was absorbed into the broader MCP structured -result contract, so `method_status` now defaults to compact structured -summary output instead of needing a standalone follow-on. +The summary-mode idea was absorbed into the broader MCP structured +result contract, but the shipped behavior preserved backward +compatibility: `method_status` still returns full status when `summary` +is omitted, and only returns the compact summary when callers pass +`summary: true`. This graveyard note remains historical context, not the +current API contract. See `docs/MCP.md` and `tests/mcp.test.ts` for the +live behavior. ## Original Proposal -Add an optional `summary` parameter (default `true`) to `method_status`. +The original proposal was to add an optional `summary` parameter with +default `true`. That default was not adopted in the final implementation. **Summary mode** returns: - Lane counts: `{ inbox: 4, asap: 22, "up-next": 15, "cool-ideas": 50, "bad-code": 124 }` From 9ab626a19c69b7d6af93f4ec032d13e00b089e16 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:11:04 -0700 Subject: [PATCH 19/29] docs: record PR feedback fixes in changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf26efc..7c47cdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,3 +39,10 @@ First public release. 29 cycles closed, 127 tests passing. - docs/VISION.md: generated executive synthesis. ## Unreleased + +### Fixed + +- Tightened MCP runtime behavior around GitHub sync flag parsing, + explicit legend normalization, and design-doc frontmatter escaping. +- Hardened the review-facing docs backlog items so their acceptance + criteria and historical notes match the live contract. From 612b1990732e6f48dd0f77660f3513e0c5e4c8a1 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:21:44 -0700 Subject: [PATCH 20/29] docs(backlog): clarify review merge blockers --- .../cool-ideas/PROCESS_review-state-query.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md index 4165480..06baa37 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md +++ b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md @@ -2,6 +2,12 @@ title: "Review State Query" legend: PROCESS lane: cool-ideas +owner: "METHOD maintainers" +priority: medium +acceptance_criteria: + - "The review-state surface reports CI/check status, unresolved review-thread count, review decision, and bot-review pending state for the active PR." + - "The output includes a deterministic merge_ready boolean plus a blocker list naming exactly which states still prevent merge." + - "Humans and agents can answer 'is this PR actually mergeable yet?' without manually reconstructing GitHub thread/check state." --- # Review State Query @@ -11,3 +17,32 @@ coordination surface. A dedicated query or signpost that summarizes what is under review, awaiting merge, or blocked in PR state would make the system cheaper for both humans and agents who currently have to bounce out to forge context to answer that simple question. + +Recent PR review work showed the sharper version of this problem: a +branch can be technically clean while still being procedurally blocked +by pending bot review, stale `reviewDecision`, unresolved threads, or +checks that have not settled yet. That state is real coordination data, +but today it lives only in ad hoc `gh` inspection. + +## Proposed Contract + +- Surface: + `method review-state [--pr |--current-branch]` and/or a small + generated signpost section. +- Required fields: + PR number and URL, review decision, unresolved-thread count, failing + or pending checks, pending bot-review state, approval/change-request + counts, `merge_ready`, and `blockers`. +- Merge-ready rule: + `merge_ready` is true only when required checks are green, unresolved + review threads are zero, no required reviewer or bot still reports a + blocking state, and no known cooldown/rate-limit rule is active. +- Blocker output: + each blocker is named explicitly, for example `CodeRabbit review in + progress`, `reviewDecision still CHANGES_REQUESTED`, or + `2 unresolved review threads`. + +## Non-goals + +- [ ] Auto-merge a PR once the blockers list is empty. +- [ ] Replace human judgment about whether a change should merge. From 790d85b9593ba0605dc2ed9afffc176b06300d2d Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:54:51 -0700 Subject: [PATCH 21/29] fix(mcp): validate runtime inputs and confine backlog paths --- src/index.ts | 12 +++++++++++- src/mcp.ts | 20 ++++++++++++++++++-- src/renderers.ts | 19 +++++++++++++------ tests/api.test.ts | 31 +++++++++++++++++++++++++++++-- tests/mcp.test.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/witness.test.ts | 39 ++++++++++++++++++++++++++++++++++----- 6 files changed, 148 insertions(+), 16 deletions(-) diff --git a/src/index.ts b/src/index.ts index eba33e1..3deb314 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import GitPlumbing from '@git-stunts/plumbing'; -import { basename, dirname, relative, resolve } from 'node:path'; +import { basename, dirname, isAbsolute, relative, resolve } from 'node:path'; import { type BacklogItem, type Cycle, @@ -343,6 +343,7 @@ export class Workspace { moveBacklogItem(path: string, targetLane: Lane | 'graveyard'): string { const fullPath = resolve(this.root, path); + assertWorkspacePath(this.root, fullPath, 'Backlog path'); if (!existsSync(fullPath)) { throw new MethodError(`Backlog item not found: ${path}`); } @@ -413,6 +414,7 @@ export class Workspace { private resolveBacklogItem(item: string): string { const direct = resolve(this.root, item); if (existsSync(direct)) { + assertWorkspacePath(this.root, direct, 'Backlog path'); return direct; } @@ -580,6 +582,14 @@ function collectMarkdownFiles(root: string, maxDepth = 10): string[] { return files.sort((left, right) => left.localeCompare(right)); } +function assertWorkspacePath(root: string, fullPath: string, label: string): void { + const relativePath = relative(root, fullPath); + if (relativePath === '' || (!relativePath.startsWith('..') && !isAbsolute(relativePath))) { + return; + } + throw new MethodError(`${label} must stay inside the workspace: ${fullPath}`); +} + function splitLegend(stem: string): { legend?: string; slug: string } { const match = LEGEND_PATTERN.exec(stem); diff --git a/src/mcp.ts b/src/mcp.ts index 76a0c1f..9b251d2 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -210,8 +210,10 @@ export function createMcpServer() { } if (request.params.name === 'method_sync_github') { - const push = (args.push as boolean | undefined) ?? !(args.pull as boolean | undefined); - const pull = (args.pull as boolean | undefined) ?? false; + const validatedPush = validateOptionalBoolean(args.push, 'push'); + const validatedPull = validateOptionalBoolean(args.pull, 'pull'); + const pull = validatedPull ?? false; + const push = validatedPush ?? !pull; const token = workspace.config.github_token; const repoFull = workspace.config.github_repo; @@ -387,6 +389,20 @@ function validateOptionalString(value: unknown, name: string): string | undefine if (typeof value !== 'string') { throw new Error(`${name} must be a string when provided.`); } + const normalized = value.trim(); + if (normalized.length === 0) { + throw new Error(`${name} must not be empty.`); + } + return normalized; +} + +function validateOptionalBoolean(value: unknown, name: string): boolean | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== 'boolean') { + throw new Error(`${name} must be a boolean.`); + } return value; } diff --git a/src/renderers.ts b/src/renderers.ts index 53cc677..674d9f6 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -1,4 +1,4 @@ -import { relative } from 'node:path'; +import * as path from 'node:path'; import type { Cycle, Outcome, WorkspaceStatus } from './domain.js'; import { readHeading } from './frontmatter.js'; @@ -78,6 +78,7 @@ export function renderWitnessDoc(options: { driftResult: string; }): string { const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); + const testResult = options.testResult.trim() || 'No test output captured.'; const driftResult = options.driftResult.trim() || 'No drift output captured.'; return [ '---', @@ -92,7 +93,7 @@ export function renderWitnessDoc(options: { '## Test Results', '', '```text', - options.testResult.trim(), + testResult, '```', '', '## Drift Results', @@ -116,17 +117,18 @@ export function renderDesignDoc(options: { backlogBody: string; }): string { const legendValue = options.legend ?? 'none'; + const sourcePath = normalizeRepoPath(options.source); return [ '---', `title: ${yamlString(options.title)}`, `legend: ${yamlString(legendValue)}`, `cycle: ${yamlString(options.cycleName)}`, - `source_backlog: ${yamlString(options.source)}`, + `source_backlog: ${yamlString(sourcePath)}`, '---', '', `# ${options.title}`, '', - `Source backlog item: \`${options.source}\``, + `Source backlog item: \`${sourcePath}\``, `Legend: ${legendValue}`, '', '## Sponsors', @@ -184,7 +186,8 @@ export function renderRetroDoc(options: { throw new Error('Outcome is required and must be one of: hill-met, partial, not-met.'); } const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); - const designDoc = relative(options.root, options.cycle.designDoc); + const designDoc = normalizeRepoPath(path.relative(options.root, options.cycle.designDoc)); + const witnessDir = normalizeRepoPath(options.witnessDir); return [ '---', `title: ${yamlString(title)}`, @@ -202,7 +205,7 @@ export function renderRetroDoc(options: { '', '## Playback Witness', '', - `Add artifacts under \`${options.witnessDir}\` and link them here.`, + `Add artifacts under \`${witnessDir}\` and link them here.`, '', '## Drift', '', @@ -233,3 +236,7 @@ function yamlString(value: string): string { .replace(/\r/gu, '\\r') .replace(/\n/gu, '\\n')}"`; } + +function normalizeRepoPath(value: string): string { + return value.replace(/\\/gu, '/'); +} diff --git a/tests/api.test.ts b/tests/api.test.ts index b46dbc4..8f6a0cc 100644 --- a/tests/api.test.ts +++ b/tests/api.test.ts @@ -1,6 +1,6 @@ -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { join, relative as pathRelative } from 'node:path'; import { afterEach, describe, expect, it } from 'vitest'; import { initWorkspace, Workspace } from '../src/index.js'; @@ -157,6 +157,33 @@ describe('Method API', () => { }); }); + it('Rejects moving backlog paths that escape the workspace root.', () => { + const root = createTempRoot(); + const outsideRoot = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + const outsidePath = join(outsideRoot, 'outside.md'); + writeFileSync(outsidePath, '# Outside\n\nBody\n', 'utf8'); + + expect(() => workspace.moveBacklogItem(outsidePath, 'graveyard')).toThrow(/workspace/u); + expect(existsSync(outsidePath)).toBe(true); + }); + + it('Rejects pulling backlog items through traversal paths that escape the workspace root.', () => { + const root = createTempRoot(); + const outsideRoot = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + const outsidePath = join(outsideRoot, 'outside.md'); + writeFileSync(outsidePath, '# Outside\n\nBody\n', 'utf8'); + const traversalPath = pathRelative(root, outsidePath); + + expect(() => workspace.pullItem(traversalPath)).toThrow(/workspace/u); + expect(existsSync(outsidePath)).toBe(true); + }); + it('If a backlog card has stale filename legend text, does `method pull` still carry the YAML legend into the design packet?', () => { const root = createTempRoot(); initWorkspace(root); diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index a361249..91a4cdc 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -271,6 +271,20 @@ describe('MCP Server', () => { expect(badCycle.isError).toBe(true); expect(badCycle.structuredContent.error.message).toContain('cycle must be a string'); + const blankCycle = await callToolHandler({ + params: { + name: 'method_close', + arguments: { + workspace: root, + cycle: ' ', + driftCheck: true, + outcome: 'hill-met', + }, + }, + }); + expect(blankCycle.isError).toBe(true); + expect(blankCycle.structuredContent.error.message).toContain('cycle must not be empty'); + vi.restoreAllMocks(); }); @@ -306,4 +320,33 @@ describe('MCP Server', () => { expect(pullBacklog).not.toHaveBeenCalled(); expect(result.content[0].text).toContain('No changes.'); }); + + it('Does `method_sync_github` reject non-boolean push and pull flags before touching GitHub?', async () => { + const root = createTempRoot(); + initWorkspace(root); + writeFileSync( + join(root, '.method.json'), + JSON.stringify({ github_token: 'test-token', github_repo: 'owner/repo' }), + 'utf8', + ); + + const pushBacklog = vi.spyOn(GitHubAdapter.prototype, 'pushBacklog').mockResolvedValue([]); + const pullBacklog = vi.spyOn(GitHubAdapter.prototype, 'pullBacklog').mockResolvedValue([]); + const callToolHandler = createCallToolHarness(); + + const badPush = await callToolHandler({ + params: { + name: 'method_sync_github', + arguments: { + workspace: root, + push: 'false', + }, + }, + }); + + expect(badPush.isError).toBe(true); + expect(badPush.structuredContent.error.message).toContain('push must be a boolean'); + expect(pushBacklog).not.toHaveBeenCalled(); + expect(pullBacklog).not.toHaveBeenCalled(); + }); }); diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 98482e1..644a75a 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -1,9 +1,9 @@ -import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { initWorkspace, Workspace } from '../src/index.js'; -import { renderDesignDoc, renderWitnessDoc } from '../src/renderers.js'; +import { renderDesignDoc, renderRetroDoc, renderWitnessDoc } from '../src/renderers.js'; const tempRoots: string[] = []; @@ -60,7 +60,7 @@ describe('Automated Witness Capture', () => { expect(content).toContain('- [x] Automated capture completed successfully.'); }); - it('Does the witness scaffold label fenced output as text and avoid an empty drift-results fence?', () => { + it('Does the witness scaffold label fenced output as text and avoid empty test or drift fences?', () => { const root = createTempRoot(); initWorkspace(root); const workspace = new Workspace(root); @@ -70,12 +70,12 @@ describe('Automated Witness Capture', () => { const content = renderWitnessDoc({ cycle, - testResult: 'npm test output', + testResult: '', driftResult: '', }); expect(content).toContain('## Test Results'); - expect(content).toContain('```text\nnpm test output\n```'); + expect(content).toContain('```text\nNo test output captured.\n```'); expect(content).toContain('## Drift Results'); expect(content).toContain('```text\nNo drift output captured.\n```'); }); @@ -104,4 +104,33 @@ describe('Automated Witness Capture', () => { expect(content).toContain('legend: "FEAT:alpha #quoted \\"value\\""'); expect(content).toContain('Legend: FEAT:alpha #quoted "value"'); }); + + it('Normalizes generated frontmatter paths to POSIX separators.', () => { + const root = createTempRoot(); + const designDocPath = join(root, 'docs\\design\\0001-windows-paths\\windows-paths.md'); + writeFileSync(designDocPath, '# Windows Paths\n', 'utf8'); + + const designContent = renderDesignDoc({ + cycleName: '0001-windows-paths', + title: 'Windows Paths', + source: 'docs\\method\\backlog\\asap\\PROTO_windows-paths.md', + backlogBody: 'Body', + }); + expect(designContent).toContain('source_backlog: "docs/method/backlog/asap/PROTO_windows-paths.md"'); + + const retroContent = renderRetroDoc({ + cycle: { + name: '0001-windows-paths', + number: 1, + slug: 'windows-paths', + designDoc: designDocPath, + retroDoc: join(root, 'unused.md'), + }, + root, + outcome: 'partial', + witnessDir: 'docs\\method\\retro\\0001-windows-paths\\witness', + }); + expect(retroContent).toContain('design_doc: "docs/design/0001-windows-paths/windows-paths.md"'); + expect(retroContent).toContain('Add artifacts under `docs/method/retro/0001-windows-paths/witness`'); + }); }); From 0c7c99c7b0d909f5e9452494ee233110b0f95e30 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:54:55 -0700 Subject: [PATCH 22/29] docs(method): sharpen retro refresh and packet lint specs --- .../PROCESS_cycle-packet-markdownlint-gate.md | 9 +- .../PROCESS_retro-scaffold-refresh-command.md | 143 +++++++++++++++--- 2 files changed, 126 insertions(+), 26 deletions(-) diff --git a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md index dff7745..50a0c16 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md +++ b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md @@ -7,6 +7,7 @@ priority: medium acceptance_criteria: - "The lint scope is explicitly limited to cycle packet markdown under docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md and docs/method/retro/[0-9][0-9][0-9][0-9]-*/**/*.md." - "The gate enforces markdownlint rules MD040 and MD031 plus cycle-packet frontmatter key checks." + - "Retro frontmatter key checks apply only to top-level retro docs; witness/**/*.md files are linted for markdown rules only unless they gain their own explicit frontmatter contract." - "The chosen entrypoint exits 0 on clean packets and non-zero with file/rule-localized failures when any packet violates the contract." --- @@ -24,6 +25,7 @@ of landing as manual review cleanup. ## Proposed Contract - Scope globs: + markdown rules cover `docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md` and `docs/method/retro/[0-9][0-9][0-9][0-9]-*/**/*.md`. - Rules: @@ -32,8 +34,11 @@ of landing as manual review cleanup. packet checks for required frontmatter keys on generated docs. - Frontmatter checks: design docs require `title`, `legend`, `cycle`, and `source_backlog`; - retro docs require `title`, `cycle`, `design_doc`, `outcome`, and - `drift_check`. + top-level retro docs at + `docs/method/retro/[0-9][0-9][0-9][0-9]-*/*.md` require `title`, + `cycle`, `design_doc`, `outcome`, and `drift_check`; witness + artifacts under `**/witness/**/*.md` are linted for markdown rules + only unless they later get their own explicit frontmatter contract. - Integration point: a dedicated `method validate markdown` entrypoint that can be invoked locally, from pre-commit, and from a CI job such as diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md index 58ab1c3..7072de7 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md @@ -5,16 +5,17 @@ lane: cool-ideas owner: "METHOD maintainers" priority: low acceptance_criteria: - - "The command accepts explicit cycle IDs and glob selectors plus a dry-run mode." - - "Refresh updates only scaffold-owned frontmatter and generated placeholders while preserving human-authored retro sections and witness files." - - "The command reports deterministic diff and exit-code behavior for success, empty matches, invalid selectors, and write failures." + - "The command accepts explicit retro packet IDs, glob selectors, and filesystem paths resolved to retro packet roots, plus --dry-run." + - "Refresh rewrites only scaffold-managed retro frontmatter keys (`title`, `cycle`, `design_doc`, `outcome`, `drift_check`) and section bodies that still match the exact scaffold placeholder text." + - "Human-authored section bodies and all files under witness/ remain byte-for-byte unchanged." + - "Batch mode reports per-packet outcomes and exits 3 when any matched packet fails during write." --- # Retro Scaffold Refresh Command When scaffold contracts change, already-generated retro and witness artifacts currently need manual repair. A narrow refresh command could -re-render committed cycle packet scaffolds in place while preserving the +re-render committed retro packet scaffolds in place while preserving the human-written sections that should not be regenerated. That would turn scaffold contract upgrades from one-off cleanup work @@ -25,33 +26,127 @@ into a repeatable maintenance move. - Command shape: `method refresh retro [--dry-run]` - Accepted inputs: - explicit cycle ids like `0003-bearing-truthfulness`, glob selectors - like `003[0-3]-*`, or a path list resolved to retro packet roots. + explicit retro packet IDs like `0033-bearing-truthfulness`, glob + selectors like `003[0-3]-*` matched against retro packet IDs, or + filesystem paths. +- Path resolution algorithm: + for each selector, first match an exact retro packet ID; otherwise, if + the selector contains glob metacharacters, expand it against directory + names directly under `docs/method/retro/`; otherwise treat it as a + filesystem path, normalize it, and walk upward to the nearest ancestor + directory that is a direct child of `docs/method/retro/`. Paths under + `witness/` resolve to their containing retro packet root; paths + outside `docs/method/retro/` or selectors that never resolve to a + retro packet root fail with exit `2`. Deduplicate resolved packet + roots before processing. +- Scaffold-managed frontmatter: + only `title`, `cycle`, `design_doc`, `outcome`, and `drift_check` are + rewritten by the refresh command. Any other frontmatter keys are + preserved unchanged. - Preserved content: - existing body text under `## Summary`, `## Playback Witness`, - `## Drift`, `## New Debt`, `## Cool Ideas`, and the current - `## Backlog Maintenance` checklist state, plus everything already - stored under each retro packet's `witness/` directory. -- Regenerated content: - scaffold-owned frontmatter fields, missing required headings, and any - generated placeholder blocks whose contract changed between renderer - versions. + any non-placeholder body text already written under `## Summary`, + `## Playback Witness`, `## Drift`, `## New Debt`, `## Cool Ideas`, + and `## Backlog Maintenance`, plus every file already stored under the + retro packet's `witness/` directory. +- Placeholder detection and regenerated content: + the command may rewrite a section only when the section body still + matches the exact scaffold placeholder text for that heading: + `## Summary` -> `TBD`; `## Playback Witness` -> `Add artifacts under + \`\` and link them here.`; `## Drift`, `## New Debt`, and + `## Cool Ideas` -> `- None recorded.`; `## Backlog Maintenance` -> the + scaffold checklist block. Missing required headings are inserted with + the current scaffold placeholder text. - Expected diff behavior: - `--dry-run` prints which files would change and exits without writing; - write mode updates files in place and prints the touched packet paths. + `--dry-run` prints `Would refresh: ` lines plus the exact + frontmatter keys and headings it would touch, then exits without + writing. Write mode prints one line per packet using `Refreshed:`, + `Unchanged:`, or `Failed:` prefixes, followed by a summary line with + counts. - Failure modes and exit codes: exit `0` when all matched packets are already current or are refreshed - successfully; exit `1` when no packets match; exit `2` for invalid - selectors or malformed packet structure; exit `3` for write failures. + successfully; exit `1` when no packets match the selector; exit `2` + for invalid selectors or malformed retro packet structure before the + write phase; exit `3` for write failures. In batch mode the command + continues processing remaining matched packets after a write failure, + reports aggregate success/failure counts, and the highest applicable + exit code wins. ## Examples -- `method refresh retro 0033-bearing-truthfulness` -- `method refresh retro 003[0-3]-* --dry-run` -- `method refresh retro docs/method/retro/0032-mcp-tool-result-contract` +### Success case + +```shell +$ method refresh retro 0033-bearing-truthfulness +Refreshed: docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md + frontmatter: title, cycle, design_doc, outcome, drift_check + headings: inserted none +Summary: refreshed=1 unchanged=0 failed=0 +Exit 0 +``` + +### Dry-run mode + +```shell +$ method refresh retro 003[0-3]-* --dry-run +Would refresh: docs/method/retro/0030-backlog-metadata-single-source-of-truth/backlog-metadata-single-source-of-truth.md + would update frontmatter: title, cycle +Would refresh: docs/method/retro/0031-generated-doc-scaffold-contract/generated-doc-scaffold-contract.md + would insert headings: none +Summary: would_refresh=2 unchanged=0 +Exit 0 (dry-run, no files modified) +``` + +### Path input resolving from a witness file + +```shell +$ method refresh retro docs/method/retro/0032-mcp-tool-result-contract/witness/verification.md +Resolved packet root: docs/method/retro/0032-mcp-tool-result-contract +Refreshed: docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md +Summary: refreshed=1 unchanged=0 failed=0 +Exit 0 +``` + +### Error case: no matches + +```shell +$ method refresh retro 9999-nonexistent +Error: No retro packets match selector '9999-nonexistent' +Exit 1 +``` + +### Error case: batch write failure + +```shell +$ method refresh retro 003[2-3]-* +Refreshed: docs/method/retro/0032-mcp-tool-result-contract/mcp-tool-result-contract.md +Failed: docs/method/retro/0033-bearing-truthfulness/bearing-truthfulness.md (permission denied) +Summary: refreshed=1 unchanged=0 failed=1 +Exit 3 +``` ## PASS Criteria -- [ ] Operators can refresh one packet or a bounded set of packets - without overwriting human-written retro content. -- [ ] The command explains exactly what changed or why it refused. +- [ ] An explicit retro packet ID refreshes only that packet and exits + `0`. +- [ ] A glob selector expands to a deduplicated retro packet set and + reports one outcome line per packet. +- [ ] A filesystem path under `docs/method/retro//...` resolves + to the containing retro packet root; a path outside that tree + exits `2`. +- [ ] `--dry-run` makes no file changes and prints `Would refresh:` + output plus the exact keys/headings it would touch. +- [ ] Write mode rewrites only `title`, `cycle`, `design_doc`, + `outcome`, and `drift_check` in frontmatter. +- [ ] Section bodies are rewritten only when they still match the exact + scaffold placeholder text for that heading. +- [ ] Human-authored section bodies and every file under `witness/` + remain byte-for-byte unchanged. +- [ ] Missing required headings are inserted with the current scaffold + placeholder text. +- [ ] Exit `1` is used for no-match selectors with an explicit error + line naming the selector. +- [ ] Exit `2` is used for invalid selectors or malformed retro packet + structure before writes begin. +- [ ] Exit `3` is used when one or more matched packets fail during + write, while other matched packets continue processing and the + final summary reports the failure count. From a8cb1661d69be2ee398ead34a86dcc0b8ac02a46 Mon Sep 17 00:00:00 2001 From: James Ross Date: Wed, 8 Apr 2026 23:55:05 -0700 Subject: [PATCH 23/29] docs: keep Unreleased changelog notes first --- CHANGELOG.md | 18 +++++++++--------- tests/docs.test.ts | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c47cdb..54af732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## Unreleased + +### Fixed + +- Tightened MCP runtime behavior around GitHub sync flag parsing, + explicit legend normalization, and design-doc frontmatter escaping. +- Hardened the review-facing docs backlog items so their acceptance + criteria and historical notes match the live contract. + ## v0.3.0 First public release. 29 cycles closed, 127 tests passing. @@ -37,12 +46,3 @@ First public release. 29 cycles closed, 127 tests passing. - docs/GUIDE.md: operator advice with generated signpost inventory. - docs/BEARING.md: generated direction signpost. - docs/VISION.md: generated executive synthesis. - -## Unreleased - -### Fixed - -- Tightened MCP runtime behavior around GitHub sync flag parsing, - explicit legend normalization, and design-doc frontmatter escaping. -- Hardened the review-facing docs backlog items so their acceptance - criteria and historical notes match the live contract. diff --git a/tests/docs.test.ts b/tests/docs.test.ts index f963da7..c6894d3 100644 --- a/tests/docs.test.ts +++ b/tests/docs.test.ts @@ -753,4 +753,11 @@ describe('METHOD docs', () => { expect(releasesGuide).toContain('Migration'); expect(releasesGuide).toContain('No migration required.'); }); + + it('keeps `CHANGELOG.md` Unreleased notes ahead of tagged releases', () => { + const changelog = readRepoFile('CHANGELOG.md'); + const sections = [...changelog.matchAll(/^##\s+(.+)$/gmu)].map((match) => match[1] ?? ''); + + expect(sections[0]).toBe('Unreleased'); + }); }); From 0f1212e3cfa398f5a5aad9716b47572c99ca986e Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:20:13 -0700 Subject: [PATCH 24/29] fix(mcp): normalize legends and preserve witness output --- src/mcp.ts | 13 ++++++++++- src/renderers.ts | 8 +++++-- tests/mcp.test.ts | 54 +++++++++++++++++++++++++++++++++++++++++++ tests/witness.test.ts | 23 ++++++++++++++++-- 4 files changed, 93 insertions(+), 5 deletions(-) diff --git a/src/mcp.ts b/src/mcp.ts index 9b251d2..ab799b5 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -313,11 +313,12 @@ function readPersistedInboxResult(workspace: Workspace, relativePath: string) { const frontmatter = workspace.readFrontmatter(relativePath); const stem = fileStem(relativePath); const { legend, slug } = splitBacklogStem(stem); + const persistedLegend = normalizePersistedLegend(frontmatter.legend); return { path: relativePath, lane: frontmatter.lane ?? 'inbox', - legend: frontmatter.legend ?? legend, + legend: persistedLegend ?? legend, title: frontmatter.title, stem, slug, @@ -382,6 +383,16 @@ function splitBacklogStem(stem: string): { legend?: string; slug: string } { return { legend: match.groups.legend, slug: match.groups.slug }; } +function normalizePersistedLegend(value: string | undefined): string | undefined { + const normalized = value?.trim().toUpperCase(); + return normalized === undefined + || normalized.length === 0 + || normalized === 'NONE' + || !/^[A-Z][A-Z0-9]*$/u.test(normalized) + ? undefined + : normalized; +} + function validateOptionalString(value: unknown, name: string): string | undefined { if (value === undefined) { return undefined; diff --git a/src/renderers.ts b/src/renderers.ts index 674d9f6..869c2e5 100644 --- a/src/renderers.ts +++ b/src/renderers.ts @@ -78,8 +78,12 @@ export function renderWitnessDoc(options: { driftResult: string; }): string { const title = readHeading(options.cycle.designDoc) || titleCase(options.cycle.slug); - const testResult = options.testResult.trim() || 'No test output captured.'; - const driftResult = options.driftResult.trim() || 'No drift output captured.'; + const testResult = options.testResult.trim().length === 0 + ? 'No test output captured.' + : options.testResult; + const driftResult = options.driftResult.trim().length === 0 + ? 'No drift output captured.' + : options.driftResult; return [ '---', `title: "Verification Witness for Cycle ${options.cycle.number}"`, diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 91a4cdc..7cdc7f1 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -208,6 +208,45 @@ describe('MCP Server', () => { } }); + it('Does `method_inbox` normalize persisted legends before returning structured content?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const persistedPath = join(root, 'docs/method/backlog/inbox/PROCESS_tampered-legend.md'); + writeFileSync( + persistedPath, + [ + '---', + 'title: Tampered Legend', + 'lane: inbox', + 'legend: foo-bar', + '---', + '', + 'Body', + '', + ].join('\n'), + 'utf8', + ); + + const captureIdea = vi.spyOn(Workspace.prototype, 'captureIdea').mockReturnValue(persistedPath); + const callToolHandler = createCallToolHarness(); + + const result = await callToolHandler({ + params: { + name: 'method_inbox', + arguments: { + workspace: root, + idea: 'ignored by mock', + }, + }, + }); + + expect(result.isError).toBe(false); + expect(result.structuredContent.tool).toBe('method_inbox'); + expect(result.structuredContent.result.path).toBe('docs/method/backlog/inbox/PROCESS_tampered-legend.md'); + expect(result.structuredContent.result.legend).toBe('PROCESS'); + expect(captureIdea).toHaveBeenCalled(); + }); + it('On MCP errors, can I inspect a structured error object instead of only a prose message?', async () => { const callToolHandler = createCallToolHarness(); @@ -348,5 +387,20 @@ describe('MCP Server', () => { expect(badPush.structuredContent.error.message).toContain('push must be a boolean'); expect(pushBacklog).not.toHaveBeenCalled(); expect(pullBacklog).not.toHaveBeenCalled(); + + const badPull = await callToolHandler({ + params: { + name: 'method_sync_github', + arguments: { + workspace: root, + pull: 'false', + }, + }, + }); + + expect(badPull.isError).toBe(true); + expect(badPull.structuredContent.error.message).toContain('pull must be a boolean'); + expect(pushBacklog).not.toHaveBeenCalled(); + expect(pullBacklog).not.toHaveBeenCalled(); }); }); diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 644a75a..26a87ab 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -1,6 +1,6 @@ -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { initWorkspace, Workspace } from '../src/index.js'; import { renderDesignDoc, renderRetroDoc, renderWitnessDoc } from '../src/renderers.js'; @@ -80,6 +80,24 @@ describe('Automated Witness Capture', () => { expect(content).toContain('```text\nNo drift output captured.\n```'); }); + it('Preserves non-empty witness command output verbatim while only trimming for blank detection.', () => { + const root = createTempRoot(); + initWorkspace(root); + const workspace = new Workspace(root); + + workspace.captureIdea('Witness Payload Test', 'PROCESS', 'Witness Payload Test'); + const cycle = workspace.pullItem('PROCESS_witness-payload-test'); + + const content = renderWitnessDoc({ + cycle, + testResult: ' indented output\n', + driftResult: '\t drift output\n', + }); + + expect(content).toMatch(/## Test Results\n\n```text\n indented output\n\n```/u); + expect(content).toMatch(/## Drift Results\n\n```text\n\t drift output\n\n```/u); + }); + it('Does YAML frontmatter escape embedded line breaks instead of letting YAML fold them away?', () => { const content = renderDesignDoc({ cycleName: '0001-line-breaks', @@ -108,6 +126,7 @@ describe('Automated Witness Capture', () => { it('Normalizes generated frontmatter paths to POSIX separators.', () => { const root = createTempRoot(); const designDocPath = join(root, 'docs\\design\\0001-windows-paths\\windows-paths.md'); + mkdirSync(dirname(designDocPath), { recursive: true }); writeFileSync(designDocPath, '# Windows Paths\n', 'utf8'); const designContent = renderDesignDoc({ From f0b47324fe2560411bc9442b1f8728f050c715e6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:20:19 -0700 Subject: [PATCH 25/29] docs(backlog): tighten review follow-up contracts --- .../PROCESS_cycle-packet-markdownlint-gate.md | 11 +++++-- .../PROCESS_retro-scaffold-refresh-command.md | 31 +++++++++++++------ .../cool-ideas/PROCESS_review-state-query.md | 25 +++++++++++++-- 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md index 50a0c16..260ec28 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md +++ b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md @@ -8,6 +8,7 @@ acceptance_criteria: - "The lint scope is explicitly limited to cycle packet markdown under docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md and docs/method/retro/[0-9][0-9][0-9][0-9]-*/**/*.md." - "The gate enforces markdownlint rules MD040 and MD031 plus cycle-packet frontmatter key checks." - "Retro frontmatter key checks apply only to top-level retro docs; witness/**/*.md files are linted for markdown rules only unless they gain their own explicit frontmatter contract." + - "The spec explicitly declares that `method validate markdown` depends on the parent `PROCESS_validate-command.md` contract and that `src/cli.ts` must expose the `validate` command surface." - "The chosen entrypoint exits 0 on clean packets and non-zero with file/rule-localized failures when any packet violates the contract." --- @@ -40,9 +41,13 @@ of landing as manual review cleanup. artifacts under `**/witness/**/*.md` are linted for markdown rules only unless they later get their own explicit frontmatter contract. - Integration point: - a dedicated `method validate markdown` entrypoint that can be invoked - locally, from pre-commit, and from a CI job such as - `markdown-contract`. + this item depends on `PROCESS_validate-command.md`. The intended + surface is `method validate markdown` as a subcommand of the parent + `method validate` CLI implemented in `src/cli.ts`, not a separate + top-level verb. Implementers should either land both contracts in the + same slice or wire this markdown gate into the existing `validate` + command surface before invoking it locally, from pre-commit, or from + a CI job such as `markdown-contract`. - Pass / fail behavior: exit `0` when all scoped files satisfy the bounded rule set; exit non-zero when any scoped file violates one of the named rules or diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md index 7072de7..fe3aa14 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md @@ -6,7 +6,7 @@ owner: "METHOD maintainers" priority: low acceptance_criteria: - "The command accepts explicit retro packet IDs, glob selectors, and filesystem paths resolved to retro packet roots, plus --dry-run." - - "Refresh rewrites only scaffold-managed retro frontmatter keys (`title`, `cycle`, `design_doc`, `outcome`, `drift_check`) and section bodies that still match the exact scaffold placeholder text." + - "Refresh rewrites only scaffold-managed retro frontmatter keys (`title`, `cycle`, `design_doc`, `outcome`, `drift_check`) and section bodies that are explicitly marked scaffold-managed or that match a supported placeholder signature for that heading." - "Human-authored section bodies and all files under witness/ remain byte-for-byte unchanged." - "Batch mode reports per-packet outcomes and exits 3 when any matched packet fails during write." --- @@ -49,13 +49,23 @@ into a repeatable maintenance move. and `## Backlog Maintenance`, plus every file already stored under the retro packet's `witness/` directory. - Placeholder detection and regenerated content: - the command may rewrite a section only when the section body still - matches the exact scaffold placeholder text for that heading: - `## Summary` -> `TBD`; `## Playback Witness` -> `Add artifacts under - \`\` and link them here.`; `## Drift`, `## New Debt`, and - `## Cool Ideas` -> `- None recorded.`; `## Backlog Maintenance` -> the - scaffold checklist block. Missing required headings are inserted with - the current scaffold placeholder text. + new scaffolds should emit + `` immediately + under each placeholder-owned heading. The refresh command first + checks for that marker; if it is absent, it normalizes surrounding + whitespace and matches the section body against supported historical + placeholder signatures for that heading: + `## Summary` -> `TBD`, `TODO`, or `To be written.`; + `## Playback Witness` -> `Add artifacts under \`\` and + link them here.` plus earlier variants that match + `Add artifacts under \`.+\` and link .* here.`; + `## Drift`, `## New Debt`, and `## Cool Ideas` -> `- None recorded.`, + `- None.`, or `None recorded.`; + `## Backlog Maintenance` -> the current scaffold checklist block or an + earlier checklist variant that still contains the same scaffold action + list. Bodies outside that marker/signature set are treated as + human-authored and preserved unchanged. Missing required headings are + inserted with the current scaffold placeholder text. - Expected diff behavior: `--dry-run` prints `Would refresh: ` lines plus the exact frontmatter keys and headings it would touch, then exits without @@ -137,8 +147,9 @@ Exit 3 output plus the exact keys/headings it would touch. - [ ] Write mode rewrites only `title`, `cycle`, `design_doc`, `outcome`, and `drift_check` in frontmatter. -- [ ] Section bodies are rewritten only when they still match the exact - scaffold placeholder text for that heading. +- [ ] Section bodies are rewritten only when they carry the + `method:scaffold-managed` marker or match a supported placeholder + signature for that heading. - [ ] Human-authored section bodies and every file under `witness/` remain byte-for-byte unchanged. - [ ] Missing required headings are inserted with the current scaffold diff --git a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md index 06baa37..53526b0 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md +++ b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md @@ -7,6 +7,7 @@ priority: medium acceptance_criteria: - "The review-state surface reports CI/check status, unresolved review-thread count, review decision, and bot-review pending state for the active PR." - "The output includes a deterministic merge_ready boolean plus a blocker list naming exactly which states still prevent merge." + - "Selector precedence is explicit: `method review-state` defaults to `--current-branch`, rejects `--pr` with `--current-branch`, and defines deterministic no-PR and ambiguous-PR outputs." - "Humans and agents can answer 'is this PR actually mergeable yet?' without manually reconstructing GitHub thread/check state." --- @@ -29,10 +30,28 @@ but today it lives only in ad hoc `gh` inspection. - Surface: `method review-state [--pr |--current-branch]` and/or a small generated signpost section. +- Selector rules: + `--pr` and `--current-branch` are mutually exclusive. If neither flag + is provided, `review-state` behaves as `--current-branch`. Passing + both flags is invalid input and should exit non-zero with a clear + error before any GitHub calls begin. +- Current-branch resolution: + when `--current-branch` maps to exactly one PR, inspect that PR. When + it maps to zero PRs, return a deterministic `status: no-pr` result + with `pr_number: null`, `pr_url: null`, `merge_ready: false`, and a + blocker such as `No PR found for current branch`. When it maps to + multiple PRs, return `status: ambiguous-pr`, keep `pr_number` and + `pr_url` null, set `merge_ready: false`, and include a blocker such as + `Multiple PRs match current branch; rerun with --pr `. - Required fields: - PR number and URL, review decision, unresolved-thread count, failing - or pending checks, pending bot-review state, approval/change-request - counts, `merge_ready`, and `blockers`. + `status`, `pr_number`, `pr_url`, `review_decision`, + `unresolved_thread_count`, `checks`, `bot_review_state`, + `approval_count`, `changes_requested_count`, `merge_ready`, and + `blockers`. The `review-state` output must include every field even in + `no-pr` or `ambiguous-pr` cases; for `no-pr`, use + `review_decision: none`, `unresolved_thread_count: 0`, `checks: []`, + `bot_review_state: none`, `approval_count: 0`, and + `changes_requested_count: 0`. - Merge-ready rule: `merge_ready` is true only when required checks are green, unresolved review threads are zero, no required reviewer or bot still reports a From e7c13dd77dc4b3dbb121387a48c6450538e88709 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:20:23 -0700 Subject: [PATCH 26/29] docs: note closeCycle breaking change --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54af732..10f7cab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +### Breaking Changes + +- `Workspace.closeCycle(cycleName, completedDriftCheck, outcome)` now + requires the `outcome` argument. Downstream `Workspace` consumers must + update any direct calls to pass one of `hill-met`, `partial`, or + `not-met`. Treat the first published release containing this API + change as semver-major, or ship an explicit migration note if the + release plan changes before publication. + ### Fixed - Tightened MCP runtime behavior around GitHub sync flag parsing, From 630f589100e2f32fc1fa04037d7e3fdd835c60e8 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:25:56 -0700 Subject: [PATCH 27/29] fix(mcp): tighten status and no-op sync contracts --- src/mcp.ts | 22 ++++++++++++++++++- tests/mcp.test.ts | 49 +++++++++++++++++++++++++++++++++++++++++++ tests/witness.test.ts | 2 +- 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/mcp.ts b/src/mcp.ts index ab799b5..df3cbd8 100644 --- a/src/mcp.ts +++ b/src/mcp.ts @@ -119,7 +119,7 @@ export function createMcpServer() { if (request.params.name === 'method_status') { const status = workspace.status(); - const summary = args.summary === true; + const summary = validateOptionalBoolean(args.summary, 'summary') ?? false; if (summary) { const summaryResult = summarizeStatus(status, workspace.closedCycles()); return successResult( @@ -215,6 +215,20 @@ export function createMcpServer() { const pull = validatedPull ?? false; const push = validatedPush ?? !pull; + if (!push && !pull) { + return toolResult( + 'method_sync_github', + 'No changes.', + { + pushRequested: false, + pullRequested: false, + pushResults: [], + pullResults: [], + }, + false, + ); + } + const token = workspace.config.github_token; const repoFull = workspace.config.github_repo; @@ -301,11 +315,17 @@ function renderStatusSummaryText(summary: McpStatusSummary): string { const activeCycles = summary.activeCycles.length === 0 ? '-' : summary.activeCycles.map((cycle) => cycle.name).join(', '); + const legendHealth = summary.legendHealth.length === 0 + ? '-' + : summary.legendHealth + .map(({ legend, backlog, active }) => `${legend} backlog=${backlog} active=${active}`) + .join('; '); return [ `Status summary for ${summary.root}.`, `Lane counts: ${laneCounts}`, `Active cycles: ${activeCycles}`, `Retros: ${summary.retroCount}`, + `Legend health: ${legendHealth}`, ].join('\n'); } diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 7cdc7f1..c99acf1 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -137,9 +137,30 @@ describe('MCP Server', () => { }); expect(statusResult.structuredContent.result.summary.backlog).toBeUndefined(); expect(statusResult.content[0].text).toContain('Lane counts:'); + expect(statusResult.content[0].text).toContain('Legend health:'); vi.restoreAllMocks(); }); + it('Does `method_status` reject non-boolean summary flags instead of silently expanding to full mode?', async () => { + const root = createTempRoot(); + initWorkspace(root); + const callToolHandler = createCallToolHarness(); + + const result = await callToolHandler({ + params: { + name: 'method_status', + arguments: { + workspace: root, + summary: 'true', + }, + }, + }); + + expect(result.isError).toBe(true); + expect(result.structuredContent.tool).toBe('method_status'); + expect(result.structuredContent.error.message).toContain('summary must be a boolean'); + }); + it('Do mutation tools (`method_inbox`, `method_pull`, `method_close`, `method_capture_witness`) return stable field-level structured content for persisted paths and cycles?', async () => { const root = createTempRoot(); initWorkspace(root); @@ -360,6 +381,34 @@ describe('MCP Server', () => { expect(result.content[0].text).toContain('No changes.'); }); + it('Does `method_sync_github` allow an explicit no-op sync without GitHub credentials?', async () => { + const root = createTempRoot(); + initWorkspace(root); + + const pushBacklog = vi.spyOn(GitHubAdapter.prototype, 'pushBacklog').mockResolvedValue([]); + const pullBacklog = vi.spyOn(GitHubAdapter.prototype, 'pullBacklog').mockResolvedValue([]); + const callToolHandler = createCallToolHarness(); + + const result = await callToolHandler({ + params: { + name: 'method_sync_github', + arguments: { + workspace: root, + push: false, + pull: false, + }, + }, + }); + + expect(result.isError).toBe(false); + expect(result.structuredContent.tool).toBe('method_sync_github'); + expect(result.structuredContent.result.pushRequested).toBe(false); + expect(result.structuredContent.result.pullRequested).toBe(false); + expect(pushBacklog).not.toHaveBeenCalled(); + expect(pullBacklog).not.toHaveBeenCalled(); + expect(result.content[0].text).toContain('No changes.'); + }); + it('Does `method_sync_github` reject non-boolean push and pull flags before touching GitHub?', async () => { const root = createTempRoot(); initWorkspace(root); diff --git a/tests/witness.test.ts b/tests/witness.test.ts index 26a87ab..f479217 100644 --- a/tests/witness.test.ts +++ b/tests/witness.test.ts @@ -125,7 +125,7 @@ describe('Automated Witness Capture', () => { it('Normalizes generated frontmatter paths to POSIX separators.', () => { const root = createTempRoot(); - const designDocPath = join(root, 'docs\\design\\0001-windows-paths\\windows-paths.md'); + const designDocPath = join(root, 'docs', 'design', '0001-windows-paths', 'windows-paths.md'); mkdirSync(dirname(designDocPath), { recursive: true }); writeFileSync(designDocPath, '# Windows Paths\n', 'utf8'); From 37755cb01b1c22e6617216b42616086721543add Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:26:00 -0700 Subject: [PATCH 28/29] docs(backlog): pin review-state and lint contracts --- .../PROCESS_cycle-packet-markdownlint-gate.md | 6 ++ .../PROCESS_retro-scaffold-refresh-command.md | 6 +- .../cool-ideas/PROCESS_review-state-query.md | 68 ++++++++++++++----- 3 files changed, 59 insertions(+), 21 deletions(-) diff --git a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md index 260ec28..e43be55 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md +++ b/docs/method/backlog/cool-ideas/PROCESS_cycle-packet-markdownlint-gate.md @@ -25,6 +25,12 @@ of landing as manual review cleanup. ## Proposed Contract +## Prerequisites + +- `PROCESS_validate-command.md` defines the parent `method validate` + surface. This item assumes that contract lands first or in the same + slice. + - Scope globs: markdown rules cover `docs/design/[0-9][0-9][0-9][0-9]-*/**/*.md` and diff --git a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md index fe3aa14..a2d2b79 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md +++ b/docs/method/backlog/cool-ideas/PROCESS_retro-scaffold-refresh-command.md @@ -56,9 +56,9 @@ into a repeatable maintenance move. whitespace and matches the section body against supported historical placeholder signatures for that heading: `## Summary` -> `TBD`, `TODO`, or `To be written.`; - `## Playback Witness` -> `Add artifacts under \`\` and - link them here.` plus earlier variants that match - `Add artifacts under \`.+\` and link .* here.`; + `## Playback Witness` -> `Add artifacts under and link + them here.` plus earlier variants that match + `Add artifacts under <.+> and link .* here.`; `## Drift`, `## New Debt`, and `## Cool Ideas` -> `- None recorded.`, `- None.`, or `None recorded.`; `## Backlog Maintenance` -> the current scaffold checklist block or an diff --git a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md index 53526b0..1546300 100644 --- a/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md +++ b/docs/method/backlog/cool-ideas/PROCESS_review-state-query.md @@ -6,7 +6,8 @@ owner: "METHOD maintainers" priority: medium acceptance_criteria: - "The review-state surface reports CI/check status, unresolved review-thread count, review decision, and bot-review pending state for the active PR." - - "The output includes a deterministic merge_ready boolean plus a blocker list naming exactly which states still prevent merge." + - "The output contract is explicit for both CLI and MCP: `method review-state` supports `--json`, `method_review_state` returns the standard structured-content envelope, and both share the same result schema." + - "The output includes a deterministic merge_ready boolean plus a canonical blocker schema naming exactly which states still prevent merge." - "Selector precedence is explicit: `method review-state` defaults to `--current-branch`, rejects `--pr` with `--current-branch`, and defines deterministic no-PR and ambiguous-PR outputs." - "Humans and agents can answer 'is this PR actually mergeable yet?' without manually reconstructing GitHub thread/check state." --- @@ -28,8 +29,12 @@ but today it lives only in ad hoc `gh` inspection. ## Proposed Contract - Surface: - `method review-state [--pr |--current-branch]` and/or a small - generated signpost section. + this slice ships both a CLI command and an MCP tool: + `method review-state [--pr |--current-branch] [--json]` for + humans and scripts, plus `method_review_state` for MCP callers. A + generated signpost is out of scope for the initial slice; if a future + signpost is added, it should be a follow-on item with its own bounded + output contract rather than implied by this note. - Selector rules: `--pr` and `--current-branch` are mutually exclusive. If neither flag is provided, `review-state` behaves as `--current-branch`. Passing @@ -43,25 +48,52 @@ but today it lives only in ad hoc `gh` inspection. multiple PRs, return `status: ambiguous-pr`, keep `pr_number` and `pr_url` null, set `merge_ready: false`, and include a blocker such as `Multiple PRs match current branch; rerun with --pr `. -- Required fields: - `status`, `pr_number`, `pr_url`, `review_decision`, - `unresolved_thread_count`, `checks`, `bot_review_state`, - `approval_count`, `changes_requested_count`, `merge_ready`, and - `blockers`. The `review-state` output must include every field even in - `no-pr` or `ambiguous-pr` cases; for `no-pr`, use - `review_decision: none`, `unresolved_thread_count: 0`, `checks: []`, - `bot_review_state: none`, `approval_count: 0`, and - `changes_requested_count: 0`. +- Output shape: + CLI default output is a bounded human-readable summary; `--json` + prints the same result object returned by the MCP tool under + `structuredContent.result`. `method_review_state` must follow the + existing MCP envelope shape: + `structuredContent = { tool, ok, result }` on success and + `structuredContent = { tool, ok: false, error.message }` on failure. +- Required `structuredContent.result` fields and types: + `status: "ready" | "blocked" | "no-pr" | "ambiguous-pr"`; + `pr_number: integer | null`; + `pr_url: string | null`; + `review_decision: "APPROVED" | "CHANGES_REQUESTED" | "REVIEW_REQUIRED" | "NONE"`; + `unresolved_thread_count: integer`; + `checks: { passing: Array<{ name: string, status: string, url: string | null }>, pending: Array<{ name: string, status: string, url: string | null }>, failing: Array<{ name: string, status: string, url: string | null }> }`; + `bot_review_state: "none" | "pending" | "approved" | "changes_requested" | "commented"`; + `approval_count: integer`; + `changes_requested_count: integer`; + `merge_ready: boolean`; + `blockers: Array<{ type: "selection" | "review_decision" | "unresolved_threads" | "pending_checks" | "failing_checks" | "bot_review" | "policy_cooldown", message: string, source: string }>`. + The result must include every field even in `no-pr` or + `ambiguous-pr` cases; for `no-pr`, use `pr_number: null`, + `pr_url: null`, `review_decision: "NONE"`, + `unresolved_thread_count: 0`, empty check arrays, + `bot_review_state: "none"`, `approval_count: 0`, + `changes_requested_count: 0`, `merge_ready: false`, and a single + `selection` blocker explaining the missing PR. - Merge-ready rule: `merge_ready` is true only when required checks are green, unresolved review threads are zero, no required reviewer or bot still reports a - blocking state, and no known cooldown/rate-limit rule is active. + blocking state, and no `policy_cooldown` blocker is active. For the + initial slice, `policy_cooldown` means an explicitly observable forge + review cooldown or rate-limit message, such as a CodeRabbit comment or + check message saying review must be retried after a timestamp. Detect + it from the latest bot review/comment/check text; if no such message + is observable via API, do not invent a cooldown blocker. - Blocker output: - each blocker is named explicitly, for example `CodeRabbit review in - progress`, `reviewDecision still CHANGES_REQUESTED`, or - `2 unresolved review threads`. + `blockers` is a canonical array of structured objects, not free-form + strings. `type` comes from the fixed set above, while `message` is + human-readable. Example blockers are illustrative messages inside that + schema, for example + `{ type: "bot_review", message: "CodeRabbit review in progress", source: "coderabbitai" }`, + `{ type: "review_decision", message: "reviewDecision still CHANGES_REQUESTED", source: "github" }`, + or + `{ type: "unresolved_threads", message: "2 unresolved review threads", source: "github" }`. ## Non-goals -- [ ] Auto-merge a PR once the blockers list is empty. -- [ ] Replace human judgment about whether a change should merge. +- Auto-merge a PR once the blockers list is empty. +- Replace human judgment about whether a change should merge. From 339d595961940f2b9ce175c35a289f8ad627154c Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 9 Apr 2026 00:26:07 -0700 Subject: [PATCH 29/29] docs: add closeCycle migration example --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10f7cab..7afb8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ update any direct calls to pass one of `hill-met`, `partial`, or `not-met`. Treat the first published release containing this API change as semver-major, or ship an explicit migration note if the - release plan changes before publication. + release plan changes before publication. Migration example: + `await workspace.closeCycle('0033-bearing-truthfulness', true, 'hill-met')`. ### Fixed