@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.
Files changed (118) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +160 -333
  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 +224 -104
  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 +2018 -1320
  52. package/dist/mcp/registry-manifest.json +3 -3
  53. package/dist/mcp/server.json +2 -2
  54. package/dist/mcp/server.mjs +1652 -892
  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 +2 -2
  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/gh_transient.ts +80 -0
  75. package/src/scripts/_lib/release_material.ts +16 -5
  76. package/src/scripts/_lib/review_independence.ts +111 -0
  77. package/src/scripts/_lib/session_register.ts +14 -0
  78. package/src/scripts/_lib/skill_catalogue.ts +20 -0
  79. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  80. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  81. package/src/scripts/ai_council/_py_json.ts +96 -0
  82. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  83. package/src/scripts/ai_council/clients.ts +80 -143
  84. package/src/scripts/ai_council/config.ts +25 -11
  85. package/src/scripts/ai_council/probe_store.ts +196 -0
  86. package/src/scripts/ai_council/qualification.ts +425 -0
  87. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  88. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  89. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  90. package/src/scripts/capture_skill_catalogue.ts +264 -4
  91. package/src/scripts/check_evaluator_schema.ts +249 -0
  92. package/src/scripts/check_pr_ci_current.ts +409 -0
  93. package/src/scripts/check_review_schema.ts +195 -0
  94. package/src/scripts/check_rule_activation_census.ts +451 -0
  95. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  96. package/src/scripts/condense.ts +1 -1
  97. package/src/scripts/council_cli.ts +141 -191
  98. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  99. package/src/scripts/hook_manifest.yaml +72 -5
  100. package/src/scripts/hooks/concern_registry.ts +4 -0
  101. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  102. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  103. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  104. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  105. package/src/scripts/hooks_doctor.ts +112 -0
  106. package/src/scripts/interruption_report.ts +443 -0
  107. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  108. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  109. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  110. package/src/scripts/release.ts +38 -53
  111. package/src/scripts/rule_activation_census.ts +292 -0
  112. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  113. package/src/scripts/schemas/review-findings.schema.json +31 -0
  114. package/src/scripts/self_review_gate.ts +6 -0
  115. package/src/scripts/session_register_hook.ts +30 -1
  116. package/src/scripts/sync_pr_branch.ts +271 -0
  117. package/src/scripts/test_council_qualification.ts +213 -0
  118. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
@@ -0,0 +1,359 @@
1
+ /**
2
+ * Evidence artifacts declare what kind of evidence they are
3
+ * (road-to-release-review-p0 Phase 2).
4
+ *
5
+ * The contract is `docs/contracts/evidence-artifact-types.md`; this is its
6
+ * check. Read that document first — in particular its scope section, because
7
+ * the single most important property of this linter is what it deliberately
8
+ * does NOT do.
9
+ *
10
+ * **It is forward-looking, and there is no baseline file.** Measured by
11
+ * `--all` at the time of writing: 332 tracked markdown artifacts under
12
+ * `agents/evidence/`, 188 already resolving a type and 144 not.
13
+ * `check_completion_review` already refused to make one marker field required
14
+ * on the stated ground that it "would have been a migration event for the
15
+ * whole evidence corpus", and that reasoning is adopted rather than
16
+ * overturned. So the gate fires only on files ADDED relative to a base ref. A
17
+ * ratchet baseline was the obvious alternative and was rejected: it would mean
18
+ * committing a 144-entry suppression list, and a new baseline file in this
19
+ * repo trips two suppression gates of its own — paying a governance cost to
20
+ * enforce a rule the scope decision says should not be enforced retroactively.
21
+ *
22
+ * **Not registered in `gate-coverage.yml`, and the reason is the manifest's
23
+ * own.** A diff-scoped gate legitimately scans 0 on most runs, so it can carry
24
+ * no honest `min_scanned` floor, and `min_scanned: 0` is precisely the
25
+ * false-count shape that file rejects. `check_rule_projection_integrity` is
26
+ * the worked precedent: NOT registered, with the reason, ledger half
27
+ * discharged. This gate follows it — it adopts `_lib/gate_ledger.ts` below, so
28
+ * `check_gate_completeness` counts it as an adopter rather than a gap.
29
+ *
30
+ * **A declaration is read wherever the author already wrote one.** Three of
31
+ * the five types are pre-existing line grammars owned by
32
+ * `check_completion_review.ts`, and this linter imports those parsers rather
33
+ * than re-implementing them. Re-declaring an artifact that already carries
34
+ * `<!-- completion-review: v1 … -->` under a second `evidence-type:` marker
35
+ * would be the ambiguity the contract exists to remove.
36
+ *
37
+ * Modes:
38
+ * --new-only <base-ref> gate: fail on an ADDED artifact with no type
39
+ * --all report: corpus census, always exit 0
40
+ * --quiet verdict line only
41
+ *
42
+ * Exit 0 = clean (or report mode). Exit 1 = an added artifact carries no
43
+ * resolvable type. Exit 2 = the linter could not run (bad ref, no git).
44
+ */
45
+
46
+ import { execFileSync } from 'node:child_process';
47
+ import fs from 'node:fs';
48
+ import path from 'node:path';
49
+ import { fileURLToPath, pathToFileURL } from 'node:url';
50
+
51
+ import { GateLedger } from './_lib/gate_ledger.js';
52
+ import { resolveBaseRef } from './_lib/ratchet_base_ref.js';
53
+ import { reportScanned } from './_lib/scan_scope.js';
54
+ import { parseHonestNull, parseMarkerLine, parseSkipDeclaration } from './check_completion_review.js';
55
+
56
+ export const EVIDENCE_ROOT = path.join('agents', 'evidence');
57
+
58
+ /** The five types the contract defines. Anything else is an error, not a warning. */
59
+ export const EVIDENCE_TYPES = [
60
+ 'original-review',
61
+ 'current-binding',
62
+ 'declared-skip',
63
+ 'honest-null',
64
+ 'analysis',
65
+ ] as const;
66
+
67
+ export type EvidenceType = (typeof EVIDENCE_TYPES)[number];
68
+
69
+ /** The explicit marker, for the two types with no pre-existing grammar. */
70
+ const TYPE_MARKER_RE = /^<!--\s*evidence-type:\s*([a-z-]+)\s*-->$/;
71
+
72
+ /** How many leading lines are scanned for the marker. A type belongs at the top. */
73
+ const MARKER_SCAN_LINES = 40;
74
+
75
+ export interface TypeResolution {
76
+ readonly type: EvidenceType | null;
77
+ /** Which declaration produced it — named so a reader can check the claim. */
78
+ readonly via: 'marker' | 'completion-review' | 'honest-null' | 'skip' | 'review-input-path' | null;
79
+ /** Set when a marker was present but named something outside the set. */
80
+ readonly invalidMarker: string | null;
81
+ }
82
+
83
+ /**
84
+ * Resolve one artifact's declared type.
85
+ *
86
+ * Order matters and encodes the contract: the pre-existing grammars are
87
+ * checked FIRST, so an artifact that already declares itself through
88
+ * `check_completion_review`'s vocabulary is never asked for a second marker.
89
+ * The path rule is last and is the contract's one acknowledged exception.
90
+ */
91
+ export function resolveEvidenceType(relPath: string, contents: string): TypeResolution {
92
+ // BOTH scans share one window. They did not: the grammar scan walked the
93
+ // whole file while the marker scan stopped at 40 lines, so a quoted
94
+ // `completion-review` line deep in a prose artifact silently overrode an
95
+ // explicit `evidence-type:` marker its author had written at the top
96
+ // (R2 finding 8). A declaration belongs near the top of the file either
97
+ // way, and one window makes the precedence readable instead of positional.
98
+ const lines = contents.split('\n').slice(0, MARKER_SCAN_LINES);
99
+
100
+ for (const raw of lines) {
101
+ const line = raw.trim();
102
+ if (line === '') {
103
+ continue;
104
+ }
105
+ if (parseMarkerLine(line) !== null) {
106
+ return { type: 'current-binding', via: 'completion-review', invalidMarker: null };
107
+ }
108
+ if (parseHonestNull(line) !== null) {
109
+ return { type: 'honest-null', via: 'honest-null', invalidMarker: null };
110
+ }
111
+ if (parseSkipDeclaration(line) !== null) {
112
+ return { type: 'declared-skip', via: 'skip', invalidMarker: null };
113
+ }
114
+ }
115
+
116
+ for (const raw of lines) {
117
+ const m = TYPE_MARKER_RE.exec(raw.trim());
118
+ if (m === null) {
119
+ continue;
120
+ }
121
+ const value = m[1] as string;
122
+ if ((EVIDENCE_TYPES as readonly string[]).includes(value)) {
123
+ return { type: value as EvidenceType, via: 'marker', invalidMarker: null };
124
+ }
125
+ // A misspelled type reads as untyped to every consumer, which is the
126
+ // state the contract exists to end — so it is louder than absence,
127
+ // not quieter.
128
+ return { type: null, via: null, invalidMarker: value };
129
+ }
130
+
131
+ // The contract's single filename-derived case. Safe because the directory
132
+ // is written by `dispatch_r2_reviewer.ts` and by nothing else, so the path
133
+ // IS the declaration rather than a heuristic about one.
134
+ // Split on `/`, never `path.sep`. Every caller feeds paths from
135
+ // `git diff --name-only` / `git ls-files`, and git emits `/`-separated
136
+ // paths on every platform — so on Windows `path.sep` (`\`) made the whole
137
+ // path one segment and this branch could never match (R2 finding 10).
138
+ // `\` is accepted too so a hand-built Windows path still resolves.
139
+ if (relPath.split(/[/\\]/).some((seg) => seg.endsWith('.review-input'))) {
140
+ return { type: 'original-review', via: 'review-input-path', invalidMarker: null };
141
+ }
142
+
143
+ return { type: null, via: null, invalidMarker: null };
144
+ }
145
+
146
+ function _git(root: string, args: readonly string[]): string {
147
+ return execFileSync('git', [...args], { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
148
+ }
149
+
150
+ /** Markdown files ADDED under `agents/evidence/` relative to `baseRef`. */
151
+ export function addedEvidenceFiles(root: string, baseRef: string): string[] {
152
+ const out = _git(root, [
153
+ 'diff',
154
+ '--name-only',
155
+ '--diff-filter=A',
156
+ `${baseRef}...HEAD`,
157
+ '--',
158
+ `${EVIDENCE_ROOT}/`,
159
+ ]);
160
+ return out
161
+ .split('\n')
162
+ .map((s) => s.trim())
163
+ .filter((s) => s !== '' && s.endsWith('.md'));
164
+ }
165
+
166
+ /** Every markdown artifact in the corpus, for the census mode. */
167
+ export function allEvidenceFiles(root: string): string[] {
168
+ const out = _git(root, ['ls-files', '--', `${EVIDENCE_ROOT}/`]);
169
+ return out
170
+ .split('\n')
171
+ .map((s) => s.trim())
172
+ .filter((s) => s !== '' && s.endsWith('.md'));
173
+ }
174
+
175
+ export interface Finding {
176
+ readonly file: string;
177
+ readonly reason: string;
178
+ }
179
+
180
+ export function checkFiles(root: string, files: readonly string[], ledger?: GateLedger): Finding[] {
181
+ const findings: Finding[] = [];
182
+ ledger?.plan([...files]);
183
+ for (const rel of files) {
184
+ let contents: string;
185
+ try {
186
+ contents = fs.readFileSync(path.join(root, rel), 'utf8');
187
+ } catch {
188
+ // A file listed by git but unreadable on disk is not this
189
+ // linter's finding to report — a deleted-then-re-added path in a
190
+ // dirty tree hits this and is somebody else's gate.
191
+ ledger?.outOfScope(rel, 'no_applicable_files');
192
+ continue;
193
+ }
194
+ const res = resolveEvidenceType(rel, contents);
195
+ if (res.invalidMarker !== null) {
196
+ ledger?.fail(rel, `invalid evidence-type: ${res.invalidMarker}`);
197
+ findings.push({
198
+ file: rel,
199
+ reason:
200
+ `evidence-type: ${res.invalidMarker} is not one of ` +
201
+ `${EVIDENCE_TYPES.join(' | ')}`,
202
+ });
203
+ continue;
204
+ }
205
+ if (res.type === null) {
206
+ ledger?.fail(rel, 'no evidence type declared');
207
+ findings.push({
208
+ file: rel,
209
+ reason:
210
+ 'no evidence type declared — add `<!-- evidence-type: analysis -->` ' +
211
+ '(or the type that fits) per docs/contracts/evidence-artifact-types.md',
212
+ });
213
+ continue;
214
+ }
215
+ ledger?.complete(rel);
216
+ }
217
+ return findings;
218
+ }
219
+
220
+ function _argValue(flag: string): string | null {
221
+ const i = process.argv.indexOf(flag);
222
+ if (i === -1) {
223
+ return null;
224
+ }
225
+ const v = process.argv[i + 1];
226
+ return v === undefined || v.startsWith('--') ? null : v;
227
+ }
228
+
229
+ export function main(): number {
230
+ const quiet = process.argv.includes('--quiet');
231
+ const root = process.cwd();
232
+ const out = (s: string): void => {
233
+ if (!quiet) {
234
+ process.stdout.write(`${s}\n`);
235
+ }
236
+ };
237
+
238
+ if (process.argv.includes('--all')) {
239
+ let files: string[];
240
+ try {
241
+ files = allEvidenceFiles(root);
242
+ } catch (exc) {
243
+ process.stdout.write(`lint_evidence_artifacts: cannot list ${EVIDENCE_ROOT} — ${String(exc)}\n`);
244
+ return 2;
245
+ }
246
+ const ledger = new GateLedger('lint_evidence_artifacts');
247
+ const findings = checkFiles(root, files, ledger);
248
+ ledger.report();
249
+ // Census mode reads the whole corpus, so a zero here IS blindness —
250
+ // no `allowEmpty`, deliberately. The gate mode below is the opposite
251
+ // case and says so on its own line.
252
+ reportScanned({
253
+ gate: 'lint_evidence_artifacts --all',
254
+ scanned: files.length,
255
+ units: 'evidence artifacts',
256
+ roots: [EVIDENCE_ROOT],
257
+ });
258
+ const typed = files.length - findings.length;
259
+ out(`lint_evidence_artifacts — census over ${String(files.length)} artifact(s)`);
260
+ out(` typed ${String(typed)}`);
261
+ out(` untyped ${String(findings.length)}`);
262
+ // Report mode, never a gate: the pre-existing untyped corpus is a
263
+ // deliberate scope exclusion, so publishing the number is the point
264
+ // and failing on it would contradict the contract.
265
+ process.stdout.write(
266
+ `lint_evidence_artifacts: census — ${String(typed)} typed, ${String(findings.length)} untyped (report only)\n`,
267
+ );
268
+ return 0;
269
+ }
270
+
271
+ // A bare invocation IS the gate, resolving its own base. Requiring an
272
+ // explicit ref would have made the roadmap's own `verify:` annotation exit
273
+ // 2 on a usage error, and a check whose default invocation is a usage
274
+ // error is a check nobody runs. `--new-only <ref>` stays as the override.
275
+ //
276
+ // `resolveBaseRef` rather than a hardcoded `origin/main`: `actions/checkout`
277
+ // performs a shallow PR-merge fetch, so a PR build frequently has no
278
+ // `origin/main` remote-tracking ref at all. That ladder already handles it
279
+ // and is the repo's one answer to this question.
280
+ const baseRef = _argValue('--new-only') ?? resolveBaseRef(root);
281
+ if (baseRef === null) {
282
+ // Fail rather than compare against an assumed-empty base — the same
283
+ // default `resolveBaseRef`'s own docstring names. An unresolvable base
284
+ // would otherwise make every artifact look un-added and the gate would
285
+ // pass by scanning nothing.
286
+ process.stdout.write(
287
+ 'lint_evidence_artifacts: no base ref resolved — pass --new-only <ref>, ' +
288
+ 'set RATCHET_BASE_REF, or run --all for the census\n',
289
+ );
290
+ return 2;
291
+ }
292
+
293
+ let files: string[];
294
+ try {
295
+ files = addedEvidenceFiles(root, baseRef);
296
+ } catch (exc) {
297
+ process.stdout.write(`lint_evidence_artifacts: cannot diff against ${baseRef} — ${String(exc)}\n`);
298
+ return 2;
299
+ }
300
+
301
+ const ledger = new GateLedger('lint_evidence_artifacts');
302
+ const findings = checkFiles(root, files, ledger);
303
+ ledger.report();
304
+ // A run that scans nothing exits green, and SAYS SO — silent green over an
305
+ // empty set is how a gate stops measuring anything without anyone
306
+ // noticing. The `allowEmpty` reason is `EMPTY_VALID` rather than
307
+ // `OPTIONAL_INPUT`: this gate measures a DIFF, not a corpus, so "no
308
+ // evidence artifact was added on this branch" genuinely is the success
309
+ // state and stays true even if `agents/evidence/` were deleted entirely.
310
+ reportScanned({
311
+ gate: 'lint_evidence_artifacts',
312
+ scanned: files.length,
313
+ units: 'added evidence artifacts',
314
+ roots: [EVIDENCE_ROOT],
315
+ allowEmpty:
316
+ 'EMPTY_VALID: the scope is the set of artifacts ADDED since the base ref; ' +
317
+ 'a branch that adds none has nothing to type, and zero is the pass.',
318
+ });
319
+ if (files.length === 0) {
320
+ process.stdout.write(
321
+ `lint_evidence_artifacts: ✅ no evidence artifact added since ${baseRef} — nothing to check\n`,
322
+ );
323
+ return 0;
324
+ }
325
+ out(`lint_evidence_artifacts — ${String(files.length)} added artifact(s) since ${baseRef}`);
326
+ if (findings.length === 0) {
327
+ process.stdout.write(
328
+ `lint_evidence_artifacts: ✅ all ${String(files.length)} added artifact(s) declare a type\n`,
329
+ );
330
+ return 0;
331
+ }
332
+ process.stdout.write(`lint_evidence_artifacts: ❌ ${String(findings.length)} untyped artifact(s)\n`);
333
+ for (const f of findings) {
334
+ process.stdout.write(` ${f.file}\n ${f.reason}\n`);
335
+ }
336
+ return 1;
337
+ }
338
+
339
+ const _HERE = fileURLToPath(import.meta.url);
340
+ function _isCliEntry(): boolean {
341
+ if (process.argv[1] === undefined) {
342
+ return false;
343
+ }
344
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
345
+ if (import.meta.url === argvUrl) {
346
+ return true;
347
+ }
348
+ try {
349
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
350
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
351
+ return here === argv;
352
+ } catch {
353
+ return false;
354
+ }
355
+ }
356
+
357
+ if (_isCliEntry() || process.argv[1] === _HERE) {
358
+ process.exit(main());
359
+ }
@@ -9,6 +9,11 @@
9
9
  * (Status, Owner, Blocks, What to do, Resolved when).
10
10
  * 2. Every inline `<!-- blocked-by: <id> -->` annotation resolves to a
11
11
  * `### blocker: <id>` entry declared in the SAME file.
12
+ * 3. An entry that declares `- **Class:**` declares a class the taxonomy
13
+ * knows (0-3), and a class-0 or class-1 entry says HOW it runs
14
+ * (`- **Run:**`). A gate that claims to be executable without naming the
15
+ * command is the same defect as a decision with no option set: it reads
16
+ * as actionable and is not.
12
17
  *
13
18
  * Fenced code blocks are stripped before scanning so a roadmap that shows
14
19
  * the `## Blockers` shape as a documentation example is not flagged.
@@ -63,6 +68,36 @@ const DECIDABILITY_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
63
68
  ['If you do nothing', /^-[ \t]*\*\*If you do nothing:\*\*[ \t]*\S/im],
64
69
  ];
65
70
 
71
+ /**
72
+ * The gate taxonomy's four classes, as authored in the blocker entry.
73
+ *
74
+ * `Class` is deliberately OPTIONAL and its absence means class 3 — human-only.
75
+ * Making the safe class the default is what keeps a misclassification a
76
+ * reviewed edit rather than a runtime judgment: nothing an author forgets to
77
+ * write can become executable by omission.
78
+ */
79
+ const CLASS_FIELD_RE = /^-[ \t]*\*\*Class:\*\*[ \t]*(.*)$/im;
80
+ const RUN_FIELD_RE = /^-[ \t]*\*\*Run:\*\*[ \t]*(\S.*)$/im;
81
+ const KNOWN_CLASSES: ReadonlySet<string> = new Set(['0', '1', '2', '3']);
82
+ /** The classes whose whole claim is that an agent can execute them. */
83
+ const RUNNABLE_CLASSES: ReadonlySet<string> = new Set(['0', '1']);
84
+
85
+ /**
86
+ * The authored class, or `''` when the entry declares none.
87
+ *
88
+ * Only the leading token is read, so `- **Class:** 1 — budget-preauthorized`
89
+ * parses as `1`: the taxonomy name is documentation for the reader and must
90
+ * not have to be spelled identically in every entry to stay valid.
91
+ */
92
+ function _blockerClass(body: string): string {
93
+ const m = CLASS_FIELD_RE.exec(body);
94
+ if (!m) {
95
+ return '';
96
+ }
97
+ const raw = ((m[1] as string) ?? '').trim();
98
+ return raw === '' ? '' : (raw.split(/[\s—–-]/)[0] as string);
99
+ }
100
+
66
101
  /** `What to do:` body — everything up to the next field marker. */
67
102
  const WHAT_TO_DO_SLICE_RE =
68
103
  /^-[ \t]*\*\*What to do:\*\*([\s\S]*?)(?=^-[ \t]*\*\*[A-Z]|^###[ \t]|$(?![\s\S]))/im;
@@ -157,6 +192,33 @@ function _scanBoth(rawText: string): ScanResult {
157
192
  // made would be churn, and the ratchet would never reach zero.
158
193
  const isResolved = /^-[ \t]*\*\*Status:\*\*[ \t]*resolved/im.test(body);
159
194
  if (!isResolved) {
195
+ // The class/run contract is HARD rather than ratcheted, and it
196
+ // can be: `Class:` is a new opt-in field, so on the day it
197
+ // ships no entry in the tree declares one and the rule fires on
198
+ // nothing. There is no backlog to grandfather — the "strict
199
+ // gate reds ~283 files" failure the decidability half had to
200
+ // avoid simply does not arise here.
201
+ const cls = _blockerClass(body);
202
+ if (cls !== '') {
203
+ const at = _lineAt(text, sectionStart + cur.start);
204
+ if (!KNOWN_CLASSES.has(cls)) {
205
+ violations.push({
206
+ line: at,
207
+ message:
208
+ `blocker '${cur.id}' declares unknown class '${cls}' ` +
209
+ '(expected 0 auto-run, 1 budget-preauthorized, ' +
210
+ '2 consent-once, or 3 human-only)',
211
+ });
212
+ } else if (RUNNABLE_CLASSES.has(cls) && !RUN_FIELD_RE.test(body)) {
213
+ violations.push({
214
+ line: at,
215
+ message:
216
+ `blocker '${cur.id}' is class ${cls} but declares no ` +
217
+ '**Run:** command — a gate that claims to be runnable ' +
218
+ 'must say how',
219
+ });
220
+ }
221
+ }
160
222
  const gaps = DECIDABILITY_FIELDS.filter(([, re]) => !re.test(body)).map(
161
223
  ([name]) => name,
162
224
  );
@@ -358,6 +420,9 @@ export {
358
420
  ROADMAP_GLOB,
359
421
  REQUIRED_FIELDS,
360
422
  DECIDABILITY_FIELDS,
423
+ KNOWN_CLASSES,
424
+ RUNNABLE_CLASSES,
425
+ _blockerClass,
361
426
  _hasExecutableSubstance,
362
427
  _scan,
363
428
  _scanBoth,
@@ -445,7 +445,13 @@ function _check_external_population_gates(text: string, warnings: string[]): voi
445
445
 
446
446
  function _check_execution_mode(fm: string, problems: string[]): void {
447
447
  if (!fm || !/^execution:/m.test(fm)) {
448
- return; // absent field = interactive default — valid
448
+ // Absent is VALID and stays valid — but it no longer means "interactive".
449
+ // Since road-to-user-out-of-the-loop Phase 1, an absent field is DERIVED
450
+ // by the `roadmap-process-loop § 3a` ladder from the invocation form, so
451
+ // the old comment here named a default that no longer exists. Nothing to
452
+ // check either way: this linter validates a declared value, and there is
453
+ // no declaration to validate.
454
+ return;
449
455
  }
450
456
  const m = EXEC_MODE_PAT.exec(fm);
451
457
  if (m === null) {
@@ -124,7 +124,9 @@ import {
124
124
  derive_category_hits,
125
125
  render_derived_head_values,
126
126
  } from './_lib/release_highlights.js';
127
+ import { gh_argv_label, gh_retry } from './_lib/gh_transient.js';
127
128
  import {
129
+ NEXT_SECTION_RE,
128
130
  extract_changelog_section,
129
131
  pr_body_from_section,
130
132
  release_notes_from_section,
@@ -616,6 +618,31 @@ function git(args: readonly string[], opts: { capture?: boolean } = {}): string
616
618
  return capture ? r.stdout.trim() : '';
617
619
  }
618
620
 
621
+ function _sleep_ms(ms: number): void {
622
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
623
+ }
624
+
625
+ /**
626
+ * `gh` with transient-failure retry and a readable death. Always captures, so
627
+ * the classifier can see stderr; `check: false` keeps the caller's fallback.
628
+ */
629
+ function gh(args: readonly string[], opts: { check?: boolean } = {}): RunResult {
630
+ const check = opts.check ?? true;
631
+ const r = gh_retry(args, (argv) => run(argv, { check: false, capture: true }), {
632
+ sleep: (ms) => (_exec_override ? undefined : _sleep_ms(ms)),
633
+ notify: (m) => process.stdout.write(`${m}\n`),
634
+ });
635
+ if (check && r.returncode !== 0) {
636
+ die(`${gh_argv_label(args)} failed (${r.returncode}):\n${(r.stderr || r.stdout).trim()}`);
637
+ }
638
+ // Mutating calls used to inherit stdio — echo, or the PR / release URL
639
+ // vanishes. Probes (`check: false`) stay quiet; their stdout is JSON.
640
+ if (check && r.stdout.trim()) {
641
+ process.stdout.write(`${r.stdout.trim()}\n`);
642
+ }
643
+ return r;
644
+ }
645
+
619
646
  /**
620
647
  * One name per failing check, from `gh pr checks --json name,bucket` output.
621
648
  *
@@ -923,13 +950,9 @@ function push_release_branch(branch: string): void {
923
950
 
924
951
  /** mergeStateStatus of the branch's PR ('' when the probe fails). */
925
952
  function _pr_merge_state(branch: string): string {
926
- const r = run(
927
- ['gh', 'pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'],
928
- {
929
- check: false,
930
- capture: true,
931
- },
932
- );
953
+ const r = gh(['pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'], {
954
+ check: false,
955
+ });
933
956
  return r.returncode === 0 ? r.stdout.trim() : '';
934
957
  }
935
958
 
@@ -973,14 +996,11 @@ function _refresh_pr_body_from_head(branch: string, target: string): void {
973
996
  }
974
997
  const capped = _cap_body(section!.body, GH_PR_BODY_LIMIT - 200, '`CHANGELOG.md` in this PR');
975
998
  const body = pr_body_from_section(capped, target);
976
- const live = run(['gh', 'pr', 'view', branch, '--json', 'body', '-q', '.body'], {
977
- check: false,
978
- capture: true,
979
- });
999
+ const live = gh(['pr', 'view', branch, '--json', 'body', '-q', '.body'], { check: false });
980
1000
  if (live.returncode === 0 && live.stdout.replace(/\r\n/gu, '\n').trim() === body.trim()) {
981
1001
  return;
982
1002
  }
983
- run(['gh', 'pr', 'edit', branch, '--body', body]);
1003
+ gh(['pr', 'edit', branch, '--body', body]);
984
1004
  }
985
1005
 
986
1006
  function merge_release_pr(branch: string, wait_for_checks: boolean): void {
@@ -1147,22 +1167,8 @@ function _push_tag(tag: string): void {
1147
1167
 
1148
1168
  /** Most recent PR (any state) with `release/X.Y.Z` as head, or null. */
1149
1169
  function _pr_for_branch(branch: string): Record<string, unknown> | null {
1150
- const r = run(
1151
- [
1152
- 'gh',
1153
- 'pr',
1154
- 'list',
1155
- '--head',
1156
- branch,
1157
- '--state',
1158
- 'all',
1159
- '--json',
1160
- 'number,state,url',
1161
- '--limit',
1162
- '1',
1163
- ],
1164
- { check: false, capture: true },
1165
- );
1170
+ const argv = ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'number,state,url'];
1171
+ const r = gh([...argv, '--limit', '1'], { check: false });
1166
1172
  if (r.returncode !== 0) {
1167
1173
  return null;
1168
1174
  }
@@ -1476,8 +1482,7 @@ function _previous_test_count_from_changelog(prev_tag: string | null): number |
1476
1482
  }
1477
1483
  const headEnd = m.index + m[0].length;
1478
1484
  const rest = text.slice(headEnd);
1479
- const next_re = /^##\s+\[?\d+\.\d+\.\d+/m;
1480
- const next_heading = next_re.exec(rest);
1485
+ const next_heading = NEXT_SECTION_RE.exec(rest);
1481
1486
  const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
1482
1487
  const section = text.slice(headEnd, sectionEnd);
1483
1488
  const count_match = _TEST_COUNT_LINE_RE.exec(section);
@@ -2141,19 +2146,8 @@ function execute(
2141
2146
  '`CHANGELOG.md` in this PR',
2142
2147
  );
2143
2148
  const pr_body = pr_body_from_section(pr_changelog, plan.target);
2144
- run([
2145
- 'gh',
2146
- 'pr',
2147
- 'create',
2148
- '--base',
2149
- MAIN_BRANCH,
2150
- '--head',
2151
- branch,
2152
- '--title',
2153
- `release: ${plan.target}`,
2154
- '--body',
2155
- pr_body,
2156
- ]);
2149
+ gh(['pr', 'create', '--base', MAIN_BRANCH, '--head', branch,
2150
+ '--title', `release: ${plan.target}`, '--body', pr_body]);
2157
2151
  }
2158
2152
 
2159
2153
  // ─── 6. wait for checks ─────────────────────────────────────────────────
@@ -2236,16 +2230,7 @@ function execute(
2236
2230
  GH_RELEASE_NOTES_LIMIT,
2237
2231
  '`CHANGELOG.md`',
2238
2232
  );
2239
- run([
2240
- 'gh',
2241
- 'release',
2242
- 'create',
2243
- plan.target,
2244
- '--title',
2245
- plan.target,
2246
- '--notes',
2247
- notes,
2248
- ]);
2233
+ gh(['release', 'create', plan.target, '--title', plan.target, '--notes', notes]);
2249
2234
 
2250
2235
  // ─── 9b. dispatch-chain the tag-triggered workflows (--ci only) ──────
2251
2236
  // release-guard.yml, publish-npm.yml, and cloud-release.yml all