gsd-pi 2.30.0-dev.92a3417 → 2.30.0-dev.ab42fba

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 (71) 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-worktree.ts +12 -8
  8. package/dist/resources/extensions/gsd/auto.ts +2 -2
  9. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  10. package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  11. package/dist/resources/extensions/gsd/git-service.ts +4 -22
  12. package/dist/resources/extensions/gsd/gitignore.ts +6 -7
  13. package/dist/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  14. package/dist/resources/extensions/gsd/guided-flow.ts +8 -11
  15. package/dist/resources/extensions/gsd/index.ts +13 -0
  16. package/dist/resources/extensions/gsd/init-wizard.ts +2 -30
  17. package/dist/resources/extensions/gsd/preferences-types.ts +0 -2
  18. package/dist/resources/extensions/gsd/preferences-validation.ts +1 -2
  19. package/dist/resources/extensions/gsd/roadmap-slices.ts +22 -7
  20. package/dist/resources/extensions/gsd/session-lock.ts +53 -4
  21. package/dist/resources/extensions/gsd/templates/preferences.md +0 -1
  22. package/dist/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  23. package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  24. package/dist/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  25. package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  26. package/dist/resources/extensions/gsd/worktree.ts +2 -2
  27. package/dist/worktree-cli.d.ts +34 -0
  28. package/dist/worktree-cli.js +294 -0
  29. package/dist/worktree-name-gen.d.ts +7 -0
  30. package/dist/worktree-name-gen.js +44 -0
  31. package/package.json +1 -1
  32. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  33. package/packages/pi-coding-agent/dist/core/agent-session.js +14 -0
  34. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  35. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  36. package/packages/pi-coding-agent/dist/core/extensions/loader.js +4 -0
  37. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  38. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  39. package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
  40. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  41. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  42. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  43. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  44. package/packages/pi-coding-agent/src/core/agent-session.ts +14 -0
  45. package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
  46. package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
  47. package/packages/pi-coding-agent/src/core/extensions/types.ts +8 -0
  48. package/src/resources/extensions/aws-auth/index.ts +144 -0
  49. package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
  50. package/src/resources/extensions/gsd/auto-prompts.ts +2 -10
  51. package/src/resources/extensions/gsd/auto-start.ts +3 -10
  52. package/src/resources/extensions/gsd/auto-worktree.ts +12 -8
  53. package/src/resources/extensions/gsd/auto.ts +2 -2
  54. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
  55. package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -4
  56. package/src/resources/extensions/gsd/git-service.ts +4 -22
  57. package/src/resources/extensions/gsd/gitignore.ts +6 -7
  58. package/src/resources/extensions/gsd/guided-flow-queue.ts +3 -7
  59. package/src/resources/extensions/gsd/guided-flow.ts +8 -11
  60. package/src/resources/extensions/gsd/index.ts +13 -0
  61. package/src/resources/extensions/gsd/init-wizard.ts +2 -30
  62. package/src/resources/extensions/gsd/preferences-types.ts +0 -2
  63. package/src/resources/extensions/gsd/preferences-validation.ts +1 -2
  64. package/src/resources/extensions/gsd/roadmap-slices.ts +22 -7
  65. package/src/resources/extensions/gsd/session-lock.ts +53 -4
  66. package/src/resources/extensions/gsd/templates/preferences.md +0 -1
  67. package/src/resources/extensions/gsd/tests/git-service.test.ts +14 -42
  68. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
  69. package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -9
  70. package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -4
  71. package/src/resources/extensions/gsd/worktree.ts +2 -2
@@ -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";
@@ -92,11 +92,12 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
92
92
  return acquireFallbackLock(basePath, lp, lockData);
93
93
  }
94
94
 
95
+ const gsdDir = gsdRoot(basePath);
96
+
95
97
  try {
96
98
  // Try to acquire an exclusive OS-level lock on the lock file.
97
99
  // We lock the directory (gsdRoot) since proper-lockfile works best
98
100
  // on directories, and the lock file itself may not exist yet.
99
- const gsdDir = gsdRoot(basePath);
100
101
  mkdirSync(gsdDir, { recursive: true });
101
102
 
102
103
  const release = lockfile.lockSync(gsdDir, {
@@ -109,16 +110,53 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
109
110
  _lockedPath = basePath;
110
111
  _lockPid = process.pid;
111
112
 
113
+ // Safety net: clean up lock dir on process exit if _releaseFunction
114
+ // wasn't called (e.g., normal exit after clean completion) (#1245).
115
+ const lockDirForCleanup = join(gsdDir + ".lock");
116
+ process.once("exit", () => {
117
+ try {
118
+ if (_releaseFunction) { _releaseFunction(); _releaseFunction = null; }
119
+ } catch { /* best-effort */ }
120
+ try {
121
+ if (existsSync(lockDirForCleanup)) rmSync(lockDirForCleanup, { recursive: true, force: true });
122
+ } catch { /* best-effort */ }
123
+ });
124
+
112
125
  // Write the informational lock data
113
126
  atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
114
127
 
115
128
  return { acquired: true };
116
129
  } catch (err) {
117
- // Lock is held by another process
130
+ // Lock is held by another process — or the .gsd.lock/ directory is stranded.
131
+ // Check: if auto.lock is gone and no process is alive, the lock dir is stale.
118
132
  const existingData = readExistingLockData(lp);
119
133
  const existingPid = existingData?.pid;
134
+
135
+ // If no lock file or no alive process, try to clean up and re-acquire (#1245)
136
+ if (!existingData || (existingPid && !isPidAlive(existingPid))) {
137
+ try {
138
+ const lockDir = join(gsdDir + ".lock");
139
+ if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
140
+ if (existsSync(lp)) unlinkSync(lp);
141
+
142
+ // Retry acquisition after cleanup
143
+ const release = lockfile.lockSync(gsdDir, {
144
+ realpath: false,
145
+ stale: 300_000,
146
+ update: 10_000,
147
+ });
148
+ _releaseFunction = release;
149
+ _lockedPath = basePath;
150
+ _lockPid = process.pid;
151
+ atomicWriteSync(lp, JSON.stringify(lockData, null, 2));
152
+ return { acquired: true };
153
+ } catch {
154
+ // Retry also failed — fall through to the error path
155
+ }
156
+ }
157
+
120
158
  const reason = existingPid
121
- ? `Another auto-mode session (PID ${existingPid}) is already running on this project.`
159
+ ? `Another auto-mode session (PID ${existingPid}) appears to be running.\nStop it with \`kill ${existingPid}\` before starting a new session.`
122
160
  : `Another auto-mode session is already running on this project.`;
123
161
 
124
162
  return { acquired: false, reason, existingPid };
@@ -233,6 +271,17 @@ export function releaseSessionLock(basePath: string): void {
233
271
  // Non-fatal
234
272
  }
235
273
 
274
+ // Remove the proper-lockfile directory (.gsd.lock/) if it exists.
275
+ // proper-lockfile creates this directory as the OS-level lock mechanism.
276
+ // If the process exits without calling _releaseFunction (SIGKILL, crash),
277
+ // this directory is stranded and blocks the next session (#1245).
278
+ try {
279
+ const lockDir = join(gsdRoot(basePath) + ".lock");
280
+ if (existsSync(lockDir)) rmSync(lockDir, { recursive: true, force: true });
281
+ } catch {
282
+ // Non-fatal
283
+ }
284
+
236
285
  _lockedPath = null;
237
286
  _lockPid = 0;
238
287
  }
@@ -20,7 +20,6 @@ git:
20
20
  main_branch:
21
21
  merge_strategy:
22
22
  isolation:
23
- commit_docs:
24
23
  manage_gitignore:
25
24
  worktree_post_create:
26
25
  unique_milestone_ids:
@@ -1086,12 +1086,12 @@ async function main(): Promise<void> {
1086
1086
  rmSync(repo, { recursive: true, force: true });
1087
1087
  }
1088
1088
 
1089
- // ─── commit_docs: false — smartStage excludes .gsd/ ──────────────────
1089
+ // ─── smartStage always excludes .gsd/ ──────────────────────────────
1090
1090
 
1091
- console.log("\n=== commit_docs: false — smartStage excludes .gsd/ ===");
1091
+ console.log("\n=== smartStage always excludes .gsd/ ===");
1092
1092
 
1093
1093
  {
1094
- const repo = mkdtempSync(join(tmpdir(), "gsd-commit-docs-"));
1094
+ const repo = mkdtempSync(join(tmpdir(), "gsd-smart-stage-excludes-"));
1095
1095
  run("git init -b main", repo);
1096
1096
  run("git config user.email test@test.com", repo);
1097
1097
  run("git config user.name Test", repo);
@@ -1104,65 +1104,37 @@ async function main(): Promise<void> {
1104
1104
  writeFileSync(join(repo, ".gsd", "preferences.md"), "---\nversion: 1\n---");
1105
1105
  writeFileSync(join(repo, "src.ts"), "const x = 1;");
1106
1106
 
1107
- // With commit_docs: false, smartStage should exclude .gsd/
1108
- const svc = new GitServiceImpl(repo, { commit_docs: false });
1109
- const msg = svc.commit({ message: "test commit" });
1110
- assertTrue(msg !== null, "commit_docs=false: commit succeeds with non-.gsd files");
1111
-
1112
- // .gsd/ files should NOT be in the commit
1113
- const committed = run("git show --name-only HEAD", repo);
1114
- assertTrue(!committed.includes(".gsd/"), "commit_docs=false: .gsd/ files not in commit");
1115
- assertTrue(committed.includes("src.ts"), "commit_docs=false: source files ARE in commit");
1116
-
1117
- rmSync(repo, { recursive: true, force: true });
1118
- }
1119
-
1120
- // ─── commit_docs: true (default) — smartStage includes .gsd/ ────────
1121
-
1122
- console.log("\n=== commit_docs: true — smartStage includes .gsd/ ===");
1123
-
1124
- {
1125
- const repo = mkdtempSync(join(tmpdir(), "gsd-commit-docs-default-"));
1126
- run("git init -b main", repo);
1127
- run("git config user.email test@test.com", repo);
1128
- run("git config user.name Test", repo);
1129
- writeFileSync(join(repo, "README.md"), "init");
1130
- run("git add -A && git commit -m init", repo);
1131
-
1132
- mkdirSync(join(repo, ".gsd", "milestones", "M001"), { recursive: true });
1133
- writeFileSync(join(repo, ".gsd", "milestones", "M001", "ROADMAP.md"), "# Roadmap");
1134
- writeFileSync(join(repo, "src.ts"), "const x = 1;");
1135
-
1136
- // Default behavior (commit_docs not set) — .gsd/ files ARE committed
1107
+ // smartStage always excludes .gsd/ — state is managed externally
1137
1108
  const svc = new GitServiceImpl(repo);
1138
1109
  const msg = svc.commit({ message: "test commit" });
1139
- assertTrue(msg !== null, "commit_docs=default: commit succeeds");
1110
+ assertTrue(msg !== null, "smartStage: commit succeeds with non-.gsd files");
1140
1111
 
1112
+ // .gsd/ files should NOT be in the commit
1141
1113
  const committed = run("git show --name-only HEAD", repo);
1142
- assertTrue(committed.includes(".gsd/"), "commit_docs=default: .gsd/ files ARE in commit");
1143
- assertTrue(committed.includes("src.ts"), "commit_docs=default: source files in commit");
1114
+ assertTrue(!committed.includes(".gsd/"), "smartStage: .gsd/ files not in commit");
1115
+ assertTrue(committed.includes("src.ts"), "smartStage: source files ARE in commit");
1144
1116
 
1145
1117
  rmSync(repo, { recursive: true, force: true });
1146
1118
  }
1147
1119
 
1148
- // ─── writeIntegrationBranch: commitDocs false skips commit ──────────
1120
+ // ─── writeIntegrationBranch: no commit (metadata in external storage) ──
1149
1121
 
1150
- console.log("\n=== writeIntegrationBranch: commitDocs false skips commit ===");
1122
+ console.log("\n=== writeIntegrationBranch: no commit ===");
1151
1123
 
1152
1124
  {
1153
1125
  const repo = initBranchTestRepo();
1154
1126
  const commitsBefore = run("git rev-list --count HEAD", repo);
1155
1127
 
1156
- writeIntegrationBranch(repo, "M001", "f-123-new-thing", { commitDocs: false });
1128
+ writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1157
1129
 
1158
1130
  // File should still be written to disk
1159
1131
  assertEq(readIntegrationBranch(repo, "M001"), "f-123-new-thing",
1160
- "commitDocs=false: metadata file exists on disk");
1132
+ "writeIntegrationBranch: metadata file exists on disk");
1161
1133
 
1162
- // But no new commit should have been created
1134
+ // No commit .gsd/ is managed externally
1163
1135
  const commitsAfter = run("git rev-list --count HEAD", repo);
1164
1136
  assertEq(commitsBefore, commitsAfter,
1165
- "commitDocs=false: no git commit created for integration branch");
1137
+ "writeIntegrationBranch: no git commit created for integration branch");
1166
1138
 
1167
1139
  rmSync(repo, { recursive: true, force: true });
1168
1140
  }
@@ -28,18 +28,12 @@ const BASE_VARS = {
28
28
  sourceFilePaths: "- **Requirements**: `.gsd/REQUIREMENTS.md`",
29
29
  };
30
30
 
31
- test("plan-slice prompt: commit step present when commit_docs=true", () => {
32
- const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Commit: `docs(S01): add slice plan`" });
33
- assert.ok(result.includes("docs(S01): add slice plan"));
31
+ test("plan-slice prompt: commit instruction says do not commit (external state)", () => {
32
+ const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally." });
33
+ assert.ok(result.includes("Do not commit planning artifacts"));
34
34
  assert.ok(!result.includes("{{commitInstruction}}"));
35
35
  });
36
36
 
37
- test("plan-slice prompt: no commit step when commit_docs=false", () => {
38
- const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit — planning docs are not tracked in git for this project." });
39
- assert.ok(!result.includes("docs(S01): add slice plan"));
40
- assert.ok(result.includes("Do not commit"));
41
- });
42
-
43
37
  test("plan-slice prompt: all variables substituted", () => {
44
38
  const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Commit: `docs(S01): add slice plan`" });
45
39
  assert.ok(!result.includes("{{"));
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Preferences tests — consolidated from:
3
- * - preferences-git.test.ts (git.isolation, git.merge_to_main, git.commit_docs)
3
+ * - preferences-git.test.ts (git.isolation, git.merge_to_main)
4
4
  * - preferences-hooks.test.ts (post-unit + pre-dispatch hook config)
5
5
  * - preferences-mode.test.ts (solo/team mode defaults, overrides)
6
6
  * - preferences-models.test.ts (model config parsing, OpenRouter, CRLF)
@@ -39,14 +39,6 @@ test("git.merge_to_main produces deprecation warning", () => {
39
39
  }
40
40
  });
41
41
 
42
- test("git.commit_docs accepts boolean, rejects string", () => {
43
- const { errors: e1, preferences: p1 } = validatePreferences({ git: { commit_docs: false } });
44
- assert.equal(e1.length, 0);
45
- assert.equal(p1.git?.commit_docs, false);
46
-
47
- const { errors: e2 } = validatePreferences({ git: { commit_docs: "no" as any } });
48
- assert.ok(e2.length > 0);
49
- });
50
42
 
51
43
  test("getIsolationMode defaults to worktree when no prefs file", { skip: "requires no global ~/.gsd/preferences.md" }, () => {
52
44
  assert.equal(getIsolationMode(), "worktree");
@@ -108,10 +108,7 @@ async function main(): Promise<void> {
108
108
  assertEq(readIntegrationBranch(repo, "M001"), "f-123-thing",
109
109
  "captureIntegrationBranch records the current branch");
110
110
 
111
- // Verify it was committed (not just written to disk)
112
- const logOut = run("git log --oneline -1", repo);
113
- assertTrue(logOut.includes("integration branch"), "metadata committed to git");
114
-
111
+ // .gsd/ metadata is written to disk only (not committed) since commit_docs removal
115
112
  rmSync(repo, { recursive: true, force: true });
116
113
  }
117
114
 
@@ -56,13 +56,13 @@ export function setActiveMilestoneId(basePath: string, milestoneId: string | nul
56
56
  * record when the user starts from a different branch (#300). Always a no-op
57
57
  * if on a GSD slice branch.
58
58
  */
59
- export function captureIntegrationBranch(basePath: string, milestoneId: string, options?: { commitDocs?: boolean }): void {
59
+ export function captureIntegrationBranch(basePath: string, milestoneId: string): void {
60
60
  // In a worktree, the base branch is implicit (worktree/<name>).
61
61
  // Writing it to META.json would leave stale metadata after merge back to main.
62
62
  if (detectWorktreeName(basePath)) return;
63
63
  const svc = getService(basePath);
64
64
  const current = svc.getCurrentBranch();
65
- writeIntegrationBranch(basePath, milestoneId, current, options);
65
+ writeIntegrationBranch(basePath, milestoneId, current);
66
66
  }
67
67
 
68
68
  // ─── Pure Utility Functions (unchanged) ────────────────────────────────────