@givolt/i3-composer-agent 2.3.4 → 2.3.5

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 (31) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +2 -0
  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 +2 -0
  7. package/README.md +1 -1
  8. package/commands/i3-prototype.toml +2 -0
  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/package.json +3 -1
  14. package/plugin.json +1 -1
  15. package/scripts/ui-prototype-binding-recovery-test.js +84 -0
  16. package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -2
  17. package/scripts/ui-prototype-codebuddy-hook-test.js +2 -2
  18. package/scripts/ui-prototype-execution-gates-test.js +4 -3
  19. package/skills/i3-ui-prototype/SKILL.md +2 -0
  20. package/skills/i3-ui-prototype/references/execution-evidence.md +6 -0
  21. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +1 -1
  22. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +25 -10
  23. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +3 -3
  24. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +1 -1
  25. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +44 -13
  26. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +7 -3
  27. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +12 -7
  28. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +7 -2
  29. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +1 -1
  30. package/skills/i3-ui-prototype/scripts/native-command.cjs +19 -2
  31. package/skills/i3-ui-prototype/scripts/native-tool-name.cjs +5 -0
@@ -6,7 +6,7 @@
6
6
  "plugins": [
7
7
  {
8
8
  "name": "i3-composer-agent",
9
- "version": "2.3.4",
9
+ "version": "2.3.5",
10
10
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
11
11
  "source": {
12
12
  "source": "local",
@@ -19,3 +19,5 @@ Use the i3-ui-prototype skill in the main session for the user's request below.
19
19
  $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
+
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.
@@ -8,7 +8,7 @@
8
8
  "plugins": [
9
9
  {
10
10
  "name": "i3-composer-agent",
11
- "version": "2.3.4",
11
+ "version": "2.3.5",
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.4",
3
+ "version": "2.3.5",
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.4",
3
+ "version": "2.3.5",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -18,4 +18,6 @@ Use the i3-ui-prototype skill in the main session for the user's request below.
18
18
  $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
+
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.
21
23
  """
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i3 Composer Agent v2.3.4
1
+ # i3 Composer Agent v2.3.5
2
2
 
3
3
  **Intelligence. Integration. Innovation.**
4
4
 
@@ -18,4 +18,6 @@ Use the i3-ui-prototype skill in the main session for the user's request below.
18
18
  $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
+
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.
21
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.4`。已安装后可随时单独检测/补装,无需重装 i3:
51
+ 值为 none、uiux、antdv 或 uiux,antdv;默认 none。可选工具跟随宿主和项目/全局范围,位于基址下 `tools/i3-prototype/`;旧项目 `.prototype-tools` 保持读取兼容。需要固定本次版本时使用 `npx @givolt/i3-composer-agent@2.3.5`。已安装后可随时单独检测/补装,无需重装 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.4 --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.5 --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.4` 的对应入口。
9
+ 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.5` 的对应入口。
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.4 --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.5 --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
 
package/package.json CHANGED
@@ -195,6 +195,7 @@
195
195
  "scripts/run-evals.js",
196
196
  "scripts/run-regressions.js",
197
197
  "scripts/template-adapter-test.js",
198
+ "scripts/ui-prototype-binding-recovery-test.js",
198
199
  "scripts/ui-prototype-buddy-lifecycle-test.js",
199
200
  "scripts/ui-prototype-codebuddy-hook-test.js",
200
201
  "scripts/ui-prototype-content-test.js",
@@ -415,6 +416,7 @@
415
416
  "scripts/run-evals.js",
416
417
  "scripts/run-regressions.js",
417
418
  "scripts/template-adapter-test.js",
419
+ "scripts/ui-prototype-binding-recovery-test.js",
418
420
  "scripts/ui-prototype-buddy-lifecycle-test.js",
419
421
  "scripts/ui-prototype-codebuddy-hook-test.js",
420
422
  "scripts/ui-prototype-content-test.js",
@@ -442,7 +444,7 @@
442
444
  "scripts/validate-versions.js"
443
445
  ]
444
446
  },
445
- "version": "2.3.4",
447
+ "version": "2.3.5",
446
448
  "repository": {
447
449
  "type": "git",
448
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.4",
3
+ "version": "2.3.5",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+ // Regression cases reduced from a real IDE failure; no native host is installed.
3
+ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),os=require('node:os'),cp=require('node:child_process');
4
+ const installed=path.join(__dirname,'lib/installation.js');
5
+ const scripts=path.join(fs.existsSync(installed)?require(installed).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');
6
+ const E=require(path.join(scripts,'execution-evidence.cjs')),H=require(path.join(scripts,'codebuddy-hook.cjs')),N=require(path.join(scripts,'native-command.cjs')),R=require(path.join(scripts,'execution-recovery.cjs'));
7
+ const raw=rows=>rows.map(row=>JSON.stringify(row)).join('\n')+'\n';
8
+ for(const flat of [false,true])test(`raw ${flat?'flat':'paired'} use_skill is a real Skill without accepting Read or forged names`,()=>{
9
+ const rows=flat?[
10
+ {type:'function_call',callId:'front',name:'use_skill',arguments:'{"command":"i3-frontend-ui-engineering"}',sessionId:'s1'},
11
+ {type:'function_call_result',callId:'front',name:'Skill',status:'completed',output:'Frontend instructions',sessionId:'s1'}
12
+ ]:[
13
+ {type:'assistant',sessionId:'s1',message:{content:[{type:'tool_use',id:'front',name:'use_skill',input:{command:'i3-frontend-ui-engineering'}}]}},
14
+ {type:'user',sessionId:'s1',message:{content:[{type:'tool_result',tool_use_id:'front',tool_name:'Skill',content:'Frontend instructions'}]}}
15
+ ];
16
+ const calls=E.parse(raw(rows));assert.equal(E.loaded(calls,'frontend').length,1);assert.equal(calls[0].name,'Skill');assert.equal(calls[0].rawName,'use_skill');
17
+ assert.equal(H.eventCall({hook_event_name:'PostToolUse',tool_name:'use_skill',tool_use_id:'front',tool_input:calls[0].input},{prompts:[{line:0}],calls}).toolCallId,'front');
18
+ for(const name of ['Read','use_skill_fake']){
19
+ const changed=JSON.parse(JSON.stringify(rows));if(flat){changed[0].name=changed[1].name=name;}else{changed[0].message.content[0].name=changed[1].message.content[0].tool_name=name;}
20
+ assert.equal(E.loaded(E.parse(raw(changed)),'frontend').length,0);
21
+ }
22
+ const wrong=JSON.parse(JSON.stringify(rows));if(flat)wrong[1].sessionId='s2';else wrong[1].sessionId='s2';assert.throws(()=>E.parse(raw(wrong)),/session/);
23
+ });
24
+ test('native execute_command success cannot hide a failed stage inside result or stdout',()=>{
25
+ for(const result of [
26
+ {type:'execute_command_result',exitCode:1,stdout:'failed'},
27
+ {type:'execute_command_result',exitCode:0,stdout:'{"status":"failed","errors":["No native Hook task binding"]}\r\n\r\n'},
28
+ {type:'execute_command_result',exitCode:0,stdout:'Error: unavailable'},
29
+ {status:'failed',errors:['native stage failed']}
30
+ ])assert.equal(E.successful({content:{status:'success',success:true,result}}),false);
31
+ assert.equal(E.successful({content:{status:'success',success:true,result:{type:'execute_command_result',exitCode:0,stdout:'{"status":"bound","runId":"r1"}'}}}),true);
32
+ assert.equal(E.successful({content:{result:{status:'failed'},observedApplicationState:true}}),true,'A browser observation of an error is not a failed tool');
33
+ });
34
+ test('frontend already loaded leaves only execution-begin missing until the native begin actually succeeds',()=>{
35
+ const directory='/work/prototypes/a',binding={entryLine:1,contextLine:0,sessionId:'s1',runId:'r1',directory,project:'/work'};
36
+ const source={prompts:[{line:1,text:'实现原型'}],contextLine:0,calls:[{skill:'frontend',success:true,sessionId:'s1',requestLine:2,resultLine:3}]};
37
+ const input={event:'PreToolUse',source,binding,tool:'Write',input:{file_path:directory+'/app/src/A.vue'},directory,cwd:'/work'};
38
+ let answer=H.decision(input);assert.equal(answer.deny,true);assert.deepEqual(answer.missing,['execution-begin']);assert.doesNotMatch(answer.reason,/调用 i3-frontend/);
39
+ const command=`node /skills/native-pilot.cjs execution-begin '${directory}'`;
40
+ const begin={toolCallId:'begin',name:'Bash',input:{command},cwd:'/work',success:false,sessionId:'s1',result:{status:'bound',runId:'r1',sessionId:'s1',directory},requestLine:4,resultLine:5};source.calls.push(begin);
41
+ assert.equal(H.decision(input).deny,true);begin.success=true;assert.equal(H.decision(input).continue,true);
42
+ begin.result={...begin.result,runId:'another-run'};assert.equal(H.decision(input).deny,true);begin.result.runId='r1';
43
+ begin.result.status='success';assert.equal(H.decision(input).deny,true);begin.result.status='bound';
44
+ source.stableThroughLine=4;assert.equal(H.decision(input).deny,true);delete source.stableThroughLine;
45
+ source.calls.push({...begin,toolCallId:'failed-rebind',requestLine:6,resultLine:7,success:false});assert.equal(H.decision(input).deny,true,'A failed rebind must not inherit old success');
46
+ });
47
+ test('binding recovery emits one absolute quoted command that the strict parser recognizes',()=>{
48
+ const directory="/work with spaces/prototypes/customer's demo";
49
+ const command=N.beginCommand(directory);const parsed=N.nativeCommand('Bash',{command},'/unrelated');
50
+ assert.equal(parsed.operation,'execution-begin');assert.equal(parsed.root,directory);assert.ok(path.isAbsolute(parsed.argv[0]));assert.ok(path.isAbsolute(parsed.argv[1]));
51
+ assert.match(R.instruction(['execution-begin'],{directory}),new RegExp('execution-begin'));assert.ok(R.instruction(['execution-begin'],{directory}).includes(command));
52
+ assert.match(R.instruction(['execution-begin'],{directory}),/管道|pipeline/);
53
+ assert.equal(N.nativeCommand('Bash',{command:`cd /work && ${command} 2>&1 | head -80`},'/work'),null);
54
+ });
55
+ test('unbound CLI execution-begin exits nonzero with an actionable standalone command and creates no proof',t=>{
56
+ const root=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'i3-unbound-regression-')));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
57
+ const result=cp.spawnSync(process.execPath,[path.join(scripts,'native-pilot.cjs'),'execution-begin',root],{encoding:'utf8'});
58
+ assert.equal(result.status,1);const failure=JSON.parse(result.stderr);assert.equal(failure.status,'failed');assert.match(failure.errors.join(),/No native Hook task binding/);
59
+ assert.ok(failure.errors.join().includes(N.beginCommand(root)));assert.deepEqual(fs.readdirSync(root),[]);
60
+ });
61
+ test('PostToolUse failures use additionalContext and keep the tool return intact',()=>{
62
+ const value=require(path.join(scripts,'hosts/codebuddy.cjs')).format({feedback:true,reason:'实际返回失败,请补调'},'PostToolUse');
63
+ assert.equal(value.output.continue,true);assert.deepEqual(value.output.hookSpecificOutput,{hookEventName:'PostToolUse',additionalContext:'实际返回失败,请补调'});
64
+ const bound={status:'bound',runId:'r1'};assert.deepEqual(E.resultValue([{type:'text',text:JSON.stringify(bound)}]),bound);
65
+ });
66
+ test('native wrappers preserve actual JSON facts and quoted findings without matching escaped wrapper bytes',()=>{
67
+ const facts={form:{errors:1},observed:true},wrapped={status:'success',success:true,result:JSON.stringify(facts)};
68
+ const A=require(path.join(scripts,'execution-assertions.cjs'));
69
+ assert.equal(A.extract(A.resultData(wrapped),'/form/errors'),1);assert.ok(E.resultText(wrapped).includes('"observed":true'));
70
+ assert.deepEqual(A.resultData({result:{status:'failed'},observedApplicationState:true}),{result:{status:'failed'},observedApplicationState:true});
71
+ assert.equal(E.successful({content:{type:'text',text:'{"status":"failed","operation":"execution-begin"}'}}),false);
72
+ });
73
+ test('loading a missing browser Skill cannot let another stage stand in for the denied finalize retry',()=>{
74
+ const source={prompts:[{line:1,digest:'task',text:'实现原型'}],contextLine:0,calls:[]},index={};
75
+ const context={source,sessionId:'s1',directory:'/work/prototypes/a',stage:'delivery',event:'PreToolUse',eventKey:'deny-finalize',expectedRetry:{kind:'stage',stage:'delivery',operation:'finalize'}};
76
+ R.record(index,context,{deny:true,missing:['browser']});
77
+ source.calls.push({skill:'browser',toolCallId:'actual-browser-load',sessionId:'s1',requestLine:2,resultLine:3,success:true});
78
+ const failedPrerequisite=R.record(index,{...context,stage:'browser-entry',event:'PostToolUse',eventKey:'failed-review',expectedRetry:{kind:'stage',stage:'browser-entry',operation:'record-devtools-review'}},{feedback:true,missing:['execution-evidence']});
79
+ assert.equal(failedPrerequisite.recovery.expectedRetry.operation,'finalize');
80
+ const other=R.record(index,{...context,stage:'browser-entry',resolved:['execution-evidence'],event:'PostToolUse',eventKey:'other-stage-result',retrySucceeded:true,retry:{kind:'stage',stage:'browser-entry',operation:'record-devtools-review',requestLine:4,resultLine:5}},{continue:true});
81
+ assert.equal(other.recovery.status,'ready-to-retry');
82
+ const correct=R.record(index,{...context,event:'PostToolUse',eventKey:'finalize-result',retrySucceeded:true,retry:{kind:'stage',stage:'delivery',operation:'finalize',requestLine:6,resultLine:7}},{continue:true});
83
+ assert.equal(correct.recovery.status,'recovered');
84
+ });
@@ -32,11 +32,11 @@ function fixture(t,host,scope,client){
32
32
  function tool(name,input,content='success',action=()=>{}){
33
33
  const id=request(name,input),decision=hook('PreToolUse',name,input,id);
34
34
  if(decision.deny){result(id,decision.reason,true);return {id,decision};}
35
- action();result(id,content);return {id,decision,post:hook('PostToolUse',name,input,id)};
35
+ action();result(id,typeof content==='function'?content():content);return {id,decision,post:hook('PostToolUse',name,input,id)};
36
36
  }
37
37
  append({type:'user',message:{content:'实现原型,使用模板模式。Synthetic fixture only.'}});
38
38
  tool('Skill',{skill:'i3-ui-prototype'},'Prototype Skill loaded');
39
- const begin=()=>tool('Bash',{command:`node '${path.join(scripts,'native-pilot.cjs')}' execution-begin '${created.directory}'`},'bound');begin();
39
+ const begin=()=>tool('Bash',{command:`node '${path.join(scripts,'native-pilot.cjs')}' execution-begin '${created.directory}'`},()=>{const pointer=JSON.parse(fs.readFileSync(path.join(created.directory,'qa/execution-context.json')));return JSON.stringify({status:'bound',runId:pointer.runId,sessionId,directory:created.directory});});begin();
40
40
  return {root,project,base,directory:created.directory,append,hook,tool,begin,result,request,read(){const pointer=JSON.parse(fs.readFileSync(path.join(created.directory,'qa/execution-context.json')));const selected=C.select({project,product:client==='CodeBuddyCLI'||!client&&host==='codebuddy'?'cli':'ide',host,home,env:{}});return JSON.parse(fs.readFileSync(path.join(C.sessionDirectory(selected,project,pointer.sessionId),'session.json')));}};
41
41
  }
42
42
  for(const [host,client] of [['codebuddy','CodeBuddyCLI'],['codebuddy','CodeBuddyIDE'],['workbuddy','WorkBuddy'],['codebuddy',null],['workbuddy',null]])for(const scope of ['project','global'])test(`${host} ${client||'native-flat-log'} ${scope}: deny, correlate successful Skills, retry and retain budget`,t=>{
@@ -62,6 +62,14 @@ test('Stop asks for missing Skills twice, then reports blocked without an endles
62
62
  f.append({type:'user',message:{content:'先分析,不改代码'}});
63
63
  const answer=f.tool('Write',{file_path:path.join(f.directory,'app/src/NoWrite.vue'),content:'no'});assert.equal(answer.decision.deny,true);assert.match(answer.decision.reason,/read-only/);
64
64
  });
65
+ test('exhausted recovery cannot silently reopen writes after late Skill loads in the same human task',t=>{
66
+ const f=fixture(t,'codebuddy','project','CodeBuddyCLI');
67
+ for(let n=0;n<3;n++){f.append({type:'assistant',message:{content:'已完成,可以交付 '+n}});f.hook('Stop');}
68
+ f.tool('Skill',{skill:'i3-frontend-ui-engineering'});f.tool('Skill',{skill:'antdv'});
69
+ const file=path.join(f.directory,'app/src/AfterBudget.vue');
70
+ const answer=f.tool('Write',{file_path:file,content:'no'},'written',()=>fs.writeFileSync(file,'no'));
71
+ assert.equal(answer.decision.deny,true);assert.equal(fs.existsSync(file),false);assert.equal(f.read().bindings[f.directory].recovery.status,'blocked');
72
+ });
65
73
  test('a duplicated native callback does not spend recovery budget when the transcript grows',t=>{
66
74
  const f=fixture(t,'workbuddy','project',null),input={file_path:path.join(f.directory,'app/src/Duplicate.vue'),content:'fixture'};
67
75
  const id=f.request('Write',input);assert.equal(f.hook('PreToolUse','Write',input,id).deny,true);
@@ -8,10 +8,10 @@ test('native command parsing handles quoted paths without evaluating shell subst
8
8
  for(const command of ['node x; node native-pilot.cjs finalize /x','$(steal) native-pilot.cjs build /x','node x | node native-pilot.cjs finalize /x'])assert.equal(H.nativeCommand('Bash',{command},'/work'),null);
9
9
  });
10
10
  test('before UI write requires a successful frontend Skill in the same live context and task',()=>{
11
- const source={prompts:[{line:1,text:'create prototype'}],calls:[],contextLine:0},binding={entryLine:1,contextLine:0,sessionId:'s1'};
11
+ const source={prompts:[{line:1,text:'create prototype'}],calls:[],contextLine:0},binding={entryLine:1,contextLine:0,sessionId:'s1',runId:'r1',directory:'/work/prototypes/p',project:'/work'};
12
12
  const input={event:'PreToolUse',source,binding,tool:'Write',input:{file_path:'/work/prototypes/p/app/src/A.vue'},directory:'/work/prototypes/p'};
13
13
  assert.equal(H.decision(input).deny,true);
14
- source.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:true}];assert.equal(H.decision(input).continue,true);
14
+ source.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:true},{name:'Bash',input:{command:'node /skills/native-pilot.cjs execution-begin /work/prototypes/p'},sessionId:'s1',result:{status:'bound',runId:'r1',sessionId:'s1',directory:binding.directory},requestLine:4,resultLine:5,success:true}];assert.equal(H.decision(input).continue,true);
15
15
  source.contextLine=4;assert.equal(H.decision(input).deny,true);
16
16
  });
17
17
  test('analysis overrides old tasks and Stop feedback is bounded without granting tool permissions',()=>{
@@ -12,17 +12,18 @@ test('native UI writes resolve relative paths from the actual tool cwd',()=>{
12
12
  });
13
13
  function fixture() {
14
14
  const file='/work/prototypes/orders/app/src/Orders.vue',bytes='<Table />';
15
- const call=(toolCallId,name,input,result,line,skill)=>({toolCallId,name,input,result,requestLine:line,resultLine:line+1,success:true,resultDigest:E.sha(result),sessionId:'s1',contextLine:0,skill});
15
+ const call=(toolCallId,name,input,result,line,skill)=>({toolCallId,name,input,result,requestLine:line+2,resultLine:line+3,success:true,resultDigest:E.sha(result),sessionId:'s1',contextLine:0,skill});
16
16
  const calls=[call('front','Skill',{},'frontend instructions',3,'frontend'),call('ant','Skill',{},'antdv instructions',5,'antdv'),call('query','Bash',{command:'antdv info Table'},'Table API: rowSelection',7),call('version','Read',{file_path:'/work/app/node_modules/antdv-next/package.json'},'{"version":"1.5.3"}',9),call('write','Write',{file_path:file},'saved',11)];
17
17
  const policy=P.create({mode:'template'}),current={digest:'candidate',inputDigest:'input',revision:'r001'};
18
18
  const binding={schemaVersion:1,runId:'r1',sessionId:'s1',projectKey:'p1',directory:'/work/prototypes/orders',instanceId:'instance',policyDigest:policy.digest,inputDigest:'input',entryLine:1};
19
+ calls.push(call('begin','Bash',{command:'node /skills/native-pilot.cjs execution-begin /work/prototypes/orders'},JSON.stringify({status:'bound',runId:'r1',sessionId:'s1',directory:binding.directory}),0));
19
20
  const proof={schemaVersion:1,runId:'r1',sessionId:'s1',projectKey:'p1',policyDigest:policy.digest,candidateDigest:'candidate',inputDigest:'input',revision:'r001',frontend:{toolCallId:'front',files:[{path:'app/src/Orders.vue',sha256:E.sha(bytes)}],requirementIds:['R1']},official:{skill:{toolCallId:'ant'},version:{toolCallId:'version',quote:'"version":"1.5.3"'},queries:[{component:'Table',toolCallId:'query',quote:'Table API: rowSelection'}]},templateChoice:{quote:'template mode'}};
20
21
  return {policy,current,binding,proof,source:{calls,contextLine:0,prompts:[{line:1,text:'generate prototype in template mode',digest:'prompt'}]},scope:{requirements:[{id:'R1'}]},files:{'app/src/Orders.vue':bytes},implementation:{bindings:[{status:'implemented',components:['Table'],files:[{path:'app/src/Orders.vue'}]}]},frameworkVersion:'1.5.3'};
21
22
  }
22
23
  test('a native frontend load is required before the first actual UI write in both modes',()=>{
23
24
  const f=fixture();assert.equal(G.evaluate(f,'implementation').status,'passed');
24
25
  f.source.calls[0].success=false;assert.equal(G.evaluate(f,'implementation').status,'failed');
25
- f.source.calls[0].success=true;f.source.calls[0].resultLine=12;assert.match(G.evaluate(f,'implementation').errors.join(),/before/);
26
+ f.source.calls[0].success=true;f.source.calls[0].resultLine=14;assert.match(G.evaluate(f,'implementation').errors.join(),/before/);
26
27
  });
27
28
  test('IDE relative write evidence uses its bound workspace while legacy evidence keeps its contract',()=>{
28
29
  const f=fixture();f.source.format='codebuddy-ide-index-v1';f.binding.project='/work';f.source.calls.find(c=>c.name==='Write').input.file_path='prototypes/orders/app/src/Orders.vue';
@@ -40,7 +41,7 @@ test('new WorkBuddy implementation uses the shared design gate and rechecks reco
40
41
  const f=fixture();f.policy=P.create({id:P.AUTO_ID,host:'workbuddy',mode:'template'});
41
42
  Object.assign(f.binding,{host:'workbuddy',identitySource:'native-client',policyDigest:f.policy.digest});f.proof.policyDigest=f.policy.digest;
42
43
  assert.equal(G.evaluate(f,'implementation').status,'passed');
43
- f.binding.recovery={status:'recovered',invocations:[{skill:'frontend',toolCallId:'front',resultDigest:f.source.calls[0].resultDigest,requestLine:3,resultLine:4,feedbackLine:1}],retry:{toolCallId:'write'}};
44
+ f.binding.recovery={status:'recovered',invocations:[{skill:'frontend',toolCallId:'front',resultDigest:f.source.calls[0].resultDigest,requestLine:5,resultLine:6,feedbackLine:1}],retry:{toolCallId:'write'}};
44
45
  assert.equal(G.evaluate(f,'implementation').status,'passed');
45
46
  f.binding.recovery.retry.toolCallId='invented';assert.equal(G.evaluate(f,'implementation').status,'failed');
46
47
  delete f.binding.recovery;f.source.calls.find(call=>call.skill==='antdv').success=false;
@@ -63,6 +63,8 @@ CodeBuddy 新建原型采用 A+B 执行合同,实际接口见 [execution-evi
63
63
 
64
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
65
 
66
+ 新建后及恢复实施时,从 `state` 的 `commands.executionBegin.shell` 取得绝对路径命令,在该宿主主会话的 Shell 工具中独立执行;也可原样执行门禁反馈给出的命令。不得拼接 `cd &&`、管道、重定向或 `head`;这些组合不能建立原生任务绑定。等待进程退出码为 0 且内层结果 `status=bound`,再进行业务写入。外层 `success:true` 仅表示工具返回,内层 `status=failed`/非零退出仍是失败;失败后立即按反馈补做绑定或 Skill 调用,再重试原操作。任务/上下文变化重新绑定,失败或未完成的 begin 不能继承旧成功;绑定由真实 PreToolUse 创建,CLI 不补造绑定或成功记录。
67
+
66
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` 不代表不可篡改认证或用户认可。
67
69
 
68
70
  ### 2. 完整内容模板与需求能力
@@ -44,6 +44,10 @@ node <skill-root>/scripts/native-pilot.cjs execution-assertions-template --scope
44
44
 
45
45
  通过自然语言/using 选到工作 Skill,或实际 `/i3-prototype` 快捷入口后,新实例执行 `execution-begin <prototype-root>`。B 回查原生用户请求和工作 Skill/快捷命令,绑定规范化 projectKey、sessionId、runId、实例、输入、模式、策略与注册。只读或停止请求不能启动写任务。新会话/新请求/压缩后的上下文在恢复实施前重新 begin 并加载适用 Skill;用户手改保留。初建骨架不等于实际实施。
46
46
 
47
+ 使用 `state` 中的 `commands.executionBegin.shell` 或拒绝反馈中的绝对路径命令,作为一次独立 Shell 调用;不加 `cd &&`、管道、重定向或 `head`。严格解析器不解释组合 Shell。成功须同时满足进程退出 0 和内层 `status=bound`;外层 `status=success`/`success:true` 不足以证明阶段成功。缺绑定、任务切换、上下文变化或输入变化时重新执行,不能由 CLI 自行写入原生绑定;A 和写入门禁回查当前任务最新一次 begin 的成功结果,失败/待返回不可复用旧成功。
48
+
49
+ 原始工具名 `use_skill`/`skill` 与 `Skill` 通过同一有限别名表识别;IDE 历史转换和原始 JSONL 采用相同规则。保留原始请求/结果摘要、调用 ID、会话与时序校验,普通 Read、未知近似名称仍不算 Skill。原生 execute_command 的内层非零退出、错误结果或 stdout 中完整 JSON `status=failed` 都按失败处理;不会把浏览器返回的任意业务 `result.status` 当成工具错误。
50
+
47
51
  `execution-template <prototype-root>` 只读返回当前证据骨架和已经成功返回的 Skill 引用;保存并填入 `qa/execution-evidence.json`。字段语义:
48
52
 
49
53
  - 固定身份:schemaVersion=1、runId、sessionId、projectKey、policyDigest、inputDigest、candidateDigest、revision,须与当前绑定一致。
@@ -74,6 +78,8 @@ B 的 PreToolUse 只拒绝可识别的缺前置 UI 写入或明确阶段命令
74
78
 
75
79
  B 拒绝后主会话立即补调,等待成功返回,再重试被拒操作;普通 Read 或另开会话不算本主会话 Skill 调用。缺项、反馈事件、后续成功 toolCallId/resultDigest 及重试结果在宿主会话状态落盘。required → ready-to-retry → recovered 分别表示缺项、已可重试、观察到成功重试;recovered 不表示业务验收通过。A 再回查补调及重试的原生引用和时序。
76
80
 
81
+ PostToolUse 观察到已落盘的 begin/阶段失败时,通过该事件的 additionalContext 返回补调指令并保留 required;Stop 才使用停止反馈协议。IDE 尚未持久化当前结果时继续等待后续原生来源核验,不制造成功。实际 Skill 已成功但缺绑定时只提示 execution-begin;补调成功不直接记 recovered,仍须观察原操作的成功重试。
82
+
77
83
  重试按 kind 区分 ui-write 和 stage;阶段重试绑定实际 operation、stage 与同一原型目录。两类新记录都以 feedbackLine 核对请求发生在反馈之后,旧成功回调不能恢复后续拒绝;旧无 kind 的记录继续仅按页面写入合同核验。
78
84
 
79
85
  默认同一缺项两次、单原生任务总计六次,重复回调不增加预算,反复 begin 和内部 providerData.isMeta 的 Hook 消息不重置任务。达到上限时保持受管操作拒绝,允许真实 blocked 报告正常结束;不无限驱动 Stop。用户只读/暂停优先。入口、Skill 发现、加载成功、CLI 查询、重试成功、交付和用户认可分开记录。
@@ -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-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-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));
@@ -4,13 +4,14 @@ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=req
4
4
  const X=require('./execution-runtime.cjs');
5
5
  const H=require('./buddy-host.cjs');
6
6
  const Recovery=require('./execution-recovery.cjs');
7
- const {tokens,nativeCommand,stageFor}=require('./native-command.cjs');
7
+ const {tokens,nativeCommand,stageFor,completedBegin}=require('./native-command.cjs');
8
+ const {toolName}=require('./native-tool-name.cjs');
8
9
  function eventCall(payload,source) {
9
10
  const callId=payload.tool_use_id||payload.call_id;
10
11
  const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
11
12
  const partial=(before,after)=>typeof before==='string'?typeof after==='string'&&after.startsWith(before):Array.isArray(before)?Array.isArray(after)&&before.length<=after.length&&before.every((value,i)=>partial(value,after[i])):before&&typeof before==='object'?after&&typeof after==='object'&&Object.keys(before).every(key=>Object.hasOwn(after,key)&&partial(before[key],after[key])):before===after;
12
13
  const matches=source.calls.filter(call=>{
13
- if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
14
+ if(call.name!==toolName(payload.tool_name)||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
14
15
  if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
15
16
  if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
16
17
  // IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
@@ -35,7 +36,7 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
35
36
  if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
36
37
  const latest=source.prompts.at(-1);
37
38
  assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
38
- const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
39
+ const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===toolName(payload.tool_name));
39
40
  if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
40
41
  else nativeRequestStatus='deferred-until-native-persistence';
41
42
  }else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
@@ -48,13 +49,13 @@ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent
48
49
  if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
49
50
  if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
50
51
  const missing=Recovery.missing({source,binding,policy});
51
- if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing)};
52
+ if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory})};
52
53
  }
53
54
  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);
54
55
  const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
55
56
  if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
56
57
  const missing=Recovery.missing({source,binding,policy,stage:'delivery'});if(!missing.length)missing.push('execution-evidence');
57
- return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing)};
58
+ return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory})};
58
59
  }
59
60
  return {continue:true};
60
61
  }
@@ -81,6 +82,7 @@ function toolCwd(payload,record,project) {
81
82
  return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
82
83
  }
83
84
  function handle(payload,options) {
85
+ payload={...payload,tool_name:toolName(payload.tool_name)};
84
86
  assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
85
87
  if(options.event==='SessionStart')return {continue:true};
86
88
  if(options.event==='UserPromptSubmit'){
@@ -132,8 +134,10 @@ function handle(payload,options) {
132
134
  const recoveryContext=target=>{
133
135
  const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
134
136
  const retryCall=options.event==='PostToolUse'&&callbackCall?.success&&(stagePassed||G.uiWrite({...callbackCall,cwd:callbackCall.cwd||cwd},target))?callbackCall:null;
135
- return {source,sessionId:payload.session_id,directory:target,event:options.event,eventKey,stage,policy:X.policy(target),resolved:[...(existing?['execution-begin']:[]),...(stagePassed?['execution-evidence']:[])],retrySucceeded:!!retryCall,
136
- ...(retryCall?{retry:{toolCallId:retryCall.toolCallId,resultDigest:retryCall.resultDigest,requestLine:retryCall.requestLine,resultLine:retryCall.resultLine,...(stagePassed?{kind:'stage',stage,operation:command.operation}:{kind:'ui-write'})}}:{})};
137
+ const input=payload.tool_input||{},uiFile=input.file_path||input.filePath||input.path||input.TargetFile;
138
+ const expectedRetry=stage?{kind:'stage',stage,operation:options.event==='Stop'?'finalize':command.operation}:G.uiWrite({name:payload.tool_name,input,cwd},target)?{kind:'ui-write',file:path.resolve(cwd,uiFile)}:null;
139
+ return {source,sessionId:payload.session_id,directory:target,event:options.event,eventKey,stage,expectedRetry,policy:X.policy(target),resolved:[...(completedBegin(source,existing)?['execution-begin']:[]),...(stagePassed?['execution-evidence']:[])],retrySucceeded:!!retryCall,
140
+ ...(retryCall?{retry:{toolCallId:retryCall.toolCallId,resultDigest:retryCall.resultDigest,requestLine:retryCall.requestLine,resultLine:retryCall.resultLine,...(stagePassed?{kind:'stage',stage,operation:command.operation}:{kind:'ui-write',file:path.resolve(cwd,uiFile)})}}:{})};
137
141
  };
138
142
  const respond=answer=>{
139
143
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -163,15 +167,26 @@ function handle(payload,options) {
163
167
  if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
164
168
  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});
165
169
  if(answer.deny)return respond(answer);
170
+ if(options.event==='PostToolUse'&&command&&(command.operation==='execution-begin'||stage)){
171
+ // IDE may not have persisted a callback's result yet. Only observed native
172
+ // failures produce recovery feedback; outer tool success is insufficient.
173
+ if(callbackCall?.resultLine&&!callbackCall.success){
174
+ const missing=command.operation==='execution-begin'?['execution-begin']:['execution-evidence'];
175
+ return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory})});
176
+ }
177
+ if(command.operation==='execution-begin')return respond({continue:true});
178
+ }
166
179
  if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)){
167
- if(!existing)return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'])});
180
+ if(!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
168
181
  const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
169
182
  if(checked.status==='failed'){
170
183
  const missing=Recovery.missing({source,binding:existing,policy:fixed,stage});if(!missing.length)missing.push('execution-evidence');
171
- return respond({deny:true,missing,reason:Recovery.instruction(missing)+' '+checked.errors.join('; ')});
184
+ return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory})+' '+checked.errors.join('; ')});
172
185
  }
173
186
  }
174
- if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event))respond({continue:true});
187
+ if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event)){
188
+ const recovery=respond({continue:true});if(recovery.deny||recovery.feedback||recovery.context)return recovery;
189
+ }
175
190
  }
176
191
  if(!Object.keys(index.bindings).length)return {continue:true};
177
192
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -3,7 +3,7 @@
3
3
  // UUID message files explicitly referenced by that index. No directory scan.
4
4
  const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
5
5
  const ID=/^[a-f0-9]{32}$/;
6
- const aliases={use_skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'};
6
+ const {toolName}=require('./native-tool-name.cjs');
7
7
  function read(file) {
8
8
  assert.equal(path.basename(file),'index.json','Expected native IDE index');
9
9
  const directory=path.dirname(file),sessionId=path.basename(directory);
@@ -52,13 +52,13 @@ function read(file) {
52
52
  if(text)latestAssistant=text;
53
53
  for(const block of message.content.filter(b=>b.type==='tool-call')){
54
54
  assert.ok(block.args&&typeof block.args==='object'&&!Array.isArray(block.args),'Invalid IDE tool arguments');
55
- events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:aliases[block.toolName]||block.toolName,input:block.args}]}});
55
+ events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:toolName(block.toolName),input:block.args}]}});
56
56
  }
57
57
  }else if(row.role==='tool'){
58
58
  for(const block of message.content.filter(b=>b.type==='tool-result')){
59
59
  const result=block.result,success=!block.isError&&result?.success===true&&result.status==='success';
60
60
  events.push({...base,type:'user',message:{content:[{type:'tool_result',tool_use_id:block.toolCallId,
61
- tool_name:aliases[block.toolName]||block.toolName,content:result,is_error:!success}]}});
61
+ tool_name:toolName(block.toolName),content:result,is_error:!success}]}});
62
62
  }
63
63
  }
64
64
  }
@@ -17,7 +17,7 @@ function validate(value,scenarioIds) {
17
17
  return value;
18
18
  }
19
19
  function resultData(result) {
20
- const text=E.contentText(result).trim();let value;
20
+ const text=E.resultText(result).trim();let value;
21
21
  try{value=JSON.parse(text);}catch{const blocks=[...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)\n```/g)];assert.equal(blocks.length,1,'One actual JSON result or JSON code block required');value=JSON.parse(blocks[0][1]);}
22
22
  assert.ok(value&&typeof value==='object','Native JSON fact result required');return value;
23
23
  }
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const P = require('./execution-policy.cjs');
7
+ const {toolName}=require('./native-tool-name.cjs');
7
8
  const sha = value => crypto.createHash('sha256').update(value).digest('hex');
8
9
  function contentText(content) {
9
10
  if (typeof content === 'string') return content;
@@ -36,17 +37,47 @@ function nativeEvent(event) {
36
37
  function successful(block) {
37
38
  if (block.is_error || block.isError || block.success === false || block.error) return false;
38
39
  const text = contentText(block.content ?? block.result ?? block.tool_response);
39
- if (!text.trim() || /^\s*(?:Error:|Error\b|错误[::]|Unknown skill\b|Skill[^\n]*not (?:found|available))/i.test(text)) return false;
40
- try {
41
- const value = JSON.parse(text);
42
- if (value && typeof value === 'object') {
43
- if (value.isError || value.success === false || value.error || value.status === 'failed') return false;
44
- if (typeof value.exitCode === 'number' && value.exitCode !== 0) return false;
45
- if (typeof value.stdout === 'string' && /^\s*(?:Error:|Error\b|错误[::])/i.test(value.stdout)) return false;
40
+ if (!text.trim()) return false;
41
+ function failed(value,depth=0) {
42
+ if(depth>8)return true;
43
+ if(typeof value==='string'){
44
+ if(/^\s*(?:Error:|Error\b|错误[::]|Unknown skill\b|Skill[^\n]*not (?:found|available))/i.test(value))return true;
45
+ try{return failed(JSON.parse(value),depth+1);}catch{return false;}
46
46
  }
47
- } catch { /* Native tool text need not be JSON. */ }
48
- return true;
47
+ if(!value||typeof value!=='object')return false;
48
+ if(value.type==='text')return failed(value.text,depth+1);
49
+ if(value.isError||value.is_error||value.success===false||value.error||['failed','error','cancelled'].includes(value.status))return true;
50
+ if(typeof value.exitCode==='number'&&value.exitCode!==0)return true;
51
+ if(typeof value.stdout==='string'&&failed(value.stdout,depth+1))return true;
52
+ // Only descend through native execution envelopes, not arbitrary observed
53
+ // application data such as {result:{status:'failed'}} from a browser check.
54
+ const envelope=typeof value.success==='boolean'||['success','error'].includes(value.status)||value.type==='execute_command_result';
55
+ return !!(envelope&&value.result!==undefined&&failed(value.result,depth+1));
56
+ }
57
+ return !failed(text);
58
+ }
59
+ function resultContent(content){
60
+ let value=content;
61
+ for(let depth=0;depth<8;depth++){
62
+ if(typeof value==='string'){
63
+ try{const parsed=JSON.parse(value);if(parsed&&typeof parsed==='object'&&(parsed.type==='text'||parsed.type==='execute_command_result'||typeof parsed.success==='boolean')){value=parsed;continue;}}catch{/* Plain text and fenced MCP facts remain text. */}
64
+ return value;
65
+ }
66
+ if(Array.isArray(value)){value=contentText(value);continue;}
67
+ if(value?.type==='text'){value=value.text;continue;}
68
+ if(!value||typeof value!=='object')return value;
69
+ if((typeof value.success==='boolean'||['success','error'].includes(value.status)||value.type==='execute_command_result')&&value.result!==undefined){value=value.result;continue;}
70
+ if(typeof value.stdout==='string'){value=value.stdout;continue;}
71
+ return value;
72
+ }
73
+ return null;
74
+ }
75
+ function resultValue(content){
76
+ const value=resultContent(content);
77
+ if(typeof value==='string'){try{return JSON.parse(value);}catch{return null;}}
78
+ return value;
49
79
  }
80
+ const resultText=content=>contentText(resultContent(content));
50
81
  function parse(text) {
51
82
  assert.ok(typeof text === 'string' && Buffer.byteLength(text) <= 64 * 1024 * 1024, 'Native transcript is too large');
52
83
  const calls = [], requests = new Map(); let nativeMessages = 0, contextLine = 0, promptLine = 0;
@@ -65,16 +96,16 @@ function parse(text) {
65
96
  if (event.type === 'assistant' && block.type === 'tool_use') {
66
97
  assert.ok(typeof block.id === 'string' && block.id && typeof block.name === 'string', 'Native request identity missing');
67
98
  assert.ok(!requests.has(block.id), 'Duplicate native tool call ID');
68
- const input = block.input || {}, skillName = ['Skill', 'skill'].includes(block.name) ? input.skill || input.name || input.command : null;
99
+ const name=toolName(block.name),input = block.input || {}, skillName = name==='Skill' ? input.skill || input.name || input.command : null;
69
100
  const normalized = typeof skillName === 'string' ? skillName.replace(/^i3-composer-agent:/, '') : null;
70
- const call = { toolCallId: block.id, name: block.name, input, skillName: normalized, skill: P.skillKey(normalized),
101
+ const call = { toolCallId: block.id, name, ...(name!==block.name?{rawName:block.name}:{}), input, skillName: normalized, skill: P.skillKey(normalized),
71
102
  sessionId:event.sessionId||event.session_id||null, cwd:event.cwd||null, nativeRequestId:event.nativeRequestId||null, contextLine, promptLine,
72
103
  requestLine: lineIndex + 1, requestDigest: sha(JSON.stringify(block)), success: false };
73
104
  requests.set(block.id, call); calls.push(call);
74
105
  } else if (event.type === 'user' && block.type === 'tool_result') {
75
106
  const call = requests.get(block.tool_use_id);
76
107
  assert.ok(call && !call.resultLine, 'Unmatched or duplicate native tool result');
77
- if (block.tool_name !== undefined) assert.equal(block.tool_name, call.name, 'Native tool result name differs');
108
+ if (block.tool_name !== undefined) assert.equal(toolName(block.tool_name), call.name, 'Native tool result name differs');
78
109
  if (call.sessionId && (event.sessionId||event.session_id)) assert.equal(call.sessionId,event.sessionId||event.session_id,'Cross-session native result');
79
110
  Object.assign(call, { resultLine: lineIndex + 1, resultDigest: sha(JSON.stringify(block)),
80
111
  result: block.content, success: successful(block) });
@@ -143,4 +174,4 @@ function readSource(file, roots, {mode='manual',project}={}) {
143
174
  ...(data.latestAssistant!==undefined?{latestAssistant:data.latestAssistant}:{}),
144
175
  sourceVerification: mode==='auto'?'native-hook-transcript':'configured-host-transcript', tamperProof: false };
145
176
  }
146
- module.exports = { sha, contentText, successful, parse, loaded, reference, describe, readSource, cursorBytes, ideWorkspace };
177
+ module.exports = { sha, contentText, successful, resultContent, resultText, resultValue, parse, loaded, reference, describe, readSource, cursorBytes, ideWorkspace };
@@ -30,7 +30,7 @@ function skill(data,ref,key,before=Infinity) {
30
30
  }
31
31
  function quote(data,ref) {
32
32
  const call=nativeRef(data,ref);assert.ok(typeof ref.quote==='string'&&ref.quote.trim(),'Actual native result quote required');
33
- assert.ok(E.contentText(call.result).includes(ref.quote),'Quoted finding absent from native result');return call;
33
+ assert.ok(E.resultText(call.result).includes(ref.quote),'Quoted finding absent from native result');return call;
34
34
  }
35
35
  function identity(d) {
36
36
  P.validate(d.policy);assert.equal(d.binding?.schemaVersion,1,'Hook task binding required');assert.equal(d.proof?.schemaVersion,1,'Execution evidence required');
@@ -38,12 +38,15 @@ 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
42
  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');}
42
43
  if(d.binding.recovery){
43
44
  const recovery=d.binding.recovery;
45
+ assert.notEqual(recovery.status,'blocked','Recovery budget exhausted; a new human task is required');
44
46
  for(const ref of recovery.invocations||[]){const call=nativeRef(d,ref);same(call.skill,ref.skill,'Recovery Skill differs');same(call.requestLine,ref.requestLine,'Recovery request cursor differs');same(call.resultLine,ref.resultLine,'Recovery result cursor differs');assert.ok(call.requestLine>ref.feedbackLine,'Recovery invocation must follow its feedback');}
45
47
  if(recovery.status==='recovered'){
46
48
  const retry=nativeRef(d,recovery.retry);
49
+ if(recovery.expectedRetry)for(const [key,value] of Object.entries(recovery.expectedRetry))same(recovery.retry[key],value,'Recovery retry differs from the denied operation');
47
50
  if(recovery.retry.kind!==undefined)assert.ok(Number.isFinite(recovery.retry.feedbackLine)&&recovery.retry.feedbackLine>=d.binding.entryLine&&retry.requestLine>recovery.retry.feedbackLine,'Recovery retry must follow its feedback');
48
51
  if(recovery.retry.kind==='stage'){
49
52
  const parsed=command.nativeCommand(retry.name,retry.input,retry.cwd||d.binding.project||d.binding.directory);
@@ -54,6 +57,7 @@ function identity(d) {
54
57
  }else {
55
58
  assert.ok(recovery.retry.kind===undefined||recovery.retry.kind==='ui-write','Unknown recovery retry kind');
56
59
  assert.ok(uiWrite({...retry,cwd:retry.cwd||d.binding.project},d.binding.directory),'Recovery retry is not a native UI write');
60
+ if(recovery.expectedRetry?.file){const input=retry.input,file=input.file_path||input.filePath||input.path||input.TargetFile;same(path.resolve(retry.cwd||d.binding.project,file),recovery.expectedRetry.file,'Recovery wrote a different UI file');}
57
61
  }
58
62
  for(const ref of recovery.invocations||[])assert.ok(retry.requestLine>ref.resultLine,'Retry must follow successful Skill results');
59
63
  }
@@ -106,7 +110,7 @@ function design(d) {
106
110
  assert.ok(query.decisions?.length,'UIUX result decisions required');
107
111
  for(const decision of query.decisions) {
108
112
  assert.ok(['adopted','adjusted','rejected','no-match'].includes(decision.decision)&&decision.rationale&&decision.identity,'UIUX decision identity and rationale required');
109
- assert.ok(E.contentText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
113
+ assert.ok(E.resultText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
110
114
  if(['adopted','adjusted'].includes(decision.decision))assert.ok(Object.hasOwn(d.files||{},decision.implementation)&&decision.scenarioId,'Adopted UIUX result needs source and verification scenario');
111
115
  }
112
116
  }
@@ -115,7 +119,7 @@ function design(d) {
115
119
  }
116
120
  function build(d) {
117
121
  assert.ok(d.build?.buildId&&d.build?.buildDigest,'Current checked build required');
118
- const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.contentText(call.result);
122
+ const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.resultText(call.result);
119
123
  assert.ok(['Bash','execute_command','run_terminal_command'].includes(call.name)&&input.includes('native-pilot.cjs')&&/\bbuild\b/.test(input)&&input.includes(d.binding.directory),'Actual current build command required');
120
124
  assert.ok(output.includes(d.build.buildId)&&output.includes(d.build.buildDigest),'Native build result differs from current build');
121
125
  same(d.build.candidateDigest,d.current.digest,'Build candidate is stale');return call;
@@ -1,16 +1,19 @@
1
1
  'use strict';
2
2
  // Deterministic recovery state; only native source results can resolve a Skill.
3
3
  const E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs');
4
+ const N=require('./native-command.cjs');
4
5
  const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
6
+ const blockedReason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
5
7
  function missing({source,binding,policy,stage}){
6
- const unbound=!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine;
8
+ const unbound=!N.completedBegin(source,binding);
7
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']:[])]:[])];
8
10
  const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
9
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)))];
10
12
  }
11
- function instruction(keys){
13
+ function instruction(keys,{directory}={}){
12
14
  const skills=keys.filter(key=>names[key]).map(key=>names[key]);
13
- return `${keys.includes('execution-begin')?'先为当前任务执行 execution-begin。':''}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。':''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
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 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
14
17
  }
15
18
  function record(index,context,answer){
16
19
  const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
@@ -19,16 +22,17 @@ function record(index,context,answer){
19
22
  index.recoveries||={};
20
23
  const state=index.recoveries[taskKey]||={schemaVersion:1,total:0,counts:{},events:{},pending:{},invocations:[],status:'required'};
21
24
  state.feedbackLine??=Math.max(prompt.line,...Object.values(state.pending).map(item=>item.line));
22
- const summary=()=>({taskKey,status:state.status,total:state.total,counts:state.counts,invocations:state.invocations,...(state.retry?{retry:state.retry}:{})});
25
+ const summary=()=>({taskKey,status:state.status,total:state.total,counts:state.counts,invocations:state.invocations,...(state.expectedRetry?{expectedRetry:state.expectedRetry}:{}),...(state.retry?{retry:state.retry}:{})});
23
26
  const remember=result=>{state.events[eventKey]=result;for(const key of Object.keys(state.events).slice(0,-128))delete state.events[key];return {...result,recovery:summary()};};
24
27
  if(state.events[eventKey])return {...state.events[eventKey],recovery:summary()};
28
+ if(state.status==='blocked'&&!answer.deny&&!answer.feedback)return remember(event==='PreToolUse'&&context.expectedRetry?{deny:true,reason:blockedReason}:{...answer,context:blockedReason});
25
29
  if(answer.deny||answer.feedback){
30
+ if(context.expectedRetry&&(!state.expectedRetry||state.status==='recovered'))state.expectedRetry=context.expectedRetry;
26
31
  const keys=[...new Set(answer.missing?.length?answer.missing:['execution-evidence'])];
27
32
  const limits=context.policy?.recovery||{perMissing:2,total:6};
28
33
  if(state.total>=limits.total||keys.some(key=>(state.counts[key]||0)>=limits.perMissing)){
29
34
  state.status='blocked';
30
- const reason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
31
- const result=answer.deny?{deny:true,reason,missing:keys}:{continue:true,context:reason};return remember(result);
35
+ const result=answer.deny?{deny:true,reason:blockedReason,missing:keys}:{continue:true,context:blockedReason};return remember(result);
32
36
  }
33
37
  state.total++;for(const key of keys)state.counts[key]=(state.counts[key]||0)+1;state.status='required';
34
38
  const line=Math.max(prompt.line,...source.calls.map(call=>call.resultLine||call.requestLine));
@@ -44,7 +48,8 @@ function record(index,context,answer){
44
48
  delete state.pending[key];
45
49
  }
46
50
  if(!Object.keys(state.pending).length&&state.status!=='recovered'){
47
- state.status=context.retrySucceeded&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
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};
48
53
  }
49
54
  }
50
55
  return remember({...answer});
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
  const F=require('./native-files.cjs'),{fs,path,assert,json,inside}=F;
3
3
  const P=require('./execution-policy.cjs'),E=require('./execution-evidence.cjs'),G=require('./execution-gates.cjs'),C=require('./codebuddy-context.cjs');
4
+ const Recovery=require('./execution-recovery.cjs');
4
5
  const optional=(root,file)=>fs.existsSync(inside(root,file))?json(inside(root,file)):null;
5
6
  function policy(directory) {
6
7
  const value=optional(directory,'inputs/execution-policy.json'),metaFile=inside(directory,'prototype.json');
@@ -11,7 +12,8 @@ function policy(directory) {
11
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;
12
13
  }
13
14
  function binding(directory) {
14
- const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; run execution-begin inside enabled CodeBuddy');
15
+ const recover=Recovery.instruction(['execution-begin'],{directory});
16
+ const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; '+recover);
15
17
  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');
16
18
  assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
17
19
  const stateDirectory=C.sessionDirectory(selected,project,pointer.sessionId),indexFile=path.join(stateDirectory,'session.json');
@@ -23,6 +25,8 @@ function binding(directory) {
23
25
  const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
24
26
  const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
25
27
  assert.ok(!C.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
28
+ assert.equal(entry.entryLine,source.prompts.at(-1).line,'Native task changed; '+recover);
29
+ assert.equal(entry.contextLine,source.contextLine,'Native context changed; '+recover);
26
30
  return {entry,source,selected};
27
31
  }
28
32
  function data(directory,lib,options={}) {
@@ -33,7 +37,7 @@ function data(directory,lib,options={}) {
33
37
  const pointer=optional(directory,'last-build.json');let build=null;
34
38
  if(pointer)try{build=lib.verifyBuild(directory,pointer.buildId);}catch{/* A changed candidate must be allowed to rebuild; build/browser gates reject null. */}
35
39
  const latest=optional(directory,'qa/latest.json');
36
- const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?json(inside(directory,proof.browser.report)):null);
40
+ const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?optional(directory,proof.browser.report):null);
37
41
  const framework=json(inside(directory,'inputs/framework.json'));
38
42
  return {policy:fixed,current,binding:entry,source,proof,assertions:json(inside(directory,'inputs/execution-assertions.json')),designReview:optional(directory,'qa/design-advice.json'),scope:json(inside(directory,'inputs/scope.json')),implementation:optional(directory,'design-system/implementation.json'),files,frameworkVersion:framework.profile?.versions?.['antdv-next']||json(inside(directory,'app/package.json')).dependencies['antdv-next'].replace(/^[~^]/,''),build,business,devtools:options.devtoolsReport||optional(directory,'qa/devtools-review.json'),selected};
39
43
  }
@@ -54,6 +58,7 @@ function template(directory,lib) {
54
58
  }
55
59
  function begin(directory,lib) {
56
60
  directory=fs.realpathSync(directory);const {entry}=binding(directory);
61
+ assert.equal(entry.inputDigest,lib.manifest(directory).inputDigest,'Inputs changed; '+Recovery.instruction(['execution-begin'],{directory}));
57
62
  return {status:'bound',operation:'execution-begin',directory,runId:entry.runId,sessionId:entry.sessionId,policyDigest:entry.policyDigest,evidenceTemplate:template(directory,lib),
58
63
  next:'Load applicable Skills through CodeBuddy, implement the current scope, and bind actual native result IDs in qa/execution-evidence.json. Binding is not completion.'};
59
64
  }
@@ -3,7 +3,7 @@
3
3
  const fs=require('node:fs'),path=require('node:path');
4
4
  function format(result,event){
5
5
  if(result.deny)return {code:0,output:{hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:result.reason}}};
6
- if(result.feedback)return {code:0,output:{continue:false,reason:result.reason}};
6
+ if(result.feedback)return {code:0,output:event==='Stop'?{continue:false,reason:result.reason}:{continue:true,hookSpecificOutput:{hookEventName:event,additionalContext:result.reason}}};
7
7
  if(result.context)return {code:0,output:{continue:true,...(result.recovery?.status==='blocked'?{systemMessage:result.context}:{}),...(['SessionStart','UserPromptSubmit','PostToolUse'].includes(event)?{hookSpecificOutput:{hookEventName:event,additionalContext:result.context}}:{})}};
8
8
  return {code:0,output:{continue:true}};
9
9
  }
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
  // One non-executing parser shared by Hook dispatch and A receipt verification.
3
- const path=require('node:path');
3
+ const path=require('node:path'),assert=require('node:assert/strict');
4
+ const {quote}=require('./codebuddy-registration.cjs');
5
+ const E=require('./execution-evidence.cjs');
4
6
  function tokens(command) {
5
7
  if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
6
8
  const values=[];let value='',quote=null,started=false;
@@ -15,4 +17,19 @@ function nativeCommand(tool,input,cwd) {
15
17
  return {operation,root,argv};
16
18
  }
17
19
  const stageFor=Object.freeze({build:'implementation','record-run':'browser-entry','record-devtools-review':'browser-entry',finalize:'delivery'});
18
- module.exports={tokens,nativeCommand,stageFor};
20
+ function beginCommand(directory){
21
+ assert.ok(typeof directory==='string'&&path.isAbsolute(directory),'Absolute prototype directory required');
22
+ return [process.execPath,path.join(__dirname,'native-pilot.cjs'),'execution-begin',directory].map(quote).join(' ');
23
+ }
24
+ function completedBegin(source,binding){
25
+ if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||(binding.contextLine||0)!==source.contextLine)return null;
26
+ const calls=source.calls.filter(call=>call.sessionId===binding.sessionId&&call.requestLine>=binding.entryLine&&call.requestLine>source.contextLine&&(()=>{
27
+ const command=nativeCommand(call.name,call.input,call.cwd||binding.project||binding.directory);
28
+ return command?.operation==='execution-begin'&&command.root===binding.directory;
29
+ })());
30
+ const latest=calls.at(-1);
31
+ if(!latest?.success||!latest.resultLine||source.stableThroughLine!==undefined&&latest.resultLine>source.stableThroughLine)return null;
32
+ const result=E.resultValue(latest.result);
33
+ return result?.status==='bound'&&result.runId===binding.runId&&result.sessionId===binding.sessionId&&result.directory===binding.directory?latest:null;
34
+ }
35
+ module.exports={tokens,nativeCommand,stageFor,beginCommand,completedBegin};
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ // Observed Buddy tool aliases only. Preserve original blocks for digest checks.
3
+ const aliases=Object.freeze({use_skill:'Skill',skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'});
4
+ const toolName=name=>Object.hasOwn(aliases,name)?aliases[name]:name;
5
+ module.exports={toolName};