gsd-pi 2.13.1 → 2.14.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 +3 -3
- package/dist/cli.js +1 -0
- package/dist/loader.js +50 -6
- package/dist/resource-loader.d.ts +7 -6
- package/dist/resource-loader.js +15 -8
- package/dist/resources/extensions/gsd/auto-worktree.ts +25 -182
- package/dist/resources/extensions/gsd/auto.ts +252 -370
- package/dist/resources/extensions/gsd/commands.ts +118 -34
- package/dist/resources/extensions/gsd/doctor.ts +24 -1
- package/dist/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/dist/resources/extensions/gsd/git-service.ts +8 -431
- package/dist/resources/extensions/gsd/gitignore.ts +11 -4
- package/dist/resources/extensions/gsd/guided-flow.ts +141 -5
- package/dist/resources/extensions/gsd/preferences.ts +18 -17
- package/dist/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/dist/resources/extensions/gsd/prompts/queue.md +7 -1
- package/dist/resources/extensions/gsd/state.ts +26 -8
- package/dist/resources/extensions/gsd/templates/state.md +0 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +1 -105
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +10 -90
- package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/dist/resources/extensions/gsd/types.ts +0 -1
- package/dist/resources/extensions/gsd/worktree.ts +7 -65
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/providers/google.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +12 -4
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +10 -2
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/src/providers/google.ts +20 -8
- package/packages/pi-ai/src/providers/mistral.ts +14 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +10 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js +4 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/extension-input.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +12 -3
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -9
- package/packages/pi-coding-agent/src/modes/interactive/components/extension-input.ts +4 -1
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +14 -3
- package/packages/pi-tui/dist/components/input.d.ts +1 -0
- package/packages/pi-tui/dist/components/input.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/input.js +10 -0
- package/packages/pi-tui/dist/components/input.js.map +1 -1
- package/packages/pi-tui/src/components/input.ts +11 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +25 -182
- package/src/resources/extensions/gsd/auto.ts +252 -370
- package/src/resources/extensions/gsd/commands.ts +118 -34
- package/src/resources/extensions/gsd/doctor.ts +24 -1
- package/src/resources/extensions/gsd/git-self-heal.ts +0 -71
- package/src/resources/extensions/gsd/git-service.ts +8 -431
- package/src/resources/extensions/gsd/gitignore.ts +11 -4
- package/src/resources/extensions/gsd/guided-flow.ts +141 -5
- package/src/resources/extensions/gsd/preferences.ts +18 -17
- package/src/resources/extensions/gsd/prompts/discuss.md +35 -0
- package/src/resources/extensions/gsd/prompts/queue.md +7 -1
- package/src/resources/extensions/gsd/state.ts +26 -8
- package/src/resources/extensions/gsd/templates/state.md +0 -1
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +35 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +2 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +1 -105
- package/src/resources/extensions/gsd/tests/git-service.test.ts +11 -770
- package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +21 -113
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +16 -82
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +29 -50
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +10 -90
- package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +28 -55
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -426
- package/src/resources/extensions/gsd/types.ts +0 -1
- package/src/resources/extensions/gsd/worktree.ts +7 -65
- package/src/resources/extensions/search-the-web/command-search-provider.ts +3 -1
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +0 -282
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +0 -107
- package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +0 -353
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ GSD v2 solves all of these because it's not a prompt framework anymore — it's
|
|
|
38
38
|
| Context management | Hope the LLM doesn't fill up | Fresh session per task, programmatic |
|
|
39
39
|
| Auto mode | LLM self-loop | State machine reading `.gsd/` files |
|
|
40
40
|
| Crash recovery | None | Lock files + session forensics |
|
|
41
|
-
| Git strategy | LLM writes git commands |
|
|
41
|
+
| Git strategy | LLM writes git commands | Worktree isolation, sequential commits, squash merge |
|
|
42
42
|
| Cost tracking | None | Per-unit token/cost ledger with dashboard |
|
|
43
43
|
| Stuck detection | None | Retry once, then stop with diagnostics |
|
|
44
44
|
| Timeout supervision | None | Soft/idle/hard timeouts with recovery steering |
|
|
@@ -111,7 +111,7 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
|
|
|
111
111
|
|
|
112
112
|
2. **Context pre-loading** — The dispatch prompt includes inlined task plans, slice plans, prior task summaries, dependency summaries, roadmap excerpts, and decisions register. The LLM starts with everything it needs instead of spending tool calls reading files.
|
|
113
113
|
|
|
114
|
-
3. **Git
|
|
114
|
+
3. **Git worktree isolation** — Each milestone runs in its own git worktree with a `milestone/<MID>` branch. All slice work commits sequentially — no branch switching, no merge conflicts. When the milestone completes, it's squash-merged to main as one clean commit.
|
|
115
115
|
|
|
116
116
|
4. **Crash recovery** — A lock file tracks the current unit. If the session dies, the next `/gsd auto` reads the surviving session file, synthesizes a recovery briefing from every tool call that made it to disk, and resumes with full context.
|
|
117
117
|
|
|
@@ -268,7 +268,7 @@ gsd/M001/S01 (deleted after merge):
|
|
|
268
268
|
feat(S01/T01): core types and interfaces
|
|
269
269
|
```
|
|
270
270
|
|
|
271
|
-
One commit per
|
|
271
|
+
One squash commit per milestone on main (or whichever branch you started from). The worktree is torn down after merge. Git bisect works. Individual milestones are revertable.
|
|
272
272
|
|
|
273
273
|
### Verification
|
|
274
274
|
|
package/dist/cli.js
CHANGED
|
@@ -91,6 +91,7 @@ const isPrintMode = cliFlags.print || cliFlags.mode !== undefined;
|
|
|
91
91
|
// `gsd config` — replay the setup wizard and exit
|
|
92
92
|
if (cliFlags.messages[0] === 'config') {
|
|
93
93
|
const authStorage = AuthStorage.create(authFilePath);
|
|
94
|
+
loadStoredEnvKeys(authStorage);
|
|
94
95
|
await runOnboarding(authStorage);
|
|
95
96
|
process.exit(0);
|
|
96
97
|
}
|
package/dist/loader.js
CHANGED
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// GSD Startup Loader
|
|
3
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
2
4
|
import { fileURLToPath } from 'url';
|
|
3
5
|
import { dirname, resolve, join, delimiter } from 'path';
|
|
4
6
|
import { existsSync, readFileSync, readdirSync, mkdirSync, symlinkSync } from 'fs';
|
|
7
|
+
// Fast-path: handle --version/-v and --help/-h before importing any heavy
|
|
8
|
+
// dependencies. This avoids loading the entire pi-coding-agent barrel import
|
|
9
|
+
// (~1s) just to print a version string.
|
|
10
|
+
const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const firstArg = args[0];
|
|
13
|
+
if (firstArg === '--version' || firstArg === '-v') {
|
|
14
|
+
try {
|
|
15
|
+
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'));
|
|
16
|
+
process.stdout.write((pkg.version || '0.0.0') + '\n');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
process.stdout.write('0.0.0\n');
|
|
20
|
+
}
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
if (firstArg === '--help' || firstArg === '-h') {
|
|
24
|
+
let version = '0.0.0';
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(join(gsdRoot, 'package.json'), 'utf-8'));
|
|
27
|
+
version = pkg.version || version;
|
|
28
|
+
}
|
|
29
|
+
catch { /* ignore */ }
|
|
30
|
+
process.stdout.write(`GSD v${version} — Get Shit Done\n\n`);
|
|
31
|
+
process.stdout.write('Usage: gsd [options] [message...]\n\n');
|
|
32
|
+
process.stdout.write('Options:\n');
|
|
33
|
+
process.stdout.write(' --mode <text|json|rpc> Output mode (default: interactive)\n');
|
|
34
|
+
process.stdout.write(' --print, -p Single-shot print mode\n');
|
|
35
|
+
process.stdout.write(' --continue, -c Resume the most recent session\n');
|
|
36
|
+
process.stdout.write(' --model <id> Override model (e.g. claude-opus-4-6)\n');
|
|
37
|
+
process.stdout.write(' --no-session Disable session persistence\n');
|
|
38
|
+
process.stdout.write(' --extension <path> Load additional extension\n');
|
|
39
|
+
process.stdout.write(' --tools <a,b,c> Restrict available tools\n');
|
|
40
|
+
process.stdout.write(' --list-models [search] List available models and exit\n');
|
|
41
|
+
process.stdout.write(' --version, -v Print version and exit\n');
|
|
42
|
+
process.stdout.write(' --help, -h Print this help and exit\n');
|
|
43
|
+
process.stdout.write('\nSubcommands:\n');
|
|
44
|
+
process.stdout.write(' config Re-run the setup wizard\n');
|
|
45
|
+
process.stdout.write(' update Update GSD to the latest version\n');
|
|
46
|
+
process.exit(0);
|
|
47
|
+
}
|
|
5
48
|
import { agentDir, appRoot } from './app-paths.js';
|
|
6
49
|
import { serializeBundledExtensionPaths } from './bundled-extension-paths.js';
|
|
7
50
|
import { renderLogo } from './logo.js';
|
|
@@ -40,7 +83,6 @@ process.env.GSD_CODING_AGENT_DIR = agentDir;
|
|
|
40
83
|
// Without this, extensions (e.g. browser-tools) can't resolve dependencies like
|
|
41
84
|
// `playwright` because jiti resolves modules from pi-coding-agent's location, not gsd's.
|
|
42
85
|
// Prepending gsd's node_modules to NODE_PATH fixes this for all extensions.
|
|
43
|
-
const gsdRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
44
86
|
const gsdNodeModules = join(gsdRoot, 'node_modules');
|
|
45
87
|
process.env.NODE_PATH = [gsdNodeModules, process.env.NODE_PATH]
|
|
46
88
|
.filter(Boolean)
|
|
@@ -64,9 +106,8 @@ process.env.GSD_BIN_PATH = process.argv[1];
|
|
|
64
106
|
// GSD_WORKFLOW_PATH — absolute path to bundled GSD-WORKFLOW.md, used by patched gsd extension
|
|
65
107
|
// when dispatching workflow prompts. Prefers dist/resources/ (stable, set at build time)
|
|
66
108
|
// over src/resources/ (live working tree) — see resource-loader.ts for rationale.
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const srcRes = join(loaderPackageRoot, 'src', 'resources');
|
|
109
|
+
const distRes = join(gsdRoot, 'dist', 'resources');
|
|
110
|
+
const srcRes = join(gsdRoot, 'src', 'resources');
|
|
70
111
|
const resourcesDir = existsSync(distRes) ? distRes : srcRes;
|
|
71
112
|
process.env.GSD_WORKFLOW_PATH = join(resourcesDir, 'GSD-WORKFLOW.md');
|
|
72
113
|
// GSD_BUNDLED_EXTENSION_PATHS — dynamically discovered bundled extension entry points.
|
|
@@ -105,8 +146,11 @@ process.env.GSD_BUNDLED_EXTENSION_PATHS = serializeBundledExtensionPaths(discove
|
|
|
105
146
|
// Respect HTTP_PROXY / HTTPS_PROXY / NO_PROXY env vars for all outbound requests.
|
|
106
147
|
// pi-coding-agent's cli.ts sets this, but GSD bypasses that entry point — so we
|
|
107
148
|
// must set it here before any SDK clients are created.
|
|
108
|
-
|
|
109
|
-
|
|
149
|
+
// Lazy-load undici (~200ms) only when proxy env vars are actually set.
|
|
150
|
+
if (process.env.HTTP_PROXY || process.env.HTTPS_PROXY || process.env.http_proxy || process.env.https_proxy) {
|
|
151
|
+
const { EnvHttpProxyAgent, setGlobalDispatcher } = await import('undici');
|
|
152
|
+
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
153
|
+
}
|
|
110
154
|
// Ensure workspace packages are linked before importing cli.js (which imports @gsd/*).
|
|
111
155
|
// npm postinstall handles this normally, but npx --ignore-scripts skips postinstall.
|
|
112
156
|
const gsdScopeDir = join(gsdNodeModules, '@gsd');
|
|
@@ -5,14 +5,15 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
|
|
|
5
5
|
/**
|
|
6
6
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
7
7
|
*
|
|
8
|
-
* - extensions/ → ~/.gsd/agent/extensions/ (
|
|
9
|
-
* - agents/ → ~/.gsd/agent/agents/ (
|
|
10
|
-
* - skills/ → ~/.gsd/agent/skills/ (
|
|
8
|
+
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
9
|
+
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
10
|
+
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
11
11
|
* - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Skips the copy when the managed-resources.json version matches the current
|
|
14
|
+
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
|
15
|
+
* After `npm update -g @glittercowboy/gsd`, versions will differ and the
|
|
16
|
+
* copy runs once to land the new resources.
|
|
16
17
|
*
|
|
17
18
|
* Inspectable: `ls ~/.gsd/agent/extensions/`
|
|
18
19
|
*/
|
package/dist/resource-loader.js
CHANGED
|
@@ -107,20 +107,27 @@ export function getNewerManagedResourceVersion(agentDir, currentVersion) {
|
|
|
107
107
|
/**
|
|
108
108
|
* Syncs all bundled resources to agentDir (~/.gsd/agent/) on every launch.
|
|
109
109
|
*
|
|
110
|
-
* - extensions/ → ~/.gsd/agent/extensions/ (
|
|
111
|
-
* - agents/ → ~/.gsd/agent/agents/ (
|
|
112
|
-
* - skills/ → ~/.gsd/agent/skills/ (
|
|
110
|
+
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
111
|
+
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
112
|
+
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
113
113
|
* - GSD-WORKFLOW.md is read directly from bundled path via GSD_WORKFLOW_PATH env var
|
|
114
114
|
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
115
|
+
* Skips the copy when the managed-resources.json version matches the current
|
|
116
|
+
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
|
117
|
+
* After `npm update -g @glittercowboy/gsd`, versions will differ and the
|
|
118
|
+
* copy runs once to land the new resources.
|
|
118
119
|
*
|
|
119
120
|
* Inspectable: `ls ~/.gsd/agent/extensions/`
|
|
120
121
|
*/
|
|
121
122
|
export function initResources(agentDir) {
|
|
122
123
|
mkdirSync(agentDir, { recursive: true });
|
|
123
|
-
//
|
|
124
|
+
// Skip resource sync when versions match — saves ~128ms of cpSync per launch
|
|
125
|
+
const currentVersion = getBundledGsdVersion();
|
|
126
|
+
const managedVersion = readManagedResourceVersion(agentDir);
|
|
127
|
+
if (managedVersion && managedVersion === currentVersion) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// Sync extensions — overwrite so updates land on next launch
|
|
124
131
|
const destExtensions = join(agentDir, 'extensions');
|
|
125
132
|
cpSync(bundledExtensionsDir, destExtensions, { recursive: true, force: true });
|
|
126
133
|
// Sync agents
|
|
@@ -129,7 +136,7 @@ export function initResources(agentDir) {
|
|
|
129
136
|
if (existsSync(srcAgents)) {
|
|
130
137
|
cpSync(srcAgents, destAgents, { recursive: true, force: true });
|
|
131
138
|
}
|
|
132
|
-
// Sync skills —
|
|
139
|
+
// Sync skills — overwrite so updates land on next launch
|
|
133
140
|
const destSkills = join(agentDir, 'skills');
|
|
134
141
|
const srcSkills = join(resourcesDir, 'skills');
|
|
135
142
|
if (existsSync(srcSkills)) {
|
|
@@ -14,20 +14,9 @@ import {
|
|
|
14
14
|
removeWorktree,
|
|
15
15
|
worktreePath,
|
|
16
16
|
} from "./worktree-manager.js";
|
|
17
|
-
import {
|
|
18
|
-
detectWorktreeName,
|
|
19
|
-
getSliceBranchName,
|
|
20
|
-
} from "./worktree.js";
|
|
21
17
|
import {
|
|
22
18
|
MergeConflictError,
|
|
23
|
-
inferCommitType,
|
|
24
19
|
} from "./git-service.js";
|
|
25
|
-
import type { MergeSliceResult } from "./git-service.js";
|
|
26
|
-
import { recoverCheckout, withMergeHeal } from "./git-self-heal.js";
|
|
27
|
-
import {
|
|
28
|
-
nativeBranchExists,
|
|
29
|
-
nativeCommitCountBetween,
|
|
30
|
-
} from "./native-git-bridge.js";
|
|
31
20
|
import { parseRoadmap } from "./files.js";
|
|
32
21
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
33
22
|
|
|
@@ -36,48 +25,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
|
36
25
|
/** Original project root before chdir into auto-worktree. */
|
|
37
26
|
let originalBase: string | null = null;
|
|
38
27
|
|
|
39
|
-
// ─── Isolation Resolver ────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Determine whether auto-mode should use worktree isolation.
|
|
43
|
-
*
|
|
44
|
-
* Resolution order:
|
|
45
|
-
* 1. Explicit git.isolation preference -> return (isolation === "worktree")
|
|
46
|
-
* 2. Legacy detection: if gsd branches exist -> return false (branch mode)
|
|
47
|
-
* 3. Default: return true (worktree mode for new projects)
|
|
48
|
-
*/
|
|
49
|
-
export function shouldUseWorktreeIsolation(basePath: string, overridePrefs?: { isolation?: string }): boolean {
|
|
50
|
-
const prefs = overridePrefs ?? loadEffectiveGSDPreferences()?.preferences?.git;
|
|
51
|
-
if (prefs?.isolation) {
|
|
52
|
-
return prefs.isolation === "worktree";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Legacy detection: check for existing gsd/*/* branches (branch-per-slice pattern)
|
|
56
|
-
try {
|
|
57
|
-
// Use unquoted glob pattern — single quotes are not interpreted by cmd.exe on Windows,
|
|
58
|
-
// causing the pattern to match literally instead of as a glob.
|
|
59
|
-
const output = execSync("git branch --list gsd/*/*", {
|
|
60
|
-
cwd: basePath,
|
|
61
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
62
|
-
encoding: "utf-8",
|
|
63
|
-
}).trim();
|
|
64
|
-
if (output) return false; // Legacy branch-per-slice project
|
|
65
|
-
} catch {
|
|
66
|
-
// If git command fails, default to worktree
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return true; // New project default
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Resolve the merge_to_main preference value.
|
|
74
|
-
* Returns "milestone" (default) or "slice".
|
|
75
|
-
*/
|
|
76
|
-
export function getMergeToMainMode(): "milestone" | "slice" {
|
|
77
|
-
const prefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
78
|
-
return prefs?.merge_to_main ?? "milestone";
|
|
79
|
-
}
|
|
80
|
-
|
|
81
28
|
// ─── Git Helpers (local, mirrors worktree-command.ts pattern) ──────────────
|
|
82
29
|
|
|
83
30
|
function resolveGitHeadPath(dir: string): string | null {
|
|
@@ -238,117 +185,6 @@ export function getAutoWorktreeOriginalBase(): string | null {
|
|
|
238
185
|
return originalBase;
|
|
239
186
|
}
|
|
240
187
|
|
|
241
|
-
// ─── Merge Slice -> Milestone ───────────────────────────────────────────────
|
|
242
|
-
|
|
243
|
-
/**
|
|
244
|
-
* Merge a completed slice branch into the milestone branch via `--no-ff`.
|
|
245
|
-
*
|
|
246
|
-
* Worktree-mode merge: `.gsd/` is local to the worktree (not tracked in
|
|
247
|
-
* git), so there are zero `.gsd/` conflict resolution concerns. No runtime
|
|
248
|
-
* exclusion untracking, no `--theirs` checkout, no snapshot creation.
|
|
249
|
-
*
|
|
250
|
-
* On conflict: throws MergeConflictError with conflicted file list.
|
|
251
|
-
* On success: deletes the slice branch and returns MergeSliceResult.
|
|
252
|
-
*/
|
|
253
|
-
export function mergeSliceToMilestone(
|
|
254
|
-
basePath: string,
|
|
255
|
-
milestoneId: string,
|
|
256
|
-
sliceId: string,
|
|
257
|
-
sliceTitle: string,
|
|
258
|
-
): MergeSliceResult {
|
|
259
|
-
if (!isInAutoWorktree(basePath)) {
|
|
260
|
-
throw new Error("mergeSliceToMilestone called outside auto-worktree");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const cwd = process.cwd();
|
|
264
|
-
const milestoneBranch = autoWorktreeBranch(milestoneId);
|
|
265
|
-
const worktreeName = detectWorktreeName(cwd);
|
|
266
|
-
const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
|
|
267
|
-
|
|
268
|
-
// Verify slice branch exists
|
|
269
|
-
if (!nativeBranchExists(cwd, sliceBranch)) {
|
|
270
|
-
throw new Error(`Slice branch "${sliceBranch}" does not exist`);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Verify slice has commits ahead of milestone branch
|
|
274
|
-
const commitCount = nativeCommitCountBetween(cwd, milestoneBranch, sliceBranch);
|
|
275
|
-
if (commitCount === 0) {
|
|
276
|
-
throw new Error(
|
|
277
|
-
`Slice branch "${sliceBranch}" has no commits ahead of "${milestoneBranch}"`,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// Checkout milestone branch (with self-healing reset)
|
|
282
|
-
recoverCheckout(cwd, milestoneBranch);
|
|
283
|
-
|
|
284
|
-
// Build rich commit message (replicates GitServiceImpl.buildRichCommitMessage format)
|
|
285
|
-
const commitType = inferCommitType(sliceTitle);
|
|
286
|
-
const subject = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
|
|
287
|
-
|
|
288
|
-
let message = subject;
|
|
289
|
-
try {
|
|
290
|
-
const logOutput = execSync(
|
|
291
|
-
`git log --oneline --format=%s ${milestoneBranch}..${sliceBranch}`,
|
|
292
|
-
{ cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" },
|
|
293
|
-
).trim();
|
|
294
|
-
|
|
295
|
-
if (logOutput) {
|
|
296
|
-
const subjects = logOutput.split("\n").filter(Boolean);
|
|
297
|
-
const MAX_ENTRIES = 20;
|
|
298
|
-
const truncated = subjects.length > MAX_ENTRIES;
|
|
299
|
-
const displayed = truncated ? subjects.slice(0, MAX_ENTRIES) : subjects;
|
|
300
|
-
const taskLines = displayed.map(s => `- ${s}`).join("\n");
|
|
301
|
-
const truncationLine = truncated
|
|
302
|
-
? `\n- ... and ${subjects.length - MAX_ENTRIES} more`
|
|
303
|
-
: "";
|
|
304
|
-
message = `${subject}\n\nTasks:\n${taskLines}${truncationLine}\n\nBranch: ${sliceBranch}`;
|
|
305
|
-
}
|
|
306
|
-
} catch {
|
|
307
|
-
// Fall back to subject-only message
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Merge --no-ff (with self-healing retry for transient failures)
|
|
311
|
-
try {
|
|
312
|
-
withMergeHeal(cwd, () => {
|
|
313
|
-
execFileSync("git", ["merge", "--no-ff", "-m", message, sliceBranch], {
|
|
314
|
-
cwd,
|
|
315
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
316
|
-
encoding: "utf-8",
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
} catch (err) {
|
|
320
|
-
if (err instanceof MergeConflictError) {
|
|
321
|
-
// Re-throw with correct branch context
|
|
322
|
-
throw new MergeConflictError(
|
|
323
|
-
err.conflictedFiles,
|
|
324
|
-
err.strategy,
|
|
325
|
-
sliceBranch,
|
|
326
|
-
milestoneBranch,
|
|
327
|
-
);
|
|
328
|
-
}
|
|
329
|
-
throw err;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Delete slice branch
|
|
333
|
-
let deletedBranch = false;
|
|
334
|
-
try {
|
|
335
|
-
execSync(`git branch -d ${sliceBranch}`, {
|
|
336
|
-
cwd,
|
|
337
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
338
|
-
encoding: "utf-8",
|
|
339
|
-
});
|
|
340
|
-
deletedBranch = true;
|
|
341
|
-
} catch {
|
|
342
|
-
// Branch deletion is best-effort
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
branch: sliceBranch,
|
|
347
|
-
mergedCommitMessage: message,
|
|
348
|
-
deletedBranch,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
188
|
// ─── Merge Milestone -> Main ───────────────────────────────────────────────
|
|
353
189
|
|
|
354
190
|
/**
|
|
@@ -416,8 +252,12 @@ export function mergeMilestoneToMain(
|
|
|
416
252
|
const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
417
253
|
const mainBranch = prefs.main_branch || "main";
|
|
418
254
|
|
|
419
|
-
// 5. Checkout main
|
|
420
|
-
|
|
255
|
+
// 5. Checkout main
|
|
256
|
+
execSync(`git checkout ${mainBranch}`, {
|
|
257
|
+
cwd: originalBasePath_,
|
|
258
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
259
|
+
encoding: "utf-8",
|
|
260
|
+
});
|
|
421
261
|
|
|
422
262
|
// 6. Build rich commit message
|
|
423
263
|
const milestoneTitle = roadmap.title.replace(/^M\d+:\s*/, "").trim() || milestoneId;
|
|
@@ -429,26 +269,29 @@ export function mergeMilestoneToMain(
|
|
|
429
269
|
}
|
|
430
270
|
const commitMessage = subject + body;
|
|
431
271
|
|
|
432
|
-
// 7. Squash merge
|
|
272
|
+
// 7. Squash merge
|
|
433
273
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
274
|
+
execSync(`git merge --squash ${milestoneBranch}`, {
|
|
275
|
+
cwd: originalBasePath_,
|
|
276
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
277
|
+
encoding: "utf-8",
|
|
278
|
+
});
|
|
279
|
+
} catch (mergeErr) {
|
|
280
|
+
// Check for real conflicts
|
|
281
|
+
try {
|
|
282
|
+
const conflictOutput = execSync("git diff --name-only --diff-filter=U", {
|
|
436
283
|
cwd: originalBasePath_,
|
|
437
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
438
284
|
encoding: "utf-8",
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
milestoneBranch,
|
|
448
|
-
mainBranch,
|
|
449
|
-
);
|
|
285
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
286
|
+
}).trim();
|
|
287
|
+
if (conflictOutput) {
|
|
288
|
+
const conflictedFiles = conflictOutput.split("\n").filter(Boolean);
|
|
289
|
+
throw new MergeConflictError(conflictedFiles, "squash", milestoneBranch, mainBranch);
|
|
290
|
+
}
|
|
291
|
+
} catch (diffErr) {
|
|
292
|
+
if (diffErr instanceof MergeConflictError) throw diffErr;
|
|
450
293
|
}
|
|
451
|
-
//
|
|
294
|
+
// No conflicts detected — possibly "already up to date", fall through to commit
|
|
452
295
|
}
|
|
453
296
|
|
|
454
297
|
// 8. Commit (handle nothing-to-commit gracefully)
|