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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
- `imupdate`:上游变化后更新模型清单、registry、deploy、路径路由和
source pin,避免继续按旧知识工作。
- `imkbinit`:为新接入的仓库分阶段初始化知识库(`kb init`:骨架 → 模块 →
深化 → 校准集),每阶段一个 PR,由人审阅合并后再进入下一阶段。
深化 → 历史 PR 学习 → 校准集),每阶段一个 PR,由人审阅合并后再进入下一阶段。
历史 PR 学习默认回溯 1,000 个已合并上游 PR,逐 PR 提炼;每次知识升级一个 commit,
汇成一个 PR,再由 Codex 审阅完整改动。

copilot 本身与具体仓库无关:每个仓库的知识住在自己的 adapter
(`adapters/<repo>/`)和知识切片(`knowledge/repos/<repo>/`)里,playbook
Expand Down
5 changes: 4 additions & 1 deletion doc/architecture/SPEC/engine/steps/knowledge.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# engine/steps/knowledge.py —— 规范

<!-- verified-against: 2026-09-30 -->
<!-- verified-against: 2026-10-01 -->

`知识服务的 knowledge.* 步骤 · refactor-status: new`

Expand All @@ -26,3 +26,6 @@ auto_merge 仓库写 outbox 的 `open_pr` 项,shadow 只记录)。逻辑全
调 `init_stages.run_stage`;`InitError`/`NotImplementedError` 与状态 `blocked` 的记录都返回 BLOCKED。`state_updates`:
`kb_init_stage`、`kb_init_status`、`kb_init_pr`。它是本模块里唯一直接写 GitHub 的步骤:经仓库主人的 `gh` 开一个由人合并的
PR,且只在 `ALLOW_PUSH=1` 与 `ALLOW_POST=1` 同时成立、非 dry run、上游非私有时。测试:`test_kb_init_skeleton.py`。

`pr-history` 阶段另接收可选 `pr_count` 与 `budget_usd`,转成整数/浮点后传入 `run_stage`;非法值返回 BLOCKED。
历史 PR 的完整选择、逐 PR checkpoint、commit 串和 Codex 审阅均由 `kb_service.init_history` 负责。
47 changes: 47 additions & 0 deletions doc/architecture/SPEC/kb_service.md
Original file line number Diff line number Diff line change
Expand Up @@ -527,3 +527,50 @@ Direct 的产品就是内嵌地图:知识侧 `_routes.yaml` 路由到的每个
用生产提取器逐页检查,缺页或 `unavailable` 即 blocked 并点名页面;本次写入或改动的页面被服务端截断(`truncated`,CI 拒绝新增的截断)也 blocked。manifest 路由的仓库没有 routes 文件、
不检查(Direct 的 manifest 回退对其页面报 `read_required`)。
测试:`test_kb_init_quick_maps.py`。

## 2026-10-01 历史 PR 学习阶段(pr-history)

阶段顺序:skeleton → modules → deepen → pr-history → harvest-calibration。
`InitConfig.pr_history_count` 为正整数,缺省 1000;CLI `--pr-count N` 可覆盖。
历史模块按当前 pin 的提交时间选最近已合并上游 PR,按合并时间从旧到新重放。
GitHub REST closed-pull 分页排除未合并与 pin 时间之后的 PR,以 update/merge 边界证明窗口完整;
不受 Search 的 1000 条限制,扫描上限仍不足以证明完整则阻塞。非 pin 祖先的 merge 明确跳过。
PR body、完整 diff、reviews、inline 回复关系与 issue 回复只在内存中暂存;文件和讨论分页,
完整 diff 独立读取以避免文件 patch 截断;超出 500KB 或文件列表不完整即拒绝提炼。

每 PR 一次 `ModelGateway` 生成调用,使用配置的 `KB_GENERATOR`,沿用价格/花费阈值检查;
raw payload 不写 trace,保留模型身份、耗时、用量和成本。模型输出 trigger/must/forbid/acceptance、
最近 owner 与当前源码行范围。只有当前源码支持的可执行规则可追加到 owner rule page;
同义合并交给提炼与整 PR 审阅,完全相同的正文只在同一 owner 的规则页/拆分页内确定性去重;
独立 owner 可保留相同合同。模型路由从 `_routes.yaml.models` 的现有 rule pages 读取,按名字和
当前源码路径提供 `model:<name>` owner;单模型合同不得落到组件页,具体变体不继承短模型名。
维护必要索引和快速入口。
每个非空升级通过 pin claims、L1、知识树与 wiki 校验后形成一个 commit 计划;无升级的 PR 不造空 commit。
本阶段不逐规则调用 advisory judge,现有阶段仍使用原 judge;最终整 PR 审阅单独记录,不冒充逐条评分。

`InitRecord.history` 保存不可变窗口、已完成 PR、pending 提炼结果及逐 PR commit delta;
每次预留费用在派发前写 checkpoint,进程中断把未知费用按整次 reservation 计入;
重跑不重复已完成 PR,完整提炼结果可复用。`--budget-usd N` 提高累计预算继续同一窗口,
不重置既有花费;首次启动时固定 knowledge base,后续 main 合并不改变同一批次的 baseline;
pin/window/backend 或目标仓库变化阻塞并保留 checkpoint(包括已准备发布的重试),
换 baseline 需新的 state directory。旧阶段的 inputs digest
保持兼容,新增 history 配置不影响其缓存。旧三阶段 init 没有 history record 时仍可 harvest;
一旦 history 开始,harvest 要等它完成并合并。已完成旧 init 也可单独添加此阶段。

已合并依赖、base、pin 与 backend 均相同时,history dry run 可提升为实际发布而不重复提炼;
提交作者变化会产生新 head,因此再次完整审阅该 head。去重保留源码 token 的大小写和内部空白。
history 的存在/状态加入后续 harvest 的 cache identity,chain 阻塞时缓存不能越过依赖检查。

`InitPublisher.build_series` 用 scratch index 在固定 base 上按顺序建非空 commit,作者/日期确定,
不 checkout 主人的工作树。出版前 journal 固定整个串;push 后 create 失败可重建同一 head 并复用分支/PR。
只开一个 draft PR。`KB_INIT_REVIEWER=codex:model[:effort]`(缺省 `DEFAULT_JUDGE`)经同一 gateway
审阅完整 base..head diff、逐 PR 来源和当前 pin 的源码证据;provider 必须为 codex,无 fallback。
完整 packet 还包含升级页所在 owner 的原有 rule pages 和组件/模型路由,以检查与未改规则的重复、冲突。
PR 描述只展示前 20 个升级及总数,完整来源留在 commit trailers/本地 checkpoint;描述限制 60KB,
避免默认 1000 条历史超过 GitHub body 限制。旧 journal 仅重建超长描述,不改已推 head。
完整审阅上下文超过 1MB 阻塞,禁止截断冒充全量;按 judge_call_usd 约定费用预留。
`InitRecord.review` 钉住 base、head、diff SHA256、请求/实际模型、verdict、findings 与 summary。
只有该身份的 approve 可复用;发布 review summary 后重新核对远程 OPEN/head,才 `gh pr ready`。
模型失败、findings 或远程 head 变化均保持阻塞;人仍负责合并。
dry run 为旧快照建本地临时 baseline,再生成相同升级串与全量 Codex 审阅,落盘预览和 `COMMITS.json`,不写 GitHub。
全程不打开服务账本。测试:`test_kb_init_history.py`、`test_kb_init_config.py`、`test_imkbinit_skill.py`。
6 changes: 4 additions & 2 deletions doc/architecture/SPEC/playbooks/PLAYBOOKS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# playbooks/*.yaml —— 规范

<!-- verified-against: 2026-09-30 -->
<!-- verified-against: 2026-10-01 -->

`9 个文件 · 声明式编排数据 · refactor-status: ok`

Expand Down Expand Up @@ -60,4 +60,6 @@ success, steps[]`。

## kb-init(candidate)
为一个仓库建知识库:单步 `knowledge.init`,每次运行一个阶段(skeleton → modules → deepen + 生命周期开关 →
harvest-calibration),每阶段一个由人合并的 PR。只由 `kb init REPO --stage S` 运行,不经 `kb serve`,不写服务账本。
pr-history → harvest-calibration),每阶段一个由人合并的 PR。只由 `kb init REPO --stage S` 运行,不经 `kb serve`,不写服务账本。
`pr-history` 默认回溯 1000 个已合并上游 PR;可传 `pr_count` 和累计花费上限 `budget_usd`。
每个非空知识升级一个 commit,汇成一个 draft PR,完整 Codex 审阅通过后才 ready。
6 changes: 4 additions & 2 deletions playbooks/kb-init.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# kb init for ONE repository: one stage per run (skeleton, then modules, then
# deepen + lifecycle flip, then harvest-calibration), each opened as a
# deepen + lifecycle flip, then pr-history, then harvest-calibration), each opened as a
# human-merged PR (design kb-init v3). Runs outside the knowledge service: it
# never writes the service ledger. Candidate.
name: kb-init
Expand All @@ -13,9 +13,11 @@ repos: []
requires: []
params:
repo: "knowledge repository name"
stage: "skeleton | modules | deepen | harvest-calibration"
stage: "skeleton | modules | deepen | pr-history | harvest-calibration"
dry_run: "true (default): write the tree and PR body locally; false needs ALLOW_PUSH=1 and ALLOW_POST=1"
pin: "upstream commit to pin (default: the default branch head)"
pr_count: "pr-history only: merged upstream PRs to replay (default: adapter pr_history_count or 1000)"
budget_usd: "pr-history only: cumulative spend ceiling; increasing it resumes the same window"
steps:
- {id: init, step: knowledge.init}
success: the stage's PR is open (or its dry run is written), or the record says why it is blocked
29 changes: 27 additions & 2 deletions plugins/infermatrix-copilot/skills/imkbinit/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: imkbinit
description: Bootstrap a repository's InferMatrixCopilot knowledge base with `kb init`, one reviewed stage at a time (skeleton, modules, deepen, harvest-calibration). Use when the user invokes /imkbinit or $imkbinit, asks to initialise or onboard a repository's knowledge base, or wants the next kb init stage run.
description: Bootstrap a repository's InferMatrixCopilot knowledge base with `kb init`, one reviewed stage at a time (skeleton, modules, deepen, pr-history, harvest-calibration). Use when the user invokes /imkbinit or $imkbinit, asks to initialise or onboard a repository's knowledge base, or wants the next kb init stage run.
---

# InferMatrix knowledge-base init
Expand Down Expand Up @@ -52,7 +52,7 @@ prints them, and makes no model call).

## 2. Find the next stage

Stages run in this order: `skeleton` → `modules` → `deepen` →
Stages run in this order: `skeleton` → `modules` → `deepen` → `pr-history` →
`harvest-calibration`. The record of each stage is
`<state-dir>/init/<repo>/<stage>.json`, where the state dir is `--state-dir`,
else `$KB_STATE_DIR`, else `~/.infermatrix-copilot/kb`.
Expand Down Expand Up @@ -91,6 +91,20 @@ What each stage opens:
- `modules`: one map card for every unrouted module.
- `deepen`: code rules for the hot modules, plus the adapter flip to
`enabled: true, mode: shadow`.
- `pr-history`: reads the latest 1,000 merged upstream PRs as of the upstream
pin, oldest first, with one extraction call per PR through `KB_GENERATOR`.
`--pr-count N` overrides `init.pr_history_count` (default 1000). Executable
rules go to their nearest component or offered model owner; model-specific
contracts stay with that model. PRs yielding no upgrade are recorded and
make no commit. One draft PR contains one commit per accepted upstream PR.
The PR body shows counts and the first 20 upgrades; all upstream references
remain in the commit trailers. Codex reviews its complete base-to-head diff,
prior owner rules, routes and pinned source evidence;
only approval of that exact head makes it ready. `KB_INIT_REVIEWER` may
pin a Codex model/effort (`codex:model[:effort]`, default
`codex:gpt-6-sol:medium`); it never changes reviewer provider. A failed review leaves
the PR draft. Dry runs also perform the complete Codex review and write a
`COMMITS.json` beside the preview. Raw PR evidence is transient.
- `harvest-calibration`: turns the merged review into
`adapters/<adapter>/kb-calibration/cases/*.json`. It refuses to open a PR
with fewer than five bad cases from the owner or from mutations.
Expand All @@ -101,6 +115,17 @@ flips it.
If the CLI or its run report blocks the stage, report the reason and stop.
Never work around it by hand.

For `pr-history`, a blocked record is a resumable checkpoint. When the user
requests a retry, rerun the same stage with the same pin/window/backend;
completed PRs and prepared commits are reused. The stage accounts cumulative
spend on its frozen knowledge baseline; unrelated main merges do not restart
the batch. Changing the baseline requires a new state directory.
`--budget-usd N` may raise the ceiling to continue after a budget stop.
Prepared publication retries also require the original immutable inputs.
It does not publish an incomplete window. Existing init runs with no history
record remain harvestable, and may add this phase after their earlier stages
have merged. Once a history record exists it must finish before harvest.

## 4. Report

Read the stage record and the PR body (`PR_BODY.md` under `pr.dry_run_dir` for
Expand Down
9 changes: 7 additions & 2 deletions src/infermatrix_copilot/engine/steps/knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,13 @@ async def init_stage(ctx: StepContext) -> StepResult:
lifecycle = rt.registry.get(repo)
if lifecycle is None:
return StepResult(False, FailureKind.BLOCKED, f"no adapter declares knowledge repo {repo!r}")
record = run_stage(rt, lifecycle, stage, dry_run=dry_run, pin=pin)
except (InitError, NotImplementedError) as exc:
count = ctx.params.get("pr_count")
kwargs = {"pr_count": int(count)} if count not in (None, "") else {}
ceiling = ctx.params.get("budget_usd")
if ceiling not in (None, ""):
kwargs["budget_usd"] = float(ceiling)
record = run_stage(rt, lifecycle, stage, dry_run=dry_run, pin=pin, **kwargs)
except (InitError, NotImplementedError, ValueError) as exc:
return StepResult(False, FailureKind.BLOCKED, str(exc))
updates = {"kb_init_stage": stage, "kb_init_status": record.status, "kb_init_pr": dict(record.pr)}
if record.status == "blocked":
Expand Down
18 changes: 17 additions & 1 deletion src/infermatrix_copilot/kb_service/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ def _init_command(args, state_dir: Path) -> int:
from .runner import run_playbook

params = {"stage": args.stage, "dry_run": "true" if args.dry_run else "false", "pin": args.pin or ""}
if args.pr_count is not None:
if args.stage != "pr-history" or args.pr_count < 1:
print("--pr-count is a positive integer for --stage pr-history only", file=sys.stderr)
return 2
params["pr_count"] = str(args.pr_count)
if args.budget_usd is not None:
import math

if args.stage != "pr-history" or not math.isfinite(args.budget_usd) or args.budget_usd <= 0:
print("--budget-usd is a finite positive ceiling for --stage pr-history only", file=sys.stderr)
return 2
params["budget_usd"] = str(args.budget_usd)
outcome, run_dir = run_playbook(Settings(), "kb-init", args.repo, state_dir=state_dir, params=params)
print(f"kb init {args.repo} {args.stage}: {outcome.status} ({run_dir})")
return 0 if outcome.status == "done" else 1
Expand Down Expand Up @@ -179,7 +191,11 @@ def main(argv: list[str] | None = None) -> int:
export.add_argument("--role", default="judge", choices=("judge", "generator"))
init = sub.add_parser("init")
init.add_argument("repo")
init.add_argument("--stage", choices=("skeleton", "modules", "deepen", "harvest-calibration"))
from .init_support import STAGES

init.add_argument("--stage", choices=STAGES)
init.add_argument("--pr-count", type=int, help="PR-history window (default: adapter pr_history_count or 1000)")
init.add_argument("--budget-usd", type=float, help="PR-history cumulative spend ceiling; may be raised to resume")
init.add_argument("--dry-run", action="store_true",
help="write the tree and PR body under the state directory instead of opening a PR")
init.add_argument("--pin", help="upstream commit to pin (default: the default branch head)")
Expand Down
4 changes: 3 additions & 1 deletion src/infermatrix_copilot/kb_service/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
_INIT_KEYS = {
"seeds", "doc_globs", "source_roots", "exclude", "module_depth", "min_module_loc",
"pr_window", "coverage_target", "budget_usd", "judge_call_usd",
"generator_call_usd", "harness_overhead_bytes",
"generator_call_usd", "harness_overhead_bytes", "pr_history_count",
}
# README* + docs/**/*.md is what profiles/establish.build_doc_corpus reads;
# the contribution and agent guides carry most cross-doc invariants.
Expand Down Expand Up @@ -66,6 +66,7 @@ class InitConfig:
min_module_loc: int = 300
pr_window_count: int = 200
pr_window_max_age_days: int = 180
pr_history_count: int = 1000
coverage_target: float = 0.85
budget_usd: float = 30.0
judge_call_usd: float = 0.50
Expand Down Expand Up @@ -195,6 +196,7 @@ def parse_init(section: object, where: str, *, repo: str, manifest: dict) -> Ini
pr_window_count=_int(window.get("count", 200), f"{where}.pr_window.count", minimum=1),
pr_window_max_age_days=_int(window.get("max_age_days", 180),
f"{where}.pr_window.max_age_days", minimum=1),
pr_history_count=_int(data.get("pr_history_count", 1000), f"{where}.pr_history_count", minimum=1),
coverage_target=coverage,
budget_usd=budget,
judge_call_usd=judge_call,
Expand Down
Loading
Loading