@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.
package/dist/render.js ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * /workflow command text rendering (§6 text face, §9 L1/L3): panorama
3
+ * (indented dependency tree + per-stage status + waiting conditions +
4
+ * parallel active count + retreat count + active diagnostics), why (one line
5
+ * per waiting stage), and log (transition stream, newest first, reason codes
6
+ * and amend records included). Plain text only — every host shape can render
7
+ * it, no TUI required.
8
+ */
9
+ import { computeWaitingOn, maxActiveOf, maxRetreatsOf } from "./engine.js";
10
+ import { WORKFLOW_TRANSITION_ENTRY, } from "./types.js";
11
+ const runtimeOf = (workflow, stageId) => workflow.runtime.find((item) => item.stageId === stageId);
12
+ function renderStageNode(workflow, spec, depth, deps, lines, visited) {
13
+ const pad = " ".repeat(depth + 1);
14
+ if (visited.has(spec.id)) {
15
+ lines.push(`${pad}-> ${spec.id} (shown above)`);
16
+ return;
17
+ }
18
+ visited.add(spec.id);
19
+ const runtime = runtimeOf(workflow, spec.id);
20
+ const bits = [`- ${spec.id} [${runtime?.status ?? "pending"}] ${spec.title}`];
21
+ if (runtime !== undefined) {
22
+ const waiting = computeWaitingOn(workflow, spec, runtime, deps);
23
+ if (waiting !== null)
24
+ bits.push(`— waiting: ${waiting}`);
25
+ if (runtime.attempts > 0)
26
+ bits.push(`· attempts ${runtime.attempts}`);
27
+ if (runtime.evidence.length > 0)
28
+ bits.push(`· evidence ${runtime.evidence.length}`);
29
+ }
30
+ lines.push(`${pad}${bits.join(" ")}`);
31
+ // DAG tree: children are dependents in declaration order; diamond edges
32
+ // collapse into a back-reference line instead of duplicating the subtree.
33
+ for (const child of workflow.stages.filter((item) => item.dependsOn.includes(spec.id))) {
34
+ renderStageNode(workflow, child, depth + 1, deps, lines, visited);
35
+ }
36
+ }
37
+ /** Panorama: one block per workflow. */
38
+ export function renderPanorama(state, deps) {
39
+ if (state.workflows.length === 0) {
40
+ return "no workflows in this session; create one with workflow_set";
41
+ }
42
+ return state.workflows
43
+ .map((workflow) => {
44
+ const lines = [];
45
+ const active = workflow.runtime.filter((item) => item.status === "active").length;
46
+ lines.push(`workflow "${workflow.title}" (${workflow.id}) — ${workflow.status} · revision ${workflow.revision} · active ${active}/${maxActiveOf(workflow)} · retreats ${workflow.retreatCount}/${maxRetreatsOf(workflow)}`);
47
+ if (workflow.goalId !== undefined)
48
+ lines.push(` goal: ${workflow.goalId}`);
49
+ const visited = new Set();
50
+ for (const spec of workflow.stages.filter((item) => item.dependsOn.length === 0)) {
51
+ renderStageNode(workflow, spec, 0, deps, lines, visited);
52
+ }
53
+ // Stages unreachable from a root (defensive: cannot happen on a
54
+ // validated acyclic graph) still render, top-level.
55
+ for (const spec of workflow.stages) {
56
+ if (!visited.has(spec.id))
57
+ renderStageNode(workflow, spec, 0, deps, lines, visited);
58
+ }
59
+ if (workflow.diagnostics.length > 0) {
60
+ lines.push(" diagnostics:");
61
+ for (const diagnostic of workflow.diagnostics)
62
+ lines.push(` - ${diagnostic}`);
63
+ }
64
+ return lines.join("\n");
65
+ })
66
+ .join("\n\n");
67
+ }
68
+ /** Why it is stopped: every stage with an unmet wait condition, one line each (§6 /workflow why). */
69
+ export function renderWhy(state, deps) {
70
+ const pending = state.workflows.filter((workflow) => workflow.status !== "completed" && workflow.status !== "cancelled");
71
+ if (pending.length === 0)
72
+ return "no open workflows; nothing is waiting";
73
+ return pending
74
+ .map((workflow) => {
75
+ const lines = [`workflow ${workflow.id} — ${workflow.status}`];
76
+ let any = false;
77
+ for (const spec of workflow.stages) {
78
+ const runtime = runtimeOf(workflow, spec.id);
79
+ if (runtime === undefined)
80
+ continue;
81
+ const waiting = computeWaitingOn(workflow, spec, runtime, deps);
82
+ if (waiting === null)
83
+ continue;
84
+ lines.push(` - ${spec.id} [${runtime.status}] waiting: ${waiting}`);
85
+ any = true;
86
+ }
87
+ if (!any)
88
+ lines.push(" no stage is waiting on an unmet condition");
89
+ for (const diagnostic of workflow.diagnostics)
90
+ lines.push(` ! ${diagnostic}`);
91
+ return lines.join("\n");
92
+ })
93
+ .join("\n\n");
94
+ }
95
+ function isTransitionEntry(data) {
96
+ if (!data || typeof data !== "object" || Array.isArray(data))
97
+ return false;
98
+ const record = data;
99
+ return (typeof record.workflowId === "string" &&
100
+ typeof record.from === "string" &&
101
+ typeof record.to === "string" &&
102
+ typeof record.revision === "number" &&
103
+ record.reason !== undefined &&
104
+ typeof record.reason === "object" &&
105
+ typeof record.reason.code === "string");
106
+ }
107
+ /** Transition stream, newest first (§8.1 "which conversion went wrong"): revision anchor + trigger + reason code. */
108
+ export function renderLog(entries, limit) {
109
+ const transitions = [];
110
+ for (const entry of entries) {
111
+ if (entry.customType !== WORKFLOW_TRANSITION_ENTRY)
112
+ continue;
113
+ if (isTransitionEntry(entry.data))
114
+ transitions.push(entry.data);
115
+ }
116
+ if (transitions.length === 0)
117
+ return "no workflow transitions recorded";
118
+ return transitions
119
+ .slice(-limit)
120
+ .reverse()
121
+ .map((transition) => {
122
+ const stage = transition.stageId !== undefined ? `/${transition.stageId}` : "";
123
+ const detail = transition.reason.detail !== undefined ? ` — ${transition.reason.detail}` : "";
124
+ return `#${transition.revision} ${transition.workflowId}${stage} ${transition.from} -> ${transition.to} [${transition.trigger}] ${transition.reason.code}: ${transition.reason.message}${detail}`;
125
+ })
126
+ .join("\n");
127
+ }
128
+ //# sourceMappingURL=render.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * /workflow command text rendering (§6 text face, §9 L1/L3): panorama\n * (indented dependency tree + per-stage status + waiting conditions +\n * parallel active count + retreat count + active diagnostics), why (one line\n * per waiting stage), and log (transition stream, newest first, reason codes\n * and amend records included). Plain text only — every host shape can render\n * it, no TUI required.\n */\n\nimport { computeWaitingOn, type EngineDeps, maxActiveOf, maxRetreatsOf } from \"./engine.ts\";\nimport {\n\ttype StageRuntime,\n\ttype StageSpec,\n\tWORKFLOW_TRANSITION_ENTRY,\n\ttype WorkflowRecord,\n\ttype WorkflowState,\n\ttype WorkflowTransitionEntry,\n} from \"./types.ts\";\n\nconst runtimeOf = (workflow: WorkflowRecord, stageId: string): StageRuntime | undefined =>\n\tworkflow.runtime.find((item) => item.stageId === stageId);\n\nfunction renderStageNode(\n\tworkflow: WorkflowRecord,\n\tspec: StageSpec,\n\tdepth: number,\n\tdeps: EngineDeps,\n\tlines: string[],\n\tvisited: Set<string>,\n): void {\n\tconst pad = \" \".repeat(depth + 1);\n\tif (visited.has(spec.id)) {\n\t\tlines.push(`${pad}-> ${spec.id} (shown above)`);\n\t\treturn;\n\t}\n\tvisited.add(spec.id);\n\tconst runtime = runtimeOf(workflow, spec.id);\n\tconst bits = [`- ${spec.id} [${runtime?.status ?? \"pending\"}] ${spec.title}`];\n\tif (runtime !== undefined) {\n\t\tconst waiting = computeWaitingOn(workflow, spec, runtime, deps);\n\t\tif (waiting !== null) bits.push(`— waiting: ${waiting}`);\n\t\tif (runtime.attempts > 0) bits.push(`· attempts ${runtime.attempts}`);\n\t\tif (runtime.evidence.length > 0) bits.push(`· evidence ${runtime.evidence.length}`);\n\t}\n\tlines.push(`${pad}${bits.join(\" \")}`);\n\t// DAG tree: children are dependents in declaration order; diamond edges\n\t// collapse into a back-reference line instead of duplicating the subtree.\n\tfor (const child of workflow.stages.filter((item) => item.dependsOn.includes(spec.id))) {\n\t\trenderStageNode(workflow, child, depth + 1, deps, lines, visited);\n\t}\n}\n\n/** Panorama: one block per workflow. */\nexport function renderPanorama(state: WorkflowState, deps: EngineDeps): string {\n\tif (state.workflows.length === 0) {\n\t\treturn \"no workflows in this session; create one with workflow_set\";\n\t}\n\treturn state.workflows\n\t\t.map((workflow) => {\n\t\t\tconst lines: string[] = [];\n\t\t\tconst active = workflow.runtime.filter((item) => item.status === \"active\").length;\n\t\t\tlines.push(\n\t\t\t\t`workflow \"${workflow.title}\" (${workflow.id}) — ${workflow.status} · revision ${workflow.revision} · active ${active}/${maxActiveOf(workflow)} · retreats ${workflow.retreatCount}/${maxRetreatsOf(workflow)}`,\n\t\t\t);\n\t\t\tif (workflow.goalId !== undefined) lines.push(` goal: ${workflow.goalId}`);\n\t\t\tconst visited = new Set<string>();\n\t\t\tfor (const spec of workflow.stages.filter((item) => item.dependsOn.length === 0)) {\n\t\t\t\trenderStageNode(workflow, spec, 0, deps, lines, visited);\n\t\t\t}\n\t\t\t// Stages unreachable from a root (defensive: cannot happen on a\n\t\t\t// validated acyclic graph) still render, top-level.\n\t\t\tfor (const spec of workflow.stages) {\n\t\t\t\tif (!visited.has(spec.id)) renderStageNode(workflow, spec, 0, deps, lines, visited);\n\t\t\t}\n\t\t\tif (workflow.diagnostics.length > 0) {\n\t\t\t\tlines.push(\" diagnostics:\");\n\t\t\t\tfor (const diagnostic of workflow.diagnostics) lines.push(` - ${diagnostic}`);\n\t\t\t}\n\t\t\treturn lines.join(\"\\n\");\n\t\t})\n\t\t.join(\"\\n\\n\");\n}\n\n/** Why it is stopped: every stage with an unmet wait condition, one line each (§6 /workflow why). */\nexport function renderWhy(state: WorkflowState, deps: EngineDeps): string {\n\tconst pending = state.workflows.filter(\n\t\t(workflow) => workflow.status !== \"completed\" && workflow.status !== \"cancelled\",\n\t);\n\tif (pending.length === 0) return \"no open workflows; nothing is waiting\";\n\treturn pending\n\t\t.map((workflow) => {\n\t\t\tconst lines = [`workflow ${workflow.id} — ${workflow.status}`];\n\t\t\tlet any = false;\n\t\t\tfor (const spec of workflow.stages) {\n\t\t\t\tconst runtime = runtimeOf(workflow, spec.id);\n\t\t\t\tif (runtime === undefined) continue;\n\t\t\t\tconst waiting = computeWaitingOn(workflow, spec, runtime, deps);\n\t\t\t\tif (waiting === null) continue;\n\t\t\t\tlines.push(` - ${spec.id} [${runtime.status}] waiting: ${waiting}`);\n\t\t\t\tany = true;\n\t\t\t}\n\t\t\tif (!any) lines.push(\" no stage is waiting on an unmet condition\");\n\t\t\tfor (const diagnostic of workflow.diagnostics) lines.push(` ! ${diagnostic}`);\n\t\t\treturn lines.join(\"\\n\");\n\t\t})\n\t\t.join(\"\\n\\n\");\n}\n\nfunction isTransitionEntry(data: unknown): data is WorkflowTransitionEntry {\n\tif (!data || typeof data !== \"object\" || Array.isArray(data)) return false;\n\tconst record = data as Partial<WorkflowTransitionEntry>;\n\treturn (\n\t\ttypeof record.workflowId === \"string\" &&\n\t\ttypeof record.from === \"string\" &&\n\t\ttypeof record.to === \"string\" &&\n\t\ttypeof record.revision === \"number\" &&\n\t\trecord.reason !== undefined &&\n\t\ttypeof record.reason === \"object\" &&\n\t\ttypeof (record.reason as { code?: unknown }).code === \"string\"\n\t);\n}\n\n/** Transition stream, newest first (§8.1 \"which conversion went wrong\"): revision anchor + trigger + reason code. */\nexport function renderLog(entries: readonly { customType?: string; data?: unknown }[], limit: number): string {\n\tconst transitions: WorkflowTransitionEntry[] = [];\n\tfor (const entry of entries) {\n\t\tif (entry.customType !== WORKFLOW_TRANSITION_ENTRY) continue;\n\t\tif (isTransitionEntry(entry.data)) transitions.push(entry.data);\n\t}\n\tif (transitions.length === 0) return \"no workflow transitions recorded\";\n\treturn transitions\n\t\t.slice(-limit)\n\t\t.reverse()\n\t\t.map((transition) => {\n\t\t\tconst stage = transition.stageId !== undefined ? `/${transition.stageId}` : \"\";\n\t\t\tconst detail = transition.reason.detail !== undefined ? ` — ${transition.reason.detail}` : \"\";\n\t\t\treturn `#${transition.revision} ${transition.workflowId}${stage} ${transition.from} -> ${transition.to} [${transition.trigger}] ${transition.reason.code}: ${transition.reason.message}${detail}`;\n\t\t})\n\t\t.join(\"\\n\");\n}\n"]}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Workflow data model — the verbatim §4 contract of
3
+ * `docs/design/工作流编排插件设计.md` plus the closed reason-code catalog
4
+ * (§8.2) and the session entry keys shared across modules.
5
+ *
6
+ * Persisted as append-only custom entries: `workflow.state` (full snapshot,
7
+ * the single rebuild source) and `workflow.transition` (per-conversion audit
8
+ * stream; §8's primary data source, never used for state rebuild).
9
+ */
10
+ /** Session protocol key of this plugin's full-state snapshot entry. */
11
+ export declare const WORKFLOW_STATE_ENTRY = "workflow.state";
12
+ /** Session protocol key of this plugin's per-conversion audit entry. */
13
+ export declare const WORKFLOW_TRANSITION_ENTRY = "workflow.transition";
14
+ export interface WorkflowState {
15
+ workflows: WorkflowRecord[];
16
+ }
17
+ export interface WorkflowRecord {
18
+ id: string;
19
+ title: string;
20
+ goalId?: string;
21
+ status: "draft" | "running" | "completed" | "failed" | "cancelled";
22
+ /** Caller-supplied on workflow_set (monotonic); engine conversions auto-increment (§5 rule 8). */
23
+ revision: number;
24
+ stages: StageSpec[];
25
+ runtime: StageRuntime[];
26
+ retreatCount: number;
27
+ maxRetreats?: number;
28
+ maxActiveStages?: number;
29
+ /** Current alerts, FIFO capped at 50; full history lives in workflow.transition. */
30
+ diagnostics: string[];
31
+ }
32
+ export interface StageSpec {
33
+ id: string;
34
+ title: string;
35
+ objective: {
36
+ /** The verifiable end state this stage must reach (required, non-empty). */
37
+ statement: string;
38
+ /** Completion criteria; empty array = lower bound is the plan's own completion evidence. */
39
+ acceptance: {
40
+ id: string;
41
+ statement: string;
42
+ }[];
43
+ };
44
+ dependsOn: string[];
45
+ planId?: string;
46
+ kind: "read" | "write";
47
+ /** Write scope tags (modules/dirs); missing = global (intersects every other write stage). */
48
+ scope?: string[];
49
+ role?: string;
50
+ advance: "auto" | "review";
51
+ retryLimit?: number;
52
+ onFail?: string;
53
+ /** Required correction reason when an active/awaiting_review stage's spec changes (§6 amend). */
54
+ amendNote?: string;
55
+ }
56
+ export interface StageRuntime {
57
+ stageId: string;
58
+ status: "pending" | "active" | "awaiting_review" | "completed" | "failed" | "skipped";
59
+ /** Failure-retry count; preserved across in-place retry and retreat re-activation (§4). */
60
+ attempts: number;
61
+ /** Appended via record-evidence / approve / complete; coverage is derived at evaluation time. */
62
+ evidence: {
63
+ acceptanceId: string;
64
+ ref: string;
65
+ }[];
66
+ }
67
+ /** Audit stream: exactly one entry per state conversion, including held/rejected attempts (§5 总则 5). */
68
+ export interface WorkflowTransitionEntry {
69
+ workflowId: string;
70
+ stageId?: string;
71
+ from: string;
72
+ to: string;
73
+ trigger: WorkflowTransitionTrigger;
74
+ reason: TransitionReason;
75
+ /** Anchors the adjacent workflow.state snapshot (§4). */
76
+ revision: number;
77
+ }
78
+ export type WorkflowTransitionTrigger = "tool" | "plan.changed" | "load-reeval" | "activation-reeval";
79
+ export interface TransitionReason {
80
+ code: TransitionReasonCode;
81
+ message: string;
82
+ detail?: string;
83
+ }
84
+ /**
85
+ * Closed reason-code catalog (§8.2). Tests must cover every code; do not add
86
+ * codes without amending the design document first.
87
+ */
88
+ export type TransitionReasonCode = "stage.activated" | "stage.activated.held-plan-dangling" | "stage.held.deps-incomplete" | "stage.held.write-scope-conflict" | "stage.held.active-limit" | "stage.awaiting-review" | "stage.completed" | "stage.completed.acceptance-covered" | "stage.skipped" | "evidence.recorded" | "stage.failed.acceptance-missing" | "stage.failed.plan-cancelled" | "stage.retry.queued" | "stage.retreated" | "workflow.failed.stage-retries" | "workflow.failed.retreat-budget" | "workflow.started" | "workflow.completed" | "workflow.cancelled" | "stage.spec-amended" | "cascade.limit-reached" | "handler.error";
89
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Workflow data model — the verbatim §4 contract of\n * `docs/design/工作流编排插件设计.md` plus the closed reason-code catalog\n * (§8.2) and the session entry keys shared across modules.\n *\n * Persisted as append-only custom entries: `workflow.state` (full snapshot,\n * the single rebuild source) and `workflow.transition` (per-conversion audit\n * stream; §8's primary data source, never used for state rebuild).\n */\n\n/** Session protocol key of this plugin's full-state snapshot entry. */\nexport const WORKFLOW_STATE_ENTRY = \"workflow.state\";\n/** Session protocol key of this plugin's per-conversion audit entry. */\nexport const WORKFLOW_TRANSITION_ENTRY = \"workflow.transition\";\n\nexport interface WorkflowState {\n\tworkflows: WorkflowRecord[];\n}\n\nexport interface WorkflowRecord {\n\tid: string;\n\ttitle: string;\n\tgoalId?: string; // soft reference to goal.state\n\tstatus: \"draft\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\t/** Caller-supplied on workflow_set (monotonic); engine conversions auto-increment (§5 rule 8). */\n\trevision: number;\n\tstages: StageSpec[];\n\truntime: StageRuntime[];\n\tretreatCount: number; // total cross-stage retreats over the workflow lifetime\n\tmaxRetreats?: number; // default 3; reaching the cap -> workflow failed\n\tmaxActiveStages?: number; // parallel activation cap, default 4\n\t/** Current alerts, FIFO capped at 50; full history lives in workflow.transition. */\n\tdiagnostics: string[];\n}\n\nexport interface StageSpec {\n\tid: string; // stable stage id, unique within the workflow\n\ttitle: string;\n\tobjective: {\n\t\t/** The verifiable end state this stage must reach (required, non-empty). */\n\t\tstatement: string;\n\t\t/** Completion criteria; empty array = lower bound is the plan's own completion evidence. */\n\t\tacceptance: { id: string; statement: string }[];\n\t};\n\tdependsOn: string[]; // hard prerequisites; empty array = root stage\n\tplanId?: string; // soft reference to plan.state; missing = pure checkpoint stage; unique per workflow\n\tkind: \"read\" | \"write\"; // default \"write\"\n\t/** Write scope tags (modules/dirs); missing = global (intersects every other write stage). */\n\tscope?: string[];\n\trole?: string; // soft reference to <agentDir>/roles/<id>.json; delegation advice only\n\tadvance: \"auto\" | \"review\"; // default \"auto\"; \"review\" only affects stages with a planId\n\tretryLimit?: number; // in-place retry cap, default 2; exhaust routes to onFail or workflow failure\n\tonFail?: string; // explicit cross-stage retreat target (a topologically earlier stage id)\n\t/** Required correction reason when an active/awaiting_review stage's spec changes (§6 amend). */\n\tamendNote?: string;\n}\n\nexport interface StageRuntime {\n\tstageId: string;\n\tstatus: \"pending\" | \"active\" | \"awaiting_review\" | \"completed\" | \"failed\" | \"skipped\";\n\t/** Failure-retry count; preserved across in-place retry and retreat re-activation (§4). */\n\tattempts: number;\n\t/** Appended via record-evidence / approve / complete; coverage is derived at evaluation time. */\n\tevidence: { acceptanceId: string; ref: string }[];\n}\n\n/** Audit stream: exactly one entry per state conversion, including held/rejected attempts (§5 总则 5). */\nexport interface WorkflowTransitionEntry {\n\tworkflowId: string;\n\tstageId?: string; // absent on workflow-level conversions (start/closure/burnout)\n\tfrom: string;\n\tto: string;\n\ttrigger: WorkflowTransitionTrigger;\n\treason: TransitionReason; // codes: §8.2 closed catalog\n\t/** Anchors the adjacent workflow.state snapshot (§4). */\n\trevision: number;\n}\n\nexport type WorkflowTransitionTrigger = \"tool\" | \"plan.changed\" | \"load-reeval\" | \"activation-reeval\";\n\nexport interface TransitionReason {\n\tcode: TransitionReasonCode;\n\tmessage: string;\n\tdetail?: string;\n}\n\n/**\n * Closed reason-code catalog (§8.2). Tests must cover every code; do not add\n * codes without amending the design document first.\n */\nexport type TransitionReasonCode =\n\t// Activation and unlocking\n\t| \"stage.activated\"\n\t| \"stage.activated.held-plan-dangling\"\n\t| \"stage.held.deps-incomplete\"\n\t| \"stage.held.write-scope-conflict\"\n\t| \"stage.held.active-limit\"\n\t| \"stage.awaiting-review\"\n\t// Completion\n\t| \"stage.completed\"\n\t| \"stage.completed.acceptance-covered\"\n\t| \"stage.skipped\"\n\t| \"evidence.recorded\"\n\t// Failure and retreat\n\t| \"stage.failed.acceptance-missing\"\n\t| \"stage.failed.plan-cancelled\"\n\t| \"stage.retry.queued\"\n\t| \"stage.retreated\"\n\t| \"workflow.failed.stage-retries\"\n\t| \"workflow.failed.retreat-budget\"\n\t// Workflow level\n\t| \"workflow.started\"\n\t| \"workflow.completed\"\n\t| \"workflow.cancelled\"\n\t// Human-in-the-loop correction\n\t| \"stage.spec-amended\"\n\t// Engine itself\n\t| \"cascade.limit-reached\"\n\t| \"handler.error\";\n"]}
package/dist/types.js ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Workflow data model — the verbatim §4 contract of
3
+ * `docs/design/工作流编排插件设计.md` plus the closed reason-code catalog
4
+ * (§8.2) and the session entry keys shared across modules.
5
+ *
6
+ * Persisted as append-only custom entries: `workflow.state` (full snapshot,
7
+ * the single rebuild source) and `workflow.transition` (per-conversion audit
8
+ * stream; §8's primary data source, never used for state rebuild).
9
+ */
10
+ /** Session protocol key of this plugin's full-state snapshot entry. */
11
+ export const WORKFLOW_STATE_ENTRY = "workflow.state";
12
+ /** Session protocol key of this plugin's per-conversion audit entry. */
13
+ export const WORKFLOW_TRANSITION_ENTRY = "workflow.transition";
14
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AACrD,wEAAwE;AACxE,MAAM,CAAC,MAAM,yBAAyB,GAAG,qBAAqB,CAAC","sourcesContent":["/**\n * Workflow data model — the verbatim §4 contract of\n * `docs/design/工作流编排插件设计.md` plus the closed reason-code catalog\n * (§8.2) and the session entry keys shared across modules.\n *\n * Persisted as append-only custom entries: `workflow.state` (full snapshot,\n * the single rebuild source) and `workflow.transition` (per-conversion audit\n * stream; §8's primary data source, never used for state rebuild).\n */\n\n/** Session protocol key of this plugin's full-state snapshot entry. */\nexport const WORKFLOW_STATE_ENTRY = \"workflow.state\";\n/** Session protocol key of this plugin's per-conversion audit entry. */\nexport const WORKFLOW_TRANSITION_ENTRY = \"workflow.transition\";\n\nexport interface WorkflowState {\n\tworkflows: WorkflowRecord[];\n}\n\nexport interface WorkflowRecord {\n\tid: string;\n\ttitle: string;\n\tgoalId?: string; // soft reference to goal.state\n\tstatus: \"draft\" | \"running\" | \"completed\" | \"failed\" | \"cancelled\";\n\t/** Caller-supplied on workflow_set (monotonic); engine conversions auto-increment (§5 rule 8). */\n\trevision: number;\n\tstages: StageSpec[];\n\truntime: StageRuntime[];\n\tretreatCount: number; // total cross-stage retreats over the workflow lifetime\n\tmaxRetreats?: number; // default 3; reaching the cap -> workflow failed\n\tmaxActiveStages?: number; // parallel activation cap, default 4\n\t/** Current alerts, FIFO capped at 50; full history lives in workflow.transition. */\n\tdiagnostics: string[];\n}\n\nexport interface StageSpec {\n\tid: string; // stable stage id, unique within the workflow\n\ttitle: string;\n\tobjective: {\n\t\t/** The verifiable end state this stage must reach (required, non-empty). */\n\t\tstatement: string;\n\t\t/** Completion criteria; empty array = lower bound is the plan's own completion evidence. */\n\t\tacceptance: { id: string; statement: string }[];\n\t};\n\tdependsOn: string[]; // hard prerequisites; empty array = root stage\n\tplanId?: string; // soft reference to plan.state; missing = pure checkpoint stage; unique per workflow\n\tkind: \"read\" | \"write\"; // default \"write\"\n\t/** Write scope tags (modules/dirs); missing = global (intersects every other write stage). */\n\tscope?: string[];\n\trole?: string; // soft reference to <agentDir>/roles/<id>.json; delegation advice only\n\tadvance: \"auto\" | \"review\"; // default \"auto\"; \"review\" only affects stages with a planId\n\tretryLimit?: number; // in-place retry cap, default 2; exhaust routes to onFail or workflow failure\n\tonFail?: string; // explicit cross-stage retreat target (a topologically earlier stage id)\n\t/** Required correction reason when an active/awaiting_review stage's spec changes (§6 amend). */\n\tamendNote?: string;\n}\n\nexport interface StageRuntime {\n\tstageId: string;\n\tstatus: \"pending\" | \"active\" | \"awaiting_review\" | \"completed\" | \"failed\" | \"skipped\";\n\t/** Failure-retry count; preserved across in-place retry and retreat re-activation (§4). */\n\tattempts: number;\n\t/** Appended via record-evidence / approve / complete; coverage is derived at evaluation time. */\n\tevidence: { acceptanceId: string; ref: string }[];\n}\n\n/** Audit stream: exactly one entry per state conversion, including held/rejected attempts (§5 总则 5). */\nexport interface WorkflowTransitionEntry {\n\tworkflowId: string;\n\tstageId?: string; // absent on workflow-level conversions (start/closure/burnout)\n\tfrom: string;\n\tto: string;\n\ttrigger: WorkflowTransitionTrigger;\n\treason: TransitionReason; // codes: §8.2 closed catalog\n\t/** Anchors the adjacent workflow.state snapshot (§4). */\n\trevision: number;\n}\n\nexport type WorkflowTransitionTrigger = \"tool\" | \"plan.changed\" | \"load-reeval\" | \"activation-reeval\";\n\nexport interface TransitionReason {\n\tcode: TransitionReasonCode;\n\tmessage: string;\n\tdetail?: string;\n}\n\n/**\n * Closed reason-code catalog (§8.2). Tests must cover every code; do not add\n * codes without amending the design document first.\n */\nexport type TransitionReasonCode =\n\t// Activation and unlocking\n\t| \"stage.activated\"\n\t| \"stage.activated.held-plan-dangling\"\n\t| \"stage.held.deps-incomplete\"\n\t| \"stage.held.write-scope-conflict\"\n\t| \"stage.held.active-limit\"\n\t| \"stage.awaiting-review\"\n\t// Completion\n\t| \"stage.completed\"\n\t| \"stage.completed.acceptance-covered\"\n\t| \"stage.skipped\"\n\t| \"evidence.recorded\"\n\t// Failure and retreat\n\t| \"stage.failed.acceptance-missing\"\n\t| \"stage.failed.plan-cancelled\"\n\t| \"stage.retry.queued\"\n\t| \"stage.retreated\"\n\t| \"workflow.failed.stage-retries\"\n\t| \"workflow.failed.retreat-budget\"\n\t// Workflow level\n\t| \"workflow.started\"\n\t| \"workflow.completed\"\n\t| \"workflow.cancelled\"\n\t// Human-in-the-loop correction\n\t| \"stage.spec-amended\"\n\t// Engine itself\n\t| \"cascade.limit-reached\"\n\t| \"handler.error\";\n"]}
@@ -0,0 +1,60 @@
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
+ import type { StageSpec, WorkflowRecord } from "./types.ts";
12
+ export interface ParsedWorkflowSet {
13
+ id: string;
14
+ title: string;
15
+ goalId?: string;
16
+ revision: number;
17
+ maxRetreats?: number;
18
+ maxActiveStages?: number;
19
+ stages: StageSpec[];
20
+ }
21
+ /** One active/awaiting_review stage that changed spec and must leave a stage.spec-amended audit (§6). */
22
+ export interface StageChangePlan {
23
+ stageId: string;
24
+ /** Amendable field names that differ from the stored spec. */
25
+ fields: string[];
26
+ /** Correction reason; required non-empty on the amend path. */
27
+ amendNote: string;
28
+ /** Acceptance ids removed by the amend that still had evidence attached (linkage detail, §6). */
29
+ prunedEvidence: string[];
30
+ }
31
+ export interface WorkflowSetDiff {
32
+ created: boolean;
33
+ /** Amend-path changes (one stage.spec-amended transition each). */
34
+ amendChanges: StageChangePlan[];
35
+ /** Stage ids appended after the existing stage sequence (append-only structure ops, §6). */
36
+ appendedStageIds: string[];
37
+ }
38
+ export declare function asObject(value: unknown, name: string): Record<string, unknown>;
39
+ export declare function asText(value: unknown, name: string): string;
40
+ export declare function parseWorkflowSetInput(raw: unknown): ParsedWorkflowSet;
41
+ /** Whether `targetId` is transitively reachable from `startId` along dependsOn edges. */
42
+ export declare function dependsOnReach(stages: StageSpec[], startId: string, targetId: string): boolean;
43
+ /**
44
+ * Full structural validation: stage id uniqueness, reference existence,
45
+ * workflow-internal planId uniqueness (cross-workflow sharing is allowed),
46
+ * onFail pointing topologically earlier, and combined dependsOn+onFail cycle
47
+ * detection (DFS over the mixed edge set — §6 names mixed cycles explicitly).
48
+ */
49
+ export declare function validateStageGraph(stages: StageSpec[]): void;
50
+ /**
51
+ * Grades a full-submit update against the stored record: append-only stage
52
+ * sequence (no mid-insert, reorder, id change, or removal — removal is `skip`
53
+ * by design), pending stages freely modifiable, active/awaiting_review stages
54
+ * amendable only within the §6 field list and only with a non-empty
55
+ * amendNote, completed/skipped specs frozen. Throws structured errors on
56
+ * violation; completed-wins race semantics (§6: amend vs completion
57
+ * evaluation) fall out naturally because a completed stage is frozen.
58
+ */
59
+ export declare function planWorkflowSpecUpdate(existing: WorkflowRecord | undefined, next: ParsedWorkflowSet): WorkflowSetDiff;
60
+ //# sourceMappingURL=validate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../src/validate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAE5D,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,0GAAyG;AACzG,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,+DAA+D;IAC/D,SAAS,EAAE,MAAM,CAAC;IAClB,kGAAiG;IACjG,cAAc,EAAE,MAAM,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,YAAY,EAAE,eAAe,EAAE,CAAC;IAChC,6FAA4F;IAC5F,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC3B;AAMD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAG9E;AAED,wBAAgB,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG3D;AA0FD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,iBAAiB,CAiBrE;AAMD,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAa9F;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAuD5D;AAsCD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,cAAc,GAAG,SAAS,EAAE,IAAI,EAAE,iBAAiB,GAAG,eAAe,CAuDrH","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"]}