@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,149 @@
1
+ /** The two persistent PATH-shim tools this before-call layer covers (Claude Code is deferred). */
2
+ export type BeforeCallTool = "codex" | "cursor";
3
+ /** The deterministic policy this layer applies — exact-duplicate context-block removal. */
4
+ export declare const BEFORE_CALL_POLICY_NAME = "repeated_context_block_dedup";
5
+ /** The honest evidence label for every pre-call figure this layer produces (both tools). */
6
+ export declare const BEFORE_CALL_EVIDENCE_LABEL = "local-estimate (pre-call)";
7
+ /** The honest reduction label — spelled out so no reader upgrades it to a saving/provider figure. */
8
+ export declare const BEFORE_CALL_REDUCTION_LABEL: string;
9
+ /**
10
+ * A duplicate context block must be at least this many characters to be worth recommending removal of.
11
+ * Conservative: tiny repeated fragments (blank lines, short markers) are never flagged — the same
12
+ * spirit as the trace-level min-duplicate thresholds. This is a size floor, NOT a scoring heuristic.
13
+ */
14
+ export declare const MIN_DUPLICATE_BLOCK_CHARS = 40;
15
+ /**
16
+ * The TOOL-AGNOSTIC result of the deterministic avoidable-context analysis (the pure core reused by
17
+ * every before-call surface — Codex/Cursor argv+stdin, and the Claude Code UserPromptSubmit hook). It
18
+ * carries counts, policy, honest labels, and the compacted input, but NO tool tag: the surface adds its
19
+ * own tag/activity framing. Deterministic + content-free-by-contract (never persist `compacted_input`).
20
+ */
21
+ export interface AvoidableContextResult {
22
+ /** True only when ≥1 exact-duplicate context block (≥ the size floor) was found in the input. */
23
+ has_avoidable_context: boolean;
24
+ /** The deterministic policy that would act (content-free identifier). */
25
+ policy: string;
26
+ /** LOCAL-ESTIMATE (chars/4) token count of the original input, pre-call. */
27
+ input_tokens_before: number;
28
+ /** LOCAL-ESTIMATE (chars/4) token count of the compacted input, pre-call. */
29
+ input_tokens_after_estimate: number;
30
+ /** `input_tokens_before - input_tokens_after_estimate` — a local-estimate delta, never a saving. */
31
+ reduction_tokens_estimate: number;
32
+ /** The honest reduction label (never billing-confirmed / provider-reported). */
33
+ reduction_label: string;
34
+ /** The honest evidence label for the figures above. */
35
+ evidence_label: string;
36
+ /** Content-free count: total blocks the input was split into. */
37
+ blocks_total: number;
38
+ /** Content-free count: how many duplicate blocks would be removed (0 when none). */
39
+ blocks_removed: number;
40
+ /**
41
+ * The compacted input, produced IN-PROCESS. Equals the original when `has_avoidable_context` is
42
+ * false. Used ONLY if the operator explicitly approves; the original is never mutated here.
43
+ */
44
+ compacted_input: string;
45
+ }
46
+ /** A tool-tagged before-call recommendation = the tool-agnostic analysis + the surface tag. */
47
+ export interface BeforeCallRecommendation extends AvoidableContextResult {
48
+ tool: BeforeCallTool;
49
+ }
50
+ /**
51
+ * The TOOL-AGNOSTIC pure core: analyze one input for avoidable duplicated context. Deterministic +
52
+ * content-free-by-contract (the caller must not persist `compacted_input`). When the input has fewer
53
+ * than two blocks, or no duplicate block meets the size floor, returns `has_avoidable_context: false`
54
+ * with `compacted_input === input` (honest no-op). Reused by every before-call surface.
55
+ */
56
+ export declare function detectAvoidableContext(input: string): AvoidableContextResult;
57
+ /**
58
+ * Analyze one input prompt for avoidable duplicated context, returning a before-call recommendation
59
+ * tagged with the tool. Thin wrapper over the tool-agnostic `detectAvoidableContext` core.
60
+ */
61
+ export declare function analyzeBeforeCall(tool: BeforeCallTool, input: string): BeforeCallRecommendation;
62
+ export interface LocatedPrompt {
63
+ /** Index into `argv` of the prompt element. */
64
+ index: number;
65
+ /** The prompt text. For `--flag=value` forms this is the value portion only. */
66
+ value: string;
67
+ /** True when the element is a `--prompt=value` / `-p=value` inline form (value after `=`). */
68
+ inline: boolean;
69
+ }
70
+ /**
71
+ * Locate the prompt element in a shim's `argv` (the args AFTER the tool name — the shim's `"$@"`).
72
+ * - Codex (`exec --json "<prompt>"`): the prompt is the LAST element, required NOT to start with `-`.
73
+ * If the last element starts with `-` (an option/flag, or an option value we cannot disambiguate),
74
+ * returns `null` (fail-closed).
75
+ * - Cursor (`-p "<prompt>"` / `--prompt "<prompt>"`, incl. `-p=…`/`--prompt=…`): the element right
76
+ * after a standalone `-p`/`--prompt`, or the value of the inline `=` form. Returns `null` when no
77
+ * such flag is present.
78
+ */
79
+ export declare function locatePromptArg(tool: BeforeCallTool, argv: string[]): LocatedPrompt | null;
80
+ /** The per-tool argv shape whitelist used to decide whether a mutation is provably safe. */
81
+ export interface SafeMutationSpec {
82
+ /**
83
+ * Flags whose FOLLOWING TOKEN (or inline `=value`) is the prompt — the ONLY tokens this layer may
84
+ * rewrite. EMPTY for both real tools today (their prompt is a bare positional). A non-empty entry is
85
+ * the single extension point that would ever make apply available.
86
+ */
87
+ promptFlags: readonly string[];
88
+ /** Non-prompt flags that CONSUME a following value token (whose value we skip, never mutate). */
89
+ valueFlags: readonly string[];
90
+ /** Boolean flags (no value). Anything not in these three sets is treated as unknown → fail-closed. */
91
+ booleanFlags: readonly string[];
92
+ /**
93
+ * EXPLICIT VERSIONED ACTIVATION (founder rule, run-11): the dormant apply path must NEVER activate
94
+ * just because a future tool version adds a flag. A non-empty `promptFlags` whitelist is honored ONLY
95
+ * when this field pins the exact tool version whose `--help` surface was verified to expose that
96
+ * value-taking prompt flag. Absent/empty → apply stays fail-closed even if `promptFlags` is populated.
97
+ * Activating a tool therefore requires ALL of: (1) a whitelist entry, (2) this pinned version, (3)
98
+ * safe-form tests, (4) flipping the dormant-guard assertion in before-call-dormant-guard.test.ts.
99
+ */
100
+ verifiedToolVersion?: string;
101
+ }
102
+ /**
103
+ * Real per-tool specs. The load-bearing safety fact is `promptFlags: []` for BOTH tools — this alone
104
+ * guarantees `resolveSafeMutation` fail-closes for every real invocation. The value/boolean tables are
105
+ * only ever consulted on the (currently unreachable) whitelist path and are kept for documentation and
106
+ * for the day a real prompt flag appears.
107
+ */
108
+ export declare const SAFE_MUTATION_SPECS: Readonly<Record<BeforeCallTool, SafeMutationSpec>>;
109
+ /** Result of the safe-mutation gate: either a proven-safe rewrite, or a fail-closed reason. */
110
+ export type SafeMutation = {
111
+ applyAvailable: true;
112
+ tool: BeforeCallTool;
113
+ /** The prompt flag that was matched (e.g. `--prompt`). */
114
+ promptFlag: string;
115
+ /** True when the match was the inline `--prompt=value` form. */
116
+ inline: boolean;
117
+ /** Index into argv of the token that holds the prompt value (the flag index for inline forms). */
118
+ promptIndex: number;
119
+ /** The original prompt value. */
120
+ originalPrompt: string;
121
+ /** Rebuild argv with ONLY the prompt value replaced — every other token byte-for-byte + in order. */
122
+ rebuild: (newPrompt: string) => string[];
123
+ } | {
124
+ applyAvailable: false;
125
+ reason: string;
126
+ };
127
+ /**
128
+ * Decide whether the prompt in `argv` can be SAFELY and provably isolated for mutation (charter
129
+ * run-11). FAIL-CLOSED: returns `{ applyAvailable: false, reason }` unless the prompt is the value of a
130
+ * whitelisted value-taking prompt flag AND every other token is a known flag. For both real tools the
131
+ * `promptFlags` whitelist is empty, so this ALWAYS returns not-available (the caller runs the original
132
+ * unchanged). The `spec` parameter is injectable ONLY so tests can exercise the (dormant) whitelist
133
+ * path with a synthetic prompt flag — production callers always use the real, empty-prompt-flag specs.
134
+ */
135
+ export declare function resolveSafeMutation(tool: BeforeCallTool, argv: string[], spec?: SafeMutationSpec): SafeMutation;
136
+ /** Result of the stdin-boundary gate: stdin is the sole prompt (safe to mediate) or a fail-closed reason. */
137
+ export interface StdinPromptBoundary {
138
+ /** True ONLY when stdin is provably the WHOLE prompt for this argv (Codex `exec`, no positional). */
139
+ safe: boolean;
140
+ /** Honest, content-free reason (used for the recommendation / activity when not safe). */
141
+ reason: string;
142
+ }
143
+ /**
144
+ * Decide whether stdin is the SOLE, explicit prompt boundary for `argv` (charter run-12). Codex-only,
145
+ * `exec`-only, fail-closed. Reuses the Codex value/boolean flag tables (`SAFE_MUTATION_SPECS.codex`) to
146
+ * skip flag values, so a value like `-m gpt-5` is never mistaken for a positional prompt. `-` (the
147
+ * documented explicit stdin marker) counts as "stdin is the prompt", NOT as a positional.
148
+ */
149
+ export declare function resolveStdinPromptBoundary(tool: BeforeCallTool, argv: string[]): StdinPromptBoundary;
@@ -0,0 +1,358 @@
1
+ import { estimateTextTokens } from "./token-estimator.js";
2
+ import { detectWaste, getCompactedMessageIds } from "./waste-detector.js";
3
+ /** The deterministic policy this layer applies — exact-duplicate context-block removal. */
4
+ export const BEFORE_CALL_POLICY_NAME = "repeated_context_block_dedup";
5
+ /** The honest evidence label for every pre-call figure this layer produces (both tools). */
6
+ export const BEFORE_CALL_EVIDENCE_LABEL = "local-estimate (pre-call)";
7
+ /** The honest reduction label — spelled out so no reader upgrades it to a saving/provider figure. */
8
+ export const BEFORE_CALL_REDUCTION_LABEL = "local-estimate input-token delta (chars/4, pre-call); NOT billing-confirmed; NOT provider-reported " +
9
+ "(provider usage does not exist until after the call); NOT a realized saving";
10
+ /**
11
+ * A duplicate context block must be at least this many characters to be worth recommending removal of.
12
+ * Conservative: tiny repeated fragments (blank lines, short markers) are never flagged — the same
13
+ * spirit as the trace-level min-duplicate thresholds. This is a size floor, NOT a scoring heuristic.
14
+ */
15
+ export const MIN_DUPLICATE_BLOCK_CHARS = 40;
16
+ /**
17
+ * Split an input prompt into blocks on blank-line boundaries, PRESERVING the exact separators so the
18
+ * kept blocks can be rejoined byte-for-byte (minus removed duplicate blocks). `split` with a capturing
19
+ * group yields `[block0, sep0, block1, sep1, …]`: even indices are blocks, odd indices are the verbatim
20
+ * separators between them.
21
+ */
22
+ function splitPreservingSeparators(input) {
23
+ return input.split(/(\n[ \t]*\n[\s]*)/);
24
+ }
25
+ /** Shape the input blocks into a synthetic block-trace the existing waste detector understands. */
26
+ function shapeBlockTrace(blocks) {
27
+ const blockIdByIndex = new Map();
28
+ const messages = [];
29
+ for (let i = 0; i < blocks.length; i += 2) {
30
+ const id = `block-${i}`;
31
+ blockIdByIndex.set(i, id);
32
+ messages.push({ id, role: "tool", content: blocks[i], timestamp: "1970-01-01T00:00:00.000Z", toolName: "context_block" });
33
+ }
34
+ const trace = {
35
+ id: "before-call-input",
36
+ title: "before-call input",
37
+ artifactVersion: "before-call-v0",
38
+ source: "manual",
39
+ createdAt: "1970-01-01T00:00:00.000Z",
40
+ generatedAt: "1970-01-01T00:00:00.000Z",
41
+ model: "unknown",
42
+ messages
43
+ };
44
+ return { trace, blockIdByIndex };
45
+ }
46
+ /**
47
+ * The TOOL-AGNOSTIC pure core: analyze one input for avoidable duplicated context. Deterministic +
48
+ * content-free-by-contract (the caller must not persist `compacted_input`). When the input has fewer
49
+ * than two blocks, or no duplicate block meets the size floor, returns `has_avoidable_context: false`
50
+ * with `compacted_input === input` (honest no-op). Reused by every before-call surface.
51
+ */
52
+ export function detectAvoidableContext(input) {
53
+ const before = estimateTextTokens(input);
54
+ const parts = splitPreservingSeparators(input);
55
+ const blockCount = Math.ceil(parts.length / 2);
56
+ const noAvoidable = () => ({
57
+ has_avoidable_context: false,
58
+ policy: BEFORE_CALL_POLICY_NAME,
59
+ input_tokens_before: before,
60
+ input_tokens_after_estimate: before,
61
+ reduction_tokens_estimate: 0,
62
+ reduction_label: BEFORE_CALL_REDUCTION_LABEL,
63
+ evidence_label: BEFORE_CALL_EVIDENCE_LABEL,
64
+ blocks_total: blockCount,
65
+ blocks_removed: 0,
66
+ compacted_input: input
67
+ });
68
+ if (blockCount < 2)
69
+ return noAvoidable();
70
+ const { trace, blockIdByIndex } = shapeBlockTrace(parts);
71
+ const findings = detectWaste(trace, { minDuplicateCharacters: MIN_DUPLICATE_BLOCK_CHARS });
72
+ const removeIds = getCompactedMessageIds(findings);
73
+ if (removeIds.size === 0)
74
+ return noAvoidable();
75
+ // Rebuild the input from the surviving blocks. When a duplicate block is removed, also drop the
76
+ // separator that IMMEDIATELY PRECEDES it (or, for the very first block, the separator that follows
77
+ // it) so the rejoined text stays clean and byte-exact for every kept block.
78
+ const removeIndexes = new Set();
79
+ for (const [index, id] of blockIdByIndex) {
80
+ if (removeIds.has(id))
81
+ removeIndexes.add(index);
82
+ }
83
+ const keep = [];
84
+ for (let i = 0; i < parts.length; i += 1) {
85
+ const isBlock = i % 2 === 0;
86
+ if (isBlock) {
87
+ if (removeIndexes.has(i)) {
88
+ // Removing block i: also drop the separator BEFORE it (parts[i-1]) if we already emitted it,
89
+ // else the separator AFTER it (parts[i+1]) for the first block.
90
+ if (keep.length > 0 && keep[keep.length - 1] === parts[i - 1]) {
91
+ keep.pop();
92
+ }
93
+ else if (i + 1 < parts.length) {
94
+ // First-block case: skip the following separator by advancing the loop over parts[i+1].
95
+ i += 1;
96
+ }
97
+ continue;
98
+ }
99
+ keep.push(parts[i]);
100
+ }
101
+ else {
102
+ keep.push(parts[i]);
103
+ }
104
+ }
105
+ const compacted = keep.join("");
106
+ const after = estimateTextTokens(compacted);
107
+ // Defensive honesty: if the rebuild did not actually shrink the estimate, do not claim a reduction.
108
+ if (after >= before)
109
+ return noAvoidable();
110
+ return {
111
+ has_avoidable_context: true,
112
+ policy: BEFORE_CALL_POLICY_NAME,
113
+ input_tokens_before: before,
114
+ input_tokens_after_estimate: after,
115
+ reduction_tokens_estimate: before - after,
116
+ reduction_label: BEFORE_CALL_REDUCTION_LABEL,
117
+ evidence_label: BEFORE_CALL_EVIDENCE_LABEL,
118
+ blocks_total: blockCount,
119
+ blocks_removed: removeIndexes.size,
120
+ compacted_input: compacted
121
+ };
122
+ }
123
+ /**
124
+ * Analyze one input prompt for avoidable duplicated context, returning a before-call recommendation
125
+ * tagged with the tool. Thin wrapper over the tool-agnostic `detectAvoidableContext` core.
126
+ */
127
+ export function analyzeBeforeCall(tool, input) {
128
+ return { tool, ...detectAvoidableContext(input) };
129
+ }
130
+ /**
131
+ * Locate the prompt element in a shim's `argv` (the args AFTER the tool name — the shim's `"$@"`).
132
+ * - Codex (`exec --json "<prompt>"`): the prompt is the LAST element, required NOT to start with `-`.
133
+ * If the last element starts with `-` (an option/flag, or an option value we cannot disambiguate),
134
+ * returns `null` (fail-closed).
135
+ * - Cursor (`-p "<prompt>"` / `--prompt "<prompt>"`, incl. `-p=…`/`--prompt=…`): the element right
136
+ * after a standalone `-p`/`--prompt`, or the value of the inline `=` form. Returns `null` when no
137
+ * such flag is present.
138
+ */
139
+ export function locatePromptArg(tool, argv) {
140
+ if (tool === "cursor") {
141
+ for (let i = 0; i < argv.length; i += 1) {
142
+ const a = argv[i];
143
+ if (a === "-p" || a === "--prompt") {
144
+ if (i + 1 < argv.length)
145
+ return { index: i + 1, value: argv[i + 1], inline: false };
146
+ return null;
147
+ }
148
+ const inline = /^(?:-p|--prompt)=(.*)$/s.exec(a);
149
+ if (inline)
150
+ return { index: i, value: inline[1], inline: true };
151
+ }
152
+ return null;
153
+ }
154
+ // codex
155
+ if (argv.length === 0)
156
+ return null;
157
+ const lastIndex = argv.length - 1;
158
+ const last = argv[lastIndex];
159
+ if (last.startsWith("-"))
160
+ return null; // an option/flag or an ambiguous option value — never guess
161
+ return { index: lastIndex, value: last, inline: false };
162
+ }
163
+ /**
164
+ * Real per-tool specs. The load-bearing safety fact is `promptFlags: []` for BOTH tools — this alone
165
+ * guarantees `resolveSafeMutation` fail-closes for every real invocation. The value/boolean tables are
166
+ * only ever consulted on the (currently unreachable) whitelist path and are kept for documentation and
167
+ * for the day a real prompt flag appears.
168
+ */
169
+ export const SAFE_MUTATION_SPECS = {
170
+ // codex exec [OPTIONS] [PROMPT]: prompt is a BARE POSITIONAL (or stdin). `-p`/`--profile` is a
171
+ // config-profile VALUE flag, NOT the prompt. No value-taking prompt flag exists → apply never
172
+ // available.
173
+ codex: {
174
+ promptFlags: [],
175
+ valueFlags: [
176
+ "-c", "--config", "--enable", "--disable", "-i", "--image", "-m", "--model",
177
+ "--local-provider", "-p", "--profile", "-s", "--sandbox", "-C", "--cd", "--add-dir",
178
+ "--output-schema", "--color", "-o", "--output-last-message"
179
+ ],
180
+ booleanFlags: [
181
+ "exec", "--strict-config", "--oss", "--dangerously-bypass-approvals-and-sandbox",
182
+ "--dangerously-bypass-hook-trust", "--skip-git-repo-check", "--ephemeral",
183
+ "--ignore-user-config", "--ignore-rules", "--json", "-h", "--help", "-V", "--version"
184
+ ]
185
+ },
186
+ // cursor `agent [options] [prompt...]`: prompt is a VARIADIC BARE POSITIONAL. `-p`/`--print` is a
187
+ // BOOLEAN flag (print responses), NOT a prompt-value flag. No value-taking prompt flag exists → apply
188
+ // never available.
189
+ cursor: {
190
+ promptFlags: [],
191
+ valueFlags: [
192
+ "--api-key", "-H", "--header", "--output-format", "--mode", "--model", "--sandbox",
193
+ "--workspace", "--add-dir", "--plugin-dir", "--worktree-base"
194
+ ],
195
+ booleanFlags: [
196
+ "-p", "--print", "-v", "--version", "--stream-partial-output", "--plan", "--continue",
197
+ "-f", "--force", "--yolo", "--auto-review", "--approve-mcps", "--trust",
198
+ "--skip-worktree-setup", "-h", "--help"
199
+ ]
200
+ }
201
+ };
202
+ /** Match `flag=value` for any flag in `flags`; returns the flag + value, or null. */
203
+ function matchInlineFlag(token, flags) {
204
+ const eq = token.indexOf("=");
205
+ if (eq <= 0)
206
+ return null;
207
+ const flag = token.slice(0, eq);
208
+ if (!flags.has(flag))
209
+ return null;
210
+ return { flag, value: token.slice(eq + 1) };
211
+ }
212
+ /**
213
+ * Decide whether the prompt in `argv` can be SAFELY and provably isolated for mutation (charter
214
+ * run-11). FAIL-CLOSED: returns `{ applyAvailable: false, reason }` unless the prompt is the value of a
215
+ * whitelisted value-taking prompt flag AND every other token is a known flag. For both real tools the
216
+ * `promptFlags` whitelist is empty, so this ALWAYS returns not-available (the caller runs the original
217
+ * unchanged). The `spec` parameter is injectable ONLY so tests can exercise the (dormant) whitelist
218
+ * path with a synthetic prompt flag — production callers always use the real, empty-prompt-flag specs.
219
+ */
220
+ export function resolveSafeMutation(tool, argv, spec = SAFE_MUTATION_SPECS[tool]) {
221
+ const promptFlags = new Set(spec.promptFlags);
222
+ // EXPLICIT VERSIONED ACTIVATION GATE (founder rule, run-11): a non-empty whitelist is honored ONLY
223
+ // with a pinned verifiedToolVersion. This makes "a future tool version added a flag, so it just turns
224
+ // on" impossible — activation must be a deliberate, version-pinned, tested decision. Fail-closed.
225
+ if (promptFlags.size > 0 && !spec.verifiedToolVersion) {
226
+ return {
227
+ applyAvailable: false,
228
+ reason: `${tool}: a prompt-flag whitelist is set but no verifiedToolVersion is pinned — activation ` +
229
+ `requires an explicit versioned whitelist + tests (founder rule, run-11) — fail-closed`
230
+ };
231
+ }
232
+ if (promptFlags.size === 0) {
233
+ return {
234
+ applyAvailable: false,
235
+ reason: `${tool}: no value-taking prompt flag exists (verified via --help); the prompt is a bare ` +
236
+ `positional and cannot be isolated with high confidence — recommendation-only (fail-closed)`
237
+ };
238
+ }
239
+ const valueFlags = new Set(spec.valueFlags);
240
+ const booleanFlags = new Set(spec.booleanFlags);
241
+ let hit = null;
242
+ for (let i = 0; i < argv.length; i += 1) {
243
+ const a = argv[i];
244
+ const inlinePrompt = matchInlineFlag(a, promptFlags);
245
+ if (inlinePrompt) {
246
+ if (hit)
247
+ return { applyAvailable: false, reason: "multiple prompt flags — ambiguous (fail-closed)" };
248
+ hit = { flagIndex: i, valueIndex: i, inline: true, flag: inlinePrompt.flag, value: inlinePrompt.value };
249
+ continue;
250
+ }
251
+ if (promptFlags.has(a)) {
252
+ if (hit)
253
+ return { applyAvailable: false, reason: "multiple prompt flags — ambiguous (fail-closed)" };
254
+ if (i + 1 >= argv.length)
255
+ return { applyAvailable: false, reason: "prompt flag has no following value (fail-closed)" };
256
+ const v = argv[i + 1];
257
+ if (v.startsWith("-"))
258
+ return { applyAvailable: false, reason: "prompt-flag value looks like a flag — ambiguous (fail-closed)" };
259
+ hit = { flagIndex: i, valueIndex: i + 1, inline: false, flag: a, value: v };
260
+ i += 1; // skip the value token — it is the prompt, already recorded
261
+ continue;
262
+ }
263
+ if (matchInlineFlag(a, valueFlags))
264
+ continue; // known inline value flag (e.g. --model=x) — skip
265
+ if (valueFlags.has(a)) {
266
+ i += 1; // known value flag — skip ITS value token (never mutate a non-prompt value)
267
+ continue;
268
+ }
269
+ if (booleanFlags.has(a))
270
+ continue;
271
+ if (a.startsWith("-")) {
272
+ return { applyAvailable: false, reason: `unknown flag '${a}' — cannot prove argv is safe (fail-closed)` };
273
+ }
274
+ // A bare positional that is NOT a prompt-flag value: could be the prompt, a subcommand, or an
275
+ // argument — the prompt is not provably isolated. Fail-closed (this is the real-tool case).
276
+ return { applyAvailable: false, reason: "bare positional token present — prompt not provably isolated (fail-closed)" };
277
+ }
278
+ if (!hit)
279
+ return { applyAvailable: false, reason: "no prompt flag found in argv (fail-closed)" };
280
+ const matched = hit;
281
+ return {
282
+ applyAvailable: true,
283
+ tool,
284
+ promptFlag: matched.flag,
285
+ inline: matched.inline,
286
+ promptIndex: matched.valueIndex,
287
+ originalPrompt: matched.value,
288
+ rebuild: (newPrompt) => {
289
+ const out = argv.slice();
290
+ out[matched.valueIndex] = matched.inline ? `${matched.flag}=${newPrompt}` : newPrompt;
291
+ return out;
292
+ }
293
+ };
294
+ }
295
+ /**
296
+ * Decide whether stdin is the SOLE, explicit prompt boundary for `argv` (charter run-12). Codex-only,
297
+ * `exec`-only, fail-closed. Reuses the Codex value/boolean flag tables (`SAFE_MUTATION_SPECS.codex`) to
298
+ * skip flag values, so a value like `-m gpt-5` is never mistaken for a positional prompt. `-` (the
299
+ * documented explicit stdin marker) counts as "stdin is the prompt", NOT as a positional.
300
+ */
301
+ export function resolveStdinPromptBoundary(tool, argv) {
302
+ if (tool !== "codex") {
303
+ return {
304
+ safe: false,
305
+ reason: `${tool}: no documented stdin prompt boundary (verified via --help) — recommendation-only (fail-closed)`
306
+ };
307
+ }
308
+ const spec = SAFE_MUTATION_SPECS.codex;
309
+ const valueFlags = new Set(spec.valueFlags);
310
+ const booleanFlags = new Set(spec.booleanFlags);
311
+ let sawExec = false;
312
+ let positionals = 0;
313
+ let sawDash = false;
314
+ for (let i = 0; i < argv.length; i += 1) {
315
+ const a = argv[i];
316
+ if (a === "exec") {
317
+ sawExec = true;
318
+ continue;
319
+ }
320
+ if (a === "-") {
321
+ // The documented explicit stdin marker — stdin is the prompt (NOT a positional prompt, NOT a flag).
322
+ sawDash = true;
323
+ continue;
324
+ }
325
+ if (matchInlineFlag(a, valueFlags))
326
+ continue; // known inline value flag (e.g. --model=x)
327
+ if (valueFlags.has(a)) {
328
+ i += 1; // known value flag — skip ITS value token (never counted as a positional)
329
+ continue;
330
+ }
331
+ if (booleanFlags.has(a))
332
+ continue;
333
+ if (a.startsWith("-")) {
334
+ return { safe: false, reason: `unknown flag '${a}' — cannot prove the stdin boundary (fail-closed)` };
335
+ }
336
+ // A bare positional that is NOT `exec` and NOT `-`: a positional PROMPT (or a nested subcommand like
337
+ // resume/review). Either way stdin is NOT the sole prompt → the boundary is ambiguous → fail-closed.
338
+ positionals += 1;
339
+ }
340
+ if (!sawExec) {
341
+ return { safe: false, reason: "not the `codex exec` batch form — no stdin prompt boundary (fail-closed)" };
342
+ }
343
+ if (positionals > 0) {
344
+ return {
345
+ safe: false,
346
+ reason: "a positional prompt is present, so stdin is not the whole prompt (Codex would append it as a " +
347
+ "<stdin> block) — the boundary is ambiguous (fail-closed to the original)"
348
+ };
349
+ }
350
+ // No positional prompt (optionally a lone `-`): stdin is provably the ENTIRE prompt → safe to mediate.
351
+ return {
352
+ safe: true,
353
+ reason: sawDash
354
+ ? "codex exec with an explicit `-` — stdin is the whole prompt (safe boundary)"
355
+ : "codex exec with no positional prompt — stdin is the whole prompt (safe boundary)"
356
+ };
357
+ }
358
+ //# sourceMappingURL=before-call.js.map
@@ -49,6 +49,26 @@ export declare const BILLING_DELTA_CRITERION: string;
49
49
  export declare const BILLING_DELTA_INTERVAL_K = 2;
50
50
  /** Verbatim-ish anecdote note required by the accepted doc (§2.4: "a single A/B run is an anecdote, not proof"). */
51
51
  export declare const BILLING_DELTA_ANECDOTE_NOTE = "A single A/B pair is an anecdote, not proof: N=1 per arm cannot distinguish a compaction effect from run-to-run noise.";
52
+ /**
53
+ * The api_metered billing-confirmed criteria, VERBATIM from the cross-surface evidence contract
54
+ * (docs/ops/cross-surface-evidence-contract.md §5, founder decision D6). A billing-confirmed
55
+ * figure is possible ONLY on api_metered workflows, ONLY through this billing-delta flow, and
56
+ * ONLY when ALL of these hold. This module never verifies them and never emits
57
+ * `billing_confirmed: true` — the checklist is surfaced so the operator can see exactly what a
58
+ * billing-confirmed figure would require. Even then the claim is workflow-scoped only;
59
+ * generalized billing-confirmed = future only.
60
+ */
61
+ export declare const API_METERED_BILLING_CONFIRMED_CRITERIA: readonly string[];
62
+ /**
63
+ * FIXED-PLAN GUARDRAIL (cross-surface contract §4, D5 — binding, no exceptions): fixed-plan
64
+ * workflows are NEVER billing-confirmed. This flow measures API-METERED workflows only (billed
65
+ * per-run cost is its input); there is no billed per-run delta on a fixed plan, so a fixed-plan
66
+ * workflow cannot honestly produce the operator-entered billed costs this record requires — and
67
+ * regardless of what an operator enters, every `billing_confirmed` field in this module is the
68
+ * LITERAL `false` type (record, report, token delta): fixed-plan-as-billing-confirmed is
69
+ * structurally unexpressible. Enforced by the structural guards below + tests.
70
+ */
71
+ export declare const BILLING_DELTA_FIXED_PLAN_NOTE = "API-metered workflows only: a fixed plan has no billed per-run delta, so a fixed-plan workflow can NEVER be billing-confirmed \u2014 this flow cannot express it (billing_confirmed is structurally always false).";
52
72
  /** Provenance link to an apply-context approval record (the treatment context provenance), recorded by path + hash. */
53
73
  export interface TreatmentApprovalLink {
54
74
  /** Path to the apply-approval-record JSON emitted by `compaction apply-context --approve-in-workflow-use`. */
@@ -288,3 +308,11 @@ export declare function addRun(record: BillingDeltaMeasurementRecord, input: Add
288
308
  */
289
309
  export declare function computeTokenDelta(record: BillingDeltaMeasurementRecord): BillingDeltaTokenDelta;
290
310
  export declare function computeReport(record: BillingDeltaMeasurementRecord, generatedAt: string): BillingDeltaReport;
311
+ type Assert<T extends true> = T;
312
+ /** The measurement record can never carry billing_confirmed: true. */
313
+ export type StructuralGuard_RecordNeverBillingConfirmed = Assert<BillingDeltaMeasurementRecord["billing_confirmed"] extends false ? true : false>;
314
+ /** The report can never carry billing_confirmed: true. */
315
+ export type StructuralGuard_ReportNeverBillingConfirmed = Assert<BillingDeltaReport["billing_confirmed"] extends false ? true : false>;
316
+ /** The token-delta section can never carry billing_confirmed: true. */
317
+ export type StructuralGuard_TokenDeltaNeverBillingConfirmed = Assert<BillingDeltaTokenDelta["billing_confirmed"] extends false ? true : false>;
318
+ export {};
@@ -39,6 +39,32 @@ export const BILLING_DELTA_CRITERION = "delta-of-means interval = [mean_delta -
39
39
  export const BILLING_DELTA_INTERVAL_K = 2;
40
40
  /** Verbatim-ish anecdote note required by the accepted doc (§2.4: "a single A/B run is an anecdote, not proof"). */
41
41
  export const BILLING_DELTA_ANECDOTE_NOTE = "A single A/B pair is an anecdote, not proof: N=1 per arm cannot distinguish a compaction effect from run-to-run noise.";
42
+ /**
43
+ * The api_metered billing-confirmed criteria, VERBATIM from the cross-surface evidence contract
44
+ * (docs/ops/cross-surface-evidence-contract.md §5, founder decision D6). A billing-confirmed
45
+ * figure is possible ONLY on api_metered workflows, ONLY through this billing-delta flow, and
46
+ * ONLY when ALL of these hold. This module never verifies them and never emits
47
+ * `billing_confirmed: true` — the checklist is surfaced so the operator can see exactly what a
48
+ * billing-confirmed figure would require. Even then the claim is workflow-scoped only;
49
+ * generalized billing-confirmed = future only.
50
+ */
51
+ export const API_METERED_BILLING_CONFIRMED_CRITERIA = [
52
+ "**cost-isolated key / quiet window** — spend attributable to the measured workflow;",
53
+ "**same model + version**, same workflow / same input (pinned workflow identity);",
54
+ "**3+ control runs and 3+ treatment runs** (the accepted N ≥ 3 / ±2·SE-excludes-zero criterion, human-accepted 2026-06-11);",
55
+ "**provider-billed or operator-entered cost** per run (`cost_source` says which — never a price-table estimate relabeled);",
56
+ "**task outcome recorded** (a cheaper failed run is not a saving)."
57
+ ];
58
+ /**
59
+ * FIXED-PLAN GUARDRAIL (cross-surface contract §4, D5 — binding, no exceptions): fixed-plan
60
+ * workflows are NEVER billing-confirmed. This flow measures API-METERED workflows only (billed
61
+ * per-run cost is its input); there is no billed per-run delta on a fixed plan, so a fixed-plan
62
+ * workflow cannot honestly produce the operator-entered billed costs this record requires — and
63
+ * regardless of what an operator enters, every `billing_confirmed` field in this module is the
64
+ * LITERAL `false` type (record, report, token delta): fixed-plan-as-billing-confirmed is
65
+ * structurally unexpressible. Enforced by the structural guards below + tests.
66
+ */
67
+ export const BILLING_DELTA_FIXED_PLAN_NOTE = "API-metered workflows only: a fixed plan has no billed per-run delta, so a fixed-plan workflow can NEVER be billing-confirmed — this flow cannot express it (billing_confirmed is structurally always false).";
42
68
  /**
43
69
  * Claim-level labels for the token-delta section. Token-level, provider-reported — never a
44
70
  * billing claim. The label is selected by the ACTUAL sign/status of the token delta so it
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared content-free record step for the unified capture→compact→record→report flow (PUBLIC CLI/SDK).
3
+ * Design: `docs/design/unified-run-flow.md` (accepted 2026-06-28).
4
+ *
5
+ * After any tool's capture (Codex live wrapper, Claude Code capture/hook, …) this records a CONTENT-FREE
6
+ * usage event to the hosted control-plane: `provider_metadata {tool, provider, model}` + `token_usage`
7
+ * with **input and output counted SEPARATELY** and an honest **`token_source`**
8
+ * (provider-reported | local-estimate). It uploads NO trace content (metrics_only) and only runs when the
9
+ * user has configured the hosted path (URL + key) — never a default network call.
10
+ *
11
+ * Honesty rails (founder D2, 2026-06-28): the three axes stay distinct — provider-reported vs
12
+ * locally-observed/estimated vs SAVINGS. This records tokens (input + output) with their source; it does
13
+ * NOT compute or send any output-savings figure (output savings stay unavailable until the output-shaping
14
+ * policy family produces measured + eval-confirmed evidence).
15
+ */
16
+ import { type ApiConfig, type TokenSource, type ToolName } from "./api-client/index.js";
17
+ import type { UsageMetadata } from "./usage-metadata.js";
18
+ /** Map a capture's UsageMetadata to the honest wire token source. */
19
+ export declare function captureTokenSource(usage: UsageMetadata): TokenSource;
20
+ export interface CaptureRecordOptions {
21
+ usage: UsageMetadata;
22
+ tool: ToolName;
23
+ /** A local reference (e.g. captured-trace.json path) — NOT content; the body is metrics_only. */
24
+ reference: string;
25
+ }
26
+ /** Build the CONTENT-FREE (`metrics_only`) record request. Input/output sent separately; honest source. */
27
+ export declare function buildCaptureRecordRequest(opts: CaptureRecordOptions): import("./api-client/types.js").OptimizeRequest;
28
+ export type CaptureRecordResult = {
29
+ recorded: true;
30
+ tool: ToolName;
31
+ source: TokenSource;
32
+ } | {
33
+ recorded: false;
34
+ reason: string;
35
+ };
36
+ /**
37
+ * Send the content-free record. Best-effort: a transport/HTTP failure returns `recorded: false` with a
38
+ * sanitized reason (never the key, headers, or body) — it must NEVER fail the user's capture.
39
+ */
40
+ export declare function recordCaptureContentFree(config: ApiConfig, opts: CaptureRecordOptions): Promise<CaptureRecordResult>;