@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- 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 +6 -6
- 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/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 +6 -6
- 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/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -355,6 +355,23 @@ export function externalProjectsRoot(): string {
|
|
|
355
355
|
return join(base, "projects");
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
/**
|
|
359
|
+
* Check whether this project already has authoritative external state.
|
|
360
|
+
*
|
|
361
|
+
* Used by legacy `.gsd/` migration to avoid re-copying a re-materialized local
|
|
362
|
+
* directory over newer state when an already-migrated symlink was replaced.
|
|
363
|
+
*/
|
|
364
|
+
export function externalStateAlreadyExistsForProject(basePath: string): boolean {
|
|
365
|
+
const base = process.env.GSD_STATE_DIR || gsdHome();
|
|
366
|
+
const computedPath = join(base, "projects", repoIdentity(basePath));
|
|
367
|
+
if (hasProjectState(computedPath)) return true;
|
|
368
|
+
|
|
369
|
+
const markerId = readGsdIdMarker(resolveGitRoot(basePath));
|
|
370
|
+
if (!markerId) return false;
|
|
371
|
+
|
|
372
|
+
return hasProjectState(join(base, "projects", markerId));
|
|
373
|
+
}
|
|
374
|
+
|
|
358
375
|
// ─── Numbered Variant Cleanup ────────────────────────────────────────────────
|
|
359
376
|
|
|
360
377
|
/**
|
|
@@ -65,11 +65,12 @@ export function captureRootDirtySnapshot(rootPath: string): RootDirtySnapshot {
|
|
|
65
65
|
for (const line of status.split("\n")) {
|
|
66
66
|
if (!line.trim()) continue;
|
|
67
67
|
const code = line.slice(0, 2);
|
|
68
|
+
if (code !== "??") continue;
|
|
68
69
|
const path = line.slice(3).replace(/^"|"$/g, "");
|
|
69
70
|
if (!path || isRootRuntimePath(path)) continue;
|
|
70
71
|
snapshot.set(path, {
|
|
71
72
|
path,
|
|
72
|
-
status: code
|
|
73
|
+
status: code,
|
|
73
74
|
fingerprint: fileFingerprint(rootPath, path),
|
|
74
75
|
});
|
|
75
76
|
}
|
|
@@ -87,7 +88,7 @@ export function detectRootWriteLeak(input: {
|
|
|
87
88
|
const leaked: RootDirtyEntry[] = [];
|
|
88
89
|
for (const entry of after.values()) {
|
|
89
90
|
const prior = input.before?.get(entry.path);
|
|
90
|
-
if (!prior || prior.
|
|
91
|
+
if (!prior || prior.fingerprint !== entry.fingerprint) {
|
|
91
92
|
leaked.push(entry);
|
|
92
93
|
}
|
|
93
94
|
}
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
isSessionSwitchAbortGraceActive,
|
|
23
23
|
} from "../auto/resolve.js";
|
|
24
24
|
import { runUnit, shouldDeferUnitFailsafeTimeout } from "../auto/run-unit.js";
|
|
25
|
-
import { scheduleAutoWakeup, _resetAutoWakeupsForTest } from "../auto/schedule-wakeup.js";
|
|
25
|
+
import { consumeAutoWakeup, scheduleAutoWakeup, _resetAutoWakeupsForTest } from "../auto/schedule-wakeup.js";
|
|
26
26
|
import { writeUnitRuntimeRecord, readUnitRuntimeRecord } from "../unit-runtime.js";
|
|
27
27
|
import { autoLoop as rawAutoLoop } from "../auto/loop.js";
|
|
28
28
|
import { runPreDispatch } from "../auto/pre-dispatch.js";
|
|
@@ -67,6 +67,76 @@ async function drainMicrotasks(turns = 20): Promise<void> {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
function guardedExitMilestoneForTest(
|
|
71
|
+
merge: (milestoneId: string, opts: { merge: boolean }) => {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
merged?: boolean;
|
|
74
|
+
codeFilesChanged?: boolean;
|
|
75
|
+
reason?: string;
|
|
76
|
+
cause?: unknown;
|
|
77
|
+
},
|
|
78
|
+
) {
|
|
79
|
+
return (
|
|
80
|
+
milestoneId: string,
|
|
81
|
+
opts: {
|
|
82
|
+
merge: boolean;
|
|
83
|
+
guardedMerge?: {
|
|
84
|
+
projectRoot: string;
|
|
85
|
+
preflightCleanRoot: (
|
|
86
|
+
basePath: string,
|
|
87
|
+
milestoneId: string,
|
|
88
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
89
|
+
) => { blocked?: boolean; blockedReason?: string; stashPushed: boolean; stashMarker?: string };
|
|
90
|
+
postflightPopStash: (
|
|
91
|
+
basePath: string,
|
|
92
|
+
milestoneId: string,
|
|
93
|
+
stashMarker: string | undefined,
|
|
94
|
+
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
95
|
+
) => { needsManualRecovery?: boolean };
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
ctx?: { notify: (message: string, level: "info" | "warning" | "error") => void },
|
|
99
|
+
) => {
|
|
100
|
+
const notify = ctx?.notify ?? (() => {});
|
|
101
|
+
const guard = opts.guardedMerge;
|
|
102
|
+
if (opts.merge && guard) {
|
|
103
|
+
const preflight = guard.preflightCleanRoot(guard.projectRoot, milestoneId, notify);
|
|
104
|
+
if (preflight.blocked) {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
reason: preflight.blockedReason?.startsWith("unmerged-conflicts")
|
|
108
|
+
? "preflight-unmerged-conflicts"
|
|
109
|
+
: "preflight-dirty-overlap",
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const mergeResult = merge(milestoneId, { merge: true });
|
|
114
|
+
const postflight = preflight.stashPushed
|
|
115
|
+
? guard.postflightPopStash(
|
|
116
|
+
guard.projectRoot,
|
|
117
|
+
milestoneId,
|
|
118
|
+
preflight.stashMarker,
|
|
119
|
+
notify,
|
|
120
|
+
)
|
|
121
|
+
: undefined;
|
|
122
|
+
|
|
123
|
+
if (!mergeResult.ok) {
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
reason: mergeResult.reason === "merge-conflict" ? "merge-conflict" : "merge-failed",
|
|
127
|
+
cause: mergeResult.cause,
|
|
128
|
+
postflight,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
if (postflight?.needsManualRecovery) {
|
|
132
|
+
return { ok: false, reason: "postflight-stash-restore-failed", postflight };
|
|
133
|
+
}
|
|
134
|
+
return mergeResult;
|
|
135
|
+
}
|
|
136
|
+
return merge(milestoneId, opts);
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
70
140
|
async function waitForMicrotasks(
|
|
71
141
|
condition: () => boolean,
|
|
72
142
|
label: string,
|
|
@@ -482,6 +552,62 @@ test("runUnit honors ScheduleWakeup by continuing the same unit session", async
|
|
|
482
552
|
assert.equal(pi.calls.length, 2);
|
|
483
553
|
});
|
|
484
554
|
|
|
555
|
+
test("runUnit clears scheduled wakeups when the unit is cancelled", async () => {
|
|
556
|
+
_resetPendingResolve();
|
|
557
|
+
_resetAutoWakeupsForTest();
|
|
558
|
+
|
|
559
|
+
const ctx = {
|
|
560
|
+
...makeMockCtx(),
|
|
561
|
+
ui: {
|
|
562
|
+
notify: () => {},
|
|
563
|
+
setStatus: () => {},
|
|
564
|
+
setWorkingMessage: () => {},
|
|
565
|
+
},
|
|
566
|
+
sessionManager: {
|
|
567
|
+
getEntries: () => [],
|
|
568
|
+
},
|
|
569
|
+
modelRegistry: {
|
|
570
|
+
getProviderAuthMode: () => undefined,
|
|
571
|
+
isProviderRequestReady: () => true,
|
|
572
|
+
},
|
|
573
|
+
} as any;
|
|
574
|
+
const pi = makeMockPi();
|
|
575
|
+
const s = makeMockSession();
|
|
576
|
+
|
|
577
|
+
const resultPromise = runUnit(
|
|
578
|
+
ctx,
|
|
579
|
+
pi,
|
|
580
|
+
s,
|
|
581
|
+
"execute-task",
|
|
582
|
+
"M001/S01/T02",
|
|
583
|
+
"submit external job",
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
await waitForMicrotasks(() => pi.calls.length === 1, "initial unit dispatch");
|
|
587
|
+
scheduleAutoWakeup({
|
|
588
|
+
basePath: s.basePath,
|
|
589
|
+
unitType: "execute-task",
|
|
590
|
+
unitId: "M001/S01/T02",
|
|
591
|
+
delayMs: 0,
|
|
592
|
+
prompt: "stale prompt from cancelled unit",
|
|
593
|
+
reason: "poll external job",
|
|
594
|
+
createdAt: Date.now(),
|
|
595
|
+
});
|
|
596
|
+
resolveAgentEndCancelled({
|
|
597
|
+
message: "Auto-mode paused",
|
|
598
|
+
category: "aborted",
|
|
599
|
+
isTransient: true,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const result = await resultPromise;
|
|
603
|
+
assert.equal(result.status, "cancelled");
|
|
604
|
+
assert.equal(
|
|
605
|
+
consumeAutoWakeup(s.basePath, "execute-task", "M001/S01/T02"),
|
|
606
|
+
null,
|
|
607
|
+
"cancelled units must not leave stale ScheduleWakeup prompts for later retries",
|
|
608
|
+
);
|
|
609
|
+
});
|
|
610
|
+
|
|
485
611
|
test("runUnit suppresses the global working-message loader for auto dashboard runs", async () => {
|
|
486
612
|
_resetPendingResolve();
|
|
487
613
|
const workingMessages: unknown[] = [];
|
|
@@ -1335,11 +1461,11 @@ function makeMockDeps(
|
|
|
1335
1461
|
GitServiceImpl: class {} as any,
|
|
1336
1462
|
lifecycle: {
|
|
1337
1463
|
enterMilestone: () => ({ ok: true, mode: "worktree", path: "/tmp/project" }),
|
|
1338
|
-
exitMilestone: (_mid
|
|
1464
|
+
exitMilestone: guardedExitMilestoneForTest((_mid, opts) => ({
|
|
1339
1465
|
ok: true,
|
|
1340
1466
|
merged: opts.merge,
|
|
1341
1467
|
codeFilesChanged: false,
|
|
1342
|
-
}),
|
|
1468
|
+
})),
|
|
1343
1469
|
} as any,
|
|
1344
1470
|
worktreeProjection: new WorktreeStateProjection(),
|
|
1345
1471
|
postUnitPreVerification: async () => {
|
|
@@ -1762,10 +1888,10 @@ test("autoLoop marks transition merge complete before postflight recovery stop",
|
|
|
1762
1888
|
enterMilestone: () => {
|
|
1763
1889
|
assert.fail("must not enter the next milestone after postflight recovery fails");
|
|
1764
1890
|
},
|
|
1765
|
-
exitMilestone: (_mid
|
|
1891
|
+
exitMilestone: guardedExitMilestoneForTest((_mid, opts) => {
|
|
1766
1892
|
if (opts.merge) mergeCalls += 1;
|
|
1767
1893
|
return { ok: true, merged: opts.merge, codeFilesChanged: false };
|
|
1768
|
-
},
|
|
1894
|
+
}),
|
|
1769
1895
|
} as any,
|
|
1770
1896
|
stopAuto: async (_ctx, _pi, reason) => {
|
|
1771
1897
|
deps.callLog.push("stopAuto");
|
|
@@ -61,6 +61,7 @@ async function runSuccessfulFinalize(s: AutoSession) {
|
|
|
61
61
|
},
|
|
62
62
|
stopAuto: async () => {},
|
|
63
63
|
pauseAuto: async () => {},
|
|
64
|
+
checkpointWorkflowDatabase() {},
|
|
64
65
|
updateProgressWidget() {},
|
|
65
66
|
postUnitPreVerification: async () => "continue",
|
|
66
67
|
runPostUnitVerification: async () => "continue",
|
|
@@ -410,9 +411,13 @@ test("runFinalize stops before merge when an isolated unit leaks app files into
|
|
|
410
411
|
const notifications: Array<{ message: string; level?: string }> = [];
|
|
411
412
|
const stopCalls: Array<{ reason?: string; preserve?: boolean }> = [];
|
|
412
413
|
const mergeCalls: string[] = [];
|
|
414
|
+
const checkpointCalls: string[] = [];
|
|
413
415
|
const result = await runFinalizeWithDeps(
|
|
414
416
|
s,
|
|
415
417
|
{
|
|
418
|
+
checkpointWorkflowDatabase() {
|
|
419
|
+
checkpointCalls.push("checkpoint");
|
|
420
|
+
},
|
|
416
421
|
stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string, options?: { preserveCompletedMilestoneBranch?: boolean }) => {
|
|
417
422
|
stopCalls.push({ reason, preserve: options?.preserveCompletedMilestoneBranch });
|
|
418
423
|
},
|
|
@@ -438,16 +443,53 @@ test("runFinalize stops before merge when an isolated unit leaks app files into
|
|
|
438
443
|
|
|
439
444
|
assert.equal(result.action, "break");
|
|
440
445
|
assert.equal(result.reason, "root-write-leak");
|
|
446
|
+
assert.deepEqual(checkpointCalls, ["checkpoint"], "root-write leak should flush DB before stopAuto");
|
|
441
447
|
assert.deepEqual(stopCalls, [{ reason: "Root-write leak during isolated auto-mode", preserve: true }]);
|
|
442
448
|
assert.deepEqual(mergeCalls, [], "root-write leak must stop before merge preflight");
|
|
443
449
|
const message = notifications.find((n) => n.level === "error")?.message ?? "";
|
|
444
450
|
assert.match(message, /execute-task M001\/S01\/T01/);
|
|
445
451
|
assert.match(message, /Project root:/);
|
|
446
452
|
assert.match(message, /Expected worktree:/);
|
|
447
|
-
assert.
|
|
453
|
+
assert.doesNotMatch(message, /index\.html/);
|
|
448
454
|
assert.match(message, /tests\/verify-s09\.sh/);
|
|
449
455
|
});
|
|
450
456
|
|
|
457
|
+
test("runFinalize ignores tracked root artifact changes during isolated units", async (t) => {
|
|
458
|
+
const root = mkdtempSync(join(tmpdir(), "gsd-root-leak-tracked-"));
|
|
459
|
+
const worktree = join(root, ".gsd", "worktrees", "M001");
|
|
460
|
+
t.after(() => {
|
|
461
|
+
rmSync(root, { recursive: true, force: true });
|
|
462
|
+
});
|
|
463
|
+
initRepo(root);
|
|
464
|
+
writeFileSync(join(root, "openapi.json"), "{}\n");
|
|
465
|
+
runGit(root, ["add", "openapi.json"]);
|
|
466
|
+
runGit(root, ["commit", "-m", "chore: add generated spec"]);
|
|
467
|
+
mkdirSync(worktree, { recursive: true });
|
|
468
|
+
|
|
469
|
+
const s = new AutoSession();
|
|
470
|
+
s.basePath = worktree;
|
|
471
|
+
s.originalBasePath = root;
|
|
472
|
+
s.currentMilestoneId = "M001";
|
|
473
|
+
s.rootWriteBaseline = captureRootDirtySnapshot(root);
|
|
474
|
+
s.currentUnit = {
|
|
475
|
+
type: "execute-task",
|
|
476
|
+
id: "M001/S01/T01",
|
|
477
|
+
startedAt: Date.now(),
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
writeFileSync(join(root, "openapi.json"), "{\"generated\":true}\n");
|
|
481
|
+
|
|
482
|
+
const stopCalls: string[] = [];
|
|
483
|
+
const result = await runFinalizeWithDeps(s, {
|
|
484
|
+
stopAuto: async (_ctx: unknown, _pi: unknown, reason?: string) => {
|
|
485
|
+
stopCalls.push(reason ?? "");
|
|
486
|
+
},
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
assert.equal(result.action, "next");
|
|
490
|
+
assert.deepEqual(stopCalls, []);
|
|
491
|
+
});
|
|
492
|
+
|
|
451
493
|
test("runFinalize allows root .gsd-only changes during isolated units", async (t) => {
|
|
452
494
|
const root = mkdtempSync(join(tmpdir(), "gsd-root-leak-gsd-"));
|
|
453
495
|
const worktree = join(root, ".gsd", "worktrees", "M001");
|
|
@@ -838,6 +838,60 @@ test("verifyExpectedArtifact plan-slice fails when a task plan file is missing (
|
|
|
838
838
|
}
|
|
839
839
|
});
|
|
840
840
|
|
|
841
|
+
test("verifyExpectedArtifact accepts flat-phase plan-slice with embedded tasks and no task plan files", () => {
|
|
842
|
+
const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
|
|
843
|
+
try {
|
|
844
|
+
const phaseDir = join(base, ".gsd", "phases", "01-test");
|
|
845
|
+
mkdirSync(join(phaseDir, "tasks"), { recursive: true });
|
|
846
|
+
writeFileSync(join(phaseDir, "01-01-PLAN.md"), [
|
|
847
|
+
"# S01: Test Slice",
|
|
848
|
+
"",
|
|
849
|
+
"<tasks>",
|
|
850
|
+
"- [ ] **T01**: Implement feature _(1h)_",
|
|
851
|
+
" - Files: `src/index.ts`",
|
|
852
|
+
" - Verify: pnpm test",
|
|
853
|
+
"- [ ] **T02**: Write tests _(1h)_",
|
|
854
|
+
"</tasks>",
|
|
855
|
+
].join("\n"));
|
|
856
|
+
|
|
857
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
858
|
+
assert.equal(result, true, "flat-phase embedded tasks should not require tasks/T##-PLAN.md files");
|
|
859
|
+
} finally {
|
|
860
|
+
cleanup(base);
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
test("verifyExpectedArtifact plan-slice still verifies per-task files for legacy ## Tasks plan with a stray empty <tasks> block", () => {
|
|
865
|
+
const base = makeTmpBase();
|
|
866
|
+
try {
|
|
867
|
+
const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
868
|
+
const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
869
|
+
// Legacy layout: real tasks live in a "## Tasks" section with separate
|
|
870
|
+
// per-task PLAN files, but the plan also carries an empty <tasks></tasks>
|
|
871
|
+
// block. The empty block must NOT flip the plan into flat-phase mode and
|
|
872
|
+
// skip the per-task artifact checks.
|
|
873
|
+
const planContent = [
|
|
874
|
+
"# S01: Test Slice",
|
|
875
|
+
"",
|
|
876
|
+
"<tasks>",
|
|
877
|
+
"</tasks>",
|
|
878
|
+
"",
|
|
879
|
+
"## Tasks",
|
|
880
|
+
"",
|
|
881
|
+
"- [ ] **T01: First task** `est:1h`",
|
|
882
|
+
"- [ ] **T02: Second task** `est:2h`",
|
|
883
|
+
].join("\n");
|
|
884
|
+
writeFileSync(planPath, planContent);
|
|
885
|
+
// Only T01-PLAN.md exists; T02's artifact is missing.
|
|
886
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
|
|
887
|
+
|
|
888
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
889
|
+
assert.equal(result, false, "a stray <tasks> block must not skip per-task file verification for a legacy ## Tasks plan");
|
|
890
|
+
} finally {
|
|
891
|
+
cleanup(base);
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
|
|
841
895
|
test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", () => {
|
|
842
896
|
const base = makeTmpBase();
|
|
843
897
|
try {
|