gsd-pi 2.35.0-dev.640d5c7 → 2.35.0-dev.67d0e02
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 +3 -1
- package/dist/cli.js +7 -2
- package/dist/resource-loader.d.ts +1 -1
- package/dist/resource-loader.js +13 -1
- package/dist/resources/extensions/async-jobs/await-tool.js +0 -2
- package/dist/resources/extensions/async-jobs/job-manager.js +0 -6
- package/dist/resources/extensions/bg-shell/output-formatter.js +1 -19
- package/dist/resources/extensions/bg-shell/process-manager.js +0 -4
- package/dist/resources/extensions/bg-shell/types.js +0 -2
- package/dist/resources/extensions/context7/index.js +5 -0
- package/dist/resources/extensions/get-secrets-from-user.js +2 -30
- package/dist/resources/extensions/google-search/index.js +5 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +43 -1
- package/dist/resources/extensions/gsd/auto-loop.js +10 -1
- package/dist/resources/extensions/gsd/auto-recovery.js +35 -0
- package/dist/resources/extensions/gsd/auto-start.js +35 -2
- package/dist/resources/extensions/gsd/auto.js +59 -4
- package/dist/resources/extensions/gsd/commands-handlers.js +2 -2
- package/dist/resources/extensions/gsd/doctor-environment.js +26 -17
- package/dist/resources/extensions/gsd/files.js +9 -1
- package/dist/resources/extensions/gsd/gitignore.js +54 -7
- package/dist/resources/extensions/gsd/guided-flow.js +1 -1
- package/dist/resources/extensions/gsd/health-widget-core.js +96 -0
- package/dist/resources/extensions/gsd/health-widget.js +97 -46
- package/dist/resources/extensions/gsd/index.js +26 -33
- package/dist/resources/extensions/gsd/migrate-external.js +55 -2
- package/dist/resources/extensions/gsd/milestone-ids.js +3 -2
- package/dist/resources/extensions/gsd/paths.js +74 -7
- package/dist/resources/extensions/gsd/post-unit-hooks.js +4 -1
- package/dist/resources/extensions/gsd/preferences-validation.js +16 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +55 -0
- package/dist/resources/extensions/gsd/session-lock.js +53 -2
- package/dist/resources/extensions/gsd/state.js +2 -1
- package/dist/resources/extensions/gsd/templates/plan.md +8 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +12 -0
- package/dist/resources/extensions/remote-questions/remote-command.js +2 -22
- package/dist/resources/extensions/shared/mod.js +1 -1
- package/dist/resources/extensions/shared/sanitize.js +30 -0
- package/dist/resources/extensions/subagent/index.js +6 -14
- package/package.json +2 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/resource-loader.js +13 -2
- package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/resource-loader.ts +13 -2
- package/src/resources/extensions/async-jobs/await-tool.ts +0 -2
- package/src/resources/extensions/async-jobs/job-manager.ts +0 -7
- package/src/resources/extensions/bg-shell/output-formatter.ts +0 -17
- package/src/resources/extensions/bg-shell/process-manager.ts +0 -4
- package/src/resources/extensions/bg-shell/types.ts +0 -12
- package/src/resources/extensions/context7/index.ts +7 -0
- package/src/resources/extensions/get-secrets-from-user.ts +2 -35
- package/src/resources/extensions/google-search/index.ts +7 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +49 -1
- package/src/resources/extensions/gsd/auto-loop.ts +11 -1
- package/src/resources/extensions/gsd/auto-recovery.ts +39 -0
- package/src/resources/extensions/gsd/auto-start.ts +42 -2
- package/src/resources/extensions/gsd/auto.ts +61 -3
- package/src/resources/extensions/gsd/commands-handlers.ts +2 -2
- package/src/resources/extensions/gsd/doctor-environment.ts +26 -16
- package/src/resources/extensions/gsd/files.ts +10 -1
- package/src/resources/extensions/gsd/gitignore.ts +54 -7
- package/src/resources/extensions/gsd/guided-flow.ts +1 -1
- package/src/resources/extensions/gsd/health-widget-core.ts +129 -0
- package/src/resources/extensions/gsd/health-widget.ts +103 -59
- package/src/resources/extensions/gsd/index.ts +30 -33
- package/src/resources/extensions/gsd/migrate-external.ts +47 -2
- package/src/resources/extensions/gsd/milestone-ids.ts +3 -2
- package/src/resources/extensions/gsd/paths.ts +73 -7
- package/src/resources/extensions/gsd/post-unit-hooks.ts +5 -1
- package/src/resources/extensions/gsd/preferences-validation.ts +16 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +66 -0
- package/src/resources/extensions/gsd/session-lock.ts +59 -2
- package/src/resources/extensions/gsd/state.ts +2 -1
- package/src/resources/extensions/gsd/templates/plan.md +8 -0
- package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +214 -0
- package/src/resources/extensions/gsd/tests/health-widget.test.ts +158 -0
- package/src/resources/extensions/gsd/tests/paths.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/preferences.test.ts +12 -2
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/test-utils.ts +165 -0
- package/src/resources/extensions/gsd/tests/validate-directory.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +7 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +32 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +11 -0
- package/src/resources/extensions/remote-questions/remote-command.ts +2 -23
- package/src/resources/extensions/shared/mod.ts +1 -1
- package/src/resources/extensions/shared/sanitize.ts +36 -0
- package/src/resources/extensions/subagent/index.ts +6 -12
- package/dist/resources/extensions/shared/wizard-ui.js +0 -478
- package/src/resources/extensions/shared/wizard-ui.ts +0 -551
package/README.md
CHANGED
|
@@ -455,7 +455,9 @@ auto_report: true
|
|
|
455
455
|
|
|
456
456
|
### Agent Instructions
|
|
457
457
|
|
|
458
|
-
|
|
458
|
+
Place an `AGENTS.md` file in any directory to provide persistent behavioral guidance for that scope. Pi core loads `AGENTS.md` automatically (with `CLAUDE.md` as a fallback) at both user and project levels. Use these files for coding standards, architectural decisions, domain terminology, or workflow preferences.
|
|
459
|
+
|
|
460
|
+
> **Note:** The legacy `agent-instructions.md` format (`~/.gsd/agent-instructions.md` and `.gsd/agent-instructions.md`) is deprecated and no longer loaded. Migrate any existing instructions to `AGENTS.md` or `CLAUDE.md`.
|
|
459
461
|
|
|
460
462
|
### Debug Mode
|
|
461
463
|
|
package/dist/cli.js
CHANGED
|
@@ -327,7 +327,10 @@ if (isPrintMode) {
|
|
|
327
327
|
markStartup('createAgentSession');
|
|
328
328
|
if (extensionsResult.errors.length > 0) {
|
|
329
329
|
for (const err of extensionsResult.errors) {
|
|
330
|
-
|
|
330
|
+
// Downgrade conflicts with built-in tools to warnings (#1347)
|
|
331
|
+
const isSuperseded = err.error.includes("supersedes");
|
|
332
|
+
const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
|
|
333
|
+
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
|
|
331
334
|
}
|
|
332
335
|
}
|
|
333
336
|
// Apply --model override if specified
|
|
@@ -456,7 +459,9 @@ const { session, extensionsResult } = await createAgentSession({
|
|
|
456
459
|
markStartup('createAgentSession');
|
|
457
460
|
if (extensionsResult.errors.length > 0) {
|
|
458
461
|
for (const err of extensionsResult.errors) {
|
|
459
|
-
|
|
462
|
+
const isSuperseded = err.error.includes("supersedes");
|
|
463
|
+
const prefix = isSuperseded ? "Extension conflict" : "Extension load error";
|
|
464
|
+
process.stderr.write(`[gsd] ${prefix}: ${err.error}\n`);
|
|
460
465
|
}
|
|
461
466
|
}
|
|
462
467
|
// Restore scoped models from settings on startup.
|
|
@@ -9,7 +9,7 @@ export declare function getNewerManagedResourceVersion(agentDir: string, current
|
|
|
9
9
|
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
10
10
|
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
11
11
|
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
12
|
-
* - GSD-WORKFLOW.md
|
|
12
|
+
* - GSD-WORKFLOW.md → ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
|
|
13
13
|
*
|
|
14
14
|
* Skips the copy when the managed-resources.json version matches the current
|
|
15
15
|
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
package/dist/resource-loader.js
CHANGED
|
@@ -18,6 +18,9 @@ import { loadRegistry, readManifestFromEntryPath, isExtensionEnabled, ensureRegi
|
|
|
18
18
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
19
|
const distResources = join(packageRoot, 'dist', 'resources');
|
|
20
20
|
const srcResources = join(packageRoot, 'src', 'resources');
|
|
21
|
+
// Use dist/resources only if it has the full expected structure.
|
|
22
|
+
// A partial build (tsc without copy-resources) creates dist/resources/extensions/
|
|
23
|
+
// but not agents/ or skills/, causing initResources to sync from an incomplete source.
|
|
21
24
|
const resourcesDir = (existsSync(distResources) && existsSync(join(distResources, 'agents')))
|
|
22
25
|
? distResources
|
|
23
26
|
: srcResources;
|
|
@@ -220,7 +223,7 @@ function copyDirRecursive(src, dest) {
|
|
|
220
223
|
* - extensions/ → ~/.gsd/agent/extensions/ (overwrite when version changes)
|
|
221
224
|
* - agents/ → ~/.gsd/agent/agents/ (overwrite when version changes)
|
|
222
225
|
* - skills/ → ~/.gsd/agent/skills/ (overwrite when version changes)
|
|
223
|
-
* - GSD-WORKFLOW.md
|
|
226
|
+
* - GSD-WORKFLOW.md → ~/.gsd/agent/GSD-WORKFLOW.md (fallback for env var miss)
|
|
224
227
|
*
|
|
225
228
|
* Skips the copy when the managed-resources.json version matches the current
|
|
226
229
|
* GSD version, avoiding ~128ms of synchronous cpSync on every startup.
|
|
@@ -247,6 +250,15 @@ export function initResources(agentDir) {
|
|
|
247
250
|
syncResourceDir(bundledExtensionsDir, join(agentDir, 'extensions'));
|
|
248
251
|
syncResourceDir(join(resourcesDir, 'agents'), join(agentDir, 'agents'));
|
|
249
252
|
syncResourceDir(join(resourcesDir, 'skills'), join(agentDir, 'skills'));
|
|
253
|
+
// Sync GSD-WORKFLOW.md to agentDir as a fallback for when GSD_WORKFLOW_PATH
|
|
254
|
+
// env var is not set (e.g. fork/dev builds, alternative entry points).
|
|
255
|
+
const workflowSrc = join(resourcesDir, 'GSD-WORKFLOW.md');
|
|
256
|
+
if (existsSync(workflowSrc)) {
|
|
257
|
+
try {
|
|
258
|
+
copyFileSync(workflowSrc, join(agentDir, 'GSD-WORKFLOW.md'));
|
|
259
|
+
}
|
|
260
|
+
catch { /* non-fatal */ }
|
|
261
|
+
}
|
|
250
262
|
// Ensure all newly copied files are owner-writable so the next run can
|
|
251
263
|
// overwrite them (covers extensions, agents, and skills in one walk).
|
|
252
264
|
makeTreeWritable(agentDir);
|
|
@@ -52,14 +52,12 @@ export function createAwaitTool(getManager) {
|
|
|
52
52
|
const running = watched.filter((j) => j.status === "running");
|
|
53
53
|
if (running.length === 0) {
|
|
54
54
|
const result = formatResults(watched);
|
|
55
|
-
manager.acknowledgeDeliveries(watched.map((j) => j.id));
|
|
56
55
|
return { content: [{ type: "text", text: result }], details: undefined };
|
|
57
56
|
}
|
|
58
57
|
// Wait for at least one to complete
|
|
59
58
|
await Promise.race(running.map((j) => j.promise));
|
|
60
59
|
// Collect all completed results (more may have finished while waiting)
|
|
61
60
|
const completed = watched.filter((j) => j.status !== "running");
|
|
62
|
-
manager.acknowledgeDeliveries(completed.map((j) => j.id));
|
|
63
61
|
const stillRunning = watched.filter((j) => j.status === "running");
|
|
64
62
|
let result = formatResults(completed);
|
|
65
63
|
if (stillRunning.length > 0) {
|
|
@@ -101,12 +101,6 @@ export class AsyncJobManager {
|
|
|
101
101
|
getAllJobs() {
|
|
102
102
|
return [...this.jobs.values()];
|
|
103
103
|
}
|
|
104
|
-
/**
|
|
105
|
-
* No-op. Retained for API compatibility with await_job tool.
|
|
106
|
-
*/
|
|
107
|
-
acknowledgeDeliveries(_jobIds) {
|
|
108
|
-
// Delivery is fire-once; no retries to cancel.
|
|
109
|
-
}
|
|
110
104
|
/**
|
|
111
105
|
* Cleanup all timers and resources.
|
|
112
106
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Output analysis, digest generation, highlights extraction, and output retrieval.
|
|
3
3
|
*/
|
|
4
4
|
import { truncateHead, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, } from "@gsd/pi-coding-agent";
|
|
5
|
-
import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE,
|
|
5
|
+
import { ERROR_PATTERN_UNION, WARNING_PATTERN_UNION, READINESS_PATTERN_UNION, BUILD_COMPLETE_PATTERN_UNION, TEST_RESULT_PATTERN_UNION, URL_PATTERN, PORT_PATTERN_SOURCE, } from "./types.js";
|
|
6
6
|
import { addEvent, pushAlert } from "./process-manager.js";
|
|
7
7
|
import { transitionToReady } from "./readiness-detector.js";
|
|
8
8
|
import { formatUptime, formatTimeAgo } from "./utilities.js";
|
|
@@ -78,24 +78,6 @@ export function analyzeLine(bg, line, stream) {
|
|
|
78
78
|
pushAlert(bg, "recovered — errors cleared");
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
-
// Dedup tracking — evict oldest entry when map exceeds LINE_DEDUP_MAX (LRU via Map insertion order)
|
|
82
|
-
bg.totalRawLines++;
|
|
83
|
-
const lineHash = line.trim().slice(0, 100);
|
|
84
|
-
const existing = bg.lineDedup.get(lineHash);
|
|
85
|
-
if (existing !== undefined) {
|
|
86
|
-
// Re-insert to update insertion order (move to tail = most recent)
|
|
87
|
-
bg.lineDedup.delete(lineHash);
|
|
88
|
-
bg.lineDedup.set(lineHash, existing + 1);
|
|
89
|
-
}
|
|
90
|
-
else {
|
|
91
|
-
if (bg.lineDedup.size >= LINE_DEDUP_MAX) {
|
|
92
|
-
// Evict oldest entry (Map iteration order = insertion order = LRU at head)
|
|
93
|
-
const oldest = bg.lineDedup.keys().next().value;
|
|
94
|
-
if (oldest !== undefined)
|
|
95
|
-
bg.lineDedup.delete(oldest);
|
|
96
|
-
}
|
|
97
|
-
bg.lineDedup.set(lineHash, 1);
|
|
98
|
-
}
|
|
99
81
|
}
|
|
100
82
|
// ── Digest Generation ──────────────────────────────────────────────────────
|
|
101
83
|
export function generateDigest(bg, mutate = false) {
|
|
@@ -135,12 +135,8 @@ export function startProcess(opts) {
|
|
|
135
135
|
group: opts.group || null,
|
|
136
136
|
lastErrorCount: 0,
|
|
137
137
|
lastWarningCount: 0,
|
|
138
|
-
commandHistory: [],
|
|
139
|
-
lineDedup: new Map(),
|
|
140
|
-
totalRawLines: 0,
|
|
141
138
|
stdoutLineCount: 0,
|
|
142
139
|
stderrLineCount: 0,
|
|
143
|
-
envKeys: Object.keys(opts.env || {}),
|
|
144
140
|
restartCount: 0,
|
|
145
141
|
startConfig: {
|
|
146
142
|
command,
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
export const MAX_BUFFER_LINES = 5000;
|
|
6
6
|
export const MAX_EVENTS = 200;
|
|
7
7
|
export const DEAD_PROCESS_TTL = 10 * 60 * 1000;
|
|
8
|
-
/** Maximum unique entries in the per-process lineDedup Map before LRU eviction. */
|
|
9
|
-
export const LINE_DEDUP_MAX = 500;
|
|
10
8
|
export const PORT_PROBE_TIMEOUT = 500;
|
|
11
9
|
export const READY_POLL_INTERVAL = 250;
|
|
12
10
|
export const DEFAULT_READY_TIMEOUT = 30000;
|
|
@@ -327,6 +327,11 @@ export default function (pi) {
|
|
|
327
327
|
return new Text(text, 0, 0);
|
|
328
328
|
},
|
|
329
329
|
});
|
|
330
|
+
// ── Session cleanup ─────────────────────────────────────────────────────
|
|
331
|
+
pi.on("session_shutdown", async () => {
|
|
332
|
+
searchCache.clear();
|
|
333
|
+
docCache.clear();
|
|
334
|
+
});
|
|
330
335
|
// ── Startup notification ─────────────────────────────────────────────────
|
|
331
336
|
pi.on("session_start", async (_event, ctx) => {
|
|
332
337
|
if (!getApiKey()) {
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
import { readFile, writeFile } from "node:fs/promises";
|
|
9
9
|
import { existsSync, statSync } from "node:fs";
|
|
10
10
|
import { resolve } from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { Editor, Key, matchesKey, Text, truncateToWidth, wrapTextWithAnsi } from "@gsd/pi-tui";
|
|
12
12
|
import { Type } from "@sinclair/typebox";
|
|
13
|
-
import { makeUI } from "./shared/mod.js";
|
|
13
|
+
import { makeUI, maskEditorLine } from "./shared/mod.js";
|
|
14
14
|
import { parseSecretsManifest, formatSecretsManifest } from "./gsd/files.js";
|
|
15
15
|
import { resolveMilestoneFile } from "./gsd/paths.js";
|
|
16
16
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
@@ -21,34 +21,6 @@ function maskPreview(value) {
|
|
|
21
21
|
return "*".repeat(value.length);
|
|
22
22
|
return `${value.slice(0, 4)}${"*".repeat(Math.max(4, value.length - 8))}${value.slice(-4)}`;
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Replace editor visible text with masked characters while preserving ANSI cursor/sequencer codes.
|
|
26
|
-
*/
|
|
27
|
-
function maskEditorLine(line) {
|
|
28
|
-
// Keep border / metadata lines readable.
|
|
29
|
-
if (line.startsWith("─")) {
|
|
30
|
-
return line;
|
|
31
|
-
}
|
|
32
|
-
let output = "";
|
|
33
|
-
let i = 0;
|
|
34
|
-
while (i < line.length) {
|
|
35
|
-
if (line.startsWith(CURSOR_MARKER, i)) {
|
|
36
|
-
output += CURSOR_MARKER;
|
|
37
|
-
i += CURSOR_MARKER.length;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const ansiMatch = /^\x1b\[[0-9;]*m/.exec(line.slice(i));
|
|
41
|
-
if (ansiMatch) {
|
|
42
|
-
output += ansiMatch[0];
|
|
43
|
-
i += ansiMatch[0].length;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
const ch = line[i];
|
|
47
|
-
output += ch === " " ? " " : "*";
|
|
48
|
-
i += 1;
|
|
49
|
-
}
|
|
50
|
-
return output;
|
|
51
|
-
}
|
|
52
24
|
function shellEscapeSingle(value) {
|
|
53
25
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
54
26
|
}
|
|
@@ -326,6 +326,11 @@ export default function (pi) {
|
|
|
326
326
|
return new Text(text, 0, 0);
|
|
327
327
|
},
|
|
328
328
|
});
|
|
329
|
+
// ── Session cleanup ─────────────────────────────────────────────────────
|
|
330
|
+
pi.on("session_shutdown", async () => {
|
|
331
|
+
resultCache.clear();
|
|
332
|
+
client = null;
|
|
333
|
+
});
|
|
329
334
|
// ── Startup notification ─────────────────────────────────────────────────
|
|
330
335
|
pi.on("session_start", async (_event, ctx) => {
|
|
331
336
|
if (process.env.GEMINI_API_KEY)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* data structure that is inspectable, testable per-rule, and extensible
|
|
9
9
|
* without modifying orchestration code.
|
|
10
10
|
*/
|
|
11
|
-
import { loadFile, loadActiveOverrides } from "./files.js";
|
|
11
|
+
import { loadFile, loadActiveOverrides, parseRoadmap } from "./files.js";
|
|
12
12
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveTaskFile, relSliceFile, buildMilestoneFileName, } from "./paths.js";
|
|
13
13
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
@@ -274,6 +274,28 @@ const DISPATCH_RULES = [
|
|
|
274
274
|
match: async ({ state, mid, midTitle, basePath, prefs }) => {
|
|
275
275
|
if (state.phase !== "validating-milestone")
|
|
276
276
|
return null;
|
|
277
|
+
// Safety guard (#1368): verify all roadmap slices have SUMMARY files before
|
|
278
|
+
// allowing milestone validation. If any slice lacks a summary, the milestone
|
|
279
|
+
// is not genuinely complete — something skipped earlier slices.
|
|
280
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
281
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
282
|
+
if (roadmapContent) {
|
|
283
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
284
|
+
const missingSlices = [];
|
|
285
|
+
for (const slice of roadmap.slices) {
|
|
286
|
+
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
287
|
+
if (!summaryPath || !existsSync(summaryPath)) {
|
|
288
|
+
missingSlices.push(slice.id);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (missingSlices.length > 0) {
|
|
292
|
+
return {
|
|
293
|
+
action: "stop",
|
|
294
|
+
reason: `Cannot validate milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. These slices may have been skipped.`,
|
|
295
|
+
level: "error",
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
277
299
|
// Skip preference: write a minimal pass-through VALIDATION file
|
|
278
300
|
if (prefs?.phases?.skip_milestone_validation) {
|
|
279
301
|
const mDir = resolveMilestonePath(basePath, mid);
|
|
@@ -308,6 +330,26 @@ const DISPATCH_RULES = [
|
|
|
308
330
|
match: async ({ state, mid, midTitle, basePath }) => {
|
|
309
331
|
if (state.phase !== "completing-milestone")
|
|
310
332
|
return null;
|
|
333
|
+
// Safety guard (#1368): verify all roadmap slices have SUMMARY files.
|
|
334
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
335
|
+
const roadmapContent = roadmapFile ? await loadFile(roadmapFile) : null;
|
|
336
|
+
if (roadmapContent) {
|
|
337
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
338
|
+
const missingSlices = [];
|
|
339
|
+
for (const slice of roadmap.slices) {
|
|
340
|
+
const summaryPath = resolveSliceFile(basePath, mid, slice.id, "SUMMARY");
|
|
341
|
+
if (!summaryPath || !existsSync(summaryPath)) {
|
|
342
|
+
missingSlices.push(slice.id);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (missingSlices.length > 0) {
|
|
346
|
+
return {
|
|
347
|
+
action: "stop",
|
|
348
|
+
reason: `Cannot complete milestone ${mid}: slices ${missingSlices.join(", ")} are missing SUMMARY files. Run /gsd doctor to diagnose.`,
|
|
349
|
+
level: "error",
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
311
353
|
return {
|
|
312
354
|
action: "dispatch",
|
|
313
355
|
unitType: "complete-milestone",
|
|
@@ -161,6 +161,15 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt, _prefs) {
|
|
|
161
161
|
const unitPromise = new Promise((resolve) => {
|
|
162
162
|
s.pendingResolve = resolve;
|
|
163
163
|
});
|
|
164
|
+
// Ensure cwd matches basePath before dispatch (#1389).
|
|
165
|
+
// async_bash and background jobs can drift cwd away from the worktree.
|
|
166
|
+
// Realigning here prevents commits from landing on the wrong branch.
|
|
167
|
+
try {
|
|
168
|
+
if (process.cwd() !== s.basePath) {
|
|
169
|
+
process.chdir(s.basePath);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
catch { /* non-fatal — chdir may fail if dir was removed */ }
|
|
164
173
|
// ── Send the prompt ──
|
|
165
174
|
debugLog("runUnit", { phase: "send-message", unitType, unitId });
|
|
166
175
|
pi.sendMessage({ customType: "gsd-auto", content: prompt, display: s.verbose }, { triggerTurn: true });
|
|
@@ -498,7 +507,7 @@ export async function autoLoop(ctx, pi, s, deps) {
|
|
|
498
507
|
}
|
|
499
508
|
// Secrets re-check gate
|
|
500
509
|
try {
|
|
501
|
-
const manifestStatus = await deps.getManifestStatus(s.basePath, mid);
|
|
510
|
+
const manifestStatus = await deps.getManifestStatus(s.basePath, mid, s.originalBasePath);
|
|
502
511
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
503
512
|
const result = await deps.collectSecretsFromManifest(s.basePath, mid, ctx);
|
|
504
513
|
if (result &&
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* Pure functions that receive all needed state as parameters — no module-level
|
|
7
7
|
* globals or AutoContext dependency.
|
|
8
8
|
*/
|
|
9
|
+
import { parseUnitId } from "./unit-id.js";
|
|
9
10
|
import { clearUnitRuntimeRecord } from "./unit-runtime.js";
|
|
10
11
|
import { clearParseCache, parseRoadmap, parsePlan } from "./files.js";
|
|
11
12
|
import { isValidationTerminal } from "./state.js";
|
|
12
13
|
import { nativeConflictFiles, nativeCommit, nativeCheckoutTheirs, nativeAddPaths, nativeMergeAbort, nativeResetHard, } from "./native-git-bridge.js";
|
|
13
14
|
import { resolveMilestonePath, resolveSlicePath, resolveSliceFile, resolveTasksDir, relMilestoneFile, relSliceFile, relSlicePath, relTaskFile, buildMilestoneFileName, buildSliceFileName, buildTaskFileName, resolveMilestoneFile, clearPathCache, resolveGsdRootFile, } from "./paths.js";
|
|
15
|
+
import { markSliceDoneInRoadmap } from "./roadmap-mutations.js";
|
|
14
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync, } from "node:fs";
|
|
15
17
|
import { dirname, join } from "node:path";
|
|
16
18
|
// ─── Artifact Resolution & Verification ───────────────────────────────────────
|
|
@@ -432,6 +434,39 @@ export async function selfHealRuntimeRecords(base, ctx) {
|
|
|
432
434
|
const now = Date.now();
|
|
433
435
|
for (const record of records) {
|
|
434
436
|
const { unitType, unitId } = record;
|
|
437
|
+
// Case 0: complete-slice with SUMMARY + UAT but unchecked roadmap (#1350).
|
|
438
|
+
// If a complete-slice was interrupted after writing artifacts but before
|
|
439
|
+
// flipping the roadmap checkbox, the verification fails and the dispatch
|
|
440
|
+
// loop relaunches the same unit forever. Auto-fix the checkbox.
|
|
441
|
+
if (unitType === "complete-slice") {
|
|
442
|
+
const { milestone: mid, slice: sid } = parseUnitId(unitId);
|
|
443
|
+
if (mid && sid) {
|
|
444
|
+
const dir = resolveSlicePath(base, mid, sid);
|
|
445
|
+
if (dir) {
|
|
446
|
+
const summaryPath = join(dir, buildSliceFileName(sid, "SUMMARY"));
|
|
447
|
+
const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
|
|
448
|
+
if (existsSync(summaryPath) && existsSync(uatPath)) {
|
|
449
|
+
const roadmapFile = resolveMilestoneFile(base, mid, "ROADMAP");
|
|
450
|
+
if (roadmapFile && existsSync(roadmapFile)) {
|
|
451
|
+
try {
|
|
452
|
+
const roadmapContent = readFileSync(roadmapFile, "utf-8");
|
|
453
|
+
const roadmap = parseRoadmap(roadmapContent);
|
|
454
|
+
const slice = (roadmap.slices ?? []).find(s => s.id === sid);
|
|
455
|
+
if (slice && !slice.done) {
|
|
456
|
+
// Auto-fix: flip the checkbox using shared utility
|
|
457
|
+
if (markSliceDoneInRoadmap(base, mid, sid)) {
|
|
458
|
+
ctx.ui.notify(`Self-heal: marked ${sid} done in roadmap (SUMMARY + UAT exist but checkbox was stale).`, "info");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
// Roadmap parse failure — don't block self-heal
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
435
470
|
// Clear stale dispatched records (dispatched > 1h ago, process crashed)
|
|
436
471
|
const age = now - (record.startedAt ?? 0);
|
|
437
472
|
if (record.phase === "dispatched" && age > STALE_THRESHOLD_MS) {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { deriveState } from "./state.js";
|
|
12
12
|
import { loadFile, getManifestStatus } from "./files.js";
|
|
13
13
|
import { loadEffectiveGSDPreferences, resolveSkillDiscoveryMode, getIsolationMode, } from "./preferences.js";
|
|
14
|
+
import { ensureGsdSymlink } from "./repo-identity.js";
|
|
15
|
+
import { migrateToExternalState, recoverFailedMigration } from "./migrate-external.js";
|
|
14
16
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
15
17
|
import { gsdRoot, resolveMilestoneFile } from "./paths.js";
|
|
16
18
|
import { invalidateAllCaches } from "./cache.js";
|
|
@@ -42,6 +44,12 @@ import { sep as pathSep } from "node:path";
|
|
|
42
44
|
* Returns false if the bootstrap aborted (e.g., guided flow returned,
|
|
43
45
|
* concurrent session detected). Returns true when ready to dispatch.
|
|
44
46
|
*/
|
|
47
|
+
/** Guard: tracks consecutive bootstrap attempts that found phase === "complete".
|
|
48
|
+
* Prevents the recursive dialog loop described in #1348 where
|
|
49
|
+
* bootstrapAutoSession → showSmartEntry → checkAutoStartAfterDiscuss → startAuto
|
|
50
|
+
* cycles indefinitely when the discuss workflow doesn't produce a milestone. */
|
|
51
|
+
let _consecutiveCompleteBootstraps = 0;
|
|
52
|
+
const MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS = 2;
|
|
45
53
|
export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, requestedStepMode, deps) {
|
|
46
54
|
const { shouldUseWorktreeIsolation, registerSigtermHandler, lockBase, buildResolver, } = deps;
|
|
47
55
|
const lockResult = acquireSessionLock(base);
|
|
@@ -60,7 +68,19 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
60
68
|
const mainBranch = loadEffectiveGSDPreferences()?.preferences?.git?.main_branch || "main";
|
|
61
69
|
nativeInit(base, mainBranch);
|
|
62
70
|
}
|
|
63
|
-
//
|
|
71
|
+
// Migrate legacy in-project .gsd/ to external state directory.
|
|
72
|
+
// Migration MUST run before ensureGitignore to avoid adding ".gsd" to
|
|
73
|
+
// .gitignore when .gsd/ is git-tracked (data-loss bug #1364).
|
|
74
|
+
recoverFailedMigration(base);
|
|
75
|
+
const migration = migrateToExternalState(base);
|
|
76
|
+
if (migration.error) {
|
|
77
|
+
ctx.ui.notify(`External state migration warning: ${migration.error}`, "warning");
|
|
78
|
+
}
|
|
79
|
+
// Ensure symlink exists (handles fresh projects and post-migration)
|
|
80
|
+
ensureGsdSymlink(base);
|
|
81
|
+
// Ensure .gitignore has baseline patterns.
|
|
82
|
+
// ensureGitignore checks for git-tracked .gsd/ files and skips the
|
|
83
|
+
// ".gsd" pattern if the project intentionally tracks .gsd/ in git.
|
|
64
84
|
const gitPrefs = loadEffectiveGSDPreferences()?.preferences?.git;
|
|
65
85
|
const commitDocs = gitPrefs?.commit_docs;
|
|
66
86
|
const manageGitignore = gitPrefs?.manage_gitignore;
|
|
@@ -186,6 +206,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
186
206
|
if (!hasSurvivorBranch) {
|
|
187
207
|
// No active work — start a new milestone via discuss flow
|
|
188
208
|
if (!state.activeMilestone || state.phase === "complete") {
|
|
209
|
+
// Guard against recursive dialog loop (#1348):
|
|
210
|
+
// If we've entered this branch multiple times in quick succession,
|
|
211
|
+
// the discuss workflow isn't producing a milestone. Break the cycle.
|
|
212
|
+
_consecutiveCompleteBootstraps++;
|
|
213
|
+
if (_consecutiveCompleteBootstraps > MAX_CONSECUTIVE_COMPLETE_BOOTSTRAPS) {
|
|
214
|
+
_consecutiveCompleteBootstraps = 0;
|
|
215
|
+
ctx.ui.notify("All milestones are complete and the discussion didn't produce a new one. " +
|
|
216
|
+
"Run /gsd to start a new milestone manually.", "warning");
|
|
217
|
+
return releaseLockAndReturn();
|
|
218
|
+
}
|
|
189
219
|
const { showSmartEntry } = await import("./guided-flow.js");
|
|
190
220
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
191
221
|
invalidateAllCaches();
|
|
@@ -193,6 +223,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
193
223
|
if (postState.activeMilestone &&
|
|
194
224
|
postState.phase !== "complete" &&
|
|
195
225
|
postState.phase !== "pre-planning") {
|
|
226
|
+
_consecutiveCompleteBootstraps = 0; // Successfully advanced past "complete"
|
|
196
227
|
state = postState;
|
|
197
228
|
}
|
|
198
229
|
else if (postState.activeMilestone &&
|
|
@@ -237,6 +268,8 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
237
268
|
await showSmartEntry(ctx, pi, base, { step: requestedStepMode });
|
|
238
269
|
return releaseLockAndReturn();
|
|
239
270
|
}
|
|
271
|
+
// Successfully resolved an active milestone — reset the re-entry guard
|
|
272
|
+
_consecutiveCompleteBootstraps = 0;
|
|
240
273
|
// ── Initialize session state ──
|
|
241
274
|
s.active = true;
|
|
242
275
|
s.stepMode = requestedStepMode;
|
|
@@ -345,7 +378,7 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
|
|
|
345
378
|
// Secrets collection gate
|
|
346
379
|
const mid = state.activeMilestone.id;
|
|
347
380
|
try {
|
|
348
|
-
const manifestStatus = await getManifestStatus(base, mid);
|
|
381
|
+
const manifestStatus = await getManifestStatus(base, mid, s.originalBasePath || base);
|
|
349
382
|
if (manifestStatus && manifestStatus.pending.length > 0) {
|
|
350
383
|
const result = await collectSecretsFromManifest(base, mid, ctx);
|
|
351
384
|
if (result &&
|
|
@@ -36,7 +36,7 @@ import { clearSkillSnapshot } from "./skill-discovery.js";
|
|
|
36
36
|
import { captureAvailableSkills, resetSkillTelemetry, } from "./skill-telemetry.js";
|
|
37
37
|
import { initMetrics, resetMetrics, getLedger, getProjectTotals, formatCost, formatTokenCount, } from "./metrics.js";
|
|
38
38
|
import { join } from "node:path";
|
|
39
|
-
import { readFileSync, existsSync, mkdirSync } from "node:fs";
|
|
39
|
+
import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from "node:fs";
|
|
40
40
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
41
41
|
import { autoCommitCurrentBranch, captureIntegrationBranch, detectWorktreeName, getCurrentBranch, getMainBranch, setActiveMilestoneId, } from "./worktree.js";
|
|
42
42
|
import { GitServiceImpl } from "./git-service.js";
|
|
@@ -325,6 +325,13 @@ export async function stopAuto(ctx, pi, reason) {
|
|
|
325
325
|
resetHookState();
|
|
326
326
|
if (s.basePath)
|
|
327
327
|
clearPersistedHookState(s.basePath);
|
|
328
|
+
// Remove paused-session metadata if present (#1383)
|
|
329
|
+
try {
|
|
330
|
+
const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
|
|
331
|
+
if (existsSync(pausedPath))
|
|
332
|
+
unlinkSync(pausedPath);
|
|
333
|
+
}
|
|
334
|
+
catch { /* non-fatal */ }
|
|
328
335
|
s.active = false;
|
|
329
336
|
s.paused = false;
|
|
330
337
|
s.stepMode = false;
|
|
@@ -369,10 +376,28 @@ export async function pauseAuto(ctx, _pi) {
|
|
|
369
376
|
return;
|
|
370
377
|
clearUnitTimeout();
|
|
371
378
|
s.pausedSessionFile = ctx?.sessionManager?.getSessionFile() ?? null;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
379
|
+
// Persist paused-session metadata so resume survives /exit (#1383).
|
|
380
|
+
// The fresh-start bootstrap checks for this file and restores worktree context.
|
|
381
|
+
try {
|
|
382
|
+
const pausedMeta = {
|
|
383
|
+
milestoneId: s.currentMilestoneId,
|
|
384
|
+
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
|
|
385
|
+
originalBasePath: s.originalBasePath,
|
|
386
|
+
stepMode: s.stepMode,
|
|
387
|
+
pausedAt: new Date().toISOString(),
|
|
388
|
+
sessionFile: s.pausedSessionFile,
|
|
389
|
+
};
|
|
390
|
+
const runtimeDir = join(gsdRoot(s.originalBasePath || s.basePath), "runtime");
|
|
391
|
+
mkdirSync(runtimeDir, { recursive: true });
|
|
392
|
+
writeFileSync(join(runtimeDir, "paused-session.json"), JSON.stringify(pausedMeta, null, 2), "utf-8");
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Non-fatal — resume will still work via full bootstrap, just without worktree context
|
|
396
|
+
}
|
|
397
|
+
if (lockBase()) {
|
|
375
398
|
releaseSessionLock(lockBase());
|
|
399
|
+
clearLock(lockBase());
|
|
400
|
+
}
|
|
376
401
|
deregisterSigtermHandler();
|
|
377
402
|
s.active = false;
|
|
378
403
|
s.paused = true;
|
|
@@ -520,6 +545,30 @@ export async function startAuto(ctx, pi, base, verboseMode, options) {
|
|
|
520
545
|
// Escape stale worktree cwd from a previous milestone (#608).
|
|
521
546
|
base = escapeStaleWorktree(base);
|
|
522
547
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
548
|
+
// Check persisted paused-session first (#1383) — survives /exit.
|
|
549
|
+
if (!s.paused) {
|
|
550
|
+
try {
|
|
551
|
+
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
552
|
+
if (existsSync(pausedPath)) {
|
|
553
|
+
const meta = JSON.parse(readFileSync(pausedPath, "utf-8"));
|
|
554
|
+
if (meta.milestoneId) {
|
|
555
|
+
s.currentMilestoneId = meta.milestoneId;
|
|
556
|
+
s.originalBasePath = meta.originalBasePath || base;
|
|
557
|
+
s.stepMode = meta.stepMode ?? requestedStepMode;
|
|
558
|
+
s.paused = true;
|
|
559
|
+
// Clean up the persisted file — we're consuming it
|
|
560
|
+
try {
|
|
561
|
+
unlinkSync(pausedPath);
|
|
562
|
+
}
|
|
563
|
+
catch { /* non-fatal */ }
|
|
564
|
+
ctx.ui.notify(`Resuming paused session for ${meta.milestoneId}${meta.worktreePath ? ` (worktree)` : ""}.`, "info");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch {
|
|
569
|
+
// Malformed or missing — proceed with fresh bootstrap
|
|
570
|
+
}
|
|
571
|
+
}
|
|
523
572
|
if (s.paused) {
|
|
524
573
|
const resumeLock = acquireSessionLock(base);
|
|
525
574
|
if (!resumeLock.acquired) {
|
|
@@ -754,6 +803,12 @@ export async function dispatchHookUnit(ctx, pi, hookName, triggerUnitType, trigg
|
|
|
754
803
|
}, hookHardTimeoutMs);
|
|
755
804
|
ctx.ui.setStatus("gsd-auto", s.stepMode ? "next" : "auto");
|
|
756
805
|
ctx.ui.notify(`Running post-unit hook: ${hookName}`, "info");
|
|
806
|
+
// Ensure cwd matches basePath before hook dispatch (#1389)
|
|
807
|
+
try {
|
|
808
|
+
if (process.cwd() !== s.basePath)
|
|
809
|
+
process.chdir(s.basePath);
|
|
810
|
+
}
|
|
811
|
+
catch { }
|
|
757
812
|
debugLog("dispatchHookUnit", {
|
|
758
813
|
phase: "send-message",
|
|
759
814
|
promptLength: hookPrompt.length,
|
|
@@ -15,7 +15,7 @@ import { isAutoActive } from "./auto.js";
|
|
|
15
15
|
import { projectRoot } from "./commands.js";
|
|
16
16
|
import { loadPrompt } from "./prompt-loader.js";
|
|
17
17
|
export function dispatchDoctorHeal(pi, scope, reportText, structuredIssues) {
|
|
18
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
18
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
19
19
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
20
20
|
const prompt = loadPrompt("doctor-heal", {
|
|
21
21
|
doctorSummary: reportText,
|
|
@@ -144,7 +144,7 @@ export async function handleTriage(ctx, pi, basePath) {
|
|
|
144
144
|
currentPlan: currentPlan || "(no active slice plan)",
|
|
145
145
|
roadmapContext: roadmapContext || "(no active roadmap)",
|
|
146
146
|
});
|
|
147
|
-
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".
|
|
147
|
+
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
148
148
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
149
149
|
pi.sendMessage({
|
|
150
150
|
customType: "gsd-triage",
|