gsd-pi 2.30.0 → 2.31.0-dev.6ffc032

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 (75) hide show
  1. package/dist/cli.js +51 -0
  2. package/dist/help-text.js +35 -0
  3. package/dist/resources/extensions/aws-auth/index.ts +144 -0
  4. package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
  5. package/dist/resources/extensions/gsd/auto-prompts.ts +2 -10
  6. package/dist/resources/extensions/gsd/auto-start.ts +3 -10
  7. package/dist/resources/extensions/gsd/auto-supervisor.ts +2 -0
  8. package/dist/resources/extensions/gsd/auto-worktree.ts +12 -8
  9. package/dist/resources/extensions/gsd/auto.ts +2 -2
  10. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  11. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  12. package/dist/resources/extensions/gsd/git-service.ts +4 -22
  13. package/dist/resources/extensions/gsd/gitignore.ts +6 -7
  14. package/dist/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  15. package/dist/resources/extensions/gsd/guided-flow.ts +8 -11
  16. package/dist/resources/extensions/gsd/index.ts +13 -0
  17. package/dist/resources/extensions/gsd/init-wizard.ts +2 -30
  18. package/dist/resources/extensions/gsd/preferences-types.ts +0 -2
  19. package/dist/resources/extensions/gsd/preferences-validation.ts +1 -2
  20. package/dist/resources/extensions/gsd/roadmap-slices.ts +22 -7
  21. package/dist/resources/extensions/gsd/session-lock.ts +72 -5
  22. package/dist/resources/extensions/gsd/templates/preferences.md +0 -1
  23. package/dist/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  24. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  25. package/dist/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  26. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  27. package/dist/resources/extensions/gsd/worktree.ts +2 -2
  28. package/dist/worktree-cli.d.ts +34 -0
  29. package/dist/worktree-cli.js +294 -0
  30. package/dist/worktree-name-gen.d.ts +7 -0
  31. package/dist/worktree-name-gen.js +44 -0
  32. package/package.json +1 -1
  33. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  34. package/packages/pi-coding-agent/dist/core/agent-session.js +14 -0
  35. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  37. package/packages/pi-coding-agent/dist/core/extensions/loader.js +4 -0
  38. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  40. package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  41. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  42. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  43. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  44. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  45. package/packages/pi-coding-agent/package.json +1 -1
  46. package/packages/pi-coding-agent/src/core/agent-session.ts +14 -0
  47. package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
  48. package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
  49. package/packages/pi-coding-agent/src/core/extensions/types.ts +8 -0
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/aws-auth/index.ts +144 -0
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
  53. package/src/resources/extensions/gsd/auto-prompts.ts +2 -10
  54. package/src/resources/extensions/gsd/auto-start.ts +3 -10
  55. package/src/resources/extensions/gsd/auto-supervisor.ts +2 -0
  56. package/src/resources/extensions/gsd/auto-worktree.ts +12 -8
  57. package/src/resources/extensions/gsd/auto.ts +2 -2
  58. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  59. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  60. package/src/resources/extensions/gsd/git-service.ts +4 -22
  61. package/src/resources/extensions/gsd/gitignore.ts +6 -7
  62. package/src/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  63. package/src/resources/extensions/gsd/guided-flow.ts +8 -11
  64. package/src/resources/extensions/gsd/index.ts +13 -0
  65. package/src/resources/extensions/gsd/init-wizard.ts +2 -30
  66. package/src/resources/extensions/gsd/preferences-types.ts +0 -2
  67. package/src/resources/extensions/gsd/preferences-validation.ts +1 -2
  68. package/src/resources/extensions/gsd/roadmap-slices.ts +22 -7
  69. package/src/resources/extensions/gsd/session-lock.ts +72 -5
  70. package/src/resources/extensions/gsd/templates/preferences.md +0 -1
  71. package/src/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  72. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  73. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  74. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  75. package/src/resources/extensions/gsd/worktree.ts +2 -2
@@ -37,7 +37,7 @@ import {
37
37
  } from "./session-lock.js";
38
38
  import { selfHealRuntimeRecords } from "./auto-recovery.js";
39
39
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
40
- import { nativeIsRepo, nativeInit, nativeAddAll, nativeCommit } from "./native-git-bridge.js";
40
+ import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
41
41
  import { GitServiceImpl } from "./git-service.js";
42
42
  import {
43
43
  captureIntegrationBranch,
@@ -109,9 +109,8 @@ export async function bootstrapAutoSession(
109
109
 
110
110
  // Ensure .gitignore has baseline patterns
111
111
  const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
112
- const commitDocs = gitPrefs?.commit_docs;
113
112
  const manageGitignore = gitPrefs?.manage_gitignore;
114
- ensureGitignore(base, { commitDocs, manageGitignore });
113
+ ensureGitignore(base, { manageGitignore });
115
114
  if (manageGitignore !== false) untrackRuntimeFiles(base);
116
115
 
117
116
  // Migrate legacy in-project .gsd/ to external state directory
@@ -127,12 +126,6 @@ export async function bootstrapAutoSession(
127
126
  const gsdDir = gsdRoot(base);
128
127
  if (!existsSync(gsdDir)) {
129
128
  mkdirSync(join(gsdDir, "milestones"), { recursive: true });
130
- if (commitDocs !== false) {
131
- try {
132
- nativeAddAll(base);
133
- nativeCommit(base, "chore: init gsd");
134
- } catch { /* nothing to commit */ }
135
- }
136
129
  }
137
130
 
138
131
  // Initialize GitServiceImpl
@@ -323,7 +316,7 @@ export async function bootstrapAutoSession(
323
316
  // Capture integration branch
324
317
  if (s.currentMilestoneId) {
325
318
  if (getIsolationMode() !== "none") {
326
- captureIntegrationBranch(base, s.currentMilestoneId, { commitDocs });
319
+ captureIntegrationBranch(base, s.currentMilestoneId);
327
320
  }
328
321
  setActiveMilestoneId(base, s.currentMilestoneId);
329
322
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { clearLock } from "./crash-recovery.js";
8
+ import { releaseSessionLock } from "./session-lock.js";
8
9
  import { nativeHasChanges } from "./native-git-bridge.js";
9
10
 
10
11
  // ─── SIGTERM Handling ─────────────────────────────────────────────────────────
@@ -23,6 +24,7 @@ export function registerSigtermHandler(
23
24
  ): () => void {
24
25
  if (previousHandler) process.off("SIGTERM", previousHandler);
25
26
  const handler = () => {
27
+ releaseSessionLock(currentBasePath);
26
28
  clearLock(currentBasePath);
27
29
  process.exit(0);
28
30
  };
@@ -6,7 +6,7 @@
6
6
  * manages create, enter, detect, and teardown for auto-mode worktrees.
7
7
  */
8
8
 
9
- import { existsSync, readFileSync, realpathSync, unlinkSync, statSync } from "node:fs";
9
+ import { existsSync, readFileSync, realpathSync, unlinkSync, statSync, rmSync } from "node:fs";
10
10
  import { isAbsolute, join, sep } from "node:path";
11
11
  import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
12
12
  import { execSync, execFileSync } from "node:child_process";
@@ -370,14 +370,18 @@ export function mergeMilestoneToMain(
370
370
  // squash merge can fail with "Your local changes would be overwritten" (#1127).
371
371
  autoCommitDirtyState(originalBasePath_);
372
372
 
373
- // 3b. Remove untracked .gsd/ files that syncStateToProjectRoot copied.
374
- // autoCommitDirtyState stages and commits everything (git add -A), but if
375
- // the project root branch has no .gsd/ tracking (e.g., .gsd/ is gitignored),
376
- // these files remain untracked and cause "untracked working tree files would
377
- // be overwritten by merge" during squash-merge (#1237).
373
+ // 3b. Remove untracked .gsd/ runtime files that syncStateToProjectRoot copied.
374
+ // Only clean specific runtime files NEVER touch milestones/, decisions, or
375
+ // other planning artifacts that represent user work (#1250).
376
+ const runtimeFilesToClean = ["STATE.md", "completed-units.json", "auto.lock", "gsd.db"];
377
+ for (const f of runtimeFilesToClean) {
378
+ const p = join(originalBasePath_, ".gsd", f);
379
+ try { if (existsSync(p)) unlinkSync(p); } catch { /* non-fatal */ }
380
+ }
378
381
  try {
379
- execFileSync("git", ["clean", "-fd", ".gsd/"], { cwd: originalBasePath_, stdio: "pipe" });
380
- } catch { /* non-fatal clean failure shouldn't block merge attempt */ }
382
+ const runtimeDir = join(originalBasePath_, ".gsd", "runtime");
383
+ if (existsSync(runtimeDir)) rmSync(runtimeDir, { recursive: true, force: true });
384
+ } catch { /* non-fatal */ }
381
385
 
382
386
  // 4. Resolve integration branch — prefer milestone metadata, fall back to preferences / "main"
383
387
  const prefs = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
@@ -1132,7 +1132,7 @@ async function dispatchNextUnit(
1132
1132
  midTitle = state.activeMilestone?.title;
1133
1133
 
1134
1134
  if (mid) {
1135
- captureIntegrationBranch(s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1135
+ captureIntegrationBranch(s.basePath, mid);
1136
1136
  try {
1137
1137
  const wtPath = createAutoWorktree(s.basePath, mid);
1138
1138
  s.basePath = wtPath;
@@ -1147,7 +1147,7 @@ async function dispatchNextUnit(
1147
1147
  }
1148
1148
  } else {
1149
1149
  if (getIsolationMode() !== "none") {
1150
- captureIntegrationBranch(s.originalBasePath || s.basePath, mid, { commitDocs: loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs });
1150
+ captureIntegrationBranch(s.originalBasePath || s.basePath, mid);
1151
1151
  }
1152
1152
  }
1153
1153
 
@@ -469,16 +469,6 @@ async function configureGit(ctx: ExtensionCommandContext, prefs: Record<string,
469
469
  git.isolation = isolationChoice;
470
470
  }
471
471
 
472
- // commit_docs
473
- const currentCommitDocs = git.commit_docs;
474
- const commitDocsChoice = await ctx.ui.select(
475
- `Track .gsd/ planning docs in git${currentCommitDocs !== undefined ? ` (current: ${currentCommitDocs})` : ""}:`,
476
- ["true", "false", "(keep current)"],
477
- );
478
- if (commitDocsChoice && commitDocsChoice !== "(keep current)") {
479
- git.commit_docs = commitDocsChoice === "true";
480
- }
481
-
482
472
  if (Object.keys(git).length > 0) {
483
473
  prefs.git = git;
484
474
  }
@@ -598,13 +588,13 @@ export async function configureMode(ctx: ExtensionCommandContext, prefs: Record<
598
588
  if (modeStr.startsWith("solo")) {
599
589
  prefs.mode = "solo";
600
590
  ctx.ui.notify(
601
- "Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=false",
591
+ "Mode: solo — defaults: auto_push=true, push_branches=false, pre_merge_check=false, merge_strategy=squash, isolation=worktree, unique_milestone_ids=false",
602
592
  "info",
603
593
  );
604
594
  } else if (modeStr.startsWith("team")) {
605
595
  prefs.mode = "team";
606
596
  ctx.ui.notify(
607
- "Mode: team — defaults: auto_push=false, push_branches=true, pre_merge_check=true, merge_strategy=squash, isolation=worktree, commit_docs=true, unique_milestone_ids=true",
597
+ "Mode: team — defaults: auto_push=false, push_branches=true, pre_merge_check=true, merge_strategy=squash, isolation=worktree, unique_milestone_ids=true",
608
598
  "info",
609
599
  );
610
600
  } else {
@@ -82,7 +82,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
82
82
  | `git.pre_merge_check` | `false` | `true` |
83
83
  | `git.merge_strategy` | `"squash"` | `"squash"` |
84
84
  | `git.isolation` | `"worktree"` | `"worktree"` |
85
- | `git.commit_docs` | `true` | `true` |
86
85
  | `unique_milestone_ids` | `false` | `true` |
87
86
 
88
87
  Quick setup: `/gsd mode` (global) or `/gsd mode project` (project-level).
@@ -127,7 +126,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
127
126
  - `main_branch`: string — the primary branch name for new git repos (e.g., `"main"`, `"master"`, `"trunk"`). Also used by `getMainBranch()` as the preferred branch when auto-detection is ambiguous. Default: `"main"`.
128
127
  - `merge_strategy`: `"squash"` or `"merge"` — controls how worktree branches are merged back. `"squash"` combines all commits into one; `"merge"` preserves individual commits. Default: `"squash"`.
129
128
  - `isolation`: `"worktree"`, `"branch"`, or `"none"` — controls auto-mode git isolation strategy. `"worktree"` creates a milestone worktree for isolated work; `"branch"` works directly in the project root but creates a milestone branch (useful for submodule-heavy repos); `"none"` works directly on the current branch with no worktree or milestone branch (ideal for step-mode with hot reloads). Default: `"worktree"`.
130
- - `commit_docs`: boolean — when `false`, prevents GSD from committing `.gsd/` planning artifacts to git. The `.gsd/` folder is added to `.gitignore` and kept local-only. Useful for teams where only some members use GSD, or when company policy requires a clean repository. Default: `true`.
131
129
  - `manage_gitignore`: boolean — when `false`, GSD will not touch `.gitignore` at all. Useful when your project has a strictly managed `.gitignore` and you don't want GSD adding entries. Default: `true`.
132
130
  - `worktree_post_create`: string — script to run after a worktree is created (both auto-mode and manual `/worktree`). Receives `SOURCE_DIR` and `WORKTREE_DIR` as environment variables. Can be absolute or relative to project root. Runs with 30-second timeout. Failure is non-fatal (logged as warning). Default: none.
133
131
 
@@ -244,7 +242,7 @@ mode: solo
244
242
  ---
245
243
  ```
246
244
 
247
- Equivalent to setting `git.auto_push: true`, `git.push_branches: false`, `git.pre_merge_check: false`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: false`.
245
+ Equivalent to setting `git.auto_push: true`, `git.push_branches: false`, `git.pre_merge_check: false`, `git.merge_strategy: squash`, `git.isolation: worktree`, `unique_milestone_ids: false`.
248
246
 
249
247
  **Team — unique IDs, push branches, pre-merge checks:**
250
248
 
@@ -255,7 +253,7 @@ mode: team
255
253
  ---
256
254
  ```
257
255
 
258
- Equivalent to setting `git.auto_push: false`, `git.push_branches: true`, `git.pre_merge_check: true`, `git.merge_strategy: squash`, `git.isolation: worktree`, `git.commit_docs: true`, `unique_milestone_ids: true`.
256
+ Equivalent to setting `git.auto_push: false`, `git.push_branches: true`, `git.pre_merge_check: true`, `git.merge_strategy: squash`, `git.isolation: worktree`, `unique_milestone_ids: true`.
259
257
 
260
258
  **Mode with overrides — team mode but with auto-push:**
261
259
 
@@ -50,11 +50,6 @@ export interface GitPreferences {
50
50
  * - "none": no git isolation — commits land on the user's current branch directly
51
51
  */
52
52
  isolation?: "worktree" | "branch" | "none";
53
- /** When false, prevents GSD from committing .gsd/ planning artifacts to git.
54
- * The .gsd/ folder is added to .gitignore and kept local-only.
55
- * Default: true (planning docs are tracked in git).
56
- */
57
- commit_docs?: boolean;
58
53
  /** When false, GSD will not modify .gitignore at all — no baseline patterns
59
54
  * are added and no self-healing occurs. Use this if you manage your own
60
55
  * .gitignore and don't want GSD touching it.
@@ -226,7 +221,7 @@ export function readIntegrationBranch(basePath: string, milestoneId: string): st
226
221
  *
227
222
  * The file is committed immediately so the metadata is persisted in git.
228
223
  */
229
- export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string, options?: { commitDocs?: boolean }): void {
224
+ export function writeIntegrationBranch(basePath: string, milestoneId: string, branch: string): void {
230
225
  // Don't record slice branches as the integration target
231
226
  if (SLICE_BRANCH_RE.test(branch)) return;
232
227
  // Validate
@@ -250,18 +245,7 @@ export function writeIntegrationBranch(basePath: string, milestoneId: string, br
250
245
 
251
246
  existing.integrationBranch = branch;
252
247
  writeFileSync(metaFile, JSON.stringify(existing, null, 2) + "\n", "utf-8");
253
-
254
- // Commit immediately so the metadata is persisted in git.
255
- // Skip when commit_docs is explicitly false — .gsd/ is local-only.
256
- if (options?.commitDocs !== false) {
257
- try {
258
- nativeAddPaths(basePath, [metaFile]);
259
- nativeCommit(basePath, `chore(${milestoneId}): record integration branch`, { allowEmpty: false });
260
- } catch {
261
- // Non-fatal — file is on disk even if commit fails (e.g. nothing to commit
262
- // because the file was already tracked with identical content)
263
- }
264
- }
248
+ // .gsd/ is managed externally (symlinked) — metadata is not committed to git.
265
249
  }
266
250
 
267
251
  // ─── Git Helper ────────────────────────────────────────────────────────────
@@ -350,10 +334,8 @@ export class GitServiceImpl {
350
334
  * @param extraExclusions Additional pathspec exclusions beyond RUNTIME_EXCLUSION_PATHS.
351
335
  */
352
336
  private smartStage(extraExclusions: readonly string[] = []): void {
353
- // When commit_docs is false, exclude the entire .gsd/ directory from staging
354
- const commitDocsDisabled = this.prefs.commit_docs === false;
355
- const gsdExclusion = commitDocsDisabled ? [".gsd/"] : [];
356
- const allExclusions = [...RUNTIME_EXCLUSION_PATHS, ...gsdExclusion, ...extraExclusions];
337
+ // Always exclude .gsd/ — state is managed externally (symlinked to ~/.gsd/projects/<hash>/)
338
+ const allExclusions = [".gsd/", ...extraExclusions];
357
339
 
358
340
  // One-time cleanup: if runtime files are already tracked in the index
359
341
  // (from older versions where the fallback bug staged them), untrack them
@@ -79,15 +79,14 @@ const BASELINE_PATTERNS = [
79
79
  ];
80
80
 
81
81
  /**
82
- * Ensure basePath/.gitignore contains all baseline patterns.
83
- * Creates the file if missing; appends only missing lines if it exists.
82
+ * Ensure basePath/.gitignore contains a blanket `.gsd/` ignore.
83
+ * Creates the file if missing; appends `.gsd/` if not present.
84
84
  * Returns true if the file was created or modified, false if already complete.
85
85
  *
86
- * When `commitDocs` is false, the entire `.gsd/` directory is added to
87
- * .gitignore instead of individual runtime patterns, keeping all GSD
88
- * artifacts local-only.
86
+ * `.gsd/` state is managed externally (symlinked to `~/.gsd/projects/<hash>/`),
87
+ * so the entire directory is always gitignored.
89
88
  */
90
- export function ensureGitignore(basePath: string, options?: { commitDocs?: boolean; manageGitignore?: boolean }): boolean {
89
+ export function ensureGitignore(basePath: string, options?: { manageGitignore?: boolean }): boolean {
91
90
  // If manage_gitignore is explicitly false, do not touch .gitignore at all
92
91
  if (options?.manageGitignore === false) return false;
93
92
 
@@ -192,7 +191,7 @@ See \`~/.gsd/agent/extensions/gsd/docs/preferences-reference.md\` for full field
192
191
  - \`models\`: Model preferences for specific task types
193
192
  - \`skill_discovery\`: Automatic skill detection preferences
194
193
  - \`auto_supervisor\`: Supervision and gating rules for autonomous modes
195
- - \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, \`commit_docs\` (set to \`false\` to keep .gsd/ local-only), etc.
194
+ - \`git\`: Git preferences — \`main_branch\` (default branch name for new repos, e.g., "main", "master", "trunk"), \`auto_push\`, \`snapshots\`, etc.
196
195
 
197
196
  ## Examples
198
197
 
@@ -25,13 +25,9 @@ import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
25
25
 
26
26
  // ─── Commit Instruction Helper (local copy — avoids circular dep) ───────────
27
27
 
28
- /** Build conditional commit instruction for queue prompts based on commit_docs preference. */
29
- function buildDocsCommitInstruction(message: string): string {
30
- const prefs = loadEffectiveGSDPreferences();
31
- const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
32
- return commitDocsEnabled
33
- ? `Commit: \`${message}\`. Stage only the .gsd/milestones/, .gsd/PROJECT.md, .gsd/REQUIREMENTS.md, .gsd/DECISIONS.md, and .gitignore files you changed — do not stage .gsd/STATE.md or other runtime files.`
34
- : "Do not commit — planning docs are not tracked in git for this project.";
28
+ /** Build commit instruction for queue prompts. .gsd/ is managed externally and always gitignored. */
29
+ function buildDocsCommitInstruction(_message: string): string {
30
+ return "Do not commit planning artifacts — .gsd/ is managed externally.";
35
31
  }
36
32
 
37
33
  // ─── Queue Entry Point ──────────────────────────────────────────────────────
@@ -47,13 +47,9 @@ export {
47
47
 
48
48
  // ─── Commit Instruction Helpers ──────────────────────────────────────────────
49
49
 
50
- /** Build conditional commit instruction for planning prompts based on commit_docs preference. */
51
- function buildDocsCommitInstruction(message: string): string {
52
- const prefs = loadEffectiveGSDPreferences();
53
- const commitDocsEnabled = prefs?.preferences?.git?.commit_docs !== false;
54
- return commitDocsEnabled
55
- ? `Commit: \`${message}\`. Stage only the .gsd/milestones/, .gsd/PROJECT.md, .gsd/REQUIREMENTS.md, .gsd/DECISIONS.md, and .gitignore files you changed — do not stage .gsd/STATE.md or other runtime files.`
56
- : "Do not commit — planning docs are not tracked in git for this project.";
50
+ /** Build commit instruction for planning prompts. .gsd/ is managed externally and always gitignored. */
51
+ function buildDocsCommitInstruction(_message: string): string {
52
+ return "Do not commit planning artifacts — .gsd/ is managed externally.";
57
53
  }
58
54
 
59
55
  // ─── Auto-start after discuss ─────────────────────────────────────────────────
@@ -269,8 +265,7 @@ function bootstrapGsdProject(basePath: string): void {
269
265
  mkdirSync(join(root, "milestones"), { recursive: true });
270
266
  mkdirSync(join(root, "runtime"), { recursive: true });
271
267
 
272
- const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
273
- ensureGitignore(basePath, { commitDocs });
268
+ ensureGitignore(basePath);
274
269
  ensurePreferences(basePath);
275
270
  untrackRuntimeFiles(basePath);
276
271
  }
@@ -507,6 +502,9 @@ export async function showDiscuss(
507
502
 
508
503
  // Loop: show picker, dispatch discuss, repeat until "not_yet"
509
504
  while (true) {
505
+ // Invalidate caches so we pick up CONTEXT files written by the just-completed discussion
506
+ invalidateAllCaches();
507
+
510
508
  // Build discussion-state map: which slices have CONTEXT files already?
511
509
  const discussedMap = new Map<string, boolean>();
512
510
  for (const s of pendingSlices) {
@@ -783,8 +781,7 @@ export async function showSmartEntry(
783
781
  }
784
782
 
785
783
  // ── Ensure .gitignore has baseline patterns ──────────────────────────
786
- const commitDocs = loadEffectiveGSDPreferences()?.preferences?.git?.commit_docs;
787
- ensureGitignore(basePath, { commitDocs });
784
+ ensureGitignore(basePath);
788
785
  untrackRuntimeFiles(basePath);
789
786
 
790
787
  // ── Self-heal stale runtime records from crashed auto-mode sessions ──
@@ -1048,6 +1048,19 @@ export default function (pi: ExtensionAPI) {
1048
1048
  } catch { /* best-effort */ }
1049
1049
  }
1050
1050
 
1051
+ // Auto-commit dirty work in CLI-spawned worktrees so nothing is lost.
1052
+ // The CLI sets GSD_CLI_WORKTREE when launched with -w.
1053
+ const cliWorktree = process.env.GSD_CLI_WORKTREE;
1054
+ if (cliWorktree) {
1055
+ try {
1056
+ const { autoCommitCurrentBranch } = await import("./worktree.js");
1057
+ const msg = autoCommitCurrentBranch(process.cwd(), "session-end", cliWorktree);
1058
+ if (msg) {
1059
+ ctx.ui.notify(`Auto-committed worktree ${cliWorktree} before exit.`, "info");
1060
+ }
1061
+ } catch { /* best-effort */ }
1062
+ }
1063
+
1051
1064
  if (!isAutoActive() && !isAutoPaused()) return;
1052
1065
 
1053
1066
  // Save the current session — the lock file stays on disk
@@ -10,7 +10,7 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent
10
10
  import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { showNextAction } from "../shared/mod.js";
13
- import { nativeIsRepo, nativeInit, nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
13
+ import { nativeIsRepo, nativeInit } from "./native-git-bridge.js";
14
14
  import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
15
15
  import { gsdRoot } from "./paths.js";
16
16
  import { assertSafeDirectory } from "./validate-directory.js";
@@ -27,7 +27,6 @@ interface InitWizardResult {
27
27
 
28
28
  interface ProjectPreferences {
29
29
  mode: "solo" | "team";
30
- commitDocs: boolean;
31
30
  gitIsolation: "worktree" | "branch" | "none";
32
31
  mainBranch: string;
33
32
  verificationCommands: string[];
@@ -41,7 +40,6 @@ interface ProjectPreferences {
41
40
 
42
41
  const DEFAULT_PREFS: ProjectPreferences = {
43
42
  mode: "solo",
44
- commitDocs: true,
45
43
  gitIsolation: "worktree",
46
44
  mainBranch: "main",
47
45
  verificationCommands: [],
@@ -149,7 +147,6 @@ export async function showProjectInit(
149
147
 
150
148
  // ── Step 5: Git preferences ────────────────────────────────────────────────
151
149
  const gitSummary: string[] = [];
152
- gitSummary.push(`Commit .gsd/ plans to git: yes`);
153
150
  gitSummary.push(`Git isolation: worktree`);
154
151
  gitSummary.push(`Main branch: ${prefs.mainBranch}`);
155
152
 
@@ -230,19 +227,9 @@ export async function showProjectInit(
230
227
  bootstrapGsdDirectory(basePath, prefs, signals);
231
228
 
232
229
  // Ensure .gitignore
233
- ensureGitignore(basePath, { commitDocs: prefs.commitDocs });
230
+ ensureGitignore(basePath);
234
231
  untrackRuntimeFiles(basePath);
235
232
 
236
- // Commit if enabled
237
- if (prefs.commitDocs && nativeIsRepo(basePath)) {
238
- try {
239
- nativeAddPaths(basePath, [".gsd", ".gitignore"]);
240
- nativeCommit(basePath, "chore: init gsd");
241
- } catch {
242
- // nothing to commit — that's fine
243
- }
244
- }
245
-
246
233
  ctx.ui.notify("GSD initialized. Starting your first milestone...", "info");
247
234
 
248
235
  return { completed: true, bootstrapped: true };
@@ -338,20 +325,6 @@ async function customizeGitPrefs(
338
325
  prefs: ProjectPreferences,
339
326
  signals: ProjectSignals,
340
327
  ): Promise<void> {
341
- // Commit docs
342
- const commitChoice = await showNextAction(ctx, {
343
- title: "Commit .gsd/ plans to git?",
344
- summary: [
345
- "When enabled, .gsd/ planning docs are tracked in version control.",
346
- "Team projects usually want this. Throwaway prototypes may not.",
347
- ],
348
- actions: [
349
- { id: "yes", label: "Yes", description: "Track .gsd/ in git", recommended: true },
350
- { id: "no", label: "No", description: "Keep .gsd/ local-only" },
351
- ],
352
- });
353
- prefs.commitDocs = commitChoice !== "no";
354
-
355
328
  // Isolation strategy
356
329
  const hasSubmodules = existsSync(join(process.cwd(), ".gitmodules"));
357
330
  const isolationActions = [
@@ -459,7 +432,6 @@ function buildPreferencesFile(prefs: ProjectPreferences): string {
459
432
 
460
433
  // Git preferences
461
434
  lines.push("git:");
462
- lines.push(` commit_docs: ${prefs.commitDocs}`);
463
435
  lines.push(` isolation: ${prefs.gitIsolation}`);
464
436
  lines.push(` main_branch: ${prefs.mainBranch}`);
465
437
  lines.push(` auto_push: ${prefs.autoPush}`);
@@ -34,7 +34,6 @@ export const MODE_DEFAULTS: Record<WorkflowMode, Partial<GSDPreferences>> = {
34
34
  pre_merge_check: false,
35
35
  merge_strategy: "squash",
36
36
  isolation: "worktree",
37
- commit_docs: true,
38
37
  },
39
38
  unique_milestone_ids: false,
40
39
  },
@@ -45,7 +44,6 @@ export const MODE_DEFAULTS: Record<WorkflowMode, Partial<GSDPreferences>> = {
45
44
  pre_merge_check: true,
46
45
  merge_strategy: "squash",
47
46
  isolation: "worktree",
48
- commit_docs: true,
49
47
  },
50
48
  unique_milestone_ids: true,
51
49
  },
@@ -559,8 +559,7 @@ export function validatePreferences(preferences: GSDPreferences): {
559
559
  }
560
560
  }
561
561
  if (g.commit_docs !== undefined) {
562
- if (typeof g.commit_docs === "boolean") git.commit_docs = g.commit_docs;
563
- else errors.push("git.commit_docs must be a boolean");
562
+ warnings.push("git.commit_docs is deprecated .gsd/ is managed externally and always gitignored. Remove this setting.");
564
563
  }
565
564
  if (g.manage_gitignore !== undefined) {
566
565
  if (typeof g.manage_gitignore === "boolean") git.manage_gitignore = g.manage_gitignore;
@@ -96,24 +96,39 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
96
96
 
97
97
  /**
98
98
  * Fallback parser for prose-style roadmaps where the LLM wrote
99
- * `## Slice S01: Title` headers instead of the machine-readable
100
- * `## Slices` checklist. Extracts slice IDs and titles so auto-mode
101
- * can at least identify slices and plan them.
99
+ * slice headers instead of the machine-readable `## Slices` checklist.
100
+ * Extracts slice IDs and titles so auto-mode can at least identify
101
+ * slices and plan them.
102
102
  *
103
- * Also handles `## S01: Title` and `## S01 — Title` variants.
103
+ * Handles these LLM-generated variants:
104
+ * ## S01: Title (H2, colon separator)
105
+ * ### S01: Title (H3)
106
+ * #### S01: Title (H4)
107
+ * ## Slice S01: Title (with "Slice" prefix)
108
+ * ## S01 — Title (em dash)
109
+ * ## S01 – Title (en dash)
110
+ * ## S01 - Title (hyphen)
111
+ * ## S01. Title (dot separator)
112
+ * ## S01 Title (space only, no separator)
113
+ * ## **S01: Title** (bold-wrapped)
114
+ * ## **S01**: Title (bold ID only)
115
+ * ## S1: Title (non-zero-padded ID)
104
116
  */
105
117
  function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
106
118
  const slices: RoadmapSliceEntry[] = [];
107
- const headerPattern = /^##\s+(?:Slice\s+)?(S\d+)[:\s—–-]+\s*(.+)/gm;
119
+ // Match H1–H4 headers containing S<digits> with optional "Slice" prefix and bold markers.
120
+ // Separator after the ID is flexible: colon, dash, em/en dash, dot, or just whitespace.
121
+ const headerPattern = /^#{1,4}\s+\*{0,2}(?:Slice\s+)?(S\d+)\*{0,2}[:\s.—–-]*\s*(.+)/gm;
108
122
  let match: RegExpExecArray | null;
109
123
 
110
124
  while ((match = headerPattern.exec(content)) !== null) {
111
125
  const id = match[1]!;
112
- const title = match[2]!.trim();
126
+ let title = match[2]!.trim().replace(/\*{1,2}$/g, "").trim(); // strip trailing bold markers
127
+ if (!title) continue; // skip if we only matched the ID with no title
113
128
 
114
129
  // Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02"
115
130
  const afterHeader = content.slice(match.index + match[0].length);
116
- const nextHeader = afterHeader.search(/^##\s/m);
131
+ const nextHeader = afterHeader.search(/^#{1,4}\s/m);
117
132
  const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500);
118
133
 
119
134
  const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i);
@@ -17,7 +17,7 @@
17
17
  */
18
18
 
19
19
  import { createRequire } from "node:module";
20
- import { existsSync, readFileSync, mkdirSync, unlinkSync } from "node:fs";
20
+ import { existsSync, readFileSync, mkdirSync, unlinkSync, rmSync, statSync } from "node:fs";
21
21
  import { join, dirname } from "node:path";
22
22
  import { gsdRoot } from "./paths.js";
23
23
  import { atomicWriteSync } from "./atomic-write.js";
@@ -51,6 +51,9 @@ let _lockedPath: string | null = null;
51
51
  /** Our PID at lock acquisition time. */
52
52
  let _lockPid: number = 0;
53
53
 
54
+ /** Set to true when proper-lockfile fires onCompromised (mtime drift, sleep, etc.). */
55
+ let _lockCompromised: boolean = false;
56
+
54
57
  const LOCK_FILE = "auto.lock";
55
58
 
56
59
  function lockPath(basePath: string): string {
@@ -92,33 +95,80 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
92
95
  return acquireFallbackLock(basePath, lp, lockData);
93
96
  }
94
97
 
98
+ const gsdDir = gsdRoot(basePath);
99
+
95
100
  try {
96
101
  // Try to acquire an exclusive OS-level lock on the lock file.
97
102
  // We lock the directory (gsdRoot) since proper-lockfile works best
98
103
  // on directories, and the lock file itself may not exist yet.
99
- const gsdDir = gsdRoot(basePath);
100
104
  mkdirSync(gsdDir, { recursive: true });
101
105
 
102
106
  const release = lockfile.lockSync(gsdDir, {
103
107
  realpath: false,
104
- stale: 300_000, // 5 minutes — consider lock stale if holder hasn't updated
108
+ stale: 1_800_000, // 30 minutes — safe for laptop sleep / long event loop stalls
105
109
  update: 10_000, // Update lock mtime every 10s to prove liveness
110
+ onCompromised: () => {
111
+ // proper-lockfile detected mtime drift (system sleep, event loop stall, etc.).
112
+ // Default handler throws inside setTimeout — an uncaught exception that crashes
113
+ // or corrupts process state. Instead, set a flag so validateSessionLock() can
114
+ // detect the compromise gracefully on the next dispatch cycle.
115
+ _lockCompromised = true;
116
+ _releaseFunction = null;
117
+ },
106
118
  });
107
119
 
108
120
  _releaseFunction = release;
109
121
  _lockedPath = basePath;
110
122
  _lockPid = process.pid;
123
+ _lockCompromised = false;
124
+
125
+ // Safety net: clean up lock dir on process exit if _releaseFunction
126
+ // wasn't called (e.g., normal exit after clean completion) (#1245).
127
+ const lockDirForCleanup = join(gsdDir + ".lock");
128
+ process.once("exit", () => {
129
+ try {
130
+ if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
131
+ } catch { /* best-effort */ }
132
+ try {
133
+ if (existsSync(lockDirForCleanup)) rmSync(lockDirForCleanup, { recursive: true, force: true });
134
+ } catch { /* best-effort */ }
135
+ });
111
136
 
112
137
  // Write the informational lock data
113
138
  atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
114
139
 
115
140
  return { acquired: true };
116
141
  } catch (err) {
117
- // Lock is held by another process
142
+ // Lock is held by another process — or the .gsd.lock/ directory is stranded.
143
+ // Check: if auto.lock is gone and no process is alive, the lock dir is stale.
118
144
  const existingData = readExistingLockData(lp);
119
145
  const existingPid = existingData?.pid;
146
+
147
+ // If no lock file or no alive process, try to clean up and re-acquire (#1245)
148
+ if (!existingData || (existingPid && !isPidAlive(existingPid))) {
149
+ try {
150
+ const lockDir = join(gsdDir + ".lock");
151
+ if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
152
+ if (existsSync(lp)) unlinkSync(lp);
153
+
154
+ // Retry acquisition after cleanup
155
+ const release = lockfile.lockSync(gsdDir, {
156
+ realpath: false,
157
+ stale: 300_000,
158
+ update: 10_000,
159
+ });
160
+ _releaseFunction = release;
161
+ _lockedPath = basePath;
162
+ _lockPid = process.pid;
163
+ atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
164
+ return { acquired: true };
165
+ } catch {
166
+ // Retry also failed — fall through to the error path
167
+ }
168
+ }
169
+
120
170
  const reason = existingPid
121
- ? `Another auto-mode session (PID ${existingPid}) is already running on this project.`
171
+ ? `Another auto-mode session (PID ${existingPid}) appears to be running.\nStop it with \`kill ${existingPid}\` before starting a new session.`
122
172
  : `Another auto-mode session is already running on this project.`;
123
173
 
124
174
  return { acquired: false, reason, existingPid };
@@ -195,6 +245,11 @@ export function updateSessionLock(
195
245
  * This is called periodically during the dispatch loop.
196
246
  */
197
247
  export function validateSessionLock(basePath: string): boolean {
248
+ // Lock was compromised by proper-lockfile (mtime drift from sleep, stall, etc.)
249
+ if (_lockCompromised) {
250
+ return false;
251
+ }
252
+
198
253
  // If we have an OS-level lock, we're still the owner
199
254
  if (_releaseFunction && _lockedPath === basePath) {
200
255
  return true;
@@ -233,8 +288,20 @@ export function releaseSessionLock(basePath: string): void {
233
288
  // Non-fatal
234
289
  }
235
290
 
291
+ // Remove the proper-lockfile directory (.gsd.lock/) if it exists.
292
+ // proper-lockfile creates this directory as the OS-level lock mechanism.
293
+ // If the process exits without calling _releaseFunction (SIGKILL, crash),
294
+ // this directory is stranded and blocks the next session (#1245).
295
+ try {
296
+ const lockDir = join(gsdRoot(basePath) + ".lock");
297
+ if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
298
+ } catch {
299
+ // Non-fatal
300
+ }
301
+
236
302
  _lockedPath = null;
237
303
  _lockPid = 0;
304
+ _lockCompromised = false;
238
305
  }
239
306
 
240
307
  /**