@multileaf/ai-workflow 0.12.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 +18 -0
- package/dist/blocking-reasons.d.ts +1 -1
- package/dist/blocking-reasons.js +18 -0
- package/dist/legacy-migration.d.ts +45 -0
- package/dist/legacy-migration.js +175 -0
- package/dist/orchestration.d.ts +21 -1
- package/dist/orchestration.js +118 -5
- package/dist/workflow.js +120 -4
- package/docs/backlog.md +3 -0
- package/docs/continuation-inventory.md +3 -0
- package/docs/opportunities/OPP-016-behavioral-eval-scenarios.md +44 -0
- package/docs/opportunities/OPP-017-orchestration-depth.md +50 -0
- package/docs/opportunities/OPP-018-legacy-migration.md +59 -0
- package/docs/opportunities/README.md +7 -0
- package/docs/orchestration.md +16 -0
- package/docs/release-process.md +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ 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
|
+
|
|
18
|
+
## [0.13.0] - 2026-11-03
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `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.
|
|
23
|
+
- `docs/release-process.md` now documents the working release chain (Auto-tag → reusable-workflow Release call; npm environment trusting `v*.*.*` tags and `main`).
|
|
24
|
+
|
|
7
25
|
## [0.12.0] - 2026-11-01
|
|
8
26
|
|
|
9
27
|
### 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", "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
|
@@ -26,6 +26,9 @@ export const BLOCKING_REASON_CODES = [
|
|
|
26
26
|
"state_mismatch",
|
|
27
27
|
"context_budget_exceeded",
|
|
28
28
|
"stage_not_in_process",
|
|
29
|
+
"migration_required",
|
|
30
|
+
"worktrees_leaked",
|
|
31
|
+
"orchestration_not_executed",
|
|
29
32
|
];
|
|
30
33
|
function command(commandText) {
|
|
31
34
|
return { kind: "command", command: commandText };
|
|
@@ -154,6 +157,21 @@ export const BLOCKING_REASON_INVENTORY = {
|
|
|
154
157
|
description: "This stage does not exist in the active workflow's process; use the process's own stages.",
|
|
155
158
|
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
159
|
},
|
|
160
|
+
migration_required: {
|
|
161
|
+
code: "migration_required",
|
|
162
|
+
description: "Legacy pre-ownership state detected; review the migration plan before continuing.",
|
|
163
|
+
continuation: () => command("aiw migrate --dry-run"),
|
|
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
|
+
},
|
|
157
175
|
};
|
|
158
176
|
export function getBlockingReasonDefinition(code) {
|
|
159
177
|
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/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
|
@@ -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() {
|
|
@@ -88,7 +89,7 @@ import { parsePrivateRegistries, parseTeamPreset, serializePrivateRegistries, se
|
|
|
88
89
|
import { enforcePolicyGate, gitPolicyTracking } from "./policy-gate.js";
|
|
89
90
|
import { DEFAULT_TELEMETRY_CONFIG, loadTelemetryConfig, offerTelemetryConsent, parseTelemetryConfig, purgeTelemetryBuffer, readTelemetryBuffer, serializeTelemetryMapping, TELEMETRY_BUFFER_CAP, TELEMETRY_BUFFER_FILE, } from "./telemetry.js";
|
|
90
91
|
import { createPluginScaffold, validatePluginDirectory, writePluginScaffold, } from "./plugin-sdk.js";
|
|
91
|
-
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";
|
|
92
93
|
function laneAdvisory(root, fs, lightChosen) {
|
|
93
94
|
const paths = collectWorkingTreeChanges(root);
|
|
94
95
|
if (!paths || paths.length === 0)
|
|
@@ -559,6 +560,37 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
559
560
|
error: "Usage: aiw orchestrate --plan=path [--execute] [--isolate] [--cleanup] [--max-parallel=number]",
|
|
560
561
|
};
|
|
561
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
|
+
}
|
|
562
594
|
const isolate = args.includes("--isolate");
|
|
563
595
|
const cleanup = args.includes("--cleanup");
|
|
564
596
|
if (cleanup) {
|
|
@@ -598,6 +630,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
598
630
|
exitCode: 1,
|
|
599
631
|
error: `Orchestration checkpoint already exists; use a new plan id: ${checkpoint}`,
|
|
600
632
|
};
|
|
633
|
+
const wallClockStart = Date.now();
|
|
601
634
|
try {
|
|
602
635
|
const isolation = isolate
|
|
603
636
|
? {
|
|
@@ -612,7 +645,7 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
612
645
|
.map(({ worktree }) => worktree);
|
|
613
646
|
removeWorktrees(root, passedPaths);
|
|
614
647
|
}
|
|
615
|
-
fs.write(checkpoint, serializeOrchestrationResults(plan, results));
|
|
648
|
+
fs.write(checkpoint, serializeOrchestrationResults(plan, results, Date.now() - wallClockStart));
|
|
616
649
|
const failed = results.filter(({ status }) => status !== "passed");
|
|
617
650
|
return failed.length
|
|
618
651
|
? {
|
|
@@ -636,9 +669,12 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
636
669
|
if (fs.pathType?.(manifestPath) === "file") {
|
|
637
670
|
const manifest = fs.read(manifestPath);
|
|
638
671
|
const active = manifest.match(/^\s*active:\s*(.+)$/m)?.[1]?.trim() ?? "unknown";
|
|
672
|
+
const legacyHint = detectLegacyState(aiw, fs)
|
|
673
|
+
? "\nLegacy pre-ownership state detected. Run `aiw migrate --dry-run` to review, then `aiw migrate` to upgrade to the current state layout."
|
|
674
|
+
: "";
|
|
639
675
|
return {
|
|
640
676
|
exitCode: 0,
|
|
641
|
-
output: `AI Workflow is already installed for target: ${active}. Existing state preserved
|
|
677
|
+
output: `AI Workflow is already installed for target: ${active}. Existing state preserved.${legacyHint}`,
|
|
642
678
|
};
|
|
643
679
|
}
|
|
644
680
|
const aiInitPath = join(root, renderAiInitPath(target));
|
|
@@ -1818,8 +1854,24 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
1818
1854
|
if (command === "doctor") {
|
|
1819
1855
|
const required = ["manifest.yml", "profile.yml", "lock.yml", "ownership.yml"];
|
|
1820
1856
|
const missing = required.filter((file) => !fs.exists(join(aiw, file)));
|
|
1821
|
-
if (missing.length)
|
|
1857
|
+
if (missing.length) {
|
|
1858
|
+
if (detectLegacyState(aiw, fs))
|
|
1859
|
+
return {
|
|
1860
|
+
exitCode: 1,
|
|
1861
|
+
error: formatBlockingOutput(describeBlockingReason("migration_required", {
|
|
1862
|
+
detail: "Legacy pre-ownership state detected; review the migration plan first.",
|
|
1863
|
+
})) + "\nNext: run `aiw migrate --dry-run`, then `aiw migrate`.",
|
|
1864
|
+
};
|
|
1822
1865
|
return { exitCode: 1, error: `Missing AI Workflow files: ${missing.join(", ")}` };
|
|
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
|
+
};
|
|
1823
1875
|
try {
|
|
1824
1876
|
parseManifest(fs.read(join(aiw, "manifest.yml")));
|
|
1825
1877
|
parseOwnership(fs.read(join(aiw, "ownership.yml")));
|
|
@@ -2141,6 +2193,70 @@ export async function runCommand(args, root, fs, services = {}) {
|
|
|
2141
2193
|
error: "Usage: aiw standards <extract|list|confirm|reject> [topic] — standards are inferred until a human confirms them.",
|
|
2142
2194
|
};
|
|
2143
2195
|
}
|
|
2196
|
+
if (command === "migrate") {
|
|
2197
|
+
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2198
|
+
return installationRequired();
|
|
2199
|
+
const dry = args.includes("--dry-run");
|
|
2200
|
+
const doRollback = args.includes("--rollback");
|
|
2201
|
+
if (doRollback) {
|
|
2202
|
+
const restored = rollbackMigration(root, aiw, fs);
|
|
2203
|
+
return { exitCode: 0, output: `Legacy state restored from snapshot: ${restored}` };
|
|
2204
|
+
}
|
|
2205
|
+
if (!detectLegacyState(aiw, fs))
|
|
2206
|
+
return {
|
|
2207
|
+
exitCode: 0,
|
|
2208
|
+
output: "Nothing to migrate: state already includes the ownership inventory.",
|
|
2209
|
+
};
|
|
2210
|
+
const manifestPath = join(aiw, "manifest.yml");
|
|
2211
|
+
const manifestTarget = /^\s*active:\s*(.+)$/m.exec(fs.read(manifestPath))?.[1]?.trim() ||
|
|
2212
|
+
detectTarget(root, fs) ||
|
|
2213
|
+
"codex";
|
|
2214
|
+
if (!isTarget(manifestTarget))
|
|
2215
|
+
throw new Error(`Unsupported target: ${manifestTarget}`);
|
|
2216
|
+
const managedCandidates = [];
|
|
2217
|
+
for (const resource of loadBundledResources(manifestTarget)) {
|
|
2218
|
+
managedCandidates.push({
|
|
2219
|
+
path: join(root, renderResourcePath(manifestTarget, resource.type, resource.id)),
|
|
2220
|
+
bundled: resource.content,
|
|
2221
|
+
metadata: {
|
|
2222
|
+
target: manifestTarget,
|
|
2223
|
+
resourceType: resource.type,
|
|
2224
|
+
resourceId: resource.id,
|
|
2225
|
+
kind: "target-resource",
|
|
2226
|
+
},
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
managedCandidates.push({
|
|
2230
|
+
path: join(root, describeResource(manifestTarget, "context", "project").path),
|
|
2231
|
+
metadata: {
|
|
2232
|
+
target: manifestTarget,
|
|
2233
|
+
resourceType: "context",
|
|
2234
|
+
resourceId: "project",
|
|
2235
|
+
kind: "target-resource",
|
|
2236
|
+
},
|
|
2237
|
+
});
|
|
2238
|
+
const plan = planLegacyMigration(root, aiw, fs, managedCandidates);
|
|
2239
|
+
const report = [
|
|
2240
|
+
`Target: ${manifestTarget}`,
|
|
2241
|
+
plan.archived.length
|
|
2242
|
+
? `Archived (superseded machine state): ${plan.archived.join(", ")}`
|
|
2243
|
+
: "",
|
|
2244
|
+
plan.preserved.length ? `Preserved byte-identical: ${plan.preserved.join(", ")}` : "",
|
|
2245
|
+
`Rebuilt: ownership inventory (${plan.rebuilt.length} managed paths)`,
|
|
2246
|
+
...plan.userModified.map((entry) => `Legacy-user-modified (sync preserves for review): ${entry}`),
|
|
2247
|
+
...plan.manualActions.map((entry) => `MANUAL ACTION REQUIRED: ${entry}`),
|
|
2248
|
+
dry ? "Mode: dry-run — nothing written." : `Rollback snapshot: ${plan.rollbackSnapshot}`,
|
|
2249
|
+
].filter((line) => line.length > 0);
|
|
2250
|
+
if (dry)
|
|
2251
|
+
return { exitCode: 0, output: ["Migration plan (dry-run):", ...report].join("\n") };
|
|
2252
|
+
try {
|
|
2253
|
+
applyMigration(root, aiw, fs, managedCandidates, plan);
|
|
2254
|
+
}
|
|
2255
|
+
catch (error) {
|
|
2256
|
+
return { exitCode: 1, error: error instanceof Error ? error.message : String(error) };
|
|
2257
|
+
}
|
|
2258
|
+
return { exitCode: 0, output: ["Migration complete.", ...report].join("\n") };
|
|
2259
|
+
}
|
|
2144
2260
|
if (command === "digest") {
|
|
2145
2261
|
if (!fs.exists(join(aiw, "manifest.yml")))
|
|
2146
2262
|
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
|
+
- [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
|
+
- [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,6 @@ 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 |
|
|
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 |
|
|
@@ -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: 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
|
+
- 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.
|
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
|
package/docs/release-process.md
CHANGED
|
@@ -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
|
|