gsd-pi 2.79.0 → 2.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/README.md +94 -47
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/contracts.js +1 -0
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +146 -0
  5. package/dist/resources/extensions/gsd/auto/phases.js +61 -7
  6. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  7. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  8. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  9. package/dist/resources/extensions/gsd/auto-prompts.js +52 -29
  10. package/dist/resources/extensions/gsd/auto-recovery.js +63 -55
  11. package/dist/resources/extensions/gsd/auto-runtime-state.js +4 -0
  12. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  13. package/dist/resources/extensions/gsd/auto.js +159 -2
  14. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +9 -1
  15. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +2 -2
  16. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +41 -45
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
  18. package/dist/resources/extensions/gsd/commands/context.js +1 -1
  19. package/dist/resources/extensions/gsd/gsd-db.js +34 -1
  20. package/dist/resources/extensions/gsd/guided-flow.js +40 -0
  21. package/dist/resources/extensions/gsd/paths.js +5 -1
  22. package/dist/resources/extensions/gsd/post-execution-checks.js +25 -6
  23. package/dist/resources/extensions/gsd/preferences-types.js +20 -2
  24. package/dist/resources/extensions/gsd/preferences-validation.js +3 -3
  25. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +82 -2
  26. package/dist/resources/extensions/gsd/unit-context-composer.js +32 -0
  27. package/dist/resources/extensions/gsd/unit-context-manifest.js +21 -0
  28. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  29. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  30. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  31. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  32. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  33. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  34. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  35. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  36. package/dist/web/standalone/.next/BUILD_ID +1 -1
  37. package/dist/web/standalone/.next/app-path-routes-manifest.json +9 -9
  38. package/dist/web/standalone/.next/build-manifest.json +2 -2
  39. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.html +1 -1
  56. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app-paths-manifest.json +9 -9
  63. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  65. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  66. package/package.json +1 -1
  67. package/packages/daemon/package.json +2 -2
  68. package/packages/mcp-server/dist/workflow-tools.d.ts +1 -1
  69. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  70. package/packages/mcp-server/dist/workflow-tools.js +53 -0
  71. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  72. package/packages/mcp-server/package.json +2 -2
  73. package/packages/mcp-server/src/workflow-tools.test.ts +129 -2
  74. package/packages/mcp-server/src/workflow-tools.ts +81 -0
  75. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  76. package/packages/native/package.json +1 -1
  77. package/packages/pi-agent-core/package.json +1 -1
  78. package/packages/pi-ai/package.json +1 -1
  79. package/packages/pi-coding-agent/package.json +1 -1
  80. package/packages/pi-tui/package.json +1 -1
  81. package/packages/rpc-client/package.json +1 -1
  82. package/pkg/package.json +1 -1
  83. package/src/resources/extensions/gsd/auto/contracts.ts +87 -0
  84. package/src/resources/extensions/gsd/auto/loop-deps.ts +10 -3
  85. package/src/resources/extensions/gsd/auto/orchestrator.ts +161 -0
  86. package/src/resources/extensions/gsd/auto/phases.ts +88 -9
  87. package/src/resources/extensions/gsd/auto/session.ts +11 -0
  88. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  89. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  90. package/src/resources/extensions/gsd/auto-prompts.ts +106 -28
  91. package/src/resources/extensions/gsd/auto-recovery.ts +59 -53
  92. package/src/resources/extensions/gsd/auto-runtime-state.ts +7 -0
  93. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  94. package/src/resources/extensions/gsd/auto.ts +167 -1
  95. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +14 -1
  96. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +2 -2
  97. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +49 -46
  98. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
  99. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
  100. package/src/resources/extensions/gsd/commands/context.ts +1 -1
  101. package/src/resources/extensions/gsd/gsd-db.ts +35 -1
  102. package/src/resources/extensions/gsd/guided-flow.ts +47 -0
  103. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  104. package/src/resources/extensions/gsd/paths.ts +6 -1
  105. package/src/resources/extensions/gsd/post-execution-checks.ts +31 -6
  106. package/src/resources/extensions/gsd/preferences-types.ts +23 -4
  107. package/src/resources/extensions/gsd/preferences-validation.ts +3 -3
  108. package/src/resources/extensions/gsd/tests/auto-abort-pause-regression.test.ts +32 -0
  109. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +353 -0
  110. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
  111. package/src/resources/extensions/gsd/tests/auto-runtime-state.test.ts +39 -0
  112. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +3 -0
  113. package/src/resources/extensions/gsd/tests/bootstrap-derive-state-db-open.test.ts +2 -2
  114. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
  115. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
  116. package/src/resources/extensions/gsd/tests/current-directory-root-homedir-fallback.test.ts +63 -0
  117. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  118. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  119. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
  120. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +95 -0
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +14 -0
  122. package/src/resources/extensions/gsd/tests/integration/auto-recovery.test.ts +79 -0
  123. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +134 -0
  124. package/src/resources/extensions/gsd/tests/parallel-skill-prompt-integration.test.ts +8 -0
  125. package/src/resources/extensions/gsd/tests/paused-session-via-db.test.ts +2 -0
  126. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +27 -0
  127. package/src/resources/extensions/gsd/tests/post-execution-checks.test.ts +46 -0
  128. package/src/resources/extensions/gsd/tests/pre-exec-gate-loop.test.ts +3 -0
  129. package/src/resources/extensions/gsd/tests/register-hooks-compaction-checkpoint.test.ts +85 -0
  130. package/src/resources/extensions/gsd/tests/run-uat-composer.test.ts +2 -0
  131. package/src/resources/extensions/gsd/tests/subagent-model-dispatch.test.ts +59 -0
  132. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +38 -0
  133. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +32 -0
  134. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  135. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  136. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +132 -3
  137. package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +3 -0
  138. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +84 -1
  139. package/src/resources/extensions/gsd/unit-context-composer.ts +49 -0
  140. package/src/resources/extensions/gsd/unit-context-manifest.ts +34 -0
  141. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  142. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  143. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  144. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  145. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  146. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  147. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  148. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  149. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  150. /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{J-CU-p_sp45CJHT3R9TJS → V-3Ehy4B24f9FCGiLPWIM}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { execFileSync } from "node:child_process";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
@@ -23,6 +23,11 @@ import {
23
23
  showSmartEntry,
24
24
  startDeepProjectSetupForeground,
25
25
  } from "../guided-flow.ts";
26
+ import {
27
+ closeDatabase,
28
+ insertMilestone,
29
+ openDatabase,
30
+ } from "../gsd-db.ts";
26
31
  import type { GSDPreferences } from "../preferences.ts";
27
32
  import type { GSDState } from "../types.ts";
28
33
 
@@ -342,6 +347,62 @@ test("deep project setup: pre-dispatch can run before the first milestone exists
342
347
  }
343
348
  });
344
349
 
350
+ test("deep project setup: bootstrap continues queued M002 without milestone context", async () => {
351
+ const base = makeRepo();
352
+ try {
353
+ writeCapturedDeepPrefs(base);
354
+ writeValidProjectAndRequirements(base);
355
+ mkdirSync(join(base, ".gsd", "runtime"), { recursive: true });
356
+ writeFileSync(join(base, ".gsd", "runtime", "research-decision.json"), '{"decision":"skip"}\n');
357
+
358
+ openDatabase(join(base, ".gsd", "gsd.db"));
359
+ insertMilestone({ id: "M001", title: "First milestone", status: "complete" });
360
+ insertMilestone({ id: "M002", title: "Second milestone", status: "queued" });
361
+ closeDatabase();
362
+
363
+ const messages: unknown[] = [];
364
+ const pi = {
365
+ ...makePi(messages),
366
+ getThinkingLevel: () => "medium",
367
+ };
368
+ const s = new AutoSession();
369
+ const ready = await bootstrapAutoSession(
370
+ s,
371
+ makeCtx(`queued-${randomUUID()}`) as any,
372
+ pi as any,
373
+ base,
374
+ false,
375
+ false,
376
+ {
377
+ shouldUseWorktreeIsolation: () => false,
378
+ registerSigtermHandler: () => {},
379
+ lockBase: () => base,
380
+ buildResolver: () => ({}) as any,
381
+ },
382
+ {
383
+ classification: "none",
384
+ lock: null,
385
+ pausedSession: null,
386
+ state: null,
387
+ recovery: null,
388
+ recoveryPrompt: null,
389
+ recoveryToolCallCount: 0,
390
+ artifactSatisfied: false,
391
+ hasResumableDiskState: false,
392
+ isBootstrapCrash: false,
393
+ },
394
+ );
395
+
396
+ assert.equal(ready, true);
397
+ assert.equal(s.active, true);
398
+ assert.equal(s.currentMilestoneId, "M002");
399
+ assert.equal(messages.length, 0, "queued deep milestone must not re-enter smart new-milestone discussion");
400
+ } finally {
401
+ try { closeDatabase(); } catch {}
402
+ rmSync(base, { recursive: true, force: true });
403
+ }
404
+ });
405
+
345
406
  test("deep project setup: pre-dispatch takes precedence over an existing draft milestone", async () => {
346
407
  const base = makeBase();
347
408
  try {
@@ -1020,7 +1081,7 @@ test("deep project setup: research-project blocker placeholder is a file, not th
1020
1081
  const base = makeBase();
1021
1082
  try {
1022
1083
  const expectedPath = resolveExpectedArtifactPath("research-project", "PROJECT-RESEARCH", base);
1023
- assert.equal(expectedPath, join(base, ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
1084
+ assert.equal(expectedPath, join(realpathSync(base), ".gsd", "research", "PROJECT-RESEARCH-BLOCKER.md"));
1024
1085
 
1025
1086
  mkdirSync(join(base, ".gsd", "research"), { recursive: true });
1026
1087
  const diagnosis = writeBlockerPlaceholder(
@@ -0,0 +1,109 @@
1
+ // gsd-2 / execute-summary-save PROJECT registration hard-fail tests
2
+ import test from "node:test";
3
+ import assert from "node:assert/strict";
4
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ import { openDatabase, closeDatabase, getAllMilestones } from "../gsd-db.ts";
10
+ import { markApprovalGateVerified, clearDiscussionFlowState } from "../bootstrap/write-gate.ts";
11
+ import { executeSummarySave } from "../tools/workflow-tool-executors.ts";
12
+
13
+ function makeTmpBase(): string {
14
+ const base = join(tmpdir(), `gsd-summary-save-empty-project-${randomUUID()}`);
15
+ mkdirSync(join(base, ".gsd"), { recursive: true });
16
+ return base;
17
+ }
18
+
19
+ function cleanup(base: string): void {
20
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* swallow */ }
21
+ }
22
+
23
+ function openTestDb(base: string): void {
24
+ openDatabase(join(base, ".gsd", "gsd.db"));
25
+ }
26
+
27
+ async function inProjectDir<T>(dir: string, fn: () => Promise<T>): Promise<T> {
28
+ const originalCwd = process.cwd();
29
+ try {
30
+ process.chdir(dir);
31
+ return await fn();
32
+ } finally {
33
+ process.chdir(originalCwd);
34
+ }
35
+ }
36
+
37
+ function setupBase(t: { after: (fn: () => void) => void }): string {
38
+ const base = makeTmpBase();
39
+ // Force deep planning so the root-artifact guard requires a verified approval gate,
40
+ // matching the production flow that surfaces the regression.
41
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\nplanning_depth: deep\n---\n");
42
+ openTestDb(base);
43
+ markApprovalGateVerified("depth_verification_project_confirm", base);
44
+ t.after(() => {
45
+ clearDiscussionFlowState(base);
46
+ closeDatabase();
47
+ cleanup(base);
48
+ });
49
+ return base;
50
+ }
51
+
52
+ test("executeSummarySave returns isError when PROJECT.md content has zero parseable milestone lines", async (t) => {
53
+ const base = setupBase(t);
54
+
55
+ const content = [
56
+ "# Project",
57
+ "",
58
+ "## What This Is",
59
+ "",
60
+ "Bad-separator regression fixture.",
61
+ "",
62
+ "## Milestone Sequence",
63
+ "",
64
+ // Wrong separator: " : " instead of em-dash / -- / - → MILESTONE_LINE_RE matches zero lines.
65
+ "- [ ] M001: Foundation : Establish the first runnable slice.",
66
+ "",
67
+ "## Next Section",
68
+ "",
69
+ "Trailing prose with no list bullets so MILESTONE_LINE_RE cannot bridge across lines.",
70
+ "",
71
+ ].join("\n");
72
+
73
+ const result = await inProjectDir(base, () => executeSummarySave({
74
+ artifact_type: "PROJECT",
75
+ content,
76
+ }, base));
77
+
78
+ assert.equal(result.isError, true);
79
+ assert.equal(result.details.error, "milestone_registration_empty_parse");
80
+ assert.match(result.content[0].text, /zero parseable milestone lines/);
81
+ assert.equal(getAllMilestones().length, 0);
82
+ });
83
+
84
+ test("executeSummarySave registers milestones when PROJECT.md uses canonical em-dash format", async (t) => {
85
+ const base = setupBase(t);
86
+
87
+ const content = [
88
+ "# Project",
89
+ "",
90
+ "## What This Is",
91
+ "",
92
+ "Canonical milestone-sequence fixture.",
93
+ "",
94
+ "## Milestone Sequence",
95
+ "",
96
+ "- [ ] M001: Foo — bar",
97
+ "- [ ] M002: Baz — qux",
98
+ "",
99
+ ].join("\n");
100
+
101
+ const result = await inProjectDir(base, () => executeSummarySave({
102
+ artifact_type: "PROJECT",
103
+ content,
104
+ }, base));
105
+
106
+ assert.notEqual(result.isError, true);
107
+ assert.deepEqual(result.details.registeredMilestones, ["M001", "M002"]);
108
+ assert.equal(getAllMilestones().length, 2);
109
+ });
@@ -1,3 +1,5 @@
1
+ // GSD Extension - Database regression tests.
2
+
1
3
  import { describe, test } from 'node:test';
2
4
  import assert from 'node:assert/strict';
3
5
  import * as fs from 'node:fs';
@@ -29,7 +31,10 @@ import {
29
31
  getTask,
30
32
  getSliceTasks,
31
33
  checkpointDatabase,
34
+ refreshOpenDatabaseFromDisk,
35
+ tryCreateMemoriesFts,
32
36
  } from '../gsd-db.ts';
37
+ import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
33
38
 
34
39
  const _require = createRequire(import.meta.url);
35
40
 
@@ -910,6 +915,31 @@ describe('gsd-db', () => {
910
915
  closeDatabase();
911
916
  });
912
917
 
918
+ test('gsd-db: FTS5 unavailable warning normalizes provider typo', () => {
919
+ const previousStderr = setStderrLoggingEnabled(false);
920
+ _resetLogs();
921
+ try {
922
+ const ok = tryCreateMemoriesFts({
923
+ exec(): void {
924
+ throw new Error('no such moduel : fts5');
925
+ },
926
+ prepare(): never {
927
+ throw new Error('prepare should not be called');
928
+ },
929
+ close(): void {},
930
+ });
931
+
932
+ assert.equal(ok, false, 'FTS5 creation should report fallback');
933
+ const warning = peekLogs().find((entry) => entry.component === 'db' && entry.message.includes('FTS5 unavailable'));
934
+ assert.ok(warning, 'FTS5 fallback warning should be logged');
935
+ assert.match(warning!.message, /no such module: fts5/);
936
+ assert.doesNotMatch(warning!.message, /moduel/);
937
+ } finally {
938
+ _resetLogs();
939
+ setStderrLoggingEnabled(previousStderr);
940
+ }
941
+ });
942
+
913
943
  // ─── checkpointDatabase ────────────────────────────────────────────────────
914
944
 
915
945
  describe('checkpointDatabase', () => {
@@ -952,6 +982,71 @@ describe('gsd-db', () => {
952
982
  });
953
983
  });
954
984
 
985
+ // ─── refreshOpenDatabaseFromDisk ───────────────────────────────────────────
986
+
987
+ describe('refreshOpenDatabaseFromDisk', () => {
988
+ test('refreshOpenDatabaseFromDisk: reopens the active file-backed database and sees external writes', (t) => {
989
+ const dbPath = tempDbPath();
990
+ t.after(() => cleanup(dbPath));
991
+
992
+ openDatabase(dbPath);
993
+ insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
994
+ insertSlice({
995
+ id: 'S01',
996
+ milestoneId: 'M001',
997
+ title: 'Slice 1',
998
+ status: 'pending',
999
+ sequence: 1,
1000
+ });
1001
+ insertTask({
1002
+ id: 'T01',
1003
+ milestoneId: 'M001',
1004
+ sliceId: 'S01',
1005
+ title: 'Task 1',
1006
+ status: 'pending',
1007
+ sequence: 1,
1008
+ });
1009
+
1010
+ const adapterBefore = _getAdapter()!;
1011
+
1012
+ const externalDb = openRawSqliteForTest(dbPath);
1013
+ try {
1014
+ externalDb.exec(`
1015
+ INSERT INTO tasks (milestone_id, slice_id, id, title, status, sequence)
1016
+ VALUES ('M001', 'S01', 'T02', 'Task 2', 'pending', 2)
1017
+ `);
1018
+ } finally {
1019
+ externalDb.close();
1020
+ }
1021
+
1022
+ const visibleBeforeRefresh = getSliceTasks('M001', 'S01').map(task => task.id);
1023
+ assert.ok(visibleBeforeRefresh.includes('T01'));
1024
+
1025
+ assert.equal(refreshOpenDatabaseFromDisk(), true);
1026
+ assert.notEqual(_getAdapter(), adapterBefore, 'refresh must replace the active adapter rather than becoming a no-op');
1027
+ const sliceTaskIds = getSliceTasks('M001', 'S01').map(task => task.id);
1028
+ assert.deepEqual(sliceTaskIds, ['T01', 'T02']);
1029
+ assert.equal(isDbAvailable(), true);
1030
+ });
1031
+
1032
+ test('refreshOpenDatabaseFromDisk: refuses in-memory databases without closing them', () => {
1033
+ openDatabase(':memory:');
1034
+ insertMilestone({ id: 'M001', title: 'Test', status: 'active' });
1035
+
1036
+ assert.equal(refreshOpenDatabaseFromDisk(), false);
1037
+ assert.equal(isDbAvailable(), true);
1038
+ assert.ok(_getAdapter()!.prepare("SELECT 1 FROM milestones WHERE id = 'M001'").get());
1039
+
1040
+ closeDatabase();
1041
+ });
1042
+
1043
+ test('refreshOpenDatabaseFromDisk: is a no-op when no database is open', () => {
1044
+ closeDatabase();
1045
+ assert.equal(refreshOpenDatabaseFromDisk(), false);
1046
+ assert.equal(isDbAvailable(), false);
1047
+ });
1048
+ });
1049
+
955
1050
  // ─── getDbStatus ───────────────────────────────────────────────────────────
956
1051
 
957
1052
  describe('getDbStatus', () => {
@@ -101,6 +101,20 @@ describe("guided-flow → auto-prompts consolidation (#5183)", () => {
101
101
  prompt.includes("Implement the thing"),
102
102
  "must include task plan body content from disk",
103
103
  );
104
+ assert.ok(prompt.includes("## Context Mode"), "execute-task should include standalone Context Mode guidance");
105
+ assert.ok(prompt.includes("execution lane"), "execute-task should render the execution lane");
106
+ });
107
+
108
+ test("buildExecuteTaskPrompt omits Context Mode when disabled", async () => {
109
+ writeFileSync(
110
+ join(base, ".gsd", "PREFERENCES.md"),
111
+ ["---", "context_mode:", " enabled: false", "---", ""].join("\n"),
112
+ );
113
+
114
+ const prompt = await buildExecuteTaskPrompt(MID, SID, S_TITLE, TID, T_TITLE, base);
115
+
116
+ assert.ok(!prompt.includes("## Context Mode"));
117
+ assert.ok(!prompt.includes("Context Mode (execution lane)"));
104
118
  });
105
119
 
106
120
  test("buildCompleteSlicePrompt carries the complete-slice contract", async () => {
@@ -1,3 +1,5 @@
1
+ // GSD Extension — Auto recovery integration tests.
2
+
1
3
  import test from "node:test";
2
4
  import assert from "node:assert/strict";
3
5
  import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, chmodSync } from "node:fs";
@@ -401,6 +403,57 @@ test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", (t
401
403
  assert.equal(result, false, "should fail when plan has no task entries (empty scaffold, #699)");
402
404
  });
403
405
 
406
+ test("verifyExpectedArtifact plan-slice trusts DB tasks over legacy plan syntax", (t) => {
407
+ const base = makeTmpBase();
408
+ t.after(() => {
409
+ closeDatabase();
410
+ cleanup(base);
411
+ });
412
+
413
+ openDatabase(join(base, ".gsd", "gsd.db"));
414
+ insertMilestone({ id: "M001", title: "Milestone", status: "active" });
415
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
416
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
417
+ insertTask({ id: "T02", milestoneId: "M001", sliceId: "S01", title: "Second task", status: "pending" });
418
+
419
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
420
+ const tasksDir = join(sliceDir, "tasks");
421
+ writeFileSync(
422
+ join(sliceDir, "S01-PLAN.md"),
423
+ "# S01: Slice\n\n## Tasks\n\nTask rows live in the DB; this projection intentionally has no legacy task syntax.\n",
424
+ );
425
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
426
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan\n");
427
+
428
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
429
+ assert.equal(result, true, "DB task rows plus task plan files should verify plan-slice");
430
+ });
431
+
432
+ test("verifyExpectedArtifact plan-slice still fails when a DB-backed task plan file is missing", (t) => {
433
+ const base = makeTmpBase();
434
+ t.after(() => {
435
+ closeDatabase();
436
+ cleanup(base);
437
+ });
438
+
439
+ openDatabase(join(base, ".gsd", "gsd.db"));
440
+ insertMilestone({ id: "M001", title: "Milestone", status: "active" });
441
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
442
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
443
+ insertTask({ id: "T02", milestoneId: "M001", sliceId: "S01", title: "Second task", status: "pending" });
444
+
445
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
446
+ const tasksDir = join(sliceDir, "tasks");
447
+ writeFileSync(
448
+ join(sliceDir, "S01-PLAN.md"),
449
+ "# S01: Slice\n\n## Tasks\n\nTask rows live in the DB; this projection intentionally has no legacy task syntax.\n",
450
+ );
451
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
452
+
453
+ const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
454
+ assert.equal(result, false, "DB task rows must still require matching task plan files");
455
+ });
456
+
404
457
  // ─── verifyExpectedArtifact: heading-style plan tasks (#1691) ─────────────
405
458
 
406
459
  test("verifyExpectedArtifact accepts plan-slice with heading-style tasks (### T01 --)", (t) => {
@@ -456,6 +509,32 @@ test("verifyExpectedArtifact accepts plan-slice with colon-style heading tasks (
456
509
  );
457
510
  });
458
511
 
512
+ test("verifyExpectedArtifact accepts indented legacy plan-slice task markers", (t) => {
513
+ const base = makeTmpBase();
514
+ t.after(() => cleanup(base));
515
+
516
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
517
+ const tasksDir = join(sliceDir, "tasks");
518
+ mkdirSync(tasksDir, { recursive: true });
519
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), [
520
+ "# S01: Test Slice",
521
+ "",
522
+ "## Tasks",
523
+ "",
524
+ " - [ ] **T01: Implement feature** `est:1h`",
525
+ "",
526
+ " ### T02 -- Write tests",
527
+ ].join("\n"));
528
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
529
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
530
+
531
+ assert.strictEqual(
532
+ verifyExpectedArtifact("plan-slice", "M001/S01", base),
533
+ true,
534
+ "Indented legacy task markers should be treated as completed plan-slice artifacts",
535
+ );
536
+ });
537
+
459
538
  test("verifyExpectedArtifact execute-task rejects heading-style plan without checked checkbox (#3607)", (t) => {
460
539
  const base = makeTmpBase();
461
540
  t.after(() => cleanup(base));
@@ -399,6 +399,140 @@ test("runDispatch pauses when complete-milestone summary exists on disk but the
399
399
  assert.equal(stopCalls, 0, "mismatch pause should not hard-stop the loop");
400
400
  });
401
401
 
402
+ test("runDispatch clears stuck state after Level 1 artifact recovery", async (t) => {
403
+ const capture = createEventCapture();
404
+ let invalidateCalls = 0;
405
+ let stopCalls = 0;
406
+ const base = join(tmpdir(), `gsd-stuck-plan-${randomUUID()}`);
407
+ t.after(() => {
408
+ closeDatabase();
409
+ rmSync(base, { recursive: true, force: true });
410
+ });
411
+
412
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
413
+ const tasksDir = join(sliceDir, "tasks");
414
+ mkdirSync(tasksDir, { recursive: true });
415
+ openDatabase(join(base, ".gsd", "gsd.db"));
416
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
417
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
418
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
419
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01\n\n## Tasks\n\n- [ ] **T01: First task** `est:1h`\n");
420
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
421
+
422
+ const deps = makeMockDeps(capture, {
423
+ invalidateAllCaches: () => { invalidateCalls++; },
424
+ stopAuto: async () => { stopCalls++; },
425
+ resolveDispatch: async () => ({
426
+ action: "dispatch" as const,
427
+ unitType: "plan-slice",
428
+ unitId: "M001/S01",
429
+ prompt: "plan the slice",
430
+ matchedRule: "planning → plan-slice",
431
+ }),
432
+ });
433
+ const ic = makeIC(deps, {
434
+ s: {
435
+ ...makeSession(),
436
+ basePath: base,
437
+ originalBasePath: base,
438
+ } as any,
439
+ });
440
+ const preData: PreDispatchData = {
441
+ state: {
442
+ phase: "planning",
443
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
444
+ activeSlice: { id: "S01", title: "Slice" },
445
+ registry: [{ id: "M001", status: "active" }],
446
+ blockers: [],
447
+ } as any,
448
+ mid: "M001",
449
+ midTitle: "Test Milestone",
450
+ };
451
+ const loopState: LoopState = {
452
+ recentUnits: [
453
+ { key: "plan-slice/M001/S01" },
454
+ { key: "plan-slice/M001/S01" },
455
+ ],
456
+ stuckRecoveryAttempts: 0,
457
+ consecutiveFinalizeTimeouts: 0,
458
+ };
459
+
460
+ const result = await runDispatch(ic, preData, loopState);
461
+
462
+ assert.equal(result.action, "continue");
463
+ assert.equal(invalidateCalls, 1, "Level 1 artifact recovery should invalidate caches");
464
+ assert.equal(stopCalls, 0, "Level 1 artifact recovery should not hard-stop");
465
+ assert.deepEqual(loopState.recentUnits, [], "Level 1 artifact recovery should clear the stuck window");
466
+ assert.equal(loopState.stuckRecoveryAttempts, 0, "Level 1 artifact recovery should reset the recovery counter");
467
+ });
468
+
469
+ test("runDispatch escapes Level 2 stuck stop when artifact verifies after cache invalidation", async (t) => {
470
+ const capture = createEventCapture();
471
+ let invalidateCalls = 0;
472
+ let stopCalls = 0;
473
+ const base = join(tmpdir(), `gsd-stuck-plan-l2-${randomUUID()}`);
474
+ t.after(() => {
475
+ closeDatabase();
476
+ rmSync(base, { recursive: true, force: true });
477
+ });
478
+
479
+ const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
480
+ const tasksDir = join(sliceDir, "tasks");
481
+ mkdirSync(tasksDir, { recursive: true });
482
+ openDatabase(join(base, ".gsd", "gsd.db"));
483
+ insertMilestone({ id: "M001", title: "Test", status: "active" });
484
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "pending" });
485
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "First task", status: "pending" });
486
+ writeFileSync(join(sliceDir, "S01-PLAN.md"), "# S01\n\n## Tasks\n\n- [ ] **T01: First task** `est:1h`\n");
487
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n");
488
+
489
+ const deps = makeMockDeps(capture, {
490
+ invalidateAllCaches: () => { invalidateCalls++; },
491
+ stopAuto: async () => { stopCalls++; },
492
+ resolveDispatch: async () => ({
493
+ action: "dispatch" as const,
494
+ unitType: "plan-slice",
495
+ unitId: "M001/S01",
496
+ prompt: "plan the slice",
497
+ matchedRule: "planning → plan-slice",
498
+ }),
499
+ });
500
+ const ic = makeIC(deps, {
501
+ s: {
502
+ ...makeSession(),
503
+ basePath: base,
504
+ originalBasePath: base,
505
+ } as any,
506
+ });
507
+ const preData: PreDispatchData = {
508
+ state: {
509
+ phase: "planning",
510
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
511
+ activeSlice: { id: "S01", title: "Slice" },
512
+ registry: [{ id: "M001", status: "active" }],
513
+ blockers: [],
514
+ } as any,
515
+ mid: "M001",
516
+ midTitle: "Test Milestone",
517
+ };
518
+ const loopState: LoopState = {
519
+ recentUnits: [
520
+ { key: "plan-slice/M001/S01" },
521
+ { key: "plan-slice/M001/S01" },
522
+ ],
523
+ stuckRecoveryAttempts: 1,
524
+ consecutiveFinalizeTimeouts: 0,
525
+ };
526
+
527
+ const result = await runDispatch(ic, preData, loopState);
528
+
529
+ assert.equal(result.action, "continue");
530
+ assert.equal(invalidateCalls, 1, "Level 2 escape should invalidate caches before rechecking artifacts");
531
+ assert.equal(stopCalls, 0, "verified artifacts should escape Level 2 hard stop");
532
+ assert.deepEqual(loopState.recentUnits, [], "Level 2 artifact escape should clear the stuck window");
533
+ assert.equal(loopState.stuckRecoveryAttempts, 0, "Level 2 artifact escape should reset the recovery counter");
534
+ });
535
+
402
536
  test("runUnitPhase emits unit-start and unit-end with causedBy reference", async () => {
403
537
  const capture = createEventCapture();
404
538
 
@@ -147,4 +147,12 @@ test("subagent dispatch prompt (buildParallelResearchSlicesPrompt) carries <skil
147
147
  prompt.includes(SKILL_ACTIVATION_SUBSTRING),
148
148
  `parallel-research-slices prompt should reference the always-used skill '${SKILL_NAME}'`,
149
149
  );
150
+ assert.ok(
151
+ prompt.includes("Context Mode (research lane):"),
152
+ "embedded parallel research subagent prompts should use nested Context Mode guidance",
153
+ );
154
+ assert.ok(
155
+ !prompt.includes("## Context Mode\n\nLane: **research lane**."),
156
+ "embedded parallel research subagent prompts should not use standalone Context Mode heading",
157
+ );
150
158
  });
@@ -64,6 +64,7 @@ test("readPausedSessionMetadata round-trips a real PausedSessionMetadata payload
64
64
  activeRunDir: null,
65
65
  autoStartTime: Date.now(),
66
66
  milestoneLock: null,
67
+ pauseReason: "Blocked: waiting for UAT",
67
68
  };
68
69
  setRuntimeKv("global", "", PAUSED_SESSION_KV_KEY, meta);
69
70
 
@@ -73,6 +74,7 @@ test("readPausedSessionMetadata round-trips a real PausedSessionMetadata payload
73
74
  assert.equal(loaded!.unitType, "plan-slice");
74
75
  assert.equal(loaded!.unitId, "M001/S01");
75
76
  assert.equal(loaded!.sessionFile, "/tmp/session.jsonl");
77
+ assert.equal(loaded!.pauseReason, "Blocked: waiting for UAT");
76
78
  });
77
79
 
78
80
  test("readPausedSessionMetadata auto-deletes stale pseudo-milestone pause rows", (t) => {
@@ -1,3 +1,5 @@
1
+ // GSD Extension — Plan-slice tool integration tests.
2
+
1
3
  import test from 'node:test';
2
4
  import assert from 'node:assert/strict';
3
5
  import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
@@ -8,6 +10,7 @@ import { openDatabase, closeDatabase, insertMilestone, insertSlice, getSlice, ge
8
10
  import { handlePlanSlice } from '../tools/plan-slice.ts';
9
11
  import { parsePlan } from '../parsers-legacy.ts';
10
12
  import { parseTaskPlanFile } from '../files.ts';
13
+ import { deriveState, invalidateStateCache } from '../state.ts';
11
14
 
12
15
  function makeTmpBase(): string {
13
16
  const base = mkdtempSync(join(tmpdir(), 'gsd-plan-slice-'));
@@ -98,6 +101,30 @@ test('handlePlanSlice writes slice/task planning state and renders plan artifact
98
101
  }
99
102
  });
100
103
 
104
+ test('handlePlanSlice advances DB-derived state out of planning immediately', async () => {
105
+ const base = makeTmpBase();
106
+ openDatabase(join(base, '.gsd', 'gsd.db'));
107
+
108
+ try {
109
+ seedParentSlice();
110
+
111
+ invalidateStateCache();
112
+ const before = await deriveState(base);
113
+ assert.equal(before.phase, 'planning');
114
+ assert.equal(before.progress?.tasks?.total, 0);
115
+
116
+ const result = await handlePlanSlice(validParams(), base);
117
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
118
+
119
+ invalidateStateCache();
120
+ const after = await deriveState(base);
121
+ assert.notEqual(after.phase, 'planning');
122
+ assert.equal(after.progress?.tasks?.total, 2);
123
+ } finally {
124
+ cleanup(base);
125
+ }
126
+ });
127
+
101
128
  test('handlePlanSlice leaves omitted enrichment fields empty instead of rendering placeholders', async () => {
102
129
  const base = makeTmpBase();
103
130
  openDatabase(join(base, '.gsd', 'gsd.db'));