taskplane 0.23.16 → 0.24.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.
@@ -56,11 +56,13 @@ import type {
56
56
  * - CONFIG_JSON_MALFORMED: File exists but is not valid JSON
57
57
  * - CONFIG_VERSION_UNSUPPORTED: configVersion is not supported by this version
58
58
  * - CONFIG_VERSION_MISSING: configVersion field is missing from JSON
59
+ * - CONFIG_LEGACY_FIELD: removed TMUX-era field/value detected; migration required
59
60
  */
60
61
  export type ConfigLoadErrorCode =
61
62
  | "CONFIG_JSON_MALFORMED"
62
63
  | "CONFIG_VERSION_UNSUPPORTED"
63
- | "CONFIG_VERSION_MISSING";
64
+ | "CONFIG_VERSION_MISSING"
65
+ | "CONFIG_LEGACY_FIELD";
64
66
 
65
67
  export class ConfigLoadError extends Error {
66
68
  code: ConfigLoadErrorCode;
@@ -110,6 +112,61 @@ function deepMerge<T extends Record<string, any>>(target: T, source: Record<stri
110
112
  return target;
111
113
  }
112
114
 
115
+ function hasOwn(obj: unknown, key: string): boolean {
116
+ return !!obj && typeof obj === "object" && Object.prototype.hasOwnProperty.call(obj, key);
117
+ }
118
+
119
+ function throwLegacyFieldError(fieldPath: string, source: string, fixHint: string): never {
120
+ throw new ConfigLoadError(
121
+ "CONFIG_LEGACY_FIELD",
122
+ `[taskplane] ${source}: "${fieldPath}" is no longer supported under Runtime V2. ${fixHint}`,
123
+ );
124
+ }
125
+
126
+ function assertNoLegacyTmuxProjectConfig(config: TaskplaneConfig, source: string): void {
127
+ const orchestratorCore = config.orchestrator?.orchestrator as Record<string, unknown> | undefined;
128
+ if (hasOwn(orchestratorCore, "tmuxPrefix")) {
129
+ throwLegacyFieldError(
130
+ "orchestrator.orchestrator.tmuxPrefix",
131
+ source,
132
+ "Use \"orchestrator.orchestrator.sessionPrefix\" instead.",
133
+ );
134
+ }
135
+ if (orchestratorCore?.spawnMode === "tmux") {
136
+ throwLegacyFieldError(
137
+ "orchestrator.orchestrator.spawnMode",
138
+ source,
139
+ "Use \"subprocess\" instead.",
140
+ );
141
+ }
142
+
143
+ const workerConfig = config.taskRunner?.worker as Record<string, unknown> | undefined;
144
+ if (workerConfig?.spawnMode === "tmux") {
145
+ throwLegacyFieldError(
146
+ "taskRunner.worker.spawnMode",
147
+ source,
148
+ "Use \"subprocess\" or remove the field to inherit defaults.",
149
+ );
150
+ }
151
+ }
152
+
153
+ function assertNoLegacyTmuxUserPreferences(raw: Record<string, any>, prefsPath: string): void {
154
+ if (hasOwn(raw, "tmuxPrefix")) {
155
+ throwLegacyFieldError(
156
+ "tmuxPrefix",
157
+ `user preferences (${prefsPath})`,
158
+ "Rename it to \"sessionPrefix\".",
159
+ );
160
+ }
161
+ if (raw.spawnMode === "tmux") {
162
+ throwLegacyFieldError(
163
+ "spawnMode",
164
+ `user preferences (${prefsPath})`,
165
+ "Set it to \"subprocess\".",
166
+ );
167
+ }
168
+ }
169
+
113
170
 
114
171
  // ── YAML snake_case → camelCase Mapping ──────────────────────────────
115
172
 
@@ -600,19 +657,25 @@ export function loadUserPreferences(): UserPreferences {
600
657
  }
601
658
 
602
659
  // Extract only allowlisted fields — unknown keys are ignored
603
- return extractAllowlistedPreferences(parsed);
660
+ return extractAllowlistedPreferences(parsed, prefsPath);
604
661
  }
605
662
 
606
663
  /**
607
664
  * Extract only recognized/allowlisted fields from a raw parsed object.
608
665
  * Unknown keys are silently dropped — this is the Layer 2 boundary guardrail.
609
666
  */
610
- function extractAllowlistedPreferences(raw: Record<string, any>): UserPreferences {
667
+ function extractAllowlistedPreferences(raw: Record<string, any>, prefsPath: string): UserPreferences {
668
+ assertNoLegacyTmuxUserPreferences(raw, prefsPath);
669
+
611
670
  const prefs: UserPreferences = {};
612
671
 
613
672
  if (typeof raw.operatorId === "string") prefs.operatorId = raw.operatorId;
614
- if (typeof raw.tmuxPrefix === "string") prefs.tmuxPrefix = raw.tmuxPrefix;
615
- if (raw.spawnMode === "tmux" || raw.spawnMode === "subprocess") prefs.spawnMode = raw.spawnMode;
673
+ if (typeof raw.sessionPrefix === "string") {
674
+ prefs.sessionPrefix = raw.sessionPrefix;
675
+ }
676
+ if (raw.spawnMode === "subprocess") {
677
+ prefs.spawnMode = "subprocess";
678
+ }
616
679
  if (typeof raw.workerModel === "string") prefs.workerModel = raw.workerModel;
617
680
  if (typeof raw.reviewerModel === "string") prefs.reviewerModel = raw.reviewerModel;
618
681
  if (typeof raw.mergeModel === "string") prefs.mergeModel = raw.mergeModel;
@@ -638,7 +701,7 @@ function extractAllowlistedPreferences(raw: Record<string, any>): UserPreference
638
701
  *
639
702
  * Mapping table:
640
703
  * prefs.operatorId → config.orchestrator.orchestrator.operatorId
641
- * prefs.tmuxPrefix → config.orchestrator.orchestrator.tmuxPrefix
704
+ * prefs.sessionPrefix → config.orchestrator.orchestrator.sessionPrefix
642
705
  * prefs.spawnMode → config.orchestrator.orchestrator.spawnMode
643
706
  * prefs.workerModel → config.taskRunner.worker.model
644
707
  * prefs.reviewerModel → config.taskRunner.reviewer.model
@@ -653,7 +716,7 @@ export function applyUserPreferences(config: TaskplaneConfig, prefs: UserPrefere
653
716
  };
654
717
 
655
718
  applyStr(prefs.operatorId, (v) => { config.orchestrator.orchestrator.operatorId = v; });
656
- applyStr(prefs.tmuxPrefix, (v) => { config.orchestrator.orchestrator.tmuxPrefix = v; });
719
+ applyStr(prefs.sessionPrefix, (v) => { config.orchestrator.orchestrator.sessionPrefix = v; });
657
720
  applyStr(prefs.workerModel, (v) => { config.taskRunner.worker.model = v; });
658
721
  applyStr(prefs.reviewerModel, (v) => { config.taskRunner.reviewer.model = v; });
659
722
  applyStr(prefs.mergeModel, (v) => { config.orchestrator.merge.model = v; });
@@ -661,6 +724,13 @@ export function applyUserPreferences(config: TaskplaneConfig, prefs: UserPrefere
661
724
 
662
725
  // spawnMode: enum — apply if defined (not a string-empty check)
663
726
  if (prefs.spawnMode !== undefined) {
727
+ if (prefs.spawnMode === "tmux") {
728
+ throwLegacyFieldError(
729
+ "spawnMode",
730
+ "user preferences (runtime)",
731
+ "Set it to \"subprocess\".",
732
+ );
733
+ }
664
734
  config.orchestrator.orchestrator.spawnMode = prefs.spawnMode;
665
735
  }
666
736
 
@@ -670,7 +740,6 @@ export function applyUserPreferences(config: TaskplaneConfig, prefs: UserPrefere
670
740
  return config;
671
741
  }
672
742
 
673
-
674
743
  // ── Unified Loader ───────────────────────────────────────────────────
675
744
 
676
745
  /**
@@ -784,9 +853,12 @@ export function loadProjectConfig(cwd: string, pointerConfigRoot?: string): Task
784
853
  };
785
854
  }
786
855
 
856
+ assertNoLegacyTmuxProjectConfig(config, `project config (${configRoot})`);
857
+
787
858
  // Layer 2: User preferences (allowlisted fields only)
788
859
  const prefs = loadUserPreferences();
789
860
  applyUserPreferences(config, prefs);
861
+ assertNoLegacyTmuxProjectConfig(config, `project config (${configRoot}) after preferences merge`);
790
862
 
791
863
  return config;
792
864
  }
@@ -810,6 +882,7 @@ export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): Taskp
810
882
  // Try JSON first
811
883
  const jsonConfig = loadJsonConfig(configRoot);
812
884
  if (jsonConfig !== null) {
885
+ assertNoLegacyTmuxProjectConfig(jsonConfig, `project config (${configRoot})`);
813
886
  return jsonConfig;
814
887
  }
815
888
 
@@ -817,12 +890,14 @@ export function loadLayer1Config(cwd: string, pointerConfigRoot?: string): Taskp
817
890
  const taskRunner = loadTaskRunnerYaml(configRoot);
818
891
  const orchestrator = loadOrchestratorYaml(configRoot);
819
892
  const workspace = loadWorkspaceYaml(configRoot);
820
- return {
893
+ const config: TaskplaneConfig = {
821
894
  configVersion: CONFIG_VERSION,
822
895
  taskRunner,
823
896
  orchestrator,
824
897
  ...(workspace ? { workspace } : {}),
825
898
  };
899
+ assertNoLegacyTmuxProjectConfig(config, `project config (${configRoot})`);
900
+ return config;
826
901
  }
827
902
 
828
903
 
@@ -847,7 +922,7 @@ export function toOrchestratorConfig(config: TaskplaneConfig): import("./types.t
847
922
  worktree_prefix: o.orchestrator.worktreePrefix,
848
923
  batch_id_format: o.orchestrator.batchIdFormat,
849
924
  spawn_mode: o.orchestrator.spawnMode,
850
- tmux_prefix: o.orchestrator.tmuxPrefix,
925
+ sessionPrefix: o.orchestrator.sessionPrefix,
851
926
  operator_id: o.orchestrator.operatorId,
852
927
  integration: o.orchestrator.integration,
853
928
  },
@@ -942,7 +1017,7 @@ export function toTaskConfig(config: TaskplaneConfig): {
942
1017
  standards: { docs: string[]; rules: string[] };
943
1018
  standards_overrides: Record<string, { docs?: string[]; rules?: string[] }>;
944
1019
  task_areas: Record<string, { path: string; [key: string]: any }>;
945
- worker: { model: string; tools: string; thinking: string; spawn_mode?: "subprocess" | "tmux" };
1020
+ worker: { model: string; tools: string; thinking: string; spawn_mode?: "subprocess" };
946
1021
  reviewer: { model: string; tools: string; thinking: string };
947
1022
  context: {
948
1023
  worker_context_window: number;
@@ -109,8 +109,8 @@ export interface WorkerConfig {
109
109
  tools: string;
110
110
  /** Thinking mode setting passed to worker agent */
111
111
  thinking: string;
112
- /** Optional spawn mode override for task-runner */
113
- spawnMode?: "subprocess" | "tmux";
112
+ /** Optional spawn mode override for task-runner (Runtime V2 subprocess-only). */
113
+ spawnMode?: "subprocess";
114
114
  }
115
115
 
116
116
  /** Reviewer agent configuration */
@@ -139,7 +139,7 @@ export interface ContextConfig {
139
139
  maxReviewCycles: number;
140
140
  /** Max no-progress iterations before marking failure */
141
141
  noProgressLimit: number;
142
- /** Optional per-worker wall-clock cap (minutes, used in tmux/orchestrated flows) */
142
+ /** Optional per-worker wall-clock cap (minutes, used in orchestrated flows) */
143
143
  maxWorkerMinutes?: number;
144
144
  }
145
145
 
@@ -260,10 +260,10 @@ export interface OrchestratorCoreConfig {
260
260
  worktreePrefix: string;
261
261
  /** Batch ID format used in logs/branch naming */
262
262
  batchIdFormat: "timestamp" | "sequential";
263
- /** How lane sessions are spawned */
264
- spawnMode: "tmux" | "subprocess";
265
- /** Prefix for orchestrator tmux sessions (tmux mode) */
266
- tmuxPrefix: string;
263
+ /** How lane sessions are spawned (Runtime V2 subprocess-only). */
264
+ spawnMode: "subprocess";
265
+ /** Prefix for orchestrator session naming */
266
+ sessionPrefix: string;
267
267
  /** Operator identifier. Auto-detected from OS username if empty */
268
268
  operatorId: string;
269
269
  /** How completed batches are integrated. manual = user runs /orch-integrate. supervised = supervisor proposes plan, asks confirmation. auto = supervisor executes without asking. */
@@ -498,7 +498,7 @@ export interface TaskplaneConfig {
498
498
  * | Preference field | Config path | Type |
499
499
  * |--------------------|--------------------------------------|---------|
500
500
  * | operatorId | orchestrator.orchestrator.operatorId | string |
501
- * | tmuxPrefix | orchestrator.orchestrator.tmuxPrefix | string |
501
+ * | sessionPrefix | orchestrator.orchestrator.sessionPrefix | string |
502
502
  * | spawnMode | orchestrator.orchestrator.spawnMode | string |
503
503
  * | workerModel | taskRunner.worker.model | string |
504
504
  * | reviewerModel | taskRunner.reviewer.model | string |
@@ -509,10 +509,10 @@ export interface TaskplaneConfig {
509
509
  export interface UserPreferences {
510
510
  /** Operator identifier (overrides orchestrator.orchestrator.operatorId) */
511
511
  operatorId?: string;
512
- /** TMUX session prefix (overrides orchestrator.orchestrator.tmuxPrefix) */
513
- tmuxPrefix?: string;
514
- /** Spawn mode override (overrides orchestrator.orchestrator.spawnMode) */
515
- spawnMode?: "tmux" | "subprocess";
512
+ /** Orchestrator session prefix (overrides orchestrator.orchestrator.sessionPrefix) */
513
+ sessionPrefix?: string;
514
+ /** Spawn mode override (overrides orchestrator.orchestrator.spawnMode). */
515
+ spawnMode?: "subprocess";
516
516
  /** Worker model override (overrides taskRunner.worker.model) */
517
517
  workerModel?: string;
518
518
  /** Reviewer model override (overrides taskRunner.reviewer.model) */
@@ -582,7 +582,7 @@ export const DEFAULT_ORCHESTRATOR_SECTION: OrchestratorSection = {
582
582
  worktreePrefix: "taskplane-wt",
583
583
  batchIdFormat: "timestamp",
584
584
  spawnMode: "subprocess",
585
- tmuxPrefix: "orch",
585
+ sessionPrefix: "orch",
586
586
  operatorId: "",
587
587
  integration: "manual",
588
588
  },
@@ -411,7 +411,7 @@ export function assembleDiagnosticInput(
411
411
  const record: PersistedTaskRecord = {
412
412
  taskId,
413
413
  laneNumber: lane?.laneNumber ?? 0,
414
- sessionName: outcome?.sessionName || lane?.tmuxSessionName || "",
414
+ sessionName: outcome?.sessionName || lane?.laneSessionId || "",
415
415
  status: outcome?.status ?? "pending",
416
416
  taskFolder: "",
417
417
  startedAt: outcome?.startTime ?? null,
@@ -49,9 +49,9 @@ export interface SessionTokenCounts {
49
49
  * | `context_overflow` | Hit context window limit (compactions + high ctx %) |
50
50
  * | `wall_clock_timeout` | Killed by task-runner's max_worker_minutes timer |
51
51
  * | `process_crash` | Non-zero exit code with no API error indicators |
52
- * | `session_vanished` | Tmux session disappeared without exit summary |
52
+ * | `session_vanished` | Session disappeared without exit summary |
53
53
  * | `stall_timeout` | No STATUS.md progress for stall_timeout minutes |
54
- * | `user_killed` | User manually killed the session (e.g., tmux kill) |
54
+ * | `user_killed` | User manually killed the session (e.g., forced process kill) |
55
55
  * | `unknown` | Could not determine cause |
56
56
  */
57
57
  export type ExitClassification =
@@ -157,7 +157,7 @@ export interface ExitSummary {
157
157
  * - `timerKilled`: true if task-runner's max_worker_minutes timer killed the session
158
158
  * - `contextKilled`: true if the task-runner explicitly killed the session due to context limit
159
159
  * - `stallDetected`: true if monitoring detected no STATUS.md progress
160
- * - `userKilled`: true if user manually killed the session (e.g., /orch-abort, tmux kill)
160
+ * - `userKilled`: true if user manually killed the session (e.g., /orch-abort, forced process kill)
161
161
  * - `contextPct`: estimated context utilization % (0-100), null if unknown
162
162
  *
163
163
  * Design: single structured input object (not positional args) for
@@ -6,19 +6,19 @@ import { existsSync, readdirSync, readFileSync, unlinkSync } from "fs";
6
6
  import { join, resolve } from "path";
7
7
 
8
8
  import { formatDiscoveryResults, runDiscovery } from "./discovery.ts";
9
- import { computeTransitiveDependents, execLog, executeLane, executeLaneV2, executeWave, tmuxKillSession } from "./execution.ts";
9
+ import { computeTransitiveDependents, execLog, executeLaneV2, executeWave, killV2LaneAgents } from "./execution.ts";
10
10
  import type { RuntimeBackend } from "./execution.ts";
11
11
  import type { MonitorUpdateCallback } from "./execution.ts";
12
12
  // classifyExit no longer called directly — Tier 0 uses exitDiagnostic.classification
13
13
  // from the diagnostic-reports pipeline (populated by assembleDiagnosticInput).
14
14
  import { getCurrentBranch, runGit } from "./git.ts";
15
- import { mergeWaveByRepo, MergeHealthMonitor } from "./merge.ts";
15
+ import { killAllMergeAgentsV2, mergeWaveByRepo, MergeHealthMonitor } from "./merge.ts";
16
16
  import { applyMergeRetryLoop, computeCleanupGatePolicy, computeMergeFailurePolicy, extractFailedRepoId, formatRepoMergeSummary, ORCH_MESSAGES } from "./messages.ts";
17
17
  import type { CleanupGateRepoFailure } from "./messages.ts";
18
18
  import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
19
19
  import { resolveOperatorId } from "./naming.ts";
20
20
  import { applyPartialProgressToOutcomes, buildTier0EventBase, deleteBatchState, emitEngineEvent, emitTier0Event, loadBatchHistory, loadBatchState, persistRuntimeState, saveBatchHistory, seedPendingOutcomesForAllocatedLanes, syncTaskOutcomesFromMonitor, upsertTaskOutcome } from "./persistence.ts";
21
- import { listOrchSessions } from "./sessions.ts";
21
+ import { readRegistrySnapshot, isTerminalStatus, isProcessAlive as registryIsProcessAlive } from "./process-registry.ts";
22
22
  import { buildBatchProgressSnapshot, buildEngineEventBase, defaultResilienceState, FATAL_DISCOVERY_CODES, generateBatchId, TIER0_RETRYABLE_CLASSIFICATIONS, TIER0_RETRY_BUDGETS, tier0ScopeKey, tier0WaveScopeKey } from "./types.ts";
23
23
  import type { AllocatedLane, AllocatedTask, BatchHistorySummary, BatchTaskSummary, BatchWaveSummary, DiscoveryResult, EngineEventCallback, EscalationContext, LaneExecutionResult, LaneTaskOutcome, MergeWaveResult, OrchBatchPhase, OrchBatchRuntimeState, OrchestratorConfig, SupervisorAlert, SupervisorAlertCallback, TaskRunnerConfig, Tier0EscalationPattern, Tier0RecoveryPattern, TokenCounts, WaveExecutionResult, WorkspaceConfig } from "./types.ts";
24
24
  import { buildDependencyGraph, computeWaves, resolveBaseBranch, resolveRepoRoot, validateGraph } from "./waves.ts";
@@ -278,8 +278,7 @@ async function attemptWorkerCrashRetry(
278
278
  // may be paused due to stop-wave policy, but Tier 0 retry should
279
279
  // attempt recovery before the stop decision takes effect (R002-4).
280
280
  const retryPauseSignal = { paused: false };
281
- const retryExecutor = (runtimeBackend === "v2") ? executeLaneV2 : executeLane;
282
- const retryResult = await retryExecutor(
281
+ const retryResult = await executeLaneV2(
283
282
  retryLane,
284
283
  orchConfig,
285
284
  repoRoot,
@@ -546,8 +545,7 @@ async function attemptModelFallbackRetry(
546
545
  // the task-runner to use the session model instead of configured model.
547
546
  // TP-089: Also include ORCH_BATCH_ID so mailbox steering works for retries.
548
547
  const modelFallbackEnv = { TASKPLANE_MODEL_FALLBACK: "1", ORCH_BATCH_ID: batchState.batchId };
549
- const retryExecutor = (runtimeBackend === "v2") ? executeLaneV2 : executeLane;
550
- const retryResult = await retryExecutor(
548
+ const retryResult = await executeLaneV2(
551
549
  retryLane,
552
550
  orchConfig,
553
551
  repoRoot,
@@ -2475,16 +2473,39 @@ export async function executeOrchBatch(
2475
2473
  if (preserveWorktreesForResume) {
2476
2474
  execLog("batch", batchState.batchId, "skipping final cleanup to preserve worktrees/branches for resume");
2477
2475
  } else {
2478
- // Kill any lingering lane tmux sessions BEFORE removing worktrees.
2479
- // On Windows, tmux sessions with cwd inside the worktree lock the
2480
- // directory, causing git worktree remove to fail.
2481
- const orchPrefix = orchConfig.orchestrator.tmux_prefix;
2482
- const lingering = listOrchSessions(orchPrefix, batchState);
2483
- if (lingering.length > 0) {
2484
- execLog("batch", batchState.batchId, `killing ${lingering.length} lingering tmux session(s) before cleanup`);
2485
- for (const sess of lingering) {
2486
- tmuxKillSession(sess.sessionName);
2476
+ // Kill lingering Runtime V2 agents BEFORE removing worktrees.
2477
+ // On Windows, lingering processes with cwd inside the worktree can lock
2478
+ // the directory and cause `git worktree remove` to fail.
2479
+ const lingeringLaneSessions = new Set<string>();
2480
+ const registry = readRegistrySnapshot(stateRoot, batchState.batchId);
2481
+ if (registry) {
2482
+ for (const manifest of Object.values(registry.agents)) {
2483
+ if (manifest.role !== "worker" && manifest.role !== "reviewer") continue;
2484
+ if (isTerminalStatus(manifest.status) || !registryIsProcessAlive(manifest.pid)) continue;
2485
+ lingeringLaneSessions.add(manifest.agentId.replace(/-(worker|reviewer)$/, ""));
2487
2486
  }
2487
+ }
2488
+
2489
+ let performedAgentCleanup = false;
2490
+ if (lingeringLaneSessions.size > 0) {
2491
+ execLog("batch", batchState.batchId, `killing ${lingeringLaneSessions.size} lingering lane agent session(s) before cleanup`);
2492
+ for (const sessionName of lingeringLaneSessions) {
2493
+ killV2LaneAgents(sessionName, {
2494
+ stateRoot,
2495
+ batchId: batchState.batchId,
2496
+ logContext: "batch",
2497
+ });
2498
+ }
2499
+ performedAgentCleanup = true;
2500
+ }
2501
+
2502
+ const killedMergeAgents = killAllMergeAgentsV2();
2503
+ if (killedMergeAgents > 0) {
2504
+ execLog("batch", batchState.batchId, `killed ${killedMergeAgents} lingering merge agent(s) before cleanup`);
2505
+ performedAgentCleanup = true;
2506
+ }
2507
+
2508
+ if (performedAgentCleanup) {
2488
2509
  sleepSync(1000); // Give OS time to release file locks
2489
2510
  }
2490
2511