@prestyj/cli 5.6.0 → 5.7.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 (100) hide show
  1. package/dist/app-sidecar.js +127 -39
  2. package/dist/app-sidecar.js.map +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +15 -0
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent-session-review-coverage.test.d.ts +2 -0
  7. package/dist/core/agent-session-review-coverage.test.d.ts.map +1 -0
  8. package/dist/core/agent-session-review-coverage.test.js +57 -0
  9. package/dist/core/agent-session-review-coverage.test.js.map +1 -0
  10. package/dist/core/agent-session.d.ts +11 -4
  11. package/dist/core/agent-session.d.ts.map +1 -1
  12. package/dist/core/agent-session.js +133 -22
  13. package/dist/core/agent-session.js.map +1 -1
  14. package/dist/core/compaction/compactor.test.js +1 -1
  15. package/dist/core/compaction/compactor.test.js.map +1 -1
  16. package/dist/core/event-bus.d.ts +2 -0
  17. package/dist/core/event-bus.d.ts.map +1 -1
  18. package/dist/core/event-bus.js.map +1 -1
  19. package/dist/core/ideal-review.d.ts +27 -0
  20. package/dist/core/ideal-review.d.ts.map +1 -1
  21. package/dist/core/ideal-review.js +64 -0
  22. package/dist/core/ideal-review.js.map +1 -1
  23. package/dist/core/ideal-review.test.js +34 -1
  24. package/dist/core/ideal-review.test.js.map +1 -1
  25. package/dist/core/lsp/client.d.ts +3 -0
  26. package/dist/core/lsp/client.d.ts.map +1 -1
  27. package/dist/core/lsp/client.js +20 -0
  28. package/dist/core/lsp/client.js.map +1 -1
  29. package/dist/core/lsp/manager.d.ts +32 -9
  30. package/dist/core/lsp/manager.d.ts.map +1 -1
  31. package/dist/core/lsp/manager.js +89 -37
  32. package/dist/core/lsp/manager.js.map +1 -1
  33. package/dist/core/lsp/manager.test.js +56 -4
  34. package/dist/core/lsp/manager.test.js.map +1 -1
  35. package/dist/core/run-lifecycle.d.ts +40 -0
  36. package/dist/core/run-lifecycle.d.ts.map +1 -0
  37. package/dist/core/run-lifecycle.js +95 -0
  38. package/dist/core/run-lifecycle.js.map +1 -0
  39. package/dist/core/run-lifecycle.test.d.ts +2 -0
  40. package/dist/core/run-lifecycle.test.d.ts.map +1 -0
  41. package/dist/core/run-lifecycle.test.js +64 -0
  42. package/dist/core/run-lifecycle.test.js.map +1 -0
  43. package/dist/core/session-manager.d.ts +25 -1
  44. package/dist/core/session-manager.d.ts.map +1 -1
  45. package/dist/core/session-manager.js +66 -1
  46. package/dist/core/session-manager.js.map +1 -1
  47. package/dist/core/session-manager.test.js +80 -2
  48. package/dist/core/session-manager.test.js.map +1 -1
  49. package/dist/core/subagent-manager.d.ts +40 -3
  50. package/dist/core/subagent-manager.d.ts.map +1 -1
  51. package/dist/core/subagent-manager.js +342 -30
  52. package/dist/core/subagent-manager.js.map +1 -1
  53. package/dist/core/subagent-manager.test.js +189 -3
  54. package/dist/core/subagent-manager.test.js.map +1 -1
  55. package/dist/core/subagent-store.d.ts +20 -0
  56. package/dist/core/subagent-store.d.ts.map +1 -0
  57. package/dist/core/subagent-store.js +177 -0
  58. package/dist/core/subagent-store.js.map +1 -0
  59. package/dist/core/subagent-store.test.d.ts +2 -0
  60. package/dist/core/subagent-store.test.d.ts.map +1 -0
  61. package/dist/core/subagent-store.test.js +94 -0
  62. package/dist/core/subagent-store.test.js.map +1 -0
  63. package/dist/modes/agent-home-mode.d.ts.map +1 -1
  64. package/dist/modes/agent-home-mode.js +10 -3
  65. package/dist/modes/agent-home-mode.js.map +1 -1
  66. package/dist/modes/serve-mode.d.ts.map +1 -1
  67. package/dist/modes/serve-mode.js +10 -3
  68. package/dist/modes/serve-mode.js.map +1 -1
  69. package/dist/modes/subagent-worker-mode.d.ts +2 -0
  70. package/dist/modes/subagent-worker-mode.d.ts.map +1 -1
  71. package/dist/modes/subagent-worker-mode.js +13 -3
  72. package/dist/modes/subagent-worker-mode.js.map +1 -1
  73. package/dist/tools/goals.d.ts +3 -3
  74. package/dist/tools/tasks.d.ts +2 -2
  75. package/dist/tools/web-fetch.d.ts +1 -1
  76. package/dist/ui/App.d.ts +7 -1
  77. package/dist/ui/App.d.ts.map +1 -1
  78. package/dist/ui/App.js +63 -17
  79. package/dist/ui/App.js.map +1 -1
  80. package/dist/ui/components/Footer.d.ts +1 -0
  81. package/dist/ui/components/Footer.d.ts.map +1 -1
  82. package/dist/ui/components/Footer.js +3 -3
  83. package/dist/ui/components/Footer.js.map +1 -1
  84. package/dist/ui/components/Footer.test.d.ts +2 -0
  85. package/dist/ui/components/Footer.test.d.ts.map +1 -0
  86. package/dist/ui/components/Footer.test.js +16 -0
  87. package/dist/ui/components/Footer.test.js.map +1 -0
  88. package/dist/ui/hooks/useAgentLoop.d.ts +8 -3
  89. package/dist/ui/hooks/useAgentLoop.d.ts.map +1 -1
  90. package/dist/ui/hooks/useAgentLoop.js +63 -7
  91. package/dist/ui/hooks/useAgentLoop.js.map +1 -1
  92. package/dist/ui/hooks/useSessionPersistence.d.ts +4 -2
  93. package/dist/ui/hooks/useSessionPersistence.d.ts.map +1 -1
  94. package/dist/ui/hooks/useSessionPersistence.js +7 -1
  95. package/dist/ui/hooks/useSessionPersistence.js.map +1 -1
  96. package/dist/ui/render.d.ts +7 -0
  97. package/dist/ui/render.d.ts.map +1 -1
  98. package/dist/ui/render.js +4 -0
  99. package/dist/ui/render.js.map +1 -1
  100. package/package.json +5 -5
@@ -18,10 +18,11 @@ import os from "node:os";
18
18
  import path from "node:path";
19
19
  import { randomUUID } from "node:crypto";
20
20
  import { parseArgs } from "node:util";
21
- import { formatError } from "@prestyj/ai";
21
+ import { environmentSecrets, formatError, redactValue } from "@prestyj/ai";
22
22
  import { runJsonMode } from "./modes/json-mode.js";
23
23
  import { runSubagentWorkerMode } from "./modes/subagent-worker-mode.js";
24
24
  import { AgentSession } from "./core/agent-session.js";
25
+ import { RunLifecycle } from "./core/run-lifecycle.js";
25
26
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, sessionsDirForChatAgent, switchChatAgent, } from "./chat-agents/index.js";
26
27
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
27
28
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
@@ -929,7 +930,8 @@ async function createSession(deps, opts) {
929
930
  const clients = new Set();
930
931
  let clientSeq = 0;
931
932
  function broadcast(type, data) {
932
- const frame = `data: ${JSON.stringify({ type, data })}\n\n`;
933
+ const safePayload = redactValue({ type, data }, { secrets: environmentSecrets(process.env) });
934
+ const frame = `data: ${JSON.stringify(safePayload)}\n\n`;
933
935
  for (const c of clients)
934
936
  c.res.write(frame);
935
937
  }
@@ -1137,6 +1139,13 @@ async function createSession(deps, opts) {
1137
1139
  session.eventBus.on("compaction_start", (d) => broadcast("compaction_start", d));
1138
1140
  session.eventBus.on("compaction_end", (d) => broadcast("compaction_end", d));
1139
1141
  let running = false;
1142
+ const runLifecycle = new RunLifecycle((runState) => {
1143
+ running = runState !== "idle";
1144
+ if (runState === "cancelling")
1145
+ broadcast("run_cancelling", { runState });
1146
+ });
1147
+ const cancelledRunEndGenerations = new Set();
1148
+ let pendingCancelDrain = null;
1140
1149
  let titleGenerated = false;
1141
1150
  // Bumped by /cancel — a run whose cancel generation changed mid-flight was
1142
1151
  // canceled and earns no XP.
@@ -1159,6 +1168,7 @@ async function createSession(deps, opts) {
1159
1168
  let autopilotCancelled = false;
1160
1169
  // Hard cap on review→prompt→review rounds per user turn (loop safety).
1161
1170
  const MAX_AUTOPILOT_ROUNDS = 3;
1171
+ const CANCEL_TIMEOUT_MS = 5_000;
1162
1172
  // Prompt bodies Autopilot Nolan injected into the BUILD session this
1163
1173
  // conversation. Passed into every Nolan digest so injected prompts render as
1164
1174
  // "Nolan autopilot (injected)" instead of `**User:**` — otherwise multi-round
@@ -1365,29 +1375,63 @@ async function createSession(deps, opts) {
1365
1375
  });
1366
1376
  }
1367
1377
  }
1368
- // Core run lifecycle shared by /prompt and the task runner: flips `running`,
1369
- // brackets the run with run_start/run_end, refreshes the footer extras, and
1370
- // generates the session title once. `label` is the text shown live with the
1371
- // run_start frame.
1378
+ function abortOwnedWork() {
1379
+ cancelGeneration++;
1380
+ abort.abort();
1381
+ // Stop a run-all sweep and every async child through AgentSession's signal.
1382
+ taskRunAll = false;
1383
+ autopilotCancelled = true;
1384
+ nolanAutoAbort.abort();
1385
+ }
1386
+ function installFreshRunControllers() {
1387
+ abort = new AbortController();
1388
+ session.setSignal(abort.signal);
1389
+ nolanAutoAbort = new AbortController();
1390
+ nolanAutoSession?.setSignal(nolanAutoAbort.signal);
1391
+ }
1392
+ function finishOwnedGeneration(generation, emitCancelledFallback) {
1393
+ const cancelled = runLifecycle.isCancellationRequested(generation);
1394
+ const settlement = runLifecycle.settle(generation);
1395
+ if (!settlement.settled)
1396
+ return cancelled;
1397
+ // A replacement signal is safe only after the provider-backed owner settled.
1398
+ installFreshRunControllers();
1399
+ if (cancelled && emitCancelledFallback && !cancelledRunEndGenerations.has(generation)) {
1400
+ cancelledRunEndGenerations.add(generation);
1401
+ broadcast("run_end", { cancelled: true, runState: runLifecycle.state });
1402
+ }
1403
+ return cancelled;
1404
+ }
1405
+ // Core provider-run bracket. Standalone runs own a lifecycle generation;
1406
+ // injected autopilot runs share the cycle's outer generation.
1372
1407
  async function runAgent(label, run) {
1373
- running = true;
1408
+ const ownsGeneration = !runLifecycle.running;
1409
+ const generation = ownsGeneration
1410
+ ? runLifecycle.begin(abortOwnedWork).generation
1411
+ : runLifecycle.generation;
1412
+ if (ownsGeneration)
1413
+ pendingCancelDrain = null;
1374
1414
  // Progress (Ranks): completed, non-canceled runs with ≥1 assistant turn earn
1375
1415
  // XP — prompt + any commits authored during the run window.
1376
1416
  const runStartedAt = Date.now();
1377
1417
  const cancelGenAtStart = cancelGeneration;
1378
1418
  const assistantsBeforeRun = countAssistantMessages(session.getMessages());
1379
1419
  let runSucceeded = false;
1380
- broadcast("run_start", { text: label });
1420
+ broadcast("run_start", { text: label, runState: runLifecycle.state });
1381
1421
  try {
1382
- await run();
1422
+ if (!runLifecycle.isCancellationRequested(generation))
1423
+ await run();
1383
1424
  runSucceeded = true;
1384
1425
  }
1385
1426
  catch (err) {
1386
- broadcastError("error", "run failed", err);
1427
+ if (!runLifecycle.isCancellationRequested(generation)) {
1428
+ broadcastError("error", "run failed", err);
1429
+ }
1387
1430
  }
1388
1431
  finally {
1389
- running = false;
1432
+ const cancelled = runLifecycle.isCancellationRequested(generation);
1390
1433
  if (runSucceeded &&
1434
+ !cancelled &&
1391
1435
  cancelGeneration === cancelGenAtStart &&
1392
1436
  countAssistantMessages(session.getMessages()) > assistantsBeforeRun) {
1393
1437
  // Fire-and-forget — XP must never delay or break run teardown.
@@ -1397,7 +1441,18 @@ async function createSession(deps, opts) {
1397
1441
  // background tasks — refresh the footer extras once it settles.
1398
1442
  gitBranch = await getGitBranch(cwd).catch(() => gitBranch);
1399
1443
  gitIsRepo = await isGitRepo(cwd).catch(() => gitIsRepo);
1400
- broadcast("run_end", {});
1444
+ if (ownsGeneration)
1445
+ finishOwnedGeneration(generation, false);
1446
+ // A cancelled injected run is still owned by the surrounding autopilot
1447
+ // cycle; its outer finalizer emits the one terminal cancelled run_end.
1448
+ if (!(cancelled && !ownsGeneration)) {
1449
+ if (cancelled)
1450
+ cancelledRunEndGenerations.add(generation);
1451
+ broadcast("run_end", {
1452
+ ...(cancelled ? { cancelled: true } : {}),
1453
+ runState: runLifecycle.state,
1454
+ });
1455
+ }
1401
1456
  // Autopilot's review loop is driven explicitly from POST /prompt (see
1402
1457
  // runAutopilotCycle), NOT from this shared finally — that keeps the
1403
1458
  // injected EZ Coder runs this cycle triggers from recursively re-entering
@@ -1406,12 +1461,8 @@ async function createSession(deps, opts) {
1406
1461
  // completed ones so they drop out of the Tasks modal automatically (users
1407
1462
  // never have to delete finished tasks by hand).
1408
1463
  broadcast("tasks_list", { tasks: pruneDoneTasksSync(cwd) });
1409
- // Queue drains into the run as steering, so it's empty by run_end —
1410
- // sync the webview indicator.
1411
1464
  broadcast("queued", { count: session.getQueuedCount() });
1412
1465
  broadcast("extras", footerExtras());
1413
- // Generate a session title once, after the first run, for the title bar
1414
- // (best-effort, async — don't block the response).
1415
1466
  if (!titleGenerated) {
1416
1467
  titleGenerated = true;
1417
1468
  void session.generateTitle().then((title) => {
@@ -1444,7 +1495,8 @@ async function createSession(deps, opts) {
1444
1495
  return parseAutopilotVerdict(lastAssistantText(ken.getMessages()));
1445
1496
  }
1446
1497
  catch (err) {
1447
- broadcastError("autopilot_error", "autopilot review failed", err);
1498
+ if (!autopilotCancelled)
1499
+ broadcastError("autopilot_error", "autopilot review failed", err);
1448
1500
  return null;
1449
1501
  }
1450
1502
  finally {
@@ -1519,6 +1571,8 @@ async function createSession(deps, opts) {
1519
1571
  async function runAutopilotCycle(originalRequest) {
1520
1572
  if (!autopilot || autopilotCancelled)
1521
1573
  return;
1574
+ const generation = runLifecycle.begin(abortOwnedWork).generation;
1575
+ pendingCancelDrain = null;
1522
1576
  autopilotActive = true;
1523
1577
  // Generation captured by the last plan review; acceptPlan re-checks it so
1524
1578
  // a user Accept/Reject landing mid-review always wins.
@@ -1633,6 +1687,8 @@ async function createSession(deps, opts) {
1633
1687
  }
1634
1688
  finally {
1635
1689
  autopilotActive = false;
1690
+ finishOwnedGeneration(generation, true);
1691
+ queueMicrotask(() => void runStrandedQueue());
1636
1692
  }
1637
1693
  }
1638
1694
  // ── Stranded-queue drain ───────────────────────────────
@@ -1825,7 +1881,7 @@ async function createSession(deps, opts) {
1825
1881
  });
1826
1882
  }
1827
1883
  function json(res, status, body) {
1828
- const payload = JSON.stringify(body);
1884
+ const payload = JSON.stringify(redactValue(body, { secrets: environmentSecrets(process.env) }));
1829
1885
  res.writeHead(status, {
1830
1886
  "content-type": "application/json",
1831
1887
  "access-control-allow-origin": "*",
@@ -1841,6 +1897,7 @@ async function createSession(deps, opts) {
1841
1897
  mode,
1842
1898
  chatAgent,
1843
1899
  running,
1900
+ runState: runLifecycle.state,
1844
1901
  ready: true,
1845
1902
  thinkingLevel: session.getThinkingLevel() ?? null,
1846
1903
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
@@ -1913,6 +1970,7 @@ async function createSession(deps, opts) {
1913
1970
  mode,
1914
1971
  chatAgent,
1915
1972
  running,
1973
+ runState: runLifecycle.state,
1916
1974
  thinkingLevel: session.getThinkingLevel() ?? null,
1917
1975
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
1918
1976
  supportsVideo: getModel(st.model)?.supportsVideo ?? false,
@@ -2409,6 +2467,13 @@ async function createSession(deps, opts) {
2409
2467
  json(res, 400, { error: "empty prompt" });
2410
2468
  return;
2411
2469
  }
2470
+ if (runLifecycle.running && runLifecycle.isCancellationRequested(runLifecycle.generation)) {
2471
+ json(res, 409, {
2472
+ error: runLifecycle.state === "cancelling" ? "run_cancelling" : "cancel_failed",
2473
+ runState: runLifecycle.state,
2474
+ });
2475
+ return;
2476
+ }
2412
2477
  if (running || autopilotActive) {
2413
2478
  // Queue prompts as mid-run steering (mirrors the CLI). Also queue while
2414
2479
  // an autopilot cycle is active but between injected runs (build idle,
@@ -2982,27 +3047,50 @@ async function createSession(deps, opts) {
2982
3047
  return;
2983
3048
  }
2984
3049
  if (method === "POST" && url === "/cancel") {
2985
- cancelGeneration++;
2986
- abort.abort();
2987
- abort = new AbortController();
2988
- session.setSignal(abort.signal);
2989
- running = false;
2990
- // Stop a run-all sweep so the next pending task isn't auto-started.
2991
- taskRunAll = false;
2992
- taskRunActive = false;
2993
- // Stop any in-flight autopilot cycle: flag it so the loop bails between
2994
- // steps, and abort a review that's mid-prompt on the nolanAuto session.
2995
- autopilotCancelled = true;
2996
- nolanAutoAbort.abort();
2997
- nolanAutoAbort = new AbortController();
2998
- nolanAutoSession?.setSignal(nolanAutoAbort.signal);
2999
- autopilotReviewing = false;
3000
- // Drop any queued steering and return it so the webview can restore it to
3001
- // the composer.
3002
- const drained = session.drainQueue();
3003
- broadcast("run_end", { cancelled: true });
3004
- broadcast("queued", { count: 0 });
3005
- json(res, 200, { cancelled: true, drained });
3050
+ void (async () => {
3051
+ // Even between task runs, cancellation stops the sweep. Active provider
3052
+ // ownership invokes the full abort hook exactly once through lifecycle.
3053
+ taskRunAll = false;
3054
+ autopilotCancelled = true;
3055
+ if (!runLifecycle.running) {
3056
+ nolanAutoAbort.abort();
3057
+ nolanAutoAbort = new AbortController();
3058
+ nolanAutoSession?.setSignal(nolanAutoAbort.signal);
3059
+ }
3060
+ const generation = runLifecycle.generation;
3061
+ if (!pendingCancelDrain || pendingCancelDrain.generation !== generation) {
3062
+ pendingCancelDrain = { generation, text: session.drainQueue() };
3063
+ broadcast("queued", { count: 0 });
3064
+ }
3065
+ const result = await runLifecycle.cancel(CANCEL_TIMEOUT_MS);
3066
+ const drained = pendingCancelDrain.text;
3067
+ if (result.status === "failed") {
3068
+ broadcast("cancel_failed", {
3069
+ error: "cancel_failed",
3070
+ reason: result.reason,
3071
+ runState: runLifecycle.state,
3072
+ });
3073
+ json(res, 504, {
3074
+ error: "cancel_failed",
3075
+ reason: result.reason,
3076
+ runState: runLifecycle.state,
3077
+ drained,
3078
+ });
3079
+ return;
3080
+ }
3081
+ json(res, 200, {
3082
+ cancelled: result.status === "cancelled",
3083
+ runState: runLifecycle.state,
3084
+ drained,
3085
+ });
3086
+ })().catch((error) => {
3087
+ broadcast("cancel_failed", { error: "cancel_failed", runState: runLifecycle.state });
3088
+ json(res, 500, {
3089
+ error: "cancel_failed",
3090
+ message: error instanceof Error ? error.message : String(error),
3091
+ runState: runLifecycle.state,
3092
+ });
3093
+ });
3006
3094
  return;
3007
3095
  }
3008
3096
  if (method === "POST" && url === "/new-session") {