import fs from "node:fs"; import path from "node:path"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const abs = (file) => path.join(root, file); const read = (file) => fs.readFileSync(abs(file), "utf8"); const maxArg = process.argv.indexOf("--max-iterations"); const maxIterations = maxArg >= 0 ? Number(process.argv[maxArg + 1]) : 0; if (!Number.isSafeInteger(maxIterations) || maxIterations < 0) throw new Error("--max-iterations must be a non-negative integer; 0 means unlimited"); const commitEnabled = process.argv.includes("--commit"); const pushEnabled = process.argv.includes("--push"); if (pushEnabled && !commitEnabled) throw new Error("--push requires --commit"); const pushRetryArg = process.argv.indexOf("--push-retries"); const pushRetries = pushRetryArg >= 0 ? Number(process.argv[pushRetryArg + 1]) : 6; if (!Number.isSafeInteger(pushRetries) || pushRetries < 0) throw new Error("--push-retries must be a non-negative integer"); const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`; function waitMs(milliseconds) { if (milliseconds <= 0) return; const wait = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(wait, 0, 0, milliseconds); } function command(command, logPath) { const started = Date.now(); const result = spawnSync("bash", ["-lc", command], { cwd: root, encoding: "utf8", maxBuffer: 16 * 1024 * 1024 }); const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; if (logPath) fs.writeFileSync(abs(logPath), output); return { code: result.status ?? 1, durationMs: Date.now() - started, output }; } function currentTask() { const queue = read("docs/EXECUTION_QUEUE.md"); const task = queue.match(/\| 当前任务 \| `(WBV2-P\d+-\d+)` \|/u)?.[1]; const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1]; if (!task || !parentManifest) throw new Error("queue pointer is incomplete"); return { task, parentManifest }; } function terminalTask(task) { try { const index = JSON.parse(read("tests/golden/WBV2/task-index.json")); const manifestPath = `tests/golden/${task}/manifest.json`; const manifest = fs.existsSync(abs(manifestPath)) ? JSON.parse(read(manifestPath)) : null; return index.activeTask === null && manifest?.task === task && manifest.status === "done" && manifest.nextTask === null; } catch { return false; } } function taskCommand(task) { const card = read(`docs/tasks/${task}.md`); const match = card.match(/- Implementation entrypoint: `([^`]+)`/u); if (!match) throw new Error(`task card has no implementation entrypoint: ${task}`); return match[1]; } let iterations = 0; const summary = []; while (maxIterations === 0 || iterations < maxIterations) { const before = currentTask(); // A completed V2 plan intentionally leaves the final task as an auditable // pointer. Do not replay its implementation command on later unattended // passes; close the runner cleanly at the terminal boundary. if (terminalTask(before.task)) { summary.push({ task: before.task, command: null, nextTask: null, terminal: true, log: null }); process.stdout.write(`continuous-terminal task=${before.task}\n`); break; } const focused = taskCommand(before.task); const logPath = `tests/golden/${before.task}/continuous-runner.log`; const focusedResult = command(focused, logPath); if (focusedResult.code !== 0) throw new Error(`${before.task} focused command failed (exit ${focusedResult.code}); log=${logPath}`); const after = currentTask(); const terminal = after.task === before.task && terminalTask(before.task); if (after.task === before.task && !terminal) throw new Error(`${before.task} focused command exited 0 without advancing queue; log=${logPath}`); // Implementation cycles keep the post-handoff gate incremental. Full API, // format, Chromium, performance and release gates belong to the explicit // final audit and must not be repeated for every batch. const gates = command(`python3 my-tools/validate_incremental_context.py --task ${before.task}`, `tests/golden/${before.task}/continuous-gates.log`); if (gates.code !== 0) throw new Error(`${before.task} post-handoff gate failed (exit ${gates.code}); log=tests/golden/${before.task}/continuous-gates.log`); iterations += 1; summary.push({ task: before.task, command: focused, nextTask: terminal ? null : after.task, terminal, log: logPath }); process.stdout.write(`continuous-iteration=${iterations} task=${before.task} next=${after.task}\n`); if (commitEnabled) { const manifestPath = `tests/golden/${before.task}/manifest.json`; const artifactPaths = fs.existsSync(abs(manifestPath)) ? Object.values(JSON.parse(read(manifestPath).toString()).artifacts ?? {}).map((artifact) => artifact?.path).filter((file) => typeof file === "string") : []; const handoffPaths = [ "docs/EXECUTION_QUEUE.md", `docs/tasks/${before.task}.md`, ...(terminal ? [] : [`docs/tasks/${after.task}.md`]), `docs/status/${before.task}.md`, ...(terminal ? [] : [`docs/status/${after.task}.md`]), manifestPath, ...artifactPaths, "tests/golden/WBV2/task-index.json", "tests/golden/WBV2/task-catalog.jsonl", ].filter((file, index, all) => file && all.indexOf(file) === index && fs.existsSync(abs(file))); // Every path named by the manifest is part of the auditable handoff. Some // task logs are intentionally ignored by the repository defaults, so use // force-add for this exact, manifest-derived list instead of failing after // the task has already produced a valid receipt. const commit = command(`git add -f ${handoffPaths.map(shellQuote).join(" ")}`, `tests/golden/${before.task}/continuous-commit.log`); if (commit.code !== 0) throw new Error(`${before.task} staging failed; log=tests/golden/${before.task}/continuous-commit.log`); const commitResult = command(`git diff --cached --quiet || git commit -m "chore(web): continuous handoff ${before.task}"`, `tests/golden/${before.task}/continuous-commit-result.log`); if (commitResult.code !== 0) throw new Error(`${before.task} commit failed; log=tests/golden/${before.task}/continuous-commit-result.log`); if (pushEnabled) { let pushed = false; for (let attempt = 0; attempt <= pushRetries; attempt += 1) { const pushLog = attempt === 0 ? `tests/golden/${before.task}/continuous-push.log` : `tests/golden/${before.task}/continuous-push-attempt-${attempt + 1}.log`; const push = command("git push origin main", pushLog); const pushVerification = command( "local_head=$(git rev-parse --verify HEAD) && remote_head=$(git ls-remote --refs origin refs/heads/main | awk '$2 == \"refs/heads/main\" { print $1; exit }') && printf 'local_head=%s remote_head=%s\\n' \"$local_head\" \"$remote_head\" && test -n \"$remote_head\" && test \"$local_head\" = \"$remote_head\"", `tests/golden/${before.task}/continuous-push-verify.log`, ); if (pushVerification.code === 0) { pushed = true; break; } if (attempt < pushRetries) waitMs(Math.min(30000, 1000 * 2 ** attempt)); } if (!pushed) throw new Error(`${before.task} remote push verification failed after ${pushRetries + 1} attempts; log=tests/golden/${before.task}/continuous-push-verify.log`); } } if (terminal) break; } process.stdout.write(`continuous-run-complete iterations=${iterations} current=${currentTask().task} committed=${commitEnabled} pushed=${pushEnabled}\n`);