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.
- package/dist/cli.js +51 -0
- package/dist/help-text.js +35 -0
- package/dist/resources/extensions/aws-auth/index.ts +144 -0
- package/dist/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/dist/resources/extensions/gsd/auto-prompts.ts +2 -10
- package/dist/resources/extensions/gsd/auto-start.ts +3 -10
- package/dist/resources/extensions/gsd/auto-worktree.ts +12 -8
- package/dist/resources/extensions/gsd/auto.ts +2 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +2 -4
- package/dist/resources/extensions/gsd/git-service.ts +4 -22
- package/dist/resources/extensions/gsd/gitignore.ts +6 -7
- package/dist/resources/extensions/gsd/guided-flow-queue.ts +3 -7
- package/dist/resources/extensions/gsd/guided-flow.ts +8 -11
- package/dist/resources/extensions/gsd/index.ts +13 -0
- package/dist/resources/extensions/gsd/init-wizard.ts +2 -30
- package/dist/resources/extensions/gsd/preferences-types.ts +0 -2
- package/dist/resources/extensions/gsd/preferences-validation.ts +1 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +22 -7
- package/dist/resources/extensions/gsd/session-lock.ts +53 -4
- package/dist/resources/extensions/gsd/templates/preferences.md +0 -1
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +14 -42
- package/dist/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
- package/dist/resources/extensions/gsd/tests/preferences.test.ts +1 -9
- package/dist/resources/extensions/gsd/tests/worktree.test.ts +1 -4
- package/dist/resources/extensions/gsd/worktree.ts +2 -2
- package/dist/worktree-cli.d.ts +34 -0
- package/dist/worktree-cli.js +294 -0
- package/dist/worktree-name-gen.d.ts +7 -0
- package/dist/worktree-name-gen.js +44 -0
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +14 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +4 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +1 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +14 -0
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +5 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +1 -0
- package/packages/pi-coding-agent/src/core/extensions/types.ts +8 -0
- package/src/resources/extensions/aws-auth/index.ts +144 -0
- package/src/resources/extensions/gsd/auto-dashboard.ts +65 -186
- package/src/resources/extensions/gsd/auto-prompts.ts +2 -10
- package/src/resources/extensions/gsd/auto-start.ts +3 -10
- package/src/resources/extensions/gsd/auto-worktree.ts +12 -8
- package/src/resources/extensions/gsd/auto.ts +2 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +2 -12
- package/src/resources/extensions/gsd/docs/preferences-reference.md +2 -4
- package/src/resources/extensions/gsd/git-service.ts +4 -22
- package/src/resources/extensions/gsd/gitignore.ts +6 -7
- package/src/resources/extensions/gsd/guided-flow-queue.ts +3 -7
- package/src/resources/extensions/gsd/guided-flow.ts +8 -11
- package/src/resources/extensions/gsd/index.ts +13 -0
- package/src/resources/extensions/gsd/init-wizard.ts +2 -30
- package/src/resources/extensions/gsd/preferences-types.ts +0 -2
- package/src/resources/extensions/gsd/preferences-validation.ts +1 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +22 -7
- package/src/resources/extensions/gsd/session-lock.ts +53 -4
- package/src/resources/extensions/gsd/templates/preferences.md +0 -1
- package/src/resources/extensions/gsd/tests/git-service.test.ts +14 -42
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +3 -9
- package/src/resources/extensions/gsd/tests/preferences.test.ts +1 -9
- package/src/resources/extensions/gsd/tests/worktree.test.ts +1 -4
- 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
|
|
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, {
|
|
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
|
|
319
|
+
captureIntegrationBranch(base, s.currentMilestoneId);
|
|
327
320
|
}
|
|
328
321
|
setActiveMilestoneId(base, s.currentMilestoneId);
|
|
329
322
|
}
|
|
@@ -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
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
380
|
-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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`, `
|
|
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`, `
|
|
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
|
|
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
|
-
//
|
|
354
|
-
const
|
|
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
|
|
83
|
-
* Creates the file if missing; appends
|
|
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
|
-
*
|
|
87
|
-
*
|
|
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?: {
|
|
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\`,
|
|
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
|
|
29
|
-
function buildDocsCommitInstruction(
|
|
30
|
-
|
|
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
|
|
51
|
-
function buildDocsCommitInstruction(
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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})
|
|
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
|
}
|