@event4u/agent-config 9.28.0 → 9.30.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 +6 -6
- package/CHANGELOG.md +172 -5
- package/README.md +2 -1
- package/agents/templates/.ai-council.yml.example +55 -24
- package/dist/agent-src/commands/analyze/inbox.md +22 -1
- package/dist/agent-src/commands/cost/report.md +2 -2
- package/dist/agent-src/commands/explain-run.md +31 -1
- package/dist/agent-src/commands/team/adversarial.md +8 -7
- package/dist/agent-src/commands/team/delegate.md +14 -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 +22 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +17 -0
- package/dist/agent-src/contexts/execution/subagent-response-contract.md +65 -1
- 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/delegation-policy.md +11 -7
- package/dist/agent-src/rules/preservation-guard.md +9 -0
- package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
- package/dist/agent-src/rules/token-budget-discipline.md +1 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +312 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +72 -4
- 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/premortem/SKILL.md +30 -1
- package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +17 -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/templates/AGENTS.md +4 -4
- 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 +2 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-22GCIJ2O.js → chunk-66OVWGB5.js} +1 -1
- package/dist/cli-delegate/{chunk-7OMQCH4V.js → chunk-BTJNRUX3.js} +22 -35
- package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-DDUNNFYX.js} +24 -17
- package/dist/cli-delegate/{chunk-PQSAQKHW.js → chunk-EQY4FLJB.js} +3 -3
- package/dist/cli-delegate/{chunk-2BE5I2MN.js → chunk-IYTGIG2Y.js} +155 -91
- package/dist/cli-delegate/{chunk-GB2FSUEE.js → chunk-LCFJIQ4J.js} +32 -0
- package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
- package/dist/cli-delegate/chunk-MMJL4TIX.js +18 -0
- package/dist/cli-delegate/{chunk-JXARCGVN.js → chunk-MP3JT3IU.js} +1 -1
- package/dist/cli-delegate/chunk-O3CO5BFB.js +161 -0
- package/dist/cli-delegate/{chunk-REKCMYOU.js → chunk-PFBV6XRT.js} +36 -20
- package/dist/cli-delegate/chunk-RVGBH4XK.js +142 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +2803 -137
- package/dist/cli-delegate/cmd_doctor.js +10 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +88 -8
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +3 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +177 -0
- package/dist/cli-delegate/cmd_settings_get.js +2 -8
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_settings_set.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +72 -23
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -2
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +39 -2
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +12 -4
- package/dist/hooks/dispatch.js +2278 -611
- package/dist/install/install.mjs +56 -17
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +4122 -699
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +54 -0
- 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 +12 -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-B5qGYOJo.js +26 -0
- package/dist/ui/assets/index-B5qGYOJo.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/contracts/settings-classes.md +58 -12
- package/package.json +1 -1
- package/src/agent-src/scripts/roadmap_gates.ts +312 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +72 -4
- package/src/agent-src/templates/AGENTS.md +4 -4
- package/src/config/agent-settings.template.yml +88 -66
- package/src/config/discovery/packs.yml +11 -0
- package/src/config/discovery/workspaces.yml +1 -1
- package/src/config/dispatch-economy-metrics.json +61 -0
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/hook-token-budget.json +91 -0
- package/src/config/recycle-threshold-budget.json +12 -0
- package/src/config/rule-enforcement-baseline.json +86 -0
- package/src/scripts/_cli/cmd_conformance.ts +31 -44
- package/src/scripts/_cli/cmd_doctor.ts +65 -48
- package/src/scripts/_cli/cmd_session_recycle.ts +226 -0
- package/src/scripts/_dispatch.bash +23 -0
- package/src/scripts/_lib/agent_settings.ts +58 -0
- 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/host_capability.ts +63 -6
- package/src/scripts/_lib/judgment_ladder.ts +634 -0
- package/src/scripts/_lib/orchestration_gate.ts +8 -0
- package/src/scripts/_lib/orchestration_record.ts +78 -10
- package/src/scripts/_lib/orchestration_savings.ts +1 -1
- package/src/scripts/_lib/recycle_envelope_paths.ts +35 -0
- package/src/scripts/_lib/self_repair.ts +354 -13
- package/src/scripts/_lib/self_repair_store.ts +28 -0
- package/src/scripts/_lib/session_eol.ts +230 -0
- package/src/scripts/_lib/session_role.ts +42 -0
- package/src/scripts/_lib/subagent_capsule.ts +341 -0
- package/src/scripts/_lib/subagent_response.ts +94 -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/worker_budget.ts +37 -2
- package/src/scripts/ai_council/clients.ts +126 -55
- 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/ask_transport.ts +221 -0
- package/src/scripts/bench_critic_protocol.ts +396 -0
- package/src/scripts/bench_hook_injection.ts +234 -0
- package/src/scripts/check_adr_frontmatter.ts +84 -40
- package/src/scripts/check_branch_freshness.ts +1 -0
- package/src/scripts/check_corpus_staleness.ts +18 -4
- package/src/scripts/check_detector_corpus.ts +219 -0
- package/src/scripts/check_gitignore_freshness.ts +42 -8
- package/src/scripts/check_hook_bundle_freshness.ts +24 -1
- package/src/scripts/check_kernel_prefix_stability.ts +1 -0
- package/src/scripts/check_standing_rule_delivery.ts +73 -32
- package/src/scripts/check_static_layer_stability.ts +154 -0
- package/src/scripts/check_suppression_hygiene.ts +48 -0
- package/src/scripts/condense.ts +162 -2
- package/src/scripts/council_cli.ts +401 -25
- package/src/scripts/dispatch_economy_report.ts +448 -0
- package/src/scripts/dispatch_r2_reviewer.ts +15 -2
- package/src/scripts/explain_run.ts +206 -4
- package/src/scripts/forensics_report.ts +333 -0
- package/src/scripts/generate_settings_reference.ts +1 -0
- package/src/scripts/handoff_context_hook.ts +112 -4
- package/src/scripts/hook_manifest.yaml +117 -10
- package/src/scripts/hooks/concern_registry.ts +8 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +106 -35
- package/src/scripts/hooks/dispatch_hook.ts +39 -2
- package/src/scripts/hooks/edit_shape_hook.ts +303 -0
- package/src/scripts/hooks/orchestration_record_hook.ts +19 -1
- package/src/scripts/hooks/reread_guard_hook.ts +314 -0
- package/src/scripts/hooks/rtk_wrap_hook.ts +133 -14
- package/src/scripts/hooks/session_eol_hook.ts +268 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +507 -0
- package/src/scripts/install-hooks.sh +1 -1
- package/src/scripts/language_mirror_hook.ts +89 -0
- package/src/scripts/lint_decision_impact.ts +14 -1
- package/src/scripts/lint_discovery_vocabulary.ts +2 -0
- package/src/scripts/lint_hook_manifest.ts +82 -0
- package/src/scripts/lint_never_silent.ts +276 -0
- package/src/scripts/lint_no_activation_gates.ts +271 -0
- package/src/scripts/lint_readme_serial_comma.ts +1 -0
- package/src/scripts/lint_rule_enforcement_declaration.ts +347 -0
- package/src/scripts/measure_turn_end_gate.ts +194 -0
- package/src/scripts/orchestration_record.ts +12 -1
- package/src/scripts/rebuild_hook_bundle.ts +178 -0
- package/src/scripts/release.ts +74 -2
- package/src/scripts/report_conformance_funnel.ts +284 -0
- package/src/scripts/report_imperative_density.ts +1 -0
- package/src/scripts/routing_doctor.ts +118 -36
- package/src/scripts/score_skill_selection.ts +19 -5
- package/src/scripts/self_repair_cli.ts +337 -59
- package/src/scripts/session_eol_report.ts +261 -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 +24 -17
- package/src/shared/settingsCarveOut.ts +0 -10
- package/src/shared/settingsClasses.ts +11 -9
- package/src/templates/compact-instructions.md +40 -0
- package/dist/ui/assets/index-DmR0mbk4.js +0 -26
- package/dist/ui/assets/index-DmR0mbk4.js.map +0 -1
|
@@ -0,0 +1,634 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Judgment ladder (Phase 2, road-to-always-on-orchestration) — the ONE
|
|
3
|
+
* resolver that decides which of the five dispatch rungs (0-4), or the
|
|
4
|
+
* silent ∅, a task resolves to. Committed table + rationale:
|
|
5
|
+
* `src/agent-src/contexts/execution/auto-dispatch-classification.md`
|
|
6
|
+
* § Judgment ladder.
|
|
7
|
+
*
|
|
8
|
+
* This WRAPS `classifyTask` rather than replacing it — rungs 1 and 2 ARE
|
|
9
|
+
* `classifyTask`'s existing dispatch verdict (single-slice vs multi-slice);
|
|
10
|
+
* rungs 0, 3, and 4 are new signal layers checked around it, in a FIXED
|
|
11
|
+
* priority order (documented on {@link classifyLadder} itself, because the
|
|
12
|
+
* order IS the contract: two signals matching the same text must resolve
|
|
13
|
+
* deterministically, never on evaluation-order accident).
|
|
14
|
+
*
|
|
15
|
+
* Three previously-scattered classification surfaces (the delegable-signal
|
|
16
|
+
* rules in `auto_dispatch.ts`, ad-hoc "should this be a team" judgment
|
|
17
|
+
* calls, and the absence of any council-routing signal on the TASK side)
|
|
18
|
+
* get ONE committed name and ONE resolver here — never a fourth parallel
|
|
19
|
+
* classifier bolted on beside it.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately independent of `ai_council/necessity.ts`: that module is the
|
|
22
|
+
* council's OWN necessity gate (its `NECESSARY_TRIGGERS` vocabulary and its
|
|
23
|
+
* `off|educate|block|warn-only` modes stay the council-side surface, per the
|
|
24
|
+
* roadmap's 2.2 note). This resolver's rung-4 signals are a NEW, narrower
|
|
25
|
+
* set — the three shapes the roadmap names explicitly (design decision,
|
|
26
|
+
* security downgrade, release-gate escalation) — kept as separate,
|
|
27
|
+
* regex-only constants rather than an import so this resolver carries no
|
|
28
|
+
* runtime dependency on the council module.
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
classifyLookup,
|
|
32
|
+
classifyTask,
|
|
33
|
+
SIZE_FLOOR,
|
|
34
|
+
type ActivationInputs,
|
|
35
|
+
type DispatchMode,
|
|
36
|
+
type TaskSignals,
|
|
37
|
+
} from './auto_dispatch.js';
|
|
38
|
+
|
|
39
|
+
/** The five dispatch rungs, or `null` for the silent ∅ (in-session / ask). */
|
|
40
|
+
export type LadderRung = 0 | 0.5 | 1 | 2 | 3 | 4 | null;
|
|
41
|
+
|
|
42
|
+
export type LadderVerdict =
|
|
43
|
+
| 'script' // rung 0 — deterministic transform/primitive, no spawn at all
|
|
44
|
+
| 'subagent' // rung 1/2 — one, or several downshifted, subagent dispatch(es)
|
|
45
|
+
| 'team' // rung 3 — communicating slices, dispatched via the host teams primitive
|
|
46
|
+
| 'council' // rung 4 — contested judgment
|
|
47
|
+
// 'ask' carries TWO shapes, split by rung (road-to-token-economy-dispatch
|
|
48
|
+
// Phase 4): rung 0.5 = a bounded question routed to ONE completion via
|
|
49
|
+
// `ask_transport` (no session, no tools, hard-capped — never the user);
|
|
50
|
+
// rung null = ambiguity, a VERDICT to the user, never a speculative spawn.
|
|
51
|
+
| 'ask'
|
|
52
|
+
| 'in-session'; // ∅ — halted, no host primitive, trivial, approval-required, or recursive guard
|
|
53
|
+
|
|
54
|
+
export interface LadderResult {
|
|
55
|
+
rung: LadderRung;
|
|
56
|
+
verdict: LadderVerdict;
|
|
57
|
+
reason: string;
|
|
58
|
+
/**
|
|
59
|
+
* Present only when a HIGHER rung's signal fired but its precondition
|
|
60
|
+
* failed (today: rung 3's `agentTeams` host-capability check) and
|
|
61
|
+
* resolution fell back to a lower rung/verdict. Names the rung the
|
|
62
|
+
* signal originally pointed at, never the resolved one.
|
|
63
|
+
*/
|
|
64
|
+
degraded_from?: LadderRung;
|
|
65
|
+
/**
|
|
66
|
+
* Carried through from `classifyTask` when the resolved rung is 1 or 2
|
|
67
|
+
* — the concrete `subagent-orchestration` mode a caller dispatches on.
|
|
68
|
+
* `null` for rung 1 (a single bounded slice has no do-in-steps /
|
|
69
|
+
* do-in-parallel shape of its own) and for every non-subagent verdict.
|
|
70
|
+
* Not part of the roadmap's minimal `{rung, verdict, reason,
|
|
71
|
+
* degraded_from?}` shape — added because the delegation-nudge hook
|
|
72
|
+
* (2.4) needs the concrete mode to build its injected line.
|
|
73
|
+
*/
|
|
74
|
+
mode?: DispatchMode | null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LadderInputs {
|
|
78
|
+
/**
|
|
79
|
+
* Raw task/prompt text — regex-only signal extraction for rungs 0, 1, 3,
|
|
80
|
+
* and 4, in the same discipline as `delegation_nudge_hook.ts`'s
|
|
81
|
+
* extractors (cheap, no LLM call; false-positive/negative trade-offs
|
|
82
|
+
* documented per pattern below).
|
|
83
|
+
*/
|
|
84
|
+
taskText: string;
|
|
85
|
+
/** Structural signals `classifyTask` already consumes — rungs 1/2. */
|
|
86
|
+
signals: TaskSignals;
|
|
87
|
+
/** Emergency halt + subagent_spawn primitive — gates every rung above 0. */
|
|
88
|
+
activation: ActivationInputs;
|
|
89
|
+
/**
|
|
90
|
+
* Host-capability manifest's `agent_teams` field
|
|
91
|
+
* (`host_capability.ts::probeHostCapabilities`) — rung 3's precondition.
|
|
92
|
+
* Absent/`false` degrades a matched rung-3 signal to rung 2.
|
|
93
|
+
*/
|
|
94
|
+
agentTeams: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Caller states this step needs an explicit human decision before
|
|
97
|
+
* anything may run (an approval gate, a Hard-Floor action, …) — NEVER
|
|
98
|
+
* inferred from `taskText`. Checked first; overrides every other signal.
|
|
99
|
+
*/
|
|
100
|
+
interactiveApprovalRequired?: boolean;
|
|
101
|
+
/**
|
|
102
|
+
* Recursive-dispatch guard (2.3). See {@link classifyLadder}'s doc
|
|
103
|
+
* comment for the honest scope of what this suite can and cannot
|
|
104
|
+
* detect on its own — this field is a CALLER-SUPPLIED fact, never
|
|
105
|
+
* something this resolver probes for itself.
|
|
106
|
+
*/
|
|
107
|
+
insideSubagentSession?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Rung 0 — mechanical-transform signals (regex-only) ──────────────────
|
|
111
|
+
//
|
|
112
|
+
// `classifyLookup` (auto_dispatch.ts, road-to-lean-agent-init L0) already
|
|
113
|
+
// covers the READ-ONLY half of "deterministic script, no spawn" (definition/
|
|
114
|
+
// references/string-existence/report-run lookups). This adds the WRITE half
|
|
115
|
+
// the roadmap names explicitly: mechanical transforms with no semantics to
|
|
116
|
+
// judge — a rename, a codemod, a formatter run, a bulk search-and-replace.
|
|
117
|
+
// Each pattern requires the verb AND its object shape (e.g. "rename X to Y",
|
|
118
|
+
// not bare "rename") so a sentence that merely MENTIONS renaming in passing
|
|
119
|
+
// ("we renamed this last week") does not false-positive into rung 0.
|
|
120
|
+
const RENAME_RE = /\brename\s+\S+\s+to\s+\S+\b/i;
|
|
121
|
+
const CODEMOD_RE = /\bcodemod\b/i;
|
|
122
|
+
const FORMATTER_RUN_RE =
|
|
123
|
+
/\bauto-?format(?:ting)?\b|\breformat(?:ting)?\b|\brun(?:ning)?\s+prettier\b|\brun(?:ning)?\s+eslint\s*--fix\b|\bapply\s+eslint\s*--fix\b/i;
|
|
124
|
+
const SEARCH_REPLACE_RE = /\bsearch-and-replace\b|\bfind-and-replace\b|\bbulk\s+rename\b/i;
|
|
125
|
+
|
|
126
|
+
export function detectMechanicalTransform(text: string): { matched: boolean; reason: string } {
|
|
127
|
+
const rename = RENAME_RE.exec(text);
|
|
128
|
+
if (rename) {
|
|
129
|
+
return { matched: true, reason: `rename signal ("${rename[0]}") — deterministic script, no spawn` };
|
|
130
|
+
}
|
|
131
|
+
const codemod = CODEMOD_RE.exec(text);
|
|
132
|
+
if (codemod) {
|
|
133
|
+
return { matched: true, reason: `codemod signal ("${codemod[0]}") — deterministic script, no spawn` };
|
|
134
|
+
}
|
|
135
|
+
const formatter = FORMATTER_RUN_RE.exec(text);
|
|
136
|
+
if (formatter) {
|
|
137
|
+
return { matched: true, reason: `formatter-run signal ("${formatter[0]}") — deterministic script, no spawn` };
|
|
138
|
+
}
|
|
139
|
+
const searchReplace = SEARCH_REPLACE_RE.exec(text);
|
|
140
|
+
if (searchReplace) {
|
|
141
|
+
return {
|
|
142
|
+
matched: true,
|
|
143
|
+
reason: `search-and-replace/bulk-rename signal ("${searchReplace[0]}") — deterministic script, no spawn`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
return { matched: false, reason: 'no mechanical-transform signal' };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── Rung 1 — single bounded read-heavy slice (regex-only) ───────────────
|
|
150
|
+
//
|
|
151
|
+
// The one rung `classifyTask` structurally cannot see: its delegable rules
|
|
152
|
+
// all require >=2 slices, an ordered plan, or a declared `parallelizable`
|
|
153
|
+
// frontmatter value. A single "read this whole file and summarize it" task
|
|
154
|
+
// is real, non-trivial work that still benefits from a lite-tier subagent
|
|
155
|
+
// (session-context savings) but has no multi-slice shape at all. Requires a
|
|
156
|
+
// read-heavy VERB plus a bounded, singular TARGET phrase — a bare "read the
|
|
157
|
+
// code" without a stated target is too weak a signal to spawn on.
|
|
158
|
+
const SINGLE_SLICE_READHEAVY_RE =
|
|
159
|
+
/\b(?:read|review|summarize|summarise|inventory|audit|investigate|research|inspect|dig into|look into)\b[^.!?\n]{0,80}\b(?:this file|the file|this codebase|the codebase|this transcript|the transcript|this log|the log|this document|the document|this diff|the diff)\b/i;
|
|
160
|
+
|
|
161
|
+
export function detectBoundedReadHeavySlice(text: string): { matched: boolean; reason: string } {
|
|
162
|
+
const m = SINGLE_SLICE_READHEAVY_RE.exec(text);
|
|
163
|
+
return m
|
|
164
|
+
? {
|
|
165
|
+
matched: true,
|
|
166
|
+
reason: `single bounded read-heavy slice ("${m[0]}") — one lite-tier subagent instead of session-context burn`,
|
|
167
|
+
}
|
|
168
|
+
: { matched: false, reason: 'no single-slice read-heavy signal' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Rung 3 — communication-need signals (team) ───────────────────────────
|
|
172
|
+
//
|
|
173
|
+
// "Slices that must communicate" per the committed table: cross-layer work,
|
|
174
|
+
// an in-flight review-with-challenge shape, or an explicit shared work
|
|
175
|
+
// queue — signals a single implementer-then-judge handoff, or a set of
|
|
176
|
+
// independent parallel slices, cannot express, because the workers need to
|
|
177
|
+
// see each other's state DURING the run, not just at handoff.
|
|
178
|
+
const CROSS_LAYER_RE =
|
|
179
|
+
/\bcross[- ]layer\b|\bcross[- ]cutting\b|\bback[- ]?end\s+and\s+front[- ]?end\b|\bfront[- ]?end\s+and\s+back[- ]?end\b/i;
|
|
180
|
+
const REVIEW_WITH_CHALLENGE_RE =
|
|
181
|
+
/\breview[- ]with[- ]challenge\b|\bchallenge[- ]and[- ]implement\b|\bimplementer\s+and\s+(?:a\s+)?(?:judge|reviewer)\s+(?:working\s+)?together\b|\bsecond\s+opinion\s+while\s+(?:building|implementing)\b/i;
|
|
182
|
+
const SHARED_TASK_LIST_RE =
|
|
183
|
+
/\bshared\s+(?:task\s+list|todo\s+list|backlog)\b|\bcoordinat(?:e|ed|ing)\s+(?:with|between|across)\s+(?:multiple\s+|several\s+)?(?:agents|workers|subagents|teammates)\b/i;
|
|
184
|
+
|
|
185
|
+
export function detectCommunicationNeed(text: string): { matched: boolean; reason: string } {
|
|
186
|
+
const crossLayer = CROSS_LAYER_RE.exec(text);
|
|
187
|
+
if (crossLayer) {
|
|
188
|
+
return { matched: true, reason: `cross-layer communication signal ("${crossLayer[0]}")` };
|
|
189
|
+
}
|
|
190
|
+
const reviewChallenge = REVIEW_WITH_CHALLENGE_RE.exec(text);
|
|
191
|
+
if (reviewChallenge) {
|
|
192
|
+
return { matched: true, reason: `review-with-challenge signal ("${reviewChallenge[0]}")` };
|
|
193
|
+
}
|
|
194
|
+
const sharedList = SHARED_TASK_LIST_RE.exec(text);
|
|
195
|
+
if (sharedList) {
|
|
196
|
+
return { matched: true, reason: `shared-task-list signal ("${sharedList[0]}")` };
|
|
197
|
+
}
|
|
198
|
+
return { matched: false, reason: 'no communication-need signal' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Rung 4 — contested-judgment signals (council) ────────────────────────
|
|
202
|
+
//
|
|
203
|
+
// "Judgment under disagreement" per the committed table: a design decision,
|
|
204
|
+
// a proposed security downgrade, or an escalation at a release gate — the
|
|
205
|
+
// three shapes the roadmap names explicitly. Deliberately its OWN, narrower
|
|
206
|
+
// vocabulary rather than an import of `ai_council/necessity.ts`'s broader
|
|
207
|
+
// `NECESSARY_TRIGGERS` (architecture/tradeoff/ambiguity/strategic): that
|
|
208
|
+
// module stays the council's own necessity gate; this resolver only needs
|
|
209
|
+
// the narrow signal that should route a TASK to rung 4 in the first place.
|
|
210
|
+
// A bare `\badr\b` (no decision-context anchor) matched "update the ADR
|
|
211
|
+
// index" and "fix the release-gate CI script typo" — routine maintenance
|
|
212
|
+
// prose that names the artefact/gate but decides nothing — and suppressed
|
|
213
|
+
// the correct rung-2 nudge on both. Both alternatives below now require an
|
|
214
|
+
// explicit decision/escalation VERB within 40 chars of the artefact/gate
|
|
215
|
+
// token, on either side, so the bare noun alone never fires.
|
|
216
|
+
const DESIGN_DECISION_RE =
|
|
217
|
+
/\bdesign\s+decision\b|\barchitectur(?:e|al)\s+decision\b|\b(?:record(?:ing)?|decide|deciding|decision|accept(?:ed|ing)?|approv(?:e|ed|ing|al)|challeng(?:e|ed|ing)|choos(?:e|ing)|reject(?:ed|ing)?)\b[^.!?\n]{0,40}\bADR\b|\bADR\b[^.!?\n]{0,40}\b(?:record(?:ing)?|decide|deciding|decision|accept(?:ed|ing)?|approv(?:e|ed|ing|al)|challeng(?:e|ed|ing)|choos(?:e|ing)|reject(?:ed|ing)?)\b/i;
|
|
218
|
+
const SECURITY_DOWNGRADE_RE = /\bsecurity\s+downgrade\b|\bdowngrade\s+(?:the\s+)?security\b|\bsecurity\s+exception\b/i;
|
|
219
|
+
const RELEASE_GATE_ESCALATION_RE =
|
|
220
|
+
/\bgate\s+escalation\b|\bescalat(?:e|ion)\s+to\s+(?:the\s+)?council\b|\b(?:escalat(?:e|ing|ion)|contest(?:ed|ing)?|dispute|disputing|override|overriding|waiv(?:e|ing)|bypass(?:ed|ing)?|challeng(?:e|ed|ing))\b[^.!?\n]{0,40}\brelease[- ]gate\b|\brelease[- ]gate\b[^.!?\n]{0,40}\b(?:escalat(?:e|ing|ion)|contest(?:ed|ing)?|dispute|disputing|override|overriding|waiv(?:e|ing)|bypass(?:ed|ing)?|challeng(?:e|ed|ing))\b/i;
|
|
221
|
+
|
|
222
|
+
export function detectContestedJudgment(text: string): { matched: boolean; reason: string } {
|
|
223
|
+
const design = DESIGN_DECISION_RE.exec(text);
|
|
224
|
+
if (design) {
|
|
225
|
+
return { matched: true, reason: `design-decision signal ("${design[0]}")` };
|
|
226
|
+
}
|
|
227
|
+
const security = SECURITY_DOWNGRADE_RE.exec(text);
|
|
228
|
+
if (security) {
|
|
229
|
+
return { matched: true, reason: `security-downgrade signal ("${security[0]}")` };
|
|
230
|
+
}
|
|
231
|
+
const releaseGate = RELEASE_GATE_ESCALATION_RE.exec(text);
|
|
232
|
+
if (releaseGate) {
|
|
233
|
+
return { matched: true, reason: `release-gate-escalation signal ("${releaseGate[0]}")` };
|
|
234
|
+
}
|
|
235
|
+
return { matched: false, reason: 'no contested-judgment signal' };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Resolve the judgment-ladder verdict for one task.
|
|
240
|
+
*
|
|
241
|
+
* FIXED priority order (the order is the contract):
|
|
242
|
+
*
|
|
243
|
+
* 1. `interactiveApprovalRequired` — caller-stated, overrides everything.
|
|
244
|
+
* 2. Rung 0 (`classifyLookup` primitive match OR a mechanical-transform
|
|
245
|
+
* signal) — never spawns, so neither the recursive guard nor the
|
|
246
|
+
* activation gate applies to it.
|
|
247
|
+
* 3. Recursive-dispatch guard (2.3) — `insideSubagentSession` resolves ∅
|
|
248
|
+
* for every rung below this point.
|
|
249
|
+
* 4. Activation gate (`halted` / no `subagent_spawn`) — nothing below this
|
|
250
|
+
* point can spawn without it.
|
|
251
|
+
* 5. Rung 4 (contested-judgment signal) — council.
|
|
252
|
+
* 6. Rung 3 (communication-need signal) — team when `agentTeams`; otherwise
|
|
253
|
+
* degrades to whatever `classifyTask` resolves on the SAME signals
|
|
254
|
+
* (usually rung 2 — slices that must communicate are usually plural
|
|
255
|
+
* slices — but this honestly follows `classifyTask`'s real verdict
|
|
256
|
+
* rather than fabricating a slice count nothing confirmed).
|
|
257
|
+
* 7. Rungs 1/2 — `classifyTask`'s dispatch verdict. `do-in-steps` and
|
|
258
|
+
* `do-in-parallel` both resolve rung 2 in this v1: the roadmap's rung
|
|
259
|
+
* 1-vs-2 split is single-slice vs multi-slice, and `classifyTask`
|
|
260
|
+
* structurally never dispatches on a single slice (its rules require
|
|
261
|
+
* >=2 slices, an ordered plan, or a declared `parallelizable` value) —
|
|
262
|
+
* so every `classifyTask` dispatch verdict is inherently multi-slice.
|
|
263
|
+
* Its `ask` / size-floor `in-session` outcomes get one more check
|
|
264
|
+
* first: rung 1's single-bounded-slice signal, the one shape
|
|
265
|
+
* `classifyTask`'s own rules cannot see.
|
|
266
|
+
*
|
|
267
|
+
* ## Recursive-dispatch guard — honest scope (2.3)
|
|
268
|
+
*
|
|
269
|
+
* No verified host discriminator exists for "is this classification running
|
|
270
|
+
* inside a subagent/teammate session". This roadmap's own
|
|
271
|
+
* `point-of-action-carrier` blocker records that the upstream PreToolUse
|
|
272
|
+
* agent-identity request is closed NOT_PLANNED, and nothing in this repo's
|
|
273
|
+
* hook envelope (`event` / `payload` / `platform` / `workspace_root` —
|
|
274
|
+
* `delegation_nudge_hook.ts`'s own `main()`) carries a session-lineage
|
|
275
|
+
* field. `insideSubagentSession` is therefore a CALLER-SUPPLIED fact: the
|
|
276
|
+
* code path that dispatched the subagent/teammate loop is the one place
|
|
277
|
+
* that genuinely knows. This resolver does NOT probe `process.env` for
|
|
278
|
+
* it — inventing an unverified `CLAUDE_AGENT_*`-shaped variable name here
|
|
279
|
+
* would be exactly the hallucinated-field failure `source-discovery-gate`
|
|
280
|
+
* exists to stop. Documented as a gap, not silently assumed away: on a
|
|
281
|
+
* host/caller that never sets this field, the guard is a no-op and rungs
|
|
282
|
+
* 1-4 resolve normally even inside a subagent — the same gap the
|
|
283
|
+
* `point-of-action-carrier` blocker already names as its pre-registered
|
|
284
|
+
* null ("no discriminator" is publishable and does not block this roadmap).
|
|
285
|
+
*/
|
|
286
|
+
/**
|
|
287
|
+
* Rung-0.5 signal — a single bounded question (token-economy-dispatch 4.2).
|
|
288
|
+
*
|
|
289
|
+
* Deliberately conservative, regex-only (the F3-lite discipline): the cost
|
|
290
|
+
* of a false NEGATIVE is one avoidable spawn; the cost of a false POSITIVE
|
|
291
|
+
* is an ask that returns INSUFFICIENT_CONTEXT and gets escalated — which the
|
|
292
|
+
* registered `ask_economy` kill criterion counts against the rung. So every
|
|
293
|
+
* ambiguity falls through (no match), and four hard exclusions apply:
|
|
294
|
+
*
|
|
295
|
+
* 1. Multi-line prompts — a bounded question is one sentence.
|
|
296
|
+
* 2. Length > 240 chars — beyond that it is a briefing, not a question.
|
|
297
|
+
* 3. Imperative work verbs (fix/implement/write/…) — those are tasks.
|
|
298
|
+
* 4. No interrogative shape — must lead with an interrogative (EN/DE) and
|
|
299
|
+
* end with `?`, OR be an explicit classify/categorize instruction
|
|
300
|
+
* (the one imperative the roadmap names as ask-shaped).
|
|
301
|
+
*/
|
|
302
|
+
const _QUESTION_IMPERATIVE_RE =
|
|
303
|
+
/\b(fix|implement|refactor|build|create|write|add|update|delete|remove|rename|migrate|install|deploy|commit|push|merge|run|execute|generate|erstelle|baue|implementiere|schreibe|behebe)\b/i;
|
|
304
|
+
const _QUESTION_LEAD_RE =
|
|
305
|
+
/^(is|are|does|do|did|can|could|should|would|will|which|what|why|how|when|where|who|whose|ist|sind|hat|haben|kann|welche[rs]?|was|warum|wie|wann|wo|wer)\b/i;
|
|
306
|
+
const _CLASSIFY_LEAD_RE = /^(classify|categori[sz]e|klassifiziere)\b/i;
|
|
307
|
+
const _QUESTION_MAX_LEN = 240;
|
|
308
|
+
|
|
309
|
+
/** A question that names a file/repo object or a concrete path needs TOOLS —
|
|
310
|
+
* a blind completion cannot read it, so it is never ask-shaped. */
|
|
311
|
+
const _NEEDS_TOOLS_RE =
|
|
312
|
+
/\b(this|the|that|my|our)\s+(?:[\w-]+\s+){0,2}?(file|files|codebase|repo|repository|transcript|log|logs|diff|document|branch|pr|route|routes|schema|module|class|function|endpoint|test|tests)\b|[\w.-]+\/[\w.-]+|\b[\w-]+\.(ts|js|py|php|md|json|yaml|yml|sh|go|rs)\b/i;
|
|
313
|
+
|
|
314
|
+
export function detectBoundedQuestion(taskText: string): { matched: boolean; reason: string } {
|
|
315
|
+
const text = taskText.trim();
|
|
316
|
+
if (!text || text.length > _QUESTION_MAX_LEN || text.includes('\n')) {
|
|
317
|
+
return { matched: false, reason: 'not-bounded' };
|
|
318
|
+
}
|
|
319
|
+
if (_NEEDS_TOOLS_RE.test(text)) {
|
|
320
|
+
return { matched: false, reason: 'references-tool-requiring-objects' };
|
|
321
|
+
}
|
|
322
|
+
// Classify-lead first: its PAYLOAD may quote work verbs ("classify this
|
|
323
|
+
// error: ETIMEDOUT on push") without being a work instruction itself.
|
|
324
|
+
if (_CLASSIFY_LEAD_RE.test(text)) {
|
|
325
|
+
return {
|
|
326
|
+
matched: true,
|
|
327
|
+
reason: 'bounded-question (classification) — rung-0.5 single completion via ask_transport, no session',
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
if (_QUESTION_IMPERATIVE_RE.test(text)) {
|
|
331
|
+
return { matched: false, reason: 'imperative-work-verb' };
|
|
332
|
+
}
|
|
333
|
+
if (_QUESTION_LEAD_RE.test(text) && text.endsWith('?')) {
|
|
334
|
+
return {
|
|
335
|
+
matched: true,
|
|
336
|
+
reason: 'bounded-question — rung-0.5 single completion via ask_transport, no session',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
return { matched: false, reason: 'no-interrogative-shape' };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function classifyLadder(inp: LadderInputs): LadderResult {
|
|
343
|
+
if (inp.interactiveApprovalRequired === true) {
|
|
344
|
+
return {
|
|
345
|
+
rung: null,
|
|
346
|
+
verdict: 'in-session',
|
|
347
|
+
reason: 'interactive-approval-required — caller-stated, needs a human decision before anything may run',
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Rung 0 — never spawns, so checked before the recursive guard and the
|
|
352
|
+
// activation gate: neither one is a precondition for a script that
|
|
353
|
+
// never dispatches anything.
|
|
354
|
+
const lookup = classifyLookup(inp.taskText);
|
|
355
|
+
if (lookup.route === 'primitive') {
|
|
356
|
+
return { rung: 0, verdict: 'script', reason: `lookup-class ${lookup.lookup_class} — ${lookup.reason}` };
|
|
357
|
+
}
|
|
358
|
+
const mechanical = detectMechanicalTransform(inp.taskText);
|
|
359
|
+
if (mechanical.matched) {
|
|
360
|
+
return { rung: 0, verdict: 'script', reason: mechanical.reason };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (inp.insideSubagentSession === true) {
|
|
364
|
+
return {
|
|
365
|
+
rung: null,
|
|
366
|
+
verdict: 'in-session',
|
|
367
|
+
reason:
|
|
368
|
+
'recursive-dispatch guard: this classification runs inside a subagent/teammate session — ' +
|
|
369
|
+
'rungs 1-4 resolve in-session',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (inp.activation.halted) {
|
|
374
|
+
return { rung: null, verdict: 'in-session', reason: 'emergency.orchestration_halt is set' };
|
|
375
|
+
}
|
|
376
|
+
if (!inp.activation.subagent_spawn) {
|
|
377
|
+
return { rung: null, verdict: 'in-session', reason: 'host has no subagent_spawn primitive' };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const contested = detectContestedJudgment(inp.taskText);
|
|
381
|
+
if (contested.matched) {
|
|
382
|
+
return { rung: 4, verdict: 'council', reason: contested.reason };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const communication = detectCommunicationNeed(inp.taskText);
|
|
386
|
+
if (communication.matched) {
|
|
387
|
+
if (inp.agentTeams) {
|
|
388
|
+
return { rung: 3, verdict: 'team', reason: communication.reason };
|
|
389
|
+
}
|
|
390
|
+
const fallback = classifyTask(inp.signals, inp.activation);
|
|
391
|
+
if (fallback.action === 'dispatch' && fallback.mode !== null) {
|
|
392
|
+
return {
|
|
393
|
+
rung: 2,
|
|
394
|
+
verdict: 'subagent',
|
|
395
|
+
mode: fallback.mode,
|
|
396
|
+
degraded_from: 3,
|
|
397
|
+
reason: `${communication.reason} — no agent_teams host capability, degraded to rung 2 (${fallback.reason})`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return {
|
|
401
|
+
rung: null,
|
|
402
|
+
verdict: fallback.action === 'ask' ? 'ask' : 'in-session',
|
|
403
|
+
degraded_from: 3,
|
|
404
|
+
reason: `${communication.reason} — no agent_teams host capability, degraded below rung 2 (${fallback.reason})`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const classification = classifyTask(inp.signals, inp.activation);
|
|
409
|
+
if (classification.action === 'dispatch' && classification.mode !== null) {
|
|
410
|
+
return { rung: 2, verdict: 'subagent', mode: classification.mode, reason: classification.reason };
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// A task below the size floor never delegates — not even via rung 1's
|
|
414
|
+
// narrower single-slice regex. `classifyTask` already enforces this
|
|
415
|
+
// floor for rung 2 (its own `inSession('task below size floor …')`
|
|
416
|
+
// verdict); checked directly on `size_estimate` here — rather than by
|
|
417
|
+
// string-matching `classification.reason` — so this guard survives a
|
|
418
|
+
// future reword of that message, and so it fires regardless of WHICH of
|
|
419
|
+
// classifyTask's `in-session` branches produced the non-dispatch verdict.
|
|
420
|
+
// Before this check, a floor-worthy prompt like "review this diff" (no
|
|
421
|
+
// enumerated slices → size_estimate 0) still matched rung 1's read-heavy
|
|
422
|
+
// regex and dispatched a subagent anyway, silently bypassing the floor
|
|
423
|
+
// `classifyTask` had just enforced one line above.
|
|
424
|
+
if (inp.signals.size_estimate <= SIZE_FLOOR) {
|
|
425
|
+
// Rung 0.5 (token-economy-dispatch 4.2) — a SELF-CONTAINED bounded
|
|
426
|
+
// question below the floor is the one below-spawn shape with its own
|
|
427
|
+
// routing verdict: one completion via ask_transport (no session, no
|
|
428
|
+
// tools, hard-capped). This is an ORCHESTRATOR-facing routing datum —
|
|
429
|
+
// the delegation-nudge carrier deliberately stays silent on it (a
|
|
430
|
+
// user's question is answered in-session at zero marginal cost; see
|
|
431
|
+
// classifyPrompt). Questions referencing files/paths are excluded by
|
|
432
|
+
// the detector: a completion without tools cannot read them.
|
|
433
|
+
const question = detectBoundedQuestion(inp.taskText);
|
|
434
|
+
if (question.matched) {
|
|
435
|
+
return { rung: 0.5, verdict: 'ask', reason: question.reason };
|
|
436
|
+
}
|
|
437
|
+
return {
|
|
438
|
+
rung: null,
|
|
439
|
+
verdict: classification.action === 'ask' ? 'ask' : 'in-session',
|
|
440
|
+
reason: classification.reason,
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const single = detectBoundedReadHeavySlice(inp.taskText);
|
|
445
|
+
if (single.matched) {
|
|
446
|
+
return { rung: 1, verdict: 'subagent', mode: null, reason: single.reason };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
rung: null,
|
|
451
|
+
verdict: classification.action === 'ask' ? 'ask' : 'in-session',
|
|
452
|
+
reason: classification.reason,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Explanation trail (additive — Phase 4.1, road-to-feedback-9-29) ──────
|
|
457
|
+
//
|
|
458
|
+
// `classifyLadder` short-circuits on the first rung that resolves, so its
|
|
459
|
+
// minimal `{rung, verdict, reason}` return cannot answer "why NOT team /
|
|
460
|
+
// why NOT council / why no spawn". This layer answers exactly that without
|
|
461
|
+
// touching the minimal shape current callers depend on: the result is taken
|
|
462
|
+
// from `classifyLadder` itself (never re-derived, so the two can never
|
|
463
|
+
// disagree on the headline), and the per-rung trail is reconstructed from
|
|
464
|
+
// the SAME exported pure detectors and gate fields the resolver reads, in
|
|
465
|
+
// the same fixed priority order.
|
|
466
|
+
|
|
467
|
+
export type RungStatus = 'taken' | 'rejected' | 'not-reached';
|
|
468
|
+
|
|
469
|
+
export interface RungEvaluation {
|
|
470
|
+
rung: 0 | 0.5 | 1 | 2 | 3 | 4;
|
|
471
|
+
/** The verdict this rung resolves to when taken. */
|
|
472
|
+
resolves_to: 'script' | 'ask' | 'subagent' | 'team' | 'council';
|
|
473
|
+
status: RungStatus;
|
|
474
|
+
reason: string;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export interface LadderExplanation {
|
|
478
|
+
/** The authoritative resolution — exactly `classifyLadder(inp)`. */
|
|
479
|
+
result: LadderResult;
|
|
480
|
+
/**
|
|
481
|
+
* Per-rung trail in the resolver's fixed priority order: 0, 4, 3, 2, 0.5, 1.
|
|
482
|
+
*
|
|
483
|
+
* 0.5 sits where the resolver checks it — inside the size-floor branch,
|
|
484
|
+
* after rung 2's verdict and before rung 1 — not where its number would
|
|
485
|
+
* sort. The order IS the contract, so the trail follows the walk.
|
|
486
|
+
*/
|
|
487
|
+
trail: RungEvaluation[];
|
|
488
|
+
/** Present only when `result.rung` is null (∅) — the why-no-spawn reason. */
|
|
489
|
+
no_spawn_reason?: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Explain one ladder resolution: the rung taken, every rung genuinely
|
|
494
|
+
* evaluated-and-rejected (with the detector's own reason), and rungs the
|
|
495
|
+
* short-circuit never reached (marked `not-reached`, never given a
|
|
496
|
+
* fabricated rejection reason — a lower rung's detector might well have
|
|
497
|
+
* matched had it been consulted).
|
|
498
|
+
*/
|
|
499
|
+
export function explainLadder(inp: LadderInputs): LadderExplanation {
|
|
500
|
+
const result = classifyLadder(inp);
|
|
501
|
+
const trail: RungEvaluation[] = [];
|
|
502
|
+
const approval = inp.interactiveApprovalRequired === true;
|
|
503
|
+
|
|
504
|
+
// Rung 0 — evaluated first, exempt from the guards below.
|
|
505
|
+
if (result.rung === 0) {
|
|
506
|
+
trail.push({ rung: 0, resolves_to: 'script', status: 'taken', reason: result.reason });
|
|
507
|
+
} else if (approval) {
|
|
508
|
+
trail.push({
|
|
509
|
+
rung: 0,
|
|
510
|
+
resolves_to: 'script',
|
|
511
|
+
status: 'not-reached',
|
|
512
|
+
reason: 'interactive-approval-required resolves before every rung',
|
|
513
|
+
});
|
|
514
|
+
} else {
|
|
515
|
+
const lookup = classifyLookup(inp.taskText);
|
|
516
|
+
const mechanical = detectMechanicalTransform(inp.taskText);
|
|
517
|
+
trail.push({ rung: 0, resolves_to: 'script', status: 'rejected', reason: `${lookup.reason}; ${mechanical.reason}` });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// A gate that resolved ∅ before rung 4 blocks rungs 4-1 wholesale; a
|
|
521
|
+
// resolution at rung 0 means they were never consulted either.
|
|
522
|
+
const gate: string | null = approval
|
|
523
|
+
? 'interactive-approval-required — caller-stated, resolves ∅'
|
|
524
|
+
: result.rung === 0
|
|
525
|
+
? 'resolved at rung 0'
|
|
526
|
+
: inp.insideSubagentSession === true
|
|
527
|
+
? 'recursive-dispatch guard — running inside a subagent/teammate session'
|
|
528
|
+
: inp.activation.halted
|
|
529
|
+
? 'emergency.orchestration_halt is set'
|
|
530
|
+
: !inp.activation.subagent_spawn
|
|
531
|
+
? 'host has no subagent_spawn primitive'
|
|
532
|
+
: null;
|
|
533
|
+
|
|
534
|
+
const notReached = (why: string): string => `not evaluated — ${why}`;
|
|
535
|
+
|
|
536
|
+
// Rung 4 — council.
|
|
537
|
+
if (gate !== null) {
|
|
538
|
+
trail.push({ rung: 4, resolves_to: 'council', status: 'not-reached', reason: notReached(gate) });
|
|
539
|
+
} else if (result.rung === 4) {
|
|
540
|
+
trail.push({ rung: 4, resolves_to: 'council', status: 'taken', reason: result.reason });
|
|
541
|
+
} else {
|
|
542
|
+
trail.push({ rung: 4, resolves_to: 'council', status: 'rejected', reason: detectContestedJudgment(inp.taskText).reason });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Rung 3 — team.
|
|
546
|
+
if (gate !== null) {
|
|
547
|
+
trail.push({ rung: 3, resolves_to: 'team', status: 'not-reached', reason: notReached(gate) });
|
|
548
|
+
} else if (result.rung === 4) {
|
|
549
|
+
trail.push({ rung: 3, resolves_to: 'team', status: 'not-reached', reason: notReached('resolved at rung 4') });
|
|
550
|
+
} else if (result.rung === 3) {
|
|
551
|
+
trail.push({ rung: 3, resolves_to: 'team', status: 'taken', reason: result.reason });
|
|
552
|
+
} else {
|
|
553
|
+
const communication = detectCommunicationNeed(inp.taskText);
|
|
554
|
+
const reason =
|
|
555
|
+
communication.matched && result.degraded_from === 3
|
|
556
|
+
? `${communication.reason} — but host reports agent_teams: false, degraded (see resolved rung)`
|
|
557
|
+
: communication.reason;
|
|
558
|
+
trail.push({ rung: 3, resolves_to: 'team', status: 'rejected', reason });
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Rungs 2 and 1 — classifyTask's verdict, then the single-slice check.
|
|
562
|
+
const resolvedAbove = result.rung === 4 || result.rung === 3;
|
|
563
|
+
if (gate !== null || resolvedAbove) {
|
|
564
|
+
const why = gate ?? `resolved at rung ${result.rung}`;
|
|
565
|
+
trail.push({ rung: 2, resolves_to: 'subagent', status: 'not-reached', reason: notReached(why) });
|
|
566
|
+
trail.push({ rung: 0.5, resolves_to: 'ask', status: 'not-reached', reason: notReached(why) });
|
|
567
|
+
trail.push({ rung: 1, resolves_to: 'subagent', status: 'not-reached', reason: notReached(why) });
|
|
568
|
+
} else {
|
|
569
|
+
const classification = classifyTask(inp.signals, inp.activation);
|
|
570
|
+
if (result.rung === 2) {
|
|
571
|
+
trail.push({ rung: 2, resolves_to: 'subagent', status: 'taken', reason: result.reason });
|
|
572
|
+
trail.push({ rung: 0.5, resolves_to: 'ask', status: 'not-reached', reason: notReached('resolved at rung 2') });
|
|
573
|
+
trail.push({ rung: 1, resolves_to: 'subagent', status: 'not-reached', reason: notReached('resolved at rung 2') });
|
|
574
|
+
} else {
|
|
575
|
+
trail.push({ rung: 2, resolves_to: 'subagent', status: 'rejected', reason: classification.reason });
|
|
576
|
+
if (result.rung === 1) {
|
|
577
|
+
trail.push({
|
|
578
|
+
rung: 0.5,
|
|
579
|
+
resolves_to: 'ask',
|
|
580
|
+
status: 'not-reached',
|
|
581
|
+
reason: notReached(`task above the size floor (${inp.signals.size_estimate} > ${SIZE_FLOOR}) — rung 0.5 is reachable only below it`),
|
|
582
|
+
});
|
|
583
|
+
trail.push({ rung: 1, resolves_to: 'subagent', status: 'taken', reason: result.reason });
|
|
584
|
+
} else if (result.degraded_from === 3) {
|
|
585
|
+
// The communication branch returns from INSIDE rung 3's handling
|
|
586
|
+
// (its `classifyTask` fallback is the rung-2 attempt), so it exits
|
|
587
|
+
// before `detectBoundedReadHeavySlice` is ever called. Reporting
|
|
588
|
+
// rung 1 as `rejected` here quoted that detector's reason — and
|
|
589
|
+
// when the detector MATCHES, that reason describes a match, so
|
|
590
|
+
// the trail read "rejected: one lite-tier subagent instead of
|
|
591
|
+
// session-context burn". A maintainer asking why nothing spawned
|
|
592
|
+
// was told rung 1 declined, hiding the missing agent_teams
|
|
593
|
+
// capability that actually ended the walk.
|
|
594
|
+
const skipped = notReached('the rung-3 communication branch resolved before it was consulted');
|
|
595
|
+
trail.push({ rung: 0.5, resolves_to: 'ask', status: 'not-reached', reason: skipped });
|
|
596
|
+
trail.push({ rung: 1, resolves_to: 'subagent', status: 'not-reached', reason: skipped });
|
|
597
|
+
} else if (inp.signals.size_estimate <= SIZE_FLOOR) {
|
|
598
|
+
// Rung 0.5 lives INSIDE the resolver's floor branch, between rung
|
|
599
|
+
// 2 and rung 1, so it is reachable only here. Emitting it in that
|
|
600
|
+
// position keeps the trail's order the resolver's order; omitting
|
|
601
|
+
// it (as this trail did before the rung landed) would report a
|
|
602
|
+
// walk that skipped a rung the resolver actually consulted.
|
|
603
|
+
if (result.rung === 0.5) {
|
|
604
|
+
trail.push({ rung: 0.5, resolves_to: 'ask', status: 'taken', reason: result.reason });
|
|
605
|
+
} else {
|
|
606
|
+
trail.push({
|
|
607
|
+
rung: 0.5,
|
|
608
|
+
resolves_to: 'ask',
|
|
609
|
+
status: 'rejected',
|
|
610
|
+
reason: detectBoundedQuestion(inp.taskText).reason,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
trail.push({
|
|
614
|
+
rung: 1,
|
|
615
|
+
resolves_to: 'subagent',
|
|
616
|
+
status: 'rejected',
|
|
617
|
+
reason: `task below size floor (${inp.signals.size_estimate} <= ${SIZE_FLOOR}) — rung 1 never bypasses the floor`,
|
|
618
|
+
});
|
|
619
|
+
} else {
|
|
620
|
+
// Above the floor the resolver never enters the branch rung 0.5
|
|
621
|
+
// lives in, so it was not consulted — not declined.
|
|
622
|
+
trail.push({
|
|
623
|
+
rung: 0.5,
|
|
624
|
+
resolves_to: 'ask',
|
|
625
|
+
status: 'not-reached',
|
|
626
|
+
reason: notReached(`task above the size floor (${inp.signals.size_estimate} > ${SIZE_FLOOR}) — rung 0.5 is reachable only below it`),
|
|
627
|
+
});
|
|
628
|
+
trail.push({ rung: 1, resolves_to: 'subagent', status: 'rejected', reason: detectBoundedReadHeavySlice(inp.taskText).reason });
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return result.rung === null ? { result, trail, no_spawn_reason: result.reason } : { result, trail };
|
|
634
|
+
}
|
|
@@ -14,6 +14,14 @@
|
|
|
14
14
|
*
|
|
15
15
|
* This helper decides; it never runs the measurement. The signal is realized
|
|
16
16
|
* orchestration telemetry, aggregated + evaluated out of band.
|
|
17
|
+
*
|
|
18
|
+
* **Amendment (road-to-always-on-orchestration Phase 1).** `subagents.auto`
|
|
19
|
+
* was DELETED as a settings key — always-on orchestration carries no
|
|
20
|
+
* per-layer on/off setting to demote. `resolveShippedDefault` (the
|
|
21
|
+
* `subagents.auto`-specific half below) has no production caller today and
|
|
22
|
+
* is kept as a historical record of the demotion mechanism, not a live
|
|
23
|
+
* decision path; `resolveRecursiveDefault` (the `verification.recursive`
|
|
24
|
+
* half) is unaffected by this deletion.
|
|
17
25
|
*/
|
|
18
26
|
|
|
19
27
|
export interface BenchmarkResult {
|