@tea-agent/loop-agent 0.18.1 → 0.20.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 (52) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +33 -0
  3. package/README.md +4 -6
  4. package/dist/application/dag/generate-task-dag.js +12 -1
  5. package/dist/commands/init.js +3 -3
  6. package/dist/executors/shell-executor.js +188 -2
  7. package/dist/governance/exec-plans.js +4 -0
  8. package/dist/worker/cli.js +13 -12
  9. package/dist/worker/console/doctor.js +55 -2
  10. package/dist/worker/console/index.js +1 -0
  11. package/dist/worker/console/loopback.js +2 -2
  12. package/dist/worker/console/observe-link.js +7 -2
  13. package/dist/worker/console/operator-actions.js +30 -2
  14. package/dist/worker/console/operator-selection.js +92 -0
  15. package/dist/worker/console/operator-surface-health.js +23 -0
  16. package/dist/worker/console/recovery-cta.js +2 -2
  17. package/dist/worker/console/routes.js +45 -19
  18. package/dist/worker/console/security.js +29 -4
  19. package/dist/worker/console/server.js +106 -8
  20. package/dist/worker/console/static/assets/index-3vsjZJHq.js +16 -0
  21. package/dist/worker/console/static/assets/index-i1wV4LrY.css +1 -0
  22. package/dist/worker/console/static/index.html +2 -2
  23. package/dist/worker/observe/routes.js +63 -21
  24. package/dist/worker/observe/server.js +3 -10
  25. package/dist/worker/observe/static/index.html +6 -3
  26. package/dist/worker/observe/static/styles.css +53 -0
  27. package/dist/workflows/dag/backend-test-markdown-workflow.js +163 -41
  28. package/dist/workflows/dag/backend-test-result-contract.js +30 -7
  29. package/dist/workflows/dag/frontend-prewrite-gate.js +77 -0
  30. package/dist/workflows/dag/frontend-repair.js +7 -1
  31. package/dist/workflows/dag/frontend-review-context.js +43 -0
  32. package/dist/workflows/dag/frontend-verification-trace.js +34 -15
  33. package/dist/workflows/dag/governance-profile.js +14 -6
  34. package/dist/workflows/dag/init-hybrid.js +143 -399
  35. package/dist/workflows/dag/types.js +30 -0
  36. package/dist/workflows/dag/validate.js +22 -1
  37. package/docs/README.md +4 -2
  38. package/docs/architecture/evolution.md +6 -6
  39. package/docs/architecture/worker-and-feature.md +9 -9
  40. package/docs/templates/agent-dag.schema.json +40 -0
  41. package/docs/templates/backend-test-dag.generate-pytest.prompt.md +23 -192
  42. package/docs/templates/backend-test-dag.json +8 -8
  43. package/docs/templates/backend-test-dag.review-cases.prompt.md +22 -75
  44. package/package.json +1 -1
  45. package/skills/agent-worker/references/agent-worker-operator.md +2 -2
  46. package/skills/frontend-implementation/references/node-contracts.md +6 -8
  47. package/skills/frontend-review/SKILL.md +5 -8
  48. package/skills/loop-agent/references/command-reference.md +4 -2
  49. package/skills/loop-agent/references/harness-policy.md +1 -1
  50. package/skills/loop-agent/references/hybrid-dag.md +2 -2
  51. package/dist/worker/console/static/assets/index-KUSib7aM.js +0 -16
  52. 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,6 +2,39 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.20.0] - 2026-07-23
6
+
7
+ ### 新增
8
+
9
+ - streamline implementation DAG (#61)
10
+ - improve human-readable artifacts
11
+
12
+ ### 修复
13
+
14
+ - relax traceability mapping
15
+
16
+ ## [0.19.0] - 2026-07-23
17
+
18
+ ### 重点更新
19
+
20
+ - Operator Console 统一整合 Operate 与 Inspect,在同一服务即可检视只读 Dashboard、DAG、时间线与 artifacts
21
+ - 原 Observe 服务进入兼容期,仍可运行但会输出迁移提示,引导平滑过渡到统一控制台
22
+
23
+ ### 新增
24
+
25
+ - 新增统一 Operator Console:`console serve` 同时提供 Operate 与 Inspect,支持通过 `/inspect/` 直接检视运行详情与 DAG
26
+ - Console 健康检查接口升级,包含 Inspect 就绪状态与路由能力信息
27
+ - `console serve` 新增局域网绑定(`--host 0.0.0.0`)与调试日志(`--debug`)选项,并在非本地访问时保留安全校验
28
+
29
+ ### 改进
30
+
31
+ - `frontend-implementation` 保留独立 contract/scout,并把 Mock 策略与 implementation contract 合入 plan,把写前授权、验证/失败评估、复验和 review context 分别收敛为组合 shell 节点。standard/high-risk 固定 15 个顶层节点、small-risk 13 个;绿色路径执行 11 个节点、7 次 Pi,同时保留唯一 prewrite 写入授权、同 writeSet repair、真实 diff review 与 fail-closed closeout。
32
+ - 优化操作与观测统一界面的导航与布局,合并确认与运行为「开始运行」,DAG 详情跳转路径更加准确
33
+
34
+ ### 修复
35
+
36
+ - 修复执行计划索引将命名约定占位符(如 `YYYY-MM-DD-<topic>.md`)误判为缺失计划引用的问题
37
+
5
38
  ## [0.18.1] - 2026-07-22
6
39
 
7
40
  ### 重点更新
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
 
@@ -105,6 +105,17 @@ function isVerificationShellTask(task) {
105
105
  const commands = resolveShellCommands(task.shell);
106
106
  return commands.some((command) => /(vitest|npm run (lint|typecheck|test)|check-repo\.sh|loop-agent-standard-verify)/.test(command));
107
107
  }
108
+ function resolveReviewPacketShellCommands(task) {
109
+ const bundle = task.shell?.frontendVerificationBundle;
110
+ if (bundle) {
111
+ return [
112
+ ...bundle.mockCommands,
113
+ ...bundle.staticCommands,
114
+ ...bundle.behaviorCommands,
115
+ ];
116
+ }
117
+ return resolveShellCommands(task.shell);
118
+ }
108
119
  async function buildReviewPacket(input) {
109
120
  const spec = parseDagSpec(JSON.parse(await readFile(input.dagPath, "utf-8")));
110
121
  const writers = collectWriterTasksForPacket(spec).map((task) => {
@@ -123,7 +134,7 @@ async function buildReviewPacket(input) {
123
134
  .filter(isVerificationShellTask)
124
135
  .map((task) => ({
125
136
  nodeId: task.id,
126
- commands: resolveShellCommands(task.shell),
137
+ commands: resolveReviewPacketShellCommands(task),
127
138
  }));
128
139
  return {
129
140
  profileRouting: {
@@ -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
  "",
@@ -12,10 +12,12 @@ import { materializeFrontendTestResult, } from "../workflows/dag/frontend-test-r
12
12
  import { formatFrontendVerificationTraceStdout, runFrontendVerificationTraceGate, } from "../workflows/dag/frontend-verification-trace.js";
13
13
  import { formatFrontendWorktreeDiffStdout, runFrontendWorktreeDiffGate, } from "../workflows/dag/frontend-worktree-diff.js";
14
14
  import { formatFrontendFailureAssessStdout, formatFrontendRepairContractStdout, runFrontendFailureAssessGate, runFrontendRepairContractGate, } from "../workflows/dag/frontend-repair.js";
15
+ import { formatFrontendPrewriteGateStdout, runFrontendPrewriteGate, } from "../workflows/dag/frontend-prewrite-gate.js";
16
+ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from "../workflows/dag/frontend-review-context.js";
15
17
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
16
18
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
17
19
  import { materializeBackendTestResultFromRunDir, parseJunitXml } from "../workflows/dag/backend-test-result-contract.js";
18
- import { inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
20
+ import { collectBackendTestHumanCaseCatalog, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
19
21
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
20
22
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
21
23
  import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
@@ -356,14 +358,16 @@ async function executeBackendTestPipeline(input, meta) {
356
358
  if (![0, 1].includes(pytestExitCode))
357
359
  throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
358
360
  const parsed = parseJunitXml(junitContent);
361
+ const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
359
362
  const htmlContent = renderBackendTestHtml({
360
363
  title: meta.spec.title,
361
364
  parsed,
365
+ cases,
362
366
  environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
363
367
  traceabilitySummary: await readFile(path.join(reportsDir, "backend-test-traceability.md"), "utf8"),
364
368
  });
365
369
  const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
366
- const facts = renderBackendTestFacts({ parsed, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
370
+ const facts = renderBackendTestFacts({ parsed, cases, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
367
371
  const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
368
372
  const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
369
373
  outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `facts=${factsPath}`, facts);
@@ -601,8 +605,190 @@ async function executePipelineCommands(input, meta, overrideCommands) {
601
605
  }
602
606
  return results;
603
607
  }
608
+ async function executeFrontendVerificationBundle(input, meta) {
609
+ const started = Date.now();
610
+ const shell = input.task.shell;
611
+ const bundle = shell.frontendVerificationBundle;
612
+ const cwd = resolveShellCwd(input.cwd, shell.cwd);
613
+ const results = [];
614
+ let beforeStatus;
615
+ try {
616
+ beforeStatus = await readGitStatusPorcelain(input.cwd);
617
+ }
618
+ catch {
619
+ beforeStatus = undefined;
620
+ }
621
+ const groups = [
622
+ { name: "mock", commands: bundle.mockCommands },
623
+ { name: "static", commands: bundle.staticCommands },
624
+ { name: "behavior", commands: bundle.behaviorCommands },
625
+ ];
626
+ for (const group of groups) {
627
+ for (const command of group.commands) {
628
+ const commandNumber = results.length + 1;
629
+ const result = await executeShellCommand({
630
+ command,
631
+ cwd,
632
+ timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
633
+ envAllowlist: shell.envAllowlist,
634
+ dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
635
+ outputArtifacts: {
636
+ stdoutPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stdout.txt`),
637
+ stderrPath: path.join(meta.runDir, input.task.id, "commands", `${commandNumber}.stderr.txt`),
638
+ },
639
+ });
640
+ results.push(result);
641
+ if (!result.ok)
642
+ break;
643
+ }
644
+ }
645
+ if (beforeStatus !== undefined) {
646
+ const guard = await runShellWriteGuard({
647
+ rootCwd: input.cwd,
648
+ task: input.task,
649
+ beforeStatus,
650
+ });
651
+ if (!guard.ok) {
652
+ return {
653
+ ok: false,
654
+ stdout: "",
655
+ stderr: `write guard failed: ${guard.violations.join(", ")}`,
656
+ failureCategory: "write-guard",
657
+ durationMs: Date.now() - started,
658
+ };
659
+ }
660
+ }
661
+ await writeDagNodeTextArtifact(meta.runDir, input.task.id, "result.summary.md", buildShellResultSummaryMarkdown({
662
+ nodeId: input.task.id,
663
+ runId: meta.runId,
664
+ rootCwd: input.cwd,
665
+ results,
666
+ }));
667
+ const commandResults = results.map((result) => ({
668
+ ok: result.ok,
669
+ exitCode: result.exitCode,
670
+ failureCategory: result.failureCategory,
671
+ command: result.command,
672
+ }));
673
+ const firstFailure = results.find((result) => !result.ok);
674
+ let traceError;
675
+ try {
676
+ await runFrontendVerificationTraceGate({
677
+ runDir: meta.runDir,
678
+ workspaceRoot: input.cwd,
679
+ evidence: {
680
+ static: {
681
+ nodeId: input.task.id,
682
+ commandLabels: bundle.staticEvidence.commandLabels,
683
+ },
684
+ behavior: {
685
+ nodeId: input.task.id,
686
+ commandLabels: bundle.behaviorEvidence.commandLabels,
687
+ },
688
+ },
689
+ });
690
+ }
691
+ catch (error) {
692
+ traceError = error instanceof Error ? error : new Error(String(error));
693
+ }
694
+ if (bundle.mode === "repair") {
695
+ if (firstFailure || traceError) {
696
+ return {
697
+ ok: false,
698
+ stdout: summarizeCommandResults(results).stdout,
699
+ stderr: firstFailure?.stderr || traceError?.message || "frontend reverify failed",
700
+ failureCategory: firstFailure?.failureCategory ?? "invalid-output",
701
+ durationMs: Date.now() - started,
702
+ ...{ commandResults },
703
+ };
704
+ }
705
+ return {
706
+ ok: true,
707
+ stdout: "Frontend reverify bundle: pass",
708
+ stderr: "",
709
+ failureCategory: "success",
710
+ durationMs: Date.now() - started,
711
+ ...{ commandResults },
712
+ };
713
+ }
714
+ const failureFacts = [];
715
+ if (firstFailure) {
716
+ failureFacts.push({
717
+ nodeId: input.task.id,
718
+ record: {
719
+ status: "FINISHED",
720
+ failureCategory: firstFailure.failureCategory,
721
+ stdout: summarizeCommandResults(results).stdout,
722
+ stderr: firstFailure.stderr,
723
+ commandResults,
724
+ },
725
+ });
726
+ }
727
+ if (traceError) {
728
+ failureFacts.push({
729
+ nodeId: "frontend-verification-trace-shell",
730
+ record: {
731
+ status: "FINISHED",
732
+ failureCategory: "invalid-output",
733
+ stderr: traceError.message,
734
+ },
735
+ });
736
+ }
737
+ try {
738
+ const assessment = await runFrontendFailureAssessGate({
739
+ runDir: meta.runDir,
740
+ failureFacts,
741
+ });
742
+ await runFrontendRepairContractGate({ runDir: meta.runDir });
743
+ return {
744
+ ok: true,
745
+ stdout: formatFrontendFailureAssessStdout(assessment),
746
+ stderr: "",
747
+ failureCategory: "success",
748
+ durationMs: Date.now() - started,
749
+ ...{ commandResults },
750
+ };
751
+ }
752
+ catch (error) {
753
+ return {
754
+ ok: false,
755
+ stdout: "",
756
+ stderr: error instanceof Error ? error.message : String(error),
757
+ failureCategory: "invalid-output",
758
+ durationMs: Date.now() - started,
759
+ ...{ commandResults },
760
+ };
761
+ }
762
+ }
604
763
  export async function executeDagShellNode(input, meta) {
605
764
  const shell = input.task.shell;
765
+ if (shell?.frontendPrewriteGate) {
766
+ const started = Date.now();
767
+ try {
768
+ const result = await runFrontendPrewriteGate({
769
+ runDir: meta.runDir,
770
+ config: shell.frontendPrewriteGate,
771
+ sourceBinding: meta.spec.sourceBinding,
772
+ });
773
+ return { ok: true, stdout: formatFrontendPrewriteGateStdout(result), stderr: "", failureCategory: "success", durationMs: Date.now() - started };
774
+ }
775
+ catch (error) {
776
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
777
+ }
778
+ }
779
+ if (shell?.frontendVerificationBundle) {
780
+ return executeFrontendVerificationBundle(input, meta);
781
+ }
782
+ if (shell?.frontendReviewContext) {
783
+ const started = Date.now();
784
+ try {
785
+ const result = await runFrontendReviewContextGate({ runDir: meta.runDir, workspaceRoot: input.cwd });
786
+ return { ok: true, stdout: formatFrontendReviewContextStdout(result), stderr: "", failureCategory: "success", durationMs: Date.now() - started };
787
+ }
788
+ catch (error) {
789
+ return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
790
+ }
791
+ }
606
792
  if (shell?.backendTestPipeline) {
607
793
  return executeBackendTestPipelineWithWriteGuard(input, meta);
608
794
  }
@@ -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);