gsd-pi 2.71.0-dev.06b86c6 → 2.71.0-dev.7a61d89

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 (151) hide show
  1. package/dist/headless-events.d.ts +2 -0
  2. package/dist/headless-events.js +7 -0
  3. package/dist/headless.js +16 -3
  4. package/dist/resource-loader.js +6 -3
  5. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +10 -4
  6. package/dist/resources/extensions/gsd/auto/infra-errors.js +34 -0
  7. package/dist/resources/extensions/gsd/auto/loop.js +32 -1
  8. package/dist/resources/extensions/gsd/auto/session.js +8 -0
  9. package/dist/resources/extensions/gsd/auto-dashboard.js +22 -16
  10. package/dist/resources/extensions/gsd/auto.js +52 -0
  11. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +66 -51
  12. package/dist/resources/extensions/gsd/commands/handlers/auto.js +10 -33
  13. package/dist/resources/extensions/gsd/commands/handlers/core.js +45 -11
  14. package/dist/resources/extensions/gsd/commands/handlers/notifications-handler.js +15 -6
  15. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +4 -10
  16. package/dist/resources/extensions/gsd/dashboard-overlay.js +8 -3
  17. package/dist/resources/extensions/gsd/forensics.js +19 -6
  18. package/dist/resources/extensions/gsd/guided-flow.js +5 -10
  19. package/dist/resources/extensions/gsd/metrics.js +1 -0
  20. package/dist/resources/extensions/gsd/milestone-actions.js +10 -4
  21. package/dist/resources/extensions/gsd/notification-overlay.js +20 -5
  22. package/dist/resources/extensions/gsd/notification-store.js +30 -0
  23. package/dist/resources/extensions/gsd/notification-widget.js +5 -13
  24. package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +8 -3
  25. package/dist/resources/extensions/gsd/shortcut-defs.js +34 -0
  26. package/dist/web/standalone/.next/BUILD_ID +1 -1
  27. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  28. package/dist/web/standalone/.next/build-manifest.json +2 -2
  29. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  30. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.html +1 -1
  47. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  54. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  55. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  56. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  57. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  58. package/package.json +1 -1
  59. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts +2 -0
  60. package/packages/pi-ai/dist/providers/anthropic-auth.test.d.ts.map +1 -0
  61. package/packages/pi-ai/dist/providers/anthropic-auth.test.js +20 -0
  62. package/packages/pi-ai/dist/providers/anthropic-auth.test.js.map +1 -0
  63. package/packages/pi-ai/dist/providers/anthropic.d.ts +2 -1
  64. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  65. package/packages/pi-ai/dist/providers/anthropic.js +7 -4
  66. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  67. package/packages/pi-ai/src/providers/anthropic-auth.test.ts +32 -0
  68. package/packages/pi-ai/src/providers/anthropic.ts +8 -4
  69. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts +2 -0
  70. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.d.ts.map +1 -0
  71. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js +61 -0
  72. package/packages/pi-coding-agent/dist/core/agent-session-renderable-tools.test.js.map +1 -0
  73. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/core/agent-session.js +2 -1
  75. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +10 -0
  77. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/core/auth-storage.js +27 -0
  79. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +85 -0
  81. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  82. package/packages/pi-coding-agent/dist/core/sdk.d.ts +11 -0
  83. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  84. package/packages/pi-coding-agent/dist/core/sdk.js +38 -5
  85. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  86. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts +2 -0
  87. package/packages/pi-coding-agent/dist/core/sdk.test.d.ts.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/sdk.test.js +71 -0
  89. package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -0
  90. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  91. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/index.js +1 -1
  93. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts +4 -0
  95. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +43 -0
  97. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +7 -2
  100. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -3
  103. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js +4 -2
  105. package/packages/pi-coding-agent/dist/modes/interactive/slash-command-handlers.js.map +1 -1
  106. package/packages/pi-coding-agent/src/core/agent-session-renderable-tools.test.ts +70 -0
  107. package/packages/pi-coding-agent/src/core/agent-session.ts +2 -1
  108. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +108 -0
  109. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -0
  110. package/packages/pi-coding-agent/src/core/sdk.test.ts +89 -0
  111. package/packages/pi-coding-agent/src/core/sdk.ts +45 -9
  112. package/packages/pi-coding-agent/src/index.ts +1 -0
  113. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +47 -0
  114. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +7 -2
  115. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -3
  116. package/packages/pi-coding-agent/src/modes/interactive/slash-command-handlers.ts +4 -2
  117. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +13 -5
  118. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +56 -4
  119. package/src/resources/extensions/gsd/auto/infra-errors.ts +38 -0
  120. package/src/resources/extensions/gsd/auto/loop.ts +45 -1
  121. package/src/resources/extensions/gsd/auto/session.ts +8 -0
  122. package/src/resources/extensions/gsd/auto-dashboard.ts +29 -18
  123. package/src/resources/extensions/gsd/auto.ts +68 -0
  124. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +82 -60
  125. package/src/resources/extensions/gsd/commands/handlers/auto.ts +10 -36
  126. package/src/resources/extensions/gsd/commands/handlers/core.ts +46 -11
  127. package/src/resources/extensions/gsd/commands/handlers/notifications-handler.ts +17 -7
  128. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +4 -10
  129. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -3
  130. package/src/resources/extensions/gsd/forensics.ts +23 -7
  131. package/src/resources/extensions/gsd/guided-flow.ts +5 -10
  132. package/src/resources/extensions/gsd/interrupted-session.ts +1 -0
  133. package/src/resources/extensions/gsd/metrics.ts +12 -1
  134. package/src/resources/extensions/gsd/milestone-actions.ts +10 -3
  135. package/src/resources/extensions/gsd/notification-overlay.ts +24 -7
  136. package/src/resources/extensions/gsd/notification-store.ts +30 -0
  137. package/src/resources/extensions/gsd/notification-widget.ts +5 -14
  138. package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +10 -3
  139. package/src/resources/extensions/gsd/shortcut-defs.ts +49 -0
  140. package/src/resources/extensions/gsd/tests/forensics-stuck-loops.test.ts +62 -0
  141. package/src/resources/extensions/gsd/tests/format-shortcut.test.ts +15 -0
  142. package/src/resources/extensions/gsd/tests/infra-errors-cooldown.test.ts +180 -0
  143. package/src/resources/extensions/gsd/tests/notification-store.test.ts +18 -0
  144. package/src/resources/extensions/gsd/tests/notification-widget.test.ts +3 -2
  145. package/src/resources/extensions/gsd/tests/notifications-handler.test.ts +90 -0
  146. package/src/resources/extensions/gsd/tests/parallel-monitor-overlay.test.ts +1 -0
  147. package/src/resources/extensions/gsd/tests/park-db-sync.test.ts +18 -0
  148. package/src/resources/extensions/gsd/tests/register-shortcuts.test.ts +62 -5
  149. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +90 -0
  150. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_buildManifest.js +0 -0
  151. /package/dist/web/standalone/.next/static/{dYVdRaunb2ZSEA8fjkT-V → ug91LJa0m7OdzrTVaz_48}/_ssgManifest.js +0 -0
@@ -0,0 +1,180 @@
1
+ // gsd / infra-errors cooldown detection tests
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import test, { describe } from "node:test";
5
+ import assert from "node:assert/strict";
6
+
7
+ import {
8
+ isTransientCooldownError,
9
+ getCooldownRetryAfterMs,
10
+ MAX_COOLDOWN_RETRIES,
11
+ COOLDOWN_FALLBACK_WAIT_MS,
12
+ } from "../auto/infra-errors.js";
13
+
14
+ // ─── Constants ────────────────────────────────────────────────────────────────
15
+
16
+ describe("infra-errors cooldown constants", () => {
17
+ test("COOLDOWN_FALLBACK_WAIT_MS is a positive number greater than the 30s rate-limit backoff", () => {
18
+ assert.ok(typeof COOLDOWN_FALLBACK_WAIT_MS === "number");
19
+ assert.ok(COOLDOWN_FALLBACK_WAIT_MS > 30_000, "should exceed the 30s rate-limit window");
20
+ });
21
+
22
+ test("MAX_COOLDOWN_RETRIES is a positive integer", () => {
23
+ assert.ok(typeof MAX_COOLDOWN_RETRIES === "number");
24
+ assert.ok(Number.isInteger(MAX_COOLDOWN_RETRIES));
25
+ assert.ok(MAX_COOLDOWN_RETRIES > 0);
26
+ });
27
+
28
+ test("COOLDOWN_FALLBACK_WAIT_MS is 35_000", () => {
29
+ assert.equal(COOLDOWN_FALLBACK_WAIT_MS, 35_000);
30
+ });
31
+
32
+ test("MAX_COOLDOWN_RETRIES is 5", () => {
33
+ assert.equal(MAX_COOLDOWN_RETRIES, 5);
34
+ });
35
+ });
36
+
37
+ // ─── isTransientCooldownError: structured detection ──────────────────────────
38
+
39
+ describe("isTransientCooldownError — structured code detection", () => {
40
+ test("returns true for an object with code === AUTH_COOLDOWN", () => {
41
+ const err = { code: "AUTH_COOLDOWN", message: "credentials in cooldown" };
42
+ assert.equal(isTransientCooldownError(err), true);
43
+ });
44
+
45
+ test("returns true for a real CredentialCooldownError-shaped error", () => {
46
+ // Simulate CredentialCooldownError without importing sdk.ts (leaf-module rule)
47
+ const err = Object.assign(new Error('All credentials for "anthropic" are in a cooldown window.'), {
48
+ code: "AUTH_COOLDOWN",
49
+ retryAfterMs: 30_000,
50
+ name: "CredentialCooldownError",
51
+ });
52
+ assert.equal(isTransientCooldownError(err), true);
53
+ });
54
+
55
+ test("returns false for an object with a different code", () => {
56
+ const err = { code: "ENOSPC", message: "disk full" };
57
+ assert.equal(isTransientCooldownError(err), false);
58
+ });
59
+
60
+ test("returns false for an object with no code property", () => {
61
+ const err = { message: "some random error" };
62
+ assert.equal(isTransientCooldownError(err), false);
63
+ });
64
+ });
65
+
66
+ // ─── isTransientCooldownError: message fallback ───────────────────────────────
67
+
68
+ describe("isTransientCooldownError — message fallback (cross-process)", () => {
69
+ test("returns true when message contains 'in a cooldown window'", () => {
70
+ const err = new Error('All credentials for "openai" are in a cooldown window. Please wait.');
71
+ assert.equal(isTransientCooldownError(err), true);
72
+ });
73
+
74
+ test("returns true when message matches case-insensitively", () => {
75
+ const err = new Error("credentials IN A COOLDOWN WINDOW");
76
+ assert.equal(isTransientCooldownError(err), true);
77
+ });
78
+
79
+ test("returns true for a plain string containing cooldown window phrase", () => {
80
+ assert.equal(isTransientCooldownError("all keys in a cooldown window"), true);
81
+ });
82
+
83
+ test("returns false for a generic error message", () => {
84
+ const err = new Error("rate limit exceeded");
85
+ assert.equal(isTransientCooldownError(err), false);
86
+ });
87
+
88
+ test("returns false for an error message about auth failure without cooldown phrase", () => {
89
+ const err = new Error("Authentication failed: invalid API key");
90
+ assert.equal(isTransientCooldownError(err), false);
91
+ });
92
+ });
93
+
94
+ // ─── isTransientCooldownError: edge cases ────────────────────────────────────
95
+
96
+ describe("isTransientCooldownError — edge cases", () => {
97
+ test("returns false for null", () => {
98
+ assert.equal(isTransientCooldownError(null), false);
99
+ });
100
+
101
+ test("returns false for undefined", () => {
102
+ assert.equal(isTransientCooldownError(undefined), false);
103
+ });
104
+
105
+ test("returns false for a number", () => {
106
+ assert.equal(isTransientCooldownError(42), false);
107
+ });
108
+
109
+ test("returns false for an empty object", () => {
110
+ assert.equal(isTransientCooldownError({}), false);
111
+ });
112
+
113
+ test("returns false for an object with code === AUTH_COOLDOWN as a non-string", () => {
114
+ // code must be a string matching "AUTH_COOLDOWN" exactly
115
+ const err = { code: 42 };
116
+ assert.equal(isTransientCooldownError(err), false);
117
+ });
118
+ });
119
+
120
+ // ─── getCooldownRetryAfterMs: structured extraction ──────────────────────────
121
+
122
+ describe("getCooldownRetryAfterMs — structured extraction", () => {
123
+ test("returns retryAfterMs when code is AUTH_COOLDOWN and retryAfterMs is set", () => {
124
+ const err = { code: "AUTH_COOLDOWN", retryAfterMs: 30_000 };
125
+ assert.equal(getCooldownRetryAfterMs(err), 30_000);
126
+ });
127
+
128
+ test("returns undefined when code is AUTH_COOLDOWN but retryAfterMs is absent", () => {
129
+ const err = { code: "AUTH_COOLDOWN" };
130
+ assert.equal(getCooldownRetryAfterMs(err), undefined);
131
+ });
132
+
133
+ test("returns 0 when retryAfterMs is explicitly 0", () => {
134
+ const err = { code: "AUTH_COOLDOWN", retryAfterMs: 0 };
135
+ assert.equal(getCooldownRetryAfterMs(err), 0);
136
+ });
137
+
138
+ test("returns undefined for an error with a different code even if retryAfterMs is set", () => {
139
+ const err = { code: "ENOSPC", retryAfterMs: 5_000 };
140
+ assert.equal(getCooldownRetryAfterMs(err), undefined);
141
+ });
142
+
143
+ test("returns undefined for a plain Error with no code property", () => {
144
+ const err = new Error("something went wrong");
145
+ assert.equal(getCooldownRetryAfterMs(err), undefined);
146
+ });
147
+
148
+ test("returns retryAfterMs from a full CredentialCooldownError-shaped object", () => {
149
+ const err = Object.assign(new Error('All credentials for "anthropic" are in a cooldown window.'), {
150
+ code: "AUTH_COOLDOWN",
151
+ retryAfterMs: 15_000,
152
+ name: "CredentialCooldownError",
153
+ });
154
+ assert.equal(getCooldownRetryAfterMs(err), 15_000);
155
+ });
156
+ });
157
+
158
+ // ─── getCooldownRetryAfterMs: edge cases ─────────────────────────────────────
159
+
160
+ describe("getCooldownRetryAfterMs — edge cases", () => {
161
+ test("returns undefined for null", () => {
162
+ assert.equal(getCooldownRetryAfterMs(null), undefined);
163
+ });
164
+
165
+ test("returns undefined for undefined", () => {
166
+ assert.equal(getCooldownRetryAfterMs(undefined), undefined);
167
+ });
168
+
169
+ test("returns undefined for a plain string", () => {
170
+ assert.equal(getCooldownRetryAfterMs("AUTH_COOLDOWN"), undefined);
171
+ });
172
+
173
+ test("returns undefined for an empty object", () => {
174
+ assert.equal(getCooldownRetryAfterMs({}), undefined);
175
+ });
176
+
177
+ test("returns undefined for a number", () => {
178
+ assert.equal(getCooldownRetryAfterMs(42), undefined);
179
+ });
180
+ });
@@ -16,6 +16,7 @@ import {
16
16
  getLineCount,
17
17
  suppressPersistence,
18
18
  unsuppressPersistence,
19
+ onNotificationStoreChange,
19
20
  _resetNotificationStore,
20
21
  } from "../notification-store.js";
21
22
 
@@ -296,4 +297,21 @@ describe("notification-store", () => {
296
297
 
297
298
  rmSync(lockPath, { force: true });
298
299
  });
300
+
301
+ test("listeners are notified on append, markAllRead, and clear", () => {
302
+ initNotificationStore(tmp);
303
+ let calls = 0;
304
+ const unsubscribe = onNotificationStoreChange(() => { calls++; });
305
+
306
+ appendNotification("msg1", "info");
307
+ assert.equal(calls, 1, "append should emit one change");
308
+
309
+ markAllRead();
310
+ assert.equal(calls, 2, "markAllRead should emit one change when state changes");
311
+
312
+ clearNotifications();
313
+ assert.equal(calls, 3, "clear should emit one change");
314
+
315
+ unsubscribe();
316
+ });
299
317
  });
@@ -7,7 +7,7 @@ import { tmpdir } from "node:os";
7
7
  import { initNotificationStore, appendNotification, _resetNotificationStore } from "../notification-store.js";
8
8
  import { buildNotificationWidgetLines } from "../notification-widget.js";
9
9
 
10
- test("buildNotificationWidgetLines includes slash-command fallback for unread notifications", () => {
10
+ test("buildNotificationWidgetLines shows unread count with shortcut pair", () => {
11
11
  const tmp = mkdtempSync(join(tmpdir(), "gsd-notification-widget-"));
12
12
  try {
13
13
  mkdirSync(join(tmp, ".gsd"), { recursive: true });
@@ -17,7 +17,8 @@ test("buildNotificationWidgetLines includes slash-command fallback for unread no
17
17
 
18
18
  const lines = buildNotificationWidgetLines();
19
19
  assert.equal(lines.length, 1);
20
- assert.match(lines[0]!, /\/gsd notifications/);
20
+ assert.match(lines[0]!, /Notifications:\s+1 unread/);
21
+ assert.match(lines[0]!, /\(.+\/.+\)/);
21
22
  } finally {
22
23
  _resetNotificationStore();
23
24
  rmSync(tmp, { recursive: true, force: true });
@@ -0,0 +1,90 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { mkdirSync, rmSync } from "node:fs";
6
+
7
+ import { handleNotificationsCommand } from "../commands/handlers/notifications-handler.ts";
8
+ import {
9
+ _resetNotificationStore,
10
+ appendNotification,
11
+ initNotificationStore,
12
+ } from "../notification-store.ts";
13
+
14
+ function makeTempDir(prefix: string): string {
15
+ const dir = join(
16
+ tmpdir(),
17
+ `gsd-notifications-handler-test-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
18
+ );
19
+ mkdirSync(dir, { recursive: true });
20
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
21
+ return dir;
22
+ }
23
+
24
+ function cleanup(dir: string): void {
25
+ try {
26
+ rmSync(dir, { recursive: true, force: true });
27
+ } catch {
28
+ // best-effort
29
+ }
30
+ }
31
+
32
+ test("notifications command falls back to text output when overlay returns undefined", async (t) => {
33
+ const base = makeTempDir("overlay-fallback");
34
+ initNotificationStore(base);
35
+ appendNotification("Build complete", "success");
36
+
37
+ t.after(() => {
38
+ _resetNotificationStore();
39
+ cleanup(base);
40
+ });
41
+
42
+ const notices: Array<{ message: string; level?: string }> = [];
43
+ await handleNotificationsCommand(
44
+ "",
45
+ {
46
+ hasUI: true,
47
+ ui: {
48
+ custom: async () => undefined,
49
+ notify: (message: string, level?: string) => {
50
+ notices.push({ message, level });
51
+ },
52
+ },
53
+ } as any,
54
+ {} as any,
55
+ );
56
+
57
+ assert.equal(notices.length, 1, "text fallback should be emitted when overlay cannot render");
58
+ assert.match(notices[0].message, /Recent notifications:/);
59
+ });
60
+
61
+ test("notifications tail caps inline output and hints to open overlay", async (t) => {
62
+ const base = makeTempDir("tail-cap");
63
+ initNotificationStore(base);
64
+ for (let i = 0; i < 55; i++) {
65
+ appendNotification(`notification-${i + 1}`, "info");
66
+ }
67
+
68
+ t.after(() => {
69
+ _resetNotificationStore();
70
+ cleanup(base);
71
+ });
72
+
73
+ const notices: Array<{ message: string; level?: string }> = [];
74
+ await handleNotificationsCommand(
75
+ "tail 200",
76
+ {
77
+ hasUI: true,
78
+ ui: {
79
+ notify: (message: string, level?: string) => {
80
+ notices.push({ message, level });
81
+ },
82
+ },
83
+ } as any,
84
+ {} as any,
85
+ );
86
+
87
+ assert.equal(notices.length, 1);
88
+ assert.match(notices[0].message, /Last 40 notification\(s\):/);
89
+ assert.match(notices[0].message, /\.\.\. and \d+ more \(open \/gsd notifications to browse all\)/);
90
+ });
@@ -56,6 +56,7 @@ describe("parallel-monitor-overlay", () => {
56
56
  overlay2.handleInput("q");
57
57
  assert.ok(closed, "pressing q should trigger onClose");
58
58
  overlay2.dispose();
59
+
59
60
  });
60
61
 
61
62
  it("ParallelMonitorOverlay clamps scrollOffset during render", async () => {
@@ -69,6 +69,24 @@ test("unparkMilestone updates DB status to 'active' (#2694)", () => {
69
69
  }
70
70
  });
71
71
 
72
+ test("unparkMilestone repairs parked DB state when PARKED.md is missing (#3707)", () => {
73
+ const base = createBase();
74
+ try {
75
+ openDatabase(":memory:");
76
+ insertMilestone({ id: "M001", title: "Test", status: "parked" });
77
+
78
+ const unparked = unparkMilestone(base, "M001");
79
+
80
+ assert.ok(unparked, "unparkMilestone should recover DB-only parked state");
81
+ assert.equal(getMilestone("M001")!.status, "active", "DB status should be repaired to active");
82
+
83
+ closeDatabase();
84
+ } finally {
85
+ closeDatabase();
86
+ rmSync(base, { recursive: true, force: true });
87
+ }
88
+ });
89
+
72
90
  test("park/unpark are safe when DB is not available (#2694 guard)", () => {
73
91
  const base = createBase();
74
92
  try {
@@ -1,6 +1,6 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
- import { mkdirSync, rmSync } from "node:fs";
3
+ import { mkdirSync, realpathSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { tmpdir } from "node:os";
6
6
 
@@ -37,10 +37,10 @@ test("dashboard shortcut resolves the project root instead of the current worktr
37
37
  });
38
38
 
39
39
  let capturedHandler: ((ctx: any) => Promise<void>) | null = null;
40
- const shortcuts: Array<{ description: string; handler: (ctx: any) => Promise<void> }> = [];
40
+ const shortcuts: Array<{ key: string; description: string; handler: (ctx: any) => Promise<void> }> = [];
41
41
  const pi = {
42
- registerShortcut: (_key: unknown, shortcut: { description: string; handler: (ctx: any) => Promise<void> }) => {
43
- shortcuts.push(shortcut);
42
+ registerShortcut: (key: unknown, shortcut: { description: string; handler: (ctx: any) => Promise<void> }) => {
43
+ shortcuts.push({ key: String(key), ...shortcut });
44
44
  if (!capturedHandler) {
45
45
  capturedHandler = shortcut.handler;
46
46
  }
@@ -69,5 +69,62 @@ test("dashboard shortcut resolves the project root instead of the current worktr
69
69
 
70
70
  assert.ok(customCalls > 0, "shortcut opens the dashboard overlay when project root is resolved");
71
71
  assert.equal(notices.length, 0, "shortcut does not fall back to the missing-.gsd warning");
72
- assert.equal(shortcuts.length, 3, "all GSD shortcuts are still registered");
72
+ assert.equal(shortcuts.length, 6, "all GSD shortcuts are still registered");
73
+ const keys = shortcuts.map((shortcut) => shortcut.key);
74
+ assert.ok(keys.includes("ctrl+alt+g"), "primary dashboard shortcut is registered");
75
+ assert.ok(keys.includes("ctrl+shift+g"), "fallback dashboard shortcut is registered");
76
+ assert.ok(keys.includes("ctrl+alt+n"), "primary notifications shortcut is registered");
77
+ assert.ok(keys.includes("ctrl+shift+n"), "fallback notifications shortcut is registered");
78
+ assert.ok(keys.includes("ctrl+alt+p"), "primary parallel shortcut is registered");
79
+ assert.ok(keys.includes("ctrl+shift+p"), "fallback parallel shortcut is registered");
80
+ });
81
+
82
+ test("parallel shortcut passes resolved project root into overlay", async (t) => {
83
+ const base = makeTempDir("parallel-root");
84
+ const worktreeRoot = join(base, ".gsd", "worktrees", "M001");
85
+ mkdirSync(join(base, ".gsd", "parallel"), { recursive: true });
86
+ mkdirSync(worktreeRoot, { recursive: true });
87
+
88
+ const originalCwd = process.cwd();
89
+ process.chdir(worktreeRoot);
90
+ t.after(() => {
91
+ process.chdir(originalCwd);
92
+ cleanup(base);
93
+ });
94
+
95
+ const shortcuts: Array<{ key: string; description: string; handler: (ctx: any) => Promise<void> }> = [];
96
+ registerShortcuts({
97
+ registerShortcut: (key: unknown, shortcut: { description: string; handler: (ctx: any) => Promise<void> }) => {
98
+ shortcuts.push({ key: String(key), ...shortcut });
99
+ },
100
+ } as any);
101
+
102
+ const parallelShortcut = shortcuts.find((shortcut) => shortcut.key === "ctrl+alt+p");
103
+ assert.ok(parallelShortcut, "parallel shortcut is registered");
104
+
105
+ let capturedBasePath: string | undefined;
106
+ await parallelShortcut!.handler({
107
+ hasUI: true,
108
+ ui: {
109
+ custom: async (factory: any) => {
110
+ const overlay = factory(
111
+ { requestRender() {} },
112
+ { fg: (_color: string, text: string) => text, bold: (text: string) => text },
113
+ null,
114
+ () => {},
115
+ );
116
+ capturedBasePath = (overlay as any).basePath;
117
+ overlay.dispose?.();
118
+ return true;
119
+ },
120
+ notify: () => {},
121
+ },
122
+ });
123
+
124
+ assert.ok(capturedBasePath, "parallel shortcut should construct overlay with a basePath");
125
+ assert.equal(
126
+ realpathSync(capturedBasePath),
127
+ realpathSync(base),
128
+ "parallel overlay should use the resolved project root, not the worktree cwd",
129
+ );
73
130
  });
@@ -0,0 +1,90 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+
6
+ const gsdDir = resolve(import.meta.dirname, "..");
7
+
8
+ function readGsdFile(relativePath: string): string {
9
+ return readFileSync(resolve(gsdDir, relativePath), "utf-8");
10
+ }
11
+
12
+ test("command entrypoints use startAutoDetached instead of awaiting startAuto (#3733)", () => {
13
+ const autoHandlerSrc = readGsdFile("commands/handlers/auto.ts");
14
+ const workflowHandlerSrc = readGsdFile("commands/handlers/workflow.ts");
15
+ const guidedFlowSrc = readGsdFile("guided-flow.ts");
16
+
17
+ assert.ok(
18
+ !autoHandlerSrc.includes("await startAuto("),
19
+ "auto command handler should not await startAuto from the active agent turn",
20
+ );
21
+ assert.ok(
22
+ !workflowHandlerSrc.includes("await startAuto("),
23
+ "workflow command handler should not await startAuto from the active agent turn",
24
+ );
25
+ assert.ok(
26
+ !guidedFlowSrc.includes("await startAuto("),
27
+ "guided flow should not await startAuto from the active agent turn",
28
+ );
29
+
30
+ assert.ok(
31
+ autoHandlerSrc.includes("startAutoDetached("),
32
+ "auto command handler should launch auto-mode through startAutoDetached",
33
+ );
34
+ assert.ok(
35
+ workflowHandlerSrc.includes("startAutoDetached("),
36
+ "workflow handler should launch auto-mode through startAutoDetached",
37
+ );
38
+ assert.ok(
39
+ guidedFlowSrc.includes("startAutoDetached("),
40
+ "guided flow should launch auto-mode through startAutoDetached",
41
+ );
42
+ });
43
+
44
+ test("startAutoDetached reports failures asynchronously (#3733)", () => {
45
+ const autoSrc = readGsdFile("auto.ts");
46
+
47
+ assert.ok(
48
+ autoSrc.includes("export function startAutoDetached"),
49
+ "auto.ts should export startAutoDetached",
50
+ );
51
+ assert.ok(
52
+ autoSrc.includes("void startAuto(ctx, pi, base, verboseMode, options).catch"),
53
+ "startAutoDetached should launch startAuto without awaiting it",
54
+ );
55
+ assert.ok(
56
+ autoSrc.includes("ctx.ui.notify(`Auto-start failed: ${message}`, \"error\")"),
57
+ "startAutoDetached should surface async startup failures to the user",
58
+ );
59
+ });
60
+
61
+ test("detached auto-start preserves milestone lock across pause/stop cleanup (#3733)", () => {
62
+ const autoSrc = readGsdFile("auto.ts");
63
+ const sessionSrc = readGsdFile("auto/session.ts");
64
+
65
+ assert.ok(
66
+ autoSrc.includes("milestoneLock?: string | null"),
67
+ "startAuto/startAutoDetached options should carry an explicit milestone lock",
68
+ );
69
+ assert.ok(
70
+ autoSrc.includes("s.sessionMilestoneLock = options.milestoneLock ?? null;"),
71
+ "startAuto should capture the requested milestone lock before async work begins",
72
+ );
73
+ assert.ok(
74
+ autoSrc.includes("milestoneLock: s.sessionMilestoneLock ?? undefined"),
75
+ "pause metadata should persist the detached milestone lock for resume",
76
+ );
77
+ assert.ok(
78
+ autoSrc.includes("s.sessionMilestoneLock = meta.milestoneLock ?? null;"),
79
+ "resume should restore the persisted milestone lock",
80
+ );
81
+ assert.ok(
82
+ autoSrc.includes("restoreMilestoneLockEnv();"),
83
+ "auto cleanup should restore the previous process milestone-lock env",
84
+ );
85
+
86
+ assert.ok(
87
+ sessionSrc.includes("sessionMilestoneLock: string | null = null;"),
88
+ "AutoSession should track the detached milestone lock explicitly",
89
+ );
90
+ });