From 36252fdcd8be85d154e34429bbd11605926643ea Mon Sep 17 00:00:00 2001 From: zengfr Date: Sun, 23 Aug 2026 11:51:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(uv):=20=E6=9E=B6=E6=9E=84=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=20=E2=80=94=20=E4=BA=8B=E4=BB=B6=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D/=E5=85=A5=E5=8F=A3=E5=B1=82=E8=A1=A5?= =?UTF-8?q?=E5=85=A8/SKILL=E5=8E=BB=E9=87=8D/review=E9=97=A8=E7=A6=81/?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E5=99=A8=E6=A0=A1=E9=AA=8C/=E7=94=9F?= =?UTF-8?q?=E6=88=90=E5=99=A8=E6=A1=A5=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P0 修复: - A-4: 事件名归一化(normalizeEventName) + recordCommandEvent 补全9个命令 - P0-1: 补全入口层 .codex/instructions.md(init-task引导+权限声明) - P0-3: skillgen 单真值源重构(DRIVE_LOOP/FLOW_DEF_TAIL常量,消除4份冗余) - P0-4: review流程 RV-1/RV-2 补 expected_events(['validate-artifact']) P1: - P1-2: preflight-check 加执行器注册校验(flows executor vs 注册表) P2: - P0-2: TS 原生生成器桥接 src/commands/generate.ts — run paused 后机械层 自动 spawn codex exec 子代理并 apply-generation,无需主会话手工介入 (unattended 模式自动接入,config.generator.engine=codex 启用) 验证: typecheck/vitest 167/167/generate-skill --check/eslint 全通过 --- .codex/instructions.md | 14 ++++ flows/review.json | 8 +- src/commands/audit-deps.ts | 3 + src/commands/check-static.ts | 8 +- src/commands/generate.ts | 121 ++++++++++++++++++++++++++++++ src/commands/guard-report.ts | 2 + src/commands/iteration-check.ts | 2 + src/commands/preflight-check.ts | 21 ++++++ src/commands/registry.ts | 3 + src/commands/run-tests.ts | 10 +-- src/commands/semantic-diff.ts | 2 + src/commands/snapshot.ts | 2 + src/commands/unattended.ts | 12 +++ src/commands/validate-artifact.ts | 2 + src/commands/validate-citation.ts | 5 +- src/commands/validate-spec.ts | 4 +- src/lib/config.ts | 16 ++++ src/lib/skillgen.ts | 45 ++++++----- src/lib/trace.ts | 23 +++++- uv.config.json | 6 ++ 20 files changed, 277 insertions(+), 32 deletions(-) create mode 100644 src/commands/generate.ts diff --git a/.codex/instructions.md b/.codex/instructions.md index 6e17ddf..57ce1df 100644 --- a/.codex/instructions.md +++ b/.codex/instructions.md @@ -46,3 +46,17 @@ 7. 迭代耗尽 → 终止报告失败,禁止无限重试(迭代检查由 phase-transition 内部机械截断) 8. 阶段间调 `context-summary` 压缩上下文, phase-enter 含上下文预算硬上限 9. 未匹配命令 → 提示正确用法 + + +## 新任务标准序列(无人值守入口) + +1. 新任务统一从 `uv init-task --flow --description "<需求>"` 开始(task.json + checkpoint) +2. 目标 skill 接管,阶段推进一律走 `uv run` 驱动循环 +3. 需要 LLM 生成时暂停 → spawn 子代理 → apply-generation → 重新 `uv run` + +## 权限声明 + +[permissions] + writable: src/ tests/ .reports/ + readonly: CONSTITUTION.md .harness.md *.json protected_files + forbidden: git push / git reset --hard / git clean -fd diff --git a/flows/review.json b/flows/review.json index f9d6d0d..fc178f6 100644 --- a/flows/review.json +++ b/flows/review.json @@ -14,7 +14,9 @@ ], "consumes": [], "on_failure": "retry", - "expected_events": [], + "expected_events": [ + "validate-artifact" + ], "max_timeout_sec": 1800, "instructions": "架构审查阶段:梳理模块分层,检查接口设计\n1. 分析项目架构和模块依赖\n2. 检查接口设计合理性\n3. 输出 .reports/arch-review.json", "steps": [ @@ -39,7 +41,9 @@ ], "consumes": [], "on_failure": "retry", - "expected_events": [], + "expected_events": [ + "validate-artifact" + ], "max_timeout_sec": 1800, "instructions": "缺陷扫描阶段:查找逻辑缺陷和边界条件遗漏\n1. 分析代码逻辑缺陷\n2. 检查边界条件处理\n3. 输出 .reports/defect-scan.json", "steps": [ diff --git a/src/commands/audit-deps.ts b/src/commands/audit-deps.ts index 1ceb5f9..50c9987 100644 --- a/src/commands/audit-deps.ts +++ b/src/commands/audit-deps.ts @@ -8,6 +8,7 @@ import { join } from "node:path"; import { ok, fail } from "../lib/result.ts"; import { parseNpmAudit, auditBlocks } from "../lib/audit.ts"; import type { ToolResult } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; export interface AuditDepsResult { audit: ReturnType; @@ -38,5 +39,7 @@ export function auditDeps(workdir: string): ToolResult { if (blocks) { return fail("audit_blocks", `依赖审计未通过: critical=${audit.critical} high=${audit.high}`, `请修复 security 漏洞后再审查`); } + recordCommandEvent(workdir, "audit-deps", { vulns: audit.critical + audit.high }); return ok({ audit, blocks, output: stdout.slice(0, 2000), path: outPath }); } + diff --git a/src/commands/check-static.ts b/src/commands/check-static.ts index 2b9ecbb..e5c459b 100644 --- a/src/commands/check-static.ts +++ b/src/commands/check-static.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { ok, fail } from "../lib/result.ts"; import { scanForbiddenPatterns } from "../lib/static.ts"; import type { ToolResult } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; function collectFiles(dir: string, base: string): string[] { const files: string[] = []; @@ -29,7 +30,10 @@ export function checkStatic(workdir: string): ToolResult<{ passed: boolean; issu const srcDir = join(workdir, "src"); const testDir = join(workdir, "tests"); const files = [...collectFiles(srcDir, srcDir), ...collectFiles(testDir, testDir)]; - if (files.length === 0) return ok({ passed: true, issues: [] }); + if (files.length === 0) { + recordCommandEvent(workdir, "check-static", { files: 0 }); + return ok({ passed: true, issues: [] }); + } const r = scanForbiddenPatterns(files, (p) => existsSync(p) ? readFileSync(p, "utf-8") : null); if (!r.passed) { const details = r.issues.filter((i) => i.severity === "error").map((i) => `${i.file}:${i.line} ${i.message}`).join("\n"); @@ -40,4 +44,4 @@ export function checkStatic(workdir: string): ToolResult<{ passed: boolean; issu // warn 级不阻断,只报告 } return ok({ passed: true, issues: r.issues }); -} \ No newline at end of file +} diff --git a/src/commands/generate.ts b/src/commands/generate.ts new file mode 100644 index 0000000..d8c7677 --- /dev/null +++ b/src/commands/generate.ts @@ -0,0 +1,121 @@ +// ================================================================ +// uv generate — TS 原生生成器桥接(P0-2 真无人值守) +// 说明:驱动循环 paused 后,由机械层自动 spawn codex 子代理生成补丁, +// 无需主会话手工介入。配置在 uv.config.json 的 generator 段。 +// 流程: generation-request.json → spawn-generation 简报 → codex exec +// → generation-patch.json → apply-generation 写前校验落地 +// ================================================================ +import { spawn } from "node:child_process"; +import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { ok, fail } from "../lib/result.ts"; +import { loadConfig } from "../lib/config.ts"; +import { appendTrace } from "../lib/trace.ts"; +import { spawnGeneration } from "./spawn-generation.ts"; +import { applyGeneration } from "./apply-generation.ts"; +import type { ToolResult, GenerationResult } from "../types.ts"; + +export interface GeneratorBridgeOptions { + workdir: string; + /** 覆盖 generator 引擎(默认取 uv.config.json) */ + engine?: "none" | "codex"; +} + +export interface GeneratorBridgeResult { + applied: boolean; + skipped?: string; + generation?: GenerationResult; + detail?: string; +} + +/** + * 生成器桥接 — 机械层自动 spawn codex 子代理并落地补丁。 + * skipped 场景(无请求/引擎禁用/无 codex 二进制)不视为失败, + * 由调用方(unattended)回退到原暂停协议。 + */ +export async function runGenerator(options: GeneratorBridgeOptions): Promise> { + const { workdir } = options; + + // 1. 必须有待消费的生成请求 + const reqPath = join(workdir, ".reports", "generation-request.json"); + if (!existsSync(reqPath)) { + return ok({ applied: false, skipped: "no_generation_request" }); + } + + // 2. 引擎配置 + const config = loadConfig(workdir); + const gen = config.generator ?? { engine: "none" as const, timeoutSec: 1800, sandbox: "workspace-write" }; + const engine = options.engine ?? gen.engine; + if (engine !== "codex") { + return ok({ applied: false, skipped: "generator_disabled" }); + } + const codexBin = gen.command ?? "codex"; + const sandbox = gen.sandbox ?? "workspace-write"; + const model = gen.model ?? ""; + const timeoutMs = (gen.timeoutSec ?? 1800) * 1000; + + // 3. 编译子代理简报(spawn-generation 写 .reports/subagent-prompt.md) + const brief = spawnGeneration({ workdir }); + if (!brief.ok) return fail("brief_failed", "生成简报失败", brief.error?.detail); + const promptPath = join(workdir, brief.data!.prompt_path); + const promptText = readFileSync(promptPath, "utf-8"); + + // 4. spawn codex exec 非交互生成补丁 + appendTrace(workdir, { event: "generator_spawn", engine: "codex", phase: brief.data!.phase_id }); + const args = ["exec", "-C", workdir, "-s", sandbox, "--json"]; + if (model && model.length > 0) { args.push("-m", model); } + args.push("-"); + + const patchPath = join(workdir, ".reports", "generation-patch.json"); + const result = await new Promise>((resolve) => { + const child = spawn(codexBin, args, { cwd: workdir, stdio: ["pipe", "pipe", "pipe"], shell: false }); + let killed = false; + const timer = setTimeout(() => { + killed = true; + child.kill("SIGKILL"); + }, timeoutMs); + + child.stdin.write(promptText); + child.stdin.end(); + + child.on("error", (err) => { + clearTimeout(timer); + const errMsg = String(err?.message ?? "codex 启动失败"); + if (/ENOENT|not found|无法识别/.test(errMsg)) { + resolve(ok({ applied: false, skipped: "codex_unavailable" })); + } else { + resolve(fail("generator_spawn_failed", errMsg)); + } + }); + + child.on("close", (code) => { + clearTimeout(timer); + if (killed) { + resolve(fail("generator_timeout", `codex 子代理超时(${timeoutMs / 1000}s)`)); + return; + } + if (!existsSync(patchPath)) { + resolve(fail("patch_not_generated", `codex 退出码 ${code ?? "未知"} 但未产出补丁`, "子代理未生成 generation-patch.json")); + return; + } + const applied = applyGeneration({ workdir, patchFile: ".reports/generation-patch.json" }); + if (!applied.ok) { + resolve(fail("apply_failed", "生成桥接写前校验失败", applied.error?.detail)); + return; + } + resolve(ok({ applied: true, generation: applied.data, detail: `补丁落地: ${(applied.data?.files_changed ?? []).length + (applied.data?.products_written ?? []).length} 个文件` })); + }); + }); + appendTrace(workdir, { + event: result.ok && result.data?.applied ? "generator_applied" : "generator_result", + ok: result.ok, applied: result.ok ? result.data?.applied : false, skipped: result.ok ? result.data?.skipped : undefined, + }); + return result; +} + +/** CLI 包装 -- uv generate */ +export async function generateCmd(options: { workdir?: string; engine?: string }): Promise> { + const workdir = options.workdir ?? process.cwd(); + const engine = options.engine === "codex" || options.engine === "none" ? options.engine : undefined; + return runGenerator({ workdir, engine }); +} diff --git a/src/commands/guard-report.ts b/src/commands/guard-report.ts index ab1ce16..c6b06f8 100644 --- a/src/commands/guard-report.ts +++ b/src/commands/guard-report.ts @@ -12,6 +12,7 @@ import { runTests } from "./run-tests.ts"; import { checkScope } from "./check-scope.ts"; import { buildGuardReport } from "../lib/guard.ts"; import type { ToolResult, GuardReport, TestSummary } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; export interface GuardReportOptions { whitelist?: string[]; @@ -72,6 +73,7 @@ export function guardReport(workdir: string, options?: GuardReportOptions): Tool return fail("guard_failed", `全局守卫未通过: ${summary.failed}/${summary.passed + summary.failed}`, failedChecks); } + recordCommandEvent(workdir, "guard-report", { checks: checks.length, failed: summary.failed }); return ok({ report, path: outPath, diff --git a/src/commands/iteration-check.ts b/src/commands/iteration-check.ts index f15cc3e..f345ffa 100644 --- a/src/commands/iteration-check.ts +++ b/src/commands/iteration-check.ts @@ -10,6 +10,7 @@ import { readCheckpoint } from "./checkpoint.ts"; import { findPhase } from "../lib/phase.ts"; import { appendLessons } from "../lib/lessons.ts"; import type { ToolResult, IterationCheckResult } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; /** 从 flows/*.json 查找阶段 max_iterations,跨所有流程搜索 */ function resolveMaxIterations(config: ReturnType, phase: string, workdir: string): number | null { @@ -58,6 +59,7 @@ export function iterationCheck( `当前迭代 ${currentIteration} 已达上限 ${maxIterations},任务终止`); } + recordCommandEvent(workdir, "iteration-check", { phase, currentIteration, maxIterations, allowed: true }); return ok({ phase, currentIteration, diff --git a/src/commands/preflight-check.ts b/src/commands/preflight-check.ts index 12786e6..984934e 100644 --- a/src/commands/preflight-check.ts +++ b/src/commands/preflight-check.ts @@ -10,6 +10,7 @@ import { ok, fail } from "../lib/result.ts"; import { isGitRepo } from "../lib/git.ts"; import { loadConfig } from "../lib/config.ts"; import type { ToolResult, PreflightCheckItem, PreflightResult } from "../types.ts"; +import { listExecutors } from "../executors/index.ts"; export function preflightCheck(workdir: string): ToolResult { const checks: PreflightCheckItem[] = []; @@ -79,6 +80,26 @@ export function preflightCheck(workdir: string): ToolResult { detail: hasFlows ? "存在" : "flows/ 目录缺少流程定义文件", }); + // P1-2: 执行器注册校验 — flows/*.json 声明的 executor 必须存在于执行器注册表(防运行时找不到) + const knownExecutors = new Set(listExecutors()); + const executorErrors: string[] = []; + try { + const cfg = loadConfig(workdir); + for (const [flowId, flow] of Object.entries(cfg.flows ?? {})) { + for (const ph of flow.phases) { + const execId = (ph as { executor?: string }).executor; + if (execId && !knownExecutors.has(execId)) { + executorErrors.push(`${flowId}/${ph.id}: 未注册执行器 "${execId}"`); + } + } + } + } catch { /* 配置加载失败已由 flows 检查覆盖 */ } + checks.push({ + name: "执行器注册", + passed: executorErrors.length === 0, + detail: executorErrors.length > 0 ? executorErrors.join("; ") : "全部已注册", + }); + // 6. 工作区卫生门禁 — 拒绝已知临时残留文件存在,保障无人值守工作区状态确定 const HYGIENE_BLOCKED = ["_dbg.mjs", "_dbg.ts", "_tmp_fix.py", ".tmp-edits.json", ".registry.bak"]; const hygieneFound: string[] = HYGIENE_BLOCKED.filter((f) => existsSync(join(workdir, f))); diff --git a/src/commands/registry.ts b/src/commands/registry.ts index 1bbb0ec..0a9ef76 100644 --- a/src/commands/registry.ts +++ b/src/commands/registry.ts @@ -266,6 +266,9 @@ export const COMMANDS: CommandSpec[] = [ simpleCmd("auto-notify", "自动通知记录", undefined, { call: (fn, opts) => fn(opts.workdir, {}) }), // ========== 生成 ========== + simpleCmd("generate", "TS 原生生成器桥接: 自动 spawn codex 子代理并落地补丁(P0-2)", [ + { flags: "--engine ", description: "覆盖生成器引擎" }, + ], { exportName: "generateCmd" }), simpleCmd("generate-skill", "从 flows/*.json 自动生成 SKILL.md", [ { flags: "--check", description: "仅校验一致性,不写入" }, ]), diff --git a/src/commands/run-tests.ts b/src/commands/run-tests.ts index f8b82ff..4b2de4c 100644 --- a/src/commands/run-tests.ts +++ b/src/commands/run-tests.ts @@ -7,7 +7,7 @@ import { logger } from "../lib/logger.ts"; import { ok, fail } from "../lib/result.ts"; import type { ToolResult, TestSummary } from "../types.ts"; import type { UvConfig } from "../lib/config.ts"; -import { appendTrace } from "../lib/trace.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; function execCapture(cwd: string, command: string): { code: number; stdout: string; stderr: string } { try { @@ -52,7 +52,7 @@ export function runTests(workdir: string, config: UvConfig): ToolResult 0)) { const out = (vit.stdout || vit.stderr).slice(0, 4000); - appendTrace(workdir, { event: "run_tests", ok: false, summary }); + recordCommandEvent(workdir, "run-tests", { ok: false, summary }); return fail( "test_failed", summary ? `测试失败: ${summary.failed} failed / ${summary.total}` : "测试命令失败", @@ -69,6 +69,6 @@ export function runTests(workdir: string, config: UvConfig): ToolResult { const spec = loadSpec(workdir); @@ -43,5 +44,6 @@ export function semanticDiff(workdir: string): ToolResult<{ report: DiffReport; ].join("\n"); return fail("semantic_mismatch", "语义比对未通过", detail); } + recordCommandEvent(workdir, "semantic-diff", { is_align: report.is_align }); return ok({ report, path: outPath }); } \ No newline at end of file diff --git a/src/commands/snapshot.ts b/src/commands/snapshot.ts index d7ba49a..44d5963 100644 --- a/src/commands/snapshot.ts +++ b/src/commands/snapshot.ts @@ -7,6 +7,7 @@ import { ok, fail } from "../lib/result.ts"; import { runTests } from "./run-tests.ts"; import { loadConfig } from "../lib/config.ts"; import type { ToolResult, TestSummary } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; export interface SnapshotOptions { targets: string[]; @@ -32,5 +33,6 @@ export function runSnapshot(workdir: string, options: SnapshotOptions): ToolResu }; const path = join(reportsDir, "baseline.json"); writeFileSync(path, JSON.stringify(baseline, null, 2), "utf-8"); + recordCommandEvent(workdir, "snapshot", { targets: options.targets.length }); return ok({ path, testSummary: { passed: tResult.data!.passed, failed: tResult.data!.failed, total: tResult.data!.total } }); } \ No newline at end of file diff --git a/src/commands/unattended.ts b/src/commands/unattended.ts index bd184d8..4fee2e9 100644 --- a/src/commands/unattended.ts +++ b/src/commands/unattended.ts @@ -13,6 +13,7 @@ import { loadConfig } from "../lib/config.ts"; import { autoRecover } from "./auto-recover.ts"; import { decideDegradation } from "../lib/degradation.ts"; import { runFlow } from "./run.ts"; +import { runGenerator } from "./generate.ts"; import { missingArtifacts, findPhase, readExecutorIteration, getPhaseMaxIterations, appendCircuitBreakerRecord } from "../lib/phase.ts"; import { autoCompress } from "../lib/compressor.ts"; import { checkRepetitiveFailure } from "../lib/fingerprint.ts"; @@ -92,6 +93,17 @@ export async function unattendedRun(options: UnattendedOptions): Promise { const path = join(workdir, artifact); @@ -43,5 +44,6 @@ export function validateArtifact(workdir: string, artifact: string): ToolResult< } } + recordCommandEvent(workdir, "validate-artifact", { artifact }); return ok({ valid: true, errors: [] }); } diff --git a/src/commands/validate-citation.ts b/src/commands/validate-citation.ts index ff58124..cb68622 100644 --- a/src/commands/validate-citation.ts +++ b/src/commands/validate-citation.ts @@ -9,6 +9,7 @@ import { join } from "node:path"; import { ok, fail } from "../lib/result.ts"; import { verifyCitations, verifyReviewLocations } from "../lib/citation.ts"; import type { ToolResult, ReverseImplItem } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; export interface ValidateCitationOptions { workdir: string; @@ -35,7 +36,8 @@ export function validateCitation(options: ValidateCitationOptions): ToolResult<{ r.issues.map((i) => i.req_id + ": " + i.reason).join("\n"), ); } - return ok({ valid: true, verified: r.verified, issues: [], mode: strict ? "strict" : "standard" }); + recordCommandEvent(workdir, "validate-citation", { verified: r.verified }); + return ok({ valid: true, verified: r.verified, issues: [], mode: strict ? "strict" : "standard" }); } // 2. 批次D: review 产物 location 真值校验 @@ -70,3 +72,4 @@ export function validateCitation(options: ValidateCitationOptions): ToolResult<{ return fail("reverse_missing", "reverse-impl.json 不存在,且无 review 产物可供校验"); } + diff --git a/src/commands/validate-spec.ts b/src/commands/validate-spec.ts index fd9edba..2d50e64 100644 --- a/src/commands/validate-spec.ts +++ b/src/commands/validate-spec.ts @@ -6,6 +6,7 @@ import { join } from "node:path"; import { ok, fail } from "../lib/result.ts"; import { validateProductByPath } from "../lib/schema.ts"; import type { ToolResult } from "../types.ts"; +import { recordCommandEvent } from "../lib/trace.ts"; export function validateSpec(workdir: string): ToolResult<{ valid: boolean; errors: string[] }> { const path = join(workdir, ".reports", "frozen-spec.json"); @@ -13,5 +14,6 @@ export function validateSpec(workdir: string): ToolResult<{ valid: boolean; erro const content = readFileSync(path, "utf-8"); const r = validateProductByPath(path, content); if (!r.valid) return fail("spec_invalid", "spec schema 校验失败", r.errors.join("; ")); + recordCommandEvent(workdir, "validate-spec"); return ok({ valid: true, errors: [] }); -} \ No newline at end of file +} diff --git a/src/lib/config.ts b/src/lib/config.ts index b1ed1f1..631a960 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -13,6 +13,19 @@ export interface CompressionThreshold { l4TokenRatio: number; } +export interface GeneratorConfig { + /** 生成器引擎: codex(自动 spawn 子代理) | none(禁用,回退暂停协议) */ + engine: "codex" | "none"; + /** codex CLI 可执行文件,默认 codex */ + command?: string; + /** 子代理模型,空则用默认 */ + model?: string; + /** 子代理沙箱级别,默认 workspace-write */ + sandbox?: "read-only" | "workspace-write" | "danger-full-access"; + /** 单次生成超时(秒),默认 1800 */ + timeoutSec?: number; +} + export interface UvConfig { testCommand: string; typecheckCommand: string; @@ -32,6 +45,8 @@ export interface UvConfig { subagentSecret?: string; /** 批次E: 阶段推进成功后自动 git commit + tag(默认开启;无 git 用户时降级为 warn 不阻断) */ autoCommitPerPhase?: boolean; + /** P0-2: TS 原生生成器桥接配置(真无人值守) */ + generator?: GeneratorConfig; /** 批次E: 阶段 tag 前缀,默认 "uv/" */ phaseTagPrefix?: string; compressionThreshold: CompressionThreshold; @@ -54,6 +69,7 @@ export const DEFAULT_CONFIG: UvConfig = { maxGenerationApplyWindowSec: 600, autoCommitPerPhase: true, phaseTagPrefix: "uv/", + generator: { engine: "codex", command: "codex", sandbox: "workspace-write", timeoutSec: 1800 }, allowGateDegrade: false, hooks: { hooks: [], on_failure: "ignore" }, }; diff --git a/src/lib/skillgen.ts b/src/lib/skillgen.ts index 8aa3b23..bc3f617 100644 --- a/src/lib/skillgen.ts +++ b/src/lib/skillgen.ts @@ -11,6 +11,28 @@ import type { FlowDefinition as FlowDef, FlowPhaseDef, IntentMapConfig } from ". /** 共享阶段模板库 flow id — 不生成独立 SKILL,仅作为 $ref 目标 */ const SHARED_FLOW_ID = "_shared"; +/** P0-3: 公共驱动循环 — 单一真值源,所有流程 skill 共享,消除四份重复文本 */ +const DRIVE_LOOP: string[] = [ + "## 驱动循环(唯一执行主线)", + "阶段推进一律由机械驱动循环执行;不手工调 phase-enter / dispatch-phase / iteration-check。", + "", + "1. 调 `npx tsx src/index.ts run` 推进一个阶段(内部:phase-enter → 机械执行 → phase-transition → 自动压缩)", + "2. 解读输出:", + " - `completed: true` → 流程完成,结束", + " - `paused: true` → 需要 LLM 生成:调 `npx tsx src/index.ts spawn-generation` 产出子代理简报 → spawn 子代理 → 子代理只写补丁 `.reports/generation-patch.json` → 调 `npx tsx src/index.ts apply-generation --patch-file .reports/generation-patch.json` 写前校验落地 → 回到步骤 1", + " - 失败(ok=false) → 读取 error.code:失败策略由 flows/*.json 的 on_failure 定义;同类失败由失败指纹熔断,自动恢复走 auto-recover", + "3. 禁止:手工写 checkpoint、跳过 phase-transition、降低门禁验证级别", + "", +]; + +/** P0-3: 公共流程定义尾部 — 单一真值源说明 */ +const FLOW_DEF_TAIL = (flowId: string): string[] => [ + "## 流程定义", + `- 真值源: \`flows/${flowId}.json\`(运行 \`npm run generate:skills\` 重新生成本文件)`, + "- 阶段产物/Schema/白名单等细节以该 JSON 为准,不再内嵌到 SKILL.md(防上下文膨胀)", + "- 生成一致性由 CI 的 `generate-skill --check` 强制", + "", +]; interface SharedPrelude { reference_docs?: string[]; @@ -111,28 +133,11 @@ function generateFlowSkill(flow: FlowDef, flowsDir: string): string { } parts.push(""); - // 驱动循环 — 唯一执行主线,机械层承担全部细节 - parts.push( - "## 驱动循环(唯一执行主线)", - "阶段推进一律由机械驱动循环执行;不手工调 phase-enter / dispatch-phase / iteration-check。", - "", - "1. 调 `npx tsx src/index.ts run` 推进一个阶段(内部:phase-enter → 机械执行 → phase-transition → 自动压缩)", - "2. 解读输出:", - " - `completed: true` → 流程完成,结束", - " - `paused: true` → 需要 LLM 生成:调 `npx tsx src/index.ts spawn-generation` 产出子代理简报 → spawn 子代理 → 子代理只写补丁 `.reports/generation-patch.json` → 调 `npx tsx src/index.ts apply-generation --patch-file .reports/generation-patch.json` 写前校验落地 → 回到步骤 1", - " - 失败(ok=false) → 读取 error.code:失败策略由 flows/*.json 的 on_failure 定义;同类失败由失败指纹熔断,自动恢复走 auto-recover", - "3. 禁止:手工写 checkpoint、跳过 phase-transition、降低门禁验证级别", - "", - ); + // P0-3: 公共驱动循环 — 单一真值源 DRIVE_LOOP,消除跨 skill 重复 + parts.push(...DRIVE_LOOP); parts.push(...generateHardConstraints(flow.protected_files, hasSubagent)); - parts.push( - "## 流程定义", - `- 真值源: \`flows/${flow.id}.json\`(运行 \`npm run generate:skills\` 重新生成本文件)`, - "- 阶段产物/Schema/白名单等细节以该 JSON 为准,不再内嵌到 SKILL.md(防上下文膨胀)", - "- 生成一致性由 CI 的 `generate-skill --check` 强制", - "", - ); + parts.push(...FLOW_DEF_TAIL(flow.id)); return parts.join("\n"); } diff --git a/src/lib/trace.ts b/src/lib/trace.ts index 2b48cbe..ce2fc12 100644 --- a/src/lib/trace.ts +++ b/src/lib/trace.ts @@ -37,6 +37,25 @@ export function findTraceEvent(workdir: string, event: string, phase?: string): * 返回连续匹配的数量;全部匹配返回 expectedEvents.length。 * 与旧版不同:要求事件按顺序连续出现,中间不能跳过。 */ +/** 事件名归一化: 下划线转连字符,保证 kebab-case 单一标准(A-4) + * flows/*.json 的 expected_events 为 kebab-case;命令内部可能写下划线名,统一归一化匹配 */ +export function normalizeEventName(name: string): string { + return name.replace(/_/g, "-"); +} + +/** 机械命令事件 — 写入 trace 前自动附加当前阶段(checkpoint.next_phase) */ +export function recordCommandEvent(workdir: string, eventName: string, detail?: Record): void { + const cpPath = join(workdir, ".reports", "checkpoint-summary.json"); + let phase: string | undefined; + try { + if (existsSync(cpPath)) { + const cp = JSON.parse(readFileSync(cpPath, "utf-8")) as { next_phase?: string }; + phase = cp.next_phase; + } + } catch { /* 阶段读取失败则不附加 phase */ } + appendTrace(workdir, { event: eventName, ...(phase ? { phase } : {}), ...(detail ?? {}) }); +} + export function matchTraceSequence(workdir: string, phase: string, expectedEvents: string[]): number { const traces = readTrace(workdir) .filter((t) => t.phase === phase) @@ -49,7 +68,8 @@ export function matchTraceSequence(workdir: string, phase: string, expectedEvent let matchCount = 0; let expectedIdx = 0; for (const trace of traces) { - if (expectedIdx < expectedEvents.length && trace.event === expectedEvents[expectedIdx]) { + const evtName = normalizeEventName(String(trace.event ?? "")); + if (expectedIdx < expectedEvents.length && evtName === expectedEvents[expectedIdx]) { matchCount++; expectedIdx++; } else if (expectedIdx > 0 && expectedIdx < expectedEvents.length) { @@ -60,3 +80,4 @@ export function matchTraceSequence(workdir: string, phase: string, expectedEvent return matchCount; } + diff --git a/uv.config.json b/uv.config.json index bd237d1..6edf128 100644 --- a/uv.config.json +++ b/uv.config.json @@ -53,5 +53,11 @@ } ], "on_failure": "warn" + }, + "generator": { + "engine": "codex", + "command": "codex", + "sandbox": "workspace-write", + "timeoutSec": 1800 } }