@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,382 @@
|
|
|
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 | 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' // ∅ — ambiguous; a VERDICT to the user, never a speculative spawn
|
|
48
|
+
| 'in-session'; // ∅ — halted, no host primitive, trivial, approval-required, or recursive guard
|
|
49
|
+
|
|
50
|
+
export interface LadderResult {
|
|
51
|
+
rung: LadderRung;
|
|
52
|
+
verdict: LadderVerdict;
|
|
53
|
+
reason: string;
|
|
54
|
+
/**
|
|
55
|
+
* Present only when a HIGHER rung's signal fired but its precondition
|
|
56
|
+
* failed (today: rung 3's `agentTeams` host-capability check) and
|
|
57
|
+
* resolution fell back to a lower rung/verdict. Names the rung the
|
|
58
|
+
* signal originally pointed at, never the resolved one.
|
|
59
|
+
*/
|
|
60
|
+
degraded_from?: LadderRung;
|
|
61
|
+
/**
|
|
62
|
+
* Carried through from `classifyTask` when the resolved rung is 1 or 2
|
|
63
|
+
* — the concrete `subagent-orchestration` mode a caller dispatches on.
|
|
64
|
+
* `null` for rung 1 (a single bounded slice has no do-in-steps /
|
|
65
|
+
* do-in-parallel shape of its own) and for every non-subagent verdict.
|
|
66
|
+
* Not part of the roadmap's minimal `{rung, verdict, reason,
|
|
67
|
+
* degraded_from?}` shape — added because the delegation-nudge hook
|
|
68
|
+
* (2.4) needs the concrete mode to build its injected line.
|
|
69
|
+
*/
|
|
70
|
+
mode?: DispatchMode | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface LadderInputs {
|
|
74
|
+
/**
|
|
75
|
+
* Raw task/prompt text — regex-only signal extraction for rungs 0, 1, 3,
|
|
76
|
+
* and 4, in the same discipline as `delegation_nudge_hook.ts`'s
|
|
77
|
+
* extractors (cheap, no LLM call; false-positive/negative trade-offs
|
|
78
|
+
* documented per pattern below).
|
|
79
|
+
*/
|
|
80
|
+
taskText: string;
|
|
81
|
+
/** Structural signals `classifyTask` already consumes — rungs 1/2. */
|
|
82
|
+
signals: TaskSignals;
|
|
83
|
+
/** Emergency halt + subagent_spawn primitive — gates every rung above 0. */
|
|
84
|
+
activation: ActivationInputs;
|
|
85
|
+
/**
|
|
86
|
+
* Host-capability manifest's `agent_teams` field
|
|
87
|
+
* (`host_capability.ts::probeHostCapabilities`) — rung 3's precondition.
|
|
88
|
+
* Absent/`false` degrades a matched rung-3 signal to rung 2.
|
|
89
|
+
*/
|
|
90
|
+
agentTeams: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Caller states this step needs an explicit human decision before
|
|
93
|
+
* anything may run (an approval gate, a Hard-Floor action, …) — NEVER
|
|
94
|
+
* inferred from `taskText`. Checked first; overrides every other signal.
|
|
95
|
+
*/
|
|
96
|
+
interactiveApprovalRequired?: boolean;
|
|
97
|
+
/**
|
|
98
|
+
* Recursive-dispatch guard (2.3). See {@link classifyLadder}'s doc
|
|
99
|
+
* comment for the honest scope of what this suite can and cannot
|
|
100
|
+
* detect on its own — this field is a CALLER-SUPPLIED fact, never
|
|
101
|
+
* something this resolver probes for itself.
|
|
102
|
+
*/
|
|
103
|
+
insideSubagentSession?: boolean;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Rung 0 — mechanical-transform signals (regex-only) ──────────────────
|
|
107
|
+
//
|
|
108
|
+
// `classifyLookup` (auto_dispatch.ts, road-to-lean-agent-init L0) already
|
|
109
|
+
// covers the READ-ONLY half of "deterministic script, no spawn" (definition/
|
|
110
|
+
// references/string-existence/report-run lookups). This adds the WRITE half
|
|
111
|
+
// the roadmap names explicitly: mechanical transforms with no semantics to
|
|
112
|
+
// judge — a rename, a codemod, a formatter run, a bulk search-and-replace.
|
|
113
|
+
// Each pattern requires the verb AND its object shape (e.g. "rename X to Y",
|
|
114
|
+
// not bare "rename") so a sentence that merely MENTIONS renaming in passing
|
|
115
|
+
// ("we renamed this last week") does not false-positive into rung 0.
|
|
116
|
+
const RENAME_RE = /\brename\s+\S+\s+to\s+\S+\b/i;
|
|
117
|
+
const CODEMOD_RE = /\bcodemod\b/i;
|
|
118
|
+
const FORMATTER_RUN_RE =
|
|
119
|
+
/\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;
|
|
120
|
+
const SEARCH_REPLACE_RE = /\bsearch-and-replace\b|\bfind-and-replace\b|\bbulk\s+rename\b/i;
|
|
121
|
+
|
|
122
|
+
export function detectMechanicalTransform(text: string): { matched: boolean; reason: string } {
|
|
123
|
+
const rename = RENAME_RE.exec(text);
|
|
124
|
+
if (rename) {
|
|
125
|
+
return { matched: true, reason: `rename signal ("${rename[0]}") — deterministic script, no spawn` };
|
|
126
|
+
}
|
|
127
|
+
const codemod = CODEMOD_RE.exec(text);
|
|
128
|
+
if (codemod) {
|
|
129
|
+
return { matched: true, reason: `codemod signal ("${codemod[0]}") — deterministic script, no spawn` };
|
|
130
|
+
}
|
|
131
|
+
const formatter = FORMATTER_RUN_RE.exec(text);
|
|
132
|
+
if (formatter) {
|
|
133
|
+
return { matched: true, reason: `formatter-run signal ("${formatter[0]}") — deterministic script, no spawn` };
|
|
134
|
+
}
|
|
135
|
+
const searchReplace = SEARCH_REPLACE_RE.exec(text);
|
|
136
|
+
if (searchReplace) {
|
|
137
|
+
return {
|
|
138
|
+
matched: true,
|
|
139
|
+
reason: `search-and-replace/bulk-rename signal ("${searchReplace[0]}") — deterministic script, no spawn`,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return { matched: false, reason: 'no mechanical-transform signal' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Rung 1 — single bounded read-heavy slice (regex-only) ───────────────
|
|
146
|
+
//
|
|
147
|
+
// The one rung `classifyTask` structurally cannot see: its delegable rules
|
|
148
|
+
// all require >=2 slices, an ordered plan, or a declared `parallelizable`
|
|
149
|
+
// frontmatter value. A single "read this whole file and summarize it" task
|
|
150
|
+
// is real, non-trivial work that still benefits from a lite-tier subagent
|
|
151
|
+
// (session-context savings) but has no multi-slice shape at all. Requires a
|
|
152
|
+
// read-heavy VERB plus a bounded, singular TARGET phrase — a bare "read the
|
|
153
|
+
// code" without a stated target is too weak a signal to spawn on.
|
|
154
|
+
const SINGLE_SLICE_READHEAVY_RE =
|
|
155
|
+
/\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;
|
|
156
|
+
|
|
157
|
+
export function detectBoundedReadHeavySlice(text: string): { matched: boolean; reason: string } {
|
|
158
|
+
const m = SINGLE_SLICE_READHEAVY_RE.exec(text);
|
|
159
|
+
return m
|
|
160
|
+
? {
|
|
161
|
+
matched: true,
|
|
162
|
+
reason: `single bounded read-heavy slice ("${m[0]}") — one lite-tier subagent instead of session-context burn`,
|
|
163
|
+
}
|
|
164
|
+
: { matched: false, reason: 'no single-slice read-heavy signal' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ── Rung 3 — communication-need signals (team) ───────────────────────────
|
|
168
|
+
//
|
|
169
|
+
// "Slices that must communicate" per the committed table: cross-layer work,
|
|
170
|
+
// an in-flight review-with-challenge shape, or an explicit shared work
|
|
171
|
+
// queue — signals a single implementer-then-judge handoff, or a set of
|
|
172
|
+
// independent parallel slices, cannot express, because the workers need to
|
|
173
|
+
// see each other's state DURING the run, not just at handoff.
|
|
174
|
+
const CROSS_LAYER_RE =
|
|
175
|
+
/\bcross[- ]layer\b|\bcross[- ]cutting\b|\bback[- ]?end\s+and\s+front[- ]?end\b|\bfront[- ]?end\s+and\s+back[- ]?end\b/i;
|
|
176
|
+
const REVIEW_WITH_CHALLENGE_RE =
|
|
177
|
+
/\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;
|
|
178
|
+
const SHARED_TASK_LIST_RE =
|
|
179
|
+
/\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;
|
|
180
|
+
|
|
181
|
+
export function detectCommunicationNeed(text: string): { matched: boolean; reason: string } {
|
|
182
|
+
const crossLayer = CROSS_LAYER_RE.exec(text);
|
|
183
|
+
if (crossLayer) {
|
|
184
|
+
return { matched: true, reason: `cross-layer communication signal ("${crossLayer[0]}")` };
|
|
185
|
+
}
|
|
186
|
+
const reviewChallenge = REVIEW_WITH_CHALLENGE_RE.exec(text);
|
|
187
|
+
if (reviewChallenge) {
|
|
188
|
+
return { matched: true, reason: `review-with-challenge signal ("${reviewChallenge[0]}")` };
|
|
189
|
+
}
|
|
190
|
+
const sharedList = SHARED_TASK_LIST_RE.exec(text);
|
|
191
|
+
if (sharedList) {
|
|
192
|
+
return { matched: true, reason: `shared-task-list signal ("${sharedList[0]}")` };
|
|
193
|
+
}
|
|
194
|
+
return { matched: false, reason: 'no communication-need signal' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Rung 4 — contested-judgment signals (council) ────────────────────────
|
|
198
|
+
//
|
|
199
|
+
// "Judgment under disagreement" per the committed table: a design decision,
|
|
200
|
+
// a proposed security downgrade, or an escalation at a release gate — the
|
|
201
|
+
// three shapes the roadmap names explicitly. Deliberately its OWN, narrower
|
|
202
|
+
// vocabulary rather than an import of `ai_council/necessity.ts`'s broader
|
|
203
|
+
// `NECESSARY_TRIGGERS` (architecture/tradeoff/ambiguity/strategic): that
|
|
204
|
+
// module stays the council's own necessity gate; this resolver only needs
|
|
205
|
+
// the narrow signal that should route a TASK to rung 4 in the first place.
|
|
206
|
+
// A bare `\badr\b` (no decision-context anchor) matched "update the ADR
|
|
207
|
+
// index" and "fix the release-gate CI script typo" — routine maintenance
|
|
208
|
+
// prose that names the artefact/gate but decides nothing — and suppressed
|
|
209
|
+
// the correct rung-2 nudge on both. Both alternatives below now require an
|
|
210
|
+
// explicit decision/escalation VERB within 40 chars of the artefact/gate
|
|
211
|
+
// token, on either side, so the bare noun alone never fires.
|
|
212
|
+
const DESIGN_DECISION_RE =
|
|
213
|
+
/\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;
|
|
214
|
+
const SECURITY_DOWNGRADE_RE = /\bsecurity\s+downgrade\b|\bdowngrade\s+(?:the\s+)?security\b|\bsecurity\s+exception\b/i;
|
|
215
|
+
const RELEASE_GATE_ESCALATION_RE =
|
|
216
|
+
/\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;
|
|
217
|
+
|
|
218
|
+
export function detectContestedJudgment(text: string): { matched: boolean; reason: string } {
|
|
219
|
+
const design = DESIGN_DECISION_RE.exec(text);
|
|
220
|
+
if (design) {
|
|
221
|
+
return { matched: true, reason: `design-decision signal ("${design[0]}")` };
|
|
222
|
+
}
|
|
223
|
+
const security = SECURITY_DOWNGRADE_RE.exec(text);
|
|
224
|
+
if (security) {
|
|
225
|
+
return { matched: true, reason: `security-downgrade signal ("${security[0]}")` };
|
|
226
|
+
}
|
|
227
|
+
const releaseGate = RELEASE_GATE_ESCALATION_RE.exec(text);
|
|
228
|
+
if (releaseGate) {
|
|
229
|
+
return { matched: true, reason: `release-gate-escalation signal ("${releaseGate[0]}")` };
|
|
230
|
+
}
|
|
231
|
+
return { matched: false, reason: 'no contested-judgment signal' };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Resolve the judgment-ladder verdict for one task.
|
|
236
|
+
*
|
|
237
|
+
* FIXED priority order (the order is the contract):
|
|
238
|
+
*
|
|
239
|
+
* 1. `interactiveApprovalRequired` — caller-stated, overrides everything.
|
|
240
|
+
* 2. Rung 0 (`classifyLookup` primitive match OR a mechanical-transform
|
|
241
|
+
* signal) — never spawns, so neither the recursive guard nor the
|
|
242
|
+
* activation gate applies to it.
|
|
243
|
+
* 3. Recursive-dispatch guard (2.3) — `insideSubagentSession` resolves ∅
|
|
244
|
+
* for every rung below this point.
|
|
245
|
+
* 4. Activation gate (`halted` / no `subagent_spawn`) — nothing below this
|
|
246
|
+
* point can spawn without it.
|
|
247
|
+
* 5. Rung 4 (contested-judgment signal) — council.
|
|
248
|
+
* 6. Rung 3 (communication-need signal) — team when `agentTeams`; otherwise
|
|
249
|
+
* degrades to whatever `classifyTask` resolves on the SAME signals
|
|
250
|
+
* (usually rung 2 — slices that must communicate are usually plural
|
|
251
|
+
* slices — but this honestly follows `classifyTask`'s real verdict
|
|
252
|
+
* rather than fabricating a slice count nothing confirmed).
|
|
253
|
+
* 7. Rungs 1/2 — `classifyTask`'s dispatch verdict. `do-in-steps` and
|
|
254
|
+
* `do-in-parallel` both resolve rung 2 in this v1: the roadmap's rung
|
|
255
|
+
* 1-vs-2 split is single-slice vs multi-slice, and `classifyTask`
|
|
256
|
+
* structurally never dispatches on a single slice (its rules require
|
|
257
|
+
* >=2 slices, an ordered plan, or a declared `parallelizable` value) —
|
|
258
|
+
* so every `classifyTask` dispatch verdict is inherently multi-slice.
|
|
259
|
+
* Its `ask` / size-floor `in-session` outcomes get one more check
|
|
260
|
+
* first: rung 1's single-bounded-slice signal, the one shape
|
|
261
|
+
* `classifyTask`'s own rules cannot see.
|
|
262
|
+
*
|
|
263
|
+
* ## Recursive-dispatch guard — honest scope (2.3)
|
|
264
|
+
*
|
|
265
|
+
* No verified host discriminator exists for "is this classification running
|
|
266
|
+
* inside a subagent/teammate session". This roadmap's own
|
|
267
|
+
* `point-of-action-carrier` blocker records that the upstream PreToolUse
|
|
268
|
+
* agent-identity request is closed NOT_PLANNED, and nothing in this repo's
|
|
269
|
+
* hook envelope (`event` / `payload` / `platform` / `workspace_root` —
|
|
270
|
+
* `delegation_nudge_hook.ts`'s own `main()`) carries a session-lineage
|
|
271
|
+
* field. `insideSubagentSession` is therefore a CALLER-SUPPLIED fact: the
|
|
272
|
+
* code path that dispatched the subagent/teammate loop is the one place
|
|
273
|
+
* that genuinely knows. This resolver does NOT probe `process.env` for
|
|
274
|
+
* it — inventing an unverified `CLAUDE_AGENT_*`-shaped variable name here
|
|
275
|
+
* would be exactly the hallucinated-field failure `source-discovery-gate`
|
|
276
|
+
* exists to stop. Documented as a gap, not silently assumed away: on a
|
|
277
|
+
* host/caller that never sets this field, the guard is a no-op and rungs
|
|
278
|
+
* 1-4 resolve normally even inside a subagent — the same gap the
|
|
279
|
+
* `point-of-action-carrier` blocker already names as its pre-registered
|
|
280
|
+
* null ("no discriminator" is publishable and does not block this roadmap).
|
|
281
|
+
*/
|
|
282
|
+
export function classifyLadder(inp: LadderInputs): LadderResult {
|
|
283
|
+
if (inp.interactiveApprovalRequired === true) {
|
|
284
|
+
return {
|
|
285
|
+
rung: null,
|
|
286
|
+
verdict: 'in-session',
|
|
287
|
+
reason: 'interactive-approval-required — caller-stated, needs a human decision before anything may run',
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Rung 0 — never spawns, so checked before the recursive guard and the
|
|
292
|
+
// activation gate: neither one is a precondition for a script that
|
|
293
|
+
// never dispatches anything.
|
|
294
|
+
const lookup = classifyLookup(inp.taskText);
|
|
295
|
+
if (lookup.route === 'primitive') {
|
|
296
|
+
return { rung: 0, verdict: 'script', reason: `lookup-class ${lookup.lookup_class} — ${lookup.reason}` };
|
|
297
|
+
}
|
|
298
|
+
const mechanical = detectMechanicalTransform(inp.taskText);
|
|
299
|
+
if (mechanical.matched) {
|
|
300
|
+
return { rung: 0, verdict: 'script', reason: mechanical.reason };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (inp.insideSubagentSession === true) {
|
|
304
|
+
return {
|
|
305
|
+
rung: null,
|
|
306
|
+
verdict: 'in-session',
|
|
307
|
+
reason:
|
|
308
|
+
'recursive-dispatch guard: this classification runs inside a subagent/teammate session — ' +
|
|
309
|
+
'rungs 1-4 resolve in-session',
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (inp.activation.halted) {
|
|
314
|
+
return { rung: null, verdict: 'in-session', reason: 'emergency.orchestration_halt is set' };
|
|
315
|
+
}
|
|
316
|
+
if (!inp.activation.subagent_spawn) {
|
|
317
|
+
return { rung: null, verdict: 'in-session', reason: 'host has no subagent_spawn primitive' };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const contested = detectContestedJudgment(inp.taskText);
|
|
321
|
+
if (contested.matched) {
|
|
322
|
+
return { rung: 4, verdict: 'council', reason: contested.reason };
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const communication = detectCommunicationNeed(inp.taskText);
|
|
326
|
+
if (communication.matched) {
|
|
327
|
+
if (inp.agentTeams) {
|
|
328
|
+
return { rung: 3, verdict: 'team', reason: communication.reason };
|
|
329
|
+
}
|
|
330
|
+
const fallback = classifyTask(inp.signals, inp.activation);
|
|
331
|
+
if (fallback.action === 'dispatch' && fallback.mode !== null) {
|
|
332
|
+
return {
|
|
333
|
+
rung: 2,
|
|
334
|
+
verdict: 'subagent',
|
|
335
|
+
mode: fallback.mode,
|
|
336
|
+
degraded_from: 3,
|
|
337
|
+
reason: `${communication.reason} — no agent_teams host capability, degraded to rung 2 (${fallback.reason})`,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
return {
|
|
341
|
+
rung: null,
|
|
342
|
+
verdict: fallback.action === 'ask' ? 'ask' : 'in-session',
|
|
343
|
+
degraded_from: 3,
|
|
344
|
+
reason: `${communication.reason} — no agent_teams host capability, degraded below rung 2 (${fallback.reason})`,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const classification = classifyTask(inp.signals, inp.activation);
|
|
349
|
+
if (classification.action === 'dispatch' && classification.mode !== null) {
|
|
350
|
+
return { rung: 2, verdict: 'subagent', mode: classification.mode, reason: classification.reason };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// A task below the size floor never delegates — not even via rung 1's
|
|
354
|
+
// narrower single-slice regex. `classifyTask` already enforces this
|
|
355
|
+
// floor for rung 2 (its own `inSession('task below size floor …')`
|
|
356
|
+
// verdict); checked directly on `size_estimate` here — rather than by
|
|
357
|
+
// string-matching `classification.reason` — so this guard survives a
|
|
358
|
+
// future reword of that message, and so it fires regardless of WHICH of
|
|
359
|
+
// classifyTask's `in-session` branches produced the non-dispatch verdict.
|
|
360
|
+
// Before this check, a floor-worthy prompt like "review this diff" (no
|
|
361
|
+
// enumerated slices → size_estimate 0) still matched rung 1's read-heavy
|
|
362
|
+
// regex and dispatched a subagent anyway, silently bypassing the floor
|
|
363
|
+
// `classifyTask` had just enforced one line above.
|
|
364
|
+
if (inp.signals.size_estimate <= SIZE_FLOOR) {
|
|
365
|
+
return {
|
|
366
|
+
rung: null,
|
|
367
|
+
verdict: classification.action === 'ask' ? 'ask' : 'in-session',
|
|
368
|
+
reason: classification.reason,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const single = detectBoundedReadHeavySlice(inp.taskText);
|
|
373
|
+
if (single.matched) {
|
|
374
|
+
return { rung: 1, verdict: 'subagent', mode: null, reason: single.reason };
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
rung: null,
|
|
379
|
+
verdict: classification.action === 'ask' ? 'ask' : 'in-session',
|
|
380
|
+
reason: classification.reason,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
@@ -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 {
|
|
@@ -28,6 +28,8 @@ export type TierSource = 'static' | 'inferred' | 'inherit';
|
|
|
28
28
|
export type Band = 'low' | 'medium' | 'high';
|
|
29
29
|
export type LinePhase = 'refine' | 'memory' | 'analyze' | 'plan' | 'implement' | 'test' | 'verify' | 'report';
|
|
30
30
|
export type LineOutcome = 'success' | 'blocked' | 'skipped' | 'error';
|
|
31
|
+
/** Which capsule-emission trigger arm fired first (Phase 1 shadow comparison). */
|
|
32
|
+
export type TriggerArm = 'watermark' | 'saturation' | 'tie';
|
|
31
33
|
/** The orchestration form the form-gate selected (road-to-opt-subagent-harvest P2). */
|
|
32
34
|
export type DispatchModeId =
|
|
33
35
|
| 'do-and-judge'
|
|
@@ -96,6 +98,20 @@ export interface RecordInput {
|
|
|
96
98
|
/** Rules the worker actually applied/cited (count of the envelope's
|
|
97
99
|
* `rules_applied`-equivalent on the worker side). Counts only. */
|
|
98
100
|
rules_used?: number | null | undefined;
|
|
101
|
+
// ── capsule shadow-measurement fields (road-to-worker-generation-recycling
|
|
102
|
+
// Phase 1.2). SHADOW ONLY: the worker still runs to stop-loss, nothing
|
|
103
|
+
// reads a capsule. All counts / enums — a capsule's CONTENT never
|
|
104
|
+
// reaches telemetry, only its shape. ──
|
|
105
|
+
/** A CHECKPOINT capsule was emitted at the watermark. */
|
|
106
|
+
capsule_emitted?: boolean | undefined;
|
|
107
|
+
/** Total entries across the capsule's arrays — a size proxy, never content. */
|
|
108
|
+
capsule_entries?: number | null | undefined;
|
|
109
|
+
/** 1-based step at which the token-watermark arm fired (null = never). */
|
|
110
|
+
watermark_step?: number | null | undefined;
|
|
111
|
+
/** 1-based step at which the novelty-saturation arm fired (null = never). */
|
|
112
|
+
saturation_step?: number | null | undefined;
|
|
113
|
+
/** Which arm fired first on this dispatch — the paired-comparison datum. */
|
|
114
|
+
trigger_arm_earlier?: TriggerArm | null | undefined;
|
|
99
115
|
// Audit-log envelope (sensible defaults for a dispatch record)
|
|
100
116
|
phase?: LinePhase | undefined;
|
|
101
117
|
outcome?: LineOutcome | undefined;
|
|
@@ -123,6 +139,7 @@ const BANDS: readonly Band[] = ['low', 'medium', 'high'];
|
|
|
123
139
|
const PHASES: readonly LinePhase[] = ['refine', 'memory', 'analyze', 'plan', 'implement', 'test', 'verify', 'report'];
|
|
124
140
|
const LOOKUP_CLASSES: readonly LookupClass[] = ['definition', 'references', 'string-existence', 'report-run'];
|
|
125
141
|
const ROUTES_TAKEN: readonly RouteTaken[] = ['primitive', 'subagent'];
|
|
142
|
+
const TRIGGER_ARMS: readonly TriggerArm[] = ['watermark', 'saturation', 'tie'];
|
|
126
143
|
/** Hex-only payload hash — a hash can never smuggle content (privacy by construction). */
|
|
127
144
|
const PAYLOAD_HASH_RE = /^[a-f0-9]{8,64}$/i;
|
|
128
145
|
/** Id-shaped origin tag — enum-ish, never free-form prose. */
|
|
@@ -233,6 +250,20 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
233
250
|
errors.push('rules_used cannot exceed rules_carried (a worker cannot apply a rule it was not given)');
|
|
234
251
|
}
|
|
235
252
|
|
|
253
|
+
if (input.capsule_emitted !== undefined && typeof input.capsule_emitted !== 'boolean') {
|
|
254
|
+
errors.push('capsule_emitted must be a boolean or omitted');
|
|
255
|
+
}
|
|
256
|
+
for (const [key, v] of [
|
|
257
|
+
['capsule_entries', input.capsule_entries],
|
|
258
|
+
['watermark_step', input.watermark_step],
|
|
259
|
+
['saturation_step', input.saturation_step],
|
|
260
|
+
] as const) {
|
|
261
|
+
if (v != null && (!isInt(v) || v < 0)) errors.push(`${key} must be a non-negative integer count`);
|
|
262
|
+
}
|
|
263
|
+
if (input.trigger_arm_earlier != null && !TRIGGER_ARMS.includes(input.trigger_arm_earlier)) {
|
|
264
|
+
errors.push(`trigger_arm_earlier must be one of ${TRIGGER_ARMS.join(' | ')} (or omitted / null when neither arm fired)`);
|
|
265
|
+
}
|
|
266
|
+
|
|
236
267
|
if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
|
|
237
268
|
if (!input.id) errors.push('id (ULID or content hash) is required');
|
|
238
269
|
|
|
@@ -267,6 +298,12 @@ export function buildOrchestrationLine(input: RecordInput): BuiltLine {
|
|
|
267
298
|
origin: input.origin ?? null,
|
|
268
299
|
rules_carried: input.rules_carried ?? null,
|
|
269
300
|
rules_used: input.rules_used ?? null,
|
|
301
|
+
// capsule shadow-measurement — readers ignore unknowns per audit-log-v1
|
|
302
|
+
capsule_emitted: input.capsule_emitted ?? null,
|
|
303
|
+
capsule_entries: input.capsule_entries ?? null,
|
|
304
|
+
watermark_step: input.watermark_step ?? null,
|
|
305
|
+
saturation_step: input.saturation_step ?? null,
|
|
306
|
+
trigger_arm_earlier: input.trigger_arm_earlier ?? null,
|
|
270
307
|
};
|
|
271
308
|
|
|
272
309
|
const line: Record<string, unknown> = {
|
|
@@ -212,7 +212,7 @@ export function aggregateOrchestrationSavings(
|
|
|
212
212
|
|
|
213
213
|
// Honest caveats — always attached so a consumer never over-reads the number.
|
|
214
214
|
if (report.dispatches === 0) {
|
|
215
|
-
report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as
|
|
215
|
+
report.notes.push('No orchestration telemetry yet (0 dispatches). Data accrues as delegable slices dispatch (always-on orchestration; recorded via orchestration_record).');
|
|
216
216
|
} else {
|
|
217
217
|
report.notes.push('Token counts are MEASURED but blind to the downshift rate win (same tokens on a cheaper tier ≈ same count). The MODELED cost-% below captures that axis.');
|
|
218
218
|
if (report.by_provenance.estimated.dispatches > 0) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a "prompt" actually is — shared by the language hook and the conformance
|
|
3
|
+
* scanner so the two cannot classify the same entry differently.
|
|
4
|
+
*
|
|
5
|
+
* They did. Round 6 measured it: `isSyntheticPrompt` lived in
|
|
6
|
+
* `language_mirror_hook` and was never referenced from `conformance_scan`, whose
|
|
7
|
+
* only general net was `length > 2500 && english`. So the hook skipped
|
|
8
|
+
* harness-generated turns and the scanner counted them, and every era split
|
|
9
|
+
* argued about two different populations without knowing it.
|
|
10
|
+
*
|
|
11
|
+
* The asymmetry ran the other way too, and that half had no net on either side:
|
|
12
|
+
* a prompt whose bulk is pasted content in another language pinned to the paste.
|
|
13
|
+
* That fired on the very session that received the round-6 review — the pin read
|
|
14
|
+
* English because an English draft was pasted below German prose.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here is language-specific. A rule that names a language cannot answer
|
|
17
|
+
* a bidirectional question.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Is this "prompt" a harness-generated turn rather than something a human typed?
|
|
22
|
+
*
|
|
23
|
+
* MEASURED, IN THE LANGUAGE HOOK'S OWN AUDIT (round 5, 2026-08-07). The state
|
|
24
|
+
* file read mid-session:
|
|
25
|
+
*
|
|
26
|
+
* { "language": "en", "source": "prompt", "prompt_chars": 6627,
|
|
27
|
+
* "de_markers": 0, "en_markers": 63 }
|
|
28
|
+
*
|
|
29
|
+
* No human wrote those 6,627 characters. They were a background-task completion
|
|
30
|
+
* notification, injected as a user turn, which `user_prompt_submit` sees and the
|
|
31
|
+
* hook classified as the trigger — flipping a German session to `en` for every
|
|
32
|
+
* later turn, and recording `source: "prompt"`, a false provenance claim.
|
|
33
|
+
*
|
|
34
|
+
* The markers are structural, not linguistic: the harness stamps these turns
|
|
35
|
+
* with fixed envelope tags. Matching prose would misfire on a human quoting a
|
|
36
|
+
* notification; matching the tags cannot, because a human typing
|
|
37
|
+
* `<task-notification>` is quoting, and a quote does not open at character zero.
|
|
38
|
+
*
|
|
39
|
+
* Deliberately conservative: an unrecognised synthetic shape falls through and
|
|
40
|
+
* is treated as a chat message. Under-filtering keeps prior behaviour;
|
|
41
|
+
* over-filtering would silently stop pinning real prompts, which is worse.
|
|
42
|
+
* Measured across 1 540 user-role entries: 509 open at character zero with a
|
|
43
|
+
* marker, and in 0 of them does human text survive wrapper removal.
|
|
44
|
+
*/
|
|
45
|
+
export function isSyntheticPrompt(prompt: string): boolean {
|
|
46
|
+
const head = prompt.trimStart().slice(0, 400);
|
|
47
|
+
return (
|
|
48
|
+
head.startsWith("[SYSTEM NOTIFICATION - NOT USER INPUT]") ||
|
|
49
|
+
head.startsWith("<task-notification>") ||
|
|
50
|
+
head.startsWith("<system-reminder>") ||
|
|
51
|
+
head.startsWith("<local-command-caveat>") ||
|
|
52
|
+
/^\[SYSTEM NOTIFICATION\b/.test(head)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A line that starts pasted DOCUMENT content rather than typed instruction.
|
|
58
|
+
*
|
|
59
|
+
* Distinct from the output-shaped heads `instructionText` already removes (stack
|
|
60
|
+
* traces, diffs, `remote:` lines). Those look like machine output. This catches
|
|
61
|
+
* the other paste shape, which looks like prose: a roadmap, a review, a spec —
|
|
62
|
+
* pasted whole, under its own headings, below one typed sentence.
|
|
63
|
+
*
|
|
64
|
+
* The markers are structural for the same reason as above: a heading, a
|
|
65
|
+
* frontmatter fence, or a bare tag line is document furniture. A typed chat turn
|
|
66
|
+
* rarely carries one; a pasted document essentially always does.
|
|
67
|
+
*/
|
|
68
|
+
const DOCUMENT_HEAD = /^\s*(#{1,6}\s|-{3,}\s*$|={3,}\s*$|<[A-Za-z][\w-]*>\s*$)/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The part of a prompt a human plausibly typed: everything before the first
|
|
72
|
+
* pasted document begins.
|
|
73
|
+
*
|
|
74
|
+
* Callers pass text that has already had output-shaped pastes removed. Returning
|
|
75
|
+
* the LEAD rather than a filtered whole is deliberate — a paste can be
|
|
76
|
+
* interleaved, and reassembling the human fragments would invent a text nobody
|
|
77
|
+
* wrote. The lead is the one span whose authorship is not in question.
|
|
78
|
+
*/
|
|
79
|
+
export function humanAuthoredLead(text: string): string {
|
|
80
|
+
const kept: string[] = [];
|
|
81
|
+
for (const line of text.split("\n")) {
|
|
82
|
+
if (DOCUMENT_HEAD.test(line)) {
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
kept.push(line);
|
|
86
|
+
}
|
|
87
|
+
return kept.join("\n");
|
|
88
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `review_skipped` telemetry line builder (pure, no I/O).
|
|
3
|
+
*
|
|
4
|
+
* Purpose-built, MINIMAL extension of the audit-log-v1 envelope
|
|
5
|
+
* (`docs/contracts/audit-log-v1.md`) for the F4-lite end-review-nudge concern
|
|
6
|
+
* (road-to-orchestrator-discipline-carriers Phase 5). It reuses the same
|
|
7
|
+
* envelope shape and the same file location
|
|
8
|
+
* (`agents/runtime/state/audit/<YYYY-MM>.jsonl`) as
|
|
9
|
+
* `_lib/orchestration_record.ts`'s `orchestration` object, but does not
|
|
10
|
+
* import that module: `buildOrchestrationLine` requires `spawn_count`, which
|
|
11
|
+
* has no meaning for a review-skip event, and its enum-constant tables are
|
|
12
|
+
* private to that file. Adding a second additive top-level object
|
|
13
|
+
* (`review_skipped`) is exactly the pattern the contract already uses for
|
|
14
|
+
* `orchestration` — "Unknown trailing fields are forward-compat extensions;
|
|
15
|
+
* readers MUST NOT raise on them."
|
|
16
|
+
*
|
|
17
|
+
* PRIVACY BY CONSTRUCTION: the `review_skipped` object carries `diff_lines`
|
|
18
|
+
* (a non-negative integer count) and `mutation_measure` (a closed 2-value
|
|
19
|
+
* enum — never free text). There is no field capable of holding a file
|
|
20
|
+
* path, a prompt, or any free-form content — never add one (mirrors
|
|
21
|
+
* `domain-safety-pii` § Surface 2 / the `artifact-engagement` event shape).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Whether `diff_lines` is an exact sum or a guaranteed-over-threshold
|
|
26
|
+
* approximation (`end_review_nudge_hook.ts`'s `UNTRACKED_FILE_CAP` path —
|
|
27
|
+
* past that many untracked non-doc files, the hook refuses to spawn one
|
|
28
|
+
* `git diff --no-index` subprocess per file and reports a value merely
|
|
29
|
+
* guaranteed to exceed the fire threshold, not an exact count). Mixing
|
|
30
|
+
* exact and approximated counts in this telemetry stream, unlabeled, would
|
|
31
|
+
* corrupt any future blocking-threshold calibration that reads it — hence
|
|
32
|
+
* this field, rather than folding the approximation silently into
|
|
33
|
+
* `diff_lines`.
|
|
34
|
+
*/
|
|
35
|
+
export type MutationMeasure = 'exact' | 'capped_approximation';
|
|
36
|
+
|
|
37
|
+
export interface ReviewSkippedInput {
|
|
38
|
+
/** Non-doc tracked-file mutation size that triggered the nudge (count only). */
|
|
39
|
+
diff_lines: number;
|
|
40
|
+
/** Whether `diff_lines` is exact or a capped approximation — see above. */
|
|
41
|
+
mutation_measure: MutationMeasure;
|
|
42
|
+
/** ISO-8601 UTC timestamp; caller supplies (keeps this fn pure/deterministic). */
|
|
43
|
+
ts: string;
|
|
44
|
+
/** Stable id (ULID, UUID, or content hash); caller supplies. */
|
|
45
|
+
id: string;
|
|
46
|
+
work_id?: string | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface BuiltReviewSkippedLine {
|
|
50
|
+
line: Record<string, unknown> | null;
|
|
51
|
+
errors: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isNonNegInt(n: unknown): n is number {
|
|
55
|
+
return typeof n === 'number' && Number.isFinite(n) && Number.isInteger(n) && n >= 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* High-risk threshold for the line's own `risk_class` (audit-log-v1 field,
|
|
60
|
+
* not the hook's fire threshold): a skipped review over a large diff carries
|
|
61
|
+
* more residual risk than one just past the fire threshold.
|
|
62
|
+
*/
|
|
63
|
+
const HIGH_RISK_DIFF_LINES = 200;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Build + validate ONE audit-log-v1 line carrying a `review_skipped` object.
|
|
67
|
+
* Returns `{line, errors}`; when `errors` is non-empty, `line` is null and
|
|
68
|
+
* the caller must NOT write.
|
|
69
|
+
*/
|
|
70
|
+
export function buildReviewSkippedLine(input: ReviewSkippedInput): BuiltReviewSkippedLine {
|
|
71
|
+
const errors: string[] = [];
|
|
72
|
+
if (!isNonNegInt(input.diff_lines)) {
|
|
73
|
+
errors.push('diff_lines must be a non-negative integer');
|
|
74
|
+
}
|
|
75
|
+
if (input.mutation_measure !== 'exact' && input.mutation_measure !== 'capped_approximation') {
|
|
76
|
+
errors.push('mutation_measure must be "exact" or "capped_approximation"');
|
|
77
|
+
}
|
|
78
|
+
if (!input.ts) errors.push('ts (ISO-8601 UTC) is required');
|
|
79
|
+
if (!input.id) errors.push('id (ULID, UUID, or content hash) is required');
|
|
80
|
+
|
|
81
|
+
if (errors.length) return { line: null, errors };
|
|
82
|
+
|
|
83
|
+
const line: Record<string, unknown> = {
|
|
84
|
+
schema_version: 1,
|
|
85
|
+
id: input.id,
|
|
86
|
+
ts: input.ts,
|
|
87
|
+
work_id: input.work_id ?? `review-skipped-${input.ts}`,
|
|
88
|
+
phase: 'report',
|
|
89
|
+
outcome: 'skipped',
|
|
90
|
+
confidence_band: 'medium',
|
|
91
|
+
risk_class: input.diff_lines >= HIGH_RISK_DIFF_LINES ? 'high' : 'medium',
|
|
92
|
+
memory: { asks: 0, hits: 0 },
|
|
93
|
+
verify: { claims: 0, first_try_passes: 0 },
|
|
94
|
+
rules_applied: ['delegation-policy'],
|
|
95
|
+
persona: null,
|
|
96
|
+
input_kind: 'prompt',
|
|
97
|
+
type: 'note',
|
|
98
|
+
review_skipped: { diff_lines: input.diff_lines, mutation_measure: input.mutation_measure },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return { line, errors: [] };
|
|
102
|
+
}
|