taskplane 0.22.10 → 0.22.11

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.
@@ -640,11 +640,31 @@ function querySessionStats() {
640
640
 
641
641
  // ── Route RPC events ─────────────────────────────────────────────────
642
642
 
643
+ // Event types worth persisting to the sidecar JSONL.
644
+ // Streaming deltas (content_block_delta, content_block_start/stop, message_start,
645
+ // input_json_delta, etc.) are omitted — they're high-volume, large, and not used
646
+ // by the dashboard or telemetry consumers. A single merge agent can produce 42MB+
647
+ // of sidecar data from streaming deltas alone.
648
+ const SIDECAR_EVENT_TYPES = new Set([
649
+ "agent_start",
650
+ "agent_end",
651
+ "message_end",
652
+ "tool_execution_start",
653
+ "tool_execution_end",
654
+ "tool_execution_update",
655
+ "auto_retry_start",
656
+ "auto_retry_end",
657
+ "auto_compaction_start",
658
+ "response",
659
+ ]);
660
+
643
661
  function handleEvent(event) {
644
662
  if (!event || !event.type) return;
645
663
 
646
- // Write ALL events to sidecar (redacted)
647
- writeSidecarEvent(args.sidecarPath, event);
664
+ // Write only telemetry-relevant events to sidecar (redacted)
665
+ if (SIDECAR_EVENT_TYPES.has(event.type)) {
666
+ writeSidecarEvent(args.sidecarPath, event);
667
+ }
648
668
 
649
669
  // Delegate state mutation to the extracted (testable) accumulator
650
670
  applyEvent(state, event);
@@ -1121,7 +1121,7 @@ function extractVerdict(reviewContent: string): string {
1121
1121
  // TP-068: Tolerate non-standard verdict formats from models that don't
1122
1122
  // follow the exact template (e.g., "Changes requested", "Needs revision").
1123
1123
  const lower = reviewContent.toLowerCase();
1124
- if (/\b(changes?\s+requested|needs?\s+revision|please\s+revise|must\s+revise)\b/.test(lower)) {
1124
+ if (/\b(request\s+changes?|changes?\s+requested|needs?\s+revision|please\s+revise|must\s+revise)\b/.test(lower)) {
1125
1125
  return "REVISE";
1126
1126
  }
1127
1127
  if (/\b(looks?\s+good|no\s+issues?\s+found|approved?)\b/.test(lower)) {
@@ -915,10 +915,8 @@ export function spawnLaneSession(
915
915
 
916
916
  // Build env vars
917
917
  const envVars = buildLaneEnvVars(lane, task.task.promptPath, repoRoot, workspaceRoot);
918
- // Pass batch ID so task-runner can include it in lane state for dashboard filtering
919
- if (config.orchestrator?.batchId) {
920
- envVars.ORCH_BATCH_ID = config.orchestrator.batchId;
921
- }
918
+ // ORCH_BATCH_ID is passed via extraEnvVars from executeWave executeLane spawnLaneSession.
919
+ // The task-runner reads it to include batchId in lane-state JSON for dashboard filtering.
922
920
  if (extraEnvVars) {
923
921
  Object.assign(envVars, extraEnvVars);
924
922
  }
@@ -2371,7 +2369,9 @@ export async function executeWave(
2371
2369
  const wsRoot = workspaceConfig ? dirname(dirname(workspaceConfig.configPath)) : undefined;
2372
2370
  const isWsMode = !!workspaceConfig;
2373
2371
  const lanePromises = lanes.map(lane =>
2374
- executeLane(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode),
2372
+ executeLane(lane, config, repoRoot, wavePauseSignal, wsRoot, isWsMode, {
2373
+ ORCH_BATCH_ID: batchId,
2374
+ }),
2375
2375
  );
2376
2376
 
2377
2377
  // Start monitoring as a sibling async loop
@@ -9,7 +9,7 @@ import { join, dirname, basename } from "path";
9
9
  import { execLog } from "./execution.ts";
10
10
  import { BATCH_STATE_SCHEMA_VERSION, StateFileError, batchStatePath, BATCH_HISTORY_MAX_ENTRIES, defaultResilienceState, defaultBatchDiagnostics } from "./types.ts";
11
11
  import type { BatchHistorySummary } from "./types.ts";
12
- import type { AllocatedLane, DiscoveryResult, EngineEvent, EscalationContext, LaneTaskOutcome, LaneTaskStatus, MonitorState, OrchBatchPhase, OrchBatchRuntimeState, PersistedBatchState, PersistedLaneRecord, PersistedMergeResult, PersistedTaskRecord, TaskMonitorSnapshot, Tier0RecoveryPattern, WorkspaceMode } from "./types.ts";
12
+ import type { AllocatedLane, DiscoveryResult, EngineEvent, EscalationContext, LaneTaskOutcome, LaneTaskStatus, MonitorState, OrchBatchPhase, OrchBatchRuntimeState, PersistedBatchState, PersistedLaneRecord, PersistedMergeResult, PersistedSegmentRecord, PersistedTaskRecord, TaskMonitorSnapshot, Tier0RecoveryPattern, WorkspaceMode } from "./types.ts";
13
13
  import { sleepSync } from "./worktree.ts";
14
14
  import type { PreserveFailedLaneProgressResult } from "./worktree.ts";
15
15
 
@@ -379,6 +379,29 @@ export function upconvertV2toV3(obj: Record<string, unknown>): void {
379
379
  if (!obj.diagnostics) obj.diagnostics = defaultBatchDiagnostics();
380
380
  }
381
381
 
382
+ /**
383
+ * Upconvert a v3 state object to v4 by adding the `segments` array.
384
+ *
385
+ * Added fields:
386
+ * - `segments`: empty array (no segment records exist in pre-v4 state)
387
+ *
388
+ * Task-level segment fields (`packetRepoId`, `packetTaskPath`,
389
+ * `segmentIds`, `activeSegmentId`) are optional and default to
390
+ * `undefined` (omitted from JSON). They are NOT backfilled here
391
+ * because their values depend on runtime discovery, not on
392
+ * migration defaults.
393
+ *
394
+ * This function is idempotent: calling it on an already-v4 object is a no-op.
395
+ *
396
+ * @param obj - Parsed state object (mutated in-place)
397
+ */
398
+ export function upconvertV3toV4(obj: Record<string, unknown>): void {
399
+ if ((obj.schemaVersion as number) >= 4) return;
400
+ obj.schemaVersion = 4;
401
+ // Backfill v4 segments with empty array only during genuine v3→v4 migration.
402
+ if (!obj.segments) obj.segments = [];
403
+ }
404
+
382
405
  /**
383
406
  * Validate a parsed JSON object as a PersistedBatchState.
384
407
  *
@@ -410,9 +433,9 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
410
433
  `Missing or invalid "schemaVersion" field (expected number, got ${typeof obj.schemaVersion})`,
411
434
  );
412
435
  }
413
- // Accept v1 (auto-upconvert to v2→v3), v2 (upconvert to v3), and v3 (current).
436
+ // Accept v1 (auto-upconvert to v2→v3→v4), v2 (upconvert to v3→v4), v3 (upconvert to v4), and v4 (current).
414
437
  // Reject anything else — including future versions from newer runtimes.
415
- const ACCEPTED_VERSIONS = [1, 2, BATCH_STATE_SCHEMA_VERSION];
438
+ const ACCEPTED_VERSIONS = [1, 2, 3, BATCH_STATE_SCHEMA_VERSION];
416
439
  if (!ACCEPTED_VERSIONS.includes(obj.schemaVersion as number)) {
417
440
  throw new StateFileError(
418
441
  "STATE_SCHEMA_INVALID",
@@ -754,12 +777,13 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
754
777
  }
755
778
  }
756
779
 
757
- // ── v1→v2→v3 upconversion ────────────────────────────────────
780
+ // ── v1→v2→v3→v4 upconversion ─────────────────────────────────
758
781
  // Apply defaults for fields that may be absent in older state files.
759
782
  // The on-disk file is NOT rewritten; upconversion is in-memory only.
760
- // Chain: v1→v2 then v2→v3 (each is idempotent / no-op if already at target).
783
+ // Chain: v1→v2 then v2→v3 then v3→v4 (each is idempotent / no-op if already at target).
761
784
  upconvertV1toV2(obj);
762
785
  upconvertV2toV3(obj);
786
+ upconvertV3toV4(obj);
763
787
 
764
788
  // ── Validate v3 resilience section ───────────────────────────
765
789
  // After upconversion, resilience must be a valid object with correct types.
@@ -928,6 +952,127 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
928
952
  );
929
953
  }
930
954
  }
955
+ // v4 optional fields: packetRepoId, packetTaskPath (string | undefined)
956
+ if (t.packetRepoId !== undefined && typeof t.packetRepoId !== "string") {
957
+ throw new StateFileError(
958
+ "STATE_SCHEMA_INVALID",
959
+ `tasks[${i}].packetRepoId is not a string (got ${typeof t.packetRepoId})`,
960
+ );
961
+ }
962
+ if (t.packetTaskPath !== undefined && typeof t.packetTaskPath !== "string") {
963
+ throw new StateFileError(
964
+ "STATE_SCHEMA_INVALID",
965
+ `tasks[${i}].packetTaskPath is not a string (got ${typeof t.packetTaskPath})`,
966
+ );
967
+ }
968
+ // v4 optional field: segmentIds (string[] | undefined)
969
+ if (t.segmentIds !== undefined) {
970
+ if (!Array.isArray(t.segmentIds)) {
971
+ throw new StateFileError(
972
+ "STATE_SCHEMA_INVALID",
973
+ `tasks[${i}].segmentIds is not an array (got ${typeof t.segmentIds})`,
974
+ );
975
+ }
976
+ for (let j = 0; j < (t.segmentIds as unknown[]).length; j++) {
977
+ if (typeof (t.segmentIds as unknown[])[j] !== "string") {
978
+ throw new StateFileError(
979
+ "STATE_SCHEMA_INVALID",
980
+ `tasks[${i}].segmentIds[${j}] is not a string`,
981
+ );
982
+ }
983
+ }
984
+ }
985
+ // v4 optional field: activeSegmentId (string | null | undefined)
986
+ if (t.activeSegmentId !== undefined && t.activeSegmentId !== null && typeof t.activeSegmentId !== "string") {
987
+ throw new StateFileError(
988
+ "STATE_SCHEMA_INVALID",
989
+ `tasks[${i}].activeSegmentId is not a string or null (got ${typeof t.activeSegmentId})`,
990
+ );
991
+ }
992
+ }
993
+
994
+ // ── Validate v4 segments array ───────────────────────────────
995
+ if (!Array.isArray(obj.segments)) {
996
+ throw new StateFileError(
997
+ "STATE_SCHEMA_INVALID",
998
+ `Missing or invalid "segments" field (expected array, got ${typeof obj.segments})`,
999
+ );
1000
+ }
1001
+ const segments = obj.segments as unknown[];
1002
+ for (let i = 0; i < segments.length; i++) {
1003
+ const s = segments[i] as Record<string, unknown>;
1004
+ if (!s || typeof s !== "object") {
1005
+ throw new StateFileError(
1006
+ "STATE_SCHEMA_INVALID",
1007
+ `segments[${i}] is not an object`,
1008
+ );
1009
+ }
1010
+ // Required string fields
1011
+ for (const field of ["segmentId", "taskId", "repoId", "laneId", "sessionName", "worktreePath", "branch", "exitReason"] as const) {
1012
+ if (typeof s[field] !== "string") {
1013
+ throw new StateFileError(
1014
+ "STATE_SCHEMA_INVALID",
1015
+ `segments[${i}].${field} is missing or not a string (got ${typeof s[field]})`,
1016
+ );
1017
+ }
1018
+ }
1019
+ // Required status field (same valid values as task status)
1020
+ if (typeof s.status !== "string" || !VALID_TASK_STATUSES.has(s.status)) {
1021
+ throw new StateFileError(
1022
+ "STATE_SCHEMA_INVALID",
1023
+ `segments[${i}].status is invalid: "${s.status}" (expected one of: ${[...VALID_TASK_STATUSES].join(", ")})`,
1024
+ );
1025
+ }
1026
+ // Nullable number fields: startedAt, endedAt
1027
+ if (s.startedAt !== null && typeof s.startedAt !== "number") {
1028
+ throw new StateFileError(
1029
+ "STATE_SCHEMA_INVALID",
1030
+ `segments[${i}].startedAt is not a number or null (got ${typeof s.startedAt})`,
1031
+ );
1032
+ }
1033
+ if (s.endedAt !== null && typeof s.endedAt !== "number") {
1034
+ throw new StateFileError(
1035
+ "STATE_SCHEMA_INVALID",
1036
+ `segments[${i}].endedAt is not a number or null (got ${typeof s.endedAt})`,
1037
+ );
1038
+ }
1039
+ // Required number: retries
1040
+ if (typeof s.retries !== "number") {
1041
+ throw new StateFileError(
1042
+ "STATE_SCHEMA_INVALID",
1043
+ `segments[${i}].retries is not a number (got ${typeof s.retries})`,
1044
+ );
1045
+ }
1046
+ // Required array: dependsOnSegmentIds
1047
+ if (!Array.isArray(s.dependsOnSegmentIds)) {
1048
+ throw new StateFileError(
1049
+ "STATE_SCHEMA_INVALID",
1050
+ `segments[${i}].dependsOnSegmentIds is not an array (got ${typeof s.dependsOnSegmentIds})`,
1051
+ );
1052
+ }
1053
+ for (let j = 0; j < (s.dependsOnSegmentIds as unknown[]).length; j++) {
1054
+ if (typeof (s.dependsOnSegmentIds as unknown[])[j] !== "string") {
1055
+ throw new StateFileError(
1056
+ "STATE_SCHEMA_INVALID",
1057
+ `segments[${i}].dependsOnSegmentIds[${j}] is not a string`,
1058
+ );
1059
+ }
1060
+ }
1061
+ // Optional exitDiagnostic
1062
+ if (s.exitDiagnostic !== undefined) {
1063
+ if (!s.exitDiagnostic || typeof s.exitDiagnostic !== "object" || Array.isArray(s.exitDiagnostic)) {
1064
+ throw new StateFileError(
1065
+ "STATE_SCHEMA_INVALID",
1066
+ `segments[${i}].exitDiagnostic is not a plain object (got ${Array.isArray(s.exitDiagnostic) ? "array" : typeof s.exitDiagnostic})`,
1067
+ );
1068
+ }
1069
+ if (typeof (s.exitDiagnostic as Record<string, unknown>).classification !== "string") {
1070
+ throw new StateFileError(
1071
+ "STATE_SCHEMA_INVALID",
1072
+ `segments[${i}].exitDiagnostic.classification is not a string`,
1073
+ );
1074
+ }
1075
+ }
931
1076
  }
932
1077
 
933
1078
  // ── Capture unknown top-level fields for roundtrip preservation ──
@@ -941,6 +1086,7 @@ export function validatePersistedState(data: unknown): PersistedBatchState {
941
1086
  "totalTasks", "succeededTasks", "failedTasks", "skippedTasks", "blockedTasks",
942
1087
  "blockedTaskIds", "lastError", "errors",
943
1088
  "resilience", "diagnostics",
1089
+ "segments",
944
1090
  "_extraFields",
945
1091
  ]);
946
1092
  const extraFields: Record<string, unknown> = {};
@@ -1049,6 +1195,20 @@ export function serializeBatchState(
1049
1195
  record.exitDiagnostic = outcome.exitDiagnostic;
1050
1196
  }
1051
1197
 
1198
+ // TP-081 v4: Serialize segment-level fields from ParsedTask or existing state
1199
+ if (allocated?.allocatedTask.task?.packetRepoId !== undefined) {
1200
+ (record as any).packetRepoId = allocated.allocatedTask.task.packetRepoId;
1201
+ }
1202
+ if (allocated?.allocatedTask.task?.packetTaskPath !== undefined) {
1203
+ (record as any).packetTaskPath = allocated.allocatedTask.task.packetTaskPath;
1204
+ }
1205
+ if (allocated?.allocatedTask.task?.segmentIds !== undefined) {
1206
+ (record as any).segmentIds = allocated.allocatedTask.task.segmentIds;
1207
+ }
1208
+ if (allocated?.allocatedTask.task?.activeSegmentId !== undefined) {
1209
+ (record as any).activeSegmentId = allocated.allocatedTask.task.activeSegmentId;
1210
+ }
1211
+
1052
1212
  return record;
1053
1213
  });
1054
1214
 
@@ -1122,6 +1282,7 @@ export function serializeBatchState(
1122
1282
  errors: [...state.errors],
1123
1283
  resilience: state.resilience ?? defaultResilienceState(),
1124
1284
  diagnostics: state.diagnostics ?? defaultBatchDiagnostics(),
1285
+ segments: state.segments ?? [],
1125
1286
  };
1126
1287
 
1127
1288
  // Merge unknown fields from loaded state to preserve roundtrip fidelity.
@@ -1167,7 +1167,9 @@ export async function resumeOrchBatch(
1167
1167
  });
1168
1168
 
1169
1169
  try {
1170
- spawnLaneSession(lane, allocatedTask, orchConfig, reExecRepoRoot);
1170
+ spawnLaneSession(lane, allocatedTask, orchConfig, reExecRepoRoot, undefined, {
1171
+ ORCH_BATCH_ID: batchState.batchId,
1172
+ });
1171
1173
  const pollResult = await pollUntilTaskComplete(
1172
1174
  lane,
1173
1175
  allocatedTask,
@@ -98,6 +98,26 @@ export interface ParsedTask {
98
98
  resolvedRepoId?: string;
99
99
  /** Optional explicit segment DAG metadata from `## Segment DAG`. */
100
100
  explicitSegmentDag?: PromptSegmentDagMetadata;
101
+ /**
102
+ * Repo ID that owns task packet files (v4, TP-081).
103
+ * Populated by execution engine in workspace mode. Undefined in repo mode.
104
+ */
105
+ packetRepoId?: string;
106
+ /**
107
+ * Absolute path to task folder in the packet repo worktree (v4, TP-081).
108
+ * Populated by execution engine. Undefined if not yet resolved.
109
+ */
110
+ packetTaskPath?: string;
111
+ /**
112
+ * Segment IDs for this task (v4, TP-081).
113
+ * Populated from TaskSegmentPlan during execution.
114
+ */
115
+ segmentIds?: string[];
116
+ /**
117
+ * Currently active segment ID (v4, TP-081).
118
+ * Null when no segment is active.
119
+ */
120
+ activeSegmentId?: string | null;
101
121
  }
102
122
 
103
123
  /** Build a stable segment ID from task + repo identity (`<taskId>::<repoId>`). */
@@ -1015,6 +1035,12 @@ export interface OrchBatchRuntimeState {
1015
1035
  * Populated from persisted state on resume; defaults used for new batches.
1016
1036
  */
1017
1037
  diagnostics?: BatchDiagnostics;
1038
+ /**
1039
+ * v4 segment records carried forward across resume cycles (TP-081).
1040
+ * Populated from persisted state on resume; empty for new batches
1041
+ * and repo-mode batches.
1042
+ */
1043
+ segments?: PersistedSegmentRecord[];
1018
1044
  /**
1019
1045
  * Unknown top-level fields from loaded persisted state.
1020
1046
  * Carried forward so they survive serialization roundtrips.
@@ -2304,15 +2330,22 @@ export function defaultBatchDiagnostics(): BatchDiagnostics {
2304
2330
  * exit summaries, batch cost). Task records gain optional
2305
2331
  * `exitDiagnostic` alongside legacy `exitReason`.
2306
2332
  * Both new sections are optional for v1/v2 migration paths.
2333
+ * v4 — Segment execution (TP-081). Adds optional `segments` array
2334
+ * for persisting per-segment runtime state. Task records gain
2335
+ * optional `packetRepoId`, `packetTaskPath`, `segmentIds`, and
2336
+ * `activeSegmentId` fields. All v4-specific fields are optional
2337
+ * for backward compatibility with v1/v2/v3 migration paths.
2338
+ * When migrating from v3, `segments` defaults to `[]` and
2339
+ * task-level segment fields default to `undefined`.
2307
2340
  *
2308
2341
  * Compatibility policy:
2309
- * - loadBatchState() accepts v1, v2, and v3 files. v1 and v2 are
2310
- * auto-upconverted to v3 in memory (chained: v1→v2→v3).
2342
+ * - loadBatchState() accepts v1, v2, v3, and v4 files. v1v2→v3→v4
2343
+ * auto-upconverted in memory (chained).
2311
2344
  * The on-disk file is NOT rewritten during load.
2312
- * - saveBatchState() always writes v3.
2313
- * - Schema versions > 3 are rejected with STATE_SCHEMA_INVALID.
2345
+ * - saveBatchState() always writes v4.
2346
+ * - Schema versions > 4 are rejected with STATE_SCHEMA_INVALID.
2314
2347
  */
2315
- export const BATCH_STATE_SCHEMA_VERSION = 3;
2348
+ export const BATCH_STATE_SCHEMA_VERSION = 4;
2316
2349
 
2317
2350
  /**
2318
2351
  * Canonical file path for persisted batch state.
@@ -2431,6 +2464,99 @@ export interface PersistedTaskRecord {
2431
2464
  * falling back to `exitReason` for display.
2432
2465
  */
2433
2466
  exitDiagnostic?: TaskExitDiagnostic;
2467
+ /**
2468
+ * Repo ID that owns task packet files (PROMPT.md/STATUS.md/.DONE) (v4, TP-081).
2469
+ *
2470
+ * In workspace mode, this is the `taskPacketRepo` from routing config.
2471
+ * Undefined in repo mode or for pre-v4 state files.
2472
+ */
2473
+ packetRepoId?: string;
2474
+ /**
2475
+ * Absolute path to the task folder in the packet repo worktree (v4, TP-081).
2476
+ *
2477
+ * Used by resume to locate packet files without re-running discovery.
2478
+ * Undefined in repo mode or for pre-v4 state files.
2479
+ */
2480
+ packetTaskPath?: string;
2481
+ /**
2482
+ * Segment IDs belonging to this task (v4, TP-081).
2483
+ *
2484
+ * Array of segment ID strings (`<taskId>::<repoId>`).
2485
+ * Empty array for repo-mode tasks or single-repo tasks.
2486
+ * Undefined for pre-v4 state files.
2487
+ */
2488
+ segmentIds?: string[];
2489
+ /**
2490
+ * Currently executing segment ID (v4, TP-081).
2491
+ *
2492
+ * Null when no segment is active (all completed or not started).
2493
+ * Undefined for pre-v4 state files.
2494
+ */
2495
+ activeSegmentId?: string | null;
2496
+ }
2497
+
2498
+ // ── Segment-Level Persisted State (v4, TP-081) ──────────────────────
2499
+
2500
+ /**
2501
+ * Segment execution status within a batch.
2502
+ *
2503
+ * State machine mirrors `LaneTaskStatus` but applies at segment granularity:
2504
+ * pending → running → succeeded
2505
+ * → failed
2506
+ * → stalled
2507
+ * pending → skipped (prior segment failed, or task skipped)
2508
+ *
2509
+ * @since v4 (TP-081)
2510
+ */
2511
+ export type PersistedSegmentStatus = "pending" | "running" | "succeeded" | "failed" | "stalled" | "skipped";
2512
+
2513
+ /**
2514
+ * Persisted record of a single segment's execution state.
2515
+ *
2516
+ * A segment is a repo-scoped execution unit within a task. Each task
2517
+ * may have one or more segments (one per repo the task touches).
2518
+ *
2519
+ * Contains everything `/orch-resume` needs to reconstruct segment-level
2520
+ * progress without re-running discovery.
2521
+ *
2522
+ * @since v4 (TP-081)
2523
+ */
2524
+ export interface PersistedSegmentRecord {
2525
+ /** Stable segment identifier (`<taskId>::<repoId>`, e.g., "TP-002::api") */
2526
+ segmentId: string;
2527
+ /** Parent task identifier */
2528
+ taskId: string;
2529
+ /** Repo ID this segment targets */
2530
+ repoId: string;
2531
+ /** Segment execution status */
2532
+ status: PersistedSegmentStatus;
2533
+ /** Lane ID the segment executed on (e.g., "lane-1"), empty if not yet assigned */
2534
+ laneId: string;
2535
+ /** TMUX session name used for this segment */
2536
+ sessionName: string;
2537
+ /** Absolute path to the worktree used for this segment */
2538
+ worktreePath: string;
2539
+ /** Git branch name checked out for this segment */
2540
+ branch: string;
2541
+ /** Epoch ms when segment execution started (null if not yet started) */
2542
+ startedAt: number | null;
2543
+ /** Epoch ms when segment execution ended (null if still pending/running) */
2544
+ endedAt: number | null;
2545
+ /** Number of retry attempts for this segment */
2546
+ retries: number;
2547
+ /**
2548
+ * Segment IDs this segment depends on (intra-task DAG edges).
2549
+ * Empty array for the first segment in a task or for tasks with no intra-task deps.
2550
+ */
2551
+ dependsOnSegmentIds: string[];
2552
+ /**
2553
+ * Structured exit diagnostic for this segment.
2554
+ * Optional: absent for segments that haven't exited yet.
2555
+ * Uses the same `TaskExitDiagnostic` shape from diagnostics.ts.
2556
+ */
2557
+ exitDiagnostic?: TaskExitDiagnostic;
2558
+ /** Human-readable exit reason (legacy compat, same as task-level) */
2559
+ exitReason: string;
2434
2560
  }
2435
2561
 
2436
2562
  /**
@@ -2545,9 +2671,17 @@ export interface PersistedRepoMergeOutcome {
2545
2671
  * data alongside legacy `exitReason` string).
2546
2672
  * - Both sections are required in v3. Migration from v1/v2 fills
2547
2673
  * conservative defaults (see `defaultResilienceState()` / `defaultBatchDiagnostics()`).
2674
+ *
2675
+ * v4 additions (TP-081):
2676
+ * - `segments` array (required): per-segment execution records for multi-repo
2677
+ * task execution. Empty array in repo mode or for pre-v4 migration.
2678
+ * - Task records gain optional `packetRepoId`, `packetTaskPath`, `segmentIds`,
2679
+ * and `activeSegmentId` for segment-level tracking.
2680
+ * - Migration from v3 fills `segments` as `[]` and leaves task-level segment
2681
+ * fields as `undefined`.
2548
2682
  */
2549
2683
  export interface PersistedBatchState {
2550
- /** Schema version — must equal BATCH_STATE_SCHEMA_VERSION (currently 3) */
2684
+ /** Schema version — must equal BATCH_STATE_SCHEMA_VERSION (currently 4) */
2551
2685
  schemaVersion: number;
2552
2686
  /** Current batch execution phase */
2553
2687
  phase: OrchBatchPhase;
@@ -2596,14 +2730,24 @@ export interface PersistedBatchState {
2596
2730
  errors: string[];
2597
2731
  /**
2598
2732
  * Resilience state for retry/recovery tracking (v3, TP-030).
2599
- * Required in v3. Migration from v1/v2 fills conservative defaults.
2733
+ * Required in v3+. Migration from v1/v2 fills conservative defaults.
2600
2734
  */
2601
2735
  resilience: ResilienceState;
2602
2736
  /**
2603
2737
  * Batch-level diagnostics for cost tracking and exit summaries (v3, TP-030).
2604
- * Required in v3. Migration from v1/v2 fills conservative defaults.
2738
+ * Required in v3+. Migration from v1/v2 fills conservative defaults.
2605
2739
  */
2606
2740
  diagnostics: BatchDiagnostics;
2741
+ /**
2742
+ * Per-segment execution records for multi-repo task execution (v4, TP-081).
2743
+ *
2744
+ * Each entry represents one repo-scoped segment of a task. In repo mode
2745
+ * or for single-repo tasks, this array is empty (segment tracking is
2746
+ * implicit via task records).
2747
+ *
2748
+ * Required in v4. Migration from v1/v2/v3 fills empty array.
2749
+ */
2750
+ segments: PersistedSegmentRecord[];
2607
2751
  /**
2608
2752
  * Unknown top-level fields captured during deserialization.
2609
2753
  * Preserved on roundtrip to avoid data loss from future schema extensions
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.22.10",
3
+ "version": "0.22.11",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -201,8 +201,29 @@ a reviewer agent. The tool takes two parameters: `step` (number) and `type`
201
201
  documentation/delivery). These are low-risk steps where review overhead exceeds
202
202
  value.
203
203
 
204
+ ### ⚠️ CRITICAL: Plan review happens BEFORE implementation
205
+
206
+ **The plan review MUST happen BEFORE you write any code for that step.**
207
+ The entire purpose of plan review is to catch design issues, missing cases, and
208
+ wrong approaches BEFORE you spend tokens implementing them. If you implement
209
+ first and then request plan review, the reviewer's feedback is wasted — the
210
+ code is already written.
211
+
212
+ **Correct sequence:**
213
+ 1. Hydrate step checkboxes (expand the plan)
214
+ 2. Commit the hydrated STATUS.md
215
+ 3. **Call `review_step(step=N, type="plan")` — BEFORE writing any code**
216
+ 4. Handle verdict (APPROVE → implement; REVISE → fix plan, re-review)
217
+ 5. Implement the step (write code, check off items)
218
+ 6. Commit implementation
219
+ 7. Call `review_step(step=N, type="code")` — AFTER implementation
220
+
221
+ **WRONG sequence (violates the protocol):**
222
+ 1. ~~Hydrate, implement, check off, commit, THEN call plan review~~ ❌
223
+ This makes plan review pointless — the work is already done.
224
+
204
225
  **Handling verdicts:**
205
- - **APPROVE** → proceed to next step
226
+ - **APPROVE** → proceed (to implementation after plan review; to next step after code review)
206
227
  - **RETHINK** → reconsider your plan approach, adjust, then implement
207
228
  - **REVISE** → read the review file in `.reviews/` for detailed feedback,
208
229
  address the issues, commit fixes, then **call `review_step` again** for re-review.
@@ -211,14 +232,16 @@ value.
211
232
 
212
233
  **Example flow for a Review Level 2 task, Step 3:**
213
234
  1. Read Step 3 requirements
214
- 2. Call `review_step(step=3, type="plan")` → get plan feedback
215
- 3. Capture baseline: run `git rev-parse HEAD` and save the SHA
216
- 4. Implement Step 3
217
- 5. Commit changes
218
- 6. Call `review_step(step=3, type="code", baseline="<saved SHA>")` → get code feedback
219
- 7. If REVISE: fix issues, commit, call `review_step(step=3, type="code")` again
220
- 8. Repeat 7 until APPROVE (max 2 code review cycles per step)
221
- 9. Move to Step 4
235
+ 2. Hydrate Step 3 checkboxes, commit STATUS.md
236
+ 3. Call `review_step(step=3, type="plan")` get plan feedback (**NO CODE YET**)
237
+ 4. If REVISE: adjust plan, re-request plan review
238
+ 5. If APPROVE: capture baseline SHA (`git rev-parse HEAD`)
239
+ 6. Implement Step 3 (write code, check off items)
240
+ 7. Commit changes
241
+ 8. Call `review_step(step=3, type="code", baseline="<saved SHA>")` get code feedback
242
+ 9. If REVISE: fix issues, commit, call `review_step(step=3, type="code")` again
243
+ 10. Repeat 9 until APPROVE (max 2 code review cycles per step)
244
+ 11. Move to Step 4
222
245
 
223
246
  If the `review_step` tool is not available (e.g., non-orchestrated mode), skip
224
247
  this protocol entirely — the task-runner handles reviews externally.