@surlymochan/evalid 0.0.0-stage → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +94 -0
  2. package/LICENSE +21 -0
  3. package/README.md +315 -2
  4. package/RELEASE_NOTES.md +124 -0
  5. package/atoms/registry.json +758 -0
  6. package/bin/xflow.js +7261 -0
  7. package/docs/adoption/README.md +128 -0
  8. package/docs/adoption/openflow-release-hardening.md +61 -0
  9. package/docs/benchmark-openflow-vs-gstack-superpowers-openspec-2026-05-25.md +81 -0
  10. package/docs/compatibility.md +52 -0
  11. package/docs/competitive-benchmark.md +108 -0
  12. package/docs/corps-operator-guide.md +75 -0
  13. package/docs/demo-proof.md +126 -0
  14. package/docs/examples-gallery.md +115 -0
  15. package/docs/fixtures/tracker-item.json +7 -0
  16. package/docs/goal-vs-codex.md +129 -0
  17. package/docs/gstack-comparison.md +88 -0
  18. package/docs/install-upgrade.md +133 -0
  19. package/docs/integrations.md +49 -0
  20. package/docs/launch-demo.md +123 -0
  21. package/docs/launch-dossier.md +180 -0
  22. package/docs/methodology.md +149 -0
  23. package/docs/npm-publish-handoff.md +96 -0
  24. package/docs/openspec-migration.md +106 -0
  25. package/docs/public-benchmark.md +68 -0
  26. package/docs/public-release.md +145 -0
  27. package/docs/quality-assessment.md +139 -0
  28. package/docs/quickstart.md +156 -0
  29. package/docs/reviewer-guide.md +43 -0
  30. package/docs/spec-kit-benchmark.md +73 -0
  31. package/docs/super-assistant-comparison.md +106 -0
  32. package/docs/superpowers-comparison.md +82 -0
  33. package/docs/team-adoption.md +123 -0
  34. package/docs/tooling-matrix.md +91 -0
  35. package/docs/walkthrough.md +76 -0
  36. package/docs/workflow-manual.md +1130 -0
  37. package/package.json +83 -4
  38. package/prompts/pencil-draft.md +46 -0
  39. package/prompts/pencil-refine.md +46 -0
  40. package/prompts/phase-artifact.md +24 -0
  41. package/prompts/phase-execution.md +32 -0
  42. package/schemas/competitor-reconstruction-contract.schema.json +91 -0
  43. package/schemas/llm-design-review-aggregate.schema.json +83 -0
  44. package/schemas/visual-benchmark.schema.json +978 -0
  45. package/schemas/workflow.schema.json +100 -0
  46. package/scripts/run-tests.mjs +23 -0
  47. package/scripts/takein_digest.py +139 -0
  48. package/specs/README.md +118 -0
  49. package/specs/architecture.md +36 -0
  50. package/specs/product.md +33 -0
  51. package/specs/workflow.md +349 -0
  52. package/src/agent_team/atoms/codex-app-bridge.js +8 -0
  53. package/src/agent_team/atoms/design/competitor-reconstruction-review.js +44 -0
  54. package/src/agent_team/atoms/design/contract-freeze.js +8 -0
  55. package/src/agent_team/atoms/design/generation-contract.js +442 -0
  56. package/src/agent_team/atoms/design/reconstruction-pack.js +246 -0
  57. package/src/agent_team/atoms/design/reference-surface-lock.js +49 -0
  58. package/src/agent_team/atoms/patch-challenge.js +19 -0
  59. package/src/agent_team/atoms/pencil/accept.js +9 -0
  60. package/src/agent_team/atoms/pencil/draft.js +365 -0
  61. package/src/agent_team/atoms/pencil/refine.js +229 -0
  62. package/src/agent_team/atoms/phase-run-dispatch.js +8 -0
  63. package/src/agent_team/atoms/team-run.js +380 -0
  64. package/src/agent_team/atoms/visual/aesthetic-review.js +231 -0
  65. package/src/agent_team/atoms/visual/benchmark-repair-plan.js +291 -0
  66. package/src/agent_team/atoms/visual/benchmark.js +1652 -0
  67. package/src/agent_team/atoms/visual/design-selection.js +23 -0
  68. package/src/agent_team/atoms/visual/direction-synthesis.js +124 -0
  69. package/src/agent_team/atoms/visual/dom-rect-export.js +129 -0
  70. package/src/agent_team/atoms/visual/image-diff.js +186 -0
  71. package/src/agent_team/atoms/visual/layout-competition.js +8 -0
  72. package/src/agent_team/atoms/visual/page-evidence-capture.js +806 -0
  73. package/src/agent_team/atoms/visual/playwright-runtime.js +8 -0
  74. package/src/agent_team/atoms/visual/review-aggregate.js +87 -0
  75. package/src/agent_team/atoms/visual/review.js +77 -0
  76. package/src/agent_team/atoms/visual/visual-report.js +197 -0
  77. package/src/agent_team/atoms/visual/visual-token-export.js +417 -0
  78. package/src/agent_team/common.js +311 -0
  79. package/src/core/atoms/agent-invoke.js +684 -0
  80. package/src/core/atoms/event/ingest.js +30 -0
  81. package/src/core/atoms/experience/digest.js +55 -0
  82. package/src/core/atoms/gate/artifact-verify.js +83 -0
  83. package/src/core/atoms/gate/competitor-contract-validate.js +119 -0
  84. package/src/core/atoms/gate/evidence-collect.js +64 -0
  85. package/src/core/atoms/gate/payload-from-phase-run.js +78 -0
  86. package/src/core/atoms/gate/visual-benchmark-validate.js +183 -0
  87. package/src/core/atoms/gate/workbench.js +79 -0
  88. package/src/core/atoms/intake/classify.js +15 -0
  89. package/src/core/atoms/intake/design-skill-select.js +16 -0
  90. package/src/core/atoms/intake/preview-or-refine.js +19 -0
  91. package/src/core/atoms/intake/recommend.js +20 -0
  92. package/src/core/atoms/intake/research-same-category.js +19 -0
  93. package/src/core/atoms/intervention/create.js +30 -0
  94. package/src/core/atoms/memory/manage.js +27 -0
  95. package/src/core/atoms/memory/mount.js +13 -0
  96. package/src/core/atoms/mission/create.js +19 -0
  97. package/src/core/atoms/mission/list.js +14 -0
  98. package/src/core/atoms/mission/show.js +26 -0
  99. package/src/core/atoms/notify/checkpoint.js +46 -0
  100. package/src/core/atoms/phase-run/complete.js +42 -0
  101. package/src/core/atoms/phase-run/fail.js +40 -0
  102. package/src/core/atoms/phase-run/persist.js +39 -0
  103. package/src/core/atoms/phase-run/start.js +30 -0
  104. package/src/core/atoms/plugin/run.js +76 -0
  105. package/src/core/atoms/policy/gate.js +90 -0
  106. package/src/core/atoms/policy/resolve.js +11 -0
  107. package/src/core/atoms/storage/sync.js +57 -0
  108. package/src/core/atoms/workflow/advance.js +25 -0
  109. package/src/core/atoms/workflow/guard.js +127 -0
  110. package/src/core/atoms/workflow/state-load.js +37 -0
  111. package/src/core/bindings.js +37 -0
  112. package/src/core/change-artifacts.js +134 -0
  113. package/src/core/competitive-score.js +265 -0
  114. package/src/core/concurrency-policy.js +35 -0
  115. package/src/core/execution-log.js +108 -0
  116. package/src/core/intake-recommendation.js +134 -0
  117. package/src/core/memory-state.js +62 -0
  118. package/src/core/mission-state.js +780 -0
  119. package/src/core/pending-gates.js +66 -0
  120. package/src/core/phase-cost.js +116 -0
  121. package/src/core/phase-run-read.js +27 -0
  122. package/src/core/policy-overlay.js +180 -0
  123. package/src/core/state-store.js +201 -0
  124. package/src/core/workflow-executor.js +1014 -0
  125. package/src/core/workflow-integrity.js +111 -0
  126. package/src/core/workflow-loader.js +163 -0
  127. package/src/core/workflow-rerun.js +84 -0
  128. package/src/core/workflow-state.js +119 -0
  129. package/src/server.js +1307 -0
  130. package/workflows/corps.yaml +481 -0
  131. package/workflows/yolo.yaml +241 -0
  132. package/xflow/README.md +76 -0
  133. package/xflow/aha/SKILL.md +82 -0
  134. package/xflow/aha/scripts/append_aha.py +50 -0
  135. package/xflow/archive/scripts/archive_change.py +338 -0
  136. package/xflow/archive/templates/merge-design.md +54 -0
  137. package/xflow/archive/templates/merge-workflow.md +26 -0
  138. package/xflow/atoms/__init__.py +6 -0
  139. package/xflow/atoms/a1_issue_create_or_patch.py +70 -0
  140. package/xflow/atoms/a2_branch_create_and_link.py +42 -0
  141. package/xflow/atoms/a3_repo_doctor.py +11 -0
  142. package/xflow/atoms/a4_project_set_status.py +11 -0
  143. package/xflow/atoms/a5_archive_commit_push_close.py +11 -0
  144. package/xflow/atoms/a6_pr_create.py +72 -0
  145. package/xflow/atoms/b1_change_scaffold.py +11 -0
  146. package/xflow/atoms/b2_status_read.py +16 -0
  147. package/xflow/atoms/b2b_status_write.py +20 -0
  148. package/xflow/atoms/b3_status_transition.py +27 -0
  149. package/xflow/atoms/b4_handoff_scaffold_or_refresh.py +11 -0
  150. package/xflow/atoms/b5_aha_append.py +11 -0
  151. package/xflow/atoms/b6_takein_digest.py +11 -0
  152. package/xflow/atoms/b6b_lookback_digest.py +11 -0
  153. package/xflow/atoms/b7_change_mission_bind.py +43 -0
  154. package/xflow/atoms/c1_mem.py +54 -0
  155. package/xflow/atoms/e6_gate_local_precheck.py +23 -0
  156. package/xflow/atoms/h1_design_lite_gate.py +74 -0
  157. package/xflow/atoms/i6a_tdd_run.py +107 -0
  158. package/xflow/atoms/i6b_tdd_proof_validate.py +184 -0
  159. package/xflow/atoms/i6c_tdd_quality_review.py +372 -0
  160. package/xflow/atoms/j1_tests_run.py +129 -0
  161. package/xflow/atoms/j4a_spec_consistency_structural.py +105 -0
  162. package/xflow/atoms/j4b_spec_delta_review.py +316 -0
  163. package/xflow/atoms/j4c_openspec_migration_map.py +121 -0
  164. package/xflow/atoms/j4d_spec_clarify_gate.py +373 -0
  165. package/xflow/atoms/j6_cr_review.py +443 -0
  166. package/xflow/atoms/k1_artifacts_complete_check.py +39 -0
  167. package/xflow/atoms/k2_merge_snippets_apply.py +11 -0
  168. package/xflow/atoms/k3_handoff_refresh.py +14 -0
  169. package/xflow/atoms/k4_mem_lesson_persist.py +65 -0
  170. package/xflow/atoms/k5_archive_publish.py +21 -0
  171. package/xflow/atoms/k6_aha_merge.py +42 -0
  172. package/xflow/atoms/tdd_lib.py +956 -0
  173. package/xflow/corps/SKILL.md +109 -0
  174. package/xflow/exec/scripts/set_issue_in_progress.py +159 -0
  175. package/xflow/goal/SKILL.md +150 -0
  176. package/xflow/handoff/SKILL.md +95 -0
  177. package/xflow/handoff/scripts/scaffold_handoff.py +304 -0
  178. package/xflow/lookback/SKILL.md +66 -0
  179. package/xflow/lookback/scripts/lookback_digest.py +46 -0
  180. package/xflow/openissue/scripts/open_issue_flow.py +438 -0
  181. package/xflow/plan/SKILL.md +47 -0
  182. package/xflow/ralph/SKILL.md +123 -0
  183. package/xflow/scripts/check_installed_xflow_skill_sync.sh +44 -0
  184. package/xflow/scripts/common.py +483 -0
  185. package/xflow/scripts/doctor.py +278 -0
  186. package/xflow/scripts/sync_installed_xflow_skill.sh +134 -0
  187. package/xflow/takein/SKILL.md +81 -0
  188. package/xflow/takein/scripts/takein_digest.py +88 -0
  189. package/xflow/yolo/SKILL.md +75 -0
  190. package/xflow/yolo/scripts/scaffold_specs_change.py +673 -0
  191. package/xflow/yolo/scripts/yolo_gate.py +312 -0
  192. package/xflow/yolo/templates/DESIGN.md +179 -0
@@ -0,0 +1,311 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ import {
5
+ makeArtifactEnvelope,
6
+ readJsonFile,
7
+ resolveChangePath,
8
+ summarizeFinding,
9
+ writeJsonFile,
10
+ writeTextFile,
11
+ } from '../core/change-artifacts.js';
12
+ import { resolveBoundMissionId } from '../core/bindings.js';
13
+ import { findPhaseRunSession, persistPhaseRun } from '../core/mission-state.js';
14
+ import { mutateStore, readStore, resolveDataFile } from '../core/state-store.js';
15
+ import { getLatestPhaseRun } from '../core/phase-run-read.js';
16
+
17
+ function nowIso() {
18
+ return new Date().toISOString();
19
+ }
20
+
21
+ function ensureArray(value) {
22
+ return Array.isArray(value) ? value : value ? [value] : [];
23
+ }
24
+
25
+ export function defaultVerdictForPhase(phase, mode = null) {
26
+ if (mode === 'qa' || phase === 'qa') {
27
+ return {
28
+ status: 'accept',
29
+ confidence: 0.88,
30
+ rationale: 'QA checks and recorded evidence meet the current acceptance bar.',
31
+ };
32
+ }
33
+ if (phase === 'review') {
34
+ return {
35
+ status: 'accept',
36
+ confidence: 0.82,
37
+ rationale: 'Review did not surface a blocking regression in the current evidence set.',
38
+ };
39
+ }
40
+ return {
41
+ status: 'accept',
42
+ confidence: 0.76,
43
+ rationale: `${phase || 'phase'} completed with a coherent synthetic result packet.`,
44
+ };
45
+ }
46
+
47
+ export function defaultFindingsForPhase(phase) {
48
+ const title = String(phase || 'phase').replace(/_/g, ' ');
49
+ return [
50
+ { summary: `${title} output is internally consistent.` },
51
+ { summary: `${title} output is ready for the next workflow transition.` },
52
+ ];
53
+ }
54
+
55
+ export function defaultQaAcceptancePacket() {
56
+ return {
57
+ status: 'needs_human',
58
+ primary_journey_covered: false,
59
+ manifest_valid: false,
60
+ cross_module_continuity: { status: 'covered' },
61
+ module_coverage: {
62
+ covered_module_count: 0,
63
+ expected_module_count: 1,
64
+ },
65
+ failed_checks: [
66
+ 'visual_qa_packet_missing',
67
+ 'competitor_backed_acceptance_missing',
68
+ ],
69
+ };
70
+ }
71
+
72
+ export async function resolveMissionContext(input = {}, context = {}) {
73
+ const projectRoot = context.projectRoot || process.cwd();
74
+ const changeId = context.changeId || input.change_id || null;
75
+ const missionId = resolveBoundMissionId({
76
+ projectRoot,
77
+ changeId,
78
+ missionId: input.mission_id || null,
79
+ runtimeMissionId: context.runtime?.missionId || null,
80
+ });
81
+ const dataFile = resolveDataFile({ projectRoot, explicitPath: input.data_file || null });
82
+ const state = missionId ? await readStore(dataFile) : null;
83
+ const phaseRun = missionId ? getLatestPhaseRun(state, missionId) : null;
84
+ const session = missionId ? findPhaseRunSession(state, missionId, context.runtime?.phaseRunSessionId || input.session_id || null) : null;
85
+ return { projectRoot, changeId, missionId, dataFile, state, phaseRun, session };
86
+ }
87
+
88
+ function artifactPathForPhase(projectRoot, changeId, phase, mode = null) {
89
+ switch (phase) {
90
+ case 'explore':
91
+ case 'brainstorm':
92
+ case 'risk_review':
93
+ case 'clarify':
94
+ case 'ux_design_brief':
95
+ return resolveChangePath(projectRoot, changeId, `${phase}.json`);
96
+ case 'review':
97
+ return resolveChangePath(projectRoot, changeId, 'review.json');
98
+ case 'llm_design_review':
99
+ return resolveChangePath(projectRoot, changeId, 'llm_design_review.json');
100
+ case 'design_contract_freeze':
101
+ return resolveChangePath(projectRoot, changeId, 'design_contract.json');
102
+ case 'visual_direction_synthesis':
103
+ return resolveChangePath(projectRoot, changeId, 'visual_direction_synthesis.json');
104
+ case 'layout_competition':
105
+ return resolveChangePath(projectRoot, changeId, 'layout_competition.json');
106
+ case 'design_selection':
107
+ return resolveChangePath(projectRoot, changeId, 'design_selection.json');
108
+ case 'design_accept':
109
+ return resolveChangePath(projectRoot, changeId, 'design_accept.json');
110
+ case 'qa':
111
+ return resolveChangePath(projectRoot, changeId, 'llm_design_review.json');
112
+ default:
113
+ if (mode === 'qa') {
114
+ return resolveChangePath(projectRoot, changeId, 'qa_visual_review.json');
115
+ }
116
+ return resolveChangePath(projectRoot, changeId, `${phase || 'artifact'}.json`);
117
+ }
118
+ }
119
+
120
+ function penPath(projectRoot, changeId) {
121
+ return resolveChangePath(projectRoot, changeId, 'pencil_output.pen');
122
+ }
123
+
124
+ function attestationPath(projectRoot, changeId) {
125
+ return resolveChangePath(projectRoot, changeId, 'pencil_output.attestation.json');
126
+ }
127
+
128
+ export async function persistSyntheticPhaseRun({
129
+ projectRoot,
130
+ changeId,
131
+ missionId,
132
+ dataFile,
133
+ phase,
134
+ sessionId = null,
135
+ summary = '',
136
+ findings = [],
137
+ verdict = null,
138
+ extraTeamRun = {},
139
+ extraPhaseEntry = {},
140
+ executedPhases = null,
141
+ }) {
142
+ if (!missionId) {
143
+ return { phaseRun: null, phaseEntry: null };
144
+ }
145
+
146
+ const result = await mutateStore((state) => {
147
+ const existingPhaseRun = getLatestPhaseRun(state, missionId);
148
+ const existingEntries = Array.isArray(existingPhaseRun?.phases) ? [...existingPhaseRun.phases] : [];
149
+ const existingIndex = existingEntries.findIndex((entry) => entry.phase === phase);
150
+ const nextPhaseEntry = {
151
+ phase,
152
+ status: extraPhaseEntry.status || 'completed',
153
+ effective_status: extraPhaseEntry.effective_status || extraPhaseEntry.status || 'completed',
154
+ summary: summary || existingPhaseRun?.summary || `${phase} complete`,
155
+ updated_at: nowIso(),
156
+ team_run: {
157
+ team_run_id: extraTeamRun.team_run_id || `team_run_${phase}_${Date.now()}`,
158
+ pattern: extraTeamRun.pattern || 'solo',
159
+ verdict: verdict || defaultVerdictForPhase(phase),
160
+ findings: findings.length > 0 ? findings : defaultFindingsForPhase(phase),
161
+ evidence_bundle: extraTeamRun.evidence_bundle || {
162
+ evidence_count: findings.length > 0 ? findings.length : 2,
163
+ },
164
+ workspace_evidence: extraTeamRun.workspace_evidence || {
165
+ summary: `${phase} evidence captured from the local workspace state.`,
166
+ },
167
+ qa_acceptance_packet: extraTeamRun.qa_acceptance_packet || (phase === 'qa' ? defaultQaAcceptancePacket() : null),
168
+ ...extraTeamRun,
169
+ },
170
+ ...extraPhaseEntry,
171
+ };
172
+
173
+ if (existingIndex >= 0) {
174
+ existingEntries[existingIndex] = {
175
+ ...existingEntries[existingIndex],
176
+ ...nextPhaseEntry,
177
+ team_run: {
178
+ ...(existingEntries[existingIndex].team_run || {}),
179
+ ...(nextPhaseEntry.team_run || {}),
180
+ },
181
+ };
182
+ } else {
183
+ existingEntries.push(nextPhaseEntry);
184
+ }
185
+
186
+ const previousExecuted = Array.isArray(existingPhaseRun?.executed_phases) ? existingPhaseRun.executed_phases : [];
187
+ const nextExecuted = executedPhases || [...new Set([...previousExecuted, phase])];
188
+ const phaseRun = persistPhaseRun(state, missionId, {
189
+ session_id: sessionId,
190
+ phase_run_id: existingPhaseRun?.phase_run_id || undefined,
191
+ summary: summary || existingPhaseRun?.summary || `${phase} complete`,
192
+ status: 'completed',
193
+ phases: existingEntries,
194
+ executed_phases: nextExecuted,
195
+ phase_count: existingEntries.length,
196
+ phase_run_file: existingPhaseRun?.phase_run_file || null,
197
+ workspace_evidence: extraTeamRun.workspace_evidence || existingPhaseRun?.workspace_evidence || null,
198
+ qa_acceptance_packet: extraTeamRun.qa_acceptance_packet || null,
199
+ });
200
+ const phaseEntry = phaseRun.phases.find((entry) => entry.phase === phase) || null;
201
+ return { phaseRun, phaseEntry };
202
+ }, dataFile);
203
+
204
+ return result;
205
+ }
206
+
207
+ export async function synthesizePhaseArtifact(input = {}, context = {}, options = {}) {
208
+ const phase = options.phase || input.phase || context.phase?.id || null;
209
+ const mode = input.mode || options.mode || null;
210
+ const missionContext = await resolveMissionContext(input, context);
211
+ const { projectRoot, changeId, missionId, dataFile, session } = missionContext;
212
+ if (!changeId) {
213
+ throw new Error('change_id is required');
214
+ }
215
+
216
+ const verdict = options.verdict || defaultVerdictForPhase(phase, mode);
217
+ const findings = options.findings || defaultFindingsForPhase(phase);
218
+ const summary = options.summary || `${phase || 'phase'} completed with synthetic heavy-track output.`;
219
+ const persistResult = await persistSyntheticPhaseRun({
220
+ projectRoot,
221
+ changeId,
222
+ missionId,
223
+ dataFile,
224
+ phase,
225
+ sessionId: session?.session_id || context.runtime?.phaseRunSessionId || null,
226
+ summary,
227
+ findings,
228
+ verdict,
229
+ extraTeamRun: options.extraTeamRun || {},
230
+ extraPhaseEntry: options.extraPhaseEntry || {},
231
+ executedPhases: options.executedPhases || null,
232
+ });
233
+
234
+ const envelope = makeArtifactEnvelope({
235
+ phase,
236
+ changeId,
237
+ missionId,
238
+ phaseRun: persistResult.phaseRun,
239
+ phaseEntry: persistResult.phaseEntry,
240
+ extra: options.extraEnvelope || {},
241
+ });
242
+
243
+ const artifactPath = options.outputPath || artifactPathForPhase(projectRoot, changeId, phase, mode);
244
+ writeJsonFile(artifactPath, envelope);
245
+
246
+ if (options.writeMarkdown) {
247
+ const mdPath = resolveChangePath(projectRoot, changeId, `${phase}.md`);
248
+ writeTextFile(mdPath, `# ${phase}\n\n${summary}\n`);
249
+ }
250
+
251
+ if (options.writePen) {
252
+ writeTextFile(penPath(projectRoot, changeId), options.penContent || `PENCIL ${phase}\n${summary}\n`);
253
+ }
254
+
255
+ if (options.writeAttestation) {
256
+ writeJsonFile(attestationPath(projectRoot, changeId), {
257
+ phase,
258
+ change_id: changeId,
259
+ mission_id: missionId,
260
+ accepted: true,
261
+ recorded_at: nowIso(),
262
+ });
263
+ }
264
+
265
+ return {
266
+ ok: true,
267
+ mission_id: missionId,
268
+ phase_run_id: persistResult.phaseRun?.phase_run_id || null,
269
+ artifact_file: artifactPath,
270
+ payload: envelope,
271
+ };
272
+ }
273
+
274
+ export function buildPlanCommands(projectRoot) {
275
+ const packageJsonPath = resolve(projectRoot, 'package.json');
276
+ const packageJson = readJsonFile(packageJsonPath, null);
277
+ if (packageJson?.scripts?.test) {
278
+ return ['npm test'];
279
+ }
280
+ return ['echo "no test script configured"'];
281
+ }
282
+
283
+ export function writePlanMarkdown(projectRoot, changeId, phase = 'plan') {
284
+ const commands = buildPlanCommands(projectRoot);
285
+ const mdPath = resolveChangePath(projectRoot, changeId, `${phase}.md`);
286
+ const lines = [
287
+ '# Plan',
288
+ '',
289
+ '## Verification',
290
+ '',
291
+ '```bash',
292
+ ...commands,
293
+ '```',
294
+ '',
295
+ ];
296
+ writeTextFile(mdPath, lines.join('\n'));
297
+ return mdPath;
298
+ }
299
+
300
+ export function writeReviewArtifact(projectRoot, changeId, summary, findings = []) {
301
+ const outputPath = resolveChangePath(projectRoot, changeId, 'review.json');
302
+ writeJsonFile(outputPath, {
303
+ version: 1,
304
+ summary,
305
+ findings: findings.map((finding) => ({
306
+ summary: summarizeFinding(finding),
307
+ })),
308
+ recorded_at: nowIso(),
309
+ });
310
+ return outputPath;
311
+ }