@surlymochan/evalid 0.0.0-stage → 0.3.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 +74 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +102 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7208 -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 +83 -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 +1125 -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 +998 -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 +479 -0
  131. package/workflows/yolo.yaml +233 -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 +104 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +357 -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 +747 -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 +38 -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 +480 -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 +66 -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,90 @@
1
+ import { readJsonFile, resolveChangePath, writeJsonFile } from '../../change-artifacts.js';
2
+ import { resolveEffectivePolicy } from '../../policy-overlay.js';
3
+
4
+ export async function policyGate(input = {}, context = {}) {
5
+ const projectRoot = context.projectRoot || process.cwd();
6
+ const changeId = context.changeId || input.change_id;
7
+ if (!changeId) throw new Error('change_id is required for P2.policy.gate');
8
+
9
+ const policyResult = resolveEffectivePolicy({
10
+ projectRoot,
11
+ changeId,
12
+ executionMode: input.execution_mode || null,
13
+ writeArtifact: true,
14
+ });
15
+ if (!policyResult.ok) {
16
+ const packet = {
17
+ version: 1,
18
+ ok: false,
19
+ generated_at: new Date().toISOString(),
20
+ failed_checks: [{ code: policyResult.error.code, message: policyResult.error.message }],
21
+ policy_error: policyResult.error,
22
+ };
23
+ const outputPath = resolveChangePath(projectRoot, changeId, 'policy_gate.json');
24
+ writeJsonFile(outputPath, packet);
25
+ return { ...packet, policy_gate_file: outputPath };
26
+ }
27
+
28
+ const policy = policyResult.policy;
29
+ const failedChecks = [
30
+ ...checkCoverage(projectRoot, changeId, policy),
31
+ ...checkReviewers(projectRoot, changeId, policy),
32
+ ...checkSmoke(projectRoot, changeId, policy),
33
+ ];
34
+ const packet = {
35
+ version: 1,
36
+ ok: failedChecks.length === 0,
37
+ generated_at: new Date().toISOString(),
38
+ execution_mode: policy.execution_mode || 'personal',
39
+ failed_checks: failedChecks,
40
+ advisory: policy.execution_mode !== 'team',
41
+ };
42
+ const outputPath = resolveChangePath(projectRoot, changeId, 'policy_gate.json');
43
+ writeJsonFile(outputPath, packet);
44
+ return { ...packet, policy_gate_file: outputPath };
45
+ }
46
+
47
+ function checkCoverage(projectRoot, changeId, policy) {
48
+ const threshold = policy?.coding?.min_changed_line_coverage;
49
+ if (threshold === undefined) return [];
50
+ const quality = readJsonFile(resolveChangePath(projectRoot, changeId, 'tdd/quality-0.json'), {});
51
+ const actual = Number(
52
+ quality.changed_line_coverage
53
+ ?? quality.incremental_line_coverage
54
+ ?? quality.coverage
55
+ ?? quality.gate_value,
56
+ );
57
+ if (!Number.isFinite(actual)) {
58
+ return [{ code: 'coverage_evidence_missing', message: 'changed-line coverage evidence is missing', expected: Number(threshold), actual: null }];
59
+ }
60
+ return actual >= Number(threshold)
61
+ ? []
62
+ : [{ code: 'coverage_below_policy', message: 'changed-line coverage is below team policy', expected: Number(threshold), actual }];
63
+ }
64
+
65
+ function checkReviewers(projectRoot, changeId, policy) {
66
+ const threshold = policy?.review?.min_reviewers;
67
+ if (threshold === undefined) return [];
68
+ const review = readJsonFile(resolveChangePath(projectRoot, changeId, 'review.json'), {});
69
+ const reviewers = Array.isArray(review.reviewers)
70
+ ? review.reviewers
71
+ : Array.isArray(review.approved_by)
72
+ ? review.approved_by
73
+ : [];
74
+ return reviewers.length >= Number(threshold)
75
+ ? []
76
+ : [{ code: 'reviewers_below_policy', message: 'reviewer count is below team policy', expected: Number(threshold), actual: reviewers.length }];
77
+ }
78
+
79
+ function checkSmoke(projectRoot, changeId, policy) {
80
+ const threshold = policy?.deploy?.required_smoke_pass_rate;
81
+ if (threshold === undefined) return [];
82
+ const smoke = readJsonFile(resolveChangePath(projectRoot, changeId, 'smoke.json'), {});
83
+ const actual = Number(smoke.pass_rate ?? smoke.required_smoke_pass_rate ?? smoke.smoke_pass_rate);
84
+ if (!Number.isFinite(actual)) {
85
+ return [{ code: 'smoke_evidence_missing', message: 'smoke pass-rate evidence is missing', expected: Number(threshold), actual: null }];
86
+ }
87
+ return actual >= Number(threshold)
88
+ ? []
89
+ : [{ code: 'smoke_pass_rate_below_policy', message: 'smoke pass rate is below team policy', expected: Number(threshold), actual }];
90
+ }
@@ -0,0 +1,11 @@
1
+ import { resolveEffectivePolicy } from '../../policy-overlay.js';
2
+
3
+ export async function policyResolve(input = {}, context = {}) {
4
+ const projectRoot = context.projectRoot || process.cwd();
5
+ const changeId = context.changeId || input.change_id || null;
6
+ return resolveEffectivePolicy({
7
+ projectRoot,
8
+ changeId,
9
+ executionMode: input.execution_mode || null,
10
+ });
11
+ }
@@ -0,0 +1,57 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ import { resolveChangePath, writeJsonFile } from '../../change-artifacts.js';
5
+
6
+ export async function storageSync(input = {}, context = {}) {
7
+ const projectRoot = context.projectRoot || process.cwd();
8
+ const changeId = context.changeId || input.change_id;
9
+ if (!changeId) throw new Error('change_id is required for storage.sync');
10
+
11
+ const provider = input.provider || 'file';
12
+ const direction = input.direction || 'import';
13
+ if (!['file', 'mock'].includes(provider)) {
14
+ return { ok: false, error: `Unsupported storage provider: ${provider}` };
15
+ }
16
+ if (!['import', 'export'].includes(direction)) {
17
+ return { ok: false, error: `Unsupported storage direction: ${direction}` };
18
+ }
19
+
20
+ const source = input.source ? resolve(projectRoot, input.source) : null;
21
+ const target = resolve(projectRoot, '.xflow', 'storage', normalizeStoragePath(input.target || 'artifact.json'));
22
+ let copied = false;
23
+ if (provider === 'file' && direction === 'import') {
24
+ if (!source || !existsSync(source)) {
25
+ return { ok: false, error: `storage source not found: ${source || '(missing)'}` };
26
+ }
27
+ mkdirSync(dirname(target), { recursive: true });
28
+ copyFileSync(source, target);
29
+ copied = true;
30
+ }
31
+
32
+ const evidence = {
33
+ version: 1,
34
+ provider,
35
+ direction,
36
+ source,
37
+ target,
38
+ copied,
39
+ generated_at: new Date().toISOString(),
40
+ };
41
+ const outputPath = resolveChangePath(projectRoot, changeId, `storage/${provider}-${direction}.json`);
42
+ writeJsonFile(outputPath, evidence);
43
+ return {
44
+ ok: true,
45
+ storage_sync_file: outputPath,
46
+ storage_target: target,
47
+ evidence,
48
+ };
49
+ }
50
+
51
+ function normalizeStoragePath(value) {
52
+ return String(value || 'artifact.json')
53
+ .replace(/^\.+/, '')
54
+ .replace(/\\/g, '/')
55
+ .replace(/\/+/g, '/')
56
+ .replace(/^\/+/, '');
57
+ }
@@ -0,0 +1,25 @@
1
+ import { resolveChangePath } from '../../change-artifacts.js';
2
+ import { advanceWorkflowState } from '../../workflow-state.js';
3
+
4
+ export async function workflowAdvance(input = {}, context = {}) {
5
+ const projectRoot = context.projectRoot || process.cwd();
6
+ const workflowName = context.workflow?.name || input.workflow || null;
7
+ const changeId = context.changeId || input.change_id || null;
8
+ const phaseOrder = Array.isArray(context.workflow?.phases) ? context.workflow.phases.map((phase) => phase.id) : [];
9
+ const expectedPhase = input.phase || context.phase?.id || null;
10
+ const artifactPath = input.artifact_path
11
+ || (changeId && expectedPhase ? resolveChangePath(projectRoot, changeId, `${expectedPhase}.json`) : null);
12
+
13
+ const workflowState = advanceWorkflowState(projectRoot, {
14
+ expectedPhase,
15
+ artifactPath,
16
+ workflowName,
17
+ changeId,
18
+ phaseOrder,
19
+ });
20
+
21
+ return {
22
+ ok: true,
23
+ workflow_state: workflowState,
24
+ };
25
+ }
@@ -0,0 +1,127 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ import { resolveChangePath } from '../../change-artifacts.js';
6
+ import { advanceWorkflowState, loadWorkflowState, resolveWorkflowStateFile, saveWorkflowState } from '../../workflow-state.js';
7
+
8
+ const VALID_ARTIFACT_STATUSES = new Set(['draft', 'ready', 'blocked', 'pass', 'fail', 'done', 'completed', 'accepted', 'needs_human']);
9
+
10
+ function makeId(prefix) {
11
+ return `${prefix}_${randomUUID().replace(/-/g, '').slice(0, 12)}`;
12
+ }
13
+
14
+ function resolveCommand(input = {}) {
15
+ if (Array.isArray(input.command) && input.command.length > 0) {
16
+ return input.command.map((part) => String(part));
17
+ }
18
+ if (typeof input.command === 'string' && input.command.trim()) {
19
+ return ['sh', '-c', input.command];
20
+ }
21
+ if (Array.isArray(input.commands) && input.commands.length > 0) {
22
+ return input.commands.map((part) => String(part));
23
+ }
24
+ throw new Error('E2.workflow.guard requires a command');
25
+ }
26
+
27
+ function validateArtifact(artifactPath, expectedPhase, gateToken) {
28
+ const raw = readFileSync(artifactPath, 'utf8');
29
+ const artifact = JSON.parse(raw);
30
+ if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
31
+ throw new Error('Workflow artifact must be a JSON object');
32
+ }
33
+ if (artifact.phase !== expectedPhase) {
34
+ throw new Error(`Workflow artifact phase mismatch: expected ${expectedPhase}, got ${String(artifact.phase || '')}`);
35
+ }
36
+ if (!VALID_ARTIFACT_STATUSES.has(String(artifact.status || ''))) {
37
+ throw new Error(`Workflow artifact status must be one of: ${[...VALID_ARTIFACT_STATUSES].join(', ')}`);
38
+ }
39
+ if (typeof artifact.summary !== 'string' || artifact.summary.trim().length === 0) {
40
+ throw new Error('Workflow artifact summary is required');
41
+ }
42
+ if (artifact.gate_token !== gateToken) {
43
+ throw new Error('Workflow artifact gate token mismatch');
44
+ }
45
+ return artifact;
46
+ }
47
+
48
+ export async function workflowGuard(input = {}, context = {}) {
49
+ const projectRoot = context.projectRoot || process.cwd();
50
+ const workflowName = context.workflow?.name || input.workflow || null;
51
+ const changeId = context.changeId || input.change_id || null;
52
+ const phaseOrder = Array.isArray(context.workflow?.phases) ? context.workflow.phases.map((phase) => phase.id) : [];
53
+ const expectedPhase = input.phase || context.phase?.id || null;
54
+ if (!expectedPhase) {
55
+ throw new Error('phase is required for E2.workflow.guard');
56
+ }
57
+
58
+ const artifactPath = input.artifact_path
59
+ || (changeId ? resolveChangePath(projectRoot, changeId, `${expectedPhase}.json`) : null);
60
+ if (!artifactPath) {
61
+ throw new Error('artifact_path is required for E2.workflow.guard');
62
+ }
63
+
64
+ const command = resolveCommand(input);
65
+ const gateToken = makeId('gt');
66
+ const state = loadWorkflowState(projectRoot, { workflowName, changeId, phaseOrder, currentPhase: expectedPhase });
67
+ if (state.current_phase && state.current_phase !== expectedPhase) {
68
+ throw new Error(`Workflow guard blocked: current phase is ${state.current_phase}, requested ${expectedPhase}`);
69
+ }
70
+
71
+ const nextState = {
72
+ ...state,
73
+ workflow: state.workflow || workflowName || null,
74
+ change_id: state.change_id || changeId || null,
75
+ current_phase: state.current_phase || expectedPhase,
76
+ current_artifact_path: artifactPath,
77
+ pending_gate_token: gateToken,
78
+ pending_gate_phase: expectedPhase,
79
+ pending_gate_issued_at: new Date().toISOString(),
80
+ };
81
+ saveWorkflowState(projectRoot, nextState);
82
+
83
+ const env = {
84
+ ...process.env,
85
+ AS_XFLOW_WORKFLOW_ID: String(nextState.workflow_id || ''),
86
+ AS_XFLOW_WORKFLOW_PHASE: expectedPhase,
87
+ AS_XFLOW_WORKFLOW_STATE_FILE: resolveWorkflowStateFile(projectRoot),
88
+ AS_XFLOW_WORKFLOW_ARTIFACT_PATH: artifactPath,
89
+ AS_XFLOW_WORKFLOW_GATE_TOKEN: gateToken,
90
+ };
91
+
92
+ const result = spawnSync(command[0], command.slice(1), {
93
+ cwd: projectRoot,
94
+ env,
95
+ encoding: 'utf8',
96
+ stdio: 'inherit',
97
+ });
98
+
99
+ if (result.error) {
100
+ throw result.error;
101
+ }
102
+ if (result.status !== 0) {
103
+ return {
104
+ ok: false,
105
+ status: result.status,
106
+ signal: result.signal || null,
107
+ workflow_state: nextState,
108
+ };
109
+ }
110
+
111
+ validateArtifact(artifactPath, expectedPhase, gateToken);
112
+ const advanced = advanceWorkflowState(projectRoot, {
113
+ expectedPhase,
114
+ artifactPath,
115
+ workflowName,
116
+ changeId,
117
+ phaseOrder,
118
+ });
119
+
120
+ return {
121
+ ok: true,
122
+ status: result.status,
123
+ signal: result.signal || null,
124
+ workflow_state: advanced,
125
+ artifact_path: artifactPath,
126
+ };
127
+ }
@@ -0,0 +1,37 @@
1
+ import { resolveBoundMissionId } from '../../bindings.js';
2
+ import { getMissionDetail } from '../../mission-state.js';
3
+ import { readStore, resolveDataFile } from '../../state-store.js';
4
+ import { loadWorkflowState, resolveWorkflowStateFile, saveWorkflowState } from '../../workflow-state.js';
5
+
6
+ export async function workflowStateLoad(input = {}, context = {}) {
7
+ const projectRoot = context.projectRoot || process.cwd();
8
+ const workflowName = context.workflow?.name || input.workflow || null;
9
+ const changeId = context.changeId || input.change_id || null;
10
+ const phaseOrder = Array.isArray(context.workflow?.phases) ? context.workflow.phases.map((phase) => phase.id) : [];
11
+ const currentPhase = context.phase?.id || input.phase || phaseOrder[0] || null;
12
+ const stateFile = resolveWorkflowStateFile(projectRoot);
13
+ const workflowState = loadWorkflowState(projectRoot, { workflowName, changeId, phaseOrder, currentPhase });
14
+ saveWorkflowState(projectRoot, workflowState);
15
+
16
+ const missionId = resolveBoundMissionId({
17
+ projectRoot,
18
+ changeId,
19
+ missionId: input.mission_id || null,
20
+ runtimeMissionId: context.runtime?.missionId || null,
21
+ });
22
+
23
+ let missionDetail = null;
24
+ if (missionId) {
25
+ const dataFile = resolveDataFile({ projectRoot, explicitPath: input.data_file || null });
26
+ const store = await readStore(dataFile);
27
+ missionDetail = getMissionDetail(store, missionId);
28
+ }
29
+
30
+ return {
31
+ ok: true,
32
+ state_file: stateFile,
33
+ workflow_state: workflowState,
34
+ mission_id: missionId,
35
+ mission_detail: missionDetail,
36
+ };
37
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ function bindingsFile(projectRoot) {
5
+ return resolve(projectRoot, '.as-xflow', 'bindings.json');
6
+ }
7
+
8
+ export function readBindings(projectRoot) {
9
+ const file = bindingsFile(projectRoot);
10
+ if (!existsSync(file)) {
11
+ return {};
12
+ }
13
+
14
+ try {
15
+ const raw = readFileSync(file, 'utf8');
16
+ return raw.trim() ? JSON.parse(raw) : {};
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ export function readBinding(projectRoot, changeId) {
23
+ return readBindings(projectRoot)[changeId] || null;
24
+ }
25
+
26
+ export function resolveBoundMissionId({ projectRoot, changeId, missionId = null, runtimeMissionId = null } = {}) {
27
+ if (missionId) {
28
+ return missionId;
29
+ }
30
+ if (runtimeMissionId) {
31
+ return runtimeMissionId;
32
+ }
33
+ if (!changeId) {
34
+ return null;
35
+ }
36
+ return readBinding(projectRoot, changeId)?.mission_id || null;
37
+ }
@@ -0,0 +1,134 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+
8
+ export function resolveChangeDir(projectRoot, changeId) {
9
+ if (!changeId) {
10
+ throw new Error('change_id is required');
11
+ }
12
+ return resolve(projectRoot, 'specs', 'changes', changeId);
13
+ }
14
+
15
+ export function resolveChangePath(projectRoot, changeId, relativePath) {
16
+ return resolve(resolveChangeDir(projectRoot, changeId), relativePath);
17
+ }
18
+
19
+ export function ensureParentDir(filePath) {
20
+ mkdirSync(dirname(filePath), { recursive: true });
21
+ }
22
+
23
+ export function writeJsonFile(filePath, payload) {
24
+ ensureParentDir(filePath);
25
+ writeFileSync(filePath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
26
+ return filePath;
27
+ }
28
+
29
+ export function writeTextFile(filePath, content) {
30
+ ensureParentDir(filePath);
31
+ writeFileSync(filePath, content.endsWith('\n') ? content : `${content}\n`, 'utf8');
32
+ return filePath;
33
+ }
34
+
35
+ export function readJsonFile(filePath, fallback = null) {
36
+ if (!existsSync(filePath)) {
37
+ return fallback;
38
+ }
39
+ try {
40
+ const raw = readFileSync(filePath, 'utf8');
41
+ return raw.trim() ? JSON.parse(raw) : fallback;
42
+ } catch {
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ export function collectExistingFiles(paths) {
48
+ return paths.filter((filePath) => existsSync(filePath));
49
+ }
50
+
51
+ export function summarizeFinding(finding = {}) {
52
+ if (typeof finding === 'string') {
53
+ return finding.trim();
54
+ }
55
+ return String(finding.summary || finding.title || finding.rationale || '').trim();
56
+ }
57
+
58
+ export function summarizeTeamRun(teamRun = {}) {
59
+ const findings = Array.isArray(teamRun.findings)
60
+ ? teamRun.findings.map((finding) => summarizeFinding(finding)).filter(Boolean)
61
+ : [];
62
+ return {
63
+ pattern: teamRun.pattern || null,
64
+ verdict: teamRun.verdict || null,
65
+ evidence_bundle: teamRun.evidence_bundle || null,
66
+ findings,
67
+ qa_acceptance_packet: teamRun.qa_acceptance_packet || null,
68
+ workspace_evidence: teamRun.workspace_evidence || null,
69
+ };
70
+ }
71
+
72
+ export function makeArtifactEnvelope({ phase, changeId, missionId = null, phaseRun = null, phaseEntry = null, extra = {} }) {
73
+ const teamRun = phaseEntry?.team_run || {};
74
+ return {
75
+ version: 1,
76
+ phase,
77
+ change_id: changeId,
78
+ mission_id: missionId,
79
+ phase_run_id: phaseRun?.phase_run_id || null,
80
+ generated_at: nowIso(),
81
+ summary: phaseEntry?.summary || phaseRun?.summary || '',
82
+ status: phaseEntry?.effective_status || phaseEntry?.status || phaseRun?.status || 'needs_human',
83
+ team_run: summarizeTeamRun(teamRun),
84
+ phase_health: phaseEntry?.health_check || null,
85
+ source: {
86
+ phase_run_file: phaseRun?.phase_run_file || null,
87
+ deployed_url: phaseRun?.deployed_url || null,
88
+ },
89
+ ...extra,
90
+ };
91
+ }
92
+
93
+ export function renderMarkdownArtifact({ title, phase, envelope }) {
94
+ const lines = [
95
+ `# ${title}`,
96
+ '',
97
+ `- phase: ${phase}`,
98
+ `- change_id: ${envelope.change_id}`,
99
+ `- mission_id: ${envelope.mission_id || ''}`,
100
+ `- phase_run_id: ${envelope.phase_run_id || ''}`,
101
+ `- status: ${envelope.status || ''}`,
102
+ '',
103
+ ];
104
+
105
+ if (envelope.summary) {
106
+ lines.push('## Summary', '', envelope.summary, '');
107
+ }
108
+
109
+ const verdictStatus = envelope.team_run?.verdict?.status;
110
+ const verdictRationale = envelope.team_run?.verdict?.rationale;
111
+ if (verdictStatus || verdictRationale) {
112
+ lines.push('## Verdict', '');
113
+ if (verdictStatus) lines.push(`- status: ${verdictStatus}`);
114
+ if (verdictRationale) lines.push(`- rationale: ${verdictRationale}`);
115
+ lines.push('');
116
+ }
117
+
118
+ if (Array.isArray(envelope.team_run?.findings) && envelope.team_run.findings.length > 0) {
119
+ lines.push('## Findings', '');
120
+ for (const finding of envelope.team_run.findings) {
121
+ lines.push(`- ${finding}`);
122
+ }
123
+ lines.push('');
124
+ }
125
+
126
+ if (envelope.qa_acceptance_packet) {
127
+ lines.push('## QA Acceptance', '');
128
+ lines.push('```json');
129
+ lines.push(JSON.stringify(envelope.qa_acceptance_packet, null, 2));
130
+ lines.push('```', '');
131
+ }
132
+
133
+ return lines.join('\n');
134
+ }