@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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The POST-APPROVAL binary auto-apply ASK (charter 2026-07-04-run-7, lane 5: "binary ask-after-approve,
|
|
3
|
+
* inferred safest scope").
|
|
4
|
+
*
|
|
5
|
+
* WHAT THIS IS: pure logic for the founder's §11a block-4 question — the ONE binary question offered
|
|
6
|
+
* AFTER a user has ALREADY explicitly approved a compaction in-workflow ("Apply this automatically
|
|
7
|
+
* next time for this workflow when safety gates pass? [y] yes / [n] no, ask me each time", default no).
|
|
8
|
+
*
|
|
9
|
+
* WHAT THIS IS NOT — the rails:
|
|
10
|
+
* - It APPLIES NOTHING. Saving a preference records INTENT for a FUTURE gated slice; it never turns
|
|
11
|
+
* into an application here (see `auto-apply-gates.ts` — report-only — and `policy-preferences.ts` —
|
|
12
|
+
* a store with no apply path). This module does no I/O at all: it returns a DECISION describing
|
|
13
|
+
* what the CLI boundary should persist (via `savePolicyPreference`) or skip. It never writes.
|
|
14
|
+
* - The offer exists ONLY after an explicit approval already happened. `approvedInWorkflow=false`
|
|
15
|
+
* → skip, always.
|
|
16
|
+
* - Default is NO. Only an explicit affirmative answer saves anything; `no` / default / anything
|
|
17
|
+
* unrecognized saves nothing.
|
|
18
|
+
* - Scope is the SAFEST inferred scope: one tool/workflow (+ repo where detectable + policy type),
|
|
19
|
+
* NEVER global, NEVER cross-tool. There is NO scope menu — the scope is inferred, never chosen.
|
|
20
|
+
* A missing/global/cross-tool tool → the offer is BLOCKED (saves nothing, fail-closed).
|
|
21
|
+
*/
|
|
22
|
+
import { AUTO_APPLY_GATE_NAMES } from "./auto-apply-gates.js";
|
|
23
|
+
import { validatePolicyPreferenceScope } from "./policy-preferences.js";
|
|
24
|
+
/** The exact §11a block-4 binary question. No scope menu — the scope is inferred, never asked. */
|
|
25
|
+
export const AUTO_APPLY_QUESTION_LINES = [
|
|
26
|
+
"Apply this automatically next time for this workflow when safety gates pass?",
|
|
27
|
+
" [y] yes",
|
|
28
|
+
" [n] no, ask me each time"
|
|
29
|
+
];
|
|
30
|
+
/** A stored preference records these gate names (the founder's gate list from `auto-apply-gates`). */
|
|
31
|
+
export const AUTO_APPLY_PREFERENCE_GATES_REQUIRED = [...AUTO_APPLY_GATE_NAMES];
|
|
32
|
+
/**
|
|
33
|
+
* Interpret a raw answer. DEFAULT IS NO: only an explicit `y` / `yes` (case-insensitive, trimmed) is
|
|
34
|
+
* `yes`; empty, `n`, `no`, undefined, or anything unrecognized is `no`. Never throws.
|
|
35
|
+
*/
|
|
36
|
+
export function interpretAutoApplyAnswer(raw) {
|
|
37
|
+
if (typeof raw !== "string")
|
|
38
|
+
return "no";
|
|
39
|
+
const value = raw.trim().toLowerCase();
|
|
40
|
+
return value === "y" || value === "yes" ? "yes" : "no";
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the binary offer into a DECISION. PURE — no I/O, no side effects. Enforces every rail:
|
|
44
|
+
* offer only after approval; default no; safest inferred scope; never global/cross-tool. Saving is
|
|
45
|
+
* left to the CLI boundary (`savePolicyPreference`); this never applies anything.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveAutoApplyOffer(input) {
|
|
48
|
+
if (!input.approvedInWorkflow) {
|
|
49
|
+
return {
|
|
50
|
+
action: "skip",
|
|
51
|
+
reason: "no in-workflow approval happened — the auto-apply offer is only made after an explicit approval"
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (input.answer !== "yes") {
|
|
55
|
+
return { action: "skip", reason: "answered no (or default) — nothing saved; Compaction will keep asking each time" };
|
|
56
|
+
}
|
|
57
|
+
const tool = input.scope.tool?.trim();
|
|
58
|
+
const policyType = input.scope.policyType?.trim();
|
|
59
|
+
const repo = input.scope.repo?.trim();
|
|
60
|
+
const candidateScope = {
|
|
61
|
+
tool: tool ?? "",
|
|
62
|
+
policy_type: policyType ?? "",
|
|
63
|
+
...(repo ? { repo } : {})
|
|
64
|
+
};
|
|
65
|
+
const { problems } = validatePolicyPreferenceScope(candidateScope);
|
|
66
|
+
if (problems.length > 0) {
|
|
67
|
+
return { action: "blocked", problems };
|
|
68
|
+
}
|
|
69
|
+
const scope = {
|
|
70
|
+
tool: candidateScope.tool,
|
|
71
|
+
policy_type: candidateScope.policy_type,
|
|
72
|
+
...(repo ? { repo } : {})
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
action: "save",
|
|
76
|
+
scope,
|
|
77
|
+
preference: "auto-when-gates-pass",
|
|
78
|
+
gatesRequired: [...AUTO_APPLY_PREFERENCE_GATES_REQUIRED]
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The §11a block-5 saved-preference confirmation (scope · gates · undo hint). Honest: states the
|
|
83
|
+
* preference is stored and that every FUTURE application would be logged, evidence-labeled, undoable —
|
|
84
|
+
* it does NOT claim anything was applied. No wall-clock, no content.
|
|
85
|
+
*/
|
|
86
|
+
export function formatSavedPreferenceConfirmation(preference) {
|
|
87
|
+
const scopeParts = ["this tool", preference.scope.repo ? "this repo" : null, "this policy type"].filter((part) => part !== null);
|
|
88
|
+
return [
|
|
89
|
+
"Saved preference:",
|
|
90
|
+
`Compaction will apply ${preference.scope.policy_type} automatically for`,
|
|
91
|
+
"this workflow when safety gates pass.",
|
|
92
|
+
"",
|
|
93
|
+
`Scope: this workflow only (${scopeParts.join(" · ")}) — never`,
|
|
94
|
+
"global, never cross-tool. Every automatic application is logged, evidence-labeled,",
|
|
95
|
+
"and undoable.",
|
|
96
|
+
"",
|
|
97
|
+
"Not yet active: no auto-apply engine exists yet — this records your preference only.",
|
|
98
|
+
"Nothing is applied automatically today.",
|
|
99
|
+
"",
|
|
100
|
+
"Review or turn off anytime:",
|
|
101
|
+
" compaction policies list",
|
|
102
|
+
` compaction policies disable ${preference.id}`
|
|
103
|
+
].join("\n");
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=auto-apply-ask.js.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPORT-ONLY auto-apply GATE CHECK (charter 2026-07-04-run-7, lane 5: "auto-apply preference model:
|
|
3
|
+
* ... fail-closed gates scaffold, tests proving OFF by default").
|
|
4
|
+
*
|
|
5
|
+
* ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
6
|
+
* │ THIS MODULE APPLIES NOTHING. It is a PURE, REPORT-ONLY verdict function. │
|
|
7
|
+
* │ │
|
|
8
|
+
* │ `evaluateAutoApplyGates(context)` reads a context object and returns a per-gate pass/fail │
|
|
9
|
+
* │ verdict plus an `allPassed` roll-up. That is ALL it does. It: │
|
|
10
|
+
* │ - imports NOTHING that mutates a session, trace, run record, file, or context; │
|
|
11
|
+
* │ - performs NO file I/O, NO network, NO process side effect of any kind; │
|
|
12
|
+
* │ - exports NO apply / execute / mutate / write / run / persist / save function; │
|
|
13
|
+
* │ - NEVER invokes the apply engine (src/engine/**) — it does not import it at all. │
|
|
14
|
+
* │ │
|
|
15
|
+
* │ A verdict of `allPassed: true` does NOT cause anything to be applied. No caller in this │
|
|
16
|
+
* │ cycle turns a verdict into an application — there is no gated auto-apply engine yet. This │
|
|
17
|
+
* │ function exists so the FUTURE gated slice has ONE honest, testable place that decides whether │
|
|
18
|
+
* │ the founder's safety gates are satisfied, and so today's tests can prove auto-apply is OFF. │
|
|
19
|
+
* └─────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
20
|
+
*
|
|
21
|
+
* FAIL-CLOSED (founder decision, active-loop-charter): every gate defaults to FAILED. A gate passes
|
|
22
|
+
* ONLY when its context field is EXACTLY boolean `true`. Any missing / undefined / null / unknown /
|
|
23
|
+
* non-`true` input → that gate is FAILED (never "assumed pass"). `allPassed` requires EVERY gate to
|
|
24
|
+
* pass — including the `explicit-opt-in` gate — so with an empty or partial context, `allPassed` is
|
|
25
|
+
* always `false`.
|
|
26
|
+
*
|
|
27
|
+
* The gate list mirrors the founder's rule verbatim ("Auto-apply allowed ONLY when: explicit opt-in +
|
|
28
|
+
* matching scope + original retained + capsule provenance + recoverability pass + threshold + risk
|
|
29
|
+
* checks + evidence label emitted + rollback path") and the store's `DEFAULT_GATES_REQUIRED`.
|
|
30
|
+
*/
|
|
31
|
+
/** The ordered auto-apply gate names. `explicit-opt-in` is first — it is the whole precondition. */
|
|
32
|
+
export declare const AUTO_APPLY_GATE_NAMES: readonly ["explicit-opt-in", "scope-match", "original-retained", "capsule-provenance", "recoverability-pass", "reduction-threshold", "risk-checks", "evidence-label-emitted", "rollback-path"];
|
|
33
|
+
export type AutoApplyGateName = (typeof AUTO_APPLY_GATE_NAMES)[number];
|
|
34
|
+
/**
|
|
35
|
+
* The inputs a caller would collect to ask "may this be auto-applied?". EVERY field is optional and
|
|
36
|
+
* defaults to FAILED — a caller must positively assert each `true`. Nothing here is content; these
|
|
37
|
+
* are boolean safety facts the (future) gated slice would establish before any application.
|
|
38
|
+
*/
|
|
39
|
+
export interface AutoApplyGateContext {
|
|
40
|
+
/** An ENABLED `auto-when-gates-pass` preference exists for this exact scope (the opt-in). */
|
|
41
|
+
explicitOptIn?: boolean;
|
|
42
|
+
/** The run's inferred scope (tool + repo? + policy type) matches the stored preference's scope. */
|
|
43
|
+
scopeMatches?: boolean;
|
|
44
|
+
/** The un-compacted original is retained and locatable. */
|
|
45
|
+
originalRetained?: boolean;
|
|
46
|
+
/** Capsule / source-pointer provenance is present for what would change. */
|
|
47
|
+
capsuleProvenancePresent?: boolean;
|
|
48
|
+
/** A recoverability check passed (the change can be reversed). */
|
|
49
|
+
recoverabilityPassed?: boolean;
|
|
50
|
+
/** The reduction threshold was met (enough avoidable context to be worth applying). */
|
|
51
|
+
reductionThresholdMet?: boolean;
|
|
52
|
+
/** Risk checks passed (no risky content class flagged). */
|
|
53
|
+
riskChecksPassed?: boolean;
|
|
54
|
+
/** An evidence label was emitted for the change. */
|
|
55
|
+
evidenceLabelPresent?: boolean;
|
|
56
|
+
/** A rollback / undo path is present. */
|
|
57
|
+
rollbackPathPresent?: boolean;
|
|
58
|
+
}
|
|
59
|
+
/** One gate's verdict. `passed` is REPORT-ONLY; `reason` explains a fail (fail-closed) or a pass. */
|
|
60
|
+
export interface AutoApplyGateVerdict {
|
|
61
|
+
name: AutoApplyGateName;
|
|
62
|
+
passed: boolean;
|
|
63
|
+
reason: string;
|
|
64
|
+
}
|
|
65
|
+
/** The whole verdict. `allPassed` is true ONLY when every gate passed. Applies nothing. */
|
|
66
|
+
export interface AutoApplyGateReport {
|
|
67
|
+
allPassed: boolean;
|
|
68
|
+
gates: AutoApplyGateVerdict[];
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* PURE + REPORT-ONLY. Evaluate the auto-apply safety gates over `context` and return per-gate
|
|
72
|
+
* verdicts + an `allPassed` roll-up. FAIL-CLOSED: a gate passes ONLY when its field is exactly
|
|
73
|
+
* `true`; every other value (missing / null / undefined / non-`true`) fails that gate. This function
|
|
74
|
+
* NEVER applies, mutates, writes, or invokes anything — the returned report is its only effect.
|
|
75
|
+
*/
|
|
76
|
+
export declare function evaluateAutoApplyGates(context?: AutoApplyGateContext): AutoApplyGateReport;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REPORT-ONLY auto-apply GATE CHECK (charter 2026-07-04-run-7, lane 5: "auto-apply preference model:
|
|
3
|
+
* ... fail-closed gates scaffold, tests proving OFF by default").
|
|
4
|
+
*
|
|
5
|
+
* ┌─────────────────────────────────────────────────────────────────────────────────────────────┐
|
|
6
|
+
* │ THIS MODULE APPLIES NOTHING. It is a PURE, REPORT-ONLY verdict function. │
|
|
7
|
+
* │ │
|
|
8
|
+
* │ `evaluateAutoApplyGates(context)` reads a context object and returns a per-gate pass/fail │
|
|
9
|
+
* │ verdict plus an `allPassed` roll-up. That is ALL it does. It: │
|
|
10
|
+
* │ - imports NOTHING that mutates a session, trace, run record, file, or context; │
|
|
11
|
+
* │ - performs NO file I/O, NO network, NO process side effect of any kind; │
|
|
12
|
+
* │ - exports NO apply / execute / mutate / write / run / persist / save function; │
|
|
13
|
+
* │ - NEVER invokes the apply engine (src/engine/**) — it does not import it at all. │
|
|
14
|
+
* │ │
|
|
15
|
+
* │ A verdict of `allPassed: true` does NOT cause anything to be applied. No caller in this │
|
|
16
|
+
* │ cycle turns a verdict into an application — there is no gated auto-apply engine yet. This │
|
|
17
|
+
* │ function exists so the FUTURE gated slice has ONE honest, testable place that decides whether │
|
|
18
|
+
* │ the founder's safety gates are satisfied, and so today's tests can prove auto-apply is OFF. │
|
|
19
|
+
* └─────────────────────────────────────────────────────────────────────────────────────────────┘
|
|
20
|
+
*
|
|
21
|
+
* FAIL-CLOSED (founder decision, active-loop-charter): every gate defaults to FAILED. A gate passes
|
|
22
|
+
* ONLY when its context field is EXACTLY boolean `true`. Any missing / undefined / null / unknown /
|
|
23
|
+
* non-`true` input → that gate is FAILED (never "assumed pass"). `allPassed` requires EVERY gate to
|
|
24
|
+
* pass — including the `explicit-opt-in` gate — so with an empty or partial context, `allPassed` is
|
|
25
|
+
* always `false`.
|
|
26
|
+
*
|
|
27
|
+
* The gate list mirrors the founder's rule verbatim ("Auto-apply allowed ONLY when: explicit opt-in +
|
|
28
|
+
* matching scope + original retained + capsule provenance + recoverability pass + threshold + risk
|
|
29
|
+
* checks + evidence label emitted + rollback path") and the store's `DEFAULT_GATES_REQUIRED`.
|
|
30
|
+
*/
|
|
31
|
+
/** The ordered auto-apply gate names. `explicit-opt-in` is first — it is the whole precondition. */
|
|
32
|
+
export const AUTO_APPLY_GATE_NAMES = [
|
|
33
|
+
"explicit-opt-in",
|
|
34
|
+
"scope-match",
|
|
35
|
+
"original-retained",
|
|
36
|
+
"capsule-provenance",
|
|
37
|
+
"recoverability-pass",
|
|
38
|
+
"reduction-threshold",
|
|
39
|
+
"risk-checks",
|
|
40
|
+
"evidence-label-emitted",
|
|
41
|
+
"rollback-path"
|
|
42
|
+
];
|
|
43
|
+
/** Map each gate to the context field it reads and the human reason strings. */
|
|
44
|
+
const GATE_CHECKS = [
|
|
45
|
+
{
|
|
46
|
+
name: "explicit-opt-in",
|
|
47
|
+
read: (c) => c.explicitOptIn,
|
|
48
|
+
passReason: "an enabled auto-when-gates-pass preference exists for this scope",
|
|
49
|
+
failReason: "no explicit opt-in — the user has not chosen auto-apply for this scope (default: ask each time)"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "scope-match",
|
|
53
|
+
read: (c) => c.scopeMatches,
|
|
54
|
+
passReason: "the run's inferred scope matches the stored preference's scope",
|
|
55
|
+
failReason: "the run's scope does not match a stored preference (never applies cross-scope)"
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: "original-retained",
|
|
59
|
+
read: (c) => c.originalRetained,
|
|
60
|
+
passReason: "the un-compacted original is retained and locatable",
|
|
61
|
+
failReason: "the original is not confirmed retained — cannot proceed without a retained original"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "capsule-provenance",
|
|
65
|
+
read: (c) => c.capsuleProvenancePresent,
|
|
66
|
+
passReason: "capsule / source-pointer provenance is present",
|
|
67
|
+
failReason: "capsule / source-pointer provenance is missing"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "recoverability-pass",
|
|
71
|
+
read: (c) => c.recoverabilityPassed,
|
|
72
|
+
passReason: "a recoverability check passed",
|
|
73
|
+
failReason: "the recoverability check did not pass"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "reduction-threshold",
|
|
77
|
+
read: (c) => c.reductionThresholdMet,
|
|
78
|
+
passReason: "the reduction threshold was met",
|
|
79
|
+
failReason: "the reduction threshold was not met"
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "risk-checks",
|
|
83
|
+
read: (c) => c.riskChecksPassed,
|
|
84
|
+
passReason: "risk checks passed",
|
|
85
|
+
failReason: "risk checks did not pass"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "evidence-label-emitted",
|
|
89
|
+
read: (c) => c.evidenceLabelPresent,
|
|
90
|
+
passReason: "an evidence label was emitted",
|
|
91
|
+
failReason: "no evidence label was emitted"
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "rollback-path",
|
|
95
|
+
read: (c) => c.rollbackPathPresent,
|
|
96
|
+
passReason: "a rollback / undo path is present",
|
|
97
|
+
failReason: "no rollback / undo path is present"
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* PURE + REPORT-ONLY. Evaluate the auto-apply safety gates over `context` and return per-gate
|
|
102
|
+
* verdicts + an `allPassed` roll-up. FAIL-CLOSED: a gate passes ONLY when its field is exactly
|
|
103
|
+
* `true`; every other value (missing / null / undefined / non-`true`) fails that gate. This function
|
|
104
|
+
* NEVER applies, mutates, writes, or invokes anything — the returned report is its only effect.
|
|
105
|
+
*/
|
|
106
|
+
export function evaluateAutoApplyGates(context = {}) {
|
|
107
|
+
const gates = GATE_CHECKS.map((gate) => {
|
|
108
|
+
const passed = gate.read(context) === true;
|
|
109
|
+
return { name: gate.name, passed, reason: passed ? gate.passReason : gate.failReason };
|
|
110
|
+
});
|
|
111
|
+
return { allPassed: gates.every((gate) => gate.passed), gates };
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=auto-apply-gates.js.map
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ActivityApprovalStatus, type ActivityEvent } from "./activity-event.js";
|
|
2
|
+
import { type BeforeCallRecommendation, type BeforeCallTool } from "./before-call.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build ONE content-free before-call activity event. `approvalStatus` records the approval OUTCOME
|
|
5
|
+
* honestly:
|
|
6
|
+
* - `"not-required"` — recommendation-only path (nothing was applied, so nothing needed approval).
|
|
7
|
+
* - `"asked-approved"` (with `applied: true`) — the operator approved a whitelist-safe mutation and the
|
|
8
|
+
* compacted input was applied to this call; the original was retained locally (pass `recoveryPointer`).
|
|
9
|
+
* - `"asked-declined"` — the operator was asked and declined; the original ran unchanged.
|
|
10
|
+
* - `"not-asked"` (with `notAvailableReason`) — apply was NOT available (ambiguous/unsupported argv, or
|
|
11
|
+
* no interactive TTY), so we did not ask; the original ran unchanged.
|
|
12
|
+
*
|
|
13
|
+
* Content-free in EVERY arm: only counts, policy id, honest labels, and (when applied) a CONTENT-SAFE
|
|
14
|
+
* recovery pointer (id/path, NEVER the prompt text) ride on the event. `applied_automatically` is the
|
|
15
|
+
* literal `false` in every arm — an approved mutation is a MANUAL per-invocation approval, never an
|
|
16
|
+
* auto-apply.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildBeforeCallActivityEvent(params: {
|
|
19
|
+
tool: BeforeCallTool;
|
|
20
|
+
recommendation: BeforeCallRecommendation;
|
|
21
|
+
approvalStatus: ActivityApprovalStatus;
|
|
22
|
+
/** True only for an operator-approved mutation that WAS applied to this call. */
|
|
23
|
+
applied?: boolean;
|
|
24
|
+
/** Content-safe recovery pointer (id/path) for an applied mutation — NEVER the prompt text. */
|
|
25
|
+
recoveryPointer?: string;
|
|
26
|
+
/** Content-free reason apply was not available (for the `not-asked` arm). */
|
|
27
|
+
notAvailableReason?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Which input boundary was mediated (content-free label, for the honest applied caveat):
|
|
30
|
+
* - `"prompt-flag"` (default, run-11): the value of a whitelisted value-taking prompt flag (dormant
|
|
31
|
+
* for the real tools — no such flag exists).
|
|
32
|
+
* - `"stdin"` (run-12): the whole stdin prompt STREAM for `codex exec` (the first real apply surface).
|
|
33
|
+
*/
|
|
34
|
+
boundary?: "prompt-flag" | "stdin";
|
|
35
|
+
}): ActivityEvent;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEFORE-CALL → metrics-only activity BRIDGE (PUBLIC CLI/SDK code — engine-free, ships in the npm
|
|
3
|
+
* package). Charter 2026-07-04-run-10. Turns a before-call recommendation + approval outcome into ONE
|
|
4
|
+
* content-free activity event, reusing the SAME event model (`buildRunCrossSurfaceEvent`) and the SAME
|
|
5
|
+
* local activity store (#597) as the run-9 measurement bridge — no new store, no new schema.
|
|
6
|
+
*
|
|
7
|
+
* Honesty rails (identical spirit to `shim-capture-bridge.ts`):
|
|
8
|
+
* - Input is a LOCAL-ESTIMATE pre-call (chars/4); output is unavailable-with-reason pre-call for BOTH
|
|
9
|
+
* tools (provider usage does not exist until after the call). The tier table structurally rejects a
|
|
10
|
+
* provider-reported label here.
|
|
11
|
+
* - CONTENT-FREE: only token COUNTS (before/after), the policy id, honest labels, and opaque ids ride
|
|
12
|
+
* on the event. No prompt/response/message text is ever read into the event.
|
|
13
|
+
* - Recommendation-only: `applied_automatically` is the literal `false`, auto-apply is OFF
|
|
14
|
+
* (`ask-each-time`), recovery `original_retained: true` (the original is never touched), sync
|
|
15
|
+
* `local-only`. The compacted input is NEVER applied by this path.
|
|
16
|
+
*/
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { createUsageMetadata } from "./usage-metadata.js";
|
|
19
|
+
import { buildRunFlowTokenReport } from "./run-flow-report.js";
|
|
20
|
+
import { buildRunCrossSurfaceEvent } from "./cross-surface-event.js";
|
|
21
|
+
import { computeActivityEventId } from "./activity-event.js";
|
|
22
|
+
import { BEFORE_CALL_REDUCTION_LABEL } from "./before-call.js";
|
|
23
|
+
/** The honest per-tool reason the OUTPUT axis is unavailable at pre-call time. */
|
|
24
|
+
const OUTPUT_UNAVAILABLE_REASON = {
|
|
25
|
+
codex: "no output tokens exist before the call; provider usage (turn.completed.usage) is only reported after codex runs",
|
|
26
|
+
cursor: "Cursor emits no provider usage, and no output exists before the call"
|
|
27
|
+
};
|
|
28
|
+
/** A content-free, deterministic run id from the tool + local-estimate counts (never any content). */
|
|
29
|
+
function contentFreeRunId(tool, before, after) {
|
|
30
|
+
const digest = createHash("sha256").update(`before-call|${tool}|${before}|${after}`).digest("hex");
|
|
31
|
+
return `before-call-${tool}-${digest.slice(0, 16)}`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build ONE content-free before-call activity event. `approvalStatus` records the approval OUTCOME
|
|
35
|
+
* honestly:
|
|
36
|
+
* - `"not-required"` — recommendation-only path (nothing was applied, so nothing needed approval).
|
|
37
|
+
* - `"asked-approved"` (with `applied: true`) — the operator approved a whitelist-safe mutation and the
|
|
38
|
+
* compacted input was applied to this call; the original was retained locally (pass `recoveryPointer`).
|
|
39
|
+
* - `"asked-declined"` — the operator was asked and declined; the original ran unchanged.
|
|
40
|
+
* - `"not-asked"` (with `notAvailableReason`) — apply was NOT available (ambiguous/unsupported argv, or
|
|
41
|
+
* no interactive TTY), so we did not ask; the original ran unchanged.
|
|
42
|
+
*
|
|
43
|
+
* Content-free in EVERY arm: only counts, policy id, honest labels, and (when applied) a CONTENT-SAFE
|
|
44
|
+
* recovery pointer (id/path, NEVER the prompt text) ride on the event. `applied_automatically` is the
|
|
45
|
+
* literal `false` in every arm — an approved mutation is a MANUAL per-invocation approval, never an
|
|
46
|
+
* auto-apply.
|
|
47
|
+
*/
|
|
48
|
+
export function buildBeforeCallActivityEvent(params) {
|
|
49
|
+
const { tool, recommendation, approvalStatus, applied, recoveryPointer, notAvailableReason } = params;
|
|
50
|
+
const boundary = params.boundary ?? "prompt-flag";
|
|
51
|
+
const usage = createUsageMetadata({
|
|
52
|
+
inputTokens: recommendation.input_tokens_before,
|
|
53
|
+
providerReportedTokens: false,
|
|
54
|
+
estimatedTokens: true,
|
|
55
|
+
limitations: [BEFORE_CALL_REDUCTION_LABEL]
|
|
56
|
+
});
|
|
57
|
+
const tokenReport = buildRunFlowTokenReport({ tool, usage, outputStatus: "unavailable" });
|
|
58
|
+
const base = buildRunCrossSurfaceEvent(tool, {
|
|
59
|
+
runId: contentFreeRunId(tool, recommendation.input_tokens_before, recommendation.input_tokens_after_estimate),
|
|
60
|
+
tokenReport,
|
|
61
|
+
reasons: { output: OUTPUT_UNAVAILABLE_REASON[tool] }
|
|
62
|
+
});
|
|
63
|
+
// Honest, content-free per-arm caveats. base.caveats already carries the reduction label.
|
|
64
|
+
const appliedWhat = boundary === "stdin"
|
|
65
|
+
? "the compacted input replaced the stdin prompt STREAM only; every command-line argument ran unchanged"
|
|
66
|
+
: "the compacted input replaced ONLY the whitelisted prompt argument — every other argument ran unchanged";
|
|
67
|
+
const armCaveats = applied
|
|
68
|
+
? [
|
|
69
|
+
`before-call compaction APPLIED to this call (operator-approved, per-invocation); ${appliedWhat}`,
|
|
70
|
+
"the ORIGINAL input was retained LOCAL-ONLY (gitignored, never uploaded) and is recoverable via the " +
|
|
71
|
+
"content-safe recovery pointer below"
|
|
72
|
+
]
|
|
73
|
+
: notAvailableReason
|
|
74
|
+
? [
|
|
75
|
+
`before-call apply not-available: ${notAvailableReason}`,
|
|
76
|
+
"the original input ran UNCHANGED (fail-closed); this is recommendation-only for this invocation"
|
|
77
|
+
]
|
|
78
|
+
: [
|
|
79
|
+
"before-call recommendation-only: nothing was applied to this call; the original input ran unchanged",
|
|
80
|
+
"to apply a before-call compaction, use the explicit `compaction apply-context` path"
|
|
81
|
+
];
|
|
82
|
+
const activity = {
|
|
83
|
+
...base,
|
|
84
|
+
// The compacted local-estimate is the pre-call INPUT AFTER figure. Realized (applied) or recommended
|
|
85
|
+
// (un-applied) — either way a LOCAL-ESTIMATE delta, never a saving, never provider-reported.
|
|
86
|
+
input_after: recommendation.input_tokens_after_estimate,
|
|
87
|
+
policy_used: recommendation.policy,
|
|
88
|
+
evidence_level: recommendation.evidence_label,
|
|
89
|
+
caveats: [...(base.caveats ?? []), ...armCaveats],
|
|
90
|
+
approval_status: approvalStatus,
|
|
91
|
+
auto_apply: {
|
|
92
|
+
// Eligible reflects whether there was avoidable context — but auto-apply stays OFF regardless.
|
|
93
|
+
// Even an approved mutation is applied_automatically:false (a manual per-invocation approval).
|
|
94
|
+
eligible: recommendation.has_avoidable_context,
|
|
95
|
+
preference: "ask-each-time",
|
|
96
|
+
applied_automatically: false
|
|
97
|
+
},
|
|
98
|
+
recovery: recoveryPointer ? { original_retained: true, location: recoveryPointer } : { original_retained: true },
|
|
99
|
+
sync_status: "local-only"
|
|
100
|
+
};
|
|
101
|
+
return { ...activity, activity_event_id: computeActivityEventId(activity) };
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=before-call-activity.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The gitignored subtree that holds retained originals (local-only). */
|
|
2
|
+
export declare const BEFORE_CALL_RECOVERY_DIRNAME = "before-call-recovery";
|
|
3
|
+
export interface RetainedOriginal {
|
|
4
|
+
/** Content-free recovery id (random; never derived from content). */
|
|
5
|
+
recoveryId: string;
|
|
6
|
+
/** Absolute path to the local-only artifact holding the original prompt. */
|
|
7
|
+
recoveryPath: string;
|
|
8
|
+
/** A content-safe pointer suitable for the activity event (id + path — NEVER the prompt text). */
|
|
9
|
+
pointer: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Retain `original` locally and return a content-safe pointer. Throws if the artifact cannot be
|
|
13
|
+
* written — the caller treats a throw as "retention failed → do NOT apply, run the original unchanged".
|
|
14
|
+
* `idFactory` is injectable for deterministic tests; production uses a random UUID.
|
|
15
|
+
*/
|
|
16
|
+
export declare function retainOriginalPrompt(original: string, opts?: {
|
|
17
|
+
cwd?: string;
|
|
18
|
+
idFactory?: () => string;
|
|
19
|
+
}): RetainedOriginal;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LOCAL-ONLY original-prompt retention for an APPROVED before-call mutation (PUBLIC CLI/SDK code —
|
|
3
|
+
* engine-free). Charter 2026-07-04-run-11 (founder SAFETY decision: "Original retention +
|
|
4
|
+
* recoverability").
|
|
5
|
+
*
|
|
6
|
+
* When — and ONLY when — the operator explicitly approves a whitelist-safe before-call compaction, the
|
|
7
|
+
* ORIGINAL prompt must remain recoverable. This module writes the original to a LOCAL-ONLY artifact
|
|
8
|
+
* under `<cwd>/.compaction/before-call-recovery/<id>` and returns a CONTENT-SAFE pointer (the id + the
|
|
9
|
+
* path). The rails (binding):
|
|
10
|
+
* - `.compaction/` is gitignored → the artifact is NEVER committed, NEVER uploaded, NEVER synced.
|
|
11
|
+
* - The recovery id is CONTENT-FREE (random, never derived from the prompt) so the pointer that rides
|
|
12
|
+
* on the activity event leaks nothing about the content.
|
|
13
|
+
* - ONLY this file holds the prompt text. The caller puts the POINTER (not the text) on the activity
|
|
14
|
+
* event. The prompt text is never logged.
|
|
15
|
+
* - If retention cannot be completed for ANY reason, the caller MUST NOT apply the mutation (it runs
|
|
16
|
+
* the original command unchanged) — an un-recoverable mutation is never performed.
|
|
17
|
+
*/
|
|
18
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
/** The gitignored subtree that holds retained originals (local-only). */
|
|
22
|
+
export const BEFORE_CALL_RECOVERY_DIRNAME = "before-call-recovery";
|
|
23
|
+
function recoveryDir(cwd) {
|
|
24
|
+
return path.join(cwd, ".compaction", BEFORE_CALL_RECOVERY_DIRNAME);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Retain `original` locally and return a content-safe pointer. Throws if the artifact cannot be
|
|
28
|
+
* written — the caller treats a throw as "retention failed → do NOT apply, run the original unchanged".
|
|
29
|
+
* `idFactory` is injectable for deterministic tests; production uses a random UUID.
|
|
30
|
+
*/
|
|
31
|
+
export function retainOriginalPrompt(original, opts = {}) {
|
|
32
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
33
|
+
const recoveryId = (opts.idFactory ?? randomUUID)();
|
|
34
|
+
const dir = recoveryDir(cwd);
|
|
35
|
+
mkdirSync(dir, { recursive: true });
|
|
36
|
+
const recoveryPath = path.join(dir, `${recoveryId}.txt`);
|
|
37
|
+
// The ORIGINAL prompt text lives ONLY here (local-only, gitignored). Never logged, never uploaded.
|
|
38
|
+
writeFileSync(recoveryPath, original, "utf8");
|
|
39
|
+
return {
|
|
40
|
+
recoveryId,
|
|
41
|
+
recoveryPath,
|
|
42
|
+
// Content-safe: id + path only. This is what may ride on the (content-free) activity event.
|
|
43
|
+
pointer: `before-call-recovery:${recoveryId}`
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=before-call-recovery.js.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEFORE-CALL STDIN-BOUNDARY apply PLANNER (PUBLIC CLI/SDK code — engine-free, ships in the npm
|
|
3
|
+
* package). Charter 2026-07-04-run-12 — the FIRST real before-call mutation surface.
|
|
4
|
+
*
|
|
5
|
+
* Run-11 proved the argv route is blocked (Codex + Cursor take bare positional prompts; no value-taking
|
|
6
|
+
* prompt flag to rewrite). Run-12 uses the OTHER boundary the founder named: stdin. Codex `exec`
|
|
7
|
+
* documents a first-class stdin prompt path ("if not provided as an argument (or if `-` is used),
|
|
8
|
+
* instructions are read from stdin"), so when the invocation is `codex exec [flags]` with NO positional
|
|
9
|
+
* prompt (or a lone `-`), the WHOLE stdin stream IS the prompt — an explicit, unambiguous boundary.
|
|
10
|
+
*
|
|
11
|
+
* This module is a PURE decision planner: given the argv, the buffered stdin content, whether a terminal
|
|
12
|
+
* is available to ask on, and an injectable `decide` callback (the `[y/n/v]` approval), it returns what
|
|
13
|
+
* the shim should feed the real tool (`compacted` or the `original` stdin) plus the honest activity
|
|
14
|
+
* outcome. It performs NO terminal I/O, NO file I/O, and NO retention — the command wraps those so the
|
|
15
|
+
* fail-closed retention rule ("if the original cannot be retained, do NOT apply") stays enforced in one
|
|
16
|
+
* place and this planner stays trivially testable.
|
|
17
|
+
*
|
|
18
|
+
* FAIL-CLOSED (binding): the plan emits `compacted` ONLY when the stdin boundary is provably safe (Codex
|
|
19
|
+
* `exec`, stdin is the sole prompt), there is avoidable context, a terminal is available, AND `decide`
|
|
20
|
+
* returns `"apply"`. Every other path emits the ORIGINAL stdin unchanged. No auto-apply, no default yes.
|
|
21
|
+
*/
|
|
22
|
+
import { type BeforeCallRecommendation, type BeforeCallTool, type StdinPromptBoundary } from "./before-call.js";
|
|
23
|
+
import type { ActivityApprovalStatus } from "./activity-event.js";
|
|
24
|
+
/** The approval decision the planner asks for (kept local to avoid a cycle with the precall command). */
|
|
25
|
+
export type StdinApplyChoice = "apply" | "decline";
|
|
26
|
+
export interface StdinApplyPlan {
|
|
27
|
+
/** The stdin-boundary safety result (Codex `exec`, stdin is the sole prompt → safe). */
|
|
28
|
+
boundary: StdinPromptBoundary;
|
|
29
|
+
/** The content-free recommendation — present ONLY when the boundary was safe (so stdin was analyzed). */
|
|
30
|
+
recommendation?: BeforeCallRecommendation;
|
|
31
|
+
/** What the shim should feed the real tool. `compacted` ONLY after an explicit approval. */
|
|
32
|
+
emit: "compacted" | "original";
|
|
33
|
+
/** The compacted stdin — present ONLY when `emit === "compacted"`. Never persisted to activity. */
|
|
34
|
+
compacted?: string;
|
|
35
|
+
/** Honest approval outcome for the activity event. */
|
|
36
|
+
approvalStatus: ActivityApprovalStatus;
|
|
37
|
+
/** True ONLY for an operator-approved compaction that will be applied to this call. */
|
|
38
|
+
applied: boolean;
|
|
39
|
+
/** Content-free reason apply was not offered/available (for the honest not-asked arm). */
|
|
40
|
+
notAvailableReason?: string;
|
|
41
|
+
/** Whether the caller should record ONE activity event (true only when there is something honest to log). */
|
|
42
|
+
record: boolean;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Plan the stdin-boundary before-call apply decision (charter run-12). Pure + fully injectable:
|
|
46
|
+
* - `interactive` is TRUE only when a terminal is available to ask on (the shim passes `[ -t 1 ]`;
|
|
47
|
+
* stdin is intentionally piped here, so interactivity is about the terminal, not stdin).
|
|
48
|
+
* - `decide(rec)` runs the `[y/n/v]` approval (the command drives it over `/dev/tty`; tests inject it).
|
|
49
|
+
*
|
|
50
|
+
* The planner NEVER applies without a safe boundary + avoidable context + a terminal + an explicit
|
|
51
|
+
* `"apply"`. Retention of the original is the COMMAND's responsibility (fail-closed if it fails).
|
|
52
|
+
*/
|
|
53
|
+
export declare function planStdinApply(params: {
|
|
54
|
+
tool: BeforeCallTool;
|
|
55
|
+
argv: string[];
|
|
56
|
+
stdinContent: string;
|
|
57
|
+
interactive: boolean;
|
|
58
|
+
decide: (rec: BeforeCallRecommendation) => StdinApplyChoice;
|
|
59
|
+
}): StdinApplyPlan;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BEFORE-CALL STDIN-BOUNDARY apply PLANNER (PUBLIC CLI/SDK code — engine-free, ships in the npm
|
|
3
|
+
* package). Charter 2026-07-04-run-12 — the FIRST real before-call mutation surface.
|
|
4
|
+
*
|
|
5
|
+
* Run-11 proved the argv route is blocked (Codex + Cursor take bare positional prompts; no value-taking
|
|
6
|
+
* prompt flag to rewrite). Run-12 uses the OTHER boundary the founder named: stdin. Codex `exec`
|
|
7
|
+
* documents a first-class stdin prompt path ("if not provided as an argument (or if `-` is used),
|
|
8
|
+
* instructions are read from stdin"), so when the invocation is `codex exec [flags]` with NO positional
|
|
9
|
+
* prompt (or a lone `-`), the WHOLE stdin stream IS the prompt — an explicit, unambiguous boundary.
|
|
10
|
+
*
|
|
11
|
+
* This module is a PURE decision planner: given the argv, the buffered stdin content, whether a terminal
|
|
12
|
+
* is available to ask on, and an injectable `decide` callback (the `[y/n/v]` approval), it returns what
|
|
13
|
+
* the shim should feed the real tool (`compacted` or the `original` stdin) plus the honest activity
|
|
14
|
+
* outcome. It performs NO terminal I/O, NO file I/O, and NO retention — the command wraps those so the
|
|
15
|
+
* fail-closed retention rule ("if the original cannot be retained, do NOT apply") stays enforced in one
|
|
16
|
+
* place and this planner stays trivially testable.
|
|
17
|
+
*
|
|
18
|
+
* FAIL-CLOSED (binding): the plan emits `compacted` ONLY when the stdin boundary is provably safe (Codex
|
|
19
|
+
* `exec`, stdin is the sole prompt), there is avoidable context, a terminal is available, AND `decide`
|
|
20
|
+
* returns `"apply"`. Every other path emits the ORIGINAL stdin unchanged. No auto-apply, no default yes.
|
|
21
|
+
*/
|
|
22
|
+
import { analyzeBeforeCall, resolveStdinPromptBoundary } from "./before-call.js";
|
|
23
|
+
/**
|
|
24
|
+
* Plan the stdin-boundary before-call apply decision (charter run-12). Pure + fully injectable:
|
|
25
|
+
* - `interactive` is TRUE only when a terminal is available to ask on (the shim passes `[ -t 1 ]`;
|
|
26
|
+
* stdin is intentionally piped here, so interactivity is about the terminal, not stdin).
|
|
27
|
+
* - `decide(rec)` runs the `[y/n/v]` approval (the command drives it over `/dev/tty`; tests inject it).
|
|
28
|
+
*
|
|
29
|
+
* The planner NEVER applies without a safe boundary + avoidable context + a terminal + an explicit
|
|
30
|
+
* `"apply"`. Retention of the original is the COMMAND's responsibility (fail-closed if it fails).
|
|
31
|
+
*/
|
|
32
|
+
export function planStdinApply(params) {
|
|
33
|
+
const { tool, argv, stdinContent, interactive, decide } = params;
|
|
34
|
+
const boundary = resolveStdinPromptBoundary(tool, argv);
|
|
35
|
+
if (!boundary.safe) {
|
|
36
|
+
// stdin is NOT provably the whole prompt → recommendation-only for this invocation; feed the ORIGINAL
|
|
37
|
+
// stdin unchanged. We do NOT analyze stdin (it may not be the prompt) → nothing honest to log.
|
|
38
|
+
return {
|
|
39
|
+
boundary,
|
|
40
|
+
emit: "original",
|
|
41
|
+
approvalStatus: "not-asked",
|
|
42
|
+
applied: false,
|
|
43
|
+
notAvailableReason: boundary.reason,
|
|
44
|
+
record: false
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const recommendation = analyzeBeforeCall(tool, stdinContent);
|
|
48
|
+
if (!recommendation.has_avoidable_context) {
|
|
49
|
+
// Honest no-op: stdin carries no avoidable duplicated context. Feed the original; no activity noise.
|
|
50
|
+
return { boundary, recommendation, emit: "original", approvalStatus: "not-required", applied: false, record: false };
|
|
51
|
+
}
|
|
52
|
+
if (!interactive) {
|
|
53
|
+
// Avoidable context exists, but there is no terminal to ask on → NEVER apply without approval.
|
|
54
|
+
return {
|
|
55
|
+
boundary,
|
|
56
|
+
recommendation,
|
|
57
|
+
emit: "original",
|
|
58
|
+
approvalStatus: "not-asked",
|
|
59
|
+
applied: false,
|
|
60
|
+
notAvailableReason: "no interactive terminal to ask on — apply not offered (fail-closed to the original stdin)",
|
|
61
|
+
record: true
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const choice = decide(recommendation);
|
|
65
|
+
if (choice === "apply") {
|
|
66
|
+
return {
|
|
67
|
+
boundary,
|
|
68
|
+
recommendation,
|
|
69
|
+
emit: "compacted",
|
|
70
|
+
compacted: recommendation.compacted_input,
|
|
71
|
+
approvalStatus: "asked-approved",
|
|
72
|
+
applied: true,
|
|
73
|
+
record: true
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { boundary, recommendation, emit: "original", approvalStatus: "asked-declined", applied: false, record: true };
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=before-call-stdin.js.map
|