taskplane 0.5.11 → 0.6.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 +1 -1
- package/bin/rpc-wrapper.mjs +777 -0
- package/dashboard/public/app.js +45 -6
- package/dashboard/public/style.css +31 -0
- package/dashboard/server.cjs +326 -1
- package/extensions/task-runner.ts +1111 -30
- package/extensions/taskplane/config-loader.ts +31 -0
- package/extensions/taskplane/config-schema.ts +88 -0
- package/extensions/taskplane/diagnostic-reports.ts +463 -0
- package/extensions/taskplane/diagnostics.ts +323 -0
- package/extensions/taskplane/engine.ts +407 -62
- package/extensions/taskplane/extension.ts +259 -8
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +787 -69
- package/extensions/taskplane/messages.ts +594 -2
- package/extensions/taskplane/persistence.ts +342 -19
- package/extensions/taskplane/quality-gate.ts +1033 -0
- package/extensions/taskplane/resume.ts +505 -36
- package/extensions/taskplane/supervisor-primer.md +664 -0
- package/extensions/taskplane/types.ts +534 -6
- package/extensions/taskplane/verification.ts +537 -0
- package/extensions/taskplane/worktree.ts +329 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/references/prompt-template.md +0 -2
- package/templates/agents/task-reviewer.md +54 -2
- package/templates/agents/task-worker.md +8 -4
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
import { execSync, execFileSync } from "child_process";
|
|
4
|
-
import { writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
4
|
+
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
5
|
+
import { join, resolve } from "path";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_ORCHESTRATOR_CONFIG,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
ORCH_MESSAGES,
|
|
12
12
|
StateFileError,
|
|
13
13
|
WorkspaceConfigError,
|
|
14
|
+
computeIntegrateCleanupResult,
|
|
14
15
|
computeWaveAssignments,
|
|
15
16
|
createOrchWidget,
|
|
16
17
|
deleteBatchState,
|
|
@@ -25,10 +26,13 @@ import {
|
|
|
25
26
|
freshOrchBatchState,
|
|
26
27
|
getCurrentBranch,
|
|
27
28
|
listOrchSessions,
|
|
29
|
+
listWorktrees,
|
|
28
30
|
loadBatchState,
|
|
29
31
|
loadOrchestratorConfig,
|
|
30
32
|
loadTaskRunnerConfig,
|
|
31
33
|
parseOrchSessionNames,
|
|
34
|
+
resolveOperatorId,
|
|
35
|
+
resolveWorktreeBasePath,
|
|
32
36
|
resumeOrchBatch,
|
|
33
37
|
runDiscovery,
|
|
34
38
|
runGit,
|
|
@@ -39,6 +43,7 @@ import { openSettingsTui } from "./settings-tui.ts";
|
|
|
39
43
|
import type {
|
|
40
44
|
AbortMode,
|
|
41
45
|
ExecutionContext,
|
|
46
|
+
IntegrateCleanupRepoFindings,
|
|
42
47
|
MonitorState,
|
|
43
48
|
OrchestratorConfig,
|
|
44
49
|
PersistedBatchState,
|
|
@@ -106,6 +111,42 @@ export function parseIntegrateArgs(raw: string | undefined): IntegrateArgs | { e
|
|
|
106
111
|
};
|
|
107
112
|
}
|
|
108
113
|
|
|
114
|
+
// ── Resume Args Parsing ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export interface ResumeArgs {
|
|
117
|
+
force: boolean;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Parse `/orch-resume` command arguments.
|
|
122
|
+
*
|
|
123
|
+
* Supported flags: --force
|
|
124
|
+
* No positional arguments accepted.
|
|
125
|
+
*
|
|
126
|
+
* Returns parsed args or an error string if arguments are invalid.
|
|
127
|
+
*/
|
|
128
|
+
export function parseResumeArgs(raw: string | undefined): ResumeArgs | { error: string } {
|
|
129
|
+
const input = raw?.trim() ?? "";
|
|
130
|
+
if (!input) return { force: false };
|
|
131
|
+
|
|
132
|
+
const tokens = input.split(/\s+/).filter(Boolean);
|
|
133
|
+
let force = false;
|
|
134
|
+
|
|
135
|
+
for (const token of tokens) {
|
|
136
|
+
if (token === "--force") {
|
|
137
|
+
force = true;
|
|
138
|
+
} else if (token === "--help") {
|
|
139
|
+
return { error: "Usage: /orch-resume [--force]\n\n --force Resume from stopped or failed state (runs pre-resume diagnostics first)" };
|
|
140
|
+
} else if (token.startsWith("--")) {
|
|
141
|
+
return { error: `Unknown flag: ${token}\n\nUsage: /orch-resume [--force]` };
|
|
142
|
+
} else {
|
|
143
|
+
return { error: `Unexpected argument: ${token}\n\nUsage: /orch-resume [--force]` };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { force };
|
|
148
|
+
}
|
|
149
|
+
|
|
109
150
|
// ── Integration Context Resolution ────────────────────────────────────
|
|
110
151
|
|
|
111
152
|
/**
|
|
@@ -493,6 +534,155 @@ function performCleanup(
|
|
|
493
534
|
return result;
|
|
494
535
|
}
|
|
495
536
|
|
|
537
|
+
// ── Post-Integration Cleanup Helpers (TP-029 Step 3) ─────────────────
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Drop batch-scoped autostash entries from a repo.
|
|
541
|
+
*
|
|
542
|
+
* Targets two stash message patterns created during orchestration:
|
|
543
|
+
* - "orch-integrate-autostash-{batchId}" (from /orch-integrate ff/merge modes)
|
|
544
|
+
* - "merge-agent-autostash-w{N}-{batchId}" (from merge.ts wave ff)
|
|
545
|
+
*
|
|
546
|
+
* Git stash subjects include a branch prefix ("On <branch>: <message>"), so
|
|
547
|
+
* we match with `String.includes()` or a regex test against the full subject.
|
|
548
|
+
*
|
|
549
|
+
* Scans the stash list bottom-to-top so that dropping entries doesn't
|
|
550
|
+
* invalidate remaining indices. Non-matching stashes are never touched.
|
|
551
|
+
*/
|
|
552
|
+
export function dropBatchAutostash(repoRoot: string, batchId: string): void {
|
|
553
|
+
if (!batchId) return;
|
|
554
|
+
|
|
555
|
+
const stashList = runGit(["stash", "list", "--format=%gd %s"], repoRoot);
|
|
556
|
+
if (!stashList.ok || !stashList.stdout.trim()) return;
|
|
557
|
+
|
|
558
|
+
// Collect indices to drop (bottom-up order — highest index first)
|
|
559
|
+
const lines = stashList.stdout.trim().split("\n");
|
|
560
|
+
const indicesToDrop: number[] = [];
|
|
561
|
+
|
|
562
|
+
// Match patterns within the full stash subject (includes "On <branch>: " prefix)
|
|
563
|
+
const integrateSubstring = `orch-integrate-autostash-${batchId}`;
|
|
564
|
+
const mergePattern = new RegExp(`merge-agent-autostash-w\\d+-${escapeRegexStr(batchId)}`);
|
|
565
|
+
|
|
566
|
+
for (const line of lines) {
|
|
567
|
+
// Format: "stash@{N} <subject>"
|
|
568
|
+
const match = line.match(/^stash@\{(\d+)\}\s+(.*)$/);
|
|
569
|
+
if (!match) continue;
|
|
570
|
+
const idx = parseInt(match[1], 10);
|
|
571
|
+
const subject = match[2];
|
|
572
|
+
if (subject.includes(integrateSubstring) || mergePattern.test(subject)) {
|
|
573
|
+
indicesToDrop.push(idx);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Sort descending so we drop from bottom up
|
|
578
|
+
indicesToDrop.sort((a, b) => b - a);
|
|
579
|
+
for (const idx of indicesToDrop) {
|
|
580
|
+
runGit(["stash", "drop", `stash@{${idx}}`], repoRoot);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Escape a string for use in a RegExp.
|
|
586
|
+
*/
|
|
587
|
+
function escapeRegexStr(s: string): string {
|
|
588
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Collect cleanup acceptance findings for a single repo.
|
|
593
|
+
*
|
|
594
|
+
* Checks the five acceptance criteria from roadmap 2d:
|
|
595
|
+
* 1. No registered lane worktrees (via listWorktrees)
|
|
596
|
+
* 2. No lane branches (task/{opId}-lane-*)
|
|
597
|
+
* 3. No orch branches (the specific orch branch for this batch)
|
|
598
|
+
* 4. No batch-scoped autostash entries
|
|
599
|
+
* 5. No non-empty .worktrees/ containers
|
|
600
|
+
*/
|
|
601
|
+
export function collectRepoCleanupFindings(
|
|
602
|
+
repoRoot: string,
|
|
603
|
+
repoId: string | undefined,
|
|
604
|
+
opId: string,
|
|
605
|
+
batchId: string,
|
|
606
|
+
worktreePrefix: string,
|
|
607
|
+
orchBranch: string,
|
|
608
|
+
orchConfig: OrchestratorConfig,
|
|
609
|
+
options?: { skipOrchBranch?: boolean },
|
|
610
|
+
): IntegrateCleanupRepoFindings {
|
|
611
|
+
const findings: IntegrateCleanupRepoFindings = {
|
|
612
|
+
repoRoot,
|
|
613
|
+
repoId,
|
|
614
|
+
staleWorktrees: [],
|
|
615
|
+
staleLaneBranches: [],
|
|
616
|
+
staleOrchBranches: [],
|
|
617
|
+
staleAutostashEntries: [],
|
|
618
|
+
nonEmptyWorktreeContainers: [],
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
// 1. Stale lane worktrees — check for any worktrees belonging to this operator+batch
|
|
622
|
+
try {
|
|
623
|
+
const wts = listWorktrees(worktreePrefix, repoRoot, opId, batchId);
|
|
624
|
+
findings.staleWorktrees = wts.map(wt => wt.path);
|
|
625
|
+
} catch { /* best effort — git worktree list may fail in unusual states */ }
|
|
626
|
+
|
|
627
|
+
// 2. Lane branches — task/{opId}-lane-*
|
|
628
|
+
try {
|
|
629
|
+
const branchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
630
|
+
if (branchResult.ok && branchResult.stdout.trim()) {
|
|
631
|
+
findings.staleLaneBranches = branchResult.stdout
|
|
632
|
+
.split("\n")
|
|
633
|
+
.map(b => b.replace(/^\*?\s+/, "").trim())
|
|
634
|
+
.filter(Boolean);
|
|
635
|
+
}
|
|
636
|
+
} catch { /* best effort */ }
|
|
637
|
+
|
|
638
|
+
// 3. Orch branch — check if the specific orch branch still exists
|
|
639
|
+
// Skip in PR mode where the orch branch is intentionally preserved for the PR.
|
|
640
|
+
if (!options?.skipOrchBranch) {
|
|
641
|
+
try {
|
|
642
|
+
const orchCheck = runGit(["rev-parse", "--verify", `refs/heads/${orchBranch}`], repoRoot);
|
|
643
|
+
if (orchCheck.ok) {
|
|
644
|
+
findings.staleOrchBranches = [orchBranch];
|
|
645
|
+
}
|
|
646
|
+
} catch { /* best effort */ }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// 4. Autostash entries — same patterns as dropBatchAutostash
|
|
650
|
+
// Git stash subjects include branch prefix ("On <branch>: <message>"),
|
|
651
|
+
// so we use substring/regex matching against the full subject.
|
|
652
|
+
if (batchId) {
|
|
653
|
+
try {
|
|
654
|
+
const stashList = runGit(["stash", "list", "--format=%gd %s"], repoRoot);
|
|
655
|
+
if (stashList.ok && stashList.stdout.trim()) {
|
|
656
|
+
const integrateSubstring = `orch-integrate-autostash-${batchId}`;
|
|
657
|
+
const mergePattern = new RegExp(`merge-agent-autostash-w\\d+-${escapeRegexStr(batchId)}`);
|
|
658
|
+
for (const line of stashList.stdout.trim().split("\n")) {
|
|
659
|
+
const match = line.match(/^stash@\{(\d+)\}\s+(.*)$/);
|
|
660
|
+
if (!match) continue;
|
|
661
|
+
const subject = match[2];
|
|
662
|
+
if (subject.includes(integrateSubstring) || mergePattern.test(subject)) {
|
|
663
|
+
findings.staleAutostashEntries.push(match[1]); // stash index
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
} catch { /* best effort */ }
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 5. Non-empty .worktrees/ containers (subdirectory mode only)
|
|
671
|
+
if (orchConfig.orchestrator.worktree_location !== "sibling") {
|
|
672
|
+
try {
|
|
673
|
+
const basePath = resolveWorktreeBasePath(repoRoot, orchConfig);
|
|
674
|
+
if (existsSync(basePath)) {
|
|
675
|
+
const entries = readdirSync(basePath);
|
|
676
|
+
if (entries.length > 0) {
|
|
677
|
+
findings.nonEmptyWorktreeContainers = [basePath];
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch { /* best effort */ }
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return findings;
|
|
684
|
+
}
|
|
685
|
+
|
|
496
686
|
// ── Extension ────────────────────────────────────────────────────────
|
|
497
687
|
|
|
498
688
|
export default function (pi: ExtensionAPI) {
|
|
@@ -615,7 +805,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
615
805
|
return;
|
|
616
806
|
|
|
617
807
|
case "cleanup-stale":
|
|
618
|
-
// No orphans + stale/
|
|
808
|
+
// No orphans + stale/completed state file — auto-delete and continue
|
|
619
809
|
try {
|
|
620
810
|
deleteBatchState(repoRoot);
|
|
621
811
|
} catch {
|
|
@@ -626,6 +816,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
626
816
|
}
|
|
627
817
|
break;
|
|
628
818
|
|
|
819
|
+
case "paused-corrupt":
|
|
820
|
+
// Corrupt/unreadable state file — do NOT auto-delete.
|
|
821
|
+
// Enter paused phase so operator-visible state reflects the issue,
|
|
822
|
+
// notify user, refresh widget, then stop.
|
|
823
|
+
orchBatchState.phase = "paused";
|
|
824
|
+
orchBatchState.errors.push(orphanResult.userMessage);
|
|
825
|
+
updateOrchWidget();
|
|
826
|
+
ctx.ui.notify(orphanResult.userMessage, "warning");
|
|
827
|
+
return;
|
|
828
|
+
|
|
629
829
|
case "start-fresh":
|
|
630
830
|
// No orphans, no state file — proceed normally
|
|
631
831
|
break;
|
|
@@ -821,10 +1021,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
821
1021
|
});
|
|
822
1022
|
|
|
823
1023
|
pi.registerCommand("orch-resume", {
|
|
824
|
-
description: "Resume a paused or interrupted batch",
|
|
825
|
-
handler: async (
|
|
1024
|
+
description: "Resume a paused or interrupted batch: /orch-resume [--force]",
|
|
1025
|
+
handler: async (args, ctx) => {
|
|
826
1026
|
if (!requireExecCtx(ctx)) return;
|
|
827
1027
|
|
|
1028
|
+
// Parse arguments
|
|
1029
|
+
const parsed = parseResumeArgs(args);
|
|
1030
|
+
if ("error" in parsed) {
|
|
1031
|
+
ctx.ui.notify(`❌ ${parsed.error}`, "error");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
828
1035
|
// Prevent resume if a batch is actively running
|
|
829
1036
|
if (orchBatchState.phase === "executing" || orchBatchState.phase === "merging" || orchBatchState.phase === "planning") {
|
|
830
1037
|
ctx.ui.notify(
|
|
@@ -855,6 +1062,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
855
1062
|
execCtx!.workspaceConfig,
|
|
856
1063
|
execCtx!.workspaceRoot,
|
|
857
1064
|
execCtx!.pointer?.agentRoot,
|
|
1065
|
+
parsed.force,
|
|
858
1066
|
);
|
|
859
1067
|
|
|
860
1068
|
// Final widget update
|
|
@@ -1253,14 +1461,57 @@ export default function (pi: ExtensionAPI) {
|
|
|
1253
1461
|
|
|
1254
1462
|
if (!allSucceeded) return;
|
|
1255
1463
|
|
|
1256
|
-
//
|
|
1464
|
+
// ── Step 4: Post-integration cleanup & acceptance ────────
|
|
1465
|
+
// Run acceptance checks BEFORE deleting batch state so recovery
|
|
1466
|
+
// context is still available if something goes wrong.
|
|
1467
|
+
|
|
1468
|
+
// Resolve all repos to verify (all workspace repos, not just those
|
|
1469
|
+
// that had the orch branch — roadmap 2d requires "any workspace repo").
|
|
1470
|
+
const allRepos: { id: string; root: string }[] = [];
|
|
1471
|
+
if (wsConfig) {
|
|
1472
|
+
for (const [repoId, repoConf] of wsConfig.repos) {
|
|
1473
|
+
allRepos.push({ id: repoId, root: repoConf.path });
|
|
1474
|
+
}
|
|
1475
|
+
} else {
|
|
1476
|
+
allRepos.push({ id: "(default)", root: repoRoot });
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
const opId = resolveOperatorId(orchConfig);
|
|
1480
|
+
const orchPrefix = orchConfig.orchestrator.worktree_prefix;
|
|
1481
|
+
|
|
1482
|
+
// Drop batch-scoped autostash entries from all repos.
|
|
1483
|
+
// Patterns: "orch-integrate-autostash-{batchId}" (from extension.ts)
|
|
1484
|
+
// "merge-agent-autostash-w*-{batchId}" (from merge.ts)
|
|
1485
|
+
for (const repo of allRepos) {
|
|
1486
|
+
dropBatchAutostash(repo.root, batchId);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Run acceptance checks across all workspace repos.
|
|
1490
|
+
// In PR mode, the orch branch is intentionally preserved for the PR,
|
|
1491
|
+
// so we skip orch branch detection to avoid contradictory output.
|
|
1492
|
+
const skipOrchBranch = parsed.mode === "pr";
|
|
1493
|
+
const repoFindings: IntegrateCleanupRepoFindings[] = [];
|
|
1494
|
+
for (const repo of allRepos) {
|
|
1495
|
+
const findings = collectRepoCleanupFindings(
|
|
1496
|
+
repo.root, repo.id === "(default)" ? undefined : repo.id,
|
|
1497
|
+
opId, batchId, orchPrefix, resolvedOrchBranch, orchConfig,
|
|
1498
|
+
{ skipOrchBranch },
|
|
1499
|
+
);
|
|
1500
|
+
repoFindings.push(findings);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const cleanupResult = computeIntegrateCleanupResult(repoFindings);
|
|
1504
|
+
|
|
1505
|
+
// NOW delete batch state (acceptance checks are done)
|
|
1257
1506
|
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
1258
1507
|
|
|
1259
|
-
const
|
|
1508
|
+
const integrationSummary = wsConfig
|
|
1260
1509
|
? `✅ Integrated ${resolvedOrchBranch} across ${reposToIntegrate.length} repo(s).\n${repoMessages.join("\n")}\n${totalCommits} total commit(s) applied.`
|
|
1261
1510
|
: `${repoMessages[0] || "✅ Integrated."}\n${commitsAhead} commit(s) applied.`;
|
|
1262
1511
|
|
|
1263
|
-
|
|
1512
|
+
const summary = integrationSummary + "\n" + cleanupResult.report;
|
|
1513
|
+
|
|
1514
|
+
ctx.ui.notify(summary, cleanupResult.notifyLevel);
|
|
1264
1515
|
},
|
|
1265
1516
|
});
|
|
1266
1517
|
|