@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,66 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ function nowIso() {
5
+ return new Date().toISOString();
6
+ }
7
+
8
+ export function resolvePendingGatesDir(projectRoot = process.cwd()) {
9
+ return resolve(projectRoot, '.as-xflow', 'pending-gates');
10
+ }
11
+
12
+ function readGateFile(filePath) {
13
+ try {
14
+ const payload = JSON.parse(readFileSync(filePath, 'utf8'));
15
+ return payload && typeof payload === 'object' ? payload : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+
21
+ export function listPendingGates(projectRoot = process.cwd()) {
22
+ const dir = resolvePendingGatesDir(projectRoot);
23
+ if (!existsSync(dir)) {
24
+ return [];
25
+ }
26
+
27
+ return readdirSync(dir)
28
+ .filter((entry) => entry.endsWith('.json'))
29
+ .map((entry) => {
30
+ const gate = readGateFile(resolve(dir, entry));
31
+ return gate ? { ...gate, gate_file: resolve(dir, entry) } : null;
32
+ })
33
+ .filter((gate) => gate && gate.status !== 'approved')
34
+ .sort((left, right) => String(right.created_at || '').localeCompare(String(left.created_at || '')));
35
+ }
36
+
37
+ export function readPendingGate(projectRoot = process.cwd(), phase) {
38
+ const gateFile = resolve(resolvePendingGatesDir(projectRoot), `${phase}.json`);
39
+ if (!existsSync(gateFile)) {
40
+ return null;
41
+ }
42
+ const gate = readGateFile(gateFile);
43
+ return gate ? { ...gate, gate_file: gateFile } : null;
44
+ }
45
+
46
+ export function ackPendingGate(projectRoot = process.cwd(), phase, options = {}) {
47
+ const gateFile = resolve(resolvePendingGatesDir(projectRoot), `${phase}.json`);
48
+ if (!existsSync(gateFile)) {
49
+ throw new Error(`Pending gate not found: ${gateFile}`);
50
+ }
51
+
52
+ const pending = readGateFile(gateFile);
53
+ if (!pending) {
54
+ throw new Error(`Pending gate is invalid JSON: ${gateFile}`);
55
+ }
56
+
57
+ mkdirSync(resolvePendingGatesDir(projectRoot), { recursive: true });
58
+ const approved = {
59
+ ...pending,
60
+ status: 'approved',
61
+ approved_at: nowIso(),
62
+ approved_by: options.approved_by || options.approvedBy || 'control-plane',
63
+ };
64
+ writeFileSync(gateFile, `${JSON.stringify(approved, null, 2)}\n`, 'utf8');
65
+ return { ...approved, gate_file: gateFile };
66
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Phase cost accounting (issue #5 P1-1).
3
+ *
4
+ * A workflow run records one entry per executed phase: how long the phase
5
+ * took and, when an atom reported usage, how many tokens it burned. The
6
+ * report compares the sum of phase durations against the run's wall clock;
7
+ * the two must agree within tolerance (default 5%) or the accounting itself
8
+ * is flagged as incomplete — an overhead number that cannot account for the
9
+ * wall clock is not evidence.
10
+ */
11
+
12
+ export const PHASE_COST_TOLERANCE_PCT = 5;
13
+
14
+ const TOTAL_KEYS = ['total_tokens', 'totalTokens', 'tokens_total', 'tokens'];
15
+ const INPUT_KEYS = ['input_tokens', 'inputTokens', 'prompt_tokens', 'promptTokens'];
16
+ const OUTPUT_KEYS = ['output_tokens', 'outputTokens', 'completion_tokens', 'completionTokens'];
17
+
18
+ function pickNumber(source, keys) {
19
+ if (!source || typeof source !== 'object') return null;
20
+ for (const key of keys) {
21
+ const value = Number(source[key]);
22
+ if (Number.isFinite(value) && value >= 0) return value;
23
+ }
24
+ return null;
25
+ }
26
+
27
+ /**
28
+ * Extract a normalized { input, output, total } token usage from an atom
29
+ * result, looking at the result itself and at common nested containers
30
+ * (`usage`, `token_usage`, `metrics`). Returns null when the atom reported
31
+ * nothing usable.
32
+ */
33
+ export function extractTokenUsage(result) {
34
+ if (!result || typeof result !== 'object') return null;
35
+ const containers = [result, result.usage, result.token_usage, result.metrics];
36
+ for (const container of containers) {
37
+ if (!container || typeof container !== 'object') continue;
38
+ const input = pickNumber(container, INPUT_KEYS);
39
+ const output = pickNumber(container, OUTPUT_KEYS);
40
+ let total = pickNumber(container, TOTAL_KEYS);
41
+ if (total === null && (input !== null || output !== null)) {
42
+ total = (input || 0) + (output || 0);
43
+ }
44
+ if (total !== null || input !== null || output !== null) {
45
+ return { input: input || 0, output: output || 0, total: total || 0 };
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ export function emptyTokenUsage() {
52
+ return { input: 0, output: 0, total: 0 };
53
+ }
54
+
55
+ export function addTokenUsage(accumulator, usage) {
56
+ if (!usage) return accumulator;
57
+ accumulator.input += usage.input || 0;
58
+ accumulator.output += usage.output || 0;
59
+ accumulator.total += usage.total || 0;
60
+ return accumulator;
61
+ }
62
+
63
+ /**
64
+ * Build the per-phase cost report for one workflow run.
65
+ *
66
+ * phases: [{ phase_id, status, duration_ms, tokens? }]
67
+ * wallClockMs: measured wall clock of the whole run.
68
+ */
69
+ export function buildPhaseCostReport({ phases = [], wallClockMs = null, tolerancePct = PHASE_COST_TOLERANCE_PCT, recordedAt = null } = {}) {
70
+ const entries = phases.map((phase) => {
71
+ const durationMs = Number.isFinite(Number(phase.duration_ms)) ? Math.max(0, Math.round(Number(phase.duration_ms))) : 0;
72
+ return {
73
+ phase_id: phase.phase_id || phase.id || 'unknown',
74
+ status: phase.status || 'completed',
75
+ duration_ms: durationMs,
76
+ tokens: phase.tokens
77
+ ? {
78
+ input: phase.tokens.input || 0,
79
+ output: phase.tokens.output || 0,
80
+ total: phase.tokens.total || 0,
81
+ }
82
+ : null,
83
+ };
84
+ });
85
+
86
+ const totalPhaseMs = entries.reduce((sum, entry) => sum + entry.duration_ms, 0);
87
+ const wall = Number.isFinite(Number(wallClockMs)) && Number(wallClockMs) >= 0 ? Math.round(Number(wallClockMs)) : null;
88
+ const unaccountedMs = wall === null ? null : wall - totalPhaseMs;
89
+ const driftPct = wall !== null && wall > 0
90
+ ? Math.round((Math.abs(unaccountedMs) / wall) * 10000) / 100
91
+ : null;
92
+
93
+ for (const entry of entries) {
94
+ entry.duration_share = totalPhaseMs > 0
95
+ ? Math.round((entry.duration_ms / totalPhaseMs) * 10000) / 100
96
+ : 0;
97
+ }
98
+
99
+ const tokens = entries.reduce(
100
+ (acc, entry) => (entry.tokens ? addTokenUsage(acc, entry.tokens) : acc),
101
+ emptyTokenUsage(),
102
+ );
103
+ const anyTokens = entries.some((entry) => entry.tokens && entry.tokens.total > 0);
104
+
105
+ return {
106
+ phases: entries,
107
+ total_phase_ms: totalPhaseMs,
108
+ wall_clock_ms: wall,
109
+ unaccounted_ms: unaccountedMs,
110
+ drift_pct: driftPct,
111
+ tolerance_pct: tolerancePct,
112
+ within_tolerance: driftPct === null ? null : driftPct <= tolerancePct,
113
+ tokens: anyTokens ? tokens : null,
114
+ recorded_at: recordedAt || new Date().toISOString(),
115
+ };
116
+ }
@@ -0,0 +1,27 @@
1
+ import { listPhaseRunSessions } from './mission-state.js';
2
+
3
+ function ensureArray(value) {
4
+ return Array.isArray(value) ? value : [];
5
+ }
6
+
7
+ export function getMissionPhaseRuns(state, missionId) {
8
+ return ensureArray(state.phase_runs)
9
+ .filter((phaseRun) => phaseRun.mission_id === missionId)
10
+ .sort((left, right) => String(right.completed_at || right.started_at || '').localeCompare(String(left.completed_at || left.started_at || '')));
11
+ }
12
+
13
+ export function getLatestPhaseRun(state, missionId) {
14
+ return getMissionPhaseRuns(state, missionId)[0] || null;
15
+ }
16
+
17
+ export function getLatestPhaseEntry(phaseRun, phase) {
18
+ if (!phaseRun || !phase) {
19
+ return null;
20
+ }
21
+ const entries = ensureArray(phaseRun.phases).filter((entry) => entry.phase === phase);
22
+ return entries.sort((left, right) => String(right.updated_at || '').localeCompare(String(left.updated_at || '')))[0] || null;
23
+ }
24
+
25
+ export function getLatestMissionSession(state, missionId) {
26
+ return listPhaseRunSessions(state, missionId)[0] || null;
27
+ }
@@ -0,0 +1,180 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+
4
+ import { resolveChangePath, writeJsonFile } from './change-artifacts.js';
5
+
6
+ const DEFAULT_PROFILE = {
7
+ version: 1,
8
+ execution_mode: 'personal',
9
+ workflow_tracks: {
10
+ personal: 'yolo|corps',
11
+ team: 'yolo|corps',
12
+ },
13
+ };
14
+
15
+ function readJson(path, fallback = null) {
16
+ if (!existsSync(path)) return fallback;
17
+ try {
18
+ const raw = readFileSync(path, 'utf8');
19
+ return raw.trim() ? JSON.parse(raw) : fallback;
20
+ } catch {
21
+ return fallback;
22
+ }
23
+ }
24
+
25
+ function writeJson(path, payload) {
26
+ mkdirSync(dirname(path), { recursive: true });
27
+ writeFileSync(path, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
28
+ return path;
29
+ }
30
+
31
+ export function resolveProfilePath(projectRoot) {
32
+ return resolve(projectRoot, '.xflow', 'profile.json');
33
+ }
34
+
35
+ export function resolveTeamPolicyPath(projectRoot) {
36
+ return resolve(projectRoot, '.xflow', 'team-policy.json');
37
+ }
38
+
39
+ export function readProfile(projectRoot) {
40
+ const profile = readJson(resolveProfilePath(projectRoot), {});
41
+ return mergePolicy(DEFAULT_PROFILE, profile || {});
42
+ }
43
+
44
+ export function writeProfile(projectRoot, patch = {}) {
45
+ const current = readProfile(projectRoot);
46
+ const next = mergePolicy(current, patch);
47
+ writeJson(resolveProfilePath(projectRoot), next);
48
+ return next;
49
+ }
50
+
51
+ export function readTeamPolicy(projectRoot) {
52
+ return readJson(resolveTeamPolicyPath(projectRoot), null);
53
+ }
54
+
55
+ export function validateTeamPolicy(policy = {}) {
56
+ const errors = [];
57
+ const coverage = policy?.coding?.min_changed_line_coverage;
58
+ if (coverage !== undefined && (!Number.isFinite(Number(coverage)) || Number(coverage) < 0 || Number(coverage) > 100)) {
59
+ errors.push('coding.min_changed_line_coverage must be a number between 0 and 100');
60
+ }
61
+ const reviewers = policy?.review?.min_reviewers;
62
+ if (reviewers !== undefined && (!Number.isFinite(Number(reviewers)) || Number(reviewers) < 0)) {
63
+ errors.push('review.min_reviewers must be a non-negative number');
64
+ }
65
+ const smoke = policy?.deploy?.required_smoke_pass_rate;
66
+ if (smoke !== undefined && (!Number.isFinite(Number(smoke)) || Number(smoke) < 0 || Number(smoke) > 100)) {
67
+ errors.push('deploy.required_smoke_pass_rate must be a number between 0 and 100');
68
+ }
69
+ return { ok: errors.length === 0, errors };
70
+ }
71
+
72
+ export function mergePolicy(personal = {}, team = {}) {
73
+ const merged = mergeValue(personal || {}, team || {});
74
+ merged.workflow_tracks = {
75
+ personal: 'yolo|corps',
76
+ team: 'yolo|corps',
77
+ ...(merged.workflow_tracks || {}),
78
+ };
79
+ return merged;
80
+ }
81
+
82
+ function mergeValue(personal, team) {
83
+ if (team === undefined || team === null) return cloneValue(personal);
84
+ if (personal === undefined || personal === null) return cloneValue(team);
85
+
86
+ if (Array.isArray(personal) || Array.isArray(team)) {
87
+ return mergeLists(team, personal);
88
+ }
89
+ if (typeof personal === 'number' || typeof team === 'number') {
90
+ const personalNumber = Number(personal);
91
+ const teamNumber = Number(team);
92
+ if (Number.isFinite(personalNumber) && Number.isFinite(teamNumber)) {
93
+ return Math.max(personalNumber, teamNumber);
94
+ }
95
+ return cloneValue(team);
96
+ }
97
+ if (isPlainObject(personal) && isPlainObject(team)) {
98
+ const result = {};
99
+ for (const key of new Set([...Object.keys(personal), ...Object.keys(team)])) {
100
+ result[key] = mergeValue(personal[key], team[key]);
101
+ }
102
+ return result;
103
+ }
104
+ return cloneValue(team);
105
+ }
106
+
107
+ function mergeLists(teamList, personalList) {
108
+ const values = [
109
+ ...(Array.isArray(teamList) ? teamList : [teamList]),
110
+ ...(Array.isArray(personalList) ? personalList : [personalList]),
111
+ ].filter((value) => value !== undefined && value !== null);
112
+ const seen = new Set();
113
+ const merged = [];
114
+ for (const value of values) {
115
+ const key = JSON.stringify(value);
116
+ if (seen.has(key)) continue;
117
+ seen.add(key);
118
+ merged.push(cloneValue(value));
119
+ }
120
+ return merged;
121
+ }
122
+
123
+ function isPlainObject(value) {
124
+ return value && typeof value === 'object' && !Array.isArray(value);
125
+ }
126
+
127
+ function cloneValue(value) {
128
+ if (value === undefined) return undefined;
129
+ return JSON.parse(JSON.stringify(value));
130
+ }
131
+
132
+ export function resolveEffectivePolicy({ projectRoot, changeId = null, executionMode = null, writeArtifact = true } = {}) {
133
+ const profile = readProfile(projectRoot);
134
+ if (executionMode) {
135
+ profile.execution_mode = executionMode;
136
+ }
137
+
138
+ const mode = profile.execution_mode || 'personal';
139
+ const teamPolicy = readTeamPolicy(projectRoot);
140
+ if (mode === 'team' && !teamPolicy) {
141
+ return {
142
+ ok: false,
143
+ error: {
144
+ code: 'team_policy_missing',
145
+ message: 'execution_mode=team requires .xflow/team-policy.json',
146
+ },
147
+ profile,
148
+ policy: null,
149
+ };
150
+ }
151
+
152
+ const validation = teamPolicy ? validateTeamPolicy(teamPolicy) : { ok: true, errors: [] };
153
+ if (!validation.ok) {
154
+ return {
155
+ ok: false,
156
+ error: {
157
+ code: 'team_policy_invalid',
158
+ message: 'team policy failed validation',
159
+ errors: validation.errors,
160
+ },
161
+ profile,
162
+ policy: null,
163
+ };
164
+ }
165
+
166
+ const policy = mode === 'team' ? mergePolicy(profile, teamPolicy) : mergePolicy(DEFAULT_PROFILE, profile);
167
+ policy.execution_mode = mode;
168
+ policy.generated_at = new Date().toISOString();
169
+ policy.source = {
170
+ profile: resolveProfilePath(projectRoot),
171
+ team_policy: teamPolicy ? resolveTeamPolicyPath(projectRoot) : null,
172
+ };
173
+ policy.authority = 'policy_overlay_only_workflows_remain_yolo_corps';
174
+
175
+ const result = { ok: true, profile, team_policy: teamPolicy, policy };
176
+ if (writeArtifact && changeId) {
177
+ result.policy_effective_file = writeJsonFile(resolveChangePath(projectRoot, changeId, 'policy_effective.json'), policy);
178
+ }
179
+ return result;
180
+ }
@@ -0,0 +1,201 @@
1
+ import { mkdir } from 'node:fs/promises';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+
5
+ export const DEFAULT_STATE = {
6
+ version: 1,
7
+ missions: [],
8
+ tasks: [],
9
+ events: [],
10
+ memories: [],
11
+ gate_decisions: [],
12
+ interventions: [],
13
+ phase_runs: [],
14
+ phase_run_sessions: [],
15
+ reruns: [],
16
+ operator_actions: [],
17
+ mission_controls: [],
18
+ };
19
+
20
+ const mutationQueue = new Map();
21
+ const SNAPSHOT_RETENTION_LIMIT = 200;
22
+
23
+ function sqlitePathFor(requestedPath) {
24
+ if (requestedPath.endsWith('.sqlite') || requestedPath.endsWith('.db')) {
25
+ return requestedPath;
26
+ }
27
+ if (requestedPath.endsWith('.json')) {
28
+ return requestedPath.replace(/\.json$/u, '.sqlite');
29
+ }
30
+ return `${requestedPath}.sqlite`;
31
+ }
32
+
33
+ function nowIso() {
34
+ return new Date().toISOString();
35
+ }
36
+
37
+ function mergeState(parsed = {}) {
38
+ return {
39
+ ...structuredClone(DEFAULT_STATE),
40
+ ...parsed,
41
+ missions: parsed.missions ?? [],
42
+ tasks: parsed.tasks ?? [],
43
+ events: parsed.events ?? [],
44
+ memories: parsed.memories ?? [],
45
+ gate_decisions: parsed.gate_decisions ?? [],
46
+ interventions: parsed.interventions ?? [],
47
+ phase_runs: parsed.phase_runs ?? [],
48
+ phase_run_sessions: parsed.phase_run_sessions ?? [],
49
+ reruns: parsed.reruns ?? [],
50
+ operator_actions: parsed.operator_actions ?? [],
51
+ mission_controls: parsed.mission_controls ?? [],
52
+ };
53
+ }
54
+
55
+ function openStateDatabase(file) {
56
+ const database = new DatabaseSync(file);
57
+ database.exec(`
58
+ PRAGMA journal_mode = WAL;
59
+ PRAGMA busy_timeout = 5000;
60
+ CREATE TABLE IF NOT EXISTS meta (
61
+ key TEXT PRIMARY KEY,
62
+ value TEXT NOT NULL
63
+ );
64
+ CREATE TABLE IF NOT EXISTS current_state (
65
+ id INTEGER PRIMARY KEY CHECK (id = 1),
66
+ state_json TEXT NOT NULL,
67
+ updated_at TEXT NOT NULL
68
+ );
69
+ CREATE TABLE IF NOT EXISTS state_snapshots (
70
+ snapshot_id INTEGER PRIMARY KEY AUTOINCREMENT,
71
+ state_json TEXT NOT NULL,
72
+ created_at TEXT NOT NULL
73
+ );
74
+ `);
75
+ return database;
76
+ }
77
+
78
+ function readCurrentStateRow(database) {
79
+ return database.prepare('SELECT state_json FROM current_state WHERE id = 1').get() || null;
80
+ }
81
+
82
+ function writeStateSnapshot(database, state, timestamp = nowIso()) {
83
+ const serialized = JSON.stringify(state, null, 2);
84
+ database.prepare(`
85
+ INSERT INTO current_state (id, state_json, updated_at)
86
+ VALUES (1, ?, ?)
87
+ ON CONFLICT(id) DO UPDATE SET
88
+ state_json = excluded.state_json,
89
+ updated_at = excluded.updated_at
90
+ `).run(serialized, timestamp);
91
+ database.prepare('INSERT INTO state_snapshots (state_json, created_at) VALUES (?, ?)').run(serialized, timestamp);
92
+ database.prepare(`
93
+ DELETE FROM state_snapshots
94
+ WHERE snapshot_id NOT IN (
95
+ SELECT snapshot_id
96
+ FROM state_snapshots
97
+ ORDER BY snapshot_id DESC
98
+ LIMIT ?
99
+ )
100
+ `).run(SNAPSHOT_RETENTION_LIMIT);
101
+ database.prepare(`
102
+ INSERT INTO meta (key, value)
103
+ VALUES ('store_backend', 'sqlite')
104
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
105
+ `).run();
106
+ }
107
+
108
+ function initializeDatabase(file) {
109
+ const database = openStateDatabase(file);
110
+ try {
111
+ if (!readCurrentStateRow(database)) {
112
+ writeStateSnapshot(database, structuredClone(DEFAULT_STATE));
113
+ }
114
+ } finally {
115
+ database.close();
116
+ }
117
+ }
118
+
119
+ export function resolveDataFile({ projectRoot = process.cwd(), explicitPath = null } = {}) {
120
+ const requested = explicitPath || process.env.AS_XFLOW_DATA_FILE || resolve(projectRoot, '.as-xflow', 'state.sqlite');
121
+ return sqlitePathFor(requested);
122
+ }
123
+
124
+ export async function ensureStore(dataFile = resolveDataFile()) {
125
+ const file = resolveDataFile({ explicitPath: dataFile });
126
+ await mkdir(dirname(file), { recursive: true });
127
+ initializeDatabase(file);
128
+ return file;
129
+ }
130
+
131
+ export async function readStore(dataFile = resolveDataFile()) {
132
+ const file = await ensureStore(dataFile);
133
+ const database = openStateDatabase(file);
134
+ try {
135
+ const row = readCurrentStateRow(database);
136
+ if (!row) {
137
+ return structuredClone(DEFAULT_STATE);
138
+ }
139
+ const parsed = row.state_json.trim() ? JSON.parse(row.state_json) : structuredClone(DEFAULT_STATE);
140
+ return mergeState(parsed);
141
+ } finally {
142
+ database.close();
143
+ }
144
+ }
145
+
146
+ export async function writeStore(state, dataFile = resolveDataFile()) {
147
+ const file = await ensureStore(dataFile);
148
+ const database = openStateDatabase(file);
149
+ try {
150
+ database.exec('BEGIN IMMEDIATE');
151
+ writeStateSnapshot(database, state);
152
+ database.exec('COMMIT');
153
+ } catch (error) {
154
+ try {
155
+ database.exec('ROLLBACK');
156
+ } catch {
157
+ // Ignore rollback failures; the original error is the useful one.
158
+ }
159
+ throw error;
160
+ } finally {
161
+ database.close();
162
+ }
163
+ }
164
+
165
+ export async function mutateStore(mutator, dataFile = resolveDataFile()) {
166
+ const file = resolveDataFile({ explicitPath: dataFile });
167
+ const previous = mutationQueue.get(file) ?? Promise.resolve();
168
+ const current = previous.then(async () => {
169
+ await ensureStore(file);
170
+ const database = openStateDatabase(file);
171
+ try {
172
+ database.exec('BEGIN IMMEDIATE');
173
+ const row = readCurrentStateRow(database);
174
+ const parsed = row?.state_json?.trim() ? JSON.parse(row.state_json) : structuredClone(DEFAULT_STATE);
175
+ const state = mergeState(parsed);
176
+ const result = await mutator(state);
177
+ writeStateSnapshot(database, state);
178
+ database.exec('COMMIT');
179
+ return result;
180
+ } catch (error) {
181
+ try {
182
+ database.exec('ROLLBACK');
183
+ } catch {
184
+ // Ignore rollback failures; the original error is the useful one.
185
+ }
186
+ throw error;
187
+ } finally {
188
+ database.close();
189
+ }
190
+ });
191
+
192
+ mutationQueue.set(file, current);
193
+
194
+ try {
195
+ return await current;
196
+ } finally {
197
+ if (mutationQueue.get(file) === current) {
198
+ mutationQueue.delete(file);
199
+ }
200
+ }
201
+ }