@tea-agent/loop-agent 0.38.2 → 0.38.3

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 (133) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -0
  3. package/bin/loop-agent.js +7 -3
  4. package/dist/application/task-lifecycle/advance.js +4 -0
  5. package/dist/build-stamp.json +3 -3
  6. package/dist/commands/task-advance.js +31 -0
  7. package/dist/executors/dag-pi-executor.js +58 -1
  8. package/dist/executors/pi-event-serializer.js +42 -1
  9. package/dist/executors/pi-executor.js +51 -0
  10. package/dist/executors/pi-extension-resolver.js +233 -0
  11. package/dist/executors/pi-sdk-executor.js +187 -59
  12. package/dist/executors/shell-executor.js +56 -2
  13. package/dist/executors/shell-write-guard.js +7 -0
  14. package/dist/task/contract/constants.js +1 -0
  15. package/dist/task/contract/diff.js +26 -0
  16. package/dist/task/contract/project.js +5 -0
  17. package/dist/task/contract/schema.js +6 -1
  18. package/dist/task/source-prepare/build-draft.js +15 -0
  19. package/dist/worker/console/pi-readiness.js +4 -4
  20. package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-C4bmo3iY.js → abnfDiagram-N423BO3Z-C4kVYweA.js} +1 -1
  21. package/dist/worker/console/static/assets/{arc-SAD7McOS.js → arc-Bj2M8iO5.js} +1 -1
  22. package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-DAVLUdK1.js → architectureDiagram-T3A2C74G-CLlXe4-6.js} +1 -1
  23. package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CPPdv4vj.js → blockDiagram-VBNYF7ZC-psEp5xH0.js} +1 -1
  24. package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-D1GRGZax.js → c4Diagram-5PPSVZJV-DeKcOCGJ.js} +1 -1
  25. package/dist/worker/console/static/assets/channel-CPF4N7pf.js +1 -0
  26. package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-D_Bq2ZqK.js → chunk-2GRJ4B5K-K0C744p1.js} +1 -1
  27. package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CPLVtamK.js → chunk-2Q5K7J3B-xbNw4J9-.js} +1 -1
  28. package/dist/worker/console/static/assets/{chunk-5RXB4S5H-DnaZWcL3.js → chunk-5RXB4S5H-DjLaSDUO.js} +1 -1
  29. package/dist/worker/console/static/assets/{chunk-5VM5RSS4-BRTmFgnq.js → chunk-5VM5RSS4-CtiPlKel.js} +1 -1
  30. package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-BRlfYZoH.js → chunk-6Q2QTUOP-DZtolir7.js} +1 -1
  31. package/dist/worker/console/static/assets/{chunk-GF5L2VYU-CjNybD4u.js → chunk-GF5L2VYU-CPcdNeew.js} +1 -1
  32. package/dist/worker/console/static/assets/{chunk-JWPE2WC7-D3e-74fG.js → chunk-JWPE2WC7-NgHc6V37.js} +1 -1
  33. package/dist/worker/console/static/assets/{chunk-KBJHAD2P-Br3ir6f2.js → chunk-KBJHAD2P-DQ_T-hg8.js} +1 -1
  34. package/dist/worker/console/static/assets/{chunk-RYQCIY6F-1NgVuRbh.js → chunk-RYQCIY6F-DgLsxcVP.js} +1 -1
  35. package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BTYuvuWN.js → chunk-XXDRQBXY-UrVoM9z1.js} +1 -1
  36. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-C2DwA_Y1.js +1 -0
  37. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-C2DwA_Y1.js +1 -0
  38. package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-BC6Bgosf.js → cose-bilkent-JH36ORCC-CLkYvTb3.js} +1 -1
  39. package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-DV-HypfL.js → cynefin-VYW2F7L2-BwT_xKrE.js} +1 -1
  40. package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-DhTaddO-.js → cynefinDiagram-MW4NZA55-Bi9tuzif.js} +1 -1
  41. package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-BnEcOmiI.js → dagre-VZM6K2ZE-BCIqkWBV.js} +1 -1
  42. package/dist/worker/console/static/assets/{diagram-7IWD3JNH-xphGRVKp.js → diagram-7IWD3JNH-Dip6l9_Z.js} +1 -1
  43. package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CrdD1mRG.js → diagram-B4RE2ZJO-B-xkh_wK.js} +1 -1
  44. package/dist/worker/console/static/assets/{diagram-LBJQPF4R-D4YdLsaF.js → diagram-LBJQPF4R-DZO0kTt_.js} +1 -1
  45. package/dist/worker/console/static/assets/{diagram-Q27KOJAE-LCKyGZ5U.js → diagram-Q27KOJAE-D6ZplNbZ.js} +1 -1
  46. package/dist/worker/console/static/assets/{diagram-UB23O5K3-BeiTe7t0.js → diagram-UB23O5K3-BPekbhoS.js} +1 -1
  47. package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-BO2cq8GS.js → ebnfDiagram-BXEA7PRR-BtmaJpK5.js} +1 -1
  48. package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-CmWww3Nt.js → erDiagram-JOGREHBK-6DS0Xc44.js} +1 -1
  49. package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-C2kkUYg5.js → flowDiagram-UKHOOZJN-CihnROSm.js} +1 -1
  50. package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-DY_JZ2iQ.js → ganttDiagram-PKOTCBZU-CZC4zOE2.js} +1 -1
  51. package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-BizhCSw9.js → gitGraphDiagram-DS77QQ5N-Dq1fhzGN.js} +1 -1
  52. package/dist/worker/console/static/assets/{index-BXN89VPX.js → index-DTZOKgAn.js} +142 -63
  53. package/dist/worker/console/static/assets/index-Ya5FE7cD.css +1 -0
  54. package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-Dlo2eK_d.js → infoDiagram-6WML65LV-DlSl4BGx.js} +1 -1
  55. package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-3EIlIIuZ.js → ishikawaDiagram-WSZJBQD7-snnzFl_U.js} +1 -1
  56. package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DiMDe5Yh.js → journeyDiagram-NVQOT4AX-BDE7qpMx.js} +1 -1
  57. package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-sN553oIN.js → kanban-definition-27J2QSJJ-CD2Ci04G.js} +1 -1
  58. package/dist/worker/console/static/assets/{linear-D9Py31Ld.js → linear-DdnapKIH.js} +1 -1
  59. package/dist/worker/console/static/assets/{mermaid.core-D7Occzk_.js → mermaid.core-BVjAT9b8.js} +5 -5
  60. package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-DjDRboiR.js → mindmap-definition-FAOFIHXS-D0yJk-zA.js} +1 -1
  61. package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-Cgc4qlzp.js → pegDiagram-VL7TDLO6-BVo9tFP-.js} +1 -1
  62. package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-DxZj8pKd.js → pieDiagram-7S7Q4E2Y-DnOV-mZ_.js} +1 -1
  63. package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-CJ50MngR.js → quadrantDiagram-CIZ2JOQS-CzUJo58i.js} +1 -1
  64. package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-BY-OD8Tr.js → railroadDiagram-AXF67PYL-Bvikrolh.js} +1 -1
  65. package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-CQFButfK.js → requirementDiagram-LRYGKXZP-DtaSVCap.js} +1 -1
  66. package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-BB_QUK_b.js → sankeyDiagram-W5VNT64P-C2c9A0wW.js} +1 -1
  67. package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CqhXZjyx.js → sequenceDiagram-SI44F4Z6-DmXJcU7r.js} +1 -1
  68. package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-2l1U4rgJ.js → sizeCapture-X5ZJPWSS-B4GUFW92.js} +1 -1
  69. package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-BX1FbrhJ.js → stateDiagram-OKZ733FA-Dsad2MXf.js} +1 -1
  70. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-B8FB_cNk.js +1 -0
  71. package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-DOSnT7hb.js → swimlanes-SLNWSIFB-DGq48Fbi.js} +2 -2
  72. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-CrZYaWfx.js +8 -0
  73. package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-DVJQA7AN.js → timeline-definition-Z64GVDOM-CSSi6OFf.js} +1 -1
  74. package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-74uxl9gx.js → vennDiagram-T6HMQDX7-BqyzPrWv.js} +1 -1
  75. package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-DsBxlcg_.js → wardleyDiagram-T6FBY63Y-DlznVb6S.js} +1 -1
  76. package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-snuW9Mjw.js → xychartDiagram-ELKLHX3M-CfBcgJ_K.js} +1 -1
  77. package/dist/worker/console/static/index.html +2 -2
  78. package/dist/worker/console/static-src/operator-chat/open-preview-in-browser.js +328 -0
  79. package/dist/worker/observe/node-input.js +72 -3
  80. package/dist/worker/observe/static/dag-history-labels.js +4 -0
  81. package/dist/worker/observe/static/state.js +2 -2
  82. package/dist/worker/observe/static/views/dag-inspector.js +51 -0
  83. package/dist/worker/observe/static/views/session-timeline.js +18 -5
  84. package/dist/workflows/dag/backend-test-case-coverage-analysis.js +56 -4
  85. package/dist/workflows/dag/backend-test-scenario-partitions.js +73 -1
  86. package/dist/workflows/dag/contract-output-registry.js +15 -0
  87. package/dist/workflows/dag/failure-category.js +4 -0
  88. package/dist/workflows/dag/frontend-implementation-contract.js +140 -39
  89. package/dist/workflows/dag/frontend-prewrite-gate.js +87 -135
  90. package/dist/workflows/dag/frontend-test-case-quality.js +5 -13
  91. package/dist/workflows/dag/frontend-test-environment-probe.js +227 -0
  92. package/dist/workflows/dag/frontend-test-markdown.js +61 -0
  93. package/dist/workflows/dag/frontend-test-result-contract.js +10 -18
  94. package/dist/workflows/dag/frontend-test-standard-scenarios.js +68 -0
  95. package/dist/workflows/dag/init-hybrid.js +79 -101
  96. package/dist/workflows/dag/node-execution.js +119 -8
  97. package/dist/workflows/dag/prompt.js +15 -1
  98. package/dist/workflows/dag/rerun-plan.js +22 -3
  99. package/dist/workflows/dag/structured-output-repair.js +712 -0
  100. package/dist/workflows/dag/types.js +23 -0
  101. package/dist/workflows/dag/validate.js +41 -1
  102. package/docs/operations/local-development-environment.md +1 -5
  103. package/docs/skills/vetted-skill-registry.md +14 -0
  104. package/docs/templates/README.md +1 -1
  105. package/docs/templates/agent-dag.schema.json +44 -0
  106. package/docs/templates/backend-test-dag.json +2 -2
  107. package/docs/templates/frontend-test-dag.json +8 -10
  108. package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +1 -1
  109. package/package.json +8 -4
  110. package/skills/analyze-product-requirements/SKILL.md +8 -5
  111. package/skills/analyze-product-requirements/references/acceptance-criteria.md +1 -1
  112. package/skills/analyze-product-requirements/references/clarification-and-knowledge.md +10 -6
  113. package/skills/analyze-product-requirements/references/example.md +50 -0
  114. package/skills/analyze-product-requirements/references/forward-test-cases.md +11 -7
  115. package/skills/analyze-product-requirements/references/kb-integration.md +5 -5
  116. package/skills/analyze-product-requirements/references/product-analysis-schema.md +8 -4
  117. package/skills/analyze-product-requirements/references/product-requirement-schema.md +2 -2
  118. package/skills/analyze-product-requirements/references/requirement-clarification-schema.md +1 -1
  119. package/skills/analyze-product-requirements/scripts/test-validators.mjs +11 -1
  120. package/skills/analyze-product-requirements/scripts/validate-product-analysis.mjs +20 -1
  121. package/skills/analyze-product-requirements/scripts/validate-requirement-clarification.mjs +9 -0
  122. package/skills/codebase-scout/SKILL.md +1 -1
  123. package/skills/improve-codebase-architecture/SKILL.md +81 -0
  124. package/skills/improve-codebase-architecture/deepening.md +37 -0
  125. package/skills/improve-codebase-architecture/html-report.md +123 -0
  126. package/skills/improve-codebase-architecture/interface-design.md +44 -0
  127. package/skills/improve-codebase-architecture/language.md +53 -0
  128. package/dist/worker/console/static/assets/channel-R6oQIDkw.js +0 -1
  129. package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-BPXlmPWI.js +0 -1
  130. package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-BPXlmPWI.js +0 -1
  131. package/dist/worker/console/static/assets/index-CuH5CNFu.css +0 -1
  132. package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DJKi0Qel.js +0 -1
  133. package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-YYe7EEm5.js +0 -8
package/CHANGELOG.md CHANGED
@@ -4,10 +4,21 @@
4
4
 
5
5
  ### 变更
6
6
 
7
+ - `next-release.yml` 防重复发布守卫补充幂等语义:`mode=recover`(dist-tags.next 已指向出自本 HEAD 的 tarball)时,手动 `workflow_dispatch` 重入不再撞 duplicate publish 硬失败,改为写 summary 后干净退出(§5.1 规则 4);非 recover 的重复发布仍硬失败。
8
+
9
+ - frozen repair 工具审计改为按 attempt 段扫描(B1):`session-events.jsonl` 跨 attempt 共享追加,attempt>1 的 `auditRepairAttemptToolUse` 现在只审本 attempt 字节段(newline 对齐),planner attempt 1 的 ls/find 不再被误判给 repair 轮;repair prompt 显式声明 read-only 工具契约与 allowlist 边界(B2),并内嵌 openspec-citations 输出格式正例;`LOOP_AGENT_IGNORE_STALE_DIST=1` 环境变量旁路 stale-dist guard(B3),Console spawn 的 sibling CLI 可继承。
10
+ - frontend plan/revision 结构化输出改为冻结 repair context + hash-bound canonical artifact:attempt>1 不再重放完整 planner prompt;成功后 stdout 只保留 artifact 路径,design review / prewrite 只消费已校验 artifact,不再从节点文本 merge。失败分类补齐 `structured-repair-exhausted` / `governance-blocked`,并修正 `structured-output-truncated` / `output-too-large` 的归一化映射。
11
+ - 将 `@earendil-works/pi-ai` 与 `@earendil-works/pi-coding-agent` 从可选依赖提升为精确 pin 的运行时依赖 `0.83.0`,将运行时直接导入的 `typebox` 精确声明为 `1.3.7`,并在根包声明 Node.js `>=22.19.0`。安装 `@tea-agent/loop-agent` 会默认装上 Pi SDK(含 `--omit=optional` 场景);`@cursor/sdk` 仍保持 optional。availability / readiness 探针、隔离 tarball canary、发布包契约测试与安装/本地环境说明同步对齐。
12
+ - backend-test Scenario Partition 仅接受绑定源有限 enum/分类集合的 GET/list query/path 轴;非法 Domain(空/重复/保留 token、无有限域、与 OpenAPI enum 不一致)报 `INVALID_PARTITION_DOMAIN` / `NO_FINITE_DOMAIN` / `PARTITION_SOURCE_MISMATCH` 根因,不再生成空后缀 `TP-SP-...-`、重复 slot,也不再级联 `MISSING_*_SLOT`。N6 对这些根因码 fail-closed;合法源分区缺槽仍阻断。N2 禁止为自由串/主键/required-only/boundary-only 建分区;N5 允许删除无源非法 Partition 及其派生槽位。
7
13
  - npm `next` 渠道发布改为提交数节流:main push 仅当距上次 next 发布的 source commit ≥ 5 个 commit 时才自动发布(新增 `scripts/next-publish-gate.mjs`,读已发布 tarball build-stamp 对比 HEAD);不足阈值时 workflow 绿色跳过,手动 `workflow_dispatch` 不受限(切 release 线前强制出包 / 紧急补发)。发布文档真源 §4.1/§5.1/§7/§12 同步。
8
14
 
9
15
  ### 新增
10
16
 
17
+ - 观察面 DAG Inspector 节点详情适配 Pi 扩展:执行器输入摘要展示节点声明的 `piExtensions` 短 id;新增「Pi 扩展运行事实」展示 `pi-extensions.json` 的 resolved(包名@版本)与 missing 降级原因(缺包降级不再静默);产物未生成/损坏时降级提示不阻断接口,绝对路径不外泄。
18
+ - 后端实现 DAG(standard / review-gated / supervised)的 Pi 节点支持按桶显式加载本机 Pi 导航扩展:读侧节点(除 closeout)加载 `pi-codegraph` + `pi-lens`,写侧节点只加载 `pi-codegraph`(pi-lens 的 `ast_grep_replace` 可绕过 writeSet,不加载);前端/测试模板与省略字段节点保持扩展全关。扩展发现始终关闭,只经白名单解析 `~/.pi/agent` 与项目 `.pi` settings 的已装包(缺包降级不失败,产物 `pi-extensions.json` 记录 requested/resolved/missing);session 后 pin 工具面剔 builtin 同名、deny 面与写工具,writer 的 `edit`/`write` 仍为 policy-wrapped;`.codegraph/**` index 同步不再计入 write-guard 越界。
19
+ - Pi SDK 终态 session-events JSONL 顶层增加可选 `finish_reason`:仅当 `message_end` / `turn_end` / `assistant_message` 明确携带非空终止原因时由序列化层提升,不伪造默认值,也不回流 `stopReason` 诊断。
20
+ - `backendTest` 布局配置纳入 Task Contract managed 注入面:`task advance` 新增 `--backend-test <json>`,可在公共任务生命周期内把 `testRoot`/`markdownRel`/`scriptRel`/`sharedSetupDoc`/`mode`/`gapDoc`/`executeScope` 写进 `task.json`,不再需要手工编辑(手工编辑会让任务漂移成 `externally-modified` 并拒绝 `--approve-gate`)。给出即整体替换、省略即保留既有配置(历史手写配置因此自愈)、`'{}'` 为显式清空回默认布局;重复 advance 幂等,契约保持 `managed`。`task contract diff` 把值变更标 routine `BACKEND_TEST_CONFIG_CHANGED`、丢键/清空额外标 elevated `BACKEND_TEST_CONFIG_WEAKENED`。
21
+ - Operator Chat 文件预览抽屉新增图标「新窗口打开」:用当前已加载内容在独立浏览器窗口打开全页阅读视图,窗口内可切换「预览 / 查看原文」(带宽高的 `window.open` popup,Markdown 排版、HTML 铺满视口的空 sandbox iframe、JSON pretty-print / 其它文本等宽滚动);打开成功后断开 `opener` 且不立刻 revoke Blob,避免空白页;弹窗被拦截时 header 附近报错且不关闭抽屉
11
22
  - backend-test 适配 A–D:可配置 `backendTest` 布局(`testRoot`/`markdownRel`/`scriptRel`)、用户公共前置文档引用与模块顶部 `user_shared_setup`(不写 conftest)、查询轴 Scenario Partitions 确定性展开与 fail-closed 门禁、`mode=gap-fill` 13 节点增量 DAG。Slot ID 以声明的 Partition ID 为真源(`TP-<Partition ID>-...`)。
12
23
  - 前端 plan 唯一机器契约化(JSON-only):`frontend-plan-pi` / `frontend-plan-revision-pi` 输出收敛为「一行导语 + 唯一 fenced `json` 契约 + 唯一 `openspec-citations` 引用块」,彻底移除「可选 Markdown 说明」;plan.md(`contracts/frontend-plan.md`)改由 prewrite gate 用纯函数 `renderFrontendPlanMarkdown` 确定性渲染(不新增 DAG 节点/不新增 Pi 调用)
13
24
  - 契约 schema 新增 5 个可选字段(`implementationSteps` / `stylingStrategy` / `dependencyPolicy` / `residualRisks` / `realIntegrationGap`),`schemaVersion` 保持 1;`docs/templates/frontend-implementation-contract.schema.json` 同步
@@ -28,6 +39,13 @@
28
39
 
29
40
  ### 改进
30
41
 
42
+ - frontend DAG 的 materialized source references 恢复统一的有界 excerpt 与按需 read 指针,不再把 requirement/acceptance 文档全文复制进每个相关节点,也不再强制 contract/plan 重读全部绑定源文件,降低长需求在多节点中的重复上下文占用。
43
+ - frontend-test prepare-package 会从 `docs/templates/`、`harness.json` `governanceRoot`/templates 以及 init 投影的 `ai_workspace/loop-agent/templates/` 拷贝标准场景;full init 目标仓不再静默落到仅含 `STD-FE-SMOKE-ENTRY` 的 fallback。
44
+ - frontend-test 环境探测区分 connection-refused(curl 7)与 HTTP 4xx/5xx,并提示先启动本地前端后再从 probe 节点续跑;不再把所有不可达都写成同一句 `frontend-base-url-unreachable`。
45
+ - frontend-test 报告解析用例 Markdown 的 `##`/`###` 标题与有序列表(`测试步骤` / `预期结果` / `前置条件与重置`);缺 `测试目的` 时回退到用例 H1,避免主 HTML 出现空步骤。
46
+ - `dag rerun --from-node` 把尚未启动的 writer/map 视为首次执行;frontend-test probe 这种失败的 exclusive shell 可以带着下游未跑节点续跑,不必整图 `standaloneTaskRerun`。
47
+ - Pi SDK 节点在 stall watchdog(默认 15 分钟无 provider 活动)触发后,会先 abort 当前等待,并在同一会话自动发送一次「继续」;仅 abort 已确认且尚未调用 write 工具时恢复,失败仍判 timeout。绝对墙钟超时与 CLI 后端不走这条恢复。
48
+ - DAG 节点检查器「执行过程」时间线默认展开原始协议事件(`message_start` / `message_end`),按钮文案为「隐藏原始协议事件」;用户可点选关闭,同会话内选择会保持。
31
49
  - Operator Chat 的乐观停止现在把“视觉已停止”和 Turn ownership 分开:Abort 获得可靠终态前明确显示“正在停止”,此时可为原会话保存一条服务端持久化的“停止后发送”消息;后一次覆盖前一次,切换会话、刷新页面或 Console 重启不会丢失,旧 Turn 结束后复用正常 Turn 准入路径自动发送,并以固定 `clientRequestId` 防止重复执行。
32
50
  - Operator Chat 修复快速终止时“停止后发送”与普通发送之间的准入空洞:发送路径改读实时 ownership;若 deferred 请求恰好撞上旧 Turn 终态,会先按服务端状态对账,再使用同一 `clientRequestId` 自动转为普通发送,不再要求用户刷新重试或丢失本次提交。
33
51
  - `dag rerun-task` 现在会把父 run 最后一轮仍未解决的 `verify-pi` / review 反馈、证据引用和修订轮次带入新完整 DAG;planner、implementer 与 verifier/reviewer 必须修复或举证说明不再适用,不能仅因文件已存在就返回 `already-satisfied`。Console 同步区分「自动修订」「从失败节点恢复」「带反馈重新执行任务」。
package/README.md CHANGED
@@ -24,6 +24,8 @@
24
24
 
25
25
  ### 安装
26
26
 
27
+ 需要 Node.js `>=22.19.0`。建议先运行 `node --version` 确认环境满足要求;启用 npm `engine-strict` 时,旧版本 Node.js 会直接拒绝安装。
28
+
27
29
  ```bash
28
30
  npm install -g @tea-agent/loop-agent@latest
29
31
  loop-agent --version
package/bin/loop-agent.js CHANGED
@@ -18,11 +18,15 @@ if (!existsSync(cliEntry)) {
18
18
  * to silently execute a dist/ that predates the latest source edit. The
19
19
  * classic failure mode is editing src/** and running the bin without
20
20
  * `npm run build`, which executes the previous build. Suppress with
21
- * --ignore-stale-dist when intentionally running a stale build.
21
+ * --ignore-stale-dist (or LOOP_AGENT_IGNORE_STALE_DIST=1, which sibling
22
+ * processes such as a Console-spawned CLI inherit automatically) when
23
+ * intentionally running a stale build.
22
24
  */
23
25
  const srcDir = join(packageRoot, "src");
24
- const ignoreStale = process.argv.includes("--ignore-stale-dist");
25
- if (ignoreStale) {
26
+ const ignoreStale =
27
+ process.argv.includes("--ignore-stale-dist") ||
28
+ process.env.LOOP_AGENT_IGNORE_STALE_DIST === "1";
29
+ if (process.argv.includes("--ignore-stale-dist")) {
26
30
  process.argv = process.argv.filter((arg) => arg !== "--ignore-stale-dist");
27
31
  }
28
32
  if (!ignoreStale && existsSync(srcDir)) {
@@ -38,6 +38,7 @@ function hasEngineeringBoundary(input) {
38
38
  return Boolean((input.allowedPaths && input.allowedPaths.length > 0) ||
39
39
  (input.forbiddenPaths && input.forbiddenPaths.length > 0) ||
40
40
  (input.verifyCommands && input.verifyCommands.length > 0) ||
41
+ input.backendTest !== undefined ||
41
42
  input.fromText ||
42
43
  input.fromDraftPath);
43
44
  }
@@ -632,6 +633,7 @@ export async function advanceTaskLifecycle(input) {
632
633
  allowedPaths: effectiveInput.allowedPaths,
633
634
  forbiddenPaths: input.forbiddenPaths,
634
635
  verifyCommands: effectiveInput.verifyCommands,
636
+ backendTest: input.backendTest,
635
637
  },
636
638
  });
637
639
  if (!prepareResult.ok) {
@@ -687,6 +689,7 @@ export async function advanceTaskLifecycle(input) {
687
689
  allowedPaths: effectiveInput.allowedPaths,
688
690
  forbiddenPaths: input.forbiddenPaths,
689
691
  verifyCommands: effectiveInput.verifyCommands,
692
+ backendTest: input.backendTest,
690
693
  },
691
694
  });
692
695
  if (semantic.ok) {
@@ -707,6 +710,7 @@ export async function advanceTaskLifecycle(input) {
707
710
  allowedPaths: effectiveInput.allowedPaths,
708
711
  forbiddenPaths: input.forbiddenPaths,
709
712
  verifyCommands: effectiveInput.verifyCommands,
713
+ backendTest: input.backendTest,
710
714
  },
711
715
  });
712
716
  if (retry.ok) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "version": "0.38.2",
4
- "gitSha": "ba66152b71609e67330471e5325c1efddd200412",
5
- "builtAt": "2026-08-20T07:31:23.829Z"
3
+ "version": "0.38.3",
4
+ "gitSha": "228da3eaaf3c1a2082af7b5c15aa2e4653afb3ce",
5
+ "builtAt": "2026-08-22T02:44:24.204Z"
6
6
  }
@@ -1,4 +1,5 @@
1
1
  import { advanceTaskLifecycle, } from "../application/task-lifecycle/index.js";
2
+ import { backendTestConfigSchema, } from "../task/config-types.js";
2
3
  import { buildOperatorResult, operatorFailed, processExitCodeForOutcome, writeOperatorJson, } from "../shared/operator/index.js";
3
4
  import { createRunDagProgressObserver, validateRunDagProgressIntervalMs, } from "./run-dag-progress.js";
4
5
  const COMMAND = "task advance";
@@ -14,6 +15,7 @@ const USAGE = `usage:
14
15
  [--forbidden-path <glob>]...
15
16
  [--verify <label:command>]...
16
17
  [--verify-timeout <label:timeoutMs>]...
18
+ [--backend-test <json>]
17
19
  [--accept-recommendations <digest>]
18
20
  [--approve-gate <gate-id:digest>]
19
21
  [--reject-gate <gate-id:digest> --reason <text>]
@@ -52,6 +54,30 @@ function parseVerifyTimeout(flag) {
52
54
  }
53
55
  return { label: flag.slice(0, idx), timeoutMs };
54
56
  }
57
+ /**
58
+ * Parse `--backend-test <json>` into a validated managed backendTest config.
59
+ * `{}` is legal (explicit clear back to the default layout).
60
+ */
61
+ function parseBackendTestFlag(raw) {
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(raw);
65
+ }
66
+ catch {
67
+ throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
68
+ }
69
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
70
+ throw new Error(`invalid --backend-test ${raw}; expected an inline JSON object`);
71
+ }
72
+ const result = backendTestConfigSchema.safeParse(parsed);
73
+ if (!result.success) {
74
+ const summary = result.error.issues
75
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
76
+ .join("; ");
77
+ throw new Error(`invalid --backend-test: ${summary}`);
78
+ }
79
+ return result.data ?? {};
80
+ }
55
81
  export function parseTaskAdvanceArgs(args) {
56
82
  const options = {};
57
83
  const positional = [];
@@ -113,6 +139,10 @@ export function parseTaskAdvanceArgs(args) {
113
139
  options.verifyTimeout = pushList(options.verifyTimeout, next());
114
140
  continue;
115
141
  }
142
+ if (token === "--backend-test") {
143
+ options.backendTest = parseBackendTestFlag(next());
144
+ continue;
145
+ }
116
146
  if (token === "--accept-recommendations") {
117
147
  options.acceptRecommendations = next();
118
148
  continue;
@@ -273,6 +303,7 @@ export function toUseCaseInput(repoRoot, taskId, options, observer) {
273
303
  allowedPaths: options.allowedPaths,
274
304
  forbiddenPaths: options.forbiddenPaths,
275
305
  verifyCommands: verifyCommands.length > 0 ? verifyCommands : undefined,
306
+ backendTest: options.backendTest,
276
307
  approveGateToken: options.approveGate,
277
308
  rejectGateToken: options.rejectGate,
278
309
  rejectReason: options.reason,
@@ -2,7 +2,8 @@ import path from "node:path";
2
2
  import { createHash } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { writeDagNodeJsonArtifact, writeTextArtifactFile, } from "../infrastructure/harness/artifact-store.js";
5
- import { executePiStep, } from "./pi-executor.js";
5
+ import { executePiStep, resolvePiBackend, } from "./pi-executor.js";
6
+ import { resolveDagPiExtensions, } from "./pi-extension-resolver.js";
6
7
  import { buildPiWriterToolPolicyContext, createPiWriterCustomTools, } from "./pi-writer-tool-policy.js";
7
8
  import { cleanupPlaywrightCliDefaultSession, createPlaywrightCliTool, PI_COMMAND_CAPABILITY_REGISTRY, resolveCaseIdFromWriteSet, resolveEvidenceDirFromWriteSet, } from "./pi-playwright-cli-tool.js";
8
9
  import { dagCommandPolicyAllows, resolveDagCommandPolicy, } from "../workflows/dag/types.js";
@@ -327,6 +328,36 @@ export async function writePiExecutorArtifacts(artifactsDir, input) {
327
328
  await writeTextArtifactFile(summaryPath, buildPiResultSummaryMarkdown(input));
328
329
  return { promptPath, summaryPath };
329
330
  }
331
+ /**
332
+ * Run-owned per-node Pi extension facts (plan 2026-08-21): requested ids,
333
+ * resolved packages (with absolute entry paths), and degraded/missing ids.
334
+ * Written only for nodes that declare `piExtensions`; never into the repo-level
335
+ * DAG template. Best-effort: artifact failures never fail the node.
336
+ */
337
+ async function writePiExtensionsArtifact(nodeArtifactsDir, requested, resolved, missing) {
338
+ try {
339
+ const runDir = path.dirname(nodeArtifactsDir);
340
+ const nodeId = path.basename(nodeArtifactsDir);
341
+ await writeDagNodeJsonArtifact(runDir, nodeId, "pi-extensions.json", {
342
+ schemaVersion: 1,
343
+ requested: [...requested],
344
+ resolved: resolved.map((entry) => ({
345
+ id: entry.id,
346
+ packageName: entry.packageName,
347
+ version: entry.version,
348
+ path: entry.entryPaths.join(", "),
349
+ })),
350
+ missing: missing.map((entry) => ({
351
+ id: entry.id,
352
+ reason: entry.reason,
353
+ ...(entry.detail ? { detail: entry.detail } : {}),
354
+ })),
355
+ });
356
+ }
357
+ catch {
358
+ // best-effort run fact; node execution continues
359
+ }
360
+ }
330
361
  async function resolveValidatedFrontendBaseUrlFromContext(workspaceRoot, runDir) {
331
362
  try {
332
363
  const capability = JSON.parse(await readFile(path.join(runDir, "preflight-frontend-browser-tool-shell", "frontend-browser-capability.json"), "utf8"));
@@ -550,6 +581,29 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
550
581
  };
551
582
  }
552
583
  }
584
+ // Plan 2026-08-21 D4/D6: resolve the node's explicit Pi extension
585
+ // allowlist against the settings inventory. Degrade on missing packages —
586
+ // never fail the node — and persist requested/resolved/missing facts.
587
+ const piExtensionsResolution = input.task.piExtensions
588
+ ? resolveDagPiExtensions({
589
+ repoRoot: input.cwd,
590
+ ids: input.task.piExtensions,
591
+ })
592
+ : undefined;
593
+ if (piExtensionsResolution) {
594
+ const piBackend = resolvePiBackend();
595
+ const cliDegraded = piBackend === "cli-only";
596
+ await writePiExtensionsArtifact(path.join(meta.runDir, input.task.id), input.task.piExtensions, cliDegraded ? [] : piExtensionsResolution.resolved, cliDegraded
597
+ ? input.task.piExtensions.map((id) => ({
598
+ id,
599
+ reason: "not-installed",
600
+ detail: "cli backend does not load pi extensions (sdk-only v1)",
601
+ }))
602
+ : piExtensionsResolution.missing);
603
+ }
604
+ const piExtensionPaths = piExtensionsResolution && resolvePiBackend() !== "cli-only"
605
+ ? piExtensionsResolution.resolved.flatMap((entry) => entry.entryPaths)
606
+ : undefined;
553
607
  result = await piStepFn({
554
608
  attachedFiles: [],
555
609
  modelConfig: resolveDagPiModelConfig(input.model, input.thinking ? { thinking: input.thinking } : undefined),
@@ -564,6 +618,9 @@ export async function executeDagPiNode(input, meta, piStepFn = executePiStep, wr
564
618
  stallTimeoutMs: input.stallTimeoutMs,
565
619
  abortGraceMs: input.abortGraceMs,
566
620
  ...(writerToolPolicy ? { writerToolPolicy } : {}),
621
+ ...(piExtensionPaths && piExtensionPaths.length > 0
622
+ ? { piExtensionPaths }
623
+ : {}),
567
624
  onActivity: bridgeActivity
568
625
  ? (activity) => {
569
626
  if (activity.kind === "lease" ||
@@ -51,9 +51,50 @@ export function normalizePiSessionEvent(event) {
51
51
  }
52
52
  return unifyToolPayloadFields(event);
53
53
  }
54
+ const TERMINAL_FINISH_REASON_TYPES = new Set([
55
+ 'message_end',
56
+ 'turn_end',
57
+ 'assistant_message',
58
+ ]);
59
+ function readExplicitFinishReason(source) {
60
+ const message = isRecord(source.message) ? source.message : undefined;
61
+ const candidates = [
62
+ source.finish_reason,
63
+ source.finishReason,
64
+ source.stop_reason,
65
+ source.stopReason,
66
+ message?.finish_reason,
67
+ message?.finishReason,
68
+ message?.stop_reason,
69
+ message?.stopReason,
70
+ ];
71
+ for (const candidate of candidates) {
72
+ if (typeof candidate === 'string' && candidate.trim().length > 0) {
73
+ return candidate;
74
+ }
75
+ }
76
+ return undefined;
77
+ }
78
+ function applyFinishReason(original, normalized) {
79
+ const originalType = typeof original.type === 'string' ? original.type : undefined;
80
+ if (!originalType || !TERMINAL_FINISH_REASON_TYPES.has(originalType)) {
81
+ return normalized;
82
+ }
83
+ const existing = readExplicitFinishReason({
84
+ finish_reason: normalized.finish_reason,
85
+ });
86
+ if (existing !== undefined) {
87
+ return normalized;
88
+ }
89
+ const promoted = readExplicitFinishReason(original);
90
+ if (promoted === undefined) {
91
+ return normalized;
92
+ }
93
+ return { ...normalized, finish_reason: promoted };
94
+ }
54
95
  /** Serialize one SDK session event as a single JSONL line. */
55
96
  export function serializeSessionEvent(event, options) {
56
- const normalized = normalizePiSessionEvent(event);
97
+ const normalized = applyFinishReason(event, normalizePiSessionEvent(event));
57
98
  if (typeof normalized.recordedAt !== 'string') {
58
99
  normalized.recordedAt = options?.recordedAt ?? new Date().toISOString();
59
100
  }
@@ -408,6 +408,50 @@ export class BoundedTextPreview {
408
408
  ].join("\n");
409
409
  }
410
410
  }
411
+ /**
412
+ * Normalize provider-specific completion-limit labels to the DAG's canonical
413
+ * `length` signal. Other terminal reasons remain observable without being
414
+ * treated as truncation.
415
+ */
416
+ export function normalizeProviderStopReason(value) {
417
+ if (typeof value !== "string" || value.trim().length === 0)
418
+ return undefined;
419
+ const trimmed = value.trim();
420
+ const compact = trimmed.toLowerCase().replace(/[\s_-]/g, "");
421
+ if ([
422
+ "length",
423
+ "maxtokens",
424
+ "maxoutputtokens",
425
+ "outputlimit",
426
+ "tokenlimit",
427
+ "tokenlimitreached",
428
+ ].includes(compact)) {
429
+ return "length";
430
+ }
431
+ return trimmed;
432
+ }
433
+ /**
434
+ * Extract a terminal stop reason from the provider event shapes used by Pi
435
+ * SDK/CLI bridges. Providers disagree on `stop*` versus `finish*`; keep this
436
+ * boundary centralized so downstream DAG recovery sees one canonical signal.
437
+ */
438
+ export function extractPiEventStopReason(event) {
439
+ const nested = [event.message, event.response, event.completion].filter(isRecord);
440
+ const containers = [...nested, event];
441
+ for (const container of containers) {
442
+ for (const key of [
443
+ "stopReason",
444
+ "stop_reason",
445
+ "finishReason",
446
+ "finish_reason",
447
+ ]) {
448
+ const normalized = normalizeProviderStopReason(container[key]);
449
+ if (normalized)
450
+ return normalized;
451
+ }
452
+ }
453
+ return undefined;
454
+ }
411
455
  export class PiJsonlStreamCollector {
412
456
  lineBuffer = "";
413
457
  discardUntilNewline = false;
@@ -415,6 +459,7 @@ export class PiJsonlStreamCollector {
415
459
  parsedEvents = 0;
416
460
  tokensUsed = 0;
417
461
  outputTooLarge = false;
462
+ stopReason;
418
463
  subagentTotalCalls = 0;
419
464
  subagentFailedCalls = 0;
420
465
  subagentModes = new Set();
@@ -454,6 +499,7 @@ export class PiJsonlStreamCollector {
454
499
  assistantText: this.assistantText.trim(),
455
500
  outputTooLarge: this.outputTooLarge,
456
501
  parsedEvents: this.parsedEvents,
502
+ stopReason: this.stopReason,
457
503
  subagentStats: this.formatSubagentStats(),
458
504
  tokensUsed: this.tokensUsed,
459
505
  };
@@ -472,6 +518,10 @@ export class PiJsonlStreamCollector {
472
518
  }
473
519
  }
474
520
  consumeEvent(event) {
521
+ const stopReason = extractPiEventStopReason(event);
522
+ if (stopReason === "length" || !this.stopReason) {
523
+ this.stopReason = stopReason ?? this.stopReason;
524
+ }
475
525
  const candidate = extractAssistantTextFromEvent(event);
476
526
  if (candidate !== undefined) {
477
527
  if (candidate.length > MAX_ASSISTANT_TEXT_CHARS) {
@@ -865,6 +915,7 @@ async function executeSingleCliAttempt(options, modelConfig, promptFilePath) {
865
915
  stdoutTruncated: stdoutPreview.truncated,
866
916
  stderrTruncated: stderrPreview.truncated,
867
917
  outputTooLarge: collected.outputTooLarge,
918
+ stopReason: collected.stopReason,
868
919
  timedOut,
869
920
  tokensUsed: collected.tokensUsed,
870
921
  subagentStats: collected.subagentStats,
@@ -0,0 +1,233 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ /**
5
+ * Pi extension allowlist for backend implementation DAGs (plan D2/D3).
6
+ * DAG specs carry these frozen short ids; the executor resolves them against
7
+ * the Pi agent settings package inventory at run time.
8
+ */
9
+ export const DAG_PI_EXTENSION_IDS = ["pi-codegraph", "pi-lens"];
10
+ /**
11
+ * Alias table: one DAG short id may resolve to multiple published package
12
+ * names. The first settings spec whose de-`npm:`-prefixed package name is in
13
+ * the alias set wins (project settings override user settings).
14
+ */
15
+ export const DAG_PI_EXTENSION_PACKAGE_ALIASES = {
16
+ "pi-codegraph": [
17
+ "@estebanforge/pi-codegraph-enhanced",
18
+ "@vndv/pi-codegraph",
19
+ "pi-codegraph",
20
+ ],
21
+ "pi-lens": ["pi-lens"],
22
+ };
23
+ /**
24
+ * Extension tools that must never be activated as extras on any node (D5):
25
+ * they mutate files without the writer path policy. Writer nodes keep only
26
+ * policy-wrapped edit/write; read nodes keep only navigation tools.
27
+ */
28
+ export const DAG_PI_EXTENSION_WRITE_TOOL_DENY = new Set([
29
+ "ast_grep_replace",
30
+ ]);
31
+ /** Builtin tool names extensions must never replace (D5). */
32
+ export const DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS = new Set(["read", "edit", "write", "bash", "grep", "find", "ls"]);
33
+ /** Deny surface extensions must never activate (D5). */
34
+ export const DAG_PI_EXTENSION_DENIED_TOOL_IDS = new Set([
35
+ "apply_patch",
36
+ "apply-patch",
37
+ "full-tools",
38
+ "full_tools",
39
+ "coding-chat",
40
+ "coding_chat",
41
+ "shell",
42
+ ]);
43
+ export function isDagPiExtensionId(value) {
44
+ return DAG_PI_EXTENSION_IDS.includes(value);
45
+ }
46
+ function getPiAgentDir() {
47
+ const envDir = process.env.PI_CODING_AGENT_DIR;
48
+ if (envDir)
49
+ return envDir;
50
+ return path.join(os.homedir(), ".pi", "agent");
51
+ }
52
+ function readSettingsPackages(settingsPath) {
53
+ try {
54
+ if (!existsSync(settingsPath))
55
+ return undefined;
56
+ const parsed = JSON.parse(readFileSync(settingsPath, "utf8"));
57
+ if (!Array.isArray(parsed.packages))
58
+ return undefined;
59
+ return parsed.packages.filter((spec) => typeof spec === "string");
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** Strip an `npm:` prefix from a settings package spec, e.g. `npm:pi-lens`. */
66
+ export function packageNameFromSpec(spec) {
67
+ return spec.startsWith("npm:") ? spec.slice(4) : spec;
68
+ }
69
+ function loadPackageInventories(repoRoot) {
70
+ const agentDir = getPiAgentDir();
71
+ const userSpecs = readSettingsPackages(path.join(agentDir, "settings.json"));
72
+ const projectSpecs = readSettingsPackages(path.join(repoRoot, ".pi", "settings.json"));
73
+ const inventories = [];
74
+ if (userSpecs) {
75
+ inventories.push({
76
+ specs: userSpecs,
77
+ nodeModulesRoot: path.join(agentDir, "npm", "node_modules"),
78
+ });
79
+ }
80
+ if (projectSpecs) {
81
+ inventories.push({
82
+ specs: projectSpecs,
83
+ nodeModulesRoot: path.join(repoRoot, "node_modules"),
84
+ });
85
+ }
86
+ return inventories;
87
+ }
88
+ const EXTENSION_FILE_RE = /\.(?:ts|js|mjs|cjs)$/;
89
+ /**
90
+ * Resolve a package's `pi.extensions` entries to absolute entry paths.
91
+ * - A file entry maps to itself.
92
+ * - A directory entry expands to every extension file directly inside it
93
+ * (e.g. `@estebanforge/pi-codegraph-enhanced` ships `./extensions` holding
94
+ * `codegraph.ts`).
95
+ * No `pi.extensions` declaration, or a declaration whose paths do not exist,
96
+ * yields no entries (caller records `no-entry`) — no heuristic scanning
97
+ * outside declared paths.
98
+ */
99
+ export function resolvePackageEntryPaths(packageDir) {
100
+ let manifest;
101
+ try {
102
+ manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
103
+ }
104
+ catch {
105
+ return { entryPaths: [], reason: "no-entry" };
106
+ }
107
+ const declared = manifest.pi?.extensions;
108
+ if (!Array.isArray(declared)) {
109
+ return { entryPaths: [], reason: "no-entry" };
110
+ }
111
+ const entryPaths = [];
112
+ for (const entry of declared) {
113
+ if (typeof entry !== "string" || entry.length === 0)
114
+ continue;
115
+ const resolved = path.resolve(packageDir, entry);
116
+ if (!existsSync(resolved))
117
+ continue;
118
+ let isDirectory = false;
119
+ try {
120
+ isDirectory = statSync(resolved).isDirectory();
121
+ }
122
+ catch {
123
+ continue;
124
+ }
125
+ if (!isDirectory) {
126
+ entryPaths.push(resolved);
127
+ continue;
128
+ }
129
+ let names;
130
+ try {
131
+ names = readdirSync(resolved);
132
+ }
133
+ catch {
134
+ continue;
135
+ }
136
+ for (const name of names.sort()) {
137
+ if (!EXTENSION_FILE_RE.test(name))
138
+ continue;
139
+ entryPaths.push(path.join(resolved, name));
140
+ }
141
+ }
142
+ if (entryPaths.length === 0) {
143
+ return { entryPaths: [], reason: "no-entry" };
144
+ }
145
+ return { entryPaths };
146
+ }
147
+ /**
148
+ * Resolve frozen DAG short ids against the Pi settings package inventory
149
+ * (user `~/.pi/agent/settings.json`, then project `.pi/settings.json` — a
150
+ * later match on the same alias wins). Pure synchronous read; never throws.
151
+ */
152
+ export function resolveDagPiExtensions(input) {
153
+ const inventories = loadPackageInventories(input.repoRoot);
154
+ const resolved = [];
155
+ const missing = [];
156
+ for (const id of input.ids) {
157
+ if (!isDagPiExtensionId(id)) {
158
+ missing.push({
159
+ id,
160
+ reason: "package-unreadable",
161
+ detail: `unknown pi extension id: ${id}`,
162
+ });
163
+ continue;
164
+ }
165
+ const aliases = DAG_PI_EXTENSION_PACKAGE_ALIASES[id];
166
+ let matched;
167
+ // Later inventories (project) override earlier (user).
168
+ for (const inventory of inventories) {
169
+ for (const spec of inventory.specs) {
170
+ const packageName = packageNameFromSpec(spec);
171
+ if (!aliases.includes(packageName))
172
+ continue;
173
+ const packageDir = path.join(inventory.nodeModulesRoot, ...packageName.split("/"));
174
+ if (!existsSync(packageDir))
175
+ continue;
176
+ try {
177
+ const manifest = JSON.parse(readFileSync(path.join(packageDir, "package.json"), "utf8"));
178
+ if (typeof manifest.version !== "string")
179
+ continue;
180
+ matched = { packageDir, packageName, version: manifest.version };
181
+ }
182
+ catch {
183
+ // unreadable manifest: skip this candidate
184
+ }
185
+ }
186
+ }
187
+ if (!matched) {
188
+ missing.push({ id, reason: "not-installed" });
189
+ continue;
190
+ }
191
+ const entries = resolvePackageEntryPaths(matched.packageDir);
192
+ if (entries.entryPaths.length === 0) {
193
+ missing.push({
194
+ id,
195
+ reason: entries.reason === "no-entry" ? "no-entry" : "package-unreadable",
196
+ });
197
+ continue;
198
+ }
199
+ resolved.push({
200
+ id,
201
+ packageName: matched.packageName,
202
+ version: matched.version,
203
+ entryPaths: entries.entryPaths,
204
+ });
205
+ }
206
+ return { resolved, missing };
207
+ }
208
+ /**
209
+ * Compute extension tool names that may join a node's active tool allowlist
210
+ * after session creation (D5): new tool names registered by the loaded
211
+ * extensions, minus protected builtins, the deny surface, and write-capable
212
+ * extension tools (both buckets — writer writes stay policy-wrapped only).
213
+ */
214
+ export function computeExtensionToolAllowlistExtras(sessionTools, baseline) {
215
+ const baselineLower = new Set(baseline.map((name) => name.toLowerCase()));
216
+ const extras = [];
217
+ for (const tool of sessionTools) {
218
+ const name = tool && typeof tool.name === "string" ? tool.name : undefined;
219
+ if (!name)
220
+ continue;
221
+ const lower = name.toLowerCase();
222
+ if (baselineLower.has(lower))
223
+ continue;
224
+ if (DAG_PI_EXTENSION_PROTECTED_BUILTIN_TOOLS.has(lower))
225
+ continue;
226
+ if (DAG_PI_EXTENSION_DENIED_TOOL_IDS.has(lower))
227
+ continue;
228
+ if (DAG_PI_EXTENSION_WRITE_TOOL_DENY.has(lower))
229
+ continue;
230
+ extras.push(name);
231
+ }
232
+ return extras;
233
+ }