gsd-pi 2.78.1-dev.e9d88a536 → 2.78.1-dev.eccf86e27
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 +5 -7
- package/dist/help-text.js +1 -1
- package/dist/resource-loader.js +6 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/detect-stuck.js +41 -5
- package/dist/resources/extensions/gsd/auto/loop.js +235 -36
- package/dist/resources/extensions/gsd/auto/phases.js +14 -7
- package/dist/resources/extensions/gsd/auto/session.js +36 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +49 -4
- package/dist/resources/extensions/gsd/auto-post-unit.js +26 -12
- package/dist/resources/extensions/gsd/auto-worktree.js +185 -201
- package/dist/resources/extensions/gsd/auto.js +139 -49
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +26 -20
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
- package/dist/resources/extensions/gsd/crash-recovery.js +160 -47
- package/dist/resources/extensions/gsd/db/auto-workers.js +227 -0
- package/dist/resources/extensions/gsd/db/command-queue.js +105 -0
- package/dist/resources/extensions/gsd/db/milestone-leases.js +210 -0
- package/dist/resources/extensions/gsd/db/runtime-kv.js +91 -0
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +322 -0
- package/dist/resources/extensions/gsd/db-writer.js +96 -16
- package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
- package/dist/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +4 -0
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +22 -6
- package/dist/resources/extensions/gsd/doctor.js +12 -2
- package/dist/resources/extensions/gsd/gsd-db.js +355 -3
- package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
- package/dist/resources/extensions/gsd/guided-flow.js +116 -26
- package/dist/resources/extensions/gsd/interrupted-session.js +18 -15
- package/dist/resources/extensions/gsd/metrics.js +287 -1
- package/dist/resources/extensions/gsd/paths.js +79 -8
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/dist/resources/extensions/gsd/state.js +21 -6
- package/dist/resources/extensions/gsd/templates/project.md +10 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
- package/dist/resources/extensions/gsd/workspace.js +59 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +79 -2
- package/dist/resources/extensions/gsd/write-intercept.js +3 -3
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/README.md +2 -11
- package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
- package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
- package/packages/mcp-server/dist/remote-questions.js +28 -0
- package/packages/mcp-server/dist/remote-questions.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts +28 -0
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +94 -4
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/src/mcp-server.test.ts +226 -0
- package/packages/mcp-server/src/remote-questions.test.ts +103 -0
- package/packages/mcp-server/src/remote-questions.ts +35 -0
- package/packages/mcp-server/src/server.ts +129 -6
- package/packages/mcp-server/src/workflow-tools.ts +1 -1
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/gsd/auto/detect-stuck.ts +37 -5
- package/src/resources/extensions/gsd/auto/loop.ts +263 -41
- package/src/resources/extensions/gsd/auto/phases.ts +15 -7
- package/src/resources/extensions/gsd/auto/session.ts +40 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +63 -4
- package/src/resources/extensions/gsd/auto-post-unit.ts +27 -12
- package/src/resources/extensions/gsd/auto-worktree.ts +218 -225
- package/src/resources/extensions/gsd/auto.ts +166 -43
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +26 -21
- package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
- package/src/resources/extensions/gsd/crash-recovery.ts +177 -43
- package/src/resources/extensions/gsd/db/auto-workers.ts +273 -0
- package/src/resources/extensions/gsd/db/command-queue.ts +149 -0
- package/src/resources/extensions/gsd/db/milestone-leases.ts +274 -0
- package/src/resources/extensions/gsd/db/runtime-kv.ts +127 -0
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +446 -0
- package/src/resources/extensions/gsd/db-writer.ts +113 -17
- package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
- package/src/resources/extensions/gsd/docs/COORDINATION.md +42 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +4 -0
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +24 -6
- package/src/resources/extensions/gsd/doctor.ts +10 -2
- package/src/resources/extensions/gsd/gsd-db.ts +354 -3
- package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
- package/src/resources/extensions/gsd/guided-flow.ts +152 -26
- package/src/resources/extensions/gsd/interrupted-session.ts +19 -12
- package/src/resources/extensions/gsd/metrics.ts +321 -1
- package/src/resources/extensions/gsd/paths.ts +67 -8
- package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
- package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
- package/src/resources/extensions/gsd/state.ts +44 -6
- package/src/resources/extensions/gsd/templates/project.md +10 -0
- package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
- package/src/resources/extensions/gsd/tests/auto-loop-no-copy-artifacts.test.ts +72 -0
- package/src/resources/extensions/gsd/tests/auto-loop-symlink-worktree.test.ts +190 -0
- package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
- package/src/resources/extensions/gsd/tests/auto-workers.test.ts +105 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
- package/src/resources/extensions/gsd/tests/command-queue.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +203 -0
- package/src/resources/extensions/gsd/tests/crash-recovery.test.ts +169 -59
- package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
- package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
- package/src/resources/extensions/gsd/tests/detect-stuck-respects-retry.test.ts +173 -0
- package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
- package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
- package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
- package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
- package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
- package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
- package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +22 -12
- package/src/resources/extensions/gsd/tests/integration/doctor-proactive.test.ts +24 -10
- package/src/resources/extensions/gsd/tests/integration/doctor-runtime.test.ts +35 -23
- package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +369 -0
- package/src/resources/extensions/gsd/tests/interrupted-session-auto.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/interrupted-session-ui.test.ts +72 -25
- package/src/resources/extensions/gsd/tests/memory-pressure-stuck-state.test.ts +9 -6
- package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
- package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
- package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
- package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
- package/src/resources/extensions/gsd/tests/milestone-leases.test.ts +152 -0
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
- package/src/resources/extensions/gsd/tests/parallel-milestone-isolation.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
- package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +119 -0
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/preferences-worktree-sync.test.ts +3 -17
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +138 -16
- package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
- package/src/resources/extensions/gsd/tests/runtime-kv.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/skipped-validation-completion.test.ts +133 -28
- package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +17 -0
- package/src/resources/extensions/gsd/tests/stuck-state-via-db.test.ts +134 -0
- package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +434 -0
- package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
- package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +98 -0
- package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
- package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +247 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +41 -1
- package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/workspace.test.ts +196 -0
- package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +94 -71
- package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
- package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
- package/src/resources/extensions/gsd/workspace.ts +95 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +78 -2
- package/src/resources/extensions/gsd/write-intercept.ts +3 -3
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +0 -213
- package/src/resources/extensions/gsd/tests/auto-stale-lock-self-kill.test.ts +0 -87
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +0 -159
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{oZGTPvJBQX_IDKKnuV8Bt → Y5UeGFkXTYM9WIQOWHkot}/_ssgManifest.js +0 -0
|
@@ -22,8 +22,14 @@ import type { GSDState } from "./types.js";
|
|
|
22
22
|
import {
|
|
23
23
|
assessInterruptedSession,
|
|
24
24
|
readPausedSessionMetadata,
|
|
25
|
+
PAUSED_SESSION_KV_KEY,
|
|
25
26
|
type InterruptedSessionAssessment,
|
|
27
|
+
type PausedSessionMetadata,
|
|
26
28
|
} from "./interrupted-session.js";
|
|
29
|
+
import {
|
|
30
|
+
setRuntimeKv,
|
|
31
|
+
deleteRuntimeKv,
|
|
32
|
+
} from "./db/runtime-kv.js";
|
|
27
33
|
import { getManifestStatus } from "./files.js";
|
|
28
34
|
export { inlinePriorMilestoneSummary } from "./files.js";
|
|
29
35
|
import { collectSecretsFromManifest } from "../get-secrets-from-user.js";
|
|
@@ -244,6 +250,7 @@ import type {
|
|
|
244
250
|
CurrentUnit,
|
|
245
251
|
UnitRouting,
|
|
246
252
|
StartModel,
|
|
253
|
+
AutoSession,
|
|
247
254
|
} from "./auto/session.js";
|
|
248
255
|
export {
|
|
249
256
|
STUB_RECOVERY_THRESHOLD,
|
|
@@ -256,6 +263,10 @@ export type {
|
|
|
256
263
|
} from "./auto/session.js";
|
|
257
264
|
import { autoSession as s } from "./auto-runtime-state.js";
|
|
258
265
|
import { gsdHome } from "./gsd-home.js";
|
|
266
|
+
import { createWorkspace, scopeMilestone } from "./workspace.js";
|
|
267
|
+
import { registerAutoWorker, markWorkerStopping } from "./db/auto-workers.js";
|
|
268
|
+
import { releaseMilestoneLease } from "./db/milestone-leases.js";
|
|
269
|
+
import { normalizeRealPath } from "./paths.js";
|
|
259
270
|
|
|
260
271
|
// ── ENCAPSULATION INVARIANT ─────────────────────────────────────────────────
|
|
261
272
|
// ALL mutable auto-mode state lives in the AutoSession class (auto/session.ts).
|
|
@@ -273,6 +284,28 @@ import { gsdHome } from "./gsd-home.js";
|
|
|
273
284
|
/** Throttle STATE.md rebuilds — at most once per 30 seconds */
|
|
274
285
|
const STATE_REBUILD_MIN_INTERVAL_MS = 30_000;
|
|
275
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Phase B — register this auto-mode process in the workers table so other
|
|
289
|
+
* workers and janitors can detect liveness via heartbeat. Best-effort: if
|
|
290
|
+
* the DB is unavailable (e.g. fresh project before init) we skip registration
|
|
291
|
+
* silently rather than blocking session start.
|
|
292
|
+
*/
|
|
293
|
+
function registerAutoWorkerForSession(session: AutoSession): void {
|
|
294
|
+
if (session.workerId) return; // already registered (e.g. resume re-runs)
|
|
295
|
+
try {
|
|
296
|
+
const projectRootRealpath = normalizeRealPath(
|
|
297
|
+
session.scope?.workspace.projectRoot
|
|
298
|
+
?? (session.originalBasePath || session.basePath),
|
|
299
|
+
);
|
|
300
|
+
session.workerId = registerAutoWorker({ projectRootRealpath });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
debugLog("autoLoop", {
|
|
303
|
+
phase: "register-worker-failed",
|
|
304
|
+
error: err instanceof Error ? err.message : String(err),
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
276
309
|
function captureProjectRootEnv(projectRoot: string): void {
|
|
277
310
|
if (!s.projectRootEnvCaptured) {
|
|
278
311
|
s.hadProjectRootEnv = Object.prototype.hasOwnProperty.call(process.env, "GSD_PROJECT_ROOT");
|
|
@@ -324,6 +357,32 @@ function restoreMilestoneLockEnv(): void {
|
|
|
324
357
|
s.milestoneLockEnvCaptured = false;
|
|
325
358
|
}
|
|
326
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Rebuild s.scope from the current s.basePath / s.originalBasePath / s.currentMilestoneId.
|
|
362
|
+
*
|
|
363
|
+
* Pass the worktree path as rawPath when entering a worktree so createWorkspace
|
|
364
|
+
* can detect the worktree layout and set mode="worktree". When no worktree is
|
|
365
|
+
* active, rawPath should equal the project root.
|
|
366
|
+
*
|
|
367
|
+
* Clears s.scope when milestoneId is absent — scope is only meaningful when a
|
|
368
|
+
* milestone is active.
|
|
369
|
+
*
|
|
370
|
+
* TODO(C8): remove basePath/originalBasePath once all readers use s.scope.
|
|
371
|
+
*/
|
|
372
|
+
function rebuildScope(rawPath: string, milestoneId: string | null): void {
|
|
373
|
+
if (!milestoneId) {
|
|
374
|
+
s.scope = null;
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
const workspace = createWorkspace(rawPath);
|
|
379
|
+
s.scope = scopeMilestone(workspace, milestoneId);
|
|
380
|
+
} catch {
|
|
381
|
+
// Non-fatal — scope is additive. Existing readers still use basePath.
|
|
382
|
+
s.scope = null;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
327
386
|
function normalizeSessionFilePath(raw: unknown): string | null {
|
|
328
387
|
if (typeof raw !== "string") return null;
|
|
329
388
|
const trimmed = raw.trim();
|
|
@@ -509,6 +568,26 @@ export function _setAutoActiveForTest(active: boolean): void {
|
|
|
509
568
|
s.active = active;
|
|
510
569
|
}
|
|
511
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Test-only seam: emit the missing-worktree warning exactly as the resume path
|
|
573
|
+
* does. Allows unit tests to verify the warning is produced without
|
|
574
|
+
* bootstrapping the full auto-mode entry point. Do not use in production code.
|
|
575
|
+
*/
|
|
576
|
+
export function _warnIfWorktreeMissingForTest(
|
|
577
|
+
worktreePath: string | null | undefined,
|
|
578
|
+
milestoneId: string,
|
|
579
|
+
): boolean {
|
|
580
|
+
if (worktreePath && !existsSync(worktreePath)) {
|
|
581
|
+
logWarning(
|
|
582
|
+
"session",
|
|
583
|
+
`Worktree was expected at ${worktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
|
|
584
|
+
{ file: "auto.ts", milestoneId },
|
|
585
|
+
);
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
return false;
|
|
589
|
+
}
|
|
590
|
+
|
|
512
591
|
export function isAutoPaused(): boolean {
|
|
513
592
|
return s.paused;
|
|
514
593
|
}
|
|
@@ -864,6 +943,21 @@ export async function stopAuto(
|
|
|
864
943
|
debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
|
|
865
944
|
}
|
|
866
945
|
|
|
946
|
+
// ── Step 1b: Coordination cleanup (Phase B) ──
|
|
947
|
+
// Release any active milestone lease so other workers don't have to
|
|
948
|
+
// wait for TTL expiry, then mark this worker as stopping. Best-effort:
|
|
949
|
+
// DB unavailability or stale state must not block shutdown.
|
|
950
|
+
try {
|
|
951
|
+
if (s.workerId && s.currentMilestoneId && s.milestoneLeaseToken) {
|
|
952
|
+
releaseMilestoneLease(s.workerId, s.currentMilestoneId, s.milestoneLeaseToken);
|
|
953
|
+
}
|
|
954
|
+
if (s.workerId) {
|
|
955
|
+
markWorkerStopping(s.workerId);
|
|
956
|
+
}
|
|
957
|
+
} catch (e) {
|
|
958
|
+
debugLog("stop-cleanup-coordination", { error: e instanceof Error ? e.message : String(e) });
|
|
959
|
+
}
|
|
960
|
+
|
|
867
961
|
// ── Step 1b: Flush queued follow-up messages (#3512) ──
|
|
868
962
|
// Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
|
|
869
963
|
// extra LLM turns after stop. Flush them the same way run-unit.ts does.
|
|
@@ -1048,11 +1142,11 @@ export async function stopAuto(
|
|
|
1048
1142
|
}
|
|
1049
1143
|
|
|
1050
1144
|
// ── Step 12: Remove paused-session metadata (#1383) ──
|
|
1145
|
+
// Phase C pt 2: deleteRuntimeKv replaces unlinkSync(paused-session.json).
|
|
1051
1146
|
try {
|
|
1052
|
-
|
|
1053
|
-
if (existsSync(pausedPath)) unlinkSync(pausedPath);
|
|
1147
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
1054
1148
|
} catch (err) { /* non-fatal */
|
|
1055
|
-
logWarning("engine", `
|
|
1149
|
+
logWarning("engine", `paused-session DB delete failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1056
1150
|
}
|
|
1057
1151
|
|
|
1058
1152
|
// ── Step 13: Restore original model + thinking (before reset clears IDs) ──
|
|
@@ -1153,10 +1247,12 @@ export async function pauseAuto(
|
|
|
1153
1247
|
s.pausedSessionFile = normalizeSessionFilePath(ctx?.sessionManager?.getSessionFile() ?? null);
|
|
1154
1248
|
|
|
1155
1249
|
// Persist paused-session metadata so resume survives /exit (#1383).
|
|
1156
|
-
//
|
|
1250
|
+
// Phase C pt 2: persisted to runtime_kv (global scope, key
|
|
1251
|
+
// PAUSED_SESSION_KV_KEY) instead of runtime/paused-session.json. The
|
|
1252
|
+
// fresh-start bootstrap below reads from the same key.
|
|
1157
1253
|
try {
|
|
1158
|
-
const pausedMeta = {
|
|
1159
|
-
milestoneId: s.currentMilestoneId,
|
|
1254
|
+
const pausedMeta: PausedSessionMetadata = {
|
|
1255
|
+
milestoneId: s.currentMilestoneId ?? undefined,
|
|
1160
1256
|
worktreePath: isInAutoWorktree(s.basePath) ? s.basePath : null,
|
|
1161
1257
|
originalBasePath: s.originalBasePath,
|
|
1162
1258
|
stepMode: s.stepMode,
|
|
@@ -1164,20 +1260,15 @@ export async function pauseAuto(
|
|
|
1164
1260
|
sessionFile: s.pausedSessionFile,
|
|
1165
1261
|
unitType: s.currentUnit?.type ?? undefined,
|
|
1166
1262
|
unitId: s.currentUnit?.id ?? undefined,
|
|
1167
|
-
activeEngineId: s.activeEngineId,
|
|
1263
|
+
activeEngineId: s.activeEngineId ?? undefined,
|
|
1168
1264
|
activeRunDir: s.activeRunDir,
|
|
1169
1265
|
autoStartTime: s.autoStartTime,
|
|
1170
1266
|
milestoneLock: s.sessionMilestoneLock ?? undefined,
|
|
1171
1267
|
};
|
|
1172
|
-
|
|
1173
|
-
atomicWriteSync(
|
|
1174
|
-
join(runtimeDir, "paused-session.json"),
|
|
1175
|
-
JSON.stringify(pausedMeta, null, 2),
|
|
1176
|
-
"utf-8",
|
|
1177
|
-
);
|
|
1268
|
+
setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, pausedMeta);
|
|
1178
1269
|
} catch (err) {
|
|
1179
1270
|
// Non-fatal — resume will still work via full bootstrap, just without worktree context
|
|
1180
|
-
logWarning("engine", `paused-session
|
|
1271
|
+
logWarning("engine", `paused-session DB write failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1181
1272
|
}
|
|
1182
1273
|
|
|
1183
1274
|
// Close out the current unit so its runtime record doesn't stay at "dispatched"
|
|
@@ -1460,8 +1551,10 @@ export async function startAuto(
|
|
|
1460
1551
|
ctx.ui.notify("Recovered unfinished migration (.gsd.migrating → .gsd).", "info");
|
|
1461
1552
|
}
|
|
1462
1553
|
|
|
1463
|
-
const freshStartAssessment = interruptedAssessment
|
|
1464
|
-
??
|
|
1554
|
+
const freshStartAssessment = await (interruptedAssessment
|
|
1555
|
+
?? (() => {
|
|
1556
|
+
return ensureDbOpen(base).then(() => assessInterruptedSession(base));
|
|
1557
|
+
})());
|
|
1465
1558
|
|
|
1466
1559
|
if (freshStartAssessment.classification === "running") {
|
|
1467
1560
|
const pid = freshStartAssessment.lock?.pid;
|
|
@@ -1476,10 +1569,20 @@ export async function startAuto(
|
|
|
1476
1569
|
|
|
1477
1570
|
// If resuming from paused state, just re-activate and dispatch next unit.
|
|
1478
1571
|
// Check persisted paused-session first (#1383) — survives /exit.
|
|
1572
|
+
// Phase C pt 2: persisted in runtime_kv (global scope) instead of
|
|
1573
|
+
// runtime/paused-session.json. The `clearPausedSession` helper
|
|
1574
|
+
// replaces every prior unlinkSync(pausedPath) call.
|
|
1575
|
+
const clearPausedSession = (logTag: string): void => {
|
|
1576
|
+
try {
|
|
1577
|
+
deleteRuntimeKv("global", "", PAUSED_SESSION_KV_KEY);
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
logWarning("session", `${logTag}: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
|
|
1479
1583
|
if (!s.paused) {
|
|
1480
1584
|
try {
|
|
1481
1585
|
const meta = freshStartAssessment.pausedSession ?? readPausedSessionMetadata(base);
|
|
1482
|
-
const pausedPath = join(gsdRoot(base), "runtime", "paused-session.json");
|
|
1483
1586
|
if (meta?.activeEngineId && meta.activeEngineId !== "dev") {
|
|
1484
1587
|
// Custom workflow resume — restore engine state
|
|
1485
1588
|
s.activeEngineId = meta.activeEngineId;
|
|
@@ -1489,11 +1592,6 @@ export async function startAuto(
|
|
|
1489
1592
|
s.autoStartTime = meta.autoStartTime || Date.now();
|
|
1490
1593
|
s.sessionMilestoneLock = meta.milestoneLock ?? null;
|
|
1491
1594
|
s.paused = true;
|
|
1492
|
-
try { unlinkSync(pausedPath); } catch (e) {
|
|
1493
|
-
if ((e as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1494
|
-
logWarning("session", `pause file cleanup failed: ${e instanceof Error ? e.message : String(e)}`, { file: "auto.ts" });
|
|
1495
|
-
}
|
|
1496
|
-
}
|
|
1497
1595
|
ctx.ui.notify(
|
|
1498
1596
|
`Resuming paused custom workflow${meta.activeRunDir ? ` (${meta.activeRunDir})` : ""}.`,
|
|
1499
1597
|
"info",
|
|
@@ -1534,11 +1632,7 @@ export async function startAuto(
|
|
|
1534
1632
|
}
|
|
1535
1633
|
}
|
|
1536
1634
|
if (!mDir || summaryIsTerminal) {
|
|
1537
|
-
|
|
1538
|
-
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
1539
|
-
logWarning("session", `pause file cleanup failed: ${err instanceof Error ? err.message : String(err)}`, { file: "auto.ts" });
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1635
|
+
clearPausedSession("paused-session DB cleanup failed (milestone gone/complete)");
|
|
1542
1636
|
ctx.ui.notify(
|
|
1543
1637
|
`Paused milestone ${meta.milestoneId} is ${!mDir ? "missing" : "already complete"}. Starting fresh.`,
|
|
1544
1638
|
"info",
|
|
@@ -1553,22 +1647,31 @@ export async function startAuto(
|
|
|
1553
1647
|
s.autoStartTime = meta.autoStartTime || Date.now();
|
|
1554
1648
|
s.sessionMilestoneLock = meta.milestoneLock ?? null;
|
|
1555
1649
|
s.paused = true;
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1650
|
+
// Build scope from persisted state. Use worktreePath when present and
|
|
1651
|
+
// still on disk so mode is detected correctly; fall back to project root.
|
|
1652
|
+
{
|
|
1653
|
+
const persistedWorktreePath = meta.worktreePath ?? null;
|
|
1654
|
+
if (persistedWorktreePath && !existsSync(persistedWorktreePath)) {
|
|
1655
|
+
logWarning(
|
|
1656
|
+
"session",
|
|
1657
|
+
`Worktree was expected at ${persistedWorktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
|
|
1658
|
+
{ file: "auto.ts", milestoneId: meta.milestoneId ?? "" },
|
|
1659
|
+
);
|
|
1559
1660
|
}
|
|
1661
|
+
const rawForScope = (persistedWorktreePath && existsSync(persistedWorktreePath))
|
|
1662
|
+
? persistedWorktreePath
|
|
1663
|
+
: (s.originalBasePath || base);
|
|
1664
|
+
rebuildScope(rawForScope, s.currentMilestoneId);
|
|
1560
1665
|
}
|
|
1561
1666
|
ctx.ui.notify(
|
|
1562
1667
|
`Resuming paused session for ${meta.milestoneId}${meta.worktreePath && existsSync(meta.worktreePath) ? ` (worktree)` : ""}.`,
|
|
1563
1668
|
"info",
|
|
1564
1669
|
);
|
|
1565
1670
|
}
|
|
1566
|
-
} else if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1671
|
+
} else if (meta) {
|
|
1672
|
+
// Stale paused-session metadata that the assessment chose not to
|
|
1673
|
+
// resume — clean it up so the next bootstrap starts fresh.
|
|
1674
|
+
clearPausedSession("stale paused-session DB cleanup failed");
|
|
1572
1675
|
}
|
|
1573
1676
|
}
|
|
1574
1677
|
} catch (err) {
|
|
@@ -1637,10 +1740,19 @@ export async function startAuto(
|
|
|
1637
1740
|
// session (e.g. isolation mode changed, detectWorktreeName differs across
|
|
1638
1741
|
// process restarts). We guard with existsSync so a stale or deleted
|
|
1639
1742
|
// worktree directory safely falls back to the project root.
|
|
1640
|
-
const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath;
|
|
1743
|
+
const resumeWorktreePath = freshStartAssessment.pausedSession?.worktreePath ?? null;
|
|
1744
|
+
if (resumeWorktreePath && !existsSync(resumeWorktreePath)) {
|
|
1745
|
+
logWarning(
|
|
1746
|
+
"session",
|
|
1747
|
+
`Worktree was expected at ${resumeWorktreePath} but is missing. Continuing in project-root mode. To restart with a fresh worktree, run /gsd-debug or recreate the milestone.`,
|
|
1748
|
+
{ file: "auto.ts", milestoneId: s.currentMilestoneId ?? "" },
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1641
1751
|
if (resumeWorktreePath && existsSync(resumeWorktreePath)) {
|
|
1642
1752
|
s.basePath = resumeWorktreePath;
|
|
1643
1753
|
}
|
|
1754
|
+
// Rebuild scope now that s.basePath reflects the actual worktree (or project root).
|
|
1755
|
+
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1644
1756
|
// Ensure the workflow-logger audit log is pinned to the project root
|
|
1645
1757
|
// even when auto-mode is entered via a path that bypasses the
|
|
1646
1758
|
// bootstrap/dynamic-tools ensureDbOpen() → setLogBasePath() chain
|
|
@@ -1669,6 +1781,8 @@ export async function startAuto(
|
|
|
1669
1781
|
buildResolver().enterMilestone(s.currentMilestoneId, {
|
|
1670
1782
|
notify: ctx.ui.notify.bind(ctx.ui),
|
|
1671
1783
|
});
|
|
1784
|
+
// s.basePath may have been updated to a worktree path by enterMilestone.
|
|
1785
|
+
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1672
1786
|
}
|
|
1673
1787
|
|
|
1674
1788
|
registerSigtermHandler(lockBase());
|
|
@@ -1737,19 +1851,23 @@ export async function startAuto(
|
|
|
1737
1851
|
s.pausedSessionFile = null;
|
|
1738
1852
|
}
|
|
1739
1853
|
|
|
1854
|
+
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1855
|
+
registerAutoWorkerForSession(s);
|
|
1740
1856
|
updateSessionLock(
|
|
1741
1857
|
lockBase(),
|
|
1742
1858
|
"resuming",
|
|
1743
1859
|
s.currentMilestoneId ?? "unknown",
|
|
1744
1860
|
);
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1861
|
+
if (s.workerId) {
|
|
1862
|
+
writeLock(
|
|
1863
|
+
lockBase(),
|
|
1864
|
+
"resuming",
|
|
1865
|
+
s.currentMilestoneId ?? "unknown",
|
|
1866
|
+
);
|
|
1867
|
+
clearPausedSession("paused-session DB cleanup failed (resume activation)");
|
|
1868
|
+
}
|
|
1750
1869
|
pi.events.emit(CMUX_CHANNELS.LOG, { preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, message: s.stepMode ? "Step-mode resumed." : "Auto-mode resumed.", level: "progress" });
|
|
1751
1870
|
|
|
1752
|
-
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1753
1871
|
startAutoCommandPolling(s.basePath);
|
|
1754
1872
|
await runAutoLoopWithUok({
|
|
1755
1873
|
ctx,
|
|
@@ -1783,7 +1901,12 @@ export async function startAuto(
|
|
|
1783
1901
|
);
|
|
1784
1902
|
if (!ready) return;
|
|
1785
1903
|
|
|
1904
|
+
// Build scope after bootstrap has populated s.basePath / s.originalBasePath /
|
|
1905
|
+
// s.currentMilestoneId (including worktree setup inside bootstrapAutoSession).
|
|
1906
|
+
rebuildScope(s.basePath, s.currentMilestoneId);
|
|
1907
|
+
|
|
1786
1908
|
captureProjectRootEnv(s.originalBasePath || s.basePath);
|
|
1909
|
+
registerAutoWorkerForSession(s);
|
|
1787
1910
|
try {
|
|
1788
1911
|
pi.events.emit(CMUX_CHANNELS.SIDEBAR, { action: "sync" as const, preferences: loadEffectiveGSDPreferences(s.basePath || undefined)?.preferences, state: await deriveState(s.basePath) });
|
|
1789
1912
|
} catch (err) {
|
|
@@ -76,7 +76,7 @@ export function registerHooks(
|
|
|
76
76
|
const { initHealthWidget } = await import("../health-widget.js");
|
|
77
77
|
initHealthWidget(ctx);
|
|
78
78
|
}
|
|
79
|
-
resetWriteGateState();
|
|
79
|
+
resetWriteGateState(process.cwd());
|
|
80
80
|
resetToolCallLoopGuard();
|
|
81
81
|
approvalQuestionAbortInFlight = false;
|
|
82
82
|
await resetAskUserQuestionsTurnCache();
|
|
@@ -126,10 +126,10 @@ export function registerHooks(
|
|
|
126
126
|
pi.on("session_switch", async (_event, ctx) => {
|
|
127
127
|
initNotificationStore(process.cwd());
|
|
128
128
|
installNotifyInterceptor(ctx);
|
|
129
|
-
resetWriteGateState();
|
|
129
|
+
resetWriteGateState(process.cwd());
|
|
130
130
|
resetToolCallLoopGuard();
|
|
131
131
|
await resetAskUserQuestionsTurnCache();
|
|
132
|
-
clearDiscussionFlowState();
|
|
132
|
+
clearDiscussionFlowState(process.cwd());
|
|
133
133
|
await syncServiceTierStatus(ctx);
|
|
134
134
|
await applyDisabledModelProviderPolicy(ctx);
|
|
135
135
|
// Skip MCP auto-prep when running inside an auto-worktree. The worktree
|
|
@@ -155,12 +155,13 @@ export function registerHooks(
|
|
|
155
155
|
const { getEcosystemReadyPromise } = await import("../ecosystem/loader.js");
|
|
156
156
|
await getEcosystemReadyPromise();
|
|
157
157
|
|
|
158
|
+
const beforeAgentBasePath = process.cwd();
|
|
158
159
|
const pendingApprovalGate = getPendingGate();
|
|
159
160
|
if (pendingApprovalGate && isExplicitApprovalResponse(event.prompt, pendingApprovalGate)) {
|
|
160
|
-
markApprovalGateVerified(pendingApprovalGate);
|
|
161
|
+
markApprovalGateVerified(pendingApprovalGate, beforeAgentBasePath);
|
|
161
162
|
const milestoneId = extractDepthVerificationMilestoneId(pendingApprovalGate);
|
|
162
|
-
if (milestoneId) markDepthVerified(milestoneId);
|
|
163
|
-
clearPendingGate();
|
|
163
|
+
if (milestoneId) markDepthVerified(milestoneId, beforeAgentBasePath);
|
|
164
|
+
clearPendingGate(beforeAgentBasePath);
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
// GSD's own context injection (existing behavior — unchanged).
|
|
@@ -346,7 +347,7 @@ export function registerHooks(
|
|
|
346
347
|
if (!shouldPauseForUserApprovalQuestion(unitType, [event.message])) return;
|
|
347
348
|
|
|
348
349
|
const gateId = approvalGateIdForUnit(unitType, unitId);
|
|
349
|
-
if (gateId) setPendingGate(gateId);
|
|
350
|
+
if (gateId) setPendingGate(gateId, process.cwd());
|
|
350
351
|
|
|
351
352
|
approvalQuestionAbortInFlight = true;
|
|
352
353
|
ctx.ui.notify(
|
|
@@ -393,7 +394,7 @@ export function registerHooks(
|
|
|
393
394
|
const questions: any[] = (event.input as any)?.questions ?? [];
|
|
394
395
|
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
|
395
396
|
if (typeof questionId === "string") {
|
|
396
|
-
setPendingGate(questionId);
|
|
397
|
+
setPendingGate(questionId, discussionBasePath);
|
|
397
398
|
}
|
|
398
399
|
}
|
|
399
400
|
|
|
@@ -555,7 +556,8 @@ export function registerHooks(
|
|
|
555
556
|
}
|
|
556
557
|
const toolName = canonicalToolName(event.toolName);
|
|
557
558
|
if (toolName !== "ask_user_questions") return;
|
|
558
|
-
const
|
|
559
|
+
const basePath = process.cwd();
|
|
560
|
+
const milestoneId = await getDiscussionMilestoneIdFor(basePath);
|
|
559
561
|
const queueActive = isQueuePhaseActive();
|
|
560
562
|
|
|
561
563
|
const details = event.details as any;
|
|
@@ -569,8 +571,13 @@ export function registerHooks(
|
|
|
569
571
|
const currentPendingGate = getPendingGate();
|
|
570
572
|
if (currentPendingGate) {
|
|
571
573
|
if (details?.cancelled || !details?.response) {
|
|
572
|
-
// Gate stays pending.
|
|
573
|
-
//
|
|
574
|
+
// Gate stays pending. Direct the agent to the most reliable recovery
|
|
575
|
+
// path — re-calling ask_user_questions with the same gate id — without
|
|
576
|
+
// misrepresenting the plain-text path. The plain-text path also works
|
|
577
|
+
// (isExplicitApprovalResponse on the next before_agent_start clears
|
|
578
|
+
// the gate when the user replies with an approval keyword), but the
|
|
579
|
+
// structured re-ask is more deterministic and gives the user a clear UI.
|
|
580
|
+
resetToolCallLoopGuard();
|
|
574
581
|
return {
|
|
575
582
|
content: [{
|
|
576
583
|
type: "text" as const,
|
|
@@ -578,8 +585,8 @@ export function registerHooks(
|
|
|
578
585
|
`HARD BLOCK: approval gate "${currentPendingGate}" is still pending.`,
|
|
579
586
|
"No user response was received for the confirmation question.",
|
|
580
587
|
"Do not infer approval from earlier or prior messages.",
|
|
581
|
-
"Do not proceed, write files, save artifacts, or call
|
|
582
|
-
|
|
588
|
+
"Do not proceed, write files, save artifacts, or call other tools.",
|
|
589
|
+
`Re-call ask_user_questions with the same gate question id ("${currentPendingGate}") and wait for the user's response.`,
|
|
583
590
|
].join(" "),
|
|
584
591
|
}],
|
|
585
592
|
};
|
|
@@ -588,10 +595,10 @@ export function registerHooks(
|
|
|
588
595
|
if (pendingQuestion) {
|
|
589
596
|
const answer = details.response?.answers?.[currentPendingGate];
|
|
590
597
|
if (isDepthConfirmationAnswer(answer?.selected, pendingQuestion.options)) {
|
|
591
|
-
markApprovalGateVerified(currentPendingGate);
|
|
598
|
+
markApprovalGateVerified(currentPendingGate, basePath);
|
|
592
599
|
const milestoneIdFromGate = extractDepthVerificationMilestoneId(currentPendingGate);
|
|
593
|
-
if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate);
|
|
594
|
-
clearPendingGate();
|
|
600
|
+
if (milestoneIdFromGate) markDepthVerified(milestoneIdFromGate, basePath);
|
|
601
|
+
clearPendingGate(basePath);
|
|
595
602
|
}
|
|
596
603
|
}
|
|
597
604
|
}
|
|
@@ -607,9 +614,9 @@ export function registerHooks(
|
|
|
607
614
|
const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
|
|
608
615
|
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
|
609
616
|
if (currentPendingGate && question.id !== currentPendingGate) break;
|
|
610
|
-
markApprovalGateVerified(question.id);
|
|
611
|
-
markDepthVerified(inferredMilestoneId);
|
|
612
|
-
clearPendingGate();
|
|
617
|
+
markApprovalGateVerified(question.id, basePath);
|
|
618
|
+
markDepthVerified(inferredMilestoneId, basePath);
|
|
619
|
+
clearPendingGate(basePath);
|
|
613
620
|
}
|
|
614
621
|
break;
|
|
615
622
|
}
|
|
@@ -617,8 +624,6 @@ export function registerHooks(
|
|
|
617
624
|
|
|
618
625
|
if (!milestoneId && !queueActive) return;
|
|
619
626
|
if (!milestoneId) return;
|
|
620
|
-
|
|
621
|
-
const basePath = process.cwd();
|
|
622
627
|
const milestoneDir = resolveMilestonePath(basePath, milestoneId);
|
|
623
628
|
if (!milestoneDir) return;
|
|
624
629
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// GSD-2 write-gate bootstrap — regression test for required basePath (commit A3)
|
|
2
|
+
//
|
|
3
|
+
// Verifies that persistWriteGateSnapshot / loadWriteGateSnapshot are pinned to
|
|
4
|
+
// the basePath argument and do not silently fall back to process.cwd(). The
|
|
5
|
+
// underlying bug: both functions defaulted `basePath = process.cwd()`, so a
|
|
6
|
+
// persist in cwd-A followed by a chdir to cwd-B and a load (which also
|
|
7
|
+
// defaulted to process.cwd(), now cwd-B) missed the persisted file entirely —
|
|
8
|
+
// the depth-verification state became invisible across cwd boundaries.
|
|
9
|
+
|
|
10
|
+
import { test, describe, before, after } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, rmSync, existsSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
markDepthVerified,
|
|
18
|
+
loadWriteGateSnapshot,
|
|
19
|
+
clearDiscussionFlowState,
|
|
20
|
+
} from "../write-gate.js";
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function makeTempDir(): string {
|
|
25
|
+
return mkdtempSync(join(tmpdir(), "wg-basepath-test-"));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Save and restore process.cwd() across tests to avoid cross-test pollution.
|
|
29
|
+
let originalCwd: string;
|
|
30
|
+
before(() => {
|
|
31
|
+
originalCwd = process.cwd();
|
|
32
|
+
});
|
|
33
|
+
after(() => {
|
|
34
|
+
if (process.cwd() !== originalCwd) {
|
|
35
|
+
process.chdir(originalCwd);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── Scenario: persist with basePath=A, chdir, load with basePath=A ─────────
|
|
40
|
+
//
|
|
41
|
+
// This is the exact failure mode from the bug: persist used process.cwd() and
|
|
42
|
+
// load used process.cwd(), and they resolved to different directories after a
|
|
43
|
+
// chdir. With the fix, both calls receive an explicit basePath so cwd changes
|
|
44
|
+
// have no effect.
|
|
45
|
+
|
|
46
|
+
describe("write-gate basePath regression", () => {
|
|
47
|
+
let baseDirA: string;
|
|
48
|
+
let baseDirB: string;
|
|
49
|
+
|
|
50
|
+
before(() => {
|
|
51
|
+
baseDirA = makeTempDir();
|
|
52
|
+
baseDirB = makeTempDir();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
after(() => {
|
|
56
|
+
// Restore cwd before cleanup to avoid issues on Windows.
|
|
57
|
+
process.chdir(originalCwd);
|
|
58
|
+
rmSync(baseDirA, { recursive: true, force: true });
|
|
59
|
+
rmSync(baseDirB, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("snapshot persisted to basePath=A is readable after chdir to basePath=B", (t) => {
|
|
63
|
+
// Arrange: enable persistence (the default when env var is not set to "0"/"false").
|
|
64
|
+
const prev = process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
65
|
+
t.after(() => {
|
|
66
|
+
if (prev === undefined) {
|
|
67
|
+
delete process.env.GSD_PERSIST_WRITE_GATE_STATE;
|
|
68
|
+
} else {
|
|
69
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = prev;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
process.env.GSD_PERSIST_WRITE_GATE_STATE = "1";
|
|
73
|
+
|
|
74
|
+
// Reset state and clear any stale snapshot files from both dirs.
|
|
75
|
+
clearDiscussionFlowState(baseDirA);
|
|
76
|
+
clearDiscussionFlowState(baseDirB);
|
|
77
|
+
|
|
78
|
+
// Act: persist a milestone as depth-verified into baseDirA.
|
|
79
|
+
markDepthVerified("M001", baseDirA);
|
|
80
|
+
|
|
81
|
+
// Confirm the snapshot file was written under baseDirA.
|
|
82
|
+
const snapshotPath = join(baseDirA, ".gsd", "runtime", "write-gate-state.json");
|
|
83
|
+
assert.ok(existsSync(snapshotPath), "snapshot file should exist under baseDirA");
|
|
84
|
+
|
|
85
|
+
// Simulate what happens when cwd changes to a different project root.
|
|
86
|
+
process.chdir(baseDirB);
|
|
87
|
+
assert.notEqual(process.cwd(), baseDirA, "cwd should differ from baseDirA after chdir");
|
|
88
|
+
|
|
89
|
+
// Load snapshot using the explicit baseDirA — must see the persisted state.
|
|
90
|
+
const snapshot = loadWriteGateSnapshot(baseDirA);
|
|
91
|
+
assert.ok(
|
|
92
|
+
snapshot.verifiedDepthMilestones.includes("M001"),
|
|
93
|
+
"loadWriteGateSnapshot(baseDirA) must return the persisted milestone despite cwd being baseDirB",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Loading with baseDirB must NOT see the state from baseDirA.
|
|
97
|
+
const snapshotB = loadWriteGateSnapshot(baseDirB);
|
|
98
|
+
assert.ok(
|
|
99
|
+
!snapshotB.verifiedDepthMilestones.includes("M001"),
|
|
100
|
+
"loadWriteGateSnapshot(baseDirB) must not bleed state from baseDirA",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
});
|