#!/usr/bin/env node /** * scripts/phase0-verify.js * * DevLoop P0 末端验收检查器(不发起任何子进程,只做静态校验)。 * * Checkpoints: * [1] 35 个 devloop 工具全部 loadable * [2] 6 个 devloop-* agent 在 system/templates/_registry.json 注册 * [3] 每个 devloop-* 目录有 prompt.md + config.json (含 cli.toolPolicy) * [4] bash-json-guard 可挂在 Bash preExecute (顶层 index.js 暴露) * [5] services/devloop 7 大件 + tool layer 7 大件 全部 require 成功 * [6] mock fixtures 默认+pm 两个文件存在 * [7] git-vault + path-resolver 接口存在 * * 通过即 exit 0,任一失败 exit 1。 * * 规格: docs/prd/newfunction/_spec/plan-migration.md · Quick-Start step 7 */ const fs = require('fs'); const path = require('path'); const ROOT = path.resolve(__dirname, '..'); const checks = []; function check(name, fn) { try { const r = fn(); if (r && r.ok === false) checks.push({ name, ok: false, reason: r.reason }); else checks.push({ name, ok: true, detail: r?.detail || '' }); } catch (e) { checks.push({ name, ok: false, reason: e.message }); } } // ── [1] tools loadable ──────────────────────────────────────── check('35 devloop tools loadable', () => { const reg = require('../server/src/tools/devloop/tool-registry'); reg._resetForTesting(); const all = reg.loadAllTools(); const names = reg.listToolNames(); const expected = [ // persistence (5) 'write_todolist', 'update_todo_item', 'update_runtime_state', 'timeline_append', 'record_memory', // doc (12) 'bootstrap_prd_site', 'bootstrap_sdd_site', 'write_chapter', 'write_mvcp_page', 'archive_doc', 'diff_html', 'diff_units', 'echo_diff', 'update_search_index', 'validate_prd_site', 'validate_sdd_consistency', 'hash_contract', // coder (3) 'contract_bridge', 'sandbox_check', 'append_coder_log', // tester (5) 'parse_vitest', 'hint_builder', 'bug_open', 'bug_transition', 'bug_list', // integrator (6) 'port_picker', 'spawn_units', 'health_wait', 'run_case', 'parse_placeholder', 'collect_stderr', // triager (4) 'extract_evidence', 'category_classifier', 'diff_contract_vs_impl', 'find_similar_bug', ]; const missing = expected.filter(n => !names.includes(n)); if (missing.length) { return { ok: false, reason: `missing tools: ${missing.join(', ')}` }; } return { detail: `${all.size} tools registered (expected ≥35)` }; }); // ── [2] _registry.json has 6 devloop-* entries ───────────────── check('_registry.json has 6 devloop-* agents', () => { const regPath = path.join(ROOT, 'system/templates/_registry.json'); const list = JSON.parse(fs.readFileSync(regPath, 'utf8')); const expected = ['devloop-pm', 'devloop-architect', 'devloop-coder', 'devloop-tester', 'devloop-integrator', 'devloop-triager']; const ids = list.map(x => x.id); const missing = expected.filter(e => !ids.includes(e)); if (missing.length) return { ok: false, reason: `missing: ${missing.join(', ')}` }; return { detail: `6/6 registered` }; }); // ── [3] per-agent dir has prompt.md + config.json ────────────── check('per-agent prompt.md + config.json (6 agents)', () => { const agents = ['devloop-pm', 'devloop-architect', 'devloop-coder', 'devloop-tester', 'devloop-integrator', 'devloop-triager']; const issues = []; for (const a of agents) { const dir = path.join(ROOT, 'system/templates', a); const promptOk = fs.existsSync(path.join(dir, 'prompt.md')); const cfgPath = path.join(dir, 'config.json'); const cfgOk = fs.existsSync(cfgPath); if (!promptOk) issues.push(`${a}: prompt.md missing`); if (!cfgOk) issues.push(`${a}: config.json missing`); if (cfgOk) { try { const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8')); const tp = cfg.cli?.toolPolicy?.allowedTools; if (!Array.isArray(tp) || tp.length === 0) { issues.push(`${a}: cli.toolPolicy.allowedTools missing`); } if (!cfg.sandbox?.writeAllowList) { issues.push(`${a}: sandbox.writeAllowList missing`); } } catch (e) { issues.push(`${a}: config.json invalid (${e.message})`); } } } if (issues.length) return { ok: false, reason: issues.join('; ') }; return { detail: '6 agents fully wired' }; }); // ── [4] bash-json-guard exported via index ───────────────────── check('bash-json-guard wired (devloop/index.js)', () => { const idx = require('../server/src/tools/devloop'); if (!idx.bashHooks || !Array.isArray(idx.bashHooks.preExecute)) { return { ok: false, reason: 'bashHooks.preExecute not exported' }; } // 触发一次 inline JSON 应抛 const hook = idx.bashHooks.preExecute[0]; let threw = false; try { hook(`curl -d '{"k":"v"}' http://x`); } catch { threw = true; } if (!threw) return { ok: false, reason: 'guard did not block inline JSON' }; return { detail: 'jsonGuard blocks inline JSON ✓' }; }); // ── [5] services/devloop modules require successfully ───────── check('services/devloop 7 modules require', () => { const mods = [ '../server/services/devloop/path-resolver', '../server/services/devloop/doc-store', '../server/services/devloop/unit-registry', '../server/services/devloop/bug-tracker', '../server/services/devloop/timeline-logger', '../server/services/devloop/changelog-writer', '../server/services/devloop/sse-broker', '../server/services/devloop/git-vault', ]; for (const m of mods) { try { require(m); } catch (e) { return { ok: false, reason: `${m}: ${e.message}` }; } } return { detail: `${mods.length}/${mods.length} services OK` }; }); // ── [6] mock fixtures present ───────────────────────────────── check('mock fixtures present', () => { const dir = path.join(ROOT, 'fixtures/devloop-mock'); for (const f of ['default.json', 'devloop-pm.json']) { if (!fs.existsSync(path.join(dir, f))) { return { ok: false, reason: `missing fixtures/devloop-mock/${f}` }; } } return { detail: 'default.json + devloop-pm.json' }; }); // ── [7] git-vault interface present ──────────────────────────── check('git-vault interface', () => { const gv = require('../server/services/devloop/git-vault'); for (const fn of ['init', 'commit', 'status', 'log']) { if (typeof gv[fn] !== 'function') { return { ok: false, reason: `git-vault.${fn} missing` }; } } return { detail: 'init/commit/status/log present' }; }); // ── [8] loop-controller present + git hook callable ─────────── check('loop-controller exposes createProject + commitAfterAgent', () => { const lc = require('../server/src/core/devloop/loop-controller'); for (const fn of ['createProject', 'commitAfterAgent']) { if (typeof lc[fn] !== 'function') { return { ok: false, reason: `loop-controller.${fn} missing` }; } } return { detail: 'createProject + commitAfterAgent present' }; }); // ── 输出 ────────────────────────────────────────────────────── let pass = 0, fail = 0; console.log('\n┌─ DevLoop Phase 0 — Static Verification ─────────────'); for (const c of checks) { const tag = c.ok ? '✓' : '✗'; const line = `│ ${tag} ${c.name.padEnd(48)} ${c.ok ? c.detail : '— ' + c.reason}`; console.log(line); c.ok ? pass++ : fail++; } console.log(`└─ ${pass}/${pass + fail} checks passed${fail ? ' · ' + fail + ' failed' : ''}\n`); process.exit(fail ? 1 : 0);