@pentoshi/clai 3.11.31 → 3.11.33

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.
@@ -63,14 +63,14 @@ import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-att
63
63
  import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
64
64
  import { stat } from "node:fs/promises";
65
65
  import { isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
66
- 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";
66
+ import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
67
67
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
68
68
  import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
69
69
  import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
70
70
  import { renderPlanForTerminal, planContextMessage, upsertPlanContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
71
71
  import { readDeclaredParentTaskId, resolveResponderParent, isExplicitResponderDelegation, delegationTaskTitle, } from "./responder-parent.js";
72
72
  import { readTaskUpdateArgs, distinctAdvancingTaskIds, isSimultaneousTaskAdvance, batchUpdateSignature, buildMultiUpdateReminder, buildMultiOpenRejection, multiUpdateToast, multiOpenToast, openingTaskIds, } from "./task-sync.js";
73
- 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";
73
+ import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteReconToolCall, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
74
74
  import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
75
75
  import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
76
76
  import { detectPackageManager } from "./workspace-orient.js";
@@ -442,19 +442,12 @@ export async function runAgentTurn(prompt, options = {}) {
442
442
  // brand-new plan (the exact failure where "what do u know till now"
443
443
  // triggered explore→plan and created an unrelated "Enhance clai" plan).
444
444
  const informationalQuery = looksLikeInformationalQuery(prompt);
445
- // Greetings / thanks / short acks must never force tools, plans, or
446
- // freshness retries — a false "act don't narrate" path burned tokens on
447
- // web.search recovery loops after a simple "hi".
445
+ // Greetings / thanks / short acks must never force tools or plans.
448
446
  const idleOrSocialPrompt = looksLikeIdleOrSocialPrompt(prompt);
449
447
  suppressOutcomeDiagnostics =
450
448
  informationalQuery ||
451
449
  idleOrSocialPrompt ||
452
450
  looksLikeContinueOrResumePrompt(prompt);
453
- const freshWebSearchRequired = !buildLikeTurn &&
454
- !pentestLikeTurn &&
455
- !idleOrSocialPrompt &&
456
- toolNames.includes("web.search") &&
457
- requiresFreshWebSearch(prompt);
458
451
  let provider = initialProvider;
459
452
  await ensureProviderConfigured(provider);
460
453
  let model = initialModel;
@@ -608,9 +601,6 @@ export async function runAgentTurn(prompt, options = {}) {
608
601
  orientInput.candidateProject = candidate;
609
602
  systemSections.push(buildWorkspaceOrientation(orientInput));
610
603
  }
611
- if (freshWebSearchRequired) {
612
- systemSections.push(freshnessGuardMessage());
613
- }
614
604
  // The live plan is mutable state: it is injected once as a keyed request
615
605
  // suffix (upsertPlanContextMessage) instead of being frozen into the stable
616
606
  // system prefix, so the model never sees a stale and a fresh plan together.
@@ -719,7 +709,6 @@ export async function runAgentTurn(prompt, options = {}) {
719
709
  content.startsWith("ACTIVE PROJECT ROOT:") ||
720
710
  content.startsWith("USER DESTINATION:") ||
721
711
  content.startsWith("WORKSPACE STATUS") ||
722
- content.startsWith("Freshness guard for this turn:") ||
723
712
  content.includes("MODE") ||
724
713
  content.includes("OUTCOME"),
725
714
  }));
@@ -939,7 +928,6 @@ export async function runAgentTurn(prompt, options = {}) {
939
928
  // (e.g. malformed extra/missing braces that are NOT simple truncation). We
940
929
  // retry instead of leaking the raw block as the final answer.
941
930
  let malformedFenceRetries = 0;
942
- let sawFreshWebSearch = false;
943
931
  const recovery = createRecoveryBudgets();
944
932
  let sawServerStart = false;
945
933
  let sawPlanCreateOk = false;
@@ -1011,70 +999,6 @@ export async function runAgentTurn(prompt, options = {}) {
1011
999
  remoteWorkVerified: planHasVerifiedRemoteWork(plan),
1012
1000
  });
1013
1001
  };
1014
- const reconcileOpenTaskBeforeFinalizing = async () => {
1015
- let plan = await loadPlan(session.sessionId).catch(() => undefined);
1016
- const open = plan?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
1017
- if (!plan || !open)
1018
- return plan;
1019
- const gate = completionGateForTask(plan, open.id);
1020
- if (!gate.ok)
1021
- return plan;
1022
- const reconciledTaskIds = [open.id];
1023
- const working = plan;
1024
- markTask(working, open.id, "done", "Completion reconciled from verified task evidence.");
1025
- while (true) {
1026
- const observation = readyPlanTasks(working).find((task) => isRuntimeObservationTask(task.title) ||
1027
- (working.kind === "pentest" && isRemoteObservationTask(task.title)));
1028
- if (!observation)
1029
- break;
1030
- const observationGate = completionGateForTask(working, observation.id);
1031
- if (!observationGate.ok)
1032
- break;
1033
- markTask(working, observation.id, "done", working.kind === "pentest"
1034
- ? "Satisfied by verified remote evidence from the preceding task."
1035
- : "Satisfied by the verified runtime evidence from the preceding task.");
1036
- reconciledTaskIds.push(observation.id);
1037
- }
1038
- if (working.status === "draft" || working.status === "approved") {
1039
- working.status = "in_progress";
1040
- }
1041
- if (isPlanTerminal(working)) {
1042
- working.status = isPlanSuccessful(working) ? "completed" : "abandoned";
1043
- }
1044
- // Replay the same reconciliation as a reducer so a concurrent
1045
- // responder settlement is preserved instead of overwritten.
1046
- const reconciledNotes = new Map(reconciledTaskIds.map((id) => [
1047
- id,
1048
- working.tasks.find((task) => task.id === id)?.note,
1049
- ]));
1050
- const reconcileResult = await mutatePlan(session.sessionId, (draft) => {
1051
- let changed = false;
1052
- for (const [id, note] of reconciledNotes) {
1053
- const task = draft.tasks.find((candidate) => candidate.id === id);
1054
- if (!task || task.state === "done")
1055
- continue;
1056
- task.state = "done";
1057
- if (note !== undefined)
1058
- task.note = note;
1059
- changed = true;
1060
- }
1061
- if (draft.status === "draft" || draft.status === "approved") {
1062
- draft.status = "in_progress";
1063
- changed = true;
1064
- }
1065
- if (isPlanTerminal(draft)) {
1066
- draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
1067
- changed = true;
1068
- }
1069
- return changed;
1070
- }).catch(() => undefined);
1071
- if (reconcileResult?.ok && reconcileResult.plan)
1072
- plan = reconcileResult.plan;
1073
- writePlanUpdate(plan, renderPlanForTerminal(plan) + "\n");
1074
- writeNotice("info", `reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence`, chalk.dim(` ℹ reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence — no duplicate verification\n`));
1075
- taskWorkLedger = null;
1076
- return plan;
1077
- };
1078
1002
  async function persistProjectRootOnPlan(root) {
1079
1003
  const pm = detectPackageManager(root);
1080
1004
  // Metadata patches go through the transactional boundary so a
@@ -1806,9 +1730,6 @@ export async function runAgentTurn(prompt, options = {}) {
1806
1730
  };
1807
1731
  }
1808
1732
  }
1809
- if (call.name === "web.search") {
1810
- sawFreshWebSearch = true;
1811
- }
1812
1733
  if (!alreadyPrintedIds.has(toolEventId)) {
1813
1734
  const toolCallLine = chalk.cyan(` ▶ ${call.name}`) + chalk.gray(` ${formatToolArgs(call)}`);
1814
1735
  writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
@@ -3274,9 +3195,7 @@ export async function runAgentTurn(prompt, options = {}) {
3274
3195
  ...(toolsAttached
3275
3196
  ? {
3276
3197
  tools: turnTools,
3277
- toolChoice: freshWebSearchRequired && !sawFreshWebSearch
3278
- ? { type: "function", name: "web.search" }
3279
- : "auto",
3198
+ toolChoice: "auto",
3280
3199
  parallelToolCalls: true,
3281
3200
  // P2-3: emit tool cards as soon as the function name arrives.
3282
3201
  onToolCallDelta: (delta) => {
@@ -3709,12 +3628,15 @@ export async function runAgentTurn(prompt, options = {}) {
3709
3628
  }
3710
3629
  if (nativeToolCalls.length) {
3711
3630
  const first = nativeToolCalls[0];
3712
- if (first.args?._parseError) {
3713
- call = undefined;
3714
- }
3715
- else {
3716
- call = normalizeToolCall({ name: first.name, args: first.args });
3717
- }
3631
+ call = first.args?._parseError
3632
+ ? {
3633
+ name: first.name || "unknown",
3634
+ args: {
3635
+ __nativeParseError: true,
3636
+ _raw: first.args._raw,
3637
+ },
3638
+ }
3639
+ : normalizeToolCall({ name: first.name, args: first.args });
3718
3640
  }
3719
3641
  else {
3720
3642
  call = parseToolCall(assistantText.visible, {
@@ -3821,19 +3743,15 @@ export async function runAgentTurn(prompt, options = {}) {
3821
3743
  retryWithoutThinking = true;
3822
3744
  commitAssistantRetry(assistantText.visible);
3823
3745
  // Keep nudges SHORT — cheap models lose the key instruction in long text.
3824
- const buildNudge = freshWebSearchRequired && !sawFreshWebSearch
3746
+ const buildNudge = isPlanMode && !activePlan
3825
3747
  ? toolsAttached
3826
- ? "No visible output. This is current or scheduled information: call web.search now. Do NOT answer from memory."
3827
- : "No visible output. This is current or scheduled information: emit exactly one valid ```tool block for web.search now. Do NOT answer from memory or hide the tool call in <think> tags."
3828
- : isPlanMode && !activePlan
3829
- ? toolsAttached
3830
- ? "No visible output. In plan mode: gather context or call plan.create when ready (do not only describe the plan)."
3831
- : "No visible output. In plan mode: emit a ```tool block for research/recon or plan.create. " +
3832
- "Do NOT hide tool calls in <think> tags — put them in the visible response."
3833
- : toolsAttached
3834
- ? "No visible output. " + toolNudge(true)
3835
- : "No visible output. Emit a ```tool block or give your final answer. " +
3836
- "Do NOT hide tool calls in <think> tags — put them in the visible response.";
3748
+ ? "No visible output. In plan mode: gather context or call plan.create when ready (do not only describe the plan)."
3749
+ : "No visible output. In plan mode: emit a ```tool block for research/recon or plan.create. " +
3750
+ "Do NOT hide tool calls in <think> tags — put them in the visible response."
3751
+ : toolsAttached
3752
+ ? "No visible output. " + toolNudge(true)
3753
+ : "No visible output. Emit a ```tool block or give your final answer. " +
3754
+ "Do NOT hide tool calls in <think> tags — put them in the visible response.";
3837
3755
  messages.push(recoveryUserMessage(buildNudge));
3838
3756
  continue;
3839
3757
  }
@@ -4029,15 +3947,12 @@ export async function runAgentTurn(prompt, options = {}) {
4029
3947
  const cleaned = collapseRepeatedText(stripSentinelTokens(canonicalAssistantVisible));
4030
3948
  const narratedAction = looksLikeActionNarration(cleaned);
4031
3949
  const narratedWebAction = looksLikeWebActionNarration(cleaned);
4032
- const reconciledPlanAtCompletion = await reconcileOpenTaskBeforeFinalizing();
4033
- const livePlanAtCompletion = reconciledPlanAtCompletion ??
4034
- (await loadPlan(session.sessionId).catch(() => undefined));
3950
+ const livePlanAtCompletion = await loadPlan(session.sessionId).catch(() => undefined);
4035
3951
  const planStatusAtCompletion = livePlanAtCompletion?.status ?? activePlan?.status;
4036
3952
  const completedPlanDuringThisTurn = activePlan?.status !== "completed" &&
4037
3953
  planStatusAtCompletion === "completed";
4038
3954
  const planHasOpenWorkNow = planHasOpenWork(planStatusAtCompletion);
4039
- const userExpectsWork = freshWebSearchRequired ||
4040
- (planHasOpenWorkNow && session.planApproved.value) ||
3955
+ const userExpectsWork = (planHasOpenWorkNow && session.planApproved.value) ||
4041
3956
  (!informationalQuery &&
4042
3957
  !idleOrSocialPrompt &&
4043
3958
  (buildLikeTurn || pentestLikeTurn));
@@ -4105,11 +4020,6 @@ export async function runAgentTurn(prompt, options = {}) {
4105
4020
  pentestSession,
4106
4021
  informationalQuery,
4107
4022
  idleOrSocialPrompt,
4108
- freshWebSearchRequired,
4109
- freshnessGuardText: freshWebSearchRequired
4110
- ? freshnessGuardMessage()
4111
- : "",
4112
- sawFreshWebSearch,
4113
4023
  sawPlanCreateOk,
4114
4024
  sawFeatureImplWrite,
4115
4025
  sawScaffoldOk,