gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.2ccf3fb

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 (256) hide show
  1. package/README.md +24 -17
  2. package/dist/cli.js +15 -9
  3. package/dist/resource-loader.js +80 -8
  4. package/dist/resources/extensions/bg-shell/process-manager.ts +13 -0
  5. package/dist/resources/extensions/gsd/auto-dashboard.ts +186 -65
  6. package/dist/resources/extensions/gsd/auto-post-unit.ts +14 -6
  7. package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
  8. package/dist/resources/extensions/gsd/auto-start.ts +25 -10
  9. package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
  10. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  11. package/dist/resources/extensions/gsd/auto.ts +67 -22
  12. package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
  13. package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
  14. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
  15. package/dist/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  16. package/dist/resources/extensions/gsd/commands.ts +75 -29
  17. package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  18. package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
  19. package/dist/resources/extensions/gsd/doctor.ts +2 -6
  20. package/dist/resources/extensions/gsd/export.ts +28 -2
  21. package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
  22. package/dist/resources/extensions/gsd/index.ts +2 -1
  23. package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
  24. package/dist/resources/extensions/gsd/metrics.ts +17 -31
  25. package/dist/resources/extensions/gsd/paths.ts +0 -8
  26. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  27. package/dist/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +10 -11
  29. package/dist/resources/extensions/gsd/routing-history.ts +13 -17
  30. package/dist/resources/extensions/gsd/session-lock.ts +284 -0
  31. package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
  32. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  33. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  34. package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  35. package/dist/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  36. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  37. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  38. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  39. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  40. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  41. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  42. package/dist/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  43. package/dist/resources/extensions/gsd/types.ts +1 -0
  44. package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
  45. package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
  46. package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
  47. package/dist/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  48. package/dist/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  49. package/dist/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  50. package/dist/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  51. package/dist/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  52. package/dist/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  53. package/dist/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  54. package/dist/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  55. package/dist/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  56. package/dist/resources/extensions/gsd/workflow-templates.ts +241 -0
  57. package/dist/resources/extensions/mcp-client/index.ts +459 -0
  58. package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  59. package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
  60. package/dist/resources/extensions/remote-questions/notify.ts +1 -2
  61. package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  62. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  63. package/dist/resources/extensions/remote-questions/types.ts +3 -0
  64. package/dist/resources/extensions/shared/mod.ts +3 -0
  65. package/dist/resources/skills/create-gsd-extension/SKILL.md +87 -0
  66. package/dist/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
  67. package/dist/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
  68. package/dist/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
  69. package/dist/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
  70. package/dist/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
  71. package/dist/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
  72. package/dist/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
  73. package/dist/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
  74. package/dist/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
  75. package/dist/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
  76. package/dist/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
  77. package/dist/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
  78. package/dist/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
  79. package/dist/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
  80. package/dist/resources/skills/create-gsd-extension/references/state-management.md +70 -0
  81. package/dist/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
  82. package/dist/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
  83. package/dist/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
  84. package/dist/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
  85. package/dist/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
  86. package/dist/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
  87. package/dist/resources/skills/create-skill/SKILL.md +184 -0
  88. package/dist/resources/skills/create-skill/references/api-security.md +226 -0
  89. package/dist/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
  90. package/dist/resources/skills/create-skill/references/common-patterns.md +595 -0
  91. package/dist/resources/skills/create-skill/references/core-principles.md +437 -0
  92. package/dist/resources/skills/create-skill/references/executable-code.md +175 -0
  93. package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
  94. package/dist/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
  95. package/dist/resources/skills/create-skill/references/recommended-structure.md +168 -0
  96. package/dist/resources/skills/create-skill/references/skill-structure.md +372 -0
  97. package/dist/resources/skills/create-skill/references/use-xml-tags.md +466 -0
  98. package/dist/resources/skills/create-skill/references/using-scripts.md +113 -0
  99. package/dist/resources/skills/create-skill/references/using-templates.md +112 -0
  100. package/dist/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
  101. package/dist/resources/skills/create-skill/templates/router-skill.md +73 -0
  102. package/dist/resources/skills/create-skill/templates/simple-skill.md +33 -0
  103. package/dist/resources/skills/create-skill/workflows/add-reference.md +96 -0
  104. package/dist/resources/skills/create-skill/workflows/add-script.md +93 -0
  105. package/dist/resources/skills/create-skill/workflows/add-template.md +74 -0
  106. package/dist/resources/skills/create-skill/workflows/add-workflow.md +120 -0
  107. package/dist/resources/skills/create-skill/workflows/audit-skill.md +148 -0
  108. package/dist/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
  109. package/dist/resources/skills/create-skill/workflows/get-guidance.md +121 -0
  110. package/dist/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
  111. package/dist/resources/skills/create-skill/workflows/verify-skill.md +204 -0
  112. package/package.json +6 -3
  113. package/packages/native/dist/native.d.ts +2 -0
  114. package/packages/native/dist/native.js +19 -5
  115. package/packages/native/src/native.ts +23 -9
  116. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/extensions/loader.js +13 -0
  118. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  120. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  122. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  124. package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
  125. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  126. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
  128. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  129. package/packages/pi-coding-agent/package.json +1 -1
  130. package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
  131. package/packages/pi-coding-agent/src/core/extensions/loader.ts +13 -0
  132. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  133. package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
  134. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
  135. package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
  136. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  137. package/packages/pi-tui/dist/autocomplete.js +14 -0
  138. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  139. package/packages/pi-tui/src/autocomplete.ts +19 -1
  140. package/pkg/package.json +1 -1
  141. package/src/resources/extensions/bg-shell/process-manager.ts +13 -0
  142. package/src/resources/extensions/gsd/auto-dashboard.ts +186 -65
  143. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  144. package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
  145. package/src/resources/extensions/gsd/auto-start.ts +25 -10
  146. package/src/resources/extensions/gsd/auto-verification.ts +41 -7
  147. package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  148. package/src/resources/extensions/gsd/auto.ts +67 -22
  149. package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
  150. package/src/resources/extensions/gsd/commands-logs.ts +536 -0
  151. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +90 -47
  152. package/src/resources/extensions/gsd/commands-workflow-templates.ts +544 -0
  153. package/src/resources/extensions/gsd/commands.ts +75 -29
  154. package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  155. package/src/resources/extensions/gsd/doctor-types.ts +13 -0
  156. package/src/resources/extensions/gsd/doctor.ts +2 -6
  157. package/src/resources/extensions/gsd/export.ts +28 -2
  158. package/src/resources/extensions/gsd/gsd-db.ts +19 -0
  159. package/src/resources/extensions/gsd/index.ts +2 -1
  160. package/src/resources/extensions/gsd/json-persistence.ts +67 -0
  161. package/src/resources/extensions/gsd/metrics.ts +17 -31
  162. package/src/resources/extensions/gsd/paths.ts +0 -8
  163. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/workflow-start.md +28 -0
  165. package/src/resources/extensions/gsd/queue-order.ts +10 -11
  166. package/src/resources/extensions/gsd/routing-history.ts +13 -17
  167. package/src/resources/extensions/gsd/session-lock.ts +284 -0
  168. package/src/resources/extensions/gsd/session-status-io.ts +23 -41
  169. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  170. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  171. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  172. package/src/resources/extensions/gsd/tests/extension-selector-separator.test.ts +60 -38
  173. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  174. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  175. package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  176. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  177. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  178. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  179. package/src/resources/extensions/gsd/tests/workflow-templates.test.ts +173 -0
  180. package/src/resources/extensions/gsd/types.ts +1 -0
  181. package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
  182. package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
  183. package/src/resources/extensions/gsd/verification-gate.ts +13 -2
  184. package/src/resources/extensions/gsd/workflow-templates/bugfix.md +87 -0
  185. package/src/resources/extensions/gsd/workflow-templates/dep-upgrade.md +74 -0
  186. package/src/resources/extensions/gsd/workflow-templates/full-project.md +41 -0
  187. package/src/resources/extensions/gsd/workflow-templates/hotfix.md +45 -0
  188. package/src/resources/extensions/gsd/workflow-templates/refactor.md +83 -0
  189. package/src/resources/extensions/gsd/workflow-templates/registry.json +85 -0
  190. package/src/resources/extensions/gsd/workflow-templates/security-audit.md +73 -0
  191. package/src/resources/extensions/gsd/workflow-templates/small-feature.md +81 -0
  192. package/src/resources/extensions/gsd/workflow-templates/spike.md +69 -0
  193. package/src/resources/extensions/gsd/workflow-templates.ts +241 -0
  194. package/src/resources/extensions/mcp-client/index.ts +459 -0
  195. package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  196. package/src/resources/extensions/remote-questions/http-client.ts +76 -0
  197. package/src/resources/extensions/remote-questions/notify.ts +1 -2
  198. package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  199. package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  200. package/src/resources/extensions/remote-questions/types.ts +3 -0
  201. package/src/resources/extensions/shared/mod.ts +3 -0
  202. package/src/resources/skills/create-gsd-extension/SKILL.md +87 -0
  203. package/src/resources/skills/create-gsd-extension/references/compaction-session-control.md +77 -0
  204. package/src/resources/skills/create-gsd-extension/references/custom-commands.md +139 -0
  205. package/src/resources/skills/create-gsd-extension/references/custom-rendering.md +108 -0
  206. package/src/resources/skills/create-gsd-extension/references/custom-tools.md +183 -0
  207. package/src/resources/skills/create-gsd-extension/references/custom-ui.md +490 -0
  208. package/src/resources/skills/create-gsd-extension/references/events-reference.md +126 -0
  209. package/src/resources/skills/create-gsd-extension/references/extension-lifecycle.md +64 -0
  210. package/src/resources/skills/create-gsd-extension/references/extensionapi-reference.md +75 -0
  211. package/src/resources/skills/create-gsd-extension/references/extensioncontext-reference.md +53 -0
  212. package/src/resources/skills/create-gsd-extension/references/key-rules-gotchas.md +36 -0
  213. package/src/resources/skills/create-gsd-extension/references/mode-behavior.md +32 -0
  214. package/src/resources/skills/create-gsd-extension/references/model-provider-management.md +89 -0
  215. package/src/resources/skills/create-gsd-extension/references/packaging-distribution.md +55 -0
  216. package/src/resources/skills/create-gsd-extension/references/remote-execution-overrides.md +90 -0
  217. package/src/resources/skills/create-gsd-extension/references/state-management.md +70 -0
  218. package/src/resources/skills/create-gsd-extension/references/system-prompt-modification.md +52 -0
  219. package/src/resources/skills/create-gsd-extension/templates/extension-skeleton.ts +51 -0
  220. package/src/resources/skills/create-gsd-extension/templates/stateful-tool-skeleton.ts +143 -0
  221. package/src/resources/skills/create-gsd-extension/workflows/add-capability.md +57 -0
  222. package/src/resources/skills/create-gsd-extension/workflows/create-extension.md +156 -0
  223. package/src/resources/skills/create-gsd-extension/workflows/debug-extension.md +74 -0
  224. package/src/resources/skills/create-skill/SKILL.md +184 -0
  225. package/src/resources/skills/create-skill/references/api-security.md +226 -0
  226. package/src/resources/skills/create-skill/references/be-clear-and-direct.md +531 -0
  227. package/src/resources/skills/create-skill/references/common-patterns.md +595 -0
  228. package/src/resources/skills/create-skill/references/core-principles.md +437 -0
  229. package/src/resources/skills/create-skill/references/executable-code.md +175 -0
  230. package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +68 -0
  231. package/src/resources/skills/create-skill/references/iteration-and-testing.md +474 -0
  232. package/src/resources/skills/create-skill/references/recommended-structure.md +168 -0
  233. package/src/resources/skills/create-skill/references/skill-structure.md +372 -0
  234. package/src/resources/skills/create-skill/references/use-xml-tags.md +466 -0
  235. package/src/resources/skills/create-skill/references/using-scripts.md +113 -0
  236. package/src/resources/skills/create-skill/references/using-templates.md +112 -0
  237. package/src/resources/skills/create-skill/references/workflows-and-validation.md +510 -0
  238. package/src/resources/skills/create-skill/templates/router-skill.md +73 -0
  239. package/src/resources/skills/create-skill/templates/simple-skill.md +33 -0
  240. package/src/resources/skills/create-skill/workflows/add-reference.md +96 -0
  241. package/src/resources/skills/create-skill/workflows/add-script.md +93 -0
  242. package/src/resources/skills/create-skill/workflows/add-template.md +74 -0
  243. package/src/resources/skills/create-skill/workflows/add-workflow.md +120 -0
  244. package/src/resources/skills/create-skill/workflows/audit-skill.md +148 -0
  245. package/src/resources/skills/create-skill/workflows/create-new-skill.md +196 -0
  246. package/src/resources/skills/create-skill/workflows/get-guidance.md +121 -0
  247. package/src/resources/skills/create-skill/workflows/upgrade-to-router.md +161 -0
  248. package/src/resources/skills/create-skill/workflows/verify-skill.md +204 -0
  249. package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
  250. package/dist/resources/extensions/mcporter/index.ts +0 -525
  251. package/dist/resources/extensions/shared/progress-widget.ts +0 -282
  252. package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
  253. package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
  254. package/src/resources/extensions/mcporter/index.ts +0 -525
  255. package/src/resources/extensions/shared/progress-widget.ts +0 -282
  256. package/src/resources/extensions/shared/thinking-widget.ts +0 -107
@@ -1,4 +1,6 @@
1
- // Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent.
1
+ // Tests for the SEPARATOR_PREFIX convention used by ExtensionSelectorComponent
2
+ // and the two-step provider→model picker in configureModels.
3
+ //
2
4
  // We cannot import the component directly in node:test because its transitive
3
5
  // dependency (countdown-timer.ts) uses TypeScript parameter properties which
4
6
  // are unsupported under --experimental-strip-types. Instead we duplicate the
@@ -69,16 +71,17 @@ describe("separator detection", () => {
69
71
  });
70
72
  });
71
73
 
72
- describe("model grouping", () => {
73
- test("groups models by provider with separator headers", () => {
74
- // Simulate the grouping logic from configureModels
75
- const availableModels = [
76
- { id: "claude-opus-4-6", provider: "anthropic" },
77
- { id: "gpt-4o", provider: "openai" },
78
- { id: "claude-sonnet-4-5", provider: "anthropic" },
79
- { id: "o3-mini", provider: "openai" },
80
- ];
74
+ describe("two-step provider→model picker", () => {
75
+ // Simulate the grouping logic from configureModels
76
+ const availableModels = [
77
+ { id: "claude-opus-4-6", provider: "anthropic" },
78
+ { id: "gpt-4o", provider: "openai" },
79
+ { id: "claude-sonnet-4-5", provider: "anthropic" },
80
+ { id: "o3-mini", provider: "openai" },
81
+ { id: "claude-haiku-4-5", provider: "anthropic" },
82
+ ];
81
83
 
84
+ function buildProviderGroups() {
82
85
  const byProvider = new Map<string, typeof availableModels>();
83
86
  for (const m of availableModels) {
84
87
  let group = byProvider.get(m.provider);
@@ -89,34 +92,53 @@ describe("model grouping", () => {
89
92
  group.push(m);
90
93
  }
91
94
  const providers = Array.from(byProvider.keys()).sort((a, b) => a.localeCompare(b));
92
-
93
- const modelOptions: string[] = [];
94
- for (const provider of providers) {
95
- const group = byProvider.get(provider)!;
96
- modelOptions.push(`${SEPARATOR_PREFIX} ${provider} (${group.length}) ${SEPARATOR_PREFIX}`);
97
- for (const m of group) {
98
- modelOptions.push(`${m.id} · ${m.provider}`);
99
- }
95
+ for (const group of byProvider.values()) {
96
+ group.sort((a, b) => a.id.localeCompare(b.id));
100
97
  }
101
- modelOptions.push("(keep current)", "(clear)");
102
-
103
- // Verify structure
104
- assert.strictEqual(modelOptions[0], `${SEPARATOR_PREFIX} anthropic (2) ${SEPARATOR_PREFIX}`);
105
- assert.strictEqual(modelOptions[1], "claude-opus-4-6 · anthropic");
106
- assert.strictEqual(modelOptions[2], "claude-sonnet-4-5 · anthropic");
107
- assert.strictEqual(modelOptions[3], `${SEPARATOR_PREFIX} openai (2) ${SEPARATOR_PREFIX}`);
108
- assert.strictEqual(modelOptions[4], "gpt-4o · openai");
109
- assert.strictEqual(modelOptions[5], "o3-mini · openai");
110
- assert.strictEqual(modelOptions[6], "(keep current)");
111
- assert.strictEqual(modelOptions[7], "(clear)");
112
-
113
- // Verify separators are correctly detected
114
- assert.ok(isSeparator(modelOptions, 0));
115
- assert.ok(!isSeparator(modelOptions, 1));
116
- assert.ok(isSeparator(modelOptions, 3));
117
- assert.ok(!isSeparator(modelOptions, 6));
118
-
119
- // Verify first selectable is index 1, not the separator at 0
120
- assert.strictEqual(nextSelectable(modelOptions, 0, 1), 1);
98
+ return { byProvider, providers };
99
+ }
100
+
101
+ test("provider menu lists providers with model counts", () => {
102
+ const { providers, byProvider } = buildProviderGroups();
103
+ const providerOptions = providers.map(p => {
104
+ const count = byProvider.get(p)!.length;
105
+ return `${p} (${count} models)`;
106
+ });
107
+ providerOptions.push("(keep current)", "(clear)", "(type manually)");
108
+
109
+ assert.strictEqual(providerOptions[0], "anthropic (3 models)");
110
+ assert.strictEqual(providerOptions[1], "openai (2 models)");
111
+ assert.strictEqual(providerOptions[2], "(keep current)");
112
+ assert.strictEqual(providerOptions[3], "(clear)");
113
+ assert.strictEqual(providerOptions[4], "(type manually)");
114
+ });
115
+
116
+ test("model menu for a provider is sorted alphabetically", () => {
117
+ const { byProvider } = buildProviderGroups();
118
+ const anthropicModels = byProvider.get("anthropic")!;
119
+ const modelOptions = anthropicModels.map(m => m.id);
120
+
121
+ assert.strictEqual(modelOptions[0], "claude-haiku-4-5");
122
+ assert.strictEqual(modelOptions[1], "claude-opus-4-6");
123
+ assert.strictEqual(modelOptions[2], "claude-sonnet-4-5");
124
+ });
125
+
126
+ test("provider name is extracted correctly from choice string", () => {
127
+ const choice = "anthropic (3 models)";
128
+ const providerName = choice.replace(/ \(\d+ models?\)$/, "");
129
+ assert.strictEqual(providerName, "anthropic");
130
+
131
+ const singleChoice = "ollama (1 model)";
132
+ const singleProvider = singleChoice.replace(/ \(\d+ models?\)$/, "");
133
+ assert.strictEqual(singleProvider, "ollama");
134
+ });
135
+
136
+ test("openai models are sorted within their group", () => {
137
+ const { byProvider } = buildProviderGroups();
138
+ const openaiModels = byProvider.get("openai")!;
139
+ const modelOptions = openaiModels.map(m => m.id);
140
+
141
+ assert.strictEqual(modelOptions[0], "gpt-4o");
142
+ assert.strictEqual(modelOptions[1], "o3-mini");
121
143
  });
122
144
  });
@@ -3,7 +3,7 @@
3
3
  // Tests the pure formatInspectOutput function with known data.
4
4
 
5
5
  import { createTestContext } from './test-helpers.ts';
6
- import { formatInspectOutput, type InspectData } from '../commands.ts';
6
+ import { formatInspectOutput, type InspectData } from '../commands-inspect.ts';
7
7
 
8
8
  const { assertEq, assertTrue, assertMatch, report } = createTestContext();
9
9
 
@@ -30,7 +30,7 @@ import {
30
30
  getBudgetAlertLevel,
31
31
  getNewBudgetAlertLevel,
32
32
  getBudgetEnforcementAction,
33
- } from '../auto.ts';
33
+ } from '../auto-budget.ts';
34
34
  import {
35
35
  type UnitMetrics,
36
36
  type MetricsLedger,
@@ -0,0 +1,315 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ import {
8
+ acquireSessionLock,
9
+ releaseSessionLock,
10
+ updateSessionLock,
11
+ validateSessionLock,
12
+ readSessionLockData,
13
+ isSessionLockHeld,
14
+ isSessionLockProcessAlive,
15
+ } from "../session-lock.ts";
16
+
17
+ // ─── acquireSessionLock ──────────────────────────────────────────────────
18
+
19
+ test("acquireSessionLock succeeds on empty directory", () => {
20
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
21
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
22
+
23
+ const result = acquireSessionLock(dir);
24
+ assert.equal(result.acquired, true, "should acquire lock on empty dir");
25
+
26
+ // Verify lock file was created with correct data
27
+ const lockPath = join(dir, ".gsd", "auto.lock");
28
+ assert.ok(existsSync(lockPath), "auto.lock should exist after acquire");
29
+
30
+ const data = JSON.parse(readFileSync(lockPath, "utf-8"));
31
+ assert.equal(data.pid, process.pid, "lock should contain current PID");
32
+ assert.equal(data.unitType, "starting", "initial unit type should be 'starting'");
33
+
34
+ releaseSessionLock(dir);
35
+ rmSync(dir, { recursive: true, force: true });
36
+ });
37
+
38
+ test("acquireSessionLock rejects when another live process holds lock", () => {
39
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
40
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
41
+
42
+ // Simulate another process holding the lock by writing a lock with parent PID
43
+ const fakeLockData = {
44
+ pid: process.ppid,
45
+ startedAt: new Date().toISOString(),
46
+ unitType: "execute-task",
47
+ unitId: "M001/S01/T01",
48
+ unitStartedAt: new Date().toISOString(),
49
+ completedUnits: 2,
50
+ };
51
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(fakeLockData, null, 2));
52
+
53
+ // First acquire to set up proper-lockfile state
54
+ const result1 = acquireSessionLock(dir);
55
+
56
+ // If proper-lockfile is available, it should manage the OS lock.
57
+ // If not (fallback mode), the PID check should detect the live process.
58
+ // Either way, we can't fully simulate another process holding an OS lock
59
+ // from within the same process, so we test the fallback path.
60
+ if (result1.acquired) {
61
+ // We got the lock (proper-lockfile saw no OS lock from another process)
62
+ // This is expected since we're in the same process
63
+ releaseSessionLock(dir);
64
+ }
65
+
66
+ rmSync(dir, { recursive: true, force: true });
67
+ });
68
+
69
+ test("acquireSessionLock takes over stale lock from dead process", () => {
70
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
71
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
72
+
73
+ // Write a lock from a dead process
74
+ const staleLockData = {
75
+ pid: 9999999,
76
+ startedAt: "2026-03-01T00:00:00Z",
77
+ unitType: "execute-task",
78
+ unitId: "M001/S01/T01",
79
+ unitStartedAt: "2026-03-01T00:00:00Z",
80
+ completedUnits: 0,
81
+ };
82
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(staleLockData, null, 2));
83
+
84
+ const result = acquireSessionLock(dir);
85
+ assert.equal(result.acquired, true, "should take over lock from dead process");
86
+
87
+ // Verify our PID is now in the lock
88
+ const data = readSessionLockData(dir);
89
+ assert.ok(data, "lock data should exist after acquire");
90
+ assert.equal(data!.pid, process.pid, "lock should contain our PID now");
91
+
92
+ releaseSessionLock(dir);
93
+ rmSync(dir, { recursive: true, force: true });
94
+ });
95
+
96
+ // ─── releaseSessionLock ─────────────────────────────────────────────────
97
+
98
+ test("releaseSessionLock removes the lock file", () => {
99
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
100
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
101
+
102
+ const result = acquireSessionLock(dir);
103
+ assert.equal(result.acquired, true);
104
+
105
+ releaseSessionLock(dir);
106
+
107
+ const lockPath = join(dir, ".gsd", "auto.lock");
108
+ assert.ok(!existsSync(lockPath), "auto.lock should be removed after release");
109
+
110
+ rmSync(dir, { recursive: true, force: true });
111
+ });
112
+
113
+ test("releaseSessionLock is safe when no lock exists", () => {
114
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
115
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
116
+
117
+ // Should not throw
118
+ releaseSessionLock(dir);
119
+
120
+ rmSync(dir, { recursive: true, force: true });
121
+ });
122
+
123
+ // ─── updateSessionLock ──────────────────────────────────────────────────
124
+
125
+ test("updateSessionLock updates the lock data without re-acquiring", () => {
126
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
127
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
128
+
129
+ const result = acquireSessionLock(dir);
130
+ assert.equal(result.acquired, true);
131
+
132
+ updateSessionLock(dir, "execute-task", "M001/S01/T02", 3, "/tmp/session.jsonl");
133
+
134
+ const data = readSessionLockData(dir);
135
+ assert.ok(data, "lock data should exist after update");
136
+ assert.equal(data!.pid, process.pid, "PID should still be ours");
137
+ assert.equal(data!.unitType, "execute-task", "unit type should be updated");
138
+ assert.equal(data!.unitId, "M001/S01/T02", "unit ID should be updated");
139
+ assert.equal(data!.completedUnits, 3, "completed count should be updated");
140
+ assert.equal(data!.sessionFile, "/tmp/session.jsonl", "session file should be recorded");
141
+
142
+ releaseSessionLock(dir);
143
+ rmSync(dir, { recursive: true, force: true });
144
+ });
145
+
146
+ // ─── validateSessionLock ────────────────────────────────────────────────
147
+
148
+ test("validateSessionLock returns true when we hold the lock", () => {
149
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
150
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
151
+
152
+ const result = acquireSessionLock(dir);
153
+ assert.equal(result.acquired, true);
154
+
155
+ assert.equal(validateSessionLock(dir), true, "should validate when we hold the lock");
156
+
157
+ releaseSessionLock(dir);
158
+ rmSync(dir, { recursive: true, force: true });
159
+ });
160
+
161
+ test("validateSessionLock returns false after release", () => {
162
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
163
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
164
+
165
+ const result = acquireSessionLock(dir);
166
+ assert.equal(result.acquired, true);
167
+ assert.equal(validateSessionLock(dir), true, "should be valid while held");
168
+
169
+ // Release the lock — both OS lock and lock file are removed
170
+ releaseSessionLock(dir);
171
+
172
+ // After release, _lockedPath is cleared and lock file is gone
173
+ assert.equal(isSessionLockHeld(dir), false, "should not be held after release");
174
+
175
+ rmSync(dir, { recursive: true, force: true });
176
+ });
177
+
178
+ test("validateSessionLock returns false when another PID owns the lock", () => {
179
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
180
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
181
+
182
+ // Write lock data with a different PID (parent process)
183
+ const foreignLockData = {
184
+ pid: process.ppid,
185
+ startedAt: new Date().toISOString(),
186
+ unitType: "execute-task",
187
+ unitId: "M001/S01/T01",
188
+ unitStartedAt: new Date().toISOString(),
189
+ completedUnits: 0,
190
+ };
191
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(foreignLockData, null, 2));
192
+
193
+ // Without holding the OS lock, validate should check PID
194
+ assert.equal(validateSessionLock(dir), false, "should fail when another PID owns lock");
195
+
196
+ rmSync(dir, { recursive: true, force: true });
197
+ });
198
+
199
+ // ─── isSessionLockHeld ──────────────────────────────────────────────────
200
+
201
+ test("isSessionLockHeld returns true after acquire", () => {
202
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
203
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
204
+
205
+ acquireSessionLock(dir);
206
+ assert.equal(isSessionLockHeld(dir), true);
207
+
208
+ releaseSessionLock(dir);
209
+ assert.equal(isSessionLockHeld(dir), false, "should return false after release");
210
+
211
+ rmSync(dir, { recursive: true, force: true });
212
+ });
213
+
214
+ // ─── isSessionLockProcessAlive ──────────────────────────────────────────
215
+
216
+ test("isSessionLockProcessAlive returns false for dead PID", () => {
217
+ const data = {
218
+ pid: 9999999,
219
+ startedAt: new Date().toISOString(),
220
+ unitType: "starting",
221
+ unitId: "bootstrap",
222
+ unitStartedAt: new Date().toISOString(),
223
+ completedUnits: 0,
224
+ };
225
+ assert.equal(isSessionLockProcessAlive(data), false);
226
+ });
227
+
228
+ test("isSessionLockProcessAlive returns false for own PID (recycled)", () => {
229
+ const data = {
230
+ pid: process.pid,
231
+ startedAt: new Date().toISOString(),
232
+ unitType: "starting",
233
+ unitId: "bootstrap",
234
+ unitStartedAt: new Date().toISOString(),
235
+ completedUnits: 0,
236
+ };
237
+ // Own PID returns false because it means the lock is from a recycled PID
238
+ assert.equal(isSessionLockProcessAlive(data), false);
239
+ });
240
+
241
+ // ─── readSessionLockData ────────────────────────────────────────────────
242
+
243
+ test("readSessionLockData returns null when no lock exists", () => {
244
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
245
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
246
+
247
+ const data = readSessionLockData(dir);
248
+ assert.equal(data, null);
249
+
250
+ rmSync(dir, { recursive: true, force: true });
251
+ });
252
+
253
+ test("readSessionLockData reads existing lock data", () => {
254
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
255
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
256
+
257
+ const lockData = {
258
+ pid: 12345,
259
+ startedAt: "2026-03-18T00:00:00Z",
260
+ unitType: "execute-task",
261
+ unitId: "M001/S01/T01",
262
+ unitStartedAt: "2026-03-18T00:01:00Z",
263
+ completedUnits: 2,
264
+ sessionFile: "/tmp/session.jsonl",
265
+ };
266
+ writeFileSync(join(dir, ".gsd", "auto.lock"), JSON.stringify(lockData, null, 2));
267
+
268
+ const data = readSessionLockData(dir);
269
+ assert.ok(data, "should read lock data");
270
+ assert.equal(data!.pid, 12345);
271
+ assert.equal(data!.unitType, "execute-task");
272
+ assert.equal(data!.unitId, "M001/S01/T01");
273
+ assert.equal(data!.completedUnits, 2);
274
+ assert.equal(data!.sessionFile, "/tmp/session.jsonl");
275
+
276
+ rmSync(dir, { recursive: true, force: true });
277
+ });
278
+
279
+ // ─── Acquire → Release → Re-Acquire lifecycle ──────────────────────────
280
+
281
+ test("session lock supports acquire → release → re-acquire cycle", () => {
282
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
283
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
284
+
285
+ // First acquire
286
+ const r1 = acquireSessionLock(dir);
287
+ assert.equal(r1.acquired, true, "first acquire should succeed");
288
+ assert.equal(isSessionLockHeld(dir), true);
289
+
290
+ // Release
291
+ releaseSessionLock(dir);
292
+ assert.equal(isSessionLockHeld(dir), false);
293
+
294
+ // Re-acquire
295
+ const r2 = acquireSessionLock(dir);
296
+ assert.equal(r2.acquired, true, "re-acquire after release should succeed");
297
+ assert.equal(isSessionLockHeld(dir), true);
298
+
299
+ releaseSessionLock(dir);
300
+ rmSync(dir, { recursive: true, force: true });
301
+ });
302
+
303
+ // ─── Lock creates .gsd/ directory if needed ─────────────────────────────
304
+
305
+ test("acquireSessionLock creates .gsd/ if it does not exist", () => {
306
+ const dir = mkdtempSync(join(tmpdir(), "gsd-session-lock-"));
307
+ // Do NOT create .gsd/ — let the lock function do it
308
+
309
+ const result = acquireSessionLock(dir);
310
+ assert.equal(result.acquired, true, "should succeed even without .gsd/");
311
+ assert.ok(existsSync(join(dir, ".gsd")), ".gsd/ should be created");
312
+
313
+ releaseSessionLock(dir);
314
+ rmSync(dir, { recursive: true, force: true });
315
+ });
@@ -290,6 +290,61 @@ test("verifyExpectedArtifact fails when VALIDATION.md is missing", () => {
290
290
  }
291
291
  });
292
292
 
293
+ test("verifyExpectedArtifact rejects VALIDATION with missing frontmatter", () => {
294
+ const base = makeTmpBase();
295
+ try {
296
+ // A VALIDATION file without frontmatter should be treated as incomplete —
297
+ // matching what deriveState expects. Without this, the artifact check passes
298
+ // but deriveState still returns validating-milestone, causing the hard skip loop.
299
+ writeValidation(base, "M001", "# Validation\nNo frontmatter here.");
300
+ clearPathCache();
301
+ clearParseCache();
302
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
303
+ assert.equal(result, false, "VALIDATION without frontmatter should fail verification");
304
+ } finally {
305
+ cleanup(base);
306
+ }
307
+ });
308
+
309
+ test("verifyExpectedArtifact rejects VALIDATION with missing verdict field", () => {
310
+ const base = makeTmpBase();
311
+ try {
312
+ writeValidation(base, "M001", "---\nremediation_round: 0\n---\n\n# Validation");
313
+ clearPathCache();
314
+ clearParseCache();
315
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
316
+ assert.equal(result, false, "VALIDATION without verdict field should fail verification");
317
+ } finally {
318
+ cleanup(base);
319
+ }
320
+ });
321
+
322
+ test("verifyExpectedArtifact rejects VALIDATION with unrecognized verdict", () => {
323
+ const base = makeTmpBase();
324
+ try {
325
+ writeValidation(base, "M001", "---\nverdict: unknown-value\nremediation_round: 0\n---\n\n# Validation");
326
+ clearPathCache();
327
+ clearParseCache();
328
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
329
+ assert.equal(result, false, "VALIDATION with unrecognized verdict should fail verification");
330
+ } finally {
331
+ cleanup(base);
332
+ }
333
+ });
334
+
335
+ test("verifyExpectedArtifact passes VALIDATION with needs-attention verdict", () => {
336
+ const base = makeTmpBase();
337
+ try {
338
+ writeValidation(base, "M001", "---\nverdict: needs-attention\nremediation_round: 0\n---\n\n# Validation\nNeeds attention.");
339
+ clearPathCache();
340
+ clearParseCache();
341
+ const result = verifyExpectedArtifact("validate-milestone", "M001", base);
342
+ assert.equal(result, true, "VALIDATION with needs-attention verdict should pass verification");
343
+ } finally {
344
+ cleanup(base);
345
+ }
346
+ });
347
+
293
348
  // ─── diagnoseExpectedArtifact ─────────────────────────────────────────────
294
349
 
295
350
  test("diagnoseExpectedArtifact returns validation path for validate-milestone", () => {