@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,261 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session_eol_report — session end-of-life baseline over the local
|
|
4
|
+
* Claude Code transcript store (road-to-token-economy-recycling Phases
|
|
5
|
+
* 1.1–1.3). Read-only; prints aggregate numbers, never content.
|
|
6
|
+
*
|
|
7
|
+
* Per session file (main chain only): final context size in parsed tokens
|
|
8
|
+
* (`cc_transcript.billableInputTokens` of the last assistant record), turn
|
|
9
|
+
* count, and every observed compaction event (`_lib/session_eol.ts`
|
|
10
|
+
* detector, pinned to the observed `compact_boundary` marker). The
|
|
11
|
+
* aggregate is the baseline the recycle threshold cites
|
|
12
|
+
* (`src/config/recycle-threshold-budget.json`), and the reproduce command
|
|
13
|
+
* for `agents/evidence/analysis/token-economy-recycling-phase1.md`:
|
|
14
|
+
*
|
|
15
|
+
* ./scripts-run src/scripts/session_eol_report # text
|
|
16
|
+
* ./scripts-run src/scripts/session_eol_report -- --json # machine
|
|
17
|
+
*
|
|
18
|
+
* Fallback honesty (Phase 1.2): sessions whose transcript yields NO
|
|
19
|
+
* parseable assistant usage are counted as `sessions_without_usage`, and
|
|
20
|
+
* the bytes↔tokens Pearson r over the READABLE set is printed so the
|
|
21
|
+
* byte-proxy's (un)fitness is a published number, not a vibe.
|
|
22
|
+
*
|
|
23
|
+
* Marker-drift alarm (blocker `compaction-marker-shape`): the scanner
|
|
24
|
+
* counts BOTH observed markers independently; a store-wide divergence
|
|
25
|
+
* between `compact_boundary` events and `isCompactSummary` records is
|
|
26
|
+
* reported as drift instead of silently zeroing the metric.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import * as fs from 'node:fs';
|
|
30
|
+
import process from 'node:process';
|
|
31
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
|
+
import * as path from 'node:path';
|
|
33
|
+
|
|
34
|
+
import { DEFAULT_PROJECTS_ROOT, listTranscriptFiles } from './_lib/cc_transcript.js';
|
|
35
|
+
import { emptyCounters, scanEolSlice, type EolCounters } from './_lib/session_eol.js';
|
|
36
|
+
|
|
37
|
+
export interface SessionSummary {
|
|
38
|
+
file: string;
|
|
39
|
+
bytes: number;
|
|
40
|
+
counters: EolCounters;
|
|
41
|
+
last_ts: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface EolReport {
|
|
45
|
+
schema_version: 1;
|
|
46
|
+
generated_at: string;
|
|
47
|
+
root: string;
|
|
48
|
+
files_scanned: number;
|
|
49
|
+
sessions: number;
|
|
50
|
+
sessions_with_usage: number;
|
|
51
|
+
sessions_without_usage: number;
|
|
52
|
+
final_context_tokens: Percentiles | null;
|
|
53
|
+
turns: Percentiles | null;
|
|
54
|
+
compaction: {
|
|
55
|
+
sessions_with_compaction: number;
|
|
56
|
+
events_total: number;
|
|
57
|
+
events_by_trigger: Record<string, number>;
|
|
58
|
+
pre_tokens: Percentiles | null;
|
|
59
|
+
post_tokens: Percentiles | null;
|
|
60
|
+
/** `isCompactSummary` records store-wide — drift alarm vs `events_total`. */
|
|
61
|
+
compact_summary_records: number;
|
|
62
|
+
marker_drift: boolean;
|
|
63
|
+
};
|
|
64
|
+
/** Pearson r of file bytes vs final context tokens over the readable set. */
|
|
65
|
+
bytes_tokens_pearson_r: number | null;
|
|
66
|
+
threshold_crossings: Record<string, number>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Percentiles {
|
|
70
|
+
n: number;
|
|
71
|
+
min: number;
|
|
72
|
+
median: number;
|
|
73
|
+
p90: number;
|
|
74
|
+
p95: number;
|
|
75
|
+
max: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function percentile(sorted: number[], p: number): number {
|
|
79
|
+
if (sorted.length === 0) return NaN;
|
|
80
|
+
const k = (sorted.length - 1) * p;
|
|
81
|
+
const f = Math.floor(k);
|
|
82
|
+
const c = Math.min(f + 1, sorted.length - 1);
|
|
83
|
+
return (sorted[f] as number) + ((sorted[c] as number) - (sorted[f] as number)) * (k - f);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function percentiles(values: number[]): Percentiles | null {
|
|
87
|
+
if (values.length === 0) return null;
|
|
88
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
89
|
+
return {
|
|
90
|
+
n: sorted.length,
|
|
91
|
+
min: sorted[0] as number,
|
|
92
|
+
median: percentile(sorted, 0.5),
|
|
93
|
+
p90: percentile(sorted, 0.9),
|
|
94
|
+
p95: percentile(sorted, 0.95),
|
|
95
|
+
max: sorted[sorted.length - 1] as number,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function pearson(pairs: Array<[number, number]>): number | null {
|
|
100
|
+
if (pairs.length < 3) return null;
|
|
101
|
+
const n = pairs.length;
|
|
102
|
+
const mx = pairs.reduce((s, [x]) => s + x, 0) / n;
|
|
103
|
+
const my = pairs.reduce((s, [, y]) => s + y, 0) / n;
|
|
104
|
+
let cov = 0;
|
|
105
|
+
let sx = 0;
|
|
106
|
+
let sy = 0;
|
|
107
|
+
for (const [x, y] of pairs) {
|
|
108
|
+
cov += (x - mx) * (y - my);
|
|
109
|
+
sx += (x - mx) ** 2;
|
|
110
|
+
sy += (y - my) ** 2;
|
|
111
|
+
}
|
|
112
|
+
if (sx === 0 || sy === 0) return null;
|
|
113
|
+
return cov / (Math.sqrt(sx) * Math.sqrt(sy));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Scan one transcript file into a session summary. Unreadable file → null. */
|
|
117
|
+
export function scanSessionFile(file: string): SessionSummary | null {
|
|
118
|
+
let text: string;
|
|
119
|
+
let bytes: number;
|
|
120
|
+
try {
|
|
121
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
122
|
+
bytes = fs.statSync(file).size;
|
|
123
|
+
} catch {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
const counters = scanEolSlice(text, emptyCounters());
|
|
127
|
+
let last_ts: string | null = null;
|
|
128
|
+
// cheap: reuse the final assistant stamp; fall back to null.
|
|
129
|
+
last_ts = counters.final_context_at;
|
|
130
|
+
return { file, bytes, counters, last_ts };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function buildReport(root: string, now: Date = new Date()): EolReport {
|
|
134
|
+
const files = listTranscriptFiles({ root });
|
|
135
|
+
const sessions: SessionSummary[] = [];
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const summary = scanSessionFile(file);
|
|
138
|
+
if (!summary) continue;
|
|
139
|
+
const c = summary.counters;
|
|
140
|
+
if (c.assistant_records === 0 && c.turns === 0 && c.compactions.length === 0) continue;
|
|
141
|
+
sessions.push(summary);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const withUsage = sessions.filter((s) => s.counters.final_context_tokens !== null);
|
|
145
|
+
const finals = withUsage.map((s) => s.counters.final_context_tokens as number);
|
|
146
|
+
const turns = withUsage.map((s) => s.counters.turns);
|
|
147
|
+
|
|
148
|
+
const allEvents = sessions.flatMap((s) => s.counters.compactions);
|
|
149
|
+
const byTrigger: Record<string, number> = {};
|
|
150
|
+
for (const e of allEvents) byTrigger[e.trigger] = (byTrigger[e.trigger] ?? 0) + 1;
|
|
151
|
+
const pre = allEvents.map((e) => e.pre_tokens).filter((v): v is number => v !== null);
|
|
152
|
+
const post = allEvents.map((e) => e.post_tokens).filter((v): v is number => v !== null);
|
|
153
|
+
const summaryRecords = sessions.reduce((s, x) => s + x.counters.compact_summaries, 0);
|
|
154
|
+
|
|
155
|
+
const crossings: Record<string, number> = {};
|
|
156
|
+
for (const thr of [200_000, 400_000, 600_000, 800_000]) {
|
|
157
|
+
crossings[`>=${thr / 1000}k`] = finals.filter((c) => c >= thr).length;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
schema_version: 1,
|
|
162
|
+
generated_at: now.toISOString(),
|
|
163
|
+
root,
|
|
164
|
+
files_scanned: files.length,
|
|
165
|
+
sessions: sessions.length,
|
|
166
|
+
sessions_with_usage: withUsage.length,
|
|
167
|
+
sessions_without_usage: sessions.length - withUsage.length,
|
|
168
|
+
final_context_tokens: percentiles(finals),
|
|
169
|
+
turns: percentiles(turns),
|
|
170
|
+
compaction: {
|
|
171
|
+
sessions_with_compaction: sessions.filter((s) => s.counters.compactions.length > 0).length,
|
|
172
|
+
events_total: allEvents.length,
|
|
173
|
+
events_by_trigger: byTrigger,
|
|
174
|
+
pre_tokens: percentiles(pre),
|
|
175
|
+
post_tokens: percentiles(post),
|
|
176
|
+
compact_summary_records: summaryRecords,
|
|
177
|
+
marker_drift: summaryRecords !== allEvents.length,
|
|
178
|
+
},
|
|
179
|
+
bytes_tokens_pearson_r: pearson(
|
|
180
|
+
withUsage
|
|
181
|
+
.filter((s) => (s.counters.final_context_tokens as number) > 1_000)
|
|
182
|
+
.map((s) => [s.bytes, s.counters.final_context_tokens as number]),
|
|
183
|
+
),
|
|
184
|
+
threshold_crossings: crossings,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function fmt(n: number): string {
|
|
189
|
+
return Math.round(n).toLocaleString('en-US');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function renderText(r: EolReport): string {
|
|
193
|
+
const lines: string[] = [];
|
|
194
|
+
lines.push(`session end-of-life baseline — ${r.root}`);
|
|
195
|
+
lines.push(`files scanned: ${r.files_scanned} · sessions: ${r.sessions}`);
|
|
196
|
+
lines.push(
|
|
197
|
+
` with parseable final usage: ${r.sessions_with_usage} · without (fallback set, 1.2): ${r.sessions_without_usage}`,
|
|
198
|
+
);
|
|
199
|
+
if (r.final_context_tokens) {
|
|
200
|
+
const f = r.final_context_tokens;
|
|
201
|
+
lines.push(
|
|
202
|
+
`final context tokens: median=${fmt(f.median)} p90=${fmt(f.p90)} p95=${fmt(f.p95)} max=${fmt(f.max)}`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (r.turns) {
|
|
206
|
+
lines.push(`turns per session: median=${fmt(r.turns.median)} p90=${fmt(r.turns.p90)} max=${fmt(r.turns.max)}`);
|
|
207
|
+
}
|
|
208
|
+
const c = r.compaction;
|
|
209
|
+
lines.push(
|
|
210
|
+
`compaction: ${c.sessions_with_compaction} sessions, ${c.events_total} events (${Object.entries(c.events_by_trigger)
|
|
211
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
212
|
+
.join(', ') || 'none'})`,
|
|
213
|
+
);
|
|
214
|
+
if (c.pre_tokens) {
|
|
215
|
+
lines.push(
|
|
216
|
+
` trigger pre-tokens: min=${fmt(c.pre_tokens.min)} median=${fmt(c.pre_tokens.median)} max=${fmt(c.pre_tokens.max)}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (c.post_tokens) {
|
|
220
|
+
lines.push(` post-tokens median=${fmt(c.post_tokens.median)}`);
|
|
221
|
+
}
|
|
222
|
+
lines.push(
|
|
223
|
+
` marker drift (${c.events_total} boundary vs ${c.compact_summary_records} summary records): ${c.marker_drift ? 'YES — detector shapes diverge, re-pin the fixture' : 'no'}`,
|
|
224
|
+
);
|
|
225
|
+
lines.push(
|
|
226
|
+
`bytes↔tokens Pearson r (readable set): ${r.bytes_tokens_pearson_r === null ? 'n/a' : r.bytes_tokens_pearson_r.toFixed(3)}`,
|
|
227
|
+
);
|
|
228
|
+
for (const [k, v] of Object.entries(r.threshold_crossings)) {
|
|
229
|
+
lines.push(`sessions ending ${k} tokens: ${v}`);
|
|
230
|
+
}
|
|
231
|
+
return lines.join('\n');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function main(argv: string[] = process.argv.slice(2)): number {
|
|
235
|
+
const json = argv.includes('--json');
|
|
236
|
+
const rootFlag = argv.indexOf('--root');
|
|
237
|
+
const root =
|
|
238
|
+
rootFlag !== -1 && argv[rootFlag + 1] ? path.resolve(argv[rootFlag + 1] as string) : DEFAULT_PROJECTS_ROOT;
|
|
239
|
+
const report = buildReport(root);
|
|
240
|
+
process.stdout.write(json ? `${JSON.stringify(report, null, 2)}\n` : `${renderText(report)}\n`);
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle.
|
|
245
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
246
|
+
function _isCliEntry(): boolean {
|
|
247
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) return false;
|
|
248
|
+
if (process.argv[1] === undefined) return false;
|
|
249
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
250
|
+
try {
|
|
251
|
+
return (
|
|
252
|
+
fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]))
|
|
253
|
+
);
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (_isCliEntry()) {
|
|
260
|
+
process.exit(main());
|
|
261
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Team-event payload shape spike (Phase 5.1, road-to-always-on-orchestration).
|
|
4
|
+
*
|
|
5
|
+
* `TaskCreated` / `TaskCompleted` / `TeammateIdle` are documented Claude Code
|
|
6
|
+
* hook events (agent teams, behind `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS`),
|
|
7
|
+
* but this repo has never observed a real payload for any of them — the
|
|
8
|
+
* shapes are documented, not verified. This script is manually startable
|
|
9
|
+
* ONLY: it is never wired into `hook_manifest.yaml` / `hooks.json`. A
|
|
10
|
+
* maintainer points a hook command at it (or pipes a captured payload) to
|
|
11
|
+
* record a SHAPE — field names + JS/JSON types — and never a value.
|
|
12
|
+
* Privacy by construction: the recursive walk below inspects `typeof` /
|
|
13
|
+
* `Array.isArray`, nothing else, so there is no code path that could copy a
|
|
14
|
+
* value into the written report.
|
|
15
|
+
*
|
|
16
|
+
* SCOPE OF THE PRIVACY CLAIM (stated honestly, per independent review): the
|
|
17
|
+
* claim above holds for a payload whose OWN keys are static field names —
|
|
18
|
+
* true of the three documented envelopes this script targets
|
|
19
|
+
* (`TaskCreated` / `TaskCompleted` / `TeammateIdle`), where a key is always
|
|
20
|
+
* a schema name like `task_id` or `teammate_name`. It does NOT hold for a
|
|
21
|
+
* payload with dynamically-named keys — a map keyed by a file path, a
|
|
22
|
+
* person's name, or any other runtime value — because `extractShape` walks
|
|
23
|
+
* `Object.keys()` and records each key verbatim as a "field name". A
|
|
24
|
+
* dynamic key is not a field name; it is a value wearing a field name's
|
|
25
|
+
* clothes, and this script would write it into the tracked report exactly
|
|
26
|
+
* as it would refuse to write a value. Point this script only at the three
|
|
27
|
+
* documented shapes (or another envelope you have separately confirmed has
|
|
28
|
+
* static keys) — never at an undocumented payload that might carry a
|
|
29
|
+
* dynamically-keyed map.
|
|
30
|
+
*
|
|
31
|
+
* Usage (only meaningful with the experimental flag set):
|
|
32
|
+
*
|
|
33
|
+
* echo '<payload-json>' | \
|
|
34
|
+
* CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1 npx tsx src/scripts/team_events_spike.ts [EventName]
|
|
35
|
+
*
|
|
36
|
+
* `EventName` is optional; if omitted, the payload's own `hook_event_name` /
|
|
37
|
+
* `event` / `eventName` / `event_name` / `type` field is read and matched
|
|
38
|
+
* against the three known `TEAM_EVENT_NAMES` — an unrecognised value falls
|
|
39
|
+
* back to `unlabeled-event` rather than copying the raw (payload-supplied)
|
|
40
|
+
* string into the report, which would itself be writing a value under the
|
|
41
|
+
* cover of a "name".
|
|
42
|
+
*
|
|
43
|
+
* Without the flag: clean one-line skip, exit 0, stdin is never touched.
|
|
44
|
+
*
|
|
45
|
+
* Output: appends one dated section per capture to
|
|
46
|
+
* `agents/evidence/analysis/team-event-shapes.md`.
|
|
47
|
+
*/
|
|
48
|
+
import * as fs from 'node:fs';
|
|
49
|
+
import * as path from 'node:path';
|
|
50
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
51
|
+
|
|
52
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
53
|
+
const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
54
|
+
const OUT_FILE = path.join(ROOT, 'agents', 'evidence', 'analysis', 'team-event-shapes.md');
|
|
55
|
+
|
|
56
|
+
export const FLAG = 'CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS';
|
|
57
|
+
export const TEAM_EVENT_NAMES = ['TaskCreated', 'TaskCompleted', 'TeammateIdle'] as const;
|
|
58
|
+
|
|
59
|
+
export interface ShapeEntry {
|
|
60
|
+
path: string;
|
|
61
|
+
type: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** JS/JSON type of a value. Reads only `typeof` / `Array.isArray` — never the value. */
|
|
65
|
+
function _typeOf(value: unknown): string {
|
|
66
|
+
if (value === null) return 'null';
|
|
67
|
+
if (Array.isArray(value)) return 'array';
|
|
68
|
+
return typeof value; // 'string' | 'number' | 'boolean' | 'object' | 'undefined'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Walk a parsed JSON payload and record field-name + type only.
|
|
73
|
+
*
|
|
74
|
+
* This never returns, prints, or embeds a value — recursion is driven
|
|
75
|
+
* entirely by structural type and object keys.
|
|
76
|
+
*/
|
|
77
|
+
export function extractShape(value: unknown, prefix = ''): ShapeEntry[] {
|
|
78
|
+
const entries: ShapeEntry[] = [];
|
|
79
|
+
const t = _typeOf(value);
|
|
80
|
+
if (prefix !== '') {
|
|
81
|
+
entries.push({ path: prefix, type: t });
|
|
82
|
+
}
|
|
83
|
+
if (t === 'array') {
|
|
84
|
+
// An event payload's arrays are homogeneous lists, not fixed tuples —
|
|
85
|
+
// merge every element's shape under one `[]` path (deduped below).
|
|
86
|
+
for (const item of value as unknown[]) {
|
|
87
|
+
entries.push(...extractShape(item, `${prefix}[]`));
|
|
88
|
+
}
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
if (t === 'object') {
|
|
92
|
+
const obj = value as Record<string, unknown>;
|
|
93
|
+
for (const key of Object.keys(obj).sort()) {
|
|
94
|
+
entries.push(...extractShape(obj[key], prefix === '' ? key : `${prefix}.${key}`));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return entries;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** De-duplicate by path+type — an array's elements repeat the same shape. */
|
|
101
|
+
function _dedupe(entries: ShapeEntry[]): ShapeEntry[] {
|
|
102
|
+
const seen = new Set<string>();
|
|
103
|
+
const out: ShapeEntry[] = [];
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
const key = `${e.path}\u0000${e.type}`;
|
|
106
|
+
if (seen.has(key)) continue;
|
|
107
|
+
seen.add(key);
|
|
108
|
+
out.push(e);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Best-effort event-name inference from the payload's own envelope.
|
|
115
|
+
*
|
|
116
|
+
* m4 fix (independent-review finding): the envelope field this reads
|
|
117
|
+
* (`hook_event_name` / `event` / `type` / …) carries a payload-SUPPLIED
|
|
118
|
+
* string — copying it verbatim into the recorded report is exactly the
|
|
119
|
+
* "copy a value into the report" this script otherwise refuses to do
|
|
120
|
+
* (see the module header's privacy claim). Matched against the known
|
|
121
|
+
* `TEAM_EVENT_NAMES` allowlist instead: a recognised name is safe to
|
|
122
|
+
* record because it is drawn from three publicly documented, stable
|
|
123
|
+
* literals, never from the untrusted payload string itself; anything
|
|
124
|
+
* else returns `null` and the caller falls back to the fixed literal
|
|
125
|
+
* `'unlabeled-event'`.
|
|
126
|
+
*/
|
|
127
|
+
export function inferEventName(payload: unknown): string | null {
|
|
128
|
+
if (payload === null || typeof payload !== 'object' || Array.isArray(payload)) return null;
|
|
129
|
+
const obj = payload as Record<string, unknown>;
|
|
130
|
+
const known = new Set<string>(TEAM_EVENT_NAMES);
|
|
131
|
+
for (const key of ['hook_event_name', 'event', 'eventName', 'event_name', 'type']) {
|
|
132
|
+
const v = obj[key];
|
|
133
|
+
if (typeof v === 'string' && known.has(v)) return v;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Render one capture as a Markdown section — field names + types only. */
|
|
139
|
+
export function renderShapeReport(eventName: string, entries: ShapeEntry[], observedAt: string): string {
|
|
140
|
+
const deduped = _dedupe(entries);
|
|
141
|
+
const rows = deduped.map((e) => `| \`${e.path}\` | ${e.type} |`).join('\n');
|
|
142
|
+
const table = rows === '' ? '_(empty payload — no fields observed)_' : `| Field | Type |\n|---|---|\n${rows}`;
|
|
143
|
+
return [`## ${eventName} — observed ${observedAt}`, '', table, ''].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function _printGuide(): void {
|
|
147
|
+
process.stdout.write(
|
|
148
|
+
[
|
|
149
|
+
'team-event shape spike (manual, not hook-bound)',
|
|
150
|
+
'',
|
|
151
|
+
`${FLAG} is set — recording.`,
|
|
152
|
+
'',
|
|
153
|
+
`Observed events: ${TEAM_EVENT_NAMES.join(', ')}`,
|
|
154
|
+
'',
|
|
155
|
+
'Pipe one captured JSON payload on stdin — values are never written,',
|
|
156
|
+
'only field names + types:',
|
|
157
|
+
'',
|
|
158
|
+
` echo '<payload-json>' | ${FLAG}=1 npx tsx src/scripts/team_events_spike.ts [EventName]`,
|
|
159
|
+
'',
|
|
160
|
+
`Shapes append to ${path.relative(ROOT, OUT_FILE)}.`,
|
|
161
|
+
'',
|
|
162
|
+
].join('\n'),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function _readStdin(): Promise<string> {
|
|
167
|
+
const chunks: Buffer[] = [];
|
|
168
|
+
// `for await` streams stdin — a synchronous `readFileSync(0)` EAGAINs
|
|
169
|
+
// past the pipe buffer on a large payload; this has no such ceiling.
|
|
170
|
+
for await (const chunk of process.stdin) {
|
|
171
|
+
chunks.push(chunk as Buffer);
|
|
172
|
+
}
|
|
173
|
+
return Buffer.concat(chunks).toString('utf-8');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface RunResult {
|
|
177
|
+
skipped: boolean;
|
|
178
|
+
reason?: string;
|
|
179
|
+
eventName?: string;
|
|
180
|
+
entryCount?: number;
|
|
181
|
+
outFile?: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RunOptions {
|
|
185
|
+
argv?: string[];
|
|
186
|
+
env?: NodeJS.ProcessEnv;
|
|
187
|
+
readStdin?: () => Promise<string>;
|
|
188
|
+
writeReport?: (section: string) => void;
|
|
189
|
+
now?: () => string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Orchestrates the spike. Every side effect is injectable so tests never
|
|
194
|
+
* touch the real environment, stdin, or the tracked report file.
|
|
195
|
+
*/
|
|
196
|
+
export async function run(opts?: RunOptions): Promise<RunResult> {
|
|
197
|
+
const env = opts?.env ?? process.env;
|
|
198
|
+
const argv = opts?.argv ?? process.argv.slice(2);
|
|
199
|
+
const readStdin = opts?.readStdin ?? _readStdin;
|
|
200
|
+
const now = opts?.now ?? (() => new Date().toISOString());
|
|
201
|
+
const writeReport =
|
|
202
|
+
opts?.writeReport ??
|
|
203
|
+
((section: string) => {
|
|
204
|
+
fs.mkdirSync(path.dirname(OUT_FILE), { recursive: true });
|
|
205
|
+
fs.appendFileSync(OUT_FILE, section, 'utf-8');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (!env[FLAG]) {
|
|
209
|
+
const reason = `${FLAG} is not set — teams are experimental and unavailable; skipping.`;
|
|
210
|
+
process.stdout.write(`${reason}\n`);
|
|
211
|
+
return { skipped: true, reason };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
_printGuide();
|
|
215
|
+
const raw = (await readStdin()).trim();
|
|
216
|
+
if (raw === '') {
|
|
217
|
+
const reason = 'no payload received on stdin — nothing recorded.';
|
|
218
|
+
process.stdout.write(`${reason}\n`);
|
|
219
|
+
return { skipped: true, reason };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let payload: unknown;
|
|
223
|
+
try {
|
|
224
|
+
payload = JSON.parse(raw);
|
|
225
|
+
} catch (e) {
|
|
226
|
+
const reason = `invalid JSON on stdin: ${(e as Error).message}`;
|
|
227
|
+
process.stderr.write(`team_events_spike: ${reason}\n`);
|
|
228
|
+
return { skipped: true, reason };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const eventName = argv[0] ?? inferEventName(payload) ?? 'unlabeled-event';
|
|
232
|
+
const entries = extractShape(payload);
|
|
233
|
+
const section = renderShapeReport(eventName, entries, now());
|
|
234
|
+
writeReport(`\n${section}`);
|
|
235
|
+
const count = _dedupe(entries).length;
|
|
236
|
+
process.stdout.write(`recorded ${count} field shape(s) for ${eventName} -> ${path.relative(ROOT, OUT_FILE)}\n`);
|
|
237
|
+
return { skipped: false, eventName, entryCount: count, outFile: OUT_FILE };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _isCliEntry(): boolean {
|
|
241
|
+
if (process.argv[1] === undefined) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
245
|
+
if (import.meta.url === argvUrl) {
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection)
|
|
249
|
+
// makes the raw URLs differ; compare realpaths so the entry guard still
|
|
250
|
+
// fires when run through a symlink.
|
|
251
|
+
try {
|
|
252
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
253
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
254
|
+
return here === argv;
|
|
255
|
+
} catch {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
261
|
+
run()
|
|
262
|
+
.then(() => {
|
|
263
|
+
// Observational spike, never a gate — always exit 0 on a
|
|
264
|
+
// completed run (skip or record alike).
|
|
265
|
+
process.exit(0);
|
|
266
|
+
})
|
|
267
|
+
.catch((e: unknown) => {
|
|
268
|
+
process.stderr.write(`team_events_spike: ${(e as Error).message}\n`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
@@ -3,14 +3,36 @@
|
|
|
3
3
|
* Team Review-Gate governance — `stop` concern (road-to-team-mode Phase 4).
|
|
4
4
|
*
|
|
5
5
|
* Minimal call-site for `src/scripts/ai_team/review_gate.ts`. On Stop it
|
|
6
|
-
* reads the dispatcher envelope, and — ONLY when `ai_team.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* reads the dispatcher envelope, and — ONLY when `ai_team.review_gate.managed`
|
|
7
|
+
* is true — locates the newest stop-gate review job the codex plugin
|
|
8
|
+
* persisted for this session, parses its first-line ALLOW/BLOCK contract,
|
|
9
|
+
* applies the consecutive-BLOCK counter, and prints the circuit-breaker
|
|
10
|
+
* notice (exactly once per trip) to stdout for the dispatch layer to
|
|
11
|
+
* render. With `managed: false` (default) the hook exits 0 without
|
|
12
|
+
* touching anything — byte-identical pre-Phase-4 Stop behavior.
|
|
13
|
+
*
|
|
14
|
+
* `ai_team.enabled` was DELETED (road-to-always-on-orchestration Phase 1,
|
|
15
|
+
* Step 1.3) — `managed` was previously double-gated behind it, but the two
|
|
16
|
+
* keys govern independent things: `managed` governs the codex PLUGIN's own
|
|
17
|
+
* Stop-hook loop (upstream's mechanism, which does not depend on our codex
|
|
18
|
+
* CLI availability), while `/team`'s own availability is a separate,
|
|
19
|
+
* CLI-resolved fact (`src/scripts/ai_team/availability.ts`) that the
|
|
20
|
+
* conversational `/team` wrappers check, not this passive Stop hook. So the
|
|
21
|
+
* two keys are decoupled: `managed` stands alone.
|
|
22
|
+
*
|
|
23
|
+
* DELIBERATELY NOT GATED ON `emergency.orchestration_halt` (independent
|
|
24
|
+
* review, road-to-always-on-orchestration): unlike `/team`'s own
|
|
25
|
+
* conversational surfaces (`assert_delegate_allowed` / `run_team_review` in
|
|
26
|
+
* `ai_team/team_dispatch.ts`, both of which check `isOrchestrationHalted`
|
|
27
|
+
* before doing anything), this hook never reads that switch. This is
|
|
28
|
+
* intentional, not an oversight — `managed` governs the UPSTREAM codex
|
|
29
|
+
* plugin's OWN Stop-hook loop, a third-party mechanism this hook merely
|
|
30
|
+
* observes and reports on (parsing an already-persisted job record); it
|
|
31
|
+
* never itself dispatches, spends, or calls a provider. The always-on-
|
|
32
|
+
* orchestration incident switch exists to pause OUR dispatch surfaces —
|
|
33
|
+
* there is no dispatch here to pause. Tripping the halt does not, and
|
|
34
|
+
* should not, silence this hook's circuit-breaker notice about the
|
|
35
|
+
* plugin's own loop.
|
|
14
36
|
*
|
|
15
37
|
* Composition point (wired): registered as the `team-review-gate` concern
|
|
16
38
|
* in `src/scripts/hook_manifest.yaml` on the claude `stop` slot, after
|
|
@@ -56,7 +78,7 @@ export function main(): number {
|
|
|
56
78
|
// owns reporting it. Defaults = managed off = strict no-op.
|
|
57
79
|
config = AI_TEAM_DEFAULTS;
|
|
58
80
|
}
|
|
59
|
-
if (!config.
|
|
81
|
+
if (!config.review_gate.managed) {
|
|
60
82
|
return 0;
|
|
61
83
|
}
|
|
62
84
|
|
|
@@ -331,18 +331,9 @@ export const settingsSchema = z.object({
|
|
|
331
331
|
}).default({}),
|
|
332
332
|
}).default({}),
|
|
333
333
|
subagents: z.object({
|
|
334
|
-
enabled: z.boolean().default(true).describe(
|
|
335
|
-
'Global master switch for the subagent layer. true (default) = available; false = fully disabled (the canonical kill-switch — no auto-dispatch, no routing, everything runs in-session).',
|
|
336
|
-
),
|
|
337
|
-
auto: z.enum(['off', 'ask', 'on']).default('on').describe(
|
|
338
|
-
'Automatic-dispatch mode. off = subagents only via explicit command. ask = classify the task and ask once before dispatching. on = auto-dispatch delegable tasks (surface the choice in one line). Shipped default is on on subagent-capable hosts (flipped from ask 2026-07-09, ADR-117), off elsewhere; no-op where the host has no subagent primitive.',
|
|
339
|
-
),
|
|
340
334
|
downshift: z.boolean().default(true).describe(
|
|
341
335
|
'Route delegable sub-tasks to the lowest-capable model tier (cost + speed via model downshift). false = every subagent runs on the session tier.',
|
|
342
336
|
),
|
|
343
|
-
budget_routing: z.enum(['ask', 'auto', 'off']).default('ask').describe(
|
|
344
|
-
'Budget-aware cheap-request delegation (docs/contracts/budget-routing.md): pick the cheapest classifier-adequate tier WITH available budget (cost.budgets.per_tier); cheap exhausted but strong funded → strong tier; all exhausted → session model + notice. Work is never blocked to save money; the session model is never switched (delegation with a model override). ask = confirm the first budget-motivated downshift per session (default). auto = apply silently with telemetry. off = today\'s behavior (single-flip rollback). Distinct from downshift: that is the quality/cost knob, this is the resource-availability gate.',
|
|
345
|
-
),
|
|
346
337
|
quota_arbitrage: z.boolean().default(true).describe(
|
|
347
338
|
'Prefer a separate quota-pool model for delegable sub-tasks where the host manifest reports one. Optional bonus only — identical behaviour (minus the quota win) where unsupported. Never load-bearing.',
|
|
348
339
|
),
|
|
@@ -353,15 +344,15 @@ export const settingsSchema = z.object({
|
|
|
353
344
|
}).default({}).describe(
|
|
354
345
|
'Per-tier model map for downshift routing. Each empty value uses the tier runtime default (no vendor model baked in).',
|
|
355
346
|
),
|
|
356
|
-
host_capabilities: z.object({}).passthrough().default({}).describe(
|
|
357
|
-
'Optional override of the host-capability manifest (subagent_spawn / parallel_spawn / status_polling / separate_quota_pool). Any field set wins; omitted fields fall back to the all-false safe default. Empty = the agent resolves the manifest from host knowledge.',
|
|
358
|
-
),
|
|
359
347
|
implementer_model: z.string().default('').describe(
|
|
360
348
|
'Override the model the orchestrator dispatches to subagents that write code (e.g. claude-sonnet-4, gpt-5). Empty (default) = inherit the session\'s primary model — cheapest and usually right.',
|
|
361
349
|
),
|
|
362
350
|
judge_model: z.string().default('').describe(
|
|
363
351
|
'Override the model used for review / judge subagents that critique implementer output. Empty (default) = one tier above the implementer model — picks up nuance the implementer missed.',
|
|
364
352
|
),
|
|
353
|
+
model_ceiling: z.string().default('').describe(
|
|
354
|
+
'Session-wide model CEILING for subagents (spend cap). Empty (default) = no ceiling. When set, suite-owned CLI spawn wrappers export CLAUDE_CODE_SUBAGENT_MODEL to the sessions they launch. Class C: a human sets it; the agent never writes or infers one.',
|
|
355
|
+
),
|
|
365
356
|
max_parallel: z.number().int().min(1).default(3).describe(
|
|
366
357
|
'Hard cap on subagents running in parallel during /do-in-parallel, /do-competitively, and /judge runs. Raise for faster fan-out, lower if you hit rate limits or want lower token spend.',
|
|
367
358
|
),
|
|
@@ -375,14 +366,11 @@ export const settingsSchema = z.object({
|
|
|
375
366
|
),
|
|
376
367
|
}),
|
|
377
368
|
ai_team: z.object({
|
|
378
|
-
enabled: z.boolean().default(false).describe(
|
|
379
|
-
'Master switch for the /team cross-model review family (docs/contracts/ai-team-config.md). false (default) = commands are never suggested and every invocation refuses with an enable pointer. true = the family is live; individual gates (allow_delegate) still apply.',
|
|
380
|
-
),
|
|
381
369
|
model: z.string().default('auto').describe(
|
|
382
370
|
"Model handed to the codex CLI. 'auto' (default) = pass no --model flag so the CLI's own default applies — tracks the subscription's current strongest model instead of pinning a stale ID. Any other value passes through verbatim as `--model <value>`.",
|
|
383
371
|
),
|
|
384
372
|
allow_delegate: z.boolean().default(false).describe(
|
|
385
|
-
'Second opt-in for the only wrapper that delegates write access (/team:delegate). false (default) = delegate refuses even when
|
|
373
|
+
'Second opt-in for the only wrapper that delegates write access (/team:delegate). false (default) = delegate refuses even when /team itself is available. Availability (codex CLI installed + authenticated) is necessary but not sufficient — this key must ALSO be true before delegation is reachable.',
|
|
386
374
|
),
|
|
387
375
|
max_calls_per_day: z.number().int().min(0).default(50).describe(
|
|
388
376
|
'Per-day cap on team calls, read against the EXISTING cli_call_budget openai bucket (~/.event4u/agent-config/cli-calls.json, daily UTC reset) — one subscription, one counter, never a parallel count. 0 = block all team calls.',
|
|
@@ -398,7 +386,15 @@ export const settingsSchema = z.object({
|
|
|
398
386
|
'Circuit-breaker bound: after this many CONSECUTIVE BLOCK verdicts in one session, a visible notice is injected exactly once and the managed layer stops re-blocking — the user decides, never an infinite Claude↔Codex loop. An ALLOW verdict resets the counter. Positive integer.',
|
|
399
387
|
),
|
|
400
388
|
}).default({ managed: false, max_consecutive_blocks: 3 }),
|
|
401
|
-
}).default({
|
|
389
|
+
}).default({ model: 'auto', allow_delegate: false, max_calls_per_day: 50, suppress_setup_hint: false, review_gate: { managed: false, max_consecutive_blocks: 3 } }),
|
|
390
|
+
emergency: z.object({
|
|
391
|
+
orchestration_halt: z.boolean().default(false).describe(
|
|
392
|
+
'The one audited incident switch over the always-on orchestration stack (subagents, council, team). NOT an activation gate: false (default) = the stack runs normally. true = halted; arming requires no ceremony. Disarming (returning to false) requires orchestration_halt_justification to be a non-empty string. Both transitions emit one telemetry line.',
|
|
393
|
+
),
|
|
394
|
+
orchestration_halt_justification: z.string().default('').describe(
|
|
395
|
+
'Required non-empty before orchestration_halt may return to false. Ignored while arming the halt.',
|
|
396
|
+
),
|
|
397
|
+
}).default({ orchestration_halt: false, orchestration_halt_justification: '' }),
|
|
402
398
|
onboarding: z.object({
|
|
403
399
|
onboarded: z.boolean().default(false).describe(
|
|
404
400
|
'Set to true once the developer has completed `agent-config setup`. The onboarding-gate rule blocks the first turn of every chat until this is true. Toggle back to false to re-trigger the wizard.',
|
|
@@ -524,6 +520,17 @@ export const settingsSchema = z.object({
|
|
|
524
520
|
'PreToolUse code-graph nudge (ADR-124 Phase 4). Default off. When on AND a native code-graph cache or a consumer-shipped graph.json/SCIP index is present, warns once per session (never blocks) as the agent is about to Grep/Glob or Read a source file — query the graph first for who-calls/where-used/impact questions (or rebuild if stale, build if absent). Source G’s strict block-first-read mode is deliberately un-ported.',
|
|
525
521
|
),
|
|
526
522
|
}).default({}),
|
|
523
|
+
turn_end_gate: z.object({
|
|
524
|
+
enabled: z.boolean().default(false).describe(
|
|
525
|
+
'Stop-slot turn-end gate (road-to-conformance-round5 Phase 3). Default off, and it is the only concern that can REFUSE a turn-end rather than a tool call — so it soaks behind an explicit opt-in before it binds. Round 5 measured the reason: both blocking carriers reached zero violations, neither advisory carrier did, and 19 wrong-language replies survived a pin that had fired 26-35 seconds earlier. A refused turn continues in the SAME turn and is never refused twice.',
|
|
526
|
+
),
|
|
527
|
+
promissory: z.boolean().default(true).describe(
|
|
528
|
+
'Detector A of the turn-end gate: refuse a turn whose closing paragraph promises work not yet performed while asking the user nothing (20 measured occurrences). Inert while hooks.turn_end_gate.enabled is false.',
|
|
529
|
+
),
|
|
530
|
+
language: z.boolean().default(true).describe(
|
|
531
|
+
'Detector B of the turn-end gate: refuse a turn whose user-visible prose is not in the pinned language, with fenced code, inline code, quoted tool output, tables, URLs, paths and identifiers excluded first (19 measured occurrences). Fires 18.0% of turns on the measured corpus, which is why the master switch is off by default. Inert while hooks.turn_end_gate.enabled is false.',
|
|
532
|
+
),
|
|
533
|
+
}).default({}),
|
|
527
534
|
}),
|
|
528
535
|
decision_engine: z.object({
|
|
529
536
|
surface_traces: z.boolean().default(false).describe(
|