@pentoshi/clai 3.8.46 → 3.8.47

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 (96) hide show
  1. package/dist/agent/compaction-summary.js +1 -1
  2. package/dist/agent/compaction-summary.js.map +1 -1
  3. package/dist/agent/plan-tool.js +110 -14
  4. package/dist/agent/plan-tool.js.map +1 -1
  5. package/dist/agent/reliability-policy.d.ts +4 -3
  6. package/dist/agent/reliability-policy.js +4 -3
  7. package/dist/agent/reliability-policy.js.map +1 -1
  8. package/dist/agent/responder-context.d.ts +10 -0
  9. package/dist/agent/responder-context.js +50 -0
  10. package/dist/agent/responder-context.js.map +1 -0
  11. package/dist/agent/runner.js +160 -12
  12. package/dist/agent/runner.js.map +1 -1
  13. package/dist/agent/task-plan.d.ts +4 -0
  14. package/dist/agent/task-plan.js.map +1 -1
  15. package/dist/app/adapters/current-jobs-adapter.js +7 -0
  16. package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
  17. package/dist/app/controllers/job-controller.d.ts +9 -6
  18. package/dist/app/controllers/job-controller.js +21 -0
  19. package/dist/app/controllers/job-controller.js.map +1 -1
  20. package/dist/app/controllers/session-controller.d.ts +14 -33
  21. package/dist/app/controllers/session-controller.js +71 -132
  22. package/dist/app/controllers/session-controller.js.map +1 -1
  23. package/dist/app/controllers/session-prompt-queue.d.ts +34 -0
  24. package/dist/app/controllers/session-prompt-queue.js +141 -0
  25. package/dist/app/controllers/session-prompt-queue.js.map +1 -0
  26. package/dist/app/controllers/session-responder.d.ts +49 -0
  27. package/dist/app/controllers/session-responder.js +311 -0
  28. package/dist/app/controllers/session-responder.js.map +1 -0
  29. package/dist/app/ports/jobs-port.d.ts +10 -7
  30. package/dist/prompts/embedded.js +1 -1
  31. package/dist/prompts/embedded.js.map +1 -1
  32. package/dist/prompts/system.agent.md +14 -9
  33. package/dist/repl.js +24 -14
  34. package/dist/repl.js.map +1 -1
  35. package/dist/safety/engagement-policy.js +39 -7
  36. package/dist/safety/engagement-policy.js.map +1 -1
  37. package/dist/store/config.js +1 -1
  38. package/dist/store/config.js.map +1 -1
  39. package/dist/store/plan.d.ts +11 -0
  40. package/dist/store/plan.js +66 -3
  41. package/dist/store/plan.js.map +1 -1
  42. package/dist/tools/definitions.js +35 -4
  43. package/dist/tools/definitions.js.map +1 -1
  44. package/dist/tools/jobs.d.ts +72 -3
  45. package/dist/tools/jobs.js +743 -121
  46. package/dist/tools/jobs.js.map +1 -1
  47. package/dist/tools/pentest-workflows.js +80 -4
  48. package/dist/tools/pentest-workflows.js.map +1 -1
  49. package/dist/tools/registry.js +60 -19
  50. package/dist/tools/registry.js.map +1 -1
  51. package/dist/tools/tool-types.d.ts +5 -0
  52. package/dist/tools/wordlists.js +74 -87
  53. package/dist/tools/wordlists.js.map +1 -1
  54. package/dist/tui-v2/actions/action-id.js +1 -1
  55. package/dist/tui-v2/actions/action-id.js.map +1 -1
  56. package/dist/tui-v2/actions/keymap.js +1 -1
  57. package/dist/tui-v2/actions/keymap.js.map +1 -1
  58. package/dist/tui-v2/app/App.js +82 -12
  59. package/dist/tui-v2/app/App.js.map +1 -1
  60. package/dist/tui-v2/app/commands/session-commands.js +0 -5
  61. package/dist/tui-v2/app/commands/session-commands.js.map +1 -1
  62. package/dist/tui-v2/bootstrap/composition-root.js +2 -0
  63. package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
  64. package/dist/tui-v2/components/jobs/jobs-panel.d.ts +12 -0
  65. package/dist/tui-v2/components/jobs/jobs-panel.js +184 -14
  66. package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
  67. package/dist/tui-v2/components/plan/plan-view.js +7 -3
  68. package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
  69. package/dist/tui-v2/components/status/status-line.js +2 -3
  70. package/dist/tui-v2/components/status/status-line.js.map +1 -1
  71. package/dist/tui-v2/components/toast/toast-host.js +16 -1
  72. package/dist/tui-v2/components/toast/toast-host.js.map +1 -1
  73. package/dist/tui-v2/components/transcript/tool-card.d.ts +3 -0
  74. package/dist/tui-v2/components/transcript/tool-card.js +12 -2
  75. package/dist/tui-v2/components/transcript/tool-card.js.map +1 -1
  76. package/dist/tui-v2/components/transcript/transcript-row.js +1 -1
  77. package/dist/tui-v2/components/transcript/transcript-row.js.map +1 -1
  78. package/dist/tui-v2/composer/composer-editor.d.ts +6 -0
  79. package/dist/tui-v2/composer/composer-editor.js +10 -17
  80. package/dist/tui-v2/composer/composer-editor.js.map +1 -1
  81. package/dist/tui-v2/composer/composer-image-paste.d.ts +1 -0
  82. package/dist/tui-v2/composer/composer-image-paste.js +22 -1
  83. package/dist/tui-v2/composer/composer-image-paste.js.map +1 -1
  84. package/dist/tui-v2/state/transcript-store.d.ts +4 -0
  85. package/dist/tui-v2/state/transcript-store.js +13 -0
  86. package/dist/tui-v2/state/transcript-store.js.map +1 -1
  87. package/dist/ui/intro-card.js +3 -3
  88. package/dist/ui/intro-card.js.map +1 -1
  89. package/dist/ui/mentions.d.ts +11 -13
  90. package/dist/ui/mentions.js +67 -26
  91. package/dist/ui/mentions.js.map +1 -1
  92. package/dist/ui/plan-pane.js +6 -2
  93. package/dist/ui/plan-pane.js.map +1 -1
  94. package/dist/version.generated.d.ts +2 -2
  95. package/dist/version.generated.js +2 -2
  96. package/package.json +1 -1
@@ -6,11 +6,34 @@ import { syntheticToolCallId, isTextOnlyModel, fromWireName, } from "../llm/tool
6
6
  import { sanitizeAssistantText } from "../ui/ansi-box.js";
7
7
  import { randomUUID } from "node:crypto";
8
8
  import { jobManager } from "../tools/jobs.js";
9
+ import { responderContextMessage, upsertResponderContextMessage, } from "./responder-context.js";
9
10
  import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, toolNudge, } from "../prompts/index.js";
10
11
  import { getConfig } from "../store/config.js";
11
12
  import { beginSessionWorkspace, getActiveSessionWorkspace, } from "../store/session-workspace.js";
12
13
  import { groqInputTokenBudget } from "../llm/groq.js";
13
14
  import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
15
+ /**
16
+ * Scope/engagement classification runs on every tool call and parses
17
+ * model-supplied arguments (URLs, hosts, commands). A malformed argument must
18
+ * never throw out of classification and abort the whole turn — degrade to
19
+ * "no target / no action" so the normal permission path still applies.
20
+ */
21
+ function safeScopeTargetForToolCall(call) {
22
+ try {
23
+ return scopeTargetForToolCall(call);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function safeEngagementActionForToolCall(call) {
30
+ try {
31
+ return engagementActionForToolCall(call);
32
+ }
33
+ catch {
34
+ return undefined;
35
+ }
36
+ }
14
37
  import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
15
38
  import { getToolDefinitions, getCompactToolDefinitions, PLAN_TOOL_NAMES, } from "../tools/definitions.js";
16
39
  import { appendAssistantWithTools, ensureUniqueToolCallIds, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
@@ -32,7 +55,7 @@ import { isScratchOnlyWrite } from "./scratch-write.js";
32
55
  import { COMPACTION_SYSTEM_PROMPT, } from "./compaction-summary.js";
33
56
  import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
34
57
  import { LoopGuard } from "./loop-guard.js";
35
- import { loadPlan, savePlan, markTask, readyPlanTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
58
+ import { loadPlan, savePlan, markTask, appendPlanTask, readyPlanTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
36
59
  import { fsWrite, isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
37
60
  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
61
  import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
@@ -574,6 +597,20 @@ export async function runAgentTurn(prompt, options = {}) {
574
597
  userMessage,
575
598
  ];
576
599
  liveMessages = messages;
600
+ const responderWakeTurn = prompt.startsWith("Responder completion ready (");
601
+ const refreshResponderInbox = () => {
602
+ const pending = responderWakeTurn
603
+ ? jobManager
604
+ .getPendingNotifications(session.sessionId)
605
+ .filter((notification) => notification.responder && Boolean(notification.deliveredAt))
606
+ : [];
607
+ upsertResponderContextMessage(messages, responderContextMessage({
608
+ running: jobManager
609
+ .getRunningJobs(session.sessionId)
610
+ .filter((job) => job.responder),
611
+ pending,
612
+ }));
613
+ };
577
614
  /** Assigned after session flags exist — see below. */
578
615
  let refreshSessionState = () => undefined;
579
616
  /** Model-only nudge — never shown as a YOU bubble / WARN in the chat UI. */
@@ -698,7 +735,7 @@ export async function runAgentTurn(prompt, options = {}) {
698
735
  };
699
736
  const reconcileOpenTaskBeforeFinalizing = async () => {
700
737
  const plan = await loadPlan(session.sessionId).catch(() => undefined);
701
- const open = plan?.tasks.find((task) => task.state === "in_progress");
738
+ const open = plan?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
702
739
  if (!plan || !open)
703
740
  return plan;
704
741
  const gate = completionGateForTask(plan, open.id);
@@ -756,9 +793,9 @@ export async function runAgentTurn(prompt, options = {}) {
756
793
  const root = getActiveProjectRoot() ?? p?.meta?.projectRoot;
757
794
  const pm = p?.meta?.packageManager ??
758
795
  (root ? detectPackageManager(root) : undefined);
759
- const open = p?.tasks.find((t) => t.state === "in_progress");
796
+ const open = p?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
760
797
  const pending = p?.tasks
761
- .filter((t) => t.state === "pending")
798
+ .filter((task) => task.state === "pending" && !task.responderOwned)
762
799
  .map((t) => `[${t.id}] ${t.title}`);
763
800
  const done = p?.tasks
764
801
  .filter((t) => t.state === "done" || t.state === "skipped")
@@ -967,7 +1004,9 @@ export async function runAgentTurn(prompt, options = {}) {
967
1004
  contextOutput: reason,
968
1005
  };
969
1006
  }
970
- if (call.name === "plan.create" || call.name === "task.update") {
1007
+ if (call.name === "plan.create" ||
1008
+ call.name === "task.add" ||
1009
+ call.name === "task.update") {
971
1010
  // Evidence gate: refuse done until at least one successful work tool
972
1011
  // ran under this task (model must see results and be satisfied).
973
1012
  if (call.name === "task.update") {
@@ -1177,8 +1216,9 @@ export async function runAgentTurn(prompt, options = {}) {
1177
1216
  if (session.planApproved.value) {
1178
1217
  const livePlanForGate = await loadPlan(session.sessionId).catch(() => undefined);
1179
1218
  if (livePlanForGate) {
1180
- const unfinished = livePlanForGate.tasks.some((t) => t.state === "pending" || t.state === "in_progress");
1181
- const inProgress = livePlanForGate.tasks.find((t) => t.state === "in_progress");
1219
+ const unfinished = livePlanForGate.tasks.some((task) => !task.responderOwned &&
1220
+ (task.state === "pending" || task.state === "in_progress"));
1221
+ const inProgress = livePlanForGate.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
1182
1222
  if (unfinished && !inProgress) {
1183
1223
  // tool.check / fs.list preflight: allow without auto-opening a task
1184
1224
  // (auto-start on preflight made models skip task.update and confused scope).
@@ -1256,9 +1296,9 @@ export async function runAgentTurn(prompt, options = {}) {
1256
1296
  writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
1257
1297
  alreadyPrintedIds.add(toolEventId);
1258
1298
  }
1259
- const scopeTarget = scopeTargetForToolCall(call);
1299
+ const scopeTarget = safeScopeTargetForToolCall(call);
1260
1300
  const engagementAction = pentestSession || isPentestToolCall(call) || Boolean(scope)
1261
- ? engagementActionForToolCall(call)
1301
+ ? safeEngagementActionForToolCall(call)
1262
1302
  : undefined;
1263
1303
  const engagementDecision = engagementAction
1264
1304
  ? evaluateEngagementAction(scope, engagementAction)
@@ -1391,7 +1431,7 @@ export async function runAgentTurn(prompt, options = {}) {
1391
1431
  }
1392
1432
  parentSignal.throwIfAborted();
1393
1433
  const planAtDispatch = await loadPlan(session.sessionId).catch(() => undefined);
1394
- dispatchedTaskId = planAtDispatch?.tasks.find((task) => task.state === "in_progress")?.id;
1434
+ dispatchedTaskId = planAtDispatch?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned)?.id;
1395
1435
  if (!dispatchedTaskId && planAtDispatch?.kind === "pentest") {
1396
1436
  const candidate = pickPendingTaskForToolCall(readyPlanTasks(planAtDispatch), call, planAtDispatch.tasks.map((task) => task.title));
1397
1437
  dispatchedTaskId = candidate?.id;
@@ -1506,6 +1546,12 @@ export async function runAgentTurn(prompt, options = {}) {
1506
1546
  confirmed: true,
1507
1547
  userPrompt: prompt,
1508
1548
  sessionId: session.sessionId,
1549
+ ...(dispatchedTaskId ? { taskId: dispatchedTaskId } : {}),
1550
+ wakeOnCompletion: true,
1551
+ monitor: {
1552
+ toolName: call.name,
1553
+ toolEventId,
1554
+ },
1509
1555
  ...(engagementAction && scope
1510
1556
  ? {
1511
1557
  engagementAuthorization: {
@@ -1739,6 +1785,102 @@ export async function runAgentTurn(prompt, options = {}) {
1739
1785
  writeNotice("info", `project root → ${fromScaffold}`, chalk.dim(` ℹ project root set to ${fromScaffold}\n`));
1740
1786
  }
1741
1787
  }
1788
+ if (result.backgroundJob) {
1789
+ const durableJob = jobManager.getJob(result.backgroundJob.id);
1790
+ // Responder linkage is opt-in: only jobs launched with responder:true
1791
+ // become fire-and-forget plan subtasks that auto-wake on completion.
1792
+ // Plain background jobs stay pollable (shell.jobs/shell.tail) as before.
1793
+ if (durableJob?.responder) {
1794
+ const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
1795
+ let linkedTaskId = dispatchedTaskId;
1796
+ let linkedParentTaskId;
1797
+ let responderTaskId;
1798
+ if (durableJob && livePlan) {
1799
+ const existing = livePlan.tasks.find((task) => task.jobId === durableJob.id);
1800
+ const parentTaskId = dispatchedTaskId && livePlan.tasks.some((task) => task.id === dispatchedTaskId)
1801
+ ? dispatchedTaskId
1802
+ : undefined;
1803
+ const terminalState = durableJob.status === "exited"
1804
+ ? "done"
1805
+ : durableJob.status === "failed" ||
1806
+ durableJob.status === "killed" ||
1807
+ durableJob.status === "lost"
1808
+ ? "failed"
1809
+ : "in_progress";
1810
+ const note = `job=${durableJob.id} pid=${durableJob.pid ?? "?"} status=${durableJob.status} ` +
1811
+ `artifact=${durableJob.stdoutArtifact}`;
1812
+ const responderTask = existing ?? appendPlanTask(livePlan, {
1813
+ title: `Responder · ${durableJob.name ?? durableJob.commandDisplay.slice(0, 96)}`,
1814
+ state: terminalState,
1815
+ note,
1816
+ dependencies: [],
1817
+ resourceLocks: [],
1818
+ parentTaskId,
1819
+ jobId: durableJob.id,
1820
+ processId: durableJob.pid,
1821
+ responderOwned: true,
1822
+ });
1823
+ responderTask.state = terminalState;
1824
+ responderTask.note = note;
1825
+ responderTask.jobId = durableJob.id;
1826
+ responderTask.processId = durableJob.pid;
1827
+ responderTask.responderOwned = true;
1828
+ if (parentTaskId)
1829
+ responderTask.parentTaskId = parentTaskId;
1830
+ if (isPlanTerminal(livePlan)) {
1831
+ livePlan.status = isPlanSuccessful(livePlan)
1832
+ ? "completed"
1833
+ : "abandoned";
1834
+ }
1835
+ else if (livePlan.status !== "draft") {
1836
+ livePlan.status = "in_progress";
1837
+ }
1838
+ let responderPlanSaved = false;
1839
+ try {
1840
+ await savePlan(livePlan);
1841
+ responderPlanSaved = true;
1842
+ }
1843
+ catch {
1844
+ writeNotice("warn", `Responder job ${durableJob.id} started, but its plan subtask could not be persisted`, chalk.yellow(` ⚠ job ${durableJob.id} is running, but task linkage persistence failed\n`));
1845
+ }
1846
+ if (responderPlanSaved) {
1847
+ linkedTaskId = responderTask.id;
1848
+ linkedParentTaskId = parentTaskId;
1849
+ responderTaskId = responderTask.id;
1850
+ pendingSessionStatePlan = livePlan;
1851
+ writePlanUpdate(livePlan, renderPlanForTerminal(livePlan) + "\n");
1852
+ }
1853
+ }
1854
+ if (durableJob) {
1855
+ const linkedJob = jobManager.linkJob(durableJob.id, {
1856
+ ...(linkedTaskId ? { taskId: linkedTaskId } : {}),
1857
+ ...(linkedParentTaskId
1858
+ ? { parentTaskId: linkedParentTaskId }
1859
+ : {}),
1860
+ wakeOnCompletion: true,
1861
+ responder: true,
1862
+ monitor: {
1863
+ ...(durableJob.monitor ?? {}),
1864
+ toolName: call.name,
1865
+ toolEventId,
1866
+ },
1867
+ });
1868
+ if (!linkedJob) {
1869
+ writeNotice("warn", `Responder job ${durableJob.id} started, but durable task linkage will be retried on completion`, chalk.yellow(` ⚠ job ${durableJob.id} is running, but durable task linkage needs recovery\n`));
1870
+ }
1871
+ else if (responderTaskId) {
1872
+ result = {
1873
+ ...result,
1874
+ output: `${result.output}\nResponder linked job ${durableJob.id} to subtask [${responderTaskId}]` +
1875
+ `${linkedParentTaskId ? ` under [${linkedParentTaskId}]` : ""}. ` +
1876
+ "This child subtask advances on its own from the real process result — do not mark, poll, or wait on it. " +
1877
+ "Mark your current launch step done and move to the next task now; do NOT shell.tail/shell.jobs/sleep to watch it. " +
1878
+ "The Responder delivers the completion into your context automatically when it is ready.",
1879
+ };
1880
+ }
1881
+ }
1882
+ }
1883
+ }
1742
1884
  const output = result.output.trim();
1743
1885
  // Always keep a full on-disk copy of tool output (any size) so the
1744
1886
  // pager never depends on a truncated in-memory preview.
@@ -2052,7 +2194,7 @@ export async function runAgentTurn(prompt, options = {}) {
2052
2194
  };
2053
2195
  async function maybeAutoCompact(reason, force = false) {
2054
2196
  const beforeTokens = estimateNextRequestTokens(messages);
2055
- // E1: auto-compact trigger (default 100k, matches hard ceiling).
2197
+ // E1: auto-compact trigger (default 72k soft; hard ceiling 100k).
2056
2198
  const compactTrigger = autoCompactTriggerTokens();
2057
2199
  if (!force && beforeTokens < compactTrigger)
2058
2200
  return;
@@ -2191,6 +2333,11 @@ export async function runAgentTurn(prompt, options = {}) {
2191
2333
  }
2192
2334
  else {
2193
2335
  await maybeAutoCompact("auto-token-budget");
2336
+ // Safe boundary: no assistant tool-call group is open here. Refresh the
2337
+ // durable Responder inbox immediately before every provider request so
2338
+ // completions arriving mid-turn are visible without corrupting native
2339
+ // tool protocol or forcing a separate busy-wait loop.
2340
+ refreshResponderInbox();
2194
2341
  const streamLabel = step === 0 ? "waiting for model" : `step ${step + 1}`;
2195
2342
  let spinner = writesDirectly
2196
2343
  ? startThinkingSpinner(streamLabel, options.signal)
@@ -3034,7 +3181,8 @@ export async function runAgentTurn(prompt, options = {}) {
3034
3181
  if (session.planApproved.value &&
3035
3182
  budgetRemaining(recovery, "prematureComplete")) {
3036
3183
  const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
3037
- const unfinished = livePlan?.tasks.filter((t) => t.state === "pending" || t.state === "in_progress");
3184
+ const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
3185
+ (task.state === "pending" || task.state === "in_progress"));
3038
3186
  if (livePlan && unfinished && unfinished.length > 0) {
3039
3187
  const next = unfinished[0];
3040
3188
  const action = recoveryForPrematureComplete({