gsd-pi 2.32.0-dev.1e39869 → 2.32.0-dev.d792ba5
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 +27 -20
- package/dist/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/dist/resources/extensions/gsd/index.ts +6 -0
- package/dist/resources/extensions/gsd/migrate-external.ts +18 -2
- package/dist/resources/extensions/gsd/quick.ts +56 -2
- package/dist/resources/extensions/gsd/repo-identity.ts +22 -1
- package/package.json +1 -1
- package/src/resources/extensions/gsd/crash-recovery.ts +15 -2
- package/src/resources/extensions/gsd/index.ts +6 -0
- package/src/resources/extensions/gsd/migrate-external.ts +18 -2
- package/src/resources/extensions/gsd/quick.ts +56 -2
- package/src/resources/extensions/gsd/repo-identity.ts +22 -1
package/README.md
CHANGED
|
@@ -24,21 +24,24 @@ One command. Walk away. Come back to a built project with clean git history.
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
-
## What's New in v2.
|
|
28
|
-
|
|
29
|
-
- **
|
|
30
|
-
-
|
|
31
|
-
- **
|
|
32
|
-
-
|
|
33
|
-
- **
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
40
|
-
- **
|
|
41
|
-
-
|
|
27
|
+
## What's New in v2.32
|
|
28
|
+
|
|
29
|
+
- **Simplified pipeline** — research merged into planning, mechanical completion (ADR-003)
|
|
30
|
+
- **Always-on health widget** — 🟢🟡🔴 traffic-light indicator in the progress widget and visualizer health tab
|
|
31
|
+
- **Environment health checks** — progress scoring and status integration for auto-mode
|
|
32
|
+
- **Extension registry** — user-managed enable/disable for bundled and custom extensions
|
|
33
|
+
- **Built-in skill authoring** — create and distribute custom skills from within GSD
|
|
34
|
+
- **Workflow templates** — right-sized workflows for every task type (research, plan, execute, complete)
|
|
35
|
+
- **AWS Bedrock auth** — automatic credential refresh via the new `aws-auth` extension
|
|
36
|
+
- **`-w` / `--worktree` CLI flag** — launch isolated worktree sessions from the command line
|
|
37
|
+
- **Native MCP client** — replaced MCPorter with a built-in MCP client for better reliability
|
|
38
|
+
- **External state directory** — `.gsd/` now lives in `~/.gsd/projects/` with a symlink (ADR-002)
|
|
39
|
+
- **Model health indicator** — live health status based on error trends and consecutive failures
|
|
40
|
+
- **Quick-task branch cleanup** — `/gsd quick` branches auto-merge back after completion
|
|
41
|
+
- **Windows EPERM fallback** — migration rename uses copy+delete when NTFS blocks rename
|
|
42
|
+
- **Worktree identity fix** — stable project hash across worktrees and main repo
|
|
43
|
+
- **Crash recovery guidance** — actionable next-step messages based on what was interrupted
|
|
44
|
+
- **UAT verdict gating** — non-PASS verdicts now block slice progression instead of being ignored
|
|
42
45
|
|
|
43
46
|
See the full [Changelog](./CHANGELOG.md) for details.
|
|
44
47
|
|
|
@@ -65,6 +68,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
|
|
|
65
68
|
- **[Visualizer](./docs/visualizer.md)** — workflow visualizer with stats and discussion status
|
|
66
69
|
- **[Remote Questions](./docs/remote-questions.md)** — route decisions to Slack or Discord when human input is needed
|
|
67
70
|
- **[Dynamic Model Routing](./docs/dynamic-model-routing.md)** — complexity-based model selection and budget pressure
|
|
71
|
+
- **[Pipeline Simplification (ADR-003)](./docs/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
|
|
68
72
|
- **[Migration from v1](./docs/migration.md)** — `.planning` → `.gsd` migration
|
|
69
73
|
|
|
70
74
|
---
|
|
@@ -141,12 +145,12 @@ The iron rule: **a task must fit in one context window.** If it can't, it's two
|
|
|
141
145
|
Each slice flows through phases automatically:
|
|
142
146
|
|
|
143
147
|
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
148
|
+
Plan (with integrated research) → Execute (per task) → Complete → Reassess Roadmap → Next Slice
|
|
149
|
+
↓ (all slices done)
|
|
150
|
+
Validate Milestone → Complete Milestone
|
|
147
151
|
```
|
|
148
152
|
|
|
149
|
-
**
|
|
153
|
+
**Plan** scouts the codebase, researches relevant docs, and decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded — then runs configured verification commands (lint, test, etc.) with auto-fix retries. **Complete** writes the summary, UAT script, marks the roadmap, and commits with meaningful messages derived from task summaries. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
|
|
150
154
|
|
|
151
155
|
### `/gsd auto` — The Main Event
|
|
152
156
|
|
|
@@ -326,6 +330,7 @@ On first run, GSD launches a branded setup wizard that walks you through LLM pro
|
|
|
326
330
|
| `gsd headless [cmd]` | Run `/gsd` commands without TUI (CI, cron, scripts) |
|
|
327
331
|
| `gsd headless query` | Instant JSON snapshot — state, next dispatch, costs (no LLM) |
|
|
328
332
|
| `gsd --continue` (`-c`) | Resume the most recent session for the current directory |
|
|
333
|
+
| `gsd --worktree` (`-w`) | Launch an isolated worktree session for the active milestone |
|
|
329
334
|
| `gsd sessions` | Interactive session picker — browse and resume any saved session |
|
|
330
335
|
|
|
331
336
|
---
|
|
@@ -483,7 +488,7 @@ See the full [Token Optimization Guide](./docs/token-optimization.md) for detail
|
|
|
483
488
|
|
|
484
489
|
### Bundled Tools
|
|
485
490
|
|
|
486
|
-
GSD ships with
|
|
491
|
+
GSD ships with 18 extensions, all loaded automatically:
|
|
487
492
|
|
|
488
493
|
| Extension | What it provides |
|
|
489
494
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
@@ -503,6 +508,8 @@ GSD ships with 16 extensions, all loaded automatically:
|
|
|
503
508
|
| **Secure Env Collect** | Masked secret collection without manual .env editing |
|
|
504
509
|
| **Remote Questions** | Route decisions to Slack/Discord when human input is needed in headless/CI mode |
|
|
505
510
|
| **Universal Config** | Discover and import MCP servers and rules from other AI coding tools |
|
|
511
|
+
| **AWS Auth** | Automatic Bedrock credential refresh for AWS-hosted models |
|
|
512
|
+
| **TTSR** | Tool-use type-safe runtime validation |
|
|
506
513
|
|
|
507
514
|
### Bundled Agents
|
|
508
515
|
|
|
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
|
|
|
98
98
|
|
|
99
99
|
/** Format crash info for display or injection into a prompt. */
|
|
100
100
|
export function formatCrashInfo(lock: LockData): string {
|
|
101
|
-
|
|
101
|
+
const lines = [
|
|
102
102
|
`Previous auto-mode session was interrupted.`,
|
|
103
103
|
` Was executing: ${lock.unitType} (${lock.unitId})`,
|
|
104
104
|
` Started at: ${lock.unitStartedAt}`,
|
|
105
105
|
` Units completed before crash: ${lock.completedUnits}`,
|
|
106
106
|
` PID: ${lock.pid}`,
|
|
107
|
-
]
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Add recovery guidance based on what was happening when it crashed
|
|
110
|
+
if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
|
|
111
|
+
lines.push(`No work was lost. Run /gsd auto to restart.`);
|
|
112
|
+
} else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
|
|
113
|
+
lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
|
|
114
|
+
} else if (lock.unitType.includes("execute")) {
|
|
115
|
+
lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
|
|
116
|
+
} else if (lock.unitType.includes("complete")) {
|
|
117
|
+
lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
108
121
|
}
|
|
@@ -795,6 +795,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
795
795
|
|
|
796
796
|
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
797
797
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
798
|
+
// Clean up quick-task branch if one just completed (#1269)
|
|
799
|
+
try {
|
|
800
|
+
const { cleanupQuickBranch } = await import("./quick.js");
|
|
801
|
+
cleanupQuickBranch();
|
|
802
|
+
} catch { /* non-fatal */ }
|
|
803
|
+
|
|
798
804
|
// If discuss phase just finished, start auto-mode
|
|
799
805
|
if (checkAutoStartAfterDiscuss()) {
|
|
800
806
|
depthVerifiedMilestones.clear();
|
|
@@ -57,8 +57,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
57
57
|
// mkdir -p the external dir
|
|
58
58
|
mkdirSync(externalPath, { recursive: true });
|
|
59
59
|
|
|
60
|
-
// Rename .gsd -> .gsd.migrating (atomic lock)
|
|
61
|
-
|
|
60
|
+
// Rename .gsd -> .gsd.migrating (atomic lock).
|
|
61
|
+
// On Windows, NTFS may reject rename with EPERM if file descriptors are
|
|
62
|
+
// open (VS Code watchers, antivirus on-access scan). Fall back to
|
|
63
|
+
// copy+delete (#1292).
|
|
64
|
+
try {
|
|
65
|
+
renameSync(localGsd, migratingPath);
|
|
66
|
+
} catch (renameErr: any) {
|
|
67
|
+
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
|
|
68
|
+
try {
|
|
69
|
+
cpSync(localGsd, migratingPath, { recursive: true, force: true });
|
|
70
|
+
rmSync(localGsd, { recursive: true, force: true });
|
|
71
|
+
} catch (copyErr) {
|
|
72
|
+
return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
throw renameErr;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
// Copy contents to external dir, skipping worktrees/
|
|
64
80
|
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
@@ -107,10 +107,11 @@ export async function handleQuick(
|
|
|
107
107
|
const skipBranch = git.prefs.isolation === "none";
|
|
108
108
|
|
|
109
109
|
let branchCreated = false;
|
|
110
|
+
let originalBranch: string | undefined;
|
|
110
111
|
if (!skipBranch) {
|
|
111
112
|
try {
|
|
112
|
-
|
|
113
|
-
if (
|
|
113
|
+
originalBranch = git.getCurrentBranch();
|
|
114
|
+
if (originalBranch !== branchName) {
|
|
114
115
|
// Auto-commit any dirty state before switching
|
|
115
116
|
try {
|
|
116
117
|
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
@@ -154,4 +155,57 @@ export async function handleQuick(
|
|
|
154
155
|
},
|
|
155
156
|
{ triggerTurn: true },
|
|
156
157
|
);
|
|
158
|
+
|
|
159
|
+
// Schedule branch merge-back after the quick task agent session ends.
|
|
160
|
+
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
161
|
+
if (branchCreated && originalBranch) {
|
|
162
|
+
_pendingQuickBranchReturn = {
|
|
163
|
+
basePath,
|
|
164
|
+
originalBranch,
|
|
165
|
+
quickBranch: branchName,
|
|
166
|
+
taskNum,
|
|
167
|
+
slug,
|
|
168
|
+
description,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
174
|
+
let _pendingQuickBranchReturn: {
|
|
175
|
+
basePath: string;
|
|
176
|
+
originalBranch: string;
|
|
177
|
+
quickBranch: string;
|
|
178
|
+
taskNum: number;
|
|
179
|
+
slug: string;
|
|
180
|
+
description: string;
|
|
181
|
+
} | null = null;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Merge the quick-task branch back to the original branch and switch.
|
|
185
|
+
* Called from the agent_end handler after a quick task completes.
|
|
186
|
+
* Returns true if a branch return was performed.
|
|
187
|
+
*/
|
|
188
|
+
export function cleanupQuickBranch(): boolean {
|
|
189
|
+
if (!_pendingQuickBranchReturn) return false;
|
|
190
|
+
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
|
|
191
|
+
_pendingQuickBranchReturn = null;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Auto-commit any remaining work
|
|
195
|
+
try { runGit(basePath, ["add", "-A"]); } catch {}
|
|
196
|
+
try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
|
|
197
|
+
|
|
198
|
+
// Switch back and merge
|
|
199
|
+
runGit(basePath, ["checkout", originalBranch]);
|
|
200
|
+
try {
|
|
201
|
+
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
202
|
+
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
203
|
+
} catch { /* merge conflict or nothing — non-fatal */ }
|
|
204
|
+
|
|
205
|
+
// Clean up quick branch
|
|
206
|
+
try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
157
211
|
}
|
|
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
13
|
+
import { join, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
15
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
|
|
|
37
37
|
*/
|
|
38
38
|
function resolveGitRoot(basePath: string): string {
|
|
39
39
|
try {
|
|
40
|
+
// In a worktree, --show-toplevel returns the worktree path, not the main
|
|
41
|
+
// repo root. Use --git-common-dir to find the shared .git directory,
|
|
42
|
+
// then derive the main repo root from it (#1288).
|
|
43
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
44
|
+
cwd: basePath,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
timeout: 5_000,
|
|
48
|
+
}).trim();
|
|
49
|
+
|
|
50
|
+
// If commonDir ends with .git/worktrees/<name>, the main repo is two
|
|
51
|
+
// levels up from the worktrees dir. If it's just .git, resolve normally.
|
|
52
|
+
if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
|
|
53
|
+
// e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
|
|
54
|
+
// or /path/to/project/.git/worktrees/M001 → /path/to/project
|
|
55
|
+
const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
|
|
56
|
+
const mainRoot = resolve(gitDir, "..");
|
|
57
|
+
return mainRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Not in a worktree — use --show-toplevel as usual
|
|
40
61
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
62
|
cwd: basePath,
|
|
42
63
|
encoding: "utf-8",
|
package/package.json
CHANGED
|
@@ -98,11 +98,24 @@ export function isLockProcessAlive(lock: LockData): boolean {
|
|
|
98
98
|
|
|
99
99
|
/** Format crash info for display or injection into a prompt. */
|
|
100
100
|
export function formatCrashInfo(lock: LockData): string {
|
|
101
|
-
|
|
101
|
+
const lines = [
|
|
102
102
|
`Previous auto-mode session was interrupted.`,
|
|
103
103
|
` Was executing: ${lock.unitType} (${lock.unitId})`,
|
|
104
104
|
` Started at: ${lock.unitStartedAt}`,
|
|
105
105
|
` Units completed before crash: ${lock.completedUnits}`,
|
|
106
106
|
` PID: ${lock.pid}`,
|
|
107
|
-
]
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
// Add recovery guidance based on what was happening when it crashed
|
|
110
|
+
if (lock.unitType === "starting" && lock.unitId === "bootstrap" && lock.completedUnits === 0) {
|
|
111
|
+
lines.push(`No work was lost. Run /gsd auto to restart.`);
|
|
112
|
+
} else if (lock.unitType.includes("research") || lock.unitType.includes("plan")) {
|
|
113
|
+
lines.push(`The ${lock.unitType} unit may be incomplete. Run /gsd auto to re-run it.`);
|
|
114
|
+
} else if (lock.unitType.includes("execute")) {
|
|
115
|
+
lines.push(`Task execution was interrupted. Run /gsd auto to resume — completed work is preserved.`);
|
|
116
|
+
} else if (lock.unitType.includes("complete")) {
|
|
117
|
+
lines.push(`Slice/milestone completion was interrupted. Run /gsd auto to finish.`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return lines.join("\n");
|
|
108
121
|
}
|
|
@@ -795,6 +795,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
795
795
|
|
|
796
796
|
// ── agent_end: auto-mode advancement or auto-start after discuss ───────────
|
|
797
797
|
pi.on("agent_end", async (event, ctx: ExtensionContext) => {
|
|
798
|
+
// Clean up quick-task branch if one just completed (#1269)
|
|
799
|
+
try {
|
|
800
|
+
const { cleanupQuickBranch } = await import("./quick.js");
|
|
801
|
+
cleanupQuickBranch();
|
|
802
|
+
} catch { /* non-fatal */ }
|
|
803
|
+
|
|
798
804
|
// If discuss phase just finished, start auto-mode
|
|
799
805
|
if (checkAutoStartAfterDiscuss()) {
|
|
800
806
|
depthVerifiedMilestones.clear();
|
|
@@ -57,8 +57,24 @@ export function migrateToExternalState(basePath: string): MigrationResult {
|
|
|
57
57
|
// mkdir -p the external dir
|
|
58
58
|
mkdirSync(externalPath, { recursive: true });
|
|
59
59
|
|
|
60
|
-
// Rename .gsd -> .gsd.migrating (atomic lock)
|
|
61
|
-
|
|
60
|
+
// Rename .gsd -> .gsd.migrating (atomic lock).
|
|
61
|
+
// On Windows, NTFS may reject rename with EPERM if file descriptors are
|
|
62
|
+
// open (VS Code watchers, antivirus on-access scan). Fall back to
|
|
63
|
+
// copy+delete (#1292).
|
|
64
|
+
try {
|
|
65
|
+
renameSync(localGsd, migratingPath);
|
|
66
|
+
} catch (renameErr: any) {
|
|
67
|
+
if (renameErr?.code === "EPERM" || renameErr?.code === "EBUSY") {
|
|
68
|
+
try {
|
|
69
|
+
cpSync(localGsd, migratingPath, { recursive: true, force: true });
|
|
70
|
+
rmSync(localGsd, { recursive: true, force: true });
|
|
71
|
+
} catch (copyErr) {
|
|
72
|
+
return { migrated: false, error: `Migration rename/copy failed: ${copyErr instanceof Error ? copyErr.message : String(copyErr)}` };
|
|
73
|
+
}
|
|
74
|
+
} else {
|
|
75
|
+
throw renameErr;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
62
78
|
|
|
63
79
|
// Copy contents to external dir, skipping worktrees/
|
|
64
80
|
const entries = readdirSync(migratingPath, { withFileTypes: true });
|
|
@@ -107,10 +107,11 @@ export async function handleQuick(
|
|
|
107
107
|
const skipBranch = git.prefs.isolation === "none";
|
|
108
108
|
|
|
109
109
|
let branchCreated = false;
|
|
110
|
+
let originalBranch: string | undefined;
|
|
110
111
|
if (!skipBranch) {
|
|
111
112
|
try {
|
|
112
|
-
|
|
113
|
-
if (
|
|
113
|
+
originalBranch = git.getCurrentBranch();
|
|
114
|
+
if (originalBranch !== branchName) {
|
|
114
115
|
// Auto-commit any dirty state before switching
|
|
115
116
|
try {
|
|
116
117
|
git.autoCommit("quick-task", `Q${taskNum}`, []);
|
|
@@ -154,4 +155,57 @@ export async function handleQuick(
|
|
|
154
155
|
},
|
|
155
156
|
{ triggerTurn: true },
|
|
156
157
|
);
|
|
158
|
+
|
|
159
|
+
// Schedule branch merge-back after the quick task agent session ends.
|
|
160
|
+
// Without this, auto-mode resumes on the quick-task branch (#1269).
|
|
161
|
+
if (branchCreated && originalBranch) {
|
|
162
|
+
_pendingQuickBranchReturn = {
|
|
163
|
+
basePath,
|
|
164
|
+
originalBranch,
|
|
165
|
+
quickBranch: branchName,
|
|
166
|
+
taskNum,
|
|
167
|
+
slug,
|
|
168
|
+
description,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Pending quick-task branch return — consumed by cleanupQuickBranch(). */
|
|
174
|
+
let _pendingQuickBranchReturn: {
|
|
175
|
+
basePath: string;
|
|
176
|
+
originalBranch: string;
|
|
177
|
+
quickBranch: string;
|
|
178
|
+
taskNum: number;
|
|
179
|
+
slug: string;
|
|
180
|
+
description: string;
|
|
181
|
+
} | null = null;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Merge the quick-task branch back to the original branch and switch.
|
|
185
|
+
* Called from the agent_end handler after a quick task completes.
|
|
186
|
+
* Returns true if a branch return was performed.
|
|
187
|
+
*/
|
|
188
|
+
export function cleanupQuickBranch(): boolean {
|
|
189
|
+
if (!_pendingQuickBranchReturn) return false;
|
|
190
|
+
const { basePath, originalBranch, quickBranch, taskNum, slug, description } = _pendingQuickBranchReturn;
|
|
191
|
+
_pendingQuickBranchReturn = null;
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
// Auto-commit any remaining work
|
|
195
|
+
try { runGit(basePath, ["add", "-A"]); } catch {}
|
|
196
|
+
try { runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${slug}`]); } catch {}
|
|
197
|
+
|
|
198
|
+
// Switch back and merge
|
|
199
|
+
runGit(basePath, ["checkout", originalBranch]);
|
|
200
|
+
try {
|
|
201
|
+
runGit(basePath, ["merge", "--squash", quickBranch]);
|
|
202
|
+
runGit(basePath, ["commit", "-m", `quick(Q${taskNum}): ${description.slice(0, 72)}`]);
|
|
203
|
+
} catch { /* merge conflict or nothing — non-fatal */ }
|
|
204
|
+
|
|
205
|
+
// Clean up quick branch
|
|
206
|
+
try { runGit(basePath, ["branch", "-D", quickBranch]); } catch {}
|
|
207
|
+
return true;
|
|
208
|
+
} catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
157
211
|
}
|
|
@@ -10,7 +10,7 @@ import { createHash } from "node:crypto";
|
|
|
10
10
|
import { execFileSync } from "node:child_process";
|
|
11
11
|
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs";
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
|
-
import { join, resolve } from "node:path";
|
|
13
|
+
import { join, resolve, sep } from "node:path";
|
|
14
14
|
|
|
15
15
|
// ─── Repo Identity ──────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -37,6 +37,27 @@ function getRemoteUrl(basePath: string): string {
|
|
|
37
37
|
*/
|
|
38
38
|
function resolveGitRoot(basePath: string): string {
|
|
39
39
|
try {
|
|
40
|
+
// In a worktree, --show-toplevel returns the worktree path, not the main
|
|
41
|
+
// repo root. Use --git-common-dir to find the shared .git directory,
|
|
42
|
+
// then derive the main repo root from it (#1288).
|
|
43
|
+
const commonDir = execFileSync("git", ["rev-parse", "--git-common-dir"], {
|
|
44
|
+
cwd: basePath,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
47
|
+
timeout: 5_000,
|
|
48
|
+
}).trim();
|
|
49
|
+
|
|
50
|
+
// If commonDir ends with .git/worktrees/<name>, the main repo is two
|
|
51
|
+
// levels up from the worktrees dir. If it's just .git, resolve normally.
|
|
52
|
+
if (commonDir.includes(`${sep}worktrees${sep}`) || commonDir.includes("/worktrees/")) {
|
|
53
|
+
// e.g., /path/to/project/.gsd/worktrees/M001/.git → /path/to/project
|
|
54
|
+
// or /path/to/project/.git/worktrees/M001 → /path/to/project
|
|
55
|
+
const gitDir = commonDir.replace(/[/\\]worktrees[/\\][^/\\]+$/, "");
|
|
56
|
+
const mainRoot = resolve(gitDir, "..");
|
|
57
|
+
return mainRoot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Not in a worktree — use --show-toplevel as usual
|
|
40
61
|
return execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
62
|
cwd: basePath,
|
|
42
63
|
encoding: "utf-8",
|