devflow-kit 3.15.0 → 3.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +5 -42
  3. package/dist/agents/code.md +2 -5
  4. package/dist/agents/design.md +1 -1
  5. package/dist/agents/fix.md +1 -2
  6. package/dist/agents/git.md +1 -2
  7. package/dist/agents/harden.md +1 -3
  8. package/dist/agents/refactor.md +1 -2
  9. package/dist/agents/review.md +1 -1
  10. package/dist/cli/commands/compliance-prompts.js +2 -2
  11. package/dist/cli/commands/init.js +5 -6
  12. package/dist/cli/commands/uninstall.js +1 -1
  13. package/dist/commands/bug-analysis.md +1 -1
  14. package/dist/commands/code-review.md +2 -2
  15. package/dist/compliance-off/commands/bug-analysis.md +1 -1
  16. package/dist/compliance-off/commands/code-review.md +2 -2
  17. package/dist/compliance-off/skills/git/references/tracker/_contract.md +1 -1
  18. package/dist/compliance-off/skills/git/references/tracker/github/ensure-pr-ready.md +3 -4
  19. package/dist/compliance-off/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -4
  20. package/dist/compliance-off/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -4
  21. package/dist/core/installed-variants.js +1 -1
  22. package/dist/core/mds-variants.js +6 -8
  23. package/dist/core/plugins.js +5 -40
  24. package/dist/core/retired-shipped-names.js +7 -0
  25. package/dist/curation-compliance-off/commands/bug-analysis.md +1 -1
  26. package/dist/curation-compliance-off/commands/code-review.md +2 -2
  27. package/dist/curation-off/agents/code.md +2 -5
  28. package/dist/curation-off/agents/design.md +1 -1
  29. package/dist/curation-off/agents/fix.md +1 -2
  30. package/dist/curation-off/agents/harden.md +1 -3
  31. package/dist/curation-off/agents/refactor.md +1 -2
  32. package/dist/curation-off/agents/review.md +1 -1
  33. package/dist/curation-off/commands/bug-analysis.md +1 -1
  34. package/dist/curation-off/commands/code-review.md +2 -2
  35. package/dist/curation-tdd-off/agents/code.md +2 -5
  36. package/dist/skills/git/references/tracker/_contract.md +1 -1
  37. package/dist/skills/git/references/tracker/github/ensure-pr-ready.md +3 -3
  38. package/dist/skills/git/references/tracker/jira/ensure-pr-ready.md +3 -3
  39. package/dist/skills/git/references/tracker/linear/ensure-pr-ready.md +3 -3
  40. package/dist/targets/claude-code/curation-install.js +28 -3
  41. package/dist/targets/claude-code/language-stamp.js +1 -2
  42. package/dist/{cli/commands → targets/claude-code}/legacy-sweep.js +45 -12
  43. package/dist/tdd-off/agents/code.md +2 -5
  44. package/dist/variant-axes.json +0 -20
  45. package/package.json +2 -3
  46. package/src/assets/agents/_partials/_code.mds +1 -2
  47. package/src/assets/agents/code.mds +1 -3
  48. package/src/assets/agents/design.mds +1 -1
  49. package/src/assets/agents/git.mds +1 -2
  50. package/src/assets/agents/harden.mds +1 -3
  51. package/src/assets/agents/review.mds +1 -1
  52. package/src/assets/agents/test.md +1 -2
  53. package/src/assets/agents/tracker.md +1 -5
  54. package/src/assets/agents/validate.md +0 -1
  55. package/src/assets/commands/_partials/_compliance.mds +0 -1
  56. package/src/assets/commands/_partials/_decisions.mds +2 -3
  57. package/src/assets/commands/_partials/_docs_root.mds +1 -1
  58. package/src/assets/commands/_partials/_plan_contract.mds +0 -56
  59. package/src/assets/commands/bug-analysis.mds +1 -1
  60. package/src/assets/commands/code-review.mds +2 -2
  61. package/src/assets/mds/git/_ops.mds +3 -20
  62. package/src/assets/mds/git/_ops_steps.mds +8 -6
  63. package/src/assets/mds/tracker/_common.mds +3 -14
  64. package/src/assets/mds/tracker/_contract.mds +1 -1
  65. package/src/assets/mds/tracker/_github.mds +0 -32
  66. package/src/assets/mds/tracker/_jira.mds +0 -31
  67. package/src/assets/mds/tracker/_linear.mds +0 -31
  68. package/src/assets/mds/tracker/_steps.mds +0 -6
  69. package/src/assets/scripts/pr-evidence.cjs +3 -268
  70. package/src/assets/scripts/resolve-base.cjs +1 -3
  71. package/src/assets/scripts/verify-evidence.cjs +13 -18
  72. package/src/assets/skills/docs-framework/SKILL.md +5 -19
  73. package/src/assets/skills/docs-framework/references/patterns.md +1 -2
  74. package/dist/commands/dynamic-build.md +0 -1218
  75. package/dist/commands/dynamic-plan.md +0 -447
  76. package/dist/commands/dynamic-profile.md +0 -102
  77. package/dist/commands/dynamic-tickets.md +0 -555
  78. package/dist/compliance-off/commands/dynamic-build.md +0 -1197
  79. package/dist/compliance-off/commands/dynamic-tickets.md +0 -555
  80. package/dist/curation-compliance-off/commands/dynamic-build.md +0 -1161
  81. package/dist/curation-compliance-off/commands/dynamic-tickets.md +0 -515
  82. package/dist/curation-off/commands/dynamic-build.md +0 -1182
  83. package/dist/curation-off/commands/dynamic-plan.md +0 -406
  84. package/dist/curation-off/commands/dynamic-tickets.md +0 -515
  85. package/dist/skills/git/references/tracker/github/post-wave-report.md +0 -44
  86. package/dist/skills/git/references/tracker/jira/post-wave-report.md +0 -49
  87. package/dist/skills/git/references/tracker/linear/post-wave-report.md +0 -49
  88. package/src/assets/commands/_partials/_engine.mds +0 -216
  89. package/src/assets/commands/_partials/_factory.mds +0 -122
  90. package/src/assets/commands/_partials/_preamble.mds +0 -112
  91. package/src/assets/commands/_partials/_roster.mds +0 -31
  92. package/src/assets/commands/_partials/_ticket_template.mds +0 -56
  93. package/src/assets/commands/_partials/_wave.mds +0 -123
  94. package/src/assets/commands/dynamic-build.mds +0 -882
  95. package/src/assets/commands/dynamic-plan.mds +0 -289
  96. package/src/assets/commands/dynamic-profile.mds +0 -103
  97. package/src/assets/commands/dynamic-tickets.mds +0 -281
@@ -453,34 +453,6 @@ The D3 section headings are the canonical ones; only the transport differs from
453
453
  <!-- compliance:end -->
454
454
  @end
455
455
 
456
- @define post_wave_report():
457
- ## Operation: post-wave-report
458
-
459
- {{ops.post_wave_report_contract()}}
460
-
461
- Load when the resolved tracker provider is `linear` and the operation is `post-wave-report`.
462
-
463
- **Mechanics held here:** the `### Process` body — locating the wave's tracking item and posting the report once.
464
-
465
- {{common.wave_report_inputs()}}
466
-
467
- ### Process
468
-
469
- **Setup (once):** resolve the capability set and the reached rung. This provider lands at rank 4, so the scan below is unfiltered by author and every run emits `TRACEABILITY: DEGRADED (dedup unavailable — duplicate possible)`; the ladder and the marker's second discriminator are stated once, with the dedup ladder in this operation's `backlink-shipped-issues` reference.
470
-
471
- 1. Check for an existing marker on the tracking item.
472
- - Read the tracking item's comments through the *list comments with authors* capability. The author column cannot be compared against devflow's own account here, so the match rests on the marker alone.
473
- - This operation owns the `devflow:wave` namespace and no other. Match **line 1** of each comment for equality against `devflow:wave {WAVE_ID} · https://github.com/dean0x/devflow`; a marker on any later line **does not suppress**.
474
- - **The scan is a FULL scan, not a newest-first early exit.** A wave report's marker carries a wave id, and wave ids are not monotonic in comment order, so an early exit can miss the one comment that matters. Bound it at `≤5` pages and **fail closed**: if the bound is reached before the scan completes, report `TRUNCATED ({n} not processed)` and **DO NOT POST** — a duplicate wave report is a worse outcome than a missing one, because the next run cannot tell which is authoritative. This is the one place the fail-closed direction wins over post-with-warning, and the difference is the condition: an absent capability says nothing about whether a post happened, while a truncated scan says the evidence exists and was not read.
475
- - If found: skip — report `Skipped: wave report for {WAVE_ID} already posted`.
476
- {{steps.wave_report_read_step()}}
477
- 3. Compose the comment: line 1 the marker `devflow:wave {WAVE_ID} · https://github.com/dean0x/devflow`, then the contents of `WAVE_REPORT_PATH`. Cap the composed content at `{{comment_cap()}}` characters; over the cap, truncate in **preservation order** — the marker, then the status and DEGRADED lines, then the pointer sentence — and end with `…truncated — full report in the local wave artifact {WAVE_REPORT_PATH} (not committed; ask the author)`.
478
- 4. Post it through `### Posting gate` below.
479
-
480
- {{mcp.posting_gate_head("the write")}}
481
- 4. Post through the *add comment* capability with arguments (issue reference, body: {SCRUBBED_BODY}).
482
- @end
483
-
484
456
  @define ensure_pr_ready():
485
457
  ## Operation: ensure-pr-ready
486
458
 
@@ -527,8 +499,5 @@ Load when the resolved tracker provider is `linear` and the operation is `ensure
527
499
  <!-- op: ensure-traceable-issue -->
528
500
  {{ensure_traceable_issue()}}
529
501
 
530
- <!-- op: post-wave-report -->
531
- {{post_wave_report()}}
532
-
533
502
  <!-- op: ensure-pr-ready -->
534
503
  {{ensure_pr_ready()}}
@@ -83,11 +83,6 @@ Neutralise any `</untrusted-issue-body>` in the fetched body before wrapping it
83
83
  5. Gate each candidate against that provider's grammar, full match and anchored at both ends. Where the grammar is `KEY-N`, its KEY must equal the resolved project key after ASCII-upper normalisation; a well-formed reference carrying another key is dropped and reported once as `TRACEABILITY: DEGRADED (foreign issue reference {ref})`. Deduplicate the SURVIVORS — after the gate, never before — then take the first ≤50, appending `…and {n} more issues` if more exist. A `Merge pull request` subject and a trailing parenthesised reference carry no keyword and are never candidates; an empty `SHIPPED_ISSUES` is reported empty, not degraded, unless the Mechanics flag merged PRs they could not resolve.
84
84
  @end
85
85
 
86
- @define wave_report_read_step():
87
- 2. Resolve and read `WAVE_REPORT_PATH`: if absolute, use as-is; if repo-relative, resolve against WORKTREE_PATH when supplied, else against cwd. Read the resulting file (the wave-report.md written by the wave orchestrator).
88
- - The wave report MUST NOT reproduce verbatim `<external-thread>` or `<untrusted-issue-body>` content (Principle 8).
89
- @end
90
-
91
86
  @export base_branch_step
92
87
  @export branch_create_steps
93
88
  @export fetch_issue_neutralise
@@ -95,4 +90,3 @@ Neutralise any `</untrusted-issue-body>` in the fetched body before wrapping it
95
90
  @export release_evidence_tag_step
96
91
  @export release_evidence_range_steps
97
92
  @export release_evidence_gate_step
98
- @export wave_report_read_step
@@ -1,7 +1,7 @@
1
1
  // src/assets/scripts/pr-evidence.cjs
2
2
  //
3
- // The PURE core of PR test-plan evidence: the TP-line, claim, exception,
4
- // EVIDENCE-line and wave-block grammars, the marker literals, the state ladder,
3
+ // The PURE core of PR test-plan evidence: the TP-line, claim, exception and
4
+ // EVIDENCE-line grammars, the marker literals, the state ladder,
5
5
  // rendering, the CRLF-aware body splice, the tally, the link check and the one
6
6
  // implementation of the trust rule. Installed as a top-level sibling of redact-secrets.cjs under
7
7
  // ~/.devflow/scripts/ and required by verify-evidence.cjs, which performs every
@@ -30,12 +30,7 @@
30
30
  /** @typedef {'ci' | 'local' | 'manual'} Method */
31
31
  /** @typedef {'PASS' | 'FAIL' | 'SKIP'} Outcome */
32
32
  /** @typedef {'ticket-link' | 'test-plan'} ExceptionKind */
33
- /** @typedef {'PASS' | 'UNVERIFIED' | 'QUARANTINED' | 'BLOCKED'} WaveVerdict */
34
- /**
35
- * @typedef {'oversize' | 'malformed' | 'empty' | 'order' | 'duplicate' | 'mismatch' | 'invalid'
36
- * | 'orphan' | 'unlinked' | 'unmerged'} ErrorCode
37
- * The last three are the wave block's cross rules (see parseWaveBlock).
38
- */
33
+ /** @typedef {'oversize' | 'malformed' | 'empty' | 'order' | 'duplicate' | 'mismatch' | 'invalid'} ErrorCode */
39
34
 
40
35
  /** The six TP states, closed. The contract's `States (closed)` list is pinned to this order. */
41
36
  const STATES = Object.freeze(/** @type {State[]} */ ([
@@ -64,9 +59,6 @@ const EXCEPTION_KINDS = Object.freeze(/** @type {ExceptionKind[]} */ (['ticket-l
64
59
  * PATH_CHARS / DIFF_FILES a diff path, and the diff paths, a glob is matched against
65
60
  * RUNS_PER_SHA runs at one SHA (`gh run list --limit 20`)
66
61
  * TRUST_LOOKUPS permission lookups per spawn
67
- * WAVE_BLOCK_CHARS a whole wave block
68
- * WAVE_ROWS a wave block's table rows, and its related lines (at most one per row,
69
- * plus the one tracking line — see D-WAVE-TRACKING)
70
62
  */
71
63
  const LIMITS = Object.freeze({
72
64
  TP_MAX: 200,
@@ -83,8 +75,6 @@ const LIMITS = Object.freeze({
83
75
  DIFF_FILES: 5000,
84
76
  RUNS_PER_SHA: 20,
85
77
  TRUST_LOOKUPS: 20,
86
- WAVE_BLOCK_CHARS: 16000,
87
- WAVE_ROWS: 100,
88
78
  });
89
79
 
90
80
  /**
@@ -210,79 +200,6 @@ const EM_DASH = '—';
210
200
  const UNTICKED = '- [ ] ';
211
201
  const TICKED = '- [x] ';
212
202
 
213
- /**
214
- * D-WAVE: the wave block — the wave PR body's `## Related Issues` section and its
215
- * evidence table, rendered by /devflow:dynamic-build and admitted only through
216
- * `verify-evidence.cjs check wave`:
217
- *
218
- * ## Related Issues
219
- * Refs #9
220
- * Closes #12
221
- * Refs #13
222
- *
223
- * ## Wave Evidence
224
- * | T | Ticket | Verdict | Evaluate | Test | Surviving | Coverage |
225
- * |---|---|---|---|---|---|---|
226
- * | T1 | #12 | PASS | PASS | PASS | 0 | complete |
227
- * | T2 | #13 | QUARANTINED | FAIL-FIXED | SKIPPED | 2 | incomplete |
228
- *
229
- * Every cell is a closed-vocabulary token or a bounded number, and every related
230
- * line is a shape-gated reference, so no byte of a ticket title, a finding or an
231
- * agent's prose can ride into the PR body on it. The two headings, in order; an
232
- * admitted block IS the body's `## Related Issues` section. `Refs #9` above is the
233
- * optional tracking line (D-WAVE-TRACKING).
234
- */
235
- const WAVE_HEADINGS = Object.freeze(['## Related Issues', '## Wave Evidence']);
236
-
237
- /** The evidence table's header line, verbatim. */
238
- const WAVE_TABLE_HEADER = '| T | Ticket | Verdict | Evaluate | Test | Surviving | Coverage |';
239
-
240
- /** The separator under it: one bare `---` per column, no alignment colon. */
241
- const WAVE_TABLE_SEPARATOR = '|---|---|---|---|---|---|---|';
242
-
243
- /**
244
- * A wave row's verdict: merged as PASS; merged as UNVERIFIED (a FAIL-FIXED Gate 2,
245
- * fixes applied and not re-run); QUARANTINED (it ran and did not merge, or its
246
- * merge was quarantined after a red build); BLOCKED (it never ran).
247
- */
248
- const WAVE_VERDICTS = Object.freeze(/** @type {WaveVerdict[]} */ (['PASS', 'UNVERIFIED', 'QUARANTINED', 'BLOCKED']));
249
-
250
- /** The verdicts of a ticket that merged — the only rows a `Closes` line may name. */
251
- const WAVE_MERGED_VERDICTS = Object.freeze(/** @type {WaveVerdict[]} */ (['PASS', 'UNVERIFIED']));
252
-
253
- /**
254
- * An Evaluate or Test cell: a Gate 2 verdict, or `—` where the gate never ran or a Test run gave no result.
255
- * `BLOCKED` is never a cell here, and it has two meanings in the table: a row Verdict `BLOCKED` is a ticket that
256
- * never ran, and a Test result `BLOCKED` is a run that gave no result, shown as `—`.
257
- */
258
- const WAVE_GATE_VALUES = Object.freeze(['PASS', 'FAIL', 'FAIL-FIXED', 'SKIPPED', EM_DASH]);
259
-
260
- /** A Coverage cell: whether the review pass covered every focus. */
261
- const WAVE_COVERAGE_VALUES = Object.freeze(['complete', 'incomplete', EM_DASH]);
262
-
263
- /** A row's first cell, `T1` … `T100` (the sequence itself is checked by the parser). */
264
- const WAVE_T_RE = /^T(?:100|[1-9][0-9]?)$/;
265
-
266
- /** A Surviving cell: the review pass's surviving-finding count, or `—`. */
267
- const WAVE_SURVIVING_RE = /^(?:[0-9]{1,3}|—)$/;
268
-
269
- /**
270
- * D-WAVE-REF: one related line of a wave block. `Closes` takes only a GitHub `#N`
271
- * — Jira and Linear render `Refs` — and `Refs` takes `#N` (an unmerged GitHub
272
- * ticket: its captured `Closes` line with the keyword swapped) or a keyed ref
273
- * whose key is the union of the Jira `[A-Z][A-Z0-9_]{1,9}` and the Linear
274
- * `[A-Z][A-Z0-9]{0,9}` key grammars. It admits every line code.md's R7 paste gate
275
- * admits, their Closes→Refs swap, and nothing else (parity-pinned by
276
- * tests/evidence/wave-block.test.ts).
277
- */
278
- const RELATED_LINE_RE = Object.freeze(/^(?:Closes (?<closes>#[1-9][0-9]{0,8})|Refs (?<refs>#[1-9][0-9]{0,8}|[A-Z][A-Z0-9_]{0,9}-[1-9][0-9]{0,8}))$/);
279
-
280
- /** A wave row's Ticket cell: a reference of RELATED_LINE_RE's shapes, or `(none)`. */
281
- const WAVE_TICKET_RE = Object.freeze(/^(?:#[1-9][0-9]{0,8}|[A-Z][A-Z0-9_]{0,9}-[1-9][0-9]{0,8}|\(none\))$/);
282
-
283
- /** The Ticket cell of a row with no reference. */
284
- const NO_TICKET = '(none)';
285
-
286
203
  /** The three evidence-file sections, by heading. A Map, so no prototype key can match. */
287
204
  const SECTION_KEYS = new Map([
288
205
  [PLAN_HEADING, 'testPlan'],
@@ -390,15 +307,6 @@ const TRUSTED_PERMISSIONS = Object.freeze(['admin', 'write']);
390
307
  * stale: readonly number[], exceptions: readonly ExceptionKind[],
391
308
  * approval: 'yes' | 'no' | 'unchecked', key: string, posted: 'yes' | 'no' | 'n/a',
392
309
  * body: 'same' | 'changed' }} EvidenceFields
393
- *
394
- * @typedef {{ keyword: 'Closes' | 'Refs', ref: string, line: number }} WaveRelated
395
- * One related line; `line` is its 1-based line in the block.
396
- * @typedef {{ k: number, ticket: string | null, verdict: WaveVerdict, evaluate: string, test: string,
397
- * surviving: number | null, coverage: string, line: number }} WaveRow
398
- * One table row; `ticket` is null for `(none)`, `surviving` for `—`.
399
- * @typedef {{ tracking: WaveRelated | null, related: readonly WaveRelated[], rows: readonly WaveRow[] }} WaveBlock
400
- * `related` holds the lines that name a row; `tracking` is the leading `Refs`
401
- * line that names none (D-WAVE-TRACKING), or null.
402
310
  */
403
311
 
404
312
  // ---------------------------------------------------------------------------
@@ -1785,171 +1693,6 @@ function parseEvidenceLine(line) {
1785
1693
  }));
1786
1694
  }
1787
1695
 
1788
- // ---------------------------------------------------------------------------
1789
- // The wave block (D-WAVE)
1790
- // ---------------------------------------------------------------------------
1791
-
1792
- /**
1793
- * Parse one wave row: seven ` | `-separated cells inside `| ` and ` |`, each in its
1794
- * closed vocabulary. No cell value holds ` | `, so the split is unambiguous and a
1795
- * forged cell leaves the row with the wrong count. A BLOCKED row never ran, so
1796
- * its last four cells are `—`. Null when the row is outside the grammar; the
1797
- * caller checks the T sequence.
1798
- *
1799
- * @param {string} line
1800
- * @returns {Omit<WaveRow, 'line'> | null}
1801
- */
1802
- function parseWaveRow(line) {
1803
- if (!line.startsWith('| ') || !line.endsWith(' |')) return null;
1804
- const cells = line.slice(2, -2).split(' | ');
1805
- if (cells.length !== 7) return null;
1806
- const [t, ticket, verdict, evaluate, test, surviving, coverage] = cells;
1807
- if (!WAVE_T_RE.test(t) || !WAVE_TICKET_RE.test(ticket)
1808
- || !WAVE_VERDICTS.includes(/** @type {WaveVerdict} */ (verdict))
1809
- || !WAVE_GATE_VALUES.includes(evaluate) || !WAVE_GATE_VALUES.includes(test)
1810
- || !WAVE_SURVIVING_RE.test(surviving) || !WAVE_COVERAGE_VALUES.includes(coverage)) return null;
1811
- if (verdict === 'BLOCKED' && [evaluate, test, surviving, coverage].some(c => c !== EM_DASH)) return null;
1812
- return {
1813
- k: Number(t.slice(1)),
1814
- ticket: ticket === NO_TICKET ? null : ticket,
1815
- verdict: /** @type {WaveVerdict} */ (verdict),
1816
- evaluate,
1817
- test,
1818
- surviving: surviving === EM_DASH ? null : Number(surviving),
1819
- coverage,
1820
- };
1821
- }
1822
-
1823
- /**
1824
- * D-WAVE-LINKS-ONLY: the links-only block, for related lines that ran to the end of the
1825
- * text. Only `Refs #<n>` and `Refs <KEY-N>` lines make a block of their own (RELATED_LINE_RE
1826
- * has already shaped each ref), and a ref may not repeat. Null when the lines are not all
1827
- * `Refs` lines: the caller refuses the block as before.
1828
- *
1829
- * @param {readonly WaveRelated[]} related
1830
- * @returns {Result<WaveBlock> | null}
1831
- */
1832
- function parseLinksOnlyBlock(related) {
1833
- if (related.length === 0 || !related.every(rel => rel.keyword === 'Refs')) return null;
1834
- /** @type {Set<string>} */
1835
- const seen = new Set();
1836
- for (const rel of related) {
1837
- if (seen.has(rel.ref)) return fail('duplicate', rel.line);
1838
- seen.add(rel.ref);
1839
- }
1840
- return ok(Object.freeze({ tracking: null, related: Object.freeze(related), rows: Object.freeze([]) }));
1841
- }
1842
-
1843
- /**
1844
- * D-WAVE: parse a wave block (LF or CRLF, at most WAVE_BLOCK_CHARS). It holds
1845
- * exactly, in order: `## Related Issues`; 0–WAVE_ROWS + 1 related lines; one
1846
- * blank line; `## Wave Evidence`; the header and separator verbatim; 1–WAVE_ROWS
1847
- * rows T1…Tn; then only blank lines. No leading text, no other line anywhere.
1848
- *
1849
- * D-WAVE-CLOSE — the cross rules, which make a wrong closing line unrepresentable:
1850
- * - a related ref names exactly one row's Ticket (`orphan` when none — the one
1851
- * tracking line below aside), and no ref repeats (`duplicate`) — nor does a
1852
- * Ticket across rows (`duplicate`), so "exactly one" is never two;
1853
- * - `Closes` names only a PASS or UNVERIFIED row (`unmerged`);
1854
- * - every PASS or UNVERIFIED row with a Ticket has its related line (`unlinked`);
1855
- * - so any other row carries at most one line, and that line is a `Refs`.
1856
- *
1857
- * D-WAVE-TRACKING: the FIRST related line may instead be the wave's tracking
1858
- * issue — a `Refs` line whose ref names no row. It is admitted once and only
1859
- * there: a later line naming no row is `orphan`, a second leading one included,
1860
- * and a `Closes` naming no row is `orphan` wherever it stands, because a tracking
1861
- * issue outlives the wave and the wave PR never closes it. The line is shape-gated
1862
- * like every related line, so it adds no free text to the body. A leading `Refs`
1863
- * that names a row is that row's line, never a tracking line.
1864
- *
1865
- * D-WAVE-LINKS-ONLY: with `options.linksOnly === true` (the opt-in, spelled
1866
- * `check wave --links-only` on the command line), a block may also be the
1867
- * `## Related Issues` heading and 1–WAVE_ROWS + 1 `Refs #<n>` or `Refs <KEY-N>` lines and
1868
- * nothing after them (blank lines aside): no evidence section, no rows. It parses to no
1869
- * tracking line and no rows. A ref may not repeat (`duplicate`). `Closes` is never
1870
- * admitted there: with no row, nothing has merged, so nothing may close an issue.
1871
- * Without the opt-in, and for any other block that stops after its related lines, a lone
1872
- * `Closes #<n>` included, the block is `malformed`, as the grammar stood before the opt-in.
1873
- *
1874
- * Structure is checked before the cross rules, so the first failure found is the
1875
- * one reported. An error carries a code and a line number, never input bytes.
1876
- *
1877
- * @param {unknown} text
1878
- * @param {{ linksOnly?: boolean }} [options]
1879
- * @returns {Result<WaveBlock>}
1880
- */
1881
- function parseWaveBlock(text, options) {
1882
- if (typeof text !== 'string') return fail('invalid');
1883
- if (text.length > LIMITS.WAVE_BLOCK_CHARS) return fail('oversize');
1884
- const rows = textLines(text);
1885
- let end = rows.length;
1886
- while (end > 0 && rows[end - 1] === '') end--;
1887
- if (rows[0] !== WAVE_HEADINGS[0]) return fail('malformed', 1);
1888
-
1889
- /** @type {WaveRelated[]} */
1890
- const related = [];
1891
- let i = 1;
1892
- for (; i < end && rows[i] !== ''; i++) {
1893
- // One line per row at most, plus the tracking line (D-WAVE-TRACKING).
1894
- if (related.length === LIMITS.WAVE_ROWS + 1) return fail('oversize', i + 1);
1895
- const m = RELATED_LINE_RE.exec(rows[i]);
1896
- if (m === null || m.groups === undefined) return fail('malformed', i + 1);
1897
- const closes = m.groups.closes;
1898
- related.push(Object.freeze({
1899
- keyword: /** @type {'Closes' | 'Refs'} */ (closes === undefined ? 'Refs' : 'Closes'),
1900
- ref: closes === undefined ? m.groups.refs : closes,
1901
- line: i + 1,
1902
- }));
1903
- }
1904
- // D-WAVE-LINKS-ONLY: the opt-in is set and the related lines ran to the end of the text.
1905
- if (isObject(options) && options.linksOnly === true && i >= end) {
1906
- const linksOnly = parseLinksOnlyBlock(related);
1907
- if (linksOnly !== null) return linksOnly;
1908
- }
1909
- // rows[i] is the one blank line; indices past `end` hold only blanks, so a
1910
- // missing section fails the first comparison that expects a non-blank line.
1911
- if (rows[i + 1] !== WAVE_HEADINGS[1]) return fail('malformed', i + 2);
1912
- if (rows[i + 2] !== WAVE_TABLE_HEADER) return fail('malformed', i + 3);
1913
- if (rows[i + 3] !== WAVE_TABLE_SEPARATOR) return fail('malformed', i + 4);
1914
-
1915
- /** @type {WaveRow[]} */
1916
- const table = [];
1917
- /** @type {Map<string, WaveRow>} */
1918
- const byTicket = new Map();
1919
- for (let j = i + 4; j < end; j++) {
1920
- if (table.length === LIMITS.WAVE_ROWS) return fail('oversize', j + 1);
1921
- const parsed = parseWaveRow(rows[j]);
1922
- if (parsed === null) return fail('malformed', j + 1);
1923
- if (parsed.k !== table.length + 1) return fail('order', j + 1);
1924
- const r = Object.freeze({ ...parsed, line: j + 1 });
1925
- if (r.ticket !== null) {
1926
- if (byTicket.has(r.ticket)) return fail('duplicate', j + 1);
1927
- byTicket.set(r.ticket, r);
1928
- }
1929
- table.push(r);
1930
- }
1931
- if (table.length === 0) return fail('empty');
1932
-
1933
- // D-WAVE-TRACKING: only the first line, only as `Refs`, and only naming no row.
1934
- const lead = related[0];
1935
- const tracking = lead !== undefined && lead.keyword === 'Refs' && !byTicket.has(lead.ref) ? lead : null;
1936
- const rowLines = tracking === null ? related : related.slice(1);
1937
-
1938
- /** @type {Set<string>} */
1939
- const linked = new Set();
1940
- for (const rel of rowLines) {
1941
- const r = byTicket.get(rel.ref);
1942
- if (r === undefined) return fail('orphan', rel.line);
1943
- if (linked.has(rel.ref)) return fail('duplicate', rel.line);
1944
- if (rel.keyword === 'Closes' && !WAVE_MERGED_VERDICTS.includes(r.verdict)) return fail('unmerged', rel.line);
1945
- linked.add(rel.ref);
1946
- }
1947
- for (const r of table) {
1948
- if (r.ticket !== null && WAVE_MERGED_VERDICTS.includes(r.verdict) && !linked.has(r.ticket)) return fail('unlinked', r.line);
1949
- }
1950
- return ok(Object.freeze({ tracking, related: Object.freeze(rowLines), rows: Object.freeze(table) }));
1951
- }
1952
-
1953
1696
  // ---------------------------------------------------------------------------
1954
1697
  // Exports — frozen; verify-evidence.cjs and the unit tests are the consumers
1955
1698
  // ---------------------------------------------------------------------------
@@ -1972,14 +1715,6 @@ module.exports = Object.freeze({
1972
1715
  SHA_RE,
1973
1716
  GLOB_RE,
1974
1717
  LOGIN_RE,
1975
- WAVE_HEADINGS,
1976
- WAVE_TABLE_HEADER,
1977
- WAVE_VERDICTS,
1978
- WAVE_MERGED_VERDICTS,
1979
- WAVE_GATE_VALUES,
1980
- RELATED_LINE_RE,
1981
- WAVE_TICKET_RE,
1982
- parseWaveBlock,
1983
1718
  parsePlan,
1984
1719
  parseClaims,
1985
1720
  parseExceptions,
@@ -126,9 +126,7 @@ const tty = require('tty');
126
126
 
127
127
  /**
128
128
  * The strict base-name class, one literal. Every prompt that checks a base name copies its
129
- * `source` exactly, and a test holds each copy equal to this export. The one deliberate
130
- * variant is the dynamic-build helper's BRANCH_NAME: a ticket branch is local, so the helper
131
- * holds this class less the `origin/` refusal, and a test holds that relation. In words: 1 to
129
+ * `source` exactly, and a test holds each copy equal to this export. In words: 1 to
132
130
  * 200 ASCII letters, digits and `._/+@,#-`; not starting with `-`, `#` or `origin/`; no `..`;
133
131
  * not the whole name `@`, `HEAD`, `FETCH_HEAD`, `ORIG_HEAD` or `MERGE_HEAD`.
134
132
  */
@@ -9,8 +9,7 @@
9
9
  // through SHA ancestry, the diff since the claim, and `gh run view --attempt`.
10
10
  //
11
11
  // Usage:
12
- // node verify-evidence.cjs check tp|block|exceptions|wave <file>
13
- // node verify-evidence.cjs check wave --links-only <file>
12
+ // node verify-evidence.cjs check tp|block|exceptions <file>
14
13
  // node verify-evidence.cjs render --plan <file>
15
14
  // node verify-evidence.cjs verify --pr <n> [--publication <mode>] [--evidence <file>]
16
15
  // [--state <dir>] [--block-out <file>] [--comment-out <file>] [--stale-out <file>] [--approval]
@@ -25,10 +24,7 @@
25
24
  // stdout (D-VERIFY-STDOUT) is empty or exactly one of these shapes, each closed-vocabulary
26
25
  // (the render block's TP lines come from the caller's own plan file):
27
26
  // check nothing — the exit code is the answer; the diagnostic is on stderr.
28
- // `check wave --links-only` also admits the links-only wave block (the
29
- // `## Related Issues` heading and `Refs #<n>` or `Refs <KEY-N>` lines, no
30
- // evidence section); plain `check wave` refuses that block as malformed
31
- // render the creation block: markers, `## Test Plan`, every TP unticked
27
+ // render the creation block: markers, `## Test Plan`, every TP unticked
32
28
  // verify EVIDENCE pr:<n> head:<sha> total:<n> VERIFIED-CI:<n> ATTESTED-LOCAL:<n>
33
29
  // UNVERIFIED:<n> STALE:<n> FAILED:<n> INDETERMINATE:<n> stale:<ids|none>
34
30
  // exceptions:<kinds|none> approval:<yes|no|unchecked> key:<hex>
@@ -262,8 +258,7 @@ const CLAIM_REFUSAL_RE = /^CLAIM refused \((?<reason>head not one sha|evidence f
262
258
  const CLAIM_ARG_RE = /^TP-(?<n>[1-9][0-9]{0,2})=(?<outcome>PASS|FAIL|SKIP)(?::(?<exit>[^\s]{1,12}))?$/;
263
259
 
264
260
  const USAGE = [
265
- 'Usage: node verify-evidence.cjs check tp|block|exceptions|wave <file>',
266
- ' node verify-evidence.cjs check wave --links-only <file>',
261
+ 'Usage: node verify-evidence.cjs check tp|block|exceptions <file>',
267
262
  ' node verify-evidence.cjs render --plan <file>',
268
263
  ' node verify-evidence.cjs verify --pr <n> [--publication <mode>] [--evidence <file>]',
269
264
  ' [--state <dir>] [--block-out <file>] [--comment-out <file>] [--stale-out <file>] [--approval]',
@@ -323,7 +318,7 @@ const USAGE = [
323
318
 
324
319
  /**
325
320
  * @typedef {{ kind: 'usage' }
326
- * | { kind: 'check', what: 'tp' | 'block' | 'exceptions' | 'wave', file: string, linksOnly: boolean }
321
+ * | { kind: 'check', what: 'tp' | 'block' | 'exceptions', file: string }
327
322
  * | { kind: 'render', plan: string }
328
323
  * | { kind: 'verify', pr: number, publication: string, evidence: string | null, state: string | null,
329
324
  * blockOut: string | null, commentOut: string | null, staleOut: string | null, approval: boolean }
@@ -394,12 +389,10 @@ function parseArgs(argv) {
394
389
  const sub = rest[0];
395
390
  const tail = rest.slice(1);
396
391
  if (sub === 'check') {
397
- // `check wave --links-only <file>` is the one three-argument form: the links-only wave block is opt-in.
398
- const linksOnly = tail.length === 3 && tail[0] === 'wave' && tail[1] === '--links-only';
399
- if ((tail.length !== 2 && !linksOnly) || !['tp', 'block', 'exceptions', 'wave'].includes(tail[0])) return USAGE_ARGS;
400
- const file = tail[tail.length - 1];
392
+ if (tail.length !== 2 || !['tp', 'block', 'exceptions'].includes(tail[0])) return USAGE_ARGS;
393
+ const file = tail[1];
401
394
  if (typeof file !== 'string' || file === '' || file.startsWith('-') || file.length > MAX_ARG_CHARS) return USAGE_ARGS;
402
- return { kind: 'check', what: /** @type {'tp' | 'block' | 'exceptions' | 'wave'} */ (tail[0]), file, linksOnly };
395
+ return { kind: 'check', what: /** @type {'tp' | 'block' | 'exceptions'} */ (tail[0]), file };
403
396
  }
404
397
  if (sub === 'render') {
405
398
  const f = parseFlags(tail, { '--plan': 'value' });
@@ -2137,8 +2130,7 @@ function sectionOrWhole(text, section) {
2137
2130
  }
2138
2131
 
2139
2132
  /**
2140
- * `check wave` reads the whole file: a wave block is a PR-body section of its
2141
- * own, never a section of an evidence file.
2133
+ * `check tp|block|exceptions`: the exit code is the answer, and stdout stays empty.
2142
2134
  *
2143
2135
  * @param {Io} io
2144
2136
  * @param {Extract<ParsedArgs, { kind: 'check' }>} args
@@ -2150,12 +2142,15 @@ function runCheck(io, args) {
2150
2142
  let result;
2151
2143
  if (args.what === 'tp') result = PE.parsePlan(sectionOrWhole(text, 'testPlan'));
2152
2144
  else if (args.what === 'exceptions') result = PE.parseExceptions(sectionOrWhole(text, 'exceptions'));
2153
- else if (args.what === 'wave') result = PE.parseWaveBlock(text, { linksOnly: args.linksOnly });
2154
- else {
2145
+ else if (args.what === 'block') {
2155
2146
  const b = PE.parseBlock(text);
2156
2147
  // R7 pastes only the creation form: TP lines, none ticked.
2157
2148
  result = b.ok && (b.value.kind !== 'lines' || b.value.ticked.length > 0) ? { ok: false, error: { code: 'invalid', line: 0 } } : b;
2158
2149
  }
2150
+ if (result === undefined) {
2151
+ io.stderr(USAGE + '\n');
2152
+ return { code: EXIT_CODES.USAGE, stdout: '' };
2153
+ }
2159
2154
  if (result.ok) return { code: EXIT_CODES.OK, stdout: '' };
2160
2155
  io.stderr('verify-evidence: check ' + args.what + ': ' + result.error.code
2161
2156
  + (result.error.line > 0 ? ' at line ' + result.error.line : '') + '\n');
@@ -46,17 +46,9 @@ All generated documentation lives under `.devflow/docs/` at the checkout's tople
46
46
  ├── design/
47
47
  │ ├── {ISSUE_ID}-{topic-slug}.{timestamp}.md # /plan, one issue
48
48
  │ ├── multi-{topic-slug}.{timestamp}.md # /plan, several issues (frontmatter issue: pending)
49
- │ ├── {topic-slug}.{timestamp}.md # /plan, no issue
50
- │ └── {slug}/{timestamp}/ # /dynamic-plan run
51
- │ ├── {ticket-slug}-plan.md # one plan per ticket
52
- │ └── DECISIONS-NEEDED.md
53
- ├── tickets/{slug}/{timestamp}/ # /dynamic-tickets
54
- │ ├── {ticket-slug}.md
55
- │ └── tracking-issue.md
56
- ├── waves/{slug}/{timestamp}/wave-report.md # /dynamic-build
49
+ │ └── {topic-slug}.{timestamp}.md # /plan, no issue
57
50
  ├── handoff-{branch-slug}.md # Code agent phase handoff; deleted once the PR exists
58
- ├── evidence-{branch-slug}.md # /implement test plan and claims; stays after the PR
59
- └── evidence-wave-{slug}.md # /dynamic-build wave test plan and claims
51
+ └── evidence-{branch-slug}.md # /implement test plan and claims; stays after the PR
60
52
 
61
53
  .devflow/memory/
62
54
  ├── WORKING-MEMORY.md # Auto-maintained by Stop hook (overwritten)
@@ -77,7 +69,6 @@ The head markers for incremental runs are not part of this tree. `/code-review`
77
69
 
78
70
  `YYYY-MM-DD_HHMM`, local time (sortable, readable), computed once per run and reused for every path of that run.
79
71
  - **Same-minute collision.** `/code-review`, `/bug-analysis` and `/research` create their run directory themselves: when it already exists, the second run appends seconds (`2025-12-26_143005`). It sorts after its minute, so newest-first listings stay correct.
80
- - **Workflow runs** (`tickets/`, `design/{slug}/`, `waves/`) are stamped in local time by the workflow that creates them and take no collision suffix: a second run in the same minute writes into the first's directory.
81
72
  - A design document's timestamp is the suffix before `.md`, so a listing sorts by that suffix, not by the leading `{ISSUE_ID}`.
82
73
 
83
74
  ### Branch Slugs
@@ -87,16 +78,11 @@ A slug builds a path or reaches a shell only when it matches `^[A-Za-z0-9._-]{1,
87
78
 
88
79
  ### Topic Slugs
89
80
 
90
- One rule for every topic and initiative `{slug}`: lowercase; each run of characters outside `a-z0-9` becomes one `-`; leading and trailing `-` are trimmed; at most 50 characters (trim again after the cut); `untitled` when nothing is left:
81
+ One rule for every topic `{slug}`: lowercase; each run of characters outside `a-z0-9` becomes one `-`; leading and trailing `-` are trimmed; at most 50 characters (trim again after the cut); `untitled` when nothing is left:
91
82
  ```bash
92
83
  TOPIC_SLUG=$(s=$(echo "$TOPIC" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//' | cut -c1-50 | sed -E 's/-+$//'); echo "${s:-untitled}")
93
84
  ```
94
- `/plan` and `/research` take `{topic-slug}` from it. `/dynamic-tickets` uses it for the initiative `{slug}`, and `/dynamic-plan` reuses the `{slug}` of the ticket set it plans (`wave` for any other source).
95
-
96
- ### Exceptions
97
-
98
- - **`{ticket-slug}`** (`tickets/…/{ticket-slug}.md`, `design/…/{ticket-slug}-plan.md`): the ticket title lowercased with each run outside `a-z0-9` as one `-`, nothing trimmed and no length cap. `/dynamic-tickets` matches a ticket file by this name when it amends the ticket, so trimming would rename a file its own amend phase already wrote.
99
- - **Wave `{slug}`** (`waves/`, `evidence-wave-{slug}.md`): the part of the integration branch after `wave/`, at most 60 characters, not derived by the topic rule. It makes `evidence-wave-{slug}.md` the `evidence-{branch-slug}.md` of the branch `wave/{slug}`.
85
+ `/plan` and `/research` take `{topic-slug}` from it.
100
86
 
101
87
  ### File Naming Patterns
102
88
 
@@ -126,7 +112,7 @@ ensure_docs_dir() { mkdir -p "$(get_docs_root)/.devflow/docs/$1"; }
126
112
  - **Reviews**: the Review agents and the Synthesize agent (review mode) write into `reviews/{branch-slug}/{timestamp}/`; `/resolve` adds `resolution-summary.md` (Phase 5); `/code-review` overwrites its `.last-review-head` marker, under the git directory, with the HEAD SHA.
127
113
  - **Bug analysis**: the Debug agents and the Synthesize agent (bug-analysis mode) write into `bug-analysis/{branch-slug}/{timestamp}/`; the scanner calls write the SARIF files, the changed-name list for the Snyk filter and the CodeQL log, and the counts step writes the findings tables, all into that run folder; `/bug-analysis` overwrites its `.last-analysis-head` marker, under the git directory, with the HEAD SHA only when every tool that was to run completed, or `--no-static` is set.
128
114
  - **Research**: the Research agents write `{type}.md`, and the Synthesize agent (research mode) writes `research-summary.md` through `RESEARCH_BASE_DIR`, both into `research/{topic-slug}/{timestamp}/`.
129
- - **Design**: `/plan` writes the design document; `/dynamic-plan` writes `design/{slug}/{timestamp}/`, `/dynamic-tickets` `tickets/{slug}/{timestamp}/`, `/dynamic-build` `waves/{slug}/{timestamp}/` and `evidence-wave-{slug}.md`.
115
+ - **Design**: `/plan` writes the design document.
130
116
  - **Handoff and evidence**: `/implement` writes `evidence-{branch-slug}.md`, which stays after the PR, and the handoff file, to which each Code agent appends one phase section and which is deleted once the PR exists.
131
117
  - **Memory and curation**: the Stop hook overwrites `.devflow/memory/WORKING-MEMORY.md`; `decisions.md` and `pitfalls.md` are rendered from `decisions-ledger.jsonl` (active entries in full, inactive ones in an Inactive table).
132
118
 
@@ -18,8 +18,7 @@ Correct patterns for Devflow documentation artifacts with templates and helper f
18
18
  │ └── {timestamp}/ # Incremental review
19
19
  │ └── ...
20
20
  ├── design/ # Implementation plans
21
- │ ├── {ISSUE_ID}-{topic-slug}.{timestamp}.md # /plan (multi-{topic-slug} / {topic-slug} without an issue)
22
- │ └── {slug}/{timestamp}/ # /dynamic-plan: {ticket-slug}-plan.md, DECISIONS-NEEDED.md
21
+ │ └── {ISSUE_ID}-{topic-slug}.{timestamp}.md # /plan (multi-{topic-slug} / {topic-slug} without an issue)
23
22
  └── CATCH_UP.md # Latest summary (overwritten)
24
23
  ```
25
24