gsd-pi 2.80.0-dev.2848d9009 → 2.80.0-dev.2be7caf18

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 (182) hide show
  1. package/README.md +3 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  4. package/dist/resources/extensions/gsd/auto-prompts.js +13 -1
  5. package/dist/resources/extensions/gsd/auto.js +7 -2
  6. package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +27 -20
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +21 -0
  8. package/dist/resources/extensions/gsd/crash-recovery.js +56 -10
  9. package/dist/resources/extensions/gsd/guided-flow.js +33 -6
  10. package/dist/resources/extensions/gsd/migrate/command.js +40 -1
  11. package/dist/resources/extensions/gsd/migration-auto-check.js +87 -0
  12. package/dist/resources/extensions/gsd/planning-path-scope.js +26 -0
  13. package/dist/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  14. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  15. package/dist/resources/extensions/gsd/prompts/quick-task.md +1 -5
  16. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  17. package/dist/resources/extensions/gsd/quick.js +34 -2
  18. package/dist/resources/extensions/gsd/tools/context-mode-tool-result.js +15 -0
  19. package/dist/resources/extensions/gsd/tools/exec-search-tool.js +5 -0
  20. package/dist/resources/extensions/gsd/tools/exec-tool.js +3 -15
  21. package/dist/resources/extensions/gsd/tools/plan-slice.js +9 -0
  22. package/dist/resources/extensions/gsd/tools/plan-task.js +9 -0
  23. package/dist/resources/extensions/gsd/tools/resume-tool.js +5 -0
  24. package/dist/resources/extensions/gsd/unit-context-composer.js +12 -3
  25. package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
  26. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  27. package/dist/web/standalone/.next/BUILD_ID +1 -1
  28. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  29. package/dist/web/standalone/.next/build-manifest.json +2 -2
  30. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  31. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/index.html +1 -1
  48. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  55. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  57. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  58. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  59. package/package.json +1 -1
  60. package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
  61. package/packages/mcp-server/dist/workflow-tools.js +22 -17
  62. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  63. package/packages/mcp-server/src/workflow-tools.test.ts +66 -0
  64. package/packages/mcp-server/src/workflow-tools.ts +30 -16
  65. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  66. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/agent-session.js +3 -0
  68. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +3 -1
  70. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts +11 -0
  72. package/packages/pi-coding-agent/dist/core/compaction/compaction.d.ts.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/compaction/compaction.js +9 -0
  74. package/packages/pi-coding-agent/dist/core/compaction/compaction.js.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts +2 -0
  76. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.d.ts.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js +103 -0
  78. package/packages/pi-coding-agent/dist/core/compaction-threshold.test.js.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts +15 -0
  80. package/packages/pi-coding-agent/dist/core/db-snapshot.d.ts.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/db-snapshot.js +66 -0
  82. package/packages/pi-coding-agent/dist/core/db-snapshot.js.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts +2 -0
  84. package/packages/pi-coding-agent/dist/core/db-snapshot.test.d.ts.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js +24 -0
  86. package/packages/pi-coding-agent/dist/core/db-snapshot.test.js.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +1 -0
  88. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -0
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +2 -0
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +7 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/settings-manager.js +25 -0
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +3 -0
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +13 -5
  106. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js +53 -0
  108. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.test.js.map +1 -1
  109. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +3 -0
  111. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts +2 -0
  113. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.d.ts.map +1 -0
  114. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js +10 -0
  115. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage-safety-guard.test.js.map +1 -0
  116. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js +3 -2
  118. package/packages/pi-coding-agent/dist/resources/extensions/memory/storage.js.map +1 -1
  119. package/packages/pi-coding-agent/src/core/agent-session.ts +3 -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/db-snapshot.test.ts +32 -0
  124. package/packages/pi-coding-agent/src/core/db-snapshot.ts +66 -0
  125. package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +2 -0
  126. package/packages/pi-coding-agent/src/core/extensions/runner.ts +3 -0
  127. package/packages/pi-coding-agent/src/core/extensions/types.ts +7 -0
  128. package/packages/pi-coding-agent/src/core/settings-manager.ts +39 -1
  129. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +4 -0
  130. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.test.ts +56 -0
  131. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +22 -7
  132. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +3 -0
  133. package/packages/pi-coding-agent/src/resources/extensions/memory/storage-safety-guard.test.ts +14 -0
  134. package/packages/pi-coding-agent/src/resources/extensions/memory/storage.ts +3 -2
  135. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  136. package/packages/pi-tui/dist/tui.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/tui.js +18 -8
  138. package/packages/pi-tui/dist/tui.js.map +1 -1
  139. package/packages/pi-tui/src/tui.ts +20 -8
  140. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  141. package/src/resources/extensions/gsd/auto/loop.ts +50 -8
  142. package/src/resources/extensions/gsd/auto-prompts.ts +17 -1
  143. package/src/resources/extensions/gsd/auto.ts +11 -2
  144. package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +27 -19
  145. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +22 -0
  146. package/src/resources/extensions/gsd/crash-recovery.ts +67 -10
  147. package/src/resources/extensions/gsd/guided-flow.ts +39 -6
  148. package/src/resources/extensions/gsd/migrate/command.ts +47 -1
  149. package/src/resources/extensions/gsd/migration-auto-check.ts +129 -0
  150. package/src/resources/extensions/gsd/planning-path-scope.ts +35 -0
  151. package/src/resources/extensions/gsd/preferences-types.ts +1 -1
  152. package/src/resources/extensions/gsd/prompts/parallel-research-slices.md +1 -1
  153. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  154. package/src/resources/extensions/gsd/prompts/quick-task.md +1 -5
  155. package/src/resources/extensions/gsd/prompts/validate-milestone.md +2 -2
  156. package/src/resources/extensions/gsd/quick.ts +37 -2
  157. package/src/resources/extensions/gsd/tests/compaction-snapshot.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/crash-handler-secondary.test.ts +55 -0
  159. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +22 -0
  160. package/src/resources/extensions/gsd/tests/exec-history.test.ts +15 -0
  161. package/src/resources/extensions/gsd/tests/exec-sandbox.test.ts +65 -0
  162. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +48 -0
  163. package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +127 -0
  164. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +50 -0
  165. package/src/resources/extensions/gsd/tests/plan-task.test.ts +21 -0
  166. package/src/resources/extensions/gsd/tests/prompt-path-audit.test.ts +40 -0
  167. package/src/resources/extensions/gsd/tests/quick-external-gsd.test.ts +40 -0
  168. package/src/resources/extensions/gsd/tests/smart-entry-complete.test.ts +20 -0
  169. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +17 -0
  170. package/src/resources/extensions/gsd/tests/status-db-open.test.ts +9 -0
  171. package/src/resources/extensions/gsd/tests/unit-context-composer.test.ts +136 -4
  172. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +7 -0
  173. package/src/resources/extensions/gsd/tools/context-mode-tool-result.ts +25 -0
  174. package/src/resources/extensions/gsd/tools/exec-search-tool.ts +7 -7
  175. package/src/resources/extensions/gsd/tools/exec-tool.ts +4 -23
  176. package/src/resources/extensions/gsd/tools/plan-slice.ts +13 -0
  177. package/src/resources/extensions/gsd/tools/plan-task.ts +10 -0
  178. package/src/resources/extensions/gsd/tools/resume-tool.ts +7 -7
  179. package/src/resources/extensions/gsd/unit-context-composer.ts +19 -4
  180. package/src/resources/extensions/gsd/unit-runtime.ts +14 -0
  181. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_buildManifest.js +0 -0
  182. /package/dist/web/standalone/.next/static/{vaQ47E8aNlbbNEDoeME6G → 3EDDd9ULaybSupPoA_vf-}/_ssgManifest.js +0 -0
@@ -30,6 +30,7 @@ import {
30
30
  isLockProcessAlive,
31
31
  } from "../crash-recovery.ts";
32
32
  import { normalizeRealPath } from "../paths.ts";
33
+ import { writeUnitRuntimeRecord } from "../unit-runtime.ts";
33
34
 
34
35
  function makeBase(): string {
35
36
  const base = mkdtempSync(join(tmpdir(), "gsd-crash-recovery-"));
@@ -102,6 +103,27 @@ test("readCrashLock synthesizes LockData from a stale dead worker (no dispatches
102
103
  assert.ok(lock!.startedAt, "startedAt populated from workers.started_at");
103
104
  });
104
105
 
106
+ test("readCrashLock falls back to latest in-flight runtime record when dispatch claim is missing", (t) => {
107
+ const base = makeBase();
108
+ t.after(() => cleanup(base));
109
+ openDatabase(join(base, ".gsd", "gsd.db"));
110
+ const projectRoot = normalizeRealPath(base);
111
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
112
+ writeUnitRuntimeRecord(base, "execute-task", "M008/S04/T02", 1778069087937, {
113
+ phase: "dispatched",
114
+ lastProgressAt: 1778069087937,
115
+ lastProgressKind: "dispatch",
116
+ });
117
+ setWorkerPid(workerId, 99999);
118
+ expireWorker(workerId);
119
+
120
+ const lock = readCrashLock(base);
121
+ assert.ok(lock, "stale worker surfaced as a crash lock");
122
+ assert.equal(lock!.unitType, "execute-task");
123
+ assert.equal(lock!.unitId, "M008/S04/T02");
124
+ assert.equal(lock!.unitStartedAt, new Date(1778069087937).toISOString());
125
+ });
126
+
105
127
  test("readCrashLock includes the most recent dispatch as unitType/unitId", (t) => {
106
128
  const base = makeBase();
107
129
  t.after(() => cleanup(base));
@@ -108,6 +108,21 @@ test('executeExecSearch: returns helpful empty-state message when no matches', (
108
108
  }
109
109
  });
110
110
 
111
+ test('executeExecSearch: returns disabled error when context_mode.enabled=false', () => {
112
+ const base = freshBase();
113
+ try {
114
+ writeRun(base, 'should-not-surface', { stdout: 'hidden\n' });
115
+ const result = executeExecSearch(
116
+ { query: 'hidden' },
117
+ { baseDir: base, preferences: { context_mode: { enabled: false } } },
118
+ );
119
+ assert.equal(result.isError, true);
120
+ assert.equal((result.details as { error?: string }).error, 'context_mode_disabled');
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
125
+
111
126
  test('executeExecSearch: includes stdout_path and preview in details', () => {
112
127
  const base = freshBase();
113
128
  try {
@@ -7,6 +7,7 @@ import { join } from 'node:path';
7
7
  import { EXEC_DEFAULTS, runExecSandbox, type ExecSandboxOptions } from '../exec-sandbox.ts';
8
8
  import { buildExecOptions, executeGsdExec } from '../tools/exec-tool.ts';
9
9
  import { isContextModeEnabled } from '../preferences-types.ts';
10
+ import { validatePreferences } from '../preferences-validation.ts';
10
11
 
11
12
  function freshBase(): string {
12
13
  return mkdtempSync(join(tmpdir(), 'gsd-exec-test-'));
@@ -174,6 +175,52 @@ test('executeGsdExec: runs when enabled explicitly set to true', async () => {
174
175
  }
175
176
  });
176
177
 
178
+ test('executeGsdExec: forwards custom exec_env_allowlist from preferences', async () => {
179
+ const base = freshBase();
180
+ try {
181
+ const result = await executeGsdExec(
182
+ {
183
+ runtime: 'bash',
184
+ script: 'printf "allowed=%s blocked=%s\\n" "$GSD_ALLOWED" "$GSD_BLOCKED"',
185
+ },
186
+ {
187
+ baseDir: base,
188
+ preferences: {
189
+ context_mode: {
190
+ enabled: true,
191
+ exec_env_allowlist: ['GSD_ALLOWED'],
192
+ },
193
+ },
194
+ env: {
195
+ PATH: '/usr/bin:/bin',
196
+ HOME: '/tmp',
197
+ GSD_ALLOWED: 'yes',
198
+ GSD_BLOCKED: 'no',
199
+ },
200
+ },
201
+ );
202
+ assert.ok(!result.isError);
203
+ assert.match(result.content[0].text, /allowed=yes blocked=/);
204
+ assert.doesNotMatch(result.content[0].text, /blocked=no/);
205
+ } finally {
206
+ cleanup(base);
207
+ }
208
+ });
209
+
210
+ test('executeGsdExec: enforces per-call timeout override end-to-end', async () => {
211
+ const base = freshBase();
212
+ try {
213
+ const result = await executeGsdExec(
214
+ { runtime: 'bash', script: 'sleep 2', timeout_ms: 1 },
215
+ { baseDir: base, preferences: { context_mode: { enabled: true, exec_timeout_ms: 10_000 } } },
216
+ );
217
+ assert.equal(result.details.timed_out, true);
218
+ assert.equal(result.isError, true);
219
+ } finally {
220
+ cleanup(base);
221
+ }
222
+ });
223
+
177
224
  test('executeGsdExec: rejects empty script', async () => {
178
225
  const base = freshBase();
179
226
  try {
@@ -188,6 +235,24 @@ test('executeGsdExec: rejects empty script', async () => {
188
235
  }
189
236
  });
190
237
 
238
+ test('validatePreferences: rejects invalid context_mode preference values', () => {
239
+ const result = validatePreferences({
240
+ context_mode: {
241
+ enabled: 'false',
242
+ exec_timeout_ms: 999,
243
+ exec_stdout_cap_bytes: 1,
244
+ exec_digest_chars: -1,
245
+ exec_env_allowlist: ['GOOD_NAME', 'bad-name'],
246
+ },
247
+ } as any);
248
+ assert.ok(result.errors.length > 0);
249
+ assert.ok(result.errors.includes('context_mode.enabled must be a boolean'));
250
+ assert.ok(result.errors.includes('context_mode.exec_timeout_ms must be a number between 1000 and 600000'));
251
+ assert.ok(result.errors.includes('context_mode.exec_stdout_cap_bytes must be a number between 4096 and 16777216'));
252
+ assert.ok(result.errors.includes('context_mode.exec_digest_chars must be a number between 0 and 4000'));
253
+ assert.ok(result.errors.includes('context_mode.exec_env_allowlist must be an array of valid env var names'));
254
+ });
255
+
191
256
  test('isContextModeEnabled: defaults to true; only explicit false disables', () => {
192
257
  assert.equal(isContextModeEnabled(undefined), true, 'undefined prefs → on');
193
258
  assert.equal(isContextModeEnabled(null), true, 'null prefs → on');
@@ -13,6 +13,9 @@ import { parseRoadmap, parsePlan } from '../parsers-legacy.ts';
13
13
  import { parseSummary } from '../files.ts';
14
14
  import { deriveState } from '../state.ts';
15
15
  import { invalidateAllCaches } from '../cache.ts';
16
+ import { ensureDbOpen } from '../bootstrap/dynamic-tools.ts';
17
+ import { closeDatabase, getAllMilestones } from '../gsd-db.ts';
18
+ import { importWrittenMigrationToDb } from '../migrate/command.ts';
16
19
  import type {
17
20
  GSDProject,
18
21
  GSDMilestone,
@@ -250,6 +253,51 @@ test('Scenario 1: Incomplete project — write, parse, deriveState', async () =>
250
253
  }
251
254
  });
252
255
 
256
+ test('Scenario 1b: written migration imports into authoritative DB state', async () => {
257
+ const base = mkdtempSync(join(tmpdir(), 'gsd-writer-db-import-'));
258
+ try {
259
+ const project = buildIncompleteProject();
260
+ await writeGSDDirectory(project, base);
261
+
262
+ assert.equal(await ensureDbOpen(base), true, 'db import: ensureDbOpen creates authoritative DB');
263
+
264
+ invalidateAllCaches();
265
+ const before = await deriveState(base);
266
+ assert.equal(before.activeMilestone, null, 'db import: markdown-only migration is invisible before DB import');
267
+
268
+ const imported = await importWrittenMigrationToDb(base);
269
+ assert.deepStrictEqual(imported.hierarchy, { milestones: 1, slices: 2, tasks: 3 }, 'db import: hierarchy counts');
270
+
271
+ invalidateAllCaches();
272
+ const after = await deriveState(base);
273
+ assert.deepStrictEqual(after.phase, 'executing', 'db import: deriveState sees imported DB hierarchy');
274
+ assert.deepStrictEqual(after.activeMilestone?.id, 'M001', 'db import: active milestone');
275
+ assert.deepStrictEqual(after.activeSlice?.id, 'S02', 'db import: active slice');
276
+ assert.deepStrictEqual(after.activeTask?.id, 'T03', 'db import: active task');
277
+ } finally {
278
+ closeDatabase();
279
+ rmSync(base, { recursive: true, force: true });
280
+ }
281
+ });
282
+
283
+ test('Scenario 1c: DB import verification fails when preview counts do not match', async () => {
284
+ const base = mkdtempSync(join(tmpdir(), 'gsd-writer-db-check-'));
285
+ try {
286
+ const project = buildIncompleteProject();
287
+ await writeGSDDirectory(project, base);
288
+
289
+ const preview = generatePreview(project);
290
+ await assert.rejects(
291
+ () => importWrittenMigrationToDb(base, { ...preview, totalTasks: preview.totalTasks + 1 }),
292
+ /migration DB import verification failed: tasks 3\/4/,
293
+ );
294
+ assert.deepStrictEqual(getAllMilestones(), [], 'db import: failed verification rolls back hierarchy rewrite');
295
+ } finally {
296
+ closeDatabase();
297
+ rmSync(base, { recursive: true, force: true });
298
+ }
299
+ });
300
+
253
301
  // ─── Scenario 2: Fully complete project ────────────────────────────────
254
302
 
255
303
  test('Scenario 2: Fully complete project — deriveState phase', async () => {
@@ -0,0 +1,127 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import test from "node:test";
6
+
7
+ import { ensureDbOpen } from "../bootstrap/dynamic-tools.ts";
8
+ import {
9
+ _getAdapter,
10
+ closeDatabase,
11
+ getAllMilestones,
12
+ getSliceTasks,
13
+ } from "../gsd-db.ts";
14
+ import {
15
+ autoImportMarkdownHierarchyIfDbMismatch,
16
+ countMarkdownHierarchy,
17
+ } from "../migration-auto-check.ts";
18
+ import { writeGSDDirectory } from "../migrate/writer.ts";
19
+ import type { GSDProject } from "../migrate/types.ts";
20
+
21
+ function makeBase(): string {
22
+ return mkdtempSync(join(tmpdir(), "gsd-migration-auto-check-"));
23
+ }
24
+
25
+ function cleanup(base: string): void {
26
+ closeDatabase();
27
+ rmSync(base, { recursive: true, force: true });
28
+ }
29
+
30
+ function projectFixture(): GSDProject {
31
+ return {
32
+ projectContent: "# Legacy Project\n",
33
+ decisionsContent: "",
34
+ requirements: [],
35
+ milestones: [
36
+ {
37
+ id: "M001",
38
+ title: "Legacy Milestone",
39
+ vision: "Carry forward previous work",
40
+ successCriteria: ["Existing task is visible"],
41
+ research: null,
42
+ boundaryMap: [],
43
+ slices: [
44
+ {
45
+ id: "S01",
46
+ title: "Legacy Slice",
47
+ risk: "medium",
48
+ depends: [],
49
+ done: false,
50
+ demo: "Legacy slice demo",
51
+ goal: "Legacy slice demo",
52
+ research: null,
53
+ summary: null,
54
+ tasks: [
55
+ {
56
+ id: "T01",
57
+ title: "Legacy Task",
58
+ description: "Task carried from markdown",
59
+ done: false,
60
+ estimate: "",
61
+ files: ["src/index.ts"],
62
+ mustHaves: [],
63
+ summary: null,
64
+ },
65
+ ],
66
+ },
67
+ ],
68
+ },
69
+ ],
70
+ };
71
+ }
72
+
73
+ test("migration auto-check imports markdown hierarchy when DB is empty", async () => {
74
+ const base = makeBase();
75
+ try {
76
+ await writeGSDDirectory(projectFixture(), base);
77
+ assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 1, tasks: 1 });
78
+
79
+ assert.equal(await ensureDbOpen(base), true);
80
+ assert.equal(getAllMilestones().length, 0, "fresh authoritative DB starts empty");
81
+
82
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
83
+ assert.equal(result.action, "imported");
84
+ assert.equal(result.reason, "db-empty");
85
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
86
+ assert.equal(getAllMilestones().length, 1);
87
+ assert.equal(getSliceTasks("M001", "S01").length, 1);
88
+ } finally {
89
+ cleanup(base);
90
+ }
91
+ });
92
+
93
+ test("migration auto-check repairs DB hierarchy count mismatch", async () => {
94
+ const base = makeBase();
95
+ try {
96
+ await writeGSDDirectory(projectFixture(), base);
97
+ await autoImportMarkdownHierarchyIfDbMismatch(base);
98
+
99
+ _getAdapter()!.prepare("DELETE FROM tasks WHERE milestone_id = ? AND slice_id = ? AND id = ?").run("M001", "S01", "T01");
100
+ assert.equal(getSliceTasks("M001", "S01").length, 0, "test fixture simulates stale DB task count");
101
+
102
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
103
+ assert.equal(result.action, "imported");
104
+ assert.equal(result.reason, "count-mismatch");
105
+ assert.deepEqual(result.beforeDb, { milestones: 1, slices: 1, tasks: 0 });
106
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
107
+ assert.equal(getSliceTasks("M001", "S01").length, 1);
108
+ } finally {
109
+ cleanup(base);
110
+ }
111
+ });
112
+
113
+ test("migration auto-check leaves matching DB hierarchy alone", async () => {
114
+ const base = makeBase();
115
+ try {
116
+ await writeGSDDirectory(projectFixture(), base);
117
+ await autoImportMarkdownHierarchyIfDbMismatch(base);
118
+
119
+ const result = await autoImportMarkdownHierarchyIfDbMismatch(base);
120
+ assert.equal(result.action, "none");
121
+ assert.equal(result.reason, "in-sync");
122
+ assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
123
+ assert.deepEqual(result.afterDb, { milestones: 1, slices: 1, tasks: 1 });
124
+ } finally {
125
+ cleanup(base);
126
+ }
127
+ });
@@ -172,6 +172,56 @@ test('handlePlanSlice rejects invalid payloads', async () => {
172
172
  }
173
173
  });
174
174
 
175
+ test('handlePlanSlice rejects absolute task IO paths outside the active worktree', async () => {
176
+ const base = makeTmpBase();
177
+ openDatabase(join(base, '.gsd', 'gsd.db'));
178
+
179
+ try {
180
+ seedParentSlice();
181
+ const outside = join(tmpdir(), 'outside-checkout', 'index.html');
182
+ const result = await handlePlanSlice({
183
+ ...validParams(),
184
+ tasks: [
185
+ {
186
+ ...validParams().tasks[0],
187
+ inputs: [outside],
188
+ expectedOutput: [outside],
189
+ },
190
+ ],
191
+ }, base);
192
+
193
+ assert.ok('error' in result);
194
+ assert.match(result.error, /validation failed: tasks\[0\]\.inputs contains absolute path outside working directory/);
195
+ assert.equal(getSliceTasks('M001', 'S02').length, 0, 'invalid planning IO must not persist tasks');
196
+ } finally {
197
+ cleanup(base);
198
+ }
199
+ });
200
+
201
+ test('handlePlanSlice accepts absolute task IO paths inside the active worktree', async () => {
202
+ const base = makeTmpBase();
203
+ openDatabase(join(base, '.gsd', 'gsd.db'));
204
+
205
+ try {
206
+ seedParentSlice();
207
+ const inside = join(base, 'index.html');
208
+ const result = await handlePlanSlice({
209
+ ...validParams(),
210
+ tasks: [
211
+ {
212
+ ...validParams().tasks[0],
213
+ inputs: [inside],
214
+ expectedOutput: [inside],
215
+ },
216
+ ],
217
+ }, base);
218
+
219
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
220
+ } finally {
221
+ cleanup(base);
222
+ }
223
+ });
224
+
175
225
  test('handlePlanSlice rejects missing parent slice', async () => {
176
226
  const base = makeTmpBase();
177
227
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -79,6 +79,27 @@ test('handlePlanTask rejects invalid payloads', async () => {
79
79
  }
80
80
  });
81
81
 
82
+ test('handlePlanTask rejects absolute task IO paths outside the active worktree', async () => {
83
+ const base = makeTmpBase();
84
+ openDatabase(join(base, '.gsd', 'gsd.db'));
85
+
86
+ try {
87
+ seedParent();
88
+ const outside = join(tmpdir(), 'outside-checkout', 'index.html');
89
+ const result = await handlePlanTask({
90
+ ...validParams(),
91
+ inputs: [outside],
92
+ expectedOutput: [outside],
93
+ }, base);
94
+
95
+ assert.ok('error' in result);
96
+ assert.match(result.error, /validation failed: inputs contains absolute path outside working directory/);
97
+ assert.equal(getTask('M001', 'S02', 'T02'), null, 'invalid planning IO must not persist the task');
98
+ } finally {
99
+ cleanup(base);
100
+ }
101
+ });
102
+
82
103
  test('handlePlanTask rejects missing parent slice', async () => {
83
104
  const base = makeTmpBase();
84
105
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -0,0 +1,40 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync, readdirSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dirname } from "node:path";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const promptsDir = join(__dirname, "..", "prompts");
11
+
12
+ test("prompt templates do not reference legacy milestone-root .gsd paths", () => {
13
+ const offenders: string[] = [];
14
+ for (const file of readdirSync(promptsDir)) {
15
+ if (!file.endsWith(".md")) continue;
16
+ const content = readFileSync(join(promptsDir, file), "utf-8");
17
+ const legacyPatterns = [
18
+ /\.gsd\/\{\{(?:milestoneId|mid)\}\}\//g,
19
+ /\.gsd\/<milestone-id>\//g,
20
+ /\.gsd\/<ID>\//g,
21
+ ];
22
+ for (const pattern of legacyPatterns) {
23
+ if (pattern.test(content)) {
24
+ offenders.push(`${file}: ${pattern.source}`);
25
+ }
26
+ }
27
+ }
28
+
29
+ assert.deepEqual(
30
+ offenders,
31
+ [],
32
+ "Milestone artifacts must use .gsd/milestones/<MID>/..., not legacy .gsd/<MID>/...",
33
+ );
34
+ });
35
+
36
+ test("quick task prompt delegates commit policy to quick.ts", () => {
37
+ const content = readFileSync(join(promptsDir, "quick-task.md"), "utf-8");
38
+ assert.match(content, /\{\{commitInstruction\}\}/);
39
+ assert.doesNotMatch(content, /Stage only relevant files/);
40
+ });
@@ -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
+ });
@@ -59,6 +59,26 @@ test("guided-flow complete branch offers a chooser for next milestone or status"
59
59
  assert.match(branchChunk, /dispatchWorkflow\(pi, await prepareAndBuildDiscussPrompt\(/, "complete branch should dispatch the prepared discuss prompt");
60
60
  });
61
61
 
62
+ test("guided-flow quick task choices prompt for text and route through /gsd quick", () => {
63
+ const guidedFlowSource = readFileSync(join(import.meta.dirname, "..", "guided-flow.ts"), "utf-8");
64
+
65
+ assert.match(
66
+ guidedFlowSource,
67
+ /async function runQuickTaskChoice\([\s\S]*ctx\.ui\.input\("Quick task"[\s\S]*await import\("\.\/quick\.js"\)[\s\S]*await handleQuick\(task,\s*ctx,\s*pi\)/,
68
+ "quick task chooser should prompt for task text and route through handleQuick",
69
+ );
70
+
71
+ const notifyOnlyPattern = /if \(choice === "quick_task"\) \{\s*ctx\.ui\.notify\("Run \/gsd quick <task>/;
72
+ assert.doesNotMatch(
73
+ guidedFlowSource,
74
+ notifyOnlyPattern,
75
+ "quick task chooser must not merely print instructions after selection",
76
+ );
77
+
78
+ const quickChoiceCalls = guidedFlowSource.match(/if \(choice === "quick_task"\) \{\s*await runQuickTaskChoice\(ctx,\s*pi\);/g) ?? [];
79
+ assert.equal(quickChoiceCalls.length, 3, "all guided-flow quick task choices should dispatch the helper");
80
+ });
81
+
62
82
  test("dispatcher routes multi-word freeform /gsd input through /gsd do", () => {
63
83
  const dispatcherSource = readFileSync(join(import.meta.dirname, "..", "commands", "dispatcher.ts"), "utf-8");
64
84
 
@@ -96,6 +96,23 @@ test("auto bootstrap validates blocked directories before touching .gsd migratio
96
96
  );
97
97
  });
98
98
 
99
+ test("fresh start registers the auto worker before bootstrap enters worktree flow (#5405)", () => {
100
+ const autoSrc = readGsdFile("auto.ts");
101
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
102
+ const startAutoBody = autoSrc.slice(startAutoIdx);
103
+
104
+ const preBootstrapRegisterIdx = startAutoBody.indexOf("registerAutoWorkerForSession(s, base);");
105
+ const bootstrapCallIdx = startAutoBody.indexOf("const ready = await bootstrapAutoSession(");
106
+
107
+ assert.ok(startAutoIdx > -1, "startAuto should exist");
108
+ assert.ok(preBootstrapRegisterIdx > -1, "startAuto should register worker before bootstrap");
109
+ assert.ok(bootstrapCallIdx > -1, "startAuto should call bootstrapAutoSession");
110
+ assert.ok(
111
+ preBootstrapRegisterIdx < bootstrapCallIdx,
112
+ "worker registration must happen before bootstrap so enterMilestone can claim milestone leases on first entry",
113
+ );
114
+ });
115
+
99
116
  test("startAutoDetached reports failures asynchronously (#3733)", () => {
100
117
  const autoSrc = readGsdFile("auto.ts");
101
118
 
@@ -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
  });