ai-delivery-workflow 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/bin/ai-delivery.mjs +2 -24
- package/docs/CLI-PARAMETER-REFERENCE.zh-CN.md +6 -67
- package/docs/DUAL-REPOSITORY-WORKSPACE.zh-CN.md +1 -1
- package/docs/FILE-REFERENCE.zh-CN.md +6 -28
- package/docs/MAINTENANCE-VERIFICATION.zh-CN.md +2 -2
- package/docs/PROJECT-MANUAL.zh-CN.md +23 -38
- package/docs/STATE-CLI-MAINTENANCE.zh-CN.md +8 -10
- package/docs/STATE-CLI-USER-GUIDE.zh-CN.md +6 -16
- package/docs/agents/workflow-manager-acceptance-matrix.md +9 -15
- package/docs/agents/workflow-manager-current-state.md +12 -15
- package/lib/delivery-state.mjs +59 -910
- package/lib/fs-utils.mjs +0 -16
- package/lib/project-bootstrap.mjs +21 -58
- package/lib/project-installer.mjs +24 -180
- package/lib/project-repair.mjs +217 -0
- package/lib/toml-hooks.mjs +2 -4
- package/lib/workflow-manager-governance.mjs +3 -47
- package/lib/workspace.mjs +2 -3
- package/package.json +2 -2
- package/skills/ai-delivery-bootstrap/SKILL.md +1 -1
- package/skills/ai-delivery-bootstrap/references/bootstrap-contract.md +1 -1
- package/skills/ai-delivery-checkpoint-task/SKILL.md +0 -4
- package/skills/ai-delivery-checkpoint-task/references/checkpoint-contract.md +2 -2
- package/skills/ai-delivery-checkpoint-task/scripts/hook-event.mjs +2 -8
- package/skills/ai-delivery-checkpoint-task/scripts/task-state.mjs +26 -46
- package/skills/ai-delivery-design-experience/SKILL.md +3 -3
- package/skills/ai-delivery-design-experience/references/experience-contract.md +1 -11
- package/skills/ai-delivery-design-experience/references/prototype-management-contract.md +1 -1
- package/skills/ai-delivery-orchestrate/SKILL.md +5 -7
- package/skills/ai-delivery-orchestrate/assets/project-template/.workflow/tools/hooks/hook-event.mjs +2 -8
- package/skills/ai-delivery-orchestrate/assets/project-template/AGENTS.md +4 -5
- package/skills/ai-delivery-orchestrate/references/formal-state-contract.md +11 -20
- package/skills/ai-delivery-orchestrate/references/workflow-model.md +1 -3
- package/skills/ai-delivery-prepare-release/assets/release-template/release-manifest.yaml +0 -2
- package/docs/PROTOTYPE-MIGRATION.zh-CN.md +0 -164
- package/docs/VIEWER-MAINTENANCE.zh-CN.md +0 -150
- package/docs/VIEWER-USER-GUIDE.zh-CN.md +0 -113
- package/lib/project-migration.mjs +0 -1193
- package/lib/prototype-migration.mjs +0 -713
- package/skills/ai-delivery-design-experience/assets/experience-template/LEGACY-COMPATIBILITY.md +0 -16
- package/skills/ai-delivery-design-experience/assets/experience-template/canvas-catalog.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/canvas-pages.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/page-catalog.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-file-terminals.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-files.csv +0 -1
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-manifest.yaml +0 -76
- package/skills/ai-delivery-design-experience/assets/experience-template/prototype-set.yaml +0 -24
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { atomicWrite, filesUnder } from "./fs-utils.mjs";
|
|
6
|
+
|
|
7
|
+
function toPosix(value) {
|
|
8
|
+
return value.replaceAll("\\", "/");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sha256(value) {
|
|
12
|
+
return crypto.createHash("sha256").update(value).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function treeDigest(root) {
|
|
16
|
+
const hash = crypto.createHash("sha256");
|
|
17
|
+
for (const file of filesUnder(root)) {
|
|
18
|
+
hash.update(toPosix(path.relative(root, file)));
|
|
19
|
+
hash.update("\0");
|
|
20
|
+
hash.update(fs.readFileSync(file));
|
|
21
|
+
hash.update("\0");
|
|
22
|
+
}
|
|
23
|
+
return hash.digest("hex");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function readStructured(file, label) {
|
|
27
|
+
try {
|
|
28
|
+
const text = fs.readFileSync(file, "utf8");
|
|
29
|
+
const value = path.extname(file).toLowerCase() === ".json" ? JSON.parse(text) : parseYaml(text);
|
|
30
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("expected a mapping");
|
|
31
|
+
return value;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw new Error(`Cannot read ${label} (${toPosix(file)}): ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function stableValue(value) {
|
|
38
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
39
|
+
if (!value || typeof value !== "object") return value;
|
|
40
|
+
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function stableJson(value) {
|
|
44
|
+
return JSON.stringify(stableValue(value));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function uniqueSorted(values) {
|
|
48
|
+
return [...new Set((Array.isArray(values) ? values : []).map(String))].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function unsignedRepairRecord(record) {
|
|
52
|
+
const unsigned = { ...record };
|
|
53
|
+
delete unsigned.record_checksum;
|
|
54
|
+
return unsigned;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function readRepairTransactions(project) {
|
|
58
|
+
const directory = path.join(project, ".workflow", "control", "repairs", "transactions");
|
|
59
|
+
const records = [];
|
|
60
|
+
let previousChecksum = null;
|
|
61
|
+
for (const file of filesUnder(directory).filter((item) => item.endsWith(".json"))) {
|
|
62
|
+
const record = readStructured(file, "repair transaction");
|
|
63
|
+
if (record.schema_version !== 1
|
|
64
|
+
|| !Number.isInteger(record.sequence)
|
|
65
|
+
|| record.sequence !== records.length + 1
|
|
66
|
+
|| record.previous_checksum !== previousChecksum
|
|
67
|
+
|| record.record_checksum !== sha256(stableJson(unsignedRepairRecord(record)))) {
|
|
68
|
+
throw new Error(`Repair transaction chain is invalid: ${toPosix(path.relative(project, file))}.`);
|
|
69
|
+
}
|
|
70
|
+
records.push(record);
|
|
71
|
+
previousChecksum = record.record_checksum;
|
|
72
|
+
}
|
|
73
|
+
return records;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function archiveIterationId(value) {
|
|
77
|
+
const iterationId = String(value || "");
|
|
78
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(iterationId)) {
|
|
79
|
+
throw new Error(`Invalid archive iteration ID: ${iterationId || "missing"}.`);
|
|
80
|
+
}
|
|
81
|
+
return iterationId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function verifiedArchiveInvalidations(project, records) {
|
|
85
|
+
for (const record of records.filter((item) => item.transaction_type === "archive-invalidation")) {
|
|
86
|
+
const iterationId = archiveIterationId(record.target?.iteration_id);
|
|
87
|
+
const expectedArchivePath = `.workflow/control/archives/versions/${iterationId}`;
|
|
88
|
+
const expectedManifestPath = `${expectedArchivePath}/version-manifest.json`;
|
|
89
|
+
if (record.target.archive_path !== expectedArchivePath
|
|
90
|
+
|| record.target.manifest_path !== expectedManifestPath
|
|
91
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.archive_sha256 || "")
|
|
92
|
+
|| !/^[a-f0-9]{64}$/.test(record.target.manifest_sha256 || "")) {
|
|
93
|
+
throw new Error(`Archive invalidation target is invalid: ${record.transaction_id}.`);
|
|
94
|
+
}
|
|
95
|
+
const archiveDirectory = path.join(project, ...expectedArchivePath.split("/"));
|
|
96
|
+
const manifestFile = path.join(project, ...expectedManifestPath.split("/"));
|
|
97
|
+
if (!fs.existsSync(manifestFile)
|
|
98
|
+
|| treeDigest(archiveDirectory) !== record.target.archive_sha256
|
|
99
|
+
|| sha256(fs.readFileSync(manifestFile)) !== record.target.manifest_sha256) {
|
|
100
|
+
throw new Error(`Invalidated archive ${iterationId} bytes changed after ${record.transaction_id}.`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function repairTransactionPath(sequence, transactionId) {
|
|
106
|
+
return `.workflow/control/repairs/transactions/${String(sequence).padStart(6, "0")}-${transactionId}.json`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function repairProject(project, options = {}) {
|
|
110
|
+
const root = path.resolve(project);
|
|
111
|
+
if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
|
|
112
|
+
throw new Error(`Project directory does not exist: ${root}`);
|
|
113
|
+
}
|
|
114
|
+
const iterationId = archiveIterationId(options.invalidateArchive);
|
|
115
|
+
const reason = String(options.reason || "").trim();
|
|
116
|
+
const actor = String(options.actor || "").trim();
|
|
117
|
+
const evidence = uniqueSorted(options.evidence);
|
|
118
|
+
if (!reason) throw new Error("Archive invalidation requires --reason.");
|
|
119
|
+
if (!actor.startsWith("human:")) {
|
|
120
|
+
throw new Error("Archive invalidation requires an explicit human actor through --actor human:<identity>.");
|
|
121
|
+
}
|
|
122
|
+
if (evidence.length < 2) {
|
|
123
|
+
throw new Error("Archive invalidation requires at least two --evidence values for RCA and recovery evidence.");
|
|
124
|
+
}
|
|
125
|
+
const archiveDirectory = path.join(root, ".workflow", "control", "archives", "versions", iterationId);
|
|
126
|
+
const manifestFile = path.join(archiveDirectory, "version-manifest.json");
|
|
127
|
+
if (!fs.existsSync(manifestFile)) throw new Error(`Version archive not found: ${iterationId}.`);
|
|
128
|
+
const manifest = readStructured(manifestFile, "version archive manifest");
|
|
129
|
+
if (manifest.iteration_id !== iterationId) {
|
|
130
|
+
throw new Error(`Version archive identity mismatch: expected ${iterationId}, found ${manifest.iteration_id || "missing"}.`);
|
|
131
|
+
}
|
|
132
|
+
const target = {
|
|
133
|
+
iteration_id: iterationId,
|
|
134
|
+
archive_path: toPosix(path.relative(root, archiveDirectory)),
|
|
135
|
+
manifest_path: toPosix(path.relative(root, manifestFile)),
|
|
136
|
+
manifest_sha256: sha256(fs.readFileSync(manifestFile)),
|
|
137
|
+
archive_sha256: treeDigest(archiveDirectory),
|
|
138
|
+
original_bytes_preserved: true,
|
|
139
|
+
};
|
|
140
|
+
const identity = { transaction_type: "archive-invalidation", target, correction: { actor, reason, evidence } };
|
|
141
|
+
const idempotencyKey = `archive-invalidation:${iterationId}:${sha256(stableJson(identity))}`;
|
|
142
|
+
const existingTransactions = readRepairTransactions(root);
|
|
143
|
+
verifiedArchiveInvalidations(root, existingTransactions);
|
|
144
|
+
const existing = existingTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
145
|
+
const sequence = existing?.sequence || existingTransactions.length + 1;
|
|
146
|
+
const transactionId = existing?.transaction_id || `REPAIR-${sha256(idempotencyKey).slice(0, 16).toUpperCase()}`;
|
|
147
|
+
const transactionPath = repairTransactionPath(sequence, transactionId);
|
|
148
|
+
const report = {
|
|
149
|
+
command: "repair",
|
|
150
|
+
action: "archive-invalidation",
|
|
151
|
+
dry_run: Boolean(options.dryRun),
|
|
152
|
+
applied: false,
|
|
153
|
+
forward_only: true,
|
|
154
|
+
original_bytes_preserved: true,
|
|
155
|
+
target,
|
|
156
|
+
actor,
|
|
157
|
+
reason,
|
|
158
|
+
evidence,
|
|
159
|
+
transaction: {
|
|
160
|
+
transaction_id: transactionId,
|
|
161
|
+
path: transactionPath,
|
|
162
|
+
previous_checksum: existing?.previous_checksum || existingTransactions.at(-1)?.record_checksum || null,
|
|
163
|
+
},
|
|
164
|
+
eligibility: existing
|
|
165
|
+
? { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId }
|
|
166
|
+
: { iteration_id: iterationId, current: "eligible", source_transaction_id: null },
|
|
167
|
+
};
|
|
168
|
+
if (report.dry_run || existing) return report;
|
|
169
|
+
|
|
170
|
+
const repairRoot = path.join(root, ".workflow", "control", "repairs");
|
|
171
|
+
const lockFile = path.join(repairRoot, ".repair.lock");
|
|
172
|
+
fs.mkdirSync(repairRoot, { recursive: true });
|
|
173
|
+
let lock;
|
|
174
|
+
try {
|
|
175
|
+
lock = fs.openSync(lockFile, "wx");
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw new Error(`Repair transaction is locked by another writer: ${error.message}`);
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const currentTransactions = readRepairTransactions(root);
|
|
181
|
+
const duplicate = currentTransactions.find((record) => record.idempotency_key === idempotencyKey);
|
|
182
|
+
if (duplicate) {
|
|
183
|
+
report.transaction.transaction_id = duplicate.transaction_id;
|
|
184
|
+
report.transaction.path = repairTransactionPath(duplicate.sequence, duplicate.transaction_id);
|
|
185
|
+
report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: duplicate.transaction_id };
|
|
186
|
+
return report;
|
|
187
|
+
}
|
|
188
|
+
if (currentTransactions.length !== existingTransactions.length
|
|
189
|
+
|| (currentTransactions.at(-1)?.record_checksum || null) !== (existingTransactions.at(-1)?.record_checksum || null)) {
|
|
190
|
+
throw new Error("Repair transaction revision changed during apply; rerun the command.");
|
|
191
|
+
}
|
|
192
|
+
if (treeDigest(archiveDirectory) !== target.archive_sha256
|
|
193
|
+
|| sha256(fs.readFileSync(manifestFile)) !== target.manifest_sha256) {
|
|
194
|
+
throw new Error(`Version archive changed during repair apply: ${iterationId}.`);
|
|
195
|
+
}
|
|
196
|
+
const transaction = {
|
|
197
|
+
schema_version: 1,
|
|
198
|
+
sequence,
|
|
199
|
+
transaction_id: transactionId,
|
|
200
|
+
transaction_type: "archive-invalidation",
|
|
201
|
+
idempotency_key: idempotencyKey,
|
|
202
|
+
occurred_at: new Date().toISOString(),
|
|
203
|
+
target,
|
|
204
|
+
correction: { actor, reason, evidence },
|
|
205
|
+
previous_checksum: existingTransactions.at(-1)?.record_checksum || null,
|
|
206
|
+
};
|
|
207
|
+
transaction.record_checksum = sha256(stableJson(transaction));
|
|
208
|
+
atomicWrite(path.join(root, ...transactionPath.split("/")), `${JSON.stringify(transaction, null, 2)}\n`);
|
|
209
|
+
report.applied = true;
|
|
210
|
+
report.transaction.previous_checksum = transaction.previous_checksum;
|
|
211
|
+
report.eligibility = { iteration_id: iterationId, current: "invalidated", source_transaction_id: transactionId };
|
|
212
|
+
return report;
|
|
213
|
+
} finally {
|
|
214
|
+
if (lock !== undefined) fs.closeSync(lock);
|
|
215
|
+
fs.rmSync(lockFile, { force: true });
|
|
216
|
+
}
|
|
217
|
+
}
|
package/lib/toml-hooks.mjs
CHANGED
|
@@ -14,7 +14,6 @@ export const HOOK_EVENTS = [
|
|
|
14
14
|
];
|
|
15
15
|
|
|
16
16
|
const HOOK_COMMAND = "node .workflow/tools/hooks/hook-event.mjs";
|
|
17
|
-
const LEGACY_HOOK_COMMAND = "node .ai-delivery/hooks/hook-event.mjs";
|
|
18
17
|
|
|
19
18
|
function hookBlock(event, newline) {
|
|
20
19
|
return [
|
|
@@ -102,9 +101,8 @@ export function mergeHookConfig(existing, { force = false } = {}) {
|
|
|
102
101
|
const newline = existing.includes("\r\n") ? "\r\n" : "\n";
|
|
103
102
|
let text = existing.trimEnd();
|
|
104
103
|
const actions = [];
|
|
105
|
-
if (text.includes(
|
|
106
|
-
|
|
107
|
-
actions.push("migrate AI delivery Hook command into .workflow/tools");
|
|
104
|
+
if (text.includes("node .ai-delivery/hooks/hook-event.mjs")) {
|
|
105
|
+
throw new Error("Unsupported legacy Hook command; remove the .ai-delivery handler before installing the current Hook.");
|
|
108
106
|
}
|
|
109
107
|
if (force) {
|
|
110
108
|
const refreshed = removeDeliveryHandlerBlocks(text);
|
|
@@ -24,7 +24,6 @@ export const WORKFLOW_MANAGER_GOVERNANCE_SCHEMA_VERSION = "workflow-manager.gove
|
|
|
24
24
|
const RELATIONS_FILE = ".workflow/control/relations.yaml";
|
|
25
25
|
const SETTINGS_FILE = ".workflow/config/manager-settings.json";
|
|
26
26
|
const RECEIPTS_ROOT = ".workflow/control/receipts";
|
|
27
|
-
const EVENTS_FILE = ".workflow/control/events/workflow-manager-events.jsonl";
|
|
28
27
|
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,191}$/;
|
|
29
28
|
const SAFE_ENTITY_TYPE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
30
29
|
const RELATION_TYPES = new Set([
|
|
@@ -59,18 +58,6 @@ function contained(root, relative) {
|
|
|
59
58
|
return absolute;
|
|
60
59
|
}
|
|
61
60
|
|
|
62
|
-
function atomicWrite(file, content) {
|
|
63
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
64
|
-
const temporary = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
65
|
-
fs.writeFileSync(temporary, content, { encoding: "utf8", flag: "wx" });
|
|
66
|
-
try {
|
|
67
|
-
fs.renameSync(temporary, file);
|
|
68
|
-
} catch (error) {
|
|
69
|
-
fs.rmSync(temporary, { force: true });
|
|
70
|
-
throw error;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
61
|
function requireHuman(actor) {
|
|
75
62
|
if (!/^human:[A-Za-z0-9._@/-]+$/.test(String(actor || ""))) {
|
|
76
63
|
throw new Error("Governed confirmation requires actor human:<identity>");
|
|
@@ -92,16 +79,13 @@ function receiptFiles(root) {
|
|
|
92
79
|
|
|
93
80
|
function replay(root, action, input, subject = null) {
|
|
94
81
|
if (!input.idempotency_key) return null;
|
|
95
|
-
const
|
|
96
|
-
const governedRequestChecksum = workflowManagerRequestChecksum(action, input, subject);
|
|
82
|
+
const expectedRequestChecksum = workflowManagerRequestChecksum(action, input, subject);
|
|
97
83
|
for (const file of receiptFiles(root)) {
|
|
98
84
|
try {
|
|
99
85
|
const receipt = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
100
86
|
if (receipt.action !== action || receipt.idempotency_key !== input.idempotency_key) continue;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
&& (receipt.subject ?? null) === subject;
|
|
104
|
-
if (!legacy && !governed) {
|
|
87
|
+
if (receipt.request_checksum !== expectedRequestChecksum
|
|
88
|
+
|| (receipt.subject ?? null) !== subject) {
|
|
105
89
|
throw new Error("Idempotency key was already used for a different request");
|
|
106
90
|
}
|
|
107
91
|
return receipt;
|
|
@@ -112,34 +96,6 @@ function replay(root, action, input, subject = null) {
|
|
|
112
96
|
return null;
|
|
113
97
|
}
|
|
114
98
|
|
|
115
|
-
function record(root, action, input, detail = {}) {
|
|
116
|
-
const timestamp = new Date().toISOString();
|
|
117
|
-
const receipt = {
|
|
118
|
-
schema_version: 1,
|
|
119
|
-
receipt_id: `WMG-${crypto.randomUUID()}`,
|
|
120
|
-
action,
|
|
121
|
-
actor: input.actor,
|
|
122
|
-
idempotency_key: input.idempotency_key || null,
|
|
123
|
-
request_checksum: checksum({ action, input }),
|
|
124
|
-
applied_at: timestamp,
|
|
125
|
-
...detail,
|
|
126
|
-
};
|
|
127
|
-
receipt.receipt_checksum = checksum(receipt);
|
|
128
|
-
atomicWrite(contained(root, `${RECEIPTS_ROOT}/${receipt.receipt_id}.json`), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
129
|
-
const event = {
|
|
130
|
-
schema_version: 1,
|
|
131
|
-
event_id: `WME-${crypto.randomUUID()}`,
|
|
132
|
-
type: `workflow-manager.${action}`,
|
|
133
|
-
occurred_at: timestamp,
|
|
134
|
-
receipt_id: receipt.receipt_id,
|
|
135
|
-
subject: detail.subject || null,
|
|
136
|
-
};
|
|
137
|
-
const eventFile = contained(root, EVENTS_FILE);
|
|
138
|
-
fs.mkdirSync(path.dirname(eventFile), { recursive: true });
|
|
139
|
-
fs.appendFileSync(eventFile, `${JSON.stringify(event)}\n`, "utf8");
|
|
140
|
-
return { receipt, event };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
99
|
function relationsDocument(root) {
|
|
144
100
|
const file = contained(root, RELATIONS_FILE);
|
|
145
101
|
if (!fs.existsSync(file)) return { file, document: { schema_version: 1, revision: 0, relations: [] }, checksum: null };
|
package/lib/workspace.mjs
CHANGED
|
@@ -4,7 +4,6 @@ import { spawnSync } from "node:child_process";
|
|
|
4
4
|
import { parse as parseYaml, stringify as stringifyYaml } from "./yaml-runtime.mjs";
|
|
5
5
|
|
|
6
6
|
export const WORKSPACE_CONFIG = path.join(".workflow", "config", "workspace.yaml");
|
|
7
|
-
export const LEGACY_WORKSPACE_CONFIG = path.join(".ai-delivery", "workspace.yaml");
|
|
8
7
|
|
|
9
8
|
function toPosix(value) {
|
|
10
9
|
return value.replaceAll("\\", "/");
|
|
@@ -114,7 +113,7 @@ function initializeWorkflowRepository(workspaceRoot, branch, remote, heads, acti
|
|
|
114
113
|
const current = gitBranch(workspaceRoot);
|
|
115
114
|
if (current !== branch) {
|
|
116
115
|
if (gitHasCommit(workspaceRoot)) {
|
|
117
|
-
throw new Error(`Existing workflow repository must already use ${branch}; automatic
|
|
116
|
+
throw new Error(`Existing workflow repository must already use ${branch}; automatic branch changes are not supported.`);
|
|
118
117
|
}
|
|
119
118
|
if (!dryRun) runGit(workspaceRoot, ["symbolic-ref", "HEAD", `refs/heads/${branch}`]);
|
|
120
119
|
}
|
|
@@ -172,7 +171,7 @@ export function validateCodePath(workspaceRoot, codePath = "code") {
|
|
|
172
171
|
if (!normalized || normalized.includes("/") || normalized === "." || normalized === "..") {
|
|
173
172
|
throw new Error("Code path must be a relative direct child directory.");
|
|
174
173
|
}
|
|
175
|
-
if (new Set([".git", ".workflow", ".
|
|
174
|
+
if (new Set([".git", ".workflow", ".agents", ".codex"]).has(normalized)) {
|
|
176
175
|
throw new Error(`Reserved code path: ${normalized}`);
|
|
177
176
|
}
|
|
178
177
|
const absolute = path.resolve(workspaceRoot, normalized);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-delivery-workflow",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Project-scoped Codex skills and hooks for artifact-driven AI software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"skills"
|
|
23
23
|
],
|
|
24
24
|
"scripts": {
|
|
25
|
-
"test": "node --test verification/*.test.mjs",
|
|
25
|
+
"test": "node --test --test-concurrency=4 verification/*.test.mjs",
|
|
26
26
|
"manager:build": "npm --prefix manager run build",
|
|
27
27
|
"skills:validate": "node bin/validate-skills.mjs",
|
|
28
28
|
"audit:generate": "node audit/generate-all.mjs",
|
|
@@ -44,7 +44,7 @@ The first product question is the safe stopping point. Do not invent the initial
|
|
|
44
44
|
- `codebase-uninitialized`: use packaged `ai-delivery bootstrap`, review the repository baseline, inventory, context index, and adoption assessment, then invoke `$ai-delivery-define-product` to distinguish observed behavior from intended product behavior.
|
|
45
45
|
- `workflow-interrupted`: do not write bootstrap artifacts. If `runtime.next_action_owner` is `user`, return the recorded `next_action` as the single business question and perform no recovery, activation, checkpoint, task, artifact, or Git mutation. If it is `external`, report the recorded wait condition without mutation. Only an `agent`-owned next action may invoke `$ai-delivery-checkpoint-task recover`, reconcile reality, inspect the pending Evolution queue without applying changes mid-task, and continue `recommended_task_id`. When required input arrives, transfer ownership with both `--next-action-owner agent` and a new idempotent `--next-action`.
|
|
46
46
|
- `workflow-ready`: when a queued task is recommended, first apply the same `next_action_owner` boundary; activate it through `$ai-delivery-checkpoint-task` only when the owner is `agent`. Otherwise use the reported specialist skill or `$ai-delivery-orchestrate` to derive the next development node. Treat `release-ready` versions as completed development evidence, not implicit release intent. Route to `$ai-delivery-orchestrate-release` only when the user explicitly requests a release.
|
|
47
|
-
- `workflow-inconsistent`: stop automatic mutation, run the available project/package doctor internally, report exact conflicts, and obtain
|
|
47
|
+
- `workflow-inconsistent`: stop automatic mutation, run the available project/package doctor internally, report exact conflicts, and obtain an explicit repair decision. Use `upgrade --dry-run` for current managed installation drift; malformed current state, missing anchors, archive conflicts, and checksum drift fail closed until repaired through the governed current-state interface. Use `upgrade --force` only when the user explicitly approves replacing the reported managed files.
|
|
48
48
|
|
|
49
49
|
## Brownfield Rules
|
|
50
50
|
|
|
@@ -28,7 +28,7 @@ After installation, `node .workflow/tools/bootstrap/bootstrap.mjs inspect` is th
|
|
|
28
28
|
|
|
29
29
|
`ai-delivery bootstrap <project-root>` may initialize only `empty-uninitialized` and `codebase-uninitialized` projects through the project-scoped installer. Preserve existing code and configuration. On partial or conflicting installation, return `workflow-inconsistent` without writes. `--force` is allowed only after explicit user approval.
|
|
30
30
|
|
|
31
|
-
For an installed
|
|
31
|
+
For an installed or conflicting state, keep Bootstrap read-only and return `workflow-inconsistent` with the failing path and remediation. Use the packaged `repair --invalidate-archive` forward transaction only for an explicitly identified archive integrity defect; it records a human actor, reason, RCA evidence, and recovery evidence without rewriting immutable history.
|
|
32
32
|
|
|
33
33
|
For an uninitialized project, generate repository inventory and context artifacts from the first read-only scan snapshot. Do not let installer-generated `AGENTS.md`, `.gitignore`, Hook, skill, standard, or delivery files contaminate the observed pre-install codebase baseline.
|
|
34
34
|
|
|
@@ -11,10 +11,6 @@ Treat explicit task snapshots as authoritative. Treat Hook events as an audit tr
|
|
|
11
11
|
|
|
12
12
|
Write portable task state to `.workflow/control/tasks/<task-id>.yaml` and `.workflow/control/checkpoints/<task-id>/<checkpoint-id>.yaml`. Keep only PID, heartbeat, temporary locks, raw Hook events, and unshared context under ignored `.workflow/delivery/runtime/`. Keep durable node documents under `.workflow/iterations/<iteration-id>/<node-id>/`; runtime snapshots never replace registered delivery artifacts. Resolve code branch and commit from `.workflow/config/workspace.yaml` and the nested code repository.
|
|
13
13
|
|
|
14
|
-
## Legacy Node Boundary
|
|
15
|
-
|
|
16
|
-
Legacy development nodes are read-only compatibility. `inspect` remains a strictly read-only diagnosis path for historical tasks. `init`, `activate`, `checkpoint`, `finish`, and `archive-version` reject legacy task targets before mutation or Hook dispatch; `recover` refuses to materialize them and `publish` refuses to share them. New tasks use only the `development-seven-node` route, while `R00-release-request` through `R10-release-archive` remain writable.
|
|
17
|
-
|
|
18
14
|
## Start Or Resume
|
|
19
15
|
|
|
20
16
|
1. Locate the project root and shared state root.
|
|
@@ -34,7 +34,7 @@ Keep `.workflow/delivery/runtime/` local and ignored. Persist durable stage outp
|
|
|
34
34
|
|
|
35
35
|
`task-state.mjs` is the only runtime-state writer. Do not directly edit task JSON, checkpoint JSON, active-task markers, recovery output, or archive indexes. Formal workflow and artifact state belong to `.workflow/tools/state/state.mjs`; do not edit its YAML or JSONL streams directly.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
Task contracts use the current `development-seven-node` route or the independent `R00-release-request` through `R10-release-archive` release route. `inspect` is read-only diagnosis; `init`, `activate`, `checkpoint`, `finish`, `recover`, `archive-version`, and `publish` operate only on current task contracts.
|
|
38
38
|
|
|
39
39
|
When the installed Evolution CLI exists, `init`, `activate`, `checkpoint`, `finish`, and `archive-version` emit idempotent schema 1 `task.before`, `task.after`, and `task.failed` events. An approved blocking `task.before` result stops the task command before mutation. Hook dispatch failures otherwise fail open as warnings and never replace checkpoint state as the completion authority.
|
|
40
40
|
|
|
@@ -75,7 +75,7 @@ For an installed dual-repository project, `task-state.mjs` resolves `code_branch
|
|
|
75
75
|
|
|
76
76
|
`blockers` represents current unresolved blockers. A `blocked` update replaces that list; any transition to another status clears it. Historical blocker text remains available in prior checkpoints and events.
|
|
77
77
|
|
|
78
|
-
`next_action_owner` is required in persisted state and has exactly three values: `agent`, `user`, or `external`;
|
|
78
|
+
`next_action_owner` is required in persisted state and has exactly three values: `agent`, `user`, or `external`; explicit null, empty, boolean, numeric, or unknown values are invalid. `user` and `external` are zero-mutation wait states. `recover` may report shared wait state but must not hydrate local task or resume files for it. After the required input arrives, `activate` requires both `--next-action-owner agent` and a new idempotent `--next-action`; it may atomically hydrate a missing local runtime snapshot from the matching shared shard and transfer ownership in the same transition. Same-revision local and shared content must have identical semantic checksums. Evidence arrays are ordered sets: replaying an already recorded `--evidence` value does not append a duplicate.
|
|
79
79
|
|
|
80
80
|
Pass `--state-root` when worktrees must write to one shared primary state directory. Pass `--worktree` during init so the script writes a local `.workflow/delivery/runtime/active-task.json` marker pointing to that state root. Later `checkpoint`, `finish`, and `recover` commands automatically use the marker's shared state root.
|
|
81
81
|
|
|
@@ -144,15 +144,9 @@ function enforceFrontendGuard(projectRoot, input) {
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
try {
|
|
147
|
-
const
|
|
148
|
-
const legacyProjectFile = currentProjectFile
|
|
149
|
-
? null
|
|
150
|
-
: findUp(process.cwd(), path.join("delivery", "project.yaml"));
|
|
151
|
-
const projectFile = currentProjectFile || legacyProjectFile;
|
|
147
|
+
const projectFile = findUp(process.cwd(), path.join(".workflow", "delivery", "project.yaml"));
|
|
152
148
|
if (!projectFile) process.exit(0);
|
|
153
|
-
const projectRoot =
|
|
154
|
-
? path.dirname(path.dirname(path.dirname(projectFile)))
|
|
155
|
-
: path.dirname(path.dirname(projectFile));
|
|
149
|
+
const projectRoot = path.dirname(path.dirname(path.dirname(projectFile)));
|
|
156
150
|
const markerFile = path.join(projectRoot, ".workflow", "delivery", "runtime", "active-task.json");
|
|
157
151
|
const marker = fs.existsSync(markerFile) ? JSON.parse(fs.readFileSync(markerFile, "utf8")) : null;
|
|
158
152
|
const raw = await readStdin();
|
|
@@ -42,19 +42,13 @@ const REPEATABLE_ARGS = new Set([
|
|
|
42
42
|
"completed-step", "blocker", "evidence", "depends-on", "acceptance", "finding-json",
|
|
43
43
|
"change-set-json",
|
|
44
44
|
]);
|
|
45
|
-
const
|
|
46
|
-
"01-product-
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
51
|
-
"
|
|
52
|
-
"07-iteration-planning",
|
|
53
|
-
"08-test-design",
|
|
54
|
-
"09-development",
|
|
55
|
-
"10-change-review",
|
|
56
|
-
"11-candidate-verification",
|
|
57
|
-
"12-version-closeout",
|
|
45
|
+
const VALID_NODE_IDS = new Set([
|
|
46
|
+
"00-bootstrap", "01-product-shaping", "02-solution-design", "03-delivery-readiness",
|
|
47
|
+
"04-implementation", "05-candidate-assurance", "06-version-closeout",
|
|
48
|
+
"R00-release-request", "R01-version-selection", "R02-release-eligibility", "R03-material-aggregation",
|
|
49
|
+
"R04-release-candidate", "R05-release-verification", "R06-release-preparation", "R07-production-approval",
|
|
50
|
+
"R08-production-deployment", "R09-production-verification", "R10-release-archive",
|
|
51
|
+
"development", "implementation", "planning", "test", "review",
|
|
58
52
|
]);
|
|
59
53
|
|
|
60
54
|
function parseArgs(argv) {
|
|
@@ -83,10 +77,8 @@ function requireValue(args, key) {
|
|
|
83
77
|
return value;
|
|
84
78
|
}
|
|
85
79
|
|
|
86
|
-
function
|
|
87
|
-
if (
|
|
88
|
-
throw new Error(`Legacy workflow node is read-only: ${node}`);
|
|
89
|
-
}
|
|
80
|
+
function assertCurrentNode(node) {
|
|
81
|
+
if (!VALID_NODE_IDS.has(node)) throw new Error(`Unsupported workflow node: ${node}`);
|
|
90
82
|
return node;
|
|
91
83
|
}
|
|
92
84
|
|
|
@@ -720,7 +712,7 @@ function repairChangesSinceBaseline(workflowRoot, baseline) {
|
|
|
720
712
|
}
|
|
721
713
|
|
|
722
714
|
function writeSharedTask(runtime, workflowRoot, task) {
|
|
723
|
-
|
|
715
|
+
assertCurrentNode(task.node);
|
|
724
716
|
const sharedFile = sharedTaskFile(workflowRoot, task.task_id);
|
|
725
717
|
let previousRevision = 0;
|
|
726
718
|
if (fs.existsSync(sharedFile)) {
|
|
@@ -786,25 +778,15 @@ function normalizeTask(task) {
|
|
|
786
778
|
task[key] = [...new Set(task[key])];
|
|
787
779
|
}
|
|
788
780
|
task.sequence = parseSequence(task.sequence);
|
|
789
|
-
task.next_action_owner = parseNextActionOwner(task.next_action_owner);
|
|
790
|
-
task.responsibility
|
|
781
|
+
task.next_action_owner = parseNextActionOwner(task.next_action_owner, undefined);
|
|
782
|
+
if (!task.responsibility) throw new Error(`Task ${task.task_id || "unknown"} is missing responsibility.`);
|
|
791
783
|
task.derived_from = task.derived_from || null;
|
|
792
784
|
task.supersedes = task.supersedes || null;
|
|
793
|
-
if (!Array.isArray(task.attempts)) {
|
|
794
|
-
|
|
795
|
-
task.attempts = legacyNumber > 0 && (task.started_at || task.status !== "queued")
|
|
796
|
-
? [normalizeAttempt(task.task_id, {
|
|
797
|
-
number: legacyNumber,
|
|
798
|
-
status: projectedTaskStatus(task.status, "attempt"),
|
|
799
|
-
started_at: task.started_at || task.created_at || null,
|
|
800
|
-
finished_at: CLOSED_STATUS.has(task.status) ? task.finished_at || task.updated_at || null : null,
|
|
801
|
-
}, legacyNumber)]
|
|
802
|
-
: [];
|
|
803
|
-
} else {
|
|
804
|
-
task.attempts = task.attempts.map((attempt, index) => normalizeAttempt(task.task_id, attempt, index + 1));
|
|
785
|
+
if (Object.prototype.hasOwnProperty.call(task, "attempt") || !Array.isArray(task.attempts)) {
|
|
786
|
+
throw new Error(`Task ${task.task_id || "unknown"} must use canonical attempts.`);
|
|
805
787
|
}
|
|
788
|
+
task.attempts = task.attempts.map((attempt, index) => normalizeAttempt(task.task_id, attempt, index + 1));
|
|
806
789
|
task.change_sets = Array.isArray(task.change_sets) ? task.change_sets.map((item) => normalizeChangeSet(item)) : [];
|
|
807
|
-
task.attempt = task.attempts.at(-1)?.number || 0;
|
|
808
790
|
return task;
|
|
809
791
|
}
|
|
810
792
|
|
|
@@ -852,7 +834,6 @@ function startAttempt(task, phase, timestamp) {
|
|
|
852
834
|
time_entries: [],
|
|
853
835
|
}, number);
|
|
854
836
|
task.attempts.push(attempt);
|
|
855
|
-
task.attempt = number;
|
|
856
837
|
return attempt;
|
|
857
838
|
}
|
|
858
839
|
|
|
@@ -1379,7 +1360,7 @@ function summarizeIteration(tasks, iterationId) {
|
|
|
1379
1360
|
function commandInit(args) {
|
|
1380
1361
|
const taskId = sanitizeId(requireValue(args, "task-id"));
|
|
1381
1362
|
const iterationId = sanitizeId(requireValue(args, "iteration-id"));
|
|
1382
|
-
const node =
|
|
1363
|
+
const node = assertCurrentNode(requireValue(args, "node"));
|
|
1383
1364
|
const { projectRoot, stateRoot, workflowRoot, runtime } = statePaths(args);
|
|
1384
1365
|
return withTaskLock(runtime, `version.${iterationId}`, () => withTaskLock(runtime, taskId, () => {
|
|
1385
1366
|
if (fs.existsSync(archivedTaskIndexFile(runtime, taskId))) {
|
|
@@ -1424,6 +1405,7 @@ function commandInit(args) {
|
|
|
1424
1405
|
const candidateContract = normalizeTask({
|
|
1425
1406
|
...(existing || {}),
|
|
1426
1407
|
...contractFields,
|
|
1408
|
+
attempts: existing?.attempts || [],
|
|
1427
1409
|
next_action_owner: existing?.next_action_owner || "agent",
|
|
1428
1410
|
});
|
|
1429
1411
|
if (existing) {
|
|
@@ -1459,7 +1441,6 @@ function commandInit(args) {
|
|
|
1459
1441
|
...contractFields,
|
|
1460
1442
|
status,
|
|
1461
1443
|
sequence: parseSequence(args.sequence ?? existing?.sequence),
|
|
1462
|
-
attempt: 0,
|
|
1463
1444
|
attempts: [],
|
|
1464
1445
|
change_sets: [],
|
|
1465
1446
|
input_artifacts: args["input-artifact"] || existing?.input_artifacts || [],
|
|
@@ -1486,7 +1467,7 @@ function commandInit(args) {
|
|
|
1486
1467
|
clearTaskMarker(task);
|
|
1487
1468
|
appendEvent(runtime, taskId, "task-initialized", {
|
|
1488
1469
|
status: task.status,
|
|
1489
|
-
|
|
1470
|
+
attempt_count: task.attempts.length,
|
|
1490
1471
|
derived_from: task.derived_from,
|
|
1491
1472
|
supersedes: task.supersedes,
|
|
1492
1473
|
});
|
|
@@ -1557,7 +1538,6 @@ function commandActivate(args) {
|
|
|
1557
1538
|
phase.status = "active";
|
|
1558
1539
|
phase.finished_at = null;
|
|
1559
1540
|
}
|
|
1560
|
-
task.attempt = attempt.number;
|
|
1561
1541
|
task.started_at ||= timestamp;
|
|
1562
1542
|
task.updated_at = timestamp;
|
|
1563
1543
|
task.finished_at = null;
|
|
@@ -1573,7 +1553,7 @@ function commandActivate(args) {
|
|
|
1573
1553
|
});
|
|
1574
1554
|
appendEvent(runtime, taskId, "task-activated", {
|
|
1575
1555
|
status,
|
|
1576
|
-
|
|
1556
|
+
attempt_count: task.attempts.length,
|
|
1577
1557
|
attempt_id: attempt.attempt_id,
|
|
1578
1558
|
phase: attempt.phases.at(-1)?.phase || null,
|
|
1579
1559
|
new_attempt: newAttempt,
|
|
@@ -1627,7 +1607,7 @@ function commandUpdate(args, finish = false) {
|
|
|
1627
1607
|
writeResume(runtime, task);
|
|
1628
1608
|
appendEvent(runtime, taskId, finish ? "task-finished" : "task-checkpointed", {
|
|
1629
1609
|
status,
|
|
1630
|
-
|
|
1610
|
+
attempt_number: attempt.number,
|
|
1631
1611
|
attempt_id: attempt.attempt_id,
|
|
1632
1612
|
phase: attempt.phases.at(-1)?.phase || null,
|
|
1633
1613
|
});
|
|
@@ -1696,7 +1676,7 @@ function commandArchiveVersion(args) {
|
|
|
1696
1676
|
.map((name) => normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8"))))
|
|
1697
1677
|
.filter((task) => task.iteration_id === iterationId);
|
|
1698
1678
|
if (!tasks.length) throw new Error(`No tasks found for version: ${iterationId}`);
|
|
1699
|
-
for (const task of tasks)
|
|
1679
|
+
for (const task of tasks) assertCurrentNode(task.node);
|
|
1700
1680
|
const sharedById = new Map(readSharedTasks(workflowRoot, stateRoot).map((task) => [task.task_id, task]));
|
|
1701
1681
|
for (const task of tasks) {
|
|
1702
1682
|
const shared = sharedById.get(task.task_id);
|
|
@@ -1795,7 +1775,7 @@ function commandRecover(args) {
|
|
|
1795
1775
|
const reportableTasks = selectedTasks
|
|
1796
1776
|
.filter((task) => !requested || task.task_id === requested)
|
|
1797
1777
|
.filter((task) => includeClosed || !RECOVERY_COMPLETE_STATUS.has(task.status));
|
|
1798
|
-
for (const task of reportableTasks)
|
|
1778
|
+
for (const task of reportableTasks) assertCurrentNode(task.node);
|
|
1799
1779
|
for (const task of reportableTasks) {
|
|
1800
1780
|
if (missingLocalTaskIds.has(task.task_id)) atomicWrite(taskFile(runtime, task.task_id), task);
|
|
1801
1781
|
}
|
|
@@ -1853,7 +1833,7 @@ function commandPublish(args) {
|
|
|
1853
1833
|
const checkpointDirectory = path.join(workflowRoot, "control", "checkpoints", taskId);
|
|
1854
1834
|
if (!fs.existsSync(sharedFile)) throw new Error(`Shared task state not found: ${taskId}`);
|
|
1855
1835
|
const shared = JSON.parse(fs.readFileSync(sharedFile, "utf8"));
|
|
1856
|
-
|
|
1836
|
+
assertCurrentNode(shared.node);
|
|
1857
1837
|
|
|
1858
1838
|
const repository = gitState(stateRoot);
|
|
1859
1839
|
if (!repository.repository || path.resolve(repository.root).toLowerCase() !== path.resolve(stateRoot).toLowerCase()) {
|
|
@@ -1978,7 +1958,7 @@ function assertWritableEvolutionTarget(args, command) {
|
|
|
1978
1958
|
if (!new Set(["init", "activate", "checkpoint", "finish", "archive-version"]).has(command)) return;
|
|
1979
1959
|
if (command === "init") {
|
|
1980
1960
|
const node = args.node && args.node !== true ? String(args.node) : null;
|
|
1981
|
-
if (node)
|
|
1961
|
+
if (node) assertCurrentNode(node);
|
|
1982
1962
|
return;
|
|
1983
1963
|
}
|
|
1984
1964
|
const { runtime, workflowRoot, stateRoot } = statePaths(args);
|
|
@@ -1990,14 +1970,14 @@ function assertWritableEvolutionTarget(args, command) {
|
|
|
1990
1970
|
if (!iterationId || !fs.existsSync(tasksDir)) return;
|
|
1991
1971
|
for (const name of fs.readdirSync(tasksDir).filter((item) => item.endsWith(".json"))) {
|
|
1992
1972
|
const task = normalizeTask(JSON.parse(fs.readFileSync(path.join(tasksDir, name), "utf8")));
|
|
1993
|
-
if (task.iteration_id === iterationId)
|
|
1973
|
+
if (task.iteration_id === iterationId) assertCurrentNode(task.node);
|
|
1994
1974
|
}
|
|
1995
1975
|
return;
|
|
1996
1976
|
}
|
|
1997
1977
|
const taskId = args["task-id"] && args["task-id"] !== true ? sanitizeId(args["task-id"]) : null;
|
|
1998
1978
|
if (!taskId) return;
|
|
1999
1979
|
const task = taskSources(runtime, workflowRoot, stateRoot, taskId).task;
|
|
2000
|
-
if (task)
|
|
1980
|
+
if (task) assertCurrentNode(task.node);
|
|
2001
1981
|
}
|
|
2002
1982
|
|
|
2003
1983
|
function dispatchTaskEvolution(context, phase, result = null, error = null) {
|