@event4u/agent-config 13.0.0 → 14.0.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +160 -333
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/roadmap.json +8 -0
- package/dist/agent-src/commands/fix/ci.md +39 -1
- package/dist/agent-src/commands/pr/create.md +65 -4
- package/dist/agent-src/commands/roadmap/next.md +28 -5
- package/dist/agent-src/commands/work.md +16 -0
- package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
- package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
- package/dist/agent-src/rules/design-review-after-ui-write.md +25 -8
- package/dist/agent-src/rules/ui-audit-gate.md +25 -8
- package/dist/agent-src/scripts/gate_execute.ts +395 -0
- package/dist/agent-src/scripts/resume_probe.ts +422 -0
- package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
- package/dist/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
- package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
- package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
- package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
- package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
- package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
- package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
- package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +43 -3
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-N7FEST6D.js → chunk-7PZ4FSZB.js} +351 -174
- package/dist/cli-delegate/{chunk-3B72LSZL.js → chunk-F3YUF7SA.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +224 -104
- package/dist/cli-delegate/cmd_doctor.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +40 -18
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +6 -5
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +5 -4
- package/dist/hooks/dispatch.js +2018 -1320
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1652 -892
- package/dist/router.json +1 -1
- package/dist/scripts/condense.js +1 -1
- package/dist/scripts/condense.js.map +1 -1
- package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
- package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/gate_execute.ts +395 -0
- package/src/agent-src/scripts/resume_probe.ts +422 -0
- package/src/agent-src/scripts/roadmap_gates.ts +138 -4
- package/src/agent-src/scripts/update_roadmap_progress.ts +53 -2
- package/src/config/ci-local-parity.yml +13 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +111 -5
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/config/rule-activation-census.json +41 -0
- package/src/config/rule-stub-ceilings.json +52 -0
- package/src/scripts/_dispatch.bash +4 -0
- package/src/scripts/_lib/evaluator_contract.ts +159 -0
- package/src/scripts/_lib/gh_transient.ts +80 -0
- package/src/scripts/_lib/release_material.ts +16 -5
- package/src/scripts/_lib/review_independence.ts +111 -0
- package/src/scripts/_lib/session_register.ts +14 -0
- package/src/scripts/_lib/skill_catalogue.ts +20 -0
- package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
- package/src/scripts/_lib/turn_end_refusals.ts +482 -0
- package/src/scripts/ai_council/_py_json.ts +96 -0
- package/src/scripts/ai_council/cli_call_budget.ts +357 -0
- package/src/scripts/ai_council/clients.ts +80 -143
- package/src/scripts/ai_council/config.ts +25 -11
- package/src/scripts/ai_council/probe_store.ts +196 -0
- package/src/scripts/ai_council/qualification.ts +425 -0
- package/src/scripts/ai_council/qualification_wiring.ts +243 -0
- package/src/scripts/ai_council/quorum_wiring.ts +211 -0
- package/src/scripts/ai_team/team_dispatch.ts +40 -6
- package/src/scripts/capture_skill_catalogue.ts +264 -4
- package/src/scripts/check_evaluator_schema.ts +249 -0
- package/src/scripts/check_pr_ci_current.ts +409 -0
- package/src/scripts/check_review_schema.ts +195 -0
- package/src/scripts/check_rule_activation_census.ts +451 -0
- package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
- package/src/scripts/condense.ts +1 -1
- package/src/scripts/council_cli.ts +141 -191
- package/src/scripts/dispatch_r2_reviewer.ts +8 -1
- package/src/scripts/hook_manifest.yaml +72 -5
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/dispatch_hook.ts +43 -1
- package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
- package/src/scripts/hooks_doctor.ts +112 -0
- package/src/scripts/interruption_report.ts +443 -0
- package/src/scripts/lint_evidence_artifacts.ts +359 -0
- package/src/scripts/lint_roadmap_blockers.ts +65 -0
- package/src/scripts/lint_roadmap_complexity.ts +7 -1
- package/src/scripts/release.ts +38 -53
- package/src/scripts/rule_activation_census.ts +292 -0
- package/src/scripts/schemas/evaluator-output.schema.json +60 -0
- package/src/scripts/schemas/review-findings.schema.json +31 -0
- package/src/scripts/self_review_gate.ts +6 -0
- package/src/scripts/session_register_hook.ts +30 -1
- package/src/scripts/sync_pr_branch.ts +271 -0
- package/src/scripts/test_council_qualification.ts +213 -0
- 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
|
-
|
|
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(
|
|
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
|
-
?
|
|
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
|
|
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
|
};
|