@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LOCAL per-run token_source records for the unified `compaction run` flow (PUBLIC CLI/SDK — engine-free).
|
|
3
|
+
* Design: `docs/design/unified-run-flow.md` (ACCEPTED 2026-06-28) — this is the LOCAL half of the record
|
|
4
|
+
* step (founder lane-splitting ruling 2026-07-02: local run/reporting work is ungated; hosted-record
|
|
5
|
+
* wiring stays gated). No network, no hosted config, no schema/trace-format change: each record is an
|
|
6
|
+
* ADDITIVE local JSON file that embeds the existing `RunFlowTokenReport` shape unchanged.
|
|
7
|
+
*
|
|
8
|
+
* What a record is: the SAME honest per-run token report the `run` front-ends already print (input and
|
|
9
|
+
* output SEPARATE, per-field `token_source` provider-reported | local-estimate | unavailable, honest
|
|
10
|
+
* notes) — persisted content-free so runs ACCUMULATE locally and `compaction summary` can roll them up
|
|
11
|
+
* per tool. Counts + sources only: no prompt/output text, no filenames from the wrapped command, no
|
|
12
|
+
* cost figures, and NEVER a savings figure for output (observed output tokens are tokens, not savings —
|
|
13
|
+
* founder D2; output-token savings stay gated on measured + eval-confirmed output-shaping).
|
|
14
|
+
*
|
|
15
|
+
* The per-tool rollup mirrors the `/app` source-status contract tiers EXACTLY
|
|
16
|
+
* (`docs/ops/app-source-status-contract.md`): live (provider-reported) / estimated (local-estimate) /
|
|
17
|
+
* unavailable — <reason> / no records yet. `output_savings` is the LITERAL string "unavailable".
|
|
18
|
+
*/
|
|
19
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { writeJsonArtifact } from "./artifact-writer.js";
|
|
22
|
+
/** Default local accumulation directory (sibling of `.compaction/runs`; flat, one file per run). */
|
|
23
|
+
export const DEFAULT_RUN_RECORDS_DIRECTORY = ".compaction/run-records";
|
|
24
|
+
/** File name of the per-run copy written next to the run's other artifacts. */
|
|
25
|
+
export const RUN_TOKEN_RECORD_FILENAME = "run-token-record.json";
|
|
26
|
+
/** Contract copy (docs/ops/app-source-status-contract.md) — tier labels, verbatim. */
|
|
27
|
+
export const TIER_LIVE_LABEL = "live (provider-reported)";
|
|
28
|
+
export const TIER_ESTIMATED_LABEL = "estimated (local-estimate)";
|
|
29
|
+
/** Contract copy — the literal output-savings line every tool renders in v1 (no tool has passed the gate). */
|
|
30
|
+
export const OUTPUT_SAVINGS_UNAVAILABLE_LINE = "output-token savings: unavailable — needs a provider-reported A/B that passes the measured criterion + the short-but-sufficient eval.";
|
|
31
|
+
export function buildLocalRunTokenRecord(params) {
|
|
32
|
+
return {
|
|
33
|
+
record_version: 1,
|
|
34
|
+
run_id: params.runId,
|
|
35
|
+
recorded_at: params.recordedAt ?? new Date().toISOString(),
|
|
36
|
+
token_report: params.tokenReport,
|
|
37
|
+
...(params.crossSurfaceEvent !== undefined ? { cross_surface_event: params.crossSurfaceEvent } : {})
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const TOKEN_SOURCES = ["provider-reported", "local-estimate", "unavailable"];
|
|
41
|
+
function isTokenSource(value) {
|
|
42
|
+
return typeof value === "string" && TOKEN_SOURCES.includes(value);
|
|
43
|
+
}
|
|
44
|
+
function isOptionalCount(value) {
|
|
45
|
+
return value === undefined || (typeof value === "number" && Number.isFinite(value) && value >= 0);
|
|
46
|
+
}
|
|
47
|
+
/** Validate a parsed record. Invalid records are SKIPPED with a reason — never guessed at. */
|
|
48
|
+
export function validateLocalRunTokenRecord(value) {
|
|
49
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
50
|
+
return { reason: "record is not a JSON object" };
|
|
51
|
+
}
|
|
52
|
+
const record = value;
|
|
53
|
+
if (record.record_version !== 1)
|
|
54
|
+
return { reason: "unsupported record_version" };
|
|
55
|
+
if (typeof record.run_id !== "string" || record.run_id.trim() === "")
|
|
56
|
+
return { reason: "missing or invalid run_id" };
|
|
57
|
+
if (typeof record.recorded_at !== "string" || record.recorded_at.trim() === "") {
|
|
58
|
+
return { reason: "missing or invalid recorded_at" };
|
|
59
|
+
}
|
|
60
|
+
const report = record.token_report;
|
|
61
|
+
if (typeof report !== "object" || report === null || Array.isArray(report)) {
|
|
62
|
+
return { reason: "missing or invalid token_report" };
|
|
63
|
+
}
|
|
64
|
+
if (typeof report.tool !== "string" || report.tool.trim() === "")
|
|
65
|
+
return { reason: "missing or invalid token_report.tool" };
|
|
66
|
+
if (!isTokenSource(report.input_token_source))
|
|
67
|
+
return { reason: "invalid token_report.input_token_source" };
|
|
68
|
+
if (!isTokenSource(report.output_token_source))
|
|
69
|
+
return { reason: "invalid token_report.output_token_source" };
|
|
70
|
+
if (!isOptionalCount(report.input_tokens))
|
|
71
|
+
return { reason: "invalid token_report.input_tokens" };
|
|
72
|
+
if (!isOptionalCount(report.output_tokens))
|
|
73
|
+
return { reason: "invalid token_report.output_tokens" };
|
|
74
|
+
if (report.input_reduction_label !== "measured" && report.input_reduction_label !== "estimated") {
|
|
75
|
+
return { reason: "invalid token_report.input_reduction_label" };
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(report.notes) || report.notes.some((note) => typeof note !== "string")) {
|
|
78
|
+
return { reason: "invalid token_report.notes" };
|
|
79
|
+
}
|
|
80
|
+
return { record: record };
|
|
81
|
+
}
|
|
82
|
+
function accumulationFileName(runId) {
|
|
83
|
+
return `${runId.replace(/[^A-Za-z0-9._-]/g, "-")}.json`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Persist the record LOCALLY, twice: once next to the run's artifacts (`run-token-record.json` in the
|
|
87
|
+
* run's output directory) and once in the flat accumulation directory so `summary` can roll runs up
|
|
88
|
+
* without scanning arbitrary --out locations. Local file writes only — never a network call.
|
|
89
|
+
*/
|
|
90
|
+
export async function writeLocalRunTokenRecord(record, artifactDirectory, recordsDirectory = DEFAULT_RUN_RECORDS_DIRECTORY) {
|
|
91
|
+
const artifactPath = await writeJsonArtifact(artifactDirectory, RUN_TOKEN_RECORD_FILENAME, record);
|
|
92
|
+
const accumulatedPath = await writeJsonArtifact(recordsDirectory, accumulationFileName(record.run_id), record);
|
|
93
|
+
return { artifactPath, accumulatedPath };
|
|
94
|
+
}
|
|
95
|
+
/** Read all accumulated records. A missing directory means "no records yet" (empty, not an error). */
|
|
96
|
+
export async function readLocalRunTokenRecords(recordsDirectory = DEFAULT_RUN_RECORDS_DIRECTORY) {
|
|
97
|
+
let fileNames;
|
|
98
|
+
try {
|
|
99
|
+
const entries = await readdir(recordsDirectory, { withFileTypes: true });
|
|
100
|
+
fileNames = entries
|
|
101
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith(".json"))
|
|
102
|
+
.map((entry) => entry.name)
|
|
103
|
+
.sort();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "unknown";
|
|
107
|
+
if (code === "ENOENT")
|
|
108
|
+
return { records: [], skipped: [] };
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
const records = [];
|
|
112
|
+
const skipped = [];
|
|
113
|
+
for (const fileName of fileNames) {
|
|
114
|
+
const path = join(recordsDirectory, fileName);
|
|
115
|
+
try {
|
|
116
|
+
const validation = validateLocalRunTokenRecord(JSON.parse(await readFile(path, "utf8")));
|
|
117
|
+
if (validation.record === undefined) {
|
|
118
|
+
skipped.push({ path, reason: validation.reason ?? "invalid record" });
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
records.push({ path, record: validation.record });
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
skipped.push({ path, reason: error instanceof SyntaxError ? "invalid JSON" : "could not read record" });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return { records, skipped };
|
|
128
|
+
}
|
|
129
|
+
const MAX_ROLLUP_NOTES = 8;
|
|
130
|
+
const AXIS_UNAVAILABLE_FALLBACK_REASON = "not safely separable / not reported";
|
|
131
|
+
function emptyAxis() {
|
|
132
|
+
return {
|
|
133
|
+
provider_reported: { runs: 0, tokens: 0 },
|
|
134
|
+
local_estimate: { runs: 0, tokens: 0 },
|
|
135
|
+
unavailable: { runs: 0, reasons: [] }
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function addDistinctCapped(list, value, cap) {
|
|
139
|
+
if (list.length < cap && !list.includes(value))
|
|
140
|
+
list.push(value);
|
|
141
|
+
}
|
|
142
|
+
function addToAxis(axis, axisName, source, tokens, notes) {
|
|
143
|
+
// Honesty rail: a count lands in a tier ONLY when the source is that tier AND a real count exists.
|
|
144
|
+
// A missing count — whatever the claimed source — is `unavailable`, never a silent zero.
|
|
145
|
+
if (source === "provider-reported" && tokens !== undefined) {
|
|
146
|
+
axis.provider_reported.runs += 1;
|
|
147
|
+
axis.provider_reported.tokens += tokens;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (source === "local-estimate" && tokens !== undefined) {
|
|
151
|
+
axis.local_estimate.runs += 1;
|
|
152
|
+
axis.local_estimate.tokens += tokens;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
axis.unavailable.runs += 1;
|
|
156
|
+
// Prefer the notes that actually talk about THIS axis (e.g. the output-unavailable reason for the
|
|
157
|
+
// output axis) so the displayed reason is the honest one; fall back to all notes, then the generic.
|
|
158
|
+
const axisNotes = notes.filter((note) => note.toLowerCase().includes(axisName));
|
|
159
|
+
const reasons = axisNotes.length > 0 ? axisNotes : notes.length > 0 ? notes : [AXIS_UNAVAILABLE_FALLBACK_REASON];
|
|
160
|
+
for (const reason of reasons)
|
|
161
|
+
addDistinctCapped(axis.unavailable.reasons, reason, MAX_ROLLUP_NOTES);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Roll accumulated records up PER TOOL (PURE). Counts + sources only — no cost, no savings math, no
|
|
165
|
+
* content. Tools are sorted for deterministic output. `output_savings` is always the literal
|
|
166
|
+
* "unavailable" (contract v1: no tool has measured + eval-confirmed output-shaping evidence).
|
|
167
|
+
*/
|
|
168
|
+
export function summarizePerToolRunRecords(records) {
|
|
169
|
+
const byTool = new Map();
|
|
170
|
+
for (const record of records) {
|
|
171
|
+
const report = record.token_report;
|
|
172
|
+
let rollup = byTool.get(report.tool);
|
|
173
|
+
if (rollup === undefined) {
|
|
174
|
+
rollup = { tool: report.tool, runs: 0, input: emptyAxis(), output: emptyAxis(), output_savings: "unavailable", notes: [] };
|
|
175
|
+
byTool.set(report.tool, rollup);
|
|
176
|
+
}
|
|
177
|
+
rollup.runs += 1;
|
|
178
|
+
addToAxis(rollup.input, "input", report.input_token_source, report.input_tokens, report.notes);
|
|
179
|
+
addToAxis(rollup.output, "output", report.output_token_source, report.output_tokens, report.notes);
|
|
180
|
+
for (const note of report.notes)
|
|
181
|
+
addDistinctCapped(rollup.notes, note, MAX_ROLLUP_NOTES);
|
|
182
|
+
}
|
|
183
|
+
return [...byTool.values()].sort((a, b) => a.tool.localeCompare(b.tool));
|
|
184
|
+
}
|
|
185
|
+
function axisParts(axis) {
|
|
186
|
+
const parts = [];
|
|
187
|
+
if (axis.provider_reported.runs > 0) {
|
|
188
|
+
parts.push(`${axis.provider_reported.tokens} tokens ${TIER_LIVE_LABEL} across ${axis.provider_reported.runs} run(s)`);
|
|
189
|
+
}
|
|
190
|
+
if (axis.local_estimate.runs > 0) {
|
|
191
|
+
parts.push(`${axis.local_estimate.tokens} tokens ${TIER_ESTIMATED_LABEL} across ${axis.local_estimate.runs} run(s)`);
|
|
192
|
+
}
|
|
193
|
+
if (axis.unavailable.runs > 0) {
|
|
194
|
+
const reason = axis.unavailable.reasons[0] ?? AXIS_UNAVAILABLE_FALLBACK_REASON;
|
|
195
|
+
parts.push(`unavailable for ${axis.unavailable.runs} run(s) — ${reason}`);
|
|
196
|
+
}
|
|
197
|
+
return parts.length > 0 ? parts : [`unavailable — ${AXIS_UNAVAILABLE_FALLBACK_REASON}`];
|
|
198
|
+
}
|
|
199
|
+
/** Header stating exactly what this section is (and is not) — printed above the per-tool lines. */
|
|
200
|
+
export const PER_TOOL_RECORDS_HEADER = "Per-tool run records (local, content-free): observed token counts + their token_source per axis; " +
|
|
201
|
+
"input and output separate; output shown as tokens, never as a savings figure.";
|
|
202
|
+
/** The honest empty state — never fabricated rows for tools that have no records. */
|
|
203
|
+
export const PER_TOOL_RECORDS_EMPTY_LINE = "- none yet — `compaction run …` / `run codex` / `run cursor` accumulate per-run token_source records locally";
|
|
204
|
+
/**
|
|
205
|
+
* Render the per-tool rollup as indented list lines (shared by the console summary and summary.md).
|
|
206
|
+
* Copy mirrors the `/app` source-status contract: live (provider-reported) / estimated (local-estimate) /
|
|
207
|
+
* unavailable — <reason>; plus the literal output-savings unavailable line for EVERY tool.
|
|
208
|
+
*/
|
|
209
|
+
export function formatPerToolRunRecordLines(rollups) {
|
|
210
|
+
if (rollups.length === 0)
|
|
211
|
+
return [PER_TOOL_RECORDS_EMPTY_LINE];
|
|
212
|
+
const lines = [];
|
|
213
|
+
for (const rollup of rollups) {
|
|
214
|
+
lines.push(`- ${rollup.tool}: ${rollup.runs} run(s)`);
|
|
215
|
+
lines.push(` - input: ${axisParts(rollup.input).join("; ")}`);
|
|
216
|
+
lines.push(` - output: ${axisParts(rollup.output).join("; ")}`);
|
|
217
|
+
lines.push(` - ${OUTPUT_SAVINGS_UNAVAILABLE_LINE}`);
|
|
218
|
+
for (const note of rollup.notes)
|
|
219
|
+
lines.push(` - note: ${note}`);
|
|
220
|
+
}
|
|
221
|
+
return lines;
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=local-run-record.js.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-shaping A/B experiment record + summary (PUBLIC CLI/SDK code — engine-free, ships in the npm CLI).
|
|
3
|
+
* Increment 4 of the output-shaping policy family (`docs/design/output-shaping-policy-family.md`): the
|
|
4
|
+
* smallest reliable OPERATOR-RUN path to produce + ingest a real provider-reported A/B comparison between
|
|
5
|
+
* a control arm (Codex live wrapper without output-shaping) and a treatment arm (with `--output-shaping`).
|
|
6
|
+
*
|
|
7
|
+
* Boundary / honesty (binding):
|
|
8
|
+
* - This PUBLIC module links the two arms and computes the **observed** measurement (means, delta,
|
|
9
|
+
* reduction %, N per arm) for operator visibility. It is **descriptive, NOT a savings claim.**
|
|
10
|
+
* - It NEVER produces a CONFIRMED output-savings number. `confirmedSavingsEligible` is the literal `false`
|
|
11
|
+
* here; a confirmed number can only ever be produced by the PRIVATE engine gate
|
|
12
|
+
* (`src/engine/output-shaping-verification.ts` — provider-reported A/B meeting the billing-delta
|
|
13
|
+
* criterion AND short-but-sufficient eval AND a reduction).
|
|
14
|
+
* - Output tokens that are not provider-reported (e.g. Cursor — local-estimate only) yield
|
|
15
|
+
* `confidence: "unavailable"` and can never be eligible. Missing output tokens stay unavailable, not 0.
|
|
16
|
+
* - The record is content-free: it stores token counts, the honest source, policy NAMES, operator eval
|
|
17
|
+
* outcome, and truncation/refusal flags — no prompt/response/trace content. Local artifact paths are
|
|
18
|
+
* operator-side references only.
|
|
19
|
+
*/
|
|
20
|
+
import type { TokenSource, ToolName } from "./api-client/index.js";
|
|
21
|
+
export declare const OUTPUT_SHAPING_AB_SCHEMA: "output-shaping.ab-experiment.v1";
|
|
22
|
+
export declare const CAPTURE_USAGE_SIDECAR_SCHEMA: "compaction.capture-usage.v1";
|
|
23
|
+
export declare const OUTPUT_SHAPING_POLICY_FAMILY: "output_shaping";
|
|
24
|
+
/**
|
|
25
|
+
* Written next to a capture artifact so a later A/B `add` can ingest the **provider-reported** tokens +
|
|
26
|
+
* (treatment) policy attribution without re-running anything. Content-free: counts + source + policy names.
|
|
27
|
+
*/
|
|
28
|
+
export interface CaptureUsageSidecar {
|
|
29
|
+
schema: typeof CAPTURE_USAGE_SIDECAR_SCHEMA;
|
|
30
|
+
tool: ToolName;
|
|
31
|
+
provider?: string;
|
|
32
|
+
model?: string;
|
|
33
|
+
inputTokens: number | null;
|
|
34
|
+
outputTokens: number | null;
|
|
35
|
+
/** True ONLY when the counts are provider-reported (never local-estimate / unavailable). */
|
|
36
|
+
providerReported: boolean;
|
|
37
|
+
tokenSource: TokenSource;
|
|
38
|
+
/** "present" when the provider emitted a usage block; "missing" when usage was absent (never invented). */
|
|
39
|
+
tokenMetadataStatus: "present" | "missing";
|
|
40
|
+
/** Present on the treatment arm when an output-shaping policy was attached BEFORE generation. */
|
|
41
|
+
outputShaping?: {
|
|
42
|
+
policyFamily: typeof OUTPUT_SHAPING_POLICY_FAMILY;
|
|
43
|
+
policyNames: string[];
|
|
44
|
+
};
|
|
45
|
+
generatedAt: string;
|
|
46
|
+
}
|
|
47
|
+
export interface BuildCaptureUsageSidecarInput {
|
|
48
|
+
tool: ToolName;
|
|
49
|
+
provider?: string;
|
|
50
|
+
model?: string;
|
|
51
|
+
inputTokens?: number;
|
|
52
|
+
outputTokens?: number;
|
|
53
|
+
providerReported: boolean;
|
|
54
|
+
tokenSource: TokenSource;
|
|
55
|
+
tokenMetadataStatus: "present" | "missing";
|
|
56
|
+
policyNames?: string[];
|
|
57
|
+
generatedAt?: string;
|
|
58
|
+
}
|
|
59
|
+
export declare function buildCaptureUsageSidecar(input: BuildCaptureUsageSidecarInput): CaptureUsageSidecar;
|
|
60
|
+
export type OutputShapingAbArm = "control" | "treatment";
|
|
61
|
+
export interface OutputShapingAbRun {
|
|
62
|
+
arm: OutputShapingAbArm;
|
|
63
|
+
/** Provider-reported output tokens; null when unavailable (never invented as 0). */
|
|
64
|
+
outputTokens: number | null;
|
|
65
|
+
inputTokens: number | null;
|
|
66
|
+
/** True ONLY when the output tokens are provider-reported. */
|
|
67
|
+
providerReported: boolean;
|
|
68
|
+
tokenSource: TokenSource;
|
|
69
|
+
/** Treatment arm: the output-shaping policy family/names attached BEFORE generation. */
|
|
70
|
+
policyFamily?: typeof OUTPUT_SHAPING_POLICY_FAMILY;
|
|
71
|
+
policyNames?: string[];
|
|
72
|
+
/**
|
|
73
|
+
* Operator/eval-recorded short-but-sufficient outcome for a TREATMENT run: did the shaped (shorter)
|
|
74
|
+
* output preserve all required task-outcome markers? `null`/absent ⇒ not yet evaluated (review required).
|
|
75
|
+
* The rigorous marker check is the engine eval; this is the recorded outcome.
|
|
76
|
+
*/
|
|
77
|
+
evalMarkersPreserved?: boolean | null;
|
|
78
|
+
/** Observed truncation of the answer (an incomplete answer is not a saving). */
|
|
79
|
+
truncated?: boolean;
|
|
80
|
+
/** Observed refusal (a refusal is not a saving). */
|
|
81
|
+
refused?: boolean;
|
|
82
|
+
/** Local artifact path — operator-side evidence only, never content. */
|
|
83
|
+
reference?: string;
|
|
84
|
+
}
|
|
85
|
+
export interface OutputShapingAbExperiment {
|
|
86
|
+
schema: typeof OUTPUT_SHAPING_AB_SCHEMA;
|
|
87
|
+
experimentId: string;
|
|
88
|
+
/** A short generic label of the shared task/prompt SHAPE (operator-supplied; not prompt content). */
|
|
89
|
+
taskShape: string;
|
|
90
|
+
createdAt: string;
|
|
91
|
+
runs: OutputShapingAbRun[];
|
|
92
|
+
}
|
|
93
|
+
export interface InitOutputShapingAbInput {
|
|
94
|
+
experimentId: string;
|
|
95
|
+
taskShape: string;
|
|
96
|
+
createdAt?: string;
|
|
97
|
+
}
|
|
98
|
+
export declare function initOutputShapingAbExperiment(input: InitOutputShapingAbInput): OutputShapingAbExperiment;
|
|
99
|
+
export declare function addOutputShapingAbRun(experiment: OutputShapingAbExperiment, run: OutputShapingAbRun): OutputShapingAbExperiment;
|
|
100
|
+
/**
|
|
101
|
+
* Public confidence ladder. NONE of these is a confirmed savings claim.
|
|
102
|
+
* - `unavailable` — an arm's output tokens are not provider-reported / are missing.
|
|
103
|
+
* - `review_required` — truncation/refusal observed, or treatment sufficiency not a clean pass.
|
|
104
|
+
* - `observed_not_confirmed` — provider-reported both arms, sufficiency passed, but N < 3 per arm.
|
|
105
|
+
* - `eligible_for_engine_confirmation` — all public preconditions met; the CONFIRMED verdict is an engine step.
|
|
106
|
+
*/
|
|
107
|
+
export type OutputShapingAbConfidence = "unavailable" | "review_required" | "observed_not_confirmed" | "eligible_for_engine_confirmation";
|
|
108
|
+
export type OutputShapingAbEvalStatus = "pass" | "fail" | "not_evaluated" | "mixed";
|
|
109
|
+
export interface OutputShapingAbSummary {
|
|
110
|
+
experimentId: string;
|
|
111
|
+
nControl: number;
|
|
112
|
+
nTreatment: number;
|
|
113
|
+
/** Mean control (no-shaping) output tokens — provider-reported; null when unavailable. */
|
|
114
|
+
outputTokensBefore: number | null;
|
|
115
|
+
/** Mean treatment (shaped) output tokens — provider-reported; null when unavailable. */
|
|
116
|
+
outputTokensAfter: number | null;
|
|
117
|
+
/** before − after: positive ⇒ treatment used FEWER output tokens. OBSERVED, not a confirmed saving. */
|
|
118
|
+
outputTokenDelta: number | null;
|
|
119
|
+
outputTokenReductionPct: number | null;
|
|
120
|
+
tokenSource: "provider-reported" | "mixed" | "local-estimate" | "unavailable";
|
|
121
|
+
policyFamily: string | null;
|
|
122
|
+
policyNames: string[];
|
|
123
|
+
evalStatus: OutputShapingAbEvalStatus;
|
|
124
|
+
confidence: OutputShapingAbConfidence;
|
|
125
|
+
/** ALWAYS false in the public CLI — a confirmed savings number is produced only by the engine gate. */
|
|
126
|
+
confirmedSavingsEligible: false;
|
|
127
|
+
reasons: string[];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Compute the OBSERVED A/B measurement + a conservative confidence. Never confirms a saving (that is the
|
|
131
|
+
* engine gate). The decision order is fail-safe: unavailable → review_required → observed_not_confirmed →
|
|
132
|
+
* eligible_for_engine_confirmation. Means/delta/pct are computed only from provider-reported output tokens.
|
|
133
|
+
*/
|
|
134
|
+
export declare function summarizeOutputShapingAb(experiment: OutputShapingAbExperiment): OutputShapingAbSummary;
|
|
135
|
+
/** Provider-reported output-token arrays per arm — the only honest input to the engine confirmation gate. */
|
|
136
|
+
export declare function providerReportedOutputArms(experiment: OutputShapingAbExperiment): {
|
|
137
|
+
controlOutputTokens: number[];
|
|
138
|
+
treatmentOutputTokens: number[];
|
|
139
|
+
bothProviderReported: boolean;
|
|
140
|
+
};
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
export const OUTPUT_SHAPING_AB_SCHEMA = "output-shaping.ab-experiment.v1";
|
|
2
|
+
export const CAPTURE_USAGE_SIDECAR_SCHEMA = "compaction.capture-usage.v1";
|
|
3
|
+
export const OUTPUT_SHAPING_POLICY_FAMILY = "output_shaping";
|
|
4
|
+
export function buildCaptureUsageSidecar(input) {
|
|
5
|
+
return {
|
|
6
|
+
schema: CAPTURE_USAGE_SIDECAR_SCHEMA,
|
|
7
|
+
tool: input.tool,
|
|
8
|
+
...(input.provider ? { provider: input.provider } : {}),
|
|
9
|
+
...(input.model ? { model: input.model } : {}),
|
|
10
|
+
inputTokens: typeof input.inputTokens === "number" ? input.inputTokens : null,
|
|
11
|
+
outputTokens: typeof input.outputTokens === "number" ? input.outputTokens : null,
|
|
12
|
+
providerReported: input.providerReported,
|
|
13
|
+
tokenSource: input.tokenSource,
|
|
14
|
+
tokenMetadataStatus: input.tokenMetadataStatus,
|
|
15
|
+
...(input.policyNames && input.policyNames.length > 0
|
|
16
|
+
? { outputShaping: { policyFamily: OUTPUT_SHAPING_POLICY_FAMILY, policyNames: input.policyNames } }
|
|
17
|
+
: {}),
|
|
18
|
+
generatedAt: input.generatedAt ?? new Date().toISOString()
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
export function initOutputShapingAbExperiment(input) {
|
|
22
|
+
return {
|
|
23
|
+
schema: OUTPUT_SHAPING_AB_SCHEMA,
|
|
24
|
+
experimentId: input.experimentId,
|
|
25
|
+
taskShape: input.taskShape,
|
|
26
|
+
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
27
|
+
runs: []
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function addOutputShapingAbRun(experiment, run) {
|
|
31
|
+
return { ...experiment, runs: [...experiment.runs, run] };
|
|
32
|
+
}
|
|
33
|
+
const MIN_RUNS_PER_ARM = 3;
|
|
34
|
+
function mean(xs) {
|
|
35
|
+
return xs.reduce((s, x) => s + x, 0) / xs.length;
|
|
36
|
+
}
|
|
37
|
+
function summarizeArmSource(runs) {
|
|
38
|
+
if (runs.length === 0)
|
|
39
|
+
return "unavailable";
|
|
40
|
+
const allProvider = runs.every((r) => r.providerReported && typeof r.outputTokens === "number");
|
|
41
|
+
if (allProvider)
|
|
42
|
+
return "provider-reported";
|
|
43
|
+
const anyProvider = runs.some((r) => r.providerReported && typeof r.outputTokens === "number");
|
|
44
|
+
if (anyProvider)
|
|
45
|
+
return "mixed";
|
|
46
|
+
const anyEstimate = runs.some((r) => r.tokenSource === "local-estimate");
|
|
47
|
+
return anyEstimate ? "local-estimate" : "unavailable";
|
|
48
|
+
}
|
|
49
|
+
function treatmentEvalStatus(treatment) {
|
|
50
|
+
if (treatment.length === 0)
|
|
51
|
+
return "not_evaluated";
|
|
52
|
+
const outcomes = treatment.map((r) => r.evalMarkersPreserved);
|
|
53
|
+
if (outcomes.some((o) => o === undefined || o === null)) {
|
|
54
|
+
return outcomes.some((o) => o === true || o === false) ? "mixed" : "not_evaluated";
|
|
55
|
+
}
|
|
56
|
+
if (outcomes.every((o) => o === true))
|
|
57
|
+
return "pass";
|
|
58
|
+
if (outcomes.every((o) => o === false))
|
|
59
|
+
return "fail";
|
|
60
|
+
return "mixed";
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Compute the OBSERVED A/B measurement + a conservative confidence. Never confirms a saving (that is the
|
|
64
|
+
* engine gate). The decision order is fail-safe: unavailable → review_required → observed_not_confirmed →
|
|
65
|
+
* eligible_for_engine_confirmation. Means/delta/pct are computed only from provider-reported output tokens.
|
|
66
|
+
*/
|
|
67
|
+
export function summarizeOutputShapingAb(experiment) {
|
|
68
|
+
const control = experiment.runs.filter((r) => r.arm === "control");
|
|
69
|
+
const treatment = experiment.runs.filter((r) => r.arm === "treatment");
|
|
70
|
+
const reasons = [];
|
|
71
|
+
const controlOut = control.filter((r) => r.providerReported && typeof r.outputTokens === "number").map((r) => r.outputTokens);
|
|
72
|
+
const treatmentOut = treatment.filter((r) => r.providerReported && typeof r.outputTokens === "number").map((r) => r.outputTokens);
|
|
73
|
+
const bothProviderReported = control.length > 0 &&
|
|
74
|
+
treatment.length > 0 &&
|
|
75
|
+
control.every((r) => r.providerReported && typeof r.outputTokens === "number") &&
|
|
76
|
+
treatment.every((r) => r.providerReported && typeof r.outputTokens === "number");
|
|
77
|
+
const outputTokensBefore = controlOut.length > 0 ? mean(controlOut) : null;
|
|
78
|
+
const outputTokensAfter = treatmentOut.length > 0 ? mean(treatmentOut) : null;
|
|
79
|
+
const outputTokenDelta = outputTokensBefore !== null && outputTokensAfter !== null ? outputTokensBefore - outputTokensAfter : null;
|
|
80
|
+
const outputTokenReductionPct = outputTokenDelta !== null && outputTokensBefore !== null && outputTokensBefore > 0
|
|
81
|
+
? (outputTokenDelta / outputTokensBefore) * 100
|
|
82
|
+
: null;
|
|
83
|
+
const controlSource = summarizeArmSource(control);
|
|
84
|
+
const treatmentSource = summarizeArmSource(treatment);
|
|
85
|
+
const tokenSource = bothProviderReported
|
|
86
|
+
? "provider-reported"
|
|
87
|
+
: controlSource === "local-estimate" || treatmentSource === "local-estimate"
|
|
88
|
+
? "local-estimate"
|
|
89
|
+
: controlSource === "mixed" || treatmentSource === "mixed"
|
|
90
|
+
? "mixed"
|
|
91
|
+
: "unavailable";
|
|
92
|
+
const policyNames = Array.from(new Set(treatment.flatMap((r) => r.policyNames ?? []))).sort();
|
|
93
|
+
const policyFamily = treatment.some((r) => r.policyFamily) ? OUTPUT_SHAPING_POLICY_FAMILY : null;
|
|
94
|
+
const evalStatus = treatmentEvalStatus(treatment);
|
|
95
|
+
const anyTruncatedOrRefused = treatment.some((r) => r.truncated || r.refused) || control.some((r) => r.truncated || r.refused);
|
|
96
|
+
let confidence;
|
|
97
|
+
if (!bothProviderReported) {
|
|
98
|
+
confidence = "unavailable";
|
|
99
|
+
reasons.push("output savings unavailable: both arms must have provider-reported output tokens (local-estimate / missing output can never produce an output-savings number).");
|
|
100
|
+
}
|
|
101
|
+
else if (anyTruncatedOrRefused) {
|
|
102
|
+
confidence = "review_required";
|
|
103
|
+
reasons.push("review required: a run was marked truncated or refused — an incomplete answer is not a saving.");
|
|
104
|
+
}
|
|
105
|
+
else if (evalStatus !== "pass") {
|
|
106
|
+
confidence = "review_required";
|
|
107
|
+
reasons.push(`review required: treatment short-but-sufficient eval status is "${evalStatus}" (a shorter-but-lossy output is not a saving; record --eval-pass/--eval-fail per treatment run).`);
|
|
108
|
+
}
|
|
109
|
+
else if (control.length < MIN_RUNS_PER_ARM || treatment.length < MIN_RUNS_PER_ARM) {
|
|
110
|
+
confidence = "observed_not_confirmed";
|
|
111
|
+
reasons.push(`observed, not confirmed: N ≥ ${MIN_RUNS_PER_ARM} per arm is required (have control=${control.length}, treatment=${treatment.length}). A single A/B pair is an anecdote.`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
confidence = "eligible_for_engine_confirmation";
|
|
115
|
+
reasons.push("eligible for engine confirmation: provider-reported both arms, N ≥ 3 per arm, sufficiency passed, no truncation/refusal. The CONFIRMED verdict is the engine gate (measured ±2·SE criterion + reduction).");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
experimentId: experiment.experimentId,
|
|
119
|
+
nControl: control.length,
|
|
120
|
+
nTreatment: treatment.length,
|
|
121
|
+
outputTokensBefore,
|
|
122
|
+
outputTokensAfter,
|
|
123
|
+
outputTokenDelta,
|
|
124
|
+
outputTokenReductionPct,
|
|
125
|
+
tokenSource,
|
|
126
|
+
policyFamily,
|
|
127
|
+
policyNames,
|
|
128
|
+
evalStatus,
|
|
129
|
+
confidence,
|
|
130
|
+
confirmedSavingsEligible: false,
|
|
131
|
+
reasons
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/** Provider-reported output-token arrays per arm — the only honest input to the engine confirmation gate. */
|
|
135
|
+
export function providerReportedOutputArms(experiment) {
|
|
136
|
+
const control = experiment.runs.filter((r) => r.arm === "control");
|
|
137
|
+
const treatment = experiment.runs.filter((r) => r.arm === "treatment");
|
|
138
|
+
const controlOutputTokens = control.filter((r) => r.providerReported && typeof r.outputTokens === "number").map((r) => r.outputTokens);
|
|
139
|
+
const treatmentOutputTokens = treatment.filter((r) => r.providerReported && typeof r.outputTokens === "number").map((r) => r.outputTokens);
|
|
140
|
+
const bothProviderReported = control.length > 0 &&
|
|
141
|
+
treatment.length > 0 &&
|
|
142
|
+
controlOutputTokens.length === control.length &&
|
|
143
|
+
treatmentOutputTokens.length === treatment.length;
|
|
144
|
+
return { controlOutputTokens, treatmentOutputTokens, bothProviderReported };
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=output-shaping-ab.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach the output-shaping instruction block to a wrapped CLI command's prompt BEFORE generation
|
|
3
|
+
* (PUBLIC CLI/SDK code, engine-free). Increment 3 of the output-shaping policy family — wiring the
|
|
4
|
+
* deterministic policies (`output-shaping.ts`) into the live wrappers (`capture codex`/`capture cursor`).
|
|
5
|
+
*
|
|
6
|
+
* Mechanism honesty: output-token reduction requires shaping the request BEFORE generation. This prepends
|
|
7
|
+
* the instruction block to the prompt argument the wrapper is about to run. It is OPT-IN, the original
|
|
8
|
+
* command is preserved by the caller, and it surfaces NO output-savings number (a savings figure requires
|
|
9
|
+
* the private measured-A/B + short-but-sufficient eval gate — `src/engine/output-shaping-verification.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import { type BuildOutputShapingOptions, type OutputShapingAttribution } from "./output-shaping.js";
|
|
12
|
+
/**
|
|
13
|
+
* Index of the last positional (prompt) argument in a wrapped command — skipping the executable, flags,
|
|
14
|
+
* value-taking flags' values, and known subcommand keywords. Returns -1 when no prompt arg is found.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findPromptArgIndex(commandParts: string[]): number;
|
|
17
|
+
export interface AttachOutputShapingResult {
|
|
18
|
+
/** The command to run (a COPY; the original is untouched). When not attached, equals the input. */
|
|
19
|
+
commandParts: string[];
|
|
20
|
+
/** The output-shaping policies applied (content-free attribution); empty when not attached. */
|
|
21
|
+
applied: OutputShapingAttribution[];
|
|
22
|
+
attached: boolean;
|
|
23
|
+
/** Why attachment did not happen (e.g. no prompt arg, no matching policies). */
|
|
24
|
+
reason?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Prepend the output-shaping instruction block to the prompt argument of `commandParts`. Returns a COPY
|
|
28
|
+
* (never mutates the input). When no prompt arg is found, or no policies match, returns `attached: false`
|
|
29
|
+
* with the original command + a reason — never silently shapes the wrong argument.
|
|
30
|
+
*/
|
|
31
|
+
export declare function attachOutputShapingToCommand(commandParts: string[], opts?: BuildOutputShapingOptions): AttachOutputShapingResult;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attach the output-shaping instruction block to a wrapped CLI command's prompt BEFORE generation
|
|
3
|
+
* (PUBLIC CLI/SDK code, engine-free). Increment 3 of the output-shaping policy family — wiring the
|
|
4
|
+
* deterministic policies (`output-shaping.ts`) into the live wrappers (`capture codex`/`capture cursor`).
|
|
5
|
+
*
|
|
6
|
+
* Mechanism honesty: output-token reduction requires shaping the request BEFORE generation. This prepends
|
|
7
|
+
* the instruction block to the prompt argument the wrapper is about to run. It is OPT-IN, the original
|
|
8
|
+
* command is preserved by the caller, and it surfaces NO output-savings number (a savings figure requires
|
|
9
|
+
* the private measured-A/B + short-but-sufficient eval gate — `src/engine/output-shaping-verification.ts`).
|
|
10
|
+
*/
|
|
11
|
+
import { buildOutputShapingPolicy } from "./output-shaping.js";
|
|
12
|
+
// Boolean flags take no value, so the token after them is still a positional (the prompt).
|
|
13
|
+
const BOOLEAN_FLAGS = new Set(["-p", "--print", "--force", "--json", "--quiet", "-q", "--yes", "-y"]);
|
|
14
|
+
const SUBCOMMAND_KEYWORDS = new Set(["cursor", "cursor-agent", "codex", "agent", "chat", "exec", "run"]);
|
|
15
|
+
/**
|
|
16
|
+
* Index of the last positional (prompt) argument in a wrapped command — skipping the executable, flags,
|
|
17
|
+
* value-taking flags' values, and known subcommand keywords. Returns -1 when no prompt arg is found.
|
|
18
|
+
*/
|
|
19
|
+
export function findPromptArgIndex(commandParts) {
|
|
20
|
+
let last = -1;
|
|
21
|
+
let skipNext = false;
|
|
22
|
+
for (let i = 1; i < commandParts.length; i++) {
|
|
23
|
+
const part = commandParts[i];
|
|
24
|
+
if (skipNext) {
|
|
25
|
+
skipNext = false; // value of a preceding value-taking flag (e.g. `--output-format json`)
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (part.startsWith("-")) {
|
|
29
|
+
if (!BOOLEAN_FLAGS.has(part) && !part.includes("="))
|
|
30
|
+
skipNext = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (SUBCOMMAND_KEYWORDS.has(part))
|
|
34
|
+
continue;
|
|
35
|
+
last = i;
|
|
36
|
+
}
|
|
37
|
+
return last;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Prepend the output-shaping instruction block to the prompt argument of `commandParts`. Returns a COPY
|
|
41
|
+
* (never mutates the input). When no prompt arg is found, or no policies match, returns `attached: false`
|
|
42
|
+
* with the original command + a reason — never silently shapes the wrong argument.
|
|
43
|
+
*/
|
|
44
|
+
export function attachOutputShapingToCommand(commandParts, opts = {}) {
|
|
45
|
+
const { instructions, applied } = buildOutputShapingPolicy(opts);
|
|
46
|
+
if (instructions === "" || applied.length === 0) {
|
|
47
|
+
return { commandParts: [...commandParts], applied: [], attached: false, reason: "no matching output-shaping policies" };
|
|
48
|
+
}
|
|
49
|
+
const idx = findPromptArgIndex(commandParts);
|
|
50
|
+
if (idx === -1) {
|
|
51
|
+
return { commandParts: [...commandParts], applied: [], attached: false, reason: "no prompt argument found to attach the output-shaping policy to" };
|
|
52
|
+
}
|
|
53
|
+
const next = [...commandParts];
|
|
54
|
+
next[idx] = `${instructions}\n\n${next[idx]}`;
|
|
55
|
+
return { commandParts: next, applied, attached: true };
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=output-shaping-attach.js.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output-shaping policy family — deterministic, rule-based (PUBLIC CLI/SDK code, engine-free).
|
|
3
|
+
*
|
|
4
|
+
* Design: `docs/design/output-shaping-policy-family.md` (accepted; implementation authorized 2026-06-29).
|
|
5
|
+
* This is **increment 1**: the public/free deterministic policies that produce a CONTENT-FREE
|
|
6
|
+
* output-shaping **instruction block** to attach to a request **BEFORE generation** (the only mechanism
|
|
7
|
+
* that can reduce provider output tokens — post-generation processing does NOT).
|
|
8
|
+
*
|
|
9
|
+
* HONESTY (binding):
|
|
10
|
+
* - This module produces INSTRUCTIONS + attribution labels only. It computes **NO output-savings number**
|
|
11
|
+
* and makes **NO claim** that output was reduced. An output-savings number requires (separately, in the
|
|
12
|
+
* private/proprietary engine) a **measured A/B** (provider-reported output; N≥3/arm, ±2·SE excludes
|
|
13
|
+
* zero) **AND** the **eval-gated short-but-sufficient** check. Until both pass, savings are unavailable.
|
|
14
|
+
* - The short-but-sufficient **eval** (policy #6 in the design) is the **private eval-gated verification**
|
|
15
|
+
* — it is NOT in this public module; this module only emits the shaping instructions + their attribution.
|
|
16
|
+
* - Content-free: instructions are generic shaping text; no prompt/completion/trace content.
|
|
17
|
+
*/
|
|
18
|
+
export type RiskLevel = "low" | "medium" | "high";
|
|
19
|
+
/** Content-free attribution for a recorded run (matches the control-plane policy_* columns). */
|
|
20
|
+
export interface OutputShapingAttribution {
|
|
21
|
+
policy_name: string;
|
|
22
|
+
policy_family: "output_shaping";
|
|
23
|
+
risk_level: RiskLevel;
|
|
24
|
+
}
|
|
25
|
+
export interface OutputShapingPolicy extends OutputShapingAttribution {
|
|
26
|
+
/** Human-readable description (what the policy instructs — never content). */
|
|
27
|
+
description: string;
|
|
28
|
+
/** Whether the policy is part of the default rule-based set. */
|
|
29
|
+
defaultOn: boolean;
|
|
30
|
+
/** Builds the instruction line(s). `verbosityBudgetTokens` is used only by the verbosity-budget policy. */
|
|
31
|
+
instruction(verbosityBudgetTokens?: number): string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The deterministic rule-based output-shaping policies (the public/free tier). The learned tier and the
|
|
35
|
+
* eval-gated verification are private/proprietary (Track A) — not here.
|
|
36
|
+
*/
|
|
37
|
+
export declare const OUTPUT_SHAPING_POLICIES: readonly OutputShapingPolicy[];
|
|
38
|
+
/** Note attached to every emitted block: this is a request-shaping instruction, not a savings claim. */
|
|
39
|
+
export declare const OUTPUT_SHAPING_HONESTY_NOTE: string;
|
|
40
|
+
export interface OutputShapingResult {
|
|
41
|
+
/** The content-free instruction block to prepend to the request/system prompt BEFORE generation. */
|
|
42
|
+
instructions: string;
|
|
43
|
+
/** Attribution for the applied policies (content-free; for recording / reporting). */
|
|
44
|
+
applied: OutputShapingAttribution[];
|
|
45
|
+
}
|
|
46
|
+
export interface BuildOutputShapingOptions {
|
|
47
|
+
/** Policy names to apply; defaults to the `defaultOn` set. Unknown names are ignored (never invented). */
|
|
48
|
+
policies?: string[];
|
|
49
|
+
/** Soft verbosity budget in output tokens (used by the verbosity-budget policy). */
|
|
50
|
+
verbosityBudgetTokens?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Build the content-free output-shaping instruction block + attribution from the deterministic family.
|
|
54
|
+
* Produces instructions only — NO savings number, NO claim of reduction.
|
|
55
|
+
*/
|
|
56
|
+
export declare function buildOutputShapingPolicy(opts?: BuildOutputShapingOptions): OutputShapingResult;
|