@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.
- package/README.md +142 -64
- 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
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { buildOptimizeRequest, type ApiConfig, type ToolName } from "../../core/api-client/index.js";
|
|
2
|
+
import type { AgentTrace } from "../../core/types.js";
|
|
3
|
+
/**
|
|
4
|
+
* V0.4 — the smallest CLI → hosted-API upgrade loop for `optimize openai-agents` (open-core).
|
|
5
|
+
*
|
|
6
|
+
* UPGRADE PATH (design `docs/design/cli-to-hosted-api-upgrade-path.md`): when — and ONLY when —
|
|
7
|
+
* both `COMPACTION_API_URL` and `COMPACTION_API_KEY` are set, `optimize` routes through the
|
|
8
|
+
* existing pure-HTTP `api-client` to the hosted Compaction API instead of the local engine /
|
|
9
|
+
* degrade path. There is NO engine import here (pure HTTP + the FREE capture adapter), so the
|
|
10
|
+
* open-core boundary is unchanged and the public npm package stays engine-free.
|
|
11
|
+
*
|
|
12
|
+
* HARD RAILS: default request is CONTENT-FREE (`metrics_only`) — the trace content is NOT
|
|
13
|
+
* uploaded. The API key is attached as `Authorization: Bearer` by the client and is NEVER logged,
|
|
14
|
+
* printed, persisted, or placed in an error. The request body never appears in an error. A hosted
|
|
15
|
+
* failure exits non-zero with a concise, actionable message and NEVER pretends a local
|
|
16
|
+
* optimization happened. No billing-confirmed or semantic-correctness claim is made.
|
|
17
|
+
*/
|
|
18
|
+
/** True iff BOTH the hosted URL and key are configured — the explicit opt-in to the hosted path. */
|
|
19
|
+
export declare function hostedConfigured(env?: NodeJS.ProcessEnv): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Build a CONTENT-FREE (`metrics_only`) optimize request from a captured trace. No inline trace
|
|
22
|
+
* content is attached — only local token-count estimates, a local reference, and CONTENT-FREE
|
|
23
|
+
* attribution (tool/provider/model identifiers). Content-free ⇒ the api-client sends without a
|
|
24
|
+
* consent/confirm gate. Attribution is only attached when actually known; the model is taken from
|
|
25
|
+
* the captured trace (never inferred).
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildContentFreeOptimizeBody(trace: AgentTrace, reference: string, attribution?: {
|
|
28
|
+
tool?: ToolName;
|
|
29
|
+
provider?: string;
|
|
30
|
+
}): import("../../core/api-client/types.js").OptimizeRequest;
|
|
31
|
+
export type HostedOptimizeResult = {
|
|
32
|
+
kind: "ok";
|
|
33
|
+
status: number;
|
|
34
|
+
body: unknown;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "error";
|
|
37
|
+
message: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Send a pre-built content-free optimize body to the hosted API. Returns a sanitized result —
|
|
41
|
+
* an error `message` NEVER contains the API key, the Authorization header, or the request body.
|
|
42
|
+
*/
|
|
43
|
+
export declare function sendHostedOptimize(config: ApiConfig, body: ReturnType<typeof buildOptimizeRequest>): Promise<HostedOptimizeResult>;
|
|
44
|
+
/** Render the hosted result honestly. No key/body in any line; the API's own honest labels only. */
|
|
45
|
+
export declare function renderHostedOptimizeLines(result: HostedOptimizeResult): string[];
|
|
46
|
+
/**
|
|
47
|
+
* Orchestrate the hosted optimize loop: capture LOCALLY (free, no engine, no upload) → build a
|
|
48
|
+
* content-free request → send via the api-client → render honestly. Sets a non-zero exit code on
|
|
49
|
+
* any failure and never claims a local optimization occurred.
|
|
50
|
+
*/
|
|
51
|
+
export declare function runHostedOptimizeOpenAIAgents(opts: {
|
|
52
|
+
out: string;
|
|
53
|
+
commandParts: string[];
|
|
54
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { ApiNotConfirmedError, ApiTransportError, buildOptimizeRequest, ConsentError, resolveApiConfig, sendRequest } from "../../core/api-client/index.js";
|
|
3
|
+
import { captureOpenAIAgentsCommand } from "../../core/openai-agents-capture.js";
|
|
4
|
+
import { estimateTraceTokens } from "../../core/token-estimator.js";
|
|
5
|
+
/**
|
|
6
|
+
* V0.4 — the smallest CLI → hosted-API upgrade loop for `optimize openai-agents` (open-core).
|
|
7
|
+
*
|
|
8
|
+
* UPGRADE PATH (design `docs/design/cli-to-hosted-api-upgrade-path.md`): when — and ONLY when —
|
|
9
|
+
* both `COMPACTION_API_URL` and `COMPACTION_API_KEY` are set, `optimize` routes through the
|
|
10
|
+
* existing pure-HTTP `api-client` to the hosted Compaction API instead of the local engine /
|
|
11
|
+
* degrade path. There is NO engine import here (pure HTTP + the FREE capture adapter), so the
|
|
12
|
+
* open-core boundary is unchanged and the public npm package stays engine-free.
|
|
13
|
+
*
|
|
14
|
+
* HARD RAILS: default request is CONTENT-FREE (`metrics_only`) — the trace content is NOT
|
|
15
|
+
* uploaded. The API key is attached as `Authorization: Bearer` by the client and is NEVER logged,
|
|
16
|
+
* printed, persisted, or placed in an error. The request body never appears in an error. A hosted
|
|
17
|
+
* failure exits non-zero with a concise, actionable message and NEVER pretends a local
|
|
18
|
+
* optimization happened. No billing-confirmed or semantic-correctness claim is made.
|
|
19
|
+
*/
|
|
20
|
+
/** True iff BOTH the hosted URL and key are configured — the explicit opt-in to the hosted path. */
|
|
21
|
+
export function hostedConfigured(env = process.env) {
|
|
22
|
+
const url = (env.COMPACTION_API_URL ?? "").trim();
|
|
23
|
+
const key = (env.COMPACTION_API_KEY ?? "").trim();
|
|
24
|
+
return url !== "" && key !== "";
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a CONTENT-FREE (`metrics_only`) optimize request from a captured trace. No inline trace
|
|
28
|
+
* content is attached — only local token-count estimates, a local reference, and CONTENT-FREE
|
|
29
|
+
* attribution (tool/provider/model identifiers). Content-free ⇒ the api-client sends without a
|
|
30
|
+
* consent/confirm gate. Attribution is only attached when actually known; the model is taken from
|
|
31
|
+
* the captured trace (never inferred).
|
|
32
|
+
*/
|
|
33
|
+
export function buildContentFreeOptimizeBody(trace, reference, attribution = {}) {
|
|
34
|
+
const tokens = estimateTraceTokens(trace);
|
|
35
|
+
const model = typeof trace.model === "string" && trace.model.trim() !== "" ? trace.model : undefined;
|
|
36
|
+
return buildOptimizeRequest({
|
|
37
|
+
trace: { redacted: true, reference },
|
|
38
|
+
payloadClass: "metrics_only",
|
|
39
|
+
tokenSource: "local-estimate",
|
|
40
|
+
inputTokens: tokens.inputTokens,
|
|
41
|
+
outputTokens: tokens.outputTokens,
|
|
42
|
+
optimizationMode: "recommend",
|
|
43
|
+
...(attribution.tool ? { tool: attribution.tool } : {}),
|
|
44
|
+
...(attribution.provider ? { provider: attribution.provider } : {}),
|
|
45
|
+
...(model ? { model } : {})
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Send a pre-built content-free optimize body to the hosted API. Returns a sanitized result —
|
|
50
|
+
* an error `message` NEVER contains the API key, the Authorization header, or the request body.
|
|
51
|
+
*/
|
|
52
|
+
export async function sendHostedOptimize(config, body) {
|
|
53
|
+
try {
|
|
54
|
+
const response = await sendRequest(config, "/v0/optimize", body);
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const code = response.body?.error ?? "error";
|
|
57
|
+
const hint = response.status === 401
|
|
58
|
+
? "unauthorized — check that COMPACTION_API_KEY is valid for this endpoint"
|
|
59
|
+
: `HTTP ${response.status} (${code})`;
|
|
60
|
+
return { kind: "error", message: `hosted optimize rejected: ${hint}` };
|
|
61
|
+
}
|
|
62
|
+
return { kind: "ok", status: response.status, body: response.body };
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
// Sanitized — never echo the key, headers, or body. Map known client errors to a hint.
|
|
66
|
+
if (error instanceof ApiTransportError) {
|
|
67
|
+
return { kind: "error", message: "could not reach the hosted Compaction API (transport error); check COMPACTION_API_URL" };
|
|
68
|
+
}
|
|
69
|
+
if (error instanceof ConsentError || error instanceof ApiNotConfirmedError) {
|
|
70
|
+
return { kind: "error", message: "request refused before sending by the no-auto-upload gate (content-free path should not hit this)" };
|
|
71
|
+
}
|
|
72
|
+
return { kind: "error", message: "hosted optimize failed" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Render the hosted result honestly. No key/body in any line; the API's own honest labels only. */
|
|
76
|
+
export function renderHostedOptimizeLines(result) {
|
|
77
|
+
if (result.kind === "error") {
|
|
78
|
+
return [
|
|
79
|
+
chalk.red(`Hosted optimize error: ${result.message}.`),
|
|
80
|
+
"Nothing was applied; NO local optimization was performed."
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
chalk.green("Hosted optimize OK (review-only; nothing applied)."),
|
|
85
|
+
JSON.stringify(result.body, null, 2),
|
|
86
|
+
"Labels above are the hosted API's own: token deltas are local-estimate, cost is " +
|
|
87
|
+
"token-estimated-cost — NOT billing-confirmed, NOT a semantic-preservation claim, and not " +
|
|
88
|
+
"extrapolated to any time period."
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Orchestrate the hosted optimize loop: capture LOCALLY (free, no engine, no upload) → build a
|
|
93
|
+
* content-free request → send via the api-client → render honestly. Sets a non-zero exit code on
|
|
94
|
+
* any failure and never claims a local optimization occurred.
|
|
95
|
+
*/
|
|
96
|
+
export async function runHostedOptimizeOpenAIAgents(opts) {
|
|
97
|
+
const config = resolveApiConfig();
|
|
98
|
+
console.log(chalk.cyan("compaction optimize openai-agents (hosted)"));
|
|
99
|
+
console.log(`Hosted Compaction API: ${config.url}`);
|
|
100
|
+
console.log("Content-free request (metrics_only): your trace content is NOT uploaded. No auto-apply, no billing claim.");
|
|
101
|
+
// 1. Capture the trace LOCALLY (free adapter — no engine, no upload).
|
|
102
|
+
const capture = await captureOpenAIAgentsCommand(opts.commandParts, opts.out);
|
|
103
|
+
if (!capture.trace) {
|
|
104
|
+
console.error(chalk.red("Hosted optimize: local capture produced no trace (see the capture report for per-event reasons)."));
|
|
105
|
+
console.error("Nothing was applied; NO local optimization was performed.");
|
|
106
|
+
process.exitCode = 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// 2. Content-free body + 3. send (Bearer key attached by the client; never logged).
|
|
110
|
+
// Attribution is the OpenAI Agents capture adapter's known identity — tool + provider only;
|
|
111
|
+
// the model rides along from the captured trace inside buildContentFreeOptimizeBody.
|
|
112
|
+
const body = buildContentFreeOptimizeBody(capture.trace, capture.paths.capturedTracePath, {
|
|
113
|
+
tool: "openai-agents",
|
|
114
|
+
provider: "openai"
|
|
115
|
+
});
|
|
116
|
+
const result = await sendHostedOptimize(config, body);
|
|
117
|
+
// 4. Render honestly.
|
|
118
|
+
for (const line of renderHostedOptimizeLines(result))
|
|
119
|
+
console.log(line);
|
|
120
|
+
if (result.kind === "error")
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=optimize-hosted.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { runEngineCommand } from "../engine-degrade.js";
|
|
3
|
+
import { hostedConfigured, runHostedOptimizeOpenAIAgents } from "./optimize-hosted.js";
|
|
3
4
|
export function registerOptimizeCommand(program) {
|
|
4
5
|
const optimize = program.command("optimize").description("Run local trace optimization loops for supported integrations.");
|
|
5
6
|
optimize
|
|
@@ -9,6 +10,13 @@ export function registerOptimizeCommand(program) {
|
|
|
9
10
|
.argument("[commandParts...]", "Command and arguments to execute after --")
|
|
10
11
|
.allowUnknownOption(true)
|
|
11
12
|
.action(async (commandParts, options) => {
|
|
13
|
+
// UPGRADE PATH: only when BOTH COMPACTION_API_URL + COMPACTION_API_KEY are set, route through
|
|
14
|
+
// the pure-HTTP api-client to the hosted Compaction API (no engine, no default network). When
|
|
15
|
+
// unconfigured, the original local-engine-or-degrade path below runs BYTE-IDENTICALLY.
|
|
16
|
+
if (hostedConfigured()) {
|
|
17
|
+
await runHostedOptimizeOpenAIAgents({ out: options.out, commandParts });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
12
20
|
await runEngineCommand(async () => {
|
|
13
21
|
// Lazy-load the proprietary engine: present in-repo/API, excluded from the public package.
|
|
14
22
|
// When absent, runEngineCommand prints the boundary message and exits cleanly (no stack trace).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `compaction output-shaping-ab` — the operator-run path to produce + ingest a real provider-reported A/B
|
|
3
|
+
* comparison for the output-shaping policy family (increment 4). Mirrors the `billing-delta` harness:
|
|
4
|
+
* local-first, content-free, no network, no credentials.
|
|
5
|
+
*
|
|
6
|
+
* compaction output-shaping-ab init --experiment <id> --out <file> [--task-shape <label>]
|
|
7
|
+
* compaction output-shaping-ab add --experiment <file> --arm control|treatment --usage <capture-usage.json>
|
|
8
|
+
* [--eval-pass | --eval-fail] [--truncated] [--refused]
|
|
9
|
+
* compaction output-shaping-ab status --experiment <file>
|
|
10
|
+
*
|
|
11
|
+
* Honesty (binding): this PUBLIC CLI shows only the OBSERVED measurement + a conservative confidence
|
|
12
|
+
* (unavailable / review_required / observed_not_confirmed / eligible_for_engine_confirmation). It NEVER
|
|
13
|
+
* shows a confirmed output-savings number — that is produced solely by the private engine gate. A/B arms
|
|
14
|
+
* without provider-reported output tokens (e.g. Cursor) stay `unavailable`.
|
|
15
|
+
*/
|
|
16
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import chalk from "chalk";
|
|
18
|
+
import { addOutputShapingAbRun, initOutputShapingAbExperiment, summarizeOutputShapingAb } from "../../core/output-shaping-ab.js";
|
|
19
|
+
const PRIVACY_NOTE = "Operator-run, local-first. compaction.dev makes NO provider call, reads NO credential, and has NO network access. " +
|
|
20
|
+
"Token counts come from YOUR capture artifacts. This CLI shows the OBSERVED A/B only — a CONFIRMED output-savings " +
|
|
21
|
+
"number is produced solely by the engine gate (provider-reported, measured criterion, short-but-sufficient, reduction).";
|
|
22
|
+
async function loadExperiment(file) {
|
|
23
|
+
return JSON.parse(await readFile(file, "utf8"));
|
|
24
|
+
}
|
|
25
|
+
async function saveExperiment(file, experiment) {
|
|
26
|
+
await writeFile(file, JSON.stringify(experiment, null, 2), "utf8");
|
|
27
|
+
}
|
|
28
|
+
function resolveArm(arm) {
|
|
29
|
+
if (arm !== "control" && arm !== "treatment") {
|
|
30
|
+
throw new Error('--arm must be "control" or "treatment".');
|
|
31
|
+
}
|
|
32
|
+
return arm;
|
|
33
|
+
}
|
|
34
|
+
/** Operator-recorded sufficiency outcome: --eval-pass → true, --eval-fail → false, neither → undefined. */
|
|
35
|
+
function resolveEval(options) {
|
|
36
|
+
if (options.evalPass && options.evalFail)
|
|
37
|
+
throw new Error("pass --eval-pass OR --eval-fail, not both.");
|
|
38
|
+
if (options.evalPass)
|
|
39
|
+
return true;
|
|
40
|
+
if (options.evalFail)
|
|
41
|
+
return false;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
export function registerOutputShapingAbCommand(program) {
|
|
45
|
+
const ab = program
|
|
46
|
+
.command("output-shaping-ab")
|
|
47
|
+
.description("Operator-run, content-free output-shaping A/B harness: link a control arm (no shaping) and a " +
|
|
48
|
+
"treatment arm (--output-shaping) of Codex captures and show the OBSERVED provider-reported output-token " +
|
|
49
|
+
"delta. Shows confirmed savings NEVER — the engine gate does that.");
|
|
50
|
+
ab.command("init")
|
|
51
|
+
.description("Create a new output-shaping A/B experiment file.")
|
|
52
|
+
.requiredOption("--experiment <id>", "Experiment id (a short label, not content).")
|
|
53
|
+
.requiredOption("--out <file>", "Path to write the experiment JSON.")
|
|
54
|
+
.option("--task-shape <label>", "Short generic label of the shared task/prompt SHAPE (not prompt content).", "unspecified-task-shape")
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
console.log(chalk.cyan("compaction output-shaping-ab init"));
|
|
57
|
+
console.log(PRIVACY_NOTE);
|
|
58
|
+
const experiment = initOutputShapingAbExperiment({ experimentId: options.experiment, taskShape: options.taskShape ?? "unspecified-task-shape" });
|
|
59
|
+
await saveExperiment(options.out, experiment);
|
|
60
|
+
console.log(chalk.green(`Wrote ${options.out}`));
|
|
61
|
+
console.log(" Next: run both arms, then `output-shaping-ab add` each capture-usage.json.");
|
|
62
|
+
console.log(' control: compaction capture codex --out runs/a1 -- codex exec --json "TASK"');
|
|
63
|
+
console.log(' treatment: compaction capture codex --output-shaping --out runs/b1 -- codex exec --json "TASK"');
|
|
64
|
+
});
|
|
65
|
+
ab.command("add")
|
|
66
|
+
.description("Ingest a capture-usage.json sidecar into an A/B arm (provider-reported tokens + policy names).")
|
|
67
|
+
.requiredOption("--experiment <file>", "Path to the experiment JSON (from `init`).")
|
|
68
|
+
.requiredOption("--arm <arm>", 'Which arm: "control" (no shaping) or "treatment" (--output-shaping).')
|
|
69
|
+
.requiredOption("--usage <file>", "Path to a capture-usage.json sidecar written by `compaction capture`.")
|
|
70
|
+
.option("--eval-pass", "Record that the treatment output preserved all required task-outcome markers (short-but-sufficient).")
|
|
71
|
+
.option("--eval-fail", "Record that the treatment output dropped a required marker (shorter-but-lossy — not a saving).")
|
|
72
|
+
.option("--truncated", "Record that the answer was truncated/incomplete (not a saving).")
|
|
73
|
+
.option("--refused", "Record that the model refused (not a saving).")
|
|
74
|
+
.action(async (options) => {
|
|
75
|
+
console.log(chalk.cyan("compaction output-shaping-ab add"));
|
|
76
|
+
const arm = resolveArm(options.arm);
|
|
77
|
+
const evalOutcome = resolveEval(options);
|
|
78
|
+
const sidecar = JSON.parse(await readFile(options.usage, "utf8"));
|
|
79
|
+
if (arm === "treatment" && (!sidecar.outputShaping || sidecar.outputShaping.policyNames.length === 0)) {
|
|
80
|
+
console.log(chalk.yellow(" ! warning: treatment arm but this capture has no output-shaping attribution (was --output-shaping used?)."));
|
|
81
|
+
}
|
|
82
|
+
if (arm === "control" && sidecar.outputShaping && sidecar.outputShaping.policyNames.length > 0) {
|
|
83
|
+
console.log(chalk.yellow(" ! warning: control arm but this capture HAS output-shaping attribution — control should be the no-shaping arm."));
|
|
84
|
+
}
|
|
85
|
+
const run = {
|
|
86
|
+
arm,
|
|
87
|
+
outputTokens: sidecar.outputTokens,
|
|
88
|
+
inputTokens: sidecar.inputTokens,
|
|
89
|
+
providerReported: sidecar.providerReported === true,
|
|
90
|
+
tokenSource: sidecar.tokenSource,
|
|
91
|
+
...(arm === "treatment" && sidecar.outputShaping
|
|
92
|
+
? { policyFamily: sidecar.outputShaping.policyFamily, policyNames: sidecar.outputShaping.policyNames }
|
|
93
|
+
: {}),
|
|
94
|
+
...(arm === "treatment" ? { evalMarkersPreserved: evalOutcome ?? null } : {}),
|
|
95
|
+
...(options.truncated ? { truncated: true } : {}),
|
|
96
|
+
...(options.refused ? { refused: true } : {}),
|
|
97
|
+
reference: options.usage
|
|
98
|
+
};
|
|
99
|
+
const experiment = addOutputShapingAbRun(await loadExperiment(options.experiment), run);
|
|
100
|
+
await saveExperiment(options.experiment, experiment);
|
|
101
|
+
const src = run.providerReported ? chalk.green("provider-reported") : chalk.yellow(`${run.tokenSource} (not provider-reported)`);
|
|
102
|
+
console.log(` Added ${chalk.bold(arm)} run: output=${run.outputTokens ?? "unavailable"} tokens, source=${src}.`);
|
|
103
|
+
if (!run.providerReported)
|
|
104
|
+
console.log(chalk.yellow(" ! this arm is not provider-reported → it can never produce a confirmed output-savings number."));
|
|
105
|
+
});
|
|
106
|
+
ab.command("status")
|
|
107
|
+
.description("Show the OBSERVED A/B measurement + conservative confidence (never a confirmed savings number).")
|
|
108
|
+
.requiredOption("--experiment <file>", "Path to the experiment JSON.")
|
|
109
|
+
.action(async (options) => {
|
|
110
|
+
console.log(chalk.cyan("compaction output-shaping-ab status"));
|
|
111
|
+
console.log(PRIVACY_NOTE);
|
|
112
|
+
const summary = summarizeOutputShapingAb(await loadExperiment(options.experiment));
|
|
113
|
+
const fmt = (n, unit = "") => (n === null ? chalk.gray("unavailable") : `${Math.round(n)}${unit}`);
|
|
114
|
+
console.log(` experiment: ${summary.experimentId}`);
|
|
115
|
+
console.log(` N (control / treatment): ${summary.nControl} / ${summary.nTreatment}`);
|
|
116
|
+
console.log(` output_tokens_before: ${fmt(summary.outputTokensBefore)}`);
|
|
117
|
+
console.log(` output_tokens_after: ${fmt(summary.outputTokensAfter)}`);
|
|
118
|
+
console.log(` output_token_delta: ${fmt(summary.outputTokenDelta)} ${chalk.gray("(observed, not confirmed)")}`);
|
|
119
|
+
console.log(` output_token_reduction_pct: ${summary.outputTokenReductionPct === null ? chalk.gray("unavailable") : `${summary.outputTokenReductionPct.toFixed(1)}%`} ${chalk.gray("(observed)")}`);
|
|
120
|
+
console.log(` token_source: ${summary.tokenSource}`);
|
|
121
|
+
console.log(` policy family / names: ${summary.policyFamily ?? chalk.gray("none")} / ${summary.policyNames.length ? summary.policyNames.join(", ") : chalk.gray("none")}`);
|
|
122
|
+
console.log(` eval (short-but-sufficient):${summary.evalStatus}`);
|
|
123
|
+
// `eligible_for_engine_confirmation` is a PRECONDITION, not a confirmation — keep it amber, not green,
|
|
124
|
+
// so the public status is never mistaken for a confirmed saving. `unavailable` is gray; the rest amber.
|
|
125
|
+
const conf = summary.confidence === "unavailable" ? chalk.gray(summary.confidence) : chalk.yellow(summary.confidence);
|
|
126
|
+
console.log(` confidence: ${conf}`);
|
|
127
|
+
console.log(` confirmed output savings: ${chalk.gray("unavailable in the public CLI — the engine gate produces a confirmed number.")}`);
|
|
128
|
+
for (const reason of summary.reasons)
|
|
129
|
+
console.log(chalk.gray(` • ${reason}`));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=output-shaping-ab.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
/**
|
|
3
|
+
* `compaction output-shaping` — print the deterministic output-shaping instruction block to attach to a
|
|
4
|
+
* request BEFORE generation. Local, content-free; emits NO output-savings number (savings require a
|
|
5
|
+
* measured before/after + eval-confirmed sufficiency — the private/gated verification).
|
|
6
|
+
*/
|
|
7
|
+
export declare function registerOutputShapingCommand(program: Command): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { buildOutputShapingPolicy, OUTPUT_SHAPING_POLICIES, OUTPUT_SHAPING_HONESTY_NOTE } from "../../core/output-shaping.js";
|
|
3
|
+
/**
|
|
4
|
+
* `compaction output-shaping` — print the deterministic output-shaping instruction block to attach to a
|
|
5
|
+
* request BEFORE generation. Local, content-free; emits NO output-savings number (savings require a
|
|
6
|
+
* measured before/after + eval-confirmed sufficiency — the private/gated verification).
|
|
7
|
+
*/
|
|
8
|
+
export function registerOutputShapingCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command("output-shaping")
|
|
11
|
+
.description("Print a deterministic output-shaping instruction block to attach to your request BEFORE generation " +
|
|
12
|
+
"(the only mechanism that reduces provider output tokens). Local, content-free; no savings claimed.")
|
|
13
|
+
.option("--budget <tokens>", "Soft verbosity budget in output tokens (used by the verbosity-budget policy)")
|
|
14
|
+
.option("--policies <names>", "Comma-separated policy names to apply (default: the default-on set)")
|
|
15
|
+
.option("--list", "List the available output-shaping policies and exit")
|
|
16
|
+
.action((options) => {
|
|
17
|
+
if (options.list) {
|
|
18
|
+
console.log(chalk.cyan("Output-shaping policies (deterministic, rule-based):"));
|
|
19
|
+
for (const p of OUTPUT_SHAPING_POLICIES) {
|
|
20
|
+
console.log(` ${p.policy_name} [risk: ${p.risk_level}${p.defaultOn ? ", default-on" : ", off by default"}] — ${p.description}`);
|
|
21
|
+
}
|
|
22
|
+
console.log(chalk.gray(`\n${OUTPUT_SHAPING_HONESTY_NOTE}`));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const budget = options.budget !== undefined ? Number.parseInt(options.budget, 10) : undefined;
|
|
26
|
+
const policies = options.policies ? options.policies.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
|
27
|
+
const result = buildOutputShapingPolicy({
|
|
28
|
+
...(budget !== undefined && Number.isFinite(budget) ? { verbosityBudgetTokens: budget } : {}),
|
|
29
|
+
...(policies ? { policies } : {})
|
|
30
|
+
});
|
|
31
|
+
if (result.instructions === "") {
|
|
32
|
+
console.error("error: no matching output-shaping policies (use --list to see available policy names).");
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(chalk.cyan("compaction output-shaping"));
|
|
37
|
+
console.log(result.instructions);
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(chalk.gray(`Applied: ${result.applied.map((a) => a.policy_name).join(", ")}`));
|
|
40
|
+
console.log(chalk.gray(OUTPUT_SHAPING_HONESTY_NOTE));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=output-shaping.js.map
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { disablePolicyPreference, explainPolicyPreference, findPolicyPreference, readPolicyPreferences } from "../../core/policy-preferences.js";
|
|
3
|
+
/**
|
|
4
|
+
* `compaction policies` — READ-ONLY management of stored auto-apply PREFERENCES, plus the single
|
|
5
|
+
* safe mutating verb `disable`. There is deliberately NO `enable` here: enabling a preference only
|
|
6
|
+
* ever happens via the future live post-approval ask (the founder's binary question), never as a
|
|
7
|
+
* CLI gesture. Disabling can only ever REDUCE what could apply, so it is always safe.
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT: none of these commands apply anything. A preference is a stored intent only; no
|
|
10
|
+
* auto-apply engine exists (see `src/core/policy-preferences.ts`).
|
|
11
|
+
*/
|
|
12
|
+
function scopeLine(preference) {
|
|
13
|
+
return preference.scope.repo
|
|
14
|
+
? `${preference.scope.tool} (repo ${preference.scope.repo})`
|
|
15
|
+
: preference.scope.tool;
|
|
16
|
+
}
|
|
17
|
+
function formatPreferencesList(preferences) {
|
|
18
|
+
if (preferences.length === 0) {
|
|
19
|
+
return "No auto-apply preferences saved. The default is ask-each-time — Compaction asks before applying anything.";
|
|
20
|
+
}
|
|
21
|
+
const header = ["id", "scope", "policy", "preference", "enabled", "gates_required"];
|
|
22
|
+
const rows = preferences.map((preference) => [
|
|
23
|
+
preference.id,
|
|
24
|
+
scopeLine(preference),
|
|
25
|
+
preference.scope.policy_type,
|
|
26
|
+
preference.preference,
|
|
27
|
+
preference.enabled ? "yes" : "no",
|
|
28
|
+
`${preference.gates_required.length} gate(s)`
|
|
29
|
+
]);
|
|
30
|
+
const widths = header.map((cell, col) => Math.max(cell.length, ...rows.map((line) => line[col].length)));
|
|
31
|
+
const pad = (cells) => cells.map((cell, col) => cell.padEnd(widths[col])).join(" ");
|
|
32
|
+
const preamble = [
|
|
33
|
+
`Saved auto-apply preferences (${preferences.length}). These are STORED INTENTS only — no auto-apply`,
|
|
34
|
+
`engine exists yet, so nothing is applied automatically today.`,
|
|
35
|
+
``
|
|
36
|
+
];
|
|
37
|
+
const lines = [pad(header), pad(header.map((_, col) => "-".repeat(widths[col]))), ...rows.map(pad)];
|
|
38
|
+
return [...preamble, ...lines, ``, `Explain one: compaction policies explain <id>`, `Turn one off: compaction policies disable <id>`].join("\n");
|
|
39
|
+
}
|
|
40
|
+
export function registerPoliciesCommand(program) {
|
|
41
|
+
const policies = program
|
|
42
|
+
.command("policies")
|
|
43
|
+
.description("View and disable stored auto-apply preferences (read-only + safe disable; no enable, no apply).");
|
|
44
|
+
policies
|
|
45
|
+
.command("list")
|
|
46
|
+
.description("List saved auto-apply preferences (id, scope, policy, preference, enabled, gates_required).")
|
|
47
|
+
.action(async () => {
|
|
48
|
+
const { preferences } = await readPolicyPreferences();
|
|
49
|
+
console.log(chalk.cyan(formatPreferencesList(preferences)));
|
|
50
|
+
});
|
|
51
|
+
policies
|
|
52
|
+
.command("disable <id>")
|
|
53
|
+
.description("Disable a saved preference by id (always safe — never enables, never applies).")
|
|
54
|
+
.action(async (id) => {
|
|
55
|
+
const result = await disablePolicyPreference(id);
|
|
56
|
+
if (!result.disabled) {
|
|
57
|
+
console.error(chalk.red(result.reason));
|
|
58
|
+
process.exitCode = 1;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (result.alreadyDisabled) {
|
|
62
|
+
console.log(chalk.yellow(`Preference ${id} was already disabled — nothing changed.`));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
console.log(chalk.green(`Disabled preference ${id}. It will not be honored by any future auto-apply.`));
|
|
66
|
+
});
|
|
67
|
+
policies
|
|
68
|
+
.command("explain <id>")
|
|
69
|
+
.description("Explain in plain language what a saved preference means and how to disable it.")
|
|
70
|
+
.action(async (id) => {
|
|
71
|
+
const preference = await findPolicyPreference(id);
|
|
72
|
+
if (!preference) {
|
|
73
|
+
console.error(chalk.red(`No preference with id "${id}" — run "compaction policies list" to see saved ids.`));
|
|
74
|
+
process.exitCode = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.cyan(explainPolicyPreference(preference)));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=policies.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { type BeforeCallRecommendation } from "../../core/before-call.js";
|
|
3
|
+
/** The input-boundary an approved apply would mutate (drives the honest UX + activity wording). */
|
|
4
|
+
export type ApplyBoundary = "prompt-flag" | "stdin";
|
|
5
|
+
/** A minimal injectable terminal I/O surface (so the [y/n/v] decision is unit-testable off /dev/tty). */
|
|
6
|
+
export interface PrecallTty {
|
|
7
|
+
write(text: string): void;
|
|
8
|
+
/** Read one trimmed line; empty string on EOF. */
|
|
9
|
+
readLine(): string;
|
|
10
|
+
}
|
|
11
|
+
export type ApplyChoice = "apply" | "decline";
|
|
12
|
+
/**
|
|
13
|
+
* Drive the interactive `[y] apply · [n] no · [v] view details` decision on `io`. FAIL-CLOSED to
|
|
14
|
+
* DECLINE: `y` (and only `y`) approves; `v` shows content-free details then re-prompts; `n`, empty
|
|
15
|
+
* (Enter), EOF, or ANY other input declines (the original runs unchanged). No default yes. This is the
|
|
16
|
+
* function the PTY/`[v]` test drives directly with a fake tty stream (charter run-11 fallback).
|
|
17
|
+
*/
|
|
18
|
+
export declare function runApplyDecision(io: PrecallTty, rec: BeforeCallRecommendation, boundary?: ApplyBoundary): ApplyChoice;
|
|
19
|
+
export declare function registerPrecallCommand(program: Command): void;
|