@deftai/directive-core 0.99.0 → 0.100.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 (102) hide show
  1. package/dist/authz/actions.d.ts +6 -0
  2. package/dist/authz/actions.js +3 -0
  3. package/dist/authz/classify.js +157 -23
  4. package/dist/authz/decompose-apply.d.ts +67 -0
  5. package/dist/authz/decompose-apply.js +302 -0
  6. package/dist/authz/index.d.ts +3 -1
  7. package/dist/authz/index.js +3 -1
  8. package/dist/authz/store.d.ts +53 -0
  9. package/dist/authz/store.js +219 -1
  10. package/dist/authz/types.d.ts +19 -2
  11. package/dist/authz/types.js +5 -1
  12. package/dist/doctor/checks.d.ts +44 -0
  13. package/dist/doctor/checks.js +428 -2
  14. package/dist/doctor/main.d.ts +6 -7
  15. package/dist/doctor/main.js +72 -79
  16. package/dist/doctor/which.d.ts +20 -1
  17. package/dist/doctor/which.js +67 -1
  18. package/dist/finish-loop/pr-finish-loop.d.ts +13 -2
  19. package/dist/finish-loop/pr-finish-loop.js +116 -5
  20. package/dist/finish-loop/types.d.ts +1 -1
  21. package/dist/hooks/dispatcher.d.ts +9 -5
  22. package/dist/hooks/dispatcher.js +23 -15
  23. package/dist/hooks/readonly.d.ts +6 -3
  24. package/dist/hooks/readonly.js +44 -25
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/init-deposit/agent-hooks.d.ts +20 -0
  28. package/dist/init-deposit/agent-hooks.js +51 -31
  29. package/dist/intake/issue-ingest.js +29 -0
  30. package/dist/lifecycle/completed-consistency.d.ts +60 -0
  31. package/dist/lifecycle/completed-consistency.js +443 -0
  32. package/dist/lifecycle/completed-tracked-on-delivery.d.ts +47 -0
  33. package/dist/lifecycle/completed-tracked-on-delivery.js +415 -0
  34. package/dist/lifecycle/index.d.ts +2 -0
  35. package/dist/lifecycle/index.js +2 -0
  36. package/dist/literal-acceptance/capture.d.ts +52 -0
  37. package/dist/literal-acceptance/capture.js +658 -0
  38. package/dist/literal-acceptance/evaluate.d.ts +47 -0
  39. package/dist/literal-acceptance/evaluate.js +193 -0
  40. package/dist/literal-acceptance/index.d.ts +12 -0
  41. package/dist/literal-acceptance/index.js +12 -0
  42. package/dist/literal-acceptance/run.d.ts +38 -0
  43. package/dist/literal-acceptance/run.js +216 -0
  44. package/dist/literal-acceptance/safety.d.ts +24 -0
  45. package/dist/literal-acceptance/safety.js +251 -0
  46. package/dist/literal-acceptance/types.d.ts +82 -0
  47. package/dist/literal-acceptance/types.js +26 -0
  48. package/dist/policy/ceremony-dial.d.ts +67 -11
  49. package/dist/policy/ceremony-dial.js +174 -23
  50. package/dist/policy/index.d.ts +1 -0
  51. package/dist/policy/index.js +1 -0
  52. package/dist/policy/merge-approval-head.d.ts +129 -0
  53. package/dist/policy/merge-approval-head.js +461 -0
  54. package/dist/policy/require-human-merge.d.ts +5 -0
  55. package/dist/policy/require-human-merge.js +5 -0
  56. package/dist/pr-merge-readiness/ci-gate.d.ts +20 -3
  57. package/dist/pr-merge-readiness/ci-gate.js +65 -5
  58. package/dist/pr-merge-readiness/compute.d.ts +13 -0
  59. package/dist/pr-merge-readiness/compute.js +99 -14
  60. package/dist/pr-merge-readiness/gh.d.ts +49 -0
  61. package/dist/pr-merge-readiness/gh.js +268 -0
  62. package/dist/pr-merge-readiness/index.d.ts +2 -2
  63. package/dist/pr-merge-readiness/index.js +2 -2
  64. package/dist/pr-merge-readiness/test-gh-fixtures.helpers.js +4 -0
  65. package/dist/pr-wait-mergeable/cascade.d.ts +11 -0
  66. package/dist/pr-wait-mergeable/cascade.js +115 -1
  67. package/dist/pr-wait-mergeable/main.d.ts +4 -0
  68. package/dist/pr-wait-mergeable/main.js +4 -0
  69. package/dist/pr-wait-mergeable/types.d.ts +3 -1
  70. package/dist/pr-wait-mergeable/wrappers.d.ts +6 -1
  71. package/dist/pr-wait-mergeable/wrappers.js +7 -1
  72. package/dist/preflight/evaluate.d.ts +15 -1
  73. package/dist/preflight/evaluate.js +33 -2
  74. package/dist/render/framework-commands.js +4 -0
  75. package/dist/scope/acceptance-evidence.d.ts +76 -0
  76. package/dist/scope/acceptance-evidence.js +348 -0
  77. package/dist/scope/coverage-map.d.ts +98 -0
  78. package/dist/scope/coverage-map.js +558 -0
  79. package/dist/scope/decompose.js +125 -11
  80. package/dist/scope/index.d.ts +3 -0
  81. package/dist/scope/index.js +3 -0
  82. package/dist/scope/parent-lineage.d.ts +125 -0
  83. package/dist/scope/parent-lineage.js +626 -0
  84. package/dist/scope/transition.d.ts +8 -0
  85. package/dist/scope/transition.js +61 -2
  86. package/dist/session/active-cli.d.ts +79 -0
  87. package/dist/session/active-cli.js +382 -0
  88. package/dist/session/effort-budget.d.ts +130 -0
  89. package/dist/session/effort-budget.js +334 -0
  90. package/dist/session/index.d.ts +2 -0
  91. package/dist/session/index.js +2 -0
  92. package/dist/session/session-start.d.ts +6 -0
  93. package/dist/session/session-start.js +53 -2
  94. package/dist/session/verify-session-ritual.d.ts +14 -0
  95. package/dist/session/verify-session-ritual.js +33 -0
  96. package/dist/story-ready/evaluate.d.ts +11 -0
  97. package/dist/story-ready/evaluate.js +41 -3
  98. package/dist/triage/actions/candidates-log.d.ts +1 -2
  99. package/dist/triage/actions/candidates-log.js +37 -6
  100. package/dist/xbrief-migrate/migrate-project.d.ts +4 -0
  101. package/dist/xbrief-migrate/migrate-project.js +92 -3
  102. package/package.json +7 -3
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Hard effort-budget detection and bank-the-pass guidance (#3266).
3
+ *
4
+ * When a host/harness declares max-turns or max-cost, agents must bank the
5
+ * *stated* acceptance pass before self-imposed deepening. Detection is
6
+ * advisory at session start (env / flags / host capability pointer #1461);
7
+ * skill text in deft-directive-build and pre-pr owns the behavioral rule.
8
+ *
9
+ * Composes dual-stop (#2442): dual-stop is the failure/budget envelope;
10
+ * this module is the success-side analog (do not gold-plate past the bar
11
+ * when remaining budget cannot also fund a fix for deeper findings).
12
+ *
13
+ * Fail-loud (#1006): when deepening is skipped for budget, summaries must
14
+ * say so — never silent gold-plate or silent skip.
15
+ */
16
+ /** Canonical max-turns env (hard turn budget). */
17
+ export declare const ENV_MAX_TURNS = "DEFT_MAX_TURNS";
18
+ /** Canonical max cost/budget env (opaque cost units from the host). */
19
+ export declare const ENV_MAX_BUDGET = "DEFT_MAX_BUDGET";
20
+ /** Remaining turns when the host updates mid-run. */
21
+ export declare const ENV_REMAINING_TURNS = "DEFT_REMAINING_TURNS";
22
+ /** Remaining cost units when the host updates mid-run. */
23
+ export declare const ENV_REMAINING_BUDGET = "DEFT_REMAINING_BUDGET";
24
+ /**
25
+ * Explicit hard-cap flag when the host has a limit but does not expose a number
26
+ * (truthy → hard-capped with unknown numeric ceiling).
27
+ */
28
+ export declare const ENV_HARD_BUDGET = "DEFT_HARD_BUDGET";
29
+ /**
30
+ * Optional JSON host capability fragment for effort caps (#1461 / #3266).
31
+ * Example: `{"maxTurns":120,"maxBudget":10,"hardBudget":true}`.
32
+ * Production session:start merges this when `effortBudgetSeams.hostDescriptor` is unset.
33
+ */
34
+ export declare const ENV_HOST_EFFORT_BUDGET = "DEFT_HOST_EFFORT_BUDGET";
35
+ /** Additional env keys accepted as max-turns aliases (harness / CLI common). */
36
+ export declare const MAX_TURNS_ENV_ALIASES: readonly ["DEFT_MAX_TURNS", "MAX_TURNS", "AGENT_MAX_TURNS", "DEFT_AGENT_MAX_TURNS", "CURSOR_MAX_TURNS"];
37
+ /** Additional env keys accepted as max-budget/cost aliases. */
38
+ export declare const MAX_BUDGET_ENV_ALIASES: readonly ["DEFT_MAX_BUDGET", "MAX_BUDGET", "AGENT_BUDGET", "DEFT_AGENT_BUDGET", "AGENT_MAX_BUDGET"];
39
+ export type EffortBudgetKind = "none" | "max-turns" | "max-cost" | "both" | "hard-flag";
40
+ /** Whether the session is under a detectable hard cap. */
41
+ export type EffortBudgetPosture = "unbounded" | "hard-capped";
42
+ /**
43
+ * Recommended self-verification depth given budget + stated-AC status.
44
+ * - stated-only: bank the stated bar; do not start self-imposed suites
45
+ * - stated-then-deepen: stated bar first; deepen only with remaining budget
46
+ * - unconstrained-deepen: no hard budget signal; dual-stop still applies
47
+ */
48
+ export type VerificationDepthPolicy = "stated-only" | "stated-then-deepen" | "unconstrained-deepen";
49
+ export interface HardEffortBudget {
50
+ readonly detected: boolean;
51
+ readonly posture: EffortBudgetPosture;
52
+ readonly kind: EffortBudgetKind;
53
+ /** Hard max turns when known; null if only a flag or cost cap is present. */
54
+ readonly maxTurns: number | null;
55
+ /** Hard max cost units when known (opaque host units). */
56
+ readonly maxBudget: number | null;
57
+ /** Remaining turns when the host exposes them; else equals maxTurns. */
58
+ readonly remainingTurns: number | null;
59
+ /** Remaining cost units when exposed; else equals maxBudget. */
60
+ readonly remainingBudget: number | null;
61
+ /** Env keys / host signals that contributed. */
62
+ readonly sources: readonly string[];
63
+ }
64
+ export interface DetectHardEffortBudgetInput {
65
+ readonly environ?: Readonly<Record<string, string | undefined>>;
66
+ /**
67
+ * Optional host capability descriptor fragment (#1461 pointer).
68
+ * Accepts maxTurns / max_turns / maxBudget / max_budget / hardBudget.
69
+ */
70
+ readonly hostDescriptor?: Readonly<Record<string, unknown>> | null;
71
+ }
72
+ export interface RecommendVerificationDepthInput {
73
+ readonly budget: HardEffortBudget;
74
+ /** Whether stated acceptance criteria (issue/xBRIEF AC) are already met. */
75
+ readonly statedAcceptanceMet: boolean;
76
+ /**
77
+ * Reserve for a fix after a deeper finding (turns). Default 3 — if remaining
78
+ * is below this, do not start self-imposed deepening.
79
+ */
80
+ readonly deepenReserveTurns?: number;
81
+ /** Reserve for a fix after a deeper finding (cost units). Default null (ignore). */
82
+ readonly deepenReserveBudget?: number | null;
83
+ }
84
+ /**
85
+ * Parse host effort-budget descriptor from env JSON (#1461 / #3266 production wire).
86
+ * Returns null when unset or unparseable (fail-open).
87
+ */
88
+ export declare function parseHostEffortBudgetEnv(environ?: Readonly<Record<string, string | undefined>>): Readonly<Record<string, unknown>> | null;
89
+ /**
90
+ * Production host-adapter: translate host/harness native signals into a descriptor
91
+ * object for detectHardEffortBudget (#1461 / #3266).
92
+ *
93
+ * Sources (later keys fill gaps only):
94
+ * 1. `DEFT_HOST_EFFORT_BUDGET` JSON (`maxTurns` / `maxBudget` / `hardBudget`)
95
+ * 2. Common harness env aliases already accepted as max-turns / max-budget
96
+ * 3. Explicit hard-budget flag env
97
+ *
98
+ * session:start CLI and resolveEffortBudget always call this so descriptor-only
99
+ * host caps are not lost when the host never set DEFT_HOST_EFFORT_BUDGET.
100
+ */
101
+ export declare function resolveProductionHostEffortDescriptor(environ?: Readonly<Record<string, string | undefined>>): Readonly<Record<string, unknown>>;
102
+ /**
103
+ * Detect a hard turn/cost budget from env and optional host descriptor (#3266).
104
+ * Defaults to unbounded when no signal is present.
105
+ * When `hostDescriptor` is omitted, falls back to `DEFT_HOST_EFFORT_BUDGET` JSON.
106
+ */
107
+ export declare function detectHardEffortBudget(input?: DetectHardEffortBudgetInput): HardEffortBudget;
108
+ /**
109
+ * Recommend self-verification depth: bank stated AC before deepening (#3266).
110
+ */
111
+ export declare function recommendVerificationDepth(input: RecommendVerificationDepthInput): VerificationDepthPolicy;
112
+ /**
113
+ * Operator-visible note when self-imposed deepening is skipped for budget (#1006).
114
+ */
115
+ export declare function formatDeepeningSkippedNote(budget: HardEffortBudget, reason?: string): string;
116
+ /** Session-start / JSON payload shape. */
117
+ export declare function effortBudgetToDict(budget: HardEffortBudget): Record<string, unknown>;
118
+ /**
119
+ * Format operator-facing effort-budget lines for session:start (#3266).
120
+ * Always emits one summary line; adds bank-the-pass guidance when hard-capped.
121
+ */
122
+ export declare function formatEffortBudgetLines(budget: HardEffortBudget): string[];
123
+ /**
124
+ * Detect + format; fail-open for session-start callers.
125
+ */
126
+ export declare function maybeFormatEffortBudgetLines(input?: DetectHardEffortBudgetInput): {
127
+ budget: HardEffortBudget;
128
+ lines: string[];
129
+ };
130
+ //# sourceMappingURL=effort-budget.d.ts.map
@@ -0,0 +1,334 @@
1
+ /**
2
+ * Hard effort-budget detection and bank-the-pass guidance (#3266).
3
+ *
4
+ * When a host/harness declares max-turns or max-cost, agents must bank the
5
+ * *stated* acceptance pass before self-imposed deepening. Detection is
6
+ * advisory at session start (env / flags / host capability pointer #1461);
7
+ * skill text in deft-directive-build and pre-pr owns the behavioral rule.
8
+ *
9
+ * Composes dual-stop (#2442): dual-stop is the failure/budget envelope;
10
+ * this module is the success-side analog (do not gold-plate past the bar
11
+ * when remaining budget cannot also fund a fix for deeper findings).
12
+ *
13
+ * Fail-loud (#1006): when deepening is skipped for budget, summaries must
14
+ * say so — never silent gold-plate or silent skip.
15
+ */
16
+ /** Canonical max-turns env (hard turn budget). */
17
+ export const ENV_MAX_TURNS = "DEFT_MAX_TURNS";
18
+ /** Canonical max cost/budget env (opaque cost units from the host). */
19
+ export const ENV_MAX_BUDGET = "DEFT_MAX_BUDGET";
20
+ /** Remaining turns when the host updates mid-run. */
21
+ export const ENV_REMAINING_TURNS = "DEFT_REMAINING_TURNS";
22
+ /** Remaining cost units when the host updates mid-run. */
23
+ export const ENV_REMAINING_BUDGET = "DEFT_REMAINING_BUDGET";
24
+ /**
25
+ * Explicit hard-cap flag when the host has a limit but does not expose a number
26
+ * (truthy → hard-capped with unknown numeric ceiling).
27
+ */
28
+ export const ENV_HARD_BUDGET = "DEFT_HARD_BUDGET";
29
+ /**
30
+ * Optional JSON host capability fragment for effort caps (#1461 / #3266).
31
+ * Example: `{"maxTurns":120,"maxBudget":10,"hardBudget":true}`.
32
+ * Production session:start merges this when `effortBudgetSeams.hostDescriptor` is unset.
33
+ */
34
+ export const ENV_HOST_EFFORT_BUDGET = "DEFT_HOST_EFFORT_BUDGET";
35
+ /** Additional env keys accepted as max-turns aliases (harness / CLI common). */
36
+ export const MAX_TURNS_ENV_ALIASES = [
37
+ ENV_MAX_TURNS,
38
+ "MAX_TURNS",
39
+ "AGENT_MAX_TURNS",
40
+ "DEFT_AGENT_MAX_TURNS",
41
+ "CURSOR_MAX_TURNS",
42
+ ];
43
+ /** Additional env keys accepted as max-budget/cost aliases. */
44
+ export const MAX_BUDGET_ENV_ALIASES = [
45
+ ENV_MAX_BUDGET,
46
+ "MAX_BUDGET",
47
+ "AGENT_BUDGET",
48
+ "DEFT_AGENT_BUDGET",
49
+ "AGENT_MAX_BUDGET",
50
+ ];
51
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
52
+ function envTruthy(value) {
53
+ return TRUTHY.has((value ?? "").trim().toLowerCase());
54
+ }
55
+ function parsePositiveNumber(raw) {
56
+ if (raw === undefined)
57
+ return null;
58
+ const text = raw.trim();
59
+ if (!text)
60
+ return null;
61
+ const n = Number(text);
62
+ if (!Number.isFinite(n) || n < 0)
63
+ return null;
64
+ return n;
65
+ }
66
+ function firstNumericFromEnv(environ, keys) {
67
+ for (const key of keys) {
68
+ const value = parsePositiveNumber(environ[key]);
69
+ if (value !== null) {
70
+ return { value, source: `env:${key}` };
71
+ }
72
+ }
73
+ return { value: null, source: null };
74
+ }
75
+ function hostNumeric(host, keys) {
76
+ if (!host)
77
+ return { value: null, source: null };
78
+ for (const key of keys) {
79
+ const raw = host[key];
80
+ if (typeof raw === "number" && Number.isFinite(raw) && raw >= 0) {
81
+ return { value: raw, source: `host:${key}` };
82
+ }
83
+ if (typeof raw === "string") {
84
+ const n = parsePositiveNumber(raw);
85
+ if (n !== null)
86
+ return { value: n, source: `host:${key}` };
87
+ }
88
+ }
89
+ return { value: null, source: null };
90
+ }
91
+ function hostTruthy(host, keys) {
92
+ if (!host)
93
+ return null;
94
+ for (const key of keys) {
95
+ const raw = host[key];
96
+ if (raw === true)
97
+ return `host:${key}`;
98
+ if (typeof raw === "string" && envTruthy(raw))
99
+ return `host:${key}`;
100
+ if (typeof raw === "number" && raw === 1)
101
+ return `host:${key}`;
102
+ }
103
+ return null;
104
+ }
105
+ /**
106
+ * Parse host effort-budget descriptor from env JSON (#1461 / #3266 production wire).
107
+ * Returns null when unset or unparseable (fail-open).
108
+ */
109
+ export function parseHostEffortBudgetEnv(environ = process.env) {
110
+ const raw = (environ[ENV_HOST_EFFORT_BUDGET] ?? "").trim();
111
+ if (!raw)
112
+ return null;
113
+ try {
114
+ const parsed = JSON.parse(raw);
115
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
116
+ return parsed;
117
+ }
118
+ }
119
+ catch {
120
+ // fail-open
121
+ }
122
+ return null;
123
+ }
124
+ /**
125
+ * Production host-adapter: translate host/harness native signals into a descriptor
126
+ * object for detectHardEffortBudget (#1461 / #3266).
127
+ *
128
+ * Sources (later keys fill gaps only):
129
+ * 1. `DEFT_HOST_EFFORT_BUDGET` JSON (`maxTurns` / `maxBudget` / `hardBudget`)
130
+ * 2. Common harness env aliases already accepted as max-turns / max-budget
131
+ * 3. Explicit hard-budget flag env
132
+ *
133
+ * session:start CLI and resolveEffortBudget always call this so descriptor-only
134
+ * host caps are not lost when the host never set DEFT_HOST_EFFORT_BUDGET.
135
+ */
136
+ export function resolveProductionHostEffortDescriptor(environ = process.env) {
137
+ const out = {
138
+ ...(parseHostEffortBudgetEnv(environ) ?? {}),
139
+ };
140
+ if (out.maxTurns === undefined) {
141
+ const turns = firstNumericFromEnv(environ, MAX_TURNS_ENV_ALIASES);
142
+ if (turns.value !== null)
143
+ out.maxTurns = turns.value;
144
+ }
145
+ if (out.maxBudget === undefined) {
146
+ const budget = firstNumericFromEnv(environ, MAX_BUDGET_ENV_ALIASES);
147
+ if (budget.value !== null)
148
+ out.maxBudget = budget.value;
149
+ }
150
+ if (out.hardBudget === undefined && envTruthy(environ[ENV_HARD_BUDGET])) {
151
+ out.hardBudget = true;
152
+ }
153
+ // Remaining counters as soft descriptor fields (detection also reads them).
154
+ if (out.remainingTurns === undefined) {
155
+ const rem = firstNumericFromEnv(environ, [ENV_REMAINING_TURNS, "REMAINING_TURNS"]);
156
+ if (rem.value !== null)
157
+ out.remainingTurns = rem.value;
158
+ }
159
+ if (out.remainingBudget === undefined) {
160
+ const rem = firstNumericFromEnv(environ, [ENV_REMAINING_BUDGET, "REMAINING_BUDGET"]);
161
+ if (rem.value !== null)
162
+ out.remainingBudget = rem.value;
163
+ }
164
+ return out;
165
+ }
166
+ /**
167
+ * Detect a hard turn/cost budget from env and optional host descriptor (#3266).
168
+ * Defaults to unbounded when no signal is present.
169
+ * When `hostDescriptor` is omitted, falls back to `DEFT_HOST_EFFORT_BUDGET` JSON.
170
+ */
171
+ export function detectHardEffortBudget(input = {}) {
172
+ const environ = input.environ ?? process.env;
173
+ const host = input.hostDescriptor !== undefined
174
+ ? (input.hostDescriptor ?? null)
175
+ : parseHostEffortBudgetEnv(environ);
176
+ const sources = [];
177
+ if (host !== null && input.hostDescriptor === undefined) {
178
+ sources.push(`env:${ENV_HOST_EFFORT_BUDGET}`);
179
+ }
180
+ const turnsEnv = firstNumericFromEnv(environ, MAX_TURNS_ENV_ALIASES);
181
+ const budgetEnv = firstNumericFromEnv(environ, MAX_BUDGET_ENV_ALIASES);
182
+ const turnsHost = hostNumeric(host, ["maxTurns", "max_turns", "maxTurnLimit"]);
183
+ const budgetHost = hostNumeric(host, ["maxBudget", "max_budget", "maxCost", "max_cost"]);
184
+ // Prefer explicit DEFT_* then first alias; host fills gaps only.
185
+ const maxTurns = turnsEnv.value ?? turnsHost.value;
186
+ const maxBudget = budgetEnv.value ?? budgetHost.value;
187
+ if (turnsEnv.source)
188
+ sources.push(turnsEnv.source);
189
+ else if (turnsHost.source)
190
+ sources.push(turnsHost.source);
191
+ if (budgetEnv.source)
192
+ sources.push(budgetEnv.source);
193
+ else if (budgetHost.source)
194
+ sources.push(budgetHost.source);
195
+ const remainingTurnsEnv = firstNumericFromEnv(environ, [ENV_REMAINING_TURNS, "REMAINING_TURNS"]);
196
+ const remainingBudgetEnv = firstNumericFromEnv(environ, [
197
+ ENV_REMAINING_BUDGET,
198
+ "REMAINING_BUDGET",
199
+ ]);
200
+ if (remainingTurnsEnv.source)
201
+ sources.push(remainingTurnsEnv.source);
202
+ if (remainingBudgetEnv.source)
203
+ sources.push(remainingBudgetEnv.source);
204
+ const remainingTurns = remainingTurnsEnv.value ?? maxTurns;
205
+ const remainingBudget = remainingBudgetEnv.value ?? maxBudget;
206
+ let hardFlag = false;
207
+ if (envTruthy(environ[ENV_HARD_BUDGET])) {
208
+ hardFlag = true;
209
+ sources.push(`env:${ENV_HARD_BUDGET}`);
210
+ }
211
+ else {
212
+ const hostFlag = hostTruthy(host, ["hardBudget", "hard_budget", "hasHardBudget"]);
213
+ if (hostFlag) {
214
+ hardFlag = true;
215
+ sources.push(hostFlag);
216
+ }
217
+ }
218
+ // Remaining-only signals still imply a hard cap even when max is unknown (#3266 Greptile).
219
+ const hasTurns = maxTurns !== null || remainingTurnsEnv.value !== null;
220
+ const hasCost = maxBudget !== null || remainingBudgetEnv.value !== null;
221
+ let kind = "none";
222
+ if (hasTurns && hasCost)
223
+ kind = "both";
224
+ else if (hasTurns)
225
+ kind = "max-turns";
226
+ else if (hasCost)
227
+ kind = "max-cost";
228
+ else if (hardFlag)
229
+ kind = "hard-flag";
230
+ const detected = kind !== "none";
231
+ return {
232
+ detected,
233
+ posture: detected ? "hard-capped" : "unbounded",
234
+ kind,
235
+ maxTurns,
236
+ maxBudget,
237
+ remainingTurns: detected ? remainingTurns : null,
238
+ remainingBudget: detected ? remainingBudget : null,
239
+ sources,
240
+ };
241
+ }
242
+ /**
243
+ * Recommend self-verification depth: bank stated AC before deepening (#3266).
244
+ */
245
+ export function recommendVerificationDepth(input) {
246
+ const { budget, statedAcceptanceMet } = input;
247
+ if (!budget.detected) {
248
+ return "unconstrained-deepen";
249
+ }
250
+ if (!statedAcceptanceMet) {
251
+ // Hard budget + AC open → only work that banks the stated pass.
252
+ return "stated-only";
253
+ }
254
+ const reserveTurns = input.deepenReserveTurns ?? 3;
255
+ const reserveBudget = input.deepenReserveBudget ?? null;
256
+ const turnsOk = budget.remainingTurns === null || budget.remainingTurns >= reserveTurns;
257
+ const costOk = reserveBudget === null ||
258
+ budget.remainingBudget === null ||
259
+ budget.remainingBudget >= reserveBudget;
260
+ // hard-flag with no numbers: after AC is met, allow stated-then-deepen only
261
+ // when remaining is unknown — agent still must fail-loud if it skips.
262
+ if (budget.kind === "hard-flag") {
263
+ return "stated-then-deepen";
264
+ }
265
+ if (turnsOk && costOk) {
266
+ return "stated-then-deepen";
267
+ }
268
+ return "stated-only";
269
+ }
270
+ /**
271
+ * Operator-visible note when self-imposed deepening is skipped for budget (#1006).
272
+ */
273
+ export function formatDeepeningSkippedNote(budget, reason) {
274
+ const detail = reason?.trim() ||
275
+ "remaining budget is insufficient to both deepen verification and fix a found defect";
276
+ const sources = budget.sources.length > 0 ? ` sources=${budget.sources.join(",")}` : "";
277
+ const rem = budget.remainingTurns !== null
278
+ ? ` remaining_turns=${budget.remainingTurns}`
279
+ : budget.remainingBudget !== null
280
+ ? ` remaining_budget=${budget.remainingBudget}`
281
+ : "";
282
+ return (`[deft effort-budget] deepening_skipped=true reason=${detail}` +
283
+ ` posture=${budget.posture} kind=${budget.kind}${rem}${sources} (#3266/#1006)`);
284
+ }
285
+ /** Session-start / JSON payload shape. */
286
+ export function effortBudgetToDict(budget) {
287
+ return {
288
+ detected: budget.detected,
289
+ posture: budget.posture,
290
+ kind: budget.kind,
291
+ max_turns: budget.maxTurns,
292
+ max_budget: budget.maxBudget,
293
+ remaining_turns: budget.remainingTurns,
294
+ remaining_budget: budget.remainingBudget,
295
+ sources: [...budget.sources],
296
+ };
297
+ }
298
+ /**
299
+ * Format operator-facing effort-budget lines for session:start (#3266).
300
+ * Always emits one summary line; adds bank-the-pass guidance when hard-capped.
301
+ */
302
+ export function formatEffortBudgetLines(budget) {
303
+ if (!budget.detected) {
304
+ return [
305
+ "[deft effort-budget] posture=unbounded — no hard max-turns/max-budget signal " +
306
+ `(env ${ENV_MAX_TURNS}/${ENV_MAX_BUDGET} or host descriptor) (#3266)`,
307
+ ];
308
+ }
309
+ const parts = [`[deft effort-budget] posture=${budget.posture}`, `kind=${budget.kind}`];
310
+ if (budget.maxTurns !== null)
311
+ parts.push(`max_turns=${budget.maxTurns}`);
312
+ if (budget.remainingTurns !== null && budget.remainingTurns !== budget.maxTurns) {
313
+ parts.push(`remaining_turns=${budget.remainingTurns}`);
314
+ }
315
+ if (budget.maxBudget !== null)
316
+ parts.push(`max_budget=${budget.maxBudget}`);
317
+ if (budget.remainingBudget !== null && budget.remainingBudget !== budget.maxBudget) {
318
+ parts.push(`remaining_budget=${budget.remainingBudget}`);
319
+ }
320
+ if (budget.sources.length > 0)
321
+ parts.push(`sources=${budget.sources.join(",")}`);
322
+ const lines = [`${parts.join(" ")} (#3266)`];
323
+ lines.push("[deft effort-budget] bank stated acceptance pass before self-imposed deepening; " +
324
+ "scale verification depth with remaining budget; fail-loud when deepening is skipped (#3266/#1006)");
325
+ return lines;
326
+ }
327
+ /**
328
+ * Detect + format; fail-open for session-start callers.
329
+ */
330
+ export function maybeFormatEffortBudgetLines(input = {}) {
331
+ const budget = detectHardEffortBudget(input);
332
+ return { budget, lines: formatEffortBudgetLines(budget) };
333
+ }
334
+ //# sourceMappingURL=effort-budget.js.map
@@ -1,5 +1,7 @@
1
+ export * from "./active-cli.js";
1
2
  export * from "./compact-ritual.js";
2
3
  export * from "./coverage-check-resume-nudge.js";
4
+ export * from "./effort-budget.js";
3
5
  export * from "./git.js";
4
6
  export * from "./json.js";
5
7
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -1,5 +1,7 @@
1
+ export * from "./active-cli.js";
1
2
  export * from "./compact-ritual.js";
2
3
  export * from "./coverage-check-resume-nudge.js";
4
+ export * from "./effort-budget.js";
3
5
  export * from "./git.js";
4
6
  export * from "./json.js";
5
7
  export * from "./openclaw-soft-rebind-deposit.js";
@@ -3,6 +3,7 @@ import { type EnvironmentContext } from "../platform/shell-context.js";
3
3
  import { type CeremonyDialInputs, type CeremonyDialSelection, type ProvisionalCeremonyEstimateHints } from "../policy/ceremony-dial.js";
4
4
  import { type ProbeScmReadinessOptions, type ScmReadinessReport } from "../scm/readiness.js";
5
5
  import { type ResolveUserMdResult } from "../user-config/resolve-user-md.js";
6
+ import { type DetectHardEffortBudgetInput } from "./effort-budget.js";
6
7
  import type { GitRunner } from "./git.js";
7
8
  import { type ReleaseAvailabilityProbeOptions } from "./release-availability.js";
8
9
  import { type RitualState, ritualStatePath } from "./ritual-sentinel.js";
@@ -76,6 +77,11 @@ export interface SessionStartOptions {
76
77
  * Fail-open advisory only — never blocks session:start.
77
78
  */
78
79
  readonly hostContentSurfaceSeams?: HostContentSurfaceSeams;
80
+ /**
81
+ * #3266: hard effort-budget detection seams (env / host descriptor).
82
+ * Fail-open advisory only — never blocks session:start.
83
+ */
84
+ readonly effortBudgetSeams?: DetectHardEffortBudgetInput;
79
85
  readonly probeReleaseAvailability?: (projectRoot: string, options: ReleaseAvailabilityProbeOptions) => {
80
86
  lines: readonly string[];
81
87
  };
@@ -21,6 +21,7 @@ import { resolveUserMdPath } from "../user-config/resolve-user-md.js";
21
21
  import { emitSessionValueReadback } from "../value/readback.js";
22
22
  import { verifyRequiredTools } from "../verify-env/verify-tools.js";
23
23
  import { maybeFormatCoverageCheckResumeNudge } from "./coverage-check-resume-nudge.js";
24
+ import { effortBudgetToDict, maybeFormatEffortBudgetLines, resolveProductionHostEffortDescriptor, } from "./effort-budget.js";
24
25
  import { defaultGitRunner, gitHead, gitIsAncestor, worktreePath } from "./git.js";
25
26
  import { emitSessionStartProcessCost } from "./process-cost.js";
26
27
  import { probeSessionReleaseAvailability, } from "./release-availability.js";
@@ -383,6 +384,38 @@ function resolveHostContentSurface(projectRoot, options, runtimeMode) {
383
384
  };
384
385
  }
385
386
  }
387
+ function resolveEffortBudget(options) {
388
+ try {
389
+ const seams = options.effortBudgetSeams ?? {};
390
+ const environ = seams.environ ?? options.env ?? process.env;
391
+ // Production host adapter (#3266 / #1461): always resolve a descriptor from env
392
+ // (and CLI-forwarded seams) so host-native max-turns/budget signals are not lost.
393
+ const productionHost = resolveProductionHostEffortDescriptor(environ);
394
+ const hostDescriptor = seams.hostDescriptor !== undefined && seams.hostDescriptor !== null
395
+ ? { ...productionHost, ...seams.hostDescriptor }
396
+ : productionHost;
397
+ return maybeFormatEffortBudgetLines({
398
+ environ,
399
+ hostDescriptor,
400
+ });
401
+ }
402
+ catch {
403
+ // best-effort — session start must not abort on effort-budget probe failures (#3266)
404
+ return {
405
+ budget: {
406
+ detected: false,
407
+ posture: "unbounded",
408
+ kind: "none",
409
+ maxTurns: null,
410
+ maxBudget: null,
411
+ remainingTurns: null,
412
+ remainingBudget: null,
413
+ sources: [],
414
+ },
415
+ lines: [],
416
+ };
417
+ }
418
+ }
386
419
  function runReadOnlySessionStart(projectRoot, options, instant, environment) {
387
420
  const lines = [];
388
421
  const resolveUserMd = options.resolveUserMd ?? ((root) => resolveUserMdPath({ projectRoot: root }));
@@ -396,11 +429,14 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
396
429
  const scm = resolveSessionScmReadiness(options, false);
397
430
  // #3162: host content-surface class + managed drift (advisory).
398
431
  const hostSurface = resolveHostContentSurface(projectRoot, options, scm.runtimeMode);
432
+ // #3266: hard effort-budget detection (advisory; bank-the-pass guidance).
433
+ const effortBudget = resolveEffortBudget(options);
399
434
  lines.push(READ_ONLY_ALIGNMENT_MESSAGE);
400
435
  lines.push(userMdLine);
401
436
  lines.push(formatEnvironmentContext(environment));
402
437
  lines.push(...formatScmReadinessLines(scm));
403
438
  lines.push(...hostSurface.lines);
439
+ lines.push(...effortBudget.lines);
404
440
  const resultPayload = {
405
441
  ready: true,
406
442
  exit_code: 0,
@@ -423,6 +459,7 @@ function runReadOnlySessionStart(projectRoot, options, instant, environment) {
423
459
  environment: environmentContextToDict(environment),
424
460
  scm: scmReadinessToDict(scm),
425
461
  host_content_surface: hostContentSurfaceToDict(hostSurface.report),
462
+ effort_budget: effortBudgetToDict(effortBudget.budget),
426
463
  message: READ_ONLY_RESULT_MESSAGE,
427
464
  };
428
465
  return { code: 0, payload: resultPayload, lines };
@@ -467,12 +504,15 @@ function runSessionRearm(projectRoot, options, instant, environment) {
467
504
  const scm = resolveSessionScmReadiness(options, false);
468
505
  // #3162: host content-surface class + managed drift (advisory).
469
506
  const hostSurface = resolveHostContentSurface(projectRoot, options, scm.runtimeMode);
507
+ // #3266: hard effort-budget detection (advisory).
508
+ const effortBudget = resolveEffortBudget(options);
470
509
  const lines = [
471
510
  READ_ONLY_ALIGNMENT_MESSAGE,
472
511
  userMdLine,
473
512
  formatEnvironmentContext(environment),
474
513
  ...formatScmReadinessLines(scm),
475
514
  ...hostSurface.lines,
515
+ ...effortBudget.lines,
476
516
  REARM_SKIPPED_FAT_PATH_MESSAGE,
477
517
  ];
478
518
  // Light branch-policy disclosure (local only) so re-arm still surfaces policy state.
@@ -665,6 +705,7 @@ function runSessionRearm(projectRoot, options, instant, environment) {
665
705
  environment: environmentContextToDict(environment),
666
706
  scm: scmReadinessToDict(scm),
667
707
  host_content_surface: hostContentSurfaceToDict(hostSurface.report),
708
+ effort_budget: effortBudgetToDict(effortBudget.budget),
668
709
  message: code === 0 ? "session ritual re-armed" : "session ritual re-arm failed",
669
710
  },
670
711
  lines,
@@ -749,9 +790,10 @@ export function runSessionStart(projectRoot, options = {}) {
749
790
  // #3214 / #3156: select ritual (ceremony) depth before building deferral maps.
750
791
  // Rapid/minimal auto-defer informational cold steps only; mutation readiness
751
792
  // (doctor, cache_fresh, agent_hooks, verify_tools) stays constant.
752
- // Two-stage + provisional intake (#3214 design note / #1581 ordering): fill
793
+ // Two-stage + provisional intake (#3214 / #3263 / #1581 ordering): fill
753
794
  // missing size/tier/shape from env/verb/files/deposit BEFORE resolve — never
754
- // block on plan-item effort (post-planning only). Cold incomplete rapid.
795
+ // block on plan-item effort (post-planning only). Cold incomplete size is
796
+ // tier-conditional (#3263): mid/low → standard; frontier/unknown → rapid.
755
797
  const { inputs: resolvedDialInputs, provisional: provisionalDial } = resolveSessionCeremonyDialInputs(projectRoot, options.ceremonyDialInputs, {
756
798
  ...options.ceremonyDialHints,
757
799
  env: options.env,
@@ -837,6 +879,14 @@ export function runSessionStart(projectRoot, options = {}) {
837
879
  name: "host_content_surface",
838
880
  duration_ms: elapsedMs(hostSurfaceStepStarted),
839
881
  });
882
+ // #3266: hard effort-budget detection (advisory; bank-the-pass guidance).
883
+ const effortBudgetStepStarted = performance.now();
884
+ const effortBudget = resolveEffortBudget(options);
885
+ lines.push(...effortBudget.lines);
886
+ stepTimings.push({
887
+ name: "effort_budget",
888
+ duration_ms: elapsedMs(effortBudgetStepStarted),
889
+ });
840
890
  if (!quickSteps.branch_policy) {
841
891
  const stepStarted = performance.now();
842
892
  const result = resolvePolicy(projectRoot);
@@ -1119,6 +1169,7 @@ export function runSessionStart(projectRoot, options = {}) {
1119
1169
  environment: environmentContextToDict(environment),
1120
1170
  scm: scmReadinessToDict(scm),
1121
1171
  host_content_surface: hostContentSurfaceToDict(hostSurface.report),
1172
+ effort_budget: effortBudgetToDict(effortBudget.budget),
1122
1173
  message: code === 0 ? "session ritual recorded" : "session ritual failed",
1123
1174
  };
1124
1175
  // #2994: local process-cost event (best-effort; never blocks ceremony).
@@ -1,3 +1,4 @@
1
+ import { type ActiveCliCheckResult, type ActiveCliCheckSeams } from "./active-cli.js";
1
2
  import { type GitRunner } from "./git.js";
2
3
  import { type DirectivePosture } from "./posture.js";
3
4
  import { type GatedStepName, type SessionCeremonyTier } from "./session-start.js";
@@ -39,6 +40,19 @@ export interface VerifySessionRitualOptions {
39
40
  readonly handoffText?: string | null;
40
41
  /** Re-run selected gated prerequisites even when their recorded step is green. */
41
42
  readonly forceGatedSteps?: readonly GatedStepName[];
43
+ /**
44
+ * #3233: engine version the post-upgrade / ritual check must match on the
45
+ * shell-active CLI. When null/omitted, still fail closed on multi-prefix
46
+ * version skew (active older than another PATH candidate).
47
+ */
48
+ readonly targetEngineVersion?: string | null;
49
+ /** Injectable active-CLI seams for hermetic tests (#3233). */
50
+ readonly activeCliSeams?: ActiveCliCheckSeams;
51
+ /**
52
+ * Override the active-CLI probe entirely (tests). Defaults to
53
+ * {@link checkActiveCliAgainstTarget}.
54
+ */
55
+ readonly checkActiveCli?: (targetVersion: string | null, seams?: ActiveCliCheckSeams) => ActiveCliCheckResult;
42
56
  }
43
57
  export interface InspectSessionRitualOptions {
44
58
  readonly tier?: "quick" | "gated";