taskplane 0.26.1 → 0.28.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 +4 -1
- package/bin/taskplane.mjs +12 -5
- package/dashboard/public/app.js +162 -13
- package/extensions/taskplane/abort.ts +2 -1
- package/extensions/taskplane/agent-host.ts +100 -1
- package/extensions/taskplane/cleanup.ts +272 -10
- package/extensions/taskplane/discovery.ts +1818 -1508
- package/extensions/taskplane/engine.ts +182 -47
- package/extensions/taskplane/execution.ts +172 -51
- package/extensions/taskplane/extension.ts +5125 -5125
- package/extensions/taskplane/formatting.ts +70 -11
- package/extensions/taskplane/git.ts +34 -0
- package/extensions/taskplane/lane-runner.ts +586 -46
- package/extensions/taskplane/merge.ts +3128 -2917
- package/extensions/taskplane/persistence.ts +3 -0
- package/extensions/taskplane/resume.ts +86 -30
- package/extensions/taskplane/supervisor-primer.md +55 -0
- package/extensions/taskplane/types.ts +52 -3
- package/package.json +1 -1
- package/skills/create-taskplane-task/SKILL.md +58 -0
- package/skills/create-taskplane-task/references/prompt-template.md +39 -0
- package/templates/agents/task-worker-segment.md +44 -0
- package/templates/agents/task-worker.md +429 -387
|
@@ -1359,6 +1359,9 @@ export function serializeBatchState(
|
|
|
1359
1359
|
endedAt: state.endedAt,
|
|
1360
1360
|
currentWaveIndex: state.currentWaveIndex,
|
|
1361
1361
|
totalWaves: state.totalWaves,
|
|
1362
|
+
// TP-166: Persist task-level wave metadata for correct display after resume
|
|
1363
|
+
...(state.taskLevelWaveCount != null ? { taskLevelWaveCount: state.taskLevelWaveCount } : {}),
|
|
1364
|
+
...(state.roundToTaskWave != null ? { roundToTaskWave: [...state.roundToTaskWave] } : {}),
|
|
1362
1365
|
wavePlan,
|
|
1363
1366
|
lanes: laneRecords,
|
|
1364
1367
|
tasks: taskRecords,
|
|
@@ -7,7 +7,7 @@ import { join } from "path";
|
|
|
7
7
|
|
|
8
8
|
import { assembleDiagnosticInput, emitDiagnosticReports } from "./diagnostic-reports.ts";
|
|
9
9
|
import { runDiscovery } from "./discovery.ts";
|
|
10
|
-
import { executeOrchBatch } from "./engine.ts";
|
|
10
|
+
import { executeOrchBatch, resolveDisplayWaveNumber } from "./engine.ts";
|
|
11
11
|
import { buildReviewerEnv, computeTransitiveDependents, execLog, executeLaneV2, executeWave, resolveCanonicalTaskPaths } from "./execution.ts";
|
|
12
12
|
import type { MonitorUpdateCallback, RuntimeBackend } from "./execution.ts";
|
|
13
13
|
import { selectRuntimeBackend } from "./engine.ts";
|
|
@@ -158,9 +158,14 @@ export function reconstructAllocatedLanes(
|
|
|
158
158
|
if (persistedTask?.resolvedRepoId !== undefined) {
|
|
159
159
|
taskStub.resolvedRepoId = persistedTask.resolvedRepoId;
|
|
160
160
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
// TP-169: Always set taskFolder on stub, even if empty string.
|
|
162
|
+
// Previously, the falsy check `if (persistedTask?.taskFolder)` skipped
|
|
163
|
+
// empty-string values, leaving taskFolder as `undefined` on the stub.
|
|
164
|
+
// This caused crashes in buildExecutionUnit and merge code when
|
|
165
|
+
// accessing `task.task.taskFolder` on dynamically-expanded segments
|
|
166
|
+
// whose persisted records had taskFolder="" (the default from
|
|
167
|
+
// serializeBatchState before enrichment).
|
|
168
|
+
taskStub.taskFolder = persistedTask?.taskFolder ?? "";
|
|
164
169
|
if ((persistedTask as any)?.packetRepoId !== undefined) {
|
|
165
170
|
(taskStub as any).packetRepoId = (persistedTask as any).packetRepoId;
|
|
166
171
|
}
|
|
@@ -1324,6 +1329,10 @@ export async function resumeOrchBatch(
|
|
|
1324
1329
|
// Preserve pauseSignal if already set during "launching" phase (TP-040)
|
|
1325
1330
|
if (!batchState.pauseSignal?.paused) batchState.pauseSignal = { paused: false };
|
|
1326
1331
|
batchState.totalWaves = persistedState.totalWaves;
|
|
1332
|
+
// TP-166: Restore task-level wave metadata for correct display.
|
|
1333
|
+
// Normalize: fall back to totalWaves for pre-TP-166 state files.
|
|
1334
|
+
batchState.taskLevelWaveCount = persistedState.taskLevelWaveCount ?? persistedState.totalWaves;
|
|
1335
|
+
batchState.roundToTaskWave = persistedState.roundToTaskWave ? [...persistedState.roundToTaskWave] : undefined;
|
|
1327
1336
|
batchState.totalTasks = persistedState.totalTasks;
|
|
1328
1337
|
batchState.succeededTasks = resumePoint.completedTaskIds.length;
|
|
1329
1338
|
batchState.failedTasks = resumePoint.failedTaskIds.length;
|
|
@@ -1372,6 +1381,42 @@ export async function resumeOrchBatch(
|
|
|
1372
1381
|
batchState._extraFields = persistedState._extraFields;
|
|
1373
1382
|
}
|
|
1374
1383
|
|
|
1384
|
+
// ── 6b. TP-169: Verify orch branch exists in all workspace repos ────
|
|
1385
|
+
// During the original batch start, the orch branch was created in every
|
|
1386
|
+
// workspace repo. On resume, we verify it still exists. If it's missing
|
|
1387
|
+
// in any repo (e.g., deleted by user, corrupted), re-create it from the
|
|
1388
|
+
// repo's current branch so that worktree creation doesn't silently fall
|
|
1389
|
+
// back to the base branch, bypassing orch branch isolation.
|
|
1390
|
+
if (workspaceConfig && batchState.orchBranch) {
|
|
1391
|
+
for (const [repoId, repoConf] of workspaceConfig.repos) {
|
|
1392
|
+
const rRoot = repoConf.path;
|
|
1393
|
+
const check = runGit(["rev-parse", "--verify", `refs/heads/${batchState.orchBranch}`], rRoot);
|
|
1394
|
+
if (!check.ok) {
|
|
1395
|
+
// Orch branch missing in this repo — re-create from current HEAD
|
|
1396
|
+
const repoBranch = getCurrentBranch(rRoot) || "HEAD";
|
|
1397
|
+
const createRes = runGit(["branch", batchState.orchBranch, repoBranch], rRoot);
|
|
1398
|
+
if (createRes.ok) {
|
|
1399
|
+
execLog("resume", batchState.batchId, `re-created missing orch branch in ${repoId}`, {
|
|
1400
|
+
orchBranch: batchState.orchBranch,
|
|
1401
|
+
base: repoBranch,
|
|
1402
|
+
});
|
|
1403
|
+
onNotify(
|
|
1404
|
+
`⚠️ Orch branch "${batchState.orchBranch}" was missing in repo "${repoId}" — re-created from ${repoBranch}`,
|
|
1405
|
+
"warning",
|
|
1406
|
+
);
|
|
1407
|
+
} else {
|
|
1408
|
+
const errMsg = `Failed to re-create orch branch "${batchState.orchBranch}" in repo "${repoId}": ${createRes.stderr}. ` +
|
|
1409
|
+
`Cannot resume without orch branch isolation.`;
|
|
1410
|
+
execLog("resume", batchState.batchId, errMsg, {
|
|
1411
|
+
orchBranch: batchState.orchBranch,
|
|
1412
|
+
error: createRes.stderr,
|
|
1413
|
+
});
|
|
1414
|
+
throw new Error(errMsg);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1375
1420
|
// ── 7. Re-run discovery for ParsedTask metadata ──────────────
|
|
1376
1421
|
// We need fresh ParsedTask data (taskFolder, promptPath) for execution.
|
|
1377
1422
|
// Use "all" to discover all areas.
|
|
@@ -1767,12 +1812,17 @@ export async function resumeOrchBatch(
|
|
|
1767
1812
|
persistedState.tasks.map((task) => [task.taskId, task.status] as const),
|
|
1768
1813
|
);
|
|
1769
1814
|
|
|
1815
|
+
// TP-166: Use task-level wave metadata for correct display.
|
|
1816
|
+
const roundToTaskWave = batchState.roundToTaskWave;
|
|
1817
|
+
const taskLevelWaveCount = batchState.taskLevelWaveCount;
|
|
1818
|
+
|
|
1770
1819
|
for (let waveIdx = resumePoint.resumeWaveIndex; waveIdx < wavePlan.length; waveIdx++) {
|
|
1771
1820
|
// Check pause signal
|
|
1772
1821
|
if (batchState.pauseSignal.paused) {
|
|
1773
1822
|
batchState.phase = "paused";
|
|
1774
1823
|
persistRuntimeState("pause-before-wave", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
1775
|
-
|
|
1824
|
+
const { displayWave: pauseWave } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
|
|
1825
|
+
onNotify(`⏸️ Batch paused before wave ${pauseWave}.`, "warning");
|
|
1776
1826
|
break;
|
|
1777
1827
|
}
|
|
1778
1828
|
|
|
@@ -1807,7 +1857,7 @@ export async function resumeOrchBatch(
|
|
|
1807
1857
|
// All tasks are terminal but the merge may have failed/been interrupted.
|
|
1808
1858
|
if (resumePoint.mergeRetryWaveIndexes.includes(waveIdx)) {
|
|
1809
1859
|
execLog("resume", batchState.batchId, `wave ${waveIdx + 1}: all tasks done but merge needs retry`);
|
|
1810
|
-
onNotify(`🔀 Wave ${waveIdx
|
|
1860
|
+
onNotify(`🔀 Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave}: retrying merge (tasks already complete, merge was missing/failed)`, "info");
|
|
1811
1861
|
|
|
1812
1862
|
// Reconstruct lanes for this wave from persisted state
|
|
1813
1863
|
const waveTaskIds = new Set(wavePlan[waveIdx]);
|
|
@@ -1922,7 +1972,7 @@ export async function resumeOrchBatch(
|
|
|
1922
1972
|
batchState.mergeResults.push(mergeRetryResult);
|
|
1923
1973
|
|
|
1924
1974
|
if (mergeRetryResult.status === "succeeded") {
|
|
1925
|
-
onNotify(`✅ Wave ${waveIdx
|
|
1975
|
+
onNotify(`✅ Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave} merge retry succeeded`, "info");
|
|
1926
1976
|
// Clean up merged branches
|
|
1927
1977
|
for (const lr of mergeRetryResult.laneResults) {
|
|
1928
1978
|
if (!lr.error && (lr.result?.status === "SUCCESS" || lr.result?.status === "CONFLICT_RESOLVED")) {
|
|
@@ -1932,7 +1982,7 @@ export async function resumeOrchBatch(
|
|
|
1932
1982
|
}
|
|
1933
1983
|
} else {
|
|
1934
1984
|
onNotify(
|
|
1935
|
-
`⚠️ Wave ${waveIdx
|
|
1985
|
+
`⚠️ Wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave} merge retry ${mergeRetryResult.status}: ${mergeRetryResult.failureReason || "unknown"}`,
|
|
1936
1986
|
"warning",
|
|
1937
1987
|
);
|
|
1938
1988
|
// Apply merge failure policy (same as normal wave merge failure)
|
|
@@ -1954,10 +2004,13 @@ export async function resumeOrchBatch(
|
|
|
1954
2004
|
continue;
|
|
1955
2005
|
}
|
|
1956
2006
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
2007
|
+
{
|
|
2008
|
+
const { displayWave, displayTotal } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
|
|
2009
|
+
onNotify(
|
|
2010
|
+
ORCH_MESSAGES.orchWaveStart(displayWave, displayTotal, waveTasks.length, Math.min(waveTasks.length, orchConfig.orchestrator.max_lanes)),
|
|
2011
|
+
"info",
|
|
2012
|
+
);
|
|
2013
|
+
}
|
|
1961
2014
|
|
|
1962
2015
|
const handleResumeMonitorUpdate: MonitorUpdateCallback = (monitorState) => {
|
|
1963
2016
|
const changed = syncTaskOutcomesFromMonitor(monitorState, allTaskOutcomes);
|
|
@@ -2064,7 +2117,7 @@ export async function resumeOrchBatch(
|
|
|
2064
2117
|
frontierSummary +
|
|
2065
2118
|
` Lane: ${laneForTask?.laneId ?? "unknown"} (lane ${laneForTask?.laneNumber ?? "?"})\n` +
|
|
2066
2119
|
` Partial progress preserved: ${hasPartialProgress ? "yes" : "no"}\n` +
|
|
2067
|
-
` Batch: wave ${waveIdx
|
|
2120
|
+
` Batch: wave ${resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave}/${taskLevelWaveCount ?? batchState.totalWaves}, ` +
|
|
2068
2121
|
`${batchState.succeededTasks} succeeded, ${batchState.failedTasks} failed\n\n` +
|
|
2069
2122
|
`Available actions:\n` +
|
|
2070
2123
|
` - orch_status() to inspect current state\n` +
|
|
@@ -2088,16 +2141,19 @@ export async function resumeOrchBatch(
|
|
|
2088
2141
|
persistRuntimeState("wave-execution-complete", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
2089
2142
|
|
|
2090
2143
|
const elapsedSec = Math.round((waveResult.endedAt - waveResult.startedAt) / 1000);
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2144
|
+
{
|
|
2145
|
+
const { displayWave: completeDisplayWave } = resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount);
|
|
2146
|
+
onNotify(
|
|
2147
|
+
ORCH_MESSAGES.orchWaveComplete(
|
|
2148
|
+
completeDisplayWave,
|
|
2149
|
+
waveResult.succeededTaskIds.length,
|
|
2150
|
+
waveResult.failedTaskIds.length,
|
|
2151
|
+
waveResult.skippedTaskIds.length,
|
|
2152
|
+
elapsedSec,
|
|
2153
|
+
),
|
|
2154
|
+
waveResult.failedTaskIds.length > 0 ? "warning" : "info",
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2101
2157
|
|
|
2102
2158
|
// Check failure policy
|
|
2103
2159
|
if (waveResult.stoppedEarly) {
|
|
@@ -2144,7 +2200,7 @@ export async function resumeOrchBatch(
|
|
|
2144
2200
|
if (mergeableLaneCount > 0) {
|
|
2145
2201
|
batchState.phase = "merging";
|
|
2146
2202
|
persistRuntimeState("merge-start", batchState, wavePlan, latestAllocatedLanes, allTaskOutcomes, discovery, stateRoot);
|
|
2147
|
-
onNotify(ORCH_MESSAGES.orchMergeStart(waveIdx
|
|
2203
|
+
onNotify(ORCH_MESSAGES.orchMergeStart(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeableLaneCount), "info");
|
|
2148
2204
|
|
|
2149
2205
|
mergeResult = await mergeWaveByRepo(
|
|
2150
2206
|
waveResult.allocatedLanes,
|
|
@@ -2197,10 +2253,10 @@ export async function resumeOrchBatch(
|
|
|
2197
2253
|
const mergeTotalSec = Math.round(mergeResult.totalDurationMs / 1000);
|
|
2198
2254
|
|
|
2199
2255
|
if (mergeResult.status === "succeeded") {
|
|
2200
|
-
onNotify(ORCH_MESSAGES.orchMergeComplete(waveIdx
|
|
2256
|
+
onNotify(ORCH_MESSAGES.orchMergeComplete(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergedCount, mergeTotalSec), "info");
|
|
2201
2257
|
} else {
|
|
2202
2258
|
onNotify(
|
|
2203
|
-
ORCH_MESSAGES.orchMergeFailed(waveIdx
|
|
2259
|
+
ORCH_MESSAGES.orchMergeFailed(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeResult.failedLane ?? 0, mergeResult.failureReason || "unknown"),
|
|
2204
2260
|
"error",
|
|
2205
2261
|
);
|
|
2206
2262
|
|
|
@@ -2231,14 +2287,14 @@ export async function resumeOrchBatch(
|
|
|
2231
2287
|
// Downstream retry/update paths assume the current wave has an entry.
|
|
2232
2288
|
batchState.mergeResults.push(mergeResult);
|
|
2233
2289
|
onNotify(
|
|
2234
|
-
ORCH_MESSAGES.orchMergeFailed(waveIdx
|
|
2290
|
+
ORCH_MESSAGES.orchMergeFailed(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave, mergeResult.failedLane, mergeResult.failureReason || "unknown"),
|
|
2235
2291
|
"error",
|
|
2236
2292
|
);
|
|
2237
2293
|
} else {
|
|
2238
|
-
onNotify(ORCH_MESSAGES.orchMergeSkipped(waveIdx
|
|
2294
|
+
onNotify(ORCH_MESSAGES.orchMergeSkipped(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave), "info");
|
|
2239
2295
|
}
|
|
2240
2296
|
} else {
|
|
2241
|
-
onNotify(ORCH_MESSAGES.orchMergeSkipped(waveIdx
|
|
2297
|
+
onNotify(ORCH_MESSAGES.orchMergeSkipped(resolveDisplayWaveNumber(waveIdx, roundToTaskWave, taskLevelWaveCount).displayWave), "info");
|
|
2242
2298
|
}
|
|
2243
2299
|
|
|
2244
2300
|
// ── TP-033: Safe-stop on rollback failure ─────────────────
|
|
@@ -2755,7 +2811,7 @@ export async function resumeOrchBatch(
|
|
|
2755
2811
|
summary:
|
|
2756
2812
|
`✅ Batch ${batchState.batchId} completed\n` +
|
|
2757
2813
|
` ${batchState.succeededTasks}/${batchState.totalTasks} tasks succeeded\n` +
|
|
2758
|
-
` ${batchState.totalWaves} wave(s), duration: ${durationStr}\n` +
|
|
2814
|
+
` ${batchState.taskLevelWaveCount ?? batchState.totalWaves} wave(s), duration: ${durationStr}\n` +
|
|
2759
2815
|
` Merged to orch branch: ${batchState.orchBranch}\n\n` +
|
|
2760
2816
|
`Ready for integration. Run orch_integrate() or review first.`,
|
|
2761
2817
|
context: {
|
|
@@ -718,6 +718,7 @@ or check status manually. The engine wakes you up when you're needed.
|
|
|
718
718
|
| `task-failure` | ⚠️ | A task failed after deterministic recovery was exhausted |
|
|
719
719
|
| `merge-failure` | ⚠️ | Wave merge failed and batch paused |
|
|
720
720
|
| `batch-complete` | ✅/⚠️ | Batch finished (all waves done, with or without failures) |
|
|
721
|
+
| `worker-exit-intercept` | 🔄 | A worker exited without making progress — session still alive, awaiting instructions |
|
|
721
722
|
|
|
722
723
|
### Alert Format
|
|
723
724
|
|
|
@@ -1011,6 +1012,60 @@ BATCH COMPLETE: {batchId}
|
|
|
1011
1012
|
| merge-failure | Agent timeout, no result | `orch_resume(force=true)` to retry | Automatic |
|
|
1012
1013
|
| batch-complete | All succeeded | Report → suggest `orch_integrate` | Report only |
|
|
1013
1014
|
| batch-complete | Some failed | Report with failure details | Report only |
|
|
1015
|
+
| worker-exit-intercept | Worker analyzing, not editing | `send_agent_message` with targeted instructions | Automatic |
|
|
1016
|
+
| worker-exit-intercept | Worker genuinely stuck | "skip" or "let it fail" to close session | Supervised |
|
|
1017
|
+
| worker-exit-intercept | Unknown reason | Read STATUS.md, diagnose, then instruct or close | Automatic |
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
## 13c. Worker Exit Interception (TP-172)
|
|
1022
|
+
|
|
1023
|
+
When a worker agent produces a text-only response (no tool calls, no file
|
|
1024
|
+
edits) without having made visible progress (no checkbox updates), the
|
|
1025
|
+
lane-runner **intercepts the exit** instead of closing the session. The worker
|
|
1026
|
+
process remains alive with its full conversation context preserved.
|
|
1027
|
+
|
|
1028
|
+
**You receive a `worker-exit-intercept` alert** with:
|
|
1029
|
+
- Lane number and task ID
|
|
1030
|
+
- Current step and unchecked checkboxes
|
|
1031
|
+
- Worker's last assistant message (truncated to 500 chars)
|
|
1032
|
+
- Iteration count and no-progress count
|
|
1033
|
+
|
|
1034
|
+
### Response Protocol
|
|
1035
|
+
|
|
1036
|
+
1. **Read the worker's message** — understand why it wants to exit.
|
|
1037
|
+
Common patterns:
|
|
1038
|
+
- "I've analyzed the code and I'm not sure how to proceed"
|
|
1039
|
+
- "I need more information about X"
|
|
1040
|
+
- Generic summary without any file edits
|
|
1041
|
+
|
|
1042
|
+
2. **Decide** — based on diagnosis:
|
|
1043
|
+
- **If the worker needs direction:** Send targeted instructions via
|
|
1044
|
+
`send_agent_message(to, content)` with specific guidance on what to
|
|
1045
|
+
implement, which file to edit, or which approach to take.
|
|
1046
|
+
- **If the task is genuinely blocked:** Reply with `"skip"` or
|
|
1047
|
+
`"let it fail"` to close the session normally.
|
|
1048
|
+
|
|
1049
|
+
3. **Send your response** — The lane-runner polls for your reply for
|
|
1050
|
+
60 seconds. If you don't respond in time, the session closes and
|
|
1051
|
+
the normal corrective re-spawn mechanism takes over.
|
|
1052
|
+
|
|
1053
|
+
### Example Instructions
|
|
1054
|
+
|
|
1055
|
+
```
|
|
1056
|
+
send_agent_message(
|
|
1057
|
+
to: "orch-henrylach-lane-1-worker",
|
|
1058
|
+
content: "Stop analyzing and start implementing. Edit agent-host.ts line 605:
|
|
1059
|
+
replace the closeStdin() call with the interception logic described in
|
|
1060
|
+
PROMPT.md Step 1. Write the code now — don't read more files."
|
|
1061
|
+
)
|
|
1062
|
+
```
|
|
1063
|
+
|
|
1064
|
+
### Interception Limits
|
|
1065
|
+
|
|
1066
|
+
Each worker session can be intercepted at most **2 times** (configurable via
|
|
1067
|
+
`maxExitInterceptions`). After the limit is reached, the session closes
|
|
1068
|
+
normally and the stall detector handles subsequent iterations.
|
|
1014
1069
|
|
|
1015
1070
|
---
|
|
1016
1071
|
|
|
@@ -125,6 +125,11 @@ export interface ParsedTask {
|
|
|
125
125
|
* Null when no segment is active.
|
|
126
126
|
*/
|
|
127
127
|
activeSegmentId?: string | null;
|
|
128
|
+
/**
|
|
129
|
+
* Step-to-segment checkbox mapping parsed from PROMPT.md `#### Segment:` markers.
|
|
130
|
+
* Populated by discovery (Phase A, TP-173). Undefined if not yet parsed.
|
|
131
|
+
*/
|
|
132
|
+
stepSegmentMap?: StepSegmentMapping[];
|
|
128
133
|
}
|
|
129
134
|
|
|
130
135
|
/** Build a stable segment ID from task + repo identity (`<taskId>::<repoId>[::N]`). */
|
|
@@ -152,6 +157,21 @@ export function buildExpansionRequestId(timestamp = Date.now()): string {
|
|
|
152
157
|
return `exp-${ts}-${random5}`;
|
|
153
158
|
}
|
|
154
159
|
|
|
160
|
+
// ── Step-Segment Mapping (Phase A: segment-scoped worker visibility) ────
|
|
161
|
+
|
|
162
|
+
/** A group of checkboxes scoped to a single repo within a step. */
|
|
163
|
+
export interface SegmentCheckboxGroup {
|
|
164
|
+
repoId: string;
|
|
165
|
+
checkboxes: string[];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Maps a step to its repo-scoped checkbox groups. */
|
|
169
|
+
export interface StepSegmentMapping {
|
|
170
|
+
stepNumber: number;
|
|
171
|
+
stepName: string;
|
|
172
|
+
segments: SegmentCheckboxGroup[];
|
|
173
|
+
}
|
|
174
|
+
|
|
155
175
|
/** One repo-scoped segment node for a task. */
|
|
156
176
|
export interface TaskSegmentNode {
|
|
157
177
|
segmentId: SegmentId;
|
|
@@ -569,7 +589,10 @@ export interface DiscoveryError {
|
|
|
569
589
|
| "TASK_REPO_UNKNOWN"
|
|
570
590
|
| "TASK_ROUTING_STRICT"
|
|
571
591
|
| "SEGMENT_DAG_INVALID"
|
|
572
|
-
| "SEGMENT_REPO_UNKNOWN"
|
|
592
|
+
| "SEGMENT_REPO_UNKNOWN"
|
|
593
|
+
| "SEGMENT_STEP_DUPLICATE_REPO"
|
|
594
|
+
| "SEGMENT_STEP_EMPTY"
|
|
595
|
+
| "SEGMENT_STEP_REPO_INVALID";
|
|
573
596
|
message: string;
|
|
574
597
|
taskPath?: string;
|
|
575
598
|
taskId?: string;
|
|
@@ -593,6 +616,7 @@ export const FATAL_DISCOVERY_CODES: ReadonlyArray<DiscoveryError["code"]> = [
|
|
|
593
616
|
"TASK_ROUTING_STRICT",
|
|
594
617
|
"SEGMENT_DAG_INVALID",
|
|
595
618
|
"SEGMENT_REPO_UNKNOWN",
|
|
619
|
+
"SEGMENT_STEP_DUPLICATE_REPO",
|
|
596
620
|
] as const;
|
|
597
621
|
|
|
598
622
|
/** Result of the full discovery pipeline */
|
|
@@ -1109,8 +1133,20 @@ export interface OrchBatchRuntimeState {
|
|
|
1109
1133
|
waveResults: WaveExecutionResult[];
|
|
1110
1134
|
/** Current wave index (0-based into waves array, -1 if not started) */
|
|
1111
1135
|
currentWaveIndex: number;
|
|
1112
|
-
/** Total number of waves planned */
|
|
1136
|
+
/** Total number of waves planned (segment rounds — internal) */
|
|
1113
1137
|
totalWaves: number;
|
|
1138
|
+
/**
|
|
1139
|
+
* Number of dependency-driven task-level waves (TP-166).
|
|
1140
|
+
* Used for operator-facing "Wave X of Y" display. When undefined,
|
|
1141
|
+
* falls back to `totalWaves` for backward compatibility.
|
|
1142
|
+
*/
|
|
1143
|
+
taskLevelWaveCount?: number;
|
|
1144
|
+
/**
|
|
1145
|
+
* Maps each segment round index (0-based) to its parent task-level
|
|
1146
|
+
* wave index (0-based). Updated when continuation rounds are inserted.
|
|
1147
|
+
* Used with `resolveDisplayWaveNumber()` for correct display. (TP-166)
|
|
1148
|
+
*/
|
|
1149
|
+
roundToTaskWave?: number[];
|
|
1114
1150
|
/** Set of task IDs blocked for future waves (from skip-dependents policy) */
|
|
1115
1151
|
blockedTaskIds: Set<string>;
|
|
1116
1152
|
/** Epoch ms when batch started */
|
|
@@ -2020,6 +2056,7 @@ export type SupervisorAlertCategory =
|
|
|
2020
2056
|
| "merge-failure"
|
|
2021
2057
|
| "batch-complete"
|
|
2022
2058
|
| "agent-message"
|
|
2059
|
+
| "worker-exit-intercept"
|
|
2023
2060
|
| "segment-expansion-requested"
|
|
2024
2061
|
| "segment-expansion-approved"
|
|
2025
2062
|
| "segment-expansion-rejected";
|
|
@@ -2921,6 +2958,16 @@ export interface PersistedBatchState {
|
|
|
2921
2958
|
currentWaveIndex: number;
|
|
2922
2959
|
/** Total number of waves in the plan */
|
|
2923
2960
|
totalWaves: number;
|
|
2961
|
+
/**
|
|
2962
|
+
* Number of dependency-driven task-level waves (TP-166).
|
|
2963
|
+
* Undefined for batches created before TP-166; falls back to totalWaves.
|
|
2964
|
+
*/
|
|
2965
|
+
taskLevelWaveCount?: number;
|
|
2966
|
+
/**
|
|
2967
|
+
* Maps segment round index (0-based) to parent task-level wave (0-based).
|
|
2968
|
+
* Undefined for batches created before TP-166.
|
|
2969
|
+
*/
|
|
2970
|
+
roundToTaskWave?: number[];
|
|
2924
2971
|
/** Wave plan: array of arrays of task IDs per wave */
|
|
2925
2972
|
wavePlan: string[][];
|
|
2926
2973
|
/** Per-lane configuration records */
|
|
@@ -4017,7 +4064,9 @@ export type RuntimeAgentEventType =
|
|
|
4017
4064
|
// Review / bridge
|
|
4018
4065
|
| "review_requested"
|
|
4019
4066
|
| "review_completed"
|
|
4020
|
-
| "review_failed"
|
|
4067
|
+
| "review_failed"
|
|
4068
|
+
// Exit interception (TP-172)
|
|
4069
|
+
| "exit_intercepted";
|
|
4021
4070
|
|
|
4022
4071
|
// ── Runtime V2 Path Helpers (TP-102) ─────────────────────────────────
|
|
4023
4072
|
|
package/package.json
CHANGED
|
@@ -369,6 +369,64 @@ files and make sure their file scopes reflect that.
|
|
|
369
369
|
|
|
370
370
|
---
|
|
371
371
|
|
|
372
|
+
## Multi-Repo Segment Markers
|
|
373
|
+
|
|
374
|
+
When a task spans multiple repos (e.g., shared-libs + web-client), the skill
|
|
375
|
+
must generate **segment markers** inside each step so the orchestrator can
|
|
376
|
+
route checkboxes to the correct repo's worker.
|
|
377
|
+
|
|
378
|
+
### Workflow
|
|
379
|
+
|
|
380
|
+
1. Read workspace config to identify available repos and their roles
|
|
381
|
+
2. Analyze the task description and file scope — determine which repos are involved
|
|
382
|
+
3. Group work into steps by logical goal, with segments per repo within each step
|
|
383
|
+
4. Write PROMPT.md with `#### Segment: <repoId>` markers in every step
|
|
384
|
+
5. Write STATUS.md with matching structure
|
|
385
|
+
|
|
386
|
+
### Marker Format
|
|
387
|
+
|
|
388
|
+
Within each step, use level-4 headings to separate work by repo:
|
|
389
|
+
|
|
390
|
+
```markdown
|
|
391
|
+
### Step 1: Create utilities and API client
|
|
392
|
+
|
|
393
|
+
#### Segment: shared-libs
|
|
394
|
+
|
|
395
|
+
- [ ] Create string utility module
|
|
396
|
+
- [ ] Export from package index
|
|
397
|
+
|
|
398
|
+
#### Segment: web-client
|
|
399
|
+
|
|
400
|
+
- [ ] Add API client wrapper
|
|
401
|
+
- [ ] Wire into app initialization
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Ordering Rules
|
|
405
|
+
|
|
406
|
+
Order steps so that dependencies flow correctly:
|
|
407
|
+
|
|
408
|
+
1. **Shared/common work** → early steps (e.g., shared libraries, schemas)
|
|
409
|
+
2. **Per-repo implementation** → middle steps (consumers of shared work)
|
|
410
|
+
3. **Integration/documentation** → final steps (always in the packet repo)
|
|
411
|
+
|
|
412
|
+
The final documentation/delivery step always uses `#### Segment: <packet-repo>`
|
|
413
|
+
where `<packet-repo>` is the repo that contains the task's PROMPT.md.
|
|
414
|
+
|
|
415
|
+
### Guidelines
|
|
416
|
+
|
|
417
|
+
- **Always write explicit markers.** Never rely on the engine's single-segment
|
|
418
|
+
fallback for multi-repo tasks. Every step must have at least one
|
|
419
|
+
`#### Segment: <repoId>` marker.
|
|
420
|
+
- **Max 10 segments per task.** Tasks spanning more repos should be split into
|
|
421
|
+
separate tasks with dependencies.
|
|
422
|
+
- **Single-repo tasks do not need segment markers.** The engine's fallback
|
|
423
|
+
handles them correctly. Only add markers when file scope spans multiple repos.
|
|
424
|
+
- **When pre-decomposition isn't possible** (e.g., the worker must discover
|
|
425
|
+
which repos are affected), include guidance about using
|
|
426
|
+
`request_segment_expansion` for dynamic expansion at runtime.
|
|
427
|
+
|
|
428
|
+
---
|
|
429
|
+
|
|
372
430
|
## Preventing Empty Completions
|
|
373
431
|
|
|
374
432
|
Workers can shortcut tasks by observing that existing code "already satisfies"
|
|
@@ -88,6 +88,37 @@ Review Level 0 is ONLY for trivial changes. Most M+ tasks need Level ≥1.
|
|
|
88
88
|
**Artifacts:**
|
|
89
89
|
- `path/to/file` (new | modified)
|
|
90
90
|
|
|
91
|
+
> **Multi-repo variant:** When file scope spans multiple repos, use
|
|
92
|
+
> `#### Segment: <repoId>` markers within each step instead of flat checkboxes.
|
|
93
|
+
> Replace the single-repo Step 1 above with segment-annotated steps like:
|
|
94
|
+
>
|
|
95
|
+
> ```markdown
|
|
96
|
+
> ### Step 1: [Name]
|
|
97
|
+
>
|
|
98
|
+
> #### Segment: shared-libs
|
|
99
|
+
>
|
|
100
|
+
> - [ ] Create string utility module
|
|
101
|
+
> - [ ] Export from package index
|
|
102
|
+
>
|
|
103
|
+
> #### Segment: web-client
|
|
104
|
+
>
|
|
105
|
+
> - [ ] Add API client wrapper
|
|
106
|
+
> - [ ] Wire into app initialization
|
|
107
|
+
>
|
|
108
|
+
> ### Step [N]: Documentation & Delivery
|
|
109
|
+
>
|
|
110
|
+
> #### Segment: [packet-repo]
|
|
111
|
+
>
|
|
112
|
+
> - [ ] "Must Update" docs modified
|
|
113
|
+
> - [ ] Discoveries logged in STATUS.md
|
|
114
|
+
> ```
|
|
115
|
+
>
|
|
116
|
+
> Rules:
|
|
117
|
+
> - Always use explicit `#### Segment: <repoId>` markers (never rely on fallback)
|
|
118
|
+
> - Order: shared/common repos → per-repo impl → integration/docs (packet repo)
|
|
119
|
+
> - Final documentation/delivery step always uses the packet repo
|
|
120
|
+
> - Max 10 segments per task; split larger tasks with dependencies
|
|
121
|
+
|
|
91
122
|
### Step [N-1]: Testing & Verification
|
|
92
123
|
|
|
93
124
|
> ZERO test failures allowed. This step runs the FULL test suite as a quality gate.
|
|
@@ -191,6 +222,14 @@ this from PROMPT.md.
|
|
|
191
222
|
|
|
192
223
|
- [ ] [High-level placeholder — worker will expand]
|
|
193
224
|
|
|
225
|
+
[Multi-repo variant — use segment markers matching PROMPT.md:]
|
|
226
|
+
|
|
227
|
+
#### Segment: [repo-a]
|
|
228
|
+
- [ ] [Item in repo-a]
|
|
229
|
+
|
|
230
|
+
#### Segment: [repo-b]
|
|
231
|
+
- [ ] [Item in repo-b]
|
|
232
|
+
|
|
194
233
|
---
|
|
195
234
|
|
|
196
235
|
### Step [N-1]: Testing & Verification
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: task-worker-segment
|
|
3
|
+
description: Segment-scoped worker for multi-repo polyrepo tasks — works only on assigned segment checkboxes
|
|
4
|
+
tools: read,write,edit,bash,grep,find,ls
|
|
5
|
+
# model:
|
|
6
|
+
---
|
|
7
|
+
## Segment-Scoped Execution Rules
|
|
8
|
+
|
|
9
|
+
You are executing ONE SEGMENT of a multi-segment polyrepo task. Your iteration
|
|
10
|
+
prompt lists which checkboxes are yours under "Your checkboxes for this step:".
|
|
11
|
+
|
|
12
|
+
**YOUR RULES (these override any conflicting general rules):**
|
|
13
|
+
|
|
14
|
+
1. **Only work on YOUR checkboxes** — the ones listed under "Your checkboxes
|
|
15
|
+
for this step:" in your iteration prompt. Do NOT work on checkboxes listed
|
|
16
|
+
under "Other segments in this step (NOT yours)."
|
|
17
|
+
|
|
18
|
+
2. **When all YOUR checkboxes are checked, your segment is done — exit.**
|
|
19
|
+
Do not continue to other steps. Do not look for more work. Your segment
|
|
20
|
+
is complete.
|
|
21
|
+
|
|
22
|
+
3. **Do NOT modify files in repos not available in your worktree.** You are
|
|
23
|
+
in a specific repo's worktree. Files in other repos are not accessible.
|
|
24
|
+
|
|
25
|
+
4. **If you discover work needed in another repo**, use `request_segment_expansion`
|
|
26
|
+
with step definitions describing what the next segment's worker should do.
|
|
27
|
+
Include a `context` field explaining what you built and what the next worker
|
|
28
|
+
needs to know.
|
|
29
|
+
|
|
30
|
+
5. **If your assigned checkbox list is empty**, do NOT exit as complete. Log a
|
|
31
|
+
blocker in STATUS.md and escalate — something is wrong with the task setup.
|
|
32
|
+
|
|
33
|
+
## Context from Prior Segments
|
|
34
|
+
|
|
35
|
+
If your prompt includes "Context from prior segment," this was written by a
|
|
36
|
+
worker who discovered the need for your work. Read it carefully — it contains
|
|
37
|
+
knowledge about what was built in a prior segment that you need to build on.
|
|
38
|
+
|
|
39
|
+
## Checkpoint Discipline
|
|
40
|
+
|
|
41
|
+
Same as the base worker prompt: check off each checkbox IMMEDIATELY after
|
|
42
|
+
completing it. Commit at step boundaries. The only difference is that your
|
|
43
|
+
"step" may contain only a subset of the full step's checkboxes (your segment's
|
|
44
|
+
portion).
|