@givolt/i3-composer-agent 2.3.1 → 2.3.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 (173) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -8,12 +8,70 @@
8
8
  默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
9
  <!-- i3:zh-guide:end -->
10
10
 
11
- create 读取业务上下文、检测默认专业增强模式、展示四类模板的可用状态并记录选择,然后初始化和适配原型。企业后台当前可用;AI 工作台、数字孪生大屏和移动端尚待建设,不能自动替换为其他模板。兼容旧版三套模板仅供已有项目继续使用。脚本返回 needs-design-provider 时不得冒充创建成功;允许先完成需求和模板比较,等待安装或用户选择 template。已有原型的 expertMode 作为迭代默认值。
11
+ ## 公共前置与副作用
12
12
 
13
- revise 必须指定未使用的新版本;改变全局导航、token、组件行为或业务状态时解释影响并形成新的评审候选。inspect 可写检查报告但不修改页面或冻结基线。confirm-baseline 必须有用户对具体内容的明确认可和完整必需证据。
13
+ 主会话通过自然语言、命令、元技能或直接 Skill 使用同一顺序:识别操作及限制,读取实际文件和清单,恢复固定输入/版本/交接,确认实际工具,确定单一修改负责人,再进入对应运行层。入口仅发现工作 Skill,不强制分派角色,不绕回命令或元技能。按操作需要复用 frontend、框架官方、UIUX 和 browser 技能,各工程技能保留原有方法。
14
14
 
15
- verification 使用 not-run、partial、passed、failed、blocked;review 使用 pending-review、changes-requested、approved。expert 使用 pending、used、skipped、missing、unusable、no-match。它们分别表示检查、用户认可及专业能力使用;不相互推断。
15
+ | 操作 | 允许范围 |
16
+ | --- | --- |
17
+ | analyze | 只读文件和已核对的只读查询,分析留在会话 |
18
+ | create | 在授权项目内准备未存在的用户模板副本、同步索引、创建新实例并实现 |
19
+ | revise | 基于实际文件及固定合同形成新候选,保留手改和旧基线 |
20
+ | inspect | 检查当前实例;用户明确只读时不保存报告或运行状态 |
21
+ | confirm-baseline | 用户认可具体版本且验收匹配后,保存不可覆盖的快照 |
16
22
 
17
- 原型可从草拟需求开始。需求确认、原型认可、正式规格批准和生产授权各自保存。返回 scope、revision、mode、verification、review、openQuestions 和下一项必要动作,避免笼统地报告全部完成。
23
+ 明确“不改代码/不写文件”优先于默认 create。analyze 不创建目录、安装升级、写 provider 结果、初始化运行记录或改变状态;约束包括检查器、官方 Skill、UIUX 和浏览器助手的下游行为。用户要求保存分析文档时只写指定输出。只读期间不得为了记录执行模式而落盘。
24
+
25
+ analyze 返回需求/通用能力覆盖、模板与框架选型、验收计划和未决项,信息足够后停止探索并回传。已有来源的固定 profile/component-map/acceptance 可支持选型,不等于逐组件逐 Prop 实施核验;只对影响选型的具体疑点定向查本地 API。完整目标版本包/源码验证留 create,不遍历 starter、node_modules、整套 CLI 帮助或组件清单来证明分析;必要定向读取仍可进行。未知点保留 openItems,不把 UIUX 准入、逐条采纳、构建或浏览器当分析前置,不把计划写成已验证结果。
26
+
27
+ 能力覆盖包含绑定参数,不能只按 Table/pagination 等名称匹配。PRD 要求可调页大小而当前绑定 `fixedPageSize: 10` 时,报告部分覆盖、明确在可写阶段需显式适配;固定值不是默认值,不能借已有通用分页能力宣称全部满足。分析完整交付可 status=complete,但覆盖检查与 openItems 如实保留冲突;仅分析未完成才 partial/blocked。只读期间不改需求、绑定或固定输入,后续计划不记为已实现或已验收。
28
+
29
+ 先按实际 `--host`、`--scope` 只读检测宿主基址 `tools/i3-prototype/` 下的工具,保留旧项目 `.prototype-tools` 检测;或使用宿主已经明确给出的绝对入口,核对固定版本后直接调用;不要反复扫描 node_modules。analyze 不运行 npm exec/npx 或任何会自动安装/补缓存的命令;缺工具报告未决项,改用现有只读资料。可写模式同样使用准备好的本地固定 CLI,只有工具准备另有授权时才在明确工具目录安装;该授权不包含业务文件、已安装 SDK、宿主设置或未选择的全局目录。项目构建依赖的 --install 是另一项写操作,不能借用查询工具授权。
30
+
31
+ create/revise 的项目写入范围不包含用户级长期记忆或宿主 auto-memory;未经用户明确要求,不创建或更新这些文件。纯文件续接依靠实例合同与交接;测试启动时核对宿主记忆开关和工具写入范围,不将 no-session-persistence 视为禁止 auto-memory,不更改全局偏好。
32
+
33
+ 执行阶段保持一个源码/合同修改负责人;另有授权的协作按实际工具和执行记录留证,不冒认其他执行者的工作。连续性来自文件。写入前核对预期 revision/candidateDigest,遇到并发变化停止该次写入并重读,不覆盖较新候选。
34
+
35
+ ## 原生组件 v2
36
+
37
+ 已有实例优先用 native-pilot.cjs state 一次恢复合同、身份、设计/构建/QA及精确 commands,不遍历依赖或反复读控制器实现。真实查询与人工设计取舍完成后,本轮 CodeBuddy 原生流程依次为 build→宿主实际加载 i3-browser-testing-with-devtools Skill→Chrome DevTools MCP 按能力执行并取得实际观测→新业务场景实测及 record-run(旧设备合同另用 browser 专项回归)→state→finalize→独立最终回复。Skill 的 DOM/计算样式、截图、console、network、a11y 与 performance 观测不能只列计划或挂名;缺任一必需工具/执行时整链不 complete。analyze 只提出这份验收计划,不启动检查。
38
+
39
+ browser 是历史设备合同的 Playwright 专项工具,自行管理服务,不替代上述 Skill 或 MCP。实际 DevTools 检查后用 record-devtools-review --report 登记独立 qa/devtools-review.json;新 create 的固定 acceptance.browserReview 要求当前报告,state 列缺项,finalize 拒绝缺失/过期/证据变化。脚本只核结构、文件和当前身份,不证明真实调用,仍输出 invocationVerified=false。主会话另核原始宿主 stream、实际观测及同一 candidate/build,交付前核对当前 state/delivery,再以自然语言给出结果和证据位置。详细证据合同见 interaction-acceptance。旧 v1 和无 browserReview 的已有 v2 保留原验收,新要求仅进入新实例或显式接受变更的输入;其他宿主的等效工具结果单列,不冒充这条指定链。
40
+
41
+ create 使用完整 enterprise-admin 内容模板、实际 scope.pages/regions 和 vue-antdv-next 框架,使用 native-pilot.cjs。用户 HTML 工作副本升级不覆盖。创建前检查标识、绑定和索引一致,固定本次范围、模板、能力、框架及必需场景。PRD 可简短,但明确/假设/待确认/范围外须有来源。
42
+
43
+ 目标目录须留给原子 create:provider 先 probe,初始化后才在实例 qa 下保存查询和设计证据。初始化前需要保存的官方/UIUX 查询使用独立准备目录,不能预先创建 prototypes/<id>/qa。没有 prototype.json 的已有目录先核对来源,不能调用 revise 冒充实例。只有确认整个目录均由本次准备操作产生,才可整体移走,确保目标不存在后重试;只移文件留下空目录仍会失败。含用户或未知文件时保留并报告冲突。
44
+
45
+ required 不可任意 N/A;检查器按已声明业务区域重算必需集合,实际页面与区域的对应由前端/浏览器复核。缺失语义、参数或范围仍待处理,不能为通过删能力。新流程的分页与四主题默认及 scope 格式见 [content-template.md](content-template.md)。初始化状态为 needs-adaptation,主会话继续实现实际业务;完成后标 adapted,才进入当前候选验收。旧设备列表的固定容量、可选主题和验收数量仍按旧 inputs,不追溯追加要求。
46
+
47
+ revise 使用 `native-pilot.cjs revise <root> --revision <new> --expected-revision <current> --expected-digest <currentDigest>`,保留实际源码而非重复制 starter。模板或框架升级是显式输入变更,旧实例继续检查固定输入。手改后旧证据失效;新交互补映射,删除必需功能产生缺口。
48
+
49
+ 确需更改固定输入时,由当前修改负责人先整理 inputs 中完整新快照、能力/范围/场景及 requirements-map,再使用同一 revise 命令加 `--accept-inputs`。该选项在锁和预期版本保护下完整校验新合同,成功才接受新 inputDigest 和版本并清除旧验证状态;失败不推进版本。它不自动下载或套用新模板、框架,不补造需求映射。普通 revise 遇到输入变化会拒绝,避免新版本带着旧摘要。
50
+
51
+ inspect、manifest、build 按 v2 合同执行;不得让 legacy 写接口接管。首次可写构建使用 `build <root> --install` 安装固定锁依赖,后续 build 复用依赖。preview 输出 URL 并持续服务,--build-id 可选择固定构建。浏览器证据导入后 `inspect <root> --acceptance` 检查覆盖与身份。
52
+
53
+ 持续写任务可 `claim <root> --owner <owner> --expected-revision <revision> --expected-digest <digest>`;revise 传同一 owner,编辑结束 `release <root> --owner <owner>` 后再验收/确认。临时修改由脚本 claim/release。锁不进入候选身份,丢失 Agent 不自动清除别人仍持有的修改权。
54
+
55
+ compare-baseline 只比较当前候选与已保存基线;confirm-baseline --confirmation 接收具体版本的人类确认,再核对当前验收并保存不可覆盖快照。证据不齐保留待确认候选,不手工构造批准状态。
56
+
57
+ ## Legacy HTML
58
+
59
+ legacy create 读取业务上下文、检测专业模式、展示四类模板可用状态并记录选择,再初始化和适配。企业后台可用;AI 工作台、数字孪生大屏和移动端 planned,不能替换生成。旧三套模板供历史实例。needs-design-provider 不等于成功;可先比较需求和模板,按已有授权修复或等待明确选择 template。已有 expertMode 作为迭代默认。
60
+
61
+ legacy revise 指定未使用版本,改变导航、token、组件行为或业务状态时解释影响。inspect 写报告服从本次范围,不改页面或冻结。确认须有具体版本的用户认可和完整必需证据。旧实例不补写 v2 metadata,不改原摘要及已冻结历史。
62
+
63
+ ## 状态与交付
64
+
65
+ verification 使用 not-run、partial、passed、failed、blocked;review 使用 pending-review、changes-requested、approved。expert 使用 pending、used、reviewed、skipped、missing、unusable、no-match。它们分别表示检查、用户认可及专业能力使用;不相互推断。
66
+
67
+ 专业查询成功、建议处理、采用和效果验证独立保存。有依据的零采用可完成复核,冻结不自动改 used。template 明确跳过专业查询但不降低功能验收。provider 不可用报告具体原因,不静默切模式。
68
+
69
+ 原型可从草拟需求开始。需求确认、原型认可、正式规格批准和生产授权分别保存。返回 operation、scope、revision、executionMode、mode、verification、review、openQuestions 和下一项必要动作;说明结构、真实宿主、浏览器和人类确认各自结果。
70
+
71
+ 各入口在工具之后用自然语言交付实际范围、身份、证据、缺项和下一步,不强制父子 JSON、标记或结构校验。validate-result.cjs 可作特定维护工具,不是普通原型入口前置。业务、当前 state/delivery、真实 QA 分别核对,不能由结构结果推断。analyze 留在会话、不写收据;参数冲突以部分覆盖和未决项说明,不把分析完成等同覆盖通过。
18
72
 
19
73
  企业后台默认 purpose=business-prototype,完整参考副本的 business.status=needs-adaptation;必须完成实际业务适配才能验收。用户明确评审 EASEL 模板本身时,使用 purpose=template-review,并将 reference-demo 范围写进交接。模板认可不等于任何新业务的需求或原型认可。
74
+
75
+ ## 项目共享与宿主缓存
76
+
77
+ 默认业务写入集中于 prototypes:准备区 .preparation/<id>、共享 index.json 和本次实例。安装与分析不提前生成这些资料;原型流程不在根目录散落准备目录、工具或浏览器 CLI 输出。工具与缓存使用实际宿主基址,global 缓存按规范化项目身份隔离;正式需求、交接与 QA 归实例。旧路径不因升级自动迁移。目录、scope.moduleIds 与 appliesTo 的唯一来源、冲突及迁移规则见 [project-layout.md](project-layout.md)。
@@ -8,7 +8,7 @@ module.exports = {
8
8
  kind: 'easel-html', identityFields: ['purpose', 'business', 'runtime'],
9
9
  requiredFiles: ['app/app.js', 'app/enterprise-bootstrap.mjs', 'app/routes.json', 'app/runtime/dom.js', 'app/styles/tokens.css', 'design-system/source.json'],
10
10
  excludedBrowserPrefixes: ['app/tools/', 'app/tests/', 'app/qa/'],
11
- protectedFiles: ['app/tokens.css', 'app/styles.css', 'app/index.html', 'app/domain.mjs', 'requirements-map.json', 'app/app.js', 'app/routes.json', 'app/config.json', 'app/enterprise-bootstrap.mjs', 'app/usability.css'],
11
+ protectedFiles: ['app/tokens.css', 'app/styles.css', 'app/index.html', 'app/domain.mjs', 'requirements-map.json', 'app/app.js', 'app/routes.json', 'app/config.json', 'app/enterprise-bootstrap.mjs', 'app/usability.css', 'app/offline.js'],
12
12
  protectedDirectories: ['design-system/', 'app/styles/', 'app/runtime/', 'app/src/'],
13
13
  },
14
14
  fingerprintInputs(context) {
@@ -0,0 +1,72 @@
1
+ 'use strict';
2
+ const { fs, path, crypto, hash, assert, json, id, inside, write } = require('./native-files.cjs');
3
+ const { spawnSync } = require('node:child_process');
4
+ const { resolveLocations } = require('./prototype-locations.cjs');
5
+ function location(options) {
6
+ id(options.runId);
7
+ const context = resolveLocations(options);
8
+ return { context, directory: inside(context.cacheDirectory, `playwright/${options.runId}`) };
9
+ }
10
+ function prepare(options = {}) {
11
+ const { context, directory } = location(options), configFile = inside(directory, 'cli.config.json');
12
+ const config = { outputDir: directory, outputMode: 'file' };
13
+ if (fs.existsSync(configFile)) assert.deepEqual(json(configFile), config, 'Existing browser output config differs; preserve it and use another run ID');
14
+ else write(directory, 'cli.config.json', config);
15
+ return { status: 'prepared', directory, configFile, host: context.host, scope: context.scope, projectKey: context.projectKey,
16
+ environment: { PLAYWRIGHT_MCP_OUTPUT_DIR: directory, PLAYWRIGHT_MCP_CONFIG: configFile } };
17
+ }
18
+ function runCli(options = {}) {
19
+ assert.ok(typeof options.cli === 'string' && path.isAbsolute(options.cli), '--cli requires an existing absolute Playwright CLI Node entry');
20
+ const entry = fs.realpathSync(options.cli); assert.ok(fs.statSync(entry).isFile(), 'CLI entry must be a file');
21
+ assert.ok(Array.isArray(options.command) && options.command.length && options.command.every(value => typeof value === 'string'), 'Provide CLI arguments after --');
22
+ assert.ok(!options.command.some(value => value === '--config' || value.startsWith('--config=')), 'Output config is managed by this wrapper');
23
+ const command = options.command.find(value => !value.startsWith('-'));
24
+ assert.ok(!['install', 'install-browser', 'close-all', 'kill-all'].includes(command), 'Installation and all-session commands are not prototype browser operations');
25
+ const output = prepare(options);
26
+ const result = spawnSync(process.execPath, [entry, '--config', output.configFile, ...options.command], {
27
+ cwd: output.directory, env: { ...process.env, ...(options.env || {}), ...output.environment }, encoding: 'utf8', timeout: 60000, maxBuffer: 16 * 1024 * 1024 });
28
+ const call = `cli-${crypto.randomUUID()}`;
29
+ write(output.directory, `${call}.stdout.log`, result.stdout || ''); write(output.directory, `${call}.stderr.log`, result.stderr || result.error?.message || '');
30
+ return { ...output, status: result.status === 0 && !result.error ? 'executed' : 'failed', exitCode: result.status,
31
+ stdout: result.stdout || '', stderr: result.stderr || result.error?.message || '', verification: 'not-assessed' };
32
+ }
33
+ function archive(options = {}) {
34
+ const { context, directory: source } = location(options);
35
+ assert.ok(fs.existsSync(source), 'Browser cache does not exist');
36
+ const files = Array.isArray(options.files) ? options.files : typeof options.files === 'string' ? options.files.split(',') : [];
37
+ assert.ok(files.length && new Set(files).size === files.length, 'Explicit unique output files are required');
38
+ const selected = files.map(file => {
39
+ assert.ok(!['archive.json', 'cli.config.json'].includes(file), 'Reserved browser output filename');
40
+ const from = inside(source, file); assert.ok(fs.statSync(from).isFile(), 'Only regular output files can be archived');
41
+ return { file, from, digest: hash(fs.readFileSync(from)) };
42
+ });
43
+ const prototype = require('./project-registry.cjs').paths(context.project, options.id).directory;
44
+ assert.ok(prototype && fs.existsSync(prototype), 'Prototype must already exist');
45
+ const ownerFile = inside(prototype, '.prototype-write.lock');
46
+ if (fs.existsSync(ownerFile)) assert.equal(json(ownerFile).owner, options.owner, 'Prototype has another active writer');
47
+ const native = require('./native-pilot-lib.cjs'), candidate = native.manifest(prototype);
48
+ const parent = inside(prototype, `qa/runs/${options.runId}`), destination = inside(parent, 'tool-output');
49
+ assert.ok(!fs.existsSync(destination), 'Archived output already exists; preserve it');
50
+ fs.mkdirSync(parent, { recursive: true });
51
+ const lockFile = inside(parent, '.tool-output.lock'), lock = fs.openSync(lockFile, 'wx');
52
+ let stage;
53
+ try {
54
+ stage = fs.mkdtempSync(path.join(parent, '.archive-'));
55
+ for (const item of selected) {
56
+ const target = inside(stage, item.file); fs.mkdirSync(path.dirname(target), { recursive: true });
57
+ fs.copyFileSync(item.from, target, fs.constants.COPYFILE_EXCL);
58
+ assert.equal(hash(fs.readFileSync(target)), item.digest, 'Output changed while archiving');
59
+ assert.equal(hash(fs.readFileSync(item.from)), item.digest, 'Source output changed while archiving');
60
+ }
61
+ write(stage, 'archive.json', { schemaVersion: 1, host: context.host, scope: context.scope, projectKey: context.projectKey, runId: options.runId,
62
+ candidateDigest: candidate.digest, revision: candidate.revision, files: Object.fromEntries(selected.map(item => [item.file, item.digest])), verification: 'not-assessed' });
63
+ assert.equal(native.manifest(prototype).digest, candidate.digest, 'Candidate changed while archiving');
64
+ assert.ok(!fs.existsSync(destination), 'Archived output appeared while copying');
65
+ fs.renameSync(stage, destination);
66
+ return { status: 'archived', directory: destination, files, verification: 'not-assessed' };
67
+ } finally {
68
+ if (stage && fs.existsSync(stage)) fs.rmSync(stage, { recursive: true, force: true });
69
+ fs.closeSync(lock); fs.unlinkSync(lockFile);
70
+ }
71
+ }
72
+ module.exports = { prepare, runCli, archive };
@@ -0,0 +1,75 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
3
+ const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs');
4
+ const FILES=['execution-policy.cjs','execution-evidence.cjs','codebuddy-ide-source.cjs','execution-gates.cjs','execution-assertions.cjs','execution-runtime.cjs','codebuddy-context.cjs','codebuddy-registration.cjs','codebuddy-hook.cjs','prototype-hooks.cjs','hosts/codebuddy.cjs','native-pilot.cjs','native-pilot-lib.cjs','native-delivery.cjs','native-devtools.cjs','native-files.cjs','native-build.cjs'];
5
+ const read=file=>R.fileIO.read(file);
6
+ function scriptsForEntry(scriptPath) {
7
+ if(path.basename(scriptPath)!=='i3-prototype.cjs')return path.dirname(path.dirname(scriptPath));
8
+ const root=path.resolve(path.dirname(scriptPath),'..');
9
+ const candidates=[root,path.dirname(root)].flatMap(directory=>['i3-ui-prototype',['ui','prototype'].join('-')].map(id=>path.join(directory,'skills',id,'scripts')));
10
+ const scripts=candidates.find(dir=>fs.existsSync(path.join(dir,'hosts/codebuddy.cjs')));assert.ok(scripts,'Unified Hook entry has no prototype Skill');return path.resolve(scripts);
11
+ }
12
+ function bundle(scriptPath) {
13
+ const scripts=scriptsForEntry(scriptPath);
14
+ const files=FILES.map(file=>[file,E.sha(fs.readFileSync(path.join(scripts,file)))]);
15
+ files.push(['profile',E.sha(fs.readFileSync(path.join(scripts,'../assets/hosts/codebuddy/profile.json')))]);
16
+ if(path.basename(scriptPath)==='i3-prototype.cjs') {
17
+ files.push(['unified-entry',E.sha(fs.readFileSync(scriptPath))]);
18
+ files.push(['unified-manager',E.sha(fs.readFileSync(path.join(path.dirname(scriptPath),'manage.cjs')))]);
19
+ }
20
+ return E.sha(JSON.stringify(files));
21
+ }
22
+ function registration(base) {
23
+ const raw=read(path.join(base,'state/i3-prototype/installation.json'));if(!raw)return null;
24
+ const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.equal(record.schemaVersion,1,'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');
25
+ const settingsText=read(path.join(base,'settings.json')),settings=R.config(settingsText);
26
+ for(const event of R.EVENTS)assert.equal((settings.hooks?.[event]||[]).filter(group=>JSON.stringify(group)===JSON.stringify(record.groups[event])).length,1,'Registered Hook was removed, duplicated, or edited');
27
+ assert.equal(bundle(record.scriptPath),record.scriptDigest,'Audit adapter bytes changed; refresh owned registration');
28
+ assert.ok(fs.existsSync(record.nodePath),'Registered Node executable is missing');
29
+ return {...record,settingsDigest:E.sha(settingsText),registrationDigest:E.sha(JSON.stringify(record))};
30
+ }
31
+ function globalBase({home=os.homedir(),env=process.env}={}) {
32
+ const configured=env.CODEBUDDY_CONFIG_DIR?.trim();
33
+ if(!configured)return path.join(home,'.codebuddy');
34
+ assert.ok(path.isAbsolute(configured)&&!/[\x00\r\n]/.test(configured),'CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
35
+ return path.normalize(configured);
36
+ }
37
+ function select({project,product,home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
38
+ const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({home,env});
39
+ const projectText=readText(path.join(projectBase,'settings.json')),localText=readText(path.join(projectBase,'settings.local.json'));
40
+ if(localText){const local=R.config(localText);assert.ok(!local.hooks&&!local.disableAllHooks&&!local.allowManagedHooksOnly,'Local Hook overrides are not supported by this profile');}
41
+ const projectSettings=R.config(projectText);assert.ok(!projectSettings.disableAllHooks&&!projectSettings.allowManagedHooksOnly,'Project Hook policy prevents automatic auditing');
42
+ const globalText=readText(path.join(userBase,'settings.json')),globalSettings=R.config(globalText);assert.ok(!globalSettings.disableAllHooks&&!globalSettings.allowManagedHooksOnly,'Global Hook policy prevents automatic auditing');
43
+ // IDE documents project settings as an override. Do not silently fall back through a shadowing file.
44
+ const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
45
+ for(const base of [...new Set(bases)]) {
46
+ const record=loadRegistration(base);if(!record)continue;
47
+ assert.equal(record.product,product,'Conflicting CodeBuddy product profiles');return record;
48
+ }
49
+ return null;
50
+ }
51
+ function projectKey(project){return `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project'}-${E.sha(project).slice(0,16)}`;}
52
+ function sessionDirectory(record,project,sessionId) {
53
+ assert.ok(typeof sessionId==='string'&&sessionId,'Native session ID required');
54
+ return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(sessionId).slice(0,24));
55
+ }
56
+ function source(payload,record,project) {
57
+ assert.ok(typeof payload.transcript_path==='string'&&typeof payload.session_id==='string','Native transcript and session fields required');
58
+ const roots=record.transcriptRoots||[],mode=record.transcriptMode||(roots.length?'manual':'auto');
59
+ const value=E.readSource(payload.transcript_path,roots,{mode,project});
60
+ assert.ok(value.prompts.length,'Original user task is absent from native transcript');
61
+ assert.ok(value.calls.every(call=>call.sessionId===payload.session_id),'Native session identity missing or mismatched');
62
+ assert.ok(value.prompts.every(prompt=>!prompt.sessionId||prompt.sessionId===payload.session_id),'Native prompt session differs');
63
+ return value;
64
+ }
65
+ function readOnly(prompt) {
66
+ if(/不(?:要)?(?:改代码|修改(?:任何)?代码|实施|生成原型)|\b(?:analysis only|do not (?:edit|modify|implement))\b/i.test(prompt))return true;
67
+ if(/(?:只|仅).{0,6}(?:分析|讨论|梳理|检查|解释)/.test(prompt))return true;
68
+ if(/先.{0,4}(?:分析|讨论|梳理|检查|解释)/.test(prompt)&&!/(?:然后|再|后).{0,8}(?:实现|实施|修改|继续)/.test(prompt))return true;
69
+ return /^\s*(?:请|先)?(?:暂停|停止|取消)(?:[,。!\s]|(?:这|当前|实施|工作|修改|生成))|^\s*(?:stop|cancel|pause)\b/i.test(prompt);
70
+ }
71
+ function prefix(file,bytes,digest) {
72
+ assert.ok(Number.isSafeInteger(bytes)&&bytes>0&&bytes<=64*1024*1024,'Invalid source cursor');
73
+ const raw=E.cursorBytes(file);assert.ok(raw.length>=bytes,'Native transcript was truncated');assert.equal(E.sha(raw.subarray(0,bytes)),digest,'Native transcript prefix changed');
74
+ }
75
+ module.exports={read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
@@ -0,0 +1,157 @@
1
+ 'use strict';
2
+ const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict');
3
+ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs'),G=require('./execution-gates.cjs');
4
+ const X=require('./execution-runtime.cjs');
5
+ function tokens(command) {
6
+ if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
7
+ const values=[];let value='',quote=null,started=false;
8
+ for(let i=0;i<command.length;i++) {const c=command[i];if(quote){if(c===quote){quote=null;started=true;}else if(c==='\\'&&quote==='"'&&i+1<command.length)value+=command[++i];else value+=c;}
9
+ else if(c==='"'||c==="'"){quote=c;started=true;}else if(c==='\\'&&i+1<command.length){value+=command[++i];started=true;}else if(/\s/.test(c)){if(started){values.push(value);value='';started=false;}}else if(/[;&|<>]/.test(c))return null;else {value+=c;started=true;}}
10
+ if(quote)return null;if(started)values.push(value);return values;
11
+ }
12
+ function nativeCommand(tool,input,cwd) {
13
+ if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
14
+ const argv=tokens(input?.command);if(!argv)return null;const index=argv.findIndex(arg=>path.basename(arg)==='native-pilot.cjs');if(index<1)return null;
15
+ const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
16
+ return {operation,root,argv};
17
+ }
18
+ function eventCall(payload,source) {
19
+ const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
20
+ const partial=(before,after)=>typeof before==='string'?typeof after==='string'&&after.startsWith(before):Array.isArray(before)?Array.isArray(after)&&before.length<=after.length&&before.every((value,i)=>partial(value,after[i])):before&&typeof before==='object'?after&&typeof after==='object'&&Object.keys(before).every(key=>Object.hasOwn(after,key)&&partial(before[key],after[key])):before===after;
21
+ const matches=source.calls.filter(call=>{
22
+ if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||payload.tool_use_id&&call.toolCallId!==payload.tool_use_id)return false;
23
+ if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
24
+ if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
25
+ // IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
26
+ // actual callback with one pending native call in the exact generation. The
27
+ // pending call remains unsuccessful until a native result is persisted.
28
+ return source.format==='codebuddy-ide-index-v1'&&payload.client==='CodeBuddyIDE'&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
29
+ });
30
+ const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
31
+ assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
32
+ }
33
+ function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
34
+ const toolEvent=['PreToolUse','PostToolUse'].includes(payload.hook_event_name);
35
+ // IDE can persist the current call only AFTER returning from both Hooks. Its
36
+ // current native prompt/generation still binds the callback. Never synthesize
37
+ // a successful call; A checks the persisted request/result at a later stage.
38
+ const attempts=toolEvent&&record.product==='ide'&&payload.client==='CodeBuddyIDE'?8:1;
39
+ for(let attempt=1;attempt<=attempts;attempt++) {
40
+ try {
41
+ const source=readSource(payload,record,project);
42
+ let nativeRequestStatus='not-tool-event';
43
+ if(toolEvent) {
44
+ if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&payload.client==='CodeBuddyIDE') {
45
+ const latest=source.prompts.at(-1);
46
+ assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
47
+ const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
48
+ if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
49
+ else nativeRequestStatus='deferred-until-native-persistence';
50
+ }else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
51
+ }
52
+ return {...source,nativeReadAttempts:attempt,nativeRequestStatus};
53
+ }catch(error){if(attempt===attempts)throw error;pause(100);}
54
+ }
55
+ }
56
+ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false}) {
57
+ if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
58
+ if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
59
+ const front=E.loaded(source.calls,'frontend').filter(c=>c.sessionId===binding?.sessionId&&c.requestLine>source.contextLine&&(source.stableThroughLine===undefined||c.resultLine<=source.stableThroughLine));
60
+ if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine||!front.length)return {deny:true,reason:'Run execution-begin for the current task and load the frontend engineering Skill successfully in this session/context before writing prototype UI. Reading SKILL.md is insufficient.'};
61
+ }
62
+ const final=source.latestAssistant||'',claim=/(?:已完成|已经完成|全部通过|可以交付|\b(?:completed|ready for review|all checks passed)\b)/i.test(final)&&!/(?:未完成|尚未|待验证|阻塞|等待|进度|blocked|awaiting|not complete|in progress)/i.test(final);
63
+ if(event==='Stop'&&binding&&!deliveryCurrent&&!stopHookActive&&claim)return {feedback:true,reason:'This prototype run has no current verified receipt. Run execution-check and resolve the missing evidence, or report blocked / awaiting-input / progress without claiming completion.'};
64
+ return {continue:true};
65
+ }
66
+ function prototypeForWrite(project,tool,input) {
67
+ if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
68
+ const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
69
+ let current=path.dirname(path.resolve(project,file));const prototypes=path.join(project,'prototypes');
70
+ while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
71
+ }
72
+ function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
73
+ const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/';
74
+ const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
75
+ const project=real(root);
76
+ // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
77
+ // or session-bound native workspace above; reject all other outside cwd.
78
+ if(payload.cwd&&!ideRoot){const relative=path.relative(project,real(payload.cwd));assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Hook cwd is outside project');}return project;
79
+ }
80
+ function saveIndex(directory,index) {
81
+ for(const entry of Object.values(index.bindings))F.write(path.dirname(directory),`runs/${entry.runId}/binding.json`,entry);
82
+ F.write(directory,'session.json',index);
83
+ }
84
+ function toolCwd(payload,record,project) {
85
+ return record.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
86
+ }
87
+ function handle(payload,options) {
88
+ assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
89
+ const project=safeProject(payload,options),record=C.select({project,product:options.product});
90
+ if(!record||record.base!==options.base)return {continue:true};
91
+ const cwd=toolCwd(payload,record,project);
92
+ // SessionStart and analysis do not create persistent per-project/session state.
93
+ if(['SessionStart','UserPromptSubmit'].includes(options.event))return {continue:true};
94
+ let source;
95
+ try {
96
+ source=eventSource(payload,record,project);
97
+ }catch(error){
98
+ const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
99
+ if(target&&X.policy(target)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},target))return {deny:true,reason:`Native evidence unavailable for strict prototype UI write: ${error.message}`};
100
+ throw error;
101
+ }
102
+ const latest=source.prompts.at(-1);
103
+ if(C.readOnly(latest.text)) {
104
+ const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
105
+ if(options.event==='PreToolUse'&&(native&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(native.operation)||root&&X.policy(root)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},root)))return {deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'};
106
+ return {continue:true};
107
+ }
108
+ const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
109
+ const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
110
+ const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
111
+ if(options.event==='PreToolUse'&&command?.operation==='create'&&!command.argv.includes('--execution-policy')&&(E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text)))return {deny:true,reason:'New CodeBuddy prototypes require --host codebuddy --execution-policy codebuddy-ab-v1 and fixed --execution-assertions. Existing artifacts keep their original contracts.'};
112
+ const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
113
+ const existing=directory?index.bindings[directory]:null;
114
+ if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
115
+ assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
116
+ const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
117
+ const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
118
+ assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
119
+ const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
120
+ const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
121
+ index.bindings[directory]={schemaVersion:1,runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
122
+ operation:meta.revision==='r001'?'create':'revise',product:record.product,productVersion:record.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
123
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
124
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
125
+ try {
126
+ const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
127
+ saveIndex(stateDirectory,fresh);
128
+ F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,product:record.product});
129
+ }finally{fs.rmdirSync(lock);}
130
+ return {continue:true};
131
+ }
132
+ if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
133
+ const answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd});if(answer.deny)return answer;
134
+ if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)&&!existing)return {deny:true,reason:'Begin this strict prototype run with execution-begin before implementation or delivery.'};
135
+ }
136
+ if(!Object.keys(index.bindings).length)return {continue:true};
137
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
138
+ try{fs.mkdirSync(lock);}catch(error){if(error.code==='EEXIST')throw new Error('Concurrent audit callback; no success evidence recorded. Retry the stage.');throw error;}
139
+ try {
140
+ const fresh=JSON.parse(C.read(file));let changed=false;
141
+ for(const entry of Object.values(fresh.bindings)) {
142
+ if(entry.entryLine!==latest.line)continue;
143
+ const eventKey=E.sha(JSON.stringify([options.event,payload.tool_use_id||null,payload.tool_name||null,payload.tool_input||null,source.digest]));
144
+ if(entry.lastEventKey===eventKey)continue;
145
+ entry.source={path:source.path,bytes:source.bytes,digest:source.digest};entry.lastEventKey=eventKey;entry.lastHook={event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus};changed=true;
146
+ if(options.event==='Stop') {
147
+ const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
148
+ const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
149
+ const feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
150
+ if(feedback.feedback){entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return feedback;}
151
+ }
152
+ }
153
+ if(changed)saveIndex(stateDirectory,fresh);
154
+ }finally{fs.rmdirSync(lock);}
155
+ return {continue:true};
156
+ }
157
+ module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
@@ -0,0 +1,69 @@
1
+ 'use strict';
2
+ // Observed CodeBuddy IDE history format: Hook-provided index.json and only the
3
+ // UUID message files explicitly referenced by that index. No directory scan.
4
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
5
+ const ID=/^[a-f0-9]{32}$/;
6
+ const aliases={use_skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'};
7
+ function read(file) {
8
+ assert.equal(path.basename(file),'index.json','Expected native IDE index');
9
+ const directory=path.dirname(file),sessionId=path.basename(directory);
10
+ assert.match(sessionId,ID,'Invalid IDE session directory');
11
+ let total=0;
12
+ const load=target=>{
13
+ const stat=fs.lstatSync(target);assert.ok(stat.isFile()&&!stat.isSymbolicLink(),'Invalid IDE history file');
14
+ total+=stat.size;assert.ok(total<=64*1024*1024,'IDE history exceeds size limit');
15
+ return JSON.parse(fs.readFileSync(target,'utf8'));
16
+ };
17
+ const index=load(file);assert.ok(Array.isArray(index.messages)&&Array.isArray(index.requests),'Unsupported IDE index');
18
+ assert.ok(index.messages.length<=10000&&index.requests.length<=10000,'IDE history index exceeds limit');
19
+ const folder=path.join(directory,'messages');assert.ok(fs.lstatSync(folder).isDirectory()&&!fs.lstatSync(folder).isSymbolicLink(),'Invalid IDE message directory');
20
+ const requestIds=new Map(),seen=new Set(),events=[];let latestAssistant='',workspace;
21
+ for(const request of index.requests){
22
+ assert.match(request.id,ID,'Invalid IDE request ID');assert.ok(Array.isArray(request.messages),'Invalid IDE request messages');
23
+ for(const id of request.messages){assert.match(id,ID,'Invalid IDE message reference');assert.ok(!requestIds.has(id),'Duplicate IDE message request');requestIds.set(id,request.id);}
24
+ }
25
+ for(const item of index.messages){
26
+ assert.match(item.id,ID,'Invalid IDE message ID');assert.ok(!seen.has(item.id),'Duplicate IDE message ID');seen.add(item.id);
27
+ const row=load(path.join(folder,item.id+'.json'));
28
+ assert.equal(row.id,item.id,'IDE message identity differs');assert.equal(row.role,item.role,'IDE message role differs');
29
+ const message=JSON.parse(row.message),extra=JSON.parse(row.extra||'{}');
30
+ assert.equal(message.role,row.role,'IDE serialized message role differs');
31
+ assert.ok(requestIds.has(item.id),'Unbound IDE message');
32
+ // The index is the primary request membership. During execution the SDK can
33
+ // write message content before its duplicate extra.requestId metadata.
34
+ const requestId=requestIds.get(item.id);
35
+ if(extra.requestId!==undefined)assert.equal(extra.requestId,requestId,'IDE request identity differs');
36
+ assert.ok(Array.isArray(message.content),'Invalid IDE message content');
37
+ const base={sessionId,nativeMessageId:item.id,nativeRequestId:requestId};
38
+ if(row.role==='user'&&!extra.isHelperMessage){
39
+ const hostText=message.content[0]?.type==='text'?message.content[0].text:'';
40
+ const context=typeof hostText==='string'?hostText.match(/^<user_info>\s*([\s\S]*?)<\/user_info>/):null;
41
+ const nativeFolder=context?.[1].match(/^Workspace Folder:\s*(\/[^\r\n]+)$/m)?.[1]?.trim();
42
+ if(nativeFolder){
43
+ assert.ok(path.isAbsolute(nativeFolder)&&!/[\x00\r\n]/.test(nativeFolder),'Invalid native IDE workspace');
44
+ if(workspace)assert.equal(workspace,nativeFolder,'Native IDE workspace changed within one session');
45
+ workspace=nativeFolder;
46
+ }
47
+ assert.ok(Array.isArray(extra.sourceContentBlocks),'Original IDE user task is missing');
48
+ const text=extra.sourceContentBlocks.filter(b=>b.type==='text'&&typeof b.text==='string').map(b=>b.text).join('\n');
49
+ if(text.trim())events.push({...base,type:'user',message:{content:[{type:'text',text}]}});
50
+ }else if(row.role==='assistant'){
51
+ const text=message.content.filter(b=>b.type==='text').map(b=>b.text||'').join('\n');
52
+ if(text)latestAssistant=text;
53
+ for(const block of message.content.filter(b=>b.type==='tool-call')){
54
+ assert.ok(block.args&&typeof block.args==='object'&&!Array.isArray(block.args),'Invalid IDE tool arguments');
55
+ events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:aliases[block.toolName]||block.toolName,input:block.args}]}});
56
+ }
57
+ }else if(row.role==='tool'){
58
+ for(const block of message.content.filter(b=>b.type==='tool-result')){
59
+ const result=block.result,success=!block.isError&&result?.success===true&&result.status==='success';
60
+ events.push({...base,type:'user',message:{content:[{type:'tool_result',tool_use_id:block.toolCallId,
61
+ tool_name:aliases[block.toolName]||block.toolName,content:result,is_error:!success}]}});
62
+ }
63
+ }
64
+ }
65
+ // Only stable prompt/call/result facts form the prefix cursor. Streaming text,
66
+ // reasoning, request state and mutable toolStatus do not invalidate old facts.
67
+ return {text:events.map(row=>JSON.stringify(row)).join('\n')+'\n',latestAssistant,workspace,format:'codebuddy-ide-index-v1'};
68
+ }
69
+ module.exports={read};
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ // Pure configuration planning. This module does not install or activate a host.
3
+ const assert = require('node:assert/strict');
4
+ const path = require('node:path');
5
+ const fs = require('node:fs');
6
+ const crypto = require('node:crypto');
7
+ const { digest } = require('./execution-policy.cjs');
8
+ const OWNER = 'i3-prototype-codebuddy-v1';
9
+ const EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'];
10
+ function quote(value) {
11
+ assert.ok(typeof value === 'string' && !/[\x00\r\n]/.test(value), 'Unsafe Hook argument');
12
+ return "'" + value.replaceAll("'", "'\\''") + "'";
13
+ }
14
+ function config(text) {
15
+ if (text === null) return {};
16
+ let value; try { value = JSON.parse(text); } catch { throw new Error('Settings must be valid JSON; preserve the original file'); }
17
+ assert.ok(value && !Array.isArray(value) && typeof value === 'object', 'Settings must be a JSON object');
18
+ assert.ok(!value.hooks || (typeof value.hooks === 'object' && !Array.isArray(value.hooks)), 'Invalid hooks configuration');
19
+ for (const groups of Object.values(value.hooks || {})) {
20
+ assert.ok(Array.isArray(groups), 'Hook groups must be arrays');
21
+ for (const group of groups) assert.ok(group && Array.isArray(group.hooks), 'Invalid Hook group');
22
+ }
23
+ return value;
24
+ }
25
+ function ownedMarker(group) { return group.hooks.some(hook => typeof hook.command === 'string' && hook.command.includes(OWNER)); }
26
+ function plan(options) {
27
+ const { action = 'keep', scope, product, base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
28
+ transcriptRoots = record?.transcriptRoots || [], transcriptMode = record?.transcriptMode || (transcriptRoots.length ? 'manual' : 'auto'), productVersion = record?.productVersion || 'unreported' } = options;
29
+ assert.ok(['keep', 'enable', 'disable'].includes(action), 'Invalid Hook action');
30
+ assert.ok(['project', 'global'].includes(scope), 'Invalid installation scope');
31
+ assert.ok(['ide', 'cli'].includes(product), 'Specify CodeBuddy product ide or cli');
32
+ for (const item of [base, project, nodePath, scriptPath]) assert.ok(typeof item === 'string' && path.isAbsolute(item) && !/[\x00\r\n]/.test(item), 'Absolute safe registration paths required');
33
+ if (process.platform === 'win32') throw new Error('CodeBuddy A+B Windows Hook quoting is not yet verified');
34
+ const before = config(currentText), after = structuredClone(before), activeRecord = record && record.owner === OWNER ? record : null;
35
+ if (record) {
36
+ assert.ok(activeRecord && activeRecord.schemaVersion === 1, 'Unknown registration ownership');
37
+ assert.equal(activeRecord.scope, scope, 'Registration scope changed');
38
+ assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
39
+ assert.equal(activeRecord.base, base, 'Registration base changed');
40
+ }
41
+ const found = {};
42
+ for (const [event, groups] of Object.entries(after.hooks || {})) {
43
+ for (const group of groups.filter(ownedMarker)) {
44
+ assert.ok(activeRecord?.groups[event], 'Hook ownership is missing; do not adopt an unknown command');
45
+ assert.deepEqual(group, activeRecord.groups[event], 'Owned Hook was modified; preserve the user changes');
46
+ assert.ok(!found[event], 'Duplicate owned Hook registration'); found[event] = group;
47
+ }
48
+ }
49
+ if (activeRecord) for (const event of Object.keys(activeRecord.groups)) assert.ok(found[event], 'Owned Hook registration changed or is missing');
50
+ const unchanged = action === 'keep' && !activeRecord;
51
+ let recordAfter = null;
52
+ if (!unchanged) {
53
+ after.hooks ||= {};
54
+ for (const event of Object.keys(found)) after.hooks[event] = after.hooks[event].filter(group => group !== found[event]);
55
+ if (action !== 'disable') {
56
+ assert.match(scriptDigest || '', /^[a-f0-9]{64}$/, 'Hook script digest required');
57
+ const groups = {};
58
+ for (const event of EVENTS) {
59
+ const argv = [nodePath, scriptPath, '--registration', OWNER, '--base', base, '--scope', scope, '--product', product, '--event', event];
60
+ const group = { ...(event.endsWith('ToolUse') ? { matcher: '' } : {}), hooks: [{ type: 'command', command: argv.map(quote).join(' '), timeout: 10 }] };
61
+ groups[event] = group; (after.hooks[event] ||= []).push(group);
62
+ }
63
+ assert.ok(Array.isArray(transcriptRoots)&&transcriptRoots.every(root=>typeof root==='string'&&path.isAbsolute(root)&&!/[\x00\r\n]/.test(root)),'Absolute native transcript roots required');
64
+ assert.ok(['auto','manual'].includes(transcriptMode),'Invalid native transcript mode');
65
+ assert.ok(transcriptMode==='manual'?transcriptRoots.length>0:transcriptRoots.length===0,'Manual transcript mode requires roots; auto uses only the native callback file');
66
+ recordAfter = { schemaVersion: 1, owner: OWNER, scope, product, base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
67
+ }
68
+ }
69
+ const afterText = unchanged ? currentText : JSON.stringify(after, null, 2) + '\n';
70
+ return { action, scope, product, base, project, settingsPath: path.join(base, 'settings.json'),
71
+ recordPath: path.join(base, 'state/i3-prototype/installation.json'), beforeDigest: digest(currentText), beforeRecordDigest: digest(record), afterText, recordAfter,
72
+ summary: { status: unchanged || action === 'disable' ? 'not-enabled' : product === 'cli' ? 'pending-host-review' : 'registered',
73
+ ...(recordAfter?{transcript:{mode:transcriptMode,status:'awaiting-native-event',invocationVerified:false}}:{}),
74
+ scope, product, settingsPath: path.join(base, 'settings.json'), events: recordAfter ? EVENTS : [], changed: afterText !== currentText || digest(recordAfter)!==digest(record),
75
+ next: product === 'cli' && recordAfter ? 'Review the external settings change in CodeBuddy /hooks; then start a new session.' : 'Actual Hook invocation must be checked in the host; registration is not activation.' } };
76
+ }
77
+ function safePath(file) {
78
+ let current = path.parse(path.resolve(file)).root;
79
+ for (const part of path.resolve(file).slice(current.length).split(path.sep)) {
80
+ current = path.join(current, part);
81
+ if (fs.existsSync(current)) assert.ok(!fs.lstatSync(current).isSymbolicLink(), 'Hook settings symlink refused');
82
+ }
83
+ }
84
+ const fileIO = {
85
+ lock(file) {safePath(file);fs.mkdirSync(path.dirname(file),{recursive:true});const lock=file+'.i3-prototype.lock';try{fs.mkdirSync(lock);}catch{throw new Error('Hook configuration is being updated; retry without overwriting the other change');}return ()=>fs.rmdirSync(lock);},
86
+ read(file) {
87
+ safePath(file); if (!fs.existsSync(file)) return null;
88
+ assert.ok(fs.statSync(file).isFile() && fs.statSync(file).size <= 4 * 1024 * 1024, 'Invalid or oversized Hook configuration');
89
+ return fs.readFileSync(file, 'utf8');
90
+ },
91
+ write(file, text, expected) {
92
+ assert.equal(digest(this.read(file)), expected, 'Hook configuration changed concurrently');
93
+ if (text === null) { if (fs.existsSync(file)) fs.unlinkSync(file); return; }
94
+ fs.mkdirSync(path.dirname(file), { recursive: true }); safePath(file);
95
+ const staging = file + '.' + crypto.randomUUID() + '.new';
96
+ try {
97
+ fs.writeFileSync(staging, text, { flag: 'wx', mode: fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600 });
98
+ assert.equal(digest(this.read(file)), expected, 'Hook configuration changed before publication');
99
+ fs.renameSync(staging, file);
100
+ } finally { if (fs.existsSync(staging)) fs.unlinkSync(staging); }
101
+ },
102
+ };
103
+ function apply(change, io = fileIO) {
104
+ const unlock=io.lock?io.lock(change.settingsPath):()=>{};
105
+ try {return applyLocked(change,io);}finally{unlock();}
106
+ }
107
+ function applyLocked(change, io) {
108
+ const before = io.read(change.settingsPath), recordBefore = io.read(change.recordPath);
109
+ assert.equal(digest(before), change.beforeDigest, 'Hook configuration changed after preview');
110
+ assert.equal(digest(recordBefore === null ? null : JSON.parse(recordBefore)), change.beforeRecordDigest, 'Hook ownership changed after preview');
111
+ const recordAfter = change.recordAfter ? JSON.stringify(change.recordAfter, null, 2) + '\n' : null;
112
+ if (change.afterText === before && recordAfter === recordBefore) return change.summary;
113
+ let settingsWritten = false;
114
+ try {
115
+ if (change.afterText !== before) { io.write(change.settingsPath, change.afterText, digest(before)); settingsWritten = true; }
116
+ if (recordAfter !== recordBefore) io.write(change.recordPath, recordAfter, digest(recordBefore));
117
+ } catch (error) {
118
+ if (settingsWritten && io.read(change.settingsPath) === change.afterText) io.write(change.settingsPath, before, digest(change.afterText));
119
+ throw error;
120
+ }
121
+ return change.summary;
122
+ }
123
+ module.exports = { OWNER, EVENTS, quote, config, plan, apply, fileIO };