gsd-pi 2.25.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -2
- package/dist/headless.js +24 -4
- package/dist/resources/extensions/async-jobs/index.ts +9 -1
- package/dist/resources/extensions/bg-shell/index.ts +3 -2
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/dist/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/dist/resources/extensions/gsd/auto.ts +81 -12
- package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/dist/resources/extensions/gsd/doctor.ts +24 -1
- package/dist/resources/extensions/gsd/files.ts +13 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +19 -9
- package/dist/resources/extensions/gsd/index.ts +48 -7
- package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/dist/resources/extensions/gsd/preferences.ts +2 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +2 -2
- package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/dist/resources/extensions/gsd/state.ts +17 -6
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/search-the-web/native-search.ts +4 -0
- package/dist/resources/extensions/shared/path-display.ts +19 -0
- package/package.json +1 -6
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +25 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +27 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
- package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
- package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
- package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/index.d.ts +2 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +5 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +63 -30
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
- package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
- package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
- package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
- package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
- package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
- package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
- package/packages/pi-coding-agent/src/index.ts +15 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +40 -4
- package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
- package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
- package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
- package/src/resources/extensions/async-jobs/index.ts +9 -1
- package/src/resources/extensions/bg-shell/index.ts +3 -2
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +14 -3
- package/src/resources/extensions/gsd/auto.ts +81 -12
- package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/src/resources/extensions/gsd/doctor.ts +24 -1
- package/src/resources/extensions/gsd/files.ts +13 -2
- package/src/resources/extensions/gsd/guided-flow.ts +19 -9
- package/src/resources/extensions/gsd/index.ts +48 -7
- package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
- package/src/resources/extensions/gsd/preferences.ts +2 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
- package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +2 -2
- package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
- package/src/resources/extensions/gsd/state.ts +17 -6
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
- package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
- package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/search-the-web/native-search.ts +4 -0
- package/src/resources/extensions/shared/path-display.ts +19 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform path display tests.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that toPosixPath correctly normalizes Windows paths and that
|
|
5
|
+
* the system prompt builder produces forward-slash paths for LLM consumption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { toPosixPath } from "../utils/path-display.js";
|
|
11
|
+
import { buildSystemPrompt } from "../core/system-prompt.js";
|
|
12
|
+
|
|
13
|
+
// ─── toPosixPath ────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
test("toPosixPath: converts Windows backslash paths to forward slashes", () => {
|
|
16
|
+
assert.equal(toPosixPath("C:\\Users\\name\\project"), "C:/Users/name/project");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("toPosixPath: handles mixed separators", () => {
|
|
20
|
+
assert.equal(toPosixPath("C:\\Users/name\\project/src"), "C:/Users/name/project/src");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("toPosixPath: no-op for Unix paths", () => {
|
|
24
|
+
assert.equal(toPosixPath("/home/user/project"), "/home/user/project");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("toPosixPath: handles empty string", () => {
|
|
28
|
+
assert.equal(toPosixPath(""), "");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("toPosixPath: handles Windows UNC paths", () => {
|
|
32
|
+
assert.equal(toPosixPath("\\\\server\\share\\dir"), "//server/share/dir");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("toPosixPath: handles .gsd/worktrees path on Windows", () => {
|
|
36
|
+
assert.equal(
|
|
37
|
+
toPosixPath("C:\\Users\\name\\project\\.gsd\\worktrees\\M001"),
|
|
38
|
+
"C:/Users/name/project/.gsd/worktrees/M001",
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// ─── System prompt path normalization ───────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
test("buildSystemPrompt: cwd uses forward slashes even with Windows input", () => {
|
|
45
|
+
const prompt = buildSystemPrompt({
|
|
46
|
+
cwd: "C:\\Users\\name\\development\\app-name",
|
|
47
|
+
});
|
|
48
|
+
assert.ok(
|
|
49
|
+
prompt.includes("C:/Users/name/development/app-name"),
|
|
50
|
+
"System prompt should contain forward-slash path",
|
|
51
|
+
);
|
|
52
|
+
assert.ok(
|
|
53
|
+
!prompt.includes("C:\\Users\\name\\development\\app-name"),
|
|
54
|
+
"System prompt must NOT contain backslash path",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("buildSystemPrompt: Unix paths pass through unchanged", () => {
|
|
59
|
+
const prompt = buildSystemPrompt({
|
|
60
|
+
cwd: "/home/user/project",
|
|
61
|
+
});
|
|
62
|
+
assert.ok(prompt.includes("/home/user/project"));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ─── Regression: no backslash paths in LLM-visible text ────────────────────
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Pattern that matches Windows-style absolute paths with backslashes.
|
|
69
|
+
* Catches: C:\Users\..., D:\Projects\..., \\server\share\...
|
|
70
|
+
* Does not match: escaped chars in regex, JSON strings, etc.
|
|
71
|
+
*/
|
|
72
|
+
const WINDOWS_ABS_PATH_RE = /[A-Z]:\\[A-Za-z]/;
|
|
73
|
+
|
|
74
|
+
test("buildSystemPrompt: no Windows absolute paths with backslashes in output", () => {
|
|
75
|
+
// Simulate a Windows-like cwd
|
|
76
|
+
const prompt = buildSystemPrompt({
|
|
77
|
+
cwd: "D:\\Projects\\my-app\\.gsd\\worktrees\\M002",
|
|
78
|
+
});
|
|
79
|
+
const lines = prompt.split("\n");
|
|
80
|
+
const violations = lines.filter(line => WINDOWS_ABS_PATH_RE.test(line));
|
|
81
|
+
assert.equal(
|
|
82
|
+
violations.length, 0,
|
|
83
|
+
`System prompt contains Windows backslash paths:\n${violations.join("\n")}`,
|
|
84
|
+
);
|
|
85
|
+
});
|
|
@@ -114,16 +114,43 @@ function readClipboardImageViaWlPaste(): ClipboardImage | null {
|
|
|
114
114
|
.filter(Boolean);
|
|
115
115
|
|
|
116
116
|
const selectedType = selectPreferredImageMimeType(types);
|
|
117
|
-
if (
|
|
118
|
-
|
|
117
|
+
if (selectedType) {
|
|
118
|
+
const data = runCommand("wl-paste", ["--type", selectedType, "--no-newline"]);
|
|
119
|
+
if (data.ok && data.stdout.length > 0) {
|
|
120
|
+
return { bytes: data.stdout, mimeType: baseMimeType(selectedType) };
|
|
121
|
+
}
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
// Fallback for WSLg/BMP: when only image/bmp is available, ask wl-paste
|
|
125
|
+
// to convert to PNG on the fly. wl-paste supports format conversion for
|
|
126
|
+
// some compositor types. If that fails, try reading BMP and converting
|
|
127
|
+
// via ImageMagick (#813).
|
|
128
|
+
const hasBmp = types.some((t) => baseMimeType(t) === "image/bmp");
|
|
129
|
+
if (!selectedType && hasBmp) {
|
|
130
|
+
// Try requesting PNG directly — wl-paste may convert
|
|
131
|
+
const pngData = runCommand("wl-paste", ["--type", "image/png", "--no-newline"]);
|
|
132
|
+
if (pngData.ok && pngData.stdout.length > 0) {
|
|
133
|
+
return { bytes: pngData.stdout, mimeType: "image/png" };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Try reading BMP and converting via ImageMagick convert
|
|
137
|
+
const bmpData = runCommand("wl-paste", ["--type", "image/bmp", "--no-newline"]);
|
|
138
|
+
if (bmpData.ok && bmpData.stdout.length > 0) {
|
|
139
|
+
const converted = spawnSync("convert", ["bmp:-", "png:-"], {
|
|
140
|
+
input: bmpData.stdout,
|
|
141
|
+
timeout: 5000,
|
|
142
|
+
maxBuffer: DEFAULT_MAX_BUFFER_BYTES,
|
|
143
|
+
});
|
|
144
|
+
if (!converted.error && converted.status === 0 && converted.stdout.length > 0) {
|
|
145
|
+
const stdout = Buffer.isBuffer(converted.stdout)
|
|
146
|
+
? converted.stdout
|
|
147
|
+
: Buffer.from(converted.stdout);
|
|
148
|
+
return { bytes: stdout, mimeType: "image/png" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
124
151
|
}
|
|
125
152
|
|
|
126
|
-
return
|
|
153
|
+
return null;
|
|
127
154
|
}
|
|
128
155
|
|
|
129
156
|
function readClipboardImageViaXclip(): ClipboardImage | null {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform path display utilities.
|
|
3
|
+
*
|
|
4
|
+
* Paths injected into LLM prompts, tool results, or any text the model
|
|
5
|
+
* processes must use forward slashes. Windows backslash paths cause bash
|
|
6
|
+
* failures when the model copies them into shell commands — bash interprets
|
|
7
|
+
* backslashes as escape characters, silently stripping them.
|
|
8
|
+
*
|
|
9
|
+
* Node's `path` module and `fs` module handle native separators correctly
|
|
10
|
+
* for filesystem operations. This module is ONLY for paths that enter
|
|
11
|
+
* text consumed by the LLM or interpreted by a shell.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* import { toPosixPath } from "./path-display.js";
|
|
15
|
+
* prompt += `Current working directory: ${toPosixPath(cwd)}`;
|
|
16
|
+
*
|
|
17
|
+
* NOT for:
|
|
18
|
+
* fs.readFile(path) — use native path as-is
|
|
19
|
+
* path.join(a, b) — use native path module
|
|
20
|
+
* spawn(cmd, { cwd: path }) — Node handles this correctly
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Convert a filesystem path to forward-slash (POSIX) form for display.
|
|
25
|
+
*
|
|
26
|
+
* On Unix this is a no-op. On Windows it converts `C:\Users\name\project`
|
|
27
|
+
* to `C:/Users/name/project`, which is valid in:
|
|
28
|
+
* - Git Bash / MSYS2
|
|
29
|
+
* - WSL bash
|
|
30
|
+
* - PowerShell
|
|
31
|
+
* - Node.js APIs (which accept both separators)
|
|
32
|
+
* - Most Windows programs
|
|
33
|
+
*/
|
|
34
|
+
export function toPosixPath(fsPath: string): string {
|
|
35
|
+
return fsPath.replaceAll("\\", "/");
|
|
36
|
+
}
|
|
@@ -54,6 +54,14 @@ export default function AsyncJobs(pi: ExtensionAPI) {
|
|
|
54
54
|
? output.slice(0, maxLen) + "\n\n[... truncated, use await_job for full output]"
|
|
55
55
|
: output;
|
|
56
56
|
|
|
57
|
+
// Deliver as follow-up without triggering a new LLM turn (#875).
|
|
58
|
+
// When the agent is streaming: the message is queued and picked up
|
|
59
|
+
// by the agent loop's getFollowUpMessages() after the current turn.
|
|
60
|
+
// When the agent is idle: the message is appended to context so it's
|
|
61
|
+
// visible on the next user-initiated prompt. Previously triggerTurn:true
|
|
62
|
+
// caused spurious autonomous turns — the model would interpret completed
|
|
63
|
+
// job output as requiring action and cascade into unbounded self-reinforcing
|
|
64
|
+
// loops (running more commands, spawning more jobs, burning context).
|
|
57
65
|
pi.sendMessage(
|
|
58
66
|
{
|
|
59
67
|
customType: "async_job_result",
|
|
@@ -64,7 +72,7 @@ export default function AsyncJobs(pi: ExtensionAPI) {
|
|
|
64
72
|
].join("\n"),
|
|
65
73
|
display: true,
|
|
66
74
|
},
|
|
67
|
-
{ deliverAs: "followUp"
|
|
75
|
+
{ deliverAs: "followUp" },
|
|
68
76
|
);
|
|
69
77
|
},
|
|
70
78
|
});
|
|
@@ -66,6 +66,7 @@ import { waitForReady } from "./readiness-detector.js";
|
|
|
66
66
|
import { queryShellEnv, sendAndWait, runOnSession } from "./interaction.js";
|
|
67
67
|
import { formatUptime, formatTokenCount, resolveBgShellPersistenceCwd } from "./utilities.js";
|
|
68
68
|
import { BgManagerOverlay } from "./overlay.js";
|
|
69
|
+
import { toPosixPath } from "../shared/path-display.js";
|
|
69
70
|
|
|
70
71
|
// ── Re-exports for consumers ───────────────────────────────────────────────
|
|
71
72
|
|
|
@@ -337,7 +338,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
337
338
|
text += ` type: ${bg.processType}\n`;
|
|
338
339
|
text += ` status: ${bg.status}\n`;
|
|
339
340
|
text += ` command: ${bg.command}\n`;
|
|
340
|
-
text += ` cwd: ${bg.cwd}`;
|
|
341
|
+
text += ` cwd: ${toPosixPath(bg.cwd)}`;
|
|
341
342
|
|
|
342
343
|
if (bg.group) text += `\n group: ${bg.group}`;
|
|
343
344
|
if (bg.readyPort) text += `\n ready_port: ${bg.readyPort}`;
|
|
@@ -694,7 +695,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
694
695
|
}
|
|
695
696
|
|
|
696
697
|
let text = `Shell environment for ${bg.id} (${bg.label}):\n`;
|
|
697
|
-
text += ` cwd: ${envResult.cwd}\n`;
|
|
698
|
+
text += ` cwd: ${toPosixPath(envResult.cwd)}\n`;
|
|
698
699
|
text += ` shell: ${envResult.shell}\n`;
|
|
699
700
|
|
|
700
701
|
const envEntries = Object.entries(envResult.env);
|
|
@@ -90,6 +90,10 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
90
90
|
const dir = resolveMilestonePath(base, mid);
|
|
91
91
|
return dir ? join(dir, buildMilestoneFileName(mid, "SUMMARY")) : null;
|
|
92
92
|
}
|
|
93
|
+
case "replan-slice": {
|
|
94
|
+
const dir = resolveSlicePath(base, mid, sid!);
|
|
95
|
+
return dir ? join(dir, buildSliceFileName(sid!, "REPLAN")) : null;
|
|
96
|
+
}
|
|
93
97
|
case "rewrite-docs":
|
|
94
98
|
return null;
|
|
95
99
|
default:
|
|
@@ -127,10 +131,9 @@ export function verifyExpectedArtifact(unitType: string, unitId: string, base: s
|
|
|
127
131
|
}
|
|
128
132
|
|
|
129
133
|
const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
if (!absPath) return unitType === "replan-slice";
|
|
134
|
+
// For unit types with no verifiable artifact (null path), the parent directory
|
|
135
|
+
// is missing on disk — treat as stale completion state so the key gets evicted (#313).
|
|
136
|
+
if (!absPath) return false;
|
|
134
137
|
if (!existsSync(absPath)) return false;
|
|
135
138
|
|
|
136
139
|
// plan-slice must produce a plan with actual task entries, not just a scaffold.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* manages create, enter, detect, and teardown for auto-mode worktrees.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync } from "node:fs";
|
|
9
|
+
import { existsSync, cpSync, readFileSync, writeFileSync, readdirSync, mkdirSync, realpathSync, utimesSync, unlinkSync } from "node:fs";
|
|
10
10
|
import { isAbsolute, join, resolve } from "node:path";
|
|
11
11
|
import { copyWorktreeDb, reconcileWorktreeDb, isDbAvailable } from "./gsd-db.js";
|
|
12
12
|
import { execSync, execFileSync } from "node:child_process";
|
|
@@ -312,7 +312,8 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
314
|
* Copy .gsd/ planning artifacts from source repo to a new worktree.
|
|
315
|
-
* Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md
|
|
315
|
+
* Copies milestones/, DECISIONS.md, REQUIREMENTS.md, PROJECT.md, QUEUE.md,
|
|
316
|
+
* STATE.md, KNOWLEDGE.md, and OVERRIDES.md.
|
|
316
317
|
* Skips runtime files (auto.lock, metrics.json, etc.) and the worktrees/ dir.
|
|
317
318
|
* Best-effort — failures are non-fatal since auto-mode can recreate artifacts.
|
|
318
319
|
*/
|
|
@@ -330,7 +331,7 @@ function copyPlanningArtifacts(srcBase: string, wtPath: string): void {
|
|
|
330
331
|
}
|
|
331
332
|
|
|
332
333
|
// Copy top-level planning files
|
|
333
|
-
for (const file of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "QUEUE.md"]) {
|
|
334
|
+
for (const file of ["DECISIONS.md", "REQUIREMENTS.md", "PROJECT.md", "QUEUE.md", "STATE.md", "KNOWLEDGE.md", "OVERRIDES.md"]) {
|
|
334
335
|
const src = join(srcGsd, file);
|
|
335
336
|
if (existsSync(src)) {
|
|
336
337
|
try {
|
|
@@ -559,6 +560,16 @@ export function mergeMilestoneToMain(
|
|
|
559
560
|
// when main is already checked out in the project-root worktree, #757)
|
|
560
561
|
const currentBranchAtBase = nativeGetCurrentBranch(originalBasePath_);
|
|
561
562
|
if (currentBranchAtBase !== mainBranch) {
|
|
563
|
+
// Remove untracked .gsd/ state files that may conflict with the branch
|
|
564
|
+
// being checked out. These are regenerated by doctor/rebuildState and
|
|
565
|
+
// are not meaningful in the main working tree — the worktree had the
|
|
566
|
+
// real state. Without this, `git checkout main` fails with
|
|
567
|
+
// "Your local changes would be overwritten" (#827).
|
|
568
|
+
const gsdStateFiles = ["STATE.md", "completed-units.json", "auto.lock"];
|
|
569
|
+
for (const f of gsdStateFiles) {
|
|
570
|
+
const p = join(originalBasePath_, ".gsd", f);
|
|
571
|
+
try { unlinkSync(p); } catch { /* non-fatal — file may not exist */ }
|
|
572
|
+
}
|
|
562
573
|
nativeCheckoutBranch(originalBasePath_, mainBranch);
|
|
563
574
|
}
|
|
564
575
|
|
|
@@ -166,6 +166,41 @@ import { hasPendingCaptures, loadPendingCaptures, countPendingCaptures } from ".
|
|
|
166
166
|
// auto-mode reads stale state from the project root and re-dispatches
|
|
167
167
|
// already-completed units.
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Sync milestone artifacts from project root INTO worktree before deriveState.
|
|
171
|
+
* Covers the case where the LLM wrote artifacts to the main repo filesystem
|
|
172
|
+
* (e.g. via absolute paths) but the worktree has stale data. Also deletes
|
|
173
|
+
* gsd.db in the worktree so it rebuilds from fresh disk state (#853).
|
|
174
|
+
* Non-fatal — sync failure should never block dispatch.
|
|
175
|
+
*/
|
|
176
|
+
function syncProjectRootToWorktree(projectRoot: string, worktreePath: string, milestoneId: string | null): void {
|
|
177
|
+
if (!worktreePath || !projectRoot || worktreePath === projectRoot) return;
|
|
178
|
+
if (!milestoneId) return;
|
|
179
|
+
|
|
180
|
+
const prGsd = join(projectRoot, ".gsd");
|
|
181
|
+
const wtGsd = join(worktreePath, ".gsd");
|
|
182
|
+
|
|
183
|
+
// Copy milestone directory from project root to worktree if the project root
|
|
184
|
+
// has newer artifacts (e.g. slices that don't exist in the worktree yet)
|
|
185
|
+
try {
|
|
186
|
+
const srcMilestone = join(prGsd, "milestones", milestoneId);
|
|
187
|
+
const dstMilestone = join(wtGsd, "milestones", milestoneId);
|
|
188
|
+
if (existsSync(srcMilestone)) {
|
|
189
|
+
mkdirSync(dstMilestone, { recursive: true });
|
|
190
|
+
cpSync(srcMilestone, dstMilestone, { recursive: true, force: false });
|
|
191
|
+
}
|
|
192
|
+
} catch { /* non-fatal */ }
|
|
193
|
+
|
|
194
|
+
// Delete worktree gsd.db so it rebuilds from the freshly synced files.
|
|
195
|
+
// Stale DB rows are the root cause of the infinite skip loop (#853).
|
|
196
|
+
try {
|
|
197
|
+
const wtDb = join(wtGsd, "gsd.db");
|
|
198
|
+
if (existsSync(wtDb)) {
|
|
199
|
+
unlinkSync(wtDb);
|
|
200
|
+
}
|
|
201
|
+
} catch { /* non-fatal */ }
|
|
202
|
+
}
|
|
203
|
+
|
|
169
204
|
/**
|
|
170
205
|
* Sync dispatch-critical .gsd/ state files from worktree to project root.
|
|
171
206
|
* Only runs when inside an auto-worktree (worktreePath differs from projectRoot).
|
|
@@ -261,28 +296,30 @@ const MAX_CONSECUTIVE_SKIPS = 3;
|
|
|
261
296
|
/** Persisted completed-unit keys — survives restarts. Loaded from .gsd/completed-units.json. */
|
|
262
297
|
const completedKeySet = new Set<string>();
|
|
263
298
|
|
|
264
|
-
/** Resource
|
|
265
|
-
* manifest changes mid-session (e.g.
|
|
299
|
+
/** Resource version captured at auto-mode start. If the managed-resources
|
|
300
|
+
* manifest version changes mid-session (e.g. npm update -g gsd-pi),
|
|
266
301
|
* templates on disk may expect variables the in-memory code doesn't provide.
|
|
267
|
-
* Detect this and stop gracefully instead of crashing.
|
|
268
|
-
|
|
302
|
+
* Detect this and stop gracefully instead of crashing.
|
|
303
|
+
* Uses gsdVersion (semver) instead of syncedAt (timestamp) so that
|
|
304
|
+
* launching a second session doesn't falsely trigger staleness (#804). */
|
|
305
|
+
let resourceVersionOnStart: string | null = null;
|
|
269
306
|
|
|
270
|
-
function
|
|
307
|
+
function readResourceVersion(): string | null {
|
|
271
308
|
const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
|
|
272
309
|
const manifestPath = join(agentDir, "managed-resources.json");
|
|
273
310
|
try {
|
|
274
311
|
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
275
|
-
return typeof manifest?.
|
|
312
|
+
return typeof manifest?.gsdVersion === "string" ? manifest.gsdVersion : null;
|
|
276
313
|
} catch {
|
|
277
314
|
return null;
|
|
278
315
|
}
|
|
279
316
|
}
|
|
280
317
|
|
|
281
318
|
function checkResourcesStale(): string | null {
|
|
282
|
-
if (
|
|
283
|
-
const current =
|
|
319
|
+
if (resourceVersionOnStart === null) return null;
|
|
320
|
+
const current = readResourceVersion();
|
|
284
321
|
if (current === null) return null;
|
|
285
|
-
if (current !==
|
|
322
|
+
if (current !== resourceVersionOnStart) {
|
|
286
323
|
return "GSD resources were updated since this session started. Restart gsd to load the new code.";
|
|
287
324
|
}
|
|
288
325
|
return null;
|
|
@@ -942,6 +979,11 @@ export async function startAuto(
|
|
|
942
979
|
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
943
980
|
}
|
|
944
981
|
|
|
982
|
+
// Invalidate all caches before initial state derivation to ensure we read
|
|
983
|
+
// fresh disk state. Without this, a stale cache from a prior session (e.g.
|
|
984
|
+
// after a discussion that wrote new artifacts) may cause deriveState to
|
|
985
|
+
// return pre-planning when the roadmap already exists (#800).
|
|
986
|
+
invalidateAllCaches();
|
|
945
987
|
let state = await deriveState(base);
|
|
946
988
|
|
|
947
989
|
// ── Stale worktree state recovery (#654) ─────────────────────────────────
|
|
@@ -1075,7 +1117,7 @@ export async function startAuto(
|
|
|
1075
1117
|
restoreHookState(base);
|
|
1076
1118
|
resetProactiveHealing();
|
|
1077
1119
|
autoStartTime = Date.now();
|
|
1078
|
-
|
|
1120
|
+
resourceVersionOnStart = readResourceVersion();
|
|
1079
1121
|
completedUnits = [];
|
|
1080
1122
|
pendingQuickTasks = [];
|
|
1081
1123
|
currentUnit = null;
|
|
@@ -1374,10 +1416,13 @@ export async function handleAgentEnd(
|
|
|
1374
1416
|
// fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
|
|
1375
1417
|
// checkboxes). Slice/milestone completion transitions (summary stubs,
|
|
1376
1418
|
// roadmap [x] marking) are left for the complete-slice dispatch unit.
|
|
1419
|
+
// Exception: after complete-slice itself, use fixLevel:"all" so roadmap
|
|
1420
|
+
// checkboxes get fixed even if complete-slice crashed (#839).
|
|
1377
1421
|
try {
|
|
1378
1422
|
const scopeParts = currentUnit.id.split("/").slice(0, 2);
|
|
1379
1423
|
const doctorScope = scopeParts.join("/");
|
|
1380
|
-
const
|
|
1424
|
+
const effectiveFixLevel = currentUnit.type === "complete-slice" ? "all" as const : "task" as const;
|
|
1425
|
+
const report = await runGSDDoctor(basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
|
|
1381
1426
|
if (report.fixesApplied.length > 0) {
|
|
1382
1427
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
1383
1428
|
}
|
|
@@ -2011,6 +2056,7 @@ async function dispatchNextUnit(
|
|
|
2011
2056
|
pi: ExtensionAPI,
|
|
2012
2057
|
): Promise<void> {
|
|
2013
2058
|
if (!active || !cmdCtx) {
|
|
2059
|
+
debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
|
|
2014
2060
|
if (active && !cmdCtx) {
|
|
2015
2061
|
ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
|
|
2016
2062
|
}
|
|
@@ -2020,6 +2066,7 @@ async function dispatchNextUnit(
|
|
|
2020
2066
|
// Reentrancy guard: allow recursive calls from skip paths (_skipDepth > 0)
|
|
2021
2067
|
// but block concurrent external calls (watchdog, step wizard, etc.)
|
|
2022
2068
|
if (_dispatching && _skipDepth === 0) {
|
|
2069
|
+
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
2023
2070
|
return; // Another dispatch is in progress — bail silently
|
|
2024
2071
|
}
|
|
2025
2072
|
_dispatching = true;
|
|
@@ -2055,7 +2102,7 @@ async function dispatchNextUnit(
|
|
|
2055
2102
|
// Lightweight check for critical issues that would cause the next unit
|
|
2056
2103
|
// to fail or corrupt state. Auto-heals what it can, blocks on the rest.
|
|
2057
2104
|
try {
|
|
2058
|
-
const healthGate = preDispatchHealthGate(basePath);
|
|
2105
|
+
const healthGate = await preDispatchHealthGate(basePath);
|
|
2059
2106
|
if (healthGate.fixesApplied.length > 0) {
|
|
2060
2107
|
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
2061
2108
|
}
|
|
@@ -2068,6 +2115,14 @@ async function dispatchNextUnit(
|
|
|
2068
2115
|
// Non-fatal — health gate failure should never block dispatch
|
|
2069
2116
|
}
|
|
2070
2117
|
|
|
2118
|
+
// ── Sync project root artifacts into worktree (#853) ─────────────────
|
|
2119
|
+
// When the LLM writes artifacts to the main repo filesystem instead of
|
|
2120
|
+
// the worktree, the worktree's gsd.db becomes stale. Sync before
|
|
2121
|
+
// deriveState to ensure the worktree has the latest artifacts.
|
|
2122
|
+
if (originalBasePath && basePath !== originalBasePath && currentMilestoneId) {
|
|
2123
|
+
syncProjectRootToWorktree(originalBasePath, basePath, currentMilestoneId);
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2071
2126
|
const stopDeriveTimer = debugTime("derive-state");
|
|
2072
2127
|
let state = await deriveState(basePath);
|
|
2073
2128
|
stopDeriveTimer({
|
|
@@ -3751,6 +3806,20 @@ export async function dispatchDirectPhase(
|
|
|
3751
3806
|
ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
|
|
3752
3807
|
return;
|
|
3753
3808
|
}
|
|
3809
|
+
|
|
3810
|
+
// When require_slice_discussion is enabled, pause auto-mode before
|
|
3811
|
+
// each new slice so the user can discuss requirements first (#789).
|
|
3812
|
+
const sliceContextFile = resolveSliceFile(base, mid, sid, "CONTEXT");
|
|
3813
|
+
const requireDiscussion = loadEffectiveGSDPreferences()?.preferences?.phases?.require_slice_discussion;
|
|
3814
|
+
if (requireDiscussion && !sliceContextFile) {
|
|
3815
|
+
ctx.ui.notify(
|
|
3816
|
+
`Slice ${sid} requires discussion before planning. Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
|
|
3817
|
+
"info",
|
|
3818
|
+
);
|
|
3819
|
+
await pauseAuto(ctx, pi);
|
|
3820
|
+
return;
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3754
3823
|
unitType = "research-slice";
|
|
3755
3824
|
unitId = `${mid}/${sid}`;
|
|
3756
3825
|
prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
@@ -19,6 +19,7 @@ import { join } from "node:path";
|
|
|
19
19
|
import { gsdRoot, resolveGsdRootFile } from "./paths.js";
|
|
20
20
|
import { readCrashLock, isLockProcessAlive, clearLock } from "./crash-recovery.js";
|
|
21
21
|
import { abortAndReset } from "./git-self-heal.js";
|
|
22
|
+
import { rebuildState } from "./doctor.js";
|
|
22
23
|
|
|
23
24
|
// ── Health Score Tracking ──────────────────────────────────────────────────
|
|
24
25
|
|
|
@@ -131,7 +132,7 @@ export interface PreDispatchHealthResult {
|
|
|
131
132
|
*
|
|
132
133
|
* Returns { proceed: true } if dispatch should continue.
|
|
133
134
|
*/
|
|
134
|
-
export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult {
|
|
135
|
+
export async function preDispatchHealthGate(basePath: string): Promise<PreDispatchHealthResult> {
|
|
135
136
|
const issues: string[] = [];
|
|
136
137
|
const fixesApplied: string[] = [];
|
|
137
138
|
|
|
@@ -172,17 +173,17 @@ export function preDispatchHealthGate(basePath: string): PreDispatchHealthResult
|
|
|
172
173
|
}
|
|
173
174
|
|
|
174
175
|
// ── STATE.md existence check ──
|
|
175
|
-
// If STATE.md is missing,
|
|
176
|
-
//
|
|
176
|
+
// If STATE.md is missing, rebuild it now so the next unit has accurate
|
|
177
|
+
// context. Non-blocking — if the rebuild throws, dispatch continues anyway.
|
|
177
178
|
try {
|
|
178
179
|
const stateFile = resolveGsdRootFile(basePath, "STATE");
|
|
179
180
|
const milestonesDir = join(gsdRoot(basePath), "milestones");
|
|
180
181
|
if (existsSync(milestonesDir) && !existsSync(stateFile)) {
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
await rebuildState(basePath);
|
|
183
|
+
fixesApplied.push("rebuilt missing STATE.md before dispatch");
|
|
183
184
|
}
|
|
184
185
|
} catch {
|
|
185
|
-
// Non-fatal
|
|
186
|
+
// Non-fatal — dispatch continues without STATE.md if rebuild fails
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// If we had critical issues that couldn't be auto-healed, block dispatch
|
|
@@ -1144,8 +1144,31 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
1144
1144
|
unitId: taskUnitId,
|
|
1145
1145
|
message: `Task ${task.id} is marked done but summary is missing`,
|
|
1146
1146
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
1147
|
-
fixable:
|
|
1147
|
+
fixable: true,
|
|
1148
1148
|
});
|
|
1149
|
+
// Write a stub summary so validate-milestone can proceed.
|
|
1150
|
+
// This prevents infinite skip loops when tasks are marked done
|
|
1151
|
+
// without summaries (#820).
|
|
1152
|
+
if (shouldFix("task_done_missing_summary")) {
|
|
1153
|
+
const stubPath = join(
|
|
1154
|
+
basePath, ".gsd", "milestones", milestoneId, "slices", slice.id, "tasks",
|
|
1155
|
+
`${task.id}-SUMMARY.md`,
|
|
1156
|
+
);
|
|
1157
|
+
const stubContent = [
|
|
1158
|
+
`---`,
|
|
1159
|
+
`status: done`,
|
|
1160
|
+
`result: unknown`,
|
|
1161
|
+
`doctor_generated: true`,
|
|
1162
|
+
`---`,
|
|
1163
|
+
``,
|
|
1164
|
+
`# ${task.id}: ${task.title || "Unknown"}`,
|
|
1165
|
+
``,
|
|
1166
|
+
`Summary stub generated by \`/gsd doctor\` — task was marked done but no summary existed.`,
|
|
1167
|
+
``,
|
|
1168
|
+
].join("\n");
|
|
1169
|
+
await saveFile(stubPath, stubContent);
|
|
1170
|
+
fixesApplied.push(`created stub summary for ${taskUnitId}`);
|
|
1171
|
+
}
|
|
1149
1172
|
}
|
|
1150
1173
|
|
|
1151
1174
|
if (!task.done && hasSummary) {
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { promises as fs } from 'node:fs';
|
|
7
7
|
import { dirname, resolve } from 'node:path';
|
|
8
|
+
import { randomBytes } from 'node:crypto';
|
|
8
9
|
import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
|
|
9
10
|
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
|
10
11
|
|
|
@@ -705,9 +706,19 @@ export async function saveFile(path: string, content: string): Promise<void> {
|
|
|
705
706
|
const dir = dirname(path);
|
|
706
707
|
await fs.mkdir(dir, { recursive: true });
|
|
707
708
|
|
|
708
|
-
|
|
709
|
+
// Use a unique temp path per call to avoid collisions when parallel
|
|
710
|
+
// tool calls target the same file (e.g. concurrent gsd_save_decision).
|
|
711
|
+
// rename() is atomic on POSIX, so last-writer-wins is correct for
|
|
712
|
+
// regenerate-from-DB writes.
|
|
713
|
+
const tmpPath = path + `.tmp.${randomBytes(4).toString("hex")}`;
|
|
709
714
|
await fs.writeFile(tmpPath, content, 'utf-8');
|
|
710
|
-
|
|
715
|
+
try {
|
|
716
|
+
await fs.rename(tmpPath, path);
|
|
717
|
+
} catch (err) {
|
|
718
|
+
// Clean up orphaned temp file on rename failure
|
|
719
|
+
await fs.unlink(tmpPath).catch(() => {});
|
|
720
|
+
throw err;
|
|
721
|
+
}
|
|
711
722
|
}
|
|
712
723
|
|
|
713
724
|
export function parseRequirementCounts(content: string | null): RequirementCounts {
|
|
@@ -636,9 +636,11 @@ async function showQueueAdd(
|
|
|
636
636
|
const existingContext = await buildExistingMilestonesContext(basePath, milestoneIds, state);
|
|
637
637
|
|
|
638
638
|
// ── Determine next milestone ID ─────────────────────────────────────
|
|
639
|
+
// Note: the LLM will use the gsd_generate_milestone_id tool to get IDs
|
|
640
|
+
// at creation time, but we still mention the next ID in the preamble
|
|
641
|
+
// for context about where the sequence is.
|
|
639
642
|
const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
640
643
|
const nextId = nextMilestoneId(milestoneIds, uniqueEnabled);
|
|
641
|
-
const nextIdPlus1 = nextMilestoneId([...milestoneIds, nextId], uniqueEnabled);
|
|
642
644
|
|
|
643
645
|
// ── Build preamble ──────────────────────────────────────────────────
|
|
644
646
|
const activePart = state.activeMilestone
|
|
@@ -659,8 +661,6 @@ async function showQueueAdd(
|
|
|
659
661
|
const queueInlinedTemplates = inlineTemplate("context", "Context");
|
|
660
662
|
const prompt = loadPrompt("queue", {
|
|
661
663
|
preamble,
|
|
662
|
-
nextId,
|
|
663
|
-
nextIdPlus1,
|
|
664
664
|
existingMilestonesContext: existingContext,
|
|
665
665
|
inlinedTemplates: queueInlinedTemplates,
|
|
666
666
|
commitInstruction: buildDocsCommitInstruction("docs: queue <milestone list>"),
|
|
@@ -959,12 +959,22 @@ export async function showDiscuss(
|
|
|
959
959
|
|
|
960
960
|
// Loop: show picker, dispatch discuss, repeat until "not_yet"
|
|
961
961
|
while (true) {
|
|
962
|
-
const actions = pendingSlices.map((s, i) =>
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
962
|
+
const actions = pendingSlices.map((s, i) => {
|
|
963
|
+
// Check if this slice has already been discussed (CONTEXT file exists)
|
|
964
|
+
const contextFile = resolveSliceFile(basePath, mid, s.id, "CONTEXT");
|
|
965
|
+
const discussed = !!contextFile;
|
|
966
|
+
const statusParts: string[] = [];
|
|
967
|
+
if (state.activeSlice?.id === s.id) statusParts.push("active");
|
|
968
|
+
else statusParts.push("upcoming");
|
|
969
|
+
statusParts.push(discussed ? "discussed ✓" : "not discussed");
|
|
970
|
+
|
|
971
|
+
return {
|
|
972
|
+
id: s.id,
|
|
973
|
+
label: `${s.id}: ${s.title}`,
|
|
974
|
+
description: statusParts.join(" · "),
|
|
975
|
+
recommended: i === 0,
|
|
976
|
+
};
|
|
977
|
+
});
|
|
968
978
|
|
|
969
979
|
const choice = await showNextAction(ctx, {
|
|
970
980
|
title: "GSD — Discuss a slice",
|