@compr/opscontext-mcp 2.4.3 → 2.5.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/dist/hooks.js CHANGED
@@ -550,4 +550,158 @@ export function formatDocCoverageViolationsJson(violations) {
550
550
  })),
551
551
  });
552
552
  }
553
+ /**
554
+ * 🔒 LOCKED [RULE-PARITY-IS-DIFF-AWARE] — 2026-08-19
555
+ * ⛔ NEVER make this fire on commits that touch none of the rule's files.
556
+ * WHY: parity is a property of the whole repo, so the naive implementation checks the
557
+ * working tree on every commit — which means one pre-existing drift blocks every
558
+ * unrelated commit until someone fixes it. That is how a useful gate becomes a gate
559
+ * everyone disables. doc_coverage already learned this: it only fires when the commit
560
+ * touches the rule's source paths.
561
+ * FIX: fire only when the commit stages a change to at least one file the rule governs.
562
+ * Editing one of the three agent docs is exactly the moment parity can break, and
563
+ * exactly the moment the author has the context to fix it. `--all` (see cliHookRuleParity)
564
+ * audits the whole repo on demand, for CI or a deliberate sweep.
565
+ *
566
+ * 🔒 LOCKED [RULE-PARITY-READS-THE-INDEX] — 2026-08-19
567
+ * ⛔ NEVER evaluate marker presence from the working tree during a pre-commit check.
568
+ * WHY: the first cut did, with the rationalisation that "for a normal `git commit` the
569
+ * working tree IS the post-commit state". That is false whenever staging is partial,
570
+ * which is the normal case for anyone using `git add -p`. Demonstrated: stage the
571
+ * REMOVAL of the marker from CLAUDE.md, then restore it in the working tree only —
572
+ * the gate passed, and the commit that deleted the rule from the file agents read
573
+ * went through clean. The check was measuring a state git was not about to record.
574
+ * Same family as [SCORE-CANARY]'s pins and [EXEC-FAILURE-IS-NOT-EMPTY]: the tool
575
+ * answered confidently about something it had not actually looked at.
576
+ * FIX: for staged files read the INDEX blob (`git show :path`) — that is literally what
577
+ * the commit will contain. Unstaged files keep their worktree content, because the
578
+ * commit leaves them untouched. `--all` audits the worktree by design: it answers
579
+ * "is the repo consistent right now", not "is this commit consistent".
580
+ */
581
+ /**
582
+ * Content of `rel` as it will exist AFTER the pending commit.
583
+ * Staged → the index blob. Not staged → the working tree (the commit does not touch it).
584
+ * Returns null when the path will not exist.
585
+ */
586
+ function contentAfterCommit(repoRoot, rel, stagedSet, useWorktree) {
587
+ if (!useWorktree && stagedSet.has(rel)) {
588
+ try {
589
+ return execSync(`git show :"${rel}"`, {
590
+ cwd: repoRoot,
591
+ encoding: "utf-8",
592
+ maxBuffer: 32 * 1024 * 1024,
593
+ });
594
+ }
595
+ catch {
596
+ // [EXEC-FAILURE-IS-NOT-EMPTY] — "git show failed" is not "the file is absent".
597
+ // Not a git repo, a corrupt index, or a path git cannot resolve all land here.
598
+ // Fall through to the working tree rather than reporting a file that plainly
599
+ // exists as missing. A staged DELETION never reaches this branch: getStagedFiles
600
+ // filters on --diff-filter=ACMR, so deleted paths are not in stagedSet and are
601
+ // correctly read as absent from the worktree below.
602
+ }
603
+ }
604
+ const abs = join(repoRoot, rel);
605
+ if (!existsSync(abs))
606
+ return null;
607
+ return readFileSync(abs, "utf-8");
608
+ }
609
+ export function runRuleParity(policy, files, repoRoot, opts = {}) {
610
+ const stagedSet = new Set(files.map((f) => f.path));
611
+ const violations = [];
612
+ for (const rule of policy.rule_parity) {
613
+ // [RULE-PARITY-IS-DIFF-AWARE] — only fire when this commit touches a governed file.
614
+ if (!opts.all && !rule.required_in.some((p) => stagedSet.has(p)))
615
+ continue;
616
+ const presentIn = [];
617
+ const missingFrom = [];
618
+ const missingFiles = [];
619
+ for (const rel of rule.required_in) {
620
+ // [RULE-PARITY-READS-THE-INDEX] — what the commit records, not what is on disk.
621
+ const content = contentAfterCommit(repoRoot, rel, stagedSet, opts.all === true);
622
+ if (content === null) {
623
+ missingFiles.push(rel);
624
+ continue;
625
+ }
626
+ if (content.includes(rule.marker))
627
+ presentIn.push(rel);
628
+ else
629
+ missingFrom.push(rel);
630
+ }
631
+ if (missingFiles.length > 0) {
632
+ violations.push({
633
+ severity: rule.severity,
634
+ ruleId: rule.id,
635
+ marker: rule.marker,
636
+ presentIn,
637
+ missingFrom,
638
+ missingFiles,
639
+ reason: "file-not-found",
640
+ });
641
+ continue;
642
+ }
643
+ // Adopted nowhere. Only a violation when the rule says it is mandatory.
644
+ if (presentIn.length === 0) {
645
+ if (rule.always_required) {
646
+ violations.push({
647
+ severity: rule.severity,
648
+ ruleId: rule.id,
649
+ marker: rule.marker,
650
+ presentIn,
651
+ missingFrom,
652
+ reason: "marker-required-but-absent-everywhere",
653
+ });
654
+ }
655
+ continue;
656
+ }
657
+ // Present somewhere but not everywhere — the drift this rule exists to catch.
658
+ if (missingFrom.length > 0) {
659
+ violations.push({
660
+ severity: rule.severity,
661
+ ruleId: rule.id,
662
+ marker: rule.marker,
663
+ presentIn,
664
+ missingFrom,
665
+ reason: "marker-missing-from-some-files",
666
+ });
667
+ }
668
+ }
669
+ return violations;
670
+ }
671
+ export function formatRuleParityViolations(violations) {
672
+ if (violations.length === 0)
673
+ return "✅ All rule-parity rules satisfied.";
674
+ const lines = [];
675
+ const blocking = violations.filter((v) => v.severity === "block").length;
676
+ lines.push(`📐 RULE PARITY: ${violations.length} violation(s) — ${blocking} blocking, ${violations.length - blocking} warning(s).`);
677
+ for (const v of violations) {
678
+ if (v.reason === "file-not-found") {
679
+ lines.push(` [${v.severity}] ${v.ruleId}: listed file(s) do not exist → ${v.missingFiles.join(", ")}`);
680
+ continue;
681
+ }
682
+ if (v.reason === "marker-required-but-absent-everywhere") {
683
+ lines.push(` [${v.severity}] ${v.ruleId}: marker "${v.marker}" is required but present in none of ${v.missingFrom.join(", ")}`);
684
+ continue;
685
+ }
686
+ lines.push(` [${v.severity}] ${v.ruleId}: marker "${v.marker}" is out of sync.`);
687
+ lines.push(` present in : ${v.presentIn.join(", ")}`);
688
+ lines.push(` MISSING from: ${v.missingFrom.join(", ")}`);
689
+ }
690
+ return lines.join("\n");
691
+ }
692
+ export function formatRuleParityViolationsJson(violations) {
693
+ return JSON.stringify({
694
+ check: "rule-parity",
695
+ violations: violations.length,
696
+ blocking: violations.filter((v) => v.severity === "block").length,
697
+ details: violations.map((v) => ({
698
+ rule_id: v.ruleId,
699
+ severity: v.severity,
700
+ reason: v.reason,
701
+ present_in: v.presentIn,
702
+ missing_from: v.missingFrom,
703
+ missing_files: v.missingFiles ?? [],
704
+ })),
705
+ });
706
+ }
553
707
  //# sourceMappingURL=hooks.js.map
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { searchChunks } from "./search.js";
8
8
  import { initEmbeddings, embedChunks, vectorSearch, isEmbeddingsReady, } from "./embeddings.js";
9
9
  import { collectProjectOps, collectSystemOps } from "./collectors.js";
10
10
  import { loadCache, saveCache } from "./cache.js";
11
- import { listProjects, checkPorts, runComplianceAudit, formatProjectList, formatPortMap, formatPlan, scoreProject, formatScoreReport, } from "./agents.js";
11
+ import { listProjects, checkPorts, runComplianceAudit, formatProjectList, formatPortMap, formatPlan, scoreProject, formatScoreReport, runScoreCanary, } from "./agents.js";
12
12
  import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
13
13
  import { verifyChain, readAuditLog, filterByRange } from "./audit.js";
14
14
  import { startEventIngestServer } from "./http-server.js";
@@ -479,6 +479,29 @@ server.tool("score_project", "Score one or all projects on AI-readiness (0-100%)
479
479
  const gate = gateCheck("score_project");
480
480
  if (gate)
481
481
  return { content: [{ type: "text", text: gate }] };
482
+ // 🔒 LOCKED [SCORE-CANARY-COVERS-EVERY-SCORER] — 2026-08-19
483
+ // ⛔ NEVER let a scoring entry point run without the canary.
484
+ // WHY: [SCORE-CANARY] was wired on the CLI only. This MCP tool — the path
485
+ // Claude Code actually scores through — had zero call sites, so a
486
+ // drifting scorer would have been caught when a human typed the
487
+ // command and missed entirely when an agent called the tool. Session
488
+ // 21 §H2 found the same shape in the fleet-write guard: a guard that
489
+ // covers one caller reads, from the outside, exactly like one that works.
490
+ // FIX: canary here too. This tool never writes SCORE.md, so a deviation
491
+ // is reported rather than fatal — but it is never silent.
492
+ const canary = runScoreCanary();
493
+ if (!canary.ok) {
494
+ return {
495
+ content: [{
496
+ type: "text",
497
+ text: "🚨 Scoring canary FAILED — these scores are NOT trustworthy.\n\n" +
498
+ canary.deviations.map((d) => ` • ${d}`).join("\n") +
499
+ (canary.inconclusive
500
+ ? "\n\nThe canary fixture could not be built, so the scorer is unverified. This is an unknown, not a pass."
501
+ : "\n\nThe scorer no longer behaves as pinned. Fix the deviation or update the pin deliberately."),
502
+ }],
503
+ };
504
+ }
482
505
  const projectDirs = loadProjectDirs();
483
506
  let scores;
484
507
  if (project) {
package/dist/policy.d.ts CHANGED
@@ -89,6 +89,89 @@ export type BypassToken = z.infer<typeof BypassTokenSchema>;
89
89
  /**
90
90
  * The full policy document — schema version 1.
91
91
  */
92
+ /**
93
+ * 🔒 LOCKED [RULE-PARITY-IS-DOC-TO-DOC] — 2026-08-19
94
+ * ⛔ NEVER fold this into doc_coverage. They answer different questions.
95
+ * WHY: doc_coverage maps SOURCE → DOC ("you changed src/audit.ts, update SKILLS.md").
96
+ * It cannot see the failure that motivated this: a rule that existed in
97
+ * ~/.claude/CLAUDE.md and AGENT_USAGE.md but NOT in .github/copilot-instructions.md
98
+ * — so it was invisible to Cursor, Windsurf and Copilot, which read only the latter.
99
+ * No source file changed, so no doc_coverage rule could ever fire. The rule was
100
+ * written, reviewed, and simply not where the readers look.
101
+ * FIX: parity between DOCS. If a marker appears in any listed file, it must appear in all
102
+ * of them. Deliberately literal-substring, not regex: a marker is a grep-able tag in
103
+ * the LOCK tradition, and a regex here would fail open on a typo.
104
+ */
105
+ export declare const RuleParitySchema: z.ZodObject<{
106
+ id: z.ZodString;
107
+ marker: z.ZodString;
108
+ required_in: z.ZodArray<z.ZodString>;
109
+ severity: z.ZodDefault<z.ZodEnum<{
110
+ warn: "warn";
111
+ block: "block";
112
+ }>>;
113
+ always_required: z.ZodDefault<z.ZodBoolean>;
114
+ description: z.ZodOptional<z.ZodString>;
115
+ }, z.core.$strip>;
116
+ /**
117
+ * Model pricing, in dollars per million tokens.
118
+ *
119
+ * 🔒 LOCKED [PRICING-LIVES-IN-POLICY] — 2026-08-19
120
+ * ⛔ NEVER hardcode a rate in the collector, the detector or the CLI.
121
+ * WHY: rates change, and the collector must be able to value runs for models
122
+ * it has never heard of. A rate baked into a compiled `dist/` is a rate
123
+ * nobody can correct without a release.
124
+ * FIX: rates live in `.contextengine/policy.json` → `agent_cost.pricing`.
125
+ * Lookup is longest-prefix with a `*` catch-all; an unmatched model is
126
+ * reported as UNPRICED, never silently valued at zero.
127
+ */
128
+ export declare const ModelPricingSchema: z.ZodObject<{
129
+ model: z.ZodString;
130
+ input_per_mtok: z.ZodNumber;
131
+ output_per_mtok: z.ZodNumber;
132
+ cache_read_per_mtok: z.ZodNumber;
133
+ cache_write_5m_per_mtok: z.ZodNumber;
134
+ cache_write_1h_per_mtok: z.ZodOptional<z.ZodNumber>;
135
+ }, z.core.$strip>;
136
+ export type ModelPricingRule = z.infer<typeof ModelPricingSchema>;
137
+ /**
138
+ * Thresholds for the context_burn and fanout_without_canary detectors.
139
+ *
140
+ * 🔒 LOCKED [BURN-IS-COST-WEIGHTED-NOT-VOLUME] — 2026-08-19
141
+ * ⛔ NEVER fire context_burn on a low output/volume ratio alone.
142
+ * WHY: a healthy 30-agent workflow measures 1.8% output by volume. That looks
143
+ * alarming and is not: cache_read is billed at 0.1x input, so those same
144
+ * tokens are 28% of cost, and the run cost $35 against $120 without cache.
145
+ * Firing on the volume ratio would flag every well-cached run ever done and
146
+ * train the user to ignore the alert — the exact failure the
147
+ * [DRIFT-HEURISTICS] LOCK in detector.ts exists to prevent.
148
+ * FIX: fire on cache INEFFICIENCY (cache_read / cache_write below
149
+ * min_cache_efficiency — the prefix is being rebuilt, not reused), on tool
150
+ * calls per agent, and on valued cost per agent. Real evidence for the
151
+ * inefficiency rule: wf_f676d824 spent 68% of $209 on cache WRITES,
152
+ * 22.7M written against only 8.5M read, across 722 agents.
153
+ */
154
+ export declare const AgentCostSchema: z.ZodObject<{
155
+ billing_mode: z.ZodDefault<z.ZodEnum<{
156
+ api: "api";
157
+ subscription: "subscription";
158
+ }>>;
159
+ pricing: z.ZodDefault<z.ZodArray<z.ZodObject<{
160
+ model: z.ZodString;
161
+ input_per_mtok: z.ZodNumber;
162
+ output_per_mtok: z.ZodNumber;
163
+ cache_read_per_mtok: z.ZodNumber;
164
+ cache_write_5m_per_mtok: z.ZodNumber;
165
+ cache_write_1h_per_mtok: z.ZodOptional<z.ZodNumber>;
166
+ }, z.core.$strip>>>;
167
+ min_cache_efficiency: z.ZodDefault<z.ZodNumber>;
168
+ max_tool_calls_per_agent: z.ZodDefault<z.ZodNumber>;
169
+ max_cost_per_agent_usd: z.ZodDefault<z.ZodNumber>;
170
+ min_fanout_for_canary: z.ZodDefault<z.ZodNumber>;
171
+ max_failed_share: z.ZodDefault<z.ZodNumber>;
172
+ description: z.ZodOptional<z.ZodString>;
173
+ }, z.core.$strip>;
174
+ export type AgentCost = z.infer<typeof AgentCostSchema>;
92
175
  export declare const PolicySchema: z.ZodObject<{
93
176
  version: z.ZodLiteral<1>;
94
177
  extends: z.ZodOptional<z.ZodString>;
@@ -133,6 +216,37 @@ export declare const PolicySchema: z.ZodObject<{
133
216
  requires_reason_min_length: z.ZodDefault<z.ZodNumber>;
134
217
  description: z.ZodOptional<z.ZodString>;
135
218
  }, z.core.$strip>>>;
219
+ rule_parity: z.ZodDefault<z.ZodArray<z.ZodObject<{
220
+ id: z.ZodString;
221
+ marker: z.ZodString;
222
+ required_in: z.ZodArray<z.ZodString>;
223
+ severity: z.ZodDefault<z.ZodEnum<{
224
+ warn: "warn";
225
+ block: "block";
226
+ }>>;
227
+ always_required: z.ZodDefault<z.ZodBoolean>;
228
+ description: z.ZodOptional<z.ZodString>;
229
+ }, z.core.$strip>>>;
230
+ agent_cost: z.ZodOptional<z.ZodObject<{
231
+ billing_mode: z.ZodDefault<z.ZodEnum<{
232
+ api: "api";
233
+ subscription: "subscription";
234
+ }>>;
235
+ pricing: z.ZodDefault<z.ZodArray<z.ZodObject<{
236
+ model: z.ZodString;
237
+ input_per_mtok: z.ZodNumber;
238
+ output_per_mtok: z.ZodNumber;
239
+ cache_read_per_mtok: z.ZodNumber;
240
+ cache_write_5m_per_mtok: z.ZodNumber;
241
+ cache_write_1h_per_mtok: z.ZodOptional<z.ZodNumber>;
242
+ }, z.core.$strip>>>;
243
+ min_cache_efficiency: z.ZodDefault<z.ZodNumber>;
244
+ max_tool_calls_per_agent: z.ZodDefault<z.ZodNumber>;
245
+ max_cost_per_agent_usd: z.ZodDefault<z.ZodNumber>;
246
+ min_fanout_for_canary: z.ZodDefault<z.ZodNumber>;
247
+ max_failed_share: z.ZodDefault<z.ZodNumber>;
248
+ description: z.ZodOptional<z.ZodString>;
249
+ }, z.core.$strip>>;
136
250
  }, z.core.$strip>;
137
251
  export type Policy = z.infer<typeof PolicySchema>;
138
252
  /**
package/dist/policy.js CHANGED
@@ -106,6 +106,90 @@ export const BypassTokenSchema = z.object({
106
106
  /**
107
107
  * The full policy document — schema version 1.
108
108
  */
109
+ /**
110
+ * 🔒 LOCKED [RULE-PARITY-IS-DOC-TO-DOC] — 2026-08-19
111
+ * ⛔ NEVER fold this into doc_coverage. They answer different questions.
112
+ * WHY: doc_coverage maps SOURCE → DOC ("you changed src/audit.ts, update SKILLS.md").
113
+ * It cannot see the failure that motivated this: a rule that existed in
114
+ * ~/.claude/CLAUDE.md and AGENT_USAGE.md but NOT in .github/copilot-instructions.md
115
+ * — so it was invisible to Cursor, Windsurf and Copilot, which read only the latter.
116
+ * No source file changed, so no doc_coverage rule could ever fire. The rule was
117
+ * written, reviewed, and simply not where the readers look.
118
+ * FIX: parity between DOCS. If a marker appears in any listed file, it must appear in all
119
+ * of them. Deliberately literal-substring, not regex: a marker is a grep-able tag in
120
+ * the LOCK tradition, and a regex here would fail open on a typo.
121
+ */
122
+ export const RuleParitySchema = z.object({
123
+ id: z.string().min(1).describe("Stable identifier for this parity rule"),
124
+ marker: z
125
+ .string()
126
+ .min(3)
127
+ .describe("Literal substring that marks the rule's presence, e.g. 'MULTI-AGENT COST'"),
128
+ required_in: z
129
+ .array(z.string())
130
+ .min(2)
131
+ .describe("Repo-relative doc paths that must agree. Fewer than 2 makes parity meaningless."),
132
+ severity: z.enum(["block", "warn"]).default("block"),
133
+ /** When true the marker must be present in EVERY file, even if currently in none. */
134
+ always_required: z.boolean().default(false),
135
+ description: z.string().optional(),
136
+ });
137
+ /**
138
+ * Model pricing, in dollars per million tokens.
139
+ *
140
+ * 🔒 LOCKED [PRICING-LIVES-IN-POLICY] — 2026-08-19
141
+ * ⛔ NEVER hardcode a rate in the collector, the detector or the CLI.
142
+ * WHY: rates change, and the collector must be able to value runs for models
143
+ * it has never heard of. A rate baked into a compiled `dist/` is a rate
144
+ * nobody can correct without a release.
145
+ * FIX: rates live in `.contextengine/policy.json` → `agent_cost.pricing`.
146
+ * Lookup is longest-prefix with a `*` catch-all; an unmatched model is
147
+ * reported as UNPRICED, never silently valued at zero.
148
+ */
149
+ export const ModelPricingSchema = z.object({
150
+ model: z.string().min(1).describe("Exact model id, a prefix of one, or '*' as catch-all"),
151
+ input_per_mtok: z.number().nonnegative(),
152
+ output_per_mtok: z.number().nonnegative(),
153
+ cache_read_per_mtok: z.number().nonnegative(),
154
+ cache_write_5m_per_mtok: z.number().nonnegative(),
155
+ cache_write_1h_per_mtok: z.number().nonnegative().optional(),
156
+ });
157
+ /**
158
+ * Thresholds for the context_burn and fanout_without_canary detectors.
159
+ *
160
+ * 🔒 LOCKED [BURN-IS-COST-WEIGHTED-NOT-VOLUME] — 2026-08-19
161
+ * ⛔ NEVER fire context_burn on a low output/volume ratio alone.
162
+ * WHY: a healthy 30-agent workflow measures 1.8% output by volume. That looks
163
+ * alarming and is not: cache_read is billed at 0.1x input, so those same
164
+ * tokens are 28% of cost, and the run cost $35 against $120 without cache.
165
+ * Firing on the volume ratio would flag every well-cached run ever done and
166
+ * train the user to ignore the alert — the exact failure the
167
+ * [DRIFT-HEURISTICS] LOCK in detector.ts exists to prevent.
168
+ * FIX: fire on cache INEFFICIENCY (cache_read / cache_write below
169
+ * min_cache_efficiency — the prefix is being rebuilt, not reused), on tool
170
+ * calls per agent, and on valued cost per agent. Real evidence for the
171
+ * inefficiency rule: wf_f676d824 spent 68% of $209 on cache WRITES,
172
+ * 22.7M written against only 8.5M read, across 722 agents.
173
+ */
174
+ export const AgentCostSchema = z.object({
175
+ /**
176
+ * `subscription` — no dollar is debited; cost is a valuation and CAPACITY is
177
+ * the scarce resource. `api` — cost is a real debit.
178
+ */
179
+ billing_mode: z.enum(["subscription", "api"]).default("subscription"),
180
+ pricing: z.array(ModelPricingSchema).default([]),
181
+ /** cache_read / cache_write below this means the cache is thrashing. */
182
+ min_cache_efficiency: z.number().nonnegative().default(3),
183
+ /** Per-agent tool calls above this means the agent is searching, not working. */
184
+ max_tool_calls_per_agent: z.number().positive().default(2),
185
+ /** Valued cost of a single agent, in dollars. */
186
+ max_cost_per_agent_usd: z.number().nonnegative().default(3),
187
+ /** Below this many agents, a fan-out is too small to need a canary. */
188
+ min_fanout_for_canary: z.number().int().positive().default(5),
189
+ /** Share of agents that may die without a result before this is a failure. */
190
+ max_failed_share: z.number().min(0).max(1).default(0.05),
191
+ description: z.string().optional(),
192
+ });
109
193
  export const PolicySchema = z.object({
110
194
  version: z.literal(1).describe("Policy schema version. Pin to 1 — bumps require a migration path."),
111
195
  extends: z
@@ -118,6 +202,8 @@ export const PolicySchema = z.object({
118
202
  deploy_verify_hosts: z.array(DeployVerifyHostSchema).default([]),
119
203
  commit_message_required: z.array(CommitMessageRequiredSchema).default([]),
120
204
  bypass_tokens: z.array(BypassTokenSchema).default([]),
205
+ rule_parity: z.array(RuleParitySchema).default([]),
206
+ agent_cost: AgentCostSchema.optional(),
121
207
  });
122
208
  export function validatePolicy(raw) {
123
209
  const result = PolicySchema.safeParse(raw);
@@ -209,6 +295,10 @@ export function formatPolicySummary(policy) {
209
295
  for (const b of policy.bypass_tokens) {
210
296
  lines.push(` - ${b.id} → TTL ${b.ttl_seconds}s, reason ≥ ${b.requires_reason_min_length} chars`);
211
297
  }
298
+ lines.push(`Rule-parity rules: ${policy.rule_parity.length}`);
299
+ for (const r of policy.rule_parity) {
300
+ lines.push(` - ${r.id} → marker "${r.marker}" must agree across ${r.required_in.length} file(s) [${r.severity}]${r.always_required ? " (always required)" : ""}`);
301
+ }
212
302
  return lines.join("\n");
213
303
  }
214
304
  export function formatValidationErrors(errors) {
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Transcript collector — per-subagent token, cost and intensity accounting
3
+ * read from Claude Code's own JSONL transcripts.
4
+ *
5
+ * Layout (verified against 2,310 real agent transcripts on 2026-08-19):
6
+ *
7
+ * ~/.claude/projects/<project-slug>/<sessionId>.jsonl parent session
8
+ * ~/.claude/projects/<project-slug>/<sessionId>/subagents/
9
+ * agent-<agentId>.jsonl Agent-tool subagent
10
+ * workflows/<wf_id>/agent-<agentId>.jsonl Workflow subagent
11
+ *
12
+ * 🔒 LOCKED [TRANSCRIPT-DEDUP-BY-MESSAGE-ID] — 2026-08-19
13
+ * ⛔ NEVER sum `message.usage` per JSONL line. One assistant `message.id` is
14
+ * written across SEVERAL lines (one per content block: thinking, text, each
15
+ * tool_use), and EVERY line repeats the SAME usage object.
16
+ * WHY: measured on a real agent transcript, naive per-line summing reported
17
+ * 624,873 cache_read tokens where the true figure was 225,183 — a 2.8x
18
+ * overcount, and 4.6x on cache_creation. A cost report that overstates by
19
+ * 3x is worse than no cost report: it gets disbelieved, then ignored.
20
+ * FIX: reduce by `message.id`. Verified invariant over 1,609 message ids in
21
+ * 126 files: input_tokens / cache_creation_input_tokens /
22
+ * cache_read_input_tokens are CONSTANT within an id (0 exceptions), and
23
+ * output_tokens increases monotonically, so the max is the final count.
24
+ * tests/transcript-collector.test.ts pins both halves.
25
+ */
26
+ /** Raw token tallies, in tokens. */
27
+ export interface TokenTally {
28
+ input: number;
29
+ cacheWrite5m: number;
30
+ cacheWrite1h: number;
31
+ cacheRead: number;
32
+ output: number;
33
+ }
34
+ /**
35
+ * How a subagent's transcript ended. `capacity_exhausted` is the one that
36
+ * matters on a subscription: the agent was launched, consumed context, and
37
+ * returned nothing because the usage window ran out.
38
+ */
39
+ export type AgentStatus = "reported_structured" | "reported_text" | "capacity_exhausted" | "output_cap" | "api_error" | "no_report";
40
+ export interface AgentUsage {
41
+ agentId: string;
42
+ file: string;
43
+ /** Dominant real model, for display. Never `<synthetic>` — see the LOCK below. */
44
+ model: string | null;
45
+ /**
46
+ * Tokens attributed to the model that actually produced them.
47
+ *
48
+ * 🔒 LOCKED [PRICE-PER-MESSAGE-MODEL-NOT-PER-AGENT] — 2026-08-19
49
+ * ⛔ NEVER price an agent's whole tally at one model taken from its last
50
+ * assistant message.
51
+ * WHY: Claude Code writes client-side notices ("You're out of usage
52
+ * credits", "API Error: …") as assistant messages with model
53
+ * `<synthetic>` and ALL-ZERO usage. They land LAST, so last-wins tagged
54
+ * every capacity-killed agent `<synthetic>`, and since that model has no
55
+ * price its real consumption was dropped as UNPRICED: 2.2M tokens
56
+ * silently missing from wf_41771d7b — precisely the agents that died,
57
+ * i.e. the cost of the failure the report exists to surface.
58
+ * FIX: tally per message model and price each group at its own rate.
59
+ * `<synthetic>` contributes 0 tokens and is excluded from `model`.
60
+ */
61
+ tokensByModel: Map<string | null, TokenTally>;
62
+ toolCalls: number;
63
+ /** Distinct assistant messages (API round-trips), after dedup. */
64
+ turns: number;
65
+ tokens: TokenTally;
66
+ startedAt: number | null;
67
+ endedAt: number | null;
68
+ durationMs: number | null;
69
+ status: AgentStatus;
70
+ /** True when the agent actually returned a result to its caller. */
71
+ reported: boolean;
72
+ }
73
+ export type RunKind = "workflow" | "agents" | "session";
74
+ export interface RunUsage {
75
+ /** Workflow id (`wf_…`) for workflow runs, else the session id. */
76
+ runId: string;
77
+ kind: RunKind;
78
+ /** Decoded project slug, e.g. `-Users-yan-Projects-ContextEngine`. */
79
+ project: string;
80
+ sessionId: string;
81
+ agents: AgentUsage[];
82
+ totals: TokenTally;
83
+ toolCalls: number;
84
+ startedAt: number | null;
85
+ endedAt: number | null;
86
+ /** Wall-clock span of the run, not the sum of agent durations. */
87
+ durationMs: number | null;
88
+ }
89
+ /** Dollars per million tokens, per tier. */
90
+ export interface ModelPricing {
91
+ model: string;
92
+ input_per_mtok: number;
93
+ output_per_mtok: number;
94
+ cache_read_per_mtok: number;
95
+ cache_write_5m_per_mtok: number;
96
+ cache_write_1h_per_mtok?: number;
97
+ }
98
+ export interface CostBreakdown {
99
+ input: number;
100
+ cacheWrite: number;
101
+ cacheRead: number;
102
+ output: number;
103
+ total: number;
104
+ /** What the same tokens would have cost with no cache at all. */
105
+ withoutCache: number;
106
+ /** Tokens with no pricing entry — surfaced, never silently zeroed. */
107
+ unpricedTokens: number;
108
+ }
109
+ /**
110
+ * Longest-prefix pricing lookup. `*` is the catch-all. Returns null when
111
+ * nothing matches — the caller must report that, not assume free.
112
+ *
113
+ * 🔒 LOCK [ABSENCE-IS-NOT-A-VERDICT] — an unpriced model is "I don't know
114
+ * what this cost", never "$0". Session 21's recurring bug shape.
115
+ */
116
+ export declare function pricingFor(model: string | null, table: ModelPricing[]): ModelPricing | null;
117
+ /**
118
+ * Value a token tally at API list prices.
119
+ *
120
+ * 🔒 LOCKED [COST-IS-NOTIONAL-ON-SUBSCRIPTION] — 2026-08-19
121
+ * ⛔ NEVER present this number as money spent, or gate anything on it alone,
122
+ * without stating the billing mode.
123
+ * WHY: this machine runs Claude Code on a Max subscription (verified:
124
+ * `subscriptionType: max`, no ANTHROPIC_API_KEY anywhere). No dollar here
125
+ * is ever debited. The figure is a VALUATION at public API rates, useful
126
+ * only to compare two approaches against each other.
127
+ * FIX: on subscription the scarce resource is CAPACITY, not money. A $75 run
128
+ * that finishes beats a $40 run that loses 13% of its agents to the usage
129
+ * window. `contextengine cost` therefore always prints volume, valued cost
130
+ * AND intensity — never one alone.
131
+ */
132
+ export declare function costOf(t: TokenTally, p: ModelPricing | null): CostBreakdown;
133
+ export declare function emptyTally(): TokenTally;
134
+ export declare function addTally(a: TokenTally, b: TokenTally): TokenTally;
135
+ export declare function totalTokens(t: TokenTally): number;
136
+ /** Tokens the model actually wrote, as a share of all tokens moved. */
137
+ export declare function outputShare(t: TokenTally): number;
138
+ /**
139
+ * cache_read / cache_write. HIGH is healthy — it means a prefix was built
140
+ * once and reused many times. LOW means the cache is being rebuilt and thrown
141
+ * away (unstable prefix, cold fan-out). This is the ratio that actually
142
+ * signals waste; a large cache_read on its own does not.
143
+ */
144
+ export declare function cacheEfficiency(t: TokenTally): number;
145
+ /** Root of Claude Code's transcript store. Env override exists for tests. */
146
+ export declare function transcriptRoot(): string;
147
+ /**
148
+ * Parse one `agent-*.jsonl`. Tolerant by design: transcripts are appended
149
+ * live and a truncated final line is normal, so unparseable lines are
150
+ * skipped rather than failing the whole run.
151
+ */
152
+ export declare function parseAgentTranscript(file: string): AgentUsage;
153
+ /**
154
+ * Terminal state of an agent, read from its own last words.
155
+ *
156
+ * 🔒 LOCKED [AGENT-REPORTED-IS-NOT-LAST-LINE] — 2026-08-19
157
+ * ⛔ NEVER decide "this agent completed" from the last LINE of the transcript.
158
+ * WHY: 2,090 of 2,310 real transcripts end on a `user` line — the tool_result
159
+ * for the agent's own final `StructuredOutput` call. Reading the last line
160
+ * classified 2,146 healthy agents as "other" and would have made
161
+ * fanout_without_canary fire on every workflow ever run.
162
+ * FIX: an agent reported if it produced a final text block, or a
163
+ * StructuredOutput call that returned success. Measured with this rule:
164
+ * 2,285 reported / 19 capacity_exhausted / 3 api_error / 2 no_report.
165
+ */
166
+ export declare function classifyStatus(lastText: string, structuredOk: boolean): AgentStatus;
167
+ export declare function isReported(s: AgentStatus): boolean;
168
+ export interface CollectOptions {
169
+ /** Only this session id (the uuid naming the transcript dir). */
170
+ session?: string;
171
+ /** Only projects whose slug contains this substring. */
172
+ project?: string;
173
+ /** Only this run id (`wf_…`). */
174
+ run?: string;
175
+ /** Ignore runs that ended before this epoch-ms. */
176
+ since?: number;
177
+ root?: string;
178
+ }
179
+ /**
180
+ * Walk the transcript store and return one RunUsage per fan-out.
181
+ *
182
+ * A "run" is a workflow directory (`subagents/workflows/wf_…`) or the loose
183
+ * `subagents/` directory of a session (Agent-tool calls). Parent-session
184
+ * transcripts are not fan-outs and are excluded — this measures the cost of
185
+ * DELEGATION, which is the thing worth deciding about before spending it.
186
+ */
187
+ export declare function collectRuns(opts?: CollectOptions): RunUsage[];
188
+ export interface RunMetrics {
189
+ agents: number;
190
+ reported: number;
191
+ capacityExhausted: number;
192
+ failed: number;
193
+ toolCalls: number;
194
+ medianToolCalls: number;
195
+ outputShare: number;
196
+ cacheEfficiency: number;
197
+ cost: CostBreakdown;
198
+ /** Agents that started before ANY sibling had reported — the un-canaried fleet. */
199
+ launchedBeforeFirstReport: number;
200
+ }
201
+ export declare function runCost(run: RunUsage, table: ModelPricing[]): CostBreakdown;
202
+ /**
203
+ * The canary count. "Run ONE unit and read its consumption before scaling"
204
+ * is only obeyed if some agent finished before the rest were launched, so
205
+ * count the agents whose start precedes the earliest sibling completion.
206
+ */
207
+ export declare function metricsFor(run: RunUsage, table: ModelPricing[]): RunMetrics;
208
+ //# sourceMappingURL=transcript-collector.d.ts.map