taskplane 0.5.7 → 0.5.9
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.
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Main batch execution engine
|
|
3
3
|
* @module orch/engine
|
|
4
4
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import { readFileSync, readdirSync, unlinkSync } from "fs";
|
|
6
|
+
import { join, resolve } from "path";
|
|
7
7
|
|
|
8
8
|
import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
|
|
9
9
|
import { execLog, executeWave, tmuxKillSession } from "./execution.ts";
|
|
@@ -364,16 +364,6 @@ export async function executeOrchBatch(
|
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
-
// ── Workspace mode: commit task artifacts to task-area repos ─
|
|
368
|
-
// In workspace mode, workers write .DONE and STATUS.md to the
|
|
369
|
-
// canonical task folder (e.g., shared-libs/task-management/...) via
|
|
370
|
-
// absolute paths, not to the lane worktree. These changes land as
|
|
371
|
-
// uncommitted modifications in the task-area repo's working tree.
|
|
372
|
-
// Commit them before the merge step so they appear in the orch branch.
|
|
373
|
-
if (workspaceConfig && waveResult.succeededTaskIds.length > 0) {
|
|
374
|
-
commitWorkspaceTaskArtifacts(discoveryRef, workspaceRoot ?? repoRoot, waveIdx + 1, batchState.batchId);
|
|
375
|
-
}
|
|
376
|
-
|
|
377
367
|
// ── Wave Merge ───────────────────────────────────────────
|
|
378
368
|
// Only merge if there are succeeded tasks in this wave
|
|
379
369
|
let mergeResult: MergeWaveResult | null = null;
|
|
@@ -882,78 +872,5 @@ export async function executeOrchBatch(
|
|
|
882
872
|
}
|
|
883
873
|
|
|
884
874
|
|
|
885
|
-
// ── Workspace Task Artifact Commit ───────────────────────────────────
|
|
886
|
-
|
|
887
|
-
/**
|
|
888
|
-
* In workspace mode, commit task artifacts (.DONE, STATUS.md) that workers
|
|
889
|
-
* wrote to the canonical task folder in the task-area repo.
|
|
890
|
-
*
|
|
891
|
-
* Workers write to absolute paths (e.g., shared-libs/task-management/.../TP-002/.DONE)
|
|
892
|
-
* which land as uncommitted changes in the task-area repo's working tree.
|
|
893
|
-
* This function finds all task-area repos with dirty task files and commits them
|
|
894
|
-
* so they appear in the lane branches and merge correctly.
|
|
895
|
-
*
|
|
896
|
-
* Best-effort: failures are logged but don't block the batch.
|
|
897
|
-
*/
|
|
898
|
-
function commitWorkspaceTaskArtifacts(
|
|
899
|
-
discovery: DiscoveryResult | null,
|
|
900
|
-
workspaceRoot: string,
|
|
901
|
-
waveIndex: number,
|
|
902
|
-
batchId: string,
|
|
903
|
-
): void {
|
|
904
|
-
if (!discovery) return;
|
|
905
|
-
|
|
906
|
-
// Collect unique repo roots that contain task folders
|
|
907
|
-
const repoRootsWithTasks = new Set<string>();
|
|
908
|
-
for (const [, task] of discovery.pending) {
|
|
909
|
-
const taskFolder = resolve(task.taskFolder);
|
|
910
|
-
// Walk up to find the git repo root for this task folder
|
|
911
|
-
const gitResult = runGit(["rev-parse", "--show-toplevel"], dirname(taskFolder));
|
|
912
|
-
if (gitResult.ok) {
|
|
913
|
-
repoRootsWithTasks.add(gitResult.stdout.trim().replace(/\\/g, "/"));
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
for (const taskRepoRoot of repoRootsWithTasks) {
|
|
918
|
-
// Check for uncommitted changes
|
|
919
|
-
const statusResult = runGit(["status", "--porcelain", "--", "task-management/"], taskRepoRoot);
|
|
920
|
-
if (!statusResult.ok) {
|
|
921
|
-
// Try without path filter (task area might have different name)
|
|
922
|
-
const statusAll = runGit(["status", "--porcelain"], taskRepoRoot);
|
|
923
|
-
if (!statusAll.ok || !statusAll.stdout.trim()) continue;
|
|
924
|
-
}
|
|
925
|
-
if (statusResult.ok && !statusResult.stdout.trim()) continue;
|
|
926
|
-
|
|
927
|
-
// Stage task artifacts (only .DONE and STATUS.md files)
|
|
928
|
-
const lines = (statusResult.stdout || "").split("\n").filter(l => l.trim());
|
|
929
|
-
let hasTaskArtifacts = false;
|
|
930
|
-
for (const line of lines) {
|
|
931
|
-
const file = line.slice(3).trim();
|
|
932
|
-
if (file.endsWith(".DONE") || file.endsWith("STATUS.md")) {
|
|
933
|
-
const addResult = runGit(["add", file], taskRepoRoot);
|
|
934
|
-
if (addResult.ok) hasTaskArtifacts = true;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
if (!hasTaskArtifacts) continue;
|
|
939
|
-
|
|
940
|
-
// Commit
|
|
941
|
-
const commitResult = runGit(
|
|
942
|
-
["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md)`],
|
|
943
|
-
taskRepoRoot,
|
|
944
|
-
);
|
|
945
|
-
if (commitResult.ok) {
|
|
946
|
-
execLog("batch", batchId, `committed workspace task artifacts`, {
|
|
947
|
-
repoRoot: taskRepoRoot,
|
|
948
|
-
wave: waveIndex,
|
|
949
|
-
});
|
|
950
|
-
} else if (!commitResult.stderr.includes("nothing to commit")) {
|
|
951
|
-
execLog("batch", batchId, `workspace task artifact commit failed (non-fatal): ${commitResult.stderr.slice(0, 200)}`, {
|
|
952
|
-
repoRoot: taskRepoRoot,
|
|
953
|
-
});
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
|
|
958
875
|
// ── Dashboard Widget (Step 6) ────────────────────────────────────────
|
|
959
876
|
|
|
@@ -1163,46 +1163,74 @@ export default function (pi: ExtensionAPI) {
|
|
|
1163
1163
|
);
|
|
1164
1164
|
|
|
1165
1165
|
// ── Step 3: Execute integration mode ─────────────────
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
} catch (err: unknown) {
|
|
1178
|
-
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
1179
|
-
return {
|
|
1180
|
-
ok: false,
|
|
1181
|
-
stdout: (e.stdout ?? "").toString().trim(),
|
|
1182
|
-
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
1183
|
-
};
|
|
1166
|
+
// In workspace mode, integrate in every repo that has the orch branch.
|
|
1167
|
+
const resolvedOrchBranch = (resolution as IntegrationContext).orchBranch;
|
|
1168
|
+
const wsConfig = execCtx!.workspaceConfig;
|
|
1169
|
+
const reposToIntegrate: { id: string; root: string }[] = [];
|
|
1170
|
+
|
|
1171
|
+
if (wsConfig) {
|
|
1172
|
+
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
1173
|
+
// Check if orch branch exists in this repo
|
|
1174
|
+
const branchCheck = runGit(["rev-parse", "--verify", `refs/heads/${resolvedOrchBranch}`], repoConf.path);
|
|
1175
|
+
if (branchCheck.ok) {
|
|
1176
|
+
reposToIntegrate.push({ id: repoId, root: repoConf.path });
|
|
1184
1177
|
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
if (!integrationResult.success) {
|
|
1190
|
-
ctx.ui.notify(integrationResult.error!, "error");
|
|
1191
|
-
return;
|
|
1178
|
+
}
|
|
1179
|
+
} else {
|
|
1180
|
+
reposToIntegrate.push({ id: "(default)", root: repoRoot });
|
|
1192
1181
|
}
|
|
1193
1182
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1183
|
+
let totalCommits = 0;
|
|
1184
|
+
let allSucceeded = true;
|
|
1185
|
+
const repoMessages: string[] = [];
|
|
1186
|
+
|
|
1187
|
+
for (const repo of reposToIntegrate) {
|
|
1188
|
+
const integrationResult = executeIntegration(parsed.mode, resolution as IntegrationContext, {
|
|
1189
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repo.root),
|
|
1190
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
1191
|
+
try {
|
|
1192
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
1193
|
+
encoding: "utf-8",
|
|
1194
|
+
timeout: 60_000,
|
|
1195
|
+
cwd: repo.root,
|
|
1196
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1197
|
+
}).trim();
|
|
1198
|
+
return { ok: true, stdout, stderr: "" };
|
|
1199
|
+
} catch (err: unknown) {
|
|
1200
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
1201
|
+
return {
|
|
1202
|
+
ok: false,
|
|
1203
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
1204
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
deleteBatchState: () => { /* handled once after all repos */ },
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
if (!integrationResult.success) {
|
|
1212
|
+
ctx.ui.notify(`❌ Integration failed in ${repo.id}:\n${integrationResult.error}`, "error");
|
|
1213
|
+
allSucceeded = false;
|
|
1214
|
+
break;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// Count commits for this repo
|
|
1218
|
+
const countResult = runGit(["rev-list", "--count", `HEAD...${resolvedOrchBranch}`], repo.root);
|
|
1219
|
+
const repoCommits = countResult.ok ? parseInt(countResult.stdout) || 0 : 0;
|
|
1220
|
+
totalCommits += repoCommits;
|
|
1221
|
+
repoMessages.push(` ${repo.id}: ${integrationResult.message}`);
|
|
1197
1222
|
}
|
|
1198
1223
|
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1224
|
+
if (!allSucceeded) return;
|
|
1225
|
+
|
|
1226
|
+
// Clean up batch state after all repos integrated
|
|
1227
|
+
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
1228
|
+
|
|
1229
|
+
const summary = wsConfig
|
|
1230
|
+
? `✅ Integrated ${resolvedOrchBranch} across ${reposToIntegrate.length} repo(s).\n${repoMessages.join("\n")}\n${totalCommits} total commit(s) applied.`
|
|
1231
|
+
: `${repoMessages[0] || "✅ Integrated."}\n${commitsAhead} commit(s) applied.`;
|
|
1232
|
+
|
|
1233
|
+
ctx.ui.notify(summary, "info");
|
|
1206
1234
|
},
|
|
1207
1235
|
});
|
|
1208
1236
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Merge orchestration, merge agents, merge worktree
|
|
3
3
|
* @module orch/merge
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, copyFileSync, mkdirSync } from "fs";
|
|
6
6
|
import { spawnSync } from "child_process";
|
|
7
|
-
import { join } from "path";
|
|
7
|
+
import { join, dirname } from "path";
|
|
8
8
|
|
|
9
9
|
import { buildLaneEnvVars, buildTmuxSpawnArgs, execLog, tmuxHasSession, tmuxKillSession, toTmuxPath } from "./execution.ts";
|
|
10
10
|
import { resolveOperatorId } from "./naming.ts";
|
|
@@ -755,6 +755,55 @@ export function mergeWave(
|
|
|
755
755
|
}
|
|
756
756
|
}
|
|
757
757
|
|
|
758
|
+
// ── Stage workspace task artifacts into merge worktree ──────────
|
|
759
|
+
// In workspace mode, workers write .DONE and STATUS.md to the canonical
|
|
760
|
+
// task folder (e.g., shared-libs/task-management/...) which is the repo's
|
|
761
|
+
// checked-out working tree (develop). These files need to be on the orch
|
|
762
|
+
// branch, not develop. Copy them into the merge worktree (which is on the
|
|
763
|
+
// orch branch's temp) and commit, so they're included in the update-ref.
|
|
764
|
+
if (mergeWorkDir) {
|
|
765
|
+
const statusResult = spawnSync("git", ["status", "--porcelain"], { cwd: repoRoot, encoding: "utf-8" });
|
|
766
|
+
if (statusResult.status === 0 && statusResult.stdout) {
|
|
767
|
+
const lines = statusResult.stdout.split("\n").filter((l: string) => l.trim());
|
|
768
|
+
const artifactFiles = lines
|
|
769
|
+
.map((l: string) => l.slice(3).trim())
|
|
770
|
+
.filter((f: string) => f.endsWith(".DONE") || f.endsWith("STATUS.md"));
|
|
771
|
+
|
|
772
|
+
if (artifactFiles.length > 0) {
|
|
773
|
+
let staged = 0;
|
|
774
|
+
for (const file of artifactFiles) {
|
|
775
|
+
const srcPath = join(repoRoot, file);
|
|
776
|
+
const destPath = join(mergeWorkDir, file);
|
|
777
|
+
try {
|
|
778
|
+
if (existsSync(srcPath)) {
|
|
779
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
780
|
+
copyFileSync(srcPath, destPath);
|
|
781
|
+
spawnSync("git", ["add", file], { cwd: mergeWorkDir });
|
|
782
|
+
staged++;
|
|
783
|
+
}
|
|
784
|
+
} catch { /* best effort */ }
|
|
785
|
+
}
|
|
786
|
+
if (staged > 0) {
|
|
787
|
+
spawnSync("git", ["commit", "-m", `checkpoint: wave ${waveIndex} task artifacts (.DONE, STATUS.md)`], { cwd: mergeWorkDir });
|
|
788
|
+
execLog("merge", `W${waveIndex}`, `committed ${staged} task artifact(s) to merge worktree`);
|
|
789
|
+
|
|
790
|
+
// Restore the repo's working tree — remove the artifacts from develop's working tree
|
|
791
|
+
// so they don't cause conflicts on /orch-integrate
|
|
792
|
+
for (const file of artifactFiles) {
|
|
793
|
+
spawnSync("git", ["checkout", "--", file], { cwd: repoRoot });
|
|
794
|
+
}
|
|
795
|
+
// Also remove any untracked .DONE files
|
|
796
|
+
for (const file of artifactFiles) {
|
|
797
|
+
if (file.endsWith(".DONE")) {
|
|
798
|
+
const srcPath = join(repoRoot, file);
|
|
799
|
+
try { if (existsSync(srcPath)) unlinkSync(srcPath); } catch { /* best effort */ }
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
758
807
|
// ── Update target branch ref and clean up merge worktree ────────
|
|
759
808
|
const anySuccess = laneResults.some(
|
|
760
809
|
r => r.result?.status === "SUCCESS" || r.result?.status === "CONFLICT_RESOLVED",
|