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 doc/architecture/SPEC/cli.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# cli/ —— 规范

<!-- verified-against: 2026-08-28 -->
<!-- verified-against: 2026-09-06 -->

`LOC ~1420(6 个文件) · 接口 + 编排门面 · refactor-status: ok`

Expand Down Expand Up @@ -67,6 +67,8 @@ flag CLI 与 `Copilot` 门面:解析 → 过门 → 执行;并持有 run 目
`resolve()` 与 `_execute` 的 state seed 都走它。
- 阻塞原因随终局落盘:`last_blocked_reason` 进入 `run_status.json` 的
note —— 以前只打在子进程 console 上就被丢掉。
- resume 成功终局调用 notifier resolution:旧 `ESCALATION.md` 归档到 history,
不让已经完成的 run 继续显示活动阻塞标记。

## 边界 —— 不属于这里
不含 step 逻辑、不含仓库知识字面量、不含 LLM prompt。**只做编排接线。**
Expand Down
5 changes: 4 additions & 1 deletion doc/architecture/SPEC/engine/steps/rebase_knowledge.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# engine/steps/rebase_knowledge.py —— 规范

<!-- verified-against: 2026-08-25 -->
<!-- verified-against: 2026-09-06 -->

`LOC ~354 · step 库(v3 知识尾段) · refactor-status: ok`

Expand All @@ -24,6 +24,9 @@ v3 playbook 的 Rev 8 §2.2 流水线尾段(`phase5_report → curate → comp
report_only 到不了 prep(它的 stores 全程只读)。
- `phase5_report` 在 curate **之前**运行、只读 substate —— curation 永远
没机会掩盖 run 实际做了什么。
- phase5 事实必须可区分:未分派 module 显示 `skipped (not assigned)`,本地
测试分别报告 manifest 总数、实际执行数与 not-run/skipped;远端 CI 同时
展示 provider 原始状态和 baseline reconciliation 后的 effective 结果。
- curate 只写 copilot **runtime** store —— 父 read-compat 层与 adapter 树
绝不被写(**D2**);watchdog 决策收割**恰好一次**(state log 的身份集是
去重权威;`_repo_run_dirs` 按 task.json 定界到本仓库的 run dirs,跨仓库
Expand Down
2 changes: 1 addition & 1 deletion doc/architecture/SPEC/engine/steps/rebase_v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ step —— 薄的受治理 wrapper,substate-first、类型化失败、发布
| `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,以及逐 job 分类证据) |
| `rebase.v3_ci` | script/push | `ci_result`、`ci_build_urls`;substate `ci`(rounds/adopted/unfixed,以及逐 job 分类证据与 monitor 时长) |
| `rebase.v3_finalize` | deterministic/read | 无发布 —— substate `phase=done|needs_human`;有失败 ⇒ BLOCKED(复用 exit 3) |

## 公开契约(注册的 step 之外)
Expand Down
7 changes: 4 additions & 3 deletions doc/architecture/SPEC/engine/steps/report.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
# engine/steps/report.py —— 规范

<!-- verified-against: 2026-08-18 -->
<!-- verified-against: 2026-09-06 -->

`LOC ~24 · step 库 · refactor-status: ok`

## 职责
`report.final_summary` —— 用累积的 step 输出写出 `RUN_REPORT.md`。
`report.final_summary` —— 用累积的 step 输出写出 `RUN_REPORT.md`;repo-rebase
将既有 `FINAL_SUMMARY.md` 嵌入该 canonical 用户报告,不再产出空壳。

## Steps
`report.final_summary`(report/report)。

## 不变量
- 纯输出;**没有失败路径**(永远 ok)。
- 读 `ctx.state["outputs"]`;只写 run 目录。
- 读 `ctx.state["outputs"]` 与同 run 的 rebase summary;只写 run 目录。

## 边界 —— 不属于这里
不做分析、不含仓库知识、除报告文件外没有任何副作用。
Expand Down
10 changes: 8 additions & 2 deletions doc/architecture/SPEC/metrics.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# metrics.py —— 规范

<!-- verified-against: 2026-08-18 -->
<!-- verified-against: 2026-09-06 -->

`LOC ~493 · 跨切(测量) · refactor-status: oversized`

## 职责
计算并持久化逐 run 的 `metrics.json`(CATQ = Q·S/C)。

## 功能
读取该 run 的 trace/产物;计算 Q(按 kind 在**已知**分量上加权的质量)、
读取该 run 的 trace/产物以及 rebase 的 substate/CI-op/push WAL;计算 Q(按 kind 在**已知**分量上加权的质量)、
S(由 incident 得出的安全系数)、C(RQS3e 式的对数成本,基于 USD + 墙钟对参考预算);
写出 `metrics.json`。

Expand All @@ -24,6 +24,12 @@ S(由 incident 得出的安全系数)、C(RQS3e 式的对数成本,基
- **成本绝不编造。** harness 后端不向 span 记账暴露 token(每项都是 `tok_out=0`),
所以来源被记为 `subscription`,**不发明 USD** —— 这正是 harness 的成本优势
**是假设而非测量**的原因(`providers/base.md`)。
- repo-rebase 的 push/build 计数来自 durable WAL,tool 数来自逐次
`tool_call` 事件;CI monitor wall-time 随 round 持久化并明确标来源。已有 build 但没有可恢复
时长、或有 LLM request 却没有 token usage 时,成本标 `partial` 且未知 CI
分钟写 `null`,绝不以 0 冒充。
- repo-rebase 的 completed/conflict/tests/purity/push_safe 从 terminal substate、
WAL 与 incident trace 推导;未观察到的信号继续为 `null`。

## 边界 —— 不属于这里
**不影响控制流。只做测量。**
Expand Down
8 changes: 5 additions & 3 deletions doc/architecture/SPEC/notify.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# notify.py —— 规范

<!-- verified-against: 2026-08-18 -->
<!-- verified-against: 2026-09-06 -->

`LOC ~112 · 跨切(升级) · refactor-status: ok`

Expand All @@ -9,16 +9,18 @@

## 功能
`Notifier.escalate` 写出 `ESCALATION.md`、发邮件(配置了就走 Resend 或 SMTP)、
把这次升级记进 trace;`BLOCKED_EXIT` = 3。
把这次升级记进 trace;恢复后的 run 成功时 `Notifier.resolve` 把活动标记移入
`ESCALATION_HISTORY.md`;`BLOCKED_EXIT` = 3。

## 公开契约
`Notifier(settings, run_dir, trace, run_id)`,带 `escalate(reason, phase,
severity, state_summary, artifacts)`;以及 `BLOCKED_EXIT`。
severity, state_summary, artifacts)`、`resolve(resolution)`;以及 `BLOCKED_EXIT`。

## 不变量
- 被阻塞的 run 会写出 `ESCALATION.md`、发出通知,调用方以退出码 3 结束。
- **升级是一等结果** —— 绝不被当作错误路径吞掉。
- 邮件失败是尽力而为的,**不得**掩盖升级本身。
- `ESCALATION.md` 只表示当前仍需关注;成功 resume 后历史保留但活动标记消失。

## 边界 —— 不属于这里
"要不要升级"由 executor 决定(类型化失败路由)—— 本文件只负责**执行通知**。
Expand Down
2 changes: 2 additions & 0 deletions doc/architecture/SPEC/rebase_engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ runner/LLM/CI client 全部可注入 —— 每个模块都能离线测试。
shell 的 venv 状态绝不影响装到哪;只有 import 验证失败才重试。
- CI 创建受守卫:durable op 记录先于 API 调用,恢复按 op id 精确匹配,
**不确定时绝不重复创建**。
- 每个被 monitor 的 CI round 持久化实际 monitor 时长,供 run metrics 从 durable
substate 计费;无法恢复的旧/中止 build 时长保持 unknown。
- CI baseline 使用最高可信来源(schedule > API > other)的最近窗口:最新
构建失败立即生效,较旧失败只有在窗口内重复出现才保留;单次陈旧红构建不会
越过更新的绿构建复活。
Expand Down
3 changes: 3 additions & 0 deletions src/infermatrix_copilot/cli/copilot.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,9 @@ def _execute(self, playbook, spec: TaskSpec, run_dir: Path, *,
outcome = asyncio.run(
run_guarded(executor.run(playbook, state), run_dir))

if outcome.status == "done":
notifier.resolve()

if self.settings.metrics_enabled:
try: # metrics are facts about the run; never let them break it
from ..metrics import collect_run_metrics
Expand Down
26 changes: 21 additions & 5 deletions src/infermatrix_copilot/engine/steps/rebase_knowledge.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,23 @@ async def _v3_phase5_report(ctx: StepContext) -> StepResult:
"## Modules"]
for name, spec in sorted(mods.items()):
spec = spec or {}
lines.append(f"- {name}: {spec.get('status', '?')}"
status = ("skipped (not assigned)" if spec.get("skip") else
spec.get("status") or "not completed")
lines.append(f"- {name}: {status}"
+ (f" — {spec.get('detail')}" if spec.get("detail")
else ""))
if not mods:
lines.append("- (none)")
passed = int(pipeline.get("passed") or 0)
failed = int(pipeline.get("failed") or 0)
skipped = int(pipeline.get("skipped") or 0)
manifest_jobs = int(ctx.state.get("manifest_jobs") or 0)
total = manifest_jobs or passed + failed + skipped
Comment on lines +142 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate the manifest count for full rebase reports

In the locked workflow, phase5_report runs only in full mode, while the only producer of manifest_jobs is rebase.v3_scan, which runs only in report-only mode. Full runs therefore always take this fallback; when the manifest contains nightly jobs, rebase.v3_test_loop excludes them, so the report labels only the local pipeline total as “manifest jobs” and omits those unrun jobs. Publish len(built.jobs) from the full test-manifest construction or derive the count from a persisted artifact.

Useful? React with 👍 / 👎.

lines += ["", "## Local tests",
f"- passed {pipeline.get('passed', 0)} / failed "
f"{pipeline.get('failed', 0)} / skipped "
f"{pipeline.get('skipped', 0)}"]
f"- manifest jobs: {total}",
(f"- executed: {passed + failed} ({passed} passed, "
f"{failed} failed)"),
f"- not run/skipped: {skipped}"]
for t in pipeline.get("failed_tests") or []:
lines.append(f" - failed: {t}")
for i in tests.get("infra_failures") or []:
Expand All @@ -147,8 +155,16 @@ async def _v3_phase5_report(ctx: StepContext) -> StepResult:
lines.append(f"- precommit: {precommit}")
if ci:
lines += ["", "## Remote CI",
f"- result: {ci.get('result', '')}"
f"- effective result: {ci.get('result', '')}"
+ (f" ({ci.get('reason')})" if ci.get("reason") else "")]
for round_ in ci.get("rounds") or []:
build = round_.get("build_id") or "?"
lines.append(
f"- raw provider state: {round_.get('build_state') or 'unknown'} "
f"(build {build}; {round_.get('passed', 0)} passed, "
f"{len(round_.get('failed') or [])} unresolved, "
f"{round_.get('ignored_baseline', 0)} baseline, "
Comment on lines +163 to +166

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include incomplete CI jobs in the unresolved count

When monitoring ends with scheduled/running jobs or per-job budget timeouts, _record_outcome stores them in incomplete and budget_timeouts, not in failed. This report therefore prints 0 unresolved for rounds that still have unresolved jobs, contradicting the raw provider state and potentially misleading operators reviewing a failed/no-signal run. Count the unresolved union rather than only failed.

Useful? React with 👍 / 👎.

f"{round_.get('ignored', 0)} ignored)")
for name in ci.get("unfixed") or []:
lines.append(f" - unfixed: {name}")
path = ctx.run_dir / "FINAL_SUMMARY.md"
Expand Down
1 change: 1 addition & 0 deletions src/infermatrix_copilot/engine/steps/rebase_v3.py
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,7 @@ async def _abort_ci_cleanup(outcome, _client=client, _ops=ops_dir):
"ignored": r.ignored,
"ignored_baseline": r.ignored_baseline,
"jobs": list(r.jobs),
"monitor_duration_sec": r.monitor_duration_sec,
} for r in result.rounds]}})
updates = {"ci_result": result.result,
"ci_build_urls": [r.build_url for r in result.rounds
Expand Down
14 changes: 14 additions & 0 deletions src/infermatrix_copilot/engine/steps/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,20 @@ async def _final_report(ctx: StepContext) -> StepResult:
if note:
lines += [f"- {note}", ""]
outputs_map = ctx.state.get("outputs") or {}
if isinstance(spec, dict) and spec.get("kind") == "repo_rebase":
rebase_summary = ctx.run_dir / "FINAL_SUMMARY.md"
if rebase_summary.is_file():
try:
summary_lines = rebase_summary.read_text(
encoding="utf-8").splitlines()
if summary_lines and summary_lines[0].startswith(
"# FINAL_SUMMARY"):
summary_lines = summary_lines[1:]
while summary_lines and not summary_lines[0].strip():
summary_lines.pop(0)
lines += ["## Rebase result", "", *summary_lines, ""]
except OSError:
pass
for key in _DELIVERABLE_KEYS:
text = ctx.state.get(key)
if not text:
Expand Down
Loading
Loading