taskplane 0.29.2 → 0.30.0

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 (41) hide show
  1. package/bin/gitignore-patterns.mjs +11 -8
  2. package/bin/rpc-wrapper.mjs +410 -357
  3. package/bin/taskplane.mjs +533 -250
  4. package/extensions/reviewer-extension.ts +17 -11
  5. package/extensions/taskplane/abort.ts +50 -18
  6. package/extensions/taskplane/agent-bridge-extension.ts +232 -105
  7. package/extensions/taskplane/agent-host.ts +224 -97
  8. package/extensions/taskplane/cleanup.ts +71 -42
  9. package/extensions/taskplane/config-loader.ts +142 -58
  10. package/extensions/taskplane/config-schema.ts +6 -13
  11. package/extensions/taskplane/config.ts +10 -2
  12. package/extensions/taskplane/diagnostic-reports.ts +59 -47
  13. package/extensions/taskplane/diagnostics.ts +13 -13
  14. package/extensions/taskplane/discovery.ts +35 -61
  15. package/extensions/taskplane/engine-worker.ts +53 -46
  16. package/extensions/taskplane/engine.ts +1760 -602
  17. package/extensions/taskplane/execution.ts +426 -206
  18. package/extensions/taskplane/extension.ts +1073 -598
  19. package/extensions/taskplane/formatting.ts +136 -124
  20. package/extensions/taskplane/git.ts +0 -2
  21. package/extensions/taskplane/lane-runner.ts +542 -311
  22. package/extensions/taskplane/mailbox.ts +57 -49
  23. package/extensions/taskplane/merge.ts +662 -383
  24. package/extensions/taskplane/messages.ts +109 -51
  25. package/extensions/taskplane/migrations.ts +1 -1
  26. package/extensions/taskplane/path-resolver.ts +8 -9
  27. package/extensions/taskplane/persistence.ts +425 -262
  28. package/extensions/taskplane/process-registry.ts +36 -7
  29. package/extensions/taskplane/quality-gate.ts +107 -55
  30. package/extensions/taskplane/resume.ts +774 -267
  31. package/extensions/taskplane/sessions.ts +1 -1
  32. package/extensions/taskplane/settings-tui.ts +505 -164
  33. package/extensions/taskplane/sidecar-telemetry.ts +25 -10
  34. package/extensions/taskplane/supervisor.ts +477 -270
  35. package/extensions/taskplane/task-executor-core.ts +178 -53
  36. package/extensions/taskplane/types.ts +186 -108
  37. package/extensions/taskplane/verification.ts +27 -22
  38. package/extensions/taskplane/waves.ts +59 -43
  39. package/extensions/taskplane/workspace.ts +14 -12
  40. package/extensions/taskplane/worktree.ts +218 -196
  41. package/package.json +14 -2
@@ -28,12 +28,37 @@
28
28
 
29
29
  import { join, dirname } from "path";
30
30
  import { fileURLToPath } from "url";
31
- import { existsSync, readFileSync, readdirSync, writeFileSync, unlinkSync, mkdirSync, renameSync, statSync, openSync, readSync, closeSync, appendFileSync } from "fs";
32
- import { stat as fsStat, open as fsOpen, readFile as fsReadFile, writeFile as fsWriteFile, rename as fsRename } from "fs/promises";
31
+ import {
32
+ existsSync,
33
+ readFileSync,
34
+ readdirSync,
35
+ writeFileSync,
36
+ unlinkSync,
37
+ mkdirSync,
38
+ renameSync,
39
+ statSync,
40
+ openSync,
41
+ readSync,
42
+ closeSync,
43
+ appendFileSync,
44
+ } from "fs";
45
+ import {
46
+ stat as fsStat,
47
+ open as fsOpen,
48
+ readFile as fsReadFile,
49
+ writeFile as fsWriteFile,
50
+ rename as fsRename,
51
+ } from "fs/promises";
33
52
  import { execFileSync } from "child_process";
34
53
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
35
54
  import type { Model, Api } from "@mariozechner/pi-ai";
36
- import type { OrchBatchRuntimeState, OrchestratorConfig, PersistedBatchState, EngineEvent, EngineEventType } from "./types.ts";
55
+ import type {
56
+ OrchBatchRuntimeState,
57
+ OrchestratorConfig,
58
+ PersistedBatchState,
59
+ EngineEvent,
60
+ EngineEventType,
61
+ } from "./types.ts";
37
62
  import type { Tier0Event, Tier0EventType } from "./persistence.ts";
38
63
 
39
64
  // ── Recovery Action Classification (TP-041 Step 4) ───────────────────
@@ -99,7 +124,9 @@ export function requiresConfirmation(
99
124
  *
100
125
  * @since TP-041
101
126
  */
102
- export const ACTION_CLASSIFICATION_EXAMPLES: Readonly<Record<RecoveryActionClassification, readonly string[]>> = {
127
+ export const ACTION_CLASSIFICATION_EXAMPLES: Readonly<
128
+ Record<RecoveryActionClassification, readonly string[]>
129
+ > = {
103
130
  diagnostic: [
104
131
  "Reading batch-state.json, STATUS.md, events.jsonl, merge results",
105
132
  "Running git status, git log, git diff",
@@ -126,7 +153,6 @@ export const ACTION_CLASSIFICATION_EXAMPLES: Readonly<Record<RecoveryActionClass
126
153
  ],
127
154
  };
128
155
 
129
-
130
156
  // ── Audit Trail (TP-041 Step 4) ──────────────────────────────────────
131
157
 
132
158
  /**
@@ -295,7 +321,6 @@ export function readAuditTrail(
295
321
  }
296
322
  }
297
323
 
298
-
299
324
  // ── Branch Protection Detection (TP-043) ─────────────────────────────
300
325
 
301
326
  /**
@@ -325,30 +350,35 @@ export type BranchProtectionStatus = "protected" | "unprotected" | "unknown";
325
350
  *
326
351
  * @since TP-043
327
352
  */
328
- export function detectBranchProtection(
329
- branch: string,
330
- cwd: string,
331
- ): BranchProtectionStatus {
353
+ export function detectBranchProtection(branch: string, cwd: string): BranchProtectionStatus {
332
354
  try {
333
355
  // Get owner/repo from gh (handles SSH, HTTPS, and gh-specific remotes)
334
- const repoInfo = execFileSync("gh", ["repo", "view", "--json", "owner,name", "--jq", ".owner.login + \"/\" + .name"], {
335
- encoding: "utf-8",
336
- timeout: 15_000,
337
- cwd,
338
- stdio: ["pipe", "pipe", "pipe"],
339
- }).trim();
356
+ const repoInfo = execFileSync(
357
+ "gh",
358
+ ["repo", "view", "--json", "owner,name", "--jq", '.owner.login + "/" + .name'],
359
+ {
360
+ encoding: "utf-8",
361
+ timeout: 15_000,
362
+ cwd,
363
+ stdio: ["pipe", "pipe", "pipe"],
364
+ },
365
+ ).trim();
340
366
 
341
367
  if (!repoInfo || !repoInfo.includes("/")) {
342
368
  return "unknown";
343
369
  }
344
370
 
345
371
  // Check branch protection via GitHub API
346
- const result = execFileSync("gh", ["api", `repos/${repoInfo}/branches/${branch}/protection`, "--silent"], {
347
- encoding: "utf-8",
348
- timeout: 15_000,
349
- cwd,
350
- stdio: ["pipe", "pipe", "pipe"],
351
- });
372
+ const result = execFileSync(
373
+ "gh",
374
+ ["api", `repos/${repoInfo}/branches/${branch}/protection`, "--silent"],
375
+ {
376
+ encoding: "utf-8",
377
+ timeout: 15_000,
378
+ cwd,
379
+ stdio: ["pipe", "pipe", "pipe"],
380
+ },
381
+ );
352
382
 
353
383
  // If we get here (no error), the API returned 200 → branch is protected
354
384
  return "protected";
@@ -366,7 +396,6 @@ export function detectBranchProtection(
366
396
  }
367
397
  }
368
398
 
369
-
370
399
  // ── Supervisor-Managed Integration Flow (TP-043) ─────────────────────
371
400
 
372
401
  /**
@@ -468,8 +497,8 @@ export function buildIntegrationPlan(
468
497
  // - Override: use as-is (test injection path)
469
498
  // - Remotes exist: detect via gh API
470
499
  // - No remotes: treat as unprotected (can't create PRs anyway)
471
- const protection = protectionOverride
472
- ?? (remotes ? detectBranchProtection(baseBranch, cwd) : "unprotected");
500
+ const protection =
501
+ protectionOverride ?? (remotes ? detectBranchProtection(baseBranch, cwd) : "unprotected");
473
502
 
474
503
  // Step 3: Always try FF first, then merge, then PR (TP-149).
475
504
  // Protected branches may still allow FF/merge via API tokens.
@@ -544,7 +573,9 @@ export function formatIntegrationPlan(plan: IntegrationPlan): string {
544
573
  lines.push(``);
545
574
  lines.push(`- **Mode:** ${modeLabels[plan.mode] || plan.mode}`);
546
575
  lines.push(`- **From:** \`${plan.orchBranch}\` → \`${plan.baseBranch}\``);
547
- lines.push(`- **Tasks:** ${plan.succeededTasks} succeeded${plan.failedTasks > 0 ? `, ${plan.failedTasks} failed` : ""}`);
576
+ lines.push(
577
+ `- **Tasks:** ${plan.succeededTasks} succeeded${plan.failedTasks > 0 ? `, ${plan.failedTasks} failed` : ""}`,
578
+ );
548
579
  lines.push(`- **Rationale:** ${plan.rationale}`);
549
580
 
550
581
  if (plan.branchProtection === "protected") {
@@ -571,7 +602,8 @@ export function formatIntegrationOutcome(
571
602
  detail: string,
572
603
  ): string {
573
604
  if (success) {
574
- const modeLabel = plan.mode === "ff" ? "Fast-forwarded" : plan.mode === "merge" ? "Merged" : "Created PR for";
605
+ const modeLabel =
606
+ plan.mode === "ff" ? "Fast-forwarded" : plan.mode === "merge" ? "Merged" : "Created PR for";
575
607
  return `✅ **Integration complete!** ${modeLabel} \`${plan.orchBranch}\` → \`${plan.baseBranch}\`.\n${detail}`;
576
608
  }
577
609
  return `❌ **Integration failed** (\`${plan.orchBranch}\` → \`${plan.baseBranch}\`).\n${detail}`;
@@ -587,8 +619,20 @@ export function formatIntegrationOutcome(
587
619
  */
588
620
  export type IntegrationExecutor = (
589
621
  mode: "ff" | "merge" | "pr",
590
- context: { orchBranch: string; baseBranch: string; batchId: string; currentBranch: string; notices: string[] },
591
- ) => { success: boolean; integratedLocally: boolean; commitCount: string; message: string; error?: string };
622
+ context: {
623
+ orchBranch: string;
624
+ baseBranch: string;
625
+ batchId: string;
626
+ currentBranch: string;
627
+ notices: string[];
628
+ },
629
+ ) => {
630
+ success: boolean;
631
+ integratedLocally: boolean;
632
+ commitCount: string;
633
+ message: string;
634
+ error?: string;
635
+ };
592
636
 
593
637
  /**
594
638
  * Dependencies for programmatic CI polling and PR merge (R002-2).
@@ -631,11 +675,15 @@ export async function pollPrCiStatus(
631
675
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
632
676
  // Wait before polling (except first attempt — check immediately)
633
677
  if (attempt > 1) {
634
- await new Promise(resolve => setTimeout(resolve, delayMs));
678
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
635
679
  }
636
680
 
637
681
  const result = deps.runCommand("gh", [
638
- "pr", "checks", orchBranch, "--json", "name,state,conclusion",
682
+ "pr",
683
+ "checks",
684
+ orchBranch,
685
+ "--json",
686
+ "name,state,conclusion",
639
687
  ]);
640
688
 
641
689
  if (!result.ok) {
@@ -661,16 +709,18 @@ export async function pollPrCiStatus(
661
709
  }
662
710
 
663
711
  // Check if all checks are complete
664
- const allComplete = checks.every(c =>
665
- c.state === "COMPLETED" || c.state === "completed",
666
- );
712
+ const allComplete = checks.every((c) => c.state === "COMPLETED" || c.state === "completed");
667
713
  if (!allComplete) continue; // Some still pending — keep polling
668
714
 
669
715
  // All complete — check conclusions
670
- const allPassing = checks.every(c =>
671
- c.conclusion === "SUCCESS" || c.conclusion === "success" ||
672
- c.conclusion === "NEUTRAL" || c.conclusion === "neutral" ||
673
- c.conclusion === "SKIPPED" || c.conclusion === "skipped",
716
+ const allPassing = checks.every(
717
+ (c) =>
718
+ c.conclusion === "SUCCESS" ||
719
+ c.conclusion === "success" ||
720
+ c.conclusion === "NEUTRAL" ||
721
+ c.conclusion === "neutral" ||
722
+ c.conclusion === "SKIPPED" ||
723
+ c.conclusion === "skipped",
674
724
  );
675
725
 
676
726
  if (allPassing) {
@@ -678,16 +728,23 @@ export async function pollPrCiStatus(
678
728
  }
679
729
 
680
730
  // Some checks failed
681
- const failed = checks.filter(c =>
682
- c.conclusion !== "SUCCESS" && c.conclusion !== "success" &&
683
- c.conclusion !== "NEUTRAL" && c.conclusion !== "neutral" &&
684
- c.conclusion !== "SKIPPED" && c.conclusion !== "skipped",
731
+ const failed = checks.filter(
732
+ (c) =>
733
+ c.conclusion !== "SUCCESS" &&
734
+ c.conclusion !== "success" &&
735
+ c.conclusion !== "NEUTRAL" &&
736
+ c.conclusion !== "neutral" &&
737
+ c.conclusion !== "SKIPPED" &&
738
+ c.conclusion !== "skipped",
685
739
  );
686
- const failedNames = failed.map(c => `${c.name}: ${c.conclusion}`).join(", ");
740
+ const failedNames = failed.map((c) => `${c.name}: ${c.conclusion}`).join(", ");
687
741
  return { status: "fail", detail: `CI check(s) failed: ${failedNames}` };
688
742
  }
689
743
 
690
- return { status: "timeout", detail: `CI checks did not complete within ${maxAttempts} polling attempts.` };
744
+ return {
745
+ status: "timeout",
746
+ detail: `CI checks did not complete within ${maxAttempts} polling attempts.`,
747
+ };
691
748
  }
692
749
 
693
750
  /**
@@ -706,13 +763,14 @@ export async function pollPrCiStatus(
706
763
  *
707
764
  * @since TP-043
708
765
  */
709
- export function mergePr(
710
- orchBranch: string,
711
- deps: CiDeps,
712
- ): { success: boolean; detail: string } {
766
+ export function mergePr(orchBranch: string, deps: CiDeps): { success: boolean; detail: string } {
713
767
  // Try regular merge first (preserves per-commit history)
714
768
  const mergeResult = deps.runCommand("gh", [
715
- "pr", "merge", orchBranch, "--merge", "--delete-branch",
769
+ "pr",
770
+ "merge",
771
+ orchBranch,
772
+ "--merge",
773
+ "--delete-branch",
716
774
  ]);
717
775
  if (mergeResult.ok) {
718
776
  return { success: true, detail: "PR merged and remote branch deleted." };
@@ -720,7 +778,11 @@ export function mergePr(
720
778
 
721
779
  // Regular merge not allowed — try squash as fallback
722
780
  const squashResult = deps.runCommand("gh", [
723
- "pr", "merge", orchBranch, "--squash", "--delete-branch",
781
+ "pr",
782
+ "merge",
783
+ orchBranch,
784
+ "--squash",
785
+ "--delete-branch",
724
786
  ]);
725
787
  if (squashResult.ok) {
726
788
  return { success: true, detail: "PR merged (squash) and remote branch deleted." };
@@ -744,9 +806,17 @@ export interface SummaryDeps {
744
806
  /** Operator identifier for file naming */
745
807
  opId: string;
746
808
  /** Batch diagnostics (taskExits, batchCost) — null if unavailable */
747
- diagnostics: { taskExits: Record<string, { classification: string; cost: number; durationSec: number }>; batchCost: number } | null;
809
+ diagnostics: {
810
+ taskExits: Record<string, { classification: string; cost: number; durationSec: number }>;
811
+ batchCost: number;
812
+ } | null;
748
813
  /** Merge results for cost breakdown */
749
- mergeResults: Array<{ waveIndex: number; status: string; failedLane: number | null; failureReason: string | null }>;
814
+ mergeResults: Array<{
815
+ waveIndex: number;
816
+ status: string;
817
+ failedLane: number | null;
818
+ failureReason: string | null;
819
+ }>;
750
820
  }
751
821
 
752
822
  /**
@@ -787,12 +857,14 @@ async function handlePrLifecycle(
787
857
  pi.sendMessage(
788
858
  {
789
859
  customType: "supervisor-integration-result",
790
- content: [{
791
- type: "text",
792
- text:
793
- `✅ **Integration complete!** PR merged into \`${plan.baseBranch}\`.\n` +
794
- `${ciResult.detail}\n${mergeOutcome.detail}`,
795
- }],
860
+ content: [
861
+ {
862
+ type: "text",
863
+ text:
864
+ `✅ **Integration complete!** PR merged into \`${plan.baseBranch}\`.\n` +
865
+ `${ciResult.detail}\n${mergeOutcome.detail}`,
866
+ },
867
+ ],
796
868
  display: "Integration complete — PR merged",
797
869
  },
798
870
  { triggerTurn: false },
@@ -801,12 +873,14 @@ async function handlePrLifecycle(
801
873
  pi.sendMessage(
802
874
  {
803
875
  customType: "supervisor-integration-result",
804
- content: [{
805
- type: "text",
806
- text:
807
- `⚠️ **CI passed but merge failed.** ${mergeOutcome.detail}\n` +
808
- `The PR is still open — merge manually on GitHub.`,
809
- }],
876
+ content: [
877
+ {
878
+ type: "text",
879
+ text:
880
+ `⚠️ **CI passed but merge failed.** ${mergeOutcome.detail}\n` +
881
+ `The PR is still open — merge manually on GitHub.`,
882
+ },
883
+ ],
810
884
  display: "CI passed but PR merge failed",
811
885
  },
812
886
  { triggerTurn: false },
@@ -816,12 +890,14 @@ async function handlePrLifecycle(
816
890
  pi.sendMessage(
817
891
  {
818
892
  customType: "supervisor-integration-result",
819
- content: [{
820
- type: "text",
821
- text:
822
- `❌ **CI checks failed.** ${ciResult.detail}\n` +
823
- `The PR is still open. Fix the issues and merge manually, or close and retry.`,
824
- }],
893
+ content: [
894
+ {
895
+ type: "text",
896
+ text:
897
+ `❌ **CI checks failed.** ${ciResult.detail}\n` +
898
+ `The PR is still open. Fix the issues and merge manually, or close and retry.`,
899
+ },
900
+ ],
825
901
  display: "CI checks failed — manual intervention needed",
826
902
  },
827
903
  { triggerTurn: false },
@@ -831,12 +907,14 @@ async function handlePrLifecycle(
831
907
  pi.sendMessage(
832
908
  {
833
909
  customType: "supervisor-integration-result",
834
- content: [{
835
- type: "text",
836
- text:
837
- `⏰ **CI check timeout.** ${ciResult.detail}\n` +
838
- `The PR is still open. Check CI status manually and merge when ready.`,
839
- }],
910
+ content: [
911
+ {
912
+ type: "text",
913
+ text:
914
+ `⏰ **CI check timeout.** ${ciResult.detail}\n` +
915
+ `The PR is still open. Check CI status manually and merge when ready.`,
916
+ },
917
+ ],
840
918
  display: "CI check timeout — check manually",
841
919
  },
842
920
  { triggerTurn: false },
@@ -845,7 +923,14 @@ async function handlePrLifecycle(
845
923
 
846
924
  // TP-043: Generate batch summary before deactivation
847
925
  if (batchState && summaryDeps && state.stateRoot) {
848
- presentBatchSummary(pi, batchState, state.stateRoot, summaryDeps.opId, summaryDeps.diagnostics, summaryDeps.mergeResults);
926
+ presentBatchSummary(
927
+ pi,
928
+ batchState,
929
+ state.stateRoot,
930
+ summaryDeps.opId,
931
+ summaryDeps.diagnostics,
932
+ summaryDeps.mergeResults,
933
+ );
849
934
  }
850
935
 
851
936
  // Always deactivate after PR lifecycle completes (R002 issue #3)
@@ -897,7 +982,14 @@ export function triggerSupervisorIntegration(
897
982
  // TP-043: Helper to generate summary before deactivation
898
983
  const summarizeAndDeactivate = () => {
899
984
  if (summaryDeps && state.stateRoot) {
900
- presentBatchSummary(pi, batchState, state.stateRoot, summaryDeps.opId, summaryDeps.diagnostics, summaryDeps.mergeResults);
985
+ presentBatchSummary(
986
+ pi,
987
+ batchState,
988
+ state.stateRoot,
989
+ summaryDeps.opId,
990
+ summaryDeps.diagnostics,
991
+ summaryDeps.mergeResults,
992
+ );
901
993
  }
902
994
  deactivateSupervisor(pi, state);
903
995
  };
@@ -910,10 +1002,12 @@ export function triggerSupervisorIntegration(
910
1002
  pi.sendMessage(
911
1003
  {
912
1004
  customType: "supervisor-integration",
913
- content: [{
914
- type: "text",
915
- text: `📋 **Batch complete.** No integration needed (no orch branch or no succeeded tasks). Supervisor deactivating.`,
916
- }],
1005
+ content: [
1006
+ {
1007
+ type: "text",
1008
+ text: `📋 **Batch complete.** No integration needed (no orch branch or no succeeded tasks). Supervisor deactivating.`,
1009
+ },
1010
+ ],
917
1011
  display: "No integration needed — supervisor deactivating",
918
1012
  },
919
1013
  { triggerTurn: false },
@@ -932,23 +1026,26 @@ export function triggerSupervisorIntegration(
932
1026
  pi.sendMessage(
933
1027
  {
934
1028
  customType: "supervisor-integration",
935
- content: [{
936
- type: "text",
937
- text:
938
- `🏁 **Batch complete!** Ready to integrate.\n\n` +
939
- planText + `\n\n` +
940
- `**Action required:** Ask the operator for confirmation.\n\n` +
941
- `Say something like: "The batch completed successfully. I'd like to integrate ` +
942
- `the changes from \`${plan.orchBranch}\` into \`${plan.baseBranch}\` using ` +
943
- `${plan.mode === "ff" ? "fast-forward" : plan.mode === "merge" ? "a merge commit" : "a pull request"}. ` +
944
- `${plan.rationale} Shall I proceed?"\n\n` +
945
- `If the operator confirms, run: \`/orch-integrate${modeFlag}\`\n` +
946
- `If the operator declines, acknowledge and deactivate.\n` +
947
- `If the operator wants a different mode, adjust the flag:\n` +
948
- ` - Fast-forward: \`/orch-integrate\`\n` +
949
- ` - Merge commit: \`/orch-integrate --merge\`\n` +
950
- ` - Pull request: \`/orch-integrate --pr\``,
951
- }],
1029
+ content: [
1030
+ {
1031
+ type: "text",
1032
+ text:
1033
+ `🏁 **Batch complete!** Ready to integrate.\n\n` +
1034
+ planText +
1035
+ `\n\n` +
1036
+ `**Action required:** Ask the operator for confirmation.\n\n` +
1037
+ `Say something like: "The batch completed successfully. I'd like to integrate ` +
1038
+ `the changes from \`${plan.orchBranch}\` into \`${plan.baseBranch}\` using ` +
1039
+ `${plan.mode === "ff" ? "fast-forward" : plan.mode === "merge" ? "a merge commit" : "a pull request"}. ` +
1040
+ `${plan.rationale} Shall I proceed?"\n\n` +
1041
+ `If the operator confirms, run: \`/orch-integrate${modeFlag}\`\n` +
1042
+ `If the operator declines, acknowledge and deactivate.\n` +
1043
+ `If the operator wants a different mode, adjust the flag:\n` +
1044
+ ` - Fast-forward: \`/orch-integrate\`\n` +
1045
+ ` - Merge commit: \`/orch-integrate --merge\`\n` +
1046
+ ` - Pull request: \`/orch-integrate --pr\``,
1047
+ },
1048
+ ],
952
1049
  display: "Integration plan ready — awaiting operator confirmation",
953
1050
  },
954
1051
  { triggerTurn: true },
@@ -972,13 +1069,16 @@ export function triggerSupervisorIntegration(
972
1069
  pi.sendMessage(
973
1070
  {
974
1071
  customType: "supervisor-integration",
975
- content: [{
976
- type: "text",
977
- text:
978
- `🏁 **Batch complete!** Integration executor unavailable.\n\n` +
979
- planText + `\n\n` +
980
- `Run \`/orch-integrate${modeFlag}\` to integrate manually.`,
981
- }],
1072
+ content: [
1073
+ {
1074
+ type: "text",
1075
+ text:
1076
+ `🏁 **Batch complete!** Integration executor unavailable.\n\n` +
1077
+ planText +
1078
+ `\n\n` +
1079
+ `Run \`/orch-integrate${modeFlag}\` to integrate manually.`,
1080
+ },
1081
+ ],
982
1082
  display: "Auto-integration fallback — run /orch-integrate",
983
1083
  },
984
1084
  { triggerTurn: false },
@@ -1017,10 +1117,12 @@ export function triggerSupervisorIntegration(
1017
1117
  pi.sendMessage(
1018
1118
  {
1019
1119
  customType: "supervisor-integration-progress",
1020
- content: [{
1021
- type: "text",
1022
- text: `${outcomeText}\n\n⏳ Waiting for CI checks to complete...`,
1023
- }],
1120
+ content: [
1121
+ {
1122
+ type: "text",
1123
+ text: `${outcomeText}\n\n⏳ Waiting for CI checks to complete...`,
1124
+ },
1125
+ ],
1024
1126
  display: "PR created — polling CI status",
1025
1127
  },
1026
1128
  { triggerTurn: false },
@@ -1034,10 +1136,12 @@ export function triggerSupervisorIntegration(
1034
1136
  pi.sendMessage(
1035
1137
  {
1036
1138
  customType: "supervisor-integration-result",
1037
- content: [{
1038
- type: "text",
1039
- text: `❌ **CI monitoring crashed:** ${msg}\nThe PR is still open — check status and merge manually.`,
1040
- }],
1139
+ content: [
1140
+ {
1141
+ type: "text",
1142
+ text: `❌ **CI monitoring crashed:** ${msg}\nThe PR is still open — check status and merge manually.`,
1143
+ },
1144
+ ],
1041
1145
  display: "CI monitoring crashed",
1042
1146
  },
1043
1147
  { triggerTurn: false },
@@ -1049,10 +1153,12 @@ export function triggerSupervisorIntegration(
1049
1153
  pi.sendMessage(
1050
1154
  {
1051
1155
  customType: "supervisor-integration-result",
1052
- content: [{
1053
- type: "text",
1054
- text: `PR created. CI polling unavailable — check status and merge manually on GitHub.`,
1055
- }],
1156
+ content: [
1157
+ {
1158
+ type: "text",
1159
+ text: `PR created. CI polling unavailable — check status and merge manually on GitHub.`,
1160
+ },
1161
+ ],
1056
1162
  display: "PR created — merge manually",
1057
1163
  },
1058
1164
  { triggerTurn: false },
@@ -1066,10 +1172,12 @@ export function triggerSupervisorIntegration(
1066
1172
  pi.sendMessage(
1067
1173
  {
1068
1174
  customType: "supervisor-integration-result",
1069
- content: [{
1070
- type: "text",
1071
- text: outcomeText,
1072
- }],
1175
+ content: [
1176
+ {
1177
+ type: "text",
1178
+ text: outcomeText,
1179
+ },
1180
+ ],
1073
1181
  display: `Integration complete (${plan.mode})`,
1074
1182
  },
1075
1183
  { triggerTurn: false },
@@ -1083,12 +1191,13 @@ export function triggerSupervisorIntegration(
1083
1191
  pi.sendMessage(
1084
1192
  {
1085
1193
  customType: "supervisor-integration-result",
1086
- content: [{
1087
- type: "text",
1088
- text:
1089
- outcomeText + `\n\n` +
1090
- `Run \`/orch-integrate\` manually to retry with a different mode.`,
1091
- }],
1194
+ content: [
1195
+ {
1196
+ type: "text",
1197
+ text:
1198
+ outcomeText + `\n\n` + `Run \`/orch-integrate\` manually to retry with a different mode.`,
1199
+ },
1200
+ ],
1092
1201
  display: "Integration failed — run /orch-integrate manually",
1093
1202
  },
1094
1203
  { triggerTurn: false },
@@ -1097,7 +1206,6 @@ export function triggerSupervisorIntegration(
1097
1206
  }
1098
1207
  }
1099
1208
 
1100
-
1101
1209
  // ── Batch Summary Generation (TP-043 Step 2) ────────────────────────
1102
1210
 
1103
1211
  /**
@@ -1236,10 +1344,7 @@ const TIER0_SUMMARY_TYPES = new Set([
1236
1344
  *
1237
1345
  * @since TP-043
1238
1346
  */
1239
- export function readTier0EventsForBatch(
1240
- stateRoot: string,
1241
- batchId: string,
1242
- ): Tier0EventSummary[] {
1347
+ export function readTier0EventsForBatch(stateRoot: string, batchId: string): Tier0EventSummary[] {
1243
1348
  const eventsPath = join(stateRoot, ".pi", "supervisor", "events.jsonl");
1244
1349
  if (!existsSync(eventsPath)) return [];
1245
1350
 
@@ -1324,24 +1429,36 @@ function computeV2BatchCost(stateRoot: string, batchId: string): number {
1324
1429
  try {
1325
1430
  const lanesDir = join(stateRoot, ".pi", "runtime", batchId, "lanes");
1326
1431
  if (!existsSync(lanesDir)) return 0;
1327
- const files = readdirSync(lanesDir).filter(f => f.startsWith("lane-") && f.endsWith(".json"));
1432
+ const files = readdirSync(lanesDir).filter((f) => f.startsWith("lane-") && f.endsWith(".json"));
1328
1433
  let total = 0;
1329
1434
  for (const f of files) {
1330
1435
  try {
1331
1436
  const snap = JSON.parse(readFileSync(join(lanesDir, f), "utf-8"));
1332
1437
  total += snap.worker?.costUsd || 0;
1333
1438
  total += snap.reviewer?.costUsd || 0;
1334
- } catch { /* skip */ }
1439
+ } catch {
1440
+ /* skip */
1441
+ }
1335
1442
  }
1336
1443
  return total;
1337
- } catch { return 0; }
1444
+ } catch {
1445
+ return 0;
1446
+ }
1338
1447
  }
1339
1448
 
1340
1449
  export function collectBatchSummaryData(
1341
1450
  batchState: OrchBatchRuntimeState,
1342
1451
  stateRoot: string,
1343
- diagnostics?: { taskExits: Record<string, { classification: string; cost: number; durationSec: number }>; batchCost: number } | null,
1344
- mergeResults?: Array<{ waveIndex: number; status: string; failedLane: number | null; failureReason: string | null }>,
1452
+ diagnostics?: {
1453
+ taskExits: Record<string, { classification: string; cost: number; durationSec: number }>;
1454
+ batchCost: number;
1455
+ } | null,
1456
+ mergeResults?: Array<{
1457
+ waveIndex: number;
1458
+ status: string;
1459
+ failedLane: number | null;
1460
+ failureReason: string | null;
1461
+ }>,
1345
1462
  ): BatchSummaryData {
1346
1463
  // Read audit trail for incidents
1347
1464
  const auditEntries = readAuditTrail(stateRoot, { batchId: batchState.batchId });
@@ -1350,7 +1467,7 @@ export function collectBatchSummaryData(
1350
1467
  const tier0Events = readTier0EventsForBatch(stateRoot, batchState.batchId);
1351
1468
 
1352
1469
  // Extract wave results (may not exist if batch failed during planning)
1353
- const waveResults = (batchState.waveResults || []).map(wr => ({
1470
+ const waveResults = (batchState.waveResults || []).map((wr) => ({
1354
1471
  waveIndex: wr.waveIndex,
1355
1472
  startedAt: wr.startedAt,
1356
1473
  endedAt: wr.endedAt,
@@ -1370,8 +1487,11 @@ export function collectBatchSummaryData(
1370
1487
  byTaskId.set(segment.taskId, existing);
1371
1488
  }
1372
1489
 
1373
- const multiSegmentTasks: NonNullable<BatchSummaryData["segmentOutcomes"]>["multiSegmentTasks"] = [];
1374
- for (const [taskId, taskSegments] of [...byTaskId.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
1490
+ const multiSegmentTasks: NonNullable<BatchSummaryData["segmentOutcomes"]>["multiSegmentTasks"] =
1491
+ [];
1492
+ for (const [taskId, taskSegments] of [...byTaskId.entries()].sort((a, b) =>
1493
+ a[0].localeCompare(b[0]),
1494
+ )) {
1375
1495
  if (taskSegments.length <= 1) continue;
1376
1496
  const succeeded = taskSegments.filter((segment) => segment.status === "succeeded").length;
1377
1497
  const failed = taskSegments.filter((segment) => segment.status === "failed").length;
@@ -1415,9 +1535,10 @@ export function collectBatchSummaryData(
1415
1535
  failedTasks: batchState.failedTasks,
1416
1536
  skippedTasks: batchState.skippedTasks,
1417
1537
  blockedTasks: batchState.blockedTasks,
1418
- batchCost: (diagnostics?.batchCost ?? 0) > 0
1419
- ? diagnostics!.batchCost
1420
- : computeV2BatchCost(stateRoot, batchState.batchId),
1538
+ batchCost:
1539
+ (diagnostics?.batchCost ?? 0) > 0
1540
+ ? diagnostics!.batchCost
1541
+ : computeV2BatchCost(stateRoot, batchState.batchId),
1421
1542
  wavePlan: [], // Not directly available on runtime state — use waveResults
1422
1543
  waveResults,
1423
1544
  taskExits: diagnostics?.taskExits ?? {},
@@ -1453,9 +1574,8 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1453
1574
  lines.push("");
1454
1575
 
1455
1576
  // Duration
1456
- const duration = data.endedAt && data.startedAt
1457
- ? formatDurationMs(data.endedAt - data.startedAt)
1458
- : "In progress";
1577
+ const duration =
1578
+ data.endedAt && data.startedAt ? formatDurationMs(data.endedAt - data.startedAt) : "In progress";
1459
1579
  lines.push(`**Duration:** ${duration}`);
1460
1580
 
1461
1581
  // Cost
@@ -1484,11 +1604,12 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1484
1604
  } else {
1485
1605
  for (const wave of data.waveResults) {
1486
1606
  const waveNum = wave.waveIndex + 1;
1487
- const taskCount = wave.succeededTaskIds.length + wave.failedTaskIds.length + wave.skippedTaskIds.length;
1607
+ const taskCount =
1608
+ wave.succeededTaskIds.length + wave.failedTaskIds.length + wave.skippedTaskIds.length;
1488
1609
  const waveDuration = formatDurationMs(wave.endedAt - wave.startedAt);
1489
1610
 
1490
1611
  // Check for merge result for this wave
1491
- const mergeResult = data.mergeResults.find(mr => mr.waveIndex === wave.waveIndex);
1612
+ const mergeResult = data.mergeResults.find((mr) => mr.waveIndex === wave.waveIndex);
1492
1613
  let mergeInfo = "";
1493
1614
  if (mergeResult) {
1494
1615
  if (mergeResult.status === "succeeded") {
@@ -1500,11 +1621,16 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1500
1621
  }
1501
1622
  }
1502
1623
 
1503
- const statusIcon = wave.overallStatus === "succeeded" ? "✅"
1504
- : wave.overallStatus === "failed" ? "❌"
1505
- : wave.overallStatus === "partial" ? "⚠️"
1506
- : wave.overallStatus === "aborted" ? "🛑"
1507
- : "";
1624
+ const statusIcon =
1625
+ wave.overallStatus === "succeeded"
1626
+ ? ""
1627
+ : wave.overallStatus === "failed"
1628
+ ? ""
1629
+ : wave.overallStatus === "partial"
1630
+ ? "⚠️"
1631
+ : wave.overallStatus === "aborted"
1632
+ ? "🛑"
1633
+ : "❓";
1508
1634
 
1509
1635
  lines.push(`- Wave ${waveNum} (${taskCount} tasks): ${waveDuration} ${statusIcon}${mergeInfo}`);
1510
1636
 
@@ -1522,7 +1648,9 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1522
1648
  if (!data.segmentOutcomes) {
1523
1649
  lines.push("Segment data not available.");
1524
1650
  } else if (data.segmentOutcomes.multiSegmentTasks.length === 0) {
1525
- lines.push(`No multi-segment task outcomes recorded (${data.segmentOutcomes.totalSegments} segment record(s) total).`);
1651
+ lines.push(
1652
+ `No multi-segment task outcomes recorded (${data.segmentOutcomes.totalSegments} segment record(s) total).`,
1653
+ );
1526
1654
  } else {
1527
1655
  const statusParts = [
1528
1656
  `${data.segmentOutcomes.succeeded} succeeded`,
@@ -1541,7 +1669,9 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1541
1669
  if (task.pending > 0) taskParts.push(`${task.pending} pending`);
1542
1670
  if (task.skipped > 0) taskParts.push(`${task.skipped} skipped`);
1543
1671
  if (task.stalled > 0) taskParts.push(`${task.stalled} stalled`);
1544
- lines.push(` - ${task.taskId}: ${task.terminalSegments}/${task.totalSegments} terminal (${taskParts.join(", ")})`);
1672
+ lines.push(
1673
+ ` - ${task.taskId}: ${task.terminalSegments}/${task.totalSegments} terminal (${taskParts.join(", ")})`,
1674
+ );
1545
1675
  }
1546
1676
  }
1547
1677
  lines.push("");
@@ -1552,7 +1682,7 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1552
1682
 
1553
1683
  // Extract incidents from audit trail: non-diagnostic actions
1554
1684
  const incidents = data.auditEntries.filter(
1555
- e => e.classification !== "diagnostic" && e.result !== "pending",
1685
+ (e) => e.classification !== "diagnostic" && e.result !== "pending",
1556
1686
  );
1557
1687
 
1558
1688
  const hasTier0Events = data.tier0Events.length > 0;
@@ -1576,16 +1706,16 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1576
1706
  }
1577
1707
 
1578
1708
  for (const [pattern, events] of byPattern) {
1579
- const attempts = events.filter(e => e.type === "tier0_recovery_attempt").length;
1580
- const successes = events.filter(e => e.type === "tier0_recovery_success").length;
1581
- const exhausted = events.filter(e => e.type === "tier0_recovery_exhausted").length;
1582
- const escalations = events.filter(e => e.type === "tier0_escalation").length;
1709
+ const attempts = events.filter((e) => e.type === "tier0_recovery_attempt").length;
1710
+ const successes = events.filter((e) => e.type === "tier0_recovery_success").length;
1711
+ const exhausted = events.filter((e) => e.type === "tier0_recovery_exhausted").length;
1712
+ const escalations = events.filter((e) => e.type === "tier0_escalation").length;
1583
1713
 
1584
- const statusIcon = exhausted > 0 || escalations > 0 ? "❌"
1585
- : successes > 0 ? "✅"
1586
- : "⏳";
1714
+ const statusIcon = exhausted > 0 || escalations > 0 ? "❌" : successes > 0 ? "✅" : "⏳";
1587
1715
 
1588
- lines.push(`- **${pattern}** ${statusIcon} — ${attempts} attempt(s), ${successes} success(es), ${exhausted} exhausted`);
1716
+ lines.push(
1717
+ `- **${pattern}** ${statusIcon} — ${attempts} attempt(s), ${successes} success(es), ${exhausted} exhausted`,
1718
+ );
1589
1719
 
1590
1720
  // Show affected tasks
1591
1721
  const taskIds = new Set<string>();
@@ -1600,21 +1730,21 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1600
1730
  }
1601
1731
 
1602
1732
  // Show escalation details
1603
- for (const evt of events.filter(e => e.type === "tier0_escalation")) {
1733
+ for (const evt of events.filter((e) => e.type === "tier0_escalation")) {
1604
1734
  if (evt.suggestion) {
1605
1735
  lines.push(` - Escalation: ${evt.suggestion}`);
1606
1736
  }
1607
1737
  }
1608
1738
 
1609
1739
  // Show resolution details
1610
- for (const evt of events.filter(e => e.type === "tier0_recovery_success")) {
1740
+ for (const evt of events.filter((e) => e.type === "tier0_recovery_success")) {
1611
1741
  if (evt.resolution) {
1612
1742
  lines.push(` - Resolution: ${evt.resolution}`);
1613
1743
  }
1614
1744
  }
1615
1745
 
1616
1746
  // Show error details for exhausted
1617
- for (const evt of events.filter(e => e.type === "tier0_recovery_exhausted")) {
1747
+ for (const evt of events.filter((e) => e.type === "tier0_recovery_exhausted")) {
1618
1748
  if (evt.error) {
1619
1749
  lines.push(` - Error: ${evt.error}`);
1620
1750
  }
@@ -1633,10 +1763,14 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1633
1763
  let incidentNum = 0;
1634
1764
  for (const entry of incidents) {
1635
1765
  incidentNum++;
1636
- const resultIcon = entry.result === "success" ? "✅"
1637
- : entry.result === "failure" ? "❌"
1638
- : entry.result === "skipped" ? "⏭️"
1639
- : "";
1766
+ const resultIcon =
1767
+ entry.result === "success"
1768
+ ? ""
1769
+ : entry.result === "failure"
1770
+ ? "❌"
1771
+ : entry.result === "skipped"
1772
+ ? "⏭️"
1773
+ : "❓";
1640
1774
  lines.push(`${incidentNum}. **${entry.action}** (${entry.classification}) ${resultIcon}`);
1641
1775
  lines.push(` ${entry.context}`);
1642
1776
  if (entry.detail && entry.detail !== entry.context) {
@@ -1666,16 +1800,22 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1666
1800
  const recommendations: string[] = [];
1667
1801
 
1668
1802
  // Timeout recommendations: look for merge failures in audit trail
1669
- const mergeFailures = data.mergeResults.filter(mr => mr.status === "failed");
1803
+ const mergeFailures = data.mergeResults.filter((mr) => mr.status === "failed");
1670
1804
  if (mergeFailures.length > 0) {
1671
- recommendations.push("- Consider increasing `merge.timeoutMinutes` — merge failures were detected during this batch.");
1805
+ recommendations.push(
1806
+ "- Consider increasing `merge.timeoutMinutes` — merge failures were detected during this batch.",
1807
+ );
1672
1808
  }
1673
1809
 
1674
1810
  // Failure rate recommendations
1675
1811
  if (data.totalTasks > 0 && data.failedTasks > 0) {
1676
1812
  const failureRate = data.failedTasks / data.totalTasks;
1677
1813
  if (failureRate > 0.3) {
1678
- recommendations.push("- High failure rate (" + Math.round(failureRate * 100) + "%) — consider reducing task scope or adding more context to PROMPT.md files.");
1814
+ recommendations.push(
1815
+ "- High failure rate (" +
1816
+ Math.round(failureRate * 100) +
1817
+ "%) — consider reducing task scope or adding more context to PROMPT.md files.",
1818
+ );
1679
1819
  }
1680
1820
  }
1681
1821
 
@@ -1683,18 +1823,28 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1683
1823
  const longTasks = Object.entries(data.taskExits).filter(([, exit]) => exit.durationSec > 3600);
1684
1824
  if (longTasks.length > 0) {
1685
1825
  const names = longTasks.map(([id]) => id).join(", ");
1686
- recommendations.push(`- Long-running tasks detected (${names}): ${longTasks.length} task(s) exceeded 1 hour — consider splitting into smaller tasks.`);
1826
+ recommendations.push(
1827
+ `- Long-running tasks detected (${names}): ${longTasks.length} task(s) exceeded 1 hour — consider splitting into smaller tasks.`,
1828
+ );
1687
1829
  }
1688
1830
 
1689
1831
  // Recovery recommendations — check both audit trail and Tier 0 events
1690
- const recoveryExhaustedAudit = data.auditEntries.filter(e => e.action === "tier0_recovery_exhausted" || (e.classification === "tier0_known" && e.result === "failure"));
1691
- const recoveryExhaustedTier0 = data.tier0Events.filter(e => e.type === "tier0_recovery_exhausted");
1692
- const escalationsTier0 = data.tier0Events.filter(e => e.type === "tier0_escalation");
1832
+ const recoveryExhaustedAudit = data.auditEntries.filter(
1833
+ (e) =>
1834
+ e.action === "tier0_recovery_exhausted" ||
1835
+ (e.classification === "tier0_known" && e.result === "failure"),
1836
+ );
1837
+ const recoveryExhaustedTier0 = data.tier0Events.filter(
1838
+ (e) => e.type === "tier0_recovery_exhausted",
1839
+ );
1840
+ const escalationsTier0 = data.tier0Events.filter((e) => e.type === "tier0_escalation");
1693
1841
  if (recoveryExhaustedAudit.length > 0 || recoveryExhaustedTier0.length > 0) {
1694
- recommendations.push("- Recovery budget was exhausted for some issues — review recurring failures and consider addressing root causes.");
1842
+ recommendations.push(
1843
+ "- Recovery budget was exhausted for some issues — review recurring failures and consider addressing root causes.",
1844
+ );
1695
1845
  }
1696
1846
  if (escalationsTier0.length > 0) {
1697
- const uniqueSuggestions = [...new Set(escalationsTier0.map(e => e.suggestion).filter(Boolean))];
1847
+ const uniqueSuggestions = [...new Set(escalationsTier0.map((e) => e.suggestion).filter(Boolean))];
1698
1848
  if (uniqueSuggestions.length > 0) {
1699
1849
  for (const suggestion of uniqueSuggestions) {
1700
1850
  recommendations.push(`- Tier 0 escalation: ${suggestion}`);
@@ -1704,7 +1854,9 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1704
1854
 
1705
1855
  // Blocked tasks recommendations
1706
1856
  if (data.blockedTasks > 0) {
1707
- recommendations.push(`- ${data.blockedTasks} task(s) were blocked due to upstream failures — fix failed tasks and re-run with \`/orch-resume\`.`);
1857
+ recommendations.push(
1858
+ `- ${data.blockedTasks} task(s) were blocked due to upstream failures — fix failed tasks and re-run with \`/orch-resume\`.`,
1859
+ );
1708
1860
  }
1709
1861
 
1710
1862
  if (recommendations.length === 0) {
@@ -1744,10 +1896,14 @@ export function formatBatchSummary(data: BatchSummaryData): string {
1744
1896
 
1745
1897
  totalCost += waveCost;
1746
1898
  const waveDurationStr = formatDurationMs(waveDurationSec * 1000);
1747
- lines.push(`| ${waveNum} | ${allTaskIds.length} | $${waveCost.toFixed(2)} | ${waveDurationStr} |`);
1899
+ lines.push(
1900
+ `| ${waveNum} | ${allTaskIds.length} | $${waveCost.toFixed(2)} | ${waveDurationStr} |`,
1901
+ );
1748
1902
  }
1749
1903
 
1750
- lines.push(`| **Total** | **${data.totalTasks}** | **$${totalCost.toFixed(2)}** | **${duration}** |`);
1904
+ lines.push(
1905
+ `| **Total** | **${data.totalTasks}** | **$${totalCost.toFixed(2)}** | **${duration}** |`,
1906
+ );
1751
1907
  }
1752
1908
  lines.push("");
1753
1909
 
@@ -1780,8 +1936,16 @@ export function generateBatchSummary(
1780
1936
  batchState: OrchBatchRuntimeState,
1781
1937
  stateRoot: string,
1782
1938
  opId: string,
1783
- diagnostics?: { taskExits: Record<string, { classification: string; cost: number; durationSec: number }>; batchCost: number } | null,
1784
- mergeResults?: Array<{ waveIndex: number; status: string; failedLane: number | null; failureReason: string | null }>,
1939
+ diagnostics?: {
1940
+ taskExits: Record<string, { classification: string; cost: number; durationSec: number }>;
1941
+ batchCost: number;
1942
+ } | null,
1943
+ mergeResults?: Array<{
1944
+ waveIndex: number;
1945
+ status: string;
1946
+ failedLane: number | null;
1947
+ failureReason: string | null;
1948
+ }>,
1785
1949
  ): string {
1786
1950
  const data = collectBatchSummaryData(batchState, stateRoot, diagnostics, mergeResults);
1787
1951
  const markdown = formatBatchSummary(data);
@@ -1822,19 +1986,29 @@ export function presentBatchSummary(
1822
1986
  batchState: OrchBatchRuntimeState,
1823
1987
  stateRoot: string,
1824
1988
  opId: string,
1825
- diagnostics?: { taskExits: Record<string, { classification: string; cost: number; durationSec: number }>; batchCost: number } | null,
1826
- mergeResults?: Array<{ waveIndex: number; status: string; failedLane: number | null; failureReason: string | null }>,
1989
+ diagnostics?: {
1990
+ taskExits: Record<string, { classification: string; cost: number; durationSec: number }>;
1991
+ batchCost: number;
1992
+ } | null,
1993
+ mergeResults?: Array<{
1994
+ waveIndex: number;
1995
+ status: string;
1996
+ failedLane: number | null;
1997
+ failureReason: string | null;
1998
+ }>,
1827
1999
  ): void {
1828
2000
  const summary = generateBatchSummary(batchState, stateRoot, opId, diagnostics, mergeResults);
1829
2001
 
1830
2002
  // Build a concise conversation message (full details in the file)
1831
- const duration = batchState.endedAt && batchState.startedAt
1832
- ? formatDurationMs(batchState.endedAt - batchState.startedAt)
1833
- : "in progress";
2003
+ const duration =
2004
+ batchState.endedAt && batchState.startedAt
2005
+ ? formatDurationMs(batchState.endedAt - batchState.startedAt)
2006
+ : "in progress";
1834
2007
  // TP-115: Use V2 lane snapshot cost when diagnostics.batchCost is zero
1835
- const rawCost = (diagnostics?.batchCost ?? 0) > 0
1836
- ? diagnostics!.batchCost
1837
- : computeV2BatchCost(stateRoot, batchState.batchId);
2008
+ const rawCost =
2009
+ (diagnostics?.batchCost ?? 0) > 0
2010
+ ? diagnostics!.batchCost
2011
+ : computeV2BatchCost(stateRoot, batchState.batchId);
1838
2012
  const cost = rawCost > 0 ? `$${rawCost.toFixed(2)}` : "not tracked";
1839
2013
  const filename = `${opId}-${batchState.batchId}-summary.md`;
1840
2014
 
@@ -1856,7 +2030,6 @@ export function presentBatchSummary(
1856
2030
  );
1857
2031
  }
1858
2032
 
1859
-
1860
2033
  // ── Supervisor Config Types ──────────────────────────────────────────
1861
2034
 
1862
2035
  /**
@@ -1906,7 +2079,6 @@ function resolvePrimerPath(): string {
1906
2079
  }
1907
2080
  }
1908
2081
 
1909
-
1910
2082
  // ── Template Loading (TP-058) ────────────────────────────────────────
1911
2083
 
1912
2084
  /**
@@ -1937,7 +2109,9 @@ function resolveBaseTemplatePath(name: string): string {
1937
2109
  *
1938
2110
  * @since TP-058
1939
2111
  */
1940
- function parseSupervisorTemplate(filePath: string): { fm: Record<string, string>; body: string } | null {
2112
+ function parseSupervisorTemplate(
2113
+ filePath: string,
2114
+ ): { fm: Record<string, string>; body: string } | null {
1941
2115
  if (!existsSync(filePath)) return null;
1942
2116
  const raw = readFileSync(filePath, "utf-8").replace(/\r\n/g, "\n");
1943
2117
  const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
@@ -1947,7 +2121,8 @@ function parseSupervisorTemplate(filePath: string): { fm: Record<string, string>
1947
2121
  const idx = line.indexOf(":");
1948
2122
  if (idx > 0) {
1949
2123
  const key = line.slice(0, idx).trim();
1950
- if (!key.startsWith("#")) { // Skip commented-out frontmatter
2124
+ if (!key.startsWith("#")) {
2125
+ // Skip commented-out frontmatter
1951
2126
  fm[key] = line.slice(idx + 1).trim();
1952
2127
  }
1953
2128
  }
@@ -1970,7 +2145,11 @@ function parseSupervisorTemplate(filePath: string): { fm: Record<string, string>
1970
2145
  *
1971
2146
  * @since TP-058
1972
2147
  */
1973
- export function loadSupervisorTemplate(name: string, stateRoot: string, localName?: string): string | null {
2148
+ export function loadSupervisorTemplate(
2149
+ name: string,
2150
+ stateRoot: string,
2151
+ localName?: string,
2152
+ ): string | null {
1974
2153
  const basePath = resolveBaseTemplatePath(name);
1975
2154
  const baseDef = parseSupervisorTemplate(basePath);
1976
2155
 
@@ -2013,7 +2192,6 @@ function replaceTemplateVars(template: string, vars: Record<string, string>): st
2013
2192
  });
2014
2193
  }
2015
2194
 
2016
-
2017
2195
  /**
2018
2196
  * Build the guardrails section dynamically based on integration mode (TP-043).
2019
2197
  * Extracted as a helper so both the template path and inline fallback can reuse it.
@@ -2021,9 +2199,10 @@ function replaceTemplateVars(template: string, vars: Record<string, string>): st
2021
2199
  */
2022
2200
  function buildGuardrailsSection(integrationMode: string): string {
2023
2201
  if (integrationMode === "supervised" || integrationMode === "auto") {
2024
- const modeNote = integrationMode === "supervised"
2025
- ? `**Supervised mode:** Before executing integration, describe your plan and ask the operator for confirmation.`
2026
- : `**Auto mode:** Execute integration directly. Report the outcome to the operator. Pause only on errors or conflicts.`;
2202
+ const modeNote =
2203
+ integrationMode === "supervised"
2204
+ ? `**Supervised mode:** Before executing integration, describe your plan and ask the operator for confirmation.`
2205
+ : `**Auto mode:** Execute integration directly. Report the outcome to the operator. Pause only on errors or conflicts.`;
2027
2206
  return `## What You Must NEVER Do
2028
2207
 
2029
2208
  1. Never delete \`.pi/batch-state.json\` without operator approval
@@ -2105,9 +2284,10 @@ export function buildSupervisorSystemPrompt(
2105
2284
  const autonomyLabel = supervisorConfig.autonomy;
2106
2285
 
2107
2286
  // Build wave plan summary
2108
- const waveSummary = batchState.totalWaves > 0
2109
- ? `${batchState.currentWaveIndex + 1}/${batchState.totalWaves} waves`
2110
- : "planning";
2287
+ const waveSummary =
2288
+ batchState.totalWaves > 0
2289
+ ? `${batchState.currentWaveIndex + 1}/${batchState.totalWaves} waves`
2290
+ : "planning";
2111
2291
 
2112
2292
  const actionsPath = auditTrailPath(stateRoot);
2113
2293
  const integrationMode = config.orchestrator.integration;
@@ -2311,7 +2491,6 @@ Now that you've activated:
2311
2491
  return prompt;
2312
2492
  }
2313
2493
 
2314
-
2315
2494
  // ── Routing System Prompt (TP-042) ───────────────────────────────────
2316
2495
 
2317
2496
  /**
@@ -2615,7 +2794,6 @@ outcomes, and can handle failures.
2615
2794
  return prompt;
2616
2795
  }
2617
2796
 
2618
-
2619
2797
  // ── Activation ───────────────────────────────────────────────────────
2620
2798
 
2621
2799
  /**
@@ -2868,7 +3046,11 @@ export async function activateSupervisor(
2868
3046
  // Idempotent — safe even if called from takeover paths that may have
2869
3047
  // started a tailer previously (stopEventTailer is called in deactivate).
2870
3048
  startEventTailer(pi, state.eventTailer, state, (key, text) => {
2871
- try { ctx.ui.setStatus(key, text); } catch { /* non-fatal */ }
3049
+ try {
3050
+ ctx.ui.setStatus(key, text);
3051
+ } catch {
3052
+ /* non-fatal */
3053
+ }
2872
3054
  });
2873
3055
 
2874
3056
  // Send activation message to trigger the supervisor's first turn.
@@ -2941,7 +3123,14 @@ export async function deactivateSupervisor(
2941
3123
  // confirmation), present it now — before we clear state refs.
2942
3124
  if (state.pendingSummaryDeps && state.batchStateRef && state.stateRoot) {
2943
3125
  const deps = state.pendingSummaryDeps;
2944
- presentBatchSummary(pi, state.batchStateRef, state.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
3126
+ presentBatchSummary(
3127
+ pi,
3128
+ state.batchStateRef,
3129
+ state.stateRoot,
3130
+ deps.opId,
3131
+ deps.diagnostics,
3132
+ deps.mergeResults,
3133
+ );
2945
3134
  state.pendingSummaryDeps = null;
2946
3135
  }
2947
3136
 
@@ -3012,7 +3201,14 @@ export async function transitionToRoutingMode(
3012
3201
  // Present deferred batch summary if any
3013
3202
  if (state.pendingSummaryDeps && state.batchStateRef && state.stateRoot) {
3014
3203
  const deps = state.pendingSummaryDeps;
3015
- presentBatchSummary(pi, state.batchStateRef, state.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
3204
+ presentBatchSummary(
3205
+ pi,
3206
+ state.batchStateRef,
3207
+ state.stateRoot,
3208
+ deps.opId,
3209
+ deps.diagnostics,
3210
+ deps.mergeResults,
3211
+ );
3016
3212
  state.pendingSummaryDeps = null;
3017
3213
  }
3018
3214
 
@@ -3028,14 +3224,16 @@ export async function transitionToRoutingMode(
3028
3224
  pi.sendMessage(
3029
3225
  {
3030
3226
  customType: "supervisor-routing-transition",
3031
- content: [{
3032
- type: "text",
3033
- text:
3034
- `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
3035
- `🔀 **Ready for your input.**\n\n` +
3036
- routingContext.contextMessage +
3037
- `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
3038
- }],
3227
+ content: [
3228
+ {
3229
+ type: "text",
3230
+ text:
3231
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
3232
+ `🔀 **Ready for your input.**\n\n` +
3233
+ routingContext.contextMessage +
3234
+ `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
3235
+ },
3236
+ ],
3039
3237
  display: `Supervisor — ${routingContext.routingState}`,
3040
3238
  },
3041
3239
  { triggerTurn: true },
@@ -3058,10 +3256,7 @@ export async function transitionToRoutingMode(
3058
3256
  *
3059
3257
  * @since TP-041
3060
3258
  */
3061
- export function registerSupervisorPromptHook(
3062
- pi: ExtensionAPI,
3063
- state: SupervisorState,
3064
- ): void {
3259
+ export function registerSupervisorPromptHook(pi: ExtensionAPI, state: SupervisorState): void {
3065
3260
  pi.on("before_agent_start", (_event) => {
3066
3261
  if (!state.active) {
3067
3262
  return undefined; // No-op: don't modify system prompt
@@ -3072,10 +3267,7 @@ export function registerSupervisorPromptHook(
3072
3267
  // batch planning, etc.), not batch monitoring. Use the routing prompt
3073
3268
  // which includes script guidance from the primer.
3074
3269
  if (state.routingContext) {
3075
- const systemPrompt = buildRoutingSystemPrompt(
3076
- state.routingContext,
3077
- state.stateRoot,
3078
- );
3270
+ const systemPrompt = buildRoutingSystemPrompt(state.routingContext, state.stateRoot);
3079
3271
  return { systemPrompt };
3080
3272
  }
3081
3273
 
@@ -3127,7 +3319,6 @@ export function resolveSupervisorConfig(
3127
3319
  };
3128
3320
  }
3129
3321
 
3130
-
3131
3322
  // ── Lockfile Types + Helpers (TP-041 Step 2) ─────────────────────────
3132
3323
 
3133
3324
  /** Heartbeat interval in milliseconds (30 seconds). */
@@ -3279,7 +3470,10 @@ export async function readLockfileAsync(stateRoot: string): Promise<SupervisorLo
3279
3470
  *
3280
3471
  * @since TP-070
3281
3472
  */
3282
- export async function writeLockfileAsync(stateRoot: string, lock: SupervisorLockfile): Promise<void> {
3473
+ export async function writeLockfileAsync(
3474
+ stateRoot: string,
3475
+ lock: SupervisorLockfile,
3476
+ ): Promise<void> {
3283
3477
  const dir = join(stateRoot, ".pi", "supervisor");
3284
3478
  if (!existsSync(dir)) {
3285
3479
  mkdirSync(dir, { recursive: true });
@@ -3357,9 +3551,7 @@ export function isLockStale(lock: SupervisorLockfile): boolean {
3357
3551
  * If batch-state.json has one of these phases, there's no active batch
3358
3552
  * and no lockfile arbitration is needed.
3359
3553
  */
3360
- const TERMINAL_PHASES = new Set<string>([
3361
- "idle", "completed", "failed", "stopped",
3362
- ]);
3554
+ const TERMINAL_PHASES = new Set<string>(["idle", "completed", "failed", "stopped"]);
3363
3555
 
3364
3556
  /**
3365
3557
  * Check whether a batch phase is terminal (no active batch).
@@ -3443,16 +3635,15 @@ export function checkSupervisorLockOnStartup(
3443
3635
  *
3444
3636
  * @since TP-041
3445
3637
  */
3446
- export function buildTakeoverSummary(
3447
- stateRoot: string,
3448
- batchState: PersistedBatchState,
3449
- ): string {
3638
+ export function buildTakeoverSummary(stateRoot: string, batchState: PersistedBatchState): string {
3450
3639
  const lines: string[] = [];
3451
3640
 
3452
3641
  lines.push(`📋 **Taking over batch ${batchState.batchId}**`);
3453
3642
  lines.push("");
3454
3643
  lines.push(`**Phase:** ${batchState.phase}`);
3455
- lines.push(`**Wave:** ${batchState.currentWaveIndex + 1}/${batchState.wavePlan?.length ?? batchState.totalWaves ?? "?"}`);
3644
+ lines.push(
3645
+ `**Wave:** ${batchState.currentWaveIndex + 1}/${batchState.wavePlan?.length ?? batchState.totalWaves ?? "?"}`,
3646
+ );
3456
3647
  lines.push(`**Base branch:** ${batchState.baseBranch}`);
3457
3648
 
3458
3649
  // Task summary from persisted state
@@ -3461,7 +3652,9 @@ export function buildTakeoverSummary(
3461
3652
  const failed = tasks.filter((t) => t.status === "failed").length;
3462
3653
  const running = tasks.filter((t) => t.status === "running").length;
3463
3654
  const pending = tasks.filter((t) => t.status === "pending").length;
3464
- lines.push(`**Tasks:** ${succeeded} succeeded, ${failed} failed, ${running} running, ${pending} pending`);
3655
+ lines.push(
3656
+ `**Tasks:** ${succeeded} succeeded, ${failed} failed, ${running} running, ${pending} pending`,
3657
+ );
3465
3658
 
3466
3659
  // Recent actions from audit trail (using readAuditTrail helper)
3467
3660
  const recentActions = readAuditTrail(stateRoot, { limit: 5 });
@@ -3543,10 +3736,12 @@ export function startHeartbeat(
3543
3736
  pi.sendMessage(
3544
3737
  {
3545
3738
  customType: "supervisor-yield",
3546
- content: [{
3547
- type: "text",
3548
- text: "⚡ Another session has taken over supervisor duties. Yielding.",
3549
- }],
3739
+ content: [
3740
+ {
3741
+ type: "text",
3742
+ text: "⚡ Another session has taken over supervisor duties. Yielding.",
3743
+ },
3744
+ ],
3550
3745
  display: "Supervisor yielded to another session",
3551
3746
  },
3552
3747
  { triggerTurn: false },
@@ -3583,7 +3778,6 @@ export function startHeartbeat(
3583
3778
  return timer;
3584
3779
  }
3585
3780
 
3586
-
3587
3781
  // ── Engine Event Consumption + Notifications (TP-041 Step 3) ─────────
3588
3782
 
3589
3783
  /**
@@ -3825,7 +4019,11 @@ export function readNewBytes(eventsPath: string, byteOffset: number): [string, n
3825
4019
  return ["", byteOffset];
3826
4020
  } finally {
3827
4021
  if (fd !== null) {
3828
- try { closeSync(fd); } catch { /* best-effort */ }
4022
+ try {
4023
+ closeSync(fd);
4024
+ } catch {
4025
+ /* best-effort */
4026
+ }
3829
4027
  }
3830
4028
  }
3831
4029
 
@@ -3843,7 +4041,10 @@ export function readNewBytes(eventsPath: string, byteOffset: number): [string, n
3843
4041
  *
3844
4042
  * @since TP-070
3845
4043
  */
3846
- export async function readNewBytesAsync(eventsPath: string, byteOffset: number): Promise<[string, number]> {
4044
+ export async function readNewBytesAsync(
4045
+ eventsPath: string,
4046
+ byteOffset: number,
4047
+ ): Promise<[string, number]> {
3847
4048
  try {
3848
4049
  const stats = await fsStat(eventsPath);
3849
4050
  const fileSize = stats.size;
@@ -3879,10 +4080,7 @@ export async function readNewBytesAsync(eventsPath: string, byteOffset: number):
3879
4080
  *
3880
4081
  * @since TP-041
3881
4082
  */
3882
- export function parseJsonlLines(
3883
- data: string,
3884
- partialLine: string,
3885
- ): [ParsedEvent[], string] {
4083
+ export function parseJsonlLines(data: string, partialLine: string): [ParsedEvent[], string] {
3886
4084
  const combined = partialLine + data;
3887
4085
  const lines = combined.split("\n");
3888
4086
 
@@ -3939,9 +4137,7 @@ export function formatEventNotification(
3939
4137
  return `🔀 Wave ${waveNum} merge starting...`;
3940
4138
  }
3941
4139
  case "merge_success": {
3942
- const waveProg = event.totalWaves
3943
- ? ` (${waveNum}/${event.totalWaves})`
3944
- : "";
4140
+ const waveProg = event.totalWaves ? ` (${waveNum}/${event.totalWaves})` : "";
3945
4141
  const testInfo = event.testCount ? ` Tests pass (${event.testCount}).` : " Tests pass.";
3946
4142
  return `✅ **Wave ${waveNum} merged successfully**${waveProg}.${testInfo}`;
3947
4143
  }
@@ -3951,8 +4147,10 @@ export function formatEventNotification(
3951
4147
  if (autonomy === "autonomous") {
3952
4148
  return `⚠️ Wave ${waveNum} merge failed${laneInfo}: ${reason}. Attempting recovery...`;
3953
4149
  }
3954
- return `⚠️ **Wave ${waveNum} merge failed**${laneInfo}: ${reason}.\n` +
3955
- ` Recovery may be needed. Check the merge logs for details.`;
4150
+ return (
4151
+ `⚠️ **Wave ${waveNum} merge failed**${laneInfo}: ${reason}.\n` +
4152
+ ` Recovery may be needed. Check the merge logs for details.`
4153
+ );
3956
4154
  }
3957
4155
  case "merge_health_warning": {
3958
4156
  const lane = event.laneNumber !== undefined ? event.laneNumber : "?";
@@ -3971,20 +4169,23 @@ export function formatEventNotification(
3971
4169
  case "batch_complete": {
3972
4170
  const parts: string[] = [];
3973
4171
  if (event.succeededTasks !== undefined) parts.push(`${event.succeededTasks} succeeded`);
3974
- if (event.failedTasks !== undefined && event.failedTasks > 0) parts.push(`${event.failedTasks} failed`);
3975
- if (event.skippedTasks !== undefined && event.skippedTasks > 0) parts.push(`${event.skippedTasks} skipped`);
3976
- if (event.blockedTasks !== undefined && event.blockedTasks > 0) parts.push(`${event.blockedTasks} blocked`);
4172
+ if (event.failedTasks !== undefined && event.failedTasks > 0)
4173
+ parts.push(`${event.failedTasks} failed`);
4174
+ if (event.skippedTasks !== undefined && event.skippedTasks > 0)
4175
+ parts.push(`${event.skippedTasks} skipped`);
4176
+ if (event.blockedTasks !== undefined && event.blockedTasks > 0)
4177
+ parts.push(`${event.blockedTasks} blocked`);
3977
4178
  const summary = parts.length > 0 ? parts.join(", ") : "all tasks processed";
3978
- const duration = event.batchDurationMs
3979
- ? ` in ${formatDuration(event.batchDurationMs)}`
3980
- : "";
4179
+ const duration = event.batchDurationMs ? ` in ${formatDuration(event.batchDurationMs)}` : "";
3981
4180
  return `🏁 **Batch complete!** ${summary}${duration}.`;
3982
4181
  }
3983
4182
  case "batch_paused": {
3984
4183
  const reason = event.reason || "unknown reason";
3985
4184
  if (autonomy === "interactive") {
3986
- return `⏸️ **Batch paused:** ${reason}\n` +
3987
- ` What would you like to do? Options: fix the issue, skip the task, or abort.`;
4185
+ return (
4186
+ `⏸️ **Batch paused:** ${reason}\n` +
4187
+ ` What would you like to do? Options: fix the issue, skip the task, or abort.`
4188
+ );
3988
4189
  }
3989
4190
  return `⏸️ **Batch paused:** ${reason}`;
3990
4191
  }
@@ -3995,12 +4196,16 @@ export function formatEventNotification(
3995
4196
  return `⚡ **Tier 0 escalation** (${pattern}): Investigating automatically. ${suggestion}`;
3996
4197
  }
3997
4198
  if (autonomy === "interactive") {
3998
- return `❌ **Tier 0 escalation** (${pattern}): ${suggestion}\n` +
3999
- ` Need your input on how to proceed.`;
4199
+ return (
4200
+ `❌ **Tier 0 escalation** (${pattern}): ${suggestion}\n` +
4201
+ ` Need your input on how to proceed.`
4202
+ );
4000
4203
  }
4001
4204
  // supervised
4002
- return `⚡ **Tier 0 escalation** (${pattern}): ${suggestion}\n` +
4003
- ` Diagnosing will ask if novel recovery is needed.`;
4205
+ return (
4206
+ `⚡ **Tier 0 escalation** (${pattern}): ${suggestion}\n` +
4207
+ ` Diagnosing — will ask if novel recovery is needed.`
4208
+ );
4004
4209
  }
4005
4210
  default:
4006
4211
  return `📌 Event: ${event.type} (wave ${waveNum})`;
@@ -4039,9 +4244,7 @@ export function formatTaskDigest(
4039
4244
  }
4040
4245
 
4041
4246
  if (buf.recoveryAttempts > 0 && autonomy !== "autonomous") {
4042
- const successRate = buf.recoverySuccesses > 0
4043
- ? ` (${buf.recoverySuccesses} succeeded)`
4044
- : "";
4247
+ const successRate = buf.recoverySuccesses > 0 ? ` (${buf.recoverySuccesses} succeeded)` : "";
4045
4248
  parts.push(`🔄 ${buf.recoveryAttempts} recovery attempt(s)${successRate}`);
4046
4249
  }
4047
4250
 
@@ -4305,7 +4508,11 @@ export function startEventTailer(
4305
4508
  if (tailer.pollTimer && typeof tailer.pollTimer === "object" && "unref" in tailer.pollTimer) {
4306
4509
  tailer.pollTimer.unref();
4307
4510
  }
4308
- if (tailer.digestTimer && typeof tailer.digestTimer === "object" && "unref" in tailer.digestTimer) {
4511
+ if (
4512
+ tailer.digestTimer &&
4513
+ typeof tailer.digestTimer === "object" &&
4514
+ "unref" in tailer.digestTimer
4515
+ ) {
4309
4516
  tailer.digestTimer.unref();
4310
4517
  }
4311
4518
  }