@compaction/cli 0.2.0 → 0.3.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.
Files changed (153) hide show
  1. package/README.md +142 -64
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +9 -5
  10. package/dist/cli/commands/dev.d.ts +17 -0
  11. package/dist/cli/commands/dev.js +131 -0
  12. package/dist/cli/commands/gateway.d.ts +31 -0
  13. package/dist/cli/commands/gateway.js +312 -0
  14. package/dist/cli/commands/hooks.d.ts +5 -0
  15. package/dist/cli/commands/hooks.js +181 -0
  16. package/dist/cli/commands/init.js +422 -17
  17. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  18. package/dist/cli/commands/input-compaction-ab.js +125 -0
  19. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  20. package/dist/cli/commands/optimize-hosted.js +123 -0
  21. package/dist/cli/commands/optimize.js +8 -0
  22. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  23. package/dist/cli/commands/output-shaping-ab.js +132 -0
  24. package/dist/cli/commands/output-shaping.d.ts +7 -0
  25. package/dist/cli/commands/output-shaping.js +43 -0
  26. package/dist/cli/commands/policies.d.ts +2 -0
  27. package/dist/cli/commands/policies.js +80 -0
  28. package/dist/cli/commands/precall.d.ts +19 -0
  29. package/dist/cli/commands/precall.js +436 -0
  30. package/dist/cli/commands/recommend.js +1 -1
  31. package/dist/cli/commands/run.js +297 -12
  32. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  33. package/dist/cli/commands/upgrade-status.js +152 -0
  34. package/dist/cli/cursor-export-read.d.ts +13 -0
  35. package/dist/cli/cursor-export-read.js +53 -0
  36. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  37. package/dist/cli/cursor-live-preflight.js +46 -0
  38. package/dist/cli/index.js +36 -2
  39. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  40. package/dist/cli/onboarding/GatewayTui.js +113 -0
  41. package/dist/cli/onboarding/model.d.ts +32 -1
  42. package/dist/cli/onboarding/model.js +27 -2
  43. package/dist/cli/terminal-logo.d.ts +27 -0
  44. package/dist/cli/terminal-logo.js +49 -0
  45. package/dist/core/activity-event.d.ts +107 -0
  46. package/dist/core/activity-event.js +182 -0
  47. package/dist/core/activity-store.d.ts +63 -0
  48. package/dist/core/activity-store.js +254 -0
  49. package/dist/core/activity-view.d.ts +76 -0
  50. package/dist/core/activity-view.js +120 -0
  51. package/dist/core/api-client/index.d.ts +3 -1
  52. package/dist/core/api-client/index.js +4 -0
  53. package/dist/core/api-client/payload.d.ts +5 -1
  54. package/dist/core/api-client/payload.js +8 -6
  55. package/dist/core/api-client/persisted-config.d.ts +57 -0
  56. package/dist/core/api-client/persisted-config.js +156 -0
  57. package/dist/core/api-client/tool.d.ts +29 -0
  58. package/dist/core/api-client/tool.js +50 -0
  59. package/dist/core/api-client/types.d.ts +15 -4
  60. package/dist/core/auto-apply-ask.d.ts +58 -0
  61. package/dist/core/auto-apply-ask.js +105 -0
  62. package/dist/core/auto-apply-gates.d.ts +76 -0
  63. package/dist/core/auto-apply-gates.js +113 -0
  64. package/dist/core/before-call-activity.d.ts +35 -0
  65. package/dist/core/before-call-activity.js +103 -0
  66. package/dist/core/before-call-recovery.d.ts +19 -0
  67. package/dist/core/before-call-recovery.js +46 -0
  68. package/dist/core/before-call-stdin.d.ts +59 -0
  69. package/dist/core/before-call-stdin.js +78 -0
  70. package/dist/core/before-call.d.ts +149 -0
  71. package/dist/core/before-call.js +358 -0
  72. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  73. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  74. package/dist/core/capture-record.d.ts +40 -0
  75. package/dist/core/capture-record.js +55 -0
  76. package/dist/core/claude-code-before-call.d.ts +37 -0
  77. package/dist/core/claude-code-before-call.js +120 -0
  78. package/dist/core/claude-code-connect.d.ts +31 -0
  79. package/dist/core/claude-code-connect.js +87 -0
  80. package/dist/core/claude-code-hook-record.d.ts +71 -0
  81. package/dist/core/claude-code-hook-record.js +0 -0
  82. package/dist/core/claude-code-hooks.d.ts +77 -0
  83. package/dist/core/claude-code-hooks.js +141 -0
  84. package/dist/core/codex-capture.d.ts +45 -0
  85. package/dist/core/codex-capture.js +204 -0
  86. package/dist/core/command-runner.js +5 -1
  87. package/dist/core/compaction-artifacts.d.ts +51 -0
  88. package/dist/core/compaction-artifacts.js +242 -0
  89. package/dist/core/compactor.d.ts +9 -0
  90. package/dist/core/compactor.js +95 -0
  91. package/dist/core/cross-surface-event.d.ts +306 -0
  92. package/dist/core/cross-surface-event.js +330 -0
  93. package/dist/core/cursor-capture.d.ts +54 -0
  94. package/dist/core/cursor-capture.js +215 -0
  95. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  96. package/dist/core/cursor-preflight-probe.js +88 -0
  97. package/dist/core/cursor-preflight.d.ts +86 -0
  98. package/dist/core/cursor-preflight.js +126 -0
  99. package/dist/core/gateway/apply-activation.d.ts +39 -0
  100. package/dist/core/gateway/apply-activation.js +84 -0
  101. package/dist/core/gateway/apply-policy.d.ts +64 -0
  102. package/dist/core/gateway/apply-policy.js +221 -0
  103. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  104. package/dist/core/gateway/apply-receipt.js +75 -0
  105. package/dist/core/gateway/cache-proof.d.ts +45 -0
  106. package/dist/core/gateway/cache-proof.js +65 -0
  107. package/dist/core/gateway/configure.d.ts +50 -0
  108. package/dist/core/gateway/configure.js +169 -0
  109. package/dist/core/gateway/openai-usage.d.ts +56 -0
  110. package/dist/core/gateway/openai-usage.js +128 -0
  111. package/dist/core/gateway/receipt.d.ts +138 -0
  112. package/dist/core/gateway/receipt.js +120 -0
  113. package/dist/core/gateway/recovery.d.ts +23 -0
  114. package/dist/core/gateway/recovery.js +68 -0
  115. package/dist/core/gateway/server.d.ts +51 -0
  116. package/dist/core/gateway/server.js +276 -0
  117. package/dist/core/gateway/status.d.ts +45 -0
  118. package/dist/core/gateway/status.js +109 -0
  119. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  120. package/dist/core/hook-usage-aggregate.js +161 -0
  121. package/dist/core/input-compaction-ab.d.ts +111 -0
  122. package/dist/core/input-compaction-ab.js +158 -0
  123. package/dist/core/local-run-record.d.ts +109 -0
  124. package/dist/core/local-run-record.js +223 -0
  125. package/dist/core/output-shaping-ab.d.ts +140 -0
  126. package/dist/core/output-shaping-ab.js +146 -0
  127. package/dist/core/output-shaping-attach.d.ts +31 -0
  128. package/dist/core/output-shaping-attach.js +57 -0
  129. package/dist/core/output-shaping.d.ts +56 -0
  130. package/dist/core/output-shaping.js +89 -0
  131. package/dist/core/policy-middleware.d.ts +121 -0
  132. package/dist/core/policy-middleware.js +919 -0
  133. package/dist/core/policy-preferences.d.ts +99 -0
  134. package/dist/core/policy-preferences.js +232 -0
  135. package/dist/core/run-aggregator.d.ts +11 -1
  136. package/dist/core/run-aggregator.js +29 -2
  137. package/dist/core/run-flow-report.d.ts +82 -0
  138. package/dist/core/run-flow-report.js +71 -0
  139. package/dist/core/safety-report.js +8 -1
  140. package/dist/core/shim-capture-bridge.d.ts +32 -0
  141. package/dist/core/shim-capture-bridge.js +88 -0
  142. package/dist/core/skill-injection-policy.d.ts +72 -0
  143. package/dist/core/skill-injection-policy.js +183 -0
  144. package/dist/core/spend-attribution.js +1 -1
  145. package/dist/core/token-accounting.d.ts +1 -1
  146. package/dist/core/tool-shim.d.ts +129 -0
  147. package/dist/core/tool-shim.js +447 -0
  148. package/dist/core/trace-parser.d.ts +13 -13
  149. package/dist/core/trace-parser.js +6 -6
  150. package/dist/core/types.d.ts +12 -2
  151. package/dist/core/waste-detector.d.ts +20 -0
  152. package/dist/core/waste-detector.js +160 -6
  153. package/package.json +1 -1
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Shim → metrics-only activity BRIDGE (PUBLIC CLI/SDK code — engine-free, ships in the npm package).
3
+ *
4
+ * This is the exact analogue of the run-8 from-hook → activity bridge (`appendClaudeCodeHookActivity`
5
+ * in `src/cli/commands/capture-claude-code.ts`), reused verbatim in shape for the Codex/Cursor PATH
6
+ * shims (charter 2026-07-04-run-9): parse CONTENT-FREE usage → build the honest per-field token report
7
+ * → build the cross-surface event → wrap as a measure-only activity event → append ONE line to the
8
+ * local activity store. THIS is what makes a subsequent `codex exec --json` / `cursor-agent …
9
+ * --output-format json` run appear in `compaction activity` with NO manual import.
10
+ *
11
+ * Honesty (identical rails to the reused parsers/validators):
12
+ * - Codex: provider-reported tokens from `turn.completed.usage` when present, else
13
+ * unavailable-with-reason — never invented, never a silent zero.
14
+ * - Cursor: LOCAL-ESTIMATE only (chars/4); NEVER provider-reported; output unavailable-with-reason
15
+ * when the `result` field is not separable. The cross-surface tier table structurally rejects a
16
+ * provider-reported label on the `cursor` surface.
17
+ * - CONTENT-FREE: only token COUNTS + honest source labels + opaque ids ride on the event. No
18
+ * prompt/response/message text is ever read into the event (the raw output is parsed for usage
19
+ * fields only, and for Cursor the prompt is counted, never stored).
20
+ * - Measurement only: approval "not-required", auto-apply OFF (not eligible, ask-each-time,
21
+ * applied_automatically false), recovery original_retained=false, sync "local-only".
22
+ * - Deterministic id → the store DEDUPES a re-parse of the same run (same state = one event).
23
+ */
24
+ import { createHash } from "node:crypto";
25
+ import path from "node:path";
26
+ import { normalizeCodexExecEvents, CODEX_UNKNOWN_MODEL } from "./codex-capture.js";
27
+ import { normalizeCursorAgentOutput } from "./cursor-capture.js";
28
+ import { buildRunFlowTokenReport } from "./run-flow-report.js";
29
+ import { buildRunCrossSurfaceEvent } from "./cross-surface-event.js";
30
+ import { buildMeasureOnlyActivityEvent } from "./activity-event.js";
31
+ import { appendActivityEvent, DEFAULT_ACTIVITY_DIRECTORY } from "./activity-store.js";
32
+ /** A content-free, deterministic run id from opaque ids + token counts (never any content). */
33
+ function contentFreeRunId(prefix, parts) {
34
+ const digest = createHash("sha256").update(parts.map((p) => String(p ?? "")).join("|")).digest("hex");
35
+ return `${prefix}-${digest.slice(0, 16)}`;
36
+ }
37
+ /** The activity directory under a given cwd (mirrors the from-hook bridge: cwd-scoped). */
38
+ function activityDirFor(cwd) {
39
+ return cwd ? path.join(cwd, ".compaction", "activity") : DEFAULT_ACTIVITY_DIRECTORY;
40
+ }
41
+ const CODEX_MISSING_USAGE_REASON = "the captured codex exec output carried no turn.completed.usage block (provider usage unavailable — not invented)";
42
+ /**
43
+ * Bridge a captured `codex exec --json` output copy into ONE metrics-only activity event.
44
+ * `surface: "codex"`, `provider: "openai"`; provider-reported tokens where usage existed, else
45
+ * unavailable-with-reason. Content-free + best-effort: a local failure is returned as an unappended
46
+ * result, never thrown, so it can never break the transparent shim.
47
+ */
48
+ export async function bridgeCodexShimActivity(input) {
49
+ const norm = normalizeCodexExecEvents({ captureId: "shim", rawOutput: input.rawOutput });
50
+ const usage = norm.usageMetadata;
51
+ const present = norm.tokenMetadataStatus === "present";
52
+ const tokenReport = buildRunFlowTokenReport({ tool: "codex", usage, outputStatus: present ? "present" : "unavailable" });
53
+ const threadId = norm.trace.id && !norm.trace.id.startsWith("trace_codex_") ? norm.trace.id : undefined;
54
+ const runId = contentFreeRunId("codex-shim", [threadId, usage.input_tokens, usage.output_tokens, usage.cache_read_input_tokens]);
55
+ const event = buildRunCrossSurfaceEvent("codex", {
56
+ runId,
57
+ tokenReport,
58
+ reasons: present ? {} : { input: CODEX_MISSING_USAGE_REASON, output: CODEX_MISSING_USAGE_REASON },
59
+ ...(usage.model && usage.model !== CODEX_UNKNOWN_MODEL ? { modelLabel: usage.model } : {})
60
+ });
61
+ const eventWithSession = threadId ? { ...event, session_id: threadId } : event;
62
+ const activityEvent = buildMeasureOnlyActivityEvent(eventWithSession, { original_retained: false });
63
+ const result = await appendActivityEvent(activityEvent, activityDirFor(input.cwd));
64
+ return { result, tokenMetadataStatus: norm.tokenMetadataStatus };
65
+ }
66
+ /**
67
+ * Bridge a captured Cursor headless output copy into ONE metrics-only activity event.
68
+ * `surface: "cursor"`, `provider: "cursor"`; LOCAL-ESTIMATE only (NEVER provider-reported), output
69
+ * unavailable-with-reason when the result is not separable. `commandParts` (the ORIGINAL invocation)
70
+ * lets input be locally estimated from the prompt (counted, never stored). Content-free, best-effort.
71
+ */
72
+ export async function bridgeCursorShimActivity(input) {
73
+ const norm = normalizeCursorAgentOutput({ captureId: "shim", rawOutput: input.rawOutput, commandParts: input.commandParts });
74
+ const usage = norm.usageMetadata;
75
+ const tokenReport = buildRunFlowTokenReport({ tool: "cursor", usage, outputStatus: norm.outputStatus });
76
+ const sessionId = norm.trace.id && !norm.trace.id.startsWith("trace_cursor_") ? norm.trace.id : undefined;
77
+ const runId = contentFreeRunId("cursor-shim", [sessionId, usage.input_tokens, usage.output_tokens]);
78
+ const event = buildRunCrossSurfaceEvent("cursor", {
79
+ runId,
80
+ tokenReport,
81
+ reasons: { input: norm.inputUnavailableReason, output: norm.outputUnavailableReason }
82
+ });
83
+ const eventWithSession = sessionId ? { ...event, session_id: sessionId } : event;
84
+ const activityEvent = buildMeasureOnlyActivityEvent(eventWithSession, { original_retained: false });
85
+ const result = await appendActivityEvent(activityEvent, activityDirFor(input.cwd));
86
+ return { result, inputStatus: norm.inputStatus, outputStatus: norm.outputStatus };
87
+ }
88
+ //# sourceMappingURL=shim-capture-bridge.js.map
@@ -0,0 +1,72 @@
1
+ import { SKILL_INJECTION_POLICY_NAME, type PolicyMiddlewareStateCapsule, type SkillInjectionProvenance } from "./policy-types.js";
2
+ import type { AgentTrace, StateCapsule, TraceMessage } from "./types.js";
3
+ export { SKILL_INJECTION_POLICY_NAME };
4
+ export type { SkillInjectionProvenance };
5
+ /**
6
+ * APPROVAL-REQUIRED rung of the role:user skill-injection compaction policy.
7
+ *
8
+ * SCOPE (per docs/design/skill-injection-compaction-policy.md §1–§5, approval-required rung):
9
+ * This module builds the byte-identical, same-skill `role:user` skill-injection compaction
10
+ * candidates and their state capsules. It is the SECOND, separately-named compaction policy
11
+ * (`repeated_skill_injection_to_state_capsule`). It NEVER runs by default: `applyCompactionPolicy`
12
+ * only invokes it when `compactSkillInjections === true`, which is set ONLY on explicit approval
13
+ * in the existing approve/apply loop (optimization-approval.ts). There is NO auto-apply path.
14
+ *
15
+ * Byte-identical-only boundary (design §2): the dedup key is the report-only detector's exact
16
+ * `skill:normalizeExactContent(content)` key. Near-identical (same skill, divergent trailing
17
+ * `ARGUMENTS:`) produces a DIFFERENT key and can NEVER be compacted. No prefix/fuzzy matching.
18
+ *
19
+ * Provenance (design §5 / Q2): the capture step prefixes SUBAGENT message ids `<agentId>-<uuid>`,
20
+ * but ROOT-agent injections carry a bare UUID (no encoded agent id). So every compacted skill
21
+ * injection's capsule carries an ADDITIVE `skillInjectionProvenance` field recording the owning
22
+ * agent id (parsed from the id prefix, or "root" when absent) AND the replaced copy's trace
23
+ * position — so a consumer can reconstruct per-agent / per-position receipt even for the bare-UUID
24
+ * root copies. This is additive metadata on the capsule; it does NOT change the AgentTrace /
25
+ * TraceSource / TraceMessage schema.
26
+ */
27
+ /**
28
+ * A skill-injection compaction candidate: a later byte-identical copy to be replaced by a
29
+ * `whole_message` state capsule pointing at the FIRST copy of its `skill:exactContent` group.
30
+ */
31
+ export interface SkillInjectionCompactionCandidate {
32
+ skillName: string;
33
+ firstCopyMessage: TraceMessage;
34
+ firstCopyIndex: number;
35
+ replacedMessage: TraceMessage;
36
+ replacedIndex: number;
37
+ replacedTokens: number;
38
+ }
39
+ /**
40
+ * Recover the owning agent id of a captured message. Subagent message ids are prefixed
41
+ * `<agentId>-<uuid>` by the claude-code adapter; root-agent ids are bare UUIDs (no prefix).
42
+ * Returns "root" with recovered=false when no agent prefix is present.
43
+ */
44
+ export declare function owningAgentIdForMessage(message: TraceMessage): {
45
+ owningAgentId: string;
46
+ recoveredFromPrefix: boolean;
47
+ };
48
+ /**
49
+ * Find byte-identical, same-skill later skill-injection copies (design §1/§2). First occurrence
50
+ * of each `skill:exactContent` group is retained; each later exact-key copy is a candidate.
51
+ * Pure: returns candidates; never mutates the trace.
52
+ */
53
+ export declare function findSkillInjectionCompactionCandidates(trace: AgentTrace): SkillInjectionCompactionCandidate[];
54
+ /**
55
+ * Build `whole_message` state capsules for skill-injection candidates, mirroring the tool-output
56
+ * policy's capsule shape (`PolicyMiddlewareStateCapsule`) and reusing the same SourcePointer /
57
+ * provenance machinery. Each capsule additionally carries the §5 `skillInjectionProvenance`.
58
+ */
59
+ export declare function createSkillInjectionCapsules(trace: AgentTrace, candidates: SkillInjectionCompactionCandidate[], baseCapsule: Pick<StateCapsule, "retainedFacts" | "openQuestions" | "safetyNotes">, idOffset?: number): PolicyMiddlewareStateCapsule[];
60
+ /**
61
+ * Rollback / recovery (design §Q5): re-expand a skill-injection capsule's first-copy pointer to
62
+ * restore the original later-copy content. Because the bytes are byte-identical by construction,
63
+ * the restored content equals the retained first copy. Returns the restored content, or null when
64
+ * the pointer no longer resolves (which a safety check would already have failed).
65
+ */
66
+ export declare function restoreCompactedSkillInjectionContent(originalTrace: AgentTrace, capsule: PolicyMiddlewareStateCapsule): string | null;
67
+ /**
68
+ * Round-trip rollback (design §Q5): given the compacted messages and the skill-injection capsules,
69
+ * restore the ORIGINAL trace messages by re-expanding each capsule pointer back to the first-copy
70
+ * bytes. Asserts byte-for-byte recovery of the later copies. Pure; returns a restored message list.
71
+ */
72
+ export declare function rollbackSkillInjectionCompaction(originalTrace: AgentTrace, compactedMessages: TraceMessage[], capsules: PolicyMiddlewareStateCapsule[]): TraceMessage[];
@@ -0,0 +1,183 @@
1
+ import { contentHash } from "./content-hash.js";
2
+ import { estimateTextTokens } from "./token-estimator.js";
3
+ import { isSkillInjection, parseSkillName } from "./skill-injection-detector.js";
4
+ import { SKILL_INJECTION_POLICY_NAME } from "./policy-types.js";
5
+ // Re-export the public policy-name + provenance type (now owned by ./policy-types.js) so existing
6
+ // consumers that import them from this module keep working unchanged.
7
+ export { SKILL_INJECTION_POLICY_NAME };
8
+ /**
9
+ * Mirrors the report-only detector's normalization (trailing whitespace only). Internal
10
+ * whitespace is preserved so near-identical bodies produce DIFFERENT keys. This MUST match
11
+ * skill-injection-detector.ts `normalizeExactContent` exactly — the same hard safety boundary.
12
+ */
13
+ function normalizeExactContent(content) {
14
+ return content.replace(/\s+$/, "");
15
+ }
16
+ /**
17
+ * Recover the owning agent id of a captured message. Subagent message ids are prefixed
18
+ * `<agentId>-<uuid>` by the claude-code adapter; root-agent ids are bare UUIDs (no prefix).
19
+ * Returns "root" with recovered=false when no agent prefix is present.
20
+ */
21
+ export function owningAgentIdForMessage(message) {
22
+ // A bare UUID has the canonical 8-4-4-4-12 hex form. Anything with an extra leading
23
+ // `<prefix>-` segment before a trailing UUID is a subagent-prefixed id.
24
+ const uuid = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}";
25
+ const bareUuid = new RegExp(`^${uuid}$`);
26
+ if (bareUuid.test(message.id)) {
27
+ return { owningAgentId: "root", recoveredFromPrefix: false };
28
+ }
29
+ const prefixed = message.id.match(new RegExp(`^(.+)-${uuid}$`));
30
+ if (prefixed) {
31
+ return { owningAgentId: prefixed[1], recoveredFromPrefix: true };
32
+ }
33
+ // Non-UUID id forms (e.g. test fixtures or `<session>-<n>` ids): not an agent-encoded id.
34
+ return { owningAgentId: "root", recoveredFromPrefix: false };
35
+ }
36
+ /**
37
+ * Find byte-identical, same-skill later skill-injection copies (design §1/§2). First occurrence
38
+ * of each `skill:exactContent` group is retained; each later exact-key copy is a candidate.
39
+ * Pure: returns candidates; never mutates the trace.
40
+ */
41
+ export function findSkillInjectionCompactionCandidates(trace) {
42
+ const candidates = [];
43
+ const firstByKey = new Map();
44
+ trace.messages.forEach((message, index) => {
45
+ if (!isSkillInjection(message)) {
46
+ return;
47
+ }
48
+ const skillName = parseSkillName(message.content);
49
+ if (skillName === null) {
50
+ return;
51
+ }
52
+ const key = `${skillName}:${normalizeExactContent(message.content)}`;
53
+ const first = firstByKey.get(key);
54
+ if (!first) {
55
+ firstByKey.set(key, { message, index });
56
+ return;
57
+ }
58
+ candidates.push({
59
+ skillName,
60
+ firstCopyMessage: first.message,
61
+ firstCopyIndex: first.index,
62
+ replacedMessage: message,
63
+ replacedIndex: index,
64
+ replacedTokens: estimateTextTokens(message.content)
65
+ });
66
+ });
67
+ return candidates;
68
+ }
69
+ function sourceExcerptPreview(content) {
70
+ const normalized = content.trim().replace(/\s+/g, " ");
71
+ return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
72
+ }
73
+ function createSourcePointer(trace, message, messageIndex) {
74
+ return {
75
+ traceId: trace.id,
76
+ messageId: message.id,
77
+ messageIndex,
78
+ contentSha256: contentHash(message.content)
79
+ };
80
+ }
81
+ function createCapsuleText(skillName, sourcePointer, replacedTokens) {
82
+ return [
83
+ `[state capsule: ${SKILL_INJECTION_POLICY_NAME}]`,
84
+ `A byte-identical repeat of the ${skillName} skill injection was compacted before this model call.`,
85
+ `First-copy pointer: trace=${sourcePointer.traceId} message=${sourcePointer.messageId} index=${sourcePointer.messageIndex} sha256=${sourcePointer.contentSha256}.`,
86
+ `The original instruction body is byte-identical to the first copy and is retained locally at that pointer for audit and replay.`,
87
+ `Estimated raw duplicate input tokens replaced: ${replacedTokens}.`
88
+ ].join("\n");
89
+ }
90
+ /**
91
+ * Build `whole_message` state capsules for skill-injection candidates, mirroring the tool-output
92
+ * policy's capsule shape (`PolicyMiddlewareStateCapsule`) and reusing the same SourcePointer /
93
+ * provenance machinery. Each capsule additionally carries the §5 `skillInjectionProvenance`.
94
+ */
95
+ export function createSkillInjectionCapsules(trace, candidates, baseCapsule, idOffset = 0) {
96
+ return candidates.map((candidate, index) => {
97
+ const sourcePointer = createSourcePointer(trace, candidate.firstCopyMessage, candidate.firstCopyIndex);
98
+ const owning = owningAgentIdForMessage(candidate.replacedMessage);
99
+ const provenance = {
100
+ skillName: candidate.skillName,
101
+ owningAgentId: owning.owningAgentId,
102
+ owningAgentIdRecoveredFromPrefix: owning.recoveredFromPrefix,
103
+ replacedMessagePosition: candidate.replacedIndex,
104
+ replacedMessageId: candidate.replacedMessage.id,
105
+ firstCopyPosition: candidate.firstCopyIndex,
106
+ firstCopyMessageId: candidate.firstCopyMessage.id
107
+ };
108
+ const capsule = {
109
+ id: `skill_injection_capsule_${idOffset + index + 1}`,
110
+ // The capsule's policyName is the SECOND policy name. The shared type widens to a
111
+ // string union via SupportedCompactionPolicyName (see policy-middleware.ts).
112
+ policyName: SKILL_INJECTION_POLICY_NAME,
113
+ traceId: trace.id,
114
+ sourcePointer,
115
+ compactedMessageIds: [candidate.replacedMessage.id],
116
+ replacementMode: "whole_message",
117
+ sourceExcerptPreview: sourceExcerptPreview(candidate.firstCopyMessage.content),
118
+ originalPayloadTokenCount: estimateTextTokens(candidate.firstCopyMessage.content),
119
+ text: createCapsuleText(candidate.skillName, sourcePointer, candidate.replacedTokens),
120
+ retainedFacts: baseCapsule.retainedFacts,
121
+ openQuestions: baseCapsule.openQuestions,
122
+ safetyNotes: [
123
+ ...baseCapsule.safetyNotes,
124
+ `Byte-identical ${candidate.skillName} skill injection ${candidate.replacedMessage.id} (position ${candidate.replacedIndex}, owning agent ${provenance.owningAgentId}) replaced with a state capsule pointing at first copy ${candidate.firstCopyMessage.id}.`
125
+ ],
126
+ skillInjectionProvenance: provenance
127
+ };
128
+ return capsule;
129
+ });
130
+ }
131
+ /**
132
+ * Rollback / recovery (design §Q5): re-expand a skill-injection capsule's first-copy pointer to
133
+ * restore the original later-copy content. Because the bytes are byte-identical by construction,
134
+ * the restored content equals the retained first copy. Returns the restored content, or null when
135
+ * the pointer no longer resolves (which a safety check would already have failed).
136
+ */
137
+ export function restoreCompactedSkillInjectionContent(originalTrace, capsule) {
138
+ const pointer = capsule.sourcePointer;
139
+ const atIndex = originalTrace.messages[pointer.messageIndex];
140
+ const firstCopy = atIndex?.id === pointer.messageId
141
+ ? atIndex
142
+ : originalTrace.messages.find((message) => message.id === pointer.messageId);
143
+ if (!firstCopy) {
144
+ return null;
145
+ }
146
+ if (contentHash(firstCopy.content) !== pointer.contentSha256) {
147
+ return null;
148
+ }
149
+ return firstCopy.content;
150
+ }
151
+ /**
152
+ * Round-trip rollback (design §Q5): given the compacted messages and the skill-injection capsules,
153
+ * restore the ORIGINAL trace messages by re-expanding each capsule pointer back to the first-copy
154
+ * bytes. Asserts byte-for-byte recovery of the later copies. Pure; returns a restored message list.
155
+ */
156
+ export function rollbackSkillInjectionCompaction(originalTrace, compactedMessages, capsules) {
157
+ const restoredById = new Map();
158
+ for (const capsule of capsules) {
159
+ const restored = restoreCompactedSkillInjectionContent(originalTrace, capsule);
160
+ if (restored === null) {
161
+ continue;
162
+ }
163
+ for (const compactedId of capsule.compactedMessageIds) {
164
+ restoredById.set(compactedId, restored);
165
+ }
166
+ }
167
+ return compactedMessages.map((message) => {
168
+ const restored = restoredById.get(message.id);
169
+ if (restored === undefined) {
170
+ return message;
171
+ }
172
+ const metadata = message.metadata ? { ...message.metadata } : undefined;
173
+ if (metadata && "compaction" in metadata) {
174
+ delete metadata.compaction;
175
+ }
176
+ return {
177
+ ...message,
178
+ content: restored,
179
+ metadata: metadata && Object.keys(metadata).length > 0 ? metadata : undefined
180
+ };
181
+ });
182
+ }
183
+ //# sourceMappingURL=skill-injection-policy.js.map
@@ -39,7 +39,7 @@ function unavailableOptimizationDelta() {
39
39
  export async function loadEngineOptimizationDeltaProvider() {
40
40
  try {
41
41
  const [{ applyCompactionPolicy }, { createRecommendation }, { createSafetyReport }] = await Promise.all([
42
- import("../engine/policy-middleware.js"),
42
+ import("./policy-middleware.js"),
43
43
  import("../engine/recommendation.js"),
44
44
  import("./safety-report.js")
45
45
  ]);
@@ -1,4 +1,4 @@
1
- import type { PolicyMiddlewareResult } from "../engine/policy-middleware.js";
1
+ import type { PolicyMiddlewareResult } from "./policy-middleware.js";
2
2
  import type { AgentTrace } from "./types.js";
3
3
  /**
4
4
  * Token & cost accounting surface (Strong-MVP Track D).
@@ -0,0 +1,129 @@
1
+ /** The tools this run can shim. `cursor` installs a shim named `cursor-agent` (the installed binary). */
2
+ export type ShimTool = "codex" | "cursor";
3
+ /** Per-tool shim config: the on-disk shim name, the capture surface, and the measurable-form detector. */
4
+ interface ShimToolConfig {
5
+ /** The executable name the shim must masquerade as (what the user actually types / PATH resolves). */
6
+ shimName: string;
7
+ /** The `compaction capture <captureTool> --from-shim` subcommand the shim calls (content-free). */
8
+ captureTool: "codex" | "cursor";
9
+ /** Whether the shim forwards the ORIGINAL args to the capture bridge (Cursor needs them for the
10
+ * local-estimate INPUT count; Codex reads usage from output alone, so it does not). */
11
+ forwardArgs: boolean;
12
+ }
13
+ export declare const SHIM_TOOLS: Readonly<Record<ShimTool, ShimToolConfig>>;
14
+ /** Marker line embedded in every generated shim: how verify + uninstall confirm the file is OURS. */
15
+ export declare const SHIM_MARKER = "COMPACTION_SHIM";
16
+ export interface ShimEnv {
17
+ COMPACTION_SHIM_DIR?: string;
18
+ COMPACTION_HOME?: string;
19
+ HOME?: string;
20
+ PATH?: string;
21
+ SHELL?: string;
22
+ }
23
+ /** The Compaction home directory: `COMPACTION_HOME` or `<home>/.compaction`. */
24
+ export declare function resolveCompactionHome(env?: ShimEnv): string;
25
+ /** The Compaction-owned shim directory: `COMPACTION_SHIM_DIR`, else `<compaction-home>/shims`. */
26
+ export declare function resolveShimDir(env?: ShimEnv): string;
27
+ export declare function shimPathFor(tool: ShimTool, env?: ShimEnv): string;
28
+ /**
29
+ * First executable named `name` on `PATH`, skipping any directory in `excludeDirs` (the shim dir).
30
+ * Returns the absolute path or `undefined`. Used at INSTALL time to record the real binary before the
31
+ * shim can shadow it, and to detect "tool not installed" (never shim a command that does not exist).
32
+ */
33
+ export declare function resolveExecutableOnPath(name: string, env?: ShimEnv, excludeDirs?: string[]): string | undefined;
34
+ /**
35
+ * Generate the transparent capturing shim script for `tool`, baking in the resolved absolute
36
+ * `realBin` path. The script: (1) execs the real binary with all args + inherited stdio + preserved
37
+ * exit code; (2) for the measurable batch form ONLY, runs a FAIL-OPEN, CONTENT-FREE before-call
38
+ * recommendation step (`compaction precall`; recommendation-only — it never mutates the input), then
39
+ * tees stdout to a temp copy and calls the content-free capture bridge, then deletes the temp;
40
+ * (3) passes everything else through untouched.
41
+ */
42
+ export declare function generateShimScript(tool: ShimTool, realBin: string): string;
43
+ /** Does the file at `shimPath` look like a Compaction shim (marker present)? Content-free identity. */
44
+ export declare function fileIsCompactionShim(shimPath: string): boolean;
45
+ export interface ShimVerification {
46
+ tool: ShimTool;
47
+ /** The path the shim WOULD live at. */
48
+ shimPath: string;
49
+ /** Whether the shim FILE exists on disk (installed), independent of PATH. */
50
+ installed: boolean;
51
+ /** What the shim NAME resolves to on the CURRENT PATH (first match), or undefined. */
52
+ resolvedPath?: string;
53
+ /** TRUE only when the shim name resolves to OUR shim file on PATH — the sole basis for "active". */
54
+ onPath: boolean;
55
+ /** TRUE only when installed AND resolves to our shim on PATH. NEVER claim connected without this. */
56
+ active: boolean;
57
+ /** The exact one line to prepend to PATH to activate the shim (shown when not-yet-active). */
58
+ exportLine: string;
59
+ /** The shim directory. */
60
+ shimDir: string;
61
+ }
62
+ /** The exact PATH line the user adds (or `--write-shell-config` appends) to activate the shim dir. */
63
+ export declare function shimExportLine(env?: ShimEnv): string;
64
+ /**
65
+ * Resolve-verify whether the shim for `tool` is ACTIVE: the shim file exists AND the tool name
66
+ * resolves to that shim on the current PATH (the shim dir is on PATH ahead of the real binary).
67
+ */
68
+ export declare function verifyShimActive(tool: ShimTool, env?: ShimEnv): ShimVerification;
69
+ export type InstallShimStatus = "installed-active" | "installed-not-on-path" | "already-active" | "no-real-binary" | "verify-failed";
70
+ export interface InstallShimResult {
71
+ tool: ShimTool;
72
+ status: InstallShimStatus;
73
+ shimName: string;
74
+ shimDir: string;
75
+ shimPath: string;
76
+ /** The absolute real-binary path recorded in the shim (present unless status is no-real-binary). */
77
+ realBin?: string;
78
+ /** The resolve-verification AFTER install. */
79
+ verification: ShimVerification;
80
+ /** The exact PATH line to add when not-yet-active. */
81
+ exportLine: string;
82
+ }
83
+ export interface InstallShimOptions {
84
+ now?: () => string;
85
+ }
86
+ /**
87
+ * Install the shim for `tool`: resolve the real binary (excluding our shim dir so it can never point
88
+ * at itself), write the shim script + a real-path record, then RE-READ the shim file to confirm it
89
+ * landed with the marker and the recorded real path, and finally resolve-verify PATH activation. The
90
+ * caller may only present "connected" for `installed-active` / `already-active` — never for
91
+ * `installed-not-on-path` (print the export line) or `verify-failed` (nothing is claimed).
92
+ */
93
+ export declare function installToolShim(tool: ShimTool, env?: ShimEnv, options?: InstallShimOptions): InstallShimResult;
94
+ export type UninstallShimStatus = "removed" | "not-installed";
95
+ export interface UninstallShimResult {
96
+ tool: ShimTool;
97
+ status: UninstallShimStatus;
98
+ shimPath: string;
99
+ /** Whether a shell-rc backup was restored (see `removeShellConfigPathLine`). */
100
+ shellConfigRestored?: string;
101
+ }
102
+ /** Remove the shim file + its record entry (reversible: the real binary was never touched). */
103
+ export declare function uninstallToolShim(tool: ShimTool, env?: ShimEnv): UninstallShimResult;
104
+ /** Pick the user's shell rc file from $SHELL (zsh → ~/.zshrc, else ~/.bashrc). Content-free. */
105
+ export declare function resolveShellRcPath(env?: ShimEnv): string;
106
+ export interface WriteShellConfigResult {
107
+ status: "appended" | "already-present" | "no-rc-file-created";
108
+ rcPath: string;
109
+ backupPath?: string;
110
+ exportLine: string;
111
+ }
112
+ /**
113
+ * Append the shim PATH line to the user's shell rc — ONLY when the caller passes an explicit
114
+ * `--write-shell-config` gesture. A `.compaction.bak` backup is written FIRST (reversible), the block
115
+ * is clearly marked, and the append is idempotent (already-present → no change). Never overwrites.
116
+ */
117
+ export declare function writeShellConfigPathLine(env?: ShimEnv): WriteShellConfigResult;
118
+ export interface RemoveShellConfigResult {
119
+ status: "removed" | "not-present" | "no-rc-file";
120
+ rcPath: string;
121
+ backupRestored?: string;
122
+ }
123
+ /**
124
+ * Reverse `writeShellConfigPathLine`: strip the marked block from the rc file. If a
125
+ * `.compaction.bak` backup exists it is restored verbatim (the fully reversible path); otherwise the
126
+ * marked block is removed in place.
127
+ */
128
+ export declare function removeShellConfigPathLine(env?: ShimEnv): RemoveShellConfigResult;
129
+ export {};