gsd-pi 2.37.1 → 2.38.0-dev.29edcdc

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 (239) hide show
  1. package/README.md +1 -1
  2. package/dist/app-paths.js +1 -1
  3. package/dist/cli.js +9 -0
  4. package/dist/extension-discovery.d.ts +5 -3
  5. package/dist/extension-discovery.js +14 -9
  6. package/dist/extension-registry.js +2 -2
  7. package/dist/onboarding.js +1 -0
  8. package/dist/remote-questions-config.js +2 -2
  9. package/dist/resource-loader.js +34 -1
  10. package/dist/resources/extensions/browser-tools/package.json +3 -1
  11. package/dist/resources/extensions/cmux/index.js +55 -1
  12. package/dist/resources/extensions/context7/package.json +1 -1
  13. package/dist/resources/extensions/env-utils.js +29 -0
  14. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  15. package/dist/resources/extensions/github-sync/cli.js +284 -0
  16. package/dist/resources/extensions/github-sync/index.js +73 -0
  17. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  18. package/dist/resources/extensions/github-sync/sync.js +424 -0
  19. package/dist/resources/extensions/github-sync/templates.js +118 -0
  20. package/dist/resources/extensions/github-sync/types.js +7 -0
  21. package/dist/resources/extensions/google-search/package.json +3 -1
  22. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  23. package/dist/resources/extensions/gsd/auto-dispatch.js +75 -10
  24. package/dist/resources/extensions/gsd/auto-loop.js +597 -588
  25. package/dist/resources/extensions/gsd/auto-post-unit.js +111 -68
  26. package/dist/resources/extensions/gsd/auto-prompts.js +114 -45
  27. package/dist/resources/extensions/gsd/auto-recovery.js +37 -1
  28. package/dist/resources/extensions/gsd/auto-start.js +13 -2
  29. package/dist/resources/extensions/gsd/auto-worktree-sync.js +13 -5
  30. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  31. package/dist/resources/extensions/gsd/auto.js +143 -96
  32. package/dist/resources/extensions/gsd/captures.js +9 -1
  33. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  34. package/dist/resources/extensions/gsd/commands-handlers.js +16 -3
  35. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  36. package/dist/resources/extensions/gsd/commands.js +24 -3
  37. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  38. package/dist/resources/extensions/gsd/detection.js +1 -2
  39. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  40. package/dist/resources/extensions/gsd/doctor-checks.js +82 -0
  41. package/dist/resources/extensions/gsd/doctor-environment.js +78 -0
  42. package/dist/resources/extensions/gsd/doctor-format.js +15 -0
  43. package/dist/resources/extensions/gsd/doctor-providers.js +62 -12
  44. package/dist/resources/extensions/gsd/doctor.js +204 -12
  45. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  46. package/dist/resources/extensions/gsd/export.js +1 -1
  47. package/dist/resources/extensions/gsd/files.js +47 -2
  48. package/dist/resources/extensions/gsd/forensics.js +1 -1
  49. package/dist/resources/extensions/gsd/git-service.js +15 -12
  50. package/dist/resources/extensions/gsd/guided-flow.js +82 -32
  51. package/dist/resources/extensions/gsd/index.js +24 -20
  52. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  53. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  54. package/dist/resources/extensions/gsd/observability-validator.js +24 -0
  55. package/dist/resources/extensions/gsd/package.json +1 -1
  56. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  57. package/dist/resources/extensions/gsd/preferences-types.js +3 -2
  58. package/dist/resources/extensions/gsd/preferences-validation.js +101 -11
  59. package/dist/resources/extensions/gsd/preferences.js +8 -5
  60. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  61. package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -2
  62. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  63. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  64. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  68. package/dist/resources/extensions/gsd/prompts/run-uat.md +27 -10
  69. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  70. package/dist/resources/extensions/gsd/reactive-graph.js +227 -0
  71. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  72. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  73. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  74. package/dist/resources/extensions/gsd/state.js +1 -1
  75. package/dist/resources/extensions/gsd/templates/task-plan.md +11 -3
  76. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  77. package/dist/resources/extensions/gsd/worktree.js +35 -16
  78. package/dist/resources/extensions/mcp-client/index.js +14 -1
  79. package/dist/resources/extensions/remote-questions/status.js +2 -1
  80. package/dist/resources/extensions/remote-questions/store.js +2 -1
  81. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  82. package/dist/resources/extensions/subagent/index.js +12 -3
  83. package/dist/resources/extensions/subagent/isolation.js +2 -1
  84. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  85. package/dist/resources/extensions/universal-config/package.json +1 -1
  86. package/dist/welcome-screen.d.ts +12 -0
  87. package/dist/welcome-screen.js +53 -0
  88. package/package.json +2 -1
  89. package/packages/pi-ai/dist/env-api-keys.js +13 -0
  90. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  91. package/packages/pi-ai/dist/models.generated.d.ts +172 -0
  92. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  93. package/packages/pi-ai/dist/models.generated.js +172 -0
  94. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  95. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts +64 -0
  96. package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -0
  97. package/packages/pi-ai/dist/providers/anthropic-shared.js +668 -0
  98. package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -0
  99. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts +5 -0
  100. package/packages/pi-ai/dist/providers/anthropic-vertex.d.ts.map +1 -0
  101. package/packages/pi-ai/dist/providers/anthropic-vertex.js +85 -0
  102. package/packages/pi-ai/dist/providers/anthropic-vertex.js.map +1 -0
  103. package/packages/pi-ai/dist/providers/anthropic.d.ts +4 -30
  104. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  105. package/packages/pi-ai/dist/providers/anthropic.js +47 -764
  106. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  107. package/packages/pi-ai/dist/providers/register-builtins.d.ts.map +1 -1
  108. package/packages/pi-ai/dist/providers/register-builtins.js +6 -0
  109. package/packages/pi-ai/dist/providers/register-builtins.js.map +1 -1
  110. package/packages/pi-ai/dist/types.d.ts +2 -2
  111. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  112. package/packages/pi-ai/dist/types.js.map +1 -1
  113. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  114. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  115. package/packages/pi-ai/package.json +1 -0
  116. package/packages/pi-ai/src/env-api-keys.ts +14 -0
  117. package/packages/pi-ai/src/models.generated.ts +172 -0
  118. package/packages/pi-ai/src/providers/anthropic-shared.ts +761 -0
  119. package/packages/pi-ai/src/providers/anthropic-vertex.ts +130 -0
  120. package/packages/pi-ai/src/providers/anthropic.ts +76 -868
  121. package/packages/pi-ai/src/providers/register-builtins.ts +7 -0
  122. package/packages/pi-ai/src/types.ts +2 -0
  123. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  124. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  125. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  126. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  127. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  128. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  129. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  130. package/packages/pi-coding-agent/dist/core/package-manager.d.ts.map +1 -1
  131. package/packages/pi-coding-agent/dist/core/package-manager.js +8 -4
  132. package/packages/pi-coding-agent/dist/core/package-manager.js.map +1 -1
  133. package/packages/pi-coding-agent/package.json +1 -1
  134. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  135. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  136. package/packages/pi-coding-agent/src/core/package-manager.ts +8 -4
  137. package/pkg/package.json +1 -1
  138. package/src/resources/extensions/cmux/index.ts +57 -1
  139. package/src/resources/extensions/env-utils.ts +31 -0
  140. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  141. package/src/resources/extensions/github-sync/cli.ts +364 -0
  142. package/src/resources/extensions/github-sync/index.ts +93 -0
  143. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  144. package/src/resources/extensions/github-sync/sync.ts +556 -0
  145. package/src/resources/extensions/github-sync/templates.ts +183 -0
  146. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  147. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  148. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  149. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  150. package/src/resources/extensions/github-sync/types.ts +47 -0
  151. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  152. package/src/resources/extensions/gsd/auto-dispatch.ts +100 -9
  153. package/src/resources/extensions/gsd/auto-loop.ts +484 -546
  154. package/src/resources/extensions/gsd/auto-post-unit.ts +92 -42
  155. package/src/resources/extensions/gsd/auto-prompts.ts +150 -48
  156. package/src/resources/extensions/gsd/auto-recovery.ts +42 -0
  157. package/src/resources/extensions/gsd/auto-start.ts +18 -2
  158. package/src/resources/extensions/gsd/auto-worktree-sync.ts +15 -4
  159. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  160. package/src/resources/extensions/gsd/auto.ts +139 -101
  161. package/src/resources/extensions/gsd/captures.ts +10 -1
  162. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  163. package/src/resources/extensions/gsd/commands-handlers.ts +17 -2
  164. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  165. package/src/resources/extensions/gsd/commands.ts +26 -4
  166. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  167. package/src/resources/extensions/gsd/detection.ts +2 -2
  168. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  169. package/src/resources/extensions/gsd/doctor-checks.ts +75 -0
  170. package/src/resources/extensions/gsd/doctor-environment.ts +82 -1
  171. package/src/resources/extensions/gsd/doctor-format.ts +20 -0
  172. package/src/resources/extensions/gsd/doctor-providers.ts +64 -10
  173. package/src/resources/extensions/gsd/doctor-types.ts +16 -1
  174. package/src/resources/extensions/gsd/doctor.ts +199 -14
  175. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  176. package/src/resources/extensions/gsd/export.ts +1 -1
  177. package/src/resources/extensions/gsd/files.ts +50 -3
  178. package/src/resources/extensions/gsd/forensics.ts +1 -1
  179. package/src/resources/extensions/gsd/git-service.ts +20 -10
  180. package/src/resources/extensions/gsd/guided-flow.ts +110 -38
  181. package/src/resources/extensions/gsd/index.ts +24 -17
  182. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  183. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  184. package/src/resources/extensions/gsd/observability-validator.ts +27 -0
  185. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  186. package/src/resources/extensions/gsd/preferences-types.ts +9 -5
  187. package/src/resources/extensions/gsd/preferences-validation.ts +92 -11
  188. package/src/resources/extensions/gsd/preferences.ts +8 -5
  189. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  190. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  191. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  192. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  193. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  194. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -1
  195. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  196. package/src/resources/extensions/gsd/prompts/reactive-execute.md +44 -0
  197. package/src/resources/extensions/gsd/prompts/run-uat.md +27 -10
  198. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  199. package/src/resources/extensions/gsd/reactive-graph.ts +289 -0
  200. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  201. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  202. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  203. package/src/resources/extensions/gsd/state.ts +1 -1
  204. package/src/resources/extensions/gsd/templates/task-plan.md +11 -3
  205. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  206. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  207. package/src/resources/extensions/gsd/tests/cmux.test.ts +93 -0
  208. package/src/resources/extensions/gsd/tests/doctor-enhancements.test.ts +266 -0
  209. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +191 -3
  210. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +111 -0
  211. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  212. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  213. package/src/resources/extensions/gsd/tests/reactive-executor.test.ts +511 -0
  214. package/src/resources/extensions/gsd/tests/reactive-graph.test.ts +299 -0
  215. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  216. package/src/resources/extensions/gsd/tests/run-uat.test.ts +11 -3
  217. package/src/resources/extensions/gsd/tests/worktree.test.ts +47 -0
  218. package/src/resources/extensions/gsd/types.ts +43 -1
  219. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  220. package/src/resources/extensions/gsd/worktree.ts +35 -15
  221. package/src/resources/extensions/mcp-client/index.ts +17 -1
  222. package/src/resources/extensions/remote-questions/status.ts +3 -1
  223. package/src/resources/extensions/remote-questions/store.ts +3 -1
  224. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  225. package/src/resources/extensions/subagent/index.ts +12 -3
  226. package/src/resources/extensions/subagent/isolation.ts +3 -1
  227. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  228. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  229. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  230. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  231. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  232. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  233. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  234. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  235. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  236. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  237. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  238. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  239. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -0,0 +1,289 @@
1
+ /**
2
+ * Reactive Task Graph — derives dependency edges from task plan IO signatures.
3
+ *
4
+ * Pure functions that build a DAG from task IO intersections and resolve
5
+ * which tasks are currently ready for parallel dispatch. Used by the
6
+ * reactive-execute dispatch path (ADR-004).
7
+ *
8
+ * Graph derivation and resolution functions are pure (no filesystem access).
9
+ * The `loadSliceTaskIO` loader at the bottom is the only async/IO function.
10
+ */
11
+
12
+ import type { TaskIO, DerivedTaskNode, ReactiveExecutionState } from "./types.js";
13
+ import { loadFile, parsePlan, parseTaskPlanIO } from "./files.js";
14
+ import { resolveTasksDir, resolveTaskFiles } from "./paths.js";
15
+ import { join } from "node:path";
16
+ import { loadJsonFileOrNull, saveJsonFile } from "./json-persistence.js";
17
+ import { existsSync, unlinkSync } from "node:fs";
18
+
19
+ // ─── Graph Construction ───────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Build a dependency graph from task IO signatures.
23
+ *
24
+ * A task T_b depends on T_a when any of T_b's inputFiles appear in T_a's
25
+ * outputFiles. Self-references are excluded.
26
+ *
27
+ * Tasks are returned in the same order as the input array.
28
+ */
29
+ export function deriveTaskGraph(tasks: TaskIO[]): DerivedTaskNode[] {
30
+ // Build output → producer lookup
31
+ const outputToProducer = new Map<string, string[]>();
32
+ for (const task of tasks) {
33
+ for (const outFile of task.outputFiles) {
34
+ const existing = outputToProducer.get(outFile);
35
+ if (existing) {
36
+ existing.push(task.id);
37
+ } else {
38
+ outputToProducer.set(outFile, [task.id]);
39
+ }
40
+ }
41
+ }
42
+
43
+ return tasks.map((task) => {
44
+ const deps = new Set<string>();
45
+ for (const inFile of task.inputFiles) {
46
+ const producers = outputToProducer.get(inFile);
47
+ if (producers) {
48
+ for (const pid of producers) {
49
+ if (pid !== task.id) deps.add(pid);
50
+ }
51
+ }
52
+ }
53
+ return {
54
+ ...task,
55
+ dependsOn: [...deps].sort(),
56
+ };
57
+ });
58
+ }
59
+
60
+ // ─── Ready Set Resolution ─────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Return task IDs whose dependencies are all in `completed`.
64
+ * Excludes tasks that are already done or in-flight.
65
+ */
66
+ export function getReadyTasks(
67
+ graph: DerivedTaskNode[],
68
+ completed: Set<string>,
69
+ inFlight: Set<string>,
70
+ ): string[] {
71
+ return graph
72
+ .filter((node) => {
73
+ if (node.done || completed.has(node.id) || inFlight.has(node.id)) return false;
74
+ return node.dependsOn.every((dep) => completed.has(dep));
75
+ })
76
+ .map((node) => node.id);
77
+ }
78
+
79
+ // ─── Conflict-Free Subset Selection ──────────────────────────────────────
80
+
81
+ /**
82
+ * Greedy selection of non-conflicting tasks up to `maxParallel`.
83
+ *
84
+ * Two tasks conflict if they share any outputFile. We also exclude tasks
85
+ * whose outputs overlap with `inFlightOutputs` (files being written by
86
+ * tasks currently in progress).
87
+ */
88
+ export function chooseNonConflictingSubset(
89
+ readyIds: string[],
90
+ graph: DerivedTaskNode[],
91
+ maxParallel: number,
92
+ inFlightOutputs: Set<string>,
93
+ ): string[] {
94
+ const nodeMap = new Map(graph.map((n) => [n.id, n]));
95
+ const claimed = new Set(inFlightOutputs);
96
+ const selected: string[] = [];
97
+
98
+ for (const id of readyIds) {
99
+ if (selected.length >= maxParallel) break;
100
+ const node = nodeMap.get(id);
101
+ if (!node) continue;
102
+
103
+ // Check for output overlap with already-selected or in-flight
104
+ const conflicts = node.outputFiles.some((f) => claimed.has(f));
105
+ if (conflicts) continue;
106
+
107
+ // Claim this task's outputs
108
+ for (const f of node.outputFiles) claimed.add(f);
109
+ selected.push(id);
110
+ }
111
+
112
+ return selected;
113
+ }
114
+
115
+ // ─── Graph Quality Checks ─────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Returns true if any incomplete task has 0 inputFiles AND 0 outputFiles.
119
+ *
120
+ * An ambiguous graph means IO annotations are too sparse to derive reliable
121
+ * edges — the dispatcher should fall back to sequential execution.
122
+ */
123
+ export function isGraphAmbiguous(graph: DerivedTaskNode[]): boolean {
124
+ return graph.some(
125
+ (node) =>
126
+ !node.done &&
127
+ node.inputFiles.length === 0 &&
128
+ node.outputFiles.length === 0,
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Detect deadlock: no tasks are ready and none are in-flight, yet incomplete
134
+ * tasks remain. This indicates a circular dependency or impossible state.
135
+ */
136
+ export function detectDeadlock(
137
+ graph: DerivedTaskNode[],
138
+ completed: Set<string>,
139
+ inFlight: Set<string>,
140
+ ): boolean {
141
+ const incomplete = graph.filter(
142
+ (n) => !n.done && !completed.has(n.id) && !inFlight.has(n.id),
143
+ );
144
+ if (incomplete.length === 0) return false; // all done
145
+ if (inFlight.size > 0) return false; // something is running, wait for it
146
+
147
+ // Nothing in flight, but incomplete tasks remain — check if any are ready
148
+ const ready = getReadyTasks(graph, completed, inFlight);
149
+ return ready.length === 0;
150
+ }
151
+
152
+ // ─── Graph Metrics ────────────────────────────────────────────────────────
153
+
154
+ /** Compute summary metrics for logging. */
155
+ export function graphMetrics(graph: DerivedTaskNode[]): {
156
+ taskCount: number;
157
+ edgeCount: number;
158
+ readySetSize: number;
159
+ ambiguous: boolean;
160
+ } {
161
+ const completed = new Set(graph.filter((n) => n.done).map((n) => n.id));
162
+ const ready = getReadyTasks(graph, completed, new Set());
163
+ const edgeCount = graph.reduce((sum, n) => sum + n.dependsOn.length, 0);
164
+
165
+ return {
166
+ taskCount: graph.length,
167
+ edgeCount,
168
+ readySetSize: ready.length,
169
+ ambiguous: isGraphAmbiguous(graph),
170
+ };
171
+ }
172
+
173
+ // ─── IO Loader (async, filesystem) ────────────────────────────────────────
174
+
175
+ /**
176
+ * Load TaskIO for all tasks in a slice by reading the slice plan (for done
177
+ * status and task IDs) and individual task plan files (for IO sections).
178
+ *
179
+ * Returns [] when the slice plan or tasks directory doesn't exist.
180
+ */
181
+ export async function loadSliceTaskIO(
182
+ basePath: string,
183
+ mid: string,
184
+ sid: string,
185
+ ): Promise<TaskIO[]> {
186
+ const { resolveSliceFile } = await import("./paths.js");
187
+ const slicePlanPath = resolveSliceFile(basePath, mid, sid, "PLAN");
188
+ const planContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
189
+ if (!planContent) return [];
190
+
191
+ const plan = parsePlan(planContent);
192
+ const tDir = resolveTasksDir(basePath, mid, sid);
193
+ if (!tDir) return [];
194
+
195
+ const results: TaskIO[] = [];
196
+
197
+ for (const taskEntry of plan.tasks) {
198
+ const planFiles = resolveTaskFiles(tDir, "PLAN");
199
+ const taskFileName = planFiles.find((f) =>
200
+ f.toUpperCase().startsWith(taskEntry.id.toUpperCase() + "-"),
201
+ );
202
+ if (!taskFileName) {
203
+ // Task plan file missing — include with empty IO (will trigger ambiguous)
204
+ results.push({
205
+ id: taskEntry.id,
206
+ title: taskEntry.title,
207
+ inputFiles: [],
208
+ outputFiles: [],
209
+ done: taskEntry.done,
210
+ });
211
+ continue;
212
+ }
213
+
214
+ const taskContent = await loadFile(join(tDir, taskFileName));
215
+ if (!taskContent) {
216
+ results.push({
217
+ id: taskEntry.id,
218
+ title: taskEntry.title,
219
+ inputFiles: [],
220
+ outputFiles: [],
221
+ done: taskEntry.done,
222
+ });
223
+ continue;
224
+ }
225
+
226
+ const io = parseTaskPlanIO(taskContent);
227
+ results.push({
228
+ id: taskEntry.id,
229
+ title: taskEntry.title,
230
+ inputFiles: io.inputFiles,
231
+ outputFiles: io.outputFiles,
232
+ done: taskEntry.done,
233
+ });
234
+ }
235
+
236
+ return results;
237
+ }
238
+
239
+ // ─── State Persistence ────────────────────────────────────────────────────
240
+
241
+ function reactiveStatePath(basePath: string, mid: string, sid: string): string {
242
+ return join(basePath, ".gsd", "runtime", `${mid}-${sid}-reactive.json`);
243
+ }
244
+
245
+ function isReactiveState(data: unknown): data is ReactiveExecutionState {
246
+ if (!data || typeof data !== "object") return false;
247
+ const d = data as Record<string, unknown>;
248
+ return typeof d.sliceId === "string" && Array.isArray(d.completed) && Array.isArray(d.dispatched);
249
+ }
250
+
251
+ /**
252
+ * Load persisted reactive execution state for a slice.
253
+ * Returns null when no state file exists or the file is invalid.
254
+ */
255
+ export function loadReactiveState(
256
+ basePath: string,
257
+ mid: string,
258
+ sid: string,
259
+ ): ReactiveExecutionState | null {
260
+ return loadJsonFileOrNull(reactiveStatePath(basePath, mid, sid), isReactiveState);
261
+ }
262
+
263
+ /**
264
+ * Save reactive execution state to disk.
265
+ */
266
+ export function saveReactiveState(
267
+ basePath: string,
268
+ mid: string,
269
+ sid: string,
270
+ state: ReactiveExecutionState,
271
+ ): void {
272
+ saveJsonFile(reactiveStatePath(basePath, mid, sid), state);
273
+ }
274
+
275
+ /**
276
+ * Remove the reactive state file when a slice completes.
277
+ */
278
+ export function clearReactiveState(
279
+ basePath: string,
280
+ mid: string,
281
+ sid: string,
282
+ ): void {
283
+ const path = reactiveStatePath(basePath, mid, sid);
284
+ try {
285
+ if (existsSync(path)) unlinkSync(path);
286
+ } catch {
287
+ // Non-fatal
288
+ }
289
+ }
@@ -12,6 +12,8 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, s
12
12
  import { homedir } from "node:os";
13
13
  import { join, resolve } from "node:path";
14
14
 
15
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
16
+
15
17
  // ─── Repo Identity ──────────────────────────────────────────────────────────
16
18
 
17
19
  /**
@@ -90,14 +92,31 @@ function resolveGitRoot(basePath: string): string {
90
92
  }
91
93
  }
92
94
 
95
+ /**
96
+ * Validate a GSD_PROJECT_ID value.
97
+ *
98
+ * Must contain only alphanumeric characters, hyphens, and underscores.
99
+ * Call this once at startup so the user gets immediate feedback on bad values.
100
+ */
101
+ export function validateProjectId(id: string): boolean {
102
+ return /^[a-zA-Z0-9_-]+$/.test(id);
103
+ }
104
+
93
105
  /**
94
106
  * Compute a stable identity for a repository.
95
107
  *
96
- * SHA-256 of `${remoteUrl}\n${resolvedRoot}`, truncated to 12 hex chars.
97
- * Deterministic: same repo always produces the same hash regardless of
98
- * which worktree the caller is inside.
108
+ * If `GSD_PROJECT_ID` is set, returns it directly (validation is expected
109
+ * to have already happened at startup via `validateProjectId`).
110
+ *
111
+ * Otherwise returns SHA-256 of `${remoteUrl}\n${resolvedRoot}`, truncated
112
+ * to 12 hex chars. Deterministic: same repo always produces the same hash
113
+ * regardless of which worktree the caller is inside.
99
114
  */
100
115
  export function repoIdentity(basePath: string): string {
116
+ const projectId = process.env.GSD_PROJECT_ID;
117
+ if (projectId) {
118
+ return projectId;
119
+ }
101
120
  const remoteUrl = getRemoteUrl(basePath);
102
121
  const root = resolveGitRoot(basePath);
103
122
  const input = `${remoteUrl}\n${root}`;
@@ -113,7 +132,7 @@ export function repoIdentity(basePath: string): string {
113
132
  * otherwise `~/.gsd/projects/<hash>`.
114
133
  */
115
134
  export function externalGsdRoot(basePath: string): string {
116
- const base = process.env.GSD_STATE_DIR || join(homedir(), ".gsd");
135
+ const base = process.env.GSD_STATE_DIR || gsdHome;
117
136
  return join(base, "projects", repoIdentity(basePath));
118
137
  }
119
138
 
@@ -11,6 +11,8 @@ import { join } from "node:path";
11
11
  import { homedir } from "node:os";
12
12
  import { resolveProjectRoot } from "./worktree.js";
13
13
 
14
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
15
+
14
16
  // ─── Resource Staleness ───────────────────────────────────────────────────
15
17
 
16
18
  /**
@@ -23,7 +25,7 @@ function isManifestWithVersion(data: unknown): data is { gsdVersion: string } {
23
25
  }
24
26
 
25
27
  export function readResourceVersion(): string | null {
26
- const agentDir = process.env.GSD_CODING_AGENT_DIR || join(homedir(), ".gsd", "agent");
28
+ const agentDir = process.env.GSD_CODING_AGENT_DIR || join(gsdHome, "agent");
27
29
  const manifestPath = join(agentDir, "managed-resources.json");
28
30
  const manifest = loadJsonFileOrNull(manifestPath, isManifestWithVersion);
29
31
  return manifest?.gsdVersion ?? null;
@@ -39,6 +39,35 @@ export function markSliceDoneInRoadmap(basePath: string, mid: string, sid: strin
39
39
  return true;
40
40
  }
41
41
 
42
+ /**
43
+ * Mark a slice as not done ([ ]) in the milestone roadmap.
44
+ * Idempotent — no-op if already unchecked or if the slice isn't found.
45
+ *
46
+ * @returns true if the roadmap was modified, false if no change was needed
47
+ */
48
+ export function markSliceUndoneInRoadmap(basePath: string, mid: string, sid: string): boolean {
49
+ const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
50
+ if (!roadmapFile) return false;
51
+
52
+ let content: string;
53
+ try {
54
+ content = readFileSync(roadmapFile, "utf-8");
55
+ } catch {
56
+ return false;
57
+ }
58
+
59
+ const updated = content.replace(
60
+ new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sid}:`, "m"),
61
+ `$1[ ] **${sid}:`,
62
+ );
63
+
64
+ if (updated === content) return false;
65
+
66
+ atomicWriteSync(roadmapFile, updated);
67
+ clearParseCache();
68
+ return true;
69
+ }
70
+
42
71
  /**
43
72
  * Mark a task as done ([x]) in the slice plan.
44
73
  * Idempotent — no-op if already checked or if the task isn't found.
@@ -31,7 +31,7 @@ import {
31
31
  gsdRoot,
32
32
  } from './paths.js';
33
33
 
34
- import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
34
+ import { milestoneIdSort, findMilestoneIds } from './milestone-ids.js';
35
35
  import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
36
36
 
37
37
  import { join, resolve } from 'path';
@@ -42,11 +42,19 @@ estimated_files: {{estimatedFiles}}
42
42
 
43
43
  ## Inputs
44
44
 
45
+ <!-- Every input MUST be a backtick-wrapped file path. These paths are machine-parsed to
46
+ derive task dependencies — vague descriptions without paths break dependency detection.
47
+ For the first task in a slice with no prior task outputs, list the existing source files
48
+ this task reads or modifies. -->
49
+
45
50
  - `{{filePath}}` — {{whatThisTaskNeedsFromPriorWork}}
46
- - {{priorTaskSummaryInsight}}
47
51
 
48
52
  ## Expected Output
49
53
 
50
- <!-- This task should produce a real increment toward making the slice goal/demo true. A full slice plan should not be able to mark every task complete while the claimed slice behavior still does not work at the stated proof level. -->
54
+ <!-- Every output MUST be a backtick-wrapped file path the specific files this task creates
55
+ or modifies. These paths are machine-parsed to derive task dependencies.
56
+ This task should produce a real increment toward making the slice goal/demo true. A full
57
+ slice plan should not be able to mark every task complete while the claimed slice behavior
58
+ still does not work at the stated proof level. -->
51
59
 
52
- - `{{filePath}}` — {{whatThisTaskShouldProduceOrModify}}
60
+ - `{{filePath}}` — {{whatThisTaskCreatesOrModifies}}
@@ -1,9 +1,9 @@
1
1
  /**
2
- * agent-end-retry.test.ts — Regression checks for the post-#1419 agent_end model.
2
+ * agent-end-retry.test.ts — Regression checks for the agent_end model.
3
3
  *
4
- * The old recursive handleAgentEnd retry path is gone. The loop now keeps
5
- * pendingResolve + pendingAgentEndQueue on AutoSession, and handleAgentEnd is
6
- * only a thin compatibility wrapper around resolveAgentEnd().
4
+ * The per-unit one-shot resolve function lives at module level in auto-loop.ts
5
+ * (_currentResolve). handleAgentEnd is a thin compatibility wrapper around
6
+ * resolveAgentEnd().
7
7
  */
8
8
 
9
9
  import test from "node:test";
@@ -14,40 +14,43 @@ import { fileURLToPath } from "node:url";
14
14
 
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  const AUTO_TS_PATH = join(__dirname, "..", "auto.ts");
17
+ const AUTO_LOOP_TS_PATH = join(__dirname, "..", "auto-loop.ts");
17
18
  const SESSION_TS_PATH = join(__dirname, "..", "auto", "session.ts");
18
19
 
19
20
  function getAutoTsSource(): string {
20
21
  return readFileSync(AUTO_TS_PATH, "utf-8");
21
22
  }
22
23
 
24
+ function getAutoLoopTsSource(): string {
25
+ return readFileSync(AUTO_LOOP_TS_PATH, "utf-8");
26
+ }
27
+
23
28
  function getSessionTsSource(): string {
24
29
  return readFileSync(SESSION_TS_PATH, "utf-8");
25
30
  }
26
31
 
27
- test("AutoSession declares pending agent_end queue state", () => {
28
- const source = getSessionTsSource();
32
+ test("auto-loop.ts declares _currentResolve for per-unit one-shot promises", () => {
33
+ const source = getAutoLoopTsSource();
29
34
  assert.ok(
30
- source.includes("pendingResolve"),
31
- "AutoSession must declare pendingResolve for the in-flight unit promise",
35
+ source.includes("_currentResolve"),
36
+ "auto-loop.ts must declare _currentResolve for the per-unit resolve function",
32
37
  );
33
38
  assert.ok(
34
- source.includes("pendingAgentEndQueue"),
35
- "AutoSession must declare pendingAgentEndQueue for between-iteration agent_end events",
39
+ source.includes("_sessionSwitchInFlight"),
40
+ "auto-loop.ts must declare _sessionSwitchInFlight guard",
36
41
  );
37
42
  });
38
43
 
39
- test("AutoSession reset clears pending agent_end queue state", () => {
44
+ test("AutoSession no longer holds promise state (moved to auto-loop.ts module scope)", () => {
40
45
  const source = getSessionTsSource();
41
- const resetIdx = source.indexOf("reset(): void");
42
- assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
43
- const resetBlock = source.slice(resetIdx, resetIdx + 4000);
46
+ // Properties should NOT exist as class fields
44
47
  assert.ok(
45
- resetBlock.includes("this.pendingResolve = null"),
46
- "reset() must clear pendingResolve",
48
+ !source.includes("pendingResolve:"),
49
+ "AutoSession must not declare pendingResolve (moved to auto-loop.ts)",
47
50
  );
48
51
  assert.ok(
49
- resetBlock.includes("this.pendingAgentEndQueue = []"),
50
- "reset() must clear pendingAgentEndQueue",
52
+ !source.includes("pendingAgentEndQueue:"),
53
+ "AutoSession must not declare pendingAgentEndQueue (removed — events are dropped)",
51
54
  );
52
55
  });
53
56