diff --git a/doc/GUIDE.md b/doc/GUIDE.md index d4d4af17..be851fe4 100644 --- a/doc/GUIDE.md +++ b/doc/GUIDE.md @@ -492,11 +492,11 @@ candidate ──人工晋升──▶ active ──▶ locked 回滚 = gi ## 6. Steps -38 个 step 在 `engine/steps/` 就地 `@step` 自注册(`rebase_native.py` / `issue.py` / +45 个 step 在 `engine/steps/` 就地 `@step` 自注册(`rebase_v3.py` / `issue.py` / `pr/publish.py` 里有几个用 `register_step` 直接注册)。每个 step 声明两个属性: -- **kind**——`deterministic`(17)· `agent`(8)· `script`(7)· `validation`(3)· - `report`(3)。`kind == "agent"` 是有语义的:它意味着这个 step 走统一的 +- **kind**——`deterministic`(25)· `agent`(8)· `script`(8)· `validation`(2)· + `report`(2)。`kind == "agent"` 是有语义的:它意味着这个 step 走统一的 `run_agent_step` 运行时治理(dispatch context、证据围栏、scope、结构化输出契约、 完整 trace),由一个参数化测试钉死。 - **risk**——`read` · `knowledge` · `write_workspace` · `push` · `report`。 @@ -504,7 +504,7 @@ candidate ──人工晋升──▶ active ──▶ locked 回滚 = gi 失败是**值不是异常**:`StepResult.failure` 是六种 `FailureKind` 之一, BLOCKED / ESCALATE / FORBIDDEN 会通知并停机,只有 RETRYABLE 会有界重试。 -### 6.1 deterministic(17)— 不花模型调用 +### 6.1 deterministic(25)— 不花模型调用 | step | risk | 做什么 | |---|---|---| @@ -522,9 +522,17 @@ BLOCKED / ESCALATE / FORBIDDEN 会通知并停机,只有 RETRYABLE 会有界 | `profile.ingest_docs` | knowledge | 摄入 AGENTS.md/CLAUDE.md 式人工指令(文档冗余行丢弃) | | `profile.detect_drift` | read | Stage 4 漂移报告——刷新材料,绝不自动修 | | `profile.decay_stale` | knowledge | Stage 4 休眠衰减:未确认的 fact 转 stale(排除但**不删**) | -| `rebase.prelude` | read | 父运行时设置 + wave 列表 | -| `rebase.phase2_prepare` | read | 预检 curator + phase-2 进度初始化 | -| `rebase.phase2_finalize` | read | 两个 wave 完成后推进父标记 | +| `workspace.guard_clean_rebase` | write_workspace | rebase 专用脏树守卫(adapter 策略委托) | +| `rebase.v3_prelude` | read | 模式解析写回、substate/锁/终局 finalizer、知识开账 | +| `rebase.v3_guard` | write_workspace | 幂等重取 checkout flock + 注入 rebase 守卫策略 | +| `rebase.v3_scan` | read | 动态测试清单(CI YAML + 活测试树 + diff 分类) | +| `rebase.v3_wheel` | write_workspace | 选 upstream commit → 装进声明 venv → 最后 pin | +| `rebase.v3_assign` | read | commit→模块归类 + path-sync 后按 wave 分派 | +| `rebase.v3_wave_gate` | read | wave-1 失败裁决 wave-2 / 按配置升级 | +| `rebase.v3_push_gate` | read | 推送闸:结构性 vs 断言失败的确定性分类 | +| `rebase.v3_knowledge_prep` | knowledge | 知识层解析 + schema v2 保障(声明未展开 ⇒ BLOCKED) | +| `rebase.v3_phase5_report` | read | phase-5 汇总进 substate | +| `rebase.v3_finalize` | read | 终局裁决:substate 有失败 ⇒ BLOCKED needs-human | ### 6.2 agent(8)— 走统一运行时治理 @@ -539,30 +547,29 @@ BLOCKED / ESCALATE / FORBIDDEN 会通知并停机,只有 RETRYABLE 会有界 | `agent.profile_consolidate` | knowledge | Stage 4:**唯一**允许 rewrite/merge 的层,强制稳定性门禁 | | `profile.judge` | read | Stage 4 只读审计 → JUDGE_REPORT.md,findings 只呈现不自动应用 | -### 6.3 script(7)— 委托或对外写 +### 6.3 script(8)— 委托或对外写 | step | risk | 做什么 | |---|---|---| | `ci.push` | **push** | 受门禁的 push(PushPolicy ∧ 保护分支双闸,默认 dry-run) | | `pr.post_review` | **push** | 单条 GitHub review + inline threads(显式 post 标志 + `ALLOW_POST`) | | `issue.post_answer` | **push** | 发布 issue 回复(同样双闸) | -| `rebase.phase4` | **push** | 父 Phase 4(push + Buildkite CI),走 copilot 的 push guard | -| `rebase.run_external` | write_workspace | 委托给已有 5 阶段编排器(locked 流水线) | -| `rebase.phase1` | write_workspace | 父 Phase 1(init) | -| `rebase.module_rebase` | write_workspace | 单模块 rebase,委托父仓库自己的 `node_rebase_module` | +| `rebase.v3_ci` | **push** | phase-4 push + CI 轮次(WAL、双闸、op-id 台账) | +| `rebase.v3_module_rebase` | write_workspace | 单模块 rebase 单元(foreach;同 checkout 串行) | +| `rebase.v3_test_loop` | write_workspace | phase-3 本地测试环(逐测试恢复、baseline 分流) | +| `rebase.v3_precommit` | write_workspace | precommit 轮(passed/failed/failed_preexisting) | +| `rebase.v3_curate` | knowledge | 运行时知识策展 + watchdog 收割(只写 runtime 侧) | > 全库只有这 4 个 `risk == "push"` 的 step,全部经 `push.py::guard_push`。 -### 6.4 validation(3)与 report(3) +### 6.4 validation(2)与 report(2) | step | kind | risk | 做什么 | |---|---|---|---| | `review.patch_gate` | validation | read | 条件式 patch review,push 前 fail-closed | | `agent.verify_module` | validation | read | 逐模块 rebase 损伤检查——纯 LLM 建议,**不是**受治理 agent step | -| `rebase.phase3` | validation | write_workspace | 父 Phase 3(本地流水线测试 + SDK debug 循环) | | `report.final_summary` | report | report | 写 `RUN_REPORT.md` + `DIAGNOSTICS.md` | -| `rebase.phase5` | report | report | 父 Phase 5 最终摘要 + 运行后 curator | -| `rebase.compare_with_locked` | report | report | `COMPARISON.md`:原生 run vs locked 基线(晋升证据) | +| `rebase.v3_compare` | report | read | 与基线对比裁决(劣于 baseline ⇒ BLOCKED) | --- diff --git a/doc/architecture/CODE_TOUR.md b/doc/architecture/CODE_TOUR.md index 0e633959..0b80d6fe 100644 --- a/doc/architecture/CODE_TOUR.md +++ b/doc/architecture/CODE_TOUR.md @@ -271,13 +271,11 @@ re-export,公开导入面不变)。 待裁决 skill candidate 队列)与 `DIAGNOSTICS.md`(逐 step 诊断,评审曾被三重 渲染并混入 blockers/confidence 噪声,现隔离)。 -- **委托/夜跑支流**(数据流出到父流水线): - **`engine/steps/rebase_ext.py`**(115)——`rebase.run_external`:锁定夜跑的 - 受监控子进程委托。**`engine/steps/rebase_native.py`**(443)——原生分解候选 - (wrap 父包函数、不重写;`repo-rebase-native` playbook,candidate)。 - **`rebase/monitor.py`**(163)——只读消费父 orchestrator 的 `state.json` - (phase/module/test 进度)→ copilot 进度事件 + 失败分类 + 升级材料,绝不写 - 父文件;`rebase/__init__.py`(13)导出。 +- **rebase 支流**(2026-08-25 cutover 后):委托版 `rebase_ext`、包装版 + `rebase_native` 与父流水线监视器 `rebase/monitor.py` 已随 v1/v2 退役删除。 + 现行实现是 **`engine/steps/rebase_v3.py`** + **`engine/steps/rebase_knowledge.py`** + (step 装配)与 **`rebase_engine/`** 包(原语:worktree/推送 WAL/CI 轮次/ + 测试环境/知识迁移;见 `SPEC/rebase_engine.md`)。 - **profile 建立支流**(通向 §6):**`engine/steps/profile.py`**(441)—— `profile.fingerprint` → `structure_scan` → `ingest_docs` → `agent.profile_repo` diff --git a/doc/architecture/SPEC/README.md b/doc/architecture/SPEC/README.md index d899f867..76f8bf13 100644 --- a/doc/architecture/SPEC/README.md +++ b/doc/architecture/SPEC/README.md @@ -59,10 +59,10 @@ |---|---| | 接口 / 任务 | `task_spec` `intent` `cli` `chat` `ui` `config` | | 引擎底座 | `engine/step` `engine/registry` `engine/executor` `engine/planner` `engine/agent_runtime` `agent_loop` `tools` `scopes` `llm` | -| Step 库 | `engine/steps/__init__` `engine/steps/_common` `engine/steps/{workspace,rebase_ext,review,report,pr,issue,profile,rebase_native}` | +| Step 库 | `engine/steps/__init__` `engine/steps/_common` `engine/steps/{workspace,review,report,pr,issue,profile,rebase_v3,rebase_knowledge}` | | 规划数据 | `playbooks/store` `playbooks/PLAYBOOKS`(yaml) | | 边缘 —— 语言 | `profiles/languages`(按语言的规则,共享) | -| 边缘 | `adapters/base` `ci/normalize` `ci/providers` `rebase/monitor` | +| 边缘 | `adapters/base` `ci/normalize` `ci/providers` `ci/buildkite` | | 安全原语 | `scopes` `push` | | Profile | `profiles/store` `profiles/establish` `profiles/repo_map` `profiles/consolidate` | | 跨切 | `review/{diff_summary,triggers,reviewer}` `memory/{debug_memory,skills}` `run_trace` `notify` `metrics` | diff --git a/doc/architecture/SPEC/adapters/base.md b/doc/architecture/SPEC/adapters/base.md index c7de26b1..89a8b101 100644 --- a/doc/architecture/SPEC/adapters/base.md +++ b/doc/architecture/SPEC/adapters/base.md @@ -1,8 +1,8 @@ # adapters/base.py —— 规范 - + -`LOC ~368 · 边缘(仓库知识) · refactor-status: ok` +`LOC ~423 · 边缘(仓库知识) · refactor-status: ok` ## 职责 `RepoAdapter`(住在边缘的仓库知识)、adapter 注册表,以及确定性的 Phase-0 引导。 @@ -14,6 +14,7 @@ (见 DESIGN 的命名说明)。 ## 公开契约 +`expand_path(value, extra?)`、`AdapterError` 与其子类 `AdapterNotFound`; `RepoAdapter`(属性 `status`、`repo_path`、`protected_branches`、`modules`、 `high_risk_modules`、`capabilities`、`skills_dir`、`debug_memory_db`、 `profile_dir`、`briefing()`;方法 `module_for_path`);`load_adapter`、 @@ -21,7 +22,12 @@ `draft_adapter`。 ## 不变量 -- **D2**:`update_manifest` **拒绝** agent 对 `push`/`repo`/`upstream` 的写入。 +- **D2**:`update_manifest` **拒绝** agent 对 `push`/`repo`/`upstream`/`rebase` 的写入。 +- `expand_path` 先展开进程 env,再用 `extra`(典型是 `Settings.expansion_env()`, + 即 `.env` 里 pydantic 吃进 Settings 而未 export 的键)做**回退**;进程 env 永远 + 获胜且**绝不被修改**;仍未解析的变量 → 返回 ""(fail-closed,能力缺口路径)。 +- **未知 adapter 名抛 `AdapterNotFound`**(子类);把"不存在"当兼容路径的调用方 + 只能捕获这个子类 —— 已知 adapter 的畸形 manifest 必须仍是硬失败。 - `capabilities` 由 manifest 推导(repo.path/language.*/ci.provider/upstream.*/ modules)+ 显式的 `capabilities:` —— 与 playbook 的 `requires:` 匹配。 - `high_risk_modules` = 标了 `risk: high` 的模块(喂给 patch-review,**A5**)。 diff --git a/doc/architecture/SPEC/chat.md b/doc/architecture/SPEC/chat.md index 7ea8d456..d919a5eb 100644 --- a/doc/architecture/SPEC/chat.md +++ b/doc/architecture/SPEC/chat.md @@ -1,8 +1,8 @@ # chat.py —— 规范 - + -`LOC ~440 · 接口(对话式 REPL) · refactor-status: split-candidate` +`LOC ~497 · 接口(对话式 REPL) · refactor-status: split-candidate` ## 职责 Claude-Code 风格的对话式 REPL(配置了 LLM 时的默认形态):一个持续的对话, diff --git a/doc/architecture/SPEC/ci/buildkite.md b/doc/architecture/SPEC/ci/buildkite.md new file mode 100644 index 00000000..67dbc04e --- /dev/null +++ b/doc/architecture/SPEC/ci/buildkite.md @@ -0,0 +1,62 @@ +# ci/buildkite.py —— 规范 + + + +`LOC ~253 · 边缘(CI provider 客户端) · refactor-status: ok` + +## 职责 +rebase 引擎受守卫构建生命周期所注入的 `CIClient` —— 定界到单条 pipeline 的 +Buildkite REST 客户端。 + +## 公开契约 +`BuildkiteCI(token, org, pipeline, build_env?, request?, ignore_branch_filters=False)`; +协议方法 `create_build / get_build / find_builds_by_meta / cancel_build / +get_job_log / list_jobs / retry_job`;采纳/基线查询 `builds_for_commit`、 +`latest_builds`;异常 `BuildkiteError`。`request`(`RequestFn`)可注入(测试 +fake 绕开 urllib)。 + +## 不变量 +- **A5**:org、pipeline、build env 全部来自 adapter(`ci.org` + `rebase.ci.*`) + —— 这里不点名任何仓库、pipeline 或队列。 +- **归一化在这个边界发生**:build dict 的 `id` 是 build **number** 的字符串 + (REST 按 number 寻址,UUID 不可路由),`web_url` 兜底拼出 —— `ci_loop` + 的 op 台账因此存跨进程重启仍可解析的 id。 +- **逐调用点的错误契约**:变更类调用与身份查询(create/cancel/ + find_builds_by_meta/retry 非 400/builds_for_commit/latest_builds)在意外 + 响应上**抛** `BuildkiteError` —— op 标记 created 之前必须先看到失败,恢复/ + 采纳必须升级而不是猜(API 错误绝不当"无匹配");轮询读(`get_build`→`{}`、 + `get_job_log`→`""`)**降级**不中止监控(**E2**;最终 reconciliation 裁决)。 +- `create_build` 只把**一种**响应转成类型化拒绝:422 + "branches have been + disabled" → `BuildCreationRefused`(round loop 据此报 schedule-only 指引, + **B1** 路由的原料);其余 4xx 一律 `BuildkiteError` —— 那条指引在运维性 + 错误(401/403/404…)上会误导。 +- adapter 显式 opt-in(`rebase.ci.ignore_branch_filters`)才在 create 上发送 + `ignore_pipeline_branch_filters`(schedule-only pipeline 的官方补救;step + 级 branch filter 依然生效);默认**绝不发送**。 +- `list_jobs` 两路读取(/jobs 端点 → build 内嵌 jobs),两路都不可读时**抛** + —— 取数失败必须与"确实没有 job"可区分,否则 API 故障期间的 reconciliation + 会静默通过。 +- `retry_job` 的 400 = 该 job **类型**不可重试(`(None, False)`,归 ignorable); + 其他失败一律抛 —— API 故障绝不被当成代码失败去派发变更 agent。 + +## 边界 —— 不属于这里 +不含轮次/监控/失败分类(`rebase_engine/ci_loop`);不含推送 +(`rebase_engine/push_to_ci`);同头 schedule 构建的**采纳决策**在 +`ci_loop.run_ci_rounds` + `rebase.v3_ci`,这里只提供查询面。 + +## 依赖(允许) +stdlib 的 `json`/`urllib`;`BuildCreationRefused` 从 `rebase_engine.ci_loop` +惰性 import(类型化拒绝定义在消费方)。 + +## 扩展点 +新的 provider 客户端 = 实现同一 `CIClient` 协议的**新文件**,不是这里的分支。 + +## 测试 +`test_ci_wiring.py`(注入 RequestFn:归一化、逐调用点错误契约、branch-filter +opt-in 默认关、BuildCreationRefused 只认 422+disabled、list_jobs/retry_job)。 + +## 重构备注 +干净。保持"变更抛 / 轮询降级"的逐调用点契约稳定 —— `create_build_guarded` +与最终 reconciliation 都押在它上面。对 `ci_loop.BuildCreationRefused` 的 +惰性 import 是一条轻微的向内依赖(边缘 → rebase_engine);接入第二个 +provider 时把这个异常类型上提到共享的 CI 契约模块。 diff --git a/doc/architecture/SPEC/cli.md b/doc/architecture/SPEC/cli.md index d129baf8..3cb2ec86 100644 --- a/doc/architecture/SPEC/cli.md +++ b/doc/architecture/SPEC/cli.md @@ -1,8 +1,8 @@ # cli/ —— 规范 - + -`LOC ~1000(5 个文件) · 接口 + 编排门面 · refactor-status: ok` +`LOC ~1240(6 个文件) · 接口 + 编排门面 · refactor-status: ok` ## 职责 flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目录、RunTrace、notifier @@ -18,6 +18,8 @@ flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目 的调用)。 - `utils.py` —— 纯格式化器:`parse_task_params`、`format_metrics_line`。 - `doctor.py` —— 预检诊断(2026-07 新增):逐项 ✓/✗,每个失败给出**唯一**确切的修复命令。 +- 子命令:`doctor` 与 `migrate-knowledge`(PR4d 部署期知识迁移;**显式 owner + 动作,零 LLM**,需 `--repo `,支持 report-only;见 RUNBOOK)。 ## 公开契约(可从 `infermatrix_copilot.cli` import) `main(argv)`;`Copilot`(`resolve`、`run_task`、`run_playbook`、`run_queue`、 @@ -33,6 +35,12 @@ flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目 - 仓库知识(保护分支、高风险模块)由 adapter 进入 run state(**A5**); 被阻塞 → 退出码 3(`BLOCKED_EXIT`)。 - `--playbook` 是运行 candidate 的**唯一**方式。 +- **rebase_mode 是带权威写回的**:`params.rebase_mode` 在过门前经 + `rebase_engine.modes` 解析并写回(`mode_state_flags` 决定 `when:` 门), + 冲突抛 `ModeConflictError` —— review 上下文向 reviewer 说明该模式下 + 哪些 step 会跑。 +- **每仓库知识锁(SHARED)持有整个 run 的生命周期**:run 之间不互斥, + 只与 `migrate-knowledge` 的 EXCLUSIVE 锁互斥 —— 迁移绝不与活跃 run 并发。 - **`doctor` 只读,且永不打印密钥的值** —— 只打印它的名字。除非传 `--probe`, 否则它不做任何付费 LLM 调用;`--probe` 是唯一的付费检查(每个已配置档位一个 token)。 `--json` 供 CI 使用,而**在没有凭据时以非零码退出正是 CI 里的预期状态**。 diff --git a/doc/architecture/SPEC/config.md b/doc/architecture/SPEC/config.md index eee2916a..5fe35f4e 100644 --- a/doc/architecture/SPEC/config.md +++ b/doc/architecture/SPEC/config.md @@ -1,28 +1,38 @@ # config.py —— 规范 - + -`LOC ~418 · 配置 · refactor-status: oversized` +`LOC ~630 · 配置 · refactor-status: oversized` ## 职责 从 env / `.env` 加载的 `Settings`(pydantic-settings),以及把本次 run 的档位与后端 选择变成具体目标的那些推导 helper。 ## 功能 -为 LLM 端点与逐档模型、仓库、引擎预算、推送安全、PR debug、外部 rebase、agent 运行时、 +为 LLM 端点与逐档模型、仓库、引擎预算、推送安全、PR debug、v3 rebase +(reviewer 模型、远端 CI 轮次/预算、`github_token` 推送凭据)、agent 运行时、 ensemble、MoA、评审深度与按 pass 路由、Strict 后端选择、profile、patch 触发器、 -metrics 与升级,提供带类型字段和安全默认值。 +metrics 与升级,提供带类型字段和安全默认值;外加 PR4d 知识运行时 cutover +(`imx_knowledge_runtime`)与 manifest 展开回退(`expansion_env()`)。 ## 公开契约 带全部可调项的 `Settings`;`reviewer` / `intent`(回退到 `agent_model`); `repo_path(name)`;`model_for(mode)`;`tier_target(role)` → `ResolvedTarget`; -以及 `strict_backend` 校验器。 +`expansion_env()`;`knowledge_runtime_repos`;以及 `strict_backend` 校验器。 ## 不变量(**A5**、**C2**、**B1**) - 密钥只经 env / `.env`(被 git 忽略,**绝不提交**)。 -- 仓库专属默认值(`default_repo`、`rebase_agent_root`、`high_risk_modules`、 - `cost_ref_*`)**只是兜底**;adapter/profile 会覆盖它们。它们是这里**唯一被允许**的 - 仓库字面量,泄漏上限为 3,并由 `test_v2_p0.py::test_repo_neutral_core` 钉住。 +- 仓库专属默认值(`default_repo`、`rebase_agent_root`、`cost_ref_*`)**只是兜底**; + adapter/profile 会覆盖它们。它们是这里**唯一被允许**的仓库字面量,由 + `test_v2_p0.py::test_repo_neutral_core` 钉住上限。`high_risk_modules` 的默认值 + 已**中立化为空**(风险声明是仓库知识,由 adapter 的 tier 声明获胜)。 +- **`expansion_env()` 过滤密钥**:名字含 key/token/secret/password 的字段与 `.env` + 键一律排除,dotenv 读取 `interpolate=False`——manifest 路径永远不能把凭据拉进 + 错误信息或日志;Settings 字段在重名时获胜(已体现进程 env 优先)。 +- **`imx_knowledge_runtime` 按仓库 fail-closed**:列出的仓库在 resolve 时校验 + `MIGRATION_COMPLETE.json`,缺标记即失败;未列出的仓库保持 legacy 路径逐字节不变。 +- `github_token` 只服务 v3 远端 CI 推送的 **header 认证**(推送传输按设计禁用 + credential helper,绝不落共享凭据存储)。 - **`STRICT_BACKEND` 在前面就被校验**,失败时列出合法集合。未知后端**绝不能**到达 `providers.resolve_provider` —— 两层,因为 `.env` 里的一个拼写错误不该启动一次注定 失败的 run。 diff --git a/doc/architecture/SPEC/engine/agent_runtime.md b/doc/architecture/SPEC/engine/agent_runtime.md index c8da7751..b98ee7e8 100644 --- a/doc/architecture/SPEC/engine/agent_runtime.md +++ b/doc/architecture/SPEC/engine/agent_runtime.md @@ -1,8 +1,8 @@ # engine/agent_runtime/ —— 规范 - + -`LOC ~1100(7 个文件) · 引擎(受治理的 agent 运行时) · refactor-status: ok` +`LOC ~1690(7 个文件) · 引擎(受治理的 agent 运行时) · refactor-status: ok` ## 职责 每个 `kind == "agent"` step 的**唯一**受治理入口,外加评审质量 ensemble。 @@ -35,8 +35,10 @@ - **唯一入口**:agent step 只能经 `run_agent_step` 做 agentic 工作 —— **不允许**为了调查而临时 `ctx.llm.create()`。 - 证据逐项封顶 + 归档 + `` 围栏(**C7**)。 -- `_ScopedKnowledge`:仓库 skill+memory 优先于共享池;提案落在仓库命名空间, - **且只能是 candidate**(**D1/D2**)。 +- `_ScopedKnowledge`:读取三层有序——runtime(已学得)→ adapter seed → 共享池, + 重名时 runtime 获胜;**写入只落 runtime 侧**(Rev 8 §10:adapter 树在运行时 + 只读——提案进仓库 runtime candidates,seed skill 的使用计数走 runtime usage + journal,seed 文件保持逐字节不变),**且提案只能是 candidate**(**D1/D2**)。 - `_repo_map_tool`:按需拉取,**绝不作为散文注入**;语言不支持 → `capability_gap`。 - briefing 只在 `profile_briefing_enabled` 时进 prompt(消融开关)。 - 输出:base+extension schema、一轮修复、状态→FailureKind;预算耗尽会**强制最终答复**。 diff --git a/doc/architecture/SPEC/engine/executor.md b/doc/architecture/SPEC/engine/executor.md index f6783d63..9bf26293 100644 --- a/doc/architecture/SPEC/engine/executor.md +++ b/doc/architecture/SPEC/engine/executor.md @@ -1,8 +1,8 @@ # engine/executor.py —— 规范 - + -`LOC ~267 · 引擎底座(那个循环) · refactor-status: ok` +`LOC ~293 · 引擎底座(那个循环) · refactor-status: ok` ## 职责 以与任务无关的保证运行一个 playbook 的各个 step:检查点/resume、`foreach` 扇出、 @@ -25,6 +25,9 @@ helper:`_eval_when`、`_merge`。 - **B3**:`when:` 先读 TaskSpec 再读 state;**未知键 → 阻塞,绝不静默**。 - **B1**:类型化路由;未处理的异常 → BLOCKED(**绝不吞掉**)。 - 只对 RETRYABLE 重试,受 `max_step_retries` 限制。 +- **检查点是崩溃可幸存的**:progress.json 走 tmp 文件 + fsync + `os.replace` + + 目录 fsync;不支持目录 fsync 的平台/文件系统退化为 rename 原子性,而真实存储 + 错误(EIO)**必须传播** —— 撕裂或丢失的 progress.json 会搁浅所有 resume 路径。 - **run 级 task params 会到达每个 step**:`state["task_spec"]["params"]` 被合并在每个 step 自己的 params **之下**,所以 `--task-param limit=5` 不会被静默丢弃。 **playbook 自己的 params 在合并中获胜** —— 其中好几个是安全攸关的 diff --git a/doc/architecture/SPEC/engine/lifecycle.md b/doc/architecture/SPEC/engine/lifecycle.md new file mode 100644 index 00000000..5c525108 --- /dev/null +++ b/doc/architecture/SPEC/engine/lifecycle.md @@ -0,0 +1,60 @@ +# engine/lifecycle.py —— 规范 + + + +`LOC ~121 · 引擎底座(run 生命周期原语) · refactor-status: ok` + +## 职责 +executor 自己给不了的两条**进程级**保证:同一 run 的互斥锁,与 run 离开事件 +循环时的 finalizer 挂钩点。 + +## 功能 +`RunLock` 对 `/.lock` 取排他 advisory `flock`(第二个并发 +`--resume` fail fast,而不是交错写进度、共享 checkout); +`register_finalizer`/`finalize` 维护逐 run 的异步 finalizer 表; +`run_guarded` 包住 executor 协程,在完成/失败/抛异常的**每条**退出路径上、 +同一事件循环内执行 finalize。 + +## 公开契约 +`RunLock(run_dir).acquire()/release()`(context manager);`RunLockHeld`; +`register_finalizer(run_dir, fn)`(`fn(outcome)`,outcome 是 RunOutcome,或 +executor 在产出之前就抛时为 None);`finalize(run_dir, outcome)`; +`run_guarded(run, run_dir) -> outcome`。调用方:`cli/copilot.py` 在 +run/resume 全程持锁并用 `run_guarded` 包 `executor.run`;目前唯一的注册者是 +rebase 流水线(flock 释放、scratch 清理、终局报告、CI abort 清理)。 + +## 不变量 +- `flock` 争用按 open file description 计:同进程内对同一路径的第二次 + `acquire` 也失败 —— 抛 `RunLockHeld`,绝不静默共存。 +- 锁文件释放后**留在原地**:它的存在不携带语义,只有 flock 本身算数。 +- 非 POSIX(无 fcntl,与 `run_status.py` 同款守卫):无 advisory 锁, + `acquire` 直接成功 —— 注释声明的刻意降级(保住 run 可用;无 trace 事件)。 +- finalizer **恰好一次**:`finalize` 先 pop 再跑,第二次调用是 no-op; + 按注册顺序执行。 +- 一个抛异常的 finalizer(**含 `CancelledError`** —— BaseException,不点名 + 就会漏)绝不掩盖 run 自身的 outcome、绝不阻断兄弟 finalizer。 +- `run_guarded` 的 teardown **不可被取消丢弃**:shield + 循环 re-await 直到 + finalize 真正完成,然后才让取消向外传播 —— 光 shield 会让外层先抛、 + loop 关闭时把孤儿 finalizer 拦腰取消。 +- 什么都没注册时整个挂钩点是 no-op(现有非 rebase playbook 行为不变)。 + +## 边界 —— 不属于这里 +不含 teardown 的内容本身(各注册方自带);不做进度/checkpoint 持久化 +(executor);不是 run 状态查询(`run_status.py`);不含 step 逻辑。 + +## 依赖(允许) +仅 stdlib(asyncio/os/pathlib/fcntl-守卫)。零 engine 内 import。 + +## 扩展点 +新的 run 级资源清理 = 调用方去 `register_finalizer`,不改这里;只有新的 +**进程级** run 保证才落在这里。 + +## 测试 +`test_run_lifecycle.py`(同进程/跨进程锁争用、release 后可重取、无 fcntl +降级、finalizer 恰好一次 / 异常与取消隔离 / 失败路径也执行)。 + +## 重构备注 +小而承重。`_finalizers` 是模块级进程状态 —— pop-before-run 就是它的自清 +机制;若将来单进程要并行多 run,把表挂到 run 对象而不是模块全局。 +`run_guarded` 的 shield 循环微妙(取消期间的 teardown 存活是评审加固过的) +—— 改动前先重跑取消路径的测试。 diff --git a/doc/architecture/SPEC/engine/steps/__init__.md b/doc/architecture/SPEC/engine/steps/__init__.md index 3af4e14b..b4594d1a 100644 --- a/doc/architecture/SPEC/engine/steps/__init__.md +++ b/doc/architecture/SPEC/engine/steps/__init__.md @@ -1,14 +1,15 @@ # engine/steps/__init__.py —— 规范 - + -`LOC ~31 · step 库聚合 · refactor-status: ok` +`LOC ~34 · step 库聚合 · refactor-status: ok` ## 职责 为了**注册副作用**而 import 每个 step 模块,并暴露 `register_builtin_steps`。 ## 功能 -import `_common` + 全部 8 个领域模块(从而运行它们的 `@step`/`register_step` 装饰器, +import `_common` + 全部 8 个领域模块(rebase 侧现为 `rebase_v3` + `rebase_knowledge`; +从而运行它们的 `@step`/`register_step` 装饰器, 填充 `_common._COLLECTED`);`register_builtin_steps` 把这份集合冲刷进一个 `StepRegistry`。 @@ -30,7 +31,7 @@ import `_common` + 全部 8 个领域模块(从而运行它们的 `@step`/`reg 新的 step 领域模块 → 加进这份副作用 import 列表。 ## 测试 -每次 `register_builtin_steps(StepRegistry())` 调用都会覆盖(冒烟:38 个 step)。 +每次 `register_builtin_steps(StepRegistry())` 调用都会覆盖(冒烟:45 个 step)。 ## 重构备注 可以考虑自动发现(遍历这个包)以去掉手工 import 列表 —— 但显式 import **可 grep**, diff --git a/doc/architecture/SPEC/engine/steps/_common.md b/doc/architecture/SPEC/engine/steps/_common.md index 3b55073c..5732cac7 100644 --- a/doc/architecture/SPEC/engine/steps/_common.md +++ b/doc/architecture/SPEC/engine/steps/_common.md @@ -1,15 +1,16 @@ # engine/steps/_common.py —— 规范 - + -`LOC ~141 · step 库基础设施 · refactor-status: ok` +`LOC ~264 · step 库基础设施 · refactor-status: ok` ## 职责 step 的自注册面,以及各 step 文件共享的跨模块 helper。 ## 功能 `@step`/`register_step` 装饰器 + `_COLLECTED` + `collected()`; -helper:`repo_path`、`task_spec`、`gh`、`git`、`gh_read_tools`、`post_step`。 +helper:`repo_path`、`require_repo`、`task_spec`、`from_state`、`published`、 +`no_llm_gap`、`gh`、`git`、`gh_read_tools`、`post_step`、`record_debug_memory`。 ## 公开契约 `step(name, kind, risk, description)`;`register_step(StepSpec)`;`collected()`; @@ -42,12 +43,11 @@ helper:`repo_path`、`task_spec`、`gh`、`git`、`gh_read_tools`、`post_step 免得将来一次改名**静默地**弄坏 patch。 ## 精简 —— **K3/K4/K7** helper 的归处 -step 样板的收敛就落在这个文件。要加入: -- `require_repo(ctx) -> Path | StepResult`(K3 —— 8 处仓库守卫)。 -- `adapter_or_result(ctx)` / `@needs_adapter`(K3 —— 7 处 profile 守卫)、 - `no_llm_gap(ctx, step, effect)`(K3 —— 4 处)、`store_for(adapter)`(K3 —— 6 处)。 -- `published(summary, *, state=None, **outputs)`(K4 —— 21 处 `state_updates` - 字面量收敛为一次调用)。 -- `from_state(ctx, key)`(K7 —— 5 处 fetch 早返回)。 -每一个都必须**保住它所包裹的那条保证**(B1 类型化返回、B2 交接、E2 `capability_gap` -事件)。**只在 ≥2 个真实现场时才抽取**(上述都满足)。 +step 样板的收敛就落在这个文件。`require_repo`(K3)、`no_llm_gap`(K3)、 +`published`(K4)、`from_state`(K7)**已落地**;`record_debug_memory` 经 +`KnowledgePaths.resolve(...).shared_write_db` 落库(**D1**/**D4**),失败被吞掉 +——学习回路绝不搞坏修复本身。仍未抽取的: +- `adapter_or_result(ctx)` / `@needs_adapter`(K3 —— profile 守卫)、 + `store_for(adapter)`(K3)。 +已落地与待抽取的每一个都必须**保住它所包裹的那条保证**(B1 类型化返回、B2 交接、 +E2 `capability_gap` 事件)。**只在 ≥2 个真实现场时才抽取**。 diff --git a/doc/architecture/SPEC/engine/steps/rebase_ext.md b/doc/architecture/SPEC/engine/steps/rebase_ext.md deleted file mode 100644 index ca51f280..00000000 --- a/doc/architecture/SPEC/engine/steps/rebase_ext.md +++ /dev/null @@ -1,31 +0,0 @@ -# engine/steps/rebase_ext.py —— 规范 - - - -`LOC ~101 · step 库(委托) · refactor-status: ok` - -## 职责 -`rebase.run_external` —— 向 locked 的 5 阶段编排器做**受监控的子进程委托** -(**包装,而非重写**)。 - -## Steps -`rebase.run_external`(script/write_workspace)。 - -## 不变量 -- **零回归**:它**不重新实现**那条流水线。 -- 把父进程的 `state.json` 流式送进 RunTrace;**陈旧状态守卫**防止上一次 run 的 - `phase=done` 掩盖本次崩溃;失败被分类成升级材料。 -- 点名父包(被允许的仓库字面量,泄漏上限为 1)。 - -## 边界 —— 不属于这里 -自身不含 rebase 逻辑;除委托给 `rebase/monitor` 外不做解析。 - -## 依赖(允许) -`rebase/monitor`、`engine/step`、`._common`;stdlib 的 asyncio/subprocess。 - -## 测试 -`test_rebase_monitor.py`(它驱动的那个 monitor)。 - -## 重构备注 -可以接受。那一个仓库字面量(`"vllm-omni-rebase-agent"`)是**设计使然**的委托文本 —— -**不要过早模板化**;如果将来真的包装了第二个外部编排器,再把名字抽到 adapter。 diff --git a/doc/architecture/SPEC/engine/steps/rebase_knowledge.md b/doc/architecture/SPEC/engine/steps/rebase_knowledge.md new file mode 100644 index 00000000..3322140c --- /dev/null +++ b/doc/architecture/SPEC/engine/steps/rebase_knowledge.md @@ -0,0 +1,61 @@ +# engine/steps/rebase_knowledge.py —— 规范 + + + +`LOC ~354 · step 库(v3 知识尾段) · refactor-status: ok` + +## 职责 +v3 playbook 的 Rev 8 §2.2 流水线尾段(`phase5_report → curate → compare` → +通用 report),外加首个 agent 写入之前的 schema 准备步。 + +## Steps(4 个) +| step | kind/risk | 发布 / 产物 | +|---|---|---| +| `rebase.v3_knowledge_prep` | deterministic/knowledge | `state_updates: knowledge_prepped`;debug store 升到 schema v2 | +| `rebase.v3_phase5_report` | deterministic/read | `FINAL_SUMMARY.md`(模块/本地测试/远端 CI,全读自 substate) | +| `rebase.v3_curate` | script/knowledge | debug-memory curation + watchdog 收割/晋升;`PROMOTION.md`;substate `curation` | +| `rebase.v3_compare` | report/read | 关账知识 attest + `COMPARISON.md`;substate `knowledge.close/drift`、`comparison` | + +## 不变量 +- **A4**:四个 step 全部 `@step(...)` 就地自注册;**B2**:`knowledge_prep` + 经 `state_updates` 发布 `knowledge_prepped`。 +- `ensure_schema_v2()` 只有**三个**受认可的可写维护入口:知识迁移 CLI、 + `v3_knowledge_prep`、`v3_curate`(belt)—— 入口普查由测试钉死; + report_only 到不了 prep(它的 stores 全程只读)。 +- `phase5_report` 在 curate **之前**运行、只读 substate —— curation 永远 + 没机会掩盖 run 实际做了什么。 +- curate 只写 copilot **runtime** store —— 父 read-compat 层与 adapter 树 + 绝不被写(**D2**);watchdog 决策收割**恰好一次**(state log 的身份集是 + 去重权威;`_repo_run_dirs` 按 task.json 定界到本仓库的 run dirs,跨仓库 + 扫描会互相污染);skill 候选与晋升摘要落 `PROMOTION.md` 人类策展面 + (**D1**)。curation 失败是**类型化的 run 失败**(**B1** —— 知识库损坏 + 不得藏在绿色报告后面)。 +- compare 的关账 attest:父层 close digest 必须与 prelude 的开账块一致, + 否则 `drift` 记入 substate 并记 `knowledge_provenance` trace(**E1**), + §8 比较 gate-ineligible —— v3 从不写父层,漂移 = 外部干扰。 +- 声明了却展不开的知识层(env var 未设)→ BLOCKED(**B1**)—— 与 prelude + 同一条"绝不知识裸奔"规则(`_knowledge_layer_paths`)。 +- 提供的 baseline(task param `baseline_status`)是**裁决输入**:不可读 → + 类型化失败;baseline 绿而本 run 非 done(failed/skipped/missing 同罪)→ + BLOCKED needs-human(soak 的 investigate-don't-average 契约)。 + +## 边界 —— 不属于这里 +不含 curation 算法(`memory/curator`)、attest 实现 +(`rebase_engine/knowledge_attest`)、watchdog 学习本体 +(`testing/watchdog_learn`);不写最终通用报告(`report.final_summary`)。 + +## 依赖(允许) +`engine/step`、`._common`;惰性的 `memory/{curator,debug_memory,skills,paths}`、 +`testing/watchdog_learn`、`rebase_engine/knowledge_attest`、`adapters/base`; +以及兄弟模块 `.rebase_v3` 的三个 helper(见重构备注)。 + +## 测试 +`test_curator.py`(curation 分歧钉扎、恰好一次收割、schema 入口普查、 +phase5/compare 的 drift 与 baseline 裁决)。 + +## 重构备注 +从 `rebase_v3.py` 拆出的尾段,内聚良好 —— 但它 import 兄弟 step 模块 +`rebase_v3` 的三个 helper,是 **A2 的字面违例**;把它们下沉 `._common` +(或一个 v3 共享 helper 模块)即可让两边都干净。`_knowledge_layer_paths` +与 prelude 的展开检查是刻意的同规则双份 —— 合并时必须保留"声明未展开 = +BLOCKED"。 diff --git a/doc/architecture/SPEC/engine/steps/rebase_native.md b/doc/architecture/SPEC/engine/steps/rebase_native.md deleted file mode 100644 index 2f89676e..00000000 --- a/doc/architecture/SPEC/engine/steps/rebase_native.md +++ /dev/null @@ -1,37 +0,0 @@ -# engine/steps/rebase_native.py —— 规范 - - - -`LOC ~397 · step 库(候选的原生 rebase) · refactor-status: ok` - -## 职责 -夜跑 rebase 的**候选**原生分解版本,import 父包自己的阶段 wrapper 与 -`node_rebase_module`。 - -## Steps(9 个) -`rebase.prelude`、`rebase.phase1..phase5`、`rebase.phase2_prepare`、 -`rebase.module_rebase`、`rebase.phase2_finalize`、`rebase.compare_with_locked`。 - -## 公开契约(可从 `engine.steps.rebase_native` import) -`_RUNTIME`(按进程记忆化的父运行时;测试 fixture 会清掉它)。 - -## 不变量 -- **对 planner 不可见**(只存在于 candidate playbook `repo-rebase-native`)。 -- phase-4 的 push 在 copilot 的推送守卫之后;env 导出被记 trace(`env_exported`)。 -- **委托给父包的函数** —— 不重新实现各阶段。 -- 点名父包(被允许的仓库字面量,泄漏上限为 6)。 - -## 边界 —— 不属于这里 -不含晋升逻辑;不重新实现 rebase。**只是 wrapper。** - -## 依赖(允许) -`rebase/monitor`、`adapters/base`(wave 交叉核对)、`engine/step`、`._common`; -以及外部的 `agent.*` 包(惰性 import,ImportError → BLOCKED)。 - -## 测试 -`test_rebase_native.py`。 - -## 重构备注 -**按设计**与父包耦合 —— 那 6 个仓库字面量和那些父包 import 是**委托面,不是坏味道**。 -它用命令式方式注册(工厂/直接 handler 混用)—— 这是 `@step` 被认可的例外。 -**只有在推进晋升路径(candidate → active → locked)时才动它。** diff --git a/doc/architecture/SPEC/engine/steps/rebase_v3.md b/doc/architecture/SPEC/engine/steps/rebase_v3.md new file mode 100644 index 00000000..e764ac87 --- /dev/null +++ b/doc/architecture/SPEC/engine/steps/rebase_v3.md @@ -0,0 +1,91 @@ +# engine/steps/rebase_v3.py —— 规范 + + + +`LOC ~2204 · step 库(v3 rebase 装配层) · refactor-status: oversized` + +## 职责 +把 `rebase_engine` 包接进 executor:locked playbook `repo-rebase-v3` 的全部 +step —— 薄的受治理 wrapper,substate-first、类型化失败、发布被消费的键。 + +## Steps(12 个) +| step | kind/risk | 发布(`state_updates`)/ 裁决 | +|---|---|---| +| `rebase.v3_prelude` | deterministic/read | `mode_*` 标志、`run_id`、`upstream_origin_path`、`last_rebase_upstream_commit`、(remote_ci) `upstream_commit`、(full) `upstream_path`;注册终局报告 finalizer;知识开账 attest | +| `rebase.v3_guard` | deterministic/write_workspace | 无发布 —— 幂等重取 checkout flock(首取在 prelude),再注入 adapter `rebase.guard` 策略委托 `workspace._guard_clean_rebase` | +| `rebase.v3_scan` | deterministic/read | `manifest_jobs`;`test_manifest.json` 产物 | +| `rebase.v3_wheel` | deterministic/write_workspace | `upstream_commit`(选 commit → 装进目标 venv → **最后**才 pin Dockerfile:装失败绝不留脏树) | +| `rebase.v3_assign` | deterministic/read | `active_modules`、`wave1_modules`、`wave2_modules`(path-sync 后按 wave 分派) | +| `rebase.v3_wave_gate` | deterministic/read | wave-1 失败 ⇒ `wave2_modules=[]`;`halt_on_module_failure` ⇒ ESCALATE | +| `rebase.v3_module_rebase` | script/write_workspace | `module__status`(foreach;同 run 串行锁 —— 同一 checkout;substate done/skipped 短路防重入二次执行) | +| `rebase.v3_test_loop` | script/write_workspace | `phase3_failed`;substate `tests.pipeline`/`infra_failures`;空 manifest ⇒ `manifest_empty` | +| `rebase.v3_precommit` | script/write_workspace | 无发布 —— substate `tests.precommit`(passed/failed/failed_preexisting/not_declared) | +| `rebase.v3_push_gate` | deterministic/read | `push_gate_flagged`、`push_gate_overrides`;不许 ⇒ FORBIDDEN;override 记 trace | +| `rebase.v3_ci` | script/push | `ci_result`、`ci_build_urls`;substate `ci`(rounds/adopted/unfixed) | +| `rebase.v3_finalize` | deterministic/read | 无发布 —— substate `phase=done|needs_human`;有失败 ⇒ BLOCKED(复用 exit 3) | + +## 公开契约(注册的 step 之外) +`_adapter_manifest/_substate/_task_params`(被 `rebase_knowledge` import)、 +`manifest_job_to_test_job`(golden 测试)、`_build_backends`(read-compat +测试)、`_make_ci_client`(模块级工厂,测试注入 fake 客户端)。 + +## 不变量 +- **A4**:12 个 step 全部 `@step(...)` 就地自注册;`when:` 门**只**读 + prelude 发布的 `mode_*` 标志(**B3** 已知键,复合标志预算于 + `rebase_engine/modes`)。**B2**:被消费的键都经 `state_updates` 发布 + (上表),substate 同时落盘 —— 两份都写。 +- **substate 裁决**:模块/测试/CI 失败是 substate **数据**,携带它们的 step + 返回 ok —— 终局裁决属于 `v3_finalize`(全 ok + substate 有失败 ⇒ BLOCKED + needs-human),且它在 report 之后运行,RUN_REPORT 必已存在(**B1**)。 +- **B1**:结构性拒绝全程类型化 —— mode 缺失/未知、adapter 非 active、声明的 + venv 展不开、无 CI token/provider/签名身份 → BLOCKED;绝不猜权限。 +- **C4**(`v3_ci`):只许推 adapter 声明的 `push.rebase_branch`(且 + `rebase_branch_allowed`,绝非受保护分支);`ALLOW_PUSH` 未设 ⇒ + push_dry_run ⇒ **FORBIDDEN**;推送走 `push_to_ci` WAL,op id 对 CI 台账与 + push WAL **双清点**后起编(resume 绝不复用已指名的 op id)。 +- 预推送采纳豁免必须**被证明**:remote ref 已等于本地 HEAD 才把 head 传给 + `ci_loop.run_ci_rounds`(同头 schedule 构建可采纳);ls-remote 失败或 + 不一致即禁用豁免(安全默认)。 +- **checkout flock 先于任何 mutator**:prelude 取锁,每个 mutating step 经 + `_ensure_checkout_locks` 幂等重取(resume 重放 prelude 不执行它);释放是 + lifecycle finalizer,每条退出路径都放。可变操作只碰 run_dir 内的 + `git clone --shared` **scratch** upstream;canonical 路径绝不被采纳为可变 + 树;resume 采纳幸存 scratch 时重新注册 teardown。 +- **C3**(`_module_scope`):repo 树 + run dir 是硬可写墙;模块 + `local_paths` + `plans/` 是 primary;未知模块得到永不匹配的 primary —— + 它的每次仓库写都被记录 out-of-scope,而非静默在圈内。 +- **E1/E2**:能力缺失记 `capability_gap` 并声明式降级(无 tier key/CI + token/客户端、debug backend 缺失 ⇒ 回归记为结构性失败),绝不静默跳过。 +- debug 尝试**快照护栏**:snapshot → agent → 内容 digest 变更检查(staged+ + unstaged+untracked 字节+mode+symlink)→ 复跑/本地验证;被拒或验证失败的 + 尝试**回滚** —— 只有绿色复跑算修好。 +- 空/损坏 manifest ⇒ `manifest_empty`(push gate 阻塞,绝不空洞通过); + 不可运行的命令归 STRUCTURAL —— 绝不借 bash rc=0 假通过。 +- **A5**:一切仓库知识来自 adapter manifest;parity 词汇泄漏上限 14, + 只能变小(`test_repo_vocabulary.py`)。 + +## 边界 —— 不属于这里 +不含 rebase 机制本体(`rebase_engine/{module_rebase,test_loop,ci_loop, +push_to_ci,wheel,push_gate,assign,…}`);不含 provider HTTP(`ci/buildkite`); +知识尾段在 `rebase_knowledge`;finalizer 机制在 `engine/lifecycle`。 + +## 依赖(允许) +`rebase_engine/*`、`engine/{step,lifecycle}`、`._common`、`scopes`、 +`ci/buildkite`(经 `_make_ci_client`)、`adapters/base`、`memory/*`、 +`testing/*`、`config`、anthropic SDK;另有对 `.workspace` 的 guard 委托 +import(见重构备注)。 + +## 测试 +`test_assembly.py`(mode 治理、push-gate、report_only e2e、终局行、同头采纳)、 +`test_v3_complete_e2e.py`(local_ci/full/remote_ci 全程)、`test_ci_wiring.py` +(`v3_ci` 接线)、`test_shell_golden.py`、`test_knowledge_readcompat.py`、 +`test_repo_vocabulary.py`(泄漏上限)。 + +## 重构备注 +2204 行 —— 全库最大的 step 文件;知识尾段已拆出(`rebase_knowledge`),下一 +刀的自然缝:① env/backends 构建(~83–465 行);② 锁与 scratch 生命周期 +(~496–635 行);③ `v3_ci` 及其 helper(~1753–2204 行)。被 +`rebase_knowledge` import 的三个 helper 应下沉 `._common`,一并消掉那条 A2 +违例;对 `.workspace._guard_clean_rebase` 的直接 import 是另一条。拆分时 +substate + `state_updates` 双写、模块短路/串行锁的 crash-window 契约**必须** +原样保留(resume 完整性测试护住)。 diff --git a/doc/architecture/SPEC/mcp_server.md b/doc/architecture/SPEC/mcp_server.md index 53177c40..e5f9d8c8 100644 --- a/doc/architecture/SPEC/mcp_server.md +++ b/doc/architecture/SPEC/mcp_server.md @@ -1,8 +1,8 @@ # mcp_server.py —— 规范 - + -`LOC ~392 · Strict 后台机器(start/poll) · refactor-status: ok` +`LOC ~421 · Strict 后台机器(start/poll) · refactor-status: ok` ## 职责 为 MCP 宿主运行 Strict 工作流:预约、拉起、跟踪、供给结果 —— @@ -26,7 +26,9 @@ (`python -m infermatrix_copilot --execute-reserved `)。两个理由都很关键: copilot 的 stdout 必须离开本进程的 JSON-RPC stdio 通道(子进程 stdout → `/console.log`),且进程级全局 tracer / `last_run_dir` **由此天然按 run 隔离**。 -- **run 由单个 worker 线程串行执行。** +- **run 由单个 worker 线程串行执行。** worker 等待子进程后把退出码交给对账: + 退出码 3 且无终态即 lock-loser 签名(真正 BLOCKED 的子进程会先写终态再退出), + 以 `suspect_lock_loser` 传入 `reconcile_after_wait`。 - 跨重启、跨多个并发 server 的轮询正确性,来自 `run_status.py` 的持久记录 + 按属主 对账,**而不是内存状态**。 - **`mcp` SDK 是可选 import**,藏在 `[mcp]` extra 之后,且**绝不能**被核心包 import diff --git a/doc/architecture/SPEC/memory/curator.md b/doc/architecture/SPEC/memory/curator.md new file mode 100644 index 00000000..1ea80b2e --- /dev/null +++ b/doc/architecture/SPEC/memory/curator.md @@ -0,0 +1,56 @@ +# memory/curator.py —— 规范 + + + +`LOC ~372 · 记忆(debug memory 的有节奏巩固层) · refactor-status: ok` + +## 职责 +对 debug-memory 库的**一次策展**(parent `agent/curator.py` 的仓库中立移植): +合并近重复、按上游 commit 距离标 stale、休眠降级、模式提取成 skill 候选。 + +## 功能 +`curate()` 依次跑四个规则式 pass(逐模块贪心单链聚类,Jaccard over +key+symptom+root_cause token),把全部变更凑成一个批次交 +`dm.apply_curation()`,返回 `CuratorReport`。 + +## 公开契约 +`DebugMemoryCurator(dm, *, repo, sim_threshold, propose_to, survivor_key, …)` +带唯一入口 `curate(recent_runs) -> CuratorReport`;`CuratorReport(.to_dict)`; +`SkillCandidate`。 + +## 不变量(**D1/D4**) +- **D4** —— 本类**就是**那个"有节奏的巩固层":唯一被允许 merge/rewrite/ + mark-stale 的地方,且纯规则、**无 LLM**(连续 LLM 重写被 D4 明令禁止)。 + 构造时强制 v2 schema,否则 `RuntimeError` —— 升级只走 sanctioned 入口。 +- **退休、绝不删除**:被合并行变为 `status='retired'` + + `derived_from=<幸存者 id>`;retired 行被排除出所有策展输入和检索, + 所以对已策展库再跑一遍是**严格 no-op**。 +- **仓库限定**:每次读取都过 `entries(repo=…)` —— 外仓行原封不动。 +- **D1** —— 模式提取只经 `propose_to.propose_if_new_identity` 写 + **candidate**,绝不自动生成生效 skill;身份是 `module+key`(非散文), + check-allocate-write 是 store 候选 flock 内的一个临界区。 + 已被现有 skill 覆盖(Jaccard 或 0.45 overlap)的簇不再提议。 +- 休眠只在**提供了 run 窗口时**判定;迁移来的行 + (source ∈ parent-db/copilot-global/adapter-tree)豁免休眠钟 + (其 run id 属外来 id 空间 —— F12),commit 距离 staleness 仍适用。 +- stale 只在 `merge-base --is-ancestor` 成立且距离超阈时标记; + diverged/未知 → **绝不标**(parent parity)。 +- `survivor_key` 默认最新 id 胜出;迁移传入来源优先序,新导入的低优先级行 + **绝不**退休更高优先级的运行时知识(F11)。 + +## 边界 —— 不属于这里 +不是逐 run 写入路径(`debug_memory.record`,追加式);不是 skill 晋升 +(`SkillStore.promote`,人类动作);不是迁移编排 +(`rebase_engine/knowledge_migrate.py` 调用它);不做 schema 升级。 + +## 依赖(允许) +`.debug_memory`、`.skills`;stdlib(`re`、`subprocess` 只为 git 距离)。 + +## 测试 +`test_curator.py`(退休带血统、二次策展 no-op、外仓不动、非可行动过滤、 +schema-v2 门、slug 冲突共存等 16 个);迁移侧策展在 `test_knowledge_migrate.py`。 + +## 重构备注 +`_extract_patterns` 一个方法背着 聚类+覆盖检查+提议 三件事,是最先该拆的; +`_non_actionable` 的 pattern/tag 表是 parent-verbatim 数据 —— 若继续膨胀, +考虑挪进 adapter 数据而非在此续表。 diff --git a/doc/architecture/SPEC/memory/debug_memory.md b/doc/architecture/SPEC/memory/debug_memory.md index 4ef781f0..d7beee9d 100644 --- a/doc/architecture/SPEC/memory/debug_memory.md +++ b/doc/architecture/SPEC/memory/debug_memory.md @@ -1,20 +1,31 @@ # memory/debug_memory.py —— 规范 - + -`LOC ~106 · 记忆(失败→修复库) · refactor-status: ok` +`LOC ~465 · 记忆(失败→修复库,schema v2) · refactor-status: ok` ## 职责 以 FTS5 存放"失败 → 修复"经验。 ## 公开契约 -`DebugMemory(db_path)`,带 `search(query, k)` 和一个要求必填字段的写入方法。 +`DebugMemory(db_path)` / `DebugMemory.open_readonly(db_path)`,带 `record`(必填 +字段强制)、`search(query, k, repo?)`、`get`、`count`、`entries(repo?, ...)`、 +`apply_curation(updates)`、`schema_v2` / `ensure_schema_v2()`;模块级 helper: +`readonly_uri`、`strip_sql_comments`、`is_fts5_table`、`fts5_unindexed_columns` +(供 attest/迁移侧做 FTS5 完整性检查)。 ## 不变量(**D1/D3**) - 一次写入必须包含 repo/module/run_id/symptom/root_cause/fix_summary/files/verification/status。 - 检索是按相关度取 top-k,**摘要优先**。 - 事实可自由记录;晋升为 skill 是**另一个**受门禁的动作。 +- **schema v2 升级只走显式维护入口**(`ensure_schema_v2()`):打开既有 DB 绝不 + 作为副作用改 schema —— report-only 路径必须能证明零写入(**D4**)。 +- `open_readonly` 经 `readonly_uri`(路径百分号转义的 `file:...?mode=ro`)打开; + 只读句柄上的写入(含 `apply_curation`)**抛错**。 +- `apply_curation` 单事务写策展字段(status/tags/watch_outs/derived_from 等 + additive 列),随后**一次性重建** FTS 镜像;status 域为 + candidate/active/stale/retired。 ## 边界 —— 不属于这里 不做按仓库的命名空间隔离(由 agent 运行时的 `_ScopedKnowledge` 施加);不含 LLM。 @@ -23,8 +34,10 @@ stdlib 的 `sqlite3`。 ## 测试 -`test_memory.py`。 +`test_memory.py`、`test_curator.py`、`test_knowledge_migrate.py`、 +`test_knowledge_readcompat.py`。 ## 重构备注 写入契约(必填字段)就是 D3 保证 —— **在写入时强制它,绝不接受残缺的记忆**。 -按仓库的 DB 路径由调用方选择(`adapter.debug_memory_db`)—— 本类保持路径无关。 +按仓库的 DB 路径由调用方选择 —— 运行时经 `KnowledgePaths.resolve(...)` +(PR4d cutover 之后是 `shared_write_db`),本类保持路径无关。 diff --git a/doc/architecture/SPEC/memory/paths.md b/doc/architecture/SPEC/memory/paths.md new file mode 100644 index 00000000..ba52b9d9 --- /dev/null +++ b/doc/architecture/SPEC/memory/paths.md @@ -0,0 +1,60 @@ +# memory/paths.py —— 规范 + + + +`LOC ~262 · 记忆(可变知识位置的唯一解析器 + run/迁移锁) · refactor-status: ok` + +## 职责 +**唯一**解析所有可变知识位置的地方:把五处散落的路径表达式收敛成一个 +frozen dataclass,给 PR4d 的 runtime-dir 切换留一个开关。 + +## 功能 +`KnowledgePaths.resolve(settings, repo, adapter_root)` 产出该仓库的全部知识 +位置(debug DB 三成员、skill seed/runtime、watchdog overlay/decisions/ +checkpoint、backups、两把锁)。切换未激活 → 逐消费者的历史位置;激活(repo ∈ +`settings.knowledge_runtime_repos`)→ 收敛到 `/state//`,但先验证迁移完成 marker。 +`KnowledgeRunLock` 是 `state//locks/knowledge.lock` 上的共享/独占 flock。 + +## 公开契约 +`KnowledgePaths`(字段见类定义)+ `resolve()` + `MIGRATION_MARKER`; +`KnowledgeStateError`;`KnowledgeLockHeld`; +`KnowledgeRunLock(.acquire_shared/.acquire_exclusive/.release)`。 + +## 不变量(**D4/E2**) +- **切换未激活 = 字节恒等**:未列名仓库解析出的每个位置都与本模块存在前 + 各消费者用的完全一致(含每个 no-adapter 回退)—— 纯重构,由测试钉死。 + debug 三成员(`rebase_backend_db`/`shared_write_db`/`debug_read_layers`) + 刻意镜像历史的**逐消费者**接线,切换前必须继续互不相同。 +- **E2(fail-closed)** —— 激活了却没有有效 marker 的仓库 → + `KnowledgeStateError`,**绝不**以空库静默起跑。marker 验证是证据式的: + repo 必须匹配(抄来的 marker 永不激活)、schema 声明 v2 之外**库本身** + 必须带 v2 列 + 全索引 FTS5 镜像 + 一次 MATCH 可用性探针(假冒的普通表 + 列名全对却 MATCH 全失败 —— F8/F9、round-2 F5、round-3 F4)。 + digest **刻意不**要求与活库相等 —— 激活后追加写入是设计内的。 +- 锁协议:每次 run 全程持**共享**(互不争用);迁移取**独占** —— + 迁移不能在任何潜在写者存活时开始,run 也不能在迁移中途开始 + (关掉 lock-census TOCTOU)。两侧都非阻塞、失败即 `KnowledgeLockHeld`; + 无 fcntl 的平台上**两侧一起**降级为不强制。 +- **D4** —— watchdog decisions/checkpoint/backups 等巩固层位置由此统一 + 发放,消费者不得自拼 state 路径。 + +## 边界 —— 不属于这里 +不做迁移本身(`rebase_engine/knowledge_migrate.py`);除 marker 验证探针外 +不做任何库 I/O;除锁文件外不创建/写任何文件;激活开关的判定数据 +(`knowledge_runtime_repos`)属于 `config`。 + +## 依赖(允许) +stdlib(`os`/`dataclasses`/`pathlib`,`fcntl` 带 POSIX 守卫);marker 验证时 +惰性 import `.debug_memory`。**绝不** import `engine/`。 + +## 测试 +`test_knowledge_foundation.py`(`test_paths_byte_identity_with/without_adapter`、 +`test_knowledge_lock_shared_and_exclusive`); +`test_knowledge_migrate.py`(marker/FTS 假冒的十余个 activation-refuses 用例、 +`test_flag_on_requires_marker_and_is_repo_scoped`、 +`test_activation_error_exits_blocked_and_releases_run_lock`)。 + +## 重构备注 +`_require_migration_marker` 是浓缩的 FTS 取证(每个分支对应一条评审发现)—— +保持"marker 的声明不是证据"这个立场,别为省事削弱任何一个探针。 +PR4d 全量切换落地后,逐消费者的三个 debug 成员应收敛并删掉分叉注释。 diff --git a/doc/architecture/SPEC/memory/skills.md b/doc/architecture/SPEC/memory/skills.md index ac7f4b36..09c4c93b 100644 --- a/doc/architecture/SPEC/memory/skills.md +++ b/doc/architecture/SPEC/memory/skills.md @@ -1,21 +1,30 @@ # memory/skills.py —— 规范 - + -`LOC ~120 · 记忆(程序性知识) · refactor-status: ok` +`LOC ~384 · 记忆(程序性知识) · refactor-status: ok` ## 职责 程序性知识,门禁比 debug memory 更严。 ## 公开契约 -`SkillStore(dir)`,带 `find`、`propose`、`promote`、`candidates`、`load_all`、 -`render_for_prompt`;以及 `Skill`(带用于召回的 `trigger`)。 +`SkillStore(dir)`,带 `find(query, module, k, extra_run_counts?)`、`propose`、 +`propose_if_new_identity`(身份去重的提案入口,curator 用)、`promote`、`touch`、 +`candidates`、`load_all`、`render_for_prompt`;`Skill`(带用于召回的 `trigger`); +模块级 usage-journal 原语 `read_usage_counts` / `append_usage`(seed skill 的 +使用计数走 journal,seed 文件在运行时保持逐字节不变——消费方是 +`_ScopedKnowledge`)。 ## 不变量(**D1**) - agent **只能 `propose`**(写入 candidates 文件);把它 `promote` 成一个生效的 `SKILL.md` 是**策展人/人类**的动作。 - `find` 按 模块命中 + 文本命中 + run_count 排序;只有 `status: active` 的 skill - 才会被加载。 + 才会被加载;`extra_run_counts` 把 journal 计数叠加在冻结的 seed frontmatter + 计数之上(使用先验,round-2 F8)。 +- **候选写入是崩溃可幸存的**:`_write_durable`(tmp + fsync + replace + 目录 + fsync),candidates 文件在 store flock 下互斥更新。 +- `touch` 只在本 store 拥有该 skill 时返回 True —— 调用方据此把 seed 使用 + 路由到 journal。 ## 边界 —— 不属于这里 不做按仓库的命名空间隔离(由 `_ScopedKnowledge` 施加);不含 LLM;不是策展 UI。 diff --git a/doc/architecture/SPEC/playbooks/PLAYBOOKS.md b/doc/architecture/SPEC/playbooks/PLAYBOOKS.md index db256135..1cbe871b 100644 --- a/doc/architecture/SPEC/playbooks/PLAYBOOKS.md +++ b/doc/architecture/SPEC/playbooks/PLAYBOOKS.md @@ -1,6 +1,6 @@ # playbooks/*.yaml —— 规范 - + `8 个文件 · 声明式编排数据 · refactor-status: ok` diff --git a/doc/architecture/SPEC/rebase/monitor.md b/doc/architecture/SPEC/rebase/monitor.md deleted file mode 100644 index 0817c559..00000000 --- a/doc/architecture/SPEC/rebase/monitor.md +++ /dev/null @@ -1,32 +0,0 @@ -# rebase/monitor.py —— 规范 - - - -`LOC ~159 · 边缘(外部流水线监控) · refactor-status: ok` - -## 职责 -为 locked 的 `rebase.run_external` 委托,读取并分类父编排器的状态。 - -## 公开契约 -`build_command`、`parse_parent_state`、`summarize_progress`、`diff_progress`、 -`classify_failure`、`build_escalation`。 - -## 不变量 -- 对父进程的 `state.json` **只读**。 -- 把退出码 + 状态分类成**类型化失败** + 升级材料。 -- **感知陈旧状态** —— 上一次 run 的 `phase=done` **绝不能**掩盖这一次 run 的崩溃。 -- 点名父包/路径(被允许的仓库字面量,泄漏上限为 1)。 - -## 边界 —— 不属于这里 -不运行也不重新实现 rebase;不含推送逻辑;不发通知(那是 `notify`)。 - -## 依赖(允许) -仅 stdlib 的 `json`/`subprocess`。 - -## 测试 -`test_rebase_monitor.py`。 - -## 重构备注 -内聚的监控/分类单元。这里的基线签名比较,正是 `ci/normalize` 模块**当初写出来就是为了 -不去继承**的已知弱点 —— 如果将来要收紧这个 monitor 的分类,请**复用 `ci/normalize`**, -而不是再实现一遍字符串比较。 diff --git a/doc/architecture/SPEC/rebase_engine.md b/doc/architecture/SPEC/rebase_engine.md new file mode 100644 index 00000000..70eb5266 --- /dev/null +++ b/doc/architecture/SPEC/rebase_engine.md @@ -0,0 +1,122 @@ +# rebase_engine/ —— 规范 + + + +`LOC ~7500(26 个模块) · repo-rebase-v3 的原生 rebase 引擎 · refactor-status: ok` + +## 职责 +父级 rebase agent 机制的仓库中立移植(PR0..PR7 分片落地):repo-rebase-v3 +的全部**引擎原语**。仓库专属内容一律作为数据来自 `adapters//`; +把原语接成 step 的装配在 `engine/steps/rebase_v3`(+`rebase_knowledge`),不在这里。 + +## 功能 +四个阶段的原语 + 底座(phase-1 分析 → phase-2 模块 agent → phase-3 本地 +测试环 → phase-4 push+CI;substate/runtime/锁/WAL)。逐模块图: + +| 模块 | 独占的那件事 | +|---|---| +| `__init__.py` | 包 docstring(仓库中立宣言);无逻辑 | +| `agent_loop.py` | rebase agent 循环(流式、`.decision.md` 计划闸、150 轮预算,cache-parity) | +| `assign.py` | commit→模块 确定性归类 + 路径漂移检查 + 报告渲染 | +| `ci_loop.py` | CI 构建生命周期:受守卫的创建/恢复、monitor、日志分类器、round 编排 | +| `gitio.py` | git 机械层:暂存纪律、签名提交重试、token 头传输、执行已授权 decision | +| `hooks.py` | `RebaseHooks` —— adapter 可定制的窄行为面,fail-closed 加载 | +| `knowledge_attest.py` | 知识层的 WAL 安全逻辑摘要 + 快照/恢复(只读) | +| `knowledge_migrate.py` | PR4d 一次性知识迁移:全锁在手、逐 store 日志化事务 | +| `modes.py` | rebase 模式治理:唯一权威解析 + 写回、`mode_*` 旗标、闸冲突裁决 | +| `module_pytest.py` | `imx-omni-pytest` 受闸测试包装(GPU 互斥、看门狗、分层超时) | +| `module_rebase.py` | 单模块 rebase 单元:prompt → agent loop → substate 结果 | +| `parent_compat.py` | 父知识库的只读兼容读取层(`mode=ro`,fail-closed 打开) | +| `path_sync.py` | 模块路径图同步 + manifest modules 段重写 + L2 决定应用 | +| `phase1_steps.py` | phase-1 组合(归类 + 路径同步),父级报告文件名不变 | +| `plan_review.py` | L4 计划评审后端(注入的 LLM client,父级形状的结果) | +| `prompt_builder.py` | 模块/调试 prompt 渲染 —— 等输入下与父级字节一致(golden 钉住) | +| `push_gate.py` | 推送闸裁决:结构性 vs 断言失败的确定性分类(Rev 8 §2.3) | +| `push_to_ci.py` | commit+push-to-CI 编排:preflight、WAL 卫生、C4 双闸、单一传输 | +| `push_wal.py` | 推送 WAL:先落盘的 intent、精确 OID 三分对账、回滚数据 | +| `rebase_tools.py` | 父级 20 工具作为 `ToolDef`;未接线后端**可见地**失败 | +| `runctx.py` | `RebaseRuntime` + `CheckoutLock`(flock+卫生盾)+ 按事件循环的注册表 | +| `substate.py` | 可持久、单写者、merge-not-overwrite 的 `state.json`(run_id 戳) | +| `test_loop.py` | 本地测试环:逐测试恢复、baseline 复跑分流回归、类型化 skip | +| `test_manifest.py` | 动态测试清单:CI YAML + 活测试树 + diff 分类 → 模块计划 | +| `testing_env.py` | agent shell 子环境 scrub 接线(仅 agent shell,进程环境不动) | +| `worktree.py` | 工作树卫生:中止残留 in-flight 状态、丢弃未跟踪产物、L2 脏树决定 | + +## 公开契约 +按簇:`run_agent_loop` / `rebase_module(ModuleRunConfig)`;`run_test_loop` +/ `run_ci_rounds`(`CIClient` 协议 + 注入动作);`commit_and_push`→ +`PushOutcome`、`evaluate_push_gate`→`GateDecision`、`PushRecord` + +`record_intent`/`reconcile`/`resolve_pending`;`resolve_effective_mode`/ +`mode_state_flags`;`Substate`、`REGISTRY`、`CheckoutLock`; +`build_manifest`、`build_module_prompt`、`build_rebase_tools`、 +`ensure_wheel_installed`(`WheelSpec`/`PinSpec`)、`load_hooks`。 +runner/LLM/CI client 全部可注入 —— 每个模块都能离线测试。 + +## 不变量 +- **C4** —— `commit_and_push` **从不自我授权**:`allowed` 与 `allow_push` + (ALLOW_PUSH)由调用方传入,`push.guard_push` 裁决;无 `allow_push` 在 + WAL 前就停为 dry-run;执行参数**只**从已授权的 `decision.command` 推导 + (`decision_push_args`),`execute_push` 对未允许的 decision 抛错。 +- **C4** —— WAL 纪律:推送前先落**可持久** intent(tmp+fsync+replace+目录 + fsync,真实存储失败传播);重入先 `resolve_pending`;对账三分 + intended⇒pushed / pre-push⇒retry / 其余⇒escalate,**绝不猜**,且先比 + canonical 远端身份;分支缺失时创建走 absence-pinned lease。token 只走 + `http.extraheader`,URL 进 argv 前去凭证,有 token 时 SSH 改写 HTTPS; + probe/push/WAL 身份共用**同一次**解析的 URL。 +- 模式与闸(Rev 8 §2.1/§2.3):可变模式**只能显式选取**,`report_only=True` + + 可变模式、strict+with-failures 都 BLOCKED(narrowing 胜,绝不猜); + 结构性失败总是阻断(除显式 `push_with_failures`,被记录),断言失败 + FLAGGED 放行。**B3**:`when:` 只用 `mode_state_flags` 的旗标。 +- **B1** —— 失败类型化且 fail-closed:`GitIOError`(坏索引绝不读作干净)、 + `PushWalError`(坏记录绝不静默跳过)、`PushPreflightError`(非 40-hex + 上游 commit / pin 不匹配拒推)、`SubstateError`(异 run substate 拒收)。 +- **C3** —— agent 的每次工具调用都过 `tools.dispatch`(写工具经 + `write_path_arg` 路径 scoping);计划闸通过前 edit/pytest/precommit + 工具**不可见**。 +- **D2** —— hooks 人闸:manifest 显式声明 + adapter active 才加载,且 + manifest `rebase` 段高风险(agent 写被拒)—— agent 无法自装 hooks。 +- **E2** —— 显式降级:未接线后端返回**可见错误**,绝不装作空搜索结果; + `parent_compat` 声明了但坏 ⇒ 构造即抛,开后故障降级为显式 error dict。 +- 检出锁:flock 拿到后必须锁内装好 `/locks/` 卫生盾(根锚定、原子写、 + 外来行保留)—— **盾装不上等于锁没拿到**;注册表按(run_dir, 事件循环 + 弱引用)发放,绝不复用死循环的原语;teardown 有界、绝不外抛。 +- GPU 受闸:`IMX_GPU_MUTEX=1` 时每次调用都持 GPU 锁,否则仅 e2e/examples + 取锁(父级 parity)。 +- wheel:每个 uv 调用 `--python` 显式指向**声明的目标 venv** —— 启动 + shell 的 venv 状态绝不影响装到哪;只有 import 验证失败才重试。 +- CI 创建受守卫:durable op 记录先于 API 调用,恢复按 op id 精确匹配, + **不确定时绝不重复创建**。 +- **A5** —— 全包仓库中立:仓库值经 `WheelSpec`/`PinSpec`/`ManifestSpec`/ + `ModulePromptData`/`tool_schemas.json`/hooks 注入;`test_repo_neutral_core` 钉住。 + +## 边界 —— 不属于这里 +推送授权规则(`push.py`);工具 scope 强制(`tools`/`scopes`);step 注册 +与编排(装配 PR);CI provider 的 HTTP 实现(adapter 接线);测试进程底座 +(`testing/`);知识存储本体(`memory/`)。 + +## 依赖(允许) +`..push`、`..tools`、`..scopes`、`..run_trace`、`..memory.debug_memory` +(migrate 惰性用 `..memory.*`、`..adapters.base`、`..llm`)、 +`..testing.{runner,watchdog,env_plan,process_tree}`;`yaml`;stdlib。 +包内 import 单向。**绝不 import `engine/`**(A2 —— 叶子包)。 + +## 扩展点 +adapter 数据(WheelSpec/PinSpec/ManifestSpec/prompt_data/tool_schemas/ +watchdog 模式);`RebaseHooks` 子类;注入可调用(`CIClient`、`RunFn`、 +test loop 动作、`precommit_fix`、`RebaseBackends`、LLM client)。 +新能力 = 新注入点或新 adapter 数据,不是仓库字面量。 + +## 测试 +`test_push_cluster.py`(gitio/push_wal/push_to_ci/push_gate)、 +`test_phase1_cluster.py`、`test_engine_core.py`(substate/runctx/modes/ +worktree)、`test_assembly.py`(tools/loop/module/prompt/wheel)、 +`test_ci_wiring.py`(ci_loop/test_loop/test_manifest)、 +`test_ext1_checkout_guard.py`(锁+卫生盾)、`test_knowledge_migrate.py`、 +`test_parent_compat.py`、`test_shell_golden.py`、`test_v3_complete_e2e.py`。 + +## 重构备注 +`ci_loop.py`(1156 行)最大:monitor/分类器/round 编排同居,再长就按 +`engine/steps/pr` 先例拆包。`knowledge_migrate.py` 与 `parent_compat.py` +是 PR4d 执行后的退役候补("无永久双 store 世界")。docstring 里的 +choke-point 编号(agent_loop/rebase_tools 写"C5")落后于 `_CONSTRAINTS.md` +目录(工具 choke point = C3)—— 值得统一,改注释不改行为。 diff --git a/doc/architecture/SPEC/testing.md b/doc/architecture/SPEC/testing.md new file mode 100644 index 00000000..e4b52bb2 --- /dev/null +++ b/doc/architecture/SPEC/testing.md @@ -0,0 +1,88 @@ +# testing/ —— 规范 + + + +`LOC ~1760(6 个文件) · 测试执行底座(rebase agent shell 测试层的 Python 移植) · refactor-status: ok` + +## 职责 +仓库中立的测试执行底座:gpu_lock.sh / kill_test_tree.sh / test_watchdog.sh / +test_runner.sh 的 Python 移植。仓库专属数据(watchdog 模式、artifact glob、 +测试 manifest)全部住在 adapter 侧,本包只**接收**它们作为输入。 + +## 功能(一行一模块) +- `env_plan.py` —— 子进程环境构造:inherit-plus-overlay(`build_subprocess_env`) + + agent-shell 的**allowlist 式**凭据洗刷(`scrub_agent_shell_env`)。 +- `gpu_lock.py` —— 跨进程 GPU 互斥(磁盘协议与 shell 字节兼容)+ nvidia-smi + 孤儿清理 + VRAM 空闲等待。 +- `process_tree.py` —— 进程树终结:BFS 收集后代、自身祖先排除、 + TERM → 宽限 → KILL、(pid, starttime) 身份校验防 PID 复用。 +- `runner.py` —— 单测试执行器:GPU 锁、watchdog、分层超时、silent-exit 尸检 + footer、cov-strip 回退、pass marker、artifact 清理、后代快照线程。 +- `watchdog.py` —— 双档日志看门狗:Tier 1 灾难模式即杀;Tier 2 去噪后交 + 廉价 reviewer 裁决(KILL/CONTINUE)。 +- `watchdog_learn.py` —— Tier-2 裁决的记录/收割/晋升:一贯良性的模式 + 自动晋升进噪声 overlay YAML(数据,而非 shell 时代的改代码)。 + +## 公开契约 +- `env_plan`:`build_subprocess_env(...)`、`scrub_agent_shell_env(env, keep_hf_token, extra_safe_prefixes)`, + 以及 `AGENT_SHELL_SAFE_EXACT` / `AGENT_SHELL_SAFE_PREFIXES` / `AGENT_SHELL_CRED_SUFFIXES` 常量。 +- `gpu_lock`:`GpuLock`(context manager)、`GpuLockTimeout`、`visible_devices`、 + `cleanup_orphan_gpu_procs`、`wait_gpu_memory_idle`。 +- `process_tree`:`collect_descendants`、`kill_tree(pids, identity=...)`、`kill_by_pattern`。 +- `runner`:`TestRunner.run(job, env, baseline, dry_run) -> TestOutcome`、 + `TestJob`、`RunPlan`、`TestOutcome`、`strip_cov_flags`、`cleanup_test_artifacts`、 + `log_file_for`/`pass_marker_for`、`TIMEOUT_RC=124`、`PY_TIMEOUT_MARGIN_SEC`。 +- `watchdog`:`WatchdogPatterns(.from_yaml)`、`LogWatchdog(.start/.stop/.check_once/.result)`、`WatchdogResult`。 +- `watchdog_learn`:`record`、`harvest`、`eligible_patterns`、`promote`、 + `read_decisions`、`normalize_pattern`、`repair_tail`。 + +## 不变量 +- **E2** —— 硬件门槛产生**显式的 `skipped` 结果**,绝不是 shell 时代那种 + 虚增通过数的静默 rc=0;skip 前先撕掉旧 pass marker。 +- 超时分层:**primary** 定时器在 `timeout_sec` 杀整个进程组 + 逐 pid 后代; + **safety** 在 `+PY_TIMEOUT_MARGIN_SEC` **严格更晚** SIGKILL。fired 的定时器 + 被 join 后才返回 —— 下一个 job 不会在后代仍占 GPU 时启动。 +- PID 复用安全:快照 map **只增不减**,身份绑定在**发现时**的 (pid, starttime), + 且记录只接受**已验证祖先链**上的 pid;`kill_tree` 丢弃 starttime 不再匹配的 + pid —— 升级 SIGKILL 绝不打中无关进程。 +- GPU 锁绝不 unlink 活主的锁:偷锁在 flock side file 的临界区内**重新判定** + staleness;release 只删除仍属于自己的锁文件。 +- **E2** —— Tier-2 无 reviewer / reviewer 异常 / 无法解析的回复一律 CONTINUE, + **绝不 wedge 一次 run**;Tier 1 不经评审即杀。 +- **E1(精神)** —— 每次 Tier-2 裁决落 decision log(`record_fn`/`result.decisions`), + kill **先于**遥测执行 —— 满盘的日志绝不留下一个坏引擎继续跑。 +- watchdog 的每次扫描以 `start_offset` 限定在**本次尝试自己的字节**内 —— + setup 输出或上次尝试的报错杀不掉一次通过的主运行。 +- **D4** —— 学习只做加法:overlay 只 append `noise`(可静音、**绝不锐化**); + `harvest` 以 state log 自身为去重权威,flock + 撕尾修复 + fsync,exactly-once。 +- 凭据洗刷是 allowlist、fail-closed:未知名字直接丢弃,凭据后缀一票否决, + HF token 仅显式 opt-in 重进;纯函数,**绝不改写自身进程环境**。 +- artifact 清理只在 repo 根的**深度 1**,含路径分隔符/`..`/`**` 的 pattern 被拒。 + +## 边界 —— 不属于这里 +watchdog 模式、artifact glob、manifest(adapter 数据);测试循环/重试编排 +(`rebase_engine/module_pytest.py`);远程执行(随 shell 层退役);不含 LLM +(reviewer 是注入的 callable)。 + +## 依赖(允许) +stdlib(`fcntl` 带 POSIX 守卫);`pyyaml`(patterns/overlay);包内互引 +(runner → gpu_lock/watchdog/process_tree);`watchdog_learn` → +`memory/skills._fsync_dir`(overlay 落盘持久化)。**绝不** import `engine/`。 + +## 扩展点 +新的 watchdog 模式/噪声 → adapter 的 seed YAML 或 learned overlay;额外安全 +env 前缀 → `scrub_agent_shell_env(extra_safe_prefixes=…)`(manifest 数据), +绝不放宽默认表;runner 的协作者(`review_fn`/`record_fn`/`available_gpus`…) +全部可注入。 + +## 测试 +`test_testing_substrate.py`(77 个,覆盖全部六模块)、`test_shell_golden.py` +(TestRunner 对 shell 的 command-echo 金样);`watchdog_learn` 另由 +`test_curator.py`(harvest exactly-once)与 `test_knowledge_foundation.py` +(record 身份字段、撕尾修复)钉住。 + +## 重构备注 +`runner.py`(643L)是包内最重的一块:`_spawn` 把快照线程、双定时器、watchdog +接线挤在一个方法里,密度高但每行都背着一条已记录的竞态教训 —— 拆分前先读 +注释里的事故史。身份校验逻辑分居 `_record_walk`(记录侧)与 `kill_tree` +(消费侧),契约靠注释维系;若再演化,考虑一个共享的 identity 小模块。 diff --git a/doc/architecture/SPEC/tools.md b/doc/architecture/SPEC/tools.md index 0df0de24..6910732f 100644 --- a/doc/architecture/SPEC/tools.md +++ b/doc/architecture/SPEC/tools.md @@ -1,8 +1,8 @@ # tools.py —— 规范 - + -`LOC ~228 · 引擎(能力 + choke point) · refactor-status: ok` +`LOC ~421 · 引擎(能力 + choke point) · refactor-status: ok` ## 职责 原子能力,以及**唯一那个强制 scope 的 dispatch choke point**。 @@ -19,7 +19,13 @@ - 每次内置调用都做 scope 检查;被拒 → **返回错误值**(绝不抛异常)。 - 越界的写**会执行**但发出 `out_of_scope_edit`;整文件写 `.py` 发出 `full_file_write`。 - **错误是观测结果,不是崩溃。** -- 额外(由 step 提供的)工具绕过内置 allowlist,但**仍然被记 trace**。 +- 额外(由 step 提供的)工具绕过内置 allowlist,但**仍然被记 trace**; + 声明了 `write_path_arg` 的 extra **选择加入**与内置同级的写路径强制 + (只读拒绝、可写墙、越界记录)——未声明的 extra 保持历史直通行为。 +- `ToolDef.audit_ok`(可选分类器):失败以普通返回值出现的工具 + (父层形状的 `{"error": ...}` 字符串),传输层保持 ok=True(字节就是结果), + 但 trace 事件按分类器判定记账 —— 缺文件/未接后端计为失败,不是成功; + 分类器自身抛错**绝不**打断 dispatch。 - **`read_file` 是窗口化的(48k 字符,用 `offset` 翻页),不是整文件读。** 整文件读会吹爆会话历史、成倍增加未缓存 token,并把会话推出可靠缓存长度 —— 这是**实测出来的成本,不是谨慎**。