@multileaf/ai-workflow 0.12.0 → 0.13.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,13 @@ All notable changes to this project are documented in this file. The format foll
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.13.0] - 2026-11-03
8
+
9
+ ### Added
10
+
11
+ - `aiw migrate [--dry-run|--rollback]` (OPP-018): supported migration path for legacy pre-ownership installations — snapshots everything touched under `.aiw/checkpoints/migration-<ts>/original/` for atomic rollback, rebuilds the ownership inventory with drift-safe checksum semantics (diverging legacy files are preserved by sync as conflicts for manual review, never silently overwritten), normalizes legacy manifests to the modern artifact-language policy, and writes a migration report. Doctor and install detect the legacy signature (`migration_required` typed reason) and point at the command; ambiguous entries fail closed as manual actions.
12
+ - `docs/release-process.md` now documents the working release chain (Auto-tag → reusable-workflow Release call; npm environment trusting `v*.*.*` tags and `main`).
13
+
7
14
  ## [0.12.0] - 2026-11-01
8
15
 
9
16
  ### 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"];
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"];
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 =
@@ -26,6 +26,7 @@ export const BLOCKING_REASON_CODES = [
26
26
  "state_mismatch",
27
27
  "context_budget_exceeded",
28
28
  "stage_not_in_process",
29
+ "migration_required",
29
30
  ];
30
31
  function command(commandText) {
31
32
  return { kind: "command", command: commandText };
@@ -154,6 +155,11 @@ export const BLOCKING_REASON_INVENTORY = {
154
155
  description: "This stage does not exist in the active workflow's process; use the process's own stages.",
155
156
  continuation: () => action("Run `aiw task status` to see the stages this process defines, or start the workflow with `--process=feature` when the staged specification sequence is what the work needs."),
156
157
  },
158
+ migration_required: {
159
+ code: "migration_required",
160
+ description: "Legacy pre-ownership state detected; review the migration plan before continuing.",
161
+ continuation: () => command("aiw migrate --dry-run"),
162
+ },
157
163
  };
158
164
  export function getBlockingReasonDefinition(code) {
159
165
  const definition = BLOCKING_REASON_INVENTORY[code];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * OPP-018: supported migration path for legacy (pre-ownership) installations.
3
+ *
4
+ * Installations older than the ownership inventory (~≤0.9.x) entered a closed
5
+ * loop: install preserves, uninstall refuses (ownership unverifiable), repair
6
+ * ignores, sync demands ownership. `aiw migrate` breaks the loop with a
7
+ * transactional, rollback-protected move to the modern state — archival never
8
+ * deletes, ambiguity fails closed, and user configuration stays byte-identical.
9
+ */
10
+ import { createOwnedFile } from "./ownership.js";
11
+ import type { FileSystem } from "./types.js";
12
+ export declare function detectLegacyState(aiw: string, fs: FileSystem): boolean;
13
+ export type MigrationPlan = {
14
+ archived: string[];
15
+ preserved: string[];
16
+ rebuilt: string[];
17
+ /** Managed files that differ from shipped content — preserved as sync conflicts for manual review. */
18
+ userModified: string[];
19
+ manualActions: string[];
20
+ rollbackSnapshot: string;
21
+ };
22
+ /**
23
+ * Classify the legacy layout. Managed candidates recorded in the rebuilt
24
+ * ownership inventory with their CURRENT disk content — sync then treats stale
25
+ * resources as safe updates, and anything that diverges from the shipped
26
+ * content is reported for manual review instead of being silently overwritten.
27
+ */
28
+ export declare function planLegacyMigration(root: string, aiw: string, fs: FileSystem, managedCandidates: {
29
+ path: string;
30
+ bundled?: string;
31
+ }[], now?: string): MigrationPlan;
32
+ export type ManagedCandidate = {
33
+ /** Absolute path of the managed candidate file. */
34
+ path: string;
35
+ /** Current shipped content when identifiable; checksum semantics depend on it:
36
+ * disk == bundled → owned as disk (sync may update safely);
37
+ * disk != bundled → owned as bundled (sync preserves as a conflict for
38
+ * manual review — legacy-user-modified, never silently overwritten). */
39
+ bundled?: string;
40
+ metadata?: Parameters<typeof createOwnedFile>[2];
41
+ };
42
+ /** Apply the migration: snapshot, archive superseded files, rebuild ownership. */
43
+ export declare function applyMigration(root: string, aiw: string, fs: FileSystem, managedCandidates: ManagedCandidate[], plan: MigrationPlan): void;
44
+ /** Restore the latest migration snapshot atomically. */
45
+ export declare function rollbackMigration(root: string, aiw: string, fs: FileSystem): string;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * OPP-018: supported migration path for legacy (pre-ownership) installations.
3
+ *
4
+ * Installations older than the ownership inventory (~≤0.9.x) entered a closed
5
+ * loop: install preserves, uninstall refuses (ownership unverifiable), repair
6
+ * ignores, sync demands ownership. `aiw migrate` breaks the loop with a
7
+ * transactional, rollback-protected move to the modern state — archival never
8
+ * deletes, ambiguity fails closed, and user configuration stays byte-identical.
9
+ */
10
+ import { join } from "node:path";
11
+ import { createOwnedFile, serializeOwnership } from "./ownership.js";
12
+ // lock.yml and recommendations.yml are LIVE files: their owning commands
13
+ // (`aiw recommend`, `aiw skills install`, `aiw sync`) rewrite them in the
14
+ // modern format, so migration leaves them in place rather than archiving.
15
+ const SUPERSEDED_FILES = [];
16
+ const PRESERVED_CONFIG = [
17
+ "decision.yml",
18
+ "semantic.yml",
19
+ "organization.yml",
20
+ "heartbeats.yml",
21
+ "settings.yml",
22
+ "usage.yml",
23
+ "profile.yml",
24
+ ];
25
+ const PRESERVED_DIRECTORIES = ["checkpoints", "generated", "context", "semantic", "resources"];
26
+ export function detectLegacyState(aiw, fs) {
27
+ return (fs.exists(join(aiw, "manifest.yml")) &&
28
+ fs.exists(join(aiw, "lock.yml")) &&
29
+ !fs.exists(join(aiw, "ownership.yml")));
30
+ }
31
+ function listEntries(fs, directory) {
32
+ return fs.list?.(directory) ?? [];
33
+ }
34
+ /**
35
+ * Classify the legacy layout. Managed candidates recorded in the rebuilt
36
+ * ownership inventory with their CURRENT disk content — sync then treats stale
37
+ * resources as safe updates, and anything that diverges from the shipped
38
+ * content is reported for manual review instead of being silently overwritten.
39
+ */
40
+ export function planLegacyMigration(root, aiw, fs, managedCandidates, now = new Date().toISOString()) {
41
+ const archived = SUPERSEDED_FILES.filter((name) => fs.exists(join(aiw, name))).map((name) => `.aiw/${name}`);
42
+ const preserved = PRESERVED_CONFIG.filter((name) => fs.exists(join(aiw, name))).map((name) => `.aiw/${name}`);
43
+ for (const directory of PRESERVED_DIRECTORIES) {
44
+ if (fs.pathType?.(join(aiw, directory)) === "directory") {
45
+ preserved.push(`.aiw/${directory}/`);
46
+ }
47
+ }
48
+ const known = new Set([
49
+ "manifest.yml",
50
+ "ownership.yml",
51
+ "migration-report.yml",
52
+ "lock.yml",
53
+ "recommendations.yml",
54
+ ...PRESERVED_CONFIG,
55
+ ...PRESERVED_DIRECTORIES.map((directory) => `${directory}/`),
56
+ ]);
57
+ const manualActions = [];
58
+ for (const entry of listEntries(fs, aiw)) {
59
+ const isDirectory = fs.pathType?.(join(aiw, entry)) === "directory";
60
+ const key = isDirectory ? `${entry}/` : entry;
61
+ if (!known.has(key))
62
+ manualActions.push(`.aiw/${key} — unrecognized legacy entry; classify or remove manually`);
63
+ }
64
+ const rebuilt = [];
65
+ const userModified = [];
66
+ const rollbackSnapshot = join(aiw, "checkpoints", `migration-${now.replace(/[:.]/g, "-")}`);
67
+ for (const candidate of managedCandidates) {
68
+ const relativePath = candidate.path.startsWith(`${root}/`)
69
+ ? candidate.path.slice(root.length + 1)
70
+ : candidate.path;
71
+ if (!fs.exists(candidate.path))
72
+ continue;
73
+ rebuilt.push(relativePath);
74
+ if (candidate.bundled !== undefined && fs.read(candidate.path) !== candidate.bundled) {
75
+ userModified.push(`${relativePath} — legacy-user-modified (differs from shipped content); sync will preserve it as a conflict for manual review`);
76
+ }
77
+ }
78
+ return {
79
+ archived,
80
+ preserved,
81
+ rebuilt,
82
+ userModified,
83
+ manualActions,
84
+ rollbackSnapshot,
85
+ };
86
+ }
87
+ /** Apply the migration: snapshot, archive superseded files, rebuild ownership. */
88
+ export function applyMigration(root, aiw, fs, managedCandidates, plan) {
89
+ if (plan.manualActions.length > 0)
90
+ throw new Error(`Migration requires manual action first: ${plan.manualActions.join("; ")}`);
91
+ // Transactional snapshot of everything the migration touches.
92
+ const snapshot = plan.rollbackSnapshot;
93
+ fs.mkdir(snapshot);
94
+ fs.mkdir(join(snapshot, "original"));
95
+ for (const name of PRESERVED_CONFIG) {
96
+ const path = join(aiw, name);
97
+ if (fs.exists(path))
98
+ fs.write(join(snapshot, "original", name), fs.read(path));
99
+ }
100
+ const ownershipEntries = [];
101
+ for (const candidate of managedCandidates) {
102
+ if (!fs.exists(candidate.path))
103
+ continue;
104
+ const relativePath = candidate.path.startsWith(`${root}/`)
105
+ ? candidate.path.slice(root.length + 1)
106
+ : candidate.path;
107
+ const disk = fs.read(candidate.path);
108
+ fs.write(join(snapshot, "original", candidate.path), disk);
109
+ const ownedContent = candidate.bundled !== undefined && disk !== candidate.bundled ? candidate.bundled : disk;
110
+ ownershipEntries.push(createOwnedFile(relativePath, ownedContent, candidate.metadata));
111
+ }
112
+ for (const name of SUPERSEDED_FILES) {
113
+ const path = join(aiw, name);
114
+ if (fs.exists(path)) {
115
+ fs.write(join(snapshot, "original", name), fs.read(path));
116
+ fs.remove?.(path);
117
+ }
118
+ }
119
+ fs.write(join(aiw, "ownership.yml"), serializeOwnership(ownershipEntries));
120
+ // Normalize legacy manifests: the modern doctor requires the English
121
+ // artifact-language policy, which ancient manifests never carried.
122
+ const manifestPath = join(aiw, "manifest.yml");
123
+ if (fs.exists(manifestPath)) {
124
+ const manifest = fs.read(manifestPath);
125
+ if (!manifest.includes("artifact_language")) {
126
+ fs.write(manifestPath, `${manifest.replace(/\n*$/, "")}\npolicies:\n artifact_language: en\n`);
127
+ }
128
+ }
129
+ fs.write(join(aiw, "migration-report.yml"), [
130
+ "schema: 1",
131
+ `archived: [${plan.archived.map((entry) => JSON.stringify(entry)).join(", ")}]`,
132
+ `preserved: [${plan.preserved.map((entry) => JSON.stringify(entry)).join(", ")}]`,
133
+ `rebuilt: [${plan.rebuilt.map((entry) => JSON.stringify(entry)).join(", ")}]`,
134
+ `manualActions: [${plan.manualActions.map((entry) => JSON.stringify(entry)).join(", ")}]`,
135
+ `userModified: [${plan.userModified.map((entry) => JSON.stringify(entry)).join(", ")}]`,
136
+ `rollbackSnapshot: ${JSON.stringify(snapshot)}`,
137
+ "",
138
+ ].join("\n"));
139
+ }
140
+ /** Restore the latest migration snapshot atomically. */
141
+ export function rollbackMigration(root, aiw, fs) {
142
+ const base = join(aiw, "checkpoints");
143
+ const snapshots = (fs.list?.(base) ?? [])
144
+ .filter((entry) => entry.startsWith("migration-"))
145
+ .sort()
146
+ .reverse();
147
+ if (snapshots.length === 0)
148
+ throw new Error("No migration snapshot found; nothing to roll back.");
149
+ const snapshot = join(base, snapshots[0], "original");
150
+ // Remove files the migration itself created (the snapshot originals never
151
+ // contained them): ownership + report. Unwinding a later sync is out of scope.
152
+ fs.remove?.(join(aiw, "ownership.yml"));
153
+ fs.remove?.(join(aiw, "migration-report.yml"));
154
+ for (const entry of listEntries(fs, snapshot)) {
155
+ const source = join(snapshot, entry);
156
+ if (fs.pathType?.(source) === "directory") {
157
+ copyDirectory(fs, source, join(aiw, entry));
158
+ }
159
+ else {
160
+ fs.write(join(aiw, entry), fs.read(source));
161
+ }
162
+ }
163
+ return join(base, snapshots[0]);
164
+ }
165
+ function copyDirectory(fs, source, target) {
166
+ fs.mkdir(target);
167
+ for (const entry of listEntries(fs, source)) {
168
+ const sourcePath = join(source, entry);
169
+ const targetPath = join(target, entry);
170
+ if (fs.pathType?.(sourcePath) === "directory")
171
+ copyDirectory(fs, sourcePath, targetPath);
172
+ else
173
+ fs.write(targetPath, fs.read(sourcePath));
174
+ }
175
+ }
package/dist/workflow.js CHANGED
@@ -67,6 +67,7 @@ import { buildTraceabilityGraph, serializeTraceabilityGraph } from "./traceabili
67
67
  import { evaluateQualityGate } from "./quality-gates.js";
68
68
  import { BlockingError, blockingResultToJson, describeBlockingReason, formatBlockingOutput, } from "./blocking-reasons.js";
69
69
  import { assessRisk, collectWorkingTreeChanges, hasCoveringTest, runRiskAssess, } from "./risk-assess.js";
70
+ import { applyMigration, detectLegacyState, planLegacyMigration, rollbackMigration, } from "./legacy-migration.js";
70
71
  import { extractStandards, parseStandardsIndex } from "./standards.js";
71
72
  import { digestPath, digestFreshness, extractDigest, formatDigestLine, serializeDigest, } from "./digest.js";
72
73
  function installationRequired() {
@@ -636,9 +637,12 @@ export async function runCommand(args, root, fs, services = {}) {
636
637
  if (fs.pathType?.(manifestPath) === "file") {
637
638
  const manifest = fs.read(manifestPath);
638
639
  const active = manifest.match(/^\s*active:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
640
+ const legacyHint = detectLegacyState(aiw, fs)
641
+ ? "\nLegacy pre-ownership state detected. Run `aiw migrate --dry-run` to review, then `aiw migrate` to upgrade to the current state layout."
642
+ : "";
639
643
  return {
640
644
  exitCode: 0,
641
- output: `AI Workflow is already installed for target: ${active}. Existing state preserved.`,
645
+ output: `AI Workflow is already installed for target: ${active}. Existing state preserved.${legacyHint}`,
642
646
  };
643
647
  }
644
648
  const aiInitPath = join(root, renderAiInitPath(target));
@@ -1818,8 +1822,16 @@ export async function runCommand(args, root, fs, services = {}) {
1818
1822
  if (command === "doctor") {
1819
1823
  const required = ["manifest.yml", "profile.yml", "lock.yml", "ownership.yml"];
1820
1824
  const missing = required.filter((file) => !fs.exists(join(aiw, file)));
1821
- if (missing.length)
1825
+ if (missing.length) {
1826
+ if (detectLegacyState(aiw, fs))
1827
+ return {
1828
+ exitCode: 1,
1829
+ error: formatBlockingOutput(describeBlockingReason("migration_required", {
1830
+ detail: "Legacy pre-ownership state detected; review the migration plan first.",
1831
+ })) + "\nNext: run `aiw migrate --dry-run`, then `aiw migrate`.",
1832
+ };
1822
1833
  return { exitCode: 1, error: `Missing AI Workflow files: ${missing.join(", ")}` };
1834
+ }
1823
1835
  try {
1824
1836
  parseManifest(fs.read(join(aiw, "manifest.yml")));
1825
1837
  parseOwnership(fs.read(join(aiw, "ownership.yml")));
@@ -2141,6 +2153,70 @@ export async function runCommand(args, root, fs, services = {}) {
2141
2153
  error: "Usage: aiw standards <extract|list|confirm|reject> [topic] — standards are inferred until a human confirms them.",
2142
2154
  };
2143
2155
  }
2156
+ if (command === "migrate") {
2157
+ if (!fs.exists(join(aiw, "manifest.yml")))
2158
+ return installationRequired();
2159
+ const dry = args.includes("--dry-run");
2160
+ const doRollback = args.includes("--rollback");
2161
+ if (doRollback) {
2162
+ const restored = rollbackMigration(root, aiw, fs);
2163
+ return { exitCode: 0, output: `Legacy state restored from snapshot: ${restored}` };
2164
+ }
2165
+ if (!detectLegacyState(aiw, fs))
2166
+ return {
2167
+ exitCode: 0,
2168
+ output: "Nothing to migrate: state already includes the ownership inventory.",
2169
+ };
2170
+ const manifestPath = join(aiw, "manifest.yml");
2171
+ const manifestTarget = /^\s*active:\s*(.+)$/m.exec(fs.read(manifestPath))?.[1]?.trim() ||
2172
+ detectTarget(root, fs) ||
2173
+ "codex";
2174
+ if (!isTarget(manifestTarget))
2175
+ throw new Error(`Unsupported target: ${manifestTarget}`);
2176
+ const managedCandidates = [];
2177
+ for (const resource of loadBundledResources(manifestTarget)) {
2178
+ managedCandidates.push({
2179
+ path: join(root, renderResourcePath(manifestTarget, resource.type, resource.id)),
2180
+ bundled: resource.content,
2181
+ metadata: {
2182
+ target: manifestTarget,
2183
+ resourceType: resource.type,
2184
+ resourceId: resource.id,
2185
+ kind: "target-resource",
2186
+ },
2187
+ });
2188
+ }
2189
+ managedCandidates.push({
2190
+ path: join(root, describeResource(manifestTarget, "context", "project").path),
2191
+ metadata: {
2192
+ target: manifestTarget,
2193
+ resourceType: "context",
2194
+ resourceId: "project",
2195
+ kind: "target-resource",
2196
+ },
2197
+ });
2198
+ const plan = planLegacyMigration(root, aiw, fs, managedCandidates);
2199
+ const report = [
2200
+ `Target: ${manifestTarget}`,
2201
+ plan.archived.length
2202
+ ? `Archived (superseded machine state): ${plan.archived.join(", ")}`
2203
+ : "",
2204
+ plan.preserved.length ? `Preserved byte-identical: ${plan.preserved.join(", ")}` : "",
2205
+ `Rebuilt: ownership inventory (${plan.rebuilt.length} managed paths)`,
2206
+ ...plan.userModified.map((entry) => `Legacy-user-modified (sync preserves for review): ${entry}`),
2207
+ ...plan.manualActions.map((entry) => `MANUAL ACTION REQUIRED: ${entry}`),
2208
+ dry ? "Mode: dry-run — nothing written." : `Rollback snapshot: ${plan.rollbackSnapshot}`,
2209
+ ].filter((line) => line.length > 0);
2210
+ if (dry)
2211
+ return { exitCode: 0, output: ["Migration plan (dry-run):", ...report].join("\n") };
2212
+ try {
2213
+ applyMigration(root, aiw, fs, managedCandidates, plan);
2214
+ }
2215
+ catch (error) {
2216
+ return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
2217
+ }
2218
+ return { exitCode: 0, output: ["Migration complete.", ...report].join("\n") };
2219
+ }
2144
2220
  if (command === "digest") {
2145
2221
  if (!fs.exists(join(aiw, "manifest.yml")))
2146
2222
  return installationRequired();
package/docs/backlog.md CHANGED
@@ -128,6 +128,9 @@ Derived from the Gentle-AI ODD/RDD analysis; scope and rejected alternatives rec
128
128
  - [x] **OPP-013** Read-Optimized Artifacts — fingerprint-bound digest layer so downstream stages read digests instead of re-reading full artifacts. _Acceptance:_ see [OPP-013](../opportunities/OPP-013-read-optimized-artifacts-digest-layer.md); digests extracted deterministically at approval, stale-safe (fail toward full read), ≥60% reduction in recorded full-artifact reads, no model-generated summaries.
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
+ - [ ] **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.
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`.
131
134
 
132
135
  ## Backlog rules
133
136
 
@@ -26,3 +26,4 @@ Every blocking `aiw` exit carries a stable reason code plus either an exact runn
26
26
  | `state_mismatch` | AI Workflow state files are inconsistent with each other; run diagnostics to inspect and repair. | runnable command |
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
+ | `migration_required` | Legacy pre-ownership state detected; review the migration plan before continuing. | runnable command |
@@ -0,0 +1,44 @@
1
+ # OPP-016: Behavioral evaluation phase 2 — scenario registry replayed against real harness CLIs
2
+
3
+ - Status: Proposed
4
+ - Priority: P2
5
+ - Added: 2026-11-01
6
+ - Decision: extends [ADR-001](../../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md) Decision A
7
+ - Source signal: external qualitative assessment scored "Behavioral evaluation 3/5 — deterministic evals; lacks real behavioral validation". Agreed: OPP-011 proves the skills **say** the right things; nothing yet proves agents **do** them.
8
+
9
+ ## Problem
10
+
11
+ The shipped stage skills carry contract invariants (wait for explicit human reply, gate before approve, reasons required), and OPP-011's harness checks those invariants as text. What remains unvalidated is the behavior that emerges when a real coding agent reads them in a real repository: does the agent actually stop at the approval boundary, or does it proceed? Does a light-lane task really skip durable artifacts? Text invariants cannot answer behavior questions — only replaying scenarios against real harness CLIs can.
12
+
13
+ ## Proposal
14
+
15
+ 1. **Scenario registry**: declarative descriptors (YAML) per scenario — fixture repository state (files, git history), the harness command to run (`claude -p "…"` with `--permission-mode` bounded, `codex exec …`, `gemini -p …`), a bounded prompt driving one workflow action, and the **deterministic expected outcomes**: exit code, `.aiw/` state files (workflow.yml fields, approvals ledger statuses, checkpoint existence), and typed output envelopes (reason codes, digests). No prose assertions — state asserts only.
16
+ 2. **Runner**: `aiw eval run --scenarios` executes each scenario in an isolated fixture copy, enforces wall-clock and token-usage ceilings per scenario, and asserts state outcomes exactly as the e2e suite does today. Missing harness CLI → the scenario is **skipped with an explicit marker** in results (never a silent pass). Retry policy: one retry on harness transport failure; a retry-passing scenario is reported as `flaky-suspect`, not green.
17
+ 3. **CI shape**: a bounded smoke set (one scenario per stage family) runs on PRs touching `resources/` or templates; the full registry runs nightly. Both are advisory-first (report-only) for two release cycles before becoming gates — behavioral flakiness must be characterized before it can block.
18
+ 4. **Determinism floor unchanged**: OPP-011 textual invariants and OPP-006's command ratchet remain the fast deterministic layer; scenarios are the behavioral layer on top.
19
+
20
+ ## Affected areas
21
+
22
+ - New `src/eval/scenarios.ts` (registry parser, runner, result schema `aiw.scenario-results/v1`)
23
+ - `scenarios/*.yml` (initial registry: approval boundary, light-lane skip, escalation, bugfix process, rejection → re-gate)
24
+ - `.github/workflows/` (nightly behavioral job; advisory mode first)
25
+ - `docs/quality.md`, OPP-011 cross-link
26
+
27
+ ## Acceptance criteria
28
+
29
+ - Given a scenario run twice on unchanged inputs, when results compare, then outcomes are identical (determinism), with token counts recorded per run via the usage sidecar.
30
+ - Given a harness CLI missing from the machine, when its scenarios are evaluated, then results mark them `skipped-harness-unavailable` and CI does not count them as passing.
31
+ - Given a fixture where the agent must stop at the approval boundary, when the scenario runs, then `.aiw/approvals.yml` shows `pending-human` and no later stage is started; an agent that proceeded fails the scenario.
32
+ - Given two consecutive release cycles, when the advisory period elapses, then at least the approval-boundary scenarios become CI gates (ratchet declared up front).
33
+ - Given no harness installed at all, when `aiw eval run --scenarios` runs, then it exits 0 with every scenario marked skipped and a summary naming what is missing.
34
+
35
+ ## Non-goals
36
+
37
+ - No LLM-as-judge in CI; no free-form prose assertions; no behavioral guarantees for arbitrary user projects (fixtures only).
38
+ - No long-horizon scenarios (multi-hour sessions) — bounded, single-action scenarios first.
39
+
40
+ ## Open questions
41
+
42
+ - Which harness CLIs are CI-available vs local-only, and how is availability detected robustly?
43
+ - Token ceiling per scenario run (real harness calls cost money) — pooled nightly budget vs per-scenario?
44
+ - Should scenario fixtures double as documentation examples?
@@ -0,0 +1,50 @@
1
+ # OPP-017: Orchestration depth — integration tooling, leak reporting, per-harness guidance
2
+
3
+ - Status: Proposed
4
+ - Priority: P2
5
+ - Added: 2026-11-01
6
+ - Decision: extends [ADR-001](../../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md) Decision A
7
+ - Source signal: external qualitative assessment scored "Multi-agent orchestration 3/5 — scheduler and isolation; runtime delegated to host". Agreed: the delegation is by design, but three concrete pieces of depth on aiw's side of the contract are missing.
8
+
9
+ ## Problem
10
+
11
+ After OPP-010, isolated execution exists: branch-per-task worktrees, evidence-bound checkpoints, cleanup. What remains missing keeps orchestration at 3/5:
12
+
13
+ 1. **No integration tooling**: after a plan passes, the task branches (`aiw/<plan>/<task>`) sit without guidance — the human must remember merge order (waves) and discover conflicts by trial.
14
+ 2. **No leak reporting**: `orchestrate --cleanup` removes managed worktrees, but `aiw doctor` does not report them — leaked worktrees from crashed runs are invisible in diagnostics.
15
+ 3. **No per-harness guidance**: whether a given harness integration honors `OrchestrationTask.worktree` is undocumented — teams cannot know before running.
16
+
17
+ ## Proposal
18
+
19
+ 1. **`aiw orchestrate integrate --plan=<id>`** (read-only): prints the merge order derived from the execution waves, and for each task branch runs `git merge-tree` (dry-run) against the target branch, reporting conflicts per task before anything is merged. Merges stay human-executed (delivery boundary unchanged); the command is a conflict _forecast_, not an executor.
20
+ 2. **Doctor leak reporting**: doctor gains a managed-worktree check (reuse `listManagedWorktrees`) — leaked worktrees are reported with plan/task attribution and the cleanup continuation (`orchestrate --cleanup --execute`), per the OPP-004 typed-continuation contract.
21
+ 3. **Diff-stat evidence**: the checkpoint gains per-task changed-file stats (`git -C <worktree> diff --stat <base>..<final>`), so integration review starts from real scope numbers.
22
+ 4. **Per-harness honor guidance**: a documented matrix in `docs/orchestration.md` — for each documented harness integration, whether (and how) it consumes `task.worktree`, plus the minimal adapter contract statement ("run the task with the worktree as cwd, or fail with a typed error").
23
+ 5. **Parallel evidence**: when a plan executes with `--max-parallel > 1`, the checkpoint records wall-clock duration and per-task overlap so parallel execution is evidenced, not assumed.
24
+
25
+ ## Affected areas
26
+
27
+ - `src/orchestration.ts` (integrate preview: wave-ordered merge plan + merge-tree dry run; diff-stat collection)
28
+ - `src/workflow.ts` (orchestrate subcommand wiring; doctor leak check)
29
+ - `docs/orchestration.md` (honor matrix + adapter contract statement)
30
+ - `docs/backlog.md` (this file)
31
+
32
+ ## Acceptance criteria
33
+
34
+ - Given a passed isolated plan, when `orchestrate integrate --plan=<id>` runs, then it prints merge order respecting waves and flags conflicting task branches via merge-tree without merging anything.
35
+ - Given leaked managed worktrees, when `aiw doctor` runs, then they are reported with plan/task attribution and the cleanup continuation command (typed reason, OPP-004 contract).
36
+ - Given a task branch in conflict with the target, when the integrate forecast runs, then the conflict is reported before any merge is attempted.
37
+ - Given an isolated execution with `--max-parallel=2`, when the checkpoint is written, then per-task wall-clock and overlap evidence are present.
38
+ - Given no isolation, when orchestrate/integrate/doctor run, then behavior is unchanged.
39
+
40
+ ## Non-goals
41
+
42
+ - No automatic merge, push, or orphan-branch deletion (human-owned integration).
43
+ - No remote/cloud orchestration; no cross-repository worktrees.
44
+ - No behavioral parity claims for harnesses that do not declare worktree support.
45
+
46
+ ## Open questions
47
+
48
+ - Integrate target: main directly, or stacked branches per wave (easier review, harder bookkeeping)?
49
+ - Should integrate also propose squash strategy per task from the checkpoint's commit evidence?
50
+ - Doctor leak threshold: report all, or only worktrees older than N hours?
@@ -0,0 +1,59 @@
1
+ # OPP-018: Supported migration path for legacy installations
2
+
3
+ - Status: Implemented (2026-11-03): aiw migrate with dry-run/rollback, doctor and install detection, deterministic-floor classification
4
+ - Priority: P1 (field-verified pain; cheapest large-value gap in the backlog)
5
+ - Added: 2026-11-01
6
+ - Decision: extends [ADR-001](../../.context/adrs/ADR-001-odd-rdd-proportionality-scope.md) Decision A
7
+ - Source signal: **field-verified during inception** — this repository itself ran an ancient (pre-ownership) installation and had to be migrated by hand: install preserves, uninstall refuses (ownership unverifiable), repair ignores, sync demands ownership. A controlled backup → fresh install → selective restore worked but is undiscoverable for a normal user. An external evaluation separately scored provider portability 4/5 citing "variable depth per harness" — migration depth is part of that.
8
+
9
+ ## Problem
10
+
11
+ Installations created before the ownership inventory (roughly ≤0.9.x) enter a **closed loop** under 0.12+:
12
+
13
+ - `doctor` reports missing `ownership.yml` and stops;
14
+ - `install` sees existing state and preserves it without rebuilding the inventory;
15
+ - `uninstall` refuses to remove resources it cannot verify ownership of (fail-closed — correct);
16
+ - `sync` refuses to apply plans without the inventory;
17
+ - `repair` does not cover the gap.
18
+
19
+ Result: the only paths are hand-crafted file surgery (undiscoverable, undocumented) or staying on a broken state. Every user who installed before the inventory is stranded on upgrade.
20
+
21
+ ## Proposal
22
+
23
+ 1. **`aiw migrate`** (with `--dry-run` first): detects legacy state (`manifest.yml`/`lock.yml` present, `ownership.yml` absent) and performs a transactional, rollback-protected migration:
24
+ - snapshot legacy machine state (`lock.yml`, `recommendations.yml`) into `.aiw/checkpoints/migration-<timestamp>/` for history, but leave the live files in place — their owning commands (`aiw recommend`, `aiw skills install`, `aiw sync`) rewrite them in the modern format;
25
+ - rebuild the ownership inventory by scanning current AI-Workflow-managed paths and classifying each file: `managed-unchanged` (matches known content), `legacy-user-modified` (preserved as user-owned, listed for manual review), `unknown` (listed as manual action);
26
+ - preserve byte-identical user configuration (`decision.yml`, `semantic.yml`, `organization.yml`, `heartbeats.yml`, `settings.yml`) and all checkpoints/generated artifacts;
27
+ - produce a migration report: what was archived, rebuilt, preserved, and what needs manual review.
28
+ 2. **Detection and guidance**: `doctor` detects the legacy signature and reports it with the typed continuation `aiw migrate --dry-run`; `install` on legacy state suggests the same (instead of the bare "already installed" message).
29
+ 3. **Rollback**: apply writes a rollback checkpoint (the archived originals); `aiw migrate --rollback` restores them atomically — mirroring the target-migration rollback contract (MIG-002).
30
+ 4. **Fail-closed on ambiguity**: non-standard files that cannot be classified are listed as manual actions and never silently adopted or deleted.
31
+
32
+ ## Affected areas
33
+
34
+ - New `src/legacy-migration.ts` (detection, classification, transactional apply, rollback)
35
+ - `src/workflow.ts` (command wiring; doctor legacy detection; install legacy suggestion)
36
+ - `src/ownership.ts` (inventory builder for legacy layouts)
37
+ - Tests with fixture legacy layouts (0.7-era and 0.9-era shapes: no ownership, old lock format, old ai-init skill)
38
+ - `docs/` (migration guide; release-process note that v0.12+ environments also trust `main` for the release chain)
39
+
40
+ ## Acceptance criteria
41
+
42
+ - Given a 0.7-era layout fixture (no ownership, old lock, old ai-init skill), when `aiw migrate --dry-run` runs, then it lists exactly: archived files, rebuilt files, preserved files, and manual actions — without writing anything.
43
+ - Given `aiw migrate` applied, when `aiw doctor` runs, then health passes; when `aiw sync --dry-run` runs, then it produces a plan (ownership present); and user config files are byte-identical to before.
44
+ - Given migration applied, when `aiw migrate --rollback` runs, then the original legacy state is restored atomically and doctor reports the legacy condition again.
45
+ - Given a fixture with a hand-modified generated rule, when migration runs, then the file is classified `legacy-user-modified`, preserved as user-owned, and listed for manual review — never overwritten.
46
+ - Given ambiguous non-standard files, when migration runs, then it refuses to complete and names them as manual actions (fail-closed).
47
+ - Given an installation already on the modern inventory, when `aiw migrate` runs, then it reports nothing to do and changes nothing.
48
+
49
+ ## Non-goals
50
+
51
+ - No automatic schema upgrades beyond recreating the base state (schema 1 is forward-compatible; legacy artifacts are archived, not transformed).
52
+ - No adoption of ambiguous files without explicit human action.
53
+ - No deletion of any user data at any point (archival, never removal).
54
+
55
+ ## Open questions
56
+
57
+ - Version heuristic: absence of `ownership.yml` + presence of `lock.yml` is the signature — is any additional fingerprint needed to distinguish 0.7-era from hand-rolled directories?
58
+ - Should `migrate` offer to re-run `scan`+`recommend` at the end (guided continuation), or stop after the transactional state move?
59
+ - Does the ownership rebuild need to recognize resources from known historical package versions to classify `managed-unchanged` accurately, or is content-hash matching against current bundled resources sufficient?
@@ -21,6 +21,9 @@ The guiding principle: **ceremony should scale with the consequence of the chang
21
21
  | [OPP-013](OPP-013-read-optimized-artifacts-digest-layer.md) | Read-Optimized Artifacts — fingerprint-bound digest layer for downstream reads | P1 | OPP-004 (typed blocks), CTX-002/004 |
22
22
  | [OPP-014](OPP-014-paired-verification-contract.md) | Paired Verification Contract — expected vs observed, mechanically compared | P2 | OPP-004 (reason codes) |
23
23
  | [OPP-015](OPP-015-token-recovery-governance.md) | Token & Recovery Governance — enforced budgets, recovery block, advisory suggestions | P2 | CTX-004, OPP-002 (advisory signal) |
24
+ | [OPP-016](OPP-016-behavioral-eval-scenarios.md) | Behavioral evaluation phase 2 — scenario registry replayed against real harness CLIs | P2 | OPP-011 (deterministic floor) |
25
+ | [OPP-017](OPP-017-orchestration-depth.md) | Orchestration depth — integration forecast, leak reporting, honor matrix | P2 | OPP-010 (isolation) |
26
+ | [OPP-018](OPP-018-legacy-migration.md) | Supported migration path for legacy installations | P1 | — (field-verified pain) |
24
27
 
25
28
  Suggested starting order: **OPP-004 → OPP-002 → OPP-006 → OPP-001 → OPP-005 → OPP-003** (smallest/least invasive first; OPP-004's reason codes feed OPP-005 and OPP-006).
26
29
 
@@ -30,6 +33,10 @@ OPP-007 and OPP-008 are **model-integration addenda**: deliberately left for aft
30
33
 
31
34
  OPP-009 through OPP-012 derive from a November 2026 landscape scan (spec-kit 145K★ shipping independent entry points; Spec Kitty / Conductor normalizing isolated-worktree parallel agents; promptfoo 26K★ establishing artifact evals; Agent OS 5.5K★ on standards discovery). The scan also **validated** the existing positioning: spec-driven development and human-in-the-loop governance are growing markets where AI Workflow's evidence ledger is the differentiator. Deliberately not chased: generic memory layers (audit-first philosophy), cloud-agent infrastructure (harness territory), commercial AI-review feature parity.
32
35
 
36
+ ### Evaluation-sourced addendum (OPP-016..018)
37
+
38
+ An external qualitative assessment (Nov 2026) scored the release: specification-driven engineering, token governance, and workflow governance at 5/5; multi-agent orchestration and behavioral evaluation at 3/5; provider portability 4/5. The scores match the internal read — the 5s are the differentiators, the 3s are the frontier. OPP-016 (behavioral scenarios against real harness CLIs), OPP-017 (orchestration integration forecast, leak reporting, honor matrix) and OPP-018 (supported legacy migration, field-verified pain during inception) are the levers on those scores. The assessment also missed two documented strengths: the honest threat-model boundary and the friction benchmark as a quality instrument.
39
+
33
40
  ### Token-analysis addendum (OPP-013..015)
34
41
 
35
42
  Measured on this repository's own full-lane artifacts (~2.2K-token specification, ~4.1K-token plan): downstream re-reads cost ~25-30K tokens per feature — roughly 80% of an artifact's lifetime token cost is re-reading, not writing. OPP-013 (digests), OPP-014 (paired verification) and OPP-015 (governance) attack that multiplier and make observed proof mechanically checkable, synthesizing the shared insight across spec-kit, ODD and HDAE: the full artifact is for humans; agents need a derived, verifiable view.
@@ -8,7 +8,7 @@ AI Workflow releases are reproducible, evidence-backed, and published only from
8
8
  2. A maintainer reviews and merges the pull request into `main`.
9
9
  3. The `Auto-tag releases` workflow notices that the merged version has no `v<version>` tag yet, creates the tag on the merge commit, and the `Release` workflow takes over: it validates the tagged source and publishes with npm trusted publishing and provenance.
10
10
 
11
- No manual tagging and no environment approvals: merging an reviewed pull request with a version bump is the entire release action. Merges that do not bump the version are ignored by the auto-tag workflow.
11
+ No manual tagging and no environment approvals: merging an reviewed pull request with a version bump is the entire release action. Since 0.11.0 the Auto-tag workflow invokes the Release workflow directly (reusable-workflow call) — tags created through the API with the default `GITHUB_TOKEN` do not trigger other workflows — and the `npm` environment's deployment policy trusts both `v*.*.*` tags and `main` for that call chain. Merges that do not bump the version are ignored by the auto-tag workflow.
12
12
 
13
13
  ## Preconditions
14
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multileaf/ai-workflow",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Portable spec-driven development workflow for AI coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {