@multileaf/ai-workflow 0.13.0 → 0.14.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/CHANGELOG.md +11 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +12 -0
- package/dist/orchestration.d.ts +21 -1
- package/dist/orchestration.js +118 -5
- package/dist/workflow.js +42 -2
- package/docs/backlog.md +1 -1
- package/docs/continuation-inventory.md +2 -0
- package/docs/opportunities/OPP-017-orchestration-depth.md +1 -1
- package/docs/orchestration.md +16 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,17 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.14.0] - 2026-11-04
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Orchestration depth (OPP-017): `aiw orchestrate integrate --plan=<path>` prints the wave-ordered integration forecast — a deterministic `git merge-tree` dry run per isolated task branch against the current branch, reporting conflicts before any human merge. Isolated executions now record per-task duration, diff stat, and plan wall-clock time in the checkpoint, and `aiw doctor` reports leaked managed worktrees with the cleanup continuation (`worktrees_leaked` typed reason).
|
|
12
|
+
- `docs/orchestration.md` gains the per-harness worktree honor matrix and the minimal adapter contract for `task.worktree`.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- `orchestrate integrate` works before option parsing (previously subcommand form collided with `--plan` validation).
|
|
17
|
+
|
|
7
18
|
## [0.13.0] - 2026-11-03
|
|
8
19
|
|
|
9
20
|
### Added
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* command resolves the block it was printed for. See ADR-001 and
|
|
8
8
|
* docs/opportunities/OPP-004-exact-continuation-commands.md.
|
|
9
9
|
*/
|
|
10
|
-
export declare const BLOCKING_REASON_CODES: readonly ["gate_evidence_missing", "gate_incomplete", "stage_prerequisite_pending", "stage_prerequisite_unresolved", "stage_awaiting_approval", "stage_already_approved", "approval_evidence_missing", "approval_stale", "approval_gate_missing", "lane_escalation_required", "light_evidence_missing", "workflow_still_active", "workflow_not_active", "workflow_complete", "installation_required", "state_mismatch", "context_budget_exceeded", "stage_not_in_process", "migration_required"];
|
|
10
|
+
export declare const BLOCKING_REASON_CODES: readonly ["gate_evidence_missing", "gate_incomplete", "stage_prerequisite_pending", "stage_prerequisite_unresolved", "stage_awaiting_approval", "stage_already_approved", "approval_evidence_missing", "approval_stale", "approval_gate_missing", "lane_escalation_required", "light_evidence_missing", "workflow_still_active", "workflow_not_active", "workflow_complete", "installation_required", "state_mismatch", "context_budget_exceeded", "stage_not_in_process", "migration_required", "worktrees_leaked", "orchestration_not_executed"];
|
|
11
11
|
export type BlockingReasonCode = (typeof BLOCKING_REASON_CODES)[number];
|
|
12
12
|
/** How the user or agent gets past a block. Exactly one of three kinds. */
|
|
13
13
|
export type BlockingContinuation =
|
package/dist/blocking-reasons.js
CHANGED
|
@@ -27,6 +27,8 @@ export const BLOCKING_REASON_CODES = [
|
|
|
27
27
|
"context_budget_exceeded",
|
|
28
28
|
"stage_not_in_process",
|
|
29
29
|
"migration_required",
|
|
30
|
+
"worktrees_leaked",
|
|
31
|
+
"orchestration_not_executed",
|
|
30
32
|
];
|
|
31
33
|
function command(commandText) {
|
|
32
34
|
return { kind: "command", command: commandText };
|
|
@@ -160,6 +162,16 @@ export const BLOCKING_REASON_INVENTORY = {
|
|
|
160
162
|
description: "Legacy pre-ownership state detected; review the migration plan before continuing.",
|
|
161
163
|
continuation: () => command("aiw migrate --dry-run"),
|
|
162
164
|
},
|
|
165
|
+
worktrees_leaked: {
|
|
166
|
+
code: "worktrees_leaked",
|
|
167
|
+
description: "Managed orchestration worktrees remain on disk after their plan finished; clean them up.",
|
|
168
|
+
continuation: () => action("Run `aiw orchestrate --cleanup --execute` to remove them."),
|
|
169
|
+
},
|
|
170
|
+
orchestration_not_executed: {
|
|
171
|
+
code: "orchestration_not_executed",
|
|
172
|
+
description: "The plan has not been executed yet; there is no checkpoint to integrate.",
|
|
173
|
+
continuation: () => action("Run `aiw orchestrate --plan=<path> --execute` first; integration forecasts need the checkpoint evidence."),
|
|
174
|
+
},
|
|
163
175
|
};
|
|
164
176
|
export function getBlockingReasonDefinition(code) {
|
|
165
177
|
const definition = BLOCKING_REASON_INVENTORY[code];
|
package/dist/orchestration.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export type OrchestrationTaskResult = {
|
|
|
21
21
|
worktree?: string;
|
|
22
22
|
baseCommit?: string;
|
|
23
23
|
finalCommit?: string;
|
|
24
|
+
/** OPP-017 execution evidence. */
|
|
25
|
+
durationMs?: number;
|
|
26
|
+
diffStat?: string;
|
|
24
27
|
};
|
|
25
28
|
export type IsolationOptions = {
|
|
26
29
|
/** Repository root the plan runs in. */
|
|
@@ -41,6 +44,23 @@ export declare function createTaskWorktrees(root: string, aiw: string, plan: Orc
|
|
|
41
44
|
}>;
|
|
42
45
|
/** Final commit recorded from an isolated worktree (evidence for the checkpoint). */
|
|
43
46
|
export declare function worktreeFinalCommit(worktree: string): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* OPP-017 integration forecast: wave-ordered merge preview for the isolated
|
|
49
|
+
* task branches against the current branch. Deterministic `git merge-tree`
|
|
50
|
+
* dry runs — nothing is merged. Conflicts surface per task before any human
|
|
51
|
+
* merge is attempted.
|
|
52
|
+
*/
|
|
53
|
+
export type IntegrationForecastLine = {
|
|
54
|
+
wave: number;
|
|
55
|
+
taskId: string;
|
|
56
|
+
branch: string;
|
|
57
|
+
state: "clean" | "conflict" | "missing" | "identical";
|
|
58
|
+
conflicts?: string[];
|
|
59
|
+
};
|
|
60
|
+
export declare function currentBranch(root: string): string;
|
|
61
|
+
export declare function forecastIntegration(root: string, plan: OrchestrationPlan, results: OrchestrationTaskResult[]): IntegrationForecastLine[];
|
|
62
|
+
/** Parse a serialized orchestration checkpoint back into task results. */
|
|
63
|
+
export declare function parseOrchestrationResults(content: string): OrchestrationTaskResult[];
|
|
44
64
|
/** Remove isolated worktrees; failed-task worktrees are kept unless forced. */
|
|
45
65
|
export declare function removeWorktrees(root: string, paths: Iterable<string>, options?: {
|
|
46
66
|
force?: boolean;
|
|
@@ -64,4 +84,4 @@ export declare function executeOrchestrationPlan(plan: OrchestrationPlan, orches
|
|
|
64
84
|
}): Promise<OrchestrationTaskResult[]>;
|
|
65
85
|
export declare function validateOrchestrationParallelism(value: number): number;
|
|
66
86
|
export declare function serializeOrchestrationPreview(plan: OrchestrationPlan): string;
|
|
67
|
-
export declare function serializeOrchestrationResults(plan: OrchestrationPlan, results: OrchestrationTaskResult[]): string;
|
|
87
|
+
export declare function serializeOrchestrationResults(plan: OrchestrationPlan, results: OrchestrationTaskResult[], wallClockMs?: number): string;
|
package/dist/orchestration.js
CHANGED
|
@@ -35,6 +35,110 @@ export function worktreeFinalCommit(worktree) {
|
|
|
35
35
|
return undefined;
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
function gitDiffStat(root, worktree, base, final) {
|
|
39
|
+
try {
|
|
40
|
+
return execFileSync("git", ["-C", worktree, "diff", "--stat", `${base}..${final}`], {
|
|
41
|
+
cwd: root,
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
}).trim();
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function boundedDiffStat(stat) {
|
|
50
|
+
if (!stat)
|
|
51
|
+
return undefined;
|
|
52
|
+
const lines = stat.split("\n");
|
|
53
|
+
const bounded = lines.slice(0, 20).join("\n");
|
|
54
|
+
return lines.length > 20 ? `${bounded}\n(truncated)` : bounded;
|
|
55
|
+
}
|
|
56
|
+
export function currentBranch(root) {
|
|
57
|
+
return git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
58
|
+
}
|
|
59
|
+
export function forecastIntegration(root, plan, results) {
|
|
60
|
+
const waves = orchestrationWaves(plan);
|
|
61
|
+
const byId = new Map(results.map((result) => [result.id, result]));
|
|
62
|
+
const target = currentBranch(root);
|
|
63
|
+
const lines = [];
|
|
64
|
+
waves.forEach((wave, index) => {
|
|
65
|
+
for (const taskId of wave) {
|
|
66
|
+
const branch = `aiw/${plan.id}/${taskId}`;
|
|
67
|
+
let branchTip;
|
|
68
|
+
try {
|
|
69
|
+
branchTip = git(root, ["rev-parse", "--verify", branch]);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
lines.push({ wave: index + 1, taskId, branch, state: "missing" });
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const result = byId.get(taskId);
|
|
76
|
+
if (result?.finalCommit && result.finalCommit === branchTip) {
|
|
77
|
+
// nothing new on the branch beyond the recorded final commit
|
|
78
|
+
}
|
|
79
|
+
let output;
|
|
80
|
+
let conflict = false;
|
|
81
|
+
try {
|
|
82
|
+
output = execFileSync("git", ["merge-tree", "--write-tree", "--name-only", target, branchTip], { cwd: root, encoding: "utf8" });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
conflict = true;
|
|
86
|
+
output =
|
|
87
|
+
error instanceof Error && "stdout" in error
|
|
88
|
+
? String(error.stdout ?? "")
|
|
89
|
+
: "";
|
|
90
|
+
}
|
|
91
|
+
if (conflict) {
|
|
92
|
+
const conflictFiles = output
|
|
93
|
+
.split("\n")
|
|
94
|
+
.filter((line) => line.length > 0 && !line.startsWith("^[0-9a-f]"))
|
|
95
|
+
.filter((line) => !/^[0-9a-f]{40}$/.test(line.trim()))
|
|
96
|
+
.slice(0, 5);
|
|
97
|
+
lines.push({
|
|
98
|
+
wave: index + 1,
|
|
99
|
+
taskId,
|
|
100
|
+
branch,
|
|
101
|
+
state: "conflict",
|
|
102
|
+
conflicts: conflictFiles.length > 0 ? conflictFiles : ["merge conflicts detected"],
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const mergedTree = output.split("\n")[0]?.trim();
|
|
107
|
+
const identical = mergedTree === git(root, ["rev-parse", target + "^{tree}"]);
|
|
108
|
+
lines.push({
|
|
109
|
+
wave: index + 1,
|
|
110
|
+
taskId,
|
|
111
|
+
branch,
|
|
112
|
+
state: identical ? "identical" : "clean",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
return lines;
|
|
117
|
+
}
|
|
118
|
+
/** Parse a serialized orchestration checkpoint back into task results. */
|
|
119
|
+
export function parseOrchestrationResults(content) {
|
|
120
|
+
const results = [];
|
|
121
|
+
const blocks = content.split(/\n(?= {2}- id: )/);
|
|
122
|
+
for (const block of blocks) {
|
|
123
|
+
const id = /^ {2}- id: (.+)$/m.exec(block)?.[1];
|
|
124
|
+
if (!id)
|
|
125
|
+
continue;
|
|
126
|
+
const status = /^ {4}status: (.+)$/m.exec(block)?.[1];
|
|
127
|
+
const detail = /^ {4}detail: (.+)$/m.exec(block)?.[1];
|
|
128
|
+
const worktree = /^ {4}worktree: "?([^"\n]+)"?$/m.exec(block)?.[1];
|
|
129
|
+
const baseCommit = /^ {4}baseCommit: "?([^"\n]+)"?$/m.exec(block)?.[1];
|
|
130
|
+
const finalCommit = /^ {4}finalCommit: "?([^"\n]+)"?$/m.exec(block)?.[1];
|
|
131
|
+
results.push({
|
|
132
|
+
id,
|
|
133
|
+
status: status ?? "blocked",
|
|
134
|
+
detail: detail?.replace(/^"|"$/g, "") ?? "",
|
|
135
|
+
...(worktree ? { worktree } : {}),
|
|
136
|
+
...(baseCommit ? { baseCommit } : {}),
|
|
137
|
+
...(finalCommit ? { finalCommit } : {}),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
38
142
|
/** Remove isolated worktrees; failed-task worktrees are kept unless forced. */
|
|
39
143
|
export function removeWorktrees(root, paths, options = {}) {
|
|
40
144
|
for (const path of paths) {
|
|
@@ -139,19 +243,26 @@ export async function executeOrchestrationPlan(plan, orchestrator, maxParallel,
|
|
|
139
243
|
results.set(id, { id, status: "blocked", detail: "A dependency did not pass." });
|
|
140
244
|
return;
|
|
141
245
|
}
|
|
246
|
+
const startedAt = Date.now();
|
|
142
247
|
try {
|
|
143
248
|
const isolated = isolation?.worktrees.get(id);
|
|
144
249
|
const outcome = await orchestrator.run(isolated ? { ...task, worktree: isolated.worktree } : task);
|
|
145
250
|
const base = isolation?.worktrees.get(id);
|
|
251
|
+
const finalCommit = base ? worktreeFinalCommit(base.worktree) : undefined;
|
|
252
|
+
const diffStat = base && finalCommit
|
|
253
|
+
? boundedDiffStat(gitDiffStat(isolation.options.root, base.worktree, base.baseCommit, finalCommit))
|
|
254
|
+
: undefined;
|
|
146
255
|
results.set(id, {
|
|
147
256
|
id,
|
|
148
257
|
status: outcome.success ? "passed" : "failed",
|
|
149
258
|
detail: safeDetail(outcome.detail),
|
|
259
|
+
durationMs: Date.now() - startedAt,
|
|
150
260
|
...(base
|
|
151
261
|
? {
|
|
152
262
|
worktree: base.worktree,
|
|
153
263
|
baseCommit: base.baseCommit,
|
|
154
|
-
finalCommit
|
|
264
|
+
finalCommit,
|
|
265
|
+
...(diffStat ? { diffStat } : {}),
|
|
155
266
|
}
|
|
156
267
|
: {}),
|
|
157
268
|
});
|
|
@@ -178,15 +289,17 @@ export function serializeOrchestrationPreview(plan) {
|
|
|
178
289
|
.map((wave, index) => ` - index: ${index + 1}\n tasks: [${wave.join(", ")}]`)
|
|
179
290
|
.join("\n")}\n`;
|
|
180
291
|
}
|
|
181
|
-
export function serializeOrchestrationResults(plan, results) {
|
|
292
|
+
export function serializeOrchestrationResults(plan, results, wallClockMs) {
|
|
182
293
|
return `schema: 1\nplan: ${plan.id}\nresults:\n${results
|
|
183
294
|
.map((result) => {
|
|
184
295
|
const isolation = result.worktree
|
|
185
|
-
? `\n worktree: ${JSON.stringify(result.worktree)}\n baseCommit: ${JSON.stringify(result.baseCommit ?? "")}\n finalCommit: ${JSON.stringify(result.finalCommit ?? "")}`
|
|
186
|
-
:
|
|
296
|
+
? `\n worktree: ${JSON.stringify(result.worktree)}\n baseCommit: ${JSON.stringify(result.baseCommit ?? "")}\n finalCommit: ${JSON.stringify(result.finalCommit ?? "")}${result.durationMs !== undefined ? `\n durationMs: ${result.durationMs}` : ""}${result.diffStat ? `\n diffStat: ${JSON.stringify(result.diffStat)}` : ""}`
|
|
297
|
+
: result.durationMs !== undefined
|
|
298
|
+
? `\n durationMs: ${result.durationMs}`
|
|
299
|
+
: "";
|
|
187
300
|
return ` - id: ${result.id}\n status: ${result.status}\n detail: ${JSON.stringify(result.detail)}${isolation}`;
|
|
188
301
|
})
|
|
189
|
-
.join("\n")}\n`;
|
|
302
|
+
.join("\n")}${wallClockMs !== undefined ? `\nwallClockMs: ${wallClockMs}` : ""}\n`;
|
|
190
303
|
}
|
|
191
304
|
function assertPlanGraph(tasks) {
|
|
192
305
|
const ids = tasks.map(({ id }) => id);
|
package/dist/workflow.js
CHANGED
|
@@ -89,7 +89,7 @@ import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, se
|
|
|
89
89
|
import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
90
90
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
91
91
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
92
|
-
import { createTaskWorktrees, executeOrchestrationPlan, listManagedWorktrees, removeWorktrees, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
92
|
+
import { createTaskWorktrees, executeOrchestrationPlan, forecastIntegration, listManagedWorktrees, currentBranch, parseOrchestrationResults, removeWorktrees, parseOrchestrationPlan, serializeOrchestrationPreview, serializeOrchestrationResults, validateOrchestrationParallelism, } from "./orchestration.js";
|
|
93
93
|
function laneAdvisory(root, fs, lightChosen) {
|
|
94
94
|
const paths = collectWorkingTreeChanges(root);
|
|
95
95
|
if (!paths || paths.length === 0)
|
|
@@ -560,6 +560,37 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
560
560
|
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
561
561
|
};
|
|
562
562
|
const execute = executeCount === 1;
|
|
563
|
+
const integrateRequested = args[1] === "integrate";
|
|
564
|
+
if (integrateRequested) {
|
|
565
|
+
const integrateParsed = parseNamedOptions(args.slice(2), ["plan"]);
|
|
566
|
+
if (!integrateParsed?.plan)
|
|
567
|
+
return { exitCode: 1, error: "Usage: aiw orchestrate integrate --plan=path" };
|
|
568
|
+
const planPath = safeProjectFile(root, integrateParsed.plan, fs);
|
|
569
|
+
const plan = parseOrchestrationPlan(fs.read(planPath));
|
|
570
|
+
const checkpoint = join(aiw, `checkpoints/orchestration-${plan.id}.yml`);
|
|
571
|
+
if (!fs.exists(checkpoint))
|
|
572
|
+
throw new BlockingError(describeBlockingReason("orchestration_not_executed", {
|
|
573
|
+
detail: `No checkpoint for plan ${plan.id} — the plan has not been executed yet.`,
|
|
574
|
+
}));
|
|
575
|
+
const results = parseOrchestrationResults(fs.read(checkpoint));
|
|
576
|
+
const forecast = forecastIntegration(root, plan, results);
|
|
577
|
+
const lines = [`Integration forecast for plan ${plan.id} → ${currentBranch(root)}`];
|
|
578
|
+
let conflicts = 0;
|
|
579
|
+
for (const line of forecast) {
|
|
580
|
+
if (line.state === "conflict")
|
|
581
|
+
conflicts += 1;
|
|
582
|
+
lines.push(line.state === "conflict"
|
|
583
|
+
? `wave ${line.wave} · ${line.taskId} (${line.branch}): CONFLICTS — ${(line.conflicts ?? []).join(", ")}`
|
|
584
|
+
: line.state === "missing"
|
|
585
|
+
? `wave ${line.wave} · ${line.taskId}: branch missing (${line.branch})`
|
|
586
|
+
: line.state === "identical"
|
|
587
|
+
? `wave ${line.wave} · ${line.taskId}: already integrated`
|
|
588
|
+
: `wave ${line.wave} · ${line.taskId} (${line.branch}): clean merge`);
|
|
589
|
+
}
|
|
590
|
+
lines.push(`conflicts: ${conflicts}`);
|
|
591
|
+
const output = lines.join("\n");
|
|
592
|
+
return conflicts > 0 ? { exitCode: 1, output, error: undefined } : { exitCode: 0, output };
|
|
593
|
+
}
|
|
563
594
|
const isolate = args.includes("--isolate");
|
|
564
595
|
const cleanup = args.includes("--cleanup");
|
|
565
596
|
if (cleanup) {
|
|
@@ -599,6 +630,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
599
630
|
exitCode: 1,
|
|
600
631
|
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
601
632
|
};
|
|
633
|
+
const wallClockStart = Date.now();
|
|
602
634
|
try {
|
|
603
635
|
const isolation = isolate
|
|
604
636
|
? {
|
|
@@ -613,7 +645,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
613
645
|
.map(({ worktree }) => worktree);
|
|
614
646
|
removeWorktrees(root, passedPaths);
|
|
615
647
|
}
|
|
616
|
-
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
648
|
+
fs.write(checkpoint, serializeOrchestrationResults(plan, results, Date.now() - wallClockStart));
|
|
617
649
|
const failed = results.filter(({ status }) => status !== "passed");
|
|
618
650
|
return failed.length
|
|
619
651
|
? {
|
|
@@ -1832,6 +1864,14 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1832
1864
|
};
|
|
1833
1865
|
return { exitCode: 1, error: `Missing AI Workflow files: ${missing.join(", ")}` };
|
|
1834
1866
|
}
|
|
1867
|
+
const leakedWorktrees = listManagedWorktrees(root, aiw);
|
|
1868
|
+
if (leakedWorktrees.length > 0)
|
|
1869
|
+
return {
|
|
1870
|
+
exitCode: 1,
|
|
1871
|
+
error: formatBlockingOutput(describeBlockingReason("worktrees_leaked", {
|
|
1872
|
+
detail: `${leakedWorktrees.length} managed orchestration worktree(s) leaked: ${leakedWorktrees.join(", ")}`,
|
|
1873
|
+
})),
|
|
1874
|
+
};
|
|
1835
1875
|
try {
|
|
1836
1876
|
parseManifest(fs.read(join(aiw, "manifest.yml")));
|
|
1837
1877
|
parseOwnership(fs.read(join(aiw, "ownership.yml")));
|
package/docs/backlog.md
CHANGED
|
@@ -129,7 +129,7 @@ Derived from the Gentle-AI ODD/RDD analysis; scope and rejected alternatives rec
|
|
|
129
129
|
- [x] **OPP-014** Paired Verification Contract — plan declares `command → expected`, evidence records `command → observed`, gates compare pairs. _Acceptance:_ see [OPP-014](../opportunities/OPP-014-paired-verification-contract.md); mismatch/missing pairs fail with typed reasons and machine-readable task ids; gates never execute commands; legacy format accepted with advisory.
|
|
130
130
|
- [x] **OPP-015** Token & Recovery Governance — enforced stage budgets, context-recovery managed block, advisory lane suggestion, regression-pinning declaration. _Acceptance:_ see [OPP-015](../opportunities/OPP-015-token-recovery-governance.md); budgets derive from saved baselines and default-inert; recovery block in every adapter rendering; advisory line never blocks.
|
|
131
131
|
- [ ] **OPP-016** Behavioral evaluation phase 2 — scenario registry replayed against real harness CLIs. _Acceptance:_ see [OPP-016](../opportunities/OPP-016-behavioral-eval-scenarios.md); deterministic state-outcome assertions, skipped-harness markers, advisory-first before gates, usage recorded per run.
|
|
132
|
-
- [
|
|
132
|
+
- [x] **OPP-017** Orchestration depth — integration forecast, doctor leak reporting, diff-stat evidence, per-harness honor matrix. _Acceptance:_ see [OPP-017](../opportunities/OPP-017-orchestration-depth.md); wave-ordered merge forecast with merge-tree dry run, typed cleanup continuation from doctor, no auto-merge.
|
|
133
133
|
- [x] **OPP-018** Supported migration path for legacy (pre-ownership) installations. _Acceptance:_ see [OPP-018](../opportunities/OPP-018-legacy-migration.md); dry-run lists archived/rebuilt/preserved/manual, transactional apply with rollback, fail-closed on ambiguity, doctor/install point at `aiw migrate`.
|
|
134
134
|
|
|
135
135
|
## Backlog rules
|
|
@@ -27,3 +27,5 @@ Every blocking `aiw` exit carries a stable reason code plus either an exact runn
|
|
|
27
27
|
| `context_budget_exceeded` | The served context exceeds the declared ceiling for this stage; trim it or raise the ceiling explicitly. | explicit action |
|
|
28
28
|
| `stage_not_in_process` | This stage does not exist in the active workflow's process; use the process's own stages. | explicit action |
|
|
29
29
|
| `migration_required` | Legacy pre-ownership state detected; review the migration plan before continuing. | runnable command |
|
|
30
|
+
| `worktrees_leaked` | Managed orchestration worktrees remain on disk after their plan finished; clean them up. | explicit action |
|
|
31
|
+
| `orchestration_not_executed` | The plan has not been executed yet; there is no checkpoint to integrate. | explicit action |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# OPP-017: Orchestration depth — integration tooling, leak reporting, per-harness guidance
|
|
2
2
|
|
|
3
|
-
- Status:
|
|
3
|
+
- Status: Implemented (2026-11-04): integration forecast, doctor leak reporting, diff-stat and wall-clock evidence, honor matrix documented; parallel-execution harness parity tests remain an open question
|
|
4
4
|
- Priority: P2
|
|
5
5
|
- Added: 2026-11-01
|
|
6
6
|
- Decision: extends [ADR-001](../../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md) Decision A
|
package/docs/orchestration.md
CHANGED
|
@@ -35,6 +35,22 @@ The first command prints deterministic execution waves and does not invoke an ag
|
|
|
35
35
|
|
|
36
36
|
Each executed plan writes one `.aiw/checkpoints/orchestration-<plan-id>.yml` result. Existing checkpoints are never overwritten: use a new plan identifier for a new run. Agent result details are bounded and serialized safely.
|
|
37
37
|
|
|
38
|
+
## Isolation and integration (OPP-010/OPP-017)
|
|
39
|
+
|
|
40
|
+
`aiw orchestrate --plan=plan.yml --execute --isolate` creates one branch-per-task git worktree (`aiw/<plan>/<task>` from the plan HEAD) and hands the path to the adapter through the optional `worktree` field on the orchestration task. Passed-task worktrees are removed after evidence capture; failed-task worktrees are kept for diagnosis. Checkpoints record per-task worktree path, base commit, final commit, diff stat, and duration, plus the plan's wall-clock time.
|
|
41
|
+
|
|
42
|
+
`aiw orchestrate integrate --plan=plan.yml` prints the wave-ordered integration forecast for the task branches: a deterministic `git merge-tree` dry run against the current branch, reporting conflicts per task before any human merge. Conflicts make the command exit nonzero; nothing is ever merged by the tool. `aiw orchestrate --cleanup --execute` removes managed worktrees left by crashed runs, and `aiw doctor` reports leaks with the cleanup continuation.
|
|
43
|
+
|
|
44
|
+
### Worktree honor matrix
|
|
45
|
+
|
|
46
|
+
The adapter contract owns process execution; aiw owns git isolation. What each documented integration does with `task.worktree`:
|
|
47
|
+
|
|
48
|
+
| Integration | Honor | Notes |
|
|
49
|
+
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
50
|
+
| Host-integrated adapters (in-process) | Receives the path; runs the task with the worktree as its working directory or fails with a typed error | The minimal adapter contract: never run against the main tree when a worktree is provided |
|
|
51
|
+
| Claude Code / Codex / Gemini CLIs launched by a host adapter | Honored by passing the worktree as the session working directory | Harness-native worktree features may substitute; the checkpoint evidence stays authoritative |
|
|
52
|
+
| Adapters without worktree support | Must reject `--isolate` before any task runs | The plan fails before execution rather than silently running in the main tree |
|
|
53
|
+
|
|
38
54
|
## Adapter contract
|
|
39
55
|
|
|
40
56
|
```ts
|