@prestyj/cli 5.20.0 → 5.22.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 (44) hide show
  1. package/README.md +1 -1
  2. package/dist/app-sidecar.js +153 -8
  3. package/dist/app-sidecar.js.map +1 -1
  4. package/dist/cli.js +5 -10
  5. package/dist/cli.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +6 -0
  7. package/dist/core/agent-session.js +11 -3
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/autopilot-cycle.d.ts +3 -4
  10. package/dist/core/autopilot-cycle.js +18 -11
  11. package/dist/core/autopilot-cycle.js.map +1 -1
  12. package/dist/core/autopilot-gate.d.ts +1 -3
  13. package/dist/core/autopilot-gate.js +4 -16
  14. package/dist/core/autopilot-gate.js.map +1 -1
  15. package/dist/core/event-bus.d.ts +3 -0
  16. package/dist/core/event-bus.js +5 -0
  17. package/dist/core/event-bus.js.map +1 -1
  18. package/dist/core/ideal-review.js +7 -1
  19. package/dist/core/ideal-review.js.map +1 -1
  20. package/dist/core/nolan-context.d.ts +1 -2
  21. package/dist/core/nolan-context.js +81 -21
  22. package/dist/core/nolan-context.js.map +1 -1
  23. package/dist/core/nolan-prompt.js +52 -48
  24. package/dist/core/nolan-prompt.js.map +1 -1
  25. package/dist/core/prompt-command-expansion.d.ts +13 -0
  26. package/dist/core/prompt-command-expansion.js +48 -0
  27. package/dist/core/prompt-command-expansion.js.map +1 -0
  28. package/dist/core/run-status.d.ts +17 -0
  29. package/dist/core/run-status.js +28 -0
  30. package/dist/core/run-status.js.map +1 -0
  31. package/dist/core/session-history.js +5 -14
  32. package/dist/core/session-history.js.map +1 -1
  33. package/dist/core/verification-gate.d.ts +6 -1
  34. package/dist/core/verification-gate.js +26 -4
  35. package/dist/core/verification-gate.js.map +1 -1
  36. package/dist/core/worktree.d.ts +161 -0
  37. package/dist/core/worktree.js +574 -0
  38. package/dist/core/worktree.js.map +1 -0
  39. package/dist/tools/subagent.d.ts +1 -0
  40. package/dist/tools/subagent.js +66 -2
  41. package/dist/tools/subagent.js.map +1 -1
  42. package/dist/ui/prompt-routing.js +2 -1
  43. package/dist/ui/prompt-routing.js.map +1 -1
  44. package/package.json +5 -5
package/README.md CHANGED
@@ -177,7 +177,7 @@ Plus the [Grep MCP](https://grep.dev) for searching across 1M+ public GitHub rep
177
177
 
178
178
  Only GPT-5.6 Sol/Terra at **Ultra** delegates proactively. Lower Sol/Terra levels use async agents only when the user or project/skill instructions request delegation; other models receive no proactive policy.
179
179
 
180
- Children share the parent working directory, not isolated worktrees. Parallel writes must target disjoint files or subsystems. Async fan-out is one level deep, child output is bounded, idle workers reap after 10 minutes, and workers are not resumable after a CLI/app restart.
180
+ Children share the parent working directory by default, so parallel writes must target disjoint files or subsystems. Pass `isolate: true` to `subagent` to give a child its own copy of the repo on a new branch instead — the right choice when several children edit at once, or when a child's changes should land on a branch of their own. The copy is reclaimed automatically when the child finishes without committing; a child that did commit keeps its branch, because that branch is the deliverable. `/integrate` then merges those branches one at a time, running the suite between each. Async fan-out is one level deep, child output is bounded, idle workers reap after 10 minutes, and workers are not resumable after a CLI/app restart.
181
181
 
182
182
  Parent cancellation interrupts active children. Session disposal shuts down every worker process alongside background commands, LSP servers, and MCP connections.
183
183
 
@@ -28,12 +28,14 @@ import { RunLifecycle } from "./core/run-lifecycle.js";
28
28
  import { RunClaim } from "./core/run-claim.js";
29
29
  import { CHAT_AGENT_IDS, chatAgentSessionsDir, createChatAgent, parseChatAgentId, switchChatAgent, } from "./chat-agents/index.js";
30
30
  import { buildJiwaTools, JiwaStore } from "./chat-agents/jiwa.js";
31
+ import { createWorktree, removeWorktree, sweepWorktrees, worktreeStatuses, } from "./core/worktree.js";
31
32
  import { buildMemoryTools, MemoryStore } from "./chat-agents/memory.js";
32
33
  import { buildNolanSystemPrompt, buildNolanAutopilotSystemPrompt } from "./core/nolan-prompt.js";
33
34
  import { buildNolanDigest, buildNolanAutopilotContext, buildNolanAutopilotPlanContext, } from "./core/nolan-context.js";
34
35
  import { parseAutopilotVerdict } from "./core/autopilot-verdict.js";
35
36
  import { isWorkflowCommandText, countAssistantMessages, shouldStartAutopilotCycle, extractTurnToolCalls, isMechanicalOnlyTurn, } from "./core/autopilot-gate.js";
36
37
  import { driveAutopilotCycle, frameAutopilotInjection } from "./core/autopilot-cycle.js";
38
+ import { describeRunVerification, describeTurnVerification } from "./core/run-status.js";
37
39
  import { validateNolanModelPref, effectiveNolanModel, } from "./core/nolan-model.js";
38
40
  import { normalizeAutopilotMarkersForHistory, normalizeAppMarkersForHistory, normalizeNolanTurnsForHistory, getHistoryMessageVisibility, replayMessagesInOrder, restoreUserRow, restoreAssistantTexts, resolveRestoredCommand, autopilotMarkerCopySeed, } from "./core/session-history.js";
39
41
  import { sessionToMarkdown, defaultExportFilename, } from "./core/session-export.js";
@@ -1829,6 +1831,11 @@ async function createSession(deps, opts) {
1829
1831
  recordApprovedPlanMarkers(d.text);
1830
1832
  });
1831
1833
  session.eventBus.on("thinking_delta", (d) => broadcast("thinking_delta", d));
1834
+ session.eventBus.on("retry", (d) => {
1835
+ if (!d.silent)
1836
+ broadcast("retry", { reason: d.reason, attempt: d.attempt, delayMs: d.delayMs });
1837
+ });
1838
+ session.eventBus.on("max_turns", (d) => broadcast("max_turns", d));
1832
1839
  // The agent consumed queued steering at a turn boundary. Re-broadcast as the
1833
1840
  // usual `queued` depth update so the webview drops the pending affordance the
1834
1841
  // moment the message lands in the loop, not at run_end.
@@ -2138,8 +2145,10 @@ async function createSession(deps, opts) {
2138
2145
  // his own Ideal self-review adds latency and can corrupt the verdict shape.
2139
2146
  nolanAgent.setIdealReviewSuppressed(true);
2140
2147
  await nolanAgent.initialize();
2141
- // Deliberately no bus bridge: the review is silent. Errors surface via the
2142
- // runAutopilotReview try/catch as autopilot_error frames.
2148
+ // Keep review text/tools silent; report usage only for whole-task accounting.
2149
+ nolanAgent.eventBus.on("turn_end", (d) => {
2150
+ broadcast("autopilot_usage", { outputTokens: d.usage.outputTokens });
2151
+ });
2143
2152
  nolanAutoSession = nolanAgent;
2144
2153
  log("INFO", "app-sidecar", "nolan autopilot session ready", {
2145
2154
  provider: target.provider,
@@ -2181,7 +2190,7 @@ async function createSession(deps, opts) {
2181
2190
  }
2182
2191
  // Core provider-run bracket. Standalone runs own a lifecycle generation;
2183
2192
  // injected autopilot runs share the cycle's outer generation.
2184
- async function runAgent(label, run) {
2193
+ async function runAgent(label, run, reviewPending = () => false) {
2185
2194
  const ownsGeneration = !runLifecycle.running;
2186
2195
  const generation = ownsGeneration
2187
2196
  ? runLifecycle.begin(abortOwnedWork).generation
@@ -2194,7 +2203,11 @@ async function createSession(deps, opts) {
2194
2203
  const cancelGenAtStart = cancelGeneration;
2195
2204
  const assistantsBeforeRun = countAssistantMessages(session.getMessages());
2196
2205
  let runSucceeded = false;
2197
- broadcast("run_start", { text: label, runState: runLifecycle.state });
2206
+ broadcast("run_start", {
2207
+ text: label,
2208
+ runState: runLifecycle.state,
2209
+ continued: !ownsGeneration,
2210
+ });
2198
2211
  try {
2199
2212
  if (!runLifecycle.isCancellationRequested(generation))
2200
2213
  await run();
@@ -2265,6 +2278,16 @@ async function createSession(deps, opts) {
2265
2278
  broadcast("run_end", {
2266
2279
  ...(cancelled ? { cancelled: true } : {}),
2267
2280
  ...(verificationProblem ? { unverified: true } : {}),
2281
+ failed: !cancelled && !runSucceeded,
2282
+ // The cycle refuses an unresolved verification gate. Do not advertise
2283
+ // a review handoff that will exit before emitting any review events.
2284
+ reviewPending: !cancelled &&
2285
+ runSucceeded &&
2286
+ !verificationProblem &&
2287
+ (reviewPending() || !ownsGeneration),
2288
+ ...describeRunVerification(session.getVerificationEvidence(), verificationProblem),
2289
+ turnVerification: describeTurnVerification(session.getRunVerificationActivity(), verificationProblem),
2290
+ ...(verificationProblem ? { verificationReason: verificationProblem } : {}),
2268
2291
  runState: runLifecycle.state,
2269
2292
  });
2270
2293
  }
@@ -2553,7 +2576,7 @@ async function createSession(deps, opts) {
2553
2576
  else {
2554
2577
  await session.prompt(next.text);
2555
2578
  }
2556
- });
2579
+ }, () => autopilot);
2557
2580
  const decision = shouldStartAutopilotCycle({
2558
2581
  enabled: autopilot,
2559
2582
  cancelled: autopilotCancelled,
@@ -2575,7 +2598,8 @@ async function createSession(deps, opts) {
2575
2598
  });
2576
2599
  await runAutopilotCycle(next.text);
2577
2600
  }
2578
- else if (autopilot) {
2601
+ else {
2602
+ broadcast("autopilot_ignored", { reason: decision.reason });
2579
2603
  log("INFO", "app-sidecar", "autopilot skipped (queued turn)", {
2580
2604
  reason: decision.reason,
2581
2605
  });
@@ -2879,6 +2903,7 @@ async function createSession(deps, opts) {
2879
2903
  mode,
2880
2904
  chatAgent,
2881
2905
  running,
2906
+ reviewPending: autopilotActive,
2882
2907
  runState: runLifecycle.state,
2883
2908
  thinkingLevel: session.getThinkingLevel() ?? null,
2884
2909
  supportedThinkingLevels: getSupportedThinkingLevels(st.provider, st.model),
@@ -3016,6 +3041,125 @@ async function createSession(deps, opts) {
3016
3041
  });
3017
3042
  return;
3018
3043
  }
3044
+ // Create an isolated git worktree for a project so a second window can work
3045
+ // on the same repo without fighting the first over the working tree. The
3046
+ // caller passes the project cwd; the module resolves the repo's MAIN root
3047
+ // itself, so opening this from an already-linked worktree still branches
3048
+ // off the real repo rather than nesting.
3049
+ //
3050
+ // The dirty gate in `createWorktree` is deliberate: branching a main
3051
+ // checkout that has uncommitted work would strand those edits on a tree the
3052
+ // user is about to stop looking at. It surfaces here as a 409 the picker
3053
+ // renders verbatim.
3054
+ if (method === "POST" && url === "/worktree") {
3055
+ void readBody(req, res).then(async (raw) => {
3056
+ if (raw === null)
3057
+ return;
3058
+ let body;
3059
+ try {
3060
+ body = JSON.parse(raw);
3061
+ }
3062
+ catch {
3063
+ json(res, 400, { error: "invalid JSON body" });
3064
+ return;
3065
+ }
3066
+ const repoDir = body.cwd?.trim();
3067
+ if (!repoDir) {
3068
+ json(res, 400, { error: "cwd is required" });
3069
+ return;
3070
+ }
3071
+ try {
3072
+ const created = await createWorktree({
3073
+ repoDir,
3074
+ branch: body.branch?.trim() || undefined,
3075
+ baseRef: body.baseRef?.trim() || undefined,
3076
+ });
3077
+ json(res, 200, created);
3078
+ }
3079
+ catch (err) {
3080
+ const message = err instanceof Error ? err.message : String(err);
3081
+ // Dirty tree / not-a-repo are the user's to resolve, not bugs.
3082
+ const expected = message.includes("uncommitted changes") || message.includes("Not a git repository");
3083
+ json(res, expected ? 409 : 500, { error: message });
3084
+ }
3085
+ });
3086
+ return;
3087
+ }
3088
+ // Every managed copy of a repo with the state the picker needs to decide
3089
+ // what to offer: branch, unsaved files, unmerged commits, and why a copy
3090
+ // cannot be reclaimed. `busy` paths come from the caller (the app knows its
3091
+ // own windows; the daemon does not), repeated as `busy=` params.
3092
+ if (method === "GET" && (url === "/worktrees" || url.startsWith("/worktrees?"))) {
3093
+ const params = new URL(url, `http://${host}`).searchParams;
3094
+ const target = params.get("cwd")?.trim() || cwd;
3095
+ void worktreeStatuses(target, params.getAll("busy"))
3096
+ .then((worktrees) => json(res, 200, { worktrees }))
3097
+ .catch((err) => {
3098
+ // Not-a-repo is the common case here, not a fault: answer "none".
3099
+ log("INFO", "app-sidecar", "worktreeStatuses failed", {
3100
+ message: err instanceof Error ? err.message : String(err),
3101
+ });
3102
+ json(res, 200, { worktrees: [] });
3103
+ });
3104
+ return;
3105
+ }
3106
+ // Remove one copy. `force` is the user having seen what would be lost and
3107
+ // said yes anyway, so it is never defaulted on.
3108
+ //
3109
+ // `path` arrives from the client, and a request naming a path is not
3110
+ // permission to delete it: `removeWorktree` re-derives the managed root
3111
+ // from the repo itself and refuses anything outside it, or reached through
3112
+ // a symlink. The check lives there so no caller can skip it.
3113
+ if (method === "DELETE" && url === "/worktree") {
3114
+ void readBody(req, res).then(async (raw) => {
3115
+ if (raw === null)
3116
+ return;
3117
+ let body;
3118
+ try {
3119
+ body = JSON.parse(raw);
3120
+ }
3121
+ catch {
3122
+ json(res, 400, { error: "invalid JSON body" });
3123
+ return;
3124
+ }
3125
+ const worktreePath = body.path?.trim();
3126
+ if (!worktreePath) {
3127
+ json(res, 400, { error: "path is required" });
3128
+ return;
3129
+ }
3130
+ const release = await removeWorktree({
3131
+ repoDir: body.cwd?.trim() || cwd,
3132
+ worktreePath,
3133
+ force: body.force === true,
3134
+ });
3135
+ // removeWorktree never throws; a refusal is a 409 the UI shows as
3136
+ // written, matching the dirty-checkout gate on POST above.
3137
+ json(res, release.freed ? 200 : 409, release);
3138
+ });
3139
+ return;
3140
+ }
3141
+ // Reclaim every copy that provably holds no work. Never forces: anything
3142
+ // with unsaved or unmerged changes comes back under `kept` with a reason
3143
+ // and waits for a human. Called on window close and on daemon startup.
3144
+ if (method === "POST" && url === "/worktrees/sweep") {
3145
+ void readBody(req, res).then(async (raw) => {
3146
+ if (raw === null)
3147
+ return;
3148
+ let body;
3149
+ try {
3150
+ body = JSON.parse(raw);
3151
+ }
3152
+ catch {
3153
+ json(res, 400, { error: "invalid JSON body" });
3154
+ return;
3155
+ }
3156
+ const busy = Array.isArray(body.busyPaths)
3157
+ ? body.busyPaths.filter((p) => typeof p === "string")
3158
+ : [];
3159
+ json(res, 200, await sweepWorktrees(body.cwd?.trim() || cwd, busy));
3160
+ });
3161
+ return;
3162
+ }
3019
3163
  // Dismiss a project from the picker (or restore it with hidden:false). The
3020
3164
  // path is kept rather than the row: sessions in scratch dirs keep
3021
3165
  // re-surfacing, so the user's decision has to outlive any one scan.
@@ -3626,7 +3770,7 @@ async function createSession(deps, opts) {
3626
3770
  // while the webview keeps showing the short `/name`.
3627
3771
  await session.prompt(text);
3628
3772
  }
3629
- });
3773
+ }, () => autopilot);
3630
3774
  // After the user's run settles, kick off Nolan's auto-review loop — but
3631
3775
  // only when the turn is actually reviewable (shouldStartAutopilotCycle):
3632
3776
  // workflow commands (/compare, /expand, …) end with reports or
@@ -3655,7 +3799,8 @@ async function createSession(deps, opts) {
3655
3799
  log("INFO", "app-sidecar", "autopilot cycle starting", { kind: decision.kind });
3656
3800
  await runAutopilotCycle(text);
3657
3801
  }
3658
- else if (autopilot) {
3802
+ else {
3803
+ broadcast("autopilot_ignored", { reason: decision.reason });
3659
3804
  log("INFO", "app-sidecar", "autopilot skipped", { reason: decision.reason });
3660
3805
  }
3661
3806
  // A prompt sent while Nolan was reviewing (build idle) queued but had no