@opengsd/gsd-pi 1.9.0-dev.d2070876 → 1.9.0-dev.e10b60d4

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 (117) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/loop.js +4 -24
  3. package/dist/resources/extensions/gsd/auto/run-unit.js +1 -19
  4. package/dist/resources/extensions/gsd/auto-start.js +9 -29
  5. package/dist/resources/extensions/gsd/auto-worktree-sync.js +3 -7
  6. package/dist/resources/extensions/gsd/auto.js +1 -12
  7. package/dist/resources/extensions/gsd/doctor-engine-checks.js +2 -21
  8. package/dist/resources/extensions/gsd/flat-phase-migration.js +0 -12
  9. package/dist/resources/extensions/gsd/gsd-db.js +31 -75
  10. package/dist/resources/extensions/gsd/worktree-state-projection.js +18 -20
  11. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +12 -12
  14. package/dist/web/standalone/.next/build-manifest.json +2 -2
  15. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  16. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/index.html +1 -1
  33. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app-paths-manifest.json +12 -12
  40. package/dist/web/standalone/.next/server/chunks/1991.js +2 -2
  41. package/dist/web/standalone/.next/server/chunks/5124.js +1 -1
  42. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  43. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  45. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  46. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  47. package/package.json +1 -1
  48. package/packages/cloud-mcp-gateway/package.json +2 -2
  49. package/packages/contracts/package.json +1 -1
  50. package/packages/daemon/package.json +4 -4
  51. package/packages/gsd-agent-core/package.json +5 -5
  52. package/packages/gsd-agent-modes/package.json +7 -7
  53. package/packages/gsd-cloud/README.md +3 -9
  54. package/packages/gsd-cloud/dist/cli.d.ts.map +1 -1
  55. package/packages/gsd-cloud/dist/cli.js +15 -121
  56. package/packages/gsd-cloud/dist/cli.js.map +1 -1
  57. package/packages/gsd-cloud/dist/cloud-config.d.ts +1 -1
  58. package/packages/gsd-cloud/dist/cloud-config.d.ts.map +1 -1
  59. package/packages/gsd-cloud/dist/cloud-config.js +1 -7
  60. package/packages/gsd-cloud/dist/cloud-config.js.map +1 -1
  61. package/packages/gsd-cloud/dist/cloud-runtime.d.ts +3 -1
  62. package/packages/gsd-cloud/dist/cloud-runtime.d.ts.map +1 -1
  63. package/packages/gsd-cloud/dist/cloud-runtime.js +7 -9
  64. package/packages/gsd-cloud/dist/cloud-runtime.js.map +1 -1
  65. package/packages/gsd-cloud/package.json +2 -2
  66. package/packages/mcp-server/dist/cli-runner.d.ts +0 -3
  67. package/packages/mcp-server/dist/cli-runner.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/cli-runner.js +33 -119
  69. package/packages/mcp-server/dist/cli-runner.js.map +1 -1
  70. package/packages/mcp-server/package.json +4 -4
  71. package/packages/native/package.json +1 -1
  72. package/packages/pi-agent-core/package.json +1 -1
  73. package/packages/pi-ai/dist/models.generated.d.ts +0 -17
  74. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  75. package/packages/pi-ai/dist/models.generated.js +0 -17
  76. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  77. package/packages/pi-ai/package.json +1 -1
  78. package/packages/pi-coding-agent/dist/core/skills.d.ts +0 -5
  79. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/skills.js +0 -54
  81. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  82. package/packages/pi-coding-agent/package.json +7 -7
  83. package/packages/pi-tui/package.json +2 -2
  84. package/packages/rpc-client/package.json +2 -2
  85. package/pkg/package.json +1 -1
  86. package/src/resources/extensions/gsd/auto/loop.ts +4 -48
  87. package/src/resources/extensions/gsd/auto/run-unit.ts +1 -20
  88. package/src/resources/extensions/gsd/auto-start.ts +8 -31
  89. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -10
  90. package/src/resources/extensions/gsd/auto.ts +1 -13
  91. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -27
  92. package/src/resources/extensions/gsd/flat-phase-migration.ts +0 -14
  93. package/src/resources/extensions/gsd/gsd-db.ts +33 -85
  94. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +0 -156
  95. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +1 -21
  96. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +1 -74
  97. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +0 -83
  98. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +9 -8
  99. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -32
  100. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +0 -13
  101. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  102. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -60
  103. package/src/resources/extensions/gsd/worktree-state-projection.ts +24 -21
  104. package/packages/gsd-cloud/dist/background-cli.test.d.ts +0 -2
  105. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +0 -1
  106. package/packages/gsd-cloud/dist/background-cli.test.js +0 -248
  107. package/packages/gsd-cloud/dist/background-cli.test.js.map +0 -1
  108. package/packages/gsd-cloud/dist/runtime-process.d.ts +0 -27
  109. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +0 -1
  110. package/packages/gsd-cloud/dist/runtime-process.js +0 -272
  111. package/packages/gsd-cloud/dist/runtime-process.js.map +0 -1
  112. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +0 -2
  113. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +0 -1
  114. package/packages/gsd-cloud/dist/runtime-process.test.js +0 -97
  115. package/packages/gsd-cloud/dist/runtime-process.test.js.map +0 -1
  116. /package/dist/web/standalone/.next/static/{sggX-h41OjePo-FjdArP5 → V9MAVyYVPboQUB9KTTAMi}/_buildManifest.js +0 -0
  117. /package/dist/web/standalone/.next/static/{sggX-h41OjePo-FjdArP5 → V9MAVyYVPboQUB9KTTAMi}/_ssgManifest.js +0 -0
@@ -155,19 +155,6 @@ function isInsideFlatProjectionBackup(backupDir: string, src: string): boolean {
155
155
  return src === phaseBackupDir || src.startsWith(`${phaseBackupDir}/`) || src.startsWith(`${phaseBackupDir}\\`);
156
156
  }
157
157
 
158
- function removeFlatProjectionBackup(backupDir: string): void {
159
- const phaseBackupDir = join(backupDir, "__phases");
160
- if (!existsSync(phaseBackupDir)) return;
161
- try {
162
- removePathWithRetries(phaseBackupDir);
163
- } catch (err) {
164
- logWarning(
165
- "migration",
166
- `flat-phase migration succeeded but could not remove temporary ${LAYOUT_SEGMENTS.level1}/ backup: ${(err as Error).message}`,
167
- );
168
- }
169
- }
170
-
171
158
  function restoreFlatProjectionFromBackup(basePath: string, backupDir: string): void {
172
159
  const phaseBackupDir = join(backupDir, "__phases");
173
160
  if (!existsSync(phaseBackupDir)) return;
@@ -507,5 +494,4 @@ export async function migrateToFlatPhase(basePath: string): Promise<void> {
507
494
  `flat-phase migration succeeded but could not remove ${LEGACY_MIGRATING_SEGMENT}: ${(err as Error).message}`,
508
495
  );
509
496
  }
510
- removeFlatProjectionBackup(backupDir);
511
497
  }
@@ -67,13 +67,6 @@ export type { SliceRow, TaskRow } from "./db-task-slice-rows.js";
67
67
 
68
68
  import { TERMINAL_STATUS_SQL } from "./db/sql-constants.js";
69
69
  import { applyStatusTransition } from "./db/writers/status.js";
70
- import {
71
- LAYOUT_SEGMENTS,
72
- derivePhaseSlug,
73
- milestoneIdToPhaseNum,
74
- milestoneIdUniqueSuffix,
75
- phaseDirName,
76
- } from "./layout-policy.js";
77
70
 
78
71
  export function insertDecision(d: Omit<Decision, "seq">): void {
79
72
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
@@ -213,47 +206,6 @@ export function insertArtifact(a: {
213
206
  });
214
207
  }
215
208
 
216
- function canonicalPhaseDirNameForDb(milestoneId: string, title: string): string {
217
- const phaseNum = milestoneIdToPhaseNum(milestoneId);
218
- const slug = derivePhaseSlug(title || milestoneId);
219
- const suffix = milestoneIdUniqueSuffix(milestoneId);
220
- return phaseDirName(phaseNum, suffix ? `${suffix}-${slug}` : slug);
221
- }
222
-
223
- function reconcileMilestonePhaseArtifactPaths(milestoneId: string, title: string): void {
224
- const db = getDbOrNull()!;
225
- const phaseNumPrefix = String(milestoneIdToPhaseNum(milestoneId)).padStart(2, "0");
226
- const canonicalDir = canonicalPhaseDirNameForDb(milestoneId, title);
227
- const staleRows = db.prepare(
228
- `SELECT path
229
- FROM artifacts
230
- WHERE milestone_id = :milestone_id
231
- AND path LIKE :phase_prefix`,
232
- ).all({
233
- ":milestone_id": milestoneId,
234
- ":phase_prefix": `${LAYOUT_SEGMENTS.level1}/${phaseNumPrefix}-%/%`,
235
- }) as Array<{ path: string }>;
236
-
237
- const updatePath = db.prepare("UPDATE artifacts SET path = :new_path WHERE path = :old_path");
238
- const deletePath = db.prepare("DELETE FROM artifacts WHERE path = :path");
239
- const existingPath = db.prepare("SELECT 1 AS present FROM artifacts WHERE path = :path");
240
-
241
- for (const row of staleRows) {
242
- const parts = row.path.split("/");
243
- if (parts.length < 3) continue;
244
- if (parts[0] !== LAYOUT_SEGMENTS.level1) continue;
245
- if (parts[1] === canonicalDir) continue;
246
- if (!parts[1]?.startsWith(`${phaseNumPrefix}-`)) continue;
247
-
248
- const newPath = [LAYOUT_SEGMENTS.level1, canonicalDir, ...parts.slice(2)].join("/");
249
- if (existingPath.get({ ":path": newPath })) {
250
- deletePath.run({ ":path": row.path });
251
- continue;
252
- }
253
- updatePath.run({ ":new_path": newPath, ":old_path": row.path });
254
- }
255
- }
256
-
257
209
  export interface MilestonePlanningRecord {
258
210
  vision: string;
259
211
  successCriteria: string[];
@@ -335,43 +287,39 @@ export function insertMilestone(m: {
335
287
 
336
288
  export function upsertMilestonePlanning(milestoneId: string, planning: Partial<MilestonePlanningRecord> & { title?: string; status?: string; depends_on?: string[] }): void {
337
289
  if (!getDbOrNull()!) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
338
- transaction(() => {
339
- getDbOrNull()!.prepare(
340
- `UPDATE milestones SET
341
- title = COALESCE(NULLIF(:title, ''), title),
342
- status = COALESCE(NULLIF(:status, ''), status),
343
- depends_on = COALESCE(:depends_on, depends_on),
344
- vision = COALESCE(:vision, vision),
345
- success_criteria = COALESCE(:success_criteria, success_criteria),
346
- key_risks = COALESCE(:key_risks, key_risks),
347
- proof_strategy = COALESCE(:proof_strategy, proof_strategy),
348
- verification_contract = COALESCE(:verification_contract, verification_contract),
349
- verification_integration = COALESCE(:verification_integration, verification_integration),
350
- verification_operational = COALESCE(:verification_operational, verification_operational),
351
- verification_uat = COALESCE(:verification_uat, verification_uat),
352
- definition_of_done = COALESCE(:definition_of_done, definition_of_done),
353
- requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
354
- boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
355
- WHERE id = :id`,
356
- ).run({
357
- ":id": milestoneId,
358
- ":title": planning.title ?? "",
359
- ":status": planning.status ?? "",
360
- ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
361
- ":vision": planning.vision ?? null,
362
- ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
363
- ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
364
- ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
365
- ":verification_contract": planning.verificationContract ?? null,
366
- ":verification_integration": planning.verificationIntegration ?? null,
367
- ":verification_operational": planning.verificationOperational ?? null,
368
- ":verification_uat": planning.verificationUat ?? null,
369
- ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
370
- ":requirement_coverage": planning.requirementCoverage ?? null,
371
- ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
372
- });
373
- const finalTitle = planning.title?.trim();
374
- if (finalTitle) reconcileMilestonePhaseArtifactPaths(milestoneId, finalTitle);
290
+ getDbOrNull()!.prepare(
291
+ `UPDATE milestones SET
292
+ title = COALESCE(NULLIF(:title, ''), title),
293
+ status = COALESCE(NULLIF(:status, ''), status),
294
+ depends_on = COALESCE(:depends_on, depends_on),
295
+ vision = COALESCE(:vision, vision),
296
+ success_criteria = COALESCE(:success_criteria, success_criteria),
297
+ key_risks = COALESCE(:key_risks, key_risks),
298
+ proof_strategy = COALESCE(:proof_strategy, proof_strategy),
299
+ verification_contract = COALESCE(:verification_contract, verification_contract),
300
+ verification_integration = COALESCE(:verification_integration, verification_integration),
301
+ verification_operational = COALESCE(:verification_operational, verification_operational),
302
+ verification_uat = COALESCE(:verification_uat, verification_uat),
303
+ definition_of_done = COALESCE(:definition_of_done, definition_of_done),
304
+ requirement_coverage = COALESCE(:requirement_coverage, requirement_coverage),
305
+ boundary_map_markdown = COALESCE(:boundary_map_markdown, boundary_map_markdown)
306
+ WHERE id = :id`,
307
+ ).run({
308
+ ":id": milestoneId,
309
+ ":title": planning.title ?? "",
310
+ ":status": planning.status ?? "",
311
+ ":depends_on": planning.depends_on ? JSON.stringify(planning.depends_on) : null,
312
+ ":vision": planning.vision ?? null,
313
+ ":success_criteria": planning.successCriteria ? JSON.stringify(planning.successCriteria) : null,
314
+ ":key_risks": planning.keyRisks ? JSON.stringify(planning.keyRisks) : null,
315
+ ":proof_strategy": planning.proofStrategy ? JSON.stringify(planning.proofStrategy) : null,
316
+ ":verification_contract": planning.verificationContract ?? null,
317
+ ":verification_integration": planning.verificationIntegration ?? null,
318
+ ":verification_operational": planning.verificationOperational ?? null,
319
+ ":verification_uat": planning.verificationUat ?? null,
320
+ ":definition_of_done": planning.definitionOfDone ? JSON.stringify(planning.definitionOfDone) : null,
321
+ ":requirement_coverage": planning.requirementCoverage ?? null,
322
+ ":boundary_map_markdown": planning.boundaryMapMarkdown ?? null,
375
323
  });
376
324
  }
377
325
 
@@ -41,7 +41,6 @@ import { claimMilestoneLease } from "../db/milestone-leases.js";
41
41
  import { recordDispatchClaim, markCanceled } from "../db/unit-dispatches.js";
42
42
  import { setRuntimeKv, getRuntimeKv } from "../db/runtime-kv.js";
43
43
  import { SourceObservationStore } from "../source-observations.js";
44
- import { autoCommitCurrentBranch } from "../worktree.js";
45
44
 
46
45
  // ─── Helpers ─────────────────────────────────────────────────────────────────
47
46
 
@@ -833,37 +832,6 @@ test("runUnit returns cancelled when session creation fails", async () => {
833
832
  assert.equal(pi.calls.length, 0);
834
833
  });
835
834
 
836
- test("runUnit returns cancelled when command context lacks newSession", async () => {
837
- _resetPendingResolve();
838
-
839
- const ctx = {
840
- ...makeMockCtx(),
841
- ui: {
842
- notify: () => {},
843
- setStatus: () => {},
844
- setWorkingMessage: () => {},
845
- },
846
- sessionManager: {
847
- getEntries: () => [],
848
- },
849
- modelRegistry: {
850
- getProviderAuthMode: () => undefined,
851
- isProviderRequestReady: () => true,
852
- },
853
- } as any;
854
- const pi = makeMockPi();
855
- const s = makeMockSession();
856
- s.cmdCtx = {} as any;
857
-
858
- const result = await runUnit(ctx, pi, s, "task", "T01", "prompt");
859
-
860
- assert.equal(result.status, "cancelled");
861
- assert.equal(result.errorContext?.category, "session-failed");
862
- assert.equal(result.errorContext?.isTransient, false);
863
- assert.match(result.errorContext?.message ?? "", /missing newSession/);
864
- assert.equal(pi.calls.length, 0);
865
- });
866
-
867
835
  test("runUnit: TypeError from newSession is classified as structural (isTransient: false)", async () => {
868
836
  // Regression for #572: a TypeError thrown from newSession (e.g. "something is
869
837
  // not a function") indicates a programming error, not a transient provider
@@ -1614,130 +1582,6 @@ test("autoLoop exits when s.active is set to false", async (t) => {
1614
1582
  );
1615
1583
  });
1616
1584
 
1617
- test("autoLoop stops before dispatch when command context lacks newSession", async () => {
1618
- _resetPendingResolve();
1619
-
1620
- const ctx = makeMockCtx();
1621
- ctx.ui.setStatus = () => {};
1622
- const pi = makeMockPi();
1623
- const s = makeLoopSession({
1624
- cmdCtx: {
1625
- getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
1626
- },
1627
- });
1628
- let stopReason: string | undefined;
1629
- let preserveWorktree: boolean | undefined;
1630
- let deriveCalled = false;
1631
-
1632
- try {
1633
- const deps = makeMockDeps({
1634
- stopAuto: async (_ctx, _pi, reason, options) => {
1635
- stopReason = reason;
1636
- preserveWorktree = options?.preserveWorktree;
1637
- s.active = false;
1638
- },
1639
- deriveState: async () => {
1640
- deriveCalled = true;
1641
- throw new Error("deriveState should not run without command session support");
1642
- },
1643
- });
1644
-
1645
- await autoLoop(ctx, pi, s, deps);
1646
-
1647
- assert.equal(stopReason, "Auto-mode has no command context for dispatch.");
1648
- assert.equal(preserveWorktree, true);
1649
- assert.equal(deriveCalled, false);
1650
- assert.equal(pi.calls.length, 0);
1651
- } finally {
1652
- rmSync(s.basePath, { recursive: true, force: true });
1653
- }
1654
- });
1655
-
1656
- test("autoLoop commits open unit work when command context lacks newSession", async () => {
1657
- _resetPendingResolve();
1658
-
1659
- const ctx = makeMockCtx();
1660
- ctx.ui.setStatus = () => {};
1661
- const pi = makeMockPi();
1662
- const s = makeLoopSession({
1663
- cmdCtx: {
1664
- getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
1665
- },
1666
- currentUnit: { type: "execute-task", id: "T01", startedAt: Date.now() },
1667
- });
1668
- const outputPath = join(s.basePath, "executor-output.txt");
1669
- let autoCommitArgs: { unitType: string; unitId: string } | undefined;
1670
- let preserveWorktree: boolean | undefined;
1671
-
1672
- try {
1673
- const deps = makeMockDeps({
1674
- autoCommitUnit: async (basePath, unitType, unitId) => {
1675
- autoCommitArgs = { unitType, unitId };
1676
- return autoCommitCurrentBranch(basePath, unitType, unitId);
1677
- },
1678
- stopAuto: async (_ctx, _pi, _reason, options) => {
1679
- preserveWorktree = options?.preserveWorktree;
1680
- s.active = false;
1681
- },
1682
- deriveState: async () => {
1683
- throw new Error("deriveState should not run without command session support");
1684
- },
1685
- });
1686
-
1687
- writeFileSync(outputPath, "open unit work before stop\n", "utf-8");
1688
-
1689
- await autoLoop(ctx, pi, s, deps);
1690
-
1691
- assert.deepEqual(autoCommitArgs, { unitType: "execute-task", unitId: "T01" });
1692
- assert.equal(preserveWorktree, true);
1693
- const committed = execSync("git show HEAD:executor-output.txt", {
1694
- cwd: s.basePath,
1695
- encoding: "utf-8",
1696
- });
1697
- assert.equal(committed, "open unit work before stop\n");
1698
- assert.equal(pi.calls.length, 0);
1699
- } finally {
1700
- rmSync(s.basePath, { recursive: true, force: true });
1701
- }
1702
- });
1703
-
1704
- test("autoLoop snapshots dirty work when unit dispatch crashes", async () => {
1705
- _resetPendingResolve();
1706
-
1707
- const ctx = makeMockCtx();
1708
- ctx.ui.setStatus = () => {};
1709
- const pi = makeMockPi();
1710
- const s = makeLoopSession();
1711
- const outputPath = join(s.basePath, "executor-output.txt");
1712
- let autoCommitCalls = 0;
1713
-
1714
- try {
1715
- const deps = makeMockDeps({
1716
- ensurePreconditions: () => {
1717
- writeFileSync(outputPath, "saved before crash\n", "utf-8");
1718
- throw new Error("dispatch crash after file write");
1719
- },
1720
- autoCommitUnit: async (basePath, unitType, unitId) => {
1721
- autoCommitCalls++;
1722
- const commitMsg = autoCommitCurrentBranch(basePath, unitType, unitId);
1723
- s.active = false;
1724
- return commitMsg;
1725
- },
1726
- });
1727
-
1728
- await autoLoop(ctx, pi, s, deps);
1729
-
1730
- assert.equal(autoCommitCalls, 1);
1731
- const committed = execSync("git show HEAD:executor-output.txt", {
1732
- cwd: s.basePath,
1733
- encoding: "utf-8",
1734
- });
1735
- assert.equal(committed, "saved before crash\n");
1736
- } finally {
1737
- rmSync(s.basePath, { recursive: true, force: true });
1738
- }
1739
- });
1740
-
1741
1585
  test("autoLoop pauses visibly when Auto Orchestration Module is not wired", async () => {
1742
1586
  _resetPendingResolve();
1743
1587
 
@@ -4,7 +4,7 @@ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
7
- import { hasGitIndexLockForTest, reconcileFlatPhaseBootstrapLayout } from "../auto-start.ts";
7
+ import { hasGitIndexLockForTest } from "../auto-start.ts";
8
8
 
9
9
  test("bootstrapAutoSession detects .git/index.lock without deleting it", (t) => {
10
10
  const base = mkdtempSync(join(tmpdir(), "gsd-index-lock-"));
@@ -24,23 +24,3 @@ test("bootstrapAutoSession reports no index lock when the lock file is absent",
24
24
 
25
25
  assert.equal(hasGitIndexLockForTest(base), false);
26
26
  });
27
-
28
- test("bootstrapAutoSession removes empty legacy milestones dir in flat-phase projects", (t) => {
29
- const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-clean-"));
30
- t.after(() => rmSync(base, { recursive: true, force: true }));
31
- mkdirSync(join(base, ".gsd", "phases"), { recursive: true });
32
- mkdirSync(join(base, ".gsd", "milestones"), { recursive: true });
33
-
34
- assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
35
- assert.equal(existsSync(join(base, ".gsd", "phases")), true);
36
- assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
37
- });
38
-
39
- test("bootstrapAutoSession creates phases instead of milestones for empty GSD state", (t) => {
40
- const base = mkdtempSync(join(tmpdir(), "gsd-flat-bootstrap-init-"));
41
- t.after(() => rmSync(base, { recursive: true, force: true }));
42
-
43
- assert.equal(reconcileFlatPhaseBootstrapLayout(base), true);
44
- assert.equal(existsSync(join(base, ".gsd", "phases")), true);
45
- assert.equal(existsSync(join(base, ".gsd", "milestones")), false);
46
- });
@@ -3,8 +3,7 @@
3
3
 
4
4
  import { describe, test } from "node:test";
5
5
  import assert from "node:assert/strict";
6
- import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
- import { execSync } from "node:child_process";
6
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync } from "node:fs";
8
7
  import { join } from "node:path";
9
8
  import { tmpdir } from "node:os";
10
9
 
@@ -97,78 +96,6 @@ describe("hook dispatch session workspace root", () => {
97
96
  assert.equal(dispatched, true);
98
97
  assert.deepEqual(newSessionOptions, { workspaceRoot: basePath });
99
98
  });
100
-
101
- test("dispatchHookUnit commits in-flight work when command context lacks newSession", async (t) => {
102
- const originalCwd = process.cwd();
103
- const basePath = realpathSync(mkdtempSync(join(tmpdir(), "gsd-hook-nosnap-")));
104
- mkdirSync(join(basePath, ".gsd"), { recursive: true });
105
- execSync("git init --initial-branch=main", { cwd: basePath, stdio: "ignore" });
106
- execSync("git config user.email test@test.com", { cwd: basePath, stdio: "ignore" });
107
- execSync("git config user.name Test", { cwd: basePath, stdio: "ignore" });
108
- execSync("git commit --allow-empty -m init", { cwd: basePath, stdio: "ignore" });
109
-
110
- autoSession.reset();
111
- autoSession.active = true;
112
- autoSession.basePath = basePath;
113
- autoSession.originalBasePath = basePath;
114
- // Simulate an in-flight unit whose executor already wrote files, then a hook
115
- // fires with a command context missing newSession.
116
- autoSession.currentUnit = {
117
- type: "execute-task",
118
- id: "M001/S01/T01",
119
- startedAt: Date.now(),
120
- };
121
- t.after(() => {
122
- try {
123
- process.chdir(originalCwd);
124
- } catch {
125
- // best effort cleanup after cwd-sensitive dispatch tests
126
- }
127
- autoSession.reset();
128
- rmSync(basePath, { recursive: true, force: true });
129
- });
130
-
131
- const outputPath = join(basePath, "executor-output.txt");
132
- writeFileSync(outputPath, "in-flight executor work\n", "utf-8");
133
-
134
- // Command context intentionally lacks newSession — the guard must snapshot
135
- // the dirty work before stopping instead of discarding it.
136
- const ctx = {
137
- ui: {
138
- notify: () => {},
139
- setStatus: () => {},
140
- setWidget: () => {},
141
- },
142
- modelRegistry: {
143
- getAvailable: () => [],
144
- },
145
- sessionManager: {
146
- getSessionFile: () => join(basePath, "session.jsonl"),
147
- },
148
- };
149
- const pi = {
150
- sendMessage: () => {},
151
- setModel: async () => true,
152
- };
153
-
154
- const dispatched = await dispatchHookUnit(
155
- ctx as any,
156
- pi as any,
157
- "review",
158
- "execute-task",
159
- "M001/S01/T01",
160
- "review the completed unit",
161
- undefined,
162
- basePath,
163
- );
164
-
165
- assert.equal(dispatched, false);
166
- const committed = execSync("git show HEAD:executor-output.txt", {
167
- cwd: basePath,
168
- encoding: "utf-8",
169
- });
170
- assert.equal(committed, "in-flight executor work\n");
171
- });
172
99
  });
173
100
 
174
101
  describe("deep setup approval questions pause immediately", () => {
@@ -167,45 +167,6 @@ test("checkEngineHealth keeps PLAN checkbox divergence after stale projection fl
167
167
  assert.match(readFileSync(plan.planPath, "utf-8"), /- \[ \] \*\*T01\*\*:/);
168
168
  });
169
169
 
170
- test("checkEngineHealth ignores stale suffixed flat-phase duplicate when bare milestone exists", async (t) => {
171
- const base = mkdtempSync(join(tmpdir(), "gsd-doctor-checkbox-flat-duplicate-"));
172
- t.after(() => rmSync(base, { recursive: true, force: true }));
173
-
174
- const gsdDir = join(base, ".gsd");
175
- mkdirSync(gsdDir, { recursive: true });
176
-
177
- openDatabase(join(gsdDir, "gsd.db"));
178
- insertMilestone({ id: "M003", title: "M003-vaz73w: New milestone M003-vaz73w", status: "active" });
179
- insertSlice({ id: "S01", milestoneId: "M003", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
180
- for (const taskId of ["T01", "T02", "T03"]) {
181
- insertTask({ id: taskId, milestoneId: "M003", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
182
- }
183
-
184
- insertMilestone({ id: "M003-vaz73w", title: "New milestone M003-vaz73w", status: "active" });
185
- insertSlice({ id: "S01", milestoneId: "M003-vaz73w", title: "Slice", status: "pending", risk: "low", depends: [], sequence: 1 });
186
- for (const taskId of ["T01", "T02", "T03"]) {
187
- insertTask({ id: taskId, milestoneId: "M003-vaz73w", sliceId: "S01", title: taskId, status: "complete", sequence: Number(taskId.slice(1)) });
188
- }
189
-
190
- const canonicalPlan = await renderPlanFromDb(base, "M003", "S01");
191
- const staleDir = join(gsdDir, "phases", "03-vaz73w-new-milestone-m003-vaz73w");
192
- mkdirSync(staleDir, { recursive: true });
193
- writeFileSync(
194
- join(staleDir, "03-01-PLAN.md"),
195
- canonicalPlan.content.replace("- [x] **T03**:", "- [ ] **T03**:"),
196
- "utf-8",
197
- );
198
-
199
- const issues: any[] = [];
200
- await checkEngineHealth(base, issues, []);
201
-
202
- assert.deepEqual(
203
- issues.filter((issue) => issue.code === "checkbox_db_status_divergence").map((issue) => issue.unitId),
204
- [],
205
- "stale suffixed duplicate phase projection must not compete with the bare milestone projection",
206
- );
207
- });
208
-
209
170
  test("checkEngineHealth reads task checkboxes from the <tasks> block, not a stray line above it", async (t) => {
210
171
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-task-section-"));
211
172
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -467,50 +428,6 @@ test("checkEngineHealth repair prunes stale phases artifact rows with present mi
467
428
  assert.deepEqual(rows.map((row) => row.path), []);
468
429
  });
469
430
 
470
- test("checkEngineHealth repair prunes stale phases artifact rows with renamed flat-phase files", async (t) => {
471
- const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-renamed-flat-phase-artifact-"));
472
- t.after(() => rmSync(base, { recursive: true, force: true }));
473
-
474
- const gsdDir = join(base, ".gsd");
475
- const stalePath = "phases/01-new-milestone-m001/01-01-PLAN.md";
476
- const replacementPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-01-PLAN.md";
477
- mkdirSync(join(gsdDir, "phases", "01-lokably-brand-foundation-and-welcome-pag"), { recursive: true });
478
- writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
479
-
480
- openDatabase(join(gsdDir, "gsd.db"));
481
- insertArtifact({
482
- path: stalePath,
483
- artifact_type: "PLAN",
484
- milestone_id: "M001",
485
- slice_id: "S01",
486
- task_id: null,
487
- full_content: "# stale plan\n",
488
- });
489
-
490
- const beforeIssues: any[] = [];
491
- await checkEngineHealth(base, beforeIssues, []);
492
-
493
- const beforeIssue = beforeIssues.find((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath);
494
- assert.ok(beforeIssue, "stale renamed phases row should be reported when repair is off");
495
- assert.equal(beforeIssue.fixable, true, "renamed phases rows with a flat replacement should be fixable");
496
-
497
- const repairIssues: any[] = [];
498
- const fixes: string[] = [];
499
- await checkEngineHealth(base, repairIssues, fixes, { repair: true });
500
-
501
- assert.equal(
502
- repairIssues.some((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath),
503
- false,
504
- "repair should not report stale rows it pruned",
505
- );
506
- assert.ok(fixes.includes(`pruned stale flat-phase artifact row ${stalePath}`));
507
-
508
- const rows = _getAdapter()!
509
- .prepare("SELECT path FROM artifacts ORDER BY path")
510
- .all() as Array<{ path: string }>;
511
- assert.deepEqual(rows.map((row) => row.path), []);
512
- });
513
-
514
431
  test("checkEngineHealth repair keeps phases rows whose own file is still present", async (t) => {
515
432
  const base = mkdtempSync(join(tmpdir(), "gsd-doctor-keep-present-phase-artifact-"));
516
433
  t.after(() => rmSync(base, { recursive: true, force: true }));
@@ -122,7 +122,7 @@ test("migrateToFlatPhase creates a backup", async () => {
122
122
  assert.ok(backups.length >= 1, "at least one migrate-* backup dir should exist");
123
123
  });
124
124
 
125
- test("migrateToFlatPhase removes disposable phases snapshot after successful migration", async () => {
125
+ test("migrateToFlatPhase backs up existing phases projection before clearing it", async () => {
126
126
  const base = makeTmp();
127
127
  const reviewPath = join(base, ".gsd", "phases", "01-foundation", "PLAN-REVIEW.md");
128
128
  mkdirSync(join(base, ".gsd", "phases", "01-foundation"), { recursive: true });
@@ -133,10 +133,11 @@ test("migrateToFlatPhase removes disposable phases snapshot after successful mig
133
133
  const backupRoot = join(base, ".gsd-backups");
134
134
  const backups = readdirSync(backupRoot).filter(d => d.startsWith("migrate-"));
135
135
  assert.equal(backups.length, 1, "one migrate backup should exist");
136
+ const backedUpReview = join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md");
136
137
  assert.equal(
137
- existsSync(join(backupRoot, backups[0]!, "__phases")),
138
- false,
139
- "temporary phases/ recovery snapshot should be removed after success",
138
+ readFileSync(backedUpReview, "utf-8"),
139
+ "# Plan Review\n\nHand-authored review.",
140
+ "pre-existing phases/ files should have a recovery copy",
140
141
  );
141
142
  });
142
143
 
@@ -201,7 +202,7 @@ test("failed migration rolls back without leaking a .gsd-backups/migrate-* dir",
201
202
  assert.equal(leaked.length, 0, "rollback must delete the migrate-* backup it created");
202
203
  });
203
204
 
204
- test("resumed migration removes disposable phases snapshot after success", async () => {
205
+ test("resumed migration stores phases backup in retained migrate snapshot", async () => {
205
206
  const base = makeTmp();
206
207
  const milestonesPath = join(base, ".gsd", "milestones");
207
208
  const migratingPath = join(base, ".gsd", "milestones.migrating");
@@ -217,9 +218,9 @@ test("resumed migration removes disposable phases snapshot after success", async
217
218
  const backups = readdirSync(backupRoot).filter((d) => d.startsWith("migrate-"));
218
219
  assert.equal(backups.length, 1, "resumed migration should create one retained migrate backup");
219
220
  assert.equal(
220
- existsSync(join(backupRoot, backups[0]!, "__phases")),
221
- false,
222
- "temporary phases/ recovery snapshot should be removed after resumed success",
221
+ readFileSync(join(backupRoot, backups[0]!, "__phases", "01-foundation", "PLAN-REVIEW.md"), "utf-8"),
222
+ "# Plan Review\n\nResume recovery copy.",
223
+ "resume should retain the phases snapshot under .gsd-backups",
223
224
  );
224
225
  assert.equal(
225
226
  existsSync(join(migratingPath, "__phases")),
@@ -46,9 +46,6 @@ import {
46
46
  tryCreateMemoriesFts,
47
47
  _isLikelyWslDrvFsPathForTest,
48
48
  _shouldAttemptVacuumRecoveryForTest,
49
- insertArtifact,
50
- getArtifact,
51
- upsertMilestonePlanning,
52
49
  } from '../gsd-db.ts';
53
50
  import { MigrationBackupError } from '../db-migration-backup.ts';
54
51
  import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
@@ -360,35 +357,6 @@ describe('gsd-db', () => {
360
357
  closeDatabase();
361
358
  });
362
359
 
363
- test("gsd-db: upsertMilestonePlanning rewrites placeholder phase artifact paths to final title slug", () => {
364
- openDatabase(":memory:");
365
-
366
- const stalePath = "phases/01-new-milestone-m001/01-ROADMAP.md";
367
- const finalPath = "phases/01-lokably-brand-foundation-and-welcome-pag/01-ROADMAP.md";
368
- insertMilestone({ id: "M001", title: "New milestone M001", status: "active" });
369
- insertArtifact({
370
- path: stalePath,
371
- artifact_type: "ROADMAP",
372
- milestone_id: "M001",
373
- slice_id: null,
374
- task_id: null,
375
- full_content: "# Placeholder roadmap\n",
376
- });
377
-
378
- upsertMilestonePlanning("M001", {
379
- title: "Lokably brand foundation and welcome page rebuild",
380
- });
381
-
382
- assert.equal(getArtifact(stalePath), null, "placeholder-derived artifact row should be removed");
383
- assert.equal(
384
- getArtifact(finalPath)?.full_content,
385
- "# Placeholder roadmap\n",
386
- "artifact row should move to the final title-derived phase path",
387
- );
388
-
389
- closeDatabase();
390
- });
391
-
392
360
  test('gsd-db: active_requirements view excludes superseded', () => {
393
361
  openDatabase(':memory:');
394
362