@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,262 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * A hook's exit code comes from the table, never from a numeral.
4
+ *
5
+ * road-to-a-kernel-that-guards-its-plumbing 4.1, the enforcing half.
6
+ * `hooks/exit_codes.ts` is the table; this is what keeps it the only one.
7
+ *
8
+ * WHAT THE NUMERALS COST. Thirty-three files under `src/scripts/hooks/` each
9
+ * declared their own `EXIT_ALLOW = 0` / `EXIT_BLOCK = 1` / `EXIT_WARN = 2`, two
10
+ * of them under a third name. The danger is not a typo: 1 and 2 mean the
11
+ * OPPOSITE things on Claude Code from what they mean in this tree's internal
12
+ * language, which is why `host_semantics.ts` exists to lower one onto the
13
+ * other. A bare `process.exit(1)` in a concern is a claim about which of the
14
+ * two dialects the writer had in mind, and the source does not record which.
15
+ *
16
+ * WHAT IT SCANS. `.ts` under `src/scripts/hooks/`, refusing a call whose
17
+ * argument is a bare numeric literal. An identifier is accepted whatever it is
18
+ * called — this gate does not verify that the identifier came from the table,
19
+ * because a gate that resolved imports would be a type-checker, and the thing
20
+ * it would catch (a local `const EXIT_BLOCK = 1` shadowing the import) is
21
+ * already visible in review as a duplicate declaration. Stated rather than left
22
+ * for a reader to assume the green covers it.
23
+ *
24
+ * WHY THE DISPATCHERS ARE OUT OF SCOPE. The `*-dispatcher.sh` trampolines carry
25
+ * `process.exit(0)` inside a `node -e` one-liner that reads a workspace root
26
+ * off stdin. They are shell, they cannot import, and their exit code is the
27
+ * one-liner's, never a verdict. The table's own header says so; this gate scans
28
+ * `.ts` only, which is the same boundary stated once more where it is enforced.
29
+ *
30
+ * THE TABLE FILE ITSELF is excluded, and that is the one exclusion: it is where
31
+ * the numerals are DEFINED, so a gate that refused them there would forbid the
32
+ * fix it exists to require.
33
+ *
34
+ * Exit codes: 0 clean · 1 a bare numeral reached `process.exit` · 2 the corpus
35
+ * could not be read.
36
+ */
37
+
38
+ import fs from 'node:fs';
39
+ import os from 'node:os';
40
+ import path from 'node:path';
41
+ import { fileURLToPath, pathToFileURL } from 'node:url';
42
+
43
+ import { GateLedger } from './_lib/gate_ledger.js';
44
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
45
+ import { 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 = 'lint_exit_codes';
51
+
52
+ const HOOKS_REL = path.join('src', 'scripts', 'hooks');
53
+
54
+ /** The file that DEFINES the numerals. Excluding it is not a carve-out. */
55
+ const TABLE_BASENAME = 'exit_codes.ts';
56
+
57
+ /**
58
+ * The fixture import line, ASSEMBLED rather than written out.
59
+ *
60
+ * `prepack-check.mjs` scans source text for relative imports and cannot tell a
61
+ * string literal from a real one, so a fixture containing the words
62
+ * `from './exit_codes.js'` reads as this file importing a sibling that does not
63
+ * exist beside it — the table lives under `hooks/`. It reported exactly that
64
+ * before this was assembled. Splitting the specifier is the smallest change
65
+ * that keeps the fixture honest about what a passing hook file looks like
66
+ * without asserting an import this file does not make.
67
+ */
68
+ const FIXTURE_IMPORT = (name: string): string => `import { ${name} } from '.` + `/${TABLE_BASENAME.replace('.ts', '.js')}';\n`;
69
+
70
+ /**
71
+ * `process.exit(<digits>)`, with optional whitespace.
72
+ *
73
+ * Deliberately anchored on `process.exit` rather than on any `exit(`: a
74
+ * concern's own helper named `exit` is not the process boundary, and widening
75
+ * the pattern to catch it would red on ordinary code.
76
+ */
77
+ const BARE_EXIT = /process\s*\.\s*exit\s*\(\s*(-?\d+)\s*\)/g;
78
+
79
+ export interface ExitFinding {
80
+ readonly file: string;
81
+ readonly line: number;
82
+ readonly code: string;
83
+ }
84
+
85
+ /** Findings in one file's text. Pure, so a test reads the decision. */
86
+ export function findBareExits(rel: string, text: string): ExitFinding[] {
87
+ const out: ExitFinding[] = [];
88
+ const lines = text.split('\n');
89
+ for (let i = 0; i < lines.length; i++) {
90
+ const line = lines[i] as string;
91
+ // A commented example is prose about the rule, not an exit.
92
+ if (/^\s*(\*|\/\/)/.test(line)) continue;
93
+ BARE_EXIT.lastIndex = 0;
94
+ let m: RegExpExecArray | null;
95
+ while ((m = BARE_EXIT.exec(line)) !== null) {
96
+ out.push({ file: rel, line: i + 1, code: m[1] as string });
97
+ }
98
+ }
99
+ return out;
100
+ }
101
+
102
+ function hookFiles(root: string): string[] {
103
+ const dir = path.join(root, HOOKS_REL);
104
+ let entries: string[];
105
+ try {
106
+ entries = fs.readdirSync(dir);
107
+ } catch {
108
+ return [];
109
+ }
110
+ return entries
111
+ .filter((f) => f.endsWith('.ts') && f !== TABLE_BASENAME)
112
+ .map((f) => path.join(HOOKS_REL, f))
113
+ .sort();
114
+ }
115
+
116
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
117
+ if (argv.includes('--self-test')) return selfTest();
118
+ const i = argv.indexOf('--root');
119
+ if (i !== -1 && (argv[i + 1] === undefined || argv[i + 1]?.startsWith('-') === true)) {
120
+ process.stderr.write(`${GATE}: \`--root\` needs a directory.\n`);
121
+ return 2;
122
+ }
123
+ const root = i === -1 ? ROOT : (argv[i + 1] as string);
124
+
125
+ const files = hookFiles(root);
126
+ const findings: ExitFinding[] = [];
127
+ // A file the gate cannot READ is not a file it cleared. The first version
128
+ // of this loop skipped it and then counted it complete, which is a false
129
+ // green with a number behind it — the exact shape `assertScanned` exists to
130
+ // refuse one level up, reintroduced inside the loop. Reported by an
131
+ // independent review before this landed.
132
+ const unreadable: string[] = [];
133
+ for (const rel of files) {
134
+ let text: string;
135
+ try {
136
+ text = fs.readFileSync(path.join(root, rel), 'utf-8');
137
+ } catch {
138
+ unreadable.push(rel);
139
+ continue;
140
+ }
141
+ findings.push(...findBareExits(rel, text));
142
+ }
143
+ if (unreadable.length > 0) {
144
+ for (const rel of unreadable) {
145
+ process.stderr.write(`❌ ${GATE}: cannot read ${rel} — the corpus is incomplete.\n`);
146
+ }
147
+ return 2;
148
+ }
149
+
150
+ // Before the verdict: a corpus of zero is a gate that read nothing, and a
151
+ // green over it says the tree is clean when nobody looked.
152
+ try {
153
+ reportScanned({ gate: GATE, scanned: files.length, units: 'hook file(s)', roots: [HOOKS_REL] });
154
+ } catch (exc) {
155
+ process.stderr.write(`❌ ${String((exc as Error).message)}\n`);
156
+ return 2;
157
+ }
158
+
159
+ const ledger = new GateLedger(GATE);
160
+ ledger.plan(files);
161
+ const failed = new Set(findings.map((f) => f.file));
162
+ for (const f of findings) {
163
+ ledger.fail(f.file, `bare exit numeral ${f.code} at line ${String(f.line)}`);
164
+ }
165
+ for (const rel of files) if (!failed.has(rel)) ledger.complete(rel);
166
+ ledger.report();
167
+
168
+ if (findings.length > 0) {
169
+ for (const f of findings) {
170
+ process.stdout.write(
171
+ `❌ ${GATE}: ${f.file}:${String(f.line)} exits with the bare numeral ` +
172
+ `\`${f.code}\`. Import the name from \`hooks/exit_codes.ts\` — the numbers ` +
173
+ 'mean opposite things in this tree and on the host, and a numeral records ' +
174
+ 'neither.\n',
175
+ );
176
+ }
177
+ return 1;
178
+ }
179
+ process.stdout.write(
180
+ `✅ ${GATE}: ${String(files.length)} hook file(s), no bare exit numeral.\n`,
181
+ );
182
+ return 0;
183
+ }
184
+
185
+ function selfTest(): number {
186
+ const plant = (name: string, body: string): string => {
187
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `exit-codes-${name}-`));
188
+ fs.mkdirSync(path.join(dir, HOOKS_REL), { recursive: true });
189
+ fs.writeFileSync(path.join(dir, HOOKS_REL, 'probe_hook.ts'), body, 'utf-8');
190
+ // The table itself, so the exclusion is exercised rather than assumed.
191
+ fs.writeFileSync(
192
+ path.join(dir, HOOKS_REL, TABLE_BASENAME),
193
+ 'export const EXIT_ALLOW = 0;\nprocess.exit(0);\n',
194
+ 'utf-8',
195
+ );
196
+ return dir;
197
+ };
198
+ const run = (root: string): number =>
199
+ runGateCli(ROOT, path.join('src', 'scripts', 'lint_exit_codes.ts'), ['--root', root], ROOT);
200
+
201
+ return runSelfTest({
202
+ gate: GATE,
203
+ minCases: 6,
204
+ minRejectCases: 4,
205
+ cases: [
206
+ {
207
+ name: 'an empty corpus is refused, not certified green',
208
+ expect: 'reject',
209
+ run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'exit-codes-empty-'))),
210
+ },
211
+ {
212
+ name: 'a bare deny numeral fails',
213
+ expect: 'reject',
214
+ run: () => run(plant('block', 'process.exit(1);\n')),
215
+ },
216
+ {
217
+ name: 'a bare allow numeral fails too — 0 is the one people think is harmless',
218
+ expect: 'reject',
219
+ run: () => run(plant('allow', 'process.exit(0);\n')),
220
+ },
221
+ {
222
+ name: 'whitespace inside the call does not hide it',
223
+ expect: 'reject',
224
+ run: () => run(plant('spaced', 'process . exit ( 2 );\n')),
225
+ },
226
+ {
227
+ name: 'an imported name passes',
228
+ expect: 'accept',
229
+ run: () =>
230
+ run(
231
+ plant(
232
+ 'named',
233
+ `${FIXTURE_IMPORT('EXIT_ALLOW')}process.exit(EXIT_ALLOW);\n`,
234
+ ),
235
+ ),
236
+ },
237
+ {
238
+ // The table defines the numerals; refusing them there would
239
+ // forbid the fix this gate exists to require. Exercised, not
240
+ // assumed — the plant writes a bare exit into it every time.
241
+ name: 'the table file is excluded even though it carries a numeral',
242
+ expect: 'accept',
243
+ run: () =>
244
+ run(
245
+ plant(
246
+ 'table',
247
+ `${FIXTURE_IMPORT('EXIT_WARN')}process.exit(EXIT_WARN);\n`,
248
+ ),
249
+ ),
250
+ },
251
+ ],
252
+ });
253
+ }
254
+
255
+ function _isCliEntry(): boolean {
256
+ const entry = process.argv[1];
257
+ return entry !== undefined && import.meta.url === pathToFileURL(entry).href;
258
+ }
259
+
260
+ if (_isCliEntry()) {
261
+ process.exit(main());
262
+ }
@@ -716,6 +716,61 @@ export function _check_host_lowering(
716
716
  }
717
717
  }
718
718
  }
719
+ _check_slot_answers(table, errors);
720
+ }
721
+
722
+ /**
723
+ * Every (host, slot) pair carries a date, and a citation to go with it.
724
+ *
725
+ * WHY AN ERROR AND NOT A WARNING. `block_exit: null` is the value the table
726
+ * carries on 29 of its 32 pairs, and until this check it meant two different
727
+ * things at once: "read the host's contract, it honours no refusal here" and
728
+ * "nobody has ever looked at this pair". Those are opposite epistemic states
729
+ * printed as the same byte, and the file's own header promises the first one
730
+ * ("`verified: null` means nobody established anything") without giving the
731
+ * per-slot layer any way to say it. `answered_at` is that way, and a soft
732
+ * warning would let the ambiguity back in one undated slot at a time — the
733
+ * exact drift the row-level `verified` gate was added to stop one level up.
734
+ *
735
+ * THE CITATION MAY BE INHERITED. A host publishes ONE hooks page, so requiring
736
+ * a per-slot `docs_url` would mean five copies of the same URL per host and
737
+ * five places for it to rot. The row's `verified.docs_url` is the citation
738
+ * unless the slot overrides it; what the check refuses is a dated answer with
739
+ * no reachable citation at all.
740
+ *
741
+ * A slot on a row with NO `verified` block is exempt from the citation half
742
+ * and not from the date: an undated row is already an error wherever it carries
743
+ * a blocking binding, and demanding a URL from a row that has none would report
744
+ * the same missing provenance twice in different words.
745
+ */
746
+ export function _check_slot_answers(table: HostLowering, errors: string[]): void {
747
+ for (const [host, surfaces] of table) {
748
+ for (const [surface, row] of surfaces) {
749
+ for (const [slot, s] of row.slots) {
750
+ const where = `host_lowering ${host}/${surface}/${slot}`;
751
+ if (s.answered_at === null) {
752
+ errors.push(
753
+ `${where}: no \`answered_at\`. Every host-slot pair carries the date it was ` +
754
+ "last answered, so that a `block_exit: null` is a finding with a date on it " +
755
+ "rather than a slot nobody looked at.",
756
+ );
757
+ continue;
758
+ }
759
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(s.answered_at)) {
760
+ errors.push(`${where}: \`answered_at: ${s.answered_at}\` is not an ISO \`YYYY-MM-DD\` date.`);
761
+ }
762
+ // The row-level exemption the block comment above states: a row with no
763
+ // `verified` block already reports its missing provenance once, at row level.
764
+ if (row.verified !== null && s.docs_url === null && row.verified.docs_url === null) {
765
+ errors.push(
766
+ `${where}: dated \`${s.answered_at}\` with no citation — the slot carries no ` +
767
+ "`docs_url` and the row's `verified.docs_url` is absent. Cite the page the " +
768
+ "answer was read off, on the slot or on the row.",
769
+ );
770
+ }
771
+ }
772
+ }
773
+ }
719
774
  }
720
775
 
721
776
  export function lint(
@@ -3,7 +3,8 @@
3
3
  * Hard-gate linter for the roadmap `## Blockers` contract
4
4
  * (`templates/roadmaps.md` rule 20 / `roadmap-ci-steps-policy` siblings).
5
5
  *
6
- * Validates, for every active roadmap under `agents/roadmaps/*.md`:
6
+ * Validates, for every active roadmap and every parked idea one directory
7
+ * down in `stubs/`:
7
8
  *
8
9
  * 1. Every `### blocker: <id>` entry declares all five required fields
9
10
  * (Status, Owner, Blocks, What to do, Resolved when).
@@ -22,11 +23,14 @@
22
23
  * road-to-asked-not-parked exists to stop; without this field a marker
23
24
  * cannot distinguish a declined decision from an unoffered one.
24
25
  *
25
- * HARD, not ratcheted, and it can be: no active roadmap carries a real
26
- * checkbox annotation today, so on the day it ships the rule fires on
27
- * nothing — the same "no backlog to grandfather" argument the `Class:`
28
- * contract above makes. `later/` and `archive/` are outside this gate's
29
- * glob and are untouched by it.
26
+ * HARD, not ratcheted, and it can be: no file in scope carries a real
27
+ * checkbox annotation pointing at a blocker today, so on the day it ships
28
+ * the rule fires on nothing — the same "no backlog to grandfather"
29
+ * argument the `Class:` contract above makes. Re-measured when `stubs/`
30
+ * entered scope on 2026-09-28: 7 stubs carry checkboxes and 0 of them
31
+ * carry a `blocked-by:` marker, so the rule stays latent there too.
32
+ * `later/`, `archive/` and `skipped/` are outside this gate's glob and
33
+ * are untouched by it; `stubs/` is inside it — see § SCOPE.
30
34
  *
31
35
  * 5. An entry that declares `- **Ownership:**` declares one of the three
32
36
  * OWNER-OWNED classes (`product-owned`, `business-owned`,
@@ -41,10 +45,49 @@
41
45
  * field, so on the day it ships no entry in the tree declares one and the
42
46
  * rule fires on nothing.
43
47
  *
48
+ * SCOPE — the active tree plus `stubs/`, and deliberately nothing else.
49
+ *
50
+ * Widened from the active tree alone by an owner ruling on 2026-09-28. Before
51
+ * it, a stub's hold was unreachable by every blocker gate: a parked idea is
52
+ * where a hold sits LONGEST, so excluding it exempted exactly the entries most
53
+ * likely to rot. The rejected alternative was to keep the scope and reword the
54
+ * acceptance criterion that named this gate — refused because a criterion
55
+ * edited to match what was achieved stops being an acceptance test.
56
+ *
57
+ * Measured on the tree the widening shipped against: 121 files under `stubs/`
58
+ * (120 stubs plus the directory README), of which 6 carry a `### blocker:`
59
+ * heading and 4 declare one open inside a `## Blockers` section. Zero hard
60
+ * violations, zero additions to the decidability ratchet, zero new
61
+ * active/archived overlaps. So it lands on nothing — the same "no backlog to
62
+ * grandfather" argument the `Class:` and `Ownership:` contracts make.
63
+ *
64
+ * REACH, stated honestly, because the obvious reading of the paragraph above
65
+ * is wrong: this does NOT hold every future stub blocker to the contract. Both
66
+ * scanners require the entry to sit under a `## Blockers` heading, and 2 of
67
+ * today's 6 put it under `## State` instead — so a third of the population is
68
+ * in the glob and still unread. Stub headings are free-form, unlike the
69
+ * template-driven roadmaps this gate was written for, so that is the likely
70
+ * shape rather than an edge case. Widening the glob fixes WHERE the gate looks
71
+ * and not WHAT it can parse; the second half is the sibling defect recorded in
72
+ * `stubs/road-to-blocker-parse-visibility.md`, and it is untouched here.
73
+ *
74
+ * `later/`, `archive/` and `skipped/` stay OUT, and the reason is not symmetry:
75
+ * those record decisions already taken (parked, closed, dropped), so a blocker
76
+ * left unresolved there is history rather than debt. A stub records a decision
77
+ * still to take. The polarity is pinned in both directions by
78
+ * `tests/scripts/lint_roadmap_blockers.test.ts` § the scanned scope, so
79
+ * widening further cannot happen by accident.
80
+ *
81
+ * One consequence worth naming rather than discovering: `_archiveOverlap`
82
+ * defaults its ACTIVE corpus to this glob, so a stub declaring a blocker open
83
+ * that an archived roadmap also declares open now fails. That is the same
84
+ * self-contradiction the assertion already caught one directory up, and it
85
+ * measured 0 on the widening tree.
86
+ *
44
87
  * Fenced code blocks are stripped before scanning so a roadmap that shows
45
88
  * the `## Blockers` shape as a documentation example is not flagged.
46
89
  *
47
- * Exit codes: 0 = clean / no active roadmaps, 1 = violations.
90
+ * Exit codes: 0 = clean / nothing in scope, 1 = violations.
48
91
  */
49
92
 
50
93
  import * as fs from 'node:fs';
@@ -58,7 +101,14 @@ const _HERE = fileURLToPath(import.meta.url);
58
101
  const QUIET = process.argv.slice(2).includes('--quiet');
59
102
 
60
103
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
61
- const ROADMAP_GLOB = 'agents/roadmaps/*.md';
104
+ /**
105
+ * The scanned scope, for display. Two plain globs rather than one brace
106
+ * expression: nothing globs with this string, and a `{,stubs/}` empty
107
+ * alternative is bash-only syntax that would mislead anyone who tried.
108
+ */
109
+ const ROADMAP_GLOB = 'agents/roadmaps/*.md + agents/roadmaps/stubs/*.md';
110
+ /** The one subdirectory inside the glob. See § SCOPE for why only this one. */
111
+ const SCANNED_SUBDIRS: readonly string[] = ['stubs'];
62
112
 
63
113
  const FENCED_CODE_RE = /^[ \t]*```[^\n]*\n[\s\S]*?^[ \t]*```[ \t]*$/gm;
64
114
  const BLOCKERS_SECTION_RE = /^##[ \t]+Blockers[ \t]*$/im;
@@ -370,13 +420,14 @@ function _scanBoth(rawText: string): ScanResult {
370
420
  return { hard: violations, decidability };
371
421
  }
372
422
 
373
- /** Sorted `agents/roadmaps/*.md` (non-recursive — active roadmaps only). */
374
- function _globRoadmaps(): string[] {
375
- const dir = path.join(REPO_ROOT, 'agents', 'roadmaps');
423
+ /** Sorted `*.md` directly inside `dir` — never recursive. */
424
+ function _globFlat(dir: string): string[] {
376
425
  let entries: fs.Dirent[];
377
426
  try {
378
427
  entries = fs.readdirSync(dir, { withFileTypes: true });
379
428
  } catch {
429
+ // A missing directory is not an error: `stubs/` is optional, and the
430
+ // dead-scope assertion in `main` is what catches a MOVED root.
380
431
  return [];
381
432
  }
382
433
  const out: string[] = [];
@@ -388,6 +439,21 @@ function _globRoadmaps(): string[] {
388
439
  return out.sort();
389
440
  }
390
441
 
442
+ /**
443
+ * The files this gate judges: active roadmaps, plus `stubs/`. See § SCOPE.
444
+ *
445
+ * Takes its root so the scope can be driven from a fixture tree. Asserting the
446
+ * rule against the real tree would only restate today's file list; the test
447
+ * pins which DIRECTORIES are in and which are out, in both directions.
448
+ */
449
+ function _globRoadmaps(roadmapRoot: string = path.join(REPO_ROOT, 'agents', 'roadmaps')): string[] {
450
+ const out = _globFlat(roadmapRoot);
451
+ for (const sub of SCANNED_SUBDIRS) {
452
+ out.push(..._globFlat(path.join(roadmapRoot, sub)));
453
+ }
454
+ return out;
455
+ }
456
+
391
457
  function _relPosix(target: string, root: string): string {
392
458
  return path.relative(root, target).split(path.sep).join('/');
393
459
  }
@@ -561,7 +627,7 @@ function main(): number {
561
627
  const roadmaps = _globRoadmaps();
562
628
  if (roadmaps.length === 0) {
563
629
  if (!QUIET) {
564
- process.stdout.write(`✅ no active roadmaps under ${ROADMAP_GLOB}\n`);
630
+ process.stdout.write(`✅ nothing in scope under ${ROADMAP_GLOB}\n`);
565
631
  }
566
632
  return 0;
567
633
  }
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S node --import tsx
1
+ #!/usr/bin/env tsx
2
2
  /**
3
3
  * lint_skill_descriptions — description-quality gate (ecosystem-harvest
4
4
  * skill-quality-gates, Phase 1).
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S node --import tsx
1
+ #!/usr/bin/env tsx
2
2
  /**
3
3
  * lint_skill_scripts_readonly — read-only-by-default script convention
4
4
  * (ecosystem-harvest skill-quality-gates, Phase 3).
@@ -69,6 +69,22 @@ export interface SkillPrompt {
69
69
  readonly kind: PromptKind;
70
70
  readonly recommended_for_user_types: readonly string[];
71
71
  readonly user_type_match: UserTypeMatch;
72
+ /**
73
+ * Declared parameters, from the frontmatter `inputs:` block.
74
+ *
75
+ * Empty for every artifact that declares none, which is the whole corpus
76
+ * today — so `arguments: []` on the wire stays the behaviour for an
77
+ * undeclared prompt rather than becoming a special case
78
+ * (road-to-an-invocation-contract-that-reaches-the-wire 3.2).
79
+ */
80
+ readonly inputs: readonly PromptInput[];
81
+ }
82
+
83
+ /** One declared parameter, reduced to what the MCP `arguments` shape carries. */
84
+ export interface PromptInput {
85
+ readonly name: string;
86
+ readonly description: string;
87
+ readonly required: boolean;
72
88
  }
73
89
 
74
90
  /** Walk up from this file to the repo root (parent of `scripts/`). */
@@ -92,6 +108,124 @@ export function _project_root(): string {
92
108
  * Avoids a YAML dependency for Phase 1; the frontmatter shape is
93
109
  * enforced by `task lint-skills` upstream.
94
110
  */
111
+ /** The raw frontmatter block of a document, or `''` when it has none. */
112
+ export function _frontmatter_text(text: string): string {
113
+ if (!text.startsWith('---\n')) return '';
114
+ const parts = _splitMax(text, '---\n', 2);
115
+ return parts.length < 3 ? '' : (parts[1] as string);
116
+ }
117
+
118
+ /**
119
+ * Parse the frontmatter `inputs:` block out of raw frontmatter text.
120
+ *
121
+ * WHY THIS EXISTS RATHER THAN A YAML CALL. `_strip_frontmatter` is a flat
122
+ * `key: value` line scanner and cannot represent a nested list — `inputs:`
123
+ * would land as an empty string and its `- name: x` lines as stray keys. The
124
+ * MCP server is deliberately stdlib-only and is bundled for the wire, so
125
+ * pulling a YAML parser in to read one optional block would pay a dependency
126
+ * on every install for a block almost nothing declares yet.
127
+ *
128
+ * IT READS ONE SHAPE AND REFUSES THE REST. Accepted:
129
+ *
130
+ * inputs:
131
+ * - name: target
132
+ * type: string
133
+ * required: true
134
+ * description: What to act on.
135
+ *
136
+ * Refused, by yielding NOTHING for the whole block: the flow form
137
+ * (`inputs: [{name: a}]`), a block scalar (`description: >`), and any nested
138
+ * mapping under an item key. Refusing is not silent — `check_inputs_parity`
139
+ * compares this reader against a real YAML parse for every declaring artifact
140
+ * and FAILS the build on any difference, so an unsupported form is a red gate
141
+ * with a named remedy rather than a wrong argument on the wire.
142
+ *
143
+ * That gate is the answer to the defect an independent review found here: the
144
+ * key scan used to run at any depth, so a `default:` MAPPING whose first child
145
+ * was `name:` overwrote the parameter's own name and the server offered a host
146
+ * an argument that did not exist. The header promised "no guessed input" while
147
+ * the code guessed. Depth is now tracked, and where this reader cannot be sure
148
+ * it declines and the parity gate makes the decline visible.
149
+ */
150
+ export function _parse_inputs(fm: string): PromptInput[] {
151
+ const lines = fm.split('\n');
152
+ let i = lines.findIndex((l) => /^inputs:/.test(l));
153
+ if (i === -1) return [];
154
+ // The flow form carries its payload on the `inputs:` line itself. This
155
+ // reader does not parse it; the parity gate turns that into a failure.
156
+ if (!/^inputs:\s*$/.test(lines[i] as string)) return [];
157
+
158
+ const out: PromptInput[] = [];
159
+ let current: { name?: string; description?: string; required?: boolean } | null = null;
160
+ /** Indent of the key lines belonging to the item being read. */
161
+ let keyIndent: number | null = null;
162
+ let refused = false;
163
+
164
+ const flush = (): void => {
165
+ if (current?.name !== undefined) {
166
+ out.push({
167
+ name: current.name,
168
+ description: current.description ?? '',
169
+ required: current.required === true,
170
+ });
171
+ }
172
+ current = null;
173
+ };
174
+
175
+ const clean = (raw: string): string => {
176
+ const v = raw.trim();
177
+ // An unquoted trailing comment is not part of the value — the same strip
178
+ // `_strip_frontmatter` applies, kept consistent deliberately.
179
+ const hashed = /^(?!["'])([^#]*?)\s+#.*$/.exec(v);
180
+ return _stripQuotes((hashed === null ? v : (hashed[1] ?? '')).trim());
181
+ };
182
+
183
+ for (i += 1; i < lines.length && !refused; i += 1) {
184
+ const line = lines[i] as string;
185
+ if (line.trim() === '') continue;
186
+ // A comment at any column is not block content and does not end the block.
187
+ if (/^\s*#/.test(line)) continue;
188
+ // A non-indented line ends the block.
189
+ if (!/^\s/.test(line)) break;
190
+
191
+ const indent = (/^(\s*)/.exec(line) as RegExpExecArray)[1]!.replace(/\t/g, ' ').length;
192
+
193
+ const item = /^\s*-\s+([A-Za-z_]\w*):\s*(.*)$/.exec(line);
194
+ if (item !== null) {
195
+ flush();
196
+ current = {};
197
+ keyIndent = line.indexOf('-') + 2;
198
+ if (item[1] === 'name') current.name = clean(item[2] ?? '');
199
+ continue;
200
+ }
201
+
202
+ const kv = /^\s+([A-Za-z_]\w*):\s*(.*)$/.exec(line);
203
+ if (kv === null || current === null || keyIndent === null) continue;
204
+ // Deeper than the item's own keys: a nested mapping or sequence this
205
+ // reader does not model. Decline the whole block rather than absorb it.
206
+ if (indent > keyIndent) {
207
+ refused = true;
208
+ break;
209
+ }
210
+ const key = kv[1] as string;
211
+ const rawValue = (kv[2] ?? '').trim();
212
+ // A block scalar or an empty value means the real content is on the
213
+ // following lines, which this reader does not join.
214
+ if (rawValue === '' || rawValue === '>' || rawValue === '|') {
215
+ refused = true;
216
+ break;
217
+ }
218
+ const value = clean(rawValue);
219
+ if (key === 'name') current.name = value;
220
+ else if (key === 'description') current.description = value;
221
+ else if (key === 'required') current.required = value.toLowerCase() === 'true';
222
+ }
223
+
224
+ if (refused) return [];
225
+ flush();
226
+ return out;
227
+ }
228
+
95
229
  export function _strip_frontmatter(text: string): [Record<string, string>, string] {
96
230
  if (!text.startsWith('---\n')) {
97
231
  return [{}, text];
@@ -365,6 +499,7 @@ function _load_file(p: string, kind: PromptKind, fallback_name: string): SkillPr
365
499
  kind,
366
500
  recommended_for_user_types: _parse_inline_array(meta.recommended_for_user_types ?? ''),
367
501
  user_type_match: '',
502
+ inputs: _parse_inputs(_frontmatter_text(text)),
368
503
  };
369
504
  }
370
505
 
@@ -548,7 +683,13 @@ export function to_mcp_prompt_meta(prompt: SkillPrompt): Record<string, unknown>
548
683
  name: wire,
549
684
  title: prompt.name,
550
685
  description: prompt.description,
551
- arguments: [],
686
+ // Derived from the declaration, never invented. An artifact that
687
+ // declares nothing still serves `[]` — see `_parse_inputs`.
688
+ arguments: prompt.inputs.map((i) => ({
689
+ name: i.name,
690
+ description: i.description,
691
+ required: i.required,
692
+ })),
552
693
  _meta: meta,
553
694
  };
554
695
  }