gsd-pi 2.74.0-dev.2b524c3 → 2.74.0-dev.b741afb

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 (159) hide show
  1. package/dist/cli.js +85 -0
  2. package/dist/headless-query.js +4 -1
  3. package/dist/help-text.js +23 -0
  4. package/dist/resources/extensions/gsd/auto/detect-stuck.js +11 -4
  5. package/dist/resources/extensions/gsd/auto/phases.js +45 -1
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +52 -56
  7. package/dist/resources/extensions/gsd/auto-prompts.js +12 -0
  8. package/dist/resources/extensions/gsd/auto.js +8 -2
  9. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +21 -8
  10. package/dist/resources/extensions/gsd/commands/catalog.js +26 -1
  11. package/dist/resources/extensions/gsd/commands/handlers/ops.js +20 -0
  12. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +68 -9
  13. package/dist/resources/extensions/gsd/commands-add-tests.js +111 -0
  14. package/dist/resources/extensions/gsd/commands-backlog.js +140 -0
  15. package/dist/resources/extensions/gsd/commands-do.js +79 -0
  16. package/dist/resources/extensions/gsd/commands-maintenance.js +6 -6
  17. package/dist/resources/extensions/gsd/commands-pr-branch.js +180 -0
  18. package/dist/resources/extensions/gsd/commands-session-report.js +82 -0
  19. package/dist/resources/extensions/gsd/commands-ship.js +187 -0
  20. package/dist/resources/extensions/gsd/db-writer.js +3 -5
  21. package/dist/resources/extensions/gsd/graph-context.js +66 -0
  22. package/dist/resources/extensions/gsd/gsd-db.js +321 -0
  23. package/dist/resources/extensions/gsd/index.js +15 -2
  24. package/dist/resources/extensions/gsd/md-importer.js +3 -4
  25. package/dist/resources/extensions/gsd/memory-store.js +19 -51
  26. package/dist/resources/extensions/gsd/milestone-validation-gates.js +13 -12
  27. package/dist/resources/extensions/gsd/native-git-bridge.js +7 -4
  28. package/dist/resources/extensions/gsd/prompts/add-tests.md +35 -0
  29. package/dist/resources/extensions/gsd/state.js +5 -1
  30. package/dist/resources/extensions/gsd/tools/complete-slice.js +15 -0
  31. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +3 -14
  32. package/dist/resources/extensions/gsd/triage-resolution.js +2 -5
  33. package/dist/resources/extensions/gsd/workflow-manifest.js +8 -69
  34. package/dist/resources/extensions/gsd/workflow-migration.js +21 -22
  35. package/dist/resources/extensions/gsd/workflow-projections.js +4 -1
  36. package/dist/resources/extensions/gsd/workflow-reconcile.js +14 -11
  37. package/dist/tsconfig.extensions.tsbuildinfo +1 -0
  38. package/dist/web/standalone/.next/BUILD_ID +1 -1
  39. package/dist/web/standalone/.next/app-path-routes-manifest.json +7 -7
  40. package/dist/web/standalone/.next/build-manifest.json +2 -2
  41. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  42. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  43. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/index.html +1 -1
  59. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app-paths-manifest.json +7 -7
  66. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  67. package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
  68. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  69. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  70. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  71. package/package.json +3 -2
  72. package/packages/daemon/package.json +2 -2
  73. package/packages/mcp-server/dist/index.d.ts +3 -0
  74. package/packages/mcp-server/dist/index.d.ts.map +1 -1
  75. package/packages/mcp-server/dist/index.js +3 -0
  76. package/packages/mcp-server/dist/index.js.map +1 -1
  77. package/packages/mcp-server/dist/readers/graph.d.ts +87 -0
  78. package/packages/mcp-server/dist/readers/graph.d.ts.map +1 -0
  79. package/packages/mcp-server/dist/readers/graph.js +548 -0
  80. package/packages/mcp-server/dist/readers/graph.js.map +1 -0
  81. package/packages/mcp-server/dist/readers/index.d.ts +2 -0
  82. package/packages/mcp-server/dist/readers/index.d.ts.map +1 -1
  83. package/packages/mcp-server/dist/readers/index.js +1 -0
  84. package/packages/mcp-server/dist/readers/index.js.map +1 -1
  85. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  86. package/packages/mcp-server/dist/server.js +65 -0
  87. package/packages/mcp-server/dist/server.js.map +1 -1
  88. package/packages/mcp-server/package.json +2 -2
  89. package/packages/mcp-server/src/index.ts +15 -0
  90. package/packages/mcp-server/src/readers/graph.test.ts +426 -0
  91. package/packages/mcp-server/src/readers/graph.ts +708 -0
  92. package/packages/mcp-server/src/readers/index.ts +12 -0
  93. package/packages/mcp-server/src/server.ts +83 -0
  94. package/packages/mcp-server/tsconfig.json +1 -0
  95. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -0
  96. package/packages/native/package.json +2 -2
  97. package/packages/native/tsconfig.tsbuildinfo +1 -0
  98. package/packages/pi-agent-core/package.json +1 -1
  99. package/packages/pi-agent-core/tsconfig.json +1 -0
  100. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -0
  101. package/packages/pi-ai/package.json +1 -1
  102. package/packages/pi-ai/tsconfig.json +1 -0
  103. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -0
  104. package/packages/pi-coding-agent/tsconfig.json +1 -0
  105. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -0
  106. package/packages/pi-tui/package.json +1 -1
  107. package/packages/pi-tui/tsconfig.json +1 -0
  108. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -0
  109. package/packages/rpc-client/package.json +1 -1
  110. package/packages/rpc-client/tsconfig.json +1 -0
  111. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -0
  112. package/src/resources/extensions/gsd/auto/detect-stuck.ts +12 -4
  113. package/src/resources/extensions/gsd/auto/loop-deps.ts +6 -0
  114. package/src/resources/extensions/gsd/auto/phases.ts +68 -1
  115. package/src/resources/extensions/gsd/auto-post-unit.ts +60 -57
  116. package/src/resources/extensions/gsd/auto-prompts.ts +13 -0
  117. package/src/resources/extensions/gsd/auto.ts +7 -0
  118. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +24 -8
  119. package/src/resources/extensions/gsd/commands/catalog.ts +26 -1
  120. package/src/resources/extensions/gsd/commands/handlers/ops.ts +20 -0
  121. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +74 -9
  122. package/src/resources/extensions/gsd/commands-add-tests.ts +137 -0
  123. package/src/resources/extensions/gsd/commands-backlog.ts +182 -0
  124. package/src/resources/extensions/gsd/commands-do.ts +109 -0
  125. package/src/resources/extensions/gsd/commands-maintenance.ts +6 -6
  126. package/src/resources/extensions/gsd/commands-pr-branch.ts +234 -0
  127. package/src/resources/extensions/gsd/commands-session-report.ts +101 -0
  128. package/src/resources/extensions/gsd/commands-ship.ts +219 -0
  129. package/src/resources/extensions/gsd/db-writer.ts +3 -5
  130. package/src/resources/extensions/gsd/graph-context.ts +85 -0
  131. package/src/resources/extensions/gsd/gsd-db.ts +467 -0
  132. package/src/resources/extensions/gsd/index.ts +18 -2
  133. package/src/resources/extensions/gsd/md-importer.ts +3 -5
  134. package/src/resources/extensions/gsd/memory-store.ts +31 -62
  135. package/src/resources/extensions/gsd/milestone-validation-gates.ts +13 -14
  136. package/src/resources/extensions/gsd/native-git-bridge.ts +11 -12
  137. package/src/resources/extensions/gsd/prompts/add-tests.md +35 -0
  138. package/src/resources/extensions/gsd/state.ts +9 -2
  139. package/src/resources/extensions/gsd/tests/commands-backlog.test.ts +158 -0
  140. package/src/resources/extensions/gsd/tests/commands-do.test.ts +127 -0
  141. package/src/resources/extensions/gsd/tests/commands-pr-branch.test.ts +68 -0
  142. package/src/resources/extensions/gsd/tests/commands-session-report.test.ts +82 -0
  143. package/src/resources/extensions/gsd/tests/commands-ship.test.ts +71 -0
  144. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +14 -0
  145. package/src/resources/extensions/gsd/tests/extension-bootstrap-isolation.test.ts +154 -0
  146. package/src/resources/extensions/gsd/tests/graph-context.test.ts +337 -0
  147. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +68 -1
  148. package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +140 -0
  149. package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +180 -0
  150. package/src/resources/extensions/gsd/tests/workflow-logger-wiring.test.ts +223 -0
  151. package/src/resources/extensions/gsd/tools/complete-slice.ts +19 -0
  152. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +3 -11
  153. package/src/resources/extensions/gsd/triage-resolution.ts +2 -7
  154. package/src/resources/extensions/gsd/workflow-manifest.ts +9 -104
  155. package/src/resources/extensions/gsd/workflow-migration.ts +21 -29
  156. package/src/resources/extensions/gsd/workflow-projections.ts +8 -1
  157. package/src/resources/extensions/gsd/workflow-reconcile.ts +15 -15
  158. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_buildManifest.js +0 -0
  159. /package/dist/web/standalone/.next/static/{YzIEI9sxJy4t5xgClF08g → XnHY5eXUsTCFmNodWHetD}/_ssgManifest.js +0 -0
@@ -92,6 +92,7 @@ function makeMockDeps(
92
92
  getPriorSliceCompletionBlocker: () => null,
93
93
  getMainBranch: () => "main",
94
94
  closeoutUnit: async () => {},
95
+ autoCommitUnit: async () => null,
95
96
  recordOutcome: () => {},
96
97
  writeLock: () => {},
97
98
  captureAvailableSkills: () => {},
@@ -567,7 +568,15 @@ test("unit-end event contains errorContext when unit is cancelled with structure
567
568
  const { resolveAgentEndCancelled, _resetPendingResolve } = await import("../auto-loop.js");
568
569
  _resetPendingResolve();
569
570
 
570
- const deps = makeMockDeps(capture);
571
+ let pauseCalls = 0;
572
+ let commitCalls = 0;
573
+ const deps = makeMockDeps(capture, {
574
+ pauseAuto: async () => { pauseCalls++; },
575
+ autoCommitUnit: async () => {
576
+ commitCalls++;
577
+ return "commit";
578
+ },
579
+ });
571
580
  const ic = makeIC(deps);
572
581
  const iterData: IterationData = {
573
582
  unitType: "execute-task",
@@ -593,10 +602,68 @@ test("unit-end event contains errorContext when unit is cancelled with structure
593
602
  // Transient timeout cancellations pause (recoverable) instead of hard-stopping
594
603
  assert.equal(result.action, "break");
595
604
  assert.equal((result as any).reason, "session-timeout");
605
+ assert.equal(pauseCalls, 1, "timeout cancellations should pause auto-mode exactly once");
606
+ assert.equal(commitCalls, 1, "timeout cancellations should flush a unit auto-commit once");
596
607
 
597
608
  // Verify error classification used structured errorContext on the window entry
598
609
  const entry = loopState.recentUnits[loopState.recentUnits.length - 1];
599
610
  assert.ok(entry.error, "window entry must have error set");
600
611
  assert.ok(entry.error!.startsWith("timeout:"), "error must start with category from errorContext");
601
612
  assert.ok(entry.error!.includes("Hard timeout error"), "error must include the errorContext message");
613
+
614
+ const endEvents = capture.events.filter(e => e.eventType === "unit-end");
615
+ assert.equal(endEvents.length, 1, "timeout cancellations should still emit unit-end");
616
+ assert.equal((endEvents[0].data as any).status, "cancelled");
617
+ assert.equal((endEvents[0].data as any).artifactVerified, false);
618
+ assert.equal((endEvents[0].data as any).errorContext.category, "timeout");
619
+ });
620
+
621
+ test("session-failed cancellations close out and emit unit-end before hard stop", async () => {
622
+ const capture = createEventCapture();
623
+ const { resolveAgentEndCancelled, _resetPendingResolve } = await import("../auto-loop.js");
624
+ _resetPendingResolve();
625
+
626
+ let closeoutCalls = 0;
627
+ let commitCalls = 0;
628
+ let stopCalls = 0;
629
+ const deps = makeMockDeps(capture, {
630
+ closeoutUnit: async () => { closeoutCalls++; },
631
+ autoCommitUnit: async () => {
632
+ commitCalls++;
633
+ return "commit";
634
+ },
635
+ stopAuto: async () => { stopCalls++; },
636
+ });
637
+ const ic = makeIC(deps);
638
+ const iterData: IterationData = {
639
+ unitType: "execute-task",
640
+ unitId: "M001/S01/T01",
641
+ prompt: "do stuff",
642
+ finalPrompt: "do stuff",
643
+ pauseAfterUatDispatch: false,
644
+ state: { phase: "executing", activeMilestone: { id: "M001" }, activeSlice: { id: "S01" }, registry: [], blockers: [] } as any,
645
+ mid: "M001",
646
+ midTitle: "Test",
647
+ isRetry: false,
648
+ previousTier: undefined,
649
+ };
650
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
651
+
652
+ const unitPromise = runUnitPhase(ic, iterData, loopState);
653
+ await new Promise(r => setTimeout(r, 50));
654
+
655
+ resolveAgentEndCancelled({ message: "session bootstrap exploded", category: "session-failed", isTransient: false });
656
+
657
+ const result = await unitPromise;
658
+ assert.equal(result.action, "break");
659
+ assert.equal((result as any).reason, "session-failed");
660
+ assert.equal(closeoutCalls, 1, "session-failed cancellations should close out the unit before stopping");
661
+ assert.equal(commitCalls, 1, "session-failed cancellations should try one auto-commit flush");
662
+ assert.equal(stopCalls, 1, "session-failed cancellations should hard-stop auto-mode");
663
+
664
+ const endEvents = capture.events.filter(e => e.eventType === "unit-end");
665
+ assert.equal(endEvents.length, 1, "session-failed cancellations should emit unit-end");
666
+ assert.equal((endEvents[0].data as any).status, "cancelled");
667
+ assert.equal((endEvents[0].data as any).artifactVerified, false);
668
+ assert.equal((endEvents[0].data as any).errorContext.category, "session-failed");
602
669
  });
@@ -0,0 +1,140 @@
1
+ // native-git-bridge-exec-fallback.test.ts — regression for #4180
2
+ //
3
+ // nativeCommit, nativeIsRepo, and nativeResetHard used execSync() (string
4
+ // command) in their fallback paths. On Windows, execSync spawns cmd.exe which
5
+ // cannot resolve git when Git for Windows is installed via MSYS2/bash but not
6
+ // in cmd.exe's PATH. All other fallback paths in this file use execFileSync()
7
+ // which invokes the binary directly — these three must do the same.
8
+ //
9
+ // Static-analysis tests fail before the fix (source still has execSync calls)
10
+ // and pass after (replaced with execFileSync). Integration tests verify the
11
+ // fallback functions behave correctly on all platforms.
12
+
13
+ import { describe, test, beforeEach, afterEach } from "node:test";
14
+ import assert from "node:assert/strict";
15
+ import { mkdtempSync, writeFileSync, readFileSync, rmSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ import { tmpdir } from "node:os";
18
+ import { execFileSync } from "node:child_process";
19
+ import { nativeIsRepo, nativeCommit, nativeResetHard } from "../native-git-bridge.js";
20
+
21
+ // ─── Static analysis ──────────────────────────────────────────────────────
22
+ // Verify the fallback paths of the three affected functions do not call the
23
+ // raw execSync() string-command variant. Replacing all execFileSync( tokens
24
+ // first ensures we match only the bare execSync( form.
25
+
26
+ const SRC_PATH = join(import.meta.dirname, "..", "native-git-bridge.ts");
27
+
28
+ function extractFunctionBody(src: string, fnName: string): string {
29
+ const idx = src.indexOf(`export function ${fnName}`);
30
+ if (idx === -1) throw new Error(`${fnName} not found in source`);
31
+ return src.slice(idx, idx + 1500);
32
+ }
33
+
34
+ function hasRawExecSync(body: string): boolean {
35
+ const withoutFileSync = body.replace(/execFileSync\(/g, "__FILESYNC__");
36
+ return withoutFileSync.includes("execSync(");
37
+ }
38
+
39
+ describe("native-git-bridge #4180: fallback paths use execFileSync not execSync", () => {
40
+ const src = readFileSync(SRC_PATH, "utf-8");
41
+
42
+ test("nativeIsRepo fallback does not use raw execSync", () => {
43
+ const body = extractFunctionBody(src, "nativeIsRepo");
44
+ assert.equal(
45
+ hasRawExecSync(body),
46
+ false,
47
+ "nativeIsRepo fallback must use execFileSync to avoid cmd.exe PATH failures on Windows",
48
+ );
49
+ });
50
+
51
+ test("nativeCommit fallback does not use raw execSync", () => {
52
+ const body = extractFunctionBody(src, "nativeCommit");
53
+ assert.equal(
54
+ hasRawExecSync(body),
55
+ false,
56
+ "nativeCommit fallback must use execFileSync to avoid cmd.exe PATH failures on Windows",
57
+ );
58
+ });
59
+
60
+ test("nativeResetHard fallback does not use raw execSync", () => {
61
+ const body = extractFunctionBody(src, "nativeResetHard");
62
+ assert.equal(
63
+ hasRawExecSync(body),
64
+ false,
65
+ "nativeResetHard fallback must use execFileSync to avoid cmd.exe PATH failures on Windows",
66
+ );
67
+ });
68
+ });
69
+
70
+ // ─── Integration tests ────────────────────────────────────────────────────
71
+ // Verify correct runtime behaviour through the fallback path (native module
72
+ // is disabled by default in tests — GSD_ENABLE_NATIVE_GSD_GIT is not set).
73
+
74
+ function git(args: string[], cwd: string): string {
75
+ return execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
76
+ }
77
+
78
+ describe("native-git-bridge #4180: fallback runtime behaviour", () => {
79
+ let repo: string;
80
+
81
+ beforeEach(() => {
82
+ repo = mkdtempSync(join(tmpdir(), "ngb4180-"));
83
+ git(["init"], repo);
84
+ git(["config", "user.email", "test@test.com"], repo);
85
+ git(["config", "user.name", "Test"], repo);
86
+ writeFileSync(join(repo, "file.txt"), "initial\n");
87
+ git(["add", "."], repo);
88
+ git(["commit", "-m", "init"], repo);
89
+ });
90
+
91
+ afterEach(() => {
92
+ rmSync(repo, { recursive: true, force: true });
93
+ });
94
+
95
+ test("nativeIsRepo returns true for a valid git repository", () => {
96
+ assert.equal(nativeIsRepo(repo), true);
97
+ });
98
+
99
+ test("nativeIsRepo returns false for a plain directory", (t) => {
100
+ const dir = mkdtempSync(join(tmpdir(), "ngb4180-notrepo-"));
101
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
102
+ assert.equal(nativeIsRepo(dir), false);
103
+ });
104
+
105
+ test("nativeCommit commits staged changes and returns non-null output", () => {
106
+ writeFileSync(join(repo, "file.txt"), "modified\n");
107
+ git(["add", "."], repo);
108
+
109
+ const result = nativeCommit(repo, "test: regression commit #4180");
110
+ assert.ok(result !== null, "should return output string for a successful commit");
111
+
112
+ const subject = git(["log", "-1", "--format=%s"], repo);
113
+ assert.equal(subject, "test: regression commit #4180");
114
+ });
115
+
116
+ test("nativeCommit returns null when nothing is staged", () => {
117
+ const result = nativeCommit(repo, "test: nothing staged");
118
+ assert.equal(result, null);
119
+ });
120
+
121
+ test("nativeCommit respects the allowEmpty option", () => {
122
+ const result = nativeCommit(repo, "test: empty commit #4180", { allowEmpty: true });
123
+ assert.ok(result !== null, "allow-empty commit should return output");
124
+
125
+ const subject = git(["log", "-1", "--format=%s"], repo);
126
+ assert.equal(subject, "test: empty commit #4180");
127
+ });
128
+
129
+ test("nativeResetHard discards unstaged working tree changes", () => {
130
+ writeFileSync(join(repo, "file.txt"), "dirty content\n");
131
+
132
+ const statusBefore = git(["status", "--short"], repo);
133
+ assert.ok(statusBefore.length > 0, "repo should be dirty before reset");
134
+
135
+ nativeResetHard(repo);
136
+
137
+ const content = readFileSync(join(repo, "file.txt"), "utf-8");
138
+ assert.equal(content, "initial\n", "file should be restored to HEAD content after hard reset");
139
+ });
140
+ });
@@ -0,0 +1,180 @@
1
+ // Structural invariant: gsd-db.ts is the single writer for .gsd/gsd.db.
2
+ //
3
+ // No file under src/resources/extensions/gsd/ may issue raw write SQL
4
+ // (INSERT/UPDATE/DELETE/REPLACE) or raw transaction control (BEGIN/COMMIT/
5
+ // ROLLBACK via `.exec(...)`) against the engine database. Every bypass must
6
+ // route through a typed wrapper exported from gsd-db.ts.
7
+ //
8
+ // Allowlist:
9
+ // - gsd-db.ts itself — the single writer
10
+ // - unit-ownership.ts — manages a separate .gsd/unit-claims.db for
11
+ // cross-worktree claim races; intentionally outside this invariant
12
+ // - tests/** — fixtures and direct DB inspection are fair game
13
+ //
14
+ // When this test fails, do not add a new suppression. Instead:
15
+ // 1. Add a typed wrapper to gsd-db.ts that captures the SQL
16
+ // 2. Switch the flagged site to call the wrapper
17
+ //
18
+ // See `.claude/plans/joyful-doodling-pony.md` for the full rationale.
19
+
20
+ import test from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { readFileSync, readdirSync } from "node:fs";
23
+ import { join, relative } from "node:path";
24
+
25
+ const gsdDir = join(process.cwd(), "src/resources/extensions/gsd");
26
+
27
+ const ALLOWLIST = new Set([
28
+ "gsd-db.ts",
29
+ "unit-ownership.ts",
30
+ ]);
31
+
32
+ /** Walk the gsd extension dir and return all .ts files outside tests/. */
33
+ function walkTsFiles(root: string): string[] {
34
+ const out: string[] = [];
35
+ const stack: string[] = [root];
36
+
37
+ while (stack.length > 0) {
38
+ const dir = stack.pop()!;
39
+ let entries;
40
+ try {
41
+ entries = readdirSync(dir, { withFileTypes: true });
42
+ } catch {
43
+ continue;
44
+ }
45
+
46
+ for (const ent of entries) {
47
+ const full = join(dir, ent.name);
48
+ if (ent.isDirectory()) {
49
+ // Skip tests/ — fixtures and direct DB inspection are expected there
50
+ if (ent.name === "tests") continue;
51
+ stack.push(full);
52
+ continue;
53
+ }
54
+ if (!ent.isFile()) continue;
55
+ if (!ent.name.endsWith(".ts")) continue;
56
+ // Skip dotfiles and backup/generated files
57
+ if (ent.name.startsWith(".")) continue;
58
+ out.push(full);
59
+ }
60
+ }
61
+
62
+ return out;
63
+ }
64
+
65
+ interface Violation {
66
+ file: string;
67
+ line: number;
68
+ snippet: string;
69
+ kind: string;
70
+ }
71
+
72
+ // Match .prepare("... INSERT|UPDATE|DELETE|REPLACE ...") in any quoting style.
73
+ const PREPARE_WRITE_RE = /\.prepare\s*\(\s*[`'"][^`'"]*\b(INSERT|UPDATE|DELETE|REPLACE)\b/i;
74
+
75
+ // Match .exec("... INSERT|UPDATE|DELETE|REPLACE ...") or raw BEGIN/COMMIT/ROLLBACK.
76
+ const EXEC_WRITE_RE = /\.exec\s*\(\s*[`'"][^`'"]*\b(INSERT|UPDATE|DELETE|REPLACE|BEGIN|COMMIT|ROLLBACK)\b/i;
77
+
78
+ test("no module outside gsd-db.ts issues raw write SQL against the engine DB", () => {
79
+ const files = walkTsFiles(gsdDir);
80
+ assert.ok(files.length >= 20, `Expected at least 20 .ts files under gsd/, found ${files.length}`);
81
+
82
+ const violations: Violation[] = [];
83
+
84
+ for (const abs of files) {
85
+ const rel = relative(gsdDir, abs);
86
+ const base = rel.split("/").pop()!;
87
+ if (ALLOWLIST.has(base)) continue;
88
+
89
+ let content: string;
90
+ try {
91
+ content = readFileSync(abs, "utf-8");
92
+ } catch {
93
+ continue;
94
+ }
95
+
96
+ const lines = content.split("\n");
97
+ for (let i = 0; i < lines.length; i++) {
98
+ const line = lines[i];
99
+
100
+ const prepareMatch = PREPARE_WRITE_RE.exec(line);
101
+ if (prepareMatch) {
102
+ violations.push({
103
+ file: rel,
104
+ line: i + 1,
105
+ snippet: line.trim(),
106
+ kind: `prepare(${prepareMatch[1].toUpperCase()})`,
107
+ });
108
+ }
109
+
110
+ const execMatch = EXEC_WRITE_RE.exec(line);
111
+ if (execMatch) {
112
+ violations.push({
113
+ file: rel,
114
+ line: i + 1,
115
+ snippet: line.trim(),
116
+ kind: `exec(${execMatch[1].toUpperCase()})`,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ if (violations.length > 0) {
123
+ const lines = violations.map(
124
+ (v) => ` ${v.file}:${v.line} [${v.kind}] — ${v.snippet}`,
125
+ );
126
+ assert.fail(
127
+ `Found ${violations.length} raw write SQL bypass(es) outside gsd-db.ts:\n` +
128
+ lines.join("\n") +
129
+ "\n\nEach of these must be replaced with a typed wrapper exported from gsd-db.ts.",
130
+ );
131
+ }
132
+ });
133
+
134
+ test("gsd-db.ts exports the expected single-writer wrappers", async () => {
135
+ // Positive assertion — fail loudly if the module layout changes so this
136
+ // structural test can't silently become a no-op.
137
+ const db = await import("../gsd-db.js");
138
+
139
+ const expected = [
140
+ "deleteDecisionById",
141
+ "deleteRequirementById",
142
+ "deleteArtifactByPath",
143
+ "clearEngineHierarchy",
144
+ "insertOrIgnoreSlice",
145
+ "insertOrIgnoreTask",
146
+ "setSliceReplanTriggeredAt",
147
+ "upsertQualityGate",
148
+ "restoreManifest",
149
+ "bulkInsertLegacyHierarchy",
150
+ "readTransaction",
151
+ "insertMemoryRow",
152
+ "rewriteMemoryId",
153
+ "updateMemoryContentRow",
154
+ "incrementMemoryHitCount",
155
+ "supersedeMemoryRow",
156
+ "markMemoryUnitProcessed",
157
+ "decayMemoriesBefore",
158
+ "supersedeLowestRankedMemories",
159
+ ];
160
+
161
+ for (const name of expected) {
162
+ assert.ok(
163
+ typeof (db as Record<string, unknown>)[name] === "function",
164
+ `gsd-db.ts must export ${name} as a function`,
165
+ );
166
+ }
167
+ });
168
+
169
+ test("the invariant test touches every .ts module under gsd/ (sanity check)", () => {
170
+ const files = walkTsFiles(gsdDir);
171
+ // Rough sanity: ensure we're not accidentally walking an empty tree
172
+ assert.ok(files.length >= 30, `Expected to scan at least 30 .ts files, scanned ${files.length}`);
173
+
174
+ // Spot-check a couple of known files that must be included
175
+ const rels = files.map((f) => relative(gsdDir, f));
176
+ assert.ok(rels.includes("gsd-db.ts"), "walker must include gsd-db.ts");
177
+ assert.ok(rels.includes("memory-store.ts"), "walker must include memory-store.ts");
178
+ assert.ok(rels.includes("workflow-manifest.ts"), "walker must include workflow-manifest.ts");
179
+ });
180
+
@@ -0,0 +1,223 @@
1
+ // GSD Extension — workflow-logger wiring regression tests
2
+ //
3
+ // Verifies the plumbing between workflow-logger and the rest of the state
4
+ // system (auto-loop phases, detect-stuck, notification store). Without this
5
+ // wiring, warnings/errors logged during a unit leak across units, never
6
+ // reach the user as a consolidated post-unit alert, and don't enrich
7
+ // stuck-detection reasons.
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
+ import {
15
+ logWarning,
16
+ logError,
17
+ peekLogs,
18
+ _resetLogs,
19
+ setStderrLoggingEnabled,
20
+ } from "../workflow-logger.ts";
21
+ import { detectStuck } from "../auto/detect-stuck.ts";
22
+
23
+ const phasesSrc = readFileSync(
24
+ join(import.meta.dirname, "..", "auto", "phases.ts"),
25
+ "utf-8",
26
+ );
27
+ const autoSrc = readFileSync(
28
+ join(import.meta.dirname, "..", "auto.ts"),
29
+ "utf-8",
30
+ );
31
+
32
+ // ─── Source-scan: phases.ts calls the logger lifecycle API ─────────────────
33
+
34
+ test("auto/phases.ts imports _resetLogs, drainAndSummarize, formatForNotification, hasAnyIssues", () => {
35
+ assert.match(
36
+ phasesSrc,
37
+ /from\s+"\.\.\/workflow-logger\.js"/,
38
+ "phases.ts imports from workflow-logger",
39
+ );
40
+ for (const name of [
41
+ "_resetLogs",
42
+ "drainLogs",
43
+ "drainAndSummarize",
44
+ "formatForNotification",
45
+ "hasAnyIssues",
46
+ ]) {
47
+ assert.ok(
48
+ phasesSrc.includes(name),
49
+ `phases.ts should reference ${name}`,
50
+ );
51
+ }
52
+ });
53
+
54
+ test("runUnitPhase calls _resetLogs() before assigning s.currentUnit", () => {
55
+ // Find the "s.currentUnit = { type: unitType" assignment line and check
56
+ // the preceding ~500 chars contain a _resetLogs() call.
57
+ const idx = phasesSrc.indexOf("s.currentUnit = { type: unitType");
58
+ assert.ok(idx > 0, "runUnitPhase should assign s.currentUnit");
59
+ const before = phasesSrc.slice(Math.max(0, idx - 500), idx);
60
+ assert.match(
61
+ before,
62
+ /_resetLogs\(\)/,
63
+ "_resetLogs() must be called immediately before s.currentUnit assignment",
64
+ );
65
+ });
66
+
67
+ test("runFinalize drains and surfaces logger buffer via ctx.ui.notify", () => {
68
+ // Locate the runFinalize success path and verify it calls drainAndSummarize
69
+ // and routes the result through ctx.ui.notify.
70
+ const runFinalizeIdx = phasesSrc.indexOf("export async function runFinalize");
71
+ assert.ok(runFinalizeIdx > 0, "runFinalize export should exist");
72
+ const finalizeBody = phasesSrc.slice(runFinalizeIdx);
73
+ assert.match(
74
+ finalizeBody,
75
+ /hasAnyIssues\(\)/,
76
+ "runFinalize should gate drain on hasAnyIssues",
77
+ );
78
+ assert.match(
79
+ finalizeBody,
80
+ /drainAndSummarize\(\)/,
81
+ "runFinalize should call drainAndSummarize on success",
82
+ );
83
+ assert.match(
84
+ finalizeBody,
85
+ /formatForNotification\(logs\)/,
86
+ "runFinalize should format drained logs for the notification",
87
+ );
88
+ });
89
+
90
+ test("runFinalize timeout branches drain the buffer to prevent bleed", () => {
91
+ // Both timeout branches null out s.currentUnit — they should also drain
92
+ // so accumulated logs for the timed-out unit don't leak into the next.
93
+ const runFinalizeIdx = phasesSrc.indexOf("export async function runFinalize");
94
+ const finalizeBody = phasesSrc.slice(runFinalizeIdx);
95
+ const drainCallCount =
96
+ (finalizeBody.match(/drainLogs\(\)/g) ?? []).length;
97
+ assert.ok(
98
+ drainCallCount >= 2,
99
+ `runFinalize timeout branches should each call drainLogs() (found ${drainCallCount}, expected >= 2)`,
100
+ );
101
+ });
102
+
103
+ // ─── Source-scan: auto.ts calls setLogBasePath in startAuto ────────────────
104
+
105
+ test("startAuto calls setLogBasePath(base) so audit log is pinned on resume", () => {
106
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto");
107
+ assert.ok(startAutoIdx > 0, "startAuto export should exist");
108
+ const body = autoSrc.slice(startAutoIdx);
109
+ assert.match(
110
+ body,
111
+ /setLogBasePath\(base\)/,
112
+ "startAuto must call setLogBasePath(base) to pin the audit log",
113
+ );
114
+ });
115
+
116
+ // ─── Runtime: detect-stuck enriches reason with summarizeLogs() ────────────
117
+
118
+ test("detectStuck reason includes workflow-logger summary when logs present", () => {
119
+ setStderrLoggingEnabled(false);
120
+ try {
121
+ _resetLogs();
122
+ logWarning("projection", "STATE.md render failed");
123
+ logError("db", "WAL checkpoint failed");
124
+
125
+ const result = detectStuck([
126
+ { key: "execute-task/slice-A/task-1", error: "ENOENT: no such file" },
127
+ { key: "execute-task/slice-A/task-1", error: "ENOENT: no such file" },
128
+ ]);
129
+
130
+ assert.notEqual(result, null);
131
+ assert.equal(result!.stuck, true);
132
+ assert.match(
133
+ result!.reason,
134
+ /Same error repeated:/,
135
+ "reason should still start with the rule string",
136
+ );
137
+ assert.match(
138
+ result!.reason,
139
+ /STATE\.md render failed/,
140
+ "reason should include the accumulated logger warning",
141
+ );
142
+ assert.match(
143
+ result!.reason,
144
+ /WAL checkpoint failed/,
145
+ "reason should include the accumulated logger error",
146
+ );
147
+
148
+ // Critical: summarizeLogs must not drain — the auto-loop's finalize
149
+ // step owns the buffer lifecycle, detect-stuck is read-only.
150
+ assert.equal(
151
+ peekLogs().length,
152
+ 2,
153
+ "detect-stuck must not drain the buffer",
154
+ );
155
+ } finally {
156
+ _resetLogs();
157
+ setStderrLoggingEnabled(true);
158
+ }
159
+ });
160
+
161
+ test("detectStuck reason unchanged when logger buffer is empty", () => {
162
+ setStderrLoggingEnabled(false);
163
+ try {
164
+ _resetLogs();
165
+ const result = detectStuck([
166
+ { key: "A", error: "boom" },
167
+ { key: "A", error: "boom" },
168
+ ]);
169
+ assert.notEqual(result, null);
170
+ // No trailing " — " suffix when there are no logs to summarize.
171
+ assert.doesNotMatch(
172
+ result!.reason,
173
+ / — \d+ (error|warning)/,
174
+ "reason should have no logger suffix when buffer is empty",
175
+ );
176
+ } finally {
177
+ setStderrLoggingEnabled(true);
178
+ }
179
+ });
180
+
181
+ // ─── Runtime: readTransaction rollback failure surfaces via logError ────────
182
+ //
183
+ // snapshotState now delegates its transaction to readTransaction() in
184
+ // gsd-db.ts (single-writer refactor in #4198), so the split-brain
185
+ // ROLLBACK-failure log lives there, not in workflow-manifest.ts.
186
+
187
+ test("readTransaction logs ROLLBACK failures as split-brain signal", () => {
188
+ const dbSrc = readFileSync(
189
+ join(import.meta.dirname, "..", "gsd-db.ts"),
190
+ "utf-8",
191
+ );
192
+ assert.match(
193
+ dbSrc,
194
+ /logError\("db",\s*"snapshotState ROLLBACK failed"/,
195
+ "readTransaction ROLLBACK catch should call logError",
196
+ );
197
+ });
198
+
199
+ // ─── Runtime: state.ts and workflow-projections.ts log silent bailouts ─────
200
+
201
+ test("state.ts logs roadmap read failures instead of silently continuing", () => {
202
+ const stateSrc = readFileSync(
203
+ join(import.meta.dirname, "..", "state.ts"),
204
+ "utf-8",
205
+ );
206
+ assert.match(
207
+ stateSrc,
208
+ /logWarning\("state",\s*"reconcileDiskToDb: roadmap read failed/,
209
+ "state.ts reconcileDiskToDb should log roadmap read failures",
210
+ );
211
+ });
212
+
213
+ test("workflow-projections.ts logs DB probe failures instead of silent return", () => {
214
+ const projectionsSrc = readFileSync(
215
+ join(import.meta.dirname, "..", "workflow-projections.ts"),
216
+ "utf-8",
217
+ );
218
+ assert.match(
219
+ projectionsSrc,
220
+ /logWarning\("projection",\s*"renderStateProjection: DB handle probe failed/,
221
+ "renderStateProjection DB probe should log on failure",
222
+ );
223
+ });
@@ -424,6 +424,25 @@ export async function handleCompleteSlice(
424
424
  logError("tool", `complete-slice event log FAILED — completion invisible to reconciliation`, { error: (eventErr as Error).message });
425
425
  }
426
426
 
427
+ // Fire-and-forget graph rebuild — must NOT await, must NOT crash slice completion.
428
+ // Dynamic import of the package name (not a relative path) so it resolves
429
+ // correctly via package.json#exports in both development and production.
430
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
431
+ (async () => {
432
+ try {
433
+ const graphMod = await import("@gsd-build/mcp-server") as {
434
+ buildGraph: (dir: string) => Promise<{ nodes: unknown[]; edges: unknown[]; builtAt: string }>;
435
+ writeGraph: (gsdRoot: string, graph: unknown) => Promise<void>;
436
+ resolveGsdRoot: (basePath: string) => string;
437
+ };
438
+ const g = await graphMod.buildGraph(basePath);
439
+ await graphMod.writeGraph(graphMod.resolveGsdRoot(basePath), g);
440
+ } catch (graphErr) {
441
+ // Graph rebuild is best-effort — log at warning level but never propagate
442
+ logWarning("tool", `complete-slice graph rebuild failed (non-fatal): ${(graphErr as Error).message ?? String(graphErr)}`);
443
+ }
444
+ })();
445
+
427
446
  return {
428
447
  sliceId: params.sliceId,
429
448
  milestoneId: params.milestoneId,