@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,780 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { 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
+ function ensureArray(value) {
14
+ if (Array.isArray(value)) {
15
+ return value.filter((item) => item !== undefined && item !== null && item !== '');
16
+ }
17
+ if (value === undefined || value === null || value === '') {
18
+ return [];
19
+ }
20
+ return [value];
21
+ }
22
+
23
+ function unique(values) {
24
+ return [...new Set(ensureArray(values))];
25
+ }
26
+
27
+ function cleanObject(value) {
28
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
29
+ return {};
30
+ }
31
+ return Object.fromEntries(Object.entries(value).filter(([, entryValue]) => entryValue !== undefined));
32
+ }
33
+
34
+ function normalizeParallelBudget(value) {
35
+ if (value === undefined || value === null || value === '') {
36
+ return null;
37
+ }
38
+ const parsed = Number(value);
39
+ if (!Number.isFinite(parsed) || parsed < 1) {
40
+ throw new Error(`Invalid parallel budget override: ${String(value)}`);
41
+ }
42
+ return Math.floor(parsed);
43
+ }
44
+
45
+ function normalizeMissionType(value) {
46
+ const allowed = [
47
+ 'primary_execution',
48
+ 'cleanup_followup',
49
+ 'hygiene_followup',
50
+ 'gate_rerun',
51
+ 'verification',
52
+ ];
53
+ return allowed.includes(value) ? value : 'primary_execution';
54
+ }
55
+
56
+ function resolveWorkspaceRoot(repoRef) {
57
+ const trimmed = String(repoRef || '').trim();
58
+ if (!trimmed) {
59
+ return null;
60
+ }
61
+
62
+ const absolute = resolve(trimmed);
63
+ return existsSync(absolute) ? absolute : null;
64
+ }
65
+
66
+ function listCurrentGateDecisions(state, missionId = null) {
67
+ return ensureArray(state.gate_decisions).filter((decision) => {
68
+ if (decision.is_current === false) {
69
+ return false;
70
+ }
71
+ return missionId ? decision.mission_id === missionId || decision.target_id === missionId : true;
72
+ });
73
+ }
74
+
75
+ export function computeMissionStatus(state, missionId, fallback = 'running') {
76
+ const tasks = ensureArray(state.tasks).filter((task) => task.mission_id === missionId);
77
+ const gates = listCurrentGateDecisions(state, missionId);
78
+ const reruns = ensureArray(state.reruns).filter((rerun) => rerun.mission_id === missionId);
79
+
80
+ if (tasks.some((task) => task.status === 'waiting_human')) return 'needs_human';
81
+ if (gates.some((gate) => gate.result === 'needs_human')) return 'needs_human';
82
+ if (reruns.some((rerun) => ['requested', 'running', 'cancel_requested'].includes(rerun.status))) return 'running';
83
+ if (tasks.some((task) => task.status === 'waiting_gate')) return 'gated';
84
+ if (gates.some((gate) => gate.result === 'fail' || gate.decision_status === 'rerun_requested')) return 'gated';
85
+ if (reruns.some((rerun) => rerun.status === 'failed')) return 'blocked';
86
+ if (reruns.some((rerun) => rerun.status === 'canceled')) return 'recovering';
87
+ if (tasks.some((task) => task.status === 'failed')) return 'blocked';
88
+ if (tasks.some((task) => task.status === 'retrying')) return 'recovering';
89
+ if (tasks.some((task) => ['running', 'ready', 'queued', 'waiting_dependency'].includes(task.status))) return 'running';
90
+ if (tasks.length > 0 && tasks.every((task) => ['passed', 'canceled'].includes(task.status))) {
91
+ return fallback === 'archived' ? 'archived' : 'completed';
92
+ }
93
+ return fallback;
94
+ }
95
+
96
+ function requireMission(state, missionId) {
97
+ const mission = ensureArray(state.missions).find((entry) => entry.mission_id === missionId);
98
+ if (!mission) {
99
+ throw new Error(`Mission not found: ${missionId}`);
100
+ }
101
+ return mission;
102
+ }
103
+
104
+ function ensureStateCollection(state, key) {
105
+ state[key] = ensureArray(state[key]);
106
+ return state[key];
107
+ }
108
+
109
+ function listMissionScoped(state, key, missionId) {
110
+ return ensureArray(state[key]).filter((entry) => entry.mission_id === missionId);
111
+ }
112
+
113
+ function derivePhaseRunSessionStatus(session, timestamp = nowIso()) {
114
+ if (!session || typeof session !== 'object') {
115
+ return 'unknown';
116
+ }
117
+ if (session.status !== 'running') {
118
+ return session.status || 'unknown';
119
+ }
120
+ if (!session.timeout_at) {
121
+ return 'running';
122
+ }
123
+ return String(session.timeout_at).localeCompare(timestamp) < 0 ? 'orphaned' : 'running';
124
+ }
125
+
126
+ function enrichPhaseRunSession(session, timestamp = nowIso()) {
127
+ return {
128
+ ...session,
129
+ derived_status: derivePhaseRunSessionStatus(session, timestamp),
130
+ };
131
+ }
132
+
133
+ function upsertPhaseRunSession(state, nextSession) {
134
+ state.phase_run_sessions = ensureArray(state.phase_run_sessions);
135
+ const existingIndex = state.phase_run_sessions.findIndex((entry) => entry.session_id === nextSession.session_id);
136
+ if (existingIndex >= 0) {
137
+ state.phase_run_sessions[existingIndex] = nextSession;
138
+ } else {
139
+ state.phase_run_sessions.unshift(nextSession);
140
+ }
141
+ return nextSession;
142
+ }
143
+
144
+ export function listPhaseRunSessions(state, missionId = null) {
145
+ const timestamp = nowIso();
146
+ const sessions = missionId
147
+ ? ensureArray(state.phase_run_sessions).filter((entry) => entry.mission_id === missionId)
148
+ : ensureArray(state.phase_run_sessions);
149
+
150
+ return sessions
151
+ .map((session) => enrichPhaseRunSession(session, timestamp))
152
+ .sort((left, right) => String(right.updated_at || right.started_at).localeCompare(String(left.updated_at || left.started_at)));
153
+ }
154
+
155
+ export function findPhaseRunSession(state, missionId, sessionId = null) {
156
+ if (sessionId) {
157
+ return ensureArray(state.phase_run_sessions).find(
158
+ (entry) => entry.mission_id === missionId && entry.session_id === sessionId,
159
+ ) || null;
160
+ }
161
+
162
+ const sessions = ensureArray(state.phase_run_sessions)
163
+ .filter((entry) => entry.mission_id === missionId)
164
+ .sort((left, right) => String(right.updated_at || right.started_at).localeCompare(String(left.updated_at || left.started_at)));
165
+
166
+ return sessions.find((entry) => entry.status === 'running')
167
+ || sessions[0]
168
+ || null;
169
+ }
170
+
171
+ function buildCurrentPhase(phaseRuns = []) {
172
+ const latestPhaseRun = phaseRuns[0] || null;
173
+ if (!latestPhaseRun) {
174
+ return null;
175
+ }
176
+ return ensureArray(latestPhaseRun.executed_phases).slice(-1)[0]
177
+ || ensureArray(latestPhaseRun.phases).slice(-1)[0]?.phase
178
+ || null;
179
+ }
180
+
181
+ function buildPhaseOverview(phaseRuns = []) {
182
+ const grouped = new Map();
183
+
184
+ for (const phaseRun of phaseRuns) {
185
+ for (const phase of ensureArray(phaseRun.phases)) {
186
+ const current = grouped.get(phase.phase) || [];
187
+ current.push({
188
+ phase_run_id: phaseRun.phase_run_id,
189
+ phase,
190
+ status: phase.status,
191
+ effective_status: phase.effective_status || phase.status,
192
+ updated_at: phase.updated_at || phaseRun.completed_at || phaseRun.updated_at || phaseRun.started_at,
193
+ });
194
+ grouped.set(phase.phase, current);
195
+ }
196
+ }
197
+
198
+ return [...grouped.entries()].map(([phase, entries]) => {
199
+ const latest = [...entries].sort((left, right) => String(right.updated_at).localeCompare(String(left.updated_at)))[0];
200
+ return {
201
+ phase,
202
+ latest_result: latest.effective_status,
203
+ latest_status: latest.status,
204
+ latest_phase_run_id: latest.phase_run_id,
205
+ updated_at: latest.updated_at,
206
+ };
207
+ });
208
+ }
209
+
210
+ export function createMission(state, request = {}) {
211
+ const timestamp = nowIso();
212
+ const goal = String(request.goal ?? '').trim();
213
+ if (!goal) {
214
+ throw new Error('Mission goal is required');
215
+ }
216
+
217
+ const missionType = normalizeMissionType(request.mission_type);
218
+ const epicId = String(request.epic_id || makeId('epc'));
219
+ const epicGoal = String(request.epic_goal || goal).trim() || goal;
220
+ const repoRef = String(request.repo_ref || request.repoRef || request.workspace || '').trim() || null;
221
+
222
+ const mission = {
223
+ mission_id: makeId('mis'),
224
+ goal,
225
+ epic_id: epicId,
226
+ epic_goal: epicGoal,
227
+ mission_type: missionType,
228
+ workspace_id: request.workspace_id || request.workspaceId || 'default-workspace',
229
+ repo_ref: repoRef,
230
+ workspace_root: resolveWorkspaceRoot(repoRef),
231
+ intake_snapshot: request.intake_snapshot ?? null,
232
+ constraints: cleanObject(request.constraints ?? {}),
233
+ runtime_context: cleanObject(request.runtime_context ?? {}),
234
+ requested_gates: unique(request.requested_gates ?? []),
235
+ priority: request.priority || 'normal',
236
+ status: 'running',
237
+ success_criteria: ensureArray(request.success_criteria),
238
+ human_owner: request.human_owner || null,
239
+ created_at: timestamp,
240
+ updated_at: timestamp,
241
+ };
242
+
243
+ state.missions.unshift(mission);
244
+
245
+ const startEvent = {
246
+ event_id: makeId('evt'),
247
+ mission_id: mission.mission_id,
248
+ task_id: null,
249
+ type: 'mission_started',
250
+ timestamp,
251
+ payload: {
252
+ goal: mission.goal,
253
+ requested_gates: mission.requested_gates,
254
+ },
255
+ runtime_context: mission.runtime_context,
256
+ };
257
+
258
+ state.events.push(startEvent);
259
+
260
+ return {
261
+ mission,
262
+ replay_event: startEvent,
263
+ };
264
+ }
265
+
266
+ export function ingestEvent(state, input = {}) {
267
+ const mission = requireMission(state, input.mission_id);
268
+ const timestamp = nowIso();
269
+ const payload = cleanObject(input.payload ?? {});
270
+ const event = {
271
+ event_id: makeId('evt'),
272
+ mission_id: mission.mission_id,
273
+ task_id: input.task_id || payload.task?.task_id || null,
274
+ type: String(input.type || '').trim() || 'event',
275
+ timestamp,
276
+ payload,
277
+ runtime_context: cleanObject(input.runtime_context ?? mission.runtime_context),
278
+ };
279
+
280
+ state.events.push(event);
281
+
282
+ if (event.type === 'task_started' && payload.task?.task_id) {
283
+ state.tasks = ensureArray(state.tasks);
284
+ const existingIndex = state.tasks.findIndex((task) => task.task_id === payload.task.task_id);
285
+ const nextTask = {
286
+ task_id: payload.task.task_id,
287
+ mission_id: mission.mission_id,
288
+ type: payload.task.type || 'task',
289
+ status: 'running',
290
+ input: cleanObject(payload.task.input),
291
+ expected_output: payload.task.expected_output || null,
292
+ owner_agent_id: payload.task.owner_agent_id || null,
293
+ checkpoint_ref: payload.task.checkpoint_ref || null,
294
+ artifacts: ensureArray(payload.task.artifacts),
295
+ created_at: existingIndex >= 0 ? state.tasks[existingIndex].created_at : timestamp,
296
+ updated_at: timestamp,
297
+ };
298
+ if (existingIndex >= 0) {
299
+ state.tasks[existingIndex] = { ...state.tasks[existingIndex], ...nextTask };
300
+ } else {
301
+ state.tasks.unshift(nextTask);
302
+ }
303
+ }
304
+
305
+ if (event.task_id && ['critical_path_succeeded', 'command_failed', 'gate_result'].includes(event.type)) {
306
+ const task = ensureArray(state.tasks).find((entry) => entry.task_id === event.task_id);
307
+ if (task) {
308
+ task.status = event.type === 'command_failed'
309
+ ? 'failed'
310
+ : event.type === 'gate_result' && payload.result !== 'pass'
311
+ ? 'waiting_gate'
312
+ : 'passed';
313
+ task.updated_at = timestamp;
314
+ }
315
+ }
316
+
317
+ let gateDecision = null;
318
+ if (event.type === 'gate_result') {
319
+ state.gate_decisions = ensureArray(state.gate_decisions);
320
+ gateDecision = {
321
+ gate_id: makeId('gate'),
322
+ mission_id: mission.mission_id,
323
+ target_id: mission.mission_id,
324
+ task_id: event.task_id,
325
+ gate_type: payload.gate_type || 'gate',
326
+ result: payload.result || 'needs_human',
327
+ original_result: payload.result || 'needs_human',
328
+ rationale: payload.rationale || '',
329
+ evidence_refs: ensureArray(payload.evidence_refs),
330
+ generated_actions: ensureArray(payload.generated_actions),
331
+ structured_payload: cleanObject(payload.structured_payload),
332
+ decision_status: 'active',
333
+ override_count: 0,
334
+ rerun_requests: 0,
335
+ is_current: true,
336
+ history: [],
337
+ created_at: timestamp,
338
+ updated_at: timestamp,
339
+ };
340
+ state.gate_decisions.push(gateDecision);
341
+ }
342
+
343
+ mission.updated_at = timestamp;
344
+ mission.status = computeMissionStatus(state, mission.mission_id, mission.status);
345
+
346
+ return {
347
+ event,
348
+ mission,
349
+ task: event.task_id ? ensureArray(state.tasks).find((entry) => entry.task_id === event.task_id) || null : null,
350
+ gate_decision: gateDecision,
351
+ };
352
+ }
353
+
354
+ export function createIntervention(state, missionId, input = {}) {
355
+ const mission = requireMission(state, missionId);
356
+ const timestamp = nowIso();
357
+ const intervention = {
358
+ intervention_id: makeId('int'),
359
+ mission_id: mission.mission_id,
360
+ action: input.action || 'note',
361
+ note: input.note || '',
362
+ requested_by: input.requested_by || 'human:control-plane',
363
+ memory_ids: unique(input.memory_ids ?? []),
364
+ metadata: cleanObject(input.metadata),
365
+ status: 'applied',
366
+ created_at: timestamp,
367
+ updated_at: timestamp,
368
+ };
369
+
370
+ state.interventions = ensureArray(state.interventions);
371
+ state.interventions.push(intervention);
372
+
373
+ const event = {
374
+ event_id: makeId('evt'),
375
+ mission_id: mission.mission_id,
376
+ task_id: null,
377
+ type: 'human_intervened',
378
+ timestamp,
379
+ payload: {
380
+ action: intervention.action,
381
+ note: intervention.note,
382
+ requested_by: intervention.requested_by,
383
+ },
384
+ runtime_context: mission.runtime_context,
385
+ };
386
+
387
+ state.events.push(event);
388
+ mission.updated_at = timestamp;
389
+
390
+ return {
391
+ intervention,
392
+ event,
393
+ mission,
394
+ };
395
+ }
396
+
397
+ export function listOperatorActions(state, missionId = null) {
398
+ const entries = missionId ? listMissionScoped(state, 'operator_actions', missionId) : ensureArray(state.operator_actions);
399
+ return [...entries].sort((left, right) => String(right.updated_at || right.created_at || '').localeCompare(String(left.updated_at || left.created_at || '')));
400
+ }
401
+
402
+ export function createOperatorAction(state, missionId, input = {}) {
403
+ const mission = requireMission(state, missionId);
404
+ const timestamp = nowIso();
405
+ const action = {
406
+ action_id: makeId('act'),
407
+ mission_id: missionId,
408
+ kind: input.kind || input.action || 'operator_action',
409
+ target_type: input.target_type || 'mission',
410
+ target_id: input.target_id || missionId,
411
+ payload: cleanObject(input.payload),
412
+ note: input.note || '',
413
+ requested_by: input.requested_by || 'human:control-plane',
414
+ status: input.status || 'applied',
415
+ created_at: timestamp,
416
+ updated_at: timestamp,
417
+ };
418
+
419
+ ensureStateCollection(state, 'operator_actions').push(action);
420
+ state.events.push({
421
+ event_id: makeId('evt'),
422
+ mission_id: missionId,
423
+ task_id: null,
424
+ type: 'operator_action_applied',
425
+ timestamp,
426
+ payload: {
427
+ kind: action.kind,
428
+ target_type: action.target_type,
429
+ target_id: action.target_id,
430
+ status: action.status,
431
+ },
432
+ runtime_context: mission.runtime_context,
433
+ });
434
+ mission.updated_at = timestamp;
435
+ return action;
436
+ }
437
+
438
+ export function getMissionControl(state, missionId) {
439
+ requireMission(state, missionId);
440
+ return ensureArray(state.mission_controls).find((entry) => entry.mission_id === missionId) || null;
441
+ }
442
+
443
+ export function setMissionControl(state, missionId, input = {}) {
444
+ const mission = requireMission(state, missionId);
445
+ const timestamp = nowIso();
446
+ const next = {
447
+ mission_id: missionId,
448
+ parallel_budget_override: normalizeParallelBudget(input.parallel_budget_override),
449
+ note: input.note || '',
450
+ updated_by: input.updated_by || input.requested_by || 'human:control-plane',
451
+ updated_at: timestamp,
452
+ };
453
+ ensureStateCollection(state, 'mission_controls');
454
+ const existingIndex = state.mission_controls.findIndex((entry) => entry.mission_id === missionId);
455
+ if (existingIndex >= 0) {
456
+ state.mission_controls[existingIndex] = { ...state.mission_controls[existingIndex], ...next };
457
+ } else {
458
+ state.mission_controls.unshift(next);
459
+ }
460
+ mission.updated_at = timestamp;
461
+ return state.mission_controls[existingIndex >= 0 ? existingIndex : 0];
462
+ }
463
+
464
+ export function listReruns(state, missionId = null) {
465
+ const entries = missionId ? listMissionScoped(state, 'reruns', missionId) : ensureArray(state.reruns);
466
+ return [...entries].sort((left, right) => String(right.updated_at || right.requested_at || '').localeCompare(String(left.updated_at || left.requested_at || '')));
467
+ }
468
+
469
+ export function getRerun(state, missionId, rerunId) {
470
+ requireMission(state, missionId);
471
+ return ensureArray(state.reruns).find((entry) => entry.mission_id === missionId && entry.rerun_id === rerunId) || null;
472
+ }
473
+
474
+ export function createRerun(state, missionId, input = {}) {
475
+ const mission = requireMission(state, missionId);
476
+ const timestamp = nowIso();
477
+ const rerun = {
478
+ rerun_id: makeId('rrn'),
479
+ mission_id: missionId,
480
+ source_rerun_id: input.source_rerun_id || null,
481
+ workflow_path: input.workflow_path || null,
482
+ project_root: input.project_root || mission.workspace_root || mission.repo_ref || null,
483
+ change_id: input.change_id || null,
484
+ phase_id: input.phase_id || null,
485
+ atom_id: input.atom_id || null,
486
+ mode: input.atom_id ? 'atom' : 'phase',
487
+ continue_strategy: input.continue_strategy || 'phase_forward',
488
+ requested_by: input.requested_by || 'human:control-plane',
489
+ request_note: input.request_note || input.note || '',
490
+ status: input.status || 'requested',
491
+ validation: cleanObject(input.validation),
492
+ budget_override: normalizeParallelBudget(input.budget_override),
493
+ action_id: input.action_id || null,
494
+ run_key: input.run_key || null,
495
+ workflow_run: cleanObject(input.workflow_run),
496
+ error: input.error || null,
497
+ requested_at: timestamp,
498
+ started_at: input.started_at || null,
499
+ finished_at: input.finished_at || null,
500
+ updated_at: timestamp,
501
+ };
502
+ ensureStateCollection(state, 'reruns').unshift(rerun);
503
+ mission.updated_at = timestamp;
504
+ return rerun;
505
+ }
506
+
507
+ export function updateRerun(state, missionId, rerunId, patch = {}) {
508
+ const mission = requireMission(state, missionId);
509
+ ensureStateCollection(state, 'reruns');
510
+ const index = state.reruns.findIndex((entry) => entry.mission_id === missionId && entry.rerun_id === rerunId);
511
+ if (index < 0) {
512
+ throw new Error(`Rerun not found: ${rerunId}`);
513
+ }
514
+ const current = state.reruns[index];
515
+ const next = {
516
+ ...current,
517
+ ...cleanObject(patch),
518
+ budget_override: patch.budget_override === undefined ? current.budget_override : normalizeParallelBudget(patch.budget_override),
519
+ workflow_run: patch.workflow_run === undefined ? current.workflow_run : cleanObject(patch.workflow_run),
520
+ validation: patch.validation === undefined ? current.validation : cleanObject(patch.validation),
521
+ updated_at: nowIso(),
522
+ };
523
+ state.reruns[index] = next;
524
+ mission.updated_at = next.updated_at;
525
+ return next;
526
+ }
527
+
528
+ function buildMissionTimeline({ replay = [], interventions = [], operatorActions = [], reruns = [], phaseRuns = [], phaseRunSessions = [] }) {
529
+ const entries = [];
530
+
531
+ for (const event of replay) {
532
+ entries.push({ kind: 'event', timestamp: event.timestamp || null, data: event });
533
+ }
534
+ for (const intervention of interventions) {
535
+ entries.push({ kind: 'intervention', timestamp: intervention.updated_at || intervention.created_at || null, data: intervention });
536
+ }
537
+ for (const action of operatorActions) {
538
+ entries.push({ kind: 'operator_action', timestamp: action.updated_at || action.created_at || null, data: action });
539
+ }
540
+ for (const rerun of reruns) {
541
+ entries.push({ kind: 'rerun', timestamp: rerun.updated_at || rerun.requested_at || null, data: rerun });
542
+ }
543
+ for (const phaseRun of phaseRuns) {
544
+ entries.push({ kind: 'phase_run', timestamp: phaseRun.updated_at || phaseRun.completed_at || phaseRun.started_at || null, data: phaseRun });
545
+ }
546
+ for (const session of phaseRunSessions) {
547
+ entries.push({ kind: 'phase_run_session', timestamp: session.updated_at || session.completed_at || session.started_at || null, data: session });
548
+ }
549
+
550
+ return entries.sort((left, right) => String(right.timestamp || '').localeCompare(String(left.timestamp || '')));
551
+ }
552
+
553
+ export function listMissions(state) {
554
+ return ensureArray(state.missions)
555
+ .map((mission) => ({
556
+ ...mission,
557
+ status: computeMissionStatus(state, mission.mission_id, mission.status),
558
+ }))
559
+ .sort((left, right) => String(right.updated_at || right.created_at).localeCompare(String(left.updated_at || left.created_at)));
560
+ }
561
+
562
+ export function getMissionDetail(state, missionId) {
563
+ const mission = requireMission(state, missionId);
564
+ const gateDecisions = ensureArray(state.gate_decisions)
565
+ .filter((decision) => decision.mission_id === missionId || decision.target_id === missionId)
566
+ .sort((left, right) => String(right.updated_at || right.created_at || '').localeCompare(String(left.updated_at || left.created_at || '')));
567
+ const phaseRuns = ensureArray(state.phase_runs)
568
+ .filter((phaseRun) => phaseRun.mission_id === missionId)
569
+ .sort((left, right) => String(right.completed_at || right.started_at).localeCompare(String(left.completed_at || left.started_at)));
570
+ const phaseRunSessions = listPhaseRunSessions(state, missionId);
571
+ const replay = ensureArray(state.events)
572
+ .filter((event) => event.mission_id === missionId)
573
+ .sort((left, right) => String(left.timestamp || '').localeCompare(String(right.timestamp || '')));
574
+ const interventions = ensureArray(state.interventions)
575
+ .filter((intervention) => intervention.mission_id === missionId)
576
+ .sort((left, right) => String(right.updated_at || right.created_at || '').localeCompare(String(left.updated_at || left.created_at || '')));
577
+ const operatorActions = listOperatorActions(state, missionId);
578
+ const reruns = listReruns(state, missionId);
579
+ const missionControl = getMissionControl(state, missionId);
580
+ const phaseOverview = buildPhaseOverview(phaseRuns);
581
+ const timeline = buildMissionTimeline({
582
+ replay,
583
+ interventions,
584
+ operatorActions,
585
+ reruns,
586
+ phaseRuns,
587
+ phaseRunSessions,
588
+ });
589
+
590
+ return {
591
+ mission: {
592
+ ...mission,
593
+ status: computeMissionStatus(state, missionId, mission.status),
594
+ },
595
+ tasks: ensureArray(state.tasks).filter((task) => task.mission_id === missionId),
596
+ replay,
597
+ gate_decisions: gateDecisions,
598
+ phase_runs: phaseRuns,
599
+ phase_run_sessions: phaseRunSessions,
600
+ phase_overview: phaseOverview,
601
+ reruns,
602
+ operator_actions: operatorActions,
603
+ controls: missionControl,
604
+ timeline,
605
+ execution_overview: {
606
+ total_phase_runs: phaseRuns.length,
607
+ latest_phase_run_id: phaseRuns[0]?.phase_run_id || null,
608
+ latest_phase_run_status: phaseRuns[0]?.status || phaseRunSessions[0]?.derived_status || null,
609
+ latest_phase_run_summary: phaseRuns[0]?.summary || phaseRunSessions[0]?.summary || null,
610
+ latest_phase_run_session_id: phaseRunSessions[0]?.session_id || null,
611
+ current_phase: buildCurrentPhase(phaseRuns),
612
+ },
613
+ interventions,
614
+ };
615
+ }
616
+
617
+ export function startPhaseRunSession(state, missionId, input = {}) {
618
+ const mission = requireMission(state, missionId);
619
+ const timestamp = nowIso();
620
+ const requestedSessionId = String(input.session_id || '').trim();
621
+ const existing = ensureArray(state.phase_run_sessions).find(
622
+ (entry) => entry.mission_id === missionId && entry.session_id === requestedSessionId,
623
+ );
624
+ const timeoutSeconds = Math.max(1, Number(input.timeout_seconds || 900));
625
+ const startedAt = existing?.started_at || timestamp;
626
+ const timeoutAt = new Date(Date.parse(startedAt) + (timeoutSeconds * 1000)).toISOString();
627
+ const session = {
628
+ ...existing,
629
+ session_id: requestedSessionId || existing?.session_id || makeId('prs'),
630
+ mission_id: missionId,
631
+ task_id: input.task_id || existing?.task_id || null,
632
+ phase_run_id: input.phase_run_id || existing?.phase_run_id || null,
633
+ summary: input.summary || mission.goal,
634
+ task_type: input.task_type || 'auto',
635
+ requested_phases: ensureArray(input.phases).length ? ensureArray(input.phases) : ensureArray(existing?.requested_phases),
636
+ requested_cost_mode: input.requested_cost_mode || input.cost_mode || existing?.requested_cost_mode || 'single-agent',
637
+ effective_cost_mode: input.effective_cost_mode || input.cost_mode || existing?.effective_cost_mode || 'single-agent',
638
+ phase_policy: input.phase_policy || existing?.phase_policy || 'adaptive',
639
+ workflow_mode: input.workflow_mode || existing?.workflow_mode || 'queued',
640
+ workflow_reason: input.workflow_reason || existing?.workflow_reason || null,
641
+ timeout_seconds: timeoutSeconds,
642
+ timeout_at: timeoutAt,
643
+ dry_run: Boolean(input.dry_run),
644
+ status: 'running',
645
+ failure: null,
646
+ stop_reason: existing?.stop_reason || null,
647
+ started_at: startedAt,
648
+ completed_at: null,
649
+ updated_at: timestamp,
650
+ };
651
+
652
+ mission.updated_at = timestamp;
653
+ return upsertPhaseRunSession(state, session);
654
+ }
655
+
656
+ export function persistPhaseRun(state, missionId, phaseRunInput = {}) {
657
+ const mission = requireMission(state, missionId);
658
+ const timestamp = nowIso();
659
+ const phaseRunId = String(phaseRunInput.phase_run_id || makeId('phr'));
660
+ const phaseRun = {
661
+ mission_id: missionId,
662
+ phase_run_id: phaseRunId,
663
+ phase_run_file: phaseRunInput.phase_run_file || null,
664
+ summary: phaseRunInput.summary || mission.goal,
665
+ task_type: phaseRunInput.task_type || 'auto',
666
+ cost_mode: phaseRunInput.cost_mode || 'single-agent',
667
+ requested_cost_mode: phaseRunInput.requested_cost_mode || phaseRunInput.cost_mode || 'single-agent',
668
+ status: phaseRunInput.status || 'needs_human',
669
+ stop_reason: phaseRunInput.stop_reason || '',
670
+ phase_policy: phaseRunInput.phase_policy || 'adaptive',
671
+ max_phase_visits: Number(phaseRunInput.max_phase_visits || 2),
672
+ intake_snapshot: phaseRunInput.intake_snapshot ?? null,
673
+ context_map: phaseRunInput.context_map ?? null,
674
+ execution_provenance: phaseRunInput.execution_provenance ?? null,
675
+ acceptance_trust: phaseRunInput.acceptance_trust ?? null,
676
+ deployed_artifact: phaseRunInput.deployed_artifact ?? null,
677
+ deployed_url: phaseRunInput.deployed_url ?? null,
678
+ workspace_evidence: phaseRunInput.workspace_evidence ?? null,
679
+ workflow_mode: phaseRunInput.workflow_mode || 'standard',
680
+ workflow_reason: phaseRunInput.workflow_reason || null,
681
+ workflow_selection_mode: phaseRunInput.workflow_selection_mode || null,
682
+ workflow_selection_score: phaseRunInput.workflow_selection_score ?? null,
683
+ workflow_selection_reasons: ensureArray(phaseRunInput.workflow_selection_reasons),
684
+ workflow_selection_category: phaseRunInput.workflow_selection_category || null,
685
+ workflow_selection_reason: phaseRunInput.workflow_selection_reason || null,
686
+ fast_lane_eligible: Boolean(phaseRunInput.fast_lane_eligible),
687
+ fast_lane_taken: Boolean(phaseRunInput.fast_lane_taken),
688
+ fast_lane_escalated: Boolean(phaseRunInput.fast_lane_escalated),
689
+ fast_lane_escalation_reason: phaseRunInput.fast_lane_escalation_reason || null,
690
+ phases: ensureArray(phaseRunInput.phases),
691
+ phase_health_checks: ensureArray(phaseRunInput.phase_health_checks),
692
+ phase_count: Number(phaseRunInput.phase_count || ensureArray(phaseRunInput.phases).length),
693
+ executed_phases: ensureArray(phaseRunInput.executed_phases),
694
+ dry_run: Boolean(phaseRunInput.dry_run),
695
+ started_at: phaseRunInput.started_at || timestamp,
696
+ completed_at: phaseRunInput.completed_at ?? (phaseRunInput.status === 'running' ? null : timestamp),
697
+ updated_at: timestamp,
698
+ };
699
+
700
+ state.phase_runs = ensureArray(state.phase_runs);
701
+ const existingIndex = state.phase_runs.findIndex(
702
+ (entry) => entry.phase_run_id === phaseRun.phase_run_id && entry.mission_id === missionId,
703
+ );
704
+ if (existingIndex >= 0) {
705
+ state.phase_runs[existingIndex] = phaseRun;
706
+ } else {
707
+ state.phase_runs.unshift(phaseRun);
708
+ }
709
+
710
+ const candidateSession = findPhaseRunSession(state, missionId, phaseRunInput.session_id || null);
711
+ if (candidateSession) {
712
+ upsertPhaseRunSession(state, {
713
+ ...candidateSession,
714
+ phase_run_id: phaseRun.phase_run_id,
715
+ updated_at: timestamp,
716
+ });
717
+ }
718
+
719
+ if (existingIndex < 0) {
720
+ state.events.push({
721
+ event_id: makeId('evt'),
722
+ mission_id: missionId,
723
+ task_id: null,
724
+ type: 'workflow_selection_determined',
725
+ timestamp,
726
+ payload: {
727
+ phase_run_id: phaseRun.phase_run_id,
728
+ summary: phaseRun.summary,
729
+ task_type: phaseRun.task_type,
730
+ phase_policy: phaseRun.phase_policy,
731
+ cost_mode: phaseRun.cost_mode,
732
+ },
733
+ runtime_context: mission.runtime_context,
734
+ });
735
+ }
736
+
737
+ mission.updated_at = timestamp;
738
+ return phaseRun;
739
+ }
740
+
741
+ export function completePhaseRunSession(state, missionId, sessionId, phaseRun = {}) {
742
+ requireMission(state, missionId);
743
+ const existing = ensureArray(state.phase_run_sessions).find(
744
+ (entry) => entry.mission_id === missionId && entry.session_id === sessionId,
745
+ );
746
+ if (!existing) {
747
+ return null;
748
+ }
749
+
750
+ const timestamp = nowIso();
751
+ return upsertPhaseRunSession(state, {
752
+ ...existing,
753
+ phase_run_id: phaseRun.phase_run_id || existing.phase_run_id || null,
754
+ status: phaseRun.status || 'completed',
755
+ stop_reason: phaseRun.stop_reason || null,
756
+ failure: null,
757
+ completed_at: phaseRun.completed_at || timestamp,
758
+ updated_at: timestamp,
759
+ });
760
+ }
761
+
762
+ export function failPhaseRunSession(state, missionId, sessionId, failure = {}) {
763
+ requireMission(state, missionId);
764
+ const existing = ensureArray(state.phase_run_sessions).find(
765
+ (entry) => entry.mission_id === missionId && entry.session_id === sessionId,
766
+ );
767
+ if (!existing) {
768
+ return null;
769
+ }
770
+
771
+ const timestamp = nowIso();
772
+ return upsertPhaseRunSession(state, {
773
+ ...existing,
774
+ status: 'failed',
775
+ stop_reason: failure.summary || existing.stop_reason || 'phase_run_failed',
776
+ failure: cleanObject(failure),
777
+ completed_at: timestamp,
778
+ updated_at: timestamp,
779
+ });
780
+ }