@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 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 =
@@ -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];
@@ -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;
@@ -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: worktreeFinalCommit(base.worktree),
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
- - [ ] **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.
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: Proposed
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
@@ -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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {