@prestyj/cli 5.20.0 → 5.21.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.
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,6 +28,7 @@ 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";
@@ -3016,6 +3017,125 @@ async function createSession(deps, opts) {
3016
3017
  });
3017
3018
  return;
3018
3019
  }
3020
+ // Create an isolated git worktree for a project so a second window can work
3021
+ // on the same repo without fighting the first over the working tree. The
3022
+ // caller passes the project cwd; the module resolves the repo's MAIN root
3023
+ // itself, so opening this from an already-linked worktree still branches
3024
+ // off the real repo rather than nesting.
3025
+ //
3026
+ // The dirty gate in `createWorktree` is deliberate: branching a main
3027
+ // checkout that has uncommitted work would strand those edits on a tree the
3028
+ // user is about to stop looking at. It surfaces here as a 409 the picker
3029
+ // renders verbatim.
3030
+ if (method === "POST" && url === "/worktree") {
3031
+ void readBody(req, res).then(async (raw) => {
3032
+ if (raw === null)
3033
+ return;
3034
+ let body;
3035
+ try {
3036
+ body = JSON.parse(raw);
3037
+ }
3038
+ catch {
3039
+ json(res, 400, { error: "invalid JSON body" });
3040
+ return;
3041
+ }
3042
+ const repoDir = body.cwd?.trim();
3043
+ if (!repoDir) {
3044
+ json(res, 400, { error: "cwd is required" });
3045
+ return;
3046
+ }
3047
+ try {
3048
+ const created = await createWorktree({
3049
+ repoDir,
3050
+ branch: body.branch?.trim() || undefined,
3051
+ baseRef: body.baseRef?.trim() || undefined,
3052
+ });
3053
+ json(res, 200, created);
3054
+ }
3055
+ catch (err) {
3056
+ const message = err instanceof Error ? err.message : String(err);
3057
+ // Dirty tree / not-a-repo are the user's to resolve, not bugs.
3058
+ const expected = message.includes("uncommitted changes") || message.includes("Not a git repository");
3059
+ json(res, expected ? 409 : 500, { error: message });
3060
+ }
3061
+ });
3062
+ return;
3063
+ }
3064
+ // Every managed copy of a repo with the state the picker needs to decide
3065
+ // what to offer: branch, unsaved files, unmerged commits, and why a copy
3066
+ // cannot be reclaimed. `busy` paths come from the caller (the app knows its
3067
+ // own windows; the daemon does not), repeated as `busy=` params.
3068
+ if (method === "GET" && (url === "/worktrees" || url.startsWith("/worktrees?"))) {
3069
+ const params = new URL(url, `http://${host}`).searchParams;
3070
+ const target = params.get("cwd")?.trim() || cwd;
3071
+ void worktreeStatuses(target, params.getAll("busy"))
3072
+ .then((worktrees) => json(res, 200, { worktrees }))
3073
+ .catch((err) => {
3074
+ // Not-a-repo is the common case here, not a fault: answer "none".
3075
+ log("INFO", "app-sidecar", "worktreeStatuses failed", {
3076
+ message: err instanceof Error ? err.message : String(err),
3077
+ });
3078
+ json(res, 200, { worktrees: [] });
3079
+ });
3080
+ return;
3081
+ }
3082
+ // Remove one copy. `force` is the user having seen what would be lost and
3083
+ // said yes anyway, so it is never defaulted on.
3084
+ //
3085
+ // `path` arrives from the client, and a request naming a path is not
3086
+ // permission to delete it: `removeWorktree` re-derives the managed root
3087
+ // from the repo itself and refuses anything outside it, or reached through
3088
+ // a symlink. The check lives there so no caller can skip it.
3089
+ if (method === "DELETE" && url === "/worktree") {
3090
+ void readBody(req, res).then(async (raw) => {
3091
+ if (raw === null)
3092
+ return;
3093
+ let body;
3094
+ try {
3095
+ body = JSON.parse(raw);
3096
+ }
3097
+ catch {
3098
+ json(res, 400, { error: "invalid JSON body" });
3099
+ return;
3100
+ }
3101
+ const worktreePath = body.path?.trim();
3102
+ if (!worktreePath) {
3103
+ json(res, 400, { error: "path is required" });
3104
+ return;
3105
+ }
3106
+ const release = await removeWorktree({
3107
+ repoDir: body.cwd?.trim() || cwd,
3108
+ worktreePath,
3109
+ force: body.force === true,
3110
+ });
3111
+ // removeWorktree never throws; a refusal is a 409 the UI shows as
3112
+ // written, matching the dirty-checkout gate on POST above.
3113
+ json(res, release.freed ? 200 : 409, release);
3114
+ });
3115
+ return;
3116
+ }
3117
+ // Reclaim every copy that provably holds no work. Never forces: anything
3118
+ // with unsaved or unmerged changes comes back under `kept` with a reason
3119
+ // and waits for a human. Called on window close and on daemon startup.
3120
+ if (method === "POST" && url === "/worktrees/sweep") {
3121
+ void readBody(req, res).then(async (raw) => {
3122
+ if (raw === null)
3123
+ return;
3124
+ let body;
3125
+ try {
3126
+ body = JSON.parse(raw);
3127
+ }
3128
+ catch {
3129
+ json(res, 400, { error: "invalid JSON body" });
3130
+ return;
3131
+ }
3132
+ const busy = Array.isArray(body.busyPaths)
3133
+ ? body.busyPaths.filter((p) => typeof p === "string")
3134
+ : [];
3135
+ json(res, 200, await sweepWorktrees(body.cwd?.trim() || cwd, busy));
3136
+ });
3137
+ return;
3138
+ }
3019
3139
  // Dismiss a project from the picker (or restore it with hidden:false). The
3020
3140
  // path is kept rather than the row: sessions in scratch dirs keep
3021
3141
  // re-surfacing, so the user's decision has to outlive any one scan.