@maxiaochao/pi-toolkit 0.10.21 → 0.10.23

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/package.json +1 -1
  3. package/packages/herdr-subagents/CHANGELOG.md +22 -0
  4. package/packages/herdr-subagents/README.md +16 -8
  5. package/packages/herdr-subagents/RESUME-LIFECYCLE-DESIGN.md +84 -0
  6. package/packages/herdr-subagents/TESTING.md +13 -3
  7. package/packages/herdr-subagents/extensions/herdr-subagents.ts +4 -17
  8. package/packages/herdr-subagents/extensions/subagent-report.ts +2 -2
  9. package/packages/herdr-subagents/package.json +1 -1
  10. package/packages/herdr-subagents/src/batch-runner.ts +56 -15
  11. package/packages/herdr-subagents/src/blocking.ts +115 -72
  12. package/packages/herdr-subagents/src/continue-run.ts +22 -7
  13. package/packages/herdr-subagents/src/fleet.ts +2 -1
  14. package/packages/herdr-subagents/src/herdr.ts +10 -1
  15. package/packages/herdr-subagents/src/historical-resume.ts +184 -0
  16. package/packages/herdr-subagents/src/history.ts +5 -131
  17. package/packages/herdr-subagents/src/live-agent.ts +99 -35
  18. package/packages/herdr-subagents/src/parameters.ts +2 -2
  19. package/packages/herdr-subagents/src/protocol.ts +3 -1
  20. package/packages/herdr-subagents/src/reconcile.ts +156 -135
  21. package/packages/herdr-subagents/src/records.ts +50 -32
  22. package/packages/herdr-subagents/src/resume-state.ts +46 -0
  23. package/packages/herdr-subagents/src/run-pane-allocator.ts +39 -37
  24. package/packages/herdr-subagents/src/run-state.ts +120 -0
  25. package/packages/herdr-subagents/src/status.ts +0 -6
  26. package/packages/herdr-subagents/src/tab-cleanup.ts +16 -16
  27. package/packages/herdr-subagents/src/tool.ts +1 -54
  28. package/packages/herdr-subagents/src/validation.ts +1 -1
  29. package/packages/herdr-subagents/tests/extension-contract.ts +4 -4
  30. package/packages/herdr-subagents/tests/run-tests.mjs +579 -60
  31. package/packages/herdr-subagents/src/runner.ts +0 -234
@@ -1,11 +1,11 @@
1
- import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
- import { basename, join } from "node:path";
1
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { basename, dirname, join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { getEventListeners } from "node:events";
5
5
  import { visibleWidth } from "@earendil-works/pi-tui";
6
6
  import { discoverPackageAgents, parseAgentMarkdown } from "../src/personas.ts";
7
7
  import { loadSubagentConfiguration } from "../src/config.ts";
8
- import { runHerdrSubagents } from "../src/runner.ts";
8
+ import { runHerdrSubagentsBatch } from "../src/batch-runner.ts";
9
9
  import { executeHerdrSubagents } from "../src/tool.ts";
10
10
  import { activityCounts, focusActiveTask } from "../src/monitor.ts";
11
11
  import { RunDispatcher } from "../src/dispatcher.ts";
@@ -23,8 +23,28 @@ import { applyTaskStatus, isSettledRunStatus, projectTaskActivity, readTaskRecor
23
23
  import { RunPaneAllocator } from "../src/run-pane-allocator.ts";
24
24
  import { aggregateTaskStatus } from "../src/run-status.ts";
25
25
  import { isAbortError } from "../src/errors.ts";
26
+ import { acquireDirectoryLock, withRunStateLock } from "../src/run-state.ts";
26
27
  import registerSubagentReport from "../extensions/subagent-report.ts";
27
28
 
29
+ /** Drives one task through the batch runner and projects its single result like a task-scoped run. */
30
+ async function runSingleTask(prompt, { label, agent, model, tools, systemPrompt, onStalled, ...options } = {}) {
31
+ const name = label ?? "task";
32
+ const batch = await runHerdrSubagentsBatch([{
33
+ name, prompt, agent: agent ?? "worker", access: "write", skills: [],
34
+ ...(model ? { model } : {}),
35
+ ...(tools ? { tools } : {}),
36
+ systemPrompt: systemPrompt ?? "You are a focused worker subagent.",
37
+ }], {
38
+ label: name, concurrency: 1, maxConcurrency: 1, ...options,
39
+ onUpdate: (partial) => {
40
+ if (onStalled && partial.summary.includes("appears stalled")) onStalled(partial.summary);
41
+ options.onUpdate?.(partial);
42
+ },
43
+ });
44
+ const task = batch.tasks[0];
45
+ return { ...batch, status: task.status, summary: task.summary, documents: task.documents, errorMessage: task.error ?? task.question, recordDirectory: task.recordDirectory, output: task.summary };
46
+ }
47
+
28
48
  let passed = 0;
29
49
  let failed = 0;
30
50
  function check(name, condition, detail = "") {
@@ -55,6 +75,7 @@ function fakeHerdr(mode = "completed") {
55
75
  let active = 0;
56
76
  let maxActive = 0;
57
77
  let taskRuns = 0;
78
+ let promptBarrier;
58
79
  const agents = new Map();
59
80
  const writeAttemptReport = (directory, report) => {
60
81
  const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
@@ -67,6 +88,18 @@ function fakeHerdr(mode = "completed") {
67
88
  return {
68
89
  calls,
69
90
  get maxActive() { return maxActive; },
91
+ expectConcurrentPrompts(count) {
92
+ let release;
93
+ const barrier = { count, arrivals: 0, promise: new Promise((resolve) => { release = resolve; }) };
94
+ const finish = () => {
95
+ if (promptBarrier !== barrier) return;
96
+ promptBarrier = undefined;
97
+ clearTimeout(barrier.timeout);
98
+ release();
99
+ };
100
+ barrier.finish = finish;
101
+ promptBarrier = barrier;
102
+ },
70
103
  async createTab(request) {
71
104
  calls.push(["createTab", request]);
72
105
  taskDirectory = request.env.PI_HERDR_SUBAGENTS_TASK_DIR;
@@ -84,7 +117,23 @@ function fakeHerdr(mode = "completed") {
84
117
  const runNumber = ++taskRuns;
85
118
  active += 1;
86
119
  maxActive = Math.max(maxActive, active);
87
- await new Promise((resolve) => setTimeout(resolve, request.prompt.includes("slow") ? 15 : 2));
120
+ const barrier = promptBarrier;
121
+ if (barrier) {
122
+ barrier.timeout ??= setTimeout(barrier.finish, 10_000);
123
+ barrier.arrivals += 1;
124
+ if (barrier.arrivals >= barrier.count) barrier.finish();
125
+ await barrier.promise;
126
+ }
127
+ await new Promise((resolve) => setTimeout(resolve, mode === "ui-blocked" ? 30 : request.prompt.includes("slow") ? 15 : 2));
128
+ if (mode === "delivery-unknown-late") {
129
+ const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
130
+ const attempts = join(directory, "attempts");
131
+ mkdirSync(attempts, { recursive: true });
132
+ writeFileSync(join(attempts, `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, state: "working" }));
133
+ setTimeout(() => writeAttemptReport(directory, { status: "completed", summary: "Late authoritative report completed.", documents: [] }), 25);
134
+ active -= 1;
135
+ throw new Error("agent_prompt_stalled: delivery acknowledgement timed out");
136
+ }
88
137
  if (mode === "delayed-report") {
89
138
  const task = JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
90
139
  const attempts = join(directory, "attempts");
@@ -117,7 +166,7 @@ function fakeHerdr(mode = "completed") {
117
166
  }
118
167
  if (mode === "post-fail" || (mode === "blocked-then-post-fail" && runNumber > 1)) { active -= 1; throw new Error("connection lost after prompt submission"); }
119
168
  writeFileSync(join(directory, "session.jsonl"), `${JSON.stringify({ type: "session", version: 3, id: request.target, cwd: root })}\n`);
120
- if (mode === "completed" || mode === "close-failed") {
169
+ if (mode === "completed" || mode === "close-failed" || mode === "ui-blocked") {
121
170
  const failed = request.prompt.includes("FAIL");
122
171
  writeFileSync(join(directory, "result.md"), request.prompt.startsWith("Review") ? "FULL RESULT THAT STAYS OUT OF PARENT CONTEXT" : `FULL ${request.prompt}`);
123
172
  writeAttemptReport(directory, {
@@ -136,16 +185,22 @@ function fakeHerdr(mode = "completed") {
136
185
  });
137
186
  } else if (mode === "malformed") {
138
187
  writeAttemptReport(directory, "{truncated");
139
- } else if (mode === "blocked" || mode === "blocked-twice" || mode === "blocked-then-post-fail") {
140
- const needsInput = ((mode === "blocked" || mode === "blocked-then-post-fail") && runNumber === 1) || (mode === "blocked-twice" && (runNumber === 1 || runNumber === 3));
188
+ } else if (mode === "blocked" || mode === "blocked-twice" || mode === "two-blocked-then-complete" || mode === "blocked-then-post-fail") {
189
+ const needsInput = ((mode === "blocked" || mode === "blocked-then-post-fail") && runNumber === 1) || (mode === "blocked-twice" && (runNumber === 1 || runNumber === 3)) || (mode === "two-blocked-then-complete" && runNumber <= 2);
141
190
  const failed = request.prompt.includes("FAIL");
142
- const question = runNumber === 3 ? "Which environment should I inspect?" : "Which branch should I inspect?";
191
+ const question = runNumber === 3 || (mode === "two-blocked-then-complete" && runNumber === 2) ? "Which environment should I inspect?" : "Which branch should I inspect?";
143
192
  writeFileSync(join(directory, "result.md"), `FULL ${request.prompt}`);
144
- writeAttemptReport(directory, { status: needsInput ? "needs-input" : failed ? "failed" : "completed", summary: needsInput ? "Need more input." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`, ...(needsInput ? { question } : {}), ...(failed ? { error: "expected failure" } : {}), documents: [] });
193
+ writeAttemptReport(directory, { status: needsInput ? "blocked" : failed ? "failed" : "completed", summary: needsInput ? "Need more input." : failed ? `${request.prompt} failed.` : `${request.prompt} completed.`, ...(needsInput ? { question } : {}), ...(failed ? { error: "expected failure" } : {}), documents: [] });
145
194
  }
146
195
  active -= 1;
147
196
  return { status: mode === "failed" ? "done" : "idle" };
148
197
  },
198
+ async getAgentState(target) {
199
+ calls.push(["getAgentState", { target }]);
200
+ if (mode === "ui-blocked") return active > 0 ? "blocked" : "idle";
201
+ if (mode === "delivery-unknown-late") return "working";
202
+ return active > 0 ? "working" : "idle";
203
+ },
149
204
  async splitPane(request) {
150
205
  calls.push(["splitPane", request]);
151
206
  paneSequence += 1;
@@ -185,6 +240,7 @@ try {
185
240
  validateControlParams({ action: "status" }),
186
241
  validateControlParams({ action: "history", runId: "not-allowed" }),
187
242
  validateControlParams({ action: "respond", runId: "run" }),
243
+ validateControlParams({ action: "respond", runId: "run", answer: "named nothing" }),
188
244
  validateControlParams({ action: "resume", runId: "run", task: 1 }),
189
245
  ].every((message) => typeof message === "string" && message.length > 0));
190
246
  let cancelledRunId;
@@ -347,8 +403,10 @@ try {
347
403
 
348
404
  setEnv({ HERDR_ENV: "0", PI_HERDR_SUBAGENTS_PI_BINARY: "/definitely/missing/pi" });
349
405
  const unavailableHerdr = fakeHerdr();
350
- const outside = await runHerdrSubagents("hello", { cwd: root, herdr: unavailableHerdr });
351
- check("Herdr is required", !outside.ok && outside.errorMessage?.includes("inside Herdr") && unavailableHerdr.calls.length === 0);
406
+ let outsideError;
407
+ try { await runSingleTask("hello", { cwd: root, herdr: unavailableHerdr }); }
408
+ catch (error) { outsideError = error; }
409
+ check("Herdr is required", outsideError?.message.includes("inside Herdr") && unavailableHerdr.calls.length === 0);
352
410
 
353
411
  setEnv({ HERDR_ENV: "1", HERDR_WORKSPACE_ID: "w1", HERDR_PANE_ID: "w1:p-parent", PI_HERDR_SUBAGENTS_PI_BINARY: "/definitely/missing/pi" });
354
412
  const childCommands = new Map();
@@ -389,7 +447,7 @@ try {
389
447
  check("full results are isolated by attempt identity", nextScopedReport.documents[0].path !== firstAttemptResult && readFileSync(firstAttemptResult, "utf8") === "Full attempt result." && readFileSync(nextScopedReport.documents[0].path, "utf8") === "New full result.");
390
448
  delete process.env.PI_HERDR_SUBAGENTS_TASK_DIR;
391
449
  const herdr = fakeHerdr();
392
- const success = await runHerdrSubagents("Review authentication", {
450
+ const success = await runSingleTask("Review authentication", {
393
451
  cwd: root,
394
452
  label: "auth-review",
395
453
  agent: "worker",
@@ -399,7 +457,7 @@ try {
399
457
  herdr,
400
458
  });
401
459
  check("compact successful report", success.ok && success.status === "completed" && success.summary === "Authentication review completed." && success.documents.length === 1 && !success.output.includes("FULL RESULT"));
402
- check("Herdr lifecycle", herdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab");
460
+ check("Herdr lifecycle", herdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab");
403
461
  const createRequest = herdr.calls.find(([name]) => name === "createTab")[1];
404
462
  const startRequest = herdr.calls.find(([name]) => name === "startAgent")[1];
405
463
  check("named unfocused tab in parent workspace", createRequest.workspaceId === "w1" && createRequest.cwd === root && createRequest.label === "SA · auth-review" && createRequest.focus === false);
@@ -417,31 +475,84 @@ try {
417
475
  check("full result and session stay on disk", readFileSync(join(taskDirectory, "result.md"), "utf8").startsWith("FULL RESULT") && existsSync(join(taskDirectory, "session.jsonl")));
418
476
 
419
477
  const extensionHerdr = fakeHerdr();
420
- const publicResult = await executeHerdrSubagents({ agent: "worker", prompt: "Review through the public tool" }, undefined, undefined, {
478
+ const publicResult = await executeHerdrSubagents({ tasks: [{ name: "public", prompt: "Review through the public tool", agent: "worker" }] }, undefined, undefined, {
421
479
  cwd: root,
422
480
  model: { provider: "fake", id: "model" },
423
481
  thinkingLevel: "medium",
424
482
  }, { agentsDirectory: join(process.cwd(), "agents"), herdr: extensionHerdr });
425
- check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
483
+ check("public extension behavior uses injectable Herdr", !publicResult.isError && publicResult.content[0].text.includes("Authentication review completed.") && !publicResult.content[0].text.includes("FULL RESULT") && extensionHerdr.calls.map(([name]) => name).join(",") === "createTab,renamePane,startAgent,promptAgent,getAgentState,isTabFocused,closeTab", JSON.stringify({ publicResult, calls: extensionHerdr.calls.map(([name]) => name) }));
426
484
  let singleStalledWarning = "";
427
- await runHerdrSubagents("single slow task", { cwd: root, herdr: fakeHerdr(), stalledWarningSeconds: 0.001, onStalled: (message) => { singleStalledWarning = message; } });
485
+ await runSingleTask("single slow task", { cwd: root, herdr: fakeHerdr(), stalledWarningSeconds: 0.001, onStalled: (message) => { singleStalledWarning = message; } });
428
486
  check("single-task execution emits configured stalled warning", singleStalledWarning.includes("appears stalled"));
429
487
 
430
488
  const failedHerdr = fakeHerdr("failed");
431
- const failedRun = await runHerdrSubagents("Fail cleanly", { cwd: root, herdr: failedHerdr });
489
+ const failedRun = await runSingleTask("Fail cleanly", { cwd: root, herdr: failedHerdr });
432
490
  check("structured child failure", !failedRun.ok && failedRun.status === "failed" && failedRun.summary === "Authentication review failed." && failedRun.errorMessage === "provider unavailable" && failedHerdr.calls.at(-1)[0] === "closeTab");
433
491
 
434
492
  const silentHerdr = fakeHerdr("silent");
435
- const silent = await runHerdrSubagents("Forget report", { cwd: root, herdr: silentHerdr });
493
+ const silent = await runSingleTask("Forget report", { cwd: root, herdr: silentHerdr });
436
494
  check("missing report is visible failure", !silent.ok && silent.errorMessage?.includes("did not submit") && silentHerdr.calls.at(-1)[0] === "closeTab");
437
- const delayedReport = await runHerdrSubagents("Retry after a transient provider error", { cwd: root, herdr: fakeHerdr("delayed-report") });
495
+ const initialIdleHerdr = fakeHerdr();
496
+ const initialIdlePrompt = initialIdleHerdr.promptAgent.bind(initialIdleHerdr);
497
+ let initialIdleAccepted = false;
498
+ initialIdleHerdr.promptAgent = async (request) => {
499
+ await new Promise((resolve) => setTimeout(resolve, 150));
500
+ initialIdleAccepted = true;
501
+ return initialIdlePrompt(request);
502
+ };
503
+ initialIdleHerdr.getAgentState = async () => initialIdleAccepted ? "working" : "idle";
504
+ const initialIdle = await runSingleTask("Wait for prompt acceptance", { cwd: root, herdr: initialIdleHerdr });
505
+ check("pre-prompt idle is not mistaken for a settled turn", initialIdle.ok && initialIdle.summary.includes("completed"));
506
+ const delayedReport = await runSingleTask("Retry after a transient provider error", { cwd: root, herdr: fakeHerdr("delayed-report") });
438
507
  check("authoritative lifecycle waits for a report after a false screen-settled state", delayedReport.ok && delayedReport.status === "completed" && delayedReport.summary === "Retry eventually completed.");
439
- const crashedAttempt = await runHerdrSubagents("Crash after reporting working", { cwd: root, herdr: fakeHerdr("working-state-crash") });
508
+ const crashedAttempt = await runSingleTask("Crash after reporting working", { cwd: root, herdr: fakeHerdr("working-state-crash") });
440
509
  check("a dead child cannot leave an authoritative working attempt waiting forever", !crashedAttempt.ok && crashedAttempt.errorMessage?.includes("child process exited"));
441
- const deadWorkingReturn = await runHerdrSubagents("Return idle then exit", { cwd: root, herdr: fakeHerdr("working-state-dead-return") });
510
+ const deadWorkingReturn = await runSingleTask("Return idle then exit", { cwd: root, herdr: fakeHerdr("working-state-dead-return") });
442
511
  check("a normal wait return cannot leave a dead working attempt waiting forever", !deadWorkingReturn.ok && deadWorkingReturn.errorMessage?.includes("Child Pi stopped while"));
512
+ const uiBlockedSnapshots = [];
513
+ const uiBlocked = await runSingleTask("Wait for an approval", { cwd: root, herdr: fakeHerdr("ui-blocked"), onUpdate: (update) => {
514
+ const taskDirectory = update.activity?.[0]?.sessionFile && dirname(update.activity[0].sessionFile);
515
+ if (taskDirectory && existsSync(join(taskDirectory, "task.json"))) uiBlockedSnapshots.push(JSON.parse(readFileSync(join(taskDirectory, "task.json"), "utf8")));
516
+ } });
517
+ check("Herdr blocked is neutral runtime telemetry and never business blocked", uiBlocked.ok && uiBlockedSnapshots.some((task) => task.status === "running" && task.runtimeState === "waiting_interaction") && !uiBlockedSnapshots.some((task) => task.status === "blocked"));
518
+ const deliverySnapshots = [];
519
+ const deliveryUnknown = await runSingleTask("Wait for a late report", { cwd: root, herdr: fakeHerdr("delivery-unknown-late"), onUpdate: (update) => {
520
+ const taskDirectory = update.activity?.[0]?.sessionFile && dirname(update.activity[0].sessionFile);
521
+ if (taskDirectory && existsSync(join(taskDirectory, "task.json"))) deliverySnapshots.push(JSON.parse(readFileSync(join(taskDirectory, "task.json"), "utf8")));
522
+ } });
523
+ check("delivery_unknown keeps running and accepts a late authoritative report", deliveryUnknown.ok && deliveryUnknown.summary === "Late authoritative report completed." && deliverySnapshots.some((task) => task.status === "running" && task.runtimeState === "delivery_unknown"));
524
+ const observationHerdr = fakeHerdr("delayed-report");
525
+ observationHerdr.getAgentState = async () => { throw new Error("temporary agent get failure"); };
526
+ const observationResult = await runSingleTask("Survive observation errors", { cwd: root, herdr: observationHerdr });
527
+ check("agent get failures remain diagnostic until an authoritative report arrives", observationResult.ok && observationResult.summary === "Retry eventually completed.");
528
+ const reportFirstHerdr = fakeHerdr();
529
+ let reportFirstDirectory;
530
+ let promptWaitReaped = false;
531
+ const reportFirstStart = reportFirstHerdr.startAgent.bind(reportFirstHerdr);
532
+ reportFirstHerdr.startAgent = async (request) => {
533
+ const sessionIndex = request.args.indexOf("--session");
534
+ reportFirstDirectory = join(request.args[sessionIndex + 1], "..");
535
+ return reportFirstStart(request);
536
+ };
537
+ reportFirstHerdr.promptAgent = async (request) => {
538
+ const task = JSON.parse(readFileSync(join(reportFirstDirectory, "task.json"), "utf8"));
539
+ mkdirSync(join(reportFirstDirectory, "reports"), { recursive: true });
540
+ writeFileSync(join(reportFirstDirectory, "reports", `${task.currentAttemptId}.json`), JSON.stringify({ attemptId: task.currentAttemptId, status: "completed", summary: "Report won the race.", documents: [] }));
541
+ await new Promise((resolve) => request.signal.addEventListener("abort", resolve, { once: true }));
542
+ promptWaitReaped = true;
543
+ return { status: "idle" };
544
+ };
545
+ const reportFirst = await runSingleTask("Report before prompt wait", { cwd: root, herdr: reportFirstHerdr });
546
+ const reportFirstTask = JSON.parse(readFileSync(join(reportFirst.recordDirectory, "task.json"), "utf8"));
547
+ check("authoritative report aborts and awaits prompt wait without reverting terminal state", reportFirst.ok && promptWaitReaped && reportFirstTask.status === "completed" && reportFirstTask.summary === undefined);
548
+ const terminalError = await runSingleTask("No report after API failure", { cwd: root, herdr: fakeHerdr("post-fail") });
549
+ const terminalErrorTask = JSON.parse(readFileSync(join(terminalError.recordDirectory, "task.json"), "utf8"));
550
+ check("terminal prompt API error without a report fails the business task", !terminalError.ok && terminalErrorTask.status === "failed" && terminalError.errorMessage.includes("connection lost"));
551
+ const retryProjection = projectTaskActivity({ id: "retry", runId: "run", order: 1, name: "retrying", agent: "worker", status: "running", runtimeState: "retrying" });
552
+ check("retrying support remains running and is visible in Fleet", retryProjection.status === "running" && retryProjection.runtimeState === "retrying" && fleetLines({ label: "retry", activity: [retryProjection] }, new FleetSelection())[1].includes("retrying"));
553
+
443
554
  const malformedExecutionHerdr = fakeHerdr("malformed");
444
- const malformedExecution = await runHerdrSubagents("Write malformed report", { cwd: root, herdr: malformedExecutionHerdr });
555
+ const malformedExecution = await runSingleTask("Write malformed report", { cwd: root, herdr: malformedExecutionHerdr });
445
556
  const malformedExecutionTaskDirectory = malformedExecution.recordDirectory;
446
557
  const repairedExecutionReport = JSON.parse(readFileSync(join(malformedExecutionTaskDirectory, "report.json"), "utf8"));
447
558
  check("malformed child report becomes a durable usable failure", !malformedExecution.ok && repairedExecutionReport.status === "failed" && readdirSync(malformedExecutionTaskDirectory).some((entry) => entry.startsWith("report.json.invalid-")) && (await listSubagentHistory(root)).some((run) => run.id === malformedExecution.runId));
@@ -450,7 +561,7 @@ try {
450
561
  check("historical attempts use distinct full-result documents", firstResultPath.endsWith("/result.md") && secondResultPath.endsWith("/turns/02-result.md") && firstResultPath !== secondResultPath);
451
562
 
452
563
  const cleanupHerdr = fakeHerdr("close-failed");
453
- const cleanupFailure = await runHerdrSubagents("Complete then fail cleanup", { cwd: root, herdr: cleanupHerdr });
564
+ const cleanupFailure = await runSingleTask("Complete then fail cleanup", { cwd: root, herdr: cleanupHerdr });
454
565
  const cleanupRunDirectory = latestRunDirectory();
455
566
  const cleanupRunRecord = JSON.parse(readFileSync(join(cleanupRunDirectory, "run.json"), "utf8"));
456
567
  const cleanupTaskDirectory = join(cleanupRunDirectory, "tasks", readdirSync(join(cleanupRunDirectory, "tasks"))[0]);
@@ -503,11 +614,12 @@ try {
503
614
  check("invalid project default thinking is diagnosed and global default remains effective", invalidExplorer?.thinking === "medium" && invalidExplorer.thinkingSource === "global" && invalidConfiguration.diagnostics.some((message) => message === "project defaultThinking must be a non-empty string"));
504
615
  writeFileSync(projectConfig, JSON.stringify({ maxConcurrency: 3, defaultThinking: "high", personas: { worker: { model: "project/worker", thinking: "high", skills: ["project-skill"] } } }));
505
616
  const configuredHerdr = fakeHerdr();
506
- const configuredRun = await executeHerdrSubagents({ agent: "worker", model: "task/model", prompt: "Use configured values" }, undefined, undefined, { cwd: configRoot, model: { provider: "parent", id: "model" }, thinkingLevel: "medium" }, { agentsDirectory: packageAgents, configurationPaths, herdr: configuredHerdr });
617
+ const configuredRun = await executeHerdrSubagents({ tasks: [{ name: "configured", prompt: "Use configured values", agent: "worker", model: "task/model" }] }, undefined, undefined, { cwd: configRoot, model: { provider: "parent", id: "model" }, thinkingLevel: "medium" }, { agentsDirectory: packageAgents, configurationPaths, herdr: configuredHerdr });
507
618
  const configuredStart = configuredHerdr.calls.find(([name]) => name === "startAgent")[1];
508
619
  check("configured persona uses displayed effective values", !configuredRun.isError && configuredStart.args.includes("task/model") && configuredStart.args.includes("high") && configuredStart.args.includes("--skill") && configuredStart.args.includes("project-skill"));
509
620
 
510
621
  const analystHerdr = fakeHerdr();
622
+ analystHerdr.expectConcurrentPrompts(3);
511
623
  const analystBatch = await executeHerdrSubagents({ label: "independent-analysis", concurrency: 3, tasks: [
512
624
  { name: "sol-audit", prompt: "slow audit", agent: "analyst", model: "vendor/sol" },
513
625
  { name: "grok-replicate", prompt: "slow replicate", agent: "analyst", model: "vendor/grok" },
@@ -520,6 +632,7 @@ try {
520
632
  check("completed analyst batch closes its run tab", analystHerdr.calls.at(-1)?.[0] === "closeTab");
521
633
 
522
634
  const batchHerdr = fakeHerdr();
635
+ batchHerdr.expectConcurrentPrompts(2);
523
636
  const batchUpdates = [];
524
637
  const batch = await executeHerdrSubagents({ label: "reviews", concurrency: 2, tasks: [
525
638
  { name: "one", prompt: "one slow", agent: "explorer" },
@@ -541,7 +654,21 @@ try {
541
654
  const parallelQuestionTask = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "task.json"), "utf8"));
542
655
  const parallelQuestionReport = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "report.json"), "utf8"));
543
656
  const parallelQuestionAttemptReport = JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "reports", `${parallelQuestionTask.currentAttemptId}.json`), "utf8"));
544
- check("parallel needs-input becomes a structured failure while independent siblings finish", parallelQuestion.isError && parallelQuestion.details.status === "partial" && parallelQuestion.details.tasks[0].status === "failed" && parallelQuestion.details.tasks[0].failureKind === "missing_input" && parallelQuestion.details.tasks[0].requiredInput === "Which branch should I inspect?" && parallelQuestion.details.tasks[1].status === "completed" && parallelQuestionTask.status === "failed" && parallelQuestionReport.status === "failed" && parallelQuestionReport.failureKind === "missing_input" && parallelQuestionAttemptReport.status === "failed" && parallelQuestionAttemptReport.failureKind === "missing_input" && parallelQuestionHerdr.calls.some(([name]) => name === "closeTab"));
657
+ check("a parallel blocked attempt report stays blocked with its question", parallelQuestionAttemptReport.status === "blocked" && parallelQuestionAttemptReport.question === "Which branch should I inspect?" && parallelQuestionReport.question === parallelQuestionAttemptReport.question);
658
+ check("a parallel task blocks like a serial one while its independent sibling finishes", !parallelQuestion.isError && parallelQuestion.details.status === "blocked" && parallelQuestion.details.question === "Which branch should I inspect?" && parallelQuestion.details.allowedActions?.join(",") === "respond,cancel" && parallelQuestion.details.tasks[0].status === "blocked" && parallelQuestion.details.tasks[1].status === "completed" && parallelQuestionTask.status === "blocked" && parallelQuestionReport.status === "blocked" && !parallelQuestionHerdr.calls.some(([name]) => name === "closeTab"));
659
+ const parallelAnswered = await executeHerdrSubagents({ action: "respond", runId: parallelQuestion.details.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelQuestionHerdr });
660
+ check("answering a blocked task in a parallel run settles and closes that run", !parallelAnswered.isError && parallelAnswered.details.status === "completed" && JSON.parse(readFileSync(join(parallelQuestion.details.tasks[0].recordDirectory, "task.json"), "utf8")).status === "completed" && parallelQuestionHerdr.calls.some(([name]) => name === "closeTab"));
661
+
662
+ const bothBlockedHerdr = fakeHerdr("two-blocked-then-complete");
663
+ const bothBlocked = await executeHerdrSubagents({ concurrency: 2, tasks: [
664
+ { name: "first-question", prompt: "ask", agent: "explorer" },
665
+ { name: "second-question", prompt: "ask too", agent: "reviewer" },
666
+ ] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
667
+ check("both lanes of a parallel batch may block at once", !bothBlocked.isError && bothBlocked.details.status === "blocked" && bothBlocked.details.tasks[0].status === "blocked" && bothBlocked.details.tasks[1].status === "blocked" && bothBlocked.details.question === "Which branch should I inspect?" && !bothBlockedHerdr.calls.some(([name]) => name === "closeTab"));
668
+ const firstOfBoth = await executeHerdrSubagents({ action: "respond", runId: bothBlocked.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
669
+ check("answering one blocked lane exposes the other lane's question and keeps the tab", !firstOfBoth.isError && firstOfBoth.details.status === "blocked" && firstOfBoth.details.question === "Which environment should I inspect?" && firstOfBoth.details.tasks[0].status === "completed" && firstOfBoth.details.tasks[1].status === "blocked" && !bothBlockedHerdr.calls.some(([name]) => name === "closeTab"));
670
+ const secondOfBoth = await executeHerdrSubagents({ action: "respond", runId: bothBlocked.details.runId, task: 2, answer: "staging" }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: bothBlockedHerdr });
671
+ check("the parallel run closes only after its last blocked lane is answered", !secondOfBoth.isError && secondOfBoth.details.status === "completed" && secondOfBoth.details.tasks.every((entry) => entry.status === "completed") && bothBlockedHerdr.calls.filter(([name]) => name === "closeTab").length === 1);
545
672
  const mixedCancelHerdr = fakeHerdr();
546
673
  const mixedCancelPrompt = mixedCancelHerdr.promptAgent.bind(mixedCancelHerdr);
547
674
  mixedCancelHerdr.promptAgent = async (request) => request.prompt.includes("WAIT") ? await new Promise((_resolve, reject) => request.signal?.addEventListener("abort", () => reject(request.signal.reason), { once: true })) : mixedCancelPrompt(request);
@@ -556,6 +683,21 @@ try {
556
683
  const batchCleanup = await executeHerdrSubagents({ tasks: [{ name: "cleanup", prompt: "cleanup", agent: "explorer" }] }, undefined, (update) => batchCleanupUpdates.push(update), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: batchCleanupHerdr });
557
684
  const batchCleanupRecord = JSON.parse(readFileSync(join(batchCleanup.details.recordDirectory, "run.json"), "utf8"));
558
685
  check("batch cleanup failure is persisted without changing the business result", !batchCleanup.isError && batchCleanupRecord.status === "completed" && batchCleanupRecord.cleanupError.includes("tab still busy") && batchCleanupUpdates.filter((update) => update.content[0]?.text.includes("cleanup failed")).length === 1);
686
+ const concurrentWriterHerdr = fakeHerdr();
687
+ let concurrentWriterRunFile;
688
+ const concurrentWriterStart = concurrentWriterHerdr.startAgent.bind(concurrentWriterHerdr);
689
+ concurrentWriterHerdr.startAgent = async (request) => {
690
+ concurrentWriterRunFile = join(request.args[request.args.indexOf("--session") + 1], "..", "..", "..", "run.json");
691
+ return concurrentWriterStart(request);
692
+ };
693
+ const concurrentWriterPrompt = concurrentWriterHerdr.promptAgent.bind(concurrentWriterHerdr);
694
+ concurrentWriterHerdr.promptAgent = async (request) => {
695
+ await withRunStateLock(concurrentWriterRunFile, undefined, (run) => Object.assign(run, { resume: { generation: 1, phase: "active", tabId: "w1:t9", paneIds: [], active: {} } }));
696
+ return concurrentWriterPrompt(request);
697
+ };
698
+ await executeHerdrSubagents({ tasks: [{ name: "concurrent-writer", prompt: "inspect", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: concurrentWriterHerdr });
699
+ const concurrentWriterRecord = JSON.parse(readFileSync(concurrentWriterRunFile, "utf8"));
700
+ check("settling a batch preserves run state written concurrently", concurrentWriterRecord.status === "completed" && concurrentWriterRecord.resume?.tabId === "w1:t9");
559
701
  const cleanupProbeHerdr = fakeHerdr();
560
702
  cleanupProbeHerdr.isTabFocused = async () => { throw new Error("tab lookup failed"); };
561
703
  const cleanupProbe = await executeHerdrSubagents({ tasks: [{ name: "cleanup-probe", prompt: "cleanup probe", agent: "explorer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: cleanupProbeHerdr });
@@ -593,7 +735,7 @@ try {
593
735
  const pendingCleanupRecord = JSON.parse(readFileSync(join(pendingCleanupRun.details.recordDirectory, "run.json"), "utf8"));
594
736
  const cleanupWhilePending = await executeHerdrSubagents({ action: "cleanup", runId: pendingCleanupRun.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: pendingCleanupHerdr });
595
737
  const resumeWhilePending = await executeHerdrSubagents({ action: "resume", runId: pendingCleanupRun.details.runId, task: 1, prompt: "too soon" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: pendingCleanupHerdr });
596
- check("pending tab cleanup blocks history resume and deletion", pendingCleanupRecord.cleanupPendingTabIds?.includes(pendingCleanupRun.details.tabId) && cleanupWhilePending.isError && resumeWhilePending.isError);
738
+ check("pending tab cleanup blocks deletion but permits an isolated resume generation", pendingCleanupRecord.cleanupPendingTabIds?.includes(pendingCleanupRun.details.tabId) && cleanupWhilePending.isError && !resumeWhilePending.isError);
597
739
  releasePendingCleanup();
598
740
  await new Promise((resolve) => setTimeout(resolve, 10));
599
741
  const releasedCleanupRecord = JSON.parse(readFileSync(join(pendingCleanupRun.details.recordDirectory, "run.json"), "utf8"));
@@ -623,10 +765,10 @@ try {
623
765
  deferredSingleHerdr.isTabFocused = async () => true;
624
766
  deferredSingleHerdr.closeTab = async () => { throw new Error("single deferred close failed"); };
625
767
  const deferredSingleUpdates = [];
626
- const deferredSingle = await executeHerdrSubagents({ prompt: "single deferred cleanup", agent: "explorer" }, undefined, (update) => deferredSingleUpdates.push(update), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deferredSingleHerdr });
768
+ const deferredSingle = await executeHerdrSubagents({ tasks: [{ name: "deferred-single", prompt: "single deferred cleanup", agent: "explorer" }] }, undefined, (update) => deferredSingleUpdates.push(update), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deferredSingleHerdr });
627
769
  await new Promise((resolve) => setTimeout(resolve, 10));
628
- const deferredSingleRun = JSON.parse(readFileSync(join(deferredSingle.details.recordDirectory, "..", "..", "run.json"), "utf8"));
629
- check("deferred single-task cleanup failure is persisted and surfaced without changing completion", deferredSingleRun.status === "completed" && deferredSingleRun.cleanupError === "single deferred close failed" && deferredSingleUpdates.some((update) => update.content[0].text.includes("cleanup failed") && update.details.status === "warning"));
770
+ const deferredSingleRun = JSON.parse(readFileSync(join(deferredSingle.details.recordDirectory, "run.json"), "utf8"));
771
+ check("deferred cleanup failure is persisted and surfaced without changing completion", deferredSingleRun.status === "completed" && deferredSingleRun.cleanupError === "single deferred close failed" && deferredSingleUpdates.some((update) => update.content[0].text.includes("cleanup failed") && update.details.cleanupError));
630
772
  const cancelledHerdr = fakeHerdr();
631
773
  cancelledHerdr.isTabFocused = async (tabId) => { cancelledHerdr.calls.push(["isTabFocused", { tabId }]); return true; };
632
774
  cancelledHerdr.waitForTabUnfocused = async () => { throw new Error("shutdown cleanup must not wait for focus"); };
@@ -653,7 +795,7 @@ try {
653
795
  });
654
796
  };
655
797
  const singleCancellation = new AbortController();
656
- const cancelledSinglePromise = runHerdrSubagents("wait", { cwd: root, herdr: cancelledSingleHerdr, signal: singleCancellation.signal });
798
+ const cancelledSinglePromise = runSingleTask("wait", { cwd: root, herdr: cancelledSingleHerdr, signal: singleCancellation.signal });
657
799
  await new Promise((resolve) => setTimeout(resolve, 5));
658
800
  singleCancellation.abort(new Error("Parent Pi session closed."));
659
801
  const cancelledSingle = await cancelledSinglePromise;
@@ -752,29 +894,32 @@ try {
752
894
  mkdirSync(staleStatusTaskDirectory, { recursive: true });
753
895
  writeFileSync(join(staleStatusTaskDirectory, "..", "..", "run.json"), JSON.stringify({ id: "stale-status", label: "stale-status", status: "running", startedAt: "2026-10-08T00:00:00.000Z" }));
754
896
  writeFileSync(join(staleStatusTaskDirectory, "task.json"), JSON.stringify({ id: "stale-task", runId: "stale-status", order: 1, name: "stale-task", agent: "reviewer", status: "running" }));
755
- writeFileSync(join(staleStatusTaskDirectory, "report.json"), JSON.stringify({ status: "needs-input", summary: "Old blocked report.", question: "Old question?", documents: [] }));
897
+ writeFileSync(join(staleStatusTaskDirectory, "report.json"), JSON.stringify({ status: "blocked", summary: "Old blocked report.", question: "Old question?", documents: [] }));
756
898
  const runningStatus = await executeHerdrSubagents({ action: "status", runId: "stale-status" }, undefined, undefined, { cwd: staleStatusRoot }, { agentsDirectory: join(process.cwd(), "agents") });
757
899
  check("running status does not expose a stale report from an earlier blocked turn", runningStatus.details.tasks[0].status === "running" && !runningStatus.details.tasks[0].question && !runningStatus.details.tasks[0].summary && !runningStatus.content[0].text.includes("Old question"));
758
900
 
759
901
  const blockedHerdr = fakeHerdr("blocked");
760
902
  const blocked = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "question", prompt: "inspect branch", agent: "explorer" }, { name: "after-answer", prompt: "continue queued", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
761
- check("needs-input returns foreground control with its full run ID and allowed actions", !blocked.isError && blocked.details.status === "blocked" && blocked.details.question === "Which branch should I inspect?" && blocked.details.tasks[0].question === blocked.details.question && blocked.details.allowedActions?.join(",") === "respond,cancel" && blocked.content[0].text.includes(blocked.details.runId) && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
903
+ check("a blocked report returns foreground control with its full run ID and allowed actions", !blocked.isError && blocked.details.status === "blocked" && blocked.details.question === "Which branch should I inspect?" && blocked.details.tasks[0].question === blocked.details.question && blocked.details.allowedActions?.join(",") === "respond,cancel" && blocked.content[0].text.includes(blocked.details.runId) && !blockedHerdr.calls.some(([name]) => name === "closeTab"));
762
904
  const blockedStatus = await executeHerdrSubagents({ action: "status", runId: basename(blocked.details.recordDirectory) }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents") });
763
905
  check("status exposes blocked task questions for respond", blockedStatus.details.status === "blocked" && blockedStatus.details.settled === false && blockedStatus.details.tasks[0].question === "Which branch should I inspect?" && blockedStatus.content[0].text.includes("Which branch should I inspect?"));
764
906
  const activeCleanup = await executeHerdrSubagents({ action: "cleanup", runId: blocked.details.runId }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
765
907
  check("cleanup refuses to delete an active blocked run", activeCleanup.isError && activeCleanup.content[0].text.includes("still active") && existsSync(blocked.details.recordDirectory));
766
- mkdirSync(join(blocked.details.recordDirectory, ".respond.lock"));
767
- const concurrentResponse = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId, answer: "duplicate" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
768
- rmSync(join(blocked.details.recordDirectory, ".respond.lock"), { recursive: true });
769
- check("concurrent blocked responses are rejected before a second prompt", concurrentResponse.isError && concurrentResponse.content[0].text.includes("already has a blocked response in progress"));
908
+ const blockedTaskLock = join(blocked.details.tasks[0].recordDirectory, ".resume.lock");
909
+ mkdirSync(blockedTaskLock);
910
+ writeFileSync(join(blockedTaskLock, "owner.json"), JSON.stringify({ token: "live-owner", processId: process.pid, createdAt: new Date().toISOString() }));
911
+ const concurrentResponse = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId, task: 1, answer: "duplicate" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
912
+ rmSync(blockedTaskLock, { recursive: true });
913
+ check("concurrent blocked responses are rejected by the task-scoped lock before a second prompt", concurrentResponse.isError && concurrentResponse.content[0].text.includes("response or resume in progress"));
770
914
  const staleLockHerdr = fakeHerdr("blocked");
771
915
  const staleLockBlocked = await executeHerdrSubagents({ tasks: [{ name: "stale-lock-question", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
772
- writeFileSync(join(staleLockBlocked.details.recordDirectory, ".respond.lock"), JSON.stringify({ processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
773
- const diagnosedStaleResponse = await executeHerdrSubagents({ action: "respond", runId: staleLockBlocked.details.runId, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
774
- check("blocked response diagnoses a lock owned by a dead process without unsafe reclamation", diagnosedStaleResponse.isError && diagnosedStaleResponse.content[0].text.includes("stale blocked-response lock"));
775
- rmSync(join(staleLockBlocked.details.recordDirectory, ".respond.lock"));
916
+ const staleResponseTaskLock = join(staleLockBlocked.details.tasks[0].recordDirectory, ".resume.lock");
917
+ mkdirSync(staleResponseTaskLock);
918
+ writeFileSync(join(staleResponseTaskLock, "owner.json"), JSON.stringify({ token: "dead-owner", processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
919
+ const diagnosedStaleResponse = await executeHerdrSubagents({ action: "respond", runId: staleLockBlocked.details.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleLockHerdr });
920
+ check("blocked response automatically reclaims a task lock owned by a dead process", !diagnosedStaleResponse.isError && !existsSync(staleResponseTaskLock));
776
921
  const resumeUpdates = [];
777
- const resumed = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId.slice(0, 8), answer: "Inspect main." }, undefined, (update) => resumeUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
922
+ const resumed = await executeHerdrSubagents({ action: "respond", runId: blocked.details.runId.slice(0, 8), task: 1, answer: "Inspect main." }, undefined, (update) => resumeUpdates.push(update.details), { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: blockedHerdr });
778
923
  const blockedStarts = blockedHerdr.calls.filter(([name]) => name === "startAgent").map(([, request]) => request);
779
924
  const blockedPrompts = blockedHerdr.calls.filter(([name]) => name === "promptAgent").map(([, request]) => request);
780
925
  check("answer resumes original live Agent and queued sibling gets a new pane", !resumed.isError && resumed.details.status === "completed" && blockedPrompts.length === 3 && blockedPrompts[1].target === blockedPrompts[0].target && blockedStarts.length === 2 && new Set(blockedStarts.map((request) => request.paneId)).size === 2);
@@ -785,22 +930,241 @@ try {
785
930
  check("blocked continuation preserves newly allocated pane IDs", resumedTasks.every((task) => resumedRunRecord.paneIds.includes(task.paneId)));
786
931
  const resumedTurn = JSON.parse(readFileSync(resumedTasks[0].currentTurnFile, "utf8"));
787
932
  check("authoritative response clears stale blocked fields", !("question" in resumedTasks[0]) && !("error" in resumedTasks[0]) && !("question" in resumedTurn) && !("error" in resumedTurn));
933
+ const runsIn = (base) => {
934
+ const directory = join(base, ".pi", "herdr-subagents", "runs");
935
+ const entries = readdirSync(directory).sort();
936
+ return join(directory, entries[entries.length - 1]);
937
+ };
938
+ const tasksIn = (runDirectory) => readdirSync(join(runDirectory, "tasks")).sort().map((entry) => join(runDirectory, "tasks", entry));
939
+ const readTaskJson = (directory) => JSON.parse(readFileSync(join(directory, "task.json"), "utf8"));
940
+ const settleWait = async (predicate) => {
941
+ for (let elapsed = 0; elapsed < 3000; elapsed += 5) {
942
+ if (predicate()) return true;
943
+ await new Promise((resolve) => setTimeout(resolve, 5));
944
+ }
945
+ return false;
946
+ };
947
+
948
+ const midBatchRoot = join(root, "mid-batch-followup");
949
+ const midBatchHerdr = fakeHerdr();
950
+ const midBatchPrompt = midBatchHerdr.promptAgent.bind(midBatchHerdr);
951
+ let releaseMidBatch;
952
+ const midBatchGate = new Promise((resolve) => { releaseMidBatch = resolve; });
953
+ let reportMidBatchGated;
954
+ const midBatchGated = new Promise((resolve) => { reportMidBatchGated = resolve; });
955
+ midBatchHerdr.promptAgent = async (request) => {
956
+ if (request.prompt.includes("WAIT")) { reportMidBatchGated(); await midBatchGate; }
957
+ return midBatchPrompt(request);
958
+ };
959
+ const midBatchPromise = executeHerdrSubagents({ label: "mid-batch", concurrency: 1, tasks: [
960
+ { name: "fails", prompt: "FAIL immediately", agent: "explorer" },
961
+ { name: "holds", prompt: "WAIT for the gate", agent: "explorer" },
962
+ { name: "last", prompt: "third task", agent: "explorer" },
963
+ ] }, undefined, undefined, { cwd: midBatchRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: midBatchHerdr });
964
+ await midBatchGated;
965
+ const midBatchRun = runsIn(midBatchRoot);
966
+ const midBatchTasks = tasksIn(midBatchRun);
967
+ check("a later task stays queued while an earlier task of the same batch fails", readTaskJson(midBatchTasks[0]).status === "failed" && readTaskJson(midBatchTasks[2]).status === "queued");
968
+ const midBatchStartsBefore = midBatchHerdr.calls.filter(([name]) => name === "startAgent").length;
969
+ const midBatchFollowUp = await executeHerdrSubagents({ action: "respond", runId: basename(midBatchRun), task: 1, answer: "retry against the other branch" }, undefined, undefined, { cwd: midBatchRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: midBatchHerdr });
970
+ check("a failed task continues in its own live pane while its batch is still running", !midBatchFollowUp.isError && readTaskJson(midBatchTasks[0]).status === "completed" && midBatchHerdr.calls.filter(([name]) => name === "createTab").length === 1);
971
+ check("continuing a failed task leaves queued siblings to the running batch", readTaskJson(midBatchTasks[2]).status === "queued" && midBatchHerdr.calls.filter(([name]) => name === "startAgent").length === midBatchStartsBefore);
972
+ releaseMidBatch();
973
+ const midBatch = await midBatchPromise;
974
+ const midBatchPrompts = midBatchHerdr.calls.filter(([name]) => name === "promptAgent").map(([, request]) => request.prompt);
975
+ check("the batch still runs its own queued work exactly once after a follow-up", midBatchPrompts.filter((prompt) => prompt === "third task").length === 1 && tasksIn(midBatchRun).every((directory) => readTaskJson(directory).status === "completed"));
976
+ check("a settling batch records the task records instead of its own stale view", JSON.parse(readFileSync(join(midBatchRun, "run.json"), "utf8")).status === "completed" && midBatch.details.tasks[0].status === "failed");
977
+
978
+ const queueOwnerRoot = join(root, "parallel-queue-ownership");
979
+ const queueOwnerHerdr = fakeHerdr("blocked");
980
+ const queueOwnerPrompt = queueOwnerHerdr.promptAgent.bind(queueOwnerHerdr);
981
+ let releaseQueueOwner;
982
+ const queueOwnerGate = new Promise((resolve) => { releaseQueueOwner = resolve; });
983
+ let reportQueueOwnerGated;
984
+ const queueOwnerGated = new Promise((resolve) => { reportQueueOwnerGated = resolve; });
985
+ queueOwnerHerdr.promptAgent = async (request) => {
986
+ const result = await queueOwnerPrompt(request);
987
+ if (request.prompt.includes("WAIT")) { reportQueueOwnerGated(); await queueOwnerGate; }
988
+ return result;
989
+ };
990
+ const queueOwnerPromise = executeHerdrSubagents({ label: "queue-owner", concurrency: 2, tasks: [
991
+ { name: "blocks", prompt: "needs an answer", agent: "explorer" },
992
+ { name: "holds", prompt: "WAIT for the gate", agent: "explorer" },
993
+ { name: "last", prompt: "queued work", agent: "explorer" },
994
+ ] }, undefined, undefined, { cwd: queueOwnerRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queueOwnerHerdr });
995
+ await queueOwnerGated;
996
+ const queueOwnerRun = runsIn(queueOwnerRoot);
997
+ const queueOwnerTasks = tasksIn(queueOwnerRun);
998
+ check("a parallel lane blocks without stopping its sibling or starting queued work", await settleWait(() => readTaskJson(queueOwnerTasks[0]).status === "blocked") && readTaskJson(queueOwnerTasks[1]).status === "running" && readTaskJson(queueOwnerTasks[2]).status === "queued");
999
+ const queueOwnerStarts = queueOwnerHerdr.calls.filter(([name]) => name === "startAgent").length;
1000
+ const queueOwnerAnswer = await executeHerdrSubagents({ action: "respond", runId: basename(queueOwnerRun), task: 1, answer: "use the main branch" }, undefined, undefined, { cwd: queueOwnerRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queueOwnerHerdr });
1001
+ check("answering a blocked lane leaves queued work to the batch that owns it", !queueOwnerAnswer.isError && queueOwnerAnswer.details.status === "running" && readTaskJson(queueOwnerTasks[0]).status === "completed" && readTaskJson(queueOwnerTasks[2]).status === "queued" && queueOwnerHerdr.calls.filter(([name]) => name === "startAgent").length === queueOwnerStarts);
1002
+ releaseQueueOwner();
1003
+ const queueOwner = await queueOwnerPromise;
1004
+ check("the batch runs its own queued work exactly once after a blocked lane is answered", queueOwnerHerdr.calls.filter(([name, request]) => name === "promptAgent" && request.prompt === "queued work").length === 1);
1005
+ check("an adopted lane drops the lifecycle fields its blocked record carried", queueOwner.details.activity[0].status === "completed" && Boolean(queueOwner.details.activity[0].completedAt) && !("updatedAt" in queueOwner.details.activity[0]));
1006
+ check("a settled batch adopts a lane that was answered while its siblings ran", !queueOwner.isError && queueOwner.details.status === "completed" && queueOwner.details.tasks[0].status === "completed" && !queueOwner.details.question && JSON.parse(readFileSync(join(queueOwnerRun, "run.json"), "utf8")).status === "completed" && queueOwnerHerdr.calls.some(([name]) => name === "closeTab"));
1007
+
1008
+ const revivedRoot = join(root, "revived-during-settle");
1009
+ const revivedHerdr = fakeHerdr();
1010
+ const revivedPrompt = revivedHerdr.promptAgent.bind(revivedHerdr);
1011
+ let revivedFlipped = false;
1012
+ revivedHerdr.promptAgent = async (request) => {
1013
+ if (request.prompt.includes("WAIT")) {
1014
+ const failedFirst = await settleWait(() => {
1015
+ const directories = tasksIn(runsIn(revivedRoot));
1016
+ return directories.length > 0 && existsSync(join(directories[0], "task.json")) && readTaskJson(directories[0]).status === "failed";
1017
+ });
1018
+ if (failedFirst) {
1019
+ const first = tasksIn(runsIn(revivedRoot))[0];
1020
+ const record = readTaskJson(first);
1021
+ delete record.completedAt;
1022
+ delete record.error;
1023
+ writeFileSync(join(first, "task.json"), JSON.stringify({ ...record, status: "running", updatedAt: new Date().toISOString() }));
1024
+ revivedFlipped = true;
1025
+ }
1026
+ }
1027
+ return revivedPrompt(request);
1028
+ };
1029
+ const revived = await executeHerdrSubagents({ label: "revived", concurrency: 2, tasks: [
1030
+ { name: "fails", prompt: "FAIL immediately", agent: "explorer" },
1031
+ { name: "holds", prompt: "WAIT then settle", agent: "explorer" },
1032
+ ] }, undefined, undefined, { cwd: revivedRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivedHerdr });
1033
+ const revivedRunRecord = JSON.parse(readFileSync(join(runsIn(revivedRoot), "run.json"), "utf8"));
1034
+ check("a settling batch never closes its tab or finishes the run while a continued task works", revivedFlipped && revivedRunRecord.status === "running" && !revivedHerdr.calls.some(([name]) => name === "closeTab") && revived.details.status === "partial");
1035
+
1036
+ const continueRoot = join(root, "continue-failed-task");
1037
+ const continueHerdr = fakeHerdr();
1038
+ const failedOnly = await executeHerdrSubagents({ label: "solo", tasks: [{ name: "solo", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: continueRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
1039
+ const continueRun = runsIn(continueRoot);
1040
+ const continueTask = tasksIn(continueRun)[0];
1041
+ check("a settled failed task keeps its saved Pi session on disk", failedOnly.isError && readTaskJson(continueTask).status === "failed" && existsSync(join(continueTask, "session.jsonl")));
1042
+ const unqualifiedResponse = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), answer: "guess" }, undefined, undefined, { cwd: continueRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
1043
+ check("respond requires its task number instead of selecting a task implicitly", unqualifiedResponse.isError && unqualifiedResponse.content[0].text.includes("task are required") && continueHerdr.calls.every(([name]) => name !== "promptAgent" || !name.includes("guess")));
1044
+ renameSync(join(continueTask, "session.jsonl"), join(continueTask, "session.jsonl.moved"));
1045
+ const withoutSession = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), task: 1, answer: "continue" }, undefined, undefined, { cwd: continueRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
1046
+ check("a failed task without a saved session reports that it cannot continue", withoutSession.isError && withoutSession.content[0].text.includes("saved Pi session"));
1047
+ renameSync(join(continueTask, "session.jsonl.moved"), join(continueTask, "session.jsonl"));
1048
+ const continued = await executeHerdrSubagents({ action: "respond", runId: basename(continueRun), task: 1, answer: "try a different approach" }, undefined, undefined, { cwd: continueRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: continueHerdr });
1049
+ check("an explicitly selected failed task continues from its saved session", !continued.isError && readTaskJson(continueTask).status === "completed" && JSON.parse(readFileSync(join(continueRun, "run.json"), "utf8")).status === "completed");
1050
+
1051
+ const sharedTabRoot = join(root, "shared-tab-continuations");
1052
+ const sharedHerdr = fakeHerdr();
1053
+ await executeHerdrSubagents({ label: "both-fail", concurrency: 2, tasks: [
1054
+ { name: "first", prompt: "FAIL first", agent: "explorer" },
1055
+ { name: "second", prompt: "FAIL second", agent: "explorer" },
1056
+ ] }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
1057
+ const sharedRun = runsIn(sharedTabRoot);
1058
+ const sharedTasks = tasksIn(sharedRun);
1059
+ const sharedClosesAfterBatch = sharedHerdr.calls.filter(([name]) => name === "closeTab").length;
1060
+ const sharedSecondPane = readTaskJson(sharedTasks[1]).paneId;
1061
+ sharedHerdr.paneExists = async (paneId) => { sharedHerdr.calls.push(["paneExists", { paneId }]); return paneId !== sharedSecondPane; };
1062
+ let releaseSharedSecond;
1063
+ const sharedSecondGate = new Promise((resolve) => { releaseSharedSecond = resolve; });
1064
+ let reportSharedSecondGated;
1065
+ const sharedSecondGated = new Promise((resolve) => { reportSharedSecondGated = resolve; });
1066
+ const sharedRename = sharedHerdr.renamePane.bind(sharedHerdr);
1067
+ sharedHerdr.renamePane = async (paneId, label) => {
1068
+ if (label.startsWith("02 ")) { reportSharedSecondGated(); await sharedSecondGate; }
1069
+ return sharedRename(paneId, label);
1070
+ };
1071
+ const sharedSecondContinuation = executeHerdrSubagents({ action: "respond", runId: basename(sharedRun), task: 2, answer: "continue the second" }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
1072
+ await sharedSecondGated;
1073
+ const sharedFirstContinuation = await executeHerdrSubagents({ action: "respond", runId: basename(sharedRun), task: 1, answer: "continue the first" }, undefined, undefined, { cwd: sharedTabRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: sharedHerdr });
1074
+ check("a finished continuation never closes a tab a sibling continuation is still claiming", !sharedFirstContinuation.isError && sharedFirstContinuation.details.status === "running" && sharedHerdr.calls.filter(([name]) => name === "closeTab").length === sharedClosesAfterBatch);
1075
+ releaseSharedSecond();
1076
+ const sharedSecondResult = await sharedSecondContinuation;
1077
+ check("the last continuation to settle closes the shared tab exactly once", !sharedSecondResult.isError && sharedSecondResult.details.status === "completed" && sharedHerdr.calls.filter(([name]) => name === "closeTab").length === sharedClosesAfterBatch + 1);
1078
+
1079
+ const queuedSiblingRoot = join(root, "queued-sibling-projection");
1080
+ const queuedSiblingHerdr = fakeHerdr();
1081
+ let releaseQueuedSibling;
1082
+ const queuedSiblingGate = new Promise((resolve) => { releaseQueuedSibling = resolve; });
1083
+ let reportQueuedSiblingGated;
1084
+ const queuedSiblingGated = new Promise((resolve) => { reportQueuedSiblingGated = resolve; });
1085
+ const queuedSiblingRename = queuedSiblingHerdr.renamePane.bind(queuedSiblingHerdr);
1086
+ queuedSiblingHerdr.renamePane = async (paneId, label) => {
1087
+ if (label.startsWith("02 ")) { reportQueuedSiblingGated(); await queuedSiblingGate; }
1088
+ return queuedSiblingRename(paneId, label);
1089
+ };
1090
+ const queuedSiblingBatch = executeHerdrSubagents({ label: "queued-sibling", concurrency: 1, tasks: [
1091
+ { name: "fails", prompt: "FAIL immediately", agent: "explorer" },
1092
+ { name: "next", prompt: "second task", agent: "explorer" },
1093
+ ] }, undefined, undefined, { cwd: queuedSiblingRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queuedSiblingHerdr });
1094
+ await queuedSiblingGated;
1095
+ const queuedSiblingRun = runsIn(queuedSiblingRoot);
1096
+ const queuedSiblingTasks = tasksIn(queuedSiblingRun);
1097
+ check("a batch task is still queued while its pane is being prepared", readTaskJson(queuedSiblingTasks[0]).status === "failed" && readTaskJson(queuedSiblingTasks[1]).status === "queued");
1098
+ const duringQueuedSibling = await executeHerdrSubagents({ action: "respond", runId: basename(queuedSiblingRun), task: 1, answer: "retry now" }, undefined, undefined, { cwd: queuedSiblingRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: queuedSiblingHerdr });
1099
+ check("a queued sibling is reported as running work, never as a fabricated input request", !duringQueuedSibling.isError && duringQueuedSibling.details.status === "running" && !duringQueuedSibling.details.question && !duringQueuedSibling.content[0].text.includes("needs input"));
1100
+ releaseQueuedSibling();
1101
+ await queuedSiblingBatch;
1102
+
1103
+ const revivePaneRoot = join(root, "continue-dead-pane");
1104
+ const revivePaneContinueHerdr = fakeHerdr();
1105
+ await executeHerdrSubagents({ label: "dead-pane", tasks: [{ name: "gone", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: revivePaneRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivePaneContinueHerdr });
1106
+ const revivePaneRun = runsIn(revivePaneRoot);
1107
+ const revivePaneTask = tasksIn(revivePaneRun)[0];
1108
+ const revivePaneAttempt = Number(readTaskJson(revivePaneTask).attempt ?? 1);
1109
+ const revivePaneTabs = revivePaneContinueHerdr.calls.filter(([name]) => name === "createTab").length;
1110
+ revivePaneContinueHerdr.paneExists = async (paneId) => { revivePaneContinueHerdr.calls.push(["paneExists", { paneId }]); return false; };
1111
+ const revivePaneContinued = await executeHerdrSubagents({ action: "respond", runId: basename(revivePaneRun), task: 1, answer: "restart from the saved session" }, undefined, undefined, { cwd: revivePaneRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: revivePaneContinueHerdr });
1112
+ check("a failed task whose pane is gone restarts from its saved session in a new pane", !revivePaneContinued.isError && readTaskJson(revivePaneTask).status === "completed" && Number(readTaskJson(revivePaneTask).attempt) === revivePaneAttempt + 1 && revivePaneContinueHerdr.calls.filter(([name]) => name === "createTab").length === revivePaneTabs + 1);
1113
+
1114
+ const reblockRoot = join(root, "continue-then-block");
1115
+ const reblockHerdr = fakeHerdr();
1116
+ await executeHerdrSubagents({ label: "reblock", tasks: [{ name: "reblock", prompt: "FAIL alone", agent: "explorer" }] }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
1117
+ const reblockRun = runsIn(reblockRoot);
1118
+ const reblockTask = tasksIn(reblockRun)[0];
1119
+ const reblockPromptAgent = reblockHerdr.promptAgent.bind(reblockHerdr);
1120
+ reblockHerdr.promptAgent = async (request) => {
1121
+ if (!request.prompt.includes("ASK AGAIN")) return reblockPromptAgent(request);
1122
+ reblockHerdr.calls.push(["promptAgent", request]);
1123
+ const attemptId = readTaskJson(reblockTask).currentAttemptId;
1124
+ mkdirSync(join(reblockTask, "reports"), { recursive: true });
1125
+ writeFileSync(join(reblockTask, "reports", `${attemptId}.json`), JSON.stringify({ attemptId, status: "blocked", summary: "Still missing a detail.", question: "Which account?", documents: [] }));
1126
+ return { status: "idle" };
1127
+ };
1128
+ const reblocked = await executeHerdrSubagents({ action: "respond", runId: basename(reblockRun), task: 1, answer: "ASK AGAIN please" }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
1129
+ check("a continued failed task may request input and becomes a normal blocked task", !reblocked.isError && reblocked.details.status === "blocked" && reblocked.details.question === "Which account?" && readTaskJson(reblockTask).status === "blocked");
1130
+ const reblockAnswer = await executeHerdrSubagents({ action: "respond", runId: basename(reblockRun), task: 1, answer: "the main account" }, undefined, undefined, { cwd: reblockRoot, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: reblockHerdr });
1131
+ check("a task that blocked during its continuation is answerable like any blocked task", !reblockAnswer.isError && readTaskJson(reblockTask).status === "completed");
1132
+
1133
+ const rejectRoot = join(root, "continue-reject-matrix");
1134
+ const rejectRunDirectory = join(rejectRoot, ".pi", "herdr-subagents", "runs", "reject");
1135
+ mkdirSync(join(rejectRunDirectory, "tasks"), { recursive: true });
1136
+ writeFileSync(join(rejectRunDirectory, "run.json"), JSON.stringify({ id: "reject", label: "reject", status: "running", startedAt: new Date().toISOString() }));
1137
+ const rejectedStatuses = ["completed", "cancelled", "queued", "running"];
1138
+ rejectedStatuses.forEach((status, index) => {
1139
+ const directory = join(rejectRunDirectory, "tasks", `0${index + 1}-task`);
1140
+ mkdirSync(directory, { recursive: true });
1141
+ const sessionFile = join(directory, "session.jsonl");
1142
+ writeFileSync(sessionFile, "{}\n");
1143
+ writeFileSync(join(directory, "task.json"), JSON.stringify({ id: `reject-${index + 1}`, runId: "reject", order: index + 1, name: status, agent: "explorer", status, paneId: "w1:p9", sessionFile }));
1144
+ });
1145
+ const rejectHerdr = fakeHerdr();
1146
+ const rejections = [];
1147
+ for (const order of [1, 2, 3, 4, 9]) {
1148
+ rejections.push(await executeHerdrSubagents({ action: "respond", runId: "reject", task: order, answer: "continue" }, undefined, undefined, { cwd: rejectRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: rejectHerdr }));
1149
+ }
1150
+ check("respond refuses every task state other than blocked or failed, without side effects", rejections.every((result) => result.isError && result.content[0].text.includes("neither blocked nor a failed task")) && rejectHerdr.calls.length === 0 && JSON.parse(readFileSync(join(rejectRunDirectory, "run.json"), "utf8")).status === "running");
1151
+
788
1152
  const twiceBlockedHerdr = fakeHerdr("blocked-twice");
789
1153
  const twiceBlocked = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "first-question", prompt: "first", agent: "explorer" }, { name: "second-question", prompt: "second", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
790
- const secondQuestion = await executeHerdrSubagents({ action: "respond", runId: twiceBlocked.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
1154
+ const secondQuestion = await executeHerdrSubagents({ action: "respond", runId: twiceBlocked.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: twiceBlockedHerdr });
791
1155
  check("queued sibling blocking returns its current question, actions, and Fleet pane", secondQuestion.details.status === "blocked" && secondQuestion.details.question === "Which environment should I inspect?" && secondQuestion.details.allowedActions?.join(",") === "respond,cancel" && secondQuestion.content[0].text.includes(secondQuestion.details.runId) && secondQuestion.details.activity[1].status === "blocked" && Boolean(secondQuestion.details.activity[1].paneId));
792
1156
  const failedAfterBlockHerdr = fakeHerdr("blocked");
793
1157
  const failedAfterBlock = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "question-first", prompt: "question", agent: "explorer" }, { name: "fails-later", prompt: "FAIL sibling", agent: "reviewer" }] }, undefined, undefined, { cwd: root, model: { provider: "fake", id: "model" } }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
794
- const failedAnswer = await executeHerdrSubagents({ action: "respond", runId: failedAfterBlock.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
1158
+ const failedAnswer = await executeHerdrSubagents({ action: "respond", runId: failedAfterBlock.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failedAfterBlockHerdr });
795
1159
  check("failed sibling after answer appears in compact result", failedAnswer.isError && failedAnswer.details.status === "partial" && failedAnswer.content[0].text.includes("fails-later") && failedAnswer.content[0].text.includes("failed"));
796
1160
  const responseFailureHerdr = fakeHerdr("blocked-then-post-fail");
797
1161
  const responseFailureRun = await executeHerdrSubagents({ tasks: [{ name: "response-fails", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
798
- const responseFailure = await executeHerdrSubagents({ action: "respond", runId: responseFailureRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
1162
+ const responseFailure = await executeHerdrSubagents({ action: "respond", runId: responseFailureRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: responseFailureHerdr });
799
1163
  const responseFailureTask = JSON.parse(readFileSync(join(responseFailureRun.details.recordDirectory, "tasks", readdirSync(join(responseFailureRun.details.recordDirectory, "tasks"))[0], "task.json"), "utf8"));
800
1164
  check("failed blocked response settles durable task instead of leaving running", responseFailure.isError && responseFailure.details.status === "failed" && responseFailureTask.status === "failed" && responseFailureHerdr.calls.at(-1)[0] === "closeTab");
801
1165
  const siblingFailureHerdr = fakeHerdr("blocked-then-post-fail");
802
1166
  const siblingFailureRun = await executeHerdrSubagents({ concurrency: 1, tasks: [{ name: "response-fails", prompt: "question", agent: "explorer" }, { name: "queued-sibling", prompt: "later", agent: "reviewer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
803
- await executeHerdrSubagents({ action: "respond", runId: siblingFailureRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
1167
+ await executeHerdrSubagents({ action: "respond", runId: siblingFailureRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingFailureHerdr });
804
1168
  const siblingFailureTasks = readdirSync(join(siblingFailureRun.details.recordDirectory, "tasks")).map((entry) => JSON.parse(readFileSync(join(siblingFailureRun.details.recordDirectory, "tasks", entry, "task.json"), "utf8")));
805
1169
  check("failed blocked response releases its slot and continues queued siblings", siblingFailureTasks.every((task) => ["failed", "completed"].includes(task.status)) && siblingFailureTasks.every((task) => task.status !== "cancelled") && siblingFailureHerdr.calls.filter(([name]) => name === "promptAgent").length === 3);
806
1170
  const abortedContinuationHerdr = fakeHerdr("blocked");
@@ -819,7 +1183,7 @@ try {
819
1183
  const deadPaneHerdr = fakeHerdr("blocked");
820
1184
  const deadPaneRun = await executeHerdrSubagents({ tasks: [{ name: "dead-pane", prompt: "question", agent: "explorer" }] }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
821
1185
  deadPaneHerdr.paneExists = async () => false;
822
- const deadPaneResume = await executeHerdrSubagents({ action: "respond", runId: deadPaneRun.details.runId, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
1186
+ const deadPaneResume = await executeHerdrSubagents({ action: "respond", runId: deadPaneRun.details.runId, task: 1, answer: "main" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: deadPaneHerdr });
823
1187
  const deadPaneTaskDirectory = join(deadPaneRun.details.recordDirectory, "tasks", readdirSync(join(deadPaneRun.details.recordDirectory, "tasks"))[0]);
824
1188
  const deadPaneTask = JSON.parse(readFileSync(join(deadPaneTaskDirectory, "task.json"), "utf8"));
825
1189
  check("dead blocked pane recovers a coherent attempt with its persona prompt", !deadPaneResume.isError && deadPaneHerdr.calls.filter(([name]) => name === "createTab").length === 2 && deadPaneTask.tabId === "w1:t2" && deadPaneTask.attempt === 2 && deadPaneTask.currentTurnFile.endsWith("02.json") && existsSync(deadPaneTask.currentTurnFile) && readFileSync(join(deadPaneTaskDirectory, "resume-system-prompt.md"), "utf8").includes("Investigate the assigned question without modifying files"));
@@ -829,7 +1193,7 @@ try {
829
1193
  staleTabHerdr.createTab = async (request) => { staleTabHerdr.calls.push(["createTab", request]); return { tabId: "w1:t-new", paneId: "w1:p-new" }; };
830
1194
  staleTabHerdr.closeTab = async (tabId) => { staleTabHerdr.calls.push(["closeTab", { tabId }]); if (tabId === "w1:t2") throw new Error("stale tab still busy"); };
831
1195
  const staleTabUpdates = [];
832
- const staleTabResume = await executeHerdrSubagents({ action: "respond", runId: staleTabRun.details.runId, answer: "main" }, undefined, (update) => staleTabUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleTabHerdr });
1196
+ const staleTabResume = await executeHerdrSubagents({ action: "respond", runId: staleTabRun.details.runId, task: 1, answer: "main" }, undefined, (update) => staleTabUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: staleTabHerdr });
833
1197
  const staleTabRecord = JSON.parse(readFileSync(join(staleTabRun.details.recordDirectory, "run.json"), "utf8"));
834
1198
  check("dead-pane migration persists and surfaces stale tab cleanup failure", !staleTabResume.isError && staleTabRecord.cleanupError === "stale tab still busy" && staleTabRecord.staleTabIds?.includes("w1:t2") && staleTabUpdates.some((update) => update.content[0].text.includes("Could not close stale Herdr tab")));
835
1199
  const invalidTurnHerdr = fakeHerdr("blocked");
@@ -843,12 +1207,12 @@ try {
843
1207
  invalidTurnTask.currentTurnFile = invalidTurnFile;
844
1208
  writeFileSync(invalidTurnTaskFile, JSON.stringify(invalidTurnTask));
845
1209
  const invalidTurnUpdates = [];
846
- await executeHerdrSubagents({ action: "respond", runId: invalidTurnRun.details.runId, answer: "main" }, undefined, (update) => invalidTurnUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: invalidTurnHerdr });
1210
+ await executeHerdrSubagents({ action: "respond", runId: invalidTurnRun.details.runId, task: 1, answer: "main" }, undefined, (update) => invalidTurnUpdates.push(update), { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: invalidTurnHerdr });
847
1211
  check("invalid durable turn is archived and surfaced before recovery", readdirSync(join(invalidTurnTaskDirectory, "turns")).some((name) => name.startsWith("01.json.invalid-")) && invalidTurnUpdates.some((update) => update.content[0].text.includes("Archived invalid durable turn")));
848
1212
  const corruptLookupRoot = join(root, "corrupt-run-lookup");
849
1213
  mkdirSync(join(corruptLookupRoot, ".pi", "herdr-subagents", "runs", "bad"), { recursive: true });
850
1214
  writeFileSync(join(corruptLookupRoot, ".pi", "herdr-subagents", "runs", "bad", "run.json"), "{truncated");
851
- const corruptLookup = await executeHerdrSubagents({ action: "respond", runId: "missing", answer: "main" }, undefined, undefined, { cwd: corruptLookupRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
1215
+ const corruptLookup = await executeHerdrSubagents({ action: "respond", runId: "missing", task: 1, answer: "main" }, undefined, undefined, { cwd: corruptLookupRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
852
1216
  check("blocked response surfaces unreadable run records instead of reporting unknown", corruptLookup.isError && corruptLookup.content[0].text.includes("Could not inspect Subagent runs"));
853
1217
  const survivingPaneRunFile = join(root, "surviving-pane-run.json");
854
1218
  const survivingPaneRun = { id: "surviving-pane", label: "surviving-pane", status: "blocked", cwd: root, tabId: "w1:t-surviving", paneIds: ["w1:p-live", "w1:p-dead"] };
@@ -895,8 +1259,68 @@ try {
895
1259
  const historicalTaskRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
896
1260
  const historicalTurnRecord = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "turns", "02.json"), "utf8"));
897
1261
  check("historical task and turn retain their tab identity", historicalTaskRecord.tabId === historicalResume.details.tabId && historicalTurnRecord.tabId === historicalResume.details.tabId);
1262
+ const lockPublicationDirectory = join(root, "lock-publication", ".state.lock");
1263
+ mkdirSync(join(root, "lock-publication"), { recursive: true });
1264
+ let publishEntered;
1265
+ const publicationEntered = new Promise((resolve) => { publishEntered = resolve; });
1266
+ let releasePublication;
1267
+ const publicationGate = new Promise((resolve) => { releasePublication = resolve; });
1268
+ let firstLockAcquired = false;
1269
+ const firstLockPromise = acquireDirectoryLock(lockPublicationDirectory, { wait: true, beforePublish: async () => { publishEntered(); await publicationGate; } }).then((lock) => { firstLockAcquired = true; return lock; });
1270
+ await publicationEntered;
1271
+ const secondPublishedLock = await acquireDirectoryLock(lockPublicationDirectory, { wait: false });
1272
+ releasePublication();
1273
+ await new Promise((resolve) => setTimeout(resolve, 20));
1274
+ const publicationWasExclusive = !firstLockAcquired;
1275
+ await secondPublishedLock.release();
1276
+ const firstPublishedLock = await firstLockPromise;
1277
+ await firstPublishedLock.release();
1278
+ check("lock owner publication gate cannot produce two owners", publicationWasExclusive && !existsSync(lockPublicationDirectory));
1279
+
1280
+ const allocationCrashRoot = join(root, "allocation-crash-recovery");
1281
+ const allocationCrashRun = join(allocationCrashRoot, ".pi", "herdr-subagents", "runs", "run");
1282
+ const allocationCrashTask = join(allocationCrashRun, "tasks", "01-task");
1283
+ mkdirSync(allocationCrashTask, { recursive: true });
1284
+ writeFileSync(join(allocationCrashRun, "run.json"), JSON.stringify({ id: "allocation-crash", label: "allocation-crash", status: "running", tabId: "w1:t-allocation", resume: { generation: 1, phase: "active", tabId: "w1:t-allocation", paneIds: ["w1:p-allocation"], active: { task: { paneId: "w1:p-allocation", generation: 1, agentName: "agent", attemptId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa", ownerProcessId: 99999999, ownerToken: "dead", phase: "allocating" } } } }));
1285
+ writeFileSync(join(allocationCrashTask, "task.json"), JSON.stringify({ id: "task", runId: "allocation-crash", order: 1, name: "task", status: "completed", sessionFile: join(allocationCrashTask, "session.jsonl") }));
1286
+ const allocationCrashHerdr = fakeHerdr();
1287
+ await reconcileSubagentRuns(allocationCrashRoot, allocationCrashHerdr);
1288
+ const recoveredAllocationRun = JSON.parse(readFileSync(join(allocationCrashRun, "run.json"), "utf8"));
1289
+ const recoveredAllocationTask = JSON.parse(readFileSync(join(allocationCrashTask, "task.json"), "utf8"));
1290
+ check("allocating member crash window is failed and claims final tab cleanup", recoveredAllocationTask.status === "cancelled" && recoveredAllocationTask.currentAttemptId === "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" && !recoveredAllocationRun.resume && allocationCrashHerdr.calls.some(([name]) => name === "closeTab"));
1291
+
1292
+ const staleReportedRoot = join(root, "stale-reported-resume-member");
1293
+ const staleReportedRun = join(staleReportedRoot, ".pi", "herdr-subagents", "runs", "run");
1294
+ const staleReportedTask = join(staleReportedRun, "tasks", "01-task");
1295
+ const staleReportedAttempt = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
1296
+ mkdirSync(join(staleReportedTask, "reports"), { recursive: true });
1297
+ writeFileSync(join(staleReportedRun, "run.json"), JSON.stringify({ id: "stale-reported", label: "stale-reported", status: "running", tabId: "w1:t-stale-reported", resume: { generation: 1, phase: "active", tabId: "w1:t-stale-reported", paneIds: ["w1:p-stale-reported"], active: { task: { paneId: "w1:p-stale-reported", generation: 1, agentName: "agent", attemptId: staleReportedAttempt, ownerSessionId: "dead-owner", ownerProcessId: 99999999, ownerToken: "dead", phase: "running" } } } }));
1298
+ writeFileSync(join(staleReportedTask, "task.json"), JSON.stringify({ id: "task", runId: "stale-reported", order: 1, name: "task", status: "running", currentAttemptId: staleReportedAttempt, sessionFile: join(staleReportedTask, "session.jsonl") }));
1299
+ const staleReportedPayload = { attemptId: staleReportedAttempt, status: "completed", summary: "Completed before the resume owner disappeared.", documents: [] };
1300
+ writeFileSync(join(staleReportedTask, "reports", `${staleReportedAttempt}.json`), JSON.stringify(staleReportedPayload));
1301
+ const staleReportedReconciliation = await reconcileSubagentRuns(staleReportedRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
1302
+ const staleReportedTaskRecord = JSON.parse(readFileSync(join(staleReportedTask, "task.json"), "utf8"));
1303
+ const staleReportedCanonical = JSON.parse(readFileSync(join(staleReportedTask, "report.json"), "utf8"));
1304
+ check("stale historical resume member adopts its completed scoped report without cancellation", staleReportedReconciliation.settledTasks === 1 && staleReportedReconciliation.cancelledTasks === 0 && staleReportedTaskRecord.status === "completed" && staleReportedCanonical.status === "completed" && staleReportedCanonical.attemptId === staleReportedAttempt && staleReportedCanonical.summary === staleReportedPayload.summary);
1305
+
1306
+ const ownerMembersRoot = join(root, "owner-member-cancel");
1307
+ const ownerMembersRun = join(ownerMembersRoot, ".pi", "herdr-subagents", "runs", "run");
1308
+ mkdirSync(join(ownerMembersRun, "tasks", "01-a"), { recursive: true });
1309
+ mkdirSync(join(ownerMembersRun, "tasks", "02-b"), { recursive: true });
1310
+ const ownerMembers = {
1311
+ a: { paneId: "w1:p-a", generation: 1, agentName: "a", attemptId: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaa1", ownerSessionId: "owner-a", ownerProcessId: process.pid, ownerToken: "a", phase: "running" },
1312
+ b: { paneId: "w1:p-b", generation: 1, agentName: "b", attemptId: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbb2", ownerSessionId: "owner-b", ownerProcessId: process.pid, ownerToken: "b", phase: "running" },
1313
+ };
1314
+ writeFileSync(join(ownerMembersRun, "run.json"), JSON.stringify({ id: "owner-members", label: "owner-members", status: "running", tabId: "w1:t-members", resume: { generation: 1, phase: "active", tabId: "w1:t-members", paneIds: ["w1:p-a", "w1:p-b"], active: ownerMembers } }));
1315
+ writeFileSync(join(ownerMembersRun, "tasks", "01-a", "task.json"), JSON.stringify({ id: "a", runId: "owner-members", order: 1, status: "running", currentAttemptId: ownerMembers.a.attemptId, sessionFile: join(ownerMembersRun, "tasks", "01-a", "session.jsonl") }));
1316
+ writeFileSync(join(ownerMembersRun, "tasks", "02-b", "task.json"), JSON.stringify({ id: "b", runId: "owner-members", order: 2, status: "running", currentAttemptId: ownerMembers.b.attemptId, sessionFile: join(ownerMembersRun, "tasks", "02-b", "session.jsonl") }));
1317
+ const ownerMembersHerdr = fakeHerdr();
1318
+ await cancelActiveSubagentRuns(ownerMembersRoot, ownerMembersHerdr, { owner: { sessionId: "owner-a", processId: process.pid } });
1319
+ const ownerMembersAfter = JSON.parse(readFileSync(join(ownerMembersRun, "run.json"), "utf8"));
1320
+ check("member cancellation cannot cancel a sibling owner or close its shared tab", !ownerMembersAfter.resume.active.a && ownerMembersAfter.resume.active.b && JSON.parse(readFileSync(join(ownerMembersRun, "tasks", "02-b", "task.json"), "utf8")).status === "running" && !ownerMembersHerdr.calls.some(([name]) => name === "closeTab"));
1321
+
898
1322
  const resumeRaceRoot = join(root, "resume-race");
899
- const resumeRaceSeed = await runHerdrSubagents("original", { cwd: resumeRaceRoot, herdr: fakeHerdr() });
1323
+ const resumeRaceSeed = await runSingleTask("original", { cwd: resumeRaceRoot, herdr: fakeHerdr() });
900
1324
  const resumeRaceHerdr = fakeHerdr();
901
1325
  const createRaceTab = resumeRaceHerdr.createTab.bind(resumeRaceHerdr);
902
1326
  let beginFirstCreate;
@@ -915,7 +1339,90 @@ try {
915
1339
  releaseFirstCreate();
916
1340
  await firstResume;
917
1341
  check("concurrent historical resume cannot start a second Pi session writer", competingResume.isError && raceCreates === 1 && resumeRaceHerdr.calls.filter(([name]) => name === "startAgent").length === 1);
918
- check("historical resume releases the run lock after settlement", !existsSync(join(resumeRaceSeed.recordDirectory, ".resume.lock")));
1342
+ const resumeRaceTaskDirectory = (await listSubagentHistory(resumeRaceRoot)).find((run) => run.id === resumeRaceSeed.runId).tasks[0].recordDirectory;
1343
+ check("historical resume releases the task lock after settlement", !existsSync(join(resumeRaceTaskDirectory, ".resume.lock")));
1344
+ const staleTaskLock = join(resumeRaceTaskDirectory, ".resume.lock");
1345
+ mkdirSync(staleTaskLock);
1346
+ writeFileSync(join(staleTaskLock, "owner.json"), JSON.stringify({ token: "dead-owner", processId: 99999999, createdAt: "2000-01-01T00:00:00.000Z" }));
1347
+ const recoveredTaskResume = await executeHerdrSubagents({ action: "resume", runId: resumeRaceSeed.runId, task: 1, prompt: "recover stale task lock" }, undefined, undefined, { cwd: resumeRaceRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() });
1348
+ check("historical resume automatically reclaims a dead-PID task lock", !recoveredTaskResume.isError && !existsSync(staleTaskLock));
1349
+
1350
+ const parallelResumeRoot = join(root, "parallel-historical-resume");
1351
+ const parallelSeed = (await executeHerdrSubagents({ tasks: [{ name: "one", prompt: "one", agent: "explorer" }, { name: "two", prompt: "two", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
1352
+ const parallelHerdr = fakeHerdr();
1353
+ const parallelPrompt = parallelHerdr.promptAgent.bind(parallelHerdr);
1354
+ const parallelRename = parallelHerdr.renamePane.bind(parallelHerdr);
1355
+ let secondAllocationEntered;
1356
+ const secondAllocation = new Promise((resolve) => { secondAllocationEntered = resolve; });
1357
+ let releaseSecondAllocation;
1358
+ const secondAllocationGate = new Promise((resolve) => { releaseSecondAllocation = resolve; });
1359
+ let parallelRenames = 0;
1360
+ parallelHerdr.renamePane = async (paneId, label) => {
1361
+ parallelRenames += 1;
1362
+ if (parallelRenames === 2) { secondAllocationEntered(); await secondAllocationGate; }
1363
+ return parallelRename(paneId, label);
1364
+ };
1365
+ let firstParallelEntered;
1366
+ let secondParallelEntered;
1367
+ const firstEntered = new Promise((resolve) => { firstParallelEntered = resolve; });
1368
+ const secondEntered = new Promise((resolve) => { secondParallelEntered = resolve; });
1369
+ let releaseFirstParallel;
1370
+ let releaseSecondParallel;
1371
+ const firstGate = new Promise((resolve) => { releaseFirstParallel = resolve; });
1372
+ const secondGate = new Promise((resolve) => { releaseSecondParallel = resolve; });
1373
+ let parallelPrompts = 0;
1374
+ parallelHerdr.promptAgent = async (request) => {
1375
+ parallelPrompts += 1;
1376
+ if (parallelPrompts === 1) { firstParallelEntered(); await firstGate; }
1377
+ else { secondParallelEntered(); await secondGate; }
1378
+ return parallelPrompt(request);
1379
+ };
1380
+ const parallelFirst = executeHerdrSubagents({ action: "resume", runId: parallelSeed.runId, task: 1, prompt: "resume one" }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelHerdr });
1381
+ await firstEntered;
1382
+ const parallelSecond = executeHerdrSubagents({ action: "resume", runId: parallelSeed.runId, task: 2, prompt: "resume two" }, undefined, undefined, { cwd: parallelResumeRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: parallelHerdr });
1383
+ await secondAllocation;
1384
+ const activeParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
1385
+ const activeMembers = Object.values(activeParallel.resume.active);
1386
+ check("different historical tasks share one tab with distinct panes without losing state", parallelHerdr.calls.filter(([name]) => name === "createTab").length === 1 && parallelHerdr.calls.filter(([name]) => name === "splitPane").length === 1 && activeMembers.length === 2 && new Set(activeMembers.map((member) => member.paneId)).size === 2 && activeParallel.resume.paneIds.length === 2);
1387
+ releaseFirstParallel();
1388
+ await parallelFirst;
1389
+ const afterFirstParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
1390
+ check("active resume membership keeps aggregate running before sibling task.json is persisted", afterFirstParallel.status === "running" && Object.keys(afterFirstParallel.resume.active).length === 1);
1391
+ check("first historical sibling completion does not close the shared tab", Object.keys(afterFirstParallel.resume.active).length === 1 && parallelHerdr.calls.filter(([name]) => name === "closeTab").length === 0);
1392
+ releaseSecondAllocation();
1393
+ await secondEntered;
1394
+ releaseSecondParallel();
1395
+ await parallelSecond;
1396
+ const afterParallel = JSON.parse(readFileSync(join(parallelSeed.recordDirectory, "run.json"), "utf8"));
1397
+ check("last historical sibling atomically claims and closes the shared tab", parallelHerdr.calls.filter(([name]) => name === "closeTab").length === 1 && !afterParallel.resume && !afterParallel.cleanupPendingTabIds);
1398
+
1399
+ const failureSiblingRoot = join(root, "failed-historical-sibling");
1400
+ const failureSiblingSeed = (await executeHerdrSubagents({ tasks: [{ name: "fails", prompt: "seed one", agent: "explorer" }, { name: "continues", prompt: "seed two", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
1401
+ const failureSiblingHerdr = fakeHerdr();
1402
+ const failurePrompt = failureSiblingHerdr.promptAgent.bind(failureSiblingHerdr);
1403
+ let failureEnteredCount = 0;
1404
+ let releaseFailure;
1405
+ let releaseSurvivor;
1406
+ const failureGate = new Promise((resolve) => { releaseFailure = resolve; });
1407
+ const survivorGate = new Promise((resolve) => { releaseSurvivor = resolve; });
1408
+ let bothFailureEntered;
1409
+ const bothEntered = new Promise((resolve) => { bothFailureEntered = resolve; });
1410
+ failureSiblingHerdr.promptAgent = async (request) => {
1411
+ failureEnteredCount += 1;
1412
+ if (failureEnteredCount === 2) bothFailureEntered();
1413
+ await (request.prompt.includes("FAIL") ? failureGate : survivorGate);
1414
+ return failurePrompt(request);
1415
+ };
1416
+ const failingResume = executeHerdrSubagents({ action: "resume", runId: failureSiblingSeed.runId, task: 1, prompt: "FAIL this turn" }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failureSiblingHerdr });
1417
+ const survivingResume = executeHerdrSubagents({ action: "resume", runId: failureSiblingSeed.runId, task: 2, prompt: "keep running" }, undefined, undefined, { cwd: failureSiblingRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: failureSiblingHerdr });
1418
+ await bothEntered;
1419
+ releaseFailure();
1420
+ const failedSiblingResult = await failingResume;
1421
+ const whileSiblingRuns = JSON.parse(readFileSync(join(failureSiblingSeed.recordDirectory, "run.json"), "utf8"));
1422
+ check("one historical failure leaves its sibling running", failedSiblingResult.isError && Object.keys(whileSiblingRuns.resume.active).length === 1 && failureSiblingHerdr.calls.filter(([name]) => name === "closeTab").length === 0);
1423
+ releaseSurvivor();
1424
+ await survivingResume;
1425
+
919
1426
  const partialResumeHerdr = fakeHerdr();
920
1427
  const partialResumeTaskFile = join((await listSubagentHistory(root)).find((run) => run.id === failedAfterBlock.details.runId).tasks[0].recordDirectory, "task.json");
921
1428
  const stalePartialTask = JSON.parse(readFileSync(partialResumeTaskFile, "utf8"));
@@ -937,14 +1444,25 @@ try {
937
1444
  historicalCancellationController.abort(new Error("Parent Pi session closed."));
938
1445
  await historicalCancellationPromise;
939
1446
  const historicalCancelledRun = JSON.parse(readFileSync(join(failedAfterBlock.details.recordDirectory, "run.json"), "utf8"));
940
- check("parent shutdown marks a historical follow-up run cancelled despite completed siblings", historicalCancelledRun.status === "cancelled" && historicalCancelledRun.error.includes("Parent Pi session closed"));
1447
+ check("parent shutdown cancels only its historical member and preserves sibling aggregation", historicalCancelledRun.status === "partial");
941
1448
  const followupBlockedHerdr = fakeHerdr("blocked");
942
1449
  const blockedFollowup = await executeHerdrSubagents({ action: "resume", runId: backgroundId, task: 1, prompt: "ask a follow-up" }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: followupBlockedHerdr });
943
1450
  const historicalTurns = join(stableHistory.tasks[0].recordDirectory, "turns");
944
- const failedInputTurn = JSON.parse(readFileSync(join(historicalTurns, "03.json"), "utf8"));
945
- const failedInputTask = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
946
- const failedInputAttempt = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "reports", `${failedInputTask.currentAttemptId}.json`), "utf8"));
947
- check("historical input requests fail instead of creating unowned blocked work", blockedFollowup.isError && blockedFollowup.details.status === "failed" && failedInputTurn.status === "failed" && failedInputAttempt.failureKind === "missing_input" && followupBlockedHerdr.calls.at(-1)[0] === "closeTab");
1451
+ const blockedInputTurn = JSON.parse(readFileSync(join(historicalTurns, "03.json"), "utf8"));
1452
+ const blockedInputTask = JSON.parse(readFileSync(join(stableHistory.tasks[0].recordDirectory, "task.json"), "utf8"));
1453
+ check("historical input requests stay blocked in the shared resume tab", !blockedFollowup.isError && blockedFollowup.details.status === "blocked" && blockedInputTurn.status === "blocked" && blockedInputTask.status === "blocked" && !followupBlockedHerdr.calls.some(([name]) => name === "closeTab"));
1454
+ const answeredHistorical = await executeHerdrSubagents({ action: "respond", runId: backgroundId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: root }, { agentsDirectory: join(process.cwd(), "agents"), herdr: followupBlockedHerdr });
1455
+ check("historical blocked resume settles through respond before tab cleanup", !answeredHistorical.isError && answeredHistorical.details.status === "completed" && followupBlockedHerdr.calls.at(-1)[0] === "closeTab");
1456
+
1457
+ const siblingInputRoot = join(root, "historical-sibling-input");
1458
+ const siblingInputSeed = (await executeHerdrSubagents({ tasks: [{ name: "branch", prompt: "seed branch", agent: "explorer" }, { name: "environment", prompt: "seed environment", agent: "reviewer" }], concurrency: 2 }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: fakeHerdr() })).details;
1459
+ const siblingInputHerdr = fakeHerdr("two-blocked-then-complete");
1460
+ const firstSiblingInput = await executeHerdrSubagents({ action: "resume", runId: siblingInputSeed.runId, task: 1, prompt: "ask for branch" }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
1461
+ const secondSiblingInput = await executeHerdrSubagents({ action: "resume", runId: siblingInputSeed.runId, task: 2, prompt: "ask for environment" }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
1462
+ const answeredOneSibling = await executeHerdrSubagents({ action: "respond", runId: siblingInputSeed.runId, task: 1, answer: "Inspect main." }, undefined, undefined, { cwd: siblingInputRoot }, { agentsDirectory: join(process.cwd(), "agents"), herdr: siblingInputHerdr });
1463
+ const siblingInputRun = JSON.parse(readFileSync(join(siblingInputSeed.recordDirectory, "run.json"), "utf8"));
1464
+ check("responding to one historical blocked sibling exposes the remaining sibling question", firstSiblingInput.details.status === "blocked" && secondSiblingInput.details.status === "blocked" && !answeredOneSibling.isError && answeredOneSibling.details.status === "blocked" && answeredOneSibling.details.question === "Which environment should I inspect?" && answeredOneSibling.details.tasks[1].question === "Which environment should I inspect?" && siblingInputRun.status === "blocked" && siblingInputRun.question === "Which environment should I inspect?");
1465
+
948
1466
  const prePromptFailureTaskFile = join(stableHistory.tasks[0].recordDirectory, "task.json");
949
1467
  const prePromptStaleTask = JSON.parse(readFileSync(prePromptFailureTaskFile, "utf8"));
950
1468
  Object.assign(prePromptStaleTask, { question: "stale question", error: "stale error", completedAt: "2000-01-01T00:00:00.000Z" });
@@ -1086,7 +1604,7 @@ try {
1086
1604
  const currentAttemptId = "22222222-2222-4222-8222-222222222222";
1087
1605
  writeFileSync(join(isolatedAttemptRun, "run.json"), JSON.stringify({ id: "isolated-attempt", label: "isolated-attempt", status: "running", startedAt: new Date().toISOString() }));
1088
1606
  writeFileSync(join(isolatedAttemptTask, "task.json"), JSON.stringify({ id: "isolated-attempt-task", runId: "isolated-attempt", order: 1, status: "running", paneId: "w1:p-live", currentAttemptId, sessionFile: join(isolatedAttemptTask, "session.jsonl") }));
1089
- writeFileSync(join(isolatedAttemptTask, "report.json"), JSON.stringify({ attemptId: "11111111-1111-4111-8111-111111111111", status: "needs-input", summary: "Prior attempt blocked.", question: "Old question?", documents: [] }));
1607
+ writeFileSync(join(isolatedAttemptTask, "report.json"), JSON.stringify({ attemptId: "11111111-1111-4111-8111-111111111111", status: "blocked", summary: "Prior attempt blocked.", question: "Old question?", documents: [] }));
1090
1608
  const isolatedReconciliation = await reconcileSubagentRuns(isolatedAttemptRoot, fakeHerdr());
1091
1609
  const isolatedTaskRecord = JSON.parse(readFileSync(join(isolatedAttemptTask, "task.json"), "utf8"));
1092
1610
  check("reconciliation ignores a prior canonical report while the current attempt is live", isolatedReconciliation.liveTasks === 1 && isolatedTaskRecord.status === "running" && isolatedTaskRecord.currentAttemptId === currentAttemptId && JSON.parse(readFileSync(join(isolatedAttemptTask, "report.json"), "utf8")).summary === "Prior attempt blocked.");
@@ -1107,7 +1625,7 @@ try {
1107
1625
  mkdirSync(join(blockedOwnerLossTask, "reports"), { recursive: true });
1108
1626
  writeFileSync(join(blockedOwnerLossRun, "run.json"), JSON.stringify({ id: "blocked-owner-loss", label: "blocked-owner-loss", status: "running", effectiveConcurrency: 1, ownerSessionId: "dead-owner", ownerProcessId: 99999999, tabId: "w1:t-blocked-owner-loss", startedAt: new Date().toISOString() }));
1109
1627
  writeFileSync(join(blockedOwnerLossTask, "task.json"), JSON.stringify({ id: "blocked-owner-loss-task", runId: "blocked-owner-loss", order: 1, status: "running", paneId: "w1:p-blocked-owner-loss", currentAttemptId: blockedOwnerLossAttempt, sessionFile: join(blockedOwnerLossTask, "session.jsonl") }));
1110
- writeFileSync(join(blockedOwnerLossTask, "reports", `${blockedOwnerLossAttempt}.json`), JSON.stringify({ attemptId: blockedOwnerLossAttempt, status: "needs-input", summary: "Need input before owner loss.", question: "Which branch?", documents: [] }));
1628
+ writeFileSync(join(blockedOwnerLossTask, "reports", `${blockedOwnerLossAttempt}.json`), JSON.stringify({ attemptId: blockedOwnerLossAttempt, status: "blocked", summary: "Need input before owner loss.", question: "Which branch?", documents: [] }));
1111
1629
  const blockedOwnerLossHerdr = fakeHerdr();
1112
1630
  await reconcileSubagentRuns(blockedOwnerLossRoot, blockedOwnerLossHerdr, { sessionId: "new-owner", processId: process.pid });
1113
1631
  const cancelledBlockedOwnerTask = JSON.parse(readFileSync(join(blockedOwnerLossTask, "task.json"), "utf8"));
@@ -1121,12 +1639,12 @@ try {
1121
1639
  mkdirSync(join(parallelInputRecoveryTask, "reports"), { recursive: true });
1122
1640
  writeFileSync(join(parallelInputRecoveryRun, "run.json"), JSON.stringify({ id: "parallel-input-recovery", label: "parallel-input-recovery", status: "running", effectiveConcurrency: 2, ownerSessionId: "dead-owner", ownerProcessId: 99999999, tabId: "w1:t-parallel-input", startedAt: new Date().toISOString() }));
1123
1641
  writeFileSync(join(parallelInputRecoveryTask, "task.json"), JSON.stringify({ id: "parallel-input-task", runId: "parallel-input-recovery", order: 1, status: "running", paneId: "w1:p-parallel-input", currentAttemptId: parallelInputRecoveryAttempt, sessionFile: join(parallelInputRecoveryTask, "session.jsonl") }));
1124
- writeFileSync(join(parallelInputRecoveryTask, "reports", `${parallelInputRecoveryAttempt}.json`), JSON.stringify({ attemptId: parallelInputRecoveryAttempt, status: "needs-input", summary: "Need a branch.", question: "Which branch?", documents: [] }));
1642
+ writeFileSync(join(parallelInputRecoveryTask, "reports", `${parallelInputRecoveryAttempt}.json`), JSON.stringify({ attemptId: parallelInputRecoveryAttempt, status: "blocked", summary: "Need a branch.", question: "Which branch?", documents: [] }));
1125
1643
  await reconcileSubagentRuns(parallelInputRecoveryRoot, fakeHerdr(), { sessionId: "new-owner", processId: process.pid });
1126
1644
  const recoveredParallelInputTask = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "task.json"), "utf8"));
1127
1645
  const recoveredParallelInputReport = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "report.json"), "utf8"));
1128
1646
  const recoveredParallelInputAttempt = JSON.parse(readFileSync(join(parallelInputRecoveryTask, "reports", `${parallelInputRecoveryAttempt}.json`), "utf8"));
1129
- check("reconciliation keeps concurrent input requests out of blocked state", recoveredParallelInputTask.status === "failed" && recoveredParallelInputTask.failureKind === "missing_input" && recoveredParallelInputReport.status === "failed" && recoveredParallelInputReport.requiredInput === "Which branch?" && recoveredParallelInputAttempt.status === "failed" && recoveredParallelInputAttempt.failureKind === "missing_input" && JSON.parse(readFileSync(join(parallelInputRecoveryRun, "run.json"), "utf8")).status === "failed");
1647
+ check("reconciliation treats a blocked report identically at any concurrency", recoveredParallelInputTask.status === "cancelled" && recoveredParallelInputReport.status === "failed" && !recoveredParallelInputReport.question && recoveredParallelInputAttempt.status === "failed" && !recoveredParallelInputAttempt.question && JSON.parse(readFileSync(join(parallelInputRecoveryRun, "run.json"), "utf8")).status === "cancelled");
1130
1648
  const malformedRoot = join(root, "malformed-recovery");
1131
1649
  const malformedRun = join(malformedRoot, ".pi", "herdr-subagents", "runs", "run");
1132
1650
  const malformedTask = join(malformedRun, "tasks", "01-task");
@@ -1228,6 +1746,7 @@ try {
1228
1746
  const queuedRecoveryHerdr = fakeHerdr();
1229
1747
  await reconcileSubagentRuns(queuedRecoveryRoot, queuedRecoveryHerdr);
1230
1748
  check("reconciliation preserves unstarted task records as queued", JSON.parse(readFileSync(join(secondRecoveryTask, "task.json"), "utf8")).status === "queued" && JSON.parse(readFileSync(join(thirdRecoveryTask, "task.json"), "utf8")).status === "queued");
1749
+ queuedRecoveryHerdr.expectConcurrentPrompts(2);
1231
1750
  await continueQueuedRun(queuedRunDirectory, queuedRecoveryHerdr);
1232
1751
  check("startup continuation restores bounded concurrency", queuedRecoveryHerdr.maxActive === 2 && JSON.parse(readFileSync(join(secondRecoveryTask, "task.json"), "utf8")).status === "completed" && JSON.parse(readFileSync(join(thirdRecoveryTask, "task.json"), "utf8")).status === "completed" && JSON.parse(readFileSync(join(queuedRunDirectory, "run.json"), "utf8")).status === "completed");
1233
1752
  const staleContinuationRoot = join(root, "stale-continuation");