gsd-pi 2.3.11 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.js +4 -2
- package/dist/pi-migration.d.ts +14 -0
- package/dist/pi-migration.js +57 -0
- package/package.json +1 -1
- package/src/resources/extensions/gsd/git-service.ts +369 -0
- package/src/resources/extensions/gsd/index.ts +11 -6
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -0
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -0
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +62 -216
- package/src/resources/extensions/gsd/tests/git-service.test.ts +892 -0
package/README.md
CHANGED
|
@@ -200,7 +200,7 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
|
|
|
200
200
|
|
|
201
201
|
### First launch
|
|
202
202
|
|
|
203
|
-
On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. Run `gsd config` anytime to re-run the wizard.
|
|
203
|
+
On first run, GSD launches a branded setup wizard that walks you through LLM provider selection (OAuth or API key), then optional tool API keys (Brave Search, Context7, Jina, Slack, Discord). Every step is skippable — press Enter to skip any. If you have an existing Pi installation, your provider credentials (LLM and tool keys) are imported automatically. Run `gsd config` anytime to re-run the wizard.
|
|
204
204
|
|
|
205
205
|
### Commands
|
|
206
206
|
|
package/dist/cli.js
CHANGED
|
@@ -2,9 +2,10 @@ import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, Ses
|
|
|
2
2
|
import { existsSync, readdirSync, renameSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { agentDir, sessionsDir, authFilePath } from './app-paths.js';
|
|
5
|
-
import { initResources } from './resource-loader.js';
|
|
5
|
+
import { initResources, buildResourceLoader } from './resource-loader.js';
|
|
6
6
|
import { ensureManagedTools } from './tool-bootstrap.js';
|
|
7
7
|
import { loadStoredEnvKeys } from './wizard.js';
|
|
8
|
+
import { migratePiCredentials } from './pi-migration.js';
|
|
8
9
|
import { shouldRunOnboarding, runOnboarding } from './onboarding.js';
|
|
9
10
|
function parseCliArgs(argv) {
|
|
10
11
|
const flags = { extensions: [], messages: [] };
|
|
@@ -74,6 +75,7 @@ if (cliFlags.messages[0] === 'config') {
|
|
|
74
75
|
ensureManagedTools(join(agentDir, 'bin'));
|
|
75
76
|
const authStorage = AuthStorage.create(authFilePath);
|
|
76
77
|
loadStoredEnvKeys(authStorage);
|
|
78
|
+
migratePiCredentials(authStorage);
|
|
77
79
|
// Run onboarding wizard on first launch (no LLM provider configured)
|
|
78
80
|
if (!isPrintMode && shouldRunOnboarding(authStorage)) {
|
|
79
81
|
await runOnboarding(authStorage);
|
|
@@ -200,7 +202,7 @@ if (existsSync(sessionsDir)) {
|
|
|
200
202
|
}
|
|
201
203
|
const sessionManager = SessionManager.create(cwd, projectSessionsDir);
|
|
202
204
|
initResources(agentDir);
|
|
203
|
-
const resourceLoader =
|
|
205
|
+
const resourceLoader = buildResourceLoader(agentDir);
|
|
204
206
|
await resourceLoader.reload();
|
|
205
207
|
const { session, extensionsResult } = await createAgentSession({
|
|
206
208
|
authStorage,
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time migration of provider credentials from Pi (~/.pi/agent/auth.json)
|
|
3
|
+
* into GSD's auth storage. Runs when GSD has no LLM providers configured,
|
|
4
|
+
* so users with an existing Pi install skip re-authentication.
|
|
5
|
+
*/
|
|
6
|
+
import type { AuthStorage } from '@mariozechner/pi-coding-agent';
|
|
7
|
+
/**
|
|
8
|
+
* Migrate provider credentials from Pi's auth.json into GSD's AuthStorage.
|
|
9
|
+
*
|
|
10
|
+
* Only runs when GSD has no LLM provider configured and Pi's auth.json exists.
|
|
11
|
+
* Copies any credentials GSD doesn't already have. Returns true if an LLM
|
|
12
|
+
* provider was migrated (so onboarding can be skipped).
|
|
13
|
+
*/
|
|
14
|
+
export declare function migratePiCredentials(authStorage: AuthStorage): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time migration of provider credentials from Pi (~/.pi/agent/auth.json)
|
|
3
|
+
* into GSD's auth storage. Runs when GSD has no LLM providers configured,
|
|
4
|
+
* so users with an existing Pi install skip re-authentication.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
const PI_AUTH_PATH = join(homedir(), '.pi', 'agent', 'auth.json');
|
|
10
|
+
const LLM_PROVIDER_IDS = [
|
|
11
|
+
'anthropic',
|
|
12
|
+
'openai',
|
|
13
|
+
'github-copilot',
|
|
14
|
+
'openai-codex',
|
|
15
|
+
'google-gemini-cli',
|
|
16
|
+
'google-antigravity',
|
|
17
|
+
'google',
|
|
18
|
+
'groq',
|
|
19
|
+
'xai',
|
|
20
|
+
'openrouter',
|
|
21
|
+
'mistral',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Migrate provider credentials from Pi's auth.json into GSD's AuthStorage.
|
|
25
|
+
*
|
|
26
|
+
* Only runs when GSD has no LLM provider configured and Pi's auth.json exists.
|
|
27
|
+
* Copies any credentials GSD doesn't already have. Returns true if an LLM
|
|
28
|
+
* provider was migrated (so onboarding can be skipped).
|
|
29
|
+
*/
|
|
30
|
+
export function migratePiCredentials(authStorage) {
|
|
31
|
+
try {
|
|
32
|
+
// Only migrate when GSD has no LLM providers
|
|
33
|
+
const existing = authStorage.list();
|
|
34
|
+
const hasLlm = existing.some(id => LLM_PROVIDER_IDS.includes(id));
|
|
35
|
+
if (hasLlm)
|
|
36
|
+
return false;
|
|
37
|
+
if (!existsSync(PI_AUTH_PATH))
|
|
38
|
+
return false;
|
|
39
|
+
const raw = readFileSync(PI_AUTH_PATH, 'utf-8');
|
|
40
|
+
const piData = JSON.parse(raw);
|
|
41
|
+
let migratedLlm = false;
|
|
42
|
+
for (const [providerId, credential] of Object.entries(piData)) {
|
|
43
|
+
if (authStorage.has(providerId))
|
|
44
|
+
continue;
|
|
45
|
+
authStorage.set(providerId, credential);
|
|
46
|
+
const isLlm = LLM_PROVIDER_IDS.includes(providerId);
|
|
47
|
+
if (isLlm)
|
|
48
|
+
migratedLlm = true;
|
|
49
|
+
process.stderr.write(`[gsd] Migrated ${isLlm ? 'LLM provider' : 'credential'}: ${providerId} (from Pi)\n`);
|
|
50
|
+
}
|
|
51
|
+
return migratedLlm;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Non-fatal — don't block startup
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GSD Git Service
|
|
3
|
+
*
|
|
4
|
+
* Core git operations for GSD: types, constants, and pure helpers.
|
|
5
|
+
* Higher-level operations (commit, staging, branching) build on these.
|
|
6
|
+
*
|
|
7
|
+
* This module centralizes the GitPreferences interface, runtime exclusion
|
|
8
|
+
* paths, commit type inference, and the runGit shell helper.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from "node:child_process";
|
|
12
|
+
import { sep } from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
detectWorktreeName,
|
|
16
|
+
getSliceBranchName,
|
|
17
|
+
SLICE_BRANCH_RE,
|
|
18
|
+
} from "./worktree.ts";
|
|
19
|
+
|
|
20
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export interface GitPreferences {
|
|
23
|
+
auto_push?: boolean;
|
|
24
|
+
push_branches?: boolean;
|
|
25
|
+
remote?: string;
|
|
26
|
+
snapshots?: boolean;
|
|
27
|
+
pre_merge_check?: boolean | string;
|
|
28
|
+
commit_type?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CommitOptions {
|
|
32
|
+
message: string;
|
|
33
|
+
allowEmpty?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface MergeSliceResult {
|
|
37
|
+
branch: string;
|
|
38
|
+
mergedCommitMessage: string;
|
|
39
|
+
deletedBranch: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── Constants ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* GSD runtime paths that should be excluded from smart staging.
|
|
46
|
+
* These are transient/generated artifacts that should never be committed.
|
|
47
|
+
* Matches the union of SKIP_PATHS + SKIP_EXACT in worktree-manager.ts
|
|
48
|
+
* and the first 6 entries in gitignore.ts BASELINE_PATTERNS.
|
|
49
|
+
*/
|
|
50
|
+
export const RUNTIME_EXCLUSION_PATHS: readonly string[] = [
|
|
51
|
+
".gsd/activity/",
|
|
52
|
+
".gsd/runtime/",
|
|
53
|
+
".gsd/worktrees/",
|
|
54
|
+
".gsd/auto.lock",
|
|
55
|
+
".gsd/metrics.json",
|
|
56
|
+
".gsd/STATE.md",
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// ─── Git Helper ────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Run a git command in the given directory.
|
|
63
|
+
* Returns trimmed stdout. Throws on non-zero exit unless allowFailure is set.
|
|
64
|
+
*/
|
|
65
|
+
export function runGit(basePath: string, args: string[], options: { allowFailure?: boolean } = {}): string {
|
|
66
|
+
try {
|
|
67
|
+
return execSync(`git ${args.join(" ")}`, {
|
|
68
|
+
cwd: basePath,
|
|
69
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
70
|
+
encoding: "utf-8",
|
|
71
|
+
}).trim();
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (options.allowFailure) return "";
|
|
74
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
75
|
+
throw new Error(`git ${args.join(" ")} failed in ${basePath}: ${message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Commit Type Inference ─────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Keyword-to-commit-type mapping. Order matters — first match wins.
|
|
83
|
+
* Each entry: [keywords[], commitType]
|
|
84
|
+
*/
|
|
85
|
+
const COMMIT_TYPE_RULES: [string[], string][] = [
|
|
86
|
+
[["fix", "bug", "patch", "hotfix"], "fix"],
|
|
87
|
+
[["refactor", "restructure", "reorganize"], "refactor"],
|
|
88
|
+
[["doc", "docs", "documentation"], "docs"],
|
|
89
|
+
[["test", "tests", "testing"], "test"],
|
|
90
|
+
[["chore", "cleanup", "clean up", "archive", "remove", "delete"], "chore"],
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Infer a conventional commit type from a slice title.
|
|
95
|
+
* Uses case-insensitive word-boundary matching against known keywords.
|
|
96
|
+
* Returns "feat" when no keywords match.
|
|
97
|
+
*/
|
|
98
|
+
// ─── GitServiceImpl ────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
export class GitServiceImpl {
|
|
101
|
+
readonly basePath: string;
|
|
102
|
+
readonly prefs: GitPreferences;
|
|
103
|
+
|
|
104
|
+
constructor(basePath: string, prefs: GitPreferences = {}) {
|
|
105
|
+
this.basePath = basePath;
|
|
106
|
+
this.prefs = prefs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Convenience wrapper: run git in this repo's basePath. */
|
|
110
|
+
private git(args: string[], options: { allowFailure?: boolean } = {}): string {
|
|
111
|
+
return runGit(this.basePath, args, options);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Smart staging: `git add -A` excluding GSD runtime paths via pathspec.
|
|
116
|
+
* Falls back to plain `git add -A` if the exclusion pathspec fails.
|
|
117
|
+
*/
|
|
118
|
+
private smartStage(): void {
|
|
119
|
+
const excludes = RUNTIME_EXCLUSION_PATHS.map(p => `':(exclude)${p}'`);
|
|
120
|
+
const args = ["add", "-A", "--", ".", ...excludes];
|
|
121
|
+
try {
|
|
122
|
+
this.git(args);
|
|
123
|
+
} catch {
|
|
124
|
+
console.error("GitService: smart staging failed, falling back to git add -A");
|
|
125
|
+
this.git(["add", "-A"]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Stage files (smart staging) and commit.
|
|
131
|
+
* Returns the commit message string on success, or null if nothing to commit.
|
|
132
|
+
*/
|
|
133
|
+
commit(opts: CommitOptions): string | null {
|
|
134
|
+
this.smartStage();
|
|
135
|
+
|
|
136
|
+
// Check if anything was actually staged
|
|
137
|
+
const staged = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
138
|
+
if (!staged && !opts.allowEmpty) return null;
|
|
139
|
+
|
|
140
|
+
this.git(["commit", "-m", JSON.stringify(opts.message), ...(opts.allowEmpty ? ["--allow-empty"] : [])]);
|
|
141
|
+
return opts.message;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Auto-commit dirty working tree with a conventional chore message.
|
|
146
|
+
* Returns the commit message on success, or null if nothing to commit.
|
|
147
|
+
*/
|
|
148
|
+
autoCommit(unitType: string, unitId: string): string | null {
|
|
149
|
+
// Quick check: is there anything dirty at all?
|
|
150
|
+
const status = this.git(["status", "--short"], { allowFailure: true });
|
|
151
|
+
if (!status) return null;
|
|
152
|
+
|
|
153
|
+
this.smartStage();
|
|
154
|
+
|
|
155
|
+
// After smart staging, check if anything was actually staged
|
|
156
|
+
// (all changes might have been runtime files that got excluded)
|
|
157
|
+
const staged = this.git(["diff", "--cached", "--stat"], { allowFailure: true });
|
|
158
|
+
if (!staged) return null;
|
|
159
|
+
|
|
160
|
+
const message = `chore(${unitId}): auto-commit after ${unitType}`;
|
|
161
|
+
this.git(["commit", "-m", JSON.stringify(message)]);
|
|
162
|
+
return message;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Branch Queries ────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get the "main" branch for this repo.
|
|
169
|
+
* In a worktree: returns worktree/<name> (the worktree's base branch).
|
|
170
|
+
* In the main tree: origin/HEAD symbolic-ref → main/master fallback → current branch.
|
|
171
|
+
*/
|
|
172
|
+
getMainBranch(): string {
|
|
173
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
174
|
+
if (wtName) {
|
|
175
|
+
const wtBranch = `worktree/${wtName}`;
|
|
176
|
+
const exists = this.git(["show-ref", "--verify", `refs/heads/${wtBranch}`], { allowFailure: true });
|
|
177
|
+
if (exists) return wtBranch;
|
|
178
|
+
return this.git(["branch", "--show-current"]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const symbolic = this.git(["symbolic-ref", "refs/remotes/origin/HEAD"], { allowFailure: true });
|
|
182
|
+
if (symbolic) {
|
|
183
|
+
const match = symbolic.match(/refs\/remotes\/origin\/(.+)$/);
|
|
184
|
+
if (match) return match[1]!;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const mainExists = this.git(["show-ref", "--verify", "refs/heads/main"], { allowFailure: true });
|
|
188
|
+
if (mainExists) return "main";
|
|
189
|
+
|
|
190
|
+
const masterExists = this.git(["show-ref", "--verify", "refs/heads/master"], { allowFailure: true });
|
|
191
|
+
if (masterExists) return "master";
|
|
192
|
+
|
|
193
|
+
return this.git(["branch", "--show-current"]);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Get the current branch name. */
|
|
197
|
+
getCurrentBranch(): string {
|
|
198
|
+
return this.git(["branch", "--show-current"]);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** True if currently on a GSD slice branch. */
|
|
202
|
+
isOnSliceBranch(): boolean {
|
|
203
|
+
const current = this.getCurrentBranch();
|
|
204
|
+
return SLICE_BRANCH_RE.test(current);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Returns the slice branch name if on one, null otherwise. */
|
|
208
|
+
getActiveSliceBranch(): string | null {
|
|
209
|
+
try {
|
|
210
|
+
const current = this.getCurrentBranch();
|
|
211
|
+
return SLICE_BRANCH_RE.test(current) ? current : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Branch Lifecycle ──────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Check if a local branch exists.
|
|
221
|
+
*/
|
|
222
|
+
private branchExists(branch: string): boolean {
|
|
223
|
+
try {
|
|
224
|
+
this.git(["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
|
|
225
|
+
return true;
|
|
226
|
+
} catch {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Ensure the slice branch exists and is checked out.
|
|
233
|
+
*
|
|
234
|
+
* Creates the branch from the current working branch if it's not a slice
|
|
235
|
+
* branch (preserves planning artifacts). Falls back to main when on another
|
|
236
|
+
* slice branch (avoids chaining slice branches).
|
|
237
|
+
*
|
|
238
|
+
* Auto-commits dirty state via smart staging before checkout so runtime
|
|
239
|
+
* files are never accidentally committed during branch switches.
|
|
240
|
+
*
|
|
241
|
+
* Returns true if the branch was newly created.
|
|
242
|
+
*/
|
|
243
|
+
ensureSliceBranch(milestoneId: string, sliceId: string): boolean {
|
|
244
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
245
|
+
const branch = getSliceBranchName(milestoneId, sliceId, wtName);
|
|
246
|
+
const current = this.getCurrentBranch();
|
|
247
|
+
|
|
248
|
+
if (current === branch) return false;
|
|
249
|
+
|
|
250
|
+
let created = false;
|
|
251
|
+
|
|
252
|
+
if (!this.branchExists(branch)) {
|
|
253
|
+
// Branch from current when it's a normal working branch (not a slice).
|
|
254
|
+
// If already on a slice branch, fall back to main to avoid chaining.
|
|
255
|
+
const mainBranch = this.getMainBranch();
|
|
256
|
+
const base = SLICE_BRANCH_RE.test(current) ? mainBranch : current;
|
|
257
|
+
this.git(["branch", branch, base]);
|
|
258
|
+
created = true;
|
|
259
|
+
} else {
|
|
260
|
+
// Branch exists — check it's not checked out in another worktree
|
|
261
|
+
const worktreeList = this.git(["worktree", "list", "--porcelain"]);
|
|
262
|
+
if (worktreeList.includes(`branch refs/heads/${branch}`)) {
|
|
263
|
+
throw new Error(
|
|
264
|
+
`Branch "${branch}" is already in use by another worktree. ` +
|
|
265
|
+
`Remove that worktree first, or switch it to a different branch.`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Auto-commit dirty state via smart staging before checkout
|
|
271
|
+
this.autoCommit("pre-switch", current);
|
|
272
|
+
|
|
273
|
+
this.git(["checkout", branch]);
|
|
274
|
+
return created;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Switch to main, auto-committing dirty state via smart staging first.
|
|
279
|
+
*/
|
|
280
|
+
switchToMain(): void {
|
|
281
|
+
const mainBranch = this.getMainBranch();
|
|
282
|
+
const current = this.getCurrentBranch();
|
|
283
|
+
if (current === mainBranch) return;
|
|
284
|
+
|
|
285
|
+
this.autoCommit("pre-switch", current);
|
|
286
|
+
|
|
287
|
+
this.git(["checkout", mainBranch]);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ─── Merge ─────────────────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Squash-merge a slice branch into main and delete it.
|
|
294
|
+
*
|
|
295
|
+
* Must be called from the main branch. Uses `inferCommitType(sliceTitle)`
|
|
296
|
+
* for the conventional commit type instead of hardcoding `feat`.
|
|
297
|
+
*
|
|
298
|
+
* Throws when:
|
|
299
|
+
* - Not currently on the main branch
|
|
300
|
+
* - The slice branch does not exist
|
|
301
|
+
* - The slice branch has no commits ahead of main
|
|
302
|
+
*/
|
|
303
|
+
mergeSliceToMain(milestoneId: string, sliceId: string, sliceTitle: string): MergeSliceResult {
|
|
304
|
+
const mainBranch = this.getMainBranch();
|
|
305
|
+
const current = this.getCurrentBranch();
|
|
306
|
+
|
|
307
|
+
if (current !== mainBranch) {
|
|
308
|
+
throw new Error(
|
|
309
|
+
`mergeSliceToMain must be called from the main branch ("${mainBranch}"), ` +
|
|
310
|
+
`but currently on "${current}"`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const wtName = detectWorktreeName(this.basePath);
|
|
315
|
+
const branch = getSliceBranchName(milestoneId, sliceId, wtName);
|
|
316
|
+
|
|
317
|
+
if (!this.branchExists(branch)) {
|
|
318
|
+
throw new Error(
|
|
319
|
+
`Slice branch "${branch}" does not exist. Nothing to merge.`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check commits ahead
|
|
324
|
+
const aheadCount = this.git(["rev-list", "--count", `${mainBranch}..${branch}`]);
|
|
325
|
+
if (aheadCount === "0") {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`Slice branch "${branch}" has no commits ahead of "${mainBranch}". Nothing to merge.`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Squash merge
|
|
332
|
+
this.git(["merge", "--squash", branch]);
|
|
333
|
+
|
|
334
|
+
// Build conventional commit message
|
|
335
|
+
const commitType = inferCommitType(sliceTitle);
|
|
336
|
+
const message = `${commitType}(${milestoneId}/${sliceId}): ${sliceTitle}`;
|
|
337
|
+
this.git(["commit", "-m", JSON.stringify(message)]);
|
|
338
|
+
|
|
339
|
+
// Delete the merged branch
|
|
340
|
+
this.git(["branch", "-D", branch]);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
branch,
|
|
344
|
+
mergedCommitMessage: message,
|
|
345
|
+
deletedBranch: true,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ─── Commit Type Inference ─────────────────────────────────────────────────
|
|
351
|
+
|
|
352
|
+
export function inferCommitType(sliceTitle: string): string {
|
|
353
|
+
const lower = sliceTitle.toLowerCase();
|
|
354
|
+
|
|
355
|
+
for (const [keywords, commitType] of COMMIT_TYPE_RULES) {
|
|
356
|
+
for (const keyword of keywords) {
|
|
357
|
+
// "clean up" is multi-word — use indexOf for it
|
|
358
|
+
if (keyword.includes(" ")) {
|
|
359
|
+
if (lower.includes(keyword)) return commitType;
|
|
360
|
+
} else {
|
|
361
|
+
// Word boundary match: keyword must not be surrounded by word chars
|
|
362
|
+
const re = new RegExp(`\\b${keyword}\\b`, "i");
|
|
363
|
+
if (re.test(lower)) return commitType;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return "feat";
|
|
369
|
+
}
|
|
@@ -158,14 +158,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
158
158
|
|
|
159
159
|
// ── session_start: render branded GSD header + remote channel status ──
|
|
160
160
|
pi.on("session_start", async (_event, ctx) => {
|
|
161
|
-
|
|
162
|
-
|
|
161
|
+
// Theme access throws in RPC mode (no TUI) — header is decorative, skip it
|
|
162
|
+
try {
|
|
163
|
+
const theme = ctx.ui.theme;
|
|
164
|
+
const version = process.env.GSD_VERSION || "0.0.0";
|
|
163
165
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
const logoText = GSD_LOGO_LINES.map((line) => theme.fg("accent", line)).join("\n");
|
|
167
|
+
const titleLine = ` ${theme.bold("Get Shit Done")} ${theme.fg("dim", `v${version}`)}`;
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
const headerContent = `${logoText}\n${titleLine}`;
|
|
170
|
+
ctx.ui.setHeader((_ui, _theme) => new Text(headerContent, 1, 0));
|
|
171
|
+
} catch {
|
|
172
|
+
// RPC mode — no TUI, skip header rendering
|
|
173
|
+
}
|
|
169
174
|
|
|
170
175
|
// Notify remote questions status if configured
|
|
171
176
|
try {
|
|
@@ -19,6 +19,7 @@ Start with the inlined context below. Treat the inlined task plan as the authori
|
|
|
19
19
|
{{priorTaskLines}}
|
|
20
20
|
|
|
21
21
|
Then:
|
|
22
|
+
0. Narrate step transitions, key implementation decisions, and verification outcomes as you work. Keep it terse — one line between tool-call clusters, not between every call.
|
|
22
23
|
1. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during execution, without relaxing required verification or artifact rules
|
|
23
24
|
2. Execute the steps in the inlined task plan
|
|
24
25
|
3. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
|
|
@@ -6,6 +6,8 @@ All relevant context has been preloaded below — start working immediately with
|
|
|
6
6
|
|
|
7
7
|
{{inlinedContext}}
|
|
8
8
|
|
|
9
|
+
Narrate your decomposition reasoning — why you're grouping work this way, what risks are driving the order, what verification strategy you're choosing and why.
|
|
10
|
+
|
|
9
11
|
Then:
|
|
10
12
|
1. Read the template at `~/.gsd/agent/extensions/gsd/templates/roadmap.md`
|
|
11
13
|
2. Read `.gsd/REQUIREMENTS.md` if it exists. Treat **Active** requirements as the capability contract for planning. If it does not exist, continue in legacy compatibility mode but explicitly note that requirement coverage is operating without a contract.
|
|
@@ -12,6 +12,8 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
12
12
|
|
|
13
13
|
{{dependencySummaries}}
|
|
14
14
|
|
|
15
|
+
Narrate your decomposition reasoning — why you're grouping work this way, what risks are driving the order, what verification strategy you're choosing and why.
|
|
16
|
+
|
|
15
17
|
Then:
|
|
16
18
|
0. If `REQUIREMENTS.md` was preloaded above, identify which Active requirements the roadmap says this slice owns or supports. These are the requirements this plan must deliver — every owned requirement needs at least one task that directly advances it, and verification must prove the requirement is met.
|
|
17
19
|
1. Read the templates:
|
|
@@ -6,7 +6,7 @@ All relevant context has been preloaded below — start working immediately with
|
|
|
6
6
|
|
|
7
7
|
{{inlinedContext}}
|
|
8
8
|
|
|
9
|
-
Then research the codebase and relevant technologies
|
|
9
|
+
Then research the codebase and relevant technologies. Narrate key findings and surprises as you go — what exists, what's missing, what constrains the approach.
|
|
10
10
|
1. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during research, without relaxing required verification or artifact rules
|
|
11
11
|
2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
|
|
12
12
|
3. Explore relevant code. For small/familiar codebases, use `rg`, `find`, and targeted reads. For large or unfamiliar codebases, use `scout` to build a broad map efficiently before diving in.
|
|
@@ -12,7 +12,7 @@ Pay particular attention to **Forward Intelligence** sections — they contain h
|
|
|
12
12
|
|
|
13
13
|
{{dependencySummaries}}
|
|
14
14
|
|
|
15
|
-
Then research what this slice needs
|
|
15
|
+
Then research what this slice needs. Narrate key findings and surprises as you go — what exists, what's missing, what constrains the approach.
|
|
16
16
|
0. If `REQUIREMENTS.md` was preloaded above, identify which Active requirements this slice owns or supports. Research should target these requirements — surfacing risks, unknowns, and implementation constraints that could affect whether the slice actually delivers them.
|
|
17
17
|
1. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during research, without relaxing required verification or artifact rules
|
|
18
18
|
2. **Skill Discovery ({{skillDiscoveryMode}}):**{{skillDiscoveryInstructions}}
|