gsd-pi 2.41.0-dev.0acbce9 → 2.41.0-dev.5a170d0

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 (291) hide show
  1. package/README.md +1 -1
  2. package/dist/cli-web-branch.d.ts +6 -0
  3. package/dist/cli-web-branch.js +17 -0
  4. package/dist/onboarding.js +2 -1
  5. package/dist/resources/extensions/gsd/auto/loop.js +89 -1
  6. package/dist/resources/extensions/gsd/auto/phases.js +28 -10
  7. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  8. package/dist/resources/extensions/gsd/auto-dashboard.js +8 -2
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +19 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +7 -0
  11. package/dist/resources/extensions/gsd/auto-recovery.js +12 -4
  12. package/dist/resources/extensions/gsd/auto-start.js +8 -3
  13. package/dist/resources/extensions/gsd/auto-worktree.js +147 -13
  14. package/dist/resources/extensions/gsd/auto.js +64 -2
  15. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +199 -164
  16. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +62 -0
  17. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +2 -0
  18. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +16 -0
  19. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  20. package/dist/resources/extensions/gsd/commands/catalog.js +40 -1
  21. package/dist/resources/extensions/gsd/commands/handlers/core.js +1 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/ops.js +5 -0
  23. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  24. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  25. package/dist/resources/extensions/gsd/context-store.js +4 -3
  26. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  27. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  28. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  29. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  30. package/dist/resources/extensions/gsd/db-writer.js +5 -2
  31. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  32. package/dist/resources/extensions/gsd/detection.js +1 -1
  33. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  34. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  35. package/dist/resources/extensions/gsd/doctor.js +11 -1
  36. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  37. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  38. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  39. package/dist/resources/extensions/gsd/exit-command.js +12 -2
  40. package/dist/resources/extensions/gsd/export.js +9 -13
  41. package/dist/resources/extensions/gsd/extension-manifest.json +2 -2
  42. package/dist/resources/extensions/gsd/files.js +28 -11
  43. package/dist/resources/extensions/gsd/forensics.js +10 -3
  44. package/dist/resources/extensions/gsd/git-service.js +5 -1
  45. package/dist/resources/extensions/gsd/graph.js +225 -0
  46. package/dist/resources/extensions/gsd/gsd-db.js +25 -8
  47. package/dist/resources/extensions/gsd/guided-flow-queue.js +1 -1
  48. package/dist/resources/extensions/gsd/guided-flow.js +7 -3
  49. package/dist/resources/extensions/gsd/journal.js +85 -0
  50. package/dist/resources/extensions/gsd/md-importer.js +5 -0
  51. package/dist/resources/extensions/gsd/milestone-ids.js +1 -1
  52. package/dist/resources/extensions/gsd/native-git-bridge.js +2 -2
  53. package/dist/resources/extensions/gsd/post-unit-hooks.js +24 -412
  54. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  55. package/dist/resources/extensions/gsd/preferences.js +1 -0
  56. package/dist/resources/extensions/gsd/prompt-loader.js +34 -4
  57. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  58. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  59. package/dist/resources/extensions/gsd/prompts/discuss.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/queue.md +1 -1
  61. package/dist/resources/extensions/gsd/repo-identity.js +46 -2
  62. package/dist/resources/extensions/gsd/rule-registry.js +489 -0
  63. package/dist/resources/extensions/gsd/rule-types.js +6 -0
  64. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  65. package/dist/resources/extensions/gsd/service-tier.js +138 -0
  66. package/dist/resources/extensions/gsd/structured-data-formatter.js +2 -1
  67. package/dist/resources/extensions/gsd/templates/decisions.md +2 -2
  68. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  69. package/dist/resources/extensions/gsd/workflow-templates.js +13 -1
  70. package/dist/resources/extensions/gsd/worktree-manager.js +20 -6
  71. package/dist/resources/extensions/gsd/worktree-resolver.js +19 -2
  72. package/dist/resources/extensions/subagent/index.js +7 -3
  73. package/dist/resources/extensions/voice/index.js +4 -4
  74. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  75. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  76. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  77. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  78. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  79. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  80. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  81. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  82. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  83. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  84. package/dist/web/standalone/.next/BUILD_ID +1 -1
  85. package/dist/web/standalone/.next/app-path-routes-manifest.json +16 -16
  86. package/dist/web/standalone/.next/build-manifest.json +3 -3
  87. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  88. package/dist/web/standalone/.next/react-loadable-manifest.json +1 -1
  89. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  90. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  91. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  92. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  93. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  94. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  95. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  96. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  97. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  98. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  99. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  100. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  101. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  102. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  103. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  104. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  105. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  109. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  111. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  113. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  115. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  120. package/dist/web/standalone/.next/server/app/index.html +1 -1
  121. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  122. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  123. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  124. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  125. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  126. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  127. package/dist/web/standalone/.next/server/app-paths-manifest.json +16 -16
  128. package/dist/web/standalone/.next/server/chunks/229.js +3 -3
  129. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  130. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  131. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  132. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  133. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  134. package/dist/web/standalone/.next/static/chunks/4024.c195dc1fdd2adbea.js +9 -0
  135. package/dist/web/standalone/.next/static/chunks/{webpack-9afaaebf6042a1d7.js → webpack-fa307370fcf9fb2c.js} +1 -1
  136. package/dist/web-mode.d.ts +2 -0
  137. package/dist/web-mode.js +29 -7
  138. package/package.json +1 -1
  139. package/packages/native/src/__tests__/text.test.mjs +33 -0
  140. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +3 -1
  141. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -1
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +10 -7
  144. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +4 -2
  149. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +11 -7
  150. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +5 -0
  151. package/src/resources/extensions/gsd/auto/loop-deps.ts +5 -1
  152. package/src/resources/extensions/gsd/auto/loop.ts +101 -1
  153. package/src/resources/extensions/gsd/auto/phases.ts +30 -10
  154. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  155. package/src/resources/extensions/gsd/auto/types.ts +4 -0
  156. package/src/resources/extensions/gsd/auto-dashboard.ts +9 -2
  157. package/src/resources/extensions/gsd/auto-dispatch.ts +25 -5
  158. package/src/resources/extensions/gsd/auto-post-unit.ts +8 -0
  159. package/src/resources/extensions/gsd/auto-recovery.ts +12 -4
  160. package/src/resources/extensions/gsd/auto-start.ts +8 -3
  161. package/src/resources/extensions/gsd/auto-worktree.ts +162 -18
  162. package/src/resources/extensions/gsd/auto.ts +71 -2
  163. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +209 -162
  164. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +62 -0
  165. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +2 -0
  166. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -0
  167. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  168. package/src/resources/extensions/gsd/commands/catalog.ts +40 -1
  169. package/src/resources/extensions/gsd/commands/handlers/core.ts +1 -0
  170. package/src/resources/extensions/gsd/commands/handlers/ops.ts +5 -0
  171. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  172. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  173. package/src/resources/extensions/gsd/context-store.ts +4 -3
  174. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  175. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  176. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  177. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  178. package/src/resources/extensions/gsd/db-writer.ts +6 -2
  179. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  180. package/src/resources/extensions/gsd/detection.ts +1 -1
  181. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  182. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  183. package/src/resources/extensions/gsd/doctor.ts +12 -1
  184. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  185. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  186. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  187. package/src/resources/extensions/gsd/exit-command.ts +14 -2
  188. package/src/resources/extensions/gsd/export.ts +8 -15
  189. package/src/resources/extensions/gsd/extension-manifest.json +2 -2
  190. package/src/resources/extensions/gsd/files.ts +29 -12
  191. package/src/resources/extensions/gsd/forensics.ts +9 -3
  192. package/src/resources/extensions/gsd/git-service.ts +5 -4
  193. package/src/resources/extensions/gsd/graph.ts +312 -0
  194. package/src/resources/extensions/gsd/gsd-db.ts +37 -8
  195. package/src/resources/extensions/gsd/guided-flow-queue.ts +1 -1
  196. package/src/resources/extensions/gsd/guided-flow.ts +7 -3
  197. package/src/resources/extensions/gsd/journal.ts +134 -0
  198. package/src/resources/extensions/gsd/md-importer.ts +6 -0
  199. package/src/resources/extensions/gsd/milestone-ids.ts +1 -1
  200. package/src/resources/extensions/gsd/native-git-bridge.ts +2 -2
  201. package/src/resources/extensions/gsd/post-unit-hooks.ts +24 -462
  202. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  203. package/src/resources/extensions/gsd/preferences.ts +1 -0
  204. package/src/resources/extensions/gsd/prompt-loader.ts +35 -4
  205. package/src/resources/extensions/gsd/prompts/complete-milestone.md +11 -10
  206. package/src/resources/extensions/gsd/prompts/discuss-headless.md +2 -2
  207. package/src/resources/extensions/gsd/prompts/discuss.md +1 -1
  208. package/src/resources/extensions/gsd/prompts/queue.md +1 -1
  209. package/src/resources/extensions/gsd/repo-identity.ts +47 -2
  210. package/src/resources/extensions/gsd/rule-registry.ts +599 -0
  211. package/src/resources/extensions/gsd/rule-types.ts +68 -0
  212. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  213. package/src/resources/extensions/gsd/service-tier.ts +171 -0
  214. package/src/resources/extensions/gsd/structured-data-formatter.ts +3 -1
  215. package/src/resources/extensions/gsd/templates/decisions.md +2 -2
  216. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +103 -120
  217. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +85 -0
  218. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -2
  219. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +202 -0
  220. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  221. package/src/resources/extensions/gsd/tests/captures.test.ts +12 -1
  222. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  223. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  224. package/src/resources/extensions/gsd/tests/context-store.test.ts +10 -5
  225. package/src/resources/extensions/gsd/tests/continue-here.test.ts +20 -20
  226. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  227. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  228. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  229. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  230. package/src/resources/extensions/gsd/tests/db-writer.test.ts +10 -0
  231. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  232. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  233. package/src/resources/extensions/gsd/tests/doctor-completion-deferral.test.ts +15 -10
  234. package/src/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +5 -4
  235. package/src/resources/extensions/gsd/tests/doctor-roadmap-summary-atomicity.test.ts +167 -0
  236. package/src/resources/extensions/gsd/tests/doctor-task-done-missing-summary-slice-loop.test.ts +174 -0
  237. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  238. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  239. package/src/resources/extensions/gsd/tests/exit-command.test.ts +55 -0
  240. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  241. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +8 -1
  242. package/src/resources/extensions/gsd/tests/gsd-tools.test.ts +7 -7
  243. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  244. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +513 -0
  245. package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +147 -0
  246. package/src/resources/extensions/gsd/tests/journal.test.ts +386 -0
  247. package/src/resources/extensions/gsd/tests/md-importer.test.ts +31 -1
  248. package/src/resources/extensions/gsd/tests/memory-store.test.ts +2 -2
  249. package/src/resources/extensions/gsd/tests/milestone-id-reservation.test.ts +1 -1
  250. package/src/resources/extensions/gsd/tests/parsers.test.ts +110 -0
  251. package/src/resources/extensions/gsd/tests/preferences.test.ts +47 -25
  252. package/src/resources/extensions/gsd/tests/prompt-db.test.ts +3 -1
  253. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +61 -1
  254. package/src/resources/extensions/gsd/tests/routing-history.test.ts +11 -22
  255. package/src/resources/extensions/gsd/tests/rule-registry.test.ts +413 -0
  256. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  257. package/src/resources/extensions/gsd/tests/service-tier.test.ts +98 -0
  258. package/src/resources/extensions/gsd/tests/skill-lifecycle.test.ts +2 -2
  259. package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +102 -0
  260. package/src/resources/extensions/gsd/tests/structured-data-formatter.test.ts +4 -3
  261. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  262. package/src/resources/extensions/gsd/tests/tool-naming.test.ts +117 -0
  263. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -1
  264. package/src/resources/extensions/gsd/tests/windows-path-normalization.test.ts +99 -0
  265. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +1 -0
  266. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +4 -0
  267. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +178 -0
  268. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +195 -105
  269. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +78 -3
  270. package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +140 -0
  271. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +74 -0
  272. package/src/resources/extensions/gsd/types.ts +3 -0
  273. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  274. package/src/resources/extensions/gsd/workflow-templates.ts +12 -1
  275. package/src/resources/extensions/gsd/worktree-manager.ts +21 -6
  276. package/src/resources/extensions/gsd/worktree-resolver.ts +30 -9
  277. package/src/resources/extensions/subagent/index.ts +7 -3
  278. package/src/resources/extensions/voice/index.ts +4 -4
  279. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  280. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  281. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  282. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  283. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  284. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  285. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  286. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  287. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  288. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  289. package/dist/web/standalone/.next/static/chunks/4024.279c423e4661ece1.js +0 -9
  290. /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → K7GYOOPvQWX6TKYEKhODM}/_buildManifest.js +0 -0
  291. /package/dist/web/standalone/.next/static/{SwbKZ7JPNFlEmU4f8pKEv → K7GYOOPvQWX6TKYEKhODM}/_ssgManifest.js +0 -0
@@ -0,0 +1,513 @@
1
+ /**
2
+ * journal-integration.test.ts — Integration tests proving that phase functions
3
+ * emit correct journal event sequences with flowId threading, rule provenance,
4
+ * and causedBy references.
5
+ *
6
+ * These tests call the real runDispatch / runUnitPhase / runPreDispatch
7
+ * functions with mock LoopDeps that capture emitJournalEvent calls.
8
+ */
9
+
10
+ import test from "node:test";
11
+ import assert from "node:assert/strict";
12
+ import { randomUUID } from "node:crypto";
13
+ import { join } from "node:path";
14
+
15
+ import type { JournalEntry } from "../journal.js";
16
+ import type { LoopDeps } from "../auto/loop-deps.js";
17
+ import type { IterationContext, LoopState, PreDispatchData, IterationData } from "../auto/types.js";
18
+ import type { SessionLockStatus } from "../session-lock.js";
19
+ import { runDispatch, runUnitPhase, runPreDispatch } from "../auto/phases.js";
20
+
21
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
22
+
23
+ /** Captured journal events from the mock deps. */
24
+ function createEventCapture() {
25
+ const events: JournalEntry[] = [];
26
+ return {
27
+ events,
28
+ emitJournalEvent: (entry: JournalEntry) => { events.push(entry); },
29
+ };
30
+ }
31
+
32
+ /** Minimal mock LoopDeps with journal event capture. */
33
+ function makeMockDeps(
34
+ capture: ReturnType<typeof createEventCapture>,
35
+ overrides?: Partial<LoopDeps>,
36
+ ): LoopDeps {
37
+ const baseDeps: LoopDeps = {
38
+ lockBase: () => "/tmp/test-lock",
39
+ buildSnapshotOpts: () => ({}),
40
+ stopAuto: async () => {},
41
+ pauseAuto: async () => {},
42
+ clearUnitTimeout: () => {},
43
+ updateProgressWidget: () => {},
44
+ syncCmuxSidebar: () => {},
45
+ logCmuxEvent: () => {},
46
+ invalidateAllCaches: () => {},
47
+ deriveState: async () => ({
48
+ phase: "executing",
49
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
50
+ activeSlice: { id: "S01", title: "Slice 1" },
51
+ activeTask: { id: "T01" },
52
+ registry: [{ id: "M001", status: "active" }],
53
+ blockers: [],
54
+ }) as any,
55
+ loadEffectiveGSDPreferences: () => ({ preferences: {} }),
56
+ preDispatchHealthGate: async () => ({ proceed: true, fixesApplied: [] }),
57
+ syncProjectRootToWorktree: () => {},
58
+ checkResourcesStale: () => null,
59
+ validateSessionLock: () => ({ valid: true }) as SessionLockStatus,
60
+ updateSessionLock: () => {},
61
+ handleLostSessionLock: () => {},
62
+ sendDesktopNotification: () => {},
63
+ setActiveMilestoneId: () => {},
64
+ pruneQueueOrder: () => {},
65
+ isInAutoWorktree: () => false,
66
+ shouldUseWorktreeIsolation: () => false,
67
+ mergeMilestoneToMain: () => ({ pushed: false, codeFilesChanged: false }),
68
+ teardownAutoWorktree: () => {},
69
+ createAutoWorktree: () => "/tmp/wt",
70
+ captureIntegrationBranch: () => {},
71
+ getIsolationMode: () => "none",
72
+ getCurrentBranch: () => "main",
73
+ autoWorktreeBranch: () => "auto/M001",
74
+ resolveMilestoneFile: () => null,
75
+ reconcileMergeState: () => false,
76
+ getLedger: () => ({ units: [] }),
77
+ getProjectTotals: () => ({ cost: 0 }),
78
+ formatCost: (c: number) => `$${c.toFixed(2)}`,
79
+ getBudgetAlertLevel: () => 0,
80
+ getNewBudgetAlertLevel: () => 0,
81
+ getBudgetEnforcementAction: () => "none",
82
+ getManifestStatus: async () => null,
83
+ collectSecretsFromManifest: async () => null,
84
+ resolveDispatch: async () => ({
85
+ action: "dispatch" as const,
86
+ unitType: "execute-task",
87
+ unitId: "M001/S01/T01",
88
+ prompt: "do the thing",
89
+ matchedRule: "test-rule-alpha",
90
+ }),
91
+ runPreDispatchHooks: () => ({ firedHooks: [], action: "proceed" }),
92
+ getPriorSliceCompletionBlocker: () => null,
93
+ getMainBranch: () => "main",
94
+ collectObservabilityWarnings: async () => [],
95
+ buildObservabilityRepairBlock: () => null,
96
+ closeoutUnit: async () => {},
97
+ verifyExpectedArtifact: () => true,
98
+ clearUnitRuntimeRecord: () => {},
99
+ writeUnitRuntimeRecord: () => {},
100
+ recordOutcome: () => {},
101
+ writeLock: () => {},
102
+ captureAvailableSkills: () => {},
103
+ ensurePreconditions: () => {},
104
+ updateSliceProgressCache: () => {},
105
+ selectAndApplyModel: async () => ({ routing: null }),
106
+ startUnitSupervision: () => {},
107
+ getDeepDiagnostic: () => null,
108
+ isDbAvailable: () => false,
109
+ reorderForCaching: (p: string) => p,
110
+ existsSync: (p: string) => p.endsWith(".git") || p.endsWith("package.json"),
111
+ readFileSync: () => "",
112
+ atomicWriteSync: () => {},
113
+ GitServiceImpl: class {} as any,
114
+ resolver: {
115
+ get workPath() { return "/tmp/project"; },
116
+ get projectRoot() { return "/tmp/project"; },
117
+ get lockPath() { return "/tmp/project"; },
118
+ enterMilestone: () => {},
119
+ exitMilestone: () => {},
120
+ mergeAndExit: () => {},
121
+ mergeAndEnterNext: () => {},
122
+ } as any,
123
+ postUnitPreVerification: async () => "continue" as const,
124
+ runPostUnitVerification: async () => "continue" as const,
125
+ postUnitPostVerification: async () => "continue" as const,
126
+ getSessionFile: () => "/tmp/session.json",
127
+ rebuildState: async () => {},
128
+ resolveModelId: (id: string, models: any[]) => models.find((m: any) => m.id === id),
129
+ emitJournalEvent: capture.emitJournalEvent,
130
+ };
131
+
132
+ return { ...baseDeps, ...overrides };
133
+ }
134
+
135
+ /** Build a mock IterationContext with real flowId and seqCounter. */
136
+ function makeIC(
137
+ deps: LoopDeps,
138
+ overrides?: Partial<IterationContext>,
139
+ ): IterationContext {
140
+ const flowId = randomUUID();
141
+ let seqCounter = 0;
142
+ return {
143
+ ctx: {
144
+ ui: { notify: () => {}, setStatus: () => {} },
145
+ model: { id: "test-model" },
146
+ modelRegistry: { getAvailable: () => [] },
147
+ } as any,
148
+ pi: {
149
+ sendMessage: () => {},
150
+ setModel: async () => true,
151
+ } as any,
152
+ s: makeSession(),
153
+ deps,
154
+ prefs: undefined,
155
+ iteration: 1,
156
+ flowId,
157
+ nextSeq: () => ++seqCounter,
158
+ ...overrides,
159
+ };
160
+ }
161
+
162
+ /** Minimal mock session for phase calls. */
163
+ function makeSession() {
164
+ return {
165
+ active: true,
166
+ verbose: false,
167
+ stepMode: false,
168
+ paused: false,
169
+ basePath: "/tmp/project",
170
+ originalBasePath: "",
171
+ currentMilestoneId: "M001",
172
+ currentUnit: null,
173
+ currentUnitRouting: null,
174
+ completedUnits: [],
175
+ resourceVersionOnStart: null,
176
+ lastPromptCharCount: undefined,
177
+ lastBaselineCharCount: undefined,
178
+ lastBudgetAlertLevel: 0,
179
+ pendingVerificationRetry: null,
180
+ pendingCrashRecovery: null,
181
+ pendingQuickTasks: [],
182
+ sidecarQueue: [],
183
+ autoModeStartModel: null,
184
+ unitDispatchCount: new Map<string, number>(),
185
+ unitLifetimeDispatches: new Map<string, number>(),
186
+ unitRecoveryCount: new Map<string, number>(),
187
+ verificationRetryCount: new Map<string, number>(),
188
+ gitService: null,
189
+ autoStartTime: Date.now(),
190
+ cmdCtx: {
191
+ newSession: () => Promise.resolve({ cancelled: false }),
192
+ getContextUsage: () => ({ percent: 10, tokens: 1000, limit: 10000 }),
193
+ },
194
+ clearTimers: () => {},
195
+ } as any;
196
+ }
197
+
198
+ // ─── Tests ───────────────────────────────────────────────────────────────────
199
+
200
+ test("runDispatch emits dispatch-match with correct rule and flowId", async () => {
201
+ const capture = createEventCapture();
202
+ const deps = makeMockDeps(capture, {
203
+ resolveDispatch: async () => ({
204
+ action: "dispatch" as const,
205
+ unitType: "execute-task",
206
+ unitId: "M001/S01/T01",
207
+ prompt: "do the thing",
208
+ matchedRule: "slice-task-rule",
209
+ }),
210
+ });
211
+ const ic = makeIC(deps);
212
+ const preData: PreDispatchData = {
213
+ state: {
214
+ phase: "executing",
215
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
216
+ activeSlice: { id: "S01", title: "Slice 1" },
217
+ activeTask: { id: "T01" },
218
+ registry: [{ id: "M001", status: "active" }],
219
+ blockers: [],
220
+ } as any,
221
+ mid: "M001",
222
+ midTitle: "Test Milestone",
223
+ };
224
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
225
+
226
+ const result = await runDispatch(ic, preData, loopState);
227
+
228
+ assert.equal(result.action, "next", "runDispatch should return next for dispatch action");
229
+
230
+ const matchEvents = capture.events.filter(e => e.eventType === "dispatch-match");
231
+ assert.equal(matchEvents.length, 1, "should emit exactly one dispatch-match event");
232
+
233
+ const ev = matchEvents[0];
234
+ assert.equal(ev.flowId, ic.flowId, "dispatch-match event should share the iteration flowId");
235
+ assert.equal(ev.rule, "slice-task-rule", "dispatch-match should carry the matched rule name");
236
+ assert.equal((ev.data as any).unitType, "execute-task");
237
+ assert.equal((ev.data as any).unitId, "M001/S01/T01");
238
+ });
239
+
240
+ test("runDispatch emits dispatch-stop when dispatch returns stop action", async () => {
241
+ const capture = createEventCapture();
242
+ const deps = makeMockDeps(capture, {
243
+ resolveDispatch: async () => ({
244
+ action: "stop" as const,
245
+ reason: "no eligible units",
246
+ level: "info" as const,
247
+ matchedRule: "<no-match>",
248
+ }),
249
+ });
250
+ const ic = makeIC(deps);
251
+ const preData: PreDispatchData = {
252
+ state: { phase: "executing", activeMilestone: { id: "M001" }, registry: [{ id: "M001", status: "active" }], blockers: [] } as any,
253
+ mid: "M001",
254
+ midTitle: "Test",
255
+ };
256
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
257
+
258
+ const result = await runDispatch(ic, preData, loopState);
259
+ assert.equal(result.action, "break");
260
+
261
+ const stopEvents = capture.events.filter(e => e.eventType === "dispatch-stop");
262
+ assert.equal(stopEvents.length, 1);
263
+ assert.equal(stopEvents[0].rule, "<no-match>");
264
+ assert.equal((stopEvents[0].data as any).reason, "no eligible units");
265
+ assert.equal(stopEvents[0].flowId, ic.flowId);
266
+ });
267
+
268
+ test("runUnitPhase emits unit-start and unit-end with causedBy reference", async () => {
269
+ const capture = createEventCapture();
270
+
271
+ // We need runUnit to return immediately — mock it by providing a session
272
+ // whose cmdCtx.newSession resolves immediately and the result is completed.
273
+ // Actually, runUnitPhase calls the real runUnit which creates a pending
274
+ // promise and blocks. We need a different approach.
275
+ //
276
+ // Instead, we test that unit-start is emitted at the right point by examining
277
+ // the event immediately after calling runUnitPhase with a session where
278
+ // newSession resolves quickly, and we resolve the agent_end externally.
279
+ const { resolveAgentEnd, _resetPendingResolve } = await import("../auto-loop.js");
280
+ _resetPendingResolve();
281
+
282
+ const deps = makeMockDeps(capture);
283
+ const ic = makeIC(deps);
284
+ const iterData: IterationData = {
285
+ unitType: "execute-task",
286
+ unitId: "M001/S01/T01",
287
+ prompt: "do stuff",
288
+ finalPrompt: "do stuff",
289
+ pauseAfterUatDispatch: false,
290
+ observabilityIssues: [],
291
+ state: { phase: "executing", activeMilestone: { id: "M001" }, activeSlice: { id: "S01" }, registry: [], blockers: [] } as any,
292
+ mid: "M001",
293
+ midTitle: "Test",
294
+ isRetry: false,
295
+ previousTier: undefined,
296
+ };
297
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0 };
298
+
299
+ // Start runUnitPhase (it will block on runUnit internally)
300
+ const unitPromise = runUnitPhase(ic, iterData, loopState);
301
+
302
+ // Give it time to reach the await inside runUnit
303
+ await new Promise(r => setTimeout(r, 50));
304
+
305
+ // Resolve the agent_end
306
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
307
+
308
+ const result = await unitPromise;
309
+ assert.equal(result.action, "next");
310
+
311
+ // Check unit-start
312
+ const startEvents = capture.events.filter(e => e.eventType === "unit-start");
313
+ assert.equal(startEvents.length, 1, "should emit exactly one unit-start");
314
+ assert.equal(startEvents[0].flowId, ic.flowId);
315
+ assert.equal((startEvents[0].data as any).unitType, "execute-task");
316
+ assert.equal((startEvents[0].data as any).unitId, "M001/S01/T01");
317
+
318
+ // Check unit-end
319
+ const endEvents = capture.events.filter(e => e.eventType === "unit-end");
320
+ assert.equal(endEvents.length, 1, "should emit exactly one unit-end");
321
+ assert.equal(endEvents[0].flowId, ic.flowId);
322
+ assert.equal((endEvents[0].data as any).unitType, "execute-task");
323
+ assert.equal((endEvents[0].data as any).unitId, "M001/S01/T01");
324
+ assert.equal((endEvents[0].data as any).status, "completed");
325
+
326
+ // Verify causedBy: unit-end references unit-start's seq
327
+ assert.ok(endEvents[0].causedBy, "unit-end must have a causedBy reference");
328
+ assert.equal(endEvents[0].causedBy!.flowId, ic.flowId);
329
+ assert.equal(endEvents[0].causedBy!.seq, startEvents[0].seq, "unit-end causedBy.seq must match unit-start.seq");
330
+ });
331
+
332
+ test("all events from a mock iteration have monotonically increasing seq and same flowId", async () => {
333
+ const capture = createEventCapture();
334
+ const { resolveAgentEnd, _resetPendingResolve } = await import("../auto-loop.js");
335
+ _resetPendingResolve();
336
+
337
+ const deps = makeMockDeps(capture, {
338
+ resolveDispatch: async () => ({
339
+ action: "dispatch" as const,
340
+ unitType: "execute-task",
341
+ unitId: "M001/S01/T01",
342
+ prompt: "do the thing",
343
+ matchedRule: "my-rule",
344
+ }),
345
+ });
346
+ const ic = makeIC(deps);
347
+
348
+ // Phase 1: Dispatch
349
+ const preData: PreDispatchData = {
350
+ state: { phase: "executing", activeMilestone: { id: "M001", title: "T", status: "active" }, activeSlice: { id: "S01" }, activeTask: { id: "T01" }, registry: [{ id: "M001", status: "active" }], blockers: [] } as any,
351
+ mid: "M001",
352
+ midTitle: "Test",
353
+ };
354
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
355
+ const dispatchResult = await runDispatch(ic, preData, loopState);
356
+ assert.equal(dispatchResult.action, "next");
357
+
358
+ // Phase 2: Unit execution
359
+ const iterData = (dispatchResult as { action: "next"; data: IterationData }).data;
360
+ const unitPromise = runUnitPhase(ic, iterData, loopState);
361
+ await new Promise(r => setTimeout(r, 50));
362
+ resolveAgentEnd({ messages: [{ role: "assistant" }] });
363
+ await unitPromise;
364
+
365
+ // Verify all events share the same flowId
366
+ assert.ok(capture.events.length >= 3, `expected at least 3 events (dispatch-match, unit-start, unit-end), got ${capture.events.length}`);
367
+ const flowId = ic.flowId;
368
+ for (const ev of capture.events) {
369
+ assert.equal(ev.flowId, flowId, `all events must share flowId=${flowId}, found event ${ev.eventType} with flowId=${ev.flowId}`);
370
+ }
371
+
372
+ // Verify monotonically increasing seq numbers
373
+ for (let i = 1; i < capture.events.length; i++) {
374
+ assert.ok(
375
+ capture.events[i].seq > capture.events[i - 1].seq,
376
+ `seq must be monotonically increasing: event[${i - 1}].seq=${capture.events[i - 1].seq} (${capture.events[i - 1].eventType}) should be less than event[${i}].seq=${capture.events[i].seq} (${capture.events[i].eventType})`,
377
+ );
378
+ }
379
+ });
380
+
381
+ test("dispatch-match events include matchedRule field matching the rule name", async () => {
382
+ const capture = createEventCapture();
383
+ const RULE_NAME = "priority-execution-rule";
384
+ const deps = makeMockDeps(capture, {
385
+ resolveDispatch: async () => ({
386
+ action: "dispatch" as const,
387
+ unitType: "execute-task",
388
+ unitId: "M001/S01/T01",
389
+ prompt: "test",
390
+ matchedRule: RULE_NAME,
391
+ }),
392
+ });
393
+ const ic = makeIC(deps);
394
+ const preData: PreDispatchData = {
395
+ state: { phase: "executing", activeMilestone: { id: "M001", title: "T", status: "active" }, activeSlice: { id: "S01" }, activeTask: { id: "T01" }, registry: [{ id: "M001", status: "active" }], blockers: [] } as any,
396
+ mid: "M001",
397
+ midTitle: "Test",
398
+ };
399
+
400
+ await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0 });
401
+
402
+ const matchEvents = capture.events.filter(e => e.eventType === "dispatch-match");
403
+ assert.equal(matchEvents.length, 1);
404
+ assert.equal(matchEvents[0].rule, RULE_NAME, "dispatch-match event.rule must equal the matchedRule from dispatch result");
405
+ });
406
+
407
+ test("pre-dispatch-hook event is emitted when hooks fire", async () => {
408
+ const capture = createEventCapture();
409
+ const deps = makeMockDeps(capture, {
410
+ resolveDispatch: async () => ({
411
+ action: "dispatch" as const,
412
+ unitType: "execute-task",
413
+ unitId: "M001/S01/T01",
414
+ prompt: "test",
415
+ matchedRule: "some-rule",
416
+ }),
417
+ runPreDispatchHooks: () => ({
418
+ firedHooks: ["observability-check", "lint-gate"],
419
+ action: "proceed",
420
+ }),
421
+ });
422
+ const ic = makeIC(deps);
423
+ const preData: PreDispatchData = {
424
+ state: { phase: "executing", activeMilestone: { id: "M001", title: "T", status: "active" }, activeSlice: { id: "S01" }, activeTask: { id: "T01" }, registry: [{ id: "M001", status: "active" }], blockers: [] } as any,
425
+ mid: "M001",
426
+ midTitle: "Test",
427
+ };
428
+
429
+ await runDispatch(ic, preData, { recentUnits: [], stuckRecoveryAttempts: 0 });
430
+
431
+ const hookEvents = capture.events.filter(e => e.eventType === "pre-dispatch-hook");
432
+ assert.equal(hookEvents.length, 1, "should emit one pre-dispatch-hook event");
433
+ assert.deepEqual((hookEvents[0].data as any).firedHooks, ["observability-check", "lint-gate"]);
434
+ assert.equal((hookEvents[0].data as any).action, "proceed");
435
+ assert.equal(hookEvents[0].flowId, ic.flowId);
436
+ });
437
+
438
+ test("terminal event is emitted on milestone-complete", async () => {
439
+ const capture = createEventCapture();
440
+ const deps = makeMockDeps(capture, {
441
+ deriveState: async () => ({
442
+ phase: "complete",
443
+ activeMilestone: { id: "M001", title: "Test", status: "complete" },
444
+ activeSlice: null,
445
+ activeTask: null,
446
+ registry: [{ id: "M001", status: "complete" }],
447
+ blockers: [],
448
+ }) as any,
449
+ });
450
+ const ic = makeIC(deps);
451
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
452
+
453
+ const result = await runPreDispatch(ic, loopState);
454
+ assert.equal(result.action, "break");
455
+
456
+ const terminalEvents = capture.events.filter(e => e.eventType === "terminal");
457
+ assert.equal(terminalEvents.length, 1, "should emit one terminal event");
458
+ assert.equal((terminalEvents[0].data as any).reason, "milestone-complete");
459
+ assert.equal(terminalEvents[0].flowId, ic.flowId);
460
+ });
461
+
462
+ test("terminal event is emitted on blocked state", async () => {
463
+ const capture = createEventCapture();
464
+ const deps = makeMockDeps(capture, {
465
+ deriveState: async () => ({
466
+ phase: "blocked",
467
+ activeMilestone: { id: "M001", title: "Test", status: "active" },
468
+ activeSlice: null,
469
+ activeTask: null,
470
+ registry: [{ id: "M001", status: "active" }],
471
+ blockers: ["Missing API key"],
472
+ }) as any,
473
+ });
474
+ const ic = makeIC(deps);
475
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
476
+
477
+ const result = await runPreDispatch(ic, loopState);
478
+ assert.equal(result.action, "break");
479
+
480
+ const terminalEvents = capture.events.filter(e => e.eventType === "terminal");
481
+ assert.equal(terminalEvents.length, 1);
482
+ assert.equal((terminalEvents[0].data as any).reason, "blocked");
483
+ assert.deepEqual((terminalEvents[0].data as any).blockers, ["Missing API key"]);
484
+ });
485
+
486
+ test("milestone-transition event is emitted when milestone changes", async () => {
487
+ const capture = createEventCapture();
488
+ const deps = makeMockDeps(capture, {
489
+ deriveState: async () => ({
490
+ phase: "executing",
491
+ activeMilestone: { id: "M002", title: "Next Milestone", status: "active" },
492
+ activeSlice: { id: "S01" },
493
+ activeTask: { id: "T01" },
494
+ registry: [
495
+ { id: "M001", status: "complete" },
496
+ { id: "M002", status: "active" },
497
+ ],
498
+ blockers: [],
499
+ }) as any,
500
+ });
501
+ const ic = makeIC(deps);
502
+ // Session says current milestone is M001, but state will return M002
503
+ ic.s.currentMilestoneId = "M001";
504
+ const loopState: LoopState = { recentUnits: [], stuckRecoveryAttempts: 0 };
505
+
506
+ await runPreDispatch(ic, loopState);
507
+
508
+ const transitionEvents = capture.events.filter(e => e.eventType === "milestone-transition");
509
+ assert.equal(transitionEvents.length, 1, "should emit one milestone-transition event");
510
+ assert.equal((transitionEvents[0].data as any).from, "M001");
511
+ assert.equal((transitionEvents[0].data as any).to, "M002");
512
+ assert.equal(transitionEvents[0].flowId, ic.flowId);
513
+ });
@@ -0,0 +1,147 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { randomUUID } from "node:crypto";
7
+
8
+ import { registerJournalTools } from "../bootstrap/journal-tools.ts";
9
+ import { emitJournalEvent, type JournalEntry } from "../journal.ts";
10
+
11
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
12
+
13
+ function makeMockPi() {
14
+ const tools: any[] = [];
15
+ return {
16
+ registerTool: (tool: any) => tools.push(tool),
17
+ tools,
18
+ } as any;
19
+ }
20
+
21
+ function makeTmpBase(): string {
22
+ const base = join(tmpdir(), `gsd-journal-tool-test-${randomUUID()}`);
23
+ mkdirSync(join(base, ".gsd"), { recursive: true });
24
+ return base;
25
+ }
26
+
27
+ function cleanup(base: string): void {
28
+ try {
29
+ rmSync(base, { recursive: true, force: true });
30
+ } catch {
31
+ /* */
32
+ }
33
+ }
34
+
35
+ function makeEntry(overrides: Partial<JournalEntry> = {}): JournalEntry {
36
+ return {
37
+ ts: "2025-03-21T10:00:00.000Z",
38
+ flowId: "flow-aaa",
39
+ seq: 0,
40
+ eventType: "iteration-start",
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ async function executeToolInDir(tool: any, params: Record<string, unknown>, dir: string) {
46
+ const originalCwd = process.cwd();
47
+ try {
48
+ process.chdir(dir);
49
+ return await tool.execute("test-call-id", params, undefined, undefined, undefined);
50
+ } finally {
51
+ process.chdir(originalCwd);
52
+ }
53
+ }
54
+
55
+ // ─── Registration ─────────────────────────────────────────────────────────────
56
+
57
+ test("registerJournalTools registers gsd_journal_query tool", () => {
58
+ const pi = makeMockPi();
59
+ registerJournalTools(pi);
60
+ assert.equal(pi.tools.length, 1, "Should register exactly one tool");
61
+ assert.equal(pi.tools[0].name, "gsd_journal_query");
62
+ });
63
+
64
+ // ─── Filtering ────────────────────────────────────────────────────────────────
65
+
66
+ test("gsd_journal_query returns filtered entries", async () => {
67
+ const base = makeTmpBase();
68
+ try {
69
+ emitJournalEvent(base, makeEntry({ seq: 0, flowId: "flow-aaa", data: { unitId: "M001/S01/T01" } }));
70
+ emitJournalEvent(base, makeEntry({ seq: 1, flowId: "flow-bbb", data: { unitId: "M001/S01/T02" } }));
71
+ emitJournalEvent(base, makeEntry({ seq: 2, flowId: "flow-aaa", data: { unitId: "M001/S01/T01" } }));
72
+
73
+ const pi = makeMockPi();
74
+ registerJournalTools(pi);
75
+ const tool = pi.tools[0];
76
+
77
+ const result = await executeToolInDir(tool, { unitId: "M001/S01/T01" }, base);
78
+ const entries = JSON.parse(result.content[0].text) as JournalEntry[];
79
+
80
+ assert.equal(entries.length, 2, "Should return 2 entries matching unitId");
81
+ assert.ok(
82
+ entries.every((e: any) => e.data?.unitId === "M001/S01/T01"),
83
+ "All entries should have matching unitId",
84
+ );
85
+ } finally {
86
+ cleanup(base);
87
+ }
88
+ });
89
+
90
+ // ─── Empty Results ────────────────────────────────────────────────────────────
91
+
92
+ test("gsd_journal_query returns 'no entries' message for empty results", async () => {
93
+ const base = makeTmpBase();
94
+ try {
95
+ emitJournalEvent(base, makeEntry({ seq: 0, flowId: "flow-aaa" }));
96
+
97
+ const pi = makeMockPi();
98
+ registerJournalTools(pi);
99
+ const tool = pi.tools[0];
100
+
101
+ const result = await executeToolInDir(tool, { flowId: "nonexistent-flow" }, base);
102
+ assert.equal(result.content[0].text, "No matching journal entries found.");
103
+ } finally {
104
+ cleanup(base);
105
+ }
106
+ });
107
+
108
+ // ─── Limit ────────────────────────────────────────────────────────────────────
109
+
110
+ test("gsd_journal_query respects limit parameter", async () => {
111
+ const base = makeTmpBase();
112
+ try {
113
+ for (let i = 0; i < 5; i++) {
114
+ emitJournalEvent(base, makeEntry({ seq: i }));
115
+ }
116
+
117
+ const pi = makeMockPi();
118
+ registerJournalTools(pi);
119
+ const tool = pi.tools[0];
120
+
121
+ const result = await executeToolInDir(tool, { limit: 2 }, base);
122
+ const entries = JSON.parse(result.content[0].text) as JournalEntry[];
123
+ assert.equal(entries.length, 2, "Should return only 2 entries");
124
+ } finally {
125
+ cleanup(base);
126
+ }
127
+ });
128
+
129
+ // ─── Error Handling ───────────────────────────────────────────────────────────
130
+
131
+ test("gsd_journal_query handles errors gracefully", async () => {
132
+ const pi = makeMockPi();
133
+ registerJournalTools(pi);
134
+ const tool = pi.tools[0];
135
+
136
+ // queryJournal returns [] for missing journal dirs (never throws), so empty
137
+ // result is the expected behavior. This confirms the tool doesn't crash and
138
+ // returns the "no entries" message when there's no journal data.
139
+ const base = join(tmpdir(), `gsd-journal-tool-test-${randomUUID()}`);
140
+ mkdirSync(base, { recursive: true }); // dir must exist for process.chdir
141
+ try {
142
+ const result = await executeToolInDir(tool, {}, base);
143
+ assert.equal(result.content[0].text, "No matching journal entries found.");
144
+ } finally {
145
+ cleanup(base);
146
+ }
147
+ });