taskplane 0.4.3 → 0.5.1
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/README.md +10 -5
- package/extensions/taskplane/engine.ts +79 -15
- package/extensions/taskplane/execution.ts +48 -1
- package/extensions/taskplane/extension.ts +562 -2
- package/extensions/taskplane/merge.ts +182 -27
- package/extensions/taskplane/messages.ts +30 -1
- package/extensions/taskplane/resume.ts +128 -11
- package/extensions/taskplane/waves.ts +16 -2
- package/extensions/taskplane/worktree.ts +3 -3
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import { execSync } from "child_process";
|
|
3
|
+
import { execSync, execFileSync } from "child_process";
|
|
4
4
|
import { writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
DEFAULT_TASK_RUNNER_CONFIG,
|
|
10
10
|
FATAL_DISCOVERY_CODES,
|
|
11
11
|
ORCH_MESSAGES,
|
|
12
|
+
StateFileError,
|
|
12
13
|
WorkspaceConfigError,
|
|
13
14
|
computeWaveAssignments,
|
|
14
15
|
createOrchWidget,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
formatPreflightResults,
|
|
23
24
|
formatWavePlan,
|
|
24
25
|
freshOrchBatchState,
|
|
26
|
+
getCurrentBranch,
|
|
25
27
|
listOrchSessions,
|
|
26
28
|
loadBatchState,
|
|
27
29
|
loadOrchestratorConfig,
|
|
@@ -29,6 +31,7 @@ import {
|
|
|
29
31
|
parseOrchSessionNames,
|
|
30
32
|
resumeOrchBatch,
|
|
31
33
|
runDiscovery,
|
|
34
|
+
runGit,
|
|
32
35
|
runPreflight,
|
|
33
36
|
} from "./index.ts";
|
|
34
37
|
import { buildExecutionContext } from "./workspace.ts";
|
|
@@ -42,6 +45,425 @@ import type {
|
|
|
42
45
|
TaskRunnerConfig,
|
|
43
46
|
} from "./index.ts";
|
|
44
47
|
|
|
48
|
+
// ── Integrate Args Parsing ────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
export type IntegrateMode = "ff" | "merge" | "pr";
|
|
51
|
+
|
|
52
|
+
export interface IntegrateArgs {
|
|
53
|
+
mode: IntegrateMode;
|
|
54
|
+
force: boolean;
|
|
55
|
+
orchBranchArg?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parse `/orch-integrate` command arguments.
|
|
60
|
+
*
|
|
61
|
+
* Supported flags: --merge, --pr, --force
|
|
62
|
+
* Optional positional: orch branch name (e.g., orch/op-batchid)
|
|
63
|
+
*
|
|
64
|
+
* Returns parsed args or an error string if arguments are invalid.
|
|
65
|
+
*/
|
|
66
|
+
export function parseIntegrateArgs(raw: string | undefined): IntegrateArgs | { error: string } {
|
|
67
|
+
const input = raw?.trim() ?? "";
|
|
68
|
+
const tokens = input.split(/\s+/).filter(Boolean);
|
|
69
|
+
|
|
70
|
+
let mode: IntegrateMode = "ff";
|
|
71
|
+
let force = false;
|
|
72
|
+
const positionals: string[] = [];
|
|
73
|
+
let hasMerge = false;
|
|
74
|
+
let hasPr = false;
|
|
75
|
+
|
|
76
|
+
for (const token of tokens) {
|
|
77
|
+
if (token === "--merge") {
|
|
78
|
+
hasMerge = true;
|
|
79
|
+
} else if (token === "--pr") {
|
|
80
|
+
hasPr = true;
|
|
81
|
+
} else if (token === "--force") {
|
|
82
|
+
force = true;
|
|
83
|
+
} else if (token.startsWith("--")) {
|
|
84
|
+
return { error: `Unknown flag: ${token}` };
|
|
85
|
+
} else {
|
|
86
|
+
positionals.push(token);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Mutual exclusion: --merge and --pr cannot be used together
|
|
91
|
+
if (hasMerge && hasPr) {
|
|
92
|
+
return { error: "Cannot use --merge and --pr together. Choose one integration mode." };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (hasMerge) mode = "merge";
|
|
96
|
+
if (hasPr) mode = "pr";
|
|
97
|
+
|
|
98
|
+
if (positionals.length > 1) {
|
|
99
|
+
return { error: `Expected at most one branch argument, got ${positionals.length}: ${positionals.join(", ")}` };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
mode,
|
|
104
|
+
force,
|
|
105
|
+
orchBranchArg: positionals[0],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Integration Context Resolution ────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Successful result from resolveIntegrationContext.
|
|
113
|
+
*/
|
|
114
|
+
export interface IntegrationContext {
|
|
115
|
+
orchBranch: string;
|
|
116
|
+
baseBranch: string;
|
|
117
|
+
batchId: string;
|
|
118
|
+
currentBranch: string;
|
|
119
|
+
/** Informational messages generated during resolution (e.g., auto-detect notices) */
|
|
120
|
+
notices: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Error result from resolveIntegrationContext.
|
|
125
|
+
*/
|
|
126
|
+
export interface IntegrationContextError {
|
|
127
|
+
error: string;
|
|
128
|
+
/** "info" for non-error states (legacy mode), "error" for real failures */
|
|
129
|
+
severity: "info" | "error";
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Dependencies injected into resolveIntegrationContext for testability.
|
|
134
|
+
*/
|
|
135
|
+
export interface IntegrationDeps {
|
|
136
|
+
loadBatchState: () => PersistedBatchState | null;
|
|
137
|
+
getCurrentBranch: () => string | null;
|
|
138
|
+
listOrchBranches: () => string[];
|
|
139
|
+
orchBranchExists: (branch: string) => boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Pure function to resolve all context needed for /orch-integrate.
|
|
144
|
+
*
|
|
145
|
+
* Resolution order:
|
|
146
|
+
* 1. Try loading persisted batch state → extract orchBranch/baseBranch
|
|
147
|
+
* 2. If state unavailable, use positional CLI arg
|
|
148
|
+
* 3. If neither, scan for orch/* branches
|
|
149
|
+
*
|
|
150
|
+
* Also performs: phase gating, legacy mode detection, branch existence check,
|
|
151
|
+
* detached HEAD check, and branch safety validation.
|
|
152
|
+
*
|
|
153
|
+
* Returns either a fully-resolved IntegrationContext or an IntegrationContextError.
|
|
154
|
+
*/
|
|
155
|
+
export function resolveIntegrationContext(
|
|
156
|
+
parsed: IntegrateArgs,
|
|
157
|
+
deps: IntegrationDeps,
|
|
158
|
+
): IntegrationContext | IntegrationContextError {
|
|
159
|
+
let orchBranch = "";
|
|
160
|
+
let baseBranch = "";
|
|
161
|
+
let batchId = "";
|
|
162
|
+
const notices: string[] = [];
|
|
163
|
+
|
|
164
|
+
// Source 1: Try loading batch state
|
|
165
|
+
try {
|
|
166
|
+
const state = deps.loadBatchState();
|
|
167
|
+
if (state) {
|
|
168
|
+
orchBranch = state.orchBranch ?? "";
|
|
169
|
+
baseBranch = state.baseBranch ?? "";
|
|
170
|
+
batchId = state.batchId;
|
|
171
|
+
|
|
172
|
+
// Phase gate: batch must be completed before integration
|
|
173
|
+
if (state.phase !== "completed") {
|
|
174
|
+
return {
|
|
175
|
+
error:
|
|
176
|
+
`⏳ Batch ${batchId} is currently in "${state.phase}" phase.\n` +
|
|
177
|
+
`Integration requires a completed batch.\n` +
|
|
178
|
+
`Run /orch-status to check progress, or wait for the batch to finish.`,
|
|
179
|
+
severity: "info",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Legacy merge mode check
|
|
184
|
+
if (!orchBranch) {
|
|
185
|
+
return {
|
|
186
|
+
error:
|
|
187
|
+
`ℹ️ Batch ${batchId} used legacy merge mode — work was already merged directly into ${baseBranch || "the base branch"}.\n` +
|
|
188
|
+
`There is no separate orch branch to integrate.`,
|
|
189
|
+
severity: "info",
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
} catch (err: unknown) {
|
|
194
|
+
// Capture the error but don't return yet — user may have provided a branch arg
|
|
195
|
+
const msg = err instanceof StateFileError
|
|
196
|
+
? (err.code === "STATE_FILE_IO_ERROR"
|
|
197
|
+
? `Could not read batch state file: ${err.message}`
|
|
198
|
+
: err.code === "STATE_FILE_PARSE_ERROR"
|
|
199
|
+
? `Batch state file contains invalid JSON: ${err.message}`
|
|
200
|
+
: `Batch state file has invalid schema: ${err.message}`)
|
|
201
|
+
: `Unexpected error loading batch state: ${(err as Error).message}`;
|
|
202
|
+
if (!parsed.orchBranchArg) {
|
|
203
|
+
return {
|
|
204
|
+
error: `⚠️ ${msg}\nYou can specify the orch branch directly: /orch-integrate <orch-branch>`,
|
|
205
|
+
severity: "error",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
notices.push(`⚠️ ${msg} — using provided branch arg instead.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Source 2: CLI positional branch arg overrides or fills in
|
|
212
|
+
if (parsed.orchBranchArg) {
|
|
213
|
+
orchBranch = parsed.orchBranchArg;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Source 3: Neither state nor arg — scan for orch/* branches
|
|
217
|
+
if (!orchBranch) {
|
|
218
|
+
const candidates = deps.listOrchBranches();
|
|
219
|
+
if (candidates.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
error:
|
|
222
|
+
"❌ No completed batch found and no orch branches exist.\n" +
|
|
223
|
+
"Run /orch first to create a batch, or specify a branch: /orch-integrate <orch-branch>",
|
|
224
|
+
severity: "error",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (candidates.length === 1) {
|
|
228
|
+
orchBranch = candidates[0];
|
|
229
|
+
notices.push(`ℹ️ No batch state found. Auto-detected orch branch: ${orchBranch}`);
|
|
230
|
+
} else {
|
|
231
|
+
return {
|
|
232
|
+
error:
|
|
233
|
+
`❌ No batch state found and multiple orch branches exist:\n` +
|
|
234
|
+
candidates.map(b => ` • ${b}`).join("\n") +
|
|
235
|
+
`\n\nSpecify which branch to integrate: /orch-integrate <orch-branch>`,
|
|
236
|
+
severity: "error",
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Verify orch branch exists
|
|
242
|
+
if (!deps.orchBranchExists(orchBranch)) {
|
|
243
|
+
return {
|
|
244
|
+
error: `❌ Branch "${orchBranch}" does not exist locally.\nCheck the branch name and try again.`,
|
|
245
|
+
severity: "error",
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Detached HEAD check
|
|
250
|
+
const currentBranch = deps.getCurrentBranch();
|
|
251
|
+
if (currentBranch === null) {
|
|
252
|
+
return {
|
|
253
|
+
error:
|
|
254
|
+
"❌ HEAD is detached — cannot integrate.\n" +
|
|
255
|
+
"Check out a branch first (e.g., `git checkout main`), then retry.",
|
|
256
|
+
severity: "error",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Infer baseBranch from current branch when state is unavailable
|
|
261
|
+
if (!baseBranch) {
|
|
262
|
+
baseBranch = currentBranch;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Branch safety: current branch must match baseBranch (unless --force)
|
|
266
|
+
if (currentBranch !== baseBranch && !parsed.force) {
|
|
267
|
+
return {
|
|
268
|
+
error:
|
|
269
|
+
`⚠️ Batch was started from ${baseBranch}, but you're on ${currentBranch}.\n` +
|
|
270
|
+
`Switch to ${baseBranch} first, or use /orch-integrate --force to skip this check.`,
|
|
271
|
+
severity: "error",
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
orchBranch,
|
|
277
|
+
baseBranch,
|
|
278
|
+
batchId,
|
|
279
|
+
currentBranch,
|
|
280
|
+
notices,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Integration Execution ─────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Result of an integration attempt.
|
|
288
|
+
*/
|
|
289
|
+
export interface IntegrationResult {
|
|
290
|
+
/** Whether the integration succeeded overall */
|
|
291
|
+
success: boolean;
|
|
292
|
+
/** True if work was integrated locally (ff/merge) — controls cleanup eligibility */
|
|
293
|
+
integratedLocally: boolean;
|
|
294
|
+
/** Number of commits applied (informational) */
|
|
295
|
+
commitCount: string;
|
|
296
|
+
/** User-facing success message */
|
|
297
|
+
message: string;
|
|
298
|
+
/** User-facing error message (only when success=false) */
|
|
299
|
+
error?: string;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Dependencies injected into executeIntegration for testability.
|
|
304
|
+
*/
|
|
305
|
+
export interface IntegrationExecDeps {
|
|
306
|
+
runGit: (args: string[]) => { ok: boolean; stdout: string; stderr: string };
|
|
307
|
+
runCommand: (cmd: string, args: string[]) => { ok: boolean; stdout: string; stderr: string };
|
|
308
|
+
deleteBatchState: () => void;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Execute the integration operation for the resolved context.
|
|
313
|
+
*
|
|
314
|
+
* Mode-specific behavior:
|
|
315
|
+
* - ff: `git merge --ff-only {orchBranch}`. On failure → suggest --merge/--pr.
|
|
316
|
+
* - merge: `git merge {orchBranch} --no-edit`. On failure → show stderr.
|
|
317
|
+
* - pr: `git push origin {orchBranch}` then `gh pr create`. Never cleans up locally.
|
|
318
|
+
*
|
|
319
|
+
* Cleanup (local branch deletion + state file removal) is gated on integratedLocally === true.
|
|
320
|
+
* Cleanup failures are non-fatal (included as warnings in the message).
|
|
321
|
+
*/
|
|
322
|
+
export function executeIntegration(
|
|
323
|
+
mode: IntegrateMode,
|
|
324
|
+
context: IntegrationContext,
|
|
325
|
+
deps: IntegrationExecDeps,
|
|
326
|
+
): IntegrationResult {
|
|
327
|
+
const { orchBranch, currentBranch, batchId } = context;
|
|
328
|
+
|
|
329
|
+
if (mode === "ff") {
|
|
330
|
+
// Fast-forward merge
|
|
331
|
+
const result = deps.runGit(["merge", "--ff-only", orchBranch]);
|
|
332
|
+
if (!result.ok) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
integratedLocally: false,
|
|
336
|
+
commitCount: "0",
|
|
337
|
+
message: "",
|
|
338
|
+
error:
|
|
339
|
+
`❌ Fast-forward failed — branches have diverged.\n` +
|
|
340
|
+
`${result.stderr}\n\n` +
|
|
341
|
+
`Try:\n` +
|
|
342
|
+
` /orch-integrate --merge Create a merge commit\n` +
|
|
343
|
+
` /orch-integrate --pr Create a pull request instead`,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// Count commits that were applied
|
|
347
|
+
const countResult = deps.runGit(["rev-list", "--count", `${orchBranch}..HEAD`]);
|
|
348
|
+
// After ff, HEAD === orchBranch tip so we use a different measurement
|
|
349
|
+
// The rev-list before the merge was computed in the handler; pass commitCount through context
|
|
350
|
+
// Actually, for ff: commits applied = what was ahead before merge.
|
|
351
|
+
// After ff merge HEAD moved forward, so we measure from the merge-base.
|
|
352
|
+
// Simplest: use "merge was successful" and the pre-computed count from the handler.
|
|
353
|
+
return performCleanup(deps, orchBranch, {
|
|
354
|
+
success: true,
|
|
355
|
+
integratedLocally: true,
|
|
356
|
+
commitCount: "?", // Overridden by caller with pre-computed count
|
|
357
|
+
message: `✅ Fast-forwarded ${currentBranch} to ${orchBranch}.`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (mode === "merge") {
|
|
362
|
+
const result = deps.runGit(["merge", orchBranch, "--no-edit"]);
|
|
363
|
+
if (!result.ok) {
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
integratedLocally: false,
|
|
367
|
+
commitCount: "0",
|
|
368
|
+
message: "",
|
|
369
|
+
error:
|
|
370
|
+
`❌ Merge failed — there may be conflicts.\n` +
|
|
371
|
+
`${result.stderr}\n\n` +
|
|
372
|
+
`Resolve conflicts manually, or try:\n` +
|
|
373
|
+
` /orch-integrate --pr Create a pull request instead`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return performCleanup(deps, orchBranch, {
|
|
377
|
+
success: true,
|
|
378
|
+
integratedLocally: true,
|
|
379
|
+
commitCount: "?",
|
|
380
|
+
message: `✅ Merged ${orchBranch} into ${currentBranch} (merge commit created).`,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// PR mode
|
|
385
|
+
// Step 1: Push the orch branch to origin
|
|
386
|
+
const pushResult = deps.runGit(["push", "origin", orchBranch]);
|
|
387
|
+
if (!pushResult.ok) {
|
|
388
|
+
return {
|
|
389
|
+
success: false,
|
|
390
|
+
integratedLocally: false,
|
|
391
|
+
commitCount: "0",
|
|
392
|
+
message: "",
|
|
393
|
+
error:
|
|
394
|
+
`❌ Failed to push ${orchBranch} to origin.\n` +
|
|
395
|
+
`${pushResult.stderr}\n\n` +
|
|
396
|
+
`Check your remote configuration and try again.`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Step 2: Create pull request via gh CLI
|
|
401
|
+
const prTitle = batchId
|
|
402
|
+
? `Integrate orch batch ${batchId}`
|
|
403
|
+
: `Integrate ${orchBranch}`;
|
|
404
|
+
const ghResult = deps.runCommand("gh", [
|
|
405
|
+
"pr", "create",
|
|
406
|
+
"--base", currentBranch,
|
|
407
|
+
"--head", orchBranch,
|
|
408
|
+
"--title", prTitle,
|
|
409
|
+
"--fill",
|
|
410
|
+
]);
|
|
411
|
+
if (!ghResult.ok) {
|
|
412
|
+
return {
|
|
413
|
+
success: false,
|
|
414
|
+
integratedLocally: false,
|
|
415
|
+
commitCount: "0",
|
|
416
|
+
message: "",
|
|
417
|
+
error:
|
|
418
|
+
`❌ Branch pushed but PR creation failed.\n` +
|
|
419
|
+
`${ghResult.stderr}\n\n` +
|
|
420
|
+
`The branch ${orchBranch} is on origin — create the PR manually.`,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const prUrl = ghResult.stdout.trim();
|
|
425
|
+
return {
|
|
426
|
+
success: true,
|
|
427
|
+
integratedLocally: false, // PR mode: branch must survive
|
|
428
|
+
commitCount: "0",
|
|
429
|
+
message:
|
|
430
|
+
`✅ Pull request created for ${orchBranch} → ${currentBranch}.\n` +
|
|
431
|
+
(prUrl ? ` ${prUrl}\n` : "") +
|
|
432
|
+
`\nThe orch branch has been kept (needed for the PR).`,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Perform post-integration cleanup: delete local orch branch and batch state.
|
|
438
|
+
* Cleanup failures are non-fatal — warnings are appended to the result message.
|
|
439
|
+
*/
|
|
440
|
+
function performCleanup(
|
|
441
|
+
deps: IntegrationExecDeps,
|
|
442
|
+
orchBranch: string,
|
|
443
|
+
result: IntegrationResult,
|
|
444
|
+
): IntegrationResult {
|
|
445
|
+
const warnings: string[] = [];
|
|
446
|
+
|
|
447
|
+
// Delete local orch branch
|
|
448
|
+
const branchDelete = deps.runGit(["branch", "-D", orchBranch]);
|
|
449
|
+
if (!branchDelete.ok) {
|
|
450
|
+
warnings.push(`⚠️ Could not delete local branch ${orchBranch}: ${branchDelete.stderr}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Delete batch state file
|
|
454
|
+
try {
|
|
455
|
+
deps.deleteBatchState();
|
|
456
|
+
} catch (err: unknown) {
|
|
457
|
+
warnings.push(`⚠️ Could not clean up batch state: ${(err as Error).message}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (warnings.length > 0) {
|
|
461
|
+
result.message += "\n" + warnings.join("\n");
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
45
467
|
// ── Extension ────────────────────────────────────────────────────────
|
|
46
468
|
|
|
47
469
|
export default function (pi: ExtensionAPI) {
|
|
@@ -647,6 +1069,143 @@ export default function (pi: ExtensionAPI) {
|
|
|
647
1069
|
},
|
|
648
1070
|
});
|
|
649
1071
|
|
|
1072
|
+
pi.registerCommand("orch-integrate", {
|
|
1073
|
+
description: "Integrate completed orch batch into your working branch",
|
|
1074
|
+
handler: async (args, ctx) => {
|
|
1075
|
+
// Show usage if no args and no active batch state to infer from
|
|
1076
|
+
if (args?.trim() === "--help" || args?.trim() === "-h") {
|
|
1077
|
+
ctx.ui.notify(
|
|
1078
|
+
"Usage: /orch-integrate [<orch-branch>] [--merge] [--pr] [--force]\n\n" +
|
|
1079
|
+
"Integrate a completed orch batch into your working branch.\n\n" +
|
|
1080
|
+
"Modes:\n" +
|
|
1081
|
+
" (default) Fast-forward merge (cleanest history)\n" +
|
|
1082
|
+
" --merge Create a real merge commit\n" +
|
|
1083
|
+
" --pr Push orch branch and create a pull request\n\n" +
|
|
1084
|
+
"Options:\n" +
|
|
1085
|
+
" --force Skip branch safety check\n" +
|
|
1086
|
+
" <branch> Orch branch name (auto-detected from batch state if omitted)\n\n" +
|
|
1087
|
+
"Examples:\n" +
|
|
1088
|
+
" /orch-integrate Auto-detect and fast-forward\n" +
|
|
1089
|
+
" /orch-integrate --merge Auto-detect with merge commit\n" +
|
|
1090
|
+
" /orch-integrate orch/op-abc123 --pr Specific branch, create PR\n" +
|
|
1091
|
+
" /orch-integrate --force Skip branch safety check",
|
|
1092
|
+
"info",
|
|
1093
|
+
);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
if (!requireExecCtx(ctx)) return;
|
|
1098
|
+
|
|
1099
|
+
// Parse arguments
|
|
1100
|
+
const parsed = parseIntegrateArgs(args);
|
|
1101
|
+
if ("error" in parsed) {
|
|
1102
|
+
ctx.ui.notify(`❌ ${parsed.error}\n\nRun /orch-integrate --help for usage.`, "error");
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// ── Step 2: Resolve integration context ──────────────────
|
|
1107
|
+
const { repoRoot } = execCtx!;
|
|
1108
|
+
const resolution = resolveIntegrationContext(parsed, {
|
|
1109
|
+
loadBatchState: () => loadBatchState(repoRoot),
|
|
1110
|
+
getCurrentBranch: () => getCurrentBranch(repoRoot),
|
|
1111
|
+
listOrchBranches: () => {
|
|
1112
|
+
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
1113
|
+
return result.ok
|
|
1114
|
+
? result.stdout.split("\n").map(b => b.replace(/^\*?\s+/, "").trim()).filter(Boolean)
|
|
1115
|
+
: [];
|
|
1116
|
+
},
|
|
1117
|
+
orchBranchExists: (branch: string) => {
|
|
1118
|
+
return runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot).ok;
|
|
1119
|
+
},
|
|
1120
|
+
});
|
|
1121
|
+
|
|
1122
|
+
if ("error" in resolution) {
|
|
1123
|
+
const severity = (resolution as IntegrationContextError).severity;
|
|
1124
|
+
ctx.ui.notify(resolution.error, severity === "info" ? "info" : "error");
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const { orchBranch, baseBranch, batchId, currentBranch, notices } = resolution as IntegrationContext;
|
|
1129
|
+
|
|
1130
|
+
// Show any notices from resolution (auto-detection messages, warnings)
|
|
1131
|
+
for (const notice of notices) {
|
|
1132
|
+
ctx.ui.notify(notice, "info");
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// ── Step 2: Pre-integration summary ──────────────────────
|
|
1136
|
+
// Count commits ahead
|
|
1137
|
+
const revListResult = runGit(
|
|
1138
|
+
["rev-list", "--count", `${currentBranch}..${orchBranch}`],
|
|
1139
|
+
repoRoot,
|
|
1140
|
+
);
|
|
1141
|
+
const commitsAhead = revListResult.ok ? revListResult.stdout.trim() : "?";
|
|
1142
|
+
|
|
1143
|
+
// Get diff summary
|
|
1144
|
+
const diffStatResult = runGit(
|
|
1145
|
+
["diff", "--stat", `${currentBranch}...${orchBranch}`],
|
|
1146
|
+
repoRoot,
|
|
1147
|
+
);
|
|
1148
|
+
const diffSummary = diffStatResult.ok ? diffStatResult.stdout.trim() : "(unable to compute diff)";
|
|
1149
|
+
|
|
1150
|
+
ctx.ui.notify(
|
|
1151
|
+
`🔀 Integration Summary\n` +
|
|
1152
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
1153
|
+
` Orch branch: ${orchBranch}\n` +
|
|
1154
|
+
` Target: ${currentBranch}\n` +
|
|
1155
|
+
` Commits: ${commitsAhead} ahead\n` +
|
|
1156
|
+
` Mode: ${parsed.mode === "ff" ? "fast-forward" : parsed.mode === "merge" ? "merge commit" : "pull request"}\n` +
|
|
1157
|
+
(batchId ? ` Batch: ${batchId}\n` : "") +
|
|
1158
|
+
(parsed.force ? ` ⚠ Force: branch safety check skipped\n` : "") +
|
|
1159
|
+
`\n` +
|
|
1160
|
+
(diffSummary ? `${diffSummary}\n` : "") +
|
|
1161
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
1162
|
+
"info",
|
|
1163
|
+
);
|
|
1164
|
+
|
|
1165
|
+
// ── Step 3: Execute integration mode ─────────────────
|
|
1166
|
+
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
1167
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repoRoot),
|
|
1168
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
1169
|
+
try {
|
|
1170
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
1171
|
+
encoding: "utf-8",
|
|
1172
|
+
timeout: 60_000,
|
|
1173
|
+
cwd: repoRoot,
|
|
1174
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1175
|
+
}).trim();
|
|
1176
|
+
return { ok: true, stdout, stderr: "" };
|
|
1177
|
+
} catch (err: unknown) {
|
|
1178
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
1179
|
+
return {
|
|
1180
|
+
ok: false,
|
|
1181
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
1182
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
},
|
|
1186
|
+
deleteBatchState: () => deleteBatchState(repoRoot),
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
if (!integrationResult.success) {
|
|
1190
|
+
ctx.ui.notify(integrationResult.error!, "error");
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Override commit count with pre-computed value for local integrations
|
|
1195
|
+
if (integrationResult.integratedLocally) {
|
|
1196
|
+
integrationResult.commitCount = commitsAhead;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
ctx.ui.notify(
|
|
1200
|
+
integrationResult.message +
|
|
1201
|
+
(integrationResult.integratedLocally
|
|
1202
|
+
? `\n${integrationResult.commitCount} commit(s) applied.`
|
|
1203
|
+
: ""),
|
|
1204
|
+
"info",
|
|
1205
|
+
);
|
|
1206
|
+
},
|
|
1207
|
+
});
|
|
1208
|
+
|
|
650
1209
|
// ── Settings TUI ─────────────────────────────────────────────────
|
|
651
1210
|
|
|
652
1211
|
pi.registerCommand("taskplane-settings", {
|
|
@@ -719,7 +1278,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
719
1278
|
"/orch <areas|all> Start batch execution\n" +
|
|
720
1279
|
"/orch-plan <areas|all> Preview execution plan\n" +
|
|
721
1280
|
"/orch-deps <areas|all> Show dependency graph\n" +
|
|
722
|
-
"/orch-sessions List TMUX sessions"
|
|
1281
|
+
"/orch-sessions List TMUX sessions\n" +
|
|
1282
|
+
"/orch-integrate Integrate orch branch into working branch",
|
|
723
1283
|
"info",
|
|
724
1284
|
);
|
|
725
1285
|
|