@backburner/cli 0.1.0

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 (202) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +329 -0
  3. package/dist/src/agents/antigravity-launch.js +162 -0
  4. package/dist/src/agents/antigravity.js +251 -0
  5. package/dist/src/agents/claude-launch.js +117 -0
  6. package/dist/src/agents/claude.js +271 -0
  7. package/dist/src/agents/codex-launch.js +82 -0
  8. package/dist/src/agents/codex.js +261 -0
  9. package/dist/src/agents/composite-runner.js +13 -0
  10. package/dist/src/agents/extraction.js +442 -0
  11. package/dist/src/agents/failure-feedback.js +132 -0
  12. package/dist/src/agents/gemini-launch.js +96 -0
  13. package/dist/src/agents/gemini.js +392 -0
  14. package/dist/src/agents/job-agent-assignments.js +180 -0
  15. package/dist/src/agents/lifecycle-feedback.js +147 -0
  16. package/dist/src/agents/limit-detector.js +83 -0
  17. package/dist/src/agents/opencode-launch.js +138 -0
  18. package/dist/src/agents/opencode.js +156 -0
  19. package/dist/src/agents/plan-breakdown-feedback.js +97 -0
  20. package/dist/src/agents/pool.js +168 -0
  21. package/dist/src/agents/prompt.js +462 -0
  22. package/dist/src/agents/provider-sessions.js +105 -0
  23. package/dist/src/agents/review-feedback.js +255 -0
  24. package/dist/src/agents/selection.js +113 -0
  25. package/dist/src/agents/service.js +1087 -0
  26. package/dist/src/agents/types.js +1 -0
  27. package/dist/src/agents/wave-assessment-feedback.js +69 -0
  28. package/dist/src/attention/derive.js +337 -0
  29. package/dist/src/capabilities/index.js +2 -0
  30. package/dist/src/capabilities/projection.js +183 -0
  31. package/dist/src/capabilities/types.js +1 -0
  32. package/dist/src/capabilities/validator.js +87 -0
  33. package/dist/src/cli/commands/broker-smoke.js +96 -0
  34. package/dist/src/cli/commands/broker.js +56 -0
  35. package/dist/src/cli/commands/init.js +4 -0
  36. package/dist/src/cli/commands/journal.js +43 -0
  37. package/dist/src/cli/commands/run-loop.js +56 -0
  38. package/dist/src/cli/commands/run.js +1241 -0
  39. package/dist/src/cli/commands/tui.js +187 -0
  40. package/dist/src/cli/dispatcher.js +51 -0
  41. package/dist/src/cli/errors.js +2 -0
  42. package/dist/src/cli/init.js +92 -0
  43. package/dist/src/cli/options.js +86 -0
  44. package/dist/src/cli/output.js +231 -0
  45. package/dist/src/cli/run-journal.js +342 -0
  46. package/dist/src/cli/run-types.js +1 -0
  47. package/dist/src/cli/run.js +32 -0
  48. package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
  49. package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
  50. package/dist/src/cli/runtime/provider-runtime.js +152 -0
  51. package/dist/src/cli/runtime/provider-tools.js +73 -0
  52. package/dist/src/cli/runtime/providers/antigravity.js +26 -0
  53. package/dist/src/cli/runtime/providers/claude.js +20 -0
  54. package/dist/src/cli/runtime/providers/codex.js +20 -0
  55. package/dist/src/cli/runtime/providers/gemini.js +20 -0
  56. package/dist/src/cli/runtime/providers/opencode.js +20 -0
  57. package/dist/src/cli/runtime/run-context.js +131 -0
  58. package/dist/src/cli/tui/index.js +3 -0
  59. package/dist/src/cli/tui/screen.js +388 -0
  60. package/dist/src/cli/tui/tracker.js +558 -0
  61. package/dist/src/cli/tui/types.js +1 -0
  62. package/dist/src/cli/tui/views.js +1006 -0
  63. package/dist/src/config/loader.js +136 -0
  64. package/dist/src/config/schemas.js +403 -0
  65. package/dist/src/config/types.js +1 -0
  66. package/dist/src/context/types.js +331 -0
  67. package/dist/src/domain/entities.js +1 -0
  68. package/dist/src/git/backburner-git-tool-service.js +680 -0
  69. package/dist/src/git/gitops-lifecycle.js +56 -0
  70. package/dist/src/git/merge-prep-workspace.js +357 -0
  71. package/dist/src/git/types.js +484 -0
  72. package/dist/src/github/broker-model-content.js +215 -0
  73. package/dist/src/github/broker-server.js +237 -0
  74. package/dist/src/github/broker.js +638 -0
  75. package/dist/src/github/composed.js +53 -0
  76. package/dist/src/github/gateway.js +478 -0
  77. package/dist/src/github/normalize.js +222 -0
  78. package/dist/src/github/pr-classification.js +73 -0
  79. package/dist/src/github/scoped-broker.js +35 -0
  80. package/dist/src/github/security.js +126 -0
  81. package/dist/src/github/service.js +332 -0
  82. package/dist/src/github/types.js +1 -0
  83. package/dist/src/github/utils.js +16 -0
  84. package/dist/src/journal/index.js +4 -0
  85. package/dist/src/journal/reader.js +62 -0
  86. package/dist/src/journal/renderer.js +50 -0
  87. package/dist/src/journal/service.js +72 -0
  88. package/dist/src/journal/types.js +1 -0
  89. package/dist/src/journal/writer.js +19 -0
  90. package/dist/src/mcps/bridge.js +166 -0
  91. package/dist/src/mcps/index.js +5 -0
  92. package/dist/src/mcps/reconcile.js +113 -0
  93. package/dist/src/mcps/smithery-client.js +186 -0
  94. package/dist/src/mcps/smithery.js +164 -0
  95. package/dist/src/mcps/types.js +1 -0
  96. package/dist/src/memory/json-store.js +71 -0
  97. package/dist/src/memory/prompt.js +16 -0
  98. package/dist/src/memory/scope.js +31 -0
  99. package/dist/src/memory/store.js +1 -0
  100. package/dist/src/memory/tool-service.js +142 -0
  101. package/dist/src/memory/types.js +12 -0
  102. package/dist/src/onboarding/config-writer.js +96 -0
  103. package/dist/src/onboarding/engine.js +86 -0
  104. package/dist/src/onboarding/prompt.js +102 -0
  105. package/dist/src/onboarding/services/provider-discovery.js +58 -0
  106. package/dist/src/onboarding/services/repo-discovery.js +246 -0
  107. package/dist/src/onboarding/steps/check-github-auth.js +32 -0
  108. package/dist/src/onboarding/steps/check-requirements.js +34 -0
  109. package/dist/src/onboarding/steps/configure-models.js +105 -0
  110. package/dist/src/onboarding/steps/configure-paths.js +33 -0
  111. package/dist/src/onboarding/steps/discover-providers.js +38 -0
  112. package/dist/src/onboarding/steps/discover-repos.js +39 -0
  113. package/dist/src/onboarding/steps/generate-config.js +106 -0
  114. package/dist/src/onboarding/steps/readiness-validation.js +158 -0
  115. package/dist/src/onboarding/steps/select-repos.js +76 -0
  116. package/dist/src/onboarding/steps/show-summary.js +97 -0
  117. package/dist/src/onboarding/steps/welcome.js +27 -0
  118. package/dist/src/onboarding/types.js +1 -0
  119. package/dist/src/onboarding/validators/git-access.js +63 -0
  120. package/dist/src/onboarding/validators/github-auth.js +85 -0
  121. package/dist/src/onboarding/validators/tool-checker.js +153 -0
  122. package/dist/src/onboarding/validators/workspace.js +83 -0
  123. package/dist/src/prompts/resolver.js +106 -0
  124. package/dist/src/retrospectives/candidates.js +224 -0
  125. package/dist/src/retrospectives/derive.js +169 -0
  126. package/dist/src/retrospectives/ingest.js +321 -0
  127. package/dist/src/retrospectives/management-prs.js +380 -0
  128. package/dist/src/retrospectives/proposals.js +199 -0
  129. package/dist/src/retrospectives/scopes.js +272 -0
  130. package/dist/src/retrospectives/scoring.js +171 -0
  131. package/dist/src/retrospectives/types.js +1 -0
  132. package/dist/src/state/loader.js +473 -0
  133. package/dist/src/state/types.js +15 -0
  134. package/dist/src/tasks/derivation/builders.js +70 -0
  135. package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
  136. package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
  137. package/dist/src/tasks/derivation/handlers/index.js +54 -0
  138. package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
  139. package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
  140. package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
  141. package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
  142. package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
  143. package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
  144. package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
  145. package/dist/src/tasks/derivation/handlers/shared.js +24 -0
  146. package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
  147. package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
  148. package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
  149. package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
  150. package/dist/src/tasks/derivation/runner.js +18 -0
  151. package/dist/src/tasks/derivation/types.js +1 -0
  152. package/dist/src/tasks/derive.js +810 -0
  153. package/dist/src/tasks/mcp-skip.js +10 -0
  154. package/dist/src/tasks/retry.js +10 -0
  155. package/dist/src/tasks/task.js +184 -0
  156. package/dist/src/tasks/types.js +1 -0
  157. package/dist/src/utils/command-runner.js +192 -0
  158. package/dist/src/utils/json.js +38 -0
  159. package/dist/src/utils/logger.js +43 -0
  160. package/dist/src/utils/paths.js +38 -0
  161. package/dist/src/utils/slug.js +10 -0
  162. package/dist/src/workflows/registry.js +174 -0
  163. package/dist/src/workflows/triage.js +90 -0
  164. package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
  165. package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
  166. package/dist/src/workflows/workstream-machine/events.js +1 -0
  167. package/dist/src/workflows/workstream-machine/machine.js +355 -0
  168. package/dist/src/workflows/workstream-machine/selectors.js +19 -0
  169. package/dist/src/workflows/workstream-machine/types.js +1 -0
  170. package/dist/src/workstreams/branch-sync.js +87 -0
  171. package/dist/src/workstreams/derive.js +629 -0
  172. package/dist/src/workstreams/mcp-blockers.js +420 -0
  173. package/dist/src/workstreams/packet-candidates.js +42 -0
  174. package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
  175. package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
  176. package/dist/src/workstreams/packet-plan.js +597 -0
  177. package/dist/src/workstreams/packet-projections.js +190 -0
  178. package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
  179. package/dist/src/workstreams/parent-branch-sync.js +148 -0
  180. package/dist/src/workstreams/phases.js +4 -0
  181. package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
  182. package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
  183. package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
  184. package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
  185. package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
  186. package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
  187. package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
  188. package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
  189. package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
  190. package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
  191. package/dist/src/workstreams/utils.js +15 -0
  192. package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
  193. package/dist/src/workstreams/wave-assessment/index.js +3 -0
  194. package/dist/src/workstreams/wave-assessment/session.js +84 -0
  195. package/dist/src/workstreams/wave-assessment/store.js +51 -0
  196. package/dist/src/workstreams/wave-assessment/tools.js +419 -0
  197. package/dist/src/workstreams/wave-assessment/types.js +1 -0
  198. package/dist/src/workstreams/wave-assessment/validate.js +78 -0
  199. package/dist/src/worktrees/service.js +839 -0
  200. package/dist/src/worktrees/workspace-ownership.js +34 -0
  201. package/dist/src/worktrees/workspace-recovery.js +177 -0
  202. package/package.json +61 -0
@@ -0,0 +1,839 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { mkdir, stat, writeFile } from "node:fs/promises";
4
+ import * as path from "node:path";
5
+ import { DefaultContextBuilder } from "../context/types.js";
6
+ import { slugify } from "../utils/slug.js";
7
+ import { getPacketSlug } from "../workstreams/utils.js";
8
+ import { workflowRegistry } from "../workflows/registry.js";
9
+ import { getCurrentWaveIndex, getUndispatchedCurrentWavePackets } from "../workstreams/packet-plan.js";
10
+ const WORKTREE_ROOT_DIR = "_worktrees";
11
+ const AGENT_DOCS_ROOT_DIR = path.join("docs", "agents");
12
+ export class ImplementationWorkflowService {
13
+ gitGateway;
14
+ gitHubGateway;
15
+ now;
16
+ constructor(gitGateway, gitHubGateway, now = () => new Date().toISOString()) {
17
+ this.gitGateway = gitGateway;
18
+ this.gitHubGateway = gitHubGateway;
19
+ this.now = now;
20
+ }
21
+ async ensureWorktreeForIssue(input) {
22
+ const branchName = buildWorktreeBranchName(input.repo, input.issue.number);
23
+ const baseBranch = input.repo.defaultBranch ?? "main";
24
+ const existing = input.existingWorktrees.find((worktree) => worktree.repoId === input.repo.id && worktree.issueNumber === input.issue.number) ??
25
+ input.existingWorktrees.find((worktree) => worktree.repoId === input.repo.id && worktree.branchName === branchName);
26
+ if (existing) {
27
+ const preparedPaths = await preparePlanDocuments(existing.path, input.issue, input.decision, existing.branchName);
28
+ return {
29
+ worktree: {
30
+ ...existing,
31
+ issueId: input.issue.id,
32
+ issueNumber: input.issue.number,
33
+ baseBranch: existing.baseBranch ?? baseBranch,
34
+ planDocumentPath: preparedPaths.planDocumentPath,
35
+ productSpecDocumentPath: preparedPaths.productSpecDocumentPath,
36
+ logDocumentPath: preparedPaths.logDocumentPath,
37
+ status: existing.status === "closed" ? "product_specification" : existing.status,
38
+ ...(input.triggerCommentText ? { triggerCommentText: input.triggerCommentText } : {}),
39
+ ...(input.triggerCommentUrl ? { triggerCommentUrl: input.triggerCommentUrl } : {}),
40
+ updatedAt: this.now()
41
+ },
42
+ created: false
43
+ };
44
+ }
45
+ const worktreePath = this.gitGateway.validatePathWithinCodeRoot(input.codeRoot, buildWorktreeRelativePath(input.repo, input.issue.number));
46
+ const startPoint = input.repo.defaultBranch ? `origin/${input.repo.defaultBranch}` : "origin/HEAD";
47
+ const createdPath = await this.gitGateway.createWorktree(input.repoPath, input.codeRoot, worktreePath, branchName, startPoint);
48
+ const preparedPaths = await preparePlanDocuments(createdPath, input.issue, input.decision, branchName);
49
+ const timestamp = this.now();
50
+ return {
51
+ created: true,
52
+ worktree: {
53
+ id: buildWorktreeId(input.repo.owner, input.repo.name, input.issue.number),
54
+ repoId: input.repo.id,
55
+ repoSlug: `${input.repo.owner}/${input.repo.name}`,
56
+ repoName: input.repo.name,
57
+ issueId: input.issue.id,
58
+ issueNumber: input.issue.number,
59
+ baseBranch,
60
+ branchName,
61
+ path: createdPath,
62
+ planDocumentPath: preparedPaths.planDocumentPath,
63
+ productSpecDocumentPath: preparedPaths.productSpecDocumentPath,
64
+ logDocumentPath: preparedPaths.logDocumentPath,
65
+ status: "product_specification",
66
+ ...(input.triggerCommentText ? { triggerCommentText: input.triggerCommentText } : {}),
67
+ ...(input.triggerCommentUrl ? { triggerCommentUrl: input.triggerCommentUrl } : {}),
68
+ createdAt: timestamp,
69
+ updatedAt: timestamp
70
+ }
71
+ };
72
+ }
73
+ async ensurePullRequestForWorktree(repo, issue, worktree) {
74
+ const existingPr = worktree.prId && worktree.prNumber !== undefined && worktree.prUrl
75
+ ? {
76
+ id: worktree.prId,
77
+ number: worktree.prNumber,
78
+ url: worktree.prUrl
79
+ }
80
+ : undefined;
81
+ const existingRemotePr = existingPr?.number !== undefined
82
+ ? existingPr
83
+ : await this.gitHubGateway.findOpenPullRequestByHeadBranch(repo.owner, repo.name, worktree.branchName);
84
+ if (!existingRemotePr) {
85
+ const baseBranch = worktree.baseBranch ?? repo.defaultBranch ?? "main";
86
+ const bootstrapCommitCreated = await this.ensureBootstrapCommit(worktree.path, issue, baseBranch);
87
+ await this.gitGateway.pushBranch(worktree.path, worktree.branchName);
88
+ const createdPr = await this.gitHubGateway.createPullRequest({
89
+ owner: repo.owner,
90
+ name: repo.name,
91
+ headBranch: worktree.branchName,
92
+ baseBranch,
93
+ title: buildPullRequestTitle(issue),
94
+ body: buildPullRequestBody(issue, worktree)
95
+ });
96
+ const timestamp = this.now();
97
+ return {
98
+ worktree: {
99
+ ...worktree,
100
+ prId: createdPr.id,
101
+ prNumber: createdPr.number,
102
+ prUrl: createdPr.url,
103
+ baseBranch,
104
+ status: worktree.productSpecWrittenAt ? "awaiting_plan_approval" : "product_specification",
105
+ bootstrapCommitCreated,
106
+ branchPushedAt: timestamp,
107
+ pullRequestCreatedAt: timestamp,
108
+ updatedAt: timestamp
109
+ },
110
+ pullRequestCreated: true
111
+ };
112
+ }
113
+ return {
114
+ worktree: {
115
+ ...worktree,
116
+ prId: existingRemotePr.id,
117
+ prNumber: existingRemotePr.number,
118
+ prUrl: existingRemotePr.url,
119
+ baseBranch: worktree.baseBranch ??
120
+ existingRemotePr.baseRefName ??
121
+ repo.defaultBranch ??
122
+ "main",
123
+ status: worktree.productSpecWrittenAt ? "awaiting_plan_approval" : "product_specification",
124
+ updatedAt: this.now()
125
+ },
126
+ pullRequestCreated: false
127
+ };
128
+ }
129
+ async ensurePacketPlanningWorktreesForCurrentWave(input) {
130
+ const { repo, repoPath, codeRoot, parentWorkstream, existingWorktrees } = input;
131
+ const packetPlan = parentWorkstream.packetPlan;
132
+ if (!packetPlan) {
133
+ return [];
134
+ }
135
+ const integrationBranch = parentWorkstream.integration?.branchName;
136
+ if (!integrationBranch) {
137
+ return [];
138
+ }
139
+ const waveIndex = getCurrentWaveIndex(packetPlan);
140
+ const packetsToDispatch = getUndispatchedCurrentWavePackets(packetPlan);
141
+ const results = [];
142
+ for (const packet of packetsToDispatch) {
143
+ const branchName = buildPacketWorktreeBranchName({ repo, parentWorkstream, packetId: packet.id });
144
+ const worktreeId = buildPacketWorktreeId({ repo, parentWorkstream, packetId: packet.id });
145
+ const existing = existingWorktrees.find((wt) => wt.packet?.parentWorkstreamId === parentWorkstream.id &&
146
+ wt.packet?.packetId === packet.id &&
147
+ wt.packet?.waveIndex === waveIndex) ??
148
+ existingWorktrees.find((wt) => wt.repoId === repo.id && wt.branchName === branchName);
149
+ if (existing) {
150
+ const preparedPaths = await preparePacketPlanDocuments({
151
+ worktreePath: existing.path,
152
+ parentWorkstream,
153
+ packet,
154
+ waveIndex,
155
+ branchName,
156
+ now: this.now,
157
+ existingPaths: {
158
+ planDocumentPath: existing.planDocumentPath,
159
+ logDocumentPath: existing.logDocumentPath
160
+ }
161
+ });
162
+ results.push({
163
+ worktree: {
164
+ ...existing,
165
+ packet: {
166
+ parentWorkstreamId: parentWorkstream.id,
167
+ packetId: packet.id,
168
+ waveIndex,
169
+ parentIntegrationBranch: integrationBranch
170
+ },
171
+ baseBranch: existing.baseBranch ?? integrationBranch,
172
+ planDocumentPath: preparedPaths.planDocumentPath,
173
+ logDocumentPath: preparedPaths.logDocumentPath,
174
+ updatedAt: this.now()
175
+ },
176
+ created: false
177
+ });
178
+ continue;
179
+ }
180
+ const worktreePath = this.gitGateway.validatePathWithinCodeRoot(codeRoot, buildPacketWorktreeRelativePath({ repo, parentWorkstream, packetId: packet.id }));
181
+ const startPoint = `origin/${integrationBranch}`;
182
+ const createdPath = await this.gitGateway.createWorktree(repoPath, codeRoot, worktreePath, branchName, startPoint);
183
+ const preparedPaths = await preparePacketPlanDocuments({
184
+ worktreePath: createdPath,
185
+ parentWorkstream,
186
+ packet,
187
+ waveIndex,
188
+ branchName,
189
+ now: this.now
190
+ });
191
+ const timestamp = this.now();
192
+ results.push({
193
+ created: true,
194
+ worktree: {
195
+ id: worktreeId,
196
+ repoId: repo.id,
197
+ repoSlug: `${repo.owner}/${repo.name}`,
198
+ repoName: repo.name,
199
+ packet: {
200
+ parentWorkstreamId: parentWorkstream.id,
201
+ packetId: packet.id,
202
+ waveIndex,
203
+ parentIntegrationBranch: integrationBranch
204
+ },
205
+ baseBranch: integrationBranch,
206
+ branchName,
207
+ path: createdPath,
208
+ planDocumentPath: preparedPaths.planDocumentPath,
209
+ logDocumentPath: preparedPaths.logDocumentPath,
210
+ status: "planning",
211
+ createdAt: timestamp,
212
+ updatedAt: timestamp
213
+ }
214
+ });
215
+ }
216
+ return results;
217
+ }
218
+ async ensurePullRequestForPacketWorktree(repo, parentWorkstream, worktree) {
219
+ if (!worktree.packet) {
220
+ throw new Error("Worktree is not a packet worktree");
221
+ }
222
+ const packet = parentWorkstream.packetPlan?.packets[worktree.packet.packetId];
223
+ if (!packet) {
224
+ throw new Error(`Packet ${worktree.packet.packetId} not found in workstream ${parentWorkstream.id}`);
225
+ }
226
+ const existingPr = worktree.prId && worktree.prNumber !== undefined && worktree.prUrl
227
+ ? {
228
+ id: worktree.prId,
229
+ number: worktree.prNumber,
230
+ url: worktree.prUrl
231
+ }
232
+ : undefined;
233
+ const existingRemotePr = existingPr?.number !== undefined
234
+ ? existingPr
235
+ : await this.gitHubGateway.findOpenPullRequestByHeadBranch(repo.owner, repo.name, worktree.branchName);
236
+ if (!existingRemotePr) {
237
+ const baseBranch = worktree.baseBranch ?? worktree.packet.parentIntegrationBranch;
238
+ const bootstrapCommitCreated = await this.ensurePacketBootstrapCommit(worktree.path, worktree.packet.packetId, baseBranch);
239
+ await this.gitGateway.pushBranch(worktree.path, worktree.branchName);
240
+ const createdPr = await this.gitHubGateway.createPullRequest({
241
+ owner: repo.owner,
242
+ name: repo.name,
243
+ headBranch: worktree.branchName,
244
+ baseBranch,
245
+ title: buildPacketPullRequestTitle({ parentWorkstream, packetId: packet.id }),
246
+ body: buildPacketPullRequestBody({ parentWorkstream, packet, worktree })
247
+ });
248
+ const timestamp = this.now();
249
+ return {
250
+ worktree: {
251
+ ...worktree,
252
+ prId: createdPr.id,
253
+ prNumber: createdPr.number,
254
+ prUrl: createdPr.url,
255
+ baseBranch,
256
+ status: "planning",
257
+ bootstrapCommitCreated,
258
+ branchPushedAt: timestamp,
259
+ pullRequestCreatedAt: timestamp,
260
+ updatedAt: timestamp
261
+ },
262
+ pullRequestCreated: true
263
+ };
264
+ }
265
+ return {
266
+ worktree: {
267
+ ...worktree,
268
+ prId: existingRemotePr.id,
269
+ prNumber: existingRemotePr.number,
270
+ prUrl: existingRemotePr.url,
271
+ baseBranch: worktree.baseBranch ??
272
+ existingRemotePr.baseRefName ??
273
+ worktree.packet.parentIntegrationBranch,
274
+ status: "planning",
275
+ updatedAt: this.now()
276
+ },
277
+ pullRequestCreated: false
278
+ };
279
+ }
280
+ linkPullRequests(worktrees, prs, repos) {
281
+ const prsByRepoAndBranch = new Map(prs.map((pr) => [`${pr.repoId}:${pr.headBranch}`, pr]));
282
+ const reposById = new Map(repos.map((repo) => [repo.id, repo]));
283
+ return worktrees.map((worktree) => {
284
+ const pr = prsByRepoAndBranch.get(`${worktree.repoId}:${worktree.branchName}`);
285
+ const repo = reposById.get(worktree.repoId);
286
+ if (!pr) {
287
+ return worktree;
288
+ }
289
+ const status = worktree.status === "implementation_done"
290
+ ? "implementation_done"
291
+ : !worktree.packet && worktree.status === "product_specification"
292
+ ? worktree.productSpecWrittenAt
293
+ ? "awaiting_plan_approval"
294
+ : "product_specification"
295
+ : !worktree.packet &&
296
+ worktree.status === "awaiting_plan_approval" &&
297
+ repo &&
298
+ pr.labels.includes(repo.labels.agentPlanApproved)
299
+ ? "implementation_ready"
300
+ : !worktree.packet &&
301
+ (worktree.status === "implementation_ready" || worktree.status === "implementing")
302
+ ? "awaiting_plan_approval"
303
+ : worktree.status;
304
+ return {
305
+ ...worktree,
306
+ prId: pr.id,
307
+ prNumber: pr.number,
308
+ prUrl: pr.url,
309
+ baseBranch: worktree.baseBranch ?? pr.baseBranch,
310
+ status,
311
+ updatedAt: this.now()
312
+ };
313
+ });
314
+ }
315
+ reconcilePacketWorktreeReadinessFromProjections(worktrees, projections, repos, tasks = [], workstreams, pullRequests) {
316
+ const reposById = new Map(repos.map((repo) => [repo.id, repo]));
317
+ const projectionsByWorktreeId = new Map(projections
318
+ .filter((p) => p.packetWorktree?.id)
319
+ .map((p) => [p.packetWorktree.id, p]));
320
+ const tasksByWorktreeId = groupTasksByWorktreeId(tasks);
321
+ return worktrees.map((worktree) => {
322
+ if (!worktree.packet) {
323
+ return worktree;
324
+ }
325
+ const parentWorkstream = workstreams?.find((w) => w.id === worktree.packet.parentWorkstreamId);
326
+ const isParentInactive = workstreams !== undefined &&
327
+ (!parentWorkstream ||
328
+ parentWorkstream.status === "closed" ||
329
+ parentWorkstream.status === "merged" ||
330
+ parentWorkstream.phase === "abandoned" ||
331
+ parentWorkstream.phase === "resolved");
332
+ const projection = projectionsByWorktreeId.get(worktree.id);
333
+ const hasClosedPr = (projection !== undefined &&
334
+ (projection.pullRequest.state === "closed" || projection.pullRequest.state === "merged")) ||
335
+ (pullRequests !== undefined &&
336
+ worktree.prNumber !== undefined &&
337
+ !pullRequests.some((pr) => pr.repoId === worktree.repoId && pr.number === worktree.prNumber));
338
+ if (isParentInactive || hasClosedPr) {
339
+ if (worktree.status === "closed") {
340
+ return worktree;
341
+ }
342
+ return {
343
+ ...worktree,
344
+ ...(projection ? {
345
+ prId: projection.pullRequest.id,
346
+ prNumber: projection.pullRequest.number,
347
+ prUrl: projection.pullRequest.url,
348
+ baseBranch: worktree.baseBranch ?? projection.pullRequest.baseBranch,
349
+ } : {}),
350
+ status: "closed",
351
+ updatedAt: this.now()
352
+ };
353
+ }
354
+ if (!projection) {
355
+ return worktree;
356
+ }
357
+ const repo = reposById.get(worktree.repoId);
358
+ const hasApproval = Boolean(repo && projection.pullRequest.labels.includes(repo.labels.agentPlanApproved));
359
+ const status = worktree.status === "implementation_done"
360
+ ? "implementation_done"
361
+ : hasApproval
362
+ ? deriveApprovedPacketWorktreeStatus(worktree, tasksByWorktreeId.get(worktree.id) ?? [])
363
+ : worktree.status === "implementation_ready" || worktree.status === "implementing"
364
+ ? "awaiting_plan_approval"
365
+ : worktree.status;
366
+ return {
367
+ ...worktree,
368
+ prId: projection.pullRequest.id,
369
+ prNumber: projection.pullRequest.number,
370
+ prUrl: projection.pullRequest.url,
371
+ baseBranch: worktree.baseBranch ?? projection.pullRequest.baseBranch,
372
+ status,
373
+ updatedAt: this.now()
374
+ };
375
+ });
376
+ }
377
+ reconcileTaskState(worktrees, tasks) {
378
+ const tasksByWorktreeId = groupTasksByWorktreeId(tasks);
379
+ const tasksByWorktreePath = groupTasksByWorktreePath(tasks);
380
+ return worktrees.map((worktree) => {
381
+ const relatedTasks = [
382
+ ...(tasksByWorktreeId.get(worktree.id) ?? []),
383
+ ...(tasksByWorktreePath.get(worktree.path) ?? [])
384
+ ];
385
+ const status = deriveWorktreeStatusFromTasks(worktree, relatedTasks);
386
+ const productSpecTask = relatedTasks.find((task) => task.kind === "write_product_spec" && task.status === "completed");
387
+ const hasProductSpecArtifact = hasMeaningfulProductSpecArtifact(worktree);
388
+ const productSpecWrittenAt = hasProductSpecArtifact
389
+ ? worktree.productSpecWrittenAt ?? productSpecTask?.updatedAt ?? productSpecTask?.createdAt
390
+ : undefined;
391
+ if (status === worktree.status && productSpecWrittenAt === worktree.productSpecWrittenAt) {
392
+ return worktree;
393
+ }
394
+ const { productSpecWrittenAt: _staleProductSpecWrittenAt, ...worktreeWithoutProductSpecWrittenAt } = worktree;
395
+ return {
396
+ ...worktreeWithoutProductSpecWrittenAt,
397
+ status,
398
+ ...(productSpecWrittenAt ? { productSpecWrittenAt } : {}),
399
+ updatedAt: this.now()
400
+ };
401
+ });
402
+ }
403
+ async finalizePushableHandoffs(worktrees, tasks, logger) {
404
+ const worktreesByPath = new Map(worktrees.map((worktree) => [worktree.path, worktree]));
405
+ const updatedWorktrees = new Map(worktrees.map((worktree) => [worktree.id, worktree]));
406
+ for (const task of tasks) {
407
+ if (task.status !== "completed" ||
408
+ task.executionPermission !== "workspace-write" ||
409
+ !task.handoffPolicy.expectsPushableHandoff ||
410
+ task.kind === "prepare_pr_for_merge") {
411
+ continue;
412
+ }
413
+ const worktree = task.context.localRepoPath
414
+ ? worktreesByPath.get(task.context.localRepoPath)
415
+ : undefined;
416
+ if (!worktree) {
417
+ continue;
418
+ }
419
+ const branchName = task.context.branchName ?? worktree.branchName;
420
+ const localRepoPath = worktree.path;
421
+ const wasClean = await this.gitGateway.isWorkingTreeClean(localRepoPath);
422
+ const committed = wasClean
423
+ ? false
424
+ : await this.gitGateway.commitAll(localRepoPath, buildHandoffCommitMessage(task, worktree));
425
+ const hasUnpushedCommits = committed
426
+ ? true
427
+ : await safeHasCommitsAhead(this.gitGateway, localRepoPath, `origin/${branchName}`);
428
+ if (!hasUnpushedCommits) {
429
+ continue;
430
+ }
431
+ await this.gitGateway.pushBranch(localRepoPath, branchName);
432
+ const pushedAt = this.now();
433
+ const updatedWorktree = {
434
+ ...worktree,
435
+ branchPushedAt: pushedAt,
436
+ updatedAt: pushedAt
437
+ };
438
+ updatedWorktrees.set(updatedWorktree.id, updatedWorktree);
439
+ worktreesByPath.set(updatedWorktree.path, updatedWorktree);
440
+ logger?.info(JSON.stringify({
441
+ component: "worktree-handoff",
442
+ taskId: task.id,
443
+ taskKind: task.kind,
444
+ repoSlug: task.repoSlug,
445
+ worktreePath: localRepoPath,
446
+ branchName,
447
+ committed,
448
+ pushed: true,
449
+ orchestratorOwnsPush: task.handoffPolicy.orchestratorOwnsPush
450
+ }));
451
+ }
452
+ return [...updatedWorktrees.values()].sort(compareWorktrees);
453
+ }
454
+ async ensureBootstrapCommit(worktreePath, issue, baseBranch) {
455
+ const hasCommitsAhead = await this.gitGateway.hasCommitsAhead(worktreePath, `origin/${baseBranch}`);
456
+ if (hasCommitsAhead) {
457
+ return false;
458
+ }
459
+ return this.gitGateway.commitAll(worktreePath, `chore(agent): initialize plan workflow for #${issue.number}`);
460
+ }
461
+ async ensurePacketBootstrapCommit(worktreePath, packetId, baseBranch) {
462
+ const hasCommitsAhead = await this.gitGateway.hasCommitsAhead(worktreePath, `origin/${baseBranch}`);
463
+ if (hasCommitsAhead) {
464
+ return false;
465
+ }
466
+ return this.gitGateway.commitAll(worktreePath, `chore(agent): initialize packet ${packetId} planning workflow`);
467
+ }
468
+ }
469
+ async function safeHasCommitsAhead(gitGateway, localPath, baseRef) {
470
+ try {
471
+ return await gitGateway.hasCommitsAhead(localPath, baseRef);
472
+ }
473
+ catch {
474
+ return true;
475
+ }
476
+ }
477
+ function buildHandoffCommitMessage(task, worktree) {
478
+ switch (task.kind) {
479
+ case "write_product_spec":
480
+ return `chore(agent): capture product spec for #${worktree.issueNumber ?? task.sourceId}`;
481
+ case "write_plan":
482
+ return `chore(agent): capture plan updates for #${worktree.issueNumber ?? task.sourceId}`;
483
+ case "implement_plan":
484
+ return `feat(agent): capture implementation updates for #${worktree.issueNumber ?? task.sourceId}`;
485
+ case "respond_pr_comment":
486
+ return `chore(agent): apply PR follow-up for ${task.repoSlug}`;
487
+ case "prepare_pr_for_merge":
488
+ return `chore(agent): capture merge preparation for #${worktree.prNumber ?? task.sourceId}`;
489
+ default:
490
+ return `chore(agent): capture task handoff for ${task.kind}`;
491
+ }
492
+ }
493
+ export function buildWorktreeProductSpecTaskRecord(worktree, issue, now) {
494
+ const executionPolicy = workflowRegistry.resolveTaskExecutionPolicy("write_product_spec");
495
+ const productSpecDocumentPath = worktree.productSpecDocumentPath ??
496
+ path.join(worktree.path, buildAgentDocumentRelativePaths(issue.number, worktree.branchName).productSpecDocumentPath);
497
+ const context = new DefaultContextBuilder().build({
498
+ taskKind: "write_product_spec",
499
+ repoIdentifier: worktree.repoSlug,
500
+ localRepoPath: worktree.path,
501
+ branchName: worktree.branchName,
502
+ productSpecDocumentPath,
503
+ planDocumentPath: worktree.planDocumentPath,
504
+ logDocumentPath: worktree.logDocumentPath,
505
+ sourceKind: "issue",
506
+ sourceUrl: issue.url,
507
+ labels: issue.labels,
508
+ issueTitle: issue.title,
509
+ issueBody: issue.body
510
+ });
511
+ return {
512
+ id: `task:write_product_spec:worktree:${worktree.id}`,
513
+ kind: "write_product_spec",
514
+ status: "pending",
515
+ executionPermission: executionPolicy.executionPermission,
516
+ handoffPolicy: executionPolicy.handoffPolicy,
517
+ repoId: worktree.repoId,
518
+ repoName: worktree.repoName,
519
+ repoSlug: worktree.repoSlug,
520
+ sourceKind: "issue",
521
+ sourceId: issue.id,
522
+ sourceUrl: issue.url,
523
+ relevant: true,
524
+ reason: "Issue has Product approval; Product Manager must write the Product Spec before engineering planning.",
525
+ summary: `Write Product Spec for issue #${issue.number}: ${issue.title}`,
526
+ context,
527
+ createdAt: now,
528
+ updatedAt: now
529
+ };
530
+ }
531
+ export function buildWorktreeId(owner, repoName, issueNumber) {
532
+ return `worktree:${owner}/${repoName}:issue:${issueNumber}`;
533
+ }
534
+ export function buildPacketWorktreeId(input) {
535
+ const { owner, name } = input.repo;
536
+ const { type, number } = input.parentWorkstream.origin;
537
+ return `worktree:${owner}/${name}:${type}:${number}:packet:${getPacketSlug(input.packetId)}`;
538
+ }
539
+ export function buildWorktreeBranchName(repo, issueNumber) {
540
+ return `agent/${slugify(`${repo.owner}-${repo.name}`)}/issue-${issueNumber}`;
541
+ }
542
+ export function buildPacketWorktreeBranchName(input) {
543
+ const { owner, name } = input.repo;
544
+ const { type, number } = input.parentWorkstream.origin;
545
+ const slug = slugify(`${owner}-${name}`);
546
+ const workstreamRef = `workstream-${type}-${number}`;
547
+ const packetRef = `packet-${getPacketSlug(input.packetId)}`;
548
+ return `agent/${slug}/${workstreamRef}/${packetRef}`;
549
+ }
550
+ export function buildWorktreeRelativePath(repo, issueNumber) {
551
+ return path.join(WORKTREE_ROOT_DIR, slugify(repo.owner), slugify(repo.name), `issue-${issueNumber}`);
552
+ }
553
+ export function buildPacketWorktreeRelativePath(input) {
554
+ const { owner, name } = input.repo;
555
+ const { type, number } = input.parentWorkstream.origin;
556
+ return path.join(WORKTREE_ROOT_DIR, slugify(owner), slugify(name), `workstream-${type}-${number}`, `packet-${getPacketSlug(input.packetId)}`);
557
+ }
558
+ async function preparePlanDocuments(worktreePath, issue, decision, branchName) {
559
+ const relativePaths = buildAgentDocumentRelativePaths(issue.number, branchName);
560
+ const planDocumentPath = path.join(worktreePath, relativePaths.planDocumentPath);
561
+ const productSpecDocumentPath = path.join(worktreePath, relativePaths.productSpecDocumentPath);
562
+ const logDocumentPath = path.join(worktreePath, relativePaths.logDocumentPath);
563
+ const docsDir = path.dirname(planDocumentPath);
564
+ await mkdir(docsDir, { recursive: true });
565
+ if (!(await pathExists(planDocumentPath))) {
566
+ await writeFile(planDocumentPath, [
567
+ `# Agent Plan`,
568
+ "",
569
+ `Issue: ${issue.url}`,
570
+ "",
571
+ `Status: Draft`,
572
+ "",
573
+ "## Objective",
574
+ decision.summary?.trim() || issue.title,
575
+ "",
576
+ "## Notes",
577
+ "- Planning document created by the orchestrator workflow.",
578
+ ""
579
+ ].join("\n"), "utf8");
580
+ }
581
+ if (!(await pathExists(logDocumentPath))) {
582
+ await writeFile(logDocumentPath, [
583
+ "# Agent Log",
584
+ "",
585
+ `- ${new Date().toISOString()}: Worktree prepared for plan-writing.`,
586
+ ""
587
+ ].join("\n"), "utf8");
588
+ }
589
+ return {
590
+ planDocumentPath,
591
+ productSpecDocumentPath,
592
+ logDocumentPath
593
+ };
594
+ }
595
+ async function preparePacketPlanDocuments(input) {
596
+ const { worktreePath, parentWorkstream, packet, branchName, now, existingPaths } = input;
597
+ const relativePaths = existingPaths
598
+ ? undefined
599
+ : buildPacketAgentDocumentRelativePaths(packet.id, branchName);
600
+ const planDocumentPath = existingPaths?.planDocumentPath ?? path.join(worktreePath, relativePaths.planDocumentPath);
601
+ const logDocumentPath = existingPaths?.logDocumentPath ?? path.join(worktreePath, relativePaths.logDocumentPath);
602
+ const docsDir = path.dirname(planDocumentPath);
603
+ await mkdir(docsDir, { recursive: true });
604
+ if (!(await pathExists(planDocumentPath))) {
605
+ const integrationBranch = parentWorkstream.integration?.branchName ?? "unknown";
606
+ await writeFile(planDocumentPath, [
607
+ `# Agent Plan`,
608
+ "",
609
+ `Parent workstream: ${parentWorkstream.id}`,
610
+ `Packet: ${packet.id}`,
611
+ "",
612
+ `Status: Draft`,
613
+ "",
614
+ "## Objective",
615
+ packet.title,
616
+ "",
617
+ packet.summary,
618
+ "",
619
+ "## Acceptance Criteria",
620
+ ...packet.acceptanceCriteria.map((ac) => `- ${ac}`),
621
+ "",
622
+ "## Likely Files",
623
+ ...packet.filesLikelyTouched.map((f) => `- ${f}`),
624
+ "",
625
+ "## Dependencies",
626
+ ...packet.dependsOn.map((d) => `- ${d}`),
627
+ "",
628
+ "## Notes",
629
+ `- This is a packet planning PR targeting \`${integrationBranch}\`.`,
630
+ "- Implementation must not begin until the `agent-plan-approved` label is present.",
631
+ ""
632
+ ].join("\n"), "utf8");
633
+ }
634
+ if (!(await pathExists(logDocumentPath))) {
635
+ await writeFile(logDocumentPath, ["# Agent Log", "", `- ${now()}: Packet planning worktree prepared.`, ""].join("\n"), "utf8");
636
+ }
637
+ return {
638
+ planDocumentPath,
639
+ logDocumentPath
640
+ };
641
+ }
642
+ async function pathExists(targetPath) {
643
+ try {
644
+ await stat(targetPath);
645
+ return true;
646
+ }
647
+ catch {
648
+ return false;
649
+ }
650
+ }
651
+ function compareWorktrees(left, right) {
652
+ const repoComparison = left.repoSlug.localeCompare(right.repoSlug);
653
+ if (repoComparison !== 0) {
654
+ return repoComparison;
655
+ }
656
+ return left.id.localeCompare(right.id);
657
+ }
658
+ function buildPullRequestTitle(issue) {
659
+ return `agent: plan for issue #${issue.number} - ${issue.title}`;
660
+ }
661
+ function buildPullRequestBody(issue, worktree) {
662
+ const planPath = toRepoRelativePath(worktree.path, worktree.planDocumentPath);
663
+ const productSpecPath = worktree.productSpecDocumentPath
664
+ ? toRepoRelativePath(worktree.path, worktree.productSpecDocumentPath)
665
+ : undefined;
666
+ const logPath = toRepoRelativePath(worktree.path, worktree.logDocumentPath);
667
+ return [
668
+ `Planning workflow for ${issue.url}.`,
669
+ "",
670
+ "This PR was created by the local agent orchestrator to support the plan-first workflow.",
671
+ "",
672
+ ...(productSpecPath ? [`- Product Spec: \`${productSpecPath}\``] : []),
673
+ `- Plan document: \`${planPath}\``,
674
+ `- Agent log: \`${logPath}\``,
675
+ "- Implementation must not begin until the `agent-plan-approved` label is present."
676
+ ].join("\n");
677
+ }
678
+ function buildPacketPullRequestTitle(input) {
679
+ const { parentWorkstream, packetId } = input;
680
+ const source = parentWorkstream.origin.type === "issue"
681
+ ? `issue #${parentWorkstream.origin.number}`
682
+ : `PR #${parentWorkstream.origin.number}`;
683
+ return `agent: plan packet ${packetId} for ${source}`;
684
+ }
685
+ function buildPacketPullRequestBody(input) {
686
+ const { parentWorkstream, packet, worktree } = input;
687
+ if (!worktree.packet)
688
+ throw new Error("Missing packet metadata");
689
+ const planPath = toRepoRelativePath(worktree.path, worktree.planDocumentPath);
690
+ const logPath = toRepoRelativePath(worktree.path, worktree.logDocumentPath);
691
+ const parentPr = parentWorkstream.activePrNumber ? `#${parentWorkstream.activePrNumber}` : "unknown";
692
+ return [
693
+ `Planning workflow for packet \`${packet.id}\` in parent workstream \`${parentWorkstream.id}\`.`,
694
+ "",
695
+ `Parent integration branch: \`${worktree.packet.parentIntegrationBranch}\``,
696
+ `Parent PR: ${parentPr}`,
697
+ `Wave index: ${worktree.packet.waveIndex}`,
698
+ `Packet title: ${packet.title}`,
699
+ "",
700
+ "Packet summary:",
701
+ packet.summary,
702
+ "",
703
+ "Acceptance criteria:",
704
+ ...packet.acceptanceCriteria.map((ac) => `- ${ac}`),
705
+ "",
706
+ "Likely files:",
707
+ ...packet.filesLikelyTouched.map((f) => `- ${f}`),
708
+ "",
709
+ "Dependencies:",
710
+ ...packet.dependsOn.map((d) => `- ${d}`),
711
+ "",
712
+ "This PR was created by the orchestrator as a packet planning PR.",
713
+ "Implementation must not begin until the `agent-plan-approved` label is present."
714
+ ].join("\n");
715
+ }
716
+ function buildAgentDocumentRelativePaths(issueNumber, branchName) {
717
+ const scopedDocsDir = path.join(AGENT_DOCS_ROOT_DIR, slugify(branchName));
718
+ return {
719
+ productSpecDocumentPath: path.join(scopedDocsDir, `issue-${issueNumber}-product-spec.md`),
720
+ planDocumentPath: path.join(scopedDocsDir, `issue-${issueNumber}-agent-plan.md`),
721
+ logDocumentPath: path.join(scopedDocsDir, `issue-${issueNumber}-agent-log.md`)
722
+ };
723
+ }
724
+ function buildPacketAgentDocumentRelativePaths(packetId, branchName) {
725
+ const branchHash = createHash("sha256").update(branchName).digest("hex").slice(0, 8);
726
+ const scopedDocsDir = path.join(AGENT_DOCS_ROOT_DIR, `${slugify(branchName)}-${branchHash}`);
727
+ return {
728
+ planDocumentPath: path.join(scopedDocsDir, `packet-${getPacketSlug(packetId)}-agent-plan.md`),
729
+ logDocumentPath: path.join(scopedDocsDir, `packet-${getPacketSlug(packetId)}-agent-log.md`)
730
+ };
731
+ }
732
+ function toRepoRelativePath(worktreePath, filePath) {
733
+ return path.relative(worktreePath, filePath).split(path.sep).join("/");
734
+ }
735
+ function extractWorktreeId(taskId) {
736
+ const prefixes = ["task:write_plan:worktree:", "task:implement_plan:worktree:"];
737
+ for (const prefix of prefixes) {
738
+ if (taskId.startsWith(prefix)) {
739
+ return taskId.slice(prefix.length);
740
+ }
741
+ }
742
+ return undefined;
743
+ }
744
+ function groupTasksByWorktreeId(tasks) {
745
+ const tasksByWorktreeId = new Map();
746
+ for (const task of tasks) {
747
+ const worktreeId = extractWorktreeId(task.id);
748
+ if (!worktreeId) {
749
+ continue;
750
+ }
751
+ const existing = tasksByWorktreeId.get(worktreeId) ?? [];
752
+ existing.push(task);
753
+ tasksByWorktreeId.set(worktreeId, existing);
754
+ }
755
+ return tasksByWorktreeId;
756
+ }
757
+ function groupTasksByWorktreePath(tasks) {
758
+ const tasksByWorktreePath = new Map();
759
+ for (const task of tasks) {
760
+ const worktreePath = task.context.localRepoPath;
761
+ if (!worktreePath) {
762
+ continue;
763
+ }
764
+ const existing = tasksByWorktreePath.get(worktreePath) ?? [];
765
+ existing.push(task);
766
+ tasksByWorktreePath.set(worktreePath, existing);
767
+ }
768
+ return tasksByWorktreePath;
769
+ }
770
+ function deriveApprovedPacketWorktreeStatus(worktree, tasks) {
771
+ if (worktree.status === "closed") {
772
+ return "closed";
773
+ }
774
+ if (worktree.status === "implementation_done") {
775
+ return "implementation_done";
776
+ }
777
+ const implementTask = tasks.find((task) => task.kind === "implement_plan");
778
+ if (implementTask) {
779
+ return deriveImplementationStatusFromTask(implementTask);
780
+ }
781
+ return "implementation_ready";
782
+ }
783
+ function deriveWorktreeStatusFromTasks(worktree, tasks) {
784
+ if (worktree.status === "closed") {
785
+ return "closed";
786
+ }
787
+ if (worktree.status === "implementation_done") {
788
+ return "implementation_done";
789
+ }
790
+ const implementTask = tasks.find((task) => task.kind === "implement_plan");
791
+ if (implementTask) {
792
+ return deriveImplementationStatusFromTask(implementTask);
793
+ }
794
+ const hasProductSpecArtifact = hasMeaningfulProductSpecArtifact(worktree);
795
+ const productSpecTask = tasks.find((task) => task.kind === "write_product_spec");
796
+ if (worktree.productSpecWrittenAt && hasProductSpecArtifact) {
797
+ const writePlanTask = tasks.find((task) => task.kind === "write_plan");
798
+ if (writePlanTask) {
799
+ return writePlanTask.status === "completed" ? "awaiting_plan_approval" : "planning";
800
+ }
801
+ return worktree.status === "product_specification" ? "planning" : worktree.status;
802
+ }
803
+ if (productSpecTask && productSpecTask.status !== "completed") {
804
+ return "product_specification";
805
+ }
806
+ const writePlanTask = tasks.find((task) => task.kind === "write_plan");
807
+ if (writePlanTask) {
808
+ return writePlanTask.status === "completed" ? "awaiting_plan_approval" : "planning";
809
+ }
810
+ if (productSpecTask?.status === "completed" && hasProductSpecArtifact) {
811
+ return "planning";
812
+ }
813
+ return worktree.status;
814
+ }
815
+ function hasMeaningfulProductSpecArtifact(worktree) {
816
+ if (!worktree.productSpecDocumentPath || !existsSync(worktree.productSpecDocumentPath)) {
817
+ return false;
818
+ }
819
+ try {
820
+ return readFileSync(worktree.productSpecDocumentPath, "utf8").trim().length > 0;
821
+ }
822
+ catch {
823
+ return false;
824
+ }
825
+ }
826
+ function deriveImplementationStatusFromTask(task) {
827
+ switch (task.status) {
828
+ case "pending":
829
+ return "implementation_ready";
830
+ case "queued":
831
+ return "implementation_ready";
832
+ case "dispatched":
833
+ return "implementing";
834
+ case "completed":
835
+ return "implementation_done";
836
+ case "failed":
837
+ return "implementing";
838
+ }
839
+ }