@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/cli.d.ts +1 -0
- package/dist/cli.js +309 -3
- package/dist/detector.d.ts +40 -1
- package/dist/detector.js +116 -0
- package/dist/hooks.d.ts +14 -0
- package/dist/hooks.js +154 -0
- package/dist/index.js +24 -1
- package/dist/policy.d.ts +114 -0
- package/dist/policy.js +90 -0
- package/dist/transcript-collector.d.ts +208 -0
- package/dist/transcript-collector.js +447 -0
- package/package.json +1 -1
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
|