@compaction/cli 0.1.4 → 0.3.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.
Files changed (170) hide show
  1. package/README.md +51 -12
  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 +24 -11
  10. package/dist/cli/commands/context.d.ts +2 -0
  11. package/dist/cli/commands/context.js +130 -0
  12. package/dist/cli/commands/dev.d.ts +17 -0
  13. package/dist/cli/commands/dev.js +131 -0
  14. package/dist/cli/commands/gateway.d.ts +31 -0
  15. package/dist/cli/commands/gateway.js +312 -0
  16. package/dist/cli/commands/hooks.d.ts +5 -0
  17. package/dist/cli/commands/hooks.js +181 -0
  18. package/dist/cli/commands/import.js +3 -1
  19. package/dist/cli/commands/init.js +422 -17
  20. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  21. package/dist/cli/commands/input-compaction-ab.js +125 -0
  22. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  23. package/dist/cli/commands/optimize-hosted.js +123 -0
  24. package/dist/cli/commands/optimize.js +8 -0
  25. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  26. package/dist/cli/commands/output-shaping-ab.js +132 -0
  27. package/dist/cli/commands/output-shaping.d.ts +7 -0
  28. package/dist/cli/commands/output-shaping.js +43 -0
  29. package/dist/cli/commands/policies.d.ts +2 -0
  30. package/dist/cli/commands/policies.js +80 -0
  31. package/dist/cli/commands/precall.d.ts +19 -0
  32. package/dist/cli/commands/precall.js +436 -0
  33. package/dist/cli/commands/recommend.js +1 -1
  34. package/dist/cli/commands/run.js +310 -18
  35. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  36. package/dist/cli/commands/upgrade-status.js +152 -0
  37. package/dist/cli/cursor-export-read.d.ts +13 -0
  38. package/dist/cli/cursor-export-read.js +53 -0
  39. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  40. package/dist/cli/cursor-live-preflight.js +46 -0
  41. package/dist/cli/index.js +39 -2
  42. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  43. package/dist/cli/onboarding/GatewayTui.js +113 -0
  44. package/dist/cli/onboarding/model.d.ts +32 -1
  45. package/dist/cli/onboarding/model.js +27 -2
  46. package/dist/cli/terminal-logo.d.ts +27 -0
  47. package/dist/cli/terminal-logo.js +49 -0
  48. package/dist/core/activity-event.d.ts +107 -0
  49. package/dist/core/activity-event.js +182 -0
  50. package/dist/core/activity-store.d.ts +63 -0
  51. package/dist/core/activity-store.js +254 -0
  52. package/dist/core/activity-view.d.ts +76 -0
  53. package/dist/core/activity-view.js +120 -0
  54. package/dist/core/api-client/index.d.ts +3 -1
  55. package/dist/core/api-client/index.js +4 -0
  56. package/dist/core/api-client/payload.d.ts +5 -1
  57. package/dist/core/api-client/payload.js +8 -6
  58. package/dist/core/api-client/persisted-config.d.ts +57 -0
  59. package/dist/core/api-client/persisted-config.js +156 -0
  60. package/dist/core/api-client/tool.d.ts +29 -0
  61. package/dist/core/api-client/tool.js +50 -0
  62. package/dist/core/api-client/types.d.ts +15 -4
  63. package/dist/core/auto-apply-ask.d.ts +58 -0
  64. package/dist/core/auto-apply-ask.js +105 -0
  65. package/dist/core/auto-apply-gates.d.ts +76 -0
  66. package/dist/core/auto-apply-gates.js +113 -0
  67. package/dist/core/before-call-activity.d.ts +35 -0
  68. package/dist/core/before-call-activity.js +103 -0
  69. package/dist/core/before-call-recovery.d.ts +19 -0
  70. package/dist/core/before-call-recovery.js +46 -0
  71. package/dist/core/before-call-stdin.d.ts +59 -0
  72. package/dist/core/before-call-stdin.js +78 -0
  73. package/dist/core/before-call.d.ts +149 -0
  74. package/dist/core/before-call.js +358 -0
  75. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  76. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  77. package/dist/core/capture-record.d.ts +40 -0
  78. package/dist/core/capture-record.js +55 -0
  79. package/dist/core/claude-code-before-call.d.ts +37 -0
  80. package/dist/core/claude-code-before-call.js +120 -0
  81. package/dist/core/claude-code-connect.d.ts +31 -0
  82. package/dist/core/claude-code-connect.js +87 -0
  83. package/dist/core/claude-code-hook-record.d.ts +71 -0
  84. package/dist/core/claude-code-hook-record.js +0 -0
  85. package/dist/core/claude-code-hooks.d.ts +77 -0
  86. package/dist/core/claude-code-hooks.js +141 -0
  87. package/dist/core/codex-capture.d.ts +45 -0
  88. package/dist/core/codex-capture.js +204 -0
  89. package/dist/core/command-runner.js +5 -1
  90. package/dist/core/compaction-artifacts.d.ts +51 -0
  91. package/dist/core/compaction-artifacts.js +242 -0
  92. package/dist/core/compactor.d.ts +9 -0
  93. package/dist/core/compactor.js +95 -0
  94. package/dist/core/context-store-eval-cases.d.ts +6 -0
  95. package/dist/core/context-store-eval-cases.js +331 -0
  96. package/dist/core/context-store-eval.d.ts +140 -0
  97. package/dist/core/context-store-eval.js +138 -0
  98. package/dist/core/context-store-fs.d.ts +73 -0
  99. package/dist/core/context-store-fs.js +157 -0
  100. package/dist/core/context-store-sufficiency.d.ts +98 -0
  101. package/dist/core/context-store-sufficiency.js +135 -0
  102. package/dist/core/context-store.d.ts +155 -0
  103. package/dist/core/context-store.js +228 -0
  104. package/dist/core/cross-surface-event.d.ts +306 -0
  105. package/dist/core/cross-surface-event.js +330 -0
  106. package/dist/core/cursor-capture.d.ts +54 -0
  107. package/dist/core/cursor-capture.js +215 -0
  108. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  109. package/dist/core/cursor-preflight-probe.js +88 -0
  110. package/dist/core/cursor-preflight.d.ts +86 -0
  111. package/dist/core/cursor-preflight.js +126 -0
  112. package/dist/core/gateway/apply-activation.d.ts +39 -0
  113. package/dist/core/gateway/apply-activation.js +84 -0
  114. package/dist/core/gateway/apply-policy.d.ts +64 -0
  115. package/dist/core/gateway/apply-policy.js +221 -0
  116. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  117. package/dist/core/gateway/apply-receipt.js +75 -0
  118. package/dist/core/gateway/cache-proof.d.ts +45 -0
  119. package/dist/core/gateway/cache-proof.js +65 -0
  120. package/dist/core/gateway/configure.d.ts +50 -0
  121. package/dist/core/gateway/configure.js +169 -0
  122. package/dist/core/gateway/openai-usage.d.ts +56 -0
  123. package/dist/core/gateway/openai-usage.js +128 -0
  124. package/dist/core/gateway/receipt.d.ts +138 -0
  125. package/dist/core/gateway/receipt.js +120 -0
  126. package/dist/core/gateway/recovery.d.ts +23 -0
  127. package/dist/core/gateway/recovery.js +68 -0
  128. package/dist/core/gateway/server.d.ts +51 -0
  129. package/dist/core/gateway/server.js +276 -0
  130. package/dist/core/gateway/status.d.ts +45 -0
  131. package/dist/core/gateway/status.js +109 -0
  132. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  133. package/dist/core/hook-usage-aggregate.js +161 -0
  134. package/dist/core/input-compaction-ab.d.ts +111 -0
  135. package/dist/core/input-compaction-ab.js +158 -0
  136. package/dist/core/local-run-record.d.ts +109 -0
  137. package/dist/core/local-run-record.js +223 -0
  138. package/dist/core/output-shaping-ab.d.ts +140 -0
  139. package/dist/core/output-shaping-ab.js +146 -0
  140. package/dist/core/output-shaping-attach.d.ts +31 -0
  141. package/dist/core/output-shaping-attach.js +57 -0
  142. package/dist/core/output-shaping.d.ts +56 -0
  143. package/dist/core/output-shaping.js +89 -0
  144. package/dist/core/policy-middleware.d.ts +121 -0
  145. package/dist/core/policy-middleware.js +919 -0
  146. package/dist/core/policy-preferences.d.ts +99 -0
  147. package/dist/core/policy-preferences.js +232 -0
  148. package/dist/core/report-generator.d.ts +19 -1
  149. package/dist/core/report-generator.js +51 -0
  150. package/dist/core/run-aggregator.d.ts +56 -1
  151. package/dist/core/run-aggregator.js +93 -2
  152. package/dist/core/run-flow-report.d.ts +82 -0
  153. package/dist/core/run-flow-report.js +71 -0
  154. package/dist/core/safety-report.js +8 -1
  155. package/dist/core/shim-capture-bridge.d.ts +32 -0
  156. package/dist/core/shim-capture-bridge.js +88 -0
  157. package/dist/core/skill-injection-policy.d.ts +72 -0
  158. package/dist/core/skill-injection-policy.js +183 -0
  159. package/dist/core/spend-attribution.js +1 -1
  160. package/dist/core/token-accounting.d.ts +1 -1
  161. package/dist/core/tool-shim.d.ts +129 -0
  162. package/dist/core/tool-shim.js +447 -0
  163. package/dist/core/trace-adapters.d.ts +8 -0
  164. package/dist/core/trace-adapters.js +4 -0
  165. package/dist/core/trace-parser.d.ts +13 -13
  166. package/dist/core/trace-parser.js +6 -6
  167. package/dist/core/types.d.ts +45 -2
  168. package/dist/core/waste-detector.d.ts +20 -0
  169. package/dist/core/waste-detector.js +160 -6
  170. package/package.json +1 -1
@@ -0,0 +1,447 @@
1
+ /**
2
+ * Transparent capturing PATH-shim infrastructure (PUBLIC CLI/SDK code — engine-free, ships in the
3
+ * npm package). This is the TRUE always-on mechanism for Codex and Cursor (charter 2026-07-04-run-9),
4
+ * mirroring the run-8 Claude Code connect→install→RE-READ→VERIFY pattern
5
+ * (`src/core/claude-code-connect.ts`): a "connected/active" claim may rest ONLY on a resolve-check —
6
+ * never on the mere act of writing a file.
7
+ *
8
+ * Mechanism (charter order: native hook → shell alias → PATH shim → else stop): a Compaction-owned
9
+ * shim directory (`~/.compaction/shims/`, overridable via `COMPACTION_SHIM_DIR` / `COMPACTION_HOME`
10
+ * for tests) holds a small bash shim named exactly like the tool (`codex`, `cursor-agent`). When that
11
+ * directory is on `PATH` AHEAD of the real binary, running the tool normally routes through the shim,
12
+ * which TRANSPARENTLY execs the REAL binary (its absolute path resolved BEFORE install and baked in so
13
+ * the shim can never recurse into itself) with ALL args, inherited stdio, and the preserved exit code.
14
+ * For the MEASURABLE batch form ONLY (`codex exec --json` / `cursor-agent … --output-format json`) it
15
+ * tees stdout to a temp copy and, after the real binary exits, invokes the CONTENT-FREE
16
+ * `compaction capture <tool> --from-shim` bridge (usage token fields only; no prompt/response/message
17
+ * text is ever read into the event). Interactive / other invocations PASS THROUGH untouched and are
18
+ * honestly NOT measured (never faked).
19
+ *
20
+ * Safety rails (charter §HARD RAILS):
21
+ * - NEVER overwrites or replaces the real `codex`/`cursor-agent` binaries.
22
+ * - NEVER edits the user's shell rc without an explicit `--write-shell-config` gesture AND a reversible
23
+ * `.bak` backup (see `writeShellConfigPathLine` / `removeShellConfigPathLine`).
24
+ * - NEVER claims active without a resolve-verification (`verifyShimActive`).
25
+ * - Local file I/O only — no network, no new dependency.
26
+ */
27
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync, copyFileSync } from "node:fs";
28
+ import { homedir } from "node:os";
29
+ import path from "node:path";
30
+ export const SHIM_TOOLS = {
31
+ codex: { shimName: "codex", captureTool: "codex", forwardArgs: false },
32
+ cursor: { shimName: "cursor-agent", captureTool: "cursor", forwardArgs: true }
33
+ };
34
+ /** Marker line embedded in every generated shim: how verify + uninstall confirm the file is OURS. */
35
+ export const SHIM_MARKER = "COMPACTION_SHIM";
36
+ /** The Compaction home directory: `COMPACTION_HOME` or `<home>/.compaction`. */
37
+ export function resolveCompactionHome(env = process.env) {
38
+ if (env.COMPACTION_HOME && env.COMPACTION_HOME.trim() !== "")
39
+ return env.COMPACTION_HOME;
40
+ const home = env.HOME && env.HOME.trim() !== "" ? env.HOME : homedir();
41
+ return path.join(home, ".compaction");
42
+ }
43
+ /** The Compaction-owned shim directory: `COMPACTION_SHIM_DIR`, else `<compaction-home>/shims`. */
44
+ export function resolveShimDir(env = process.env) {
45
+ if (env.COMPACTION_SHIM_DIR && env.COMPACTION_SHIM_DIR.trim() !== "")
46
+ return env.COMPACTION_SHIM_DIR;
47
+ return path.join(resolveCompactionHome(env), "shims");
48
+ }
49
+ export function shimPathFor(tool, env = process.env) {
50
+ return path.join(resolveShimDir(env), SHIM_TOOLS[tool].shimName);
51
+ }
52
+ /* ------------------------------------------------------------------------------------------------
53
+ * Resolve the REAL binary on the current PATH, EXCLUDING our shim dir (so the shim never records or
54
+ * execs itself). This is `which <name>` minus the shim directory.
55
+ * ---------------------------------------------------------------------------------------------- */
56
+ function isExecutableFile(file) {
57
+ try {
58
+ const st = statSync(file);
59
+ return st.isFile() && (st.mode & 0o111) !== 0;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ /**
66
+ * First executable named `name` on `PATH`, skipping any directory in `excludeDirs` (the shim dir).
67
+ * Returns the absolute path or `undefined`. Used at INSTALL time to record the real binary before the
68
+ * shim can shadow it, and to detect "tool not installed" (never shim a command that does not exist).
69
+ */
70
+ export function resolveExecutableOnPath(name, env = process.env, excludeDirs = []) {
71
+ const pathValue = env.PATH ?? "";
72
+ const excluded = new Set(excludeDirs.map((d) => path.resolve(d)));
73
+ for (const dir of pathValue.split(path.delimiter)) {
74
+ if (dir.trim() === "")
75
+ continue;
76
+ if (excluded.has(path.resolve(dir)))
77
+ continue;
78
+ const candidate = path.join(dir, name);
79
+ if (isExecutableFile(candidate))
80
+ return path.resolve(candidate);
81
+ }
82
+ return undefined;
83
+ }
84
+ /* ------------------------------------------------------------------------------------------------
85
+ * Shim script generation.
86
+ * ---------------------------------------------------------------------------------------------- */
87
+ /** Single-quote a value for safe embedding in the generated bash (no interpolation, no injection). */
88
+ function bashSingleQuote(value) {
89
+ return `'${value.replace(/'/g, `'\\''`)}'`;
90
+ }
91
+ /** The measurable-form detector snippet per tool (sets `__measurable=1` only for the batch form). */
92
+ function detectorSnippet(tool) {
93
+ if (tool === "codex") {
94
+ return [
95
+ "# Codex measurable batch form: `codex exec ... --json` (an interactive `codex` is NOT measured).",
96
+ "__has_exec=0; __has_json=0",
97
+ 'for __a in "$@"; do',
98
+ ' case "$__a" in',
99
+ " exec) __has_exec=1 ;;",
100
+ " --json) __has_json=1 ;;",
101
+ " esac",
102
+ "done",
103
+ 'if [ "$__has_exec" = "1" ] && [ "$__has_json" = "1" ]; then __measurable=1; fi'
104
+ ].join("\n");
105
+ }
106
+ // cursor
107
+ return [
108
+ "# Cursor measurable batch form: `--output-format json` (or =json / stream-json). Interactive is NOT measured.",
109
+ '__prev=""',
110
+ 'for __a in "$@"; do',
111
+ ' case "$__a" in',
112
+ " --output-format=json|--output-format=stream-json) __measurable=1 ;;",
113
+ ' json|stream-json) if [ "$__prev" = "--output-format" ]; then __measurable=1; fi ;;',
114
+ " esac",
115
+ ' __prev="$__a"',
116
+ "done"
117
+ ].join("\n");
118
+ }
119
+ /**
120
+ * STDIN-BOUNDARY apply block (charter 2026-07-04-run-12 — the FIRST real before-call mutation surface).
121
+ * ONLY Codex has a documented stdin prompt boundary (`codex exec` reads instructions from stdin when no
122
+ * positional prompt is given), so this block is emitted ONLY for the codex shim; the cursor shim stays
123
+ * byte-for-byte unchanged (recommendation-only). When stdin is PIPED, the shim buffers it, lets
124
+ * `precall` decide (safe boundary + avoidable context + explicit `/dev/tty` approval → compacted stdin
125
+ * written to a file; anything else → nothing written), and feeds the chosen stream to the real binary.
126
+ * Every failure feeds the ORIGINAL buffered stdin unchanged (fail-open); the exit code is preserved via
127
+ * PIPESTATUS. Interactive runs (stdin is a tty) skip this and use the recommendation-only path.
128
+ */
129
+ function stdinApplyBlock(tool) {
130
+ if (tool !== "codex")
131
+ return ""; // Cursor: no documented stdin prompt boundary → no mediation.
132
+ const cfg = SHIM_TOOLS[tool];
133
+ return ` # STDIN-BOUNDARY apply (Codex, run-12) — mutate the model's input ONLY here, ONLY after approval.
134
+ # Engage ONLY when stdin is PIPED (not a tty) AND an ARGV-ONLY probe confirms stdin is the whole prompt
135
+ # (no positional prompt). The probe reads NO stdin, so we never buffer (never risk blocking) unless
136
+ # stdin genuinely IS the prompt. Anything else falls through to the recommendation-only path below.
137
+ if [ ! -t 0 ] && "$COMPACTION_BIN" precall ${cfg.captureTool} --stdin-boundary-check -- "$@" </dev/null >/dev/null 2>&1; then
138
+ __ttyavail=0; if [ -t 1 ]; then __ttyavail=1; fi
139
+ __sfile="$(mktemp "\${TMPDIR:-/tmp}/compaction-${cfg.shimName}-stdin-XXXXXX" 2>/dev/null)"
140
+ __cfile="$(mktemp "\${TMPDIR:-/tmp}/compaction-${cfg.shimName}-cpct-XXXXXX" 2>/dev/null)"
141
+ if [ -n "$__sfile" ] && [ -n "$__cfile" ]; then
142
+ cat > "$__sfile"
143
+ # precall writes the compacted stdin to __cfile ONLY on explicit approval; its own stdin is
144
+ # /dev/null and its stdout is discarded (the [y/n/v] prompt goes to /dev/tty). '|| true' → the
145
+ # step can never break the tool. An empty __cfile means "feed the ORIGINAL" (the fail-closed default).
146
+ "$COMPACTION_BIN" precall ${cfg.captureTool} --interactive "$__ttyavail" --stdin-file "$__sfile" --compacted-out "$__cfile" -- "$@" </dev/null >/dev/null 2>&1 || true
147
+ __infile="$__sfile"
148
+ if [ -s "$__cfile" ]; then __infile="$__cfile"; fi
149
+ __tmp="$(mktemp "\${TMPDIR:-/tmp}/compaction-${cfg.shimName}-XXXXXX" 2>/dev/null)" || { "$REAL_BIN" "$@" < "$__infile"; __c=$?; rm -f "$__sfile" "$__cfile" >/dev/null 2>&1 || true; exit "$__c"; }
150
+ "$REAL_BIN" "$@" < "$__infile" | tee "$__tmp"
151
+ __code=\${PIPESTATUS[0]}
152
+ ${captureSnippet(tool)}
153
+ rm -f "$__tmp" "$__sfile" "$__cfile" >/dev/null 2>&1 || true
154
+ exit "$__code"
155
+ fi
156
+ rm -f "$__sfile" "$__cfile" >/dev/null 2>&1 || true
157
+ fi
158
+ `;
159
+ }
160
+ /** The content-free capture call the shim runs AFTER the real binary exits (best-effort, silenced). */
161
+ function captureSnippet(tool) {
162
+ const cfg = SHIM_TOOLS[tool];
163
+ if (cfg.forwardArgs) {
164
+ // Cursor: forward the original invocation so the bridge can LOCAL-ESTIMATE the input count
165
+ // (content-free — only the token COUNT rides on the event; the prompt text is never stored).
166
+ return `"$COMPACTION_BIN" capture ${cfg.captureTool} --from-shim "$__tmp" -- ${cfg.shimName} "$@" >/dev/null 2>&1 || true`;
167
+ }
168
+ return `"$COMPACTION_BIN" capture ${cfg.captureTool} --from-shim "$__tmp" >/dev/null 2>&1 || true`;
169
+ }
170
+ /**
171
+ * Generate the transparent capturing shim script for `tool`, baking in the resolved absolute
172
+ * `realBin` path. The script: (1) execs the real binary with all args + inherited stdio + preserved
173
+ * exit code; (2) for the measurable batch form ONLY, runs a FAIL-OPEN, CONTENT-FREE before-call
174
+ * recommendation step (`compaction precall`; recommendation-only — it never mutates the input), then
175
+ * tees stdout to a temp copy and calls the content-free capture bridge, then deletes the temp;
176
+ * (3) passes everything else through untouched.
177
+ */
178
+ export function generateShimScript(tool, realBin) {
179
+ const cfg = SHIM_TOOLS[tool];
180
+ return `#!/usr/bin/env bash
181
+ # ${SHIM_MARKER}: ${tool}
182
+ # Compaction transparent capturing shim for "${cfg.shimName}" — installed by \`compaction init --connect\`.
183
+ #
184
+ # It TRANSPARENTLY execs the REAL binary below with ALL args, inherited stdio, and the preserved exit
185
+ # code. For the MEASURABLE batch form ONLY it captures provider usage token fields as a CONTENT-FREE
186
+ # side effect (no prompt/response/message text is ever stored). Interactive / other invocations pass
187
+ # through untouched and are NOT measured (never faked).
188
+ #
189
+ # The real binary path was resolved on your PATH BEFORE this shim was installed, so the shim can never
190
+ # recurse into itself. This file NEVER replaces the real binary.
191
+ # Uninstall (reversible): compaction init --disconnect ${tool}
192
+ set -u
193
+
194
+ REAL_BIN=${bashSingleQuote(realBin)}
195
+ COMPACTION_BIN="\${COMPACTION_BIN:-compaction}"
196
+
197
+ if [ ! -x "$REAL_BIN" ]; then
198
+ echo "compaction shim: the recorded ${cfg.shimName} binary was not found at $REAL_BIN — re-run 'compaction init --connect' to re-resolve it." 1>&2
199
+ exit 127
200
+ fi
201
+
202
+ __measurable=0
203
+ ${detectorSnippet(tool)}
204
+
205
+ if [ "$__measurable" = "1" ]; then
206
+ ${stdinApplyBlock(tool)} # BEFORE-CALL recommendation (charter 2026-07-04-run-10) — RECOMMENDATION-ONLY, FAIL-OPEN, CONTENT-FREE.
207
+ # 'compaction precall' is invoked purely for its side effects: when the input carries avoidable
208
+ # duplicated context it surfaces a recommendation on /dev/tty (interactive terminals only) and records
209
+ # ONE content-free activity event. It NEVER mutates the input — the real binary below always runs on
210
+ # the ORIGINAL "$@". precall's stdin is /dev/null (so it can never steal the tool's stdin) and its
211
+ # stdout/stderr are discarded (so it can never alter the tool's output); the human recommendation goes
212
+ # to /dev/tty directly. '|| true' makes any precall failure a no-op, so this step can never break or
213
+ # block the tool. NO-TTY / piped runs (__itty=0) get NO prompt and NO mutation.
214
+ __itty=0
215
+ if [ -t 0 ] && [ -t 1 ]; then __itty=1; fi
216
+ "$COMPACTION_BIN" precall ${cfg.captureTool} --interactive "$__itty" -- "$@" </dev/null >/dev/null 2>&1 || true
217
+
218
+ __tmp="$(mktemp "\${TMPDIR:-/tmp}/compaction-${cfg.shimName}-XXXXXX" 2>/dev/null)" || exec "$REAL_BIN" "$@"
219
+ # Transparent: the user sees identical stdout via tee; stderr passes straight through; the real
220
+ # binary's exit code is preserved via PIPESTATUS (never tee's or the capture call's).
221
+ "$REAL_BIN" "$@" | tee "$__tmp"
222
+ __code=\${PIPESTATUS[0]}
223
+ # Content-free capture side effect — never touches the user's stdout/stderr or exit code.
224
+ ${captureSnippet(tool)}
225
+ rm -f "$__tmp" >/dev/null 2>&1 || true
226
+ exit "$__code"
227
+ else
228
+ exec "$REAL_BIN" "$@"
229
+ fi
230
+ `;
231
+ }
232
+ /** Does the file at `shimPath` look like a Compaction shim (marker present)? Content-free identity. */
233
+ export function fileIsCompactionShim(shimPath) {
234
+ try {
235
+ return readFileSync(shimPath, "utf8").includes(`${SHIM_MARKER}:`);
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
241
+ /** The exact PATH line the user adds (or `--write-shell-config` appends) to activate the shim dir. */
242
+ export function shimExportLine(env = process.env) {
243
+ const dir = resolveShimDir(env);
244
+ // Prefer a $HOME-relative form for the default location (matches what the user will recognize).
245
+ const home = env.HOME && env.HOME.trim() !== "" ? env.HOME : homedir();
246
+ const defaultDir = path.join(home, ".compaction", "shims");
247
+ const shown = path.resolve(dir) === path.resolve(defaultDir) ? '$HOME/.compaction/shims' : dir;
248
+ return `export PATH="${shown}:$PATH"`;
249
+ }
250
+ /**
251
+ * Resolve-verify whether the shim for `tool` is ACTIVE: the shim file exists AND the tool name
252
+ * resolves to that shim on the current PATH (the shim dir is on PATH ahead of the real binary).
253
+ */
254
+ export function verifyShimActive(tool, env = process.env) {
255
+ const shimDir = resolveShimDir(env);
256
+ const shimPath = shimPathFor(tool, env);
257
+ const installed = existsSync(shimPath) && fileIsCompactionShim(shimPath);
258
+ // Resolve the shim NAME on the FULL PATH (do NOT exclude the shim dir — we want to know whether it
259
+ // wins). onPath is true only when that first match IS our shim file.
260
+ const resolvedPath = resolveExecutableOnPath(SHIM_TOOLS[tool].shimName, env);
261
+ const onPath = installed && resolvedPath !== undefined && path.resolve(resolvedPath) === path.resolve(shimPath);
262
+ return {
263
+ tool,
264
+ shimPath,
265
+ installed,
266
+ ...(resolvedPath !== undefined ? { resolvedPath } : {}),
267
+ onPath,
268
+ active: installed && onPath,
269
+ exportLine: shimExportLine(env),
270
+ shimDir
271
+ };
272
+ }
273
+ const RECORD_FILENAME = ".shim-record.json";
274
+ function recordPath(env) {
275
+ return path.join(resolveShimDir(env), RECORD_FILENAME);
276
+ }
277
+ function readRecord(env) {
278
+ try {
279
+ const parsed = JSON.parse(readFileSync(recordPath(env), "utf8"));
280
+ if (parsed && typeof parsed === "object" && parsed.shims)
281
+ return parsed;
282
+ }
283
+ catch {
284
+ /* fall through to empty */
285
+ }
286
+ return { version: 1, shims: {} };
287
+ }
288
+ function writeRecord(env, record) {
289
+ writeFileSync(recordPath(env), `${JSON.stringify(record, null, 2)}\n`, "utf8");
290
+ }
291
+ /**
292
+ * Install the shim for `tool`: resolve the real binary (excluding our shim dir so it can never point
293
+ * at itself), write the shim script + a real-path record, then RE-READ the shim file to confirm it
294
+ * landed with the marker and the recorded real path, and finally resolve-verify PATH activation. The
295
+ * caller may only present "connected" for `installed-active` / `already-active` — never for
296
+ * `installed-not-on-path` (print the export line) or `verify-failed` (nothing is claimed).
297
+ */
298
+ export function installToolShim(tool, env = process.env, options = {}) {
299
+ const now = options.now ?? (() => new Date().toISOString());
300
+ const shimDir = resolveShimDir(env);
301
+ const shimPath = shimPathFor(tool, env);
302
+ const shimName = SHIM_TOOLS[tool].shimName;
303
+ const exportLine = shimExportLine(env);
304
+ // Idempotent: already installed + active → report without rewriting.
305
+ const before = verifyShimActive(tool, env);
306
+ if (before.active) {
307
+ const record = readRecord(env);
308
+ return {
309
+ tool,
310
+ status: "already-active",
311
+ shimName,
312
+ shimDir,
313
+ shimPath,
314
+ ...(record.shims[tool]?.realBin ? { realBin: record.shims[tool].realBin } : {}),
315
+ verification: before,
316
+ exportLine
317
+ };
318
+ }
319
+ // Resolve the REAL binary BEFORE (re)writing the shim, excluding the shim dir (never itself).
320
+ const realBin = resolveExecutableOnPath(shimName, env, [shimDir]);
321
+ if (realBin === undefined) {
322
+ return {
323
+ tool,
324
+ status: "no-real-binary",
325
+ shimName,
326
+ shimDir,
327
+ shimPath,
328
+ verification: before,
329
+ exportLine
330
+ };
331
+ }
332
+ mkdirSync(shimDir, { recursive: true });
333
+ writeFileSync(shimPath, generateShimScript(tool, realBin), "utf8");
334
+ chmodSync(shimPath, 0o755);
335
+ const record = readRecord(env);
336
+ record.shims[tool] = { shimName, realBin, installedAt: now() };
337
+ writeRecord(env, record);
338
+ // RE-READ to verify the write landed (marker present + recorded real path baked in + executable).
339
+ let reReadOk = false;
340
+ try {
341
+ const contents = readFileSync(shimPath, "utf8");
342
+ const st = statSync(shimPath);
343
+ reReadOk = contents.includes(`${SHIM_MARKER}: ${tool}`) && contents.includes(realBin) && (st.mode & 0o111) !== 0;
344
+ }
345
+ catch {
346
+ reReadOk = false;
347
+ }
348
+ if (!reReadOk) {
349
+ return { tool, status: "verify-failed", shimName, shimDir, shimPath, realBin, verification: verifyShimActive(tool, env), exportLine };
350
+ }
351
+ const verification = verifyShimActive(tool, env);
352
+ return {
353
+ tool,
354
+ status: verification.active ? "installed-active" : "installed-not-on-path",
355
+ shimName,
356
+ shimDir,
357
+ shimPath,
358
+ realBin,
359
+ verification,
360
+ exportLine
361
+ };
362
+ }
363
+ /** Remove the shim file + its record entry (reversible: the real binary was never touched). */
364
+ export function uninstallToolShim(tool, env = process.env) {
365
+ const shimPath = shimPathFor(tool, env);
366
+ const existed = existsSync(shimPath);
367
+ if (existed)
368
+ rmSync(shimPath, { force: true });
369
+ const record = readRecord(env);
370
+ if (record.shims[tool]) {
371
+ delete record.shims[tool];
372
+ try {
373
+ writeRecord(env, record);
374
+ }
375
+ catch {
376
+ /* record dir may already be gone; not fatal */
377
+ }
378
+ }
379
+ return { tool, status: existed ? "removed" : "not-installed", shimPath };
380
+ }
381
+ /* ------------------------------------------------------------------------------------------------
382
+ * OPT-IN, reversible shell-rc editing (only with an explicit --write-shell-config gesture).
383
+ * ---------------------------------------------------------------------------------------------- */
384
+ const RC_BLOCK_START = "# >>> compaction shim PATH (added by `compaction init --connect --write-shell-config`) >>>";
385
+ const RC_BLOCK_END = "# <<< compaction shim PATH <<<";
386
+ /** Pick the user's shell rc file from $SHELL (zsh → ~/.zshrc, else ~/.bashrc). Content-free. */
387
+ export function resolveShellRcPath(env = process.env) {
388
+ const home = env.HOME && env.HOME.trim() !== "" ? env.HOME : homedir();
389
+ const shell = env.SHELL ?? "";
390
+ if (shell.includes("zsh"))
391
+ return path.join(home, ".zshrc");
392
+ return path.join(home, ".bashrc");
393
+ }
394
+ /**
395
+ * Append the shim PATH line to the user's shell rc — ONLY when the caller passes an explicit
396
+ * `--write-shell-config` gesture. A `.compaction.bak` backup is written FIRST (reversible), the block
397
+ * is clearly marked, and the append is idempotent (already-present → no change). Never overwrites.
398
+ */
399
+ export function writeShellConfigPathLine(env = process.env) {
400
+ const rcPath = resolveShellRcPath(env);
401
+ const exportLine = shimExportLine(env);
402
+ const existing = existsSync(rcPath) ? readFileSync(rcPath, "utf8") : "";
403
+ if (existing.includes(RC_BLOCK_START) || existing.includes(exportLine)) {
404
+ return { status: "already-present", rcPath, exportLine };
405
+ }
406
+ const backupPath = `${rcPath}.compaction.bak`;
407
+ let createdBackup;
408
+ if (existsSync(rcPath)) {
409
+ copyFileSync(rcPath, backupPath);
410
+ createdBackup = backupPath;
411
+ }
412
+ const block = `\n${RC_BLOCK_START}\n${exportLine}\n${RC_BLOCK_END}\n`;
413
+ writeFileSync(rcPath, existing + block, "utf8");
414
+ return {
415
+ status: existing === "" ? "no-rc-file-created" : "appended",
416
+ rcPath,
417
+ ...(createdBackup ? { backupPath: createdBackup } : {}),
418
+ exportLine
419
+ };
420
+ }
421
+ /**
422
+ * Reverse `writeShellConfigPathLine`: strip the marked block from the rc file. If a
423
+ * `.compaction.bak` backup exists it is restored verbatim (the fully reversible path); otherwise the
424
+ * marked block is removed in place.
425
+ */
426
+ export function removeShellConfigPathLine(env = process.env) {
427
+ const rcPath = resolveShellRcPath(env);
428
+ if (!existsSync(rcPath))
429
+ return { status: "no-rc-file", rcPath };
430
+ const backupPath = `${rcPath}.compaction.bak`;
431
+ const contents = readFileSync(rcPath, "utf8");
432
+ if (!contents.includes(RC_BLOCK_START))
433
+ return { status: "not-present", rcPath };
434
+ if (existsSync(backupPath)) {
435
+ copyFileSync(backupPath, rcPath);
436
+ rmSync(backupPath, { force: true });
437
+ return { status: "removed", rcPath, backupRestored: backupPath };
438
+ }
439
+ // No backup (block appended to a then-nonexistent file): remove the marked block only.
440
+ const stripped = contents.replace(new RegExp(`\\n?${escapeRegExp(RC_BLOCK_START)}[\\s\\S]*?${escapeRegExp(RC_BLOCK_END)}\\n?`, "g"), "\n");
441
+ writeFileSync(rcPath, stripped, "utf8");
442
+ return { status: "removed", rcPath };
443
+ }
444
+ function escapeRegExp(value) {
445
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
446
+ }
447
+ //# sourceMappingURL=tool-shim.js.map
@@ -19,6 +19,14 @@ export interface TraceAdapter {
19
19
  displayName: string;
20
20
  description: string;
21
21
  supportedSource: TraceAdapterSource;
22
+ /**
23
+ * Honest integration-readiness label for this source (AGENTS.md Level 0–6 +
24
+ * validation status). All import sources are Level 1 (local export/import — no live
25
+ * integration); this string also discloses how PROVEN the source is (e.g. validated
26
+ * on a synthetic fixture vs real-artifact validation still open), so `--list-sources`
27
+ * never reads as a stronger integration claim than the evidence supports.
28
+ */
29
+ readiness: string;
22
30
  limitations: string[];
23
31
  canHandle(input: unknown): boolean;
24
32
  normalize(input: unknown, options: TraceAdapterOptions): TraceAdapterResult;
@@ -504,6 +504,7 @@ export const agentTraceAdapter = {
504
504
  displayName: "Internal AgentTrace",
505
505
  description: "Validates a local file that already matches the internal AgentTrace JSON shape.",
506
506
  supportedSource: "agent-trace",
507
+ readiness: "Level 1 (local import) · canonical internal format, schema-validated",
507
508
  limitations: [
508
509
  "Only local JSON files are supported.",
509
510
  "Live provider/runtime capture is future work.",
@@ -519,6 +520,7 @@ export const messagesAdapter = {
519
520
  displayName: "Simple messages",
520
521
  description: "Normalizes a local array of role/content messages, or an object with a messages array, into AgentTrace.",
521
522
  supportedSource: "messages",
523
+ readiness: "Level 1 (local import) · generic role/content messages shape",
522
524
  limitations: [
523
525
  "Only local JSON files are supported.",
524
526
  "Messages need supported roles and string content.",
@@ -535,6 +537,7 @@ export const codexExecJsonlAdapter = {
535
537
  displayName: "Codex exec JSONL",
536
538
  description: "Normalizes a local JSONL stream produced by Codex exec --json into AgentTrace without launching Codex or calling provider APIs.",
537
539
  supportedSource: "codex-exec-jsonl",
540
+ readiness: "Level 1 (local export import) · validated end-to-end on a synthetic fixture; real-artifact validation OPEN (a real `codex exec --json` export has not yet been walked)",
538
541
  limitations: [
539
542
  "Only local user-supplied JSONL files are supported.",
540
543
  "This adapter does not launch Codex, call OpenAI APIs, scrape private data, or upload artifacts.",
@@ -564,6 +567,7 @@ export const unknownAdapter = {
564
567
  displayName: "Unknown local trace source",
565
568
  description: "Detects supported local AgentTrace and simple messages shapes before normalizing with the matching adapter.",
566
569
  supportedSource: "unknown",
570
+ readiness: "Level 1 (local import) · best-effort shape detection (AgentTrace / messages / Codex JSONL)",
567
571
  limitations: [
568
572
  "Detection is shape-based and local-file only.",
569
573
  "AgentTrace, simple messages, and Codex exec JSONL shapes are detected in v0.",
@@ -6,7 +6,7 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
6
6
  title: z.ZodString;
7
7
  artifactVersion: z.ZodOptional<z.ZodString>;
8
8
  artifact_version: z.ZodOptional<z.ZodString>;
9
- source: z.ZodOptional<z.ZodEnum<["manual", "cli_wrapper", "local_command", "demo", "real_captured", "provider_usage", "codex_import"]>>;
9
+ source: z.ZodOptional<z.ZodEnum<["manual", "cli_wrapper", "local_command", "demo", "real_captured", "provider_usage", "codex_import", "cursor_import"]>>;
10
10
  createdAt: z.ZodOptional<z.ZodString>;
11
11
  created_at: z.ZodOptional<z.ZodString>;
12
12
  generatedAt: z.ZodOptional<z.ZodString>;
@@ -20,13 +20,13 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
20
20
  }, "strip", z.ZodTypeAny, {
21
21
  command: string;
22
22
  args: string[];
23
- cwd?: string | undefined;
24
23
  shell?: string | undefined;
24
+ cwd?: string | undefined;
25
25
  }, {
26
26
  command: string;
27
27
  args: string[];
28
- cwd?: string | undefined;
29
28
  shell?: string | undefined;
29
+ cwd?: string | undefined;
30
30
  }>>;
31
31
  durationMs: z.ZodOptional<z.ZodNumber>;
32
32
  duration_ms: z.ZodOptional<z.ZodNumber>;
@@ -69,12 +69,12 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
69
69
  command?: {
70
70
  command: string;
71
71
  args: string[];
72
- cwd?: string | undefined;
73
72
  shell?: string | undefined;
73
+ cwd?: string | undefined;
74
74
  } | undefined;
75
75
  artifactVersion?: string | undefined;
76
76
  artifact_version?: string | undefined;
77
- source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | undefined;
77
+ source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import" | undefined;
78
78
  createdAt?: string | undefined;
79
79
  created_at?: string | undefined;
80
80
  generatedAt?: string | undefined;
@@ -98,12 +98,12 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
98
98
  command?: {
99
99
  command: string;
100
100
  args: string[];
101
- cwd?: string | undefined;
102
101
  shell?: string | undefined;
102
+ cwd?: string | undefined;
103
103
  } | undefined;
104
104
  artifactVersion?: string | undefined;
105
105
  artifact_version?: string | undefined;
106
- source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | undefined;
106
+ source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import" | undefined;
107
107
  createdAt?: string | undefined;
108
108
  created_at?: string | undefined;
109
109
  generatedAt?: string | undefined;
@@ -127,12 +127,12 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
127
127
  command?: {
128
128
  command: string;
129
129
  args: string[];
130
- cwd?: string | undefined;
131
130
  shell?: string | undefined;
131
+ cwd?: string | undefined;
132
132
  } | undefined;
133
133
  artifactVersion?: string | undefined;
134
134
  artifact_version?: string | undefined;
135
- source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | undefined;
135
+ source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import" | undefined;
136
136
  createdAt?: string | undefined;
137
137
  created_at?: string | undefined;
138
138
  generatedAt?: string | undefined;
@@ -156,12 +156,12 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
156
156
  command?: {
157
157
  command: string;
158
158
  args: string[];
159
- cwd?: string | undefined;
160
159
  shell?: string | undefined;
160
+ cwd?: string | undefined;
161
161
  } | undefined;
162
162
  artifactVersion?: string | undefined;
163
163
  artifact_version?: string | undefined;
164
- source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | undefined;
164
+ source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import" | undefined;
165
165
  createdAt?: string | undefined;
166
166
  created_at?: string | undefined;
167
167
  generatedAt?: string | undefined;
@@ -185,12 +185,12 @@ export declare const agentTraceSchema: z.ZodEffects<z.ZodEffects<z.ZodObject<{
185
185
  command?: {
186
186
  command: string;
187
187
  args: string[];
188
- cwd?: string | undefined;
189
188
  shell?: string | undefined;
189
+ cwd?: string | undefined;
190
190
  } | undefined;
191
191
  artifactVersion?: string | undefined;
192
192
  artifact_version?: string | undefined;
193
- source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | undefined;
193
+ source?: "manual" | "cli_wrapper" | "local_command" | "demo" | "real_captured" | "provider_usage" | "codex_import" | "cursor_import" | undefined;
194
194
  createdAt?: string | undefined;
195
195
  created_at?: string | undefined;
196
196
  generatedAt?: string | undefined;
@@ -16,11 +16,11 @@ const commandMetadataSchema = z.object({
16
16
  cwd: z.string().min(1).optional(),
17
17
  shell: z.string().min(1).optional()
18
18
  });
19
- // `codex_import` is deliberately NOT a command trace: an imported `codex exec --json`
20
- // JSONL export carries no command/exitCode/durationMs provenance, so it must not be
21
- // subjected to command-trace validation (that is why the Codex import path could not
22
- // reuse `cli_wrapper`/`local_command`). Keep this exclusion in lockstep with the
23
- // `imported_local` evidence mapping in safety-report.ts.
19
+ // `codex_import` (and `cursor_import`) are deliberately NOT command traces: an imported
20
+ // `codex exec --json` JSONL / Cursor session export carries no command/exitCode/durationMs
21
+ // provenance, so they must not be subjected to command-trace validation (that is why the
22
+ // import paths could not reuse `cli_wrapper`/`local_command`). Keep this exclusion in
23
+ // lockstep with the `imported_local` evidence mapping in safety-report.ts.
24
24
  function isCommandTrace(source) {
25
25
  return source === "cli_wrapper" || source === "local_command";
26
26
  }
@@ -30,7 +30,7 @@ const rawAgentTraceSchema = z
30
30
  title: z.string().min(1),
31
31
  artifactVersion: z.string().min(1).optional(),
32
32
  artifact_version: z.string().min(1).optional(),
33
- source: z.enum(["manual", "cli_wrapper", "local_command", "demo", "real_captured", "provider_usage", "codex_import"]).optional(),
33
+ source: z.enum(["manual", "cli_wrapper", "local_command", "demo", "real_captured", "provider_usage", "codex_import", "cursor_import"]).optional(),
34
34
  createdAt: isoTimestampSchema.optional(),
35
35
  created_at: isoTimestampSchema.optional(),
36
36
  generatedAt: isoTimestampSchema.optional(),