gsd-pi 2.79.0-dev.5c910bb05 → 2.79.0-dev.8f814f3af

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 (84) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +6 -1
  3. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +2 -0
  5. package/dist/resources/extensions/gsd/auto-recovery.js +18 -3
  6. package/dist/resources/extensions/gsd/auto-start.js +3 -2
  7. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +8 -8
  8. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +8 -8
  9. package/dist/resources/extensions/gsd/guided-flow.js +40 -0
  10. package/dist/resources/extensions/gsd/paths.js +5 -1
  11. package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +45 -4
  12. package/dist/resources/extensions/gsd/uok/audit.js +23 -9
  13. package/dist/resources/extensions/gsd/uok/contracts.js +69 -1
  14. package/dist/resources/extensions/gsd/uok/dispatch-envelope.js +3 -0
  15. package/dist/resources/extensions/gsd/uok/loop-adapter.js +48 -33
  16. package/dist/resources/extensions/gsd/uok/timeline.js +125 -0
  17. package/dist/resources/extensions/shared/gsd-phase-state.js +45 -3
  18. package/dist/resources/extensions/shared/interview-ui.js +15 -4
  19. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  20. package/dist/web/standalone/.next/BUILD_ID +1 -1
  21. package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
  22. package/dist/web/standalone/.next/build-manifest.json +2 -2
  23. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  24. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  33. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/index.html +1 -1
  41. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  44. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
  48. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  49. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  50. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  51. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  52. package/package.json +1 -1
  53. package/packages/mcp-server/src/workflow-tools.test.ts +13 -2
  54. package/src/resources/extensions/gsd/auto/phases.ts +6 -1
  55. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -2
  56. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -0
  57. package/src/resources/extensions/gsd/auto-recovery.ts +17 -3
  58. package/src/resources/extensions/gsd/auto-start.ts +3 -2
  59. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +11 -8
  60. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-shouldblock-basepath.test.ts +97 -0
  61. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +8 -4
  62. package/src/resources/extensions/gsd/guided-flow.ts +47 -0
  63. package/src/resources/extensions/gsd/paths.ts +6 -1
  64. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +108 -1
  65. package/src/resources/extensions/gsd/tests/check-auto-start-pending-gate.test.ts +203 -0
  66. package/src/resources/extensions/gsd/tests/check-auto-start-ready-guard.test.ts +148 -0
  67. package/src/resources/extensions/gsd/tests/deep-planning-mode-dispatch.test.ts +42 -0
  68. package/src/resources/extensions/gsd/tests/deep-project-auto-loop.test.ts +63 -2
  69. package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +109 -0
  70. package/src/resources/extensions/gsd/tests/uok-contracts.test.ts +109 -1
  71. package/src/resources/extensions/gsd/tests/uok-loop-adapter-writer.test.ts +98 -0
  72. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +36 -7
  73. package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +47 -4
  74. package/src/resources/extensions/gsd/uok/audit.ts +25 -9
  75. package/src/resources/extensions/gsd/uok/contracts.ts +105 -0
  76. package/src/resources/extensions/gsd/uok/dispatch-envelope.ts +4 -0
  77. package/src/resources/extensions/gsd/uok/loop-adapter.ts +60 -45
  78. package/src/resources/extensions/gsd/uok/timeline.ts +158 -0
  79. package/src/resources/extensions/shared/gsd-phase-state.ts +56 -3
  80. package/src/resources/extensions/shared/interview-ui.ts +18 -5
  81. package/src/resources/extensions/shared/tests/gsd-phase-state.test.ts +43 -1
  82. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +41 -0
  83. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → -O_omrDQQ0SfyCjHy578c}/_buildManifest.js +0 -0
  84. /package/dist/web/standalone/.next/static/{DSZPSz1kgrF8zPIrV_AMD → -O_omrDQQ0SfyCjHy578c}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * GSD Phase State — cross-extension coordination
2
+ * GSD2 Phase State — cross-extension coordination
3
3
  * Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
4
4
  *
5
5
  * Lightweight module-level state that GSD auto-mode writes to and the
@@ -7,28 +7,81 @@
7
7
  * a module variable is sufficient — no file I/O needed.
8
8
  */
9
9
 
10
+ import { buildAuditEnvelope, emitUokAuditEvent } from "../gsd/uok/audit.js";
11
+
10
12
  let _active = false;
11
13
  let _currentPhase: string | null = null;
12
14
 
15
+ export interface GSDPhaseAuditContext {
16
+ basePath: string;
17
+ traceId: string;
18
+ turnId?: string;
19
+ causedBy?: string;
20
+ }
21
+
22
+ let _auditContext: GSDPhaseAuditContext | null = null;
23
+
24
+ function emitPhaseChange(action: string, previousPhase: string | null, nextPhase: string | null): void {
25
+ if (!_auditContext) return;
26
+ emitUokAuditEvent(
27
+ _auditContext.basePath,
28
+ buildAuditEnvelope({
29
+ traceId: _auditContext.traceId,
30
+ turnId: _auditContext.turnId,
31
+ causedBy: _auditContext.causedBy,
32
+ category: "orchestration",
33
+ type: "phase_changed",
34
+ payload: {
35
+ action,
36
+ active: _active,
37
+ previousPhase,
38
+ nextPhase,
39
+ },
40
+ }),
41
+ );
42
+ }
43
+
44
+ export function configureGSDPhaseAudit(context: GSDPhaseAuditContext | null): void {
45
+ _auditContext = context;
46
+ }
47
+
13
48
  /** Mark GSD auto-mode as active. */
14
- export function activateGSD(): void {
49
+ export function activateGSD(context?: GSDPhaseAuditContext): void {
50
+ if (context) _auditContext = context;
51
+ const previousPhase = _currentPhase;
15
52
  _active = true;
53
+ emitPhaseChange("activate", previousPhase, _currentPhase);
16
54
  }
17
55
 
18
56
  /** Mark GSD auto-mode as inactive and clear the current phase. */
19
57
  export function deactivateGSD(): void {
58
+ const previousPhase = _currentPhase;
20
59
  _active = false;
21
60
  _currentPhase = null;
61
+ emitPhaseChange("deactivate", previousPhase, _currentPhase);
62
+ _auditContext = null;
22
63
  }
23
64
 
24
65
  /** Set the currently dispatched GSD phase (e.g. "plan-milestone"). */
25
- export function setCurrentPhase(phase: string): void {
66
+ export function setCurrentPhase(phase: string, context?: GSDPhaseAuditContext): boolean {
67
+ if (context) _auditContext = context;
68
+ if (!_active) {
69
+ process.emitWarning(`Ignoring GSD phase "${phase}" while GSD auto-mode is inactive`, {
70
+ code: "GSD_PHASE_INACTIVE",
71
+ });
72
+ return false;
73
+ }
74
+ const previousPhase = _currentPhase;
26
75
  _currentPhase = phase;
76
+ emitPhaseChange("set", previousPhase, _currentPhase);
77
+ return true;
27
78
  }
28
79
 
29
80
  /** Clear the current phase (unit completed or aborted). */
30
81
  export function clearCurrentPhase(): void {
82
+ const previousPhase = _currentPhase;
31
83
  _currentPhase = null;
84
+ emitPhaseChange("clear", previousPhase, _currentPhase);
32
85
  }
33
86
 
34
87
  /** Returns true if GSD auto-mode is currently active. */
@@ -1,3 +1,4 @@
1
+ // GSD2 — Shared interview round UI widget
1
2
  /**
2
3
  * Shared interview round UI widget.
3
4
  *
@@ -224,12 +225,24 @@ export async function showInterviewRound(
224
225
  let showingExitConfirm = false;
225
226
  let exitCursor = 0; // 0 = keep going (default), 1 = end interview
226
227
  let cachedLines: string[] | undefined;
228
+ let completed = false;
229
+ let removeAbortListener: (() => void) | undefined;
230
+
231
+ function finish(result: RoundResult) {
232
+ if (completed) return;
233
+ completed = true;
234
+ removeAbortListener?.();
235
+ done(result);
236
+ }
227
237
 
228
238
  // External cancellation (e.g. remote channel won the race)
229
239
  if (opts.signal) {
230
- const onAbort = () => done({ endInterview: false, answers: {} });
240
+ const onAbort = () => finish({ endInterview: false, answers: {} });
231
241
  if (opts.signal.aborted) { onAbort(); }
232
- else { opts.signal.addEventListener("abort", onAbort, { once: true }); }
242
+ else {
243
+ opts.signal.addEventListener("abort", onAbort, { once: true });
244
+ removeAbortListener = () => opts.signal?.removeEventListener("abort", onAbort);
245
+ }
233
246
  }
234
247
 
235
248
  // Editor is created once; editorTheme comes from the design system
@@ -312,7 +325,7 @@ export async function showInterviewRound(
312
325
 
313
326
  function submit() {
314
327
  saveEditorToState();
315
- done(buildResult());
328
+ finish(buildResult());
316
329
  }
317
330
 
318
331
  function goNextOrSubmit() {
@@ -355,10 +368,10 @@ export async function showInterviewRound(
355
368
  if (matchesKey(data, Key.up) || matchesKey(data, Key.left)) { exitCursor = 0; refresh(); return; }
356
369
  if (matchesKey(data, Key.down) || matchesKey(data, Key.right)) { exitCursor = 1; refresh(); return; }
357
370
  if (data === "1") { showingExitConfirm = false; refresh(); return; }
358
- if (data === "2") { done({ endInterview: false, answers: {} }); return; }
371
+ if (data === "2") { finish({ endInterview: false, answers: {} }); return; }
359
372
  if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
360
373
  if (exitCursor === 0) { showingExitConfirm = false; refresh(); }
361
- else { done({ endInterview: false, answers: {} }); }
374
+ else { finish({ endInterview: false, answers: {} }); }
362
375
  return;
363
376
  }
364
377
  if (matchesKey(data, Key.escape)) { showingExitConfirm = false; refresh(); return; }
@@ -1,7 +1,13 @@
1
+ // GSD2 Shared Phase State Coordination Tests
2
+
1
3
  import { describe, it, beforeEach } from "node:test";
2
4
  import assert from "node:assert/strict";
5
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join } from "node:path";
3
8
  import {
4
9
  activateGSD,
10
+ configureGSDPhaseAudit,
5
11
  deactivateGSD,
6
12
  setCurrentPhase,
7
13
  clearCurrentPhase,
@@ -25,12 +31,18 @@ describe("gsd-phase-state", () => {
25
31
  it("tracks the current phase when active", () => {
26
32
  activateGSD();
27
33
  assert.equal(getCurrentPhase(), null);
28
- setCurrentPhase("plan-milestone");
34
+ assert.equal(setCurrentPhase("plan-milestone"), true);
29
35
  assert.equal(getCurrentPhase(), "plan-milestone");
30
36
  clearCurrentPhase();
31
37
  assert.equal(getCurrentPhase(), null);
32
38
  });
33
39
 
40
+ it("rejects phase changes while inactive", () => {
41
+ assert.equal(setCurrentPhase("plan-milestone"), false);
42
+ activateGSD();
43
+ assert.equal(getCurrentPhase(), null);
44
+ });
45
+
34
46
  it("returns null phase when inactive even if phase was set", () => {
35
47
  activateGSD();
36
48
  setCurrentPhase("plan-milestone");
@@ -45,4 +57,34 @@ describe("gsd-phase-state", () => {
45
57
  activateGSD();
46
58
  assert.equal(getCurrentPhase(), null);
47
59
  });
60
+
61
+ it("deactivation clears the audit context so later events do not carry stale trace data", () => {
62
+ const basePath = mkdtempSync(join(tmpdir(), "gsd-phase-state-audit-"));
63
+ try {
64
+ activateGSD({ basePath, traceId: "stale-trace", causedBy: "test" });
65
+ setCurrentPhase("plan-milestone");
66
+ deactivateGSD();
67
+
68
+ // Re-activate WITHOUT a context. If deactivate did not clear the
69
+ // stored context, this setCurrentPhase would emit an audit event
70
+ // using "stale-trace".
71
+ activateGSD();
72
+ setCurrentPhase("execute-task");
73
+
74
+ const eventsPath = join(basePath, ".gsd", "audit", "events.jsonl");
75
+ if (existsSync(eventsPath)) {
76
+ const contents = readFileSync(eventsPath, "utf-8");
77
+ assert.equal(
78
+ contents.includes("stale-trace") &&
79
+ contents.split("\n").filter((line) => line.includes("stale-trace") && line.includes("execute-task")).length > 0,
80
+ false,
81
+ "execute-task phase change must not be emitted under the deactivated trace",
82
+ );
83
+ }
84
+ } finally {
85
+ configureGSDPhaseAudit(null);
86
+ deactivateGSD();
87
+ rmSync(basePath, { recursive: true, force: true });
88
+ }
89
+ });
48
90
  });
@@ -139,4 +139,45 @@ describe("interview-ui notes loop regression (#3502)", () => {
139
139
  assert.ok(answer, "answer for q1 should exist");
140
140
  assert.equal(answer.selected, "Web App");
141
141
  });
142
+
143
+ it("ignores abort signals after a submitted answer", async () => {
144
+ const controller = new AbortController();
145
+ const doneCalls: RoundResult[] = [];
146
+ let widget: { handleInput(input: string): void } | undefined;
147
+
148
+ const resultPromise = showInterviewRound(questions, { signal: controller.signal }, {
149
+ ui: {
150
+ custom: (factory: any) => new Promise<RoundResult>((resolve) => {
151
+ const mockTui = { requestRender: () => {} };
152
+ const mockTheme = {
153
+ fg: (_c: string, t: string) => t,
154
+ bold: (t: string) => t,
155
+ dim: (t: string) => t,
156
+ italic: (t: string) => t,
157
+ strikethrough: (t: string) => t,
158
+ accent: (t: string) => t,
159
+ success: (t: string) => t,
160
+ warning: (t: string) => t,
161
+ error: (t: string) => t,
162
+ info: (t: string) => t,
163
+ muted: (t: string) => t,
164
+ dimmed: (t: string) => t,
165
+ };
166
+ widget = factory(mockTui, mockTheme, {}, (result: RoundResult) => {
167
+ doneCalls.push(result);
168
+ resolve(result);
169
+ });
170
+ }),
171
+ },
172
+ } as any);
173
+
174
+ assert.ok(widget, "widget should be created synchronously");
175
+ widget.handleInput(ENTER);
176
+ widget.handleInput(ENTER);
177
+ controller.abort();
178
+
179
+ const result = await resultPromise;
180
+ assert.equal(doneCalls.length, 1, "abort after submit must not emit a second empty result");
181
+ assert.deepEqual(result.answers.q1, { selected: "Web App", notes: "" });
182
+ });
142
183
  });