Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .codex/instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,17 @@
7. 迭代耗尽 → 终止报告失败,禁止无限重试(迭代检查由 phase-transition 内部机械截断)
8. 阶段间调 `context-summary` 压缩上下文, phase-enter 含上下文预算硬上限
9. 未匹配命令 → 提示正确用法


## 新任务标准序列(无人值守入口)

1. 新任务统一从 `uv init-task --flow <bugfix|feature|refactor|review> --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
8 changes: 6 additions & 2 deletions flows/review.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand All @@ -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": [
Expand Down
3 changes: 3 additions & 0 deletions src/commands/audit-deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof parseNpmAudit>;
Expand Down Expand Up @@ -38,5 +39,7 @@ export function auditDeps(workdir: string): ToolResult<AuditDepsResult> {
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 });
}

8 changes: 6 additions & 2 deletions src/commands/check-static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand All @@ -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");
Expand All @@ -40,4 +44,4 @@ export function checkStatic(workdir: string): ToolResult<{ passed: boolean; issu
// warn 级不阻断,只报告
}
return ok({ passed: true, issues: r.issues });
}
}
121 changes: 121 additions & 0 deletions src/commands/generate.ts
Original file line number Diff line number Diff line change
@@ -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";

Check warning on line 9 in src/commands/generate.ts

View workflow job for this annotation

GitHub Actions / check

'mkdirSync' is defined but never used

Check warning on line 9 in src/commands/generate.ts

View workflow job for this annotation

GitHub Actions / check

'writeFileSync' is defined but never used
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<ToolResult<GeneratorBridgeResult>> {
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<ToolResult<GeneratorBridgeResult>>((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<ToolResult<GeneratorBridgeResult>> {
const workdir = options.workdir ?? process.cwd();
const engine = options.engine === "codex" || options.engine === "none" ? options.engine : undefined;
return runGenerator({ workdir, engine });
}
2 changes: 2 additions & 0 deletions src/commands/guard-report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/iteration-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof loadConfig>, phase: string, workdir: string): number | null {
Expand Down Expand Up @@ -58,6 +59,7 @@ export function iterationCheck(
`当前迭代 ${currentIteration} 已达上限 ${maxIterations},任务终止`);
}

recordCommandEvent(workdir, "iteration-check", { phase, currentIteration, maxIterations, allowed: true });
return ok({
phase,
currentIteration,
Expand Down
21 changes: 21 additions & 0 deletions src/commands/preflight-check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<PreflightResult> {
const checks: PreflightCheckItem[] = [];
Expand Down Expand Up @@ -79,6 +80,26 @@ export function preflightCheck(workdir: string): ToolResult<PreflightResult> {
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)));
Expand Down
3 changes: 3 additions & 0 deletions src/commands/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <codex|none>", description: "覆盖生成器引擎" },
], { exportName: "generateCmd" }),
simpleCmd("generate-skill", "从 flows/*.json 自动生成 SKILL.md", [
{ flags: "--check", description: "仅校验一致性,不写入" },
]),
Expand Down
10 changes: 5 additions & 5 deletions src/commands/run-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -52,15 +52,15 @@ export function runTests(workdir: string, config: UvConfig): ToolResult<TestSumm
logger.info("run-tests", "类型检查: " + config.typecheckCommand);
const tsc = execCapture(workdir, config.typecheckCommand);
if (tsc.code !== 0) {
appendTrace(workdir, { event: "run_tests", ok: false, phase: "typecheck" });
recordCommandEvent(workdir, "run-tests", { ok: false, phase: "typecheck" });
return fail("typecheck_failed", "tsc --noEmit 未通过", (tsc.stderr || tsc.stdout).slice(0, 4000));
}
logger.info("run-tests", "测试: " + config.testCommand + " --reporter=json");
const vit = execCapture(workdir, config.testCommand + " --reporter=json");
const summary = parseVitestSummary(vit.stdout) ?? parseVitestSummary(vit.stderr);
if (vit.code !== 0 && (!summary || summary.failed > 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}` : "测试命令失败",
Expand All @@ -69,6 +69,6 @@ export function runTests(workdir: string, config: UvConfig): ToolResult<TestSumm
}
const sum = summary ?? { passed: 0, failed: 0, total: 0 };
// A-1: 机械记录测试运行事件,供 verify-action 行为检测
appendTrace(workdir, { event: "run_tests", ok: true, summary: sum });
recordCommandEvent(workdir, "run-tests", { ok: true, summary: sum });
return ok({ ...sum, output: (vit.stdout || vit.stderr).slice(0, 4000) });
}
}
2 changes: 2 additions & 0 deletions src/commands/semantic-diff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ok, fail } from "../lib/result.ts";
import { loadSpec } from "../lib/spec.ts";
import { compareSpecImpl } from "../lib/semantic.ts";
import type { ToolResult, DiffReport, ReverseImpl } from "../types.ts";
import { recordCommandEvent } from "../lib/trace.ts";

export function semanticDiff(workdir: string): ToolResult<{ report: DiffReport; path: string }> {
const spec = loadSpec(workdir);
Expand Down Expand Up @@ -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 });
}
2 changes: 2 additions & 0 deletions src/commands/snapshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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 } });
}
12 changes: 12 additions & 0 deletions src/commands/unattended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -92,6 +93,17 @@ export async function unattendedRun(options: UnattendedOptions): Promise<ToolRes
return ok({ completed: true, phases: [...completed, ...(d.phases ?? [])] });
}
if (d.paused) {
// P0-2: TS 原生生成器桥接 — generator.engine=codex 时机械层自动 spawn 子代理
const autoGen = await runGenerator({ workdir });
if (autoGen.ok && autoGen.data?.applied) {
appendTrace(workdir, { event: "unattended_auto_generated", phase: phaseId });
continue; // 补丁已落地,重新驱动循环
}
if (!autoGen.ok) {
return fail("autogen_failed", `无人值守: 自动生成失败(${autoGen.error?.code})`, autoGen.error?.detail);
}
appendTrace(workdir, { event: "unattended_autogen_skipped", phase: phaseId, reason: autoGen.data?.skipped ?? "unknown" });
// 回退: generator 未启用/无请求/无 codex → 走原暂停协议
// 需要 LLM 生成: noPause 模式下拒绝假完成,否则暂停等待 apply-generation
if (noPause) {
appendTrace(workdir, { event: "unattended_generation_blocked", phase: phaseId });
Expand Down
2 changes: 2 additions & 0 deletions src/commands/validate-artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { validateProductByPath } from "../lib/schema.ts";
import { isGitRepo } from "../lib/git.ts";
import { execSync } from "node:child_process";
import type { ToolResult } from "../types.ts";
import { recordCommandEvent } from "../lib/trace.ts";

export function validateArtifact(workdir: string, artifact: string): ToolResult<{ valid: boolean; errors: string[] }> {
const path = join(workdir, artifact);
Expand Down Expand Up @@ -43,5 +44,6 @@ export function validateArtifact(workdir: string, artifact: string): ToolResult<
}
}

recordCommandEvent(workdir, "validate-artifact", { artifact });
return ok({ valid: true, errors: [] });
}
Loading
Loading