@event4u/agent-config 9.32.0 → 9.34.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/CHANGELOG.md +85 -72
- package/dist/agent-src/commands/agent-handoff.md +15 -3
- package/dist/agent-src/commands/review/routing.md +3 -4
- package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
- package/dist/agent-src/rules/delegation-policy.md +6 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
- package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
- package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
- package/dist/agent-src/skills/docker/SKILL.md +11 -0
- package/dist/agent-src/skills/laravel/SKILL.md +11 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
- package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
- package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
- package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
- package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/dist/cli/registry.js +4 -4
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
- package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
- package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
- package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
- package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-REMEZSVP.js} +1660 -146
- package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
- package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
- package/dist/cli-delegate/cmd_conformance.js +256 -226
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +21 -28
- package/dist/cli-delegate/cmd_memory_get.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +20 -5
- package/dist/cli-delegate/cmd_settings_get.js +4 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_settings_set.js +5 -3
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +17 -17
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +2 -2
- package/dist/discovery/dormancy-report.md +17 -0
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +3 -3
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +3 -3
- package/dist/hooks/dispatch.js +114 -7
- package/dist/install/install.mjs +7 -4
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +3755 -1168
- package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
- package/package.json +2 -2
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
- package/src/agent-src/scripts/roadmap_gates.ts +107 -3
- package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
- package/src/config/cost-parity-budget.json +167 -0
- package/src/config/gate-coverage.yml +44 -5
- package/src/config/quorum-attendance-budget.json +39 -0
- package/src/scripts/_cli/cmd_doctor.ts +69 -1
- package/src/scripts/_cli/cmd_session_recycle.ts +25 -4
- package/src/scripts/_dispatch.bash +5 -3
- package/src/scripts/_lib/env_kill_switch.ts +37 -0
- package/src/scripts/_lib/orchestration_record.ts +45 -0
- package/src/scripts/_lib/security_lint.ts +18 -4
- package/src/scripts/_lib/self_repair.ts +60 -0
- package/src/scripts/_lib/self_repair_store.ts +65 -5
- package/src/scripts/_lib/subagent_capsule.ts +78 -1
- package/src/scripts/ai_council/clients.ts +31 -0
- package/src/scripts/ai_council/events_log.ts +130 -8
- package/src/scripts/ai_council/orchestrator.ts +50 -3
- package/src/scripts/ai_council/pricing.ts +48 -1
- package/src/scripts/ai_council/prompts.ts +165 -3
- package/src/scripts/ai_council/quorum.ts +30 -0
- package/src/scripts/ai_council/session.ts +11 -0
- package/src/scripts/ai_team/review_gate.ts +2 -2
- package/src/scripts/ask_transport.ts +20 -1
- package/src/scripts/build_discovery_manifest.ts +165 -0
- package/src/scripts/cache_realization_report.ts +38 -0
- package/src/scripts/check_claims.ts +51 -2
- package/src/scripts/check_release_highlights.ts +7 -1
- package/src/scripts/ci_time_ratio.ts +3 -1
- package/src/scripts/conformance_scan.ts +105 -0
- package/src/scripts/cost/track.mjs +39 -0
- package/src/scripts/cost_summary.ts +104 -1
- package/src/scripts/council_cli.ts +131 -13
- package/src/scripts/discovery_graph.ts +201 -27
- package/src/scripts/dispatch_economy_report.ts +5 -2
- package/src/scripts/dispatch_r2_reviewer.ts +102 -1
- package/src/scripts/install.ts +20 -6
- package/src/scripts/lint_budget_ownership.ts +81 -0
- package/src/scripts/lint_handoffs.ts +92 -2
- package/src/scripts/lint_hedge_words.ts +317 -0
- package/src/scripts/lint_mcp_config_security.ts +1 -1
- package/src/scripts/lint_skill_frontmatter_safety.ts +56 -2
- package/src/scripts/lint_workflow_paths.ts +384 -0
- package/src/scripts/preamble_byte_census.ts +9 -1
- package/src/scripts/report_carrier_divergence.ts +10 -0
- package/src/scripts/report_conformance_funnel.ts +40 -0
- package/src/scripts/schemas/subagent.schema.json +3 -2
- package/src/scripts/self_repair_cli.ts +26 -1
- package/src/scripts/skill_linter.ts +58 -2
|
@@ -48,10 +48,20 @@ import type { ExternalAIClient } from './ai_council/clients.js';
|
|
|
48
48
|
import { buildOrchestrationLine } from './_lib/orchestration_record.js';
|
|
49
49
|
import { DEFAULT_DIR } from './orchestration_record.js';
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Placeholder written into `AskResult.model` when the member reported no id.
|
|
53
|
+
* Named so the audit path can recognise and refuse it rather than passing a
|
|
54
|
+
* sentinel off as a real model id.
|
|
55
|
+
*/
|
|
56
|
+
export const UNKNOWN_MODEL = 'unknown';
|
|
57
|
+
|
|
51
58
|
export interface AskResult {
|
|
52
59
|
answer: string;
|
|
53
60
|
member: string;
|
|
61
|
+
/** The REQUESTED model id — what the member was configured to call. */
|
|
54
62
|
model: string;
|
|
63
|
+
/** The model the provider reported serving; `''` when it reports none. */
|
|
64
|
+
model_served: string;
|
|
55
65
|
input_tokens: number;
|
|
56
66
|
output_tokens: number;
|
|
57
67
|
latency_ms: number;
|
|
@@ -94,6 +104,14 @@ function recordAskLine(auditDir: string, result: AskResult | null, member: strin
|
|
|
94
104
|
dispatch_tokens: result === null ? null : result.input_tokens + result.output_tokens,
|
|
95
105
|
wall_clock_ms: result === null ? 0 : Math.max(0, Math.round(result.latency_ms)),
|
|
96
106
|
agent_combo: [member || 'none'],
|
|
107
|
+
// `AskResult.model` substitutes an `'unknown'` SENTINEL when the member
|
|
108
|
+
// reported no id (below). That sentinel must never reach the audit line
|
|
109
|
+
// as a model_requested: `modelDivergent` treats any non-empty string as
|
|
110
|
+
// a real id, so it would fabricate a `true` on a line whose entire
|
|
111
|
+
// purpose is making real substitutions credible. null keeps the
|
|
112
|
+
// three-valued semantics — and the field's own "id only" contract.
|
|
113
|
+
model_requested: result === null || result.model === UNKNOWN_MODEL ? null : result.model,
|
|
114
|
+
model_served: result === null ? null : result.model_served,
|
|
97
115
|
origin: 'dispatch-economy-2026',
|
|
98
116
|
ts,
|
|
99
117
|
id,
|
|
@@ -145,7 +163,8 @@ export async function askOnce(prompt: string, opts: AskOptions = {}): Promise<As
|
|
|
145
163
|
{
|
|
146
164
|
answer: text,
|
|
147
165
|
member: provider,
|
|
148
|
-
model: r.model ??
|
|
166
|
+
model: r.model ?? UNKNOWN_MODEL,
|
|
167
|
+
model_served: r.model_served ?? '',
|
|
149
168
|
input_tokens: r.input_tokens ?? 0,
|
|
150
169
|
output_tokens: r.output_tokens ?? 0,
|
|
151
170
|
latency_ms: r.latency_ms ?? 0,
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
*
|
|
39
39
|
* Schema: docs/contracts/discovery-manifest.schema.json
|
|
40
40
|
*/
|
|
41
|
+
import * as child_process from 'node:child_process';
|
|
41
42
|
import * as crypto from 'node:crypto';
|
|
42
43
|
import * as fs from 'node:fs';
|
|
43
44
|
import * as path from 'node:path';
|
|
@@ -83,6 +84,7 @@ interface ModuleConfig {
|
|
|
83
84
|
DEFAULT_DEPRECATION_REPORT: string;
|
|
84
85
|
DEFAULT_TRUST_REPORT: string;
|
|
85
86
|
DEFAULT_ORPHAN_REPORT: string;
|
|
87
|
+
DEFAULT_DORMANCY_REPORT: string;
|
|
86
88
|
DEFAULT_WORKSPACES_JSON: string;
|
|
87
89
|
DEFAULT_PACKS_JSON: string;
|
|
88
90
|
// Patchable seams (Python monkeypatches these module attributes).
|
|
@@ -101,6 +103,7 @@ function _deriveConfig(root: string): ModuleConfig {
|
|
|
101
103
|
DEFAULT_DEPRECATION_REPORT: path.join(disc, 'deprecation-report.md'),
|
|
102
104
|
DEFAULT_TRUST_REPORT: path.join(disc, 'trust-report.md'),
|
|
103
105
|
DEFAULT_ORPHAN_REPORT: path.join(disc, 'orphan-report.md'),
|
|
106
|
+
DEFAULT_DORMANCY_REPORT: path.join(disc, 'dormancy-report.md'),
|
|
104
107
|
DEFAULT_WORKSPACES_JSON: path.join(disc, 'workspaces.json'),
|
|
105
108
|
DEFAULT_PACKS_JSON: path.join(disc, 'packs.json'),
|
|
106
109
|
artefact_roots: _artefact_roots,
|
|
@@ -937,6 +940,156 @@ function _deprecation_report(manifest: JsonObject): string {
|
|
|
937
940
|
return lines.join('\n') + '\n';
|
|
938
941
|
}
|
|
939
942
|
|
|
943
|
+
/** Dormancy window, in months. `docs/governance.md` § Skill lifecycle policy. */
|
|
944
|
+
const _DORMANCY_MONTHS = 6;
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* What the dormancy report could actually establish from git in this checkout.
|
|
948
|
+
*
|
|
949
|
+
* `ok: false` is a first-class outcome, not an error: a shallow clone or a
|
|
950
|
+
* repository younger than the window cannot distinguish "untouched for six
|
|
951
|
+
* months" from "history truncated three months ago", and CI clones are shallow
|
|
952
|
+
* by default. The report NAMES that instead of emitting a list built on it.
|
|
953
|
+
*/
|
|
954
|
+
export interface DormancyProbe {
|
|
955
|
+
ok: boolean;
|
|
956
|
+
/** Why the signal is unavailable — set iff `ok` is false. */
|
|
957
|
+
reason?: string;
|
|
958
|
+
/** Repo-relative paths with at least one commit inside the window. */
|
|
959
|
+
touched: Set<string>;
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function _git(root: string, args: readonly string[]): string | null {
|
|
963
|
+
try {
|
|
964
|
+
return child_process.execFileSync('git', [...args], {
|
|
965
|
+
cwd: root,
|
|
966
|
+
encoding: 'utf-8',
|
|
967
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
968
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
969
|
+
});
|
|
970
|
+
} catch {
|
|
971
|
+
return null;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
/**
|
|
976
|
+
* Probe git once for the whole estate: one bounded `--since` walk, exactly the
|
|
977
|
+
* command `governance.md` names, rather than one `git log` per artefact.
|
|
978
|
+
*/
|
|
979
|
+
function _dormancy_probe(root: string, now: Date = new Date()): DormancyProbe {
|
|
980
|
+
const since = `${String(_DORMANCY_MONTHS)} months ago`;
|
|
981
|
+
if (_git(root, ['rev-parse', '--git-dir']) === null) {
|
|
982
|
+
return { ok: false, reason: 'not a git checkout — commit history is unavailable', touched: new Set() };
|
|
983
|
+
}
|
|
984
|
+
if ((_git(root, ['rev-parse', '--is-shallow-repository']) ?? '').trim() === 'true') {
|
|
985
|
+
return {
|
|
986
|
+
ok: false,
|
|
987
|
+
reason:
|
|
988
|
+
'shallow clone — a truncated history is indistinguishable from a dormant artefact. ' +
|
|
989
|
+
'Re-run after `git fetch --unshallow`.',
|
|
990
|
+
touched: new Set(),
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const oldest = (_git(root, ['log', '--reverse', '--format=%cI', '--max-count=1']) ?? '').trim();
|
|
994
|
+
if (oldest !== '') {
|
|
995
|
+
const cutoff = new Date(now);
|
|
996
|
+
cutoff.setMonth(cutoff.getMonth() - _DORMANCY_MONTHS);
|
|
997
|
+
if (new Date(oldest) > cutoff) {
|
|
998
|
+
return {
|
|
999
|
+
ok: false,
|
|
1000
|
+
reason:
|
|
1001
|
+
`repository history starts ${oldest.slice(0, 10)}, inside the ` +
|
|
1002
|
+
`${String(_DORMANCY_MONTHS)}-month window — every artefact would read as dormant`,
|
|
1003
|
+
touched: new Set(),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const out = _git(root, ['log', `--since=${since}`, '--name-only', '--format=']);
|
|
1008
|
+
if (out === null) {
|
|
1009
|
+
return { ok: false, reason: 'git log failed', touched: new Set() };
|
|
1010
|
+
}
|
|
1011
|
+
const touched = new Set(out.split('\n').map((l) => l.trim()).filter((l) => l !== ''));
|
|
1012
|
+
return { ok: true, touched };
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* The unit dormancy is measured over. A skill is a directory (governance names
|
|
1017
|
+
* `git log -- src/skills/<name>/`), so a commit touching its `reference.md`
|
|
1018
|
+
* keeps the skill alive; every other artefact is its own file.
|
|
1019
|
+
*/
|
|
1020
|
+
function _dormancy_unit(artefactPath: string): string {
|
|
1021
|
+
return path.basename(artefactPath) === 'SKILL.md' ? path.dirname(artefactPath) : artefactPath;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/**
|
|
1025
|
+
* Artefacts with no commit inside the dormancy window.
|
|
1026
|
+
*
|
|
1027
|
+
* Report-only by construction: `governance.md` § Skill lifecycle policy says
|
|
1028
|
+
* dormancy triggers review, never auto-deprecation — and a finished artefact is
|
|
1029
|
+
* indistinguishable from an abandoned one from commit dates, so the
|
|
1030
|
+
* false-positive class is not empty and no argument makes it so.
|
|
1031
|
+
*/
|
|
1032
|
+
function _dormant_artefacts(manifest: JsonObject, probe: DormancyProbe): JsonObject[] {
|
|
1033
|
+
if (!probe.ok) {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
const alive = new Set<string>();
|
|
1037
|
+
for (const p of probe.touched) {
|
|
1038
|
+
alive.add(p);
|
|
1039
|
+
// A touched file keeps its containing directory-shaped artefact alive.
|
|
1040
|
+
for (let d = path.dirname(p); d !== '.' && d !== path.sep; d = path.dirname(d)) {
|
|
1041
|
+
alive.add(d);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
const out: JsonObject[] = [];
|
|
1045
|
+
for (const a of manifest['artefacts'] as JsonObject[]) {
|
|
1046
|
+
const unit = _dormancy_unit(a['path'] as string);
|
|
1047
|
+
if (!alive.has(unit)) {
|
|
1048
|
+
out.push({ path: a['path'], unit, category: a['category'] });
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
out.sort((a, b) => _cmpStr(a['path'] as string, b['path'] as string));
|
|
1052
|
+
return out;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** The dormancy section of the discovery report family (governance.md § Skill lifecycle policy). */
|
|
1056
|
+
function _dormancy_report(manifest: JsonObject, probe: DormancyProbe): string {
|
|
1057
|
+
const lines = ['# Discovery — Dormancy Report', ''];
|
|
1058
|
+
lines.push(`- Generated: \`${String(manifest['generated_at'])}\``);
|
|
1059
|
+
lines.push(`- Window: **${String(_DORMANCY_MONTHS)} months** without a commit`);
|
|
1060
|
+
lines.push('');
|
|
1061
|
+
lines.push('> Dormancy triggers **review, never auto-deprecation**');
|
|
1062
|
+
lines.push('> (`docs/governance.md` § Skill lifecycle policy). A finished artefact and an');
|
|
1063
|
+
lines.push('> abandoned one look identical from commit dates, so this is a prompt to ask');
|
|
1064
|
+
lines.push('> "still earning its slot?" — never a removal list, and never a gate.');
|
|
1065
|
+
lines.push('');
|
|
1066
|
+
if (!probe.ok) {
|
|
1067
|
+
lines.push('## Signal unavailable');
|
|
1068
|
+
lines.push('');
|
|
1069
|
+
lines.push(`**No dormancy list is published**: ${String(probe.reason)}`);
|
|
1070
|
+
lines.push('');
|
|
1071
|
+
lines.push('An empty section here would read as "nothing is dormant", which is a');
|
|
1072
|
+
lines.push('different claim from "the signal could not be computed".');
|
|
1073
|
+
lines.push('');
|
|
1074
|
+
return lines.join('\n') + '\n';
|
|
1075
|
+
}
|
|
1076
|
+
const items = _dormant_artefacts(manifest, probe);
|
|
1077
|
+
lines.push(`- Dormant artefacts: **${items.length}**`);
|
|
1078
|
+
lines.push('');
|
|
1079
|
+
if (items.length === 0) {
|
|
1080
|
+
lines.push('_None. Every artefact was touched inside the window._');
|
|
1081
|
+
lines.push('');
|
|
1082
|
+
return lines.join('\n') + '\n';
|
|
1083
|
+
}
|
|
1084
|
+
lines.push('| Path | Unit | Category |');
|
|
1085
|
+
lines.push('|---|---|---|');
|
|
1086
|
+
for (const a of items) {
|
|
1087
|
+
lines.push(`| \`${String(a['path'])}\` | \`${String(a['unit'])}\` | ${String(a['category'])} |`);
|
|
1088
|
+
}
|
|
1089
|
+
lines.push('');
|
|
1090
|
+
return lines.join('\n') + '\n';
|
|
1091
|
+
}
|
|
1092
|
+
|
|
940
1093
|
/** Trust-level breakdown by workspace + human-review sanity flag. Mirrors `_trust_report`. */
|
|
941
1094
|
function _trust_report(manifest: JsonObject): string {
|
|
942
1095
|
const byWs = new Map<string, JsonObject>();
|
|
@@ -1195,6 +1348,7 @@ interface ParsedArgs {
|
|
|
1195
1348
|
deprecation_report: string;
|
|
1196
1349
|
trust_report: string;
|
|
1197
1350
|
orphan_report: string;
|
|
1351
|
+
dormancy_report: string;
|
|
1198
1352
|
workspaces_json: string;
|
|
1199
1353
|
packs_json: string;
|
|
1200
1354
|
strict: boolean;
|
|
@@ -1231,6 +1385,7 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
1231
1385
|
deprecation_report: _config.DEFAULT_DEPRECATION_REPORT,
|
|
1232
1386
|
trust_report: _config.DEFAULT_TRUST_REPORT,
|
|
1233
1387
|
orphan_report: _config.DEFAULT_ORPHAN_REPORT,
|
|
1388
|
+
dormancy_report: _config.DEFAULT_DORMANCY_REPORT,
|
|
1234
1389
|
workspaces_json: _config.DEFAULT_WORKSPACES_JSON,
|
|
1235
1390
|
packs_json: _config.DEFAULT_PACKS_JSON,
|
|
1236
1391
|
strict: false,
|
|
@@ -1242,6 +1397,7 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
1242
1397
|
' [--deprecation-report DEPRECATION_REPORT]\n' +
|
|
1243
1398
|
' [--trust-report TRUST_REPORT]\n' +
|
|
1244
1399
|
' [--orphan-report ORPHAN_REPORT]\n' +
|
|
1400
|
+
' [--dormancy-report DORMANCY_REPORT]\n' +
|
|
1245
1401
|
' [--workspaces-json WORKSPACES_JSON]\n' +
|
|
1246
1402
|
' [--packs-json PACKS_JSON] [--strict]\n' +
|
|
1247
1403
|
' [--quiet]\n';
|
|
@@ -1252,6 +1408,7 @@ function parse_args(argv: readonly string[]): ParsedArgs {
|
|
|
1252
1408
|
'--deprecation-report': 'deprecation_report',
|
|
1253
1409
|
'--trust-report': 'trust_report',
|
|
1254
1410
|
'--orphan-report': 'orphan_report',
|
|
1411
|
+
'--dormancy-report': 'dormancy_report',
|
|
1255
1412
|
'--workspaces-json': 'workspaces_json',
|
|
1256
1413
|
'--packs-json': 'packs_json',
|
|
1257
1414
|
};
|
|
@@ -1324,6 +1481,10 @@ function main(argv: readonly string[]): number {
|
|
|
1324
1481
|
_writeFileMkdir(args.deprecation_report, _deprecation_report(manifest));
|
|
1325
1482
|
_writeFileMkdir(args.trust_report, _trust_report(manifest));
|
|
1326
1483
|
_writeFileMkdir(args.orphan_report, _orphan_report(manifest));
|
|
1484
|
+
_writeFileMkdir(
|
|
1485
|
+
args.dormancy_report,
|
|
1486
|
+
_dormancy_report(manifest, _dormancy_probe(_config.ROOT)),
|
|
1487
|
+
);
|
|
1327
1488
|
// Phase 5 sub-views.
|
|
1328
1489
|
_writeFileMkdir(
|
|
1329
1490
|
args.workspaces_json,
|
|
@@ -1425,6 +1586,10 @@ export {
|
|
|
1425
1586
|
_trust_report,
|
|
1426
1587
|
_orphan_artefacts,
|
|
1427
1588
|
_orphan_report,
|
|
1589
|
+
_dormancy_probe,
|
|
1590
|
+
_dormancy_unit,
|
|
1591
|
+
_dormant_artefacts,
|
|
1592
|
+
_dormancy_report,
|
|
1428
1593
|
_workspaces_view,
|
|
1429
1594
|
_packs_view,
|
|
1430
1595
|
_summary,
|
|
@@ -88,6 +88,25 @@ export interface ColdStartStats {
|
|
|
88
88
|
first_call_cache_read_share: number;
|
|
89
89
|
/** Σ(first-call cache_creation_input_tokens) / Σ(all subagent cache_creation_input_tokens) — the C-1 metric. */
|
|
90
90
|
cold_start_share_of_write_volume: number;
|
|
91
|
+
/**
|
|
92
|
+
* The write-share SIGNATURE across ALL subagent records (not just first
|
|
93
|
+
* calls): how the billable input splits between cache writes, cache reads,
|
|
94
|
+
* and uncached input.
|
|
95
|
+
*
|
|
96
|
+
* The three shares are over the same denominator and sum to 1 (up to
|
|
97
|
+
* float error) whenever any billable input exists, which is what makes it
|
|
98
|
+
* a signature rather than three unrelated ratios — a workload that is
|
|
99
|
+
* mostly `write` is paying the cache premium without collecting the read
|
|
100
|
+
* discount, and that is invisible in `cold_start_share_of_write_volume`,
|
|
101
|
+
* which only says how much of the writing happened on a first call.
|
|
102
|
+
*
|
|
103
|
+
* All three are 0 when there is no billable input at all. That is the
|
|
104
|
+
* empty-corpus reading, NOT "a perfectly uncached workload" — read it
|
|
105
|
+
* beside `legs`.
|
|
106
|
+
*/
|
|
107
|
+
write_share_of_billable: number;
|
|
108
|
+
read_share_of_billable: number;
|
|
109
|
+
uncached_share_of_billable: number;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
112
|
/**
|
|
@@ -132,12 +151,22 @@ export function computeColdStarts(records: readonly TranscriptRecord[]): ColdSta
|
|
|
132
151
|
const sumFirstCallBillable = firstCalls.reduce((s, r) => s + billableInputTokens(r.usage), 0);
|
|
133
152
|
const sumFirstCallWrite = firstCalls.reduce((s, r) => s + r.usage.cache_creation_input_tokens, 0);
|
|
134
153
|
|
|
154
|
+
// Signature over ALL subagent records — the shape of the workload, where
|
|
155
|
+
// the metrics above describe only its first calls.
|
|
156
|
+
const sumBillable = subagentRecords.reduce((s, r) => s + billableInputTokens(r.usage), 0);
|
|
157
|
+
const sumWrite = totalWriteVolume;
|
|
158
|
+
const sumRead = subagentRecords.reduce((s, r) => s + r.usage.cache_read_input_tokens, 0);
|
|
159
|
+
const sumUncached = subagentRecords.reduce((s, r) => s + r.usage.input_tokens, 0);
|
|
160
|
+
|
|
135
161
|
return {
|
|
136
162
|
legs: firstCalls.length,
|
|
137
163
|
median_first_call_written_or_uncached: median(writtenOrUncached),
|
|
138
164
|
mean_first_call_written_or_uncached: mean(writtenOrUncached),
|
|
139
165
|
first_call_cache_read_share: sumFirstCallBillable > 0 ? sumFirstCallRead / sumFirstCallBillable : 0,
|
|
140
166
|
cold_start_share_of_write_volume: totalWriteVolume > 0 ? sumFirstCallWrite / totalWriteVolume : 0,
|
|
167
|
+
write_share_of_billable: sumBillable > 0 ? sumWrite / sumBillable : 0,
|
|
168
|
+
read_share_of_billable: sumBillable > 0 ? sumRead / sumBillable : 0,
|
|
169
|
+
uncached_share_of_billable: sumBillable > 0 ? sumUncached / sumBillable : 0,
|
|
141
170
|
};
|
|
142
171
|
}
|
|
143
172
|
|
|
@@ -737,6 +766,15 @@ export function renderText(r: Report): string {
|
|
|
737
766
|
);
|
|
738
767
|
out.push(` first_call_cache_read_share=${pct(cs.first_call_cache_read_share)}`);
|
|
739
768
|
out.push(` cold_start_share_of_subagent_write_volume=${pct(cs.cold_start_share_of_write_volume)}`);
|
|
769
|
+
// Write-share signature over ALL subagent records: the three shares sum to
|
|
770
|
+
// 100% and say where the billable input actually went. A write-heavy split
|
|
771
|
+
// is paying the cache premium without collecting the read discount — which
|
|
772
|
+
// the cold-start metrics above cannot show, since they only look at first
|
|
773
|
+
// calls. All-zero means no billable input at all; read it beside legs.
|
|
774
|
+
out.push(
|
|
775
|
+
` write_share=${pct(cs.write_share_of_billable)} read_share=${pct(cs.read_share_of_billable)}` +
|
|
776
|
+
` uncached_share=${pct(cs.uncached_share_of_billable)} (of billable input, all subagent records)`,
|
|
777
|
+
);
|
|
740
778
|
out.push('');
|
|
741
779
|
|
|
742
780
|
out.push(`Per-agent breakdown: ${r.by_agent.length} distinct subagent legs (--format json carries all; top 10 by weighted_input_units below)`);
|
|
@@ -55,7 +55,14 @@ const LEDGER_REL = 'docs/CLAIMS.md';
|
|
|
55
55
|
const SURFACE_ROOTS = ['README.md', 'docs'];
|
|
56
56
|
|
|
57
57
|
const CLAIM_MARKER = /<!--\s*claim:([A-Za-z0-9._-]+)\s*-->/g;
|
|
58
|
-
|
|
58
|
+
// The documented grammar is `https://… (YYYY-MM-DD)` — with a space, in both
|
|
59
|
+
// docs/CLAIMS.md § Evidence-pointer grammar and this file's own header. The
|
|
60
|
+
// original pattern required the stamp to abut the URL (`\S+\(`), so every
|
|
61
|
+
// pointer written the documented way was rejected. That went unnoticed because
|
|
62
|
+
// the form had zero usage until the external research citations were bound;
|
|
63
|
+
// the space is now optional, which accepts both spellings and keeps the
|
|
64
|
+
// enforcement matching the spec rather than the other way round.
|
|
65
|
+
const URL_DATED = /^https?:\/\/\S+\s*\(\d{4}-\d{2}-\d{2}\)\s*$/;
|
|
59
66
|
|
|
60
67
|
/** Witness sweep (P1.2) — marketing-class surfaces where a bare number must
|
|
61
68
|
* not appear as a capability claim. docs/proof.md and docs/comparison.yaml
|
|
@@ -118,6 +125,17 @@ export interface LedgerEntry {
|
|
|
118
125
|
evidence: string;
|
|
119
126
|
status: string;
|
|
120
127
|
last_verified: string;
|
|
128
|
+
/**
|
|
129
|
+
* Optional forward link on a `resolved-null` entry: the id of the claim that
|
|
130
|
+
* reopened the same question by a different mechanism. The retire-never-delete
|
|
131
|
+
* lifecycle keeps the null readable forever, but a reader arriving at a closed
|
|
132
|
+
* question has no way to learn it was later reopened — this is that link.
|
|
133
|
+
*
|
|
134
|
+
* Parsed and gated rather than merely documented: an unparsed field would be a
|
|
135
|
+
* documented claim the code does not honour, which is the defect class this
|
|
136
|
+
* ledger exists to make impossible.
|
|
137
|
+
*/
|
|
138
|
+
superseded_by: string;
|
|
121
139
|
}
|
|
122
140
|
|
|
123
141
|
interface Args {
|
|
@@ -158,6 +176,7 @@ function load_ledger(): Map<string, LedgerEntry> {
|
|
|
158
176
|
evidence: cur.evidence ?? '',
|
|
159
177
|
status: cur.status ?? '',
|
|
160
178
|
last_verified: cur.last_verified ?? '',
|
|
179
|
+
superseded_by: cur.superseded_by ?? '',
|
|
161
180
|
});
|
|
162
181
|
}
|
|
163
182
|
cur = {};
|
|
@@ -170,7 +189,7 @@ function load_ledger(): Map<string, LedgerEntry> {
|
|
|
170
189
|
continue;
|
|
171
190
|
}
|
|
172
191
|
if (!cur.id) continue;
|
|
173
|
-
const field = line.match(/^-\s+(claim|kind|evidence|status|last_verified):\s*(.*)$/);
|
|
192
|
+
const field = line.match(/^-\s+(claim|kind|evidence|status|last_verified|superseded_by):\s*(.*)$/);
|
|
174
193
|
if (field) {
|
|
175
194
|
const key = field[1] as keyof LedgerEntry;
|
|
176
195
|
(cur as Record<string, string>)[key] = (field[2] ?? '').trim();
|
|
@@ -320,6 +339,36 @@ function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
320
339
|
if (un) findings.push({ id: entry.id, file: LEDGER_REL, reason: `backed entry has dangling evidence — ${un}` });
|
|
321
340
|
}
|
|
322
341
|
|
|
342
|
+
// 2b. Successor pointers: a `superseded_by` must name a real ledger entry,
|
|
343
|
+
// and only a closed question can have a successor. The field exists so a
|
|
344
|
+
// reader arriving at a `resolved-null` learns the question was reopened
|
|
345
|
+
// by a different mechanism — a dangling id would send them nowhere, and
|
|
346
|
+
// the field on a live entry would claim a closure that never happened.
|
|
347
|
+
for (const entry of ledger.values()) {
|
|
348
|
+
if (!entry.superseded_by) continue;
|
|
349
|
+
if (entry.status !== 'resolved-null') {
|
|
350
|
+
findings.push({
|
|
351
|
+
id: entry.id,
|
|
352
|
+
file: LEDGER_REL,
|
|
353
|
+
reason: `superseded_by is only meaningful on a resolved-null entry (status: ${entry.status || 'missing'})`,
|
|
354
|
+
});
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (!ledger.has(entry.superseded_by)) {
|
|
358
|
+
findings.push({
|
|
359
|
+
id: entry.id,
|
|
360
|
+
file: LEDGER_REL,
|
|
361
|
+
reason: `superseded_by names claim:${entry.superseded_by}, which is not in the ledger`,
|
|
362
|
+
});
|
|
363
|
+
} else if (entry.superseded_by === entry.id) {
|
|
364
|
+
findings.push({
|
|
365
|
+
id: entry.id,
|
|
366
|
+
file: LEDGER_REL,
|
|
367
|
+
reason: 'superseded_by points at its own entry',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
323
372
|
// 3. Witness sweep (road-to-opt-subagent-harvest P1.2): a QUANTIFIED
|
|
324
373
|
// capability claim on a marketing-class surface must be witnessed — a
|
|
325
374
|
// `<!-- claim:ID -->` marker on the same line, or an explicit
|
|
@@ -114,7 +114,13 @@ export function highlight_contradictions(
|
|
|
114
114
|
return out;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Exported so the advisory-vs-blocking decision is a fixture, not a claim.
|
|
119
|
+
* `stale_draft_labels` firing while the exit code stays 0 is the behaviour the
|
|
120
|
+
* cadence blocker adjudicates; pinning it here makes either branch of that
|
|
121
|
+
* decision a one-line diff with a test that notices.
|
|
122
|
+
*/
|
|
123
|
+
export function main(argv: readonly string[]): number {
|
|
118
124
|
let version: string | null = null;
|
|
119
125
|
let from: string | null = null;
|
|
120
126
|
let to = 'HEAD';
|
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* - Median ratio < 3× across all classes → structural overhead acceptable
|
|
21
21
|
*
|
|
22
22
|
* Output: human-readable table on stdout + JSON to
|
|
23
|
-
* `agents/
|
|
23
|
+
* `agents/reports/ci-time-ratio.json` — the tracked report directory, and
|
|
24
|
+
* the path `DEFAULT_OUT` has always written. This docstring named a
|
|
25
|
+
* different, untracked directory until 2026-08-11; the code never did.
|
|
24
26
|
*
|
|
25
27
|
* Usage:
|
|
26
28
|
* node ci_time_ratio.js --limit 30
|
|
@@ -174,6 +174,79 @@ export interface ScanReport {
|
|
|
174
174
|
rate: RateRecord;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
export type CheckId = Violation["check"];
|
|
178
|
+
|
|
179
|
+
export const CHECK_IDS: readonly CheckId[] = [
|
|
180
|
+
"language-pin",
|
|
181
|
+
"git-authorization",
|
|
182
|
+
"vacuous-evidence",
|
|
183
|
+
"evidence-steering",
|
|
184
|
+
];
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* What each mechanised check actually detects, in the words a reader needs in
|
|
188
|
+
* order to decide whether a hit is a defect or a false read. `--why` prints
|
|
189
|
+
* this ALONGSIDE the hits rather than instead of them: a count with no
|
|
190
|
+
* definition is the shape that let a figure like "109 divergent pairs" travel
|
|
191
|
+
* through five reviews unchallenged.
|
|
192
|
+
*/
|
|
193
|
+
export const CHECK_MEANINGS: Record<CheckId, string> = {
|
|
194
|
+
"language-pin":
|
|
195
|
+
"An assistant turn whose user-visible prose is not in the language that " +
|
|
196
|
+
"turn's own pin named. Carries provenance (turns_since_prompt, " +
|
|
197
|
+
"compaction_since_prompt) so a pin that was ABSENT because compaction " +
|
|
198
|
+
"removed it stays distinguishable from one that was present and ignored.",
|
|
199
|
+
"git-authorization":
|
|
200
|
+
"A gated git operation performed in a session where no authorization for " +
|
|
201
|
+
"that operation was observed. Authorization is per-operation and never " +
|
|
202
|
+
"carries from one to the next.",
|
|
203
|
+
"vacuous-evidence":
|
|
204
|
+
"A CI poll read as settled without ever observing an in-flight run — the " +
|
|
205
|
+
"poll gap that reports a green settle over a window in which nothing was " +
|
|
206
|
+
"pending yet.",
|
|
207
|
+
"evidence-steering":
|
|
208
|
+
"A self-commissioned evaluation whose prompt pre-loaded its own verdict, " +
|
|
209
|
+
"or a second self-scoped evaluation of the same subject within one turn.",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* `--why <id>` — trace one conformance id: what it detects, whether it fired in
|
|
214
|
+
* this window, and every hit with its session and detail.
|
|
215
|
+
*
|
|
216
|
+
* A check that did not fire prints as *did not fire*, never as silence. Zero is
|
|
217
|
+
* a real answer, and the difference between "clean" and "not measured" is the
|
|
218
|
+
* whole reason to print it.
|
|
219
|
+
*/
|
|
220
|
+
export function renderWhy(report: ScanReport, id: CheckId): string {
|
|
221
|
+
const hits = report.per_session.flatMap((s) => s.violations.filter((v) => v.check === id));
|
|
222
|
+
const lines: string[] = [
|
|
223
|
+
`conformance:why · ${id} · ${report.sessions} session(s) · ${report.store}`,
|
|
224
|
+
"",
|
|
225
|
+
" What it detects:",
|
|
226
|
+
` ${CHECK_MEANINGS[id]}`,
|
|
227
|
+
"",
|
|
228
|
+
];
|
|
229
|
+
if (hits.length === 0) {
|
|
230
|
+
lines.push(` Did NOT fire in this window (0 hits over ${report.sessions} session(s)).`);
|
|
231
|
+
lines.push(" A measured zero, not an unmeasured one — the check ran.");
|
|
232
|
+
return lines.join("\n");
|
|
233
|
+
}
|
|
234
|
+
lines.push(` Fired ${hits.length} time(s):`);
|
|
235
|
+
for (const v of hits) {
|
|
236
|
+
lines.push(` · ${v.session} @ ${v.at}`);
|
|
237
|
+
lines.push(` ${v.detail}`);
|
|
238
|
+
const since = (v as { turns_since_prompt?: number }).turns_since_prompt;
|
|
239
|
+
const compacted = (v as { compaction_since_prompt?: boolean }).compaction_since_prompt;
|
|
240
|
+
if (since !== undefined || compacted !== undefined) {
|
|
241
|
+
lines.push(
|
|
242
|
+
` provenance: turns_since_prompt=${since ?? "—"} · ` +
|
|
243
|
+
`compaction_since_prompt=${compacted ?? "—"}`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return lines.join("\n");
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
type Entry = Record<string, unknown>;
|
|
178
251
|
|
|
179
252
|
function _isObject(v: unknown): v is Record<string, unknown> {
|
|
@@ -641,6 +714,7 @@ export function main(argv?: string[]): number {
|
|
|
641
714
|
let json = false;
|
|
642
715
|
let out: string | null = null;
|
|
643
716
|
let record: string | null = null;
|
|
717
|
+
let why: string | null = null;
|
|
644
718
|
for (let i = 0; i < args.length; i += 1) {
|
|
645
719
|
const a = args[i];
|
|
646
720
|
if (a === "--store" && args[i + 1] !== undefined) {
|
|
@@ -651,6 +725,8 @@ export function main(argv?: string[]): number {
|
|
|
651
725
|
json = true;
|
|
652
726
|
} else if (a === "--output" && args[i + 1] !== undefined) {
|
|
653
727
|
out = args[++i] as string;
|
|
728
|
+
} else if (a === "--why" && args[i + 1] !== undefined) {
|
|
729
|
+
why = args[++i] as string;
|
|
654
730
|
} else if (a === "--record") {
|
|
655
731
|
// Opt-in, and a bare `--record` takes the default path rather than
|
|
656
732
|
// swallowing the next flag as a filename.
|
|
@@ -660,6 +736,13 @@ export function main(argv?: string[]): number {
|
|
|
660
736
|
: DEFAULT_RATE_SERIES;
|
|
661
737
|
}
|
|
662
738
|
}
|
|
739
|
+
if (why !== null && !CHECK_IDS.includes(why as CheckId)) {
|
|
740
|
+
process.stderr.write(
|
|
741
|
+
`conformance:why: unknown check id ${JSON.stringify(why)} — known ids: ` +
|
|
742
|
+
`${CHECK_IDS.join(", ")}\n`,
|
|
743
|
+
);
|
|
744
|
+
return 2;
|
|
745
|
+
}
|
|
663
746
|
const resolved = store ?? defaultStore(process.cwd());
|
|
664
747
|
if (!fs.existsSync(resolved)) {
|
|
665
748
|
process.stderr.write(`conformance:behavior: no transcript store at ${resolved}\n`);
|
|
@@ -675,6 +758,28 @@ export function main(argv?: string[]): number {
|
|
|
675
758
|
// fs.writeSync, not process.stdout.write: `process.exit()` below does not
|
|
676
759
|
// flush an async pipe write, so `--json | jq` reproducibly received a
|
|
677
760
|
// truncated 64 KB document with no error (measured: 85,169 vs 65,536 bytes).
|
|
761
|
+
if (why !== null) {
|
|
762
|
+
const id = why as CheckId;
|
|
763
|
+
fs.writeSync(
|
|
764
|
+
1,
|
|
765
|
+
json
|
|
766
|
+
? `${JSON.stringify(
|
|
767
|
+
{
|
|
768
|
+
check: id,
|
|
769
|
+
meaning: CHECK_MEANINGS[id],
|
|
770
|
+
store: report.store,
|
|
771
|
+
sessions: report.sessions,
|
|
772
|
+
hits: report.per_session.flatMap((s) =>
|
|
773
|
+
s.violations.filter((v) => v.check === id),
|
|
774
|
+
),
|
|
775
|
+
},
|
|
776
|
+
null,
|
|
777
|
+
2,
|
|
778
|
+
)}\n`
|
|
779
|
+
: `${renderWhy(report, id)}\n`,
|
|
780
|
+
);
|
|
781
|
+
return 0;
|
|
782
|
+
}
|
|
678
783
|
fs.writeSync(1, json ? `${JSON.stringify(report, null, 2)}\n` : `${render(report)}\n`);
|
|
679
784
|
return 0;
|
|
680
785
|
}
|
|
@@ -59,6 +59,19 @@ function modelTier(model) {
|
|
|
59
59
|
// cache_creation_input_tokens NOT covered by that split (older records that
|
|
60
60
|
// predate the TTL breakdown) is priced at the 5m rate — Anthropic's default
|
|
61
61
|
// TTL — rather than silently dropped.
|
|
62
|
+
// A tier with no PRICING row prices at zero. That zero is indistinguishable
|
|
63
|
+
// from a genuinely free message, so the ROW carries `rate_missing` and the run
|
|
64
|
+
// warns once on stderr — the alternative (throwing) would lose a whole session
|
|
65
|
+
// of real token counts over one unrecognised model id. Token counts are kept
|
|
66
|
+
// untouched precisely so a later re-pricing pass can backfill the cost.
|
|
67
|
+
// Every token class that costs money. A record where this is 0 costs 0 at any
|
|
68
|
+
// rate, so a missing rate cannot understate it.
|
|
69
|
+
function billableTokens(u) {
|
|
70
|
+
if (!u) return 0;
|
|
71
|
+
return (u.input_tokens || 0) + (u.output_tokens || 0)
|
|
72
|
+
+ (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
function costForUsage(tier, u) {
|
|
63
76
|
const p = PRICING[tier];
|
|
64
77
|
if (!p || !u) return 0;
|
|
@@ -163,6 +176,8 @@ function summarizeSession(files) {
|
|
|
163
176
|
let sessionId = null, cwd = null;
|
|
164
177
|
let totalSeen = 0;
|
|
165
178
|
const seen = new Set();
|
|
179
|
+
// Model ids that priced at zero because no PRICING tier claimed them.
|
|
180
|
+
const rateMissingModels = new Set();
|
|
166
181
|
|
|
167
182
|
for (const file of files) {
|
|
168
183
|
let text;
|
|
@@ -188,6 +203,15 @@ function summarizeSession(files) {
|
|
|
188
203
|
const tier = modelTier(model);
|
|
189
204
|
const u = usageTokens(m.message.usage);
|
|
190
205
|
const cost = costForUsage(tier, u);
|
|
206
|
+
// Key on the ACTUAL zero condition — `!PRICING[tier]`, what
|
|
207
|
+
// costForUsage returns 0 on — not on `tier === 'unknown'`. The two
|
|
208
|
+
// coincide only because `unknown` is today the sole tier modelTier()
|
|
209
|
+
// returns that PRICING lacks; adding a vendor family without a rate row
|
|
210
|
+
// would otherwise restore the exact silent zero this flag removes.
|
|
211
|
+
// The usage guard keeps the flag worth reading: a message with no
|
|
212
|
+
// billable tokens costs nothing at any rate, so flagging it would claim
|
|
213
|
+
// an understatement that is not there.
|
|
214
|
+
if (!PRICING[tier] && billableTokens(u) > 0) rateMissingModels.add(model);
|
|
191
215
|
|
|
192
216
|
const slot = byModel[model] || { tier, input_tokens: 0, output_tokens: 0,
|
|
193
217
|
cache_creation_input_tokens: 0, cache_read_input_tokens: 0, messages: 0, cost_usd: 0 };
|
|
@@ -213,6 +237,12 @@ function summarizeSession(files) {
|
|
|
213
237
|
const dedup_ratio = totalSeen > 0 ? (totalSeen - dedupedCount) / totalSeen : 0;
|
|
214
238
|
return { sessionId, cwd, startedAt: firstTs, endedAt: lastTs, messageCount,
|
|
215
239
|
byModel, byTier, byBucket, total_cost_usd: totalCost,
|
|
240
|
+
// `rate_missing` marks a row whose total_cost_usd is understated because
|
|
241
|
+
// at least one model priced at zero for want of a rate — never because the
|
|
242
|
+
// work was free. `rate_missing_models` carries the ids so a backfill has a
|
|
243
|
+
// shape to work against instead of re-deriving it from the transcript.
|
|
244
|
+
rate_missing: rateMissingModels.size > 0,
|
|
245
|
+
rate_missing_models: [...rateMissingModels].sort(),
|
|
216
246
|
totalRecordsSeen: totalSeen, dedupedRecordsCount: dedupedCount, dedup_ratio,
|
|
217
247
|
capturedAt: new Date().toISOString() };
|
|
218
248
|
}
|
|
@@ -237,6 +267,15 @@ function main() {
|
|
|
237
267
|
}
|
|
238
268
|
const files = collectSessionFiles(projectDir, sessionPath);
|
|
239
269
|
const summary = summarizeSession(files);
|
|
270
|
+
// ONE warning per run, before the quiet return: a suppressed report is a
|
|
271
|
+
// display choice, an understated cost figure is a data-integrity problem.
|
|
272
|
+
if (summary.rate_missing) {
|
|
273
|
+
console.error(
|
|
274
|
+
`cost-track: rate_missing — no price tier for ${summary.rate_missing_models.join(', ')}; ` +
|
|
275
|
+
`those messages priced at $0 and the session total is understated. ` +
|
|
276
|
+
`Token counts are kept, so the row can be re-priced later.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
240
279
|
if (process.env.TRACK_OUT) writeFileSync(process.env.TRACK_OUT, JSON.stringify(summary, null, 2));
|
|
241
280
|
const store = process.env.TRACK_STORE || DEFAULT_STORE;
|
|
242
281
|
let res = { ok: false, reason: 'dry-run' };
|