@tea-agent/loop-agent 0.42.0-next.5 → 0.42.0-next.7
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 +22 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +75 -13
- package/dist/task/source-prepare/fragment-inventory.js +49 -16
- package/dist/task/source-prepare/prepare.js +37 -20
- package/dist/worker/console/chat/model-resolver.js +7 -4
- package/dist/worker/console/chat/pi-runtime.js +40 -1
- package/dist/worker/console/chat/routes.js +25 -24
- package/dist/worker/console/chat/sdd-data-alignment.js +6 -3
- package/dist/worker/console/pi-readiness.js +3 -2
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BJpsGgAv.js → abnfDiagram-N423BO3Z-BQBHX31n.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DjMFPsEd.js → arc-MSJ199eR.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DqdZlr1O.js → architectureDiagram-T3A2C74G-DiRq2qMX.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BmZnfFEO.js → blockDiagram-VBNYF7ZC-rlaxFOvi.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-3cKberpL.js → c4Diagram-5PPSVZJV-BSpi9ump.js} +1 -1
- package/dist/worker/console/static/assets/channel-BdTaMnYP.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-q-0yjQ1h.js → chunk-2GRJ4B5K-BN8VNeh5.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DnONBaDW.js → chunk-2Q5K7J3B-CDa1OVRA.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-v3a6PCOS.js → chunk-5RXB4S5H-zxMQ5b8E.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D1kLrlU_.js → chunk-5VM5RSS4-XTqNlzH4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BPelzihi.js → chunk-6Q2QTUOP-DpZQfFTq.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DauNE69H.js → chunk-GF5L2VYU-DPv743zu.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-RIg5_bFJ.js → chunk-JWPE2WC7-CFsCFoF4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BuILuoxs.js → chunk-KBJHAD2P-CV__aJTt.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-VHORV5oW.js → chunk-RYQCIY6F-BshKlNir.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJYr4xHC.js → chunk-XXDRQBXY-B-A3ErBP.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cid7CSRK.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cid7CSRK.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BS6nuqQn.js → cose-bilkent-JH36ORCC-25xj8928.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BBhF0NAT.js → cynefin-VYW2F7L2-DFmv38cl.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D1135Ifg.js → cynefinDiagram-MW4NZA55-hReuWDQP.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-vAOYPUWJ.js → dagre-VZM6K2ZE-C9X3YPUI.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DuaSPkfa.js → diagram-7IWD3JNH-CAVmqjg9.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-mMvXvGU6.js → diagram-B4RE2ZJO-DYNlLsIm.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDIEJtlL.js → diagram-LBJQPF4R-BP5mlbfb.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Bi_6JvCW.js → diagram-Q27KOJAE-KWOHK5ZM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DAqj57yN.js → diagram-UB23O5K3-D25Ro5y-.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DfWaxNEo.js → ebnfDiagram-BXEA7PRR-kLaahtbh.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt7QqkNG.js → erDiagram-JOGREHBK-1tgKuLez.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CRRhQwT2.js → flowDiagram-UKHOOZJN-DARr-Cvn.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DAV1O5B6.js → ganttDiagram-PKOTCBZU-BgwjBv_V.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js → gitGraphDiagram-DS77QQ5N-DneIlYSI.js} +1 -1
- package/dist/worker/console/static/assets/{index-CVdddQsA.js → index-Dkv0ezuE.js} +75 -77
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C5W5eILC.js → infoDiagram-6WML65LV-DA7ATqY6.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js → ishikawaDiagram-WSZJBQD7-DnOS-E1Z.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CKDRcaKf.js → journeyDiagram-NVQOT4AX-C8JnwJ_s.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cc7_TuSb.js → kanban-definition-27J2QSJJ-Cw0_OjEC.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BcQYYmeT.js → linear-mrxq3fyn.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CwLZPtQo.js → mermaid.core-Bagus6zF.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D29M025z.js → mindmap-definition-FAOFIHXS-Bf95bLuW.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CV-ZAWYe.js → pegDiagram-VL7TDLO6-BvtUW6w2.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C6iPS_o5.js → pieDiagram-7S7Q4E2Y-CbWtT-Ry.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BFCD1sTg.js → quadrantDiagram-CIZ2JOQS-ctnxxIdD.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CztNj4O9.js → railroadDiagram-AXF67PYL-BZ7Z3X0-.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BWIELSNd.js → requirementDiagram-LRYGKXZP-BMX1xnIy.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CPtxrjJQ.js → sankeyDiagram-W5VNT64P-MShOQznz.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BEWVSFsu.js → sequenceDiagram-SI44F4Z6-CTGOazaB.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DyhzoX_J.js → sizeCapture-X5ZJPWSS-zGbfM4nV.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-xLox0nu7.js → stateDiagram-OKZ733FA-Brf8Wwzr.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-CIkoculx.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CLC_ow9A.js → swimlanes-SLNWSIFB-CvNw3ZqF.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-pJ-V5EdT.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DmJZ4uyY.js → timeline-definition-Z64GVDOM-z2LyZpDt.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DD7GGi9Z.js → vennDiagram-T6HMQDX7-Bc_coprH.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bi8PQoGt.js → wardleyDiagram-T6FBY63Y-M1NSmWq-.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-uKdAY8Dp.js → xychartDiagram-ELKLHX3M-Ct0rjGXW.js} +1 -1
- package/dist/worker/console/static/index.html +1 -1
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
- package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
- package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
- package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
- package/dist/worker/observe/static/dag-helpers.js +4 -2
- package/dist/worker/observe/static/inspect-workspace.js +23 -0
- package/dist/worker/observe/static/kpi.js +2 -2
- package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
- package/dist/worker/observe/static/operator-chrome.js +37 -23
- package/dist/worker/observe/static/relations.js +2 -2
- package/dist/worker/observe/static/router.d.ts +12 -1
- package/dist/worker/observe/static/router.js +48 -6
- package/dist/worker/observe/static/run-processing.js +4 -2
- package/dist/worker/observe/static/shell-chrome.js +2 -2
- package/dist/worker/observe/static/state.js +5 -0
- package/dist/worker/observe/static/styles.css +95 -0
- package/dist/worker/observe/static/views/dag-inspector.js +14 -7
- package/dist/worker/observe/static/views/dag.js +10 -2
- package/dist/worker/observe/static/views/dags.js +3 -1
- package/dist/worker/observe/static/views/dashboard.js +14 -6
- package/dist/worker/observe/static/views/pool.js +7 -2
- package/dist/worker/observe/static/views/run.js +12 -3
- package/dist/worker/observe/static/views/session-timeline.js +69 -10
- package/dist/worker/observe/static/views/task.js +7 -2
- package/dist/workflows/dag/backend-test-case-coverage-analysis.js +58 -15
- package/dist/workflows/dag/backend-test-plan-protocol.js +106 -0
- package/dist/workflows/dag/backend-test-scenario-param.js +16 -5
- package/dist/workflows/dag/backend-test-writer-completeness.js +42 -6
- package/dist/workflows/dag/frontend-shadow-dual-write.js +33 -6
- package/dist/workflows/dag/init-hybrid.js +193 -32
- package/dist/workflows/dag/node-execution.js +1 -1
- package/dist/workflows/dag/prompt.js +36 -2
- package/dist/workflows/dag/types.js +1 -1
- package/docs/templates/backend-test-dag.json +5 -5
- package/package.json +3 -2
- package/dist/worker/console/static/assets/channel-DWYwciiv.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-Dvpxn-bf.js +0 -8
package/AGENTS.md
CHANGED
|
@@ -81,7 +81,7 @@ DAG 执行期间,主 agent 不得修改 writeSet 外的任何工作区文件
|
|
|
81
81
|
|
|
82
82
|
权威源:`docs/governance/verification-matrix.md`。
|
|
83
83
|
|
|
84
|
-
默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push` 经 `node scripts/pre-push-verify.mjs` 在 receipt 命中时复用、miss 时跑 full `bash scripts/ci.sh`(全量 typecheck + `npm test`)。每个 clone 安装一次 hooks:`bash scripts/install-git-hooks.sh`。
|
|
84
|
+
默认节奏:编辑中只跑矩阵「最低验证」(typecheck + 定向 Vitest);常规 `npm test` 跳过约 6 分钟的 I/O-heavy 池(fast + integration);`git commit` 的 `pre-commit` 只做 `check-repo`;`git push` 的 `pre-push` 经 `node scripts/pre-push-verify.mjs` 在 receipt 命中时复用、miss 时跑 full `bash scripts/ci.sh`(全量 typecheck + 含 I/O-heavy 的 `npm run test:full`)。每个 clone 安装一次 hooks:`bash scripts/install-git-hooks.sh`。
|
|
85
85
|
|
|
86
86
|
连续 source→target 交付遵循**单一最终树全量验证**:同一 Git tree、相同环境与命令合同在 receipt TTL 内只选一个本地 full authority(有效 receipt / target pre-push / 可选 `npm run verify:tree`),不按分支名策略化,也不机械双跑 full CI。细节见 verification-matrix 与 branch-merge-guideline。
|
|
87
87
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- 修复前端 `--from-text` 任务缺少 source-fidelity ledger、DAG 生成后降级为 v1 并在 `frontend-contract-pi` 0ms 失败的问题:文本入口现在持久化可校验的 v2 绑定输入,init-hybrid 复用任务自有需求源;Console 默认模型与 Pi readiness 同时排除无凭据 provider,避免把不可执行模型误报为可用。
|
|
6
|
+
- 常规回归 `npm test` 默认跳过约 6 分钟的 I/O-heavy 测试池(仍跑 fast + integration);完整三池改为 `npm run test:full` / `LOOP_AGENT_FULL_TEST=1`。本地 `ci.sh`、GitHub PR 与 release/next 发布门禁仍跑全量,不降低交付验证。
|
|
7
|
+
- 修复 backend-test correspondence 把前面函数的 `pytest.param(..., id="TP-...")` 叠进后续 Case `parameterIds` 的问题:decorator 区域遇到上一 `def`/`class` 或非 decorator 续行即停,不再把整组打成 `TEST_POINT_BINDING_EXTRA`。eligibility 门禁不放宽。
|
|
8
|
+
- 修复 backend-test scenario facts 把内部 max/min/oversize helper 哨兵 `-1/-2/-3` 写成 `observedLength`、导致 N10 在 schema `min(0)` 上 `invalid-output` 的问题:这些 helper 仍按 Markdown bound MATCH,持久化长度只保留非负值。
|
|
9
|
+
- 修复 backend-test 把 GET `params=`/`query=` 过滤字段、Python `True`/`False` 布尔字面量和混合大小写 `pytest.param` 列表漏出 correspondence/payload/scenario 观察的问题:这些 variant 现在可以按 item 授权,而不再把整 Case 打成 `PAYLOAD_SHAPE_MISMATCH` / `VARIANT_PARAMETER_MISSING`。eligibility 门禁不放宽。
|
|
10
|
+
- 修复 backend-test 计划协议标题被翻译成中文后 N3 直接 `missing-module-index`、以及 Markdown writer 漏写精确 Scenario Partition slot 只能等到 N6 才失败的问题:N3 现在把 `## 模块索引/覆盖范围/覆盖矩阵/场景分区` 确定性改写成英文 canonical heading 并最多写回一次 run-owned `plan.md`,同时展开 slot 清单写入 `contracts/backend-test-scenario-partition-slots.json` 并注入 map child `requiredVariantSlots`。Markdown shard completeness 对缺失精确 `TP-SP-...` variant 报 `incomplete-partition-slot-set` 并允许一次有界补写。覆盖语义按方案 A:Case 数量可以少于枚举数量,但每个 slot 仍须独立 variant Test Point;`SINGLE`/`MULTIPLE` 不计覆盖。N6 fail-closed 不放宽。
|
|
11
|
+
- 修复 backend-test strict Markdown map writer 的 upstream artifact pointer 泄漏:当长上游输出的 `stdout.md` 不在当前节点 `readSet` 时,不再向 Pi 注入不可读取的 artifact 路径,避免模型触发 `read-outside-read-set` 后进入 pending;保留有界 preview,并新增回归覆盖严格 writer 边界。
|
|
5
12
|
- 修复 backend-test Manifest 把模型生成的 Module Stem 展示单元格当成模块身份真源、导致 low-tier 输出(缺 `test_` 前缀或整段 Markdown 链接)在 N3 `module-markdown-path-mismatch` 反复失败的问题:N3 现在先解析唯一合法 Markdown Path 与 `<stem>.md` 相对链接作为 canonical 身份,再校验业务 ownership/split 与路径安全;Stem 单元格只作冗余展示并能确定性重写为 canonical 文件名 stem。strict `backendTest.moduleLayout` 改为先按唯一 required Markdown Path 定位模块,再恢复冻结 Markdown/Pytest path pair,避免错误 stem 提前丢弃整行,同时保留 Plan-only pytest path 修复。Markdown Path 缺失/多候选/越界或与 strict path pair 冲突、以及多行 canonicalize 后同 identity(`duplicate-canonical-module-stem`)继续 fail-closed,不再静默去重。同步加强 N2 prompt(Stem 单元格仅允许纯文件名 stem)。
|
|
6
13
|
- 修复 Next Release 的 io-heavy 测试并发不稳定:发布 workflow 将该池固定为单 worker,避免全部测试已通过后触发 Vitest `onTaskUpdate` RPC settlement timeout,并消除 controller identity/package fingerprint 测试之间的共享状态竞态;不跳过或删除任何测试。
|
|
7
14
|
- 修复 backend-test 唯一 pytest collection repair 已产生受控非空修改、却因模型遗漏 `IMPLEMENTATION_OUTCOME` 而阻断整条执行链的问题:仅 N11 exact repair writer 可把完全缺失的 outcome 确定性恢复为候选 changed,随后仍必须由 N12 重新通过 scenario、correspondence、payload、collection、fixture 和 readiness 门禁;unknown/conflicting/blocked、空 diff、越界写入及 provider/context failure 继续 fail-closed。
|
|
@@ -9,6 +16,7 @@
|
|
|
9
16
|
- 新建对话会优先复用当前工作区中真正的空白会话,避免重复创建;有历史消息、工具调用、待发送内容或正在运行的会话不会被复用。
|
|
10
17
|
- 会话列表刷新现在会复用未变化的行和列表引用,标题、运行状态或队列变化时只更新对应会话,侧栏切换和刷新更平滑。
|
|
11
18
|
- Operator Chat 新增仅对具备 Aimax 凭证的用户可见的 `/sdd-bingo` 数据对齐命令;`wizard-local` 模型请求会自动携带与上报一致的 OpenCode 风格会话身份、当前模型和最新 API key,请求头与资格文件变更均在下一次操作自动生效。
|
|
19
|
+
- DAG 节点检查器的「执行记录」默认自动跟随最新内容;右下角新增纯图标悬浮开关,可显式暂停并保留阅读位置,恢复后立即回到底部,悬停时显示当前可执行动作。
|
|
12
20
|
- 修复前端任务产物派生续跑无法识别当前节点链的问题;从实现节点继续时,设计约束、写入准入、验证和收尾步骤会按现行流程安全重放。
|
|
13
21
|
- 统一工作区、Todo 和其他滚动区域的滚动条样式,默认更轻巧,鼠标悬浮时也更容易辨认。
|
|
14
22
|
- HTML 文件预览现在支持页面内的标签切换等交互;较大的自包含 HTML 可以在新窗口中直接完整加载,不再受源码摘要长度影响。
|
|
@@ -235,6 +243,20 @@
|
|
|
235
243
|
- 右侧辅助栏统一承载文件、上下文、调用记录、运行活动和当前 DAG;点击消息流中的运行状态即可在同一侧栏查看运行、门禁与产物,并可自由调整侧栏宽度。
|
|
236
244
|
- 新增任务清单与结构化提问卡片,让长任务的进度和需要用户选择的内容更容易理解。
|
|
237
245
|
- 「对话 / 操作 / 观测」使用统一导航;操作和观测页面可以切换工作区查看对应数据。
|
|
246
|
+
## [0.41.1] - 2026-09-02
|
|
247
|
+
|
|
248
|
+
### 修复
|
|
249
|
+
|
|
250
|
+
- 修复 Next Release 发布 workflow 的 io-heavy 测试并发不稳定:将该测试池固定为单 worker,避免全部测试通过后因过高并发触发 Vitest `onTaskUpdate` RPC settlement timeout,并消除 controller identity / package fingerprint 测试之间的共享状态竞态。
|
|
251
|
+
- 修复 Operator Chat 旧会话继续使用过期模型认证的问题:模型配置变化后,下一轮发送或手动重新加载会自动刷新模型目录和当前模型认证。
|
|
252
|
+
- 修复 Operator Chat 长工具链的消息错序:模型过程文字、思考和工具卡片现在按服务端事件顺序稳定交替展示,刷新历史会话后也保持相同顺序。
|
|
253
|
+
- 统一工作区、Todo 和其他滚动区域的滚动条样式,默认更轻巧,鼠标悬浮时也更容易辨认。
|
|
254
|
+
- HTML 文件预览现在支持页面内的标签切换等交互;较大的自包含 HTML 可以在新窗口中直接完整加载,不再受源码摘要长度限制。
|
|
255
|
+
|
|
256
|
+
### 测试
|
|
257
|
+
|
|
258
|
+
- `harness.json` Pi MED 档测试不再 pin 具体模型 id,改为校验 tier 形状,避免每次 dogfood 档位切换都造成测试失败。
|
|
259
|
+
|
|
238
260
|
## [0.41.0] - 2026-08-31
|
|
239
261
|
|
|
240
262
|
### 新增
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.42.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.42.0-next.7",
|
|
4
|
+
"gitSha": "dc38c285736865703929d788b0f1929513ebe24c",
|
|
5
|
+
"builtAt": "2026-09-02T17:21:58.319Z"
|
|
6
6
|
}
|
|
@@ -2019,6 +2019,41 @@ export async function createFrontendContractTools(input) {
|
|
|
2019
2019
|
},
|
|
2020
2020
|
};
|
|
2021
2021
|
}
|
|
2022
|
+
async function resolveFrontendScoutSourceDeclaredPaths(input) {
|
|
2023
|
+
const binding = input.spec.sourceBinding;
|
|
2024
|
+
if (!binding?.sources?.length)
|
|
2025
|
+
return undefined;
|
|
2026
|
+
const declared = new Set();
|
|
2027
|
+
const pathToken = /(?:\.?\.?\/)?[A-Za-z0-9_.-]+(?:\/[A-Za-z0-9_.-]+)+\/?/g;
|
|
2028
|
+
const writerPaths = input.spec.tasks
|
|
2029
|
+
.filter((task) => task.executor === "pi" && task.toolProfile === "write")
|
|
2030
|
+
.flatMap((task) => task.allowedPaths ?? []);
|
|
2031
|
+
const prefixes = writerPaths
|
|
2032
|
+
.map((pattern) => pattern.replaceAll("\\", "/").replace(/^\.\//, ""))
|
|
2033
|
+
.map((pattern) => pattern.slice(0, pattern.search(/[!*?[{]/) >= 0 ? pattern.search(/[!*?[{]/) : pattern.length))
|
|
2034
|
+
.map((prefix) => prefix.slice(0, prefix.lastIndexOf("/") + 1))
|
|
2035
|
+
.filter(Boolean);
|
|
2036
|
+
for (const source of binding.sources) {
|
|
2037
|
+
try {
|
|
2038
|
+
const text = await readFile(path.resolve(input.cwd, source.path), "utf8");
|
|
2039
|
+
for (const match of text.matchAll(pathToken)) {
|
|
2040
|
+
const token = match[0].replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2041
|
+
if (!token || token.startsWith("http") || token.startsWith("api/"))
|
|
2042
|
+
continue;
|
|
2043
|
+
declared.add(token);
|
|
2044
|
+
for (const prefix of prefixes) {
|
|
2045
|
+
if (!token.startsWith(prefix) && !token.startsWith(".harness/"))
|
|
2046
|
+
declared.add(`${prefix}${token}`);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
catch {
|
|
2051
|
+
// The source binding itself remains the authority; an unreadable source
|
|
2052
|
+
// produces an empty declaration set, so future test paths fail closed.
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
return [...declared].sort();
|
|
2056
|
+
}
|
|
2022
2057
|
/**
|
|
2023
2058
|
* A+B: `frontend-scout-pi` incremental evidence tools (origin=scout). Runtime
|
|
2024
2059
|
* enriches committed target-surface / design-evidence facts with hash/section/
|
|
@@ -2043,6 +2078,12 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2043
2078
|
content: [{ type: "text", text: JSON.stringify(details) }],
|
|
2044
2079
|
details,
|
|
2045
2080
|
});
|
|
2081
|
+
const sourceDeclaredPaths = (input.sourceDeclaredPaths ?? []).map((value) => value.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, ""));
|
|
2082
|
+
const hasSourceDeclarations = input.sourceDeclaredPaths !== undefined;
|
|
2083
|
+
const isSourceDeclared = (candidate) => {
|
|
2084
|
+
const normalized = candidate.replaceAll("\\", "/").replace(/^\.\//, "").replace(/\/$/, "");
|
|
2085
|
+
return sourceDeclaredPaths.some((declared) => declared === normalized || declared.startsWith(`${normalized}/`));
|
|
2086
|
+
};
|
|
2046
2087
|
// A+B (AC-003): runtime enriches declared scout paths with hash/freshness
|
|
2047
2088
|
// from the real filesystem. The model's self-reported path list is never
|
|
2048
2089
|
// trusted for content identity; a missing file fails closed to fresh=false
|
|
@@ -2056,7 +2097,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2056
2097
|
const absolute = path.resolve(workspaceRoot, relative);
|
|
2057
2098
|
if (absolute !== workspaceRoot &&
|
|
2058
2099
|
!absolute.startsWith(`${workspaceRoot}${path.sep}`)) {
|
|
2059
|
-
evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false });
|
|
2100
|
+
evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false, sourceDeclared: isSourceDeclared(relative) });
|
|
2060
2101
|
continue;
|
|
2061
2102
|
}
|
|
2062
2103
|
try {
|
|
@@ -2073,6 +2114,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2073
2114
|
path: relative,
|
|
2074
2115
|
sha256: createHash("sha256").update(`directory:${relative}`).digest("hex"),
|
|
2075
2116
|
fresh: true,
|
|
2117
|
+
sourceDeclared: isSourceDeclared(relative),
|
|
2076
2118
|
});
|
|
2077
2119
|
continue;
|
|
2078
2120
|
}
|
|
@@ -2081,10 +2123,11 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2081
2123
|
path: relative,
|
|
2082
2124
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
2083
2125
|
fresh: true,
|
|
2126
|
+
sourceDeclared: isSourceDeclared(relative),
|
|
2084
2127
|
});
|
|
2085
2128
|
}
|
|
2086
2129
|
catch {
|
|
2087
|
-
evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false });
|
|
2130
|
+
evidence.push({ path: relative, sha256: "0".repeat(64), fresh: false, sourceDeclared: isSourceDeclared(relative) });
|
|
2088
2131
|
}
|
|
2089
2132
|
}
|
|
2090
2133
|
return evidence;
|
|
@@ -2157,6 +2200,7 @@ export async function createFrontendScoutEvidenceTools(input) {
|
|
|
2157
2200
|
dataSource: params?.dataSource ?? "",
|
|
2158
2201
|
allowedPathConflicts: params?.allowedPathConflicts ?? [],
|
|
2159
2202
|
unresolvedPaths: params?.unresolvedPaths ?? [],
|
|
2203
|
+
...(hasSourceDeclarations ? { sourceDeclaredPaths } : {}),
|
|
2160
2204
|
...(pathEvidence.length > 0 ? { pathEvidence } : {}),
|
|
2161
2205
|
});
|
|
2162
2206
|
return receipt(result);
|
|
@@ -3798,6 +3842,10 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
3798
3842
|
runDir: meta.runDir,
|
|
3799
3843
|
nodeId: input.task.id,
|
|
3800
3844
|
workspaceRoot: input.cwd,
|
|
3845
|
+
sourceDeclaredPaths: await resolveFrontendScoutSourceDeclaredPaths({
|
|
3846
|
+
cwd: input.cwd,
|
|
3847
|
+
spec: meta.spec,
|
|
3848
|
+
}),
|
|
3801
3849
|
});
|
|
3802
3850
|
writerToolPolicy = {
|
|
3803
3851
|
requireSdk: true,
|
|
@@ -4091,24 +4139,37 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4091
4139
|
})),
|
|
4092
4140
|
];
|
|
4093
4141
|
if (readBudgetIssues.length > 0) {
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
//
|
|
4097
|
-
//
|
|
4098
|
-
|
|
4142
|
+
const hasTypedFrontendTerminal = isFrontendReviewTypedTerminalNode(input.task) ||
|
|
4143
|
+
isFrontendDesignTypedTerminalNode(input.task);
|
|
4144
|
+
// A stale/generated DAG may still carry the legacy read budget. Once a
|
|
4145
|
+
// design/review node has a successful typed terminal, telemetry is
|
|
4146
|
+
// diagnostic only; the terminal shadow below still fails closed when the
|
|
4147
|
+
// fact is missing or conflicting.
|
|
4148
|
+
if (mapped.ok && hasTypedFrontendTerminal) {
|
|
4149
|
+
// Continue to the typed terminal shadow validation below.
|
|
4150
|
+
}
|
|
4151
|
+
else {
|
|
4152
|
+
// Read-budget telemetry is diagnostic only when the provider/executor
|
|
4153
|
+
// has already failed. In particular, keep context-overflow so
|
|
4154
|
+
// node-execution selects its compact retry envelope instead of treating
|
|
4155
|
+
// the attempt as a generic read-burst retry.
|
|
4156
|
+
if (!mapped.ok) {
|
|
4157
|
+
return {
|
|
4158
|
+
...mapped,
|
|
4159
|
+
stderr: [mapped.stderr, ...readBudgetIssues]
|
|
4160
|
+
.filter(Boolean)
|
|
4161
|
+
.join("\n\n"),
|
|
4162
|
+
};
|
|
4163
|
+
}
|
|
4099
4164
|
return {
|
|
4100
4165
|
...mapped,
|
|
4166
|
+
ok: false,
|
|
4167
|
+
failureCategory: "read-burst",
|
|
4101
4168
|
stderr: [mapped.stderr, ...readBudgetIssues]
|
|
4102
4169
|
.filter(Boolean)
|
|
4103
4170
|
.join("\n\n"),
|
|
4104
4171
|
};
|
|
4105
4172
|
}
|
|
4106
|
-
return {
|
|
4107
|
-
...mapped,
|
|
4108
|
-
ok: false,
|
|
4109
|
-
failureCategory: "read-burst",
|
|
4110
|
-
stderr: [mapped.stderr, ...readBudgetIssues].filter(Boolean).join("\n\n"),
|
|
4111
|
-
};
|
|
4112
4173
|
}
|
|
4113
4174
|
if (isFrontendReviewTypedTerminalNode(input.task)) {
|
|
4114
4175
|
return await runFrontendReviewTerminalShadow({
|
|
@@ -4500,6 +4561,7 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
|
|
|
4500
4561
|
? "pytest-generate"
|
|
4501
4562
|
: "md-generate",
|
|
4502
4563
|
writeSet: input.task.writeSet ?? [],
|
|
4564
|
+
runDir: meta.runDir,
|
|
4503
4565
|
})
|
|
4504
4566
|
: isBackendTestMdPlanTask(input.task)
|
|
4505
4567
|
? await assessBackendTestMdPlanCompleteness(input.cwd, backendLayout)
|
|
@@ -16,17 +16,15 @@ const NON_GOAL_HEADING_RE = /(?:^|\s)(?:非目标|out of scope|non-goals|non goa
|
|
|
16
16
|
const OBJECTIVE_HEADING_RE = /(?:^|\s)(?:目标|objective|问题与目标|业务目标|需求概述)(?:\s|$)/iu;
|
|
17
17
|
/** 可执行片段启发式(AC 风格/祈使语气):命中则不可按 context-only 降级。 */
|
|
18
18
|
const EXECUTABLE_HINT_RE = /(?:必须|应当|应支持|需要|需|不得|should|must|shall|will|require|support|implement|provide|ensure|verify|当.*?时|when.*?then)/iu;
|
|
19
|
-
/** 显式
|
|
20
|
-
* 与 checklist
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
* 捕获组 1 = AC id,捕获组 2 = 标题余下文本。 */
|
|
29
|
-
const EXPLICIT_AC_HEADING_RE = /^\s*(AC(?:[-_](?:FE|BE|UI|API))?[-_][A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/iu;
|
|
19
|
+
/** 显式 requirement ID:AC-001 / BR-001 / REQ-SRC-001 等(允许 bullet 标记后空格
|
|
20
|
+
* 与 checklist 勾选框)。AC/BR/REQ 都是 DAG 的合法 canonical requirement 身份;
|
|
21
|
+
* 之前这里只识别 AC,导致已进入 canonicalRequirements 的 BR 片段无法绑定来源。
|
|
22
|
+
* 捕获组 1 = requirement id,捕获组 2 = 余下文本。 */
|
|
23
|
+
const EXPLICIT_REQUIREMENT_ID_RE = /^\s*(?:(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?)?((?:REQ|BR|AC)(?:[-_][A-Z0-9]+)+)\s*[::]?\s*(.*)$/iu;
|
|
24
|
+
/** 显式 requirement 标题式:`#### AC-FE-001 Save nickname` / `### BR-001:正文`。
|
|
25
|
+
* 普通单词(Acceptance Criteria / Acceptance)永不匹配,按章节分类生成稳定
|
|
26
|
+
* REQ-SRC-* 片段。捕获组 1 = requirement id,捕获组 2 = 标题余下文本。 */
|
|
27
|
+
const EXPLICIT_REQUIREMENT_HEADING_RE = /^\s*((?:REQ|BR|AC)(?:[-_][A-Z0-9]+)+)\s*[::]?\s*(.*)$/iu;
|
|
30
28
|
/** Markdown 表格数据行(非分隔行)。 */
|
|
31
29
|
const TABLE_DATA_ROW_RE = /^\s*\|.*\|\s*$/;
|
|
32
30
|
const TABLE_SEPARATOR_ROW_RE = /^\s*\|[\s:|-]+\|\s*$/;
|
|
@@ -35,11 +33,11 @@ function isTableHeaderRow(line, nextLine) {
|
|
|
35
33
|
return isTableDataRow(line) && TABLE_SEPARATOR_ROW_RE.test(nextLine.trim());
|
|
36
34
|
}
|
|
37
35
|
/** 表格行首格显式 AC ID:`| AC-001 | 订单列表加载完成 |` → id=AC-001, rest=订单列表加载完成。 */
|
|
38
|
-
function
|
|
36
|
+
function matchTableRowRequirementId(line) {
|
|
39
37
|
const trimmed = line.trim().replace(/^\|/, "");
|
|
40
38
|
const cells = trimmed.split("|");
|
|
41
39
|
const first = cells[0]?.trim() ?? "";
|
|
42
|
-
const idMatch =
|
|
40
|
+
const idMatch = EXPLICIT_REQUIREMENT_HEADING_RE.exec(first);
|
|
43
41
|
if (!idMatch)
|
|
44
42
|
return null;
|
|
45
43
|
const id = (idMatch[1] ?? "").trim().toUpperCase().replace(/_/g, "-");
|
|
@@ -52,6 +50,14 @@ function matchTableRowAcId(line) {
|
|
|
52
50
|
.trim();
|
|
53
51
|
return { id, rest: rest || id };
|
|
54
52
|
}
|
|
53
|
+
/** The splitter may use only AC ids as fragment identities; BR/REQ ids are
|
|
54
|
+
* mapped later while keeping the generated REQ-SRC fragment identity. */
|
|
55
|
+
function matchTableRowAcId(line) {
|
|
56
|
+
const match = matchTableRowRequirementId(line);
|
|
57
|
+
if (!match || !/^AC(?:-[A-Z0-9]+)+$/iu.test(match.id))
|
|
58
|
+
return null;
|
|
59
|
+
return match;
|
|
60
|
+
}
|
|
55
61
|
export function sha256Text(text) {
|
|
56
62
|
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
57
63
|
}
|
|
@@ -66,7 +72,20 @@ export function normalizeFragmentText(text) {
|
|
|
66
72
|
.trim();
|
|
67
73
|
}
|
|
68
74
|
export function matchExplicitAcId(line) {
|
|
69
|
-
const match =
|
|
75
|
+
const match = EXPLICIT_REQUIREMENT_ID_RE.exec(line.trim());
|
|
76
|
+
if (!match)
|
|
77
|
+
return null;
|
|
78
|
+
const rawId = match[1] ?? "";
|
|
79
|
+
if (!/^AC(?:[-_][A-Z0-9]+)+$/iu.test(rawId))
|
|
80
|
+
return null;
|
|
81
|
+
const id = rawId.trim().toUpperCase().replace(/_/g, "-");
|
|
82
|
+
if (!id)
|
|
83
|
+
return null;
|
|
84
|
+
return { id, rest: (match[2] ?? "").trim() };
|
|
85
|
+
}
|
|
86
|
+
/** Match an explicit AC/BR/REQ id in a list item or a paragraph fragment. */
|
|
87
|
+
export function matchExplicitRequirementId(line) {
|
|
88
|
+
const match = EXPLICIT_REQUIREMENT_ID_RE.exec(line.trim());
|
|
70
89
|
if (!match)
|
|
71
90
|
return null;
|
|
72
91
|
const rawId = match[1] ?? "";
|
|
@@ -77,7 +96,20 @@ export function matchExplicitAcId(line) {
|
|
|
77
96
|
}
|
|
78
97
|
/** V4 显式 AC 标题式匹配:`#### AC-FE-001 Save nickname` → id=AC-FE-001, rest=Save nickname。 */
|
|
79
98
|
export function matchExplicitAcHeading(headingText) {
|
|
80
|
-
const match =
|
|
99
|
+
const match = EXPLICIT_REQUIREMENT_HEADING_RE.exec(headingText.trim());
|
|
100
|
+
if (!match)
|
|
101
|
+
return null;
|
|
102
|
+
const rawId = match[1] ?? "";
|
|
103
|
+
if (!/^AC(?:[-_][A-Z0-9]+)+$/iu.test(rawId))
|
|
104
|
+
return null;
|
|
105
|
+
const id = rawId.trim().toUpperCase().replace(/_/g, "-");
|
|
106
|
+
if (!id)
|
|
107
|
+
return null;
|
|
108
|
+
return { id, rest: (match[2] ?? "").trim() };
|
|
109
|
+
}
|
|
110
|
+
/** Match an explicit AC/BR/REQ id in a heading. */
|
|
111
|
+
export function matchExplicitRequirementHeading(headingText) {
|
|
112
|
+
const match = EXPLICIT_REQUIREMENT_HEADING_RE.exec(headingText.trim());
|
|
81
113
|
if (!match)
|
|
82
114
|
return null;
|
|
83
115
|
const rawId = match[1] ?? "";
|
|
@@ -389,7 +421,8 @@ export function buildFragmentInventory(input) {
|
|
|
389
421
|
: null;
|
|
390
422
|
const explicit = explicitIdFromId
|
|
391
423
|
? { id: explicitIdFromId, rest: fragment.text }
|
|
392
|
-
:
|
|
424
|
+
: matchExplicitRequirementId(fragment.text) ??
|
|
425
|
+
matchTableRowRequirementId(fragment.text);
|
|
393
426
|
// V4 标题/条目下正文继承的显式 AC ID(不生成取代它的 REQ-SRC-*)。
|
|
394
427
|
const inherited = !explicit && fragment.explicitAcId
|
|
395
428
|
? { id: fragment.explicitAcId, rest: fragment.text }
|
|
@@ -796,29 +796,46 @@ export async function prepareTaskSource(input) {
|
|
|
796
796
|
// -------------------------------------------------------------------
|
|
797
797
|
let ledger;
|
|
798
798
|
let ledgerSummary;
|
|
799
|
-
|
|
800
|
-
sourceIntegrity
|
|
801
|
-
|
|
799
|
+
const hasAuthoritativeImportedDocs = Boolean(hasImportedPrd &&
|
|
800
|
+
sourceIntegrity?.parseableDocuments.some((doc) => doc.role === "requirement" || doc.role === "acceptance"));
|
|
801
|
+
// Frontend tasks created from text still need the same source-fidelity
|
|
802
|
+
// contract as imported-PRD tasks. In that case the projected requirement
|
|
803
|
+
// document is the canonical, task-owned source that the DAG can bind to;
|
|
804
|
+
// without it init-hybrid would silently emit a v1 binding and the first
|
|
805
|
+
// frontend-contract node would fail before making a model call.
|
|
806
|
+
if (built.draft.taskKind === "frontend-implementation" &&
|
|
807
|
+
(hasAuthoritativeImportedDocs ||
|
|
808
|
+
(input.intent.kind === "facts" && Boolean(input.intent.text)))) {
|
|
802
809
|
try {
|
|
803
|
-
const authoritativeDocs = sourceIntegrity
|
|
810
|
+
const authoritativeDocs = sourceIntegrity?.parseableDocuments.filter((doc) => (doc.role === "requirement" || doc.role === "acceptance") &&
|
|
804
811
|
typeof doc.content === "string" &&
|
|
805
|
-
doc.content.trim().length > 0);
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
path
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
812
|
+
doc.content.trim().length > 0) ?? [];
|
|
813
|
+
const requirementProjection = projectRequirementMarkdown(built.draft);
|
|
814
|
+
const sourceDocuments = authoritativeDocs.length > 0
|
|
815
|
+
? authoritativeDocs.map((doc) => {
|
|
816
|
+
// Canonical source identity 单一边界(AC-001 / AC-FIDELITY-REPAIR-001):
|
|
817
|
+
// ledger sourceDocuments[].path 与 DAG 侧 serializeDagTaskSourcePath /
|
|
818
|
+
// resolveDagTaskSourcePath 一致(repo-root relative POSIX),路径转换只
|
|
819
|
+
// 发生在该边界,不再二次转换进入 digest 计算。
|
|
820
|
+
const absolute = path.resolve(contractPaths.sourceDir, doc.materializedPath);
|
|
821
|
+
return {
|
|
822
|
+
path: path
|
|
823
|
+
.relative(repoRoot, absolute)
|
|
824
|
+
.split(path.sep)
|
|
825
|
+
.join("/"),
|
|
826
|
+
content: doc.content,
|
|
827
|
+
};
|
|
828
|
+
})
|
|
829
|
+
: [
|
|
830
|
+
{
|
|
831
|
+
path: path
|
|
832
|
+
.relative(repoRoot, contractPaths.requirementPath)
|
|
833
|
+
.split(path.sep)
|
|
834
|
+
.join("/"),
|
|
835
|
+
content: requirementProjection,
|
|
836
|
+
},
|
|
837
|
+
];
|
|
820
838
|
if (sourceDocuments.length > 0) {
|
|
821
|
-
const requirementProjection = projectRequirementMarkdown(built.draft);
|
|
822
839
|
ledger = await buildRequirementLedger({
|
|
823
840
|
taskId,
|
|
824
841
|
requirementSha256: sha256Utf8(requirementProjection),
|
|
@@ -125,11 +125,14 @@ function resolveTierDescriptor(selection, available) {
|
|
|
125
125
|
});
|
|
126
126
|
return {
|
|
127
127
|
provider: selection.provider,
|
|
128
|
-
resolved: Boolean(entry),
|
|
128
|
+
resolved: Boolean(entry && entry.hasCredentials !== false),
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
const entry = findProviderForModelId(selection.modelId, available);
|
|
132
|
-
return {
|
|
132
|
+
return {
|
|
133
|
+
provider: entry?.provider,
|
|
134
|
+
resolved: Boolean(entry && entry.hasCredentials !== false),
|
|
135
|
+
};
|
|
133
136
|
}
|
|
134
137
|
/**
|
|
135
138
|
* Resolve harness executors.pi tiers (LOW/MED/HIGH) to provider descriptors.
|
|
@@ -196,7 +199,7 @@ export async function resolveDefaultChatModel(repoRoot, available, options) {
|
|
|
196
199
|
const entry = findProviderForModelId(selection.modelId, available, {
|
|
197
200
|
preferredProvider: selection.provider,
|
|
198
201
|
});
|
|
199
|
-
if (!entry)
|
|
202
|
+
if (!entry || entry.hasCredentials === false)
|
|
200
203
|
return undefined;
|
|
201
204
|
return {
|
|
202
205
|
provider: selection.provider,
|
|
@@ -205,7 +208,7 @@ export async function resolveDefaultChatModel(repoRoot, available, options) {
|
|
|
205
208
|
};
|
|
206
209
|
}
|
|
207
210
|
const entry = findProviderForModelId(selection.modelId, available);
|
|
208
|
-
if (!entry)
|
|
211
|
+
if (!entry || entry.hasCredentials === false)
|
|
209
212
|
return undefined;
|
|
210
213
|
return {
|
|
211
214
|
provider: entry.provider,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* `createDefaultPiSdkBindings()`.
|
|
27
27
|
*/
|
|
28
28
|
import { createHash, randomBytes } from "node:crypto";
|
|
29
|
-
import { access, stat } from "node:fs/promises";
|
|
29
|
+
import { access, mkdir, rm, stat, writeFile } from "node:fs/promises";
|
|
30
30
|
import path from "node:path";
|
|
31
31
|
import { extractAssistantContentBlocks, extractAssistantText, isAssistantSdkMessage, } from "./assistant-content.js";
|
|
32
32
|
import { parseProviderError, } from "./provider-error.js";
|
|
@@ -1331,6 +1331,15 @@ export class ConsolePiRuntime {
|
|
|
1331
1331
|
if (!sessionFile) {
|
|
1332
1332
|
throw new Error(`chat session shell missing sessionFile for ${sessionId}`);
|
|
1333
1333
|
}
|
|
1334
|
+
// Pi's SessionManager.create deliberately postpones writing a JSONL until
|
|
1335
|
+
// an assistant response. Materialize a valid header now, otherwise Console
|
|
1336
|
+
// could persist a path that disappears on a restart before first reply.
|
|
1337
|
+
await this.bindings.materializeSessionShell({
|
|
1338
|
+
sessionManager,
|
|
1339
|
+
sessionId,
|
|
1340
|
+
sessionFile,
|
|
1341
|
+
cwd: this.options.cwd,
|
|
1342
|
+
});
|
|
1334
1343
|
this.sessionManagers.set(sessionId, sessionManager);
|
|
1335
1344
|
this.thinkingSelections.set(sessionId, "auto");
|
|
1336
1345
|
// Detached single-flight materialization; dependent routes await the same promise.
|
|
@@ -3173,6 +3182,36 @@ export function createDefaultPiSdkBindings() {
|
|
|
3173
3182
|
id: opts.sessionId,
|
|
3174
3183
|
});
|
|
3175
3184
|
},
|
|
3185
|
+
materializeSessionShell: async (opts) => {
|
|
3186
|
+
const sdk = (await loadSdk());
|
|
3187
|
+
const manager = opts.sessionManager;
|
|
3188
|
+
if (typeof manager.setSessionFile !== "function") {
|
|
3189
|
+
throw new Error("Pi SessionManager cannot materialize a session file");
|
|
3190
|
+
}
|
|
3191
|
+
let created = false;
|
|
3192
|
+
try {
|
|
3193
|
+
await mkdir(path.dirname(opts.sessionFile), { recursive: true });
|
|
3194
|
+
// Match Pi's documented session header. Rebinding through the public
|
|
3195
|
+
// SessionManager API marks it flushed, so Pi appends the first user
|
|
3196
|
+
// entry instead of trying to create the already-present file later.
|
|
3197
|
+
await writeFile(opts.sessionFile, `${JSON.stringify({
|
|
3198
|
+
type: "session",
|
|
3199
|
+
version: typeof sdk.CURRENT_SESSION_VERSION === "number"
|
|
3200
|
+
? sdk.CURRENT_SESSION_VERSION
|
|
3201
|
+
: 3,
|
|
3202
|
+
id: opts.sessionId,
|
|
3203
|
+
timestamp: new Date().toISOString(),
|
|
3204
|
+
cwd: opts.cwd,
|
|
3205
|
+
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
3206
|
+
created = true;
|
|
3207
|
+
manager.setSessionFile(opts.sessionFile);
|
|
3208
|
+
}
|
|
3209
|
+
catch (error) {
|
|
3210
|
+
if (created)
|
|
3211
|
+
await rm(opts.sessionFile, { force: true }).catch(() => undefined);
|
|
3212
|
+
throw error;
|
|
3213
|
+
}
|
|
3214
|
+
},
|
|
3176
3215
|
createInMemorySessionManager: async (opts) => {
|
|
3177
3216
|
const sdk = (await loadSdk());
|
|
3178
3217
|
return sdk.SessionManager.inMemory(opts.cwd);
|
|
@@ -561,9 +561,15 @@ workspaceId) {
|
|
|
561
561
|
});
|
|
562
562
|
return;
|
|
563
563
|
}
|
|
564
|
+
let createdSessionId;
|
|
565
|
+
let persisted = false;
|
|
564
566
|
try {
|
|
565
567
|
const snapshot = snapshotInput === undefined ? undefined : projectCompactSnapshot(snapshotInput);
|
|
566
|
-
//
|
|
568
|
+
// A Console record promises that its Pi JSONL can be reopened after a
|
|
569
|
+
// Console restart. Do not make that promise until Pi has finished
|
|
570
|
+
// materializing the session: the old fast path wrote a record for a
|
|
571
|
+
// future JSONL path, leaving an unrecoverable orphan when init failed or
|
|
572
|
+
// the process restarted in that window.
|
|
567
573
|
const handle = await deps.runtime.createSession({
|
|
568
574
|
...(provider && modelId ? { model: { provider, modelId } } : {}),
|
|
569
575
|
...(snapshot
|
|
@@ -583,40 +589,26 @@ workspaceId) {
|
|
|
583
589
|
});
|
|
584
590
|
return;
|
|
585
591
|
}
|
|
592
|
+
createdSessionId = handle.sessionId;
|
|
593
|
+
await deps.runtime.ensureSessionReady(handle.sessionId);
|
|
594
|
+
const selection = deps.runtime.getRuntimeSelection(handle.sessionId);
|
|
586
595
|
const session = await deps.store.create({
|
|
587
596
|
sessionId: handle.sessionId,
|
|
588
597
|
sessionFile: handle.sessionFile,
|
|
589
|
-
modelProvider: handle.model?.provider,
|
|
590
|
-
modelId: handle.model?.modelId,
|
|
598
|
+
modelProvider: selection.model?.provider ?? handle.model?.provider,
|
|
599
|
+
modelId: selection.model?.modelId ?? handle.model?.modelId,
|
|
591
600
|
// AC-R7-002: explicit parameter from the dispatcher — the runtime
|
|
592
601
|
// store.create wrapper is gone; body workspaceId (if any) only acts as
|
|
593
602
|
// a fallback for direct (non-dispatcher) handler tests.
|
|
594
603
|
workspaceId: workspaceId ?? (typeof body.workspaceId === "string" && body.workspaceId ? body.workspaceId : undefined),
|
|
595
604
|
});
|
|
605
|
+
persisted = true;
|
|
596
606
|
await deps.attachSession?.(handle.sessionId).catch(() => undefined);
|
|
597
607
|
if (snapshot)
|
|
598
608
|
deps.events.append(handle.sessionId, `${handle.sessionId}:snapshot`, {
|
|
599
609
|
kind: "compact",
|
|
600
610
|
data: snapshot,
|
|
601
611
|
});
|
|
602
|
-
// Best-effort: after materialization, patch resolved default model onto the
|
|
603
|
-
// durable record. Failures stay on the init fence (PI_SESSION_INIT_FAILED).
|
|
604
|
-
if (typeof deps.runtime.ensureSessionReady === "function") {
|
|
605
|
-
void deps.runtime
|
|
606
|
-
.ensureSessionReady(handle.sessionId)
|
|
607
|
-
.then(async () => {
|
|
608
|
-
if (typeof deps.runtime.getRuntimeSelection !== "function")
|
|
609
|
-
return;
|
|
610
|
-
const selection = deps.runtime.getRuntimeSelection(handle.sessionId);
|
|
611
|
-
if (!selection?.model)
|
|
612
|
-
return;
|
|
613
|
-
await deps.store.setRuntimeSelection(handle.sessionId, {
|
|
614
|
-
modelProvider: selection.model.provider,
|
|
615
|
-
modelId: selection.model.modelId,
|
|
616
|
-
});
|
|
617
|
-
})
|
|
618
|
-
.catch(() => undefined);
|
|
619
|
-
}
|
|
620
612
|
sendJson(res, 201, {
|
|
621
613
|
ok: true,
|
|
622
614
|
sessionId: handle.sessionId,
|
|
@@ -625,14 +617,23 @@ workspaceId) {
|
|
|
625
617
|
session: { ...session, isBlank: isBlankChatSession(session) },
|
|
626
618
|
activeTools: handle.activeTools,
|
|
627
619
|
hasBash: true,
|
|
628
|
-
...(handle.initializing ? { initializing: true } : {}),
|
|
629
620
|
});
|
|
630
621
|
}
|
|
631
622
|
catch (error) {
|
|
623
|
+
// Before store.create succeeds, this handle has no durable owner. Dispose
|
|
624
|
+
// it so a failed create cannot survive in-memory until the next restart.
|
|
625
|
+
if (createdSessionId && !persisted)
|
|
626
|
+
deps.runtime.dispose(createdSessionId);
|
|
632
627
|
const message = error instanceof Error ? error.message : String(error);
|
|
633
|
-
|
|
628
|
+
const code = error && typeof error === "object" && "code" in error
|
|
629
|
+
? String(error.code ?? "")
|
|
630
|
+
: "";
|
|
631
|
+
sendJson(res, code === "PI_SESSION_INIT_FAILED" ? 503 : 500, {
|
|
634
632
|
ok: false,
|
|
635
|
-
error: {
|
|
633
|
+
error: {
|
|
634
|
+
code: code === "PI_SESSION_INIT_FAILED" ? code : "CHAT_SESSION_CREATE_FAILED",
|
|
635
|
+
message,
|
|
636
|
+
},
|
|
636
637
|
});
|
|
637
638
|
}
|
|
638
639
|
}
|
|
@@ -203,10 +203,13 @@ export async function injectWizardLocalHeaders(options) {
|
|
|
203
203
|
return;
|
|
204
204
|
if (!(await options.service.isEligible()))
|
|
205
205
|
return;
|
|
206
|
-
const
|
|
207
|
-
if (!
|
|
206
|
+
const modelRegistry = options.context.modelRegistry;
|
|
207
|
+
if (!modelRegistry?.getApiKeyAndHeaders)
|
|
208
208
|
return;
|
|
209
|
-
|
|
209
|
+
// Keep the method call attached to ModelRegistry. Its implementation reads
|
|
210
|
+
// the private runtime through `this`; extracting the function would make the
|
|
211
|
+
// SDK path fail closed and silently drop all custom headers.
|
|
212
|
+
const auth = await modelRegistry.getApiKeyAndHeaders(model);
|
|
210
213
|
if (!auth.ok || typeof auth.apiKey !== "string" || !auth.apiKey)
|
|
211
214
|
return;
|
|
212
215
|
setStaticHeader(options.headers, auth.headers, "client-code", "OpenCode");
|
|
@@ -170,11 +170,12 @@ export async function discoverPiReadinessProbe(options) {
|
|
|
170
170
|
const availableCount = countAvailableModels(snapshot);
|
|
171
171
|
const providerConfigured = hasProviderConfig(snapshot);
|
|
172
172
|
const availableModels = mapSnapshotAvailableModels(snapshot);
|
|
173
|
+
const credentialedAvailableCount = availableModels.filter((model) => model.hasCredentials !== false).length;
|
|
173
174
|
const harnessMatrix = await resolveHarnessPiExecutorMatrix(repoRoot, availableModels);
|
|
174
175
|
const resolvedHarnessModels = [];
|
|
175
176
|
if (typeof runtime.getModel === "function") {
|
|
176
177
|
for (const row of harnessMatrix) {
|
|
177
|
-
if (!row.provider)
|
|
178
|
+
if (!row.provider || !row.resolved)
|
|
178
179
|
continue;
|
|
179
180
|
try {
|
|
180
181
|
if (runtime.getModel(row.provider, row.modelId)) {
|
|
@@ -195,7 +196,7 @@ export async function discoverPiReadinessProbe(options) {
|
|
|
195
196
|
.join(",")
|
|
196
197
|
: "none";
|
|
197
198
|
const authReady = availableCount > 0 || providerConfigured;
|
|
198
|
-
const modelsAvailable =
|
|
199
|
+
const modelsAvailable = credentialedAvailableCount > 0 || resolvedHarnessModels.length > 0;
|
|
199
200
|
let interviewSessionOk = false;
|
|
200
201
|
let sessionMessage;
|
|
201
202
|
if (!authReady || !modelsAvailable) {
|