@tea-agent/loop-agent 0.42.0-next.6 → 0.42.0-next.8
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 +20 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +74 -13
- package/dist/executors/shell-executor.js +3 -1
- 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 +60 -5
- package/dist/worker/console/chat/routes.js +69 -46
- package/dist/worker/console/chat/sdd-data-alignment.js +6 -3
- package/dist/worker/console/chat/session-store.js +16 -0
- package/dist/worker/console/pi-readiness.js +3 -2
- package/dist/worker/console/server.js +19 -1
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BJpsGgAv.js → abnfDiagram-N423BO3Z-C6n9_m0P.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DjMFPsEd.js → arc-DQh-IfZ1.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DqdZlr1O.js → architectureDiagram-T3A2C74G-54NnrwUC.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BmZnfFEO.js → blockDiagram-VBNYF7ZC-pivRALGK.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-3cKberpL.js → c4Diagram-5PPSVZJV-BR7OV2NJ.js} +1 -1
- package/dist/worker/console/static/assets/channel-DsZxrgqe.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-q-0yjQ1h.js → chunk-2GRJ4B5K-B1Aq6BcK.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DnONBaDW.js → chunk-2Q5K7J3B-CbdU0rjo.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-v3a6PCOS.js → chunk-5RXB4S5H-Dqi2GJeD.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D1kLrlU_.js → chunk-5VM5RSS4-BYn1Hu4R.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BPelzihi.js → chunk-6Q2QTUOP-DRLjDL0k.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DauNE69H.js → chunk-GF5L2VYU-CY9Xx2jV.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-RIg5_bFJ.js → chunk-JWPE2WC7-BNCZs7_z.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BuILuoxs.js → chunk-KBJHAD2P-DZ8AStLL.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-VHORV5oW.js → chunk-RYQCIY6F-DsxjYrzz.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJYr4xHC.js → chunk-XXDRQBXY-Ddx1KC1l.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BS6nuqQn.js → cose-bilkent-JH36ORCC-W9TveCnK.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BBhF0NAT.js → cynefin-VYW2F7L2-l9j_ztZH.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D1135Ifg.js → cynefinDiagram-MW4NZA55-BMJMKi4G.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-vAOYPUWJ.js → dagre-VZM6K2ZE-Bb4mG9pH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DuaSPkfa.js → diagram-7IWD3JNH-BMgL_Qv0.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-mMvXvGU6.js → diagram-B4RE2ZJO-Bvo2T4OQ.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDIEJtlL.js → diagram-LBJQPF4R-_5kWRN9c.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Bi_6JvCW.js → diagram-Q27KOJAE-DqdMrltM.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-DAqj57yN.js → diagram-UB23O5K3-CDDYsNkp.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DfWaxNEo.js → ebnfDiagram-BXEA7PRR-c4nfnZUV.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt7QqkNG.js → erDiagram-JOGREHBK-DnkUcNMs.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CRRhQwT2.js → flowDiagram-UKHOOZJN-Dz0KGLZE.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DAV1O5B6.js → ganttDiagram-PKOTCBZU-Cj1t1uka.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js → gitGraphDiagram-DS77QQ5N-tp2FrBHd.js} +1 -1
- package/dist/worker/console/static/assets/index-D9Sc0f0y.js +449 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C5W5eILC.js → infoDiagram-6WML65LV-DQwJS8DH.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js → ishikawaDiagram-WSZJBQD7-eimCQWD4.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CKDRcaKf.js → journeyDiagram-NVQOT4AX-Dd9Gbwgv.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cc7_TuSb.js → kanban-definition-27J2QSJJ-Qve3cyft.js} +1 -1
- package/dist/worker/console/static/assets/{linear-BcQYYmeT.js → linear-UXKSe36Z.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-CwLZPtQo.js → mermaid.core-CWhj4JXN.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D29M025z.js → mindmap-definition-FAOFIHXS-yTcwf4SJ.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CV-ZAWYe.js → pegDiagram-VL7TDLO6-_7qToaZP.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C6iPS_o5.js → pieDiagram-7S7Q4E2Y-CXmrKmDm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BFCD1sTg.js → quadrantDiagram-CIZ2JOQS-hMmrJyaS.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CztNj4O9.js → railroadDiagram-AXF67PYL-BXw8tCBe.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BWIELSNd.js → requirementDiagram-LRYGKXZP-BmjgnLZl.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CPtxrjJQ.js → sankeyDiagram-W5VNT64P-Bj77SPpN.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BEWVSFsu.js → sequenceDiagram-SI44F4Z6-B2FDVysL.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DyhzoX_J.js → sizeCapture-X5ZJPWSS-UChNY9ZZ.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-xLox0nu7.js → stateDiagram-OKZ733FA-wbAEkbd7.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CLC_ow9A.js → swimlanes-SLNWSIFB-DCEkU8HR.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DmJZ4uyY.js → timeline-definition-Z64GVDOM-BGp7H06U.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DD7GGi9Z.js → vennDiagram-T6HMQDX7-DsCIQuWR.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bi8PQoGt.js → wardleyDiagram-T6FBY63Y-cRZLP4Xe.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-uKdAY8Dp.js → xychartDiagram-ELKLHX3M-CP0xLR9Y.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/operator-chat/runtime-snapshot-store.js +10 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +31 -2
- package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -3
- package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
- package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
- 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 +40 -9
- package/dist/workflows/dag/backend-test-scenario-param.js +126 -19
- package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
- package/dist/workflows/dag/backend-test-writer-completeness.js +7 -3
- package/dist/workflows/dag/frontend-shadow-dual-write.js +33 -6
- package/dist/workflows/dag/init-hybrid.js +175 -27
- package/dist/workflows/dag/node-execution.js +1 -1
- package/dist/workflows/dag/types.js +1 -1
- 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/index-CVdddQsA.js +0 -451
- 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,25 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- 修复 Operator Chat 打开已丢失会话时把孤儿 JSONL 升级成 503、以及删会话后残留 workspace 选择被误报为跨工作区冲突的问题:缺失会话文件现在自愈为 404 并清掉记录,前端恢复路径安静忘记该会话。
|
|
6
|
+
- 修复前端 `--from-text` 任务缺少 source-fidelity ledger、DAG 生成后降级为 v1 并在 `frontend-contract-pi` 0ms 失败的问题:文本入口现在持久化可校验的 v2 绑定输入,init-hybrid 复用任务自有需求源;Console 默认模型与 Pi readiness 同时排除无凭据 provider,避免把不可执行模型误报为可用。
|
|
7
|
+
- 常规回归 `npm test` 默认跳过约 6 分钟的 I/O-heavy 测试池(仍跑 fast + integration);完整三池改为 `npm run test:full` / `LOOP_AGENT_FULL_TEST=1`。本地 `ci.sh`、GitHub PR 与 release/next 发布门禁仍跑全量,不降低交付验证。
|
|
8
|
+
- 修复 backend-test 把 `pytest.param({"title": ""})` 整段 dict 当成 empty 观察、且 `example=` 空值导致 `target=body.title` 解析失败的问题:空 example 仍解析 field,dict 内空字符串按字段 MATCH。
|
|
9
|
+
- 修复 backend-test 把自动化映射多写的横切测试点当成 Markdown BLOCKED、挡住 N11 修 docstring 的问题:只拦 `### 测试点` 未恰好绑定一次。
|
|
10
|
+
- 修复 backend-test 把 pytest `_MISSING` 哨兵当成普通 Name、可选 `min; bound=0` UNDETERMINED 的问题:`_MISSING` 与 `_OMIT` 同为省略键。
|
|
11
|
+
- 修复 backend-test 把可选字段 `min; bound=0` 的 `_OMIT` 判成 UNDETERMINED 的问题:省略键视为空最小;`OMITTED` TP 视为 missing。
|
|
12
|
+
- 修复 backend-test 对 `http_client(operation, json_body=dict)` 观察不到 payload 的问题:http-like Name-call 的第一参是参数化 method 变量时仍计入 `json_body`。
|
|
13
|
+
- 修复 backend-test 把 Markdown `Payload Required Paths: false` 当成路径、GET filter 整 Case UNSAFE 的问题:`true`/`false` 与 `none` 一样不是 path。
|
|
14
|
+
- 修复 backend-test 把列表反引号里的 `场景意图: ... bound=0` 解析失败、并把 `max+1` 的数字 1 当成 minLength 的问题:围栏行可解析,`+/-` 后缀数字不进 prose 窗。
|
|
15
|
+
- 修复 backend-test 把 fixture `http("POST", path, json_body={"title": ...})` 当成无 payload、整 Case PAYLOAD_SHAPE_MISMATCH 的问题:首参为 HTTP method 的 Name-call 进入 observedPaths。eligibility 门禁不放宽。
|
|
16
|
+
- 修复 backend-test 把 `intent=min; bound=0` 当成缺 bound 并落到 `maxLength`、导致空 content MISMATCH 的问题:无 `minLength=` / TP LEN 时保留 0;`max; bound=0` 仍是哨兵。
|
|
17
|
+
- 修复 backend-test 把 evidence-only pytest stub(模块 docstring + Case-ID、无 `def test_`)当成截断脚本、N9 `incomplete-write-set` 的问题:闭合 stub 可通过 completeness,半截 `def (` 仍 fail-closed。
|
|
18
|
+
- 修复 backend-test 把 Markdown 机器行 `intent=max-1; bound=99` 当成契约 max、导致 `x*99` MISMATCH 的问题:`max-1`/`max+1`/`min-1` 优先 `maxLength=` 与文案三元组。eligibility 门禁不放宽。
|
|
19
|
+
- 修复 backend-test 把 pytest 多写、Markdown 没有的 `0:1` extra symbol 当成 Markdown Test Point binding 缺陷并 BLOCKED 的问题:这类条目进入 N11 删除多余函数,不再清空 pytest repair。
|
|
20
|
+
- 修复 backend-test N12 只读 effective collection 再次跑 scenario-param 确定性写回、在 `filesystem-only` 下 `write-guard` 失败的问题:N12 只复验,不再改 pytest。N10 仍可修一次。
|
|
21
|
+
- 修复 backend-test 把 `脚本:无` 的 evidence-only Case 当成缺 pytest 符号送进 N11 的问题:精确 1:0 元用例不再触发 `missing-pytest-symbol` repair,N11 不得发明业务函数。真缺符号的 1:0 仍可修。eligibility 门禁不放宽。
|
|
22
|
+
- 修复 backend-test 把 Scenario Partitions 数据行的 exact `TP-SP-...` 清单写成第 8 列、导致 N6 `MISSING_PARTITION` 的问题:仅当尾列全是该 Partition 的 slot ID 时确定性裁成 7 列并写回 run-owned plan。7 列契约与非 slot 多余列 fail-closed 不放宽。
|
|
23
|
+
- 修复 backend-test scenario 把 `intent=max-1` 当成 unknown、以及 Markdown 只写「100 个字符的最大 / 1999、2000、2001」时解析不到 bound 的问题:`max-1` 进入 closed set,长度清单三元组与「N 个字符的最大」可得到 MATCH。eligibility 门禁不放宽。
|
|
5
24
|
- 修复 backend-test correspondence 把前面函数的 `pytest.param(..., id="TP-...")` 叠进后续 Case `parameterIds` 的问题:decorator 区域遇到上一 `def`/`class` 或非 decorator 续行即停,不再把整组打成 `TEST_POINT_BINDING_EXTRA`。eligibility 门禁不放宽。
|
|
6
25
|
- 修复 backend-test scenario facts 把内部 max/min/oversize helper 哨兵 `-1/-2/-3` 写成 `observedLength`、导致 N10 在 schema `min(0)` 上 `invalid-output` 的问题:这些 helper 仍按 Markdown bound MATCH,持久化长度只保留非负值。
|
|
7
26
|
- 修复 backend-test 把 GET `params=`/`query=` 过滤字段、Python `True`/`False` 布尔字面量和混合大小写 `pytest.param` 列表漏出 correspondence/payload/scenario 观察的问题:这些 variant 现在可以按 item 授权,而不再把整 Case 打成 `PAYLOAD_SHAPE_MISMATCH` / `VARIANT_PARAMETER_MISSING`。eligibility 门禁不放宽。
|
|
@@ -14,6 +33,7 @@
|
|
|
14
33
|
- 新建对话会优先复用当前工作区中真正的空白会话,避免重复创建;有历史消息、工具调用、待发送内容或正在运行的会话不会被复用。
|
|
15
34
|
- 会话列表刷新现在会复用未变化的行和列表引用,标题、运行状态或队列变化时只更新对应会话,侧栏切换和刷新更平滑。
|
|
16
35
|
- Operator Chat 新增仅对具备 Aimax 凭证的用户可见的 `/sdd-bingo` 数据对齐命令;`wizard-local` 模型请求会自动携带与上报一致的 OpenCode 风格会话身份、当前模型和最新 API key,请求头与资格文件变更均在下一次操作自动生效。
|
|
36
|
+
- DAG 节点检查器的「执行记录」默认自动跟随最新内容;右下角新增纯图标悬浮开关,可显式暂停并保留阅读位置,恢复后立即回到底部,悬停时显示当前可执行动作。
|
|
17
37
|
- 修复前端任务产物派生续跑无法识别当前节点链的问题;从实现节点继续时,设计约束、写入准入、验证和收尾步骤会按现行流程安全重放。
|
|
18
38
|
- 统一工作区、Todo 和其他滚动区域的滚动条样式,默认更轻巧,鼠标悬浮时也更容易辨认。
|
|
19
39
|
- HTML 文件预览现在支持页面内的标签切换等交互;较大的自包含 HTML 可以在新窗口中直接完整加载,不再受源码摘要长度影响。
|
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.8",
|
|
4
|
+
"gitSha": "1d1ada8ede350e5778d753c04ed72bacd778e43d",
|
|
5
|
+
"builtAt": "2026-09-02T23:47:25.236Z"
|
|
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({
|
|
@@ -58,6 +58,7 @@ import { readProjectGovernanceContext } from "../workflows/dag/project-governanc
|
|
|
58
58
|
import { assertMavenPlanFresh, MavenPlanStaleError, } from "../verification/maven/index.js";
|
|
59
59
|
async function runBackendScenarioParamPreflight(input) {
|
|
60
60
|
const outputs = [];
|
|
61
|
+
const allowRepair = input.allowRepair !== false;
|
|
61
62
|
const initial = await assessBackendScenarioParamConsistency({
|
|
62
63
|
workspaceRoot: input.workspaceRoot,
|
|
63
64
|
phase: "initial",
|
|
@@ -73,7 +74,7 @@ async function runBackendScenarioParamPreflight(input) {
|
|
|
73
74
|
outputs.push(`scenarioParamInitial=${initialArtifacts.reportPath}`);
|
|
74
75
|
let finalFacts;
|
|
75
76
|
let finalMarkdown;
|
|
76
|
-
if (initial.facts.repairEligible) {
|
|
77
|
+
if (allowRepair && initial.facts.repairEligible) {
|
|
77
78
|
const repair = await applyDeterministicScenarioParamRepairs({
|
|
78
79
|
workspaceRoot: input.workspaceRoot,
|
|
79
80
|
facts: initial.facts,
|
|
@@ -979,6 +980,7 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
979
980
|
workspaceRoot: input.cwd,
|
|
980
981
|
runDir: meta.runDir,
|
|
981
982
|
layout: backendLayout,
|
|
983
|
+
allowRepair: false,
|
|
982
984
|
strictScenarioParamGate: Boolean((meta.spec.globalConstraints ?? []).some((item) => /strictScenarioParamGate\s*=\s*true/i.test(item))),
|
|
983
985
|
});
|
|
984
986
|
outputs.push(...scenario.outputs);
|
|
@@ -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.
|
|
@@ -2338,6 +2347,16 @@ export class ConsolePiRuntime {
|
|
|
2338
2347
|
});
|
|
2339
2348
|
}
|
|
2340
2349
|
catch (error) {
|
|
2350
|
+
const code = error && typeof error === "object" && "code" in error
|
|
2351
|
+
? String(error.code ?? "")
|
|
2352
|
+
: "";
|
|
2353
|
+
if (code === "SESSION_FILE_MISSING" || code === "ENOENT") {
|
|
2354
|
+
return {
|
|
2355
|
+
ok: false,
|
|
2356
|
+
code: "NO_SESSION_FILE",
|
|
2357
|
+
message: `Pi session file missing or not openable: ${init.sessionFile}`,
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2341
2360
|
return {
|
|
2342
2361
|
ok: false,
|
|
2343
2362
|
code: "REOPEN_FAILED",
|
|
@@ -3173,6 +3192,36 @@ export function createDefaultPiSdkBindings() {
|
|
|
3173
3192
|
id: opts.sessionId,
|
|
3174
3193
|
});
|
|
3175
3194
|
},
|
|
3195
|
+
materializeSessionShell: async (opts) => {
|
|
3196
|
+
const sdk = (await loadSdk());
|
|
3197
|
+
const manager = opts.sessionManager;
|
|
3198
|
+
if (typeof manager.setSessionFile !== "function") {
|
|
3199
|
+
throw new Error("Pi SessionManager cannot materialize a session file");
|
|
3200
|
+
}
|
|
3201
|
+
let created = false;
|
|
3202
|
+
try {
|
|
3203
|
+
await mkdir(path.dirname(opts.sessionFile), { recursive: true });
|
|
3204
|
+
// Match Pi's documented session header. Rebinding through the public
|
|
3205
|
+
// SessionManager API marks it flushed, so Pi appends the first user
|
|
3206
|
+
// entry instead of trying to create the already-present file later.
|
|
3207
|
+
await writeFile(opts.sessionFile, `${JSON.stringify({
|
|
3208
|
+
type: "session",
|
|
3209
|
+
version: typeof sdk.CURRENT_SESSION_VERSION === "number"
|
|
3210
|
+
? sdk.CURRENT_SESSION_VERSION
|
|
3211
|
+
: 3,
|
|
3212
|
+
id: opts.sessionId,
|
|
3213
|
+
timestamp: new Date().toISOString(),
|
|
3214
|
+
cwd: opts.cwd,
|
|
3215
|
+
})}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
3216
|
+
created = true;
|
|
3217
|
+
manager.setSessionFile(opts.sessionFile);
|
|
3218
|
+
}
|
|
3219
|
+
catch (error) {
|
|
3220
|
+
if (created)
|
|
3221
|
+
await rm(opts.sessionFile, { force: true }).catch(() => undefined);
|
|
3222
|
+
throw error;
|
|
3223
|
+
}
|
|
3224
|
+
},
|
|
3176
3225
|
createInMemorySessionManager: async (opts) => {
|
|
3177
3226
|
const sdk = (await loadSdk());
|
|
3178
3227
|
return sdk.SessionManager.inMemory(opts.cwd);
|
|
@@ -3189,13 +3238,19 @@ export function createDefaultPiSdkBindings() {
|
|
|
3189
3238
|
// wrong conversation under the durable Console sessionId. Fail closed
|
|
3190
3239
|
// before open when the persisted file is absent.
|
|
3191
3240
|
await access(opts.sessionFile);
|
|
3192
|
-
return sdk.SessionManager.open(opts.sessionFile);
|
|
3193
3241
|
}
|
|
3194
|
-
catch {
|
|
3195
|
-
|
|
3196
|
-
|
|
3242
|
+
catch (error) {
|
|
3243
|
+
const code = error && typeof error === "object" && "code" in error
|
|
3244
|
+
? String(error.code ?? "")
|
|
3245
|
+
: "";
|
|
3246
|
+
if (code === "ENOENT") {
|
|
3247
|
+
throw Object.assign(new Error(`SESSION_FILE_MISSING: ${opts.sessionFile}`), { code: "SESSION_FILE_MISSING", cause: error });
|
|
3248
|
+
}
|
|
3249
|
+
// File exists but is unreadable (permissions / I/O) — caller maps
|
|
3250
|
+
// undefined to 503 rather than silently creating a fresh session.
|
|
3197
3251
|
return undefined;
|
|
3198
3252
|
}
|
|
3253
|
+
return sdk.SessionManager.open(opts.sessionFile);
|
|
3199
3254
|
},
|
|
3200
3255
|
resolveModel: (opts) => {
|
|
3201
3256
|
const rt = opts.modelRuntime;
|