@compr/opscontext-mcp 2.4.3 → 2.5.1

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/detector.js CHANGED
@@ -333,4 +333,122 @@ export const _internal = {
333
333
  detectLoop, detectStuck, detectContextBloat, detectFabrication,
334
334
  detectDrift, detectNoInsight, detectSilentFailure, detectStaleDocSignal,
335
335
  };
336
+ import { metricsFor } from "./transcript-collector.js";
337
+ import { DEFAULT_PRICING } from "./default-pricing.js";
338
+ export const DEFAULT_COST_THRESHOLDS = {
339
+ billing_mode: "subscription",
340
+ // [DEFAULT-RATES-SHIP-WITH-THE-PACKAGE] — never [] again.
341
+ pricing: DEFAULT_PRICING,
342
+ min_cache_efficiency: 3,
343
+ max_tool_calls_per_agent: 2,
344
+ max_cost_per_agent_usd: 3,
345
+ min_fanout_for_canary: 5,
346
+ max_failed_share: 0.05,
347
+ };
348
+ function mkRun(kind, severity, reason, payload) {
349
+ // evidence is AuditRecord[]; transcript signals carry their detail in
350
+ // payload instead of inventing records that were never written.
351
+ return { kind, severity, reason, evidence: [], payload, detectedAt: Date.now() };
352
+ }
353
+ /**
354
+ * context_burn — the run is paying for context it is not reusing.
355
+ *
356
+ * Fires on cache INEFFICIENCY, tool-call inflation, or per-agent valued cost.
357
+ * Deliberately does NOT fire on a low output/volume ratio: see
358
+ * [BURN-IS-COST-WEIGHTED-NOT-VOLUME] in policy.ts.
359
+ */
360
+ export function detectContextBurn(run, t, m) {
361
+ const x = m ?? metricsFor(run, t.pricing);
362
+ if (x.agents === 0)
363
+ return null;
364
+ const reasons = [];
365
+ let severity = "warn";
366
+ const cacheWrite = run.totals.cacheWrite5m + run.totals.cacheWrite1h;
367
+ // Only meaningful once enough was written to judge reuse.
368
+ if (cacheWrite > 100_000 && x.cacheEfficiency < t.min_cache_efficiency) {
369
+ const writeShare = x.cost.total > 0 ? x.cost.cacheWrite / x.cost.total : 0;
370
+ reasons.push(`cache reused ${x.cacheEfficiency.toFixed(1)}x (floor ${t.min_cache_efficiency}x) — ` +
371
+ `${(cacheWrite / 1e6).toFixed(1)}M written vs ${(run.totals.cacheRead / 1e6).toFixed(1)}M read, ` +
372
+ `${(writeShare * 100).toFixed(0)}% of valued cost is cache WRITES`);
373
+ if (writeShare > 0.5)
374
+ severity = "critical";
375
+ }
376
+ if (x.medianToolCalls > t.max_tool_calls_per_agent) {
377
+ reasons.push(`median ${x.medianToolCalls} tool calls/agent (max ${t.max_tool_calls_per_agent}) — ` +
378
+ `agents are searching for their inputs instead of being handed them`);
379
+ }
380
+ const perAgent = x.cost.total / x.agents;
381
+ if (perAgent > t.max_cost_per_agent_usd) {
382
+ reasons.push(`$${perAgent.toFixed(2)}/agent valued (max $${t.max_cost_per_agent_usd.toFixed(2)})`);
383
+ }
384
+ if (!reasons.length)
385
+ return null;
386
+ return mkRun("context_burn", severity, `${run.runId}: ${reasons.join("; ")}`, {
387
+ runId: run.runId, project: run.project, sessionId: run.sessionId,
388
+ agents: x.agents, medianToolCalls: x.medianToolCalls,
389
+ cacheEfficiency: Number.isFinite(x.cacheEfficiency) ? x.cacheEfficiency : null,
390
+ cacheWriteTokens: cacheWrite, cacheReadTokens: run.totals.cacheRead,
391
+ outputShare: x.outputShare, costUsd: x.cost.total, costPerAgentUsd: perAgent,
392
+ billingMode: t.billing_mode, costIsNotional: t.billing_mode === "subscription",
393
+ });
394
+ }
395
+ /**
396
+ * fanout_without_canary — the fleet was launched before any single unit had
397
+ * reported, so nothing was known about per-agent consumption when the spend
398
+ * was committed.
399
+ *
400
+ * 🔒 LOCKED [CANARY-IS-A-TIME-ORDERING] — 2026-08-19
401
+ * ⛔ NEVER implement this as "no agent reported". A completed 300-agent run
402
+ * has 300 reports and was still un-canaried.
403
+ * WHY: the rule being enforced is "run ONE unit and read its consumption
404
+ * BEFORE scaling". That is a statement about ordering, not about outcomes,
405
+ * and it is only checkable by comparing each agent's start time against the
406
+ * earliest sibling completion.
407
+ * FIX: count agents that started before the first report landed. Severity
408
+ * rises when agents then died at the usage window — on a subscription that
409
+ * is the failure that actually costs something (real case: 15 of 51 agents
410
+ * lost in wf_41771d7b, 0 completed).
411
+ */
412
+ export function detectFanoutWithoutCanary(run, t, m) {
413
+ const x = m ?? metricsFor(run, t.pricing);
414
+ if (x.agents < t.min_fanout_for_canary)
415
+ return null;
416
+ if (x.launchedBeforeFirstReport < t.min_fanout_for_canary)
417
+ return null;
418
+ const failedShare = x.agents ? x.failed / x.agents : 0;
419
+ let severity = "warn";
420
+ const parts = [
421
+ `${x.launchedBeforeFirstReport} of ${x.agents} agents launched before any had reported`,
422
+ ];
423
+ if (x.capacityExhausted > 0) {
424
+ severity = "critical";
425
+ parts.push(`${x.capacityExhausted} died at the usage window (${(100 * x.capacityExhausted / x.agents).toFixed(0)}%) — ` +
426
+ `capacity spent for no result`);
427
+ }
428
+ else if (failedShare > t.max_failed_share) {
429
+ severity = "critical";
430
+ parts.push(`${x.failed}/${x.agents} returned nothing (${(failedShare * 100).toFixed(0)}%)`);
431
+ }
432
+ return mkRun("fanout_without_canary", severity, `${run.runId}: ${parts.join("; ")}`, {
433
+ runId: run.runId, project: run.project, sessionId: run.sessionId,
434
+ agents: x.agents, launchedBeforeFirstReport: x.launchedBeforeFirstReport,
435
+ reported: x.reported, failed: x.failed, capacityExhausted: x.capacityExhausted,
436
+ failedShare, costUsd: x.cost.total,
437
+ billingMode: t.billing_mode, costIsNotional: t.billing_mode === "subscription",
438
+ });
439
+ }
440
+ /** Run both transcript heuristics over a set of runs. */
441
+ export function runTranscriptHeuristics(runs, t = DEFAULT_COST_THRESHOLDS) {
442
+ const out = [];
443
+ for (const run of runs) {
444
+ const m = metricsFor(run, t.pricing);
445
+ const burn = detectContextBurn(run, t, m);
446
+ if (burn)
447
+ out.push(burn);
448
+ const fan = detectFanoutWithoutCanary(run, t, m);
449
+ if (fan)
450
+ out.push(fan);
451
+ }
452
+ return out;
453
+ }
336
454
  //# sourceMappingURL=detector.js.map
package/dist/hooks.d.ts CHANGED
@@ -137,4 +137,18 @@ export declare function formatCommitMessageViolations(violations: CommitMessageV
137
137
  export declare function formatCommitMessageViolationsJson(violations: CommitMessageViolation[]): string;
138
138
  export declare function formatSecretViolationsJson(violations: SecretViolation[]): string;
139
139
  export declare function formatDocCoverageViolationsJson(violations: DocCoverageViolation[]): string;
140
+ export interface RuleParityViolation {
141
+ severity: "block" | "warn";
142
+ ruleId: string;
143
+ marker: string;
144
+ presentIn: string[];
145
+ missingFrom: string[];
146
+ reason: "marker-missing-from-some-files" | "marker-required-but-absent-everywhere" | "file-not-found";
147
+ missingFiles?: string[];
148
+ }
149
+ export declare function runRuleParity(policy: Policy, files: StagedFile[], repoRoot: string, opts?: {
150
+ all?: boolean;
151
+ }): RuleParityViolation[];
152
+ export declare function formatRuleParityViolations(violations: RuleParityViolation[]): string;
153
+ export declare function formatRuleParityViolationsJson(violations: RuleParityViolation[]): string;
140
154
  //# sourceMappingURL=hooks.d.ts.map
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) {