@opengsd/gsd-pi 1.5.0-dev.4fb2d460 → 1.5.0-dev.c4c8885f

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 (234) hide show
  1. package/dist/resources/.managed-resources-content-hash +1 -1
  2. package/dist/resources/extensions/browser-tools/engine/managed-gsd-browser.js +114 -36
  3. package/dist/resources/extensions/gsd/activity-log.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/orchestrator.js +16 -10
  5. package/dist/resources/extensions/gsd/auto-artifact-paths.js +2 -2
  6. package/dist/resources/extensions/gsd/auto-dispatch.js +53 -15
  7. package/dist/resources/extensions/gsd/auto-model-selection.js +28 -1
  8. package/dist/resources/extensions/gsd/auto-post-unit.js +4 -3
  9. package/dist/resources/extensions/gsd/auto-prompts.js +29 -117
  10. package/dist/resources/extensions/gsd/auto-start.js +53 -30
  11. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +12 -2
  12. package/dist/resources/extensions/gsd/auto-timers.js +9 -2
  13. package/dist/resources/extensions/gsd/auto.js +97 -31
  14. package/dist/resources/extensions/gsd/compat/compat-marker.js +44 -0
  15. package/dist/resources/extensions/gsd/config-overlay.js +7 -2
  16. package/dist/resources/extensions/gsd/db/engine.js +33 -0
  17. package/dist/resources/extensions/gsd/db-adapter.js +30 -3
  18. package/dist/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  19. package/dist/resources/extensions/gsd/journal.js +1 -1
  20. package/dist/resources/extensions/gsd/markdown-renderer.js +34 -91
  21. package/dist/resources/extensions/gsd/metrics.js +2 -2
  22. package/dist/resources/extensions/gsd/paths.js +70 -49
  23. package/dist/resources/extensions/gsd/post-unit-hooks.js +27 -0
  24. package/dist/resources/extensions/gsd/preferences-models.js +66 -15
  25. package/dist/resources/extensions/gsd/preferences-validation.js +66 -10
  26. package/dist/resources/extensions/gsd/rule-registry.js +106 -10
  27. package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +1 -10
  28. package/dist/resources/extensions/gsd/state-reconciliation/drift/completion.js +3 -24
  29. package/dist/resources/extensions/gsd/state-reconciliation/index.js +10 -0
  30. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -5
  31. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +16 -10
  32. package/dist/resources/extensions/gsd/tools/validate-milestone.js +3 -8
  33. package/dist/resources/extensions/gsd/uat-dispatch.js +94 -0
  34. package/dist/resources/extensions/gsd/uok/audit-toggle.js +31 -2
  35. package/dist/resources/extensions/gsd/uok/audit.js +3 -0
  36. package/dist/resources/extensions/gsd/uok/gate-runner.js +22 -30
  37. package/dist/resources/extensions/gsd/uok/gitops.js +18 -15
  38. package/dist/resources/extensions/gsd/uok/kernel.js +120 -46
  39. package/dist/resources/extensions/gsd/uok/model-policy.js +17 -14
  40. package/dist/resources/extensions/gsd/workflow-event-ledger.js +1 -1
  41. package/dist/resources/extensions/gsd/workflow-logger.js +1 -1
  42. package/dist/resources/extensions/gsd/workflow-projections.js +9 -26
  43. package/dist/resources/extensions/gsd/workspace.js +5 -14
  44. package/dist/resources/extensions/shared/gsd-browser-cli.js +29 -0
  45. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  46. package/dist/web/standalone/.next/BUILD_ID +1 -1
  47. package/dist/web/standalone/.next/app-path-routes-manifest.json +10 -10
  48. package/dist/web/standalone/.next/build-manifest.json +2 -2
  49. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  50. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  53. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  61. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  62. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/api/boot/route.js.nft.json +1 -1
  67. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
  68. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
  69. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
  70. package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
  71. package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
  72. package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
  73. package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
  74. package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
  75. package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
  76. package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
  77. package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
  78. package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
  79. package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
  80. package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
  81. package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
  82. package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
  83. package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
  84. package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
  87. package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
  88. package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
  89. package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
  91. package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
  92. package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
  93. package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
  94. package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
  95. package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
  96. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
  97. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
  98. package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
  99. package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
  100. package/dist/web/standalone/.next/server/app/index.html +1 -1
  101. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  106. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  107. package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
  108. package/dist/web/standalone/.next/server/chunks/{1128.js → 1991.js} +1 -1
  109. package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
  110. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  111. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  112. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  113. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  114. package/dist/worktree-cli-create.d.ts +10 -0
  115. package/dist/worktree-cli-create.js +11 -0
  116. package/dist/worktree-cli-format.d.ts +12 -0
  117. package/dist/worktree-cli-format.js +38 -0
  118. package/dist/worktree-cli-plan.d.ts +16 -0
  119. package/dist/worktree-cli-plan.js +19 -0
  120. package/dist/worktree-cli-session.d.ts +13 -0
  121. package/dist/worktree-cli-session.js +18 -0
  122. package/dist/worktree-cli-status.d.ts +35 -0
  123. package/dist/worktree-cli-status.js +54 -0
  124. package/dist/worktree-cli.d.ts +1 -16
  125. package/dist/worktree-cli.js +28 -122
  126. package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
  127. package/integrations/hermes/pyproject.toml +1 -1
  128. package/package.json +1 -1
  129. package/packages/cloud-mcp-gateway/package.json +2 -2
  130. package/packages/contracts/package.json +1 -1
  131. package/packages/daemon/dist/orchestrator-agent.d.ts +21 -0
  132. package/packages/daemon/dist/orchestrator-agent.d.ts.map +1 -0
  133. package/packages/daemon/dist/orchestrator-agent.js +113 -0
  134. package/packages/daemon/dist/orchestrator-agent.js.map +1 -0
  135. package/packages/daemon/dist/orchestrator-tools.d.ts +12 -0
  136. package/packages/daemon/dist/orchestrator-tools.d.ts.map +1 -0
  137. package/packages/daemon/dist/orchestrator-tools.js +137 -0
  138. package/packages/daemon/dist/orchestrator-tools.js.map +1 -0
  139. package/packages/daemon/dist/orchestrator.d.ts +4 -56
  140. package/packages/daemon/dist/orchestrator.d.ts.map +1 -1
  141. package/packages/daemon/dist/orchestrator.js +13 -303
  142. package/packages/daemon/dist/orchestrator.js.map +1 -1
  143. package/packages/daemon/dist/orchestrator.test.js +11 -1
  144. package/packages/daemon/dist/orchestrator.test.js.map +1 -1
  145. package/packages/daemon/package.json +4 -4
  146. package/packages/gsd-agent-core/package.json +5 -5
  147. package/packages/gsd-agent-modes/package.json +7 -7
  148. package/packages/mcp-server/package.json +4 -4
  149. package/packages/native/package.json +1 -1
  150. package/packages/pi-agent-core/package.json +1 -1
  151. package/packages/pi-ai/package.json +1 -1
  152. package/packages/pi-coding-agent/package.json +7 -7
  153. package/packages/pi-tui/package.json +2 -2
  154. package/packages/rpc-client/package.json +2 -2
  155. package/pkg/package.json +1 -1
  156. package/src/resources/extensions/browser-tools/engine/managed-gsd-browser.ts +146 -33
  157. package/src/resources/extensions/browser-tools/tests/gsd-browser-launch-config.test.mjs +25 -0
  158. package/src/resources/extensions/browser-tools/tests/managed-gsd-browser-tools.test.mjs +93 -0
  159. package/src/resources/extensions/gsd/activity-log.ts +1 -1
  160. package/src/resources/extensions/gsd/auto/orchestrator.ts +17 -11
  161. package/src/resources/extensions/gsd/auto-artifact-paths.ts +2 -1
  162. package/src/resources/extensions/gsd/auto-dispatch.ts +59 -14
  163. package/src/resources/extensions/gsd/auto-model-selection.ts +33 -1
  164. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -2
  165. package/src/resources/extensions/gsd/auto-prompts.ts +53 -113
  166. package/src/resources/extensions/gsd/auto-start.ts +57 -28
  167. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -2
  168. package/src/resources/extensions/gsd/auto-timers.ts +9 -2
  169. package/src/resources/extensions/gsd/auto.ts +103 -30
  170. package/src/resources/extensions/gsd/compat/compat-marker.ts +47 -0
  171. package/src/resources/extensions/gsd/config-overlay.ts +7 -1
  172. package/src/resources/extensions/gsd/db/engine.ts +36 -0
  173. package/src/resources/extensions/gsd/db-adapter.ts +29 -3
  174. package/src/resources/extensions/gsd/docs/preferences-reference.md +3 -3
  175. package/src/resources/extensions/gsd/journal.ts +1 -1
  176. package/src/resources/extensions/gsd/markdown-renderer.ts +52 -103
  177. package/src/resources/extensions/gsd/metrics.ts +2 -2
  178. package/src/resources/extensions/gsd/paths.ts +87 -48
  179. package/src/resources/extensions/gsd/post-unit-hooks.ts +32 -0
  180. package/src/resources/extensions/gsd/preferences-models.ts +74 -20
  181. package/src/resources/extensions/gsd/preferences-types.ts +24 -1
  182. package/src/resources/extensions/gsd/preferences-validation.ts +77 -9
  183. package/src/resources/extensions/gsd/rule-registry.ts +111 -9
  184. package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +1 -9
  185. package/src/resources/extensions/gsd/state-reconciliation/drift/completion.ts +2 -22
  186. package/src/resources/extensions/gsd/state-reconciliation/index.ts +14 -0
  187. package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +41 -2
  188. package/src/resources/extensions/gsd/tests/auto-post-unit-artifact-diagnostic.test.ts +48 -1
  189. package/src/resources/extensions/gsd/tests/auto-start-project-milestone-reconcile.test.ts +29 -1
  190. package/src/resources/extensions/gsd/tests/auto-timeout-recovery-supervisor-inflight-guard.test.ts +159 -0
  191. package/src/resources/extensions/gsd/tests/commands-verdict.test.ts +3 -1
  192. package/src/resources/extensions/gsd/tests/compat-marker.test.ts +85 -0
  193. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +45 -0
  194. package/src/resources/extensions/gsd/tests/db-adapter.test.ts +39 -0
  195. package/src/resources/extensions/gsd/tests/db-writable-probe.test.ts +66 -0
  196. package/src/resources/extensions/gsd/tests/flat-phase-renderer.test.ts +22 -1
  197. package/src/resources/extensions/gsd/tests/flat-phase-validation-path.test.ts +72 -32
  198. package/src/resources/extensions/gsd/tests/integration/paths.test.ts +88 -1
  199. package/src/resources/extensions/gsd/tests/milestone-closeout.test.ts +2 -1
  200. package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +0 -1
  201. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +269 -0
  202. package/src/resources/extensions/gsd/tests/post-unit-retry-on-orchestrator-bridge.test.ts +8 -1
  203. package/src/resources/extensions/gsd/tests/read-uat-gate-verdict.test.ts +54 -0
  204. package/src/resources/extensions/gsd/tests/reassess-handler.test.ts +49 -1
  205. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +95 -1
  206. package/src/resources/extensions/gsd/tests/sibling-model-fallbacks.test.ts +189 -0
  207. package/src/resources/extensions/gsd/tests/single-field-model-thinking.test.ts +156 -0
  208. package/src/resources/extensions/gsd/tests/skipped-validation-db-atomicity.test.ts +6 -1
  209. package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +32 -0
  210. package/src/resources/extensions/gsd/tests/supervisor-model-swap-inflight-guard.test.ts +181 -0
  211. package/src/resources/extensions/gsd/tests/supervisor-model-swap-thinking-clamp.test.ts +122 -0
  212. package/src/resources/extensions/gsd/tests/uat-dispatch.test.ts +211 -0
  213. package/src/resources/extensions/gsd/tests/uok-gate-runner.test.ts +39 -0
  214. package/src/resources/extensions/gsd/tests/uok-kernel-path.test.ts +226 -2
  215. package/src/resources/extensions/gsd/tests/uok-model-policy.test.ts +6 -0
  216. package/src/resources/extensions/gsd/tests/workflow-projections.test.ts +213 -2
  217. package/src/resources/extensions/gsd/tools/complete-milestone.ts +9 -5
  218. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +19 -8
  219. package/src/resources/extensions/gsd/tools/validate-milestone.ts +13 -9
  220. package/src/resources/extensions/gsd/types.ts +23 -4
  221. package/src/resources/extensions/gsd/uat-dispatch.ts +176 -0
  222. package/src/resources/extensions/gsd/uok/audit-toggle.ts +32 -2
  223. package/src/resources/extensions/gsd/uok/audit.ts +2 -0
  224. package/src/resources/extensions/gsd/uok/gate-runner.ts +26 -36
  225. package/src/resources/extensions/gsd/uok/gitops.ts +21 -18
  226. package/src/resources/extensions/gsd/uok/kernel.ts +169 -48
  227. package/src/resources/extensions/gsd/uok/model-policy.ts +20 -17
  228. package/src/resources/extensions/gsd/workflow-event-ledger.ts +1 -1
  229. package/src/resources/extensions/gsd/workflow-logger.ts +1 -1
  230. package/src/resources/extensions/gsd/workflow-projections.ts +9 -24
  231. package/src/resources/extensions/gsd/workspace.ts +5 -21
  232. package/src/resources/extensions/shared/gsd-browser-cli.ts +29 -0
  233. /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_buildManifest.js +0 -0
  234. /package/dist/web/standalone/.next/static/{gXoWec4UNqcR0tkuHwYCr → NPMNi2HV8BuK3baQsEUhk}/_ssgManifest.js +0 -0
@@ -1,6 +1,6 @@
1
1
  import test from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
3
+ import { mkdtempSync, mkdirSync, rmSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
4
4
  import { join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
 
@@ -394,6 +394,54 @@ test('handleReassessRoadmap invalidates stale milestone-validation when roadmap
394
394
  }
395
395
  });
396
396
 
397
+ test('handleReassessRoadmap deletes flat-phase compatibility validation files when roadmap changes (#2957)', async (t) => {
398
+ const base = makeTmpBase();
399
+ t.after(() => cleanup(base));
400
+ openDatabase(join(base, '.gsd', 'gsd.db'));
401
+
402
+ insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
403
+ insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Slice One', status: 'complete', demo: 'Demo' });
404
+
405
+ const legacyValidationPath = join(base, '.gsd', 'phases', '01-test', 'M001-VALIDATION.md');
406
+ const canonicalValidationPath = join(base, '.gsd', 'phases', '01-test', '01-VALIDATION.md');
407
+ const staleValidation = '---\nverdict: pass\n---\n\n# Stale validation\n';
408
+ writeFileSync(legacyValidationPath, staleValidation, 'utf-8');
409
+ writeFileSync(canonicalValidationPath, staleValidation, 'utf-8');
410
+ insertAssessment({
411
+ path: legacyValidationPath,
412
+ milestoneId: 'M001',
413
+ sliceId: null,
414
+ taskId: null,
415
+ status: 'pass',
416
+ scope: 'milestone-validation',
417
+ fullContent: staleValidation,
418
+ });
419
+
420
+ const result = await handleReassessRoadmap({
421
+ milestoneId: 'M001',
422
+ completedSliceId: 'S01',
423
+ verdict: 'confirmed',
424
+ assessment: 'S01 completed. Adding S02 changes roadmap structure.',
425
+ sliceChanges: {
426
+ modified: [],
427
+ added: [
428
+ {
429
+ sliceId: 'S02',
430
+ title: 'New Slice Two',
431
+ risk: 'low',
432
+ depends: ['S01'],
433
+ demo: 'Demo two.',
434
+ },
435
+ ],
436
+ removed: [],
437
+ },
438
+ }, base);
439
+
440
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
441
+ assert.equal(existsSync(legacyValidationPath), false, 'stale compatibility validation artifact should be deleted');
442
+ assert.equal(existsSync(canonicalValidationPath), false, 'canonical validation artifact should not remain after reassess');
443
+ });
444
+
397
445
  test('handleReassessRoadmap does NOT invalidate validation when no roadmap structural changes (#2957)', async () => {
398
446
  const base = makeTmpBase();
399
447
  openDatabase(join(base, '.gsd', 'gsd.db'));
@@ -5,7 +5,7 @@
5
5
 
6
6
  import assert from 'node:assert/strict';
7
7
  import { test, describe, beforeEach } from "node:test";
8
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
8
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, realpathSync } from "node:fs";
9
9
  import { tmpdir } from "node:os";
10
10
  import { join } from "node:path";
11
11
  import { emitJournalEvent } from "../journal.ts";
@@ -544,3 +544,97 @@ describe("RuleRegistry", () => {
544
544
  assert.deepStrictEqual(result.matchedRule, "<no-match>", "matchedRule is '<no-match>' on fallback");
545
545
  });
546
546
  });
547
+
548
+ describe("resolveHookArtifactPath", () => {
549
+ test("resolves a phase-level artifact from the .gsd/phases layout", () => {
550
+ const originalGsdHome = process.env.GSD_HOME;
551
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-phase-"));
552
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
553
+ try {
554
+ process.env.GSD_HOME = tempGsdHome;
555
+ const phaseDir = join(realpathSync(projectRoot), ".gsd", "phases", "50-some-phase");
556
+ mkdirSync(phaseDir, { recursive: true });
557
+ const artifactPath = join(phaseDir, "BROWSER-RUNTIME-EVIDENCE.md");
558
+ writeFileSync(artifactPath, "---\nverdict: advisory\n---\n", "utf-8");
559
+
560
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "BROWSER-RUNTIME-EVIDENCE.md");
561
+ assert.equal(resolved, artifactPath, "resolves the canonical phase-level artifact");
562
+ } finally {
563
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
564
+ else process.env.GSD_HOME = originalGsdHome;
565
+ rmSync(projectRoot, { recursive: true, force: true });
566
+ rmSync(tempGsdHome, { recursive: true, force: true });
567
+ }
568
+ });
569
+
570
+ test("falls back to the legacy milestones/slices/tasks layout", () => {
571
+ const originalGsdHome = process.env.GSD_HOME;
572
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-"));
573
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
574
+ try {
575
+ process.env.GSD_HOME = tempGsdHome;
576
+ const tasksDir = join(projectRoot, ".gsd", "milestones", "M050", "slices", "S02", "tasks");
577
+ mkdirSync(tasksDir, { recursive: true });
578
+ const artifactPath = join(tasksDir, "T01-REVIEW.md");
579
+ writeFileSync(artifactPath, "---\nverdict: pass\n---\n", "utf-8");
580
+
581
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
582
+ assert.equal(resolved, artifactPath, "resolves the task-prefixed legacy artifact");
583
+ } finally {
584
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
585
+ else process.env.GSD_HOME = originalGsdHome;
586
+ rmSync(projectRoot, { recursive: true, force: true });
587
+ rmSync(tempGsdHome, { recursive: true, force: true });
588
+ }
589
+ });
590
+
591
+ test("legacy: nested task artifact wins over a milestone-root file of the same name", () => {
592
+ const originalGsdHome = process.env.GSD_HOME;
593
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-root-"));
594
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
595
+ try {
596
+ process.env.GSD_HOME = tempGsdHome;
597
+ const milestoneDir = join(projectRoot, ".gsd", "milestones", "M050");
598
+ const tasksDir = join(milestoneDir, "slices", "S02", "tasks");
599
+ mkdirSync(tasksDir, { recursive: true });
600
+ // The correct task-scoped artifact lives in the nested slices/tasks tree.
601
+ const nestedPath = join(tasksDir, "T01-REVIEW.md");
602
+ writeFileSync(nestedPath, "---\nverdict: pass\n---\n", "utf-8");
603
+ // A same-named decoy at the legacy milestone root must NOT win — before the
604
+ // fix, resolveMilestonePath returned the milestone root and it was probed
605
+ // ahead of the nested task path (#1264 Bugbot follow-up).
606
+ writeFileSync(join(milestoneDir, "REVIEW.md"), "---\nverdict: failed\n---\n", "utf-8");
607
+
608
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
609
+ assert.equal(resolved, nestedPath, "task-scoped legacy path wins over the milestone-root file");
610
+ } finally {
611
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
612
+ else process.env.GSD_HOME = originalGsdHome;
613
+ rmSync(projectRoot, { recursive: true, force: true });
614
+ rmSync(tempGsdHome, { recursive: true, force: true });
615
+ }
616
+ });
617
+
618
+ test("legacy: missing-artifact fallback points at the nested task path, not the milestone root", () => {
619
+ const originalGsdHome = process.env.GSD_HOME;
620
+ const projectRoot = mkdtempSync(join(tmpdir(), "gsd-hook-legacy-miss-"));
621
+ const tempGsdHome = mkdtempSync(join(tmpdir(), "gsd-hook-home-"));
622
+ try {
623
+ process.env.GSD_HOME = tempGsdHome;
624
+ const milestoneDir = join(projectRoot, ".gsd", "milestones", "M050");
625
+ mkdirSync(milestoneDir, { recursive: true });
626
+ // Content-bearing legacy milestone dir (a non-META file) so resolveMilestonePath
627
+ // returns it, but the requested gate artifact does not exist anywhere.
628
+ writeFileSync(join(milestoneDir, "M050-ROADMAP.md"), "# roadmap\n", "utf-8");
629
+
630
+ const resolved = resolveHookArtifactPath(projectRoot, "M050/S02/T01", "REVIEW.md");
631
+ const expected = join(milestoneDir, "slices", "S02", "tasks", "T01-REVIEW.md");
632
+ assert.equal(resolved, expected, "diagnostic fallback uses the nested task path for a legacy task-scoped unit");
633
+ } finally {
634
+ if (originalGsdHome === undefined) delete process.env.GSD_HOME;
635
+ else process.env.GSD_HOME = originalGsdHome;
636
+ rmSync(projectRoot, { recursive: true, force: true });
637
+ rmSync(tempGsdHome, { recursive: true, force: true });
638
+ }
639
+ });
640
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Sibling single-model fields honor `fallbacks[]` (#1229).
3
+ *
4
+ * `post_unit_hooks[].model`, `auto_supervisor.model`, and
5
+ * `reactive_execution.subagent_model` accept the same
6
+ * `{ model, provider?, fallbacks? }` object form as `models.<phase>` so a
7
+ * transient provider trip falls back to the next entry instead of hard-failing
8
+ * (and, for a blocking hook, pausing the whole auto-mode run).
9
+ */
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+
17
+ import {
18
+ normalizeModelFieldConfig,
19
+ resolveModelWithFallbacksForUnit,
20
+ resolveAutoSupervisorConfig,
21
+ } from "../preferences-models.ts";
22
+ import { validatePreferences } from "../preferences-validation.ts";
23
+
24
+ function withPreferences<T>(frontmatter: string[], fn: () => T): T {
25
+ const oldHome = process.env.GSD_HOME;
26
+ const home = mkdtempSync(join(tmpdir(), "gsd-sibling-models-"));
27
+ try {
28
+ process.env.GSD_HOME = home;
29
+ writeFileSync(join(home, "preferences.md"), ["---", ...frontmatter, "---", ""].join("\n"));
30
+ return fn();
31
+ } finally {
32
+ if (oldHome === undefined) delete process.env.GSD_HOME;
33
+ else process.env.GSD_HOME = oldHome;
34
+ rmSync(home, { recursive: true, force: true });
35
+ }
36
+ }
37
+
38
+ // ─── normalizeModelFieldConfig ───────────────────────────────────────────────
39
+
40
+ test("normalizeModelFieldConfig: bare string → primary with empty fallbacks", () => {
41
+ assert.deepEqual(normalizeModelFieldConfig("my-model"), { primary: "my-model", fallbacks: [] });
42
+ });
43
+
44
+ test("normalizeModelFieldConfig: object form carries fallbacks", () => {
45
+ assert.deepEqual(
46
+ normalizeModelFieldConfig({ model: "primary", fallbacks: ["fb-1", "fb-2"] }),
47
+ { primary: "primary", fallbacks: ["fb-1", "fb-2"] },
48
+ );
49
+ });
50
+
51
+ test("normalizeModelFieldConfig: provider prefix is prepended to bare model", () => {
52
+ assert.deepEqual(
53
+ normalizeModelFieldConfig({ model: "gpt-5.4", provider: "openai-codex" }),
54
+ { primary: "openai-codex/gpt-5.4", fallbacks: [] },
55
+ );
56
+ });
57
+
58
+ test("normalizeModelFieldConfig: unset / blank / model-less → undefined", () => {
59
+ assert.equal(normalizeModelFieldConfig(undefined), undefined);
60
+ assert.equal(normalizeModelFieldConfig(" "), undefined);
61
+ assert.equal(normalizeModelFieldConfig({ model: "" }), undefined);
62
+ });
63
+
64
+ // ─── post_unit_hooks[].model fallback resolution ─────────────────────────────
65
+
66
+ test("hook/<name> resolves the hook's object-form model with fallbacks", () => {
67
+ withPreferences(
68
+ [
69
+ "post_unit_hooks:",
70
+ " - name: plan-review",
71
+ " after: [plan-slice]",
72
+ " prompt: review the plan",
73
+ " model:",
74
+ " model: primary-model",
75
+ " fallbacks:",
76
+ " - fallback-a",
77
+ " - fallback-b",
78
+ ],
79
+ () => {
80
+ const resolved = resolveModelWithFallbacksForUnit("hook/plan-review");
81
+ assert.deepEqual(resolved, { primary: "primary-model", fallbacks: ["fallback-a", "fallback-b"] });
82
+ },
83
+ );
84
+ });
85
+
86
+ test("hook/<name> still resolves the legacy bare-string model form", () => {
87
+ withPreferences(
88
+ [
89
+ "post_unit_hooks:",
90
+ " - name: plan-review",
91
+ " after: [plan-slice]",
92
+ " prompt: review the plan",
93
+ " model: solo-model",
94
+ ],
95
+ () => {
96
+ assert.deepEqual(
97
+ resolveModelWithFallbacksForUnit("hook/plan-review"),
98
+ { primary: "solo-model", fallbacks: [] },
99
+ );
100
+ },
101
+ );
102
+ });
103
+
104
+ test("hook/<name> resolves undefined when the hook has no model", () => {
105
+ withPreferences(
106
+ [
107
+ "post_unit_hooks:",
108
+ " - name: plan-review",
109
+ " after: [plan-slice]",
110
+ " prompt: review the plan",
111
+ ],
112
+ () => {
113
+ assert.equal(resolveModelWithFallbacksForUnit("hook/plan-review"), undefined);
114
+ assert.equal(resolveModelWithFallbacksForUnit("hook/unknown-hook"), undefined);
115
+ },
116
+ );
117
+ });
118
+
119
+ // ─── auto_supervisor.model ───────────────────────────────────────────────────
120
+
121
+ test("auto_supervisor.model object form resolves with fallbacks", () => {
122
+ withPreferences(
123
+ [
124
+ "auto_supervisor:",
125
+ " model:",
126
+ " model: supervisor-primary",
127
+ " fallbacks: [supervisor-fb]",
128
+ ],
129
+ () => {
130
+ assert.deepEqual(resolveModelWithFallbacksForUnit("supervisor"), {
131
+ primary: "supervisor-primary",
132
+ fallbacks: ["supervisor-fb"],
133
+ });
134
+ const supervisor = resolveAutoSupervisorConfig();
135
+ assert.equal(supervisor.model, "supervisor-primary");
136
+ assert.deepEqual(supervisor.modelFallbacks, ["supervisor-fb"]);
137
+ },
138
+ );
139
+ });
140
+
141
+ // ─── validation accepts the object form ──────────────────────────────────────
142
+
143
+ test("validatePreferences accepts object-form hook and subagent models", () => {
144
+ const { preferences: validated, errors } = validatePreferences({
145
+ post_unit_hooks: [
146
+ {
147
+ name: "plan-review",
148
+ after: ["plan-slice"],
149
+ prompt: "review",
150
+ model: { model: "primary", fallbacks: ["fb-1"] },
151
+ },
152
+ ],
153
+ reactive_execution: {
154
+ enabled: true,
155
+ subagent_model: { model: "sub-primary", fallbacks: ["sub-fb"] },
156
+ },
157
+ } as never);
158
+
159
+ assert.deepEqual(errors, []);
160
+ assert.deepEqual(validated.post_unit_hooks?.[0].model, { model: "primary", fallbacks: ["fb-1"] });
161
+ assert.deepEqual(validated.reactive_execution?.subagent_model, { model: "sub-primary", fallbacks: ["sub-fb"] });
162
+ });
163
+
164
+ test("validatePreferences rejects a model-less object form", () => {
165
+ const { errors } = validatePreferences({
166
+ post_unit_hooks: [
167
+ { name: "plan-review", after: ["plan-slice"], prompt: "review", model: { fallbacks: ["fb"] } },
168
+ ],
169
+ } as never);
170
+ assert.ok(errors.some((e) => e.includes('requires a non-empty "model"')));
171
+ });
172
+
173
+ test("validatePreferences accepts object-form auto_supervisor.model", () => {
174
+ const { preferences: validated, errors } = validatePreferences({
175
+ auto_supervisor: { model: { model: "sup-primary", fallbacks: ["sup-fb"] } },
176
+ } as never);
177
+ assert.deepEqual(errors, []);
178
+ assert.deepEqual(validated.auto_supervisor?.model, { model: "sup-primary", fallbacks: ["sup-fb"] });
179
+ });
180
+
181
+ test("validatePreferences rejects a model-less auto_supervisor.model object", () => {
182
+ const { errors } = validatePreferences({
183
+ auto_supervisor: { model: { fallbacks: ["sup-fb"] } },
184
+ } as never);
185
+ assert.ok(
186
+ errors.some((e) => e.includes("auto_supervisor.model") && e.includes('requires a non-empty "model"')),
187
+ `expected supervisor model error, got: ${JSON.stringify(errors)}`,
188
+ );
189
+ });
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Regression tests for #1269 — the object model form's `thinking` sub-field on
3
+ * the three single-model fields (`post_unit_hooks[].model`,
4
+ * `auto_supervisor.model`, and `reactive_execution.subagent_model`) was
5
+ * silently dropped after validation/normalization, so a configured
6
+ * hook/supervisor/subagent reasoning level never applied.
7
+ *
8
+ * These exercise the real preference pipeline through the exported runtime APIs:
9
+ * sanitize → validatePreferences
10
+ * normalize → normalizeModelFieldConfig
11
+ * resolve → resolveThinkingLevelForUnit (supervisor + hook/* early path)
12
+ *
13
+ * The `supervisor` and `hook/*` unit types have no phase-bucket chain, so the
14
+ * pre-#1269 resolver walk never covered them and returned `undefined`.
15
+ */
16
+
17
+ import test from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { tmpdir } from "node:os";
22
+
23
+ import { validatePreferences } from "../preferences-validation.ts";
24
+ import { normalizeModelFieldConfig, resolveThinkingLevelForUnit } from "../preferences-models.ts";
25
+ import { invalidateAllCaches } from "../cache.ts";
26
+
27
+ /** Read the per-field `thinking` off a validated/normalized model config. */
28
+ function thinkingOf(model: unknown): string | undefined {
29
+ return (model as { thinking?: string } | undefined)?.thinking;
30
+ }
31
+
32
+ /** Write a project-level `.gsd/PREFERENCES.md` and run `fn` with its basePath. */
33
+ function withProjectPreferences<T>(frontmatter: string, fn: (basePath: string) => T): T {
34
+ const base = mkdtempSync(join(tmpdir(), "gsd-1269-thinking-"));
35
+ mkdirSync(join(base, ".gsd"), { recursive: true });
36
+ writeFileSync(join(base, ".gsd", "PREFERENCES.md"), `---\n${frontmatter}\n---\n`, "utf-8");
37
+ invalidateAllCaches();
38
+ try {
39
+ return fn(base);
40
+ } finally {
41
+ invalidateAllCaches();
42
+ rmSync(base, { recursive: true, force: true });
43
+ }
44
+ }
45
+
46
+ // ── sanitize: validatePreferences preserves a valid object-form thinking ──────
47
+
48
+ test("validatePreferences preserves auto_supervisor.model object-form thinking", () => {
49
+ const { preferences, errors } = validatePreferences({
50
+ auto_supervisor: { model: { model: "supervisor-model", thinking: "high" } },
51
+ });
52
+ assert.equal(errors.length, 0);
53
+ assert.equal(thinkingOf(preferences.auto_supervisor?.model), "high");
54
+ });
55
+
56
+ test("validatePreferences preserves post_unit_hooks[].model object-form thinking", () => {
57
+ const { preferences, errors } = validatePreferences({
58
+ post_unit_hooks: [
59
+ {
60
+ name: "code-review",
61
+ after: ["execute-task"],
62
+ prompt: "Review the diff",
63
+ model: { model: "hook-model", thinking: "low" },
64
+ },
65
+ ],
66
+ });
67
+ assert.equal(errors.length, 0);
68
+ assert.equal(thinkingOf(preferences.post_unit_hooks?.[0]?.model), "low");
69
+ });
70
+
71
+ test("validatePreferences preserves reactive_execution.subagent_model object-form thinking", () => {
72
+ const { preferences, errors } = validatePreferences({
73
+ reactive_execution: {
74
+ enabled: true,
75
+ max_parallel: 2,
76
+ isolation_mode: "same-tree",
77
+ subagent_model: { model: "subagent-model", thinking: "medium" },
78
+ },
79
+ });
80
+ assert.equal(errors.length, 0);
81
+ assert.equal(thinkingOf(preferences.reactive_execution?.subagent_model), "medium");
82
+ });
83
+
84
+ // ── sanitize: an invalid thinking level is warned and stripped ────────────────
85
+
86
+ test("validatePreferences warns and strips an invalid object-form thinking level", () => {
87
+ const { preferences, errors, warnings } = validatePreferences({
88
+ auto_supervisor: { model: { model: "supervisor-model", thinking: "ludicrous" } },
89
+ } as never);
90
+ // The model survives; only the bogus thinking is dropped, so a typo can never
91
+ // reach the resolver and masquerade as explicit configuration.
92
+ assert.equal(errors.length, 0);
93
+ assert.equal(thinkingOf(preferences.auto_supervisor?.model), undefined);
94
+ assert.ok(
95
+ warnings.some((w) => w.includes("auto_supervisor.model") && w.includes("thinking")),
96
+ `expected a thinking warning, got: ${JSON.stringify(warnings)}`,
97
+ );
98
+ });
99
+
100
+ // ── normalize: normalizeModelFieldConfig carries thinking through ─────────────
101
+
102
+ test("normalizeModelFieldConfig carries object-form thinking through", () => {
103
+ const config = normalizeModelFieldConfig({ model: "hook-model", thinking: "xhigh" });
104
+ assert.equal(config?.primary, "hook-model");
105
+ assert.equal(config?.thinking, "xhigh");
106
+ });
107
+
108
+ test("normalizeModelFieldConfig leaves thinking absent when omitted", () => {
109
+ const fromObject = normalizeModelFieldConfig({ model: "hook-model" });
110
+ assert.equal(fromObject?.primary, "hook-model");
111
+ assert.equal(fromObject?.thinking, undefined);
112
+
113
+ const fromString = normalizeModelFieldConfig("hook-model");
114
+ assert.equal(fromString?.primary, "hook-model");
115
+ assert.equal(fromString?.thinking, undefined);
116
+ });
117
+
118
+ // ── resolve: the single-field unit types surface their per-field thinking ──────
119
+
120
+ test("resolveThinkingLevelForUnit('supervisor') returns auto_supervisor.model thinking", () => {
121
+ withProjectPreferences(
122
+ ["auto_supervisor:", " model:", " model: supervisor-model", " thinking: high"].join("\n"),
123
+ (base) => {
124
+ assert.equal(resolveThinkingLevelForUnit("supervisor", base), "high");
125
+ },
126
+ );
127
+ });
128
+
129
+ test("resolveThinkingLevelForUnit('hook/<name>') returns the matching hook model thinking", () => {
130
+ withProjectPreferences(
131
+ [
132
+ "post_unit_hooks:",
133
+ " - name: code-review",
134
+ " after:",
135
+ " - execute-task",
136
+ " prompt: Review the diff",
137
+ " model:",
138
+ " model: hook-model",
139
+ " thinking: low",
140
+ ].join("\n"),
141
+ (base) => {
142
+ assert.equal(resolveThinkingLevelForUnit("hook/code-review", base), "low");
143
+ },
144
+ );
145
+ });
146
+
147
+ test("resolveThinkingLevelForUnit('supervisor') stays undefined when thinking is omitted", () => {
148
+ // Bare-string model form carries no thinking → dispatch falls back to the
149
+ // session/floor level, unchanged from pre-#1269 behavior.
150
+ withProjectPreferences(
151
+ ["auto_supervisor:", " model: supervisor-model"].join("\n"),
152
+ (base) => {
153
+ assert.equal(resolveThinkingLevelForUnit("supervisor", base), undefined);
154
+ },
155
+ );
156
+ });
@@ -52,7 +52,12 @@ test("skipped validation dispatch persists the validation file and DB assessment
52
52
  const artifactPath = "milestones/M001/slices/S01/S01-ASSESSMENT.md";
53
53
  const assessmentPath = `.gsd/${artifactPath}`;
54
54
  assert.equal(getArtifact(artifactPath)?.artifact_type, "ASSESSMENT");
55
- assert.equal(getAssessment(assessmentPath)?.scope, "run-uat");
55
+ // #1258: the per-slice ASSESSMENT fabricated by the pre-validation backfill
56
+ // is a placeholder (no UAT ever ran for this slice), so it is filed under the
57
+ // `backfill` scope — never `run-uat` — so `readUatGateVerdict` cannot mistake
58
+ // it for a genuine UAT sign-off. Its verdict body stays PASS to satisfy the
59
+ // MV02 artifact-existence requirement.
60
+ assert.equal(getAssessment(assessmentPath)?.scope, "backfill");
56
61
  assert.equal(getAssessment(assessmentPath)?.status, "pass");
57
62
  assert.equal(
58
63
  getLatestAssessmentByScope("M001", "milestone-validation")?.status,
@@ -208,6 +208,38 @@ test("startAutoDetached reports failures asynchronously (#3733)", () => {
208
208
  );
209
209
  });
210
210
 
211
+ test("failed auto-start always clears leaked s.active so later /gsd auto is not bricked (#1235)", () => {
212
+ const autoSrc = readGsdFile("auto.ts");
213
+
214
+ // 1. startAutoDetached is the single funnel for any exception thrown out of
215
+ // startAuto — including a bootstrap throw that escapes before the loop's
216
+ // try/finally could run cleanupAfterLoopExit. The failure handler must
217
+ // clear leaked activation, or s.active + the on-disk auto lock persist and
218
+ // the re-entry guard silently no-ops every later /gsd auto until restart.
219
+ const detachedIdx = autoSrc.indexOf("export function startAutoDetached");
220
+ assert.ok(detachedIdx > -1, "startAutoDetached should exist");
221
+ const detachedBody = autoSrc.slice(detachedIdx, autoSrc.indexOf("\n}", detachedIdx));
222
+ assert.ok(
223
+ detachedBody.includes("if (s.active)") &&
224
+ detachedBody.includes("cleanupAfterLoopExit(ctx)"),
225
+ "startAutoDetached failure handler must run cleanupAfterLoopExit when auto-mode is still active",
226
+ );
227
+
228
+ // 2. bootstrapAutoSession sets s.active = true before some bail-outs return
229
+ // false (e.g. the SQLite-unavailable gate), so the fresh-start !ready path
230
+ // must clear the leaked activation before returning.
231
+ const startAutoIdx = autoSrc.indexOf("export async function startAuto(");
232
+ assert.ok(startAutoIdx > -1, "startAuto should exist");
233
+ const startAutoBody = autoSrc.slice(startAutoIdx);
234
+ const notReadyIdx = startAutoBody.indexOf("if (!ready) {");
235
+ assert.ok(notReadyIdx > -1, "fresh start should guard on bootstrap readiness with a block body");
236
+ const notReadyBlock = startAutoBody.slice(notReadyIdx, notReadyIdx + 600);
237
+ assert.ok(
238
+ notReadyBlock.includes("if (s.active) await cleanupAfterLoopExit(ctx)"),
239
+ "fresh-start !ready bail-out must clear leaked auto activation before returning",
240
+ );
241
+ });
242
+
211
243
  test("detached auto-start keeps a ref'ed handle until the run settles", async () => {
212
244
  const originalSetInterval = globalThis.setInterval;
213
245
  const originalClearInterval = globalThis.clearInterval;