@opengsd/gsd-pi 1.6.0 → 1.7.0

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 (92) hide show
  1. package/README.md +6 -10
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
  4. package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
  5. package/dist/resources/extensions/gsd/auto.js +12 -0
  6. package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
  7. package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
  8. package/dist/resources/extensions/gsd/session-status-io.js +31 -0
  9. package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
  10. package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -5
  11. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  12. package/dist/web/standalone/.next/BUILD_ID +1 -1
  13. package/dist/web/standalone/.next/app-path-routes-manifest.json +11 -11
  14. package/dist/web/standalone/.next/build-manifest.json +2 -2
  15. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  16. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  17. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  24. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  25. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/index.html +1 -1
  32. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  33. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app-paths-manifest.json +11 -11
  39. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  40. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  41. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  42. package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
  43. package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
  44. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  45. package/integrations/hermes/pyproject.toml +1 -1
  46. package/package.json +6 -6
  47. package/packages/cloud-mcp-gateway/package.json +2 -2
  48. package/packages/contracts/package.json +1 -1
  49. package/packages/daemon/dist/cloud-cli.d.ts +1 -1
  50. package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
  51. package/packages/daemon/dist/cloud-cli.js +42 -2
  52. package/packages/daemon/dist/cloud-cli.js.map +1 -1
  53. package/packages/daemon/dist/cloud-config.d.ts +1 -0
  54. package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
  55. package/packages/daemon/dist/cloud-config.js +1 -1
  56. package/packages/daemon/dist/cloud-config.js.map +1 -1
  57. package/packages/daemon/dist/device-flow.d.ts +24 -0
  58. package/packages/daemon/dist/device-flow.d.ts.map +1 -0
  59. package/packages/daemon/dist/device-flow.js +128 -0
  60. package/packages/daemon/dist/device-flow.js.map +1 -0
  61. package/packages/daemon/dist/device-flow.test.d.ts +2 -0
  62. package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
  63. package/packages/daemon/dist/device-flow.test.js +172 -0
  64. package/packages/daemon/dist/device-flow.test.js.map +1 -0
  65. package/packages/daemon/package.json +5 -5
  66. package/packages/gsd-agent-core/package.json +5 -5
  67. package/packages/gsd-agent-modes/package.json +7 -7
  68. package/packages/gsd-cloud/README.md +34 -0
  69. package/packages/gsd-cloud/bin/gsd-cloud.js +27 -0
  70. package/packages/gsd-cloud/package.json +38 -0
  71. package/packages/mcp-server/package.json +4 -4
  72. package/packages/native/package.json +1 -1
  73. package/packages/pi-agent-core/package.json +1 -1
  74. package/packages/pi-ai/package.json +1 -1
  75. package/packages/pi-coding-agent/package.json +7 -7
  76. package/packages/pi-tui/package.json +2 -2
  77. package/packages/rpc-client/package.json +2 -2
  78. package/pkg/package.json +1 -1
  79. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
  80. package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
  81. package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
  82. package/src/resources/extensions/gsd/auto.ts +12 -0
  83. package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
  84. package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
  85. package/src/resources/extensions/gsd/session-status-io.ts +34 -0
  86. package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
  87. package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
  88. package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
  89. package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
  90. package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
  91. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → enWQFRY-aLTUq3wmxG2Qz}/_buildManifest.js +0 -0
  92. /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → enWQFRY-aLTUq3wmxG2Qz}/_ssgManifest.js +0 -0
@@ -0,0 +1,117 @@
1
+ // GSD — reopen-reason injection tests (#1272)
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ import test from 'node:test';
5
+ import assert from 'node:assert/strict';
6
+ import { mkdtempSync, mkdirSync, rmSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+
10
+ import {
11
+ openDatabase,
12
+ closeDatabase,
13
+ insertMilestone,
14
+ insertSlice,
15
+ insertTask,
16
+ } from '../gsd-db.ts';
17
+ import { handleReopenTask } from '../tools/reopen-task.ts';
18
+ import {
19
+ reopenReasonArtifactPath,
20
+ writeReopenReason,
21
+ claimReopenReasonForInjection,
22
+ } from '../reopen-reason.ts';
23
+
24
+ function makeTmpBase(): string {
25
+ const base = mkdtempSync(join(tmpdir(), 'gsd-reopen-reason-'));
26
+ mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
27
+ return base;
28
+ }
29
+
30
+ function cleanup(base: string): void {
31
+ try { closeDatabase(); } catch { /* noop */ }
32
+ try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
33
+ }
34
+
35
+ function seedCompleteTask(): void {
36
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
37
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'in_progress' });
38
+ insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task One', status: 'complete' });
39
+ }
40
+
41
+ test('writeReopenReason then claim: injects the reason, then is one-shot', () => {
42
+ const base = makeTmpBase();
43
+ try {
44
+ const reason = 'T01 shifted the descendants count to 12. Fix: update the assertion to 12.';
45
+ writeReopenReason(base, 'M001', 'S01', 'T01', reason);
46
+
47
+ const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
48
+ assert.ok(existsSync(path), 'artifact should be written');
49
+
50
+ const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
51
+ assert.ok(claimed, 'first claim should return an injection block');
52
+ assert.match(claimed!.injectionBlock, /Reopened — Reason/);
53
+ assert.match(claimed!.injectionBlock, /descendants count to 12/);
54
+ assert.ok(!existsSync(path), 'artifact should be deleted after claim (one-shot)');
55
+
56
+ const second = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
57
+ assert.equal(second, null, 'second claim should return null');
58
+ } finally {
59
+ cleanup(base);
60
+ }
61
+ });
62
+
63
+ test('claimReopenReasonForInjection: returns null when nothing pending', () => {
64
+ const base = makeTmpBase();
65
+ try {
66
+ assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
67
+ } finally {
68
+ cleanup(base);
69
+ }
70
+ });
71
+
72
+ test('writeReopenReason: empty/whitespace reason is a no-op', () => {
73
+ const base = makeTmpBase();
74
+ try {
75
+ writeReopenReason(base, 'M001', 'S01', 'T01', ' ');
76
+ const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
77
+ assert.ok(!existsSync(path), 'no artifact should be written for an empty reason');
78
+ } finally {
79
+ cleanup(base);
80
+ }
81
+ });
82
+
83
+ test('handleReopenTask: persists a claimable reopen reason when reason is provided', async () => {
84
+ const base = makeTmpBase();
85
+ openDatabase(join(base, '.gsd', 'gsd.db'));
86
+ try {
87
+ seedCompleteTask();
88
+
89
+ const result = await handleReopenTask({
90
+ milestoneId: 'M001',
91
+ sliceId: 'S01',
92
+ taskId: 'T01',
93
+ reason: 'Full suite caught NavNodeTest regression — update count assertion to 12.',
94
+ }, base);
95
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
96
+
97
+ const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
98
+ assert.ok(claimed, 'reopen reason should be claimable after handleReopenTask');
99
+ assert.match(claimed!.injectionBlock, /NavNodeTest regression/);
100
+ } finally {
101
+ cleanup(base);
102
+ }
103
+ });
104
+
105
+ test('handleReopenTask: no reason provided leaves nothing to claim', async () => {
106
+ const base = makeTmpBase();
107
+ openDatabase(join(base, '.gsd', 'gsd.db'));
108
+ try {
109
+ seedCompleteTask();
110
+
111
+ await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
112
+
113
+ assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
114
+ } finally {
115
+ cleanup(base);
116
+ }
117
+ });
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Tests: awaitWorkerResume — headless worker pause survives without a resumer.
3
+ *
4
+ * Regression for #1273: a worker `pause` used to call a terminal pauseAuto()
5
+ * that exited the process with BLOCKED (10). On the headless path there is no
6
+ * resumer to send the matching `resume`, so the worker was stranded after its
7
+ * first unit. awaitWorkerResume lets the worker wait for the coordinator to lift
8
+ * the pause and, when no resumer responds, report "timeout" so the caller can
9
+ * degrade to in-process serialization instead of halting forever.
10
+ */
11
+
12
+ import assert from "node:assert/strict";
13
+ import { describe, it } from "node:test";
14
+ import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { tmpdir } from "node:os";
17
+
18
+ import { awaitWorkerResume, sendSignal } from "../session-status-io.ts";
19
+
20
+ function makeBase(): { base: string; cleanup: () => void } {
21
+ const base = mkdtempSync(join(tmpdir(), "gsd-worker-resume-"));
22
+ mkdirSync(join(base, ".gsd", "parallel"), { recursive: true });
23
+ return { base, cleanup: () => rmSync(base, { recursive: true, force: true }) };
24
+ }
25
+
26
+ describe("awaitWorkerResume", () => {
27
+ it("returns 'resume' when the coordinator sends resume", async () => {
28
+ const { base, cleanup } = makeBase();
29
+ try {
30
+ sendSignal(base, "M001", "resume");
31
+ const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
32
+ assert.equal(outcome, "resume");
33
+ } finally {
34
+ cleanup();
35
+ }
36
+ });
37
+
38
+ it("returns 'stop' when the coordinator sends stop", async () => {
39
+ const { base, cleanup } = makeBase();
40
+ try {
41
+ sendSignal(base, "M001", "stop");
42
+ const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
43
+ assert.equal(outcome, "stop");
44
+ } finally {
45
+ cleanup();
46
+ }
47
+ });
48
+
49
+ it("returns 'timeout' when no resumer lifts the pause (headless, #1273)", async () => {
50
+ const { base, cleanup } = makeBase();
51
+ try {
52
+ const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 100, pollMs: 20 });
53
+ assert.equal(outcome, "timeout");
54
+ } finally {
55
+ cleanup();
56
+ }
57
+ });
58
+
59
+ it("ignores a repeated pause and still resumes when resume arrives", async () => {
60
+ const { base, cleanup } = makeBase();
61
+ try {
62
+ // A duplicate pause must not be treated as a resume, nor reset the wait.
63
+ sendSignal(base, "M001", "pause");
64
+ const waiting = awaitWorkerResume(base, "M001", { timeoutMs: 1000, pollMs: 20 });
65
+ // Deliver the resume shortly after the pause is consumed and ignored.
66
+ setTimeout(() => sendSignal(base, "M001", "resume"), 60);
67
+ const outcome = await waiting;
68
+ assert.equal(outcome, "resume");
69
+ } finally {
70
+ cleanup();
71
+ }
72
+ });
73
+ });
@@ -23,6 +23,7 @@ import { flushWorkflowProjections } from "../projection-flush.js";
23
23
  import { writeManifest } from "../workflow-manifest.js";
24
24
  import { appendEvent } from "../workflow-events.js";
25
25
  import { logWarning } from "../workflow-logger.js";
26
+ import { writeReopenReason } from "../reopen-reason.js";
26
27
  import { existsSync, unlinkSync } from "node:fs";
27
28
  import { join, basename, dirname } from "node:path";
28
29
  import { resolveTasksDir, resolveSlicePath, clearPathCache } from "../paths.js";
@@ -125,6 +126,20 @@ export async function handleReopenTask(
125
126
  }
126
127
  clearPathCache();
127
128
 
129
+ // ── Persist the reopen reason for the next execute-task dispatch (#1272) ──
130
+ // Without this, the re-dispatched executor receives the original task plan
131
+ // and verify command with zero signal that it was reopened — it re-runs the
132
+ // (still-green) scoped verify and re-completes the task, never touching the
133
+ // regression the gate actually caught. buildExecuteTaskPrompt claims this
134
+ // artifact and prepends the diagnosis to the next dispatch.
135
+ if (params.reason && params.reason.trim() !== "") {
136
+ try {
137
+ writeReopenReason(basePath, params.milestoneId, params.sliceId, params.taskId, params.reason);
138
+ } catch (reasonErr) {
139
+ logWarning("tool", `reopen-task reason persistence warning: ${(reasonErr as Error).message}`);
140
+ }
141
+ }
142
+
128
143
  // ── Post-mutation hook ───────────────────────────────────────────────────
129
144
  try {
130
145
  await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
@@ -355,14 +355,16 @@ export function resolveGsdBrowserMcpLaunchConfig(
355
355
  }
356
356
 
357
357
  /**
358
- * CLI invocation that starts the gsd-browser session daemon with the same
359
- * session and identity flags as {@link resolveGsdBrowserMcpLaunchConfig}, so
360
- * browser UAT can warm Chrome/CDP before the first MCP navigation.
358
+ * CLI invocation that runs a `daemon <action>` command against the gsd-browser
359
+ * session daemon with the same session and identity flags as
360
+ * {@link resolveGsdBrowserMcpLaunchConfig}, so start (warm-up) and stop
361
+ * (teardown) address the exact same managed daemon.
361
362
  */
362
- export function resolveGsdBrowserDaemonStartInvocation(
363
+ function resolveGsdBrowserDaemonInvocation(
364
+ action: "start" | "stop",
363
365
  projectRoot: string,
364
- env: NodeJS.ProcessEnv = process.env,
365
- options: GsdBrowserMcpLaunchOptions = {},
366
+ env: NodeJS.ProcessEnv,
367
+ options: GsdBrowserMcpLaunchOptions,
366
368
  ): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
367
369
  const launch = resolveGsdBrowserMcpLaunchConfig(projectRoot, env, options);
368
370
  const mcpIndex = launch.args.indexOf("mcp");
@@ -374,8 +376,34 @@ export function resolveGsdBrowserDaemonStartInvocation(
374
376
  const sessionFlags = launch.args.slice(mcpIndex + 1);
375
377
  return {
376
378
  command: launch.command,
377
- args: [...prefix, "daemon", "start", ...sessionFlags],
379
+ args: [...prefix, "daemon", action, ...sessionFlags],
378
380
  cwd: launch.cwd,
379
381
  ...(launch.env ? { env: launch.env } : {}),
380
382
  };
381
383
  }
384
+
385
+ /**
386
+ * CLI invocation that starts the gsd-browser session daemon with the same
387
+ * session and identity flags as {@link resolveGsdBrowserMcpLaunchConfig}, so
388
+ * browser UAT can warm Chrome/CDP before the first MCP navigation.
389
+ */
390
+ export function resolveGsdBrowserDaemonStartInvocation(
391
+ projectRoot: string,
392
+ env: NodeJS.ProcessEnv = process.env,
393
+ options: GsdBrowserMcpLaunchOptions = {},
394
+ ): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
395
+ return resolveGsdBrowserDaemonInvocation("start", projectRoot, env, options);
396
+ }
397
+
398
+ /**
399
+ * CLI invocation that stops the gsd-browser session daemon warmed for browser
400
+ * UAT, so the Chrome process is torn down instead of lingering after the
401
+ * triggering task/session ends.
402
+ */
403
+ export function resolveGsdBrowserDaemonStopInvocation(
404
+ projectRoot: string,
405
+ env: NodeJS.ProcessEnv = process.env,
406
+ options: GsdBrowserMcpLaunchOptions = {},
407
+ ): Pick<GsdBrowserMcpLaunchConfig, "command" | "args" | "cwd" | "env"> {
408
+ return resolveGsdBrowserDaemonInvocation("stop", projectRoot, env, options);
409
+ }