@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.
- package/CHANGELOG.md +74 -0
- package/LICENSE +21 -0
- package/README.md +315 -2
- package/RELEASE_NOTES.md +102 -0
- package/atoms/registry.json +758 -0
- package/bin/xflow.js +7208 -0
- package/docs/adoption/README.md +128 -0
- package/docs/adoption/openflow-release-hardening.md +61 -0
- package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
- package/docs/compatibility.md +52 -0
- package/docs/competitive-benchmark.md +108 -0
- package/docs/corps-operator-guide.md +75 -0
- package/docs/demo-proof.md +126 -0
- package/docs/examples-gallery.md +115 -0
- package/docs/fixtures/tracker-item.json +7 -0
- package/docs/goal-vs-codex.md +129 -0
- package/docs/gstack-comparison.md +88 -0
- package/docs/install-upgrade.md +133 -0
- package/docs/integrations.md +49 -0
- package/docs/launch-demo.md +123 -0
- package/docs/launch-dossier.md +180 -0
- package/docs/methodology.md +149 -0
- package/docs/npm-publish-handoff.md +83 -0
- package/docs/openspec-migration.md +106 -0
- package/docs/public-benchmark.md +68 -0
- package/docs/public-release.md +145 -0
- package/docs/quality-assessment.md +139 -0
- package/docs/quickstart.md +156 -0
- package/docs/reviewer-guide.md +43 -0
- package/docs/spec-kit-benchmark.md +73 -0
- package/docs/super-assistant-comparison.md +106 -0
- package/docs/superpowers-comparison.md +82 -0
- package/docs/team-adoption.md +123 -0
- package/docs/tooling-matrix.md +91 -0
- package/docs/walkthrough.md +76 -0
- package/docs/workflow-manual.md +1125 -0
- package/package.json +83 -4
- package/prompts/pencil-draft.md +46 -0
- package/prompts/pencil-refine.md +46 -0
- package/prompts/phase-artifact.md +24 -0
- package/prompts/phase-execution.md +32 -0
- package/schemas/competitor-reconstruction-contract.schema.json +91 -0
- package/schemas/llm-design-review-aggregate.schema.json +83 -0
- package/schemas/visual-benchmark.schema.json +978 -0
- package/schemas/workflow.schema.json +100 -0
- package/scripts/run-tests.mjs +23 -0
- package/scripts/takein_digest.py +139 -0
- package/specs/README.md +118 -0
- package/specs/architecture.md +36 -0
- package/specs/product.md +33 -0
- package/specs/workflow.md +349 -0
- package/src/agent_team/atoms/codex-app-bridge.js +8 -0
- package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
- package/src/agent_team/atoms/design/contract-freeze.js +8 -0
- package/src/agent_team/atoms/design/generation-contract.js +442 -0
- package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
- package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
- package/src/agent_team/atoms/patch-challenge.js +19 -0
- package/src/agent_team/atoms/pencil/accept.js +9 -0
- package/src/agent_team/atoms/pencil/draft.js +365 -0
- package/src/agent_team/atoms/pencil/refine.js +229 -0
- package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
- package/src/agent_team/atoms/team-run.js +380 -0
- package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
- package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
- package/src/agent_team/atoms/visual/benchmark.js +1652 -0
- package/src/agent_team/atoms/visual/design-selection.js +23 -0
- package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
- package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
- package/src/agent_team/atoms/visual/image-diff.js +186 -0
- package/src/agent_team/atoms/visual/layout-competition.js +8 -0
- package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
- package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
- package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
- package/src/agent_team/atoms/visual/review.js +77 -0
- package/src/agent_team/atoms/visual/visual-report.js +197 -0
- package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
- package/src/agent_team/common.js +311 -0
- package/src/core/atoms/agent-invoke.js +684 -0
- package/src/core/atoms/event/ingest.js +30 -0
- package/src/core/atoms/experience/digest.js +55 -0
- package/src/core/atoms/gate/artifact-verify.js +83 -0
- package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
- package/src/core/atoms/gate/evidence-collect.js +64 -0
- package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
- package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
- package/src/core/atoms/gate/workbench.js +79 -0
- package/src/core/atoms/intake/classify.js +15 -0
- package/src/core/atoms/intake/design-skill-select.js +16 -0
- package/src/core/atoms/intake/preview-or-refine.js +19 -0
- package/src/core/atoms/intake/recommend.js +20 -0
- package/src/core/atoms/intake/research-same-category.js +19 -0
- package/src/core/atoms/intervention/create.js +30 -0
- package/src/core/atoms/memory/manage.js +27 -0
- package/src/core/atoms/memory/mount.js +13 -0
- package/src/core/atoms/mission/create.js +19 -0
- package/src/core/atoms/mission/list.js +14 -0
- package/src/core/atoms/mission/show.js +26 -0
- package/src/core/atoms/notify/checkpoint.js +46 -0
- package/src/core/atoms/phase-run/complete.js +42 -0
- package/src/core/atoms/phase-run/fail.js +40 -0
- package/src/core/atoms/phase-run/persist.js +39 -0
- package/src/core/atoms/phase-run/start.js +30 -0
- package/src/core/atoms/plugin/run.js +76 -0
- package/src/core/atoms/policy/gate.js +90 -0
- package/src/core/atoms/policy/resolve.js +11 -0
- package/src/core/atoms/storage/sync.js +57 -0
- package/src/core/atoms/workflow/advance.js +25 -0
- package/src/core/atoms/workflow/guard.js +127 -0
- package/src/core/atoms/workflow/state-load.js +37 -0
- package/src/core/bindings.js +37 -0
- package/src/core/change-artifacts.js +134 -0
- package/src/core/competitive-score.js +265 -0
- package/src/core/concurrency-policy.js +35 -0
- package/src/core/execution-log.js +108 -0
- package/src/core/intake-recommendation.js +134 -0
- package/src/core/memory-state.js +62 -0
- package/src/core/mission-state.js +780 -0
- package/src/core/pending-gates.js +66 -0
- package/src/core/phase-cost.js +116 -0
- package/src/core/phase-run-read.js +27 -0
- package/src/core/policy-overlay.js +180 -0
- package/src/core/state-store.js +201 -0
- package/src/core/workflow-executor.js +998 -0
- package/src/core/workflow-integrity.js +111 -0
- package/src/core/workflow-loader.js +163 -0
- package/src/core/workflow-rerun.js +84 -0
- package/src/core/workflow-state.js +119 -0
- package/src/server.js +1307 -0
- package/workflows/corps.yaml +479 -0
- package/workflows/yolo.yaml +233 -0
- package/xflow/README.md +76 -0
- package/xflow/aha/SKILL.md +82 -0
- package/xflow/aha/scripts/append_aha.py +50 -0
- package/xflow/archive/scripts/archive_change.py +338 -0
- package/xflow/archive/templates/merge-design.md +54 -0
- package/xflow/archive/templates/merge-workflow.md +26 -0
- package/xflow/atoms/__init__.py +6 -0
- package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
- package/xflow/atoms/a2_branch_create_and_link.py +42 -0
- package/xflow/atoms/a3_repo_doctor.py +11 -0
- package/xflow/atoms/a4_project_set_status.py +11 -0
- package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
- package/xflow/atoms/a6_pr_create.py +72 -0
- package/xflow/atoms/b1_change_scaffold.py +11 -0
- package/xflow/atoms/b2_status_read.py +16 -0
- package/xflow/atoms/b2b_status_write.py +20 -0
- package/xflow/atoms/b3_status_transition.py +27 -0
- package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
- package/xflow/atoms/b5_aha_append.py +11 -0
- package/xflow/atoms/b6_takein_digest.py +11 -0
- package/xflow/atoms/b6b_lookback_digest.py +11 -0
- package/xflow/atoms/b7_change_mission_bind.py +43 -0
- package/xflow/atoms/c1_mem.py +54 -0
- package/xflow/atoms/e6_gate_local_precheck.py +23 -0
- package/xflow/atoms/h1_design_lite_gate.py +74 -0
- package/xflow/atoms/i6a_tdd_run.py +104 -0
- package/xflow/atoms/i6b_tdd_proof_validate.py +165 -0
- package/xflow/atoms/i6c_tdd_quality_review.py +357 -0
- package/xflow/atoms/j1_tests_run.py +129 -0
- package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
- package/xflow/atoms/j4b_spec_delta_review.py +316 -0
- package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
- package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
- package/xflow/atoms/j6_cr_review.py +443 -0
- package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
- package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
- package/xflow/atoms/k3_handoff_refresh.py +14 -0
- package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
- package/xflow/atoms/k5_archive_publish.py +21 -0
- package/xflow/atoms/k6_aha_merge.py +42 -0
- package/xflow/atoms/tdd_lib.py +747 -0
- package/xflow/corps/SKILL.md +109 -0
- package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
- package/xflow/goal/SKILL.md +150 -0
- package/xflow/handoff/SKILL.md +95 -0
- package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
- package/xflow/lookback/SKILL.md +66 -0
- package/xflow/lookback/scripts/lookback_digest.py +46 -0
- package/xflow/openissue/scripts/open_issue_flow.py +438 -0
- package/xflow/plan/SKILL.md +38 -0
- package/xflow/ralph/SKILL.md +123 -0
- package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
- package/xflow/scripts/common.py +480 -0
- package/xflow/scripts/doctor.py +278 -0
- package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
- package/xflow/takein/SKILL.md +81 -0
- package/xflow/takein/scripts/takein_digest.py +88 -0
- package/xflow/yolo/SKILL.md +66 -0
- package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
- package/xflow/yolo/scripts/yolo_gate.py +312 -0
- package/xflow/yolo/templates/DESIGN.md +179 -0
|
@@ -0,0 +1,998 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkflowExecutor — iterates phases and dispatches atoms.
|
|
3
|
+
*
|
|
4
|
+
* Gate type dispatch:
|
|
5
|
+
* deterministic → run gate.atom via runAtomSync(), expect exit 0 + JSON {ok: true}
|
|
6
|
+
* artifact-verify → check all phase.artifacts exist and are non-empty
|
|
7
|
+
* llm-judge → on yolo track: return {verdict: 'needs-human', reason: 'llm-judge phases require corps track'}
|
|
8
|
+
* on corps track: delegate to agent_team runner (D3a/I1/E4 atoms)
|
|
9
|
+
* human → write gate_decision row, pause and wait for xflow gate ack or manual gate override
|
|
10
|
+
* skip → no gate check
|
|
11
|
+
*
|
|
12
|
+
* E6 ≡ E1+E2 invariant enforced: executor routes ALL deterministic gate checks
|
|
13
|
+
* through E6.gate.local_precheck Python atom, never re-implementing logic here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync, statSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
17
|
+
import { resolve, dirname } from 'path';
|
|
18
|
+
import { spawnSync } from 'child_process';
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { appendExecutionLog, appendExecutionLogSync } from './execution-log.js';
|
|
22
|
+
import { addTokenUsage, buildPhaseCostReport, emptyTokenUsage, extractTokenUsage } from './phase-cost.js';
|
|
23
|
+
import { createConcurrencyPolicy } from './concurrency-policy.js';
|
|
24
|
+
import { buildWorkflowManifest } from './workflow-integrity.js';
|
|
25
|
+
|
|
26
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
27
|
+
const ROOT = resolve(__dirname, '..', '..');
|
|
28
|
+
|
|
29
|
+
function tail(value, maxChars = 4000) {
|
|
30
|
+
if (value == null) return null;
|
|
31
|
+
const text = String(value);
|
|
32
|
+
if (!text) return null;
|
|
33
|
+
return text.length > maxChars ? text.slice(-maxChars) : text;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class WorkflowExecutor {
|
|
37
|
+
constructor({ workflow, phases, registry, workflowPath = null, projectRoot, changeId, input = {}, exitOnFailure = true, maxParallelAgentsOverride = null, abortSignal = null }) {
|
|
38
|
+
this.workflow = workflow;
|
|
39
|
+
this.phases = phases;
|
|
40
|
+
this.registry = registry;
|
|
41
|
+
this.workflowPath = workflowPath;
|
|
42
|
+
this.workflowManifest = buildWorkflowManifest({ workflowPath, workflow, phases, registry });
|
|
43
|
+
this.projectRoot = projectRoot || process.cwd();
|
|
44
|
+
this.changeId = changeId || input.change_id || process.env.CHANGE_ID;
|
|
45
|
+
this.input = input;
|
|
46
|
+
this.exitOnFailure = exitOnFailure;
|
|
47
|
+
this.stateFile = resolve(this.projectRoot, '.as-xflow', 'workflow-state.json');
|
|
48
|
+
this.changeStatusFile = this.changeId
|
|
49
|
+
? resolve(this.projectRoot, 'specs', 'changes', this.changeId, 'status.json')
|
|
50
|
+
: null;
|
|
51
|
+
this.abortSignal = abortSignal;
|
|
52
|
+
this.concurrencyPolicy = createConcurrencyPolicy({ workflow, maxParallelAgentsOverride });
|
|
53
|
+
this.maxParallelAgents = this.concurrencyPolicy.maxAgents;
|
|
54
|
+
this.runtime = {
|
|
55
|
+
missionId: null,
|
|
56
|
+
workflowRunId: `wf_${randomUUID()}`,
|
|
57
|
+
phaseRunId: null,
|
|
58
|
+
phaseRunSessionId: null,
|
|
59
|
+
lastAtomResult: null,
|
|
60
|
+
atomResults: {},
|
|
61
|
+
phaseCosts: [],
|
|
62
|
+
currentPhaseTokens: null,
|
|
63
|
+
workflowStartedAtMs: null,
|
|
64
|
+
costReport: null,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ─── Main execution loop ─────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
// ─── Phase cost accounting ─────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
recordPhaseCost(phase, status, startedAtMs) {
|
|
73
|
+
const entry = {
|
|
74
|
+
phase_id: phase?.id || 'unknown',
|
|
75
|
+
status,
|
|
76
|
+
duration_ms: Math.max(0, Date.now() - startedAtMs),
|
|
77
|
+
tokens: this.runtime.currentPhaseTokens && this.runtime.currentPhaseTokens.total > 0
|
|
78
|
+
? { ...this.runtime.currentPhaseTokens }
|
|
79
|
+
: null,
|
|
80
|
+
};
|
|
81
|
+
this.runtime.phaseCosts.push(entry);
|
|
82
|
+
this.runtime.currentPhaseTokens = emptyTokenUsage();
|
|
83
|
+
return entry;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
buildCostReport() {
|
|
87
|
+
const wallClockMs = this.runtime.workflowStartedAtMs
|
|
88
|
+
? Date.now() - this.runtime.workflowStartedAtMs
|
|
89
|
+
: null;
|
|
90
|
+
this.runtime.costReport = buildPhaseCostReport({
|
|
91
|
+
phases: this.runtime.phaseCosts,
|
|
92
|
+
wallClockMs,
|
|
93
|
+
});
|
|
94
|
+
return this.runtime.costReport;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async run() {
|
|
98
|
+
this.runtime.workflowStartedAtMs = Date.now();
|
|
99
|
+
this.runtime.currentPhaseTokens = emptyTokenUsage();
|
|
100
|
+
const state = this.loadState();
|
|
101
|
+
let startIdx = 0;
|
|
102
|
+
const rerunRequest = state.rerun && typeof state.rerun === 'object' ? state.rerun : null;
|
|
103
|
+
this.ensureNotAborted();
|
|
104
|
+
|
|
105
|
+
await this.logExecutionEvent('workflow_started', {
|
|
106
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
107
|
+
workflow: this.workflow.name,
|
|
108
|
+
track: this.workflow.track,
|
|
109
|
+
workflow_integrity: this.workflowManifest,
|
|
110
|
+
max_parallel_agents: this.maxParallelAgents,
|
|
111
|
+
parallel_policy: this.concurrencyPolicy.mode,
|
|
112
|
+
parallel_source: this.concurrencyPolicy.source,
|
|
113
|
+
change_id: this.changeId || null,
|
|
114
|
+
mission_id: this.runtime.missionId || null,
|
|
115
|
+
resume_from_phase: state.last_completed_phase || null,
|
|
116
|
+
rerun_phase: rerunRequest?.phase_id || null,
|
|
117
|
+
rerun_atom: rerunRequest?.atom_id || null,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (rerunRequest?.phase_id) {
|
|
121
|
+
const idx = this.phases.findIndex((phase) => phase.id === rerunRequest.phase_id);
|
|
122
|
+
if (idx >= 0) {
|
|
123
|
+
startIdx = idx;
|
|
124
|
+
console.log(`\nRerunning from phase ${startIdx + 1}/${this.phases.length} ("${rerunRequest.phase_id}")`);
|
|
125
|
+
}
|
|
126
|
+
} else if (state.last_completed_phase) {
|
|
127
|
+
// Resume from last completed phase
|
|
128
|
+
const idx = this.phases.findIndex(p => p.id === state.last_completed_phase);
|
|
129
|
+
if (idx >= 0) {
|
|
130
|
+
startIdx = idx + 1;
|
|
131
|
+
console.log(`\nResuming from phase ${startIdx + 1}/${this.phases.length} (after "${state.last_completed_phase}")`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (let i = startIdx; i < this.phases.length; i++) {
|
|
136
|
+
this.ensureNotAborted();
|
|
137
|
+
const phase = this.phases[i];
|
|
138
|
+
const phaseStartedAtMs = Date.now();
|
|
139
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
140
|
+
console.log(`Phase ${i + 1}/${this.phases.length}: ${phase.label || phase.id}`);
|
|
141
|
+
console.log('═'.repeat(60));
|
|
142
|
+
await this.logExecutionEvent('phase_started', {
|
|
143
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
144
|
+
workflow: this.workflow.name,
|
|
145
|
+
track: this.workflow.track,
|
|
146
|
+
phase_id: phase.id,
|
|
147
|
+
phase_label: phase.label || null,
|
|
148
|
+
phase_index: i,
|
|
149
|
+
change_id: this.changeId || null,
|
|
150
|
+
mission_id: this.runtime.missionId || null,
|
|
151
|
+
workflow_integrity_digest: this.workflowManifest.digest,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Condition check (skip if not applicable)
|
|
155
|
+
if (phase.condition) {
|
|
156
|
+
const inputVal = this.input[phase.condition.input_field];
|
|
157
|
+
if (inputVal !== phase.condition.equals) {
|
|
158
|
+
console.log(` ↷ Skipping (condition: ${phase.condition.input_field} !== ${phase.condition.equals})`);
|
|
159
|
+
await this.logExecutionEvent('phase_skipped', {
|
|
160
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
161
|
+
workflow: this.workflow.name,
|
|
162
|
+
track: this.workflow.track,
|
|
163
|
+
phase_id: phase.id,
|
|
164
|
+
phase_label: phase.label || null,
|
|
165
|
+
phase_index: i,
|
|
166
|
+
reason: 'condition_mismatch',
|
|
167
|
+
change_id: this.changeId || null,
|
|
168
|
+
mission_id: this.runtime.missionId || null,
|
|
169
|
+
workflow_integrity_digest: this.workflowManifest.digest,
|
|
170
|
+
});
|
|
171
|
+
this.recordPhaseCost(phase, 'skipped', phaseStartedAtMs);
|
|
172
|
+
this.saveState({ ...state, last_completed_phase: phase.id });
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Preconditions
|
|
178
|
+
if (phase.preconditions?.length > 0) {
|
|
179
|
+
const precondOk = this.checkPreconditions(phase.preconditions);
|
|
180
|
+
if (!precondOk) {
|
|
181
|
+
this.recordPhaseCost(phase, 'failed', phaseStartedAtMs);
|
|
182
|
+
this.fail(phase, 'precondition_failed');
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (phase.id === 'plan') {
|
|
188
|
+
this.ensurePlanArtifact();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Run atoms
|
|
192
|
+
const atomsToRun = rerunRequest?.phase_id === phase.id && rerunRequest?.atom_id
|
|
193
|
+
? (phase.atoms || []).filter((atomRef) => atomRef.id === rerunRequest.atom_id)
|
|
194
|
+
: (phase.atoms || []);
|
|
195
|
+
if (atomsToRun.length > 0) {
|
|
196
|
+
for (const batch of this.buildAtomBatches(atomsToRun)) {
|
|
197
|
+
this.ensureNotAborted();
|
|
198
|
+
const results = batch.parallel
|
|
199
|
+
? await this.runParallelBatch(batch.atoms, phase)
|
|
200
|
+
: [await this.runAtom(batch.atoms[0], phase)];
|
|
201
|
+
|
|
202
|
+
for (let index = 0; index < batch.atoms.length; index += 1) {
|
|
203
|
+
const atomRef = batch.atoms[index];
|
|
204
|
+
const result = results[index];
|
|
205
|
+
if (!result.ok) {
|
|
206
|
+
console.error(` ✗ Atom ${atomRef.id} failed`);
|
|
207
|
+
if (phase.gate?.on_fail === 'stop') {
|
|
208
|
+
this.recordPhaseCost(phase, 'failed', phaseStartedAtMs);
|
|
209
|
+
this.fail(phase, 'atom_failed', atomRef.id);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.log(` ✓ Atom ${atomRef.id}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Gate check
|
|
220
|
+
const gateResult = await this.runGate(phase);
|
|
221
|
+
await this.logExecutionEvent('gate_check', {
|
|
222
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
223
|
+
workflow: this.workflow.name,
|
|
224
|
+
track: this.workflow.track,
|
|
225
|
+
phase_id: phase.id,
|
|
226
|
+
phase_label: phase.label || null,
|
|
227
|
+
ok: gateResult.ok !== false,
|
|
228
|
+
status: gateResult.status || null,
|
|
229
|
+
verdict: gateResult.verdict || null,
|
|
230
|
+
gate_type: phase.gate?.type || 'skip',
|
|
231
|
+
change_id: this.changeId || null,
|
|
232
|
+
mission_id: this.runtime.missionId || null,
|
|
233
|
+
workflow_integrity_digest: this.workflowManifest.digest,
|
|
234
|
+
});
|
|
235
|
+
if (!gateResult.ok) {
|
|
236
|
+
const onFail = phase.gate?.on_fail || 'stop';
|
|
237
|
+
if (onFail === 'stop') {
|
|
238
|
+
this.recordPhaseCost(phase, 'failed', phaseStartedAtMs);
|
|
239
|
+
this.fail(phase, 'gate_failed', JSON.stringify(gateResult));
|
|
240
|
+
return;
|
|
241
|
+
} else if (onFail === 'advance-with-warning') {
|
|
242
|
+
console.warn(` ⚠ Gate soft-failed, advancing with warning: ${JSON.stringify(gateResult)}`);
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
console.log(` ✓ Gate: ${phase.gate?.type || 'skip'}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Advance
|
|
249
|
+
state.last_completed_phase = phase.id;
|
|
250
|
+
state.completed_phases = [...(state.completed_phases || []), phase.id];
|
|
251
|
+
if (rerunRequest?.phase_id === phase.id) {
|
|
252
|
+
delete state.rerun;
|
|
253
|
+
}
|
|
254
|
+
state.updated_at = new Date().toISOString();
|
|
255
|
+
this.saveState(state);
|
|
256
|
+
const phaseCost = this.recordPhaseCost(phase, 'completed', phaseStartedAtMs);
|
|
257
|
+
await this.logExecutionEvent('phase_completed', {
|
|
258
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
259
|
+
workflow: this.workflow.name,
|
|
260
|
+
track: this.workflow.track,
|
|
261
|
+
phase_id: phase.id,
|
|
262
|
+
phase_label: phase.label || null,
|
|
263
|
+
phase_index: i,
|
|
264
|
+
change_id: this.changeId || null,
|
|
265
|
+
mission_id: this.runtime.missionId || null,
|
|
266
|
+
workflow_integrity_digest: this.workflowManifest.digest,
|
|
267
|
+
duration_ms: phaseCost.duration_ms,
|
|
268
|
+
cost: phaseCost,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`\n${'═'.repeat(60)}`);
|
|
273
|
+
console.log(`✓ Workflow "${this.workflow.name}" completed`);
|
|
274
|
+
console.log('═'.repeat(60));
|
|
275
|
+
await this.logExecutionEvent('workflow_completed', {
|
|
276
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
277
|
+
workflow: this.workflow.name,
|
|
278
|
+
track: this.workflow.track,
|
|
279
|
+
workflow_integrity: this.workflowManifest,
|
|
280
|
+
change_id: this.changeId || null,
|
|
281
|
+
mission_id: this.runtime.missionId || null,
|
|
282
|
+
completed_phases: this.phases.map((phase) => phase.id),
|
|
283
|
+
cost: this.buildCostReport(),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ─── Atom dispatch ────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
ensurePlanArtifact() {
|
|
290
|
+
const changeRoot = resolve(this.projectRoot, 'specs', 'changes', this.changeId);
|
|
291
|
+
const planPath = resolve(changeRoot, 'plan.md');
|
|
292
|
+
const currentPlan = existsSync(planPath) ? readFileSync(planPath, 'utf8') : '';
|
|
293
|
+
|
|
294
|
+
if (this.isReusablePlan(currentPlan)) {
|
|
295
|
+
return { reused: true, generated: false, path: planPath };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const proposalPath = resolve(changeRoot, 'proposal.md');
|
|
299
|
+
const proposalText = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf8') : '';
|
|
300
|
+
const title = this.input.title || this.workflow.name || this.changeId;
|
|
301
|
+
const objective = this.extractSection(proposalText, ['Objective', 'Target Outcome', 'Problem / Context', 'Problem']) || title;
|
|
302
|
+
const chosenPath = this.extractSection(proposalText, ['Chosen Path', 'Scope', 'In Scope']) || 'Use the accepted proposal as the implementation baseline.';
|
|
303
|
+
const inScope = this.extractSection(proposalText, ['In Scope', 'Scope']) || '- Follow the frozen proposal scope.';
|
|
304
|
+
const outOfScope = this.extractSection(proposalText, ['Out of Scope']) || '- Everything outside the accepted proposal scope.';
|
|
305
|
+
const verification = this.defaultVerificationCommands();
|
|
306
|
+
const mainRisk = this.extractSection(proposalText, ['Main Risk', 'Risks', 'Risk']) || 'Plan drift or missing verification commands will block execution.';
|
|
307
|
+
|
|
308
|
+
mkdirSync(changeRoot, { recursive: true });
|
|
309
|
+
const content = [
|
|
310
|
+
'# Plan',
|
|
311
|
+
'',
|
|
312
|
+
'## Target Outcome',
|
|
313
|
+
'',
|
|
314
|
+
objective.trim(),
|
|
315
|
+
'',
|
|
316
|
+
'## Chosen Path',
|
|
317
|
+
'',
|
|
318
|
+
chosenPath.trim(),
|
|
319
|
+
'',
|
|
320
|
+
'## In Scope',
|
|
321
|
+
'',
|
|
322
|
+
inScope.trim(),
|
|
323
|
+
'',
|
|
324
|
+
'## Out of Scope',
|
|
325
|
+
'',
|
|
326
|
+
outOfScope.trim(),
|
|
327
|
+
'',
|
|
328
|
+
'## Verification Commands',
|
|
329
|
+
'',
|
|
330
|
+
'```bash',
|
|
331
|
+
verification,
|
|
332
|
+
'```',
|
|
333
|
+
'',
|
|
334
|
+
'## Main Risk',
|
|
335
|
+
'',
|
|
336
|
+
mainRisk.trim(),
|
|
337
|
+
'',
|
|
338
|
+
].join('\n');
|
|
339
|
+
writeFileSync(planPath, content, 'utf8');
|
|
340
|
+
return { reused: false, generated: true, path: planPath };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
isReusablePlan(text) {
|
|
344
|
+
const normalized = (text || '').trim();
|
|
345
|
+
if (!normalized) return false;
|
|
346
|
+
if (normalized.length < 120) return false;
|
|
347
|
+
if (/## Task Slices\b/.test(normalized)) return false;
|
|
348
|
+
if (/^\s*-\s*$/m.test(normalized)) return false;
|
|
349
|
+
if (/## Verification Commands\s*\n\s*```bash\s*# Fill in the smallest proof commands\s*```/.test(normalized)) return false;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
extractSection(text, headings) {
|
|
354
|
+
if (!text) return '';
|
|
355
|
+
const lines = text.split(/\r?\n/);
|
|
356
|
+
const headingSet = new Set(headings.map((heading) => heading.toLowerCase()));
|
|
357
|
+
let capture = false;
|
|
358
|
+
const collected = [];
|
|
359
|
+
|
|
360
|
+
for (const line of lines) {
|
|
361
|
+
const headingMatch = line.match(/^##\s+(.+)$/);
|
|
362
|
+
if (headingMatch) {
|
|
363
|
+
const current = headingMatch[1].trim().toLowerCase();
|
|
364
|
+
if (capture) {
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
capture = headingSet.has(current);
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (capture) {
|
|
371
|
+
collected.push(line);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return collected.join('\n').trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
defaultVerificationCommands() {
|
|
379
|
+
if (this.workflow.track === 'heavy') {
|
|
380
|
+
return 'npm test';
|
|
381
|
+
}
|
|
382
|
+
return 'npm test';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async runAtom(atomRef, phase = null) {
|
|
386
|
+
this.ensureNotAborted();
|
|
387
|
+
const def = this.registry.atoms[atomRef.id];
|
|
388
|
+
if (!def) {
|
|
389
|
+
return { ok: false, error: `Unknown atom: ${atomRef.id}` };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Resolve template variables in `with`
|
|
393
|
+
const withArgs = this.buildAtomArgs(atomRef, phase);
|
|
394
|
+
const startedAt = Date.now();
|
|
395
|
+
const logBase = {
|
|
396
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
397
|
+
workflow: this.workflow.name,
|
|
398
|
+
track: this.workflow.track,
|
|
399
|
+
phase_id: phase?.id || null,
|
|
400
|
+
phase_label: phase?.label || null,
|
|
401
|
+
atom_id: atomRef.id,
|
|
402
|
+
atom_type: def.type,
|
|
403
|
+
parallel_group: atomRef.parallel_group || null,
|
|
404
|
+
parallel_weight: atomRef.parallel_group ? this.concurrencyPolicy.weightFor(atomRef, this.registry) : null,
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
if (def.type === 'python') {
|
|
408
|
+
const scriptPath = resolve(ROOT, def.script);
|
|
409
|
+
if (!existsSync(scriptPath)) {
|
|
410
|
+
return { ok: false, error: `Script not found: ${scriptPath}` };
|
|
411
|
+
}
|
|
412
|
+
const args = this.objectToCliArgs(withArgs);
|
|
413
|
+
args.push('--project-root', this.projectRoot);
|
|
414
|
+
if (this.changeId) args.push('--change-id', this.changeId);
|
|
415
|
+
|
|
416
|
+
console.log(` → python3 ${def.script} ${args.slice(0, 4).join(' ')}${args.length > 4 ? ' ...' : ''}`);
|
|
417
|
+
const result = spawnSync('python3', [scriptPath, ...args], {
|
|
418
|
+
stdio: ['inherit', 'pipe', 'inherit'],
|
|
419
|
+
encoding: 'utf8',
|
|
420
|
+
});
|
|
421
|
+
const stdout = result.stdout || '';
|
|
422
|
+
let parsed = {};
|
|
423
|
+
try { parsed = JSON.parse(stdout.trim()); } catch { parsed = { ok: result.status === 0 }; }
|
|
424
|
+
if (stdout.trim()) console.log(` ← ${stdout.trim().slice(0, 120)}`);
|
|
425
|
+
const output = { ok: result.status === 0, ...parsed };
|
|
426
|
+
this.captureAtomResult(atomRef.id, output);
|
|
427
|
+
await this.logAtomRun({
|
|
428
|
+
...logBase,
|
|
429
|
+
ok: output.ok !== false,
|
|
430
|
+
exit_code: result.status ?? 1,
|
|
431
|
+
duration_ms: Date.now() - startedAt,
|
|
432
|
+
});
|
|
433
|
+
return output;
|
|
434
|
+
|
|
435
|
+
} else if (def.type === 'shell') {
|
|
436
|
+
const scriptPath = resolve(ROOT, def.script);
|
|
437
|
+
const args = [atomRef.with?.subcommand || '', ...(atomRef.with?.args || [])].filter(Boolean);
|
|
438
|
+
const result = spawnSync('bash', [scriptPath, ...args], { stdio: 'inherit' });
|
|
439
|
+
const output = { ok: result.status === 0 };
|
|
440
|
+
this.captureAtomResult(atomRef.id, output);
|
|
441
|
+
await this.logAtomRun({
|
|
442
|
+
...logBase,
|
|
443
|
+
ok: output.ok !== false,
|
|
444
|
+
exit_code: result.status ?? 1,
|
|
445
|
+
duration_ms: Date.now() - startedAt,
|
|
446
|
+
});
|
|
447
|
+
return output;
|
|
448
|
+
|
|
449
|
+
} else if (def.type === 'js') {
|
|
450
|
+
const modulePath = resolve(ROOT, def.module);
|
|
451
|
+
if (!existsSync(modulePath)) {
|
|
452
|
+
return { ok: false, error: `Module not found: ${modulePath}` };
|
|
453
|
+
}
|
|
454
|
+
if (def.requires_http && !this.httpAvailable()) {
|
|
455
|
+
return { ok: false, error: `Atom ${atomRef.id} requires HTTP server (xflow serve)` };
|
|
456
|
+
}
|
|
457
|
+
const mod = await import(pathToFileURL(modulePath).href);
|
|
458
|
+
const handler = mod[def.export || 'default'];
|
|
459
|
+
if (typeof handler !== 'function') {
|
|
460
|
+
return { ok: false, error: `Export "${def.export || 'default'}" not found in ${def.module}` };
|
|
461
|
+
}
|
|
462
|
+
console.log(` → js ${def.module}#${def.export || 'default'}`);
|
|
463
|
+
const handlerInput = { ...this.input, ...withArgs };
|
|
464
|
+
const output = await handler(handlerInput, {
|
|
465
|
+
workflow: this.workflow,
|
|
466
|
+
phase,
|
|
467
|
+
registry: this.registry,
|
|
468
|
+
projectRoot: this.projectRoot,
|
|
469
|
+
changeId: this.changeId,
|
|
470
|
+
input: this.input,
|
|
471
|
+
runtime: this.runtime,
|
|
472
|
+
});
|
|
473
|
+
const normalized = output && typeof output === 'object' ? output : { ok: true, value: output };
|
|
474
|
+
const finalOutput = { ok: normalized.ok !== false, ...normalized };
|
|
475
|
+
this.captureAtomResult(atomRef.id, finalOutput);
|
|
476
|
+
await this.logAtomRun({
|
|
477
|
+
...logBase,
|
|
478
|
+
ok: finalOutput.ok !== false,
|
|
479
|
+
status: finalOutput.status || null,
|
|
480
|
+
adapter: finalOutput.adapter || null,
|
|
481
|
+
adapter_reason: finalOutput.adapter_reason || null,
|
|
482
|
+
artifact_type: finalOutput.artifact?.type || null,
|
|
483
|
+
duration_ms: Date.now() - startedAt,
|
|
484
|
+
});
|
|
485
|
+
return finalOutput;
|
|
486
|
+
} else if (def.type === 'agent_invoke') {
|
|
487
|
+
const modPath = resolve(ROOT, def.module);
|
|
488
|
+
if (!existsSync(modPath)) {
|
|
489
|
+
return { ok: false, error: `Module not found: ${modPath}` };
|
|
490
|
+
}
|
|
491
|
+
const { default: handler } = await import(pathToFileURL(modPath).href);
|
|
492
|
+
console.log(` → agent_invoke ${def.module}`);
|
|
493
|
+
const output = await handler(withArgs, {
|
|
494
|
+
workflow: this.workflow,
|
|
495
|
+
phase,
|
|
496
|
+
registry: this.registry,
|
|
497
|
+
projectRoot: this.projectRoot,
|
|
498
|
+
changeId: this.changeId,
|
|
499
|
+
input: this.input,
|
|
500
|
+
runtime: this.runtime,
|
|
501
|
+
atomDef: { id: atomRef.id, ...def },
|
|
502
|
+
});
|
|
503
|
+
const normalized = output && typeof output === 'object' ? output : { ok: true, value: output };
|
|
504
|
+
const finalOutput = { ok: normalized.ok !== false, ...normalized };
|
|
505
|
+
this.captureAtomResult(atomRef.id, finalOutput);
|
|
506
|
+
await this.logAtomRun({
|
|
507
|
+
...logBase,
|
|
508
|
+
ok: finalOutput.ok !== false,
|
|
509
|
+
status: finalOutput.status || null,
|
|
510
|
+
adapter: finalOutput.adapter || null,
|
|
511
|
+
adapter_reason: finalOutput.adapter_reason || null,
|
|
512
|
+
artifact_type: finalOutput.artifact?.type || null,
|
|
513
|
+
exit_code: finalOutput.code ?? null,
|
|
514
|
+
signal: finalOutput.signal || null,
|
|
515
|
+
timed_out: finalOutput.timed_out === true,
|
|
516
|
+
force_killed: finalOutput.force_killed === true,
|
|
517
|
+
attempts: finalOutput.attempts || null,
|
|
518
|
+
retries_used: finalOutput.retries_used || null,
|
|
519
|
+
error: finalOutput.error || null,
|
|
520
|
+
stdout_tail: tail(finalOutput.stdout),
|
|
521
|
+
stderr_tail: tail(finalOutput.stderr),
|
|
522
|
+
duration_ms: Date.now() - startedAt,
|
|
523
|
+
});
|
|
524
|
+
return finalOutput;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return { ok: false, error: `Unknown atom type: ${def.type}` };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async logAtomRun(entry) {
|
|
531
|
+
await this.logExecutionEvent('atom_run', entry);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async logExecutionEvent(kind, entry) {
|
|
535
|
+
try {
|
|
536
|
+
await appendExecutionLog(this.projectRoot, {
|
|
537
|
+
kind,
|
|
538
|
+
change_id: this.changeId || null,
|
|
539
|
+
mission_id: this.runtime.missionId || null,
|
|
540
|
+
...entry,
|
|
541
|
+
});
|
|
542
|
+
} catch {
|
|
543
|
+
// Logging is best-effort and must not break workflow execution.
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// ─── Gate dispatch ────────────────────────────────────────────────────────
|
|
548
|
+
|
|
549
|
+
async runGate(phase) {
|
|
550
|
+
const gate = phase.gate;
|
|
551
|
+
if (!gate || gate.type === 'skip') return { ok: true };
|
|
552
|
+
|
|
553
|
+
switch (gate.type) {
|
|
554
|
+
case 'deterministic': {
|
|
555
|
+
// Route through E6 (Python) for ALL structural checks — invariant enforced
|
|
556
|
+
if (gate.atom) {
|
|
557
|
+
const result = await this.runAtom({ id: gate.atom, with: gate.with }, phase);
|
|
558
|
+
return result;
|
|
559
|
+
}
|
|
560
|
+
return { ok: true }; // no gate atom = pass
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
case 'artifact-verify': {
|
|
564
|
+
const artifacts = phase.artifacts || [];
|
|
565
|
+
const missing = [];
|
|
566
|
+
for (const art of artifacts) {
|
|
567
|
+
if (art.optional) continue;
|
|
568
|
+
const artPath = this.resolveArtifactPath(art.path);
|
|
569
|
+
if (!existsSync(artPath)) {
|
|
570
|
+
missing.push(artPath);
|
|
571
|
+
} else if (statSync(artPath).size < 5) {
|
|
572
|
+
missing.push(`${artPath} (too small)`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (missing.length > 0) {
|
|
576
|
+
return { ok: false, verdict: 'artifact-missing', missing };
|
|
577
|
+
}
|
|
578
|
+
return { ok: true, verdict: 'pass' };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
case 'llm-judge': {
|
|
582
|
+
if (this.workflow.track === 'lite') {
|
|
583
|
+
// Lite track cannot run LLM judge phases — requires corps
|
|
584
|
+
return {
|
|
585
|
+
ok: false,
|
|
586
|
+
verdict: 'needs-corps',
|
|
587
|
+
message: `Phase "${phase.id}" requires llm-judge gate. Use workflows/corps.yaml for this phase.`,
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
return this.runHeavyJudge(phase);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
case 'human': {
|
|
594
|
+
// Write a gate decision record and pause
|
|
595
|
+
const gateFile = resolve(this.projectRoot, '.as-xflow', 'pending-gates', `${phase.id}.json`);
|
|
596
|
+
const autoApproveHumanGates = ['1', 'true', 'yes'].includes(String(process.env.XFLOW_AUTO_HUMAN_GATES || '').toLowerCase());
|
|
597
|
+
if (existsSync(gateFile)) {
|
|
598
|
+
try {
|
|
599
|
+
const pending = JSON.parse(readFileSync(gateFile, 'utf8'));
|
|
600
|
+
if (pending.status === 'approved') {
|
|
601
|
+
console.log(` ✓ Human gate approved: ${phase.id}`);
|
|
602
|
+
return { ok: true, verdict: 'approved', gate_file: gateFile };
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
// Fall through and rewrite the pending gate record.
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
mkdirSync(dirname(gateFile), { recursive: true });
|
|
609
|
+
const gatePayload = autoApproveHumanGates
|
|
610
|
+
? {
|
|
611
|
+
phase: phase.id,
|
|
612
|
+
status: 'approved',
|
|
613
|
+
created_at: new Date().toISOString(),
|
|
614
|
+
approved_at: new Date().toISOString(),
|
|
615
|
+
approved_by: 'xflow-auto-human-gate',
|
|
616
|
+
instructions: `Automatically approved in scripted workflow mode.`,
|
|
617
|
+
}
|
|
618
|
+
: {
|
|
619
|
+
phase: phase.id,
|
|
620
|
+
status: 'pending_human',
|
|
621
|
+
created_at: new Date().toISOString(),
|
|
622
|
+
instructions: `Review phase "${phase.id}" and run: xflow gate ack ${phase.id}`,
|
|
623
|
+
};
|
|
624
|
+
writeFileSync(gateFile, JSON.stringify(gatePayload, null, 2));
|
|
625
|
+
if (autoApproveHumanGates) {
|
|
626
|
+
console.log(` ✓ Human gate auto-approved: ${phase.id}`);
|
|
627
|
+
return { ok: true, verdict: 'approved', gate_file: gateFile, auto_approved: true };
|
|
628
|
+
}
|
|
629
|
+
console.log(`\n ⏸ Human gate: phase "${phase.id}"`);
|
|
630
|
+
console.log(` Pending gate written to: ${gateFile}`);
|
|
631
|
+
console.log(` To continue: xflow gate ack ${phase.id}`);
|
|
632
|
+
return { ok: false, verdict: 'pending_human', gate_file: gateFile };
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
default:
|
|
636
|
+
return { ok: false, error: `Unknown gate type: ${gate.type}` };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// ─── State management ─────────────────────────────────────────────────────
|
|
641
|
+
|
|
642
|
+
loadState() {
|
|
643
|
+
if (existsSync(this.stateFile)) {
|
|
644
|
+
try {
|
|
645
|
+
const parsed = JSON.parse(readFileSync(this.stateFile, 'utf8'));
|
|
646
|
+
if (this.changeId && parsed?.change_id && parsed.change_id !== this.changeId) {
|
|
647
|
+
return {
|
|
648
|
+
workflow: this.workflow.name,
|
|
649
|
+
change_id: this.changeId,
|
|
650
|
+
started_at: new Date().toISOString(),
|
|
651
|
+
completed_phases: [],
|
|
652
|
+
last_completed_phase: null,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
if (this.changeStatusFile && !existsSync(this.changeStatusFile)) {
|
|
656
|
+
return {
|
|
657
|
+
workflow: this.workflow.name,
|
|
658
|
+
change_id: this.changeId,
|
|
659
|
+
started_at: new Date().toISOString(),
|
|
660
|
+
completed_phases: [],
|
|
661
|
+
last_completed_phase: null,
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
if (
|
|
665
|
+
parsed?.workflow_integrity?.digest
|
|
666
|
+
&& parsed.workflow_integrity.digest !== this.workflowManifest.digest
|
|
667
|
+
) {
|
|
668
|
+
this.fail(null, 'workflow_integrity_mismatch', `state digest ${parsed.workflow_integrity.digest} != current ${this.workflowManifest.digest}`);
|
|
669
|
+
return parsed;
|
|
670
|
+
}
|
|
671
|
+
return parsed;
|
|
672
|
+
} catch (error) {
|
|
673
|
+
if (error instanceof Error && /workflow_integrity_mismatch/.test(error.message)) {
|
|
674
|
+
throw error;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
workflow: this.workflow.name,
|
|
680
|
+
change_id: this.changeId,
|
|
681
|
+
started_at: new Date().toISOString(),
|
|
682
|
+
completed_phases: [],
|
|
683
|
+
last_completed_phase: null,
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
saveState(state) {
|
|
688
|
+
mkdirSync(dirname(this.stateFile), { recursive: true });
|
|
689
|
+
const payload = { ...state, updated_at: new Date().toISOString() };
|
|
690
|
+
payload.workflow_integrity = this.workflowManifest;
|
|
691
|
+
writeFileSync(this.stateFile, JSON.stringify(payload, null, 2));
|
|
692
|
+
if (this.changeStatusFile) {
|
|
693
|
+
mkdirSync(dirname(this.changeStatusFile), { recursive: true });
|
|
694
|
+
let existingStatus = {};
|
|
695
|
+
if (existsSync(this.changeStatusFile)) {
|
|
696
|
+
try {
|
|
697
|
+
existingStatus = JSON.parse(readFileSync(this.changeStatusFile, 'utf8'));
|
|
698
|
+
} catch {
|
|
699
|
+
existingStatus = {};
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
writeFileSync(this.changeStatusFile, JSON.stringify({
|
|
703
|
+
...existingStatus,
|
|
704
|
+
...payload,
|
|
705
|
+
}, null, 2));
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
fail(phase, reason, detail = '') {
|
|
710
|
+
try {
|
|
711
|
+
appendExecutionLogSync(this.projectRoot, {
|
|
712
|
+
kind: 'workflow_failed',
|
|
713
|
+
workflow_run_id: this.runtime.workflowRunId,
|
|
714
|
+
workflow: this.workflow.name,
|
|
715
|
+
track: this.workflow.track,
|
|
716
|
+
workflow_integrity_digest: this.workflowManifest.digest,
|
|
717
|
+
change_id: this.changeId || null,
|
|
718
|
+
mission_id: this.runtime.missionId || null,
|
|
719
|
+
phase_id: phase?.id || null,
|
|
720
|
+
phase_label: phase?.label || null,
|
|
721
|
+
reason,
|
|
722
|
+
detail: detail || null,
|
|
723
|
+
});
|
|
724
|
+
} catch {
|
|
725
|
+
// Failure logging is best-effort and must not hide the real stop reason.
|
|
726
|
+
}
|
|
727
|
+
const phaseId = phase?.id || 'workflow';
|
|
728
|
+
console.error(`\n✗ Workflow "${this.workflow.name}" stopped at phase "${phaseId}"`);
|
|
729
|
+
console.error(` Reason: ${reason}${detail ? ` — ${detail}` : ''}`);
|
|
730
|
+
console.error(` Resume: xflow workflow run workflows/${this.workflow.name}.yaml`);
|
|
731
|
+
if (this.exitOnFailure) {
|
|
732
|
+
process.exit(1);
|
|
733
|
+
}
|
|
734
|
+
throw new Error(`Workflow "${this.workflow.name}" stopped at phase "${phaseId}": ${reason}${detail ? ` — ${detail}` : ''}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
resolveTemplateValue(value) {
|
|
740
|
+
if (typeof value === 'string') {
|
|
741
|
+
return value
|
|
742
|
+
.replace(/\$\{change_id\}/g, this.changeId || '')
|
|
743
|
+
.replace(/\$\{input\.(\w+)\}/g, (_, key) => this.input[key] || '')
|
|
744
|
+
.replace(/\$\{runtime\.mission_id\}/g, this.runtime.missionId || '')
|
|
745
|
+
.replace(/\$\{runtime\.phase_run_id\}/g, this.runtime.phaseRunId || '')
|
|
746
|
+
.replace(/\$\{runtime\.phase_run_session_id\}/g, this.runtime.phaseRunSessionId || '');
|
|
747
|
+
}
|
|
748
|
+
if (Array.isArray(value)) {
|
|
749
|
+
return value.map((entry) => this.resolveTemplateValue(entry));
|
|
750
|
+
}
|
|
751
|
+
if (value && typeof value === 'object') {
|
|
752
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, this.resolveTemplateValue(v)]));
|
|
753
|
+
}
|
|
754
|
+
return value;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
resolveTemplates(obj) {
|
|
758
|
+
return this.resolveTemplateValue(obj || {});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
buildAtomArgs(atomRef, phase = null) {
|
|
762
|
+
const withArgs = this.resolveTemplates(atomRef.with || {});
|
|
763
|
+
const needsMissionId = ['B7.change_mission.bind', 'D2b.mission.show', 'D3a.phase_run.start', 'D3b.phase_run.persist', 'D3c.phase_run.complete', 'D3d.phase_run.fail']
|
|
764
|
+
.includes(atomRef.id);
|
|
765
|
+
if (needsMissionId && !withArgs.mission_id && this.runtime.missionId) {
|
|
766
|
+
withArgs.mission_id = this.runtime.missionId;
|
|
767
|
+
}
|
|
768
|
+
if (['D3b.phase_run.persist', 'D3c.phase_run.complete', 'D3d.phase_run.fail'].includes(atomRef.id)) {
|
|
769
|
+
if (!withArgs.session_id && this.runtime.phaseRunSessionId) {
|
|
770
|
+
withArgs.session_id = this.runtime.phaseRunSessionId;
|
|
771
|
+
}
|
|
772
|
+
if (!withArgs.phase_run_id && this.runtime.phaseRunId) {
|
|
773
|
+
withArgs.phase_run_id = this.runtime.phaseRunId;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (!withArgs.phase && phase?.id && atomRef.id.startsWith('D3')) {
|
|
777
|
+
withArgs.phase = phase.id;
|
|
778
|
+
}
|
|
779
|
+
return withArgs;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
buildAtomBatches(atomRefs = []) {
|
|
783
|
+
const batches = [];
|
|
784
|
+
let currentParallelGroup = null;
|
|
785
|
+
let currentBatch = null;
|
|
786
|
+
|
|
787
|
+
for (const atomRef of atomRefs) {
|
|
788
|
+
const group = atomRef.parallel_group || null;
|
|
789
|
+
if (!group) {
|
|
790
|
+
currentParallelGroup = null;
|
|
791
|
+
currentBatch = null;
|
|
792
|
+
batches.push({ parallel: false, atoms: [atomRef] });
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (currentParallelGroup !== group || !currentBatch) {
|
|
797
|
+
currentParallelGroup = group;
|
|
798
|
+
currentBatch = { parallel: true, group, atoms: [atomRef] };
|
|
799
|
+
batches.push(currentBatch);
|
|
800
|
+
} else {
|
|
801
|
+
currentBatch.atoms.push(atomRef);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
return batches;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
async runParallelBatch(atomRefs = [], phase = null) {
|
|
809
|
+
const results = new Array(atomRefs.length);
|
|
810
|
+
let cursor = 0;
|
|
811
|
+
let running = 0;
|
|
812
|
+
let usedBudget = 0;
|
|
813
|
+
let completed = 0;
|
|
814
|
+
|
|
815
|
+
return new Promise((resolveBatch, rejectBatch) => {
|
|
816
|
+
const launchReady = () => {
|
|
817
|
+
this.ensureNotAborted();
|
|
818
|
+
if (completed >= atomRefs.length) {
|
|
819
|
+
resolveBatch(results);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
while (cursor < atomRefs.length) {
|
|
824
|
+
const atomRef = atomRefs[cursor];
|
|
825
|
+
const charge = this.concurrencyPolicy.chargeFor(atomRef, this.registry);
|
|
826
|
+
if (running > 0 && usedBudget + charge > this.maxParallelAgents) {
|
|
827
|
+
break;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const index = cursor;
|
|
831
|
+
cursor += 1;
|
|
832
|
+
running += 1;
|
|
833
|
+
usedBudget += charge;
|
|
834
|
+
|
|
835
|
+
Promise.resolve()
|
|
836
|
+
.then(() => {
|
|
837
|
+
this.ensureNotAborted();
|
|
838
|
+
return this.runAtom(atomRef, phase);
|
|
839
|
+
})
|
|
840
|
+
.then((result) => {
|
|
841
|
+
results[index] = result;
|
|
842
|
+
})
|
|
843
|
+
.catch(rejectBatch)
|
|
844
|
+
.finally(() => {
|
|
845
|
+
running -= 1;
|
|
846
|
+
usedBudget -= charge;
|
|
847
|
+
completed += 1;
|
|
848
|
+
launchReady();
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
launchReady();
|
|
855
|
+
} catch (error) {
|
|
856
|
+
rejectBatch(error);
|
|
857
|
+
}
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
captureAtomResult(atomId, result) {
|
|
862
|
+
this.runtime.lastAtomResult = result;
|
|
863
|
+
this.runtime.atomResults[atomId] = result;
|
|
864
|
+
if (this.runtime.currentPhaseTokens) {
|
|
865
|
+
addTokenUsage(this.runtime.currentPhaseTokens, extractTokenUsage(result));
|
|
866
|
+
}
|
|
867
|
+
this.runtime.missionId = result.mission?.mission_id || result.mission_id || result.binding?.mission_id || this.runtime.missionId;
|
|
868
|
+
this.runtime.phaseRunId = result.phase_run?.phase_run_id || result.phase_run_id || this.runtime.phaseRunId;
|
|
869
|
+
this.runtime.phaseRunSessionId = result.session?.session_id || result.phase_run_session?.session_id || this.runtime.phaseRunSessionId;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
resolveArtifactPath(pathTemplate) {
|
|
873
|
+
const resolved = pathTemplate
|
|
874
|
+
.replace(/\$\{change_id\}/g, this.changeId || '')
|
|
875
|
+
.replace(/\$\{input\.(\w+)\}/g, (_, key) => this.input[key] || '');
|
|
876
|
+
return resolve(this.projectRoot, resolved);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
ensureNotAborted() {
|
|
880
|
+
if (this.abortSignal?.aborted) {
|
|
881
|
+
const error = new Error('Workflow execution canceled');
|
|
882
|
+
error.name = 'AbortError';
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
loadArtifactPayload(artPath) {
|
|
888
|
+
if (!existsSync(artPath)) {
|
|
889
|
+
return null;
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const raw = readFileSync(artPath, 'utf8');
|
|
893
|
+
return raw.trim() ? JSON.parse(raw) : null;
|
|
894
|
+
} catch {
|
|
895
|
+
return null;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
deriveJudgeVerdict(payload, artPath) {
|
|
900
|
+
if (!payload || typeof payload !== 'object') {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const explicitVerdict = payload.team_run?.verdict?.status
|
|
905
|
+
|| payload.verdict?.status
|
|
906
|
+
|| payload.qa_status
|
|
907
|
+
|| payload.recommendation
|
|
908
|
+
|| payload.status
|
|
909
|
+
|| null;
|
|
910
|
+
|
|
911
|
+
if (explicitVerdict) {
|
|
912
|
+
const normalized = String(explicitVerdict).toLowerCase();
|
|
913
|
+
if (['accept', 'accepted', 'pass', 'passed', 'completed', 'ready', 'active'].includes(normalized)) {
|
|
914
|
+
return { ok: true, verdict: 'pass', source: artPath };
|
|
915
|
+
}
|
|
916
|
+
if (['reject', 'rejected', 'fail', 'failed', 'blocked'].includes(normalized)) {
|
|
917
|
+
return { ok: false, verdict: 'fail', source: artPath };
|
|
918
|
+
}
|
|
919
|
+
if (['needs_human', 'pending_human'].includes(normalized)) {
|
|
920
|
+
return { ok: false, verdict: 'needs_human', source: artPath };
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
const rationale = payload.team_run?.verdict?.rationale || payload.summary || '';
|
|
925
|
+
if (typeof rationale === 'string' && rationale.trim()) {
|
|
926
|
+
return { ok: true, verdict: 'pass', source: artPath };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
return null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
runHeavyJudge(phase) {
|
|
933
|
+
const candidates = [];
|
|
934
|
+
|
|
935
|
+
for (const art of (phase.artifacts || [])) {
|
|
936
|
+
candidates.push(this.resolveArtifactPath(art.path));
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
candidates.push(resolve(this.projectRoot, 'specs', 'changes', this.changeId || '', `${phase.id}.json`));
|
|
940
|
+
const uniqueCandidates = [...new Set(candidates.filter(Boolean))];
|
|
941
|
+
|
|
942
|
+
for (const artPath of uniqueCandidates) {
|
|
943
|
+
const payload = this.loadArtifactPayload(artPath);
|
|
944
|
+
const derived = this.deriveJudgeVerdict(payload, artPath);
|
|
945
|
+
if (derived) {
|
|
946
|
+
return derived;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const existing = uniqueCandidates.find((artPath) => existsSync(artPath));
|
|
951
|
+
if (existing) {
|
|
952
|
+
return { ok: true, verdict: 'pass', source: existing, message: 'Artifact exists; defaulting heavy judge to pass.' };
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
ok: false,
|
|
957
|
+
verdict: 'needs_human',
|
|
958
|
+
message: `No artifact evidence found for llm-judge phase "${phase.id}"`,
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
objectToCliArgs(obj) {
|
|
963
|
+
const args = [];
|
|
964
|
+
for (const [k, v] of Object.entries(obj || {})) {
|
|
965
|
+
if (v === null || v === undefined || v === '') continue;
|
|
966
|
+
const flag = `--${k.replace(/_/g, '-')}`;
|
|
967
|
+
if (typeof v === 'boolean') {
|
|
968
|
+
if (v) args.push(flag);
|
|
969
|
+
} else if (Array.isArray(v)) {
|
|
970
|
+
for (const item of v) args.push(flag, String(item));
|
|
971
|
+
} else if (typeof v === 'object') {
|
|
972
|
+
args.push(flag, JSON.stringify(v));
|
|
973
|
+
} else {
|
|
974
|
+
args.push(flag, String(v));
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return args;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
checkPreconditions(preconditions) {
|
|
981
|
+
for (const pre of preconditions) {
|
|
982
|
+
if (pre.phase_artifact) {
|
|
983
|
+
const artPath = this.resolveArtifactPath(pre.phase_artifact);
|
|
984
|
+
if (pre.predicate === 'exists' && !existsSync(artPath)) {
|
|
985
|
+
console.error(` Precondition failed: ${artPath} does not exist`);
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return true;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
httpAvailable() {
|
|
994
|
+
// Check if agentos HTTP server is reachable
|
|
995
|
+
// For now, assume available if AGENTOS_HTTP env is set
|
|
996
|
+
return !!process.env.AGENTOS_HTTP || !!process.env.AS_XFLOW_HTTP;
|
|
997
|
+
}
|
|
998
|
+
}
|