taskplane 0.22.9 → 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.
- package/bin/rpc-wrapper.mjs +22 -2
- package/extensions/task-runner.ts +1 -1
- package/extensions/taskplane/execution.ts +5 -5
- package/extensions/taskplane/extension.ts +5 -0
- package/extensions/taskplane/persistence.ts +166 -5
- package/extensions/taskplane/resume.ts +3 -1
- package/extensions/taskplane/types.ts +152 -8
- package/extensions/taskplane/waves.ts +31 -4
- package/package.json +1 -1
- package/templates/agents/task-worker.md +32 -9
package/bin/rpc-wrapper.mjs
CHANGED
|
@@ -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
|
|
647
|
-
|
|
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
|
-
//
|
|
919
|
-
|
|
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
|
|
@@ -1685,6 +1685,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1685
1685
|
discovery.pending,
|
|
1686
1686
|
discovery.completed,
|
|
1687
1687
|
orchConfig,
|
|
1688
|
+
{
|
|
1689
|
+
workspaceRepoIds: execCtx!.workspaceConfig
|
|
1690
|
+
? execCtx!.workspaceConfig.repos.keys()
|
|
1691
|
+
: undefined,
|
|
1692
|
+
},
|
|
1688
1693
|
);
|
|
1689
1694
|
|
|
1690
1695
|
ctx.ui.notify(
|
|
@@ -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
|
|
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
|
|
2310
|
-
* auto-upconverted
|
|
2342
|
+
* - loadBatchState() accepts v1, v2, v3, and v4 files. v1→v2→v3→v4
|
|
2343
|
+
* auto-upconverted in memory (chained).
|
|
2311
2344
|
* The on-disk file is NOT rewritten during load.
|
|
2312
|
-
* - saveBatchState() always writes
|
|
2313
|
-
* - Schema versions >
|
|
2345
|
+
* - saveBatchState() always writes v4.
|
|
2346
|
+
* - Schema versions > 4 are rejected with STATE_SCHEMA_INVALID.
|
|
2314
2347
|
*/
|
|
2315
|
-
export const BATCH_STATE_SCHEMA_VERSION =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
@@ -617,8 +617,24 @@ function normalizeRepoIdCandidate(raw: string): string | null {
|
|
|
617
617
|
return candidate;
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
-
|
|
620
|
+
interface SegmentPlanBuildOptions {
|
|
621
|
+
/** Optional workspace repo IDs used to validate file-scope repo prefixes. */
|
|
622
|
+
workspaceRepoIds?: Iterable<string>;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function collectKnownRepoIds(
|
|
626
|
+
pending: Map<string, ParsedTask>,
|
|
627
|
+
workspaceRepoIds?: Iterable<string>,
|
|
628
|
+
): Set<string> {
|
|
621
629
|
const known = new Set<string>();
|
|
630
|
+
|
|
631
|
+
if (workspaceRepoIds) {
|
|
632
|
+
for (const repoIdRaw of workspaceRepoIds) {
|
|
633
|
+
const repoId = normalizeRepoIdCandidate(String(repoIdRaw));
|
|
634
|
+
if (repoId) known.add(repoId);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
622
638
|
for (const task of pending.values()) {
|
|
623
639
|
if (task.resolvedRepoId) {
|
|
624
640
|
const repoId = normalizeRepoIdCandidate(task.resolvedRepoId);
|
|
@@ -784,8 +800,11 @@ export function buildSegmentPlanForTask(
|
|
|
784
800
|
}
|
|
785
801
|
|
|
786
802
|
/** Build a deterministic taskId→segmentPlan map for the whole pending set. */
|
|
787
|
-
export function buildTaskSegmentPlans(
|
|
788
|
-
|
|
803
|
+
export function buildTaskSegmentPlans(
|
|
804
|
+
pending: Map<string, ParsedTask>,
|
|
805
|
+
options: SegmentPlanBuildOptions = {},
|
|
806
|
+
): TaskSegmentPlanMap {
|
|
807
|
+
const knownRepoIds = collectKnownRepoIds(pending, options.workspaceRepoIds);
|
|
789
808
|
const plans: TaskSegmentPlanMap = new Map();
|
|
790
809
|
for (const taskId of [...pending.keys()].sort()) {
|
|
791
810
|
const task = pending.get(taskId);
|
|
@@ -1349,10 +1368,16 @@ export function allocateLanes(
|
|
|
1349
1368
|
* Returns WaveAssignment[] with wave numbers and lane assignments,
|
|
1350
1369
|
* plus any errors encountered.
|
|
1351
1370
|
*/
|
|
1371
|
+
export interface WaveComputationOptions {
|
|
1372
|
+
/** Optional workspace repo IDs used by segment inference in workspace mode. */
|
|
1373
|
+
workspaceRepoIds?: Iterable<string>;
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1352
1376
|
export function computeWaveAssignments(
|
|
1353
1377
|
pending: Map<string, ParsedTask>,
|
|
1354
1378
|
completed: Set<string>,
|
|
1355
1379
|
config: OrchestratorConfig,
|
|
1380
|
+
options: WaveComputationOptions = {},
|
|
1356
1381
|
): WaveComputationResult {
|
|
1357
1382
|
const errors: DiscoveryError[] = [];
|
|
1358
1383
|
|
|
@@ -1372,7 +1397,9 @@ export function computeWaveAssignments(
|
|
|
1372
1397
|
}
|
|
1373
1398
|
|
|
1374
1399
|
// Step 3.5: Build additive segment planning output (deterministic map)
|
|
1375
|
-
const segmentPlans = buildTaskSegmentPlans(pending
|
|
1400
|
+
const segmentPlans = buildTaskSegmentPlans(pending, {
|
|
1401
|
+
workspaceRepoIds: options.workspaceRepoIds,
|
|
1402
|
+
});
|
|
1376
1403
|
|
|
1377
1404
|
// Step 4: Assign tasks to lanes within each wave
|
|
1378
1405
|
const waveAssignments: WaveAssignment[] = [];
|
package/package.json
CHANGED
|
@@ -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.
|
|
215
|
-
3.
|
|
216
|
-
4.
|
|
217
|
-
5.
|
|
218
|
-
6.
|
|
219
|
-
7.
|
|
220
|
-
8.
|
|
221
|
-
9.
|
|
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.
|