@pentoshi/clai 3.11.7 → 3.11.8

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.
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Pure finalize-time gate: pick the recovery nudge that must replace a final
3
+ * answer, or nothing when the turn may finalize. No I/O, no budget mutation.
4
+ */
5
+ import { type RecoveryAction, type RecoveryBudgets } from "./must-continue.js";
6
+ /** Task fields the gate reads. */
7
+ export interface FinalizeGatePlanTask {
8
+ id: string;
9
+ title: string;
10
+ state: string;
11
+ responderOwned?: boolean | undefined;
12
+ }
13
+ /** Plain-data snapshot of the live plan, resolved once by the caller. */
14
+ export interface FinalizeGatePlan {
15
+ kind: string;
16
+ hasVerifiedRuntime: boolean;
17
+ tasks: readonly FinalizeGatePlanTask[];
18
+ }
19
+ /** Everything the finalize cascade reads, as data. */
20
+ export interface FinalizeGateInput {
21
+ cleaned: string;
22
+ recovery: RecoveryBudgets;
23
+ toolsAttached: boolean;
24
+ productiveSteps: number;
25
+ planApproved: boolean;
26
+ planHasOpenWork: boolean;
27
+ activePlanExists: boolean;
28
+ wantsAction: boolean;
29
+ narratedAction: boolean;
30
+ narratedWebAction: boolean;
31
+ isPlanMode: boolean;
32
+ buildLikeTurn: boolean;
33
+ pentestLikeTurn: boolean;
34
+ buildLike: boolean;
35
+ pentestLike: boolean;
36
+ pentestSession: boolean;
37
+ informationalQuery: boolean;
38
+ idleOrSocialPrompt: boolean;
39
+ freshWebSearchRequired: boolean;
40
+ freshnessGuardText: string;
41
+ sawFreshWebSearch: boolean;
42
+ sawPlanCreateOk: boolean;
43
+ sawFeatureImplWrite: boolean;
44
+ sawScaffoldOk: boolean;
45
+ sawLocalAppMaterialWork: boolean;
46
+ sawServerStart: boolean;
47
+ sawServerTail: boolean;
48
+ sawLocalHttpProbe: boolean;
49
+ sawFailedLocalHttpProbe: boolean;
50
+ sawActivePentestTest: boolean;
51
+ sawSuccessfulMutation: boolean;
52
+ featureAppAsk: boolean;
53
+ projectRoot: string | undefined;
54
+ plan: FinalizeGatePlan | undefined;
55
+ deferResponderReport: boolean;
56
+ }
57
+ export declare function chooseFinalizeRecovery(input: FinalizeGateInput): RecoveryAction | undefined;
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Pure finalize-time gate: pick the recovery nudge that must replace a final
3
+ * answer, or nothing when the turn may finalize. No I/O, no budget mutation.
4
+ */
5
+ import { budgetRemaining, freestyleClaimsAppReady, looksLikeShallowPentestReport, recoveryForErrorDiagnosis, recoveryForFailedProbe, recoveryForFreshness, recoveryForMissingFeature, recoveryForMissingPlan, recoveryForNarration, recoveryForPrematureComplete, recoveryForRuntimeVerify, recoveryForShallowPentest, } from "./must-continue.js";
6
+ import { looksLikeErrorDiagnosisWithFixIntent, looksLikePlanNarration, } from "./tool-call-parser.js";
7
+ export function chooseFinalizeRecovery(input) {
8
+ const { cleaned, recovery, toolsAttached, productiveSteps, plan } = input;
9
+ const planNarrated = (input.buildLikeTurn || input.pentestLikeTurn) &&
10
+ !input.activePlanExists &&
11
+ looksLikePlanNarration(cleaned);
12
+ const errorFixNarration = !input.sawSuccessfulMutation &&
13
+ looksLikeErrorDiagnosisWithFixIntent(cleaned);
14
+ const shouldRetryBeforeFinalizing = productiveSteps === 0 ||
15
+ planNarrated ||
16
+ ((input.narratedAction || input.narratedWebAction) &&
17
+ !input.informationalQuery) ||
18
+ (input.planApproved &&
19
+ input.planHasOpenWork &&
20
+ (input.narratedAction || errorFixNarration)) ||
21
+ (input.planApproved && errorFixNarration) ||
22
+ (input.buildLikeTurn && errorFixNarration);
23
+ if (input.wantsAction &&
24
+ cleaned.trim().length > 0 &&
25
+ shouldRetryBeforeFinalizing) {
26
+ let action;
27
+ if (errorFixNarration && budgetRemaining(recovery, "errorFix")) {
28
+ action = recoveryForErrorDiagnosis(toolsAttached);
29
+ }
30
+ else if (budgetRemaining(recovery, "actionIntent") &&
31
+ input.planHasOpenWork &&
32
+ input.planApproved) {
33
+ action = recoveryForNarration(toolsAttached, "plan_open");
34
+ }
35
+ else if (budgetRemaining(recovery, "actionIntent") &&
36
+ input.pentestLikeTurn) {
37
+ action = recoveryForNarration(toolsAttached, "pentest");
38
+ }
39
+ else if (budgetRemaining(recovery, "actionIntent") &&
40
+ (input.freshWebSearchRequired || input.narratedWebAction)) {
41
+ action = recoveryForNarration(toolsAttached, "web");
42
+ }
43
+ else if (budgetRemaining(recovery, "actionIntent") &&
44
+ input.buildLikeTurn &&
45
+ (planNarrated || productiveSteps > 0)) {
46
+ action = recoveryForNarration(toolsAttached, "build_plan_prose");
47
+ }
48
+ else if (budgetRemaining(recovery, "actionIntent") &&
49
+ input.buildLikeTurn) {
50
+ action = recoveryForNarration(toolsAttached, "build");
51
+ }
52
+ else if (budgetRemaining(recovery, "actionIntent")) {
53
+ action = recoveryForNarration(toolsAttached, "generic");
54
+ }
55
+ if (action)
56
+ return action;
57
+ }
58
+ if (input.freshWebSearchRequired &&
59
+ !input.sawFreshWebSearch &&
60
+ budgetRemaining(recovery, "freshnessUsed")) {
61
+ return recoveryForFreshness(input.freshnessGuardText +
62
+ (toolsAttached
63
+ ? " Call the web_search tool now."
64
+ : " Reply with ONLY a fenced ```tool block for web.search now."));
65
+ }
66
+ if (input.isPlanMode &&
67
+ !input.informationalQuery &&
68
+ !input.idleOrSocialPrompt &&
69
+ budgetRemaining(recovery, "forcePlan")) {
70
+ if (!plan && !input.sawPlanCreateOk) {
71
+ return recoveryForMissingPlan(toolsAttached);
72
+ }
73
+ }
74
+ if (input.buildLike &&
75
+ !input.pentestLike &&
76
+ !input.pentestSession &&
77
+ input.planApproved &&
78
+ input.featureAppAsk &&
79
+ !input.sawFeatureImplWrite &&
80
+ (input.sawScaffoldOk || input.sawLocalAppMaterialWork) &&
81
+ productiveSteps > 0 &&
82
+ budgetRemaining(recovery, "featureImpl")) {
83
+ return recoveryForMissingFeature(input.projectRoot);
84
+ }
85
+ if (input.buildLike &&
86
+ !input.pentestLike &&
87
+ !input.pentestSession &&
88
+ budgetRemaining(recovery, "runtimeVerify") &&
89
+ (!input.featureAppAsk || input.sawFeatureImplWrite)) {
90
+ const planRuntimeOk = Boolean(plan && plan.hasVerifiedRuntime);
91
+ const sessionRuntimeOk = input.sawServerStart &&
92
+ (input.sawServerTail || input.sawLocalHttpProbe || planRuntimeOk);
93
+ if (!planRuntimeOk && !sessionRuntimeOk) {
94
+ const codingPlanFinished = Boolean(plan &&
95
+ input.planApproved &&
96
+ plan.kind !== "pentest" &&
97
+ plan.tasks.length > 0 &&
98
+ plan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
99
+ const freestyleLocalAppDone = !input.planApproved &&
100
+ input.sawLocalAppMaterialWork &&
101
+ productiveSteps > 0 &&
102
+ freestyleClaimsAppReady(cleaned) &&
103
+ (input.projectRoot !== undefined ||
104
+ /\b(?:npm|pnpm|yarn|bun)\s+run\s+dev\b/i.test(cleaned) ||
105
+ /\bopen\s+http:\/\/localhost\b/i.test(cleaned));
106
+ if (codingPlanFinished || freestyleLocalAppDone) {
107
+ return recoveryForRuntimeVerify(input.projectRoot);
108
+ }
109
+ }
110
+ }
111
+ if (input.buildLike &&
112
+ !input.pentestLike &&
113
+ !input.pentestSession &&
114
+ input.sawFailedLocalHttpProbe &&
115
+ !input.sawLocalHttpProbe &&
116
+ budgetRemaining(recovery, "failedProbe") &&
117
+ cleaned.trim().length > 0) {
118
+ return recoveryForFailedProbe();
119
+ }
120
+ if ((input.pentestLike || input.pentestSession) &&
121
+ budgetRemaining(recovery, "shallowPentest") &&
122
+ looksLikeShallowPentestReport(cleaned, {
123
+ productiveSteps,
124
+ sawActiveTest: input.sawActivePentestTest,
125
+ })) {
126
+ return recoveryForShallowPentest();
127
+ }
128
+ if (input.planApproved && budgetRemaining(recovery, "prematureComplete")) {
129
+ const unfinished = plan?.tasks.filter((task) => !task.responderOwned &&
130
+ (task.state === "pending" || task.state === "in_progress"));
131
+ if (plan &&
132
+ unfinished &&
133
+ unfinished.length > 0 &&
134
+ !input.deferResponderReport) {
135
+ const next = unfinished[0];
136
+ return recoveryForPrematureComplete({
137
+ unfinished,
138
+ next,
139
+ pentest: plan.kind === "pentest" || input.pentestSession,
140
+ errorFix: errorFixNarration,
141
+ });
142
+ }
143
+ }
144
+ return undefined;
145
+ }
146
+ //# sourceMappingURL=finalize-gate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finalize-gate.js","sourceRoot":"","sources":["../../src/agent/finalize-gate.ts"],"names":[],"mappings":"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"}
@@ -64,7 +64,7 @@ import { resolveRequestBudget } from "./request-budget.js";
64
64
  import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
65
65
  import { stat } from "node:fs/promises";
66
66
  import { isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
67
- import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, looksLikeErrorDiagnosisWithFixIntent, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
67
+ import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
68
68
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
69
69
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
70
70
  import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
@@ -75,7 +75,8 @@ import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLoc
75
75
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
76
76
  import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
77
77
  import { detectPackageManager } from "./workspace-orient.js";
78
- import { budgetRemaining, consumeBudget, createRecoveryBudgets, freestyleClaimsAppReady, looksLikeShallowPentestReport, recoveryForErrorDiagnosis, recoveryForFailedProbe, recoveryForFreshness, recoveryForMissingFeature, recoveryForMissingPlan, recoveryForNarration, recoveryForPrematureComplete, recoveryForRuntimeVerify, recoveryForShallowPentest, } from "./must-continue.js";
78
+ import { budgetRemaining, consumeBudget, createRecoveryBudgets, } from "./must-continue.js";
79
+ import { chooseFinalizeRecovery } from "./finalize-gate.js";
79
80
  import { outOfScopeToolMessage, scopeContextMessage } from "./scope-context.js";
80
81
  import { EngagementPolicyEngine, actionFromUrl, engagementActionsForToolCall, evaluateEngagementAction, } from "../safety/engagement-policy.js";
81
82
  import { patchPlanMeta } from "../store/plan.js";
@@ -543,6 +544,9 @@ export async function runAgentTurn(prompt, options = {}) {
543
544
  // Request, project, workspace, recovery, scope, and plan state are appended
544
545
  // later as system-marked turns so a changing byte cannot invalidate the
545
546
  // constitution (and, on Anthropic, the native tool schemas before it).
547
+ // The red-team methodology block is ~940 tokens on every request. Attach it
548
+ // only when this turn is actually a remote-security engagement.
549
+ const pentestPromptTurn = pentestLikeTurn || activePlan?.kind === "pentest";
546
550
  const buildStableSystemContent = (native) => {
547
551
  const reliability = getReliabilityPolicy();
548
552
  const visionAvailable = modelSupportsVision(provider, model);
@@ -554,6 +558,7 @@ export async function runAgentTurn(prompt, options = {}) {
554
558
  imageView: visionAvailable,
555
559
  // E6: slim native constitution when API tool schemas are attached.
556
560
  ...(native ? { slimNative: reliability.slimNativePrompt } : {}),
561
+ ...(pentestPromptTurn ? { pentest: true } : {}),
557
562
  });
558
563
  };
559
564
  const systemSections = [renderRequestEnvironmentContext()];
@@ -4022,182 +4027,61 @@ export async function runAgentTurn(prompt, options = {}) {
4022
4027
  "If analysis is incomplete, call only the bounded evidence tool needed now. If each result has been analyzed and is satisfactory, you MUST call job.read with its jobId or exact notificationId before giving a final response. job.read does not require an active plan; do not create or update a plan merely to acknowledge a result."));
4023
4028
  continue;
4024
4029
  }
4025
- const planNarrated = (buildLikeTurn || pentestLikeTurn) &&
4026
- !activePlan &&
4027
- looksLikePlanNarration(cleaned);
4028
- // Only force "diagnosed but not fixed" when the model is still
4029
- // narrating a fix without having applied one this turn. Post-fix
4030
- // summaries ("I've fixed…", build passed) must never re-enter.
4031
- const errorFixNarration = !sawSuccessfulMutation &&
4032
- looksLikeErrorDiagnosisWithFixIntent(cleaned);
4033
- const shouldRetryBeforeFinalizing = productiveSteps === 0 ||
4034
- planNarrated ||
4035
- ((narratedAction || narratedWebAction) && !informationalQuery) ||
4036
- (session.planApproved.value &&
4037
- planHasOpenWorkNow &&
4038
- (narratedAction || errorFixNarration)) ||
4039
- // errorFix only when no mutation yet (gate is in errorFixNarration)
4040
- (session.planApproved.value && errorFixNarration) ||
4041
- (buildLikeTurn && errorFixNarration);
4042
- if (wantsAction &&
4043
- cleaned.trim().length > 0 &&
4044
- shouldRetryBeforeFinalizing) {
4045
- let action;
4046
- if (errorFixNarration && budgetRemaining(recovery, "errorFix")) {
4047
- action = recoveryForErrorDiagnosis(toolsAttached);
4048
- }
4049
- else if (budgetRemaining(recovery, "actionIntent") &&
4050
- planHasOpenWorkNow &&
4051
- session.planApproved.value) {
4052
- action = recoveryForNarration(toolsAttached, "plan_open");
4053
- }
4054
- else if (budgetRemaining(recovery, "actionIntent") &&
4055
- pentestLikeTurn) {
4056
- action = recoveryForNarration(toolsAttached, "pentest");
4057
- }
4058
- else if (budgetRemaining(recovery, "actionIntent") &&
4059
- (freshWebSearchRequired || narratedWebAction)) {
4060
- action = recoveryForNarration(toolsAttached, "web");
4061
- }
4062
- else if (budgetRemaining(recovery, "actionIntent") &&
4063
- buildLikeTurn &&
4064
- (planNarrated || productiveSteps > 0)) {
4065
- action = recoveryForNarration(toolsAttached, "build_plan_prose");
4066
- }
4067
- else if (budgetRemaining(recovery, "actionIntent") &&
4068
- buildLikeTurn) {
4069
- action = recoveryForNarration(toolsAttached, "build");
4070
- }
4071
- else if (budgetRemaining(recovery, "actionIntent")) {
4072
- action = recoveryForNarration(toolsAttached, "generic");
4073
- }
4074
- if (action) {
4075
- consumeBudget(recovery, action.budgetKey);
4076
- commitAssistantRetry(assistantText.visible);
4077
- messages.push(recoveryUserMessage(action.message));
4078
- continue;
4079
- }
4080
- }
4081
- if (freshWebSearchRequired &&
4082
- !sawFreshWebSearch &&
4083
- budgetRemaining(recovery, "freshnessUsed")) {
4084
- const action = recoveryForFreshness(freshnessGuardMessage() +
4085
- (toolsAttached
4086
- ? " Call the web_search tool now."
4087
- : " Reply with ONLY a fenced ```tool block for web.search now."));
4088
- consumeBudget(recovery, action.budgetKey);
4089
- commitAssistantRetry(assistantText.visible);
4090
- messages.push(recoveryUserMessage(action.message));
4091
- continue;
4092
- }
4093
- if (isPlanMode &&
4094
- !informationalQuery &&
4095
- !idleOrSocialPrompt &&
4096
- budgetRemaining(recovery, "forcePlan")) {
4097
- const planAtEnd = await loadPlan(session.sessionId).catch(() => undefined);
4098
- if (!planAtEnd && !sawPlanCreateOk) {
4099
- const action = recoveryForMissingPlan(toolsAttached);
4100
- consumeBudget(recovery, action.budgetKey);
4101
- commitAssistantRetry(assistantText.visible);
4102
- messages.push(recoveryUserMessage(action.message));
4103
- continue;
4104
- }
4105
- }
4106
- if (buildLike &&
4107
- !pentestLike &&
4108
- !pentestSession &&
4109
- session.planApproved.value &&
4110
- featureAppAsk &&
4111
- !sawFeatureImplWrite &&
4112
- (sawScaffoldOk || sawLocalAppMaterialWork) &&
4113
- productiveSteps > 0 &&
4114
- budgetRemaining(recovery, "featureImpl")) {
4115
- const action = recoveryForMissingFeature(getActiveProjectRoot());
4116
- consumeBudget(recovery, action.budgetKey);
4117
- commitAssistantRetry(assistantText.visible);
4118
- messages.push(recoveryUserMessage(action.message));
4119
- continue;
4120
- }
4121
- if (buildLike &&
4122
- !pentestLike &&
4123
- !pentestSession &&
4124
- budgetRemaining(recovery, "runtimeVerify") &&
4125
- (!featureAppAsk || sawFeatureImplWrite)) {
4126
- const runtimePlan = await loadPlan(session.sessionId).catch(() => undefined);
4127
- // Durable plan evidence or multi-signal proof this turn is enough
4128
- const planRuntimeOk = Boolean(runtimePlan && planHasVerifiedRuntime(runtimePlan));
4129
- const sessionRuntimeOk = sawServerStart &&
4130
- (sawServerTail || sawLocalHttpProbe || planRuntimeOk);
4131
- if (!planRuntimeOk && !sessionRuntimeOk) {
4132
- const codingPlanFinished = Boolean(runtimePlan &&
4133
- session.planApproved.value &&
4134
- runtimePlan.kind !== "pentest" &&
4135
- runtimePlan.tasks.length > 0 &&
4136
- runtimePlan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
4137
- const freestyleLocalAppDone = !session.planApproved.value &&
4138
- sawLocalAppMaterialWork &&
4139
- productiveSteps > 0 &&
4140
- freestyleClaimsAppReady(cleaned) &&
4141
- (getActiveProjectRoot() !== undefined ||
4142
- /\b(?:npm|pnpm|yarn|bun)\s+run\s+dev\b/i.test(cleaned) ||
4143
- /\bopen\s+http:\/\/localhost\b/i.test(cleaned));
4144
- if (codingPlanFinished || freestyleLocalAppDone) {
4145
- const action = recoveryForRuntimeVerify(getActiveProjectRoot());
4146
- consumeBudget(recovery, action.budgetKey);
4147
- commitAssistantRetry(assistantText.visible);
4148
- messages.push(recoveryUserMessage(action.message));
4149
- continue;
4030
+ const deferResponderReport = session.planApproved.value &&
4031
+ budgetRemaining(recovery, "prematureComplete")
4032
+ ? shouldYieldForDeclaredResponderDependency(livePlanAtCompletion, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId)
4033
+ : false;
4034
+ const finalizeRecovery = chooseFinalizeRecovery({
4035
+ cleaned,
4036
+ recovery,
4037
+ toolsAttached,
4038
+ productiveSteps,
4039
+ planApproved: session.planApproved.value,
4040
+ planHasOpenWork: planHasOpenWorkNow,
4041
+ activePlanExists: Boolean(activePlan),
4042
+ wantsAction,
4043
+ narratedAction,
4044
+ narratedWebAction,
4045
+ isPlanMode,
4046
+ buildLikeTurn,
4047
+ pentestLikeTurn,
4048
+ buildLike,
4049
+ pentestLike,
4050
+ pentestSession,
4051
+ informationalQuery,
4052
+ idleOrSocialPrompt,
4053
+ freshWebSearchRequired,
4054
+ freshnessGuardText: freshWebSearchRequired
4055
+ ? freshnessGuardMessage()
4056
+ : "",
4057
+ sawFreshWebSearch,
4058
+ sawPlanCreateOk,
4059
+ sawFeatureImplWrite,
4060
+ sawScaffoldOk,
4061
+ sawLocalAppMaterialWork,
4062
+ sawServerStart,
4063
+ sawServerTail,
4064
+ sawLocalHttpProbe,
4065
+ sawFailedLocalHttpProbe,
4066
+ sawActivePentestTest,
4067
+ sawSuccessfulMutation,
4068
+ featureAppAsk,
4069
+ projectRoot: getActiveProjectRoot(),
4070
+ plan: livePlanAtCompletion
4071
+ ? {
4072
+ kind: livePlanAtCompletion.kind,
4073
+ hasVerifiedRuntime: planHasVerifiedRuntime(livePlanAtCompletion),
4074
+ tasks: livePlanAtCompletion.tasks,
4150
4075
  }
4151
- }
4152
- }
4153
- if (buildLike &&
4154
- !pentestLike &&
4155
- !pentestSession &&
4156
- sawFailedLocalHttpProbe &&
4157
- !sawLocalHttpProbe &&
4158
- budgetRemaining(recovery, "failedProbe") &&
4159
- cleaned.trim().length > 0) {
4160
- const action = recoveryForFailedProbe();
4161
- consumeBudget(recovery, action.budgetKey);
4162
- commitAssistantRetry(assistantText.visible);
4163
- messages.push(recoveryUserMessage(action.message));
4164
- continue;
4165
- }
4166
- if ((pentestLike || pentestSession) &&
4167
- budgetRemaining(recovery, "shallowPentest") &&
4168
- looksLikeShallowPentestReport(cleaned, {
4169
- productiveSteps,
4170
- sawActiveTest: sawActivePentestTest,
4171
- })) {
4172
- const action = recoveryForShallowPentest();
4173
- consumeBudget(recovery, action.budgetKey);
4076
+ : undefined,
4077
+ deferResponderReport,
4078
+ });
4079
+ if (finalizeRecovery) {
4080
+ consumeBudget(recovery, finalizeRecovery.budgetKey);
4174
4081
  commitAssistantRetry(assistantText.visible);
4175
- messages.push(recoveryUserMessage(action.message));
4082
+ messages.push(recoveryUserMessage(finalizeRecovery.message));
4176
4083
  continue;
4177
4084
  }
4178
- if (session.planApproved.value &&
4179
- budgetRemaining(recovery, "prematureComplete")) {
4180
- const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
4181
- const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
4182
- (task.state === "pending" || task.state === "in_progress"));
4183
- const deferReport = shouldYieldForDeclaredResponderDependency(livePlan, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId);
4184
- if (livePlan &&
4185
- unfinished &&
4186
- unfinished.length > 0 &&
4187
- !deferReport) {
4188
- const next = unfinished[0];
4189
- const action = recoveryForPrematureComplete({
4190
- unfinished,
4191
- next,
4192
- pentest: livePlan.kind === "pentest" || pentestSession,
4193
- errorFix: errorFixNarration,
4194
- });
4195
- consumeBudget(recovery, action.budgetKey);
4196
- commitAssistantRetry(assistantText.visible);
4197
- messages.push(recoveryUserMessage(action.message));
4198
- continue;
4199
- }
4200
- }
4201
4085
  let outcomeStatus = "succeeded";
4202
4086
  const remainingCriteria = [];
4203
4087
  if (session.planApproved.value) {