@event4u/agent-config 12.1.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 (185) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +164 -186
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/cost/report.md +23 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +8 -0
  6. package/dist/agent-src/commands/fix/ci.md +39 -1
  7. package/dist/agent-src/commands/pr/create.md +65 -4
  8. package/dist/agent-src/commands/roadmap/next.md +28 -5
  9. package/dist/agent-src/commands/work.md +16 -0
  10. package/dist/agent-src/contexts/contracts/command-suggestion-flow.md +47 -0
  11. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +94 -0
  12. package/dist/agent-src/contexts/execution/plan-confidence-gate.md +30 -0
  13. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +21 -8
  14. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +55 -8
  15. package/dist/agent-src/contexts/execution/subagent-response-contract.md +42 -0
  16. package/dist/agent-src/contexts/execution/subagent-routing.md +148 -33
  17. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +29 -0
  18. package/dist/agent-src/contexts/subagent-configuration.md +69 -7
  19. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  20. package/dist/agent-src/rules/command-suggestion-policy.md +33 -0
  21. package/dist/agent-src/rules/design-fidelity.md +1 -1
  22. package/dist/agent-src/rules/design-review-after-ui-write.md +36 -14
  23. package/dist/agent-src/rules/evaluator-independence.md +27 -5
  24. package/dist/agent-src/rules/git-history-discipline.md +5 -1
  25. package/dist/agent-src/rules/missing-skill-recovery.md +89 -0
  26. package/dist/agent-src/rules/ui-audit-gate.md +34 -12
  27. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  28. package/dist/agent-src/scripts/gate_execute.ts +395 -0
  29. package/dist/agent-src/scripts/resume_probe.ts +422 -0
  30. package/dist/agent-src/scripts/roadmap_gates.ts +138 -4
  31. package/dist/agent-src/scripts/update_roadmap_progress.ts +70 -4
  32. package/dist/agent-src/skills/ai-council/SKILL.md +61 -0
  33. package/dist/agent-src/skills/authz-review/SKILL.md +4 -0
  34. package/dist/agent-src/skills/design-system-capture/SKILL.md +4 -0
  35. package/dist/agent-src/skills/design-system-capture/references/snapshot-preference-order.md +77 -0
  36. package/dist/agent-src/skills/experiment-loop/SKILL.md +116 -0
  37. package/dist/agent-src/skills/experiment-loop/evals/output-schema.yml +27 -0
  38. package/dist/agent-src/skills/experiment-loop/evals/triggers.json +17 -0
  39. package/dist/agent-src/skills/experiment-loop/references/pivot-ladder.md +54 -0
  40. package/dist/agent-src/skills/experiment-loop/references/protocol.md +61 -0
  41. package/dist/agent-src/skills/experiment-loop/references/register-format.md +66 -0
  42. package/dist/agent-src/skills/merge-conflicts/SKILL.md +4 -0
  43. package/dist/agent-src/skills/prompt-optimizer/SKILL.md +17 -1
  44. package/dist/agent-src/skills/roadmap-management/SKILL.md +2 -2
  45. package/dist/agent-src/skills/roadmap-writing/SKILL.md +5 -5
  46. package/dist/agent-src/skills/subagent-orchestration/schemas/team-review-status.json +8 -3
  47. package/dist/agent-src/skills/systematic-debugging/SKILL.md +4 -0
  48. package/dist/agent-src/skills/threat-modeling/SKILL.md +3 -0
  49. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  50. package/dist/agent-src/templates/roadmaps.md +43 -3
  51. package/dist/cli/registry.js +1 -0
  52. package/dist/cli/registry.js.map +1 -1
  53. package/dist/cli-delegate/{chunk-K34QTSD5.js → chunk-7PZ4FSZB.js} +368 -191
  54. package/dist/cli-delegate/{chunk-55WZN5VF.js → chunk-EVTY7ISR.js} +2 -2
  55. package/dist/cli-delegate/{chunk-O52B4F5S.js → chunk-F3YUF7SA.js} +1 -1
  56. package/dist/cli-delegate/{chunk-LKZ5DOYW.js → chunk-XURJ4AQC.js} +11 -3
  57. package/dist/cli-delegate/cmd_conformance.js +224 -104
  58. package/dist/cli-delegate/cmd_doctor.js +2 -2
  59. package/dist/cli-delegate/cmd_export.js +2 -2
  60. package/dist/cli-delegate/cmd_handoff.js +1 -1
  61. package/dist/cli-delegate/cmd_prune.js +2 -2
  62. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  63. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  64. package/dist/cli-delegate/cmd_sync.js +2 -2
  65. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  66. package/dist/cli-delegate/cmd_validate.js +2 -2
  67. package/dist/discovery/deprecation-report.md +1 -1
  68. package/dist/discovery/discovery-manifest.json +73 -30
  69. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  70. package/dist/discovery/discovery-manifest.summary.md +3 -3
  71. package/dist/discovery/dormancy-report.md +1 -1
  72. package/dist/discovery/orphan-report.md +1 -1
  73. package/dist/discovery/packs.json +7 -5
  74. package/dist/discovery/trust-report.md +3 -3
  75. package/dist/discovery/workspaces.json +6 -4
  76. package/dist/hooks/dispatch.js +2653 -1365
  77. package/dist/install/install.mjs +12 -4
  78. package/dist/mcp/registry-manifest.json +3 -3
  79. package/dist/mcp/server.json +2 -2
  80. package/dist/mcp/server.mjs +2679 -887
  81. package/dist/router.json +1 -1
  82. package/dist/scripts/condense.js +1 -1
  83. package/dist/scripts/condense.js.map +1 -1
  84. package/dist/server/schemas/settings.js +2 -2
  85. package/dist/server/schemas/settings.js.map +1 -1
  86. package/dist/ui/assets/{index-B0aMJnuz.js → index-C1pBRm1f.js} +2 -2
  87. package/dist/ui/assets/index-C1pBRm1f.js.map +1 -0
  88. package/dist/ui/index.html +1 -1
  89. package/docs/guidelines/design-fidelity-mechanics.md +21 -0
  90. package/docs/guidelines/design-handover-extraction.md +87 -0
  91. package/docs/guidelines/prompt-templates.md +24 -1
  92. package/package.json +1 -1
  93. package/src/agent-src/scripts/archive_completed_roadmaps.ts +33 -0
  94. package/src/agent-src/scripts/gate_execute.ts +395 -0
  95. package/src/agent-src/scripts/resume_probe.ts +422 -0
  96. package/src/agent-src/scripts/roadmap_gates.ts +138 -4
  97. package/src/agent-src/scripts/update_roadmap_progress.ts +70 -4
  98. package/src/config/ci-local-parity.yml +13 -0
  99. package/src/config/evaluator-budgets.json +9 -8
  100. package/src/config/gate-coverage.yml +140 -12
  101. package/src/config/gate-violation-baselines.json +9 -3
  102. package/src/config/hook-token-budget.json +8 -1
  103. package/src/config/rule-activation-census.json +41 -0
  104. package/src/config/rule-stub-ceilings.json +52 -0
  105. package/src/scripts/_dispatch.bash +4 -0
  106. package/src/scripts/_lib/bench_ab_complexity.ts +439 -0
  107. package/src/scripts/_lib/bench_ab_safety_tier.ts +142 -0
  108. package/src/scripts/_lib/bench_ab_scoring_v2.ts +28 -3
  109. package/src/scripts/_lib/bench_ab_search_adherence.ts +423 -0
  110. package/src/scripts/_lib/bench_ab_size_claim.ts +206 -0
  111. package/src/scripts/_lib/bench_ab_workspace.ts +111 -0
  112. package/src/scripts/_lib/evaluator_contract.ts +159 -0
  113. package/src/scripts/_lib/gh_transient.ts +80 -0
  114. package/src/scripts/_lib/release_material.ts +16 -5
  115. package/src/scripts/_lib/review_independence.ts +111 -0
  116. package/src/scripts/_lib/session_register.ts +14 -0
  117. package/src/scripts/_lib/skill_catalogue.ts +278 -5
  118. package/src/scripts/_lib/skill_catalogue_series.ts +411 -0
  119. package/src/scripts/_lib/tier_budget_routing.ts +31 -331
  120. package/src/scripts/_lib/turn_end_refusals.ts +482 -0
  121. package/src/scripts/ai_council/_py_json.ts +96 -0
  122. package/src/scripts/ai_council/cli_call_budget.ts +357 -0
  123. package/src/scripts/ai_council/clients.ts +101 -164
  124. package/src/scripts/ai_council/config.ts +25 -11
  125. package/src/scripts/ai_council/probe_store.ts +196 -0
  126. package/src/scripts/ai_council/qualification.ts +425 -0
  127. package/src/scripts/ai_council/qualification_wiring.ts +243 -0
  128. package/src/scripts/ai_council/quorum_wiring.ts +211 -0
  129. package/src/scripts/ai_team/team_dispatch.ts +40 -6
  130. package/src/scripts/bench_ab_v2_complexity.ts +252 -0
  131. package/src/scripts/bench_ab_v2_run.ts +3 -42
  132. package/src/scripts/bench_ab_v2_safety.ts +296 -0
  133. package/src/scripts/bench_ab_v2_search.ts +343 -0
  134. package/src/scripts/bench_ab_v2_stats.ts +191 -0
  135. package/src/scripts/build_archive_index.ts +383 -0
  136. package/src/scripts/capture_skill_catalogue.ts +369 -4
  137. package/src/scripts/check_evaluator_schema.ts +249 -0
  138. package/src/scripts/check_pr_ci_current.ts +409 -0
  139. package/src/scripts/check_review_schema.ts +195 -0
  140. package/src/scripts/check_rule_activation_census.ts +451 -0
  141. package/src/scripts/check_rule_stub_ceiling.ts +1128 -0
  142. package/src/scripts/check_source_size_budget.ts +428 -0
  143. package/src/scripts/check_suppression_hygiene.ts +5 -1
  144. package/src/scripts/condense.ts +1 -1
  145. package/src/scripts/cost/backfill_rates.mjs +298 -0
  146. package/src/scripts/cost/budget.mjs +12 -32
  147. package/src/scripts/cost_summary.ts +25 -0
  148. package/src/scripts/council_cli.ts +141 -191
  149. package/src/scripts/dispatch_r2_reviewer.ts +8 -1
  150. package/src/scripts/external_sources_denylist.json +1 -0
  151. package/src/scripts/hook_manifest.yaml +99 -6
  152. package/src/scripts/hooks/concern_registry.ts +6 -0
  153. package/src/scripts/hooks/dispatch_hook.ts +43 -1
  154. package/src/scripts/hooks/interruption_ledger_hook.ts +256 -0
  155. package/src/scripts/hooks/ship_diff_volume_hook.ts +180 -0
  156. package/src/scripts/hooks/skill_route_hook.ts +226 -0
  157. package/src/scripts/hooks/turn_end_gate_hook.ts +119 -26
  158. package/src/scripts/hooks/ui_route_nudge_hook.ts +6 -3
  159. package/src/scripts/hooks_doctor.ts +112 -0
  160. package/src/scripts/interruption_report.ts +443 -0
  161. package/src/scripts/lint_design_antipattern_parity.ts +40 -6
  162. package/src/scripts/lint_evidence_artifacts.ts +359 -0
  163. package/src/scripts/lint_harvest_provenance.ts +35 -1
  164. package/src/scripts/lint_roadmap_blockers.ts +65 -0
  165. package/src/scripts/lint_roadmap_complexity.ts +7 -1
  166. package/src/scripts/lint_rule_skill_pack_reach.ts +43 -0
  167. package/src/scripts/lint_skill_router_head.ts +21 -0
  168. package/src/scripts/mcp_server/consumer_tool_catalog.json +26 -3
  169. package/src/scripts/mcp_server/tool_catalog_source.ts +6 -24
  170. package/src/scripts/mcp_server/tools.ts +87 -0
  171. package/src/scripts/release.ts +43 -58
  172. package/src/scripts/routing_doctor.ts +50 -13
  173. package/src/scripts/rule_activation_census.ts +292 -0
  174. package/src/scripts/schemas/command.schema.json +1 -1
  175. package/src/scripts/schemas/evaluator-output.schema.json +60 -0
  176. package/src/scripts/schemas/review-findings.schema.json +31 -0
  177. package/src/scripts/schemas/skill.schema.json +18 -1
  178. package/src/scripts/self_review_gate.ts +6 -0
  179. package/src/scripts/session_register_hook.ts +30 -1
  180. package/src/scripts/sync_pr_branch.ts +271 -0
  181. package/src/scripts/test_council_qualification.ts +213 -0
  182. package/src/scripts/trigger_coverage.ts +224 -3
  183. package/src/server/schemas/settings.ts +2 -2
  184. package/dist/ui/assets/index-B0aMJnuz.js.map +0 -1
  185. package/src/config/budget-routing.json +0 -7
@@ -29,6 +29,7 @@ import * as fs from 'node:fs';
29
29
  import * as path from 'node:path';
30
30
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
31
 
32
+ import { GateLedger } from './_lib/gate_ledger.js';
32
33
  import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
33
34
 
34
35
  const QUIET = process.argv.includes('--quiet');
@@ -44,6 +45,14 @@ export type Status = (typeof STATUSES)[number];
44
45
  export interface ParityFinding {
45
46
  kind: string;
46
47
  msg: string;
48
+ /**
49
+ * The catalog id this finding is about, when it is about one.
50
+ *
51
+ * Carried explicitly so the per-target ledger can attribute a failure
52
+ * without parsing it back out of `msg`. Absent on structural findings
53
+ * (a missing § Detector status section is about the document, not an id).
54
+ */
55
+ id?: string;
47
56
  }
48
57
 
49
58
  const ENTRY_ROW = /^\|\s*((?:V|C|T|L|M|CP)\d+)\s*\|/gm;
@@ -89,7 +98,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
89
98
  const entries = catalogEntries(catalogMd);
90
99
  const seen = new Set<string>();
91
100
  for (const id of entries) {
92
- if (seen.has(id)) out.push({ kind: 'duplicate-entry', msg: `${id}: appears more than once in the catalog tables` });
101
+ if (seen.has(id)) out.push({ kind: 'duplicate-entry', id, msg: `${id}: appears more than once in the catalog tables` });
93
102
  seen.add(id);
94
103
  }
95
104
 
@@ -98,13 +107,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
98
107
  // A — every catalog entry is classified.
99
108
  for (const id of seen) {
100
109
  if (!rows.has(id)) {
101
- out.push({ kind: 'unclassified', msg: `${id}: catalog entry missing from § Detector status` });
110
+ out.push({ kind: 'unclassified', id, msg: `${id}: catalog entry missing from § Detector status` });
102
111
  }
103
112
  }
104
113
  // B — every classified row names a real entry.
105
114
  for (const id of rows.keys()) {
106
115
  if (!seen.has(id)) {
107
- out.push({ kind: 'orphan-status', msg: `${id}: § Detector status row names an entry that no catalog table defines` });
116
+ out.push({ kind: 'orphan-status', id, msg: `${id}: § Detector status row names an entry that no catalog table defines` });
108
117
  }
109
118
  }
110
119
  // D + E — status vocabulary and reasons.
@@ -112,12 +121,13 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
112
121
  if (!(STATUSES as readonly string[]).includes(row.status)) {
113
122
  out.push({
114
123
  kind: 'bad-status',
124
+ id,
115
125
  msg: `${id}: status '${row.status || '(empty)'}' is not one of ${STATUSES.join(', ')}`,
116
126
  });
117
127
  continue;
118
128
  }
119
129
  if (row.status !== 'backed' && row.note === '') {
120
- out.push({ kind: 'missing-reason', msg: `${id}: status '${row.status}' carries no reason` });
130
+ out.push({ kind: 'missing-reason', id, msg: `${id}: status '${row.status}' carries no reason` });
121
131
  }
122
132
  }
123
133
 
@@ -126,7 +136,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
126
136
  const registry = new Set(registryCatalogIds(registryTs));
127
137
  for (const id of backed) {
128
138
  if (!registry.has(id)) {
129
- out.push({ kind: 'backed-without-rule', msg: `${id}: marked backed but no registry rule declares that catalogId` });
139
+ out.push({ kind: 'backed-without-rule', id, msg: `${id}: marked backed but no registry rule declares that catalogId` });
130
140
  }
131
141
  }
132
142
  for (const id of registry) {
@@ -134,6 +144,7 @@ export function parityFindings(catalogMd: string, registryTs: string): ParityFin
134
144
  const known = rows.get(id);
135
145
  out.push({
136
146
  kind: 'rule-without-backed',
147
+ id,
137
148
  msg: known
138
149
  ? `${id}: a registry rule declares this catalogId but § Detector status marks it '${known.status}'`
139
150
  : `${id}: a registry rule declares this catalogId and no catalog entry carries it`,
@@ -167,8 +178,31 @@ export function main(): number {
167
178
  const registryTs = fs.readFileSync(REGISTRY, 'utf-8');
168
179
  const findings = parityFindings(catalogMd, registryTs);
169
180
 
181
+ const rows = statusRows(catalogMd);
170
182
  const entries = catalogEntries(catalogMd).length;
171
- const backed = [...statusRows(catalogMd).values()].filter((r) => r.status === 'backed').length;
183
+ const backed = [...rows.values()].filter((r) => r.status === 'backed').length;
184
+
185
+ // Per-target completeness accounting. The target is a catalog ID, and the
186
+ // planned set is the UNION of the three places an id can appear — catalog
187
+ // tables, the § Detector status table, and the rule registry — because this
188
+ // gate's whole subject is parity BETWEEN those sets. Planning only the
189
+ // catalog would leave an orphan status row or a registry-only id
190
+ // unaccounted, which is the direction the gate exists to catch.
191
+ const ledger = new GateLedger('lint_design_antipattern_parity');
192
+ const targets = [
193
+ ...new Set([
194
+ ...catalogEntries(catalogMd),
195
+ ...rows.keys(),
196
+ ...registryCatalogIds(registryTs),
197
+ ]),
198
+ ].sort();
199
+ ledger.plan(targets);
200
+ const failedIds = new Set(findings.map((f) => f.id).filter((v): v is string => v !== undefined));
201
+ for (const id of targets) {
202
+ if (failedIds.has(id)) ledger.fail(id, 'parity finding');
203
+ else ledger.complete(id);
204
+ }
205
+ ledger.report();
172
206
 
173
207
  if (findings.length === 0) {
174
208
  _print(`✅ catalog ↔ rule parity OK — ${entries} entries classified, ${backed} detector-backed`);
@@ -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
+ }
@@ -49,6 +49,7 @@ import * as fs from 'node:fs';
49
49
  import * as path from 'node:path';
50
50
  import { fileURLToPath, pathToFileURL } from 'node:url';
51
51
 
52
+ import { GateLedger } from './_lib/gate_ledger.js';
52
53
  import { assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
53
54
 
54
55
  const _FILE = fileURLToPath(import.meta.url);
@@ -432,7 +433,40 @@ export function main(argv: string[] = process.argv.slice(2)): number {
432
433
  const text = fs.readFileSync(ledgerPath, 'utf-8');
433
434
  const { records, findings } = lintLedgerText(text, REPO);
434
435
  const citations = [...collectCitations(REPO), ...collectPersonaSources(REPO)];
435
- const allFindings = [...findings, ...findOrphanCitations(citations, records)];
436
+ const orphans = findOrphanCitations(citations, records);
437
+ const allFindings = [...findings, ...orphans];
438
+
439
+ // Per-target completeness accounting over the two populations this gate
440
+ // reasons about: the ledger's own rows, and the citations that point at
441
+ // them. Both are legitimately EMPTY today, and that is exactly why the
442
+ // accounting is worth having — a green line over zero rows and a green line
443
+ // over a hundred checked rows read identically without it, which is the
444
+ // failure the success message above already tries to talk its way out of in
445
+ // prose. The ledger states it structurally instead.
446
+ const ledger = new GateLedger('lint_harvest_provenance');
447
+ const known = new Set(records.map((r) => r.harvest_id));
448
+ // The row population is every PARSED ledger line, not `records`: a row that
449
+ // fails validation or uniqueness is `continue`d and never reaches `records`,
450
+ // so planning `records` would plan exactly the rows that passed and account
451
+ // for none of the ones that did not — an accounting that can only ever read
452
+ // 100 %. Lines are the stable key; a Finding already carries one.
453
+ const { parsed } = parseLedgerText(text);
454
+ const badLines = new Set(findings.filter((f) => f.line > 0).map((f) => f.line));
455
+ ledger.plan([
456
+ ...parsed.map((r) => `row:${String(r.line)}`),
457
+ ...citations.map((c) => `citation:${c.id}@${c.file}`),
458
+ ]);
459
+ for (const r of parsed) {
460
+ const id = `row:${String(r.line)}`;
461
+ if (badLines.has(r.line)) ledger.fail(id, 'ledger-row finding');
462
+ else ledger.complete(id);
463
+ }
464
+ for (const c of citations) {
465
+ const id = `citation:${c.id}@${c.file}`;
466
+ if (known.has(c.id)) ledger.complete(id);
467
+ else ledger.fail(id, 'orphan citation — no ledger row carries this harvest id');
468
+ }
469
+ ledger.report();
436
470
 
437
471
  if (allFindings.length > 0) {
438
472
  for (const f of allFindings) {
@@ -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) {