@event4u/agent-config 13.0.0 → 14.1.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 (123) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +195 -315
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  5. package/dist/agent-src/commands/fix/ci.md +39 -1
  6. package/dist/agent-src/commands/pr/create.md +65 -4
  7. package/dist/agent-src/commands/roadmap/next.md +28 -5
  8. package/dist/agent-src/commands/work.md +16 -0
  9. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  10. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  11. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  12. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  14. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  15. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  16. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  17. package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
  18. package/dist/agent-src/rules/ui-audit-gate.md +25 -8
  19. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  20. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  21. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  22. package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
  23. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  24. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  25. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  26. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  27. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  28. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  29. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  30. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  31. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  32. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  33. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  34. package/dist/agent-src/templates/roadmaps.md +43 -3
  35. package/dist/cli/registry.js +1 -0
  36. package/dist/cli/registry.js.map +1 -1
  37. package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
  38. package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
  39. package/dist/cli-delegate/cmd_conformance.js +336 -112
  40. package/dist/cli-delegate/cmd_doctor.js +2 -2
  41. package/dist/cli-delegate/cmd_handoff.js +1 -1
  42. package/dist/discovery/deprecation-report.md +1 -1
  43. package/dist/discovery/discovery-manifest.json +40 -18
  44. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  45. package/dist/discovery/discovery-manifest.summary.md +3 -3
  46. package/dist/discovery/dormancy-report.md +1 -1
  47. package/dist/discovery/orphan-report.md +1 -1
  48. package/dist/discovery/packs.json +6 -5
  49. package/dist/discovery/trust-report.md +2 -2
  50. package/dist/discovery/workspaces.json +5 -4
  51. package/dist/hooks/dispatch.js +2128 -1326
  52. package/dist/mcp/registry-manifest.json +3 -3
  53. package/dist/mcp/server.json +2 -2
  54. package/dist/mcp/server.mjs +1764 -899
  55. package/dist/router.json +1 -1
  56. package/dist/scripts/condense.js +1 -1
  57. package/dist/scripts/condense.js.map +1 -1
  58. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  59. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  60. package/dist/ui/index.html +1 -1
  61. package/package.json +1 -1
  62. package/src/agent-src/scripts/gate_execute.ts +395 -0
  63. package/src/agent-src/scripts/resume_probe.ts +422 -0
  64. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  65. package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
  66. package/src/config/ci-local-parity.yml +13 -0
  67. package/src/config/evaluator-budgets.json +4 -4
  68. package/src/config/gate-coverage.yml +111 -5
  69. package/src/config/gate-violation-baselines.json +5 -5
  70. package/src/config/rule-activation-census.json +41 -0
  71. package/src/config/rule-stub-ceilings.json +52 -0
  72. package/src/scripts/_dispatch.bash +4 -0
  73. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  74. package/src/scripts/_lib/gate_ledger.ts +5 -1
  75. package/src/scripts/_lib/gh_transient.ts +80 -0
  76. package/src/scripts/_lib/release_material.ts +16 -5
  77. package/src/scripts/_lib/review_independence.ts +111 -0
  78. package/src/scripts/_lib/session_register.ts +14 -0
  79. package/src/scripts/_lib/skill_catalogue.ts +20 -0
  80. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  81. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  82. package/src/scripts/ai_council/_py_json.ts +96 -0
  83. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  84. package/src/scripts/ai_council/clients.ts +80 -143
  85. package/src/scripts/ai_council/config.ts +25 -11
  86. package/src/scripts/ai_council/probe_store.ts +196 -0
  87. package/src/scripts/ai_council/qualification.ts +425 -0
  88. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  89. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  90. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  91. package/src/scripts/build_archive_index.ts +62 -3
  92. package/src/scripts/capture_skill_catalogue.ts +264 -4
  93. package/src/scripts/check_evaluator_schema.ts +249 -0
  94. package/src/scripts/check_gate_coverage.ts +93 -0
  95. package/src/scripts/check_pr_ci_current.ts +492 -0
  96. package/src/scripts/check_review_schema.ts +195 -0
  97. package/src/scripts/check_rule_activation_census.ts +451 -0
  98. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  99. package/src/scripts/condense.ts +1 -1
  100. package/src/scripts/council_cli.ts +141 -191
  101. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  102. package/src/scripts/git_authorization_hook.ts +186 -1
  103. package/src/scripts/hook_manifest.yaml +72 -5
  104. package/src/scripts/hooks/block_unauthorized_git.ts +62 -6
  105. package/src/scripts/hooks/concern_registry.ts +4 -0
  106. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  107. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  108. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  109. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  110. package/src/scripts/hooks_doctor.ts +112 -0
  111. package/src/scripts/interruption_report.ts +443 -0
  112. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  113. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  114. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  115. package/src/scripts/release.ts +38 -53
  116. package/src/scripts/rule_activation_census.ts +292 -0
  117. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  118. package/src/scripts/schemas/review-findings.schema.json +31 -0
  119. package/src/scripts/self_review_gate.ts +6 -0
  120. package/src/scripts/session_register_hook.ts +30 -1
  121. package/src/scripts/sync_pr_branch.ts +271 -0
  122. package/src/scripts/test_council_qualification.ts +213 -0
  123. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Resume-condition probe — a gate that opened with nobody standing in front
3
+ * of it is the third failure shape `road-to-gate-autonomy` § 0 names.
4
+ *
5
+ * Every roadmap parked under `agents/roadmaps/later/` records a resume
6
+ * condition; `lint_roadmap_later_disposition` enforces that one is **written**
7
+ * and never asks whether it has since come true. Measured 2026-08-17:
8
+ * `later/road-to-request-scoped-rule-load.md` waits on "P2.1 of
9
+ * `road-to-rule-delivery-integrity` closes", and that whole roadmap archived —
10
+ * the condition fired, the file stayed parked, and nothing in the tree could
11
+ * have said so.
12
+ *
13
+ * This is a **class-0 gate** by the § 2 taxonomy: deterministic, free,
14
+ * reversible, and its output IS the unblock signal. It decides nothing — it
15
+ * reports which parked roadmaps are now resumable and hands that to a human.
16
+ *
17
+ * ## What it can and cannot decide, stated plainly
18
+ *
19
+ * A resume condition is prose. Two shapes are machine-decidable and the rest
20
+ * are not, so the verdict set has three values rather than two:
21
+ *
22
+ * - `fired` — every roadmap the condition names has archived, or the
23
+ * named step in a still-active roadmap is ticked.
24
+ * - `unmet` — at least one named roadmap is still active with the
25
+ * named step (or the whole file) still open.
26
+ * - `undecidable` — the condition names no roadmap this tree can resolve.
27
+ *
28
+ * `undecidable` is reported as its own count and never folded into "nothing
29
+ * fired". A probe that reads 44 files, understands 12 of them and announces
30
+ * silence is the gate-that-scans-nothing shape this repo has already paid for
31
+ * once; the honest form says how much of the population it could actually read.
32
+ */
33
+
34
+ import * as fs from 'node:fs';
35
+ import * as path from 'node:path';
36
+
37
+ /**
38
+ * The markers this probe treats as a DEPENDENCY condition.
39
+ *
40
+ * Deliberately NARROWER than `lint_roadmap_later_disposition`'s set, which
41
+ * also accepts a bare `trigger`. That lint answers "is a condition recorded",
42
+ * where any of the five words is evidence; this probe answers "has it fired",
43
+ * and in this tree `Trigger:` is overwhelmingly a *provenance* idiom —
44
+ * "**Trigger:** Spun out of `road-to-governance-moat` (Iron Law 3 …)" names
45
+ * where the roadmap came from, not what it waits for. Measured on the first
46
+ * live run: accepting `trigger` reported that note as FIRED because the
47
+ * roadmap it was spun out of had since archived. A probe whose headline
48
+ * number is inflated by provenance mentions is worse than one that stays
49
+ * quiet — it proposes resuming work on evidence that says nothing.
50
+ */
51
+ const RESUME_LINE_RE = /\b(blocked until|resume when|blocked-until|resume-when)\b/i;
52
+
53
+ /**
54
+ * Structure that says the condition has parts beyond its roadmap references.
55
+ *
56
+ * `BOTH`, an `AND`, or an enumerated `(1)` / `(a)` list means the author wrote
57
+ * a conjunction, and this probe can only read one conjunct — the roadmap
58
+ * dispositions. Reporting FIRED because that conjunct came true would claim
59
+ * the whole condition on a third of the evidence. Measured on the first live
60
+ * run: `later/road-to-deferred-rule-retriever.md` waits on three archived
61
+ * roadmaps AND three demand signals no filesystem check can see.
62
+ *
63
+ * Case-INSENSITIVE, and it was not for one revision: every sibling regex here
64
+ * carries `i` and this one did not, so a lowercase "and" walked through the
65
+ * guard and its note would have fired on the single conjunct the probe can
66
+ * resolve — the exact over-report the guard exists to stop.
67
+ */
68
+ const COMPOUND_RE = /\bboth\b|\band\b|\([1-9]\)|\([a-e]\)|`[ \t]*\+|\+[ \t]*`/i;
69
+
70
+ /**
71
+ * A bolded field label that ENDS the condition.
72
+ *
73
+ * Park notes carry sibling fields in the same blockquote — `**Origin:**`,
74
+ * `**Owner:**`, `**Context, not prerequisites:**` — and swallowing them turns
75
+ * every roadmap the note credits into a claimed dependency. Measured on the
76
+ * first live run: `later/road-to-per-workspace-license-policy.md` waits on "a
77
+ * real consumer repo hits the v1 escalation" and merely *cites* the roadmap it
78
+ * was spun out of under `**Origin:**`; the probe read that citation, found it
79
+ * archived, and reported the note resumable.
80
+ *
81
+ * Same terminator discipline `_blockerField` uses in the dashboard parser, and
82
+ * for the same reason: a field ends where the next one begins.
83
+ */
84
+ const FIELD_LABEL_RE = /\*\*[^*]+:\*\*/;
85
+ /** A roadmap slug as written in prose, with or without the `.md` suffix. */
86
+ const ROADMAP_REF_RE = /\broad-to-[a-z0-9][a-z0-9-]*\b/g;
87
+ /**
88
+ * A step reference: `P2.1`, `Phase 2.1`, or a bare `2.1`.
89
+ *
90
+ * The bare form is deliberately last and deliberately narrow — two digits
91
+ * around a dot. Widening it to match a version number or a percentage would
92
+ * make the probe claim to test something it is not reading.
93
+ */
94
+ const STEP_REF_RE = /\b(?:P|Phase[ \t]+)?(\d+\.\d+)\b/;
95
+
96
+ type Disposition = 'active' | 'archive' | 'later' | 'skipped' | 'missing';
97
+ type Verdict = 'fired' | 'unmet' | 'undecidable';
98
+
99
+ interface ResumeFinding {
100
+ /** Path relative to the roadmap root, e.g. `later/road-to-x.md`. */
101
+ file: string;
102
+ /** The resume-condition sentence, as authored. */
103
+ condition: string;
104
+ /** Roadmap slugs the condition names, excluding the parked file itself. */
105
+ refs: string[];
106
+ verdict: Verdict;
107
+ /** One clause naming what decided it — always populated. */
108
+ why: string;
109
+ }
110
+
111
+ function _read(p: string): string {
112
+ try {
113
+ return fs.readFileSync(p, 'utf-8');
114
+ } catch {
115
+ return '';
116
+ }
117
+ }
118
+
119
+ function _exists(p: string): boolean {
120
+ try {
121
+ return fs.statSync(p).isFile();
122
+ } catch {
123
+ return false;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Where a roadmap slug currently lives.
129
+ *
130
+ * `missing` is a real answer and is NOT read as "closed": a slug that resolves
131
+ * nowhere is a broken reference in the park note, and reporting it as fired
132
+ * would resume a roadmap on the strength of a typo.
133
+ */
134
+ function roadmapDisposition(roadmapRoot: string, slug: string): Disposition {
135
+ const name = `${slug}.md`;
136
+ if (_exists(path.join(roadmapRoot, name))) {
137
+ return 'active';
138
+ }
139
+ for (const dir of ['archive', 'later', 'skipped'] as const) {
140
+ if (_exists(path.join(roadmapRoot, dir, name))) {
141
+ return dir;
142
+ }
143
+ }
144
+ return 'missing';
145
+ }
146
+
147
+ /**
148
+ * The resume-condition sentence of a park note.
149
+ *
150
+ * Park notes are blockquotes that wrap, so the marker line alone truncates the
151
+ * condition mid-sentence — the same defect `_blockerTodo` and the legacy
152
+ * blocked-until parser both had to fix. Continue through following quoted
153
+ * lines until the quote block breaks.
154
+ */
155
+ function extractCondition(text: string): string {
156
+ const lines = _blankFencedCode(text).split(/\r?\n/);
157
+ // The marker must be inside the park note's blockquote. Searching the whole
158
+ // file let ordinary body prose containing "blocked until" become "the
159
+ // condition", and the continuation loop then read from the wrong anchor.
160
+ const start = lines.findIndex(
161
+ (l) => _strip(l).startsWith('>') && RESUME_LINE_RE.test(l),
162
+ );
163
+ if (start === -1) {
164
+ return '';
165
+ }
166
+ const parts: string[] = [_strip(lines[start] as string)];
167
+ for (let i = start + 1; i < lines.length; i++) {
168
+ const l = _strip(lines[i] as string);
169
+ // The condition ends at the end of the quote block or a blank quoted
170
+ // line — a park note routinely carries several paragraphs of context
171
+ // after it, and swallowing those would turn every mentioned roadmap
172
+ // into a claimed dependency.
173
+ if (!l.startsWith('>')) {
174
+ break;
175
+ }
176
+ const body = _strip(l.replace(/^>[ \t]?/, ''));
177
+ if (body === '') {
178
+ break;
179
+ }
180
+ parts.push(body);
181
+ }
182
+ const joined = parts
183
+ .map((p) => _strip(p.replace(/^>[ \t]?/, '')))
184
+ .join(' ')
185
+ .trim();
186
+ return _truncateAtNextField(joined);
187
+ }
188
+
189
+ /**
190
+ * Cut the condition at the first bolded label that is not a resume marker.
191
+ *
192
+ * The resume marker itself is bolded (`**Resume when …**`), so the scan starts
193
+ * after it and keeps any further label only when it is another resume marker —
194
+ * a note that writes `**Blocked until:** … **Resume when** …` is stating one
195
+ * condition twice, not two fields.
196
+ */
197
+ function _truncateAtNextField(condition: string): string {
198
+ let cut = condition.length;
199
+ const re = new RegExp(FIELD_LABEL_RE.source, 'g');
200
+ let m: RegExpExecArray | null;
201
+ while ((m = re.exec(condition)) !== null) {
202
+ const label = m[0] as string;
203
+ if (RESUME_LINE_RE.test(label)) {
204
+ continue;
205
+ }
206
+ // A label at offset 0 would cut the condition to nothing; the marker
207
+ // line legitimately opens with one in notes shaped `**Blocked until:**`.
208
+ if (m.index === 0) {
209
+ continue;
210
+ }
211
+ cut = m.index;
212
+ break;
213
+ }
214
+ return _strip(condition.slice(0, cut));
215
+ }
216
+
217
+ function _strip(s: string): string {
218
+ return s.replace(/^[ \t\f\v]+|[ \t\f\v\r]+$/g, '');
219
+ }
220
+
221
+ /**
222
+ * Blank fenced code, preserving line count.
223
+ *
224
+ * A park note that SHOWS the resume-condition syntax in a fenced example is
225
+ * documenting it, not stating one — the same reason `parse_blockers` and
226
+ * `lint_roadmap_blockers` both strip fences before they look for blockers.
227
+ */
228
+ function _blankFencedCode(text: string): string {
229
+ return text.replace(/^[ \t]*```[^\n]*\n[\s\S]*?^[ \t]*```[ \t]*$/gm, (m) =>
230
+ '\n'.repeat((m.match(/\n/g) ?? []).length),
231
+ );
232
+ }
233
+
234
+ /**
235
+ * The condition CLAUSE inside the condition text.
236
+ *
237
+ * Park notes bold the condition and then explain it in prose:
238
+ * `**Resume when P2.1 of \`road-to-x\` closes** — the catalogue-logging
239
+ * falsifier that measures whether … and the report itself declines to …`.
240
+ * The clause is the bolded span; everything after it is commentary.
241
+ *
242
+ * The distinction is load-bearing and was measured the hard way. Making
243
+ * `COMPOUND_RE` case-insensitive (correct in itself — the flag was missing)
244
+ * immediately produced the opposite false result: an ordinary "and" in the
245
+ * *explanation* read as a conjunction of conditions, and the one genuinely
246
+ * fired note in the tree dropped out. Analysing the clause rather than the
247
+ * paragraph is what makes a case-insensitive conjunction test safe.
248
+ *
249
+ * Falls back to the whole string when no bolded span carries the marker — a
250
+ * note that writes its condition unbolded gets the blunt reading, which is the
251
+ * conservative direction.
252
+ */
253
+ function conditionClause(condition: string): string {
254
+ const re = /\*\*([^*]+)\*\*/g;
255
+ let m: RegExpExecArray | null;
256
+ while ((m = re.exec(condition)) !== null) {
257
+ const span = m[1] as string;
258
+ if (RESUME_LINE_RE.test(span)) {
259
+ // The bolded span sometimes stops before the payload
260
+ // (`**Resume when** that pruning lands`), so take the span plus the
261
+ // remainder of its sentence — up to an em dash or a full stop.
262
+ const after = condition.slice(m.index + m[0].length);
263
+ const tail = /^[^—–.]*/.exec(after)?.[0] ?? '';
264
+ return `${span}${tail}`;
265
+ }
266
+ }
267
+ return condition;
268
+ }
269
+
270
+ /** Roadmap slugs named by the condition, minus the parked file's own slug. */
271
+ function referencedRoadmaps(condition: string, ownSlug: string): string[] {
272
+ const out = new Set<string>();
273
+ ROADMAP_REF_RE.lastIndex = 0;
274
+ let m: RegExpExecArray | null;
275
+ while ((m = ROADMAP_REF_RE.exec(condition)) !== null) {
276
+ const slug = (m[0] as string).replace(/\.md$/, '');
277
+ if (slug !== ownSlug) {
278
+ out.add(slug);
279
+ }
280
+ }
281
+ return [...out].sort();
282
+ }
283
+
284
+ /**
285
+ * Is `stepId` (e.g. `2.1`) ticked in `text`? `null` when it is not found.
286
+ *
287
+ * The id must sit in the step's LABEL position — immediately after the
288
+ * checkbox, optionally bolded — not anywhere on the line. Matching it anywhere
289
+ * meant a line like `- [x] **1.4** raise the cap from 2.0 to 2.1` decided the
290
+ * verdict for step 2.1, and because `exec` returns the first match in the file
291
+ * the wrong line won whenever it came first.
292
+ */
293
+ function stepIsDone(text: string, stepId: string): boolean | null {
294
+ const escaped = stepId.replace(/\./g, '\\.');
295
+ const re = new RegExp(
296
+ `^[ \\t]*-[ \\t]*\\[([ xX~-])\\][ \\t]*\\*{0,2}(?:P|Phase[ \\t]+)?${escaped}\\b`,
297
+ 'm',
298
+ );
299
+ const m = re.exec(text);
300
+ if (!m) {
301
+ return null;
302
+ }
303
+ return (m[1] as string).toLowerCase() === 'x';
304
+ }
305
+
306
+ /**
307
+ * Probe every park note under `later/`.
308
+ *
309
+ * Pure over the filesystem it is handed, so the tests point it at a fixture
310
+ * tree rather than the live estate — a probe whose only test is the live tree
311
+ * stops testing anything the day the live tree changes.
312
+ */
313
+ function probeLater(roadmapRoot: string): ResumeFinding[] {
314
+ const laterDir = path.join(roadmapRoot, 'later');
315
+ let entries: string[];
316
+ try {
317
+ entries = fs
318
+ .readdirSync(laterDir, { withFileTypes: true })
319
+ .filter((e) => e.isFile() && e.name.endsWith('.md') && e.name !== 'README.md')
320
+ .map((e) => e.name)
321
+ .sort();
322
+ } catch {
323
+ return [];
324
+ }
325
+
326
+ const findings: ResumeFinding[] = [];
327
+ for (const name of entries) {
328
+ const text = _read(path.join(laterDir, name));
329
+ const ownSlug = name.replace(/\.md$/, '');
330
+ const condition = extractCondition(text);
331
+ if (condition === '') {
332
+ findings.push({
333
+ file: `later/${name}`,
334
+ condition: '',
335
+ refs: [],
336
+ verdict: 'undecidable',
337
+ why: 'no resume-condition line — lint_roadmap_later_disposition owns that gap',
338
+ });
339
+ continue;
340
+ }
341
+ // Both tests read the CLAUSE, never the surrounding explanation: an
342
+ // "and" or a roadmap name in the prose after the condition is
343
+ // commentary, not a second conjunct and not a dependency.
344
+ const clause = conditionClause(condition);
345
+ if (COMPOUND_RE.test(clause)) {
346
+ findings.push({
347
+ file: `later/${name}`,
348
+ condition,
349
+ refs: referencedRoadmaps(clause, ownSlug),
350
+ verdict: 'undecidable',
351
+ why: 'compound condition — the probe reads the roadmap references and not the rest',
352
+ });
353
+ continue;
354
+ }
355
+ const refs = referencedRoadmaps(clause, ownSlug);
356
+ if (refs.length === 0) {
357
+ findings.push({
358
+ file: `later/${name}`,
359
+ condition,
360
+ refs,
361
+ verdict: 'undecidable',
362
+ why: 'the condition names no roadmap this tree can resolve',
363
+ });
364
+ continue;
365
+ }
366
+
367
+ const stepMatch = STEP_REF_RE.exec(clause);
368
+ const stepId = stepMatch ? (stepMatch[1] as string) : null;
369
+
370
+ const reasons: string[] = [];
371
+ let unmet = false;
372
+ let unresolvable = false;
373
+ for (const slug of refs) {
374
+ const where = roadmapDisposition(roadmapRoot, slug);
375
+ if (where === 'archive') {
376
+ reasons.push(`${slug} archived`);
377
+ continue;
378
+ }
379
+ if (where === 'missing') {
380
+ unresolvable = true;
381
+ reasons.push(`${slug} resolves nowhere`);
382
+ continue;
383
+ }
384
+ if (where === 'active' && stepId !== null) {
385
+ const done = stepIsDone(_read(path.join(roadmapRoot, `${slug}.md`)), stepId);
386
+ if (done === true) {
387
+ reasons.push(`${slug} step ${stepId} is ticked`);
388
+ continue;
389
+ }
390
+ reasons.push(done === null ? `${slug} has no step ${stepId}` : `${slug} step ${stepId} still open`);
391
+ unmet = true;
392
+ continue;
393
+ }
394
+ reasons.push(`${slug} still in ${where}`);
395
+ unmet = true;
396
+ }
397
+
398
+ // Conservative on purpose: a compound condition fires only when every
399
+ // roadmap it names has closed. Resuming on a partial match would put
400
+ // the file back into the active tree still waiting on something.
401
+ const verdict: Verdict = unmet ? 'unmet' : unresolvable ? 'undecidable' : 'fired';
402
+ findings.push({
403
+ file: `later/${name}`,
404
+ condition,
405
+ refs,
406
+ verdict,
407
+ why: reasons.join('; '),
408
+ });
409
+ }
410
+ return findings;
411
+ }
412
+
413
+ export {
414
+ conditionClause,
415
+ extractCondition,
416
+ probeLater,
417
+ referencedRoadmaps,
418
+ roadmapDisposition,
419
+ stepIsDone,
420
+ RESUME_LINE_RE,
421
+ };
422
+ export type { Disposition, ResumeFinding, Verdict };
@@ -47,6 +47,15 @@
47
47
  * ./agent-config gates --json # machine-readable
48
48
  * ./agent-config gates --reply # reply-close form; empty when none
49
49
  * ./agent-config gates --pending # staged actions awaiting confirmation
50
+ * ./agent-config gates --execute <id> # echo what would run
51
+ * ./agent-config gates --execute <id> --confirm # run it and record the evidence
52
+ *
53
+ * `--execute` is the only mode that writes anything, and it takes exactly one
54
+ * blocker id. **Without `--confirm` it runs nothing** — it prints the command
55
+ * the entry authored so the operator sees the exact string first. Everything
56
+ * else here reads. See `gate_execute.ts` for what it refuses to do — no sweep,
57
+ * no resolve on a failed command, no invented budget ledger, no Hard-Floor
58
+ * command even with `--confirm`, and class 3 unchanged.
50
59
  *
51
60
  * `--pending` reads a different source — the staged-confirmation store, not the
52
61
  * roadmap tree — and stays out of `--reply` on purpose. ADR-222 fixes the
@@ -63,8 +72,11 @@ import { spawnSync } from 'node:child_process';
63
72
  import {
64
73
  collect,
65
74
  blocker_needs_user as needsUser,
75
+ blocker_class as blockerClass,
66
76
  type Blocker,
67
77
  } from './update_roadmap_progress.js';
78
+ import { probeLater, type ResumeFinding } from './resume_probe.js';
79
+ import { execute } from './gate_execute.js';
68
80
  import { listPending } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation_store.js';
69
81
  import type { StagedAction } from '../templates/scripts/work_engine/hooks/builtin/staged_confirmation.js';
70
82
 
@@ -227,7 +239,54 @@ function renderEntry(e: Entry, index: number): string[] {
227
239
  return out;
228
240
  }
229
241
 
230
- function render(entries: readonly Entry[], all: boolean): string {
242
+ /**
243
+ * The FIRED section — parked roadmaps whose resume condition has come true.
244
+ *
245
+ * Rendered after the blockers because it is a different kind of item: a
246
+ * blocker is a gate nobody has opened, this is a gate that opened with nobody
247
+ * standing in front of it. A tree with no `later/` notes at all renders
248
+ * nothing; a tree that has them but none fired still prints the coverage line
249
+ * below, deliberately — see the next paragraph.
250
+ *
251
+ * The undecidable count is printed even when zero fired, and that is the
252
+ * point: "no resume condition has fired" and "the probe could read 12 of 44
253
+ * conditions" are different statements, and only the second one is honest
254
+ * about its own coverage.
255
+ */
256
+ function renderResumed(findings: readonly ResumeFinding[]): string[] {
257
+ const fired = findings.filter((f) => f.verdict === 'fired');
258
+ const undecidable = findings.filter((f) => f.verdict === 'undecidable').length;
259
+ if (findings.length === 0) {
260
+ return [];
261
+ }
262
+ const out: string[] = [];
263
+ if (fired.length > 0) {
264
+ out.push('');
265
+ const head = `FIRED · ${fired.length} parked roadmap${fired.length !== 1 ? 's' : ''} can resume`;
266
+ out.push(`── ${head} ${'─'.repeat(Math.max(3, WIDTH - head.length - 4))}`);
267
+ for (const f of fired) {
268
+ out.push(...field('Roadmap:', f.file));
269
+ out.push(...field('Condition:', f.condition));
270
+ out.push(...field('Fired because:', f.why));
271
+ }
272
+ out.push('');
273
+ out.push('Resume one with: `git mv agents/roadmaps/later/<file> agents/roadmaps/`');
274
+ out.push('then `agent-config roadmap:progress`.');
275
+ }
276
+ out.push('');
277
+ out.push(
278
+ `Resume-probe coverage: ${findings.length - undecidable} of ${findings.length} ` +
279
+ `park note${findings.length !== 1 ? 's' : ''} carry a machine-decidable condition` +
280
+ (undecidable > 0 ? ` · ${undecidable} undecidable` : ''),
281
+ );
282
+ return out;
283
+ }
284
+
285
+ function render(
286
+ entries: readonly Entry[],
287
+ all: boolean,
288
+ resumed: readonly ResumeFinding[] = [],
289
+ ): string {
231
290
  const mine = entries.filter((e) => needsUser(e.blocker.owner));
232
291
  const others = entries.filter((e) => !needsUser(e.blocker.owner));
233
292
  const shown = all ? entries : mine;
@@ -267,6 +326,7 @@ function render(entries: readonly Entry[], all: boolean): string {
267
326
  lines.push(' "guide me through <id>" — one decision, step by step');
268
327
  lines.push('');
269
328
  }
329
+ lines.push(...renderResumed(resumed));
270
330
  return lines.join('\n') + '\n';
271
331
  }
272
332
 
@@ -340,18 +400,36 @@ function renderReply(entries: readonly Entry[]): string {
340
400
  return lines.join('\n') + '\n';
341
401
  }
342
402
 
343
- function renderJson(entries: readonly Entry[], all: boolean): string {
403
+ function renderJson(
404
+ entries: readonly Entry[],
405
+ all: boolean,
406
+ resumed: readonly ResumeFinding[] = [],
407
+ ): string {
344
408
  const pick = all ? entries : entries.filter((e) => needsUser(e.blocker.owner));
345
409
  return (
346
410
  JSON.stringify(
347
411
  {
348
412
  needsYou: entries.filter((e) => needsUser(e.blocker.owner)).length,
349
413
  other: entries.filter((e) => !needsUser(e.blocker.owner)).length,
414
+ resumeFired: resumed.filter((f) => f.verdict === 'fired').length,
415
+ resumeUndecidable: resumed.filter((f) => f.verdict === 'undecidable').length,
416
+ resumed: resumed.filter((f) => f.verdict === 'fired'),
350
417
  blockers: pick.map((e) => ({
351
418
  id: e.blocker.id,
352
419
  roadmap: e.roadmapRel,
353
420
  owner: e.blocker.owner,
354
421
  needsYou: needsUser(e.blocker.owner),
422
+ // Resolved through the same absent-field default every
423
+ // other consumer applies, so a synthesised `legacy` note
424
+ // reads as class 3 rather than as a hole. `class` ships and
425
+ // the sibling `run` does not, and the asymmetry is the
426
+ // roadmap's, not a preference: `road-to-gate-autonomy` step
427
+ // 1.3 names this field as its acceptance condition, while
428
+ // nothing asks for the command here — `--execute` reads it
429
+ // off the entry via `locate()`. Emitting `run` would also
430
+ // need a call on the authored-vs-`commandOf()`-stripped
431
+ // form, which no consumer exists to settle.
432
+ class: blockerClass(e.blocker),
355
433
  blocks: e.blocker.blocks,
356
434
  unblocksSteps: e.openSteps,
357
435
  todo: regroupTodo(e.blocker.todo),
@@ -487,6 +565,39 @@ function main(argv?: readonly string[]): number {
487
565
  const reply = args.includes('--reply');
488
566
  const pending = args.includes('--pending');
489
567
 
568
+ // `--execute <id>` is answered FIRST, and it is the only mode that writes.
569
+ // It used to sit after the `--pending` early return, so `--pending
570
+ // --execute <id>` silently did nothing; and it ignored `--json`, so a
571
+ // JSON-requesting caller got prose. Both combinations now refuse loudly.
572
+ const execIdx = args.indexOf('--execute');
573
+ if (execIdx !== -1) {
574
+ const id = args[execIdx + 1];
575
+ if (id === undefined || id.startsWith('--')) {
576
+ process.stderr.write(
577
+ 'usage: gates --execute <blocker-id> [--confirm]\n' +
578
+ ' Without --confirm the command is echoed and nothing runs.\n',
579
+ );
580
+ return 1;
581
+ }
582
+ if (pending) {
583
+ process.stderr.write('gates: --execute and --pending are different sources; pick one.\n');
584
+ return 1;
585
+ }
586
+ if (json) {
587
+ process.stderr.write('gates --execute has no JSON form; its output is a report.\n');
588
+ return 1;
589
+ }
590
+ const root = _resolveRepoRoot(process.cwd());
591
+ const dir = path.join(root, 'agents', 'roadmaps');
592
+ if (!_isDir(dir)) {
593
+ process.stderr.write('No roadmaps directory — nothing to execute.\n');
594
+ return 1;
595
+ }
596
+ const r = execute(dir, id, new Date(), { confirm: args.includes('--confirm') });
597
+ process.stdout.write(r.report);
598
+ return r.code;
599
+ }
600
+
490
601
  // `--pending` resolves against `agents/runtime/`, not `agents/roadmaps/`,
491
602
  // and is answered BEFORE the roadmaps-directory exit below: a staged action
492
603
  // is independent of whether this project plans in roadmaps at all, so
@@ -508,15 +619,37 @@ function main(argv?: readonly string[]): number {
508
619
  reply
509
620
  ? ''
510
621
  : json
511
- ? '{"needsYou":0,"other":0,"blockers":[]}\n'
622
+ ? // Every key `renderJson` emits, so a consumer reading the
623
+ // resume fields gets 0 rather than `undefined` on the
624
+ // branch where there is no roadmap tree to probe.
625
+ JSON.stringify(
626
+ {
627
+ needsYou: 0,
628
+ other: 0,
629
+ resumeFired: 0,
630
+ resumeUndecidable: 0,
631
+ resumed: [],
632
+ blockers: [],
633
+ },
634
+ null,
635
+ 2,
636
+ ) + '\n'
512
637
  : 'No roadmaps directory — nothing to report.\n',
513
638
  );
514
639
  return 0;
515
640
  }
516
641
 
517
642
  const entries = collectEntries(roadmapRoot);
643
+ // `--reply` deliberately does not carry the probe: ADR-222 fixes that form
644
+ // at exactly ONE decision rendered in full, and a fired resume condition is
645
+ // not a decision the reader owes anybody — it is a file that can move.
646
+ const resumed = reply ? [] : probeLater(roadmapRoot);
518
647
  process.stdout.write(
519
- reply ? renderReply(entries) : json ? renderJson(entries, all) : render(entries, all),
648
+ reply
649
+ ? renderReply(entries)
650
+ : json
651
+ ? renderJson(entries, all, resumed)
652
+ : render(entries, all, resumed),
520
653
  );
521
654
  return 0;
522
655
  }
@@ -553,6 +686,7 @@ export {
553
686
  render,
554
687
  renderJson,
555
688
  renderReply,
689
+ renderResumed,
556
690
  renderPending,
557
691
  renderPendingJson,
558
692
  };