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,312 @@
1
+ /**
2
+ * graph.ts — Pure data module for GRAPH.yaml workflow step tracking.
3
+ *
4
+ * Provides types and functions for reading, writing, and querying the
5
+ * step graph that drives CustomWorkflowEngine. Zero engine dependencies.
6
+ *
7
+ * GRAPH.yaml lives in a run directory and tracks step statuses
8
+ * (pending → active → complete) with optional dependency edges.
9
+ *
10
+ * Observability:
11
+ * - readGraph/writeGraph use YAML on disk — human-readable, diffable,
12
+ * inspectable with `cat` or any YAML viewer.
13
+ * - Each GraphStep has status, startedAt, finishedAt fields visible in GRAPH.yaml.
14
+ * - writeGraph uses atomic write (tmp + rename) for crash safety.
15
+ * - All operations are immutable — callers always get a new graph object.
16
+ */
17
+
18
+ import { parse, stringify } from "yaml";
19
+ import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
20
+ import { join } from "node:path";
21
+ import type { WorkflowDefinition } from "./definition-loader.js";
22
+
23
+ // ─── Types ───────────────────────────────────────────────────────────────
24
+
25
+ export interface GraphStep {
26
+ /** Unique step identifier within the workflow. */
27
+ id: string;
28
+ /** Human-readable step title. */
29
+ title: string;
30
+ /** Current status: pending → active → complete → expanded (iterate parent). */
31
+ status: "pending" | "active" | "complete" | "expanded";
32
+ /** The prompt to dispatch for this step. */
33
+ prompt: string;
34
+ /** IDs of steps that must be "complete" before this step can run. */
35
+ dependsOn: string[];
36
+ /** For iteration instances: ID of the parent step that was expanded. */
37
+ parentStepId?: string;
38
+ /** ISO timestamp when the step started executing. */
39
+ startedAt?: string;
40
+ /** ISO timestamp when the step finished executing. */
41
+ finishedAt?: string;
42
+ }
43
+
44
+ export interface WorkflowGraph {
45
+ /** Ordered list of steps in the workflow. */
46
+ steps: GraphStep[];
47
+ /** Workflow metadata. */
48
+ metadata: {
49
+ name: string;
50
+ createdAt: string;
51
+ };
52
+ }
53
+
54
+ // ─── YAML schema mapping ─────────────────────────────────────────────────
55
+
56
+ const GRAPH_FILENAME = "GRAPH.yaml";
57
+
58
+ /**
59
+ * Internal YAML shape — uses snake_case for YAML keys.
60
+ * Converted to/from the camelCase TypeScript types on read/write.
61
+ */
62
+ interface YamlStep {
63
+ id: string;
64
+ title: string;
65
+ status: string;
66
+ prompt: string;
67
+ depends_on?: string[];
68
+ parent_step_id?: string;
69
+ started_at?: string;
70
+ finished_at?: string;
71
+ }
72
+
73
+ interface YamlGraph {
74
+ steps: YamlStep[];
75
+ metadata: { name: string; created_at: string };
76
+ }
77
+
78
+ // ─── Functions ───────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Read and parse GRAPH.yaml from a run directory.
82
+ *
83
+ * @param runDir — directory containing GRAPH.yaml
84
+ * @returns Parsed workflow graph
85
+ * @throws Error if file doesn't exist or YAML is malformed
86
+ */
87
+ export function readGraph(runDir: string): WorkflowGraph {
88
+ const filePath = join(runDir, GRAPH_FILENAME);
89
+ if (!existsSync(filePath)) {
90
+ throw new Error(`GRAPH.yaml not found: ${filePath}`);
91
+ }
92
+ const raw = readFileSync(filePath, "utf-8");
93
+ const yaml = parse(raw) as YamlGraph;
94
+
95
+ if (!yaml?.steps || !Array.isArray(yaml.steps)) {
96
+ throw new Error(`Invalid GRAPH.yaml: missing or invalid 'steps' array in ${filePath}`);
97
+ }
98
+
99
+ return {
100
+ steps: yaml.steps.map((s) => ({
101
+ id: s.id,
102
+ title: s.title,
103
+ status: s.status as GraphStep["status"],
104
+ prompt: s.prompt,
105
+ dependsOn: s.depends_on ?? [],
106
+ ...(s.parent_step_id != null ? { parentStepId: s.parent_step_id } : {}),
107
+ ...(s.started_at != null ? { startedAt: s.started_at } : {}),
108
+ ...(s.finished_at != null ? { finishedAt: s.finished_at } : {}),
109
+ })),
110
+ metadata: {
111
+ name: yaml.metadata?.name ?? "unnamed",
112
+ createdAt: yaml.metadata?.created_at ?? new Date().toISOString(),
113
+ },
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Write a workflow graph to GRAPH.yaml in a run directory.
119
+ * Creates the directory if it doesn't exist. Write is atomic (write + rename).
120
+ *
121
+ * @param runDir — directory to write GRAPH.yaml into
122
+ * @param graph — the workflow graph to serialize
123
+ */
124
+ export function writeGraph(runDir: string, graph: WorkflowGraph): void {
125
+ if (!existsSync(runDir)) {
126
+ mkdirSync(runDir, { recursive: true });
127
+ }
128
+
129
+ const yamlData: YamlGraph = {
130
+ steps: graph.steps.map((s) => ({
131
+ id: s.id,
132
+ title: s.title,
133
+ status: s.status,
134
+ prompt: s.prompt,
135
+ depends_on: s.dependsOn.length > 0 ? s.dependsOn : undefined,
136
+ parent_step_id: s.parentStepId ?? undefined,
137
+ started_at: s.startedAt ?? undefined,
138
+ finished_at: s.finishedAt ?? undefined,
139
+ })) as YamlStep[],
140
+ metadata: {
141
+ name: graph.metadata.name,
142
+ created_at: graph.metadata.createdAt,
143
+ },
144
+ };
145
+
146
+ const filePath = join(runDir, GRAPH_FILENAME);
147
+ const tmpPath = filePath + ".tmp";
148
+ const content = stringify(yamlData);
149
+ writeFileSync(tmpPath, content, "utf-8");
150
+ // Atomic rename for crash safety
151
+ renameSync(tmpPath, filePath);
152
+ }
153
+
154
+ /**
155
+ * Get the next pending step whose dependencies are all complete.
156
+ *
157
+ * Returns the first step (in array order) with status "pending" where
158
+ * every step in its `dependsOn` list has status "complete".
159
+ *
160
+ * @param graph — the workflow graph to query
161
+ * @returns The next dispatchable step, or null if none available
162
+ */
163
+ export function getNextPendingStep(graph: WorkflowGraph): GraphStep | null {
164
+ const statusMap = new Map(graph.steps.map((s) => [s.id, s.status]));
165
+
166
+ for (const step of graph.steps) {
167
+ if (step.status !== "pending") continue;
168
+ const depsComplete = step.dependsOn.every(
169
+ (depId) => statusMap.get(depId) === "complete",
170
+ );
171
+ if (depsComplete) return step;
172
+ }
173
+
174
+ return null;
175
+ }
176
+
177
+ /**
178
+ * Return a new graph with the specified step marked as "complete".
179
+ * Immutable — does not mutate the input graph.
180
+ *
181
+ * @param graph — the current workflow graph
182
+ * @param stepId — ID of the step to mark complete
183
+ * @returns New graph with the step's status set to "complete"
184
+ * @throws Error if stepId is not found in the graph
185
+ */
186
+ export function markStepComplete(
187
+ graph: WorkflowGraph,
188
+ stepId: string,
189
+ ): WorkflowGraph {
190
+ const found = graph.steps.some((s) => s.id === stepId);
191
+ if (!found) {
192
+ throw new Error(`Step not found: ${stepId}`);
193
+ }
194
+
195
+ return {
196
+ ...graph,
197
+ steps: graph.steps.map((s) =>
198
+ s.id === stepId
199
+ ? { ...s, status: "complete" as const, finishedAt: new Date().toISOString() }
200
+ : s,
201
+ ),
202
+ };
203
+ }
204
+
205
+ // ─── Iteration expansion ─────────────────────────────────────────────────
206
+
207
+ /**
208
+ * Expand an iterate step into concrete instances. Pure and deterministic —
209
+ * identical inputs always produce identical output.
210
+ *
211
+ * Given a parent step with status "pending" and an array of matched items,
212
+ * creates one instance step per item, marks the parent as "expanded", and
213
+ * rewrites any downstream dependsOn references from the parent ID to the
214
+ * full set of instance IDs.
215
+ *
216
+ * @param graph — the current workflow graph (not mutated)
217
+ * @param stepId — ID of the iterate step to expand
218
+ * @param items — matched items from the source artifact
219
+ * @param promptTemplate — template with {{item}} placeholders
220
+ * @returns New WorkflowGraph with instances inserted and deps rewritten
221
+ * @throws Error if stepId not found or step is not pending
222
+ */
223
+ export function expandIteration(
224
+ graph: WorkflowGraph,
225
+ stepId: string,
226
+ items: string[],
227
+ promptTemplate: string,
228
+ ): WorkflowGraph {
229
+ const parentIndex = graph.steps.findIndex((s) => s.id === stepId);
230
+ if (parentIndex === -1) {
231
+ throw new Error(`expandIteration: step not found: ${stepId}`);
232
+ }
233
+ const parentStep = graph.steps[parentIndex];
234
+ if (parentStep.status !== "pending") {
235
+ throw new Error(
236
+ `expandIteration: step "${stepId}" has status "${parentStep.status}", expected "pending"`,
237
+ );
238
+ }
239
+
240
+ // Create instance steps
241
+ const instanceIds: string[] = [];
242
+ const instances: GraphStep[] = items.map((item, i) => {
243
+ const instanceId = `${stepId}--${String(i + 1).padStart(3, "0")}`;
244
+ instanceIds.push(instanceId);
245
+ return {
246
+ id: instanceId,
247
+ title: `${parentStep.title}: ${item}`,
248
+ status: "pending" as const,
249
+ prompt: promptTemplate.replace(/\{\{item\}\}/g, () => item),
250
+ dependsOn: [...parentStep.dependsOn],
251
+ parentStepId: stepId,
252
+ };
253
+ });
254
+
255
+ // Build new steps array: copy everything, mark parent as expanded,
256
+ // insert instances right after the parent, rewrite downstream deps.
257
+ const newSteps: GraphStep[] = [];
258
+ for (let i = 0; i < graph.steps.length; i++) {
259
+ if (i === parentIndex) {
260
+ // Mark parent as expanded
261
+ newSteps.push({ ...parentStep, status: "expanded" as const });
262
+ // Insert instances immediately after parent
263
+ newSteps.push(...instances);
264
+ } else {
265
+ const step = graph.steps[i];
266
+ // Rewrite dependsOn: replace parent ID with all instance IDs
267
+ const hasDep = step.dependsOn.includes(stepId);
268
+ if (hasDep) {
269
+ const rewritten = step.dependsOn.flatMap((dep) =>
270
+ dep === stepId ? instanceIds : [dep],
271
+ );
272
+ newSteps.push({ ...step, dependsOn: rewritten });
273
+ } else {
274
+ newSteps.push(step);
275
+ }
276
+ }
277
+ }
278
+
279
+ return {
280
+ ...graph,
281
+ steps: newSteps,
282
+ };
283
+ }
284
+
285
+ // ─── Definition → Graph conversion ──────────────────────────────────────
286
+
287
+ /**
288
+ * Convert a parsed WorkflowDefinition into a WorkflowGraph with all
289
+ * steps in "pending" status. Used by run-manager to generate the initial
290
+ * GRAPH.yaml for a new run.
291
+ *
292
+ * @param def — a validated WorkflowDefinition from definition-loader
293
+ * @returns WorkflowGraph with pending steps and metadata from the definition
294
+ */
295
+ export function initializeGraph(def: WorkflowDefinition): WorkflowGraph {
296
+ return {
297
+ steps: def.steps.map((s) => ({
298
+ id: s.id,
299
+ title: s.name,
300
+ status: "pending" as const,
301
+ prompt: s.prompt,
302
+ dependsOn: s.requires ?? [],
303
+ })),
304
+ metadata: {
305
+ name: def.name,
306
+ createdAt: new Date().toISOString(),
307
+ },
308
+ };
309
+ }
310
+
311
+ /** @deprecated Use initializeGraph instead. Kept for backward compatibility. */
312
+ export { initializeGraph as graphFromDefinition };
@@ -847,6 +847,7 @@ export function nativeMergeSquash(basePath: string, branch: string): GitMergeRes
847
847
  cwd: basePath,
848
848
  stdio: ["ignore", "pipe", "pipe"],
849
849
  encoding: "utf-8",
850
+ env: GIT_NO_PROMPT_ENV,
850
851
  });
851
852
  return { success: true, conflicts: [] };
852
853
  } catch (err: unknown) {
@@ -89,6 +89,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
89
89
  "reactive_execution",
90
90
  "github",
91
91
  "service_tier",
92
+ "forensics_dedup",
92
93
  ]);
93
94
 
94
95
  /** Canonical list of all dispatch unit types. */
@@ -223,6 +224,8 @@ export interface GSDPreferences {
223
224
  github?: GitHubSyncConfig;
224
225
  /** OpenAI service tier preference. "priority" = 2x cost, faster. "flex" = 0.5x cost, slower. Only affects gpt-5.4 models. */
225
226
  service_tier?: "priority" | "flex";
227
+ /** Opt-in: search existing issues and PRs before filing from /gsd forensics. Uses additional AI tokens. */
228
+ forensics_dedup?: boolean;
226
229
  }
227
230
 
228
231
  export interface LoadedGSDPreferences {
@@ -200,12 +200,22 @@ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedG
200
200
  export function parsePreferencesMarkdown(content: string): GSDPreferences | null {
201
201
  // Use indexOf instead of [\s\S]*? regex to avoid backtracking (#468)
202
202
  const startMarker = content.startsWith('---\r\n') ? '---\r\n' : '---\n';
203
- if (!content.startsWith(startMarker)) return null;
204
- const searchStart = startMarker.length;
205
- const endIdx = content.indexOf('\n---', searchStart);
206
- if (endIdx === -1) return null;
207
- const block = content.slice(searchStart, endIdx);
208
- return parseFrontmatterBlock(block.replace(/\r/g, ''));
203
+ if (content.startsWith(startMarker)) {
204
+ const searchStart = startMarker.length;
205
+ const endIdx = content.indexOf('\n---', searchStart);
206
+ if (endIdx === -1) return null;
207
+ const block = content.slice(searchStart, endIdx);
208
+ return parseFrontmatterBlock(block.replace(/\r/g, ''));
209
+ }
210
+
211
+ // Fallback: heading+list format (e.g. "## Git\n- isolation: none") (#2036)
212
+ // GSD agents may write preferences files without frontmatter delimiters.
213
+ if (/^##\s+\w/m.test(content)) {
214
+ return parseHeadingListFormat(content);
215
+ }
216
+
217
+ console.warn("[parsePreferencesMarkdown] preferences.md exists but uses an unrecognized format — skipping.");
218
+ return null;
209
219
  }
210
220
 
211
221
  function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
@@ -221,6 +231,51 @@ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
221
231
  }
222
232
  }
223
233
 
234
+ /**
235
+ * Parse heading+list format into a nested object, then cast to GSDPreferences.
236
+ * Handles markdown like:
237
+ * ## Git
238
+ * - isolation: none
239
+ * - commit_docs: true
240
+ * ## Models
241
+ * - planner: sonnet
242
+ */
243
+ function parseHeadingListFormat(content: string): GSDPreferences {
244
+ const result: Record<string, Record<string, string>> = {};
245
+ let currentSection: string | null = null;
246
+
247
+ for (const rawLine of content.split('\n')) {
248
+ const line = rawLine.replace(/\r$/, '');
249
+ const headingMatch = line.match(/^##\s+(.+)$/);
250
+ if (headingMatch) {
251
+ currentSection = headingMatch[1].trim().toLowerCase().replace(/\s+/g, '_');
252
+ continue;
253
+ }
254
+ if (currentSection) {
255
+ const itemMatch = line.match(/^-\s+([^:]+):\s*(.*)$/);
256
+ if (itemMatch) {
257
+ if (!result[currentSection]) result[currentSection] = {};
258
+ const value = itemMatch[2].trim();
259
+ // Coerce "true"/"false" strings and numbers
260
+ result[currentSection][itemMatch[1].trim()] = value;
261
+ }
262
+ }
263
+ }
264
+
265
+ // Convert string values to appropriate types via YAML parser for each section
266
+ const typed: Record<string, unknown> = {};
267
+ for (const [section, entries] of Object.entries(result)) {
268
+ const yamlLines = Object.entries(entries).map(([k, v]) => `${k}: ${v}`).join('\n');
269
+ try {
270
+ typed[section] = parseYaml(yamlLines);
271
+ } catch {
272
+ typed[section] = entries;
273
+ }
274
+ }
275
+
276
+ return typed as GSDPreferences;
277
+ }
278
+
224
279
  // ─── Merging ────────────────────────────────────────────────────────────────
225
280
 
226
281
  /**
@@ -286,6 +341,7 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
286
341
  ? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
287
342
  : undefined,
288
343
  service_tier: override.service_tier ?? base.service_tier,
344
+ forensics_dedup: override.forensics_dedup ?? base.forensics_dedup,
289
345
  };
290
346
  }
291
347
 
@@ -101,11 +101,19 @@ Explain your findings:
101
101
  - **Code snippet** — the problematic code and what it should do instead
102
102
  - **Recovery** — what the user can do right now to get unstuck
103
103
 
104
+ {{dedupSection}}
105
+
104
106
  Then **offer GitHub issue creation**: "Would you like me to create a GitHub issue for this on gsd-build/gsd-2?"
105
107
 
106
- If yes, create using `gh issue create` with this format:
108
+ **CRITICAL: The `github_issues` tool ONLY targets the current user's repository — it has no `repo` parameter. You MUST use `gh issue create --repo gsd-build/gsd-2` via the `bash` tool to file on the correct repo. Do NOT use the `github_issues` tool for this.**
107
109
 
108
- ```
110
+ If yes, create using the `bash` tool:
111
+
112
+ ```bash
113
+ gh issue create --repo gsd-build/gsd-2 \
114
+ --title "..." \
115
+ --label "bug" --label "auto-generated" \
116
+ --body "$(cat <<'EOF'
109
117
  ## Problem
110
118
  [1-2 sentence summary]
111
119
 
@@ -128,11 +136,10 @@ If yes, create using `gh issue create` with this format:
128
136
 
129
137
  ---
130
138
  *Auto-generated by `/gsd forensics`*
139
+ EOF
140
+ )"
131
141
  ```
132
142
 
133
- **Repository:** gsd-build/gsd-2
134
- **Labels:** bug, auto-generated
135
-
136
143
  ### Redaction Rules (CRITICAL)
137
144
 
138
145
  Before creating the issue, you MUST:
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { createHash } from "node:crypto";
10
10
  import { execFileSync } from "node:child_process";
11
- import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
11
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
12
12
  import { homedir } from "node:os";
13
13
  import { basename, dirname, join, resolve } from "node:path";
14
14
 
@@ -271,15 +271,54 @@ export function externalProjectsRoot(): string {
271
271
  return join(base, "projects");
272
272
  }
273
273
 
274
+ // ─── Numbered Variant Cleanup ────────────────────────────────────────────────
275
+
276
+ /**
277
+ * macOS collision pattern: `.gsd 2`, `.gsd 3`, `.gsd 4`, etc.
278
+ *
279
+ * When `symlinkSync` (or Finder) tries to create `.gsd` but a real directory
280
+ * already exists at that path, macOS APFS silently renames the new entry to
281
+ * `.gsd 2`, then `.gsd 3`, and so on. These numbered variants confuse GSD
282
+ * because the canonical `.gsd` path no longer resolves to the external state
283
+ * directory, making tracked planning files appear deleted.
284
+ *
285
+ * This helper scans the project root for entries matching `.gsd <digits>` and
286
+ * removes them. It is called early in `ensureGsdSymlink()` so that the
287
+ * canonical `.gsd` path is always the one in use.
288
+ */
289
+ const GSD_NUMBERED_VARIANT_RE = /^\.gsd \d+$/;
290
+
291
+ export function cleanNumberedGsdVariants(projectPath: string): string[] {
292
+ const removed: string[] = [];
293
+ try {
294
+ const entries = readdirSync(projectPath);
295
+ for (const entry of entries) {
296
+ if (GSD_NUMBERED_VARIANT_RE.test(entry)) {
297
+ const fullPath = join(projectPath, entry);
298
+ try {
299
+ rmSync(fullPath, { recursive: true, force: true });
300
+ removed.push(entry);
301
+ } catch {
302
+ // Best-effort: if removal fails (e.g. permissions), continue with next
303
+ }
304
+ }
305
+ }
306
+ } catch {
307
+ // Non-fatal: readdir failure should not block symlink creation
308
+ }
309
+ return removed;
310
+ }
311
+
274
312
  // ─── Symlink Management ─────────────────────────────────────────────────────
275
313
 
276
314
  /**
277
315
  * Ensure the `<project>/.gsd` symlink points to the external state directory.
278
316
  *
279
- * 1. mkdir -p the external dir
280
- * 2. If `<project>/.gsd` doesn't exist → create symlink
281
- * 3. If `<project>/.gsd` is already the correct symlink → no-op
282
- * 4. If `<project>/.gsd` is a real directoryreturn as-is (migration handles later)
317
+ * 1. Clean up any macOS numbered collision variants (`.gsd 2`, `.gsd 3`, etc.)
318
+ * 2. mkdir -p the external dir
319
+ * 3. If `<project>/.gsd` doesn't exist create symlink
320
+ * 4. If `<project>/.gsd` is already the correct symlink no-op
321
+ * 5. If `<project>/.gsd` is a real directory → return as-is (migration handles later)
283
322
  *
284
323
  * Returns the resolved external path.
285
324
  */
@@ -297,6 +336,10 @@ export function ensureGsdSymlink(projectPath: string): string {
297
336
  return localGsd;
298
337
  }
299
338
 
339
+ // Clean up macOS numbered collision variants (.gsd 2, .gsd 3, etc.) before
340
+ // any existence checks — otherwise they accumulate and confuse state (#2205).
341
+ cleanNumberedGsdVariants(projectPath);
342
+
300
343
  // Ensure external directory exists
301
344
  mkdirSync(externalPath, { recursive: true });
302
345