@tea-agent/loop-agent 0.40.0-next.2 → 0.40.0-next.4
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 +9 -2
- package/README.md +2 -2
- package/dist/application/task-lifecycle/advance.js +12 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/update/runtime-activity.js +1 -29
- package/dist/commands/dag-rerun.js +1 -1
- package/dist/executors/dag-pi-executor.js +46 -5
- package/dist/executors/shell-executor.js +28 -0
- package/dist/executors/workspace-write-snapshot.js +68 -0
- package/dist/shared/dag-prompt-override.js +4 -5
- package/dist/shared/update/npm-client.js +33 -11
- package/dist/task/config-types.js +10 -0
- package/dist/task/contract/apply.js +11 -1
- package/dist/task/contract/constants.js +2 -0
- package/dist/task/contract/hash.js +3 -0
- package/dist/task/contract/observe.js +25 -4
- package/dist/task/contract/paths.js +2 -1
- package/dist/task/contract/project.js +15 -1
- package/dist/task/contract/schema.js +2 -0
- package/dist/task/contract/transaction.js +12 -0
- package/dist/task/source-prepare/completeness.js +188 -0
- package/dist/task/source-prepare/fragment-inventory.js +474 -0
- package/dist/task/source-prepare/index.js +3 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +127 -0
- package/dist/task/source-prepare/ledger-review.js +214 -0
- package/dist/task/source-prepare/ledger.js +545 -0
- package/dist/task/source-prepare/prepare.js +262 -1
- package/dist/task/source-prepare/semantic-intake.js +19 -3
- package/dist/task/source-prepare/source-fidelity-pi.js +384 -0
- package/dist/task/source-prepare/types.js +22 -0
- package/dist/worker/cli.js +12 -26
- package/dist/worker/console/console-handoff.js +82 -0
- package/dist/worker/console/console-update-and-init.js +115 -0
- package/dist/worker/console/console-update-runtime.js +84 -0
- package/dist/worker/console/draft-store.js +49 -0
- package/dist/worker/console/operation-store.js +1 -1
- package/dist/worker/console/operator-actions.js +129 -4
- package/dist/worker/console/operator-surface-health.js +1 -0
- package/dist/worker/console/prd-intake-bridge.js +13 -0
- package/dist/worker/console/routes.js +3 -0
- package/dist/worker/console/server.js +73 -5
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-DLMldIE8.js → abnfDiagram-N423BO3Z-CTx1TrNa.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DVOg1dSW.js → arc-B4i9cVxX.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-715v7joo.js → architectureDiagram-T3A2C74G-BAXE8_Rl.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BtMEGtAQ.js → blockDiagram-VBNYF7ZC-C5Xh59at.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-qUxXsdxu.js → c4Diagram-5PPSVZJV-B_asOzAu.js} +1 -1
- package/dist/worker/console/static/assets/channel-Dj_4Moei.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-BYMag72f.js → chunk-2GRJ4B5K-wRO_iRNc.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-C5jt5rtx.js → chunk-2Q5K7J3B-ByOaSh_G.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DvZghfS2.js → chunk-5RXB4S5H-DjLzudan.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-CXutxAmY.js → chunk-5VM5RSS4-JCNGkXYT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BkANIDWk.js → chunk-6Q2QTUOP-BhrPXXMZ.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-Bwy1wwlr.js → chunk-GF5L2VYU-TJ5JuEBT.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3pWYVCG.js → chunk-JWPE2WC7-VVthn_mD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-DRzjzmFD.js → chunk-KBJHAD2P-D60-Mny4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-C4xXDxXL.js → chunk-RYQCIY6F-zXv7aVye.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BngIZ90b.js → chunk-XXDRQBXY-By9VQoi1.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C0YYAyCa.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-B40ACoFn.js → cose-bilkent-JH36ORCC-C4zQLe-M.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BE-1ZRAI.js → cynefin-VYW2F7L2-wh5N2ReN.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-BoYbwVTh.js → cynefinDiagram-MW4NZA55-Dlu5wIqs.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-B5-KA0fK.js → dagre-VZM6K2ZE-CrDgthc6.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DJpCToT5.js → diagram-7IWD3JNH-zN1clg27.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-De8R04Ng.js → diagram-B4RE2ZJO-BN-o7J7-.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-BU1N8DiA.js → diagram-LBJQPF4R-C8SuPt0d.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-BUUUzSYI.js → diagram-Q27KOJAE-BoizuHbZ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-zLoT1RC4.js → diagram-UB23O5K3-BzUzXixG.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-_q4-4N8c.js → ebnfDiagram-BXEA7PRR-Ev3lA4Cf.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CTqzoNSL.js → erDiagram-JOGREHBK-GALUzRbs.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CMKSYKJM.js → flowDiagram-UKHOOZJN-CZJN65Sb.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-Dj6U8Sb7.js → ganttDiagram-PKOTCBZU-JLB9OESl.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BfoxVYky.js → gitGraphDiagram-DS77QQ5N-CcvCr4GD.js} +1 -1
- package/dist/worker/console/static/assets/{index-CA0nQEbb.js → index-CW88Fjbw.js} +81 -81
- package/dist/worker/console/static/assets/index-DP0ZJl39.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-T5NfvHGV.js → infoDiagram-6WML65LV-HG5z7aVg.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-BEhUMkut.js → ishikawaDiagram-WSZJBQD7-BHEu9chR.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-B2Ld6nNt.js → journeyDiagram-NVQOT4AX-Cp_Up8-a.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-nV9T85yS.js → kanban-definition-27J2QSJJ-DvVeY-em.js} +1 -1
- package/dist/worker/console/static/assets/{linear-CIO1zDYh.js → linear-aGmcjgTn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-B_ucAqLq.js → mermaid.core-Biyo-tqA.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-BIA3VNcZ.js → mindmap-definition-FAOFIHXS-DyxY2pIb.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CNzLjNV3.js → pegDiagram-VL7TDLO6-CkdGuc50.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-PWytzfc_.js → pieDiagram-7S7Q4E2Y-NNQI9xVR.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CS0wGoIq.js → quadrantDiagram-CIZ2JOQS-PAJkuz_G.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-C3Wsqph8.js → railroadDiagram-AXF67PYL-ZwrQod0O.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-DF6FDTYg.js → requirementDiagram-LRYGKXZP-D3ZfrzM2.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BdQUJm6y.js → sankeyDiagram-W5VNT64P-DDGFmWk5.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-Bv9rppGg.js → sequenceDiagram-SI44F4Z6-DO6mmXTA.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-C3Hpvb_B.js → sizeCapture-X5ZJPWSS-DMbCQiJt.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-CZOWLpno.js → stateDiagram-OKZ733FA-Di_YUEfC.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DrdDvela.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CPQRIZx5.js → swimlanes-SLNWSIFB-Bn2PphNJ.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DPkEAULl.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CkwM9BgA.js → timeline-definition-Z64GVDOM-DPh8pRsp.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-wGSoDzYc.js → vennDiagram-T6HMQDX7-GMfl26pW.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-GNVas2gx.js → wardleyDiagram-T6FBY63Y-DTlmVrmF.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-C5USnHw7.js → xychartDiagram-ELKLHX3M-YGBjKAS5.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +40 -1
- package/dist/worker/console/static-src/app/useOperatorActions.js +5 -0
- package/dist/worker/console/static-src/app/useTaskWizard.js +73 -1
- package/dist/worker/observability/init-runtime-activity.js +26 -0
- package/dist/worker/observe/node-input.js +88 -2
- package/dist/worker/observe/routes.js +188 -0
- package/dist/worker/observe/static/api.js +3 -3
- package/dist/worker/observe/static/dag-node-purpose.d.ts +6 -0
- package/dist/worker/observe/static/dag-node-purpose.js +203 -0
- package/dist/worker/observe/static/format.js +37 -0
- package/dist/worker/observe/static/index.html +5 -0
- package/dist/worker/observe/static/prompt-restart-candidates.d.ts +18 -0
- package/dist/worker/observe/static/prompt-restart-candidates.js +36 -0
- package/dist/worker/observe/static/state.js +2 -2
- package/dist/worker/observe/static/styles.css +357 -35
- package/dist/worker/observe/static/views/dag-graph.js +4 -2
- package/dist/worker/observe/static/views/dag-inspector.js +531 -78
- package/dist/worker/observe/static/views/dag.d.ts +13 -0
- package/dist/worker/observe/static/views/dag.js +4 -3
- package/dist/worker/observe/static/views/task.js +55 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +88 -2
- package/dist/workflows/dag/frontend-plan-render.js +24 -0
- package/dist/workflows/dag/frontend-prewrite-gate.js +197 -0
- package/dist/workflows/dag/frontend-review-context.js +92 -10
- package/dist/workflows/dag/init-hybrid.js +144 -4
- package/dist/workflows/dag/node-execution.js +45 -0
- package/dist/workflows/dag/types.js +45 -12
- package/docs/README.md +1 -0
- package/docs/architecture/evolution.md +45 -0
- package/docs/init-surface.manifest.json +1 -0
- package/docs/templates/README.md +1 -1
- package/docs/templates/frontend-implementation-contract.schema.json +681 -24
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +7 -0
- package/dist/worker/console/static/assets/channel-BG6TLen_.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-GuyUqs0h.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-GuyUqs0h.js +0 -1
- package/dist/worker/console/static/assets/index-C3GgHg-g.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-Dgsvn63q.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DaNfG1BJ.js +0 -8
package/AGENTS.md
CHANGED
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
4. 涉及测试纪律/验证声明/调试时继续读:`docs/governance/harness-methodology-*.md`。
|
|
42
42
|
5. 查看最近提交、相关 plan/progress/report;`git status --short --branch`;跑最小基线验证。
|
|
43
43
|
6. 后端/接口/pytest → `taskKind: "backend-test"`(不是 `--profile`);知识回写 `knowledge-sync`;图谱开荒 `knowledge-graph-bootstrap`。`--profile` 仅 `auto|minimal|standard|reviewed|supervised`。
|
|
44
|
-
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;listen 成功后默认打开系统浏览器,`--no-open` 禁止)(`/inspect/`
|
|
44
|
+
7. 看板/observe → `agent-worker console`(默认 repo=当前目录、port=8790;listen 成功后默认打开系统浏览器,`--no-open` 禁止)(`/inspect/` 只读;唯一实验例外:节点 inspector 默认出现「提示词重启」tab 并注入 scoped mutation token;设置 `HARNESS_DAG_RERUN_PROMPT_OVERRIDE=0|false|off` 可紧急关闭,此时零 token 零 tab);`observe serve` 已下线(`OBSERVE_SERVE_REMOVED` + exit 2);`observe snapshot` 仍可用。
|
|
45
45
|
8. 分支合并 → 先读 `docs/operations/branch-merge-guideline.md`。
|
|
46
46
|
9. 涉及版本发布、npm 渠道、release 分支、GitHub Actions 发布链或 dist-tag → 先读 `docs/operations/version-and-release-management.md`。
|
|
47
47
|
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
### 重点更新
|
|
6
6
|
|
|
7
|
+
- Console DAG 检查器「提示词重启」面板改为与检查器一致的卡片层级:版本选择复用自定义下拉;编辑模式与页签同款分段按钮;提交按钮用品牌橙药丸并吸底;追加/替换编辑区放到底稿前面,长底稿默认折叠以免把操作挤出视口。
|
|
8
|
+
- Console「操作」模式新增人类专用的“更新并初始化”入口:默认更新 npm `latest` 稳定版,也可明确选择 `next` 预发布版;先 fail-closed 检查活跃 DAG/Worker/Chat/Console 操作与 npm 来源,再精确安装并验证所选版本、仅执行 `init update --apply-safe`,并以 hash-bound handoff 在原端口接棒新 Console。`latest` 保持稳定版且拒绝回退,`next` 仅在人工选择时允许精确 prerelease;渠道写入 operation 审计身份。Inspect 与模型工具均不能触发该写入操作。启动时不再在终端询问更新,页面会在接棒后自动刷新并通过新的 `consoleInstanceId` 识别实例。
|
|
7
9
|
- 修复 Console 四处评审缺陷(合并前收口):① 向导「运行」进度块接通——`runDag` 只返回 operationId,原实现读的 `recovery.dagRunId` 在向导路径永远为空;现在确认页先快照基线 run、进入运行页后轮询 `observeLink`,以 (taskId, operationId) 任一变化为失效信号(operationId 覆盖「重新运行」按钮——它不改变向导步骤;taskId 覆盖运行步骤中手动改任务 ID 的场景,失效时基线一并重置,避免沿用上一任务的 run):新派发/切任务会清掉旧选中 run 并重新解析,**只接受与当前任务基线不同的新 run**;从 Recovery 带回的共享选中 run 先做归属校验——`GET /api/dag-runs/:id` 详情响应新增 `taskId` 投影(源自 run.json 的 `taskContractBinding`→`sourceBinding`→Hybrid 标题,与 observeLink/candidates 同一绑定真源,不受候选列表 limit 截断影响;404 视为失效选中、无绑定视为不可判定),非本任务的 run 不展示;校验全程由 effect 生命周期栅栏保护——任务/运行/选中在异步校验期间变化时,旧响应直接丢弃、不会改写新状态;校验暂不可用时按有界退避(2s→30s,约 2 分钟)自动重试而非被动等待状态变化,重试计划为纯函数并有行为测试;**展示层另有闸门**——进度面板与运行操作只消费「本解析器已采纳/已验证」的 run(组件 state),未验证期间(校验中、重试中、give-up 后)进度面板保持空缺,不会即时或永久显示其他任务的 DAG;「打开检视」「侧栏预览」两个入口同样双重门禁(按钮无已验证 run 即禁用 + 处理器复核 verifiedRunId),共享的 openInspectSplit 解析器不再能带出未验证的外来 run。解析决策本身抽取为纯状态机 `pages/tasks/run-id-resolution.ts`(首跑/重跑/切任务/外来选中/基线门控五类迁移),Effects 只做薄适配,主流程「确认页基线快照 → runDag 派发 → observeLink 轮询 → 新 run 采纳」有完整行为测试。基线快照为三态(成功/权威无运行/暂不可查),**查询失败不会被当成「没有历史运行」**——快照重试期间基线保持未知、探测轮询等待,不会把旧运行误判为新运行;新 run 创建失败或注册较慢时进度面板宁可空缺也不把上一次已终止的 DAG 当作当前运行(轮询上限 5 分钟后安静停止)。② 失败建议数据契约修正——旅程卡原先把原始 run 级 `failureCategory` 直接查归一化建议表(`missing-api-key`/`nonzero-exit` 等永远查不中,节点分类又在安全投影中被丢弃);现在纯分类核心下沉 `src/shared/dag-failure-category.ts`(console↔workflows 双向共享叶,不破坏 console 禁止 import workflows 的边界测试),服务端投影把 run 级与节点级分类都归一化输出(success/skipped 不出),卡片按「失败节点分类优先、run 级兜底」取值,失败卡始终有可操作建议。③ Recovery interrupt 资格轮询在运行中档位同步刷新 run 状态——原先状态只在选中时读一次,运行中的 run 结束后仍永久保持 4 秒快轮询;现在 4 秒档附带轻量 `GET /api/dag-runs/:id` 刷新 `effectiveStatus`,终态后自动退到 30 秒。④ Inspect 紧凑检视一次瞬时刷新失败不再永久显示错误——成功刷新回合会清除上一轮 error 横幅。
|
|
8
10
|
- Operator Chat 输入框两项交互增强:① **↑/↓ 历史输入召回**——每次发送的文本进入输入历史(连续重复折叠、上限 100 条),光标在首行按 ↑ 逐条向旧回溯、按 ↓ 向新前进,越过最新一条时恢复回溯前**未发送的草稿**(草稿不会被历史浏览冲掉);多行文本中部的方向键仍是普通光标移动,IME 组词与 @文件/命令面板打开时不抢占。② **运行中排队下一轮指令**——当前回合运行中 Enter 不再被吞掉,消息作为「排队消息」绑定当前 Turn(服务端 `PUT deferred-turn` 从仅限 stopping 放开为 running/stopping,错误码 `TURN_NOT_STOPPING`→`TURN_NOT_ACTIVE`),当前回合结束(正常完成或停止)后自动发送;排队条提供「立即发送」(停止当前回复并立即发出)、「编辑」(移回输入框改写后再排)、「删除」三种操作,运行中输入框右侧提供排队发送按钮(与停止按钮并列);排队条单行展示(状态 + 省略号预览 + 操作按钮一行),输入框默认一行高(自动增高上限 280px 不变)。
|
|
9
11
|
- 向导「运行」面板结构化与预期管理(A2/C9):新增实时运行进度块——节点完成计数与百分比进度条(含 progressbar 无障碍语义)、当前运行节点名称与已运行时长、失败节点与错误摘要,数据来自 `/api/dag-runs/:id` 每 5 秒轮询并在终态自动停止;`effectiveStatus=running-suspected-stall` 时显示橙色「疑似停滞」提示条(建议打开检视查看节点活动或到恢复页协作中止/诊断),把 20–40 分钟长跑的「日志噪音等待」变为有阶段感、可判断的等待。原始 SSE 事件日志默认折叠为「原始事件日志(最近 80 条)」,需要排查时再展开。
|
|
@@ -29,9 +31,11 @@
|
|
|
29
31
|
|
|
30
32
|
### 新增
|
|
31
33
|
|
|
34
|
+
- Frontend source fidelity 生产 Pi 接线(2026-08-24-source-fidelity-production-pi-wiring):真实 `task advance` 的 prepare-contract 主路径与 semantic-intake retry 路径现在对 `frontend-implementation` 任务注入生产 reconciliation executor 与独立 reviewer(`src/task/source-prepare/source-fidelity-pi.ts`):叙述性/歧义片段按标题顺序串行、逐块 `budgetChars=4000` 有界调用,结构化显式 AC/checklist/table 片段仍走 `isStructuredMappableFragment` 代码映射且零 Pi 调用;context-only/duplicate/conflict 在 reconciliation 完成后的独立阶段由 Pi reviewer 审核(`LEDGER_REVIEW_MAX_REVISIONS=1`,第二次拒绝 fail closed)。接线侧门 = `taskKind === 'frontend-implementation'`(`input.taskKind` 优先,缺省回退 `loadTaskConfig`),ineligible 不接线、行为不变。模型队列镜像 semantic-intake(MED→HIGH→LOW + Pi SDK 目录),单次尝试沿用有界超时,工具面仅只读 read/grep/find/ls;模型输出经 validateOutput + zod 严格校验,无效 JSON/schema、越界 canonical ID、排除类 disposition 缺理由、队列空、执行失败/超时一律以稳定 `SOURCE_FIDELITY_*` 阻断码 fail closed,生产路径无确定性 reviewer 回退。Mock 策略 not-needed(无 HTTP API 面,Pi 为 CLI/SDK 模型路径,不添加 Mock 文件/依赖);聚焦测试在 Pi 执行边界打桩(`vi.mock` / 可注入 `executePi` 桩,默认仍为真实 `executePiStep`),覆盖生产生命周期接线、reconcile→review 调用序、结构化零调用、fail-closed 阻断码与 ineligible 零调用。
|
|
35
|
+
- Frontend 原始需求完整性门禁修复 v2(source fidelity ledger repair):① DAG 绑定改读 Task Contract 已持久化的 `source/requirement-ledger.json`(与 intake/apply/prewrite 同一份受管账本的原始字节/hash/inputDigest),删除从 materialized reference path 重建 ledger 的逻辑;缺失/损坏/漂移 fail closed(`SOURCE_FIDELITY_LEDGER_MISSING`/`SOURCE_FIDELITY_LEDGER_STALE`)。② `dagSourceBindingSchema` 拆为 v1(strict 拒绝 ledger 字段)与 v2(schemaVersion=2,`ledgerPath`/`ledgerSha256`/`inputDigest`/`requirementToFragments` 全部必填)的 discriminated union,禁止 v1+optional 冒充 v2;契约级 sourceBinding 增加 all-or-none 约束。③ canonical source identity 单一边界:ledger `sourceDocuments[].path` 与 DAG 侧 `serializeDagTaskSourcePath`/`resolveDagTaskSourcePath` 一致(repo-root relative),路径不再二次转换进入 digest。④ 确定性分片补全:Markdown 表格数据行独立切分、跨文档稳定哈希 `REQ-SRC-*` ID(消除 per-document counter reset 碰撞、文档枚举顺序无关)、V4 `#### AC-*` 标题式显式 ID 原样保留并关联正文、fragment ID 防伪造校验。⑤ 顺序 reconciliation(`ledger-reconciliation.ts`):可注入 executor 显式接口,仅叙述性/歧义 chunk 调用、按标题顺序串行、逐块有界,失败/无效返回/identity 越界 fail closed。⑥ 独立 reviewer(`ledger-review.ts`):context-only/duplicate/conflict 独立审核(不同调用阶段),最多一次有界修订,低风险 context-only 仅 reviewer pass 放行、高风险始终阻断。⑦ human resolution 真正生效:与当前 inputDigest + conflict/fragment identity 匹配且 human-confirmed 的 resolution 解除 blocking conflict/unresolved 并保留 audit,stale/伪造/未确认不生效。⑧ Console/Observe 展示 v2 binding 语义(`binding=v2`、ledger sha256、inputDigest)与稳定 failure code;`GET /api/tasks/:id/ledger` 增加绑定语义字段(向后兼容)。
|
|
32
36
|
- Operator Chat 时间线左缘新增「用户消息锚点栏」:每个用户输入一枚横向小条(12×3px 微圆角),紧凑等距排列并在时间线高度内垂直居中;条数超出栏高时相邻锚点自动聚合为稍宽的聚合条(悬停展开「N 条聚合输入」逐条列表,时间 + 单行预览,逐条点击定位)。悬停时小条向对话一侧平滑伸长(12→18px)并变橙,浮窗右侧弹出(白底圆角,正文最多 3 行、超出省略号,附序号与时间);单击平滑滚动定位到对应消息(精确停靠在视口顶部偏移 8px);覆盖视口顶部的锚点保持高亮。空会话自动隐藏;历史分页/流式更新/窗口缩放时自动重算(rAF 节流)。
|
|
33
37
|
- frontend-test 产物根可配置:`task.json.frontendTest.testRoot` 缺省仍为 `testcase/frontend`,省略时生成的 DAG 路径与历史默认根一致;自定义根(如 `qa/frontend-test`)在编译期冻结并驱动 writer、gate、playwright 围栏与 `allowedPaths`。`.harness/**` 等隐藏/治理树在生成期拒绝。
|
|
34
|
-
-
|
|
38
|
+
- 实验能力「编辑失败节点提示词后从节点重跑」默认开放:无需环境变量即可使用 `dag rerun --prompt-override <nodeId>@<file>`(全文替换)与 `--prompt-override-append <nodeId>@<file>`(追加指令:保留冻结原提示词,末尾追加 `<operator_addendum>` 块);`HARNESS_DAG_RERUN_PROMPT_OVERRIDE=0|false|off` 保留为紧急关闭开关。仅限 ERROR 且位于 reset closure 内的节点;mode + base/最终 prompt sha256 计入 planHash(execute 漂移即 `PLAN_DRIFT`),父 run spec 冻结、provenance(含 mode)落 `continuation.promptOverrides`。Console 新增 `dagPromptOverrideConfig` 开关探测 action,`dagRerunPlan`/`dagRerun` 接受 `promptOverrides`(`{nodeId, prompt, mode?}`);Inspect DAG 节点 inspector 新增「提示词重启」tab——默认「追加指令」模式(只读底稿 + 追加编辑器),可切换「全文替换(高级)」,预检通过展示执行估算,提交后续跑并跳转新 run,全程不离开图页;默认开启时 `/inspect` 只注入 **scoped** 变体凭据(独立 cookie + `__INSPECT_MUTATION_TOKEN__`,与 Console 通用 token 互不可重放),mutations 路由对 inspect 凭据仅放行 `dagRerunPlan`/`dagRerun`(其余 action 403 `ACTION_NOT_IN_INSPECT_SCOPE`),Inspect 不获得通用写能力;显式关闭时不注入任何 token、tab 不出现,检视面保持严格只读。
|
|
35
39
|
- 观察面 DAG Inspector 节点详情适配 Pi 扩展:执行器输入摘要展示节点声明的 `piExtensions` 短 id;新增「Pi 扩展运行事实」展示 `pi-extensions.json` 的 resolved(包名@版本)与 missing 降级原因(缺包降级不再静默);产物未生成/损坏时降级提示不阻断接口,绝对路径不外泄。
|
|
36
40
|
- 后端实现 DAG(standard / review-gated / supervised)的 Pi 节点支持按桶显式加载本机 Pi 导航扩展:读侧节点(除 closeout)加载 `pi-codegraph` + `pi-lens`,写侧节点只加载 `pi-codegraph`(pi-lens 的 `ast_grep_replace` 可绕过 writeSet,不加载);前端/测试模板与省略字段节点保持扩展全关。扩展发现始终关闭,只经白名单解析 `~/.pi/agent` 与项目 `.pi` settings 的已装包(缺包降级不失败,产物 `pi-extensions.json` 记录 requested/resolved/missing);session 后 pin 工具面剔 builtin 同名、deny 面与写工具,writer 的 `edit`/`write` 仍为 policy-wrapped;`.codegraph/**` index 同步不再计入 write-guard 越界。
|
|
37
41
|
- Pi SDK 终态 session-events JSONL 顶层增加可选 `finish_reason`:仅当 `message_end` / `turn_end` / `assistant_message` 明确携带非空终止原因时由序列化层提升,不伪造默认值,也不回流 `stopReason` 诊断。
|
|
@@ -57,6 +61,9 @@
|
|
|
57
61
|
|
|
58
62
|
### 改进
|
|
59
63
|
|
|
64
|
+
- 检查器「提示词重启」只读态的提示词卡片会撑满剩余高度并在卡片内滚动,不再在面板底部留下大块空白;失败节点的可编辑布局不变。
|
|
65
|
+
- DAG 节点表「备注」改为节点用途简介(是什么、做什么、目的),不再重复节点 ID;失败节点仍显示错误摘要。检查器标题与图节点悬浮提示同步展示同一句说明。
|
|
66
|
+
- 实验「提示词重启」会为 Pi 节点的每次失败 attempt 保存独立、脱敏且 64 KiB 有界的提示词候选;Inspector 始终展示提示词版本选择框,新 run 会列出冻结原始提示词与全部失败尝试(含自动重试产生的恢复提示词),历史 run 未保存候选时明确说明,成功尝试不混入。提示词卡片右上角提供复制图标并以勾选图标反馈成功;已截断候选复制时明确标记为截断预览,并保持只读、禁止直接提交,避免用不完整提示词发起续跑。选择失败候选会载入「全文替换」编辑器;即使节点后续自动重试成功,历史失败候选仍可只读切换查看。
|
|
60
67
|
- DAG 节点检查器「执行过程」按执行器分流:Pi 仍看 session-events;有命令的 shell 展示 `shell-command-receipt` 与 stdout/stderr;`frontendBrowserToolPreflight` 等内部处理器展示步骤回执(历史 run 可从 capability / attempt 产物合成)。空态不再把 shell/static 误说成「暂无过程日志」。节点输入补充 named handler 名称。脚本仍属于节点输入,不假装已执行。
|
|
61
68
|
- DAG 节点检查器「节点输出」在整段内容为完整 JSON 对象或数组时改为缩进代码块展示,不再把紧凑 `JSON.stringify` 挤成一段 Markdown 段落;截断或非法 JSON、普通 Markdown 仍走原阅读器。
|
|
62
69
|
- frontend DAG 的 materialized source references 恢复统一的有界 excerpt 与按需 read 指针,不再把 requirement/acceptance 文档全文复制进每个相关节点,也不再强制 contract/plan 重读全部绑定源文件,降低长需求在多节点中的重复上下文占用。
|
|
@@ -65,7 +72,7 @@
|
|
|
65
72
|
- frontend-test 报告解析用例 Markdown 的 `##`/`###` 标题与有序列表(`测试步骤` / `预期结果` / `前置条件与重置`);缺 `测试目的` 时回退到用例 H1,避免主 HTML 出现空步骤。
|
|
66
73
|
- `dag rerun --from-node` 把尚未启动的 writer/map 视为首次执行;frontend-test probe 这种失败的 exclusive shell 可以带着下游未跑节点续跑,不必整图 `standaloneTaskRerun`。
|
|
67
74
|
- Pi SDK 节点在 stall watchdog(默认 15 分钟无 provider 活动)触发后,会先 abort 当前等待,并在同一会话自动发送一次「继续」;仅 abort 已确认且尚未调用 write 工具时恢复,失败仍判 timeout。绝对墙钟超时与 CLI 后端不走这条恢复。
|
|
68
|
-
- DAG
|
|
75
|
+
- DAG 节点检查器「执行过程」时间线默认隐藏原始协议事件(`message_start` / `message_end`),按钮文案为「展开原始协议事件」;用户可点选展开,同会话内选择会保持。
|
|
69
76
|
- Operator Chat 的乐观停止现在把“视觉已停止”和 Turn ownership 分开:Abort 获得可靠终态前明确显示“正在停止”,此时可为原会话保存一条服务端持久化的“停止后发送”消息;后一次覆盖前一次,切换会话、刷新页面或 Console 重启不会丢失,旧 Turn 结束后复用正常 Turn 准入路径自动发送,并以固定 `clientRequestId` 防止重复执行。
|
|
70
77
|
- Operator Chat 修复快速终止时“停止后发送”与普通发送之间的准入空洞:发送路径改读实时 ownership;若 deferred 请求恰好撞上旧 Turn 终态,会先按服务端状态对账,再使用同一 `clientRequestId` 自动转为普通发送,不再要求用户刷新重试或丢失本次提交。
|
|
71
78
|
- `dag rerun-task` 现在会把父 run 最后一轮仍未解决的 `verify-pi` / review 反馈、证据引用和修订轮次带入新完整 DAG;planner、implementer 与 verifier/reviewer 必须修复或举证说明不再适用,不能仅因文件已存在就返回 `already-satisfied`。Console 同步区分「自动修订」「从失败节点恢复」「带反馈重新执行任务」。
|
package/README.md
CHANGED
|
@@ -116,7 +116,7 @@ loop-agent dag report --run-id <run-id>
|
|
|
116
116
|
| --- | --- | --- |
|
|
117
117
|
| 受治理 CLI | `loop-agent …` / `agent-worker …` | 权威写入与诊断 |
|
|
118
118
|
| Loop Operator Console | `agent-worker console` | Official 本地控制面(默认 repo=当前目录、`127.0.0.1:8790`,含 Operate 与 Inspect) |
|
|
119
|
-
| Inspect(只读) | `agent-worker console` → `/inspect/`;`observe snapshot` | 统一 Console 内只读检视;`observe serve` 已下线(REMOVED / exit 2
|
|
119
|
+
| Inspect(只读) | `agent-worker console` → `/inspect/`;`observe snapshot` | 统一 Console 内只读检视;`observe serve` 已下线(REMOVED / exit 2)。实验例外:节点 inspector 默认显示失败节点「提示词重启」tab;紧急回退可设置 `HARNESS_DAG_RERUN_PROMPT_OVERRIDE=0`(也接受 `false` / `off`),关闭后不注入任何 mutation token |
|
|
120
120
|
|
|
121
121
|
```bash
|
|
122
122
|
# 终端 A:Console(任务/运行操作面;canonical mutation 只经 sibling loop-agent)
|
|
@@ -126,7 +126,7 @@ agent-worker console doctor --repo .
|
|
|
126
126
|
|
|
127
127
|
```
|
|
128
128
|
|
|
129
|
-
裸 `agent-worker console`
|
|
129
|
+
裸 `agent-worker console` 不在终端询问升级。Operate 右上角提供非阻塞的更新入口,默认选择 npm `latest` 稳定版,也可由操作者显式选择 `next` 预发布版;更新前会先检查活跃运行和全局安装来源。`console doctor`、帮助和版本命令均不触发更新检查。
|
|
130
130
|
|
|
131
131
|
访问 Console <http://127.0.0.1:8790/>;**Operator Chat Official** 是 Console 内的主引导交互,默认打开「对话」工作区。构建时设置 `VITE_OPERATOR_CHAT_DEFAULT_LANDING=tasks` 可改回 Tasks;URL hash(`#/tasks`)与 localStorage 仍优先于默认值。顶部「检视」以及 `/inspect/` 提供只读 Dashboard、DAG、时间线与 artifacts;`loop-agent …` / `agent-worker …` CLI direct path 仍是受治理 fallback。当前状态为 **code ready, dogfood pending/unmet**,不宣称已替代 openCode 或其他外部主会话。
|
|
132
132
|
|
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { createTask, getTaskPaths, loadTaskConfig, } from "../../task/runtime.js";
|
|
4
4
|
import { importPrdDocument } from "../../task/source-references.js";
|
|
5
5
|
import { prepareTaskSource } from "../../task/source-prepare/index.js";
|
|
6
|
+
import { resolveProductionSourceFidelityPi } from "../../task/source-prepare/source-fidelity-pi.js";
|
|
6
7
|
import { isIntakeSoftGapCode, resolvePrdImportRole, } from "../../task/source-prepare/artifact-meta.js";
|
|
7
8
|
import { sanitizeDraftPlaceholders } from "../../task/source-prepare/placeholder-paths.js";
|
|
8
9
|
import { evaluateSemanticIntakeEligibility, isSemanticIntakeDisabled, readSemanticIntakeAttempted, runSemanticIntake, } from "../../task/source-prepare/semantic-intake.js";
|
|
@@ -585,6 +586,15 @@ export async function advanceTaskLifecycle(input) {
|
|
|
585
586
|
continue;
|
|
586
587
|
}
|
|
587
588
|
if (action.type === "prepare-contract") {
|
|
589
|
+
// 生产 source fidelity Pi 接线(AC-PROD-001/005):仅 frontend-implementation
|
|
590
|
+
// 任务注入 reconcile executor + 独立 reviewer;ineligible 返回 undefined 不接线。
|
|
591
|
+
// 主路径与 semantic-intake retry 路径共用同一接线,保证任意一条生产路径都实际
|
|
592
|
+
// 触发 reconciliation executor(修复前两处均缺省导致生产路径从不调用 Pi)。
|
|
593
|
+
const sourceFidelity = await resolveProductionSourceFidelityPi({
|
|
594
|
+
repoRoot: input.repoRoot,
|
|
595
|
+
taskId: input.taskId,
|
|
596
|
+
taskKind: input.taskKind,
|
|
597
|
+
});
|
|
588
598
|
let draftIntent = null;
|
|
589
599
|
if (input.fromDraftPath) {
|
|
590
600
|
try {
|
|
@@ -635,6 +645,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
635
645
|
verifyCommands: effectiveInput.verifyCommands,
|
|
636
646
|
backendTest: input.backendTest,
|
|
637
647
|
},
|
|
648
|
+
...(sourceFidelity ? { sourceFidelity } : {}),
|
|
638
649
|
});
|
|
639
650
|
if (!prepareResult.ok) {
|
|
640
651
|
const gapCodes = (prepareResult.gaps ?? [])
|
|
@@ -712,6 +723,7 @@ export async function advanceTaskLifecycle(input) {
|
|
|
712
723
|
verifyCommands: effectiveInput.verifyCommands,
|
|
713
724
|
backendTest: input.backendTest,
|
|
714
725
|
},
|
|
726
|
+
...(sourceFidelity ? { sourceFidelity } : {}),
|
|
715
727
|
});
|
|
716
728
|
if (retry.ok) {
|
|
717
729
|
warnings.push({
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.40.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.40.0-next.4",
|
|
4
|
+
"gitSha": "84dabbbd0cdd880a29ed8cf22e11feb7f4550188",
|
|
5
|
+
"builtAt": "2026-08-26T13:58:53.067Z"
|
|
6
6
|
}
|
|
@@ -1,29 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { buildGlobalSnapshot } from "../../worker/observability/read-model.js";
|
|
4
|
-
/**
|
|
5
|
-
* Read-only runtime guard for init-surface writes.
|
|
6
|
-
*
|
|
7
|
-
* DAG activeness is anchored to real directories in the canonical active
|
|
8
|
-
* lifecycle root. Worker activeness reuses the Observe read model so Task Pool
|
|
9
|
-
* status precedence and composite identity stay owned by the Worker adapter.
|
|
10
|
-
* Projection failures block writes because liveness cannot be proven safely.
|
|
11
|
-
*/
|
|
12
|
-
export async function readInitRuntimeActivity(repoRoot) {
|
|
13
|
-
const resolvedRoot = path.resolve(repoRoot);
|
|
14
|
-
const activeDagRoot = path.join(resolvedRoot, ".harness", "dag-runs", "active");
|
|
15
|
-
const entries = await readdir(activeDagRoot, { withFileTypes: true }).catch(() => []);
|
|
16
|
-
const activeDagRunIds = entries
|
|
17
|
-
.filter((entry) => entry.isDirectory() && entry.name !== ".gitkeep")
|
|
18
|
-
.map((entry) => entry.name)
|
|
19
|
-
.sort();
|
|
20
|
-
const snapshot = await buildGlobalSnapshot({ repoRoot: resolvedRoot });
|
|
21
|
-
return {
|
|
22
|
-
activeDagRunIds,
|
|
23
|
-
activeWorkerBatches: snapshot.health.worker.activeBatches,
|
|
24
|
-
activeWorkerTasks: snapshot.health.worker.activeTasks,
|
|
25
|
-
...(snapshot.projectionError
|
|
26
|
-
? { workerProjectionError: snapshot.projectionError.message }
|
|
27
|
-
: {}),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
1
|
+
export { readInitRuntimeActivity } from "../../worker/observability/init-runtime-activity.js";
|
|
@@ -117,7 +117,7 @@ export async function runDagRerun(repoRoot, rawArgs) {
|
|
|
117
117
|
parsed.promptOverrideAppendSpecs.length > 0;
|
|
118
118
|
if (hasOverrides) {
|
|
119
119
|
if (!isDagRerunPromptOverrideEnabled()) {
|
|
120
|
-
throw new Error("prompt override rerun is disabled
|
|
120
|
+
throw new Error("prompt override rerun is disabled by HARNESS_DAG_RERUN_PROMPT_OVERRIDE; unset it or use a value other than 0, false, or off to enable this experimental capability");
|
|
121
121
|
}
|
|
122
122
|
promptOverrides = [
|
|
123
123
|
...(await loadPromptOverrides(parsed.promptOverrideSpecs, "replace")),
|
|
@@ -9,6 +9,7 @@ import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND
|
|
|
9
9
|
import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
|
|
10
10
|
import { redactPromptForLog, truncateOutput, } from "../shared/output-truncation.js";
|
|
11
11
|
import { GitStatusUnavailableError, pathsChangedDuringRun, readGitStatusPorcelain, recoverRootNulArtifact, snapshotGitStatusPathFingerprints, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
12
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
12
13
|
import { isTargetTemplateTransientRetryNode, isWriterEmptyDiffRetryCandidate, isWriterTransportRetryCandidate, INCOMPLETE_WRITE_SET_RETRY_CATEGORY, WRITER_CLEAN_TIMEOUT_RETRY_CATEGORY, WRITER_EMPTY_DIFF_RETRY_CATEGORY, } from "../workflows/dag/retry-policy.js";
|
|
13
14
|
import { assessBackendTestMdPlanCompleteness, assessBackendTestMdWriterCompleteness, assessBackendTestPytestPlanCompleteness, assessBackendTestPytestWriterCompleteness, assessBackendTestShardChildCompleteness, backendTestWriterProgressRoleForTask, classifyBackendTestWriterCompletenessFailure, isBackendTestCompletenessRetryCandidate, isBackendTestMdPlanTask, isBackendTestPytestPlanTask, isBackendTestShardChildTask, writeBackendTestWriterProgressArtifacts, } from "../workflows/dag/backend-test-writer-completeness.js";
|
|
14
15
|
import { resolveBackendTestLayout } from "../workflows/dag/backend-test-layout.js";
|
|
@@ -446,9 +447,28 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
446
447
|
};
|
|
447
448
|
}
|
|
448
449
|
let beforeStatus;
|
|
450
|
+
let beforeWorkspaceSnapshot;
|
|
449
451
|
let beforePathFingerprints;
|
|
450
|
-
/** tools-only
|
|
451
|
-
const
|
|
452
|
+
/** tools-only and filesystem-only keep the tool sandbox but skip Git baseline. */
|
|
453
|
+
const filesystemOnly = isWriteTask && meta.spec.backendTestWorkspaceControl === "filesystem-only";
|
|
454
|
+
const skipGitWriteGuard = isWriteTask && (input.task.writeGuardPolicy === "tools-only" || filesystemOnly);
|
|
455
|
+
if (isWriteTask && filesystemOnly) {
|
|
456
|
+
try {
|
|
457
|
+
beforeWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
458
|
+
rootCwd: input.cwd,
|
|
459
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return {
|
|
464
|
+
ok: false,
|
|
465
|
+
stdout: "",
|
|
466
|
+
stderr: `workspace filesystem baseline unavailable before Pi execution: ${error instanceof Error ? error.message : String(error)}`,
|
|
467
|
+
failureCategory: "write-guard",
|
|
468
|
+
durationMs: Date.now() - started,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
}
|
|
452
472
|
if (isWriteTask && !skipGitWriteGuard) {
|
|
453
473
|
try {
|
|
454
474
|
beforeStatus = await writeGuardDependencies.readGitStatusPorcelain(input.cwd, { phase: "pi-writer-before" });
|
|
@@ -678,6 +698,27 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
678
698
|
let writeGuardViolations = [];
|
|
679
699
|
let changeManifestAfterStatus;
|
|
680
700
|
let changeManifestChangedFiles;
|
|
701
|
+
let afterWorkspaceSnapshot;
|
|
702
|
+
if (filesystemOnly && beforeWorkspaceSnapshot) {
|
|
703
|
+
try {
|
|
704
|
+
afterWorkspaceSnapshot = await captureWorkspaceWriteSnapshot({
|
|
705
|
+
rootCwd: input.cwd,
|
|
706
|
+
paths: input.task.writeSet ?? input.task.allowedPaths,
|
|
707
|
+
});
|
|
708
|
+
changeManifestChangedFiles = diffWorkspaceWriteSnapshots(beforeWorkspaceSnapshot, afterWorkspaceSnapshot);
|
|
709
|
+
const guard = validateShellWriteGuardFromDiff({
|
|
710
|
+
changedFiles: changeManifestChangedFiles,
|
|
711
|
+
task: input.task,
|
|
712
|
+
concurrentSiblingWriteSets: meta.concurrentSiblingWriteSets,
|
|
713
|
+
});
|
|
714
|
+
writeGuardOk = guard.ok;
|
|
715
|
+
writeGuardViolations = guard.violations;
|
|
716
|
+
}
|
|
717
|
+
catch (error) {
|
|
718
|
+
writeGuardOk = false;
|
|
719
|
+
writeGuardViolations = [`workspace snapshot unavailable: ${error instanceof Error ? error.message : String(error)}`];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
681
722
|
if (beforeStatus !== undefined) {
|
|
682
723
|
let recoveryEvidence;
|
|
683
724
|
let removalPendingRecheck;
|
|
@@ -850,7 +891,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
850
891
|
}
|
|
851
892
|
}
|
|
852
893
|
if (writeGuardOk &&
|
|
853
|
-
beforeStatus !== undefined &&
|
|
894
|
+
(beforeStatus !== undefined || beforeWorkspaceSnapshot !== undefined) &&
|
|
854
895
|
changeManifestChangedFiles !== undefined) {
|
|
855
896
|
await persistWriterChangeManifest({
|
|
856
897
|
runDir: meta.runDir,
|
|
@@ -860,8 +901,8 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
860
901
|
approvalSourceNodeId: input.task.resolvedFinalWriteSetApproval?.approvalSourceNodeId,
|
|
861
902
|
approvalDigest: input.task.resolvedFinalWriteSetApproval?.approvalDigest,
|
|
862
903
|
changedFiles: changeManifestChangedFiles,
|
|
863
|
-
beforeStatus,
|
|
864
|
-
afterStatus: changeManifestAfterStatus ??
|
|
904
|
+
beforeStatus: beforeStatus ?? JSON.stringify(beforeWorkspaceSnapshot),
|
|
905
|
+
afterStatus: changeManifestAfterStatus ?? JSON.stringify(afterWorkspaceSnapshot),
|
|
865
906
|
});
|
|
866
907
|
}
|
|
867
908
|
let completenessFailure;
|
|
@@ -41,6 +41,7 @@ import { computeL5ReportMetrics } from "../workflows/dag/l5-report-metrics.js";
|
|
|
41
41
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
42
42
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
43
43
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
44
|
+
import { captureWorkspaceWriteSnapshot, diffWorkspaceWriteSnapshots, } from "./workspace-write-snapshot.js";
|
|
44
45
|
import { parseAndValidateFinalWriteSetApproval, } from "../workflows/dag/node-execution.js";
|
|
45
46
|
import { buildShellProcessEnv } from "./shell-verification.js";
|
|
46
47
|
import { readRunState } from "../workflows/dag/run-store.js";
|
|
@@ -1719,6 +1720,33 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
1719
1720
|
}
|
|
1720
1721
|
}
|
|
1721
1722
|
async function executeBackendTestPipelineWithWriteGuard(input, meta) {
|
|
1723
|
+
if (meta.spec.backendTestWorkspaceControl === "filesystem-only") {
|
|
1724
|
+
const paths = input.task.writeSet ?? input.task.allowedPaths;
|
|
1725
|
+
let beforeSnapshot;
|
|
1726
|
+
try {
|
|
1727
|
+
beforeSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1728
|
+
}
|
|
1729
|
+
catch (error) {
|
|
1730
|
+
return { ok: false, stdout: "", stderr: `workspace filesystem baseline unavailable: ${error instanceof Error ? error.message : String(error)}`, failureCategory: "write-guard", durationMs: 0 };
|
|
1731
|
+
}
|
|
1732
|
+
const result = await executeBackendTestPipeline(input, meta);
|
|
1733
|
+
try {
|
|
1734
|
+
const afterSnapshot = await captureWorkspaceWriteSnapshot({ rootCwd: input.cwd, paths });
|
|
1735
|
+
const guard = validateShellWriteGuard({
|
|
1736
|
+
changedPaths: diffWorkspaceWriteSnapshots(beforeSnapshot, afterSnapshot),
|
|
1737
|
+
writePolicy: input.task.writePolicy,
|
|
1738
|
+
writeSet: input.task.writeSet,
|
|
1739
|
+
allowedPaths: input.task.allowedPaths,
|
|
1740
|
+
forbiddenPaths: input.task.forbiddenPaths,
|
|
1741
|
+
});
|
|
1742
|
+
if (!guard.ok)
|
|
1743
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${guard.violations.join(", ")}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1744
|
+
return result;
|
|
1745
|
+
}
|
|
1746
|
+
catch (error) {
|
|
1747
|
+
return { ...result, ok: false, stderr: [result.stderr, `filesystem write guard failed: ${error instanceof Error ? error.message : String(error)}`].filter(Boolean).join("\n\n"), failureCategory: "write-guard" };
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1722
1750
|
let beforeStatus;
|
|
1723
1751
|
try {
|
|
1724
1752
|
beforeStatus = await readGitStatusPorcelain(input.cwd);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readdir, readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { pathMatchesPattern } from "../shared/git-progress.js";
|
|
5
|
+
const normalize = (value) => value.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
6
|
+
function rootBeforeWildcard(pattern) {
|
|
7
|
+
const value = normalize(pattern);
|
|
8
|
+
const wildcard = value.search(/[?*]/);
|
|
9
|
+
if (wildcard < 0)
|
|
10
|
+
return path.posix.dirname(value);
|
|
11
|
+
const prefix = value.slice(0, wildcard);
|
|
12
|
+
return prefix.slice(0, prefix.lastIndexOf("/")) || ".";
|
|
13
|
+
}
|
|
14
|
+
async function walk(rootCwd, relative, files) {
|
|
15
|
+
let entries;
|
|
16
|
+
try {
|
|
17
|
+
entries = await readdir(path.resolve(rootCwd, relative), { withFileTypes: true });
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
if ([".git", ".harness", "node_modules"].includes(entry.name))
|
|
24
|
+
continue;
|
|
25
|
+
const child = normalize(path.posix.join(relative, entry.name));
|
|
26
|
+
if (entry.isDirectory())
|
|
27
|
+
await walk(rootCwd, child, files);
|
|
28
|
+
else if (entry.isFile())
|
|
29
|
+
files.add(child);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function captureWorkspaceWriteSnapshot(input) {
|
|
33
|
+
const candidates = new Set();
|
|
34
|
+
for (const pattern of input.paths) {
|
|
35
|
+
await walk(input.rootCwd, rootBeforeWildcard(pattern), candidates);
|
|
36
|
+
if (!/[?*]/.test(pattern)) {
|
|
37
|
+
try {
|
|
38
|
+
if ((await lstat(path.resolve(input.rootCwd, pattern))).isFile())
|
|
39
|
+
candidates.add(normalize(pattern));
|
|
40
|
+
}
|
|
41
|
+
catch { /* absent target */ }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const files = {};
|
|
45
|
+
let totalBytes = 0;
|
|
46
|
+
for (const relative of [...candidates].filter((file) => input.paths.some((pattern) => pathMatchesPattern(file, pattern))).sort()) {
|
|
47
|
+
if (Object.keys(files).length >= 20_000)
|
|
48
|
+
throw new Error("workspace snapshot file limit exceeded");
|
|
49
|
+
const absolute = path.resolve(input.rootCwd, relative);
|
|
50
|
+
const stat = await lstat(absolute);
|
|
51
|
+
if (!stat.isFile())
|
|
52
|
+
throw new Error(`workspace snapshot encountered non-regular file: ${relative}`);
|
|
53
|
+
totalBytes += stat.size;
|
|
54
|
+
if (totalBytes > 64 * 1024 * 1024)
|
|
55
|
+
throw new Error("workspace snapshot byte limit exceeded");
|
|
56
|
+
files[relative] = { size: stat.size, sha256: createHash("sha256").update(await readFile(absolute)).digest("hex") };
|
|
57
|
+
}
|
|
58
|
+
const body = JSON.stringify(files);
|
|
59
|
+
return { schemaVersion: 1, controlMode: "filesystem-only", files, sha256: createHash("sha256").update(body).digest("hex") };
|
|
60
|
+
}
|
|
61
|
+
export function diffWorkspaceWriteSnapshots(before, after) {
|
|
62
|
+
const paths = new Set([...Object.keys(before.files), ...Object.keys(after.files)]);
|
|
63
|
+
return [...paths].sort().filter((file) => {
|
|
64
|
+
const left = before.files[file];
|
|
65
|
+
const right = after.files[file];
|
|
66
|
+
return !left || !right || left.size !== right.size || left.sha256 !== right.sha256;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Internal switch for the experimental "rerun failed node with an edited
|
|
3
|
-
* prompt" capability.
|
|
4
|
-
*
|
|
5
|
-
* feature is internal.
|
|
3
|
+
* prompt" capability. The feature is enabled by default; the environment
|
|
4
|
+
* variable remains as an emergency kill switch while the feature is internal.
|
|
6
5
|
*/
|
|
7
6
|
export const DAG_RERUN_PROMPT_OVERRIDE_ENV = "HARNESS_DAG_RERUN_PROMPT_OVERRIDE";
|
|
8
7
|
/** Upper bound for an override input body (characters). */
|
|
9
8
|
export const MAX_DAG_RERUN_PROMPT_OVERRIDE_CHARS = 65_536;
|
|
10
9
|
export function isDagRerunPromptOverrideEnabled(env = process.env) {
|
|
11
|
-
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV];
|
|
12
|
-
return value
|
|
10
|
+
const value = env[DAG_RERUN_PROMPT_OVERRIDE_ENV]?.trim().toLowerCase();
|
|
11
|
+
return value !== "0" && value !== "false" && value !== "off";
|
|
13
12
|
}
|
|
14
13
|
export const DAG_RERUN_PROMPT_ADDENDUM_TAG = "operator_addendum";
|
|
15
14
|
/**
|
|
@@ -11,10 +11,10 @@ export function createNpmUpdateClient(options) {
|
|
|
11
11
|
const timeoutMs = options.timeoutMs ?? DEFAULT_NPM_QUERY_TIMEOUT_MS;
|
|
12
12
|
const cwd = options.cwd;
|
|
13
13
|
const env = options.env;
|
|
14
|
-
const
|
|
14
|
+
const distTagVersionCheck = async (channel) => {
|
|
15
15
|
let result;
|
|
16
16
|
try {
|
|
17
|
-
result = await runner(npmCommand, ["view", PACKAGE_NAME,
|
|
17
|
+
result = await runner(npmCommand, ["view", PACKAGE_NAME, `dist-tags.${channel}`, "--json"], { timeoutMs, cwd, env });
|
|
18
18
|
}
|
|
19
19
|
catch {
|
|
20
20
|
return { ok: false, reason: "spawn-failure" };
|
|
@@ -27,17 +27,19 @@ export function createNpmUpdateClient(options) {
|
|
|
27
27
|
if (result.exitCode !== 0) {
|
|
28
28
|
return { ok: false, reason: "command-failed" };
|
|
29
29
|
}
|
|
30
|
-
const version =
|
|
30
|
+
const version = parseNpmViewDistTag(result.stdout, channel);
|
|
31
31
|
return version
|
|
32
32
|
? { ok: true, version }
|
|
33
33
|
: { ok: false, reason: "invalid-response" };
|
|
34
34
|
};
|
|
35
|
+
const latestVersionCheck = () => distTagVersionCheck("latest");
|
|
35
36
|
return {
|
|
36
37
|
async latestVersion() {
|
|
37
38
|
const result = await latestVersionCheck();
|
|
38
39
|
return result.ok ? result.version : undefined;
|
|
39
40
|
},
|
|
40
41
|
latestVersionCheck,
|
|
42
|
+
distTagVersionCheck,
|
|
41
43
|
async canAutoInstall() {
|
|
42
44
|
return proveGlobalNpmInstall({
|
|
43
45
|
currentVersion: options.currentVersion,
|
|
@@ -49,10 +51,11 @@ export function createNpmUpdateClient(options) {
|
|
|
49
51
|
timeoutMs,
|
|
50
52
|
});
|
|
51
53
|
},
|
|
52
|
-
async installExact(version) {
|
|
53
|
-
if (!
|
|
54
|
+
async installExact(version, channel = "latest") {
|
|
55
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
54
56
|
return { ok: false, reason: "invalid-version" };
|
|
55
|
-
|
|
57
|
+
}
|
|
58
|
+
const result = await runner(npmCommand, buildExactInstallArgs(version, channel), {
|
|
56
59
|
timeoutMs: Math.max(timeoutMs, 120000),
|
|
57
60
|
cwd,
|
|
58
61
|
env,
|
|
@@ -79,6 +82,13 @@ export function createNpmUpdateClient(options) {
|
|
|
79
82
|
};
|
|
80
83
|
}
|
|
81
84
|
export function parseNpmViewLatest(stdout) {
|
|
85
|
+
return parseNpmViewDistTag(stdout, "latest");
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* `latest` stays stable-only. `next` is explicitly selected by a human and
|
|
89
|
+
* may legitimately be a SemVer prerelease, but remains an exact version.
|
|
90
|
+
*/
|
|
91
|
+
export function parseNpmViewDistTag(stdout, channel) {
|
|
82
92
|
const trimmed = stdout.trim();
|
|
83
93
|
let value = trimmed;
|
|
84
94
|
try {
|
|
@@ -89,11 +99,12 @@ export function parseNpmViewLatest(stdout) {
|
|
|
89
99
|
}
|
|
90
100
|
if (typeof value !== "string")
|
|
91
101
|
return undefined;
|
|
92
|
-
return
|
|
102
|
+
return isAllowedChannelVersion(value, channel) ? value : undefined;
|
|
93
103
|
}
|
|
94
|
-
export function buildExactInstallArgs(version) {
|
|
95
|
-
if (!
|
|
96
|
-
throw new Error(`invalid
|
|
104
|
+
export function buildExactInstallArgs(version, channel = "latest") {
|
|
105
|
+
if (!isAllowedChannelVersion(version, channel)) {
|
|
106
|
+
throw new Error(`invalid ${channel} version: ${version}`);
|
|
107
|
+
}
|
|
97
108
|
return ["install", "-g", `${PACKAGE_NAME}@${version}`, "--no-fund", "--no-audit"];
|
|
98
109
|
}
|
|
99
110
|
export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
@@ -106,7 +117,13 @@ export function verifyNpmListVersion(stdout, expectedVersion) {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
export function verifyPathVersion(stdout, expectedVersion) {
|
|
109
|
-
|
|
120
|
+
// `loop-agent --version` may append a build provenance marker. The release
|
|
121
|
+
// version itself must still match exactly; accepting no other trailing form
|
|
122
|
+
// keeps PATH verification bound to the requested stable package.
|
|
123
|
+
return new RegExp(`^${escapeRegExp(expectedVersion)}(?: \\(build [^)]+\\))?$`).test(stdout.trim());
|
|
124
|
+
}
|
|
125
|
+
function escapeRegExp(value) {
|
|
126
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
110
127
|
}
|
|
111
128
|
async function proveGlobalNpmInstall(options) {
|
|
112
129
|
const root = await options.runner(options.npmCommand, ["root", "-g"], {
|
|
@@ -142,6 +159,11 @@ function isStrictRelease(version) {
|
|
|
142
159
|
const parsed = semver.parse(version);
|
|
143
160
|
return Boolean(parsed && parsed.prerelease.length === 0);
|
|
144
161
|
}
|
|
162
|
+
function isAllowedChannelVersion(version, channel) {
|
|
163
|
+
if (channel === "latest")
|
|
164
|
+
return isStrictRelease(version);
|
|
165
|
+
return Boolean(semver.valid(version));
|
|
166
|
+
}
|
|
145
167
|
async function runCommand(command, args, options) {
|
|
146
168
|
return new Promise((resolve) => {
|
|
147
169
|
const child = spawn(command, [...args], {
|
|
@@ -147,6 +147,8 @@ export const backendTestConfigSchema = z
|
|
|
147
147
|
gapDoc: z.string().min(1).optional(),
|
|
148
148
|
/** gap-fill pytest execution scope: only affected items (default) or all eligible. */
|
|
149
149
|
executeScope: z.enum(["affected", "all"]).optional(),
|
|
150
|
+
/** Backend-test-only workspace mutation control; omitted means Git guard. */
|
|
151
|
+
workspaceControl: z.enum(["git", "filesystem-only"]).optional(),
|
|
150
152
|
})
|
|
151
153
|
.strict()
|
|
152
154
|
.optional();
|
|
@@ -237,5 +239,13 @@ export const taskConfigSchema = z.preprocess((raw) => {
|
|
|
237
239
|
if ("executor" in obj || "cursorModel" in obj) {
|
|
238
240
|
throw new Error(CURSOR_TASK_FIELD_REMOVED_ERROR);
|
|
239
241
|
}
|
|
242
|
+
const backendTest = obj.backendTest;
|
|
243
|
+
if (backendTest &&
|
|
244
|
+
typeof backendTest === "object" &&
|
|
245
|
+
!Array.isArray(backendTest) &&
|
|
246
|
+
"workspaceControl" in backendTest &&
|
|
247
|
+
obj.taskKind !== "backend-test") {
|
|
248
|
+
throw new Error("backendTest.workspaceControl is only valid when taskKind is backend-test");
|
|
249
|
+
}
|
|
240
250
|
return raw;
|
|
241
251
|
}, taskConfigObjectSchema);
|
|
@@ -232,8 +232,11 @@ export async function applyTaskContract(input) {
|
|
|
232
232
|
// Re-parse through schema to ensure full TaskConfig shape for hash stability
|
|
233
233
|
const fullConfig = parseTaskConfigForRuntime(mergedConfig);
|
|
234
234
|
const taskConfigContent = serializeTaskConfigForDisk(fullConfig);
|
|
235
|
-
const requirementContent = projectRequirementMarkdown(input.draft);
|
|
235
|
+
const requirementContent = projectRequirementMarkdown(input.draft, input.requirementLedger);
|
|
236
236
|
const constraintsContent = projectConstraintsMarkdown(input.draft);
|
|
237
|
+
const requirementLedgerContent = input.requirementLedger !== undefined
|
|
238
|
+
? `${JSON.stringify(input.requirementLedger, null, 2)}\n`
|
|
239
|
+
: undefined;
|
|
237
240
|
// Validate references manifest hash if present (do not modify)
|
|
238
241
|
const paths = getTaskPaths(input.repoRoot, input.taskId);
|
|
239
242
|
const manifestText = await readFile(getSourceManifestPath(paths.sourceDir), "utf-8").catch(() => null);
|
|
@@ -241,6 +244,7 @@ export async function applyTaskContract(input) {
|
|
|
241
244
|
requirementBytes: requirementContent,
|
|
242
245
|
constraintsBytes: constraintsContent,
|
|
243
246
|
referenceManifestBytes: manifestText,
|
|
247
|
+
requirementLedgerBytes: requirementLedgerContent,
|
|
244
248
|
});
|
|
245
249
|
const taskConfigSha256 = computeTaskConfigSha256(fullConfig);
|
|
246
250
|
const canonicalHash = computeCanonicalHash({
|
|
@@ -277,6 +281,9 @@ export async function applyTaskContract(input) {
|
|
|
277
281
|
constraintsContent,
|
|
278
282
|
taskConfigContent,
|
|
279
283
|
contractRef: nextRef,
|
|
284
|
+
...(requirementLedgerContent !== undefined
|
|
285
|
+
? { requirementLedgerContent }
|
|
286
|
+
: {}),
|
|
280
287
|
},
|
|
281
288
|
});
|
|
282
289
|
await appendRequestLedgerEntry({
|
|
@@ -312,6 +319,9 @@ export async function applyTaskContract(input) {
|
|
|
312
319
|
changedFiles: [
|
|
313
320
|
"source/需求.md",
|
|
314
321
|
"source/执行约束.md",
|
|
322
|
+
...(requirementLedgerContent !== undefined
|
|
323
|
+
? ["source/requirement-ledger.json"]
|
|
324
|
+
: []),
|
|
315
325
|
"task.json",
|
|
316
326
|
"contract.json",
|
|
317
327
|
],
|
|
@@ -29,3 +29,5 @@ export const TRANSACTIONS_DIRNAME = TRANSACTIONS_DIR_NAME;
|
|
|
29
29
|
export const REQUEST_LEDGER_BASENAME = REQUEST_LEDGER_FILE_NAME;
|
|
30
30
|
export const REQUIREMENT_FILE_NAME = "需求.md";
|
|
31
31
|
export const CONSTRAINTS_FILE_NAME = "执行约束.md";
|
|
32
|
+
/** Source fidelity ledger (20260824-frontend-source-fidelity-ledger-v1). */
|
|
33
|
+
export const REQUIREMENT_LEDGER_FILE_NAME = "requirement-ledger.json";
|
|
@@ -35,6 +35,9 @@ export function computeSourceHashes(input) {
|
|
|
35
35
|
if (input.referenceManifestBytes) {
|
|
36
36
|
sourceHashes.referenceManifestSha256 = sha256Utf8(input.referenceManifestBytes);
|
|
37
37
|
}
|
|
38
|
+
if (input.requirementLedgerBytes) {
|
|
39
|
+
sourceHashes.requirementLedgerSha256 = sha256Utf8(input.requirementLedgerBytes);
|
|
40
|
+
}
|
|
38
41
|
return sourceHashes;
|
|
39
42
|
}
|
|
40
43
|
export function refMatchesObserved(ref, observedCanonicalHash) {
|