@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,228 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Every in-body `${x}` on an artifact that declares `inputs:` is backed by a
4
+ * declaration.
5
+ *
6
+ * road-to-an-invocation-contract-that-reaches-the-wire 2.2.
7
+ *
8
+ * SCOPED TO DECLARING ARTIFACTS, ON PURPOSE
9
+ *
10
+ * An artifact with no `inputs:` block is not checked at all. That is what lets
11
+ * this arrive with zero findings over a corpus where nothing declares anything
12
+ * yet, and grow only with adoption — a check that fired on every artifact would
13
+ * have to be introduced together with a migration nobody asked for, which is
14
+ * exactly what the optional block was designed to avoid.
15
+ *
16
+ * THE COUNTING UNIT IS THE CENSUS'S, IMPORTED
17
+ *
18
+ * A reference counts when it appears in PROSE — outside fenced code blocks,
19
+ * indented blocks and inline code spans. `proseOnly` is imported from
20
+ * `report_invocation_surface.ts` rather than restated, because two copies of
21
+ * that definition would drift and the whole point of this phase is that the
22
+ * declaration and what depends on it cannot disagree.
23
+ *
24
+ * The consequence is stated rather than hidden: a `${target}` inside a fenced
25
+ * usage example is NOT checked. That is the conservative direction. A skill
26
+ * teaching Terraform has `${local.x}` in a fence and would otherwise be
27
+ * reported against a declaration it never claimed to satisfy — a false positive
28
+ * on working content, which is the failure that makes a young gate get
29
+ * disabled. When adoption makes in-fence usage examples common, widening the
30
+ * unit is a deliberate change with its own diff.
31
+ *
32
+ * Exit codes: 0 every reference backed · 1 an unbacked reference · 2 the corpus
33
+ * could not be read.
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
+ import { proseOnly } from './report_invocation_surface.js';
47
+
48
+ const _HERE = fileURLToPath(import.meta.url);
49
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
50
+
51
+ export const GATE = 'check_input_references';
52
+
53
+ /** The reference form this checks. Only the plurality syntax is a reference. */
54
+ const REFERENCE_RE = /\$\{([a-z][a-z0-9_]*)\}/g;
55
+
56
+ export interface Unbacked {
57
+ readonly file: string;
58
+ readonly name: string;
59
+ readonly declared: readonly string[];
60
+ }
61
+
62
+ /** Declared input names, or `null` when the artifact declares no block at all. */
63
+ export function declaredInputs(text: string): string[] | null {
64
+ const m = /^---\n([\s\S]*?)\n---/.exec(text);
65
+ if (m === null) return null;
66
+ let fm: unknown;
67
+ try {
68
+ fm = yaml.load(m[1]!);
69
+ } catch {
70
+ return null;
71
+ }
72
+ if (fm === null || typeof fm !== 'object') return null;
73
+ const inputs = (fm as Record<string, unknown>)['inputs'];
74
+ if (!Array.isArray(inputs)) return null;
75
+ const names: string[] = [];
76
+ for (const item of inputs) {
77
+ if (item === null || typeof item !== 'object') continue;
78
+ const name = (item as Record<string, unknown>)['name'];
79
+ if (typeof name === 'string') names.push(name);
80
+ }
81
+ return names;
82
+ }
83
+
84
+ /**
85
+ * Prose `${x}` references with no matching declaration.
86
+ *
87
+ * Returns `[]` for an artifact that declares no block — not because it has no
88
+ * references, but because it is out of scope.
89
+ */
90
+ export function unbackedReferences(text: string, file = '<data>'): Unbacked[] {
91
+ const declared = declaredInputs(text);
92
+ if (declared === null) return [];
93
+ const seen = new Set<string>();
94
+ const out: Unbacked[] = [];
95
+ for (const m of proseOnly(text).matchAll(REFERENCE_RE)) {
96
+ const name = m[1]!;
97
+ if (declared.includes(name) || seen.has(name)) continue;
98
+ seen.add(name);
99
+ out.push({ file, name, declared });
100
+ }
101
+ return out;
102
+ }
103
+
104
+ function walk(dir: string, match: (n: string) => boolean, out: string[] = []): string[] {
105
+ if (!fs.existsSync(dir)) return out;
106
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
107
+ const p = path.join(dir, e.name);
108
+ if (e.isDirectory()) walk(p, match, out);
109
+ else if (match(e.name)) out.push(p);
110
+ }
111
+ return out;
112
+ }
113
+
114
+ export function scan(root: string): { findings: Unbacked[]; scanned: number; declaring: number } {
115
+ const files = [
116
+ ...walk(path.join(root, 'dist', 'agent-src', 'commands'), (n) => n.endsWith('.md')),
117
+ ...walk(path.join(root, 'src', 'skills'), (n) => n === 'SKILL.md'),
118
+ ].sort();
119
+
120
+ const findings: Unbacked[] = [];
121
+ let declaring = 0;
122
+ for (const f of files) {
123
+ const text = fs.readFileSync(f, 'utf8');
124
+ if (declaredInputs(text) !== null) declaring += 1;
125
+ findings.push(...unbackedReferences(text, path.relative(root, f)));
126
+ }
127
+ return { findings, scanned: files.length, declaring };
128
+ }
129
+
130
+ export function main(argv: readonly string[]): number {
131
+ if (argv.includes('--self-test')) return selfTest();
132
+ const rootIdx = argv.indexOf('--root');
133
+ const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
134
+
135
+ const { findings, scanned, declaring } = scan(root);
136
+ const ledger = new GateLedger(GATE);
137
+ ledger.plan(findings.map((f) => `${f.file}:${f.name}`));
138
+ for (const f of findings) {
139
+ ledger.fail(`${f.file}:${f.name}`, `\`\${${f.name}}\` is not declared`);
140
+ }
141
+
142
+ // A gate that read nothing has not passed. This fires BEFORE the verdict,
143
+ // because certifying against an empty corpus is the false green the coverage
144
+ // manifest exists to refuse — named by the provider-diverse ratification
145
+ // review as one condition that would have flipped its verdict.
146
+ assertScanned({ gate: GATE, scanned: scanned, units: 'artifact(s)', roots: ['dist/agent-src/commands', 'src/skills'] });
147
+
148
+ reportScanned({
149
+ gate: GATE,
150
+ scanned,
151
+ units: 'artifact(s)',
152
+ roots: ['dist/agent-src/commands', 'src/skills'],
153
+ });
154
+ ledger.report();
155
+
156
+ if (findings.length > 0) {
157
+ for (const f of findings) {
158
+ process.stdout.write(
159
+ `❌ ${GATE}: ${f.file} references \`\${${f.name}}\` with no \`inputs.${f.name}\` ` +
160
+ `(declared: ${f.declared.length > 0 ? f.declared.join(', ') : 'none'}).\n`,
161
+ );
162
+ }
163
+ process.stdout.write(
164
+ '\nAdd the declaration, or drop the reference. Only artifacts that declare ' +
165
+ '`inputs:` at all are checked, so this never asks an artifact to start ' +
166
+ 'declaring — it asks a declaration to be complete.\n',
167
+ );
168
+ return 1;
169
+ }
170
+
171
+ process.stdout.write(
172
+ `✅ ${GATE}: every in-body reference is backed ` +
173
+ `(${String(declaring)} of ${String(scanned)} artifact(s) declare \`inputs:\`).\n`,
174
+ );
175
+ return 0;
176
+ }
177
+
178
+ /**
179
+ * A self-test is what proves a gate DISCRIMINATES. An enforced scan floor only
180
+ * proves it read something; the verdict has to move.
181
+ */
182
+ function selfTest(): number {
183
+ const plant = (name: string, body: string): string => {
184
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `input-refs-${name}-`));
185
+ const skills = path.join(dir, 'src', 'skills', 'probe');
186
+ fs.mkdirSync(skills, { recursive: true });
187
+ fs.writeFileSync(path.join(skills, 'SKILL.md'), body, 'utf-8');
188
+ return dir;
189
+ };
190
+ const declaring = (ref: string): string =>
191
+ `---\nname: probe\ninputs:\n - name: a\n type: string\n---\n\nUses ${ref} here.\n`;
192
+
193
+ const run = (root: string): number =>
194
+ runGateCli(ROOT, path.join('src', 'scripts', 'check_input_references.ts'), ['--root', root], ROOT);
195
+
196
+ return runSelfTest({
197
+ gate: GATE,
198
+ minCases: 4,
199
+ minRejectCases: 2,
200
+ cases: [
201
+ {
202
+ name: 'an empty corpus is refused, not certified green',
203
+ expect: 'reject',
204
+ run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'input-refs-empty-'))),
205
+ },
206
+ {
207
+ name: 'a reference the declaration backs passes',
208
+ expect: 'accept',
209
+ run: () => run(plant('backed', declaring('${a}'))),
210
+ },
211
+ {
212
+ name: 'a reference nothing declares fails',
213
+ expect: 'reject',
214
+ run: () => run(plant('unbacked', declaring('${b}'))),
215
+ },
216
+ {
217
+ name: 'an artifact with no declaration is out of scope, however it references',
218
+ expect: 'accept',
219
+ run: () =>
220
+ run(plant('noblock', '---\nname: probe\n---\n\nUses ${anything} here.\n')),
221
+ },
222
+ ],
223
+ });
224
+ }
225
+
226
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
227
+ process.exit(main(process.argv.slice(2)));
228
+ }
@@ -0,0 +1,235 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * What the MCP server reads from an `inputs:` block equals what a real YAML
4
+ * parser reads from it.
5
+ *
6
+ * road-to-an-invocation-contract-that-reaches-the-wire — added after review.
7
+ *
8
+ * WHY THIS GATE EXISTS
9
+ *
10
+ * The server's `_parse_inputs` is a hand-rolled reader of a YAML subset,
11
+ * because the MCP server is stdlib-only and bundled and must not grow a YAML
12
+ * dependency to read one optional block. A hand-rolled reader of a format it
13
+ * does not fully implement has exactly one dangerous failure mode: reading
14
+ * something DIFFERENT from what the author wrote, silently. A host is then told
15
+ * about an argument that does not exist, or not told about one that does — and
16
+ * the schema validates either way, because the schema checks the YAML, not the
17
+ * reader.
18
+ *
19
+ * An independent review found that failure live: the reader's key scan ran at
20
+ * any depth, so a `default:` mapping whose first child was `name:` overwrote
21
+ * the parameter's own name. It also silently misread `required: True`, the
22
+ * flow form, a comment at column zero, and a block scalar. Every one of those
23
+ * is schema-valid.
24
+ *
25
+ * So the reader now DECLINES what it cannot model, and this gate makes every
26
+ * decline and every divergence a build failure with a named remedy. The
27
+ * contract is: write the block form, or the build tells you. That is strictly
28
+ * better than a reader that is clever and occasionally wrong, and it is why the
29
+ * reader is allowed to stay stdlib-only.
30
+ *
31
+ * Exit codes: 0 parity holds · 1 a divergence · 2 the corpus could not be read.
32
+ */
33
+
34
+ import fs from 'node:fs';
35
+ import os from 'node:os';
36
+ import path from 'node:path';
37
+ import { fileURLToPath, pathToFileURL } from 'node:url';
38
+
39
+ import * as yaml from 'js-yaml';
40
+
41
+ import { GateLedger } from './_lib/gate_ledger.js';
42
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
43
+ import { assertScanned, reportScanned } from './_lib/scan_scope.js';
44
+ import { _frontmatter_text, _parse_inputs, type PromptInput } from './mcp_server/prompts.js';
45
+
46
+ const _HERE = fileURLToPath(import.meta.url);
47
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
48
+
49
+ export const GATE = 'check_inputs_parity';
50
+
51
+ /** The same reduction `_parse_inputs` produces, taken from a real YAML parse. */
52
+ export function yamlInputs(fmText: string): PromptInput[] | null {
53
+ let doc: unknown;
54
+ try {
55
+ doc = yaml.load(fmText);
56
+ } catch {
57
+ return null;
58
+ }
59
+ if (doc === null || typeof doc !== 'object') return null;
60
+ const raw = (doc as Record<string, unknown>)['inputs'];
61
+ if (raw === undefined) return null;
62
+ if (!Array.isArray(raw)) return [];
63
+ const out: PromptInput[] = [];
64
+ for (const item of raw) {
65
+ if (item === null || typeof item !== 'object') continue;
66
+ const r = item as Record<string, unknown>;
67
+ if (typeof r['name'] !== 'string') continue;
68
+ out.push({
69
+ name: r['name'],
70
+ description: typeof r['description'] === 'string' ? r['description'] : '',
71
+ required: r['required'] === true,
72
+ });
73
+ }
74
+ return out;
75
+ }
76
+
77
+ export interface Divergence {
78
+ readonly file: string;
79
+ readonly server: PromptInput[];
80
+ readonly yaml: PromptInput[];
81
+ }
82
+
83
+ const same = (a: readonly PromptInput[], b: readonly PromptInput[]): boolean =>
84
+ a.length === b.length &&
85
+ a.every(
86
+ (x, i) =>
87
+ x.name === b[i]!.name &&
88
+ x.required === b[i]!.required &&
89
+ x.description === b[i]!.description,
90
+ );
91
+
92
+ /** Compare both readers over one document. `null` when it declares no block. */
93
+ export function compare(text: string, file: string): Divergence | null {
94
+ const fm = _frontmatter_text(text);
95
+ if (fm === '') return null;
96
+ const expected = yamlInputs(fm);
97
+ if (expected === null) return null;
98
+ const actual = _parse_inputs(fm);
99
+ return same(actual, expected) ? null : { file, server: actual, yaml: expected };
100
+ }
101
+
102
+ function walk(dir: string, match: (n: string) => boolean, out: string[] = []): string[] {
103
+ if (!fs.existsSync(dir)) return out;
104
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
105
+ const p = path.join(dir, e.name);
106
+ if (e.isDirectory()) walk(p, match, out);
107
+ else if (match(e.name)) out.push(p);
108
+ }
109
+ return out;
110
+ }
111
+
112
+ export function scan(root: string): { findings: Divergence[]; scanned: number } {
113
+ const files = [
114
+ ...walk(path.join(root, 'src', 'domains'), (n) => n === 'command.md'),
115
+ ...walk(path.join(root, 'src', 'skills'), (n) => n === 'SKILL.md'),
116
+ ...walk(path.join(root, 'dist', 'agent-src', 'commands'), (n) => n.endsWith('.md')),
117
+ ].sort();
118
+
119
+ const findings: Divergence[] = [];
120
+ for (const f of files) {
121
+ const d = compare(fs.readFileSync(f, 'utf8'), path.relative(root, f));
122
+ if (d !== null) findings.push(d);
123
+ }
124
+ return { findings, scanned: files.length };
125
+ }
126
+
127
+ export function main(argv: readonly string[]): number {
128
+ if (argv.includes('--self-test')) return selfTest();
129
+ const rootIdx = argv.indexOf('--root');
130
+ const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
131
+
132
+ const { findings, scanned } = scan(root);
133
+ const ledger = new GateLedger(GATE);
134
+ ledger.plan(findings.map((f) => f.file));
135
+ for (const f of findings) {
136
+ ledger.fail(f.file, `server read ${String(f.server.length)}, YAML read ${String(f.yaml.length)}`);
137
+ }
138
+
139
+ // A gate that read nothing has not passed. This fires BEFORE the verdict,
140
+ // because certifying against an empty corpus is the false green the coverage
141
+ // manifest exists to refuse — named by the provider-diverse ratification
142
+ // review as one condition that would have flipped its verdict.
143
+ assertScanned({ gate: GATE, scanned: scanned, units: 'artifact(s)', roots: ['src/domains', 'src/skills', 'dist/agent-src/commands'] });
144
+
145
+ reportScanned({
146
+ gate: GATE,
147
+ scanned,
148
+ units: 'artifact(s)',
149
+ roots: ['src/domains', 'src/skills', 'dist/agent-src/commands'],
150
+ });
151
+ ledger.report();
152
+
153
+ if (findings.length > 0) {
154
+ for (const f of findings) {
155
+ process.stdout.write(
156
+ `❌ ${GATE}: ${f.file} — the MCP reader and YAML disagree about \`inputs:\`.\n` +
157
+ ` server: ${JSON.stringify(f.server)}\n` +
158
+ ` yaml: ${JSON.stringify(f.yaml)}\n`,
159
+ );
160
+ }
161
+ process.stdout.write(
162
+ '\nThe server reads one shape, deliberately — a block sequence of `- name:` items ' +
163
+ 'with scalar values on their own lines. Rewrite the declaration in that shape. ' +
164
+ 'The flow form, a block scalar and a nested mapping under an item key are ' +
165
+ 'refused rather than guessed at, and this gate is how the refusal is visible ' +
166
+ 'instead of becoming a wrong argument on the wire.\n',
167
+ );
168
+ return 1;
169
+ }
170
+
171
+ process.stdout.write(
172
+ `✅ ${GATE}: the MCP reader and YAML agree on every declared \`inputs:\` block.\n`,
173
+ );
174
+ return 0;
175
+ }
176
+
177
+ /**
178
+ * One rejecting case per way the reader and YAML can part company, plus the
179
+ * agreeing case — because a suite that only proves passes proves nothing.
180
+ */
181
+ function selfTest(): number {
182
+ const plant = (name: string, fm: string): string => {
183
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `inputs-parity-${name}-`));
184
+ const skills = path.join(dir, 'src', 'skills', 'probe');
185
+ fs.mkdirSync(skills, { recursive: true });
186
+ fs.writeFileSync(path.join(skills, 'SKILL.md'), `---\nname: probe\n${fm}---\n\nbody\n`, 'utf-8');
187
+ return dir;
188
+ };
189
+ const run = (root: string): number =>
190
+ runGateCli(ROOT, path.join('src', 'scripts', 'check_inputs_parity.ts'), ['--root', root], ROOT);
191
+
192
+ return runSelfTest({
193
+ gate: GATE,
194
+ minCases: 6,
195
+ minRejectCases: 5,
196
+ cases: [
197
+ {
198
+ name: 'an empty corpus is refused, not certified green',
199
+ expect: 'reject',
200
+ run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'inputs-parity-empty-'))),
201
+ },
202
+ {
203
+ name: 'the supported block form agrees',
204
+ expect: 'accept',
205
+ run: () => run(plant('ok', 'inputs:\n - name: a\n type: string\n required: true\n')),
206
+ },
207
+ {
208
+ name: 'the flow form is a divergence',
209
+ expect: 'reject',
210
+ run: () => run(plant('flow', 'inputs: [{name: a, type: string}]\n')),
211
+ },
212
+ {
213
+ name: 'a nested mapping under an item key is a divergence',
214
+ expect: 'reject',
215
+ run: () =>
216
+ run(plant('nested', 'inputs:\n - name: opts\n type: string\n default:\n name: fallback\n')),
217
+ },
218
+ {
219
+ name: 'a block scalar is a divergence',
220
+ expect: 'reject',
221
+ run: () =>
222
+ run(plant('scalar', 'inputs:\n - name: a\n type: string\n description: >\n long\n')),
223
+ },
224
+ {
225
+ name: 'a dash on its own line is a divergence',
226
+ expect: 'reject',
227
+ run: () => run(plant('bareDash', 'inputs:\n -\n name: a\n type: string\n')),
228
+ },
229
+ ],
230
+ });
231
+ }
232
+
233
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
234
+ process.exit(main(process.argv.slice(2)));
235
+ }
@@ -30,6 +30,8 @@
30
30
  *
31
31
  * Gated: a diff touching a kernel rule under `src/rules/` (the nine of
32
32
  * `_lib/kernel_rules.ts`), a governance hook (`src/scripts/hooks/block_*.ts`),
33
+ * a hook-plumbing SOURCE (`hook_manifest.yaml`, `host_lowering.yaml`, a
34
+ * `*-dispatcher.sh`, either hook budget file — see `PLUMBING_SOURCE_RE`),
33
35
  * or this gate itself — so it cannot be weakened without its own record. Such
34
36
  * a diff must carry a ratification artifact under
35
37
  * the ratifications directory with `verdict: ratified`, valid per the
@@ -122,6 +124,31 @@ export const ANCHOR_PATHS: readonly string[] = [
122
124
  /** Governance hooks: the blocking PreToolUse guards. */
123
125
  const GOVERNANCE_HOOK_RE = /^src\/scripts\/hooks\/block_[a-z0-9_]+\.ts$/;
124
126
 
127
+ /**
128
+ * Hook-plumbing SOURCES — the files that decide which concern runs, on which
129
+ * host, under which budget.
130
+ *
131
+ * `road-to-a-kernel-that-guards-its-plumbing` 1.1. The nine kernel rules were
132
+ * governed and the plumbing that decides whether a guard runs at all was not,
133
+ * so a diff deleting `block-no-verify` from the manifest carried no record
134
+ * while a typo in the rule it enforces carried one. The asymmetry was the
135
+ * defect: a concern removed from `hook_manifest.yaml` is a refusal that stops
136
+ * happening, which is the same authority change as loosening the rule behind
137
+ * it, reached one file earlier.
138
+ *
139
+ * This is the ADR-268 § 4 mechanism EXTENDED, not a second deny. Nothing new
140
+ * refuses a tool call here; the same CI gate simply reads a wider path set,
141
+ * and every property the gate already had — base-revision code, in-repo quorum
142
+ * policy, the whole diff as its unit — carries over unchanged.
143
+ *
144
+ * Sources only. `dist/hooks/dispatch.js` and `hooks/hooks.json` are BUILD
145
+ * OUTPUTS with no legitimate hand edit at all, so they are refused at tool-call
146
+ * time by `block_plumbing_writes.ts` rather than ratified here — a record for a
147
+ * file whose every hand edit is illegitimate would be a record of a mistake.
148
+ */
149
+ const PLUMBING_SOURCE_RE =
150
+ /^(?:src\/scripts\/hook_manifest\.yaml|src\/scripts\/hooks\/host_lowering\.yaml|src\/scripts\/hooks\/[a-z0-9-]+-dispatcher\.sh|src\/config\/hook-(?:token|latency)-budget\.json)$/;
151
+
125
152
  /**
126
153
  * A kernel rule, in the source tree or in any projection.
127
154
  *
@@ -141,6 +168,8 @@ const KERNEL_RULE_PATH_RE = /(?:^|\/)rules\/([a-z0-9-]+)\.md$/;
141
168
  export interface GatedPaths {
142
169
  kernelRules: string[];
143
170
  governanceHooks: string[];
171
+ /** Hook-plumbing sources — manifest, host lowering, dispatchers, budgets. */
172
+ plumbing: string[];
144
173
  self: boolean;
145
174
  }
146
175
 
@@ -148,6 +177,7 @@ export interface GatedPaths {
148
177
  export function classifyPaths(files: readonly string[]): GatedPaths {
149
178
  const kernelRules: string[] = [];
150
179
  const governanceHooks: string[] = [];
180
+ const plumbing: string[] = [];
151
181
  let self = false;
152
182
  for (const raw of files) {
153
183
  const p = raw.replace(/\\/g, '/').trim();
@@ -171,14 +201,23 @@ export function classifyPaths(files: readonly string[]): GatedPaths {
171
201
  }
172
202
  if (GOVERNANCE_HOOK_RE.test(p)) {
173
203
  governanceHooks.push(p);
204
+ continue;
205
+ }
206
+ if (PLUMBING_SOURCE_RE.test(p)) {
207
+ plumbing.push(p);
174
208
  }
175
209
  }
176
- return { kernelRules, governanceHooks, self };
210
+ return { kernelRules, governanceHooks, plumbing, self };
177
211
  }
178
212
 
179
213
  /** True when anything in the diff requires a ratification artifact. */
180
214
  export function requiresRatification(gated: GatedPaths): boolean {
181
- return gated.kernelRules.length > 0 || gated.governanceHooks.length > 0 || gated.self;
215
+ return (
216
+ gated.kernelRules.length > 0 ||
217
+ gated.governanceHooks.length > 0 ||
218
+ gated.plumbing.length > 0 ||
219
+ gated.self
220
+ );
182
221
  }
183
222
 
184
223
  /** Ratification artifacts present in the diff, as repo-relative paths. */
@@ -253,6 +292,7 @@ export function evaluate(
253
292
  const gatedSet = new Set<string>([
254
293
  ...gated.kernelRules,
255
294
  ...gated.governanceHooks,
295
+ ...gated.plumbing,
256
296
  ...(gated.self ? [SELF_PATH] : []),
257
297
  ]);
258
298
  const artifactSet = new Set(ratificationArtifactsIn(files));
@@ -286,7 +326,8 @@ export function evaluate(
286
326
 
287
327
  if (!requiresRatification(gated)) {
288
328
  lines.push(
289
- '✅ no kernel rule, governance hook or self edit in the diff — nothing to ratify',
329
+ '✅ no kernel rule, governance hook, hook-plumbing source or self edit in the ' +
330
+ 'diff — nothing to ratify',
290
331
  );
291
332
  close(true);
292
333
  return { exitCode: 0, lines, scanned };
@@ -295,6 +336,7 @@ export function evaluate(
295
336
  const touched = [
296
337
  ...gated.kernelRules.map((p) => `kernel rule ${p}`),
297
338
  ...gated.governanceHooks.map((p) => `governance hook ${p}`),
339
+ ...gated.plumbing.map((p) => `hook-plumbing source ${p}`),
298
340
  ...(gated.self ? ['the ratification mechanism itself (gate, reader, policy or workflow)'] : []),
299
341
  ];
300
342
  lines.push(`Gated surfaces in this diff (${touched.length}):`);
@@ -446,8 +488,8 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
446
488
  roots: ['<the diff against the base ref>'],
447
489
  allowEmpty:
448
490
  'EMPTY_VALID: the corpus is one diff. A commit range that changed nothing is a ' +
449
- 'real state, and there is then no kernel rule, governance hook or mechanism file ' +
450
- 'to ratify — the gate has read everything there was.',
491
+ 'real state, and there is then no kernel rule, governance hook, plumbing source or ' +
492
+ 'mechanism file to ratify — the gate has read everything there was.',
451
493
  });
452
494
  if (!quiet || result.exitCode !== 0) {
453
495
  for (const l of result.lines) {