@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 +1 -1
- package/dist/app-sidecar.js +120 -0
- package/dist/app-sidecar.js.map +1 -1
- package/dist/core/worktree.d.ts +161 -0
- package/dist/core/worktree.js +574 -0
- package/dist/core/worktree.js.map +1 -0
- package/dist/tools/subagent.d.ts +1 -0
- package/dist/tools/subagent.js +66 -2
- package/dist/tools/subagent.js.map +1 -1
- 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
|
|
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
|
|
package/dist/app-sidecar.js
CHANGED
|
@@ -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.
|