@event4u/agent-config 9.27.0 → 9.29.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,484 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Delegation nudge — `user_prompt_submit` concern (F3-lite, road-to-orchestrator-
4
+ * discipline-carriers Phase 4).
5
+ *
6
+ * Deterministic, no-LLM carrier for the `delegation-policy` rule's "decompose
7
+ * and dispatch" obligation. `classifyTask` (`_lib/auto_dispatch.ts`) has zero
8
+ * production callers today — its only importer is the `routing:doctor`
9
+ * diagnostic — so the delegate-by-default rule never actually sees a real
10
+ * prompt. This concern is the missing signal producer: it extracts cheap,
11
+ * regex-only structural signals from the SUBMITTED prompt text (enumerated
12
+ * file lists, "for each"/"alle …" shapes, explicit slice counts, ordered-plan
13
+ * markers, multi-deliverable conjunctions), feeds them to the judgment
14
+ * ladder (`_lib/judgment_ladder.ts`, which wraps `classifyTask` — see the
15
+ * JUDGMENT-LADDER CITATION note below) together with the REAL activation
16
+ * gate (the emergency incident switch +
17
+ * the host-capability manifest, resolved via `probeHostCapabilities(hostId)`
18
+ * — the F5 committed registry merged with observable environment facts;
19
+ * always-on orchestration removed the `subagents.host_capabilities` settings
20
+ * override — the same source `routing_doctor.collect_orchestration` uses),
21
+ * and — only on a positive
22
+ * `do-in-parallel` / `do-in-steps` verdict — injects
23
+ * ONE line naming the mode, the slice count, and a tier recommendation for
24
+ * the slices (`resolveSubagentRouting`'s first production caller, applied
25
+ * with a conservative "lite, downshifted" default).
26
+ *
27
+ * ANTI-CANARY CONDITION (roadmap 4.2). Silence is the default: no enumerated
28
+ * signal → no injection, not even a "nothing to delegate here" line. This is
29
+ * deliberate — a per-turn reminder that fires on every turn is exactly the
30
+ * canary shape that measured a 24/29 miss rate; this concern only ever speaks
31
+ * when it has a concrete, falsifiable verdict to report.
32
+ *
33
+ * JUDGMENT-LADDER CITATION (road-to-always-on-orchestration Phase 2.4). The
34
+ * verdict now comes from `_lib/judgment_ladder.ts::classifyLadder` (which
35
+ * WRAPS `classifyTask` rather than replacing it) instead of calling
36
+ * `classifyTask` directly, and the injected line names the resolved rung
37
+ * (`rung-2: dispatch 6 lite slices …`). This concern stays scoped to the
38
+ * SUBAGENT dispatch rungs (1/2) — a rung-0 script verdict, a rung-3/4
39
+ * team/council escalation, and the silent ∅ (ask/in-session) verdicts all
40
+ * resolve to silence here exactly as before this concern cited the ladder
41
+ * at all; team and council get their own carrier once Phase 3/4 give them
42
+ * one.
43
+ *
44
+ * DELIVERY PATH — fixed to mirror `language_mirror_hook.ts` (found while
45
+ * building this concern; the original `{decision:"allow", context:…}` shape
46
+ * below is superseded). `context_blocks` forwarding in `dispatch_hook.ts` is
47
+ * restricted to the `session_start` event by the code's own comment
48
+ * ("session_start context forwarding … All other events keep the swallow-
49
+ * stdout contract unchanged"). Empirically verified against the real
50
+ * dispatcher (`dispatch_entry.ts`, platform `claude`): a `user_prompt_submit`
51
+ * concern that returns `{decision:"allow", context: "…"}` at exit 0 produces
52
+ * ZERO stdout from the dispatcher — the `context` field is silently dropped,
53
+ * because `host_semantics.emitFor` returns `{exit:0, stdout:"", stderr:""}`
54
+ * unconditionally for severity `"allow"` (derived from the concern's own exit
55
+ * code, which is 0 here). The ONLY path that survives to
56
+ * `hookSpecificOutput.additionalContext` on `user_prompt_submit` is a concern
57
+ * that reports `reason`/`additional_context` and an exit code that reduces to
58
+ * `"warn"` (2) — `language_mirror_hook.ts`'s shipped pattern. This concern now
59
+ * follows that pattern exactly: `{decision:"warn", reason, additional_context}`
60
+ * at exit `EXIT_WARN` (2).
61
+ *
62
+ * WHY EXIT 2 NEVER BLOCKS. `host_semantics.emitFor`'s `severity === "warn"`
63
+ * branch returns `{exit: 0, stdout: claudeAdditionalContext(event, reason)}`
64
+ * UNCONDITIONALLY — it does not consult `CLAUDE_BLOCK_CAPABLE_EVENTS` at all
65
+ * (that check only gates the `"block"` severity branch). So on the verified
66
+ * `claude` platform the real process exit handed back to the host is always
67
+ * 0 for a warn verdict, on `user_prompt_submit` exactly as on every other
68
+ * event — the internal `EXIT_WARN` is dispatcher-internal bookkeeping, never
69
+ * the byte that reaches Claude Code. This is the same mechanism that makes
70
+ * `language_mirror_hook.ts` (which returns the same exit code on the same
71
+ * slot, on effectively every prompt carrying language markers) run in
72
+ * production today without ever blocking a turn.
73
+ *
74
+ * PLATFORM SCOPE — bound only on `claude` in the manifest (not the full
75
+ * `language-mirror` platform list). `host_semantics.VERIFIED_PLATFORMS`
76
+ * covers only `claude`, so `claude` is the one platform where the "never
77
+ * blocks" claim above is verified against documented, testable behaviour.
78
+ * `cowork` is DELIBERATELY NOT bound (F10, review) — `scripts/hooks/
79
+ * cowork-dispatcher.sh` discards the dispatcher's exit code and stdout
80
+ * unconditionally (`>/dev/null 2>&1 || true; exit 0`), and this concern's
81
+ * entire delivery depends on `additional_context` reaching the model via
82
+ * stdout at the verified exit-2 warn path — a delivery that trampoline makes
83
+ * structurally impossible regardless of platform. Binding it there would be
84
+ * dead weight: the concern still runs (settings read, classifier,
85
+ * activation resolution), still costs the dispatch, and its entire output
86
+ * is thrown away for nothing. Cursor / Cline / Windsurf / Gemini are
87
+ * deliberately NOT added here either: their trampolines were not inspected
88
+ * for the same discard property, and extending the exit-2 pattern to an
89
+ * unverified propagation path is exactly the speculative mapping
90
+ * `host_semantics.ts`'s own header calls out as "the same class of bug this
91
+ * module exists to remove". `language-mirror`'s existing broader binding is
92
+ * a pre-existing fact this change does not touch or relitigate.
93
+ */
94
+ import fs from "node:fs";
95
+ import path from "node:path";
96
+ import { fileURLToPath, pathToFileURL } from "node:url";
97
+ import { readHookStdin } from "./hook_stdin.js";
98
+ import { load_agent_settings } from "../_lib/agent_settings.js";
99
+ import { probeHostCapabilities } from "../_lib/host_capability.js";
100
+ import type { ActivationInputs, Classification, TaskSignals } from "../_lib/auto_dispatch.js";
101
+ import { classifyLadder, type LadderRung } from "../_lib/judgment_ladder.js";
102
+ import { resolveSubagentRouting, type Tier } from "../_lib/subagent_routing.js";
103
+ import { isSyntheticPrompt } from "../_lib/prompt_shape.js";
104
+
105
+ const EXIT_ALLOW = 0;
106
+ // Severity is taken from the EXIT CODE, not from the `decision` field in the
107
+ // stdout payload — mirrors `language_mirror_hook.ts`'s own note (found the
108
+ // same way: by tracing delivery, not by re-reading the unit tests). A warn
109
+ // verdict is reported at exit 2 so `host_semantics.emitFor` reduces it to
110
+ // severity `"warn"` and forwards `additional_context`; see the file header
111
+ // for why this exit code never actually blocks the turn.
112
+ const EXIT_WARN = 2;
113
+
114
+ type JsonValue = string | number | boolean | null | JsonValue[] | { [k: string]: JsonValue };
115
+ type JsonObject = { [k: string]: JsonValue };
116
+
117
+ function _isObject(v: unknown): v is JsonObject {
118
+ return typeof v === "object" && v !== null && !Array.isArray(v);
119
+ }
120
+
121
+ // ── Signal extraction (cheap, regex-only — no LLM call) ──────────────────
122
+
123
+ /**
124
+ * Enumerated file-like tokens: backtick-quoted or bare `name.ext` shapes.
125
+ * Counts UNIQUE matches — "a.ts a.ts" is one file mentioned twice, not two.
126
+ */
127
+ const FILE_TOKEN_RE =
128
+ /`?\b[\w][\w.\-]{0,80}\.(ts|tsx|js|jsx|mjs|cjs|py|php|rb|go|rs|java|kt|swift|md|mdx|json|ya?ml|css|scss|less|html|vue|svelte|sql|sh|bash|c|cpp|h|hpp|cs|toml|xml)\b`?/gi;
129
+
130
+ export function detectEnumeratedFiles(text: string): number {
131
+ const matches = text.match(FILE_TOKEN_RE) ?? [];
132
+ const unique = new Set(matches.map((m) => m.replace(/`/g, "").toLowerCase()));
133
+ return unique.size;
134
+ }
135
+
136
+ /**
137
+ * Explicit slice counts: "6 files", "3 modules", "5 Dateien" — a number
138
+ * immediately followed by a plural noun the classifier recognises as a
139
+ * countable unit of work.
140
+ */
141
+ const EXPLICIT_COUNT_RE =
142
+ /\b(\d+)\s+(files?|modules?|components?|endpoints?|scripts?|skills?|rules?|tickets?|tests?|screens?|pages?|repos?|services?|classes?|functions?|methods?|slices?|tasks?|items?|targets?|routes?|controllers?|Dateien|Module|Tests|Komponenten|Skripte|Regeln|Tickets|Aufgaben|Klassen|Funktionen|Endpunkte)\b/i;
143
+
144
+ export function detectExplicitSliceCount(text: string): number | null {
145
+ const m = EXPLICIT_COUNT_RE.exec(text);
146
+ if (!m) return null;
147
+ const n = Number.parseInt(m[1] ?? "", 10);
148
+ return Number.isFinite(n) && n > 0 ? n : null;
149
+ }
150
+
151
+ /**
152
+ * "for each" / "for every" / "alle …" / "jede(n|s|r) …" — a same-shape-repeated
153
+ * signal with no explicit count attached. Contributes a minimal slice count
154
+ * (2) when nothing more specific was found, since the shape itself implies
155
+ * "more than one" without saying how many.
156
+ */
157
+ const FOR_EACH_RE =
158
+ /\bfor\s+each\b|\bfor\s+every\b|\beach\s+of\s+the\b|\bevery\s+one\s+of\b|\ball(?:e|en)\b|\bjed(?:e|er|es|en)\b|\bjeweils\b/i;
159
+
160
+ export function detectForEachShape(text: string): boolean {
161
+ return FOR_EACH_RE.test(text);
162
+ }
163
+
164
+ /**
165
+ * Multi-deliverable conjunction: "X, Y, and Z" / "X, Y, und Z" — an
166
+ * Oxford-comma-style list of three or more items closed by "and"/"und"
167
+ * immediately after the LAST comma in a sentence. Deliberately requires the
168
+ * comma before the conjunction (>= 2 commas total): a looser, single-comma
169
+ * match ("A, B and C") would also match a stray "<item>, and <note>" aside
170
+ * that is not an enumeration at all. Known gap (documented, not fixed
171
+ * here): a German list that skips the comma before "und" ("Docs, Changelog
172
+ * und README") has only 1 comma and is NOT detected by this function —
173
+ * `detectForEachShape`'s "alle"/"jede…" patterns are the German-first signal
174
+ * for that shape instead.
175
+ */
176
+ export function detectMultiDeliverableConjunction(text: string): number {
177
+ for (const sentence of text.split(/[.!?\n]+/)) {
178
+ const commaCount = (sentence.match(/,/g) ?? []).length;
179
+ if (commaCount < 2) continue;
180
+ const tail = sentence.slice(sentence.lastIndexOf(",") + 1);
181
+ if (/^\s*(and|und)\b/i.test(tail)) {
182
+ return commaCount + 1;
183
+ }
184
+ }
185
+ return 0;
186
+ }
187
+
188
+ /**
189
+ * Ordered-plan markers: "Step 1"/"Schritt 2", or lines opening with `1. `/`2)`.
190
+ * Two or more distinct markers is treated as an explicit ordered plan — one
191
+ * numbered reference alone ("see step 3") is not a plan shape.
192
+ */
193
+ const STEP_WORD_RE = /\b(?:step|schritt)\s*\d+\b/gi;
194
+ const STEP_LINE_RE = /^\s*\d+[.)]\s+\S/gm;
195
+
196
+ /**
197
+ * Minimum bare numbered-list lines ("1. … \n2. …") before the LINE shape
198
+ * counts as an ordered plan (F7, review): a 2-line numbered list is common,
199
+ * ordinary prose structure ("1. rename a.ts\n2. rename b.ts" is not itself
200
+ * an enumerated work plan) and was firing as a false positive at the
201
+ * previous threshold of 2. The explicit "Step N"/"Schritt N" WORD marker
202
+ * (`STEP_WORD_RE`) is a much stronger, author-stated signal even at 2 — an
203
+ * author who writes "Step 1 … Step 2 …" is naming a plan on purpose — and
204
+ * keeps its own, lower threshold.
205
+ */
206
+ const ORDERED_LINE_FLOOR = 3;
207
+ const ORDERED_WORD_FLOOR = 2;
208
+
209
+ export function detectOrderedPlan(text: string): { ordered: boolean; stepCount: number } {
210
+ const wordMatches = text.match(STEP_WORD_RE) ?? [];
211
+ const lineMatches = text.match(STEP_LINE_RE) ?? [];
212
+ const ordered = wordMatches.length >= ORDERED_WORD_FLOOR || lineMatches.length >= ORDERED_LINE_FLOOR;
213
+ const count = Math.max(wordMatches.length, lineMatches.length);
214
+ return { ordered, stepCount: count };
215
+ }
216
+
217
+ export interface ExtractedSignals {
218
+ signals: TaskSignals;
219
+ /** Slice count to surface in the injected line, matching whichever shape fired. */
220
+ sliceCountForLine: number;
221
+ }
222
+
223
+ /**
224
+ * Turn a raw submitted-prompt string into `TaskSignals` for `classifyTask`.
225
+ * Pure and total — never throws; an unparseable/empty prompt resolves to
226
+ * "no signal" (`size_estimate: 0`), which the size floor alone already
227
+ * routes to `in-session`.
228
+ */
229
+ export function extractTaskSignals(text: string): ExtractedSignals {
230
+ const ordered = detectOrderedPlan(text);
231
+ if (ordered.ordered) {
232
+ return {
233
+ signals: {
234
+ parallelizable: null,
235
+ ordered_plan: true,
236
+ independent_slices: 0,
237
+ size_estimate: Math.max(ordered.stepCount, 2),
238
+ },
239
+ sliceCountForLine: ordered.stepCount,
240
+ };
241
+ }
242
+
243
+ const fileCount = detectEnumeratedFiles(text);
244
+ const explicit = detectExplicitSliceCount(text) ?? 0;
245
+ const conjunction = detectMultiDeliverableConjunction(text);
246
+ const forEach = detectForEachShape(text);
247
+
248
+ // F7 (review): a bare file-token count below 3 is common and NOT itself
249
+ // evidence of independent, parallelizable work — "rename a.ts to b.ts"
250
+ // names 2 files but is one mechanical edit, not two delegable slices. The
251
+ // explicit "N files/Dateien" phrase (`detectExplicitSliceCount`) is a
252
+ // stronger, author-stated signal and keeps its existing floor of N>=2.
253
+ const fileSignal = fileCount >= 3 ? fileCount : 0;
254
+
255
+ let slices = Math.max(fileSignal, explicit, conjunction);
256
+ if (slices === 0 && forEach) {
257
+ slices = 2;
258
+ }
259
+
260
+ const parallelizable: TaskSignals["parallelizable"] =
261
+ fileSignal >= 3 ? "files" : forEach ? "independent" : null;
262
+
263
+ return {
264
+ signals: {
265
+ parallelizable,
266
+ ordered_plan: false,
267
+ independent_slices: slices,
268
+ size_estimate: slices > 0 ? Math.max(slices, 2) : 0,
269
+ },
270
+ sliceCountForLine: slices,
271
+ };
272
+ }
273
+
274
+ // ── Activation resolution (mirrors routing_doctor.collect_orchestration) ──
275
+
276
+ /**
277
+ * `hostId` is the caller-supplied platform identifier (the envelope's own
278
+ * `platform` field, e.g. `"claude"`) — `probeHostCapabilities` resolves the
279
+ * committed registry row for it, merged with observable environment facts.
280
+ * Always-on orchestration (road-to-always-on-orchestration Phase 1):
281
+ * capability is a fact about the host, never a settings decision, so there
282
+ * is no more `subagents.host_capabilities` override to read here.
283
+ */
284
+ export function resolveActivation(
285
+ workspace_root: string,
286
+ hostId: string | null | undefined,
287
+ ): {
288
+ activation: ActivationInputs;
289
+ downshift: boolean;
290
+ separate_quota_pool: boolean;
291
+ /** Host-capability manifest's `agent_teams` field — the ladder's rung-3 precondition. */
292
+ agentTeams: boolean;
293
+ } {
294
+ const settings = load_agent_settings({ cwd: workspace_root }) as Record<string, unknown>;
295
+ const emergency = (settings["emergency"] ?? {}) as Record<string, unknown>;
296
+ const halted = emergency["orchestration_halt"] === true;
297
+ const host_manifest = probeHostCapabilities(hostId);
298
+ const sub = (settings["subagents"] ?? {}) as Record<string, unknown>;
299
+ const downshift = sub["downshift"] !== false;
300
+ return {
301
+ activation: { halted, subagent_spawn: host_manifest.subagent_spawn },
302
+ downshift,
303
+ separate_quota_pool: host_manifest.separate_quota_pool,
304
+ agentTeams: host_manifest.agent_teams,
305
+ };
306
+ }
307
+
308
+ /**
309
+ * Tier recommendation for the (not-yet-inspected) slices: `resolveSubagentRouting`'s
310
+ * first production caller. Conservative "lite, downshifted" default — the
311
+ * concern has no per-slice type classification available at prompt-submit
312
+ * time, so it recommends the cheapest starting point and lets the downshift
313
+ * setting (shipped default: on) decide whether that survives.
314
+ */
315
+ export function recommendSliceTier(downshift: boolean, separate_quota_pool: boolean): Tier {
316
+ const decision = resolveSubagentRouting({
317
+ task_tier: "lite",
318
+ session_tier: "high",
319
+ downshift,
320
+ quota_arbitrage: false,
321
+ model_map: {},
322
+ separate_quota_pool,
323
+ });
324
+ return decision.tier;
325
+ }
326
+
327
+ /**
328
+ * `rung` names the resolved judgment-ladder rung (2.4): `null` renders as
329
+ * `rung-?` — this concern only ever calls it for a `subagent` verdict
330
+ * (rung 1 or 2), so `null` is unreachable in production and exists only so
331
+ * the function stays total for direct unit-test callers.
332
+ */
333
+ export function buildNudgeLine(
334
+ rung: LadderRung,
335
+ classification: Classification,
336
+ sliceCount: number,
337
+ tier: Tier,
338
+ ): string {
339
+ const unit = sliceCount === 1 ? "slice" : "slices";
340
+ const rungLabel = rung === null ? "rung-?" : `rung-${rung}`;
341
+ // Rung 1 (single bounded slice) carries no do-in-steps/do-in-parallel
342
+ // mode of its own — `classification.mode` is `null` for that shape.
343
+ const modeLabel = classification.mode ?? "single-slice";
344
+ return (
345
+ `<delegation-nudge>${rungLabel}: dispatch ${modeLabel} ` +
346
+ `(${sliceCount} ${unit}, ${tier} tier recommended). Consider dispatching via ` +
347
+ `subagent-orchestration instead of doing every slice in-session — ` +
348
+ `${classification.reason}.</delegation-nudge>`
349
+ );
350
+ }
351
+
352
+ export interface NudgeResult {
353
+ classification: Classification;
354
+ sliceCount: number;
355
+ tier: Tier;
356
+ /** The judgment-ladder rung this verdict resolved to (2.4) — 1 or 2 only, this concern's scope. */
357
+ rung: LadderRung;
358
+ }
359
+
360
+ /**
361
+ * Full pipeline: prompt text + workspace root + host id → a positive nudge
362
+ * verdict, or `null` when nothing should be injected (no signal, a rung-0/3/4
363
+ * verdict out of this concern's scope, activation gate closed, or an
364
+ * internal error — all degrade to the same silent outcome).
365
+ */
366
+ export function classifyPrompt(
367
+ prompt: string,
368
+ workspace_root: string,
369
+ hostId: string | null | undefined,
370
+ ): NudgeResult | null {
371
+ try {
372
+ const { signals, sliceCountForLine } = extractTaskSignals(prompt);
373
+ const { activation, downshift, separate_quota_pool, agentTeams } = resolveActivation(
374
+ workspace_root,
375
+ hostId,
376
+ );
377
+ const ladder = classifyLadder({ taskText: prompt, signals, activation, agentTeams });
378
+ if (ladder.verdict !== "subagent") {
379
+ return null; // rung-0 script, rung-3/4 team/council, or ∅ ask/in-session — out of scope here
380
+ }
381
+ const tier = recommendSliceTier(downshift, separate_quota_pool);
382
+ // Rung 1 (single bounded slice) has no explicit slice count of its own —
383
+ // extractTaskSignals never produces one for that shape — so the line
384
+ // always names it as exactly one slice.
385
+ const sliceCount = ladder.rung === 1 ? 1 : sliceCountForLine;
386
+ const classification: Classification = {
387
+ delegable: true,
388
+ action: "dispatch",
389
+ mode: ladder.mode ?? null,
390
+ reason: ladder.reason,
391
+ };
392
+ return { classification, sliceCount, tier, rung: ladder.rung };
393
+ } catch {
394
+ return null; // classifier/settings error → silence, never a crash-to-block
395
+ }
396
+ }
397
+
398
+ function _extractPrompt(payload: JsonObject): string {
399
+ for (const key of ["prompt", "userPrompt", "user_prompt", "message", "text"]) {
400
+ const v = payload[key];
401
+ if (typeof v === "string" && v.trim()) {
402
+ return v;
403
+ }
404
+ }
405
+ return "";
406
+ }
407
+
408
+ function _workspaceRoot(envelope: JsonObject): string {
409
+ const v = envelope["workspace_root"];
410
+ if (typeof v === "string" && v) {
411
+ return v;
412
+ }
413
+ return process.cwd();
414
+ }
415
+
416
+ export function main(): number {
417
+ let envelope: JsonValue = {};
418
+ try {
419
+ const raw = readHookStdin();
420
+ envelope = raw.trim() ? (JSON.parse(raw) as JsonValue) : {};
421
+ } catch {
422
+ return EXIT_ALLOW; // malformed envelope — never block
423
+ }
424
+ const env: JsonObject = _isObject(envelope) ? envelope : {};
425
+
426
+ const event = env["event"];
427
+ const slot = typeof event === "string" && event !== "" ? event : "user_prompt_submit";
428
+ if (slot !== "user_prompt_submit") {
429
+ return EXIT_ALLOW; // this concern only reads the submitted prompt
430
+ }
431
+
432
+ const payload = _isObject(env["payload"]) ? (env["payload"] as JsonObject) : env;
433
+ const prompt = _extractPrompt(payload);
434
+ if (!prompt) {
435
+ return EXIT_ALLOW;
436
+ }
437
+ if (isSyntheticPrompt(prompt)) {
438
+ return EXIT_ALLOW; // harness-generated turn, not a human-authored task
439
+ }
440
+
441
+ const root = _workspaceRoot(env);
442
+ const platform = env["platform"];
443
+ const hostId = typeof platform === "string" && platform ? platform : null;
444
+ const result = classifyPrompt(prompt, root, hostId);
445
+ if (!result) {
446
+ return EXIT_ALLOW; // anti-canary default: no verdict → no output
447
+ }
448
+
449
+ process.stdout.write(
450
+ `${JSON.stringify({
451
+ decision: "warn",
452
+ reason: `delegation-nudge: rung-${result.rung} verdict (${result.sliceCount} slices, ${result.tier} tier)`,
453
+ additional_context: buildNudgeLine(result.rung, result.classification, result.sliceCount, result.tier),
454
+ })}\n`,
455
+ );
456
+ return EXIT_WARN;
457
+ }
458
+
459
+ // Bundle-safety: never auto-run when inlined into an esbuild bundle, where
460
+ // every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
461
+ declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
462
+ function _isCliEntry(): boolean {
463
+ if (typeof __AGENT_CONFIG_BUNDLE__ !== "undefined" && __AGENT_CONFIG_BUNDLE__) {
464
+ return false;
465
+ }
466
+ if (process.argv[1] === undefined) {
467
+ return false;
468
+ }
469
+ const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
470
+ if (import.meta.url === argvUrl) {
471
+ return true;
472
+ }
473
+ try {
474
+ const here = fs.realpathSync(fileURLToPath(import.meta.url));
475
+ const argv = fs.realpathSync(path.resolve(process.argv[1]));
476
+ return here === argv;
477
+ } catch {
478
+ return false;
479
+ }
480
+ }
481
+
482
+ if (_isCliEntry()) {
483
+ process.exit(main());
484
+ }