@pentoshi/clai 3.8.21 → 3.8.22

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.
@@ -37,6 +37,7 @@ import { fsWrite, isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools
37
37
  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";
38
38
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
39
39
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
40
+ import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
40
41
  import { renderPlanForTerminal, planContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
41
42
  import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteObservationTask, isRemoteReconToolCall, isRuntimeObservationTask, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
42
43
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
@@ -793,6 +794,15 @@ export async function runAgentTurn(prompt, options = {}) {
793
794
  const hasHistory = (options.history?.length ?? 0) > 0;
794
795
  const buildLike = buildLikeTurn;
795
796
  const pentestLike = looksLikePentestTask(prompt, options.history);
797
+ /** Coding/build sessions never hard-pause mid-turn for the progress governor. */
798
+ let codingSession = codingSessionFromContext({
799
+ buildLike,
800
+ planKind: activePlan?.kind,
801
+ });
802
+ let pauseMode = progressPauseMode({
803
+ codingSession,
804
+ autoConfirm: Boolean(options.autoConfirm),
805
+ });
796
806
  const continueExistingOutcome = /^(?:continue|resume|proceed|keep\s+going|finish|next)\b/i.test(prompt.trim()) ||
797
807
  Boolean(activePlan && !isPlanTerminal(activePlan));
798
808
  const outcomeState = await openOutcomeState({
@@ -1767,27 +1777,52 @@ export async function runAgentTurn(prompt, options = {}) {
1767
1777
  ((call.name === "shell.exec" || call.name === "shell.start") &&
1768
1778
  isPackageInstallCommand(String(call.args.command ?? "")));
1769
1779
  }
1770
- const governed = governProgress(governorState, "activity", {
1771
- evidenceDelta: newEvidence.length,
1772
- hypothesisDelta,
1773
- repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1774
- policy: {
1775
- resourceEnvelope: Math.max(12, maxSteps),
1776
- emergencyCeiling: Math.max(70, maxSteps * 3),
1777
- reflectionAfterNoDelta: 3,
1778
- pauseAfterNoDelta: 6,
1779
- repetitionThreshold: 0.8,
1780
- },
1781
- });
1782
- governorState = governed.state;
1783
- if (governed.recommendation === "reflect") {
1784
- deferredPostToolMessages.push({
1785
- role: "system",
1786
- content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.`,
1780
+ // Protocol-repair placeholders are not live work — never let them
1781
+ // accumulate into a mid-turn pause (they used to look like failed tools).
1782
+ if (!isProtocolPlaceholderOutput(result.output)) {
1783
+ const governed = governProgress(governorState, "activity", {
1784
+ evidenceDelta: newEvidence.length,
1785
+ hypothesisDelta,
1786
+ repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
1787
+ policy: {
1788
+ resourceEnvelope: Math.max(12, maxSteps),
1789
+ // Coding builds get a much higher ceiling; never use the tight
1790
+ // default that stopped multi-file scaffolds after a handful of steps.
1791
+ emergencyCeiling: codingSession
1792
+ ? Math.max(200, maxSteps * 5)
1793
+ : Math.max(70, maxSteps * 3),
1794
+ reflectionAfterNoDelta: codingSession ? 5 : 3,
1795
+ pauseAfterNoDelta: codingSession ? 24 : 6,
1796
+ repetitionThreshold: 0.8,
1797
+ },
1787
1798
  });
1788
- }
1789
- else if (governed.recommendation === "paused_budget") {
1790
- governorPauseReason = governed.reason;
1799
+ governorState = governed.state;
1800
+ if (governed.recommendation === "reflect") {
1801
+ deferredPostToolMessages.push({
1802
+ role: "system",
1803
+ content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.` +
1804
+ (codingSession
1805
+ ? " Keep working — coding builds do not stop for a continue prompt."
1806
+ : ""),
1807
+ });
1808
+ }
1809
+ else if (governed.recommendation === "paused_budget") {
1810
+ if (pauseMode === "never") {
1811
+ // Soft reset so we do not re-trip every subsequent tool.
1812
+ governorState = {
1813
+ ...governed.state,
1814
+ consecutiveNoDelta: 0,
1815
+ };
1816
+ deferredPostToolMessages.push({
1817
+ role: "system",
1818
+ content: `PROGRESS GOVERNOR (soft, coding build): ${governed.reason}. ` +
1819
+ "Change approach if stuck, but keep implementing — do not stop for user confirmation.",
1820
+ });
1821
+ }
1822
+ else {
1823
+ governorPauseReason = governed.reason;
1824
+ }
1825
+ }
1791
1826
  }
1792
1827
  await saveOutcomeState(outcomeState);
1793
1828
  loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode);
@@ -2033,14 +2068,53 @@ export async function runAgentTurn(prompt, options = {}) {
2033
2068
  // advances when the previous iteration actually executed a tool.
2034
2069
  step = productiveSteps;
2035
2070
  if (governorPauseReason) {
2036
- const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2037
- outcomeState.outcome.status = "paused_budget";
2038
- await saveOutcomeState(outcomeState);
2039
- moveTurn("paused_budget", governorPauseReason);
2040
- lastAnswer = richSummary;
2041
- return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2042
- .filter((criterion) => criterion.required && criterion.status !== "proven")
2043
- .map((criterion) => criterion.statement), governorPauseReason);
2071
+ // Non-coding: always ask continue/stop. Coding never sets this reason
2072
+ // (pauseMode === "never"), but guard anyway.
2073
+ if (pauseMode === "never") {
2074
+ governorPauseReason = undefined;
2075
+ governorState = {
2076
+ ...governorState,
2077
+ consecutiveNoDelta: 0,
2078
+ };
2079
+ }
2080
+ else {
2081
+ const confirmPort = options.confirm;
2082
+ let keepGoing = false;
2083
+ if (confirmPort?.confirmContinue) {
2084
+ try {
2085
+ keepGoing = await confirmPort.confirmContinue(productiveSteps, governorPauseReason);
2086
+ }
2087
+ catch {
2088
+ keepGoing = false;
2089
+ }
2090
+ finally {
2091
+ restoreInteractiveStdin();
2092
+ }
2093
+ }
2094
+ if (keepGoing) {
2095
+ writeNotice("info", "continuing after progress pause", chalk.dim(` ℹ continuing after pause (${governorPauseReason}) — change approach if stuck\n`));
2096
+ deferredPostToolMessages.push({
2097
+ role: "system",
2098
+ content: `User chose CONTINUE after progress pause (${governorPauseReason}). ` +
2099
+ "Do not repeat the same failing step; change approach and produce new evidence.",
2100
+ });
2101
+ governorPauseReason = undefined;
2102
+ governorState = {
2103
+ ...governorState,
2104
+ consecutiveNoDelta: 0,
2105
+ };
2106
+ }
2107
+ else {
2108
+ const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
2109
+ outcomeState.outcome.status = "paused_budget";
2110
+ await saveOutcomeState(outcomeState);
2111
+ moveTurn("paused_budget", governorPauseReason);
2112
+ lastAnswer = richSummary;
2113
+ return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
2114
+ .filter((criterion) => criterion.required && criterion.status !== "proven")
2115
+ .map((criterion) => criterion.statement), governorPauseReason);
2116
+ }
2117
+ }
2044
2118
  }
2045
2119
  options.signal?.throwIfAborted();
2046
2120
  let call;
@@ -3216,6 +3290,19 @@ export async function runAgentTurn(prompt, options = {}) {
3216
3290
  else {
3217
3291
  session.planApproved.value = true;
3218
3292
  }
3293
+ // Re-derive pause policy from the new plan kind (coding builds
3294
+ // must not hard-pause even if the free-text prompt was generic).
3295
+ const kindArg = typeof res.call.args.kind === "string"
3296
+ ? res.call.args.kind
3297
+ : undefined;
3298
+ codingSession = codingSessionFromContext({
3299
+ buildLike,
3300
+ planKind: kindArg,
3301
+ });
3302
+ pauseMode = progressPauseMode({
3303
+ codingSession,
3304
+ autoConfirm: Boolean(options.autoConfirm),
3305
+ });
3219
3306
  }
3220
3307
  // User Esc/Ctrl+C only — never cancel siblings because a delete failed
3221
3308
  // or a confirm was declined; the model must see every tool result.
@@ -3335,6 +3422,8 @@ export async function runAgentTurn(prompt, options = {}) {
3335
3422
  }
3336
3423
  }
3337
3424
  }
3425
+ // Hard iteration ceiling (hundreds of steps) — rare. Mid-turn governor
3426
+ // pauses already confirm for non-coding; coding never hard-pauses there.
3338
3427
  const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
3339
3428
  lastAnswer = richSummary;
3340
3429
  outcomeState.outcome.status = "paused_budget";