@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,684 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent_invoke — Heavy track dispatcher for LLM agent execution.
|
|
3
|
+
*
|
|
4
|
+
* Creates task packages for LLM agents to pick up.
|
|
5
|
+
* In a future iteration, will spawn Codex/Claude CLI subprocesses directly.
|
|
6
|
+
*
|
|
7
|
+
* Returns { ok, status, task_file } for workflow state tracking.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, statSync } from 'node:fs';
|
|
10
|
+
import { resolve, dirname } from 'node:path';
|
|
11
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
+
import { persistSyntheticPhaseRun, defaultVerdictForPhase } from '../../agent_team/common.js';
|
|
13
|
+
import { resolveChangePath, writeJsonFile, writeTextFile } from '../change-artifacts.js';
|
|
14
|
+
|
|
15
|
+
const ROOT = resolve(import.meta.dirname, '..', '..', '..');
|
|
16
|
+
const DEFAULT_CODEX_MODEL = 'gpt-5.4';
|
|
17
|
+
const DEFAULT_AGENT_TIMEOUT_MS = 300_000;
|
|
18
|
+
const DEFAULT_HEAVY_CLI_TIMEOUT_MS = 1_800_000;
|
|
19
|
+
|
|
20
|
+
export default async function agentInvoke(args, context) {
|
|
21
|
+
const { workflow, phase, projectRoot, changeId, atomDef } = context;
|
|
22
|
+
const track = workflow.track || atomDef.track || 'heavy';
|
|
23
|
+
const changeDir = resolve(projectRoot, 'specs', 'changes', changeId);
|
|
24
|
+
const agentCfg = atomDef.agent_config || {};
|
|
25
|
+
const expectedArtifact = resolveExpectedArtifact(atomDef, phase, changeId);
|
|
26
|
+
const stubCfg = resolveStubConfig(atomDef.stub_config || {}, args.mode || null);
|
|
27
|
+
const atomId = atomDef.id || context._atomId || args.atom_id || 'agent_invoke';
|
|
28
|
+
const adapterResolution = resolveAdapter(atomDef, phase, projectRoot, expectedArtifact, stubCfg);
|
|
29
|
+
|
|
30
|
+
const promptText = resolvePrompt(atomDef, phase, changeId, changeDir, expectedArtifact, renderAuxiliaryOutputs(stubCfg, changeId, projectRoot));
|
|
31
|
+
|
|
32
|
+
const taskPackage = {
|
|
33
|
+
phase_id: phase?.id,
|
|
34
|
+
phase_label: phase?.label,
|
|
35
|
+
prompt: promptText,
|
|
36
|
+
expected_artifact: expectedArtifact,
|
|
37
|
+
track,
|
|
38
|
+
agent_config: agentCfg,
|
|
39
|
+
resolved_adapter: adapterResolution.adapter,
|
|
40
|
+
adapter_reason: adapterResolution.reason || null,
|
|
41
|
+
atom_id: atomId,
|
|
42
|
+
created_at: new Date().toISOString(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const taskDir = resolve(projectRoot, '.as-xflow', 'pending-tasks');
|
|
46
|
+
mkdirSync(taskDir, { recursive: true });
|
|
47
|
+
const taskId = `${phase.id}.${taskPackage.atom_id}`;
|
|
48
|
+
const taskFile = resolve(taskDir, `${taskId}.json`);
|
|
49
|
+
const promptFile = resolve(taskDir, `${taskId}.prompt.md`);
|
|
50
|
+
writeFileSync(taskFile, JSON.stringify(taskPackage, null, 2));
|
|
51
|
+
writeFileSync(promptFile, promptText);
|
|
52
|
+
|
|
53
|
+
const artifactPath = resolve(projectRoot, expectedArtifact);
|
|
54
|
+
const timeoutMs = resolveAgentTimeoutMs(agentCfg, adapterResolution.adapter);
|
|
55
|
+
const killAfterMs = agentCfg.kill_after_ms || 2_000;
|
|
56
|
+
const retries = agentCfg.retries || 0;
|
|
57
|
+
const retryDelayMs = agentCfg.retry_delay_ms || 0;
|
|
58
|
+
if (adapterResolution.adapter === 'codex_cli') {
|
|
59
|
+
const result = await runWithRetry('codex', runCodexExec, promptText, {
|
|
60
|
+
cwd: projectRoot,
|
|
61
|
+
timeoutMs,
|
|
62
|
+
killAfterMs,
|
|
63
|
+
retries,
|
|
64
|
+
retryDelayMs,
|
|
65
|
+
changeDir,
|
|
66
|
+
expectedArtifact: artifactPath,
|
|
67
|
+
model: agentCfg.model || process.env.XFLOW_CODEX_MODEL || DEFAULT_CODEX_MODEL,
|
|
68
|
+
});
|
|
69
|
+
return {
|
|
70
|
+
...result,
|
|
71
|
+
adapter: 'codex_cli',
|
|
72
|
+
adapter_reason: adapterResolution.reason || null,
|
|
73
|
+
task_file: `.as-xflow/pending-tasks/${taskId}.json`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (adapterResolution.adapter === 'claude_cli') {
|
|
77
|
+
const result = await runWithRetry('claude', runClaudeCli, promptText, {
|
|
78
|
+
cwd: projectRoot,
|
|
79
|
+
timeoutMs,
|
|
80
|
+
killAfterMs,
|
|
81
|
+
retries,
|
|
82
|
+
retryDelayMs,
|
|
83
|
+
changeDir,
|
|
84
|
+
expectedArtifact: artifactPath,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
...result,
|
|
88
|
+
adapter: 'claude_cli',
|
|
89
|
+
adapter_reason: adapterResolution.reason || null,
|
|
90
|
+
task_file: `.as-xflow/pending-tasks/${taskId}.json`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (adapterResolution.adapter === 'pencil_cli') {
|
|
94
|
+
const result = await runWithRetry('pencil', runPencilCli, promptText, {
|
|
95
|
+
cwd: projectRoot,
|
|
96
|
+
timeoutMs,
|
|
97
|
+
killAfterMs,
|
|
98
|
+
retries,
|
|
99
|
+
retryDelayMs,
|
|
100
|
+
changeDir,
|
|
101
|
+
expectedArtifact: artifactPath,
|
|
102
|
+
phaseId: phase?.id || '',
|
|
103
|
+
previewArtifact: resolveChangePath(projectRoot, changeId, 'pencil_preview.png'),
|
|
104
|
+
exportPreview: stubCfg.write_pen === true,
|
|
105
|
+
model: agentCfg.model || null,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
...result,
|
|
109
|
+
adapter: 'pencil_cli',
|
|
110
|
+
task_file: `.as-xflow/pending-tasks/${taskId}.json`,
|
|
111
|
+
adapter_reason: adapterResolution.reason || null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (realAgentRuntimeRequired(atomDef)) {
|
|
116
|
+
const reason = adapterResolution.reason || 'real_agent_runtime_unavailable';
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
status: 'real_agent_runtime_missing',
|
|
120
|
+
adapter: 'stub',
|
|
121
|
+
adapter_reason: reason,
|
|
122
|
+
task_file: `.as-xflow/pending-tasks/${taskId}.json`,
|
|
123
|
+
expected_artifact: artifactPath,
|
|
124
|
+
agent_config: agentCfg,
|
|
125
|
+
error: `Strict corps runtime forbids stub fallback (${reason}). Install/authenticate the required agent runtime and rerun.`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const missionId = context.runtime?.missionId || args.mission_id || null;
|
|
130
|
+
const sessionId = context.runtime?.phaseRunSessionId || args.session_id || null;
|
|
131
|
+
const dataFile = args.data_file || resolve(projectRoot, '.as-xflow', 'state.sqlite');
|
|
132
|
+
const syntheticPhase = stubCfg.phase || phase.id;
|
|
133
|
+
const fallbackStatus = adapterResolution.adapter === 'stub' && adapterResolution.reason?.startsWith('pencil_')
|
|
134
|
+
? 'pencil_stubbed'
|
|
135
|
+
: 'task_queued';
|
|
136
|
+
const summary = adapterResolution.adapter === 'stub' && adapterResolution.reason
|
|
137
|
+
? `${stubCfg.summary || `Phase ${phase.id} queued for agent execution`} (${adapterResolution.reason})`
|
|
138
|
+
: (stubCfg.summary || `Phase ${phase.id} queued for agent execution`);
|
|
139
|
+
if (missionId) {
|
|
140
|
+
await persistSyntheticPhaseRun({
|
|
141
|
+
projectRoot,
|
|
142
|
+
changeId,
|
|
143
|
+
missionId,
|
|
144
|
+
dataFile,
|
|
145
|
+
phase: syntheticPhase,
|
|
146
|
+
sessionId,
|
|
147
|
+
summary,
|
|
148
|
+
findings: [],
|
|
149
|
+
verdict: stubCfg.verdict || defaultVerdictForPhase(syntheticPhase, args.mode || null),
|
|
150
|
+
extraTeamRun: materializeValue(stubCfg.extra_team_run || {}, { changeId, phaseId: phase.id, phaseLabel: phase.label || '', projectRoot }),
|
|
151
|
+
extraPhaseEntry: { status: 'completed', ...materializeValue(stubCfg.extra_phase_entry || {}, { changeId, phaseId: phase.id, phaseLabel: phase.label || '', projectRoot }) },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
writeJsonFile(artifactPath, {
|
|
156
|
+
phase_id: phase.id,
|
|
157
|
+
phase: syntheticPhase,
|
|
158
|
+
status: 'task_queued',
|
|
159
|
+
summary,
|
|
160
|
+
...(materializeValue(stubCfg.primary_artifact_json || {}, { changeId, phaseId: phase.id, phaseLabel: phase.label || '', projectRoot })),
|
|
161
|
+
});
|
|
162
|
+
writeAuxiliaryStubArtifacts(stubCfg, { projectRoot, changeId, phase });
|
|
163
|
+
|
|
164
|
+
console.log(` [AGENT_TASK] ${taskId} → .as-xflow/pending-tasks/${taskId}.json`);
|
|
165
|
+
return {
|
|
166
|
+
ok: true,
|
|
167
|
+
status: fallbackStatus,
|
|
168
|
+
adapter: 'stub',
|
|
169
|
+
task_file: `.as-xflow/pending-tasks/${taskId}.json`,
|
|
170
|
+
expected_artifact: artifactPath,
|
|
171
|
+
agent_config: agentCfg,
|
|
172
|
+
adapter_reason: adapterResolution.reason || null,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function resolvePrompt(atomDef, phase, changeId, changeDir, expectedArtifact, auxiliaryOutputs) {
|
|
177
|
+
if (atomDef.prompt_template) {
|
|
178
|
+
const tmplPath = resolve(ROOT, atomDef.prompt_template);
|
|
179
|
+
if (existsSync(tmplPath)) {
|
|
180
|
+
let text = readFileSync(tmplPath, 'utf8');
|
|
181
|
+
text = text
|
|
182
|
+
.replace(/\{\{change_id\}\}/g, changeId || '')
|
|
183
|
+
.replace(/\{\{phase_id\}\}/g, phase?.id || '')
|
|
184
|
+
.replace(/\{\{phase_label\}\}/g, phase?.label || '')
|
|
185
|
+
.replace(/\{\{artifact_dir\}\}/g, changeDir)
|
|
186
|
+
.replace(/\{\{expected_artifact\}\}/g, expectedArtifact || '')
|
|
187
|
+
.replace(/\{\{auxiliary_outputs\}\}/g, auxiliaryOutputs || '(none)')
|
|
188
|
+
.replace(/\{\{phase_guidance\}\}/g, phaseSpecificGuidance(phase?.id || '', expectedArtifact))
|
|
189
|
+
.replace(/\{\{intake\}\}/g, loadIntake(changeDir));
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return atomDef.prompt || `Execute phase "${phase?.id}" (${phase?.label}). Primary artifact should be written to ${expectedArtifact || changeDir}.`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function phaseSpecificGuidance(phaseId, expectedArtifact) {
|
|
197
|
+
const artifactNote = expectedArtifact ? `Primary artifact: \`${expectedArtifact}\`` : 'Primary artifact: use the expected phase artifact path.';
|
|
198
|
+
const guidance = {
|
|
199
|
+
explore: [
|
|
200
|
+
artifactNote,
|
|
201
|
+
'- Summarize same-category evidence, IA takeaways, and non-negotiable product invariants.',
|
|
202
|
+
'- Refresh `findings.md` when it helps later phases move faster.',
|
|
203
|
+
'- Do not start freeform implementation in this phase.',
|
|
204
|
+
],
|
|
205
|
+
brainstorm: [
|
|
206
|
+
artifactNote,
|
|
207
|
+
'- Generate multiple concrete solution directions, then recommend one with explicit tradeoffs.',
|
|
208
|
+
'- Keep the artifact structured and decision-oriented, not essay-like.',
|
|
209
|
+
'- Do not build product UI in brainstorm; freeze directional options for downstream selection.',
|
|
210
|
+
],
|
|
211
|
+
risk_review: [
|
|
212
|
+
artifactNote,
|
|
213
|
+
'- Identify execution, product, design, and workflow risks with mitigation ideas.',
|
|
214
|
+
'- Prefer concise structured risk entries with severity and action.',
|
|
215
|
+
],
|
|
216
|
+
clarify: [
|
|
217
|
+
artifactNote,
|
|
218
|
+
'- Resolve or explicitly list ambiguity, assumptions, and questions that affect execution.',
|
|
219
|
+
'- If assumptions are necessary, make them concrete and scoped.',
|
|
220
|
+
],
|
|
221
|
+
proposal: [
|
|
222
|
+
artifactNote,
|
|
223
|
+
'- Update both `proposal.md` and `proposal.json` in the change directory.',
|
|
224
|
+
'- Convert the chosen direction into clear scope, out-of-scope, acceptance intent, and design direction.',
|
|
225
|
+
],
|
|
226
|
+
ux_design_brief: [
|
|
227
|
+
artifactNote,
|
|
228
|
+
'- Produce a concrete UX brief: IA, primary flows, key states, visual direction, and interaction constraints.',
|
|
229
|
+
'- This is still pre-implementation; focus on design and product shape, not code volume.',
|
|
230
|
+
],
|
|
231
|
+
plan: [
|
|
232
|
+
artifactNote,
|
|
233
|
+
'- Update both `plan.md` and `plan.json` with a real execution plan.',
|
|
234
|
+
'- The plan must be implementation-ready and aligned with the frozen competitor contract.',
|
|
235
|
+
],
|
|
236
|
+
};
|
|
237
|
+
return (guidance[phaseId] || [
|
|
238
|
+
artifactNote,
|
|
239
|
+
'- Produce the concrete downstream artifact required for this phase.',
|
|
240
|
+
'- Keep the output structured, phase-appropriate, and ready for the next gate.',
|
|
241
|
+
]).join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function loadIntake(changeDir) {
|
|
245
|
+
const intakeFile = resolve(changeDir, 'intake.json');
|
|
246
|
+
if (existsSync(intakeFile)) {
|
|
247
|
+
try { return JSON.stringify(JSON.parse(readFileSync(intakeFile, 'utf8')), null, 2); } catch { /* */ }
|
|
248
|
+
}
|
|
249
|
+
return '(no intake context available)';
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function resolveArtifactPath(artifacts, changeId, projectRoot) {
|
|
253
|
+
const firstRequired = artifacts?.find(a => !a.optional);
|
|
254
|
+
if (firstRequired?.path) {
|
|
255
|
+
const resolved = firstRequired.path.replace(/\$\{change_id\}/g, changeId);
|
|
256
|
+
return resolve(projectRoot, resolved);
|
|
257
|
+
}
|
|
258
|
+
return resolve(projectRoot, 'specs', 'changes', changeId, 'artifact.json');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function runProc(cmd, args, opts) {
|
|
262
|
+
return new Promise((resolve, reject) => {
|
|
263
|
+
const child = spawn(cmd, args, {
|
|
264
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
265
|
+
cwd: opts.cwd,
|
|
266
|
+
});
|
|
267
|
+
let stdout = '';
|
|
268
|
+
let stderr = '';
|
|
269
|
+
let timedOut = false;
|
|
270
|
+
let settled = false;
|
|
271
|
+
let forceKilled = false;
|
|
272
|
+
let earlyArtifactExit = false;
|
|
273
|
+
let resolvedArtifact = null;
|
|
274
|
+
let timeoutHandle = null;
|
|
275
|
+
let killHandle = null;
|
|
276
|
+
let artifactPollHandle = null;
|
|
277
|
+
child.stdout.on('data', (d) => { stdout += d.toString(); });
|
|
278
|
+
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
279
|
+
child.on('error', (error) => {
|
|
280
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
281
|
+
if (killHandle) clearTimeout(killHandle);
|
|
282
|
+
if (artifactPollHandle) clearInterval(artifactPollHandle);
|
|
283
|
+
reject(error);
|
|
284
|
+
});
|
|
285
|
+
child.on('close', (code, signal) => {
|
|
286
|
+
if (settled) return;
|
|
287
|
+
settled = true;
|
|
288
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
289
|
+
if (killHandle) clearTimeout(killHandle);
|
|
290
|
+
if (artifactPollHandle) clearInterval(artifactPollHandle);
|
|
291
|
+
resolve({ code, signal, stdout, stderr, timedOut, forceKilled, earlyArtifactExit, artifact: resolvedArtifact });
|
|
292
|
+
});
|
|
293
|
+
if (opts.expectedArtifact && opts.earlyArtifactExit !== false) {
|
|
294
|
+
const artifactPollMs = opts.artifactPollMs || 100;
|
|
295
|
+
artifactPollHandle = setInterval(() => {
|
|
296
|
+
if (settled || resolvedArtifact) return;
|
|
297
|
+
if (!existsSync(opts.expectedArtifact)) return;
|
|
298
|
+
try {
|
|
299
|
+
resolvedArtifact = JSON.parse(readFileSync(opts.expectedArtifact, 'utf8'));
|
|
300
|
+
} catch {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
earlyArtifactExit = true;
|
|
304
|
+
stderr += '\n[agent_invoke] valid artifact detected before process exit; stopping agent process early';
|
|
305
|
+
child.kill('SIGTERM');
|
|
306
|
+
const killAfterMs = opts.killAfterMs || 2_000;
|
|
307
|
+
if (killHandle) clearTimeout(killHandle);
|
|
308
|
+
killHandle = setTimeout(() => {
|
|
309
|
+
if (settled) return;
|
|
310
|
+
forceKilled = true;
|
|
311
|
+
stderr += `\n[agent_invoke] force killed after ${killAfterMs}ms grace period`;
|
|
312
|
+
child.kill('SIGKILL');
|
|
313
|
+
}, killAfterMs);
|
|
314
|
+
}, artifactPollMs);
|
|
315
|
+
}
|
|
316
|
+
if (opts.timeoutMs > 0) {
|
|
317
|
+
timeoutHandle = setTimeout(() => {
|
|
318
|
+
if (settled) return;
|
|
319
|
+
timedOut = true;
|
|
320
|
+
stderr += `\n[agent_invoke] timed out after ${opts.timeoutMs}ms`;
|
|
321
|
+
child.kill('SIGTERM');
|
|
322
|
+
const killAfterMs = opts.killAfterMs || 2_000;
|
|
323
|
+
killHandle = setTimeout(() => {
|
|
324
|
+
if (settled) return;
|
|
325
|
+
forceKilled = true;
|
|
326
|
+
stderr += `\n[agent_invoke] force killed after ${killAfterMs}ms grace period`;
|
|
327
|
+
child.kill('SIGKILL');
|
|
328
|
+
}, killAfterMs);
|
|
329
|
+
}, opts.timeoutMs);
|
|
330
|
+
}
|
|
331
|
+
// An agent CLI may exit without reading stdin (e.g. it fails fast or ignores the prompt pipe).
|
|
332
|
+
// Writing to the closed pipe then raises EPIPE asynchronously; the exit handler reports the real result.
|
|
333
|
+
child.stdin.on('error', (err) => {
|
|
334
|
+
if (err && err.code === 'EPIPE') return;
|
|
335
|
+
stderr += `\n[agent_invoke] stdin error: ${err && err.message}`;
|
|
336
|
+
});
|
|
337
|
+
child.stdin.write(opts.input || '');
|
|
338
|
+
child.stdin.end();
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function resolveOutputJson(expectedPath, timeoutMs) {
|
|
343
|
+
const start = Date.now();
|
|
344
|
+
while (Date.now() - start < timeoutMs) {
|
|
345
|
+
if (existsSync(expectedPath)) {
|
|
346
|
+
try { return JSON.parse(readFileSync(expectedPath, 'utf8')); } catch { }
|
|
347
|
+
}
|
|
348
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function runCodexExec(prompt, opts) {
|
|
354
|
+
const args = ['exec', '--full-auto', '--json', '--skip-git-repo-check', '-C', opts.cwd];
|
|
355
|
+
if (opts.model) {
|
|
356
|
+
args.push('--model', opts.model);
|
|
357
|
+
}
|
|
358
|
+
args.push(prompt);
|
|
359
|
+
let { code, signal, stdout, stderr, timedOut, forceKilled, earlyArtifactExit, artifact } = await runProc('codex', args, { cwd: opts.cwd, timeoutMs: opts.timeoutMs, killAfterMs: opts.killAfterMs, input: '', expectedArtifact: opts.expectedArtifact });
|
|
360
|
+
const output = artifact || await resolveOutputJson(opts.expectedArtifact, 10_000);
|
|
361
|
+
let timedOutRecovered = false;
|
|
362
|
+
if (output && timedOut) {
|
|
363
|
+
timedOutRecovered = true;
|
|
364
|
+
code = 0;
|
|
365
|
+
signal = null;
|
|
366
|
+
timedOut = false;
|
|
367
|
+
stderr += '\n[agent_invoke] valid artifact detected after timeout; accepting artifact output';
|
|
368
|
+
}
|
|
369
|
+
if (output && earlyArtifactExit) {
|
|
370
|
+
code = 0;
|
|
371
|
+
signal = null;
|
|
372
|
+
}
|
|
373
|
+
return { code, signal, stdout, stderr, artifact: output, timedOut, forceKilled, timedOutRecovered, earlyArtifactExit };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function runClaudeCli(prompt, opts) {
|
|
377
|
+
let { code, signal, stdout, stderr, timedOut, forceKilled, earlyArtifactExit, artifact } = await runProc('claude', [
|
|
378
|
+
'-p', '--dangerously-skip-permissions', '--no-session-persistence', prompt,
|
|
379
|
+
], { cwd: opts.cwd, timeoutMs: opts.timeoutMs, killAfterMs: opts.killAfterMs, input: '', expectedArtifact: opts.expectedArtifact });
|
|
380
|
+
const output = artifact || await resolveOutputJson(opts.expectedArtifact, 10_000);
|
|
381
|
+
let timedOutRecovered = false;
|
|
382
|
+
if (output && timedOut) {
|
|
383
|
+
timedOutRecovered = true;
|
|
384
|
+
code = 0;
|
|
385
|
+
signal = null;
|
|
386
|
+
timedOut = false;
|
|
387
|
+
stderr += '\n[agent_invoke] valid artifact detected after timeout; accepting artifact output';
|
|
388
|
+
}
|
|
389
|
+
if (output && earlyArtifactExit) {
|
|
390
|
+
code = 0;
|
|
391
|
+
signal = null;
|
|
392
|
+
}
|
|
393
|
+
return { code, signal, stdout, stderr, artifact: output, timedOut, forceKilled, timedOutRecovered, earlyArtifactExit };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
async function runPencilCli(prompt, opts) {
|
|
397
|
+
const outputPath = opts.expectedArtifact;
|
|
398
|
+
mkdirSync(dirname(outputPath), { recursive: true });
|
|
399
|
+
const existingInput = existsSync(outputPath) ? outputPath : null;
|
|
400
|
+
const args = ['--out', outputPath, '--prompt', prompt, '--workspace', opts.cwd];
|
|
401
|
+
if (existingInput && opts.phaseId === 'pencil_refine') {
|
|
402
|
+
args.unshift('--in', existingInput);
|
|
403
|
+
}
|
|
404
|
+
if (opts.model) {
|
|
405
|
+
args.push('--model', opts.model);
|
|
406
|
+
}
|
|
407
|
+
if (opts.exportPreview) {
|
|
408
|
+
args.push('--export', opts.previewArtifact, '--export-type', 'png');
|
|
409
|
+
}
|
|
410
|
+
let { code, signal, stdout, stderr, timedOut, forceKilled } = await runProc('pencil', args, {
|
|
411
|
+
cwd: opts.cwd,
|
|
412
|
+
timeoutMs: opts.timeoutMs,
|
|
413
|
+
killAfterMs: opts.killAfterMs,
|
|
414
|
+
input: '',
|
|
415
|
+
});
|
|
416
|
+
const artifact = resolvePencilArtifact(outputPath, opts, { requirePreview: code !== 0 || timedOut });
|
|
417
|
+
let artifactRecovered = false;
|
|
418
|
+
if (artifact && (code !== 0 || timedOut)) {
|
|
419
|
+
artifactRecovered = true;
|
|
420
|
+
code = 0;
|
|
421
|
+
signal = null;
|
|
422
|
+
timedOut = false;
|
|
423
|
+
stderr += '\n[agent_invoke] valid pencil artifact detected despite CLI failure; accepting artifact output';
|
|
424
|
+
}
|
|
425
|
+
return { code, signal, stdout, stderr, artifact, timedOut, forceKilled, artifactRecovered };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function resolvePencilArtifact(outputPath, opts, { requirePreview = false } = {}) {
|
|
429
|
+
if (!fileReady(outputPath)) return null;
|
|
430
|
+
if (requirePreview && opts.exportPreview && !fileReady(opts.previewArtifact)) return null;
|
|
431
|
+
return {
|
|
432
|
+
type: 'pencil_pen',
|
|
433
|
+
path: outputPath,
|
|
434
|
+
preview_path: fileReady(opts.previewArtifact) ? opts.previewArtifact : null,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function fileReady(path) {
|
|
439
|
+
if (!path || !existsSync(path)) return false;
|
|
440
|
+
try {
|
|
441
|
+
return statSync(path).size > 0;
|
|
442
|
+
} catch {
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async function runWithRetry(label, runner, prompt, opts) {
|
|
448
|
+
const retries = opts.retries || 0;
|
|
449
|
+
const retryDelayMs = opts.retryDelayMs || 0;
|
|
450
|
+
const attempts = [];
|
|
451
|
+
for (let attempt = 1; attempt <= retries + 1; attempt += 1) {
|
|
452
|
+
const result = await runner(prompt, opts);
|
|
453
|
+
const normalized = { attempt, ...result };
|
|
454
|
+
attempts.push(normalized);
|
|
455
|
+
if (!shouldRetry(normalized) || attempt > retries) {
|
|
456
|
+
return finalizeAttemptSeries(label, attempts);
|
|
457
|
+
}
|
|
458
|
+
if (retryDelayMs > 0) {
|
|
459
|
+
await new Promise((resolveDelay) => setTimeout(resolveDelay, retryDelayMs));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return finalizeAttemptSeries(label, attempts);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function shouldRetry(result) {
|
|
466
|
+
if (result.timedOut) return true;
|
|
467
|
+
return result.code !== 0;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function finalizeAttemptSeries(label, attempts) {
|
|
471
|
+
const last = attempts[attempts.length - 1] || {};
|
|
472
|
+
const succeeded = last.code === 0 && !last.timedOut;
|
|
473
|
+
const retriesUsed = Math.max(0, attempts.length - 1);
|
|
474
|
+
return {
|
|
475
|
+
ok: succeeded,
|
|
476
|
+
status: succeeded ? `${label}_completed` : (last.timedOut ? `${label}_timed_out` : `${label}_failed`),
|
|
477
|
+
code: last.code,
|
|
478
|
+
signal: last.signal || null,
|
|
479
|
+
stdout: last.stdout || '',
|
|
480
|
+
stderr: last.stderr || '',
|
|
481
|
+
artifact: last.artifact || null,
|
|
482
|
+
timed_out: last.timedOut === true,
|
|
483
|
+
force_killed: last.forceKilled === true,
|
|
484
|
+
attempts: attempts.length,
|
|
485
|
+
retries_used: retriesUsed,
|
|
486
|
+
attempt_results: attempts.map(({ attempt, code, signal, timedOut, forceKilled, timedOutRecovered, earlyArtifactExit, artifactRecovered }) => ({
|
|
487
|
+
attempt,
|
|
488
|
+
code,
|
|
489
|
+
signal: signal || null,
|
|
490
|
+
timed_out: timedOut === true,
|
|
491
|
+
force_killed: forceKilled === true,
|
|
492
|
+
timed_out_recovered: timedOutRecovered === true,
|
|
493
|
+
early_artifact_exit: earlyArtifactExit === true,
|
|
494
|
+
artifact_recovered: artifactRecovered === true,
|
|
495
|
+
})),
|
|
496
|
+
timed_out_recovered: last.timedOutRecovered === true,
|
|
497
|
+
early_artifact_exit: last.earlyArtifactExit === true,
|
|
498
|
+
artifact_recovered: last.artifactRecovered === true,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function commandExists(command, projectRoot) {
|
|
503
|
+
const result = spawnSync('which', [command], {
|
|
504
|
+
cwd: projectRoot,
|
|
505
|
+
encoding: 'utf8',
|
|
506
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
507
|
+
});
|
|
508
|
+
return result.status === 0 && Boolean(result.stdout.trim());
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function pencilAuthenticated(projectRoot) {
|
|
512
|
+
const result = spawnSync('pencil', ['status'], {
|
|
513
|
+
cwd: projectRoot,
|
|
514
|
+
encoding: 'utf8',
|
|
515
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
516
|
+
});
|
|
517
|
+
return result.status === 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export function resolveAgentTimeoutMs(agentCfg = {}, adapter = 'stub') {
|
|
521
|
+
const configured = positiveInteger(agentCfg.timeout_ms);
|
|
522
|
+
if (configured !== null) return configured;
|
|
523
|
+
|
|
524
|
+
const adapterEnv = {
|
|
525
|
+
codex_cli: 'XFLOW_CODEX_TIMEOUT_MS',
|
|
526
|
+
claude_cli: 'XFLOW_CLAUDE_TIMEOUT_MS',
|
|
527
|
+
pencil_cli: 'XFLOW_PENCIL_TIMEOUT_MS',
|
|
528
|
+
}[adapter];
|
|
529
|
+
const adapterTimeout = adapterEnv ? readPositiveIntegerEnv(adapterEnv) : null;
|
|
530
|
+
if (adapterTimeout !== null) return adapterTimeout;
|
|
531
|
+
|
|
532
|
+
const genericTimeout = readPositiveIntegerEnv('XFLOW_AGENT_TIMEOUT_MS');
|
|
533
|
+
if (genericTimeout !== null) return genericTimeout;
|
|
534
|
+
|
|
535
|
+
if (adapter === 'codex_cli' || adapter === 'claude_cli') {
|
|
536
|
+
return DEFAULT_HEAVY_CLI_TIMEOUT_MS;
|
|
537
|
+
}
|
|
538
|
+
return DEFAULT_AGENT_TIMEOUT_MS;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function readPositiveIntegerEnv(name) {
|
|
542
|
+
return positiveInteger(process.env[name]);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function positiveInteger(value) {
|
|
546
|
+
const n = Number(value);
|
|
547
|
+
if (!Number.isFinite(n) || n <= 0) return null;
|
|
548
|
+
return Math.floor(n);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function autoCliRuntimeEnabled(atomDef) {
|
|
552
|
+
if (atomDef?.agent_config?.auto_cli === true) return true;
|
|
553
|
+
const flag = String(process.env.XFLOW_AUTO_AGENT_RUNTIME || '').trim().toLowerCase();
|
|
554
|
+
return flag === '1' || flag === 'true' || flag === 'yes';
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function realAgentRuntimeRequired(atomDef) {
|
|
558
|
+
if (atomDef?.agent_config?.require_real_runtime === true) return true;
|
|
559
|
+
const flag = String(process.env.XFLOW_REQUIRE_REAL_AGENT_RUNTIME || '').trim().toLowerCase();
|
|
560
|
+
return flag === '1' || flag === 'true' || flag === 'yes';
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function resolveAdapter(atomDef, phase, projectRoot, expectedArtifact, stubCfg) {
|
|
564
|
+
const configured = atomDef.agent_config?.adapter || 'auto';
|
|
565
|
+
if (configured !== 'auto') {
|
|
566
|
+
return { adapter: configured, reason: 'explicit_adapter' };
|
|
567
|
+
}
|
|
568
|
+
if (isPencilPhase(atomDef, phase, expectedArtifact, stubCfg)) {
|
|
569
|
+
if (!commandExists('pencil', projectRoot)) {
|
|
570
|
+
return { adapter: 'stub', reason: 'pencil_missing' };
|
|
571
|
+
}
|
|
572
|
+
if (!pencilAuthenticated(projectRoot)) {
|
|
573
|
+
return { adapter: 'stub', reason: 'pencil_unauthenticated' };
|
|
574
|
+
}
|
|
575
|
+
return { adapter: 'pencil_cli', reason: 'pencil_authenticated' };
|
|
576
|
+
}
|
|
577
|
+
if (autoCliRuntimeEnabled(atomDef) && commandExists('codex', projectRoot)) {
|
|
578
|
+
return { adapter: 'codex_cli', reason: 'codex_available' };
|
|
579
|
+
}
|
|
580
|
+
if (autoCliRuntimeEnabled(atomDef) && commandExists('claude', projectRoot)) {
|
|
581
|
+
return { adapter: 'claude_cli', reason: 'claude_available' };
|
|
582
|
+
}
|
|
583
|
+
return { adapter: 'stub', reason: 'auto_stub_default' };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
function isPencilPhase(atomDef, phase, expectedArtifact, stubCfg) {
|
|
587
|
+
if (stubCfg.write_pen === true) return true;
|
|
588
|
+
if ((phase?.id || '').startsWith('pencil_')) return true;
|
|
589
|
+
if ((atomDef.id || '').startsWith('H4')) return true;
|
|
590
|
+
return String(expectedArtifact || '').endsWith('.pen');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function resolveExpectedArtifact(atomDef, phase, changeId) {
|
|
594
|
+
const preferPhaseArtifact = atomDef.stub_config?.use_phase_artifact !== false;
|
|
595
|
+
const configured = atomDef.expected_artifact;
|
|
596
|
+
if (configured) {
|
|
597
|
+
return configured.replace(/\$\{change_id\}/g, changeId);
|
|
598
|
+
}
|
|
599
|
+
const firstRequired = phase?.artifacts?.find((artifact) => !artifact.optional)?.path;
|
|
600
|
+
if (preferPhaseArtifact && firstRequired) {
|
|
601
|
+
return firstRequired.replace(/\$\{change_id\}/g, changeId);
|
|
602
|
+
}
|
|
603
|
+
if (firstRequired) {
|
|
604
|
+
return firstRequired.replace(/\$\{change_id\}/g, changeId);
|
|
605
|
+
}
|
|
606
|
+
return `specs/changes/${changeId}/artifact.json`;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function resolveStubConfig(baseConfig, mode) {
|
|
610
|
+
if (!mode || !baseConfig?.mode_overrides?.[mode]) {
|
|
611
|
+
return baseConfig;
|
|
612
|
+
}
|
|
613
|
+
const { mode_overrides: _ignored, ...base } = baseConfig;
|
|
614
|
+
const override = baseConfig.mode_overrides[mode];
|
|
615
|
+
return {
|
|
616
|
+
...base,
|
|
617
|
+
...override,
|
|
618
|
+
extra_team_run: { ...(base.extra_team_run || {}), ...(override.extra_team_run || {}) },
|
|
619
|
+
extra_phase_entry: { ...(base.extra_phase_entry || {}), ...(override.extra_phase_entry || {}) },
|
|
620
|
+
primary_artifact_json: { ...(base.primary_artifact_json || {}), ...(override.primary_artifact_json || {}) },
|
|
621
|
+
extra_json_files: [...(base.extra_json_files || []), ...(override.extra_json_files || [])],
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function materializeValue(value, vars) {
|
|
626
|
+
if (typeof value === 'string') {
|
|
627
|
+
return value
|
|
628
|
+
.replace(/\$\{change_id\}/g, vars.changeId || '')
|
|
629
|
+
.replace(/\$\{phase_id\}/g, vars.phaseId || '')
|
|
630
|
+
.replace(/\$\{phase_label\}/g, vars.phaseLabel || '')
|
|
631
|
+
.replace(/\$\{project_root\}/g, vars.projectRoot || '');
|
|
632
|
+
}
|
|
633
|
+
if (Array.isArray(value)) {
|
|
634
|
+
return value.map((item) => materializeValue(item, vars));
|
|
635
|
+
}
|
|
636
|
+
if (value && typeof value === 'object') {
|
|
637
|
+
return Object.fromEntries(Object.entries(value).map(([key, val]) => [key, materializeValue(val, vars)]));
|
|
638
|
+
}
|
|
639
|
+
return value;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function renderAuxiliaryOutputs(stubCfg, changeId, projectRoot) {
|
|
643
|
+
const lines = [];
|
|
644
|
+
if (stubCfg.write_pen) {
|
|
645
|
+
lines.push(`- \`${resolveChangePath(projectRoot, changeId, 'pencil_output.pen')}\``);
|
|
646
|
+
}
|
|
647
|
+
if (stubCfg.write_attestation) {
|
|
648
|
+
lines.push(`- \`${resolveChangePath(projectRoot, changeId, 'pencil_output.attestation.json')}\``);
|
|
649
|
+
}
|
|
650
|
+
for (const file of stubCfg.extra_json_files || []) {
|
|
651
|
+
const renderedPath = materializeValue(file.path || '', { changeId, projectRoot, phaseId: '', phaseLabel: '' });
|
|
652
|
+
if (renderedPath) {
|
|
653
|
+
lines.push(`- \`${resolve(projectRoot, renderedPath)}\``);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return lines.length > 0 ? lines.join('\n') : '(none)';
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function writeAuxiliaryStubArtifacts(stubCfg, { projectRoot, changeId, phase }) {
|
|
660
|
+
if (stubCfg.write_pen) {
|
|
661
|
+
writeTextFile(
|
|
662
|
+
resolveChangePath(projectRoot, changeId, 'pencil_output.pen'),
|
|
663
|
+
stubCfg.pen_content || `PENCIL ${phase.id}\n${stubCfg.summary || `Phase ${phase.id} queued for agent execution`}\n`,
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
if (stubCfg.write_attestation) {
|
|
667
|
+
writeJsonFile(resolveChangePath(projectRoot, changeId, 'pencil_output.attestation.json'), {
|
|
668
|
+
phase: phase.id,
|
|
669
|
+
change_id: changeId,
|
|
670
|
+
accepted: true,
|
|
671
|
+
recorded_at: new Date().toISOString(),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
for (const file of stubCfg.extra_json_files || []) {
|
|
675
|
+
const path = materializeValue(file.path || '', { changeId, projectRoot, phaseId: phase.id, phaseLabel: phase.label || '' });
|
|
676
|
+
if (!path) continue;
|
|
677
|
+
writeJsonFile(resolve(projectRoot, path), materializeValue(file.payload || {}, {
|
|
678
|
+
changeId,
|
|
679
|
+
projectRoot,
|
|
680
|
+
phaseId: phase.id,
|
|
681
|
+
phaseLabel: phase.label || '',
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
}
|