taskplane 0.30.5 → 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/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 +1130 -96
- 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-primer.md +182 -2
- package/extensions/taskplane/supervisor.ts +225 -17
- package/extensions/taskplane/types.ts +142 -4
- package/extensions/taskplane/worktree.ts +143 -2
- package/package.json +1 -1
|
@@ -369,6 +369,10 @@ export interface TaskRunnerConfig {
|
|
|
369
369
|
tools: string;
|
|
370
370
|
/** Package specifiers to exclude from extension forwarding (exact match). @since TP-180 */
|
|
371
371
|
excludeExtensions?: string[];
|
|
372
|
+
/** Ordered severity vocabulary for review finding-count analysis (review-boundary notifications). */
|
|
373
|
+
severityLabels?: string[];
|
|
374
|
+
/** Revision-spiral detection tuning. */
|
|
375
|
+
spiral?: import("./config-schema.ts").ReviewSpiralConfig;
|
|
372
376
|
};
|
|
373
377
|
/**
|
|
374
378
|
* Worker agent model/thinking/tools configuration.
|
|
@@ -384,6 +388,8 @@ export interface TaskRunnerConfig {
|
|
|
384
388
|
tools: string;
|
|
385
389
|
/** Package specifiers to exclude from extension forwarding (exact match). @since TP-180 */
|
|
386
390
|
excludeExtensions?: string[];
|
|
391
|
+
/** Exit-intercept supervisor-reply window in seconds (default 60; 15..1800). */
|
|
392
|
+
exitInterceptTimeoutSec?: number;
|
|
387
393
|
};
|
|
388
394
|
/** Worker agent extension exclusion list. @since TP-180 */
|
|
389
395
|
workerExcludeExtensions?: string[];
|
|
@@ -561,6 +567,13 @@ export class WorktreeError extends Error {
|
|
|
561
567
|
* catching errors for expected idempotent scenarios.
|
|
562
568
|
*/
|
|
563
569
|
export interface RemoveWorktreeResult {
|
|
570
|
+
/**
|
|
571
|
+
* #628: removal was refused because the worktree has uncommitted changes and
|
|
572
|
+
* the caller did not pass allowDirty. The worktree and branch are preserved.
|
|
573
|
+
*/
|
|
574
|
+
refusedDirty?: boolean;
|
|
575
|
+
/** Number of uncommitted paths found when refusedDirty is true. */
|
|
576
|
+
dirtyFileCount?: number;
|
|
564
577
|
/** Whether the worktree directory was removed in this call */
|
|
565
578
|
removed: boolean;
|
|
566
579
|
/** Whether the worktree was already absent (idempotent no-op) */
|
|
@@ -1132,8 +1145,15 @@ export interface WaveExecutionResult {
|
|
|
1132
1145
|
stoppedEarly: boolean;
|
|
1133
1146
|
/** Task IDs that failed (including stalled) */
|
|
1134
1147
|
failedTaskIds: string[];
|
|
1135
|
-
/** Task IDs that were skipped (due to
|
|
1148
|
+
/** Task IDs that were skipped (due to prior failure in lane, or policy) */
|
|
1136
1149
|
skippedTaskIds: string[];
|
|
1150
|
+
/**
|
|
1151
|
+
* Task IDs that did NOT run to a terminal state because the batch was PAUSED
|
|
1152
|
+
* while they were pending/holding. They remain `pending` (not skipped, not
|
|
1153
|
+
* counted) and re-execute on resume. A wave with any paused task is not
|
|
1154
|
+
* complete; the engine finalizes the batch as `paused` instead of merging.
|
|
1155
|
+
*/
|
|
1156
|
+
pausedTaskIds?: string[];
|
|
1137
1157
|
/** Task IDs that succeeded */
|
|
1138
1158
|
succeededTaskIds: string[];
|
|
1139
1159
|
/** Task IDs blocked for future waves (transitive dependents of failed tasks) */
|
|
@@ -1191,6 +1211,20 @@ export type OrchBatchPhase =
|
|
|
1191
1211
|
* - Tracks pauseSignal for /orch-pause
|
|
1192
1212
|
* - Accumulates wave results for summary
|
|
1193
1213
|
*/
|
|
1214
|
+
/**
|
|
1215
|
+
* Shared pause signal (engine ⇄ waves ⇄ lanes).
|
|
1216
|
+
*
|
|
1217
|
+
* `cause` says WHY the batch is paused: `operator` = /orch-pause (or an orphan
|
|
1218
|
+
* engine winding down), `abort` = stop-all failure policy / orch_abort,
|
|
1219
|
+
* `stop-wave` reserved for the stop-wave policy. Tier-0 retry may clear ONLY
|
|
1220
|
+
* a policy cause — one boolean let a successful retry erase an operator's
|
|
1221
|
+
* pause (Sage review of the 20260906T194514 incident).
|
|
1222
|
+
*/
|
|
1223
|
+
export interface PauseSignal {
|
|
1224
|
+
paused: boolean;
|
|
1225
|
+
cause?: "operator" | "stop-wave" | "abort" | "merge-failure";
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1194
1228
|
export interface OrchBatchRuntimeState {
|
|
1195
1229
|
/** Current execution phase */
|
|
1196
1230
|
phase: OrchBatchPhase;
|
|
@@ -1200,10 +1234,18 @@ export interface OrchBatchRuntimeState {
|
|
|
1200
1234
|
baseBranch: string;
|
|
1201
1235
|
/** Orchestrator-managed branch name (e.g., 'orch/henry-20260318T140000'). Empty = legacy mode (merge into baseBranch directly). */
|
|
1202
1236
|
orchBranch: string;
|
|
1237
|
+
/**
|
|
1238
|
+
* #610: epoch ms when this batch was integrated (manual or auto). Set in
|
|
1239
|
+
* memory by the integration path so a batch-end epilogue that was DEFERRED
|
|
1240
|
+
* behind the integrating turn is skipped instead of showing stale "ready for
|
|
1241
|
+
* integration" banners. Not persisted (the persisted checkpoint is deleted
|
|
1242
|
+
* on integration; batch-history carries its own integratedAt).
|
|
1243
|
+
*/
|
|
1244
|
+
integratedAt?: number;
|
|
1203
1245
|
/** Workspace execution mode (v2). Defaults to "repo" for backward compatibility. */
|
|
1204
1246
|
mode: WorkspaceMode;
|
|
1205
1247
|
/** Shared pause signal — set by /orch-pause, read by executeLane/executeWave */
|
|
1206
|
-
pauseSignal:
|
|
1248
|
+
pauseSignal: PauseSignal;
|
|
1207
1249
|
/** All wave results in order (grows as waves complete) */
|
|
1208
1250
|
waveResults: WaveExecutionResult[];
|
|
1209
1251
|
/** Current wave index (0-based into waves array, -1 if not started) */
|
|
@@ -2028,7 +2070,14 @@ export type EngineEventType =
|
|
|
2028
2070
|
| "merge_health_dead"
|
|
2029
2071
|
| "merge_health_stuck"
|
|
2030
2072
|
| "batch_complete"
|
|
2031
|
-
| "batch_paused"
|
|
2073
|
+
| "batch_paused"
|
|
2074
|
+
// Review boundaries (review-boundary supervisor notifications). Bridged from
|
|
2075
|
+
// the per-agent RuntimeAgentEvent review_* stream by lane-runner so the
|
|
2076
|
+
// supervisor's live events.jsonl tailer surfaces every review start/end and
|
|
2077
|
+
// can adjudicate revisions case-by-case.
|
|
2078
|
+
| "review_started"
|
|
2079
|
+
| "review_completed"
|
|
2080
|
+
| "review_failed";
|
|
2032
2081
|
|
|
2033
2082
|
/**
|
|
2034
2083
|
* Structured engine event written to `.pi/supervisor/events.jsonl`.
|
|
@@ -2101,6 +2150,31 @@ export interface EngineEvent {
|
|
|
2101
2150
|
healthStatus?: MergeHealthStatus;
|
|
2102
2151
|
/** Minutes since last activity (for merge_health_warning, merge_health_stuck) */
|
|
2103
2152
|
stalledMinutes?: number;
|
|
2153
|
+
|
|
2154
|
+
// ── Review-boundary fields (review_started/completed/failed) ────
|
|
2155
|
+
|
|
2156
|
+
/** Worker agent ID that owns the review (for review_* events) */
|
|
2157
|
+
agentId?: string;
|
|
2158
|
+
/** Step number under review (for review_* events) */
|
|
2159
|
+
reviewStep?: number;
|
|
2160
|
+
/** Review type, e.g. "plan" | "code" (for review_* events) */
|
|
2161
|
+
reviewType?: string;
|
|
2162
|
+
/** Normalized reviewer verdict (for review_completed, review_failed) */
|
|
2163
|
+
disposition?: ReviewDisposition;
|
|
2164
|
+
/** Per-step review round (Nth verdict-producing review of this step) */
|
|
2165
|
+
reviewRound?: number;
|
|
2166
|
+
/** Human/file-correlation label, e.g. "R008-code-step4" */
|
|
2167
|
+
reviewLabel?: string;
|
|
2168
|
+
/** Review file path (relative), for correlation / optional re-parse */
|
|
2169
|
+
reviewPath?: string;
|
|
2170
|
+
/** Finding counts by severity label for this review */
|
|
2171
|
+
findingCounts?: Record<string, number>;
|
|
2172
|
+
/** Converging-vs-circling trend vs the previous round */
|
|
2173
|
+
findingTrend?: "dropping" | "flat" | "rising";
|
|
2174
|
+
/** Per-severity delta (curr - prev) */
|
|
2175
|
+
findingDeltas?: Record<string, number>;
|
|
2176
|
+
/** Whether severities moved in opposing directions */
|
|
2177
|
+
findingMixed?: boolean;
|
|
2104
2178
|
}
|
|
2105
2179
|
|
|
2106
2180
|
/**
|
|
@@ -2145,7 +2219,21 @@ export type SupervisorAlertCategory =
|
|
|
2145
2219
|
| "worker-exit-intercept"
|
|
2146
2220
|
| "segment-expansion-requested"
|
|
2147
2221
|
| "segment-expansion-approved"
|
|
2148
|
-
| "segment-expansion-rejected"
|
|
2222
|
+
| "segment-expansion-rejected"
|
|
2223
|
+
// Review-boundary supervisor notifications: an actionable escalation when a
|
|
2224
|
+
// step's reviews are spiraling (repeated non-APPROVE) or the worker tripped
|
|
2225
|
+
// the order-of-operations guard (REFUSED). Delivered `steer` (urgent) so the
|
|
2226
|
+
// supervisor can adjudicate mid-run. `context.reviewInterventionKind`
|
|
2227
|
+
// distinguishes the two situations.
|
|
2228
|
+
| "review-intervention-needed";
|
|
2229
|
+
|
|
2230
|
+
/** Which review situation triggered a `review-intervention-needed` alert. */
|
|
2231
|
+
export type ReviewInterventionKind =
|
|
2232
|
+
| "revision-spiral"
|
|
2233
|
+
| "order-violation"
|
|
2234
|
+
// #626 minimal cut: a task attempted to finalize (.DONE) while a step's
|
|
2235
|
+
// LATEST review verdict is still REVISE/RETHINK — finalization was refused.
|
|
2236
|
+
| "unresolved-verdict";
|
|
2149
2237
|
|
|
2150
2238
|
/**
|
|
2151
2239
|
* Structured context payload for supervisor alerts.
|
|
@@ -2215,6 +2303,31 @@ export interface SupervisorAlertContext {
|
|
|
2215
2303
|
messageId?: string;
|
|
2216
2304
|
/** Segment expansion request ID (for segment-expansion alerts) */
|
|
2217
2305
|
expansionRequestId?: string;
|
|
2306
|
+
// ── Review-intervention fields (review-intervention-needed alerts) ────
|
|
2307
|
+
/** Which review situation triggered the escalation. */
|
|
2308
|
+
reviewInterventionKind?: ReviewInterventionKind;
|
|
2309
|
+
/** Step number under review. */
|
|
2310
|
+
reviewStep?: number;
|
|
2311
|
+
/** Review type ("plan" | "code"). */
|
|
2312
|
+
reviewType?: string;
|
|
2313
|
+
/** Per-step review round (Nth verdict-producing review of this step). */
|
|
2314
|
+
reviewRound?: number;
|
|
2315
|
+
/** Human/file-correlation label, e.g. "R008-code-step4". */
|
|
2316
|
+
reviewLabel?: string;
|
|
2317
|
+
/** Latest normalized disposition. */
|
|
2318
|
+
disposition?: ReviewDisposition;
|
|
2319
|
+
/** Recent disposition history for this step (oldest→newest, bounded). */
|
|
2320
|
+
recentDispositions?: ReviewDisposition[];
|
|
2321
|
+
/** Consecutive non-APPROVE count for this step at escalation time. */
|
|
2322
|
+
consecutiveNonApprove?: number;
|
|
2323
|
+
/** Finding counts by severity label for the latest review. */
|
|
2324
|
+
findingCounts?: Record<string, number>;
|
|
2325
|
+
/** Converging-vs-circling trend vs the previous round. */
|
|
2326
|
+
findingTrend?: "dropping" | "flat" | "rising";
|
|
2327
|
+
/** Per-severity delta (curr - prev) for the latest review. */
|
|
2328
|
+
findingDeltas?: Record<string, number>;
|
|
2329
|
+
/** Whether severities moved in opposing directions. */
|
|
2330
|
+
findingMixed?: boolean;
|
|
2218
2331
|
/** Whether partial progress was preserved (for task-failure alerts) */
|
|
2219
2332
|
partialProgress?: boolean;
|
|
2220
2333
|
/** Batch progress summary */
|
|
@@ -4211,6 +4324,31 @@ export type RuntimeAgentEventType =
|
|
|
4211
4324
|
// Exit interception (TP-172)
|
|
4212
4325
|
| "exit_intercepted";
|
|
4213
4326
|
|
|
4327
|
+
/**
|
|
4328
|
+
* Normalized outcome of a `review_step` tool call, extracted from the reviewer
|
|
4329
|
+
* verdict the tool returns to the worker. Used by the review-boundary
|
|
4330
|
+
* notification pipeline (agent-host emits it in `review_completed`; the
|
|
4331
|
+
* supervisor adjudicates on it).
|
|
4332
|
+
*
|
|
4333
|
+
* - `APPROVE` — reviewer approved the step.
|
|
4334
|
+
* - `REVISE` — changes requested (spiral-relevant).
|
|
4335
|
+
* - `RETHINK` — reconsider the approach (spiral-relevant).
|
|
4336
|
+
* - `REFUSED` — the TP-186 death-spiral guard refused to spawn a reviewer
|
|
4337
|
+
* (step prematurely marked Complete). A correctness signal,
|
|
4338
|
+
* not a normal verdict.
|
|
4339
|
+
* - `UNAVAILABLE` — the reviewer subprocess failed / produced no output. A
|
|
4340
|
+
* "reviewer broken" signal (surfaced as `review_failed`), NOT
|
|
4341
|
+
* counted toward the revision spiral.
|
|
4342
|
+
* - `UNKNOWN` — verdict could not be parsed.
|
|
4343
|
+
*/
|
|
4344
|
+
export type ReviewDisposition =
|
|
4345
|
+
| "APPROVE"
|
|
4346
|
+
| "REVISE"
|
|
4347
|
+
| "RETHINK"
|
|
4348
|
+
| "REFUSED"
|
|
4349
|
+
| "UNAVAILABLE"
|
|
4350
|
+
| "UNKNOWN";
|
|
4351
|
+
|
|
4214
4352
|
// ── Runtime V2 Path Helpers (TP-102) ─────────────────────────────────
|
|
4215
4353
|
|
|
4216
4354
|
/**
|
|
@@ -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
|
}
|