@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
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
* - already `rtk …` wrapped, or piped / compound (`|`, `&&`, `||`, `;`)
|
|
27
27
|
* - any program not in the verbose-CLI allowlist
|
|
28
28
|
*
|
|
29
|
+
* Second branch (token-economy-cache Phase 4.2, advisory-degraded): for
|
|
30
|
+
* commands rtk cannot wrap — an unbounded tree-wide search per the committed
|
|
31
|
+
* OUTPUT_CAP_TABLE below — emit a warn naming the bounded alternative. No
|
|
32
|
+
* rewrite, no truncation; see the table's comment for the degradation record.
|
|
33
|
+
*
|
|
29
34
|
* Exit codes (dispatcher contract): 0 allow · 2 warn (+ JSON reason on stdout).
|
|
30
35
|
*/
|
|
31
36
|
import fs from "node:fs";
|
|
@@ -175,6 +180,108 @@ export function classify(command: string): Eligibility {
|
|
|
175
180
|
return { eligible: true, program };
|
|
176
181
|
}
|
|
177
182
|
|
|
183
|
+
// ── Unbounded-output cap ADVISORY (token-economy-cache Phase 4.2) ──────────
|
|
184
|
+
// The roadmap step asked for a deterministic PreToolUse cap REWRITE; the v1
|
|
185
|
+
// dispatcher contract carries no `updatedInput`, so per the roadmap's
|
|
186
|
+
// pre-registered consequence this degrades to an ADVISORY: a committed
|
|
187
|
+
// per-command cap table (this constant — rows individually removable,
|
|
188
|
+
// per-row opt-out via `enabled`) and a warn naming the bounded alternative.
|
|
189
|
+
// Nothing is ever truncated; ignoring the advisory IS the uncapped re-run,
|
|
190
|
+
// and repeated fires in the dispatcher's warn counters are the demand
|
|
191
|
+
// signal that calibrates the table. Table scope is evidence-bound: only the
|
|
192
|
+
// class the 2026-08-10 corpus run measured (tree-wide search — 302 KB raw
|
|
193
|
+
// vs 21 KB bounded on this repo; internal/bench/rtk-savings/RESULTS.md).
|
|
194
|
+
export interface CapRow {
|
|
195
|
+
enabled: boolean;
|
|
196
|
+
/** long flags whose presence means the output is already bounded */
|
|
197
|
+
bounded_long_flags: readonly string[];
|
|
198
|
+
/** short-option letters (combined forms like `-rln` count) that bound output */
|
|
199
|
+
bounded_short_letters: string;
|
|
200
|
+
/** fire only when a recursive/tree-wide flag is present ("" = always tree-wide) */
|
|
201
|
+
recursive_short_letters: string;
|
|
202
|
+
recursive_long_flags: readonly string[];
|
|
203
|
+
hint: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export const OUTPUT_CAP_TABLE: Readonly<Record<string, CapRow>> = {
|
|
207
|
+
grep: {
|
|
208
|
+
enabled: true,
|
|
209
|
+
bounded_long_flags: ["--files-with-matches", "--count", "--max-count"],
|
|
210
|
+
bounded_short_letters: "lcm",
|
|
211
|
+
recursive_short_letters: "rR",
|
|
212
|
+
recursive_long_flags: ["--recursive", "--dereference-recursive"],
|
|
213
|
+
hint: "add `-l` (files only) or `-m 5` (per-file cap), or pipe through `| head -n 100`",
|
|
214
|
+
},
|
|
215
|
+
rg: {
|
|
216
|
+
enabled: true,
|
|
217
|
+
bounded_long_flags: ["--files-with-matches", "--count", "--max-count", "--count-matches"],
|
|
218
|
+
bounded_short_letters: "lcm",
|
|
219
|
+
recursive_short_letters: "",
|
|
220
|
+
recursive_long_flags: [],
|
|
221
|
+
hint: "add `-l` (files only) or `--max-count 5`, or pipe through `| head -n 100`",
|
|
222
|
+
},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
/** Pipeline members that already bound or consume the stream. */
|
|
226
|
+
const _BOUNDING_PROGRAMS: ReadonlySet<string> = new Set(["head", "tail", "wc", "rtk"]);
|
|
227
|
+
|
|
228
|
+
function _shortLetters(token: string): string | null {
|
|
229
|
+
return /^-[A-Za-z]+$/.test(token) ? token.slice(1) : null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function _hasAnyFlag(group: readonly string[], longFlags: readonly string[], shortLetters: string): boolean {
|
|
233
|
+
for (const t of group) {
|
|
234
|
+
if (longFlags.some((f) => t === f || t.startsWith(`${f}=`))) return true;
|
|
235
|
+
const letters = _shortLetters(t);
|
|
236
|
+
if (letters && shortLetters && [...shortLetters].some((c) => letters.includes(c))) return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface CapVerdict {
|
|
242
|
+
program: string;
|
|
243
|
+
hint: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* An unbounded tree-wide search in the command (any pipeline segment) with no
|
|
248
|
+
* bounding flag and no bounding pipeline member → advisory verdict. Fail-open:
|
|
249
|
+
* parse errors and anything ambiguous return null.
|
|
250
|
+
*/
|
|
251
|
+
export function classifyCap(
|
|
252
|
+
command: string,
|
|
253
|
+
table: Readonly<Record<string, CapRow>> = OUTPUT_CAP_TABLE,
|
|
254
|
+
): CapVerdict | null {
|
|
255
|
+
const cmd = command.trim();
|
|
256
|
+
if (!cmd) return null;
|
|
257
|
+
let tokens: string[];
|
|
258
|
+
try {
|
|
259
|
+
tokens = shlexSplit(cmd);
|
|
260
|
+
} catch (e) {
|
|
261
|
+
if (e instanceof ShlexError) return null;
|
|
262
|
+
throw e;
|
|
263
|
+
}
|
|
264
|
+
const groups = _split_subcommands(tokens);
|
|
265
|
+
let hit: CapVerdict | null = null;
|
|
266
|
+
for (const group of groups) {
|
|
267
|
+
let i = 0;
|
|
268
|
+
while (i < group.length && _is_env_assignment(group[i] as string)) i += 1;
|
|
269
|
+
const program = group[i];
|
|
270
|
+
if (!program) continue;
|
|
271
|
+
// A bounding consumer anywhere in the chain (head/tail/wc/rtk) → bounded.
|
|
272
|
+
if (_BOUNDING_PROGRAMS.has(program)) return null;
|
|
273
|
+
const row = table[program];
|
|
274
|
+
if (!row || !row.enabled) continue;
|
|
275
|
+
const needsRecursive = row.recursive_short_letters !== "" || row.recursive_long_flags.length > 0;
|
|
276
|
+
if (needsRecursive && !_hasAnyFlag(group, row.recursive_long_flags, row.recursive_short_letters)) {
|
|
277
|
+
continue; // single-file / non-tree invocation — not the measured class
|
|
278
|
+
}
|
|
279
|
+
if (_hasAnyFlag(group, row.bounded_long_flags, row.bounded_short_letters)) continue;
|
|
280
|
+
if (hit === null) hit = { program, hint: row.hint };
|
|
281
|
+
}
|
|
282
|
+
return hit;
|
|
283
|
+
}
|
|
284
|
+
|
|
178
285
|
function _readStdin(): string {
|
|
179
286
|
return readHookStdin();
|
|
180
287
|
}
|
|
@@ -203,6 +310,9 @@ export function main(): number {
|
|
|
203
310
|
: ".";
|
|
204
311
|
if (!_enabled(root)) return EXIT_ALLOW;
|
|
205
312
|
|
|
313
|
+
const command = _extract_command(envelope);
|
|
314
|
+
if (!command) return EXIT_ALLOW;
|
|
315
|
+
|
|
206
316
|
// Deterministic gate: the live probe, NOT a self-reported flag — and an
|
|
207
317
|
// IDENTITY probe, not bare presence: the `rtk` binary name collides with
|
|
208
318
|
// the unrelated Rust Type Kit, and nudging the agent to wrap commands with
|
|
@@ -211,21 +321,30 @@ export function main(): number {
|
|
|
211
321
|
// NOT — fail closed for behavior, fail open for the command itself.
|
|
212
322
|
// Identity is cached user-globally keyed on the binary's path+mtime+size,
|
|
213
323
|
// so the `rtk gain` probe runs once per installed binary, not per command.
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
324
|
+
if (rtk_available() && detectRtkCached().identity === "token-killer") {
|
|
325
|
+
const verdict = classify(command);
|
|
326
|
+
if (verdict.eligible) {
|
|
327
|
+
const reason =
|
|
328
|
+
`rtk is installed and wraps verbose CLI output (upstream reports 60–90% fewer output tokens). ` +
|
|
329
|
+
`Re-run wrapped: \`rtk ${command.trim()}\`. ` +
|
|
330
|
+
`(Skip for completeness-critical output like \`git diff\`.)`;
|
|
331
|
+
process.stdout.write(`${_jsonReason(reason)}\n`);
|
|
332
|
+
return EXIT_WARN;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
222
335
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
336
|
+
// Cap advisory needs no rtk — it covers exactly the commands rtk cannot
|
|
337
|
+
// wrap (compound/piped, or outside its allowlist).
|
|
338
|
+
const cap = classifyCap(command);
|
|
339
|
+
if (cap) {
|
|
340
|
+
const reason =
|
|
341
|
+
`Unbounded \`${cap.program}\` output: a tree-wide search can return hundreds of KB into context ` +
|
|
342
|
+
`(measured here: 302 KB raw vs 21 KB bounded — internal/bench/rtk-savings/RESULTS.md). ` +
|
|
343
|
+
`Prefer a bounded form — ${cap.hint}. Re-running unbounded is fine when completeness is required.`;
|
|
344
|
+
process.stdout.write(`${_jsonReason(reason)}\n`);
|
|
345
|
+
return EXIT_WARN;
|
|
346
|
+
}
|
|
347
|
+
return EXIT_ALLOW;
|
|
229
348
|
}
|
|
230
349
|
|
|
231
350
|
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* session-eol — Stop-slot session end-of-life instrument + recycle advisory
|
|
4
|
+
* (road-to-token-economy-recycling Phases 1.1 / 3.2 / 4.2).
|
|
5
|
+
*
|
|
6
|
+
* Phase 1.1 (record-only): per session, maintain counts-only end-of-life
|
|
7
|
+
* state — final main-chain context size in parsed tokens (via
|
|
8
|
+
* `_lib/session_eol.ts` → `cc_transcript.billableInputTokens`), turn count,
|
|
9
|
+
* and every observed compaction event — under
|
|
10
|
+
* `agents/runtime/state/session-eol/<sha256(session)>.json`. Incremental:
|
|
11
|
+
* each Stop reads only the transcript bytes appended since the last scan.
|
|
12
|
+
*
|
|
13
|
+
* Phase 3.2 (advisory, once per session): past the committed recycle
|
|
14
|
+
* threshold (`src/config/recycle-threshold-budget.json`), inject ONE line
|
|
15
|
+
* advising `session:recycle` — the F2 once-per-session pattern of
|
|
16
|
+
* `end_review_nudge_hook.ts`. Absent/unreadable config or transcript is
|
|
17
|
+
* SILENCE, never a block (fail-open); hooks cannot inject `/clear`, so the
|
|
18
|
+
* recycle action itself stays advisory-carried by design (roadmap 5.1).
|
|
19
|
+
*
|
|
20
|
+
* Phase 4.2 (read surface): every Stop also overwrites
|
|
21
|
+
* `agents/runtime/state/context-fill.json` with the machine-readable fill
|
|
22
|
+
* level + threshold state — display substrate for an external statusline;
|
|
23
|
+
* carries counts only, and this hook's behaviour is identical whether or
|
|
24
|
+
* not anything reads it.
|
|
25
|
+
*
|
|
26
|
+
* Never blocks: exit 0 on every silent path; the advisory reports at
|
|
27
|
+
* exit 2 ({decision:"warn", additional_context}) which `host_semantics`
|
|
28
|
+
* reduces to a non-blocking warn on claude. `AGENT_CONFIG_REPLAY=1` → no-op.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import * as crypto from 'node:crypto';
|
|
32
|
+
import * as fs from 'node:fs';
|
|
33
|
+
import * as path from 'node:path';
|
|
34
|
+
import process from 'node:process';
|
|
35
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
36
|
+
|
|
37
|
+
import recycleThresholdConfig from '../../config/recycle-threshold-budget.json';
|
|
38
|
+
|
|
39
|
+
import {
|
|
40
|
+
emptyCounters,
|
|
41
|
+
readNewCompleteLines,
|
|
42
|
+
scanEolSlice,
|
|
43
|
+
type EolCounters,
|
|
44
|
+
} from '../_lib/session_eol.js';
|
|
45
|
+
import { unwrap, type JsonObject, type JsonValue } from './envelope.js';
|
|
46
|
+
import { readHookStdin } from './hook_stdin.js';
|
|
47
|
+
import { atomic_write_json, is_replay_mode } from './state_io.js';
|
|
48
|
+
import { isSafeTranscriptPath } from './end_review_nudge_hook.js';
|
|
49
|
+
|
|
50
|
+
const EXIT_WARN = 2;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Per-process threshold override — the test seam and the emergency off
|
|
54
|
+
* switch (`0` or any non-positive value disables the advisory lane while
|
|
55
|
+
* recording continues). The committed value lives in
|
|
56
|
+
* `src/config/recycle-threshold-budget.json` and is statically imported so
|
|
57
|
+
* the hook bundle carries it into consumers — one threshold, one constant.
|
|
58
|
+
*/
|
|
59
|
+
export const THRESHOLD_OVERRIDE_ENV = 'AGENT_RECYCLE_THRESHOLD_TOKENS';
|
|
60
|
+
|
|
61
|
+
/** Machine-readable fill-level surface (Phase 4.2). Counts only, overwritten per Stop. */
|
|
62
|
+
export const CONTEXT_FILL_REL = path.join('agents', 'runtime', 'state', 'context-fill.json');
|
|
63
|
+
|
|
64
|
+
export interface SessionEolState {
|
|
65
|
+
schema_version: 1;
|
|
66
|
+
counters: EolCounters;
|
|
67
|
+
/** ISO stamp when the recycle advisory fired for this session (F2), or null. */
|
|
68
|
+
advisory_fired_at: string | null;
|
|
69
|
+
updated_at: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function str(value: JsonValue | undefined): string {
|
|
73
|
+
return typeof value === 'string' ? value : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stateDir(workspaceRoot: string): string {
|
|
77
|
+
return path.join(workspaceRoot, 'agents', 'runtime', 'state', 'session-eol');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function stateFile(workspaceRoot: string, sessionKey: string): string {
|
|
81
|
+
return path.join(stateDir(workspaceRoot), `${sessionKey}.json`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Same derivation as `end_review_nudge_hook.deriveSessionKey` — hashed, never raw. */
|
|
85
|
+
export function deriveSessionKey(envelope: JsonObject, payload: JsonObject): string {
|
|
86
|
+
const raw =
|
|
87
|
+
str(envelope['session_id'] as JsonValue | undefined) ||
|
|
88
|
+
str((payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined);
|
|
89
|
+
return crypto.createHash('sha256').update(raw || 'unknown-session').digest('hex');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Load prior per-session state; any read/shape error reads as "fresh session". */
|
|
93
|
+
export function readState(file: string): SessionEolState {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as Partial<SessionEolState>;
|
|
96
|
+
if (
|
|
97
|
+
parsed &&
|
|
98
|
+
typeof parsed === 'object' &&
|
|
99
|
+
parsed.schema_version === 1 &&
|
|
100
|
+
parsed.counters &&
|
|
101
|
+
typeof parsed.counters.scanned_bytes === 'number'
|
|
102
|
+
) {
|
|
103
|
+
return {
|
|
104
|
+
schema_version: 1,
|
|
105
|
+
counters: parsed.counters,
|
|
106
|
+
advisory_fired_at:
|
|
107
|
+
typeof parsed.advisory_fired_at === 'string' ? parsed.advisory_fired_at : null,
|
|
108
|
+
updated_at: typeof parsed.updated_at === 'string' ? parsed.updated_at : '',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
// fall through — fresh state
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
schema_version: 1,
|
|
116
|
+
counters: emptyCounters(),
|
|
117
|
+
advisory_fired_at: null,
|
|
118
|
+
updated_at: '',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Resolve the recycle threshold (Phase 3.1): the per-process override wins
|
|
124
|
+
* (tests; `0` = advisory lane off), else the committed constant from
|
|
125
|
+
* `recycle-threshold-budget.json`. Returns `null` when the lane is
|
|
126
|
+
* disabled or the value is not a positive number — recording continues
|
|
127
|
+
* either way (fail-open, never a block).
|
|
128
|
+
*/
|
|
129
|
+
export function readThresholdTokens(): number | null {
|
|
130
|
+
const override = process.env[THRESHOLD_OVERRIDE_ENV];
|
|
131
|
+
if (override !== undefined) {
|
|
132
|
+
const value = Number(override);
|
|
133
|
+
return Number.isFinite(value) && value > 0 ? value : null;
|
|
134
|
+
}
|
|
135
|
+
const value = (recycleThresholdConfig as Record<string, unknown>)['recycle_threshold_tokens'];
|
|
136
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The exactly-one advisory line injected past threshold (Phase 3.2). */
|
|
140
|
+
export function buildAdvisoryLine(tokens: number, threshold: number): string {
|
|
141
|
+
return (
|
|
142
|
+
`context past recycle threshold (${tokens.toLocaleString('en-US')} of ` +
|
|
143
|
+
`${threshold.toLocaleString('en-US')} tokens): run \`agent-config session:recycle\` to ` +
|
|
144
|
+
`write the recycle envelope, then /clear — the successor session resumes from the ` +
|
|
145
|
+
`envelope at session_start (road-to-token-economy-recycling)`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Overwrite the Phase 4.2 read surface. Counts only; failures are swallowed. */
|
|
150
|
+
function writeContextFill(
|
|
151
|
+
workspaceRoot: string,
|
|
152
|
+
tokens: number | null,
|
|
153
|
+
threshold: number | null,
|
|
154
|
+
ts: string,
|
|
155
|
+
): void {
|
|
156
|
+
try {
|
|
157
|
+
atomic_write_json(path.join(workspaceRoot, CONTEXT_FILL_REL), {
|
|
158
|
+
schema_version: 1,
|
|
159
|
+
final_context_tokens: tokens,
|
|
160
|
+
recycle_threshold_tokens: threshold,
|
|
161
|
+
past_threshold: tokens !== null && threshold !== null && tokens >= threshold,
|
|
162
|
+
updated_at: ts,
|
|
163
|
+
});
|
|
164
|
+
} catch {
|
|
165
|
+
// a state-write failure must never block the Stop path
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function main(): number {
|
|
170
|
+
const [envelope, payload] = unwrap(readHookStdin(), 'claude');
|
|
171
|
+
|
|
172
|
+
const event = String(envelope['event'] ?? '');
|
|
173
|
+
if (event !== '' && event !== 'stop') {
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
if (is_replay_mode()) {
|
|
177
|
+
return 0; // replay fixtures never mutate session state
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const workspaceRoot = String(envelope['workspace_root'] ?? '').trim() || process.cwd();
|
|
181
|
+
|
|
182
|
+
const transcriptPath = str(
|
|
183
|
+
(payload['transcript_path'] ?? payload['transcriptPath']) as JsonValue | undefined,
|
|
184
|
+
).trim();
|
|
185
|
+
if (!transcriptPath || !isSafeTranscriptPath(transcriptPath)) {
|
|
186
|
+
return 0; // no readable ledger — silence, never a block
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const sessionKey = deriveSessionKey(envelope, payload);
|
|
190
|
+
const file = stateFile(workspaceRoot, sessionKey);
|
|
191
|
+
const state = readState(file);
|
|
192
|
+
|
|
193
|
+
let counters = state.counters;
|
|
194
|
+
const read = readNewCompleteLines(transcriptPath, counters.scanned_bytes);
|
|
195
|
+
if (read.truncated) {
|
|
196
|
+
const full = readNewCompleteLines(transcriptPath, 0);
|
|
197
|
+
counters = scanEolSlice(full.text, emptyCounters());
|
|
198
|
+
counters.scanned_bytes = full.nextOffset;
|
|
199
|
+
} else if (read.text.length > 0) {
|
|
200
|
+
counters = scanEolSlice(read.text, counters);
|
|
201
|
+
counters.scanned_bytes = read.nextOffset;
|
|
202
|
+
} else {
|
|
203
|
+
counters = { ...counters, scanned_bytes: read.nextOffset };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const now = new Date().toISOString();
|
|
207
|
+
const threshold = readThresholdTokens();
|
|
208
|
+
const tokens = counters.final_context_tokens;
|
|
209
|
+
|
|
210
|
+
const shouldAdvise =
|
|
211
|
+
threshold !== null &&
|
|
212
|
+
tokens !== null &&
|
|
213
|
+
tokens >= threshold &&
|
|
214
|
+
state.advisory_fired_at === null;
|
|
215
|
+
|
|
216
|
+
const nextState: SessionEolState = {
|
|
217
|
+
schema_version: 1,
|
|
218
|
+
counters,
|
|
219
|
+
advisory_fired_at: shouldAdvise ? now : state.advisory_fired_at,
|
|
220
|
+
updated_at: now,
|
|
221
|
+
};
|
|
222
|
+
try {
|
|
223
|
+
atomic_write_json(file, nextState);
|
|
224
|
+
} catch {
|
|
225
|
+
// recording is best-effort; never block the Stop path
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
writeContextFill(workspaceRoot, tokens, threshold, now);
|
|
229
|
+
|
|
230
|
+
if (shouldAdvise && threshold !== null && tokens !== null) {
|
|
231
|
+
process.stdout.write(
|
|
232
|
+
`${JSON.stringify({
|
|
233
|
+
decision: 'warn',
|
|
234
|
+
reason: `session-eol: context ${tokens} >= recycle threshold ${threshold}`,
|
|
235
|
+
additional_context: buildAdvisoryLine(tokens, threshold),
|
|
236
|
+
})}\n`,
|
|
237
|
+
);
|
|
238
|
+
return EXIT_WARN;
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Bundle-safety: never auto-run when inlined into an esbuild bundle, where
|
|
244
|
+
// every module shares the bundle's `import.meta.url` (see cmd_migrate.ts).
|
|
245
|
+
declare const __AGENT_CONFIG_BUNDLE__: boolean | undefined;
|
|
246
|
+
function _isCliEntry(): boolean {
|
|
247
|
+
if (typeof __AGENT_CONFIG_BUNDLE__ !== 'undefined' && __AGENT_CONFIG_BUNDLE__) {
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
if (process.argv[1] === undefined) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
254
|
+
if (import.meta.url === argvUrl) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
259
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
260
|
+
return here === argv;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (_isCliEntry()) {
|
|
267
|
+
process.exit(main());
|
|
268
|
+
}
|