@event4u/agent-config 14.20.0 → 14.22.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 (242) 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 +441 -0
  5. package/CREDITS.md +37 -0
  6. package/README.md +1 -1
  7. package/agents/templates/.ai-council.yml.example +10 -0
  8. package/dist/agent-src/commands/analyze/inbox.md +381 -6
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +251 -34
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
  16. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
  17. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  18. package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
  19. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  20. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  21. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  22. package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
  23. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  24. package/dist/agent-src/rules/token-efficiency.md +26 -3
  25. package/dist/agent-src/scripts/_cli_entry.ts +51 -0
  26. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  27. package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
  28. package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
  29. package/dist/agent-src/scripts/stubs_due.ts +4 -18
  30. package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
  31. package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
  32. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  33. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  34. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  35. package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
  36. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  37. package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
  38. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  39. package/dist/agent-src/templates/agent-settings.md +0 -1
  40. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  41. package/dist/agent-src/templates/features.md +5 -0
  42. package/dist/agent-src/templates/roadmaps.md +15 -0
  43. package/dist/cli-delegate/_cli_entry.js +8 -0
  44. package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
  45. package/dist/cli-delegate/archival_sweep.js +8 -0
  46. package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
  47. package/dist/cli-delegate/check_update_banner.js +923 -0
  48. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  49. package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
  50. package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
  51. package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
  52. package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-BIXEDQPZ.js} +7 -1
  53. package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
  54. package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
  55. package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
  56. package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
  57. package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
  58. package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
  59. package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
  60. package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
  61. package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
  62. package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
  63. package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
  64. package/dist/cli-delegate/cmd_conformance.js +19 -5
  65. package/dist/cli-delegate/cmd_doctor.js +3 -3
  66. package/dist/cli-delegate/cmd_export.js +2 -2
  67. package/dist/cli-delegate/cmd_handoff.js +27 -3
  68. package/dist/cli-delegate/cmd_prune.js +2 -2
  69. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  70. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  71. package/dist/cli-delegate/cmd_sync.js +2 -2
  72. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  73. package/dist/cli-delegate/cmd_validate.js +2 -2
  74. package/dist/cli-delegate/dashboard_mode.js +16 -0
  75. package/dist/cli-delegate/gate_budget.js +24 -0
  76. package/dist/cli-delegate/gate_execute.js +25 -0
  77. package/dist/cli-delegate/guarded_baseline.js +18 -0
  78. package/dist/cli-delegate/resume_probe.js +22 -0
  79. package/dist/cli-delegate/roadmap_gates.js +638 -0
  80. package/dist/cli-delegate/roadmap_set_step.js +173 -0
  81. package/dist/cli-delegate/stub_queue.js +47 -0
  82. package/dist/cli-delegate/stubs_due.js +38 -0
  83. package/dist/cli-delegate/ticket_bundles.js +59 -0
  84. package/dist/cli-delegate/update_roadmap_progress.js +60 -0
  85. package/dist/discovery/deprecation-report.md +1 -1
  86. package/dist/discovery/discovery-manifest.json +60 -29
  87. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  88. package/dist/discovery/discovery-manifest.summary.md +3 -3
  89. package/dist/discovery/dormancy-report.md +1 -1
  90. package/dist/discovery/orphan-report.md +1 -1
  91. package/dist/discovery/packs.json +7 -6
  92. package/dist/discovery/trust-report.md +2 -2
  93. package/dist/discovery/workspaces.json +6 -5
  94. package/dist/hooks/dispatch.js +1522 -1008
  95. package/dist/install/claudeLayerCarriage.js +237 -0
  96. package/dist/install/claudePathsPlan.js +79 -0
  97. package/dist/install/hostLayerFingerprint.js +1 -1
  98. package/dist/install/install.mjs +10 -1
  99. package/dist/install/partitionEligibility.js +231 -117
  100. package/dist/install/ruleLayerPartition.js +10 -7
  101. package/dist/mcp/registry-manifest.json +3 -3
  102. package/dist/mcp/server.json +2 -2
  103. package/dist/mcp/server.mjs +39376 -38902
  104. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  105. package/dist/scripts/ai_council/seat_policy.js +106 -0
  106. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  107. package/dist/scripts/condense.js +32 -25
  108. package/docs/MIGRATION.md +49 -1
  109. package/docs/contracts/settings-classes.md +1 -1
  110. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
  111. package/llms.txt +14 -14
  112. package/package.json +7 -4
  113. package/src/agent-src/scripts/_cli_entry.ts +51 -0
  114. package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
  115. package/src/agent-src/scripts/roadmap_gates.ts +4 -20
  116. package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
  117. package/src/agent-src/scripts/stubs_due.ts +4 -18
  118. package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
  119. package/src/config/continuity-surface.json +509 -0
  120. package/src/config/estate-count-budget.json +1 -1
  121. package/src/config/gate-coverage.yml +29 -8
  122. package/src/config/gate-violation-baselines.json +8 -8
  123. package/src/config/gitignore-block.txt +19 -0
  124. package/src/config/host-injection-effect.json +73 -0
  125. package/src/config/packed-binary-manifest.json +33 -0
  126. package/src/config/preamble-payload-budget.json +112 -105
  127. package/src/config/publish-surface.json +4 -2
  128. package/src/config/release-gate-locality.yml +4 -3
  129. package/src/install/claudeLayerCarriage.ts +273 -0
  130. package/src/install/claudePathsPlan.ts +76 -0
  131. package/src/install/hostLayerFingerprint.ts +1 -1
  132. package/src/install/partitionEligibility.ts +261 -127
  133. package/src/install/ruleLayerPartition.ts +19 -23
  134. package/src/scripts/_dispatch.bash +77 -27
  135. package/src/scripts/_lib/auto_dispatch.ts +2 -2
  136. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  137. package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
  138. package/src/scripts/_lib/continuity_surface.ts +296 -0
  139. package/src/scripts/_lib/corpus_manifest.ts +26 -9
  140. package/src/scripts/_lib/hook_settings.ts +1 -1
  141. package/src/scripts/_lib/host_capability.ts +38 -1
  142. package/src/scripts/_lib/injection_effect.ts +151 -0
  143. package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
  144. package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
  145. package/src/scripts/_lib/pin_resolver.ts +13 -0
  146. package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
  147. package/src/scripts/_lib/release_highlights.ts +19 -211
  148. package/src/scripts/_lib/release_material.ts +27 -77
  149. package/src/scripts/_lib/retired_status.ts +74 -0
  150. package/src/scripts/_lib/skill_catalogue.ts +156 -21
  151. package/src/scripts/_lib/structured_ask.ts +117 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +117 -8
  153. package/src/scripts/_lib/tty_prompt.ts +76 -0
  154. package/src/scripts/ai_council/api_transport.ts +168 -0
  155. package/src/scripts/ai_council/clients.ts +58 -63
  156. package/src/scripts/ai_council/config.ts +13 -0
  157. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  158. package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
  159. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  160. package/src/scripts/ai_council/prompts.ts +9 -0
  161. package/src/scripts/ai_council/seat_policy.ts +137 -0
  162. package/src/scripts/ai_council/status_surface.ts +79 -0
  163. package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
  164. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  165. package/src/scripts/ai_tells_rules.ts +446 -5
  166. package/src/scripts/ask_block_census.ts +499 -0
  167. package/src/scripts/bench_humanizer_eval.ts +264 -40
  168. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  169. package/src/scripts/capture_skill_catalogue.ts +24 -11
  170. package/src/scripts/check_branch_work_committed.ts +234 -0
  171. package/src/scripts/check_candidate_lines.ts +499 -0
  172. package/src/scripts/check_continuity_surface.ts +475 -0
  173. package/src/scripts/check_dependency_floors.ts +13 -6
  174. package/src/scripts/check_estate_count.ts +40 -79
  175. package/src/scripts/check_pack_size.ts +263 -24
  176. package/src/scripts/check_release_highlights.ts +0 -157
  177. package/src/scripts/check_roadmap_trackable.ts +19 -16
  178. package/src/scripts/check_rule_layer_partition.ts +54 -34
  179. package/src/scripts/check_single_delivery.ts +97 -31
  180. package/src/scripts/check_update_banner.ts +13 -0
  181. package/src/scripts/code_graph/build.ts +138 -15
  182. package/src/scripts/code_graph/cli.ts +76 -8
  183. package/src/scripts/code_graph/extract.ts +11 -0
  184. package/src/scripts/code_graph/grammar_probe.ts +43 -0
  185. package/src/scripts/code_graph/loader.ts +88 -14
  186. package/src/scripts/code_graph/query.ts +121 -11
  187. package/src/scripts/code_graph/resolution_tiers.ts +238 -0
  188. package/src/scripts/code_graph/sqlite_store.ts +247 -6
  189. package/src/scripts/code_graph/types.ts +66 -1
  190. package/src/scripts/code_graph/validate.ts +23 -0
  191. package/src/scripts/condense.ts +34 -22
  192. package/src/scripts/corpus_manifest.ts +5 -5
  193. package/src/scripts/council_cli.ts +29 -39
  194. package/src/scripts/count_intent_disagreement.ts +426 -0
  195. package/src/scripts/detect_ai_tells.ts +142 -20
  196. package/src/scripts/handoff_context_hook.ts +79 -0
  197. package/src/scripts/harvest_reference_tokens.ts +698 -0
  198. package/src/scripts/hook_effect_doctor.ts +82 -1
  199. package/src/scripts/hook_manifest.json +1 -1
  200. package/src/scripts/hook_manifest.yaml +94 -12
  201. package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
  202. package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
  203. package/src/scripts/hooks/concern_registry.ts +6 -2
  204. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  205. package/src/scripts/hooks/host_semantics.ts +1 -1
  206. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  207. package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
  208. package/src/scripts/hooks/rule_inject_hook.ts +1 -1
  209. package/src/scripts/hooks/skill_route_hook.ts +11 -4
  210. package/src/scripts/inbox_source_census.ts +322 -0
  211. package/src/scripts/install-hooks.sh +134 -0
  212. package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
  213. package/src/scripts/lint_plan_risk_register.ts +13 -17
  214. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  215. package/src/scripts/mcp_server/suggest_skill.ts +72 -0
  216. package/src/scripts/mcp_server/tools.ts +2 -56
  217. package/src/scripts/media/lib/adapter-contract.md +7 -2
  218. package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
  219. package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
  220. package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
  221. package/src/scripts/migrate_command_suggestions.ts +1 -1
  222. package/src/scripts/probe_unblocked_ask.ts +135 -2
  223. package/src/scripts/rdp_candidates_delta.ts +276 -0
  224. package/src/scripts/rdp_quality_eval.ts +217 -19
  225. package/src/scripts/release.ts +26 -71
  226. package/src/scripts/release_drill.ts +6 -10
  227. package/src/scripts/release_env.ts +1 -0
  228. package/src/scripts/release_mix_taxonomy.json +3 -2
  229. package/src/scripts/release_publication.ts +34 -29
  230. package/src/scripts/report_host_injection_effect.ts +189 -0
  231. package/src/scripts/report_layer_overlap.ts +41 -0
  232. package/src/scripts/report_skill_menu_census.ts +341 -0
  233. package/src/scripts/self_review_gate.ts +409 -15
  234. package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
  235. package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
  236. package/src/vendor/grammars/README.md +61 -0
  237. package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
  238. package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
  239. package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
  240. package/src/scripts/_lib/carrier_status.ts +0 -118
  241. package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
  242. package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * PreToolUse guard: one question per structured-ask call.
4
+ *
5
+ * road-to-asked-not-parked 5.1. `ask-when-uncertain`'s Iron Law is one
6
+ * question per turn, and `user-interaction` § question pacing narrows what
7
+ * "one" means: one decision point, answerable with one reply. A host's native
8
+ * question picker makes it cheap to violate that — a payload can carry N
9
+ * questions in one call, and the user then answers a form instead of a
10
+ * question.
11
+ *
12
+ * DENY, NEVER TRUNCATE. Silently keeping the first question and dropping the
13
+ * rest would hide exactly the decisions the user was owed; the deny message
14
+ * names the count so the caller can split the ask instead of guessing what
15
+ * happened.
16
+ *
17
+ * WHAT IT SEES, honestly. `_lib/structured_ask.ts` carries no observed
18
+ * per-host tool, so this guard matches on a name SHAPE and is filtered by the
19
+ * manifest's per-concern `tools:` key to the same candidate names. On every
20
+ * host measured today it therefore fires on nothing — the concern exists so
21
+ * that the first host to ship a picker meets the rule already in force, not
22
+ * because a violation has been observed.
23
+ *
24
+ * ENFORCEMENT REACH, also honestly. `fail_closed: false` and the exit-1 deny
25
+ * are only a deny where the host both binds `pre_tool_use` AND honours the
26
+ * dispatcher's verdict. `hook-architecture-v1.md` § Which hosts carry
27
+ * pre_tool_use is the table; `agent-config hooks:status` answers it for the
28
+ * host actually running. This file claims no more than that.
29
+ *
30
+ * A payload whose shape is not recognised at all (`countStructuredAskQuestions`
31
+ * returns -1) is ALLOWED. A guard that blocks what it cannot parse would turn
32
+ * an unknown picker shape into a wedged session, and the failure it guards
33
+ * against — too many questions — is a shape defect, not a safety one.
34
+ *
35
+ * Exit codes (docs/contracts/hook-architecture-v1.md):
36
+ * 0 — allow
37
+ * 1 — block (the call carries more than one question)
38
+ */
39
+ import { fileURLToPath, pathToFileURL } from 'node:url';
40
+
41
+ import { countStructuredAskQuestions, isStructuredAskTool } from '../_lib/structured_ask.js';
42
+ import { readHookStdin } from './hook_stdin.js';
43
+
44
+ const _HERE = fileURLToPath(import.meta.url);
45
+
46
+ type JsonObject = Record<string, unknown>;
47
+
48
+ function _isObject(v: unknown): v is JsonObject {
49
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
50
+ }
51
+
52
+ export interface AskCall {
53
+ readonly tool: string;
54
+ readonly questions: number;
55
+ }
56
+
57
+ /** Read (tool name, question count) off a PreToolUse envelope. */
58
+ export function extractAskCall(envelope: JsonObject): AskCall {
59
+ const payload = _isObject(envelope['payload']) ? envelope['payload'] : envelope;
60
+ const nameVal =
61
+ payload['tool_name'] ??
62
+ payload['toolName'] ??
63
+ payload['tool'] ??
64
+ envelope['tool_name'] ??
65
+ envelope['tool'];
66
+ const tool = typeof nameVal === 'string' ? nameVal : '';
67
+ const ti = _isObject(payload['tool_input'])
68
+ ? payload['tool_input']
69
+ : _isObject(envelope['tool_input'])
70
+ ? envelope['tool_input']
71
+ : null;
72
+ return { tool, questions: ti === null ? -1 : countStructuredAskQuestions(ti) };
73
+ }
74
+
75
+ /** The verdict, separated from I/O so the fixtures exercise the decision itself. */
76
+ export function verdict(call: AskCall): { block: boolean; reason: string } {
77
+ if (call.tool === '' || !isStructuredAskTool(call.tool)) {
78
+ return { block: false, reason: 'not a structured-ask call' };
79
+ }
80
+ if (call.questions < 0) {
81
+ return { block: false, reason: 'payload shape not recognised — allowed, never guessed' };
82
+ }
83
+ if (call.questions <= 1) {
84
+ return { block: false, reason: 'one question' };
85
+ }
86
+ return {
87
+ block: true,
88
+ reason:
89
+ `${call.tool} carries ${String(call.questions)} questions in one call. ` +
90
+ 'One decision per ask: put the first question, wait for the answer, then ' +
91
+ 'the next. The call is denied rather than truncated — dropping the extra ' +
92
+ 'questions would hide the decisions the user is owed.',
93
+ };
94
+ }
95
+
96
+ export function main(argv?: string[]): number {
97
+ const args = argv ?? process.argv.slice(2);
98
+ for (const a of args) {
99
+ if (a === '--self-test') {
100
+ return selfTest();
101
+ }
102
+ }
103
+ const raw = readHookStdin();
104
+ if (!raw.trim()) {
105
+ return 0;
106
+ }
107
+ let envelope: JsonObject;
108
+ try {
109
+ const parsed: unknown = JSON.parse(raw);
110
+ envelope = _isObject(parsed) ? parsed : {};
111
+ } catch {
112
+ return 0;
113
+ }
114
+ const v = verdict(extractAskCall(envelope));
115
+ if (v.block) {
116
+ process.stderr.write(`one-question-per-ask: BLOCKED — ${v.reason}\n`);
117
+ return 1;
118
+ }
119
+ return 0;
120
+ }
121
+
122
+ export function selfTest(): number {
123
+ const cases: Array<[string, JsonObject, boolean]> = [
124
+ [
125
+ 'two questions in one call is denied',
126
+ { tool_name: 'AskUserQuestion', tool_input: { questions: [{}, {}] } },
127
+ true,
128
+ ],
129
+ [
130
+ 'four questions is denied',
131
+ { tool_name: 'AskUserQuestion', tool_input: { questions: [{}, {}, {}, {}] } },
132
+ true,
133
+ ],
134
+ [
135
+ 'one question passes',
136
+ { tool_name: 'AskUserQuestion', tool_input: { questions: [{}] } },
137
+ false,
138
+ ],
139
+ [
140
+ 'a single-question field passes',
141
+ { tool_name: 'ask_user_question', tool_input: { question: 'which one?' } },
142
+ false,
143
+ ],
144
+ [
145
+ 'an empty ask passes — malformed, but not this guard\'s failure',
146
+ { tool_name: 'AskUserQuestion', tool_input: { questions: [] } },
147
+ false,
148
+ ],
149
+ [
150
+ 'an unrecognised payload shape passes rather than wedging the session',
151
+ { tool_name: 'AskUserQuestion', tool_input: { something: 'else' } },
152
+ false,
153
+ ],
154
+ ['an ordinary tool is never seen', { tool_name: 'Bash', tool_input: { command: 'ls' } }, false],
155
+ [
156
+ 'a Write with two questions in its body is not an ask call',
157
+ { tool_name: 'Write', tool_input: { questions: [{}, {}] } },
158
+ false,
159
+ ],
160
+ ['an empty envelope passes', {}, false],
161
+ ];
162
+ let failed = 0;
163
+ for (const [name, envelope, want] of cases) {
164
+ const got = verdict(extractAskCall(envelope)).block;
165
+ const ok = got === want;
166
+ process.stdout.write(
167
+ `${ok ? '✅' : '❌'} ${name} — block=${String(got)}` +
168
+ (ok ? '\n' : ` (wanted ${String(want)})\n`),
169
+ );
170
+ if (!ok) failed += 1;
171
+ }
172
+ const positives = cases.filter((c) => c[2]).length;
173
+ if (positives < 2) {
174
+ process.stderr.write(
175
+ '❌ one_question_per_ask --self-test: fewer than 2 deny cases — a guard never ' +
176
+ 'seen refuse has unknown sensitivity.\n',
177
+ );
178
+ return 1;
179
+ }
180
+ process.stdout.write(
181
+ `\none_question_per_ask --self-test: ${String(cases.length - failed)}/${String(cases.length)} ` +
182
+ `case(s) behaved (${String(positives)} deny, ${String(cases.length - positives)} allow)\n`,
183
+ );
184
+ return failed > 0 ? 1 : 0;
185
+ }
186
+
187
+ if (process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href) {
188
+ process.exit(main());
189
+ }
190
+
191
+ export { _HERE };
@@ -16,7 +16,7 @@
16
16
  * Fires only on an `Agent` / `Task` tool-use completion (the two names this
17
17
  * repo has observed for the subagent-dispatch tool across hosts/versions).
18
18
  * Every other tool call is a silent no-op — same tool-filter discipline as
19
- * `code-graph-nudge` (`tools: [Agent, Task]` in the manifest entry keeps the
19
+ * `code-graph-context` (`tools: [Agent, Task]` in the manifest entry keeps the
20
20
  * dispatcher from paying this concern's cost on unrelated calls).
21
21
  *
22
22
  * Sync vs async (road-to-orchestrator-first-execution measured: 326 of 370
@@ -89,7 +89,7 @@ export const DISPATCH_TOOL_NAMES: ReadonlySet<string> = new Set(['Agent', 'Task'
89
89
  * Unwrap the dispatcher envelope (`{schema_version, platform, event,
90
90
  * payload}`) down to the platform-native payload. Falls back to the
91
91
  * top-level object for direct/legacy invocation (same shape both
92
- * `pr_url_reminder_hook.ts` and `code_graph_nudge_hook.ts` handle).
92
+ * `pr_url_reminder_hook.ts` and `code_graph_context_hook.ts` handle).
93
93
  */
94
94
  export function unwrapPayload(envelope: JsonObject): JsonObject {
95
95
  const inner = envelope['payload'];
@@ -34,7 +34,7 @@
34
34
  * NEVER BLOCKS. Every failure path returns 0: unreadable stdin, malformed JSON,
35
35
  * missing router, unreadable body, unwritable state. The one non-zero exit is
36
36
  * the host's advisory context channel (exit 2 + `decision: "warn"`), the same
37
- * channel `ui-route-nudge` and `code-graph-nudge` already use on `pre_tool_use`
37
+ * channel `ui-route-nudge` and `code-graph-context` already use on `pre_tool_use`
38
38
  * with `severity: advisory` — a warn there is an injection, not a deny.
39
39
  *
40
40
  * HOST BOUNDARY, STATED RATHER THAN IMPLIED. `user_prompt_submit` and
@@ -140,7 +140,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
140
140
 
141
141
  import { readHookStdin } from "./hook_stdin.js";
142
142
  import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
143
- import { OBSERVATION_LOG, readObservationLog, resolveSkillsRoot } from "../_lib/skill_catalogue.js";
143
+ import { OBSERVATION_LOG, readObservationLog, resolveSkillCatalogueRoots } from "../_lib/skill_catalogue.js";
144
144
  import { knownBareNames } from "../_lib/skill_catalogue_series.js";
145
145
  import { _tokenize, rank, type RankRow } from "../skill_tools/score_skill_relevance.js";
146
146
 
@@ -252,13 +252,20 @@ export interface RouteDecision {
252
252
  */
253
253
  export function routeDecision(
254
254
  prompt: string,
255
- skillsDir: string | null,
255
+ // One root, or every readable root. `null` and `[]` are the same answer — no
256
+ // catalogue was read — and both must stay silent rather than rank nothing and
257
+ // call that a result.
258
+ skillsDir: string | readonly string[] | null,
256
259
  bareProvider?: () => Set<string> | null,
257
260
  ): RouteDecision {
258
261
  const silent: RouteDecision = { rows: [], suppressed: 0 };
259
262
  if (skillsDir === null) return silent;
263
+ if (Array.isArray(skillsDir) && skillsDir.length === 0) return silent;
260
264
  // Denominator floor BEFORE the catalogue read: a prompt too short to score
261
- // meaningfully cannot produce a pointer worth 337 file reads either.
265
+ // meaningfully cannot produce a pointer worth the read either. The count is
266
+ // deliberately not stated — it used to say "337 file reads", which was one
267
+ // root; the union reads every readable root and the figure depends on the
268
+ // machine's install.
262
269
  try {
263
270
  if (_tokenize(prompt).size < MIN_TASK_TERMS) return silent;
264
271
  } catch {
@@ -346,7 +353,7 @@ export function main(): number {
346
353
  const root = _workspaceRoot(env);
347
354
  const platform = env["platform"];
348
355
  const host = typeof platform === "string" && platform ? platform : null;
349
- const { rows, suppressed } = routeDecision(prompt, resolveSkillsRoot(root), () =>
356
+ const { rows, suppressed } = routeDecision(prompt, resolveSkillCatalogueRoots(root), () =>
350
357
  knownBareForHost(root, host),
351
358
  );
352
359
  if (rows.length === 0) return EXIT_ALLOW; // silence is the default
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * inbox_source_census — the denominator an inbox run cannot shrink by reading badly.
4
+ *
5
+ * THE GAP THIS CLOSES.
6
+ *
7
+ * `/analyze:inbox` already ends with a **point ledger**: points extracted in its
8
+ * Phase 3 against points dispositioned in Phases 4–5b, per bucket, as counts
9
+ * that must balance. That ledger proves the run answered what it read. It
10
+ * cannot prove the run read what arrived — a point nobody extracted appears in
11
+ * neither column, and the ledger balances perfectly without it. Under-extraction
12
+ * is invisible to the only completeness check the command has.
13
+ *
14
+ * Measured shape of the input that exposed it: round `inbox-2026-09-r` arrived
15
+ * as 12 topic folders, 67 files and ~55,800 lines, where one folder alone held
16
+ * six revisions of a plan beside a 58 KB transcript. Read per file, the
17
+ * revisions look like six sources; read as a set, they are one argument whose
18
+ * later revisions silently drop items the earlier ones carried.
19
+ *
20
+ * WHAT THIS COUNTS — AND WHAT IT DELIBERATELY DOES NOT.
21
+ *
22
+ * Only what is **mechanical and genre-independent**:
23
+ *
24
+ * 1. **Source sets** — files grouped by stem, so a revision family
25
+ * (`x.v1/x.v2`, `x.consolidated.v1`, `x-loop-1`, `x-2026-09-06`) reports as
26
+ * ONE set with its members ordered, instead of as N unrelated files.
27
+ * 2. **Anchors** — the four countable structures a source can carry: turn
28
+ * separators, headings, checkbox steps, and numbered items. Every class is
29
+ * counted on every file; the run picks the ones its genre uses.
30
+ * 3. **Decision ids** — the `D2` / `E1` / `K7` tokens a source argues in. If
31
+ * those do not appear in what the run emits, the emitted plan is
32
+ * undecidable by construction and no reader can tell, because the ids left
33
+ * with the source.
34
+ *
35
+ * It does NOT guess genre, judge relevance, or extract demands. Those are the
36
+ * reading, and the whole point of a denominator is that it is produced without
37
+ * one — a census that needed judgement would be as shrinkable as the reading it
38
+ * is supposed to bound.
39
+ *
40
+ * HONEST LIMIT.
41
+ *
42
+ * An anchor count is a floor on what must be accounted for, never a target and
43
+ * never a quality measure. A 400-anchor source can carry three demands and a
44
+ * 12-anchor transcript can carry twelve. What the number buys is that a run
45
+ * reporting "41 anchors → 41 accounted" can be challenged by anyone holding the
46
+ * folder, where a prose summary cannot.
47
+ *
48
+ * USAGE.
49
+ *
50
+ * ./scripts-run src/scripts/inbox_source_census --root agents/tmp/<round-id>
51
+ * ./scripts-run src/scripts/inbox_source_census --root <dir> --json
52
+ *
53
+ * Exit 2 when the root is missing, unreadable, or enumerates zero files — a
54
+ * census over an empty set is vacuous and must not read as coverage.
55
+ */
56
+
57
+ import * as fs from 'node:fs';
58
+ import * as path from 'node:path';
59
+ import { fileURLToPath, pathToFileURL } from 'node:url';
60
+
61
+ const _HERE = fileURLToPath(import.meta.url);
62
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
63
+
64
+ /** Extensions worth counting. Binaries and archives carry no anchors. */
65
+ const TEXT_EXT = new Set(['.md', '.txt', '.markdown', '.mdx']);
66
+
67
+ /**
68
+ * Version markers that make two files revisions of one source rather than two
69
+ * sources. Ordered most-specific first: `consolidated.v2` must not be stripped
70
+ * to `consolidated` by the bare `.vN` rule.
71
+ */
72
+ const VERSION_MARKERS: readonly RegExp[] = [
73
+ /[.-]consolidated[.-]v\d+$/i,
74
+ /[.-]consolidated$/i,
75
+ /[.-]loop[.-]?\d+.*$/i,
76
+ /[.-]v\d+$/i,
77
+ /[.-]\d{4}-\d{2}-\d{2}$/,
78
+ /[.-](final|draft|deep|master)$/i,
79
+ ];
80
+
81
+ export interface AnchorCounts {
82
+ /** `----`-style turn separators — the transcript spine. */
83
+ separators: number;
84
+ /** ATX headings, h1 through h4. */
85
+ headings: number;
86
+ /** `- [ ]` / `- [x]` / `- [~]` / `- [-]` roadmap steps. */
87
+ steps: number;
88
+ /** `1.`-style numbered items and `**bold lead-in**` paragraph openers. */
89
+ enumerated: number;
90
+ }
91
+
92
+ export interface FileCensus {
93
+ file: string;
94
+ bytes: number;
95
+ lines: number;
96
+ anchors: AnchorCounts;
97
+ /** Sum of the four anchor classes — the file's floor. */
98
+ total: number;
99
+ }
100
+
101
+ export interface SourceSet {
102
+ /** The shared stem the members were grouped under. */
103
+ stem: string;
104
+ /** Members, oldest-looking first (lexical on the version marker). */
105
+ members: FileCensus[];
106
+ /** True when more than one member shares the stem. */
107
+ isRevisionSet: boolean;
108
+ }
109
+
110
+ export interface TopicCensus {
111
+ topic: string;
112
+ sets: SourceSet[];
113
+ decisionIds: string[];
114
+ fileCount: number;
115
+ anchorTotal: number;
116
+ }
117
+
118
+ /**
119
+ * Strip a trailing version marker to the stem two revisions share.
120
+ *
121
+ * Applied repeatedly, most-specific first, so `road-to-x.consolidated.v3`
122
+ * reduces through `road-to-x.consolidated` to `road-to-x` — otherwise a
123
+ * `.consolidated` family and a plain `.vN` family of the same plan report as
124
+ * two sets and the diff obligation never fires on the pair.
125
+ */
126
+ export function stemOf(filename: string): string {
127
+ let s = filename.replace(/\.(md|markdown|mdx|txt)$/i, '');
128
+ let changed = true;
129
+ while (changed) {
130
+ changed = false;
131
+ for (const re of VERSION_MARKERS) {
132
+ const next = s.replace(re, '');
133
+ if (next !== s && next.length > 0) {
134
+ s = next;
135
+ changed = true;
136
+ break;
137
+ }
138
+ }
139
+ }
140
+ return s;
141
+ }
142
+
143
+ /** Count the four anchor classes. Fenced code is skipped — a fence is content. */
144
+ export function countAnchors(text: string): AnchorCounts {
145
+ const out: AnchorCounts = { separators: 0, headings: 0, steps: 0, enumerated: 0 };
146
+ let inFence = false;
147
+ for (const raw of text.split(/\r?\n/)) {
148
+ const line = raw.trimEnd();
149
+ if (/^\s*(```|~~~)/.test(line)) {
150
+ inFence = !inFence;
151
+ continue;
152
+ }
153
+ if (inFence) continue;
154
+ if (/^-{4,}$|^={4,}$|^\*{4,}$/.test(line.trim())) {
155
+ out.separators += 1;
156
+ continue;
157
+ }
158
+ if (/^#{1,4} \S/.test(line)) {
159
+ out.headings += 1;
160
+ continue;
161
+ }
162
+ if (/^\s*[-*] \[[ xX~-]\]/.test(line)) {
163
+ out.steps += 1;
164
+ continue;
165
+ }
166
+ if (/^\s*\d+[.)] \S/.test(line) || /^\*\*[^*]+\*\*/.test(line)) {
167
+ out.enumerated += 1;
168
+ }
169
+ }
170
+ return out;
171
+ }
172
+
173
+ /**
174
+ * Decision / open-question ids the source argues in — `D2`, `E1`, `K7`, `AC3`.
175
+ *
176
+ * Deliberately narrow: one to three capitals followed by one or two digits, as
177
+ * a whole token. A wider pattern matches version strings, HTTP codes and every
178
+ * `P0` in ordinary prose, and a noisy id set is worse than none — the run would
179
+ * stop reading the row.
180
+ */
181
+ export function decisionIds(text: string): string[] {
182
+ const hits = text.match(/\b[A-Z]{1,3}\d{1,2}\b/g) ?? [];
183
+ return [...new Set(hits)].sort();
184
+ }
185
+
186
+ function censusFile(abs: string, rel: string): FileCensus {
187
+ const text = fs.readFileSync(abs, 'utf-8');
188
+ const anchors = countAnchors(text);
189
+ return {
190
+ file: rel,
191
+ bytes: Buffer.byteLength(text),
192
+ lines: text.split(/\r?\n/).length,
193
+ anchors,
194
+ total: anchors.separators + anchors.headings + anchors.steps + anchors.enumerated,
195
+ };
196
+ }
197
+
198
+ /** Group one topic folder's files into source sets and count its anchors. */
199
+ export function censusTopic(dir: string, topic: string): TopicCensus {
200
+ const names = fs
201
+ .readdirSync(dir, { withFileTypes: true })
202
+ .filter((d) => d.isFile() && TEXT_EXT.has(path.extname(d.name).toLowerCase()))
203
+ .map((d) => d.name)
204
+ .sort();
205
+
206
+ const byStem = new Map<string, FileCensus[]>();
207
+ const ids = new Set<string>();
208
+ for (const name of names) {
209
+ const abs = path.join(dir, name);
210
+ const fc = censusFile(abs, name);
211
+ for (const id of decisionIds(fs.readFileSync(abs, 'utf-8'))) ids.add(id);
212
+ const stem = stemOf(name);
213
+ const bucket = byStem.get(stem);
214
+ if (bucket) bucket.push(fc);
215
+ else byStem.set(stem, [fc]);
216
+ }
217
+
218
+ const sets: SourceSet[] = [...byStem.entries()]
219
+ .map(([stem, members]) => ({
220
+ stem,
221
+ members: members.sort((a, b) => a.file.localeCompare(b.file)),
222
+ isRevisionSet: members.length > 1,
223
+ }))
224
+ .sort((a, b) => a.stem.localeCompare(b.stem));
225
+
226
+ return {
227
+ topic,
228
+ sets,
229
+ decisionIds: [...ids].sort(),
230
+ fileCount: names.length,
231
+ anchorTotal: sets.reduce(
232
+ (n, s) => n + s.members.reduce((m, f) => m + f.total, 0),
233
+ 0,
234
+ ),
235
+ };
236
+ }
237
+
238
+ /** Walk a round root: each immediate subdirectory is a topic, plus the root itself. */
239
+ export function censusRoot(root: string): TopicCensus[] {
240
+ const out: TopicCensus[] = [];
241
+ const rootLevel = censusTopic(root, '.');
242
+ if (rootLevel.fileCount > 0) out.push(rootLevel);
243
+ for (const d of fs.readdirSync(root, { withFileTypes: true })) {
244
+ if (!d.isDirectory()) continue;
245
+ const t = censusTopic(path.join(root, d.name), d.name);
246
+ if (t.fileCount > 0) out.push(t);
247
+ }
248
+ return out;
249
+ }
250
+
251
+ function render(topics: TopicCensus[], write: (s: string) => unknown): void {
252
+ let files = 0;
253
+ let anchors = 0;
254
+ let revisionSets = 0;
255
+ for (const t of topics) {
256
+ write(`\n## ${t.topic}\n`);
257
+ for (const s of t.sets) {
258
+ const tag = s.isRevisionSet ? ` — REVISION SET (${String(s.members.length)}), diff consecutive members` : '';
259
+ write(` ${s.stem}${tag}\n`);
260
+ for (const f of s.members) {
261
+ const a = f.anchors;
262
+ write(
263
+ ` ${f.file} ${String(f.bytes)}B/${String(f.lines)}L ` +
264
+ `sep ${String(a.separators)} · head ${String(a.headings)} · ` +
265
+ `step ${String(a.steps)} · enum ${String(a.enumerated)} = ${String(f.total)}\n`,
266
+ );
267
+ }
268
+ if (s.isRevisionSet) revisionSets += 1;
269
+ }
270
+ if (t.decisionIds.length > 0) write(` decision ids: ${t.decisionIds.join(' ')}\n`);
271
+ files += t.fileCount;
272
+ anchors += t.anchorTotal;
273
+ }
274
+ write(
275
+ `\ntopics ${String(topics.length)} · files ${String(files)} · ` +
276
+ `revision sets ${String(revisionSets)} · anchors ${String(anchors)}\n` +
277
+ 'Every anchor leaves the run carrying a ledger row or named in one `no-demand` line.\n' +
278
+ 'Every revision set owes a consecutive-member diff (/analyze:inbox Phase 2b).\n',
279
+ );
280
+ }
281
+
282
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
283
+ const rootIdx = argv.indexOf('--root');
284
+ const rootArg = rootIdx === -1 ? undefined : argv[rootIdx + 1];
285
+ if (rootArg === undefined) {
286
+ process.stderr.write('usage: inbox_source_census --root <dir> [--json]\n');
287
+ return 2;
288
+ }
289
+ const root = path.resolve(REPO_ROOT, rootArg);
290
+ let stat: fs.Stats;
291
+ try {
292
+ stat = fs.statSync(root);
293
+ } catch {
294
+ process.stderr.write(`❌ inbox_source_census: root not readable: ${root}\n`);
295
+ return 2;
296
+ }
297
+ if (!stat.isDirectory()) {
298
+ process.stderr.write(`❌ inbox_source_census: root is not a directory: ${root}\n`);
299
+ return 2;
300
+ }
301
+
302
+ const topics = censusRoot(root);
303
+ const files = topics.reduce((n, t) => n + t.fileCount, 0);
304
+ if (files === 0) {
305
+ process.stderr.write(
306
+ `❌ inbox_source_census: 0 text file(s) under ${root} — a census over an ` +
307
+ 'empty set is vacuous and must not read as coverage.\n',
308
+ );
309
+ return 2;
310
+ }
311
+
312
+ if (argv.includes('--json')) {
313
+ process.stdout.write(`${JSON.stringify({ root, topics }, null, 2)}\n`);
314
+ return 0;
315
+ }
316
+ render(topics, (s) => process.stdout.write(s));
317
+ return 0;
318
+ }
319
+
320
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
321
+ process.exit(main());
322
+ }