taskplane 0.7.1 → 0.7.2
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/dashboard/public/app.js +17 -5
- package/extensions/taskplane/engine.ts +6 -6
- package/extensions/taskplane/extension.ts +194 -5
- package/extensions/taskplane/merge.ts +20 -20
- package/extensions/taskplane/messages.ts +4 -4
- package/extensions/taskplane/resume.ts +6 -6
- package/extensions/taskplane/supervisor.ts +95 -6
- package/extensions/taskplane/types.ts +2 -2
- package/extensions/taskplane/worktree.ts +13 -0
- package/package.json +1 -1
- package/templates/agents/local/task-reviewer.md +1 -1
- package/templates/agents/local/task-worker.md +1 -1
- package/templates/agents/task-merger.md +215 -215
- package/templates/agents/task-reviewer.md +1 -1
- package/templates/agents/task-worker.md +1 -0
package/dashboard/public/app.js
CHANGED
|
@@ -308,9 +308,17 @@ function renderSummary(batch) {
|
|
|
308
308
|
let batchChecked = 0, batchTotal = 0;
|
|
309
309
|
const waveStats = wavePlan.map((taskIds, waveIdx) => {
|
|
310
310
|
let wChecked = 0, wTotal = 0;
|
|
311
|
+
let allSucceeded = taskIds.length > 0;
|
|
311
312
|
for (const tid of taskIds) {
|
|
312
313
|
const t = taskMap.get(tid);
|
|
313
|
-
if (t
|
|
314
|
+
if (!t || t.status !== "succeeded") allSucceeded = false;
|
|
315
|
+
if (t && t.status === "succeeded" && t.statusData) {
|
|
316
|
+
// Succeeded task with statusData: count as fully done even if
|
|
317
|
+
// STATUS.md checkboxes weren't all ticked before .DONE was created
|
|
318
|
+
const total = t.statusData.total || 1;
|
|
319
|
+
wChecked += total;
|
|
320
|
+
wTotal += total;
|
|
321
|
+
} else if (t && t.statusData) {
|
|
314
322
|
wChecked += t.statusData.checked || 0;
|
|
315
323
|
wTotal += t.statusData.total || 0;
|
|
316
324
|
} else if (t && t.status === "succeeded") {
|
|
@@ -322,7 +330,7 @@ function renderSummary(batch) {
|
|
|
322
330
|
}
|
|
323
331
|
batchChecked += wChecked;
|
|
324
332
|
batchTotal += wTotal;
|
|
325
|
-
return { waveIdx, taskIds, checked: wChecked, total: wTotal };
|
|
333
|
+
return { waveIdx, taskIds, checked: wChecked, total: wTotal, allSucceeded };
|
|
326
334
|
});
|
|
327
335
|
|
|
328
336
|
const overallPct = batchTotal > 0 ? Math.round((batchChecked / batchTotal) * 100) : 0;
|
|
@@ -333,15 +341,19 @@ function renderSummary(batch) {
|
|
|
333
341
|
for (const ws of waveStats) {
|
|
334
342
|
const segWidthPct = batchTotal > 0 ? (ws.total / batchTotal) * 100 : (100 / waveStats.length);
|
|
335
343
|
const fillPct = ws.total > 0 ? (ws.checked / ws.total) * 100 : 0;
|
|
336
|
-
const
|
|
337
|
-
const
|
|
344
|
+
const checkboxDone = ws.checked === ws.total && ws.total > 0;
|
|
345
|
+
const pastWave = ws.waveIdx < currentWaveIdx;
|
|
346
|
+
const batchDone = batch.phase === "completed" || batch.phase === "merging";
|
|
347
|
+
const isDone = checkboxDone || pastWave || batchDone || ws.allSucceeded;
|
|
348
|
+
const isCurrent = ws.waveIdx === currentWaveIdx && (batch.phase === "executing" || batch.phase === "merging");
|
|
338
349
|
const isFuture = ws.waveIdx > currentWaveIdx && batch.phase === "executing";
|
|
339
350
|
|
|
340
351
|
const fillClass = isDone ? "pct-hi" : fillPct > 50 ? "pct-mid" : fillPct > 0 ? "pct-low" : "pct-0";
|
|
352
|
+
const fillWidth = isDone ? 100 : fillPct;
|
|
341
353
|
const segClass = isCurrent ? "wave-seg-current" : isFuture ? "wave-seg-future" : "";
|
|
342
354
|
|
|
343
355
|
barHtml += `<div class="wave-seg ${segClass}" style="width:${segWidthPct.toFixed(1)}%" title="W${ws.waveIdx + 1}: ${ws.checked}/${ws.total} checkboxes (${ws.taskIds.join(', ')})">`;
|
|
344
|
-
barHtml += ` <div class="wave-seg-fill ${fillClass}" style="width:${
|
|
356
|
+
barHtml += ` <div class="wave-seg-fill ${fillClass}" style="width:${fillWidth.toFixed(1)}%"></div>`;
|
|
345
357
|
barHtml += ` <span class="wave-seg-label">W${ws.waveIdx + 1}</span>`;
|
|
346
358
|
barHtml += `</div>`;
|
|
347
359
|
}
|
|
@@ -1058,7 +1058,7 @@ export async function executeOrchBatch(
|
|
|
1058
1058
|
laneCount: mergeableLaneCount,
|
|
1059
1059
|
}, onEngineEvent);
|
|
1060
1060
|
|
|
1061
|
-
mergeResult = mergeWaveByRepo(
|
|
1061
|
+
mergeResult = await mergeWaveByRepo(
|
|
1062
1062
|
waveResult.allocatedLanes,
|
|
1063
1063
|
waveResult,
|
|
1064
1064
|
waveIdx + 1,
|
|
@@ -1237,14 +1237,14 @@ export async function executeOrchBatch(
|
|
|
1237
1237
|
const mergeRepoId = extractFailedRepoId(mergeResult) ?? null;
|
|
1238
1238
|
const mergeFailedLane = mergeResult.failedLane ?? undefined;
|
|
1239
1239
|
|
|
1240
|
-
const retryOutcome = applyMergeRetryLoop(
|
|
1240
|
+
const retryOutcome = await applyMergeRetryLoop(
|
|
1241
1241
|
mergeResult,
|
|
1242
1242
|
waveIdx,
|
|
1243
1243
|
batchState.resilience.retryCountByScope,
|
|
1244
1244
|
{
|
|
1245
|
-
performMerge: () => {
|
|
1245
|
+
performMerge: async () => {
|
|
1246
1246
|
batchState.phase = "merging";
|
|
1247
|
-
return mergeWaveByRepo(
|
|
1247
|
+
return await mergeWaveByRepo(
|
|
1248
1248
|
waveResult.allocatedLanes,
|
|
1249
1249
|
waveResult,
|
|
1250
1250
|
waveIdx + 1,
|
|
@@ -1986,8 +1986,8 @@ export async function executeOrchBatch(
|
|
|
1986
1986
|
const totalElapsedSec = Math.round((batchState.endedAt - batchState.startedAt) / 1000);
|
|
1987
1987
|
|
|
1988
1988
|
// Determine final batch state. Cast to OrchBatchPhase to bypass control-flow
|
|
1989
|
-
// narrowing — mergeWave()
|
|
1990
|
-
//
|
|
1989
|
+
// narrowing — mergeWave() could leave phase as "merging" if an unexpected
|
|
1990
|
+
// throw occurs between setting "merging" and restoring "executing".
|
|
1991
1991
|
if ((batchState.phase as OrchBatchPhase) === "executing" || (batchState.phase as OrchBatchPhase) === "merging") {
|
|
1992
1992
|
// Normal completion (not stopped, paused, or aborted)
|
|
1993
1993
|
if (batchState.failedTasks > 0) {
|
|
@@ -46,6 +46,7 @@ import { openSettingsTui } from "./settings-tui.ts";
|
|
|
46
46
|
import {
|
|
47
47
|
activateSupervisor,
|
|
48
48
|
deactivateSupervisor,
|
|
49
|
+
transitionToRoutingMode,
|
|
49
50
|
freshSupervisorState,
|
|
50
51
|
registerSupervisorPromptHook,
|
|
51
52
|
checkSupervisorLockOnStartup,
|
|
@@ -55,8 +56,9 @@ import {
|
|
|
55
56
|
DEFAULT_SUPERVISOR_CONFIG,
|
|
56
57
|
triggerSupervisorIntegration,
|
|
57
58
|
presentBatchSummary,
|
|
59
|
+
resolveModelFromString,
|
|
58
60
|
} from "./supervisor.ts";
|
|
59
|
-
import type { SupervisorConfig, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
|
|
61
|
+
import type { SupervisorConfig, SupervisorRoutingContext, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
|
|
60
62
|
import type {
|
|
61
63
|
AbortMode,
|
|
62
64
|
ExecutionContext,
|
|
@@ -719,6 +721,127 @@ export function collectRepoCleanupFindings(
|
|
|
719
721
|
* This prevents unhandled promise rejections from crashing the session
|
|
720
722
|
* or leaving batch state inconsistent.
|
|
721
723
|
*/
|
|
724
|
+
|
|
725
|
+
// ── Model Availability Pre-Flight ───────────────────────────────────
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* A single model configuration to validate.
|
|
729
|
+
*/
|
|
730
|
+
interface ModelCheckEntry {
|
|
731
|
+
/** Role label for display (e.g., "Worker", "Reviewer") */
|
|
732
|
+
role: string;
|
|
733
|
+
/** Model string from config (empty = inherit session model) */
|
|
734
|
+
modelStr: string;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Result of a single model availability check.
|
|
739
|
+
*/
|
|
740
|
+
export interface ModelCheckResult {
|
|
741
|
+
role: string;
|
|
742
|
+
modelStr: string;
|
|
743
|
+
status: "inherit" | "found" | "not-found";
|
|
744
|
+
resolvedName?: string;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Validate that all configured agent models are available in the model registry.
|
|
749
|
+
*
|
|
750
|
+
* Checks worker, reviewer, merger, and supervisor model settings. Models set to
|
|
751
|
+
* empty string ("") or not configured inherit the session model and are always valid.
|
|
752
|
+
*
|
|
753
|
+
* Does NOT validate API keys (that would require side-effectful setModel calls).
|
|
754
|
+
* This catches the most common misconfiguration: specifying a model that isn't
|
|
755
|
+
* registered in pi (wrong name, missing provider, etc.).
|
|
756
|
+
*
|
|
757
|
+
* @param orchConfig - Orchestrator configuration
|
|
758
|
+
* @param runnerConfig - Task runner configuration
|
|
759
|
+
* @param supervisorConfig - Supervisor configuration
|
|
760
|
+
* @param ctx - Extension context with model registry
|
|
761
|
+
* @returns Array of check results (one per role)
|
|
762
|
+
*
|
|
763
|
+
* @since v0.7.2
|
|
764
|
+
*/
|
|
765
|
+
export function validateModelAvailability(
|
|
766
|
+
orchConfig: OrchestratorConfig,
|
|
767
|
+
runnerConfig: TaskRunnerConfig,
|
|
768
|
+
supervisorConfig: SupervisorConfig,
|
|
769
|
+
ctx: ExtensionContext,
|
|
770
|
+
): ModelCheckResult[] {
|
|
771
|
+
const entries: ModelCheckEntry[] = [
|
|
772
|
+
{ role: "Worker", modelStr: runnerConfig.worker?.model ?? "" },
|
|
773
|
+
{ role: "Reviewer", modelStr: runnerConfig.reviewer?.model ?? "" },
|
|
774
|
+
{ role: "Merger", modelStr: orchConfig.merge?.model ?? "" },
|
|
775
|
+
{ role: "Supervisor", modelStr: supervisorConfig.model ?? "" },
|
|
776
|
+
];
|
|
777
|
+
|
|
778
|
+
const sessionModel = ctx.model;
|
|
779
|
+
const results: ModelCheckResult[] = [];
|
|
780
|
+
|
|
781
|
+
for (const entry of entries) {
|
|
782
|
+
if (!entry.modelStr) {
|
|
783
|
+
// Empty = inherit session model
|
|
784
|
+
results.push({
|
|
785
|
+
role: entry.role,
|
|
786
|
+
modelStr: "(inherit)",
|
|
787
|
+
status: "inherit",
|
|
788
|
+
resolvedName: sessionModel
|
|
789
|
+
? `${(sessionModel as any).provider ?? ""}/${sessionModel.id}`.replace(/^\//, "")
|
|
790
|
+
: "session default",
|
|
791
|
+
});
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const resolved = resolveModelFromString(entry.modelStr, ctx);
|
|
796
|
+
if (resolved) {
|
|
797
|
+
results.push({
|
|
798
|
+
role: entry.role,
|
|
799
|
+
modelStr: entry.modelStr,
|
|
800
|
+
status: "found",
|
|
801
|
+
resolvedName: `${(resolved as any).provider ?? ""}/${resolved.id}`.replace(/^\//, ""),
|
|
802
|
+
});
|
|
803
|
+
} else {
|
|
804
|
+
results.push({
|
|
805
|
+
role: entry.role,
|
|
806
|
+
modelStr: entry.modelStr,
|
|
807
|
+
status: "not-found",
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return results;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Format model validation results for display.
|
|
817
|
+
*
|
|
818
|
+
* @param results - Model check results from validateModelAvailability
|
|
819
|
+
* @returns Formatted string for ctx.ui.notify
|
|
820
|
+
*/
|
|
821
|
+
export function formatModelValidation(results: ModelCheckResult[]): string {
|
|
822
|
+
const lines: string[] = ["Model Configuration:"];
|
|
823
|
+
let hasFailure = false;
|
|
824
|
+
|
|
825
|
+
for (const r of results) {
|
|
826
|
+
if (r.status === "inherit") {
|
|
827
|
+
lines.push(` ✅ ${r.role.padEnd(12)} inherit → ${r.resolvedName}`);
|
|
828
|
+
} else if (r.status === "found") {
|
|
829
|
+
lines.push(` ✅ ${r.role.padEnd(12)} ${r.modelStr} → ${r.resolvedName}`);
|
|
830
|
+
} else {
|
|
831
|
+
lines.push(` ❌ ${r.role.padEnd(12)} ${r.modelStr} — NOT FOUND in model registry`);
|
|
832
|
+
hasFailure = true;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (hasFailure) {
|
|
837
|
+
lines.push("");
|
|
838
|
+
lines.push(" Fix: update the model in .pi/taskplane-config.json or /taskplane-settings,");
|
|
839
|
+
lines.push(" or remove the override to inherit the session model.");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
return lines.join("\n");
|
|
843
|
+
}
|
|
844
|
+
|
|
722
845
|
export function startBatchAsync(
|
|
723
846
|
engineFn: () => Promise<void>,
|
|
724
847
|
batchState: import("./types.ts").OrchBatchRuntimeState,
|
|
@@ -1186,6 +1309,15 @@ export default function (pi: ExtensionAPI) {
|
|
|
1186
1309
|
|
|
1187
1310
|
if (!requireExecCtx(ctx)) return;
|
|
1188
1311
|
|
|
1312
|
+
// ── TP-128: Transition from routing-mode supervisor to batch execution ──
|
|
1313
|
+
// If the supervisor is active in routing mode (conversational, no batch),
|
|
1314
|
+
// deactivate it so the batch can start fresh with monitoring-mode supervisor.
|
|
1315
|
+
// This enables the workflow: /orch → conversation → "run the tasks" → /orch all
|
|
1316
|
+
// without the operator needing to know about internal mode distinctions.
|
|
1317
|
+
if (supervisorState.active && supervisorState.routingContext) {
|
|
1318
|
+
await deactivateSupervisor(pi, supervisorState);
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1189
1321
|
// Prevent concurrent batch execution (merging is an active state)
|
|
1190
1322
|
if (orchBatchState.phase !== "idle" && orchBatchState.phase !== "completed" && orchBatchState.phase !== "failed" && orchBatchState.phase !== "stopped") {
|
|
1191
1323
|
ctx.ui.notify(
|
|
@@ -1265,6 +1397,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
1265
1397
|
break;
|
|
1266
1398
|
}
|
|
1267
1399
|
|
|
1400
|
+
// ── Model availability pre-flight ────────────────────────
|
|
1401
|
+
// Validate that all configured agent models are resolvable in
|
|
1402
|
+
// the model registry before starting. Catches misconfigured
|
|
1403
|
+
// model names early instead of failing hours into a batch.
|
|
1404
|
+
const modelResults = validateModelAvailability(orchConfig, runnerConfig, supervisorConfig, ctx);
|
|
1405
|
+
const modelFailures = modelResults.filter(r => r.status === "not-found");
|
|
1406
|
+
ctx.ui.notify(formatModelValidation(modelResults), modelFailures.length > 0 ? "error" : "info");
|
|
1407
|
+
if (modelFailures.length > 0) {
|
|
1408
|
+
ctx.ui.notify(
|
|
1409
|
+
`❌ Cannot start batch — ${modelFailures.length} model(s) not found: ` +
|
|
1410
|
+
modelFailures.map(f => `${f.role} (${f.modelStr})`).join(", ") +
|
|
1411
|
+
`.\n\nFix the model configuration and try again.`,
|
|
1412
|
+
"error",
|
|
1413
|
+
);
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1268
1417
|
// Reset batch state for new execution
|
|
1269
1418
|
orchBatchState = freshOrchBatchState();
|
|
1270
1419
|
latestMonitorState = null;
|
|
@@ -1377,9 +1526,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
1377
1526
|
{ triggerTurn: false },
|
|
1378
1527
|
);
|
|
1379
1528
|
}
|
|
1380
|
-
// TP-043: Generate summary before
|
|
1529
|
+
// TP-043: Generate summary before transition
|
|
1381
1530
|
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1382
|
-
|
|
1531
|
+
// TP-128: Transition to routing mode instead of deactivating.
|
|
1532
|
+
// The operator can continue the conversation (integrate, plan
|
|
1533
|
+
// next batch, create tasks) without re-invoking /orch.
|
|
1534
|
+
const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
|
|
1535
|
+
? {
|
|
1536
|
+
routingState: "completed-batch",
|
|
1537
|
+
contextMessage:
|
|
1538
|
+
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
1539
|
+
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1540
|
+
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1541
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
1542
|
+
`You can also:\n` +
|
|
1543
|
+
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
1544
|
+
`• Create new tasks for the next batch\n` +
|
|
1545
|
+
`• Run a health check`,
|
|
1546
|
+
}
|
|
1547
|
+
: {
|
|
1548
|
+
routingState: "no-tasks",
|
|
1549
|
+
contextMessage:
|
|
1550
|
+
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1551
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
1552
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
1553
|
+
`What would you like to do next?`,
|
|
1554
|
+
};
|
|
1555
|
+
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
1383
1556
|
},
|
|
1384
1557
|
);
|
|
1385
1558
|
|
|
@@ -1697,9 +1870,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
1697
1870
|
{ triggerTurn: false },
|
|
1698
1871
|
);
|
|
1699
1872
|
}
|
|
1700
|
-
// TP-043: Generate summary before
|
|
1873
|
+
// TP-043: Generate summary before transition
|
|
1701
1874
|
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1702
|
-
|
|
1875
|
+
// TP-128: Transition to routing mode (same as /orch onTerminal)
|
|
1876
|
+
const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
|
|
1877
|
+
? {
|
|
1878
|
+
routingState: "completed-batch",
|
|
1879
|
+
contextMessage:
|
|
1880
|
+
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
1881
|
+
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1882
|
+
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1883
|
+
`Would you like me to integrate it, or would you prefer to review first?`,
|
|
1884
|
+
}
|
|
1885
|
+
: {
|
|
1886
|
+
routingState: "no-tasks",
|
|
1887
|
+
contextMessage:
|
|
1888
|
+
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1889
|
+
`What would you like to do next?`,
|
|
1890
|
+
};
|
|
1891
|
+
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
1703
1892
|
},
|
|
1704
1893
|
);
|
|
1705
1894
|
|
|
@@ -11,7 +11,7 @@ import { resolveOperatorId } from "./naming.ts";
|
|
|
11
11
|
import { MERGE_POLL_INTERVAL_MS, MERGE_RESULT_GRACE_MS, MERGE_RESULT_READ_RETRIES, MERGE_RESULT_READ_RETRY_DELAY_MS, MERGE_SPAWN_RETRY_MAX, MERGE_TIMEOUT_MAX_RETRIES, MERGE_TIMEOUT_MS, MergeError, VALID_MERGE_STATUSES } from "./types.ts";
|
|
12
12
|
import type { AllocatedLane, LaneExecutionResult, MergeLaneResult, MergeResult, MergeResultStatus, MergeWaveResult, OrchestratorConfig, RepoMergeOutcome, TaskRunnerConfig, TransactionRecord, TransactionStatus, VerificationBaselineResult, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
13
13
|
import { resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
|
|
14
|
-
import { generateMergeWorktreePath, sleepSync } from "./worktree.ts";
|
|
14
|
+
import { generateMergeWorktreePath, sleepAsync, sleepSync } from "./worktree.ts";
|
|
15
15
|
import { getCurrentBranch, runGit } from "./git.ts";
|
|
16
16
|
import { ORCH_MESSAGES } from "./messages.ts";
|
|
17
17
|
import { loadOrchestratorConfig } from "./config.ts";
|
|
@@ -353,7 +353,7 @@ export function buildMergeRequest(
|
|
|
353
353
|
* @param agentRoot - Root for agent prompts. When pointer is resolved, this is the config repo's agent dir. Falls back to `<stateRoot>/.pi/agents/` or `<repoRoot>/.pi/agents/`.
|
|
354
354
|
* @throws MergeError if spawn fails after retries
|
|
355
355
|
*/
|
|
356
|
-
export function spawnMergeAgent(
|
|
356
|
+
export async function spawnMergeAgent(
|
|
357
357
|
sessionName: string,
|
|
358
358
|
repoRoot: string,
|
|
359
359
|
mergeWorkDir: string,
|
|
@@ -361,7 +361,7 @@ export function spawnMergeAgent(
|
|
|
361
361
|
config: OrchestratorConfig,
|
|
362
362
|
stateRoot?: string,
|
|
363
363
|
agentRoot?: string,
|
|
364
|
-
): void {
|
|
364
|
+
): Promise<void> {
|
|
365
365
|
execLog("merge", sessionName, "preparing to spawn merge agent", {
|
|
366
366
|
mergeWorkDir,
|
|
367
367
|
mergeRequestPath,
|
|
@@ -371,7 +371,7 @@ export function spawnMergeAgent(
|
|
|
371
371
|
if (tmuxHasSession(sessionName)) {
|
|
372
372
|
execLog("merge", sessionName, "killing stale merge session");
|
|
373
373
|
tmuxKillSession(sessionName);
|
|
374
|
-
|
|
374
|
+
await sleepAsync(500);
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
// Build the pi command for the merge agent.
|
|
@@ -424,7 +424,7 @@ export function spawnMergeAgent(
|
|
|
424
424
|
execLog("merge", sessionName, `merge spawn attempt ${attempt} failed: ${lastError}`);
|
|
425
425
|
|
|
426
426
|
if (attempt <= MERGE_SPAWN_RETRY_MAX) {
|
|
427
|
-
|
|
427
|
+
await sleepAsync(attempt * 1000);
|
|
428
428
|
}
|
|
429
429
|
}
|
|
430
430
|
|
|
@@ -480,11 +480,11 @@ const SUCCESSFUL_MERGE_STATUSES = new Set<string>(["SUCCESS", "CONFLICT_RESOLVED
|
|
|
480
480
|
* @returns Validated MergeResult
|
|
481
481
|
* @throws MergeError on timeout, session death, or invalid result
|
|
482
482
|
*/
|
|
483
|
-
export function waitForMergeResult(
|
|
483
|
+
export async function waitForMergeResult(
|
|
484
484
|
resultPath: string,
|
|
485
485
|
sessionName: string,
|
|
486
486
|
timeoutMs: number = MERGE_TIMEOUT_MS,
|
|
487
|
-
): MergeResult {
|
|
487
|
+
): Promise<MergeResult> {
|
|
488
488
|
const startTime = Date.now();
|
|
489
489
|
let sessionDiedAt: number | null = null;
|
|
490
490
|
|
|
@@ -559,7 +559,7 @@ export function waitForMergeResult(
|
|
|
559
559
|
// parseMergeResult already retries, so if it throws, it's final.
|
|
560
560
|
if (err instanceof MergeError && err.code === "MERGE_RESULT_INVALID") {
|
|
561
561
|
// Wait a bit and try once more (file might still be in flight)
|
|
562
|
-
|
|
562
|
+
await sleepAsync(MERGE_RESULT_READ_RETRY_DELAY_MS);
|
|
563
563
|
if (existsSync(resultPath)) {
|
|
564
564
|
try {
|
|
565
565
|
return parseMergeResult(resultPath);
|
|
@@ -605,7 +605,7 @@ export function waitForMergeResult(
|
|
|
605
605
|
}
|
|
606
606
|
|
|
607
607
|
// Poll interval
|
|
608
|
-
|
|
608
|
+
await sleepAsync(MERGE_POLL_INTERVAL_MS);
|
|
609
609
|
}
|
|
610
610
|
}
|
|
611
611
|
|
|
@@ -931,7 +931,7 @@ function runPostMergeVerification(
|
|
|
931
931
|
* @param baseBranch - Branch to merge into (captured at batch start)
|
|
932
932
|
* @returns MergeWaveResult with per-lane outcomes
|
|
933
933
|
*/
|
|
934
|
-
export function mergeWave(
|
|
934
|
+
export async function mergeWave(
|
|
935
935
|
completedLanes: AllocatedLane[],
|
|
936
936
|
waveResult: WaveExecutionResult,
|
|
937
937
|
waveIndex: number,
|
|
@@ -943,7 +943,7 @@ export function mergeWave(
|
|
|
943
943
|
agentRoot?: string,
|
|
944
944
|
testingCommands?: Record<string, string>,
|
|
945
945
|
repoId?: string,
|
|
946
|
-
): MergeWaveResult {
|
|
946
|
+
): Promise<MergeWaveResult> {
|
|
947
947
|
const startTime = Date.now();
|
|
948
948
|
const tmuxPrefix = config.orchestrator.tmux_prefix;
|
|
949
949
|
const opId = resolveOperatorId(config);
|
|
@@ -1008,7 +1008,7 @@ export function mergeWave(
|
|
|
1008
1008
|
forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
|
|
1009
1009
|
if (existsSync(mergeWorkDir)) {
|
|
1010
1010
|
// Force cleanup didn't fully remove — wait and retry once
|
|
1011
|
-
|
|
1011
|
+
await sleepAsync(500);
|
|
1012
1012
|
forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
|
|
1013
1013
|
}
|
|
1014
1014
|
try {
|
|
@@ -1242,14 +1242,14 @@ export function mergeWave(
|
|
|
1242
1242
|
}
|
|
1243
1243
|
|
|
1244
1244
|
// Re-spawn merge agent for the retry
|
|
1245
|
-
spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
|
|
1245
|
+
await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
|
|
1246
1246
|
} else {
|
|
1247
1247
|
// First attempt: spawn merge agent
|
|
1248
|
-
spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
|
|
1248
|
+
await spawnMergeAgent(sessionName, repoRoot, mergeWorkDir, requestFilePath, config, stateRoot, agentRoot);
|
|
1249
1249
|
}
|
|
1250
1250
|
|
|
1251
1251
|
try {
|
|
1252
|
-
mergeResult = waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
|
|
1252
|
+
mergeResult = await waitForMergeResult(resultFilePath, sessionName, currentTimeoutMs);
|
|
1253
1253
|
lastTimeoutError = null;
|
|
1254
1254
|
break; // Success — exit retry loop
|
|
1255
1255
|
} catch (waitErr: unknown) {
|
|
@@ -1732,7 +1732,7 @@ export function mergeWave(
|
|
|
1732
1732
|
forceRemoveMergeWorktree(mergeWorkDir, repoRoot, `W${waveIndex}`);
|
|
1733
1733
|
try {
|
|
1734
1734
|
// Small delay to ensure worktree lock is released
|
|
1735
|
-
|
|
1735
|
+
await sleepAsync(500);
|
|
1736
1736
|
spawnSync("git", ["branch", "-D", tempBranch], { cwd: repoRoot });
|
|
1737
1737
|
} catch { /* best effort */ }
|
|
1738
1738
|
}
|
|
@@ -1845,7 +1845,7 @@ export function groupLanesByRepo(
|
|
|
1845
1845
|
* @param workspaceConfig - Workspace configuration (null in repo mode)
|
|
1846
1846
|
* @returns MergeWaveResult with per-lane and per-repo outcomes
|
|
1847
1847
|
*/
|
|
1848
|
-
export function mergeWaveByRepo(
|
|
1848
|
+
export async function mergeWaveByRepo(
|
|
1849
1849
|
completedLanes: AllocatedLane[],
|
|
1850
1850
|
waveResult: WaveExecutionResult,
|
|
1851
1851
|
waveIndex: number,
|
|
@@ -1857,7 +1857,7 @@ export function mergeWaveByRepo(
|
|
|
1857
1857
|
stateRoot?: string,
|
|
1858
1858
|
agentRoot?: string,
|
|
1859
1859
|
testingCommands?: Record<string, string>,
|
|
1860
|
-
): MergeWaveResult {
|
|
1860
|
+
): Promise<MergeWaveResult> {
|
|
1861
1861
|
const startTime = Date.now();
|
|
1862
1862
|
|
|
1863
1863
|
// Build lane outcome lookup for merge eligibility (same logic as mergeWave).
|
|
@@ -1901,7 +1901,7 @@ export function mergeWaveByRepo(
|
|
|
1901
1901
|
// In repo mode (single group with repoId=undefined), delegate directly
|
|
1902
1902
|
// to mergeWave() for zero-overhead backward compatibility.
|
|
1903
1903
|
if (repoGroups.length === 1 && repoGroups[0].repoId === undefined) {
|
|
1904
|
-
const result = mergeWave(
|
|
1904
|
+
const result = await mergeWave(
|
|
1905
1905
|
completedLanes,
|
|
1906
1906
|
waveResult,
|
|
1907
1907
|
waveIndex,
|
|
@@ -1956,7 +1956,7 @@ export function mergeWaveByRepo(
|
|
|
1956
1956
|
allocatedLanes: waveResult.allocatedLanes.filter(l => groupLaneNumbers.has(l.laneNumber)),
|
|
1957
1957
|
};
|
|
1958
1958
|
|
|
1959
|
-
const groupResult = mergeWave(
|
|
1959
|
+
const groupResult = await mergeWave(
|
|
1960
1960
|
group.lanes,
|
|
1961
1961
|
filteredWaveResult,
|
|
1962
1962
|
waveIndex,
|
|
@@ -698,12 +698,12 @@ export function extractFailedRepoId(mergeResult: MergeWaveResult): string | unde
|
|
|
698
698
|
* @returns Outcome describing what happened during the retry cycle
|
|
699
699
|
* @since TP-033 R006
|
|
700
700
|
*/
|
|
701
|
-
export function applyMergeRetryLoop(
|
|
701
|
+
export async function applyMergeRetryLoop(
|
|
702
702
|
mergeResult: MergeWaveResult,
|
|
703
703
|
waveIdx: number,
|
|
704
704
|
retryCountByScope: Record<string, number>,
|
|
705
705
|
callbacks: MergeRetryCallbacks,
|
|
706
|
-
): MergeRetryLoopOutcome {
|
|
706
|
+
): Promise<MergeRetryLoopOutcome> {
|
|
707
707
|
let currentResult = mergeResult;
|
|
708
708
|
|
|
709
709
|
// Classify the initial failure
|
|
@@ -766,12 +766,12 @@ export function applyMergeRetryLoop(
|
|
|
766
766
|
);
|
|
767
767
|
|
|
768
768
|
if (lastDecision.cooldownMs > 0) {
|
|
769
|
-
callbacks.sleep(lastDecision.cooldownMs);
|
|
769
|
+
await callbacks.sleep(lastDecision.cooldownMs);
|
|
770
770
|
}
|
|
771
771
|
|
|
772
772
|
// Re-invoke merge
|
|
773
773
|
callbacks.persist("merge-retry-start");
|
|
774
|
-
currentResult = callbacks.performMerge();
|
|
774
|
+
currentResult = await callbacks.performMerge();
|
|
775
775
|
callbacks.updateMergeResult(currentResult);
|
|
776
776
|
callbacks.persist("merge-retry-complete");
|
|
777
777
|
|
|
@@ -1246,7 +1246,7 @@ export async function resumeOrchBatch(
|
|
|
1246
1246
|
allocatedLanes: reExecAllocatedLanes,
|
|
1247
1247
|
};
|
|
1248
1248
|
|
|
1249
|
-
const reExecMergeResult = mergeWaveByRepo(
|
|
1249
|
+
const reExecMergeResult = await mergeWaveByRepo(
|
|
1250
1250
|
reExecAllocatedLanes,
|
|
1251
1251
|
syntheticWaveResult,
|
|
1252
1252
|
RE_EXEC_WAVE_INDEX,
|
|
@@ -1451,7 +1451,7 @@ export async function resumeOrchBatch(
|
|
|
1451
1451
|
batchState.phase = "merging";
|
|
1452
1452
|
persistRuntimeState("merge-retry-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1453
1453
|
|
|
1454
|
-
const mergeRetryResult = mergeWaveByRepo(
|
|
1454
|
+
const mergeRetryResult = await mergeWaveByRepo(
|
|
1455
1455
|
mergeRetryLanes,
|
|
1456
1456
|
syntheticWaveResult,
|
|
1457
1457
|
waveIdx + 1,
|
|
@@ -1630,7 +1630,7 @@ export async function resumeOrchBatch(
|
|
|
1630
1630
|
persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1631
1631
|
onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx + 1, mergeableLaneCount), "info");
|
|
1632
1632
|
|
|
1633
|
-
mergeResult = mergeWaveByRepo(
|
|
1633
|
+
mergeResult = await mergeWaveByRepo(
|
|
1634
1634
|
waveResult.allocatedLanes,
|
|
1635
1635
|
waveResult,
|
|
1636
1636
|
waveIdx + 1,
|
|
@@ -1762,14 +1762,14 @@ export async function resumeOrchBatch(
|
|
|
1762
1762
|
batchState.resilience = defaultResilienceState();
|
|
1763
1763
|
}
|
|
1764
1764
|
|
|
1765
|
-
const retryOutcome = applyMergeRetryLoop(
|
|
1765
|
+
const retryOutcome = await applyMergeRetryLoop(
|
|
1766
1766
|
mergeResult,
|
|
1767
1767
|
waveIdx,
|
|
1768
1768
|
batchState.resilience.retryCountByScope,
|
|
1769
1769
|
{
|
|
1770
|
-
performMerge: () => {
|
|
1770
|
+
performMerge: async () => {
|
|
1771
1771
|
batchState.phase = "merging";
|
|
1772
|
-
return mergeWaveByRepo(
|
|
1772
|
+
return await mergeWaveByRepo(
|
|
1773
1773
|
waveResult.allocatedLanes,
|
|
1774
1774
|
waveResult,
|
|
1775
1775
|
waveIdx + 1,
|
|
@@ -2149,13 +2149,30 @@ Read the relevant script section now before starting the conversation.
|
|
|
2149
2149
|
- Summarize what you'll create before writing files — let the operator confirm
|
|
2150
2150
|
- If the operator says "just give me defaults", do it and move on
|
|
2151
2151
|
|
|
2152
|
+
## Starting a Batch
|
|
2153
|
+
|
|
2154
|
+
When the operator wants to run pending tasks, use the \`/orch all\` command.
|
|
2155
|
+
You can invoke it directly — it will seamlessly transition you from conversational
|
|
2156
|
+
mode to batch monitoring mode. Examples of operator intent:
|
|
2157
|
+
|
|
2158
|
+
- "run the open tasks" → respond with a brief confirmation, then invoke \`/orch all\`
|
|
2159
|
+
- "start the batch" → invoke \`/orch all\`
|
|
2160
|
+
- "run just the platform tasks" → invoke \`/orch platform\` (with the area name)
|
|
2161
|
+
|
|
2162
|
+
Before starting, you may optionally:
|
|
2163
|
+
- Show a quick summary of pending tasks and wave plan (\`/orch-plan all\`)
|
|
2164
|
+
- Ask for confirmation if the operator's intent was ambiguous
|
|
2165
|
+
|
|
2166
|
+
After \`/orch all\` starts, your system prompt will automatically switch to
|
|
2167
|
+
batch monitoring mode. You'll have full visibility into wave progress, task
|
|
2168
|
+
outcomes, and can handle failures.
|
|
2169
|
+
|
|
2152
2170
|
## What You Must NEVER Do
|
|
2153
2171
|
|
|
2154
|
-
1. Never
|
|
2155
|
-
2. Never
|
|
2156
|
-
3. Never
|
|
2157
|
-
4. Never
|
|
2158
|
-
5. Never make assumptions about project conventions — detect them
|
|
2172
|
+
1. Never modify existing code files (only create config/scaffolding)
|
|
2173
|
+
2. Never \`git push\` to any remote
|
|
2174
|
+
3. Never overwrite existing config files without asking
|
|
2175
|
+
4. Never make assumptions about project conventions — detect them
|
|
2159
2176
|
`;
|
|
2160
2177
|
|
|
2161
2178
|
return prompt;
|
|
@@ -2252,7 +2269,7 @@ export function freshSupervisorState(): SupervisorState {
|
|
|
2252
2269
|
* @returns The resolved Model, or undefined if not found
|
|
2253
2270
|
* @since TP-041
|
|
2254
2271
|
*/
|
|
2255
|
-
function resolveModelFromString(
|
|
2272
|
+
export function resolveModelFromString(
|
|
2256
2273
|
modelStr: string,
|
|
2257
2274
|
ctx: ExtensionContext,
|
|
2258
2275
|
): Model<Api> | undefined {
|
|
@@ -2504,6 +2521,78 @@ export async function deactivateSupervisor(
|
|
|
2504
2521
|
state.pendingSummaryDeps = null;
|
|
2505
2522
|
}
|
|
2506
2523
|
|
|
2524
|
+
/**
|
|
2525
|
+
* Transition the supervisor from batch-monitoring mode back to routing mode.
|
|
2526
|
+
*
|
|
2527
|
+
* Called after a batch completes (or fails/pauses) instead of fully deactivating.
|
|
2528
|
+
* Tears down batch-monitoring infrastructure (lockfile, heartbeat, event tailer)
|
|
2529
|
+
* but keeps the supervisor active with a routing context — so the operator can
|
|
2530
|
+
* continue the conversation (plan next batch, create tasks, integrate, etc.)
|
|
2531
|
+
* without needing to re-invoke `/orch`.
|
|
2532
|
+
*
|
|
2533
|
+
* This enables the continuous workflow:
|
|
2534
|
+
* /orch → conversation → "run the tasks" → batch runs → batch completes →
|
|
2535
|
+
* conversation continues → "create more tasks" → "run them" → repeat
|
|
2536
|
+
*
|
|
2537
|
+
* @param pi - The ExtensionAPI instance
|
|
2538
|
+
* @param state - Supervisor state to transition
|
|
2539
|
+
* @param routingContext - The routing context for the new conversational mode
|
|
2540
|
+
*
|
|
2541
|
+
* @since TP-128
|
|
2542
|
+
*/
|
|
2543
|
+
export async function transitionToRoutingMode(
|
|
2544
|
+
pi: ExtensionAPI,
|
|
2545
|
+
state: SupervisorState,
|
|
2546
|
+
routingContext: SupervisorRoutingContext,
|
|
2547
|
+
): Promise<void> {
|
|
2548
|
+
if (!state.active) return;
|
|
2549
|
+
|
|
2550
|
+
// Tear down batch-monitoring infrastructure
|
|
2551
|
+
stopEventTailer(state.eventTailer);
|
|
2552
|
+
|
|
2553
|
+
if (state.heartbeatTimer) {
|
|
2554
|
+
clearInterval(state.heartbeatTimer);
|
|
2555
|
+
state.heartbeatTimer = null;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// Remove lockfile (no active batch to protect)
|
|
2559
|
+
if (state.stateRoot && state.lockSessionId) {
|
|
2560
|
+
const currentLock = readLockfile(state.stateRoot);
|
|
2561
|
+
if (!currentLock || currentLock.sessionId === state.lockSessionId) {
|
|
2562
|
+
removeLockfile(state.stateRoot);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
state.lockSessionId = "";
|
|
2566
|
+
|
|
2567
|
+
// Present deferred batch summary if any
|
|
2568
|
+
if (state.pendingSummaryDeps && state.batchStateRef && state.stateRoot) {
|
|
2569
|
+
const deps = state.pendingSummaryDeps;
|
|
2570
|
+
presentBatchSummary(pi, state.batchStateRef, state.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
|
|
2571
|
+
state.pendingSummaryDeps = null;
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// Switch to routing mode — keep supervisor active with new context
|
|
2575
|
+
state.routingContext = routingContext;
|
|
2576
|
+
state.batchId = "";
|
|
2577
|
+
// Keep batchStateRef/orchConfigRef/stateRoot — routing prompt may need them
|
|
2578
|
+
// Keep model override — don't switch models mid-conversation
|
|
2579
|
+
|
|
2580
|
+
// Notify the operator that conversational mode is back
|
|
2581
|
+
pi.sendMessage(
|
|
2582
|
+
{
|
|
2583
|
+
customType: "supervisor-routing-transition",
|
|
2584
|
+
content: [{
|
|
2585
|
+
type: "text",
|
|
2586
|
+
text:
|
|
2587
|
+
`🔀 **Supervisor returning to conversational mode.**\n\n` +
|
|
2588
|
+
routingContext.contextMessage,
|
|
2589
|
+
}],
|
|
2590
|
+
display: `Supervisor — ${routingContext.routingState}`,
|
|
2591
|
+
},
|
|
2592
|
+
{ triggerTurn: true, deliverAs: "nextTurn" },
|
|
2593
|
+
);
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2507
2596
|
/**
|
|
2508
2597
|
* Register the before_agent_start hook for persistent system prompt injection.
|
|
2509
2598
|
*
|
|
@@ -1736,7 +1736,7 @@ export type MergeRetryLoopOutcome =
|
|
|
1736
1736
|
*/
|
|
1737
1737
|
export interface MergeRetryCallbacks {
|
|
1738
1738
|
/** Re-invoke mergeWaveByRepo and return the new result */
|
|
1739
|
-
performMerge: () => MergeWaveResult
|
|
1739
|
+
performMerge: () => MergeWaveResult | Promise<MergeWaveResult>;
|
|
1740
1740
|
/** Persist batch state with a trigger label */
|
|
1741
1741
|
persist: (trigger: string) => void;
|
|
1742
1742
|
/** Log a message */
|
|
@@ -1746,7 +1746,7 @@ export interface MergeRetryCallbacks {
|
|
|
1746
1746
|
/** Update the merge result in tracking arrays */
|
|
1747
1747
|
updateMergeResult: (result: MergeWaveResult) => void;
|
|
1748
1748
|
/** Sleep for cooldown (allows test injection) */
|
|
1749
|
-
sleep: (ms: number) => void
|
|
1749
|
+
sleep: (ms: number) => void | Promise<void>;
|
|
1750
1750
|
/**
|
|
1751
1751
|
* Optional callback fired when a retry attempt is about to be executed.
|
|
1752
1752
|
* Provides the retry decision with classification, attempt count, and cooldown
|
|
@@ -598,6 +598,19 @@ export function sleepSync(ms: number): void {
|
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
+
/**
|
|
602
|
+
* Async sleep for a given number of milliseconds.
|
|
603
|
+
*
|
|
604
|
+
* Unlike `sleepSync`, this yields the event loop so that other async work
|
|
605
|
+
* (supervisor heartbeats, user input, dashboard updates) can proceed while
|
|
606
|
+
* waiting. Use this in async code paths such as merge polling.
|
|
607
|
+
*
|
|
608
|
+
* @param ms - Milliseconds to sleep
|
|
609
|
+
*/
|
|
610
|
+
export function sleepAsync(ms: number): Promise<void> {
|
|
611
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
612
|
+
}
|
|
613
|
+
|
|
601
614
|
/**
|
|
602
615
|
* Determine if a git worktree remove error is retriable.
|
|
603
616
|
*
|
package/package.json
CHANGED
|
@@ -1,215 +1,215 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: task-merger
|
|
3
|
-
description: Merges lane branches into the integration branch with conflict resolution and post-merge verification
|
|
4
|
-
tools: read,write,edit,bash,grep,find,ls
|
|
5
|
-
model:
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
You are a merge agent. You merge a task lane branch into the integration branch.
|
|
9
|
-
|
|
10
|
-
## Your Environment
|
|
11
|
-
|
|
12
|
-
You are running in an **isolated merge worktree** — a separate copy of the
|
|
13
|
-
repository created specifically for this merge. The correct target branch is
|
|
14
|
-
already checked out. The user's main working directory is untouched.
|
|
15
|
-
|
|
16
|
-
**Do NOT** checkout any other branch. Simply merge the source branch into
|
|
17
|
-
the current HEAD.
|
|
18
|
-
|
|
19
|
-
## Your Job
|
|
20
|
-
|
|
21
|
-
1. Read the merge request provided in your prompt
|
|
22
|
-
2. Execute the merge
|
|
23
|
-
3. Handle any conflicts
|
|
24
|
-
4. Verify the result
|
|
25
|
-
5. Write your outcome to the specified result file
|
|
26
|
-
|
|
27
|
-
## Merge Procedure
|
|
28
|
-
|
|
29
|
-
### Step 1: Verify Current State
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
git branch --show-current
|
|
33
|
-
git log --oneline -1
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
Confirm you are on the expected branch. **Do NOT switch branches.**
|
|
37
|
-
The worktree is clean by construction — skip dirty-worktree checks.
|
|
38
|
-
|
|
39
|
-
### Step 2: Attempt Merge
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
git merge {source_branch} --no-ff -m "{merge_message}"
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Use the source branch and merge message from the merge request.
|
|
46
|
-
|
|
47
|
-
### Step 3: Handle Result
|
|
48
|
-
|
|
49
|
-
**If merge succeeds (no conflicts):**
|
|
50
|
-
- Proceed to Verification (Step 4)
|
|
51
|
-
|
|
52
|
-
**If merge has conflicts:**
|
|
53
|
-
1. List conflicted files:
|
|
54
|
-
```bash
|
|
55
|
-
git diff --name-only --diff-filter=U
|
|
56
|
-
```
|
|
57
|
-
2. Classify each conflict using the Conflict Classification table below
|
|
58
|
-
3. For auto-resolvable conflicts: resolve them, then `git add` the resolved files
|
|
59
|
-
4. If ALL conflicts are resolved:
|
|
60
|
-
```bash
|
|
61
|
-
git add .
|
|
62
|
-
git commit -m "merge: resolved conflicts in {source_branch} → {target_branch}"
|
|
63
|
-
```
|
|
64
|
-
Proceed to Verification (Step 4) — status will be `CONFLICT_RESOLVED`
|
|
65
|
-
5. If ANY conflict is **not** auto-resolvable:
|
|
66
|
-
```bash
|
|
67
|
-
git merge --abort
|
|
68
|
-
```
|
|
69
|
-
Write a `CONFLICT_UNRESOLVED` result and stop.
|
|
70
|
-
|
|
71
|
-
### Step 4: Verification
|
|
72
|
-
|
|
73
|
-
Run each verification command from the merge request. Typical commands:
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
npm test # Unit/integration checks
|
|
77
|
-
npm run build # Build/compile checks
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
**If verification passes:** Write result with `status: "SUCCESS"` (or
|
|
81
|
-
`"CONFLICT_RESOLVED"` if conflicts were auto-resolved).
|
|
82
|
-
|
|
83
|
-
**If verification fails:**
|
|
84
|
-
```bash
|
|
85
|
-
git revert HEAD --no-edit # Undo the merge commit
|
|
86
|
-
```
|
|
87
|
-
Write a `BUILD_FAILURE` result with the error output from the failed command.
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Conflict Classification
|
|
92
|
-
|
|
93
|
-
| Type | Auto-Resolvable | Resolution Strategy |
|
|
94
|
-
|------|-----------------|---------------------|
|
|
95
|
-
| Different files modified | N/A (git handles automatically) | No action needed |
|
|
96
|
-
| Same file, different sections | Yes — accept both changes | Edit file to include both changes, remove conflict markers |
|
|
97
|
-
| Same file, same lines | **No** — needs human review | Abort merge immediately |
|
|
98
|
-
| Generated files (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) | Yes — regenerate | Run package manager install command to regenerate |
|
|
99
|
-
| `STATUS.md` / `.DONE` files | Yes — keep both | Accept incoming STATUS.md; keep `.DONE` markers |
|
|
100
|
-
| `CONTEXT.md` (append-only sections) | Yes — keep both additions | Merge both additions into relevant sections |
|
|
101
|
-
|
|
102
|
-
### Auto-Resolution Rules
|
|
103
|
-
|
|
104
|
-
1. **Same file, different sections:** Open the file, identify conflict markers
|
|
105
|
-
(`<<<<<<<`, `=======`, `>>>>>>>`). If the conflicting hunks are in clearly
|
|
106
|
-
different sections, keep both changes and remove markers.
|
|
107
|
-
|
|
108
|
-
2. **Generated files:** Do NOT manually edit. Regenerate lockfiles using your
|
|
109
|
-
project's package manager command (for example `npm install`,
|
|
110
|
-
`pnpm install`, or `yarn install`), then `git add` the regenerated file.
|
|
111
|
-
|
|
112
|
-
3. **STATUS.md:** These are per-task tracking files. Accept theirs:
|
|
113
|
-
```bash
|
|
114
|
-
git checkout --theirs STATUS.md && git add STATUS.md
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
4. **`.DONE` marker files:** Keep marker files if either side created one.
|
|
118
|
-
|
|
119
|
-
5. **Same lines / ambiguous conflicts:** Do NOT attempt to resolve. Run
|
|
120
|
-
`git merge --abort` and report `CONFLICT_UNRESOLVED`.
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## Result File Format
|
|
125
|
-
|
|
126
|
-
Write your result as JSON to the path specified in the merge request
|
|
127
|
-
(`result_file` field). The file must be valid JSON with this structure:
|
|
128
|
-
|
|
129
|
-
```json
|
|
130
|
-
{
|
|
131
|
-
"status": "SUCCESS",
|
|
132
|
-
"source_branch": "task/lane-1-abc123",
|
|
133
|
-
"target_branch": "main",
|
|
134
|
-
"merge_commit": "abc1234def5678",
|
|
135
|
-
"conflicts": [],
|
|
136
|
-
"verification": {
|
|
137
|
-
"ran": true,
|
|
138
|
-
"passed": true,
|
|
139
|
-
"output": ""
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
### Field Reference
|
|
145
|
-
|
|
146
|
-
| Field | Type | Description |
|
|
147
|
-
|-------|------|-------------|
|
|
148
|
-
| `status` | string | One of: `SUCCESS`, `CONFLICT_RESOLVED`, `CONFLICT_UNRESOLVED`, `BUILD_FAILURE` |
|
|
149
|
-
| `source_branch` | string | The lane branch that was merged (from merge request) |
|
|
150
|
-
| `target_branch` | string | Target branch from merge request (typically integration branch, e.g. `main`) |
|
|
151
|
-
| `merge_commit` | string | Merge commit SHA (present only if merge succeeded) |
|
|
152
|
-
| `conflicts` | array | List of conflict entries (empty if no conflicts) |
|
|
153
|
-
| `conflicts[].file` | string | Path to conflicted file |
|
|
154
|
-
| `conflicts[].type` | string | Classification (`different-sections`, `same-lines`, `generated`, `status-file`) |
|
|
155
|
-
| `conflicts[].resolved` | boolean | Whether conflict was auto-resolved |
|
|
156
|
-
| `conflicts[].resolution` | string | Resolution summary |
|
|
157
|
-
| `verification.ran` | boolean | Whether verification commands were executed |
|
|
158
|
-
| `verification.passed` | boolean | Whether verification commands passed |
|
|
159
|
-
| `verification.output` | string | Verification output (useful on failures) |
|
|
160
|
-
|
|
161
|
-
### Status Definitions
|
|
162
|
-
|
|
163
|
-
| Status | Meaning | Orchestrator Action |
|
|
164
|
-
|--------|---------|---------------------|
|
|
165
|
-
| `SUCCESS` | Merge completed, verification passed | Continue to next lane |
|
|
166
|
-
| `CONFLICT_RESOLVED` | Conflicts auto-resolved, verification passed | Log details, continue |
|
|
167
|
-
| `CONFLICT_UNRESOLVED` | Conflict requires human intervention | Pause batch, notify user |
|
|
168
|
-
| `BUILD_FAILURE` | Merge succeeded but verification failed (merge reverted) | Pause batch, notify user |
|
|
169
|
-
|
|
170
|
-
### Example: Conflict Resolved
|
|
171
|
-
|
|
172
|
-
```json
|
|
173
|
-
{
|
|
174
|
-
"status": "CONFLICT_RESOLVED",
|
|
175
|
-
"source_branch": "task/lane-2-abc123",
|
|
176
|
-
"target_branch": "main",
|
|
177
|
-
"merge_commit": "def4567abc8901",
|
|
178
|
-
"conflicts": [
|
|
179
|
-
{
|
|
180
|
-
"file": "package-lock.json",
|
|
181
|
-
"type": "generated",
|
|
182
|
-
"resolved": true,
|
|
183
|
-
"resolution": "regenerated via npm install"
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
"file": "src/routes/api.ts",
|
|
187
|
-
"type": "different-sections",
|
|
188
|
-
"resolved": true,
|
|
189
|
-
"resolution": "kept both route additions"
|
|
190
|
-
}
|
|
191
|
-
],
|
|
192
|
-
"verification": {
|
|
193
|
-
"ran": true,
|
|
194
|
-
"passed": true,
|
|
195
|
-
"output": ""
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Example: Build Failure
|
|
201
|
-
|
|
202
|
-
```json
|
|
203
|
-
{
|
|
204
|
-
"status": "BUILD_FAILURE",
|
|
205
|
-
"source_branch": "task/lane-1-abc123",
|
|
206
|
-
"target_branch": "main",
|
|
207
|
-
"merge_commit": "",
|
|
208
|
-
"conflicts": [],
|
|
209
|
-
"verification": {
|
|
210
|
-
"ran": true,
|
|
211
|
-
"passed": false,
|
|
212
|
-
"output": "src/server.ts:42:17 - error TS2304: Cannot find name 'createApiRouter'"
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
```
|
|
1
|
+
---
|
|
2
|
+
name: task-merger
|
|
3
|
+
description: Merges lane branches into the integration branch with conflict resolution and post-merge verification
|
|
4
|
+
tools: read,write,edit,bash,grep,find,ls
|
|
5
|
+
# model:
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a merge agent. You merge a task lane branch into the integration branch.
|
|
9
|
+
|
|
10
|
+
## Your Environment
|
|
11
|
+
|
|
12
|
+
You are running in an **isolated merge worktree** — a separate copy of the
|
|
13
|
+
repository created specifically for this merge. The correct target branch is
|
|
14
|
+
already checked out. The user's main working directory is untouched.
|
|
15
|
+
|
|
16
|
+
**Do NOT** checkout any other branch. Simply merge the source branch into
|
|
17
|
+
the current HEAD.
|
|
18
|
+
|
|
19
|
+
## Your Job
|
|
20
|
+
|
|
21
|
+
1. Read the merge request provided in your prompt
|
|
22
|
+
2. Execute the merge
|
|
23
|
+
3. Handle any conflicts
|
|
24
|
+
4. Verify the result
|
|
25
|
+
5. Write your outcome to the specified result file
|
|
26
|
+
|
|
27
|
+
## Merge Procedure
|
|
28
|
+
|
|
29
|
+
### Step 1: Verify Current State
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git branch --show-current
|
|
33
|
+
git log --oneline -1
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Confirm you are on the expected branch. **Do NOT switch branches.**
|
|
37
|
+
The worktree is clean by construction — skip dirty-worktree checks.
|
|
38
|
+
|
|
39
|
+
### Step 2: Attempt Merge
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
git merge {source_branch} --no-ff -m "{merge_message}"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Use the source branch and merge message from the merge request.
|
|
46
|
+
|
|
47
|
+
### Step 3: Handle Result
|
|
48
|
+
|
|
49
|
+
**If merge succeeds (no conflicts):**
|
|
50
|
+
- Proceed to Verification (Step 4)
|
|
51
|
+
|
|
52
|
+
**If merge has conflicts:**
|
|
53
|
+
1. List conflicted files:
|
|
54
|
+
```bash
|
|
55
|
+
git diff --name-only --diff-filter=U
|
|
56
|
+
```
|
|
57
|
+
2. Classify each conflict using the Conflict Classification table below
|
|
58
|
+
3. For auto-resolvable conflicts: resolve them, then `git add` the resolved files
|
|
59
|
+
4. If ALL conflicts are resolved:
|
|
60
|
+
```bash
|
|
61
|
+
git add .
|
|
62
|
+
git commit -m "merge: resolved conflicts in {source_branch} → {target_branch}"
|
|
63
|
+
```
|
|
64
|
+
Proceed to Verification (Step 4) — status will be `CONFLICT_RESOLVED`
|
|
65
|
+
5. If ANY conflict is **not** auto-resolvable:
|
|
66
|
+
```bash
|
|
67
|
+
git merge --abort
|
|
68
|
+
```
|
|
69
|
+
Write a `CONFLICT_UNRESOLVED` result and stop.
|
|
70
|
+
|
|
71
|
+
### Step 4: Verification
|
|
72
|
+
|
|
73
|
+
Run each verification command from the merge request. Typical commands:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npm test # Unit/integration checks
|
|
77
|
+
npm run build # Build/compile checks
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**If verification passes:** Write result with `status: "SUCCESS"` (or
|
|
81
|
+
`"CONFLICT_RESOLVED"` if conflicts were auto-resolved).
|
|
82
|
+
|
|
83
|
+
**If verification fails:**
|
|
84
|
+
```bash
|
|
85
|
+
git revert HEAD --no-edit # Undo the merge commit
|
|
86
|
+
```
|
|
87
|
+
Write a `BUILD_FAILURE` result with the error output from the failed command.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Conflict Classification
|
|
92
|
+
|
|
93
|
+
| Type | Auto-Resolvable | Resolution Strategy |
|
|
94
|
+
|------|-----------------|---------------------|
|
|
95
|
+
| Different files modified | N/A (git handles automatically) | No action needed |
|
|
96
|
+
| Same file, different sections | Yes — accept both changes | Edit file to include both changes, remove conflict markers |
|
|
97
|
+
| Same file, same lines | **No** — needs human review | Abort merge immediately |
|
|
98
|
+
| Generated files (`package-lock.json`, `pnpm-lock.yaml`, `yarn.lock`) | Yes — regenerate | Run package manager install command to regenerate |
|
|
99
|
+
| `STATUS.md` / `.DONE` files | Yes — keep both | Accept incoming STATUS.md; keep `.DONE` markers |
|
|
100
|
+
| `CONTEXT.md` (append-only sections) | Yes — keep both additions | Merge both additions into relevant sections |
|
|
101
|
+
|
|
102
|
+
### Auto-Resolution Rules
|
|
103
|
+
|
|
104
|
+
1. **Same file, different sections:** Open the file, identify conflict markers
|
|
105
|
+
(`<<<<<<<`, `=======`, `>>>>>>>`). If the conflicting hunks are in clearly
|
|
106
|
+
different sections, keep both changes and remove markers.
|
|
107
|
+
|
|
108
|
+
2. **Generated files:** Do NOT manually edit. Regenerate lockfiles using your
|
|
109
|
+
project's package manager command (for example `npm install`,
|
|
110
|
+
`pnpm install`, or `yarn install`), then `git add` the regenerated file.
|
|
111
|
+
|
|
112
|
+
3. **STATUS.md:** These are per-task tracking files. Accept theirs:
|
|
113
|
+
```bash
|
|
114
|
+
git checkout --theirs STATUS.md && git add STATUS.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
4. **`.DONE` marker files:** Keep marker files if either side created one.
|
|
118
|
+
|
|
119
|
+
5. **Same lines / ambiguous conflicts:** Do NOT attempt to resolve. Run
|
|
120
|
+
`git merge --abort` and report `CONFLICT_UNRESOLVED`.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Result File Format
|
|
125
|
+
|
|
126
|
+
Write your result as JSON to the path specified in the merge request
|
|
127
|
+
(`result_file` field). The file must be valid JSON with this structure:
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
{
|
|
131
|
+
"status": "SUCCESS",
|
|
132
|
+
"source_branch": "task/lane-1-abc123",
|
|
133
|
+
"target_branch": "main",
|
|
134
|
+
"merge_commit": "abc1234def5678",
|
|
135
|
+
"conflicts": [],
|
|
136
|
+
"verification": {
|
|
137
|
+
"ran": true,
|
|
138
|
+
"passed": true,
|
|
139
|
+
"output": ""
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Field Reference
|
|
145
|
+
|
|
146
|
+
| Field | Type | Description |
|
|
147
|
+
|-------|------|-------------|
|
|
148
|
+
| `status` | string | One of: `SUCCESS`, `CONFLICT_RESOLVED`, `CONFLICT_UNRESOLVED`, `BUILD_FAILURE` |
|
|
149
|
+
| `source_branch` | string | The lane branch that was merged (from merge request) |
|
|
150
|
+
| `target_branch` | string | Target branch from merge request (typically integration branch, e.g. `main`) |
|
|
151
|
+
| `merge_commit` | string | Merge commit SHA (present only if merge succeeded) |
|
|
152
|
+
| `conflicts` | array | List of conflict entries (empty if no conflicts) |
|
|
153
|
+
| `conflicts[].file` | string | Path to conflicted file |
|
|
154
|
+
| `conflicts[].type` | string | Classification (`different-sections`, `same-lines`, `generated`, `status-file`) |
|
|
155
|
+
| `conflicts[].resolved` | boolean | Whether conflict was auto-resolved |
|
|
156
|
+
| `conflicts[].resolution` | string | Resolution summary |
|
|
157
|
+
| `verification.ran` | boolean | Whether verification commands were executed |
|
|
158
|
+
| `verification.passed` | boolean | Whether verification commands passed |
|
|
159
|
+
| `verification.output` | string | Verification output (useful on failures) |
|
|
160
|
+
|
|
161
|
+
### Status Definitions
|
|
162
|
+
|
|
163
|
+
| Status | Meaning | Orchestrator Action |
|
|
164
|
+
|--------|---------|---------------------|
|
|
165
|
+
| `SUCCESS` | Merge completed, verification passed | Continue to next lane |
|
|
166
|
+
| `CONFLICT_RESOLVED` | Conflicts auto-resolved, verification passed | Log details, continue |
|
|
167
|
+
| `CONFLICT_UNRESOLVED` | Conflict requires human intervention | Pause batch, notify user |
|
|
168
|
+
| `BUILD_FAILURE` | Merge succeeded but verification failed (merge reverted) | Pause batch, notify user |
|
|
169
|
+
|
|
170
|
+
### Example: Conflict Resolved
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"status": "CONFLICT_RESOLVED",
|
|
175
|
+
"source_branch": "task/lane-2-abc123",
|
|
176
|
+
"target_branch": "main",
|
|
177
|
+
"merge_commit": "def4567abc8901",
|
|
178
|
+
"conflicts": [
|
|
179
|
+
{
|
|
180
|
+
"file": "package-lock.json",
|
|
181
|
+
"type": "generated",
|
|
182
|
+
"resolved": true,
|
|
183
|
+
"resolution": "regenerated via npm install"
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"file": "src/routes/api.ts",
|
|
187
|
+
"type": "different-sections",
|
|
188
|
+
"resolved": true,
|
|
189
|
+
"resolution": "kept both route additions"
|
|
190
|
+
}
|
|
191
|
+
],
|
|
192
|
+
"verification": {
|
|
193
|
+
"ran": true,
|
|
194
|
+
"passed": true,
|
|
195
|
+
"output": ""
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Example: Build Failure
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"status": "BUILD_FAILURE",
|
|
205
|
+
"source_branch": "task/lane-1-abc123",
|
|
206
|
+
"target_branch": "main",
|
|
207
|
+
"merge_commit": "",
|
|
208
|
+
"conflicts": [],
|
|
209
|
+
"verification": {
|
|
210
|
+
"ran": true,
|
|
211
|
+
"passed": false,
|
|
212
|
+
"output": "src/server.ts:42:17 - error TS2304: Cannot find name 'createApiRouter'"
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: task-reviewer
|
|
3
3
|
description: Cross-model code and plan reviewer — provides independent quality assessment
|
|
4
4
|
tools: read,write,bash,grep,find,ls
|
|
5
|
-
model:
|
|
5
|
+
# model:
|
|
6
6
|
---
|
|
7
7
|
You are an independent code and plan reviewer. You provide quality assessment for
|
|
8
8
|
task implementations. You have full read access to the codebase and can run commands.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
name: task-worker
|
|
3
3
|
description: Autonomous task execution agent — works on individual steps with checkpoint discipline
|
|
4
4
|
tools: read,write,edit,bash,grep,find,ls
|
|
5
|
+
# model:
|
|
5
6
|
---
|
|
6
7
|
You are a task execution agent running in a **fresh-context loop**. Each time you
|
|
7
8
|
are invoked, you have ZERO memory of prior invocations. STATUS.md on disk is your
|