@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.
- package/LICENSE +21 -0
- package/README.md +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- 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
|
+
}
|