taskplane 0.6.1 → 0.7.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.
- package/README.md +6 -5
- package/dashboard/public/app.js +227 -0
- package/dashboard/public/index.html +19 -0
- package/dashboard/public/style.css +319 -0
- package/dashboard/server.cjs +219 -1
- package/extensions/taskplane/config-loader.ts +7 -1
- package/extensions/taskplane/config-schema.ts +19 -2
- package/extensions/taskplane/config.ts +23 -1
- package/extensions/taskplane/engine.ts +913 -46
- package/extensions/taskplane/execution.ts +1 -0
- package/extensions/taskplane/extension.ts +975 -54
- package/extensions/taskplane/formatting.ts +713 -712
- package/extensions/taskplane/index.ts +1 -0
- package/extensions/taskplane/merge.ts +236 -48
- package/extensions/taskplane/messages.ts +10 -0
- package/extensions/taskplane/persistence.ts +183 -3
- package/extensions/taskplane/resume.ts +243 -24
- package/extensions/taskplane/settings-tui.ts +10 -3
- package/extensions/taskplane/supervisor-primer.md +626 -0
- package/extensions/taskplane/supervisor.ts +3659 -0
- package/extensions/taskplane/types.ts +330 -3
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
2
2
|
|
|
3
3
|
import { execSync, execFileSync } from "child_process";
|
|
4
4
|
import { writeFileSync, unlinkSync, mkdirSync, existsSync, readdirSync } from "fs";
|
|
5
|
-
import { join
|
|
5
|
+
import { join } from "path";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_ORCHESTRATOR_CONFIG,
|
|
@@ -25,10 +25,13 @@ import {
|
|
|
25
25
|
formatWavePlan,
|
|
26
26
|
freshOrchBatchState,
|
|
27
27
|
getCurrentBranch,
|
|
28
|
+
hasConfigFiles,
|
|
29
|
+
resolveConfigRoot,
|
|
28
30
|
listOrchSessions,
|
|
29
31
|
listWorktrees,
|
|
30
32
|
loadBatchState,
|
|
31
33
|
loadOrchestratorConfig,
|
|
34
|
+
loadSupervisorConfig,
|
|
32
35
|
loadTaskRunnerConfig,
|
|
33
36
|
parseOrchSessionNames,
|
|
34
37
|
resolveOperatorId,
|
|
@@ -40,6 +43,20 @@ import {
|
|
|
40
43
|
} from "./index.ts";
|
|
41
44
|
import { buildExecutionContext } from "./workspace.ts";
|
|
42
45
|
import { openSettingsTui } from "./settings-tui.ts";
|
|
46
|
+
import {
|
|
47
|
+
activateSupervisor,
|
|
48
|
+
deactivateSupervisor,
|
|
49
|
+
freshSupervisorState,
|
|
50
|
+
registerSupervisorPromptHook,
|
|
51
|
+
checkSupervisorLockOnStartup,
|
|
52
|
+
buildTakeoverSummary,
|
|
53
|
+
isProcessAlive,
|
|
54
|
+
isBatchTerminal,
|
|
55
|
+
DEFAULT_SUPERVISOR_CONFIG,
|
|
56
|
+
triggerSupervisorIntegration,
|
|
57
|
+
presentBatchSummary,
|
|
58
|
+
} from "./supervisor.ts";
|
|
59
|
+
import type { SupervisorConfig, IntegrationExecutor, CiDeps, SummaryDeps } from "./supervisor.ts";
|
|
43
60
|
import type {
|
|
44
61
|
AbortMode,
|
|
45
62
|
ExecutionContext,
|
|
@@ -683,6 +700,350 @@ export function collectRepoCleanupFindings(
|
|
|
683
700
|
return findings;
|
|
684
701
|
}
|
|
685
702
|
|
|
703
|
+
// ── TP-040: Non-Blocking Engine Launch Helper ───────────────────────
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Start an engine execution (batch or resume) without blocking the caller.
|
|
707
|
+
*
|
|
708
|
+
* Launches `engineFn` as a fire-and-forget promise — the command handler
|
|
709
|
+
* returns immediately so the pi session stays interactive. All state
|
|
710
|
+
* transitions are communicated via the existing callback mechanism
|
|
711
|
+
* (onNotify, onMonitorUpdate) and engine events.
|
|
712
|
+
*
|
|
713
|
+
* The `.catch()` error boundary handles unexpected rejections from the
|
|
714
|
+
* engine by:
|
|
715
|
+
* 1. Setting the batch state to "failed" with the error
|
|
716
|
+
* 2. Notifying the operator
|
|
717
|
+
* 3. Refreshing the dashboard widget
|
|
718
|
+
*
|
|
719
|
+
* This prevents unhandled promise rejections from crashing the session
|
|
720
|
+
* or leaving batch state inconsistent.
|
|
721
|
+
*/
|
|
722
|
+
export function startBatchAsync(
|
|
723
|
+
engineFn: () => Promise<void>,
|
|
724
|
+
batchState: import("./types.ts").OrchBatchRuntimeState,
|
|
725
|
+
ctx: ExtensionContext,
|
|
726
|
+
updateWidget: () => void,
|
|
727
|
+
onTerminal?: () => void,
|
|
728
|
+
): void {
|
|
729
|
+
// Detach engine start to the next tick so the command handler returns
|
|
730
|
+
// immediately. Without this, the synchronous planning/discovery phase
|
|
731
|
+
// of the engine would block the handler until its first await.
|
|
732
|
+
setTimeout(() => {
|
|
733
|
+
engineFn()
|
|
734
|
+
.then(() => {
|
|
735
|
+
// Engine completed normally — final widget update
|
|
736
|
+
updateWidget();
|
|
737
|
+
// TP-041 R002-3: Deactivate supervisor on all terminal paths
|
|
738
|
+
onTerminal?.();
|
|
739
|
+
})
|
|
740
|
+
.catch((err: unknown) => {
|
|
741
|
+
// Unhandled engine rejection — surface to operator and update state
|
|
742
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
743
|
+
if (batchState.phase !== "completed" && batchState.phase !== "failed") {
|
|
744
|
+
batchState.phase = "failed";
|
|
745
|
+
batchState.endedAt = Date.now();
|
|
746
|
+
batchState.errors.push(`Unhandled engine error: ${errMsg}`);
|
|
747
|
+
}
|
|
748
|
+
ctx.ui.notify(
|
|
749
|
+
`❌ Engine crashed with unhandled error: ${errMsg}\n` +
|
|
750
|
+
` Batch ${batchState.batchId} marked as failed.`,
|
|
751
|
+
"error",
|
|
752
|
+
);
|
|
753
|
+
updateWidget();
|
|
754
|
+
// TP-041 R002-3: Deactivate supervisor on all terminal paths
|
|
755
|
+
onTerminal?.();
|
|
756
|
+
});
|
|
757
|
+
}, 0);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ── TP-043 R002-2: Integration Executor Builder ─────────────────────
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Build an integration executor callback for `triggerSupervisorIntegration`.
|
|
764
|
+
*
|
|
765
|
+
* Wraps `executeIntegration` with the appropriate deps (runGit, runCommand,
|
|
766
|
+
* deleteBatchState) so the supervisor module can execute integration without
|
|
767
|
+
* importing from extension.ts (avoiding circular dependencies).
|
|
768
|
+
*
|
|
769
|
+
* The executor ensures the working directory is on the base branch before
|
|
770
|
+
* executing, matching the behavior of `/orch-integrate`.
|
|
771
|
+
*
|
|
772
|
+
* @param repoRoot - Repository root directory for git operations
|
|
773
|
+
* @returns Integration executor callback
|
|
774
|
+
*
|
|
775
|
+
* @since TP-043 R002
|
|
776
|
+
*/
|
|
777
|
+
export function buildIntegrationExecutor(repoRoot: string): IntegrationExecutor {
|
|
778
|
+
return (mode, context) => {
|
|
779
|
+
// Ensure we're on the base branch before integrating
|
|
780
|
+
const currentBranch = getCurrentBranch(repoRoot);
|
|
781
|
+
if (currentBranch && currentBranch !== context.baseBranch) {
|
|
782
|
+
const checkoutResult = runGit(["checkout", context.baseBranch], repoRoot);
|
|
783
|
+
if (!checkoutResult.ok) {
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
integratedLocally: false,
|
|
787
|
+
commitCount: "0",
|
|
788
|
+
message: "",
|
|
789
|
+
error: `Failed to switch to base branch ${context.baseBranch}: ${checkoutResult.stderr}`,
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// Build deps matching the /orch-integrate handler pattern
|
|
795
|
+
const deps: IntegrationExecDeps = {
|
|
796
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repoRoot),
|
|
797
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
798
|
+
try {
|
|
799
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
800
|
+
encoding: "utf-8",
|
|
801
|
+
timeout: 60_000,
|
|
802
|
+
cwd: repoRoot,
|
|
803
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
804
|
+
}).trim();
|
|
805
|
+
return { ok: true, stdout, stderr: "" };
|
|
806
|
+
} catch (err: unknown) {
|
|
807
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
808
|
+
return {
|
|
809
|
+
ok: false,
|
|
810
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
811
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
deleteBatchState: () => {
|
|
816
|
+
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
817
|
+
},
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
return executeIntegration(mode as IntegrateMode, {
|
|
821
|
+
...context,
|
|
822
|
+
currentBranch: context.baseBranch,
|
|
823
|
+
}, deps);
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Build CI deps for programmatic PR polling and merge (R002-2).
|
|
829
|
+
*
|
|
830
|
+
* Creates the `CiDeps` object needed by `triggerSupervisorIntegration`
|
|
831
|
+
* for auto/PR mode CI status polling and PR merge operations.
|
|
832
|
+
*
|
|
833
|
+
* @param repoRoot - Repository root directory
|
|
834
|
+
* @returns CiDeps with gh CLI and git wrappers
|
|
835
|
+
*
|
|
836
|
+
* @since TP-043
|
|
837
|
+
*/
|
|
838
|
+
export function buildCiDeps(repoRoot: string): CiDeps {
|
|
839
|
+
return {
|
|
840
|
+
runCommand: (cmd: string, cmdArgs: string[]) => {
|
|
841
|
+
try {
|
|
842
|
+
const stdout = execFileSync(cmd, cmdArgs, {
|
|
843
|
+
encoding: "utf-8",
|
|
844
|
+
timeout: 60_000,
|
|
845
|
+
cwd: repoRoot,
|
|
846
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
847
|
+
}).trim();
|
|
848
|
+
return { ok: true, stdout, stderr: "" };
|
|
849
|
+
} catch (err: unknown) {
|
|
850
|
+
const e = err as { stdout?: string; stderr?: string; message?: string };
|
|
851
|
+
return {
|
|
852
|
+
ok: false,
|
|
853
|
+
stdout: (e.stdout ?? "").toString().trim(),
|
|
854
|
+
stderr: (e.stderr ?? e.message ?? "unknown error").toString().trim(),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
runGit: (gitArgs: string[]) => runGit(gitArgs, repoRoot),
|
|
859
|
+
deleteBatchState: () => {
|
|
860
|
+
try { deleteBatchState(repoRoot); } catch { /* best effort */ }
|
|
861
|
+
},
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// ── /orch Routing Logic (TP-042) ─────────────────────────────────────
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Project state for /orch no-args routing.
|
|
869
|
+
*
|
|
870
|
+
* Evaluated in strict precedence order (R001-3) — the first matching state wins:
|
|
871
|
+
* 1. active-batch → Batch is running (non-terminal phase) → status report
|
|
872
|
+
* 2. completed-batch → Completed batch + orch branch exists → offer integration
|
|
873
|
+
* 3. no-config → No taskplane config exists → onboarding flow
|
|
874
|
+
* 4. pending-tasks → Config exists, pending tasks found → offer to start batch
|
|
875
|
+
* 5. no-tasks → Config exists, no pending tasks → help create tasks
|
|
876
|
+
*
|
|
877
|
+
* Active batch and completed batch are checked before no-config so that an
|
|
878
|
+
* orphaned batch-state.json or orch branch isn't silently ignored even if
|
|
879
|
+
* the config file was deleted.
|
|
880
|
+
*
|
|
881
|
+
* @since TP-042
|
|
882
|
+
*/
|
|
883
|
+
export type OrchProjectState =
|
|
884
|
+
| "no-config"
|
|
885
|
+
| "active-batch"
|
|
886
|
+
| "completed-batch"
|
|
887
|
+
| "pending-tasks"
|
|
888
|
+
| "no-tasks";
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Result of detectOrchState — provides the detected state plus
|
|
892
|
+
* context data for supervisor routing (e.g., batch info, task count).
|
|
893
|
+
*
|
|
894
|
+
* @since TP-042
|
|
895
|
+
*/
|
|
896
|
+
export interface OrchStateDetection {
|
|
897
|
+
/** The detected project state */
|
|
898
|
+
state: OrchProjectState;
|
|
899
|
+
/** Human-readable context message for the supervisor activation prompt */
|
|
900
|
+
contextMessage: string;
|
|
901
|
+
/** Number of pending tasks (only set for pending-tasks state) */
|
|
902
|
+
pendingTaskCount?: number;
|
|
903
|
+
/** Batch ID (set for active-batch and completed-batch states) */
|
|
904
|
+
batchId?: string;
|
|
905
|
+
/** Batch phase (set for active-batch state) */
|
|
906
|
+
batchPhase?: string;
|
|
907
|
+
/** Orch branch name (set for completed-batch state) */
|
|
908
|
+
orchBranch?: string;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Dependencies injected into detectOrchState for testability.
|
|
913
|
+
*
|
|
914
|
+
* @since TP-042
|
|
915
|
+
*/
|
|
916
|
+
export interface OrchStateDetectionDeps {
|
|
917
|
+
/** Check if any taskplane config file exists (JSON or YAML) */
|
|
918
|
+
hasConfig: () => boolean;
|
|
919
|
+
/** Load persisted batch state (null if no state file) */
|
|
920
|
+
loadBatchState: () => PersistedBatchState | null;
|
|
921
|
+
/** List local orch/* branches */
|
|
922
|
+
listOrchBranches: () => string[];
|
|
923
|
+
/** Run task discovery to count pending tasks */
|
|
924
|
+
countPendingTasks: () => number;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Detect the current project state for /orch no-args routing.
|
|
929
|
+
*
|
|
930
|
+
* Evaluates state in strict precedence order (see OrchProjectState).
|
|
931
|
+
* The first matching condition wins — no further checks are performed.
|
|
932
|
+
*
|
|
933
|
+
* This is a pure function with injected dependencies for testability.
|
|
934
|
+
*
|
|
935
|
+
* @param deps - Injected dependencies for state detection
|
|
936
|
+
* @returns Detection result with state and context message
|
|
937
|
+
*
|
|
938
|
+
* @since TP-042
|
|
939
|
+
*/
|
|
940
|
+
export function detectOrchState(deps: OrchStateDetectionDeps): OrchStateDetection {
|
|
941
|
+
// Precedence order (R001-3): active batch → completed-needs-integration
|
|
942
|
+
// → no-config → pending tasks → no tasks. Active batch is checked first
|
|
943
|
+
// because an orphaned batch-state.json should be surfaced even if config
|
|
944
|
+
// was deleted. No-config (onboarding) is checked after batch states so
|
|
945
|
+
// an in-progress/completed batch isn't silently ignored.
|
|
946
|
+
|
|
947
|
+
// ── 1. Active batch (non-terminal phase) → status report ─────
|
|
948
|
+
try {
|
|
949
|
+
const batchState = deps.loadBatchState();
|
|
950
|
+
if (batchState && !isBatchTerminal(batchState.phase)) {
|
|
951
|
+
const elapsed = batchState.endedAt
|
|
952
|
+
? Math.round((batchState.endedAt - batchState.startedAt) / 1000)
|
|
953
|
+
: Math.round((Date.now() - batchState.startedAt) / 1000);
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
state: "active-batch",
|
|
957
|
+
batchId: batchState.batchId,
|
|
958
|
+
batchPhase: batchState.phase,
|
|
959
|
+
contextMessage:
|
|
960
|
+
`Batch ${batchState.batchId} is currently ${batchState.phase}. ` +
|
|
961
|
+
`Wave ${batchState.currentWaveIndex + 1}/${batchState.totalWaves ?? "?"}, ` +
|
|
962
|
+
`${batchState.succeededTasks ?? 0} succeeded, ` +
|
|
963
|
+
`${batchState.failedTasks ?? 0} failed, ` +
|
|
964
|
+
`${batchState.skippedTasks ?? 0} skipped / ` +
|
|
965
|
+
`${batchState.totalTasks ?? "?"} total. ` +
|
|
966
|
+
`Elapsed: ${elapsed}s.`,
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// ── 2. Completed batch + orch branch → offer integration ───
|
|
971
|
+
// R002-2: Validate that the orch branch still exists in git before
|
|
972
|
+
// offering integration. Stale batch-state can reference a deleted branch.
|
|
973
|
+
if (batchState && batchState.phase === "completed" && batchState.orchBranch) {
|
|
974
|
+
const existingBranches = deps.listOrchBranches();
|
|
975
|
+
const branchExists = existingBranches.includes(batchState.orchBranch);
|
|
976
|
+
if (branchExists) {
|
|
977
|
+
return {
|
|
978
|
+
state: "completed-batch",
|
|
979
|
+
batchId: batchState.batchId,
|
|
980
|
+
orchBranch: batchState.orchBranch,
|
|
981
|
+
contextMessage:
|
|
982
|
+
`Your last batch (${batchState.batchId}) completed — ` +
|
|
983
|
+
`${batchState.succeededTasks ?? 0}/${batchState.totalTasks ?? "?"} tasks succeeded. ` +
|
|
984
|
+
`The orch branch \`${batchState.orchBranch}\` is ready to integrate. ` +
|
|
985
|
+
`Want me to create a PR to ${batchState.baseBranch}, or integrate directly?`,
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
// Branch was deleted — fall through to remaining checks
|
|
989
|
+
}
|
|
990
|
+
} catch {
|
|
991
|
+
// Batch state unreadable — fall through to check for orch branches
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ── 2b. No batch state but orch branches exist → offer integration
|
|
995
|
+
// Covers the case where batch-state.json was deleted but an orch branch remains.
|
|
996
|
+
const orchBranches = deps.listOrchBranches();
|
|
997
|
+
if (orchBranches.length > 0) {
|
|
998
|
+
const branchList = orchBranches.map(b => `\`${b}\``).join(", ");
|
|
999
|
+
return {
|
|
1000
|
+
state: "completed-batch",
|
|
1001
|
+
orchBranch: orchBranches[0],
|
|
1002
|
+
contextMessage:
|
|
1003
|
+
orchBranches.length === 1
|
|
1004
|
+
? `I found an orch branch (${branchList}) that hasn't been integrated yet. ` +
|
|
1005
|
+
`Want me to integrate it, or would you like to start fresh?`
|
|
1006
|
+
: `I found ${orchBranches.length} orch branches (${branchList}) that haven't been integrated. ` +
|
|
1007
|
+
`Would you like to integrate one, or start fresh?`,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// ── 3. No config exists → onboarding ─────────────────────────
|
|
1012
|
+
if (!deps.hasConfig()) {
|
|
1013
|
+
return {
|
|
1014
|
+
state: "no-config",
|
|
1015
|
+
contextMessage:
|
|
1016
|
+
"Welcome to Taskplane! I don't see a configuration for this project yet. " +
|
|
1017
|
+
"Let me help you get set up. I'll analyze your project structure, help you " +
|
|
1018
|
+
"define task areas, check your git branching strategy, and generate the config files.",
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// ── 4. Pending tasks exist → offer to start batch ────────────
|
|
1023
|
+
const pendingCount = deps.countPendingTasks();
|
|
1024
|
+
if (pendingCount > 0) {
|
|
1025
|
+
return {
|
|
1026
|
+
state: "pending-tasks",
|
|
1027
|
+
pendingTaskCount: pendingCount,
|
|
1028
|
+
contextMessage:
|
|
1029
|
+
`Welcome back! You have ${pendingCount} pending task${pendingCount === 1 ? "" : "s"} ready to run. ` +
|
|
1030
|
+
`Want me to start the batch, or would you like to review the plan first?`,
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ── 5. No pending tasks → help create tasks ──────────────────
|
|
1035
|
+
return {
|
|
1036
|
+
state: "no-tasks",
|
|
1037
|
+
contextMessage:
|
|
1038
|
+
"No pending tasks right now. Here's what I can help with:\n" +
|
|
1039
|
+
"• Create tasks from a spec or design doc\n" +
|
|
1040
|
+
"• Pull in GitHub Issues\n" +
|
|
1041
|
+
"• Write a new spec for something you want to build\n" +
|
|
1042
|
+
"• Run a project health check\n" +
|
|
1043
|
+
"What interests you?",
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
|
|
686
1047
|
// ── Extension ────────────────────────────────────────────────────────
|
|
687
1048
|
|
|
688
1049
|
export default function (pi: ExtensionAPI) {
|
|
@@ -692,6 +1053,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
692
1053
|
let orchWidgetCtx: ExtensionContext | undefined;
|
|
693
1054
|
let latestMonitorState: MonitorState | null = null;
|
|
694
1055
|
|
|
1056
|
+
// ── Supervisor State (TP-041) ────────────────────────────────────
|
|
1057
|
+
let supervisorState = freshSupervisorState();
|
|
1058
|
+
let supervisorConfig: SupervisorConfig = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
1059
|
+
|
|
1060
|
+
// Register supervisor prompt hook: while active, injects supervisor
|
|
1061
|
+
// system prompt on every LLM turn. No-op when supervisor is inactive.
|
|
1062
|
+
registerSupervisorPromptHook(pi, supervisorState);
|
|
1063
|
+
|
|
695
1064
|
/**
|
|
696
1065
|
* Execution context loaded at session start. Null if startup failed
|
|
697
1066
|
* (e.g., workspace config present but invalid). Commands check this
|
|
@@ -735,18 +1104,83 @@ export default function (pi: ExtensionAPI) {
|
|
|
735
1104
|
// ── Commands ─────────────────────────────────────────────────────
|
|
736
1105
|
|
|
737
1106
|
pi.registerCommand("orch", {
|
|
738
|
-
description: "Start batch execution: /orch <areas|paths|all>",
|
|
1107
|
+
description: "Start batch execution or supervisor: /orch [<areas|paths|all>]",
|
|
739
1108
|
handler: async (args, ctx) => {
|
|
1109
|
+
// ── TP-042: No-args → supervisor routing ─────────────────
|
|
1110
|
+
// When /orch is called without arguments, detect project state
|
|
1111
|
+
// and activate the supervisor with routing context instead of
|
|
1112
|
+
// showing usage. The supervisor then guides the operator through
|
|
1113
|
+
// the appropriate flow (onboarding, batch planning, etc.).
|
|
740
1114
|
if (!args?.trim()) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1115
|
+
// For "no-config" state we don't need execCtx — just send the
|
|
1116
|
+
// routing context. For all other states, we need it.
|
|
1117
|
+
// R002-1: Mirror the config loading resolution chain (resolveConfigRoot)
|
|
1118
|
+
// so /orch routing detects config in the same location the loader uses.
|
|
1119
|
+
// This handles pointer-based workspace setups where config lives at
|
|
1120
|
+
// pointer.configRoot, not at the worktree cwd.
|
|
1121
|
+
const cwd = execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? ctx.cwd;
|
|
1122
|
+
const pointerConfigRoot = execCtx?.pointer?.configRoot;
|
|
1123
|
+
const resolvedConfigRoot = resolveConfigRoot(cwd, pointerConfigRoot);
|
|
1124
|
+
const stateRoot = execCtx?.repoRoot ?? ctx.cwd;
|
|
1125
|
+
const repoRoot = execCtx?.repoRoot ?? ctx.cwd;
|
|
1126
|
+
|
|
1127
|
+
// Detect project state with strict precedence order
|
|
1128
|
+
const detection = detectOrchState({
|
|
1129
|
+
hasConfig: () => hasConfigFiles(resolvedConfigRoot),
|
|
1130
|
+
loadBatchState: () => {
|
|
1131
|
+
try { return loadBatchState(stateRoot); }
|
|
1132
|
+
catch { return null; }
|
|
1133
|
+
},
|
|
1134
|
+
listOrchBranches: () => {
|
|
1135
|
+
const result = runGit(["branch", "--list", "orch/*"], repoRoot);
|
|
1136
|
+
return result.ok
|
|
1137
|
+
? result.stdout.split("\n").map(b => b.replace(/^\*?\s+/, "").trim()).filter(Boolean)
|
|
1138
|
+
: [];
|
|
1139
|
+
},
|
|
1140
|
+
countPendingTasks: () => {
|
|
1141
|
+
if (!execCtx) return 0;
|
|
1142
|
+
try {
|
|
1143
|
+
const discovery = runDiscovery("all", runnerConfig.task_areas, execCtx.workspaceRoot, {
|
|
1144
|
+
dependencySource: orchConfig.dependencies.source,
|
|
1145
|
+
useDependencyCache: orchConfig.dependencies.cache,
|
|
1146
|
+
workspaceConfig: execCtx.workspaceConfig,
|
|
1147
|
+
});
|
|
1148
|
+
return discovery.pending.size;
|
|
1149
|
+
} catch { return 0; }
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// ── Active batch → show status only (supervisor already running) ─
|
|
1154
|
+
if (detection.state === "active-batch") {
|
|
1155
|
+
ctx.ui.notify(
|
|
1156
|
+
`🔀 ${detection.contextMessage}\n\n` +
|
|
1157
|
+
`Use /orch-status for full details, or /orch-pause to pause.`,
|
|
1158
|
+
"info",
|
|
1159
|
+
);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// For non-onboarding states, we need execCtx
|
|
1164
|
+
if (detection.state !== "no-config" && !requireExecCtx(ctx)) return;
|
|
1165
|
+
|
|
1166
|
+
// Activate supervisor with routing context.
|
|
1167
|
+
// The routingContext parameter skips lockfile/heartbeat/event-tailer
|
|
1168
|
+
// (no active batch to monitor) and sends a routing-specific activation
|
|
1169
|
+
// message instead of the generic "Batch started" one.
|
|
1170
|
+
activateSupervisor(
|
|
1171
|
+
pi,
|
|
1172
|
+
supervisorState,
|
|
1173
|
+
orchBatchState,
|
|
1174
|
+
orchConfig,
|
|
1175
|
+
supervisorConfig,
|
|
1176
|
+
stateRoot,
|
|
1177
|
+
ctx,
|
|
1178
|
+
{
|
|
1179
|
+
routingState: detection.state,
|
|
1180
|
+
contextMessage: detection.contextMessage,
|
|
1181
|
+
},
|
|
749
1182
|
);
|
|
1183
|
+
|
|
750
1184
|
return;
|
|
751
1185
|
}
|
|
752
1186
|
|
|
@@ -834,37 +1268,139 @@ export default function (pi: ExtensionAPI) {
|
|
|
834
1268
|
// Reset batch state for new execution
|
|
835
1269
|
orchBatchState = freshOrchBatchState();
|
|
836
1270
|
latestMonitorState = null;
|
|
1271
|
+
|
|
1272
|
+
// ── TP-040: Set launching phase synchronously ────────────
|
|
1273
|
+
// Mark as "launching" before the setTimeout detach so that
|
|
1274
|
+
// /orch-status, /orch-pause, /orch-abort issued immediately
|
|
1275
|
+
// after /orch returns can see that a batch is being started.
|
|
1276
|
+
// The engine will transition from "launching" → "planning"
|
|
1277
|
+
// on the next tick when it actually begins work.
|
|
1278
|
+
orchBatchState.phase = "launching";
|
|
1279
|
+
orchBatchState.startedAt = Date.now();
|
|
837
1280
|
updateOrchWidget();
|
|
838
1281
|
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1282
|
+
// ── TP-040: Non-blocking engine launch ───────────────────
|
|
1283
|
+
// Start the engine without awaiting — the command handler returns
|
|
1284
|
+
// immediately so the pi session remains interactive (enables
|
|
1285
|
+
// supervisor agent and operator conversation during batch).
|
|
1286
|
+
// The .catch() error boundary ensures unhandled rejections from
|
|
1287
|
+
// the engine are surfaced to the operator and reflected in state.
|
|
1288
|
+
startBatchAsync(
|
|
1289
|
+
() => executeOrchBatch(
|
|
1290
|
+
args,
|
|
1291
|
+
orchConfig,
|
|
1292
|
+
runnerConfig,
|
|
1293
|
+
repoRoot,
|
|
1294
|
+
orchBatchState,
|
|
1295
|
+
(message, level) => {
|
|
1296
|
+
ctx.ui.notify(message, level);
|
|
1297
|
+
updateOrchWidget(); // Refresh widget on every phase message
|
|
1298
|
+
},
|
|
1299
|
+
(monState: MonitorState) => {
|
|
1300
|
+
const changed = !latestMonitorState ||
|
|
1301
|
+
latestMonitorState.totalDone !== monState.totalDone ||
|
|
1302
|
+
latestMonitorState.totalFailed !== monState.totalFailed ||
|
|
1303
|
+
latestMonitorState.lanes.some((l, i) =>
|
|
1304
|
+
l.currentTaskId !== monState.lanes[i]?.currentTaskId ||
|
|
1305
|
+
l.currentStep !== monState.lanes[i]?.currentStep ||
|
|
1306
|
+
l.completedChecks !== monState.lanes[i]?.completedChecks,
|
|
1307
|
+
);
|
|
1308
|
+
latestMonitorState = monState;
|
|
1309
|
+
if (changed) updateOrchWidget(); // Only refresh on actual state change
|
|
1310
|
+
},
|
|
1311
|
+
execCtx!.workspaceConfig,
|
|
1312
|
+
execCtx!.workspaceRoot,
|
|
1313
|
+
execCtx!.pointer?.agentRoot,
|
|
1314
|
+
),
|
|
844
1315
|
orchBatchState,
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
(
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1316
|
+
ctx,
|
|
1317
|
+
updateOrchWidget,
|
|
1318
|
+
// TP-043: Deferred supervisor deactivation (R002-1).
|
|
1319
|
+
// Integration is ONLY triggered when batch completes successfully
|
|
1320
|
+
// (phase === "completed"). For paused/stopped/crash states, the
|
|
1321
|
+
// supervisor is deactivated immediately — no integration on partial
|
|
1322
|
+
// batches.
|
|
1323
|
+
// TP-043 Step 2: Batch summary is generated on all terminal paths
|
|
1324
|
+
// before supervisor deactivation.
|
|
1325
|
+
() => {
|
|
1326
|
+
const mode = orchConfig.orchestrator.integration;
|
|
1327
|
+
// TP-043: Build summary deps for all terminal paths
|
|
1328
|
+
const opId = resolveOperatorId(orchConfig);
|
|
1329
|
+
const sDeps: SummaryDeps = {
|
|
1330
|
+
opId,
|
|
1331
|
+
diagnostics: orchBatchState.diagnostics ?? null,
|
|
1332
|
+
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
1333
|
+
waveIndex: mr.waveIndex,
|
|
1334
|
+
status: mr.status,
|
|
1335
|
+
failedLane: mr.failedLane,
|
|
1336
|
+
failureReason: mr.failureReason,
|
|
1337
|
+
})),
|
|
1338
|
+
};
|
|
1339
|
+
if (
|
|
1340
|
+
orchBatchState.phase === "completed" &&
|
|
1341
|
+
(mode === "supervised" || mode === "auto")
|
|
1342
|
+
) {
|
|
1343
|
+
// Supervisor stays alive — trigger programmatic integration
|
|
1344
|
+
// flow. Supervisor deactivates itself after integration
|
|
1345
|
+
// completes (or fails) via the callback in
|
|
1346
|
+
// triggerSupervisorIntegration. Summary generated there.
|
|
1347
|
+
triggerSupervisorIntegration(
|
|
1348
|
+
pi,
|
|
1349
|
+
supervisorState,
|
|
1350
|
+
orchBatchState,
|
|
1351
|
+
mode,
|
|
1352
|
+
repoRoot,
|
|
1353
|
+
buildIntegrationExecutor(repoRoot),
|
|
1354
|
+
buildCiDeps(repoRoot),
|
|
1355
|
+
sDeps,
|
|
1356
|
+
);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
// Non-completed phase or manual mode — deactivate immediately.
|
|
1360
|
+
// Inform operator if integration was expected but skipped.
|
|
1361
|
+
if (
|
|
1362
|
+
(mode === "supervised" || mode === "auto") &&
|
|
1363
|
+
orchBatchState.phase !== "completed"
|
|
1364
|
+
) {
|
|
1365
|
+
pi.sendMessage(
|
|
1366
|
+
{
|
|
1367
|
+
customType: "supervisor-integration-skipped",
|
|
1368
|
+
content: [{
|
|
1369
|
+
type: "text",
|
|
1370
|
+
text:
|
|
1371
|
+
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
1372
|
+
`Integration skipped — only completed batches are eligible.\n` +
|
|
1373
|
+
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
1374
|
+
}],
|
|
1375
|
+
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
1376
|
+
},
|
|
1377
|
+
{ triggerTurn: false },
|
|
857
1378
|
);
|
|
858
|
-
|
|
859
|
-
|
|
1379
|
+
}
|
|
1380
|
+
// TP-043: Generate summary before deactivation (manual mode or non-completed)
|
|
1381
|
+
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1382
|
+
deactivateSupervisor(pi, supervisorState);
|
|
860
1383
|
},
|
|
861
|
-
execCtx!.workspaceConfig,
|
|
862
|
-
execCtx!.workspaceRoot,
|
|
863
|
-
execCtx!.pointer?.agentRoot,
|
|
864
1384
|
);
|
|
865
1385
|
|
|
866
|
-
//
|
|
867
|
-
|
|
1386
|
+
// ── TP-041: Activate supervisor agent ────────────────────
|
|
1387
|
+
// After the engine is launched (non-blocking), activate the
|
|
1388
|
+
// supervisor in this pi session. The system prompt is rebuilt
|
|
1389
|
+
// dynamically on each LLM turn from the live batchState ref,
|
|
1390
|
+
// ensuring batch metadata (batchId, wave/task counts) is always
|
|
1391
|
+
// current even though the engine populates it asynchronously.
|
|
1392
|
+
// Model override is resolved inside activateSupervisor via ctx.
|
|
1393
|
+
// Uses workspaceRoot (not repoRoot) so lockfile/events/batch-state
|
|
1394
|
+
// all resolve to the same .pi tree the engine writes to (R006-1).
|
|
1395
|
+
activateSupervisor(
|
|
1396
|
+
pi,
|
|
1397
|
+
supervisorState,
|
|
1398
|
+
orchBatchState,
|
|
1399
|
+
orchConfig,
|
|
1400
|
+
supervisorConfig,
|
|
1401
|
+
execCtx!.workspaceRoot,
|
|
1402
|
+
ctx,
|
|
1403
|
+
);
|
|
868
1404
|
},
|
|
869
1405
|
});
|
|
870
1406
|
|
|
@@ -978,8 +1514,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
978
1514
|
pi.registerCommand("orch-status", {
|
|
979
1515
|
description: "Show current batch progress",
|
|
980
1516
|
handler: async (_args, ctx) => {
|
|
1517
|
+
// ── TP-040: Disk fallback for idle in-memory state ────────
|
|
1518
|
+
// When in-memory state is idle, try loading from persisted
|
|
1519
|
+
// batch-state.json. This covers fresh-session queries (pi
|
|
1520
|
+
// restarted while a batch was running in tmux lanes) and
|
|
1521
|
+
// post-crash recovery where in-memory state was lost.
|
|
981
1522
|
if (orchBatchState.phase === "idle") {
|
|
982
|
-
|
|
1523
|
+
const stateRoot = execCtx?.workspaceRoot ?? execCtx?.repoRoot ?? ctx.cwd;
|
|
1524
|
+
let diskState: PersistedBatchState | null = null;
|
|
1525
|
+
try {
|
|
1526
|
+
diskState = loadBatchState(stateRoot);
|
|
1527
|
+
} catch {
|
|
1528
|
+
// Ignore errors — fall through to "no batch" message
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (!diskState) {
|
|
1532
|
+
ctx.ui.notify("No batch is running. Use /orch <areas|paths|all> to start.", "info");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// Show status from persisted state
|
|
1537
|
+
const elapsedSec = diskState.endedAt
|
|
1538
|
+
? Math.round((diskState.endedAt - diskState.startedAt) / 1000)
|
|
1539
|
+
: Math.round((Date.now() - diskState.startedAt) / 1000);
|
|
1540
|
+
|
|
1541
|
+
const lines: string[] = [
|
|
1542
|
+
`📊 Batch ${diskState.batchId} — ${diskState.phase} (from disk)`,
|
|
1543
|
+
` Wave: ${diskState.currentWaveIndex + 1}/${diskState.totalWaves}`,
|
|
1544
|
+
` Tasks: ${diskState.succeededTasks} succeeded, ${diskState.failedTasks} failed, ${diskState.skippedTasks} skipped, ${diskState.blockedTasks} blocked / ${diskState.totalTasks} total`,
|
|
1545
|
+
` Elapsed: ${elapsedSec}s`,
|
|
1546
|
+
];
|
|
1547
|
+
|
|
1548
|
+
if (diskState.errors.length > 0) {
|
|
1549
|
+
lines.push(` Errors: ${diskState.errors.length}`);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
983
1553
|
return;
|
|
984
1554
|
}
|
|
985
1555
|
|
|
@@ -1032,8 +1602,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1032
1602
|
return;
|
|
1033
1603
|
}
|
|
1034
1604
|
|
|
1035
|
-
// Prevent resume if a batch is actively running
|
|
1036
|
-
if (orchBatchState.phase === "executing" || orchBatchState.phase === "merging" || orchBatchState.phase === "planning") {
|
|
1605
|
+
// Prevent resume if a batch is actively running (includes "launching" from non-blocking detach)
|
|
1606
|
+
if (orchBatchState.phase === "launching" || orchBatchState.phase === "executing" || orchBatchState.phase === "merging" || orchBatchState.phase === "planning") {
|
|
1037
1607
|
ctx.ui.notify(
|
|
1038
1608
|
`⚠️ A batch is currently ${orchBatchState.phase} (${orchBatchState.batchId}). Cannot resume.`,
|
|
1039
1609
|
"warning",
|
|
@@ -1044,29 +1614,107 @@ export default function (pi: ExtensionAPI) {
|
|
|
1044
1614
|
// Reset batch state for resume
|
|
1045
1615
|
orchBatchState = freshOrchBatchState();
|
|
1046
1616
|
latestMonitorState = null;
|
|
1617
|
+
|
|
1618
|
+
// ── TP-040: Set launching phase synchronously ────────────
|
|
1619
|
+
// Same as /orch — mark as "launching" before setTimeout detach
|
|
1620
|
+
// so commands issued immediately see an active batch.
|
|
1621
|
+
orchBatchState.phase = "launching";
|
|
1622
|
+
orchBatchState.startedAt = Date.now();
|
|
1047
1623
|
updateOrchWidget();
|
|
1048
1624
|
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1625
|
+
// ── TP-040: Non-blocking resume launch ───────────────────
|
|
1626
|
+
// Same fire-and-forget pattern as /orch — see startBatchAsync.
|
|
1627
|
+
startBatchAsync(
|
|
1628
|
+
() => resumeOrchBatch(
|
|
1629
|
+
orchConfig,
|
|
1630
|
+
runnerConfig,
|
|
1631
|
+
execCtx!.repoRoot,
|
|
1632
|
+
orchBatchState,
|
|
1633
|
+
(message, level) => {
|
|
1634
|
+
ctx.ui.notify(message, level);
|
|
1635
|
+
updateOrchWidget();
|
|
1636
|
+
},
|
|
1637
|
+
(monState: MonitorState) => {
|
|
1638
|
+
latestMonitorState = monState;
|
|
1639
|
+
updateOrchWidget();
|
|
1640
|
+
},
|
|
1641
|
+
execCtx!.workspaceConfig,
|
|
1642
|
+
execCtx!.workspaceRoot,
|
|
1643
|
+
execCtx!.pointer?.agentRoot,
|
|
1644
|
+
parsed.force,
|
|
1645
|
+
),
|
|
1053
1646
|
orchBatchState,
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1647
|
+
ctx,
|
|
1648
|
+
updateOrchWidget,
|
|
1649
|
+
// TP-043: Deferred supervisor deactivation (R002-1, parity with /orch).
|
|
1650
|
+
// Only trigger integration on completed batches.
|
|
1651
|
+
// TP-043 Step 2: Batch summary on all terminal paths.
|
|
1652
|
+
() => {
|
|
1653
|
+
const mode = orchConfig.orchestrator.integration;
|
|
1654
|
+
const opId = resolveOperatorId(orchConfig);
|
|
1655
|
+
const sDeps: SummaryDeps = {
|
|
1656
|
+
opId,
|
|
1657
|
+
diagnostics: orchBatchState.diagnostics ?? null,
|
|
1658
|
+
mergeResults: (orchBatchState.mergeResults || []).map(mr => ({
|
|
1659
|
+
waveIndex: mr.waveIndex,
|
|
1660
|
+
status: mr.status,
|
|
1661
|
+
failedLane: mr.failedLane,
|
|
1662
|
+
failureReason: mr.failureReason,
|
|
1663
|
+
})),
|
|
1664
|
+
};
|
|
1665
|
+
if (
|
|
1666
|
+
orchBatchState.phase === "completed" &&
|
|
1667
|
+
(mode === "supervised" || mode === "auto")
|
|
1668
|
+
) {
|
|
1669
|
+
triggerSupervisorIntegration(
|
|
1670
|
+
pi,
|
|
1671
|
+
supervisorState,
|
|
1672
|
+
orchBatchState,
|
|
1673
|
+
mode,
|
|
1674
|
+
execCtx!.repoRoot,
|
|
1675
|
+
buildIntegrationExecutor(execCtx!.repoRoot),
|
|
1676
|
+
buildCiDeps(execCtx!.repoRoot),
|
|
1677
|
+
sDeps,
|
|
1678
|
+
);
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (
|
|
1682
|
+
(mode === "supervised" || mode === "auto") &&
|
|
1683
|
+
orchBatchState.phase !== "completed"
|
|
1684
|
+
) {
|
|
1685
|
+
pi.sendMessage(
|
|
1686
|
+
{
|
|
1687
|
+
customType: "supervisor-integration-skipped",
|
|
1688
|
+
content: [{
|
|
1689
|
+
type: "text",
|
|
1690
|
+
text:
|
|
1691
|
+
`📋 **Batch ended** (phase: ${orchBatchState.phase}). ` +
|
|
1692
|
+
`Integration skipped — only completed batches are eligible.\n` +
|
|
1693
|
+
`Use \`/orch-resume\` to continue or \`/orch-integrate\` manually after resolving issues.`,
|
|
1694
|
+
}],
|
|
1695
|
+
display: `Integration skipped — batch ${orchBatchState.phase}`,
|
|
1696
|
+
},
|
|
1697
|
+
{ triggerTurn: false },
|
|
1698
|
+
);
|
|
1699
|
+
}
|
|
1700
|
+
// TP-043: Generate summary before deactivation
|
|
1701
|
+
presentBatchSummary(pi, orchBatchState, execCtx!.workspaceRoot, opId, orchBatchState.diagnostics, sDeps.mergeResults);
|
|
1702
|
+
deactivateSupervisor(pi, supervisorState);
|
|
1061
1703
|
},
|
|
1062
|
-
execCtx!.workspaceConfig,
|
|
1063
|
-
execCtx!.workspaceRoot,
|
|
1064
|
-
execCtx!.pointer?.agentRoot,
|
|
1065
|
-
parsed.force,
|
|
1066
1704
|
);
|
|
1067
1705
|
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1706
|
+
// ── TP-041: Activate supervisor agent on resume ──────────
|
|
1707
|
+
// supervisorConfig is loaded at session_start from unified config.
|
|
1708
|
+
// Uses workspaceRoot so supervisor state root matches engine (R006-1).
|
|
1709
|
+
activateSupervisor(
|
|
1710
|
+
pi,
|
|
1711
|
+
supervisorState,
|
|
1712
|
+
orchBatchState,
|
|
1713
|
+
orchConfig,
|
|
1714
|
+
supervisorConfig,
|
|
1715
|
+
execCtx!.workspaceRoot,
|
|
1716
|
+
ctx,
|
|
1717
|
+
);
|
|
1070
1718
|
},
|
|
1071
1719
|
});
|
|
1072
1720
|
|
|
@@ -1182,6 +1830,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1182
1830
|
}
|
|
1183
1831
|
|
|
1184
1832
|
// ── Step 6: Clean up batch state ────────────────────────
|
|
1833
|
+
// TP-041: Deactivate supervisor on abort
|
|
1834
|
+
deactivateSupervisor(pi, supervisorState);
|
|
1835
|
+
|
|
1185
1836
|
try {
|
|
1186
1837
|
orchBatchState.phase = "stopped";
|
|
1187
1838
|
orchBatchState.endedAt = Date.now();
|
|
@@ -1306,6 +1957,136 @@ export default function (pi: ExtensionAPI) {
|
|
|
1306
1957
|
},
|
|
1307
1958
|
});
|
|
1308
1959
|
|
|
1960
|
+
// ── TP-041 Step 2: /orch-takeover — force supervisor takeover ────
|
|
1961
|
+
pi.registerCommand("orch-takeover", {
|
|
1962
|
+
description: "Force takeover supervisor from another session: /orch-takeover",
|
|
1963
|
+
handler: async (_args, ctx) => {
|
|
1964
|
+
// Use workspaceRoot so supervisor state root matches engine (R006-1).
|
|
1965
|
+
const stateRoot = execCtx.workspaceRoot;
|
|
1966
|
+
|
|
1967
|
+
// If this session already owns the supervisor, nothing to do.
|
|
1968
|
+
if (supervisorState.active) {
|
|
1969
|
+
ctx.ui.notify(
|
|
1970
|
+
"✅ This session is already the active supervisor.\n\n" +
|
|
1971
|
+
` Session: ${supervisorState.lockSessionId}\n` +
|
|
1972
|
+
` Batch: ${supervisorState.batchId || orchBatchState.batchId}`,
|
|
1973
|
+
"info",
|
|
1974
|
+
);
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
// Re-check lock state (may have changed since session_start).
|
|
1979
|
+
const lockResult = checkSupervisorLockOnStartup(stateRoot, loadBatchState);
|
|
1980
|
+
|
|
1981
|
+
switch (lockResult.status) {
|
|
1982
|
+
case "no-active-batch":
|
|
1983
|
+
ctx.ui.notify(
|
|
1984
|
+
"No active batch to supervise.\n\nStart a batch with /orch first.",
|
|
1985
|
+
"info",
|
|
1986
|
+
);
|
|
1987
|
+
return;
|
|
1988
|
+
|
|
1989
|
+
case "no-lockfile":
|
|
1990
|
+
case "corrupt":
|
|
1991
|
+
case "stale": {
|
|
1992
|
+
// No live lock to take over — just activate normally.
|
|
1993
|
+
const batchState = lockResult.batchState;
|
|
1994
|
+
const summary = buildTakeoverSummary(stateRoot, batchState);
|
|
1995
|
+
const reason =
|
|
1996
|
+
lockResult.status === "stale"
|
|
1997
|
+
? (isProcessAlive(lockResult.lock.pid)
|
|
1998
|
+
? `Previous supervisor (PID ${lockResult.lock.pid}) has a stale heartbeat (last: ${lockResult.lock.heartbeat}).`
|
|
1999
|
+
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`)
|
|
2000
|
+
: lockResult.status === "corrupt"
|
|
2001
|
+
? "Found a corrupt supervisor lockfile."
|
|
2002
|
+
: "No supervisor lockfile found.";
|
|
2003
|
+
|
|
2004
|
+
ctx.ui.notify(
|
|
2005
|
+
`🔄 **${reason}** Activating supervisor.\n\n` + summary,
|
|
2006
|
+
"info",
|
|
2007
|
+
);
|
|
2008
|
+
|
|
2009
|
+
// Populate orchBatchState from persisted state
|
|
2010
|
+
orchBatchState.batchId = batchState.batchId;
|
|
2011
|
+
orchBatchState.phase = batchState.phase as typeof orchBatchState.phase;
|
|
2012
|
+
orchBatchState.baseBranch = batchState.baseBranch;
|
|
2013
|
+
orchBatchState.orchBranch = batchState.orchBranch ?? "";
|
|
2014
|
+
orchBatchState.currentWaveIndex = batchState.currentWaveIndex;
|
|
2015
|
+
orchBatchState.totalWaves = batchState.wavePlan?.length ?? batchState.totalWaves ?? 0;
|
|
2016
|
+
orchBatchState.totalTasks = batchState.totalTasks ?? 0;
|
|
2017
|
+
orchBatchState.succeededTasks = batchState.succeededTasks ?? 0;
|
|
2018
|
+
orchBatchState.failedTasks = batchState.failedTasks ?? 0;
|
|
2019
|
+
orchBatchState.skippedTasks = batchState.skippedTasks ?? 0;
|
|
2020
|
+
orchBatchState.blockedTasks = batchState.blockedTasks ?? 0;
|
|
2021
|
+
orchBatchState.startedAt = batchState.startedAt;
|
|
2022
|
+
orchBatchState.endedAt = batchState.endedAt ?? null;
|
|
2023
|
+
|
|
2024
|
+
await activateSupervisor(
|
|
2025
|
+
pi,
|
|
2026
|
+
supervisorState,
|
|
2027
|
+
orchBatchState,
|
|
2028
|
+
orchConfig,
|
|
2029
|
+
supervisorConfig,
|
|
2030
|
+
stateRoot,
|
|
2031
|
+
ctx,
|
|
2032
|
+
);
|
|
2033
|
+
|
|
2034
|
+
updateOrchWidget();
|
|
2035
|
+
break;
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2038
|
+
case "live": {
|
|
2039
|
+
// Force takeover from another live session.
|
|
2040
|
+
// Write a new lock — the old session's heartbeat will detect
|
|
2041
|
+
// the sessionId mismatch and yield gracefully.
|
|
2042
|
+
const lock = lockResult.lock;
|
|
2043
|
+
const batchState = lockResult.batchState;
|
|
2044
|
+
const summary = buildTakeoverSummary(stateRoot, batchState);
|
|
2045
|
+
|
|
2046
|
+
ctx.ui.notify(
|
|
2047
|
+
`⚡ **Forcing supervisor takeover from PID ${lock.pid}.**\n\n` +
|
|
2048
|
+
` Previous session: ${lock.sessionId}\n` +
|
|
2049
|
+
` Previous heartbeat: ${lock.heartbeat}\n\n` +
|
|
2050
|
+
`The other session will yield on its next heartbeat check.\n\n` +
|
|
2051
|
+
summary,
|
|
2052
|
+
"warning",
|
|
2053
|
+
);
|
|
2054
|
+
|
|
2055
|
+
// Populate orchBatchState from persisted state
|
|
2056
|
+
orchBatchState.batchId = batchState.batchId;
|
|
2057
|
+
orchBatchState.phase = batchState.phase as typeof orchBatchState.phase;
|
|
2058
|
+
orchBatchState.baseBranch = batchState.baseBranch;
|
|
2059
|
+
orchBatchState.orchBranch = batchState.orchBranch ?? "";
|
|
2060
|
+
orchBatchState.currentWaveIndex = batchState.currentWaveIndex;
|
|
2061
|
+
orchBatchState.totalWaves = batchState.wavePlan?.length ?? batchState.totalWaves ?? 0;
|
|
2062
|
+
orchBatchState.totalTasks = batchState.totalTasks ?? 0;
|
|
2063
|
+
orchBatchState.succeededTasks = batchState.succeededTasks ?? 0;
|
|
2064
|
+
orchBatchState.failedTasks = batchState.failedTasks ?? 0;
|
|
2065
|
+
orchBatchState.skippedTasks = batchState.skippedTasks ?? 0;
|
|
2066
|
+
orchBatchState.blockedTasks = batchState.blockedTasks ?? 0;
|
|
2067
|
+
orchBatchState.startedAt = batchState.startedAt;
|
|
2068
|
+
orchBatchState.endedAt = batchState.endedAt ?? null;
|
|
2069
|
+
|
|
2070
|
+
// activateSupervisor writes a new lock with this session's ID.
|
|
2071
|
+
// The old session's heartbeat timer will detect the sessionId
|
|
2072
|
+
// mismatch and deactivate automatically.
|
|
2073
|
+
await activateSupervisor(
|
|
2074
|
+
pi,
|
|
2075
|
+
supervisorState,
|
|
2076
|
+
orchBatchState,
|
|
2077
|
+
orchConfig,
|
|
2078
|
+
supervisorConfig,
|
|
2079
|
+
stateRoot,
|
|
2080
|
+
ctx,
|
|
2081
|
+
);
|
|
2082
|
+
|
|
2083
|
+
updateOrchWidget();
|
|
2084
|
+
break;
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
},
|
|
2088
|
+
});
|
|
2089
|
+
|
|
1309
2090
|
pi.registerCommand("orch-integrate", {
|
|
1310
2091
|
description: "Integrate completed orch batch into your working branch",
|
|
1311
2092
|
handler: async (args, ctx) => {
|
|
@@ -1512,6 +2293,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1512
2293
|
const summary = integrationSummary + "\n" + cleanupResult.report;
|
|
1513
2294
|
|
|
1514
2295
|
ctx.ui.notify(summary, cleanupResult.notifyLevel);
|
|
2296
|
+
|
|
2297
|
+
// TP-043 R004: If supervisor has a deferred batch summary (supervised mode),
|
|
2298
|
+
// present it now that integration is complete, then deactivate.
|
|
2299
|
+
if (supervisorState.active && supervisorState.pendingSummaryDeps) {
|
|
2300
|
+
const deps = supervisorState.pendingSummaryDeps;
|
|
2301
|
+
supervisorState.pendingSummaryDeps = null;
|
|
2302
|
+
if (supervisorState.batchStateRef && supervisorState.stateRoot) {
|
|
2303
|
+
presentBatchSummary(pi, supervisorState.batchStateRef, supervisorState.stateRoot, deps.opId, deps.diagnostics, deps.mergeResults);
|
|
2304
|
+
}
|
|
2305
|
+
deactivateSupervisor(pi, supervisorState);
|
|
2306
|
+
}
|
|
1515
2307
|
},
|
|
1516
2308
|
});
|
|
1517
2309
|
|
|
@@ -1565,6 +2357,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
1565
2357
|
orchConfig = execCtx.orchestratorConfig;
|
|
1566
2358
|
runnerConfig = execCtx.taskRunnerConfig;
|
|
1567
2359
|
|
|
2360
|
+
// TP-041: Load supervisor config from unified config.
|
|
2361
|
+
// Uses execCtx.repoRoot (not ctx.cwd) for consistency with the
|
|
2362
|
+
// established pattern — all config loading after buildExecutionContext
|
|
2363
|
+
// uses the resolved execution context paths.
|
|
2364
|
+
try {
|
|
2365
|
+
supervisorConfig = loadSupervisorConfig(
|
|
2366
|
+
execCtx.repoRoot,
|
|
2367
|
+
execCtx.pointer?.configRoot,
|
|
2368
|
+
);
|
|
2369
|
+
} catch {
|
|
2370
|
+
// Non-fatal — use defaults if supervisor config fails to load
|
|
2371
|
+
supervisorConfig = { ...DEFAULT_SUPERVISOR_CONFIG };
|
|
2372
|
+
}
|
|
2373
|
+
|
|
1568
2374
|
// Set status line
|
|
1569
2375
|
const areaCount = Object.keys(runnerConfig.task_areas).length;
|
|
1570
2376
|
const modeLabel = execCtx.mode === "workspace" ? "workspace" : "repo";
|
|
@@ -1576,6 +2382,109 @@ export default function (pi: ExtensionAPI) {
|
|
|
1576
2382
|
// Register initial dashboard widget (idle state)
|
|
1577
2383
|
updateOrchWidget();
|
|
1578
2384
|
|
|
2385
|
+
// ── TP-041 Step 2: Supervisor startup gate ───────────────────
|
|
2386
|
+
// Check for an active batch with an existing lockfile. This covers
|
|
2387
|
+
// session reconnection scenarios (pi restarted while a batch runs
|
|
2388
|
+
// in tmux lanes) and crashed supervisor recovery.
|
|
2389
|
+
// Uses workspaceRoot so supervisor state root matches engine (R006-1).
|
|
2390
|
+
{
|
|
2391
|
+
const stateRoot = execCtx.workspaceRoot;
|
|
2392
|
+
const lockResult = checkSupervisorLockOnStartup(stateRoot, loadBatchState);
|
|
2393
|
+
|
|
2394
|
+
switch (lockResult.status) {
|
|
2395
|
+
case "no-active-batch":
|
|
2396
|
+
// Nothing to do — normal startup
|
|
2397
|
+
break;
|
|
2398
|
+
|
|
2399
|
+
case "no-lockfile":
|
|
2400
|
+
case "corrupt":
|
|
2401
|
+
case "stale": {
|
|
2402
|
+
// Become the supervisor for the existing batch.
|
|
2403
|
+
// Stale = previous supervisor crashed (pid dead or heartbeat expired).
|
|
2404
|
+
// Corrupt = lockfile malformed (treat as stale per R003).
|
|
2405
|
+
// No lockfile = active batch without a supervisor (e.g., engine running
|
|
2406
|
+
// from a previous /orch that didn't have supervisor support yet).
|
|
2407
|
+
const batchState = lockResult.batchState;
|
|
2408
|
+
const summary = buildTakeoverSummary(stateRoot, batchState);
|
|
2409
|
+
const reason =
|
|
2410
|
+
lockResult.status === "stale"
|
|
2411
|
+
? (isProcessAlive(lockResult.lock.pid)
|
|
2412
|
+
? `Previous supervisor (PID ${lockResult.lock.pid}) has a stale heartbeat (last: ${lockResult.lock.heartbeat}). Process may be hung.`
|
|
2413
|
+
: `Previous supervisor (PID ${lockResult.lock.pid}) process is dead.`)
|
|
2414
|
+
: lockResult.status === "corrupt"
|
|
2415
|
+
? "Found a corrupt supervisor lockfile (treating as stale)."
|
|
2416
|
+
: "No supervisor lockfile found for the active batch.";
|
|
2417
|
+
|
|
2418
|
+
ctx.ui.notify(
|
|
2419
|
+
`🔄 **Active batch detected — ${reason}**\n\n` +
|
|
2420
|
+
`Taking over supervisor duties for batch ${batchState.batchId}.\n\n` +
|
|
2421
|
+
summary,
|
|
2422
|
+
"info",
|
|
2423
|
+
);
|
|
2424
|
+
|
|
2425
|
+
// Populate orchBatchState from persisted state for the supervisor
|
|
2426
|
+
// prompt rebuild. We copy the key fields used by the system prompt.
|
|
2427
|
+
orchBatchState.batchId = batchState.batchId;
|
|
2428
|
+
orchBatchState.phase = batchState.phase as typeof orchBatchState.phase;
|
|
2429
|
+
orchBatchState.baseBranch = batchState.baseBranch;
|
|
2430
|
+
orchBatchState.orchBranch = batchState.orchBranch ?? "";
|
|
2431
|
+
orchBatchState.currentWaveIndex = batchState.currentWaveIndex;
|
|
2432
|
+
orchBatchState.totalWaves = batchState.wavePlan?.length ?? batchState.totalWaves ?? 0;
|
|
2433
|
+
orchBatchState.totalTasks = batchState.totalTasks ?? 0;
|
|
2434
|
+
orchBatchState.succeededTasks = batchState.succeededTasks ?? 0;
|
|
2435
|
+
orchBatchState.failedTasks = batchState.failedTasks ?? 0;
|
|
2436
|
+
orchBatchState.skippedTasks = batchState.skippedTasks ?? 0;
|
|
2437
|
+
orchBatchState.blockedTasks = batchState.blockedTasks ?? 0;
|
|
2438
|
+
orchBatchState.startedAt = batchState.startedAt;
|
|
2439
|
+
orchBatchState.endedAt = batchState.endedAt ?? null;
|
|
2440
|
+
|
|
2441
|
+
// Activate supervisor with rehydration context.
|
|
2442
|
+
// activateSupervisor writes the lockfile and starts heartbeat.
|
|
2443
|
+
activateSupervisor(
|
|
2444
|
+
pi,
|
|
2445
|
+
supervisorState,
|
|
2446
|
+
orchBatchState,
|
|
2447
|
+
orchConfig,
|
|
2448
|
+
supervisorConfig,
|
|
2449
|
+
stateRoot,
|
|
2450
|
+
ctx,
|
|
2451
|
+
);
|
|
2452
|
+
|
|
2453
|
+
updateOrchWidget();
|
|
2454
|
+
break;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
case "live": {
|
|
2458
|
+
// Another supervisor is actively running (pid alive, heartbeat fresh).
|
|
2459
|
+
// Warn the operator and offer force takeover via /orch-takeover.
|
|
2460
|
+
const lock = lockResult.lock;
|
|
2461
|
+
const batchState = lockResult.batchState;
|
|
2462
|
+
ctx.ui.notify(
|
|
2463
|
+
`⚠️ **Another supervisor is already monitoring batch ${batchState.batchId}.**\n\n` +
|
|
2464
|
+
` PID: ${lock.pid}\n` +
|
|
2465
|
+
` Session: ${lock.sessionId}\n` +
|
|
2466
|
+
` Started: ${lock.startedAt}\n` +
|
|
2467
|
+
` Last heartbeat: ${lock.heartbeat}\n\n` +
|
|
2468
|
+
`To force takeover, run \`/orch-takeover\`.\n` +
|
|
2469
|
+
`The other session will yield on its next heartbeat.\n\n` +
|
|
2470
|
+
`Otherwise, use the other terminal or the dashboard to monitor the batch.`,
|
|
2471
|
+
"warning",
|
|
2472
|
+
);
|
|
2473
|
+
|
|
2474
|
+
// Store the live lock info so the /orch handler can detect it
|
|
2475
|
+
// (preventing a second /orch from starting a concurrent batch).
|
|
2476
|
+
orchBatchState.batchId = batchState.batchId;
|
|
2477
|
+
orchBatchState.phase = batchState.phase as typeof orchBatchState.phase;
|
|
2478
|
+
orchBatchState.baseBranch = batchState.baseBranch;
|
|
2479
|
+
orchBatchState.orchBranch = batchState.orchBranch ?? "";
|
|
2480
|
+
orchBatchState.startedAt = batchState.startedAt;
|
|
2481
|
+
|
|
2482
|
+
updateOrchWidget();
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
|
|
1579
2488
|
// Notify user of available commands
|
|
1580
2489
|
ctx.ui.notify(
|
|
1581
2490
|
"Task Orchestrator ready\n\n" +
|
|
@@ -1588,6 +2497,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1588
2497
|
"/orch-plan <areas|all> Preview execution plan\n" +
|
|
1589
2498
|
"/orch-deps <areas|all> Show dependency graph\n" +
|
|
1590
2499
|
"/orch-sessions List TMUX sessions\n" +
|
|
2500
|
+
"/orch-takeover Force supervisor takeover\n" +
|
|
1591
2501
|
"/orch-integrate Integrate orch branch into working branch",
|
|
1592
2502
|
"info",
|
|
1593
2503
|
);
|
|
@@ -1595,6 +2505,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
1595
2505
|
// Check for taskplane updates (non-blocking)
|
|
1596
2506
|
checkForUpdate(ctx);
|
|
1597
2507
|
});
|
|
2508
|
+
|
|
2509
|
+
// ── Session shutdown cleanup ─────────────────────────────────────
|
|
2510
|
+
// Ensure supervisor lockfile/heartbeat are cleaned up on normal session exit.
|
|
2511
|
+
// This avoids leaving a live-looking lock when the process exits cleanly.
|
|
2512
|
+
pi.on("session_end", async () => {
|
|
2513
|
+
try {
|
|
2514
|
+
await deactivateSupervisor(pi, supervisorState);
|
|
2515
|
+
} catch {
|
|
2516
|
+
// Best effort only — session is already ending.
|
|
2517
|
+
}
|
|
2518
|
+
});
|
|
1598
2519
|
}
|
|
1599
2520
|
|
|
1600
2521
|
// ── Update Check ─────────────────────────────────────────────────────
|