@compr/opscontext-mcp 2.4.2 → 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/CHANGELOG.md +51 -0
- package/dist/audit.d.ts +31 -0
- package/dist/audit.js +140 -40
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +339 -9
- 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/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,57 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
|
|
|
4
4
|
|
|
5
5
|
> Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
|
|
6
6
|
|
|
7
|
+
## [2.4.3] — 2026-08-17 — The audit verifier called concurrency "tampering" and condemned 316k records
|
|
8
|
+
|
|
9
|
+
`audit-verify` reported `❌ Audit chain BROKEN at index 2826 (of 319438)` and told the user the
|
|
10
|
+
log "was either edited after the fact, or a record was partially written during a crash… treat
|
|
11
|
+
all records from the break onward as unverified." That declared the entire SOC 2 / ISO evidence
|
|
12
|
+
base worthless — for a condition it had never actually tested.
|
|
13
|
+
|
|
14
|
+
Measured against the real 319k-record log: **0 records with altered content, 0 orphans, 66
|
|
15
|
+
concurrent-append forks.** Nothing had ever been edited. The verifier was reporting a verdict
|
|
16
|
+
about something it had not assessed — `[ABSENCE-IS-NOT-A-VERDICT]`, in the compliance feature
|
|
17
|
+
itself.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`verifyChain` now classifies instead of bailing on the first linkage mismatch**
|
|
22
|
+
(`[VERIFY-FORK-IS-NOT-TAMPER]`). Three distinct findings, previously collapsed into one:
|
|
23
|
+
| Condition | Meaning | Verdict |
|
|
24
|
+
|---|---|---|
|
|
25
|
+
| record's own hash ≠ its content | content was altered | **TAMPER** — fail |
|
|
26
|
+
| `prev_hash` names a hash absent from the log | history deleted/truncated | **ORPHAN** — fail |
|
|
27
|
+
| `prev_hash` names a *known earlier* head | two processes appended concurrently | **FORK** — warn, still valid |
|
|
28
|
+
Content is now hashed against each record's **own** `prev_hash`, so a single fork no longer
|
|
29
|
+
cascades into false tamper reports for every record after it. The report carries
|
|
30
|
+
`tamperedIndices`, `orphanIndices` and `forkIndices`, and the CLI states plainly that a forked
|
|
31
|
+
log must **not** be rewritten to linearise it — doing so destroys the evidence it exists to provide.
|
|
32
|
+
- **Head-of-chain read is now O(1)** (`[AUDIT-TAIL-READ-IS-O1]`). It read the whole log and split
|
|
33
|
+
it on every append — **215 ms on a 120 MB log, inside the append lock**, allocating a
|
|
34
|
+
319k-element array each time. Now seeks the last 64 KB. Measured **0.3 ms/append, ~700× faster**;
|
|
35
|
+
the lock is held for microseconds instead of a fifth of a second.
|
|
36
|
+
- **The in-process head cache is gone** (`[AUDIT-HEAD-FROM-DISK]`). Its safety guard compared a
|
|
37
|
+
locally *incremented* byte count against the real file size, so any divergence silently hashed
|
|
38
|
+
onto a stale head. The file's own `[audit-001-write-race]` LOCK already warned the cache was
|
|
39
|
+
"a perf optimization, NOT a correctness guarantee". With the O(1) read there is nothing left to
|
|
40
|
+
optimise, so the head is read from disk under the lock, every time.
|
|
41
|
+
|
|
42
|
+
### Honest scope
|
|
43
|
+
|
|
44
|
+
The 64 forks dated 2026-06-10 predate the append lock (added 2026-06-24, v2.1.1). Two more dated
|
|
45
|
+
2026-08-13 are **not explained** — the locked writer could not be made to fork under 16 concurrent
|
|
46
|
+
processes on either a small or a 120 MB log, and `emit-event` does use the locked path. The cache
|
|
47
|
+
removal eliminates the most plausible remaining mechanism *by construction*, but that is reasoning,
|
|
48
|
+
not a reproduction. Recorded rather than papered over.
|
|
49
|
+
|
|
50
|
+
**No log rewrite. No re-anchoring.** The forks are honest history.
|
|
51
|
+
|
|
52
|
+
### Tests
|
|
53
|
+
|
|
54
|
+
381 pass (8 new): fork-vs-tamper-vs-orphan classification, fork non-cascade, orphan/fork
|
|
55
|
+
disambiguation, cross-process head pickup, and tail-window overflow. Two existing tests asserted
|
|
56
|
+
the old prose and were re-pointed at the new classification rather than deleted.
|
|
57
|
+
|
|
7
58
|
## [2.4.2] — 2026-08-16 — Close the three holes 2.4.1 left in "a directory is not a project"
|
|
8
59
|
|
|
9
60
|
2.4.1 made `score`'s scope explicit but enforced it on only one of three entry points. An
|
package/dist/audit.d.ts
CHANGED
|
@@ -14,7 +14,38 @@ export interface IntegrityReport {
|
|
|
14
14
|
total: number;
|
|
15
15
|
breakAtIndex: number | null;
|
|
16
16
|
breakReason: string | null;
|
|
17
|
+
/** Records whose own hash does not match their content. Non-empty = TAMPERED. */
|
|
18
|
+
tamperedIndices?: number[];
|
|
19
|
+
/** Records whose prev_hash names a hash that appears nowhere earlier in the log.
|
|
20
|
+
* Indicates deletion/truncation of history — treated as tampering. */
|
|
21
|
+
orphanIndices?: number[];
|
|
22
|
+
/** Records whose prev_hash names a KNOWN earlier head — a concurrent-append fork.
|
|
23
|
+
* Content is provably intact; only the linkage is non-linear. Not tampering. */
|
|
24
|
+
forkIndices?: number[];
|
|
17
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* 🔒 LOCKED [VERIFY-FORK-IS-NOT-TAMPER] — 2026-08-17
|
|
28
|
+
* ⛔ NEVER report a forked chain as "the log was edited", and never stop at the first
|
|
29
|
+
* linkage mismatch without first checking whether any record's CONTENT is altered.
|
|
30
|
+
* WHY: the previous verifier returned on the first `prev_hash !== prev` and told the user
|
|
31
|
+
* the log "was either edited after the fact, or a record was partially written during
|
|
32
|
+
* a crash… treat all records from the break onward as unverified." On the author's log
|
|
33
|
+
* that meant declaring 316,000 records unverifiable — destroying the entire SOC 2 /
|
|
34
|
+
* ISO evidence claim — for a condition it had never actually tested. The truth, once
|
|
35
|
+
* measured: 0 of 319,461 records had an invalid self-hash (nothing was ever edited),
|
|
36
|
+
* 0 orphans (nothing was deleted), and all 66 breaks were forks where two processes
|
|
37
|
+
* read the same head and both appended.
|
|
38
|
+
* Tampering and concurrency produce DIFFERENT evidence, and conflating them is
|
|
39
|
+
* [ABSENCE-IS-NOT-A-VERDICT] applied to the compliance feature itself: the verifier
|
|
40
|
+
* reported a verdict ("edited") for something it had not assessed.
|
|
41
|
+
* FIX: classify every anomaly instead of bailing on the first.
|
|
42
|
+
* - self-hash mismatch → TAMPER (fail hard; content was altered)
|
|
43
|
+
* - prev_hash unknown → ORPHAN (fail hard; history was deleted/truncated)
|
|
44
|
+
* - prev_hash = a known earlier head → FORK (warn; concurrent append, content intact)
|
|
45
|
+
* `ok` is true when there are no tampered and no orphan records. Forks are surfaced
|
|
46
|
+
* with counts and indices so the report stays honest in both directions — it must
|
|
47
|
+
* never claim a forked log is pristine either.
|
|
48
|
+
*/
|
|
18
49
|
export declare function verifyChain(): IntegrityReport;
|
|
19
50
|
export declare function filterByRange(records: AuditRecord[], since?: string, until?: string): AuditRecord[];
|
|
20
51
|
export declare function toCsv(records: AuditRecord[]): string;
|
package/dist/audit.js
CHANGED
|
@@ -25,16 +25,18 @@
|
|
|
25
25
|
// (activation server + main MCP) reading the same prev_hash before
|
|
26
26
|
// either had flushed. The lock serializes the read-then-write
|
|
27
27
|
// window across processes.
|
|
28
|
-
// ⛔ NEVER
|
|
29
|
-
//
|
|
30
|
-
// OUR last write and OUR next read.
|
|
28
|
+
// ⛔ NEVER reintroduce an in-process head cache. STRENGTHENED 2026-08-17:
|
|
29
|
+
// the cache is GONE, not merely guarded. See [AUDIT-HEAD-FROM-DISK].
|
|
31
30
|
// WHY: audit-001-write-race documented in Session 11 SCORE.md. The
|
|
32
|
-
// in-process chain cache
|
|
33
|
-
// guarantee —
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
31
|
+
// in-process chain cache was a perf optimization, NOT a correctness
|
|
32
|
+
// guarantee — and the size-mismatch guard that was supposed to make it
|
|
33
|
+
// safe compared a locally-INCREMENTED byte count against the real file
|
|
34
|
+
// size, so any divergence silently hashed onto a stale head.
|
|
35
|
+
// The optimization is also now pointless: [AUDIT-TAIL-READ-IS-O1] made
|
|
36
|
+
// reading the true head ~0ms, down from 215ms on a 120MB log.
|
|
37
|
+
// FIX: hold the lock, read the real head from disk, append. Nothing else.
|
|
38
|
+
// Don't remove the lock, and don't add a cache back to "speed up" a
|
|
39
|
+
// path that is already O(1).
|
|
38
40
|
//
|
|
39
41
|
// Tamper-evident audit log — hash-chained JSONL at ~/.contextengine/audit.log.
|
|
40
42
|
//
|
|
@@ -44,7 +46,7 @@
|
|
|
44
46
|
// Records every state-changing operation. Each line carries the SHA-256 hash
|
|
45
47
|
// of the previous line's canonical content, so mutation of any historical
|
|
46
48
|
// record breaks chain verification at that index.
|
|
47
|
-
import { existsSync, mkdirSync, readFileSync, appendFileSync, openSync, closeSync, unlinkSync, statSync, writeSync, constants, } from "fs";
|
|
49
|
+
import { existsSync, mkdirSync, readFileSync, appendFileSync, openSync, closeSync, unlinkSync, statSync, writeSync, readSync, constants, } from "fs";
|
|
48
50
|
import { join } from "path";
|
|
49
51
|
import { homedir } from "os";
|
|
50
52
|
import { createHash } from "crypto";
|
|
@@ -132,17 +134,68 @@ function ensureDir() {
|
|
|
132
134
|
mkdirSync(dir, { recursive: true });
|
|
133
135
|
}
|
|
134
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* 🔒 LOCKED [AUDIT-TAIL-READ-IS-O1] — 2026-08-17
|
|
139
|
+
* ⛔ NEVER go back to readFileSync(whole log) + split("\n") to find the head.
|
|
140
|
+
* WHY: this ran INSIDE the append lock, so its cost was lock hold time. On the author's
|
|
141
|
+
* 319k-record / 120 MB log it measured **215 ms per append**, and it grows without
|
|
142
|
+
* bound. `acquireLockSync` force-breaks any lock older than STALE_LOCK_MS (10 s) to
|
|
143
|
+
* recover from crashed holders — which means a slow-but-perfectly-alive holder can
|
|
144
|
+
* have its lock STOLEN under load. Both processes then compute a hash from the same
|
|
145
|
+
* head and append: a forked chain. 66 forks exist in the log, all with prev_hash
|
|
146
|
+
* pointing at a known earlier head, none with tampered content.
|
|
147
|
+
* The whole-file read also allocated a 319k-element string array per append, on a
|
|
148
|
+
* path invoked once per PostToolUse hook firing.
|
|
149
|
+
* FIX: seek the tail. Read at most TAIL_READ_BYTES from the end and take the last complete
|
|
150
|
+
* line. O(1) in log size, ~0 ms, so the lock is held for microseconds and stale-break
|
|
151
|
+
* cannot fire on a live holder. Falls back to a full read only if the tail window
|
|
152
|
+
* somehow contains no complete line (pathologically long single record).
|
|
153
|
+
*/
|
|
154
|
+
const TAIL_READ_BYTES = 64 * 1024;
|
|
135
155
|
function readLastHash() {
|
|
136
156
|
const path = auditPath();
|
|
137
157
|
if (!existsSync(path))
|
|
138
158
|
return GENESIS_HASH;
|
|
159
|
+
const size = statSync(path).size;
|
|
160
|
+
if (size === 0)
|
|
161
|
+
return GENESIS_HASH;
|
|
162
|
+
const start = Math.max(0, size - TAIL_READ_BYTES);
|
|
163
|
+
let tail;
|
|
164
|
+
const fd = openSync(path, constants.O_RDONLY);
|
|
165
|
+
try {
|
|
166
|
+
const buf = Buffer.alloc(size - start);
|
|
167
|
+
readSync(fd, buf, 0, buf.length, start);
|
|
168
|
+
tail = buf.toString("utf-8");
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
closeSync(fd);
|
|
172
|
+
}
|
|
173
|
+
// Drop a leading partial line when we started mid-record.
|
|
174
|
+
if (start > 0) {
|
|
175
|
+
const nl = tail.indexOf("\n");
|
|
176
|
+
tail = nl === -1 ? "" : tail.slice(nl + 1);
|
|
177
|
+
}
|
|
178
|
+
const lines = tail.split("\n").filter(Boolean);
|
|
179
|
+
if (lines.length === 0) {
|
|
180
|
+
// Tail window held no complete record — fall back to the full read.
|
|
181
|
+
return readLastHashFullScan();
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return JSON.parse(lines[lines.length - 1]).hash;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return GENESIS_HASH;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Fallback for the pathological case: a single record longer than TAIL_READ_BYTES. */
|
|
191
|
+
function readLastHashFullScan() {
|
|
192
|
+
const path = auditPath();
|
|
139
193
|
const data = readFileSync(path, "utf-8");
|
|
140
194
|
const lines = data.split("\n").filter(Boolean);
|
|
141
195
|
if (lines.length === 0)
|
|
142
196
|
return GENESIS_HASH;
|
|
143
197
|
try {
|
|
144
|
-
|
|
145
|
-
return last.hash;
|
|
198
|
+
return JSON.parse(lines[lines.length - 1]).hash;
|
|
146
199
|
}
|
|
147
200
|
catch {
|
|
148
201
|
return GENESIS_HASH;
|
|
@@ -163,28 +216,32 @@ export function appendAudit(event, payload, actor = "system") {
|
|
|
163
216
|
const release = acquireLockSync();
|
|
164
217
|
try {
|
|
165
218
|
const path = auditPath();
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
219
|
+
// 🔒 LOCKED [AUDIT-HEAD-FROM-DISK] — 2026-08-17
|
|
220
|
+
// ⛔ NEVER derive the head hash from an in-process cache again.
|
|
221
|
+
// WHY: the previous code trusted `cachedLastHash` whenever `statSync().size` matched
|
|
222
|
+
// a locally-tracked `cachedSize` that was ARITHMETIC (`cachedSize += byteLength`),
|
|
223
|
+
// not observed. Any divergence between bytes-we-think-we-wrote and bytes-on-disk
|
|
224
|
+
// — a partial write, a concurrent writer whose bytes happened to sum the same, an
|
|
225
|
+
// externally rotated/truncated log — left us hashing onto a head that is not the
|
|
226
|
+
// real tail, forking the chain. It was a correctness guarantee resting on a
|
|
227
|
+
// perf cache, which the file's own [audit-001-write-race] LOCK explicitly warns
|
|
228
|
+
// against ("the in-process chain cache is a perf optimization, NOT a correctness
|
|
229
|
+
// guarantee").
|
|
230
|
+
// FIX: with [AUDIT-TAIL-READ-IS-O1] the true head costs ~0 ms, so there is nothing left
|
|
231
|
+
// to optimise. Read it from disk under the lock, every time. The cache is gone.
|
|
232
|
+
const prevHash = readLastHash();
|
|
174
233
|
const ts = new Date().toISOString();
|
|
175
|
-
const hash = computeHash(
|
|
234
|
+
const hash = computeHash(prevHash, ts, event, actor, payload);
|
|
176
235
|
const record = {
|
|
177
236
|
ts,
|
|
178
237
|
event,
|
|
179
238
|
actor,
|
|
180
239
|
payload,
|
|
181
|
-
prev_hash:
|
|
240
|
+
prev_hash: prevHash,
|
|
182
241
|
hash,
|
|
183
242
|
};
|
|
184
243
|
const line = JSON.stringify(record) + "\n";
|
|
185
244
|
appendFileSync(path, line);
|
|
186
|
-
cachedLastHash = hash;
|
|
187
|
-
cachedSize += Buffer.byteLength(line, "utf-8");
|
|
188
245
|
return record;
|
|
189
246
|
}
|
|
190
247
|
finally {
|
|
@@ -208,6 +265,29 @@ export function readAuditLog() {
|
|
|
208
265
|
}
|
|
209
266
|
});
|
|
210
267
|
}
|
|
268
|
+
/**
|
|
269
|
+
* 🔒 LOCKED [VERIFY-FORK-IS-NOT-TAMPER] — 2026-08-17
|
|
270
|
+
* ⛔ NEVER report a forked chain as "the log was edited", and never stop at the first
|
|
271
|
+
* linkage mismatch without first checking whether any record's CONTENT is altered.
|
|
272
|
+
* WHY: the previous verifier returned on the first `prev_hash !== prev` and told the user
|
|
273
|
+
* the log "was either edited after the fact, or a record was partially written during
|
|
274
|
+
* a crash… treat all records from the break onward as unverified." On the author's log
|
|
275
|
+
* that meant declaring 316,000 records unverifiable — destroying the entire SOC 2 /
|
|
276
|
+
* ISO evidence claim — for a condition it had never actually tested. The truth, once
|
|
277
|
+
* measured: 0 of 319,461 records had an invalid self-hash (nothing was ever edited),
|
|
278
|
+
* 0 orphans (nothing was deleted), and all 66 breaks were forks where two processes
|
|
279
|
+
* read the same head and both appended.
|
|
280
|
+
* Tampering and concurrency produce DIFFERENT evidence, and conflating them is
|
|
281
|
+
* [ABSENCE-IS-NOT-A-VERDICT] applied to the compliance feature itself: the verifier
|
|
282
|
+
* reported a verdict ("edited") for something it had not assessed.
|
|
283
|
+
* FIX: classify every anomaly instead of bailing on the first.
|
|
284
|
+
* - self-hash mismatch → TAMPER (fail hard; content was altered)
|
|
285
|
+
* - prev_hash unknown → ORPHAN (fail hard; history was deleted/truncated)
|
|
286
|
+
* - prev_hash = a known earlier head → FORK (warn; concurrent append, content intact)
|
|
287
|
+
* `ok` is true when there are no tampered and no orphan records. Forks are surfaced
|
|
288
|
+
* with counts and indices so the report stays honest in both directions — it must
|
|
289
|
+
* never claim a forked log is pristine either.
|
|
290
|
+
*/
|
|
211
291
|
export function verifyChain() {
|
|
212
292
|
let records;
|
|
213
293
|
try {
|
|
@@ -221,29 +301,49 @@ export function verifyChain() {
|
|
|
221
301
|
breakReason: e instanceof Error ? e.message : String(e),
|
|
222
302
|
};
|
|
223
303
|
}
|
|
304
|
+
const tampered = [];
|
|
305
|
+
const orphans = [];
|
|
306
|
+
const forks = [];
|
|
307
|
+
// Every hash observed so far, so a fork (parent = a known earlier head) can be told
|
|
308
|
+
// apart from an orphan (parent never existed in this log).
|
|
309
|
+
const seen = new Set([GENESIS_HASH]);
|
|
224
310
|
let prev = GENESIS_HASH;
|
|
225
311
|
for (let i = 0; i < records.length; i++) {
|
|
226
312
|
const r = records[i];
|
|
313
|
+
// 1. Content integrity — the only check that can prove tampering. Computed against
|
|
314
|
+
// the record's OWN prev_hash, so a fork does not cascade into false tamper reports
|
|
315
|
+
// for every record after it.
|
|
316
|
+
const expected = computeHash(r.prev_hash, r.ts, r.event, r.actor, r.payload);
|
|
317
|
+
if (r.hash !== expected)
|
|
318
|
+
tampered.push(i);
|
|
319
|
+
// 2. Linkage — fork vs orphan.
|
|
227
320
|
if (r.prev_hash !== prev) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
breakReason: `prev_hash mismatch at index ${i}`,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
const expected = computeHash(prev, r.ts, r.event, r.actor, r.payload);
|
|
236
|
-
if (r.hash !== expected) {
|
|
237
|
-
return {
|
|
238
|
-
ok: false,
|
|
239
|
-
total: records.length,
|
|
240
|
-
breakAtIndex: i,
|
|
241
|
-
breakReason: `hash mismatch at index ${i} (record tampered)`,
|
|
242
|
-
};
|
|
321
|
+
if (seen.has(r.prev_hash))
|
|
322
|
+
forks.push(i);
|
|
323
|
+
else
|
|
324
|
+
orphans.push(i);
|
|
243
325
|
}
|
|
326
|
+
seen.add(r.hash);
|
|
244
327
|
prev = r.hash;
|
|
245
328
|
}
|
|
246
|
-
|
|
329
|
+
const ok = tampered.length === 0 && orphans.length === 0;
|
|
330
|
+
const firstProblem = tampered.length > 0 ? tampered[0] : orphans.length > 0 ? orphans[0] : null;
|
|
331
|
+
let reason = null;
|
|
332
|
+
if (tampered.length > 0) {
|
|
333
|
+
reason = `${tampered.length} record(s) with altered content — first at index ${tampered[0]}`;
|
|
334
|
+
}
|
|
335
|
+
else if (orphans.length > 0) {
|
|
336
|
+
reason = `${orphans.length} record(s) whose parent is absent from the log (deleted or truncated history) — first at index ${orphans[0]}`;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
ok,
|
|
340
|
+
total: records.length,
|
|
341
|
+
breakAtIndex: firstProblem,
|
|
342
|
+
breakReason: reason,
|
|
343
|
+
tamperedIndices: tampered,
|
|
344
|
+
orphanIndices: orphans,
|
|
345
|
+
forkIndices: forks,
|
|
346
|
+
};
|
|
247
347
|
}
|
|
248
348
|
export function filterByRange(records, since, until) {
|
|
249
349
|
return records.filter((r) => {
|
package/dist/cli.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* contextengine save-learning Save a learning (terminal fallback for MCP)
|
|
13
13
|
* contextengine score [project|path] AI-readiness score for one project (default: cwd; --all for fleet)
|
|
14
14
|
* contextengine audit Run compliance audit across all projects
|
|
15
|
+
* contextengine cost Multi-agent token/cost/capacity report from transcripts
|
|
15
16
|
* contextengine help Show this message
|
|
16
17
|
*/
|
|
17
18
|
export {};
|