@compaction/cli 0.1.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +51 -12
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +24 -11
  10. package/dist/cli/commands/context.d.ts +2 -0
  11. package/dist/cli/commands/context.js +130 -0
  12. package/dist/cli/commands/dev.d.ts +17 -0
  13. package/dist/cli/commands/dev.js +131 -0
  14. package/dist/cli/commands/gateway.d.ts +31 -0
  15. package/dist/cli/commands/gateway.js +312 -0
  16. package/dist/cli/commands/hooks.d.ts +5 -0
  17. package/dist/cli/commands/hooks.js +181 -0
  18. package/dist/cli/commands/import.js +3 -1
  19. package/dist/cli/commands/init.js +422 -17
  20. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  21. package/dist/cli/commands/input-compaction-ab.js +125 -0
  22. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  23. package/dist/cli/commands/optimize-hosted.js +123 -0
  24. package/dist/cli/commands/optimize.js +8 -0
  25. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  26. package/dist/cli/commands/output-shaping-ab.js +132 -0
  27. package/dist/cli/commands/output-shaping.d.ts +7 -0
  28. package/dist/cli/commands/output-shaping.js +43 -0
  29. package/dist/cli/commands/policies.d.ts +2 -0
  30. package/dist/cli/commands/policies.js +80 -0
  31. package/dist/cli/commands/precall.d.ts +19 -0
  32. package/dist/cli/commands/precall.js +436 -0
  33. package/dist/cli/commands/recommend.js +1 -1
  34. package/dist/cli/commands/run.js +310 -18
  35. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  36. package/dist/cli/commands/upgrade-status.js +152 -0
  37. package/dist/cli/cursor-export-read.d.ts +13 -0
  38. package/dist/cli/cursor-export-read.js +53 -0
  39. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  40. package/dist/cli/cursor-live-preflight.js +46 -0
  41. package/dist/cli/index.js +39 -2
  42. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  43. package/dist/cli/onboarding/GatewayTui.js +113 -0
  44. package/dist/cli/onboarding/model.d.ts +32 -1
  45. package/dist/cli/onboarding/model.js +27 -2
  46. package/dist/cli/terminal-logo.d.ts +27 -0
  47. package/dist/cli/terminal-logo.js +49 -0
  48. package/dist/core/activity-event.d.ts +107 -0
  49. package/dist/core/activity-event.js +182 -0
  50. package/dist/core/activity-store.d.ts +63 -0
  51. package/dist/core/activity-store.js +254 -0
  52. package/dist/core/activity-view.d.ts +76 -0
  53. package/dist/core/activity-view.js +120 -0
  54. package/dist/core/api-client/index.d.ts +3 -1
  55. package/dist/core/api-client/index.js +4 -0
  56. package/dist/core/api-client/payload.d.ts +5 -1
  57. package/dist/core/api-client/payload.js +8 -6
  58. package/dist/core/api-client/persisted-config.d.ts +57 -0
  59. package/dist/core/api-client/persisted-config.js +156 -0
  60. package/dist/core/api-client/tool.d.ts +29 -0
  61. package/dist/core/api-client/tool.js +50 -0
  62. package/dist/core/api-client/types.d.ts +15 -4
  63. package/dist/core/auto-apply-ask.d.ts +58 -0
  64. package/dist/core/auto-apply-ask.js +105 -0
  65. package/dist/core/auto-apply-gates.d.ts +76 -0
  66. package/dist/core/auto-apply-gates.js +113 -0
  67. package/dist/core/before-call-activity.d.ts +35 -0
  68. package/dist/core/before-call-activity.js +103 -0
  69. package/dist/core/before-call-recovery.d.ts +19 -0
  70. package/dist/core/before-call-recovery.js +46 -0
  71. package/dist/core/before-call-stdin.d.ts +59 -0
  72. package/dist/core/before-call-stdin.js +78 -0
  73. package/dist/core/before-call.d.ts +149 -0
  74. package/dist/core/before-call.js +358 -0
  75. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  76. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  77. package/dist/core/capture-record.d.ts +40 -0
  78. package/dist/core/capture-record.js +55 -0
  79. package/dist/core/claude-code-before-call.d.ts +37 -0
  80. package/dist/core/claude-code-before-call.js +120 -0
  81. package/dist/core/claude-code-connect.d.ts +31 -0
  82. package/dist/core/claude-code-connect.js +87 -0
  83. package/dist/core/claude-code-hook-record.d.ts +71 -0
  84. package/dist/core/claude-code-hook-record.js +0 -0
  85. package/dist/core/claude-code-hooks.d.ts +77 -0
  86. package/dist/core/claude-code-hooks.js +141 -0
  87. package/dist/core/codex-capture.d.ts +45 -0
  88. package/dist/core/codex-capture.js +204 -0
  89. package/dist/core/command-runner.js +5 -1
  90. package/dist/core/compaction-artifacts.d.ts +51 -0
  91. package/dist/core/compaction-artifacts.js +242 -0
  92. package/dist/core/compactor.d.ts +9 -0
  93. package/dist/core/compactor.js +95 -0
  94. package/dist/core/context-store-eval-cases.d.ts +6 -0
  95. package/dist/core/context-store-eval-cases.js +331 -0
  96. package/dist/core/context-store-eval.d.ts +140 -0
  97. package/dist/core/context-store-eval.js +138 -0
  98. package/dist/core/context-store-fs.d.ts +73 -0
  99. package/dist/core/context-store-fs.js +157 -0
  100. package/dist/core/context-store-sufficiency.d.ts +98 -0
  101. package/dist/core/context-store-sufficiency.js +135 -0
  102. package/dist/core/context-store.d.ts +155 -0
  103. package/dist/core/context-store.js +228 -0
  104. package/dist/core/cross-surface-event.d.ts +306 -0
  105. package/dist/core/cross-surface-event.js +330 -0
  106. package/dist/core/cursor-capture.d.ts +54 -0
  107. package/dist/core/cursor-capture.js +215 -0
  108. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  109. package/dist/core/cursor-preflight-probe.js +88 -0
  110. package/dist/core/cursor-preflight.d.ts +86 -0
  111. package/dist/core/cursor-preflight.js +126 -0
  112. package/dist/core/gateway/apply-activation.d.ts +39 -0
  113. package/dist/core/gateway/apply-activation.js +84 -0
  114. package/dist/core/gateway/apply-policy.d.ts +64 -0
  115. package/dist/core/gateway/apply-policy.js +221 -0
  116. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  117. package/dist/core/gateway/apply-receipt.js +75 -0
  118. package/dist/core/gateway/cache-proof.d.ts +45 -0
  119. package/dist/core/gateway/cache-proof.js +65 -0
  120. package/dist/core/gateway/configure.d.ts +50 -0
  121. package/dist/core/gateway/configure.js +169 -0
  122. package/dist/core/gateway/openai-usage.d.ts +56 -0
  123. package/dist/core/gateway/openai-usage.js +128 -0
  124. package/dist/core/gateway/receipt.d.ts +138 -0
  125. package/dist/core/gateway/receipt.js +120 -0
  126. package/dist/core/gateway/recovery.d.ts +23 -0
  127. package/dist/core/gateway/recovery.js +68 -0
  128. package/dist/core/gateway/server.d.ts +51 -0
  129. package/dist/core/gateway/server.js +276 -0
  130. package/dist/core/gateway/status.d.ts +45 -0
  131. package/dist/core/gateway/status.js +109 -0
  132. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  133. package/dist/core/hook-usage-aggregate.js +161 -0
  134. package/dist/core/input-compaction-ab.d.ts +111 -0
  135. package/dist/core/input-compaction-ab.js +158 -0
  136. package/dist/core/local-run-record.d.ts +109 -0
  137. package/dist/core/local-run-record.js +223 -0
  138. package/dist/core/output-shaping-ab.d.ts +140 -0
  139. package/dist/core/output-shaping-ab.js +146 -0
  140. package/dist/core/output-shaping-attach.d.ts +31 -0
  141. package/dist/core/output-shaping-attach.js +57 -0
  142. package/dist/core/output-shaping.d.ts +56 -0
  143. package/dist/core/output-shaping.js +89 -0
  144. package/dist/core/policy-middleware.d.ts +121 -0
  145. package/dist/core/policy-middleware.js +919 -0
  146. package/dist/core/policy-preferences.d.ts +99 -0
  147. package/dist/core/policy-preferences.js +232 -0
  148. package/dist/core/report-generator.d.ts +19 -1
  149. package/dist/core/report-generator.js +51 -0
  150. package/dist/core/run-aggregator.d.ts +56 -1
  151. package/dist/core/run-aggregator.js +93 -2
  152. package/dist/core/run-flow-report.d.ts +82 -0
  153. package/dist/core/run-flow-report.js +71 -0
  154. package/dist/core/safety-report.js +8 -1
  155. package/dist/core/shim-capture-bridge.d.ts +32 -0
  156. package/dist/core/shim-capture-bridge.js +88 -0
  157. package/dist/core/skill-injection-policy.d.ts +72 -0
  158. package/dist/core/skill-injection-policy.js +183 -0
  159. package/dist/core/spend-attribution.js +1 -1
  160. package/dist/core/token-accounting.d.ts +1 -1
  161. package/dist/core/tool-shim.d.ts +129 -0
  162. package/dist/core/tool-shim.js +447 -0
  163. package/dist/core/trace-adapters.d.ts +8 -0
  164. package/dist/core/trace-adapters.js +4 -0
  165. package/dist/core/trace-parser.d.ts +13 -13
  166. package/dist/core/trace-parser.js +6 -6
  167. package/dist/core/types.d.ts +45 -2
  168. package/dist/core/waste-detector.d.ts +20 -0
  169. package/dist/core/waste-detector.js +160 -6
  170. package/package.json +1 -1
@@ -0,0 +1,99 @@
1
+ /** Default local preference file (under the gitignored `.compaction/`). */
2
+ export declare const DEFAULT_POLICY_PREFERENCES_DIRECTORY = ".compaction";
3
+ export declare const POLICY_PREFERENCES_FILENAME = "policy-preferences.json";
4
+ /** The binary preference (mirror of `AUTO_APPLY_PREFERENCES` in activity-event.ts). Default = ask. */
5
+ export declare const POLICY_PREFERENCE_VALUES: readonly ["ask-each-time", "auto-when-gates-pass"];
6
+ export type PolicyPreferenceValue = (typeof POLICY_PREFERENCE_VALUES)[number];
7
+ /** Tool values that are NEVER an allowed scope — a preference is never global or cross-tool. */
8
+ export declare const REJECTED_GLOBAL_TOOL_VALUES: readonly string[];
9
+ /**
10
+ * The safety gates a preference's auto-apply WOULD require (founder decision: "explicit opt-in +
11
+ * matching scope + original retained + capsule provenance + recoverability pass + threshold + risk
12
+ * checks + evidence label emitted + rollback path"). POPULATED for provenance/explanation; NOT
13
+ * EVALUATED this cycle — no gate engine exists, so no preference can drive an application.
14
+ */
15
+ export declare const DEFAULT_GATES_REQUIRED: readonly string[];
16
+ /** The inferred safest scope of a preference: one tool/workflow, optionally one repo, one policy type. */
17
+ export interface PolicyPreferenceScope {
18
+ /** REQUIRED. The workflow/tool this preference is scoped to (never "global"/"all"/cross-tool). */
19
+ tool: string;
20
+ /** OPTIONAL. The repo the preference is scoped to, when detectable (content-free identifier). */
21
+ repo?: string;
22
+ /** REQUIRED. Which policy type this preference concerns (content-free identifier). */
23
+ policy_type: string;
24
+ }
25
+ /** One stored preference record. Content-free; no wall-clock; id is deterministic over the scope. */
26
+ export interface PolicyPreference {
27
+ /** Deterministic content-free id: `pref-` + 24 hex of sha-256 over the canonical scope. */
28
+ id: string;
29
+ scope: PolicyPreferenceScope;
30
+ preference: PolicyPreferenceValue;
31
+ /** Whether this preference is live. `disable` sets this false; there is NO enable in the CLI. */
32
+ enabled: boolean;
33
+ /** Safety gates the (future) gated auto-apply slice would require. POPULATED, NOT EVALUATED here. */
34
+ gates_required: string[];
35
+ }
36
+ /** Input to save a preference — id, enabled default, and gates_required are derived when absent. */
37
+ export interface SavePolicyPreferenceInput {
38
+ scope: PolicyPreferenceScope;
39
+ preference: PolicyPreferenceValue;
40
+ /** Defaults to true (the record is stored so the future live ask can honor it); disable turns it off. */
41
+ enabled?: boolean;
42
+ /** Defaults to `DEFAULT_GATES_REQUIRED`; a caller may pass a narrower list (never evaluated here). */
43
+ gates_required?: string[];
44
+ }
45
+ export interface PolicyPreferenceValidation {
46
+ problems: string[];
47
+ }
48
+ /** The longest a content-free scope/gate identifier may be — anything longer looks like content. */
49
+ export declare const POLICY_PREFERENCE_MAX_STRING_LENGTH = 200;
50
+ /**
51
+ * Validate a scope: `tool` and `policy_type` REQUIRED and content-free; `repo` optional; the tool
52
+ * is NEVER a global/all/cross-tool value (case-insensitive). Report-only (never throws).
53
+ */
54
+ export declare function validatePolicyPreferenceScope(scope: unknown): PolicyPreferenceValidation;
55
+ /** Deterministic content-free id over the scope only (so a scope has ONE preference record). */
56
+ export declare function computePolicyPreferenceId(scope: PolicyPreferenceScope): string;
57
+ export declare const POLICY_PREFERENCE_ID_PATTERN: RegExp;
58
+ /** Read all stored preferences. A missing file means "none saved yet" (empty, not an error). */
59
+ export declare function readPolicyPreferences(directory?: string): Promise<{
60
+ preferences: PolicyPreference[];
61
+ }>;
62
+ export type SavePolicyPreferenceResult = {
63
+ saved: true;
64
+ preference: PolicyPreference;
65
+ path: string;
66
+ replacedExisting: boolean;
67
+ } | {
68
+ saved: false;
69
+ problems: string[];
70
+ };
71
+ /**
72
+ * Save (upsert by deterministic scope id) ONE preference to the local store. Fail-closed: an
73
+ * invalid scope saves NOTHING. THIS DOES NOT APPLY ANYTHING — it writes one JSON file and returns;
74
+ * there is no application path here (proven by the fail-closed test).
75
+ */
76
+ export declare function savePolicyPreference(input: SavePolicyPreferenceInput, directory?: string): Promise<SavePolicyPreferenceResult>;
77
+ export type DisablePolicyPreferenceResult = {
78
+ disabled: true;
79
+ preference: PolicyPreference;
80
+ path: string;
81
+ alreadyDisabled: boolean;
82
+ } | {
83
+ disabled: false;
84
+ reason: string;
85
+ };
86
+ /**
87
+ * Disable a preference by id (set enabled=false). The safe management verb — disabling can only
88
+ * ever REDUCE what could apply, so it is always safe. Unknown id → a clear error, nothing changed.
89
+ * Idempotent: disabling an already-disabled preference is a reported no-op (no throw).
90
+ */
91
+ export declare function disablePolicyPreference(id: string, directory?: string): Promise<DisablePolicyPreferenceResult>;
92
+ /** Look up one preference by id (read-only). */
93
+ export declare function findPolicyPreference(id: string, directory?: string): Promise<PolicyPreference | undefined>;
94
+ /**
95
+ * Plain-language explanation of what a preference MEANS — honest that auto-apply is NOT YET ACTIVE
96
+ * (the preference is stored; application is pending the gated auto-apply slice, which does not
97
+ * exist yet). No wall-clock, no content — scope, policy, gates, recovery, and how to disable.
98
+ */
99
+ export declare function explainPolicyPreference(preference: PolicyPreference): string;
@@ -0,0 +1,232 @@
1
+ /**
2
+ * LOCAL auto-apply PREFERENCE store (charter 2026-07-04-run-7, lane 4: "compaction policies
3
+ * list/disable/explain" + "auto-apply preference model: binary ask-after-approve, inferred safest
4
+ * scope, fail-closed gates scaffold, tests proving OFF by default").
5
+ *
6
+ * WHAT THIS IS — and, more importantly, WHAT IT IS NOT:
7
+ * This module STORES and READS a user's binary auto-apply *preference* (the one the founder's live
8
+ * post-approval ask would save: "Apply this automatically next time for this workflow when safety
9
+ * gates pass? [yes/no, default no]"). It is a PREFERENCE record only. It is NOT an application
10
+ * engine:
11
+ *
12
+ * FAIL-CLOSED INVARIANT: a preference EXISTING (even enabled=true, even
13
+ * preference="auto-when-gates-pass") does NOT cause anything to be applied anywhere. There is no
14
+ * apply path in this module — it imports nothing that mutates a user session, trace, run record,
15
+ * or context, and it exports no apply function. `gates_required` is POPULATED but NEVER EVALUATED
16
+ * this cycle (no gate engine exists). Proven by `tests/core/policy-preferences.test.ts`
17
+ * (the fail-closed proof: saving/enabling a preference mutates ONLY this store's JSON file and
18
+ * invokes no application).
19
+ *
20
+ * SCOPE IS NEVER GLOBAL AND NEVER CROSS-TOOL (founder decision): `tool` is REQUIRED, and a
21
+ * "global"/"all"/"*" tool value is REJECTED at validation. A preference is always scoped to one
22
+ * workflow/tool (+ repo where detectable + policy type).
23
+ *
24
+ * NO WALL-CLOCK: the record carries no `created_at` — the id is DETERMINISTIC over the scope, so
25
+ * the same scope always maps to the same preference record (an upsert key, content-free).
26
+ *
27
+ * LOCAL FILE I/O ONLY: one JSON file under `.compaction/` (already gitignored). No network, no new
28
+ * dependencies.
29
+ */
30
+ import { createHash } from "node:crypto";
31
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
32
+ import { dirname, join } from "node:path";
33
+ /** Default local preference file (under the gitignored `.compaction/`). */
34
+ export const DEFAULT_POLICY_PREFERENCES_DIRECTORY = ".compaction";
35
+ export const POLICY_PREFERENCES_FILENAME = "policy-preferences.json";
36
+ /** The binary preference (mirror of `AUTO_APPLY_PREFERENCES` in activity-event.ts). Default = ask. */
37
+ export const POLICY_PREFERENCE_VALUES = ["ask-each-time", "auto-when-gates-pass"];
38
+ /** Tool values that are NEVER an allowed scope — a preference is never global or cross-tool. */
39
+ export const REJECTED_GLOBAL_TOOL_VALUES = ["global", "all", "*", "any", "cross-tool"];
40
+ /**
41
+ * The safety gates a preference's auto-apply WOULD require (founder decision: "explicit opt-in +
42
+ * matching scope + original retained + capsule provenance + recoverability pass + threshold + risk
43
+ * checks + evidence label emitted + rollback path"). POPULATED for provenance/explanation; NOT
44
+ * EVALUATED this cycle — no gate engine exists, so no preference can drive an application.
45
+ */
46
+ export const DEFAULT_GATES_REQUIRED = [
47
+ "scope-match",
48
+ "original-retained",
49
+ "capsule-provenance",
50
+ "recoverability-pass",
51
+ "reduction-threshold",
52
+ "risk-checks",
53
+ "evidence-label-emitted",
54
+ "rollback-path"
55
+ ];
56
+ function isNonEmptyString(value) {
57
+ return typeof value === "string" && value.trim() !== "";
58
+ }
59
+ /** The longest a content-free scope/gate identifier may be — anything longer looks like content. */
60
+ export const POLICY_PREFERENCE_MAX_STRING_LENGTH = 200;
61
+ /**
62
+ * Validate a scope: `tool` and `policy_type` REQUIRED and content-free; `repo` optional; the tool
63
+ * is NEVER a global/all/cross-tool value (case-insensitive). Report-only (never throws).
64
+ */
65
+ export function validatePolicyPreferenceScope(scope) {
66
+ const problems = [];
67
+ if (typeof scope !== "object" || scope === null || Array.isArray(scope)) {
68
+ return { problems: ["scope: must be an object ({ tool, repo?, policy_type })"] };
69
+ }
70
+ const record = scope;
71
+ if (!isNonEmptyString(record.tool)) {
72
+ problems.push("scope.tool: required — a preference is always scoped to one workflow/tool (never global)");
73
+ }
74
+ else if (REJECTED_GLOBAL_TOOL_VALUES.includes(record.tool.trim().toLowerCase())) {
75
+ problems.push(`scope.tool: "${record.tool}" is rejected — a preference is NEVER global and NEVER cross-tool (founder rule); scope it to one tool`);
76
+ }
77
+ if (!isNonEmptyString(record.policy_type)) {
78
+ problems.push("scope.policy_type: required — which policy this preference concerns (content-free identifier)");
79
+ }
80
+ if (record.repo !== undefined && !isNonEmptyString(record.repo)) {
81
+ problems.push("scope.repo: when present, must be a non-empty content-free repo identifier");
82
+ }
83
+ for (const [key, value] of Object.entries(record)) {
84
+ if (!["tool", "repo", "policy_type"].includes(key)) {
85
+ problems.push(`scope.${key}: not an allowed scope field (only tool, repo, policy_type)`);
86
+ continue;
87
+ }
88
+ if (typeof value === "string" && value.length > POLICY_PREFERENCE_MAX_STRING_LENGTH) {
89
+ problems.push(`scope.${key}: string exceeds the content-free bound (${value.length} chars) — that is content-sized`);
90
+ }
91
+ }
92
+ return { problems };
93
+ }
94
+ /** Deterministic content-free id over the scope only (so a scope has ONE preference record). */
95
+ export function computePolicyPreferenceId(scope) {
96
+ const canonical = JSON.stringify({
97
+ policy_type: scope.policy_type,
98
+ repo: scope.repo ?? null,
99
+ tool: scope.tool
100
+ });
101
+ const digest = createHash("sha256").update(canonical).digest("hex");
102
+ return `pref-${digest.slice(0, 24)}`;
103
+ }
104
+ export const POLICY_PREFERENCE_ID_PATTERN = /^pref-[0-9a-f]{24}$/;
105
+ function preferencesPath(directory) {
106
+ return join(directory, POLICY_PREFERENCES_FILENAME);
107
+ }
108
+ /** Read all stored preferences. A missing file means "none saved yet" (empty, not an error). */
109
+ export async function readPolicyPreferences(directory = DEFAULT_POLICY_PREFERENCES_DIRECTORY) {
110
+ let raw;
111
+ try {
112
+ raw = await readFile(preferencesPath(directory), "utf8");
113
+ }
114
+ catch (error) {
115
+ const code = typeof error === "object" && error !== null && "code" in error ? String(error.code) : "unknown";
116
+ if (code === "ENOENT")
117
+ return { preferences: [] };
118
+ throw error;
119
+ }
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(raw);
123
+ }
124
+ catch {
125
+ throw new Error(`${preferencesPath(directory)} is not valid JSON — refusing to guess (fail-closed)`);
126
+ }
127
+ const list = Array.isArray(parsed)
128
+ ? parsed
129
+ : typeof parsed === "object" && parsed !== null && Array.isArray(parsed.preferences)
130
+ ? parsed.preferences
131
+ : [];
132
+ const preferences = list.filter((entry) => {
133
+ if (typeof entry !== "object" || entry === null)
134
+ return false;
135
+ const candidate = entry;
136
+ return (typeof candidate.id === "string" &&
137
+ typeof candidate.enabled === "boolean" &&
138
+ POLICY_PREFERENCE_VALUES.includes(candidate.preference) &&
139
+ validatePolicyPreferenceScope(candidate.scope).problems.length === 0);
140
+ });
141
+ return { preferences };
142
+ }
143
+ /**
144
+ * Save (upsert by deterministic scope id) ONE preference to the local store. Fail-closed: an
145
+ * invalid scope saves NOTHING. THIS DOES NOT APPLY ANYTHING — it writes one JSON file and returns;
146
+ * there is no application path here (proven by the fail-closed test).
147
+ */
148
+ export async function savePolicyPreference(input, directory = DEFAULT_POLICY_PREFERENCES_DIRECTORY) {
149
+ const problems = validatePolicyPreferenceScope(input.scope).problems;
150
+ if (!POLICY_PREFERENCE_VALUES.includes(input.preference)) {
151
+ problems.push(`preference: must be one of ${POLICY_PREFERENCE_VALUES.join(" | ")} (exact)`);
152
+ }
153
+ if (problems.length > 0)
154
+ return { saved: false, problems };
155
+ const scope = {
156
+ tool: input.scope.tool,
157
+ policy_type: input.scope.policy_type,
158
+ ...(input.scope.repo !== undefined ? { repo: input.scope.repo } : {})
159
+ };
160
+ const record = {
161
+ id: computePolicyPreferenceId(scope),
162
+ scope,
163
+ preference: input.preference,
164
+ enabled: input.enabled ?? true,
165
+ gates_required: input.gates_required ? [...input.gates_required] : [...DEFAULT_GATES_REQUIRED]
166
+ };
167
+ const { preferences } = await readPolicyPreferences(directory);
168
+ const existingIndex = preferences.findIndex((entry) => entry.id === record.id);
169
+ const replacedExisting = existingIndex >= 0;
170
+ const next = replacedExisting
171
+ ? preferences.map((entry, index) => (index === existingIndex ? record : entry))
172
+ : [...preferences, record];
173
+ const path = preferencesPath(directory);
174
+ await mkdir(dirname(path), { recursive: true });
175
+ await writeFile(path, `${JSON.stringify({ preferences: next }, null, 2)}\n`, "utf8");
176
+ return { saved: true, preference: record, path, replacedExisting };
177
+ }
178
+ /**
179
+ * Disable a preference by id (set enabled=false). The safe management verb — disabling can only
180
+ * ever REDUCE what could apply, so it is always safe. Unknown id → a clear error, nothing changed.
181
+ * Idempotent: disabling an already-disabled preference is a reported no-op (no throw).
182
+ */
183
+ export async function disablePolicyPreference(id, directory = DEFAULT_POLICY_PREFERENCES_DIRECTORY) {
184
+ const { preferences } = await readPolicyPreferences(directory);
185
+ const index = preferences.findIndex((entry) => entry.id === id);
186
+ if (index < 0) {
187
+ return { disabled: false, reason: `no preference with id "${id}" — nothing changed (run "compaction policies list")` };
188
+ }
189
+ const target = preferences[index];
190
+ if (target.enabled === false) {
191
+ return { disabled: true, preference: target, path: preferencesPath(directory), alreadyDisabled: true };
192
+ }
193
+ const updated = { ...target, enabled: false };
194
+ const next = preferences.map((entry, i) => (i === index ? updated : entry));
195
+ const path = preferencesPath(directory);
196
+ await mkdir(dirname(path), { recursive: true });
197
+ await writeFile(path, `${JSON.stringify({ preferences: next }, null, 2)}\n`, "utf8");
198
+ return { disabled: true, preference: updated, path, alreadyDisabled: false };
199
+ }
200
+ /** Look up one preference by id (read-only). */
201
+ export async function findPolicyPreference(id, directory = DEFAULT_POLICY_PREFERENCES_DIRECTORY) {
202
+ const { preferences } = await readPolicyPreferences(directory);
203
+ return preferences.find((entry) => entry.id === id);
204
+ }
205
+ /**
206
+ * Plain-language explanation of what a preference MEANS — honest that auto-apply is NOT YET ACTIVE
207
+ * (the preference is stored; application is pending the gated auto-apply slice, which does not
208
+ * exist yet). No wall-clock, no content — scope, policy, gates, recovery, and how to disable.
209
+ */
210
+ export function explainPolicyPreference(preference) {
211
+ const scopeLine = preference.scope.repo
212
+ ? `the "${preference.scope.tool}" workflow in repo "${preference.scope.repo}"`
213
+ : `the "${preference.scope.tool}" workflow`;
214
+ const lines = [
215
+ `Preference ${preference.id}`,
216
+ ``,
217
+ `Scope: ${scopeLine}`,
218
+ `Policy: ${preference.scope.policy_type}`,
219
+ `Preference: ${preference.preference}`,
220
+ `Enabled: ${preference.enabled ? "yes" : "no (disabled)"}`,
221
+ ``
222
+ ];
223
+ if (preference.preference === "auto-when-gates-pass") {
224
+ lines.push(`What it would mean: for ${scopeLine}, you chose "apply automatically next time WHEN ALL`, `safety gates pass". Auto-apply would happen ONLY if every one of these gates passes:`, ...preference.gates_required.map((gate) => ` - ${gate}`), ``, `NOT YET ACTIVE: no auto-apply engine exists yet. This preference is STORED only; nothing is`, `applied automatically today. When the gated auto-apply slice ships, an application will run`, `ONLY inside these gates, the original will be retained, and every application will be`, `recoverable and logged with an evidence label.`);
225
+ }
226
+ else {
227
+ lines.push(`What it means: for ${scopeLine}, Compaction ASKS every time before applying anything`, `(the default). Nothing is ever applied automatically under this preference.`);
228
+ }
229
+ lines.push(``, `Originals are always retained; every application (once the gated slice exists) is reversible.`, `To turn this preference off: compaction policies disable ${preference.id}`);
230
+ return lines.join("\n");
231
+ }
232
+ //# sourceMappingURL=policy-preferences.js.map
@@ -1,5 +1,5 @@
1
1
  import type { SkillInjectionAdvisory } from "./skill-injection-detector.js";
2
- import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, TokenEstimate, WasteFinding } from "./types.js";
2
+ import type { AgentTrace, CompactionPolicy, CompactionReport, CostEstimate, SavingsEvidence, TokenEstimate, WasteFinding } from "./types.js";
3
3
  import type { UsageMetadata } from "./usage-metadata.js";
4
4
  /**
5
5
  * Attach the content-addressed per-run trace fingerprint to a compaction report (PURE,
@@ -14,6 +14,24 @@ import type { UsageMetadata } from "./usage-metadata.js";
14
14
  * attribution/integrity signal, NOT a billing, provider, or semantic-preservation claim.
15
15
  */
16
16
  export declare function withTraceFingerprint(report: CompactionReport, trace: AgentTrace): CompactionReport;
17
+ /**
18
+ * Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
19
+ * additive, backward-compatible). V0.2 "evidence labels per result": it composes the
20
+ * report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
21
+ * honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
22
+ * `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
23
+ * the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
24
+ * the aggregate increment across N≥3 distinct runs).
25
+ *
26
+ * `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
27
+ * source ONLY when the run genuinely carries it. `recoverability` defaults to
28
+ * `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
29
+ * when one ran. Returns a new object; the input report is not mutated.
30
+ */
31
+ export declare function withSavingsEvidence(report: CompactionReport, opts?: {
32
+ costSource?: SavingsEvidence["cost_source"];
33
+ recoverability?: SavingsEvidence["recoverability"];
34
+ }): CompactionReport;
17
35
  export declare function formatAnalyzeReport(trace: AgentTrace, tokens: TokenEstimate, cost: CostEstimate, findings?: WasteFinding[], usage?: UsageMetadata, skillInjectionAdvisory?: SkillInjectionAdvisory): string;
18
36
  export declare function formatCompactionReport(report: CompactionReport): string;
19
37
  export declare function formatCompactionMarkdownReport(report: CompactionReport, policy: CompactionPolicy): string;
@@ -16,6 +16,56 @@ import { computeTraceFingerprint } from "./trace-fingerprint.js";
16
16
  export function withTraceFingerprint(report, trace) {
17
17
  return { ...report, trace_fingerprint: computeTraceFingerprint(trace) };
18
18
  }
19
+ /**
20
+ * Attach the composed per-run SAVINGS EVIDENCE record to a compaction report (PURE,
21
+ * additive, backward-compatible). V0.2 "evidence labels per result": it composes the
22
+ * report's EXISTING numbers into ONE explicitly-labeled record and asserts the weakest
23
+ * honest rung — a single run is rung 1 (local estimate). It makes NO new/stronger claim:
24
+ * `billing_confirmed` is always false, `semantic_preservation` always `not_evaluated`, and
25
+ * the measured rung 1.5 (`measured_caveated_estimate_delta`) is NEVER produced here (only by
26
+ * the aggregate increment across N≥3 distinct runs).
27
+ *
28
+ * `costSource` defaults to the weakest honest label (`local_estimate`); pass a stronger
29
+ * source ONLY when the run genuinely carries it. `recoverability` defaults to
30
+ * `not_evaluated` (the eval is the separate `compact --eval` path); pass the eval result
31
+ * when one ran. Returns a new object; the input report is not mutated.
32
+ */
33
+ export function withSavingsEvidence(report, opts = {}) {
34
+ const cost_source = opts.costSource ?? "local_estimate";
35
+ const recoverability = opts.recoverability ?? "not_evaluated";
36
+ const savings_evidence = {
37
+ trace_identity: report.trace_fingerprint ? "fingerprinted" : "unidentified",
38
+ cost_source,
39
+ recoverability,
40
+ semantic_preservation: "not_evaluated",
41
+ evidence_rung: "local_estimate_single_run",
42
+ billing_confirmed: false,
43
+ label: `Per-run ${cost_source.replace(/_/g, " ")} delta (single run → local-estimate rung). ` +
44
+ `Recoverability: ${recoverability}` +
45
+ (recoverability === "not_evaluated" ? " (run `compact --eval`)" : "") +
46
+ ". Semantic preservation: not_evaluated. NOT billing-confirmed, NOT realized savings, NOT extrapolated."
47
+ };
48
+ return { ...report, savings_evidence };
49
+ }
50
+ /** Render the composed per-run savings-evidence record for the markdown report. */
51
+ function formatSavingsEvidenceLines(report) {
52
+ const se = report.savings_evidence;
53
+ if (!se) {
54
+ return [];
55
+ }
56
+ return [
57
+ "",
58
+ "## Savings Evidence (per-run, labeled)",
59
+ "",
60
+ `- Trace identity: ${se.trace_identity}`,
61
+ `- Cost source: ${se.cost_source}`,
62
+ `- Recoverability: ${se.recoverability}`,
63
+ `- Semantic preservation: ${se.semantic_preservation}`,
64
+ `- Evidence rung: ${se.evidence_rung}`,
65
+ `- Billing-confirmed: ${se.billing_confirmed}`,
66
+ `- ${se.label}`
67
+ ];
68
+ }
19
69
  function formatTokenBreakdown(tokens) {
20
70
  return `${tokens.totalTokens} total (${tokens.inputTokens} input, ${tokens.outputTokens} output)`;
21
71
  }
@@ -217,6 +267,7 @@ export function formatCompactionMarkdownReport(report, policy) {
217
267
  `- Saving per run: $${report.saving_per_run.toFixed(6)} (estimated)`,
218
268
  ...formatValueProofLines(report),
219
269
  ...formatSpendBySourceLines(report),
270
+ ...formatSavingsEvidenceLines(report),
220
271
  "",
221
272
  "## Waste Pattern",
222
273
  "",
@@ -1,3 +1,4 @@
1
+ import { type PerToolRunRecordRollup } from "./local-run-record.js";
1
2
  import type { CompactionReport, WasteFinding } from "./types.js";
2
3
  /**
3
4
  * Duplicate-safety summary for the `summary` rollup, by content fingerprint. A count of DISTINCT
@@ -35,8 +36,44 @@ export interface SkippedReportFile {
35
36
  path: string;
36
37
  reason: string;
37
38
  }
39
+ /**
40
+ * V0.2 Increment 2 — the MEASURED-across-distinct-runs estimate delta. Aggregates the per-run
41
+ * estimate reductions over N DISTINCT runs (dedup by content fingerprint) into a mean + dispersion,
42
+ * and applies the accepted criterion (`docs/design/billing-delta-loop.md`): **N ≥ 3 distinct runs
43
+ * AND the mean ±2·SE interval excludes zero**. When BOTH hold, the figure earns the
44
+ * `measured_caveated_estimate_delta` rung (claims-and-evidence-ladder rung 1.5) — a measured
45
+ * estimate, NOT a single anecdote. Below the bar it stays rung 1 (`local_estimate_single_run`).
46
+ *
47
+ * It is STILL an estimate (chars/4 + price-table): explicitly NOT `provider-reported`, NOT
48
+ * eval-backed (rung 3), and NEVER `billing-confirmed` (rung 5). No time-period extrapolation.
49
+ */
50
+ export interface MeasuredEstimateDelta {
51
+ /** N distinct runs used (≥3 required to qualify). */
52
+ distinct_run_count: number;
53
+ /** Mean per-run percent reduction across the distinct runs (estimate). */
54
+ mean_percent_reduction: number;
55
+ /** Standard error of the mean percent reduction (0 when N<2 — no dispersion). */
56
+ std_error: number;
57
+ /** Lower / upper bound of the mean ±2·SE interval. */
58
+ ci_low: number;
59
+ ci_high: number;
60
+ /** Does the ±2·SE interval exclude zero (lower bound > 0)? */
61
+ excludes_zero: boolean;
62
+ /** Criterion met: N≥3 AND the ±2·SE interval excludes zero. */
63
+ qualifies: boolean;
64
+ /** The claims-ladder rung this aggregate stands on. */
65
+ evidence_rung: "measured_caveated_estimate_delta" | "local_estimate_single_run";
66
+ /** One-line honest label — estimate-class, never billing-confirmed, never eval-backed. */
67
+ label: string;
68
+ }
38
69
  export interface RunSummary extends RunSummaryBucket {
39
70
  average_percent_reduction: number;
71
+ /**
72
+ * V0.2 measured-across-distinct-runs estimate delta (the "measured" rung). Mean ±2·SE over the
73
+ * distinct runs' estimate reductions; `qualifies` only when N≥3 AND the interval excludes zero.
74
+ * Estimate-class — never billing-confirmed, never eval-backed.
75
+ */
76
+ measured_estimate_delta: MeasuredEstimateDelta;
40
77
  savings_by_policy: Record<string, RunSummaryBucket>;
41
78
  savings_by_waste_pattern: Record<string, RunSummaryBucket>;
42
79
  top_savings_runs: TopSavingsRun[];
@@ -47,6 +84,15 @@ export interface RunSummary extends RunSummaryBucket {
47
84
  * are never inflated. Runs without a fingerprint are each counted as distinct.
48
85
  */
49
86
  distinctness: RunDistinctnessSummary;
87
+ /**
88
+ * ADDITIVE (unified-run-flow LOCAL layer, 2026-07-02): per-tool rollup of the locally accumulated
89
+ * per-run token_source records (`.compaction/run-records/*.json`). Observed token COUNTS + their
90
+ * honest per-axis `token_source` tiers ONLY (mirrors docs/ops/app-source-status-contract.md) —
91
+ * no cost figures, and `output_savings` is the LITERAL "unavailable" for every tool (gated).
92
+ */
93
+ per_tool_token_records: PerToolRunRecordRollup[];
94
+ run_records_source_glob: string;
95
+ run_record_skipped_files: SkippedReportFile[];
50
96
  generated_at: string;
51
97
  source_glob: string;
52
98
  }
@@ -54,6 +100,15 @@ interface LoadedReport {
54
100
  path: string;
55
101
  report: CompactionReport;
56
102
  }
103
+ /** Minimum DISTINCT runs for a measured (not single-anecdote) estimate delta. */
104
+ export declare const MIN_RUNS_FOR_MEASURED_DELTA = 3;
105
+ /**
106
+ * Compute the measured-across-distinct-runs estimate delta over a set of per-run percent
107
+ * reductions (PURE). Applies the accepted criterion: N ≥ 3 AND the mean ±2·SE interval excludes
108
+ * zero. Estimate-class only — the result is NEVER billing-confirmed or eval-backed. Uses the
109
+ * sample standard deviation (n−1); with n<2 there is no dispersion (SE 0) so it cannot qualify.
110
+ */
111
+ export declare function computeMeasuredEstimateDelta(percentReductions: number[]): MeasuredEstimateDelta;
57
112
  export declare function validateCompactionReport(value: unknown): {
58
113
  report?: CompactionReport;
59
114
  reason?: string;
@@ -63,7 +118,7 @@ export declare function readCompactionRunReports(runsDirectory?: string): Promis
63
118
  skippedFiles: SkippedReportFile[];
64
119
  }>;
65
120
  export declare function aggregateCompactionReports(reports: LoadedReport[], skippedFiles?: SkippedReportFile[], generatedAt?: string, sourceGlob?: string): RunSummary;
66
- export declare function createRunSummary(runsDirectory?: string): Promise<RunSummary>;
121
+ export declare function createRunSummary(runsDirectory?: string, recordsDirectory?: string): Promise<RunSummary>;
67
122
  export declare function formatRunSummary(summary: RunSummary): string;
68
123
  export declare function formatRunSummaryMarkdown(summary: RunSummary): string;
69
124
  export {};