gsd-pi 2.78.1-dev.84a383f51 → 2.78.1-dev.8a893322c
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 -0
- package/dist/bundled-resource-path.d.ts +7 -0
- package/dist/bundled-resource-path.js +34 -2
- package/dist/claude-cli-check.js +18 -6
- package/dist/headless-query.js +21 -6
- package/dist/loader.js +2 -3
- package/dist/resource-loader.js +2 -8
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/readiness.js +19 -7
- package/dist/resources/extensions/google-search/index.js +2 -6
- package/dist/resources/extensions/gsd/auto/phases.js +3 -11
- package/dist/resources/extensions/gsd/auto/session.js +2 -6
- package/dist/resources/extensions/gsd/auto-dashboard.js +3 -2
- package/dist/resources/extensions/gsd/auto-dispatch.js +18 -6
- package/dist/resources/extensions/gsd/auto-prompts.js +63 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +30 -13
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +19 -1
- package/dist/resources/extensions/gsd/bootstrap/subagent-input.js +22 -0
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -0
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +84 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +8 -1
- package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
- package/dist/resources/extensions/gsd/commands/handlers/ops.js +8 -0
- package/dist/resources/extensions/gsd/commands-config.js +3 -2
- package/dist/resources/extensions/gsd/commands-extensions.js +46 -3
- package/dist/resources/extensions/gsd/commands-handlers.js +3 -2
- package/dist/resources/extensions/gsd/commands-worktree.js +309 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/dist/resources/extensions/gsd/doctor-providers.js +2 -1
- package/dist/resources/extensions/gsd/forensics.js +8 -6
- package/dist/resources/extensions/gsd/guided-flow.js +2 -1
- package/dist/resources/extensions/gsd/home-dir.js +16 -0
- package/dist/resources/extensions/gsd/key-manager.js +2 -1
- package/dist/resources/extensions/gsd/migrate/command.js +3 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/dist/resources/extensions/gsd/unit-context-manifest.js +29 -4
- package/dist/resources/extensions/gsd/worktree-manager.js +20 -1
- package/dist/resources/extensions/gsd/worktree-resolver.js +4 -13
- package/dist/resources/extensions/gsd/worktree-root.js +124 -0
- package/dist/resources/extensions/gsd/worktree.js +4 -115
- package/dist/resources/extensions/mcp-client/index.js +0 -6
- package/dist/resources/extensions/ollama/index.js +15 -2
- package/dist/resources/extensions/ollama/model-capabilities.js +31 -0
- package/dist/resources/extensions/ollama/ollama-client.js +40 -4
- package/dist/resources/extensions/subagent/index.js +324 -178
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/welcome-screen.js +27 -1
- package/dist/worktree-cli.d.ts +1 -0
- package/dist/worktree-cli.js +9 -3
- package/package.json +1 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +52 -0
- package/packages/native/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/readiness.ts +20 -7
- package/src/resources/extensions/google-search/index.ts +2 -9
- package/src/resources/extensions/gsd/auto/phases.ts +3 -11
- package/src/resources/extensions/gsd/auto/session.ts +2 -6
- package/src/resources/extensions/gsd/auto-dashboard.ts +3 -2
- package/src/resources/extensions/gsd/auto-dispatch.ts +18 -6
- package/src/resources/extensions/gsd/auto-prompts.ts +60 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +44 -12
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +19 -0
- package/src/resources/extensions/gsd/bootstrap/subagent-input.ts +20 -0
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +103 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +8 -1
- package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
- package/src/resources/extensions/gsd/commands/handlers/ops.ts +10 -0
- package/src/resources/extensions/gsd/commands-config.ts +3 -2
- package/src/resources/extensions/gsd/commands-extensions.ts +43 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +3 -2
- package/src/resources/extensions/gsd/commands-worktree.ts +383 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +6 -0
- package/src/resources/extensions/gsd/doctor-providers.ts +2 -1
- package/src/resources/extensions/gsd/forensics.ts +10 -5
- package/src/resources/extensions/gsd/guided-flow.ts +2 -1
- package/src/resources/extensions/gsd/home-dir.ts +19 -0
- package/src/resources/extensions/gsd/journal.ts +4 -1
- package/src/resources/extensions/gsd/key-manager.ts +2 -1
- package/src/resources/extensions/gsd/migrate/command.ts +3 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +10 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +10 -0
- package/src/resources/extensions/gsd/prompts/refine-slice.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/bundled-skill-triggers.test.ts +50 -27
- package/src/resources/extensions/gsd/tests/commands-extensions-version-compare.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/commands-worktree-clean.test.ts +48 -0
- package/src/resources/extensions/gsd/tests/google-search-stub.test.ts +25 -65
- package/src/resources/extensions/gsd/tests/home-dir.test.ts +52 -0
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/milestone-report-path.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +17 -1
- package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +38 -3
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +34 -33
- package/src/resources/extensions/gsd/tests/worktree.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +116 -1
- package/src/resources/extensions/gsd/unit-context-manifest.ts +36 -4
- package/src/resources/extensions/gsd/worktree-manager.ts +40 -1
- package/src/resources/extensions/gsd/worktree-resolver.ts +4 -14
- package/src/resources/extensions/gsd/worktree-root.ts +144 -0
- package/src/resources/extensions/gsd/worktree.ts +8 -119
- package/src/resources/extensions/mcp-client/index.ts +0 -7
- package/src/resources/extensions/ollama/index.ts +16 -2
- package/src/resources/extensions/ollama/model-capabilities.ts +34 -0
- package/src/resources/extensions/ollama/ollama-client.ts +41 -4
- package/src/resources/extensions/ollama/tests/model-capabilities.test.ts +96 -0
- package/src/resources/extensions/ollama/tests/ollama-client-timeout-env.test.ts +147 -0
- package/src/resources/extensions/subagent/index.ts +165 -7
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{UF5VF4F1tB0miEtJS7LyX → QK8fABiGPmonfTgboN0Y9}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// GSD-2 — In-TUI handler for /gsd worktree commands (list, merge, clean, remove).
|
|
2
|
+
//
|
|
3
|
+
// Mirrors the CLI subcommands in src/worktree-cli.ts but emits results via
|
|
4
|
+
// ctx.ui.notify() instead of writing colored output to stderr. Reuses the
|
|
5
|
+
// same extension modules (worktree-manager, native-git-bridge, etc.) so the
|
|
6
|
+
// behavior is identical to the CLI surface.
|
|
7
|
+
|
|
8
|
+
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
|
|
11
|
+
import { projectRoot } from "./commands/context.js";
|
|
12
|
+
import {
|
|
13
|
+
listWorktrees,
|
|
14
|
+
removeWorktree,
|
|
15
|
+
mergeWorktreeToMain,
|
|
16
|
+
diffWorktreeAll,
|
|
17
|
+
diffWorktreeNumstat,
|
|
18
|
+
worktreeBranchName,
|
|
19
|
+
} from "./worktree-manager.js";
|
|
20
|
+
import {
|
|
21
|
+
nativeHasChanges,
|
|
22
|
+
nativeDetectMainBranch,
|
|
23
|
+
nativeCommitCountBetween,
|
|
24
|
+
} from "./native-git-bridge.js";
|
|
25
|
+
import { inferCommitType } from "./git-service.js";
|
|
26
|
+
import { autoCommitCurrentBranch } from "./worktree.js";
|
|
27
|
+
import { GSDError, GSD_GIT_ERROR } from "./errors.js";
|
|
28
|
+
|
|
29
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface WorktreeStatus {
|
|
32
|
+
name: string;
|
|
33
|
+
path: string;
|
|
34
|
+
branch: string;
|
|
35
|
+
exists: boolean;
|
|
36
|
+
filesChanged: number;
|
|
37
|
+
linesAdded: number;
|
|
38
|
+
linesRemoved: number;
|
|
39
|
+
uncommitted: boolean;
|
|
40
|
+
commits: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ─── Status helper ─────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function getStatus(basePath: string, name: string, wtPath: string): WorktreeStatus {
|
|
46
|
+
const diff = diffWorktreeAll(basePath, name);
|
|
47
|
+
const numstat = diffWorktreeNumstat(basePath, name);
|
|
48
|
+
const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
|
|
49
|
+
let linesAdded = 0;
|
|
50
|
+
let linesRemoved = 0;
|
|
51
|
+
for (const s of numstat) {
|
|
52
|
+
linesAdded += s.added;
|
|
53
|
+
linesRemoved += s.removed;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let uncommitted = false;
|
|
57
|
+
try {
|
|
58
|
+
uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
|
|
59
|
+
} catch {
|
|
60
|
+
// native check failure → treat as clean for display purposes
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let commits = 0;
|
|
64
|
+
try {
|
|
65
|
+
const main = nativeDetectMainBranch(basePath);
|
|
66
|
+
commits = nativeCommitCountBetween(basePath, main, worktreeBranchName(name));
|
|
67
|
+
} catch {
|
|
68
|
+
// commit count unavailable → leave at 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
name,
|
|
73
|
+
path: wtPath,
|
|
74
|
+
branch: worktreeBranchName(name),
|
|
75
|
+
exists: existsSync(wtPath),
|
|
76
|
+
filesChanged,
|
|
77
|
+
linesAdded,
|
|
78
|
+
linesRemoved,
|
|
79
|
+
uncommitted,
|
|
80
|
+
commits,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Formatters (exported for tests) ────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export function formatWorktreeList(statuses: WorktreeStatus[]): string {
|
|
87
|
+
if (statuses.length === 0) {
|
|
88
|
+
return "No worktrees.\n\nCreate one from the CLI: gsd -w <name>";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const lines: string[] = [`Worktrees — ${statuses.length}`, ""];
|
|
92
|
+
for (const s of statuses) {
|
|
93
|
+
const badge = s.uncommitted
|
|
94
|
+
? "(uncommitted)"
|
|
95
|
+
: s.filesChanged > 0
|
|
96
|
+
? "(unmerged)"
|
|
97
|
+
: "(clean)";
|
|
98
|
+
lines.push(` ${s.name} ${badge}`);
|
|
99
|
+
lines.push(` branch ${s.branch}`);
|
|
100
|
+
lines.push(` path ${s.path}`);
|
|
101
|
+
if (s.filesChanged > 0) {
|
|
102
|
+
lines.push(
|
|
103
|
+
` diff ${s.filesChanged} file${s.filesChanged === 1 ? "" : "s"}, +${s.linesAdded} -${s.linesRemoved}, ${s.commits} commit${s.commits === 1 ? "" : "s"}`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
lines.push("Commands:");
|
|
109
|
+
lines.push(" /gsd worktree merge <name> Merge into main and clean up");
|
|
110
|
+
lines.push(" /gsd worktree remove <name> Remove a worktree (--force to skip safety checks)");
|
|
111
|
+
lines.push(" /gsd worktree clean Remove all merged/empty worktrees");
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function formatCleanKeepReason(status: WorktreeStatus): string {
|
|
116
|
+
if (!status.exists) {
|
|
117
|
+
return "directory missing — run 'git worktree prune' to unregister";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (status.filesChanged > 0) {
|
|
121
|
+
return `${status.filesChanged} changed file${status.filesChanged === 1 ? "" : "s"}${status.uncommitted ? ", uncommitted" : ""}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return "uncommitted changes";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── Subcommand: list ───────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async function handleList(ctx: ExtensionCommandContext): Promise<void> {
|
|
130
|
+
const basePath = projectRoot();
|
|
131
|
+
const worktrees = listWorktrees(basePath);
|
|
132
|
+
const statuses = worktrees.map((wt) => getStatus(basePath, wt.name, wt.path));
|
|
133
|
+
ctx.ui.notify(formatWorktreeList(statuses), "info");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Subcommand: merge ──────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
async function handleMerge(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
139
|
+
const basePath = projectRoot();
|
|
140
|
+
const worktrees = listWorktrees(basePath);
|
|
141
|
+
const trimmed = args.trim();
|
|
142
|
+
|
|
143
|
+
let target = trimmed;
|
|
144
|
+
if (!target) {
|
|
145
|
+
if (worktrees.length === 1) {
|
|
146
|
+
target = worktrees[0].name;
|
|
147
|
+
} else if (worktrees.length === 0) {
|
|
148
|
+
ctx.ui.notify("No worktrees to merge.", "info");
|
|
149
|
+
return;
|
|
150
|
+
} else {
|
|
151
|
+
const names = worktrees.map((w) => w.name).join(", ");
|
|
152
|
+
ctx.ui.notify(`Usage: /gsd worktree merge <name>\n\nWorktrees: ${names}`, "warning");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const wt = worktrees.find((w) => w.name === target);
|
|
158
|
+
if (!wt) {
|
|
159
|
+
const available = worktrees.map((w) => w.name).join(", ") || "(none)";
|
|
160
|
+
ctx.ui.notify(`Worktree "${target}" not found.\n\nAvailable: ${available}`, "error");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const status = getStatus(basePath, target, wt.path);
|
|
165
|
+
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
166
|
+
try {
|
|
167
|
+
removeWorktree(basePath, target, { deleteBranch: true });
|
|
168
|
+
ctx.ui.notify(`Removed empty worktree ${target}.`, "info");
|
|
169
|
+
} catch (err) {
|
|
170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
171
|
+
ctx.ui.notify(
|
|
172
|
+
`Worktree partially removed: ${msg}\n\nRun 'git worktree prune' to clean up any dangling registrations.`,
|
|
173
|
+
"error",
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (status.uncommitted) {
|
|
180
|
+
try {
|
|
181
|
+
autoCommitCurrentBranch(wt.path, "worktree-merge", target);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
184
|
+
ctx.ui.notify(
|
|
185
|
+
[
|
|
186
|
+
`Auto-commit before merge failed: ${msg}`,
|
|
187
|
+
"",
|
|
188
|
+
`Commit or stash changes in ${wt.path}, then re-run /gsd worktree merge ${target}.`,
|
|
189
|
+
].join("\n"),
|
|
190
|
+
"error",
|
|
191
|
+
);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const commitType = inferCommitType(target);
|
|
197
|
+
const mainBranch = nativeDetectMainBranch(basePath);
|
|
198
|
+
const commitMessage = `${commitType}: merge worktree ${target}\n\nGSD-Worktree: ${target}`;
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
mergeWorktreeToMain(basePath, target, commitMessage);
|
|
202
|
+
} catch (err) {
|
|
203
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
204
|
+
if (err instanceof GSDError && err.code === GSD_GIT_ERROR) {
|
|
205
|
+
ctx.ui.notify(
|
|
206
|
+
`Merge requires the main branch to be checked out: ${msg}\n\nSwitch to ${mainBranch} (e.g. 'git checkout ${mainBranch}'), then re-run /gsd worktree merge ${target}.`,
|
|
207
|
+
"error",
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
ctx.ui.notify(
|
|
211
|
+
`Merge failed: ${msg}\n\nResolve conflicts manually, then run /gsd worktree merge ${target} again.`,
|
|
212
|
+
"error",
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const successLines = [
|
|
219
|
+
`Merged ${target} → ${mainBranch}`,
|
|
220
|
+
` ${status.filesChanged} file${status.filesChanged === 1 ? "" : "s"}, +${status.linesAdded} -${status.linesRemoved}`,
|
|
221
|
+
` commit: ${commitMessage.split("\n")[0]}`,
|
|
222
|
+
];
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
removeWorktree(basePath, target, { deleteBranch: true });
|
|
226
|
+
ctx.ui.notify(successLines.join("\n"), "info");
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
229
|
+
const cleanupLines = [
|
|
230
|
+
...successLines,
|
|
231
|
+
"",
|
|
232
|
+
`Cleanup failed after the merge succeeded: ${msg}`,
|
|
233
|
+
err instanceof GSDError && err.code === GSD_GIT_ERROR
|
|
234
|
+
? `Switch to ${mainBranch} (e.g. 'git checkout ${mainBranch}'), then remove the worktree manually with /gsd worktree remove ${target} --force.`
|
|
235
|
+
: `Remove the worktree manually with /gsd worktree remove ${target} --force, or run 'git worktree prune' to clean up dangling registrations.`,
|
|
236
|
+
];
|
|
237
|
+
ctx.ui.notify(cleanupLines.join("\n"), "warning");
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Subcommand: clean ──────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
async function handleClean(ctx: ExtensionCommandContext): Promise<void> {
|
|
244
|
+
const basePath = projectRoot();
|
|
245
|
+
const worktrees = listWorktrees(basePath);
|
|
246
|
+
if (worktrees.length === 0) {
|
|
247
|
+
ctx.ui.notify("No worktrees to clean.", "info");
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const removed: string[] = [];
|
|
252
|
+
const kept: string[] = [];
|
|
253
|
+
for (const wt of worktrees) {
|
|
254
|
+
const status = getStatus(basePath, wt.name, wt.path);
|
|
255
|
+
if (status.filesChanged === 0 && !status.uncommitted) {
|
|
256
|
+
try {
|
|
257
|
+
removeWorktree(basePath, wt.name, { deleteBranch: true });
|
|
258
|
+
removed.push(wt.name);
|
|
259
|
+
} catch (err) {
|
|
260
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
261
|
+
kept.push(`${wt.name} (failed: ${msg})`);
|
|
262
|
+
}
|
|
263
|
+
} else {
|
|
264
|
+
const reason = formatCleanKeepReason(status);
|
|
265
|
+
kept.push(`${wt.name} (${reason})`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const lines: string[] = [`Cleaned ${removed.length} worktree${removed.length === 1 ? "" : "s"}.`];
|
|
270
|
+
if (removed.length > 0) {
|
|
271
|
+
lines.push("", "Removed:");
|
|
272
|
+
for (const n of removed) lines.push(` ✓ ${n}`);
|
|
273
|
+
}
|
|
274
|
+
if (kept.length > 0) {
|
|
275
|
+
lines.push("", "Kept:");
|
|
276
|
+
for (const n of kept) lines.push(` ─ ${n}`);
|
|
277
|
+
}
|
|
278
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ─── Subcommand: remove ─────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async function handleRemove(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
284
|
+
const basePath = projectRoot();
|
|
285
|
+
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
286
|
+
const force = tokens.includes("--force");
|
|
287
|
+
const name = tokens.find((t) => t !== "--force");
|
|
288
|
+
if (!name) {
|
|
289
|
+
ctx.ui.notify("Usage: /gsd worktree remove <name> [--force]", "warning");
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const worktrees = listWorktrees(basePath);
|
|
294
|
+
const wt = worktrees.find((w) => w.name === name);
|
|
295
|
+
if (!wt) {
|
|
296
|
+
const available = worktrees.map((w) => w.name).join(", ") || "(none)";
|
|
297
|
+
ctx.ui.notify(`Worktree "${name}" not found.\n\nAvailable: ${available}`, "error");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const status = getStatus(basePath, name, wt.path);
|
|
302
|
+
if ((status.filesChanged > 0 || status.uncommitted) && !force) {
|
|
303
|
+
ctx.ui.notify(
|
|
304
|
+
[
|
|
305
|
+
`Worktree "${name}" has pending changes (${formatCleanKeepReason(status)}).`,
|
|
306
|
+
"",
|
|
307
|
+
` Merge first: /gsd worktree merge ${name}`,
|
|
308
|
+
` Or force-remove: /gsd worktree remove ${name} --force`,
|
|
309
|
+
].join("\n"),
|
|
310
|
+
"warning",
|
|
311
|
+
);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
removeWorktree(basePath, name, { deleteBranch: true });
|
|
317
|
+
ctx.ui.notify(`Removed worktree ${name}.`, "info");
|
|
318
|
+
} catch (err) {
|
|
319
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
320
|
+
ctx.ui.notify(
|
|
321
|
+
`Worktree partially removed: ${msg}\n\nRun 'git worktree prune' to clean up any dangling registrations.`,
|
|
322
|
+
"error",
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ─── Help text ──────────────────────────────────────────────────────────────
|
|
328
|
+
|
|
329
|
+
const HELP_TEXT = [
|
|
330
|
+
"Usage: /gsd worktree <command> [args]",
|
|
331
|
+
"",
|
|
332
|
+
"Commands:",
|
|
333
|
+
" list Show all worktrees with status",
|
|
334
|
+
" merge [name] Merge a worktree into main, then remove it",
|
|
335
|
+
" remove <name> [--force] Remove a worktree (refuses unmerged changes without --force)",
|
|
336
|
+
" clean Remove all merged/empty worktrees",
|
|
337
|
+
"",
|
|
338
|
+
"The -w flag (CLI only) creates/resumes worktrees on session start:",
|
|
339
|
+
" gsd -w Auto-name a new worktree, or resume the only active one",
|
|
340
|
+
" gsd -w my-feature Create or resume a named worktree",
|
|
341
|
+
].join("\n");
|
|
342
|
+
|
|
343
|
+
// ─── Dispatcher ─────────────────────────────────────────────────────────────
|
|
344
|
+
|
|
345
|
+
export async function handleWorktree(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
346
|
+
const trimmed = args.trim();
|
|
347
|
+
const lowered = trimmed.toLowerCase();
|
|
348
|
+
|
|
349
|
+
if (!lowered || lowered === "help" || lowered === "--help" || lowered === "-h") {
|
|
350
|
+
ctx.ui.notify(HELP_TEXT, "info");
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
if (lowered === "list" || lowered === "ls") {
|
|
356
|
+
await handleList(ctx);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (lowered === "merge" || lowered.startsWith("merge ")) {
|
|
360
|
+
await handleMerge(trimmed.replace(/^merge\s*/i, ""), ctx);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (lowered === "clean") {
|
|
364
|
+
await handleClean(ctx);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (
|
|
368
|
+
lowered === "remove" ||
|
|
369
|
+
lowered.startsWith("remove ") ||
|
|
370
|
+
lowered === "rm" ||
|
|
371
|
+
lowered.startsWith("rm ")
|
|
372
|
+
) {
|
|
373
|
+
const stripped = trimmed.replace(/^(remove|rm)\s*/i, "");
|
|
374
|
+
await handleRemove(stripped, ctx);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
ctx.ui.notify(`Unknown worktree command: ${trimmed}\n\n${HELP_TEXT}`, "warning");
|
|
379
|
+
} catch (err) {
|
|
380
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
381
|
+
ctx.ui.notify(`Worktree command failed: ${msg}`, "error");
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -165,6 +165,12 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
|
|
|
165
165
|
- `skip_reassess`: boolean — force-disable roadmap reassessment even if `reassess_after_slice` is enabled. Default: `false`.
|
|
166
166
|
- `skip_slice_research`: boolean — skip per-slice research. Default: `false`.
|
|
167
167
|
|
|
168
|
+
- `reactive_execution`: controls automatic parallel task dispatch inside a slice. Reactive execution is enabled by default when omitted; set `enabled: false` to opt out. With default-on behavior, GSD only attempts a reactive batch when at least three ready tasks are available and the task-plan IO graph is non-ambiguous. If you set `enabled: true` explicitly, GSD uses the earlier opt-in threshold of two ready tasks. Keys:
|
|
169
|
+
- `enabled`: boolean — set `false` to force sequential task execution. Default: `true`.
|
|
170
|
+
- `max_parallel`: number — maximum tasks to dispatch in one batch, range `1`-`8`. Default: `2`.
|
|
171
|
+
- `isolation_mode`: `"same-tree"` — currently the only supported value.
|
|
172
|
+
- `subagent_model`: string — optional model override for reactive task subagents. Falls back to the `models.subagent` routing when omitted.
|
|
173
|
+
|
|
168
174
|
- `remote_questions`: route interactive questions to Slack/Discord for headless auto-mode. Keys:
|
|
169
175
|
- `channel`: `"slack"` or `"discord"` — channel type.
|
|
170
176
|
- `channel_id`: string or number — channel ID.
|
|
@@ -17,6 +17,7 @@ import { AuthStorage } from "@gsd/pi-coding-agent";
|
|
|
17
17
|
import { getEnvApiKey } from "@gsd/pi-ai";
|
|
18
18
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
19
19
|
import { getAuthPath, PROVIDER_REGISTRY, type ProviderCategory } from "./key-manager.js";
|
|
20
|
+
import { getHomeDir } from "./home-dir.js";
|
|
20
21
|
|
|
21
22
|
// ── Types ──────────────────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -194,7 +195,7 @@ function isCliBinaryInPath(providerId: string): boolean {
|
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
function modelsJsonPaths(): string[] {
|
|
197
|
-
const home =
|
|
198
|
+
const home = getHomeDir();
|
|
198
199
|
return [
|
|
199
200
|
join(home, ".gsd", "agent", "models.json"),
|
|
200
201
|
// Keep parity with custom-provider discovery during auto bootstrap.
|
|
@@ -12,7 +12,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
|
|
|
12
12
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join, dirname, relative } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import {
|
|
15
|
+
import { getHomeDir } from "./home-dir.js";
|
|
16
16
|
|
|
17
17
|
import { extractTrace, type ExecutionTrace } from "./session-forensics.js";
|
|
18
18
|
import { nativeParseJsonlTail } from "./native-parser-bridge.js";
|
|
@@ -244,7 +244,7 @@ export async function handleForensics(
|
|
|
244
244
|
// when import.meta.url resolves to the npm-global install path (Windows).
|
|
245
245
|
let gsdSourceDir = dirname(fileURLToPath(import.meta.url));
|
|
246
246
|
if (!existsSync(join(gsdSourceDir, "prompts"))) {
|
|
247
|
-
const gsdHome = process.env.GSD_HOME || join(
|
|
247
|
+
const gsdHome = process.env.GSD_HOME || join(getHomeDir(), ".gsd");
|
|
248
248
|
const fallback = join(gsdHome, "agent", "extensions", "gsd");
|
|
249
249
|
if (existsSync(join(fallback, "prompts"))) gsdSourceDir = fallback;
|
|
250
250
|
}
|
|
@@ -1299,10 +1299,15 @@ function formatReportForPrompt(report: ForensicReport): string {
|
|
|
1299
1299
|
function redactForGitHub(text: string, basePath: string): string {
|
|
1300
1300
|
let result = text;
|
|
1301
1301
|
|
|
1302
|
+
// Build regex that matches both / and \ separator variants (Windows)
|
|
1303
|
+
// Normalize to / first, escape for regex, then replace each / with [/\\]
|
|
1304
|
+
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1305
|
+
const pathRe = (p: string) =>
|
|
1306
|
+
new RegExp(esc(p.replace(/\\/g, "/")).replace(/\//g, "[/\\\\]"), "gi");
|
|
1307
|
+
|
|
1302
1308
|
// Replace absolute paths
|
|
1303
|
-
result = result.
|
|
1304
|
-
|
|
1305
|
-
if (home) result = result.replaceAll(home, "~");
|
|
1309
|
+
result = result.replace(pathRe(basePath), ".");
|
|
1310
|
+
result = result.replace(pathRe(getHomeDir()), "~");
|
|
1306
1311
|
|
|
1307
1312
|
// Strip API key patterns
|
|
1308
1313
|
result = result.replace(/sk-[a-zA-Z0-9]{20,}/g, "sk-***");
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
} from "./interrupted-session.js";
|
|
26
26
|
import { listUnitRuntimeRecords, clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
27
27
|
import { resolveExpectedArtifactPath } from "./auto.js";
|
|
28
|
+
import { getHomeDir } from "./home-dir.js";
|
|
28
29
|
import {
|
|
29
30
|
gsdRoot, milestonesDir, resolveMilestoneFile, resolveMilestonePath,
|
|
30
31
|
resolveSliceFile, resolveSlicePath, resolveGsdRootFile, relGsdRootFile,
|
|
@@ -607,7 +608,7 @@ async function dispatchWorkflow(
|
|
|
607
608
|
});
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(
|
|
611
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(getHomeDir(), ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
611
612
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
612
613
|
|
|
613
614
|
pi.sendMessage(
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform home directory resolution.
|
|
3
|
+
*
|
|
4
|
+
* `process.env.HOME` is not set on Windows (CMD/PowerShell).
|
|
5
|
+
* Falls back to USERPROFILE, then os.homedir(), then throws.
|
|
6
|
+
*
|
|
7
|
+
* @see https://github.com/gsd-build/gsd-2/issues/5015
|
|
8
|
+
*/
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
export function getHomeDir(): string {
|
|
12
|
+
const home = process.env.HOME || process.env.USERPROFILE || homedir();
|
|
13
|
+
if (!home) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Cannot resolve home directory. Set HOME or USERPROFILE environment variable.",
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return home;
|
|
19
|
+
}
|
|
@@ -60,7 +60,10 @@ export type JournalEventType =
|
|
|
60
60
|
| "canonical-root-redirect"
|
|
61
61
|
// #4765 — slice-cadence collapse
|
|
62
62
|
| "slice-merged"
|
|
63
|
-
| "milestone-resquash"
|
|
63
|
+
| "milestone-resquash"
|
|
64
|
+
// dispatch telemetry — measure agent/subagent invocation frequency and shape
|
|
65
|
+
| "subagent-invoked"
|
|
66
|
+
| "subagent-completed";
|
|
64
67
|
|
|
65
68
|
/** A single structured event in the journal. */
|
|
66
69
|
export interface JournalEntry {
|
|
@@ -17,6 +17,7 @@ import { existsSync, statSync, chmodSync } from "node:fs";
|
|
|
17
17
|
import { join, dirname } from "node:path";
|
|
18
18
|
import { mkdirSync } from "node:fs";
|
|
19
19
|
import { getErrorMessage } from "./error-utils.js";
|
|
20
|
+
import { getHomeDir } from "./home-dir.js";
|
|
20
21
|
|
|
21
22
|
// ─── Provider Registry ─────────────────────────────────────────────────────────
|
|
22
23
|
|
|
@@ -122,7 +123,7 @@ export function describeCredential(cred: AuthCredential): string {
|
|
|
122
123
|
* Get the auth.json path.
|
|
123
124
|
*/
|
|
124
125
|
export function getAuthPath(): string {
|
|
125
|
-
return join(
|
|
126
|
+
return join(getHomeDir(), ".gsd", "agent", "auth.json");
|
|
126
127
|
}
|
|
127
128
|
|
|
128
129
|
/**
|
|
@@ -15,6 +15,7 @@ import { resolve, join, dirname } from "node:path";
|
|
|
15
15
|
import { gsdRoot } from "../paths.js";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
17
|
import { showNextAction } from "../../shared/tui.js";
|
|
18
|
+
import { getHomeDir } from "../home-dir.js";
|
|
18
19
|
import {
|
|
19
20
|
validatePlanningDirectory,
|
|
20
21
|
parsePlanningDirectory,
|
|
@@ -85,9 +86,9 @@ export async function handleMigrate(
|
|
|
85
86
|
// Default to cwd when no args given; expand ~ to HOME
|
|
86
87
|
let rawPath = args.trim() || ".";
|
|
87
88
|
if (rawPath.startsWith("~/")) {
|
|
88
|
-
rawPath = join(
|
|
89
|
+
rawPath = join(getHomeDir(), rawPath.slice(2));
|
|
89
90
|
} else if (rawPath === "~") {
|
|
90
|
-
rawPath =
|
|
91
|
+
rawPath = getHomeDir();
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
let sourcePath = resolve(process.cwd(), rawPath);
|
|
@@ -16,6 +16,16 @@ Start with what the excerpts give you. Read full files when the section heads si
|
|
|
16
16
|
|
|
17
17
|
**On-demand Read ordering:** Complete all slice SUMMARY Reads you need for cross-slice synthesis, the Decision Re-evaluation table, and LEARNINGS **before** calling `gsd_complete_milestone` (step 10). Once that tool runs, the milestone is marked complete in the DB — running out of tool budget between step 10 and the LEARNINGS write (step 12) leaves the milestone committed without its LEARNINGS artifact.
|
|
18
18
|
|
|
19
|
+
### Delegate Review Work
|
|
20
|
+
|
|
21
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. For non-trivial milestones, delegate before drafting LEARNINGS:
|
|
22
|
+
|
|
23
|
+
- **Cross-slice integrations or new public APIs** → dispatch the **reviewer** agent with the milestone diff and roadmap; treat its findings as input to your Decision Re-evaluation and LEARNINGS sections.
|
|
24
|
+
- **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit across the merged slices.
|
|
25
|
+
- **Significant test surface added or changed** → dispatch the **tester** agent to assess coverage gaps relative to the milestone success criteria.
|
|
26
|
+
|
|
27
|
+
Subagents read the diff and report findings — they do **not** write user source. Apply their feedback into the milestone summary and any captured decisions before calling `gsd_complete_milestone`.
|
|
28
|
+
|
|
19
29
|
{{inlinedContext}}
|
|
20
30
|
|
|
21
31
|
Then:
|
|
@@ -20,6 +20,16 @@ All relevant context has been preloaded below — the slice plan, all task summa
|
|
|
20
20
|
|
|
21
21
|
**Match effort to complexity.** A simple slice with 1-2 tasks needs a brief summary and lightweight verification. A complex slice with 5 tasks across multiple subsystems needs thorough verification and a detailed summary. Scale the work below accordingly.
|
|
22
22
|
|
|
23
|
+
### Delegate Review Work
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate review work that benefits from a fresh context window. Strongly consider delegating when the slice is non-trivial:
|
|
26
|
+
|
|
27
|
+
- **Cross-cutting code or new abstractions** → dispatch the **reviewer** agent with the slice diff and plan; apply High/Critical findings before completing.
|
|
28
|
+
- **Touched auth, network, parsing, file IO, shell exec, or crypto** → dispatch the **security** agent for an OWASP-style audit.
|
|
29
|
+
- **Added or modified tests** → dispatch the **tester** agent to assess coverage gaps relative to the slice plan.
|
|
30
|
+
|
|
31
|
+
Subagents read the diff and report findings — they do **not** write user source. You remain responsible for acting on their feedback before calling `gsd_complete_slice` with `milestoneId` and `sliceId`.
|
|
32
|
+
|
|
23
33
|
Then:
|
|
24
34
|
1. Use the **Slice Summary** and **UAT** output templates from the inlined context above
|
|
25
35
|
2. {{skillActivation}}
|
|
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
20
20
|
|
|
21
21
|
You have full tool access. Before decomposing, explore the relevant code to ground your plan in reality.
|
|
22
22
|
|
|
23
|
+
### Delegate Recon and Sub-Decomposition When Useful
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate work that benefits from an isolated context window. Prefer delegation over inline work when:
|
|
26
|
+
|
|
27
|
+
- You'd otherwise read more than ~3 files to understand a subsystem → dispatch the **scout** agent for codebase recon and work from its compressed report.
|
|
28
|
+
- The slice spans multiple subsystems and the decomposition isn't obvious → dispatch the **planner** agent or use the **decompose-into-slices** skill on a focused sub-area, then integrate.
|
|
29
|
+
- You need current external information (library docs, API behavior, recent changes) → dispatch the **researcher** agent.
|
|
30
|
+
|
|
31
|
+
**Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) from this unit — they would write user source and bypass this unit's write isolation. Implementation belongs in `execute-task`.
|
|
32
|
+
|
|
23
33
|
### Verify Roadmap Assumptions (JIT Reassessment — ADR-003 §4)
|
|
24
34
|
|
|
25
35
|
Before planning this slice, verify that the roadmap's assumptions still hold given prior slice summaries. Check inlined dependency summaries (below) for discovered constraints, changed approaches, or flagged fragility.
|
|
@@ -20,6 +20,16 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
20
20
|
|
|
21
21
|
## Your Role in the Pipeline
|
|
22
22
|
|
|
23
|
+
### Delegate Recon When Useful
|
|
24
|
+
|
|
25
|
+
This unit runs under the `planning-dispatch` tools-policy: you may use the `subagent` tool to delegate recon and sub-decomposition. Prefer delegation over inline work when:
|
|
26
|
+
|
|
27
|
+
- You'd otherwise read more than ~3 files to understand a subsystem touched by the sketch → dispatch the **scout** agent and work from its compressed report.
|
|
28
|
+
- A specific area of the refinement needs deeper architectural analysis → dispatch the **planner** agent for a focused sub-plan, then integrate.
|
|
29
|
+
- You need current external information (library docs, API behavior) → dispatch the **researcher** agent.
|
|
30
|
+
|
|
31
|
+
**Do not** dispatch implementation-tier agents (`worker`, `refactorer`, `tester`) — they would write user source and bypass write isolation. Implementation belongs in `execute-task`.
|
|
32
|
+
|
|
23
33
|
### Respect the Sketch Scope
|
|
24
34
|
|
|
25
35
|
The sketch scope inlined above is a **hard constraint**. Plan within it. If, after exploring the codebase, the scope is too narrow to deliver the goal, surface this as a deviation in the plan's narrative and still produce the plan — do not silently expand the scope.
|
|
@@ -14,6 +14,7 @@ import assert from "node:assert/strict";
|
|
|
14
14
|
import { readFileSync } from "node:fs";
|
|
15
15
|
import { join, dirname } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { AutoSession } from "../auto/session.ts";
|
|
17
18
|
|
|
18
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const AUTO_TS_PATH = join(__dirname, "..", "auto.ts");
|
|
@@ -32,6 +33,20 @@ function getRuntimeStateTsSource(): string {
|
|
|
32
33
|
return readFileSync(RUNTIME_STATE_TS_PATH, "utf-8");
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
test("AutoSession.lockBasePath uses GSD_PROJECT_ROOT for symlink-resolved worktrees", () => {
|
|
37
|
+
const savedProjectRoot = process.env.GSD_PROJECT_ROOT;
|
|
38
|
+
process.env.GSD_PROJECT_ROOT = "/real/project";
|
|
39
|
+
try {
|
|
40
|
+
const session = new AutoSession();
|
|
41
|
+
session.basePath = "/Users/dev/.gsd/projects/abc123/worktrees/M001/slices/S01";
|
|
42
|
+
|
|
43
|
+
assert.equal(session.lockBasePath, "/real/project");
|
|
44
|
+
} finally {
|
|
45
|
+
if (savedProjectRoot === undefined) delete process.env.GSD_PROJECT_ROOT;
|
|
46
|
+
else process.env.GSD_PROJECT_ROOT = savedProjectRoot;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
35
50
|
// ── Invariant 1: No module-level mutable variables in auto.ts ────────────────
|
|
36
51
|
|
|
37
52
|
test("auto.ts has no module-level let declarations", () => {
|