@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +13 -12
- package/CHANGELOG.md +165 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/conformance.md +24 -0
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +13 -12
- package/dist/agent-src/commands/team/review.md +9 -8
- package/dist/agent-src/commands/team/status.md +9 -8
- package/dist/agent-src/commands/team.md +29 -19
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
- package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
- package/dist/agent-src/contexts/execution/subagent-steering.md +6 -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 +38 -10
- 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/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +4 -2
- 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/forensics-report/SKILL.md +114 -0
- package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
- 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/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -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/subagent-orchestration/SKILL.md +7 -5
- package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
- package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/dist/agent-src/templates/AGENTS.md +8 -12
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +23 -0
- package/dist/cli/registry.js +7 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
- package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
- package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
- package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
- package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
- package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
- 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 +10 -47
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +10 -6
- package/dist/cli-delegate/cmd_explain.js +80 -330
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_handoff.js +3 -3
- 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 +6 -6
- 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 +266 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +8 -2968
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- 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 +6 -6
- package/dist/cli-delegate/cmd_versions.js +4 -4
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +164 -55
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -3
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +44 -5
- package/dist/discovery/trust-report.md +11 -11
- package/dist/discovery/workspaces.json +16 -6
- package/dist/hooks/dispatch.js +9341 -6131
- package/dist/install/install.mjs +739 -524
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +777 -281
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +134 -15
- package/dist/scripts/_lib/agent_settings.js.map +1 -1
- package/dist/scripts/condense.js +147 -2
- package/dist/scripts/condense.js.map +1 -1
- package/dist/server/schemas/settings.js +6 -7
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/settingsCarveOut.js +0 -9
- package/dist/shared/settingsCarveOut.js.map +1 -1
- package/dist/shared/settingsClasses.js +11 -9
- package/dist/shared/settingsClasses.js.map +1 -1
- package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
- package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +68 -17
- package/docs/guidelines/agent-infra/memory-access.md +14 -3
- package/package.json +1 -1
- package/src/agent-src/templates/AGENTS.md +8 -12
- package/src/config/agent-settings.template.yml +52 -66
- package/src/config/budgets.yml +29 -0
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +26 -0
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/scripts/_cli/cmd_brand_status.ts +174 -0
- package/src/scripts/_cli/cmd_conformance.ts +14 -42
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- 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 +140 -16
- package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
- package/src/scripts/_lib/auto_dispatch.ts +23 -18
- package/src/scripts/_lib/capsule_trigger.ts +124 -0
- package/src/scripts/_lib/carrier_divergence.ts +135 -0
- package/src/scripts/_lib/host_capability.ts +144 -4
- package/src/scripts/_lib/judgment_ladder.ts +382 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +37 -0
- package/src/scripts/_lib/orchestration_savings.ts +1 -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 +489 -0
- package/src/scripts/_lib/self_repair_store.ts +128 -0
- package/src/scripts/_lib/subagent_capsule.ts +182 -0
- package/src/scripts/_lib/subagent_response.ts +19 -0
- package/src/scripts/_lib/subagent_spawn.ts +13 -0
- package/src/scripts/_lib/subagent_steering.ts +9 -4
- package/src/scripts/_lib/tier_budget_routing.ts +6 -1
- package/src/scripts/_lib/transcript_entry.ts +80 -0
- package/src/scripts/_lib/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +76 -54
- package/src/scripts/ai_council/config.ts +94 -6
- package/src/scripts/ai_council/handoff.ts +113 -0
- package/src/scripts/ai_council/modes.ts +19 -4
- package/src/scripts/ai_council/orchestrator.ts +105 -0
- package/src/scripts/ai_council/quorum.ts +84 -0
- package/src/scripts/ai_council/session.ts +44 -0
- package/src/scripts/ai_council/transport_resolver.ts +127 -1
- package/src/scripts/ai_team/availability.ts +110 -0
- package/src/scripts/ai_team/config.ts +34 -14
- package/src/scripts/ai_team/team_dispatch.ts +71 -29
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/check_branch_freshness.ts +255 -0
- package/src/scripts/check_council_config_location.ts +125 -1
- package/src/scripts/check_detector_corpus.ts +202 -0
- package/src/scripts/check_hook_bundle_freshness.ts +4 -1
- package/src/scripts/check_standing_rule_delivery.ts +314 -0
- package/src/scripts/condense.ts +162 -2
- 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 +527 -32
- package/src/scripts/forensics_report.ts +333 -0
- 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 +484 -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/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/measure_scope_dedup.ts +19 -13
- package/src/scripts/onboarding_gate_hook.ts +40 -1
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/preamble_byte_census.ts +9 -20
- package/src/scripts/rebuild_hook_bundle.ts +177 -0
- package/src/scripts/release.ts +74 -2
- 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 +95 -39
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +470 -0
- package/src/scripts/self_repair_hook.ts +241 -0
- package/src/scripts/team_events_spike.ts +271 -0
- package/src/scripts/team_review_gate_hook.ts +31 -9
- package/src/server/schemas/settings.ts +10 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- 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
|
+
}
|