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