@ai-agent-forge/plugin-workflow 0.85.20

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.
@@ -0,0 +1,311 @@
1
+ /**
2
+ * workflow_set input parsing and structural validation (§6), including the
3
+ * running-modification grading rules (§6 分级 / principle 9: pending is free,
4
+ * active/awaiting_review amend with a mandatory amendNote, completed/skipped
5
+ * are frozen history).
6
+ *
7
+ * All failures are structured errors (thrown Error with a field-specific
8
+ * message), matching the goal_set/plan_set validation style: the original
9
+ * error always reaches the caller; nothing is silently coerced.
10
+ */
11
+ // ---------------------------------------------------------------------------
12
+ // Primitive parsers (goal_set/plan_set style)
13
+ // ---------------------------------------------------------------------------
14
+ export function asObject(value, name) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value))
16
+ throw new Error(`${name} must be an object`);
17
+ return value;
18
+ }
19
+ export function asText(value, name) {
20
+ if (typeof value !== "string" || value.trim() === "")
21
+ throw new Error(`${name} must be a non-empty string`);
22
+ return value;
23
+ }
24
+ function optionalText(value, name) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ return asText(value, name);
28
+ }
29
+ function intAtLeast(value, minimum, name) {
30
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum) {
31
+ throw new Error(`${name} must be an integer >= ${minimum}`);
32
+ }
33
+ return value;
34
+ }
35
+ function stringArray(value, name) {
36
+ if (value === undefined)
37
+ return [];
38
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string" || item.trim() === "")) {
39
+ throw new Error(`${name} must be an array of non-empty strings`);
40
+ }
41
+ return [...value];
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // workflow_set input parsing
45
+ // ---------------------------------------------------------------------------
46
+ function parseStage(raw, index) {
47
+ const stage = asObject(raw, `stages[${index}]`);
48
+ const id = asText(stage.id, `stages[${index}].id`);
49
+ const field = (name) => `stage ${id}: ${name}`;
50
+ const title = asText(stage.title, field("title"));
51
+ const objectiveRaw = asObject(stage.objective, field("objective"));
52
+ const statement = asText(objectiveRaw.statement, field("objective.statement"));
53
+ let acceptance = [];
54
+ if (objectiveRaw.acceptance !== undefined) {
55
+ if (!Array.isArray(objectiveRaw.acceptance)) {
56
+ throw new Error(field("objective.acceptance must be an array"));
57
+ }
58
+ acceptance = objectiveRaw.acceptance.map((item, itemIndex) => {
59
+ const entry = asObject(item, field(`objective.acceptance[${itemIndex}]`));
60
+ return {
61
+ id: asText(entry.id, field(`objective.acceptance[${itemIndex}].id`)),
62
+ statement: asText(entry.statement, field(`objective.acceptance[${itemIndex}].statement`)),
63
+ };
64
+ });
65
+ const seen = new Set();
66
+ for (const item of acceptance) {
67
+ if (seen.has(item.id))
68
+ throw new Error(field(`duplicate acceptance id ${item.id}`));
69
+ seen.add(item.id);
70
+ }
71
+ }
72
+ let kind = "write";
73
+ if (stage.kind !== undefined) {
74
+ if (stage.kind !== "read" && stage.kind !== "write") {
75
+ throw new Error(field('kind must be "read" or "write"'));
76
+ }
77
+ kind = stage.kind;
78
+ }
79
+ let advance = "auto";
80
+ if (stage.advance !== undefined) {
81
+ if (stage.advance !== "auto" && stage.advance !== "review") {
82
+ throw new Error(field('advance must be "auto" or "review"'));
83
+ }
84
+ advance = stage.advance;
85
+ }
86
+ const scopeRaw = stringArray(stage.scope, field("scope"));
87
+ const retryLimit = stage.retryLimit === undefined ? undefined : intAtLeast(stage.retryLimit, 0, field("retryLimit"));
88
+ const planId = optionalText(stage.planId, field("planId"));
89
+ const role = optionalText(stage.role, field("role"));
90
+ const onFail = optionalText(stage.onFail, field("onFail"));
91
+ const amendNote = optionalText(stage.amendNote, field("amendNote"));
92
+ return {
93
+ id,
94
+ title,
95
+ objective: { statement, acceptance },
96
+ dependsOn: stringArray(stage.dependsOn, field("dependsOn")),
97
+ ...(planId !== undefined ? { planId } : {}),
98
+ kind,
99
+ ...(scopeRaw.length > 0 ? { scope: scopeRaw } : {}),
100
+ ...(role !== undefined ? { role } : {}),
101
+ advance,
102
+ ...(retryLimit !== undefined ? { retryLimit } : {}),
103
+ ...(onFail !== undefined ? { onFail } : {}),
104
+ ...(amendNote !== undefined ? { amendNote } : {}),
105
+ };
106
+ }
107
+ export function parseWorkflowSetInput(raw) {
108
+ const value = asObject(raw, "workflow input");
109
+ if (!Array.isArray(value.stages) || value.stages.length === 0) {
110
+ throw new Error("stages must be a non-empty array");
111
+ }
112
+ const goalId = optionalText(value.goalId, "goalId");
113
+ return {
114
+ id: asText(value.id, "id"),
115
+ title: asText(value.title, "title"),
116
+ revision: intAtLeast(value.revision, 0, "revision"),
117
+ ...(goalId !== undefined ? { goalId } : {}),
118
+ ...(value.maxRetreats === undefined ? {} : { maxRetreats: intAtLeast(value.maxRetreats, 0, "maxRetreats") }),
119
+ ...(value.maxActiveStages === undefined
120
+ ? {}
121
+ : { maxActiveStages: intAtLeast(value.maxActiveStages, 1, "maxActiveStages") }),
122
+ stages: value.stages.map((stage, index) => parseStage(stage, index)),
123
+ };
124
+ }
125
+ // ---------------------------------------------------------------------------
126
+ // Structural graph validation (§6 workflow_set)
127
+ // ---------------------------------------------------------------------------
128
+ /** Whether `targetId` is transitively reachable from `startId` along dependsOn edges. */
129
+ export function dependsOnReach(stages, startId, targetId) {
130
+ const byId = new Map(stages.map((stage) => [stage.id, stage]));
131
+ const visited = new Set();
132
+ const queue = [startId];
133
+ while (queue.length > 0) {
134
+ const current = queue.pop();
135
+ if (current === undefined || visited.has(current))
136
+ continue;
137
+ visited.add(current);
138
+ if (current === targetId)
139
+ return true;
140
+ const spec = byId.get(current);
141
+ if (spec !== undefined)
142
+ queue.push(...spec.dependsOn);
143
+ }
144
+ return false;
145
+ }
146
+ /**
147
+ * Full structural validation: stage id uniqueness, reference existence,
148
+ * workflow-internal planId uniqueness (cross-workflow sharing is allowed),
149
+ * onFail pointing topologically earlier, and combined dependsOn+onFail cycle
150
+ * detection (DFS over the mixed edge set — §6 names mixed cycles explicitly).
151
+ */
152
+ export function validateStageGraph(stages) {
153
+ if (stages.length === 0)
154
+ throw new Error("stages must not be empty");
155
+ const byId = new Map();
156
+ for (const stage of stages) {
157
+ if (byId.has(stage.id))
158
+ throw new Error(`duplicate stage id: ${stage.id}`);
159
+ byId.set(stage.id, stage);
160
+ }
161
+ const planOwner = new Map();
162
+ for (const stage of stages) {
163
+ if (stage.planId === undefined)
164
+ continue;
165
+ const owner = planOwner.get(stage.planId);
166
+ if (owner !== undefined) {
167
+ throw new Error(`stage ${stage.id}: planId ${stage.planId} is already referenced by stage ${owner} ` +
168
+ "(planId uniqueness applies within a workflow; cross-workflow references are allowed)");
169
+ }
170
+ planOwner.set(stage.planId, stage.id);
171
+ }
172
+ for (const stage of stages) {
173
+ for (const dep of stage.dependsOn) {
174
+ if (!byId.has(dep))
175
+ throw new Error(`stage ${stage.id}: dependsOn references unknown stage ${dep}`);
176
+ }
177
+ if (stage.onFail !== undefined) {
178
+ if (!byId.has(stage.onFail)) {
179
+ throw new Error(`stage ${stage.id}: onFail references unknown stage ${stage.onFail}`);
180
+ }
181
+ if (stage.onFail === stage.id) {
182
+ throw new Error(`stage ${stage.id}: onFail must target a different stage`);
183
+ }
184
+ if (dependsOnReach(stages, stage.onFail, stage.id)) {
185
+ throw new Error(`stage ${stage.id}: onFail target ${stage.onFail} is topologically later ` +
186
+ `(it transitively depends on ${stage.id})`);
187
+ }
188
+ }
189
+ }
190
+ // DFS cycle detection over dependsOn + onFail edges (mixed cycles included).
191
+ const state = new Map();
192
+ const visit = (id, path) => {
193
+ const mark = state.get(id);
194
+ if (mark === "done")
195
+ return;
196
+ if (mark === "visiting") {
197
+ throw new Error(`dependency graph has a cycle: ${[...path, id].join(" -> ")} (dependsOn/onFail edges)`);
198
+ }
199
+ state.set(id, "visiting");
200
+ const spec = byId.get(id);
201
+ if (spec !== undefined) {
202
+ for (const next of spec.dependsOn)
203
+ visit(next, [...path, id]);
204
+ if (spec.onFail !== undefined)
205
+ visit(spec.onFail, [...path, id]);
206
+ }
207
+ state.set(id, "done");
208
+ };
209
+ for (const stage of stages)
210
+ visit(stage.id, []);
211
+ }
212
+ // ---------------------------------------------------------------------------
213
+ // Running-modification grading (§6 分级 / principle 9)
214
+ // ---------------------------------------------------------------------------
215
+ /** Fields an active/awaiting_review stage may NOT change (§6 amend list is the complement). */
216
+ const FROZEN_STAGE_FIELDS = ["title", "dependsOn", "kind", "advance", "retryLimit", "onFail"];
217
+ function acceptanceEqual(a, b) {
218
+ return (a.length === b.length &&
219
+ a.every((item, index) => item.id === b[index].id && item.statement === b[index].statement));
220
+ }
221
+ function stringArrayEqual(a, b) {
222
+ const left = a ?? [];
223
+ const right = b ?? [];
224
+ return left.length === right.length && left.every((item, index) => item === right[index]);
225
+ }
226
+ function changedStageFields(previous, next) {
227
+ const fields = [];
228
+ if (previous.title !== next.title)
229
+ fields.push("title");
230
+ if (previous.objective.statement !== next.objective.statement)
231
+ fields.push("objective.statement");
232
+ if (!acceptanceEqual(previous.objective.acceptance, next.objective.acceptance))
233
+ fields.push("acceptance");
234
+ if (!stringArrayEqual(previous.dependsOn, next.dependsOn))
235
+ fields.push("dependsOn");
236
+ if ((previous.planId ?? "") !== (next.planId ?? ""))
237
+ fields.push("planId");
238
+ if (!stringArrayEqual(previous.scope, next.scope))
239
+ fields.push("scope");
240
+ if ((previous.role ?? "") !== (next.role ?? ""))
241
+ fields.push("role");
242
+ if (previous.kind !== next.kind)
243
+ fields.push("kind");
244
+ if (previous.advance !== next.advance)
245
+ fields.push("advance");
246
+ if ((previous.retryLimit ?? -1) !== (next.retryLimit ?? -1))
247
+ fields.push("retryLimit");
248
+ if ((previous.onFail ?? "") !== (next.onFail ?? ""))
249
+ fields.push("onFail");
250
+ return fields;
251
+ }
252
+ /**
253
+ * Grades a full-submit update against the stored record: append-only stage
254
+ * sequence (no mid-insert, reorder, id change, or removal — removal is `skip`
255
+ * by design), pending stages freely modifiable, active/awaiting_review stages
256
+ * amendable only within the §6 field list and only with a non-empty
257
+ * amendNote, completed/skipped specs frozen. Throws structured errors on
258
+ * violation; completed-wins race semantics (§6: amend vs completion
259
+ * evaluation) fall out naturally because a completed stage is frozen.
260
+ */
261
+ export function planWorkflowSpecUpdate(existing, next) {
262
+ if (existing === undefined) {
263
+ return { created: true, amendChanges: [], appendedStageIds: next.stages.map((stage) => stage.id) };
264
+ }
265
+ const oldIds = existing.stages.map((stage) => stage.id);
266
+ const nextIds = next.stages.map((stage) => stage.id);
267
+ for (let index = 0; index < oldIds.length; index += 1) {
268
+ if (nextIds[index] !== oldIds[index]) {
269
+ throw new Error(`stage sequence changed at position ${index} (expected ${oldIds[index]}, found ${nextIds[index] ?? "end"}): ` +
270
+ "stages may only be appended; inserting, reordering, changing ids, and removing stages are not allowed " +
271
+ "(drop a stage with workflow_advance skip instead)");
272
+ }
273
+ }
274
+ const nextById = new Map(next.stages.map((stage) => [stage.id, stage]));
275
+ const amendChanges = [];
276
+ for (const previous of existing.stages) {
277
+ const updated = nextById.get(previous.id);
278
+ if (updated === undefined)
279
+ continue; // covered by the sequence check above
280
+ const runtime = existing.runtime.find((item) => item.stageId === previous.id);
281
+ const status = runtime?.status ?? "pending";
282
+ const fields = changedStageFields(previous, updated);
283
+ if (fields.length === 0)
284
+ continue;
285
+ if (status === "completed" || status === "skipped") {
286
+ throw new Error(`stage ${previous.id} is ${status} and its spec is frozen (history must stay interpretable); ` +
287
+ "use workflow_advance retreat or append a corrective stage instead");
288
+ }
289
+ if (status === "pending")
290
+ continue; // free modification (§6)
291
+ // active / awaiting_review / failed: amend path (§6). `failed` is unreachable
292
+ // in a non-terminal workflow (failure disposal resets to pending) but graded
293
+ // as amend defensively.
294
+ const frozen = fields.filter((name) => FROZEN_STAGE_FIELDS.includes(name));
295
+ if (frozen.length > 0) {
296
+ throw new Error(`stage ${previous.id} is ${status}; only objective/acceptance/scope/role/planId may be amended ` +
297
+ `(changed frozen fields: ${frozen.join(", ")})`);
298
+ }
299
+ const amendNote = updated.amendNote ?? "";
300
+ if (amendNote.trim() === "") {
301
+ throw new Error(`stage ${previous.id} is ${status} and changed; amendNote must carry the correction reason`);
302
+ }
303
+ const prunedEvidence = previous.objective.acceptance
304
+ .filter((item) => !updated.objective.acceptance.some((candidate) => candidate.id === item.id) &&
305
+ (runtime?.evidence.some((entry) => entry.acceptanceId === item.id) ?? false))
306
+ .map((item) => item.id);
307
+ amendChanges.push({ stageId: previous.id, fields, amendNote, prunedEvidence });
308
+ }
309
+ return { created: false, amendChanges, appendedStageIds: nextIds.slice(oldIds.length) };
310
+ }
311
+ //# sourceMappingURL=validate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.js","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * workflow_set input parsing and structural validation (§6), including the\n * running-modification grading rules (§6 分级 / principle 9: pending is free,\n * active/awaiting_review amend with a mandatory amendNote, completed/skipped\n * are frozen history).\n *\n * All failures are structured errors (thrown Error with a field-specific\n * message), matching the goal_set/plan_set validation style: the original\n * error always reaches the caller; nothing is silently coerced.\n */\n\nimport type { StageSpec, WorkflowRecord } from \"./types.ts\";\n\nexport interface ParsedWorkflowSet {\n\tid: string;\n\ttitle: string;\n\tgoalId?: string;\n\trevision: number;\n\tmaxRetreats?: number;\n\tmaxActiveStages?: number;\n\tstages: StageSpec[];\n}\n\n/** One active/awaiting_review stage that changed spec and must leave a stage.spec-amended audit (§6). */\nexport interface StageChangePlan {\n\tstageId: string;\n\t/** Amendable field names that differ from the stored spec. */\n\tfields: string[];\n\t/** Correction reason; required non-empty on the amend path. */\n\tamendNote: string;\n\t/** Acceptance ids removed by the amend that still had evidence attached (linkage detail, §6). */\n\tprunedEvidence: string[];\n}\n\nexport interface WorkflowSetDiff {\n\tcreated: boolean;\n\t/** Amend-path changes (one stage.spec-amended transition each). */\n\tamendChanges: StageChangePlan[];\n\t/** Stage ids appended after the existing stage sequence (append-only structure ops, §6). */\n\tappendedStageIds: string[];\n}\n\n// ---------------------------------------------------------------------------\n// Primitive parsers (goal_set/plan_set style)\n// ---------------------------------------------------------------------------\n\nexport function asObject(value: unknown, name: string): Record<string, unknown> {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) throw new Error(`${name} must be an object`);\n\treturn value as Record<string, unknown>;\n}\n\nexport function asText(value: unknown, name: string): string {\n\tif (typeof value !== \"string\" || value.trim() === \"\") throw new Error(`${name} must be a non-empty string`);\n\treturn value;\n}\n\nfunction optionalText(value: unknown, name: string): string | undefined {\n\tif (value === undefined) return undefined;\n\treturn asText(value, name);\n}\n\nfunction intAtLeast(value: unknown, minimum: number, name: string): number {\n\tif (typeof value !== \"number\" || !Number.isInteger(value) || value < minimum) {\n\t\tthrow new Error(`${name} must be an integer >= ${minimum}`);\n\t}\n\treturn value;\n}\n\nfunction stringArray(value: unknown, name: string): string[] {\n\tif (value === undefined) return [];\n\tif (!Array.isArray(value) || value.some((item) => typeof item !== \"string\" || item.trim() === \"\")) {\n\t\tthrow new Error(`${name} must be an array of non-empty strings`);\n\t}\n\treturn [...(value as string[])];\n}\n\n// ---------------------------------------------------------------------------\n// workflow_set input parsing\n// ---------------------------------------------------------------------------\n\nfunction parseStage(raw: unknown, index: number): StageSpec {\n\tconst stage = asObject(raw, `stages[${index}]`);\n\tconst id = asText(stage.id, `stages[${index}].id`);\n\tconst field = (name: string) => `stage ${id}: ${name}`;\n\tconst title = asText(stage.title, field(\"title\"));\n\n\tconst objectiveRaw = asObject(stage.objective, field(\"objective\"));\n\tconst statement = asText(objectiveRaw.statement, field(\"objective.statement\"));\n\tlet acceptance: { id: string; statement: string }[] = [];\n\tif (objectiveRaw.acceptance !== undefined) {\n\t\tif (!Array.isArray(objectiveRaw.acceptance)) {\n\t\t\tthrow new Error(field(\"objective.acceptance must be an array\"));\n\t\t}\n\t\tacceptance = objectiveRaw.acceptance.map((item, itemIndex) => {\n\t\t\tconst entry = asObject(item, field(`objective.acceptance[${itemIndex}]`));\n\t\t\treturn {\n\t\t\t\tid: asText(entry.id, field(`objective.acceptance[${itemIndex}].id`)),\n\t\t\t\tstatement: asText(entry.statement, field(`objective.acceptance[${itemIndex}].statement`)),\n\t\t\t};\n\t\t});\n\t\tconst seen = new Set<string>();\n\t\tfor (const item of acceptance) {\n\t\t\tif (seen.has(item.id)) throw new Error(field(`duplicate acceptance id ${item.id}`));\n\t\t\tseen.add(item.id);\n\t\t}\n\t}\n\n\tlet kind: StageSpec[\"kind\"] = \"write\";\n\tif (stage.kind !== undefined) {\n\t\tif (stage.kind !== \"read\" && stage.kind !== \"write\") {\n\t\t\tthrow new Error(field('kind must be \"read\" or \"write\"'));\n\t\t}\n\t\tkind = stage.kind;\n\t}\n\tlet advance: StageSpec[\"advance\"] = \"auto\";\n\tif (stage.advance !== undefined) {\n\t\tif (stage.advance !== \"auto\" && stage.advance !== \"review\") {\n\t\t\tthrow new Error(field('advance must be \"auto\" or \"review\"'));\n\t\t}\n\t\tadvance = stage.advance;\n\t}\n\tconst scopeRaw = stringArray(stage.scope, field(\"scope\"));\n\tconst retryLimit = stage.retryLimit === undefined ? undefined : intAtLeast(stage.retryLimit, 0, field(\"retryLimit\"));\n\tconst planId = optionalText(stage.planId, field(\"planId\"));\n\tconst role = optionalText(stage.role, field(\"role\"));\n\tconst onFail = optionalText(stage.onFail, field(\"onFail\"));\n\tconst amendNote = optionalText(stage.amendNote, field(\"amendNote\"));\n\n\treturn {\n\t\tid,\n\t\ttitle,\n\t\tobjective: { statement, acceptance },\n\t\tdependsOn: stringArray(stage.dependsOn, field(\"dependsOn\")),\n\t\t...(planId !== undefined ? { planId } : {}),\n\t\tkind,\n\t\t...(scopeRaw.length > 0 ? { scope: scopeRaw } : {}),\n\t\t...(role !== undefined ? { role } : {}),\n\t\tadvance,\n\t\t...(retryLimit !== undefined ? { retryLimit } : {}),\n\t\t...(onFail !== undefined ? { onFail } : {}),\n\t\t...(amendNote !== undefined ? { amendNote } : {}),\n\t};\n}\n\nexport function parseWorkflowSetInput(raw: unknown): ParsedWorkflowSet {\n\tconst value = asObject(raw, \"workflow input\");\n\tif (!Array.isArray(value.stages) || value.stages.length === 0) {\n\t\tthrow new Error(\"stages must be a non-empty array\");\n\t}\n\tconst goalId = optionalText(value.goalId, \"goalId\");\n\treturn {\n\t\tid: asText(value.id, \"id\"),\n\t\ttitle: asText(value.title, \"title\"),\n\t\trevision: intAtLeast(value.revision, 0, \"revision\"),\n\t\t...(goalId !== undefined ? { goalId } : {}),\n\t\t...(value.maxRetreats === undefined ? {} : { maxRetreats: intAtLeast(value.maxRetreats, 0, \"maxRetreats\") }),\n\t\t...(value.maxActiveStages === undefined\n\t\t\t? {}\n\t\t\t: { maxActiveStages: intAtLeast(value.maxActiveStages, 1, \"maxActiveStages\") }),\n\t\tstages: value.stages.map((stage, index) => parseStage(stage, index)),\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Structural graph validation (§6 workflow_set)\n// ---------------------------------------------------------------------------\n\n/** Whether `targetId` is transitively reachable from `startId` along dependsOn edges. */\nexport function dependsOnReach(stages: StageSpec[], startId: string, targetId: string): boolean {\n\tconst byId = new Map(stages.map((stage) => [stage.id, stage]));\n\tconst visited = new Set<string>();\n\tconst queue = [startId];\n\twhile (queue.length > 0) {\n\t\tconst current = queue.pop();\n\t\tif (current === undefined || visited.has(current)) continue;\n\t\tvisited.add(current);\n\t\tif (current === targetId) return true;\n\t\tconst spec = byId.get(current);\n\t\tif (spec !== undefined) queue.push(...spec.dependsOn);\n\t}\n\treturn false;\n}\n\n/**\n * Full structural validation: stage id uniqueness, reference existence,\n * workflow-internal planId uniqueness (cross-workflow sharing is allowed),\n * onFail pointing topologically earlier, and combined dependsOn+onFail cycle\n * detection (DFS over the mixed edge set — §6 names mixed cycles explicitly).\n */\nexport function validateStageGraph(stages: StageSpec[]): void {\n\tif (stages.length === 0) throw new Error(\"stages must not be empty\");\n\tconst byId = new Map<string, StageSpec>();\n\tfor (const stage of stages) {\n\t\tif (byId.has(stage.id)) throw new Error(`duplicate stage id: ${stage.id}`);\n\t\tbyId.set(stage.id, stage);\n\t}\n\tconst planOwner = new Map<string, string>();\n\tfor (const stage of stages) {\n\t\tif (stage.planId === undefined) continue;\n\t\tconst owner = planOwner.get(stage.planId);\n\t\tif (owner !== undefined) {\n\t\t\tthrow new Error(\n\t\t\t\t`stage ${stage.id}: planId ${stage.planId} is already referenced by stage ${owner} ` +\n\t\t\t\t\t\"(planId uniqueness applies within a workflow; cross-workflow references are allowed)\",\n\t\t\t);\n\t\t}\n\t\tplanOwner.set(stage.planId, stage.id);\n\t}\n\tfor (const stage of stages) {\n\t\tfor (const dep of stage.dependsOn) {\n\t\t\tif (!byId.has(dep)) throw new Error(`stage ${stage.id}: dependsOn references unknown stage ${dep}`);\n\t\t}\n\t\tif (stage.onFail !== undefined) {\n\t\t\tif (!byId.has(stage.onFail)) {\n\t\t\t\tthrow new Error(`stage ${stage.id}: onFail references unknown stage ${stage.onFail}`);\n\t\t\t}\n\t\t\tif (stage.onFail === stage.id) {\n\t\t\t\tthrow new Error(`stage ${stage.id}: onFail must target a different stage`);\n\t\t\t}\n\t\t\tif (dependsOnReach(stages, stage.onFail, stage.id)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`stage ${stage.id}: onFail target ${stage.onFail} is topologically later ` +\n\t\t\t\t\t\t`(it transitively depends on ${stage.id})`,\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t}\n\t// DFS cycle detection over dependsOn + onFail edges (mixed cycles included).\n\tconst state = new Map<string, \"visiting\" | \"done\">();\n\tconst visit = (id: string, path: string[]): void => {\n\t\tconst mark = state.get(id);\n\t\tif (mark === \"done\") return;\n\t\tif (mark === \"visiting\") {\n\t\t\tthrow new Error(`dependency graph has a cycle: ${[...path, id].join(\" -> \")} (dependsOn/onFail edges)`);\n\t\t}\n\t\tstate.set(id, \"visiting\");\n\t\tconst spec = byId.get(id);\n\t\tif (spec !== undefined) {\n\t\t\tfor (const next of spec.dependsOn) visit(next, [...path, id]);\n\t\t\tif (spec.onFail !== undefined) visit(spec.onFail, [...path, id]);\n\t\t}\n\t\tstate.set(id, \"done\");\n\t};\n\tfor (const stage of stages) visit(stage.id, []);\n}\n\n// ---------------------------------------------------------------------------\n// Running-modification grading (§6 分级 / principle 9)\n// ---------------------------------------------------------------------------\n\n/** Fields an active/awaiting_review stage may NOT change (§6 amend list is the complement). */\nconst FROZEN_STAGE_FIELDS: readonly string[] = [\"title\", \"dependsOn\", \"kind\", \"advance\", \"retryLimit\", \"onFail\"];\n\nfunction acceptanceEqual(a: { id: string; statement: string }[], b: { id: string; statement: string }[]): boolean {\n\treturn (\n\t\ta.length === b.length &&\n\t\ta.every((item, index) => item.id === b[index].id && item.statement === b[index].statement)\n\t);\n}\n\nfunction stringArrayEqual(a: string[] | undefined, b: string[] | undefined): boolean {\n\tconst left = a ?? [];\n\tconst right = b ?? [];\n\treturn left.length === right.length && left.every((item, index) => item === right[index]);\n}\n\nfunction changedStageFields(previous: StageSpec, next: StageSpec): string[] {\n\tconst fields: string[] = [];\n\tif (previous.title !== next.title) fields.push(\"title\");\n\tif (previous.objective.statement !== next.objective.statement) fields.push(\"objective.statement\");\n\tif (!acceptanceEqual(previous.objective.acceptance, next.objective.acceptance)) fields.push(\"acceptance\");\n\tif (!stringArrayEqual(previous.dependsOn, next.dependsOn)) fields.push(\"dependsOn\");\n\tif ((previous.planId ?? \"\") !== (next.planId ?? \"\")) fields.push(\"planId\");\n\tif (!stringArrayEqual(previous.scope, next.scope)) fields.push(\"scope\");\n\tif ((previous.role ?? \"\") !== (next.role ?? \"\")) fields.push(\"role\");\n\tif (previous.kind !== next.kind) fields.push(\"kind\");\n\tif (previous.advance !== next.advance) fields.push(\"advance\");\n\tif ((previous.retryLimit ?? -1) !== (next.retryLimit ?? -1)) fields.push(\"retryLimit\");\n\tif ((previous.onFail ?? \"\") !== (next.onFail ?? \"\")) fields.push(\"onFail\");\n\treturn fields;\n}\n\n/**\n * Grades a full-submit update against the stored record: append-only stage\n * sequence (no mid-insert, reorder, id change, or removal — removal is `skip`\n * by design), pending stages freely modifiable, active/awaiting_review stages\n * amendable only within the §6 field list and only with a non-empty\n * amendNote, completed/skipped specs frozen. Throws structured errors on\n * violation; completed-wins race semantics (§6: amend vs completion\n * evaluation) fall out naturally because a completed stage is frozen.\n */\nexport function planWorkflowSpecUpdate(existing: WorkflowRecord | undefined, next: ParsedWorkflowSet): WorkflowSetDiff {\n\tif (existing === undefined) {\n\t\treturn { created: true, amendChanges: [], appendedStageIds: next.stages.map((stage) => stage.id) };\n\t}\n\tconst oldIds = existing.stages.map((stage) => stage.id);\n\tconst nextIds = next.stages.map((stage) => stage.id);\n\tfor (let index = 0; index < oldIds.length; index += 1) {\n\t\tif (nextIds[index] !== oldIds[index]) {\n\t\t\tthrow new Error(\n\t\t\t\t`stage sequence changed at position ${index} (expected ${oldIds[index]}, found ${nextIds[index] ?? \"end\"}): ` +\n\t\t\t\t\t\"stages may only be appended; inserting, reordering, changing ids, and removing stages are not allowed \" +\n\t\t\t\t\t\"(drop a stage with workflow_advance skip instead)\",\n\t\t\t);\n\t\t}\n\t}\n\tconst nextById = new Map(next.stages.map((stage) => [stage.id, stage]));\n\tconst amendChanges: StageChangePlan[] = [];\n\tfor (const previous of existing.stages) {\n\t\tconst updated = nextById.get(previous.id);\n\t\tif (updated === undefined) continue; // covered by the sequence check above\n\t\tconst runtime = existing.runtime.find((item) => item.stageId === previous.id);\n\t\tconst status = runtime?.status ?? \"pending\";\n\t\tconst fields = changedStageFields(previous, updated);\n\t\tif (fields.length === 0) continue;\n\t\tif (status === \"completed\" || status === \"skipped\") {\n\t\t\tthrow new Error(\n\t\t\t\t`stage ${previous.id} is ${status} and its spec is frozen (history must stay interpretable); ` +\n\t\t\t\t\t\"use workflow_advance retreat or append a corrective stage instead\",\n\t\t\t);\n\t\t}\n\t\tif (status === \"pending\") continue; // free modification (§6)\n\t\t// active / awaiting_review / failed: amend path (§6). `failed` is unreachable\n\t\t// in a non-terminal workflow (failure disposal resets to pending) but graded\n\t\t// as amend defensively.\n\t\tconst frozen = fields.filter((name) => FROZEN_STAGE_FIELDS.includes(name));\n\t\tif (frozen.length > 0) {\n\t\t\tthrow new Error(\n\t\t\t\t`stage ${previous.id} is ${status}; only objective/acceptance/scope/role/planId may be amended ` +\n\t\t\t\t\t`(changed frozen fields: ${frozen.join(\", \")})`,\n\t\t\t);\n\t\t}\n\t\tconst amendNote = updated.amendNote ?? \"\";\n\t\tif (amendNote.trim() === \"\") {\n\t\t\tthrow new Error(`stage ${previous.id} is ${status} and changed; amendNote must carry the correction reason`);\n\t\t}\n\t\tconst prunedEvidence = previous.objective.acceptance\n\t\t\t.filter(\n\t\t\t\t(item) =>\n\t\t\t\t\t!updated.objective.acceptance.some((candidate) => candidate.id === item.id) &&\n\t\t\t\t\t(runtime?.evidence.some((entry) => entry.acceptanceId === item.id) ?? false),\n\t\t\t)\n\t\t\t.map((item) => item.id);\n\t\tamendChanges.push({ stageId: previous.id, fields, amendNote, prunedEvidence });\n\t}\n\treturn { created: false, amendChanges, appendedStageIds: nextIds.slice(oldIds.length) };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ai-agent-forge/plugin-workflow",
3
+ "version": "0.85.20",
4
+ "description": "Workflow orchestration capability plugin for Agent Forge (stage DAG state machine over session entries)",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "agent-forge.json",
17
+ "plugin.json",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "clean": "shx rm -rf dist",
22
+ "build": "tsgo -p tsconfig.build.json",
23
+ "test": "vitest --run",
24
+ "prepublishOnly": "npm run clean && npm run build"
25
+ },
26
+ "keywords": [
27
+ "agent-forge",
28
+ "plugin",
29
+ "workflow"
30
+ ],
31
+ "author": "Agent Forge contributors",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/agent-forge/agent-forge.git",
36
+ "directory": "plugins/workflow"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/agent-forge/agent-forge/issues"
40
+ },
41
+ "homepage": "https://github.com/agent-forge/agent-forge#readme",
42
+ "engines": {
43
+ "node": ">=22.19.0"
44
+ },
45
+ "dependencies": {
46
+ "typebox": "1.3.27",
47
+ "@ai-agent-forge/plugin-sdk": "^0.85.20"
48
+ },
49
+ "devDependencies": {
50
+ "shx": "0.4.0",
51
+ "vitest": "4.1.9",
52
+ "@ai-agent-forge/agent-forge": "^0.85.20"
53
+ }
54
+ }
package/plugin.json ADDED
@@ -0,0 +1,12 @@
1
+ {
2
+ "id": "agent-forge.plugin.workflow",
3
+ "version": "0.85.20",
4
+ "apiVersion": "1-draft",
5
+ "entry": "./dist/entry.js",
6
+ "hostVersion": ">=0.84.4",
7
+ "requiredCapabilities": [
8
+ "session",
9
+ "events",
10
+ "tools"
11
+ ]
12
+ }