gsd-pi 2.78.1-dev.b6a389b66 → 2.78.1-dev.d8826a445

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 (155) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/gsd/auto/phases.js +7 -2
  3. package/dist/resources/extensions/gsd/auto/session.js +3 -0
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +3 -2
  5. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -1
  6. package/dist/resources/extensions/gsd/auto-worktree.js +185 -40
  7. package/dist/resources/extensions/gsd/auto.js +62 -1
  8. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +1 -1
  9. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +17 -16
  10. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +67 -55
  11. package/dist/resources/extensions/gsd/db-writer.js +96 -16
  12. package/dist/resources/extensions/gsd/delegation-policy.js +155 -0
  13. package/dist/resources/extensions/gsd/gsd-db.js +194 -0
  14. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  15. package/dist/resources/extensions/gsd/guided-flow.js +117 -25
  16. package/dist/resources/extensions/gsd/metrics.js +287 -1
  17. package/dist/resources/extensions/gsd/paths.js +79 -8
  18. package/dist/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -3
  20. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  23. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  24. package/dist/resources/extensions/gsd/templates/project.md +10 -0
  25. package/dist/resources/extensions/gsd/workflow-mcp.js +2 -2
  26. package/dist/resources/extensions/gsd/workspace.js +59 -0
  27. package/dist/resources/extensions/gsd/worktree-resolver.js +15 -2
  28. package/dist/resources/extensions/gsd/write-intercept.js +3 -3
  29. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  30. package/dist/web/standalone/.next/BUILD_ID +1 -1
  31. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  32. package/dist/web/standalone/.next/build-manifest.json +2 -2
  33. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  34. package/dist/web/standalone/.next/required-server-files.json +1 -1
  35. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  36. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  37. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  39. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  43. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  44. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  45. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/index.html +1 -1
  52. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  59. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  61. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  62. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  63. package/dist/web/standalone/server.js +1 -1
  64. package/package.json +1 -1
  65. package/packages/mcp-server/README.md +2 -11
  66. package/packages/mcp-server/dist/remote-questions.d.ts +27 -0
  67. package/packages/mcp-server/dist/remote-questions.d.ts.map +1 -1
  68. package/packages/mcp-server/dist/remote-questions.js +28 -0
  69. package/packages/mcp-server/dist/remote-questions.js.map +1 -1
  70. package/packages/mcp-server/dist/server.d.ts +28 -0
  71. package/packages/mcp-server/dist/server.d.ts.map +1 -1
  72. package/packages/mcp-server/dist/server.js +94 -4
  73. package/packages/mcp-server/dist/server.js.map +1 -1
  74. package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
  75. package/packages/mcp-server/src/mcp-server.test.ts +226 -0
  76. package/packages/mcp-server/src/remote-questions.test.ts +103 -0
  77. package/packages/mcp-server/src/remote-questions.ts +35 -0
  78. package/packages/mcp-server/src/server.ts +129 -6
  79. package/packages/mcp-server/src/workflow-tools.ts +1 -1
  80. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  81. package/src/resources/extensions/gsd/auto/phases.ts +8 -2
  82. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  83. package/src/resources/extensions/gsd/auto-dispatch.ts +10 -2
  84. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -1
  85. package/src/resources/extensions/gsd/auto-worktree.ts +225 -47
  86. package/src/resources/extensions/gsd/auto.ts +79 -1
  87. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +1 -1
  88. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +17 -17
  89. package/src/resources/extensions/gsd/bootstrap/tests/write-gate-basepath.test.ts +103 -0
  90. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +80 -55
  91. package/src/resources/extensions/gsd/db-writer.ts +113 -17
  92. package/src/resources/extensions/gsd/delegation-policy.ts +197 -0
  93. package/src/resources/extensions/gsd/gsd-db.ts +184 -0
  94. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  95. package/src/resources/extensions/gsd/guided-flow.ts +154 -25
  96. package/src/resources/extensions/gsd/metrics.ts +321 -1
  97. package/src/resources/extensions/gsd/paths.ts +67 -8
  98. package/src/resources/extensions/gsd/prompts/complete-slice.md +4 -4
  99. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -3
  100. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +8 -1
  101. package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +22 -7
  102. package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +6 -2
  103. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -1
  104. package/src/resources/extensions/gsd/templates/project.md +10 -0
  105. package/src/resources/extensions/gsd/tests/auto-discuss-milestone-deadlock-4973.test.ts +14 -14
  106. package/src/resources/extensions/gsd/tests/auto-session-scope.test.ts +331 -0
  107. package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +176 -0
  108. package/src/resources/extensions/gsd/tests/db-writer-path-containment.test.ts +152 -0
  109. package/src/resources/extensions/gsd/tests/db-writer-root-artifact.test.ts +221 -0
  110. package/src/resources/extensions/gsd/tests/db-writer-scope.test.ts +230 -0
  111. package/src/resources/extensions/gsd/tests/delegation-policy.test.ts +151 -0
  112. package/src/resources/extensions/gsd/tests/dispatch-backgroundable-annotation.test.ts +55 -0
  113. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +3 -23
  114. package/src/resources/extensions/gsd/tests/gate-1b-orphan-discrimination.test.ts +193 -0
  115. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound-corrections.test.ts +246 -0
  116. package/src/resources/extensions/gsd/tests/gate-1b-recovery-bound.test.ts +218 -0
  117. package/src/resources/extensions/gsd/tests/gsd-db-failed-open-restore.test.ts +117 -0
  118. package/src/resources/extensions/gsd/tests/gsd-db-workspace-scope.test.ts +226 -0
  119. package/src/resources/extensions/gsd/tests/gsd-root-canonical.test.ts +66 -0
  120. package/src/resources/extensions/gsd/tests/gsd-root-home-guard.test.ts +68 -5
  121. package/src/resources/extensions/gsd/tests/guided-flow-prompt-consolidation.test.ts +4 -4
  122. package/src/resources/extensions/gsd/tests/integration/workspace-collapse-integration.test.ts +371 -0
  123. package/src/resources/extensions/gsd/tests/metrics-atomic-merge.test.ts +222 -0
  124. package/src/resources/extensions/gsd/tests/metrics-lock-hardening.test.ts +400 -0
  125. package/src/resources/extensions/gsd/tests/metrics-lock-not-acquired.test.ts +141 -0
  126. package/src/resources/extensions/gsd/tests/metrics-lock-retry-sleep.test.ts +287 -0
  127. package/src/resources/extensions/gsd/tests/metrics-prune-cache-invalidation.test.ts +149 -0
  128. package/src/resources/extensions/gsd/tests/metrics-scope.test.ts +378 -0
  129. package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +329 -0
  130. package/src/resources/extensions/gsd/tests/path-cache-decoupled.test.ts +209 -0
  131. package/src/resources/extensions/gsd/tests/path-normalization-unified.test.ts +175 -0
  132. package/src/resources/extensions/gsd/tests/paths-cache.test.ts +170 -0
  133. package/src/resources/extensions/gsd/tests/pending-autostart-scope.test.ts +120 -0
  134. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +150 -7
  135. package/src/resources/extensions/gsd/tests/ready-phrase-no-files-4573.test.ts +74 -0
  136. package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +28 -16
  137. package/src/resources/extensions/gsd/tests/resume-missing-worktree-warning.test.ts +209 -0
  138. package/src/resources/extensions/gsd/tests/sync-layer-scope.test.ts +453 -0
  139. package/src/resources/extensions/gsd/tests/teardown-chdir-failure-clears-registry.test.ts +162 -0
  140. package/src/resources/extensions/gsd/tests/teardown-cleanup-parity.test.ts +102 -0
  141. package/src/resources/extensions/gsd/tests/teardown-failure-clears-registry.test.ts +186 -0
  142. package/src/resources/extensions/gsd/tests/tool-invocation-error-loop-break.test.ts +1 -1
  143. package/src/resources/extensions/gsd/tests/validator-scope-parity.test.ts +239 -0
  144. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +2 -2
  145. package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +9 -15
  146. package/src/resources/extensions/gsd/tests/workspace.test.ts +190 -0
  147. package/src/resources/extensions/gsd/tests/write-gate-predicates.test.ts +35 -35
  148. package/src/resources/extensions/gsd/tests/write-gate.test.ts +67 -52
  149. package/src/resources/extensions/gsd/tests/write-intercept.test.ts +1 -1
  150. package/src/resources/extensions/gsd/workflow-mcp.ts +2 -2
  151. package/src/resources/extensions/gsd/workspace.ts +95 -0
  152. package/src/resources/extensions/gsd/worktree-resolver.ts +16 -2
  153. package/src/resources/extensions/gsd/write-intercept.ts +3 -3
  154. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_buildManifest.js +0 -0
  155. /package/dist/web/standalone/.next/static/{HahrZrc_Xn4wumj0O1Ydp → AT5qi39nKXkdmQIOIoh0f}/_ssgManifest.js +0 -0
@@ -21,9 +21,13 @@ Before your first action, print this banner verbatim in chat:
21
21
  ## Pre-flight
22
22
 
23
23
  1. Read `.gsd/PROJECT.md` end-to-end. If it does not exist, STOP and emit: `"PROJECT.md missing — run discuss-project first."`
24
- 2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence.
24
+ 2. Extract: Core Value, Anti-goals, Constraints, Milestone Sequence, and the project shape verdict — read the `## Project Shape` section and look for `**Complexity:**` (verdict is either `simple` or `complex`; default to `complex` if the section is missing or unclear).
25
25
  3. Check for existing `.gsd/REQUIREMENTS.md` — if present, this is a refinement pass, not a fresh write. Read existing requirements and treat them as the working set.
26
26
 
27
+ **Shape-dependent cadence:**
28
+ - **`simple`** — favor a single fast pass: extract requirements directly from PROJECT.md, ask 1–2 plain-text clarifying questions only if a class or status assignment is genuinely ambiguous, then write REQUIREMENTS.md.
29
+ - **`complex`** — full multi-round questioning with structured 3–4-option questions where alternatives matter.
30
+
27
31
  ---
28
32
 
29
33
  ## Interview Protocol
@@ -51,7 +55,7 @@ Ask **1–3 questions per round**. Each round targets one dimension:
51
55
 
52
56
  **Never fabricate or simulate user input.** Wait for actual responses.
53
57
 
54
- **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
58
+ **If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions`. Every question object MUST include a stable lowercase `id`. For class assignments, present the allowed classes as multi-select options. For status, present the four statuses as exclusive options. In **`complex`** mode, any free-form question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option grounded in the investigation above. The class-assignment and status questions are exempt — they have fixed enumerations. Ask 1–3 questions per call. Wait for each tool result before asking the next round.
55
59
 
56
60
  **If `{{structuredQuestionsAvailable}}` is `false`:** ask in plain text. Keep each round to 1–3 questions.
57
61
 
@@ -8,6 +8,13 @@ Your goal is **not** to center the discussion on tech stack trivia, naming conve
8
8
 
9
9
  ## Interview Protocol
10
10
 
11
+ ### Read project shape
12
+
13
+ Before your first question round, read `.gsd/PROJECT.md` and look for `## Project Shape` → `**Complexity:**`. The verdict is either **`simple`** or **`complex`** (default to `complex` if the section is missing or unclear).
14
+
15
+ - **`simple`** — favor 1–2 plain-text rounds, write the slice context fast. Skip parallel-research investigation.
16
+ - **`complex`** — full investigation with structured 3–4-option questions.
17
+
11
18
  ### Before your first question round
12
19
 
13
20
  Do a lightweight targeted investigation so your questions are grounded in reality:
@@ -24,7 +31,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
24
31
 
25
32
  **Never fabricate or simulate user input.** Never generate fake transcript markers like `[User]`, `[Human]`, or `User:`. Ask one question round, then wait for the user's actual response before continuing.
26
33
 
27
- **If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
34
+ **If `{{structuredQuestionsAvailable}}` is `true`:** Ask **1–3 questions per round** using `ask_user_questions`. In **`complex`** mode, each multi-choice question MUST present **3 or 4 concrete, researched options** plus a final **"Other — let me discuss"** option; options must be grounded in the investigation above (codebase signals, library docs, prior `.gsd/` artifacts), not generic placeholders. In **`simple`** mode, 2 options is fine. Binary wrap-up gates are exempt from the 3-or-4 rule. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.**
28
35
  **If `{{structuredQuestionsAvailable}}` is `false`:** Ask **1–3 questions per round** in plain text. Number them and wait for the user's response before asking the next round.
29
36
  Keep each question focused on one of:
30
37
  - **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
@@ -11,6 +11,16 @@
11
11
 
12
12
  {{theOneThingThatMustWorkEvenIfEverythingElseIsCut}}
13
13
 
14
+ ## Project Shape
15
+
16
+ <!-- Drives questioning depth in downstream stages. `simple` → short plain-text
17
+ rounds, fast PROJECT/CONTEXT/REQUIREMENTS writes. `complex` → researched
18
+ 3–4-option questions with an "Other — let me discuss" hatch.
19
+ Default to `complex` when uncertain. -->
20
+
21
+ - **Complexity:** {{simple | complex}}
22
+ - **Why:** {{one-line rationale citing the signals that decided it}}
23
+
14
24
  ## Current State
15
25
 
16
26
  {{whatHasBeenBuiltSoFar — what works, what exists, what's deployed}}
@@ -35,7 +35,7 @@ import { _setAutoActiveForTest } from '../auto.ts';
35
35
  // Reset all relevant state before and after each test.
36
36
  function resetState(): void {
37
37
  _setAutoActiveForTest(false);
38
- clearDiscussionFlowState();
38
+ clearDiscussionFlowState(process.cwd());
39
39
  }
40
40
 
41
41
  describe('auto-discuss-milestone-deadlock-4973', () => {
@@ -51,7 +51,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
51
51
  _setAutoActiveForTest(true);
52
52
 
53
53
  // Before mark: blocked
54
- const snapshotBefore = loadWriteGateSnapshot();
54
+ const snapshotBefore = loadWriteGateSnapshot(process.cwd());
55
55
  const beforeResult = shouldBlockContextArtifactSaveInSnapshot(
56
56
  snapshotBefore,
57
57
  'CONTEXT',
@@ -61,10 +61,10 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
61
61
  assert.strictEqual(beforeResult.block, true, 'should block before markDepthVerified');
62
62
 
63
63
  // Simulate what the dispatch rule now does in auto-mode
64
- markDepthVerified('M001');
64
+ markDepthVerified('M001', process.cwd());
65
65
 
66
66
  // After mark: unblocked
67
- const snapshotAfter = loadWriteGateSnapshot();
67
+ const snapshotAfter = loadWriteGateSnapshot(process.cwd());
68
68
  const afterResult = shouldBlockContextArtifactSaveInSnapshot(
69
69
  snapshotAfter,
70
70
  'CONTEXT',
@@ -87,7 +87,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
87
87
  assert.strictEqual(beforeResult.block, true, 'write should be blocked before markDepthVerified');
88
88
 
89
89
  // Simulate dispatch rule auto-mark
90
- markDepthVerified('M001');
90
+ markDepthVerified('M001', process.cwd());
91
91
 
92
92
  // After mark: unblocked
93
93
  const afterResult = shouldBlockContextWrite('write', contextPath, 'M001');
@@ -109,8 +109,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
109
109
  // the dispatch-site call site is safe regardless of prior session state.
110
110
  test('Test 3: session_switch ordering — clearDiscussionFlowState clears mark; dispatch-site call re-establishes it', () => {
111
111
  // Simulate a mark from a prior session
112
- markDepthVerified('M001');
113
- let snapshot = loadWriteGateSnapshot();
112
+ markDepthVerified('M001', process.cwd());
113
+ let snapshot = loadWriteGateSnapshot(process.cwd());
114
114
  assert.strictEqual(
115
115
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
116
116
  true,
@@ -119,8 +119,8 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
119
119
 
120
120
  // session_switch fires clearDiscussionFlowState() — this is exactly what
121
121
  // register-hooks.ts:106 does
122
- clearDiscussionFlowState();
123
- snapshot = loadWriteGateSnapshot();
122
+ clearDiscussionFlowState(process.cwd());
123
+ snapshot = loadWriteGateSnapshot(process.cwd());
124
124
  assert.strictEqual(
125
125
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
126
126
  false,
@@ -130,9 +130,9 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
130
130
  // Now the dispatch rule fires (after session_switch cleared state)
131
131
  // and re-establishes the mark for the new session
132
132
  _setAutoActiveForTest(true);
133
- markDepthVerified('M001'); // this is what the dispatch rule does
133
+ markDepthVerified('M001', process.cwd()); // this is what the dispatch rule does
134
134
 
135
- snapshot = loadWriteGateSnapshot();
135
+ snapshot = loadWriteGateSnapshot(process.cwd());
136
136
  assert.strictEqual(
137
137
  isMilestoneDepthVerifiedInSnapshot(snapshot, 'M001'),
138
138
  true,
@@ -158,7 +158,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
158
158
 
159
159
  // CONTEXT artifact save is still blocked
160
160
  const snapshotResult = shouldBlockContextArtifactSaveInSnapshot(
161
- loadWriteGateSnapshot(),
161
+ loadWriteGateSnapshot(process.cwd()),
162
162
  'CONTEXT',
163
163
  'M002',
164
164
  null,
@@ -238,7 +238,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
238
238
  );
239
239
 
240
240
  // ── Deep auto-mode case: the user-facing approval gate must stay closed ──
241
- clearDiscussionFlowState();
241
+ clearDiscussionFlowState(process.cwd());
242
242
  if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
243
243
  _setAutoActiveForTest(true);
244
244
  const deepCtx = {
@@ -264,7 +264,7 @@ describe('auto-discuss-milestone-deadlock-4973', () => {
264
264
  // ── Interactive case: the rule must NOT call markDepthVerified ──
265
265
  // clearDiscussionFlowState() only deletes the snapshot at process.cwd(),
266
266
  // so we must explicitly remove the snapshot under our tempBase too.
267
- clearDiscussionFlowState();
267
+ clearDiscussionFlowState(process.cwd());
268
268
  if (existsSync(snapshotFile)) unlinkSync(snapshotFile);
269
269
  _setAutoActiveForTest(false);
270
270
  snap = loadWriteGateSnapshot(tempBase);
@@ -0,0 +1,331 @@
1
+ // GSD-2 + Tests for MilestoneScope threading through AutoSession state (C2)
2
+ //
3
+ // Strategy: construct AutoSession directly + call createWorkspace/scopeMilestone
4
+ // to mirror the rebuildScope() helper in auto.ts — avoids importing the full
5
+ // auto.ts module (too many .js resolved imports).
6
+
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { mkdtempSync, mkdirSync, rmSync, realpathSync, existsSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+
13
+ import { AutoSession } from "../auto/session.ts";
14
+ import { createWorkspace, scopeMilestone } from "../workspace.ts";
15
+ import type { MilestoneScope } from "../workspace.ts";
16
+
17
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
18
+
19
+ function makeProjectDir(): string {
20
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-scope-test-")));
21
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ function makeWorktreeDir(projectDir: string, milestoneId: string): string {
26
+ const wt = join(projectDir, ".gsd", "worktrees", milestoneId);
27
+ mkdirSync(wt, { recursive: true });
28
+ return wt;
29
+ }
30
+
31
+ /**
32
+ * Mirror the rebuildScope() helper from auto.ts — computes s.scope from the
33
+ * same inputs so tests can verify the behaviour without importing auto.ts.
34
+ */
35
+ function applyRebuildScope(
36
+ s: AutoSession,
37
+ rawPath: string,
38
+ milestoneId: string | null,
39
+ ): void {
40
+ if (!milestoneId) {
41
+ s.scope = null;
42
+ return;
43
+ }
44
+ try {
45
+ const workspace = createWorkspace(rawPath);
46
+ s.scope = scopeMilestone(workspace, milestoneId);
47
+ } catch {
48
+ s.scope = null;
49
+ }
50
+ }
51
+
52
+ // ─── Tests ───────────────────────────────────────────────────────────────────
53
+
54
+ describe("AutoSession.scope — project mode (basePath equals originalBasePath)", () => {
55
+ let s: AutoSession;
56
+ let projectDir: string;
57
+
58
+ beforeEach(() => {
59
+ projectDir = makeProjectDir();
60
+ s = new AutoSession();
61
+ });
62
+
63
+ afterEach(() => {
64
+ rmSync(projectDir, { recursive: true, force: true });
65
+ });
66
+
67
+ test("scope is null when milestoneId is null", () => {
68
+ s.basePath = projectDir;
69
+ s.originalBasePath = projectDir;
70
+ s.currentMilestoneId = null;
71
+
72
+ applyRebuildScope(s, projectDir, null);
73
+
74
+ assert.equal(s.scope, null);
75
+ });
76
+
77
+ test("scope mode is 'project' when basePath equals originalBasePath", () => {
78
+ const mid = "M001";
79
+ s.basePath = projectDir;
80
+ s.originalBasePath = projectDir;
81
+ s.currentMilestoneId = mid;
82
+
83
+ applyRebuildScope(s, projectDir, mid);
84
+
85
+ assert.ok(s.scope, "scope should be set");
86
+ assert.equal(s.scope.workspace.mode, "project");
87
+ assert.equal(s.scope.milestoneId, mid);
88
+ });
89
+
90
+ test("scope projectRoot matches realpath of projectDir", () => {
91
+ const mid = "M001";
92
+ s.basePath = projectDir;
93
+ s.originalBasePath = projectDir;
94
+ s.currentMilestoneId = mid;
95
+
96
+ applyRebuildScope(s, projectDir, mid);
97
+
98
+ assert.ok(s.scope, "scope should be set");
99
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
100
+ });
101
+
102
+ test("scope worktreeRoot is null in project mode", () => {
103
+ const mid = "M001";
104
+ s.basePath = projectDir;
105
+ s.originalBasePath = projectDir;
106
+ s.currentMilestoneId = mid;
107
+
108
+ applyRebuildScope(s, projectDir, mid);
109
+
110
+ assert.ok(s.scope, "scope should be set");
111
+ assert.equal(s.scope.workspace.worktreeRoot, null);
112
+ });
113
+
114
+ test("scope path methods resolve under the .gsd directory", () => {
115
+ const mid = "M002";
116
+ s.basePath = projectDir;
117
+ s.originalBasePath = projectDir;
118
+ s.currentMilestoneId = mid;
119
+
120
+ applyRebuildScope(s, projectDir, mid);
121
+
122
+ assert.ok(s.scope, "scope should be set");
123
+ const gsd = join(projectDir, ".gsd");
124
+ assert.equal(s.scope.contextFile(), join(gsd, "milestones", mid, `${mid}-CONTEXT.md`));
125
+ assert.equal(s.scope.roadmapFile(), join(gsd, "milestones", mid, `${mid}-ROADMAP.md`));
126
+ assert.equal(s.scope.stateFile(), join(gsd, "STATE.md"));
127
+ });
128
+ });
129
+
130
+ describe("AutoSession.scope — worktree mode (basePath differs from originalBasePath)", () => {
131
+ let s: AutoSession;
132
+ let projectDir: string;
133
+ let worktreeDir: string;
134
+
135
+ beforeEach(() => {
136
+ projectDir = makeProjectDir();
137
+ worktreeDir = makeWorktreeDir(projectDir, "M001");
138
+ s = new AutoSession();
139
+ });
140
+
141
+ afterEach(() => {
142
+ rmSync(projectDir, { recursive: true, force: true });
143
+ });
144
+
145
+ test("scope mode is 'worktree' when basePath is the worktree path", () => {
146
+ const mid = "M001";
147
+ s.basePath = worktreeDir;
148
+ s.originalBasePath = projectDir;
149
+ s.currentMilestoneId = mid;
150
+
151
+ applyRebuildScope(s, worktreeDir, mid);
152
+
153
+ assert.ok(s.scope, "scope should be set");
154
+ assert.equal(s.scope.workspace.mode, "worktree");
155
+ });
156
+
157
+ test("scope worktreeRoot matches realpath of worktreeDir", () => {
158
+ const mid = "M001";
159
+ s.basePath = worktreeDir;
160
+ s.originalBasePath = projectDir;
161
+ s.currentMilestoneId = mid;
162
+
163
+ applyRebuildScope(s, worktreeDir, mid);
164
+
165
+ assert.ok(s.scope, "scope should be set");
166
+ assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
167
+ });
168
+
169
+ test("scope projectRoot resolves to project root (not worktree)", () => {
170
+ const mid = "M001";
171
+ s.basePath = worktreeDir;
172
+ s.originalBasePath = projectDir;
173
+ s.currentMilestoneId = mid;
174
+
175
+ applyRebuildScope(s, worktreeDir, mid);
176
+
177
+ assert.ok(s.scope, "scope should be set");
178
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
179
+ });
180
+
181
+ test("scope milestoneId matches the milestone being tracked", () => {
182
+ const mid = "M001";
183
+ s.basePath = worktreeDir;
184
+ s.originalBasePath = projectDir;
185
+ s.currentMilestoneId = mid;
186
+
187
+ applyRebuildScope(s, worktreeDir, mid);
188
+
189
+ assert.ok(s.scope, "scope should be set");
190
+ assert.equal(s.scope.milestoneId, mid);
191
+ });
192
+ });
193
+
194
+ describe("AutoSession.scope — milestoneId change rebuilds scope", () => {
195
+ let s: AutoSession;
196
+ let projectDir: string;
197
+
198
+ beforeEach(() => {
199
+ projectDir = makeProjectDir();
200
+ s = new AutoSession();
201
+ });
202
+
203
+ afterEach(() => {
204
+ rmSync(projectDir, { recursive: true, force: true });
205
+ });
206
+
207
+ test("scope reflects the new milestoneId after rebuild", () => {
208
+ s.basePath = projectDir;
209
+ s.originalBasePath = projectDir;
210
+ s.currentMilestoneId = "M001";
211
+ applyRebuildScope(s, projectDir, "M001");
212
+
213
+ assert.ok(s.scope, "initial scope should be set");
214
+ assert.equal(s.scope.milestoneId, "M001");
215
+
216
+ // Simulate milestone transition mid-session
217
+ s.currentMilestoneId = "M002";
218
+ applyRebuildScope(s, projectDir, "M002");
219
+
220
+ assert.ok(s.scope, "scope should be set after transition");
221
+ assert.equal(s.scope.milestoneId, "M002");
222
+ });
223
+
224
+ test("scope contextFile changes when milestoneId changes", () => {
225
+ s.basePath = projectDir;
226
+ s.originalBasePath = projectDir;
227
+
228
+ s.currentMilestoneId = "M001";
229
+ applyRebuildScope(s, projectDir, "M001");
230
+ const ctxM001 = s.scope?.contextFile();
231
+
232
+ s.currentMilestoneId = "M002";
233
+ applyRebuildScope(s, projectDir, "M002");
234
+ const ctxM002 = s.scope?.contextFile();
235
+
236
+ assert.ok(ctxM001, "M001 contextFile should be set");
237
+ assert.ok(ctxM002, "M002 contextFile should be set");
238
+ assert.notEqual(ctxM001, ctxM002, "contextFile must differ between milestone IDs");
239
+ assert.ok(ctxM001.includes("M001"), "M001 path should contain M001");
240
+ assert.ok(ctxM002.includes("M002"), "M002 path should contain M002");
241
+ });
242
+ });
243
+
244
+ describe("AutoSession.scope — resume from persisted state", () => {
245
+ let s: AutoSession;
246
+ let projectDir: string;
247
+ let worktreeDir: string;
248
+
249
+ beforeEach(() => {
250
+ projectDir = makeProjectDir();
251
+ worktreeDir = makeWorktreeDir(projectDir, "M003");
252
+ s = new AutoSession();
253
+ });
254
+
255
+ afterEach(() => {
256
+ rmSync(projectDir, { recursive: true, force: true });
257
+ });
258
+
259
+ test("resume without worktree: scope mode is project, projectRoot is base", () => {
260
+ // Mirror the paused-session resume path:
261
+ // s.currentMilestoneId = meta.milestoneId
262
+ // s.originalBasePath = meta.originalBasePath || base
263
+ // rawPath = originalBasePath (no worktreePath present)
264
+ const mid = "M003";
265
+ s.currentMilestoneId = mid;
266
+ s.originalBasePath = projectDir;
267
+ s.basePath = projectDir;
268
+
269
+ applyRebuildScope(s, s.originalBasePath, s.currentMilestoneId);
270
+
271
+ assert.ok(s.scope, "scope should be reconstructed");
272
+ assert.equal(s.scope.milestoneId, mid);
273
+ assert.equal(s.scope.workspace.mode, "project");
274
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
275
+ assert.equal(s.scope.workspace.worktreeRoot, null);
276
+ });
277
+
278
+ test("resume with valid worktree path: scope mode is worktree", () => {
279
+ // Mirror the paused-session resume path where worktreePath exists on disk:
280
+ // rawPath = worktreePath (existsSync true)
281
+ const mid = "M003";
282
+ s.currentMilestoneId = mid;
283
+ s.originalBasePath = projectDir;
284
+ s.basePath = worktreeDir;
285
+
286
+ assert.ok(existsSync(worktreeDir), "worktreeDir must exist for this test");
287
+
288
+ applyRebuildScope(s, worktreeDir, s.currentMilestoneId);
289
+
290
+ assert.ok(s.scope, "scope should be reconstructed");
291
+ assert.equal(s.scope.milestoneId, mid);
292
+ assert.equal(s.scope.workspace.mode, "worktree");
293
+ assert.equal(s.scope.workspace.projectRoot, realpathSync(projectDir));
294
+ assert.equal(s.scope.workspace.worktreeRoot, realpathSync(worktreeDir));
295
+ });
296
+
297
+ test("scope is consistent with direct createWorkspace + scopeMilestone for same inputs", () => {
298
+ const mid = "M003";
299
+ s.currentMilestoneId = mid;
300
+ s.originalBasePath = projectDir;
301
+ s.basePath = projectDir;
302
+
303
+ applyRebuildScope(s, projectDir, mid);
304
+ assert.ok(s.scope, "scope should be set");
305
+
306
+ // Build expected scope via lower-level API to verify equivalence
307
+ const ws = createWorkspace(projectDir);
308
+ const expected = scopeMilestone(ws, mid);
309
+
310
+ assert.equal(s.scope.milestoneId, expected.milestoneId);
311
+ assert.equal(s.scope.contextFile(), expected.contextFile());
312
+ assert.equal(s.scope.roadmapFile(), expected.roadmapFile());
313
+ assert.equal(s.scope.stateFile(), expected.stateFile());
314
+ assert.equal(s.scope.dbPath(), expected.dbPath());
315
+ assert.equal(s.scope.milestoneDir(), expected.milestoneDir());
316
+ assert.equal(s.scope.metaJson(), expected.metaJson());
317
+ });
318
+
319
+ test("reset() clears scope", () => {
320
+ const mid = "M003";
321
+ s.basePath = projectDir;
322
+ s.originalBasePath = projectDir;
323
+ s.currentMilestoneId = mid;
324
+
325
+ applyRebuildScope(s, projectDir, mid);
326
+ assert.ok(s.scope, "scope should be set before reset");
327
+
328
+ s.reset();
329
+ assert.equal(s.scope, null, "scope must be null after reset()");
330
+ });
331
+ });
@@ -0,0 +1,176 @@
1
+ // GSD-2 + Unit tests for the workspace registry that replaced the originalBase singleton
2
+
3
+ import { describe, test, beforeEach } from "node:test";
4
+ import assert from "node:assert/strict";
5
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { execFileSync } from "node:child_process";
9
+
10
+ import {
11
+ getAutoWorktreeOriginalBase,
12
+ getActiveAutoWorktreeContext,
13
+ _resetAutoWorktreeOriginalBaseForTests,
14
+ createAutoWorktree,
15
+ enterAutoWorktree,
16
+ teardownAutoWorktree,
17
+ } from "../auto-worktree.ts";
18
+
19
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
20
+
21
+ // Safe: all inputs below are hardcoded test strings, not user input.
22
+ function git(subArgs: string[], cwd: string): void {
23
+ execFileSync("git", subArgs, { cwd, stdio: ["ignore", "pipe", "pipe"] });
24
+ }
25
+
26
+ function createTempRepo(t: { after: (fn: () => void) => void }): string {
27
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "awreg-test-")));
28
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
29
+ git(["init"], dir);
30
+ git(["config", "user.email", "test@test.com"], dir);
31
+ git(["config", "user.name", "Test"], dir);
32
+ writeFileSync(join(dir, "README.md"), "# test\n");
33
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
34
+ git(["add", "."], dir);
35
+ git(["commit", "-m", "init"], dir);
36
+ git(["branch", "-M", "main"], dir);
37
+ return dir;
38
+ }
39
+
40
+ // ─── Tests ───────────────────────────────────────────────────────────────────
41
+
42
+ describe("auto-worktree workspace registry", () => {
43
+ const savedCwd = process.cwd();
44
+
45
+ beforeEach(() => {
46
+ _resetAutoWorktreeOriginalBaseForTests();
47
+ process.chdir(savedCwd);
48
+ });
49
+
50
+ test("getAutoWorktreeOriginalBase() is null at baseline", () => {
51
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
52
+ });
53
+
54
+ test("getActiveAutoWorktreeContext() is null at baseline", () => {
55
+ assert.strictEqual(getActiveAutoWorktreeContext(), null);
56
+ });
57
+
58
+ test("_resetAutoWorktreeOriginalBaseForTests() clears the registry — idempotent", () => {
59
+ _resetAutoWorktreeOriginalBaseForTests();
60
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
61
+ _resetAutoWorktreeOriginalBaseForTests();
62
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null);
63
+ });
64
+
65
+ test("behavioral equivalence: createAutoWorktree populates registry; teardown clears it", (t) => {
66
+ const tempDir = createTempRepo(t);
67
+ const msDir = join(tempDir, ".gsd", "milestones", "M001");
68
+ mkdirSync(msDir, { recursive: true });
69
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M001 Context\n");
70
+ git(["add", "."], tempDir);
71
+ git(["commit", "-m", "add milestone"], tempDir);
72
+
73
+ // Before entering: registry must be empty
74
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
75
+ "originalBase is null before entering worktree");
76
+
77
+ createAutoWorktree(tempDir, "M001");
78
+
79
+ // After enter: getAutoWorktreeOriginalBase must equal tempDir
80
+ assert.strictEqual(
81
+ getAutoWorktreeOriginalBase(),
82
+ tempDir,
83
+ "getAutoWorktreeOriginalBase() returns projectRoot after createAutoWorktree",
84
+ );
85
+
86
+ // getActiveAutoWorktreeContext must return the correct shape
87
+ const ctx = getActiveAutoWorktreeContext();
88
+ assert.ok(ctx !== null, "context is non-null inside worktree");
89
+ assert.strictEqual(ctx.originalBase, tempDir, "context.originalBase matches tempDir");
90
+ assert.strictEqual(ctx.worktreeName, "M001", "context.worktreeName is M001");
91
+ assert.strictEqual(ctx.branch, "milestone/M001", "context.branch is milestone/M001");
92
+
93
+ // Teardown: registry must be cleared
94
+ teardownAutoWorktree(tempDir, "M001");
95
+
96
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
97
+ "getAutoWorktreeOriginalBase() is null after teardown");
98
+ assert.strictEqual(getActiveAutoWorktreeContext(), null,
99
+ "getActiveAutoWorktreeContext() is null after teardown");
100
+
101
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
102
+ });
103
+
104
+ test("behavioral equivalence: enterAutoWorktree also populates registry", (t) => {
105
+ const tempDir = createTempRepo(t);
106
+ const msDir = join(tempDir, ".gsd", "milestones", "M002");
107
+ mkdirSync(msDir, { recursive: true });
108
+ writeFileSync(join(msDir, "CONTEXT.md"), "# M002 Context\n");
109
+ git(["add", "."], tempDir);
110
+ git(["commit", "-m", "add milestone"], tempDir);
111
+
112
+ createAutoWorktree(tempDir, "M002");
113
+
114
+ // Simulate leaving the worktree (crash/pause)
115
+ _resetAutoWorktreeOriginalBaseForTests();
116
+ process.chdir(tempDir);
117
+
118
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null,
119
+ "registry is empty after manual reset");
120
+
121
+ // Re-enter via enterAutoWorktree
122
+ enterAutoWorktree(tempDir, "M002");
123
+
124
+ assert.strictEqual(
125
+ getAutoWorktreeOriginalBase(),
126
+ tempDir,
127
+ "getAutoWorktreeOriginalBase() returns projectRoot after enterAutoWorktree",
128
+ );
129
+ const ctx = getActiveAutoWorktreeContext();
130
+ assert.ok(ctx !== null, "context is non-null after re-entry");
131
+ assert.strictEqual(ctx.originalBase, tempDir);
132
+ assert.strictEqual(ctx.worktreeName, "M002");
133
+ assert.strictEqual(ctx.branch, "milestone/M002");
134
+
135
+ teardownAutoWorktree(tempDir, "M002");
136
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
137
+ });
138
+
139
+ test("single-occupancy: entering a new workspace replaces the previous one", (t) => {
140
+ const dir1 = createTempRepo(t);
141
+ const dir2 = createTempRepo(t);
142
+
143
+ // Set up milestone in dir1
144
+ const ms1Dir = join(dir1, ".gsd", "milestones", "M010");
145
+ mkdirSync(ms1Dir, { recursive: true });
146
+ writeFileSync(join(ms1Dir, "CONTEXT.md"), "# M010\n");
147
+ git(["add", "."], dir1);
148
+ git(["commit", "-m", "add milestone"], dir1);
149
+
150
+ // Set up milestone in dir2
151
+ const ms2Dir = join(dir2, ".gsd", "milestones", "M020");
152
+ mkdirSync(ms2Dir, { recursive: true });
153
+ writeFileSync(join(ms2Dir, "CONTEXT.md"), "# M020\n");
154
+ git(["add", "."], dir2);
155
+ git(["commit", "-m", "add milestone"], dir2);
156
+
157
+ // Enter dir1/M010
158
+ createAutoWorktree(dir1, "M010");
159
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir1,
160
+ "registry holds dir1 after entering M010");
161
+
162
+ // Tear down dir1 cleanly
163
+ teardownAutoWorktree(dir1, "M010");
164
+ assert.strictEqual(getAutoWorktreeOriginalBase(), null, "registry cleared after M010 teardown");
165
+
166
+ // Enter dir2/M020 — registry should now hold dir2 only
167
+ createAutoWorktree(dir2, "M020");
168
+ assert.strictEqual(getAutoWorktreeOriginalBase(), dir2,
169
+ "registry holds dir2 after entering M020 (single-occupancy preserved)");
170
+ assert.notStrictEqual(getAutoWorktreeOriginalBase(), dir1,
171
+ "dir1 is no longer in the registry");
172
+
173
+ teardownAutoWorktree(dir2, "M020");
174
+ try { process.chdir(savedCwd); } catch { /* ignore */ }
175
+ });
176
+ });