gsd-pi 2.82.0-dev.dfbc5f58f → 2.82.0-dev.e7a7f1ed5
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 +1 -1
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +73 -30
- package/dist/resources/extensions/gsd/auto-dashboard.js +66 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +10 -16
- package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
- package/dist/resources/extensions/gsd/auto-start.js +3 -3
- package/dist/resources/extensions/gsd/auto-verification.js +17 -4
- package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
- package/dist/resources/extensions/gsd/auto.js +7 -2
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +27 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -2
- package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
- package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
- package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
- package/dist/resources/extensions/gsd/doctor-git-checks.js +46 -1
- package/dist/resources/extensions/gsd/git-service.js +6 -2
- package/dist/resources/extensions/gsd/gsd-db.js +20 -6
- package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
- package/dist/resources/extensions/gsd/guided-flow.js +95 -116
- package/dist/resources/extensions/gsd/guided-unit-context.js +23 -0
- package/dist/resources/extensions/gsd/migration-auto-check.js +12 -17
- package/dist/resources/extensions/gsd/pending-auto-start.js +52 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/dist/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/queue.md +4 -4
- package/dist/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
- package/dist/resources/extensions/gsd/smart-entry-routing.js +36 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +9 -14
- package/dist/resources/extensions/gsd/state-reconciliation/drift/roadmap.js +19 -24
- package/dist/resources/extensions/gsd/status-guards.js +7 -0
- package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
- 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 +9 -9
- 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/api/browse-directories/route.js +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 +9 -9
- 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/package.json +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
- package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
- package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +24 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +23 -7
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
- package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
- package/packages/pi-tui/dist/terminal.d.ts +2 -0
- package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
- package/packages/pi-tui/dist/terminal.js +12 -0
- package/packages/pi-tui/dist/terminal.js.map +1 -1
- package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
- package/packages/pi-tui/src/terminal.ts +11 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
- package/src/resources/extensions/gsd/auto/phases.ts +83 -37
- package/src/resources/extensions/gsd/auto-dashboard.ts +72 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +10 -16
- package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
- package/src/resources/extensions/gsd/auto-start.ts +2 -3
- package/src/resources/extensions/gsd/auto-verification.ts +22 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
- package/src/resources/extensions/gsd/auto.ts +8 -2
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +36 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -2
- package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
- package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
- package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
- package/src/resources/extensions/gsd/doctor-git-checks.ts +45 -1
- package/src/resources/extensions/gsd/doctor-types.ts +1 -0
- package/src/resources/extensions/gsd/git-service.ts +6 -3
- package/src/resources/extensions/gsd/gsd-db.ts +18 -6
- package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
- package/src/resources/extensions/gsd/guided-flow.ts +128 -133
- package/src/resources/extensions/gsd/guided-unit-context.ts +30 -0
- package/src/resources/extensions/gsd/migration-auto-check.ts +15 -23
- package/src/resources/extensions/gsd/pending-auto-start.ts +79 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +8 -8
- package/src/resources/extensions/gsd/prompts/discuss.md +9 -9
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +4 -4
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +3 -3
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/queue.md +4 -4
- package/src/resources/extensions/gsd/prompts/refine-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/rewrite-docs.md +1 -1
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
- package/src/resources/extensions/gsd/smart-entry-routing.ts +77 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +12 -15
- package/src/resources/extensions/gsd/state-reconciliation/drift/roadmap.ts +17 -25
- package/src/resources/extensions/gsd/status-guards.ts +8 -0
- package/src/resources/extensions/gsd/tests/auto-dashboard.test.ts +71 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +29 -1
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +53 -2
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
- package/src/resources/extensions/gsd/tests/auto-stop-notification.test.ts +20 -0
- package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/complete-slice.test.ts +5 -9
- package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +208 -0
- package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/doctor-empty-worktree.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
- package/src/resources/extensions/gsd/tests/guided-discuss-project-prompt-rendering.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/guided-dispatch-root.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/guided-flow-session-isolation.test.ts +59 -11
- package/src/resources/extensions/gsd/tests/guided-tool-contract.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/headless-milestone-parity.test.ts +7 -7
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +26 -18
- package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +29 -5
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +2 -0
- package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +37 -1
- package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
- package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
- package/src/resources/extensions/gsd/tests/smart-entry-routing.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +119 -23
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
- package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
- package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
- package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{q0WYuDVbHeFFYbdd-fei2 → 4dSwdrs__8NwCZggxP9KF}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Project/App: GSD-2
|
|
2
2
|
// File Purpose: ADR-017 roadmap-divergence drift handler. Detects mismatches
|
|
3
|
-
// between ROADMAP.md (parsed slice sequence
|
|
4
|
-
// DB slice rows for that milestone, then
|
|
5
|
-
//
|
|
3
|
+
// between ROADMAP.md (parsed slice sequence, depends declarations, and
|
|
4
|
+
// checkboxes) and the DB slice rows for that milestone, then re-renders the
|
|
5
|
+
// ROADMAP projection from the authoritative DB rows.
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
8
|
|
|
@@ -10,12 +10,12 @@ import {
|
|
|
10
10
|
getMilestone,
|
|
11
11
|
getMilestoneSlices,
|
|
12
12
|
isDbAvailable,
|
|
13
|
-
syncSliceDependencies,
|
|
14
13
|
} from "../../gsd-db.js";
|
|
15
|
-
import {
|
|
14
|
+
import { renderRoadmapFromDb } from "../../markdown-renderer.js";
|
|
16
15
|
import { findMilestoneIds } from "../../milestone-ids.js";
|
|
17
16
|
import { parseRoadmap } from "../../parsers-legacy.js";
|
|
18
17
|
import { resolveMilestoneFile } from "../../paths.js";
|
|
18
|
+
import { isClosedStatus } from "../../status-guards.js";
|
|
19
19
|
import type { GSDState } from "../../types.js";
|
|
20
20
|
import type { DriftContext, DriftHandler, DriftRecord } from "../types.js";
|
|
21
21
|
|
|
@@ -46,14 +46,20 @@ function milestoneHasDivergence(
|
|
|
46
46
|
|
|
47
47
|
const dbSlices = getMilestoneSlices(milestoneId);
|
|
48
48
|
const dbSliceMap = new Map(dbSlices.map((s) => [s.id, s]));
|
|
49
|
+
const roadmapSliceIds = new Set<string>();
|
|
49
50
|
|
|
50
51
|
for (let i = 0; i < roadmap.slices.length; i++) {
|
|
51
52
|
const roadmapSlice = roadmap.slices[i]!;
|
|
53
|
+
roadmapSliceIds.add(roadmapSlice.id);
|
|
52
54
|
const expectedSequence = i + 1;
|
|
53
55
|
const dbSlice = dbSliceMap.get(roadmapSlice.id);
|
|
54
56
|
if (!dbSlice) return true; // Roadmap has a slice the DB doesn't.
|
|
55
57
|
if (dbSlice.sequence !== expectedSequence) return true;
|
|
56
58
|
if (!arraysEqual(dbSlice.depends, roadmapSlice.depends)) return true;
|
|
59
|
+
if (isClosedStatus(dbSlice.status) !== roadmapSlice.done) return true;
|
|
60
|
+
}
|
|
61
|
+
for (const dbSlice of dbSlices) {
|
|
62
|
+
if (!roadmapSliceIds.has(dbSlice.id)) return true;
|
|
57
63
|
}
|
|
58
64
|
return false;
|
|
59
65
|
}
|
|
@@ -77,29 +83,15 @@ export function detectRoadmapDivergenceDrift(
|
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/**
|
|
80
|
-
* Repair a milestone's roadmap divergence
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* 2. syncSliceDependencies updates the junction table per slice — the
|
|
84
|
-
* importer only writes the JSON column, not the relational view.
|
|
86
|
+
* Repair a milestone's roadmap divergence by regenerating the projection from
|
|
87
|
+
* DB rows. ROADMAP.md is a projection; runtime reconciliation must not import
|
|
88
|
+
* slice presence, sequence, dependencies, or checkbox state from markdown.
|
|
85
89
|
*/
|
|
86
|
-
export function repairRoadmapDivergence(
|
|
90
|
+
export async function repairRoadmapDivergence(
|
|
87
91
|
record: RoadmapDivergenceDrift,
|
|
88
92
|
ctx: DriftContext,
|
|
89
|
-
): void {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const roadmapPath = resolveMilestoneFile(ctx.basePath, record.milestoneId, "ROADMAP");
|
|
93
|
-
if (!roadmapPath || !existsSync(roadmapPath)) return;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const roadmap = parseRoadmap(readFileSync(roadmapPath, "utf-8"));
|
|
97
|
-
for (const slice of roadmap.slices) {
|
|
98
|
-
syncSliceDependencies(record.milestoneId, slice.id, slice.depends);
|
|
99
|
-
}
|
|
100
|
-
} catch {
|
|
101
|
-
/* parse failure: detector will fire again next pass */
|
|
102
|
-
}
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
await renderRoadmapFromDb(ctx.basePath, record.milestoneId);
|
|
103
95
|
}
|
|
104
96
|
|
|
105
97
|
export const roadmapDivergenceHandler: DriftHandler<RoadmapDivergenceDrift> = {
|
|
@@ -30,3 +30,11 @@ export function isInactiveStatus(status: string): boolean {
|
|
|
30
30
|
export function isSkippedForDispatch(status: string): boolean {
|
|
31
31
|
return isClosedStatus(status) || status === "parked" || isDeferredStatus(status);
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns true when a milestone is future/backlog work (not currently executing).
|
|
36
|
+
* Includes legacy/project-specific alias "planned" for compatibility.
|
|
37
|
+
*/
|
|
38
|
+
export function isFutureMilestoneStatus(status: string): boolean {
|
|
39
|
+
return status === "pending" || status === "queued" || status === "planned";
|
|
40
|
+
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatWidgetTokens,
|
|
14
14
|
estimateTimeRemaining,
|
|
15
15
|
extractUatSliceId,
|
|
16
|
+
buildPhaseHandoffOutcome,
|
|
16
17
|
updateProgressWidget,
|
|
17
18
|
setAutoOutcomeWidget,
|
|
18
19
|
getRoadmapSlicesSync,
|
|
@@ -255,6 +256,76 @@ test("setAutoOutcomeWidget renders a durable next-action handoff", () => {
|
|
|
255
256
|
assert.match(output, /\/gsd auto/);
|
|
256
257
|
});
|
|
257
258
|
|
|
259
|
+
test("buildPhaseHandoffOutcome summarizes the last phase result", () => {
|
|
260
|
+
const snapshot = buildPhaseHandoffOutcome({
|
|
261
|
+
unitType: "plan-slice",
|
|
262
|
+
unitId: "M005/S01",
|
|
263
|
+
agentEndMessages: [
|
|
264
|
+
{ message: { role: "assistant", content: "Planned S01 with category-aware filtering and validation steps." } },
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
assert.equal(snapshot.status, "complete");
|
|
269
|
+
assert.equal(snapshot.title, "PLAN complete");
|
|
270
|
+
assert.match(snapshot.detail ?? "", /category-aware filtering/);
|
|
271
|
+
assert.equal(snapshot.unitLabel, "planning M005/S01");
|
|
272
|
+
assert.match(snapshot.nextAction, /next phase/);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("buildPhaseHandoffOutcome ignores non-assistant trailing messages", () => {
|
|
276
|
+
const snapshot = buildPhaseHandoffOutcome({
|
|
277
|
+
unitType: "plan-slice",
|
|
278
|
+
unitId: "M005/S01",
|
|
279
|
+
agentEndMessages: [
|
|
280
|
+
{ message: { role: "assistant", content: "Assistant summary to hand off." } },
|
|
281
|
+
{ role: "tool", content: "Tool output should not be shown." },
|
|
282
|
+
{ role: "user", content: "User follow-up should not be shown." },
|
|
283
|
+
],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
assert.match(snapshot.detail ?? "", /Assistant summary/);
|
|
287
|
+
assert.doesNotMatch(snapshot.detail ?? "", /Tool output/);
|
|
288
|
+
assert.doesNotMatch(snapshot.detail ?? "", /User follow-up/);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("updateProgressWidget preserves the phase handoff during session switching", () => {
|
|
292
|
+
const calls: Array<[string, unknown]> = [];
|
|
293
|
+
updateProgressWidget(
|
|
294
|
+
{
|
|
295
|
+
hasUI: true,
|
|
296
|
+
ui: {
|
|
297
|
+
setWidget(key: string, factory: unknown) {
|
|
298
|
+
calls.push([key, factory]);
|
|
299
|
+
},
|
|
300
|
+
setHeader() {},
|
|
301
|
+
setStatus() {},
|
|
302
|
+
},
|
|
303
|
+
} as any,
|
|
304
|
+
"execute-task",
|
|
305
|
+
"M005/S01/T01",
|
|
306
|
+
{
|
|
307
|
+
phase: "executing",
|
|
308
|
+
activeSlice: { id: "S01", title: "Filter chip bar" },
|
|
309
|
+
activeTask: { id: "T01", title: "Add category filter" },
|
|
310
|
+
} as any,
|
|
311
|
+
{
|
|
312
|
+
getAutoStartTime: () => Date.now(),
|
|
313
|
+
isStepMode: () => false,
|
|
314
|
+
getCmdCtx: () => null,
|
|
315
|
+
getBasePath: () => "",
|
|
316
|
+
isVerbose: () => false,
|
|
317
|
+
isSessionSwitching: () => true,
|
|
318
|
+
getCurrentDispatchedModelId: () => null,
|
|
319
|
+
},
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
assert.ok(calls.some(([key]) => key === "gsd-progress"));
|
|
323
|
+
assert.ok(
|
|
324
|
+
!calls.some(([key, value]) => key === "gsd-outcome" && value === undefined),
|
|
325
|
+
"handoff widget should stay visible until the next progress frame renders",
|
|
326
|
+
);
|
|
327
|
+
});
|
|
328
|
+
|
|
258
329
|
test("shouldRenderRoadmapProgress hides pre-roadmap zero-slice progress", () => {
|
|
259
330
|
assert.equal(shouldRenderRoadmapProgress(null), false);
|
|
260
331
|
assert.equal(shouldRenderRoadmapProgress({ done: 0, total: 0, activeSliceTasks: null } as any), false);
|
|
@@ -6,7 +6,7 @@ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "nod
|
|
|
6
6
|
import { tmpdir } from "node:os";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
|
|
9
|
-
import { cleanupAfterLoopExit, rerootCommandSession, stopAuto } from "../auto.ts";
|
|
9
|
+
import { cleanupAfterLoopExit, pauseAuto, rerootCommandSession, stopAuto } from "../auto.ts";
|
|
10
10
|
import { autoSession } from "../auto-runtime-state.ts";
|
|
11
11
|
import { closeDatabase, insertMilestone, insertSlice, openDatabase } from "../gsd-db.ts";
|
|
12
12
|
import { WorktreeLifecycle } from "../worktree-lifecycle.ts";
|
|
@@ -117,6 +117,34 @@ test("cleanupAfterLoopExit clears progress widget after stopAuto reset", async (
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
test("pauseAuto preserves artifact retry counts across pause/resume", async () => {
|
|
121
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-pause-retry-count-"));
|
|
122
|
+
const previousCwd = process.cwd();
|
|
123
|
+
const retryKey = "execute-task:M001/S01/T01";
|
|
124
|
+
|
|
125
|
+
autoSession.reset();
|
|
126
|
+
autoSession.active = true;
|
|
127
|
+
autoSession.verificationRetryCount.set(retryKey, 2);
|
|
128
|
+
autoSession.pendingVerificationRetry = {
|
|
129
|
+
unitId: "M001/S01/T01",
|
|
130
|
+
failureContext: "Missing expected artifact (attempt 2/3).",
|
|
131
|
+
attempt: 2,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
process.chdir(base);
|
|
136
|
+
await pauseAuto();
|
|
137
|
+
|
|
138
|
+
assert.equal(autoSession.paused, true);
|
|
139
|
+
assert.equal(autoSession.pendingVerificationRetry, null);
|
|
140
|
+
assert.equal(autoSession.verificationRetryCount.get(retryKey), 2);
|
|
141
|
+
} finally {
|
|
142
|
+
autoSession.reset();
|
|
143
|
+
process.chdir(previousCwd);
|
|
144
|
+
rmSync(base, { recursive: true, force: true });
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
120
148
|
test("cleanupAfterLoopExit restores project root through lifecycle and preserves chdir", async (t) => {
|
|
121
149
|
const base = mkdtempSync(join(tmpdir(), "gsd-cleanup-lifecycle-"));
|
|
122
150
|
const worktree = join(base, ".gsd", "worktrees", "M001");
|
|
@@ -62,7 +62,11 @@ async function runSuccessfulFinalize(s: AutoSession) {
|
|
|
62
62
|
);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
async function runFinalizeWithDeps(
|
|
65
|
+
async function runFinalizeWithDeps(
|
|
66
|
+
s: AutoSession,
|
|
67
|
+
depsOverrides: Record<string, unknown>,
|
|
68
|
+
ctxOverride?: Record<string, unknown>,
|
|
69
|
+
) {
|
|
66
70
|
const unit = s.currentUnit;
|
|
67
71
|
assert.ok(unit, "test setup must provide currentUnit");
|
|
68
72
|
|
|
@@ -86,7 +90,7 @@ async function runFinalizeWithDeps(s: AutoSession, depsOverrides: Record<string,
|
|
|
86
90
|
|
|
87
91
|
return runFinalize(
|
|
88
92
|
{
|
|
89
|
-
ctx: { ui: { notify() {} } },
|
|
93
|
+
ctx: ctxOverride ?? { ui: { notify() {} } },
|
|
90
94
|
pi: {},
|
|
91
95
|
s,
|
|
92
96
|
deps,
|
|
@@ -223,3 +227,50 @@ test("runFinalize merges a verified complete-milestone immediately and only once
|
|
|
223
227
|
assert.equal(lifecycleMergeCalls, 1);
|
|
224
228
|
assert.equal(resolverMergeCalls, 0);
|
|
225
229
|
});
|
|
230
|
+
|
|
231
|
+
test("runFinalize does not render next-phase handoff for complete-milestone", async (t) => {
|
|
232
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-finalize-complete-handoff-"));
|
|
233
|
+
t.after(() => {
|
|
234
|
+
rmSync(base, { recursive: true, force: true });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const s = new AutoSession();
|
|
238
|
+
const widgetCalls: Array<[string, unknown]> = [];
|
|
239
|
+
s.basePath = base;
|
|
240
|
+
s.originalBasePath = base;
|
|
241
|
+
s.currentMilestoneId = "M001";
|
|
242
|
+
s.currentUnit = {
|
|
243
|
+
type: "complete-milestone",
|
|
244
|
+
id: "M001",
|
|
245
|
+
startedAt: Date.now(),
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const result = await runFinalizeWithDeps(
|
|
249
|
+
s,
|
|
250
|
+
{
|
|
251
|
+
preflightCleanRoot: () => ({ stashPushed: false }),
|
|
252
|
+
postflightPopStash: () => ({ needsManualRecovery: false }),
|
|
253
|
+
lifecycle: {
|
|
254
|
+
exitMilestone() {
|
|
255
|
+
return { ok: true, merged: true, codeFilesChanged: false };
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
hasUI: true,
|
|
261
|
+
ui: {
|
|
262
|
+
notify() {},
|
|
263
|
+
setWidget(key: string, value: unknown) {
|
|
264
|
+
widgetCalls.push([key, value]);
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
assert.equal(result.action, "next");
|
|
271
|
+
assert.equal(
|
|
272
|
+
widgetCalls.some(([key]) => key === "gsd-outcome"),
|
|
273
|
+
false,
|
|
274
|
+
"complete-milestone finalize should leave terminal completion UI to stopAuto",
|
|
275
|
+
);
|
|
276
|
+
});
|
|
@@ -807,6 +807,51 @@ test("hasImplementationArtifacts finds integration implementation-only commits w
|
|
|
807
807
|
}
|
|
808
808
|
});
|
|
809
809
|
|
|
810
|
+
test("hasImplementationArtifacts ignores corrupted milestone/* integration metadata", () => {
|
|
811
|
+
const base = makeGitBase();
|
|
812
|
+
try {
|
|
813
|
+
mkdirSync(join(base, "src"), { recursive: true });
|
|
814
|
+
writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
|
|
815
|
+
execFileSync("git", ["add", "src/feature.ts"], { cwd: base, stdio: "ignore" });
|
|
816
|
+
execFileSync("git", ["commit", "-m", "feat: add milestone feature\n\nGSD-Task: S01/T01"], { cwd: base, stdio: "ignore" });
|
|
817
|
+
|
|
818
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
819
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
820
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
821
|
+
insertSlice({
|
|
822
|
+
id: "S01",
|
|
823
|
+
milestoneId: "M001",
|
|
824
|
+
title: "Slice One",
|
|
825
|
+
status: "complete",
|
|
826
|
+
risk: "low",
|
|
827
|
+
depends: [],
|
|
828
|
+
});
|
|
829
|
+
insertTask({
|
|
830
|
+
id: "T01",
|
|
831
|
+
sliceId: "S01",
|
|
832
|
+
milestoneId: "M001",
|
|
833
|
+
title: "Task One",
|
|
834
|
+
status: "complete",
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
execFileSync("git", ["checkout", "-b", "milestone/M001"], { cwd: base, stdio: "ignore" });
|
|
838
|
+
mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
|
|
839
|
+
writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\nDone.");
|
|
840
|
+
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
841
|
+
execFileSync("git", ["commit", "-m", "chore: auto-commit after complete-milestone\n\nGSD-Unit: M001"], { cwd: base, stdio: "ignore" });
|
|
842
|
+
|
|
843
|
+
writeFileSync(
|
|
844
|
+
join(base, ".gsd", "milestones", "M001", "M001-META.json"),
|
|
845
|
+
JSON.stringify({ integrationBranch: "milestone/M001" }, null, 2) + "\n",
|
|
846
|
+
);
|
|
847
|
+
|
|
848
|
+
const result = hasImplementationArtifacts(base, "M001");
|
|
849
|
+
assert.equal(result, "present", "corrupted milestone integration metadata should fall back to main branch for artifact detection");
|
|
850
|
+
} finally {
|
|
851
|
+
cleanup(base);
|
|
852
|
+
}
|
|
853
|
+
});
|
|
854
|
+
|
|
810
855
|
test("hasImplementationArtifacts backfills untagged main implementation commits from completed task file hints", () => {
|
|
811
856
|
const base = makeGitBase();
|
|
812
857
|
try {
|
|
@@ -1043,24 +1088,50 @@ test("hasImplementationArtifacts binds GSD-Task trailer to milestone via DB stat
|
|
|
1043
1088
|
}
|
|
1044
1089
|
});
|
|
1045
1090
|
|
|
1046
|
-
test("hasImplementationArtifacts does not
|
|
1091
|
+
test("hasImplementationArtifacts does not claim Sxx/Tyy commit trailers across milestones when ownership points elsewhere", () => {
|
|
1047
1092
|
const base = makeGitBase();
|
|
1048
1093
|
try {
|
|
1049
1094
|
writeFileSync(join(base, ".git", "info", "exclude"), ".gsd/\n");
|
|
1095
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
1096
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1097
|
+
insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
|
|
1098
|
+
insertMilestone({ id: "M002", title: "Milestone Two", status: "active" });
|
|
1099
|
+
insertSlice({
|
|
1100
|
+
id: "S01",
|
|
1101
|
+
milestoneId: "M002",
|
|
1102
|
+
title: "Slice One",
|
|
1103
|
+
status: "complete",
|
|
1104
|
+
risk: "low",
|
|
1105
|
+
depends: [],
|
|
1106
|
+
});
|
|
1107
|
+
insertTask({
|
|
1108
|
+
id: "T01",
|
|
1109
|
+
sliceId: "S01",
|
|
1110
|
+
milestoneId: "M002",
|
|
1111
|
+
title: "Task One",
|
|
1112
|
+
status: "complete",
|
|
1113
|
+
});
|
|
1114
|
+
|
|
1050
1115
|
mkdirSync(join(base, "src"), { recursive: true });
|
|
1051
1116
|
writeFileSync(join(base, "src", "feature.ts"), "export function feature() {}\n");
|
|
1052
1117
|
execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
|
|
1053
1118
|
execFileSync(
|
|
1054
1119
|
"git",
|
|
1055
|
-
["commit", "-m", "feat: add feature\n\nGSD-Task: S01/T01"],
|
|
1120
|
+
["commit", "-m", "feat: add sibling feature\n\nGSD-Task: S01/T01"],
|
|
1056
1121
|
{ cwd: base, stdio: "ignore" },
|
|
1057
1122
|
);
|
|
1058
1123
|
|
|
1059
|
-
const
|
|
1124
|
+
const m001Result = hasImplementationArtifacts(base, "M001");
|
|
1125
|
+
const m002Result = hasImplementationArtifacts(base, "M002");
|
|
1060
1126
|
assert.equal(
|
|
1061
|
-
|
|
1127
|
+
m001Result,
|
|
1062
1128
|
"absent",
|
|
1063
|
-
"
|
|
1129
|
+
"Sxx/Tyy commit trailers owned by M002 must not be attributed to M001",
|
|
1130
|
+
);
|
|
1131
|
+
assert.equal(
|
|
1132
|
+
m002Result,
|
|
1133
|
+
"present",
|
|
1134
|
+
"the owning milestone should still claim the implementation-bearing commit",
|
|
1064
1135
|
);
|
|
1065
1136
|
} finally {
|
|
1066
1137
|
cleanup(base);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Project/App: GSD-2
|
|
2
|
+
// File Purpose: Regression tests for auto-mode stop notification formatting.
|
|
3
|
+
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
|
|
7
|
+
import { formatAutoStopNotification } from "../auto.ts";
|
|
8
|
+
|
|
9
|
+
test("auto stop notification keeps session totals on a separate line", () => {
|
|
10
|
+
const message = formatAutoStopNotification(
|
|
11
|
+
"Auto-mode stopped",
|
|
12
|
+
{ cost: 0.652, tokens: { total: 87000 } },
|
|
13
|
+
2,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
assert.equal(
|
|
17
|
+
message,
|
|
18
|
+
"Auto-mode stopped.\nSession: $0.652 · 87.0k tokens · 2 units",
|
|
19
|
+
);
|
|
20
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
|
|
8
|
+
import { checkoutBranchWithStashGuard } from "../auto-worktree.ts";
|
|
9
|
+
|
|
10
|
+
function git(args: string[], cwd: string): string {
|
|
11
|
+
return execFileSync("git", args, {
|
|
12
|
+
cwd,
|
|
13
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function createRepo(t: { after: (fn: () => void) => void }): string {
|
|
19
|
+
const dir = realpathSync(mkdtempSync(join(tmpdir(), "checkout-stash-guard-")));
|
|
20
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
21
|
+
git(["init"], dir);
|
|
22
|
+
git(["config", "user.email", "test@example.com"], dir);
|
|
23
|
+
git(["config", "user.name", "Test User"], dir);
|
|
24
|
+
writeFileSync(join(dir, "note.txt"), "base\n");
|
|
25
|
+
git(["add", "note.txt"], dir);
|
|
26
|
+
git(["commit", "-m", "init"], dir);
|
|
27
|
+
git(["branch", "-M", "main"], dir);
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe("checkoutBranchWithStashGuard", () => {
|
|
32
|
+
test("restores dirty working tree after successful checkout", (t) => {
|
|
33
|
+
const repo = createRepo(t);
|
|
34
|
+
git(["checkout", "-b", "milestone/M001"], repo);
|
|
35
|
+
git(["checkout", "main"], repo);
|
|
36
|
+
|
|
37
|
+
writeFileSync(join(repo, "note.txt"), "dirty\n");
|
|
38
|
+
|
|
39
|
+
checkoutBranchWithStashGuard(repo, "milestone/M001", "test-success");
|
|
40
|
+
|
|
41
|
+
const branch = git(["branch", "--show-current"], repo).trim();
|
|
42
|
+
assert.equal(branch, "milestone/M001");
|
|
43
|
+
const content = git(["show", "HEAD:note.txt"], repo).trim();
|
|
44
|
+
assert.equal(content, "base");
|
|
45
|
+
const wtContent = readFileSync(join(repo, "note.txt"), "utf8");
|
|
46
|
+
assert.equal(wtContent, "dirty\n");
|
|
47
|
+
const status = git(["status", "--porcelain"], repo);
|
|
48
|
+
assert.match(status, /note\.txt/);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("restores dirty working tree when checkout throws", (t) => {
|
|
52
|
+
const repo = createRepo(t);
|
|
53
|
+
writeFileSync(join(repo, "note.txt"), "dirty\n");
|
|
54
|
+
|
|
55
|
+
assert.throws(
|
|
56
|
+
() => checkoutBranchWithStashGuard(repo, "milestone/DOES-NOT-EXIST", "test-failure"),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const status = git(["status", "--porcelain"], repo);
|
|
60
|
+
assert.match(status, /note\.txt/);
|
|
61
|
+
const stashList = git(["stash", "list"], repo).trim();
|
|
62
|
+
assert.equal(stashList, "");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("surfaces distinct error when checkout succeeds but stash pop conflicts", (t) => {
|
|
66
|
+
const repo = createRepo(t);
|
|
67
|
+
// Branch B has a divergent version of note.txt so popping a stash made
|
|
68
|
+
// against main will conflict after the checkout to B.
|
|
69
|
+
git(["checkout", "-b", "milestone/B"], repo);
|
|
70
|
+
writeFileSync(join(repo, "note.txt"), "B-version\n");
|
|
71
|
+
git(["add", "note.txt"], repo);
|
|
72
|
+
git(["commit", "-m", "B"], repo);
|
|
73
|
+
git(["checkout", "main"], repo);
|
|
74
|
+
|
|
75
|
+
writeFileSync(join(repo, "note.txt"), "local\n");
|
|
76
|
+
|
|
77
|
+
assert.throws(
|
|
78
|
+
() => checkoutBranchWithStashGuard(repo, "milestone/B", "test-pop-failure"),
|
|
79
|
+
/checkout to 'milestone\/B' succeeded but stash restore failed/,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const branch = git(["branch", "--show-current"], repo).trim();
|
|
83
|
+
assert.equal(branch, "milestone/B");
|
|
84
|
+
const stashList = git(["stash", "list"], repo).trim();
|
|
85
|
+
assert.match(stashList, /gsd: checkout stash/);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -17,6 +17,15 @@ import {
|
|
|
17
17
|
setPendingAutoStart,
|
|
18
18
|
} from "../guided-flow.ts";
|
|
19
19
|
|
|
20
|
+
function pendingInput(basePath: string, milestoneId: string) {
|
|
21
|
+
return {
|
|
22
|
+
basePath,
|
|
23
|
+
milestoneId,
|
|
24
|
+
ctx: { ui: { notify: () => undefined } } as any,
|
|
25
|
+
pi: { sendMessage: () => undefined } as any,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
20
29
|
afterEach(() => {
|
|
21
30
|
clearPendingAutoStart();
|
|
22
31
|
});
|
|
@@ -28,7 +37,7 @@ describe("clear stale pending auto-start (#3667)", () => {
|
|
|
28
37
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
29
38
|
const before = Date.now();
|
|
30
39
|
|
|
31
|
-
setPendingAutoStart(base,
|
|
40
|
+
setPendingAutoStart(base, pendingInput(base, "M001"));
|
|
32
41
|
|
|
33
42
|
const entry = _getPendingAutoStart(base);
|
|
34
43
|
assert.ok(entry);
|
|
@@ -41,7 +50,7 @@ describe("clear stale pending auto-start (#3667)", () => {
|
|
|
41
50
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
42
51
|
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
43
52
|
|
|
44
|
-
setPendingAutoStart(base, {
|
|
53
|
+
setPendingAutoStart(base, { ...pendingInput(base, "M001"), createdAt: 123 });
|
|
45
54
|
|
|
46
55
|
assert.equal(_getPendingAutoStart(base)?.createdAt, 123);
|
|
47
56
|
});
|
|
@@ -408,10 +408,10 @@ console.log('\n=== complete-slice: handler with missing roadmap ===');
|
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
411
|
-
// complete-slice:
|
|
411
|
+
// complete-slice: PROJECT refresh uses DB-backed artifact tool.
|
|
412
412
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
413
413
|
|
|
414
|
-
console.log('\n=== complete-slice:
|
|
414
|
+
console.log('\n=== complete-slice: PROJECT refresh uses gsd_summary_save ===');
|
|
415
415
|
{
|
|
416
416
|
const promptPath = path.join(
|
|
417
417
|
path.dirname(new URL(import.meta.url).pathname),
|
|
@@ -419,13 +419,9 @@ console.log('\n=== complete-slice: step 13 specifies write tool for PROJECT.md (
|
|
|
419
419
|
);
|
|
420
420
|
const prompt = fs.readFileSync(promptPath, 'utf-8');
|
|
421
421
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const mentionsWriteTool =
|
|
426
|
-
/PROJECT\.md.*\bwrite\b/i.test(prompt) ||
|
|
427
|
-
/\bwrite\b.*PROJECT\.md/i.test(prompt);
|
|
428
|
-
assertTrue(mentionsWriteTool, 'step 13 must name the `write` tool when updating PROJECT.md');
|
|
422
|
+
assertTrue(prompt.includes('gsd_summary_save'), 'PROJECT refresh must use gsd_summary_save');
|
|
423
|
+
assertTrue(prompt.includes('artifact_type: "PROJECT"'), 'PROJECT refresh must use artifact_type PROJECT');
|
|
424
|
+
assertTrue(!/with a full `write`/i.test(prompt), 'prompt must not instruct direct PROJECT.md writes');
|
|
429
425
|
}
|
|
430
426
|
|
|
431
427
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -225,6 +225,25 @@ test("clearLock removes the session_file row for the active worker", (t) => {
|
|
|
225
225
|
"session_file row deleted by clearLock");
|
|
226
226
|
});
|
|
227
227
|
|
|
228
|
+
test("clearLock marks stale worker as stopping when no current-process worker matches", (t) => {
|
|
229
|
+
const base = makeBase();
|
|
230
|
+
t.after(() => cleanup(base));
|
|
231
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
232
|
+
const projectRoot = normalizeRealPath(base);
|
|
233
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
234
|
+
|
|
235
|
+
setRuntimeKv("worker", workerId, "session_file", "/tmp/stale-session.jsonl");
|
|
236
|
+
setWorkerPid(workerId, 99999);
|
|
237
|
+
expireWorker(workerId);
|
|
238
|
+
assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
|
|
239
|
+
|
|
240
|
+
clearLock(base);
|
|
241
|
+
|
|
242
|
+
assert.equal(getAutoWorker(workerId)?.status, "stopping");
|
|
243
|
+
assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
|
|
244
|
+
assert.equal(readCrashLock(base), null);
|
|
245
|
+
});
|
|
246
|
+
|
|
228
247
|
test("clearStaleWorkerLock crashes stale worker and cancels latest active dispatch", (t) => {
|
|
229
248
|
const base = makeBase();
|
|
230
249
|
t.after(() => cleanup(base));
|
|
@@ -264,3 +283,27 @@ test("clearStaleWorkerLock crashes stale worker and cancels latest active dispat
|
|
|
264
283
|
assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
|
|
265
284
|
assert.equal(readCrashLock(base), null);
|
|
266
285
|
});
|
|
286
|
+
|
|
287
|
+
test("clearLock marks stale worker crashed and releases held milestone lease", (t) => {
|
|
288
|
+
const base = makeBase();
|
|
289
|
+
t.after(() => cleanup(base));
|
|
290
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
291
|
+
insertMilestone({ id: "M001", title: "T", status: "active" });
|
|
292
|
+
const projectRoot = normalizeRealPath(base);
|
|
293
|
+
const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
|
|
294
|
+
const lease = claimMilestoneLease(workerId, "M001");
|
|
295
|
+
assert.equal(lease.ok, true);
|
|
296
|
+
if (!lease.ok) return;
|
|
297
|
+
|
|
298
|
+
setWorkerPid(workerId, 99999);
|
|
299
|
+
expireWorker(workerId);
|
|
300
|
+
assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
|
|
301
|
+
|
|
302
|
+
clearLock(base);
|
|
303
|
+
|
|
304
|
+
assert.equal(getAutoWorker(workerId)?.status, "crashed");
|
|
305
|
+
const leaseRow = _getAdapter()!.prepare(
|
|
306
|
+
`SELECT status FROM milestone_leases WHERE milestone_id = :m`,
|
|
307
|
+
).get({ ":m": "M001" }) as { status: string } | undefined;
|
|
308
|
+
assert.equal(leaseRow?.status, "released");
|
|
309
|
+
});
|