@cjhyy/code-shell-capability-coding 0.8.20 → 0.9.1
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.
|
@@ -2,6 +2,8 @@ import { Hunk } from "./types.js";
|
|
|
2
2
|
export interface ApplyPatchOptions {
|
|
3
3
|
/** Working directory for resolving relative paths in the patch. */
|
|
4
4
|
cwd: string;
|
|
5
|
+
/** Immutable roots authorized for this run. Relative patch paths still resolve from cwd. */
|
|
6
|
+
workspaceRoots?: readonly string[];
|
|
5
7
|
/**
|
|
6
8
|
* If true, skip the snapshot/rollback safety net and apply each planned
|
|
7
9
|
* change one at a time, leaving partial work on disk if a later write
|
|
@@ -27,10 +27,10 @@ export async function applyPatch(hunks, options) {
|
|
|
27
27
|
if (hunks.length === 0) {
|
|
28
28
|
throw new Error("No files were modified.");
|
|
29
29
|
}
|
|
30
|
-
const planned = await planHunks(hunks, options.cwd);
|
|
30
|
+
const planned = await planHunks(hunks, options.cwd, options.workspaceRoots ?? [options.cwd]);
|
|
31
31
|
return commitPlanned(planned, !!options.allowPartialOnCommit);
|
|
32
32
|
}
|
|
33
|
-
async function planHunks(hunks, cwd) {
|
|
33
|
+
async function planHunks(hunks, cwd, workspaceRoots) {
|
|
34
34
|
const set = {
|
|
35
35
|
byPath: new Map(),
|
|
36
36
|
added: [],
|
|
@@ -45,7 +45,7 @@ async function planHunks(hunks, cwd) {
|
|
|
45
45
|
}
|
|
46
46
|
for (const hunk of hunks) {
|
|
47
47
|
const original = hunk.path;
|
|
48
|
-
const sourcePath = resolveAgainst(original, cwd);
|
|
48
|
+
const sourcePath = resolveAgainst(original, cwd, workspaceRoots);
|
|
49
49
|
if (hunk.kind === "add") {
|
|
50
50
|
const existed = await readIfExists(sourcePath);
|
|
51
51
|
schedule(sourcePath, {
|
|
@@ -87,7 +87,7 @@ async function planHunks(hunks, cwd) {
|
|
|
87
87
|
const originalText = toLf(originalRaw);
|
|
88
88
|
const newText = applyEol(applyChunksToText(originalText, hunk.chunks, sourcePath), eol);
|
|
89
89
|
if (hunk.movePath !== undefined) {
|
|
90
|
-
const destPath = resolveAgainst(hunk.movePath, cwd);
|
|
90
|
+
const destPath = resolveAgainst(hunk.movePath, cwd, workspaceRoots);
|
|
91
91
|
if (destPath === sourcePath) {
|
|
92
92
|
// Rename to self → degenerate update.
|
|
93
93
|
schedule(sourcePath, {
|
|
@@ -248,12 +248,12 @@ async function writeChange(change) {
|
|
|
248
248
|
* symlink planted inside cwd that points outside — a plain string `resolve()`
|
|
249
249
|
* would look contained while the write follows the link out.
|
|
250
250
|
*/
|
|
251
|
-
function resolveAgainst(p, cwd) {
|
|
251
|
+
function resolveAgainst(p, cwd, workspaceRoots) {
|
|
252
252
|
const resolved = isAbsolute(p) ? p : resolve(cwd, p);
|
|
253
|
-
const realCwd = nearestRealPath(cwd);
|
|
254
253
|
const realTarget = nearestRealPath(resolved);
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
const realRoots = workspaceRoots.map(nearestRealPath);
|
|
255
|
+
if (!realRoots.some((root) => isInside(realTarget, root))) {
|
|
256
|
+
throw new Error(`Refusing to apply patch outside the workspace roots: "${p}" resolves to "${realTarget}".`);
|
|
257
257
|
}
|
|
258
258
|
return resolved;
|
|
259
259
|
}
|
|
@@ -72,7 +72,10 @@ export async function applyPatchTool(args, ctx) {
|
|
|
72
72
|
const cwd = ctx?.cwd ?? process.cwd();
|
|
73
73
|
let result;
|
|
74
74
|
try {
|
|
75
|
-
result = await applyPatch(parsed.hunks, {
|
|
75
|
+
result = await applyPatch(parsed.hunks, {
|
|
76
|
+
cwd,
|
|
77
|
+
workspaceRoots: ctx?.workspace?.roots.map((root) => root.path),
|
|
78
|
+
});
|
|
76
79
|
}
|
|
77
80
|
catch (err) {
|
|
78
81
|
return `Error applying patch: ${err.message}`;
|
|
@@ -275,6 +275,23 @@ function safeFinalizeDriveWorktree(managed) {
|
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Finalize a managed worktree once `run` settles, never before.
|
|
280
|
+
*
|
|
281
|
+
* Aborting a run only starts the driver's async process-tree teardown, so the
|
|
282
|
+
* external CLI can still be holding the worktree (and git's index.lock) when
|
|
283
|
+
* abort() returns. Cleaning up synchronously races that; waiting for the run to
|
|
284
|
+
* settle does not. Doubles as the rejection sink for an aborted run.
|
|
285
|
+
*/
|
|
286
|
+
function finalizeDriveWorktreeAfter(run, managed) {
|
|
287
|
+
void run
|
|
288
|
+
.catch(() => undefined)
|
|
289
|
+
.then(() => {
|
|
290
|
+
// safeFinalizeDriveWorktree already degrades a failed cleanup to a
|
|
291
|
+
// "kept" note rather than throwing, so nothing can escape this chain.
|
|
292
|
+
safeFinalizeDriveWorktree(managed);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
278
295
|
function summarizePrompt(prompt, max = 120) {
|
|
279
296
|
const oneLine = prompt.replace(/\s+/g, " ").trim();
|
|
280
297
|
if (oneLine.length <= max)
|
|
@@ -607,6 +624,16 @@ function attachDriveCompletion(params) {
|
|
|
607
624
|
});
|
|
608
625
|
}
|
|
609
626
|
function trackBackgroundRun(params) {
|
|
627
|
+
const { sessionId } = params;
|
|
628
|
+
if (!isValidSessionId(sessionId)) {
|
|
629
|
+
// Mirrors the background:true refusal above: a job whose completion
|
|
630
|
+
// notification cannot be routed would run and then vanish. Say so instead
|
|
631
|
+
// of registering work nobody can track. The caller aborts the run and
|
|
632
|
+
// finalizes any managed worktree on this error path.
|
|
633
|
+
return {
|
|
634
|
+
error: "Error: cannot track this DriveAgent run without a session — its result notification would be dropped. Re-run inside a session, or keep the task short enough to finish in the foreground.",
|
|
635
|
+
};
|
|
636
|
+
}
|
|
610
637
|
const conflict = duplicateCwdError(params.effectiveWorkspaceCwd, params.effectiveWorkspaceRoot, params.writable);
|
|
611
638
|
if (conflict)
|
|
612
639
|
return { error: conflict };
|
|
@@ -616,7 +643,7 @@ function trackBackgroundRun(params) {
|
|
|
616
643
|
// Publish ownership before starting the external process. Registry
|
|
617
644
|
// listeners are synchronous and may re-enter session teardown from the
|
|
618
645
|
// start notification; starting first would leave that process orphaned.
|
|
619
|
-
backgroundJobRegistry.start(jobId,
|
|
646
|
+
backgroundJobRegistry.start(jobId, sessionId, params.label, {
|
|
620
647
|
kind: "drive-agent",
|
|
621
648
|
launchCwd: params.cwd,
|
|
622
649
|
effectiveWorkspaceCwd: params.effectiveWorkspaceCwd,
|
|
@@ -660,7 +687,7 @@ function trackBackgroundRun(params) {
|
|
|
660
687
|
backgroundJobRegistry.finish(jobId, { status: "failed", finalText: message });
|
|
661
688
|
return { error: `Error: failed to start DriveAgent job: ${message}` };
|
|
662
689
|
}
|
|
663
|
-
attachDriveCompletion({ ...params, jobId, run });
|
|
690
|
+
attachDriveCompletion({ ...params, sessionId, jobId, run });
|
|
664
691
|
return { jobId };
|
|
665
692
|
}
|
|
666
693
|
async function waitForForegroundOrHandoff(run, handoffMs) {
|
|
@@ -963,13 +990,26 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
963
990
|
lease.release();
|
|
964
991
|
return appendLifecycleNote(`${cliName} 运行出错:${error instanceof Error ? error.message : String(error)}`, lifecycle);
|
|
965
992
|
}
|
|
966
|
-
if (result.kind === "handoff"
|
|
993
|
+
if (result.kind === "handoff") {
|
|
994
|
+
// Handing off is NOT gated on a valid ctx.sessionId. Gating it there left
|
|
995
|
+
// a session-less foreground run falling through to `await run`, blocking
|
|
996
|
+
// until the 30min tool cap whose abort kills the external CLI — the work
|
|
997
|
+
// was destroyed with no jobId and no notification path. trackBackgroundRun
|
|
998
|
+
// owns the session check instead: it registers a tracked job when the
|
|
999
|
+
// session can receive the completion, and otherwise returns an error the
|
|
1000
|
+
// branch below turns into an abort + deferred cleanup + explanation.
|
|
1001
|
+
//
|
|
967
1002
|
// Registration below is synchronous. Release the foreground lease and
|
|
968
1003
|
// replace it with the background registry entry in the same event-loop
|
|
969
|
-
// turn, so another dispatch cannot slip into a gap.
|
|
1004
|
+
// turn, so another dispatch cannot slip into a gap. The same invariant
|
|
1005
|
+
// covers the failure path: the lease is dropped and no job exists between
|
|
1006
|
+
// here and the return below, so that span MUST stay synchronous — an
|
|
1007
|
+
// await would open the gap to a concurrent writable dispatch. Worktree
|
|
1008
|
+
// finalization is deliberately NOT in that span: it is deferred to run
|
|
1009
|
+
// settlement, after this turn has already returned.
|
|
970
1010
|
lease.release();
|
|
971
1011
|
const tracked = trackBackgroundRun({
|
|
972
|
-
sessionId: ctx
|
|
1012
|
+
sessionId: ctx?.sessionId,
|
|
973
1013
|
label,
|
|
974
1014
|
cli,
|
|
975
1015
|
cwd,
|
|
@@ -993,9 +1033,16 @@ export function makeDriveAgentTool(runner = defaultRunner, fixedCli, options = {
|
|
|
993
1033
|
originClientMessageId: ctx?.originClientMessageId,
|
|
994
1034
|
});
|
|
995
1035
|
if ("error" in tracked) {
|
|
1036
|
+
// abort() only STARTS teardown (SIGTERM, grace, SIGKILL); the CLI is
|
|
1037
|
+
// still alive when it returns. Finalizing here would race the live
|
|
1038
|
+
// process for the worktree and git's index.lock, so defer cleanup to
|
|
1039
|
+
// run settlement and consume the abort rejection while we are at it.
|
|
1040
|
+
// The message must not claim a cleanup that has not happened yet.
|
|
996
1041
|
foregroundAbort.abort();
|
|
997
|
-
|
|
998
|
-
return
|
|
1042
|
+
finalizeDriveWorktreeAfter(run, managedWorktree);
|
|
1043
|
+
return managedWorktree
|
|
1044
|
+
? `${tracked.error}\n\n[worktree lifecycle] Cleaning up ${managedWorktree.session.worktreePath} once ${cliName} exits.`
|
|
1045
|
+
: tracked.error;
|
|
999
1046
|
}
|
|
1000
1047
|
ctx?.runYield?.request("background_notification");
|
|
1001
1048
|
return [
|
package/dist/tools/worktree.js
CHANGED
|
@@ -27,7 +27,7 @@ export async function switchSessionWorkspaceTool(args, ctx) {
|
|
|
27
27
|
const target = stringArg(args.target);
|
|
28
28
|
if (!target)
|
|
29
29
|
return "Error: target is required";
|
|
30
|
-
const bridge = ctx
|
|
30
|
+
const bridge = workspaceBridgeForContext(ctx);
|
|
31
31
|
if (!bridge) {
|
|
32
32
|
return ("SwitchSessionWorkspace is not available in this host. " +
|
|
33
33
|
"Use the host's supported workspace controls instead.");
|
|
@@ -48,6 +48,22 @@ export async function switchSessionWorkspaceTool(args, ctx) {
|
|
|
48
48
|
return `Error: switching workspace failed: ${err.message}`;
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function workspaceBridgeForContext(ctx) {
|
|
52
|
+
if (ctx?.workspaceBridge)
|
|
53
|
+
return ctx.workspaceBridge;
|
|
54
|
+
// Before multi-root authority landed, the host bridge occupied `workspace`.
|
|
55
|
+
// Keep those single-root hosts and injected harnesses working, but never
|
|
56
|
+
// interpret a versioned/root-bearing WorkspaceContext as an executable bridge.
|
|
57
|
+
const legacy = ctx?.workspace;
|
|
58
|
+
if (!legacy || typeof legacy !== "object")
|
|
59
|
+
return undefined;
|
|
60
|
+
if ("version" in legacy || "roots" in legacy || "projectId" in legacy)
|
|
61
|
+
return undefined;
|
|
62
|
+
const candidate = legacy;
|
|
63
|
+
return typeof candidate.switch === "function"
|
|
64
|
+
? candidate
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
51
67
|
export const enterWorktreeToolDef = {
|
|
52
68
|
name: "EnterWorktree",
|
|
53
69
|
description: "Switch the current session workspace. Target can be a new worktree slug, " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cjhyy/code-shell-capability-coding",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "Coding capability pack for the generic code-shell agent core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@cjhyy/code-shell-core": "0.
|
|
42
|
+
"@cjhyy/code-shell-core": "0.9.1"
|
|
43
43
|
},
|
|
44
44
|
"engines": {
|
|
45
45
|
"node": ">=20.10"
|