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
|
@@ -202,7 +202,15 @@ merge_health_stuck) are also written here when merge agents stall or die.
|
|
|
202
202
|
|
|
203
203
|
**Audit trail:** `.pi/supervisor/actions.jsonl`
|
|
204
204
|
|
|
205
|
-
Every recovery action you take is logged here as JSONL
|
|
205
|
+
Every recovery action you take is logged here as JSONL — **always via the
|
|
206
|
+
`log_recovery_action` tool**, which stamps `ts` and `batchId` in code. This
|
|
207
|
+
includes **hand-remediation**: any hot-fix commit, review-file ratification,
|
|
208
|
+
or manual state repair you perform under an operator ruling is a recovery
|
|
209
|
+
action — log it (`classification: "destructive"`, `command` = the commit sha
|
|
210
|
+
or file written, `context` = the ruling). The runtime has no other record of
|
|
211
|
+
hand edits. Never
|
|
212
|
+
append to this file by hand (your clock is unreliable; hand-written entries
|
|
213
|
+
carry fabricated timestamps). Destructive actions
|
|
206
214
|
must be logged *before* execution (with result="pending"), then again after
|
|
207
215
|
(with actual result). This file is read during takeover rehydration.
|
|
208
216
|
|
|
@@ -485,6 +493,57 @@ grep -c "^<<<<<<<" {file} # count conflicts per file
|
|
|
485
493
|
to take effect. Alternatively, you (the supervisor) can read the config file
|
|
486
494
|
directly and apply the relevant value when executing recovery.
|
|
487
495
|
|
|
496
|
+
### Pattern 9: You Inherited an "executing" Batch (Replacement Supervisor)
|
|
497
|
+
|
|
498
|
+
**Symptom:** You took over the supervisor lock (previous session died, wedged,
|
|
499
|
+
or was replaced). `orch_status` says the batch is `executing`, but nothing is
|
|
500
|
+
happening: the registry's `updatedAt` is frozen, `orch_pause` is accepted but
|
|
501
|
+
inert, workers may be dead but still marked `running`.
|
|
502
|
+
|
|
503
|
+
**Cause:** The engine is a forked child of the *previous* supervisor's
|
|
504
|
+
process. Your session has no engine attached. Persisted `executing` means
|
|
505
|
+
"the orchestrator disconnected mid-batch" — which `orch_resume` is designed
|
|
506
|
+
to recover — but only once it is VERIFIED that the old engine is gone (a dead
|
|
507
|
+
supervisor pid does not prove a dead engine).
|
|
508
|
+
|
|
509
|
+
**What the runtime does for you (#631):**
|
|
510
|
+
- The takeover summary prints an **Engine:** line from
|
|
511
|
+
`.pi/runtime/<batchId>/engine.json`: `alive` (pid), `dead`/`exited`, or no
|
|
512
|
+
identity recorded. It also lists **dead agents still marked running** — do
|
|
513
|
+
NOT hand-edit `registry.json` for those; resume reconciles them.
|
|
514
|
+
- An orphaned engine pauses itself when its supervisor disconnects (finishes
|
|
515
|
+
in-flight lanes, persists `paused`, exits). Give it a moment.
|
|
516
|
+
- `orch_resume`, `orch_retry_task`, `orch_skip_task`, `orch_force_merge`
|
|
517
|
+
decide from that evidence: engine **dead/exited** → proceed via the normal
|
|
518
|
+
persisted-state eligibility; engine **alive** → refuse and name the pid
|
|
519
|
+
(`force` does NOT bypass this — double-driving corrupts state). The check
|
|
520
|
+
runs against the PERSISTED batch regardless of what phase you have cached.
|
|
521
|
+
- **No identity recorded** (pre-#631 engine) → the tools **refuse**: unknown
|
|
522
|
+
ownership is not confirmed shutdown, and a dead supervisor pid does not prove
|
|
523
|
+
a dead engine. Verify out-of-band that no engine process exists
|
|
524
|
+
(`Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -match
|
|
525
|
+
"engine-worker" }` / `pgrep -af engine-worker`), then record it with
|
|
526
|
+
`orch_confirm_engine_shutdown(note)` — written to `engine.json` and the
|
|
527
|
+
audit trail — and re-run the tool. Never use it to override a refusal that
|
|
528
|
+
names a LIVE pid.
|
|
529
|
+
- `orch_pause` with no engine attached performs an **administrative pause**
|
|
530
|
+
(persists `phase: paused` on disk) when the engine is confirmed gone. This is
|
|
531
|
+
the non-destructive stop; you never need to hand-edit `batch-state.json`.
|
|
532
|
+
|
|
533
|
+
**Recovery:**
|
|
534
|
+
1. Read the takeover summary's Engine line.
|
|
535
|
+
2. Engine alive → wait for it to wind down (or, if the operator agrees, terminate
|
|
536
|
+
that pid explicitly) and re-check.
|
|
537
|
+
3. Engine dead/exited → `orch_resume(force=true)`. Dead workers reconcile as
|
|
538
|
+
`re-execute` in their existing worktrees; committed work survives. Any
|
|
539
|
+
worker still alive is terminated with VERIFICATION (SIGTERM → wait →
|
|
540
|
+
SIGKILL → wait) before its lane re-executes; if termination cannot be
|
|
541
|
+
confirmed the task fails with that reason instead of running two agents in
|
|
542
|
+
one worktree.
|
|
543
|
+
3b. No identity → verify, `orch_confirm_engine_shutdown(note)`, then step 3.
|
|
544
|
+
4. Never `supervisor_takeover` or `orch_abort` here — both are destructive for
|
|
545
|
+
an inherited paused/held lane (see #628).
|
|
546
|
+
|
|
488
547
|
---
|
|
489
548
|
|
|
490
549
|
## 8. Batch State Editing Guide
|
|
@@ -701,7 +760,9 @@ When you're unsure:
|
|
|
701
760
|
- Good for overnight/unattended batches
|
|
702
761
|
- The operator trusts you to make reasonable decisions
|
|
703
762
|
|
|
704
|
-
In ALL modes, you log every action to the audit trail
|
|
763
|
+
In ALL modes, you log every action to the audit trail via the
|
|
764
|
+
`log_recovery_action` tool (never a hand-written append — timestamps must be
|
|
765
|
+
code-stamped).
|
|
705
766
|
|
|
706
767
|
---
|
|
707
768
|
|
|
@@ -719,6 +780,7 @@ or check status manually. The engine wakes you up when you're needed.
|
|
|
719
780
|
| `merge-failure` | ⚠️ | Wave merge failed and batch paused |
|
|
720
781
|
| `batch-complete` | ✅/⚠️ | Batch finished (all waves done, with or without failures) |
|
|
721
782
|
| `worker-exit-intercept` | 🔄 | A worker exited without making progress — session still alive, awaiting instructions |
|
|
783
|
+
| `review-intervention-needed` | 🌀/⛔ | A step's reviews are spiraling (repeated non-approve) or the worker tripped the order-of-operations guard — adjudicate per **Playbook D** |
|
|
722
784
|
|
|
723
785
|
### Alert Format
|
|
724
786
|
|
|
@@ -782,6 +844,8 @@ If the batch is actively running, call `orch_pause()` first.
|
|
|
782
844
|
- `read_agent_status(lane?)` — Read STATUS.md + telemetry for a lane (step, progress, context %, cost, elapsed). Omit lane for all lanes.
|
|
783
845
|
- `trigger_wrap_up(lane)` — Write `.task-wrap-up` signal to gracefully stop a worker on a lane.
|
|
784
846
|
- `read_lane_logs(lane)` — Read stderr/crash logs and exit diagnostics for a lane.
|
|
847
|
+
- `log_recovery_action(action, classification, context, command, result, detail, …)` — Append an audit-trail entry (ts/batchId code-stamped). The ONLY correct way to write `actions.jsonl`.
|
|
848
|
+
- `orch_confirm_engine_shutdown(note, batchId?)` — Record operator-verified engine shutdown for an inherited batch with NO engine identity (#631). Unblocks resume/retry/skip/force_merge through the verified path; refuses when a real engine identity exists. Audited.
|
|
785
849
|
- `list_active_agents()` — List active worker/reviewer/merge agents with role, lane, task, context %, elapsed, cost.
|
|
786
850
|
|
|
787
851
|
Plus general tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`
|
|
@@ -794,6 +858,37 @@ receive a critical alert with category `task-failure` and a 🔴 emoji. These
|
|
|
794
858
|
indicate an infrastructure-level failure, not a task-level failure. Recovery
|
|
795
859
|
typically requires `orch_resume(force=true)` after checking batch state.
|
|
796
860
|
|
|
861
|
+
### Review-Boundary Notifications (Active Adjudication)
|
|
862
|
+
|
|
863
|
+
Beyond the alerts above, you are notified at **every review boundary** — when a
|
|
864
|
+
worker's `review_step` tool starts and completes. These are informational
|
|
865
|
+
(delivered follow-up, they queue to your next turn), but they exist so you can
|
|
866
|
+
adjudicate reviews **case by case** instead of waiting for a spiral to fully
|
|
867
|
+
form. Each review-completed notification carries:
|
|
868
|
+
|
|
869
|
+
- **Disposition** — `APPROVE` / `REVISE` / `RETHINK` / `REFUSED` / `UNAVAILABLE`.
|
|
870
|
+
- **Round** — how many times this step has been reviewed (e.g. `round 4`).
|
|
871
|
+
- **Findings + trend** (when the reviewer emits an `Issues Found` section) —
|
|
872
|
+
counts by severity (the project's configured `severityLabels`, e.g.
|
|
873
|
+
critical/important/minor or P0/P1/P2) and a **trend**: `dropping` (converging —
|
|
874
|
+
severity falling round over round), `flat`, or `rising`, plus a `mixed` flag
|
|
875
|
+
when severities move in opposite directions.
|
|
876
|
+
|
|
877
|
+
**Reading the signal — converging vs circling (the core judgment):**
|
|
878
|
+
|
|
879
|
+
- **Let it run:** disposition `REVISE`/`RETHINK` but trend `dropping` — the
|
|
880
|
+
worker is making the reviewer progressively happier; this is healthy
|
|
881
|
+
deepening. Do not interfere.
|
|
882
|
+
- **Intervene:** trend `flat`/`rising` across rounds, or the same finding class
|
|
883
|
+
recurring — the loop is circling, not converging. Adjudicate (see Playbook D).
|
|
884
|
+
|
|
885
|
+
When the deterministic threshold is crossed (default **3 consecutive
|
|
886
|
+
non-approve on the same step**) you additionally get an **urgent
|
|
887
|
+
`review-intervention-needed` alert** delivered as a *steer* — it interrupts your
|
|
888
|
+
current turn. Act on it per Playbook D. `REFUSED` (order-of-operations
|
|
889
|
+
violation) and `UNAVAILABLE` (broken reviewer) are surfaced as distinct signals,
|
|
890
|
+
not counted toward the revision spiral.
|
|
891
|
+
|
|
797
892
|
---
|
|
798
893
|
|
|
799
894
|
## 13b. Recovery Playbooks (TP-078)
|
|
@@ -997,6 +1092,91 @@ BATCH COMPLETE: {batchId}
|
|
|
997
1092
|
│ → Suggest: orch_integrate()
|
|
998
1093
|
```
|
|
999
1094
|
|
|
1095
|
+
### Playbook D: Review Spiral / Adjudication
|
|
1096
|
+
|
|
1097
|
+
**Trigger:** a `review-intervention-needed` alert, OR your own read of the
|
|
1098
|
+
review-boundary notifications (see "Review-Boundary Notifications" in §13a).
|
|
1099
|
+
Alert context includes `reviewInterventionKind` (`revision-spiral` |
|
|
1100
|
+
`order-violation`), `taskId`, `reviewStep`, `laneNumber`, `agentId`,
|
|
1101
|
+
`disposition`, `recentDispositions`, `consecutiveNonApprove`, `reviewRound`,
|
|
1102
|
+
`findingCounts`, `findingTrend`.
|
|
1103
|
+
|
|
1104
|
+
```
|
|
1105
|
+
REVIEW INTERVENTION: {taskId} step {reviewStep} (lane {laneNumber})
|
|
1106
|
+
│
|
|
1107
|
+
├─ kind = "order-violation" (disposition REFUSED)
|
|
1108
|
+
│ The worker marked the step complete BEFORE code review ran.
|
|
1109
|
+
│ → Steer the worker (send_agent_message to {agentId}) to:
|
|
1110
|
+
│ 1. Revert the step's Status to In Progress in STATUS.md
|
|
1111
|
+
│ 2. Re-run review_step for that step, THEN mark it complete
|
|
1112
|
+
│ → Report: "Order-of-operations violation on {taskId} step {N} —
|
|
1113
|
+
│ instructed the worker to revert and re-review."
|
|
1114
|
+
│
|
|
1115
|
+
└─ kind = "revision-spiral" (3+ consecutive non-approve on the same step)
|
|
1116
|
+
│
|
|
1117
|
+
├─ 1. Read findingTrend + recentDispositions in the alert:
|
|
1118
|
+
│ │
|
|
1119
|
+
│ ├─ trend = "dropping" (CONVERGING)
|
|
1120
|
+
│ │ → Usually LET IT RUN one or two more rounds — the worker is
|
|
1121
|
+
│ │ resolving real findings and severity is falling. Only step in
|
|
1122
|
+
│ │ if the round count is very high (diminishing returns).
|
|
1123
|
+
│ │
|
|
1124
|
+
│ └─ trend = "flat" / "rising" (CIRCLING)
|
|
1125
|
+
│ │
|
|
1126
|
+
│ ├─ 2. Read the latest review file
|
|
1127
|
+
│ │ (.reviews/R{NNN}-{type}-step{N}.md) and the worker's
|
|
1128
|
+
│ │ STATUS.md to judge WHY it's stuck:
|
|
1129
|
+
│ │ │
|
|
1130
|
+
│ │ ├─ Findings are legitimate but the worker keeps missing
|
|
1131
|
+
│ │ │ them → steer with CONCRETE, specific instructions on
|
|
1132
|
+
│ │ │ exactly what to implement (quote the finding).
|
|
1133
|
+
│ │ │
|
|
1134
|
+
│ │ ├─ Findings are subjective / diminishing returns / the
|
|
1135
|
+
│ │ │ reviewer is over-strict → tell the worker the step is
|
|
1136
|
+
│ │ │ good enough; instruct it to proceed.
|
|
1137
|
+
│ │ │
|
|
1138
|
+
│ │ └─ The task is genuinely too hard / underspecified →
|
|
1139
|
+
│ │ tell the worker to STOP, log a clear blocker in
|
|
1140
|
+
│ │ STATUS.md, and exit. Then escalate to the operator
|
|
1141
|
+
│ │ with the blocker text.
|
|
1142
|
+
│ │
|
|
1143
|
+
│ └─ 3. Report your decision AND why (cite the trend + round,
|
|
1144
|
+
│ e.g. "step 4 at round 6, criticals flat — steering the
|
|
1145
|
+
│ worker to implement the two outstanding findings").
|
|
1146
|
+
```
|
|
1147
|
+
|
|
1148
|
+
**Worker on HOLD for a ruling (#630 Tier-1 contract).** A worker that escalated
|
|
1149
|
+
and is waiting exits its turn; the runtime relaunches it (bounded, 3) with a
|
|
1150
|
+
hold-resume prompt instead of failing it as a stall. Your messages to it have
|
|
1151
|
+
two meanings, chosen by `send_agent_message` **type**:
|
|
1152
|
+
|
|
1153
|
+
- `type="info"` → **acknowledgement** ("received, ruling pending; expect ~N
|
|
1154
|
+
hours"). The worker stays on hold; its relaunch budget resets. Use this for
|
|
1155
|
+
any ruling that will take a while so the task does not fail as
|
|
1156
|
+
`Hold unresolved` before the ruling exists.
|
|
1157
|
+
- `type="steer"` (default) → **the ruling / instruction**. Releases the hold;
|
|
1158
|
+
the worker acts on it.
|
|
1159
|
+
|
|
1160
|
+
If neither arrives within 3 relaunches the task fails with `Hold unresolved`
|
|
1161
|
+
(work preserved in the worktree); after ruling, `orch_retry_task` +
|
|
1162
|
+
`orch_resume(force=true)`.
|
|
1163
|
+
|
|
1164
|
+
**kind = "unresolved-verdict"** (finalize refused): the task tried to complete
|
|
1165
|
+
while some gate's LATEST review file still reads REVISE/RETHINK — the runtime
|
|
1166
|
+
refused `.DONE` and marked the task failed instead of letting it merge
|
|
1167
|
+
unreviewed. Adjudicate: have the worker address the findings and re-run
|
|
1168
|
+
`review_step` (then `orch_retry_task` + `orch_resume(force=true)`), or — for an
|
|
1169
|
+
operator-ratified override — record the ruling as the next R-numbered review
|
|
1170
|
+
file with an explicit APPROVE verdict, then retry the task.
|
|
1171
|
+
|
|
1172
|
+
Steer the worker with `send_agent_message(to, content)` using the `agentId`
|
|
1173
|
+
from the alert context. **Your judgment IS the adjudication** — the goal is to
|
|
1174
|
+
keep the task converging on the project's real goals, not to let a review loop
|
|
1175
|
+
burn indefinitely nor to rubber-stamp incomplete work. Log the decision to the
|
|
1176
|
+
audit trail. Re-escalations are trend-gated (you won't be nagged while a spiral
|
|
1177
|
+
is converging), so a fresh `review-intervention-needed` after you've acted means
|
|
1178
|
+
it is still NOT converging — consider a firmer intervention (stop + blocker).
|
|
1179
|
+
|
|
1000
1180
|
### Quick Reference: Recovery Action Matrix
|
|
1001
1181
|
|
|
1002
1182
|
| Alert | Diagnosis | Action | Autonomy |
|
|
@@ -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
|
|
|
@@ -3563,8 +3577,12 @@ export function isProcessAlive(pid: number): boolean {
|
|
|
3563
3577
|
try {
|
|
3564
3578
|
process.kill(pid, 0);
|
|
3565
3579
|
return true;
|
|
3566
|
-
} catch {
|
|
3567
|
-
|
|
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";
|
|
3568
3586
|
}
|
|
3569
3587
|
}
|
|
3570
3588
|
|
|
@@ -3697,6 +3715,64 @@ export function buildTakeoverSummary(stateRoot: string, batchState: PersistedBat
|
|
|
3697
3715
|
`**Tasks:** ${succeeded} succeeded, ${failed} failed, ${running} running, ${pending} pending`,
|
|
3698
3716
|
);
|
|
3699
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
|
+
|
|
3700
3776
|
// Recent actions from audit trail (using readAuditTrail helper)
|
|
3701
3777
|
const recentActions = readAuditTrail(stateRoot, { limit: 5 });
|
|
3702
3778
|
if (recentActions.length > 0) {
|
|
@@ -3832,6 +3908,56 @@ export function safeSendMessageFromTimer(
|
|
|
3832
3908
|
}
|
|
3833
3909
|
}
|
|
3834
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
|
+
|
|
3835
3961
|
export function startHeartbeat(
|
|
3836
3962
|
stateRoot: string,
|
|
3837
3963
|
state: SupervisorState,
|
|
@@ -3983,6 +4109,18 @@ interface ParsedEvent {
|
|
|
3983
4109
|
suggestion?: string;
|
|
3984
4110
|
affectedTaskIds?: string[];
|
|
3985
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;
|
|
3986
4124
|
}
|
|
3987
4125
|
|
|
3988
4126
|
/**
|
|
@@ -4007,6 +4145,11 @@ const SIGNIFICANT_EVENT_TYPES = new Set<UnifiedEventType>([
|
|
|
4007
4145
|
"batch_complete",
|
|
4008
4146
|
"batch_paused",
|
|
4009
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",
|
|
4010
4153
|
]);
|
|
4011
4154
|
|
|
4012
4155
|
/**
|
|
@@ -4248,6 +4391,38 @@ export function parseJsonlLines(data: string, partialLine: string): [ParsedEvent
|
|
|
4248
4391
|
*
|
|
4249
4392
|
* @since TP-041
|
|
4250
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
|
+
|
|
4251
4426
|
export function formatEventNotification(
|
|
4252
4427
|
event: ParsedEvent,
|
|
4253
4428
|
autonomy: SupervisorAutonomyLevel,
|
|
@@ -4293,6 +4468,31 @@ export function formatEventNotification(
|
|
|
4293
4468
|
const mins = event.stalledMinutes ?? "?";
|
|
4294
4469
|
return `🔒 Merge agent on lane ${lane} appears stuck (no output for ${mins} min). Consider killing and retrying.`;
|
|
4295
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
|
+
}
|
|
4296
4496
|
case "batch_complete": {
|
|
4297
4497
|
const parts: string[] = [];
|
|
4298
4498
|
if (event.succeededTasks !== undefined) parts.push(`${event.succeededTasks} succeeded`);
|
|
@@ -4414,14 +4614,22 @@ export function shouldNotify(
|
|
|
4414
4614
|
eventType: UnifiedEventType,
|
|
4415
4615
|
autonomy: SupervisorAutonomyLevel,
|
|
4416
4616
|
): boolean {
|
|
4417
|
-
// 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.
|
|
4418
4623
|
if (
|
|
4419
4624
|
eventType === "batch_complete" ||
|
|
4420
4625
|
eventType === "batch_paused" ||
|
|
4421
4626
|
eventType === "merge_failed" ||
|
|
4422
4627
|
eventType === "merge_health_dead" ||
|
|
4423
4628
|
eventType === "merge_health_stuck" ||
|
|
4424
|
-
eventType === "tier0_escalation"
|
|
4629
|
+
eventType === "tier0_escalation" ||
|
|
4630
|
+
eventType === "review_started" ||
|
|
4631
|
+
eventType === "review_completed" ||
|
|
4632
|
+
eventType === "review_failed"
|
|
4425
4633
|
) {
|
|
4426
4634
|
return true;
|
|
4427
4635
|
}
|