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
|
@@ -50,6 +50,12 @@ import {
|
|
|
50
50
|
rename as fsRename,
|
|
51
51
|
} from "fs/promises";
|
|
52
52
|
import { execFileSync } from "child_process";
|
|
53
|
+
import { assessEngineLiveness } from "./engine-identity.ts";
|
|
54
|
+
import {
|
|
55
|
+
isProcessAlive as registryIsProcessAlive,
|
|
56
|
+
isTerminalStatus as registryIsTerminalStatus,
|
|
57
|
+
readRegistrySnapshot,
|
|
58
|
+
} from "./process-registry.ts";
|
|
53
59
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
54
60
|
import type { Model, Api } from "@mariozechner/pi-ai";
|
|
55
61
|
import type {
|
|
@@ -2383,6 +2389,15 @@ Use these to:
|
|
|
2383
2389
|
issue using the patterns in supervisor-primer.md and take appropriate
|
|
2384
2390
|
recovery action based on your autonomy level (${autonomyLabel}).
|
|
2385
2391
|
|
|
2392
|
+
2a. **Adjudicate reviews.** You are notified at every review boundary, and get
|
|
2393
|
+
an urgent (steer) \`review-intervention-needed\` alert when a step's reviews
|
|
2394
|
+
spiral (repeated non-approve) or a worker trips the order-of-operations guard.
|
|
2395
|
+
Actively adjudicate — don't just relay to the operator. Use the finding
|
|
2396
|
+
**trend** to tell converging (\`dropping\` — let it run) from circling
|
|
2397
|
+
(\`flat\`/\`rising\` — intervene), then steer the worker to a resolution
|
|
2398
|
+
(implement the remaining valid findings, or stop and log a blocker) via
|
|
2399
|
+
\`send_agent_message\`. Follow **Playbook D** in supervisor-primer.md.
|
|
2400
|
+
|
|
2386
2401
|
3. **Keep the operator informed.** Provide clear, natural status updates.
|
|
2387
2402
|
When the operator asks "how's it going?" — read batch state and summarize.
|
|
2388
2403
|
|
|
@@ -2430,24 +2445,23 @@ ${autonomyGuidance}
|
|
|
2430
2445
|
|
|
2431
2446
|
## Audit Trail
|
|
2432
2447
|
|
|
2433
|
-
Log every recovery action
|
|
2448
|
+
Log every recovery action with the **\`log_recovery_action\` tool** — it appends
|
|
2449
|
+
to \`${actionsPath}\` with a **code-stamped timestamp and batchId**.
|
|
2434
2450
|
|
|
2435
|
-
**
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
\`\`\`
|
|
2451
|
+
**NEVER hand-write \`actions.jsonl\`** (no bash \`echo >>\`): you have no reliable
|
|
2452
|
+
clock, so hand-written entries carry fabricated timestamps and break the audit
|
|
2453
|
+
trail's integrity as evidence.
|
|
2439
2454
|
|
|
2440
2455
|
**Rules:**
|
|
2441
|
-
1. For **destructive** actions:
|
|
2442
|
-
|
|
2443
|
-
2. For **diagnostic** and **tier0_known** actions:
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
4. Use the \`bash\` tool to append entries. Example:
|
|
2447
|
-
\`echo '{"ts":"...","action":"merge_retry","classification":"tier0_known","context":"merge timeout on wave 2","command":"git merge --no-ff task/lane-2","result":"success","detail":"merged with 0 conflicts","batchId":"..."}' >> ${actionsPath}\`
|
|
2456
|
+
1. For **destructive** actions: call \`log_recovery_action(..., result="pending")\`
|
|
2457
|
+
BEFORE executing, then call again AFTER with \`"success"\` or \`"failure"\` and detail.
|
|
2458
|
+
2. For **diagnostic** and **tier0_known** actions: one call AFTER execution.
|
|
2459
|
+
3. Include \`waveIndex\`, \`laneNumber\`, \`taskId\` when relevant.
|
|
2460
|
+
4. Stick to the schema fields — do not invent ad-hoc field names.
|
|
2448
2461
|
|
|
2449
2462
|
**Why this matters:** When you're taken over by another session or the operator
|
|
2450
|
-
asks "what did you do?", the audit trail is the definitive record
|
|
2463
|
+
asks "what did you do?", the audit trail is the definitive record — and its
|
|
2464
|
+
timestamps are only trustworthy because code stamps them.
|
|
2451
2465
|
|
|
2452
2466
|
## Operational Knowledge
|
|
2453
2467
|
|
|
@@ -3200,13 +3214,18 @@ export async function deactivateSupervisor(
|
|
|
3200
3214
|
*
|
|
3201
3215
|
* @since TP-128
|
|
3202
3216
|
*/
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
)
|
|
3208
|
-
|
|
3209
|
-
|
|
3217
|
+
/**
|
|
3218
|
+
* Tear down batch-monitoring infrastructure (event tailer, heartbeat timer,
|
|
3219
|
+
* lockfile). Idempotent — safe to call multiple times.
|
|
3220
|
+
*
|
|
3221
|
+
* Extracted from `transitionToRoutingMode` (#621) so the batch-end epilogue can
|
|
3222
|
+
* stop background timers EAGERLY when it must defer its display banners past an
|
|
3223
|
+
* in-flight tool call. Stopping the heartbeat immediately prevents a
|
|
3224
|
+
* timer-origin `pi.sendMessage(..., {triggerTurn:false})` from splicing a custom
|
|
3225
|
+
* entry between an assistant `tool_use` and its `tool_result` during the defer
|
|
3226
|
+
* window.
|
|
3227
|
+
*/
|
|
3228
|
+
export function stopBatchMonitoring(state: SupervisorState): void {
|
|
3210
3229
|
// Tear down batch-monitoring infrastructure
|
|
3211
3230
|
stopEventTailer(state.eventTailer);
|
|
3212
3231
|
|
|
@@ -3223,6 +3242,16 @@ export async function transitionToRoutingMode(
|
|
|
3223
3242
|
}
|
|
3224
3243
|
}
|
|
3225
3244
|
state.lockSessionId = "";
|
|
3245
|
+
}
|
|
3246
|
+
|
|
3247
|
+
export async function transitionToRoutingMode(
|
|
3248
|
+
pi: ExtensionAPI,
|
|
3249
|
+
state: SupervisorState,
|
|
3250
|
+
routingContext: SupervisorRoutingContext,
|
|
3251
|
+
): Promise<void> {
|
|
3252
|
+
if (!state.active) return;
|
|
3253
|
+
|
|
3254
|
+
stopBatchMonitoring(state);
|
|
3226
3255
|
|
|
3227
3256
|
// Present deferred batch summary if any
|
|
3228
3257
|
if (state.pendingSummaryDeps && state.batchStateRef && state.stateRoot) {
|
|
@@ -3548,8 +3577,12 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
3548
3577
|
try {
|
|
3549
3578
|
process.kill(pid, 0);
|
|
3550
3579
|
return true;
|
|
3551
|
-
} catch {
|
|
3552
|
-
|
|
3580
|
+
} catch (err: unknown) {
|
|
3581
|
+
// #631: only ESRCH (no such process) is "dead". EPERM = exists without
|
|
3582
|
+
// signal permission; unknown errors fail closed (alive) — this feeds the
|
|
3583
|
+
// lock-takeover ownership decision.
|
|
3584
|
+
const code = (err as { code?: string } | null)?.code;
|
|
3585
|
+
return code !== "ESRCH";
|
|
3553
3586
|
}
|
|
3554
3587
|
}
|
|
3555
3588
|
|
|
@@ -3682,6 +3715,64 @@ export function buildTakeoverSummary(stateRoot: string, batchState: PersistedBat
|
|
|
3682
3715
|
`**Tasks:** ${succeeded} succeeded, ${failed} failed, ${running} running, ${pending} pending`,
|
|
3683
3716
|
);
|
|
3684
3717
|
|
|
3718
|
+
// #631: ownership evidence — is the previous ENGINE still running? A dead
|
|
3719
|
+
// supervisor pid does not imply a dead engine (forked child). This line is
|
|
3720
|
+
// what tells the replacement operator whether orch_resume can proceed.
|
|
3721
|
+
const activePhase =
|
|
3722
|
+
batchState.phase === "executing" ||
|
|
3723
|
+
batchState.phase === "launching" ||
|
|
3724
|
+
batchState.phase === "merging" ||
|
|
3725
|
+
batchState.phase === "planning";
|
|
3726
|
+
const liveness = assessEngineLiveness(stateRoot, batchState.batchId);
|
|
3727
|
+
if (liveness.identity?.taskplaneBuild) {
|
|
3728
|
+
lines.push(
|
|
3729
|
+
`**Build:** taskplane ${liveness.identity.taskplaneVersion ?? "?"} (build ${liveness.identity.taskplaneBuild}) drove this batch`,
|
|
3730
|
+
);
|
|
3731
|
+
}
|
|
3732
|
+
if (liveness.status === "alive") {
|
|
3733
|
+
lines.push(
|
|
3734
|
+
`**Engine:** ⚠️ PID ${liveness.identity!.pid} is still ALIVE (forked by supervisor PID ${liveness.identity!.supervisorPid}). ` +
|
|
3735
|
+
`This session has no engine attached; recovery tools will refuse until it exits (it pauses itself on supervisor disconnect) or is terminated.`,
|
|
3736
|
+
);
|
|
3737
|
+
} else if (liveness.status === "dead" || liveness.status === "exited") {
|
|
3738
|
+
lines.push(
|
|
3739
|
+
`**Engine:** PID ${liveness.identity!.pid} is ${liveness.status}${liveness.identity!.exitReason ? ` (${liveness.identity!.exitReason})` : ""}` +
|
|
3740
|
+
(activePhase
|
|
3741
|
+
? ` — persisted phase "${batchState.phase}" is an orphan; orch_resume(force=true) reconciles and re-drives it.`
|
|
3742
|
+
: "."),
|
|
3743
|
+
);
|
|
3744
|
+
} else if (activePhase) {
|
|
3745
|
+
lines.push(
|
|
3746
|
+
`**Engine:** no identity recorded for this batch (pre-#631 engine or never forked). Recovery tools decide from the previous supervisor's liveness.`,
|
|
3747
|
+
);
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
// #631/#630: workers the registry still calls "running" whose process is gone.
|
|
3751
|
+
// Operators should NOT hand-edit registry.json for these — resume's liveness
|
|
3752
|
+
// check (`!terminal && isProcessAlive(pid)`) already treats them as dead.
|
|
3753
|
+
try {
|
|
3754
|
+
const registry = readRegistrySnapshot(stateRoot, batchState.batchId);
|
|
3755
|
+
if (registry) {
|
|
3756
|
+
const deadRunning = Object.values(registry.agents).filter(
|
|
3757
|
+
(m) => !registryIsTerminalStatus(m.status) && !registryIsProcessAlive(m.pid),
|
|
3758
|
+
);
|
|
3759
|
+
if (deadRunning.length > 0) {
|
|
3760
|
+
lines.push("");
|
|
3761
|
+
lines.push(
|
|
3762
|
+
`**Dead agents still marked ${deadRunning[0].status} in the registry** (${deadRunning.length}):`,
|
|
3763
|
+
);
|
|
3764
|
+
for (const m of deadRunning) {
|
|
3765
|
+
lines.push(
|
|
3766
|
+
` - ${m.agentId} (${m.role}${m.taskId ? `, ${m.taskId}` : ""}) PID ${m.pid} — process gone; registry last updated ${new Date(registry.updatedAt).toISOString()}. ` +
|
|
3767
|
+
`No registry edit needed: orch_resume reconciles it (re-execute in the existing worktree).`,
|
|
3768
|
+
);
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
} catch {
|
|
3773
|
+
/* best effort */
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3685
3776
|
// Recent actions from audit trail (using readAuditTrail helper)
|
|
3686
3777
|
const recentActions = readAuditTrail(stateRoot, { limit: 5 });
|
|
3687
3778
|
if (recentActions.length > 0) {
|
|
@@ -3817,6 +3908,56 @@ export function safeSendMessageFromTimer(
|
|
|
3817
3908
|
}
|
|
3818
3909
|
}
|
|
3819
3910
|
|
|
3911
|
+
/**
|
|
3912
|
+
* Stale-safe wrapper for a UI / side-effect call that touches a possibly-stale
|
|
3913
|
+
* `ExtensionContext` or `ExtensionAPI` from a long-lived ASYNC callback —
|
|
3914
|
+
* engine-worker IPC handlers (`child.on("message"|"error"|"exit")`), widget
|
|
3915
|
+
* refresh, the batch-end epilogue, and supervisor-alert delivery (#620).
|
|
3916
|
+
*
|
|
3917
|
+
* Pi invalidates captured `ctx`/`pi` handles on session replacement/reload, and
|
|
3918
|
+
* also at the end of headless `-p` runs while the forked engine worker is still
|
|
3919
|
+
* emitting IPC. Every `ctx` accessor (`ctx.ui`, `ctx.isIdle()`, …) and the
|
|
3920
|
+
* `pi.send*` methods then call Pi's `assertActive`, which throws
|
|
3921
|
+
* `"This extension ctx is stale after session replacement or reload"`. Such a
|
|
3922
|
+
* throw inside a `child_process` / EventEmitter callback is an
|
|
3923
|
+
* `uncaughtException` that kills the supervising Pi process.
|
|
3924
|
+
*
|
|
3925
|
+
* Return value semantics (to prevent caller misuse):
|
|
3926
|
+
* - `false` → STALE only. The session is gone; caller should skip any
|
|
3927
|
+
* further UI work for this event.
|
|
3928
|
+
* - `true` → success OR a non-stale failure that was logged. NOT a
|
|
3929
|
+
* success-only signal — a `true` may mean "logged and continued".
|
|
3930
|
+
*
|
|
3931
|
+
* Any non-stale error is logged (so genuine failures still surface in
|
|
3932
|
+
* stderr/telemetry) but is deliberately NOT rethrown:
|
|
3933
|
+
* rethrowing from an IPC/EventEmitter callback would re-introduce the exact
|
|
3934
|
+
* process-fatal crash class this guards against. This mirrors the proven #597
|
|
3935
|
+
* `safeSendMessageFromTimer` contract, generalized to any thunk so it can wrap
|
|
3936
|
+
* `ctx.ui.notify`, `ctx.ui.setWidget`, and `pi.sendUserMessage` alike.
|
|
3937
|
+
*
|
|
3938
|
+
* @since #620
|
|
3939
|
+
*/
|
|
3940
|
+
export function safeCtxCallFromCallback(fn: () => void, label = "ui"): boolean {
|
|
3941
|
+
try {
|
|
3942
|
+
fn();
|
|
3943
|
+
return true;
|
|
3944
|
+
} catch (err) {
|
|
3945
|
+
if (isStaleExtensionCtx(err)) {
|
|
3946
|
+
// Pi replaced/ended the session — no live UI sink. Skip, never crash.
|
|
3947
|
+
return false;
|
|
3948
|
+
}
|
|
3949
|
+
// Not stale: surface it (so real failures are visible) but do not rethrow,
|
|
3950
|
+
// because a throw from an async IPC callback is a process-fatal uncaught
|
|
3951
|
+
// exception — the very failure mode #620 fixes.
|
|
3952
|
+
console.error(
|
|
3953
|
+
`[taskplane] ${label} call from async callback threw (non-stale): ${
|
|
3954
|
+
err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
3955
|
+
}`,
|
|
3956
|
+
);
|
|
3957
|
+
return true;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3820
3961
|
export function startHeartbeat(
|
|
3821
3962
|
stateRoot: string,
|
|
3822
3963
|
state: SupervisorState,
|
|
@@ -3968,6 +4109,18 @@ interface ParsedEvent {
|
|
|
3968
4109
|
suggestion?: string;
|
|
3969
4110
|
affectedTaskIds?: string[];
|
|
3970
4111
|
message?: string;
|
|
4112
|
+
// ── Review-boundary optional fields ──────────────────────────
|
|
4113
|
+
agentId?: string;
|
|
4114
|
+
reviewStep?: number;
|
|
4115
|
+
reviewType?: string;
|
|
4116
|
+
disposition?: string;
|
|
4117
|
+
reviewRound?: number;
|
|
4118
|
+
reviewLabel?: string;
|
|
4119
|
+
reviewPath?: string;
|
|
4120
|
+
findingCounts?: Record<string, number>;
|
|
4121
|
+
findingTrend?: "dropping" | "flat" | "rising";
|
|
4122
|
+
findingDeltas?: Record<string, number>;
|
|
4123
|
+
findingMixed?: boolean;
|
|
3971
4124
|
}
|
|
3972
4125
|
|
|
3973
4126
|
/**
|
|
@@ -3992,6 +4145,11 @@ const SIGNIFICANT_EVENT_TYPES = new Set<UnifiedEventType>([
|
|
|
3992
4145
|
"batch_complete",
|
|
3993
4146
|
"batch_paused",
|
|
3994
4147
|
"tier0_escalation",
|
|
4148
|
+
// Review boundaries: surfaced at EVERY start/end so the supervisor can
|
|
4149
|
+
// adjudicate each revision case-by-case (not coalesced into digests).
|
|
4150
|
+
"review_started",
|
|
4151
|
+
"review_completed",
|
|
4152
|
+
"review_failed",
|
|
3995
4153
|
]);
|
|
3996
4154
|
|
|
3997
4155
|
/**
|
|
@@ -4233,6 +4391,38 @@ export function parseJsonlLines(data: string, partialLine: string): [ParsedEvent
|
|
|
4233
4391
|
*
|
|
4234
4392
|
* @since TP-041
|
|
4235
4393
|
*/
|
|
4394
|
+
/**
|
|
4395
|
+
* Compact "where" descriptor for a review-boundary notification: task, step,
|
|
4396
|
+
* and lane so the supervisor can address the right worker when adjudicating.
|
|
4397
|
+
*/
|
|
4398
|
+
function reviewLocation(event: ParsedEvent): string {
|
|
4399
|
+
const parts: string[] = [];
|
|
4400
|
+
if (event.taskId) parts.push(`task ${event.taskId}`);
|
|
4401
|
+
if (typeof event.reviewStep === "number") parts.push(`step ${event.reviewStep}`);
|
|
4402
|
+
if (typeof event.laneNumber === "number") parts.push(`lane ${event.laneNumber}`);
|
|
4403
|
+
return parts.length > 0 ? parts.join(", ") : "a step";
|
|
4404
|
+
}
|
|
4405
|
+
|
|
4406
|
+
/**
|
|
4407
|
+
* Compact adjudication signals for a review notification: round, finding counts,
|
|
4408
|
+
* and severity trend — the three signals an adjudicating supervisor uses to tell
|
|
4409
|
+
* "converging (let it run)" from "circling (intervene)" at a glance.
|
|
4410
|
+
*/
|
|
4411
|
+
function reviewSignals(event: ParsedEvent): string {
|
|
4412
|
+
const bits: string[] = [];
|
|
4413
|
+
if (typeof event.reviewRound === "number") bits.push(`round ${event.reviewRound}`);
|
|
4414
|
+
if (event.findingCounts && Object.keys(event.findingCounts).length > 0) {
|
|
4415
|
+
const counts = Object.entries(event.findingCounts)
|
|
4416
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
4417
|
+
.join(" ");
|
|
4418
|
+
const trend = event.findingTrend
|
|
4419
|
+
? `, trend ${event.findingTrend}${event.findingMixed ? " (mixed)" : ""}`
|
|
4420
|
+
: "";
|
|
4421
|
+
bits.push(`findings ${counts}${trend}`);
|
|
4422
|
+
}
|
|
4423
|
+
return bits.length > 0 ? ` [${bits.join("; ")}]` : "";
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4236
4426
|
export function formatEventNotification(
|
|
4237
4427
|
event: ParsedEvent,
|
|
4238
4428
|
autonomy: SupervisorAutonomyLevel,
|
|
@@ -4278,6 +4468,31 @@ export function formatEventNotification(
|
|
|
4278
4468
|
const mins = event.stalledMinutes ?? "?";
|
|
4279
4469
|
return `🔒 Merge agent on lane ${lane} appears stuck (no output for ${mins} min). Consider killing and retrying.`;
|
|
4280
4470
|
}
|
|
4471
|
+
case "review_started": {
|
|
4472
|
+
const loc = reviewLocation(event);
|
|
4473
|
+
const typeLabel = event.reviewType ? `${event.reviewType} ` : "";
|
|
4474
|
+
return `🔍 **Review starting** — ${typeLabel}review of ${loc}.`;
|
|
4475
|
+
}
|
|
4476
|
+
case "review_completed": {
|
|
4477
|
+
const loc = reviewLocation(event);
|
|
4478
|
+
const disp = (event.disposition || "UNKNOWN").toUpperCase();
|
|
4479
|
+
const icon =
|
|
4480
|
+
disp === "APPROVE" ? "✅" : disp === "REFUSED" ? "⛔" : disp === "UNKNOWN" ? "❔" : "🔁";
|
|
4481
|
+
const tail =
|
|
4482
|
+
disp === "APPROVE"
|
|
4483
|
+
? ""
|
|
4484
|
+
: disp === "REFUSED"
|
|
4485
|
+
? " — reviewer refused (step marked complete before review). The worker must revert and re-review."
|
|
4486
|
+
: " — changes requested. Watch for repeated revisions on this step.";
|
|
4487
|
+
return `${icon} **Review ${disp}** — ${loc}.${reviewSignals(event)}${tail}`;
|
|
4488
|
+
}
|
|
4489
|
+
case "review_failed": {
|
|
4490
|
+
const loc = reviewLocation(event);
|
|
4491
|
+
return (
|
|
4492
|
+
`⚠️ **Reviewer unavailable** — ${loc}. The reviewer subprocess failed or produced no ` +
|
|
4493
|
+
`verdict (not a revision spiral — a broken-reviewer signal). Consider checking reviewer config.`
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4281
4496
|
case "batch_complete": {
|
|
4282
4497
|
const parts: string[] = [];
|
|
4283
4498
|
if (event.succeededTasks !== undefined) parts.push(`${event.succeededTasks} succeeded`);
|
|
@@ -4399,14 +4614,22 @@ export function shouldNotify(
|
|
|
4399
4614
|
eventType: UnifiedEventType,
|
|
4400
4615
|
autonomy: SupervisorAutonomyLevel,
|
|
4401
4616
|
): boolean {
|
|
4402
|
-
// Always notify for terminal/failure events regardless of autonomy
|
|
4617
|
+
// Always notify for terminal/failure events regardless of autonomy.
|
|
4618
|
+
// Review boundaries are included on purpose: the supervisor must be informed
|
|
4619
|
+
// at EVERY review start/end in ALL autonomy levels — in autonomous mode this
|
|
4620
|
+
// is precisely when it adjudicates revisions case-by-case (operator-as-alarm
|
|
4621
|
+
// is the opposite of autonomous execution). Suppressing review_* in
|
|
4622
|
+
// autonomous mode would silently disable the feature where it matters most.
|
|
4403
4623
|
if (
|
|
4404
4624
|
eventType === "batch_complete" ||
|
|
4405
4625
|
eventType === "batch_paused" ||
|
|
4406
4626
|
eventType === "merge_failed" ||
|
|
4407
4627
|
eventType === "merge_health_dead" ||
|
|
4408
4628
|
eventType === "merge_health_stuck" ||
|
|
4409
|
-
eventType === "tier0_escalation"
|
|
4629
|
+
eventType === "tier0_escalation" ||
|
|
4630
|
+
eventType === "review_started" ||
|
|
4631
|
+
eventType === "review_completed" ||
|
|
4632
|
+
eventType === "review_failed"
|
|
4410
4633
|
) {
|
|
4411
4634
|
return true;
|
|
4412
4635
|
}
|
|
@@ -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
|
/**
|