@surlymochan/evalid 0.0.0-stage → 0.4.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.
Files changed (192) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +124 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7261 -0
  7. package/docs/adoption/README.md +128 -0
  8. package/docs/adoption/openflow-release-hardening.md +61 -0
  9. package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
  10. package/docs/compatibility.md +52 -0
  11. package/docs/competitive-benchmark.md +108 -0
  12. package/docs/corps-operator-guide.md +75 -0
  13. package/docs/demo-proof.md +126 -0
  14. package/docs/examples-gallery.md +115 -0
  15. package/docs/fixtures/tracker-item.json +7 -0
  16. package/docs/goal-vs-codex.md +129 -0
  17. package/docs/gstack-comparison.md +88 -0
  18. package/docs/install-upgrade.md +133 -0
  19. package/docs/integrations.md +49 -0
  20. package/docs/launch-demo.md +123 -0
  21. package/docs/launch-dossier.md +180 -0
  22. package/docs/methodology.md +149 -0
  23. package/docs/npm-publish-handoff.md +96 -0
  24. package/docs/openspec-migration.md +106 -0
  25. package/docs/public-benchmark.md +68 -0
  26. package/docs/public-release.md +145 -0
  27. package/docs/quality-assessment.md +139 -0
  28. package/docs/quickstart.md +156 -0
  29. package/docs/reviewer-guide.md +43 -0
  30. package/docs/spec-kit-benchmark.md +73 -0
  31. package/docs/super-assistant-comparison.md +106 -0
  32. package/docs/superpowers-comparison.md +82 -0
  33. package/docs/team-adoption.md +123 -0
  34. package/docs/tooling-matrix.md +91 -0
  35. package/docs/walkthrough.md +76 -0
  36. package/docs/workflow-manual.md +1130 -0
  37. package/package.json +83 -4
  38. package/prompts/pencil-draft.md +46 -0
  39. package/prompts/pencil-refine.md +46 -0
  40. package/prompts/phase-artifact.md +24 -0
  41. package/prompts/phase-execution.md +32 -0
  42. package/schemas/competitor-reconstruction-contract.schema.json +91 -0
  43. package/schemas/llm-design-review-aggregate.schema.json +83 -0
  44. package/schemas/visual-benchmark.schema.json +978 -0
  45. package/schemas/workflow.schema.json +100 -0
  46. package/scripts/run-tests.mjs +23 -0
  47. package/scripts/takein_digest.py +139 -0
  48. package/specs/README.md +118 -0
  49. package/specs/architecture.md +36 -0
  50. package/specs/product.md +33 -0
  51. package/specs/workflow.md +349 -0
  52. package/src/agent_team/atoms/codex-app-bridge.js +8 -0
  53. package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
  54. package/src/agent_team/atoms/design/contract-freeze.js +8 -0
  55. package/src/agent_team/atoms/design/generation-contract.js +442 -0
  56. package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
  57. package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
  58. package/src/agent_team/atoms/patch-challenge.js +19 -0
  59. package/src/agent_team/atoms/pencil/accept.js +9 -0
  60. package/src/agent_team/atoms/pencil/draft.js +365 -0
  61. package/src/agent_team/atoms/pencil/refine.js +229 -0
  62. package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
  63. package/src/agent_team/atoms/team-run.js +380 -0
  64. package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
  65. package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
  66. package/src/agent_team/atoms/visual/benchmark.js +1652 -0
  67. package/src/agent_team/atoms/visual/design-selection.js +23 -0
  68. package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
  69. package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
  70. package/src/agent_team/atoms/visual/image-diff.js +186 -0
  71. package/src/agent_team/atoms/visual/layout-competition.js +8 -0
  72. package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
  73. package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
  74. package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
  75. package/src/agent_team/atoms/visual/review.js +77 -0
  76. package/src/agent_team/atoms/visual/visual-report.js +197 -0
  77. package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
  78. package/src/agent_team/common.js +311 -0
  79. package/src/core/atoms/agent-invoke.js +684 -0
  80. package/src/core/atoms/event/ingest.js +30 -0
  81. package/src/core/atoms/experience/digest.js +55 -0
  82. package/src/core/atoms/gate/artifact-verify.js +83 -0
  83. package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
  84. package/src/core/atoms/gate/evidence-collect.js +64 -0
  85. package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
  86. package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
  87. package/src/core/atoms/gate/workbench.js +79 -0
  88. package/src/core/atoms/intake/classify.js +15 -0
  89. package/src/core/atoms/intake/design-skill-select.js +16 -0
  90. package/src/core/atoms/intake/preview-or-refine.js +19 -0
  91. package/src/core/atoms/intake/recommend.js +20 -0
  92. package/src/core/atoms/intake/research-same-category.js +19 -0
  93. package/src/core/atoms/intervention/create.js +30 -0
  94. package/src/core/atoms/memory/manage.js +27 -0
  95. package/src/core/atoms/memory/mount.js +13 -0
  96. package/src/core/atoms/mission/create.js +19 -0
  97. package/src/core/atoms/mission/list.js +14 -0
  98. package/src/core/atoms/mission/show.js +26 -0
  99. package/src/core/atoms/notify/checkpoint.js +46 -0
  100. package/src/core/atoms/phase-run/complete.js +42 -0
  101. package/src/core/atoms/phase-run/fail.js +40 -0
  102. package/src/core/atoms/phase-run/persist.js +39 -0
  103. package/src/core/atoms/phase-run/start.js +30 -0
  104. package/src/core/atoms/plugin/run.js +76 -0
  105. package/src/core/atoms/policy/gate.js +90 -0
  106. package/src/core/atoms/policy/resolve.js +11 -0
  107. package/src/core/atoms/storage/sync.js +57 -0
  108. package/src/core/atoms/workflow/advance.js +25 -0
  109. package/src/core/atoms/workflow/guard.js +127 -0
  110. package/src/core/atoms/workflow/state-load.js +37 -0
  111. package/src/core/bindings.js +37 -0
  112. package/src/core/change-artifacts.js +134 -0
  113. package/src/core/competitive-score.js +265 -0
  114. package/src/core/concurrency-policy.js +35 -0
  115. package/src/core/execution-log.js +108 -0
  116. package/src/core/intake-recommendation.js +134 -0
  117. package/src/core/memory-state.js +62 -0
  118. package/src/core/mission-state.js +780 -0
  119. package/src/core/pending-gates.js +66 -0
  120. package/src/core/phase-cost.js +116 -0
  121. package/src/core/phase-run-read.js +27 -0
  122. package/src/core/policy-overlay.js +180 -0
  123. package/src/core/state-store.js +201 -0
  124. package/src/core/workflow-executor.js +1014 -0
  125. package/src/core/workflow-integrity.js +111 -0
  126. package/src/core/workflow-loader.js +163 -0
  127. package/src/core/workflow-rerun.js +84 -0
  128. package/src/core/workflow-state.js +119 -0
  129. package/src/server.js +1307 -0
  130. package/workflows/corps.yaml +481 -0
  131. package/workflows/yolo.yaml +241 -0
  132. package/xflow/README.md +76 -0
  133. package/xflow/aha/SKILL.md +82 -0
  134. package/xflow/aha/scripts/append_aha.py +50 -0
  135. package/xflow/archive/scripts/archive_change.py +338 -0
  136. package/xflow/archive/templates/merge-design.md +54 -0
  137. package/xflow/archive/templates/merge-workflow.md +26 -0
  138. package/xflow/atoms/__init__.py +6 -0
  139. package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
  140. package/xflow/atoms/a2_branch_create_and_link.py +42 -0
  141. package/xflow/atoms/a3_repo_doctor.py +11 -0
  142. package/xflow/atoms/a4_project_set_status.py +11 -0
  143. package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
  144. package/xflow/atoms/a6_pr_create.py +72 -0
  145. package/xflow/atoms/b1_change_scaffold.py +11 -0
  146. package/xflow/atoms/b2_status_read.py +16 -0
  147. package/xflow/atoms/b2b_status_write.py +20 -0
  148. package/xflow/atoms/b3_status_transition.py +27 -0
  149. package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
  150. package/xflow/atoms/b5_aha_append.py +11 -0
  151. package/xflow/atoms/b6_takein_digest.py +11 -0
  152. package/xflow/atoms/b6b_lookback_digest.py +11 -0
  153. package/xflow/atoms/b7_change_mission_bind.py +43 -0
  154. package/xflow/atoms/c1_mem.py +54 -0
  155. package/xflow/atoms/e6_gate_local_precheck.py +23 -0
  156. package/xflow/atoms/h1_design_lite_gate.py +74 -0
  157. package/xflow/atoms/i6a_tdd_run.py +107 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +184 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +372 -0
  160. package/xflow/atoms/j1_tests_run.py +129 -0
  161. package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
  162. package/xflow/atoms/j4b_spec_delta_review.py +316 -0
  163. package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
  164. package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
  165. package/xflow/atoms/j6_cr_review.py +443 -0
  166. package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
  167. package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
  168. package/xflow/atoms/k3_handoff_refresh.py +14 -0
  169. package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
  170. package/xflow/atoms/k5_archive_publish.py +21 -0
  171. package/xflow/atoms/k6_aha_merge.py +42 -0
  172. package/xflow/atoms/tdd_lib.py +956 -0
  173. package/xflow/corps/SKILL.md +109 -0
  174. package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
  175. package/xflow/goal/SKILL.md +150 -0
  176. package/xflow/handoff/SKILL.md +95 -0
  177. package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
  178. package/xflow/lookback/SKILL.md +66 -0
  179. package/xflow/lookback/scripts/lookback_digest.py +46 -0
  180. package/xflow/openissue/scripts/open_issue_flow.py +438 -0
  181. package/xflow/plan/SKILL.md +47 -0
  182. package/xflow/ralph/SKILL.md +123 -0
  183. package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
  184. package/xflow/scripts/common.py +483 -0
  185. package/xflow/scripts/doctor.py +278 -0
  186. package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
  187. package/xflow/takein/SKILL.md +81 -0
  188. package/xflow/takein/scripts/takein_digest.py +88 -0
  189. package/xflow/yolo/SKILL.md +75 -0
  190. package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
  191. package/xflow/yolo/scripts/yolo_gate.py +312 -0
  192. package/xflow/yolo/templates/DESIGN.md +179 -0
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { resolve } from 'node:path';
3
+
4
+ export const STRICT_CORPS_FORBIDDEN_ADAPTERS = new Set(['stub']);
5
+ export const STRICT_CORPS_FORBIDDEN_STATUSES = new Set(['task_queued', 'pencil_stubbed']);
6
+
7
+ export function stableStringify(value) {
8
+ return JSON.stringify(sortForStableStringify(value));
9
+ }
10
+
11
+ function sortForStableStringify(value) {
12
+ if (Array.isArray(value)) {
13
+ return value.map((entry) => sortForStableStringify(entry));
14
+ }
15
+ if (value && typeof value === 'object') {
16
+ return Object.fromEntries(
17
+ Object.entries(value)
18
+ .sort(([a], [b]) => a.localeCompare(b))
19
+ .map(([key, entry]) => [key, sortForStableStringify(entry)]),
20
+ );
21
+ }
22
+ return value;
23
+ }
24
+
25
+ export function hashPayload(value) {
26
+ return createHash('sha256').update(stableStringify(value)).digest('hex');
27
+ }
28
+
29
+ export function buildLogEntryHash(entry) {
30
+ const { entry_hash: _entryHash, ...payload } = entry || {};
31
+ return hashPayload(payload);
32
+ }
33
+
34
+ export function validateExecutionLogChain(entries = [], options = {}) {
35
+ const issues = [];
36
+ let previousHash = options.initialPreviousHash || null;
37
+ const allowExternalPreviousHash = options.allowExternalPreviousHash === true;
38
+ entries.forEach((entry, index) => {
39
+ const expectedPrevious = previousHash || null;
40
+ if (!(index === 0 && allowExternalPreviousHash) && (entry.prev_hash || null) !== expectedPrevious) {
41
+ issues.push(`execution_log_chain_prev_mismatch:${index}`);
42
+ }
43
+ const expectedHash = buildLogEntryHash(entry);
44
+ if (entry.entry_hash !== expectedHash) {
45
+ issues.push(`execution_log_chain_hash_mismatch:${index}`);
46
+ }
47
+ previousHash = entry.entry_hash || expectedHash;
48
+ });
49
+ return {
50
+ ok: issues.length === 0,
51
+ issues,
52
+ entries_checked: entries.length,
53
+ };
54
+ }
55
+
56
+ export function buildWorkflowManifest({ workflowPath = null, workflow = {}, phases = [], registry = {} }) {
57
+ const referencedAtomIds = new Set();
58
+ for (const phase of phases) {
59
+ for (const atomRef of phase.atoms || []) {
60
+ referencedAtomIds.add(atomRef.id);
61
+ }
62
+ if (phase.gate?.atom) {
63
+ referencedAtomIds.add(phase.gate.atom);
64
+ }
65
+ }
66
+
67
+ const atomEntries = [...referencedAtomIds].sort().map((atomId) => {
68
+ const def = registry.atoms?.[atomId] || {};
69
+ return {
70
+ id: atomId,
71
+ track: def.track || null,
72
+ type: def.type || null,
73
+ script: def.script || null,
74
+ module: def.module || null,
75
+ export: def.export || null,
76
+ expected_artifact: def.expected_artifact || null,
77
+ prompt_template: def.prompt_template || null,
78
+ agent_adapter: def.agent_config?.adapter || null,
79
+ requires_http: def.requires_http === true,
80
+ };
81
+ });
82
+
83
+ const body = {
84
+ version: 1,
85
+ workflow_name: workflow.name || null,
86
+ track: workflow.track || null,
87
+ phase_count: phases.length,
88
+ phase_ids: phases.map((phase) => phase.id),
89
+ phases: phases.map((phase) => ({
90
+ id: phase.id,
91
+ required: phase.required !== false,
92
+ atom_ids: (phase.atoms || []).map((atomRef) => atomRef.id),
93
+ gate: {
94
+ type: phase.gate?.type || 'skip',
95
+ atom: phase.gate?.atom || null,
96
+ on_fail: phase.gate?.on_fail || null,
97
+ },
98
+ artifacts: (phase.artifacts || []).map((artifact) => ({
99
+ path: artifact.path || null,
100
+ optional: artifact.optional === true,
101
+ })),
102
+ })),
103
+ atoms: atomEntries,
104
+ };
105
+
106
+ return {
107
+ ...body,
108
+ workflow_path: workflowPath ? resolve(workflowPath) : null,
109
+ digest: hashPayload(body),
110
+ };
111
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * XFlow Workflow Loader
3
+ *
4
+ * Responsibilities:
5
+ * 1. Load and validate a workflow YAML against schemas/workflow.schema.json
6
+ * 2. Resolve every atom ID through atoms/registry.json
7
+ * 3. Verify track constraints (lite atoms must not appear in heavy-only positions)
8
+ * 4. Check runtime preconditions (agentos_http, gh, etc.)
9
+ * 5. Provide a WorkflowRunner that iterates phases and dispatches atoms
10
+ *
11
+ * E6 ≡ E1+E2 invariant: this loader does NOT perform structural artifact checks
12
+ * directly — it always routes through E6.gate.local_precheck (Python) for
13
+ * deterministic gate checks, regardless of track. Single source of truth.
14
+ */
15
+
16
+ import { readFileSync, existsSync } from 'fs';
17
+ import { resolve, dirname } from 'path';
18
+ import { fileURLToPath } from 'url';
19
+ import yaml from 'js-yaml';
20
+ import Ajv from 'ajv';
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const ROOT = resolve(__dirname, '..', '..');
24
+
25
+ // ─── Schema + Registry ───────────────────────────────────────────────────────
26
+
27
+ function loadSchema() {
28
+ const path = resolve(ROOT, 'schemas', 'workflow.schema.json');
29
+ return JSON.parse(readFileSync(path, 'utf8'));
30
+ }
31
+
32
+ function loadRegistry() {
33
+ const path = resolve(ROOT, 'atoms', 'registry.json');
34
+ return JSON.parse(readFileSync(path, 'utf8'));
35
+ }
36
+
37
+ // ─── Core loader ─────────────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Load and validate a workflow YAML file.
41
+ * Returns { workflow, phases, registry } ready for execution.
42
+ *
43
+ * @param {string} workflowPath - absolute or relative path to .yaml file
44
+ * @param {object} options
45
+ * @param {boolean} options.skipRuntimeChecks - skip gh/agentos-bin checks (for testing)
46
+ * @returns {{ workflow, phases, registry, errors }}
47
+ */
48
+ export function load(workflowPath, { skipRuntimeChecks = false } = {}) {
49
+ const absPath = resolve(process.cwd(), workflowPath);
50
+ if (!existsSync(absPath)) {
51
+ throw new Error(`Workflow file not found: ${absPath}`);
52
+ }
53
+
54
+ const raw = readFileSync(absPath, 'utf8');
55
+ const workflow = yaml.load(raw);
56
+
57
+ // 1. Schema validation
58
+ const schema = loadSchema();
59
+ const ajv = new Ajv({ allErrors: true });
60
+ const validate = ajv.compile(schema);
61
+ if (!validate(workflow)) {
62
+ const errors = validate.errors.map(e => ` ${e.instancePath} ${e.message}`).join('\n');
63
+ throw new Error(`Workflow schema validation failed for ${absPath}:\n${errors}`);
64
+ }
65
+
66
+ // 2. Registry resolution + track checks
67
+ const registry = loadRegistry();
68
+ const errors = [];
69
+
70
+ for (const phase of workflow.phases) {
71
+ for (const atomRef of (phase.atoms || [])) {
72
+ const atomDef = registry.atoms[atomRef.id];
73
+ if (!atomDef) {
74
+ errors.push(`Phase "${phase.id}": unknown atom "${atomRef.id}"`);
75
+ continue;
76
+ }
77
+ // Track constraint: lite workflow cannot use heavy atoms
78
+ if (workflow.track === 'lite' && atomDef.track === 'heavy') {
79
+ errors.push(`Phase "${phase.id}": lite workflow cannot use heavy atom "${atomRef.id}"`);
80
+ }
81
+ }
82
+ // Gate atom check
83
+ if (phase.gate?.atom) {
84
+ const gateAtom = registry.atoms[phase.gate.atom];
85
+ if (!gateAtom) {
86
+ errors.push(`Phase "${phase.id}" gate: unknown atom "${phase.gate.atom}"`);
87
+ }
88
+ }
89
+ }
90
+
91
+ if (errors.length > 0) {
92
+ throw new Error(`Workflow validation errors in ${absPath}:\n${errors.map(e => ` - ${e}`).join('\n')}`);
93
+ }
94
+
95
+ // 3. Runtime requirement checks
96
+ if (!skipRuntimeChecks) {
97
+ const req = workflow.requires || {};
98
+ if (req.agentos_bin) {
99
+ checkBinOnPath('xflow', 'xflow CLI on PATH');
100
+ }
101
+ if (req.gh) {
102
+ checkBinOnPath('gh', 'GitHub CLI (brew install gh)');
103
+ }
104
+ }
105
+
106
+ return { workflow, phases: workflow.phases, registry };
107
+ }
108
+
109
+ // ─── Runtime checks ──────────────────────────────────────────────────────────
110
+
111
+ import { spawnSync } from 'child_process';
112
+
113
+ function checkBinOnPath(bin, description) {
114
+ const result = spawnSync('which', [bin], { encoding: 'utf8' });
115
+ if (result.status !== 0) {
116
+ throw new Error(`Required binary not found: ${bin} (${description})`);
117
+ }
118
+ }
119
+
120
+ // ─── WorkflowRunner ──────────────────────────────────────────────────────────
121
+
122
+ /**
123
+ * Dry-run: resolve all phases and atoms, print what would execute.
124
+ * Does not mutate any state.
125
+ */
126
+ export function dryRun(workflowPath, input = {}) {
127
+ const { workflow, phases, registry } = load(workflowPath, { skipRuntimeChecks: true });
128
+
129
+ console.log(`\n── Workflow: ${workflow.name} (${workflow.track}) ──`);
130
+ console.log(` requires: ${JSON.stringify(workflow.requires)}`);
131
+ console.log(` phases: ${phases.length}\n`);
132
+
133
+ for (let i = 0; i < phases.length; i++) {
134
+ const phase = phases[i];
135
+ const atoms = (phase.atoms || []).map(a => {
136
+ const def = registry.atoms[a.id];
137
+ const impl = def?.script || def?.module || 'unknown';
138
+ return `${a.id} [${def?.track || '?'}] → ${impl}`;
139
+ });
140
+
141
+ console.log(` ${String(i + 1).padStart(2)}. [${phase.required ? 'required' : 'optional'}] ${phase.id}`);
142
+ if (phase.label) console.log(` ${phase.label}`);
143
+ for (const a of atoms) {
144
+ console.log(` atom: ${a}`);
145
+ }
146
+ if (phase.gate) {
147
+ console.log(` gate: ${phase.gate.type}${phase.gate.atom ? ` via ${phase.gate.atom}` : ''}`);
148
+ }
149
+ console.log('');
150
+ }
151
+
152
+ return { workflow, phases };
153
+ }
154
+
155
+ /**
156
+ * Resolve a single atom from the registry to its invocation descriptor.
157
+ * Used by the execution layer to dispatch atoms.
158
+ */
159
+ export function resolveAtom(atomId, registry) {
160
+ const def = registry.atoms[atomId];
161
+ if (!def) throw new Error(`Unknown atom: ${atomId}`);
162
+ return { ...def, id: atomId };
163
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+
8
+ function loadExecutorState(stateFile) {
9
+ if (!existsSync(stateFile)) {
10
+ return null;
11
+ }
12
+ try {
13
+ const raw = readFileSync(stateFile, 'utf8');
14
+ return raw.trim() ? JSON.parse(raw) : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function saveExecutorState(stateFile, state) {
21
+ mkdirSync(dirname(stateFile), { recursive: true });
22
+ writeFileSync(stateFile, `${JSON.stringify({ ...state, updated_at: nowIso() }, null, 2)}\n`, 'utf8');
23
+ }
24
+
25
+ function trimCompletedPhases(completedPhases = [], allowedPhaseIds = new Set()) {
26
+ return (Array.isArray(completedPhases) ? completedPhases : []).filter((entry) => {
27
+ if (typeof entry === 'string') {
28
+ return allowedPhaseIds.has(entry);
29
+ }
30
+ if (entry && typeof entry === 'object') {
31
+ return allowedPhaseIds.has(entry.phase);
32
+ }
33
+ return false;
34
+ });
35
+ }
36
+
37
+ export function prepareWorkflowRerun({
38
+ workflow,
39
+ phases,
40
+ projectRoot = process.cwd(),
41
+ changeId = null,
42
+ phaseId,
43
+ atomId = null,
44
+ }) {
45
+ const targetPhase = phases.find((phase) => phase.id === phaseId);
46
+ if (!targetPhase) {
47
+ throw new Error(`Unknown rerun phase: ${phaseId}`);
48
+ }
49
+
50
+ if (atomId && !(targetPhase.atoms || []).some((atomRef) => atomRef.id === atomId)) {
51
+ throw new Error(`Atom "${atomId}" is not present in phase "${phaseId}"`);
52
+ }
53
+
54
+ const phaseOrder = phases.map((phase) => phase.id);
55
+ const phaseIndex = phaseOrder.indexOf(phaseId);
56
+ const previousPhase = phaseIndex > 0 ? phaseOrder[phaseIndex - 1] : null;
57
+ const allowedCompleted = new Set(previousPhase ? phaseOrder.slice(0, phaseIndex) : []);
58
+ const stateFile = resolve(projectRoot, '.as-xflow', 'workflow-state.json');
59
+ const current = loadExecutorState(stateFile) || {};
60
+ const nextState = {
61
+ workflow: workflow.name,
62
+ change_id: current.change_id || changeId || null,
63
+ started_at: current.started_at || nowIso(),
64
+ status: 'active',
65
+ completed_phases: trimCompletedPhases(current.completed_phases, allowedCompleted),
66
+ last_completed_phase: previousPhase,
67
+ rerun: {
68
+ phase_id: phaseId,
69
+ atom_id: atomId || null,
70
+ requested_at: nowIso(),
71
+ },
72
+ };
73
+
74
+ saveExecutorState(stateFile, nextState);
75
+
76
+ const pendingGateFile = resolve(projectRoot, '.as-xflow', 'pending-gates', `${phaseId}.json`);
77
+ rmSync(pendingGateFile, { force: true });
78
+
79
+ return {
80
+ state_file: stateFile,
81
+ state: nextState,
82
+ rerun: nextState.rerun,
83
+ };
84
+ }
@@ -0,0 +1,119 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+
5
+ function nowIso() {
6
+ return new Date().toISOString();
7
+ }
8
+
9
+ function makeId(prefix) {
10
+ return `${prefix}_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
11
+ }
12
+
13
+ export function resolveWorkflowStateFile(projectRoot = process.cwd()) {
14
+ return resolve(projectRoot, '.as-xflow', 'workflow-state.json');
15
+ }
16
+
17
+ function normalizePhaseOrder(phaseOrder = [], fallbackPhase = null) {
18
+ const order = Array.isArray(phaseOrder) ? phaseOrder.filter(Boolean) : [];
19
+ if (order.length > 0) {
20
+ return order;
21
+ }
22
+ return fallbackPhase ? [fallbackPhase] : [];
23
+ }
24
+
25
+ function ensureStateDefaults(state = {}, { workflowName = null, changeId = null, phaseOrder = [], currentPhase = null } = {}) {
26
+ const now = nowIso();
27
+ const order = normalizePhaseOrder(phaseOrder, currentPhase || state.current_phase || state.last_completed_phase || null);
28
+ const phase = currentPhase || state.current_phase || state.last_completed_phase || order[0] || null;
29
+ const phaseIndex = phase ? order.indexOf(phase) : null;
30
+ return {
31
+ version: state.version || 1,
32
+ workflow_id: state.workflow_id || makeId('wf'),
33
+ workflow: state.workflow || workflowName || null,
34
+ change_id: state.change_id || changeId || null,
35
+ status: state.status || 'active',
36
+ started_at: state.started_at || now,
37
+ updated_at: now,
38
+ current_phase: phase,
39
+ current_phase_index: phaseIndex >= 0 ? phaseIndex : null,
40
+ completed_phases: Array.isArray(state.completed_phases) ? state.completed_phases : [],
41
+ phase_order: order,
42
+ current_artifact_path: state.current_artifact_path || null,
43
+ last_transition: state.last_transition || null,
44
+ last_completed_phase: state.last_completed_phase || null,
45
+ pending_gate_token: state.pending_gate_token || null,
46
+ pending_gate_phase: state.pending_gate_phase || null,
47
+ pending_gate_issued_at: state.pending_gate_issued_at || null,
48
+ };
49
+ }
50
+
51
+ export function loadWorkflowState(projectRoot = process.cwd(), options = {}) {
52
+ const file = resolveWorkflowStateFile(projectRoot);
53
+ if (!existsSync(file)) {
54
+ return ensureStateDefaults({}, options);
55
+ }
56
+
57
+ try {
58
+ const raw = readFileSync(file, 'utf8');
59
+ const parsed = raw.trim() ? JSON.parse(raw) : {};
60
+ return ensureStateDefaults(parsed, options);
61
+ } catch {
62
+ return ensureStateDefaults({}, options);
63
+ }
64
+ }
65
+
66
+ export function saveWorkflowState(projectRoot = process.cwd(), state = {}) {
67
+ const file = resolveWorkflowStateFile(projectRoot);
68
+ mkdirSync(dirname(file), { recursive: true });
69
+ writeFileSync(file, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
70
+ return file;
71
+ }
72
+
73
+ export function advanceWorkflowState(projectRoot = process.cwd(), { expectedPhase = null, artifactPath = null, workflowName = null, changeId = null, phaseOrder = [] } = {}) {
74
+ const state = loadWorkflowState(projectRoot, { workflowName, changeId, phaseOrder, currentPhase: expectedPhase });
75
+ const currentPhase = expectedPhase || state.current_phase;
76
+ const order = normalizePhaseOrder(state.phase_order, currentPhase);
77
+ const currentIndex = currentPhase ? order.indexOf(currentPhase) : -1;
78
+
79
+ if (!currentPhase || currentIndex < 0) {
80
+ throw new Error(`Cannot advance workflow state from unknown phase: ${String(currentPhase || '')}`);
81
+ }
82
+
83
+ const nextPhase = order[currentIndex + 1] || null;
84
+ const timestamp = nowIso();
85
+ const completedEntry = {
86
+ phase: currentPhase,
87
+ artifact_path: artifactPath || state.current_artifact_path || null,
88
+ completed_at: timestamp,
89
+ };
90
+
91
+ const completedPhases = Array.isArray(state.completed_phases) ? [...state.completed_phases] : [];
92
+ completedPhases.push(completedEntry);
93
+
94
+ const nextState = {
95
+ ...state,
96
+ workflow: state.workflow || workflowName || null,
97
+ change_id: state.change_id || changeId || null,
98
+ phase_order: order,
99
+ completed_phases: completedPhases,
100
+ current_phase: nextPhase,
101
+ current_phase_index: nextPhase ? order.indexOf(nextPhase) : null,
102
+ current_artifact_path: null,
103
+ last_transition: {
104
+ from: currentPhase,
105
+ to: nextPhase,
106
+ artifact_path: artifactPath || null,
107
+ transitioned_at: timestamp,
108
+ },
109
+ last_completed_phase: currentPhase,
110
+ updated_at: timestamp,
111
+ status: nextPhase ? 'active' : 'complete',
112
+ pending_gate_token: null,
113
+ pending_gate_phase: null,
114
+ pending_gate_issued_at: null,
115
+ };
116
+
117
+ saveWorkflowState(projectRoot, nextState);
118
+ return nextState;
119
+ }