@tea-agent/loop-agent 0.18.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +29 -73
  3. package/README.md +4 -6
  4. package/dist/commands/docs-audit.js +1 -0
  5. package/dist/commands/init.js +3 -3
  6. package/dist/governance/exec-plans.js +4 -0
  7. package/dist/worker/cli.js +13 -12
  8. package/dist/worker/console/doctor.js +55 -2
  9. package/dist/worker/console/index.js +1 -0
  10. package/dist/worker/console/loopback.js +2 -2
  11. package/dist/worker/console/observe-link.js +7 -2
  12. package/dist/worker/console/operator-actions.js +30 -2
  13. package/dist/worker/console/operator-selection.js +92 -0
  14. package/dist/worker/console/operator-surface-health.js +23 -0
  15. package/dist/worker/console/recovery-cta.js +2 -2
  16. package/dist/worker/console/routes.js +45 -19
  17. package/dist/worker/console/security.js +29 -4
  18. package/dist/worker/console/server.js +106 -8
  19. package/dist/worker/console/static/assets/index-3vsjZJHq.js +16 -0
  20. package/dist/worker/console/static/assets/index-i1wV4LrY.css +1 -0
  21. package/dist/worker/console/static/index.html +2 -2
  22. package/dist/worker/observe/routes.js +63 -21
  23. package/dist/worker/observe/server.js +3 -10
  24. package/dist/worker/observe/static/index.html +6 -3
  25. package/dist/worker/observe/static/styles.css +53 -0
  26. package/dist/workflows/dag/backend-test-markdown-workflow.js +58 -33
  27. package/dist/workflows/dag/init-hybrid.js +3 -3
  28. package/docs/README.md +3 -2
  29. package/docs/architecture/evolution.md +6 -6
  30. package/docs/architecture/worker-and-feature.md +9 -9
  31. package/docs/templates/backend-test-dag.json +4 -4
  32. package/package.json +1 -1
  33. package/skills/agent-worker/references/agent-worker-operator.md +2 -2
  34. package/skills/loop-agent/references/command-reference.md +4 -2
  35. package/skills/loop-agent/references/harness-policy.md +1 -1
  36. package/dist/worker/console/static/assets/index-KUSib7aM.js +0 -16
  37. package/dist/worker/console/static/assets/index-ucIzpaGJ.css +0 -1
package/AGENTS.md CHANGED
@@ -54,7 +54,7 @@
54
54
  10. 检查 `git status --short --branch`。
55
55
  11. 运行本次任务相关的最小基线验证。
56
56
  12. 如果用户提到"后端测试"、"接口测试"、"pytest"、"自动化测试",在任务 `task.json` 中设置 `taskKind: "backend-test"` 再 `dag run-task`;不要用 `--profile backend-test`(CLI 不接受该值,专用模板只走 taskKind)。知识回写用 `taskKind: "knowledge-sync"`(须 `featureId`),图谱开荒用 `taskKind: "knowledge-graph-bootstrap"`。`--profile` 仅表示治理强度:`auto|minimal|standard|reviewed|supervised`。
57
- 13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker observe serve --repo . --port 8787` 命令启动。
57
+ 13. 如果用户提到"看板"、"observe"、"监控面板"、"启动看板",使用 `agent-worker console serve --repo . --port 8790` 启动统一 Operator Console;`/inspect/` 提供只读检视。`agent-worker observe serve --repo . --port 8787` 仅为兼容入口。
58
58
  14. 如果用户要求“合并 `<source>` 到 `<target>`”或“合并 origin/main 到当前分支”,先阅读 `docs/branch-merge-guideline.md`,按影响自动选择快速、标准或深度模式;始终冻结 source SHA、审查双方功能、运行 merge-tree、生成 source-SHA 合并报告,并在提交前再次 fetch 防止主干前进。
59
59
 
60
60
  ## 会话协议
@@ -89,7 +89,7 @@
89
89
  - 长期决策写入 `docs/`,不要只留在聊天里。
90
90
  - 分支合并遵循 `docs/branch-merge-guideline.md`;快速模式只用于可证明的低风险/no-op 合并,涉及冲突、init/package/runtime/release/public API 时必须升级为标准或深度模式。
91
91
  - 后端测试、接口/API 测试、pytest 或明确的后端自动化测试,必须把 `.harness/tasks/<task-id>/task.json` 的 `taskKind` 设置为 `"backend-test"`,不得保留默认 `standard`。`backend-test` 是 `taskKind`,不是 `--profile` 的可选值;`dag run-task` 继续使用 `--profile auto` 选择治理等级。仅说“自动化测试”且前后端不明时,先根据任务源和项目技术栈判断,禁止无条件路由。
92
- - 本地只读运行看板:`agent-worker observe serve --repo . --port 8787`,访问 `http://127.0.0.1:8787/`;看板只读,默认只绑定本机 `127.0.0.1`,不要直接暴露到公开网络。
92
+ - 本地 Operator Console:`agent-worker console serve --repo . --port 8790`,访问 `http://127.0.0.1:8790/`;其中 `/inspect/` 为只读运行检视。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` / `--debug`,不要直接暴露到公开网络。
93
93
  - 面向使用者的新增、修改、删除或修复,应同步更新根目录 `CHANGELOG.md`;保持版本级摘要即可,不写过细技术细节。
94
94
  - 面向用户的中文更新日志、README 和说明文档应使用自然、结果导向的表达:先说明用户能获得什么或问题如何改善,保留必要的命令和产品术语,避免逐字翻译、内部实现细节和无意义的中英混杂。
95
95
  - 涉及 `loop-agent init` 或目标项目投影的改动,必须同步考虑目标项目生成物:`AGENTS.md`、`README.md`、`harness.json`、`ai_workspace/loop-agent/`、`scripts/`、`.agents/skills/`、`.harness/prompts`、`.gitignore`(loop-agent runtime managed block)和 npm 包内置 assets;目标项目根 `docs/` 和根 `skills/` 的旧投影需要由 `init update --apply-safe` 安全迁移或退役。
package/CHANGELOG.md CHANGED
@@ -2,86 +2,42 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- - `backend-test` active runtime/template 改为 8 节点 Markdown-first 流程:环境 Shell 先检查 Python/pytest、配置/fixture/target 和 HTML renderer;随后生成并独立 Review Markdown 用例,经过确定性 Markdown/traceability gate 后只执行一次 pytest,同时生成 JUnit、self-contained HTML 和 Markdown facts,最终输出 Markdown 测试报告与 L-5。新流程不再要求模型生成 Analysis/Execution/Manifest/Classification/Result/L-5 业务 JSON,历史 JSON artifacts 仍保留读取兼容。
5
+ ## [0.19.0] - 2026-07-23
6
6
 
7
- ### 修复
7
+ ### 重点更新
8
8
 
9
- - `backend-test` Markdown 用例生成与独立审查现在默认采用简体中文:用例标题、策略、前置条件、测试数据、步骤、预期结果、自动化说明、表头和证据缺口优先中文表达;Case/AC/REQ/BR ID、固定章节标题、HTTP 方法、路径、字段名、枚举值、命令和代码符号继续保持精确机器契约,不做无意义双语翻译。
10
- - `backend-test` traceability 现在识别带 `-> None` 等返回类型 annotation 以及可选 `async` 的 pytest 函数,不再把合法 typed test function 全部误报为缺失映射。
11
- - `backend-test` Markdown gate 只要求 case-level AC;pytest 执行、MD↔pytest 追溯、JUnit/HTML/stdout/stderr 和失败分类等下游证据 AC 交给 traceability、execute 与 report 节点,不再被错误要求写入单条 Markdown case。
12
- - `backend-test` Markdown gate 的 `Steps` 仍要求编号列表;`Expected Results` 现在接受编号列表或 Markdown bullet 列表,只要保持结构化可断言,不再误拒绝模型生成的合法 bullet 断言;普通散文仍 fail-closed。
13
- - `backend-test` 的 pre-run Spine 审计不再要求尚未执行的 `artifacts/requirement-coverage.json`:未覆盖需求在该 taskKind 下记为 warning,并交给运行内 Markdown validation 与 traceability gate 确定性阻断;普通实现任务仍保持 error/fail-closed。
14
- - `backend-test` Markdown 用例门禁现在从 DAG `sourceBinding` 精确识别 `Source References`,支持反引号以及中英文括号/标点,不再将说明文字吞入路径;未绑定引用、路径前缀伪装和绑定文件缺失仍然 fail-closed。
15
- - `backend-test` Markdown writer/reviewer 的 reference index 现在同时提供 task-relative 引用路径与 repo-relative `readPath`,Pi 会直接只读 `.harness/tasks/<taskId>/source/**` 的绑定文件,不再把 `source/references/**` 误读为仓库根路径,也不会在读取失败后回退到 `docs/**` 替代文件。
16
- - `backend-test` Markdown 用例门禁会通过 DAG `sourceBinding` 将 `source/需求.md`、`source/执行约束.md` 和 `source/references/**` 解析到对应任务目录,不再错误地相对仓库根查找,也不会接受未绑定的同名仓库文件。
17
- - `backend-test` traceability 门禁现在优先用 Markdown 中已知 Case ID 匹配 `test_BE_<MODULE>_<NNN>_` 函数前缀,不再把函数描述中的 `100`、`400`、`404` 等三位数字误识别为 Case ID。
18
- - `backend-test` traceability 门禁允许仅包裹 cleanup 调用的 `except requests.RequestException: pass`,避免 best-effort 资源清理被误报;bare/宽异常、`AssertionError` 及非 cleanup 主体仍然 fail-closed。
9
+ - Operator Console 统一整合 Operate 与 Inspect,在同一服务即可检视只读 Dashboard、DAG、时间线与 artifacts
10
+ - Observe 服务进入兼容期,仍可运行但会输出迁移提示,引导平滑过渡到统一控制台
11
+
12
+ ### 新增
19
13
 
20
- - DAG runtime task-contract 启动门禁现在只将 `writePolicy: "exclusive"` 视为 writer execution:历史只读/static/shell DAG 可继续执行,legacy writer DAG 仍必须升级为 DagSpec v4 并绑定 managed Task Contract;真实 DAG 生成会冻结当前 managed binding。测试执行 fixture、Worker materializer 和 Windows npm/tmp 路径也已同步适配该契约。
21
- - Pi CLI / SDK executor 不再把单个节点的完整 stdout 累积为一个 JS 字符串;长 `frontend-plan-pi` 或大段结构化输出会保留有界预览并继续流式解析最终 assistant 事件,避免触发 V8 `Invalid string length` 或卡死。
22
- - DAG 节点新增 `outputMode: "structured-required"`;前端实现计划/修订节点显式使用该模式,并在 `output-too-large` 时用紧凑结构化输出指令重试,避免直接截断计划后继续执行,也减少长计划输出阻断后续流程的概率。
23
- - `frontend-mock-assess-pi` 的 `MOCK_STRATEGY:` 协议行规范化现在可处理列表、引用、inline code、中文冒号、`Mock strategy` 标签和标签/值分行等常见格式漂移;仍只接受明确 Mock strategy 标签和五态白名单,不从普通正文猜默认策略。
14
+ - 新增统一 Operator Console:`console serve` 同时提供 Operate Inspect,支持通过 `/inspect/` 直接检视运行详情与 DAG
15
+ - Console 健康检查接口升级,包含 Inspect 就绪状态与路由能力信息
16
+ - `console serve` 新增局域网绑定(`--host 0.0.0.0`)与调试日志(`--debug`)选项,并在非本地访问时保留安全校验
24
17
 
25
18
  ### 改进
26
19
 
27
- - `backend-test` 的 source intake 能力已并入 Markdown 用例生成/Review:派生 `需求.md` / `执行约束.md` 有界内联,reference 使用 path/SHA-256/角色线索索引并按需精确读取;pytest writer 不再读取 `source/**`。
28
-
29
- - **Console Phase 3(Observe 协作)**:
30
- - Observe `GET /api/health` 升级为 versioned DTO(`schemaVersion: 1`、`repoFingerprint` 复用 Console `repoFingerprintV1`、package name/version、由 ROUTES 派生的 `routeCapabilities`)。
31
- - Console `observeLink` fail-closed:仅 health **match**(同仓 fingerprint + schema + 目标 kind 所需 capability)时返回深链;offline / mismatch 返回诊断与 `agent-worker observe serve --repo . --port 8787`,不打开错误 URL。
32
- - Doctor 的 Observe 探测扩展 match / mismatch / unavailable(Observe 仍非阻塞,doctor `ok` 保持)。
33
- - Recovery CTA 矩阵(report / doctor / decision / resume / reconcile / regenerate / observeLink):**无** Cancel、**无**主 CTA「直接改代码」。
34
- - Console 与 Observe 仍为独立进程;不 mount、不 proxy、不复制第二套 read model。
35
- - Phase 1 Console skeleton:`agent-worker console serve|doctor`(默认 loopback `127.0.0.1:8790`、非 loopback fail-closed)、Vite SPA 静态产物 + Node health/static server、boot capability cookie + mutation Host/Origin 门禁、封闭 ResourceLoader 骨架、sibling `loop-agent` identity doctor、`repoFingerprintV1` / Observe 深链 builder、以及 `UPSTREAM.md`(pi-web 源 commit)与预算 report。
36
- - Phase 0.5 operator surface repair:`buildOperatorCapabilitiesDocument()` 在权威 `src/shared/operator` 上补齐 Console 协商块 `dag`(`generatedSpecVersion: 4`、`requiresTaskContractBinding: true`,与 `dagSpecVersions` 同源常量),并删除冲突的 `src/task/operator` 双实现;CLI `--json` 继续写 `process.stdout`,测试侧改为捕获 stdout。
37
- - Nightly 自动发布改为双基线:`lastTag`(最新稳定 `vX.Y.Z`)只负责未发布增量、breaking 检测与 release notes;当前 minor 的 `vX.Y.0`(缺失则回退 `lastTag`)只累计严格 Conventional Commit `feat:` 以决定 minor/patch。手动 patch 不再抹掉同 minor 周期的 feature 信号;历史 minor 窗口中的 breaking 不再永久阻断后续自动发布。`Publish npm` 手动入口未改。
38
-
39
- - `frontend-implementation` 绿色路径由固定全链改为按需分支:删除重复的首轮 design shell gate,初审通过直接复用原计划,只有 `request-revision` 才执行计划修订与终审;只有可修复失败才执行 repair/reverify/retrace。常规全绿路径因此少执行 3 个 Pi 与 4 个 shell 节点,同时保留 pass-only 写入门禁、验证/评审证据和只读 `frontend-closeout-pi`。原计划/修订计划的结构化合同与需求覆盖使用同一有效来源,且只有主分支产物缺失时才回退;已存在但 malformed/schema-invalid 的产物继续 fail-closed。
40
- - Observe 节点检查器“规范证据”页签新增安全文件预览:已绑定任务源和已读取规范文件条目现以可聚焦按钮呈现,点击后在检查器内下钻查看内容(不新增弹窗),提供“返回规范证据”按钮并恢复触发行焦点。新增只读接口 `GET /api/dag-runs/:runId/nodes/:nodeId/spec-evidence/file?source=binding|read&path=...`,服务端重新校验证据成员身份(binding 必须在 `sourceBinding.sources`,read 必须有成功配对 read),拒 `..`/绝对/目录/二进制/仓库外符号链接;正文上限 64KB 并返回截断标志,统一走 Observe 脱敏规则;binding 返回 `sha256`/`currentSha256`/`hashMatch`,read 返回 session event 中实际读取正文与 `readAt`(不以当前工作区文件替代)。
41
- - 通用 standard/reviewed/supervised 实现 DAG 现在会在目标仓库存在 `AGENTS.md` 时,按本次 writer 的实际变更解析适用的根级/嵌套指令和仓库内代码规范索引:standard 复用 `verify-pi` 与条件 shell gate,reviewed/supervised 复用现有 `review-pi`/review gate,不新增模型节点;无 `AGENTS.md` 或无适用规范时保持原行为,审查节点只读且不会直接改代码。专用测试、前端和知识类 DAG 不受影响。
42
-
43
- - GitHub Actions CI 降低私有仓分钟消耗:合并为单 job(避免两次 `npm ci`)、同分支/`PR` concurrency 取消旧 run、Draft PR 跳过完整 CI,且仅忽略 `docs/reports/**`、`docs/progress/**`、`docs/design/archive/**`、`docs/exec-plans/completed/**` 等纯运营文档路径;治理文档与代码门禁仍全量跑。
44
- - 明确 **Compatibility / Operator Assist**:openCode 等主会话只编排已发布 `loop-agent` / `agent-worker` CLI 与只读诊断,不得绕过 CLI 直接改业务实现,失败只走 doctor / reconcile / human gate / CLI 重跑。`loop-agent init` 写入的 `AGENTS.md` managed block、包内 skills 与 website 快速开始/治理说明已对齐;架构决策见 `docs/decisions/0005-governed-operator-surface.md`(accepted)。已有目标项目用 `loop-agent init check-update` / `init update --apply-safe` 或 `init reconcile` 刷新 managed block。
45
- - 前端测试 DAG 收紧体验:execution preflight 仅硬校验绝对非生产 `baseUrl`;用例 map 缩短为优先用 `playwright-cli` 执行;复盘合并执行证据审查且不依赖 outcome=pass,失败也能出报告。
46
- - 前端测试用例生成现在会解析明确 `baseUrl`:优先读取任务源 `config.md` 中的前端 URL,缺失时默认 `http://localhost:5173`,并写入 RAG `context.md`;生成命令不得再保留 `<base-url>` 占位符。
47
- - 前端浏览器测试 DAG 的复盘报告现写入 `testcase/frontend/reports/**`,不再要求 `docs/test-reports/**` 权限;任务执行约束可以安全禁止整个 `docs/**`,同时仍保留可审计的测试资产。
48
- - `frontend-test` 结果链新增 run-owned `frontend-test-result-v1`、单次用例修订与终审门禁;只有所有浏览器用例和 AC 覆盖通过且结果合同明确为 real/pass 时,Worker 才会投影真实集成。
49
- - 后端测试 DAG 从 24 个收敛为 15 个真实顶层节点;用例与语义 `request-revision` 由确定性 gate 直接失败,pytest 只执行一次,TestBug 保留分类并转为独立 follow-up,不再触发同 run repair 或 final rerun。唯一 initial Result 会确定性复制为 canonical `contracts/backend-test-result.json`,最终 outcome 只认该结构化事实。
50
- - Backend Test Analysis、Case Manifest、Semantic Review 与 Classification 的严格 JSON 契约进一步对齐,生成节点明确字段白名单、`sourceBinding` 与 evidence gap 约束,避免模型自定义字段导致确定性门禁失败。
51
- - 新增升级后 init surface notifier:普通安全仓库命令结束后,当目标项目存在有效 `.harness/init-surface.json` 且 `controllerVersion` 与当前包版本不同时自动检测。surface 缺失/损坏、源仓库、未初始化目录与版本一致时直接跳过;`checkInitUpdate().ok` 时仅在无活跃运行时静默刷新 state;非 TTY 只向 stderr 输出 `loop-agent init reconcile --repo-root ...` 提示且不写入目标;TTY 且无 human decisions、无活跃 DAG/Worker 时可经明确 `y/yes` 同意后应用 deterministic safe actions;只有 model merge 时直接输出有边界指引,不询问“应用 0 个动作”。notifier 遵循全局 `--repo-root`、不写 stdout、不改变原命令退出码,CI、禁用环境变量、help/version、`--json`/`--markdown`、`init`、`run-dag`、`dag`、`loop`、`delegate`、`pi-prompt`、`cursor-prompt` 均跳过。
52
- - 新增统一命令 `loop-agent init reconcile --repo-root <target>`:surface 缺失返回 `needs-baseline` 且零写入,存在 human decisions 返回 `needs-human-decision` 且零写入,活跃 DAG/Worker 或 Worker 状态无法确认时返回 `blocked-active-runtime` 且零写入;其余情况复用现有 `applyInitUpdate({ applySafe: true })` 并复查返回 `clean`/`needs-model-merge`/`needs-safe-update`,支持 `--json`/`--markdown`。
53
- - 测试报告新增 L-5 指标消费与 Python coverage.py JSON、Java JaCoCo XML 的统一 Code Coverage v1 报告入口,覆盖通过率、AC/自动化覆盖率、稳定性、失败原因、缺陷、风险和回归建议;缺失覆盖率证据保持 `unavailable`,不改变 backend-test outcome gate。
54
- - 现有 backend-test DAG 在 retrospective 之后新增独立的 `l5-metrics-pi` 指标节点;它基于前序测试报告和 run-owned 证据输出 `L-5 ready/not-ready`,不创建独立 L-5 DAG,也不改变 Result v1 outcome gate。
55
- - 后端测试 DAG 从 38 个收敛为 24 个真实顶层节点;使用 fail-closed `runIf` 和复合 Shell capability 减少调度,同时保留双合同、Manifest、语义评审、JUnit、Result、分类、修复安全、追踪和最终 outcome 证据。三条可选修订/修复分支仍各最多执行一次。
56
- - Backend Test Analysis、Case Manifest、Semantic Review 与 Classification 的严格 JSON 契约进一步对齐,生成和修订节点明确字段白名单、`sourceBinding` 与 evidence gap 约束,避免模型自定义字段导致确定性门禁失败。
57
-
58
- ### 修复
59
-
60
- - Init evolution 高影响校验现在覆盖升级后的 init surface notifier、eligibility policy、runtime activity gate 与 CLI entry 接线;这些专用路径单独变化时会触发 `model-review`,不再以 `no trigger matched` 静默跳过审查,同时不扩大到通用 CLI program/command registry 文件。
20
+ - 优化操作与观测统一界面的导航与布局,合并确认与运行为「开始运行」,DAG 详情跳转路径更加准确
61
21
 
62
- - Windows DAG Shell/verification 的 clean environment 现在保留 `APPDATA`,使 Python 3.12/pip 可按系统规则发现用户级 `site-packages` 与其中的 pytest;仍不会继承完整宿主环境或透传未授权 token/credential,backend-test 无需硬编码用户路径。
63
- - Observe 节点检查器的“规范证据”现在会按任务目录解析 `sourceBinding` 中的相对路径,`source/需求.md` 等已绑定文件可正常打开;路径成员校验、目录穿越、绝对路径、二进制文件和符号链接逃逸防护保持不变。
64
- - Decision Gate 的 Pi 执行指令不再与输出合同冲突:启用 `decisionGate` 的只读节点会明确输出唯一的 `DECISION_ENVELOPE_JSON` fenced block,普通只读节点仍保持原有无代码围栏约束,严格解析规则不放宽。
65
-
66
- - Backend-test Analysis v2 intake 现在可确定性归一化模型常见的外部依赖 `type` / `target` 与风险 `type` 字段:dependency `target` 映射为 canonical `name`,扩展字段不落盘;dependency/risk 仍使用类别化白名单,未知字段和 `risks[].target` 继续 fail-closed,避免节点 02 因合法近似形态过早中断。
67
-
68
- - Backend-test 唯一 JSON object 提取器在扫描裸对象前会等长忽略完整 Markdown 行内代码中的对象形状示例(如 `{error:{code,message,type}}`);唯一真实 envelope 可正常物化,多个真实 JSON、fence 外额外对象与未闭合对象继续 fail-closed。
69
- - Backend-test 节点 10 `backend-test-semantic-gate-shell` 不再用 Markdown `first-verdict-line` 读取 raw Pi 输出;改为同一 `semantic-initial` pipeline 按 task id 只读 `contracts/backend-test-semantic-review.json`,仅 `verdict=pass` 放行 pytest,`request-revision`/缺失/malformed 清晰 fail-closed;节点 09 仍只负责 materialize+trace,15 节点与单次 pytest 不变。
70
- - 公开 Case Manifest schema(`docs/templates/backend-test-case-manifest.schema.json`)对齐 materializer dual-scope coverage:`coverageSummary` 在 `additionalProperties:false` 下 required 并声明 `global`/`inScope`(`total`/`covered`/`gapped`/`ratio`)与 `crossDomainGapCount`;回归测试以无依赖轻量 contract 锁定 schema↔canonical 一致性;不改 materializer/15 节点。
71
- - Backend-test Case Manifest materializer 将模型顶层 `coverageSummary` 视为非权威:secret 扫描后剥离,再 strict parse 其余字段,并始终 `computeCaseManifestCoverageSummary` 写入 canonical summary;my-webapp 自定义/部分 summary 不再导致 invalid-output;prompt 禁止模型输出 coverageSummary;15 节点与单次 pytest 不变。
72
- - Backend-test 裸 object 路径现消费 scanner 的 `unclosedObject`:当文本含一个完整裸 JSON object,且其前或后仍有未闭合裸 object 时一律 fail-closed,不再因 `spans.length === 1` 错误接受完整对象;Semantic Review / Classification / contracts envelope 共用该行为。
73
- - Backend-test contracts composite pipeline (`validate-backend-test-contracts-shell`) 使用唯一确定性 envelope 提取:接受 pure JSON、唯一 fenced json、prose+唯一 fence、prose+唯一括号平衡裸 object;对无 JSON、多候选、未闭合、非 object、缺 `analysis`/`execution` fail-closed。提取后仍走 Analysis v2 / Execution v1 schema、source-binding 与 secret 门禁;不引入全局 Pi output rewrite,不将 `invalid-output` 加入自动 retry,不改 15 节点拓扑。
74
- - Backend-test Semantic Review(08→09)与 Classification(12→13)materializer 复用同一 `extractUniqueJsonObject` scanner:接受 pure / whole-fence / prose+唯一 fence / prose+唯一裸 object;多候选、未闭合、非 object 与 strict schema 错误继续 fail-closed;不改 Case Manifest、Pi SDK/retry 或 15 节点拓扑。
75
- - 节点检查器「执行过程」中的事件时间戳现在同时显示本地日期与 24 小时制 `HH:mm:ss`(例如 `2026/7/16 08:09:10`),不再被裁掉时间部分;缺少时间戳时仍显示「未记录时间」,事件字段优先级(`timestamp` → `at` → `recordedAt`)保持不变。
76
- - `frontend-test` case review 现在是 fail-closed browser gate:只有 `VERDICT: pass` 才能物化 manifest 并启动动态 browser map;生成的 case 固定使用默认 browser session,要求每个子场景的 fixture/UI reset 和 fresh snapshot,并由确定性节点校验每个 case 的 `execution.md`、`case-result.json`、`caseId`、`status`、`evidencePaths` 及 blocked `blockedReason`。
77
- - 前端实现计划/修订节点会注入当前包内权威 `frontend-implementation-contract-v1` Schema 与固定 source binding,避免模型猜测字段导致契约门禁失败。
78
- - 前端 Mock 策略节点的 canonical 输出会把首条 `MOCK_STRATEGY:` 协议行提升为第一行,避免解释性前言触发 `first-non-empty` 门禁误判。
79
- - 前端规范回退目录统一为本地 `openspec/`,DAG 能力发现、提示词、Skill 与验证证据检查不再查找大小写不一致的旧目录名。
80
- - 后端测试复合 Shell pipeline 现在与普通 Shell 节点共享 Git write guard;即使命令退出成功,只要越过 `read-only`、`allowedPaths` 或 `forbiddenPaths` 边界,节点仍会 fail-closed。
81
- - Observe 对新 backend-test run 不再伪造 repair attempt/status,只投影 single-run/canonical Result、分类与 coverage;历史 24 节点 run 的 eligibility、repair、final 和 effective evidence 仍可兼容读取。
82
- - Pi SDK 对缺少响应 ID 的累计 Token 生命周期事件改为取本次执行最大快照,避免同一响应的匿名 usage 被重复累加。
83
- - Pi SDK 执行长推理或大段结构化输出时不再把高频流式增量事件无界累积到内存;同一响应在多个生命周期事件中重复出现的 Token 用量只统计一次,避免 `Invalid string length` 和成本数据虚高。
84
- - 后端测试复合执行节点继续保持 clean environment、失败分类和 fail-closed outcome,并为唯一 JUnit/initial Result、canonical Result、traceability 与 Observe 投影保留结构化运行证据。
22
+ ### 修复
23
+
24
+ - 修复执行计划索引将命名约定占位符(如 `YYYY-MM-DD-<topic>.md`)误判为缺失计划引用的问题
25
+
26
+ ## [0.18.1] - 2026-07-22
27
+
28
+ ### 重点更新
29
+
30
+ - 放宽后端测试 Markdown 用例的映射与验收标准校验,减少合法用例被误拦截的情况
31
+ - 修复文档审计报错未阻断 CI 的治理漏洞,并全面修正失效链接
32
+ - 清理发布日志中残留的已发布记录,避免将已上线能力误展示为未发布
33
+
34
+ ### 修复
35
+
36
+ - 放宽后端测试 Markdown 用例的每条补充 Case 必须包含 AC 标识的硬性要求,改为确保任务要求的业务 AC 场景至少被一条最终用例覆盖即可,整体缺失必需 AC 时仍会严格阻断
37
+ - 放宽后端测试 traceability 映射校验,Case ID 可位于测试函数名、装饰器、函数体或文档字符串区域,不再强制首行文档字符串或严格一对一映射,避免重复映射等情况导致的误阻断
38
+ - 修复文档审计在发现错误时未返回非零退出码导致 CI 假绿的问题,并将链接门禁覆盖范围扩展至完整的 docs/ 目录
39
+ - 修正文档报告分类迁移后遗留的 269 条失效链接
40
+ - 修复自动发布创建版本区块时未正确消费已发布内容并清理残留记录的问题,移除了随 0.18.0 版本发布的 59 条残留日志
85
41
 
86
42
  ## [0.18.0] - 2026-07-22
87
43
 
package/README.md CHANGED
@@ -85,24 +85,22 @@ loop-agent dag doctor --run-id <run-id> --markdown
85
85
  loop-agent dag report --run-id <run-id>
86
86
  ```
87
87
 
88
- ### Official 本地操作面(CLI + Console + 独立 Observe
88
+ ### Official 本地操作面(CLI + 统一 Operator Console
89
89
 
90
90
  | 入口 | 命令 | 角色 |
91
91
  | --- | --- | --- |
92
92
  | 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
93
- | Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790`) |
94
- | Observe | `agent-worker observe serve --repo . --port 8787` | 独立只读看板 |
93
+ | Loop Operator Console | `agent-worker console serve --repo .` | Official 本地控制面(默认 `127.0.0.1:8790`,含 Operate 与 Inspect) |
94
+ | Observe | `agent-worker observe serve --repo . --port 8787` | 兼容期独立只读入口;不再作为推荐路径 |
95
95
 
96
96
  ```bash
97
97
  # 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
98
98
  agent-worker console serve --repo .
99
99
  agent-worker console doctor --repo .
100
100
 
101
- # 终端 B:独立 Observe(不经 Console mount/proxy)
102
- agent-worker observe serve --repo . --port 8787
103
101
  ```
104
102
 
105
- 访问 Console <http://127.0.0.1:8790/> Observe <http://127.0.0.1:8787/>。Observe 只读运行事实;Console 通过 versioned health(同仓 `repoFingerprint` + route capability)fail-closed 构建深链。openCode 等主会话仍是 **Compatibility / Operator Assist**,不得升级为与 Official 同等保证。
103
+ 访问 Console <http://127.0.0.1:8790/>;顶部「检视」以及 `/inspect/` 提供原 Observe 的只读 Dashboard、DAG、时间线与 artifacts。需要保留的旧书签或脚本仍可在兼容窗口使用 Observe `:8787`。openCode 等主会话仍是 **Compatibility / Operator Assist**,不得升级为与 Official 同等保证。
106
104
 
107
105
  ## 核心边界
108
106
 
@@ -2,4 +2,5 @@ import { auditDocs } from '../governance/checks.js';
2
2
  export async function runDocsAudit(repoRoot) {
3
3
  const result = await auditDocs(repoRoot);
4
4
  console.log(JSON.stringify(result, null, 2));
5
+ process.exitCode = result.summary.errorCount > 0 ? 1 : 0;
5
6
  }
@@ -783,13 +783,13 @@ function buildManagedAgentsBlock(input) {
783
783
  "",
784
784
  "### 运行看板(只读)",
785
785
  "",
786
- "启动本地只读运行看板:",
786
+ "启动统一 Operator Console(含只读 Inspect):",
787
787
  "",
788
788
  "```bash",
789
- "agent-worker observe serve --repo . --port 8787",
789
+ "agent-worker console serve --repo . --port 8790",
790
790
  "```",
791
791
  "",
792
- "浏览器打开 `http://127.0.0.1:8787/`。看板只读,默认只绑定本机 `127.0.0.1`;不要直接暴露到公开网络。",
792
+ "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;可用 `--host 0.0.0.0` 做局域网访问(无网络鉴权,仅可信网络),`--debug` 输出请求日志。不要直接暴露到公开网络。`agent-worker observe serve` 仅为兼容入口。",
793
793
  "",
794
794
  "### DAG 诊断与收口",
795
795
  "",
@@ -177,6 +177,10 @@ function localIndexReference(rawReference) {
177
177
  .replaceAll("\\", "/");
178
178
  if (!normalized.endsWith(".md") || normalized.includes("/"))
179
179
  return null;
180
+ // Naming-convention placeholders (e.g. `YYYY-MM-DD-<topic>.md`) and other
181
+ // unsafe stems are documentation, not index entries. Align with assertSafePlanId.
182
+ if (/[<>:"|?*]/.test(normalized))
183
+ return null;
180
184
  return normalized;
181
185
  }
182
186
  function extractIndexReferences(indexContent) {
@@ -559,9 +559,9 @@ export function buildAgentWorkerProgram() {
559
559
  .command("serve")
560
560
  .requiredOption("--repo <repo-root>", "Target repo root")
561
561
  .option("--port <port>", "HTTP port", "8787")
562
- .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no auth)", "127.0.0.1")
562
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
563
563
  .option("--debug", "Log request source/method/path/status to stderr")
564
- .description("Start read-only observe HTTP server")
564
+ .description("Start read-only observe HTTP server (compat entry; prefer console serve)")
565
565
  .action(async (options) => {
566
566
  const repoRoot = path.resolve(options.repo);
567
567
  const server = await createObserveServer({
@@ -570,6 +570,9 @@ export function buildAgentWorkerProgram() {
570
570
  port: Number.parseInt(options.port, 10),
571
571
  debug: options.debug === true,
572
572
  });
573
+ process.stderr.write("[OBSERVE_SERVE_DEPRECATED] agent-worker observe serve 作为独立看板入口将在兼容窗口后下线。\n" +
574
+ "请改用:agent-worker console serve --repo <repo> --port 8790\n" +
575
+ "统一 Operator Console 已包含原 Observe 的只读检视能力:http://127.0.0.1:8790/\n");
573
576
  process.stdout.write(`${server.url}\n`);
574
577
  await new Promise((resolve) => {
575
578
  const shutdown = () => resolve();
@@ -593,22 +596,18 @@ export function buildAgentWorkerProgram() {
593
596
  .command("serve")
594
597
  .requiredOption("--repo <repo-root>", "Target repo root")
595
598
  .option("--port <port>", "HTTP port", "8790")
596
- .option("--host <host>", "Bind host (default 127.0.0.1; non-loopback is rejected)", "127.0.0.1")
599
+ .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
600
+ .option("--debug", "Log request source/method/path/status to stderr")
597
601
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
598
- .description("Start Loop Operator Console HTTP server (static SPA + Operator API)")
602
+ .description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
599
603
  .action(async (options) => {
600
604
  const { createConsoleServer } = await import("./console/server.js");
601
- const { isLoopbackHost } = await import("./console/loopback.js");
602
- if (!isLoopbackHost(options.host)) {
603
- process.stderr.write(`console serve: non-loopback host "${options.host}" is not allowed (fail closed)\n`);
604
- process.exitCode = 1;
605
- return;
606
- }
607
605
  const repoRoot = path.resolve(options.repo);
608
606
  const server = await createConsoleServer({
609
607
  repoRoot,
610
608
  host: options.host,
611
609
  port: Number.parseInt(options.port, 10),
610
+ debug: options.debug === true,
612
611
  appDataRoot: options.appData
613
612
  ? path.resolve(options.appData)
614
613
  : undefined,
@@ -625,13 +624,15 @@ export function buildAgentWorkerProgram() {
625
624
  .command("doctor")
626
625
  .requiredOption("--repo <repo-root>", "Target repo root")
627
626
  .option("--json", "Emit JSON report on stdout")
628
- .option("--observe-url <url>", "Optional Observe base URL to probe (non-blocking)", "http://127.0.0.1:8787")
629
- .description("Report sibling loop-agent identity, repo preflight, and optional Observe health")
627
+ .option("--observe-url <url>", "Legacy external Observe base URL to probe (non-blocking)")
628
+ .option("--console-url <url>", "Unified Console base URL to probe")
629
+ .description("Report sibling identity, preflight, and unified Inspect readiness")
630
630
  .action(async (options) => {
631
631
  const { formatConsoleDoctorHuman, runConsoleDoctor } = await import("./console/doctor.js");
632
632
  const report = await runConsoleDoctor({
633
633
  repoRoot: path.resolve(options.repo),
634
634
  observeBaseUrl: options.observeUrl,
635
+ consoleBaseUrl: options.consoleUrl,
635
636
  });
636
637
  if (options.json) {
637
638
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
@@ -1,3 +1,4 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdtemp } from "node:fs/promises";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -7,6 +8,8 @@ import { probePiReadiness, } from "./pi-readiness.js";
7
8
  import { DEFAULT_OBSERVE_BASE_URL, OBSERVE_START_COMMAND, probeAndClassifyObserve, } from "./observe-health-match.js";
8
9
  import { repoFingerprintV1 } from "./repo-fingerprint.js";
9
10
  import { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
11
+ import { defaultObserveStaticDir, ROUTES } from "../observe/routes.js";
12
+ import { deriveObserveRouteCapabilities } from "../observe/health.js";
10
13
  async function probeObserve(url, localFingerprint, fetchImpl) {
11
14
  const classified = await probeAndClassifyObserve({
12
15
  baseUrl: url,
@@ -42,6 +45,48 @@ async function probeObserve(url, localFingerprint, fetchImpl) {
42
45
  message: classified.message,
43
46
  };
44
47
  }
48
+ async function probeInspect(consoleBaseUrl, fetchImpl) {
49
+ const routeCapabilities = deriveObserveRouteCapabilities(ROUTES);
50
+ if (!consoleBaseUrl) {
51
+ const ready = existsSync(path.join(defaultObserveStaticDir(), "index.html"));
52
+ return {
53
+ status: ready ? "ready" : "degraded",
54
+ source: "static-assets",
55
+ routeCapabilities,
56
+ message: ready
57
+ ? "Inspect 静态资源可用;启动 agent-worker console serve 以提供统一入口"
58
+ : "Inspect 静态资源缺失;请运行构建或检查发布包",
59
+ };
60
+ }
61
+ const url = `${consoleBaseUrl.replace(/\/+$/, "")}/api/health`;
62
+ try {
63
+ const response = await fetchImpl(url, {
64
+ method: "GET",
65
+ signal: AbortSignal.timeout(1500),
66
+ });
67
+ const body = (await response.json());
68
+ const ready = response.ok &&
69
+ body.ok === true &&
70
+ body.schemaVersion === 1 &&
71
+ body.inspect?.ready === true;
72
+ return {
73
+ status: ready ? "ready" : "degraded",
74
+ source: "console-health",
75
+ url,
76
+ routeCapabilities: body.inspect?.routeCapabilities ?? routeCapabilities,
77
+ message: ready ? undefined : "Console health 未确认 Inspect 已就绪",
78
+ };
79
+ }
80
+ catch (error) {
81
+ return {
82
+ status: "degraded",
83
+ source: "console-health",
84
+ url,
85
+ routeCapabilities,
86
+ message: error instanceof Error ? error.message : String(error),
87
+ };
88
+ }
89
+ }
45
90
  /**
46
91
  * Run console doctor checks. Observe unavailability never fails the report.
47
92
  */
@@ -115,8 +160,14 @@ export async function runConsoleDoctor(options) {
115
160
  message: "cannot run preflight without sibling loop-agent bin",
116
161
  };
117
162
  }
118
- const observeBase = options.observeBaseUrl ?? DEFAULT_OBSERVE_BASE_URL;
119
- const observe = await probeObserve(observeBase, fingerprint, options.fetchImpl ?? fetch);
163
+ const inspect = await probeInspect(options.consoleBaseUrl, options.fetchImpl ?? fetch);
164
+ const observe = options.observeBaseUrl
165
+ ? await probeObserve(options.observeBaseUrl, fingerprint, options.fetchImpl ?? fetch)
166
+ : {
167
+ status: "unavailable",
168
+ url: `${DEFAULT_OBSERVE_BASE_URL}/api/health`,
169
+ message: "未探测兼容 Observe;统一 Inspect 不依赖 8787",
170
+ };
120
171
  const pi = await probePiReadiness({
121
172
  override: options.piReadinessProbe,
122
173
  siblingControllerOk: Boolean(controllerIdentity && "packageVersion" in controllerIdentity),
@@ -133,6 +184,7 @@ export async function runConsoleDoctor(options) {
133
184
  piReadiness: pi.state,
134
185
  pi,
135
186
  observe,
187
+ inspect,
136
188
  generatedAt: new Date().toISOString(),
137
189
  };
138
190
  }
@@ -148,6 +200,7 @@ export function formatConsoleDoctorHuman(report) {
148
200
  `pi.interviewSession: ${report.pi.probe.interviewSessionOk}`,
149
201
  `preflight: ${report.preflight.ok ? `ok (${report.preflight.summary})` : `fail ${report.preflight.code}: ${report.preflight.message}`}`,
150
202
  `observe: ${report.observe.status}${report.observe.status === "unavailable" || report.observe.status === "mismatch" ? ` (${report.observe.message})` : ""}`,
203
+ `inspect: ${report.inspect.status}${report.inspect.message ? ` (${report.inspect.message})` : ""}`,
151
204
  ];
152
205
  if (report.observe.status === "unavailable" ||
153
206
  report.observe.status === "mismatch") {
@@ -1,4 +1,5 @@
1
1
  export { createConsoleServer, defaultConsoleStaticDir } from "./server.js";
2
+ export { buildOperatorSurfaceHealthV1 } from "./operator-surface-health.js";
2
3
  export { runConsoleDoctor, formatConsoleDoctorHuman } from "./doctor.js";
3
4
  export { isLoopbackHost } from "./loopback.js";
4
5
  export { repoFingerprintV1, normalizeWorktreeRealpath, } from "./repo-fingerprint.js";
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Console-local loopback host check. Non-loopback binds fail closed
3
- * (unlike Observe, which only warns).
2
+ * Console-local loopback host check.
3
+ * Used for default bind safety messaging and mutation Host checks on loopback binds.
4
4
  */
5
5
  export function isLoopbackHost(host) {
6
6
  const normalized = host.trim().toLowerCase();
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Observe hash-route deep-link builder (Phase 1 skeleton).
3
- * Does not mount/proxy Observe or fetch its snapshot.
2
+ * Deprecated Observe deep-link compatibility helpers.
3
+ * New unified-surface callers should use buildInspectDeepLink.
4
4
  */
5
+ import { buildInspectUrl, selectionFromObserveLinkTarget, } from "./operator-selection.js";
5
6
  function stripTrailingSlash(baseUrl) {
6
7
  return baseUrl.replace(/\/+$/, "");
7
8
  }
@@ -31,3 +32,7 @@ export function buildObserveDeepLink(baseUrl, target) {
31
32
  }
32
33
  }
33
34
  }
35
+ /** Build a canonical same-origin Inspect URL for the unified Operator surface. */
36
+ export function buildInspectDeepLink(origin, target) {
37
+ return buildInspectUrl(origin, selectionFromObserveLinkTarget(target));
38
+ }
@@ -5,7 +5,7 @@ import { getTaskPaths } from "../../task/runtime.js";
5
5
  import { stageUtf8Text } from "./app-data.js";
6
6
  import { ALL_CONFIRMATION_CHALLENGES, hashDagBytes, } from "./dag-confirmation.js";
7
7
  import { capabilityForObserveTarget, DEFAULT_OBSERVE_BASE_URL, OBSERVE_START_COMMAND, probeAndClassifyObserve, } from "./observe-health-match.js";
8
- import { buildObserveDeepLink, } from "./observe-link.js";
8
+ import { buildInspectDeepLink, buildObserveDeepLink, } from "./observe-link.js";
9
9
  import { resolveLatestDagRunIdForTask } from "./resolve-dag-run-for-task.js";
10
10
  import { repoFingerprintV1 } from "./repo-fingerprint.js";
11
11
  import { scheduleOperation, } from "./operation-runner.js";
@@ -285,7 +285,8 @@ export async function dispatchOperatorAction(ctx, req) {
285
285
  };
286
286
  }
287
287
  case "observeLink": {
288
- const baseUrl = ctx.observeBaseUrl ?? DEFAULT_OBSERVE_BASE_URL;
288
+ const consoleOrigin = ctx.getConsoleOrigin?.();
289
+ const baseUrl = ctx.observeBaseUrl ?? consoleOrigin ?? DEFAULT_OBSERVE_BASE_URL;
289
290
  const dagRunId = str(p.dagRunId);
290
291
  const taskId = str(p.taskId);
291
292
  const featureId = str(p.featureId);
@@ -320,6 +321,33 @@ export async function dispatchOperatorAction(ctx, req) {
320
321
  const requiredCapability = target
321
322
  ? capabilityForObserveTarget(target)
322
323
  : undefined;
324
+ const usesUnifiedSurface = Boolean(consoleOrigin) &&
325
+ (!ctx.observeBaseUrl ||
326
+ ctx.observeBaseUrl.replace(/\/+$/, "") ===
327
+ consoleOrigin?.replace(/\/+$/, ""));
328
+ if (usesUnifiedSurface) {
329
+ const inspectPath = target
330
+ ? buildInspectDeepLink(consoleOrigin, target).slice(consoleOrigin.length)
331
+ : "/inspect/#/";
332
+ return {
333
+ kind: "sync",
334
+ status: 200,
335
+ body: operatorSucceeded("observeLink", {
336
+ url: target
337
+ ? buildInspectDeepLink(consoleOrigin, target)
338
+ : `${consoleOrigin.replace(/\/+$/, "")}/inspect/#/`,
339
+ inspectPath,
340
+ status: "same-origin",
341
+ requiredCapability: requiredCapability ?? null,
342
+ dagRunId: target?.kind === "dag"
343
+ ? target.dagRunId
344
+ : resolvedFromTask?.dagRunId,
345
+ taskId: taskId ?? undefined,
346
+ home: openHome || undefined,
347
+ fallbackToTask: fallbackToTask || undefined,
348
+ }),
349
+ };
350
+ }
323
351
  let localFingerprint;
324
352
  try {
325
353
  localFingerprint = repoFingerprintV1(ctx.repoRoot);
@@ -0,0 +1,92 @@
1
+ function segment(value) {
2
+ return encodeURIComponent(value);
3
+ }
4
+ function nonEmpty(value) {
5
+ return typeof value === "string" && value.length > 0;
6
+ }
7
+ export function encodeInspectHash(selection) {
8
+ switch (selection.kind) {
9
+ case "feature":
10
+ return `#/feature/${segment(selection.featureId)}`;
11
+ case "feature-task":
12
+ return `#/feature/${segment(selection.featureId)}/task/${segment(selection.taskId)}`;
13
+ case "legacy-task":
14
+ return `#/task/${segment(selection.taskId)}`;
15
+ case "dag-run":
16
+ // Inspect currently owns DAG-level routing. nodeId remains URL-adjacent
17
+ // selection state until the static router explicitly supports it.
18
+ return `#/dag/${segment(selection.dagRunId)}`;
19
+ case "worker-run":
20
+ return `#/run/${segment(selection.workerRunId)}`;
21
+ case "batch":
22
+ return `#/batch/${segment(selection.batchRunId)}`;
23
+ }
24
+ }
25
+ export function buildInspectPath(selection) {
26
+ return `/inspect/${selection ? encodeInspectHash(selection) : "#/"}`;
27
+ }
28
+ export function buildInspectUrl(origin, selection) {
29
+ const base = origin.trim().replace(/\/+$/, "");
30
+ if (!base)
31
+ throw new Error("buildInspectUrl: origin is required");
32
+ return `${base}${buildInspectPath(selection)}`;
33
+ }
34
+ function decodeSegment(value) {
35
+ if (!nonEmpty(value))
36
+ return null;
37
+ try {
38
+ const decoded = decodeURIComponent(value);
39
+ return decoded ? decoded : null;
40
+ }
41
+ catch {
42
+ return null;
43
+ }
44
+ }
45
+ export function decodeInspectHash(hash) {
46
+ const normalized = hash.startsWith("#") ? hash.slice(1) : hash;
47
+ const parts = normalized.split("/").filter(Boolean);
48
+ if (parts.length === 0)
49
+ return null;
50
+ const [kind, first, third, fourth] = parts;
51
+ const id = decodeSegment(first);
52
+ if (!id)
53
+ return null;
54
+ if (kind === "feature" && parts.length === 2) {
55
+ return { kind: "feature", featureId: id };
56
+ }
57
+ if (kind === "feature" && parts.length === 4 && third === "task") {
58
+ const taskId = decodeSegment(fourth);
59
+ return taskId ? { kind: "feature-task", featureId: id, taskId } : null;
60
+ }
61
+ if (kind === "task" && parts.length === 2)
62
+ return { kind: "legacy-task", taskId: id };
63
+ if (kind === "dag" && parts.length === 2)
64
+ return { kind: "dag-run", dagRunId: id };
65
+ if (kind === "run" && parts.length === 2)
66
+ return { kind: "worker-run", workerRunId: id };
67
+ if (kind === "batch" && parts.length === 2)
68
+ return { kind: "batch", batchRunId: id };
69
+ return null;
70
+ }
71
+ export function selectionFromObserveLinkTarget(target) {
72
+ switch (target.kind) {
73
+ case "dag":
74
+ return { kind: "dag-run", dagRunId: target.dagRunId };
75
+ case "task":
76
+ return { kind: "legacy-task", taskId: target.taskId };
77
+ case "feature":
78
+ return { kind: "feature", featureId: target.featureId };
79
+ case "worker":
80
+ return { kind: "worker-run", workerRunId: target.workerRunId };
81
+ case "batch":
82
+ return { kind: "batch", batchRunId: target.batchRunId };
83
+ }
84
+ }
85
+ /** Canonicalize only the historical Console root hash shape. */
86
+ export function normalizeLegacyConsoleDagHash(pathnameAndHash) {
87
+ const [pathname, hash = ""] = pathnameAndHash.split("#", 2);
88
+ if ((pathname === "/" || pathname === "") && /^\/dag\/[^/]+$/.test(hash)) {
89
+ return `/inspect/#${hash}`;
90
+ }
91
+ return pathnameAndHash;
92
+ }
@@ -0,0 +1,23 @@
1
+ import { deriveObserveRouteCapabilities, resolveObservePackageIdentity, } from "../observe/health.js";
2
+ import { repoFingerprintV1 } from "./repo-fingerprint.js";
3
+ export function buildOperatorSurfaceHealthV1(input) {
4
+ const { packageName, packageVersion } = resolveObservePackageIdentity();
5
+ const snapshotAvailable = input.routes.some((route) => route.method === "GET" && route.pattern.test("/api/snapshot"));
6
+ const eventStreamAvailable = input.routes.some((route) => route.method === "GET" && route.pattern.test("/api/events/stream"));
7
+ return {
8
+ schemaVersion: 1,
9
+ ok: true,
10
+ product: "loop-operator-console",
11
+ repoFingerprint: repoFingerprintV1(input.repoRoot),
12
+ packageName,
13
+ packageVersion,
14
+ piReadiness: input.piReadiness,
15
+ inspect: {
16
+ ready: input.inspectReady,
17
+ routeCapabilities: deriveObserveRouteCapabilities(input.routes),
18
+ snapshotAvailable,
19
+ eventStreamAvailable,
20
+ },
21
+ generatedAt: input.generatedAt ?? new Date().toISOString(),
22
+ };
23
+ }
@@ -38,9 +38,9 @@ const CTA = {
38
38
  },
39
39
  observeLink: {
40
40
  id: "observeLink",
41
- label: "打开 Observe 深链",
41
+ label: "打开检视",
42
42
  action: "observeLink",
43
- commandHint: "agent-worker observe serve --repo . --port 8787",
43
+ commandHint: "agent-worker console serve --repo . --port 8790",
44
44
  },
45
45
  };
46
46
  const MATRIX = {