@event4u/agent-config 17.0.0 → 17.1.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +104 -485
- package/README.md +2 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +5 -3
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +5 -1
- package/dist/cli-delegate/{chunk-IROAQ77A.js → chunk-3PK3VZHB.js} +107 -9
- package/dist/cli-delegate/{chunk-2SFOOFJJ.js → chunk-BZJ4Y3CI.js} +1 -1
- package/dist/cli-delegate/{chunk-CAL76ADI.js → chunk-CMZEKSNN.js} +52 -10
- package/dist/cli-delegate/{chunk-ESUNQ5XR.js → chunk-E3CFULNX.js} +22 -4
- package/dist/cli-delegate/{chunk-GOMILC3O.js → chunk-QBHEI5N4.js} +4 -3
- package/dist/cli-delegate/{chunk-SIOMDAKL.js → chunk-VYZGJLCA.js} +29 -39
- package/dist/cli-delegate/chunk-W5JXMF2T.js +77 -0
- package/dist/cli-delegate/{chunk-4SHNERBL.js → chunk-XIAPY44N.js} +1 -1
- package/dist/cli-delegate/{chunk-JHMMWNJ2.js → chunk-YHAPONWV.js} +1 -1
- package/dist/cli-delegate/cmd_conformance.js +9 -8
- package/dist/cli-delegate/cmd_doctor.js +4 -4
- package/dist/cli-delegate/cmd_doctor_neighbours.js +2 -2
- package/dist/cli-delegate/cmd_explain.js +1 -1
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_fleet.js +3 -3
- package/dist/cli-delegate/cmd_preflight.js +3 -3
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_settings_set.js +1 -1
- 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 +9 -13
- package/dist/cli-delegate/cmd_validate.js +6 -6
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +98 -74
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/install/claudeRuleRewrite.js +10 -3
- package/dist/install/conflict.js +24 -2
- package/dist/install/conflictTracker.js +14 -3
- package/dist/install/install.mjs +222 -117
- package/dist/install/installThinLayer.js +1 -0
- package/dist/install/loggedWrites.js +142 -0
- package/dist/install/paths.js +12 -2
- package/dist/install/preserve.js +25 -4
- package/dist/install/txlog.js +10 -6
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +845 -294
- package/dist/scripts/_lib/egress_rules.js +4 -1
- package/dist/scripts/_lib/identifier_redaction.js +35 -2
- package/dist/scripts/ai_council/seat_policy.js +7 -1
- package/dist/server/schemas/settings.js +1 -1
- package/package.json +1 -1
- package/src/config/ci-local-parity.yml +1 -1
- package/src/config/gate-coverage.yml +27 -7
- package/src/config/gate-violation-baselines.json +8 -2
- package/src/config/hook-bundle-budget.json +2 -2
- package/src/config/production-entries.json +18 -0
- package/src/config/release-gate-locality.yml +0 -7
- package/src/install/claudeRuleRewrite.ts +10 -1
- package/src/install/conflict.ts +24 -1
- package/src/install/conflictTracker.ts +18 -2
- package/src/install/installThinLayer.ts +6 -0
- package/src/install/loggedWrites.ts +148 -0
- package/src/install/paths.ts +11 -2
- package/src/install/preserve.ts +40 -4
- package/src/install/txlog.ts +10 -6
- package/src/scripts/_cli/cmd_conformance.ts +3 -2
- package/src/scripts/_cli/cmd_update.ts +15 -17
- package/src/scripts/_lib/council_settings_block.ts +9 -2
- package/src/scripts/_lib/egress_rules.ts +4 -1
- package/src/scripts/_lib/host_capability.ts +10 -5
- package/src/scripts/_lib/identifier_redaction.ts +38 -2
- package/src/scripts/_lib/price_freshness.ts +112 -0
- package/src/scripts/_lib/production_reach.ts +194 -0
- package/src/scripts/_lib/quota_parked.ts +17 -28
- package/src/scripts/_lib/release_findings_ingest.ts +13 -335
- package/src/scripts/_lib/tier_downshift_verdict.ts +81 -35
- package/src/scripts/ai_council/chairman.ts +5 -2
- package/src/scripts/ai_council/clients.ts +4 -3
- package/src/scripts/ai_council/free_hand_shadow.ts +24 -12
- package/src/scripts/ai_council/free_route_catalog.ts +142 -5
- package/src/scripts/ai_council/free_route_client.ts +4 -0
- package/src/scripts/ai_council/hand_payload.ts +115 -7
- package/src/scripts/ai_council/orchestrator.ts +7 -10
- package/src/scripts/ai_council/route_chooser.ts +1 -1
- package/src/scripts/ai_council/route_member.ts +66 -4
- package/src/scripts/ai_council/seat_policy.ts +8 -1
- package/src/scripts/ai_council/self_hosted_probe.ts +327 -0
- package/src/scripts/ai_council/status_surface.ts +2 -0
- package/src/scripts/ask_transport.ts +137 -15
- package/src/scripts/check_finding_dispositions.ts +25 -71
- package/src/scripts/check_release_pr_shape.ts +3 -3
- package/src/scripts/check_step_production_reach.ts +257 -0
- package/src/scripts/council_cli.ts +10 -8
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +3 -0
- package/src/scripts/hooks/dispatch_hook.ts +2 -2
- package/src/scripts/hooks/payload_stub.ts +27 -0
- package/src/scripts/hooks/spawn_guard_shadow_hook.ts +29 -70
- package/src/scripts/hooks/subagent_ledger_hook.ts +80 -0
- package/src/scripts/install.ts +2 -2
- package/src/scripts/lint_decision_classes.ts +34 -0
- package/src/scripts/lint_hook_manifest.ts +24 -0
- package/src/scripts/lint_roadmap_blockers.ts +52 -3
- package/src/scripts/release.ts +5 -9
- package/src/scripts/release_drill.ts +11 -165
- package/src/scripts/release_publication.ts +0 -9
- package/src/scripts/report_production_reach.ts +35 -0
- package/src/scripts/worker_reuse_shadow_report.ts +399 -1
- package/src/server/schemas/settings.ts +1 -1
- package/dist/cli-delegate/chunk-A4QJ6ENT.js +0 -50
- /package/dist/cli-delegate/{chunk-E6XNBVE4.js → chunk-KR5ZIFPO.js} +0 -0
|
@@ -14,7 +14,12 @@ import * as fs from 'node:fs';
|
|
|
14
14
|
import * as path from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { median, readLedgerStarts, segmentLegsIntoRuns, type SubagentRun } from './cache_realization_report.js';
|
|
18
|
+
import { scanTranscripts } from './_lib/cc_transcript.js';
|
|
19
|
+
import type { Tier } from './_lib/subagent_routing.js';
|
|
20
|
+
import { watermarkForTier } from './_lib/worker_budget.js';
|
|
21
|
+
import { REFUSAL_REASONS, shadowReuseVerdict, type ReuseClass, type ReuseContext, type WorkerState } from './_lib/worker_reuse.js';
|
|
22
|
+
import { LEDGER_DIR, refFor } from './hooks/subagent_ledger_hook.js';
|
|
18
23
|
|
|
19
24
|
export interface ShadowLine {
|
|
20
25
|
event: string;
|
|
@@ -91,6 +96,332 @@ export function flipReadiness(w: ShadowWindow, m: FlipMeasurements): { ready: bo
|
|
|
91
96
|
return { ready: missing.length === 0, missing };
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
// Council 2026-10-22, decision 1: the hook records the precondition and the join keys only. The full verdict
|
|
100
|
+
// for each spawn is rebuilt here, at report time, from the ledger and the
|
|
101
|
+
// stopped workers' transcripts, and every output carries `basis:
|
|
102
|
+
// 'reconstructed'`. A spawn whose join lacks an input is `incomplete_join` with
|
|
103
|
+
// the missing field named — never silently `fresh`.
|
|
104
|
+
|
|
105
|
+
/** Any subagent-ledger line; only the fields the reconstruction joins on are typed. */
|
|
106
|
+
export interface LedgerLine {
|
|
107
|
+
event: string;
|
|
108
|
+
ts: string;
|
|
109
|
+
tool?: string;
|
|
110
|
+
dispatch_kind?: string;
|
|
111
|
+
ref?: string;
|
|
112
|
+
session_id?: string | null;
|
|
113
|
+
agent_type?: string | null;
|
|
114
|
+
name_key?: string;
|
|
115
|
+
head?: string | null;
|
|
116
|
+
cwd_key?: string | null;
|
|
117
|
+
epoch?: number | null;
|
|
118
|
+
reuse?: ShadowLine['reuse'];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** A stopped worker's transcript, keyed by its ledger ref. */
|
|
122
|
+
export interface WorkerTranscript {
|
|
123
|
+
/**
|
|
124
|
+
* Assistant calls, chronological. `context_tokens` is billable input + output of that call;
|
|
125
|
+
* `input_tokens` is its total input (uncached + cache write + cache read).
|
|
126
|
+
*/
|
|
127
|
+
calls: Array<{ ts: string; context_tokens: number; input_tokens: number }>;
|
|
128
|
+
/** Timestamps of `compact_boundary` records. */
|
|
129
|
+
compact_ts: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** What a package definition under `src/subagents/` says; absent → no `reuse:` → not continuity. */
|
|
133
|
+
export interface DefinitionFacts {
|
|
134
|
+
reuse: ReuseClass | null;
|
|
135
|
+
tier: Tier;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ReconstructionInputs {
|
|
139
|
+
lines: readonly LedgerLine[];
|
|
140
|
+
transcripts: ReadonlyMap<string, WorkerTranscript>;
|
|
141
|
+
definitions: Readonly<Record<string, DefinitionFacts>>;
|
|
142
|
+
fresh_cold_start_by_type: Readonly<Record<string, number | null>>;
|
|
143
|
+
/** Host default prompt-cache TTL. */
|
|
144
|
+
cache_ttl_ms?: number;
|
|
145
|
+
/** A spawn line without a session id is joined to the next start of its type inside this window. */
|
|
146
|
+
session_join_window_ms?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Spawns before this instant are not reported. `lines` must then reach back past it: a session
|
|
149
|
+
* with register lines before it has a register the window cut, so it reads as unrecorded.
|
|
150
|
+
*/
|
|
151
|
+
window_start?: string | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export type ReconstructedSpawn =
|
|
155
|
+
| { ts: string; agent_type: string; basis: 'reconstructed'; verdict: 'would_resume'; name: string; reasons: []; candidates: number }
|
|
156
|
+
| { ts: string; agent_type: string; basis: 'reconstructed'; verdict: 'fresh'; name: null; reasons: string[]; candidates: number }
|
|
157
|
+
| { ts: string; agent_type: string | null; basis: 'reconstructed'; verdict: 'incomplete_join'; missing: string[] };
|
|
158
|
+
|
|
159
|
+
export const HOST_DEFAULT_CACHE_TTL_MS = 300_000;
|
|
160
|
+
|
|
161
|
+
/** Stated beside every reconstructed window: what was approximated, and by what. */
|
|
162
|
+
export const RECONSTRUCTION_APPROXIMATIONS = [
|
|
163
|
+
'package_version := epoch of the stop/spawn line (bundle mtime); a change is reported once, as epoch_changed (the decision function\'s policy_epoch_changed)',
|
|
164
|
+
'skill_set_signature := not recorded per worker, so not compared — workspace_changed reflects worktree and head only',
|
|
165
|
+
'last_request_at := the worker\'s last subagent_stop ts',
|
|
166
|
+
'cache_ttl := host default 300000 ms',
|
|
167
|
+
'host_resume := true (worker_resume live on claude under CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS, council 2026-10-22)',
|
|
168
|
+
'observed fresh cold start := median over the whole window, not only runs before the spawn',
|
|
169
|
+
'transcript_tokens := last streamed record per message id of the worker\'s last call before the spawn',
|
|
170
|
+
'register := a session with register lines before --since, or a named worker whose start is unseen, is worker_register_unrecorded',
|
|
171
|
+
] as const;
|
|
172
|
+
|
|
173
|
+
/** The pre-registered secondary reading of condition (c); never merged into the primary series. */
|
|
174
|
+
export const SECONDARY_C_LIKE_FOR_LIKE_LABEL =
|
|
175
|
+
'secondary — pre-registered 2026-10-23 after the primary window; computed over the same window for information only; a decision on (c) waits for a later window not used to choose it';
|
|
176
|
+
|
|
177
|
+
const ms = (ts: string): number => Date.parse(ts);
|
|
178
|
+
const REGISTER_EVENTS = new Set(['subagent_start', 'subagent_stop', 'subagent_named']);
|
|
179
|
+
|
|
180
|
+
function isSpawn(l: LedgerLine): boolean {
|
|
181
|
+
return l.event === 'spawn_guard_shadow' && l.dispatch_kind === 'spawn' && (l.tool === 'Agent' || l.tool === 'Task');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function reconstructedReason(r: string): string {
|
|
185
|
+
return r === 'policy_epoch_changed' ? 'epoch_changed' : r;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function reconstructSpawnVerdicts(input: ReconstructionInputs): ReconstructedSpawn[] {
|
|
189
|
+
const ttl = input.cache_ttl_ms ?? HOST_DEFAULT_CACHE_TTL_MS;
|
|
190
|
+
const joinWindow = input.session_join_window_ms ?? 10_000;
|
|
191
|
+
const windowStartMs = input.window_start ? ms(input.window_start) : null;
|
|
192
|
+
const lines = [...input.lines].filter((l) => Number.isFinite(ms(l.ts))).sort((a, b) => ms(a.ts) - ms(b.ts));
|
|
193
|
+
const out: ReconstructedSpawn[] = [];
|
|
194
|
+
for (const sp of lines.filter(isSpawn)) {
|
|
195
|
+
const spMs = ms(sp.ts);
|
|
196
|
+
if (windowStartMs !== null && spMs < windowStartMs) continue;
|
|
197
|
+
const type = typeof sp.agent_type === 'string' && sp.agent_type ? sp.agent_type : null;
|
|
198
|
+
const missing: string[] = [];
|
|
199
|
+
if (type === null) missing.push('agent_type');
|
|
200
|
+
let session = typeof sp.session_id === 'string' && sp.session_id ? sp.session_id : null;
|
|
201
|
+
if (session === null && type !== null) {
|
|
202
|
+
const start = lines.find((l) => l.event === 'subagent_start' && l.agent_type === type && ms(l.ts) >= spMs && ms(l.ts) - spMs <= joinWindow);
|
|
203
|
+
session = typeof start?.session_id === 'string' && start.session_id ? start.session_id : null;
|
|
204
|
+
}
|
|
205
|
+
if (session === null) missing.push('session_id');
|
|
206
|
+
for (const f of ['cwd_key', 'head', 'epoch'] as const) if (sp[f] === null || sp[f] === undefined) missing.push(`spawn_${f}`);
|
|
207
|
+
if (missing.length > 0) {
|
|
208
|
+
out.push({ ts: sp.ts, agent_type: type, basis: 'reconstructed', verdict: 'incomplete_join', missing });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const before = lines.filter((l) => l.session_id === session && ms(l.ts) < spMs);
|
|
212
|
+
const context: ReuseContext = {
|
|
213
|
+
domain: type!,
|
|
214
|
+
now_ms: spMs,
|
|
215
|
+
observed_fresh_cold_start_tokens: input.fresh_cold_start_by_type[type!] ?? null,
|
|
216
|
+
worktree: sp.cwd_key!,
|
|
217
|
+
branch_head: sp.head!,
|
|
218
|
+
skill_set_signature: 'unrecorded',
|
|
219
|
+
package_version: String(sp.epoch),
|
|
220
|
+
};
|
|
221
|
+
const named = new Map<string, string>();
|
|
222
|
+
for (const l of before) if (l.event === 'subagent_named' && l.ref && l.name_key) named.set(l.ref, l.name_key);
|
|
223
|
+
const cutByWindow = windowStartMs !== null && before.some((l) => REGISTER_EVENTS.has(l.event) && ms(l.ts) < windowStartMs);
|
|
224
|
+
const startUnseen = [...named.keys()].some((ref) => !before.some((l) => l.ref === ref && l.event === 'subagent_start'));
|
|
225
|
+
if (cutByWindow || startUnseen) {
|
|
226
|
+
const v = shadowReuseVerdict({ host_resume: true, workers: null, context });
|
|
227
|
+
out.push({ ts: sp.ts, agent_type: type!, basis: 'reconstructed', verdict: 'fresh', name: null, reasons: v.reasons.map(reconstructedReason), candidates: 0 });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const workers: WorkerState[] = [];
|
|
231
|
+
const workerMissing = new Set<string>();
|
|
232
|
+
const knownRefusals = new Set<string>();
|
|
233
|
+
const candidateRefs = [...named.keys()]
|
|
234
|
+
.map((ref) => {
|
|
235
|
+
const legs = before.filter((l) => l.ref === ref && (l.event === 'subagent_start' || l.event === 'subagent_stop'));
|
|
236
|
+
const lastStart = legs.filter((l) => l.event === 'subagent_start').pop();
|
|
237
|
+
const lastStop = legs.filter((l) => l.event === 'subagent_stop').pop();
|
|
238
|
+
return { ref, lastStart, lastStop };
|
|
239
|
+
})
|
|
240
|
+
.filter((c) => c.lastStop !== undefined && (c.lastStart === undefined || ms(c.lastStart.ts) <= ms(c.lastStop.ts)))
|
|
241
|
+
.sort((a, b) => ms(b.lastStop!.ts) - ms(a.lastStop!.ts));
|
|
242
|
+
for (const { ref, lastStart, lastStop } of candidateRefs) {
|
|
243
|
+
const stop = lastStop!;
|
|
244
|
+
const typeField = (l: LedgerLine | undefined): string | null => (typeof l?.agent_type === 'string' && l.agent_type ? l.agent_type : null);
|
|
245
|
+
const wType = typeField(stop) ?? typeField(lastStart);
|
|
246
|
+
const def = wType === null ? undefined : input.definitions[wType];
|
|
247
|
+
const tr = input.transcripts.get(ref);
|
|
248
|
+
const calls = tr?.calls.filter((c) => ms(c.ts) < spMs) ?? [];
|
|
249
|
+
const gaps: string[] = [];
|
|
250
|
+
if (wType === null) gaps.push('worker_agent_type');
|
|
251
|
+
if (calls.length === 0) gaps.push('worker_transcript');
|
|
252
|
+
for (const f of ['cwd_key', 'head', 'epoch'] as const) if (stop[f] === null || stop[f] === undefined) gaps.push(`worker_${f}`);
|
|
253
|
+
if (gaps.length > 0) {
|
|
254
|
+
// Only a worker that could be resumed can leave the join incomplete.
|
|
255
|
+
if (wType !== null && wType !== type) knownRefusals.add('domain_mismatch');
|
|
256
|
+
else if (wType !== null && def?.reuse !== 'continuity') knownRefusals.add('not_continuity');
|
|
257
|
+
else for (const g of gaps) workerMissing.add(g);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
workers.push({
|
|
261
|
+
name: named.get(ref)!,
|
|
262
|
+
domain: wType!,
|
|
263
|
+
reuse: def?.reuse ?? 'never',
|
|
264
|
+
last_request_at_ms: ms(stop.ts),
|
|
265
|
+
cache_ttl_ms: ttl,
|
|
266
|
+
transcript_tokens: calls[calls.length - 1]!.context_tokens,
|
|
267
|
+
capsule_watermark_tokens: watermarkForTier(def?.tier ?? 'inherit'),
|
|
268
|
+
compacted: (tr?.compact_ts ?? []).some((t) => ms(t) < spMs),
|
|
269
|
+
worktree: stop.cwd_key!,
|
|
270
|
+
branch_head: stop.head!,
|
|
271
|
+
skill_set_signature: 'unrecorded',
|
|
272
|
+
package_version: String(stop.epoch),
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (workerMissing.size > 0) {
|
|
276
|
+
out.push({ ts: sp.ts, agent_type: type, basis: 'reconstructed', verdict: 'incomplete_join', missing: [...workerMissing].sort() });
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
const v = workers.length === 0 && knownRefusals.size > 0 ? null : shadowReuseVerdict({ host_resume: true, workers, context });
|
|
280
|
+
if (v !== null && v.verdict === 'would_resume') {
|
|
281
|
+
out.push({ ts: sp.ts, agent_type: type!, basis: 'reconstructed', verdict: 'would_resume', name: v.name, reasons: [], candidates: workers.length });
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const seen = new Set<string>([...(v?.reasons ?? []), ...knownRefusals]);
|
|
285
|
+
const ordered = [...REFUSAL_REASONS.filter((r) => seen.has(r)), ...[...seen].filter((r) => !(REFUSAL_REASONS as readonly string[]).includes(r))];
|
|
286
|
+
out.push({ ts: sp.ts, agent_type: type!, basis: 'reconstructed', verdict: 'fresh', name: null, reasons: ordered.map(reconstructedReason), candidates: workers.length });
|
|
287
|
+
}
|
|
288
|
+
return out;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Median first-call TOTAL input (uncached + cache write + cache read) of fresh runs, per definition — the like-for-like threshold. */
|
|
292
|
+
export function likeForLikeColdStartByType(transcripts: ReadonlyMap<string, WorkerTranscript>, lines: readonly LedgerLine[]): Record<string, number> {
|
|
293
|
+
const typeOf = new Map<string, string>();
|
|
294
|
+
for (const l of lines) if (l.event === 'subagent_start' && l.ref && typeof l.agent_type === 'string' && !typeOf.has(l.ref)) typeOf.set(l.ref, l.agent_type);
|
|
295
|
+
const by: Record<string, number[]> = {};
|
|
296
|
+
for (const [ref, t] of typeOf) {
|
|
297
|
+
const first = transcripts.get(ref)?.calls[0];
|
|
298
|
+
if (first !== undefined) (by[t] ??= []).push(first.input_tokens);
|
|
299
|
+
}
|
|
300
|
+
return Object.fromEntries(Object.entries(by).map(([t, v]) => [t, median(v)]));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Median first-call written-or-uncached tokens of FRESH (ordinal 1) runs, per definition. */
|
|
304
|
+
export function freshColdStartByType(runs: readonly SubagentRun[], lines: readonly LedgerLine[]): Record<string, number> {
|
|
305
|
+
const typeOf = new Map<string, string>();
|
|
306
|
+
for (const l of lines) if (l.event === 'subagent_start' && l.ref && typeof l.agent_type === 'string') typeOf.set(l.ref, l.agent_type);
|
|
307
|
+
const by: Record<string, number[]> = {};
|
|
308
|
+
for (const r of runs) {
|
|
309
|
+
const t = typeOf.get(r.ref);
|
|
310
|
+
if (r.ordinal !== 1 || t === undefined) continue;
|
|
311
|
+
(by[t] ??= []).push(r.first_call_written_or_uncached);
|
|
312
|
+
}
|
|
313
|
+
return Object.fromEntries(Object.entries(by).map(([t, v]) => [t, median(v)]));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface ReconstructionSummary {
|
|
317
|
+
basis: 'reconstructed';
|
|
318
|
+
spawns: number;
|
|
319
|
+
complete: number;
|
|
320
|
+
would_resume: number;
|
|
321
|
+
incomplete_joins: number;
|
|
322
|
+
incomplete_by_field: Record<string, number>;
|
|
323
|
+
window: ShadowWindow;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function summariseReconstruction(spawns: readonly ReconstructedSpawn[]): ReconstructionSummary {
|
|
327
|
+
const incomplete = spawns.filter((s) => s.verdict === 'incomplete_join');
|
|
328
|
+
const byField: Record<string, number> = {};
|
|
329
|
+
for (const s of incomplete) if (s.verdict === 'incomplete_join') for (const f of s.missing) byField[f] = (byField[f] ?? 0) + 1;
|
|
330
|
+
const complete = spawns.flatMap((s): ShadowLine[] =>
|
|
331
|
+
s.verdict === 'incomplete_join'
|
|
332
|
+
? []
|
|
333
|
+
: [{ event: 'spawn_guard_shadow', ts: s.ts, tool: 'Agent', dispatch_kind: 'spawn', reuse: { verdict: s.verdict, name: s.name, reasons: s.reasons } }],
|
|
334
|
+
);
|
|
335
|
+
const window = summariseShadowWindow(complete);
|
|
336
|
+
return {
|
|
337
|
+
basis: 'reconstructed',
|
|
338
|
+
spawns: spawns.length,
|
|
339
|
+
complete: complete.length,
|
|
340
|
+
would_resume: window.would_resume,
|
|
341
|
+
incomplete_joins: incomplete.length,
|
|
342
|
+
incomplete_by_field: sorted(byField),
|
|
343
|
+
window,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function readLedgerLines(ledgerDir: string, sinceIso: string | null = null): LedgerLine[] {
|
|
348
|
+
if (!fs.existsSync(ledgerDir)) return [];
|
|
349
|
+
const out: LedgerLine[] = [];
|
|
350
|
+
for (const f of fs.readdirSync(ledgerDir).filter((n) => n.endsWith('.jsonl')).sort()) {
|
|
351
|
+
for (const line of fs.readFileSync(path.join(ledgerDir, f), 'utf8').split('\n')) {
|
|
352
|
+
if (!line.trim()) continue;
|
|
353
|
+
try {
|
|
354
|
+
const o = JSON.parse(line) as LedgerLine;
|
|
355
|
+
if (typeof o.event === 'string' && typeof o.ts === 'string' && (sinceIso === null || Date.parse(o.ts) >= Date.parse(sinceIso))) out.push(o);
|
|
356
|
+
} catch {
|
|
357
|
+
// A torn line is not a record.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return out;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/** Subagent transcripts (`agent-<agentId>.jsonl`) under `root`, keyed by the ledger's `refFor(agentId)`. */
|
|
365
|
+
export function readWorkerTranscripts(root: string): Map<string, WorkerTranscript> {
|
|
366
|
+
const out = new Map<string, WorkerTranscript>();
|
|
367
|
+
const stack = [root];
|
|
368
|
+
while (stack.length > 0) {
|
|
369
|
+
const dir = stack.pop()!;
|
|
370
|
+
let entries: fs.Dirent[];
|
|
371
|
+
try {
|
|
372
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
373
|
+
} catch {
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
for (const e of entries) {
|
|
377
|
+
const full = path.join(dir, e.name);
|
|
378
|
+
if (e.isDirectory()) stack.push(full);
|
|
379
|
+
const m = /^agent-(.+)\.jsonl$/.exec(e.name);
|
|
380
|
+
if (!e.isFile() || m === null) continue;
|
|
381
|
+
// Streamed records repeat one message id with growing usage; the last one is the call.
|
|
382
|
+
const byId = new Map<string, { ts: string; context_tokens: number; input_tokens: number }>();
|
|
383
|
+
const t: WorkerTranscript = { calls: [], compact_ts: [] };
|
|
384
|
+
for (const line of fs.readFileSync(full, 'utf8').split('\n')) {
|
|
385
|
+
if (!line.trim()) continue;
|
|
386
|
+
let o: Record<string, unknown>;
|
|
387
|
+
try {
|
|
388
|
+
o = JSON.parse(line) as Record<string, unknown>;
|
|
389
|
+
} catch {
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const ts = typeof o['timestamp'] === 'string' ? o['timestamp'] : null;
|
|
393
|
+
if (ts === null) continue;
|
|
394
|
+
if (o['subtype'] === 'compact_boundary') t.compact_ts.push(ts);
|
|
395
|
+
const msg = o['message'] as { id?: string; usage?: Record<string, number> } | undefined;
|
|
396
|
+
if (o['type'] !== 'assistant' || !msg?.usage) continue;
|
|
397
|
+
const u = msg.usage;
|
|
398
|
+
const n = (k: string): number => (typeof u[k] === 'number' ? u[k] : 0);
|
|
399
|
+
const input = n('input_tokens') + n('cache_read_input_tokens') + n('cache_creation_input_tokens');
|
|
400
|
+
byId.set(msg.id ?? ts, { ts, context_tokens: input + n('output_tokens'), input_tokens: input });
|
|
401
|
+
}
|
|
402
|
+
t.calls = [...byId.values()].sort((a, b) => Date.parse(a.ts) - Date.parse(b.ts));
|
|
403
|
+
out.set(refFor(m[1]!), t);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** `reuse:` and `model_tier:` from each `<dir>/<name>.md` frontmatter. */
|
|
410
|
+
export function readDefinitions(dir: string): Record<string, DefinitionFacts> {
|
|
411
|
+
const out: Record<string, DefinitionFacts> = {};
|
|
412
|
+
if (!fs.existsSync(dir)) return out;
|
|
413
|
+
for (const f of fs.readdirSync(dir).filter((n) => n.endsWith('.md'))) {
|
|
414
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(fs.readFileSync(path.join(dir, f), 'utf8'))?.[1] ?? '';
|
|
415
|
+
const reuse = /^reuse:\s*(\S+)/m.exec(fm)?.[1];
|
|
416
|
+
const tier = /^model_tier:\s*(\S+)/m.exec(fm)?.[1];
|
|
417
|
+
out[f.replace(/\.md$/, '')] = {
|
|
418
|
+
reuse: reuse === 'continuity' || reuse === 'never' ? reuse : null,
|
|
419
|
+
tier: tier === 'lite' || tier === 'medium' || tier === 'high' ? tier : 'inherit',
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return out;
|
|
423
|
+
}
|
|
424
|
+
|
|
94
425
|
function readLedger(root: string): ShadowLine[] {
|
|
95
426
|
const dir = path.join(root, LEDGER_DIR);
|
|
96
427
|
if (!fs.existsSync(dir)) return [];
|
|
@@ -108,7 +439,74 @@ function readLedger(root: string): ShadowLine[] {
|
|
|
108
439
|
return out;
|
|
109
440
|
}
|
|
110
441
|
|
|
442
|
+
export interface ReconstructArgs {
|
|
443
|
+
ledgerDirs: string[];
|
|
444
|
+
transcriptRoots: string[];
|
|
445
|
+
definitionsDir: string;
|
|
446
|
+
since: string | null;
|
|
447
|
+
/** Lines after this instant are ignored, so a re-run reads the window that was filed. */
|
|
448
|
+
until?: string | null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function parseReconstructArgs(argv: readonly string[]): ReconstructArgs | null {
|
|
452
|
+
const a: ReconstructArgs = { ledgerDirs: [], transcriptRoots: [], definitionsDir: path.join(process.cwd(), 'src', 'subagents'), since: null };
|
|
453
|
+
for (let i = 0; i < argv.length; i++) {
|
|
454
|
+
const k = argv[i];
|
|
455
|
+
const v = argv[i + 1];
|
|
456
|
+
if (v === undefined) continue;
|
|
457
|
+
if (k === '--ledger-dir') a.ledgerDirs.push(v);
|
|
458
|
+
else if (k === '--transcripts') a.transcriptRoots.push(v);
|
|
459
|
+
else if (k === '--definitions') a.definitionsDir = v;
|
|
460
|
+
else if (k === '--since') a.since = v;
|
|
461
|
+
else if (k === '--until') a.until = v;
|
|
462
|
+
else continue;
|
|
463
|
+
i++;
|
|
464
|
+
}
|
|
465
|
+
return a.ledgerDirs.length > 0 ? a : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function reconstructFromDisk(a: ReconstructArgs): Record<string, unknown> {
|
|
469
|
+
// Read the whole ledger: a register cut by --since must be seen to be reported as unrecorded.
|
|
470
|
+
const untilMs = a.until ? Date.parse(a.until) : null;
|
|
471
|
+
const inWindowEnd = (ts: string): boolean => untilMs === null || Date.parse(ts) <= untilMs;
|
|
472
|
+
const all = a.ledgerDirs.flatMap((d) => readLedgerLines(d)).filter((l) => inWindowEnd(l.ts));
|
|
473
|
+
const sinceMs = a.since === null ? null : Date.parse(a.since);
|
|
474
|
+
const lines = sinceMs === null ? all : all.filter((l) => Date.parse(l.ts) >= sinceMs);
|
|
475
|
+
const transcripts = new Map<string, WorkerTranscript>();
|
|
476
|
+
for (const r of a.transcriptRoots) for (const [k, v] of readWorkerTranscripts(r)) transcripts.set(k, v);
|
|
477
|
+
const records = a.transcriptRoots.flatMap((r) => scanTranscripts({ root: r }).records);
|
|
478
|
+
const starts = a.ledgerDirs.flatMap((d) => readLedgerStarts(d)).filter((s) => (sinceMs === null || Date.parse(s.ts) >= sinceMs) && inWindowEnd(s.ts));
|
|
479
|
+
const runs = segmentLegsIntoRuns(records, starts).runs;
|
|
480
|
+
const fresh = freshColdStartByType(runs, lines);
|
|
481
|
+
const definitions = readDefinitions(a.definitionsDir);
|
|
482
|
+
const base = { lines: all, transcripts, definitions, window_start: a.since };
|
|
483
|
+
const spawns = reconstructSpawnVerdicts({ ...base, fresh_cold_start_by_type: fresh });
|
|
484
|
+
const summary = summariseReconstruction(spawns);
|
|
485
|
+
const likeForLike = likeForLikeColdStartByType(transcripts, lines);
|
|
486
|
+
const secondarySpawns = reconstructSpawnVerdicts({ ...base, fresh_cold_start_by_type: likeForLike });
|
|
487
|
+
return {
|
|
488
|
+
basis: 'reconstructed',
|
|
489
|
+
approximations: RECONSTRUCTION_APPROXIMATIONS,
|
|
490
|
+
observed_fresh_cold_start_by_type: fresh,
|
|
491
|
+
summary,
|
|
492
|
+
flip: flipReadiness(summary.window, { false_positive_rate: null, cost_avoided: null, friction: null }),
|
|
493
|
+
secondary_c_like_for_like: {
|
|
494
|
+
label: SECONDARY_C_LIKE_FOR_LIKE_LABEL,
|
|
495
|
+
condition_c: 'transcript_tokens < median first-call TOTAL input tokens (input + cache_creation + cache_read) of fresh runs of the definition',
|
|
496
|
+
threshold_by_type: likeForLike,
|
|
497
|
+
summary: summariseReconstruction(secondarySpawns),
|
|
498
|
+
spawns: secondarySpawns,
|
|
499
|
+
},
|
|
500
|
+
spawns,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
111
504
|
function main(): number {
|
|
505
|
+
const recon = parseReconstructArgs(process.argv.slice(2));
|
|
506
|
+
if (recon !== null) {
|
|
507
|
+
process.stdout.write(`${JSON.stringify(reconstructFromDisk(recon), null, 2)}\n`);
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
112
510
|
const root = process.argv[2] ?? process.cwd();
|
|
113
511
|
const w = summariseShadowWindow(readLedger(root));
|
|
114
512
|
const r = flipReadiness(w, { false_positive_rate: null, cost_avoided: null, friction: null });
|
|
@@ -450,7 +450,7 @@ export const settingsSchema = z.object({
|
|
|
450
450
|
egress: z.object({
|
|
451
451
|
placeholders: z.object({
|
|
452
452
|
mode: z.enum(['off', 'observe', 'protect', 'strict']).default('off').describe(
|
|
453
|
-
'Dispatcher egress stage (docs/contracts/egress-placeholders-v1.md). off (default) = nothing. observe = detect and count listed values in prompts and tool bodies, types and counts only, and change nothing a hook concern sees. protect = also give every recording and advisory concern a copy with each value replaced by a placeholder token, so local stores keep tokens; guards still see the original. strict = as protect until the host rewrite exists. No mode yet changes what reaches the model; the rule table holds credential
|
|
453
|
+
'Dispatcher egress stage (docs/contracts/egress-placeholders-v1.md). off (default) = nothing. observe = detect and count listed values in prompts and tool bodies, types and counts only, and change nothing a hook concern sees. protect = also give every recording and advisory concern a copy with each value replaced by a placeholder token, so local stores keep tokens; guards still see the original. strict = as protect until the host rewrite exists. No mode yet changes what reaches the model; the rule table holds credential rows and the validated identifier rows. Project layers only.',
|
|
454
454
|
),
|
|
455
455
|
}).default({}),
|
|
456
456
|
}).default({}),
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { createRequire as __acCreateRequire } from 'node:module'; const require = globalThis.require ?? __acCreateRequire(import.meta.url);
|
|
2
|
-
|
|
3
|
-
// src/install/detect.ts
|
|
4
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
5
|
-
import { join, resolve } from "node:path";
|
|
6
|
-
var SCOPE_DETECT_MANIFESTS = [
|
|
7
|
-
"package.json",
|
|
8
|
-
"composer.json",
|
|
9
|
-
"pyproject.toml",
|
|
10
|
-
"Cargo.toml",
|
|
11
|
-
"go.mod",
|
|
12
|
-
"Gemfile"
|
|
13
|
-
];
|
|
14
|
-
var SCOPE_DETECT_AI_DIRS = [
|
|
15
|
-
".claude",
|
|
16
|
-
".cursor",
|
|
17
|
-
".windsurf",
|
|
18
|
-
".augment",
|
|
19
|
-
".clinerules",
|
|
20
|
-
".copilot",
|
|
21
|
-
".gemini",
|
|
22
|
-
".codex",
|
|
23
|
-
".aider",
|
|
24
|
-
".continue",
|
|
25
|
-
".roo",
|
|
26
|
-
".kilocode"
|
|
27
|
-
];
|
|
28
|
-
function detectToolPresence(root) {
|
|
29
|
-
return {
|
|
30
|
-
augment: isDir(join(root, ".augment")),
|
|
31
|
-
claude: isDir(join(root, ".claude")),
|
|
32
|
-
cursor: isDir(join(root, ".cursor")),
|
|
33
|
-
cline: isDir(join(root, ".clinerules")),
|
|
34
|
-
windsurf: isDir(join(root, ".windsurf")) || existsSync(join(root, ".windsurfrules")),
|
|
35
|
-
agentsMd: existsSync(join(root, "AGENTS.md"))
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function isDir(path) {
|
|
39
|
-
try {
|
|
40
|
-
return statSync(path).isDirectory();
|
|
41
|
-
} catch {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export {
|
|
47
|
-
SCOPE_DETECT_MANIFESTS,
|
|
48
|
-
SCOPE_DETECT_AI_DIRS,
|
|
49
|
-
detectToolPresence
|
|
50
|
-
};
|
|
File without changes
|