@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +12 -11
- package/CHANGELOG.md +99 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +10 -3
- package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
- package/dist/agent-src/rules/context-hygiene.md +3 -1
- package/dist/agent-src/rules/council-availability.md +93 -0
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +29 -5
- package/dist/agent-src/rules/design-fidelity.md +3 -0
- package/dist/agent-src/rules/evaluator-independence.md +2 -1
- package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
- package/dist/agent-src/rules/finance-safety-floor.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +1 -1
- package/dist/agent-src/rules/history-discipline.md +3 -1
- package/dist/agent-src/rules/legal-safety-floor.md +1 -1
- package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
- package/dist/agent-src/rules/onboarding-gate.md +3 -1
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
- package/dist/agent-src/rules/role-mode-adherence.md +1 -1
- package/dist/agent-src/rules/scale-discipline.md +3 -1
- package/dist/agent-src/rules/self-repair-loop.md +103 -0
- package/dist/agent-src/rules/session-canary.md +2 -1
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +3 -1
- package/dist/agent-src/rules/tool-safety.md +5 -1
- package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
- package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +6 -10
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-22GCIJ2O.js +270 -0
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-2BE5I2MN.js} +8 -8
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-7OMQCH4V.js} +1 -1
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-GB2FSUEE.js} +57 -7
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-JXARCGVN.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-PQSAQKHW.js} +466 -304
- package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +134 -0
- package/dist/cli-delegate/cmd_conformance.js +7 -7
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +5 -5
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_mcp_available.js +433 -0
- package/dist/cli-delegate/cmd_migrate.js +3 -3
- package/dist/cli-delegate/cmd_packs_active.js +153 -0
- package/dist/cli-delegate/cmd_preflight.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +5 -5
- package/dist/cli-delegate/cmd_route_audit.js +8 -8
- package/dist/cli-delegate/cmd_route_explain.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +272 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +5 -5
- package/dist/cli-delegate/cmd_settings_set.js +7 -2967
- package/dist/cli-delegate/cmd_sync.js +5 -5
- package/dist/cli-delegate/cmd_uninstall.js +5 -5
- package/dist/cli-delegate/cmd_update.js +4 -4
- package/dist/cli-delegate/cmd_upgrade.js +4 -4
- package/dist/cli-delegate/cmd_validate.js +5 -5
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +100 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +6 -4
- package/dist/hooks/dispatch.js +9004 -6132
- package/dist/install/install.mjs +697 -507
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +127 -20
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +80 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/docs/contracts/settings-classes.md +29 -8
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +6 -10
- package/src/config/budgets.yml +29 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
- package/src/scripts/_cli/cmd_packs_active.ts +225 -0
- package/src/scripts/_cli/cmd_settings_get.ts +324 -0
- package/src/scripts/_dispatch.bash +57 -0
- package/src/scripts/_lib/agent_settings.ts +82 -16
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +84 -1
- package/src/scripts/_lib/prompt_shape.ts +88 -0
- package/src/scripts/_lib/review_skipped_record.ts +102 -0
- package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
- package/src/scripts/_lib/self_repair.ts +342 -0
- package/src/scripts/_lib/self_repair_store.ts +100 -0
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/config/profiles.ts +13 -3
- package/src/scripts/conformance_scan.ts +272 -24
- package/src/scripts/council_availability_hook.ts +188 -0
- package/src/scripts/council_cli.ts +126 -7
- package/src/scripts/hook_manifest.yaml +107 -26
- package/src/scripts/hooks/block_no_verify.ts +39 -2
- package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
- package/src/scripts/hooks/concern_registry.ts +10 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +450 -0
- package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
- package/src/scripts/install.ts +151 -0
- package/src/scripts/language_mirror_hook.ts +30 -37
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +4 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/report_carrier_divergence.ts +364 -0
- package/src/scripts/report_skill_activation.ts +257 -0
- package/src/scripts/report_skill_obligation_violations.ts +579 -0
- package/src/scripts/routing_doctor.ts +77 -6
- package/src/scripts/self_repair_cli.ts +240 -0
- 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
|
+
}
|