taskplane 0.30.4 → 0.30.6
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/extensions/taskplane/agent-bridge-extension.ts +66 -8
- package/extensions/taskplane/agent-host.ts +170 -9
- package/extensions/taskplane/config-loader.ts +9 -0
- package/extensions/taskplane/config-schema.ts +47 -1
- package/extensions/taskplane/context-repair.ts +158 -0
- package/extensions/taskplane/diagnostic-reports.ts +109 -6
- package/extensions/taskplane/diagnostics.ts +3 -0
- package/extensions/taskplane/engine-identity.ts +401 -0
- package/extensions/taskplane/engine-worker.ts +59 -3
- package/extensions/taskplane/engine.ts +137 -17
- package/extensions/taskplane/execution.ts +89 -8
- package/extensions/taskplane/extension.ts +1288 -241
- package/extensions/taskplane/git.ts +74 -0
- package/extensions/taskplane/lane-runner.ts +971 -95
- package/extensions/taskplane/process-registry.ts +7 -2
- package/extensions/taskplane/resume.ts +559 -23
- package/extensions/taskplane/review-analysis.ts +450 -0
- package/extensions/taskplane/segment-recovery.ts +192 -0
- package/extensions/taskplane/supervisor-dispatch.ts +103 -0
- package/extensions/taskplane/supervisor-primer.md +182 -2
- package/extensions/taskplane/supervisor.ts +247 -24
- package/extensions/taskplane/types.ts +142 -4
- package/extensions/taskplane/worktree.ts +143 -2
- package/package.json +1 -1
|
@@ -9,7 +9,8 @@ import { join, basename, resolve } from "path";
|
|
|
9
9
|
import { execLog } from "./execution.ts";
|
|
10
10
|
import { runGit } from "./git.ts";
|
|
11
11
|
import { resolveOperatorId } from "./naming.ts";
|
|
12
|
-
import { DEFAULT_ORCHESTRATOR_CONFIG, WorktreeError } from "./types.ts";
|
|
12
|
+
import { DEFAULT_ORCHESTRATOR_CONFIG, WorktreeError, runtimeRoot } from "./types.ts";
|
|
13
|
+
import { assessEngineLiveness } from "./engine-identity.ts";
|
|
13
14
|
import type {
|
|
14
15
|
AllocatedLane,
|
|
15
16
|
BulkWorktreeError,
|
|
@@ -716,10 +717,48 @@ export function runWindowsCmdRd(absolutePath: string): {
|
|
|
716
717
|
* @throws WorktreeError with WORKTREE_REMOVE_FAILED for terminal (non-retriable) errors
|
|
717
718
|
* @throws WorktreeError with WORKTREE_BRANCH_DELETE_FAILED if branch cleanup fails
|
|
718
719
|
*/
|
|
720
|
+
/**
|
|
721
|
+
* #628: does this worktree have uncommitted changes? Returns the count, 0 for
|
|
722
|
+
* clean, or null when it CANNOT be assessed — e.g. the path is a corrupted or
|
|
723
|
+
* orphaned worktree whose git context resolves to the PARENT repo (running
|
|
724
|
+
* `git status` there would report the parent's state, a false positive).
|
|
725
|
+
* Callers treat null as "proceed with removal" so corruption-recovery paths
|
|
726
|
+
* keep working; only a confirmed-dirty, functioning worktree refuses.
|
|
727
|
+
*/
|
|
728
|
+
function worktreeUncommittedCount(worktreePath: string): number | null {
|
|
729
|
+
const top = runGit(["rev-parse", "--show-toplevel"], worktreePath);
|
|
730
|
+
if (!top.ok) return null;
|
|
731
|
+
const norm = (p: string) => {
|
|
732
|
+
// realpathSync.native expands Windows 8.3 short names (HENRYL~1 → HenryLach)
|
|
733
|
+
// so git's long-form output compares equal to a short-form input path.
|
|
734
|
+
let r: string;
|
|
735
|
+
try {
|
|
736
|
+
r = realpathSync.native(p.trim());
|
|
737
|
+
} catch {
|
|
738
|
+
r = resolve(p.trim());
|
|
739
|
+
}
|
|
740
|
+
r = r.replace(/[\\/]+/g, "/");
|
|
741
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
742
|
+
};
|
|
743
|
+
if (norm(top.stdout) !== norm(worktreePath)) return null; // not this dir's own repo context
|
|
744
|
+
const st = runGit(["status", "--porcelain"], worktreePath);
|
|
745
|
+
if (!st.ok) return null;
|
|
746
|
+
const t = st.stdout.trim();
|
|
747
|
+
return t.length === 0 ? 0 : t.split(/\r?\n/).length;
|
|
748
|
+
}
|
|
749
|
+
|
|
719
750
|
export function removeWorktree(
|
|
720
751
|
worktree: WorktreeInfo,
|
|
721
752
|
repoRoot: string,
|
|
722
753
|
targetBranch?: string,
|
|
754
|
+
options?: {
|
|
755
|
+
/**
|
|
756
|
+
* #628: permit removal even when the worktree has uncommitted changes.
|
|
757
|
+
* Only pass true when the caller has ALREADY preserved progress (commit,
|
|
758
|
+
* stash, or progress branch). Default false = refuse when dirty.
|
|
759
|
+
*/
|
|
760
|
+
allowDirty?: boolean;
|
|
761
|
+
},
|
|
723
762
|
): RemoveWorktreeResult {
|
|
724
763
|
const { path: worktreePath, branch } = worktree;
|
|
725
764
|
|
|
@@ -755,6 +794,33 @@ export function removeWorktree(
|
|
|
755
794
|
};
|
|
756
795
|
}
|
|
757
796
|
|
|
797
|
+
// ── #628: uncommitted-work guard ────────────────────────────
|
|
798
|
+
// Removal uses `git worktree remove --force`, which destroys uncommitted
|
|
799
|
+
// changes. In the reported incident a takeover path removed a held lane's
|
|
800
|
+
// worktree and the worker's uncommitted files were lost (recovered only via
|
|
801
|
+
// dangling objects). Safety invariant: NEVER remove a worktree with
|
|
802
|
+
// uncommitted changes unless the caller explicitly opts in after preserving
|
|
803
|
+
// progress. Refusal is non-fatal — callers already handle removed:false.
|
|
804
|
+
if (pathExists && !options?.allowDirty) {
|
|
805
|
+
const dirtyFileCount = worktreeUncommittedCount(worktreePath);
|
|
806
|
+
if (dirtyFileCount !== null && dirtyFileCount > 0) {
|
|
807
|
+
execLog(
|
|
808
|
+
"cleanup",
|
|
809
|
+
"worktree",
|
|
810
|
+
`REFUSED to remove worktree with ${dirtyFileCount} uncommitted change(s) — commit/stash or pass allowDirty after preserving progress (#628)`,
|
|
811
|
+
{ path: worktreePath, branch },
|
|
812
|
+
);
|
|
813
|
+
return {
|
|
814
|
+
removed: false,
|
|
815
|
+
alreadyRemoved: false,
|
|
816
|
+
branchDeleted: false,
|
|
817
|
+
branchPreserved: true,
|
|
818
|
+
refusedDirty: true,
|
|
819
|
+
dirtyFileCount,
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
758
824
|
// ── Attempt removal with retry/backoff ───────────────────────
|
|
759
825
|
const RETRY_DELAYS_MS = [1000, 2000, 4000, 8000, 16000];
|
|
760
826
|
const MAX_ATTEMPTS = RETRY_DELAYS_MS.length + 1; // first attempt + retries
|
|
@@ -2105,9 +2171,31 @@ export function forceCleanupWorktree(
|
|
|
2105
2171
|
worktree: WorktreeInfo,
|
|
2106
2172
|
repoRoot: string,
|
|
2107
2173
|
batchId: string,
|
|
2174
|
+
options?: {
|
|
2175
|
+
/** #628: permit force-removal even with uncommitted changes. Only after preserving progress. */
|
|
2176
|
+
allowDirty?: boolean;
|
|
2177
|
+
},
|
|
2108
2178
|
): void {
|
|
2109
2179
|
const { path: worktreePath, branch, laneNumber } = worktree;
|
|
2110
2180
|
|
|
2181
|
+
// ── #628: uncommitted-work guard (same invariant as removeWorktree) ───
|
|
2182
|
+
// This is the raw-rmSync last resort — without the guard it silently
|
|
2183
|
+
// destroys uncommitted worker files (e.g. batch-start cleanup of a prior
|
|
2184
|
+
// batch's held lane). "Force" here means stubborn-removal MECHANICS
|
|
2185
|
+
// (Windows reserved names), not overriding the data-safety invariant.
|
|
2186
|
+
if (existsSync(worktreePath) && !options?.allowDirty) {
|
|
2187
|
+
const dirtyFileCount = worktreeUncommittedCount(worktreePath);
|
|
2188
|
+
if (dirtyFileCount !== null && dirtyFileCount > 0) {
|
|
2189
|
+
execLog(
|
|
2190
|
+
"cleanup",
|
|
2191
|
+
`lane-${laneNumber}`,
|
|
2192
|
+
`REFUSED force-cleanup: worktree has ${dirtyFileCount} uncommitted change(s) — preserve progress first (#628)`,
|
|
2193
|
+
{ path: worktreePath, branch, batchId },
|
|
2194
|
+
);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2111
2199
|
// Step 1: Force-remove the directory
|
|
2112
2200
|
if (existsSync(worktreePath)) {
|
|
2113
2201
|
try {
|
|
@@ -2615,6 +2703,33 @@ export interface StaleBranchCleanupResult {
|
|
|
2615
2703
|
deletedSavedBranches: string[];
|
|
2616
2704
|
/** Branches that failed to delete (best-effort) */
|
|
2617
2705
|
failedDeletes: string[];
|
|
2706
|
+
/**
|
|
2707
|
+
* #631: branches of OTHER batches that were kept because that batch's engine
|
|
2708
|
+
* is alive, or its ownership is unknown (runtime dir present, no identity).
|
|
2709
|
+
*/
|
|
2710
|
+
skippedOwnedBranches?: string[];
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* #631: may a lane branch belonging to ANOTHER batch be swept as an orphan?
|
|
2715
|
+
* The TP-051 operator-wide sweep is kept (orphans from finished batches do
|
|
2716
|
+
* accumulate), but never for a batch whose engine is alive, nor for one whose
|
|
2717
|
+
* ownership is unknown (a runtime dir exists with no engine identity — a
|
|
2718
|
+
* pre-#631 engine of unknown state). No runtime dir at all = no engine
|
|
2719
|
+
* evidence anywhere → a pure leftover → sweepable.
|
|
2720
|
+
*/
|
|
2721
|
+
function otherBatchBranchSweepable(ownershipRoot: string, otherBatchId: string): boolean {
|
|
2722
|
+
const liveness = assessEngineLiveness(ownershipRoot, otherBatchId);
|
|
2723
|
+
if (liveness.status === "alive") return false;
|
|
2724
|
+
if (liveness.status === "none" && existsSync(runtimeRoot(ownershipRoot, otherBatchId)))
|
|
2725
|
+
return false;
|
|
2726
|
+
return true;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
/** `task/{opId}-lane-{N}-{batchId}` / `saved/task/…-{batchId}` → batchId (last dash segment). */
|
|
2730
|
+
function laneBranchBatchId(branch: string): string | null {
|
|
2731
|
+
const m = /-lane-\d+-([A-Za-z0-9._]+)$/.exec(branch);
|
|
2732
|
+
return m ? m[1] : null;
|
|
2618
2733
|
}
|
|
2619
2734
|
|
|
2620
2735
|
/**
|
|
@@ -2645,10 +2760,34 @@ export function deleteStaleBranches(
|
|
|
2645
2760
|
repoRoot: string,
|
|
2646
2761
|
opId: string,
|
|
2647
2762
|
batchId: string,
|
|
2763
|
+
/**
|
|
2764
|
+
* #631: root under which `.pi/runtime/<batchId>/engine.json` lives (workspace
|
|
2765
|
+
* root in workspace mode). Defaults to repoRoot.
|
|
2766
|
+
*/
|
|
2767
|
+
ownershipRoot: string = repoRoot,
|
|
2648
2768
|
): StaleBranchCleanupResult {
|
|
2649
2769
|
const deletedTaskBranches: string[] = [];
|
|
2650
2770
|
const deletedSavedBranches: string[] = [];
|
|
2651
2771
|
const failedDeletes: string[] = [];
|
|
2772
|
+
const skippedOwnedBranches: string[] = [];
|
|
2773
|
+
// #631: a lane branch of another batch is only swept when that batch's engine
|
|
2774
|
+
// is verifiably gone and its ownership is not unknown.
|
|
2775
|
+
const guardOtherBatch = (branch: string): boolean => {
|
|
2776
|
+
const other = laneBranchBatchId(branch);
|
|
2777
|
+
if (!other || other === batchId) return true;
|
|
2778
|
+
if (otherBatchBranchSweepable(ownershipRoot, other)) return true;
|
|
2779
|
+
skippedOwnedBranches.push(branch);
|
|
2780
|
+
execLog(
|
|
2781
|
+
"cleanup",
|
|
2782
|
+
batchId,
|
|
2783
|
+
`kept lane branch of another batch (engine alive or ownership unknown, #631)`,
|
|
2784
|
+
{
|
|
2785
|
+
branch,
|
|
2786
|
+
otherBatchId: other,
|
|
2787
|
+
},
|
|
2788
|
+
);
|
|
2789
|
+
return false;
|
|
2790
|
+
};
|
|
2652
2791
|
|
|
2653
2792
|
// 1. Delete task/{opId}-lane-* branches
|
|
2654
2793
|
const taskBranchResult = runGit(["branch", "--list", `task/${opId}-lane-*`], repoRoot);
|
|
@@ -2659,6 +2798,7 @@ export function deleteStaleBranches(
|
|
|
2659
2798
|
.filter(Boolean);
|
|
2660
2799
|
|
|
2661
2800
|
for (const branch of branches) {
|
|
2801
|
+
if (!guardOtherBatch(branch)) continue;
|
|
2662
2802
|
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2663
2803
|
if (deleted) {
|
|
2664
2804
|
deletedTaskBranches.push(branch);
|
|
@@ -2677,6 +2817,7 @@ export function deleteStaleBranches(
|
|
|
2677
2817
|
.filter(Boolean);
|
|
2678
2818
|
|
|
2679
2819
|
for (const branch of branches) {
|
|
2820
|
+
if (!guardOtherBatch(branch)) continue;
|
|
2680
2821
|
const deleted = deleteBranchBestEffort(branch, repoRoot);
|
|
2681
2822
|
if (deleted) {
|
|
2682
2823
|
deletedSavedBranches.push(branch);
|
|
@@ -2721,5 +2862,5 @@ export function deleteStaleBranches(
|
|
|
2721
2862
|
});
|
|
2722
2863
|
}
|
|
2723
2864
|
|
|
2724
|
-
return { deletedTaskBranches, deletedSavedBranches, failedDeletes };
|
|
2865
|
+
return { deletedTaskBranches, deletedSavedBranches, failedDeletes, skippedOwnedBranches };
|
|
2725
2866
|
}
|