agentplane 0.1.6 → 0.1.7
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/assets/AGENTS.md +1 -1
- package/assets/agents/ORCHESTRATOR.json +1 -1
- package/assets/agents/UPGRADER.json +1 -1
- package/dist/cli/run-cli.d.ts.map +1 -1
- package/dist/cli/run-cli.js +22 -7
- package/dist/commands/branch/index.d.ts +60 -0
- package/dist/commands/branch/index.d.ts.map +1 -0
- package/dist/commands/branch/index.js +511 -0
- package/dist/commands/guard/index.d.ts +67 -0
- package/dist/commands/guard/index.d.ts.map +1 -0
- package/dist/commands/guard/index.js +367 -0
- package/dist/commands/hooks/index.d.ts +18 -0
- package/dist/commands/hooks/index.d.ts.map +1 -0
- package/dist/commands/hooks/index.js +290 -0
- package/dist/commands/pr/index.d.ts +46 -0
- package/dist/commands/pr/index.d.ts.map +1 -0
- package/dist/commands/pr/index.js +854 -0
- package/dist/commands/shared/git-diff.d.ts +9 -0
- package/dist/commands/shared/git-diff.d.ts.map +1 -0
- package/dist/commands/shared/git-diff.js +41 -0
- package/dist/commands/shared/git-ops.d.ts +24 -0
- package/dist/commands/shared/git-ops.d.ts.map +1 -0
- package/dist/commands/shared/git-ops.js +181 -0
- package/dist/commands/shared/git-worktree.d.ts +8 -0
- package/dist/commands/shared/git-worktree.d.ts.map +1 -0
- package/dist/commands/shared/git-worktree.js +48 -0
- package/dist/commands/shared/git.d.ts +4 -0
- package/dist/commands/shared/git.d.ts.map +1 -0
- package/dist/commands/shared/git.js +14 -0
- package/dist/commands/shared/path.d.ts +3 -0
- package/dist/commands/shared/path.d.ts.map +1 -0
- package/dist/commands/shared/path.js +14 -0
- package/dist/commands/shared/pr-meta.d.ts +21 -0
- package/dist/commands/shared/pr-meta.d.ts.map +1 -0
- package/dist/commands/shared/pr-meta.js +72 -0
- package/dist/commands/shared/task-backend.d.ts +15 -0
- package/dist/commands/shared/task-backend.d.ts.map +1 -0
- package/dist/commands/shared/task-backend.js +55 -0
- package/dist/commands/task/add.d.ts +8 -0
- package/dist/commands/task/add.d.ts.map +1 -0
- package/dist/commands/task/add.js +164 -0
- package/dist/commands/task/block.d.ts +19 -0
- package/dist/commands/task/block.d.ts.map +1 -0
- package/dist/commands/task/block.js +86 -0
- package/dist/commands/task/comment.d.ts +8 -0
- package/dist/commands/task/comment.d.ts.map +1 -0
- package/dist/commands/task/comment.js +29 -0
- package/dist/commands/task/doc.d.ts +17 -0
- package/dist/commands/task/doc.d.ts.map +1 -0
- package/dist/commands/task/doc.js +220 -0
- package/dist/commands/task/export.d.ts +5 -0
- package/dist/commands/task/export.d.ts.map +1 -0
- package/dist/commands/task/export.js +27 -0
- package/dist/commands/task/finish.d.ts +27 -0
- package/dist/commands/task/finish.d.ts.map +1 -0
- package/dist/commands/task/finish.js +131 -0
- package/dist/commands/task/index.d.ts +23 -0
- package/dist/commands/task/index.d.ts.map +1 -0
- package/dist/commands/task/index.js +22 -0
- package/dist/commands/task/lint.d.ts +5 -0
- package/dist/commands/task/lint.d.ts.map +1 -0
- package/dist/commands/task/lint.js +22 -0
- package/dist/commands/task/list.d.ts +11 -0
- package/dist/commands/task/list.d.ts.map +1 -0
- package/dist/commands/task/list.js +54 -0
- package/dist/commands/task/migrate.d.ts +6 -0
- package/dist/commands/task/migrate.d.ts.map +1 -0
- package/dist/commands/task/migrate.js +70 -0
- package/dist/commands/task/new.d.ts +8 -0
- package/dist/commands/task/new.d.ts.map +1 -0
- package/dist/commands/task/new.js +117 -0
- package/dist/commands/task/next.d.ts +6 -0
- package/dist/commands/task/next.d.ts.map +1 -0
- package/dist/commands/task/next.js +45 -0
- package/dist/commands/task/normalize.d.ts +6 -0
- package/dist/commands/task/normalize.d.ts.map +1 -0
- package/dist/commands/task/normalize.js +46 -0
- package/dist/commands/task/ready.d.ts +6 -0
- package/dist/commands/task/ready.d.ts.map +1 -0
- package/dist/commands/task/ready.js +57 -0
- package/dist/commands/task/scaffold.d.ts +8 -0
- package/dist/commands/task/scaffold.d.ts.map +1 -0
- package/dist/commands/task/scaffold.js +131 -0
- package/dist/commands/task/scrub.d.ts +8 -0
- package/dist/commands/task/scrub.d.ts.map +1 -0
- package/dist/commands/task/scrub.js +121 -0
- package/dist/commands/task/search.d.ts +7 -0
- package/dist/commands/task/search.d.ts.map +1 -0
- package/dist/commands/task/search.js +79 -0
- package/dist/commands/task/set-status.d.ts +19 -0
- package/dist/commands/task/set-status.d.ts.map +1 -0
- package/dist/commands/task/set-status.js +123 -0
- package/dist/commands/task/shared.d.ts +46 -0
- package/dist/commands/task/shared.d.ts.map +1 -0
- package/dist/commands/task/shared.js +283 -0
- package/dist/commands/task/show.d.ts +6 -0
- package/dist/commands/task/show.d.ts.map +1 -0
- package/dist/commands/task/show.js +35 -0
- package/dist/commands/task/start.d.ts +19 -0
- package/dist/commands/task/start.d.ts.map +1 -0
- package/dist/commands/task/start.js +109 -0
- package/dist/commands/task/update.d.ts +8 -0
- package/dist/commands/task/update.d.ts.map +1 -0
- package/dist/commands/task/update.js +144 -0
- package/dist/commands/task/verify.d.ts +14 -0
- package/dist/commands/task/verify.d.ts.map +1 -0
- package/dist/commands/task/verify.js +362 -0
- package/dist/commands/workflow.d.ts +5 -364
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +6 -4617
- package/package.json +2 -2
|
@@ -0,0 +1,854 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { loadConfig, resolveBaseBranch, resolveProject, } from "@agentplaneorg/core";
|
|
4
|
+
import { mapBackendError, mapCoreError } from "../../cli/error-map.js";
|
|
5
|
+
import { fileExists } from "../../cli/fs-utils.js";
|
|
6
|
+
import { successMessage, unknownEntityMessage, usageMessage, workflowModeMessage, } from "../../cli/output.js";
|
|
7
|
+
import { CliError } from "../../shared/errors.js";
|
|
8
|
+
import { ensureGitClean } from "../guard/index.js";
|
|
9
|
+
import { execFileAsync, gitEnv } from "../shared/git.js";
|
|
10
|
+
import { gitDiffNames, gitDiffStat, gitShowFile, toGitPath } from "../shared/git-diff.js";
|
|
11
|
+
import { gitBranchExists, gitCurrentBranch, gitRevParse } from "../shared/git-ops.js";
|
|
12
|
+
import { findWorktreeForBranch } from "../shared/git-worktree.js";
|
|
13
|
+
import { appendVerifyLog, extractLastVerifiedSha, parsePrMeta, runShellCommand, } from "../shared/pr-meta.js";
|
|
14
|
+
import { isPathWithin } from "../shared/path.js";
|
|
15
|
+
import { loadBackendTask } from "../shared/task-backend.js";
|
|
16
|
+
import { cmdFinish } from "../task/index.js";
|
|
17
|
+
export const PR_OPEN_USAGE = "Usage: agentplane pr open <task-id> --author <id> [--branch <name>]";
|
|
18
|
+
export const PR_OPEN_USAGE_EXAMPLE = "agentplane pr open 202602030608-F1Q8AB --author CODER";
|
|
19
|
+
export const PR_UPDATE_USAGE = "Usage: agentplane pr update <task-id>";
|
|
20
|
+
export const PR_UPDATE_USAGE_EXAMPLE = "agentplane pr update 202602030608-F1Q8AB";
|
|
21
|
+
export const PR_CHECK_USAGE = "Usage: agentplane pr check <task-id>";
|
|
22
|
+
export const PR_CHECK_USAGE_EXAMPLE = "agentplane pr check 202602030608-F1Q8AB";
|
|
23
|
+
export const PR_NOTE_USAGE = "Usage: agentplane pr note <task-id> --author <id> --body <text>";
|
|
24
|
+
export const PR_NOTE_USAGE_EXAMPLE = 'agentplane pr note 202602030608-F1Q8AB --author REVIEWER --body "..."';
|
|
25
|
+
export const INTEGRATE_USAGE = "Usage: agentplane integrate <task-id> [--branch <name>] [--base <name>] [--merge-strategy squash|merge|rebase] [--run-verify] [--dry-run] [--quiet]";
|
|
26
|
+
export const INTEGRATE_USAGE_EXAMPLE = "agentplane integrate 202602030608-F1Q8AB --run-verify";
|
|
27
|
+
function nowIso() {
|
|
28
|
+
return new Date().toISOString();
|
|
29
|
+
}
|
|
30
|
+
function renderPrReviewTemplate(opts) {
|
|
31
|
+
return [
|
|
32
|
+
"# PR Review",
|
|
33
|
+
"",
|
|
34
|
+
`Opened by ${opts.author} on ${opts.createdAt}`,
|
|
35
|
+
`Branch: ${opts.branch}`,
|
|
36
|
+
"",
|
|
37
|
+
"## Summary",
|
|
38
|
+
"",
|
|
39
|
+
"- ",
|
|
40
|
+
"",
|
|
41
|
+
"## Checklist",
|
|
42
|
+
"",
|
|
43
|
+
"- [ ] Tests added/updated",
|
|
44
|
+
"- [ ] Lint/format passes",
|
|
45
|
+
"- [ ] Verify passed",
|
|
46
|
+
"- [ ] Docs updated (if needed)",
|
|
47
|
+
"",
|
|
48
|
+
"## Handoff Notes",
|
|
49
|
+
"",
|
|
50
|
+
"<!-- Add review notes here. -->",
|
|
51
|
+
"",
|
|
52
|
+
"<!-- BEGIN AUTO SUMMARY -->",
|
|
53
|
+
"<!-- END AUTO SUMMARY -->",
|
|
54
|
+
"",
|
|
55
|
+
].join("\n");
|
|
56
|
+
}
|
|
57
|
+
function updateAutoSummaryBlock(text, summary) {
|
|
58
|
+
const start = "<!-- BEGIN AUTO SUMMARY -->";
|
|
59
|
+
const end = "<!-- END AUTO SUMMARY -->";
|
|
60
|
+
const startIdx = text.indexOf(start);
|
|
61
|
+
const endIdx = text.indexOf(end);
|
|
62
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
|
|
63
|
+
return `${text.trimEnd()}\n\n${start}\n${summary}\n${end}\n`;
|
|
64
|
+
}
|
|
65
|
+
const before = text.slice(0, startIdx + start.length);
|
|
66
|
+
const after = text.slice(endIdx);
|
|
67
|
+
return `${before}\n${summary}\n${after}`;
|
|
68
|
+
}
|
|
69
|
+
function appendHandoffNote(review, note) {
|
|
70
|
+
const marker = "## Handoff Notes";
|
|
71
|
+
const idx = review.indexOf(marker);
|
|
72
|
+
if (idx === -1)
|
|
73
|
+
return `${review.trimEnd()}\n\n${marker}\n\n- ${note}\n`;
|
|
74
|
+
const head = review.slice(0, idx + marker.length);
|
|
75
|
+
const tail = review.slice(idx + marker.length);
|
|
76
|
+
const trimmedTail = tail.startsWith("\n") ? tail.slice(1) : tail;
|
|
77
|
+
return `${head}\n\n- ${note}\n${trimmedTail}`;
|
|
78
|
+
}
|
|
79
|
+
async function resolvePrPaths(opts) {
|
|
80
|
+
const resolved = await resolveProject({
|
|
81
|
+
cwd: opts.cwd,
|
|
82
|
+
rootOverride: opts.rootOverride ?? null,
|
|
83
|
+
});
|
|
84
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
85
|
+
const taskDir = path.join(resolved.gitRoot, loaded.config.paths.workflow_dir, opts.taskId);
|
|
86
|
+
const prDir = path.join(taskDir, "pr");
|
|
87
|
+
return {
|
|
88
|
+
resolved,
|
|
89
|
+
config: loaded.config,
|
|
90
|
+
prDir,
|
|
91
|
+
metaPath: path.join(prDir, "meta.json"),
|
|
92
|
+
diffstatPath: path.join(prDir, "diffstat.txt"),
|
|
93
|
+
verifyLogPath: path.join(prDir, "verify.log"),
|
|
94
|
+
reviewPath: path.join(prDir, "review.md"),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
async function readPrArtifact(opts) {
|
|
98
|
+
const filePath = path.join(opts.prDir, opts.fileName);
|
|
99
|
+
if (await fileExists(filePath)) {
|
|
100
|
+
return await readFile(filePath, "utf8");
|
|
101
|
+
}
|
|
102
|
+
const rel = toGitPath(path.relative(opts.resolved.gitRoot, filePath));
|
|
103
|
+
try {
|
|
104
|
+
return await gitShowFile(opts.resolved.gitRoot, opts.branch, rel);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function validateReviewContents(review, errors) {
|
|
111
|
+
const requiredSections = ["## Summary", "## Checklist", "## Handoff Notes"];
|
|
112
|
+
for (const section of requiredSections) {
|
|
113
|
+
if (!review.includes(section))
|
|
114
|
+
errors.push(`Missing section: ${section}`);
|
|
115
|
+
}
|
|
116
|
+
if (!review.includes("<!-- BEGIN AUTO SUMMARY -->")) {
|
|
117
|
+
errors.push("Missing auto summary start marker");
|
|
118
|
+
}
|
|
119
|
+
if (!review.includes("<!-- END AUTO SUMMARY -->")) {
|
|
120
|
+
errors.push("Missing auto summary end marker");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export async function cmdPrOpen(opts) {
|
|
124
|
+
try {
|
|
125
|
+
const author = opts.author.trim();
|
|
126
|
+
if (!author)
|
|
127
|
+
throw new CliError({
|
|
128
|
+
exitCode: 2,
|
|
129
|
+
code: "E_USAGE",
|
|
130
|
+
message: usageMessage(PR_OPEN_USAGE, PR_OPEN_USAGE_EXAMPLE),
|
|
131
|
+
});
|
|
132
|
+
const { task } = await loadBackendTask({
|
|
133
|
+
cwd: opts.cwd,
|
|
134
|
+
rootOverride: opts.rootOverride,
|
|
135
|
+
taskId: opts.taskId,
|
|
136
|
+
});
|
|
137
|
+
const { resolved, config, prDir, metaPath, diffstatPath, verifyLogPath, reviewPath } = await resolvePrPaths(opts);
|
|
138
|
+
if (config.workflow_mode !== "branch_pr") {
|
|
139
|
+
throw new CliError({
|
|
140
|
+
exitCode: 2,
|
|
141
|
+
code: "E_USAGE",
|
|
142
|
+
message: workflowModeMessage(config.workflow_mode, "branch_pr"),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const branch = (opts.branch ?? (await gitCurrentBranch(resolved.gitRoot))).trim();
|
|
146
|
+
if (!branch)
|
|
147
|
+
throw new CliError({
|
|
148
|
+
exitCode: 2,
|
|
149
|
+
code: "E_USAGE",
|
|
150
|
+
message: usageMessage(PR_OPEN_USAGE, PR_OPEN_USAGE_EXAMPLE),
|
|
151
|
+
});
|
|
152
|
+
await mkdir(prDir, { recursive: true });
|
|
153
|
+
const now = nowIso();
|
|
154
|
+
let meta = null;
|
|
155
|
+
if (await fileExists(metaPath)) {
|
|
156
|
+
const raw = await readFile(metaPath, "utf8");
|
|
157
|
+
meta = parsePrMeta(raw, task.id);
|
|
158
|
+
}
|
|
159
|
+
const createdAt = meta?.created_at ?? now;
|
|
160
|
+
const nextMeta = {
|
|
161
|
+
schema_version: 1,
|
|
162
|
+
task_id: task.id,
|
|
163
|
+
branch,
|
|
164
|
+
created_at: createdAt,
|
|
165
|
+
updated_at: now,
|
|
166
|
+
last_verified_sha: meta?.last_verified_sha ?? null,
|
|
167
|
+
last_verified_at: meta?.last_verified_at ?? null,
|
|
168
|
+
verify: meta?.verify ?? { status: "skipped" },
|
|
169
|
+
};
|
|
170
|
+
await writeFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
171
|
+
if (!(await fileExists(diffstatPath)))
|
|
172
|
+
await writeFile(diffstatPath, "", "utf8");
|
|
173
|
+
if (!(await fileExists(verifyLogPath)))
|
|
174
|
+
await writeFile(verifyLogPath, "", "utf8");
|
|
175
|
+
if (!(await fileExists(reviewPath))) {
|
|
176
|
+
const review = renderPrReviewTemplate({ author, createdAt, branch });
|
|
177
|
+
await writeFile(reviewPath, review, "utf8");
|
|
178
|
+
}
|
|
179
|
+
process.stdout.write(`${successMessage("pr open", path.relative(resolved.gitRoot, prDir))}\n`);
|
|
180
|
+
return 0;
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
if (err instanceof CliError)
|
|
184
|
+
throw err;
|
|
185
|
+
throw mapBackendError(err, { command: "pr open", root: opts.rootOverride ?? null });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
export async function cmdPrUpdate(opts) {
|
|
189
|
+
try {
|
|
190
|
+
await loadBackendTask({
|
|
191
|
+
cwd: opts.cwd,
|
|
192
|
+
rootOverride: opts.rootOverride,
|
|
193
|
+
taskId: opts.taskId,
|
|
194
|
+
});
|
|
195
|
+
const { resolved, config, prDir, metaPath, diffstatPath, reviewPath } = await resolvePrPaths(opts);
|
|
196
|
+
if (config.workflow_mode !== "branch_pr") {
|
|
197
|
+
throw new CliError({
|
|
198
|
+
exitCode: 2,
|
|
199
|
+
code: "E_USAGE",
|
|
200
|
+
message: workflowModeMessage(config.workflow_mode, "branch_pr"),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
if (!(await fileExists(metaPath)) || !(await fileExists(reviewPath))) {
|
|
204
|
+
const missing = [];
|
|
205
|
+
if (!(await fileExists(metaPath)))
|
|
206
|
+
missing.push(path.relative(resolved.gitRoot, metaPath));
|
|
207
|
+
if (!(await fileExists(reviewPath)))
|
|
208
|
+
missing.push(path.relative(resolved.gitRoot, reviewPath));
|
|
209
|
+
throw new CliError({
|
|
210
|
+
exitCode: 3,
|
|
211
|
+
code: "E_VALIDATION",
|
|
212
|
+
message: `PR artifacts missing: ${missing.join(", ")} (run \`agentplane pr open\`)`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const baseBranch = await resolveBaseBranch({
|
|
216
|
+
cwd: opts.cwd,
|
|
217
|
+
rootOverride: opts.rootOverride ?? null,
|
|
218
|
+
cliBaseOpt: null,
|
|
219
|
+
mode: config.workflow_mode,
|
|
220
|
+
});
|
|
221
|
+
if (!baseBranch) {
|
|
222
|
+
throw new CliError({
|
|
223
|
+
exitCode: 2,
|
|
224
|
+
code: "E_USAGE",
|
|
225
|
+
message: "Base branch could not be resolved (use `agentplane branch base set`).",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const branch = await gitCurrentBranch(resolved.gitRoot);
|
|
229
|
+
const { stdout: diffStatOut } = await execFileAsync("git", ["diff", "--stat", `${baseBranch}...HEAD`], { cwd: resolved.gitRoot, env: gitEnv() });
|
|
230
|
+
const diffstat = diffStatOut.trimEnd();
|
|
231
|
+
await writeFile(diffstatPath, diffstat ? `${diffstat}\n` : "", "utf8");
|
|
232
|
+
const { stdout: headOut } = await execFileAsync("git", ["rev-parse", "HEAD"], {
|
|
233
|
+
cwd: resolved.gitRoot,
|
|
234
|
+
env: gitEnv(),
|
|
235
|
+
});
|
|
236
|
+
const headSha = headOut.trim();
|
|
237
|
+
const summaryLines = [
|
|
238
|
+
`- Updated: ${nowIso()}`,
|
|
239
|
+
`- Branch: ${branch}`,
|
|
240
|
+
`- Head: ${headSha.slice(0, 12)}`,
|
|
241
|
+
"- Diffstat:",
|
|
242
|
+
"```",
|
|
243
|
+
diffstat || "No changes detected.",
|
|
244
|
+
"```",
|
|
245
|
+
];
|
|
246
|
+
const reviewText = await readFile(reviewPath, "utf8");
|
|
247
|
+
const nextReview = updateAutoSummaryBlock(reviewText, summaryLines.join("\n"));
|
|
248
|
+
await writeFile(reviewPath, nextReview, "utf8");
|
|
249
|
+
const rawMeta = await readFile(metaPath, "utf8");
|
|
250
|
+
const meta = parsePrMeta(rawMeta, opts.taskId);
|
|
251
|
+
const nextMeta = {
|
|
252
|
+
...meta,
|
|
253
|
+
branch,
|
|
254
|
+
updated_at: nowIso(),
|
|
255
|
+
last_verified_sha: meta.last_verified_sha ?? null,
|
|
256
|
+
last_verified_at: meta.last_verified_at ?? null,
|
|
257
|
+
};
|
|
258
|
+
await writeFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
259
|
+
process.stdout.write(`${successMessage("pr update", path.relative(resolved.gitRoot, prDir))}\n`);
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
catch (err) {
|
|
263
|
+
if (err instanceof CliError)
|
|
264
|
+
throw err;
|
|
265
|
+
throw mapBackendError(err, { command: "pr update", root: opts.rootOverride ?? null });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
export async function cmdPrCheck(opts) {
|
|
269
|
+
try {
|
|
270
|
+
const { task } = await loadBackendTask({
|
|
271
|
+
cwd: opts.cwd,
|
|
272
|
+
rootOverride: opts.rootOverride,
|
|
273
|
+
taskId: opts.taskId,
|
|
274
|
+
});
|
|
275
|
+
const { resolved, config, prDir, metaPath, diffstatPath, verifyLogPath, reviewPath } = await resolvePrPaths(opts);
|
|
276
|
+
if (config.workflow_mode !== "branch_pr") {
|
|
277
|
+
throw new CliError({
|
|
278
|
+
exitCode: 2,
|
|
279
|
+
code: "E_USAGE",
|
|
280
|
+
message: workflowModeMessage(config.workflow_mode, "branch_pr"),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
const errors = [];
|
|
284
|
+
const relPrDir = path.relative(resolved.gitRoot, prDir);
|
|
285
|
+
const relMetaPath = path.relative(resolved.gitRoot, metaPath);
|
|
286
|
+
const relDiffstatPath = path.relative(resolved.gitRoot, diffstatPath);
|
|
287
|
+
const relVerifyLogPath = path.relative(resolved.gitRoot, verifyLogPath);
|
|
288
|
+
const relReviewPath = path.relative(resolved.gitRoot, reviewPath);
|
|
289
|
+
if (!(await fileExists(prDir)))
|
|
290
|
+
errors.push(`Missing PR directory: ${relPrDir}`);
|
|
291
|
+
if (!(await fileExists(metaPath)))
|
|
292
|
+
errors.push(`Missing ${relMetaPath}`);
|
|
293
|
+
if (!(await fileExists(diffstatPath)))
|
|
294
|
+
errors.push(`Missing ${relDiffstatPath}`);
|
|
295
|
+
if (!(await fileExists(verifyLogPath)))
|
|
296
|
+
errors.push(`Missing ${relVerifyLogPath}`);
|
|
297
|
+
if (!(await fileExists(reviewPath)))
|
|
298
|
+
errors.push(`Missing ${relReviewPath}`);
|
|
299
|
+
let meta = null;
|
|
300
|
+
if (await fileExists(metaPath)) {
|
|
301
|
+
try {
|
|
302
|
+
meta = parsePrMeta(await readFile(metaPath, "utf8"), task.id);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
306
|
+
errors.push(message);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (await fileExists(reviewPath)) {
|
|
310
|
+
const review = await readFile(reviewPath, "utf8");
|
|
311
|
+
validateReviewContents(review, errors);
|
|
312
|
+
}
|
|
313
|
+
if (task.verify && task.verify.length > 0) {
|
|
314
|
+
if (meta?.verify?.status !== "pass") {
|
|
315
|
+
errors.push("Verify requirements not satisfied (meta.verify.status != pass)");
|
|
316
|
+
}
|
|
317
|
+
if (!meta?.last_verified_sha || !meta.last_verified_at) {
|
|
318
|
+
errors.push("Verify metadata missing (last_verified_sha/last_verified_at)");
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
if (errors.length > 0) {
|
|
322
|
+
throw new CliError({ exitCode: 3, code: "E_VALIDATION", message: errors.join("\n") });
|
|
323
|
+
}
|
|
324
|
+
process.stdout.write(`${successMessage("pr check", path.relative(resolved.gitRoot, prDir))}\n`);
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
if (err instanceof CliError)
|
|
329
|
+
throw err;
|
|
330
|
+
throw mapBackendError(err, { command: "pr check", root: opts.rootOverride ?? null });
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export async function cmdPrNote(opts) {
|
|
334
|
+
try {
|
|
335
|
+
const author = opts.author.trim();
|
|
336
|
+
const body = opts.body.trim();
|
|
337
|
+
if (!author || !body) {
|
|
338
|
+
throw new CliError({
|
|
339
|
+
exitCode: 2,
|
|
340
|
+
code: "E_USAGE",
|
|
341
|
+
message: usageMessage(PR_NOTE_USAGE, PR_NOTE_USAGE_EXAMPLE),
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const { config, reviewPath, resolved } = await resolvePrPaths(opts);
|
|
345
|
+
if (config.workflow_mode !== "branch_pr") {
|
|
346
|
+
throw new CliError({
|
|
347
|
+
exitCode: 2,
|
|
348
|
+
code: "E_USAGE",
|
|
349
|
+
message: workflowModeMessage(config.workflow_mode, "branch_pr"),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (!(await fileExists(reviewPath))) {
|
|
353
|
+
const relReviewPath = path.relative(resolved.gitRoot, reviewPath);
|
|
354
|
+
throw new CliError({
|
|
355
|
+
exitCode: 3,
|
|
356
|
+
code: "E_VALIDATION",
|
|
357
|
+
message: `Missing ${relReviewPath} (run \`agentplane pr open\`)`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
const review = await readFile(reviewPath, "utf8");
|
|
361
|
+
const updated = appendHandoffNote(review, `${author}: ${body}`);
|
|
362
|
+
await writeFile(reviewPath, updated, "utf8");
|
|
363
|
+
process.stdout.write(`${successMessage("pr note", opts.taskId)}\n`);
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
catch (err) {
|
|
367
|
+
if (err instanceof CliError)
|
|
368
|
+
throw err;
|
|
369
|
+
throw mapCoreError(err, { command: "pr note", root: opts.rootOverride ?? null });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
export async function cmdIntegrate(opts) {
|
|
373
|
+
let tempWorktreePath = null;
|
|
374
|
+
let createdTempWorktree = false;
|
|
375
|
+
try {
|
|
376
|
+
const { task } = await loadBackendTask({
|
|
377
|
+
cwd: opts.cwd,
|
|
378
|
+
rootOverride: opts.rootOverride,
|
|
379
|
+
taskId: opts.taskId,
|
|
380
|
+
});
|
|
381
|
+
const resolved = await resolveProject({
|
|
382
|
+
cwd: opts.cwd,
|
|
383
|
+
rootOverride: opts.rootOverride ?? null,
|
|
384
|
+
});
|
|
385
|
+
const loaded = await loadConfig(resolved.agentplaneDir);
|
|
386
|
+
if (loaded.config.workflow_mode !== "branch_pr") {
|
|
387
|
+
throw new CliError({
|
|
388
|
+
exitCode: 2,
|
|
389
|
+
code: "E_USAGE",
|
|
390
|
+
message: workflowModeMessage(loaded.config.workflow_mode, "branch_pr"),
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
await ensureGitClean({ cwd: opts.cwd, rootOverride: opts.rootOverride });
|
|
394
|
+
if (opts.base?.trim().length === 0) {
|
|
395
|
+
throw new CliError({
|
|
396
|
+
exitCode: 2,
|
|
397
|
+
code: "E_USAGE",
|
|
398
|
+
message: usageMessage(INTEGRATE_USAGE, INTEGRATE_USAGE_EXAMPLE),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
const baseBranch = await resolveBaseBranch({
|
|
402
|
+
cwd: opts.cwd,
|
|
403
|
+
rootOverride: opts.rootOverride ?? null,
|
|
404
|
+
cliBaseOpt: opts.base ?? null,
|
|
405
|
+
mode: loaded.config.workflow_mode,
|
|
406
|
+
});
|
|
407
|
+
if (!baseBranch) {
|
|
408
|
+
throw new CliError({
|
|
409
|
+
exitCode: 2,
|
|
410
|
+
code: "E_USAGE",
|
|
411
|
+
message: "Base branch could not be resolved (use `agentplane branch base set` or --base).",
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
const currentBranch = await gitCurrentBranch(resolved.gitRoot);
|
|
415
|
+
if (currentBranch !== baseBranch) {
|
|
416
|
+
throw new CliError({
|
|
417
|
+
exitCode: 5,
|
|
418
|
+
code: "E_GIT",
|
|
419
|
+
message: `integrate must run on base branch ${baseBranch} (current: ${currentBranch})`,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const { prDir, metaPath, diffstatPath, verifyLogPath } = await resolvePrPaths({
|
|
423
|
+
cwd: opts.cwd,
|
|
424
|
+
rootOverride: opts.rootOverride,
|
|
425
|
+
taskId: opts.taskId,
|
|
426
|
+
});
|
|
427
|
+
let meta = null;
|
|
428
|
+
let branch = (opts.branch ?? "").trim();
|
|
429
|
+
if (await fileExists(metaPath)) {
|
|
430
|
+
meta = parsePrMeta(await readFile(metaPath, "utf8"), task.id);
|
|
431
|
+
if (!branch)
|
|
432
|
+
branch = (meta.branch ?? "").trim();
|
|
433
|
+
}
|
|
434
|
+
if (!branch) {
|
|
435
|
+
throw new CliError({
|
|
436
|
+
exitCode: 2,
|
|
437
|
+
code: "E_USAGE",
|
|
438
|
+
message: usageMessage(INTEGRATE_USAGE, INTEGRATE_USAGE_EXAMPLE),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
if (!(await gitBranchExists(resolved.gitRoot, branch))) {
|
|
442
|
+
throw new CliError({
|
|
443
|
+
exitCode: 2,
|
|
444
|
+
code: "E_USAGE",
|
|
445
|
+
message: unknownEntityMessage("branch", branch),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
const metaSource = meta ??
|
|
449
|
+
parsePrMeta(await gitShowFile(resolved.gitRoot, branch, toGitPath(path.relative(resolved.gitRoot, metaPath))), task.id);
|
|
450
|
+
const baseCandidate = opts.base ?? metaSource.base_branch ?? baseBranch;
|
|
451
|
+
const base = typeof baseCandidate === "string" && baseCandidate.trim().length > 0
|
|
452
|
+
? baseCandidate.trim()
|
|
453
|
+
: baseBranch;
|
|
454
|
+
const errors = [];
|
|
455
|
+
const relDiffstat = path.relative(resolved.gitRoot, path.join(prDir, "diffstat.txt"));
|
|
456
|
+
const relVerifyLog = path.relative(resolved.gitRoot, path.join(prDir, "verify.log"));
|
|
457
|
+
const relReview = path.relative(resolved.gitRoot, path.join(prDir, "review.md"));
|
|
458
|
+
const diffstatText = await readPrArtifact({
|
|
459
|
+
resolved,
|
|
460
|
+
prDir,
|
|
461
|
+
fileName: "diffstat.txt",
|
|
462
|
+
branch,
|
|
463
|
+
});
|
|
464
|
+
if (diffstatText === null)
|
|
465
|
+
errors.push(`Missing ${relDiffstat}`);
|
|
466
|
+
const verifyLogText = await readPrArtifact({
|
|
467
|
+
resolved,
|
|
468
|
+
prDir,
|
|
469
|
+
fileName: "verify.log",
|
|
470
|
+
branch,
|
|
471
|
+
});
|
|
472
|
+
if (verifyLogText === null)
|
|
473
|
+
errors.push(`Missing ${relVerifyLog}`);
|
|
474
|
+
const reviewText = await readPrArtifact({
|
|
475
|
+
resolved,
|
|
476
|
+
prDir,
|
|
477
|
+
fileName: "review.md",
|
|
478
|
+
branch,
|
|
479
|
+
});
|
|
480
|
+
if (reviewText === null)
|
|
481
|
+
errors.push(`Missing ${relReview}`);
|
|
482
|
+
if (reviewText)
|
|
483
|
+
validateReviewContents(reviewText, errors);
|
|
484
|
+
if (errors.length > 0) {
|
|
485
|
+
throw new CliError({ exitCode: 3, code: "E_VALIDATION", message: errors.join("\n") });
|
|
486
|
+
}
|
|
487
|
+
const changedPaths = await gitDiffNames(resolved.gitRoot, base, branch);
|
|
488
|
+
const tasksPath = loaded.config.paths.tasks_path;
|
|
489
|
+
if (changedPaths.includes(tasksPath)) {
|
|
490
|
+
throw new CliError({
|
|
491
|
+
exitCode: 5,
|
|
492
|
+
code: "E_GIT",
|
|
493
|
+
message: `Branch ${branch} modifies ${tasksPath} (single-writer violation)`,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
const rawVerify = task.verify;
|
|
497
|
+
const verifyCommands = Array.isArray(rawVerify)
|
|
498
|
+
? rawVerify
|
|
499
|
+
.filter((item) => typeof item === "string")
|
|
500
|
+
.map((item) => item.trim())
|
|
501
|
+
.filter(Boolean)
|
|
502
|
+
: [];
|
|
503
|
+
let branchHeadSha = await gitRevParse(resolved.gitRoot, [branch]);
|
|
504
|
+
let alreadyVerifiedSha = null;
|
|
505
|
+
if (verifyCommands.length > 0) {
|
|
506
|
+
const metaVerified = metaSource?.last_verified_sha ?? null;
|
|
507
|
+
if (metaVerified && metaVerified === branchHeadSha) {
|
|
508
|
+
alreadyVerifiedSha = branchHeadSha;
|
|
509
|
+
}
|
|
510
|
+
else if (verifyLogText) {
|
|
511
|
+
const logSha = extractLastVerifiedSha(verifyLogText);
|
|
512
|
+
if (logSha && logSha === branchHeadSha)
|
|
513
|
+
alreadyVerifiedSha = logSha;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
let shouldRunVerify = opts.runVerify || (verifyCommands.length > 0 && alreadyVerifiedSha === null);
|
|
517
|
+
if (opts.dryRun) {
|
|
518
|
+
if (!opts.quiet) {
|
|
519
|
+
process.stdout.write(`${successMessage("integrate dry-run", task.id, `base=${base} branch=${branch} verify=${shouldRunVerify ? "yes" : "no"}`)}\n`);
|
|
520
|
+
}
|
|
521
|
+
return 0;
|
|
522
|
+
}
|
|
523
|
+
let worktreePath = await findWorktreeForBranch(resolved.gitRoot, branch);
|
|
524
|
+
if (opts.mergeStrategy === "rebase" && !worktreePath) {
|
|
525
|
+
throw new CliError({
|
|
526
|
+
exitCode: 2,
|
|
527
|
+
code: "E_USAGE",
|
|
528
|
+
message: "rebase strategy requires an existing worktree for the task branch",
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (shouldRunVerify && !worktreePath) {
|
|
532
|
+
const worktreesDir = path.resolve(resolved.gitRoot, loaded.config.paths.worktrees_dir);
|
|
533
|
+
if (!isPathWithin(resolved.gitRoot, worktreesDir)) {
|
|
534
|
+
throw new CliError({
|
|
535
|
+
exitCode: 5,
|
|
536
|
+
code: "E_GIT",
|
|
537
|
+
message: `worktrees_dir must be inside the repo: ${worktreesDir}`,
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
tempWorktreePath = path.join(worktreesDir, `_integrate_tmp_${task.id}`);
|
|
541
|
+
const tempExists = await fileExists(tempWorktreePath);
|
|
542
|
+
if (tempExists) {
|
|
543
|
+
const registered = await findWorktreeForBranch(resolved.gitRoot, branch);
|
|
544
|
+
if (!registered) {
|
|
545
|
+
throw new CliError({
|
|
546
|
+
exitCode: 5,
|
|
547
|
+
code: "E_GIT",
|
|
548
|
+
message: `Temp worktree path exists but is not registered: ${tempWorktreePath}`,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
await mkdir(worktreesDir, { recursive: true });
|
|
554
|
+
await execFileAsync("git", ["worktree", "add", tempWorktreePath, branch], {
|
|
555
|
+
cwd: resolved.gitRoot,
|
|
556
|
+
env: gitEnv(),
|
|
557
|
+
});
|
|
558
|
+
createdTempWorktree = true;
|
|
559
|
+
}
|
|
560
|
+
worktreePath = tempWorktreePath;
|
|
561
|
+
}
|
|
562
|
+
const verifyEntries = [];
|
|
563
|
+
if (opts.mergeStrategy !== "rebase" && shouldRunVerify && verifyCommands.length > 0) {
|
|
564
|
+
if (!worktreePath) {
|
|
565
|
+
throw new CliError({
|
|
566
|
+
exitCode: 2,
|
|
567
|
+
code: "E_USAGE",
|
|
568
|
+
message: "Unable to locate or create a worktree for verify execution",
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
for (const command of verifyCommands) {
|
|
572
|
+
if (!opts.quiet) {
|
|
573
|
+
process.stdout.write(`$ ${command}\n`);
|
|
574
|
+
}
|
|
575
|
+
const timestamp = nowIso();
|
|
576
|
+
const result = await runShellCommand(command, worktreePath);
|
|
577
|
+
const shaPrefix = branchHeadSha ? `sha=${branchHeadSha} ` : "";
|
|
578
|
+
verifyEntries.push({
|
|
579
|
+
header: `[${timestamp}] ${shaPrefix}$ ${command}`.trimEnd(),
|
|
580
|
+
content: result.output,
|
|
581
|
+
});
|
|
582
|
+
if (result.code !== 0) {
|
|
583
|
+
throw new CliError({
|
|
584
|
+
exitCode: result.code || 1,
|
|
585
|
+
code: "E_IO",
|
|
586
|
+
message: `Verify command failed: ${command}`,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (branchHeadSha) {
|
|
591
|
+
verifyEntries.push({
|
|
592
|
+
header: `[${nowIso()}] ✅ verified_sha=${branchHeadSha}`,
|
|
593
|
+
content: "",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (!opts.quiet) {
|
|
597
|
+
process.stdout.write(`${successMessage("verify passed", task.id)}\n`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const baseShaBeforeMerge = await gitRevParse(resolved.gitRoot, [base]);
|
|
601
|
+
const headBeforeMerge = await gitRevParse(resolved.gitRoot, ["HEAD"]);
|
|
602
|
+
let mergeHash = "";
|
|
603
|
+
if (opts.mergeStrategy === "squash") {
|
|
604
|
+
try {
|
|
605
|
+
await execFileAsync("git", ["merge", "--squash", branch], {
|
|
606
|
+
cwd: resolved.gitRoot,
|
|
607
|
+
env: gitEnv(),
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
await execFileAsync("git", ["reset", "--hard", headBeforeMerge], {
|
|
612
|
+
cwd: resolved.gitRoot,
|
|
613
|
+
env: gitEnv(),
|
|
614
|
+
});
|
|
615
|
+
const message = err instanceof Error ? err.message : "git merge --squash failed";
|
|
616
|
+
throw new CliError({ exitCode: 2, code: "E_GIT", message });
|
|
617
|
+
}
|
|
618
|
+
const { stdout: staged } = await execFileAsync("git", ["diff", "--cached", "--name-only"], {
|
|
619
|
+
cwd: resolved.gitRoot,
|
|
620
|
+
env: gitEnv(),
|
|
621
|
+
});
|
|
622
|
+
if (!staged.trim()) {
|
|
623
|
+
await execFileAsync("git", ["reset", "--hard", headBeforeMerge], {
|
|
624
|
+
cwd: resolved.gitRoot,
|
|
625
|
+
env: gitEnv(),
|
|
626
|
+
});
|
|
627
|
+
throw new CliError({
|
|
628
|
+
exitCode: 2,
|
|
629
|
+
code: "E_USAGE",
|
|
630
|
+
message: `Nothing to integrate: ${branch} is already merged into ${base}`,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const { stdout: subjectOut } = await execFileAsync("git", ["log", "-1", "--pretty=format:%s", branch], { cwd: resolved.gitRoot, env: gitEnv() });
|
|
634
|
+
let subject = subjectOut.trim();
|
|
635
|
+
if (!subject.includes(task.id)) {
|
|
636
|
+
subject = `🧩 ${task.id} integrate ${branch}`;
|
|
637
|
+
}
|
|
638
|
+
const env = {
|
|
639
|
+
...process.env,
|
|
640
|
+
AGENTPLANE_TASK_ID: task.id,
|
|
641
|
+
AGENTPLANE_ALLOW_BASE: "1",
|
|
642
|
+
AGENTPLANE_ALLOW_TASKS: "0",
|
|
643
|
+
};
|
|
644
|
+
try {
|
|
645
|
+
await execFileAsync("git", ["commit", "-m", subject], {
|
|
646
|
+
cwd: resolved.gitRoot,
|
|
647
|
+
env,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (err) {
|
|
651
|
+
await execFileAsync("git", ["reset", "--hard", headBeforeMerge], {
|
|
652
|
+
cwd: resolved.gitRoot,
|
|
653
|
+
env: gitEnv(),
|
|
654
|
+
});
|
|
655
|
+
const message = err instanceof Error ? err.message : "git commit failed";
|
|
656
|
+
throw new CliError({ exitCode: 2, code: "E_GIT", message });
|
|
657
|
+
}
|
|
658
|
+
mergeHash = await gitRevParse(resolved.gitRoot, ["HEAD"]);
|
|
659
|
+
}
|
|
660
|
+
else if (opts.mergeStrategy === "merge") {
|
|
661
|
+
const env = {
|
|
662
|
+
...process.env,
|
|
663
|
+
AGENTPLANE_TASK_ID: task.id,
|
|
664
|
+
AGENTPLANE_ALLOW_BASE: "1",
|
|
665
|
+
AGENTPLANE_ALLOW_TASKS: "0",
|
|
666
|
+
};
|
|
667
|
+
try {
|
|
668
|
+
await execFileAsync("git", ["merge", "--no-ff", branch, "-m", `🔀 ${task.id} merge ${branch}`], {
|
|
669
|
+
cwd: resolved.gitRoot,
|
|
670
|
+
env,
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
await execFileAsync("git", ["merge", "--abort"], { cwd: resolved.gitRoot, env: gitEnv() });
|
|
675
|
+
const message = err instanceof Error ? err.message : "git merge failed";
|
|
676
|
+
throw new CliError({ exitCode: 2, code: "E_GIT", message });
|
|
677
|
+
}
|
|
678
|
+
mergeHash = await gitRevParse(resolved.gitRoot, ["HEAD"]);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
if (!worktreePath) {
|
|
682
|
+
throw new CliError({
|
|
683
|
+
exitCode: 2,
|
|
684
|
+
code: "E_USAGE",
|
|
685
|
+
message: "rebase strategy requires an existing worktree for the task branch",
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
try {
|
|
689
|
+
await execFileAsync("git", ["rebase", base], { cwd: worktreePath, env: gitEnv() });
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
await execFileAsync("git", ["rebase", "--abort"], { cwd: worktreePath, env: gitEnv() });
|
|
693
|
+
const message = err instanceof Error ? err.message : "git rebase failed";
|
|
694
|
+
throw new CliError({ exitCode: 2, code: "E_GIT", message });
|
|
695
|
+
}
|
|
696
|
+
branchHeadSha = await gitRevParse(resolved.gitRoot, [branch]);
|
|
697
|
+
if (!opts.runVerify && verifyCommands.length > 0) {
|
|
698
|
+
alreadyVerifiedSha = null;
|
|
699
|
+
const metaVerified = metaSource?.last_verified_sha ?? null;
|
|
700
|
+
if (metaVerified && metaVerified === branchHeadSha) {
|
|
701
|
+
alreadyVerifiedSha = branchHeadSha;
|
|
702
|
+
}
|
|
703
|
+
else if (verifyLogText) {
|
|
704
|
+
const logSha = extractLastVerifiedSha(verifyLogText);
|
|
705
|
+
if (logSha && logSha === branchHeadSha)
|
|
706
|
+
alreadyVerifiedSha = logSha;
|
|
707
|
+
}
|
|
708
|
+
shouldRunVerify = alreadyVerifiedSha === null;
|
|
709
|
+
}
|
|
710
|
+
if (shouldRunVerify && verifyCommands.length > 0) {
|
|
711
|
+
if (!worktreePath) {
|
|
712
|
+
throw new CliError({
|
|
713
|
+
exitCode: 2,
|
|
714
|
+
code: "E_USAGE",
|
|
715
|
+
message: "Unable to locate or create a worktree for verify execution",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
for (const command of verifyCommands) {
|
|
719
|
+
if (!opts.quiet) {
|
|
720
|
+
process.stdout.write(`$ ${command}\n`);
|
|
721
|
+
}
|
|
722
|
+
const timestamp = nowIso();
|
|
723
|
+
const result = await runShellCommand(command, worktreePath);
|
|
724
|
+
const shaPrefix = branchHeadSha ? `sha=${branchHeadSha} ` : "";
|
|
725
|
+
verifyEntries.push({
|
|
726
|
+
header: `[${timestamp}] ${shaPrefix}$ ${command}`.trimEnd(),
|
|
727
|
+
content: result.output,
|
|
728
|
+
});
|
|
729
|
+
if (result.code !== 0) {
|
|
730
|
+
throw new CliError({
|
|
731
|
+
exitCode: result.code || 1,
|
|
732
|
+
code: "E_IO",
|
|
733
|
+
message: `Verify command failed: ${command}`,
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
if (branchHeadSha) {
|
|
738
|
+
verifyEntries.push({
|
|
739
|
+
header: `[${nowIso()}] ✅ verified_sha=${branchHeadSha}`,
|
|
740
|
+
content: "",
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
if (!opts.quiet) {
|
|
744
|
+
process.stdout.write(`${successMessage("verify passed", task.id)}\n`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
try {
|
|
748
|
+
await execFileAsync("git", ["merge", "--ff-only", branch], {
|
|
749
|
+
cwd: resolved.gitRoot,
|
|
750
|
+
env: gitEnv(),
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
catch (err) {
|
|
754
|
+
await execFileAsync("git", ["reset", "--hard", headBeforeMerge], {
|
|
755
|
+
cwd: resolved.gitRoot,
|
|
756
|
+
env: gitEnv(),
|
|
757
|
+
}).catch(() => null);
|
|
758
|
+
const message = err instanceof Error ? err.message : "git merge --ff-only failed";
|
|
759
|
+
throw new CliError({ exitCode: 2, code: "E_GIT", message });
|
|
760
|
+
}
|
|
761
|
+
mergeHash = await gitRevParse(resolved.gitRoot, ["HEAD"]);
|
|
762
|
+
}
|
|
763
|
+
if (!(await fileExists(prDir))) {
|
|
764
|
+
throw new CliError({
|
|
765
|
+
exitCode: 3,
|
|
766
|
+
code: "E_VALIDATION",
|
|
767
|
+
message: `Missing PR artifact dir after merge: ${path.relative(resolved.gitRoot, prDir)}`,
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
if (verifyEntries.length > 0) {
|
|
771
|
+
for (const entry of verifyEntries) {
|
|
772
|
+
await appendVerifyLog(verifyLogPath, entry.header, entry.content);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
const rawMeta = await readFile(metaPath, "utf8");
|
|
776
|
+
const mergedMeta = parsePrMeta(rawMeta, task.id);
|
|
777
|
+
const now = nowIso();
|
|
778
|
+
const nextMeta = {
|
|
779
|
+
...mergedMeta,
|
|
780
|
+
branch,
|
|
781
|
+
base_branch: base,
|
|
782
|
+
merge_strategy: opts.mergeStrategy,
|
|
783
|
+
status: "MERGED",
|
|
784
|
+
merged_at: mergedMeta.merged_at ?? now,
|
|
785
|
+
merge_commit: mergeHash,
|
|
786
|
+
head_sha: branchHeadSha,
|
|
787
|
+
updated_at: now,
|
|
788
|
+
};
|
|
789
|
+
if (verifyCommands.length > 0 && (shouldRunVerify || alreadyVerifiedSha)) {
|
|
790
|
+
nextMeta.last_verified_sha = branchHeadSha;
|
|
791
|
+
nextMeta.last_verified_at = now;
|
|
792
|
+
nextMeta.verify = mergedMeta.verify
|
|
793
|
+
? { ...mergedMeta.verify, status: "pass" }
|
|
794
|
+
: { status: "pass", command: verifyCommands.join(" && ") };
|
|
795
|
+
}
|
|
796
|
+
await writeFile(metaPath, `${JSON.stringify(nextMeta, null, 2)}\n`, "utf8");
|
|
797
|
+
const diffstat = await gitDiffStat(resolved.gitRoot, baseShaBeforeMerge, branch);
|
|
798
|
+
await writeFile(diffstatPath, diffstat ? `${diffstat}\n` : "", "utf8");
|
|
799
|
+
const verifyDesc = verifyCommands.length === 0
|
|
800
|
+
? "skipped(no commands)"
|
|
801
|
+
: shouldRunVerify
|
|
802
|
+
? "ran"
|
|
803
|
+
: alreadyVerifiedSha
|
|
804
|
+
? `skipped(already verified_sha=${alreadyVerifiedSha})`
|
|
805
|
+
: "skipped";
|
|
806
|
+
const finishBody = `Verified: Integrated via ${opts.mergeStrategy}; verify=${verifyDesc}; pr=${path.relative(resolved.gitRoot, prDir)}.`;
|
|
807
|
+
await cmdFinish({
|
|
808
|
+
cwd: opts.cwd,
|
|
809
|
+
rootOverride: opts.rootOverride,
|
|
810
|
+
taskIds: [task.id],
|
|
811
|
+
author: "INTEGRATOR",
|
|
812
|
+
body: finishBody,
|
|
813
|
+
commit: undefined,
|
|
814
|
+
skipVerify: false,
|
|
815
|
+
force: false,
|
|
816
|
+
noRequireTaskIdInCommit: false,
|
|
817
|
+
commitFromComment: false,
|
|
818
|
+
commitEmoji: undefined,
|
|
819
|
+
commitAllow: [],
|
|
820
|
+
commitAutoAllow: false,
|
|
821
|
+
commitAllowTasks: false,
|
|
822
|
+
commitRequireClean: false,
|
|
823
|
+
statusCommit: false,
|
|
824
|
+
statusCommitEmoji: undefined,
|
|
825
|
+
statusCommitAllow: [],
|
|
826
|
+
statusCommitAutoAllow: false,
|
|
827
|
+
statusCommitRequireClean: false,
|
|
828
|
+
confirmStatusCommit: false,
|
|
829
|
+
quiet: opts.quiet,
|
|
830
|
+
});
|
|
831
|
+
if (!opts.quiet) {
|
|
832
|
+
process.stdout.write(`${successMessage("integrate", task.id, `merge=${mergeHash.slice(0, 12)}`)}\n`);
|
|
833
|
+
}
|
|
834
|
+
return 0;
|
|
835
|
+
}
|
|
836
|
+
catch (err) {
|
|
837
|
+
if (err instanceof CliError)
|
|
838
|
+
throw err;
|
|
839
|
+
throw mapBackendError(err, { command: "integrate", root: opts.rootOverride ?? null });
|
|
840
|
+
}
|
|
841
|
+
finally {
|
|
842
|
+
if (createdTempWorktree && tempWorktreePath) {
|
|
843
|
+
try {
|
|
844
|
+
await execFileAsync("git", ["worktree", "remove", "--force", tempWorktreePath], {
|
|
845
|
+
cwd: opts.cwd,
|
|
846
|
+
env: gitEnv(),
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
catch {
|
|
850
|
+
// ignore cleanup errors
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|