@ordewell/core 0.6.0 → 0.6.1
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/dist/{IFileSystem-D4dAl2r8.d.ts → IFileSystem-DuEiG_kA.d.ts} +1 -1
- package/dist/{IFileSystem-CFYf4yR2.d.mts → IFileSystem-d_SV7jAH.d.mts} +1 -1
- package/dist/{ModeResolver-a4xJFlEp.d.mts → ModeResolver-DIReYAOq.d.mts} +1 -1
- package/dist/{ModeResolver-DVYVRbJp.d.ts → ModeResolver-DXxV4n_V.d.ts} +1 -1
- package/dist/{Task-CZJr6rmz.d.mts → Task-ib_U0YP7.d.mts} +56 -5
- package/dist/{Task-CZJr6rmz.d.ts → Task-ib_U0YP7.d.ts} +56 -5
- package/dist/{chunk-2SOVKO7P.mjs → chunk-I6U3PVHL.mjs} +39 -16
- package/dist/chunk-I6U3PVHL.mjs.map +1 -0
- package/dist/{chunk-EDGUFCIR.mjs → chunk-JRZPSNRO.mjs} +7 -3
- package/dist/chunk-JRZPSNRO.mjs.map +1 -0
- package/dist/{chunk-OM4D274O.mjs → chunk-M4KXBEFJ.mjs} +5 -3
- package/dist/chunk-M4KXBEFJ.mjs.map +1 -0
- package/dist/{chunk-C44UWIAD.mjs → chunk-N66ZHFGN.mjs} +2 -2
- package/dist/chunk-N66ZHFGN.mjs.map +1 -0
- package/dist/index.d.mts +129 -33
- package/dist/index.d.ts +129 -33
- package/dist/index.js +445 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -60
- package/dist/index.mjs.map +1 -1
- package/dist/order-labels.d.mts +1 -1
- package/dist/order-labels.d.ts +1 -1
- package/dist/{parsing-Bd6-b644.d.ts → parsing-C8Z22twN.d.ts} +2 -2
- package/dist/{parsing-DIaGZmiQ.d.mts → parsing-DjRJJX-y.d.mts} +2 -2
- package/dist/parsing.d.mts +3 -3
- package/dist/parsing.d.ts +3 -3
- package/dist/parsing.js +5 -2
- package/dist/parsing.js.map +1 -1
- package/dist/parsing.mjs +2 -2
- package/dist/{plan-utils-Ba-l8EZS.d.mts → plan-utils-D4ioaM8q.d.mts} +25 -4
- package/dist/{plan-utils-_T5S7Mnk.d.ts → plan-utils-DvHPzAfV.d.ts} +25 -4
- package/dist/plan-utils.d.mts +3 -3
- package/dist/plan-utils.d.ts +3 -3
- package/dist/plan-utils.js.map +1 -1
- package/dist/plan-utils.mjs +2 -2
- package/dist/testing.d.mts +14 -5
- package/dist/testing.d.ts +14 -5
- package/dist/testing.js +30 -3
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +30 -3
- package/dist/testing.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-2SOVKO7P.mjs.map +0 -1
- package/dist/chunk-C44UWIAD.mjs.map +0 -1
- package/dist/chunk-EDGUFCIR.mjs.map +0 -1
- package/dist/chunk-OM4D274O.mjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -400,7 +400,7 @@ function resolveOrderLabel(tasks, label) {
|
|
|
400
400
|
|
|
401
401
|
// src/models/Task.ts
|
|
402
402
|
function isAwaitingReason(value) {
|
|
403
|
-
return value === "input" || value === "checkpoint" || value === "conflict";
|
|
403
|
+
return value === "input" || value === "checkpoint" || value === "conflict" || value === "files-changed";
|
|
404
404
|
}
|
|
405
405
|
function emptyWarnings() {
|
|
406
406
|
return { deletedCompleted: [], changedCompleted: [], deletedInProgress: [], modifiedInProgress: [], brokenDependencies: [] };
|
|
@@ -548,7 +548,8 @@ function createTask(overrides = {}) {
|
|
|
548
548
|
completionMarker: overrides.completionMarker ?? (0, import_uuid.v4)(),
|
|
549
549
|
autonomy: overrides.autonomy,
|
|
550
550
|
sliceType: overrides.sliceType,
|
|
551
|
-
userStoriesCovered: overrides.userStoriesCovered
|
|
551
|
+
userStoriesCovered: overrides.userStoriesCovered,
|
|
552
|
+
...overrides.ops === true && (overrides.type ?? "ai") === "ai" ? { ops: true } : {}
|
|
552
553
|
};
|
|
553
554
|
}
|
|
554
555
|
function createEmptyPlan() {
|
|
@@ -648,6 +649,9 @@ function keepExecutionState(current, rewrite) {
|
|
|
648
649
|
verdict: prior?.verdict,
|
|
649
650
|
outputSummary: prior?.outputSummary,
|
|
650
651
|
transport: prior?.transport,
|
|
652
|
+
// Where a task that has run ran is fixed, like its transport (ADR-0020).
|
|
653
|
+
...prior?.status === "failed" ? { ops: prior.ops } : {},
|
|
654
|
+
forcedPastGate: prior?.forcedPastGate,
|
|
651
655
|
subtasks: overlay(t.subtasks ?? [], prior?.subtasks ?? [])
|
|
652
656
|
};
|
|
653
657
|
});
|
|
@@ -6168,7 +6172,7 @@ function taskIsolationOf(record, repairLimit) {
|
|
|
6168
6172
|
};
|
|
6169
6173
|
}
|
|
6170
6174
|
function handoffOf(run) {
|
|
6171
|
-
const merged = Object.values(run.tasks).filter((r) => r.status === "merged").sort((a, b) => a.order - b.order);
|
|
6175
|
+
const merged = Object.values(run.tasks).filter((r) => r.status === "merged" && !r.inHead).sort((a, b) => a.order - b.order);
|
|
6172
6176
|
const entry = (r) => ({
|
|
6173
6177
|
taskId: r.taskId,
|
|
6174
6178
|
order: r.order,
|
|
@@ -6550,6 +6554,18 @@ function leftoverMarkerFiles(check) {
|
|
|
6550
6554
|
const files = check.split(/\r?\n/).flatMap((line) => /^(.+):\d+: leftover conflict marker$/.exec(line)?.[1] ?? []);
|
|
6551
6555
|
return [...new Set(files)];
|
|
6552
6556
|
}
|
|
6557
|
+
function rawDiffStates(raw) {
|
|
6558
|
+
const parts = raw.split("\0");
|
|
6559
|
+
const states = {};
|
|
6560
|
+
for (let i = 0; i + 1 < parts.length; i += 2) {
|
|
6561
|
+
const meta = parts[i].trim();
|
|
6562
|
+
const file = parts[i + 1];
|
|
6563
|
+
if (!meta.startsWith(":") || !file) continue;
|
|
6564
|
+
const [, mode, , blob] = meta.slice(1).split(" ");
|
|
6565
|
+
states[file] = `${mode}:${blob}`;
|
|
6566
|
+
}
|
|
6567
|
+
return states;
|
|
6568
|
+
}
|
|
6553
6569
|
function settleStatus(record, status) {
|
|
6554
6570
|
record.status = status;
|
|
6555
6571
|
delete record.repairBase;
|
|
@@ -6649,6 +6665,8 @@ var GitWorktreeIsolation = class {
|
|
|
6649
6665
|
adminChains = /* @__PURE__ */ new Map();
|
|
6650
6666
|
waiting = [];
|
|
6651
6667
|
draining = false;
|
|
6668
|
+
/** Landings and Merge all take turns on this, so a merge never reads an integration branch mid-landing. */
|
|
6669
|
+
turns = Promise.resolve();
|
|
6652
6670
|
async isActive(workspaceRoot) {
|
|
6653
6671
|
if (!this.deps.config.worktreeIsolation) return { active: false, reason: "disabled" };
|
|
6654
6672
|
const version = await this.tryGit(void 0, ["--version"]);
|
|
@@ -7005,6 +7023,62 @@ var GitWorktreeIsolation = class {
|
|
|
7005
7023
|
return !linked.has(rel) && !linked.has(rel.split("/")[0]);
|
|
7006
7024
|
});
|
|
7007
7025
|
}
|
|
7026
|
+
async findInHead(run) {
|
|
7027
|
+
const found = [];
|
|
7028
|
+
for (const record of Object.values(run.tasks)) {
|
|
7029
|
+
if (record.status !== "merged" || record.inHead) continue;
|
|
7030
|
+
if (!await this.headContains(run, record)) continue;
|
|
7031
|
+
record.inHead = true;
|
|
7032
|
+
found.push(record.taskId);
|
|
7033
|
+
}
|
|
7034
|
+
return found;
|
|
7035
|
+
}
|
|
7036
|
+
async headContains(run, record) {
|
|
7037
|
+
for (const repo of run.repos) {
|
|
7038
|
+
const entry = record.repos[repo.path];
|
|
7039
|
+
if (!entry?.changed) continue;
|
|
7040
|
+
const merged = await this.tryGit(repo.root, ["merge-base", "--is-ancestor", entry.landedTip ?? repo.integrationBranch, "HEAD"]);
|
|
7041
|
+
if (!merged.ok) return false;
|
|
7042
|
+
}
|
|
7043
|
+
return true;
|
|
7044
|
+
}
|
|
7045
|
+
async snapshotTree(workspaceRoot, exclude) {
|
|
7046
|
+
const scan = await this.scanGroup(workspaceRoot);
|
|
7047
|
+
if ("refused" in scan) return null;
|
|
7048
|
+
const repos = (await this.committedRepos(workspaceRoot, scan.paths)).filter((repoPath) => !exclude.includes(repoPath));
|
|
7049
|
+
if (repos.length === 0) return null;
|
|
7050
|
+
const snapshot = {};
|
|
7051
|
+
for (const repoPath of repos) {
|
|
7052
|
+
const states = await this.trackedStates(repoRootOf(workspaceRoot, repoPath));
|
|
7053
|
+
if (states) snapshot[repoPath] = states;
|
|
7054
|
+
}
|
|
7055
|
+
return snapshot;
|
|
7056
|
+
}
|
|
7057
|
+
async changedSince(workspaceRoot, snapshot) {
|
|
7058
|
+
const changed = [];
|
|
7059
|
+
for (const [repoPath, before] of Object.entries(snapshot)) {
|
|
7060
|
+
const now = await this.trackedStates(repoRootOf(workspaceRoot, repoPath));
|
|
7061
|
+
if (!now) continue;
|
|
7062
|
+
for (const [file, state] of Object.entries(now)) {
|
|
7063
|
+
if (before[file] !== state) changed.push(repoPath === SELF_REPO ? file : `${repoPath}/${file}`);
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
return changed.sort();
|
|
7067
|
+
}
|
|
7068
|
+
/**
|
|
7069
|
+
* Each tracked file that differs from HEAD, with what it holds. `git stash
|
|
7070
|
+
* create` records the working tree as a commit without touching the tree,
|
|
7071
|
+
* the index or the stash, so a file changed again is told apart from one
|
|
7072
|
+
* left as it was. Null when git cannot tell — a merge in progress, say.
|
|
7073
|
+
*/
|
|
7074
|
+
async trackedStates(root) {
|
|
7075
|
+
const stash = await this.tryGit(root, ["stash", "create"]);
|
|
7076
|
+
if (!stash.ok) return null;
|
|
7077
|
+
const commit = stash.stdout.trim();
|
|
7078
|
+
if (!commit) return {};
|
|
7079
|
+
const diff = await this.tryGit(root, ["diff", "--raw", "--no-abbrev", "--no-renames", "-z", "HEAD", commit]);
|
|
7080
|
+
return diff.ok ? rawDiffStates(diff.stdout) : null;
|
|
7081
|
+
}
|
|
7008
7082
|
/**
|
|
7009
7083
|
* One section per repo with changes. A repo below the workspace root gets a
|
|
7010
7084
|
* header and paths prefixed with its own, so the whole reads as one patch
|
|
@@ -7022,7 +7096,10 @@ var GitWorktreeIsolation = class {
|
|
|
7022
7096
|
}
|
|
7023
7097
|
return diff;
|
|
7024
7098
|
}
|
|
7025
|
-
|
|
7099
|
+
mergeIntoCheckedOut(run) {
|
|
7100
|
+
return this.inTurn(() => this.mergeAll(run));
|
|
7101
|
+
}
|
|
7102
|
+
async mergeAll(run) {
|
|
7026
7103
|
const partial = await this.settleLanding(run);
|
|
7027
7104
|
if (partial.length > 0) return { outcome: "blocked", blocked: partial.map((repo) => ({ repo, reason: "partial-landing", files: [] })) };
|
|
7028
7105
|
const repos = await this.reposWithWork(run);
|
|
@@ -7156,7 +7233,7 @@ var GitWorktreeIsolation = class {
|
|
|
7156
7233
|
if (entry2.run.id === runId && (pick < 0 || entry2.task.order < this.waiting[pick].task.order)) pick = i;
|
|
7157
7234
|
});
|
|
7158
7235
|
const [entry] = this.waiting.splice(pick, 1);
|
|
7159
|
-
entry.settle(await this.land(entry.task, entry.run, entry.persist).catch(() => "failed"));
|
|
7236
|
+
entry.settle(await this.inTurn(() => this.land(entry.task, entry.run, entry.persist)).catch(() => "failed"));
|
|
7160
7237
|
}
|
|
7161
7238
|
} finally {
|
|
7162
7239
|
this.draining = false;
|
|
@@ -7208,6 +7285,10 @@ var GitWorktreeIsolation = class {
|
|
|
7208
7285
|
return (await this.settleLanding(run)).length === 0 ? this.stopLanding(record, outcome, repo, files, error) : this.stopLanding(record, "failed", repo);
|
|
7209
7286
|
}
|
|
7210
7287
|
}
|
|
7288
|
+
for (const repo of changed) {
|
|
7289
|
+
const tip = await this.tryGit(repo.root, ["rev-parse", "--verify", "-q", repo.integrationBranch]);
|
|
7290
|
+
if (tip.ok) record.repos[repo.path].landedTip = tip.stdout.trim();
|
|
7291
|
+
}
|
|
7211
7292
|
delete run.landing;
|
|
7212
7293
|
settleStatus(record, "merged");
|
|
7213
7294
|
delete record.conflictRepo;
|
|
@@ -7482,6 +7563,11 @@ var GitWorktreeIsolation = class {
|
|
|
7482
7563
|
async abortMerge(cwd) {
|
|
7483
7564
|
if (!(await this.tryGit(cwd, ["merge", "--abort"])).ok) await this.tryGit(cwd, ["reset", "--hard"]);
|
|
7484
7565
|
}
|
|
7566
|
+
inTurn(fn) {
|
|
7567
|
+
const turn = this.turns.then(fn, fn);
|
|
7568
|
+
this.turns = turn.catch(() => void 0);
|
|
7569
|
+
return turn;
|
|
7570
|
+
}
|
|
7485
7571
|
admin(repoKey, fn) {
|
|
7486
7572
|
const prior = this.adminChains.get(repoKey) ?? Promise.resolve();
|
|
7487
7573
|
const next = prior.then(fn, fn);
|
|
@@ -8155,8 +8241,22 @@ function researchPhaseBlock(harnessMode) {
|
|
|
8155
8241
|
"- Research and questions can interleave \u2014 you can explore a file, then ask a question grounded in it, in one response"
|
|
8156
8242
|
].join("\n");
|
|
8157
8243
|
}
|
|
8158
|
-
var ISOLATED_PARALLELISM_RULE = "- Each
|
|
8244
|
+
var ISOLATED_PARALLELISM_RULE = "- Each change task runs in its own git worktree, and its work is merged in afterwards, so tasks that edit the same files can still run in parallel. Never add a dependency just because two tasks touch the same file \u2014 keep dependencies for genuine logical ordering.";
|
|
8159
8245
|
var OVERLAP_AVOIDANCE_RULE = "- For parallel tasks, specify different target files to avoid merge conflicts.";
|
|
8246
|
+
function opsTaskSection(conversation) {
|
|
8247
|
+
return [
|
|
8248
|
+
"",
|
|
8249
|
+
"CHANGE AND OPS TASKS:",
|
|
8250
|
+
"- Every AI task is either a change task or an ops task. Decide by one test: is the task's result a change to files in the repository?",
|
|
8251
|
+
'- A change task (the default) edits repository files. Omit "ops".',
|
|
8252
|
+
`- An ops task changes no repository files: it acts on systems outside the repository (a cloud CLI, a deployment, a pipeline, a provisioned resource) or on the git refs and history of the user's branch (push, tag, reword commits). Set "ops": true. Only top-level "ai" tasks can be ops; a subtask runs with its parent.`,
|
|
8253
|
+
`- Ops tasks run in the user's own checkout, after the user has merged the change tasks they depend on. Give an ops task a dependency on every change task whose result it acts on \u2014 "deploy the fix" depends on the fix.`,
|
|
8254
|
+
'- Split a request that mixes the two into a change task and an ops task that depends on it: "bump the version and redeploy" is a change task that bumps and commits the version, then an ops task that pushes and watches the pipeline.',
|
|
8255
|
+
"- Ops tasks run in parallel as their dependencies allow. Two ops tasks whose git operations would collide (a push and a reword of the same branch) must not run in parallel \u2014 make one depend on the other.",
|
|
8256
|
+
"- An ops task runs until what it acts on has finished: one that watches a pipeline reports done only when the pipeline has ended.",
|
|
8257
|
+
...conversation ? [`- A question is not a task. "What is the frontend's URL?" is answered in this conversation, from your research; never make a task of it.`] : []
|
|
8258
|
+
];
|
|
8259
|
+
}
|
|
8160
8260
|
var SHARED_APPEND_FILE_RULE = "- Tasks that run in parallel must not each append to the same shared file (changelogs, registries, index lists) \u2014 give that edit to the final task, or to one task the others depend on.";
|
|
8161
8261
|
function repoGroupSection(isolated) {
|
|
8162
8262
|
if (!isolated) return [];
|
|
@@ -8236,6 +8336,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
8236
8336
|
' "taskMode": "' + modeExamples + '",',
|
|
8237
8337
|
' "autonomy": "AFK|HITL",',
|
|
8238
8338
|
' "sliceType": "AFK|HITL",',
|
|
8339
|
+
' "ops": true,',
|
|
8239
8340
|
' "userStoriesCovered": ["user story text"],',
|
|
8240
8341
|
' "subtasks": [{ "id": "sub-id-string", "order": 1, "title": "Sub-step title", "description": "What it accomplishes", "type": "ai", "dependencies": [], "prompt": "Detailed instructions", "autonomy": "AFK|HITL", "sliceType": "AFK|HITL", "subtasks": [] }]',
|
|
8241
8342
|
" }",
|
|
@@ -8261,6 +8362,7 @@ function buildConversationBody(goal, context, modelsJson, runners, modeGuide, mo
|
|
|
8261
8362
|
SHARED_APPEND_FILE_RULE,
|
|
8262
8363
|
...variant.isolatedExecution ? [ISOLATED_PARALLELISM_RULE] : [],
|
|
8263
8364
|
...repoGroupSection(variant.isolatedExecution),
|
|
8365
|
+
...opsTaskSection(true),
|
|
8264
8366
|
"",
|
|
8265
8367
|
"RULES:",
|
|
8266
8368
|
"- Do NOT wrap the JSON in markdown code blocks. Output ONLY the JSON object when committing the plan.",
|
|
@@ -8336,6 +8438,7 @@ function corePlannerPrompt(isolatedExecution) {
|
|
|
8336
8438
|
' "taskMode": "{{MODE_EXAMPLES}}",',
|
|
8337
8439
|
' "autonomy": "AFK|HITL",',
|
|
8338
8440
|
' "sliceType": "AFK|HITL",',
|
|
8441
|
+
' "ops": true,',
|
|
8339
8442
|
' "userStoriesCovered": ["user story text"],',
|
|
8340
8443
|
' "subtasks": [{ "id": "sub-id-string", "order": 1, "title": "Sub-step title", "description": "What it accomplishes", "type": "ai", "dependencies": [], "prompt": "Detailed instructions", "autonomy": "AFK|HITL", "sliceType": "AFK|HITL", "subtasks": [] }]',
|
|
8341
8444
|
" }",
|
|
@@ -8363,6 +8466,7 @@ function corePlannerPrompt(isolatedExecution) {
|
|
|
8363
8466
|
SHARED_APPEND_FILE_RULE,
|
|
8364
8467
|
isolatedExecution ? ISOLATED_PARALLELISM_RULE : "- Slices that touch different areas of the codebase are naturally parallel.",
|
|
8365
8468
|
...repoGroupSection(isolatedExecution),
|
|
8469
|
+
...opsTaskSection(false),
|
|
8366
8470
|
"",
|
|
8367
8471
|
"RULES:",
|
|
8368
8472
|
'- Mark as type "ai" any task the coding assistant can do autonomously.',
|
|
@@ -8759,7 +8863,9 @@ function parseTask(raw, runners, runnerModes, autonomousDefault = true, inherite
|
|
|
8759
8863
|
),
|
|
8760
8864
|
autonomy,
|
|
8761
8865
|
sliceType,
|
|
8762
|
-
userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : void 0
|
|
8866
|
+
userStoriesCovered: Array.isArray(raw.userStoriesCovered) ? raw.userStoriesCovered.map(String) : void 0,
|
|
8867
|
+
// A subtask runs with its parent, so only a top-level AI task carries it (ADR-0020).
|
|
8868
|
+
ops: !inherited && taskType === "ai" && raw.ops === true
|
|
8763
8869
|
});
|
|
8764
8870
|
}
|
|
8765
8871
|
function validateVerticalSliceShape(tasks, raw, parentTitle) {
|
|
@@ -9013,7 +9119,7 @@ function coerceAssignments(tasks, perRunnerAllowlist, allowedRunners, modelsByRu
|
|
|
9013
9119
|
}
|
|
9014
9120
|
|
|
9015
9121
|
// src/services/TaskEditValidator.ts
|
|
9016
|
-
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy"];
|
|
9122
|
+
var AI_ONLY_FIELDS = ["assignedModel", "thinkingEffort", "taskMode", "autonomy", "ops"];
|
|
9017
9123
|
function checkModelAndModeValidity(runner, assignedModel, taskMode, catalog) {
|
|
9018
9124
|
if (!catalog) return { ok: true };
|
|
9019
9125
|
if (assignedModel) {
|
|
@@ -9050,6 +9156,19 @@ function typeCoherenceCheck(target, changes) {
|
|
|
9050
9156
|
}
|
|
9051
9157
|
return { ok: true, clear: target.userSteps && target.userSteps.length > 0 ? ["userSteps"] : [] };
|
|
9052
9158
|
}
|
|
9159
|
+
var NOT_STARTED = /* @__PURE__ */ new Set(["pending", "approved", "blocked"]);
|
|
9160
|
+
function opsCheck(tasks, target, changes) {
|
|
9161
|
+
if (!("ops" in changes) || !!changes.ops === !!target.ops) return null;
|
|
9162
|
+
if (!NOT_STARTED.has(target.status)) {
|
|
9163
|
+
return { ok: false, error: `"${target.title}" has started, so whether it is an ops task is fixed` };
|
|
9164
|
+
}
|
|
9165
|
+
if (!changes.ops) return null;
|
|
9166
|
+
if ((changes.type ?? target.type) !== "ai") return { ok: false, error: `"${target.title}" is a manual task \u2014 only an AI task can be an ops task` };
|
|
9167
|
+
if (tasks.some((t) => t.subtasks?.some((s) => s.id === target.id))) {
|
|
9168
|
+
return { ok: false, error: `"${target.title}" is a subtask, which runs with its parent \u2014 only a top-level task can be an ops task` };
|
|
9169
|
+
}
|
|
9170
|
+
return null;
|
|
9171
|
+
}
|
|
9053
9172
|
function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
9054
9173
|
const target = tasks.find((t) => t.id === taskId);
|
|
9055
9174
|
if (!target) return { ok: false, error: "Task not found" };
|
|
@@ -9063,6 +9182,8 @@ function validateTaskEdit(actor, tasks, taskId, changes, catalog) {
|
|
|
9063
9182
|
}
|
|
9064
9183
|
const coherence = typeCoherenceCheck(target, changes);
|
|
9065
9184
|
if (coherence && !coherence.ok) return coherence;
|
|
9185
|
+
const ops = opsCheck(tasks, target, changes);
|
|
9186
|
+
if (ops) return ops;
|
|
9066
9187
|
const runner = changes.assignedRunner ?? target.assignedRunner;
|
|
9067
9188
|
const validity = checkModelAndModeValidity(runner, changes.assignedModel, changes.taskMode, catalog);
|
|
9068
9189
|
if (!validity.ok) return validity;
|
|
@@ -9082,7 +9203,8 @@ var UPDATABLE_FIELDS = [
|
|
|
9082
9203
|
"type",
|
|
9083
9204
|
"userSteps",
|
|
9084
9205
|
"autonomy",
|
|
9085
|
-
"sliceType"
|
|
9206
|
+
"sliceType",
|
|
9207
|
+
"ops"
|
|
9086
9208
|
];
|
|
9087
9209
|
function taskOpsProtocol(executionNote = "") {
|
|
9088
9210
|
const updateChanges = UPDATABLE_FIELDS.map((f) => f === "dependencies" ? '"dependencies"?:["<id or #order>"]' : `"${f}"?`).join(",");
|
|
@@ -9090,7 +9212,7 @@ function taskOpsProtocol(executionNote = "") {
|
|
|
9090
9212
|
"- To modify specific tasks, reply with ONLY this JSON object:",
|
|
9091
9213
|
' {"taskOps": [',
|
|
9092
9214
|
` {"op":"update","taskId":"<id or #order>","changes":{${updateChanges}}},`,
|
|
9093
|
-
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?},"handle"?:"<name>"},',
|
|
9215
|
+
' {"op":"add","task":{"title","description","prompt","dependencies":["<id or #order>"],"assignedRunner"?,"assignedModel"?,"ops"?},"handle"?:"<name>"},',
|
|
9094
9216
|
' {"op":"remove","taskId":"<id or #order>"},',
|
|
9095
9217
|
' {"op":"reorder","taskIds":["<id or #order>", "... every task exactly once"]}, // only to re-prioritise INDEPENDENT tasks',
|
|
9096
9218
|
' {"op":"merge","taskIds":["<id or #order>", "..."],"merged":{"title","description","prompt"?,"assignedRunner"?,"assignedModel"?,...},"handle"?:"<name>"},',
|
|
@@ -9193,6 +9315,14 @@ function refCollidesWithExisting(name, originalFlatAll) {
|
|
|
9193
9315
|
if (/^\d+$/.test(orderStr) && originalFlatAll.some((t) => t.order === Number(orderStr))) return true;
|
|
9194
9316
|
return originalFlatAll.some((t) => t.title === name);
|
|
9195
9317
|
}
|
|
9318
|
+
function updatableChanges(raw) {
|
|
9319
|
+
const changes = {};
|
|
9320
|
+
for (const key of UPDATABLE_FIELDS) {
|
|
9321
|
+
if (raw && key in raw) changes[key] = raw[key];
|
|
9322
|
+
}
|
|
9323
|
+
if ("ops" in changes) changes.ops = changes.ops === true ? true : void 0;
|
|
9324
|
+
return changes;
|
|
9325
|
+
}
|
|
9196
9326
|
function findInTasks(tasks, id) {
|
|
9197
9327
|
return flattenTasks(tasks).find((t) => t.id === id);
|
|
9198
9328
|
}
|
|
@@ -9374,10 +9504,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9374
9504
|
errors.push(`${label}: task "${op.taskId}" no longer exists in this batch (removed, merged, or split by an earlier op)`);
|
|
9375
9505
|
break;
|
|
9376
9506
|
}
|
|
9377
|
-
const changes =
|
|
9378
|
-
for (const key of UPDATABLE_FIELDS) {
|
|
9379
|
-
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
9380
|
-
}
|
|
9507
|
+
const changes = updatableChanges(op.changes);
|
|
9381
9508
|
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
9382
9509
|
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
9383
9510
|
break;
|
|
@@ -9529,6 +9656,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9529
9656
|
taskMode: spec.taskMode ?? toMerge[0].taskMode,
|
|
9530
9657
|
autonomy: spec.autonomy ?? toMerge.find((t) => t.autonomy)?.autonomy,
|
|
9531
9658
|
sliceType: spec.sliceType ?? toMerge.find((t) => t.sliceType)?.sliceType,
|
|
9659
|
+
ops: spec.ops ?? toMerge.every((t) => t.ops),
|
|
9532
9660
|
userStoriesCovered: spec.userStoriesCovered ?? (toMerge.flatMap((t) => t.userStoriesCovered ?? []).length ? [...new Set(toMerge.flatMap((t) => t.userStoriesCovered ?? []))] : void 0)
|
|
9533
9661
|
});
|
|
9534
9662
|
const mergedId = mergedTask.id;
|
|
@@ -9609,6 +9737,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9609
9737
|
taskMode: spec.taskMode ?? target.taskMode,
|
|
9610
9738
|
autonomy: spec.autonomy ?? target.autonomy,
|
|
9611
9739
|
sliceType: spec.sliceType ?? target.sliceType,
|
|
9740
|
+
ops: spec.ops ?? target.ops,
|
|
9612
9741
|
userStoriesCovered: spec.userStoriesCovered ?? target.userStoriesCovered
|
|
9613
9742
|
});
|
|
9614
9743
|
newTasks.push(nt);
|
|
@@ -9644,10 +9773,7 @@ function applyTaskOps(currentTasks, ops, runners, catalog) {
|
|
|
9644
9773
|
errors.push(`${label}: "${target.title}" is running and cannot be re-armed`);
|
|
9645
9774
|
break;
|
|
9646
9775
|
}
|
|
9647
|
-
const changes =
|
|
9648
|
-
for (const key of UPDATABLE_FIELDS) {
|
|
9649
|
-
if (op.changes && key in op.changes) changes[key] = op.changes[key];
|
|
9650
|
-
}
|
|
9776
|
+
const changes = updatableChanges(op.changes);
|
|
9651
9777
|
if (changes.assignedRunner && !runners.includes(changes.assignedRunner)) {
|
|
9652
9778
|
errors.push(`${label}: runner "${changes.assignedRunner}" is not in this plan's runner set [${runners.join(", ")}]`);
|
|
9653
9779
|
break;
|
|
@@ -11477,6 +11603,17 @@ You are running as: ${currentTask.assignedRunner}` : "";
|
|
|
11477
11603
|
lines.join("\n") + footer + runnerNote
|
|
11478
11604
|
].join("\n");
|
|
11479
11605
|
}
|
|
11606
|
+
function renderPreviousAttempt(output) {
|
|
11607
|
+
const tail = output.trim() ? defuseMarkers(output.trim()).split("\n").map((l) => " " + l).join("\n") : " (no output captured)";
|
|
11608
|
+
return [
|
|
11609
|
+
"## Previous attempt",
|
|
11610
|
+
"",
|
|
11611
|
+
"This task ran before and did not finish. What it did outside the repository was not undone: before acting, check what already exists \u2014 resources, deployments, pushed refs \u2014 and do not repeat a step that already took effect.",
|
|
11612
|
+
"",
|
|
11613
|
+
"The end of its output:",
|
|
11614
|
+
tail
|
|
11615
|
+
].join("\n");
|
|
11616
|
+
}
|
|
11480
11617
|
function renderCompletionMarker(task) {
|
|
11481
11618
|
return `
|
|
11482
11619
|
|
|
@@ -11528,6 +11665,7 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
11528
11665
|
const flat = flattenTasks(allTasks);
|
|
11529
11666
|
const outputs = collectDirectDependencyOutputs(task, flat);
|
|
11530
11667
|
if (outputs.length > 0) blocks.push(renderPriorOutputs(outputs));
|
|
11668
|
+
if (opts?.previousAttempt !== void 0) blocks.push(renderPreviousAttempt(opts.previousAttempt));
|
|
11531
11669
|
if (opts?.tddEnabled) {
|
|
11532
11670
|
blocks.push(renderTddInstruction());
|
|
11533
11671
|
}
|
|
@@ -11540,9 +11678,9 @@ function composeAugmentedPrompt(task, allTasks, opts) {
|
|
|
11540
11678
|
|
|
11541
11679
|
${basePrompt}${marker2}`;
|
|
11542
11680
|
}
|
|
11543
|
-
function composeContinuationPrompt(task, message) {
|
|
11681
|
+
function composeContinuationPrompt(task, message, opts = {}) {
|
|
11544
11682
|
const reminder = [
|
|
11545
|
-
"(Ordewell) You are continuing this task in the same session. Your working directory was recreated from the integration branch: work from your earlier attempt is there only if it landed, so check the files before relying on them."
|
|
11683
|
+
opts.ops ? "(Ordewell) You are continuing this task in the same session, in the same checkout. What your earlier attempt did outside the repository was not undone: check what already exists before acting again." : "(Ordewell) You are continuing this task in the same session. Your working directory was recreated from the integration branch: work from your earlier attempt is there only if it landed, so check the files before relying on them."
|
|
11546
11684
|
];
|
|
11547
11685
|
if (isHitlTask(task)) {
|
|
11548
11686
|
reminder.push(`If you reach a decision that needs human judgment, print one line holding only the checkpoint marker and wait for ORDEWELL_CONTINUE or ORDEWELL_REJECT. ${CHECKPOINT_MARKER_HOWTO}.`);
|
|
@@ -12282,6 +12420,8 @@ var PlanStore = class {
|
|
|
12282
12420
|
_planTasks = [];
|
|
12283
12421
|
_allTasks = [];
|
|
12284
12422
|
_taskMap = /* @__PURE__ */ new Map();
|
|
12423
|
+
/** Task id → the top-level task it hangs under, itself for a top-level task. */
|
|
12424
|
+
_rootMap = /* @__PURE__ */ new Map();
|
|
12285
12425
|
_planRunners = ["claude-code"];
|
|
12286
12426
|
_onMutate = null;
|
|
12287
12427
|
_executionLog = [];
|
|
@@ -12319,6 +12459,14 @@ var PlanStore = class {
|
|
|
12319
12459
|
get(taskId) {
|
|
12320
12460
|
return this._taskMap.get(taskId);
|
|
12321
12461
|
}
|
|
12462
|
+
/**
|
|
12463
|
+
* Whether a task runs as an ops task (ADR-0020). Only a top-level AI task
|
|
12464
|
+
* can be one; a subtask runs with the top-level task it hangs under.
|
|
12465
|
+
*/
|
|
12466
|
+
isOps(taskId) {
|
|
12467
|
+
const root = this._rootMap.get(taskId);
|
|
12468
|
+
return root?.type === "ai" && root.ops === true;
|
|
12469
|
+
}
|
|
12322
12470
|
/** A copy of the task tree, detached from the store: later status changes do not reach it. */
|
|
12323
12471
|
snapshot() {
|
|
12324
12472
|
return cloneTasks(this._planTasks);
|
|
@@ -12413,6 +12561,7 @@ ${taskB.description}`,
|
|
|
12413
12561
|
assignedRunner: taskA.assignedRunner,
|
|
12414
12562
|
assignedModel: taskA.assignedModel,
|
|
12415
12563
|
taskMode: taskA.taskMode,
|
|
12564
|
+
ops: taskA.ops === true && taskB.ops === true,
|
|
12416
12565
|
order: Math.min(taskA.order, taskB.order)
|
|
12417
12566
|
});
|
|
12418
12567
|
const mergedId = merged.id;
|
|
@@ -12442,7 +12591,8 @@ ${taskB.description}`,
|
|
|
12442
12591
|
taskMode: spec.taskMode ?? original.taskMode,
|
|
12443
12592
|
autonomy: spec.autonomy ?? original.autonomy,
|
|
12444
12593
|
sliceType: spec.sliceType ?? original.sliceType,
|
|
12445
|
-
userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered
|
|
12594
|
+
userStoriesCovered: spec.userStoriesCovered ?? original.userStoriesCovered,
|
|
12595
|
+
ops: spec.ops ?? original.ops
|
|
12446
12596
|
}));
|
|
12447
12597
|
});
|
|
12448
12598
|
const tailId = finalized[finalized.length - 1].id;
|
|
@@ -12496,8 +12646,14 @@ ${taskB.description}`,
|
|
|
12496
12646
|
this.setStatus(id, "pending");
|
|
12497
12647
|
task.verdict = void 0;
|
|
12498
12648
|
task.outputSummary = void 0;
|
|
12649
|
+
delete task.forcedPastGate;
|
|
12499
12650
|
}
|
|
12500
12651
|
}
|
|
12652
|
+
/** Kept on the task: it was force-started past its merge gate, before these dependencies' work was merged (ADR-0020). */
|
|
12653
|
+
setForcedPastGate(id, dependencies) {
|
|
12654
|
+
const task = this._taskMap.get(id);
|
|
12655
|
+
if (task) task.forcedPastGate = dependencies;
|
|
12656
|
+
}
|
|
12501
12657
|
/** A reason outlives nothing: any status but `awaiting_user` drops it. */
|
|
12502
12658
|
setStatus(id, status) {
|
|
12503
12659
|
const task = this._taskMap.get(id);
|
|
@@ -12580,6 +12736,10 @@ ${taskB.description}`,
|
|
|
12580
12736
|
for (const task of this._allTasks) {
|
|
12581
12737
|
this._taskMap.set(task.id, task);
|
|
12582
12738
|
}
|
|
12739
|
+
this._rootMap.clear();
|
|
12740
|
+
for (const root of this._planTasks) {
|
|
12741
|
+
for (const task of flattenTasks([root])) this._rootMap.set(task.id, root);
|
|
12742
|
+
}
|
|
12583
12743
|
}
|
|
12584
12744
|
validateAssignedRunners() {
|
|
12585
12745
|
for (const task of this._allTasks) {
|
|
@@ -12633,9 +12793,9 @@ var IsolationRunController = class {
|
|
|
12633
12793
|
/** Copied paths already reported for the current run: every task gets the same copies. */
|
|
12634
12794
|
reportedCopies = /* @__PURE__ */ new Set();
|
|
12635
12795
|
/**
|
|
12636
|
-
* How the open run executes; null while no run is open
|
|
12637
|
-
*
|
|
12638
|
-
* is stopped.
|
|
12796
|
+
* How the open run executes; null while no run is open, `undecided` until
|
|
12797
|
+
* its first change task asks. A run is one Execute-Plan or one manual task
|
|
12798
|
+
* run, from its start until it settles or is stopped.
|
|
12639
12799
|
*/
|
|
12640
12800
|
mode = null;
|
|
12641
12801
|
/** Resolver task id → the conflicted task it resolves; see {@link linkResolver}. */
|
|
@@ -12659,6 +12819,10 @@ var IsolationRunController = class {
|
|
|
12659
12819
|
get isOpen() {
|
|
12660
12820
|
return this.mode !== null;
|
|
12661
12821
|
}
|
|
12822
|
+
/** The open run has decided whether it isolates. */
|
|
12823
|
+
get decided() {
|
|
12824
|
+
return this.mode === "isolated" || this.mode === "shared";
|
|
12825
|
+
}
|
|
12662
12826
|
/** The open run executes in worktrees. */
|
|
12663
12827
|
get isolating() {
|
|
12664
12828
|
return this.mode === "isolated" && this.run !== null;
|
|
@@ -12715,13 +12879,22 @@ var IsolationRunController = class {
|
|
|
12715
12879
|
this.tell("warn", `Could not prune leftover worktrees: ${err instanceof Error ? err.message : String(err)}`);
|
|
12716
12880
|
}
|
|
12717
12881
|
}
|
|
12882
|
+
/** Open a run if none is, leaving whether it isolates to its first change task. */
|
|
12883
|
+
open() {
|
|
12884
|
+
if (this.mode) return;
|
|
12885
|
+
this.mode = "undecided";
|
|
12886
|
+
this.listener.opened();
|
|
12887
|
+
}
|
|
12718
12888
|
/**
|
|
12719
|
-
*
|
|
12720
|
-
* worktrees
|
|
12721
|
-
*
|
|
12889
|
+
* Decide once per run, as its first change task starts, whether it executes
|
|
12890
|
+
* in worktrees (ADR-0020): an ops task needs none, so a run of only ops
|
|
12891
|
+
* tasks never asks. `resume` is what a dirty tree parks until the user
|
|
12892
|
+
* chooses how to go on. Resolves false while the run waits on that choice.
|
|
12722
12893
|
*/
|
|
12723
|
-
|
|
12724
|
-
|
|
12894
|
+
decide(resume) {
|
|
12895
|
+
this.open();
|
|
12896
|
+
if (this.decided) return Promise.resolve(true);
|
|
12897
|
+
if (this.blockedStart) return Promise.resolve(false);
|
|
12725
12898
|
this.opening ??= this.activate(resume).finally(() => {
|
|
12726
12899
|
this.opening = null;
|
|
12727
12900
|
});
|
|
@@ -12736,8 +12909,8 @@ var IsolationRunController = class {
|
|
|
12736
12909
|
* the safe rule then.
|
|
12737
12910
|
*/
|
|
12738
12911
|
async plannerLayout() {
|
|
12739
|
-
if (this.
|
|
12740
|
-
const decision = await this.
|
|
12912
|
+
if (this.decided) return this.isolating && this.run ? layoutOf(this.run) : false;
|
|
12913
|
+
const decision = await this.assess(this.workspaceRoot());
|
|
12741
12914
|
return decision.mode === "isolated" ? decision.layout : false;
|
|
12742
12915
|
}
|
|
12743
12916
|
/**
|
|
@@ -12772,7 +12945,7 @@ var IsolationRunController = class {
|
|
|
12772
12945
|
const { cwd: cwd2 } = await this.isolation.reopen(task, this.run);
|
|
12773
12946
|
return { cwd: cwd2, worktree: true };
|
|
12774
12947
|
}
|
|
12775
|
-
if (!this.isolating || !this.run) {
|
|
12948
|
+
if (opts.ops || !this.isolating || !this.run) {
|
|
12776
12949
|
await this.release(task.id, { keep: false });
|
|
12777
12950
|
return { cwd: this.workspaceRoot(), worktree: false };
|
|
12778
12951
|
}
|
|
@@ -12817,18 +12990,71 @@ var IsolationRunController = class {
|
|
|
12817
12990
|
}
|
|
12818
12991
|
this.listener.changed();
|
|
12819
12992
|
}
|
|
12820
|
-
/**
|
|
12993
|
+
/**
|
|
12994
|
+
* Close the open run. An isolated one hands its integration branch over for
|
|
12995
|
+
* review — unless the user merged everything it landed at merge gates
|
|
12996
|
+
* already, which leaves nothing to hand over, so it is cleared up as a Merge
|
|
12997
|
+
* all would have.
|
|
12998
|
+
*/
|
|
12821
12999
|
async close() {
|
|
12822
13000
|
const mode = this.mode;
|
|
12823
13001
|
this.mode = null;
|
|
12824
|
-
|
|
13002
|
+
const run = this.run;
|
|
13003
|
+
if (mode !== "isolated" || !run) return;
|
|
13004
|
+
await this.refreshInHead();
|
|
13005
|
+
const records = Object.values(run.tasks);
|
|
13006
|
+
if (records.length > 0 && records.every((r) => r.status === "merged" && r.inHead)) {
|
|
13007
|
+
this.tell("info", "Everything this run landed is merged into your branch already.");
|
|
13008
|
+
await this.clearMerged(run);
|
|
13009
|
+
return;
|
|
13010
|
+
}
|
|
12825
13011
|
try {
|
|
12826
|
-
this.listener.handoff(await this.isolation.handoff(
|
|
13012
|
+
this.listener.handoff(await this.isolation.handoff(run));
|
|
12827
13013
|
} catch (err) {
|
|
12828
13014
|
this.tell("warn", `Could not hand the run's integration branch over: ${err instanceof Error ? err.message : String(err)}`);
|
|
12829
13015
|
}
|
|
12830
13016
|
this.listener.changed();
|
|
12831
13017
|
}
|
|
13018
|
+
/**
|
|
13019
|
+
* Whether a task's landed work still waits to be merged into the user's
|
|
13020
|
+
* branch — what holds an ops or user task at its merge gate (ADR-0020). A
|
|
13021
|
+
* task with no record never landed anything to wait for.
|
|
13022
|
+
*/
|
|
13023
|
+
awaitsMerge(taskId) {
|
|
13024
|
+
const record = this.run?.tasks[taskId];
|
|
13025
|
+
return !!record && record.status === "merged" && !record.inHead;
|
|
13026
|
+
}
|
|
13027
|
+
/**
|
|
13028
|
+
* Look again for landed work the user has merged — by Merge all, or by hand
|
|
13029
|
+
* with git. Resolves true when some was found.
|
|
13030
|
+
*/
|
|
13031
|
+
async refreshInHead() {
|
|
13032
|
+
const run = this.run;
|
|
13033
|
+
if (!run || !Object.values(run.tasks).some((r) => r.status === "merged" && !r.inHead)) return false;
|
|
13034
|
+
const found = await this.isolation.findInHead(run).catch(() => []);
|
|
13035
|
+
if (found.length > 0) this.listener.changed();
|
|
13036
|
+
return found.length > 0;
|
|
13037
|
+
}
|
|
13038
|
+
/**
|
|
13039
|
+
* The tracked state of the workspace as an ops task starts, for
|
|
13040
|
+
* {@link filesChangedSince} (ADR-0020). Null where the check does not run:
|
|
13041
|
+
* a run that shares the workspace root, where every task's edits land in
|
|
13042
|
+
* it, or a workspace git cannot isolate at all.
|
|
13043
|
+
*/
|
|
13044
|
+
async snapshotWorkspace() {
|
|
13045
|
+
if (this.mode === "shared") return null;
|
|
13046
|
+
const root = this.workspaceRoot();
|
|
13047
|
+
if (!this.isolating) {
|
|
13048
|
+
const availability = await this.isolation.isActive(root).catch(() => null);
|
|
13049
|
+
if (!availability || !availability.active && availability.reason !== "dirty") return null;
|
|
13050
|
+
}
|
|
13051
|
+
return this.isolation.snapshotTree(root, this.run?.sharedRepos ?? []).catch(() => null);
|
|
13052
|
+
}
|
|
13053
|
+
/** Tracked files changed since `snapshot`; none when the run has since gone on in the workspace root. */
|
|
13054
|
+
async filesChangedSince(snapshot) {
|
|
13055
|
+
if (!snapshot || this.mode === "shared") return [];
|
|
13056
|
+
return this.isolation.changedSince(this.workspaceRoot(), snapshot).catch(() => []);
|
|
13057
|
+
}
|
|
12832
13058
|
/**
|
|
12833
13059
|
* A stop or a plan load: the open run ends without a handoff and a parked
|
|
12834
13060
|
* start is dropped. `keepOpen` spares a run the scheduler is still driving.
|
|
@@ -12859,19 +13085,23 @@ var IsolationRunController = class {
|
|
|
12859
13085
|
}
|
|
12860
13086
|
/**
|
|
12861
13087
|
* "Merge all": the run's integration branches into whatever the user has
|
|
12862
|
-
* checked out, in every repo or none.
|
|
12863
|
-
*
|
|
12864
|
-
*
|
|
13088
|
+
* checked out, in every repo or none. During a run it merges what has
|
|
13089
|
+
* landed so far and the run goes on (ADR-0020): its branches stay for the
|
|
13090
|
+
* tasks still to land. Once a settled run merged everything, it has nothing
|
|
13091
|
+
* left to hand over, so it is cleared up and forgotten; a branch the user's
|
|
13092
|
+
* HEAD somehow does not contain stays for the next run's sweep.
|
|
12865
13093
|
*/
|
|
12866
13094
|
async merge() {
|
|
12867
13095
|
const run = this.requireRun();
|
|
13096
|
+
const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
|
|
12868
13097
|
const result = await this.isolation.mergeIntoCheckedOut(run);
|
|
12869
13098
|
const branch = integrationBranchNameOf(run);
|
|
12870
13099
|
const group = run.repos.some((r) => r.path !== SELF_REPO);
|
|
12871
|
-
const repaired = handoffOf(run).landed.filter((t) => t.repairedFiles?.length);
|
|
12872
13100
|
const { level, message } = describeMergeResult(result, branch, group, repaired);
|
|
12873
13101
|
this.tell(level, message);
|
|
12874
|
-
if (result.outcome
|
|
13102
|
+
if (result.outcome !== "merged") return result;
|
|
13103
|
+
await this.refreshInHead();
|
|
13104
|
+
if (!this.isOpen) await this.clearMerged(run);
|
|
12875
13105
|
return result;
|
|
12876
13106
|
}
|
|
12877
13107
|
/** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
|
|
@@ -12903,7 +13133,7 @@ var IsolationRunController = class {
|
|
|
12903
13133
|
this.resolvers = {};
|
|
12904
13134
|
this.listener.changed();
|
|
12905
13135
|
}
|
|
12906
|
-
async
|
|
13136
|
+
async assess(root) {
|
|
12907
13137
|
const availability = await this.isolation.isActive(root);
|
|
12908
13138
|
const continued = this.continuableRun(root);
|
|
12909
13139
|
const continuing = continued !== null;
|
|
@@ -12918,7 +13148,7 @@ var IsolationRunController = class {
|
|
|
12918
13148
|
}
|
|
12919
13149
|
async activate(resume) {
|
|
12920
13150
|
const root = this.workspaceRoot();
|
|
12921
|
-
const decision = await this.
|
|
13151
|
+
const decision = await this.assess(root);
|
|
12922
13152
|
if (decision.mode === "blocked") {
|
|
12923
13153
|
this.blockedStart = resume;
|
|
12924
13154
|
this.blockedRepos = decision.repos;
|
|
@@ -12945,8 +13175,8 @@ var IsolationRunController = class {
|
|
|
12945
13175
|
return true;
|
|
12946
13176
|
}
|
|
12947
13177
|
begin(mode) {
|
|
13178
|
+
this.open();
|
|
12948
13179
|
this.mode = mode;
|
|
12949
|
-
this.listener.opened();
|
|
12950
13180
|
}
|
|
12951
13181
|
/** What earlier runs left merged in the group goes; a failure here is worth a word, never a stopped run. */
|
|
12952
13182
|
async sweep() {
|
|
@@ -13293,8 +13523,9 @@ var MessageQueue = class {
|
|
|
13293
13523
|
// src/services/readiness.ts
|
|
13294
13524
|
function selectReadyTasks(input) {
|
|
13295
13525
|
const { store, onHold, runs, active, maxParallel } = input;
|
|
13296
|
-
if (active >= maxParallel) return { ready: [], candidateCount: 0, excluded: [] };
|
|
13526
|
+
if (active >= maxParallel) return { ready: [], candidateCount: 0, excluded: [], gated: [] };
|
|
13297
13527
|
const availableSlots = maxParallel - active;
|
|
13528
|
+
const gated = [];
|
|
13298
13529
|
const candidates = store.allTasks.filter((t) => {
|
|
13299
13530
|
if (t.status !== "pending" && t.status !== "approved") return false;
|
|
13300
13531
|
if (t.type === "user") return false;
|
|
@@ -13302,13 +13533,21 @@ function selectReadyTasks(input) {
|
|
|
13302
13533
|
if (onHold.has(t.id)) return false;
|
|
13303
13534
|
if (isBlocked(t, store)) return false;
|
|
13304
13535
|
if (!t.dependencies.every((depId) => dependencyMet(depId, store, runs))) return false;
|
|
13536
|
+
if (store.isOps(t.id)) {
|
|
13537
|
+
if (input.merging) return false;
|
|
13538
|
+
if (mergeGate(t, store, runs).length > 0) {
|
|
13539
|
+
gated.push(t);
|
|
13540
|
+
return false;
|
|
13541
|
+
}
|
|
13542
|
+
}
|
|
13305
13543
|
return true;
|
|
13306
13544
|
});
|
|
13307
|
-
const excluded = store.allTasks.filter((t) => t.type === "ai" && t.prompt && !candidates.includes(t)).map((task) => ({ task, reasons: exclusionReasons(task,
|
|
13545
|
+
const excluded = store.allTasks.filter((t) => t.type === "ai" && t.prompt && !candidates.includes(t)).map((task) => ({ task, reasons: exclusionReasons(task, input) }));
|
|
13308
13546
|
return {
|
|
13309
13547
|
ready: candidates.sort((a, b) => a.order - b.order).slice(0, availableSlots),
|
|
13310
13548
|
candidateCount: candidates.length,
|
|
13311
|
-
excluded
|
|
13549
|
+
excluded,
|
|
13550
|
+
gated
|
|
13312
13551
|
};
|
|
13313
13552
|
}
|
|
13314
13553
|
function isBlocked(task, store) {
|
|
@@ -13321,12 +13560,19 @@ function dependencyMet(depId, store, runs) {
|
|
|
13321
13560
|
const record = runs.openRecord(depId);
|
|
13322
13561
|
return !record || record.status === "merged";
|
|
13323
13562
|
}
|
|
13324
|
-
function
|
|
13563
|
+
function mergeGate(task, store, runs) {
|
|
13564
|
+
if (task.type !== "user" && !store.isOps(task.id)) return [];
|
|
13565
|
+
return task.dependencies.filter((depId) => store.isCompleted(depId) && runs.awaitsMerge(depId));
|
|
13566
|
+
}
|
|
13567
|
+
function exclusionReasons(task, input) {
|
|
13568
|
+
const { store, onHold, runs } = input;
|
|
13325
13569
|
const reasons = [];
|
|
13326
13570
|
if (task.status !== "pending" && task.status !== "approved") reasons.push(`status=${task.status}`);
|
|
13327
13571
|
if (onHold.has(task.id)) reasons.push("on-hold");
|
|
13328
13572
|
if (isBlocked(task, store)) reasons.push("blocked");
|
|
13329
13573
|
if (!task.dependencies.every((depId) => dependencyMet(depId, store, runs))) reasons.push("deps");
|
|
13574
|
+
if (mergeGate(task, store, runs).length > 0) reasons.push("merge-gate");
|
|
13575
|
+
if (input.merging && store.isOps(task.id)) reasons.push("merging");
|
|
13330
13576
|
return reasons;
|
|
13331
13577
|
}
|
|
13332
13578
|
|
|
@@ -16217,6 +16463,10 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16217
16463
|
* pending and nothing ran until the user also re-ran the whole plan.
|
|
16218
16464
|
*/
|
|
16219
16465
|
haltedByFailure = false;
|
|
16466
|
+
/** A Merge all is under way: no ops task starts until it ends (ADR-0020). */
|
|
16467
|
+
merging = false;
|
|
16468
|
+
/** The last "waiting for Merge all" notice, so a run paused at a gate says it once, not on every tick. */
|
|
16469
|
+
gateNotice = "";
|
|
16220
16470
|
/** The workspace's own variables for a task's cwd (ADR-0016); swapped out in tests. */
|
|
16221
16471
|
workspaceEnv;
|
|
16222
16472
|
/** What the workspace env has already warned about, so a run says it once, not per task. */
|
|
@@ -16525,9 +16775,48 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16525
16775
|
async reviewRunDiff() {
|
|
16526
16776
|
return this.runs.reviewDiff();
|
|
16527
16777
|
}
|
|
16528
|
-
/**
|
|
16778
|
+
/**
|
|
16779
|
+
* "Merge all"; a settled run that merged everything is cleared up and
|
|
16780
|
+
* forgotten. During a run it merges what has landed and the run goes on:
|
|
16781
|
+
* the tasks waiting at a merge gate for that work then start (ADR-0020).
|
|
16782
|
+
* Never while an ops task runs, and no ops task starts until it ends — the
|
|
16783
|
+
* one overlap the plan cannot order, because the merge is the user's.
|
|
16784
|
+
*/
|
|
16529
16785
|
async mergeRun() {
|
|
16530
|
-
|
|
16786
|
+
const ops = [...this.attempts.values()].find((a) => a.ops);
|
|
16787
|
+
if (ops) {
|
|
16788
|
+
const title = this.store.get(ops.taskId)?.title ?? ops.taskId;
|
|
16789
|
+
throw new TaskControlError(`Ops task "${title}" is running in your checkout \u2014 Merge all once it has finished.`);
|
|
16790
|
+
}
|
|
16791
|
+
this.merging = true;
|
|
16792
|
+
let result;
|
|
16793
|
+
try {
|
|
16794
|
+
result = await this.runs.merge();
|
|
16795
|
+
} finally {
|
|
16796
|
+
this.merging = false;
|
|
16797
|
+
}
|
|
16798
|
+
await this.tick();
|
|
16799
|
+
return result;
|
|
16800
|
+
}
|
|
16801
|
+
/**
|
|
16802
|
+
* The run as a surface offers Merge all mid-run (ADR-0020): what has landed
|
|
16803
|
+
* and is not merged, and whether the run is paused at its gates with nothing
|
|
16804
|
+
* else running. Null while no task waits at a gate.
|
|
16805
|
+
*/
|
|
16806
|
+
mergeGateView() {
|
|
16807
|
+
const handoff = this.runs.view()?.handoff;
|
|
16808
|
+
if (!handoff || !this.store.allTasks.some((t) => this.getMergeGate(t.id).length > 0)) return null;
|
|
16809
|
+
return { paused: this.running && this.attempts.size === 0, repos: handoff.repos, landed: handoff.landed };
|
|
16810
|
+
}
|
|
16811
|
+
/**
|
|
16812
|
+
* The dependencies an ops or user task waits on at its merge gate: those
|
|
16813
|
+
* whose landed work is not in the user's branch yet (ADR-0020). Empty once
|
|
16814
|
+
* the task has started, and for every other task.
|
|
16815
|
+
*/
|
|
16816
|
+
getMergeGate(taskId) {
|
|
16817
|
+
const task = this.store.get(taskId);
|
|
16818
|
+
if (!task || task.status !== "pending" && task.status !== "approved") return [];
|
|
16819
|
+
return mergeGate(task, this.store, this.runs);
|
|
16531
16820
|
}
|
|
16532
16821
|
/** Worktrees and task branches go; the integration branch and the record stay for review and merge. */
|
|
16533
16822
|
async cleanupRun() {
|
|
@@ -16627,7 +16916,7 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16627
16916
|
this.emit("onReviewNeeded", { tasks: this.store.planTasks, planRunners: this.store.planRunners });
|
|
16628
16917
|
return;
|
|
16629
16918
|
}
|
|
16630
|
-
|
|
16919
|
+
this.runs.open();
|
|
16631
16920
|
console.log(`[TaskOrchestrator] Starting with ${this.store.allTasks.length} tasks (${this.store.allTasks.filter((t) => t.type === "ai" && t.prompt).length} AI ready)`);
|
|
16632
16921
|
this.running = true;
|
|
16633
16922
|
this.haltedByFailure = false;
|
|
@@ -16669,13 +16958,17 @@ var TaskOrchestrator = class _TaskOrchestrator {
|
|
|
16669
16958
|
const summary = await this.output.finalText({ ...attempt, completionMarker: task.completionMarker }, doneToken);
|
|
16670
16959
|
if (this.attempts.get(taskId) !== attempt) return;
|
|
16671
16960
|
const landing = verdict.outcome === "pass" ? await this.land(attempt, () => this.landing.landPassed(task, attempt)) : null;
|
|
16961
|
+
const changedFiles = landing && attempt.ops ? await this.runs.filesChangedSince(attempt.snapshot) : [];
|
|
16672
16962
|
const unresumedReason = this.unresumed(attempt) ? unresumedMessage(task, `${attempt.runner} could not find its saved session`) : null;
|
|
16673
16963
|
if (this.attempts.get(taskId) !== attempt) return;
|
|
16674
16964
|
this.endAttempt(taskId, "verdict");
|
|
16675
16965
|
this.store.setTaskVerdict(taskId, verdict);
|
|
16676
16966
|
console.error(`[TaskOrchestrator] Output summary:
|
|
16677
16967
|
${summary || "(empty \u2014 no output captured)"}`);
|
|
16678
|
-
if (
|
|
16968
|
+
if (changedFiles.length > 0) {
|
|
16969
|
+
this.store.markAwaitingUser(taskId, "files-changed");
|
|
16970
|
+
this.tell("warn", `Ops task "${task.title}" finished, but changed tracked files in your checkout: ${capConflictFiles(changedFiles)}. An ops task must not change repository files, so nothing was committed \u2014 look at the changes, then mark it complete or retry it.`);
|
|
16971
|
+
} else if (landing) {
|
|
16679
16972
|
await this.applyLanding(task, landing, () => this.notifications.info(`Task "${task.title}" completed.`));
|
|
16680
16973
|
} else if (classifyRunnerStop(attempt.session?.getOutput() ?? "") === "usage-limit") {
|
|
16681
16974
|
this.store.markAwaitingUser(taskId);
|
|
@@ -16777,21 +17070,35 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
16777
17070
|
});
|
|
16778
17071
|
}
|
|
16779
17072
|
getReadyTasks() {
|
|
16780
|
-
|
|
17073
|
+
return this.readiness().ready;
|
|
17074
|
+
}
|
|
17075
|
+
readiness() {
|
|
17076
|
+
if (!this.running) return { ready: [], candidateCount: 0, excluded: [], gated: [] };
|
|
16781
17077
|
const maxParallel = this.config.maxParallelSessions;
|
|
16782
17078
|
const active = this.attempts.size;
|
|
16783
|
-
const
|
|
17079
|
+
const readiness = selectReadyTasks({
|
|
16784
17080
|
store: this.store,
|
|
16785
17081
|
onHold: this.onHold,
|
|
16786
17082
|
runs: this.runs,
|
|
16787
17083
|
active,
|
|
16788
|
-
maxParallel
|
|
17084
|
+
maxParallel,
|
|
17085
|
+
merging: this.merging
|
|
16789
17086
|
});
|
|
16790
|
-
for (const { task, reasons } of excluded) {
|
|
17087
|
+
for (const { task, reasons } of readiness.excluded) {
|
|
16791
17088
|
console.log(`[TaskOrchestrator] excluded: #${task.order} "${task.title}" \u2014 ${reasons.join(", ")}`);
|
|
16792
17089
|
}
|
|
16793
|
-
console.log(`[TaskOrchestrator] getReadyTasks: ${candidateCount} candidates, ${Math.max(0, maxParallel - active)} slots, maxParallel=${maxParallel}`);
|
|
16794
|
-
return
|
|
17090
|
+
console.log(`[TaskOrchestrator] getReadyTasks: ${readiness.candidateCount} candidates, ${Math.max(0, maxParallel - active)} slots, maxParallel=${maxParallel}`);
|
|
17091
|
+
return readiness;
|
|
17092
|
+
}
|
|
17093
|
+
/**
|
|
17094
|
+
* Open the run a task starts in, and for a change task decide whether it
|
|
17095
|
+
* isolates (ADR-0020): an ops task needs no worktree, so it never waits on
|
|
17096
|
+
* that choice. False while a dirty tree waits on the user; `resume` is what
|
|
17097
|
+
* their choice replays.
|
|
17098
|
+
*/
|
|
17099
|
+
async openFor(taskId, resume) {
|
|
17100
|
+
this.runs.open();
|
|
17101
|
+
return this.store.isOps(taskId) || this.runs.decide(resume);
|
|
16795
17102
|
}
|
|
16796
17103
|
/**
|
|
16797
17104
|
* Cancel a running (or scheduled) task: kill its session and return it to
|
|
@@ -16919,7 +17226,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
16919
17226
|
const runner = this.store.resolveTaskRunner(task);
|
|
16920
17227
|
const route = routeTransport("structured", runner, this.registry);
|
|
16921
17228
|
if (route.transport !== "structured") throw new TaskControlError(`Task "${task.title}" cannot be continued: ${route.fallback}. Use Retry instead.`);
|
|
16922
|
-
if (!await this.
|
|
17229
|
+
if (!await this.openFor(taskId, () => this.continueTask(taskId, message))) return;
|
|
16923
17230
|
if (this.attempts.has(taskId) || !continuability(task).ok) return;
|
|
16924
17231
|
this.retryCounts.set(taskId, (this.retryCounts.get(taskId) ?? 0) + 1);
|
|
16925
17232
|
this.endAttempt(taskId, "retry");
|
|
@@ -16941,10 +17248,29 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
16941
17248
|
const task = this.store.get(taskId);
|
|
16942
17249
|
if (!task || task.type !== "ai") return;
|
|
16943
17250
|
if (this.attempts.has(taskId)) return;
|
|
16944
|
-
|
|
17251
|
+
this.refuseOpsDuringMerge(task);
|
|
17252
|
+
if (!await this.openFor(taskId, () => this.forceStartTask(taskId))) return;
|
|
16945
17253
|
this.onHold.delete(taskId);
|
|
17254
|
+
this.notePastGate(task);
|
|
16946
17255
|
await this.startTask(task);
|
|
16947
17256
|
}
|
|
17257
|
+
refuseOpsDuringMerge(task) {
|
|
17258
|
+
if (this.merging && this.store.isOps(task.id)) {
|
|
17259
|
+
throw new TaskControlError(`A Merge all is under way \u2014 ops task "${task.title}" can start once it has finished.`);
|
|
17260
|
+
}
|
|
17261
|
+
}
|
|
17262
|
+
/**
|
|
17263
|
+
* A force start passes a merge gate (ADR-0020), and the surface confirmed it
|
|
17264
|
+
* first; the choice is kept on the task and said, because the work it acts
|
|
17265
|
+
* on is not in the user's branch.
|
|
17266
|
+
*/
|
|
17267
|
+
notePastGate(task) {
|
|
17268
|
+
const unmerged = mergeGate(task, this.store, this.runs);
|
|
17269
|
+
if (unmerged.length === 0) return;
|
|
17270
|
+
const titles = unmerged.map((id) => `"${this.store.get(id)?.title ?? id}"`);
|
|
17271
|
+
this.store.setForcedPastGate(task.id, titles);
|
|
17272
|
+
this.tell("warn", `Task "${task.title}" was force-started before the work of ${titles.join(", ")} was merged into your branch.`);
|
|
17273
|
+
}
|
|
16948
17274
|
/**
|
|
16949
17275
|
* Run exactly one task outside full-plan scheduling. The active/starting
|
|
16950
17276
|
* session still contributes to {@link hasLiveWork} so every surface exposes
|
|
@@ -16955,8 +17281,10 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
16955
17281
|
if (this.hasLiveWork) return;
|
|
16956
17282
|
const task = this.store.get(taskId);
|
|
16957
17283
|
if (!task || task.type !== "ai") return;
|
|
16958
|
-
|
|
17284
|
+
this.refuseOpsDuringMerge(task);
|
|
17285
|
+
if (!await this.openFor(taskId, () => this.runTask(taskId))) return;
|
|
16959
17286
|
this.onHold.delete(taskId);
|
|
17287
|
+
this.notePastGate(task);
|
|
16960
17288
|
await this.startTask(task);
|
|
16961
17289
|
}
|
|
16962
17290
|
getCompletedCount() {
|
|
@@ -16996,7 +17324,11 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
16996
17324
|
}
|
|
16997
17325
|
return;
|
|
16998
17326
|
}
|
|
16999
|
-
|
|
17327
|
+
let readiness = this.readiness();
|
|
17328
|
+
const atGate = readiness.gated.length > 0 || this.store.allTasks.some((t) => t.type === "user" && this.getMergeGate(t.id).length > 0);
|
|
17329
|
+
if (atGate && await this.runs.refreshInHead()) readiness = this.readiness();
|
|
17330
|
+
if (!this.running) return;
|
|
17331
|
+
const { ready } = readiness;
|
|
17000
17332
|
console.log(`[TaskOrchestrator] tick(): ${ready.length} ready, ${this.attempts.size} active, queue=${this.messageQueue.length}`);
|
|
17001
17333
|
if (ready.length === 0 && this.attempts.size === 0) {
|
|
17002
17334
|
const remaining = this.store.allTasks.filter((t) => t.status !== "completed");
|
|
@@ -17016,14 +17348,29 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17016
17348
|
this.emit("onExecutionComplete");
|
|
17017
17349
|
} else {
|
|
17018
17350
|
this.notifications.info("Remaining tasks require user action or are on hold.");
|
|
17351
|
+
this.sayGate();
|
|
17019
17352
|
this.emit("onTaskChanged");
|
|
17020
17353
|
this.emit("onTick");
|
|
17021
17354
|
}
|
|
17022
17355
|
return;
|
|
17023
17356
|
}
|
|
17024
|
-
for (const task of ready)
|
|
17357
|
+
for (const task of ready) {
|
|
17358
|
+
if (this.store.isOps(task.id) ? this.merging : !await this.runs.decide(() => this.tick())) continue;
|
|
17359
|
+
await this.startTask(task);
|
|
17360
|
+
}
|
|
17025
17361
|
this.emit("onTick");
|
|
17026
17362
|
}
|
|
17363
|
+
/**
|
|
17364
|
+
* A run paused at merge gates says so, as plainly as a task that waits on
|
|
17365
|
+
* the user: which tasks wait, and that Merge all lets them go on (ADR-0020).
|
|
17366
|
+
*/
|
|
17367
|
+
sayGate() {
|
|
17368
|
+
const waiting = this.store.allTasks.filter((t) => this.getMergeGate(t.id).length > 0);
|
|
17369
|
+
const notice = waiting.length === 0 ? "" : `Waiting for Merge all: ${waiting.map((t) => `"${t.title}"`).join(", ")} ${waiting.length === 1 ? "needs" : "need"} work that is not merged into your branch yet.`;
|
|
17370
|
+
if (notice === this.gateNotice) return;
|
|
17371
|
+
this.gateNotice = notice;
|
|
17372
|
+
if (notice) this.tell("info", notice);
|
|
17373
|
+
}
|
|
17027
17374
|
async startTask(task, continuation = null) {
|
|
17028
17375
|
if (this.attempts.has(task.id)) return;
|
|
17029
17376
|
const attempt = {
|
|
@@ -17037,6 +17384,8 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17037
17384
|
integration: null,
|
|
17038
17385
|
repair: this.landing.nextRepair(task.id),
|
|
17039
17386
|
continuation,
|
|
17387
|
+
ops: this.store.isOps(task.id),
|
|
17388
|
+
snapshot: null,
|
|
17040
17389
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17041
17390
|
decided: false
|
|
17042
17391
|
};
|
|
@@ -17062,18 +17411,22 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17062
17411
|
*/
|
|
17063
17412
|
async spawnAttempt(task, attempt) {
|
|
17064
17413
|
try {
|
|
17065
|
-
const { cwd, worktree } = await this.runs.attemptCwd(task, { repair: attempt.repair !== null });
|
|
17414
|
+
const { cwd, worktree } = await this.runs.attemptCwd(task, { repair: attempt.repair !== null, ops: attempt.ops });
|
|
17066
17415
|
attempt.cwd = cwd;
|
|
17067
17416
|
attempt.worktree = worktree;
|
|
17417
|
+
if (attempt.ops) attempt.snapshot = await this.runs.snapshotWorkspace();
|
|
17068
17418
|
if (this.attempts.get(task.id) !== attempt) {
|
|
17069
17419
|
if (attempt.worktree && !this.attempts.has(task.id)) await this.runs.release(task.id, { keep: attempt.repair !== null });
|
|
17070
17420
|
this.abandonSpawn(task, attempt);
|
|
17071
17421
|
return false;
|
|
17072
17422
|
}
|
|
17073
|
-
const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
|
|
17423
|
+
const finalPrompt = attempt.continuation ? composeContinuationPrompt(task, attempt.continuation.message, { ops: attempt.ops }) : composeAugmentedPrompt(attempt.repair ? { ...task, prompt: this.landing.repairPrompt(task) } : task, this.store.planTasks, {
|
|
17074
17424
|
planMapEnabled: this.config.planMapEnabled,
|
|
17075
17425
|
// A merge to resolve is not new behaviour to drive test-first.
|
|
17076
|
-
tddEnabled: !attempt.repair && this.tddEnabled()
|
|
17426
|
+
tddEnabled: !attempt.repair && this.tddEnabled(),
|
|
17427
|
+
// An ops task's effects outlive a failed attempt and are never rolled
|
|
17428
|
+
// back, so the next one is told what the last one did (ADR-0020).
|
|
17429
|
+
previousAttempt: attempt.ops ? this.output.liveTail(task.id, { maxLines: OPS_RETRY_TAIL_LINES })?.text : void 0
|
|
17077
17430
|
});
|
|
17078
17431
|
this.lingering.close(task.id);
|
|
17079
17432
|
const env = await this.envForTask(cwd);
|
|
@@ -17250,6 +17603,7 @@ ${summary || "(empty \u2014 no output captured)"}`);
|
|
|
17250
17603
|
return ended;
|
|
17251
17604
|
}
|
|
17252
17605
|
};
|
|
17606
|
+
var OPS_RETRY_TAIL_LINES = 60;
|
|
17253
17607
|
function unresumedMessage(task, why) {
|
|
17254
17608
|
return `Could not continue task "${task.title}": ${why}. Retry starts it afresh.`;
|
|
17255
17609
|
}
|
|
@@ -19707,7 +20061,7 @@ Execution is RUNNING${locked.length ? ` \u2014 these tasks are locked: ${locked.
|
|
|
19707
20061
|
return tasks.map((t) => {
|
|
19708
20062
|
const isMan = t.type === "user";
|
|
19709
20063
|
const runFields = isMan ? `steps:${t.userSteps?.length ?? 0}` : `${t.assignedModel ? `model:${t.assignedModel.modelId} ` : ""}mode:${t.taskMode ?? "build"} effort:${t.thinkingEffort ?? "-"}`;
|
|
19710
|
-
return `#${t.order} id=${t.id} "${t.title}" [${t.status}] type:${isMan ? "MAN" : "AI"} runner:${t.assignedRunner} ${runFields} autonomy:${t.autonomy ?? "-"} slice:${t.sliceType ?? "-"} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`;
|
|
20064
|
+
return `#${t.order} id=${t.id} "${t.title}" [${t.status}] type:${isMan ? "MAN" : t.ops ? "AI ops" : "AI"} runner:${t.assignedRunner} ${runFields} autonomy:${t.autonomy ?? "-"} slice:${t.sliceType ?? "-"} deps:[${t.dependencies.map((d) => orderOf.get(d) ?? d).join(", ")}]`;
|
|
19711
20065
|
});
|
|
19712
20066
|
}
|
|
19713
20067
|
};
|
|
@@ -20163,10 +20517,11 @@ function serializeTask(t) {
|
|
|
20163
20517
|
thinkingEffort: t.thinkingEffort || void 0,
|
|
20164
20518
|
autonomy: t.autonomy || void 0,
|
|
20165
20519
|
sliceType: t.sliceType || void 0,
|
|
20166
|
-
userStoriesCovered: t.userStoriesCovered || void 0
|
|
20520
|
+
userStoriesCovered: t.userStoriesCovered || void 0,
|
|
20521
|
+
...t.ops ? { ops: true } : {}
|
|
20167
20522
|
};
|
|
20168
20523
|
}
|
|
20169
|
-
function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0) {
|
|
20524
|
+
function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [], awaitingApproval = 0, mergeGate2 = []) {
|
|
20170
20525
|
return {
|
|
20171
20526
|
id: t.id,
|
|
20172
20527
|
status: t.status,
|
|
@@ -20177,7 +20532,9 @@ function serializeTaskStatus(t, idleSince = null, isolation = null, queued = [],
|
|
|
20177
20532
|
...t.status === "awaiting_user" && t.awaitingReason ? { awaitingReason: t.awaitingReason } : {},
|
|
20178
20533
|
...queued.length > 0 ? { queued: queued.map((m) => ({ ...m })) } : {},
|
|
20179
20534
|
...canContinue(t) ? { continuable: true } : {},
|
|
20180
|
-
...awaitingApproval > 0 ? { awaitingApproval } : {}
|
|
20535
|
+
...awaitingApproval > 0 ? { awaitingApproval } : {},
|
|
20536
|
+
...mergeGate2.length > 0 ? { mergeGate: [...mergeGate2] } : {},
|
|
20537
|
+
...t.forcedPastGate?.length ? { forcedPastGate: [...t.forcedPastGate] } : {}
|
|
20181
20538
|
};
|
|
20182
20539
|
}
|
|
20183
20540
|
function serializePlan(plan) {
|
|
@@ -20263,6 +20620,7 @@ var SessionEventRelay = class {
|
|
|
20263
20620
|
this.statusOwed = this.statusHeld;
|
|
20264
20621
|
if (this.statusHeld) return;
|
|
20265
20622
|
plan.tasks = this.store.snapshot();
|
|
20623
|
+
const gate = this.orchestrator.mergeGateView();
|
|
20266
20624
|
this.broadcast({
|
|
20267
20625
|
type: "status_update",
|
|
20268
20626
|
tasks: this.store.allTasks.map((t) => serializeTaskStatus(
|
|
@@ -20270,8 +20628,10 @@ var SessionEventRelay = class {
|
|
|
20270
20628
|
this.orchestrator.getIdleSince(t.id),
|
|
20271
20629
|
this.orchestrator.getTaskIsolation(t.id),
|
|
20272
20630
|
this.orchestrator.getQueuedTaskMessages(t.id),
|
|
20273
|
-
this.awaitingApproval(t.id)
|
|
20274
|
-
|
|
20631
|
+
this.awaitingApproval(t.id),
|
|
20632
|
+
this.orchestrator.getMergeGate(t.id)
|
|
20633
|
+
)),
|
|
20634
|
+
...gate ? { gate } : {}
|
|
20275
20635
|
});
|
|
20276
20636
|
}
|
|
20277
20637
|
/**
|
|
@@ -21604,6 +21964,14 @@ var Session = class {
|
|
|
21604
21964
|
isolationView() {
|
|
21605
21965
|
return this.orchestrator.isolationView();
|
|
21606
21966
|
}
|
|
21967
|
+
/** The dependencies a task waits on at its merge gate (ADR-0020); empty when it waits for no Merge all. */
|
|
21968
|
+
mergeGate(taskId) {
|
|
21969
|
+
return this.orchestrator.getMergeGate(taskId);
|
|
21970
|
+
}
|
|
21971
|
+
/** What Merge all would merge mid-run, while any task waits at a merge gate; null otherwise. */
|
|
21972
|
+
mergeGateView() {
|
|
21973
|
+
return this.orchestrator.mergeGateView();
|
|
21974
|
+
}
|
|
21607
21975
|
/** The run's integration branch against its base ref, as a unified diff. */
|
|
21608
21976
|
async reviewRunDiff() {
|
|
21609
21977
|
if (!this.orchestrator.isolationRecord) throw new PlanEditError("This plan has no isolated run");
|
|
@@ -21613,10 +21981,12 @@ var Session = class {
|
|
|
21613
21981
|
* "Merge all": each repo's integration branch into whatever the user has
|
|
21614
21982
|
* checked out there — every repo, or none if any cannot take it. The one
|
|
21615
21983
|
* irreversible step of isolated execution, so this explicit call is the
|
|
21616
|
-
* only way it ever happens.
|
|
21984
|
+
* only way it ever happens. During a run it merges what has landed so far,
|
|
21985
|
+
* which is what opens a merge gate (ADR-0020); it waits for no ops task,
|
|
21986
|
+
* and refuses while one runs.
|
|
21617
21987
|
*/
|
|
21618
21988
|
async mergeRun() {
|
|
21619
|
-
this.
|
|
21989
|
+
if (!this.orchestrator.isolationRecord) throw new PlanEditError("This plan has no isolated run");
|
|
21620
21990
|
const result = await this.orchestrator.mergeRun();
|
|
21621
21991
|
this.broadcast({ type: "isolation_merge", result });
|
|
21622
21992
|
return result;
|
|
@@ -21706,7 +22076,8 @@ var Session = class {
|
|
|
21706
22076
|
* falls through to the no-op `store.update` below instead of throwing.
|
|
21707
22077
|
*/
|
|
21708
22078
|
async updateTask(taskId, changes) {
|
|
21709
|
-
if (
|
|
22079
|
+
if ("ops" in changes) changes = { ...changes, ops: changes.ops === true ? true : void 0 };
|
|
22080
|
+
if ((changes.dependencies || changes.type || changes.assignedModel || changes.taskMode || "ops" in changes) && this.store.get(taskId)) {
|
|
21710
22081
|
const check = validateTaskEdit("direct", this.store.planTasks, taskId, changes, this.editCatalog());
|
|
21711
22082
|
if (!check.ok) throw new PlanEditError(check.error ?? "Those changes are not valid");
|
|
21712
22083
|
if (check.clear?.length) {
|