taskplane 0.28.8 → 0.29.1
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/bin/get-version.mjs +50 -0
- package/bin/taskplane.mjs +5 -8
- package/extensions/taskplane/agent-bridge-extension.ts +58 -3
- package/extensions/taskplane/agent-host.ts +11 -17
- package/extensions/taskplane/config-schema.ts +17 -12
- package/extensions/taskplane/diagnostics.ts +9 -0
- package/extensions/taskplane/engine-worker.ts +27 -0
- package/extensions/taskplane/engine.ts +235 -1
- package/extensions/taskplane/execution.ts +115 -6
- package/extensions/taskplane/extension.ts +303 -0
- package/extensions/taskplane/lane-runner.ts +74 -3
- package/extensions/taskplane/mailbox.ts +83 -0
- package/extensions/taskplane/messages.ts +19 -0
- package/extensions/taskplane/path-resolver.ts +63 -24
- package/extensions/taskplane/persistence.ts +378 -3
- package/extensions/taskplane/resume.ts +64 -7
- package/extensions/taskplane/tool-allowlist-constants.ts +37 -0
- package/extensions/taskplane/types.ts +60 -7
- package/extensions/taskplane/worktree.ts +5 -1
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +37 -0
- package/templates/agents/supervisor.md +62 -1
- package/templates/agents/task-worker.md +58 -6
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
* State persistence, serialization, orphan detection
|
|
3
3
|
* @module orch/persistence
|
|
4
4
|
*/
|
|
5
|
-
import { readFileSync, writeFileSync, existsSync, unlinkSync, renameSync, mkdirSync, appendFileSync } from "fs";
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync, unlinkSync, renameSync, mkdirSync, appendFileSync, readdirSync, statSync } from "fs";
|
|
6
6
|
import { join, dirname, basename } from "path";
|
|
7
7
|
|
|
8
8
|
import { execLog } from "./execution.ts";
|
|
9
|
-
import { BATCH_STATE_SCHEMA_VERSION, StateFileError, batchStatePath, BATCH_HISTORY_MAX_ENTRIES, defaultResilienceState, defaultBatchDiagnostics } from "./types.ts";
|
|
10
|
-
import type { BatchHistorySummary } from "./types.ts";
|
|
9
|
+
import { BATCH_STATE_SCHEMA_VERSION, StateFileError, batchStatePath, BATCH_HISTORY_MAX_ENTRIES, defaultResilienceState, defaultBatchDiagnostics, runtimeRoot, runtimeManifestPath } from "./types.ts";
|
|
10
|
+
import type { BatchHistorySummary, RuntimeAgentManifest } from "./types.ts";
|
|
11
11
|
import type { AllocatedLane, DiscoveryResult, EngineEvent, EscalationContext, LaneTaskOutcome, LaneTaskStatus, MonitorState, OrchBatchPhase, OrchBatchRuntimeState, PersistedBatchState, PersistedLaneRecord, PersistedMergeResult, PersistedSegmentRecord, PersistedTaskRecord, TaskMonitorSnapshot, Tier0RecoveryPattern, WorkspaceMode } from "./types.ts";
|
|
12
12
|
import { sleepSync } from "./worktree.ts";
|
|
13
13
|
import type { PreserveFailedLaneProgressResult } from "./worktree.ts";
|
|
@@ -2085,3 +2085,378 @@ export function emitEngineEvent(
|
|
|
2085
2085
|
}
|
|
2086
2086
|
}
|
|
2087
2087
|
|
|
2088
|
+
|
|
2089
|
+
// ── TP-187 (#539): Batch-Meta Runtime Artifact ─────────────────────
|
|
2090
|
+
//
|
|
2091
|
+
// Small JSON file written at batch-start to `.pi/runtime/<batchId>/batch-meta.json`.
|
|
2092
|
+
// Captures the wave plan and the few non-recoverable scalars (baseBranch,
|
|
2093
|
+
// orchBranch, mode, startedAt, totalWaves) so that `orch_resume(force=true)`
|
|
2094
|
+
// can deterministically reconstruct a validator-compliant PersistedBatchState
|
|
2095
|
+
// after `orch_abort()` deletes `.pi/batch-state.json`.
|
|
2096
|
+
//
|
|
2097
|
+
// Without this artifact the wave topology is unrecoverable from the surviving
|
|
2098
|
+
// runtime registry alone (manifests don't carry wave info) and a flattened
|
|
2099
|
+
// "single wave with all surviving tasks" reconstruction can violate DAG
|
|
2100
|
+
// dependency ordering. See R003 plan review.
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Schema-tagged batch metadata persisted alongside per-batch runtime state.
|
|
2104
|
+
*
|
|
2105
|
+
* @since TP-187 (#539)
|
|
2106
|
+
*/
|
|
2107
|
+
export interface BatchMetaArtifact {
|
|
2108
|
+
schemaVersion: 1;
|
|
2109
|
+
batchId: string;
|
|
2110
|
+
wavePlan: string[][];
|
|
2111
|
+
baseBranch: string;
|
|
2112
|
+
orchBranch: string;
|
|
2113
|
+
mode: WorkspaceMode;
|
|
2114
|
+
startedAt: number;
|
|
2115
|
+
totalWaves: number;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
/** Path to the batch-meta artifact for a given batch. */
|
|
2119
|
+
function batchMetaPath(stateRoot: string, batchId: string): string {
|
|
2120
|
+
return join(runtimeRoot(stateRoot, batchId), "batch-meta.json");
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
/**
|
|
2124
|
+
* Persist the wave plan and core batch metadata to the runtime artifact
|
|
2125
|
+
* directory. Best-effort: failures are logged but do NOT crash the batch.
|
|
2126
|
+
*
|
|
2127
|
+
* Called once at batch-start (after wavePlan is finalized) and re-written
|
|
2128
|
+
* whenever the wave plan mutates (segment expansion).
|
|
2129
|
+
*
|
|
2130
|
+
* @since TP-187 (#539)
|
|
2131
|
+
*/
|
|
2132
|
+
export function saveBatchMetaRuntimeArtifact(
|
|
2133
|
+
stateRoot: string,
|
|
2134
|
+
artifact: BatchMetaArtifact,
|
|
2135
|
+
): void {
|
|
2136
|
+
try {
|
|
2137
|
+
const path = batchMetaPath(stateRoot, artifact.batchId);
|
|
2138
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2139
|
+
const tmp = path + ".tmp";
|
|
2140
|
+
writeFileSync(tmp, JSON.stringify(artifact, null, 2) + "\n", "utf-8");
|
|
2141
|
+
renameSync(tmp, path);
|
|
2142
|
+
execLog("state", artifact.batchId, "persisted batch-meta runtime artifact", {
|
|
2143
|
+
waves: artifact.wavePlan.length,
|
|
2144
|
+
tasks: artifact.wavePlan.reduce((sum, w) => sum + w.length, 0),
|
|
2145
|
+
});
|
|
2146
|
+
} catch (err) {
|
|
2147
|
+
execLog("state", artifact.batchId, `batch-meta write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
/**
|
|
2152
|
+
* Load the batch-meta artifact for a given batch, or null if missing/invalid.
|
|
2153
|
+
*
|
|
2154
|
+
* @since TP-187 (#539)
|
|
2155
|
+
*/
|
|
2156
|
+
export function loadBatchMetaRuntimeArtifact(
|
|
2157
|
+
stateRoot: string,
|
|
2158
|
+
batchId: string,
|
|
2159
|
+
): BatchMetaArtifact | null {
|
|
2160
|
+
const path = batchMetaPath(stateRoot, batchId);
|
|
2161
|
+
if (!existsSync(path)) return null;
|
|
2162
|
+
try {
|
|
2163
|
+
const raw = readFileSync(path, "utf-8");
|
|
2164
|
+
const parsed = JSON.parse(raw);
|
|
2165
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
2166
|
+
const obj = parsed as Record<string, unknown>;
|
|
2167
|
+
if (obj.schemaVersion !== 1) return null;
|
|
2168
|
+
if (typeof obj.batchId !== "string" || obj.batchId !== batchId) return null;
|
|
2169
|
+
if (!Array.isArray(obj.wavePlan)) return null;
|
|
2170
|
+
for (const wave of obj.wavePlan) {
|
|
2171
|
+
if (!Array.isArray(wave)) return null;
|
|
2172
|
+
for (const taskId of wave) {
|
|
2173
|
+
if (typeof taskId !== "string") return null;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
if (typeof obj.baseBranch !== "string") return null;
|
|
2177
|
+
if (typeof obj.orchBranch !== "string") return null;
|
|
2178
|
+
if (obj.mode !== "repo" && obj.mode !== "workspace") return null;
|
|
2179
|
+
if (typeof obj.startedAt !== "number") return null;
|
|
2180
|
+
if (typeof obj.totalWaves !== "number") return null;
|
|
2181
|
+
return obj as unknown as BatchMetaArtifact;
|
|
2182
|
+
} catch {
|
|
2183
|
+
return null;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
|
|
2188
|
+
// ── TP-187 (#539): Reconstruct PersistedBatchState from runtime artifacts ──
|
|
2189
|
+
|
|
2190
|
+
/**
|
|
2191
|
+
* Result of `reconstructBatchStateFromRuntime`. On success, contains the
|
|
2192
|
+
* validator-compliant state, the selected batchId, and a human-readable note
|
|
2193
|
+
* about how the selection was made (used by resume's onNotify output). On
|
|
2194
|
+
* failure, names the missing or corrupt artifact for fail-loud reporting.
|
|
2195
|
+
*
|
|
2196
|
+
* @since TP-187 (#539)
|
|
2197
|
+
*/
|
|
2198
|
+
export type ReconstructResult =
|
|
2199
|
+
| { ok: true; state: PersistedBatchState; batchId: string; selectionNote: string }
|
|
2200
|
+
| { ok: false; error: string };
|
|
2201
|
+
|
|
2202
|
+
/**
|
|
2203
|
+
* List candidate `.pi/runtime/<batchId>/` directories newest-first by mtime,
|
|
2204
|
+
* with lex-largest tie-break for determinism.
|
|
2205
|
+
*/
|
|
2206
|
+
function listRuntimeBatchDirs(stateRoot: string): { batchId: string; mtimeMs: number }[] {
|
|
2207
|
+
const root = join(stateRoot, ".pi", "runtime");
|
|
2208
|
+
if (!existsSync(root)) return [];
|
|
2209
|
+
let entries: string[] = [];
|
|
2210
|
+
try {
|
|
2211
|
+
entries = readdirSync(root);
|
|
2212
|
+
} catch {
|
|
2213
|
+
return [];
|
|
2214
|
+
}
|
|
2215
|
+
const candidates: { batchId: string; mtimeMs: number }[] = [];
|
|
2216
|
+
for (const name of entries) {
|
|
2217
|
+
const dir = join(root, name);
|
|
2218
|
+
try {
|
|
2219
|
+
const st = statSync(dir);
|
|
2220
|
+
if (!st.isDirectory()) continue;
|
|
2221
|
+
candidates.push({ batchId: name, mtimeMs: st.mtimeMs });
|
|
2222
|
+
} catch {
|
|
2223
|
+
continue;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
candidates.sort((a, b) => {
|
|
2227
|
+
if (b.mtimeMs !== a.mtimeMs) return b.mtimeMs - a.mtimeMs;
|
|
2228
|
+
return b.batchId.localeCompare(a.batchId);
|
|
2229
|
+
});
|
|
2230
|
+
return candidates;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Read all worker manifests under `.pi/runtime/<batchId>/agents/`.
|
|
2235
|
+
*
|
|
2236
|
+
* Returns an empty array if the agents directory is missing.
|
|
2237
|
+
*/
|
|
2238
|
+
function readWorkerManifests(stateRoot: string, batchId: string): RuntimeAgentManifest[] {
|
|
2239
|
+
const agentsDir = join(runtimeRoot(stateRoot, batchId), "agents");
|
|
2240
|
+
if (!existsSync(agentsDir)) return [];
|
|
2241
|
+
let entries: string[] = [];
|
|
2242
|
+
try {
|
|
2243
|
+
entries = readdirSync(agentsDir);
|
|
2244
|
+
} catch {
|
|
2245
|
+
return [];
|
|
2246
|
+
}
|
|
2247
|
+
const manifests: RuntimeAgentManifest[] = [];
|
|
2248
|
+
for (const agentId of entries) {
|
|
2249
|
+
const manifestPath = runtimeManifestPath(stateRoot, batchId, agentId);
|
|
2250
|
+
if (!existsSync(manifestPath)) continue;
|
|
2251
|
+
try {
|
|
2252
|
+
const raw = readFileSync(manifestPath, "utf-8");
|
|
2253
|
+
const parsed = JSON.parse(raw) as RuntimeAgentManifest;
|
|
2254
|
+
if (parsed && typeof parsed === "object" && parsed.role === "worker") {
|
|
2255
|
+
manifests.push(parsed);
|
|
2256
|
+
}
|
|
2257
|
+
} catch {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
return manifests;
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
/**
|
|
2265
|
+
* Deterministically reconstruct a validator-compliant `PersistedBatchState`
|
|
2266
|
+
* from the surviving runtime artifacts after `.pi/batch-state.json` has been
|
|
2267
|
+
* deleted (typically by `orch_abort()`).
|
|
2268
|
+
*
|
|
2269
|
+
* Required artifacts: at least one `.pi/runtime/<batchId>/` directory whose
|
|
2270
|
+
* `batch-meta.json` parses cleanly AND has at least one worker manifest with
|
|
2271
|
+
* an existing worktree on disk. Anything else returns a fail-loud error so
|
|
2272
|
+
* the caller can surface a clear "no resumable state" message instead of
|
|
2273
|
+
* silently producing an invalid state.
|
|
2274
|
+
*
|
|
2275
|
+
* @since TP-187 (#539)
|
|
2276
|
+
*/
|
|
2277
|
+
export function reconstructBatchStateFromRuntime(stateRoot: string): ReconstructResult {
|
|
2278
|
+
const candidates = listRuntimeBatchDirs(stateRoot);
|
|
2279
|
+
if (candidates.length === 0) {
|
|
2280
|
+
return { ok: false, error: "no .pi/runtime/ directory or no batch subdirectories" };
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
// Try the newest batch first; if its required artifacts are missing, fall
|
|
2284
|
+
// through to the next candidate. We stop at the first batch with a parseable
|
|
2285
|
+
// batch-meta + at least one viable worker manifest.
|
|
2286
|
+
const failures: string[] = [];
|
|
2287
|
+
for (let idx = 0; idx < candidates.length; idx++) {
|
|
2288
|
+
const cand = candidates[idx];
|
|
2289
|
+
const meta = loadBatchMetaRuntimeArtifact(stateRoot, cand.batchId);
|
|
2290
|
+
if (!meta) {
|
|
2291
|
+
failures.push(`${cand.batchId}: batch-meta.json missing or invalid`);
|
|
2292
|
+
continue;
|
|
2293
|
+
}
|
|
2294
|
+
const manifests = readWorkerManifests(stateRoot, cand.batchId);
|
|
2295
|
+
if (manifests.length === 0) {
|
|
2296
|
+
failures.push(`${cand.batchId}: no worker manifests`);
|
|
2297
|
+
continue;
|
|
2298
|
+
}
|
|
2299
|
+
const workerManifestsWithWorktree = manifests.filter(m => typeof m.cwd === "string" && m.cwd.length > 0 && existsSync(m.cwd));
|
|
2300
|
+
if (workerManifestsWithWorktree.length === 0) {
|
|
2301
|
+
failures.push(`${cand.batchId}: worktree paths from manifests no longer exist on disk`);
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// TP-187 (#539) — sage post-integration follow-up: refuse reconstruction
|
|
2306
|
+
// when the runtime artifacts indicate this batch was multi-repo (segment
|
|
2307
|
+
// expansion). Reconstruction hardcodes `segments: []` and cannot recover
|
|
2308
|
+
// the per-segment topology that lives only in the deleted batch-state.
|
|
2309
|
+
// Resuming with `segments: []` for a multi-repo batch would silently lose
|
|
2310
|
+
// the expansion state and could re-execute already-done segments OR fail
|
|
2311
|
+
// dependency checks for cross-repo waves. Detection heuristic: if worker
|
|
2312
|
+
// manifests carry more than one distinct repoId, segment expansion was
|
|
2313
|
+
// active. Single-repo batches (the common case, including Taskplane's
|
|
2314
|
+
// own self-orchestration) are unaffected.
|
|
2315
|
+
{
|
|
2316
|
+
const distinctRepoIds = new Set<string>();
|
|
2317
|
+
for (const m of workerManifestsWithWorktree) {
|
|
2318
|
+
if (typeof m.repoId === "string" && m.repoId.length > 0) {
|
|
2319
|
+
distinctRepoIds.add(m.repoId);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
if (distinctRepoIds.size > 1) {
|
|
2323
|
+
failures.push(
|
|
2324
|
+
`${cand.batchId}: multi-repo batch detected (${distinctRepoIds.size} distinct repoIds: ` +
|
|
2325
|
+
`${[...distinctRepoIds].slice(0, 4).join(", ")}` +
|
|
2326
|
+
`${distinctRepoIds.size > 4 ? ", ..." : ""}); reconstruction would lose segment ` +
|
|
2327
|
+
`expansion state and is refused. Restore .pi/batch-state.json from backup or start a new batch.`
|
|
2328
|
+
);
|
|
2329
|
+
continue;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
// Build per-lane aggregation from worker manifests.
|
|
2334
|
+
const laneMap = new Map<number, { laneNumber: number; agentId: string; worktreePath: string; repoId: string; taskIds: string[] }>();
|
|
2335
|
+
for (const m of workerManifestsWithWorktree) {
|
|
2336
|
+
if (typeof m.laneNumber !== "number") continue;
|
|
2337
|
+
const lane = laneMap.get(m.laneNumber) ?? {
|
|
2338
|
+
laneNumber: m.laneNumber,
|
|
2339
|
+
agentId: m.agentId,
|
|
2340
|
+
worktreePath: m.cwd,
|
|
2341
|
+
repoId: m.repoId ?? "default",
|
|
2342
|
+
taskIds: [] as string[],
|
|
2343
|
+
};
|
|
2344
|
+
if (typeof m.taskId === "string" && m.taskId && !lane.taskIds.includes(m.taskId)) {
|
|
2345
|
+
lane.taskIds.push(m.taskId);
|
|
2346
|
+
}
|
|
2347
|
+
laneMap.set(m.laneNumber, lane);
|
|
2348
|
+
}
|
|
2349
|
+
if (laneMap.size === 0) {
|
|
2350
|
+
failures.push(`${cand.batchId}: no lane numbers in manifests`);
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// Tasks: union of taskIds across all lanes, plus any wavePlan tasks that
|
|
2355
|
+
// are not represented (they are pending, not yet executed).
|
|
2356
|
+
const knownTaskIds = new Set<string>();
|
|
2357
|
+
for (const lane of laneMap.values()) {
|
|
2358
|
+
for (const tid of lane.taskIds) knownTaskIds.add(tid);
|
|
2359
|
+
}
|
|
2360
|
+
for (const wave of meta.wavePlan) {
|
|
2361
|
+
for (const tid of wave) knownTaskIds.add(tid);
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
// Build task records with conservative defaults; resume's reconciliation
|
|
2365
|
+
// pass will re-detect succeeded tasks via `.DONE` markers and STATUS.md.
|
|
2366
|
+
const tasks: PersistedTaskRecord[] = [];
|
|
2367
|
+
const manifestByTaskId = new Map<string, RuntimeAgentManifest>();
|
|
2368
|
+
for (const m of workerManifestsWithWorktree) {
|
|
2369
|
+
if (typeof m.taskId === "string" && m.taskId) {
|
|
2370
|
+
manifestByTaskId.set(m.taskId, m);
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
for (const taskId of knownTaskIds) {
|
|
2374
|
+
const m = manifestByTaskId.get(taskId);
|
|
2375
|
+
const lane = m ? laneMap.get(m.laneNumber) : undefined;
|
|
2376
|
+
const taskRecord: PersistedTaskRecord = {
|
|
2377
|
+
taskId,
|
|
2378
|
+
taskName: taskId,
|
|
2379
|
+
taskFolder: m?.packet?.taskFolder ?? "",
|
|
2380
|
+
status: "pending",
|
|
2381
|
+
sessionName: m?.agentId ?? "",
|
|
2382
|
+
laneNumber: lane?.laneNumber ?? 0,
|
|
2383
|
+
startedAt: typeof m?.startedAt === "number" ? m.startedAt : null,
|
|
2384
|
+
endedAt: null,
|
|
2385
|
+
exitReason: "",
|
|
2386
|
+
doneFileFound: false,
|
|
2387
|
+
};
|
|
2388
|
+
if (m?.repoId) taskRecord.repoId = m.repoId;
|
|
2389
|
+
if (m?.packet?.packetRepoId) (taskRecord as Record<string, unknown>).packetRepoId = m.packet.packetRepoId;
|
|
2390
|
+
if (m?.packet?.packetTaskPath) (taskRecord as Record<string, unknown>).packetTaskPath = m.packet.packetTaskPath;
|
|
2391
|
+
tasks.push(taskRecord);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
// Build lane records.
|
|
2395
|
+
const lanes: PersistedLaneRecord[] = Array.from(laneMap.values())
|
|
2396
|
+
.sort((a, b) => a.laneNumber - b.laneNumber)
|
|
2397
|
+
.map(l => {
|
|
2398
|
+
const sessionId = l.agentId.replace(/-(worker|reviewer)$/, "");
|
|
2399
|
+
const rec: PersistedLaneRecord = {
|
|
2400
|
+
laneId: `lane-${l.laneNumber}`,
|
|
2401
|
+
laneNumber: l.laneNumber,
|
|
2402
|
+
laneSessionId: sessionId,
|
|
2403
|
+
worktreePath: l.worktreePath,
|
|
2404
|
+
branch: meta.orchBranch ? `${meta.orchBranch}-lane-${l.laneNumber}` : `lane-${l.laneNumber}`,
|
|
2405
|
+
taskIds: [...l.taskIds],
|
|
2406
|
+
};
|
|
2407
|
+
if (l.repoId && l.repoId !== "default") rec.repoId = l.repoId;
|
|
2408
|
+
return rec;
|
|
2409
|
+
});
|
|
2410
|
+
|
|
2411
|
+
const now = Date.now();
|
|
2412
|
+
const reconstructed: PersistedBatchState = {
|
|
2413
|
+
schemaVersion: BATCH_STATE_SCHEMA_VERSION,
|
|
2414
|
+
batchId: meta.batchId,
|
|
2415
|
+
phase: "stopped",
|
|
2416
|
+
baseBranch: meta.baseBranch,
|
|
2417
|
+
orchBranch: meta.orchBranch,
|
|
2418
|
+
mode: meta.mode,
|
|
2419
|
+
startedAt: meta.startedAt,
|
|
2420
|
+
endedAt: null,
|
|
2421
|
+
updatedAt: now,
|
|
2422
|
+
currentWaveIndex: 0,
|
|
2423
|
+
totalWaves: meta.totalWaves,
|
|
2424
|
+
totalTasks: tasks.length,
|
|
2425
|
+
succeededTasks: 0,
|
|
2426
|
+
failedTasks: 0,
|
|
2427
|
+
skippedTasks: 0,
|
|
2428
|
+
blockedTasks: 0,
|
|
2429
|
+
wavePlan: meta.wavePlan.map(wave => [...wave]),
|
|
2430
|
+
lanes,
|
|
2431
|
+
tasks,
|
|
2432
|
+
mergeResults: [],
|
|
2433
|
+
blockedTaskIds: [],
|
|
2434
|
+
errors: [],
|
|
2435
|
+
segments: [],
|
|
2436
|
+
lastError: null,
|
|
2437
|
+
resilience: { ...defaultResilienceState(), resumeForced: true },
|
|
2438
|
+
diagnostics: defaultBatchDiagnostics(),
|
|
2439
|
+
} as PersistedBatchState;
|
|
2440
|
+
|
|
2441
|
+
// Validate the reconstructed shape against the on-disk schema gate.
|
|
2442
|
+
try {
|
|
2443
|
+
const json = JSON.stringify(reconstructed);
|
|
2444
|
+
validatePersistedState(JSON.parse(json));
|
|
2445
|
+
} catch (err) {
|
|
2446
|
+
failures.push(`${cand.batchId}: reconstructed state failed validation: ${err instanceof Error ? err.message : String(err)}`);
|
|
2447
|
+
continue;
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
const totalCandidates = candidates.length;
|
|
2451
|
+
const selectionNote = totalCandidates === 1
|
|
2452
|
+
? `single batch in .pi/runtime/`
|
|
2453
|
+
: `selected from ${totalCandidates} candidate(s) by mtime newest-first (skipped ${idx} earlier candidate(s))`;
|
|
2454
|
+
return { ok: true, state: reconstructed, batchId: meta.batchId, selectionNote };
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
return {
|
|
2458
|
+
ok: false,
|
|
2459
|
+
error: `no reconstructable batch found in .pi/runtime/ (${failures.length} candidate(s) inspected: ${failures.slice(0, 3).join("; ")}${failures.length > 3 ? "; ..." : ""})`,
|
|
2460
|
+
};
|
|
2461
|
+
}
|
|
2462
|
+
|
|
@@ -7,7 +7,7 @@ import { join } from "path";
|
|
|
7
7
|
|
|
8
8
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
9
9
|
import { runDiscovery } from "./discovery.ts";
|
|
10
|
-
import { executeOrchBatch, resolveDisplayWaveNumber } from "./engine.ts";
|
|
10
|
+
import { executeOrchBatch, resolveDisplayWaveNumber, buildSpawnFailureAlertExtras } from "./engine.ts";
|
|
11
11
|
import { buildReviewerEnv, buildWorkerEnv, buildWorkerExcludeEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
13
|
import { selectRuntimeBackend } from "./engine.ts";
|
|
@@ -37,7 +37,7 @@ import { mergeWaveByRepo } from "./merge.ts";
|
|
|
37
37
|
import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, extractFailedRepoId, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
|
|
38
38
|
import type { CleanupGateRepoFailure } from "./messages.ts";
|
|
39
39
|
import { resolveOperatorId } from "./naming.ts";
|
|
40
|
-
import { applyPartialProgressToOutcomes, deleteBatchState, hasTaskDoneMarker, loadBatchState, persistRuntimeState, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
40
|
+
import { applyPartialProgressToOutcomes, deleteBatchState, hasTaskDoneMarker, loadBatchState, persistRuntimeState, reconstructBatchStateFromRuntime, saveBatchState, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
|
|
41
41
|
import { buildBatchProgressSnapshot, buildSupervisorSegmentFrontierSnapshot, defaultResilienceState, StateFileError } from "./types.ts";
|
|
42
42
|
import type { AllocatedLane, AllocatedTask, LaneExecutionResult, LaneTaskOutcome, LaneTaskStatus, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, ParsedTask, PersistedBatchState, PersistedLaneRecord, PersistedSegmentRecord, ReconciledTaskState, ResumeEligibility, ResumePoint, TaskRunnerConfig, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
|
|
43
43
|
import { buildDependencyGraph, resolveBaseBranch, resolveRepoRoot } from "./waves.ts";
|
|
@@ -1070,6 +1070,18 @@ export async function resumeOrchBatch(
|
|
|
1070
1070
|
force: boolean = false,
|
|
1071
1071
|
onSupervisorAlert?: import("./types.ts").SupervisorAlertCallback | null,
|
|
1072
1072
|
supervisorAutonomy: "interactive" | "supervised" | "autonomous" = "autonomous",
|
|
1073
|
+
/**
|
|
1074
|
+
* TP-187 (#538): Optional callback fired when a lane reaches a terminal
|
|
1075
|
+
* state during a resumed batch. Threaded through to executeWave so the
|
|
1076
|
+
* supervisor process keeps suppressing zombie alerts after resume too.
|
|
1077
|
+
*/
|
|
1078
|
+
onLaneTerminated?: import("./types.ts").LaneTerminatedCallback | null,
|
|
1079
|
+
/**
|
|
1080
|
+
* TP-187 (#538): Optional callback fired when a lane is freshly
|
|
1081
|
+
* (re-)allocated during resume. The supervisor uses it to lift any
|
|
1082
|
+
* carried-over zombie-alert suppression.
|
|
1083
|
+
*/
|
|
1084
|
+
onLaneRespawned?: ((laneNumber: number, agentId: string, batchId: string) => void) | null,
|
|
1073
1085
|
): Promise<void> {
|
|
1074
1086
|
const repoRoot = cwd;
|
|
1075
1087
|
// State files (.pi/batch-state.json, lane-state, etc.) belong in the workspace root,
|
|
@@ -1111,13 +1123,49 @@ export async function resumeOrchBatch(
|
|
|
1111
1123
|
}
|
|
1112
1124
|
|
|
1113
1125
|
if (!persistedState) {
|
|
1126
|
+
if (!force) {
|
|
1127
|
+
onNotify(
|
|
1128
|
+
ORCH_MESSAGES.resumeNoState(),
|
|
1129
|
+
"error",
|
|
1130
|
+
);
|
|
1131
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
1132
|
+
batchState.phase = "idle";
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
// TP-187 (#539): On force-resume, attempt deterministic reconstruction
|
|
1136
|
+
// from .pi/runtime/<batchId>/ runtime artifacts (typically left intact
|
|
1137
|
+
// by `orch_abort()` even though `.pi/batch-state.json` is deleted).
|
|
1138
|
+
const reconstruction = reconstructBatchStateFromRuntime(stateRoot);
|
|
1139
|
+
if (!reconstruction.ok) {
|
|
1140
|
+
onNotify(
|
|
1141
|
+
ORCH_MESSAGES.resumeNoStateAfterAbort(reconstruction.error, null),
|
|
1142
|
+
"error",
|
|
1143
|
+
);
|
|
1144
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
1145
|
+
batchState.phase = "idle";
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
// Successful reconstruction: persist so the rest of resumeOrchBatch
|
|
1149
|
+
// proceeds with a normal on-disk batch-state.json picture.
|
|
1114
1150
|
onNotify(
|
|
1115
|
-
ORCH_MESSAGES.
|
|
1116
|
-
"
|
|
1151
|
+
ORCH_MESSAGES.resumeReconstructed(reconstruction.batchId, reconstruction.selectionNote),
|
|
1152
|
+
"warning",
|
|
1117
1153
|
);
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1154
|
+
try {
|
|
1155
|
+
saveBatchState(JSON.stringify(reconstruction.state, null, 2), stateRoot);
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
onNotify(
|
|
1158
|
+
ORCH_MESSAGES.resumeNoStateAfterAbort(
|
|
1159
|
+
`reconstructed state could not be persisted: ${err instanceof Error ? err.message : String(err)}`,
|
|
1160
|
+
reconstruction.batchId,
|
|
1161
|
+
),
|
|
1162
|
+
"error",
|
|
1163
|
+
);
|
|
1164
|
+
// TP-040 R006: Reset phase on pre-execution early return
|
|
1165
|
+
batchState.phase = "idle";
|
|
1166
|
+
return;
|
|
1167
|
+
}
|
|
1168
|
+
persistedState = reconstruction.state;
|
|
1121
1169
|
}
|
|
1122
1170
|
|
|
1123
1171
|
// ── 2. Check eligibility ─────────────────────────────────────
|
|
@@ -2050,6 +2098,8 @@ export async function resumeOrchBatch(
|
|
|
2050
2098
|
runnerConfig.reviewer,
|
|
2051
2099
|
runnerConfig.worker,
|
|
2052
2100
|
runnerConfig.workerExcludeExtensions ?? [],
|
|
2101
|
+
onLaneTerminated ?? undefined,
|
|
2102
|
+
onLaneRespawned ?? undefined,
|
|
2053
2103
|
);
|
|
2054
2104
|
|
|
2055
2105
|
batchState.waveResults.push(waveResult);
|
|
@@ -2110,11 +2160,17 @@ export async function resumeOrchBatch(
|
|
|
2110
2160
|
const frontierSummary = segmentFrontier
|
|
2111
2161
|
? ` Segment frontier: ${segmentFrontier.terminalSegments}/${segmentFrontier.totalSegments} terminal\n`
|
|
2112
2162
|
: "";
|
|
2163
|
+
// TP-190 (#561): Mirror engine.ts emission — propagate the structured
|
|
2164
|
+
// exit category so /orch-resume task-failure alerts route through the
|
|
2165
|
+
// same supervisor playbook branches as /orch. Shared helper enforces
|
|
2166
|
+
// payload parity between the two emission sites.
|
|
2167
|
+
const { exitCategory, summaryLine: spawnFailureLine } = buildSpawnFailureAlertExtras(outcome);
|
|
2113
2168
|
emitAlert({
|
|
2114
2169
|
category: "task-failure",
|
|
2115
2170
|
summary:
|
|
2116
2171
|
`⚠️ Task failure: ${taskId}\n` +
|
|
2117
2172
|
` Exit reason: ${exitReason}\n` +
|
|
2173
|
+
spawnFailureLine +
|
|
2118
2174
|
segmentSummary +
|
|
2119
2175
|
frontierSummary +
|
|
2120
2176
|
` Lane: ${laneForTask?.laneId ?? "unknown"} (lane ${laneForTask?.laneNumber ?? "?"})\n` +
|
|
@@ -2134,6 +2190,7 @@ export async function resumeOrchBatch(
|
|
|
2134
2190
|
laneNumber: laneForTask?.laneNumber,
|
|
2135
2191
|
waveIndex: waveIdx,
|
|
2136
2192
|
exitReason,
|
|
2193
|
+
exitCategory,
|
|
2137
2194
|
partialProgress: hasPartialProgress,
|
|
2138
2195
|
batchProgress: buildBatchProgressSnapshot(batchState),
|
|
2139
2196
|
},
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight, import-free constants module for the worker tool allowlist.
|
|
3
|
+
*
|
|
4
|
+
* This module exists so that pure-data layers (`config-schema.ts`,
|
|
5
|
+
* `types.ts`) can reference the canonical `DEFAULT_WORKER_USER_TOOLS`
|
|
6
|
+
* literal without pulling `agent-host.ts`'s heavy `child_process` / `fs`
|
|
7
|
+
* imports into the schema/types graph (which would either be circular
|
|
8
|
+
* or pull subprocess plumbing into pure-data files).
|
|
9
|
+
*
|
|
10
|
+
* **Strict invariant:** this module MUST NOT have any imports beyond
|
|
11
|
+
* TypeScript built-ins. Anything more would re-introduce the very
|
|
12
|
+
* coupling this module exists to break.
|
|
13
|
+
*
|
|
14
|
+
* The companion `agent-host.ts` re-exports `DEFAULT_WORKER_USER_TOOLS`
|
|
15
|
+
* from this module for backward compatibility — existing internal
|
|
16
|
+
* imports (e.g., `execution.ts`, `worker-tools-allowlist.test.ts`)
|
|
17
|
+
* continue to work via the agent-host re-export. New code may import
|
|
18
|
+
* from either location; this module is the source of truth.
|
|
19
|
+
*
|
|
20
|
+
* `ENGINE_BRIDGE_TOOLS` and the `buildWorkerToolsAllowlist()` helper
|
|
21
|
+
* remain in `agent-host.ts` because that's where their consumers live
|
|
22
|
+
* and there is no duplication problem to solve for them.
|
|
23
|
+
*
|
|
24
|
+
* @module taskplane/tool-allowlist-constants
|
|
25
|
+
* @since TP-189 (Cluster B)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Default user-tools portion of the worker `--tools` allowlist. This is the
|
|
30
|
+
* fallback used when neither `taskRunner.worker.tools` config nor the
|
|
31
|
+
* `TASKPLANE_WORKER_TOOLS` env var supplies a value. Engine bridge tools
|
|
32
|
+
* (review_step, notify_supervisor, escalate_to_supervisor,
|
|
33
|
+
* request_segment_expansion) are appended on top by
|
|
34
|
+
* `buildWorkerToolsAllowlist()` at the spawn site — they are NOT part of
|
|
35
|
+
* this default and should not be added by callers.
|
|
36
|
+
*/
|
|
37
|
+
export const DEFAULT_WORKER_USER_TOOLS = "read,write,edit,bash,grep,find,ls";
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import type { ExitClassification, TaskExitDiagnostic } from "./diagnostics.js";
|
|
7
|
+
// TP-189 (Cluster B): single source of truth for the worker user-tools
|
|
8
|
+
// default literal. The constants module is import-free so this does NOT
|
|
9
|
+
// create a cycle (types.ts -> tool-allowlist-constants.ts is a leaf).
|
|
10
|
+
import { DEFAULT_WORKER_USER_TOOLS } from "./tool-allowlist-constants.ts";
|
|
7
11
|
|
|
8
12
|
// ── Types ────────────────────────────────────────────────────────────
|
|
9
13
|
|
|
@@ -390,11 +394,12 @@ export const DEFAULT_ORCHESTRATOR_CONFIG: OrchestratorConfig = {
|
|
|
390
394
|
},
|
|
391
395
|
merge: {
|
|
392
396
|
model: "",
|
|
393
|
-
//
|
|
394
|
-
// `
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
|
|
397
|
+
// TP-189 (Cluster B): merge default sourced from the import-free
|
|
398
|
+
// `tool-allowlist-constants.ts` module. The previous concern about
|
|
399
|
+
// importing from `agent-host.ts` (which DOES depend on types.ts and
|
|
400
|
+
// would create a cycle) no longer applies because the constant
|
|
401
|
+
// lives in a leaf module that imports nothing.
|
|
402
|
+
tools: DEFAULT_WORKER_USER_TOOLS,
|
|
398
403
|
thinking: "off",
|
|
399
404
|
verify: [],
|
|
400
405
|
order: "fewest-files-first",
|
|
@@ -1805,8 +1810,13 @@ export type Tier0RecoveryPattern =
|
|
|
1805
1810
|
*
|
|
1806
1811
|
* These are transient failures where re-running the task has a reasonable
|
|
1807
1812
|
* chance of success. Classifications NOT in this set (e.g., user_killed,
|
|
1808
|
-
* stall_timeout, context_overflow) indicate persistent
|
|
1809
|
-
* won't be fixed by retrying.
|
|
1813
|
+
* stall_timeout, context_overflow, spawn_failure) indicate persistent
|
|
1814
|
+
* problems that won't be fixed by retrying.
|
|
1815
|
+
*
|
|
1816
|
+
* **TP-190 (#561):** `spawn_failure` is intentionally excluded — spawn-stage
|
|
1817
|
+
* errors (Pi CLI not findable, worktree provisioning failure, branch
|
|
1818
|
+
* collision) are never transient and require operator action. Retrying
|
|
1819
|
+
* silently would just burn the retry budget and delay the alert.
|
|
1810
1820
|
*
|
|
1811
1821
|
* @since TP-039
|
|
1812
1822
|
*/
|
|
@@ -2127,6 +2137,25 @@ export interface SupervisorAlertContext {
|
|
|
2127
2137
|
waveIndex?: number;
|
|
2128
2138
|
/** Exit reason string (for task-failure alerts) */
|
|
2129
2139
|
exitReason?: string;
|
|
2140
|
+
/**
|
|
2141
|
+
* Structured exit category for task-failure alerts.
|
|
2142
|
+
*
|
|
2143
|
+
* Mirrors `LaneTaskOutcome.exitDiagnostic.classification` for IPC
|
|
2144
|
+
* consumption by the supervisor. Optional for backward compatibility
|
|
2145
|
+
* — absent when the engine produces a task-failure alert without
|
|
2146
|
+
* structured diagnostic data.
|
|
2147
|
+
*
|
|
2148
|
+
* Notable values consumed by the supervisor playbook:
|
|
2149
|
+
* - `"spawn_failure"` (TP-190, #561): worker process never spawned
|
|
2150
|
+
* (Pi CLI not findable, worktree provisioning error, etc.). Never
|
|
2151
|
+
* transient — the playbook MUST escalate immediately rather than
|
|
2152
|
+
* retry. When the post-wave phase-transition logic detects an
|
|
2153
|
+
* all-spawn-failed wave it also flips `batchState.phase` to
|
|
2154
|
+
* `"failed"`; that transition is independent of this alert.
|
|
2155
|
+
*
|
|
2156
|
+
* @since TP-190 (#561)
|
|
2157
|
+
*/
|
|
2158
|
+
exitCategory?: ExitClassification;
|
|
2130
2159
|
/** Segment frontier snapshot for task-failure diagnosis */
|
|
2131
2160
|
segmentFrontier?: SupervisorSegmentFrontierSnapshot;
|
|
2132
2161
|
/** Agent ID (for agent-message alerts) */
|
|
@@ -2189,6 +2218,30 @@ export interface SupervisorAlert {
|
|
|
2189
2218
|
*/
|
|
2190
2219
|
export type SupervisorAlertCallback = (alert: SupervisorAlert) => void;
|
|
2191
2220
|
|
|
2221
|
+
/**
|
|
2222
|
+
* Information about a lane that has just reached a terminal state.
|
|
2223
|
+
*
|
|
2224
|
+
* Emitted at the no-progress kill and hard-fail decision points so the
|
|
2225
|
+
* supervisor process can mark the lane as terminated and drop any further
|
|
2226
|
+
* alerts queued for it (see {@link LaneTerminatedCallback}).
|
|
2227
|
+
*
|
|
2228
|
+
* @since TP-187 (#538)
|
|
2229
|
+
*/
|
|
2230
|
+
export interface LaneTerminatedInfo {
|
|
2231
|
+
laneNumber: number;
|
|
2232
|
+
agentId: string;
|
|
2233
|
+
batchId: string;
|
|
2234
|
+
terminatedAt: number;
|
|
2235
|
+
reason: "no-progress-kill" | "hard-fail" | "supervisor-takeover";
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
/**
|
|
2239
|
+
* Callback invoked when a lane reaches a terminal state.
|
|
2240
|
+
*
|
|
2241
|
+
* @since TP-187 (#538)
|
|
2242
|
+
*/
|
|
2243
|
+
export type LaneTerminatedCallback = (info: LaneTerminatedInfo) => void;
|
|
2244
|
+
|
|
2192
2245
|
/**
|
|
2193
2246
|
* Build a batch progress snapshot from runtime state.
|
|
2194
2247
|
*
|
|
@@ -1901,7 +1901,11 @@ export function runPreflight(config: OrchestratorConfig, repoRoot?: string): Pre
|
|
|
1901
1901
|
switch (piResult.errorKind) {
|
|
1902
1902
|
case "not-found":
|
|
1903
1903
|
message = "Pi not found on PATH";
|
|
1904
|
-
|
|
1904
|
+
// Issue #560: Pi was renamed from @mariozechner to @earendil-works
|
|
1905
|
+
// in v0.74.0. Recommend the new scope for new installs; the legacy
|
|
1906
|
+
// scope still resolves at runtime via Pi's bundled aliasing if a
|
|
1907
|
+
// transitional install has it.
|
|
1908
|
+
hint = "Install Pi: npm install -g @earendil-works/pi-coding-agent (legacy: @mariozechner/pi-coding-agent)";
|
|
1905
1909
|
break;
|
|
1906
1910
|
case "timeout":
|
|
1907
1911
|
message = `Pi did not respond within ${PI_PREFLIGHT_TIMEOUT_MS / 1000}s (retried once)`;
|