gsd-pi 2.11.0 → 2.13.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/dist/cli.js +18 -1
- package/dist/onboarding.js +3 -0
- package/dist/resource-loader.d.ts +2 -0
- package/dist/resource-loader.js +36 -1
- package/dist/resources/extensions/bg-shell/index.ts +51 -7
- package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/dist/resources/extensions/gsd/auto.ts +381 -13
- package/dist/resources/extensions/gsd/commands.ts +9 -3
- package/dist/resources/extensions/gsd/doctor.ts +254 -3
- package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/dist/resources/extensions/gsd/git-service.ts +11 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
- package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/dist/resources/extensions/gsd/preferences.ts +209 -1
- package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/system.md +32 -29
- package/dist/resources/extensions/gsd/templates/context.md +1 -1
- package/dist/resources/extensions/gsd/templates/state.md +3 -3
- package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/dist/resources/extensions/gsd/types.ts +109 -0
- package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
- package/dist/resources/extensions/search-the-web/provider.ts +19 -2
- package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
- package/dist/wizard.js +1 -0
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +169 -55
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +13 -1
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +16 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +91 -1
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.ts +273 -63
- package/packages/pi-agent-core/src/agent.ts +24 -0
- package/packages/pi-agent-core/src/types.ts +98 -0
- package/packages/pi-ai/dist/env-api-keys.js +1 -0
- package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +314 -0
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +236 -0
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -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/env-api-keys.ts +1 -0
- package/packages/pi-ai/src/models.generated.ts +236 -0
- package/packages/pi-ai/src/types.ts +2 -1
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +2 -1
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -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 +69 -8
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
- package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
- package/packages/pi-coding-agent/dist/core/sdk.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 +6 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/cli/args.ts +2 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
- package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
- package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
- package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
- package/packages/pi-tui/dist/components/editor.d.ts +11 -0
- package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/editor.js +64 -6
- package/packages/pi-tui/dist/components/editor.js.map +1 -1
- package/packages/pi-tui/src/components/editor.ts +71 -6
- package/src/resources/extensions/bg-shell/index.ts +51 -7
- package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
- package/src/resources/extensions/gsd/auto.ts +381 -13
- package/src/resources/extensions/gsd/commands.ts +9 -3
- package/src/resources/extensions/gsd/doctor.ts +254 -3
- package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
- package/src/resources/extensions/gsd/git-service.ts +11 -0
- package/src/resources/extensions/gsd/guided-flow.ts +81 -9
- package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
- package/src/resources/extensions/gsd/preferences.ts +209 -1
- package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
- package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
- package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
- package/src/resources/extensions/gsd/prompts/queue.md +3 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/system.md +32 -29
- package/src/resources/extensions/gsd/templates/context.md +1 -1
- package/src/resources/extensions/gsd/templates/state.md +3 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
- package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
- package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
- package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
- package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
- package/src/resources/extensions/gsd/types.ts +109 -0
- package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
- package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
- package/src/resources/extensions/search-the-web/native-search.ts +15 -10
- package/src/resources/extensions/search-the-web/provider.ts +19 -2
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
- package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
|
@@ -18,17 +18,18 @@ import type {
|
|
|
18
18
|
|
|
19
19
|
import { deriveState, invalidateStateCache } from "./state.js";
|
|
20
20
|
import type { GSDState } from "./types.js";
|
|
21
|
-
import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus } from "./files.js";
|
|
21
|
+
import { loadFile, parseContinue, parsePlan, parseRoadmap, parseSummary, extractUatType, inlinePriorMilestoneSummary, getManifestStatus, clearParseCache } from "./files.js";
|
|
22
22
|
export { inlinePriorMilestoneSummary };
|
|
23
23
|
import type { UatType } from "./files.js";
|
|
24
24
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
25
|
-
import { loadPrompt } from "./prompt-loader.js";
|
|
25
|
+
import { loadPrompt, inlineTemplate } from "./prompt-loader.js";
|
|
26
26
|
import {
|
|
27
27
|
gsdRoot, resolveMilestoneFile, resolveSliceFile, resolveSlicePath,
|
|
28
28
|
resolveMilestonePath, resolveDir, resolveTasksDir, resolveTaskFiles, resolveTaskFile,
|
|
29
29
|
relMilestoneFile, relSliceFile, relTaskFile, relSlicePath, relMilestonePath,
|
|
30
30
|
milestonesDir, resolveGsdRootFile, relGsdRootFile,
|
|
31
31
|
buildMilestoneFileName, buildSliceFileName, buildTaskFileName,
|
|
32
|
+
clearPathCache,
|
|
32
33
|
} from "./paths.js";
|
|
33
34
|
import { saveActivityLog } from "./activity-log.js";
|
|
34
35
|
import { synthesizeCrashRecovery, getDeepDiagnostic } from "./session-forensics.js";
|
|
@@ -42,6 +43,18 @@ import {
|
|
|
42
43
|
} from "./unit-runtime.js";
|
|
43
44
|
import { resolveAutoSupervisorConfig, resolveModelForUnit, resolveModelWithFallbacksForUnit, resolveSkillDiscoveryMode, loadEffectiveGSDPreferences } from "./preferences.js";
|
|
44
45
|
import type { GSDPreferences } from "./preferences.js";
|
|
46
|
+
import {
|
|
47
|
+
checkPostUnitHooks,
|
|
48
|
+
getActiveHook,
|
|
49
|
+
resetHookState,
|
|
50
|
+
isRetryPending,
|
|
51
|
+
consumeRetryTrigger,
|
|
52
|
+
runPreDispatchHooks,
|
|
53
|
+
persistHookState,
|
|
54
|
+
restoreHookState,
|
|
55
|
+
clearPersistedHookState,
|
|
56
|
+
formatHookStatus,
|
|
57
|
+
} from "./post-unit-hooks.js";
|
|
45
58
|
import {
|
|
46
59
|
validatePlanBoundary,
|
|
47
60
|
validateExecuteBoundary,
|
|
@@ -73,6 +86,19 @@ import {
|
|
|
73
86
|
import { GitServiceImpl, runGit } from "./git-service.js";
|
|
74
87
|
import { nativeCommitCountBetween } from "./native-git-bridge.js";
|
|
75
88
|
import { getPriorSliceCompletionBlocker } from "./dispatch-guard.js";
|
|
89
|
+
import { formatGitError } from "./git-self-heal.js";
|
|
90
|
+
import {
|
|
91
|
+
createAutoWorktree,
|
|
92
|
+
enterAutoWorktree,
|
|
93
|
+
teardownAutoWorktree,
|
|
94
|
+
isInAutoWorktree,
|
|
95
|
+
getAutoWorktreePath,
|
|
96
|
+
getAutoWorktreeOriginalBase,
|
|
97
|
+
mergeSliceToMilestone,
|
|
98
|
+
mergeMilestoneToMain,
|
|
99
|
+
shouldUseWorktreeIsolation,
|
|
100
|
+
getMergeToMainMode,
|
|
101
|
+
} from "./auto-worktree.js";
|
|
76
102
|
import type { GitPreferences } from "./git-service.js";
|
|
77
103
|
import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
|
|
78
104
|
import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
|
|
@@ -131,6 +157,7 @@ let stepMode = false;
|
|
|
131
157
|
let verbose = false;
|
|
132
158
|
let cmdCtx: ExtensionCommandContext | null = null;
|
|
133
159
|
let basePath = "";
|
|
160
|
+
let originalBasePath = "";
|
|
134
161
|
let gitService: GitServiceImpl | null = null;
|
|
135
162
|
|
|
136
163
|
/** Track total dispatches per unit to detect stuck loops (catches A→B→A→B patterns) */
|
|
@@ -138,6 +165,12 @@ const unitDispatchCount = new Map<string, number>();
|
|
|
138
165
|
const MAX_UNIT_DISPATCHES = 3;
|
|
139
166
|
/** Retry index at which a stub summary placeholder is written when the summary is still absent. */
|
|
140
167
|
const STUB_RECOVERY_THRESHOLD = 2;
|
|
168
|
+
/** Hard cap on total dispatches per unit across ALL reconciliation cycles.
|
|
169
|
+
* unitDispatchCount can be reset by loop-recovery/self-repair paths, but this
|
|
170
|
+
* counter is never reset — it catches infinite reconciliation loops where
|
|
171
|
+
* artifacts exist but deriveState keeps returning the same unit. */
|
|
172
|
+
const unitLifetimeDispatches = new Map<string, number>();
|
|
173
|
+
const MAX_LIFETIME_DISPATCHES = 6;
|
|
141
174
|
|
|
142
175
|
/** Tracks recovery attempt count per unit for backoff and diagnostics. */
|
|
143
176
|
const unitRecoveryCount = new Map<string, number>();
|
|
@@ -330,6 +363,27 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
330
363
|
|
|
331
364
|
// Remove SIGTERM handler registered at auto-mode start
|
|
332
365
|
deregisterSigtermHandler();
|
|
366
|
+
|
|
367
|
+
// ── Auto-worktree: exit worktree and reset basePath on stop ──
|
|
368
|
+
if (currentMilestoneId && isInAutoWorktree(basePath)) {
|
|
369
|
+
try {
|
|
370
|
+
teardownAutoWorktree(originalBasePath, currentMilestoneId);
|
|
371
|
+
basePath = originalBasePath;
|
|
372
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
373
|
+
ctx?.ui.notify("Exited auto-worktree.", "info");
|
|
374
|
+
} catch (err) {
|
|
375
|
+
ctx?.ui.notify(
|
|
376
|
+
`Auto-worktree teardown failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
377
|
+
"warning",
|
|
378
|
+
);
|
|
379
|
+
// Force basePath back to original even if teardown failed
|
|
380
|
+
if (originalBasePath) {
|
|
381
|
+
basePath = originalBasePath;
|
|
382
|
+
try { process.chdir(basePath); } catch { /* best-effort */ }
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
333
387
|
const ledger = getLedger();
|
|
334
388
|
if (ledger && ledger.units.length > 0) {
|
|
335
389
|
const totals = getProjectTotals(ledger.units);
|
|
@@ -347,13 +401,17 @@ export async function stopAuto(ctx?: ExtensionContext, pi?: ExtensionAPI): Promi
|
|
|
347
401
|
}
|
|
348
402
|
|
|
349
403
|
resetMetrics();
|
|
404
|
+
resetHookState();
|
|
405
|
+
if (basePath) clearPersistedHookState(basePath);
|
|
350
406
|
active = false;
|
|
351
407
|
paused = false;
|
|
352
408
|
stepMode = false;
|
|
353
409
|
unitDispatchCount.clear();
|
|
354
410
|
unitRecoveryCount.clear();
|
|
411
|
+
unitLifetimeDispatches.clear();
|
|
355
412
|
currentUnit = null;
|
|
356
413
|
currentMilestoneId = null;
|
|
414
|
+
originalBasePath = "";
|
|
357
415
|
cachedSliceProgress = null;
|
|
358
416
|
pendingCrashRecovery = null;
|
|
359
417
|
_handlingAgentEnd = false;
|
|
@@ -503,10 +561,17 @@ async function mergeOrphanedSliceBranches(
|
|
|
503
561
|
"info",
|
|
504
562
|
);
|
|
505
563
|
try {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
564
|
+
let mergeResult;
|
|
565
|
+
if (isInAutoWorktree(base) && getMergeToMainMode() !== "slice") {
|
|
566
|
+
mergeResult = mergeSliceToMilestone(
|
|
567
|
+
base, milestoneId, sliceId, sliceEntry.title || sliceId,
|
|
568
|
+
);
|
|
569
|
+
} else {
|
|
570
|
+
switchToMain(base);
|
|
571
|
+
mergeResult = mergeSliceToMain(
|
|
572
|
+
base, milestoneId, sliceId, sliceEntry.title || sliceId,
|
|
573
|
+
);
|
|
574
|
+
}
|
|
510
575
|
ctx.ui.notify(
|
|
511
576
|
`Merged orphaned branch ${mergeResult.branch} → ${mainBranch}.`,
|
|
512
577
|
"info",
|
|
@@ -553,17 +618,44 @@ export async function startAuto(
|
|
|
553
618
|
cmdCtx = ctx;
|
|
554
619
|
basePath = base;
|
|
555
620
|
unitDispatchCount.clear();
|
|
621
|
+
unitLifetimeDispatches.clear();
|
|
556
622
|
// Re-initialize metrics in case ledger was lost during pause
|
|
557
623
|
if (!getLedger()) initMetrics(base);
|
|
558
624
|
// Ensure milestone ID is set on git service for integration branch resolution
|
|
559
625
|
if (currentMilestoneId) setActiveMilestoneId(base, currentMilestoneId);
|
|
560
626
|
|
|
627
|
+
// ── Auto-worktree: re-enter worktree on resume if not already inside ──
|
|
628
|
+
if (currentMilestoneId && originalBasePath && !isInAutoWorktree(basePath) && shouldUseWorktreeIsolation(originalBasePath)) {
|
|
629
|
+
try {
|
|
630
|
+
const existingWtPath = getAutoWorktreePath(originalBasePath, currentMilestoneId);
|
|
631
|
+
if (existingWtPath) {
|
|
632
|
+
const wtPath = enterAutoWorktree(originalBasePath, currentMilestoneId);
|
|
633
|
+
basePath = wtPath;
|
|
634
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
635
|
+
ctx.ui.notify(`Re-entered auto-worktree at ${wtPath}`, "info");
|
|
636
|
+
} else {
|
|
637
|
+
// Worktree was deleted while paused — recreate it.
|
|
638
|
+
const wtPath = createAutoWorktree(originalBasePath, currentMilestoneId);
|
|
639
|
+
basePath = wtPath;
|
|
640
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
641
|
+
ctx.ui.notify(`Recreated auto-worktree at ${wtPath}`, "info");
|
|
642
|
+
}
|
|
643
|
+
} catch (err) {
|
|
644
|
+
ctx.ui.notify(
|
|
645
|
+
`Auto-worktree re-entry failed: ${err instanceof Error ? err.message : String(err)}. Continuing at current path.`,
|
|
646
|
+
"warning",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
561
651
|
// Re-register SIGTERM handler for the resumed session
|
|
562
|
-
registerSigtermHandler(
|
|
652
|
+
registerSigtermHandler(basePath);
|
|
563
653
|
|
|
564
654
|
ctx.ui.setStatus("gsd-auto", stepMode ? "next" : "auto");
|
|
565
655
|
ctx.ui.setFooter(hideFooter);
|
|
566
656
|
ctx.ui.notify(stepMode ? "Step-mode resumed." : "Auto-mode resumed.", "info");
|
|
657
|
+
// Restore hook state from disk in case session was interrupted
|
|
658
|
+
restoreHookState(base);
|
|
567
659
|
// Rebuild disk state before resuming — user interaction during pause may have changed files
|
|
568
660
|
try { await rebuildState(base); } catch { /* non-fatal */ }
|
|
569
661
|
try {
|
|
@@ -575,6 +667,8 @@ export async function startAuto(
|
|
|
575
667
|
// Self-heal: clear stale runtime records where artifacts already exist
|
|
576
668
|
await selfHealRuntimeRecords(base, ctx);
|
|
577
669
|
invalidateStateCache();
|
|
670
|
+
clearParseCache();
|
|
671
|
+
clearPathCache();
|
|
578
672
|
await dispatchNextUnit(ctx, pi);
|
|
579
673
|
return;
|
|
580
674
|
}
|
|
@@ -668,8 +762,11 @@ export async function startAuto(
|
|
|
668
762
|
basePath = base;
|
|
669
763
|
unitDispatchCount.clear();
|
|
670
764
|
unitRecoveryCount.clear();
|
|
765
|
+
unitLifetimeDispatches.clear();
|
|
671
766
|
completedKeySet.clear();
|
|
672
767
|
loadPersistedKeys(base, completedKeySet);
|
|
768
|
+
resetHookState();
|
|
769
|
+
restoreHookState(base);
|
|
673
770
|
autoStartTime = Date.now();
|
|
674
771
|
completedUnits = [];
|
|
675
772
|
currentUnit = null;
|
|
@@ -689,6 +786,36 @@ export async function startAuto(
|
|
|
689
786
|
setActiveMilestoneId(base, currentMilestoneId);
|
|
690
787
|
}
|
|
691
788
|
|
|
789
|
+
// ── Auto-worktree: create or enter worktree for the active milestone ──
|
|
790
|
+
// Store the original project root before any chdir so we can restore on stop.
|
|
791
|
+
originalBasePath = base;
|
|
792
|
+
if (currentMilestoneId && shouldUseWorktreeIsolation(base)) {
|
|
793
|
+
try {
|
|
794
|
+
const existingWtPath = getAutoWorktreePath(base, currentMilestoneId);
|
|
795
|
+
if (existingWtPath) {
|
|
796
|
+
// Worktree already exists (e.g., previous session created it) — enter it.
|
|
797
|
+
const wtPath = enterAutoWorktree(base, currentMilestoneId);
|
|
798
|
+
basePath = wtPath;
|
|
799
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
800
|
+
ctx.ui.notify(`Entered auto-worktree at ${wtPath}`, "info");
|
|
801
|
+
} else {
|
|
802
|
+
// Fresh start — create worktree and enter it.
|
|
803
|
+
const wtPath = createAutoWorktree(base, currentMilestoneId);
|
|
804
|
+
basePath = wtPath;
|
|
805
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
806
|
+
ctx.ui.notify(`Created auto-worktree at ${wtPath}`, "info");
|
|
807
|
+
}
|
|
808
|
+
// Re-register SIGTERM handler with the new basePath
|
|
809
|
+
registerSigtermHandler(basePath);
|
|
810
|
+
} catch (err) {
|
|
811
|
+
// Worktree creation is non-fatal — continue in the project root.
|
|
812
|
+
ctx.ui.notify(
|
|
813
|
+
`Auto-worktree setup failed: ${err instanceof Error ? err.message : String(err)}. Continuing in project root.`,
|
|
814
|
+
"warning",
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
692
819
|
// Initialize metrics — loads existing ledger from disk
|
|
693
820
|
initMetrics(base);
|
|
694
821
|
|
|
@@ -767,6 +894,8 @@ export async function handleAgentEnd(
|
|
|
767
894
|
// Invalidate deriveState() cache — the unit just completed and may have
|
|
768
895
|
// written planning files (task summaries, roadmap checkboxes, etc.)
|
|
769
896
|
invalidateStateCache();
|
|
897
|
+
clearParseCache();
|
|
898
|
+
clearPathCache();
|
|
770
899
|
|
|
771
900
|
// Small delay to let files settle (git commits, file writes)
|
|
772
901
|
await new Promise(r => setTimeout(r, 500));
|
|
@@ -804,6 +933,97 @@ export async function handleAgentEnd(
|
|
|
804
933
|
} catch {
|
|
805
934
|
// Non-fatal
|
|
806
935
|
}
|
|
936
|
+
|
|
937
|
+
// ── Path A fix: verify artifact and persist completion before re-entering dispatch ──
|
|
938
|
+
// After doctor + rebuildState, check whether the just-completed unit actually
|
|
939
|
+
// produced its expected artifact. If so, persist the completion key now so the
|
|
940
|
+
// idempotency check at the top of dispatchNextUnit() skips it — even if
|
|
941
|
+
// deriveState() still returns this unit as active (e.g. branch mismatch).
|
|
942
|
+
try {
|
|
943
|
+
if (verifyExpectedArtifact(currentUnit.type, currentUnit.id, basePath)) {
|
|
944
|
+
const completionKey = `${currentUnit.type}/${currentUnit.id}`;
|
|
945
|
+
if (!completedKeySet.has(completionKey)) {
|
|
946
|
+
persistCompletedKey(basePath, completionKey);
|
|
947
|
+
completedKeySet.add(completionKey);
|
|
948
|
+
}
|
|
949
|
+
invalidateStateCache();
|
|
950
|
+
}
|
|
951
|
+
} catch {
|
|
952
|
+
// Non-fatal — worst case we fall through to normal dispatch which has its own checks
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// ── Post-unit hooks: check if a configured hook should run before normal dispatch ──
|
|
957
|
+
if (currentUnit && !stepMode) {
|
|
958
|
+
const hookUnit = checkPostUnitHooks(currentUnit.type, currentUnit.id, basePath);
|
|
959
|
+
if (hookUnit) {
|
|
960
|
+
// Dispatch the hook unit instead of normal flow
|
|
961
|
+
const hookStartedAt = Date.now();
|
|
962
|
+
if (currentUnit) {
|
|
963
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
964
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
965
|
+
saveActivityLog(ctx, basePath, currentUnit.type, currentUnit.id);
|
|
966
|
+
}
|
|
967
|
+
currentUnit = { type: hookUnit.unitType, id: hookUnit.unitId, startedAt: hookStartedAt };
|
|
968
|
+
writeUnitRuntimeRecord(basePath, hookUnit.unitType, hookUnit.unitId, hookStartedAt, {
|
|
969
|
+
phase: "dispatched",
|
|
970
|
+
wrapupWarningSent: false,
|
|
971
|
+
timeoutAt: null,
|
|
972
|
+
lastProgressAt: hookStartedAt,
|
|
973
|
+
progressCount: 0,
|
|
974
|
+
lastProgressKind: "dispatch",
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const state = await deriveState(basePath);
|
|
978
|
+
updateProgressWidget(ctx, hookUnit.unitType, hookUnit.unitId, state);
|
|
979
|
+
const hookState = getActiveHook();
|
|
980
|
+
ctx.ui.notify(
|
|
981
|
+
`Running post-unit hook: ${hookUnit.hookName} (cycle ${hookState?.cycle ?? 1})`,
|
|
982
|
+
"info",
|
|
983
|
+
);
|
|
984
|
+
|
|
985
|
+
// Switch model if the hook specifies one
|
|
986
|
+
if (hookUnit.model) {
|
|
987
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
988
|
+
const match = availableModels.find(m =>
|
|
989
|
+
m.id === hookUnit.model || `${m.provider}/${m.id}` === hookUnit.model,
|
|
990
|
+
);
|
|
991
|
+
if (match) {
|
|
992
|
+
try {
|
|
993
|
+
await pi.setModel(match);
|
|
994
|
+
} catch { /* non-fatal — use current model */ }
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const result = await cmdCtx!.newSession();
|
|
999
|
+
if (result.cancelled) {
|
|
1000
|
+
resetHookState();
|
|
1001
|
+
await stopAuto(ctx, pi);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1005
|
+
writeLock(basePath, hookUnit.unitType, hookUnit.unitId, completedUnits.length, sessionFile);
|
|
1006
|
+
// Persist hook state so cycle counts survive crashes
|
|
1007
|
+
persistHookState(basePath);
|
|
1008
|
+
pi.sendMessage(
|
|
1009
|
+
{ customType: "gsd-auto", content: hookUnit.prompt, display: verbose },
|
|
1010
|
+
{ triggerTurn: true },
|
|
1011
|
+
);
|
|
1012
|
+
return; // handleAgentEnd will fire again when hook session completes
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Check if a hook requested a retry of the trigger unit
|
|
1016
|
+
if (isRetryPending()) {
|
|
1017
|
+
const trigger = consumeRetryTrigger();
|
|
1018
|
+
if (trigger) {
|
|
1019
|
+
ctx.ui.notify(
|
|
1020
|
+
`Hook requested retry of ${trigger.unitType} ${trigger.unitId}.`,
|
|
1021
|
+
"info",
|
|
1022
|
+
);
|
|
1023
|
+
// Fall through to normal dispatchNextUnit — state derivation will
|
|
1024
|
+
// re-select the same unit since it hasn't been marked complete
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
807
1027
|
}
|
|
808
1028
|
|
|
809
1029
|
// In step mode, pause and show a wizard instead of immediately dispatching
|
|
@@ -949,6 +1169,7 @@ export function describeNextUnit(state: GSDState): { label: string; description:
|
|
|
949
1169
|
// ─── Progress Widget ──────────────────────────────────────────────────────
|
|
950
1170
|
|
|
951
1171
|
function unitVerb(unitType: string): string {
|
|
1172
|
+
if (unitType.startsWith("hook/")) return `hook: ${unitType.slice(5)}`;
|
|
952
1173
|
switch (unitType) {
|
|
953
1174
|
case "research-milestone":
|
|
954
1175
|
case "research-slice": return "researching";
|
|
@@ -965,6 +1186,7 @@ function unitVerb(unitType: string): string {
|
|
|
965
1186
|
}
|
|
966
1187
|
|
|
967
1188
|
function unitPhaseLabel(unitType: string): string {
|
|
1189
|
+
if (unitType.startsWith("hook/")) return "HOOK";
|
|
968
1190
|
switch (unitType) {
|
|
969
1191
|
case "research-milestone": return "RESEARCH";
|
|
970
1192
|
case "research-slice": return "RESEARCH";
|
|
@@ -981,7 +1203,14 @@ function unitPhaseLabel(unitType: string): string {
|
|
|
981
1203
|
}
|
|
982
1204
|
|
|
983
1205
|
function peekNext(unitType: string, state: GSDState): string {
|
|
1206
|
+
// Show active hook info in progress display
|
|
1207
|
+
const activeHookState = getActiveHook();
|
|
1208
|
+
if (activeHookState) {
|
|
1209
|
+
return `hook: ${activeHookState.hookName} (cycle ${activeHookState.cycle})`;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
984
1212
|
const sid = state.activeSlice?.id ?? "";
|
|
1213
|
+
if (unitType.startsWith("hook/")) return `continue ${sid}`;
|
|
985
1214
|
switch (unitType) {
|
|
986
1215
|
case "research-milestone": return "plan milestone roadmap";
|
|
987
1216
|
case "plan-milestone": return "plan or execute first slice";
|
|
@@ -1275,6 +1504,12 @@ async function dispatchNextUnit(
|
|
|
1275
1504
|
return;
|
|
1276
1505
|
}
|
|
1277
1506
|
|
|
1507
|
+
// Clear stale directory listing cache so deriveState sees fresh disk state (#431)
|
|
1508
|
+
clearPathCache();
|
|
1509
|
+
// Clear parsed roadmap/plan cache — doctor may have re-populated it with
|
|
1510
|
+
// stale data between handleAgentEnd and this dispatch call (Path B fix).
|
|
1511
|
+
clearParseCache();
|
|
1512
|
+
|
|
1278
1513
|
let state = await deriveState(basePath);
|
|
1279
1514
|
let mid = state.activeMilestone?.id;
|
|
1280
1515
|
let midTitle = state.activeMilestone?.title;
|
|
@@ -1288,8 +1523,9 @@ async function dispatchNextUnit(
|
|
|
1288
1523
|
// Reset stuck detection for new milestone
|
|
1289
1524
|
unitDispatchCount.clear();
|
|
1290
1525
|
unitRecoveryCount.clear();
|
|
1526
|
+
unitLifetimeDispatches.clear();
|
|
1291
1527
|
// Capture integration branch for the new milestone and update git service
|
|
1292
|
-
captureIntegrationBranch(basePath, mid);
|
|
1528
|
+
captureIntegrationBranch(originalBasePath || basePath, mid);
|
|
1293
1529
|
}
|
|
1294
1530
|
if (mid) {
|
|
1295
1531
|
currentMilestoneId = mid;
|
|
@@ -1338,6 +1574,8 @@ async function dispatchNextUnit(
|
|
|
1338
1574
|
}
|
|
1339
1575
|
// Re-derive state from the now-merged working tree
|
|
1340
1576
|
invalidateStateCache();
|
|
1577
|
+
clearParseCache();
|
|
1578
|
+
clearPathCache();
|
|
1341
1579
|
state = await deriveState(basePath);
|
|
1342
1580
|
mid = state.activeMilestone?.id;
|
|
1343
1581
|
midTitle = state.activeMilestone?.title;
|
|
@@ -1392,10 +1630,17 @@ async function dispatchNextUnit(
|
|
|
1392
1630
|
if (sliceEntry?.done) {
|
|
1393
1631
|
try {
|
|
1394
1632
|
const sliceTitleForMerge = sliceEntry.title || branchSid;
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1633
|
+
let mergeResult;
|
|
1634
|
+
if (isInAutoWorktree(basePath) && getMergeToMainMode() !== "slice") {
|
|
1635
|
+
mergeResult = mergeSliceToMilestone(
|
|
1636
|
+
basePath, branchMid, branchSid, sliceTitleForMerge,
|
|
1637
|
+
);
|
|
1638
|
+
} else {
|
|
1639
|
+
switchToMain(basePath);
|
|
1640
|
+
mergeResult = mergeSliceToMain(
|
|
1641
|
+
basePath, branchMid, branchSid, sliceTitleForMerge,
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1399
1644
|
const targetBranch = getMainBranch(basePath);
|
|
1400
1645
|
ctx.ui.notify(
|
|
1401
1646
|
`Merged ${mergeResult.branch} → ${targetBranch}.`,
|
|
@@ -1403,6 +1648,8 @@ async function dispatchNextUnit(
|
|
|
1403
1648
|
);
|
|
1404
1649
|
// Re-derive state from main so downstream logic sees merged state
|
|
1405
1650
|
invalidateStateCache();
|
|
1651
|
+
clearParseCache();
|
|
1652
|
+
clearPathCache();
|
|
1406
1653
|
state = await deriveState(basePath);
|
|
1407
1654
|
mid = state.activeMilestone?.id;
|
|
1408
1655
|
midTitle = state.activeMilestone?.title;
|
|
@@ -1457,7 +1704,7 @@ async function dispatchNextUnit(
|
|
|
1457
1704
|
}
|
|
1458
1705
|
|
|
1459
1706
|
// Non-conflict errors: reset and stop
|
|
1460
|
-
const message = error instanceof Error ? error
|
|
1707
|
+
const message = formatGitError(error instanceof Error ? error : String(error));
|
|
1461
1708
|
try {
|
|
1462
1709
|
const status = runGit(basePath, ["status", "--porcelain"], { allowFailure: true });
|
|
1463
1710
|
if (status && (status.includes("UU ") || status.includes("AA ") || status.includes("UD "))) {
|
|
@@ -1514,6 +1761,27 @@ async function dispatchNextUnit(
|
|
|
1514
1761
|
if (existsSync(file)) writeFileSync(file, JSON.stringify([]), "utf-8");
|
|
1515
1762
|
completedKeySet.clear();
|
|
1516
1763
|
} catch { /* non-fatal */ }
|
|
1764
|
+
|
|
1765
|
+
// ── Milestone merge: squash-merge milestone branch to main before stopping ──
|
|
1766
|
+
if (currentMilestoneId && isInAutoWorktree(basePath) && originalBasePath && getMergeToMainMode() === "milestone") {
|
|
1767
|
+
try {
|
|
1768
|
+
const roadmapPath = resolveMilestoneFile(originalBasePath, currentMilestoneId, "ROADMAP");
|
|
1769
|
+
const roadmapContent = readFileSync(roadmapPath, "utf-8");
|
|
1770
|
+
const mergeResult = mergeMilestoneToMain(originalBasePath, currentMilestoneId, roadmapContent);
|
|
1771
|
+
basePath = originalBasePath;
|
|
1772
|
+
gitService = new GitServiceImpl(basePath, loadEffectiveGSDPreferences()?.preferences?.git ?? {});
|
|
1773
|
+
ctx.ui.notify(
|
|
1774
|
+
`Milestone ${currentMilestoneId} merged to main.${mergeResult.pushed ? " Pushed to remote." : ""}`,
|
|
1775
|
+
"info",
|
|
1776
|
+
);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
ctx.ui.notify(
|
|
1779
|
+
`Milestone merge failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1780
|
+
"warning",
|
|
1781
|
+
);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1517
1785
|
await stopAuto(ctx, pi);
|
|
1518
1786
|
return;
|
|
1519
1787
|
}
|
|
@@ -1715,6 +1983,28 @@ async function dispatchNextUnit(
|
|
|
1715
1983
|
}
|
|
1716
1984
|
}
|
|
1717
1985
|
|
|
1986
|
+
// ── Pre-dispatch hooks: modify, skip, or replace the unit before dispatch ──
|
|
1987
|
+
const preDispatchResult = runPreDispatchHooks(unitType, unitId, prompt, basePath);
|
|
1988
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
1989
|
+
ctx.ui.notify(
|
|
1990
|
+
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1991
|
+
"info",
|
|
1992
|
+
);
|
|
1993
|
+
}
|
|
1994
|
+
if (preDispatchResult.action === "skip") {
|
|
1995
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
1996
|
+
// Yield then re-dispatch to advance to next unit
|
|
1997
|
+
await new Promise(r => setImmediate(r));
|
|
1998
|
+
await dispatchNextUnit(ctx, pi);
|
|
1999
|
+
return;
|
|
2000
|
+
}
|
|
2001
|
+
if (preDispatchResult.action === "replace") {
|
|
2002
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
2003
|
+
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
2004
|
+
} else if (preDispatchResult.prompt) {
|
|
2005
|
+
prompt = preDispatchResult.prompt;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1718
2008
|
const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
|
|
1719
2009
|
if (priorSliceBlocker) {
|
|
1720
2010
|
await stopAuto(ctx, pi);
|
|
@@ -1754,6 +2044,26 @@ async function dispatchNextUnit(
|
|
|
1754
2044
|
// Pattern A→B→A→B would reset retryCount every time; this map catches it.
|
|
1755
2045
|
const dispatchKey = `${unitType}/${unitId}`;
|
|
1756
2046
|
const prevCount = unitDispatchCount.get(dispatchKey) ?? 0;
|
|
2047
|
+
|
|
2048
|
+
// Hard lifetime cap — survives counter resets from loop-recovery/self-repair.
|
|
2049
|
+
// Catches the case where reconciliation "succeeds" (artifacts exist) but
|
|
2050
|
+
// deriveState keeps returning the same unit, creating an infinite cycle.
|
|
2051
|
+
const lifetimeCount = (unitLifetimeDispatches.get(dispatchKey) ?? 0) + 1;
|
|
2052
|
+
unitLifetimeDispatches.set(dispatchKey, lifetimeCount);
|
|
2053
|
+
if (lifetimeCount > MAX_LIFETIME_DISPATCHES) {
|
|
2054
|
+
if (currentUnit) {
|
|
2055
|
+
const modelId = ctx.model?.id ?? "unknown";
|
|
2056
|
+
snapshotUnitMetrics(ctx, currentUnit.type, currentUnit.id, currentUnit.startedAt, modelId);
|
|
2057
|
+
}
|
|
2058
|
+
saveActivityLog(ctx, basePath, unitType, unitId);
|
|
2059
|
+
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
2060
|
+
await stopAuto(ctx, pi);
|
|
2061
|
+
ctx.ui.notify(
|
|
2062
|
+
`Hard loop detected: ${unitType} ${unitId} dispatched ${lifetimeCount} times total (across reconciliation cycles). Stopping.${expected ? `\n Expected artifact: ${expected}` : ""}\n This may indicate deriveState() keeps returning the same unit despite artifacts existing.\n Check .gsd/completed-units.json and the slice plan checkbox state.`,
|
|
2063
|
+
"error",
|
|
2064
|
+
);
|
|
2065
|
+
return;
|
|
2066
|
+
}
|
|
1757
2067
|
if (prevCount >= MAX_UNIT_DISPATCHES) {
|
|
1758
2068
|
if (currentUnit) {
|
|
1759
2069
|
const modelId = ctx.model?.id ?? "unknown";
|
|
@@ -1778,7 +2088,13 @@ async function dispatchNextUnit(
|
|
|
1778
2088
|
`Loop recovery: ${unitId} reconciled after ${prevCount + 1} dispatches — blocker artifacts written, pipeline advancing.\n Review ${status.summaryPath} and replace the placeholder with real work.`,
|
|
1779
2089
|
"warning",
|
|
1780
2090
|
);
|
|
2091
|
+
// Persist completion so idempotency check prevents re-dispatch
|
|
2092
|
+
// if deriveState keeps returning this unit (#462).
|
|
2093
|
+
const reconciledKey = `${unitType}/${unitId}`;
|
|
2094
|
+
persistCompletedKey(basePath, reconciledKey);
|
|
2095
|
+
completedKeySet.add(reconciledKey);
|
|
1781
2096
|
unitDispatchCount.delete(dispatchKey);
|
|
2097
|
+
invalidateStateCache();
|
|
1782
2098
|
await new Promise(r => setImmediate(r));
|
|
1783
2099
|
await dispatchNextUnit(ctx, pi);
|
|
1784
2100
|
return;
|
|
@@ -1787,6 +2103,30 @@ async function dispatchNextUnit(
|
|
|
1787
2103
|
}
|
|
1788
2104
|
}
|
|
1789
2105
|
|
|
2106
|
+
// General reconciliation: if the last attempt DID produce the expected
|
|
2107
|
+
// artifact on disk, clear the counter and advance instead of stopping.
|
|
2108
|
+
// The execute-task path above handles its special case (writing placeholder
|
|
2109
|
+
// summaries). This catch-all covers complete-slice, plan-slice,
|
|
2110
|
+
// research-slice, and all other unit types where the Nth attempt at the
|
|
2111
|
+
// dispatch limit succeeded but the counter check fires before anyone
|
|
2112
|
+
// verifies disk state. Without this, a successful final attempt is
|
|
2113
|
+
// indistinguishable from a failed one.
|
|
2114
|
+
if (verifyExpectedArtifact(unitType, unitId, basePath)) {
|
|
2115
|
+
ctx.ui.notify(
|
|
2116
|
+
`Loop recovery: ${unitType} ${unitId} — artifact verified after ${prevCount + 1} dispatches. Advancing.`,
|
|
2117
|
+
"info",
|
|
2118
|
+
);
|
|
2119
|
+
// Persist completion so the idempotency check prevents re-dispatch
|
|
2120
|
+
// if deriveState keeps returning this unit (see #462).
|
|
2121
|
+
persistCompletedKey(basePath, dispatchKey);
|
|
2122
|
+
completedKeySet.add(dispatchKey);
|
|
2123
|
+
unitDispatchCount.delete(dispatchKey);
|
|
2124
|
+
invalidateStateCache();
|
|
2125
|
+
await new Promise(r => setImmediate(r));
|
|
2126
|
+
await dispatchNextUnit(ctx, pi);
|
|
2127
|
+
return;
|
|
2128
|
+
}
|
|
2129
|
+
|
|
1790
2130
|
const expected = diagnoseExpectedArtifact(unitType, unitId, basePath);
|
|
1791
2131
|
const remediation = buildLoopRemediationSteps(unitType, unitId, basePath);
|
|
1792
2132
|
await stopAuto(ctx, pi);
|
|
@@ -1813,7 +2153,12 @@ async function dispatchNextUnit(
|
|
|
1813
2153
|
`Self-repaired ${unitId}: summary existed but checkbox was unmarked. Marked [x] and advancing.`,
|
|
1814
2154
|
"warning",
|
|
1815
2155
|
);
|
|
2156
|
+
// Persist completion so idempotency check prevents re-dispatch (#462).
|
|
2157
|
+
const repairedKey = `${unitType}/${unitId}`;
|
|
2158
|
+
persistCompletedKey(basePath, repairedKey);
|
|
2159
|
+
completedKeySet.add(repairedKey);
|
|
1816
2160
|
unitDispatchCount.delete(dispatchKey);
|
|
2161
|
+
invalidateStateCache();
|
|
1817
2162
|
await new Promise(r => setImmediate(r));
|
|
1818
2163
|
await dispatchNextUnit(ctx, pi);
|
|
1819
2164
|
return;
|
|
@@ -2285,6 +2630,7 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
|
|
|
2285
2630
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2286
2631
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
2287
2632
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2633
|
+
inlined.push(inlineTemplate("research", "Research"));
|
|
2288
2634
|
|
|
2289
2635
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2290
2636
|
|
|
@@ -2317,6 +2663,11 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
|
|
|
2317
2663
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2318
2664
|
const decisionsInline = await inlineGsdRootFile(base, "decisions.md", "Decisions");
|
|
2319
2665
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2666
|
+
inlined.push(inlineTemplate("roadmap", "Roadmap"));
|
|
2667
|
+
inlined.push(inlineTemplate("decisions", "Decisions"));
|
|
2668
|
+
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
2669
|
+
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
2670
|
+
inlined.push(inlineTemplate("secrets-manifest", "Secrets Manifest"));
|
|
2320
2671
|
|
|
2321
2672
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2322
2673
|
|
|
@@ -2353,6 +2704,7 @@ async function buildResearchSlicePrompt(
|
|
|
2353
2704
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2354
2705
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
2355
2706
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2707
|
+
inlined.push(inlineTemplate("research", "Research"));
|
|
2356
2708
|
|
|
2357
2709
|
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
2358
2710
|
|
|
@@ -2388,6 +2740,8 @@ async function buildPlanSlicePrompt(
|
|
|
2388
2740
|
if (decisionsInline) inlined.push(decisionsInline);
|
|
2389
2741
|
const requirementsInline = await inlineGsdRootFile(base, "requirements.md", "Requirements");
|
|
2390
2742
|
if (requirementsInline) inlined.push(requirementsInline);
|
|
2743
|
+
inlined.push(inlineTemplate("plan", "Slice Plan"));
|
|
2744
|
+
inlined.push(inlineTemplate("task-plan", "Task Plan"));
|
|
2391
2745
|
|
|
2392
2746
|
const depContent = await inlineDependencySummaries(mid, sid, base);
|
|
2393
2747
|
|
|
@@ -2449,6 +2803,10 @@ async function buildExecuteTaskPrompt(
|
|
|
2449
2803
|
);
|
|
2450
2804
|
|
|
2451
2805
|
const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
|
|
2806
|
+
const inlinedTemplates = [
|
|
2807
|
+
inlineTemplate("task-summary", "Task Summary"),
|
|
2808
|
+
inlineTemplate("decisions", "Decisions"),
|
|
2809
|
+
].join("\n\n---\n\n");
|
|
2452
2810
|
|
|
2453
2811
|
const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
|
|
2454
2812
|
|
|
@@ -2463,6 +2821,7 @@ async function buildExecuteTaskPrompt(
|
|
|
2463
2821
|
resumeSection,
|
|
2464
2822
|
priorTaskLines: priorLines,
|
|
2465
2823
|
taskSummaryPath,
|
|
2824
|
+
inlinedTemplates,
|
|
2466
2825
|
});
|
|
2467
2826
|
}
|
|
2468
2827
|
|
|
@@ -2495,6 +2854,8 @@ async function buildCompleteSlicePrompt(
|
|
|
2495
2854
|
}
|
|
2496
2855
|
}
|
|
2497
2856
|
}
|
|
2857
|
+
inlined.push(inlineTemplate("slice-summary", "Slice Summary"));
|
|
2858
|
+
inlined.push(inlineTemplate("uat", "UAT"));
|
|
2498
2859
|
|
|
2499
2860
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2500
2861
|
|
|
@@ -2547,6 +2908,7 @@ async function buildCompleteMilestonePrompt(
|
|
|
2547
2908
|
const contextRel = relMilestoneFile(base, mid, "CONTEXT");
|
|
2548
2909
|
const contextInline = await inlineFileOptional(contextPath, contextRel, "Milestone Context");
|
|
2549
2910
|
if (contextInline) inlined.push(contextInline);
|
|
2911
|
+
inlined.push(inlineTemplate("milestone-summary", "Milestone Summary"));
|
|
2550
2912
|
|
|
2551
2913
|
const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
|
|
2552
2914
|
|
|
@@ -2993,6 +3355,9 @@ async function collectObservabilityWarnings(
|
|
|
2993
3355
|
unitType: string,
|
|
2994
3356
|
unitId: string,
|
|
2995
3357
|
): Promise<import("./observability-validator.ts").ValidationIssue[]> {
|
|
3358
|
+
// Hook units have custom artifacts — skip standard observability checks
|
|
3359
|
+
if (unitType.startsWith("hook/")) return [];
|
|
3360
|
+
|
|
2996
3361
|
const parts = unitId.split("/");
|
|
2997
3362
|
const mid = parts[0];
|
|
2998
3363
|
const sid = parts[1];
|
|
@@ -3404,6 +3769,9 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
|
|
|
3404
3769
|
* skipped writing the UAT file (see #176).
|
|
3405
3770
|
*/
|
|
3406
3771
|
export function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
|
|
3772
|
+
// Clear stale directory listing cache so artifact checks see fresh disk state (#431)
|
|
3773
|
+
clearPathCache();
|
|
3774
|
+
|
|
3407
3775
|
// fix-merge has no file artifact — verify by checking git state
|
|
3408
3776
|
if (unitType === "fix-merge") {
|
|
3409
3777
|
const unmerged = runGit(base, ["diff", "--name-only", "--diff-filter=U"], { allowFailure: true });
|