@opengsd/gsd-pi 1.9.0-dev.97ef11e3 → 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 (113) 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 +10 -10
  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 +10 -10
  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/package.json +7 -7
  79. package/packages/pi-tui/package.json +2 -2
  80. package/packages/rpc-client/package.json +2 -2
  81. package/pkg/package.json +1 -1
  82. package/src/resources/extensions/gsd/auto/loop.ts +4 -48
  83. package/src/resources/extensions/gsd/auto/run-unit.ts +1 -20
  84. package/src/resources/extensions/gsd/auto-start.ts +8 -31
  85. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -10
  86. package/src/resources/extensions/gsd/auto.ts +1 -13
  87. package/src/resources/extensions/gsd/doctor-engine-checks.ts +2 -27
  88. package/src/resources/extensions/gsd/flat-phase-migration.ts +0 -14
  89. package/src/resources/extensions/gsd/gsd-db.ts +33 -85
  90. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +0 -156
  91. package/src/resources/extensions/gsd/tests/auto-start-index-lock.test.ts +1 -21
  92. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +1 -74
  93. package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +0 -83
  94. package/src/resources/extensions/gsd/tests/flat-phase-migration.test.ts +9 -8
  95. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +0 -32
  96. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +0 -13
  97. package/src/resources/extensions/gsd/tests/worktree-state-projection.test.ts +2 -2
  98. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +0 -60
  99. package/src/resources/extensions/gsd/worktree-state-projection.ts +24 -21
  100. package/packages/gsd-cloud/dist/background-cli.test.d.ts +0 -2
  101. package/packages/gsd-cloud/dist/background-cli.test.d.ts.map +0 -1
  102. package/packages/gsd-cloud/dist/background-cli.test.js +0 -248
  103. package/packages/gsd-cloud/dist/background-cli.test.js.map +0 -1
  104. package/packages/gsd-cloud/dist/runtime-process.d.ts +0 -27
  105. package/packages/gsd-cloud/dist/runtime-process.d.ts.map +0 -1
  106. package/packages/gsd-cloud/dist/runtime-process.js +0 -272
  107. package/packages/gsd-cloud/dist/runtime-process.js.map +0 -1
  108. package/packages/gsd-cloud/dist/runtime-process.test.d.ts +0 -2
  109. package/packages/gsd-cloud/dist/runtime-process.test.d.ts.map +0 -1
  110. package/packages/gsd-cloud/dist/runtime-process.test.js +0 -97
  111. package/packages/gsd-cloud/dist/runtime-process.test.js.map +0 -1
  112. /package/dist/web/standalone/.next/static/{DXD9fZPnqtjZSlT5xdC3v → V9MAVyYVPboQUB9KTTAMi}/_buildManifest.js +0 -0
  113. /package/dist/web/standalone/.next/static/{DXD9fZPnqtjZSlT5xdC3v → V9MAVyYVPboQUB9KTTAMi}/_ssgManifest.js +0 -0
@@ -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
 
@@ -1428,19 +1428,6 @@ process.exit(result.status ?? 0);
1428
1428
  rmSync(repo, { recursive: true, force: true });
1429
1429
  });
1430
1430
 
1431
- test('Integration branch: write does not create legacy milestones scaffold', () => {
1432
- const repo = initBranchTestRepo();
1433
- mkdirSync(join(repo, ".gsd", "phases"), { recursive: true });
1434
-
1435
- writeIntegrationBranch(repo, "M001", "f-123-new-thing");
1436
-
1437
- assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), "f-123-new-thing");
1438
- assert.ok(existsSync(join(repo, ".gsd", "M001-META.json")), "metadata is written at the flat .gsd root");
1439
- assert.ok(!existsSync(join(repo, ".gsd", "milestones")), "legacy milestones/ root is not created");
1440
-
1441
- rmSync(repo, { recursive: true, force: true });
1442
- });
1443
-
1444
1431
  // ─── writeIntegrationBranch: updates when branch changes (#300) ──────
1445
1432
 
1446
1433
  test('Integration branch: updates on branch change', () => {
@@ -117,8 +117,8 @@ test("projectRootToWorktree projects flat-phase discuss artifacts before dispatc
117
117
  new WorktreeStateProjection().projectRootToWorktree(scope);
118
118
 
119
119
  assert.ok(
120
- !existsSync(join(worktree, ".gsd", "milestones")),
121
- "stale metadata-only legacy milestones/ scaffold is not projected into the worktree",
120
+ existsSync(join(worktree, ".gsd", "milestones", "M001", "M001-META.json")),
121
+ "milestone metadata is projected into the worktree",
122
122
  );
123
123
  assert.ok(
124
124
  existsSync(join(worktree, ".gsd", "phases", "01-foundation", "01-CONTEXT.md")),
@@ -246,66 +246,6 @@ describe('worktree-sync-milestones', async () => {
246
246
  }
247
247
  }
248
248
 
249
- // ─── 7b. flat-phase sync skips empty legacy milestones root ───────────
250
- console.log('\n=== 7b. flat-phase sync skips empty legacy milestones root ===');
251
- {
252
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-main-'));
253
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-wt-'));
254
-
255
- try {
256
- const phaseDir = join(mainBase, '.gsd', 'phases', '01-foundation');
257
- mkdirSync(phaseDir, { recursive: true });
258
- mkdirSync(join(mainBase, '.gsd', 'milestones'), { recursive: true });
259
- mkdirSync(join(wtBase, '.gsd'), { recursive: true });
260
- writeFileSync(join(phaseDir, '01-CONTEXT.md'), '# Foundation\n');
261
-
262
- syncGsdStateToWorktree(mainBase, wtBase);
263
-
264
- assert.ok(
265
- existsSync(join(wtBase, '.gsd', 'phases', '01-foundation', '01-CONTEXT.md')),
266
- 'flat-phase artifact is synced to worktree',
267
- );
268
- assert.ok(
269
- !existsSync(join(wtBase, '.gsd', 'milestones')),
270
- 'empty legacy milestones/ root is not recreated in worktree',
271
- );
272
- } finally {
273
- rmSync(mainBase, { recursive: true, force: true });
274
- rmSync(wtBase, { recursive: true, force: true });
275
- }
276
- }
277
-
278
- // ─── 7c. flat-phase sync skips metadata-only legacy milestone dirs ────
279
- console.log('\n=== 7c. flat-phase sync skips metadata-only legacy milestone dirs ===');
280
- {
281
- const mainBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-meta-main-'));
282
- const wtBase = mkdtempSync(join(tmpdir(), 'gsd-wt-sync-flat-meta-wt-'));
283
-
284
- try {
285
- const phaseDir = join(mainBase, '.gsd', 'phases', '01-foundation');
286
- const metaDir = join(mainBase, '.gsd', 'milestones', 'M001');
287
- mkdirSync(phaseDir, { recursive: true });
288
- mkdirSync(metaDir, { recursive: true });
289
- mkdirSync(join(wtBase, '.gsd'), { recursive: true });
290
- writeFileSync(join(phaseDir, '01-CONTEXT.md'), '# Foundation\n');
291
- writeFileSync(join(metaDir, 'M001-META.json'), '{"integrationBranch":"main"}\n');
292
-
293
- syncGsdStateToWorktree(mainBase, wtBase);
294
-
295
- assert.ok(
296
- existsSync(join(wtBase, '.gsd', 'phases', '01-foundation', '01-CONTEXT.md')),
297
- 'flat-phase artifact is synced to worktree',
298
- );
299
- assert.ok(
300
- !existsSync(join(wtBase, '.gsd', 'milestones')),
301
- 'metadata-only legacy milestones/ scaffold is not recreated in worktree',
302
- );
303
- } finally {
304
- rmSync(mainBase, { recursive: true, force: true });
305
- rmSync(wtBase, { recursive: true, force: true });
306
- }
307
- }
308
-
309
249
  // ─── 8. syncWorktreeStateBack does not copy task projections ───────────
310
250
  console.log('\n=== 8. syncWorktreeStateBack leaves task projections in worktree ===');
311
251
  {
@@ -32,7 +32,7 @@ import {
32
32
  import { join } from "node:path";
33
33
 
34
34
  import { reconcileWorktreeDb } from "./gsd-db.js";
35
- import { dirIsContentBearingLegacyMilestone, resolveGsdPathContract } from "./paths.js";
35
+ import { resolveGsdPathContract } from "./paths.js";
36
36
  import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
37
37
  import type { MilestoneScope } from "./workspace.js";
38
38
  import { logError, logWarning } from "./workflow-logger.js";
@@ -161,7 +161,6 @@ function syncOtherMilestoneArtifacts(
161
161
  // additive safeCopyRecursive; skip it here to avoid redundant work.
162
162
  if (milestoneEntry.name === currentMilestoneId) continue;
163
163
  const srcMilestoneDir = join(srcMilestonesDir, milestoneEntry.name);
164
- if (!dirIsContentBearingLegacyMilestone(srcMilestoneDir)) continue;
165
164
  const dstMilestoneDir = join(dstMilestonesDir, milestoneEntry.name);
166
165
 
167
166
  // Additively project the entire milestone subtree (force:false), not just
@@ -272,32 +271,36 @@ export function _projectRootToWorktreeImpl(
272
271
  // Without this, worktree-local files (e.g. VALIDATION.md written
273
272
  // by validate-milestone) get clobbered by stale project root copies,
274
273
  // causing an infinite re-validation loop (#1886).
275
- const prMilestoneDir = join(prGsd, "milestones", milestoneId);
276
- const wtMilestoneDir = join(wtGsd, "milestones", milestoneId);
277
- if (dirIsContentBearingLegacyMilestone(prMilestoneDir)) {
278
- safeCopyRecursive(prMilestoneDir, wtMilestoneDir, { force: false });
279
-
280
- // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
281
- // The additive-only copy above preserves worktree-local files, but
282
- // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
283
- // syncs it to the project root, the worktree may retain a stale copy (e.g.
284
- // verdict:fail while the project root has verdict:pass from a retry). On
285
- // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
286
- // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
287
- // run-uat indefinitely (stuck-loop ×9).
288
- forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
289
- forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
290
- }
274
+ safeCopyRecursive(
275
+ join(prGsd, "milestones", milestoneId),
276
+ join(wtGsd, "milestones", milestoneId),
277
+ { force: false },
278
+ );
291
279
 
292
- // Additively project the full subtree of every OTHER content-bearing legacy
293
- // milestone so worktree-bound units can read prior-milestone context artifacts
294
- // without recreating empty/meta-only milestones/ scaffolds in flat-phase projects.
280
+ // Additively project the full subtree of every OTHER milestone so
281
+ // worktree-bound units can read prior-milestone context artifacts and so
282
+ // completed milestones retain their per-slice/per-task SUMMARY.md and
283
+ // UAT.md on the worktree filesystem (otherwise the stale-render detector
284
+ // flags them as "complete in DB but missing on disk").
295
285
  syncOtherMilestoneArtifacts(
296
286
  join(prGsd, "milestones"),
297
287
  join(wtGsd, "milestones"),
298
288
  milestoneId,
299
289
  );
300
290
 
291
+ // Force-sync ASSESSMENT files that have a verdict from project root (#2821).
292
+ // The additive-only copy above preserves worktree-local files, but
293
+ // ASSESSMENT files are special: after run-uat writes a verdict and post-unit
294
+ // syncs it to the project root, the worktree may retain a stale copy (e.g.
295
+ // verdict:fail while the project root has verdict:pass from a retry). On
296
+ // session resume the DB is rebuilt from disk, and if the stale ASSESSMENT
297
+ // persists, checkNeedsRunUat finds no passing verdict → re-dispatches
298
+ // run-uat indefinitely (stuck-loop ×9).
299
+ const prMilestoneDir = join(prGsd, "milestones", milestoneId);
300
+ const wtMilestoneDir = join(wtGsd, "milestones", milestoneId);
301
+ forceOverwriteValidationWithVerdict(prMilestoneDir, wtMilestoneDir, milestoneId);
302
+ forceOverwriteAssessmentsWithVerdict(prMilestoneDir, wtMilestoneDir);
303
+
301
304
  // Forward-sync completed-units.json from project root to worktree.
302
305
  // Project root is authoritative for completion state after crash recovery;
303
306
  // without this, the worktree re-dispatches already-completed units (#1886).
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=background-cli.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"background-cli.test.d.ts","sourceRoot":"","sources":["../src/background-cli.test.ts"],"names":[],"mappings":""}