@tea-agent/loop-agent 0.24.10 → 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.
- package/CHANGELOG.md +12 -0
- package/dist/commands/init.js +2 -16
- package/dist/executors/shell-executor.js +6 -122
- package/dist/worker/cli.js +14 -59
- package/dist/worker/delivery/git-transaction.js +7 -32
- package/dist/worker/delivery/package.js +5 -9
- package/dist/worker/delivery/verification-bundle.js +5 -26
- package/dist/worker/observe/routes.js +0 -17
- package/dist/worker/observe/static/api.js +0 -9
- package/dist/worker/observe/static/constants.js +0 -9
- package/dist/worker/observe/static/state.js +0 -14
- package/dist/worker/observe/static/styles.css +0 -74
- package/dist/worker/observe/static/views/dag-inspector.js +15 -371
- package/dist/worker/outcomes/projector.js +1 -5
- package/dist/worker/runner/run-ready.js +1 -41
- package/dist/workflows/dag/backend-test-markdown-workflow.js +9 -75
- package/dist/workflows/dag/backend-test-result-contract.js +0 -103
- package/dist/workflows/dag/frontend-test-result-contract.js +41 -106
- package/dist/workflows/dag/init-hybrid.js +14 -13
- package/dist/workflows/dag/node-execution.js +2 -3
- package/dist/workflows/dag/types.js +0 -2
- package/dist/workflows/dag/validate.js +2 -3
- package/docs/architecture/worker-and-feature.md +11 -0
- package/docs/init-surface.manifest.json +3 -0
- package/docs/templates/agent-dag.schema.json +9 -0
- package/docs/templates/frontend-implementation-contract.schema.json +2 -2
- package/package.json +1 -1
- package/skills/frontend-bounded-implement/SKILL.md +53 -0
- package/skills/frontend-implementation/SKILL.md +3 -7
- package/skills/frontend-implementation/references/node-contracts.md +2 -2
- package/dist/worker/observe/dag-run-artifacts.js +0 -90
- package/dist/worker/observe/node-input.js +0 -444
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
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
|
+
|
|
5
15
|
## [0.24.10] - 2026-07-29
|
|
6
16
|
|
|
7
17
|
### 修复
|
|
@@ -53,6 +63,8 @@
|
|
|
53
63
|
|
|
54
64
|
### 修复
|
|
55
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
|
|
56
68
|
- Outcome 投影强制写出 repo-relative artifact 路径
|
|
57
69
|
- Delivery `verifyArtifactRefs` 接受位于仓库内的绝对路径(兼容旧 envelope)
|
|
58
70
|
- `verifyBundleTaskSpecBindings` 接受 `qa-testcode`(backend-test)与 `qa-execute`(frontend-test)
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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}`,
|
|
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
|
}
|
package/dist/worker/cli.js
CHANGED
|
@@ -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
|
-
.
|
|
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 (
|
|
602
|
+
.description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
|
|
624
603
|
.action(async (options) => {
|
|
625
|
-
await
|
|
626
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -284,15 +284,11 @@ export async function validateDeliveryPackage(input) {
|
|
|
284
284
|
blockers.push(`Acceptance coverage status is not derivable: ${ac.id}`);
|
|
285
285
|
if (JSON.stringify([...item.blockedBy].sort()) !== JSON.stringify([...blockedBy].sort()))
|
|
286
286
|
blockers.push(`Acceptance blockers changed: ${ac.id}`);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
|
|
293
|
-
if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
|
|
294
|
-
blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
|
|
295
|
-
}
|
|
287
|
+
for (const ref of expectedRefs)
|
|
288
|
+
if (!item.evidence.some((candidate) => candidate.path === ref.path && candidate.sha256 === ref.sha256))
|
|
289
|
+
blockers.push(`Acceptance run evidence is missing: ${ac.id}`);
|
|
290
|
+
if (qa?.data.acIds.includes(ac.id) && !item.evidence.some((candidate) => candidate.path === qa.ref.path && candidate.sha256 === qa.ref.sha256))
|
|
291
|
+
blockers.push(`Acceptance QA evidence is missing: ${ac.id}`);
|
|
296
292
|
for (const ref of item.evidence)
|
|
297
293
|
if (!await hashedRefValid(repoRoot, ref))
|
|
298
294
|
blockers.push(`Acceptance evidence changed: ${item.acId}`);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
const lexical = 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"
|