gsd-pi 2.41.0-dev.cac69f9 → 2.42.0-dev.1df898f

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 (263) hide show
  1. package/README.md +23 -0
  2. package/dist/cli.js +18 -3
  3. package/dist/loader.js +3 -1
  4. package/dist/resource-loader.js +39 -6
  5. package/dist/resources/extensions/async-jobs/async-bash-tool.js +52 -4
  6. package/dist/resources/extensions/async-jobs/await-tool.js +5 -0
  7. package/dist/resources/extensions/async-jobs/index.js +2 -0
  8. package/dist/resources/extensions/gsd/auto/loop.js +80 -0
  9. package/dist/resources/extensions/gsd/auto/phases.js +3 -5
  10. package/dist/resources/extensions/gsd/auto/session.js +6 -0
  11. package/dist/resources/extensions/gsd/auto-dashboard.js +2 -0
  12. package/dist/resources/extensions/gsd/auto-prompts.js +3 -16
  13. package/dist/resources/extensions/gsd/auto-start.js +8 -11
  14. package/dist/resources/extensions/gsd/auto.js +28 -1
  15. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +11 -5
  16. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +7 -2
  17. package/dist/resources/extensions/gsd/commands/catalog.js +32 -0
  18. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +146 -0
  19. package/dist/resources/extensions/gsd/context-injector.js +74 -0
  20. package/dist/resources/extensions/gsd/custom-execution-policy.js +47 -0
  21. package/dist/resources/extensions/gsd/custom-verification.js +145 -0
  22. package/dist/resources/extensions/gsd/custom-workflow-engine.js +164 -0
  23. package/dist/resources/extensions/gsd/dashboard-overlay.js +1 -0
  24. package/dist/resources/extensions/gsd/definition-loader.js +352 -0
  25. package/dist/resources/extensions/gsd/detection.js +19 -0
  26. package/dist/resources/extensions/gsd/dev-execution-policy.js +24 -0
  27. package/dist/resources/extensions/gsd/dev-workflow-engine.js +82 -0
  28. package/dist/resources/extensions/gsd/doctor-checks.js +31 -1
  29. package/dist/resources/extensions/gsd/doctor-providers.js +10 -0
  30. package/dist/resources/extensions/gsd/engine-resolver.js +40 -0
  31. package/dist/resources/extensions/gsd/engine-types.js +8 -0
  32. package/dist/resources/extensions/gsd/execution-policy.js +8 -0
  33. package/dist/resources/extensions/gsd/forensics.js +84 -0
  34. package/dist/resources/extensions/gsd/git-constants.js +1 -0
  35. package/dist/resources/extensions/gsd/git-service.js +1 -1
  36. package/dist/resources/extensions/gsd/graph.js +225 -0
  37. package/dist/resources/extensions/gsd/native-git-bridge.js +1 -0
  38. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  39. package/dist/resources/extensions/gsd/preferences.js +59 -8
  40. package/dist/resources/extensions/gsd/prompts/forensics.md +12 -5
  41. package/dist/resources/extensions/gsd/repo-identity.js +46 -5
  42. package/dist/resources/extensions/gsd/run-manager.js +134 -0
  43. package/dist/resources/extensions/gsd/service-tier.js +13 -4
  44. package/dist/resources/extensions/gsd/session-lock.js +2 -2
  45. package/dist/resources/extensions/gsd/workflow-engine.js +7 -0
  46. package/dist/resources/extensions/gsd/worktree-resolver.js +2 -2
  47. package/dist/resources/extensions/gsd/worktree.js +2 -2
  48. package/dist/resources/extensions/mcp-client/index.js +2 -1
  49. package/dist/resources/extensions/search-the-web/tool-search.js +3 -3
  50. package/dist/resources/skills/create-workflow/SKILL.md +103 -0
  51. package/dist/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  52. package/dist/resources/skills/create-workflow/references/verification-policies.md +76 -0
  53. package/dist/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  54. package/dist/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  55. package/dist/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  56. package/dist/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  57. package/dist/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  58. package/dist/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  59. package/dist/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  60. package/dist/web/standalone/.next/BUILD_ID +1 -1
  61. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  62. package/dist/web/standalone/.next/build-manifest.json +2 -2
  63. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  64. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  65. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  73. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/index.html +1 -1
  82. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  84. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  89. package/dist/web/standalone/.next/server/chunks/229.js +2 -2
  90. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  91. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  92. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  93. package/dist/web-mode.d.ts +2 -0
  94. package/dist/web-mode.js +40 -4
  95. package/package.json +1 -1
  96. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  97. package/packages/pi-agent-core/dist/agent.js +2 -0
  98. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  99. package/packages/pi-agent-core/dist/types.d.ts +6 -0
  100. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  101. package/packages/pi-agent-core/dist/types.js.map +1 -1
  102. package/packages/pi-agent-core/src/agent.test.ts +53 -0
  103. package/packages/pi-agent-core/src/agent.ts +3 -0
  104. package/packages/pi-agent-core/src/types.ts +6 -0
  105. package/packages/pi-agent-core/tsconfig.json +1 -1
  106. package/packages/pi-ai/dist/models.d.ts +5 -3
  107. package/packages/pi-ai/dist/models.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/models.generated.d.ts +801 -1468
  109. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  110. package/packages/pi-ai/dist/models.generated.js +1135 -1588
  111. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  112. package/packages/pi-ai/dist/models.js.map +1 -1
  113. package/packages/pi-ai/dist/utils/oauth/github-copilot.d.ts.map +1 -1
  114. package/packages/pi-ai/dist/utils/oauth/github-copilot.js +60 -2
  115. package/packages/pi-ai/dist/utils/oauth/github-copilot.js.map +1 -1
  116. package/packages/pi-ai/scripts/generate-models.ts +1543 -0
  117. package/packages/pi-ai/src/models.generated.ts +1140 -1593
  118. package/packages/pi-ai/src/models.ts +7 -4
  119. package/packages/pi-ai/src/utils/oauth/github-copilot.ts +74 -2
  120. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/agent-session.js +8 -1
  122. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +7 -0
  124. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/auth-storage.js +29 -2
  126. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/auth-storage.test.js +60 -0
  128. package/packages/pi-coding-agent/dist/core/auth-storage.test.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/extensions/loader.js +18 -0
  131. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  132. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  133. package/packages/pi-coding-agent/dist/core/lsp/client.js +23 -0
  134. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  135. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/core/model-registry.js +2 -0
  137. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/core/package-manager.d.ts +6 -0
  139. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  140. package/packages/pi-coding-agent/dist/core/package-manager.js +63 -11
  141. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  142. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts +9 -0
  143. package/packages/pi-coding-agent/dist/core/resource-loader.d.ts.map +1 -1
  144. package/packages/pi-coding-agent/dist/core/resource-loader.js +20 -6
  145. package/packages/pi-coding-agent/dist/core/resource-loader.js.map +1 -1
  146. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  147. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -5
  148. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  150. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +3 -0
  151. package/packages/pi-coding-agent/dist/modes/interactive/components/extension-editor.js.map +1 -1
  152. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  153. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +9 -6
  154. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  155. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  156. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +30 -10
  157. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  158. package/packages/pi-coding-agent/package.json +1 -1
  159. package/packages/pi-coding-agent/src/core/agent-session.ts +7 -1
  160. package/packages/pi-coding-agent/src/core/auth-storage.test.ts +68 -0
  161. package/packages/pi-coding-agent/src/core/auth-storage.ts +30 -2
  162. package/packages/pi-coding-agent/src/core/extensions/loader.ts +18 -0
  163. package/packages/pi-coding-agent/src/core/lsp/client.ts +29 -0
  164. package/packages/pi-coding-agent/src/core/model-registry.ts +3 -0
  165. package/packages/pi-coding-agent/src/core/package-manager.ts +99 -58
  166. package/packages/pi-coding-agent/src/core/resource-loader.ts +24 -6
  167. package/packages/pi-coding-agent/src/core/system-prompt.ts +6 -5
  168. package/packages/pi-coding-agent/src/modes/interactive/components/extension-editor.ts +3 -0
  169. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -6
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +31 -11
  171. package/pkg/package.json +1 -1
  172. package/src/resources/extensions/async-jobs/async-bash-timeout.test.ts +122 -0
  173. package/src/resources/extensions/async-jobs/async-bash-tool.ts +40 -4
  174. package/src/resources/extensions/async-jobs/await-tool.test.ts +47 -0
  175. package/src/resources/extensions/async-jobs/await-tool.ts +5 -0
  176. package/src/resources/extensions/async-jobs/index.ts +1 -0
  177. package/src/resources/extensions/async-jobs/job-manager.ts +2 -0
  178. package/src/resources/extensions/gsd/auto/loop-deps.ts +0 -1
  179. package/src/resources/extensions/gsd/auto/loop.ts +91 -0
  180. package/src/resources/extensions/gsd/auto/phases.ts +3 -5
  181. package/src/resources/extensions/gsd/auto/session.ts +6 -0
  182. package/src/resources/extensions/gsd/auto-dashboard.ts +2 -0
  183. package/src/resources/extensions/gsd/auto-prompts.ts +2 -18
  184. package/src/resources/extensions/gsd/auto-start.ts +7 -10
  185. package/src/resources/extensions/gsd/auto.ts +31 -1
  186. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +13 -5
  187. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +9 -2
  188. package/src/resources/extensions/gsd/commands/catalog.ts +32 -0
  189. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +164 -0
  190. package/src/resources/extensions/gsd/context-injector.ts +100 -0
  191. package/src/resources/extensions/gsd/custom-execution-policy.ts +73 -0
  192. package/src/resources/extensions/gsd/custom-verification.ts +180 -0
  193. package/src/resources/extensions/gsd/custom-workflow-engine.ts +216 -0
  194. package/src/resources/extensions/gsd/dashboard-overlay.ts +1 -0
  195. package/src/resources/extensions/gsd/definition-loader.ts +462 -0
  196. package/src/resources/extensions/gsd/detection.ts +19 -0
  197. package/src/resources/extensions/gsd/dev-execution-policy.ts +51 -0
  198. package/src/resources/extensions/gsd/dev-workflow-engine.ts +110 -0
  199. package/src/resources/extensions/gsd/doctor-checks.ts +32 -1
  200. package/src/resources/extensions/gsd/doctor-providers.ts +13 -0
  201. package/src/resources/extensions/gsd/doctor-types.ts +1 -0
  202. package/src/resources/extensions/gsd/engine-resolver.ts +57 -0
  203. package/src/resources/extensions/gsd/engine-types.ts +71 -0
  204. package/src/resources/extensions/gsd/execution-policy.ts +43 -0
  205. package/src/resources/extensions/gsd/forensics.ts +92 -0
  206. package/src/resources/extensions/gsd/git-constants.ts +1 -0
  207. package/src/resources/extensions/gsd/git-service.ts +0 -1
  208. package/src/resources/extensions/gsd/gitignore.ts +1 -1
  209. package/src/resources/extensions/gsd/graph.ts +312 -0
  210. package/src/resources/extensions/gsd/native-git-bridge.ts +1 -0
  211. package/src/resources/extensions/gsd/preferences-types.ts +3 -0
  212. package/src/resources/extensions/gsd/preferences.ts +62 -6
  213. package/src/resources/extensions/gsd/prompts/forensics.md +12 -5
  214. package/src/resources/extensions/gsd/repo-identity.ts +48 -5
  215. package/src/resources/extensions/gsd/run-manager.ts +180 -0
  216. package/src/resources/extensions/gsd/service-tier.ts +17 -4
  217. package/src/resources/extensions/gsd/session-lock.ts +2 -2
  218. package/src/resources/extensions/gsd/tests/activity-log.test.ts +31 -69
  219. package/src/resources/extensions/gsd/tests/bundled-workflow-defs.test.ts +180 -0
  220. package/src/resources/extensions/gsd/tests/commands-workflow-custom.test.ts +283 -0
  221. package/src/resources/extensions/gsd/tests/context-injector.test.ts +313 -0
  222. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +540 -0
  223. package/src/resources/extensions/gsd/tests/custom-verification.test.ts +382 -0
  224. package/src/resources/extensions/gsd/tests/custom-workflow-engine.test.ts +339 -0
  225. package/src/resources/extensions/gsd/tests/dashboard-custom-engine.test.ts +87 -0
  226. package/src/resources/extensions/gsd/tests/definition-loader.test.ts +778 -0
  227. package/src/resources/extensions/gsd/tests/dev-engine-wrapper.test.ts +318 -0
  228. package/src/resources/extensions/gsd/tests/e2e-workflow-pipeline-integration.test.ts +476 -0
  229. package/src/resources/extensions/gsd/tests/engine-interfaces-contract.test.ts +271 -0
  230. package/src/resources/extensions/gsd/tests/forensics-dedup.test.ts +48 -0
  231. package/src/resources/extensions/gsd/tests/forensics-issue-routing.test.ts +43 -0
  232. package/src/resources/extensions/gsd/tests/git-locale.test.ts +133 -0
  233. package/src/resources/extensions/gsd/tests/git-service.test.ts +44 -0
  234. package/src/resources/extensions/gsd/tests/graph-operations.test.ts +599 -0
  235. package/src/resources/extensions/gsd/tests/iterate-engine-integration.test.ts +429 -0
  236. package/src/resources/extensions/gsd/tests/journal.test.ts +82 -127
  237. package/src/resources/extensions/gsd/tests/manifest-status.test.ts +73 -82
  238. package/src/resources/extensions/gsd/tests/run-manager.test.ts +229 -0
  239. package/src/resources/extensions/gsd/tests/service-tier.test.ts +30 -1
  240. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +56 -3
  241. package/src/resources/extensions/gsd/tests/symlink-numbered-variants.test.ts +151 -0
  242. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +45 -0
  243. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +156 -263
  244. package/src/resources/extensions/gsd/tests/worktree-health-dispatch.test.ts +35 -78
  245. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +81 -74
  246. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +1 -2
  247. package/src/resources/extensions/gsd/workflow-engine.ts +38 -0
  248. package/src/resources/extensions/gsd/worktree-resolver.ts +2 -3
  249. package/src/resources/extensions/gsd/worktree.ts +2 -2
  250. package/src/resources/extensions/mcp-client/index.ts +5 -1
  251. package/src/resources/extensions/search-the-web/tool-search.ts +3 -3
  252. package/src/resources/skills/create-workflow/SKILL.md +103 -0
  253. package/src/resources/skills/create-workflow/references/feature-patterns.md +128 -0
  254. package/src/resources/skills/create-workflow/references/verification-policies.md +76 -0
  255. package/src/resources/skills/create-workflow/references/yaml-schema-v1.md +46 -0
  256. package/src/resources/skills/create-workflow/templates/blog-post-pipeline.yaml +60 -0
  257. package/src/resources/skills/create-workflow/templates/code-audit.yaml +60 -0
  258. package/src/resources/skills/create-workflow/templates/release-checklist.yaml +66 -0
  259. package/src/resources/skills/create-workflow/templates/workflow-definition.yaml +32 -0
  260. package/src/resources/skills/create-workflow/workflows/create-from-scratch.md +104 -0
  261. package/src/resources/skills/create-workflow/workflows/create-from-template.md +72 -0
  262. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_buildManifest.js +0 -0
  263. /package/dist/web/standalone/.next/static/{EnGUNqHeGbE0tuuUkTJVA → qw8qDHXOTLUXBq1vEknSz}/_ssgManifest.js +0 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * run-manager.ts — Create and list isolated workflow run directories.
3
+ *
4
+ * Each run lives under `.gsd/workflow-runs/<name>/<timestamp>/` and contains:
5
+ * - DEFINITION.yaml — frozen snapshot of the workflow definition at run-creation time
6
+ * - GRAPH.yaml — initialized step graph with all steps pending
7
+ * - PARAMS.json — (optional) parameter overrides used for this run
8
+ *
9
+ * Observability:
10
+ * - All run state is on disk in human-readable YAML/JSON — inspectable with cat/less.
11
+ * - `listRuns()` returns structured metadata including step counts and overall status.
12
+ * - Timestamp directory names are filesystem-safe (ISO with hyphens replacing colons).
13
+ * - Errors include the full path context for diagnosis.
14
+ */
15
+
16
+ import { mkdirSync, writeFileSync, existsSync, readdirSync, statSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import { stringify } from "yaml";
19
+ import { loadDefinition, substituteParams } from "./definition-loader.js";
20
+ import { initializeGraph, writeGraph, readGraph } from "./graph.js";
21
+ import type { WorkflowDefinition } from "./definition-loader.js";
22
+ import type { WorkflowGraph } from "./graph.js";
23
+
24
+ // ─── Types ───────────────────────────────────────────────────────────────
25
+
26
+ export interface RunMetadata {
27
+ /** Workflow definition name. */
28
+ name: string;
29
+ /** Filesystem-safe timestamp string used as dir name. */
30
+ timestamp: string;
31
+ /** Full path to the run directory. */
32
+ runDir: string;
33
+ /** Step counts derived from GRAPH.yaml. */
34
+ steps: { total: number; completed: number; pending: number; active: number };
35
+ /** Overall status derived from step states. */
36
+ status: "pending" | "running" | "complete";
37
+ }
38
+
39
+ // ─── Constants ───────────────────────────────────────────────────────────
40
+
41
+ const RUNS_DIR = "workflow-runs";
42
+ const DEFS_DIR = "workflow-defs";
43
+
44
+ // ─── Helpers ─────────────────────────────────────────────────────────────
45
+
46
+ /**
47
+ * Generate a filesystem-safe timestamp: `YYYY-MM-DDTHH-MM-SS`.
48
+ * Replaces colons with hyphens so the string is safe as a directory name
49
+ * on all platforms (Windows forbids colons in paths).
50
+ */
51
+ function makeTimestamp(date: Date = new Date()): string {
52
+ return date.toISOString().replace(/:/g, "-").replace(/\.\d{3}Z$/, "");
53
+ }
54
+
55
+ /**
56
+ * Derive overall status from a graph's step statuses.
57
+ */
58
+ function deriveStatus(graph: WorkflowGraph): "pending" | "running" | "complete" {
59
+ const hasActive = graph.steps.some((s) => s.status === "active");
60
+ const allDone = graph.steps.every(
61
+ (s) => s.status === "complete" || s.status === "expanded",
62
+ );
63
+ if (allDone) return "complete";
64
+ if (hasActive) return "running";
65
+ return "pending";
66
+ }
67
+
68
+ // ─── Public API ──────────────────────────────────────────────────────────
69
+
70
+ /**
71
+ * Create a new isolated run directory for a workflow definition.
72
+ *
73
+ * 1. Loads the definition from `<basePath>/.gsd/workflow-defs/<defName>.yaml`
74
+ * 2. Applies parameter substitution if overrides are provided
75
+ * 3. Creates `<basePath>/.gsd/workflow-runs/<defName>/<timestamp>/`
76
+ * 4. Writes frozen DEFINITION.yaml, initialized GRAPH.yaml, and optional PARAMS.json
77
+ *
78
+ * @param basePath — project root directory
79
+ * @param defName — definition filename (without .yaml extension)
80
+ * @param overrides — optional parameter overrides (merged with definition defaults)
81
+ * @returns Full path to the created run directory
82
+ * @throws Error if the definition file doesn't exist or is invalid
83
+ */
84
+ export function createRun(
85
+ basePath: string,
86
+ defName: string,
87
+ overrides?: Record<string, string>,
88
+ ): string {
89
+ const defsDir = join(basePath, ".gsd", DEFS_DIR);
90
+
91
+ // Load and validate the definition
92
+ const rawDef = loadDefinition(defsDir, defName);
93
+
94
+ // Apply parameter substitution if overrides provided
95
+ const def: WorkflowDefinition = overrides
96
+ ? substituteParams(rawDef, overrides)
97
+ : substituteParams(rawDef); // still resolve default params if any
98
+
99
+ // Create the run directory
100
+ const timestamp = makeTimestamp();
101
+ const runDir = join(basePath, ".gsd", RUNS_DIR, defName, timestamp);
102
+ mkdirSync(runDir, { recursive: true });
103
+
104
+ // Freeze the definition as DEFINITION.yaml
105
+ writeFileSync(join(runDir, "DEFINITION.yaml"), stringify(def), "utf-8");
106
+
107
+ // Initialize and write GRAPH.yaml
108
+ const graph = initializeGraph(def);
109
+ writeGraph(runDir, graph);
110
+
111
+ // Write PARAMS.json if overrides were provided
112
+ if (overrides && Object.keys(overrides).length > 0) {
113
+ writeFileSync(
114
+ join(runDir, "PARAMS.json"),
115
+ JSON.stringify(overrides, null, 2),
116
+ "utf-8",
117
+ );
118
+ }
119
+
120
+ return runDir;
121
+ }
122
+
123
+ /**
124
+ * List existing workflow runs with metadata.
125
+ *
126
+ * Scans `<basePath>/.gsd/workflow-runs/` for run directories. Each run's
127
+ * GRAPH.yaml is read to derive step counts and overall status.
128
+ *
129
+ * @param basePath — project root directory
130
+ * @param defName — optional filter: only list runs for this definition name
131
+ * @returns Array of run metadata, sorted newest-first within each definition
132
+ */
133
+ export function listRuns(basePath: string, defName?: string): RunMetadata[] {
134
+ const runsRoot = join(basePath, ".gsd", RUNS_DIR);
135
+ if (!existsSync(runsRoot)) return [];
136
+
137
+ const results: RunMetadata[] = [];
138
+
139
+ // Get workflow name directories
140
+ const nameDirs = defName ? [defName] : readdirSync(runsRoot).filter((entry) => {
141
+ const full = join(runsRoot, entry);
142
+ return statSync(full).isDirectory();
143
+ });
144
+
145
+ for (const name of nameDirs) {
146
+ const nameDir = join(runsRoot, name);
147
+ if (!existsSync(nameDir)) continue;
148
+
149
+ const timestamps = readdirSync(nameDir).filter((entry) => {
150
+ const full = join(nameDir, entry);
151
+ return statSync(full).isDirectory();
152
+ });
153
+
154
+ // Sort newest-first (ISO strings sort lexicographically)
155
+ timestamps.sort().reverse();
156
+
157
+ for (const ts of timestamps) {
158
+ const runDir = join(nameDir, ts);
159
+ try {
160
+ const graph = readGraph(runDir);
161
+ const total = graph.steps.length;
162
+ const completed = graph.steps.filter((s) => s.status === "complete").length;
163
+ const pending = graph.steps.filter((s) => s.status === "pending").length;
164
+ const active = graph.steps.filter((s) => s.status === "active").length;
165
+
166
+ results.push({
167
+ name,
168
+ timestamp: ts,
169
+ runDir,
170
+ steps: { total, completed, pending, active },
171
+ status: deriveStatus(graph),
172
+ });
173
+ } catch {
174
+ // Skip runs with invalid/missing GRAPH.yaml
175
+ }
176
+ }
177
+ }
178
+
179
+ return results;
180
+ }
@@ -23,6 +23,8 @@ import { ensurePreferencesFile, serializePreferencesToFrontmatter } from "./comm
23
23
 
24
24
  export type ServiceTierSetting = "priority" | "flex" | undefined;
25
25
 
26
+ const SERVICE_TIER_SCOPE_NOTE = "Only affects gpt-5.4 models, regardless of provider.";
27
+
26
28
  // ─── Gating ──────────────────────────────────────────────────────────────────
27
29
 
28
30
  /**
@@ -51,7 +53,7 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
51
53
  " /gsd fast flex Set to flex (0.5x cost, slower)",
52
54
  " /gsd fast off Disable service tier",
53
55
  "",
54
- "Only affects gpt-5.4 models.",
56
+ SERVICE_TIER_SCOPE_NOTE,
55
57
  ].join("\n");
56
58
  }
57
59
 
@@ -64,10 +66,18 @@ export function formatServiceTierStatus(tier: ServiceTierSetting): string {
64
66
  " /gsd fast flex Set to flex (0.5x cost, slower)",
65
67
  " /gsd fast off Disable service tier",
66
68
  "",
67
- "Only affects gpt-5.4 models.",
69
+ SERVICE_TIER_SCOPE_NOTE,
68
70
  ].join("\n");
69
71
  }
70
72
 
73
+ export function formatServiceTierFooterStatus(
74
+ tier: ServiceTierSetting,
75
+ modelId: string | undefined,
76
+ ): string | undefined {
77
+ if (!tier || !modelId || !supportsServiceTier(modelId)) return undefined;
78
+ return tier === "priority" ? "fast: ⚡ priority" : "fast: 💰 flex";
79
+ }
80
+
71
81
  // ─── Icon Resolution ─────────────────────────────────────────────────────────
72
82
 
73
83
  /**
@@ -148,19 +158,22 @@ export async function handleFast(args: string, ctx: ExtensionCommandContext): Pr
148
158
 
149
159
  if (trimmed === "on") {
150
160
  await writeGlobalServiceTier(ctx, "priority");
151
- ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models.", "info");
161
+ ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("priority", ctx.model?.id));
162
+ ctx.ui.notify("Service tier set to priority (2x cost, faster responses). Only affects gpt-5.4 models, regardless of provider.", "info");
152
163
  return;
153
164
  }
154
165
 
155
166
  if (trimmed === "off") {
156
167
  await writeGlobalServiceTier(ctx, undefined);
168
+ ctx.ui.setStatus("gsd-fast", undefined);
157
169
  ctx.ui.notify("Service tier disabled.", "info");
158
170
  return;
159
171
  }
160
172
 
161
173
  if (trimmed === "flex") {
162
174
  await writeGlobalServiceTier(ctx, "flex");
163
- ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models.", "info");
175
+ ctx.ui.setStatus("gsd-fast", formatServiceTierFooterStatus("flex", ctx.model?.id));
176
+ ctx.ui.notify("Service tier set to flex (0.5x cost, slower responses). Only affects gpt-5.4 models, regardless of provider.", "info");
164
177
  return;
165
178
  }
166
179
 
@@ -239,7 +239,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
239
239
  const elapsed = Date.now() - _lockAcquiredAt;
240
240
  if (elapsed < 1_800_000) {
241
241
  process.stderr.write(
242
- `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
242
+ `[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
243
243
  );
244
244
  return; // Suppress false positive
245
245
  }
@@ -299,7 +299,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
299
299
  const elapsed = Date.now() - _lockAcquiredAt;
300
300
  if (elapsed < 1_800_000) {
301
301
  process.stderr.write(
302
- `[gsd] Lock heartbeat mismatch after ${Math.round(elapsed / 1000)}s — event loop stall, continuing.\n`,
302
+ `[gsd] Lock heartbeat caught up after ${Math.round(elapsed / 1000)}s — long LLM call, no action needed.\n`,
303
303
  );
304
304
  return;
305
305
  }
@@ -4,7 +4,7 @@
4
4
  * - activity-log-save.test.ts (caching, dedup, collision recovery)
5
5
  */
6
6
 
7
- import test from "node:test";
7
+ import { describe, test, beforeEach, afterEach } from "node:test";
8
8
  import assert from "node:assert/strict";
9
9
  import { existsSync, mkdtempSync, mkdirSync, readdirSync, realpathSync, rmSync, utimesSync, writeFileSync, readFileSync } from "node:fs";
10
10
  import { join, dirname } from "node:path";
@@ -48,9 +48,12 @@ function createCtx(entries: unknown[]) {
48
48
 
49
49
  // ── Pruning ──────────────────────────────────────────────────────────────────
50
50
 
51
- test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () => {
52
- const dir = createTmpDir();
53
- try {
51
+ describe("pruneActivityLogs", () => {
52
+ let dir: string;
53
+ beforeEach(() => { dir = createTmpDir(); });
54
+ afterEach(() => { rmSync(dir, { recursive: true, force: true }); });
55
+
56
+ test("deletes old files, keeps recent and highest-seq", () => {
54
57
  const f001 = writeActivityFile(dir, "001", "execute-task-M001-S01-T01");
55
58
  writeActivityFile(dir, "002", "execute-task-M001-S01-T02");
56
59
  writeActivityFile(dir, "003", "execute-task-M001-S01-T03");
@@ -61,14 +64,9 @@ test("pruneActivityLogs deletes old files, keeps recent and highest-seq", () =>
61
64
  assert.ok(!remaining.includes("001-execute-task-M001-S01-T01.jsonl"));
62
65
  assert.ok(remaining.includes("002-execute-task-M001-S01-T02.jsonl"));
63
66
  assert.ok(remaining.includes("003-execute-task-M001-S01-T03.jsonl"));
64
- } finally {
65
- rmSync(dir, { recursive: true, force: true });
66
- }
67
- });
67
+ });
68
68
 
69
- test("pruneActivityLogs preserves highest-seq even when all files are old", () => {
70
- const dir = createTmpDir();
71
- try {
69
+ test("preserves highest-seq even when all files are old", () => {
72
70
  const f001 = writeActivityFile(dir, "001", "t1");
73
71
  const f002 = writeActivityFile(dir, "002", "t2");
74
72
  const f003 = writeActivityFile(dir, "003", "t3");
@@ -78,14 +76,9 @@ test("pruneActivityLogs preserves highest-seq even when all files are old", () =
78
76
  const remaining = listFiles(dir);
79
77
  assert.equal(remaining.length, 1);
80
78
  assert.ok(remaining[0].startsWith("003-"));
81
- } finally {
82
- rmSync(dir, { recursive: true, force: true });
83
- }
84
- });
79
+ });
85
80
 
86
- test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
87
- const dir = createTmpDir();
88
- try {
81
+ test("with retentionDays=0 keeps only highest-seq", () => {
89
82
  writeActivityFile(dir, "001", "t1");
90
83
  writeActivityFile(dir, "002", "t2");
91
84
  writeActivityFile(dir, "003", "t3");
@@ -94,51 +87,31 @@ test("pruneActivityLogs with retentionDays=0 keeps only highest-seq", () => {
94
87
  const remaining = listFiles(dir);
95
88
  assert.equal(remaining.length, 1);
96
89
  assert.ok(remaining[0].startsWith("003-"));
97
- } finally {
98
- rmSync(dir, { recursive: true, force: true });
99
- }
100
- });
90
+ });
101
91
 
102
- test("pruneActivityLogs no-op when all files are recent", () => {
103
- const dir = createTmpDir();
104
- try {
92
+ test("no-op when all files are recent", () => {
105
93
  writeActivityFile(dir, "001", "t1");
106
94
  writeActivityFile(dir, "002", "t2");
107
95
  writeActivityFile(dir, "003", "t3");
108
96
 
109
97
  pruneActivityLogs(dir, 30);
110
98
  assert.equal(listFiles(dir).length, 3);
111
- } finally {
112
- rmSync(dir, { recursive: true, force: true });
113
- }
114
- });
99
+ });
115
100
 
116
- test("pruneActivityLogs handles empty directory", () => {
117
- const dir = createTmpDir();
118
- try {
101
+ test("handles empty directory", () => {
119
102
  assert.doesNotThrow(() => pruneActivityLogs(dir, 30));
120
103
  assert.equal(readdirSync(dir).length, 0);
121
- } finally {
122
- rmSync(dir, { recursive: true, force: true });
123
- }
124
- });
104
+ });
125
105
 
126
- test("pruneActivityLogs preserves single old file (it is highest-seq)", () => {
127
- const dir = createTmpDir();
128
- try {
106
+ test("preserves single old file (it is highest-seq)", () => {
129
107
  const f = writeActivityFile(dir, "001", "t1");
130
108
  backdateFile(f, 100);
131
109
 
132
110
  pruneActivityLogs(dir, 30);
133
111
  assert.equal(listFiles(dir).length, 1);
134
- } finally {
135
- rmSync(dir, { recursive: true, force: true });
136
- }
137
- });
112
+ });
138
113
 
139
- test("pruneActivityLogs ignores non-matching filenames", () => {
140
- const dir = createTmpDir();
141
- try {
114
+ test("ignores non-matching filenames", () => {
142
115
  const f001 = writeActivityFile(dir, "001", "t1");
143
116
  writeFileSync(join(dir, "notes.txt"), "some notes\n", "utf-8");
144
117
  backdateFile(f001, 40);
@@ -148,16 +121,17 @@ test("pruneActivityLogs ignores non-matching filenames", () => {
148
121
  assert.ok(remaining.includes("notes.txt"));
149
122
  // 001 is the only seq file, so it's highest-seq and survives
150
123
  assert.ok(remaining.includes("001-t1.jsonl"));
151
- } finally {
152
- rmSync(dir, { recursive: true, force: true });
153
- }
124
+ });
154
125
  });
155
126
 
156
127
  // ── Save: caching, dedup, collision recovery ─────────────────────────────────
157
128
 
158
- test("saveActivityLog caches sequence instead of rescanning", () => {
159
- const baseDir = createTmpDir();
160
- try {
129
+ describe("saveActivityLog", () => {
130
+ let baseDir: string;
131
+ beforeEach(() => { baseDir = createTmpDir(); });
132
+ afterEach(() => { rmSync(baseDir, { recursive: true, force: true }); });
133
+
134
+ test("caches sequence instead of rescanning", () => {
161
135
  saveActivityLog(createCtx([{ kind: "first", n: 1 }]) as any, baseDir, "execute-task", "M001/S01/T01");
162
136
  writeFileSync(join(activityDir(baseDir), "999-external.jsonl"), '{"x":1}\n', "utf-8");
163
137
  saveActivityLog(createCtx([{ kind: "second", n: 2 }]) as any, baseDir, "execute-task", "M001/S01/T02");
@@ -166,14 +140,9 @@ test("saveActivityLog caches sequence instead of rescanning", () => {
166
140
  assert.ok(files.includes("001-execute-task-M001-S01-T01.jsonl"));
167
141
  assert.ok(files.includes("002-execute-task-M001-S01-T02.jsonl"));
168
142
  assert.ok(!files.some(f => f.startsWith("1000-")));
169
- } finally {
170
- rmSync(baseDir, { recursive: true, force: true });
171
- }
172
- });
143
+ });
173
144
 
174
- test("saveActivityLog deduplicates identical snapshots for same unit", () => {
175
- const baseDir = createTmpDir();
176
- try {
145
+ test("deduplicates identical snapshots for same unit", () => {
177
146
  const ctx = createCtx([{ role: "assistant", content: "same" }]);
178
147
  saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
179
148
  saveActivityLog(ctx as any, baseDir, "plan-slice", "M002/S01");
@@ -184,14 +153,9 @@ test("saveActivityLog deduplicates identical snapshots for same unit", () => {
184
153
  saveActivityLog(createCtx([{ role: "assistant", content: "changed" }]) as any, baseDir, "plan-slice", "M002/S01");
185
154
  files = listFiles(activityDir(baseDir));
186
155
  assert.equal(files.length, 2);
187
- } finally {
188
- rmSync(baseDir, { recursive: true, force: true });
189
- }
190
- });
156
+ });
191
157
 
192
- test("saveActivityLog recovers on sequence collision", () => {
193
- const baseDir = createTmpDir();
194
- try {
158
+ test("recovers on sequence collision", () => {
195
159
  saveActivityLog(createCtx([{ turn: 1 }]) as any, baseDir, "execute-task", "M003/S02/T01");
196
160
  writeFileSync(join(activityDir(baseDir), "002-execute-task-M003-S02-T02.jsonl"), '{"collision":true}\n', "utf-8");
197
161
  saveActivityLog(createCtx([{ turn: 2 }]) as any, baseDir, "execute-task", "M003/S02/T02");
@@ -199,9 +163,7 @@ test("saveActivityLog recovers on sequence collision", () => {
199
163
  const files = listFiles(activityDir(baseDir));
200
164
  assert.ok(files.includes("002-execute-task-M003-S02-T02.jsonl"));
201
165
  assert.ok(files.includes("003-execute-task-M003-S02-T02.jsonl"));
202
- } finally {
203
- rmSync(baseDir, { recursive: true, force: true });
204
- }
166
+ });
205
167
  });
206
168
 
207
169
  // ── Prompt text assertion ────────────────────────────────────────────────────
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Bundled workflow definition validation tests.
3
+ *
4
+ * Verifies that every example YAML in src/resources/skills/create-workflow/templates/
5
+ * passes validateDefinition() from definition-loader.ts with { valid: true, errors: [] }.
6
+ *
7
+ * Also validates scaffold template and structural properties of each example
8
+ * (step counts, feature usage) to guard against accidental regressions.
9
+ */
10
+
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { readFileSync } from "node:fs";
14
+ import { join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { parse } from "yaml";
17
+
18
+ import { validateDefinition } from "../definition-loader.ts";
19
+
20
+ // ─── Path resolution ─────────────────────────────────────────────────────
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ // Navigate from tests/ → extensions/gsd/ → extensions/ → resources/ → skills/create-workflow/templates/
24
+ const templatesDir = join(
25
+ __dirname,
26
+ "..",
27
+ "..",
28
+ "..",
29
+ "skills",
30
+ "create-workflow",
31
+ "templates",
32
+ );
33
+
34
+ function loadYaml(filename: string): unknown {
35
+ const raw = readFileSync(join(templatesDir, filename), "utf-8");
36
+ return parse(raw);
37
+ }
38
+
39
+ // ─── Scaffold template ──────────────────────────────────────────────────
40
+
41
+ test("scaffold template (workflow-definition.yaml) passes validation", () => {
42
+ const parsed = loadYaml("workflow-definition.yaml");
43
+ const result = validateDefinition(parsed);
44
+ assert.equal(result.valid, true, `Scaffold invalid: ${result.errors.join("; ")}`);
45
+ assert.equal(result.errors.length, 0);
46
+ });
47
+
48
+ // ─── blog-post-pipeline.yaml ────────────────────────────────────────────
49
+
50
+ test("blog-post-pipeline.yaml passes validation", () => {
51
+ const parsed = loadYaml("blog-post-pipeline.yaml");
52
+ const result = validateDefinition(parsed);
53
+ assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
54
+ assert.equal(result.errors.length, 0);
55
+ });
56
+
57
+ test("blog-post-pipeline.yaml: 3 steps, context_from, params, content-heuristic", () => {
58
+ const parsed = loadYaml("blog-post-pipeline.yaml") as Record<string, unknown>;
59
+ const steps = parsed.steps as Array<Record<string, unknown>>;
60
+
61
+ // 3 steps
62
+ assert.equal(steps.length, 3, "Expected 3 steps");
63
+
64
+ // params defined
65
+ assert.ok(parsed.params, "Expected params to be defined");
66
+ const params = parsed.params as Record<string, string>;
67
+ assert.ok("topic" in params, "Expected 'topic' param");
68
+ assert.ok("audience" in params, "Expected 'audience' param");
69
+
70
+ // At least one step uses context_from
71
+ const hasContextFrom = steps.some(
72
+ (s) => Array.isArray(s.context_from) && s.context_from.length > 0,
73
+ );
74
+ assert.ok(hasContextFrom, "Expected at least one step with context_from");
75
+
76
+ // All steps use content-heuristic verify
77
+ for (const step of steps) {
78
+ const verify = step.verify as Record<string, unknown> | undefined;
79
+ assert.ok(verify, `Step "${step.id}" missing verify`);
80
+ assert.equal(verify.policy, "content-heuristic", `Step "${step.id}" should use content-heuristic`);
81
+ }
82
+ });
83
+
84
+ // ─── code-audit.yaml ────────────────────────────────────────────────────
85
+
86
+ test("code-audit.yaml passes validation", () => {
87
+ const parsed = loadYaml("code-audit.yaml");
88
+ const result = validateDefinition(parsed);
89
+ assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
90
+ assert.equal(result.errors.length, 0);
91
+ });
92
+
93
+ test("code-audit.yaml: iterate with capture group and shell-command verify", () => {
94
+ const parsed = loadYaml("code-audit.yaml") as Record<string, unknown>;
95
+ const steps = parsed.steps as Array<Record<string, unknown>>;
96
+
97
+ // Find step with iterate
98
+ const iterateStep = steps.find((s) => s.iterate != null);
99
+ assert.ok(iterateStep, "Expected a step with iterate config");
100
+
101
+ const iterate = iterateStep.iterate as Record<string, unknown>;
102
+ assert.equal(typeof iterate.source, "string", "iterate.source must be a string");
103
+ assert.equal(typeof iterate.pattern, "string", "iterate.pattern must be a string");
104
+
105
+ // Pattern has a capture group
106
+ const pattern = iterate.pattern as string;
107
+ assert.ok(/\((?!\?)/.test(pattern), "iterate.pattern must contain a capture group");
108
+
109
+ // Pattern is valid regex
110
+ assert.doesNotThrow(() => new RegExp(pattern), "iterate.pattern must be valid regex");
111
+
112
+ // Has shell-command verify
113
+ const verify = iterateStep.verify as Record<string, unknown>;
114
+ assert.equal(verify.policy, "shell-command");
115
+ assert.equal(typeof verify.command, "string");
116
+ });
117
+
118
+ // ─── release-checklist.yaml ─────────────────────────────────────────────
119
+
120
+ test("release-checklist.yaml passes validation", () => {
121
+ const parsed = loadYaml("release-checklist.yaml");
122
+ const result = validateDefinition(parsed);
123
+ assert.equal(result.valid, true, `Invalid: ${result.errors.join("; ")}`);
124
+ assert.equal(result.errors.length, 0);
125
+ });
126
+
127
+ test("release-checklist.yaml: diamond dependencies and human-review", () => {
128
+ const parsed = loadYaml("release-checklist.yaml") as Record<string, unknown>;
129
+ const steps = parsed.steps as Array<Record<string, unknown>>;
130
+
131
+ // 4 steps
132
+ assert.equal(steps.length, 4, "Expected 4 steps");
133
+
134
+ // Diamond pattern: two steps depend on the same parent
135
+ const changelog = steps.find((s) => s.id === "changelog");
136
+ const versionBump = steps.find((s) => s.id === "version-bump");
137
+ const testSuite = steps.find((s) => s.id === "test-suite");
138
+ const publish = steps.find((s) => s.id === "publish");
139
+
140
+ assert.ok(changelog, "Expected 'changelog' step");
141
+ assert.ok(versionBump, "Expected 'version-bump' step");
142
+ assert.ok(testSuite, "Expected 'test-suite' step");
143
+ assert.ok(publish, "Expected 'publish' step");
144
+
145
+ // Both version-bump and test-suite depend on changelog
146
+ const vbReqs = versionBump.requires as string[];
147
+ const tsReqs = testSuite.requires as string[];
148
+ assert.ok(vbReqs.includes("changelog"), "version-bump should require changelog");
149
+ assert.ok(tsReqs.includes("changelog"), "test-suite should require changelog");
150
+
151
+ // publish depends on both (diamond join)
152
+ const pubReqs = publish.requires as string[];
153
+ assert.ok(pubReqs.includes("version-bump"), "publish should require version-bump");
154
+ assert.ok(pubReqs.includes("test-suite"), "publish should require test-suite");
155
+
156
+ // publish uses human-review
157
+ const verify = publish.verify as Record<string, unknown>;
158
+ assert.equal(verify.policy, "human-review");
159
+ });
160
+
161
+ // ─── Cross-cutting: no path traversal in produces ───────────────────────
162
+
163
+ test("no produces path contains '..'", () => {
164
+ const files = [
165
+ "blog-post-pipeline.yaml",
166
+ "code-audit.yaml",
167
+ "release-checklist.yaml",
168
+ ];
169
+
170
+ for (const file of files) {
171
+ const parsed = loadYaml(file) as Record<string, unknown>;
172
+ const steps = parsed.steps as Array<Record<string, unknown>>;
173
+ for (const step of steps) {
174
+ const produces = (step.produces as string[]) ?? [];
175
+ for (const p of produces) {
176
+ assert.ok(!p.includes(".."), `${file} step "${step.id}" produces path contains '..': ${p}`);
177
+ }
178
+ }
179
+ }
180
+ });