@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.
Files changed (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +85 -72
  3. package/dist/agent-src/commands/agent-handoff.md +15 -3
  4. package/dist/agent-src/commands/review/routing.md +3 -4
  5. package/dist/agent-src/contexts/communication/rules-auto/reply-close-mechanics.md +65 -0
  6. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +3 -0
  7. package/dist/agent-src/rules/delegation-policy.md +6 -0
  8. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  9. package/dist/agent-src/scripts/roadmap_gates.ts +107 -3
  10. package/dist/agent-src/skills/corpus-grounding/SKILL.md +21 -12
  11. package/dist/agent-src/skills/design-tokens/SKILL.md +15 -0
  12. package/dist/agent-src/skills/docker/SKILL.md +11 -0
  13. package/dist/agent-src/skills/laravel/SKILL.md +11 -0
  14. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +24 -0
  15. package/dist/agent-src/skills/review-routing/SKILL.md +23 -6
  16. package/dist/agent-src/skills/rule-writing/SKILL.md +23 -0
  17. package/dist/agent-src/skills/skill-writing/SKILL.md +80 -2
  18. package/dist/agent-src/skills/systematic-debugging/SKILL.md +5 -3
  19. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +24 -0
  20. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  21. package/dist/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  22. package/dist/cli/registry.js +4 -4
  23. package/dist/cli/registry.js.map +1 -1
  24. package/dist/cli-delegate/chunk-HTIU3W56.js +66 -0
  25. package/dist/cli-delegate/{chunk-2WELHO3P.js → chunk-I5EAFRII.js} +35 -0
  26. package/dist/cli-delegate/{chunk-ITIQSGOQ.js → chunk-MA4GFWDY.js} +7 -4
  27. package/dist/cli-delegate/{chunk-PFBV6XRT.js → chunk-Q26HT4QT.js} +1 -63
  28. package/dist/cli-delegate/{chunk-542DZR4P.js → chunk-REMEZSVP.js} +1660 -146
  29. package/dist/cli-delegate/{chunk-3EWYC3YK.js → chunk-UEZ7D35A.js} +4 -3
  30. package/dist/cli-delegate/{chunk-LVRYBKTI.js → chunk-YPAWXXXR.js} +158 -0
  31. package/dist/cli-delegate/cmd_conformance.js +256 -226
  32. package/dist/cli-delegate/cmd_doctor.js +3 -2
  33. package/dist/cli-delegate/cmd_export.js +1 -1
  34. package/dist/cli-delegate/cmd_handoff.js +21 -28
  35. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  36. package/dist/cli-delegate/cmd_prune.js +1 -1
  37. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  38. package/dist/cli-delegate/cmd_session_recycle.js +20 -5
  39. package/dist/cli-delegate/cmd_settings_get.js +4 -2
  40. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  41. package/dist/cli-delegate/cmd_settings_set.js +5 -3
  42. package/dist/cli-delegate/cmd_sync.js +1 -1
  43. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  44. package/dist/cli-delegate/cmd_validate.js +1 -1
  45. package/dist/discovery/deprecation-report.md +1 -1
  46. package/dist/discovery/discovery-manifest.json +17 -17
  47. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  48. package/dist/discovery/discovery-manifest.summary.md +2 -2
  49. package/dist/discovery/dormancy-report.md +17 -0
  50. package/dist/discovery/orphan-report.md +1 -1
  51. package/dist/discovery/packs.json +3 -3
  52. package/dist/discovery/trust-report.md +1 -1
  53. package/dist/discovery/workspaces.json +3 -3
  54. package/dist/hooks/dispatch.js +114 -7
  55. package/dist/install/install.mjs +7 -4
  56. package/dist/mcp/registry-manifest.json +3 -3
  57. package/dist/mcp/server.json +2 -2
  58. package/dist/mcp/server.mjs +3755 -1168
  59. package/docs/guidelines/agent-infra/failure-signatures.md +47 -12
  60. package/package.json +2 -2
  61. package/src/agent-src/scripts/archive_completed_roadmaps.ts +35 -1
  62. package/src/agent-src/scripts/roadmap_gates.ts +107 -3
  63. package/src/agent-src/templates/scripts/ownership-map.example.yml +1 -1
  64. package/src/config/cost-parity-budget.json +167 -0
  65. package/src/config/gate-coverage.yml +44 -5
  66. package/src/config/quorum-attendance-budget.json +39 -0
  67. package/src/scripts/_cli/cmd_doctor.ts +69 -1
  68. package/src/scripts/_cli/cmd_session_recycle.ts +25 -4
  69. package/src/scripts/_dispatch.bash +5 -3
  70. package/src/scripts/_lib/env_kill_switch.ts +37 -0
  71. package/src/scripts/_lib/orchestration_record.ts +45 -0
  72. package/src/scripts/_lib/security_lint.ts +18 -4
  73. package/src/scripts/_lib/self_repair.ts +60 -0
  74. package/src/scripts/_lib/self_repair_store.ts +65 -5
  75. package/src/scripts/_lib/subagent_capsule.ts +78 -1
  76. package/src/scripts/ai_council/clients.ts +31 -0
  77. package/src/scripts/ai_council/events_log.ts +130 -8
  78. package/src/scripts/ai_council/orchestrator.ts +50 -3
  79. package/src/scripts/ai_council/pricing.ts +48 -1
  80. package/src/scripts/ai_council/prompts.ts +165 -3
  81. package/src/scripts/ai_council/quorum.ts +30 -0
  82. package/src/scripts/ai_council/session.ts +11 -0
  83. package/src/scripts/ai_team/review_gate.ts +2 -2
  84. package/src/scripts/ask_transport.ts +20 -1
  85. package/src/scripts/build_discovery_manifest.ts +165 -0
  86. package/src/scripts/cache_realization_report.ts +38 -0
  87. package/src/scripts/check_claims.ts +51 -2
  88. package/src/scripts/check_release_highlights.ts +7 -1
  89. package/src/scripts/ci_time_ratio.ts +3 -1
  90. package/src/scripts/conformance_scan.ts +105 -0
  91. package/src/scripts/cost/track.mjs +39 -0
  92. package/src/scripts/cost_summary.ts +104 -1
  93. package/src/scripts/council_cli.ts +131 -13
  94. package/src/scripts/discovery_graph.ts +201 -27
  95. package/src/scripts/dispatch_economy_report.ts +5 -2
  96. package/src/scripts/dispatch_r2_reviewer.ts +102 -1
  97. package/src/scripts/install.ts +20 -6
  98. package/src/scripts/lint_budget_ownership.ts +81 -0
  99. package/src/scripts/lint_handoffs.ts +92 -2
  100. package/src/scripts/lint_hedge_words.ts +317 -0
  101. package/src/scripts/lint_mcp_config_security.ts +1 -1
  102. package/src/scripts/lint_skill_frontmatter_safety.ts +56 -2
  103. package/src/scripts/lint_workflow_paths.ts +384 -0
  104. package/src/scripts/preamble_byte_census.ts +9 -1
  105. package/src/scripts/report_carrier_divergence.ts +10 -0
  106. package/src/scripts/report_conformance_funnel.ts +40 -0
  107. package/src/scripts/schemas/subagent.schema.json +3 -2
  108. package/src/scripts/self_repair_cli.ts +26 -1
  109. 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 ?? 'unknown',
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
- const URL_DATED = /^https?:\/\/\S+\(\d{4}-\d{2}-\d{2}\)\s*$/;
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
- function main(argv: readonly string[]): number {
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/runtime/reports/ci-time-ratio.json`.
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' };