gsd-pi 2.80.0-dev.e146beb20 → 2.80.0-dev.e51d2c88c

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 (197) hide show
  1. package/README.md +4 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/phases.js +29 -15
  4. package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
  5. package/dist/resources/extensions/gsd/auto/run-unit.js +13 -1
  6. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  7. package/dist/resources/extensions/gsd/auto-recovery.js +43 -1
  8. package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
  9. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
  10. package/dist/resources/extensions/gsd/auto.js +66 -4
  11. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +21 -2
  12. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  13. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  14. package/dist/resources/extensions/gsd/context-budget.js +37 -2
  15. package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
  16. package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
  17. package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
  18. package/dist/resources/extensions/gsd/gsd-db.js +46 -13
  19. package/dist/resources/extensions/gsd/guided-flow.js +33 -4
  20. package/dist/resources/extensions/gsd/memory-store.js +69 -12
  21. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  22. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  23. package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
  24. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  25. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  26. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  27. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  28. package/dist/resources/extensions/gsd/quick.js +34 -2
  29. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  30. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  31. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  32. package/dist/resources/extensions/gsd/tools/memory-tools.js +1 -0
  33. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  34. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
  35. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  36. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +18 -18
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +18 -18
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  68. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  69. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  70. package/package.json +3 -3
  71. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  73. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  74. package/packages/mcp-server/src/workflow-tools.test.ts +75 -2
  75. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  76. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  77. package/packages/native/tsconfig.tsbuildinfo +1 -1
  78. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js +32 -0
  79. package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  84. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  86. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  88. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  90. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  92. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  96. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  98. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  100. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  103. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  105. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  107. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  109. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  112. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  114. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  118. package/packages/pi-coding-agent/src/core/agent-session-abort-order.test.ts +36 -0
  119. package/packages/pi-coding-agent/src/core/agent-session.ts +8 -0
  120. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +3 -1
  121. package/packages/pi-coding-agent/src/core/compaction/compaction.ts +18 -0
  122. package/packages/pi-coding-agent/src/core/compaction-threshold.test.ts +121 -0
  123. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  124. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  125. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  126. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  127. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  128. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  129. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  130. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  131. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  132. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  133. package/packages/pi-tui/dist/tui.js +18 -8
  134. package/packages/pi-tui/dist/tui.js.map +1 -1
  135. package/packages/pi-tui/src/tui.ts +20 -8
  136. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  137. package/src/resources/extensions/gsd/auto/phases.ts +35 -20
  138. package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
  139. package/src/resources/extensions/gsd/auto/run-unit.ts +18 -1
  140. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  141. package/src/resources/extensions/gsd/auto-recovery.ts +54 -0
  142. package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
  143. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
  144. package/src/resources/extensions/gsd/auto.ts +78 -3
  145. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +21 -1
  146. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  147. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  148. package/src/resources/extensions/gsd/context-budget.ts +44 -2
  149. package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
  150. package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
  151. package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
  152. package/src/resources/extensions/gsd/gsd-db.ts +50 -13
  153. package/src/resources/extensions/gsd/guided-flow.ts +49 -4
  154. package/src/resources/extensions/gsd/memory-store.ts +77 -12
  155. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  156. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  157. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  158. package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
  159. package/src/resources/extensions/gsd/prompts/complete-milestone.md +14 -13
  160. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  161. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  162. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  163. package/src/resources/extensions/gsd/quick.ts +37 -2
  164. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +71 -0
  165. package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
  166. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
  167. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +14 -1
  168. package/src/resources/extensions/gsd/tests/context-budget.test.ts +10 -1
  169. package/src/resources/extensions/gsd/tests/dispatch-rule-coverage.test.ts +313 -0
  170. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  171. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  172. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +234 -0
  173. package/src/resources/extensions/gsd/tests/memory-decay-factor.test.ts +90 -0
  174. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  175. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  176. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  177. package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -0
  178. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  179. package/src/resources/extensions/gsd/tests/schema-v27-v28-sequence.test.ts +156 -0
  180. package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
  181. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
  182. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +55 -0
  183. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  184. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  185. package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +30 -0
  186. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
  187. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
  188. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  189. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  190. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  191. package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
  192. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  193. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
  194. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  195. package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
  196. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_buildManifest.js +0 -0
  197. /package/dist/web/standalone/.next/static/{y73quA-XdLo9n41nxphjW → 8F5YpnZNBaooIWGF4GBV3}/_ssgManifest.js +0 -0
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { spawnSync } from "node:child_process";
7
+ import { buildQuickCommitInstruction } from "../quick.ts";
8
+
9
+ function git(cwd: string, args: string[]): void {
10
+ const result = spawnSync("git", args, { cwd, encoding: "utf-8" });
11
+ assert.equal(result.status, 0, result.stderr || result.stdout);
12
+ }
13
+
14
+ test("quick task commit instruction does not ask agents to stage external .gsd quick files", { skip: process.platform === "win32" }, () => {
15
+ const tempRoot = realpathSync(mkdtempSync(join(tmpdir(), "gsd-quick-ext-")));
16
+ const repo = join(tempRoot, "repo");
17
+ const externalGsd = join(tempRoot, "state");
18
+ mkdirSync(repo);
19
+ mkdirSync(externalGsd);
20
+
21
+ const previousCwd = process.cwd();
22
+ try {
23
+ git(repo, ["init"]);
24
+ git(repo, ["config", "user.email", "test@example.com"]);
25
+ git(repo, ["config", "user.name", "Test User"]);
26
+ writeFileSync(join(repo, "README.md"), "# Test\n", "utf-8");
27
+ git(repo, ["add", "README.md"]);
28
+ git(repo, ["commit", "-m", "init"]);
29
+ symlinkSync(externalGsd, join(repo, ".gsd"), "dir");
30
+
31
+ const instruction = buildQuickCommitInstruction(repo, join(repo, ".gsd"));
32
+
33
+ assert.match(instruction, /do not stage or commit `\.gsd\/quick\/\.\.\.`/);
34
+ assert.match(instruction, /nothing in the project repo to commit/);
35
+ assert.match(instruction, /Write the quick summary file directly/);
36
+ } finally {
37
+ process.chdir(previousCwd);
38
+ rmSync(tempRoot, { recursive: true, force: true });
39
+ }
40
+ });
@@ -0,0 +1,156 @@
1
+ // gsd-2 / V27 + V28 schema migration regression tests
2
+ //
3
+ // Same bug class as #4591 (schema-v21-sequence): a migration block can be
4
+ // added but the SCHEMA_VERSION constant left unchanged, causing fresh-install
5
+ // + upgrade paths to silently skip the column add. This file pins both V27
6
+ // (artifacts.content_hash) and V28 (memories.last_hit_at) at the schema and
7
+ // write-path level on fresh-install DBs.
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+ import * as os from "node:os";
14
+
15
+ import {
16
+ openDatabase,
17
+ closeDatabase,
18
+ _getAdapter,
19
+ insertArtifact,
20
+ insertMemoryRow,
21
+ incrementMemoryHitCount,
22
+ SCHEMA_VERSION,
23
+ } from "../gsd-db.ts";
24
+
25
+ function makeTmp(): string {
26
+ return fs.mkdtempSync(path.join(os.tmpdir(), "gsd-v27v28-"));
27
+ }
28
+
29
+ function cleanup(base: string): void {
30
+ try { closeDatabase(); } catch { /* noop */ }
31
+ try { fs.rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
32
+ }
33
+
34
+ test("SCHEMA_VERSION constant is at least 28 (V28 migration committed)", () => {
35
+ assert.ok(
36
+ SCHEMA_VERSION >= 28,
37
+ `SCHEMA_VERSION must be ≥ 28 after V28 migration; got ${SCHEMA_VERSION}`,
38
+ );
39
+ });
40
+
41
+ test("fresh-install DB has artifacts.content_hash column (V27)", () => {
42
+ const base = makeTmp();
43
+ const dbPath = path.join(base, "gsd.db");
44
+ try {
45
+ openDatabase(dbPath);
46
+ const db = _getAdapter()!;
47
+ const cols = db.prepare("PRAGMA table_info(artifacts)").all() as Array<Record<string, unknown>>;
48
+ const colNames = new Set(cols.map((c) => c["name"] as string));
49
+ assert.ok(
50
+ colNames.has("content_hash"),
51
+ "V27 must add content_hash column to artifacts on fresh install",
52
+ );
53
+ } finally {
54
+ cleanup(base);
55
+ }
56
+ });
57
+
58
+ test("fresh-install DB has memories.last_hit_at column (V28)", () => {
59
+ const base = makeTmp();
60
+ const dbPath = path.join(base, "gsd.db");
61
+ try {
62
+ openDatabase(dbPath);
63
+ const db = _getAdapter()!;
64
+ const cols = db.prepare("PRAGMA table_info(memories)").all() as Array<Record<string, unknown>>;
65
+ const colNames = new Set(cols.map((c) => c["name"] as string));
66
+ assert.ok(
67
+ colNames.has("last_hit_at"),
68
+ "V28 must add last_hit_at column to memories on fresh install",
69
+ );
70
+ } finally {
71
+ cleanup(base);
72
+ }
73
+ });
74
+
75
+ test("fresh-install DB stamps SCHEMA_VERSION (≥28) in schema_version table", () => {
76
+ const base = makeTmp();
77
+ const dbPath = path.join(base, "gsd.db");
78
+ try {
79
+ openDatabase(dbPath);
80
+ const db = _getAdapter()!;
81
+ const row = db.prepare("SELECT MAX(version) as v FROM schema_version").get() as Record<string, unknown> | undefined;
82
+ const max = (row?.["v"] as number) ?? 0;
83
+ assert.ok(max >= 28, `fresh install must record schema_version ≥ 28; got ${max}`);
84
+ } finally {
85
+ cleanup(base);
86
+ }
87
+ });
88
+
89
+ test("insertArtifact populates content_hash with SHA-256 of full_content (V27 write-path)", () => {
90
+ const base = makeTmp();
91
+ const dbPath = path.join(base, "gsd.db");
92
+ try {
93
+ openDatabase(dbPath);
94
+ insertArtifact({
95
+ path: "M001/PROJECT.md",
96
+ artifact_type: "PROJECT",
97
+ milestone_id: "M001",
98
+ slice_id: null,
99
+ task_id: null,
100
+ full_content: "hello world",
101
+ });
102
+
103
+ const db = _getAdapter()!;
104
+ const row = db
105
+ .prepare("SELECT content_hash FROM artifacts WHERE path = :p")
106
+ .get({ ":p": "M001/PROJECT.md" }) as Record<string, unknown> | undefined;
107
+ const hash = row?.["content_hash"] as string | null | undefined;
108
+
109
+ // SHA-256 of "hello world" hex-encoded:
110
+ const expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
111
+ assert.equal(hash, expected, "content_hash must be SHA-256 hex of full_content");
112
+ } finally {
113
+ cleanup(base);
114
+ }
115
+ });
116
+
117
+ test("incrementMemoryHitCount sets last_hit_at alongside hit_count (V28 write-path)", () => {
118
+ const base = makeTmp();
119
+ const dbPath = path.join(base, "gsd.db");
120
+ try {
121
+ openDatabase(dbPath);
122
+
123
+ const created = "2026-01-01T00:00:00.000Z";
124
+ insertMemoryRow({
125
+ id: "MEM001",
126
+ category: "gotcha",
127
+ content: "test memory",
128
+ confidence: 0.9,
129
+ sourceUnitType: null,
130
+ sourceUnitId: null,
131
+ createdAt: created,
132
+ updatedAt: created,
133
+ scope: "project",
134
+ tags: [],
135
+ structuredFields: null,
136
+ });
137
+
138
+ // Before increment: last_hit_at should be NULL
139
+ const db = _getAdapter()!;
140
+ const before = db
141
+ .prepare("SELECT last_hit_at FROM memories WHERE id = :id")
142
+ .get({ ":id": "MEM001" }) as Record<string, unknown>;
143
+ assert.equal(before["last_hit_at"], null, "last_hit_at starts NULL on fresh insert");
144
+
145
+ const hitTime = "2026-02-01T00:00:00.000Z";
146
+ incrementMemoryHitCount("MEM001", hitTime);
147
+
148
+ const after = db
149
+ .prepare("SELECT hit_count, last_hit_at FROM memories WHERE id = :id")
150
+ .get({ ":id": "MEM001" }) as Record<string, unknown>;
151
+ assert.equal(after["hit_count"], 1, "hit_count increments");
152
+ assert.equal(after["last_hit_at"], hitTime, "last_hit_at set to provided timestamp");
153
+ } finally {
154
+ cleanup(base);
155
+ }
156
+ });
@@ -101,3 +101,30 @@ test("registerSigtermHandler deregisters previous handler from all signals", ()
101
101
  // Clean up
102
102
  deregisterSigtermHandler(handler2);
103
103
  });
104
+
105
+ test("registered signal handler runs best-effort cleanup before exiting", () => {
106
+ let cleanupCalled = false;
107
+ let exitCode: number | string | null | undefined;
108
+ const originalExit = process.exit;
109
+ const handler = registerSigtermHandler(
110
+ "/tmp/test-signal-cleanup",
111
+ null,
112
+ () => {
113
+ cleanupCalled = true;
114
+ },
115
+ );
116
+
117
+ (process as any).exit = ((code?: number | string | null) => {
118
+ exitCode = code;
119
+ throw new Error("process.exit intercepted");
120
+ }) as never;
121
+
122
+ try {
123
+ assert.throws(() => handler(), /process\.exit intercepted/);
124
+ assert.equal(cleanupCalled, true);
125
+ assert.equal(exitCode, 0);
126
+ } finally {
127
+ (process as any).exit = originalExit;
128
+ deregisterSigtermHandler(handler);
129
+ }
130
+ });
@@ -15,10 +15,11 @@
15
15
  * (the fix) and verifies it does not crash.
16
16
  */
17
17
 
18
- import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
18
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
19
19
  import { join } from "node:path";
20
20
  import { tmpdir } from "node:os";
21
21
  import { recoverTimedOutUnit, type RecoveryContext } from "../auto-timeout-recovery.ts";
22
+ import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
22
23
  import { test } from 'node:test';
23
24
  import assert from 'node:assert/strict';
24
25
 
@@ -39,6 +40,14 @@ function makeMockPi() {
39
40
  } as any;
40
41
  }
41
42
 
43
+ function makeRecordingPi() {
44
+ const messages: unknown[] = [];
45
+ return {
46
+ messages,
47
+ sendMessage: (message: unknown) => { messages.push(message); },
48
+ } as any;
49
+ }
50
+
42
51
  // ═══ #1855: empty RecoveryContext (basePath undefined) crashes ════════════════
43
52
 
44
53
  {
@@ -63,6 +72,45 @@ function makeMockPi() {
63
72
  assert.ok(crashed, "should crash when basePath is undefined (reproduces #1855)");
64
73
  }
65
74
 
75
+ // ═══ DB-complete execute-task recovery advances without steering ═════════════
76
+
77
+ {
78
+ console.log("\n=== execute-task timeout recovery trusts closed DB status ===");
79
+ const base = mkdtempSync(join(tmpdir(), "gsd-timeout-db-complete-"));
80
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
81
+
82
+ try {
83
+ openDatabase(join(base, ".gsd", "gsd.db"));
84
+ insertMilestone({ id: "M001", title: "Milestone", status: "active" });
85
+ insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "in_progress" });
86
+ insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "complete" });
87
+ writeFileSync(
88
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
89
+ "# S01\n\n## Tasks\n\n- [ ] **T01: Task** `est:10m`\n",
90
+ "utf-8",
91
+ );
92
+ writeFileSync(join(base, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: Task\n", "utf-8");
93
+
94
+ const ctx = makeMockCtx();
95
+ const pi = makeRecordingPi();
96
+ const result = await recoverTimedOutUnit(ctx, pi, "execute-task", "M001/S01/T01", "idle", {
97
+ basePath: base,
98
+ verbose: false,
99
+ currentUnitStartedAt: Date.now(),
100
+ unitRecoveryCount: new Map(),
101
+ });
102
+
103
+ assert.equal(result, "recovered", "db-complete task should recover immediately");
104
+ assert.equal(pi.messages.length, 0, "db-complete task should not send steering recovery");
105
+ const runtime = JSON.parse(readFileSync(join(base, ".gsd", "runtime", "units", "execute-task-M001-S01-T01.json"), "utf-8"));
106
+ assert.equal(runtime.phase, "finalized", "db-complete task should be finalized");
107
+ assert.equal(runtime.recovery.dbComplete, true, "runtime recovery should record DB completion");
108
+ } finally {
109
+ closeDatabase();
110
+ rmSync(base, { recursive: true, force: true });
111
+ }
112
+ }
113
+
66
114
  // ═══ #1855: valid RecoveryContext does not crash ═════════════════════════════
67
115
 
68
116
  {
@@ -4,6 +4,7 @@ import { readFileSync } from "node:fs";
4
4
  import { resolve } from "node:path";
5
5
 
6
6
  import { _withDetachedAutoKeepaliveForTest } from "../auto.ts";
7
+ import { _scheduleAutoStartAfterIdleForTest } from "../guided-flow.ts";
7
8
 
8
9
  const gsdDir = resolve(import.meta.dirname, "..");
9
10
 
@@ -96,6 +97,23 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
96
97
  );
97
98
  });
98
99
 
100
+ test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
101
+ const autoSrc = readGsdFile("auto.ts");
102
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
103
+ const startAutoBody = autoSrc.slice(startAutoIdx);
104
+
105
+ const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
106
+ const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
107
+
108
+ assert.ok(startAutoIdx > -1, "startAuto should exist");
109
+ assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
110
+ assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
111
+ assert.ok(
112
+ preBootstrapRegisterIdx < bootstrapCallIdx,
113
+ "worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
114
+ );
115
+ });
116
+
99
117
  test("startAutoDetached reports failures asynchronously (#3733)", () => {
100
118
  const autoSrc = readGsdFile("auto.ts");
101
119
 
@@ -185,3 +203,40 @@ test("detached auto-start preserves milestone lock across pause/stop cleanup (#3
185
203
  "AutoSession should track the detached milestone lock explicitly",
186
204
  );
187
205
  });
206
+
207
+ test("discussion auto-start waits for the current command context to become idle", async () => {
208
+ let releaseIdle!: () => void;
209
+ const idle = new Promise<void>((resolveIdle) => {
210
+ releaseIdle = resolveIdle;
211
+ });
212
+ const launches: unknown[][] = [];
213
+ const ctx = {
214
+ waitForIdle: () => idle,
215
+ ui: {
216
+ notify: () => {},
217
+ },
218
+ } as any;
219
+
220
+ _scheduleAutoStartAfterIdleForTest(
221
+ ctx,
222
+ {} as any,
223
+ "/tmp/gsd-auto-start-idle-test",
224
+ false,
225
+ { step: true },
226
+ (...args: unknown[]) => {
227
+ launches.push(args);
228
+ },
229
+ );
230
+
231
+ await Promise.resolve();
232
+ assert.equal(launches.length, 0, "auto-start must not launch before waitForIdle resolves");
233
+
234
+ releaseIdle();
235
+ await Promise.resolve();
236
+ assert.equal(launches.length, 0, "auto-start should defer launch to the next timer turn");
237
+
238
+ await new Promise((resolveTimer) => setTimeout(resolveTimer, 0));
239
+ assert.equal(launches.length, 1);
240
+ assert.equal(launches[0][2], "/tmp/gsd-auto-start-idle-test");
241
+ assert.deepEqual(launches[0][4], { step: true });
242
+ });
@@ -44,4 +44,13 @@ describe('status opens DB before deriveState (#3691)', () => {
44
44
  assert.match(quickSrc, /getIsolationMode\(\)\s*!==\s*"none"/,
45
45
  'quick.ts should compare isolation mode against "none"');
46
46
  });
47
+
48
+ test('quick task prompt handles external .gsd without staging quick files', () => {
49
+ assert.match(quickSrc, /isExternalGsdRoot/,
50
+ 'quick.ts should detect whether .gsd resolves outside the project repo');
51
+ assert.match(quickSrc, /do not stage or commit `\.gsd\/quick\/\.\.\.`/,
52
+ 'external-state quick tasks must tell the agent not to stage .gsd/quick files');
53
+ assert.match(quickSrc, /nothing in the project repo to commit/,
54
+ 'external-state quick tasks should allow summary-only work without a git commit');
55
+ });
47
56
  });
@@ -21,12 +21,18 @@ import type {
21
21
  ComputedArtifactRegistry,
22
22
  UnitContextManifest,
23
23
  } from "../unit-context-manifest.ts";
24
- import { UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
- import { buildReassessRoadmapPrompt } from "../auto-prompts.ts";
24
+ import { KNOWN_UNIT_TYPES, UNIT_MANIFESTS } from "../unit-context-manifest.ts";
25
+ import {
26
+ buildExecuteTaskPrompt,
27
+ buildGateEvaluatePrompt,
28
+ buildReassessRoadmapPrompt,
29
+ buildWorkflowPreferencesPrompt,
30
+ } from "../auto-prompts.ts";
26
31
  import { invalidateAllCaches } from "../cache.ts";
27
32
  import {
28
33
  openDatabase,
29
34
  closeDatabase,
35
+ insertGateRow,
30
36
  insertMilestone,
31
37
  upsertMilestonePlanning,
32
38
  insertSlice,
@@ -121,7 +127,7 @@ test("Context Mode composer: standalone output starts with heading and includes
121
127
  assert.ok(out.startsWith("## Context Mode"));
122
128
  assert.match(out, /execution lane/i);
123
129
  assert.match(out, /`gsd_exec`/);
124
- assert.match(out, /noisy commands/);
130
+ assert.match(out, /builds, tests, and diagnostics/);
125
131
  assert.match(out, /`gsd_exec_search`/);
126
132
  assert.match(out, /before reruns/);
127
133
  assert.match(out, /`gsd_resume`/);
@@ -136,7 +142,45 @@ test("Context Mode composer: nested output is compact single sentence", () => {
136
142
  assert.match(out, /`gsd_exec`/);
137
143
  assert.match(out, /`gsd_exec_search`/);
138
144
  assert.match(out, /`gsd_resume`/);
139
- assert.ok(out.length < 180, `nested guidance should stay compact, got ${out.length} chars`);
145
+ assert.ok(out.length < 240, `nested guidance should stay compact, got ${out.length} chars`);
146
+ });
147
+
148
+ const laneLabelByMode: Record<string, string> = {
149
+ interview: "interview",
150
+ research: "research",
151
+ planning: "planning",
152
+ execution: "execution",
153
+ verification: "verification",
154
+ orchestration: "orchestration",
155
+ docs: "documentation",
156
+ };
157
+
158
+ test("Context Mode composer: every known eligible unit renders its configured lane and required tools", () => {
159
+ for (const unitType of KNOWN_UNIT_TYPES) {
160
+ const manifest = UNIT_MANIFESTS[unitType];
161
+ assert.ok(manifest, `missing manifest for ${unitType}`);
162
+ const out = composeContextModeInstructions(unitType, { enabled: true, renderMode: "standalone" });
163
+ if (manifest.contextMode === "none") {
164
+ assert.strictEqual(out, "", `${unitType} should not render Context Mode`);
165
+ continue;
166
+ }
167
+ assert.ok(out.startsWith("## Context Mode"), `${unitType} should render standalone Context Mode heading`);
168
+ assert.match(out, new RegExp(`Lane: \\*\\*${laneLabelByMode[manifest.contextMode]} lane\\*\\*\\.`, "i"));
169
+ assert.match(out, /`gsd_exec`/, `${unitType} should mention gsd_exec`);
170
+ assert.match(out, /`gsd_exec_search`/, `${unitType} should mention gsd_exec_search`);
171
+ assert.match(out, /`gsd_resume`/, `${unitType} should mention gsd_resume`);
172
+ }
173
+ });
174
+
175
+ test("Context Mode composer: workflow-preferences and research-decision render no Context Mode block", () => {
176
+ assert.strictEqual(
177
+ composeContextModeInstructions("workflow-preferences", { enabled: true, renderMode: "standalone" }),
178
+ "",
179
+ );
180
+ assert.strictEqual(
181
+ composeContextModeInstructions("research-decision", { enabled: true, renderMode: "standalone" }),
182
+ "",
183
+ );
140
184
  });
141
185
 
142
186
  // ─── Integration: migrated buildReassessRoadmapPrompt ─────────────────────
@@ -220,6 +264,94 @@ test("#4782 phase 2: buildReassessRoadmapPrompt emits composer-shaped context wi
220
264
  assert.ok(!prompt.includes("Slice Context (from discussion)"));
221
265
  });
222
266
 
267
+ test("Context Mode resume injection: eligible prompts include one bounded snapshot block above inlined context", async (t) => {
268
+ const base = makeFixtureBase();
269
+ t.after(() => cleanup(base));
270
+ invalidateAllCaches();
271
+
272
+ seed(base, "M001");
273
+ writeArtifacts(base);
274
+ writeFileSync(
275
+ join(base, ".gsd", "last-snapshot.md"),
276
+ "# GSD context snapshot\n\nResume evidence.\n",
277
+ "utf-8",
278
+ );
279
+
280
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
281
+
282
+ assert.equal(prompt.match(/## Context Snapshot/g)?.length, 1);
283
+ assert.match(prompt, /Source: `\.gsd\/last-snapshot\.md`/);
284
+ assert.match(prompt, /Resume evidence/);
285
+ assert.ok(prompt.indexOf("## Context Mode") < prompt.indexOf("## Context Snapshot"));
286
+ assert.ok(prompt.indexOf("## Context Snapshot") < prompt.indexOf("## Inlined Context"));
287
+ });
288
+
289
+ test("Context Mode resume injection: missing snapshot does not add an empty block", async (t) => {
290
+ const base = makeFixtureBase();
291
+ t.after(() => cleanup(base));
292
+ invalidateAllCaches();
293
+
294
+ seed(base, "M001");
295
+ writeArtifacts(base);
296
+
297
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
298
+
299
+ assert.match(prompt, /## Context Mode/);
300
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
301
+ });
302
+
303
+ test("Context Mode resume injection: disabled mode suppresses guidance and snapshot reads", async (t) => {
304
+ const base = makeFixtureBase();
305
+ t.after(() => cleanup(base));
306
+ invalidateAllCaches();
307
+
308
+ seed(base, "M001");
309
+ writeArtifacts(base);
310
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
311
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
312
+
313
+ const prompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
314
+
315
+ assert.doesNotMatch(prompt, /## Context Mode/);
316
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
317
+ assert.doesNotMatch(prompt, /Do not inject/);
318
+ });
319
+
320
+ test("Context Mode resume injection: none-mode units do not inject snapshots", async () => {
321
+ const base = makeFixtureBase();
322
+ try {
323
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nNo lane.\n", "utf-8");
324
+ const prompt = await buildWorkflowPreferencesPrompt(base);
325
+ assert.doesNotMatch(prompt, /## Context Mode/);
326
+ assert.doesNotMatch(prompt, /## Context Snapshot/);
327
+ assert.doesNotMatch(prompt, /No lane/);
328
+ } finally {
329
+ cleanup(base);
330
+ }
331
+ });
332
+
333
+ test("Context Mode prompt suppression: disabled inlined, phase-anchor, and nested prompts omit Context Mode", async (t) => {
334
+ const base = makeFixtureBase();
335
+ t.after(() => cleanup(base));
336
+ invalidateAllCaches();
337
+
338
+ seed(base, "M001");
339
+ writeArtifacts(base);
340
+ insertGateRow({ milestoneId: "M001", sliceId: "S01", gateId: "Q3", scope: "slice" });
341
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), "---\ncontext_mode:\n enabled: false\n---\n", "utf-8");
342
+ writeFileSync(join(base, ".gsd", "last-snapshot.md"), "# GSD context snapshot\n\nDo not inject.\n", "utf-8");
343
+
344
+ const inlinedPrompt = await buildReassessRoadmapPrompt("M001", "Test", "S01", base);
345
+ assert.doesNotMatch(inlinedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
346
+
347
+ const phaseAnchorPrompt = await buildExecuteTaskPrompt("M001", "S01", "First", "T01", "Task", base);
348
+ assert.doesNotMatch(phaseAnchorPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
349
+
350
+ const nestedPrompt = await buildGateEvaluatePrompt("M001", "Test", "S01", "First", base);
351
+ assert.match(nestedPrompt, /Use this as the prompt for a `subagent` call/);
352
+ assert.doesNotMatch(nestedPrompt, /## Context Mode|Context Mode \(|## Context Snapshot/);
353
+ });
354
+
223
355
  // ─── v2 surface (#4924) ───────────────────────────────────────────────────
224
356
 
225
357
  const fakeBase: BaseResolverContext = {
@@ -21,6 +21,7 @@ import {
21
21
  markFailed,
22
22
  markStuck,
23
23
  markCanceled,
24
+ markLatestActiveForWorkerCanceled,
24
25
  getRecentForUnit,
25
26
  getLatestForUnit,
26
27
  } from "../db/unit-dispatches.ts";
@@ -196,6 +197,35 @@ test("markStuck and markCanceled set their respective statuses", (t) => {
196
197
  assert.equal(getLatestForUnit("M001/S01/T01")!.status, "canceled");
197
198
  });
198
199
 
200
+ test("markLatestActiveForWorkerCanceled cancels only the latest active dispatch for a worker", (t) => {
201
+ const base = makeBase();
202
+ t.after(() => cleanup(base));
203
+ const { workerId, leaseToken } = setup(base);
204
+
205
+ const first = recordDispatchClaim({
206
+ traceId: "tc-1", workerId, milestoneLeaseToken: leaseToken,
207
+ milestoneId: "M001", unitType: "plan-slice", unitId: "M001/S01",
208
+ });
209
+ assert.equal(first.ok, true);
210
+ if (!first.ok) return;
211
+ markCompleted(first.dispatchId);
212
+
213
+ const second = recordDispatchClaim({
214
+ traceId: "tc-2", workerId, milestoneLeaseToken: leaseToken,
215
+ milestoneId: "M001", unitType: "run-task", unitId: "M001/S01/T01",
216
+ });
217
+ assert.equal(second.ok, true);
218
+ if (!second.ok) return;
219
+ markRunning(second.dispatchId);
220
+
221
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), true);
222
+ assert.equal(getLatestForUnit("M001/S01")!.status, "completed");
223
+ const latest = getLatestForUnit("M001/S01/T01")!;
224
+ assert.equal(latest.status, "canceled");
225
+ assert.equal(latest.exit_reason, "signal-exit");
226
+ assert.equal(markLatestActiveForWorkerCanceled(workerId, "signal-exit"), false);
227
+ });
228
+
199
229
  test("terminal transitions do not overwrite an already terminal dispatch", (t) => {
200
230
  const base = makeBase();
201
231
  t.after(() => cleanup(base));
@@ -9,6 +9,7 @@ import {
9
9
  readUnitRuntimeRecord,
10
10
  writeUnitRuntimeRecord,
11
11
  } from "../unit-runtime.ts";
12
+ import { closeDatabase, insertMilestone, insertSlice, insertTask, openDatabase } from "../gsd-db.ts";
12
13
  import { clearPathCache } from '../paths.ts';
13
14
  import { test } from 'node:test';
14
15
  import assert from 'node:assert/strict';
@@ -72,6 +73,35 @@ console.log("\n=== runtime record cleanup ===");
72
73
  assert.deepStrictEqual(loaded, null, "record removed");
73
74
  }
74
75
 
76
+ console.log("\n=== execute-task durability trusts closed DB task status ===");
77
+ {
78
+ const dbBase = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-db-test-"));
79
+ mkdirSync(join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "tasks"), { recursive: true });
80
+ try {
81
+ openDatabase(join(dbBase, ".gsd", "gsd.db"));
82
+ insertMilestone({ id: "M300", title: "DB Milestone", status: "active" });
83
+ insertSlice({ id: "S01", milestoneId: "M300", title: "DB Slice", status: "in_progress" });
84
+ insertTask({ id: "T01", milestoneId: "M300", sliceId: "S01", title: "DB Task", status: "complete" });
85
+ writeFileSync(
86
+ join(dbBase, ".gsd", "milestones", "M300", "slices", "S01", "S01-PLAN.md"),
87
+ "# S01\n\n## Tasks\n\n- [ ] **T01: DB Task** `est:10m`\n",
88
+ "utf-8",
89
+ );
90
+ writeFileSync(join(dbBase, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S01: DB task\n", "utf-8");
91
+
92
+ const status = await inspectExecuteTaskDurability(dbBase, "M300/S01/T01");
93
+ assert.ok(status !== null, "db-complete: status exists");
94
+ assert.equal(status!.dbComplete, true, "db-complete: closed DB status is captured");
95
+ assert.equal(status!.summaryExists, false, "db-complete: summary can still be missing");
96
+ assert.equal(status!.taskChecked, false, "db-complete: checkbox can still be unchecked");
97
+ assert.equal(status!.nextActionAdvanced, false, "db-complete: next action can still point at task");
98
+ assert.equal(formatExecuteTaskRecoveryStatus(status!), "DB task status is closed");
99
+ } finally {
100
+ closeDatabase();
101
+ rmSync(dbBase, { recursive: true, force: true });
102
+ }
103
+ }
104
+
75
105
  console.log("\n=== hook unit type sanitization (slash in unitType) ===");
76
106
  {
77
107
  // Hook units have unitType like "hook/code-review" with a slash
@@ -165,6 +165,9 @@ test("executeMilestoneStatus returns milestone metadata and slice counts", async
165
165
  assert.equal(parsed.sliceCount, 1);
166
166
  assert.equal(parsed.slices[0].id, "S01");
167
167
  assert.equal(parsed.slices[0].taskCounts.pending, 1);
168
+ assert.equal(result.details.status, "active");
169
+ assert.equal(result.details.title, "Milestone One");
170
+ assert.deepEqual(result.details.slices, parsed.slices);
168
171
  } finally {
169
172
  closeDatabase();
170
173
  cleanup(base);