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
|
@@ -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
|
}
|
|
@@ -1086,12 +1086,12 @@ async function main(): Promise<void> {
|
|
|
1086
1086
|
rmSync(repo, { recursive: true, force: true });
|
|
1087
1087
|
}
|
|
1088
1088
|
|
|
1089
|
-
// ───
|
|
1089
|
+
// ─── smartStage always excludes .gsd/ ──────────────────────────────
|
|
1090
1090
|
|
|
1091
|
-
console.log("\n===
|
|
1091
|
+
console.log("\n=== smartStage always excludes .gsd/ ===");
|
|
1092
1092
|
|
|
1093
1093
|
{
|
|
1094
|
-
const repo = mkdtempSync(join(tmpdir(), "gsd-
|
|
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
|
-
//
|
|
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, "
|
|
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/"), "
|
|
1143
|
-
assertTrue(committed.includes("src.ts"), "
|
|
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:
|
|
1120
|
+
// ─── writeIntegrationBranch: no commit (metadata in external storage) ──
|
|
1149
1121
|
|
|
1150
|
-
console.log("\n=== writeIntegrationBranch:
|
|
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"
|
|
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
|
-
"
|
|
1132
|
+
"writeIntegrationBranch: metadata file exists on disk");
|
|
1161
1133
|
|
|
1162
|
-
//
|
|
1134
|
+
// No commit — .gsd/ is managed externally
|
|
1163
1135
|
const commitsAfter = run("git rev-list --count HEAD", repo);
|
|
1164
1136
|
assertEq(commitsBefore, commitsAfter,
|
|
1165
|
-
"
|
|
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
|
|
32
|
-
const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "
|
|
33
|
-
assert.ok(result.includes("
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
|
65
|
+
writeIntegrationBranch(basePath, milestoneId, current);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// ─── Pure Utility Functions (unchanged) ────────────────────────────────────
|