gsd-pi 2.3.5 → 2.3.6

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 CHANGED
@@ -2,11 +2,11 @@
2
2
 
3
3
  # GSD 2
4
4
 
5
- **The evolution of [Get Shit Done](https://github.com/glittercowboy/get-shit-done) — now a real coding agent.**
5
+ **The evolution of [Get Shit Done](https://github.com/gsd-build/get-shit-done) — now a real coding agent.**
6
6
 
7
7
  [![npm version](https://img.shields.io/npm/v/gsd-pi?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/gsd-pi)
8
8
  [![npm downloads](https://img.shields.io/npm/dm/gsd-pi?style=for-the-badge&logo=npm&logoColor=white&color=CB3837)](https://www.npmjs.com/package/gsd-pi)
9
- [![GitHub stars](https://img.shields.io/github/stars/glittercowboy/gsd-pi?style=for-the-badge&logo=github&color=181717)](https://github.com/glittercowboy/gsd-pi)
9
+ [![GitHub stars](https://img.shields.io/github/stars/gsd-build/GSD-2?style=for-the-badge&logo=github&color=181717)](https://github.com/gsd-build/GSD-2)
10
10
  [![License](https://img.shields.io/badge/license-MIT-blue?style=for-the-badge)](LICENSE)
11
11
 
12
12
  The original GSD went viral as a prompt framework for Claude Code. It worked, but it was fighting the tool — injecting prompts through slash commands, hoping the LLM would follow instructions, with no actual control over context windows, sessions, or execution.
@@ -122,16 +122,18 @@ Auto mode is a state machine driven by files on disk. It reads `.gsd/STATE.md`,
122
122
 
123
123
  9. **Escape hatch** — Press Escape to pause. The conversation is preserved. Interact with the agent, inspect what happened, or just `/gsd auto` to resume from disk state.
124
124
 
125
- ### The `/gsd` Wizard
125
+ ### `/gsd` and `/gsd next` — Step Mode
126
126
 
127
- When you're not in auto mode, `/gsd` reads disk state and shows contextual options:
127
+ By default, `/gsd` runs in **step mode**: the same state machine as auto mode, but it pauses between units with a wizard showing what completed and what's next. You advance one step at a time, review the output, and continue when ready.
128
128
 
129
129
  - **No `.gsd/` directory** → Start a new project. Discussion flow captures your vision, constraints, and preferences.
130
130
  - **Milestone exists, no roadmap** → Discuss or research the milestone.
131
- - **Roadmap exists, slices pending** → Plan the next slice, or jump straight to auto.
131
+ - **Roadmap exists, slices pending** → Plan the next slice, execute one task, or switch to auto.
132
132
  - **Mid-task** → Resume from where you left off.
133
133
 
134
- The wizard is the on-ramp. Auto mode is the highway.
134
+ `/gsd next` is an explicit alias for step mode. You can switch from step → auto mid-session via the wizard.
135
+
136
+ Step mode is the on-ramp. Auto mode is the highway.
135
137
 
136
138
  ---
137
139
 
@@ -170,7 +172,7 @@ gsd
170
172
 
171
173
  GSD opens an interactive agent session. From there, you have two ways to work:
172
174
 
173
- **`/gsd` — guided mode.** Type `/gsd` and GSD reads your project state and walks you through whatever's next. No project yet? It helps you describe what you want to build. Roadmap exists? It plans the next slice. Mid-task? It resumes. This is the hands-on mode where you work *with* the agent step by step.
175
+ **`/gsd` — step mode.** Type `/gsd` and GSD executes one unit of work at a time, pausing between each with a wizard showing what completed and what's next. Same state machine as auto mode, but you stay in the loop. No project yet? It starts the discussion flow. Roadmap exists? It plans or executes the next step.
174
176
 
175
177
  **`/gsd auto` — autonomous mode.** Type `/gsd auto` and walk away. GSD researches, plans, executes, verifies, commits, and advances through every slice until the milestone is complete. Fresh context window per task. No babysitting.
176
178
 
@@ -196,13 +198,14 @@ Both terminals read and write the same `.gsd/` files on disk. Your decisions in
196
198
 
197
199
  ### First launch
198
200
 
199
- On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
201
+ On first run, GSD prompts for optional API keys (Brave Search, Google Gemini, Context7, Jina) for web research and documentation tools. All optional — press Enter to skip any.
200
202
 
201
203
  ### Commands
202
204
 
203
205
  | Command | What it does |
204
206
  |---------|-------------|
205
- | `/gsd` | Guided mode — reads project state, walks you through what's next |
207
+ | `/gsd` | Step mode — executes one unit at a time, pauses between each |
208
+ | `/gsd next` | Explicit step mode (same as bare `/gsd`) |
206
209
  | `/gsd auto` | Autonomous mode — researches, plans, executes, commits, repeats |
207
210
  | `/gsd stop` | Stop auto mode gracefully |
208
211
  | `/gsd discuss` | Discuss architecture and decisions (works alongside auto mode) |
@@ -211,7 +214,13 @@ On first run, GSD prompts for optional API keys (Brave Search, Context7, Jina) f
211
214
  | `/gsd prefs` | Model selection, timeouts, budget ceiling |
212
215
  | `/gsd migrate` | Migrate a v1 `.planning` directory to `.gsd` format |
213
216
  | `/gsd doctor` | Validate `.gsd/` integrity, find and fix issues |
217
+ | `/worktree` (`/wt`) | Git worktree lifecycle — create, switch, merge, remove |
218
+ | `/voice` | Toggle real-time speech-to-text (macOS only) |
219
+ | `/exit` | Kill GSD process immediately |
220
+ | `/clear` | Start a new session (alias for `/new`) |
214
221
  | `Ctrl+Alt+G` | Toggle dashboard overlay |
222
+ | `Ctrl+Alt+V` | Toggle voice transcription |
223
+ | `Ctrl+Alt+B` | Show background shell processes |
215
224
 
216
225
  ---
217
226
 
@@ -311,16 +320,20 @@ budget_ceiling: 50.00
311
320
 
312
321
  ### Bundled Tools
313
322
 
314
- GSD ships with 9 extensions, all loaded automatically:
323
+ GSD ships with 13 extensions, all loaded automatically:
315
324
 
316
325
  | Extension | What it provides |
317
326
  |-----------|-----------------|
318
327
  | **GSD** | Core workflow engine, auto mode, commands, dashboard |
319
328
  | **Browser Tools** | Playwright-based browser for UI verification |
320
329
  | **Search the Web** | Brave Search + Jina page extraction |
330
+ | **Google Search** | Gemini-powered web search with AI-synthesized answers |
321
331
  | **Context7** | Up-to-date library/framework documentation |
322
332
  | **Background Shell** | Long-running process management with readiness detection |
323
333
  | **Subagent** | Delegated tasks with isolated context windows |
334
+ | **Mac Tools** | macOS native app automation via Accessibility APIs |
335
+ | **MCPorter** | Lazy on-demand MCP server integration |
336
+ | **Voice** | Real-time speech-to-text transcription (macOS) |
324
337
  | **Slash Commands** | Custom command creation |
325
338
  | **Ask User Questions** | Structured user input with single/multi-select |
326
339
  | **Secure Env Collect** | Masked secret collection without manual .env editing |
@@ -345,12 +358,12 @@ GSD is a TypeScript application that embeds the Pi coding agent SDK.
345
358
  gsd (CLI binary)
346
359
  └─ loader.ts Sets PI_PACKAGE_DIR, GSD env vars, dynamic-imports cli.ts
347
360
  └─ cli.ts Wires SDK managers, loads extensions, starts InteractiveMode
348
- ├─ wizard.ts First-run API key collection (Brave/Context7/Jina)
361
+ ├─ wizard.ts First-run API key collection (Brave/Gemini/Context7/Jina)
349
362
  ├─ app-paths.ts ~/.gsd/agent/, ~/.gsd/sessions/, auth.json
350
363
  ├─ resource-loader.ts Syncs bundled extensions + agents to ~/.gsd/agent/
351
364
  └─ src/resources/
352
365
  ├─ extensions/gsd/ Core GSD extension (auto, state, commands, ...)
353
- ├─ extensions/... 10 supporting extensions
366
+ ├─ extensions/... 12 supporting extensions
354
367
  ├─ agents/ scout, researcher, worker
355
368
  ├─ AGENTS.md Agent routing instructions
356
369
  └─ GSD-WORKFLOW.md Manual bootstrap protocol
@@ -373,6 +386,7 @@ gsd (CLI binary)
373
386
 
374
387
  Optional:
375
388
  - Brave Search API key (web research)
389
+ - Google Gemini API key (web research via Gemini Search grounding)
376
390
  - Context7 API key (library docs)
377
391
  - Jina API key (page extraction)
378
392
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AuthStorage, DefaultResourceLoader, ModelRegistry, SettingsManager, SessionManager, createAgentSession, InteractiveMode, runPrintMode, } from '@mariozechner/pi-coding-agent';
2
- import { readFileSync } from 'node:fs';
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
5
  import { initResources } from './resource-loader.js';
@@ -144,6 +144,29 @@ if (isPrintMode) {
144
144
  const cwd = process.cwd();
145
145
  const safePath = `--${cwd.replace(/^[/\\]/, '').replace(/[/\\:]/g, '-')}--`;
146
146
  const projectSessionsDir = join(sessionsDir, safePath);
147
+ // Migrate legacy flat sessions: before per-directory scoping, all .jsonl session
148
+ // files lived directly in ~/.gsd/sessions/. Move them into the correct per-cwd
149
+ // subdirectory so /resume can find them.
150
+ if (existsSync(sessionsDir)) {
151
+ try {
152
+ const entries = readdirSync(sessionsDir);
153
+ const flatJsonl = entries.filter(f => f.endsWith('.jsonl'));
154
+ if (flatJsonl.length > 0) {
155
+ const { mkdirSync } = await import('node:fs');
156
+ mkdirSync(projectSessionsDir, { recursive: true });
157
+ for (const file of flatJsonl) {
158
+ const src = join(sessionsDir, file);
159
+ const dst = join(projectSessionsDir, file);
160
+ if (!existsSync(dst)) {
161
+ renameSync(src, dst);
162
+ }
163
+ }
164
+ }
165
+ }
166
+ catch {
167
+ // Non-fatal — don't block startup if migration fails
168
+ }
169
+ }
147
170
  const sessionManager = SessionManager.create(cwd, projectSessionsDir);
148
171
  initResources(agentDir);
149
172
  const resourceLoader = new DefaultResourceLoader({ agentDir });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -44,10 +44,12 @@ try {
44
44
  }
45
45
 
46
46
  // Install Playwright chromium for browser tools (non-fatal)
47
- const args = os.platform() === 'linux' ? '--with-deps' : ''
48
47
  try {
49
- execSync(`npx playwright install chromium ${args}`, { stdio: 'inherit' })
48
+ execSync('npx playwright install chromium', { stdio: 'inherit' })
50
49
  process.stderr.write(`\n ${green}✓${reset} Browser tools ready\n\n`)
51
50
  } catch {
52
- process.stderr.write(`\n ${yellow}⚠${reset} Browser tools unavailable run ${cyan}npx playwright install chromium${reset} to enable\n\n`)
51
+ const hint = os.platform() === 'linux'
52
+ ? `${cyan}npx playwright install --with-deps chromium${reset}`
53
+ : `${cyan}npx playwright install chromium${reset}`
54
+ process.stderr.write(`\n ${yellow}⚠${reset} Browser tools unavailable — run ${hint} to enable\n\n`)
53
55
  }
@@ -86,6 +86,17 @@ export function ensureSliceBranch(basePath: string, milestoneId: string, sliceId
86
86
  created = true;
87
87
  }
88
88
 
89
+ // Auto-commit dirty files before checkout to prevent "would be overwritten" errors.
90
+ // This handles cases where doctor, STATE.md rebuild, or agent work left uncommitted changes.
91
+ const status = runGit(basePath, ["status", "--short"]);
92
+ if (status.trim()) {
93
+ runGit(basePath, ["add", "-A"]);
94
+ const staged = runGit(basePath, ["diff", "--cached", "--stat"]);
95
+ if (staged.trim()) {
96
+ runGit(basePath, ["commit", "-m", `"chore: auto-commit before switching to ${branch}"`]);
97
+ }
98
+ }
99
+
89
100
  runGit(basePath, ["checkout", branch]);
90
101
  return created;
91
102
  }
@@ -1,207 +0,0 @@
1
- /**
2
- * Formatters — produce text summaries for issues, PRs, comments, etc.
3
- *
4
- * Used by both tools (LLM context) and renderers (TUI display).
5
- */
6
-
7
- import type { GhIssue, GhPullRequest, GhComment, GhReview, GhLabel, GhMilestone } from "./gh-api.js";
8
-
9
- // ─── Helpers ──────────────────────────────────────────────────────────────────
10
-
11
- function timeAgo(dateStr: string): string {
12
- const now = Date.now();
13
- const then = new Date(dateStr).getTime();
14
- const diff = now - then;
15
- const mins = Math.floor(diff / 60000);
16
- if (mins < 1) return "just now";
17
- if (mins < 60) return `${mins}m ago`;
18
- const hours = Math.floor(mins / 60);
19
- if (hours < 24) return `${hours}h ago`;
20
- const days = Math.floor(hours / 24);
21
- if (days < 30) return `${days}d ago`;
22
- const months = Math.floor(days / 30);
23
- if (months < 12) return `${months}mo ago`;
24
- return `${Math.floor(months / 12)}y ago`;
25
- }
26
-
27
- function stateIcon(state: string, draft?: boolean): string {
28
- if (draft) return "◇";
29
- switch (state) {
30
- case "open":
31
- return "●";
32
- case "closed":
33
- return "✓";
34
- case "merged":
35
- return "⊕";
36
- default:
37
- return "○";
38
- }
39
- }
40
-
41
- function truncateBody(body: string | null, maxLines = 10): string {
42
- if (!body) return "(no description)";
43
- const lines = body.split("\n");
44
- if (lines.length <= maxLines) return body;
45
- return lines.slice(0, maxLines).join("\n") + `\n... (${lines.length - maxLines} more lines)`;
46
- }
47
-
48
- // ─── Issue formatting ─────────────────────────────────────────────────────────
49
-
50
- export function formatIssueOneLiner(issue: GhIssue): string {
51
- const icon = stateIcon(issue.state);
52
- const labels = issue.labels.map((l) => l.name).join(", ");
53
- const labelStr = labels ? ` [${labels}]` : "";
54
- const assignee = issue.assignees.length ? ` → ${issue.assignees.map((a) => a.login).join(", ")}` : "";
55
- return `${icon} #${issue.number} ${issue.title}${labelStr}${assignee} (${timeAgo(issue.updated_at)})`;
56
- }
57
-
58
- export function formatIssueDetail(issue: GhIssue): string {
59
- const lines: string[] = [];
60
- lines.push(`# Issue #${issue.number}: ${issue.title}`);
61
- lines.push(`State: ${issue.state} | Author: @${issue.user.login} | Created: ${timeAgo(issue.created_at)} | Updated: ${timeAgo(issue.updated_at)}`);
62
-
63
- if (issue.assignees.length) {
64
- lines.push(`Assignees: ${issue.assignees.map((a) => `@${a.login}`).join(", ")}`);
65
- }
66
- if (issue.labels.length) {
67
- lines.push(`Labels: ${issue.labels.map((l) => l.name).join(", ")}`);
68
- }
69
- if (issue.milestone) {
70
- lines.push(`Milestone: ${issue.milestone.title}`);
71
- }
72
- lines.push(`Comments: ${issue.comments}`);
73
- lines.push(`URL: ${issue.html_url}`);
74
- lines.push("");
75
- lines.push(truncateBody(issue.body, 30));
76
- return lines.join("\n");
77
- }
78
-
79
- export function formatIssueList(issues: GhIssue[]): string {
80
- if (!issues.length) return "No issues found.";
81
- return issues.map(formatIssueOneLiner).join("\n");
82
- }
83
-
84
- // ─── PR formatting ────────────────────────────────────────────────────────────
85
-
86
- export function formatPROneLiner(pr: GhPullRequest): string {
87
- const icon = stateIcon(pr.merged_at ? "merged" : pr.state, pr.draft);
88
- const labels = pr.labels.map((l) => l.name).join(", ");
89
- const labelStr = labels ? ` [${labels}]` : "";
90
- const draftStr = pr.draft ? " (draft)" : "";
91
- const reviewers = pr.requested_reviewers.map((r) => r.login).join(", ");
92
- const reviewerStr = reviewers ? ` ⟵ ${reviewers}` : "";
93
- return `${icon} #${pr.number} ${pr.title}${draftStr}${labelStr}${reviewerStr} (${timeAgo(pr.updated_at)})`;
94
- }
95
-
96
- export function formatPRDetail(pr: GhPullRequest): string {
97
- const lines: string[] = [];
98
- const mergedState = pr.merged_at ? "merged" : pr.state;
99
- lines.push(`# PR #${pr.number}: ${pr.title}`);
100
- lines.push(`State: ${mergedState}${pr.draft ? " (draft)" : ""} | Author: @${pr.user.login} | Created: ${timeAgo(pr.created_at)} | Updated: ${timeAgo(pr.updated_at)}`);
101
- lines.push(`Branch: ${pr.head.ref} → ${pr.base.ref}`);
102
-
103
- if (pr.assignees.length) {
104
- lines.push(`Assignees: ${pr.assignees.map((a) => `@${a.login}`).join(", ")}`);
105
- }
106
- if (pr.labels.length) {
107
- lines.push(`Labels: ${pr.labels.map((l) => l.name).join(", ")}`);
108
- }
109
- if (pr.milestone) {
110
- lines.push(`Milestone: ${pr.milestone.title}`);
111
- }
112
- if (pr.requested_reviewers.length) {
113
- lines.push(`Reviewers: ${pr.requested_reviewers.map((r) => `@${r.login}`).join(", ")}`);
114
- }
115
-
116
- lines.push(`Mergeable: ${pr.mergeable === null ? "checking..." : pr.mergeable ? "yes" : "no"} (${pr.mergeable_state})`);
117
- lines.push(`Comments: ${pr.comments} | Review comments: ${pr.review_comments}`);
118
- lines.push(`URL: ${pr.html_url}`);
119
- lines.push("");
120
- lines.push(truncateBody(pr.body, 30));
121
- return lines.join("\n");
122
- }
123
-
124
- export function formatPRList(prs: GhPullRequest[]): string {
125
- if (!prs.length) return "No pull requests found.";
126
- return prs.map(formatPROneLiner).join("\n");
127
- }
128
-
129
- // ─── Comment formatting ──────────────────────────────────────────────────────
130
-
131
- export function formatComment(comment: GhComment): string {
132
- return `@${comment.user.login} (${timeAgo(comment.created_at)}):\n${truncateBody(comment.body, 8)}`;
133
- }
134
-
135
- export function formatCommentList(comments: GhComment[]): string {
136
- if (!comments.length) return "No comments.";
137
- return comments.map(formatComment).join("\n\n---\n\n");
138
- }
139
-
140
- // ─── Review formatting ───────────────────────────────────────────────────────
141
-
142
- function reviewStateIcon(state: string): string {
143
- switch (state) {
144
- case "APPROVED":
145
- return "✓";
146
- case "CHANGES_REQUESTED":
147
- return "✗";
148
- case "COMMENTED":
149
- return "💬";
150
- case "DISMISSED":
151
- return "—";
152
- case "PENDING":
153
- return "…";
154
- default:
155
- return "?";
156
- }
157
- }
158
-
159
- export function formatReview(review: GhReview): string {
160
- const icon = reviewStateIcon(review.state);
161
- const body = review.body ? `\n${truncateBody(review.body, 5)}` : "";
162
- return `${icon} @${review.user.login}: ${review.state} (${timeAgo(review.submitted_at)})${body}`;
163
- }
164
-
165
- export function formatReviewList(reviews: GhReview[]): string {
166
- if (!reviews.length) return "No reviews.";
167
- return reviews.map(formatReview).join("\n\n");
168
- }
169
-
170
- // ─── Label / Milestone formatting ─────────────────────────────────────────────
171
-
172
- export function formatLabel(label: GhLabel): string {
173
- const desc = label.description ? ` — ${label.description}` : "";
174
- return `• ${label.name} (#${label.color})${desc}`;
175
- }
176
-
177
- export function formatLabelList(labels: GhLabel[]): string {
178
- if (!labels.length) return "No labels.";
179
- return labels.map(formatLabel).join("\n");
180
- }
181
-
182
- export function formatMilestone(ms: GhMilestone): string {
183
- const progress = ms.open_issues + ms.closed_issues > 0 ? Math.round((ms.closed_issues / (ms.open_issues + ms.closed_issues)) * 100) : 0;
184
- const due = ms.due_on ? ` | Due: ${new Date(ms.due_on).toISOString().split("T")[0]}` : "";
185
- return `• ${ms.title} (${ms.state}) — ${progress}% complete (${ms.closed_issues}/${ms.open_issues + ms.closed_issues})${due}`;
186
- }
187
-
188
- export function formatMilestoneList(milestones: GhMilestone[]): string {
189
- if (!milestones.length) return "No milestones.";
190
- return milestones.map(formatMilestone).join("\n");
191
- }
192
-
193
- // ─── File change formatting ───────────────────────────────────────────────────
194
-
195
- export function formatFileChanges(
196
- files: { filename: string; status: string; additions: number; deletions: number; changes: number }[],
197
- ): string {
198
- if (!files.length) return "No files changed.";
199
- const lines = files.map((f) => {
200
- const statusIcon = f.status === "added" ? "+" : f.status === "removed" ? "-" : "~";
201
- return `${statusIcon} ${f.filename} (+${f.additions} -${f.deletions})`;
202
- });
203
- const totalAdd = files.reduce((s, f) => s + f.additions, 0);
204
- const totalDel = files.reduce((s, f) => s + f.deletions, 0);
205
- lines.push(`\n${files.length} files changed, +${totalAdd} -${totalDel}`);
206
- return lines.join("\n");
207
- }