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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// GSD-2 — WorktreeResolver: encapsulates worktree path state and merge/exit lifecycle.
|
|
1
2
|
/**
|
|
2
3
|
* WorktreeResolver — encapsulates worktree path state and merge/exit lifecycle.
|
|
3
4
|
*
|
|
@@ -23,7 +24,21 @@ import { emitJournalEvent } from "./journal.js";
|
|
|
23
24
|
import { emitWorktreeCreated, emitWorktreeMerged } from "./worktree-telemetry.js";
|
|
24
25
|
import { getCollapseCadence, getMilestoneResquash, resquashMilestoneOnMain } from "./slice-cadence.js";
|
|
25
26
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
26
|
-
import { resolveWorktreeProjectRoot } from "./worktree-root.js";
|
|
27
|
+
import { resolveWorktreeProjectRoot, normalizeWorktreePathForCompare } from "./worktree-root.js";
|
|
28
|
+
import { claimMilestoneLease, releaseMilestoneLease } from "./db/milestone-leases.js";
|
|
29
|
+
|
|
30
|
+
// ─── Path Comparison Helper ────────────────────────────────────────────────
|
|
31
|
+
/**
|
|
32
|
+
* Compare two paths for physical identity, tolerating trailing slashes,
|
|
33
|
+
* symlink differences, and case variations on case-insensitive volumes.
|
|
34
|
+
*
|
|
35
|
+
* Used in place of string `===` / `!==` wherever one operand may be
|
|
36
|
+
* realpath-normalised (e.g. from the workspace registry) and the other
|
|
37
|
+
* may not be (e.g. a raw caller-supplied basePath).
|
|
38
|
+
*/
|
|
39
|
+
function isSamePath(a: string, b: string): boolean {
|
|
40
|
+
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
41
|
+
}
|
|
27
42
|
|
|
28
43
|
// ─── Dependency Interface ──────────────────────────────────────────────────
|
|
29
44
|
|
|
@@ -185,6 +200,67 @@ export class WorktreeResolver {
|
|
|
185
200
|
return;
|
|
186
201
|
}
|
|
187
202
|
|
|
203
|
+
// Phase B: claim a milestone lease before any worktree mutation. Two
|
|
204
|
+
// workers cannot enter the same milestone concurrently. Best-effort:
|
|
205
|
+
// skip if no worker registered (single-worker fallback) or DB
|
|
206
|
+
// unavailable; reuse existing lease if we already hold it on this
|
|
207
|
+
// milestone (re-entry within the same session).
|
|
208
|
+
if (this.s.workerId) {
|
|
209
|
+
if (this.s.currentMilestoneId === milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
210
|
+
// Already held — no-op, the heartbeat in loop.ts refreshes TTL.
|
|
211
|
+
} else {
|
|
212
|
+
// If we held a different milestone, release it first so other
|
|
213
|
+
// workers don't have to wait for TTL.
|
|
214
|
+
if (this.s.currentMilestoneId && this.s.currentMilestoneId !== milestoneId && this.s.milestoneLeaseToken !== null) {
|
|
215
|
+
try {
|
|
216
|
+
releaseMilestoneLease(this.s.workerId, this.s.currentMilestoneId, this.s.milestoneLeaseToken);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
debugLog("WorktreeResolver", {
|
|
219
|
+
action: "enterMilestone",
|
|
220
|
+
milestoneId,
|
|
221
|
+
releasePriorLeaseError: err instanceof Error ? err.message : String(err),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
this.s.milestoneLeaseToken = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const claim = claimMilestoneLease(this.s.workerId, milestoneId);
|
|
229
|
+
if (claim.ok) {
|
|
230
|
+
this.s.milestoneLeaseToken = claim.token;
|
|
231
|
+
debugLog("WorktreeResolver", {
|
|
232
|
+
action: "enterMilestone",
|
|
233
|
+
milestoneId,
|
|
234
|
+
leaseAcquired: true,
|
|
235
|
+
fencingToken: claim.token,
|
|
236
|
+
expiresAt: claim.expiresAt,
|
|
237
|
+
});
|
|
238
|
+
} else {
|
|
239
|
+
// Lease held by another worker — fail loud so the user can
|
|
240
|
+
// see the conflict instead of silently double-running.
|
|
241
|
+
const msg = `Milestone ${milestoneId} is held by worker ${claim.byWorker} until ${claim.expiresAt}.`;
|
|
242
|
+
debugLog("WorktreeResolver", {
|
|
243
|
+
action: "enterMilestone",
|
|
244
|
+
milestoneId,
|
|
245
|
+
leaseHeldByOther: claim.byWorker,
|
|
246
|
+
expiresAt: claim.expiresAt,
|
|
247
|
+
});
|
|
248
|
+
ctx.notify(`${msg} Another auto-mode worker is active. Stop it before entering ${milestoneId}.`, "error");
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
} catch (err) {
|
|
252
|
+
// DB unavailable or other error — log and fall through to the
|
|
253
|
+
// pre-Phase-B single-worker behavior so a fresh project before
|
|
254
|
+
// DB init still works.
|
|
255
|
+
debugLog("WorktreeResolver", {
|
|
256
|
+
action: "enterMilestone",
|
|
257
|
+
milestoneId,
|
|
258
|
+
leaseError: err instanceof Error ? err.message : String(err),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
188
264
|
// Resolve the project root for worktree operations via shared helper.
|
|
189
265
|
// Handles the case where originalBasePath is falsy and basePath is itself
|
|
190
266
|
// a worktree path — prevents double-nested worktree paths (#3729).
|
|
@@ -589,7 +665,7 @@ export class WorktreeResolver {
|
|
|
589
665
|
milestoneId,
|
|
590
666
|
"ROADMAP",
|
|
591
667
|
);
|
|
592
|
-
if (!roadmapPath && this.s.basePath
|
|
668
|
+
if (!roadmapPath && !isSamePath(this.s.basePath, originalBase)) {
|
|
593
669
|
roadmapPath = this.deps.resolveMilestoneFile(
|
|
594
670
|
this.s.basePath,
|
|
595
671
|
milestoneId,
|
|
@@ -91,9 +91,9 @@ function matchesBlockedPattern(path: string): boolean {
|
|
|
91
91
|
* Directs the agent to use engine tool calls instead.
|
|
92
92
|
*/
|
|
93
93
|
export const BLOCKED_WRITE_ERROR = `Direct writes to .gsd/STATE.md and .gsd/gsd.db are blocked. Use engine tool calls instead:
|
|
94
|
-
- To complete a task: call
|
|
95
|
-
- To complete a slice: call
|
|
96
|
-
- To save a decision: call
|
|
94
|
+
- To complete a task: call gsd_task_complete(milestone_id, slice_id, task_id, summary)
|
|
95
|
+
- To complete a slice: call gsd_slice_complete(milestone_id, slice_id, summary, uat_result)
|
|
96
|
+
- To save a decision: call gsd_decision_save(scope, decision, choice, rationale)
|
|
97
97
|
- To start a task: call gsd_start_task(milestone_id, slice_id, task_id)
|
|
98
98
|
- To record verification: call gsd_record_verification(milestone_id, slice_id, task_id, evidence)
|
|
99
99
|
- To report a blocker: call gsd_report_blocker(milestone_id, slice_id, task_id, description)`;
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
-
import { createRequire } from "node:module";
|
|
5
|
-
import { join } from "node:path";
|
|
6
|
-
import { tmpdir } from "node:os";
|
|
7
|
-
|
|
8
|
-
import { writeLock, readCrashLock, clearLock, isLockProcessAlive } from "../crash-recovery.ts";
|
|
9
|
-
import { acquireSessionLock, releaseSessionLock } from "../session-lock.ts";
|
|
10
|
-
|
|
11
|
-
const require = createRequire(import.meta.url);
|
|
12
|
-
|
|
13
|
-
function hasProperLockfile(): boolean {
|
|
14
|
-
try {
|
|
15
|
-
require("proper-lockfile");
|
|
16
|
-
return true;
|
|
17
|
-
} catch {
|
|
18
|
-
return false;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const properLockfileAvailable = hasProperLockfile();
|
|
23
|
-
|
|
24
|
-
// ─── writeLock creates auto.lock in .gsd/ ────────────────────────────────
|
|
25
|
-
|
|
26
|
-
test("writeLock creates auto.lock with correct structure", () => {
|
|
27
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
28
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
29
|
-
|
|
30
|
-
writeLock(dir, "starting", "M001");
|
|
31
|
-
|
|
32
|
-
const lockPath = join(dir, ".gsd", "auto.lock");
|
|
33
|
-
assert.ok(existsSync(lockPath), "auto.lock should exist after writeLock");
|
|
34
|
-
|
|
35
|
-
const data = JSON.parse(readFileSync(lockPath, "utf-8"));
|
|
36
|
-
assert.equal(data.pid, process.pid, "lock should contain current PID");
|
|
37
|
-
assert.equal(data.unitType, "starting", "lock should contain unit type");
|
|
38
|
-
assert.equal(data.unitId, "M001", "lock should contain unit ID");
|
|
39
|
-
assert.ok(data.startedAt, "lock should have startedAt timestamp");
|
|
40
|
-
|
|
41
|
-
rmSync(dir, { recursive: true, force: true });
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test("writeLock updates existing lock with new unit info", () => {
|
|
45
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
46
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
47
|
-
|
|
48
|
-
writeLock(dir, "starting", "M001");
|
|
49
|
-
writeLock(dir, "execute-task", "M001/S01/T01", "/tmp/session.jsonl");
|
|
50
|
-
|
|
51
|
-
const data = JSON.parse(readFileSync(join(dir, ".gsd", "auto.lock"), "utf-8"));
|
|
52
|
-
assert.equal(data.unitType, "execute-task", "lock should be updated to new unit type");
|
|
53
|
-
assert.equal(data.unitId, "M001/S01/T01", "lock should be updated to new unit ID");
|
|
54
|
-
assert.equal(data.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
|
|
55
|
-
|
|
56
|
-
rmSync(dir, { recursive: true, force: true });
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ─── readCrashLock reads auto.lock data ──────────────────────────────────
|
|
60
|
-
|
|
61
|
-
test("readCrashLock returns null when no lock file exists", () => {
|
|
62
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
63
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
64
|
-
|
|
65
|
-
const lock = readCrashLock(dir);
|
|
66
|
-
assert.equal(lock, null, "should return null when no lock file");
|
|
67
|
-
|
|
68
|
-
rmSync(dir, { recursive: true, force: true });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("readCrashLock returns lock data when file exists", () => {
|
|
72
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
73
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
74
|
-
|
|
75
|
-
writeLock(dir, "plan-milestone", "M002");
|
|
76
|
-
const lock = readCrashLock(dir);
|
|
77
|
-
|
|
78
|
-
assert.ok(lock, "should return lock data");
|
|
79
|
-
assert.equal(lock!.unitType, "plan-milestone");
|
|
80
|
-
assert.equal(lock!.unitId, "M002");
|
|
81
|
-
|
|
82
|
-
rmSync(dir, { recursive: true, force: true });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ─── clearLock removes auto.lock ─────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
test("clearLock removes the lock file", () => {
|
|
88
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
89
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
90
|
-
|
|
91
|
-
writeLock(dir, "starting", "M001");
|
|
92
|
-
assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "lock should exist before clear");
|
|
93
|
-
|
|
94
|
-
clearLock(dir);
|
|
95
|
-
assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "lock should be removed after clear");
|
|
96
|
-
|
|
97
|
-
rmSync(dir, { recursive: true, force: true });
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("clearLock is safe when no lock file exists", () => {
|
|
101
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
102
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
103
|
-
|
|
104
|
-
// Should not throw
|
|
105
|
-
clearLock(dir);
|
|
106
|
-
|
|
107
|
-
rmSync(dir, { recursive: true, force: true });
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test("bootstrap cleanup releases session lock artifacts", (t) => {
|
|
111
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
112
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
113
|
-
|
|
114
|
-
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
115
|
-
|
|
116
|
-
const result = acquireSessionLock(dir);
|
|
117
|
-
assert.equal(result.acquired, true, "session lock should be acquired");
|
|
118
|
-
assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should exist while lock is held");
|
|
119
|
-
if (properLockfileAvailable) {
|
|
120
|
-
assert.ok(existsSync(join(dir, ".gsd.lock")), ".gsd.lock should exist while lock is held");
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
releaseSessionLock(dir);
|
|
124
|
-
clearLock(dir);
|
|
125
|
-
|
|
126
|
-
assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should be removed by bootstrap cleanup");
|
|
127
|
-
assert.ok(!existsSync(join(dir, ".gsd.lock")), ".gsd.lock should be removed by bootstrap cleanup");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ─── isLockProcessAlive detects live vs dead PIDs ────────────────────────
|
|
131
|
-
|
|
132
|
-
test("isLockProcessAlive returns false for dead PID", () => {
|
|
133
|
-
const lock = {
|
|
134
|
-
pid: 9999999,
|
|
135
|
-
startedAt: new Date().toISOString(),
|
|
136
|
-
unitType: "execute-task",
|
|
137
|
-
unitId: "M001/S01/T01",
|
|
138
|
-
unitStartedAt: new Date().toISOString(),
|
|
139
|
-
};
|
|
140
|
-
assert.equal(isLockProcessAlive(lock), false, "dead PID should return false");
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("#2470: isLockProcessAlive returns true for own PID (we hold the lock)", () => {
|
|
144
|
-
const lock = {
|
|
145
|
-
pid: process.pid,
|
|
146
|
-
startedAt: new Date().toISOString(),
|
|
147
|
-
unitType: "execute-task",
|
|
148
|
-
unitId: "M001/S01/T01",
|
|
149
|
-
unitStartedAt: new Date().toISOString(),
|
|
150
|
-
};
|
|
151
|
-
assert.equal(isLockProcessAlive(lock), true, "own PID means we are alive — not stale (#2470)");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("isLockProcessAlive returns false for invalid PID", () => {
|
|
155
|
-
const lock = {
|
|
156
|
-
pid: -1,
|
|
157
|
-
startedAt: new Date().toISOString(),
|
|
158
|
-
unitType: "execute-task",
|
|
159
|
-
unitId: "M001/S01/T01",
|
|
160
|
-
unitStartedAt: new Date().toISOString(),
|
|
161
|
-
};
|
|
162
|
-
assert.equal(isLockProcessAlive(lock), false, "negative PID should return false");
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ─── Cross-process detection via lock file ───────────────────────────────
|
|
166
|
-
|
|
167
|
-
test("lock file enables cross-process auto-mode detection", () => {
|
|
168
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
169
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
170
|
-
|
|
171
|
-
// Use the parent process PID — guaranteed alive on all platforms (Unix and Windows).
|
|
172
|
-
// PID 1 (init) only works on Unix; on Windows it doesn't exist.
|
|
173
|
-
const alivePid = process.ppid;
|
|
174
|
-
const lockData = {
|
|
175
|
-
pid: alivePid,
|
|
176
|
-
startedAt: new Date().toISOString(),
|
|
177
|
-
unitType: "execute-task",
|
|
178
|
-
unitId: "M001/S01/T02",
|
|
179
|
-
unitStartedAt: new Date().toISOString(),
|
|
180
|
-
};
|
|
181
|
-
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
182
|
-
|
|
183
|
-
const lock = readCrashLock(dir);
|
|
184
|
-
assert.ok(lock, "should read the lock");
|
|
185
|
-
assert.equal(lock!.pid, alivePid);
|
|
186
|
-
|
|
187
|
-
// Parent PID is always alive — isLockProcessAlive should detect it
|
|
188
|
-
const alive = isLockProcessAlive(lock!);
|
|
189
|
-
assert.equal(alive, true, "parent PID should be detected as alive");
|
|
190
|
-
|
|
191
|
-
rmSync(dir, { recursive: true, force: true });
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
test("stale lock from dead process is detected as not alive", () => {
|
|
195
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
|
|
196
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
197
|
-
|
|
198
|
-
// Simulate a stale lock from a process that no longer exists
|
|
199
|
-
const lockData = {
|
|
200
|
-
pid: 9999999,
|
|
201
|
-
startedAt: "2026-03-01T00:00:00Z",
|
|
202
|
-
unitType: "plan-slice",
|
|
203
|
-
unitId: "M001/S02",
|
|
204
|
-
unitStartedAt: "2026-03-01T00:05:00Z",
|
|
205
|
-
};
|
|
206
|
-
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
207
|
-
|
|
208
|
-
const lock = readCrashLock(dir);
|
|
209
|
-
assert.ok(lock, "should read the stale lock");
|
|
210
|
-
assert.equal(isLockProcessAlive(lock!), false, "dead process should not be alive");
|
|
211
|
-
|
|
212
|
-
rmSync(dir, { recursive: true, force: true });
|
|
213
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, mkdtempSync, writeFileSync, existsSync, rmSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
|
|
7
|
-
import { writeLock, readCrashLock, clearLock } from "../crash-recovery.ts";
|
|
8
|
-
import { checkRemoteAutoSession, stopAutoRemote } from "../auto.ts";
|
|
9
|
-
|
|
10
|
-
function makeTmpProject(): string {
|
|
11
|
-
const dir = mkdtempSync(join(tmpdir(), "gsd-stale-lock-test-"));
|
|
12
|
-
mkdirSync(join(dir, ".gsd"), { recursive: true });
|
|
13
|
-
return dir;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ─── checkRemoteAutoSession: own-PID filtering (#2730) ───────────────────
|
|
17
|
-
|
|
18
|
-
test("#2730: checkRemoteAutoSession returns { running: false } when lock PID matches current process", (t) => {
|
|
19
|
-
const dir = makeTmpProject();
|
|
20
|
-
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
21
|
-
|
|
22
|
-
// Write a lock with the current process PID — simulates a stale lock
|
|
23
|
-
// left behind after step-mode exit without full cleanup.
|
|
24
|
-
writeLock(dir, "execute-task", "M001/S01/T01");
|
|
25
|
-
|
|
26
|
-
const lock = readCrashLock(dir);
|
|
27
|
-
assert.ok(lock, "lock file should exist");
|
|
28
|
-
assert.equal(lock!.pid, process.pid, "lock should have our PID");
|
|
29
|
-
|
|
30
|
-
const result = checkRemoteAutoSession(dir);
|
|
31
|
-
assert.equal(result.running, false, "own PID must not be treated as a remote session");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("#2730: checkRemoteAutoSession still detects a genuine remote session (different PID)", (t) => {
|
|
35
|
-
const dir = makeTmpProject();
|
|
36
|
-
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
37
|
-
|
|
38
|
-
// Use parent PID — guaranteed alive, guaranteed not our PID.
|
|
39
|
-
const remotePid = process.ppid;
|
|
40
|
-
const lockData = {
|
|
41
|
-
pid: remotePid,
|
|
42
|
-
startedAt: new Date().toISOString(),
|
|
43
|
-
unitType: "execute-task",
|
|
44
|
-
unitId: "M001/S01/T02",
|
|
45
|
-
unitStartedAt: new Date().toISOString(),
|
|
46
|
-
};
|
|
47
|
-
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
48
|
-
|
|
49
|
-
const result = checkRemoteAutoSession(dir);
|
|
50
|
-
assert.equal(result.running, true, "different live PID should be detected as running");
|
|
51
|
-
assert.equal(result.pid, remotePid);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// ─── stopAutoRemote: self-kill prevention (#2730) ────────────────────────
|
|
55
|
-
|
|
56
|
-
test("#2730: stopAutoRemote does not send SIGTERM when lock PID matches current process", (t) => {
|
|
57
|
-
const dir = makeTmpProject();
|
|
58
|
-
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
59
|
-
|
|
60
|
-
// Write a lock with our own PID
|
|
61
|
-
writeLock(dir, "execute-task", "M001/S01/T01");
|
|
62
|
-
|
|
63
|
-
const result = stopAutoRemote(dir);
|
|
64
|
-
assert.equal(result.found, false, "own PID must not be signalled");
|
|
65
|
-
|
|
66
|
-
// The lock should be cleared as part of the self-detection cleanup
|
|
67
|
-
assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "stale self-lock should be cleared");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test("#2730: stopAutoRemote clears stale lock from dead remote process without error", (t) => {
|
|
71
|
-
const dir = makeTmpProject();
|
|
72
|
-
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
73
|
-
|
|
74
|
-
// Simulate a stale lock from a process that no longer exists
|
|
75
|
-
const lockData = {
|
|
76
|
-
pid: 9999999,
|
|
77
|
-
startedAt: "2026-03-01T00:00:00Z",
|
|
78
|
-
unitType: "plan-slice",
|
|
79
|
-
unitId: "M001/S02",
|
|
80
|
-
unitStartedAt: "2026-03-01T00:05:00Z",
|
|
81
|
-
};
|
|
82
|
-
writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
|
|
83
|
-
|
|
84
|
-
const result = stopAutoRemote(dir);
|
|
85
|
-
assert.equal(result.found, false, "dead remote PID should not be reported as found");
|
|
86
|
-
assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "stale lock should be cleaned up");
|
|
87
|
-
});
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import test from "node:test";
|
|
2
|
-
import assert from "node:assert/strict";
|
|
3
|
-
import { mkdirSync, rmSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { spawn, type ChildProcess } from "node:child_process";
|
|
8
|
-
|
|
9
|
-
import { writeFileSync } from "node:fs";
|
|
10
|
-
import {
|
|
11
|
-
writeLock,
|
|
12
|
-
readCrashLock,
|
|
13
|
-
clearLock,
|
|
14
|
-
isLockProcessAlive,
|
|
15
|
-
} from "../crash-recovery.ts";
|
|
16
|
-
import { stopAutoRemote } from "../auto.ts";
|
|
17
|
-
|
|
18
|
-
function makeTmpBase(): string {
|
|
19
|
-
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
20
|
-
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
21
|
-
return base;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function cleanup(base: string): void {
|
|
25
|
-
try { rmSync(base, { recursive: true, force: true }); } catch { /* */ }
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function waitForChildExit(child: ChildProcess, timeoutMs = 10000): Promise<number | null> {
|
|
29
|
-
return new Promise((resolve) => {
|
|
30
|
-
if (child.exitCode !== null) {
|
|
31
|
-
resolve(child.exitCode);
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const timeout = setTimeout(() => {
|
|
36
|
-
child.off("exit", onExit);
|
|
37
|
-
resolve(child.exitCode);
|
|
38
|
-
}, timeoutMs);
|
|
39
|
-
|
|
40
|
-
const onExit = (code: number | null) => {
|
|
41
|
-
clearTimeout(timeout);
|
|
42
|
-
resolve(code);
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
child.once("exit", onExit);
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ─── stopAutoRemote ──────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
test("stopAutoRemote returns found:false when no lock file exists", () => {
|
|
52
|
-
const base = makeTmpBase();
|
|
53
|
-
try {
|
|
54
|
-
const result = stopAutoRemote(base);
|
|
55
|
-
assert.equal(result.found, false);
|
|
56
|
-
assert.equal(result.pid, undefined);
|
|
57
|
-
assert.equal(result.error, undefined);
|
|
58
|
-
} finally {
|
|
59
|
-
cleanup(base);
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("stopAutoRemote cleans up stale lock (dead PID) and returns found:false", () => {
|
|
64
|
-
const base = makeTmpBase();
|
|
65
|
-
try {
|
|
66
|
-
// Write a lock with a PID that doesn't exist
|
|
67
|
-
writeLock(base, "execute-task", "M001/S01/T01");
|
|
68
|
-
// Overwrite PID to a dead one
|
|
69
|
-
const lock = readCrashLock(base)!;
|
|
70
|
-
const staleData = { ...lock, pid: 999999999 };
|
|
71
|
-
writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(staleData, null, 2), "utf-8");
|
|
72
|
-
|
|
73
|
-
const result = stopAutoRemote(base);
|
|
74
|
-
assert.equal(result.found, false, "stale lock should not be found as running");
|
|
75
|
-
|
|
76
|
-
// Lock should be cleaned up
|
|
77
|
-
assert.equal(readCrashLock(base), null, "stale lock should be removed");
|
|
78
|
-
} finally {
|
|
79
|
-
cleanup(base);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// KNOWN FLAKE: This test is timing-sensitive — it spawns a child, writes a lock file,
|
|
84
|
-
// sends SIGTERM, and asserts the child exited. Under heavy CI load the child may
|
|
85
|
-
// not be ready when SIGTERM is sent. Mitigations: 500ms startup delay, 10s exit timeout.
|
|
86
|
-
test("stopAutoRemote sends SIGTERM to a live process and returns found:true", { timeout: 15000 }, async () => {
|
|
87
|
-
const base = makeTmpBase();
|
|
88
|
-
|
|
89
|
-
// Spawn a child process that prints "ready" then sleeps, acting as a fake auto-mode session
|
|
90
|
-
const child = spawn(
|
|
91
|
-
process.execPath,
|
|
92
|
-
["-e", "process.on('SIGTERM', () => process.exit(0)); process.stdout.write('ready'); setTimeout(() => process.exit(1), 30000);"],
|
|
93
|
-
{ stdio: ["ignore", "pipe", "ignore"], detached: false },
|
|
94
|
-
);
|
|
95
|
-
|
|
96
|
-
if (!child.pid) {
|
|
97
|
-
throw new Error("failed to spawn child process for stopAutoRemote test");
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
// Wait for child to signal readiness via stdout
|
|
102
|
-
await new Promise<void>((resolve) => {
|
|
103
|
-
child.stdout!.once("data", () => resolve());
|
|
104
|
-
setTimeout(resolve, 2000); // fallback timeout
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Write lock with child's PID
|
|
108
|
-
const lockData = {
|
|
109
|
-
pid: child.pid,
|
|
110
|
-
startedAt: new Date().toISOString(),
|
|
111
|
-
unitType: "execute-task",
|
|
112
|
-
unitId: "M001/S01/T01",
|
|
113
|
-
unitStartedAt: new Date().toISOString(),
|
|
114
|
-
};
|
|
115
|
-
writeFileSync(join(base, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2), "utf-8");
|
|
116
|
-
|
|
117
|
-
const exitPromise = waitForChildExit(child);
|
|
118
|
-
const result = stopAutoRemote(base);
|
|
119
|
-
assert.equal(result.found, true, "should find running auto-mode");
|
|
120
|
-
assert.equal(result.pid, child.pid, "should return the PID");
|
|
121
|
-
|
|
122
|
-
// Wait for child to exit (it should receive SIGTERM)
|
|
123
|
-
const exitCode = await exitPromise;
|
|
124
|
-
// On Windows, SIGTERM is not interceptable — the process exits with code 1
|
|
125
|
-
// rather than running the handler. Accept either clean exit (0) or forced (1).
|
|
126
|
-
assert.ok(exitCode !== null, "child should have exited after SIGTERM");
|
|
127
|
-
if (process.platform !== "win32") {
|
|
128
|
-
assert.equal(exitCode, 0, "child should have exited cleanly via SIGTERM");
|
|
129
|
-
}
|
|
130
|
-
} finally {
|
|
131
|
-
try { child.kill("SIGKILL"); } catch { /* already dead */ }
|
|
132
|
-
cleanup(base);
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ─── Lock path: original project root vs worktree ────────────────────────
|
|
137
|
-
|
|
138
|
-
test("lock file should be discoverable from project root and worktree path", () => {
|
|
139
|
-
const projectRoot = makeTmpBase();
|
|
140
|
-
const worktreePath = join(projectRoot, ".gsd", "worktrees", "M001");
|
|
141
|
-
mkdirSync(join(worktreePath, ".gsd"), { recursive: true });
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
// Simulate: auto-mode writes lock to project root (the fix)
|
|
145
|
-
writeLock(projectRoot, "execute-task", "M001/S01/T01");
|
|
146
|
-
|
|
147
|
-
// Second terminal checks project root — should find the lock
|
|
148
|
-
const lock = readCrashLock(projectRoot);
|
|
149
|
-
assert.ok(lock, "lock should be found at project root");
|
|
150
|
-
assert.equal(lock!.unitType, "execute-task");
|
|
151
|
-
|
|
152
|
-
// Worktree path resolves to the same canonical project .gsd lock.
|
|
153
|
-
const worktreeLock = readCrashLock(worktreePath);
|
|
154
|
-
assert.ok(worktreeLock, "lock should be discoverable via worktree path");
|
|
155
|
-
assert.equal(worktreeLock!.unitType, "execute-task");
|
|
156
|
-
} finally {
|
|
157
|
-
cleanup(projectRoot);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
File without changes
|
|
File without changes
|