@compaction/cli 0.2.0 → 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.
- package/README.md +41 -12
- package/dist/cli/commands/activity.d.ts +8 -0
- package/dist/cli/commands/activity.js +35 -0
- package/dist/cli/commands/apply-context.js +68 -0
- package/dist/cli/commands/billing-delta.js +12 -1
- package/dist/cli/commands/capture-claude-code.d.ts +44 -0
- package/dist/cli/commands/capture-claude-code.js +206 -0
- package/dist/cli/commands/capture.js +283 -1
- package/dist/cli/commands/compact.js +9 -5
- package/dist/cli/commands/dev.d.ts +17 -0
- package/dist/cli/commands/dev.js +131 -0
- package/dist/cli/commands/gateway.d.ts +31 -0
- package/dist/cli/commands/gateway.js +312 -0
- package/dist/cli/commands/hooks.d.ts +5 -0
- package/dist/cli/commands/hooks.js +181 -0
- package/dist/cli/commands/init.js +422 -17
- package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
- package/dist/cli/commands/input-compaction-ab.js +125 -0
- package/dist/cli/commands/optimize-hosted.d.ts +54 -0
- package/dist/cli/commands/optimize-hosted.js +123 -0
- package/dist/cli/commands/optimize.js +8 -0
- package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
- package/dist/cli/commands/output-shaping-ab.js +132 -0
- package/dist/cli/commands/output-shaping.d.ts +7 -0
- package/dist/cli/commands/output-shaping.js +43 -0
- package/dist/cli/commands/policies.d.ts +2 -0
- package/dist/cli/commands/policies.js +80 -0
- package/dist/cli/commands/precall.d.ts +19 -0
- package/dist/cli/commands/precall.js +436 -0
- package/dist/cli/commands/recommend.js +1 -1
- package/dist/cli/commands/run.js +297 -12
- package/dist/cli/commands/upgrade-status.d.ts +42 -0
- package/dist/cli/commands/upgrade-status.js +152 -0
- package/dist/cli/cursor-export-read.d.ts +13 -0
- package/dist/cli/cursor-export-read.js +53 -0
- package/dist/cli/cursor-live-preflight.d.ts +19 -0
- package/dist/cli/cursor-live-preflight.js +46 -0
- package/dist/cli/index.js +36 -2
- package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
- package/dist/cli/onboarding/GatewayTui.js +113 -0
- package/dist/cli/onboarding/model.d.ts +32 -1
- package/dist/cli/onboarding/model.js +27 -2
- package/dist/cli/terminal-logo.d.ts +27 -0
- package/dist/cli/terminal-logo.js +49 -0
- package/dist/core/activity-event.d.ts +107 -0
- package/dist/core/activity-event.js +182 -0
- package/dist/core/activity-store.d.ts +63 -0
- package/dist/core/activity-store.js +254 -0
- package/dist/core/activity-view.d.ts +76 -0
- package/dist/core/activity-view.js +120 -0
- package/dist/core/api-client/index.d.ts +3 -1
- package/dist/core/api-client/index.js +4 -0
- package/dist/core/api-client/payload.d.ts +5 -1
- package/dist/core/api-client/payload.js +8 -6
- package/dist/core/api-client/persisted-config.d.ts +57 -0
- package/dist/core/api-client/persisted-config.js +156 -0
- package/dist/core/api-client/tool.d.ts +29 -0
- package/dist/core/api-client/tool.js +50 -0
- package/dist/core/api-client/types.d.ts +15 -4
- package/dist/core/auto-apply-ask.d.ts +58 -0
- package/dist/core/auto-apply-ask.js +105 -0
- package/dist/core/auto-apply-gates.d.ts +76 -0
- package/dist/core/auto-apply-gates.js +113 -0
- package/dist/core/before-call-activity.d.ts +35 -0
- package/dist/core/before-call-activity.js +103 -0
- package/dist/core/before-call-recovery.d.ts +19 -0
- package/dist/core/before-call-recovery.js +46 -0
- package/dist/core/before-call-stdin.d.ts +59 -0
- package/dist/core/before-call-stdin.js +78 -0
- package/dist/core/before-call.d.ts +149 -0
- package/dist/core/before-call.js +358 -0
- package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
- package/dist/core/billing-delta/billing-delta-record.js +26 -0
- package/dist/core/capture-record.d.ts +40 -0
- package/dist/core/capture-record.js +55 -0
- package/dist/core/claude-code-before-call.d.ts +37 -0
- package/dist/core/claude-code-before-call.js +120 -0
- package/dist/core/claude-code-connect.d.ts +31 -0
- package/dist/core/claude-code-connect.js +87 -0
- package/dist/core/claude-code-hook-record.d.ts +71 -0
- package/dist/core/claude-code-hook-record.js +0 -0
- package/dist/core/claude-code-hooks.d.ts +77 -0
- package/dist/core/claude-code-hooks.js +141 -0
- package/dist/core/codex-capture.d.ts +45 -0
- package/dist/core/codex-capture.js +204 -0
- package/dist/core/command-runner.js +5 -1
- package/dist/core/compaction-artifacts.d.ts +51 -0
- package/dist/core/compaction-artifacts.js +242 -0
- package/dist/core/compactor.d.ts +9 -0
- package/dist/core/compactor.js +95 -0
- package/dist/core/cross-surface-event.d.ts +306 -0
- package/dist/core/cross-surface-event.js +330 -0
- package/dist/core/cursor-capture.d.ts +54 -0
- package/dist/core/cursor-capture.js +215 -0
- package/dist/core/cursor-preflight-probe.d.ts +8 -0
- package/dist/core/cursor-preflight-probe.js +88 -0
- package/dist/core/cursor-preflight.d.ts +86 -0
- package/dist/core/cursor-preflight.js +126 -0
- package/dist/core/gateway/apply-activation.d.ts +39 -0
- package/dist/core/gateway/apply-activation.js +84 -0
- package/dist/core/gateway/apply-policy.d.ts +64 -0
- package/dist/core/gateway/apply-policy.js +221 -0
- package/dist/core/gateway/apply-receipt.d.ts +36 -0
- package/dist/core/gateway/apply-receipt.js +75 -0
- package/dist/core/gateway/cache-proof.d.ts +45 -0
- package/dist/core/gateway/cache-proof.js +65 -0
- package/dist/core/gateway/configure.d.ts +50 -0
- package/dist/core/gateway/configure.js +169 -0
- package/dist/core/gateway/openai-usage.d.ts +56 -0
- package/dist/core/gateway/openai-usage.js +128 -0
- package/dist/core/gateway/receipt.d.ts +138 -0
- package/dist/core/gateway/receipt.js +120 -0
- package/dist/core/gateway/recovery.d.ts +23 -0
- package/dist/core/gateway/recovery.js +68 -0
- package/dist/core/gateway/server.d.ts +51 -0
- package/dist/core/gateway/server.js +276 -0
- package/dist/core/gateway/status.d.ts +45 -0
- package/dist/core/gateway/status.js +109 -0
- package/dist/core/hook-usage-aggregate.d.ts +47 -0
- package/dist/core/hook-usage-aggregate.js +161 -0
- package/dist/core/input-compaction-ab.d.ts +111 -0
- package/dist/core/input-compaction-ab.js +158 -0
- package/dist/core/local-run-record.d.ts +109 -0
- package/dist/core/local-run-record.js +223 -0
- package/dist/core/output-shaping-ab.d.ts +140 -0
- package/dist/core/output-shaping-ab.js +146 -0
- package/dist/core/output-shaping-attach.d.ts +31 -0
- package/dist/core/output-shaping-attach.js +57 -0
- package/dist/core/output-shaping.d.ts +56 -0
- package/dist/core/output-shaping.js +89 -0
- package/dist/core/policy-middleware.d.ts +121 -0
- package/dist/core/policy-middleware.js +919 -0
- package/dist/core/policy-preferences.d.ts +99 -0
- package/dist/core/policy-preferences.js +232 -0
- package/dist/core/run-aggregator.d.ts +11 -1
- package/dist/core/run-aggregator.js +29 -2
- package/dist/core/run-flow-report.d.ts +82 -0
- package/dist/core/run-flow-report.js +71 -0
- package/dist/core/safety-report.js +8 -1
- package/dist/core/shim-capture-bridge.d.ts +32 -0
- package/dist/core/shim-capture-bridge.js +88 -0
- package/dist/core/skill-injection-policy.d.ts +72 -0
- package/dist/core/skill-injection-policy.js +183 -0
- package/dist/core/spend-attribution.js +1 -1
- package/dist/core/token-accounting.d.ts +1 -1
- package/dist/core/tool-shim.d.ts +129 -0
- package/dist/core/tool-shim.js +447 -0
- package/dist/core/trace-parser.d.ts +13 -13
- package/dist/core/trace-parser.js +6 -6
- package/dist/core/types.d.ts +12 -2
- package/dist/core/waste-detector.d.ts +20 -0
- package/dist/core/waste-detector.js +160 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,11 @@ supported agent sessions on your own machine, shows input/output tokens and esti
|
|
|
5
5
|
with honest labels, and writes local reports and redacted share bundles. Everything runs
|
|
6
6
|
locally and writes local files only — **no account, no telemetry, no upload by default.**
|
|
7
7
|
|
|
8
|
-
> **The free CLI captures, measures, inspects, reports, and
|
|
9
|
-
>
|
|
10
|
-
>
|
|
8
|
+
> **The free CLI captures, measures, inspects, reports, redacts — and performs deterministic
|
|
9
|
+
> local compaction with the basic policies (`compact`), writing honest before/after artifacts.**
|
|
10
|
+
> Advanced verification, recommendation and apply guidance, advanced policies, and team
|
|
11
|
+
> reporting are **not** in the free CLI — they are the **opt-in Compaction API**, which is
|
|
12
|
+
> **not a live hosted service yet**.
|
|
11
13
|
|
|
12
14
|
## What the CLI does
|
|
13
15
|
|
|
@@ -22,6 +24,11 @@ Everything below runs on your machine and writes local files only:
|
|
|
22
24
|
run carries provider usage metadata; otherwise they are a **local estimate (chars/4)**. Cost is
|
|
23
25
|
a **price-table estimate** labeled `local estimate` / `token-estimated cost` — **never
|
|
24
26
|
billing-confirmed**.
|
|
27
|
+
- **Compact deterministically, locally.** `compact` applies the basic deterministic policies
|
|
28
|
+
(e.g. `stale_tool_output_to_state_capsule`) to a captured/imported trace and writes honest
|
|
29
|
+
before/after artifacts locally (report, safety/recoverability report, state capsules). The
|
|
30
|
+
original trace is retained, token figures stay **local estimates** unless provider-reported,
|
|
31
|
+
and **nothing is applied to your workflow automatically**.
|
|
25
32
|
- **Inspect spend and roll up runs.** `spend`, `summary`, and `aggregate` summarize where context
|
|
26
33
|
spend came from and roll up local runs — local files only, no hosted dashboard.
|
|
27
34
|
- **Build a local context store and retrieve from it.** `context add` accumulates source-pointed
|
|
@@ -48,10 +55,11 @@ and roughly what does it cost?"* — entirely locally, with honest labels.
|
|
|
48
55
|
|
|
49
56
|
These are **not** part of the free local CLI:
|
|
50
57
|
|
|
51
|
-
- **
|
|
52
|
-
- **Advanced verification** — stronger checks on what a compacted context retains.
|
|
58
|
+
- **Recommendation** — a recommended-policy verdict (`recommend`) for a captured trace.
|
|
59
|
+
- **Advanced verification** — stronger checks on what a compacted context retains (incl. `compact --eval`).
|
|
53
60
|
- **Apply guidance** — a readiness verdict and a review/approval path before any context is applied.
|
|
54
|
-
- **
|
|
61
|
+
- **Advanced policies & team reporting** — configurable behavior controls beyond the basic
|
|
62
|
+
deterministic policies, and cross-run / team reporting.
|
|
55
63
|
|
|
56
64
|
> **The Compaction API is not broadly hosted yet.** There is **no live hosted service today**.
|
|
57
65
|
> Until the API is available, these capabilities are **on the roadmap / opt-in**, not present in
|
|
@@ -74,7 +82,7 @@ npx @compaction/cli --help # no install at all
|
|
|
74
82
|
```
|
|
75
83
|
|
|
76
84
|
The installed binary is `compaction`. After a global install, sanity-check with
|
|
77
|
-
`compaction --version` (→ `0.
|
|
85
|
+
`compaction --version` (→ `0.3.0`) and `compaction --help`.
|
|
78
86
|
|
|
79
87
|
### curl one-liner
|
|
80
88
|
|
|
@@ -109,8 +117,27 @@ compaction spend ./my-trace/captured-trace.json
|
|
|
109
117
|
compaction feedback --redact --yes
|
|
110
118
|
```
|
|
111
119
|
|
|
112
|
-
|
|
113
|
-
free
|
|
120
|
+
Deterministic basic compaction (`compaction compact <trace> --out <dir>`) also runs locally in
|
|
121
|
+
the free CLI. Advanced verification, recommendation, and apply guidance are the opt-in
|
|
122
|
+
Compaction API — not steps of the free local CLI.
|
|
123
|
+
|
|
124
|
+
## Gateway (local, byte-safe)
|
|
125
|
+
|
|
126
|
+
The Compaction **Gateway** is a local, OpenAI-compatible reverse proxy. Point your OpenAI client at it
|
|
127
|
+
(or launch your workflow through it) and it forwards traffic to the provider and records **content-free**
|
|
128
|
+
token/cache receipts — no prompt/response content, and your provider key stays on the client. Set it up
|
|
129
|
+
from onboarding (`compaction` → **Gateway setup**), or directly:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
compaction gateway start # start the local gateway (RECORD mode; byte-safe, no mutation)
|
|
133
|
+
compaction gateway run -- npm run dev # run a command through the gateway (no baseURL copy-paste)
|
|
134
|
+
compaction gateway status # requests observed + a content-free receipts rollup
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Record mode never changes your request or response. An **experimental**, explicit-opt-in deterministic
|
|
138
|
+
`apply` mode (`--mode apply --policy deterministic-dedupe`) can remove exact-duplicate large blocks from
|
|
139
|
+
known-safe requests, retaining the original locally (`compaction gateway recover <id>`). Nothing is called
|
|
140
|
+
a saving — the gateway reports provider-reported token/cache counts (or "unavailable"), never a cost figure.
|
|
114
141
|
|
|
115
142
|
## Supported local workflows
|
|
116
143
|
|
|
@@ -141,6 +168,7 @@ compaction capture provider-usage --endpoint <url> [--credential-env-var <NAME>]
|
|
|
141
168
|
compaction import <trace-file> --source <source> --out <dir>
|
|
142
169
|
compaction import --list-sources
|
|
143
170
|
compaction analyze <trace-file>
|
|
171
|
+
compaction compact <trace-file> --out <dir> # deterministic local compaction (basic policies) + honest artifacts
|
|
144
172
|
compaction context add <trace-file> # build a local context store from a captured/imported trace
|
|
145
173
|
compaction context get "<query>" # retrieve + assemble the active context (with source pointers)
|
|
146
174
|
compaction spend <trace-or-optimization-path> # context-spend attribution
|
|
@@ -180,9 +208,10 @@ Every figure this tool produces carries one of these labels — and nothing stro
|
|
|
180
208
|
|
|
181
209
|
## Known limitations
|
|
182
210
|
|
|
183
|
-
- **The free CLI captures and
|
|
184
|
-
verification
|
|
185
|
-
|
|
211
|
+
- **The free CLI captures, measures, and compacts deterministically; it does not perform
|
|
212
|
+
advanced verification.** Recommendation, advanced verification (incl. `compact --eval`), apply
|
|
213
|
+
guidance, advanced policies, and team reporting are the **opt-in Compaction API** — not the
|
|
214
|
+
free local CLI. The API is **not broadly hosted yet**.
|
|
186
215
|
- **Cost figures are estimates or operator-entered records, not billing-confirmed.** Locally
|
|
187
216
|
computed cost figures are price-table estimates; provider-usage records carry operator-entered/
|
|
188
217
|
captured console figures (not provider-verified by this tool). **No figure is billing-confirmed.**
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `compaction activity` — READ-ONLY, metrics-only view of recent runs from the local activity
|
|
4
|
+
* store (`.compaction/activity/activity.jsonl`). Content-free: it renders only the counts/labels
|
|
5
|
+
* the events already carry; it never reads or prints prompt/response content, and it writes
|
|
6
|
+
* NOTHING. Empty store → a friendly message, exit 0.
|
|
7
|
+
*/
|
|
8
|
+
export declare function registerActivityCommand(program: Command): void;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { readActivityEvents } from "../../core/activity-store.js";
|
|
3
|
+
import { DEFAULT_ACTIVITY_LIMIT, buildActivityJson, buildActivityRows, formatActivityTable } from "../../core/activity-view.js";
|
|
4
|
+
/**
|
|
5
|
+
* `compaction activity` — READ-ONLY, metrics-only view of recent runs from the local activity
|
|
6
|
+
* store (`.compaction/activity/activity.jsonl`). Content-free: it renders only the counts/labels
|
|
7
|
+
* the events already carry; it never reads or prints prompt/response content, and it writes
|
|
8
|
+
* NOTHING. Empty store → a friendly message, exit 0.
|
|
9
|
+
*/
|
|
10
|
+
export function registerActivityCommand(program) {
|
|
11
|
+
program
|
|
12
|
+
.command("activity")
|
|
13
|
+
.description("Show recent runs recorded in the local metrics-only activity store (read-only, content-free).")
|
|
14
|
+
.option("--limit <n>", `Max runs to show, most recent first (default ${DEFAULT_ACTIVITY_LIMIT})`)
|
|
15
|
+
.option("--surface <surface>", "Only show runs from this surface (e.g. cursor, codex, cli, claude_code)")
|
|
16
|
+
.option("--json", "Machine-readable JSON output")
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const limit = options.limit !== undefined ? Number.parseInt(options.limit, 10) : DEFAULT_ACTIVITY_LIMIT;
|
|
19
|
+
if (Number.isNaN(limit) || limit < 0) {
|
|
20
|
+
console.error(`Invalid --limit "${options.limit}": expected a non-negative integer.`);
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const { events, skipped } = await readActivityEvents();
|
|
25
|
+
const rows = buildActivityRows(events, { limit, surface: options.surface });
|
|
26
|
+
const meta = { totalEvents: events.length, limit, surface: options.surface, skippedCount: skipped.length };
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(JSON.stringify(buildActivityJson(rows, meta), null, 2));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const table = formatActivityTable(rows, meta);
|
|
32
|
+
console.log(rows.length === 0 ? chalk.yellow(table) : chalk.cyan(table));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=activity.js.map
|
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { Option } from "commander";
|
|
4
|
+
import { AUTO_APPLY_QUESTION_LINES, formatSavedPreferenceConfirmation, resolveAutoApplyOffer } from "../../core/auto-apply-ask.js";
|
|
5
|
+
import { savePolicyPreference } from "../../core/policy-preferences.js";
|
|
4
6
|
import { parseTraceFile } from "../../core/trace-parser.js";
|
|
5
7
|
import { runEngineCommand } from "../engine-degrade.js";
|
|
6
8
|
function defaultOutputDir(artifactRoot, traceId) {
|
|
7
9
|
return path.join(artifactRoot, traceId);
|
|
8
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Infer the SAFEST workflow tool for the auto-apply preference scope. Prefers the explicit
|
|
13
|
+
* `--workflow-tool` flag; otherwise maps an unambiguous trace source to its tool. Returns undefined
|
|
14
|
+
* when it cannot infer safely (then the offer fails closed and saves nothing — never guesses global).
|
|
15
|
+
*/
|
|
16
|
+
function inferWorkflowTool(explicit, traceSource) {
|
|
17
|
+
if (explicit && explicit.trim() !== "")
|
|
18
|
+
return explicit.trim();
|
|
19
|
+
if (traceSource === "codex_import")
|
|
20
|
+
return "codex";
|
|
21
|
+
if (traceSource === "cursor_import")
|
|
22
|
+
return "cursor";
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
9
25
|
/**
|
|
10
26
|
* `compaction apply-context <trace-file> --out <dir>` — LEVEL-5 IN-WORKFLOW APPLY, Form A.
|
|
11
27
|
*
|
|
@@ -26,6 +42,12 @@ export function registerApplyContextCommand(program) {
|
|
|
26
42
|
.addOption(new Option("--approve-skill-injection-policy", "EXPLICITLY also authorize the approval-required repeated_skill_injection_to_state_capsule policy. Default off; no auto-apply."))
|
|
27
43
|
.addOption(new Option("--attest-used", "OPTIONAL operator attestation: AFTER an approval, the operator records that they used the approved context in a live call. Stored as operator-ATTESTED provenance (the operator's own claim), never observed by compaction.dev."))
|
|
28
44
|
.option("--attest-note <text>", "Optional free-text note recorded with the operator attestation")
|
|
45
|
+
.addOption(new Option("--offer-auto-apply", "OPT-IN. AFTER an explicit approval, offer the ONE binary question 'apply this automatically next time when safety gates pass?' (default no). Saving a preference records intent only — it NEVER applies anything (no auto-apply engine exists)."))
|
|
46
|
+
.addOption(new Option("--remember-auto-apply", "Non-interactive answer YES to the auto-apply offer (requires --offer-auto-apply and a prior approval). Saves a preference for the inferred safest scope; applies nothing now."))
|
|
47
|
+
.addOption(new Option("--no-remember", "Non-interactive answer NO to the auto-apply offer (the default): save nothing, keep asking each time."))
|
|
48
|
+
.option("--workflow-tool <tool>", "Tool/workflow to scope a saved auto-apply preference to (never global/cross-tool). Inferred from the trace source when omitted.")
|
|
49
|
+
.option("--workflow-repo <repo>", "Optional content-free repo identifier to narrow a saved auto-apply preference's scope.")
|
|
50
|
+
.option("--policy-type <type>", "Policy type a saved auto-apply preference concerns (defaults to the approved policy name).")
|
|
29
51
|
.action(async (traceFile, options) => {
|
|
30
52
|
await runEngineCommand(async () => {
|
|
31
53
|
// Lazy-load the proprietary engine: present in-repo/API, excluded from the public package.
|
|
@@ -70,6 +92,52 @@ export function registerApplyContextCommand(program) {
|
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
console.log(chalk.green(`Output directory: ${result.outputDirectory}`));
|
|
95
|
+
// OPT-IN post-approval auto-apply OFFER (§11a block 4/5). This offer is made ONLY after an
|
|
96
|
+
// explicit in-workflow approval already emitted an approved context. It APPLIES NOTHING: on
|
|
97
|
+
// yes it records a PREFERENCE (intent for the future gated slice) via the #598 store; on
|
|
98
|
+
// no/default it saves nothing. It never touches apply execution semantics.
|
|
99
|
+
if (options.offerAutoApply === true && result.approvedContextEmitted) {
|
|
100
|
+
console.log("");
|
|
101
|
+
for (const line of AUTO_APPLY_QUESTION_LINES)
|
|
102
|
+
console.log(chalk.cyan(line));
|
|
103
|
+
const answer = options.rememberAutoApply === true ? "yes" : "no";
|
|
104
|
+
const decision = resolveAutoApplyOffer({
|
|
105
|
+
approvedInWorkflow: true,
|
|
106
|
+
answer,
|
|
107
|
+
scope: {
|
|
108
|
+
tool: inferWorkflowTool(options.workflowTool, trace.source),
|
|
109
|
+
repo: options.workflowRepo,
|
|
110
|
+
policyType: options.policyType ?? result.review.policies_applied[0]
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (decision.action === "save") {
|
|
114
|
+
const saved = await savePolicyPreference({
|
|
115
|
+
scope: decision.scope,
|
|
116
|
+
preference: decision.preference,
|
|
117
|
+
gates_required: decision.gatesRequired
|
|
118
|
+
});
|
|
119
|
+
if (saved.saved) {
|
|
120
|
+
console.log("");
|
|
121
|
+
console.log(chalk.green(formatSavedPreferenceConfirmation(saved.preference)));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(chalk.yellow("Preference NOT saved (fail-closed) — nothing was applied:"));
|
|
126
|
+
for (const problem of saved.problems)
|
|
127
|
+
console.log(chalk.yellow(` - ${problem}`));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (decision.action === "blocked") {
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(chalk.yellow("Preference NOT saved — could not infer a safe (non-global, non-cross-tool) scope. Nothing applied:"));
|
|
133
|
+
for (const problem of decision.problems)
|
|
134
|
+
console.log(chalk.yellow(` - ${problem}`));
|
|
135
|
+
console.log(chalk.dim(" Pass --workflow-tool <tool> (and optionally --policy-type / --workflow-repo) to record a preference."));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.log(chalk.dim("No auto-apply preference saved — Compaction will keep asking each time (the default)."));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
73
141
|
// Exit non-zero when no approved context was emitted (refusal / review-only), mirroring apply/approve.
|
|
74
142
|
if (!result.approvedContextEmitted) {
|
|
75
143
|
process.exitCode = 1;
|
|
@@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
|
|
5
|
-
import { addRun, computeReport, initMeasurementRecord, BILLING_DELTA_CRITERION, BILLING_DELTA_RECORD_ROOT } from "../../core/billing-delta/billing-delta-record.js";
|
|
5
|
+
import { addRun, computeReport, initMeasurementRecord, API_METERED_BILLING_CONFIRMED_CRITERIA, BILLING_DELTA_CRITERION, BILLING_DELTA_FIXED_PLAN_NOTE, BILLING_DELTA_RECORD_ROOT } from "../../core/billing-delta/billing-delta-record.js";
|
|
6
6
|
const PRIVACY_NOTE = "Operator-run, local-first. compaction.dev makes NO provider call, reads NO credential, and has NO network access. " +
|
|
7
7
|
"All billing figures are operator-entered (read from YOUR provider console/billing export) and are NOT provider-verified by this tool. " +
|
|
8
8
|
"Aggregate cost only — no prompt/completion content is stored.";
|
|
@@ -81,6 +81,17 @@ function buildReportMarkdown(report) {
|
|
|
81
81
|
"",
|
|
82
82
|
`- ${report.criterion}`,
|
|
83
83
|
"",
|
|
84
|
+
"## API-metered billing-confirmed criteria (cross-surface contract §5, D6 — checklist only)",
|
|
85
|
+
"",
|
|
86
|
+
"> A billing-confirmed figure is possible ONLY on api_metered workflows, ONLY through this",
|
|
87
|
+
"> billing-delta flow, and ONLY when ALL of the criteria below hold. This report does NOT verify",
|
|
88
|
+
"> them and NEVER carries billing_confirmed: true. Even when all criteria hold, the claim is",
|
|
89
|
+
"> workflow-scoped only — generalized billing-confirmed = future only.",
|
|
90
|
+
">",
|
|
91
|
+
`> ${BILLING_DELTA_FIXED_PLAN_NOTE}`,
|
|
92
|
+
"",
|
|
93
|
+
...API_METERED_BILLING_CONFIRMED_CRITERIA.map((c, i) => `${i + 1}. ${c}`),
|
|
94
|
+
"",
|
|
84
95
|
"## Verdict notes",
|
|
85
96
|
"",
|
|
86
97
|
...report.verdict_notes.map((n) => `- ${n}`),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UsageMetadata } from "../../core/usage-metadata.js";
|
|
1
2
|
interface DiscoverClaudeCodeOptions {
|
|
2
3
|
projectsDir?: string;
|
|
3
4
|
}
|
|
@@ -8,6 +9,49 @@ interface CaptureClaudeCodeOptions {
|
|
|
8
9
|
includeSubagents?: boolean;
|
|
9
10
|
}
|
|
10
11
|
export declare function captureClaudeCodeCommand(options: CaptureClaudeCodeOptions): Promise<void>;
|
|
12
|
+
/** A normalized usage summary for the hook path — usage + a content-free fingerprint + message count. */
|
|
13
|
+
interface HookNormalizeResult {
|
|
14
|
+
usage: UsageMetadata;
|
|
15
|
+
messageCount: number;
|
|
16
|
+
fingerprint?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface FromHookDeps {
|
|
19
|
+
readStdin?: () => Promise<string>;
|
|
20
|
+
normalize?: (transcriptPath: string) => Promise<HookNormalizeResult>;
|
|
21
|
+
cwd?: string;
|
|
22
|
+
now?: () => string;
|
|
23
|
+
hostedConfigured?: () => boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface FromHookOptions {
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* `compaction capture claude-code --from-hook`. Reads the Stop payload from stdin, resolves the session
|
|
30
|
+
* JSONL via `transcript_path`, and records CONTENT-FREE usage only — never message content, never
|
|
31
|
+
* `last_assistant_message`. Idempotent via a content-free dedup ledger. FAIL-OPEN by contract: any error is
|
|
32
|
+
* swallowed and the process exits 0 so the hook can never break Claude Code.
|
|
33
|
+
*/
|
|
34
|
+
export declare function captureClaudeCodeFromHook(options?: FromHookOptions, deps?: FromHookDeps): Promise<void>;
|
|
35
|
+
export interface FromPromptHookDeps {
|
|
36
|
+
readStdin?: () => Promise<string>;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `compaction capture claude-code --from-prompt-hook` — the BEFORE-CALL recommendation path (charter
|
|
41
|
+
* 2026-07-04-run-13). The Claude Code **UserPromptSubmit** hook runs this BEFORE the model processes the
|
|
42
|
+
* prompt (a genuine pre-call event — NOT the post-session Stop hook). It reads the UserPromptSubmit
|
|
43
|
+
* payload from stdin, analyzes the pending PROMPT for avoidable duplicated context IN-PROCESS
|
|
44
|
+
* (content-free), and — when present — records ONE metrics-only `claude_code` before-call RECOMMENDATION
|
|
45
|
+
* activity event so it appears in `compaction activity`. Apply is a PROVEN BLOCKER on this surface (no
|
|
46
|
+
* hook can reduce the model's context; hooks are non-interactive) → recommendation-only; the original
|
|
47
|
+
* prompt ALWAYS runs UNCHANGED.
|
|
48
|
+
*
|
|
49
|
+
* CRITICAL: this path writes NOTHING to stdout — on UserPromptSubmit, a hook's stdout is ADDED to the
|
|
50
|
+
* model's context, so injecting anything would be both intrusive and self-defeating for a compaction
|
|
51
|
+
* tool. It is silent and transparent; the recommendation lives in `compaction activity`. FAIL-OPEN +
|
|
52
|
+
* CONTENT-FREE: any error is swallowed and the process exits 0; the prompt text is never persisted.
|
|
53
|
+
*/
|
|
54
|
+
export declare function captureClaudeCodeFromPromptHook(deps?: FromPromptHookDeps): Promise<void>;
|
|
11
55
|
/**
|
|
12
56
|
* Discover local Claude Code sessions (metadata only) and print, for each, the
|
|
13
57
|
* ready-to-run `capture claude-code --session <path>` command (guided capture).
|
|
@@ -1,6 +1,18 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
1
3
|
import { writeJsonArtifact, writeTextArtifact } from "../../core/artifact-writer.js";
|
|
2
4
|
import { ClaudeCodeAdapter, PRIVACY_WARNING } from "../../core/adapters/claude-code-adapter.js";
|
|
3
5
|
import { discoverClaudeCodeSessions, DISCOVERY_PRIVACY_NOTE } from "../../core/adapters/claude-code-discovery.js";
|
|
6
|
+
import { recordCaptureContentFree } from "../../core/capture-record.js";
|
|
7
|
+
import { buildRunFlowTokenReport, formatRunFlowTokenReport } from "../../core/run-flow-report.js";
|
|
8
|
+
import { buildRunCrossSurfaceEvent } from "../../core/cross-surface-event.js";
|
|
9
|
+
import { buildMeasureOnlyActivityEvent } from "../../core/activity-event.js";
|
|
10
|
+
import { appendActivityEvent } from "../../core/activity-store.js";
|
|
11
|
+
import { detectAvoidableContext } from "../../core/before-call.js";
|
|
12
|
+
import { parseUserPromptSubmitPayload, buildClaudeCodeBeforeCallEvent } from "../../core/claude-code-before-call.js";
|
|
13
|
+
import { resolveApiConfig } from "../../core/api-client/index.js";
|
|
14
|
+
import { hostedConfigured } from "./optimize-hosted.js";
|
|
15
|
+
import { buildClaudeCodeHookRecord, computeDedupKey, emptyLedger, isAlreadyRecorded, parseStopPayload, resolveTranscriptPath } from "../../core/claude-code-hook-record.js";
|
|
4
16
|
function buildOutputDir(sessionPath, out) {
|
|
5
17
|
if (out)
|
|
6
18
|
return out;
|
|
@@ -120,6 +132,16 @@ export async function captureClaudeCodeCommand(options) {
|
|
|
120
132
|
console.log(" figures shown by 'spend' and 'compact' are deterministic LOCAL ESTIMATES (chars/4) — a");
|
|
121
133
|
console.log(" different measurement, labeled there as local estimates; neither figure is billing-confirmed.");
|
|
122
134
|
}
|
|
135
|
+
// 6b-iii. Shared UNIFIED-FLOW honest token report (docs/design/unified-run-flow.md, D1/D3): the SAME
|
|
136
|
+
// block that `run codex` / `run cursor` print, so all three tools present ONE honest `token_source`
|
|
137
|
+
// summary. Claude Code usage is PROVIDER-REPORTED and whole (no per-field unavailable split, like
|
|
138
|
+
// Codex), so outputStatus is "present"; when a session lacks provider usage the shared builder honestly
|
|
139
|
+
// downgrades to local-estimate / unavailable — it never labels anything stronger than the evidence. This
|
|
140
|
+
// is a reporting surface only: input/output are shown SEPARATELY with an explicit source, and output is
|
|
141
|
+
// shown as TOKENS ONLY (never a saving). No logic, adapter, or record behavior changes here.
|
|
142
|
+
const runFlowReport = buildRunFlowTokenReport({ tool: "claude-code", usage, outputStatus: "present" });
|
|
143
|
+
for (const line of formatRunFlowTokenReport(runFlowReport))
|
|
144
|
+
console.log(line);
|
|
123
145
|
// 6c. Per-run distinctness fingerprint (a one-way digest over canonical normalized
|
|
124
146
|
// content — NOT raw content). Lets a re-capture of the same session be told apart
|
|
125
147
|
// from a genuinely new one; when unavailable, distinctness is `not_verified`.
|
|
@@ -138,6 +160,20 @@ export async function captureClaudeCodeCommand(options) {
|
|
|
138
160
|
await writeTextArtifact(outDir, "capture-report.md", markdownReport);
|
|
139
161
|
// 7. Print artifacts written
|
|
140
162
|
console.log(`Artifacts written to: ${outDir}/`);
|
|
163
|
+
// Unified flow (docs/design/unified-run-flow.md): content-free record (input/output separate +
|
|
164
|
+
// honest source) when hosted-configured. No default network call — only when COMPACTION_API_URL +
|
|
165
|
+
// COMPACTION_API_KEY are set. Claude Code session usage is provider-reported.
|
|
166
|
+
if (hostedConfigured()) {
|
|
167
|
+
const rec = await recordCaptureContentFree(resolveApiConfig(), {
|
|
168
|
+
usage,
|
|
169
|
+
tool: "claude-code",
|
|
170
|
+
reference: `${outDir}/captured-trace.json`
|
|
171
|
+
});
|
|
172
|
+
console.log(rec.recorded ? `Recorded content-free usage (tool=claude-code, source=${rec.source}).` : `Not recorded: ${rec.reason}.`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
console.log("Not recorded (set COMPACTION_API_URL + COMPACTION_API_KEY to record content-free usage).");
|
|
176
|
+
}
|
|
141
177
|
// 8. Print next step — chain into the verified strong-MVP Claude Code flow:
|
|
142
178
|
// spend (where the spend goes) → compact --eval (compaction + strong apply-readiness:
|
|
143
179
|
// deterministic recoverability + commitment-preservation + fixture task-check + token/cost).
|
|
@@ -151,6 +187,176 @@ export async function captureClaudeCodeCommand(options) {
|
|
|
151
187
|
console.log(" # fixture task-check, and token/cost accounting — nothing is applied automatically.");
|
|
152
188
|
console.log(` (or: compaction analyze ${outDir}/captured-trace.json for a quick read-only estimate)`);
|
|
153
189
|
}
|
|
190
|
+
async function readAllStdin() {
|
|
191
|
+
if (process.stdin.isTTY)
|
|
192
|
+
return "";
|
|
193
|
+
const chunks = [];
|
|
194
|
+
for await (const chunk of process.stdin)
|
|
195
|
+
chunks.push(Buffer.from(chunk));
|
|
196
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
197
|
+
}
|
|
198
|
+
async function defaultHookNormalize(transcriptPath) {
|
|
199
|
+
const adapter = new ClaudeCodeAdapter();
|
|
200
|
+
const { trace, usage, provenance } = await adapter.normalize({ sourcePath: transcriptPath });
|
|
201
|
+
return {
|
|
202
|
+
usage,
|
|
203
|
+
messageCount: trace.messages.length,
|
|
204
|
+
...(provenance.traceFingerprint ? { fingerprint: provenance.traceFingerprint.content_sha256 } : {})
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function hookDir(cwd) {
|
|
208
|
+
return path.join(cwd, ".compaction", "hooks", "claude-code");
|
|
209
|
+
}
|
|
210
|
+
async function loadLedger(cwd) {
|
|
211
|
+
try {
|
|
212
|
+
const raw = await readFile(path.join(hookDir(cwd), "ledger.json"), "utf8");
|
|
213
|
+
const parsed = JSON.parse(raw);
|
|
214
|
+
return Array.isArray(parsed.entries) ? parsed : emptyLedger();
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return emptyLedger();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Append ONE metrics-only activity event for a Stop-hook capture (the always-on bridge). Builds the
|
|
222
|
+
* `claude_code` cross-surface event from the SAME honest token report the record uses, then the
|
|
223
|
+
* measure-only activity extension, and appends it to `.compaction/activity/` under the hook's cwd
|
|
224
|
+
* (so tmp-cwd tests and real projects both scope correctly). Content-free and best-effort: any
|
|
225
|
+
* local failure is swallowed here so it can NEVER break the fail-open hook.
|
|
226
|
+
*/
|
|
227
|
+
async function appendClaudeCodeHookActivity(input) {
|
|
228
|
+
try {
|
|
229
|
+
// outputStatus "present": buildRunFlowTokenReport honestly downgrades a missing output axis to
|
|
230
|
+
// unavailable — a genuinely-absent field is never labeled provider-reported (no silent zero).
|
|
231
|
+
const tokenReport = buildRunFlowTokenReport({ tool: "claude-code", usage: input.usage, outputStatus: "present" });
|
|
232
|
+
const crossSurfaceEvent = buildRunCrossSurfaceEvent("claude_code", {
|
|
233
|
+
// run_id is the deterministic content-free dedup key → deterministic activity_event_id → the
|
|
234
|
+
// store dedupes a re-captured session (same state = one event).
|
|
235
|
+
runId: `claude-code-${input.dedupKey}`,
|
|
236
|
+
tokenReport,
|
|
237
|
+
reasons: {
|
|
238
|
+
input: "the Claude Code session usage did not report input tokens for this session state",
|
|
239
|
+
output: "the Claude Code session usage did not report output tokens for this session state"
|
|
240
|
+
},
|
|
241
|
+
...(input.usage.model ? { modelLabel: input.usage.model } : {})
|
|
242
|
+
});
|
|
243
|
+
// session_id rides on the event too (content-free identity), when the Stop payload carried one.
|
|
244
|
+
const eventWithSession = input.sessionId
|
|
245
|
+
? { ...crossSurfaceEvent, session_id: input.sessionId }
|
|
246
|
+
: crossSurfaceEvent;
|
|
247
|
+
// Measurement only: nothing was applied or retained by Compaction, so original_retained=false.
|
|
248
|
+
const activityEvent = buildMeasureOnlyActivityEvent(eventWithSession, { original_retained: false });
|
|
249
|
+
const result = await appendActivityEvent(activityEvent, path.join(input.cwd, ".compaction", "activity"));
|
|
250
|
+
console.log(result.appended
|
|
251
|
+
? `compaction hook: recorded metrics-only activity (surface=claude_code, id ${result.activity_event_id.slice(0, 12)}…) — see 'compaction activity'.`
|
|
252
|
+
: `compaction hook: activity not appended (${result.reason}).`);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
// Best-effort local append — never fail the fail-open hook on a filesystem/store error.
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* `compaction capture claude-code --from-hook`. Reads the Stop payload from stdin, resolves the session
|
|
260
|
+
* JSONL via `transcript_path`, and records CONTENT-FREE usage only — never message content, never
|
|
261
|
+
* `last_assistant_message`. Idempotent via a content-free dedup ledger. FAIL-OPEN by contract: any error is
|
|
262
|
+
* swallowed and the process exits 0 so the hook can never break Claude Code.
|
|
263
|
+
*/
|
|
264
|
+
export async function captureClaudeCodeFromHook(options = {}, deps = {}) {
|
|
265
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
266
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
267
|
+
const readStdin = deps.readStdin ?? readAllStdin;
|
|
268
|
+
const normalize = deps.normalize ?? defaultHookNormalize;
|
|
269
|
+
const hostedIsConfigured = deps.hostedConfigured ?? hostedConfigured;
|
|
270
|
+
try {
|
|
271
|
+
const payload = parseStopPayload(await readStdin());
|
|
272
|
+
const transcriptPath = resolveTranscriptPath(payload);
|
|
273
|
+
if (!transcriptPath) {
|
|
274
|
+
console.log("compaction hook: no transcript_path in the Stop payload — nothing recorded (content-free).");
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
const sessionId = typeof payload?.session_id === "string" ? payload.session_id : undefined;
|
|
278
|
+
const { usage, messageCount, fingerprint } = await normalize(transcriptPath);
|
|
279
|
+
const dedupKey = computeDedupKey({
|
|
280
|
+
sessionId,
|
|
281
|
+
fingerprint,
|
|
282
|
+
messageCount,
|
|
283
|
+
inputTokens: typeof usage.input_tokens === "number" ? usage.input_tokens : null,
|
|
284
|
+
outputTokens: typeof usage.output_tokens === "number" ? usage.output_tokens : null
|
|
285
|
+
});
|
|
286
|
+
const ledger = await loadLedger(cwd);
|
|
287
|
+
if (isAlreadyRecorded(ledger, dedupKey)) {
|
|
288
|
+
console.log(`compaction hook: already recorded this session state (dedup ${dedupKey.slice(0, 8)}…) — skipped.`);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
const record = buildClaudeCodeHookRecord({ usage, ...(sessionId ? { sessionId } : {}), messageCount, dedupKey, recordedAt: now() });
|
|
292
|
+
if (options.dryRun) {
|
|
293
|
+
console.log("compaction hook --dry-run: would record (content-free, nothing written):");
|
|
294
|
+
console.log(JSON.stringify(record, null, 2));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const dir = hookDir(cwd);
|
|
298
|
+
await mkdir(path.join(dir, "records"), { recursive: true });
|
|
299
|
+
await writeFile(path.join(dir, "records", `${dedupKey}.json`), `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
300
|
+
ledger.entries.push({ dedupKey, ...(sessionId ? { sessionId } : {}), recordedAt: record.recordedAt });
|
|
301
|
+
await writeFile(path.join(dir, "ledger.json"), `${JSON.stringify(ledger, null, 2)}\n`, "utf8");
|
|
302
|
+
console.log(`compaction hook: recorded content-free usage (source=${record.tokenSource}, dedup ${dedupKey.slice(0, 8)}…).`);
|
|
303
|
+
// ALWAYS-ON BRIDGE (charter 2026-07-04-run-8): the Stop hook runs this command at session end,
|
|
304
|
+
// so the SAME content-free usage ALSO appends ONE metrics-only activity event to the local
|
|
305
|
+
// activity store (.compaction/activity/). THIS is what makes a subsequent Claude Code session
|
|
306
|
+
// appear in `compaction activity` with NO manual import. Honesty: surface "claude_code",
|
|
307
|
+
// provider "anthropic", token axes copied VERBATIM from the SAME token report (provider-reported
|
|
308
|
+
// where the session usage carried them, else unavailable-with-reason — never a silent zero);
|
|
309
|
+
// measure-only, so approval "not-required", auto_apply OFF (not eligible, "ask-each-time",
|
|
310
|
+
// applied_automatically false — no auto-apply logic exists), sync "local-only", recovery
|
|
311
|
+
// original_retained=false (measurement only — Compaction modified/retained nothing). The event
|
|
312
|
+
// id is DETERMINISTIC from session state (run_id = the content-free dedup key), so a re-captured
|
|
313
|
+
// session yields the SAME id and the store dedupes it. Best-effort + content-free; a local
|
|
314
|
+
// filesystem failure here never breaks the hook.
|
|
315
|
+
await appendClaudeCodeHookActivity({ cwd, usage, dedupKey, sessionId });
|
|
316
|
+
if (hostedIsConfigured()) {
|
|
317
|
+
const rec = await recordCaptureContentFree(resolveApiConfig(), { usage, tool: "claude-code", reference: `hook:${dedupKey}` });
|
|
318
|
+
console.log(rec.recorded ? `compaction hook: also recorded to hosted (source=${rec.source}).` : `compaction hook: hosted not recorded: ${rec.reason}.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
// FAIL-OPEN: never propagate — the hook must not break Claude Code.
|
|
323
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
324
|
+
console.log(`compaction hook: skipped (non-fatal: ${message}).`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* `compaction capture claude-code --from-prompt-hook` — the BEFORE-CALL recommendation path (charter
|
|
329
|
+
* 2026-07-04-run-13). The Claude Code **UserPromptSubmit** hook runs this BEFORE the model processes the
|
|
330
|
+
* prompt (a genuine pre-call event — NOT the post-session Stop hook). It reads the UserPromptSubmit
|
|
331
|
+
* payload from stdin, analyzes the pending PROMPT for avoidable duplicated context IN-PROCESS
|
|
332
|
+
* (content-free), and — when present — records ONE metrics-only `claude_code` before-call RECOMMENDATION
|
|
333
|
+
* activity event so it appears in `compaction activity`. Apply is a PROVEN BLOCKER on this surface (no
|
|
334
|
+
* hook can reduce the model's context; hooks are non-interactive) → recommendation-only; the original
|
|
335
|
+
* prompt ALWAYS runs UNCHANGED.
|
|
336
|
+
*
|
|
337
|
+
* CRITICAL: this path writes NOTHING to stdout — on UserPromptSubmit, a hook's stdout is ADDED to the
|
|
338
|
+
* model's context, so injecting anything would be both intrusive and self-defeating for a compaction
|
|
339
|
+
* tool. It is silent and transparent; the recommendation lives in `compaction activity`. FAIL-OPEN +
|
|
340
|
+
* CONTENT-FREE: any error is swallowed and the process exits 0; the prompt text is never persisted.
|
|
341
|
+
*/
|
|
342
|
+
export async function captureClaudeCodeFromPromptHook(deps = {}) {
|
|
343
|
+
const readStdin = deps.readStdin ?? readAllStdin;
|
|
344
|
+
try {
|
|
345
|
+
const payload = parseUserPromptSubmitPayload(await readStdin());
|
|
346
|
+
if (!payload)
|
|
347
|
+
return; // malformed/absent payload → nothing recorded (fail-open)
|
|
348
|
+
const result = detectAvoidableContext(payload.prompt);
|
|
349
|
+
if (!result.has_avoidable_context)
|
|
350
|
+
return; // honest no-op → no activity noise, no stdout
|
|
351
|
+
const cwd = payload.cwd ?? deps.cwd ?? process.cwd();
|
|
352
|
+
const event = buildClaudeCodeBeforeCallEvent(result, payload.sessionId ? { sessionId: payload.sessionId } : {});
|
|
353
|
+
await appendActivityEvent(event, path.join(cwd, ".compaction", "activity"));
|
|
354
|
+
// No stdout (would inject into the model context); the recommendation is in `compaction activity`.
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
// FAIL-OPEN: never propagate — the before-call hook must not break Claude Code.
|
|
358
|
+
}
|
|
359
|
+
}
|
|
154
360
|
/**
|
|
155
361
|
* Discover local Claude Code sessions (metadata only) and print, for each, the
|
|
156
362
|
* ready-to-run `capture claude-code --session <path>` command (guided capture).
|