gsd-pi 2.24.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 +13 -3
- package/dist/headless.js +24 -4
- package/dist/models-resolver.d.ts +0 -11
- package/dist/models-resolver.js +0 -15
- package/dist/resource-loader.d.ts +0 -1
- package/dist/resource-loader.js +0 -9
- package/dist/resources/GSD-WORKFLOW.md +12 -9
- package/dist/resources/extensions/async-jobs/index.ts +9 -1
- package/dist/resources/extensions/bg-shell/index.ts +3 -2
- package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
- package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
- package/dist/resources/extensions/gsd/activity-log.ts +5 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/dist/resources/extensions/gsd/auto-worktree.ts +132 -3
- package/dist/resources/extensions/gsd/auto.ts +265 -48
- package/dist/resources/extensions/gsd/cache.ts +3 -1
- package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/dist/resources/extensions/gsd/doctor.ts +26 -1
- package/dist/resources/extensions/gsd/files.ts +13 -2
- package/dist/resources/extensions/gsd/git-service.ts +74 -14
- package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
- package/dist/resources/extensions/gsd/guided-flow.ts +54 -22
- package/dist/resources/extensions/gsd/index.ts +62 -8
- package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/dist/resources/extensions/gsd/memory-store.ts +441 -0
- package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
- 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/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
- package/dist/resources/extensions/gsd/prompts/discuss.md +5 -5
- package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -3
- package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- 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/auto-recovery.test.ts +54 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- 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/git-service.test.ts +70 -4
- package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- 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/smart-entry-draft.test.ts +1 -1
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
- package/dist/resources/extensions/gsd/types.ts +2 -0
- package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/dist/resources/extensions/gsd/worktree.ts +9 -2
- package/dist/resources/extensions/search-the-web/native-search.ts +19 -5
- package/dist/resources/extensions/shared/path-display.ts +19 -0
- package/package.json +1 -6
- package/packages/pi-agent-core/dist/agent-loop.js +2 -0
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +2 -0
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +64 -0
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/mistral.js +3 -0
- package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +23 -1
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +65 -1
- package/packages/pi-ai/src/providers/mistral.ts +3 -0
- package/packages/pi-ai/src/types.ts +19 -1
- 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/components/tool-execution.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -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 +135 -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/components/tool-execution.ts +18 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +124 -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/GSD-WORKFLOW.md +12 -9
- package/src/resources/extensions/async-jobs/index.ts +9 -1
- package/src/resources/extensions/bg-shell/index.ts +3 -2
- package/src/resources/extensions/bg-shell/overlay.ts +18 -17
- package/src/resources/extensions/get-secrets-from-user.ts +5 -23
- package/src/resources/extensions/gsd/activity-log.ts +5 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
- package/src/resources/extensions/gsd/auto-worktree.ts +132 -3
- package/src/resources/extensions/gsd/auto.ts +265 -48
- package/src/resources/extensions/gsd/cache.ts +3 -1
- package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
- package/src/resources/extensions/gsd/doctor.ts +26 -1
- package/src/resources/extensions/gsd/files.ts +13 -2
- package/src/resources/extensions/gsd/git-service.ts +74 -14
- package/src/resources/extensions/gsd/gsd-db.ts +78 -1
- package/src/resources/extensions/gsd/guided-flow.ts +54 -22
- package/src/resources/extensions/gsd/index.ts +62 -8
- package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
- package/src/resources/extensions/gsd/memory-store.ts +441 -0
- package/src/resources/extensions/gsd/migrate/command.ts +2 -2
- 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/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
- package/src/resources/extensions/gsd/prompts/discuss.md +5 -5
- package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +3 -3
- package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
- 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/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- 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/git-service.test.ts +70 -4
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
- 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/smart-entry-draft.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
- package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
- package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
- package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
- package/src/resources/extensions/gsd/triage-ui.ts +1 -1
- package/src/resources/extensions/gsd/types.ts +2 -0
- package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
- package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
- package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
- package/src/resources/extensions/gsd/worktree.ts +9 -2
- package/src/resources/extensions/search-the-web/native-search.ts +19 -5
- package/src/resources/extensions/shared/path-display.ts +19 -0
|
@@ -16,9 +16,9 @@ import type {
|
|
|
16
16
|
ExtensionCommandContext,
|
|
17
17
|
} from "@gsd/pi-coding-agent";
|
|
18
18
|
|
|
19
|
-
import { deriveState
|
|
19
|
+
import { deriveState } from "./state.js";
|
|
20
20
|
import type { BudgetEnforcementMode, GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides } from "./files.js";
|
|
21
|
+
import { loadFile, parseRoadmap, getManifestStatus, resolveAllOverrides, parseSummary } from "./files.js";
|
|
22
22
|
import { loadPrompt } from "./prompt-loader.js";
|
|
23
23
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
24
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
@@ -94,7 +94,7 @@ import {
|
|
|
94
94
|
parseSliceBranch,
|
|
95
95
|
setActiveMilestoneId,
|
|
96
96
|
} from "./worktree.js";
|
|
97
|
-
import { GitServiceImpl } from "./git-service.js";
|
|
97
|
+
import { GitServiceImpl, type TaskCommitContext } from "./git-service.js";
|
|
98
98
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
99
99
|
import { formatGitError } from "./git-self-heal.js";
|
|
100
100
|
import {
|
|
@@ -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;
|
|
@@ -889,23 +926,37 @@ export async function startAuto(
|
|
|
889
926
|
);
|
|
890
927
|
return;
|
|
891
928
|
}
|
|
892
|
-
// Stale lock from a dead process —
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
929
|
+
// Stale lock from a dead process — validate before synthesizing recovery context.
|
|
930
|
+
// If the recovered unit belongs to a fully-completed milestone (SUMMARY exists),
|
|
931
|
+
// discard recovery context to prevent phantom skip loops (#790).
|
|
932
|
+
const recoveredMid = crashLock.unitId.split("/")[0];
|
|
933
|
+
const milestoneAlreadyComplete = recoveredMid
|
|
934
|
+
? !!resolveMilestoneFile(base, recoveredMid, "SUMMARY")
|
|
935
|
+
: false;
|
|
936
|
+
|
|
937
|
+
if (milestoneAlreadyComplete) {
|
|
900
938
|
ctx.ui.notify(
|
|
901
|
-
|
|
902
|
-
"
|
|
939
|
+
`Crash recovery: discarding stale context for ${crashLock.unitId} — milestone ${recoveredMid} is already complete.`,
|
|
940
|
+
"info",
|
|
903
941
|
);
|
|
904
942
|
} else {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
943
|
+
const activityDir = join(gsdRoot(base), "activity");
|
|
944
|
+
const recovery = synthesizeCrashRecovery(
|
|
945
|
+
base, crashLock.unitType, crashLock.unitId,
|
|
946
|
+
crashLock.sessionFile, activityDir,
|
|
908
947
|
);
|
|
948
|
+
if (recovery && recovery.trace.toolCallCount > 0) {
|
|
949
|
+
pendingCrashRecovery = recovery.prompt;
|
|
950
|
+
ctx.ui.notify(
|
|
951
|
+
`${formatCrashInfo(crashLock)}\nRecovered ${recovery.trace.toolCallCount} tool calls from crashed session. Resuming with full context.`,
|
|
952
|
+
"warning",
|
|
953
|
+
);
|
|
954
|
+
} else {
|
|
955
|
+
ctx.ui.notify(
|
|
956
|
+
`${formatCrashInfo(crashLock)}\nNo session data recovered. Resuming from disk state.`,
|
|
957
|
+
"warning",
|
|
958
|
+
);
|
|
959
|
+
}
|
|
909
960
|
}
|
|
910
961
|
clearLock(base);
|
|
911
962
|
}
|
|
@@ -928,6 +979,11 @@ export async function startAuto(
|
|
|
928
979
|
ctx.ui.notify(`Debug logging enabled → ${getDebugLogPath()}`, "info");
|
|
929
980
|
}
|
|
930
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();
|
|
931
987
|
let state = await deriveState(base);
|
|
932
988
|
|
|
933
989
|
// ── Stale worktree state recovery (#654) ─────────────────────────────────
|
|
@@ -1061,7 +1117,7 @@ export async function startAuto(
|
|
|
1061
1117
|
restoreHookState(base);
|
|
1062
1118
|
resetProactiveHealing();
|
|
1063
1119
|
autoStartTime = Date.now();
|
|
1064
|
-
|
|
1120
|
+
resourceVersionOnStart = readResourceVersion();
|
|
1065
1121
|
completedUnits = [];
|
|
1066
1122
|
pendingQuickTasks = [];
|
|
1067
1123
|
currentUnit = null;
|
|
@@ -1314,12 +1370,41 @@ export async function handleAgentEnd(
|
|
|
1314
1370
|
// Small delay to let files settle (git commits, file writes)
|
|
1315
1371
|
await new Promise(r => setTimeout(r, 500));
|
|
1316
1372
|
|
|
1317
|
-
//
|
|
1373
|
+
// Commit any dirty files the LLM left behind on the current branch.
|
|
1374
|
+
// For execute-task units, build a meaningful commit message from the
|
|
1375
|
+
// task summary (one-liner, key_files, inferred type). For other unit
|
|
1376
|
+
// types, fall back to the generic chore() message.
|
|
1318
1377
|
if (currentUnit) {
|
|
1319
1378
|
try {
|
|
1320
|
-
|
|
1379
|
+
let taskContext: TaskCommitContext | undefined;
|
|
1380
|
+
|
|
1381
|
+
if (currentUnit.type === "execute-task") {
|
|
1382
|
+
const parts = currentUnit.id.split("/");
|
|
1383
|
+
const [mid, sid, tid] = parts;
|
|
1384
|
+
if (mid && sid && tid) {
|
|
1385
|
+
const summaryPath = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
|
|
1386
|
+
if (summaryPath) {
|
|
1387
|
+
try {
|
|
1388
|
+
const summaryContent = await loadFile(summaryPath);
|
|
1389
|
+
if (summaryContent) {
|
|
1390
|
+
const summary = parseSummary(summaryContent);
|
|
1391
|
+
taskContext = {
|
|
1392
|
+
taskId: `${sid}/${tid}`,
|
|
1393
|
+
taskTitle: summary.title?.replace(/^T\d+:\s*/, "") || tid,
|
|
1394
|
+
oneLiner: summary.oneLiner || undefined,
|
|
1395
|
+
keyFiles: summary.frontmatter.key_files?.filter(f => !f.includes("{{")) || undefined,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
} catch {
|
|
1399
|
+
// Non-fatal — fall back to generic message
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
const commitMsg = autoCommitCurrentBranch(basePath, currentUnit.type, currentUnit.id, taskContext);
|
|
1321
1406
|
if (commitMsg) {
|
|
1322
|
-
ctx.ui.notify(`
|
|
1407
|
+
ctx.ui.notify(`Committed: ${commitMsg.split("\n")[0]}`, "info");
|
|
1323
1408
|
}
|
|
1324
1409
|
} catch {
|
|
1325
1410
|
// Non-fatal
|
|
@@ -1331,10 +1416,13 @@ export async function handleAgentEnd(
|
|
|
1331
1416
|
// fixLevel:"task" ensures doctor only fixes task-level issues (e.g. marking
|
|
1332
1417
|
// checkboxes). Slice/milestone completion transitions (summary stubs,
|
|
1333
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).
|
|
1334
1421
|
try {
|
|
1335
1422
|
const scopeParts = currentUnit.id.split("/").slice(0, 2);
|
|
1336
1423
|
const doctorScope = scopeParts.join("/");
|
|
1337
|
-
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 });
|
|
1338
1426
|
if (report.fixesApplied.length > 0) {
|
|
1339
1427
|
ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
|
|
1340
1428
|
}
|
|
@@ -1372,7 +1460,8 @@ export async function handleAgentEnd(
|
|
|
1372
1460
|
}
|
|
1373
1461
|
try {
|
|
1374
1462
|
await rebuildState(basePath);
|
|
1375
|
-
|
|
1463
|
+
// State rebuild commit is bookkeeping — generic message is appropriate
|
|
1464
|
+
autoCommitCurrentBranch(basePath, "state-rebuild", currentUnit.id);
|
|
1376
1465
|
} catch {
|
|
1377
1466
|
// Non-fatal
|
|
1378
1467
|
}
|
|
@@ -1465,7 +1554,7 @@ export async function handleAgentEnd(
|
|
|
1465
1554
|
persistCompletedKey(basePath, completionKey);
|
|
1466
1555
|
completedKeySet.add(completionKey);
|
|
1467
1556
|
}
|
|
1468
|
-
|
|
1557
|
+
invalidateAllCaches();
|
|
1469
1558
|
}
|
|
1470
1559
|
} catch {
|
|
1471
1560
|
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
|
|
@@ -1504,7 +1593,16 @@ export async function handleAgentEnd(
|
|
|
1504
1593
|
if (currentUnit) {
|
|
1505
1594
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1506
1595
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
1507
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1596
|
+
const hookActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1597
|
+
if (hookActivityFile) {
|
|
1598
|
+
try {
|
|
1599
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1600
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1601
|
+
if (llmCallFn) {
|
|
1602
|
+
extractMemoriesFromUnit(hookActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1603
|
+
}
|
|
1604
|
+
} catch { /* non-fatal */ }
|
|
1605
|
+
}
|
|
1508
1606
|
}
|
|
1509
1607
|
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
1510
1608
|
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
@@ -1646,7 +1744,16 @@ export async function handleAgentEnd(
|
|
|
1646
1744
|
if (currentUnit) {
|
|
1647
1745
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1648
1746
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1649
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1747
|
+
const triageActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1748
|
+
if (triageActivityFile) {
|
|
1749
|
+
try {
|
|
1750
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1751
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1752
|
+
if (llmCallFn) {
|
|
1753
|
+
extractMemoriesFromUnit(triageActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1754
|
+
}
|
|
1755
|
+
} catch { /* non-fatal */ }
|
|
1756
|
+
}
|
|
1650
1757
|
}
|
|
1651
1758
|
|
|
1652
1759
|
// Dispatch triage as a new unit (early-dispatch-and-return)
|
|
@@ -1724,7 +1831,16 @@ export async function handleAgentEnd(
|
|
|
1724
1831
|
if (currentUnit) {
|
|
1725
1832
|
const modelId = ctx.model?.id ?? "unknown";
|
|
1726
1833
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
1727
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1834
|
+
const qtActivityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
1835
|
+
if (qtActivityFile) {
|
|
1836
|
+
try {
|
|
1837
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
1838
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
1839
|
+
if (llmCallFn) {
|
|
1840
|
+
extractMemoriesFromUnit(qtActivityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
1841
|
+
}
|
|
1842
|
+
} catch { /* non-fatal */ }
|
|
1843
|
+
}
|
|
1728
1844
|
}
|
|
1729
1845
|
|
|
1730
1846
|
// Dispatch quick-task as a new unit
|
|
@@ -1940,6 +2056,7 @@ async function dispatchNextUnit(
|
|
|
1940
2056
|
pi: ExtensionAPI,
|
|
1941
2057
|
): Promise<void> {
|
|
1942
2058
|
if (!active || !cmdCtx) {
|
|
2059
|
+
debugLog(`dispatchNextUnit early return — active=${active}, cmdCtx=${!!cmdCtx}`);
|
|
1943
2060
|
if (active && !cmdCtx) {
|
|
1944
2061
|
ctx.ui.notify("Auto-mode session expired. Run /gsd auto to restart.", "info");
|
|
1945
2062
|
}
|
|
@@ -1949,6 +2066,7 @@ async function dispatchNextUnit(
|
|
|
1949
2066
|
// Reentrancy guard: allow recursive calls from skip paths (_skipDepth > 0)
|
|
1950
2067
|
// but block concurrent external calls (watchdog, step wizard, etc.)
|
|
1951
2068
|
if (_dispatching && _skipDepth === 0) {
|
|
2069
|
+
debugLog("dispatchNextUnit reentrancy guard — another dispatch in progress, bailing");
|
|
1952
2070
|
return; // Another dispatch is in progress — bail silently
|
|
1953
2071
|
}
|
|
1954
2072
|
_dispatching = true;
|
|
@@ -1984,7 +2102,7 @@ async function dispatchNextUnit(
|
|
|
1984
2102
|
// Lightweight check for critical issues that would cause the next unit
|
|
1985
2103
|
// to fail or corrupt state. Auto-heals what it can, blocks on the rest.
|
|
1986
2104
|
try {
|
|
1987
|
-
const healthGate = preDispatchHealthGate(basePath);
|
|
2105
|
+
const healthGate = await preDispatchHealthGate(basePath);
|
|
1988
2106
|
if (healthGate.fixesApplied.length > 0) {
|
|
1989
2107
|
ctx.ui.notify(`Pre-dispatch: ${healthGate.fixesApplied.join(", ")}`, "info");
|
|
1990
2108
|
}
|
|
@@ -1997,6 +2115,14 @@ async function dispatchNextUnit(
|
|
|
1997
2115
|
// Non-fatal — health gate failure should never block dispatch
|
|
1998
2116
|
}
|
|
1999
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
|
+
|
|
2000
2126
|
const stopDeriveTimer = debugTime("derive-state");
|
|
2001
2127
|
let state = await deriveState(basePath);
|
|
2002
2128
|
stopDeriveTimer({
|
|
@@ -2418,26 +2544,61 @@ async function dispatchNextUnit(
|
|
|
2418
2544
|
const skipCount = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2419
2545
|
unitConsecutiveSkips.set(idempotencyKey, skipCount);
|
|
2420
2546
|
if (skipCount > MAX_CONSECUTIVE_SKIPS) {
|
|
2547
|
+
// Cross-check: verify deriveState actually returns this unit (#790).
|
|
2548
|
+
// If the unit's milestone is already complete, this is a phantom skip
|
|
2549
|
+
// loop from stale crash recovery context — don't evict.
|
|
2550
|
+
const skippedMid = unitId.split("/")[0];
|
|
2551
|
+
const skippedMilestoneComplete = skippedMid
|
|
2552
|
+
? !!resolveMilestoneFile(basePath, skippedMid, "SUMMARY")
|
|
2553
|
+
: false;
|
|
2554
|
+
if (skippedMilestoneComplete) {
|
|
2555
|
+
// Milestone is complete — evicting this key would fight self-heal.
|
|
2556
|
+
// Clear skip counter and re-dispatch from fresh state.
|
|
2557
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2558
|
+
invalidateAllCaches();
|
|
2559
|
+
ctx.ui.notify(
|
|
2560
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid}. Re-dispatching from fresh state.`,
|
|
2561
|
+
"info",
|
|
2562
|
+
);
|
|
2563
|
+
_skipDepth++;
|
|
2564
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2565
|
+
await dispatchNextUnit(ctx, pi);
|
|
2566
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2421
2569
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2422
2570
|
completedKeySet.delete(idempotencyKey);
|
|
2423
2571
|
removePersistedKey(basePath, idempotencyKey);
|
|
2424
|
-
|
|
2572
|
+
invalidateAllCaches();
|
|
2425
2573
|
ctx.ui.notify(
|
|
2426
2574
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2427
2575
|
"warning",
|
|
2428
2576
|
);
|
|
2577
|
+
if (!active) return;
|
|
2429
2578
|
_skipDepth++;
|
|
2430
|
-
await new Promise(r => setTimeout(r,
|
|
2579
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2431
2580
|
await dispatchNextUnit(ctx, pi);
|
|
2432
2581
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2433
2582
|
return;
|
|
2434
2583
|
}
|
|
2584
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2585
|
+
const lifeSkip = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2586
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip);
|
|
2587
|
+
if (lifeSkip > MAX_LIFETIME_DISPATCHES) {
|
|
2588
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2589
|
+
ctx.ui.notify(
|
|
2590
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip} iterations).`,
|
|
2591
|
+
"error",
|
|
2592
|
+
);
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2435
2595
|
ctx.ui.notify(
|
|
2436
2596
|
`Skipping ${unitType} ${unitId} — already completed in a prior session. Advancing.`,
|
|
2437
2597
|
"info",
|
|
2438
2598
|
);
|
|
2599
|
+
if (!active) return;
|
|
2439
2600
|
_skipDepth++;
|
|
2440
|
-
await new Promise(r => setTimeout(r,
|
|
2601
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2441
2602
|
await dispatchNextUnit(ctx, pi);
|
|
2442
2603
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2443
2604
|
return;
|
|
@@ -2460,31 +2621,62 @@ async function dispatchNextUnit(
|
|
|
2460
2621
|
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2461
2622
|
persistCompletedKey(basePath, idempotencyKey);
|
|
2462
2623
|
completedKeySet.add(idempotencyKey);
|
|
2463
|
-
|
|
2624
|
+
invalidateAllCaches();
|
|
2464
2625
|
// Same consecutive-skip guard as the idempotency path above.
|
|
2465
2626
|
const skipCount2 = (unitConsecutiveSkips.get(idempotencyKey) ?? 0) + 1;
|
|
2466
2627
|
unitConsecutiveSkips.set(idempotencyKey, skipCount2);
|
|
2467
2628
|
if (skipCount2 > MAX_CONSECUTIVE_SKIPS) {
|
|
2629
|
+
// Cross-check: verify the unit's milestone is still active (#790).
|
|
2630
|
+
const skippedMid2 = unitId.split("/")[0];
|
|
2631
|
+
const skippedMilestoneComplete2 = skippedMid2
|
|
2632
|
+
? !!resolveMilestoneFile(basePath, skippedMid2, "SUMMARY")
|
|
2633
|
+
: false;
|
|
2634
|
+
if (skippedMilestoneComplete2) {
|
|
2635
|
+
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2636
|
+
invalidateAllCaches();
|
|
2637
|
+
ctx.ui.notify(
|
|
2638
|
+
`Phantom skip loop cleared: ${unitType} ${unitId} belongs to completed milestone ${skippedMid2}. Re-dispatching from fresh state.`,
|
|
2639
|
+
"info",
|
|
2640
|
+
);
|
|
2641
|
+
_skipDepth++;
|
|
2642
|
+
await new Promise(r => setTimeout(r, 50));
|
|
2643
|
+
await dispatchNextUnit(ctx, pi);
|
|
2644
|
+
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2468
2647
|
unitConsecutiveSkips.delete(idempotencyKey);
|
|
2469
2648
|
completedKeySet.delete(idempotencyKey);
|
|
2470
2649
|
removePersistedKey(basePath, idempotencyKey);
|
|
2471
|
-
|
|
2650
|
+
invalidateAllCaches();
|
|
2472
2651
|
ctx.ui.notify(
|
|
2473
2652
|
`Skip loop detected: ${unitType} ${unitId} skipped ${skipCount2} times without advancing. Evicting completion record and forcing reconciliation.`,
|
|
2474
2653
|
"warning",
|
|
2475
2654
|
);
|
|
2655
|
+
if (!active) return;
|
|
2476
2656
|
_skipDepth++;
|
|
2477
|
-
await new Promise(r => setTimeout(r,
|
|
2657
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2478
2658
|
await dispatchNextUnit(ctx, pi);
|
|
2479
2659
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2480
2660
|
return;
|
|
2481
2661
|
}
|
|
2662
|
+
// Count toward lifetime cap so hard-stop fires during skip loops (#792)
|
|
2663
|
+
const lifeSkip2 = (unitLifetimeDispatches.get(idempotencyKey) ?? 0) + 1;
|
|
2664
|
+
unitLifetimeDispatches.set(idempotencyKey, lifeSkip2);
|
|
2665
|
+
if (lifeSkip2 > MAX_LIFETIME_DISPATCHES) {
|
|
2666
|
+
await stopAuto(ctx, pi, `Hard loop: ${unitType} ${unitId} (skip cycle)`);
|
|
2667
|
+
ctx.ui.notify(
|
|
2668
|
+
`Hard loop detected: ${unitType} ${unitId} hit lifetime cap during skip cycle (${lifeSkip2} iterations).`,
|
|
2669
|
+
"error",
|
|
2670
|
+
);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2482
2673
|
ctx.ui.notify(
|
|
2483
2674
|
`Skipping ${unitType} ${unitId} — artifact exists but completion key was missing. Repaired and advancing.`,
|
|
2484
2675
|
"info",
|
|
2485
2676
|
);
|
|
2677
|
+
if (!active) return;
|
|
2486
2678
|
_skipDepth++;
|
|
2487
|
-
await new Promise(r => setTimeout(r,
|
|
2679
|
+
await new Promise(r => setTimeout(r, 150));
|
|
2488
2680
|
await dispatchNextUnit(ctx, pi);
|
|
2489
2681
|
_skipDepth = Math.max(0, _skipDepth - 1);
|
|
2490
2682
|
return;
|
|
@@ -2554,7 +2746,7 @@ async function dispatchNextUnit(
|
|
|
2554
2746
|
persistCompletedKey(basePath, reconciledKey);
|
|
2555
2747
|
completedKeySet.add(reconciledKey);
|
|
2556
2748
|
unitDispatchCount.delete(dispatchKey);
|
|
2557
|
-
|
|
2749
|
+
invalidateAllCaches();
|
|
2558
2750
|
await new Promise(r => setImmediate(r));
|
|
2559
2751
|
await dispatchNextUnit(ctx, pi);
|
|
2560
2752
|
return;
|
|
@@ -2581,7 +2773,7 @@ async function dispatchNextUnit(
|
|
|
2581
2773
|
persistCompletedKey(basePath, dispatchKey);
|
|
2582
2774
|
completedKeySet.add(dispatchKey);
|
|
2583
2775
|
unitDispatchCount.delete(dispatchKey);
|
|
2584
|
-
|
|
2776
|
+
invalidateAllCaches();
|
|
2585
2777
|
await new Promise(r => setImmediate(r));
|
|
2586
2778
|
await dispatchNextUnit(ctx, pi);
|
|
2587
2779
|
return;
|
|
@@ -2601,7 +2793,7 @@ async function dispatchNextUnit(
|
|
|
2601
2793
|
persistCompletedKey(basePath, dispatchKey);
|
|
2602
2794
|
completedKeySet.add(dispatchKey);
|
|
2603
2795
|
unitDispatchCount.delete(dispatchKey);
|
|
2604
|
-
|
|
2796
|
+
invalidateAllCaches();
|
|
2605
2797
|
await new Promise(r => setImmediate(r));
|
|
2606
2798
|
await dispatchNextUnit(ctx, pi);
|
|
2607
2799
|
return;
|
|
@@ -2642,7 +2834,7 @@ async function dispatchNextUnit(
|
|
|
2642
2834
|
persistCompletedKey(basePath, repairedKey);
|
|
2643
2835
|
completedKeySet.add(repairedKey);
|
|
2644
2836
|
unitDispatchCount.delete(dispatchKey);
|
|
2645
|
-
|
|
2837
|
+
invalidateAllCaches();
|
|
2646
2838
|
await new Promise(r => setImmediate(r));
|
|
2647
2839
|
await dispatchNextUnit(ctx, pi);
|
|
2648
2840
|
return;
|
|
@@ -2686,7 +2878,18 @@ async function dispatchNextUnit(
|
|
|
2686
2878
|
if (currentUnit) {
|
|
2687
2879
|
const modelId = ctx.model?.id ?? "unknown";
|
|
2688
2880
|
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId, { promptCharCount: lastPromptCharCount, baselineCharCount: lastBaselineCharCount, ...(currentUnitRouting ?? {}) });
|
|
2689
|
-
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2881
|
+
const activityFile = saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
2882
|
+
|
|
2883
|
+
// Fire-and-forget memory extraction from completed unit
|
|
2884
|
+
if (activityFile) {
|
|
2885
|
+
try {
|
|
2886
|
+
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
2887
|
+
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
2888
|
+
if (llmCallFn) {
|
|
2889
|
+
extractMemoriesFromUnit(activityFile, currentUnit.type, currentUnit.id, llmCallFn).catch(() => {});
|
|
2890
|
+
}
|
|
2891
|
+
} catch { /* non-fatal */ }
|
|
2892
|
+
}
|
|
2690
2893
|
|
|
2691
2894
|
// Record routing outcome for adaptive learning
|
|
2692
2895
|
if (currentUnitRouting) {
|
|
@@ -3603,6 +3806,20 @@ export async function dispatchDirectPhase(
|
|
|
3603
3806
|
ctx.ui.notify("Cannot dispatch research-slice: no active slice.", "warning");
|
|
3604
3807
|
return;
|
|
3605
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
|
+
|
|
3606
3823
|
unitType = "research-slice";
|
|
3607
3824
|
unitId = `${mid}/${sid}`;
|
|
3608
3825
|
prompt = await buildResearchSlicePrompt(mid, midTitle, sid, sTitle, base);
|
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
import { invalidateStateCache } from './state.js';
|
|
13
13
|
import { clearPathCache } from './paths.js';
|
|
14
14
|
import { clearParseCache } from './files.js';
|
|
15
|
+
import { clearArtifacts } from './gsd-db.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Invalidate all GSD runtime caches in one call.
|
|
18
19
|
*
|
|
19
20
|
* Call this after file writes, milestone transitions, merge reconciliation,
|
|
20
21
|
* or any operation that changes .gsd/ contents on disk. Forgetting to clear
|
|
21
|
-
* any single cache causes stale reads (see #431).
|
|
22
|
+
* any single cache causes stale reads (see #431, #793).
|
|
22
23
|
*/
|
|
23
24
|
export function invalidateAllCaches(): void {
|
|
24
25
|
invalidateStateCache();
|
|
25
26
|
clearPathCache();
|
|
26
27
|
clearParseCache();
|
|
28
|
+
clearArtifacts();
|
|
27
29
|
}
|
|
@@ -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
|
|
@@ -4,6 +4,7 @@ import { join, sep } from "node:path";
|
|
|
4
4
|
import { loadFile, parsePlan, parseRoadmap, parseSummary, saveFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.js";
|
|
5
5
|
import { resolveMilestoneFile, resolveMilestonePath, resolveSliceFile, resolveSlicePath, resolveTaskFile, resolveTaskFiles, resolveTasksDir, milestonesDir, gsdRoot, relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relGsdRootFile, resolveGsdRootFile } from "./paths.js";
|
|
6
6
|
import { deriveState, isMilestoneComplete } from "./state.js";
|
|
7
|
+
import { invalidateAllCaches } from "./cache.js";
|
|
7
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
8
9
|
import { listWorktrees, resolveGitDir } from "./worktree-manager.js";
|
|
9
10
|
import { abortAndReset } from "./git-self-heal.js";
|
|
@@ -200,6 +201,7 @@ async function updateStateFile(basePath: string, fixesApplied: string[]): Promis
|
|
|
200
201
|
|
|
201
202
|
/** Rebuild STATE.md from current disk state. Exported for auto-mode post-hooks. */
|
|
202
203
|
export async function rebuildState(basePath: string): Promise<void> {
|
|
204
|
+
invalidateAllCaches();
|
|
203
205
|
const state = await deriveState(basePath);
|
|
204
206
|
const path = resolveGsdRootFile(basePath, "STATE");
|
|
205
207
|
await saveFile(path, buildStateMarkdown(state));
|
|
@@ -1142,8 +1144,31 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
1142
1144
|
unitId: taskUnitId,
|
|
1143
1145
|
message: `Task ${task.id} is marked done but summary is missing`,
|
|
1144
1146
|
file: relTaskFile(basePath, milestoneId, slice.id, task.id, "SUMMARY"),
|
|
1145
|
-
fixable:
|
|
1147
|
+
fixable: true,
|
|
1146
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
|
+
}
|
|
1147
1172
|
}
|
|
1148
1173
|
|
|
1149
1174
|
if (!task.done && hasSummary) {
|