@event4u/agent-config 16.1.0 → 16.2.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 (217) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -0,0 +1,287 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Where an artifact declares `inputs:`, its `argument-hint` is DERIVED from
4
+ * that declaration — and a hand-written hint on the same artifact is a
5
+ * conflict, not a merge.
6
+ *
7
+ * road-to-an-invocation-contract-that-reaches-the-wire 3.1.
8
+ *
9
+ * WHY A CONFLICT RATHER THAN A MERGE
10
+ *
11
+ * The tree already answers "what does this take?" in prose, through
12
+ * `argument-hint`, which nothing validates. Adding a structured declaration
13
+ * beside it creates a SECOND authority for the same question, and two surfaces
14
+ * that can answer one question drift — which is the failure this whole roadmap
15
+ * exists to make unrepresentable (Risk 1). Making the derived direction
16
+ * one-way, and a disagreement a build failure, is what keeps the second
17
+ * authority from forming. Merging the two would BE the second authority.
18
+ *
19
+ * An artifact with no declaration is untouched: its hand-written hint stays
20
+ * exactly as it is, because there is nothing to derive from and no conflict to
21
+ * have. That is why the 43 hintless commands are a generation gap that closes
22
+ * as declarations land, rather than 43 prose edits nobody reviewed.
23
+ *
24
+ * THE FORM, AND WHY IT ROUND-TRIPS
25
+ *
26
+ * `<name>` required · `[name]` optional · `:a|b` appended for an enum. The
27
+ * corpus already writes `[path]` and `[--force]` in exactly this shape, so the
28
+ * generated hint is not a new dialect. {@link parseHint} recovers the name,
29
+ * requiredness and enum from the rendered string, and the round-trip is
30
+ * asserted rather than assumed — a generator whose output its own parser cannot
31
+ * read would make the conflict check unfalsifiable.
32
+ *
33
+ * Exit codes: 0 in sync · 1 a conflict or a missing derived hint · 2 unreadable.
34
+ */
35
+
36
+ import fs from 'node:fs';
37
+ import os from 'node:os';
38
+ import path from 'node:path';
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
+
41
+ import * as yaml from 'js-yaml';
42
+
43
+ import { GateLedger } from './_lib/gate_ledger.js';
44
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
45
+ import { assertScanned, reportScanned } from './_lib/scan_scope.js';
46
+
47
+ const _HERE = fileURLToPath(import.meta.url);
48
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
49
+
50
+ export const GATE = 'check_argument_hint';
51
+
52
+ export interface InputDecl {
53
+ readonly name: string;
54
+ readonly type: string;
55
+ readonly required?: boolean;
56
+ readonly enum?: readonly string[];
57
+ }
58
+
59
+ /** Render the canonical hint for a declaration. */
60
+ export function hintFor(inputs: readonly InputDecl[]): string {
61
+ return inputs
62
+ .map((i) => {
63
+ const body = i.enum && i.enum.length > 0 ? `${i.name}:${i.enum.join('|')}` : i.name;
64
+ return i.required === true ? `<${body}>` : `[${body}]`;
65
+ })
66
+ .join(' ');
67
+ }
68
+
69
+ export interface ParsedHint {
70
+ readonly name: string;
71
+ readonly required: boolean;
72
+ readonly enum?: readonly string[];
73
+ }
74
+
75
+ /** Recover declarations from a rendered hint. The inverse of {@link hintFor}. */
76
+ export function parseHint(hint: string): ParsedHint[] {
77
+ const out: ParsedHint[] = [];
78
+ for (const m of hint.matchAll(/([<[])([a-z][a-z0-9_]*)(?::([^\]>]+))?([\]>])/g)) {
79
+ const open = m[1]!;
80
+ const close = m[4]!;
81
+ if ((open === '<') !== (close === '>')) continue;
82
+ const parsed: ParsedHint = {
83
+ name: m[2]!,
84
+ required: open === '<',
85
+ ...(m[3] === undefined ? {} : { enum: m[3].split('|') }),
86
+ };
87
+ out.push(parsed);
88
+ }
89
+ return out;
90
+ }
91
+
92
+ /** Frontmatter of one artifact, or `null` when it has none or it is malformed. */
93
+ function frontmatter(text: string): Record<string, unknown> | null {
94
+ const m = /^---\n([\s\S]*?)\n---/.exec(text);
95
+ if (m === null) return null;
96
+ try {
97
+ const fm = yaml.load(m[1]!);
98
+ return fm !== null && typeof fm === 'object' ? (fm as Record<string, unknown>) : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ export function declaredInputs(fm: Record<string, unknown> | null): InputDecl[] | null {
105
+ if (fm === null) return null;
106
+ const raw = fm['inputs'];
107
+ if (!Array.isArray(raw)) return null;
108
+ const out: InputDecl[] = [];
109
+ for (const item of raw) {
110
+ if (item === null || typeof item !== 'object') continue;
111
+ const r = item as Record<string, unknown>;
112
+ if (typeof r['name'] !== 'string' || typeof r['type'] !== 'string') continue;
113
+ out.push({
114
+ name: r['name'],
115
+ type: r['type'],
116
+ ...(typeof r['required'] === 'boolean' ? { required: r['required'] } : {}),
117
+ ...(Array.isArray(r['enum'])
118
+ ? { enum: r['enum'].filter((x): x is string => typeof x === 'string') }
119
+ : {}),
120
+ });
121
+ }
122
+ return out;
123
+ }
124
+
125
+ export type FindingKind = 'conflict' | 'missing';
126
+
127
+ export interface Finding {
128
+ readonly file: string;
129
+ readonly kind: FindingKind;
130
+ readonly derived: string;
131
+ /** The hand-written hint, for a conflict. */
132
+ readonly written?: string;
133
+ }
134
+
135
+ /** Compare one artifact's declaration against its hint. */
136
+ export function checkArtifact(text: string, file: string): Finding | null {
137
+ const fm = frontmatter(text);
138
+ const inputs = declaredInputs(fm);
139
+ if (inputs === null || inputs.length === 0) return null;
140
+ const derived = hintFor(inputs);
141
+ const written = fm?.['argument-hint'];
142
+ if (typeof written !== 'string') return { file, kind: 'missing', derived };
143
+ if (written.trim() !== derived) return { file, kind: 'conflict', derived, written };
144
+ return null;
145
+ }
146
+
147
+ function walk(dir: string, match: (n: string) => boolean, out: string[] = []): string[] {
148
+ if (!fs.existsSync(dir)) return out;
149
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
150
+ const p = path.join(dir, e.name);
151
+ if (e.isDirectory()) walk(p, match, out);
152
+ else if (match(e.name)) out.push(p);
153
+ }
154
+ return out;
155
+ }
156
+
157
+ /**
158
+ * COMMANDS ONLY, and the reason is structural rather than a scoping preference:
159
+ * `argument-hint` is a key of `command.schema.json` and of no other schema. A
160
+ * skill cannot carry one, so reporting a skill that declares `inputs:` as
161
+ * "missing its hint" would demand a field the schema forbids. Skills still get
162
+ * their declaration onto the wire — that is `to_mcp_prompt_meta`'s job (3.2),
163
+ * which reads the declaration directly and needs no hint at all.
164
+ */
165
+ export function scan(root: string): { findings: Finding[]; scanned: number; declaring: number } {
166
+ const files = walk(path.join(root, 'src', 'domains'), (n) => n === 'command.md').sort();
167
+
168
+ const findings: Finding[] = [];
169
+ let declaring = 0;
170
+ for (const f of files) {
171
+ const text = fs.readFileSync(f, 'utf8');
172
+ const inputs = declaredInputs(frontmatter(text));
173
+ if (inputs !== null && inputs.length > 0) declaring += 1;
174
+ const finding = checkArtifact(text, path.relative(root, f));
175
+ if (finding !== null) findings.push(finding);
176
+ }
177
+ return { findings, scanned: files.length, declaring };
178
+ }
179
+
180
+ export function main(argv: readonly string[]): number {
181
+ if (argv.includes('--self-test')) return selfTest();
182
+ const rootIdx = argv.indexOf('--root');
183
+ const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
184
+
185
+ const { findings, scanned, declaring } = scan(root);
186
+ const ledger = new GateLedger(GATE);
187
+ ledger.plan(findings.map((f) => f.file));
188
+ for (const f of findings) ledger.fail(f.file, f.kind);
189
+
190
+ // A gate that read nothing has not passed. This fires BEFORE the verdict,
191
+ // because certifying against an empty corpus is the false green the coverage
192
+ // manifest exists to refuse — named by the provider-diverse ratification
193
+ // review as one condition that would have flipped its verdict.
194
+ assertScanned({ gate: GATE, scanned: scanned, units: 'artifact(s)', roots: ['src/domains'] });
195
+
196
+ reportScanned({
197
+ gate: GATE,
198
+ scanned,
199
+ units: 'artifact(s)',
200
+ roots: ['src/domains'],
201
+ });
202
+ ledger.report();
203
+
204
+ if (findings.length > 0) {
205
+ for (const f of findings) {
206
+ if (f.kind === 'conflict') {
207
+ process.stdout.write(
208
+ `❌ ${GATE}: ${f.file} has a hand-written \`argument-hint\` that contradicts ` +
209
+ `its declaration.\n written: ${f.written ?? ''}\n derived: ${f.derived}\n`,
210
+ );
211
+ } else {
212
+ process.stdout.write(
213
+ `❌ ${GATE}: ${f.file} declares \`inputs:\` but carries no \`argument-hint\`.\n` +
214
+ ` derived: ${f.derived}\n`,
215
+ );
216
+ }
217
+ }
218
+ process.stdout.write(
219
+ '\nWhere a declaration exists the hint is derived from it, one way. Fix the ' +
220
+ 'declaration, or delete the hand-written hint — never reconcile the two by ' +
221
+ 'hand, which is the second authority this check exists to prevent.\n',
222
+ );
223
+ return 1;
224
+ }
225
+
226
+ process.stdout.write(
227
+ `✅ ${GATE}: every derived \`argument-hint\` matches its declaration ` +
228
+ `(${String(declaring)} of ${String(scanned)} artifact(s) declare \`inputs:\`).\n`,
229
+ );
230
+ return 0;
231
+ }
232
+
233
+ /**
234
+ * A self-test is what proves a gate DISCRIMINATES. An enforced scan floor only
235
+ * proves it read something; the verdict has to move.
236
+ */
237
+ function selfTest(): number {
238
+ const plant = (name: string, fm: string): string => {
239
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `arg-hint-${name}-`));
240
+ const cmd = path.join(dir, 'src', 'domains', 'probe-pack', 'probe');
241
+ fs.mkdirSync(cmd, { recursive: true });
242
+ fs.writeFileSync(path.join(cmd, 'command.md'), `---\nname: probe\n${fm}---\n\nbody\n`, 'utf-8');
243
+ return dir;
244
+ };
245
+
246
+ const run = (root: string): number =>
247
+ runGateCli(ROOT, path.join('src', 'scripts', 'check_argument_hint.ts'), ['--root', root], ROOT);
248
+
249
+ return runSelfTest({
250
+ gate: GATE,
251
+ minCases: 5,
252
+ minRejectCases: 3,
253
+ cases: [
254
+ {
255
+ name: 'an empty corpus is refused, not certified green',
256
+ expect: 'reject',
257
+ run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'arg-hint-empty-'))),
258
+ },
259
+ {
260
+ name: 'a hint matching its declaration passes',
261
+ expect: 'accept',
262
+ run: () =>
263
+ run(plant('match', 'argument-hint: "<a>"\ninputs:\n - name: a\n type: string\n required: true\n')),
264
+ },
265
+ {
266
+ name: 'a hand-written hint contradicting the declaration fails',
267
+ expect: 'reject',
268
+ run: () =>
269
+ run(plant('conflict', 'argument-hint: "[other]"\ninputs:\n - name: a\n type: string\n required: true\n')),
270
+ },
271
+ {
272
+ name: 'a declaration with no hint at all fails',
273
+ expect: 'reject',
274
+ run: () => run(plant('missing', 'inputs:\n - name: a\n type: string\n')),
275
+ },
276
+ {
277
+ name: 'an artifact with no declaration keeps its free-text hint',
278
+ expect: 'accept',
279
+ run: () => run(plant('nodecl', 'argument-hint: "[free text]"\n')),
280
+ },
281
+ ],
282
+ });
283
+ }
284
+
285
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
286
+ process.exit(main(process.argv.slice(2)));
287
+ }
@@ -208,9 +208,76 @@ const SELF_COUNT = new RegExp(
208
208
  'i',
209
209
  );
210
210
 
211
+ /**
212
+ * Byte-unit metric ids, read from `src/config/metric-registry.yml`.
213
+ *
214
+ * WHY THE IDS AND NOT THE WORD `bytes`. A detector keyed on a number near the
215
+ * word would red on ordinary documentation — a file size in a README, a payload
216
+ * example in a contract — and the cheapest repair for a gate that reds on
217
+ * correct prose is to weaken it until it finds nothing. Keyed on the DECLARED
218
+ * ids, only a figure claiming a registered metric is checked, and a line that
219
+ * merely mentions bytes is not a claim about this package at all.
220
+ *
221
+ * `MAGNITUDE` already covers `KB|MB|GB`, so this is not the first byte-shaped
222
+ * figure the gate sees; what it adds is the id-bearing form, which is how
223
+ * `road-to-a-bytes-row-that-exists` made a byte statement expressible in the
224
+ * first place.
225
+ *
226
+ * Parsed with a line scanner rather than a YAML dependency: the shape read is
227
+ * two adjacent keys at a fixed indent, and an unreadable registry yields an
228
+ * EMPTY id list, which makes the detector inert rather than throwing inside a
229
+ * gate whose job is to report other people's problems.
230
+ */
231
+ export function parse_byte_metric_ids(raw: string): string[] {
232
+ const out: string[] = [];
233
+ let current: string | null = null;
234
+ for (const line of raw.split('\n')) {
235
+ const id = /^\s*-\s+id:\s*([A-Za-z0-9._-]+)\s*$/.exec(line);
236
+ if (id?.[1] !== undefined) {
237
+ current = id[1];
238
+ continue;
239
+ }
240
+ const unit = /^\s*unit:\s*(\S+)\s*$/.exec(line);
241
+ if (unit?.[1] !== undefined && current !== null && /^bytes/.test(unit[1])) {
242
+ out.push(current);
243
+ current = null;
244
+ }
245
+ }
246
+ return out;
247
+ }
248
+
249
+ const METRIC_REGISTRY_REL = 'src/config/metric-registry.yml';
250
+ let _byteMetricIds: string[] | null = null;
251
+
252
+ function byteMetricIds(): string[] {
253
+ if (_byteMetricIds === null) {
254
+ try {
255
+ _byteMetricIds = parse_byte_metric_ids(
256
+ fs.readFileSync(path.join(REPO, METRIC_REGISTRY_REL), 'utf-8'),
257
+ );
258
+ } catch {
259
+ _byteMetricIds = [];
260
+ }
261
+ }
262
+ return _byteMetricIds;
263
+ }
264
+
265
+ /**
266
+ * A line naming a declared byte metric AND carrying a figure.
267
+ *
268
+ * Both halves are required. The id alone is prose about the metric — the
269
+ * registry entry itself says what it is for, and that is not a claim. A number
270
+ * alone is every other line in the tree.
271
+ */
272
+ export function is_byte_metric_claim(line: string, ids: string[] = byteMetricIds()): boolean {
273
+ if (ids.length === 0) return false;
274
+ if (!/\b\d[\d,._]*\b/.test(line)) return false;
275
+ return ids.some((id) => new RegExp(String.raw`\b${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\b`).test(line));
276
+ }
277
+
211
278
  /** True when a line carries any figure shape that must bind to a claim. */
212
279
  export function is_quantified_claim(line: string): boolean {
213
- return RATIO.test(line) || MAGNITUDE.test(line) || SELF_COUNT.test(line);
280
+ return RATIO.test(line) || MAGNITUDE.test(line) || SELF_COUNT.test(line) || is_byte_metric_claim(line);
214
281
  }
215
282
 
216
283
  class ExitCode extends Error {
@@ -1041,6 +1108,17 @@ function selfTest(): number {
1041
1108
 
1042
1109
  const CLEAN_README = '# Agent Config\n\nGoverned skills, rules and commands.\n';
1043
1110
 
1111
+ /** A registry carrying exactly one byte-unit metric, for the 4.1 cases. */
1112
+ const BYTE_REGISTRY = [
1113
+ 'metrics:',
1114
+ ' - id: tool-raw-bytes',
1115
+ ' unit: bytes',
1116
+ ' basis: measured',
1117
+ ' - id: some-token-metric',
1118
+ ' unit: tokens',
1119
+ '',
1120
+ ].join('\n');
1121
+
1044
1122
  const cases: SelfTestCase[] = [
1045
1123
  {
1046
1124
  name: 'a retired phrasing absent from every publish surface → accept',
@@ -1071,6 +1149,52 @@ function selfTest(): number {
1071
1149
  expect: 'reject',
1072
1150
  run: () => fixture({ 'docs/CLAIMS.md': closed('daemon'), 'README.md': CLEAN_README }),
1073
1151
  },
1152
+
1153
+ // road-to-a-bytes-row-that-exists 4.1 — both directions, because a
1154
+ // one-directional fixture cannot tell a working detector from one that
1155
+ // reds on everything. The registry file is part of the fixture: the
1156
+ // gate reads it relative to its own location, so the copied script in
1157
+ // the temp root reads the copied registry.
1158
+ {
1159
+ name: 'a byte figure naming a metric with no ledger entry → reject',
1160
+ expect: 'reject',
1161
+ run: () =>
1162
+ fixture({
1163
+ 'docs/CLAIMS.md': closed(null),
1164
+ 'src/config/metric-registry.yml': BYTE_REGISTRY,
1165
+ 'README.md': `${CLEAN_README}\nEach dispatch records 2048 tool-raw-bytes.\n`,
1166
+ }),
1167
+ },
1168
+ {
1169
+ name: 'the same figure once a markered ledger entry backs it → accept',
1170
+ expect: 'accept',
1171
+ run: () =>
1172
+ fixture({
1173
+ 'docs/CLAIMS.md': [
1174
+ closed(null),
1175
+ '### claim: tool-raw-bytes-observed',
1176
+ '- claim: Each dispatch records 2048 tool-raw-bytes.',
1177
+ '- kind: quant',
1178
+ '- evidence: docs/evidence.md#ANCHOR',
1179
+ '- status: backed',
1180
+ '- last_verified: 2026-09-29',
1181
+ '',
1182
+ ].join('\n'),
1183
+ 'src/config/metric-registry.yml': BYTE_REGISTRY,
1184
+ 'README.md':
1185
+ `${CLEAN_README}\nEach dispatch records 2048 tool-raw-bytes. <!-- claim:tool-raw-bytes-observed -->\n`,
1186
+ }),
1187
+ },
1188
+ {
1189
+ name: 'ordinary prose carrying a byte size but no declared id → accept',
1190
+ expect: 'accept',
1191
+ run: () =>
1192
+ fixture({
1193
+ 'docs/CLAIMS.md': closed(null),
1194
+ 'src/config/metric-registry.yml': BYTE_REGISTRY,
1195
+ 'README.md': `${CLEAN_README}\nThe example payload below is 2048 bytes long.\n`,
1196
+ }),
1197
+ },
1074
1198
  {
1075
1199
  name: 'the never-published sentinel with a stated reason → accept',
1076
1200
  expect: 'accept',
@@ -260,13 +260,22 @@ function load_corpus(dirs: readonly string[], files: readonly string[] = []): st
260
260
  * whether or not anyone chooses to. Conflating the two is how a package ends up
261
261
  * believing an on-demand tool is a gate.
262
262
  */
263
- function reachable_scripts(wiring: string): Set<string> {
264
- const scripts_dir = path.join(REPO_ROOT, 'src', 'scripts');
263
+ /**
264
+ * Transitive closure of the scripts a wiring surface reaches.
265
+ *
266
+ * `roots` is a seam, not a feature: it defaults to the real tree, and a test
267
+ * passes a synthetic one. Without it the only way to exercise the fixed point
268
+ * is over ~650 real files, where a chain deep enough to catch a broken worklist
269
+ * cannot be constructed on purpose.
270
+ */
271
+ export function reachable_scripts(wiring: string, roots?: { dir: string; base: string }): Set<string> {
272
+ const scripts_dir = roots?.dir ?? path.join(REPO_ROOT, 'src', 'scripts');
273
+ const base_dir = roots?.base ?? REPO_ROOT;
265
274
  const all: string[] = fs.existsSync(scripts_dir)
266
275
  ? walk(scripts_dir).filter((p) => p.endsWith('.ts'))
267
276
  : [];
268
277
 
269
- const rel = (abs: string): string => path.relative(REPO_ROOT, abs);
278
+ const rel = (abs: string): string => path.relative(base_dir, abs);
270
279
  const stem = (abs: string): string => path.basename(abs).replace(/\.ts$/, '');
271
280
 
272
281
  const reached = new Set<string>();
@@ -274,24 +283,44 @@ function reachable_scripts(wiring: string): Set<string> {
274
283
  if (wiring.includes(rel(abs)) || wiring.includes(stem(abs))) reached.add(rel(abs));
275
284
  }
276
285
 
277
- let grew = true;
278
- while (grew) {
279
- grew = false;
280
- const frontier = [...reached];
286
+ // WORKLIST, not a re-scan of everything reached so far.
287
+ //
288
+ // The previous shape rebuilt `bodies` from EVERY reached file on every
289
+ // iteration and re-tested every unreached file against the whole thing. The
290
+ // corpus is ~650 scripts, `mentions_as_code` compiles and runs two regexes
291
+ // per candidate, and the concatenated body grows into the megabytes — so the
292
+ // cost was iterations x unreached x |all bodies so far|, with each iteration
293
+ // re-doing every comparison the previous ones had already made.
294
+ //
295
+ // Measured, because this was found by profile rather than by reading:
296
+ // `build_proof.render()` -- which calls this transitively -- took 241 s on a
297
+ // developer machine under plain `tsx`, and the CPU profile attributed
298
+ // essentially all of it to this function's regex sweep, spread over hundreds
299
+ // of per-name `RegExp` frames. The file's own header had recorded ~54 s.
300
+ //
301
+ // Re-testing an old body is provably redundant: a file that did not match a
302
+ // body in the iteration that body was added cannot match it later, because
303
+ // neither the body nor the test changes. So each round searches ONLY the
304
+ // bodies admitted in the previous round. Same fixed point, same result --
305
+ // the loop still runs until nothing new is reached.
306
+ let frontier = [...reached];
307
+ while (frontier.length > 0) {
281
308
  const bodies = frontier
282
309
  .map((r) => {
283
- const abs = path.join(REPO_ROOT, r);
310
+ const abs = path.join(base_dir, r);
284
311
  return fs.existsSync(abs) ? strip_comments(fs.readFileSync(abs, 'utf-8')) : '';
285
312
  })
286
313
  .join('\n');
314
+ const next: string[] = [];
287
315
  for (const abs of all) {
288
316
  const r = rel(abs);
289
317
  if (reached.has(r)) continue;
290
318
  if (mentions_as_code(bodies, path.basename(abs), stem(abs))) {
291
319
  reached.add(r);
292
- grew = true;
320
+ next.push(r);
293
321
  }
294
322
  }
323
+ frontier = next;
295
324
  }
296
325
  return reached;
297
326
  }