@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.
Files changed (115) hide show
  1. package/AGENTS.md +1 -1
  2. package/CHANGELOG.md +20 -0
  3. package/dist/build-stamp.json +3 -3
  4. package/dist/executors/dag-pi-executor.js +74 -13
  5. package/dist/executors/shell-executor.js +3 -1
  6. package/dist/task/source-prepare/fragment-inventory.js +49 -16
  7. package/dist/task/source-prepare/prepare.js +37 -20
  8. package/dist/worker/console/chat/model-resolver.js +7 -4
  9. package/dist/worker/console/chat/pi-runtime.js +60 -5
  10. package/dist/worker/console/chat/routes.js +69 -46
  11. package/dist/worker/console/chat/sdd-data-alignment.js +6 -3
  12. package/dist/worker/console/chat/session-store.js +16 -0
  13. package/dist/worker/console/pi-readiness.js +3 -2
  14. package/dist/worker/console/server.js +19 -1
  15. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-BJpsGgAv.js → abnfDiagram-N423BO3Z-C6n9_m0P.js} +1 -1
  16. package/dist/worker/console/static/assets/{arc-DjMFPsEd.js → arc-DQh-IfZ1.js} +1 -1
  17. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DqdZlr1O.js → architectureDiagram-T3A2C74G-54NnrwUC.js} +1 -1
  18. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-BmZnfFEO.js → blockDiagram-VBNYF7ZC-pivRALGK.js} +1 -1
  19. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-3cKberpL.js → c4Diagram-5PPSVZJV-BR7OV2NJ.js} +1 -1
  20. package/dist/worker/console/static/assets/channel-DsZxrgqe.js +1 -0
  21. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-q-0yjQ1h.js → chunk-2GRJ4B5K-B1Aq6BcK.js} +1 -1
  22. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-DnONBaDW.js → chunk-2Q5K7J3B-CbdU0rjo.js} +1 -1
  23. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-v3a6PCOS.js → chunk-5RXB4S5H-Dqi2GJeD.js} +1 -1
  24. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D1kLrlU_.js → chunk-5VM5RSS4-BYn1Hu4R.js} +1 -1
  25. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BPelzihi.js → chunk-6Q2QTUOP-DRLjDL0k.js} +1 -1
  26. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-DauNE69H.js → chunk-GF5L2VYU-CY9Xx2jV.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-RIg5_bFJ.js → chunk-JWPE2WC7-BNCZs7_z.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BuILuoxs.js → chunk-KBJHAD2P-DZ8AStLL.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-VHORV5oW.js → chunk-RYQCIY6F-DsxjYrzz.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BJYr4xHC.js → chunk-XXDRQBXY-Ddx1KC1l.js} +1 -1
  31. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-CuToPeGV.js +1 -0
  32. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-CuToPeGV.js +1 -0
  33. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BS6nuqQn.js → cose-bilkent-JH36ORCC-W9TveCnK.js} +1 -1
  34. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BBhF0NAT.js → cynefin-VYW2F7L2-l9j_ztZH.js} +1 -1
  35. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-D1135Ifg.js → cynefinDiagram-MW4NZA55-BMJMKi4G.js} +1 -1
  36. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-vAOYPUWJ.js → dagre-VZM6K2ZE-Bb4mG9pH.js} +1 -1
  37. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-DuaSPkfa.js → diagram-7IWD3JNH-BMgL_Qv0.js} +1 -1
  38. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-mMvXvGU6.js → diagram-B4RE2ZJO-Bvo2T4OQ.js} +1 -1
  39. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-CDIEJtlL.js → diagram-LBJQPF4R-_5kWRN9c.js} +1 -1
  40. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Bi_6JvCW.js → diagram-Q27KOJAE-DqdMrltM.js} +1 -1
  41. package/dist/worker/console/static/assets/{diagram-UB23O5K3-DAqj57yN.js → diagram-UB23O5K3-CDDYsNkp.js} +1 -1
  42. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-DfWaxNEo.js → ebnfDiagram-BXEA7PRR-c4nfnZUV.js} +1 -1
  43. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bt7QqkNG.js → erDiagram-JOGREHBK-DnkUcNMs.js} +1 -1
  44. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-CRRhQwT2.js → flowDiagram-UKHOOZJN-Dz0KGLZE.js} +1 -1
  45. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DAV1O5B6.js → ganttDiagram-PKOTCBZU-Cj1t1uka.js} +1 -1
  46. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-0X2Qo4Lp.js → gitGraphDiagram-DS77QQ5N-tp2FrBHd.js} +1 -1
  47. package/dist/worker/console/static/assets/index-D9Sc0f0y.js +449 -0
  48. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-C5W5eILC.js → infoDiagram-6WML65LV-DQwJS8DH.js} +1 -1
  49. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-5SfqU-Tb.js → ishikawaDiagram-WSZJBQD7-eimCQWD4.js} +1 -1
  50. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-CKDRcaKf.js → journeyDiagram-NVQOT4AX-Dd9Gbwgv.js} +1 -1
  51. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-Cc7_TuSb.js → kanban-definition-27J2QSJJ-Qve3cyft.js} +1 -1
  52. package/dist/worker/console/static/assets/{linear-BcQYYmeT.js → linear-UXKSe36Z.js} +1 -1
  53. package/dist/worker/console/static/assets/{mermaid.core-CwLZPtQo.js → mermaid.core-CWhj4JXN.js} +5 -5
  54. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-D29M025z.js → mindmap-definition-FAOFIHXS-yTcwf4SJ.js} +1 -1
  55. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CV-ZAWYe.js → pegDiagram-VL7TDLO6-_7qToaZP.js} +1 -1
  56. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-C6iPS_o5.js → pieDiagram-7S7Q4E2Y-CXmrKmDm.js} +1 -1
  57. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-BFCD1sTg.js → quadrantDiagram-CIZ2JOQS-hMmrJyaS.js} +1 -1
  58. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CztNj4O9.js → railroadDiagram-AXF67PYL-BXw8tCBe.js} +1 -1
  59. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-BWIELSNd.js → requirementDiagram-LRYGKXZP-BmjgnLZl.js} +1 -1
  60. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-CPtxrjJQ.js → sankeyDiagram-W5VNT64P-Bj77SPpN.js} +1 -1
  61. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-BEWVSFsu.js → sequenceDiagram-SI44F4Z6-B2FDVysL.js} +1 -1
  62. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-DyhzoX_J.js → sizeCapture-X5ZJPWSS-UChNY9ZZ.js} +1 -1
  63. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-xLox0nu7.js → stateDiagram-OKZ733FA-wbAEkbd7.js} +1 -1
  64. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-yPoT19ft.js +1 -0
  65. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-CLC_ow9A.js → swimlanes-SLNWSIFB-DCEkU8HR.js} +2 -2
  66. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-VWdarchG.js +8 -0
  67. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DmJZ4uyY.js → timeline-definition-Z64GVDOM-BGp7H06U.js} +1 -1
  68. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-DD7GGi9Z.js → vennDiagram-T6HMQDX7-DsCIQuWR.js} +1 -1
  69. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-Bi8PQoGt.js → wardleyDiagram-T6FBY63Y-cRZLP4Xe.js} +1 -1
  70. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-uKdAY8Dp.js → xychartDiagram-ELKLHX3M-CP0xLR9Y.js} +1 -1
  71. package/dist/worker/console/static/index.html +1 -1
  72. package/dist/worker/console/static-src/app/useRecoveryConsole.js +5 -5
  73. package/dist/worker/console/static-src/operator-chat/input-history.js +8 -6
  74. package/dist/worker/console/static-src/operator-chat/runtime-snapshot-store.js +10 -0
  75. package/dist/worker/console/static-src/operator-chat/useChatSessions.js +31 -2
  76. package/dist/worker/console/static-src/operator-chat/useComposer.js +8 -3
  77. package/dist/worker/console/static-src/operator-chat/useRuntimeControls.js +29 -6
  78. package/dist/worker/console/static-src/operator-chat/useRuntimeSnapshot.js +8 -2
  79. package/dist/worker/console/static-src/shell/console-update-reload.js +36 -9
  80. package/dist/worker/console/static-src/shell/workspace-route.js +11 -0
  81. package/dist/worker/observe/static/dag-helpers.js +4 -2
  82. package/dist/worker/observe/static/inspect-workspace.js +23 -0
  83. package/dist/worker/observe/static/kpi.js +2 -2
  84. package/dist/worker/observe/static/operator-chrome.d.ts +10 -2
  85. package/dist/worker/observe/static/operator-chrome.js +37 -23
  86. package/dist/worker/observe/static/relations.js +2 -2
  87. package/dist/worker/observe/static/router.d.ts +12 -1
  88. package/dist/worker/observe/static/router.js +48 -6
  89. package/dist/worker/observe/static/run-processing.js +4 -2
  90. package/dist/worker/observe/static/shell-chrome.js +2 -2
  91. package/dist/worker/observe/static/state.js +5 -0
  92. package/dist/worker/observe/static/styles.css +95 -0
  93. package/dist/worker/observe/static/views/dag-inspector.js +14 -7
  94. package/dist/worker/observe/static/views/dag.js +10 -2
  95. package/dist/worker/observe/static/views/dags.js +3 -1
  96. package/dist/worker/observe/static/views/dashboard.js +14 -6
  97. package/dist/worker/observe/static/views/pool.js +7 -2
  98. package/dist/worker/observe/static/views/run.js +12 -3
  99. package/dist/worker/observe/static/views/session-timeline.js +69 -10
  100. package/dist/worker/observe/static/views/task.js +7 -2
  101. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +40 -9
  102. package/dist/workflows/dag/backend-test-scenario-param.js +126 -19
  103. package/dist/workflows/dag/backend-test-scenario-partitions.js +62 -1
  104. package/dist/workflows/dag/backend-test-writer-completeness.js +7 -3
  105. package/dist/workflows/dag/frontend-shadow-dual-write.js +33 -6
  106. package/dist/workflows/dag/init-hybrid.js +175 -27
  107. package/dist/workflows/dag/node-execution.js +1 -1
  108. package/dist/workflows/dag/types.js +1 -1
  109. package/package.json +3 -2
  110. package/dist/worker/console/static/assets/channel-DWYwciiv.js +0 -1
  111. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-D6jOf_l4.js +0 -1
  112. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-D6jOf_l4.js +0 -1
  113. package/dist/worker/console/static/assets/index-CVdddQsA.js +0 -451
  114. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DS5ZRvJG.js +0 -1
  115. 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 可以在新窗口中直接完整加载,不再受源码摘要长度影响。
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.42.0-next.6",
4
- "gitSha": "6919b06fcb268f3a61890e85b6bc4dcbf11b62e1",
5
- "builtAt": "2026-09-02T06:01:45.078Z"
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
- // Read-budget telemetry is diagnostic only when the provider/executor has
4095
- // already failed. In particular, keep context-overflow so node-execution
4096
- // selects its compact retry envelope instead of treating the attempt as a
4097
- // generic read-burst retry.
4098
- if (!mapped.ok) {
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
- /** 显式 AC 式 ID 前缀:AC-001 / AC-FE-001 / AC-FE-INS-001 等(允许 bullet 标记后空格
20
- * 与 checklist 勾选框)。收紧解析(AC-001 / AC-HARD-001):AC 后必须跟分隔符
21
- * (-/_)+ 大写字母/数字 id 形态(含可选 FE/BE/UI/API 作用域前缀与 V4 `#### AC-*`
22
- * 标题归属);`## Acceptance Criteria` / `## Acceptance` / Ac 开头普通单词永不产生
23
- * 伪造 id(ACCEPTANCE 类 id 全量消失)。捕获组 1 = AC id,捕获组 2 = 余下文本。 */
24
- const EXPLICIT_AC_ID_RE = /^\s*(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?(AC(?:[-_](?:FE|BE|UI|API))?[-_][A-Z0-9]+(?:[-_][A-Z0-9]+)*)\s*[::]?\s*(.*)$/iu;
25
- /** 显式 AC 标题式:`#### AC-FE-001 Save nickname` / `### AC-001:正文`。
26
- * AC 后必须跟分隔符(-/_)+ id 形态;普通单词(Acceptance Criteria / Acceptance)
27
- * 永不匹配,按 ACCEPTANCE_HEADING_RE 章节分类生成稳定 REQ-SRC-* 片段。
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 matchTableRowAcId(line) {
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 = EXPLICIT_AC_HEADING_RE.exec(first);
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 = EXPLICIT_AC_ID_RE.exec(line.trim());
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 = EXPLICIT_AC_HEADING_RE.exec(headingText.trim());
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
- : matchExplicitAcId(fragment.text);
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
- if (hasImportedPrd &&
800
- sourceIntegrity &&
801
- sourceIntegrity.parseableDocuments.some((doc) => doc.role === "requirement" || doc.role === "acceptance")) {
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.parseableDocuments.filter((doc) => (doc.role === "requirement" || doc.role === "acceptance") &&
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 sourceDocuments = authoritativeDocs.map((doc) => {
807
- // Canonical source identity 单一边界(AC-001 / AC-FIDELITY-REPAIR-001):
808
- // ledger sourceDocuments[].path 与 DAG 侧 serializeDagTaskSourcePath /
809
- // resolveDagTaskSourcePath 一致(repo-root relative POSIX),路径转换只
810
- // 发生在该边界,不再二次转换进入 digest 计算。
811
- const absolute = path.resolve(contractPaths.sourceDir, doc.materializedPath);
812
- return {
813
- path: path
814
- .relative(repoRoot, absolute)
815
- .split(path.sep)
816
- .join("/"),
817
- content: doc.content,
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 { provider: entry?.provider, resolved: Boolean(entry) };
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
- // Missing/corrupt session file — caller fails closed rather than
3196
- // silently creating a fresh session and losing prior context.
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;