gsd-pi 2.3.11 → 2.5.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.
Files changed (27) hide show
  1. package/README.md +5 -4
  2. package/dist/cli.js +4 -2
  3. package/dist/pi-migration.d.ts +14 -0
  4. package/dist/pi-migration.js +57 -0
  5. package/package.json +2 -2
  6. package/src/resources/GSD-WORKFLOW.md +7 -7
  7. package/src/resources/extensions/gsd/auto.ts +78 -23
  8. package/src/resources/extensions/gsd/docs/preferences-reference.md +27 -0
  9. package/src/resources/extensions/gsd/git-service.ts +588 -0
  10. package/src/resources/extensions/gsd/index.ts +11 -6
  11. package/src/resources/extensions/gsd/preferences.ts +51 -0
  12. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  13. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  14. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -1
  15. package/src/resources/extensions/gsd/prompts/plan-milestone.md +2 -0
  16. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -0
  17. package/src/resources/extensions/gsd/prompts/replan-slice.md +1 -1
  18. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  19. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  20. package/src/resources/extensions/gsd/prompts/system.md +62 -216
  21. package/src/resources/extensions/gsd/templates/preferences.md +7 -0
  22. package/src/resources/extensions/gsd/tests/git-service.test.ts +1250 -0
  23. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +0 -2
  24. package/src/resources/extensions/gsd/worktree-command.ts +48 -6
  25. package/src/resources/extensions/gsd/worktree.ts +40 -147
  26. package/src/resources/extensions/search-the-web/index.ts +16 -25
  27. package/src/resources/extensions/search-the-web/native-search.ts +157 -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
 
@@ -252,17 +252,18 @@ Branch-per-slice with squash merge. Fully automated.
252
252
 
253
253
  ```
254
254
  main:
255
- feat(M001/S03): auth and session management
255
+ docs(M001/S04): workflow documentation and examples
256
+ fix(M001/S03): bug fixes and doc corrections
256
257
  feat(M001/S02): API endpoints and middleware
257
258
  feat(M001/S01): data model and type system
258
259
 
259
- gsd/M001/S01 (preserved):
260
+ gsd/M001/S01 (deleted after merge):
260
261
  feat(S01/T03): file writer with round-trip fidelity
261
262
  feat(S01/T02): markdown parser for plan files
262
263
  feat(S01/T01): core types and interfaces
263
264
  ```
264
265
 
265
- One commit per slice on main. Per-task history preserved on branches. Git bisect works. Individual slices are revertable.
266
+ One commit per slice on main. Squash commits are the permanent record — branches are deleted after merge. Git bisect works. Individual slices are revertable.
266
267
 
267
268
  ### Verification
268
269
 
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 = new DefaultResourceLoader({ agentDir });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.11",
3
+ "version": "2.5.0",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,7 +35,7 @@
35
35
  "scripts": {
36
36
  "build": "tsc && npm run copy-themes",
37
37
  "copy-themes": "node -e \"const{mkdirSync,cpSync}=require('fs');const{resolve}=require('path');const src=resolve(__dirname,'node_modules/@mariozechner/pi-coding-agent/dist/modes/interactive/theme');mkdirSync('pkg/dist/modes/interactive/theme',{recursive:true});cpSync(src,'pkg/dist/modes/interactive/theme',{recursive:true})\"",
38
- "test": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test 'src/resources/extensions/gsd/tests/*.test.ts' 'src/resources/extensions/gsd/tests/*.test.mjs' 'src/tests/*.test.ts'",
38
+ "test": "node --import ./src/resources/extensions/gsd/tests/resolve-ts.mjs --experimental-strip-types --test src/resources/extensions/gsd/tests/*.test.ts src/resources/extensions/gsd/tests/*.test.mjs src/tests/*.test.ts",
39
39
  "dev": "tsc --watch",
40
40
  "postinstall": "node scripts/postinstall.js",
41
41
  "pi:install-global": "node scripts/install-pi-global.js",
@@ -548,7 +548,7 @@ If files disagree, **pause and surface to the user**:
548
548
  1. **Slice starts** → create branch `gsd/M001/S01` from main
549
549
  2. **Per-task commits** on the branch — atomic, descriptive, bisectable
550
550
  3. **Slice completes** → squash merge to main as one clean commit
551
- 4. **Branch kept** — not deleted, available for per-task history
551
+ 4. **Branch deleted** — squash commit on main is the permanent record
552
552
 
553
553
  ### What Main Looks Like
554
554
 
@@ -566,21 +566,21 @@ One commit per slice. Individually revertable. Reads like a changelog.
566
566
  gsd/M001/S01:
567
567
  test(S01): round-trip tests passing
568
568
  feat(S01/T03): file writer with round-trip fidelity
569
- checkpoint(S01/T03): pre-task
569
+ chore(S01/T03): auto-commit after task
570
570
  feat(S01/T02): markdown parser for plan files
571
- checkpoint(S01/T02): pre-task
571
+ chore(S01/T02): auto-commit after task
572
572
  feat(S01/T01): core types and interfaces
573
- checkpoint(S01/T01): pre-task
573
+ chore(S01/T01): auto-commit after task
574
574
  ```
575
575
 
576
576
  ### Commit Conventions
577
577
 
578
578
  | When | Format | Example |
579
579
  |------|--------|---------|
580
- | Before each task | `checkpoint(S01/T02): pre-task` | Safety net for `git reset` |
580
+ | Auto-commit (dirty state) | `chore(S01/T02): auto-commit after task` | Automatic save of work in progress |
581
581
  | After task verified | `feat(S01/T02): <what was built>` | The real work |
582
582
  | Plan/docs committed | `docs(S01): add slice plan` | Bundled with first task |
583
- | Slice squash to main | `feat(M001/S01): <slice title>` | Clean one-liner on main |
583
+ | Slice squash to main | `type(M001/S01): <slice title>` | Type inferred from title (`feat`, `fix`, `docs`, etc.) |
584
584
 
585
585
  Commit types: `feat`, `fix`, `test`, `refactor`, `docs`, `chore`
586
586
 
@@ -601,7 +601,7 @@ Tasks completed:
601
601
 
602
602
  | Problem | Fix |
603
603
  |---------|-----|
604
- | Bad task | `git reset --hard` to checkpoint on the branch |
604
+ | Bad task | `git reset --hard HEAD~1` to previous commit on the branch |
605
605
  | Bad slice | `git revert <squash commit>` on main |
606
606
  | UAT failure after merge | Fix tasks on `gsd/M001/S01-fix` branch, squash as `fix(M001/S01): <fix>` |
607
607
 
@@ -62,11 +62,12 @@ import {
62
62
  ensureSliceBranch,
63
63
  getCurrentBranch,
64
64
  getMainBranch,
65
- getSliceBranchName,
66
65
  parseSliceBranch,
67
66
  switchToMain,
68
67
  mergeSliceToMain,
69
68
  } from "./worktree.ts";
69
+ import { GitServiceImpl } from "./git-service.ts";
70
+ import type { GitPreferences } from "./git-service.ts";
70
71
  import { truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
71
72
  import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
72
73
  import { showNextAction } from "../shared/next-action-ui.js";
@@ -93,6 +94,18 @@ function persistCompletedKey(base: string, key: string): void {
93
94
  }
94
95
  }
95
96
 
97
+ /** Remove a stale completed unit key from disk. */
98
+ function removePersistedKey(base: string, key: string): void {
99
+ const file = completedKeysPath(base);
100
+ try {
101
+ if (existsSync(file)) {
102
+ let keys: string[] = JSON.parse(readFileSync(file, "utf-8"));
103
+ keys = keys.filter(k => k !== key);
104
+ writeFileSync(file, JSON.stringify(keys), "utf-8");
105
+ }
106
+ } catch { /* non-fatal */ }
107
+ }
108
+
96
109
  /** Load all completed unit keys from disk into the in-memory set. */
97
110
  function loadPersistedKeys(base: string, target: Set<string>): void {
98
111
  const file = completedKeysPath(base);
@@ -112,6 +125,7 @@ let stepMode = false;
112
125
  let verbose = false;
113
126
  let cmdCtx: ExtensionCommandContext | null = null;
114
127
  let basePath = "";
128
+ let gitService: GitServiceImpl | null = null;
115
129
 
116
130
  /** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
117
131
  const unitDispatchCount = new Map<string, number>();
@@ -376,6 +390,9 @@ export async function startAuto(
376
390
  } catch { /* nothing to commit */ }
377
391
  }
378
392
 
393
+ // Initialize GitServiceImpl — basePath is set and git repo confirmed
394
+ gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
395
+
379
396
  // Check for crash from previous session
380
397
  const crashLock = readCrashLock(base);
381
398
  if (crashLock) {
@@ -1190,15 +1207,27 @@ async function dispatchNextUnit(
1190
1207
  // Idempotency: skip units already completed in a prior session.
1191
1208
  const idempotencyKey = `${unitType}/${unitId}`;
1192
1209
  if (completedKeySet.has(idempotencyKey)) {
1193
- ctx.ui.notify(
1194
- `Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
1195
- "info",
1196
- );
1197
- // Yield to the event loop before re-dispatching to avoid tight recursion
1198
- // when many units are already completed (e.g., after crash recovery).
1199
- await new Promise(r => setImmediate(r));
1200
- await dispatchNextUnit(ctx, pi);
1201
- return;
1210
+ // Cross-validate: does the expected artifact actually exist?
1211
+ const artifactExists = verifyExpectedArtifact(unitType, unitId, basePath);
1212
+ if (artifactExists) {
1213
+ ctx.ui.notify(
1214
+ `Skipping ${unitType} ${unitId} already completed in a prior session. Advancing.`,
1215
+ "info",
1216
+ );
1217
+ // Yield to the event loop before re-dispatching to avoid tight recursion
1218
+ // when many units are already completed (e.g., after crash recovery).
1219
+ await new Promise(r => setImmediate(r));
1220
+ await dispatchNextUnit(ctx, pi);
1221
+ return;
1222
+ } else {
1223
+ // Stale completion record — artifact missing. Remove and re-run.
1224
+ completedKeySet.delete(idempotencyKey);
1225
+ removePersistedKey(basePath, idempotencyKey);
1226
+ ctx.ui.notify(
1227
+ `Re-running ${unitType} ${unitId} — marked complete but expected artifact missing.`,
1228
+ "warning",
1229
+ );
1230
+ }
1202
1231
  }
1203
1232
 
1204
1233
  // Stuck detection — tracks total dispatches per unit (not just consecutive repeats).
@@ -1234,20 +1263,26 @@ async function dispatchNextUnit(
1234
1263
  snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
1235
1264
  saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
1236
1265
 
1237
- // Persist completion to disk BEFORE updating memory — so a crash here is recoverable.
1266
+ // Only mark the previous unit as completed if:
1267
+ // 1. We're not about to re-dispatch the same unit (retry scenario)
1268
+ // 2. The expected artifact actually exists on disk
1238
1269
  const closeoutKey = `${currentUnit.type}/${currentUnit.id}`;
1239
- persistCompletedKey(basePath, closeoutKey);
1240
- completedKeySet.add(closeoutKey);
1241
-
1242
- completedUnits.push({
1243
- type: currentUnit.type,
1244
- id: currentUnit.id,
1245
- startedAt: currentUnit.startedAt,
1246
- finishedAt: Date.now(),
1247
- });
1248
- clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
1249
- unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1250
- unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1270
+ const incomingKey = `${unitType}/${unitId}`;
1271
+ const artifactVerified = verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath);
1272
+ if (closeoutKey !== incomingKey && artifactVerified) {
1273
+ persistCompletedKey(basePath, closeoutKey);
1274
+ completedKeySet.add(closeoutKey);
1275
+
1276
+ completedUnits.push({
1277
+ type: currentUnit.type,
1278
+ id: currentUnit.id,
1279
+ startedAt: currentUnit.startedAt,
1280
+ finishedAt: Date.now(),
1281
+ });
1282
+ clearUnitRuntimeRecord(basePath, currentUnit.type, currentUnit.id);
1283
+ unitDispatchCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1284
+ unitRecoveryCount.delete(`${currentUnit.type}/${currentUnit.id}`);
1285
+ }
1251
1286
  }
1252
1287
  currentUnit = { type: unitType, id: unitId, startedAt: Date.now() };
1253
1288
  writeUnitRuntimeRecord(basePath, unitType, unitId, currentUnit.startedAt, {
@@ -2587,6 +2622,15 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
2587
2622
  const dir = resolveSlicePath(base, mid, sid!);
2588
2623
  return dir ? join(dir, buildSliceFileName(sid!, "UAT-RESULT")) : null;
2589
2624
  }
2625
+ case "execute-task": {
2626
+ const tid = parts[2];
2627
+ const dir = resolveSlicePath(base, mid, sid!);
2628
+ return dir && tid ? join(dir, "tasks", buildTaskFileName(tid, "SUMMARY")) : null;
2629
+ }
2630
+ case "complete-slice": {
2631
+ const dir = resolveSlicePath(base, mid, sid!);
2632
+ return dir ? join(dir, buildSliceFileName(sid!, "SUMMARY")) : null;
2633
+ }
2590
2634
  case "complete-milestone": {
2591
2635
  const dir = resolveMilestonePath(base, mid);
2592
2636
  return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
@@ -2596,6 +2640,17 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
2596
2640
  }
2597
2641
  }
2598
2642
 
2643
+ /**
2644
+ * Check whether the expected artifact for a unit exists on disk.
2645
+ * Returns true if the artifact file exists, or if the unit type has no
2646
+ * single verifiable artifact (e.g., replan-slice).
2647
+ */
2648
+ function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
2649
+ const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
2650
+ if (!absPath) return true;
2651
+ return existsSync(absPath);
2652
+ }
2653
+
2599
2654
  /**
2600
2655
  * Write a placeholder artifact so the pipeline can advance past a stuck unit.
2601
2656
  * Returns the relative path written, or null if the path couldn't be resolved.
@@ -40,6 +40,14 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
40
40
  - `idle_timeout_minutes`: minutes of inactivity before the supervisor intervenes (default: 10).
41
41
  - `hard_timeout_minutes`: minutes before the supervisor forces termination (default: 30).
42
42
 
43
+ - `git`: configures GSD's git behavior. All fields are optional — omit any to use defaults. Keys:
44
+ - `auto_push`: boolean — automatically push commits to the remote after committing. Default: `false`.
45
+ - `push_branches`: boolean — push newly created slice branches to the remote. Default: `false`.
46
+ - `remote`: string — git remote name to push to. Default: `"origin"`.
47
+ - `snapshots`: boolean — create snapshot commits (WIP saves) during long-running tasks. Default: `false`.
48
+ - `pre_merge_check`: boolean or `"auto"` — run pre-merge checks before merging a slice branch. `true` always runs, `false` never runs, `"auto"` runs when CI is detected. Default: `false`.
49
+ - `commit_type`: string — override the conventional commit type prefix. Must be one of: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf`, `ci`, `build`, `style`. Default: inferred from diff content.
50
+
43
51
  ---
44
52
 
45
53
  ## Best Practices
@@ -101,3 +109,22 @@ skill_rules:
101
109
  - find-skills
102
110
  ---
103
111
  ```
112
+
113
+ ---
114
+
115
+ ## Git Preferences Example
116
+
117
+ ```yaml
118
+ ---
119
+ version: 1
120
+ git:
121
+ auto_push: true
122
+ push_branches: true
123
+ remote: origin
124
+ snapshots: true
125
+ pre_merge_check: auto
126
+ commit_type: feat
127
+ ---
128
+ ```
129
+
130
+ All git fields are optional. Omit any field to use the default behavior. Project-level preferences override global preferences on a per-field basis.