@tea-agent/loop-agent 0.24.4 → 0.24.6
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/AGENTS.md +1 -1
- package/CHANGELOG.md +47 -0
- package/README.md +5 -2
- package/dist/application/dag/generate-task-dag.js +5 -8
- package/dist/commands/init.js +19 -5
- package/dist/executors/shell-executor.js +10 -2
- package/dist/task/task-demand-routing.js +27 -14
- package/dist/worker/cli.js +59 -14
- package/dist/worker/observe/dag-run-artifacts.js +90 -0
- package/dist/worker/observe/node-input.js +444 -0
- package/dist/worker/observe/routes.js +17 -0
- package/dist/worker/observe/static/api.js +9 -0
- package/dist/worker/observe/static/constants.js +9 -0
- package/dist/worker/observe/static/state.js +14 -0
- package/dist/worker/observe/static/styles.css +74 -0
- package/dist/worker/observe/static/views/dag-inspector.js +371 -15
- package/dist/workflows/dag/backend-test-markdown-workflow.js +75 -9
- package/dist/workflows/dag/backend-test-result-contract.js +103 -0
- package/dist/workflows/dag/init-hybrid.js +9 -8
- package/dist/workflows/dag/node-execution.js +3 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +3 -2
- package/docs/templates/backend-test-dag.json +7 -7
- package/docs/templates/backend-test-dag.review-cases.prompt.md +1 -1
- package/docs/templates/evaluation/agents-map-slim-v1.md +1 -1
- package/docs/templates/evaluation/agents-map-verbose-v0.md +2 -2
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/agent-worker/SKILL.md +1 -1
- package/skills/loop-agent/SKILL.md +1 -1
- package/skills/loop-agent/references/command-reference.md +3 -2
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
package/AGENTS.md
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
|
|
38
38
|
5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
|
|
39
39
|
6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
|
|
40
|
-
7. 看板/observe → `agent-worker console serve --repo . --port 8790
|
|
40
|
+
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;兼容入口 `agent-worker console serve --repo . --port 8790`)(`/inspect/` 只读);`observe serve` 仅为兼容入口。
|
|
41
41
|
8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
|
|
42
42
|
|
|
43
43
|
## 会话协议
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- Observe / Inspect 节点检查器在「节点输出」左侧新增「节点输入」页签:只读投影冻结 `run.json` 顶层节点定义(任务正文、依赖、执行器摘要、边界)与可选 assembled prompt 指纹;完整 assembled prompt 与动态展开子节点仍不在本轮范围
|
|
8
|
+
|
|
9
|
+
## [0.24.6] - 2026-07-29
|
|
10
|
+
|
|
11
|
+
### 重点更新
|
|
12
|
+
|
|
13
|
+
- 修复后端测试 Markdown-first 成功路径未投影 `backend-test-result` 的问题,Ready Planner 可再次放行 FE-TEST
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- Markdown-first `markdown-execute-html` 在解析原生 pytest-html 后立即物化 `contracts/backend-test-result.json`(Result v1),避免 styled HTML 覆盖后丢失 JSON island
|
|
18
|
+
- `node-execution` 对 `markdown-execute-html` 绑定 `structuredArtifact*`,Outcome 适配器可投影 `kind: backend-test-result`
|
|
19
|
+
|
|
20
|
+
## [0.24.5] - 2026-07-28
|
|
21
|
+
|
|
22
|
+
### 重点更新
|
|
23
|
+
|
|
24
|
+
- 后端测试 Markdown 模块与 pytest 脚本实现一对一精准映射,提升测试执行的确定性
|
|
25
|
+
- 修复后端测试 HTML 报告中请求参数显示为空的问题,正确展示请求与响应日志
|
|
26
|
+
- 初始化项目支持将「帮我完成/实现/修复」等通用需求表达直接路由进 Agent DAG
|
|
27
|
+
- 新增裸 agent-worker console 入口,简化本地看板启动流程
|
|
28
|
+
|
|
29
|
+
### 新增
|
|
30
|
+
|
|
31
|
+
- 初始化后的目标项目支持将「帮我完成/实现/修复/开发」等通用需求表达确定性地路由进 Agent DAG(new-task → dag run-task → dag validate → run-dag),无需追加额外的 cli 关键词
|
|
32
|
+
- 新增裸 agent-worker console 入口,直接启动 Loop Operator Console,repo 默认当前目录、port 默认 8790
|
|
33
|
+
- check-skill-entry.sh 及初始化投影脚本支持解析 YAML 折叠块,对通用需求触发词建立机器门禁以防漂移
|
|
34
|
+
|
|
35
|
+
### 改进
|
|
36
|
+
|
|
37
|
+
- 后端测试收敛模块映射,生成与评审 prompt 强制模块文件一对一映射,在映射缺失时支持确定性恢复
|
|
38
|
+
- 后端测试 execute/report 节点文案与 pytest-html 真源对齐,移除残留的 JUnit 表述
|
|
39
|
+
- 后端测试 HTML 报告对齐 R04 模板样式,采用全 inline style 设计(渐变 banner、可折叠用例卡片、状态胶囊及请求/响应分块展示)
|
|
40
|
+
- 本地看板启动更简,端口被占用时不再自动更换,改为明确提示使用 --port 显式指定其它端口
|
|
41
|
+
- 旧项目经 init check-update 与 apply-safe 即可安全补回新的「通用需求实现」路由行,并保留用户自定义前言
|
|
42
|
+
|
|
43
|
+
### 修复
|
|
44
|
+
|
|
45
|
+
- 修复后端测试 HTML 报告中请求参数显示为空的问题:渲染器现已兼容 payload= 键或 JSON request_record 格式的请求摘要
|
|
46
|
+
- 修复后端测试报告无法正确获取请求/响应日志的问题:兼容 HTTP_REQUEST 与 HTTP REQUEST 两种日志前缀,并在解析前正确解码 HTML 实体
|
|
47
|
+
- 修复 shell-executor 在 Windows 环境下因反斜杠路径导致 runDir 损坏的问题,确保路径注入稳定
|
|
48
|
+
- 修复前端 lint baseline 在 Windows 下因路径解析逻辑缺陷,拒绝相对路径输出导致评估误报为不可用的问题
|
|
49
|
+
- 修复多个测试固件在 Windows 下因嵌套 Shell 引号剥离导致命令执行输出为空或报错的问题(涉及 composite-shell 与 init-command)
|
|
50
|
+
|
|
5
51
|
## [0.24.4] - 2026-07-28
|
|
6
52
|
|
|
7
53
|
### 重点更新
|
|
@@ -26,6 +72,7 @@
|
|
|
26
72
|
### 重点更新
|
|
27
73
|
|
|
28
74
|
- 发布工作流改为发布已通过完整门禁并经 SHA-256 校验的 npm tarball,避免 `npm publish` 再次触发全量测试、类型检查与构建。
|
|
75
|
+
- 默认 `standard` 实现任务在 React、Next.js 或 Vue 项目中直接选择 `frontend-implementation` DAG,不再依赖需求关键词;明确后端、文档/测试范围或前端否定仍保留对应通用流程。
|
|
29
76
|
|
|
30
77
|
## [0.24.2] - 2026-07-28
|
|
31
78
|
|
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ loop-agent init reconcile --repo-root .
|
|
|
52
52
|
|
|
53
53
|
### 运行第一个任务
|
|
54
54
|
|
|
55
|
+
初始化后,在已初始化的目标项目中你可以直接对宿主 agent 说「**loop-agent 帮我完成 XXX 需求**」(或「帮我实现 / 帮我修复 / 帮我开发 XXX」「使用 loop-agent 完成 XXX」「按 loop-agent 流程处理 XXX」),初始化写入的 `AGENTS.md` 与 `.agents/skills/loop-agent/` 会把这类通用需求表达确定性地路由进 Agent DAG,**无需**追加额外的 `cli` 关键词。主会话只负责编排与验证,业务实现由受治理 DAG writer 完成。
|
|
56
|
+
|
|
55
57
|
```bash
|
|
56
58
|
loop-agent new-task example-task "实现一个有明确验收标准的小功能"
|
|
57
59
|
```
|
|
@@ -90,12 +92,13 @@ loop-agent dag report --run-id <run-id>
|
|
|
90
92
|
| 入口 | 命令 | 角色 |
|
|
91
93
|
| --- | --- | --- |
|
|
92
94
|
| 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
|
|
93
|
-
| Loop Operator Console | `agent-worker console
|
|
95
|
+
| Loop Operator Console | `agent-worker console` | Official 本地控制面(默认 repo=当前目录、`127.0.0.1:8790`,含 Operate 与 Inspect) |
|
|
94
96
|
| Observe | `agent-worker observe serve --repo . --port 8787` | 兼容期独立只读入口;不再作为推荐路径 |
|
|
95
97
|
|
|
96
98
|
```bash
|
|
97
99
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
98
|
-
agent-worker console
|
|
100
|
+
agent-worker console # 默认 repo=当前目录、port=8790
|
|
101
|
+
agent-worker console serve --repo . # 兼容入口,等价于裸入口
|
|
99
102
|
agent-worker console doctor --repo .
|
|
100
103
|
|
|
101
104
|
```
|
|
@@ -239,10 +239,8 @@ export async function generateTaskDagUseCase(input) {
|
|
|
239
239
|
reasons: ["dag run-task validate did not report governanceProfile"],
|
|
240
240
|
});
|
|
241
241
|
const hasExplicitSpecializedTaskKind = candidateResult.templateSelection.source === "taskKind";
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
profileRouting.selectedTemplate !== "supervised-implementation";
|
|
245
|
-
if (hasExplicitSpecializedTaskKind || hasSafeAutomaticTaskSourceRoute) {
|
|
242
|
+
const hasAutomaticTaskSourceRoute = candidateResult.templateSelection.source === "task-source";
|
|
243
|
+
if (hasExplicitSpecializedTaskKind || hasAutomaticTaskSourceRoute) {
|
|
246
244
|
profileRouting.selectedTemplate = candidateResult.template;
|
|
247
245
|
profileRouting.source = hasExplicitSpecializedTaskKind
|
|
248
246
|
? "taskKind"
|
|
@@ -256,10 +254,9 @@ export async function generateTaskDagUseCase(input) {
|
|
|
256
254
|
profileRouting.selectedByProfile = parsed.profile;
|
|
257
255
|
}
|
|
258
256
|
}
|
|
259
|
-
// Persist
|
|
260
|
-
//
|
|
261
|
-
|
|
262
|
-
if (hasSafeAutomaticTaskSourceRoute && candidateResult.template === "frontend-implementation") {
|
|
257
|
+
// Persist the automatic business workflow route while preserving the
|
|
258
|
+
// governance profile as a separate routing dimension.
|
|
259
|
+
if (hasAutomaticTaskSourceRoute && candidateResult.template === "frontend-implementation") {
|
|
263
260
|
const taskConfig = await loadTaskConfig(repoRoot, parsed.taskId);
|
|
264
261
|
if (!taskConfig.taskKind || taskConfig.taskKind === "standard") {
|
|
265
262
|
await writeTaskConfig(repoRoot, parsed.taskId, {
|
package/dist/commands/init.js
CHANGED
|
@@ -415,6 +415,18 @@ 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
|
+
|
|
418
430
|
description_line="$(grep -E '^description:' .agents/skills/agent-worker/SKILL.md | head -n 1 | tr '[:upper:]' '[:lower:]')"
|
|
419
431
|
for term in "agent-worker" "feature packet" "taskspec" "task pool" "self-host" "candidate" "loop-agent"; do
|
|
420
432
|
if [[ "\${description_line}" != *"\${term}"* ]]; then
|
|
@@ -802,6 +814,7 @@ function buildManagedAgentsBlock(input) {
|
|
|
802
814
|
"| 初始化更新校验 / 检查初始化更新 / loop-agent 初始化更新校验 / loop agent初始化更新校验 | 更新校验 | 只读报告,不写入 |",
|
|
803
815
|
"| 初始化对齐 / 升级后对齐 / reconcile 初始化 / loop-agent 初始化对齐 | 升级对齐 | 自动应用确定性安全动作;活跃 DAG/Worker 或人工决策时零写入 |",
|
|
804
816
|
"| 初始化安全更新 / 应用初始化更新 / 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`;主会话不得直接修改业务实现 |",
|
|
805
818
|
"",
|
|
806
819
|
"**更新校验(只读)**:只读执行下面命令;**不得自动**执行 `apply-safe` 或模型合并。",
|
|
807
820
|
"",
|
|
@@ -861,10 +874,11 @@ function buildManagedAgentsBlock(input) {
|
|
|
861
874
|
"### 运行看板(只读)",
|
|
862
875
|
"",
|
|
863
876
|
"```bash",
|
|
864
|
-
"agent-worker console
|
|
877
|
+
"agent-worker console # 默认 repo=当前目录,port=8790",
|
|
878
|
+
"agent-worker console serve --repo . --port 8790 # 兼容入口,等价于上面裸入口",
|
|
865
879
|
"```",
|
|
866
880
|
"",
|
|
867
|
-
"浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1
|
|
881
|
+
"浏览器打开 `http://127.0.0.1:8790/`;检视面为 `http://127.0.0.1:8790/inspect/`。默认绑定本机 `127.0.0.1`;不要直接暴露到公开网络。端口被占用时不会自动更换,请用 `--port <port>` 显式指定。`agent-worker observe serve` 仅为兼容入口。",
|
|
868
882
|
"",
|
|
869
883
|
"### DAG 诊断与收口",
|
|
870
884
|
"",
|
|
@@ -2049,11 +2063,11 @@ function buildTargetFeatureWorkflow(input) {
|
|
|
2049
2063
|
"",
|
|
2050
2064
|
"## Specialized Task Kinds",
|
|
2051
2065
|
"",
|
|
2052
|
-
"- standard tasks
|
|
2053
|
-
"-
|
|
2066
|
+
"- standard tasks first use the structured task type in `source/需求.md`, then combine `allowedPaths` with strong React/Next/Vue project evidence for deterministic routing.",
|
|
2067
|
+
"- A frontend project defaults eligible implementation work to the frontend DAG without requirement keyword matching. Explicit backend, mixed, frontend-negated, and documentation/test-only scopes keep the template selected by the normal governance profile.",
|
|
2054
2068
|
"- `frontend-mock-assess-pi` reads Mock/API/schema evidence and selects `native|browser-intercept|request-adapter|not-needed|blocked` before frontend planning; its deterministic gate rejects blocked or malformed output, while the existing frontend implementer remains the only writer.",
|
|
2055
2069
|
"- Mock-backed verification never proves real API integration. When the backend was not exercised, closeout must retain the gap and name `<task-id>-real-api-integration-verify`; that follow-up is explicitly created/run after backend readiness, never automatic.",
|
|
2056
|
-
"- Automatic frontend classification
|
|
2070
|
+
"- Automatic frontend classification selects the frontend implementation workflow and persists taskKind; explicit profiles, workflowPolicy, and supervised quality gates record governance strength without switching the business workflow back to a generic DAG.",
|
|
2057
2071
|
"- A backend implementation does not select the backend test DAG. `backend-test` remains an explicit test-engineering workflow.",
|
|
2058
2072
|
"- Explicit specialized `taskKind` values remain compatible and take precedence over task-source classification.",
|
|
2059
2073
|
"",
|
|
@@ -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 { materializeBackendTestResultFromRunDir, parsePytestHtmlReport } from "../workflows/dag/backend-test-result-contract.js";
|
|
20
|
+
import { materializeBackendTestResultFromPytestHtml, 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";
|
|
@@ -414,6 +414,14 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
414
414
|
if (![0, 1].includes(pytestExitCode))
|
|
415
415
|
throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
|
|
416
416
|
const parsed = parsePytestHtmlReport(pytestHtmlContent);
|
|
417
|
+
// Bind Result v1 from the native pytest-html report BEFORE overwriting with the
|
|
418
|
+
// styled renderer (which drops the data-jsonblob island).
|
|
419
|
+
const resultArtifact = await materializeBackendTestResultFromPytestHtml({
|
|
420
|
+
runDir: meta.runDir,
|
|
421
|
+
htmlRelativePath: "reports/backend-test.html",
|
|
422
|
+
htmlContent: pytestHtmlContent,
|
|
423
|
+
pytestExitCode,
|
|
424
|
+
});
|
|
417
425
|
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
418
426
|
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
419
427
|
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
@@ -430,7 +438,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
430
438
|
const markdownPath = await writeRunReport(meta.runDir, "backend-test.md", facts);
|
|
431
439
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
432
440
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
433
|
-
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, facts);
|
|
441
|
+
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `markdown=${markdownPath}`, `facts=${factsPath}`, `result=${resultArtifact.path}`, facts);
|
|
434
442
|
}
|
|
435
443
|
else if (pipeline === "contracts") {
|
|
436
444
|
const wrapperPath = path.join(meta.runDir, "analyze-and-discover-backend-test-pi.json");
|
|
@@ -298,6 +298,11 @@ export function classifyTaskDemand(input) {
|
|
|
298
298
|
message: "allowedPaths include a backend-specific source path",
|
|
299
299
|
});
|
|
300
300
|
}
|
|
301
|
+
const allowedPathsOnlyCoverNonProductArtifacts = input.allowedPaths.length > 0 &&
|
|
302
|
+
input.allowedPaths.every((rawPath) => {
|
|
303
|
+
const allowedPath = normalizedAllowedPath(rawPath);
|
|
304
|
+
return matchesAny(allowedPath, NON_PRODUCT_ALLOWED_PATH_PATTERNS);
|
|
305
|
+
});
|
|
301
306
|
const pathSupportedFrontendDelivery = frontendPath &&
|
|
302
307
|
(titleSignals.pathSupportedFrontendDelivery ||
|
|
303
308
|
requirementSignals.pathSupportedFrontendDelivery ||
|
|
@@ -328,20 +333,27 @@ export function classifyTaskDemand(input) {
|
|
|
328
333
|
message: "task text contains user-visible interaction/state delivery and the project has strong frontend framework evidence",
|
|
329
334
|
});
|
|
330
335
|
}
|
|
336
|
+
const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
|
|
337
|
+
const frontendProjectDefaultImplementation = hasStrongFrontendProjectEvidence &&
|
|
338
|
+
!backendDelivery &&
|
|
339
|
+
!frontendNegated &&
|
|
340
|
+
!allowedPathsOnlyCoverNonProductArtifacts;
|
|
341
|
+
if (frontendProjectDefaultImplementation) {
|
|
342
|
+
addSignal(signals, {
|
|
343
|
+
id: "frontend-project-default-implementation",
|
|
344
|
+
source: "project",
|
|
345
|
+
message: "frontend project evidence selects the frontend implementation workflow by default",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
331
348
|
const frontendDelivery = titleSignals.frontendDelivery ||
|
|
332
349
|
requirementSignals.frontendDelivery ||
|
|
333
350
|
pathSupportedFrontendDelivery ||
|
|
334
|
-
projectSupportedFrontendDelivery
|
|
335
|
-
|
|
336
|
-
input.allowedPaths.every((rawPath) => {
|
|
337
|
-
const allowedPath = normalizedAllowedPath(rawPath);
|
|
338
|
-
return matchesAny(allowedPath, NON_PRODUCT_ALLOWED_PATH_PATTERNS);
|
|
339
|
-
});
|
|
351
|
+
projectSupportedFrontendDelivery ||
|
|
352
|
+
frontendProjectDefaultImplementation;
|
|
340
353
|
if (allowedPathsOnlyCoverNonProductArtifacts && frontendDelivery) {
|
|
341
354
|
blockers.push("non-product-allowed-paths");
|
|
342
355
|
}
|
|
343
356
|
const eligibleFrontendDelivery = frontendDelivery && !allowedPathsOnlyCoverNonProductArtifacts;
|
|
344
|
-
const backendDelivery = titleSignals.backendDelivery || requirementSignals.backendDelivery;
|
|
345
357
|
const taskTypeConflict = (hasFrontendTaskType && (backendDelivery || frontendNegated)) ||
|
|
346
358
|
(hasBackendTaskType && eligibleFrontendDelivery);
|
|
347
359
|
if (taskTypeConflict)
|
|
@@ -402,13 +414,6 @@ export function resolveTaskDagTemplateSelection(input) {
|
|
|
402
414
|
reasons: [],
|
|
403
415
|
};
|
|
404
416
|
}
|
|
405
|
-
if (requestedTemplate !== "standard-dag") {
|
|
406
|
-
return {
|
|
407
|
-
template: requestedTemplate,
|
|
408
|
-
source: "requested-template",
|
|
409
|
-
reasons: [],
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
417
|
const classification = classifyTaskDemand(input);
|
|
413
418
|
if (classification.kind === "frontend") {
|
|
414
419
|
const frontendSignals = classification.signals
|
|
@@ -423,6 +428,14 @@ export function resolveTaskDagTemplateSelection(input) {
|
|
|
423
428
|
classification,
|
|
424
429
|
};
|
|
425
430
|
}
|
|
431
|
+
if (requestedTemplate !== "standard-dag") {
|
|
432
|
+
return {
|
|
433
|
+
template: requestedTemplate,
|
|
434
|
+
source: "requested-template",
|
|
435
|
+
reasons: [],
|
|
436
|
+
classification,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
426
439
|
return {
|
|
427
440
|
template: requestedTemplate,
|
|
428
441
|
source: "requested-template",
|
package/dist/worker/cli.js
CHANGED
|
@@ -41,7 +41,8 @@ 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()
|
|
44
|
+
.showSuggestionAfterError()
|
|
45
|
+
.enablePositionalOptions();
|
|
45
46
|
const task = program.command("task").description("TaskSpec utilities");
|
|
46
47
|
const batch = program
|
|
47
48
|
.command("batch")
|
|
@@ -593,18 +594,17 @@ export function buildAgentWorkerProgram() {
|
|
|
593
594
|
process.stdout.write(`${JSON.stringify(snapshot)}\n`);
|
|
594
595
|
});
|
|
595
596
|
consoleCmd
|
|
596
|
-
.
|
|
597
|
-
.
|
|
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())
|
|
598
601
|
.option("--port <port>", "HTTP port", "8790")
|
|
599
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")
|
|
600
603
|
.option("--debug", "Log request source/method/path/status to stderr")
|
|
601
604
|
.option("--app-data <path>", "Override Console application-data root (Draft/Operation/confirmation)")
|
|
602
|
-
.description("Start Loop Operator Console HTTP server (Operate + Inspect; static SPA + Operator API)")
|
|
603
605
|
.action(async (options) => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
const server = await createConsoleServer({
|
|
607
|
-
repoRoot,
|
|
606
|
+
await runConsoleServe({
|
|
607
|
+
repoRoot: path.resolve(options.repo),
|
|
608
608
|
host: options.host,
|
|
609
609
|
port: Number.parseInt(options.port, 10),
|
|
610
610
|
debug: options.debug === true,
|
|
@@ -612,13 +612,25 @@ export function buildAgentWorkerProgram() {
|
|
|
612
612
|
? path.resolve(options.appData)
|
|
613
613
|
: undefined,
|
|
614
614
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
615
|
+
});
|
|
616
|
+
consoleCmd
|
|
617
|
+
.command("serve")
|
|
618
|
+
.option("--repo <repo-root>", "Target repo root (default: current working directory)", process.cwd())
|
|
619
|
+
.option("--port <port>", "HTTP port", "8790")
|
|
620
|
+
.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
|
+
.option("--debug", "Log request source/method/path/status to stderr")
|
|
622
|
+
.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`)")
|
|
624
|
+
.action(async (options) => {
|
|
625
|
+
await runConsoleServe({
|
|
626
|
+
repoRoot: path.resolve(options.repo),
|
|
627
|
+
host: options.host,
|
|
628
|
+
port: Number.parseInt(options.port, 10),
|
|
629
|
+
debug: options.debug === true,
|
|
630
|
+
appDataRoot: options.appData
|
|
631
|
+
? path.resolve(options.appData)
|
|
632
|
+
: undefined,
|
|
620
633
|
});
|
|
621
|
-
await server.close();
|
|
622
634
|
});
|
|
623
635
|
consoleCmd
|
|
624
636
|
.command("doctor")
|
|
@@ -693,6 +705,39 @@ export async function main(argv = process.argv) {
|
|
|
693
705
|
async function readTaskYaml(taskSpecPath) {
|
|
694
706
|
return YAML.parse(await readFile(taskSpecPath, "utf-8"));
|
|
695
707
|
}
|
|
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
|
+
}
|
|
696
741
|
function handlePoolRecoveryCliError(error) {
|
|
697
742
|
const detail = error instanceof Error ? error.message : String(error);
|
|
698
743
|
process.stderr.write(`${detail}\n`);
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSafeObservabilityIdentifier } from "../observability/event-store.js";
|
|
4
|
+
export const DAG_RUN_LIFECYCLES = ["active", "completed", "paused"];
|
|
5
|
+
function isPathInside(root, target) {
|
|
6
|
+
const normalizedRoot = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
7
|
+
const normalizedTarget = path.normalize(target);
|
|
8
|
+
return (normalizedTarget === root || normalizedTarget.startsWith(normalizedRoot));
|
|
9
|
+
}
|
|
10
|
+
function normalizeRelativeSegments(relativeSegments) {
|
|
11
|
+
if (!Array.isArray(relativeSegments) || relativeSegments.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const segments = [];
|
|
15
|
+
for (const segment of relativeSegments) {
|
|
16
|
+
if (typeof segment !== "string" || !segment)
|
|
17
|
+
return null;
|
|
18
|
+
if (segment === "." || segment === "..")
|
|
19
|
+
return null;
|
|
20
|
+
if (segment.includes("/") || segment.includes("\\") || segment.includes("\0")) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
segments.push(segment);
|
|
24
|
+
}
|
|
25
|
+
return segments;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve a path under `.harness/dag-runs/{lifecycle}/{dagRunId}/...`.
|
|
29
|
+
* Fail closed on invalid ids, path escape, or the same runId appearing in
|
|
30
|
+
* multiple lifecycle directories.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveDagRunArtifact(repoRoot, dagRunId, relativeSegments) {
|
|
33
|
+
if (!isSafeObservabilityIdentifier(dagRunId)) {
|
|
34
|
+
return { ok: false, reason: "invalid-id" };
|
|
35
|
+
}
|
|
36
|
+
const segments = normalizeRelativeSegments(relativeSegments);
|
|
37
|
+
if (!segments) {
|
|
38
|
+
return { ok: false, reason: "unsafe-path" };
|
|
39
|
+
}
|
|
40
|
+
for (const segment of segments) {
|
|
41
|
+
if (segment !== "run.json" &&
|
|
42
|
+
segment !== "state.json" &&
|
|
43
|
+
!isSafeObservabilityIdentifier(segment) &&
|
|
44
|
+
!/^[a-zA-Z0-9._-]+$/.test(segment)) {
|
|
45
|
+
// Allow known artifact filenames under a validated node id prefix.
|
|
46
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(segment)) {
|
|
47
|
+
return { ok: false, reason: "unsafe-path" };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const dagRunsRoot = path.resolve(repoRoot, ".harness", "dag-runs");
|
|
52
|
+
const matches = [];
|
|
53
|
+
for (const lifecycle of DAG_RUN_LIFECYCLES) {
|
|
54
|
+
const runDir = path.join(dagRunsRoot, lifecycle, dagRunId);
|
|
55
|
+
const absolutePath = path.resolve(runDir, ...segments);
|
|
56
|
+
if (!isPathInside(dagRunsRoot, absolutePath))
|
|
57
|
+
continue;
|
|
58
|
+
const relative = path.relative(dagRunsRoot, absolutePath);
|
|
59
|
+
const parts = relative.split(path.sep).filter(Boolean);
|
|
60
|
+
if (parts[0] !== lifecycle || parts[1] !== dagRunId)
|
|
61
|
+
continue;
|
|
62
|
+
if (!existsSync(absolutePath))
|
|
63
|
+
continue;
|
|
64
|
+
matches.push({
|
|
65
|
+
lifecycle,
|
|
66
|
+
runDir,
|
|
67
|
+
absolutePath,
|
|
68
|
+
relativePath: path.join(lifecycle, dagRunId, ...segments),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (matches.length === 0) {
|
|
72
|
+
return { ok: false, reason: "not-found" };
|
|
73
|
+
}
|
|
74
|
+
if (matches.length > 1) {
|
|
75
|
+
return { ok: false, reason: "ambiguous" };
|
|
76
|
+
}
|
|
77
|
+
return { ok: true, result: matches[0] };
|
|
78
|
+
}
|
|
79
|
+
export function resolveDagRunJson(repoRoot, dagRunId) {
|
|
80
|
+
return resolveDagRunArtifact(repoRoot, dagRunId, ["run.json"]);
|
|
81
|
+
}
|
|
82
|
+
export function resolveDagNodePromptRedacted(repoRoot, dagRunId, nodeId) {
|
|
83
|
+
if (!isSafeObservabilityIdentifier(nodeId)) {
|
|
84
|
+
return { ok: false, reason: "invalid-id" };
|
|
85
|
+
}
|
|
86
|
+
return resolveDagRunArtifact(repoRoot, dagRunId, [
|
|
87
|
+
nodeId,
|
|
88
|
+
"prompt.redacted.md",
|
|
89
|
+
]);
|
|
90
|
+
}
|