taskplane 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -21
- package/dashboard/public/app.js +10 -5
- package/extensions/taskplane/extension.ts +730 -548
- package/extensions/taskplane/persistence.ts +4 -2
- package/extensions/taskplane/supervisor.ts +34 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { Type } from "@mariozechner/pi-ai";
|
|
2
3
|
|
|
3
4
|
import { execSync, execFileSync } from "child_process";
|
|
4
5
|
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
@@ -1729,375 +1730,617 @@ export default function (pi: ExtensionAPI) {
|
|
|
1729
1730
|
},
|
|
1730
1731
|
});
|
|
1731
1732
|
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
// ── TP-040: Disk fallback for idle in-memory state ────────
|
|
1736
|
-
// When in-memory state is idle, try loading from persisted
|
|
1737
|
-
// batch-state.json. This covers fresh-session queries (pi
|
|
1738
|
-
// restarted while a batch was running in tmux lanes) and
|
|
1739
|
-
// post-crash recovery where in-memory state was lost.
|
|
1740
|
-
if (orchBatchState.phase === "idle") {
|
|
1741
|
-
const stateRoot = execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? ctx.cwd;
|
|
1742
|
-
let diskState: PersistedBatchState | null = null;
|
|
1743
|
-
try {
|
|
1744
|
-
diskState = loadBatchState(stateRoot);
|
|
1745
|
-
} catch {
|
|
1746
|
-
// Ignore errors — fall through to "no batch" message
|
|
1747
|
-
}
|
|
1733
|
+
// ── TP-053: Shared helpers for command + tool handlers ────────────
|
|
1734
|
+
// Each helper extracts the core logic from its command handler so both
|
|
1735
|
+
// the slash command and the registered tool can call the same function.
|
|
1748
1736
|
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
` Tasks: ${diskState.succeededTasks} succeeded, ${diskState.failedTasks} failed, ${diskState.skippedTasks} skipped, ${diskState.blockedTasks} blocked / ${diskState.totalTasks} total`,
|
|
1763
|
-
` Elapsed: ${elapsedSec}s`,
|
|
1764
|
-
];
|
|
1765
|
-
|
|
1766
|
-
if (diskState.errors.length > 0) {
|
|
1767
|
-
lines.push(` Errors: ${diskState.errors.length}`);
|
|
1768
|
-
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Core logic for orch-status. Returns a formatted status string.
|
|
1739
|
+
* Reads in-memory state first, falls back to disk if idle.
|
|
1740
|
+
*/
|
|
1741
|
+
function doOrchStatus(cwd: string): string {
|
|
1742
|
+
if (orchBatchState.phase === "idle") {
|
|
1743
|
+
const stateRoot = execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? cwd;
|
|
1744
|
+
let diskState: PersistedBatchState | null = null;
|
|
1745
|
+
try {
|
|
1746
|
+
diskState = loadBatchState(stateRoot);
|
|
1747
|
+
} catch {
|
|
1748
|
+
// Ignore errors — fall through to "no batch" message
|
|
1749
|
+
}
|
|
1769
1750
|
|
|
1770
|
-
|
|
1771
|
-
return;
|
|
1751
|
+
if (!diskState) {
|
|
1752
|
+
return "No batch is running. Use /orch <areas|paths|all> to start.";
|
|
1772
1753
|
}
|
|
1773
1754
|
|
|
1774
|
-
const elapsedSec =
|
|
1775
|
-
? Math.round((
|
|
1776
|
-
: Math.round((Date.now() -
|
|
1755
|
+
const elapsedSec = diskState.endedAt
|
|
1756
|
+
? Math.round((diskState.endedAt - diskState.startedAt) / 1000)
|
|
1757
|
+
: Math.round((Date.now() - diskState.startedAt) / 1000);
|
|
1777
1758
|
|
|
1778
1759
|
const lines: string[] = [
|
|
1779
|
-
`📊 Batch ${
|
|
1780
|
-
` Wave: ${
|
|
1781
|
-
` Tasks: ${
|
|
1760
|
+
`📊 Batch ${diskState.batchId} — ${diskState.phase} (from disk)`,
|
|
1761
|
+
` Wave: ${diskState.currentWaveIndex + 1}/${diskState.totalWaves}`,
|
|
1762
|
+
` Tasks: ${diskState.succeededTasks} succeeded, ${diskState.failedTasks} failed, ${diskState.skippedTasks} skipped, ${diskState.blockedTasks} blocked / ${diskState.totalTasks} total`,
|
|
1782
1763
|
` Elapsed: ${elapsedSec}s`,
|
|
1783
1764
|
];
|
|
1784
1765
|
|
|
1785
|
-
if (
|
|
1786
|
-
lines.push(` Errors: ${
|
|
1766
|
+
if (diskState.errors.length > 0) {
|
|
1767
|
+
lines.push(` Errors: ${diskState.errors.length}`);
|
|
1787
1768
|
}
|
|
1788
1769
|
|
|
1789
|
-
|
|
1790
|
-
}
|
|
1791
|
-
});
|
|
1770
|
+
return lines.join("\n");
|
|
1771
|
+
}
|
|
1792
1772
|
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (orchBatchState.phase === "idle" || orchBatchState.phase === "completed" || orchBatchState.phase === "failed" || orchBatchState.phase === "stopped") {
|
|
1797
|
-
ctx.ui.notify(ORCH_MESSAGES.pauseNoBatch(), "warning");
|
|
1798
|
-
return;
|
|
1799
|
-
}
|
|
1800
|
-
if (orchBatchState.phase === "paused" || orchBatchState.pauseSignal.paused) {
|
|
1801
|
-
ctx.ui.notify(ORCH_MESSAGES.pauseAlreadyPaused(orchBatchState.batchId), "warning");
|
|
1802
|
-
return;
|
|
1803
|
-
}
|
|
1804
|
-
// Set pause signal — executeLane() checks this between tasks
|
|
1805
|
-
orchBatchState.pauseSignal.paused = true;
|
|
1806
|
-
ctx.ui.notify(ORCH_MESSAGES.pauseActivated(orchBatchState.batchId), "info");
|
|
1807
|
-
updateOrchWidget();
|
|
1808
|
-
},
|
|
1809
|
-
});
|
|
1773
|
+
const elapsedSec = orchBatchState.endedAt
|
|
1774
|
+
? Math.round((orchBatchState.endedAt - orchBatchState.startedAt) / 1000)
|
|
1775
|
+
: Math.round((Date.now() - orchBatchState.startedAt) / 1000);
|
|
1810
1776
|
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1777
|
+
const lines: string[] = [
|
|
1778
|
+
`📊 Batch ${orchBatchState.batchId} — ${orchBatchState.phase}`,
|
|
1779
|
+
` Wave: ${orchBatchState.currentWaveIndex + 1}/${orchBatchState.totalWaves}`,
|
|
1780
|
+
` Tasks: ${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ${orchBatchState.skippedTasks} skipped, ${orchBatchState.blockedTasks} blocked / ${orchBatchState.totalTasks} total`,
|
|
1781
|
+
` Elapsed: ${elapsedSec}s`,
|
|
1782
|
+
];
|
|
1815
1783
|
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
ctx.ui.notify(`❌ ${parsed.error}`, "error");
|
|
1820
|
-
return;
|
|
1821
|
-
}
|
|
1784
|
+
if (orchBatchState.errors.length > 0) {
|
|
1785
|
+
lines.push(` Errors: ${orchBatchState.errors.length}`);
|
|
1786
|
+
}
|
|
1822
1787
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
ctx.ui.notify(
|
|
1826
|
-
`⚠️ A batch is currently ${orchBatchState.phase} (${orchBatchState.batchId}). Cannot resume.`,
|
|
1827
|
-
"warning",
|
|
1828
|
-
);
|
|
1829
|
-
return;
|
|
1830
|
-
}
|
|
1788
|
+
return lines.join("\n");
|
|
1789
|
+
}
|
|
1831
1790
|
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1791
|
+
/**
|
|
1792
|
+
* Core logic for orch-pause. Returns a status message string.
|
|
1793
|
+
*/
|
|
1794
|
+
function doOrchPause(): string {
|
|
1795
|
+
if (orchBatchState.phase === "idle" || orchBatchState.phase === "completed" || orchBatchState.phase === "failed" || orchBatchState.phase === "stopped") {
|
|
1796
|
+
return ORCH_MESSAGES.pauseNoBatch();
|
|
1797
|
+
}
|
|
1798
|
+
if (orchBatchState.phase === "paused" || orchBatchState.pauseSignal.paused) {
|
|
1799
|
+
return ORCH_MESSAGES.pauseAlreadyPaused(orchBatchState.batchId);
|
|
1800
|
+
}
|
|
1801
|
+
orchBatchState.pauseSignal.paused = true;
|
|
1802
|
+
updateOrchWidget();
|
|
1803
|
+
return ORCH_MESSAGES.pauseActivated(orchBatchState.batchId);
|
|
1804
|
+
}
|
|
1835
1805
|
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1806
|
+
/**
|
|
1807
|
+
* Core logic for orch-resume. Returns an immediate status message.
|
|
1808
|
+
* The actual batch resume runs asynchronously via startBatchAsync.
|
|
1809
|
+
* Returns null if execCtx is missing (caller must handle).
|
|
1810
|
+
*/
|
|
1811
|
+
function doOrchResume(force: boolean, ctx: ExtensionContext): { message: string; error?: boolean } {
|
|
1812
|
+
if (!execCtx) {
|
|
1813
|
+
return {
|
|
1814
|
+
message: "❌ Orchestrator not initialized. Workspace configuration failed at startup.\nFix the workspace config or remove it to use repo mode, then restart.",
|
|
1815
|
+
error: true,
|
|
1816
|
+
};
|
|
1817
|
+
}
|
|
1842
1818
|
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
()
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
orchBatchState,
|
|
1851
|
-
(message, level) => {
|
|
1852
|
-
ctx.ui.notify(message, level);
|
|
1853
|
-
updateOrchWidget();
|
|
1854
|
-
},
|
|
1855
|
-
(monState: MonitorState) => {
|
|
1856
|
-
latestMonitorState = monState;
|
|
1857
|
-
updateOrchWidget();
|
|
1858
|
-
},
|
|
1859
|
-
execCtx!.workspaceConfig,
|
|
1860
|
-
execCtx!.workspaceRoot,
|
|
1861
|
-
execCtx!.pointer?.agentRoot,
|
|
1862
|
-
parsed.force,
|
|
1863
|
-
),
|
|
1864
|
-
orchBatchState,
|
|
1865
|
-
ctx,
|
|
1866
|
-
updateOrchWidget,
|
|
1867
|
-
// TP-043: Deferred supervisor deactivation (R002-1, parity with /orch).
|
|
1868
|
-
// Only trigger integration on completed batches.
|
|
1869
|
-
// TP-043 Step 2: Batch summary on all terminal paths.
|
|
1870
|
-
() => {
|
|
1871
|
-
const mode = orchConfig.orchestrator.integration;
|
|
1872
|
-
const opId = resolveOperatorId(orchConfig);
|
|
1873
|
-
const sDeps: SummaryDeps = {
|
|
1874
|
-
opId,
|
|
1875
|
-
diagnostics: orchBatchState.diagnostics ?? null,
|
|
1876
|
-
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
1877
|
-
waveIndex: mr.waveIndex,
|
|
1878
|
-
status: mr.status,
|
|
1879
|
-
failedLane: mr.failedLane,
|
|
1880
|
-
failureReason: mr.failureReason,
|
|
1881
|
-
})),
|
|
1882
|
-
};
|
|
1883
|
-
if (
|
|
1884
|
-
orchBatchState.phase === "completed" &&
|
|
1885
|
-
(mode === "supervised" || mode === "auto")
|
|
1886
|
-
) {
|
|
1887
|
-
triggerSupervisorIntegration(
|
|
1888
|
-
pi,
|
|
1889
|
-
supervisorState,
|
|
1890
|
-
orchBatchState,
|
|
1891
|
-
mode,
|
|
1892
|
-
execCtx!.repoRoot,
|
|
1893
|
-
buildIntegrationExecutor(execCtx!.repoRoot, opId),
|
|
1894
|
-
buildCiDeps(execCtx!.repoRoot),
|
|
1895
|
-
sDeps,
|
|
1896
|
-
);
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
if (
|
|
1900
|
-
(mode === "supervised" || mode === "auto") &&
|
|
1901
|
-
orchBatchState.phase !== "completed"
|
|
1902
|
-
) {
|
|
1903
|
-
pi.sendMessage(
|
|
1904
|
-
{
|
|
1905
|
-
customType: "supervisor-integration-skipped",
|
|
1906
|
-
content: [{
|
|
1907
|
-
type: "text",
|
|
1908
|
-
text:
|
|
1909
|
-
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
1910
|
-
`Integration skipped — only completed batches are eligible.\n` +
|
|
1911
|
-
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
1912
|
-
}],
|
|
1913
|
-
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
1914
|
-
},
|
|
1915
|
-
{ triggerTurn: false },
|
|
1916
|
-
);
|
|
1917
|
-
}
|
|
1918
|
-
// TP-043: Generate summary before transition
|
|
1919
|
-
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1920
|
-
// TP-128: Transition to routing mode (same as /orch onTerminal)
|
|
1921
|
-
const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
|
|
1922
|
-
? {
|
|
1923
|
-
routingState: "completed-batch",
|
|
1924
|
-
contextMessage:
|
|
1925
|
-
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
1926
|
-
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1927
|
-
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1928
|
-
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
1929
|
-
`You can also:\n` +
|
|
1930
|
-
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
1931
|
-
`• Create new tasks for the next batch\n` +
|
|
1932
|
-
`• Run a health check`,
|
|
1933
|
-
}
|
|
1934
|
-
: {
|
|
1935
|
-
routingState: "no-tasks",
|
|
1936
|
-
contextMessage:
|
|
1937
|
-
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1938
|
-
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
1939
|
-
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
1940
|
-
`What would you like to do next?`,
|
|
1941
|
-
};
|
|
1942
|
-
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
1943
|
-
},
|
|
1944
|
-
);
|
|
1819
|
+
// Prevent resume if a batch is actively running
|
|
1820
|
+
if (orchBatchState.phase === "launching" || orchBatchState.phase === "executing" || orchBatchState.phase === "merging" || orchBatchState.phase === "planning") {
|
|
1821
|
+
return {
|
|
1822
|
+
message: `⚠️ A batch is currently ${orchBatchState.phase} (${orchBatchState.batchId}). Cannot resume.`,
|
|
1823
|
+
error: true,
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1945
1826
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1827
|
+
// Reset batch state for resume
|
|
1828
|
+
orchBatchState = freshOrchBatchState();
|
|
1829
|
+
latestMonitorState = null;
|
|
1830
|
+
|
|
1831
|
+
orchBatchState.phase = "launching";
|
|
1832
|
+
orchBatchState.startedAt = Date.now();
|
|
1833
|
+
updateOrchWidget();
|
|
1834
|
+
|
|
1835
|
+
// Fire-and-forget resume via startBatchAsync
|
|
1836
|
+
startBatchAsync(
|
|
1837
|
+
() => resumeOrchBatch(
|
|
1953
1838
|
orchConfig,
|
|
1954
|
-
|
|
1839
|
+
runnerConfig,
|
|
1840
|
+
execCtx!.repoRoot,
|
|
1841
|
+
orchBatchState,
|
|
1842
|
+
(message, level) => {
|
|
1843
|
+
ctx.ui.notify(message, level);
|
|
1844
|
+
updateOrchWidget();
|
|
1845
|
+
},
|
|
1846
|
+
(monState: MonitorState) => {
|
|
1847
|
+
latestMonitorState = monState;
|
|
1848
|
+
updateOrchWidget();
|
|
1849
|
+
},
|
|
1850
|
+
execCtx!.workspaceConfig,
|
|
1955
1851
|
execCtx!.workspaceRoot,
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1852
|
+
execCtx!.pointer?.agentRoot,
|
|
1853
|
+
force,
|
|
1854
|
+
),
|
|
1855
|
+
orchBatchState,
|
|
1856
|
+
ctx,
|
|
1857
|
+
updateOrchWidget,
|
|
1858
|
+
() => {
|
|
1859
|
+
const mode = orchConfig.orchestrator.integration;
|
|
1860
|
+
const opId = resolveOperatorId(orchConfig);
|
|
1861
|
+
const sDeps: SummaryDeps = {
|
|
1862
|
+
opId,
|
|
1863
|
+
diagnostics: orchBatchState.diagnostics ?? null,
|
|
1864
|
+
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
1865
|
+
waveIndex: mr.waveIndex,
|
|
1866
|
+
status: mr.status,
|
|
1867
|
+
failedLane: mr.failedLane,
|
|
1868
|
+
failureReason: mr.failureReason,
|
|
1869
|
+
})),
|
|
1870
|
+
};
|
|
1871
|
+
if (
|
|
1872
|
+
orchBatchState.phase === "completed" &&
|
|
1873
|
+
(mode === "supervised" || mode === "auto")
|
|
1874
|
+
) {
|
|
1875
|
+
triggerSupervisorIntegration(
|
|
1876
|
+
pi,
|
|
1877
|
+
supervisorState,
|
|
1878
|
+
orchBatchState,
|
|
1879
|
+
mode,
|
|
1880
|
+
execCtx!.repoRoot,
|
|
1881
|
+
buildIntegrationExecutor(execCtx!.repoRoot, opId),
|
|
1882
|
+
buildCiDeps(execCtx!.repoRoot),
|
|
1883
|
+
sDeps,
|
|
1884
|
+
);
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1887
|
+
if (
|
|
1888
|
+
(mode === "supervised" || mode === "auto") &&
|
|
1889
|
+
orchBatchState.phase !== "completed"
|
|
1890
|
+
) {
|
|
1891
|
+
pi.sendMessage(
|
|
1892
|
+
{
|
|
1893
|
+
customType: "supervisor-integration-skipped",
|
|
1894
|
+
content: [{
|
|
1895
|
+
type: "text",
|
|
1896
|
+
text:
|
|
1897
|
+
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
1898
|
+
`Integration skipped — only completed batches are eligible.\n` +
|
|
1899
|
+
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
1900
|
+
}],
|
|
1901
|
+
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
1902
|
+
},
|
|
1903
|
+
{ triggerTurn: false },
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1907
|
+
const postBatchContext: SupervisorRoutingContext = orchBatchState.phase === "completed"
|
|
1908
|
+
? {
|
|
1909
|
+
routingState: "completed-batch",
|
|
1910
|
+
contextMessage:
|
|
1911
|
+
`Batch **${orchBatchState.batchId}** completed — ` +
|
|
1912
|
+
`${orchBatchState.succeededTasks}/${orchBatchState.totalTasks} tasks succeeded.\n\n` +
|
|
1913
|
+
`The orch branch \`${orchBatchState.orchBranch}\` is ready to integrate.\n` +
|
|
1914
|
+
`Would you like me to integrate it, or would you prefer to review first?\n\n` +
|
|
1915
|
+
`You can also:\n` +
|
|
1916
|
+
`• Run \`/orch-integrate\` (or \`/orch-integrate --pr\`) to integrate\n` +
|
|
1917
|
+
`• Create new tasks for the next batch\n` +
|
|
1918
|
+
`• Run a health check`,
|
|
1919
|
+
}
|
|
1920
|
+
: {
|
|
1921
|
+
routingState: "no-tasks",
|
|
1922
|
+
contextMessage:
|
|
1923
|
+
`Batch **${orchBatchState.batchId}** ended (${orchBatchState.phase}).\n\n` +
|
|
1924
|
+
`${orchBatchState.succeededTasks} succeeded, ${orchBatchState.failedTasks} failed, ` +
|
|
1925
|
+
`${orchBatchState.skippedTasks} skipped.\n\n` +
|
|
1926
|
+
`What would you like to do next?`,
|
|
1927
|
+
};
|
|
1928
|
+
transitionToRoutingMode(pi, supervisorState, postBatchContext);
|
|
1929
|
+
},
|
|
1930
|
+
);
|
|
1960
1931
|
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
// Fall back to ctx.cwd if no execution context is available.
|
|
1972
|
-
// Uses repoRoot for consistency with engine/resume/execution
|
|
1973
|
-
// which all persist state and poll abort signals from repoRoot.
|
|
1974
|
-
const stateRoot = execCtx?.repoRoot ?? ctx.cwd;
|
|
1932
|
+
// Activate supervisor agent on resume
|
|
1933
|
+
activateSupervisor(
|
|
1934
|
+
pi,
|
|
1935
|
+
supervisorState,
|
|
1936
|
+
orchBatchState,
|
|
1937
|
+
orchConfig,
|
|
1938
|
+
supervisorConfig,
|
|
1939
|
+
execCtx!.workspaceRoot,
|
|
1940
|
+
ctx,
|
|
1941
|
+
);
|
|
1975
1942
|
|
|
1976
|
-
|
|
1943
|
+
return { message: `🔄 Resume initiated for batch. Phase: launching.` };
|
|
1944
|
+
}
|
|
1977
1945
|
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
mkdirSync(join(stateRoot, ".pi"), { recursive: true });
|
|
1986
|
-
writeFileSync(abortSignalFile, `abort requested at ${new Date().toISOString()} (mode: ${mode})`, "utf-8");
|
|
1987
|
-
ctx.ui.notify(" ✓ Abort signal file written (.pi/orch-abort-signal)", "info");
|
|
1988
|
-
} catch (err) {
|
|
1989
|
-
ctx.ui.notify(` ⚠ Failed to write abort signal file: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
1990
|
-
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Core logic for orch-abort. Returns accumulated status messages.
|
|
1948
|
+
* Works even without execCtx (safety-critical).
|
|
1949
|
+
*/
|
|
1950
|
+
function doOrchAbort(hard: boolean, ctx: ExtensionContext): string {
|
|
1951
|
+
const mode: AbortMode = hard ? "hard" : "graceful";
|
|
1952
|
+
const prefix = orchConfig.orchestrator.tmux_prefix;
|
|
1991
1953
|
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1954
|
+
const stateRoot = execCtx?.repoRoot ?? ctx.cwd;
|
|
1955
|
+
const messages: string[] = [`🛑 Abort requested (${mode} mode, prefix: ${prefix})...`];
|
|
1956
|
+
|
|
1957
|
+
// Step 1: Write abort signal file
|
|
1958
|
+
const abortSignalFile = join(stateRoot, ".pi", "orch-abort-signal");
|
|
1959
|
+
try {
|
|
1960
|
+
mkdirSync(join(stateRoot, ".pi"), { recursive: true });
|
|
1961
|
+
writeFileSync(abortSignalFile, `abort requested at ${new Date().toISOString()} (mode: ${mode})`, "utf-8");
|
|
1962
|
+
messages.push(" ✓ Abort signal file written (.pi/orch-abort-signal)");
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
messages.push(` ⚠ Failed to write abort signal file: ${err instanceof Error ? err.message : String(err)}`);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
// Step 2: Set pause signal
|
|
1968
|
+
if (orchBatchState.pauseSignal) {
|
|
1969
|
+
orchBatchState.pauseSignal.paused = true;
|
|
1970
|
+
messages.push(" ✓ Pause signal set on in-memory batch state");
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
// Step 3: Check what we're aborting
|
|
1974
|
+
const hasActiveBatch = orchBatchState.phase !== "idle" &&
|
|
1975
|
+
orchBatchState.phase !== "completed" &&
|
|
1976
|
+
orchBatchState.phase !== "failed" &&
|
|
1977
|
+
orchBatchState.phase !== "stopped";
|
|
1978
|
+
|
|
1979
|
+
let persistedState: PersistedBatchState | null = null;
|
|
1980
|
+
try {
|
|
1981
|
+
persistedState = loadBatchState(stateRoot);
|
|
1982
|
+
} catch {
|
|
1983
|
+
// Ignore
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
messages.push(
|
|
1987
|
+
` Batch state: in-memory=${hasActiveBatch ? orchBatchState.phase : "none"}, ` +
|
|
1988
|
+
`persisted=${persistedState ? persistedState.batchId : "none"}`,
|
|
1989
|
+
);
|
|
1990
|
+
|
|
1991
|
+
// If no batch AND no sessions, nothing to abort
|
|
1992
|
+
if (!hasActiveBatch && !persistedState) {
|
|
1993
|
+
// Still check for sessions below, but short-circuit if none
|
|
1994
|
+
let allSessionNames: string[] = [];
|
|
1995
|
+
try {
|
|
1996
|
+
const tmuxOutput = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
1997
|
+
encoding: "utf-8",
|
|
1998
|
+
timeout: 5000,
|
|
1999
|
+
}).trim();
|
|
2000
|
+
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
2001
|
+
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
2002
|
+
} catch {
|
|
2003
|
+
// tmux not available
|
|
2004
|
+
}
|
|
2005
|
+
if (allSessionNames.length === 0) {
|
|
2006
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
2007
|
+
return ORCH_MESSAGES.abortNoBatch();
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
1999
2010
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2011
|
+
const batchId = orchBatchState.batchId || persistedState?.batchId || "unknown";
|
|
2012
|
+
|
|
2013
|
+
// Step 5: Kill sessions
|
|
2014
|
+
let allSessionNames: string[] = [];
|
|
2015
|
+
try {
|
|
2016
|
+
const tmuxOutput = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
2017
|
+
encoding: "utf-8",
|
|
2018
|
+
timeout: 5000,
|
|
2019
|
+
}).trim();
|
|
2020
|
+
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
2021
|
+
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
2022
|
+
messages.push(` Found ${allSessionNames.length} session(s) matching prefix "${prefix}-"`);
|
|
2023
|
+
} catch {
|
|
2024
|
+
messages.push(" ⚠ Could not list tmux sessions (tmux not available?)");
|
|
2025
|
+
}
|
|
2005
2026
|
|
|
2006
|
-
|
|
2027
|
+
if (allSessionNames.length > 0) {
|
|
2028
|
+
messages.push(` Killing ${allSessionNames.length} tmux session(s)...`);
|
|
2029
|
+
let killed = 0;
|
|
2030
|
+
for (const name of allSessionNames) {
|
|
2031
|
+
try {
|
|
2032
|
+
execSync(`tmux kill-session -t "${name}-worker" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2033
|
+
} catch {}
|
|
2007
2034
|
try {
|
|
2008
|
-
|
|
2035
|
+
execSync(`tmux kill-session -t "${name}-reviewer" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2036
|
+
} catch {}
|
|
2037
|
+
try {
|
|
2038
|
+
execSync(`tmux kill-session -t "${name}" 2>/dev/null`, { timeout: 3000 }).toString();
|
|
2039
|
+
killed++;
|
|
2040
|
+
messages.push(` ✓ Killed: ${name}`);
|
|
2009
2041
|
} catch {
|
|
2010
|
-
|
|
2042
|
+
messages.push(` · ${name} (already exited)`);
|
|
2043
|
+
killed++;
|
|
2011
2044
|
}
|
|
2045
|
+
}
|
|
2046
|
+
messages.push(` ✓ ${killed}/${allSessionNames.length} session(s) terminated`);
|
|
2047
|
+
} else {
|
|
2048
|
+
messages.push(" No tmux sessions to kill");
|
|
2049
|
+
}
|
|
2012
2050
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2051
|
+
// Step 6: Clean up batch state
|
|
2052
|
+
deactivateSupervisor(pi, supervisorState);
|
|
2053
|
+
|
|
2054
|
+
try {
|
|
2055
|
+
orchBatchState.phase = "stopped";
|
|
2056
|
+
orchBatchState.endedAt = Date.now();
|
|
2057
|
+
updateOrchWidget();
|
|
2058
|
+
messages.push(" ✓ In-memory batch state set to 'stopped'");
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
messages.push(` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
try {
|
|
2064
|
+
deleteBatchState(stateRoot);
|
|
2065
|
+
messages.push(" ✓ Batch state file deleted (.pi/batch-state.json)");
|
|
2066
|
+
} catch (err) {
|
|
2067
|
+
messages.push(` ⚠ Failed to delete batch state file: ${err instanceof Error ? err.message : String(err)}`);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
// Step 7: Clean up abort signal file
|
|
2071
|
+
try { unlinkSync(abortSignalFile); } catch {}
|
|
2072
|
+
|
|
2073
|
+
messages.push(
|
|
2074
|
+
`✅ Abort complete for batch ${batchId}. Sessions killed, state cleaned up.\n` +
|
|
2075
|
+
` Worktrees and branches are preserved for inspection.`,
|
|
2076
|
+
);
|
|
2077
|
+
|
|
2078
|
+
return messages.join("\n");
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
/**
|
|
2082
|
+
* Core logic for orch-integrate. Returns a result message string.
|
|
2083
|
+
* On error, returns an object with error flag.
|
|
2084
|
+
*/
|
|
2085
|
+
async function doOrchIntegrate(
|
|
2086
|
+
args: string | undefined,
|
|
2087
|
+
ctx: ExtensionContext,
|
|
2088
|
+
): Promise<{ message: string; error?: boolean; level?: "info" | "warning" | "error" }> {
|
|
2089
|
+
if (!execCtx) {
|
|
2090
|
+
return {
|
|
2091
|
+
message: "❌ Orchestrator not initialized. Workspace configuration failed at startup.\nFix the workspace config or remove it to use repo mode, then restart.",
|
|
2092
|
+
error: true,
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// Parse arguments
|
|
2097
|
+
const parsed = parseIntegrateArgs(args);
|
|
2098
|
+
if ("error" in parsed) {
|
|
2099
|
+
return { message: `❌ ${parsed.error}\n\nRun /orch-integrate --help for usage.`, error: true };
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Resolve integration context
|
|
2103
|
+
const { repoRoot } = execCtx!;
|
|
2104
|
+
const resolution = resolveIntegrationContext(parsed, {
|
|
2105
|
+
loadBatchState: () => loadBatchState(repoRoot),
|
|
2106
|
+
getCurrentBranch: () => getCurrentBranch(repoRoot),
|
|
2107
|
+
listOrchBranches: () => {
|
|
2108
|
+
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
2109
|
+
return result.ok
|
|
2110
|
+
? result.stdout.split("\n").map(b => b.replace(/^\*?\s+/, "").trim()).filter(Boolean)
|
|
2111
|
+
: [];
|
|
2112
|
+
},
|
|
2113
|
+
orchBranchExists: (branch: string) => {
|
|
2114
|
+
return runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot).ok;
|
|
2115
|
+
},
|
|
2116
|
+
});
|
|
2117
|
+
|
|
2118
|
+
if ("error" in resolution) {
|
|
2119
|
+
const severity = (resolution as IntegrationContextError).severity;
|
|
2120
|
+
return { message: resolution.error, error: severity !== "info" };
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const { orchBranch, baseBranch, batchId, currentBranch, notices } = resolution as IntegrationContext;
|
|
2124
|
+
const outputLines: string[] = [];
|
|
2125
|
+
let hasWarning = false;
|
|
2126
|
+
|
|
2127
|
+
for (const notice of notices) {
|
|
2128
|
+
outputLines.push(notice);
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
// Branch protection pre-check (TP-052)
|
|
2132
|
+
if (parsed.mode !== "pr") {
|
|
2133
|
+
const { detectBranchProtection } = await import("./supervisor.ts");
|
|
2134
|
+
const protectionStatus = detectBranchProtection(baseBranch, repoRoot);
|
|
2135
|
+
if (protectionStatus === "protected") {
|
|
2136
|
+
hasWarning = true;
|
|
2137
|
+
outputLines.push(
|
|
2138
|
+
`⚠️ Branch \`${baseBranch}\` has branch protection rules enabled.\n` +
|
|
2139
|
+
`Direct merges may be blocked by your repository settings.\n\n` +
|
|
2140
|
+
`Recommended: use \`/orch-integrate --pr\` to create a pull request instead.`,
|
|
2017
2141
|
);
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2018
2144
|
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
}).trim();
|
|
2026
|
-
const all = tmuxOutput ? tmuxOutput.split("\n").map(s => s.trim()).filter(Boolean) : [];
|
|
2027
|
-
allSessionNames = all.filter(name => name.startsWith(`${prefix}-`));
|
|
2028
|
-
ctx.ui.notify(` Found ${allSessionNames.length} session(s) matching prefix "${prefix}-": ${allSessionNames.join(", ") || "(none)"}`, "info");
|
|
2029
|
-
} catch {
|
|
2030
|
-
ctx.ui.notify(" ⚠ Could not list tmux sessions (tmux not available?)", "warning");
|
|
2031
|
-
}
|
|
2145
|
+
// Pre-integration summary
|
|
2146
|
+
const revListResult = runGit(
|
|
2147
|
+
["rev-list", "--count", `${currentBranch}..${orchBranch}`],
|
|
2148
|
+
repoRoot,
|
|
2149
|
+
);
|
|
2150
|
+
const commitsAhead = revListResult.ok ? revListResult.stdout.trim() : "?";
|
|
2032
2151
|
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2152
|
+
const diffStatResult = runGit(
|
|
2153
|
+
["diff", "--stat", `${currentBranch}...${orchBranch}`],
|
|
2154
|
+
repoRoot,
|
|
2155
|
+
);
|
|
2156
|
+
const diffSummary = diffStatResult.ok ? diffStatResult.stdout.trim() : "(unable to compute diff)";
|
|
2157
|
+
|
|
2158
|
+
outputLines.push(
|
|
2159
|
+
`🔀 Integration Summary\n` +
|
|
2160
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2161
|
+
` Orch branch: ${orchBranch}\n` +
|
|
2162
|
+
` Target: ${currentBranch}\n` +
|
|
2163
|
+
` Commits: ${commitsAhead} ahead\n` +
|
|
2164
|
+
` Mode: ${parsed.mode === "ff" ? "fast-forward" : parsed.mode === "merge" ? "merge commit" : "pull request"}\n` +
|
|
2165
|
+
(batchId ? ` Batch: ${batchId}\n` : "") +
|
|
2166
|
+
(parsed.force ? ` ⚠ Force: branch safety check skipped\n` : "") +
|
|
2167
|
+
`\n` +
|
|
2168
|
+
(diffSummary ? `${diffSummary}\n` : "") +
|
|
2169
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
2170
|
+
);
|
|
2171
|
+
|
|
2172
|
+
// Execute integration
|
|
2173
|
+
const resolvedOrchBranch = (resolution as IntegrationContext).orchBranch;
|
|
2174
|
+
const wsConfig = execCtx!.workspaceConfig;
|
|
2175
|
+
const reposToIntegrate: { id: string; root: string }[] = [];
|
|
2176
|
+
|
|
2177
|
+
if (wsConfig) {
|
|
2178
|
+
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
2179
|
+
const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${resolvedOrchBranch}`], repoConf.path);
|
|
2180
|
+
if (branchCheck.ok) {
|
|
2181
|
+
reposToIntegrate.push({ id: repoId, root: repoConf.path });
|
|
2039
2182
|
}
|
|
2183
|
+
}
|
|
2184
|
+
} else {
|
|
2185
|
+
reposToIntegrate.push({ id: "(default)", root: repoRoot });
|
|
2186
|
+
}
|
|
2040
2187
|
|
|
2041
|
-
|
|
2188
|
+
let totalCommits = 0;
|
|
2189
|
+
let allSucceeded = true;
|
|
2190
|
+
const repoMessages: string[] = [];
|
|
2042
2191
|
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
}
|
|
2192
|
+
for (const repo of reposToIntegrate) {
|
|
2193
|
+
const preCountResult = runGit(["rev-list", "--count", `HEAD..${resolvedOrchBranch}`], repo.root);
|
|
2194
|
+
const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
|
|
2195
|
+
|
|
2196
|
+
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
2197
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
|
|
2198
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
2199
|
+
try {
|
|
2200
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
2201
|
+
encoding: "utf-8",
|
|
2202
|
+
timeout: 60_000,
|
|
2203
|
+
cwd: repo.root,
|
|
2204
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2205
|
+
}).trim();
|
|
2206
|
+
return { ok: true, stdout, stderr: "" };
|
|
2207
|
+
} catch (err: unknown) {
|
|
2208
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
2209
|
+
return {
|
|
2210
|
+
ok: false,
|
|
2211
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
2212
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
2213
|
+
};
|
|
2066
2214
|
}
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
}
|
|
2215
|
+
},
|
|
2216
|
+
deleteBatchState: () => { /* handled once after all repos */ },
|
|
2217
|
+
});
|
|
2071
2218
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2219
|
+
if (!integrationResult.success) {
|
|
2220
|
+
return { message: `❌ Integration failed in ${repo.id}:\n${integrationResult.error}`, error: true };
|
|
2221
|
+
}
|
|
2075
2222
|
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
updateOrchWidget();
|
|
2080
|
-
ctx.ui.notify(" ✓ In-memory batch state set to 'stopped'", "info");
|
|
2081
|
-
} catch (err) {
|
|
2082
|
-
ctx.ui.notify(` ⚠ Failed to update in-memory state: ${err instanceof Error ? err.message : String(err)}`, "warning");
|
|
2083
|
-
}
|
|
2223
|
+
totalCommits += repoCommitsBefore;
|
|
2224
|
+
repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
|
|
2225
|
+
}
|
|
2084
2226
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2227
|
+
// Post-integration cleanup & acceptance
|
|
2228
|
+
const allRepos: { id: string; root: string }[] = [];
|
|
2229
|
+
if (wsConfig) {
|
|
2230
|
+
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
2231
|
+
allRepos.push({ id: repoId, root: repoConf.path });
|
|
2232
|
+
}
|
|
2233
|
+
} else {
|
|
2234
|
+
allRepos.push({ id: "(default)", root: repoRoot });
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
const opId = resolveOperatorId(orchConfig);
|
|
2238
|
+
const orchPrefix = orchConfig.orchestrator.worktree_prefix;
|
|
2239
|
+
|
|
2240
|
+
for (const repo of allRepos) {
|
|
2241
|
+
dropBatchAutostash(repo.root, batchId);
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
const branchCleanupLines: string[] = [];
|
|
2245
|
+
for (const repo of allRepos) {
|
|
2246
|
+
const branchCleanup = deleteStaleBranches(repo.root, opId, batchId);
|
|
2247
|
+
const totalDeleted = branchCleanup.deletedTaskBranches.length + branchCleanup.deletedSavedBranches.length;
|
|
2248
|
+
if (totalDeleted > 0 || branchCleanup.failedDeletes.length > 0) {
|
|
2249
|
+
const label = repo.id === "(default)" ? "" : ` (${repo.id})`;
|
|
2250
|
+
if (branchCleanup.deletedTaskBranches.length > 0) {
|
|
2251
|
+
branchCleanupLines.push(` 🗑️ Deleted ${branchCleanup.deletedTaskBranches.length} task branch(es)${label}`);
|
|
2252
|
+
}
|
|
2253
|
+
if (branchCleanup.deletedSavedBranches.length > 0) {
|
|
2254
|
+
branchCleanupLines.push(` 🗑️ Deleted ${branchCleanup.deletedSavedBranches.length} saved branch(es)${label}`);
|
|
2090
2255
|
}
|
|
2256
|
+
if (branchCleanup.failedDeletes.length > 0) {
|
|
2257
|
+
branchCleanupLines.push(` ⚠️ Failed to delete ${branchCleanup.failedDeletes.length} branch(es)${label}: ${branchCleanup.failedDeletes.join(", ")}`);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
if (branchCleanupLines.length > 0) {
|
|
2262
|
+
outputLines.push("Branch cleanup:\n" + branchCleanupLines.join("\n"));
|
|
2263
|
+
}
|
|
2091
2264
|
|
|
2092
|
-
|
|
2093
|
-
|
|
2265
|
+
const skipOrchBranch = parsed.mode === "pr";
|
|
2266
|
+
const repoFindings: IntegrateCleanupRepoFindings[] = [];
|
|
2267
|
+
for (const repo of allRepos) {
|
|
2268
|
+
const findings = collectRepoCleanupFindings(
|
|
2269
|
+
repo.root, repo.id === "(default)" ? undefined : repo.id,
|
|
2270
|
+
opId, batchId, orchPrefix, resolvedOrchBranch, orchConfig,
|
|
2271
|
+
{ skipOrchBranch },
|
|
2272
|
+
);
|
|
2273
|
+
repoFindings.push(findings);
|
|
2274
|
+
}
|
|
2094
2275
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2276
|
+
const cleanupResult = computeIntegrateCleanupResult(repoFindings);
|
|
2277
|
+
if (cleanupResult.notifyLevel === "warning") {
|
|
2278
|
+
hasWarning = true;
|
|
2279
|
+
}
|
|
2280
|
+
|
|
2281
|
+
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
2282
|
+
|
|
2283
|
+
const integrationSummary = wsConfig
|
|
2284
|
+
? `✅ Integrated ${resolvedOrchBranch} across ${reposToIntegrate.length} repo(s).\n${repoMessages.join("\n")}\n${totalCommits} total commit(s) applied.`
|
|
2285
|
+
: `${repoMessages[0] || "✅ Integrated."}\n${commitsAhead} commit(s) applied.`;
|
|
2286
|
+
|
|
2287
|
+
outputLines.push(integrationSummary + "\n" + cleanupResult.report);
|
|
2288
|
+
|
|
2289
|
+
// TP-043 R004: deferred batch summary
|
|
2290
|
+
if (supervisorState.active && supervisorState.pendingSummaryDeps) {
|
|
2291
|
+
const deps = supervisorState.pendingSummaryDeps;
|
|
2292
|
+
supervisorState.pendingSummaryDeps = null;
|
|
2293
|
+
if (supervisorState.batchStateRef && supervisorState.stateRoot) {
|
|
2294
|
+
presentBatchSummary(pi, supervisorState.batchStateRef, supervisorState.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
|
|
2295
|
+
}
|
|
2296
|
+
deactivateSupervisor(pi, supervisorState);
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
return { message: outputLines.join("\n\n"), level: hasWarning ? "warning" : "info" };
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
pi.registerCommand("orch-status", {
|
|
2303
|
+
description: "Show current batch progress",
|
|
2304
|
+
handler: async (_args, ctx) => {
|
|
2305
|
+
const result = doOrchStatus(ctx.cwd);
|
|
2306
|
+
ctx.ui.notify(result, "info");
|
|
2307
|
+
},
|
|
2308
|
+
});
|
|
2309
|
+
|
|
2310
|
+
pi.registerCommand("orch-pause", {
|
|
2311
|
+
description: "Pause batch after current tasks finish",
|
|
2312
|
+
handler: async (_args, ctx) => {
|
|
2313
|
+
const result = doOrchPause();
|
|
2314
|
+
// Determine notification level from result content
|
|
2315
|
+
const level = result.includes("No batch") || result.includes("already paused") ? "warning" : "info";
|
|
2316
|
+
ctx.ui.notify(result, level);
|
|
2317
|
+
},
|
|
2318
|
+
});
|
|
2319
|
+
|
|
2320
|
+
pi.registerCommand("orch-resume", {
|
|
2321
|
+
description: "Resume a paused or interrupted batch: /orch-resume [--force]",
|
|
2322
|
+
handler: async (args, ctx) => {
|
|
2323
|
+
if (!requireExecCtx(ctx)) return;
|
|
2324
|
+
|
|
2325
|
+
// Parse arguments
|
|
2326
|
+
const parsed = parseResumeArgs(args);
|
|
2327
|
+
if ("error" in parsed) {
|
|
2328
|
+
ctx.ui.notify(`❌ ${parsed.error}`, "error");
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
const result = doOrchResume(parsed.force, ctx);
|
|
2333
|
+
ctx.ui.notify(result.message, result.error ? "warning" : "info");
|
|
2334
|
+
},
|
|
2335
|
+
});
|
|
2336
|
+
|
|
2337
|
+
pi.registerCommand("orch-abort", {
|
|
2338
|
+
description: "Abort batch: /orch-abort [--hard]",
|
|
2339
|
+
handler: async (args, ctx) => {
|
|
2340
|
+
try {
|
|
2341
|
+
const hard = args?.trim() === "--hard";
|
|
2342
|
+
const result = doOrchAbort(hard, ctx);
|
|
2343
|
+
ctx.ui.notify(result, "info");
|
|
2101
2344
|
} catch (err) {
|
|
2102
2345
|
// Top-level catch: ensure the user ALWAYS sees something
|
|
2103
2346
|
ctx.ui.notify(
|
|
@@ -2354,238 +2597,177 @@ export default function (pi: ExtensionAPI) {
|
|
|
2354
2597
|
|
|
2355
2598
|
if (!requireExecCtx(ctx)) return;
|
|
2356
2599
|
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
return;
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
// ── Step 2: Resolve integration context ──────────────────
|
|
2365
|
-
const { repoRoot } = execCtx!;
|
|
2366
|
-
const resolution = resolveIntegrationContext(parsed, {
|
|
2367
|
-
loadBatchState: () => loadBatchState(repoRoot),
|
|
2368
|
-
getCurrentBranch: () => getCurrentBranch(repoRoot),
|
|
2369
|
-
listOrchBranches: () => {
|
|
2370
|
-
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
2371
|
-
return result.ok
|
|
2372
|
-
? result.stdout.split("\n").map(b => b.replace(/^\*?\s+/, "").trim()).filter(Boolean)
|
|
2373
|
-
: [];
|
|
2374
|
-
},
|
|
2375
|
-
orchBranchExists: (branch: string) => {
|
|
2376
|
-
return runGit(["rev-parse", "--verify", `refs/heads/${branch}`], repoRoot).ok;
|
|
2377
|
-
},
|
|
2378
|
-
});
|
|
2379
|
-
|
|
2380
|
-
if ("error" in resolution) {
|
|
2381
|
-
const severity = (resolution as IntegrationContextError).severity;
|
|
2382
|
-
ctx.ui.notify(resolution.error, severity === "info" ? "info" : "error");
|
|
2383
|
-
return;
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
const { orchBranch, baseBranch, batchId, currentBranch, notices } = resolution as IntegrationContext;
|
|
2387
|
-
|
|
2388
|
-
// Show any notices from resolution (auto-detection messages, warnings)
|
|
2389
|
-
for (const notice of notices) {
|
|
2390
|
-
ctx.ui.notify(notice, "info");
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
// ── Step 2a: Branch protection pre-check (TP-052) ───────
|
|
2394
|
-
// When using ff or merge mode (direct push), check if the target
|
|
2395
|
-
// branch has protection rules. If protected, warn and suggest --pr.
|
|
2396
|
-
// Graceful degradation: if gh is unavailable, skip the check.
|
|
2397
|
-
if (parsed.mode !== "pr") {
|
|
2398
|
-
const { detectBranchProtection } = await import("./supervisor.ts");
|
|
2399
|
-
const protectionStatus = detectBranchProtection(baseBranch, repoRoot);
|
|
2400
|
-
if (protectionStatus === "protected") {
|
|
2401
|
-
ctx.ui.notify(
|
|
2402
|
-
`⚠️ Branch \`${baseBranch}\` has branch protection rules enabled.\n` +
|
|
2403
|
-
`Direct merges may be blocked by your repository settings.\n\n` +
|
|
2404
|
-
`Recommended: use \`/orch-integrate --pr\` to create a pull request instead.`,
|
|
2405
|
-
"warning",
|
|
2406
|
-
);
|
|
2407
|
-
// Don't block — proceed with the attempt. The merge will fail
|
|
2408
|
-
// gracefully and show a clear error if protection blocks it.
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
|
|
2412
|
-
// ── Step 2b: Pre-integration summary ─────────────────────
|
|
2413
|
-
// Count commits ahead
|
|
2414
|
-
const revListResult = runGit(
|
|
2415
|
-
["rev-list", "--count", `${currentBranch}..${orchBranch}`],
|
|
2416
|
-
repoRoot,
|
|
2417
|
-
);
|
|
2418
|
-
const commitsAhead = revListResult.ok ? revListResult.stdout.trim() : "?";
|
|
2419
|
-
|
|
2420
|
-
// Get diff summary
|
|
2421
|
-
const diffStatResult = runGit(
|
|
2422
|
-
["diff", "--stat", `${currentBranch}...${orchBranch}`],
|
|
2423
|
-
repoRoot,
|
|
2424
|
-
);
|
|
2425
|
-
const diffSummary = diffStatResult.ok ? diffStatResult.stdout.trim() : "(unable to compute diff)";
|
|
2426
|
-
|
|
2427
|
-
ctx.ui.notify(
|
|
2428
|
-
`🔀 Integration Summary\n` +
|
|
2429
|
-
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2430
|
-
` Orch branch: ${orchBranch}\n` +
|
|
2431
|
-
` Target: ${currentBranch}\n` +
|
|
2432
|
-
` Commits: ${commitsAhead} ahead\n` +
|
|
2433
|
-
` Mode: ${parsed.mode === "ff" ? "fast-forward" : parsed.mode === "merge" ? "merge commit" : "pull request"}\n` +
|
|
2434
|
-
(batchId ? ` Batch: ${batchId}\n` : "") +
|
|
2435
|
-
(parsed.force ? ` ⚠ Force: branch safety check skipped\n` : "") +
|
|
2436
|
-
`\n` +
|
|
2437
|
-
(diffSummary ? `${diffSummary}\n` : "") +
|
|
2438
|
-
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
2439
|
-
"info",
|
|
2440
|
-
);
|
|
2441
|
-
|
|
2442
|
-
// ── Step 3: Execute integration mode ─────────────────
|
|
2443
|
-
// In workspace mode, integrate in every repo that has the orch branch.
|
|
2444
|
-
const resolvedOrchBranch = (resolution as IntegrationContext).orchBranch;
|
|
2445
|
-
const wsConfig = execCtx!.workspaceConfig;
|
|
2446
|
-
const reposToIntegrate: { id: string; root: string }[] = [];
|
|
2447
|
-
|
|
2448
|
-
if (wsConfig) {
|
|
2449
|
-
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
2450
|
-
// Check if orch branch exists in this repo
|
|
2451
|
-
const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${resolvedOrchBranch}`], repoConf.path);
|
|
2452
|
-
if (branchCheck.ok) {
|
|
2453
|
-
reposToIntegrate.push({ id: repoId, root: repoConf.path });
|
|
2454
|
-
}
|
|
2455
|
-
}
|
|
2456
|
-
} else {
|
|
2457
|
-
reposToIntegrate.push({ id: "(default)", root: repoRoot });
|
|
2458
|
-
}
|
|
2459
|
-
|
|
2460
|
-
let totalCommits = 0;
|
|
2461
|
-
let allSucceeded = true;
|
|
2462
|
-
const repoMessages: string[] = [];
|
|
2463
|
-
|
|
2464
|
-
for (const repo of reposToIntegrate) {
|
|
2465
|
-
// Count commits BEFORE integration (after ff, HEAD === orch tip so count would be 0)
|
|
2466
|
-
const preCountResult = runGit(["rev-list", "--count", `HEAD..${resolvedOrchBranch}`], repo.root);
|
|
2467
|
-
const repoCommitsBefore = preCountResult.ok ? parseInt(preCountResult.stdout) || 0 : 0;
|
|
2468
|
-
|
|
2469
|
-
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
2470
|
-
runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
|
|
2471
|
-
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
2472
|
-
try {
|
|
2473
|
-
const stdout = execFileSync(cmd, cmdArgs, {
|
|
2474
|
-
encoding: "utf-8",
|
|
2475
|
-
timeout: 60_000,
|
|
2476
|
-
cwd: repo.root,
|
|
2477
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2478
|
-
}).trim();
|
|
2479
|
-
return { ok: true, stdout, stderr: "" };
|
|
2480
|
-
} catch (err: unknown) {
|
|
2481
|
-
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
2482
|
-
return {
|
|
2483
|
-
ok: false,
|
|
2484
|
-
stdout: (e.stdout ?? "").toString().trim(),
|
|
2485
|
-
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
2486
|
-
};
|
|
2487
|
-
}
|
|
2488
|
-
},
|
|
2489
|
-
deleteBatchState: () => { /* handled once after all repos */ },
|
|
2490
|
-
});
|
|
2491
|
-
|
|
2492
|
-
if (!integrationResult.success) {
|
|
2493
|
-
ctx.ui.notify(`❌ Integration failed in ${repo.id}:\n${integrationResult.error}`, "error");
|
|
2494
|
-
allSucceeded = false;
|
|
2495
|
-
break;
|
|
2496
|
-
}
|
|
2497
|
-
|
|
2498
|
-
totalCommits += repoCommitsBefore;
|
|
2499
|
-
repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
|
|
2500
|
-
}
|
|
2501
|
-
|
|
2502
|
-
if (!allSucceeded) return;
|
|
2503
|
-
|
|
2504
|
-
// ── Step 4: Post-integration cleanup & acceptance ────────
|
|
2505
|
-
// Run acceptance checks BEFORE deleting batch state so recovery
|
|
2506
|
-
// context is still available if something goes wrong.
|
|
2600
|
+
const result = await doOrchIntegrate(args, ctx);
|
|
2601
|
+
ctx.ui.notify(result.message, result.error ? "error" : (result.level ?? "info"));
|
|
2602
|
+
},
|
|
2603
|
+
});
|
|
2507
2604
|
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2605
|
+
// ── TP-053: Register orchestrator tools for supervisor agent ─────
|
|
2606
|
+
|
|
2607
|
+
pi.registerTool({
|
|
2608
|
+
name: "orch_status",
|
|
2609
|
+
label: "Orchestrator Status",
|
|
2610
|
+
description:
|
|
2611
|
+
"Check the current batch status. Returns batch phase, wave progress, " +
|
|
2612
|
+
"task counts, and elapsed time. Works even when no batch is running.",
|
|
2613
|
+
promptSnippet: "orch_status() — check current batch status",
|
|
2614
|
+
promptGuidelines: [
|
|
2615
|
+
"Call orch_status to get a snapshot of the current batch.",
|
|
2616
|
+
"Use this when the operator asks 'how is the batch going?' or you need to check progress.",
|
|
2617
|
+
"If no batch is running, the result will say so.",
|
|
2618
|
+
],
|
|
2619
|
+
parameters: Type.Object({}),
|
|
2620
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, ctx) {
|
|
2621
|
+
try {
|
|
2622
|
+
const result = doOrchStatus(ctx.cwd);
|
|
2623
|
+
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
return {
|
|
2626
|
+
content: [{ type: "text" as const, text: `Error checking status: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2627
|
+
details: undefined,
|
|
2628
|
+
};
|
|
2517
2629
|
}
|
|
2630
|
+
},
|
|
2631
|
+
});
|
|
2518
2632
|
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2633
|
+
pi.registerTool({
|
|
2634
|
+
name: "orch_pause",
|
|
2635
|
+
label: "Pause Batch",
|
|
2636
|
+
description:
|
|
2637
|
+
"Pause the running batch after current tasks finish. " +
|
|
2638
|
+
"Tasks already in progress will complete, but no new tasks will start.",
|
|
2639
|
+
promptSnippet: "orch_pause() — pause the running batch",
|
|
2640
|
+
promptGuidelines: [
|
|
2641
|
+
"Call orch_pause to pause a running batch gracefully.",
|
|
2642
|
+
"Current tasks will finish, but no new tasks will be launched.",
|
|
2643
|
+
"Use this when you need to investigate an issue before more tasks run.",
|
|
2644
|
+
"After pausing, use orch_resume to continue.",
|
|
2645
|
+
],
|
|
2646
|
+
parameters: Type.Object({}),
|
|
2647
|
+
async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
|
|
2648
|
+
try {
|
|
2649
|
+
const result = doOrchPause();
|
|
2650
|
+
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
2651
|
+
} catch (err) {
|
|
2652
|
+
return {
|
|
2653
|
+
content: [{ type: "text" as const, text: `Error pausing batch: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2654
|
+
details: undefined,
|
|
2655
|
+
};
|
|
2527
2656
|
}
|
|
2657
|
+
},
|
|
2658
|
+
});
|
|
2528
2659
|
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2660
|
+
pi.registerTool({
|
|
2661
|
+
name: "orch_resume",
|
|
2662
|
+
label: "Resume Batch",
|
|
2663
|
+
description:
|
|
2664
|
+
"Resume a paused or interrupted batch. " +
|
|
2665
|
+
"The batch will continue from where it left off. " +
|
|
2666
|
+
"Use force=true to resume from a stopped or failed state.",
|
|
2667
|
+
promptSnippet: "orch_resume(force?) — resume a paused batch",
|
|
2668
|
+
promptGuidelines: [
|
|
2669
|
+
"Call orch_resume to continue a paused or interrupted batch.",
|
|
2670
|
+
"Set force=true to resume from a stopped or failed state (runs pre-resume diagnostics).",
|
|
2671
|
+
"Cannot resume if a batch is already actively running (launching, executing, merging, planning).",
|
|
2672
|
+
"The resume happens asynchronously — the tool returns immediately with a status message.",
|
|
2673
|
+
],
|
|
2674
|
+
parameters: Type.Object({
|
|
2675
|
+
force: Type.Optional(Type.Boolean({
|
|
2676
|
+
description: "Resume from stopped or failed state (default: false)",
|
|
2677
|
+
})),
|
|
2678
|
+
}),
|
|
2679
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2680
|
+
try {
|
|
2681
|
+
const result = doOrchResume(params.force ?? false, ctx);
|
|
2682
|
+
return { content: [{ type: "text" as const, text: result.message }], details: undefined };
|
|
2683
|
+
} catch (err) {
|
|
2684
|
+
return {
|
|
2685
|
+
content: [{ type: "text" as const, text: `Error resuming batch: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2686
|
+
details: undefined,
|
|
2687
|
+
};
|
|
2551
2688
|
}
|
|
2689
|
+
},
|
|
2690
|
+
});
|
|
2552
2691
|
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
for (
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2692
|
+
pi.registerTool({
|
|
2693
|
+
name: "orch_abort",
|
|
2694
|
+
label: "Abort Batch",
|
|
2695
|
+
description:
|
|
2696
|
+
"Abort the running batch. Kills tmux sessions, cleans up state. " +
|
|
2697
|
+
"Use hard=true for immediate kill (no grace period). " +
|
|
2698
|
+
"Works even without execution context (safety-critical).",
|
|
2699
|
+
promptSnippet: "orch_abort(hard?) — abort the running batch",
|
|
2700
|
+
promptGuidelines: [
|
|
2701
|
+
"Call orch_abort to stop a running batch.",
|
|
2702
|
+
"Default (hard=false) is graceful abort — writes signal file and kills sessions.",
|
|
2703
|
+
"Set hard=true for immediate termination without grace period.",
|
|
2704
|
+
"Use this when a batch is stuck, failing repeatedly, or the operator requests it.",
|
|
2705
|
+
"Worktrees and branches are preserved for inspection after abort.",
|
|
2706
|
+
],
|
|
2707
|
+
parameters: Type.Object({
|
|
2708
|
+
hard: Type.Optional(Type.Boolean({
|
|
2709
|
+
description: "Hard abort — immediate kill without grace period (default: false)",
|
|
2710
|
+
})),
|
|
2711
|
+
}),
|
|
2712
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2713
|
+
try {
|
|
2714
|
+
const result = doOrchAbort(params.hard ?? false, ctx);
|
|
2715
|
+
return { content: [{ type: "text" as const, text: result }], details: undefined };
|
|
2716
|
+
} catch (err) {
|
|
2717
|
+
return {
|
|
2718
|
+
content: [{ type: "text" as const, text: `Error aborting batch: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2719
|
+
details: undefined,
|
|
2720
|
+
};
|
|
2565
2721
|
}
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2566
2724
|
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2725
|
+
pi.registerTool({
|
|
2726
|
+
name: "orch_integrate",
|
|
2727
|
+
label: "Integrate Batch",
|
|
2728
|
+
description:
|
|
2729
|
+
"Integrate a completed orch batch into the working branch. " +
|
|
2730
|
+
"Supports fast-forward (default), merge commit, or pull request modes.",
|
|
2731
|
+
promptSnippet: "orch_integrate(mode?, force?, branch?) — integrate completed batch",
|
|
2732
|
+
promptGuidelines: [
|
|
2733
|
+
"Call orch_integrate after a batch completes to merge changes into the working branch.",
|
|
2734
|
+
"mode='fast-forward' (default) — cleanest history, requires linear history.",
|
|
2735
|
+
"mode='merge' — creates a merge commit.",
|
|
2736
|
+
"mode='pr' — pushes orch branch and creates a pull request (safest for protected branches).",
|
|
2737
|
+
"Set force=true to skip branch safety checks.",
|
|
2738
|
+
"The branch parameter is optional — auto-detected from batch state if omitted.",
|
|
2739
|
+
"If the target branch has protection rules, prefer mode='pr'.",
|
|
2740
|
+
],
|
|
2741
|
+
parameters: Type.Object({
|
|
2742
|
+
mode: Type.Optional(Type.Union(
|
|
2743
|
+
[Type.Literal("fast-forward"), Type.Literal("merge"), Type.Literal("pr")],
|
|
2744
|
+
{ description: 'Integration mode (default: "fast-forward")' },
|
|
2745
|
+
)),
|
|
2746
|
+
force: Type.Optional(Type.Boolean({
|
|
2747
|
+
description: "Skip branch safety check (default: false)",
|
|
2748
|
+
})),
|
|
2749
|
+
branch: Type.Optional(Type.String({
|
|
2750
|
+
description: "Orch branch name (auto-detected from batch state if omitted)",
|
|
2751
|
+
})),
|
|
2752
|
+
}),
|
|
2753
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
2754
|
+
try {
|
|
2755
|
+
// Build args string from tool parameters to pass to doOrchIntegrate
|
|
2756
|
+
const argParts: string[] = [];
|
|
2757
|
+
if (params.branch) argParts.push(params.branch);
|
|
2758
|
+
const mode = params.mode ?? "fast-forward";
|
|
2759
|
+
if (mode === "merge") argParts.push("--merge");
|
|
2760
|
+
else if (mode === "pr") argParts.push("--pr");
|
|
2761
|
+
if (params.force) argParts.push("--force");
|
|
2762
|
+
|
|
2763
|
+
const argsStr = argParts.length > 0 ? argParts.join(" ") : undefined;
|
|
2764
|
+
const result = await doOrchIntegrate(argsStr, ctx);
|
|
2765
|
+
return { content: [{ type: "text" as const, text: result.message }], details: undefined };
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
return {
|
|
2768
|
+
content: [{ type: "text" as const, text: `Error integrating batch: ${err instanceof Error ? err.message : String(err)}` }],
|
|
2769
|
+
details: undefined,
|
|
2770
|
+
};
|
|
2589
2771
|
}
|
|
2590
2772
|
},
|
|
2591
2773
|
});
|