@givolt/i3-composer-agent 2.3.5 → 2.3.6

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 (32) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +1 -1
  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 +1 -1
  7. package/README.md +1 -1
  8. package/commands/i3-prototype.toml +1 -1
  9. package/docs/codebuddy-setup.md +1 -1
  10. package/docs/codex-setup.md +1 -1
  11. package/docs/other-hosts.md +1 -1
  12. package/docs/workbuddy-setup.md +1 -1
  13. package/hooks/PROTOTYPE.md +19 -0
  14. package/hooks/i3-prototype.cjs +2 -1
  15. package/package.json +1 -1
  16. package/plugin.json +1 -1
  17. package/scripts/ui-prototype-codebuddy-hook-test.js +2 -2
  18. package/skills/i3-ui-prototype/SKILL.md +2 -8
  19. package/skills/i3-ui-prototype/references/execution-evidence.md +7 -1
  20. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +3 -3
  21. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +114 -25
  22. package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +94 -0
  23. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +46 -1
  24. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +5 -4
  25. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +3 -1
  26. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +4 -3
  27. package/skills/i3-ui-prototype/scripts/native-files.cjs +10 -1
  28. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +6 -1
  29. package/skills/i3-ui-prototype/scripts/native-tool-result.cjs +14 -0
  30. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +1 -1
  31. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +7 -3
  32. package/skills/i3-ui-prototype/scripts/ptc-callbacks.cjs +131 -0
@@ -6,7 +6,7 @@
6
6
  "plugins": [
7
7
  {
8
8
  "name": "i3-composer-agent",
9
- "version": "2.3.5",
9
+ "version": "2.3.6",
10
10
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
11
11
  "source": {
12
12
  "source": "local",
@@ -20,4 +20,4 @@ $ARGUMENTS
20
20
 
21
21
  With no arguments use create; explicit analysis or read-only limits take precedence. Preserve the user's scope, manual edits, fixed inputs and approved baselines. Use supporting engineering, framework, design and browser skills as the working skill requires; keep its workflow and evidence requirements there.
22
22
 
23
- For a managed Buddy implementation, execute the Skill's printed absolute execution-begin command on its own and wait for status=bound before writing UI. Follow immediate recovery feedback and retry the blocked operation; an outer tool success does not override an inner stage failure.
23
+ Create the prototype in the current session. Load the required frontend/framework/design Skills before implementation and use the browser Skill for interaction acceptance. Installed Hooks run automatically; do not inspect Hook internals, search host logs, or run a manual binding step. When a required Skill is missing, invoke it and retry; report an infrastructure failure briefly through the separate maintenance entry.
@@ -8,7 +8,7 @@
8
8
  "plugins": [
9
9
  {
10
10
  "name": "i3-composer-agent",
11
- "version": "2.3.5",
11
+ "version": "2.3.6",
12
12
  "source": "./",
13
13
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
14
14
  "homepage": "https://github.com/49780522/i3-composer-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -19,5 +19,5 @@ $ARGUMENTS
19
19
 
20
20
  With no arguments use create; explicit analysis or read-only limits take precedence. Preserve the user's scope, manual edits, fixed inputs and approved baselines. Use supporting engineering, framework, design and browser skills as the working skill requires; keep its workflow and evidence requirements there.
21
21
 
22
- For a managed Buddy implementation, execute the Skill's printed absolute execution-begin command on its own and wait for status=bound before writing UI. Follow immediate recovery feedback and retry the blocked operation; an outer tool success does not override an inner stage failure.
22
+ Create the prototype in the current session. Load the required frontend/framework/design Skills before implementation and use the browser Skill for interaction acceptance. Installed Hooks run automatically; do not inspect Hook internals, search host logs, or run a manual binding step. When a required Skill is missing, invoke it and retry; report an infrastructure failure briefly through the separate maintenance entry.
23
23
  """
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i3 Composer Agent v2.3.5
1
+ # i3 Composer Agent v2.3.6
2
2
 
3
3
  **Intelligence. Integration. Innovation.**
4
4
 
@@ -19,5 +19,5 @@ $ARGUMENTS
19
19
 
20
20
  With no arguments use create; explicit analysis or read-only limits take precedence. Preserve the user's scope, manual edits, fixed inputs and approved baselines. Use supporting engineering, framework, design and browser skills as the working skill requires; keep its workflow and evidence requirements there.
21
21
 
22
- For a managed Buddy implementation, execute the Skill's printed absolute execution-begin command on its own and wait for status=bound before writing UI. Follow immediate recovery feedback and retry the blocked operation; an outer tool success does not override an inner stage failure.
22
+ Create the prototype in the current session. Load the required frontend/framework/design Skills before implementation and use the browser Skill for interaction acceptance. Installed Hooks run automatically; do not inspect Hook internals, search host logs, or run a manual binding step. When a required Skill is missing, invoke it and retry; report an infrastructure failure briefly through the separate maintenance entry.
23
23
  """
@@ -48,7 +48,7 @@ CodeBuddy 与 WorkBuddy 项目共用 `.codebuddy/`;同版本追加客户端会
48
48
  npx @givolt/i3-composer-agent --codebuddy --scope project --yes --prototype-skills uiux,antdv
49
49
  ```
50
50
 
51
- 值为 none、uiux、antdv 或 uiux,antdv;默认 none。可选工具跟随宿主和项目/全局范围,位于基址下 `tools/i3-prototype/`;旧项目 `.prototype-tools` 保持读取兼容。需要固定本次版本时使用 `npx @givolt/i3-composer-agent@2.3.5`。已安装后可随时单独检测/补装,无需重装 i3:
51
+ 值为 none、uiux、antdv 或 uiux,antdv;默认 none。可选工具跟随宿主和项目/全局范围,位于基址下 `tools/i3-prototype/`;旧项目 `.prototype-tools` 保持读取兼容。需要固定本次版本时使用 `npx @givolt/i3-composer-agent@2.3.6`。已安装后可随时单独检测/补装,无需重装 i3:
52
52
 
53
53
  ```bash
54
54
  node .codebuddy/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs check --host codebuddy --scope project --project .
@@ -37,6 +37,6 @@ Codex 已有原生技能路由,勿把元技能全文预加载为常驻上下
37
37
 
38
38
  ## 可选原型工具
39
39
 
40
- 原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。项目安装可使用 `npx @givolt/i3-composer-agent@2.3.5 --codex --scope project --yes --prototype-skills uiux,antdv`;全局安装将 scope 改为 global。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
40
+ 原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。项目安装可使用 `npx @givolt/i3-composer-agent@2.3.6 --codex --scope project --yes --prototype-skills uiux,antdv`;全局安装将 scope 改为 global。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
41
41
 
42
42
  原型共享资料统一在项目 prototypes/:新索引 prototypes/index.json,准备范围 prototypes/.preparation/<id>/scope.json。新原生 scope.moduleIds 自动生成 appliesTo;旧索引、输入和实例保持兼容,安装升级不自动迁移。工具及运行缓存使用所选宿主基址;全局浏览器缓存按业务项目身份隔离。详细规则和只读查询/显式迁移命令见 i3-ui-prototype/references/project-layout.md。
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 使用本地分发目录
8
8
 
9
- 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.5` 的对应入口。
9
+ 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.6` 的对应入口。
10
10
 
11
11
  ### fx
12
12
 
@@ -85,7 +85,7 @@ npx @givolt/i3-composer-agent --workbuddy --scope global --yes --force
85
85
 
86
86
  ## 可选原型工具
87
87
 
88
- 原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。推荐使用 `npx @givolt/i3-composer-agent@2.3.5 --workbuddy --scope global --yes --prototype-skills uiux,antdv`;项目安装将 scope 改为 project。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
88
+ 原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。推荐使用 `npx @givolt/i3-composer-agent@2.3.6 --workbuddy --scope global --yes --prototype-skills uiux,antdv`;项目安装将 scope 改为 project。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
89
89
 
90
90
  原型共享资料统一在项目 prototypes/:新索引 prototypes/index.json,准备范围 prototypes/.preparation/<id>/scope.json。新原生 scope.moduleIds 自动生成 appliesTo;旧索引、输入和实例保持兼容,安装升级不自动迁移。工具及运行缓存使用所选宿主基址;全局浏览器缓存按业务项目身份隔离。详细规则和只读查询/显式迁移命令见 i3-ui-prototype/references/project-layout.md。
91
91
 
@@ -0,0 +1,19 @@
1
+ # 原型 Hooks 安装与维护
2
+
3
+ <!-- i3:zh-guide:start -->
4
+ ## 中文使用说明
5
+
6
+ 按下文完整工程步骤、检查项与验证条件执行。根据任务选择适用步骤,保留证据、风险和退出条件;正文示例中的英文标签在面向用户输出时译为中文。
7
+
8
+ 默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
+ <!-- i3:zh-guide:end -->
10
+
11
+ 本文件面向安装器和维护人员。i3-ui-prototype 的正常职责是制作、修订和验收原型,不承担宿主或 Hook 排障。
12
+
13
+ 安装器在 CodeBuddy/WorkBuddy 项目或全局范围注册 prototype 组。宿主自动触发;不要求原型使用者额外执行 execution-begin 或确认任务绑定。必要 Skill 的真实调用由原生 Pre/Post 回调记录,PTC/REPL 的内部工具分别记录;REPL 脚本文本不能代替调用成功。Bash 不提供 stdout 时由本包第一方入口捕获实际结果;其他只读查询可使用 native-tool-result.cjs 的本地绝对入口,不安装依赖。
14
+
15
+ 检查命令:`npx --yes @givolt/i3-composer-agent hooks check --host codebuddy --scope project --project .`;全局将 scope 改为 global。检查只读,不启用/修复设置。registered 表示已注册;active 表示观察到当前注册的有效运行;failure 显示已触发但处理失败的原因、会话、模式、次数和恢复状态。两次同类恢复未成功后转 blocked,不会因重试旧 begin 清零。只读维护不消耗恢复次数。
16
+
17
+ 状态与 PTC 机器回调位于宿主基址的 state/i3-prototype/;它们是诊断/证据记录,不是业务项目的原型目录。项目/全局、宿主/会话和当前用户任务分别隔离。保留上游 Hooks、用户注册、旧 HTML 和已有原型;旧 execution-* 命令仅供兼容,不是新流程的前置。
18
+
19
+ CodeBuddy 项目/全局 × IDE/CLI(CLI direct/PTC)真实注册、触发、必要 Skill 调用、缺失补调和失败展示必须分别验收。自动化回放不能冒充真实宿主;正式发布门禁在仓库维护工具中执行,不放到业务原型 Skill 内。
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // Stable host Hook entry; the workflow and A+B implementation remain in the Skill.
3
+ // Stable host Hook entry. Shared legacy helpers are an implementation detail;
4
+ // installation, automatic dispatch and diagnostics belong to Hooks, not UI tasks.
4
5
  const fs=require('node:fs'),path=require('node:path');
5
6
  function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8'),load=()=>require(path.join(require('./manage.cjs').modules().scripts,'hosts/codebuddy.cjs'))) {
6
7
  return load().main(argv,raw);
package/package.json CHANGED
@@ -444,7 +444,7 @@
444
444
  "scripts/validate-versions.js"
445
445
  ]
446
446
  },
447
- "version": "2.3.5",
447
+ "version": "2.3.6",
448
448
  "repository": {
449
449
  "type": "git",
450
450
  "url": "https://github.com/49780522/i3-composer-agent.git"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.5",
3
+ "version": "2.3.6",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -75,7 +75,7 @@ test('IDE streaming requests correlate only within the exact native generation a
75
75
  assert.throws(()=>H.eventCall({...payload,tool_input:{content:'unrelated'}},source),/uniquely/);
76
76
  source.calls.push({...source.calls[0],toolCallId:'second-call'});assert.throws(()=>H.eventCall(payload,source),/uniquely/);
77
77
  });
78
- test('IDE transient reads retry while wrong generations and mismatched pending requests stay rejected',()=>{
78
+ test('IDE and CLI transient reads retry within a fixed limit; wrong generations and requests remain rejected',()=>{
79
79
  const payload={hook_event_name:'PreToolUse',client:'CodeBuddyIDE',generation_id:'g1',tool_name:'Write',tool_input:{content:'ui'}};
80
80
  const pending={format:'codebuddy-ide-index-v1',prompts:[{line:1,nativeRequestId:'g1'}],calls:[{name:'Write',toolCallId:'w1',nativeRequestId:'g1',input:{content:'ui'},requestLine:2}]};
81
81
  let reads=0,waits=0;
@@ -84,7 +84,7 @@ test('IDE transient reads retry while wrong generations and mismatched pending r
84
84
  assert.equal(result.calls[0].toolCallId,'w1');assert.equal(result.nativeReadAttempts,3);assert.equal(waits,2);
85
85
  reads=0;assert.throws(()=>H.eventSource({...payload,generation_id:'g2'},{product:'ide'},'/work',{readSource:()=>{reads++;return pending;},pause:()=>{}}),/generation/);assert.equal(reads,8);
86
86
  assert.throws(()=>H.eventSource({...payload,tool_input:{content:'different'}},{product:'ide'},'/work',{readSource:()=>pending,pause:()=>{}}),/uniquely/);
87
- reads=0;assert.throws(()=>H.eventSource({...payload,client:'cli'},{product:'cli'},'/work',{readSource:()=>{reads++;return {...pending,calls:[]};},pause:()=>assert.fail('CLI must not retry')}),/uniquely/);assert.equal(reads,1);
87
+ reads=0;assert.throws(()=>H.eventSource({...payload,client:'cli'},{product:'cli'},'/work',{readSource:()=>{reads++;return {...pending,calls:[]};},pause:()=>{}}),/uniquely/);assert.equal(reads,8);
88
88
  });
89
89
  test('IDE defers current callback evidence until the host persists it and never invents Skill success',()=>{
90
90
  const source={format:'codebuddy-ide-index-v1',prompts:[{line:1,nativeRequestId:'g1',text:'implement prototype'}],calls:[],contextLine:0};
@@ -57,15 +57,9 @@ analyze 的交付是需求/能力覆盖、模板与框架选型、交互验收
57
57
 
58
58
  已有 v2 实例先运行一次 `node <skill-root>/scripts/native-pilot.cjs state <prototype-root>`,从聚合结果恢复身份、固定需求、设计/构建/实际QA、负责人和下一步的精确命令。不要为寻找参数反复遍历全仓库、node_modules或阅读控制器内部实现。这里只读检查,不安装、启动浏览器或更新状态。
59
59
 
60
- CodeBuddy 新建原型采用 A+B 执行合同,实际接口见 [execution-evidence.md](references/execution-evidence.md)。先只读检查该安装范围的 `prototype-hooks.cjs check`;未启用或来源协议未核实则报告缺项,不能自行改宿主配置或声称严格完成。analyze 和纯读取不进入制作门禁。已有实例没有 `runtime.executionPolicy` 时保留原合同,不隐式追加输入或重建原型。
60
+ 制作前实际调用所需的前端/框架/设计 Skill,真实浏览器验收使用浏览器 Skill;不能以读取 SKILL.md 代替调用。安装器注册的 Hooks 自动运行。收到缺少 Skill 的反馈时,补调该 Skill、等待成功后重试原操作;基础设施异常只简要报告未验收项,由独立维护入口处理,不在原型任务中排查。
61
61
 
62
- 项目/全局 IDE/CLI 新启用默认从真实 Hook 的 `transcript_path` 自动取得当前会话文件;不让模型搜索/填写日志、不选择最近文件。`transcript.status=awaiting-native-event` 表示尚未观察,不阻止启动已授权任务及 `execution-begin`;进入制作和交付仍必须有可解析的原生证据。已有手动目录限制保持,可由用户显式切回 auto;缺字段、不可读、格式不支持或会话不符时报告未核验,不能用手写记录补齐。
63
-
64
- CodeBuddy/WorkBuddy 新 create 前,用 `native-pilot.cjs execution-assertions-template --scope <scope-file>` 取得完整场景,按本次 PRD 与能力参数填充每项预期 JSON 事实,存于该原型 `.preparation` 目录。create 增加 `--host <codebuddy|workbuddy> --execution-policy buddy-ab-v2 --execution-assertions <assertions-file>`;官方 Skill 真实不可用时才显式 `--official-source-fallback`,保留其失败调用定位。随后在实际所选宿主主会话执行 `execution-begin <prototype-root>`,以原生 Skill 工具加载当前上下文需要的前端 Skill 后再写业务界面。旧 codebuddy-ab-v1 及未采用 executionPolicy 的实例保留原合同。其他宿主尚未适配此 Hook 合同,按原流程披露调用核验边界。
65
-
66
- 新建后及恢复实施时,从 `state` 的 `commands.executionBegin.shell` 取得绝对路径命令,在该宿主主会话的 Shell 工具中独立执行;也可原样执行门禁反馈给出的命令。不得拼接 `cd &&`、管道、重定向或 `head`;这些组合不能建立原生任务绑定。等待进程退出码为 0 且内层结果 `status=bound`,再进行业务写入。外层 `success:true` 仅表示工具返回,内层 `status=failed`/非零退出仍是失败;失败后立即按反馈补做绑定或 Skill 调用,再重试原操作。任务/上下文变化重新绑定,失败或未完成的 begin 不能继承旧成功;绑定由真实 PreToolUse 创建,CLI 不补造绑定或成功记录。
67
-
68
- `execution-template <prototype-root>` 返回当前身份与已观察到的真实加载引用;以实际查询、实现复核及浏览器结果补齐 `qa/execution-evidence.json`,不能补造调用 ID。构建、record-run、record-devtools-review、finalize 与 `execution-check --stage <stage>` 共用 A;B 在可识别的写入前检查,反馈明确 Skill 名与下一动作,成功补调后重试;Stop 按同缺项两次、当前原生任务总计六次的预算继续纠正,到上限如实报告阻塞,反复 begin 不清零。只读分析、进度、阻塞及用户停止可正常结束。未通过门禁时交付候选和缺项;新收据的 `native-source-correlated` 不代表不可篡改认证或用户认可。
62
+ 新 create 使用实际 `--host <codebuddy|workbuddy>`。按 PRD 和固定能力制定交互验收场景,使用 `native-pilot.cjs execution-assertions-template --scope <scope-file>` 取得场景骨架,填充预期 JSON 事实并传 `--execution-assertions <assertions-file>`。这是原型交互验收,不要求手动绑定会话或填写 Hook 调用记录。旧 HTML 和既有 execution-* 入口保持兼容。
69
63
 
70
64
  ### 2. 完整内容模板与需求能力
71
65
 
@@ -1,4 +1,4 @@
1
- # Buddy A+B 执行与补调合同
1
+ # 原型调用记录:兼容合同
2
2
 
3
3
  <!-- i3:zh-guide:start -->
4
4
  ## 中文使用说明
@@ -8,6 +8,12 @@
8
8
  默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
9
9
  <!-- i3:zh-guide:end -->
10
10
 
11
+ 当前正常流程由安装器注册的 Hooks 自动采集真实 Skill 和工具调用。原型会话直接制作、修订和验收 UI;没有手动 execution-begin、检查 Hook 或填写内部调用记录的前置。缺少必要 Skill 时只补调该 Skill 并重试原操作。安装、原生协议和失败诊断统一见随包 [Hooks 维护说明](../../../hooks/PROTOTYPE.md),不属于原型工作步骤。
12
+
13
+ 以下为已有显式 execution-* 使用者保留的兼容格式与旧手动流程,不作为新任务指导;已有原型不会自动改写。
14
+
15
+ ## 旧 Buddy A+B 执行与补调合同
16
+
11
17
  本合同适配 CodeBuddy IDE/CLI 和 WorkBuddy 桌面 command Hooks;各宿主项目/全局的真实调用、补调及业务效果仍须逐项手测,profile 保持 pending-user。不是新增专责 Agent,不改 using 的发现/路由、上游前端/浏览器方法或上游 Hooks。A 是确定性门禁,B 采集与绑定原生来源;没有 C 提示词对照表方案。
12
18
 
13
19
  ## 安装范围与原生生效
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
  const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
3
3
  const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
4
- const FILES=['native-tool-name.cjs','native-command.cjs','buddy-host.cjs','execution-recovery.cjs','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'];
4
+ const FILES=['native-tool-name.cjs','native-command.cjs','buddy-host.cjs','execution-recovery.cjs','execution-diagnostics.cjs','ptc-callbacks.cjs','native-tool-result.cjs','prototype-dependencies.cjs','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
5
  const read=file=>R.fileIO.read(file);
6
6
  function scriptsForEntry(scriptPath) {
7
7
  if(path.basename(scriptPath)!=='i3-prototype.cjs')return path.dirname(path.dirname(scriptPath));
@@ -60,7 +60,7 @@ function source(payload,record,project) {
60
60
  assert.ok(value.prompts.length,'Original user task is absent from native transcript');
61
61
  assert.ok(value.calls.every(call=>call.sessionId===payload.session_id),'Native session identity missing or mismatched');
62
62
  assert.ok(value.prompts.every(prompt=>!prompt.sessionId||prompt.sessionId===payload.session_id),'Native prompt session differs');
63
- return value;
63
+ return record.product==='cli'?require('./ptc-callbacks.cjs').read(value,{record,project,sessionId:payload.session_id}):value;
64
64
  }
65
65
  function readOnly(prompt) {
66
66
  if(/不(?:要)?(?:改代码|修改(?:任何)?代码|实施|生成原型)|\b(?:analysis only|do not (?:edit|modify|implement))\b/i.test(prompt))return true;
@@ -72,4 +72,4 @@ function prefix(file,bytes,digest) {
72
72
  assert.ok(Number.isSafeInteger(bytes)&&bytes>0&&bytes<=64*1024*1024,'Invalid source cursor');
73
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
74
  }
75
- module.exports={read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
75
+ module.exports={FILES:Object.freeze(FILES),read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
@@ -19,7 +19,11 @@ function eventCall(payload,source) {
19
19
  // pending call remains unsuccessful until a native result is persisted.
20
20
  return source.format==='codebuddy-ide-index-v1'&&H.isIDE(payload)&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
21
21
  });
22
- const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
22
+ // IDE can omit the call ID and invoke Post before persisting the result.
23
+ // A failed earlier attempt can have identical args; only the still-pending
24
+ // request in this exact generation can be the current callback.
25
+ const pending=matches.filter(call=>!call.resultLine);
26
+ const current=payload.hook_event_name==='PreToolUse'||source.format==='codebuddy-ide-index-v1'&&pending.length?pending:matches;
23
27
  assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
24
28
  }
25
29
  function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
@@ -27,10 +31,13 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
27
31
  // IDE can persist the current call only AFTER returning from both Hooks. Its
28
32
  // current native prompt/generation still binds the callback. Never synthesize
29
33
  // a successful call; A checks the persisted request/result at a later stage.
30
- const attempts=toolEvent&&record.product==='ide'&&H.isIDE(payload)?8:1;
34
+ const attempts=toolEvent?8:1;
31
35
  for(let attempt=1;attempt<=attempts;attempt++) {
32
36
  try {
33
- const source=readSource(payload,record,project);
37
+ let source=readSource(payload,record,project);
38
+ if(toolEvent&&record.product==='cli'&&(payload.agent_type==='ptc'||source.calls.some(c=>c.name==='REPL'&&!c.resultLine))){
39
+ source=require('./ptc-callbacks.cjs').observe({payload,record,project,source});
40
+ }
34
41
  let nativeRequestStatus='not-tool-event';
35
42
  if(toolEvent) {
36
43
  if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
@@ -49,26 +56,26 @@ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent
49
56
  if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
50
57
  if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
51
58
  const missing=Recovery.missing({source,binding,policy});
52
- if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory})};
59
+ if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
53
60
  }
54
61
  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);
55
62
  const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
56
63
  if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
57
64
  const missing=Recovery.missing({source,binding,policy,stage:'delivery'});if(!missing.length)missing.push('execution-evidence');
58
- return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory})};
65
+ return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
59
66
  }
60
67
  return {continue:true};
61
68
  }
62
- function prototypeForWrite(project,tool,input) {
69
+ function prototypeForWrite(project,tool,input,cwd=project) {
63
70
  if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
64
71
  const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
65
- let current=path.dirname(path.resolve(project,file));const prototypes=path.join(project,'prototypes');
72
+ let current=path.dirname(path.resolve(cwd,file));const prototypes=path.join(project,'prototypes');
66
73
  while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
67
74
  }
68
75
  function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
69
76
  const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
70
77
  const hostProject=options.host==='workbuddy'?env.WORKBUDDY_PROJECT_DIR:env.CODEBUDDY_PROJECT_DIR;
71
- const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):hostProject||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
78
+ const root=options.scope==='project'?path.dirname(options.base):options.nativeProject|| (ideRoot?workspace(payload.transcript_path,payload.session_id):hostProject||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null));assert.ok(root&&path.isAbsolute(root),'Native project directory required');
72
79
  const project=real(root);
73
80
  // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
74
81
  // or session-bound native workspace above; reject all other outside cwd.
@@ -81,20 +88,45 @@ function saveIndex(directory,index) {
81
88
  function toolCwd(payload,record,project) {
82
89
  return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
83
90
  }
84
- function handle(payload,options) {
91
+ function settleNativeRetries(index,source,{project,sessionId}) {
92
+ let changed=false;
93
+ for(const entry of Object.values(index.bindings)) {
94
+ const repair=entry.recovery,expected=repair?.expectedRetry;
95
+ if(!expected||!['required','ready-to-retry'].includes(repair.status)||entry.entryLine!==source.prompts.at(-1)?.line||entry.contextLine!==source.contextLine)continue;
96
+ const matches=source.calls.filter(call=>{
97
+ if(!call.success||!call.resultLine||call.sessionId!==sessionId||call.requestLine<=entry.entryLine||source.stableThroughLine!==undefined&&call.resultLine>source.stableThroughLine)return false;
98
+ if(expected.kind==='ui-write') {
99
+ const input=call.input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
100
+ return typeof file==='string'&&path.resolve(call.cwd||project,file)===expected.file&&G.uiWrite({...call,cwd:call.cwd||project},entry.directory);
101
+ }
102
+ const parsed=nativeCommand(call.name,call.input,call.cwd||project);
103
+ return expected.kind==='stage'&&parsed?.root===entry.directory&&parsed.operation===expected.operation;
104
+ });
105
+ for(const call of matches) {
106
+ const stage=expected.kind==='stage'?expected.stage:null;
107
+ if(stage&&X.check(entry.directory,stage,require('./native-pilot-lib.cjs')).status!=='passed')continue;
108
+ const retry={...expected,toolCallId:call.toolCallId,resultDigest:call.resultDigest,requestLine:call.requestLine,resultLine:call.resultLine};
109
+ const answer=Recovery.record(index,{source,sessionId,directory:entry.directory,event:'PostToolUse',eventKey:E.sha(JSON.stringify(['persisted-native-result',call.toolCallId,call.resultDigest])),stage,policy:X.policy(entry.directory),resolved:stage?['execution-evidence']:[],retrySucceeded:true,retry},{continue:true});
110
+ if(answer.recovery&&JSON.stringify(answer.recovery)!==JSON.stringify(entry.recovery)){entry.recovery=answer.recovery;changed=true;}
111
+ if(entry.recovery?.status==='recovered')break;
112
+ }
113
+ }
114
+ return changed;
115
+ }
116
+ function dispatch(payload,options) {
85
117
  payload={...payload,tool_name:toolName(payload.tool_name)};
86
118
  assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
87
119
  if(options.event==='SessionStart')return {continue:true};
88
120
  if(options.event==='UserPromptSubmit'){
89
121
  const prompt=payload.prompt||'';
90
- if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:先 execution-begin,再实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并通过 prototype-deps query 或检测出的本地 Node 入口查询组件。专业模式还须实际调用 UIUX。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
122
+ if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:界面实现前实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并查询实际组件。专业模式还须调用 UIUX。真实浏览器验收使用浏览器 Skill。缺少必要 Skill 时立即补调,成功返回后重试原操作。'};
91
123
  return {continue:true};
92
124
  }
93
125
  let identity,installed;
94
126
  try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
95
127
  const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
96
128
  if(options.event==='PreToolUse'&&project&&path.isAbsolute(project)){
97
- const native=nativeCommand(payload.tool_name,payload.tool_input,project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
129
+ const native=nativeCommand(payload.tool_name,payload.tool_input,payload.cwd||project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,payload.cwd==='/'?project:payload.cwd||project);
98
130
  if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
99
131
  }
100
132
  throw error;
@@ -107,26 +139,32 @@ function handle(payload,options) {
107
139
  try {
108
140
  source=eventSource(payload,record,project);
109
141
  }catch(error){
110
- const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
111
- 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}`};
142
+ // The outer diagnostic handler must persist and bound this failure as well;
143
+ // returning an uncounted denial here used to create an endless repair loop.
112
144
  throw error;
113
145
  }
114
146
  const latest=source.prompts.at(-1);
115
147
  if(C.readOnly(latest.text)) {
116
- const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
148
+ const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
117
149
  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.'};
118
150
  return {continue:true};
119
151
  }
120
152
  const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
121
- const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
153
+ let index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
154
+ if(Object.values(index.bindings).some(entry=>['required','ready-to-retry'].includes(entry.recovery?.status))) {
155
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
156
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
157
+ try{index=JSON.parse(C.read(file));if(settleNativeRetries(index,source,{project,sessionId:payload.session_id}))saveIndex(stateDirectory,index);}
158
+ finally{fs.rmdirSync(lock);}
159
+ }
122
160
  const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
123
161
  if(options.event==='PreToolUse'&&command?.operation==='create'&&(installed?.schemaVersion===2||E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text))){
124
162
  const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
125
163
  const hostAt=command.argv.indexOf('--host');
126
164
  if(at<0||command.argv[at+1]!==expected||installed?.schemaVersion===2&&(hostAt<0||command.argv[hostAt+1]!==identity.host))return {deny:true,reason:`New ${identity.host} prototypes require --host ${identity.host} --execution-policy ${expected} and fixed --execution-assertions. Existing artifacts keep their original contracts.`};
127
165
  }
128
- const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
129
- const existing=directory?index.bindings[directory]:null;
166
+ const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
167
+ let existing=directory?index.bindings[directory]:null;
130
168
  const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
131
169
  let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
132
170
  const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
@@ -145,24 +183,28 @@ function handle(payload,options) {
145
183
  try{const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');const result=Recovery.record(fresh,recoveryContext(directory),answer);if(fresh.bindings[directory]&&result.recovery)fresh.bindings[directory].recovery=result.recovery;saveIndex(stateDirectory,fresh);return result;}
146
184
  finally{fs.rmdirSync(lock);}
147
185
  };
148
- if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
186
+ const automatic=options.event==='PreToolUse'&&directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)&&
187
+ (command||G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory))&&command?.operation!=='execution-begin'&&
188
+ (!existing||existing.entryLine!==latest.line||existing.contextLine!==source.contextLine||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
189
+ if(options.event==='PreToolUse'&&command?.operation==='execution-begin'||automatic) {
149
190
  assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
150
191
  const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
151
192
  assert.equal(fixed.host,identity.host,'This prototype has a fixed contract for another host; inspect it or explicitly migrate and revalidate, do not inherit its invocation proof');
152
193
  const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
153
- assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
194
+ assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text)||automatic&&require('./execution-diagnostics.cjs').intent(latest.text),'Native prototype Skill or actual shortcut entry required');
154
195
  const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
155
196
  const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
156
- 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,
197
+ index.bindings[directory]={schemaVersion:1,...(automatic?{association:'automatic'}:{}),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,
157
198
  operation:meta.revision==='r001'?'create':'revise',host:identity.host,identitySource:identity.identitySource,product:record.product,productVersion:identity.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}};
158
199
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
159
- try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
200
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
160
201
  try {
161
202
  const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
162
203
  saveIndex(stateDirectory,fresh);
163
204
  F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
164
205
  }finally{fs.rmdirSync(lock);}
165
- return {continue:true};
206
+ existing=index.bindings[directory];
207
+ if(!automatic)return {continue:true};
166
208
  }
167
209
  if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
168
210
  const fixed=X.policy(directory),answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd,policy:fixed});
@@ -172,16 +214,16 @@ function handle(payload,options) {
172
214
  // failures produce recovery feedback; outer tool success is insufficient.
173
215
  if(callbackCall?.resultLine&&!callbackCall.success){
174
216
  const missing=command.operation==='execution-begin'?['execution-begin']:['execution-evidence'];
175
- return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory})});
217
+ return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})});
176
218
  }
177
219
  if(command.operation==='execution-begin')return respond({continue:true});
178
220
  }
179
221
  if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)){
180
- if(!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
222
+ if(existing?.association!=='automatic'&&!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
181
223
  const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
182
224
  if(checked.status==='failed'){
183
225
  const missing=Recovery.missing({source,binding:existing,policy:fixed,stage});if(!missing.length)missing.push('execution-evidence');
184
- return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory})+' '+checked.errors.join('; ')});
226
+ return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})+' '+checked.errors.join('; ')});
185
227
  }
186
228
  }
187
229
  if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event)){
@@ -208,4 +250,51 @@ function handle(payload,options) {
208
250
  }finally{fs.rmdirSync(lock);}
209
251
  return {continue:true};
210
252
  }
253
+ function handle(payload,options){
254
+ const D=require('./execution-diagnostics.cjs');
255
+ payload={...payload,tool_name:toolName(payload.tool_name)};
256
+ let diagnostic;
257
+ try{
258
+ if(options.event==='UserPromptSubmit')D.remember(payload,options);
259
+ diagnostic=D.context(payload,options);
260
+ }catch{/* The authoritative dispatcher still checks registration and identity. */}
261
+ const toolEvent=['PreToolUse','PostToolUse'].includes(options.event);
262
+ const cwd=diagnostic&&toolCwd(payload,diagnostic.record,diagnostic.project);
263
+ const command=diagnostic&&nativeCommand(payload.tool_name,payload.tool_input,cwd);
264
+ const root=diagnostic&&(command?.root||prototypeForWrite(diagnostic.project,payload.tool_name,payload.tool_input,cwd));
265
+ const mutation=command&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(command.operation)||root&&G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},root);
266
+ if(diagnostic&&toolEvent&&['Write','Edit','MultiEdit'].includes(payload.tool_name)&&!mutation)return {continue:true};
267
+ if(diagnostic?.task.readOnly&&toolEvent)return options.event==='PreToolUse'&&mutation?{deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'}:{continue:true};
268
+ if(diagnostic&&!diagnostic.task.active&&toolEvent&&!mutation)return {continue:true};
269
+ if(diagnostic&&mutation&&!diagnostic.task.active)diagnostic.task={...diagnostic.task,active:true};
270
+ const guarded=mutation?D.guard(diagnostic,payload):null;if(guarded)return guarded;
271
+ if(D.blocked(diagnostic)&&toolEvent&&!mutation)return {continue:true};
272
+ try{
273
+ const answer=dispatch(payload,diagnostic?{...options,nativeProject:diagnostic.project}:options);
274
+ if(toolEvent&&!answer.deny&&!answer.feedback)D.observed(diagnostic,payload);
275
+ if(options.event==='PostToolUse'&&command?.operation==='execution-begin'&&!answer.deny&&root&&fs.existsSync(path.join(root,'qa/execution-context.json'))){
276
+ const pointer=F.json(path.join(root,'qa/execution-context.json'));
277
+ const source=diagnostic&&C.source(payload,diagnostic.record,diagnostic.project);
278
+ if(pointer&&completedBegin(source,{...pointer,entryLine:source.prompts.at(-1).line,contextLine:source.contextLine,directory:root}))D.recover(diagnostic);
279
+ }
280
+ return answer;
281
+ }catch(error){
282
+ if(diagnostic){const failure=D.failure(diagnostic,payload,error);if(!mutation&&failure.deny){const {deny,reason,...rest}=failure;return {...rest,continue:true,context:reason};}return failure;}
283
+ if(options.event==='PreToolUse') {
284
+ // A damaged/absent task source must not turn a protected write into exit 1
285
+ // (which CodeBuddy treats as non-blocking). This is a failure-only fallback,
286
+ // not a successful task association or invocation receipt.
287
+ const cwd=payload.cwd==='/'&&options.scope==='project'?path.dirname(options.base):payload.cwd;
288
+ if(cwd&&path.isAbsolute(cwd)) {
289
+ const parsed=nativeCommand(payload.tool_name,payload.tool_input,cwd),input=payload.tool_input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
290
+ let target=typeof file==='string'?path.dirname(path.resolve(cwd,file)):parsed?.root;
291
+ while(target&&target!==path.dirname(target)) {
292
+ if(fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target)&&(parsed||G.uiWrite({name:payload.tool_name,input,cwd},target)))return {deny:true,reason:'原型 Hook 运行异常,当前操作未放行:'+error.message+'。请停止自动重试并交由维护入口处理;原型会话不排查 Hook。'};
293
+ target=path.dirname(target);
294
+ }
295
+ }
296
+ }
297
+ throw error;
298
+ }
299
+ }
211
300
  module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+ // Protocol failures exist before a run binding. Never turn them into invocation proof.
3
+ const fs=require('node:fs'),path=require('node:path');
4
+ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
5
+ const intent=text=>typeof text==='string'&&!C.readOnly(text)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(text)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(text));
6
+ const read=file=>fs.existsSync(file)?JSON.parse(C.read(file)):null;
7
+ function location(payload,options){
8
+ if(!payload.session_id||!path.isAbsolute(options.base||''))return null;
9
+ const installation=read(path.join(options.base,'state/i3-prototype/installation.json'));if(!installation)return null;
10
+ const identity=H.identify(payload,installation,options),pointer=`state/i3-prototype/tasks/${E.sha(identity.host+':'+identity.product+':'+payload.session_id).slice(0,24)}.json`;
11
+ const previous=read(F.inside(options.base,pointer));
12
+ let project=options.scope==='project'?path.dirname(options.base):previous?.project;
13
+ if(!project)project=H.isIDE(payload)&&payload.cwd==='/'?E.ideWorkspace(payload.transcript_path,payload.session_id):payload.cwd;
14
+ if(!project||!path.isAbsolute(project))return null;project=fs.realpathSync(project);
15
+ if(payload.cwd&&payload.cwd!=='/'){
16
+ const relative=path.relative(project,fs.realpathSync(payload.cwd));if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
17
+ }
18
+ const record={...installation,...identity,base:options.base,registrationDigest:E.sha(JSON.stringify(installation)),settingsDigest:E.sha(C.read(path.join(options.base,'settings.json')))};
19
+ return {project,record,identity,pointer,previous,directory:C.sessionDirectory(record,project,payload.session_id)};
20
+ }
21
+ function context(payload,options,{source,prompt}={}){
22
+ const where=location(payload,options);if(!where)return null;
23
+ let native=source;if(!native)try{native=C.source(payload,where.record,where.project);}catch{/* Failure diagnostics must survive unreadable native history. */}
24
+ const latest=native?.prompts.at(-1),text=prompt??latest?.text??where.previous?.text;
25
+ if(typeof text!=='string')return null;
26
+ const digest=latest?.digest||E.sha(text),entryLine=latest?.line||where.previous?.entryLine||null;
27
+ const taskKey=E.sha(JSON.stringify([payload.session_id,payload.prompt_id||null,entryLine,digest]));
28
+ // A callback without prompt_id reuses only the exact observed native task.
29
+ const task=payload.hook_event_name!=='UserPromptSubmit'&&where.previous&&where.previous.digest===digest&&where.previous.entryLine===entryLine?where.previous:
30
+ {schemaVersion:1,taskKey,sessionId:payload.session_id,project:where.project,host:where.identity.host,product:where.identity.product,text,digest,entryLine,active:intent(text),readOnly:C.readOnly(text),at:new Date().toISOString()};
31
+ return {...where,source:native,task};
32
+ }
33
+ function remember(payload,options){
34
+ const value=context(payload,options,{prompt:payload.prompt});if(!value)return;
35
+ F.write(options.base,value.pointer,value.task);
36
+ }
37
+ function state(value){return read(path.join(value.directory,'diagnostics.json'))||{schemaVersion:1,tasks:{}};}
38
+ function current(value){return value&&state(value).tasks[value.task.taskKey];}
39
+ function blocked(value){return current(value)?.status==='blocked';}
40
+ function message(failure){return `原型 Hook 运行异常${failure.status==='blocked'?',已达到自动恢复上限':''}:${failure.reason}。请简要报告当前原型及未验收项,由维护入口处理;不要在原型任务中检查日志、修复 Hook 或重复绑定,也不能宣称已验证。`;}
41
+ function failure(value,payload,error){
42
+ if(!value?.task.active)return {continue:true};
43
+ return F.withLock(value.directory,'.diagnostics.lock',()=>{
44
+ const store=state(value),key=value.task.taskKey;
45
+ const phase=/JSON|transcript|source|history|日志/i.test(error.message)?'native-source':'native-request';
46
+ const eventKey=E.sha(JSON.stringify([key,payload.tool_use_id||payload.call_id||payload.hook_event_name,payload.tool_name||null,payload.tool_input||null,phase]));
47
+ const item=store.tasks[key]||{schemaVersion:1,taskKey:key,total:0,counts:{},events:{},status:'required'};
48
+ if(!item.events[eventKey]&&item.status!=='blocked'){
49
+ if(item.total>=6||(item.counts[phase]||0)>=2)item.status='blocked';
50
+ else {item.total++;item.counts[phase]=(item.counts[phase]||0)+1;item.status='required';}
51
+ Object.assign(item,{eventKey,reason:String(error.message).slice(0,2000),phase,at:new Date().toISOString(),host:value.identity.host,product:value.identity.product,productVersion:value.identity.productVersion,
52
+ callMode:payload.agent_type==='ptc'||value.source?.calls.some(c=>c.name==='REPL')?'ptc-repl':value.identity.product==='ide'?'native-ide':'direct',sessionId:payload.session_id,project:value.project,
53
+ registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,transcriptPath:payload.transcript_path});
54
+ item.events[eventKey]=true;store.tasks[key]=item;F.write(value.directory,'diagnostics.json',store);
55
+ }
56
+ const result={reason:message(item),failure:item};
57
+ return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:payload.hook_event_name==='Stop'?{...result,feedback:true}:{...result,continue:true,context:result.reason};
58
+ });
59
+ }
60
+ function guard(value,payload){
61
+ if(!value?.task.active||!blocked(value))return null;
62
+ const item=current(value),reason=message(item);
63
+ // Do not trap the whole host. Read tools and explicit state/check commands remain available.
64
+ const command=require('./native-command.cjs').nativeCommand(payload.tool_name,payload.tool_input,value.project);
65
+ const managed=command&&['create','execution-begin','revise','replan','build','record-run','record-devtools-review','finalize'].includes(command.operation)||['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(payload.tool_name);
66
+ if(payload.hook_event_name==='PreToolUse'&&managed)return {deny:true,reason,failure:item};
67
+ return null;
68
+ }
69
+ function recover(value){
70
+ if(!value)return;
71
+ return F.withLock(value.directory,'.diagnostics.lock',()=>{const store=state(value),item=store.tasks[value.task.taskKey];
72
+ if(item&&item.status!=='blocked'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
73
+ });
74
+ }
75
+ function observed(value,payload){
76
+ if(!value?.task.active)return;
77
+ const source=C.source(payload,value.record,value.project),at=new Date().toISOString();
78
+ const entry={schemaVersion:1,sessionId:payload.session_id,taskKey:value.task.taskKey,project:value.project,projectKey:C.projectKey(value.project),host:value.identity.host,product:value.identity.product,
79
+ productVersion:value.identity.productVersion,identitySource:value.identity.identitySource,registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,
80
+ source:{path:source.path,bytes:source.bytes,digest:source.digest},callMode:source.callMode||'direct',skills:Object.fromEntries(['prototype','frontend','antdv','uiux','browser'].map(key=>[key,E.loaded(source.calls,key).length])),
81
+ lastHook:{event:payload.hook_event_name,at}};
82
+ F.write(value.directory,'observation.json',entry);
83
+ }
84
+ function latest({base,project,host,record}){
85
+ const parent=path.join(base,'state/i3-prototype',C.projectKey(project));let found=null;
86
+ if(!fs.existsSync(parent))return null;
87
+ for(const name of fs.readdirSync(parent)){
88
+ if(!/^[a-f0-9]{24}$/.test(name))continue;
89
+ const data=read(path.join(parent,name,'diagnostics.json'));if(!data)continue;
90
+ for(const item of Object.values(data.tasks||{}))if(item.host===host&&item.project===project&&item.registrationDigest===record.registrationDigest&&item.settingsDigest===record.settingsDigest&&item.status!=='recovered'&&(!found||item.at>found.at))found=item;
91
+ }
92
+ if(!found)return null;const {events,...summary}=found;return summary;
93
+ }
94
+ module.exports={intent,context,remember,current,blocked,failure,guard,recover,observed,latest};
@@ -38,7 +38,11 @@ function identity(d) {
38
38
  same(d.binding.policyDigest,d.policy.digest,'Execution policy differs');same(d.binding.inputDigest,d.current.inputDigest,'Inputs changed; begin a new execution run');
39
39
  for(const key of ['inputDigest','candidateDigest','revision'])same(d.proof[key],key==='candidateDigest'?d.current.digest:d.current[key],`Current ${key} differs`);
40
40
  assert.ok(d.source.prompts.some(p=>p.line===d.binding.entryLine),'Native user task source missing');
41
- assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
41
+ if(d.binding.association==='automatic'){
42
+ const prompt=d.source.prompts.find(p=>p.line===d.binding.entryLine);
43
+ same(prompt?.digest,d.binding.promptDigest,'Automatic execution context belongs to a different native task');
44
+ same(d.binding.contextLine,d.source.contextLine,'Automatic execution context changed');
45
+ }else assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
42
46
  if(d.policy.id===P.AUTO_ID){same(d.binding.host,d.policy.host,'Actual native host differs from the fixed policy');assert.ok(d.binding.identitySource&&d.binding.identitySource!=='legacy-profile','Automatic policy requires actual native host identity');}
43
47
  if(d.binding.recovery){
44
48
  const recovery=d.binding.recovery;
@@ -158,8 +162,49 @@ function browser(d) {
158
162
  A.compare(A.extract(A.resultData(call.result),check.pointer),check);
159
163
  }
160
164
  }
165
+ function automatic(d,stage){
166
+ P.validate(d.policy);
167
+ const prompt=d.source.prompts.at(-1);same(d.binding.entryLine,prompt?.line,'Current prototype task differs');same(d.binding.promptDigest,prompt?.digest,'Current prototype task changed');
168
+ same(d.binding.inputDigest,d.current.inputDigest,'Prototype inputs changed');same(d.binding.contextLine,d.source.contextLine,'Native context changed');
169
+ assert.notEqual(d.binding.recovery?.status,'blocked','Skill recovery budget exhausted');
170
+ const needs=P.requirements(d.policy,stage),calls=d.source.calls.filter(c=>c.sessionId===d.binding.sessionId&&c.success&&c.resultLine&&c.requestLine>d.source.contextLine);
171
+ const refs={},required=needs.filter(k=>['frontend','antdv','uiux','browser'].includes(k));
172
+ if(needs.includes('implementation'))required.push('frontend',...(d.policy.officialCallable?['antdv']:[]),...(d.policy.mode==='professional'?['uiux']:[]));
173
+ const writes=calls.filter(c=>c.requestLine>=d.binding.entryLine&&uiWrite({...c,cwd:c.cwd||d.binding.project},d.binding.directory));
174
+ for(const key of [...new Set(required)]){
175
+ const loaded=calls.filter(c=>c.skill===key),firstWrite=writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity;
176
+ const call=['frontend','antdv','uiux'].includes(key)?loaded.find(c=>c.resultLine<firstWrite):loaded.at(-1);
177
+ assert.ok(call,`Actual ${key} Skill invocation required before UI implementation; invoke it and retry this operation`);
178
+ if(['frontend','antdv','uiux'].includes(key)&&writes.length)assert.ok(call.resultLine<Math.min(...writes.map(c=>c.requestLine)),`${key} Skill must precede business UI writes`);
179
+ refs[key]={toolCallId:call.toolCallId,resultDigest:call.resultDigest};
180
+ }
181
+ if(needs.includes('implementation'))assert.ok(writes.length,'Actual business UI implementation write required');
182
+ if(needs.includes('build')){
183
+ assert.ok(d.build&&d.build.candidateDigest===d.current.digest,'Current prototype build required');
184
+ const built=calls.find(c=>{const commandInfo=command.nativeCommand(c.name,c.input,c.cwd||d.binding.project);const result=E.resultValue(c.result);return commandInfo?.operation==='build'&&commandInfo.root===d.binding.directory&&result?.buildId===d.build.buildId&&result?.buildDigest===d.build.buildDigest;});
185
+ assert.ok(built,'Actual current build result required');refs.build={toolCallId:built.toolCallId,resultDigest:built.resultDigest};
186
+ }
187
+ if(needs.includes('browser-results')){
188
+ const loaded=calls.filter(c=>c.skill==='browser').at(-1),built=calls.find(c=>c.toolCallId===refs.build?.toolCallId);
189
+ const observed=calls.filter(c=>/^mcp__.*chrome.*devtools.*__/.test(c.name)&&c.requestLine>Math.max(loaded.resultLine,built.resultLine));
190
+ assert.ok(observed.some(c=>/__(?:new_page|navigate_page)$/.test(c.name)),'Actual browser navigation required');
191
+ assert.ok(observed.some(c=>c.name.endsWith('__evaluate_script')&&E.resultText(c.result).includes(d.current.digest)),'Actual browser candidate identity required');
192
+ for(const method of ['take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_stop_trace'])assert.ok(observed.some(c=>c.name.endsWith('__'+method)),`Actual browser ${method} required`);
193
+ assert.ok(d.business&&d.business.candidateDigest===d.current.digest&&d.business.buildId===d.build.buildId,'Current business browser report required');
194
+ const fixed=A.validate(d.assertions,d.business.journeys.map(j=>j.id));
195
+ for(const scenario of fixed.scenarios)for(const check of scenario.checks){
196
+ const observation=d.business.generationObservations?.find(o=>o.scenarioId===scenario.scenarioId);
197
+ const results=observed.filter(c=>c.name.endsWith('__'+check.method)&&(!observation||observation.toolCallIds.includes(c.toolCallId)));
198
+ assert.ok(results.some(c=>{try{A.compare(A.extract(A.resultData(c.result),check.pointer),check);return true;}catch{return false;}}),`Actual browser assertion missing: ${scenario.scenarioId} ${check.pointer}`);
199
+ }
200
+ }
201
+ return {status:'passed',stage,policyDigest:d.policy.digest,runId:d.binding.runId,sessionId:d.binding.sessionId,projectKey:d.binding.projectKey,host:d.policy.host,productForm:d.binding.product,productVersion:d.binding.productVersion,installationScope:d.binding.scope,
202
+ operation:d.binding.operation,mode:d.policy.mode,inputDigest:d.current.inputDigest,candidateDigest:d.current.digest,revision:d.current.revision,association:'automatic',skills:refs,source:{path:d.source.path,bytes:d.source.bytes,digest:d.source.digest},
203
+ ...(d.source.callbackJournal?{callbackJournal:d.source.callbackJournal}:{}),...(d.binding.recovery?{recovery:d.binding.recovery}:{}),invocationVerification:'native-source-correlated',tamperProof:false};
204
+ }
161
205
  function evaluate(data,stage) {
162
206
  try {
207
+ if(data.binding?.association==='automatic')return automatic(data,stage);
163
208
  identity(data);const needs=P.requirements(data.policy,stage);
164
209
  if(needs.includes('frontend')||needs.includes('implementation'))frontend(data,needs.includes('implementation'));
165
210
  if(needs.some(n=>['antdv','official-source','uiux'].includes(n)))design(data);
@@ -5,15 +5,15 @@ const N=require('./native-command.cjs');
5
5
  const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
6
6
  const blockedReason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
7
7
  function missing({source,binding,policy,stage}){
8
- const unbound=!N.completedBegin(source,binding);
8
+ const unbound=binding?.association!=='automatic'&&!N.completedBegin(source,binding);
9
9
  const required=stage&&policy?P.requirements(policy,stage).filter(key=>Object.hasOwn(names,key)):['frontend',...(policy?.id===P.AUTO_ID?[...(policy.officialCallable?['antdv']:[]),...(policy.mode==='professional'?['uiux']:[])]:[])];
10
10
  const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
11
11
  return [...(unbound?['execution-begin']:[]),...required.filter(key=>!E.loaded(source.calls,key).some(call=>call.sessionId===sessionId&&call.requestLine>source.contextLine&&(source.stableThroughLine===undefined||call.resultLine<=source.stableThroughLine)))];
12
12
  }
13
- function instruction(keys,{directory}={}){
13
+ function instruction(keys,{directory,automatic=false}={}){
14
14
  const skills=keys.filter(key=>names[key]).map(key=>names[key]);
15
15
  const begin=keys.includes('execution-begin')?`先为当前任务独立执行 execution-begin${directory?':\n'+N.beginCommand(directory)+'\n':'。'}不得拼接 cd &&、管道、重定向或 head;等待 status=bound 且退出码为 0,再重试原操作。`:'';
16
- return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。':''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
16
+ return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?(automatic?'按本次构建或交互验收返回的具体缺项修正原型后重试;调用记录由 Hooks 自动采集,不检查或修复 Hooks。':'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。'):''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
17
17
  }
18
18
  function record(index,context,answer){
19
19
  const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
@@ -49,7 +49,8 @@ function record(index,context,answer){
49
49
  }
50
50
  if(!Object.keys(state.pending).length&&state.status!=='recovered'){
51
51
  const matchingRetry=!state.expectedRetry||Object.entries(state.expectedRetry).every(([key,value])=>context.retry?.[key]===value);
52
- state.status=context.retrySucceeded&&matchingRetry&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
52
+ const afterSkills=state.invocations.every(call=>context.retry?.requestLine>call.resultLine);
53
+ state.status=context.retrySucceeded&&matchingRetry&&afterSkills&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
53
54
  }
54
55
  }
55
56
  return remember({...answer});
@@ -12,7 +12,9 @@ function policy(directory) {
12
12
  const acceptance=json(inside(directory,'inputs/acceptance.json'));require('./execution-assertions.cjs').validate(json(inside(directory,'inputs/execution-assertions.json')),[...acceptance.requiredScenarios,...acceptance.frameworkScenarios]);return value;
13
13
  }
14
14
  function binding(directory) {
15
- const recover=Recovery.instruction(['execution-begin'],{directory});
15
+ const metadata=inside(directory,'prototype.json');
16
+ const automaticExpected=fs.existsSync(metadata)&&json(metadata).runtime?.executionPolicy===P.AUTO_ID;
17
+ const recover=automaticExpected?'Installed Hooks collect this automatically on the next prototype UI operation. Load the required Skills and continue the UI task; report an infrastructure fault through maintenance if collection fails.':Recovery.instruction(['execution-begin'],{directory});
16
18
  const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; '+recover);
17
19
  const project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product,host:pointer.host||'codebuddy'});assert.ok(selected,'Hooks are not enabled or are shadowed');
18
20
  assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
@@ -56,14 +56,15 @@ function state(directory,options,lib) {
56
56
  }
57
57
  if(build.status==='current')commands.preview=command('preview',['--build-id',build.buildId]);
58
58
  if(devtoolsReview.required)commands.recordDevtoolsReview=command('record-devtools-review',['--report',path.join(directory,'qa/devtools-review-input.json')]);
59
- if(meta.runtime.executionPolicy){commands.executionBegin=command('execution-begin');commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
59
+ const executionContext=read(directory,'qa/execution-context.json');
60
+ if(meta.runtime.executionPolicy&&executionContext){commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
60
61
  let generation=null;
61
62
  if(meta.runtime.generationContract===1)try{const fixed=json(inside(directory,'inputs/generation.json'));const plan=require('./generation-contract.cjs').view(fixed);generation={presentation:fixed.presentation,pendingDecisions:fixed.pendingDecisions,pendingReferenceElements:fixed.pendingReferenceElements.length,referenceReview:plan.referenceReview,implementation:require('./generation-evidence.cjs').validateImplementation(directory,fixed,json(inside(directory,'design-system/implementation.json')),{frameworkMap:json(inside(directory,'inputs/framework.json')).componentMap})};}catch(error){generation={status:'failed',errors:[error.message]};}
62
63
  const nextAction=ownership?'wait-for-owner':structure.status!=='passed'?'repair-structure':generation&&(generation.pendingDecisions.length||generation.pendingReferenceElements)?'resolve-generation-decisions':generation?.implementation.status==='pending'?'implement-business':meta.runtime.contentReference&&meta.business?.status!=='adapted'?'implement-business':!['reviewed','no-match','skipped'].includes(design.status)?'review-design':build.status!=='current'?'build':browser.status!=='current'?(meta.runtime.contentReference?'business-browser-review':'browser'):devtoolsReview.required&&devtoolsReview.status!=='current'?'devtools-review':deliveryCurrent?'return-result':'finalize';
63
64
  if(generation)commands.replan=command('replan',['--scope',path.join(directory,'inputs/scope.json'),'--revision',nextRevision,...expected]);
64
65
  const fixedScope=json(inside(directory,'inputs/scope.json'));
65
66
  const scopes=meta.runtime.generationContract===1?{sourceFile:'inputs/scope.json',schemaVersion:fixedScope.schemaVersion,title:fixedScope.title,moduleIds:fixedScope.moduleIds,pages:fixedScope.pages.map(page=>({id:page.id,title:page.title,referencePage:page.referencePage,regions:page.regions.map(region=>({id:region.id,pattern:region.pattern||region.mode,fields:region.fields?.length||0,actions:region.actions?.length||0}))})),requirements:fixedScope.requirements.map(item=>({id:item.id,pages:item.pages,source:{type:item.source.type,reference:item.source.reference}}))}:fixedScope;
66
- return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction:meta.runtime.executionPolicy&&execution.status==='failed'?'resolve-execution-evidence':nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed commands; no source-code inspection is required to recover flags or digests.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy invocation fields retain their original meaning. New policy checks are reported separately in execution; native-source correlation is not tamper-proof attestation.']};
67
+ return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed prototype commands; installed Hooks execute automatically.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy execution commands remain available for compatibility; prototype work does not require manual binding or Hook diagnosis.']};
67
68
  }
68
69
 
69
70
  function finalize(directory,options,lib) {
@@ -85,7 +86,7 @@ function finalize(directory,options,lib) {
85
86
  const delivery={schemaVersion:1,status:'complete',operation:'finalize',id:meta.id,revision:before.revision,candidateDigest:before.digest,inputDigest:before.inputDigest,framework:meta.framework,template:meta.template,expertMode:meta.expertMode,expert:meta.expert.status,verification:'passed',review:review.status,checks:{structure:'passed',acceptance:'passed',build:'current',browser:'current'},buildId:report.buildId,buildDigest:report.buildDigest,report:latest.report,executor:report.executor,tool:report.tool,handoff:path.join(directory,'HANDOFF.md'),receipt:'qa/delivery.json',openQuestions:scope.openQuestions||[],humanConfirmationRequired:review.status!=='approved'};
86
87
  const execution=require('./execution-runtime.cjs').requireStage(directory,'delivery',lib);
87
88
  delivery.invocationVerification=execution.invocationVerification;delivery.tamperProof=false;
88
- if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));}
89
+ if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=execution.association==='automatic'?F.hash(JSON.stringify(execution)):F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));delivery.executionEvidenceSource=execution.association==='automatic'?'automatic-native-observation':'legacy-execution-evidence';}
89
90
  delivery.browserReportDigest=F.hash(fs.readFileSync(inside(directory,latest.report)));
90
91
  if(devtoolsReview.required){delivery.devtoolsReview=devtoolsReview.report;delivery.devtoolsReviewDigest=devtoolsReview.reportDigest;delivery.checks.devtoolsReview='current';delivery.invocationEvidence=execution.status==='passed'?'Native response IDs correlated with the configured host transcript; not tamper-proof.':'Verify actual Skill/MCP calls independently in the host transcript';}
91
92
  const start='<!-- native-delivery:start -->',end='<!-- native-delivery:end -->';
@@ -28,8 +28,17 @@ function write(root,relative,value) {
28
28
  try { fs.writeFileSync(tmp,typeof value==='string'?value:JSON.stringify(value,null,2)+'\n',{flag:'wx'}); fs.renameSync(tmp,file); }
29
29
  finally { if(fs.existsSync(tmp)) fs.unlinkSync(tmp); }
30
30
  }
31
+ function withLock(root,name,work) {
32
+ fs.mkdirSync(root,{recursive:true,mode:0o700});const lock=inside(root,name);let acquired=false;
33
+ for(let attempt=0;attempt<100;attempt++) {
34
+ try{fs.mkdirSync(lock);acquired=true;break;}
35
+ catch(error){if(error.code!=='EEXIST')throw error;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}
36
+ }
37
+ assert.ok(acquired,'Concurrent native audit update; no success recorded');
38
+ try{return work();}finally{fs.rmdirSync(lock);}
39
+ }
31
40
  function entries(root,files) { return Object.fromEntries([...new Set(files)].sort().map(file=>[file,hash(fs.readFileSync(inside(root,file)))])); }
32
41
  const cacheRoots = new Set(['node_modules','dist','.vite','.git','coverage',...['app','starter'].flatMap(root=>['node_modules','dist','.vite','.git','coverage'].map(name=>`${root}/${name}`))]);
33
42
  // A source/public subdirectory named "dist", or an imported .log file, is still source.
34
43
  const excluded = (relative,name) => cacheRoots.has(relative) || name==='.DS_Store' || ['app/tsconfig.tsbuildinfo','starter/tsconfig.tsbuildinfo'].includes(relative);
35
- module.exports={fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded};
44
+ module.exports={fs,path,crypto,assert,hash,json,id,inside,walk,write,withLock,entries,excluded};
@@ -50,5 +50,10 @@ async function main(argv=process.argv.slice(2)) {
50
50
  default: throw new Error(`Unknown operation: ${operation}`);
51
51
  }
52
52
  }
53
- if(require.main===module) main().then(result=>{if(result!==null){process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}}).catch(error=>{process.stderr.write(JSON.stringify({status:'failed',operation:process.argv[2]||'analyze',errors:[error.message]})+'\n');process.exitCode=1;});
53
+ function capture(result,exitCode){
54
+ const args=process.argv.slice(2),at=args.indexOf('--project');let project=at>=0&&args[at+1]?path.resolve(args[at+1]):process.cwd();
55
+ const target=args[1];if(target&&path.isAbsolute(target)&&target.includes(path.sep+'prototypes'+path.sep))project=target.slice(0,target.indexOf(path.sep+'prototypes'+path.sep));
56
+ require('./ptc-callbacks.cjs').captureResult({script:__filename,argv:args,project,result,exitCode});
57
+ }
58
+ if(require.main===module) main().then(result=>{if(result!==null){const code=result.status==='failed'?1:0;capture(result,code);process.stdout.write(JSON.stringify(result,null,2)+'\n');process.exitCode=code;}}).catch(error=>{const result={status:'failed',operation:process.argv[2]||'analyze',errors:[error.message]};try{capture(result,1);}catch{/* The primary failure remains authoritative. */}process.stderr.write(JSON.stringify(result)+'\n');process.exitCode=1;});
54
59
  module.exports={main};
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Capture a real read-only query result when the host's Bash callback omits stdout.
4
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{spawnSync}=require('node:child_process');
5
+ const J=require('./ptc-callbacks.cjs');
6
+ function main(argv=process.argv.slice(2)){
7
+ const args=[...argv];assert.equal(args.shift(),'--project','Explicit query project required');const project=fs.realpathSync(args.shift());assert.equal(args.shift(),'--','Separate the executable and its arguments');
8
+ const executable=args.shift();assert.ok(path.isAbsolute(executable)&&fs.statSync(executable).isFile(),'Use the detected absolute local executable; this command does not install tools');
9
+ const run=spawnSync(executable,args,{cwd:project,encoding:'utf8',timeout:30000,maxBuffer:8*1024*1024});
10
+ const result={type:'execute_command_result',exitCode:run.status??1,stdout:run.stdout||'',stderr:run.stderr||'',...(run.error?{error:run.error.message}:{})};
11
+ J.captureResult({script:__filename,argv,project,result,exitCode:result.exitCode});return result;
12
+ }
13
+ module.exports={main};
14
+ if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result)+'\n');process.exitCode=result.exitCode;}catch(error){process.stderr.write(JSON.stringify({status:'failed',errors:[error.message]})+'\n');process.exitCode=1;}
@@ -99,5 +99,5 @@ async function main(argv = process.argv.slice(2)) {
99
99
  return operation === 'check' ? check(options) : operation==='query'?query(options):require('./optional-skill-install.cjs').install(options);
100
100
  }
101
101
  module.exports = { check, skillAt, antdvTool, query, parse, main };
102
- if (require.main === module) main().then(result => {process.stdout.write(JSON.stringify(result, null, 2) + '\n');if(result.status==='failed')process.exitCode=1;})
102
+ if (require.main === module) main().then(result => {const {options}=parse(process.argv.slice(2));require('./ptc-callbacks.cjs').captureResult({script:__filename,argv:process.argv.slice(2),project:path.resolve(options.project||process.cwd()),result,exitCode:result.status==='failed'?1:0});process.stdout.write(JSON.stringify(result, null, 2) + '\n');if(result.status==='failed')process.exitCode=1;})
103
103
  .catch(error => { process.stderr.write(JSON.stringify({ status: 'failed', errors: [error.message] }) + '\n'); process.exitCode = 1; });
@@ -56,8 +56,9 @@ function check(options,runtime={}) {
56
56
  const entries=runtime.entries||[];
57
57
  if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
58
58
  if(!/^[a-f0-9]{24}$/.test(name))continue;
59
- const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
60
- entries.push(...Object.values(JSON.parse(raw).bindings||{}));
59
+ const raw=R.fileIO.read(path.join(parent,name,'session.json'));
60
+ if(raw)entries.push(...Object.values(JSON.parse(raw).bindings||{}));
61
+ const observed=R.fileIO.read(path.join(parent,name,'observation.json'));if(observed)entries.push(JSON.parse(observed));
61
62
  }
62
63
  for(const entry of entries)if((entry.host||'codebuddy')===host&&entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
63
64
  if(observation) {
@@ -68,9 +69,12 @@ function check(options,runtime={}) {
68
69
  result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
69
70
  const observedProduct=observation.product||(record.product!=='auto'?record.product:null);
70
71
  assert.ok(['ide','cli'].includes(observedProduct),'Observed native product identity missing');
71
- Object.assign(result,{product:observedProduct,productVersion:observation.productVersion||'unreported',effective:effectiveByProduct[observedProduct]===true,identitySource:observation.identitySource||'legacy-profile',recovery:observation.recovery||null});
72
+ Object.assign(result,{product:observedProduct,productVersion:observation.productVersion||'unreported',effective:effectiveByProduct[observedProduct]===true,identitySource:observation.identitySource||'legacy-profile',recovery:observation.recovery||null,...(observation.callMode?{callMode:observation.callMode,skills:observation.skills}:{})});
72
73
  }
73
74
  const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
75
+ const failure=require('./execution-diagnostics.cjs').latest({base:current.base,project:current.project,host,record});
76
+ if(failure&&(!observation||failure.at>=observation.lastHook.at))return {...result,status:'failed',failure,errors:[failure.reason],transcript:{...result.transcript,status:'failed',invocationVerified:false},
77
+ next:failure.status==='blocked'?'修复宿主或适配后,由用户发起新的继续任务;只读诊断不消耗恢复次数。':'按 failure.reason 检查当前宿主协议;最多两次补调,不能将注册状态当作执行成功。'};
74
78
  return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
75
79
  activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
76
80
  next:'CLI: review /hooks; desktop: reload. Verify an actual Skill invocation and missing-Skill recovery in the chosen host.'};
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+ // Native Pre/Post callbacks are the evidence. REPL source and claimed returns are never interpreted.
3
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
4
+ const F=require('./native-files.cjs'),E=require('./execution-evidence.cjs'),C=require('./codebuddy-context.cjs'),P=require('./execution-policy.cjs');
5
+ const {toolName}=require('./native-tool-name.cjs');
6
+ const canonical=x=>Array.isArray(x)?x.map(canonical):x&&typeof x==='object'?Object.fromEntries(Object.keys(x).sort().map(k=>[k,canonical(x[k])])):x;
7
+ const digest=x=>E.sha(JSON.stringify(canonical(x)));
8
+ const fileFor=(record,project,sessionId)=>path.join(C.sessionDirectory(record,project,sessionId),'ptc-callbacks.json');
9
+ function load(file){
10
+ if(!fs.existsSync(file))return null;
11
+ assert.ok(!fs.lstatSync(file).isSymbolicLink()&&fs.statSync(file).size<=64*1024*1024,'Invalid PTC callback journal');return JSON.parse(fs.readFileSync(file,'utf8'));
12
+ }
13
+ function locked(file,work){
14
+ const folder=path.dirname(file);fs.mkdirSync(folder,{recursive:true,mode:0o700});const lock=file+'.lock';let acquired=false;
15
+ for(let n=0;n<40;n++){try{fs.mkdirSync(lock);acquired=true;break;}catch(error){if(error.code!=='EEXIST')throw error;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}}
16
+ assert.ok(acquired,'Concurrent native PTC callback; retry without inventing evidence');
17
+ try{return work();}finally{fs.rmdirSync(lock);}
18
+ }
19
+ function validate(store,source,{record,project,sessionId}){
20
+ assert.equal(store.schemaVersion,1,'Unsupported PTC callback schema');assert.equal(store.sessionId,sessionId,'PTC callback session differs');assert.equal(store.project,project,'PTC callback project differs');
21
+ assert.equal(store.registrationDigest,record.registrationDigest,'PTC callback registration digest changed');assert.equal(store.settingsDigest,record.settingsDigest,'PTC callback settings digest changed');
22
+ assert.equal(store.sourcePath,source.path,'PTC native transcript path changed');
23
+ assert.ok(Number.isSafeInteger(store.sequence)&&store.sequence>=0&&store.sequence<900000,'Invalid native callback sequence');
24
+ const sequences=new Set();
25
+ const sequence=value=>{assert.ok(Number.isSafeInteger(value)&&value>0&&value<=store.sequence&&!sequences.has(value),'Invalid or duplicate native callback sequence');sequences.add(value);};
26
+ for(const [id,item] of Object.entries(store.calls)) {
27
+ assert.equal(item.id,id,'Native callback call ID changed');assert.equal(item.sessionId,sessionId,'Native callback call session changed');
28
+ assert.equal(digest(item.pre),digest({id:item.id,name:item.name,input:item.input,sessionId:item.sessionId,parentId:item.parentId,cwd:item.cwd}),'Native callback request changed');
29
+ sequence(item.preSequence);
30
+ if(item.post){sequence(item.postSequence);assert.ok(item.postSequence>item.preSequence,'Native callback Post must follow Pre');}
31
+ else assert.equal(item.postSequence,undefined,'Native callback result sequence without a result');
32
+ }
33
+ for(const parent of Object.values(store.parents)){
34
+ const call=source.calls.find(c=>c.toolCallId===parent.toolCallId);
35
+ assert.ok(call?.name==='REPL'&&call.sessionId===sessionId,'Native parent REPL request missing or session differs');
36
+ assert.equal(call.requestDigest,parent.requestDigest,'Native parent request digest changed');assert.equal(call.requestLine,parent.requestLine,'Native parent request cursor changed');
37
+ assert.ok(source.prompts.some(p=>p.line===parent.promptLine&&p.digest===parent.promptDigest),'Native parent task source changed');
38
+ }
39
+ }
40
+ function read(source,{record,project,sessionId}){
41
+ source={...source,calls:source.calls.filter(c=>c.evidenceSource!=='native-hook-callback')};
42
+ const store=load(fileFor(record,project,sessionId));if(!store)return source;
43
+ validate(store,source,{record,project,sessionId});
44
+ const inner=[];
45
+ for(const item of Object.values(store.calls)){
46
+ const parent=store.parents[item.parentId];assert.ok(parent,'PTC parent callback anchor missing');
47
+ const name=toolName(item.name),input=item.input,skillName=name==='Skill'?input?.skill||input?.name||input?.command:null;
48
+ assert.ok(!source.calls.some(c=>c.toolCallId===item.id),'Duplicate native inner tool call ID');
49
+ const call={toolCallId:item.id,name,input,skillName:skillName||null,skill:P.skillKey(skillName),sessionId,cwd:item.cwd,contextLine:parent.contextLine,promptLine:parent.promptLine,
50
+ requestLine:parent.requestLine+item.preSequence/1e6,requestDigest:digest(item.pre),success:false,parentToolCallId:item.parentId,evidenceSource:'native-hook-callback'};
51
+ if(item.post){
52
+ let result=item.post.response;
53
+ if(item.cliResult){
54
+ const value=item.cliResult;assert.equal(E.sha(fs.readFileSync(value.script)),value.scriptDigest,'Captured CLI script bytes changed');
55
+ assert.equal(value.inputDigest,digest(item.input),'Captured CLI input differs');
56
+ assert.equal(item.post.response?.exitCode,value.exitCode,'Native Bash exit differs from captured CLI result');
57
+ result={type:'execute_command_result',stdout:typeof value.result==='string'?value.result:JSON.stringify(value.result),exitCode:value.exitCode};
58
+ }
59
+ call.result=result;call.resultLine=parent.requestLine+item.postSequence/1e6;call.resultDigest=digest({post:item.post,cliResult:item.cliResult||null});
60
+ call.success=!item.denied&&!result?.interrupted&&!result?.sandboxDenied&&!result?.signal&&E.successful({content:result});
61
+ }
62
+ inner.push(call);
63
+ }
64
+ return {...source,calls:[...source.calls,...inner].sort((a,b)=>a.requestLine-b.requestLine),callMode:'ptc-repl',callbackJournal:{path:fileFor(record,project,sessionId),digest:digest(store),tamperProof:false}};
65
+ }
66
+ function observe({payload,record,project,source}){
67
+ const sessionId=payload.session_id,name=toolName(payload.tool_name),id=payload.tool_use_id||payload.call_id;
68
+ assert.ok(sessionId&&id,'Native PTC session and tool call ID required');
69
+ assert.ok(!payload.agent_id&&!payload.parent_agent_id,'PTC main task cannot inherit subagent callbacks');
70
+ assert.ok(source.calls.every(c=>c.sessionId===sessionId),'Native PTC callback session differs');
71
+ const task=source.prompts.at(-1);assert.ok(task,'Native PTC user task missing');
72
+ const candidates=source.calls.filter(c=>c.name==='REPL'&&!c.resultLine&&c.requestLine>task.line&&c.contextLine===source.contextLine);
73
+ const direct=source.calls.find(c=>c.toolCallId===id);
74
+ if(direct&&!direct.evidenceSource&&direct.name!=='REPL')return source;
75
+ if(name==='REPL'&&payload.hook_event_name==='PostToolUse'&&direct?.name==='REPL')return read(source,{record,project,sessionId});
76
+ assert.equal(candidates.length,1,'Native PTC callback requires a unique pending parent REPL');
77
+ const parent=candidates[0],file=fileFor(record,project,sessionId);
78
+ locked(file,()=>{
79
+ const store=load(file)||{schemaVersion:1,sessionId,project,sourcePath:source.path,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,sequence:0,parents:{},calls:{}};
80
+ validate(store,source,{record,project,sessionId});
81
+ const next=()=>{assert.ok(++store.sequence<900000,'Native PTC callback journal limit reached');return store.sequence;};
82
+ if(name==='REPL'){
83
+ assert.equal(id,parent.toolCallId,'Native outer REPL call differs');assert.equal(digest(payload.tool_input||{}),digest(parent.input),'Native outer REPL input differs');
84
+ if(payload.hook_event_name==='PreToolUse')store.parents[id]||={toolCallId:id,requestDigest:parent.requestDigest,requestLine:parent.requestLine,promptLine:task.line,promptDigest:task.digest,contextLine:source.contextLine};
85
+ }else{
86
+ assert.ok(store.parents[parent.toolCallId],'Native parent PreToolUse callback missing');
87
+ const input=payload.tool_input||{},pre={id,name,input,sessionId,parentId:parent.toolCallId,cwd:payload.cwd};
88
+ if(payload.hook_event_name==='PreToolUse'){
89
+ if(store.calls[id])assert.equal(digest(store.calls[id].pre),digest(pre),'Conflicting native PTC call input or parent changed');
90
+ else store.calls[id]={...pre,pre,preSequence:next()};
91
+ }else if(payload.hook_event_name==='PostToolUse'){
92
+ const item=store.calls[id];assert.ok(item,'Paired native PTC PreToolUse request missing');assert.equal(digest(item.pre),digest(pre),'Native PTC Post input differs');
93
+ assert.ok(Object.hasOwn(payload,'tool_response'),'Native PTC Post response missing');const post={response:payload.tool_response};
94
+ if(item.post)assert.equal(digest(item.post),digest(post),'Conflicting native PTC Post result changed');
95
+ else {item.post=post;item.postSequence=next();}
96
+ }
97
+ }
98
+ F.write(path.dirname(file),path.basename(file),store);fs.chmodSync(file,0o600);
99
+ });
100
+ return read(source,{record,project,sessionId});
101
+ }
102
+ function captureResult({script,argv,project=process.cwd(),result,exitCode}){
103
+ // Only an actually running first-party entry may capture stdout. Match its exact pending native command.
104
+ const candidates=[path.join(project,'.codebuddy')];let folder=path.dirname(script);
105
+ for(let n=0;n<5;n++,folder=path.dirname(folder))candidates.push(folder);
106
+ const global=C.globalBase();candidates.push(global);
107
+ const matches=[];
108
+ for(const base of [...new Set(candidates)]){
109
+ if(!fs.existsSync(path.join(base,'state/i3-prototype/installation.json')))continue;
110
+ const record=C.registration(base),parent=path.join(base,'state/i3-prototype',C.projectKey(project));if(!fs.existsSync(parent))continue;
111
+ for(const session of fs.readdirSync(parent)){
112
+ if(!/^[a-f0-9]{24}$/.test(session))continue;const file=path.join(parent,session,'ptc-callbacks.json'),store=load(file);if(!store)continue;
113
+ if(store.registrationDigest!==record.registrationDigest||store.project!==project)continue;
114
+ for(const item of Object.values(store.calls)){
115
+ if(item.name!=='Bash'||item.post||item.cliResult)continue;
116
+ const tokens=require('./native-command.cjs').tokens(item.input?.command);
117
+ if(!tokens||tokens.length!==argv.length+2||digest(tokens.slice(2))!==digest(argv))continue;
118
+ const cwd=item.cwd||project,real=file=>{try{return fs.realpathSync(file);}catch{return null;}};
119
+ const executable=tokens[0].includes(path.sep)?path.resolve(cwd,tokens[0]):(process.env.PATH||'').split(path.delimiter).map(dir=>path.resolve(cwd,dir,tokens[0])).find(file=>{try{fs.accessSync(file,fs.constants.X_OK);return fs.statSync(file).isFile();}catch{return false;}});
120
+ if(executable&&real(executable)===real(process.execPath)&&real(path.resolve(cwd,tokens[1]))===real(script))matches.push({file,item});
121
+ }
122
+ }
123
+ }
124
+ if(!matches.length)return false;assert.equal(matches.length,1,'Captured CLI result has multiple pending native requests');
125
+ const {file,item}=matches[0];locked(file,()=>{
126
+ const store=load(file),current=store.calls[item.id];assert.ok(current&&!current.post&&!current.cliResult,'Captured CLI callback changed');
127
+ current.cliResult={script,scriptDigest:E.sha(fs.readFileSync(script)),argv,inputDigest:digest(current.input),result,exitCode,at:new Date().toISOString()};
128
+ F.write(path.dirname(file),path.basename(file),store);
129
+ });return true;
130
+ }
131
+ module.exports={observe,read,captureResult};