@tea-agent/loop-agent 0.24.9 → 0.24.11-beta.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 (31) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/commands/init.js +2 -16
  3. package/dist/executors/shell-executor.js +6 -122
  4. package/dist/worker/cli.js +14 -59
  5. package/dist/worker/delivery/git-transaction.js +7 -32
  6. package/dist/worker/delivery/verification-bundle.js +5 -26
  7. package/dist/worker/observe/routes.js +0 -17
  8. package/dist/worker/observe/static/api.js +0 -9
  9. package/dist/worker/observe/static/constants.js +0 -9
  10. package/dist/worker/observe/static/state.js +0 -14
  11. package/dist/worker/observe/static/styles.css +0 -74
  12. package/dist/worker/observe/static/views/dag-inspector.js +15 -371
  13. package/dist/worker/outcomes/projector.js +1 -5
  14. package/dist/worker/runner/run-ready.js +1 -41
  15. package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -75
  16. package/dist/workflows/dag/backend-test-result-contract.js +0 -103
  17. package/dist/workflows/dag/frontend-test-result-contract.js +41 -106
  18. package/dist/workflows/dag/init-hybrid.js +14 -13
  19. package/dist/workflows/dag/node-execution.js +2 -3
  20. package/dist/workflows/dag/types.js +0 -2
  21. package/dist/workflows/dag/validate.js +2 -3
  22. package/docs/architecture/worker-and-feature.md +11 -0
  23. package/docs/init-surface.manifest.json +3 -0
  24. package/docs/templates/agent-dag.schema.json +9 -0
  25. package/docs/templates/frontend-implementation-contract.schema.json +2 -2
  26. package/package.json +1 -1
  27. package/skills/frontend-bounded-implement/SKILL.md +53 -0
  28. package/skills/frontend-implementation/SKILL.md +3 -7
  29. package/skills/frontend-implementation/references/node-contracts.md +2 -2
  30. package/dist/worker/observe/dag-run-artifacts.js +0 -90
  31. package/dist/worker/observe/node-input.js +0 -444
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ### 改进
6
+
7
+ - Feature Delivery 在省略 `--qa-evidence` / `--final-verification` 时默认使用 `feature verify-final` 写入的 evidence 路径(ADR 0007)
8
+
9
+ ### 文档
10
+
11
+ - ADR 0007:Final Verification 唯一权威为 `agent-worker feature verify-final`;packet FINAL-VERIFY 仅 local smoke
12
+ - F-2026-005 design / FINAL-VERIFY TaskSpec 与发布列车状态对齐 I39 Delivery + 收口下一步 I40
13
+
14
+
15
+ ## [0.24.10] - 2026-07-29
16
+
17
+ ### 修复
18
+
19
+ - Closeout 重验不再要求 `should`/partial AC 绑定完整 run evidence(仅 `covered` must AC 强制 hash 绑定)
20
+
5
21
  ## [0.24.9] - 2026-07-29
6
22
 
7
23
  ### 重点更新
@@ -22,6 +38,12 @@
22
38
 
23
39
  - 修复前端证据校验在 Windows 下通过长 node -e 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
24
40
 
41
+ ## [0.24.9] - 2026-07-29
42
+
43
+ ### 修复
44
+
45
+ - Closeout 重验不再要求 `should`/partial AC 绑定完整 run evidence(仅 `covered` must AC 强制 hash 绑定)
46
+
25
47
  ## [0.24.8] - 2026-07-29
26
48
 
27
49
  ### 重点更新
@@ -41,6 +63,8 @@
41
63
 
42
64
  ### 修复
43
65
 
66
+ - 修复前端证据校验在 Windows 下通过长 `node -e` 命令执行时的终端转义崩溃:改为由 runtime 内部直接校验,兼容 CMD、Git Bash、PowerShell 与 Unix shell
67
+ - 修复 `frontend-implement-pi` 因复用需求发现 skill 而长时间侦查后空实现:contract 现在强制 requirement outcome 与 interaction 行为语义,implement/repair 改用不含递归 OpenSpec/AI workspace 搜索的 bounded skill,并由 `implementation-outcome-v1` 机器校验声明结果与实际 diff;只有显式 already-satisfied 才允许空 diff
44
68
  - Outcome 投影强制写出 repo-relative artifact 路径
45
69
  - Delivery `verifyArtifactRefs` 接受位于仓库内的绝对路径(兼容旧 envelope)
46
70
  - `verifyBundleTaskSpecBindings` 接受 `qa-testcode`(backend-test)与 `qa-execute`(frontend-test)
@@ -415,18 +415,6 @@ check_skill "loop-agent" \\
415
415
  "references/command-reference.md"
416
416
  check_skill "agent-worker" "references/agent-worker-operator.md"
417
417
 
418
- # loop-agent description is a YAML folded block (>-); grep ^description: only
419
- # captures the indicator line. Validate the general-demand trigger terms
420
- # against the whole skill file so host auto-discovery phrasing cannot drift.
421
- loop_agent_skill=".agents/skills/loop-agent/SKILL.md"
422
- loop_agent_text="$(tr '[:upper:]' '[:lower:]' < "\${loop_agent_skill}")"
423
- for term in "loop-agent 帮我完成" "帮我实现" "帮我修复" "帮我开发" "agent dag" "new-task" "dag run-task" "dag validate" "run-dag"; do
424
- if [[ "\${loop_agent_text}" != *"\${term}"* ]]; then
425
- echo "skill entry 检查失败(loop-agent):description 缺少触发/路由词 \${term}" >&2
426
- exit 1
427
- fi
428
- done
429
-
430
418
  description_line="$(grep -E '^description:' .agents/skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
431
419
  for term in "agent-worker" "feature packet" "taskspec" "task pool" "self-host" "candidate" "loop-agent"; do
432
420
  if [[ "\${description_line}" != *"\${term}"* ]]; then
@@ -814,7 +802,6 @@ function buildManagedAgentsBlock(input) {
814
802
  "| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |",
815
803
  "| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;活跃 DAG/Worker 或人工决策时零写入 |",
816
804
  "| 初始化安全更新 / 应用初始化更新 / loop-agent 初始化安全更新 / loop agent初始化安全更新 | 安全更新 | 先 check-update,再只执行确定性安全动作 |",
817
- "| loop-agent 帮我完成 / 帮我实现 / 帮我修复 / 帮我开发 <需求>;使用 loop-agent 完成 <X>;按 loop-agent 流程处理 <X> | 通用需求实现 | 先建任务源与 `task.json.allowedPaths` / `forbiddenPaths`,再 `new-task` → `dag run-task` → `dag validate` → `run-dag`;主会话不得直接修改业务实现 |",
818
805
  "",
819
806
  "**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。",
820
807
  "",
@@ -874,11 +861,10 @@ function buildManagedAgentsBlock(input) {
874
861
  "### 运行看板(只读)",
875
862
  "",
876
863
  "```bash",
877
- "agent-worker console # 默认 repo=当前目录,port=8790",
878
- "agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口",
864
+ "agent-worker console serve --repo . --port 8790",
879
865
  "```",
880
866
  "",
881
- "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 仅为兼容入口。",
867
+ "浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。`agent-worker observe serve` 仅为兼容入口。",
882
868
  "",
883
869
  "### DAG 诊断与收口",
884
870
  "",
@@ -8,7 +8,7 @@ import { buildRequirementCoverageGateShellCommand, expandShellPreset, buildVerdi
8
8
  import { materializeBackendTestAnalysisContract } from "../workflows/dag/backend-test-analysis-contract.js";
9
9
  import { extractBackendTestContractEnvelope } from "../workflows/dag/backend-test-contract-envelope.js";
10
10
  import { materializeFrontendImplementationContract } from "../workflows/dag/frontend-implementation-contract.js";
11
- import { materializeFrontendTestResult, validateFrontendCaseEvidence, } from "../workflows/dag/frontend-test-result-contract.js";
11
+ import { materializeFrontendTestResult, } from "../workflows/dag/frontend-test-result-contract.js";
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";
@@ -17,7 +17,7 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
17
17
  import { materializeFrontendLintAssessment, materializeFrontendLintBaseline, } from "../workflows/dag/frontend-lint-baseline.js";
18
18
  import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
19
19
  import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
20
- import { materializeBackendTestResultFromPytestHtml, materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
20
+ import { materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
21
21
  import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
22
22
  import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
23
23
  import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
@@ -385,101 +385,28 @@ async function executeBackendTestPipeline(input, meta) {
385
385
  const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
386
386
  const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
387
387
  const pytestTargets = mappedScripts.map(shellQuote).join(" ");
388
- const mappedScriptDiagLines = mappedScripts
389
- .map((script) => `echo " - ${script.replaceAll('"', '\\"')}" >> "\${DIAG_FILE}"`)
390
- .join("; ");
391
- // Diagnostic-first single shell: keep pytest exactly once, but always
392
- // emit STEP markers, mapped targets, interpreter identity, exit code
393
- // and html presence so empty nonzero-exit failures remain auditable.
394
388
  const pytestCommand = [
395
389
  'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
396
- 'DIAG_FILE="${HARNESS_DAG_RUN_DIR}/reports/backend-test-execute-diagnostics.md"',
397
- [
398
- 'echo "# Backend Test Execute Diagnostics" > "${DIAG_FILE}"',
399
- 'echo >> "${DIAG_FILE}"',
400
- 'echo "- startedAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
401
- 'echo "- pwd: $(pwd)" >> "${DIAG_FILE}"',
402
- 'echo "- HARNESS_DAG_RUN_DIR: ${HARNESS_DAG_RUN_DIR}" >> "${DIAG_FILE}"',
403
- 'echo "- shell: ${BASH:-bash} (${BASH_VERSION:-unknown})" >> "${DIAG_FILE}"',
404
- `echo "- mappedScriptCount: ${mappedScripts.length}" >> "\${DIAG_FILE}"`,
405
- 'echo "- mappedScripts:" >> "${DIAG_FILE}"',
406
- ].join("; "),
407
- mappedScriptDiagLines || 'echo " - <none>" >> "${DIAG_FILE}"',
408
- 'echo "STEP=resolve-python"',
409
390
  'PYTHON_BIN="$(command -v python || command -v python3 || true)"',
410
- [
411
- 'echo "- command_v_python: $(command -v python || true)" >> "${DIAG_FILE}"',
412
- 'echo "- command_v_python3: $(command -v python3 || true)" >> "${DIAG_FILE}"',
413
- 'echo "- PYTHON_BIN: ${PYTHON_BIN:-<empty>}" >> "${DIAG_FILE}"',
414
- ].join("; "),
415
- 'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
416
- 'echo "STEP=python-identity"',
417
- 'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -c "import sys; print(sys.executable); print(sys.version.splitlines()[0])" >> "${DIAG_FILE}" 2>&1; then echo "STEP=python-identity-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
418
- 'echo "STEP=pytest-version"',
419
- 'if ! PYTHONUNBUFFERED=1 "${PYTHON_BIN}" -m pytest --version >> "${DIAG_FILE}" 2>&1; then echo "STEP=pytest-version-failed" | tee -a "${DIAG_FILE}" >&2; exit 127; fi',
420
- 'echo "STEP=pytest-run"',
421
- `echo "- pytestCommand: python -m pytest ${mappedScripts.join(" ")} -v -p no:cacheprovider --html=reports/backend-test.html --self-contained-html" >> "\${DIAG_FILE}"`,
391
+ 'if [ -z "${PYTHON_BIN}" ]; then echo "python/python3 is required for backend-test execution" >&2; exit 127; fi',
422
392
  // Run pytest exactly once and emit the native pytest-html
423
393
  // self-contained report. The HTML/facts renderers read per-case
424
394
  // captured stdout (HTTP_REQUEST/HTTP_RESPONSE) directly from the
425
395
  // pytest-html data-jsonblob island, so a JUnit XML report is no
426
396
  // longer generated.
427
- `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
397
+ `PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 "\${PYTHON_BIN}" -m pytest ${pytestTargets} -v -p no:cacheprovider --html="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" --self-contained-html`,
428
398
  "STATUS=$?",
429
399
  'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
430
- [
431
- 'echo "- pytestExitCode: ${STATUS}" >> "${DIAG_FILE}"',
432
- 'if [ -f "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then echo "- pytestHtml: present ($(wc -c < "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" | tr -d " ") bytes)" >> "${DIAG_FILE}"; else echo "- pytestHtml: missing" >> "${DIAG_FILE}"; fi',
433
- 'echo "- finishedAt: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date)" >> "${DIAG_FILE}"',
434
- ].join("; "),
435
- 'echo "STEP=pytest-finished status=${STATUS}"',
436
400
  // exit 0 (all pass) or 1 (assertion failures) with a valid
437
401
  // pytest-html report are reportable; collection errors / crashes
438
402
  // (exit >= 2) or a missing report surface as a pipeline error below.
439
403
  'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ]; then exit 0; fi',
440
- 'echo "STEP=pipeline-failed status=${STATUS} html=$([ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.html" ] && echo present || echo missing)" >&2',
441
404
  'exit "${STATUS}"',
442
405
  ].join("; ");
443
406
  const results = await executePipelineCommands(input, meta, [pytestCommand]);
444
407
  if (!results.every((result) => result.ok)) {
445
408
  const failure = results.find((result) => !result.ok);
446
- const combinedStdout = results.map((result) => result.stdout).join("\n");
447
- let diagnosticsText = "";
448
- try {
449
- diagnosticsText = await readFile(path.join(meta.runDir, "reports", "backend-test-execute-diagnostics.md"), "utf8");
450
- }
451
- catch {
452
- diagnosticsText = "";
453
- }
454
- const stderrSummary = [
455
- "backend-test markdown-execute-html shell failed",
456
- `failureCategory=${failure.failureCategory}`,
457
- `exitCode=${failure.exitCode ?? "null"}`,
458
- `durationMs=${failure.durationMs}`,
459
- `mappedScripts=${mappedScripts.join(",") || "<none>"}`,
460
- `shellStdoutEmpty=${combinedStdout.trim().length === 0}`,
461
- `shellStderrEmpty=${failure.stderr.trim().length === 0}`,
462
- failure.stdoutArtifactPath
463
- ? `commandStdoutArtifact=${failure.stdoutArtifactPath}`
464
- : undefined,
465
- failure.stderrArtifactPath
466
- ? `commandStderrArtifact=${failure.stderrArtifactPath}`
467
- : undefined,
468
- "diagnostics=reports/backend-test-execute-diagnostics.md",
469
- failure.stderr.trim() || "(shell stderr empty)",
470
- diagnosticsText.trim()
471
- ? `--- diagnostics ---\n${diagnosticsText.trim()}`
472
- : "(diagnostics file missing or empty)",
473
- ]
474
- .filter((line) => Boolean(line))
475
- .join("\n");
476
- return {
477
- ok: false,
478
- stdout: combinedStdout,
479
- stderr: stderrSummary,
480
- failureCategory: failure.failureCategory,
481
- durationMs: Date.now() - started,
482
- };
409
+ return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
483
410
  }
484
411
  const reportsDir = path.join(meta.runDir, "reports");
485
412
  const pytestHtmlContent = await readFile(path.join(reportsDir, "backend-test.html"), "utf8");
@@ -487,14 +414,6 @@ async function executeBackendTestPipeline(input, meta) {
487
414
  if (![0, 1].includes(pytestExitCode))
488
415
  throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
489
416
  const parsed = parsePytestHtmlReport(pytestHtmlContent);
490
- // Bind Result v1 from the native pytest-html report BEFORE overwriting with the
491
- // styled renderer (which drops the data-jsonblob island).
492
- const resultArtifact = await materializeBackendTestResultFromPytestHtml({
493
- runDir: meta.runDir,
494
- htmlRelativePath: "reports/backend-test.html",
495
- htmlContent: pytestHtmlContent,
496
- pytestExitCode,
497
- });
498
417
  const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
499
418
  const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
500
419
  const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
@@ -511,7 +430,7 @@ async function executeBackendTestPipeline(input, meta) {
511
430
  const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
512
431
  const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
513
432
  const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
514
- outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `result=${resultArtifact.path}`, facts);
433
+ outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, facts);
515
434
  }
516
435
  else if (pipeline === "contracts") {
517
436
  const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
@@ -970,38 +889,6 @@ async function executeFrontendVerificationBundle(input, meta) {
970
889
  };
971
890
  }
972
891
  }
973
- async function executeFrontendTestEvidenceValidation(input) {
974
- const started = Date.now();
975
- try {
976
- const result = await validateFrontendCaseEvidence({ workspaceRoot: input.cwd });
977
- const output = `frontend case evidence validation cases=${result.cases} findings=${result.issues.length}${result.issues.length ? ` issues=${JSON.stringify(result.issues)}` : ""}`;
978
- if (result.hardFail) {
979
- return {
980
- ok: false,
981
- stdout: "",
982
- stderr: `frontend-test evidence hard-fail: ${JSON.stringify(result.issues)}`,
983
- failureCategory: "nonzero-exit",
984
- durationMs: Date.now() - started,
985
- };
986
- }
987
- return {
988
- ok: true,
989
- stdout: output,
990
- stderr: "",
991
- failureCategory: "success",
992
- durationMs: Date.now() - started,
993
- };
994
- }
995
- catch (error) {
996
- return {
997
- ok: false,
998
- stdout: "",
999
- stderr: error instanceof Error ? error.message : String(error),
1000
- failureCategory: "invalid-output",
1001
- durationMs: Date.now() - started,
1002
- };
1003
- }
1004
- }
1005
892
  async function executeFrontendLintBaseline(input, meta) {
1006
893
  const started = Date.now();
1007
894
  const shell = input.task.shell;
@@ -1102,9 +989,6 @@ export async function executeDagShellNode(input, meta) {
1102
989
  return { ok: false, stdout: "", stderr: error instanceof Error ? error.message : String(error), failureCategory: "invalid-output", durationMs: Date.now() - started };
1103
990
  }
1104
991
  }
1105
- if (shell?.frontendTestEvidenceValidation) {
1106
- return executeFrontendTestEvidenceValidation(input);
1107
- }
1108
992
  if (shell?.backendTestPipeline) {
1109
993
  return executeBackendTestPipelineWithWriteGuard(input, meta);
1110
994
  }
@@ -41,8 +41,7 @@ export function buildAgentWorkerProgram() {
41
41
  .description("Local product-line worker utilities for TaskSpec validation")
42
42
  .version(readPackageVersion(path.dirname(path.dirname(path.dirname(fileURLToPath(import.meta.url))))) ?? "0.0.0", "-V, --version", "display version")
43
43
  .showHelpAfterError()
44
- .showSuggestionAfterError()
45
- .enablePositionalOptions();
44
+ .showSuggestionAfterError();
46
45
  const task = program.command("task").description("TaskSpec utilities");
47
46
  const batch = program
48
47
  .command("batch")
@@ -593,37 +592,19 @@ export function buildAgentWorkerProgram() {
593
592
  });
594
593
  process.stdout.write(`${JSON.stringify(snapshot)}\n`);
595
594
  });
596
- consoleCmd
597
- .description("Loop Operator Console (local); bare `agent-worker console` starts it directly (repo defaults to current working directory, port defaults to 8790)")
598
- .enablePositionalOptions()
599
- .passThroughOptions()
600
- .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
601
- .option("--port <port>", "HTTP port", "8790")
602
- .option("--host <host>", "Bind host (default 127.0.0.1; use 0.0.0.0 for LAN, no network auth)", "127.0.0.1")
603
- .option("--debug", "Log request source/method/path/status to stderr")
604
- .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
605
- .action(async (options) => {
606
- await runConsoleServe({
607
- repoRoot: path.resolve(options.repo),
608
- host: options.host,
609
- port: Number.parseInt(options.port, 10),
610
- debug: options.debug === true,
611
- appDataRoot: options.appData
612
- ? path.resolve(options.appData)
613
- : undefined,
614
- });
615
- });
616
595
  consoleCmd
617
596
  .command("serve")
618
- .option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
597
+ .requiredOption("--repo <repo-root>", "Target repo root")
619
598
  .option("--port <port>", "HTTP port", "8790")
620
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")
621
600
  .option("--debug", "Log request source/method/path/status to stderr")
622
601
  .option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
623
- .description("Start Loop Operator Console HTTP server (compat entry; equivalent to bare `agent-worker console`)")
602
+ .description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
624
603
  .action(async (options) => {
625
- await runConsoleServe({
626
- repoRoot: path.resolve(options.repo),
604
+ const { createConsoleServer } = await import("./console/server.js");
605
+ const repoRoot = path.resolve(options.repo);
606
+ const server = await createConsoleServer({
607
+ repoRoot,
627
608
  host: options.host,
628
609
  port: Number.parseInt(options.port, 10),
629
610
  debug: options.debug === true,
@@ -631,6 +612,13 @@ export function buildAgentWorkerProgram() {
631
612
  ? path.resolve(options.appData)
632
613
  : undefined,
633
614
  });
615
+ process.stdout.write(`${server.url}\n`);
616
+ await new Promise((resolve) => {
617
+ const shutdown = () => resolve();
618
+ process.once("SIGINT", shutdown);
619
+ process.once("SIGTERM", shutdown);
620
+ });
621
+ await server.close();
634
622
  });
635
623
  consoleCmd
636
624
  .command("doctor")
@@ -705,39 +693,6 @@ export async function main(argv = process.argv) {
705
693
  async function readTaskYaml(taskSpecPath) {
706
694
  return YAML.parse(await readFile(taskSpecPath, "utf-8"));
707
695
  }
708
- /** Shared console serve entry used by both bare `console` and `console serve`. */
709
- async function runConsoleServe(options) {
710
- const { createConsoleServer } = await import("./console/server.js");
711
- try {
712
- const server = await createConsoleServer({
713
- repoRoot: options.repoRoot,
714
- host: options.host,
715
- port: options.port,
716
- debug: options.debug === true,
717
- appDataRoot: options.appDataRoot,
718
- });
719
- process.stdout.write(`${server.url}\n`);
720
- await new Promise((resolve) => {
721
- const shutdown = () => resolve();
722
- process.once("SIGINT", shutdown);
723
- process.once("SIGTERM", shutdown);
724
- });
725
- await server.close();
726
- }
727
- catch (error) {
728
- const detail = error instanceof Error ? error.message : String(error);
729
- const code = error?.code;
730
- const isPortInUse = code === "EADDRINUSE" ||
731
- /listen EADDRINUSE/i.test(detail) ||
732
- /address already in use/i.test(detail);
733
- if (isPortInUse) {
734
- process.stderr.write(`端口 ${options.port} 已被占用,请使用 --port <port> 指定其它端口后重试。\n`);
735
- process.exit(1);
736
- }
737
- process.stderr.write(`${detail}\n`);
738
- process.exit(1);
739
- }
740
- }
741
696
  function handlePoolRecoveryCliError(error) {
742
697
  const detail = error instanceof Error ? error.message : String(error);
743
698
  process.stderr.write(`${detail}\n`);
@@ -101,12 +101,11 @@ export async function finalizeGitTask(transaction, outcome) {
101
101
  await assertTransactionPosition(transaction.repoRoot, current);
102
102
  const currentIgnored = await readIgnoredBaseline(transaction.repoRoot);
103
103
  const ignoredBaselineByPath = new Map(current.ignoredBaseline.map((entry) => [entry.path, entry.sha256]));
104
- const changedBaselineIgnored = currentIgnored
105
- .filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256)
106
- .map((entry) => entry.path);
107
- const missingBaselineIgnored = current.ignoredBaseline
108
- .filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path))
109
- .map((entry) => entry.path);
104
+ const changedBaselineIgnored = currentIgnored.filter((entry) => ignoredBaselineByPath.has(entry.path) && ignoredBaselineByPath.get(entry.path) !== entry.sha256).map((entry) => entry.path);
105
+ const missingBaselineIgnored = current.ignoredBaseline.filter((entry) => !currentIgnored.some((candidate) => candidate.path === entry.path)).map((entry) => entry.path);
106
+ if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
107
+ throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
108
+ }
110
109
  const existing = current.checkpoints.find((entry) => entry.workerRunId === outcome.workerRunId);
111
110
  if (outcome.status === "reused" || existing) {
112
111
  if (!existing)
@@ -124,28 +123,12 @@ export async function finalizeGitTask(transaction, outcome) {
124
123
  const newIgnored = currentIgnored.map((entry) => entry.path).filter((entry) => !ignoredBaselineByPath.has(entry));
125
124
  if (outcome.status === "succeeded") {
126
125
  const allowedPaths = expandAllowedPathsForWorkflow(resolveWorkflow(outcome.taskSpec).workflow, outcome.taskSpec.constraints.allowed_paths);
127
- const isAllowedEvidencePath = (entry) => !isSensitivePath(entry) &&
128
- !isEphemeralToolCachePath(entry) &&
129
- allowedPaths.some((glob) => matchesGlob(entry, glob));
130
- // Pre-existing ignored evidence under allowed_paths may be rewritten by
131
- // FINAL-VERIFY smoke / verify-final and must force-add into the checkpoint.
132
- // Other baseline ignored mutations remain fail-closed (e.g. dist/, secrets).
133
- const blockedBaselineChanged = changedBaselineIgnored.filter((entry) => !isAllowedEvidencePath(entry));
134
- const blockedBaselineMissing = missingBaselineIgnored.filter((entry) => !isEphemeralToolCachePath(entry) && !isAllowedEvidencePath(entry));
135
- if (blockedBaselineChanged.length > 0 || blockedBaselineMissing.length > 0) {
136
- throw new Error(`pre-existing ignored file changed during Git transaction: ${[...blockedBaselineChanged, ...blockedBaselineMissing].join(", ")}`);
137
- }
138
126
  // Evidence under allowed_paths may be gitignored for local convenience (e.g.
139
127
  // reports/welcome/** smoke JSON) but must still checkpoint for Task Pool Done.
140
128
  // Sensitive / out-of-scope ignored files remain fail-closed; ephemeral tool
141
129
  // caches neither block nor force-add.
142
130
  const forceAddIgnored = [];
143
131
  const blockedIgnored = [];
144
- const rewrittenBaselineEvidence = changedBaselineIgnored.filter((entry) => isAllowedEvidencePath(entry));
145
- for (const entry of rewrittenBaselineEvidence) {
146
- if (!forceAddIgnored.includes(entry))
147
- forceAddIgnored.push(entry);
148
- }
149
132
  for (const entry of newIgnored) {
150
133
  if (isEphemeralToolCachePath(entry))
151
134
  continue;
@@ -154,8 +137,7 @@ export async function finalizeGitTask(transaction, outcome) {
154
137
  continue;
155
138
  }
156
139
  if (allowedPaths.some((glob) => matchesGlob(entry, glob))) {
157
- if (!forceAddIgnored.includes(entry))
158
- forceAddIgnored.push(entry);
140
+ forceAddIgnored.push(entry);
159
141
  }
160
142
  else {
161
143
  blockedIgnored.push(entry);
@@ -184,10 +166,7 @@ export async function finalizeGitTask(transaction, outcome) {
184
166
  await assertTransactionPosition(transaction.repoRoot, { ...current, lastCheckpoint: commit });
185
167
  await assertCheckpointMetadata(transaction.repoRoot, commit, outcome.taskSpec, outcome.workerRunId);
186
168
  const afterCommitIgnored = await readIgnoredBaseline(transaction.repoRoot);
187
- // force-add may promote allowed evidence from ignored → tracked; drop those
188
- // paths from the post-commit baseline equality check.
189
- const forceAdded = new Set(forceAddIgnored);
190
- assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path) && !forceAdded.has(entry.path)));
169
+ assertIgnoredBaselineUnchanged(current.ignoredBaseline.filter((entry) => !isEphemeralToolCachePath(entry.path)), afterCommitIgnored.filter((entry) => !isEphemeralToolCachePath(entry.path)));
191
170
  }
192
171
  catch (error) {
193
172
  const branch = await git(transaction.repoRoot, ["branch", "--show-current"]).catch(() => "");
@@ -220,10 +199,6 @@ export async function finalizeGitTask(transaction, outcome) {
220
199
  changedFiles: checkpointFiles,
221
200
  };
222
201
  }
223
- // Failed / keep-diff paths still fail closed on unexpected baseline ignored mutations.
224
- if (changedBaselineIgnored.length > 0 || missingBaselineIgnored.length > 0) {
225
- throw new Error(`pre-existing ignored file changed during Git transaction: ${[...changedBaselineIgnored, ...missingBaselineIgnored].join(", ")}`);
226
- }
227
202
  const artifactDir = path.join(path.dirname(transaction.recordPath), "failures", outcome.workerRunId);
228
203
  await captureFailureArtifacts(transaction.repoRoot, artifactDir, changes, newIgnored, outcome);
229
204
  if (outcome.keepFailedDiff) {
@@ -404,23 +404,7 @@ export async function verifyBundleOutcomes(repoRoot, bundle) {
404
404
  return false;
405
405
  if (JSON.stringify(verified.artifacts) !== JSON.stringify(outcome.artifacts))
406
406
  return false;
407
- // Rehash only durable typed evidence. run_record/dag_json may be rewritten
408
- // after outcome projection (worker-run-record appends report commands).
409
- const evidenceArtifacts = outcome.artifacts.filter((artifact) => {
410
- const kind = artifact.kind ?? "";
411
- if (kind === "run_record" || kind === "dag_json")
412
- return false;
413
- if (kind === "backend-test-result" ||
414
- kind === "frontend-test-result" ||
415
- kind.includes("backend-test-result") ||
416
- kind.includes("frontend-test-result")) {
417
- return true;
418
- }
419
- // Unknown kinds: still rehash unless clearly infrastructure paths.
420
- return (!artifact.path.includes("worker-run-record") &&
421
- !artifact.path.includes("-dag.json"));
422
- });
423
- if (!await verifyArtifactRefs(repoRoot, evidenceArtifacts))
407
+ if (!await verifyArtifactRefs(repoRoot, outcome.artifacts))
424
408
  return false;
425
409
  }
426
410
  return true;
@@ -433,12 +417,9 @@ export function verifyBundleTaskSpecBindings(bundle, specs) {
433
417
  return false;
434
418
  taskIds.add(outcome.taskId);
435
419
  const spec = specs.get(outcome.taskId);
436
- // Typed verification tasks may be qa-execute (frontend-test) or qa-testcode
437
- // (backend-test Markdown-first). Delivery binds on feature_id + workflow.
438
- const typedVerificationTypes = new Set(["qa-execute", "qa-testcode"]);
439
420
  if (!spec ||
440
421
  spec.feature_id !== bundle.featureId ||
441
- !typedVerificationTypes.has(spec.type) ||
422
+ spec.type !== "qa-execute" ||
442
423
  spec.execution?.workflow !== outcome.workflow)
443
424
  return false;
444
425
  }
@@ -479,11 +460,9 @@ async function verifyArtifactRefs(repoRoot, artifacts) {
479
460
  return false;
480
461
  }
481
462
  for (const artifact of artifacts) {
482
- // Accept absolute paths only when they resolve inside the repo (legacy
483
- // envelopes may store abs paths). Prefer relative for new projections.
484
- const lexical = path.isAbsolute(artifact.path)
485
- ? path.resolve(artifact.path)
486
- : path.resolve(canonicalRepoRoot, artifact.path);
463
+ if (path.isAbsolute(artifact.path))
464
+ return false;
465
+ const lexical = path.resolve(canonicalRepoRoot, artifact.path);
487
466
  let resolved;
488
467
  let content;
489
468
  try {
@@ -13,7 +13,6 @@ import { dagSourceBindingSchema } from "../../workflows/dag/types.js";
13
13
  import { getTaskPoolRoot } from "../pool/run-store.js";
14
14
  import { isAllowedArtifactTextPath, resolveArtifactPath, resolveRepoFilePreview, toRepoRelativeArtifactPath, } from "./paths.js";
15
15
  import { extractSpecEvidence, extractSpecReadContent, } from "./spec-evidence.js";
16
- import { buildDagNodeInput } from "./node-input.js";
17
16
  import { buildObserveHealthV1 } from "./health.js";
18
17
  const ARTIFACT_PREVIEW_MAX_BYTES = 64 * 1024;
19
18
  export function createObserveSnapshotCache() {
@@ -87,11 +86,6 @@ export const ROUTES = [
87
86
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/session-events$/,
88
87
  handler: handleDagNodeSessionEvents,
89
88
  },
90
- {
91
- method: "GET",
92
- pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/input$/,
93
- handler: handleDagNodeInput,
94
- },
95
89
  {
96
90
  method: "GET",
97
91
  pattern: /^\/api\/dag-runs\/([^/]+)\/nodes\/([^/]+)\/spec-evidence$/,
@@ -461,17 +455,6 @@ async function handleDagRunById(_req, res, match, ctx) {
461
455
  }
462
456
  sendJson(res, 200, dagRun);
463
457
  }
464
- async function handleDagNodeInput(_req, res, match, ctx) {
465
- const dagRunId = match.params.id;
466
- const nodeId = match.params.sub;
467
- if (!isSafeObservabilityIdentifier(dagRunId) ||
468
- !isSafeObservabilityIdentifier(nodeId)) {
469
- sendJson(res, 400, { error: "Invalid dag run or node identifier" });
470
- return;
471
- }
472
- const result = await buildDagNodeInput(ctx.repoRoot, dagRunId, nodeId);
473
- sendJson(res, result.status, result.body);
474
- }
475
458
  async function handleDagNodeSessionEvents(_req, res, match, ctx) {
476
459
  const dagRunId = match.params.id;
477
460
  const nodeId = match.params.sub;
@@ -29,15 +29,6 @@ export async function fetchJsonResult(url) {
29
29
  }
30
30
  }
31
31
 
32
- /**
33
- * Fetch a dag node input projection. Preserves HTTP status so callers can
34
- * distinguish missing runs, unavailable nodes, and transport errors.
35
- */
36
- export async function fetchDagNodeInput(runId, nodeId) {
37
- const url = `/api/dag-runs/${encodeURIComponent(runId)}/nodes/${encodeURIComponent(nodeId)}/input`;
38
- return fetchJsonResult(url);
39
- }
40
-
41
32
  /**
42
33
  * Fetch a spec-evidence file preview (binding source or successful read).
43
34
  * Resolves to the parsed JSON body, or throws an Error carrying the HTTP
@@ -33,15 +33,6 @@ export const UI_TEXT = {
33
33
  noBatches: "暂无 Worker 批次,运行后将出现在这里。",
34
34
  noNodes: "暂无节点。",
35
35
  noOutput: "暂无节点输出。",
36
- noInput: "暂无节点输入。",
37
- inputLoading: "正在加载节点输入…",
38
- inputUnavailable: "未在冻结 DAG tasks[] 中找到该节点输入。",
39
- inputDynamicUnsupported:
40
- "该节点为运行时动态展开节点,Phase A 暂不提供完整输入。",
41
- inputRunSpecUnavailable: "无法读取冻结 DAG 规格(run.json)。",
42
- inputError: "加载节点输入失败。",
43
- inputFrozenLabel: "冻结 DAG 节点定义",
44
- inputFingerprintNote: "仅保存 assembled prompt 指纹,全文未持久化",
45
36
  noSessionEvents: "该节点暂无过程日志",
46
37
  noArtifacts: "暂无产物记录。",
47
38
  noActiveCommand: "当前没有正在执行的命令。",
@@ -34,13 +34,6 @@ export const uiState = {
34
34
  // previous node can never mutate the current node's event cache.
35
35
  sessionEventIdentity: null,
36
36
  sessionEventIdentitySeq: 0,
37
- /** Node-input request identity ("<dagRunId>:<nodeId>#<seq>"). */
38
- dagNodeInputIdentity: null,
39
- dagNodeInputRequestSeq: 0,
40
- /** Last successful / in-flight node-input payload for the active identity. */
41
- dagNodeInputData: null,
42
- dagNodeInputError: null,
43
- dagNodeInputLoading: false,
44
37
  /** When true,「执行过程」展开 message_start/end 原始协议事件。 */
45
38
  sessionTimelineShowProtocol: false,
46
39
  /** Open <details> keys restored across timeline re-renders. */
@@ -52,7 +45,6 @@ export const uiState = {
52
45
  dagGraphViewportState: null,
53
46
  dagTimelineViewportState: null,
54
47
  dagNodeOutputViewportState: null,
55
- dagNodeInputViewportState: null,
56
48
  runOutputViewportState: null,
57
49
  /**
58
50
  * Spec-evidence file preview drill-down. null = list view; otherwise
@@ -73,12 +65,6 @@ export function makeSessionEventIdentity(dagRunId, nodeId) {
73
65
  return `${dagRunId}:${nodeId}#${uiState.sessionEventIdentitySeq}`;
74
66
  }
75
67
 
76
- /** Build a fresh node-input identity token and bump its sequence. */
77
- export function makeDagNodeInputIdentity(dagRunId, nodeId) {
78
- uiState.dagNodeInputRequestSeq += 1;
79
- return `${dagRunId}:${nodeId}#${uiState.dagNodeInputRequestSeq}`;
80
- }
81
-
82
68
  export function isPageVisible() {
83
69
  return (
84
70
  typeof document === "undefined" || document.visibilityState !== "hidden"