taskplane 0.4.2 → 0.5.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.
@@ -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