@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
@@ -9,10 +9,10 @@
9
9
  *
10
10
  * Corpus: ready (non-draft) roadmap files directly under `agents/roadmaps/` code-comment-allow provenance-comment -- the path is this script's operand, not where the code came from
11
11
  * (top level only — `archive/`, `skipped/`, `later/`, `stubs/` excluded, as
12
- * are `template.md` and `dashboard*`). `status: draft` and `status: carrier`
13
- * frontmatter exempt a file, each reported under its own name (`draft-exempt`
14
- * / `carrier-exempt`) and still counted as scanned — one label for two states
15
- * made the gate's own output name a state it had not acted on.
12
+ * are `template.md` and `dashboard*`). `status: draft` frontmatter exempts a
13
+ * file, reported as `draft-exempt` and still counted as scanned. A second
14
+ * exempting status, `carrier`, was deleted by ADR-262: a receiver of deferred
15
+ * work is an ordinary plan and carries a real risk register like any other.
16
16
  *
17
17
  * Checks per ready file:
18
18
  * 1. `## Risk Register` section exists — missing = fail unless the
@@ -329,20 +329,16 @@ export function validateTable(
329
329
  }
330
330
 
331
331
  /** A frontmatter status that exempts the file, or `null` when none does. */
332
- export type ExemptingStatus = 'draft' | 'carrier';
332
+ export type ExemptingStatus = 'draft';
333
333
 
334
334
  /**
335
335
  * Which frontmatter status exempts this file, per contract § 1.
336
336
  *
337
- * `draft` and `carrier` both exempt, and WHICH one is returned rather than a
338
- * bare boolean: the previous predicate answered "is this exempt" and the gate
339
- * then printed `draft-exempt` for a carrier, so its own output named a state it
340
- * had not acted on.
341
- *
342
- * A carrier is not a plan. It holds obligations deferred out of an archived
343
- * parent, each with an unmet resumption trigger, so a plan risk register for it
344
- * would be manufactured rather than reported. Same category reason that exempts
345
- * it from `check_roadmap_trackable`'s `## Phase` requirement.
337
+ * The status is returned rather than a bare boolean so the gate's own output
338
+ * names the state it acted on. That mattered while `carrier` was a second
339
+ * exempting value and the predicate printed `draft-exempt` for it; ADR-262
340
+ * deleted that value, and the shape is kept so a future exemption is one enum
341
+ * member rather than a re-argued boolean.
346
342
  */
347
343
  export function exemptingStatus(text: string): ExemptingStatus | null {
348
344
  const lines = _splitLines(text);
@@ -350,7 +346,7 @@ export function exemptingStatus(text: string): ExemptingStatus | null {
350
346
  for (let i = 1; i < lines.length; i++) {
351
347
  const line = lines[i] as string;
352
348
  if (line.trim() === '---') return null;
353
- const m = /^status:\s*(draft|carrier)\s*$/.exec(line);
349
+ const m = /^status:\s*(draft)\s*$/.exec(line);
354
350
  if (m !== null) return m[1] as ExemptingStatus;
355
351
  }
356
352
  return null;
@@ -592,7 +588,7 @@ function _blobAt(absPath: string, sha: string): string | null {
592
588
  return _git(root, ['show', `${sha}:${rel}`]);
593
589
  }
594
590
 
595
- export type FileStatus = 'ok' | 'draft-exempt' | 'carrier-exempt' | 'grandfathered' | 'fail';
591
+ export type FileStatus = 'ok' | 'draft-exempt' | 'grandfathered' | 'fail';
596
592
 
597
593
  export interface FileResult {
598
594
  file: string;
@@ -879,7 +875,7 @@ export function main(argv?: readonly string[]): number {
879
875
  for (const p of targets) {
880
876
  const result = checkFile(p);
881
877
  scanned += 1;
882
- if (result.status === 'draft-exempt' || result.status === 'carrier-exempt') {
878
+ if (result.status === 'draft-exempt') {
883
879
  statusLines.push(` ${result.status}: ${p}`);
884
880
  } else if (result.status === 'grandfathered') {
885
881
  statusLines.push(` grandfathered: ${p}`);
@@ -14,6 +14,19 @@
14
14
  * (`- **Run:**`). A gate that claims to be executable without naming the
15
15
  * command is the same defect as a decision with no option set: it reads
16
16
  * as actionable and is not.
17
+ * 4. An annotation pointing at a USER-DECISION blocker (one whose `Owner:`
18
+ * is the maintainer, the user, or the owner) carries an `asked:` field
19
+ * recording whether the question was put and, when it was not, why —
20
+ * `<!-- blocked-by: <id> | asked: no — <reason> -->`. A decision only the
21
+ * user can make, filed in a roadmap without ever being put, is the defect
22
+ * road-to-asked-not-parked exists to stop; without this field a marker
23
+ * cannot distinguish a declined decision from an unoffered one.
24
+ *
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.
17
30
  *
18
31
  * Fenced code blocks are stripped before scanning so a roadmap that shows
19
32
  * the `## Blockers` shape as a documentation example is not flagged.
@@ -42,7 +55,19 @@ const BLOCKER_HEADING_RE = /^###[ \t]+blocker:[ \t]*(.+?)[ \t]*$/gim;
42
55
  // wrapped continuation line or inline-code documentation example of the
43
56
  // syntax (as this very lint script's own roadmap step describes it) must
44
57
  // not be mistaken for a live cross-reference.
45
- const BLOCKED_BY_LINE_RE = /^-[ \t]*\[[ xX~-]\].*<!--[ \t]*blocked-by:[ \t]*([a-z0-9-]+)[ \t]*-->/i;
58
+ const BLOCKED_BY_LINE_RE =
59
+ /^-[ \t]*\[[ xX~-]\].*<!--[ \t]*blocked-by:[ \t]*([a-z0-9-]+)[ \t]*(?:\|[ \t]*asked:[ \t]*(yes|no)[ \t]*(?:[\u2014-][ \t]*([^>]*?))?[ \t]*)?-->/i;
60
+
61
+ /**
62
+ * The owners whose blockers are the USER-DECISION class.
63
+ *
64
+ * Read off `Owner:` rather than guessed from prose: `terminal-states.md`
65
+ * defines `blocked` to include "a decision only the user can make", and the
66
+ * owner field is the only place a roadmap says which blockers those are.
67
+ * `council`, `implementer` and `agent` are deliberately outside the class —
68
+ * those are not decisions the user is waiting to be asked.
69
+ */
70
+ const USER_DECISION_OWNER_RE = /^-[ \t]*\*\*Owner:\*\*[ \t]*(maintainer|user|owner)\b/im;
46
71
 
47
72
  const REQUIRED_FIELDS: ReadonlyArray<readonly [string, RegExp]> = [
48
73
  ['Status', /^-[ \t]*\*\*Status:\*\*/im],
@@ -154,6 +179,7 @@ function _scanBoth(rawText: string): ScanResult {
154
179
  const decidability: Violation[] = [];
155
180
  const text = _stripFencedCode(rawText);
156
181
  const declaredIds = new Set<string>();
182
+ const userDecisionIds = new Set<string>();
157
183
 
158
184
  const sectionMatch = BLOCKERS_SECTION_RE.exec(text);
159
185
  if (sectionMatch) {
@@ -181,6 +207,9 @@ function _scanBoth(rawText: string): ScanResult {
181
207
  declaredIds.add(cur.id);
182
208
  const bodyEnd = i + 1 < heads.length ? (heads[i + 1] as { start: number }).start : section.length;
183
209
  const body = section.slice(cur.end, bodyEnd);
210
+ if (USER_DECISION_OWNER_RE.test(body)) {
211
+ userDecisionIds.add(cur.id);
212
+ }
184
213
  const missing = REQUIRED_FIELDS.filter(([, re]) => !re.test(body)).map(([name]) => name);
185
214
  if (missing.length) {
186
215
  violations.push({
@@ -256,6 +285,27 @@ function _scanBoth(rawText: string): ScanResult {
256
285
  `(no matching '### blocker: ${id}' in this file)`,
257
286
  });
258
287
  }
288
+ if (userDecisionIds.has(id)) {
289
+ const asked = m[2] === undefined ? null : (m[2] as string).toLowerCase();
290
+ const reason = ((m[3] as string | undefined) ?? '').trim();
291
+ if (asked === null) {
292
+ violations.push({
293
+ line: i + 1,
294
+ message:
295
+ `blocked-by '${id}' is a user-decision blocker and carries no ` +
296
+ 'asked: field — write `| asked: yes` when the question was put, ' +
297
+ 'or `| asked: no — <reason>` when it was not',
298
+ });
299
+ } else if (asked === 'no' && reason === '') {
300
+ violations.push({
301
+ line: i + 1,
302
+ message:
303
+ `blocked-by '${id}' says asked: no with no reason — a marker that ` +
304
+ 'does not say why the question was never put is the parking lot ' +
305
+ 'this field exists to end',
306
+ });
307
+ }
308
+ }
259
309
  }
260
310
  violations.sort((a, b) => a.line - b.line);
261
311
  decidability.sort((a, b) => a.line - b.line);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The `suggest_skill_for_task` MCP handler, extracted from `tools.ts`.
3
+ *
4
+ * `tools.ts` sits ~580 lines past the 1,500-line ceiling `check_source_size_budget`
5
+ * enforces as a shrink-only ratchet, so every line there costs one unit of excess
6
+ * and the gate names re-pinning the baseline a defect rather than a fix. Adding a
7
+ * `skills_roots` field to this handler's answer cost four; extracting the handler
8
+ * pays that back many times over and puts it in a file where its prose is free.
9
+ *
10
+ * Cohesive on its own terms too: this is the one handler in the allowlist that
11
+ * owns a ranking policy rather than reading a file, and it is the only consumer
12
+ * of the skill-catalogue resolver on the server side.
13
+ */
14
+
15
+ /**
16
+ * `suggest_skill_for_task` — the recovery path for a truncated catalogue.
17
+ *
18
+ * Promoted from a discovery stub because the defect it answers is measured, not
19
+ * hypothetical: on the one host that publishes its own truncation, a default
20
+ * install had 402 entries dropped from the model-visible skills list — the
21
+ * host's own count against its own denominator, not a subtraction over ours.
22
+ * An agent
23
+ * that cannot see a skill also cannot ask for it by name, so the only reachable
24
+ * question is "what fits this task" — which is what the deterministic ranker
25
+ * answers, over the TREE rather than over the host's catalogue.
26
+ *
27
+ * Read-only and shell-free by construction: it reads SKILL.md frontmatter and
28
+ * returns names, scores and declared personas. No skill BODY is returned — the
29
+ * caller opens what it picked, so a wrong rank costs one read rather than a
30
+ * context payload.
31
+ */
32
+ export async function suggestSkillForTask(
33
+ args: Record<string, unknown>,
34
+ consumerRoot: string,
35
+ ): Promise<Record<string, unknown>> {
36
+ const task = typeof args['task'] === 'string' ? args['task'] : '';
37
+ if (task.trim() === '') {
38
+ return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
39
+ }
40
+ // `limit`, not `top` — the stub published `limit` and a consumer may have
41
+ // read that contract already. Renaming it on promotion would break a
42
+ // caller for cosmetic reasons.
43
+ const limitRaw = args['limit'];
44
+ const limit =
45
+ typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
46
+
47
+ const catalogue = await import('../_lib/skill_catalogue.js');
48
+ // EVERY readable root — see `resolveSkillCatalogueRoots` on why not the first.
49
+ const roots = catalogue.resolveSkillCatalogueRoots(consumerRoot);
50
+ if (roots.length === 0) {
51
+ // A missing catalogue is UNKNOWN, never "no skill fits this task". The
52
+ // second reads as a ranked answer and would be a confident empty list —
53
+ // the same zero-from-silence failure `capture_skill_catalogue` refuses.
54
+ return {
55
+ status: 'no_catalogue',
56
+ searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
57
+ consumer_root: consumerRoot,
58
+ suggestions: [],
59
+ };
60
+ }
61
+
62
+ const { rank } = await import('../skill_tools/score_skill_relevance.js');
63
+ const rows = rank(task, roots).slice(0, Math.max(1, limit));
64
+ return {
65
+ status: 'ok',
66
+ // `skills_root` kept verbatim (a consumer may read it); `skills_roots` is
67
+ // the additive truth about what was actually ranked.
68
+ skills_root: roots[0] as string,
69
+ skills_roots: roots,
70
+ suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
71
+ };
72
+ }
@@ -47,6 +47,7 @@ import {
47
47
  slice_around as _chSliceAround,
48
48
  read_header as _chReadHeaderImpl,
49
49
  } from '../chat_history.js';
50
+ import { suggestSkillForTask } from './suggest_skill.js';
50
51
  import {
51
52
  type CatalogEntry,
52
53
  install_hint as _catalog_install_hint,
@@ -1210,61 +1211,6 @@ async function _readResourceBodyHandler(
1210
1211
  // unlisted tool. Boot-time stderr log enumerates the registered set.
1211
1212
  // ---------------------------------------------------------------------
1212
1213
 
1213
- /**
1214
- * `suggest_skill_for_task` — the recovery path for a truncated catalogue.
1215
- *
1216
- * Promoted from a discovery stub because the defect it answers is measured, not
1217
- * hypothetical: on the one host that publishes its own truncation, a default
1218
- * install had 402 entries dropped from the model-visible skills list — the
1219
- * host's own count against its own denominator, not a subtraction over ours
1220
- * (`agents/evidence/analysis/scoped-projection-host-delivery.md`). An agent
1221
- * that cannot see a skill also cannot ask for it by name, so the only reachable
1222
- * question is "what fits this task" — which is what the deterministic ranker
1223
- * answers, over the TREE rather than over the host's catalogue.
1224
- *
1225
- * Read-only and shell-free by construction: it reads SKILL.md frontmatter and
1226
- * returns names, scores and declared personas. No skill BODY is returned — the
1227
- * caller opens what it picked, so a wrong rank costs one read rather than a
1228
- * context payload.
1229
- */
1230
- async function _suggestSkillForTaskHandler(
1231
- args: Record<string, unknown>,
1232
- consumerRoot: string,
1233
- ): Promise<Record<string, unknown>> {
1234
- const task = typeof args['task'] === 'string' ? args['task'] : '';
1235
- if (task.trim() === '') {
1236
- return { status: 'error', error: 'task must be a non-empty string', suggestions: [] };
1237
- }
1238
- // `limit`, not `top` — the stub published `limit` and a consumer may have
1239
- // read that contract already. Renaming it on promotion would break a
1240
- // caller for cosmetic reasons.
1241
- const limitRaw = args['limit'];
1242
- const limit =
1243
- typeof limitRaw === 'number' && Number.isFinite(limitRaw) ? Math.floor(limitRaw) : 5;
1244
-
1245
- const catalogue = await import('../_lib/skill_catalogue.js');
1246
- const skillsDir = catalogue.resolveSkillsRoot(consumerRoot);
1247
- if (skillsDir === null) {
1248
- // A missing catalogue is UNKNOWN, never "no skill fits this task". The
1249
- // second reads as a ranked answer and would be a confident empty list —
1250
- // the same zero-from-silence failure `capture_skill_catalogue` refuses.
1251
- return {
1252
- status: 'no_catalogue',
1253
- searched: [...catalogue.DEFAULT_CATALOGUE_ROOTS],
1254
- consumer_root: consumerRoot,
1255
- suggestions: [],
1256
- };
1257
- }
1258
-
1259
- const { rank } = await import('../skill_tools/score_skill_relevance.js');
1260
- const rows = rank(task, skillsDir).slice(0, Math.max(1, limit));
1261
- return {
1262
- status: 'ok',
1263
- skills_root: skillsDir,
1264
- suggestions: rows.map(([name, score, personas]) => ({ skill: name, score, personas })),
1265
- };
1266
- }
1267
-
1268
1214
  export const ALLOWLIST: Record<string, BuiltinTool> = {
1269
1215
  suggest_skill_for_task: {
1270
1216
  name: 'suggest_skill_for_task',
@@ -1296,7 +1242,7 @@ export const ALLOWLIST: Record<string, BuiltinTool> = {
1296
1242
  },
1297
1243
  additionalProperties: false,
1298
1244
  },
1299
- handler: _suggestSkillForTaskHandler,
1245
+ handler: suggestSkillForTask,
1300
1246
  },
1301
1247
  lint_skills: {
1302
1248
  name: 'lint_skills',
@@ -355,8 +355,13 @@ the explicit `dry-run` subcommand:
355
355
  `scripts/media/lib/fixtures/<adapter-id>/`.
356
356
  - Exit code 0.
357
357
 
358
- Fixtures are committed and cover one happy path per adapter.
359
- Phase 6 golden runs assert byte-identical stdout under
358
+ Fixtures are committed and cover one happy path per adapter;
359
+ committed media assets are **non-decodable ASCII placeholders**
360
+ whose documented paths and presence, rather than encoded-media
361
+ validity, form the fixture contract. The placeholder literal is
362
+ `FIXTURE-<adapter-id>-<ext>`. A test that needs decodable media
363
+ generates it at runtime — it must not read a fixture expecting
364
+ one. Phase 6 golden runs assert byte-identical stdout under
360
365
  `AIV_DRYRUN=true`.
361
366
 
362
367
  ## Logging & redaction
@@ -73,7 +73,7 @@ export const ELIGIBLE: Record<string, [string, string]> = {
73
73
  'audit my agent docs, check the state of the agents/ directory',
74
74
  'stale files under agents/ or recent edits to .augment/ without doc updates',
75
75
  ],
76
- 'analyze-reference-repo': [
76
+ 'analyze-repo': [
77
77
  "look at how X does this, compare with that other repo, study this competitor's approach",
78
78
  'external repo URL or path mentioned in the prompt',
79
79
  ],
@@ -47,18 +47,40 @@
47
47
  * Both edges are printed. A reader who cannot see the bracket cannot judge the
48
48
  * number.
49
49
  *
50
+ * The `form` dimension (road-to-asked-not-parked 1.1)
51
+ *
52
+ * Every unblocked ask is additionally partitioned by the FORM the ask took:
53
+ *
54
+ * - `native` — the same assistant turn carries a tool call whose name is a
55
+ * structured-ask tool (`_lib/structured_ask.ts`, `isStructuredAskTool`).
56
+ * - `text` — everything else, i.e. the ask was prose.
57
+ *
58
+ * The two are exhaustive and disjoint by construction, so `text + native`
59
+ * always equals `unblockedAsks`. That identity is asserted in `--self-test`
60
+ * rather than left to a reader to check.
61
+ *
62
+ * Expect `native` to be **0** on every corpus this repo can scan today: no host
63
+ * in the capability registry carries an observed structured-ask tool, and
64
+ * `structured_ask.ts` states why matching on a name-shape pattern is not a claim
65
+ * that any host ships one. A measured zero and an uninstrumented zero look the
66
+ * same in a report and are not the same fact — this axis is what makes them
67
+ * distinguishable later.
68
+ *
50
69
  * Usage:
51
70
  * ./scripts-run src/scripts/probe_unblocked_ask [--limit N] [--store PATH] [--json]
52
71
  * ./scripts-run src/scripts/probe_unblocked_ask --self-test
72
+ * ./scripts-run src/scripts/probe_unblocked_ask --help
53
73
  *
54
74
  * Exit codes: 0 always — a measurement, not a gate. `--self-test` exits 1 when
55
75
  * the detector fails to fire on its fixtures.
56
76
  */
57
77
  import fs from 'node:fs';
78
+ import os from 'node:os';
58
79
  import path from 'node:path';
59
80
  import { pathToFileURL } from 'node:url';
60
81
 
61
82
  import { defaultStore, HARNESS_TEXT, isInjectedBody } from './conformance_scan.js';
83
+ import { isStructuredAskTool } from './_lib/structured_ask.js';
62
84
  import { isSyntheticPrompt } from './_lib/prompt_shape.js';
63
85
 
64
86
  /** A numbered-options block — the surface `check_reply_consistency` already reads. */
@@ -88,6 +110,8 @@ interface Turn {
88
110
  role: 'user' | 'assistant';
89
111
  text: string;
90
112
  at: string;
113
+ /** Tool names invoked in this turn — empty for a user turn. */
114
+ tools: string[];
91
115
  }
92
116
 
93
117
  function turns(lines: string[]): Turn[] {
@@ -116,10 +140,16 @@ function turns(lines: string[]): Turn[] {
116
140
  .map((b: { text?: string }) => b.text ?? '')
117
141
  .join('\n')
118
142
  : '';
143
+ const tools = Array.isArray(c)
144
+ ? c
145
+ .filter((b: { type?: string }) => b?.type === 'tool_use')
146
+ .map((b: { name?: string }) => String(b?.name ?? ''))
147
+ .filter((n: string) => n !== '')
148
+ : [];
119
149
  if (!text.trim()) continue;
120
150
  if (role === 'assistant' && HARNESS_TEXT.test(text.trim())) continue;
121
151
  if (role === 'user' && (isSyntheticPrompt(text) || isInjectedBody(text))) continue;
122
- out.push({ role, text, at: String(e['timestamp'] ?? '') });
152
+ out.push({ role, text, at: String(e['timestamp'] ?? ''), tools });
123
153
  }
124
154
  return out;
125
155
  }
@@ -139,16 +169,39 @@ export function classifyTail(tail: string): AskVerdict {
139
169
  return { unblockedAsk: true, malformed: !RECOMMENDATION.test(tail) };
140
170
  }
141
171
 
172
+ /** The ask FORM partition. Exhaustive and disjoint: `text + native === unblockedAsks`. */
173
+ export interface FormBreakdown {
174
+ text: number;
175
+ native: number;
176
+ }
177
+
178
+ /**
179
+ * Which FORM an ask took. `native` iff the turn carried a structured-ask tool
180
+ * call; `text` otherwise. Exported so the partition is one function rather than
181
+ * an inline condition a later edit can quietly make non-exhaustive.
182
+ */
183
+ export function askForm(tools: readonly string[]): 'text' | 'native' {
184
+ return tools.some((n) => isStructuredAskTool(n)) ? 'native' : 'text';
185
+ }
186
+
142
187
  export interface Result {
143
188
  handbacks: number;
144
189
  withBlock: number;
145
190
  unblockedAsks: number;
146
191
  malformed: number;
192
+ form: FormBreakdown;
147
193
  samples: { session: string; at: string; span: string }[];
148
194
  }
149
195
 
150
196
  export function measure(store: string, limit: number): Result {
151
- const r: Result = { handbacks: 0, withBlock: 0, unblockedAsks: 0, malformed: 0, samples: [] };
197
+ const r: Result = {
198
+ handbacks: 0,
199
+ withBlock: 0,
200
+ unblockedAsks: 0,
201
+ malformed: 0,
202
+ form: { text: 0, native: 0 },
203
+ samples: [],
204
+ };
152
205
  let files: string[];
153
206
  try {
154
207
  files = fs.readdirSync(store).filter((f) => f.endsWith('.jsonl'));
@@ -179,6 +232,7 @@ export function measure(store: string, limit: number): Result {
179
232
  const v = classifyTail(tail);
180
233
  if (!v.unblockedAsk) continue;
181
234
  r.unblockedAsks += 1;
235
+ r.form[askForm(t.tools)] += 1;
182
236
  if (v.malformed) {
183
237
  r.malformed += 1;
184
238
  if (r.samples.length < 5) {
@@ -221,6 +275,52 @@ export function selfTest(): number {
221
275
  );
222
276
  if (!ok) failed += 1;
223
277
  }
278
+ for (const [tools, want] of [
279
+ [[], 'text'],
280
+ [['Bash', 'Read'], 'text'],
281
+ [['AskUserQuestion'], 'native'],
282
+ [['ask_user_question'], 'native'],
283
+ ] as Array<[string[], 'text' | 'native']>) {
284
+ const got = askForm(tools);
285
+ const ok = got === want;
286
+ process.stdout.write(
287
+ `${ok ? '✅' : '❌'} form of [${tools.join(', ')}] — ${got}` +
288
+ (ok ? '\n' : ` (wanted ${want})\n`),
289
+ );
290
+ if (!ok) failed += 1;
291
+ }
292
+ const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'probe-ask-form-'));
293
+ try {
294
+ const mk = (role: string, blocks: unknown): string =>
295
+ JSON.stringify({ type: role, timestamp: '2026-01-01T00:00:00Z', message: { role, content: blocks } });
296
+ fs.writeFileSync(
297
+ path.join(fx, 'fixture.jsonl'),
298
+ [
299
+ mk('assistant', [{ type: 'text', text: 'Done.\n\nSoll ich das so umsetzen?' }]),
300
+ mk('user', 'ja'),
301
+ mk('assistant', [
302
+ { type: 'text', text: 'Done.\n\nShall I take the second half too?' },
303
+ { type: 'tool_use', name: 'AskUserQuestion' },
304
+ ]),
305
+ mk('user', 'ja'),
306
+ ].join('\n'),
307
+ 'utf8',
308
+ );
309
+ const r = measure(fx, 10);
310
+ const sums = r.form.text + r.form.native === r.unblockedAsks;
311
+ process.stdout.write(
312
+ `${sums ? '✅' : '❌'} form partition is exhaustive — text ${String(r.form.text)} + ` +
313
+ `native ${String(r.form.native)} = unblocked ${String(r.unblockedAsks)}\n`,
314
+ );
315
+ if (!sums) failed += 1;
316
+ const split = r.form.text === 1 && r.form.native === 1;
317
+ process.stdout.write(
318
+ `${split ? '✅' : '❌'} the fixture's two asks split one per class\n`,
319
+ );
320
+ if (!split) failed += 1;
321
+ } finally {
322
+ fs.rmSync(fx, { recursive: true, force: true });
323
+ }
224
324
  const positives = cases.filter((c) => c[3]).length;
225
325
  if (positives < 3) {
226
326
  process.stderr.write('❌ probe_unblocked_ask --self-test: fewer than 3 positive cases — a truncated suite must fail.\n');
@@ -233,6 +333,32 @@ export function selfTest(): number {
233
333
  return failed > 0 ? 1 : 0;
234
334
  }
235
335
 
336
+ const HELP = `probe_unblocked_ask — the unblocked-ask rate, and the FORM each ask took.
337
+
338
+ Usage:
339
+ probe_unblocked_ask [--limit N] [--store PATH] [--json]
340
+ probe_unblocked_ask --self-test
341
+ probe_unblocked_ask --help
342
+
343
+ Dimensions reported:
344
+ hand-back turns assistant turns immediately followed by a user turn
345
+ unblocked asks …that hand a decision with no numbered-options block
346
+ malformed …of those, carrying no Recommendation:/Empfehlung: label
347
+ form the ask FORM partition of the unblocked asks:
348
+ native the turn carried a structured-ask tool call
349
+ text the ask was prose
350
+ text + native always equals the unblocked-ask total.
351
+ native is 0 wherever no host has an observed structured-ask
352
+ tool — a measured zero, not an uninstrumented one.
353
+
354
+ Options:
355
+ --limit N most recent N sessions (default 60)
356
+ --store PATH transcript store (default: the host's own)
357
+ --json emit the Result object instead of the report
358
+ --self-test run the detector fixtures, incl. the form-partition identity
359
+ --help this text
360
+ `;
361
+
236
362
  export function main(argv: string[]): number {
237
363
  let limit = 60;
238
364
  let storeArg: string | null = null;
@@ -246,6 +372,10 @@ export function main(argv: string[]): number {
246
372
  // in-process, and resolving a transcript store that may not exist here
247
373
  // would make the sensitivity proof depend on the host having one.
248
374
  else if (a === '--self-test') return selfTest();
375
+ else if (a === '--help' || a === '-h') {
376
+ process.stdout.write(HELP);
377
+ return 0;
378
+ }
249
379
  else {
250
380
  process.stderr.write(`probe_unblocked_ask: unrecognized argument: ${a}\n`);
251
381
  return 0;
@@ -264,6 +394,9 @@ export function main(argv: string[]): number {
264
394
  ` …with a numbered block ${String(r.withBlock)} (excluded — check_reply_consistency reads that surface)\n` +
265
395
  ` …unblocked asks ${String(r.unblockedAsks)} ${pct(r.unblockedAsks, r.handbacks)} of hand-backs\n` +
266
396
  ` …of those, malformed ${String(r.malformed)} ${pct(r.malformed, r.unblockedAsks)} of unblocked asks\n` +
397
+ ` form: text ${String(r.form.text)} ${pct(r.form.text, r.unblockedAsks)} of unblocked asks\n` +
398
+ ` form: native ${String(r.form.native)} ${pct(r.form.native, r.unblockedAsks)} of unblocked asks\n` +
399
+ ` form: text + native ${String(r.form.text + r.form.native)} (must equal unblocked asks: ${String(r.unblockedAsks)})\n` +
267
400
  '\n CEILING, not a point estimate: "hands a decision" is a heuristic and a\n' +
268
401
  ' rhetorical question can match it. A recommendation is matched on its LABEL,\n' +
269
402
  ' which is the rule\'s own bar but a bar on form.\n',