gsd-pi 2.44.0-dev.0b97ffd → 2.44.0-dev.73f2fd5

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 (173) hide show
  1. package/dist/resources/extensions/gsd/auto/infra-errors.js +3 -0
  2. package/dist/resources/extensions/gsd/auto/phases.js +36 -36
  3. package/dist/resources/extensions/gsd/auto-prompts.js +24 -1
  4. package/dist/resources/extensions/gsd/auto-timers.js +57 -3
  5. package/dist/resources/extensions/gsd/auto-worktree-sync.js +4 -0
  6. package/dist/resources/extensions/gsd/auto-worktree.js +9 -6
  7. package/dist/resources/extensions/gsd/auto.js +30 -3
  8. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +156 -0
  9. package/dist/resources/extensions/gsd/bootstrap/system-context.js +46 -12
  10. package/dist/resources/extensions/gsd/commands/catalog.js +6 -1
  11. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  12. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  13. package/dist/resources/extensions/gsd/commands-mcp-status.js +187 -0
  14. package/dist/resources/extensions/gsd/db-writer.js +34 -16
  15. package/dist/resources/extensions/gsd/doctor.js +8 -0
  16. package/dist/resources/extensions/gsd/git-service.js +8 -3
  17. package/dist/resources/extensions/gsd/gsd-db.js +12 -1
  18. package/dist/resources/extensions/gsd/markdown-renderer.js +1 -1
  19. package/dist/resources/extensions/gsd/preferences.js +9 -1
  20. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  21. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  22. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  23. package/dist/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  24. package/dist/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  25. package/dist/resources/extensions/gsd/provider-error-pause.js +7 -0
  26. package/dist/resources/extensions/gsd/state.js +19 -2
  27. package/dist/resources/extensions/gsd/tools/plan-slice.js +1 -0
  28. package/dist/resources/extensions/gsd/tools/plan-task.js +1 -0
  29. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -0
  30. package/dist/resources/extensions/gsd/tools/validate-milestone.js +88 -0
  31. package/dist/resources/extensions/gsd/worktree-resolver.js +6 -0
  32. package/dist/resources/extensions/mcp-client/index.js +14 -0
  33. package/dist/web/standalone/.next/BUILD_ID +1 -1
  34. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  35. package/dist/web/standalone/.next/build-manifest.json +2 -2
  36. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  37. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  38. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.html +1 -1
  54. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  61. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  62. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  63. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  64. package/package.json +1 -1
  65. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +3 -1
  66. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  67. package/packages/pi-coding-agent/dist/core/auth-storage.js +15 -1
  68. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  69. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts +15 -0
  70. package/packages/pi-coding-agent/dist/core/local-model-check.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/local-model-check.js +41 -0
  72. package/packages/pi-coding-agent/dist/core/local-model-check.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +11 -0
  74. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/model-registry.js +20 -1
  76. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  78. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/settings-manager.js +6 -0
  80. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  82. package/packages/pi-coding-agent/dist/main.js +17 -0
  83. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  84. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js +32 -0
  87. package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/timestamp.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts +3 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +8 -1
  91. package/packages/pi-coding-agent/dist/modes/interactive/components/assistant-message.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +12 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts +15 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js +40 -0
  99. package/packages/pi-coding-agent/dist/modes/interactive/components/timestamp.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +4 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  103. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts +5 -2
  104. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js +13 -2
  106. package/packages/pi-coding-agent/dist/modes/interactive/components/user-message.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  108. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +17 -8
  109. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  110. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -3
  112. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  113. package/packages/pi-coding-agent/src/core/auth-storage.ts +15 -1
  114. package/packages/pi-coding-agent/src/core/local-model-check.ts +45 -0
  115. package/packages/pi-coding-agent/src/core/model-registry.ts +21 -1
  116. package/packages/pi-coding-agent/src/core/settings-manager.ts +9 -0
  117. package/packages/pi-coding-agent/src/main.ts +19 -0
  118. package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/timestamp.test.ts +38 -0
  119. package/packages/pi-coding-agent/src/modes/interactive/components/assistant-message.ts +10 -0
  120. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +15 -0
  121. package/packages/pi-coding-agent/src/modes/interactive/components/timestamp.ts +48 -0
  122. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +3 -1
  123. package/packages/pi-coding-agent/src/modes/interactive/components/user-message.ts +18 -3
  124. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +16 -7
  125. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +8 -1
  126. package/src/resources/extensions/gsd/auto/infra-errors.ts +3 -0
  127. package/src/resources/extensions/gsd/auto/phases.ts +45 -48
  128. package/src/resources/extensions/gsd/auto-prompts.ts +24 -1
  129. package/src/resources/extensions/gsd/auto-timers.ts +64 -3
  130. package/src/resources/extensions/gsd/auto-worktree-sync.ts +5 -0
  131. package/src/resources/extensions/gsd/auto-worktree.ts +9 -6
  132. package/src/resources/extensions/gsd/auto.ts +37 -3
  133. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +148 -0
  134. package/src/resources/extensions/gsd/bootstrap/system-context.ts +48 -11
  135. package/src/resources/extensions/gsd/commands/catalog.ts +6 -1
  136. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  137. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  138. package/src/resources/extensions/gsd/commands-mcp-status.ts +247 -0
  139. package/src/resources/extensions/gsd/db-writer.ts +39 -17
  140. package/src/resources/extensions/gsd/doctor.ts +7 -1
  141. package/src/resources/extensions/gsd/git-service.ts +6 -2
  142. package/src/resources/extensions/gsd/gsd-db.ts +16 -1
  143. package/src/resources/extensions/gsd/markdown-renderer.ts +1 -1
  144. package/src/resources/extensions/gsd/preferences.ts +11 -1
  145. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -4
  146. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  147. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +6 -6
  148. package/src/resources/extensions/gsd/prompts/replan-slice.md +3 -14
  149. package/src/resources/extensions/gsd/prompts/validate-milestone.md +7 -37
  150. package/src/resources/extensions/gsd/provider-error-pause.ts +9 -0
  151. package/src/resources/extensions/gsd/state.ts +19 -1
  152. package/src/resources/extensions/gsd/tests/auto-pr-bugs.test.ts +88 -0
  153. package/src/resources/extensions/gsd/tests/completed-units-metrics-sync.test.ts +114 -0
  154. package/src/resources/extensions/gsd/tests/db-writer.test.ts +79 -0
  155. package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +60 -0
  156. package/src/resources/extensions/gsd/tests/est-annotation-timeout.test.ts +120 -0
  157. package/src/resources/extensions/gsd/tests/infra-error.test.ts +20 -2
  158. package/src/resources/extensions/gsd/tests/knowledge.test.ts +89 -0
  159. package/src/resources/extensions/gsd/tests/mcp-status.test.ts +103 -0
  160. package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +66 -0
  161. package/src/resources/extensions/gsd/tests/preferences.test.ts +27 -0
  162. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +11 -7
  163. package/src/resources/extensions/gsd/tests/stop-auto-merge-back.test.ts +67 -0
  164. package/src/resources/extensions/gsd/tests/terminated-transient.test.ts +49 -0
  165. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +2 -1
  166. package/src/resources/extensions/gsd/tools/plan-slice.ts +2 -0
  167. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -0
  168. package/src/resources/extensions/gsd/tools/replan-slice.ts +3 -0
  169. package/src/resources/extensions/gsd/tools/validate-milestone.ts +127 -0
  170. package/src/resources/extensions/gsd/worktree-resolver.ts +7 -0
  171. package/src/resources/extensions/mcp-client/index.ts +20 -0
  172. /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_buildManifest.js +0 -0
  173. /package/dist/web/standalone/.next/static/{alS4hoANx0TK4UVZY27da → kxxAA66bah_yhPYqLBHE2}/_ssgManifest.js +0 -0
@@ -48,6 +48,7 @@ import {
48
48
  getSliceTasks,
49
49
  getReplanHistory,
50
50
  getSlice,
51
+ insertMilestone,
51
52
  type MilestoneRow,
52
53
  type SliceRow,
53
54
  type TaskRow,
@@ -257,7 +258,24 @@ function isStatusDone(status: string): boolean {
257
258
  export async function deriveStateFromDb(basePath: string): Promise<GSDState> {
258
259
  const requirements = parseRequirementCounts(await loadFile(resolveGsdRootFile(basePath, "REQUIREMENTS")));
259
260
 
260
- const allMilestones = getAllMilestones();
261
+ let allMilestones = getAllMilestones();
262
+
263
+ // Incremental disk→DB sync: milestone directories created outside the DB
264
+ // write path (via /gsd queue, manual mkdir, or complete-milestone writing the
265
+ // next CONTEXT.md) are never inserted by the initial migration guard in
266
+ // auto-start.ts because that guard only runs when gsd.db doesn't exist yet.
267
+ // Reconcile here so deriveStateFromDb never silently misses queued milestones.
268
+ // insertMilestone uses INSERT OR IGNORE, so this is safe to call every time.
269
+ const dbIdSet = new Set(allMilestones.map(m => m.id));
270
+ const diskIds = findMilestoneIds(basePath);
271
+ let synced = false;
272
+ for (const diskId of diskIds) {
273
+ if (!dbIdSet.has(diskId) && !isGhostMilestone(basePath, diskId)) {
274
+ insertMilestone({ id: diskId, status: 'active' });
275
+ synced = true;
276
+ }
277
+ }
278
+ if (synced) allMilestones = getAllMilestones();
261
279
 
262
280
  // Parallel worker isolation: when locked, filter to just the locked milestone
263
281
  const milestoneLock = process.env.GSD_MILESTONE_LOCK;
@@ -0,0 +1,88 @@
1
+ /**
2
+ * auto-pr-bugs.test.ts — Regression tests for #2302.
3
+ *
4
+ * Three interacting bugs prevented auto_pr from ever creating a PR:
5
+ * 1. auto_pr was gated on `pushed` (which requires auto_push)
6
+ * 2. Milestone branch was not pushed to remote before PR creation
7
+ * 3. createDraftPR in git-service.ts lacked --head/--base parameters
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { readFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ // ─── Bug 1: auto_pr should not depend on auto_push / pushed flag ────────────
16
+
17
+ const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
18
+ const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
19
+
20
+ test("#2302 bug 1: auto_pr condition should not require pushed flag", () => {
21
+ // Find the auto_pr block in mergeMilestoneToMain
22
+ const autoPrIdx = autoWorktreeSrc.indexOf("auto_pr");
23
+ assert.ok(autoPrIdx !== -1, "auto_pr reference exists in auto-worktree.ts");
24
+
25
+ // Get context around the auto_pr check
26
+ const lineStart = autoWorktreeSrc.lastIndexOf("\n", autoPrIdx) + 1;
27
+ const lineEnd = autoWorktreeSrc.indexOf("\n", autoPrIdx);
28
+ const autoPrLine = autoWorktreeSrc.slice(lineStart, lineEnd);
29
+
30
+ // The condition should NOT include `&& pushed`
31
+ assert.ok(
32
+ !autoPrLine.includes("&& pushed"),
33
+ "auto_pr condition should not be gated on pushed flag (auto_push dependency)",
34
+ );
35
+ });
36
+
37
+ // ─── Bug 2: phases.ts should not duplicate PR creation ──────────────────────
38
+
39
+ const phasesSrcPath = join(import.meta.dirname, "..", "auto", "phases.ts");
40
+ const phasesSrc = readFileSync(phasesSrcPath, "utf-8");
41
+
42
+ test("#2302 bug 2: phases.ts should not call createDraftPR (handled by mergeMilestoneToMain)", () => {
43
+ // After fix, phases.ts should not import or call createDraftPR because
44
+ // PR creation is handled inside mergeMilestoneToMain in auto-worktree.ts
45
+ const createDraftPRCalls = phasesSrc.match(/createDraftPR\(/g) || [];
46
+
47
+ assert.equal(
48
+ createDraftPRCalls.length,
49
+ 0,
50
+ "phases.ts should not call createDraftPR — it's handled by mergeMilestoneToMain",
51
+ );
52
+ });
53
+
54
+ // ─── Bug 3: createDraftPR should accept head and base branch parameters ─────
55
+
56
+ const gitServiceSrcPath = join(import.meta.dirname, "..", "git-service.ts");
57
+ const gitServiceSrc = readFileSync(gitServiceSrcPath, "utf-8");
58
+
59
+ test("#2302 bug 3: createDraftPR should accept head and base branch parameters", () => {
60
+ // Find the createDraftPR function signature
61
+ const fnIdx = gitServiceSrc.indexOf("function createDraftPR");
62
+ assert.ok(fnIdx !== -1, "createDraftPR function exists");
63
+
64
+ // Get the function signature (up to the closing paren)
65
+ const sigEnd = gitServiceSrc.indexOf(")", fnIdx);
66
+ const signature = gitServiceSrc.slice(fnIdx, sigEnd);
67
+
68
+ // Should have head and base parameters
69
+ assert.ok(
70
+ signature.includes("head") || signature.includes("branch"),
71
+ "createDraftPR should accept a head/branch parameter",
72
+ );
73
+ });
74
+
75
+ test("#2302 bug 3: createDraftPR should pass --head and --base to gh pr create", () => {
76
+ const fnIdx = gitServiceSrc.indexOf("function createDraftPR");
77
+ const fnEnd = gitServiceSrc.indexOf("\n}", fnIdx);
78
+ const fnBody = gitServiceSrc.slice(fnIdx, fnEnd);
79
+
80
+ assert.ok(
81
+ fnBody.includes("--head"),
82
+ "createDraftPR should pass --head to gh pr create",
83
+ );
84
+ assert.ok(
85
+ fnBody.includes("--base"),
86
+ "createDraftPR should pass --base to gh pr create",
87
+ );
88
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * completed-units-metrics-sync.test.ts — Regression tests for #2313.
3
+ *
4
+ * 1. completed-units.json should be archived (not wiped) on milestone transition
5
+ * 2. metrics.json should be in the worktree → project root sync file list
6
+ */
7
+
8
+ import test from "node:test";
9
+ import assert from "node:assert/strict";
10
+ import { readFileSync, mkdtempSync, mkdirSync, writeFileSync, existsSync, cpSync } from "node:fs";
11
+ import { join } from "node:path";
12
+ import { tmpdir } from "node:os";
13
+
14
+ // ─── Bug 1: completed-units.json should be archived, not wiped ─────────────
15
+
16
+ const phasesSrcPath = join(import.meta.dirname, "..", "auto", "phases.ts");
17
+ const phasesSrc = readFileSync(phasesSrcPath, "utf-8");
18
+
19
+ test("#2313: completed-units.json should not be blindly wiped to [] on milestone transition", () => {
20
+ // The milestone transition block should NOT write an empty array to completed-units.json
21
+ // without first archiving the existing data. Look for the archive/rename pattern.
22
+ const transitionIdx = phasesSrc.indexOf("Milestone transition");
23
+ assert.ok(transitionIdx !== -1, "Milestone transition section exists");
24
+
25
+ // Find the completed-units handling block
26
+ const completedUnitsIdx = phasesSrc.indexOf("completed-units", transitionIdx);
27
+ assert.ok(completedUnitsIdx !== -1, "completed-units handling exists in transition");
28
+
29
+ // Get a window around the completed-units handling (1200 chars to
30
+ // accommodate CRLF line endings on Windows which inflate byte offsets).
31
+ const windowStart = Math.max(0, completedUnitsIdx - 300);
32
+ const windowEnd = Math.min(phasesSrc.length, completedUnitsIdx + 900);
33
+ const window = phasesSrc.slice(windowStart, windowEnd).toLowerCase();
34
+
35
+ // Should archive/rename the old file before resetting
36
+ const hasArchive = window.includes("archive") ||
37
+ window.includes("rename") ||
38
+ window.includes("cpsync") ||
39
+ window.includes("safecopy") ||
40
+ window.includes("completed-units-");
41
+
42
+ assert.ok(
43
+ hasArchive,
44
+ "completed-units.json should be archived before reset during milestone transition",
45
+ );
46
+ });
47
+
48
+ // ─── Bug 2: metrics.json should be in the sync file lists ──────────────────
49
+
50
+ test("#2313: syncStateToProjectRoot should sync metrics.json", () => {
51
+ const syncSrcPath = join(import.meta.dirname, "..", "auto-worktree-sync.ts");
52
+ const syncSrc = readFileSync(syncSrcPath, "utf-8");
53
+
54
+ // syncStateToProjectRoot should copy metrics.json from worktree to project root
55
+ assert.ok(
56
+ syncSrc.includes("metrics.json"),
57
+ "auto-worktree-sync.ts should reference metrics.json for sync",
58
+ );
59
+ });
60
+
61
+ test("#2313: syncWorktreeStateBack should include metrics.json in root files list", () => {
62
+ const autoWorktreeSrcPath = join(import.meta.dirname, "..", "auto-worktree.ts");
63
+ const autoWorktreeSrc = readFileSync(autoWorktreeSrcPath, "utf-8");
64
+
65
+ // Find the rootFiles array in syncWorktreeStateBack
66
+ const syncBackIdx = autoWorktreeSrc.indexOf("syncWorktreeStateBack");
67
+ assert.ok(syncBackIdx !== -1, "syncWorktreeStateBack exists");
68
+
69
+ const rootFilesIdx = autoWorktreeSrc.indexOf("rootFiles", syncBackIdx);
70
+ assert.ok(rootFilesIdx !== -1, "rootFiles list exists in syncWorktreeStateBack");
71
+
72
+ // Get the rootFiles array content
73
+ const arrayStart = autoWorktreeSrc.indexOf("[", rootFilesIdx);
74
+ const arrayEnd = autoWorktreeSrc.indexOf("]", arrayStart);
75
+ const rootFilesBlock = autoWorktreeSrc.slice(arrayStart, arrayEnd);
76
+
77
+ assert.ok(
78
+ rootFilesBlock.includes("metrics.json"),
79
+ "metrics.json should be in syncWorktreeStateBack rootFiles list",
80
+ );
81
+ });
82
+
83
+ // ─── Functional test: completed-units archive ────────────────────────────────
84
+
85
+ test("#2313: functional — completed-units archive creates milestone-specific file", () => {
86
+ const tmpBase = mkdtempSync(join(tmpdir(), "gsd-completed-units-"));
87
+ const gsdDir = join(tmpBase, ".gsd");
88
+ mkdirSync(gsdDir, { recursive: true });
89
+
90
+ // Simulate existing completed-units.json with data
91
+ const existing = [
92
+ { type: "task", id: "T01" },
93
+ { type: "slice", id: "S01" },
94
+ ];
95
+ const completedKeysPath = join(gsdDir, "completed-units.json");
96
+ writeFileSync(completedKeysPath, JSON.stringify(existing, null, 2));
97
+
98
+ // Simulate the archive behavior: copy to milestone-specific file
99
+ const milestoneId = "M001";
100
+ const archivePath = join(gsdDir, `completed-units-${milestoneId}.json`);
101
+ cpSync(completedKeysPath, archivePath);
102
+
103
+ // Reset the main file
104
+ writeFileSync(completedKeysPath, JSON.stringify([], null, 2));
105
+
106
+ // Verify archive exists with original data
107
+ assert.ok(existsSync(archivePath), "archive file should exist");
108
+ const archived = JSON.parse(readFileSync(archivePath, "utf-8"));
109
+ assert.deepEqual(archived, existing, "archived data should match original");
110
+
111
+ // Verify main file is reset
112
+ const current = JSON.parse(readFileSync(completedKeysPath, "utf-8"));
113
+ assert.deepEqual(current, [], "current completed-units should be empty after transition");
114
+ });
@@ -483,6 +483,85 @@ describe('db-writer', () => {
483
483
  }
484
484
  });
485
485
 
486
+ test('saveArtifactToDb — shrinkage guard preserves larger existing file', async () => {
487
+ const tmpDir = makeTmpDir();
488
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
489
+ openDatabase(dbPath);
490
+
491
+ try {
492
+ const fullContent = '# Full Research\n\n' + 'x'.repeat(20000) + '\n';
493
+ const abbreviatedContent = '# Summary\n\nShort version.\n';
494
+
495
+ // Pre-create the file with full content (simulating a prior `write` tool call)
496
+ const relPath = 'milestones/M001/M001-RESEARCH.md';
497
+ const filePath = path.join(tmpDir, '.gsd', relPath);
498
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
499
+ fs.writeFileSync(filePath, fullContent);
500
+
501
+ // Call saveArtifactToDb with abbreviated content — should trigger shrinkage guard
502
+ await saveArtifactToDb({
503
+ path: relPath,
504
+ artifact_type: 'RESEARCH',
505
+ content: abbreviatedContent,
506
+ milestone_id: 'M001',
507
+ }, tmpDir);
508
+
509
+ // Disk file should be preserved (not overwritten)
510
+ assert.deepStrictEqual(
511
+ fs.readFileSync(filePath, 'utf-8'),
512
+ fullContent,
513
+ 'disk file preserved — shrinkage guard prevented overwrite',
514
+ );
515
+
516
+ // DB should contain the full disk content, not the abbreviated content
517
+ const adapter = _getAdapter();
518
+ const row = adapter!
519
+ .prepare('SELECT full_content FROM artifacts WHERE path = ?')
520
+ .get(relPath);
521
+ assert.deepStrictEqual(
522
+ row!['full_content'],
523
+ fullContent,
524
+ 'DB stores the richer disk content instead of abbreviated content',
525
+ );
526
+ } finally {
527
+ closeDatabase();
528
+ cleanupDir(tmpDir);
529
+ }
530
+ });
531
+
532
+ test('saveArtifactToDb — allows overwrite when new content is similar size', async () => {
533
+ const tmpDir = makeTmpDir();
534
+ const dbPath = path.join(tmpDir, '.gsd', 'gsd.db');
535
+ openDatabase(dbPath);
536
+
537
+ try {
538
+ const oldContent = '# Summary v1\n\nOriginal content here.\n';
539
+ const newContent = '# Summary v2\n\nUpdated content here with more details.\n';
540
+
541
+ const relPath = 'milestones/M001/M001-SUMMARY.md';
542
+ const filePath = path.join(tmpDir, '.gsd', relPath);
543
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
544
+ fs.writeFileSync(filePath, oldContent);
545
+
546
+ await saveArtifactToDb({
547
+ path: relPath,
548
+ artifact_type: 'SUMMARY',
549
+ content: newContent,
550
+ milestone_id: 'M001',
551
+ }, tmpDir);
552
+
553
+ // Disk file should be updated (new content is >=50% of old size)
554
+ assert.deepStrictEqual(
555
+ fs.readFileSync(filePath, 'utf-8'),
556
+ newContent,
557
+ 'disk file updated when new content is similar size',
558
+ );
559
+ } finally {
560
+ closeDatabase();
561
+ cleanupDir(tmpDir);
562
+ }
563
+ });
564
+
486
565
  // ═══════════════════════════════════════════════════════════════════════════
487
566
  // Full Round-Trip: DB → Markdown → Parse → Compare
488
567
  // ═══════════════════════════════════════════════════════════════════════════
@@ -11,6 +11,7 @@ import {
11
11
  insertArtifact,
12
12
  isDbAvailable,
13
13
  insertMilestone,
14
+ getAllMilestones,
14
15
  insertSlice,
15
16
  insertTask,
16
17
  } from '../gsd-db.ts';
@@ -962,4 +963,63 @@ describe('derive-state-db', async () => {
962
963
  cleanup(base);
963
964
  }
964
965
  });
966
+
967
+ // ─── Regression: disk-only milestones synced into DB (#2416) ─────────
968
+ test('derive-state-db: disk-only milestone auto-synced into DB (#2416)', async () => {
969
+ const base = createFixtureBase();
970
+ try {
971
+ // M001 is complete and exists in DB. M002 was queued on disk only — no DB row.
972
+ writeFile(base, 'milestones/M001/M001-SUMMARY.md', '# M001 Summary\n\nDone.');
973
+ writeFile(base, 'milestones/M002/M002-CONTEXT.md', '# M002: Queued\n\nQueued milestone.');
974
+
975
+ openDatabase(':memory:');
976
+ // Only insert M001 — simulates the state after migration guard ran then /gsd queue added M002
977
+ insertMilestone({ id: 'M001', title: 'First', status: 'complete' });
978
+
979
+ invalidateStateCache();
980
+ const state = await deriveStateFromDb(base);
981
+
982
+ // Before the fix, M002 was invisible: getAllMilestones() returned only M001
983
+ // (complete) → phase='complete' → auto-mode stopped.
984
+ // After the fix, deriveStateFromDb reconciles disk dirs and inserts M002.
985
+ assert.deepStrictEqual(state.phase, 'pre-planning', 'disk-sync-2416: phase is pre-planning, not complete');
986
+ assert.deepStrictEqual(state.registry.length, 2, 'disk-sync-2416: both milestones visible in registry');
987
+ assert.deepStrictEqual(state.registry[0]?.id, 'M001', 'disk-sync-2416: registry[0] is M001');
988
+ assert.deepStrictEqual(state.registry[0]?.status, 'complete', 'disk-sync-2416: M001 is complete');
989
+ assert.deepStrictEqual(state.registry[1]?.id, 'M002', 'disk-sync-2416: registry[1] is M002');
990
+ assert.deepStrictEqual(state.registry[1]?.status, 'active', 'disk-sync-2416: M002 is active');
991
+ assert.deepStrictEqual(state.activeMilestone?.id, 'M002', 'disk-sync-2416: activeMilestone is M002');
992
+
993
+ closeDatabase();
994
+ } finally {
995
+ closeDatabase();
996
+ cleanup(base);
997
+ }
998
+ });
999
+
1000
+ // ─── Queued milestone row not clobbered by later plan (#2416 root cause) ──
1001
+ test('derive-state-db: queued milestone row survives gsd_plan_milestone INSERT OR IGNORE', async () => {
1002
+ try {
1003
+ openDatabase(':memory:');
1004
+
1005
+ // Simulates gsd_milestone_generate_id inserting a minimal queued row
1006
+ insertMilestone({ id: 'M001', status: 'queued' });
1007
+
1008
+ const before = getAllMilestones();
1009
+ assert.equal(before.length, 1, 'queued-row: one row after generate_id');
1010
+ assert.equal(before[0]!.status, 'queued', 'queued-row: status is queued');
1011
+
1012
+ // Simulates gsd_plan_milestone calling insertMilestone (INSERT OR IGNORE)
1013
+ insertMilestone({ id: 'M001', title: 'Planned Title', status: 'active' });
1014
+
1015
+ const after = getAllMilestones();
1016
+ assert.equal(after.length, 1, 'queued-row: still one row after plan');
1017
+ // INSERT OR IGNORE keeps the original row — status stays 'queued'
1018
+ assert.equal(after[0]!.status, 'queued', 'queued-row: INSERT OR IGNORE preserves original status');
1019
+
1020
+ closeDatabase();
1021
+ } finally {
1022
+ closeDatabase();
1023
+ }
1024
+ });
965
1025
  });
@@ -0,0 +1,120 @@
1
+ /**
2
+ * est-annotation-timeout.test.ts — Regression tests for #2243.
3
+ *
4
+ * Tasks with `est: 30m` or `est: 2h` annotations should get extended
5
+ * supervision timeouts. The parseEstimateMinutes helper should parse
6
+ * estimate strings, and startUnitSupervision should use them.
7
+ */
8
+
9
+ import test from "node:test";
10
+ import assert from "node:assert/strict";
11
+ import { readFileSync } from "node:fs";
12
+ import { join } from "node:path";
13
+
14
+ const timersSrcPath = join(import.meta.dirname, "..", "auto-timers.ts");
15
+ const timersSrc = readFileSync(timersSrcPath, "utf-8");
16
+
17
+ // ─── Source analysis: parseEstimateMinutes exists and is exported ────────────
18
+
19
+ test("#2243: auto-timers.ts should export parseEstimateMinutes", () => {
20
+ assert.ok(
21
+ timersSrc.includes("export function parseEstimateMinutes"),
22
+ "parseEstimateMinutes should be exported from auto-timers.ts",
23
+ );
24
+ });
25
+
26
+ // ─── Inline unit test of parseEstimateMinutes logic ─────────────────────────
27
+ // Since importing the module pulls in heavy deps, test the parsing logic inline.
28
+
29
+ function parseEstimateMinutes(estimate: string): number | null {
30
+ if (!estimate || typeof estimate !== "string") return null;
31
+ const trimmed = estimate.trim();
32
+ if (!trimmed) return null;
33
+
34
+ let totalMinutes = 0;
35
+ let matched = false;
36
+
37
+ const hoursMatch = trimmed.match(/(\d+)\s*h/i);
38
+ if (hoursMatch) {
39
+ totalMinutes += Number(hoursMatch[1]) * 60;
40
+ matched = true;
41
+ }
42
+
43
+ const minutesMatch = trimmed.match(/(\d+)\s*m/i);
44
+ if (minutesMatch) {
45
+ totalMinutes += Number(minutesMatch[1]);
46
+ matched = true;
47
+ }
48
+
49
+ return matched ? totalMinutes : null;
50
+ }
51
+
52
+ test("#2243: parseEstimateMinutes parses '30m' correctly", () => {
53
+ assert.equal(parseEstimateMinutes("30m"), 30);
54
+ });
55
+
56
+ test("#2243: parseEstimateMinutes parses '2h' correctly", () => {
57
+ assert.equal(parseEstimateMinutes("2h"), 120);
58
+ });
59
+
60
+ test("#2243: parseEstimateMinutes parses '1h30m' correctly", () => {
61
+ assert.equal(parseEstimateMinutes("1h30m"), 90);
62
+ });
63
+
64
+ test("#2243: parseEstimateMinutes parses '15m' correctly", () => {
65
+ assert.equal(parseEstimateMinutes("15m"), 15);
66
+ });
67
+
68
+ test("#2243: parseEstimateMinutes returns null for empty string", () => {
69
+ assert.equal(parseEstimateMinutes(""), null);
70
+ });
71
+
72
+ test("#2243: parseEstimateMinutes returns null for invalid string", () => {
73
+ assert.equal(parseEstimateMinutes("not a time"), null);
74
+ });
75
+
76
+ // ─── Source analysis: startUnitSupervision uses task estimates ───────────────
77
+
78
+ test("#2243: startUnitSupervision should reference task estimates for timeout scaling", () => {
79
+ const usesEstimate =
80
+ timersSrc.includes("parseEstimateMinutes") &&
81
+ timersSrc.includes("estimateMinutes") &&
82
+ timersSrc.includes("taskEstimate");
83
+
84
+ assert.ok(
85
+ usesEstimate,
86
+ "startUnitSupervision should use task estimate annotations for timeout scaling",
87
+ );
88
+ });
89
+
90
+ test("#2243: SupervisionContext should accept an optional taskEstimate field", () => {
91
+ const ctxIdx = timersSrc.indexOf("SupervisionContext");
92
+ assert.ok(ctxIdx !== -1, "SupervisionContext interface exists");
93
+
94
+ const ctxEnd = timersSrc.indexOf("}", ctxIdx);
95
+ const ctxBlock = timersSrc.slice(ctxIdx, ctxEnd);
96
+
97
+ assert.ok(
98
+ ctxBlock.includes("taskEstimate"),
99
+ "SupervisionContext should include a taskEstimate field",
100
+ );
101
+ });
102
+
103
+ test("#2243: timeouts should be scaled by estimate (timeoutScale in source)", () => {
104
+ assert.ok(
105
+ timersSrc.includes("timeoutScale"),
106
+ "auto-timers.ts should use a timeoutScale factor derived from est: annotations",
107
+ );
108
+ });
109
+
110
+ test("#2243: idle timeout should NOT be scaled (idle is idle regardless of estimate)", () => {
111
+ // Find the idleTimeoutMs line
112
+ const idleIdx = timersSrc.indexOf("const idleTimeoutMs");
113
+ assert.ok(idleIdx !== -1, "idleTimeoutMs variable exists");
114
+
115
+ const idleLine = timersSrc.slice(idleIdx, timersSrc.indexOf("\n", idleIdx));
116
+ assert.ok(
117
+ !idleLine.includes("timeoutScale"),
118
+ "idleTimeoutMs should NOT be scaled — idle is idle",
119
+ );
120
+ });
@@ -7,10 +7,13 @@ import { isInfrastructureError, INFRA_ERROR_CODES } from "../auto/infra-errors.j
7
7
  // ── INFRA_ERROR_CODES constant ───────────────────────────────────────────────
8
8
 
9
9
  test("INFRA_ERROR_CODES contains the expected codes", () => {
10
- for (const code of ["ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE"]) {
10
+ for (const code of [
11
+ "ENOSPC", "ENOMEM", "EROFS", "EDQUOT", "EMFILE", "ENFILE",
12
+ "ECONNREFUSED", "ENOTFOUND", "ENETUNREACH",
13
+ ]) {
11
14
  assert.ok(INFRA_ERROR_CODES.has(code), `missing ${code}`);
12
15
  }
13
- assert.equal(INFRA_ERROR_CODES.size, 6, "unexpected extra codes");
16
+ assert.equal(INFRA_ERROR_CODES.size, 9, "unexpected extra codes");
14
17
  });
15
18
 
16
19
  // ── isInfrastructureError: code property detection ───────────────────────────
@@ -45,6 +48,21 @@ test("detects ENFILE via code property", () => {
45
48
  assert.equal(isInfrastructureError(err), "ENFILE");
46
49
  });
47
50
 
51
+ test("detects ECONNREFUSED via code property", () => {
52
+ const err = Object.assign(new Error("connect ECONNREFUSED 127.0.0.1:3000"), { code: "ECONNREFUSED" });
53
+ assert.equal(isInfrastructureError(err), "ECONNREFUSED");
54
+ });
55
+
56
+ test("detects ENOTFOUND via code property", () => {
57
+ const err = Object.assign(new Error("getaddrinfo ENOTFOUND api.example.com"), { code: "ENOTFOUND" });
58
+ assert.equal(isInfrastructureError(err), "ENOTFOUND");
59
+ });
60
+
61
+ test("detects ENETUNREACH via code property", () => {
62
+ const err = Object.assign(new Error("connect ENETUNREACH 2607:f8b0:4004::"), { code: "ENETUNREACH" });
63
+ assert.equal(isInfrastructureError(err), "ENETUNREACH");
64
+ });
65
+
48
66
  // ── isInfrastructureError: message fallback ──────────────────────────────────
49
67
 
50
68
  test("falls back to message scanning when no code property", () => {
@@ -6,6 +6,7 @@
6
6
  * - resolveGsdRootFile resolves KNOWLEDGE paths correctly
7
7
  * - inlineGsdRootFile works with the KNOWLEDGE key
8
8
  * - before_agent_start hook includes/omits knowledge block appropriately
9
+ * - loadKnowledgeBlock merges global and project knowledge correctly
9
10
  */
10
11
 
11
12
  import test from 'node:test';
@@ -16,6 +17,7 @@ import { tmpdir } from 'node:os';
16
17
  import { GSD_ROOT_FILES, resolveGsdRootFile } from '../paths.ts';
17
18
  import { inlineGsdRootFile } from '../auto-prompts.ts';
18
19
  import { appendKnowledge } from '../files.ts';
20
+ import { loadKnowledgeBlock } from '../bootstrap/system-context.ts';
19
21
 
20
22
  // ─── KNOWLEDGE is registered in GSD_ROOT_FILES ─────────────────────────────
21
23
 
@@ -159,3 +161,90 @@ test('knowledge: appendKnowledge handles lesson type', async () => {
159
161
 
160
162
  rmSync(tmp, { recursive: true, force: true });
161
163
  });
164
+
165
+ // ─── loadKnowledgeBlock — global + project merge ────────────────────────────
166
+
167
+ test('loadKnowledgeBlock: returns empty block when neither file exists', () => {
168
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
169
+ const gsdHome = join(tmp, 'home');
170
+ const cwd = join(tmp, 'project');
171
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
172
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
173
+
174
+ const result = loadKnowledgeBlock(gsdHome, cwd);
175
+ assert.strictEqual(result.block, '');
176
+ assert.strictEqual(result.globalSizeKb, 0);
177
+
178
+ rmSync(tmp, { recursive: true, force: true });
179
+ });
180
+
181
+ test('loadKnowledgeBlock: uses project knowledge alone when no global file', () => {
182
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
183
+ const gsdHome = join(tmp, 'home');
184
+ const cwd = join(tmp, 'project');
185
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
186
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
187
+ writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Use real DB');
188
+
189
+ const result = loadKnowledgeBlock(gsdHome, cwd);
190
+ assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
191
+ assert.ok(result.block.includes('## Project Knowledge'));
192
+ assert.ok(result.block.includes('K001: Use real DB'));
193
+ assert.ok(!result.block.includes('## Global Knowledge'));
194
+ assert.strictEqual(result.globalSizeKb, 0);
195
+
196
+ rmSync(tmp, { recursive: true, force: true });
197
+ });
198
+
199
+ test('loadKnowledgeBlock: uses global knowledge alone when no project file', () => {
200
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
201
+ const gsdHome = join(tmp, 'home');
202
+ const cwd = join(tmp, 'project');
203
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
204
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
205
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Respond in English');
206
+
207
+ const result = loadKnowledgeBlock(gsdHome, cwd);
208
+ assert.ok(result.block.includes('[KNOWLEDGE — Rules, patterns, and lessons learned]'));
209
+ assert.ok(result.block.includes('## Global Knowledge'));
210
+ assert.ok(result.block.includes('G001: Respond in English'));
211
+ assert.ok(!result.block.includes('## Project Knowledge'));
212
+ assert.ok(result.globalSizeKb > 0);
213
+
214
+ rmSync(tmp, { recursive: true, force: true });
215
+ });
216
+
217
+ test('loadKnowledgeBlock: merges global before project when both exist', () => {
218
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
219
+ const gsdHome = join(tmp, 'home');
220
+ const cwd = join(tmp, 'project');
221
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
222
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
223
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'G001: Global rule');
224
+ writeFileSync(join(cwd, '.gsd', 'KNOWLEDGE.md'), 'K001: Project rule');
225
+
226
+ const result = loadKnowledgeBlock(gsdHome, cwd);
227
+ assert.ok(result.block.includes('## Global Knowledge'));
228
+ assert.ok(result.block.includes('## Project Knowledge'));
229
+ assert.ok(result.block.includes('G001: Global rule'));
230
+ assert.ok(result.block.includes('K001: Project rule'));
231
+ // Global section appears before project section
232
+ assert.ok(result.block.indexOf('## Global Knowledge') < result.block.indexOf('## Project Knowledge'));
233
+
234
+ rmSync(tmp, { recursive: true, force: true });
235
+ });
236
+
237
+ test('loadKnowledgeBlock: reports globalSizeKb above 4KB threshold', () => {
238
+ const tmp = realpathSync(mkdtempSync(join(tmpdir(), 'gsd-kb-')));
239
+ const gsdHome = join(tmp, 'home');
240
+ const cwd = join(tmp, 'project');
241
+ mkdirSync(join(cwd, '.gsd'), { recursive: true });
242
+ mkdirSync(join(gsdHome, 'agent'), { recursive: true });
243
+ // Write > 4KB of content
244
+ writeFileSync(join(gsdHome, 'agent', 'KNOWLEDGE.md'), 'x'.repeat(5000));
245
+
246
+ const result = loadKnowledgeBlock(gsdHome, cwd);
247
+ assert.ok(result.globalSizeKb > 4, `expected > 4KB, got ${result.globalSizeKb}`);
248
+
249
+ rmSync(tmp, { recursive: true, force: true });
250
+ });