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
package/README.md
CHANGED
|
@@ -32,6 +32,7 @@ Full documentation is available in the [`docs/`](./docs/) directory:
|
|
|
32
32
|
- **[Token Optimization](./docs/token-optimization.md)** — profiles, context compression, complexity routing (v2.17)
|
|
33
33
|
- **[Cost Management](./docs/cost-management.md)** — budgets, tracking, projections
|
|
34
34
|
- **[Git Strategy](./docs/git-strategy.md)** — worktree isolation, branching, merge behavior
|
|
35
|
+
- **[Parallel Orchestration](./docs/parallel-orchestration.md)** — run multiple milestones simultaneously
|
|
35
36
|
- **[Working in Teams](./docs/working-in-teams.md)** — unique IDs, shared artifacts
|
|
36
37
|
- **[Skills](./docs/skills.md)** — bundled skills, discovery, custom authoring
|
|
37
38
|
- **[Commands Reference](./docs/commands.md)** — all commands and keyboard shortcuts
|
|
@@ -112,9 +113,11 @@ Each slice flows through phases automatically:
|
|
|
112
113
|
|
|
113
114
|
```
|
|
114
115
|
Research → Plan → Execute (per task) → Complete → Reassess Roadmap → Next Slice
|
|
116
|
+
↓ (all slices done)
|
|
117
|
+
Validate Milestone → Complete Milestone
|
|
115
118
|
```
|
|
116
119
|
|
|
117
|
-
**Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded. **Complete** writes the summary, UAT script, marks the roadmap, and commits. **Reassess** checks if the roadmap still makes sense given what was learned.
|
|
120
|
+
**Research** scouts the codebase and relevant docs. **Plan** decomposes the slice into tasks with must-haves (mechanically verifiable outcomes). **Execute** runs each task in a fresh context window with only the relevant files pre-loaded. **Complete** writes the summary, UAT script, marks the roadmap, and commits. **Reassess** checks if the roadmap still makes sense given what was learned. **Validate Milestone** runs a reconciliation gate after all slices complete — comparing roadmap success criteria against actual results before sealing the milestone.
|
|
118
121
|
|
|
119
122
|
### `/gsd auto` — The Main Event
|
|
120
123
|
|
|
@@ -556,7 +559,13 @@ Anthropic, OpenAI, Google (Gemini), OpenRouter, GitHub Copilot, Amazon Bedrock,
|
|
|
556
559
|
|
|
557
560
|
If you have a **Claude Max**, **Codex**, or **GitHub Copilot** subscription, you can use those directly — Pi handles the OAuth flow. No API key needed.
|
|
558
561
|
|
|
559
|
-
>
|
|
562
|
+
> **⚠️ Important:** Using OAuth tokens from subscription plans outside their native applications may violate the provider's Terms of Service. In particular:
|
|
563
|
+
>
|
|
564
|
+
> - **Google Gemini** — Using Gemini CLI or Antigravity OAuth tokens in third-party tools has resulted in **Google account suspensions**. This affects your entire Google account, not just the Gemini service. **Use a Gemini API key instead.**
|
|
565
|
+
> - **Claude Max** — Anthropic's ToS may not explicitly permit OAuth use outside Claude's own applications.
|
|
566
|
+
> - **GitHub Copilot** — Usage outside GitHub's own tools may be restricted by your subscription terms.
|
|
567
|
+
>
|
|
568
|
+
> GSD supports API key authentication for all providers as the safe alternative. **We strongly recommend using API keys over OAuth for Google Gemini.**
|
|
560
569
|
|
|
561
570
|
### OpenRouter
|
|
562
571
|
|
package/dist/headless.js
CHANGED
|
@@ -137,18 +137,37 @@ function formatProgress(event, verbose) {
|
|
|
137
137
|
// ---------------------------------------------------------------------------
|
|
138
138
|
// Completion Detection
|
|
139
139
|
// ---------------------------------------------------------------------------
|
|
140
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Detect genuine auto-mode termination notifications.
|
|
142
|
+
*
|
|
143
|
+
* Only matches the actual stop signals emitted by stopAuto():
|
|
144
|
+
* "Auto-mode stopped..."
|
|
145
|
+
* "Step-mode stopped..."
|
|
146
|
+
*
|
|
147
|
+
* Does NOT match progress notifications that happen to contain words like
|
|
148
|
+
* "complete" or "stopped" (e.g., "Override resolved — rewrite-docs completed",
|
|
149
|
+
* "All slices are complete — nothing to discuss", "Skipped 5+ completed units").
|
|
150
|
+
*
|
|
151
|
+
* Blocked detection is separate — checked via isBlockedNotification.
|
|
152
|
+
*/
|
|
153
|
+
const TERMINAL_PREFIXES = ['auto-mode stopped', 'step-mode stopped'];
|
|
141
154
|
const IDLE_TIMEOUT_MS = 15_000;
|
|
155
|
+
// new-milestone is a long-running creative task where the LLM may pause
|
|
156
|
+
// between tool calls (e.g. after mkdir, before writing files). Use a
|
|
157
|
+
// longer idle timeout to avoid killing the session prematurely (#808).
|
|
158
|
+
const NEW_MILESTONE_IDLE_TIMEOUT_MS = 120_000;
|
|
142
159
|
function isTerminalNotification(event) {
|
|
143
160
|
if (event.type !== 'extension_ui_request' || event.method !== 'notify')
|
|
144
161
|
return false;
|
|
145
162
|
const message = String(event.message ?? '').toLowerCase();
|
|
146
|
-
return
|
|
163
|
+
return TERMINAL_PREFIXES.some((prefix) => message.startsWith(prefix));
|
|
147
164
|
}
|
|
148
165
|
function isBlockedNotification(event) {
|
|
149
166
|
if (event.type !== 'extension_ui_request' || event.method !== 'notify')
|
|
150
167
|
return false;
|
|
151
|
-
|
|
168
|
+
const message = String(event.message ?? '').toLowerCase();
|
|
169
|
+
// Blocked notifications come through stopAuto as "Auto-mode stopped (Blocked: ...)"
|
|
170
|
+
return message.includes('blocked:');
|
|
152
171
|
}
|
|
153
172
|
function isMilestoneReadyNotification(event) {
|
|
154
173
|
if (event.type !== 'extension_ui_request' || event.method !== 'notify')
|
|
@@ -285,6 +304,7 @@ export async function runHeadless(options) {
|
|
|
285
304
|
});
|
|
286
305
|
// Idle timeout — fallback completion detection
|
|
287
306
|
let idleTimer = null;
|
|
307
|
+
const effectiveIdleTimeout = isNewMilestone ? NEW_MILESTONE_IDLE_TIMEOUT_MS : IDLE_TIMEOUT_MS;
|
|
288
308
|
function resetIdleTimer() {
|
|
289
309
|
if (idleTimer)
|
|
290
310
|
clearTimeout(idleTimer);
|
|
@@ -292,7 +312,7 @@ export async function runHeadless(options) {
|
|
|
292
312
|
idleTimer = setTimeout(() => {
|
|
293
313
|
completed = true;
|
|
294
314
|
resolveCompletion();
|
|
295
|
-
},
|
|
315
|
+
}, effectiveIdleTimeout);
|
|
296
316
|
}
|
|
297
317
|
}
|
|
298
318
|
// Overall timeout
|
|
@@ -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",
|
|
@@ -36,7 +36,7 @@ import { loadPrompt } from "./prompt-loader.js";
|
|
|
36
36
|
import { deriveState } from "./state.js";
|
|
37
37
|
import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData, markToolStart, markToolEnd } from "./auto.js";
|
|
38
38
|
import { saveActivityLog } from "./activity-log.js";
|
|
39
|
-
import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId } from "./guided-flow.js";
|
|
39
|
+
import { checkAutoStartAfterDiscuss, getDiscussionMilestoneId, findMilestoneIds, nextMilestoneId } from "./guided-flow.js";
|
|
40
40
|
import { GSDDashboardOverlay } from "./dashboard-overlay.js";
|
|
41
41
|
import {
|
|
42
42
|
loadEffectiveGSDPreferences,
|
|
@@ -59,6 +59,7 @@ import { homedir } from "node:os";
|
|
|
59
59
|
import { shortcutDesc } from "../shared/terminal.js";
|
|
60
60
|
import { Text } from "@gsd/pi-tui";
|
|
61
61
|
import { pauseAutoForProviderError } from "./provider-error-pause.js";
|
|
62
|
+
import { toPosixPath } from "../shared/path-display.js";
|
|
62
63
|
|
|
63
64
|
// ── Agent Instructions ────────────────────────────────────────────────────
|
|
64
65
|
// Lightweight "always follow" files injected into every GSD agent session.
|
|
@@ -467,6 +468,46 @@ export default function (pi: ExtensionAPI) {
|
|
|
467
468
|
},
|
|
468
469
|
});
|
|
469
470
|
|
|
471
|
+
// ── gsd_generate_milestone_id — canonical milestone ID generation ──────
|
|
472
|
+
// The LLM cannot generate random suffixes for unique_milestone_ids on its
|
|
473
|
+
// own. This tool calls back into the TS code that owns ID generation,
|
|
474
|
+
// ensuring the preference is always respected and IDs are always valid.
|
|
475
|
+
pi.registerTool({
|
|
476
|
+
name: "gsd_generate_milestone_id",
|
|
477
|
+
label: "Generate Milestone ID",
|
|
478
|
+
description:
|
|
479
|
+
"Generate the next milestone ID for a new GSD milestone. " +
|
|
480
|
+
"Scans existing milestones on disk and respects the unique_milestone_ids preference. " +
|
|
481
|
+
"Always use this tool when creating a new milestone — never invent milestone IDs manually.",
|
|
482
|
+
promptSnippet: "Generate a valid milestone ID (respects unique_milestone_ids preference)",
|
|
483
|
+
promptGuidelines: [
|
|
484
|
+
"ALWAYS call gsd_generate_milestone_id before creating a new milestone directory or writing milestone files.",
|
|
485
|
+
"Never invent or hardcode milestone IDs like M001, M002 — always use this tool.",
|
|
486
|
+
"Call it once per milestone you need to create. For multi-milestone projects, call it once for each milestone in sequence.",
|
|
487
|
+
"The tool returns the correct format based on project preferences (e.g. M001 or M001-r5jzab).",
|
|
488
|
+
],
|
|
489
|
+
parameters: Type.Object({}),
|
|
490
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
491
|
+
try {
|
|
492
|
+
const basePath = process.cwd();
|
|
493
|
+
const existingIds = findMilestoneIds(basePath);
|
|
494
|
+
const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
495
|
+
const newId = nextMilestoneId(existingIds, uniqueEnabled);
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: "text" as const, text: newId }],
|
|
498
|
+
details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, uniqueEnabled },
|
|
499
|
+
};
|
|
500
|
+
} catch (err) {
|
|
501
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
502
|
+
return {
|
|
503
|
+
content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
|
|
504
|
+
isError: true,
|
|
505
|
+
details: { operation: "generate_milestone_id", error: msg },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
470
511
|
// ── session_start: render branded GSD header + load tool keys + remote status ──
|
|
471
512
|
pi.on("session_start", async (_event, ctx) => {
|
|
472
513
|
// Theme access throws in RPC mode (no TUI) — header is decorative, skip it
|
|
@@ -608,12 +649,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
608
649
|
"",
|
|
609
650
|
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
|
|
610
651
|
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
|
|
611
|
-
`The actual current working directory is: ${process.cwd()}`,
|
|
652
|
+
`The actual current working directory is: ${toPosixPath(process.cwd())}`,
|
|
612
653
|
"",
|
|
613
654
|
`You are working inside a GSD worktree.`,
|
|
614
655
|
`- Worktree name: ${worktreeName}`,
|
|
615
|
-
`- Worktree path (this is the real cwd): ${process.cwd()}`,
|
|
616
|
-
`- Main project: ${worktreeMainCwd}`,
|
|
656
|
+
`- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
|
|
657
|
+
`- Main project: ${toPosixPath(worktreeMainCwd)}`,
|
|
617
658
|
`- Branch: worktree/${worktreeName}`,
|
|
618
659
|
"",
|
|
619
660
|
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|
|
@@ -625,12 +666,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
625
666
|
"",
|
|
626
667
|
"[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
|
|
627
668
|
`IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
|
|
628
|
-
`The actual current working directory is: ${process.cwd()}`,
|
|
669
|
+
`The actual current working directory is: ${toPosixPath(process.cwd())}`,
|
|
629
670
|
"",
|
|
630
671
|
"You are working inside a GSD auto-worktree.",
|
|
631
672
|
`- Milestone worktree: ${autoWorktree.worktreeName}`,
|
|
632
|
-
`- Worktree path (this is the real cwd): ${process.cwd()}`,
|
|
633
|
-
`- Main project: ${autoWorktree.originalBase}`,
|
|
673
|
+
`- Worktree path (this is the real cwd): ${toPosixPath(process.cwd())}`,
|
|
674
|
+
`- Main project: ${toPosixPath(autoWorktree.originalBase)}`,
|
|
634
675
|
`- Branch: ${autoWorktree.branch}`,
|
|
635
676
|
"",
|
|
636
677
|
"All file operations, bash commands, and GSD state resolve against the worktree path above.",
|