@givolt/i3-composer-agent 2.3.3 → 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 (49) 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 +2 -2
  8. package/bin/i3-composer-agent.cjs +18 -15
  9. package/bin/wizard.cjs +12 -52
  10. package/commands/i3-prototype.toml +2 -0
  11. package/docs/codebuddy-setup.md +21 -7
  12. package/docs/codex-setup.md +1 -1
  13. package/docs/other-hosts.md +1 -1
  14. package/docs/workbuddy-setup.md +18 -2
  15. package/hooks/manage.cjs +22 -14
  16. package/package.json +7 -1
  17. package/plugin.json +1 -1
  18. package/scripts/ui-prototype-binding-recovery-test.js +84 -0
  19. package/scripts/ui-prototype-buddy-lifecycle-test.js +97 -0
  20. package/scripts/ui-prototype-codebuddy-hook-test.js +3 -3
  21. package/scripts/ui-prototype-dependencies-test.js +13 -0
  22. package/scripts/ui-prototype-execution-gates-test.js +27 -2
  23. package/scripts/ui-prototype-execution-policy-test.js +12 -1
  24. package/scripts/ui-prototype-recovery-test.js +72 -0
  25. package/skills/i3-ui-prototype/SKILL.md +7 -3
  26. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +10 -6
  27. package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
  28. package/skills/i3-ui-prototype/references/execution-evidence.md +31 -8
  29. package/skills/i3-ui-prototype/references/interaction-acceptance.md +1 -1
  30. package/skills/i3-ui-prototype/references/project-layout.md +1 -1
  31. package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
  32. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +14 -14
  33. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +90 -36
  34. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +3 -3
  35. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +7 -5
  36. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +1 -1
  37. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +45 -14
  38. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +30 -5
  39. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +28 -7
  40. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +57 -0
  41. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +8 -3
  42. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +9 -5
  43. package/skills/i3-ui-prototype/scripts/native-command.cjs +35 -0
  44. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +2 -4
  45. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +5 -1
  46. package/skills/i3-ui-prototype/scripts/native-tool-name.cjs +5 -0
  47. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
  48. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +13 -13
  49. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +1 -1
@@ -39,7 +39,7 @@
39
39
 
40
40
  固定工具的输入事件也须实际验证:本轮 Chrome DevTools MCP 1.11.0 内捆的短文本 fill 路径对空字符串直接设置 DOM value 后结束,没有发送 input/change,可能使界面为空而查询或校验仍使用旧绑定值。保存原始失败观测。独立真实 MCP 实测可用原生 fill 单个空格使输入活跃,再只读取得实际 value 长度和 selectionStart/selectionEnd;空格可能追加到恢复的旧值,不能假定只有一字。确认光标位于末尾后,按实际文本长度逐次原生 press_key Backspace,核对空值,再查询/保存并核对绑定结果。本轮 headless 的 Meta+A/Control+A 未全选,快捷键须按实际结果核对。不能只看 DOM value 或工具成功文本,也不修改组件库或用 evaluate_script 修补事件。其他版本按实际行为判断。该工具事件限制、宿主 PreToolUse 参数兼容、原型业务缺陷分别诊断,不以其中一项通过代替其他项。
41
41
 
42
- `native-pilot.cjs browser` 的 Playwright 执行器保留原型专项回归,包括确定性能力、故障与隔离检查;它是补充证据,不能代替浏览器 Skill、MCP 调用或上述实际观测。指定的 CodeBuddy 完整链必须同时满足 Skill、DevTools 和专项回归,缺可调用 Skill 或 MCP 时明确 blocked/not-run,最终结果为 partial/blocked。只有补充回归通过时,不将整链写为 complete。
42
+ `native-pilot.cjs browser` 的 Playwright 执行器保留原型专项回归,包括确定性能力、故障与隔离检查;它是补充证据,不能代替浏览器 Skill、MCP 调用或上述实际观测。新完整业务的 CodeBuddy/WorkBuddy 严格链必须满足 Skill、DevTools 和固定业务场景实测;Playwright 执行器仅适用于原设备列表固定合同,缺可调用 Skill 或 MCP 时明确 blocked/not-run,最终结果为 partial/blocked。只有补充回归通过时,不将整链写为 complete。
43
43
 
44
44
  新 create 将 `browserReview` 要求固定到 inputs/acceptance.json;字段缺失的旧 v2 维持原行为,v1 不迁移。现有实例需明确接受完整验收输入变更后才启用,不在 inspect 中追随全局策略。其他宿主的等效真实浏览器渠道如实单列,不能借其结果宣称这条 CodeBuddy 指定链已通过。
45
45
 
@@ -36,7 +36,7 @@ paths 返回的 scope 是安装范围,scopeFile 是准备区 scope.json 的完
36
36
 
37
37
  ## scope.moduleIds 与 appliesTo
38
38
 
39
- 新原生 scope 使用 schemaVersion=2,moduleIds 为数组,例如 `["asset-management", "work-order-management"]`。标识须来自实际 PRD 或已确定项目模块:小写字母开头,使用字母、数字和单个连字符分隔,总长不超过 128;不把文档名、需求全文、代码路径或通配符当模块标识。模块身份不清楚时填 [],不编造标识。
39
+ 新原生 scope 使用 schemaVersion=3,moduleIds 为数组,例如 `["asset-management", "work-order-management"]`。标识须来自实际 PRD 或已确定项目模块:小写字母开头,使用字母、数字和单个连字符分隔,总长不超过 128;不把文档名、需求全文、代码路径或通配符当模块标识。模块身份不清楚时填 [],不编造标识。
40
40
 
41
41
  需求的 source.reference 保留具体 PRD、版本与需求编号,statement 保留真实依据。多个 PRD 同模块去重;不同模块取本原型 scope 范围内并集。需求 ID 在本 scope 内唯一,文档冲突明确留作待确认项;模块关联不能代替需求覆盖或完成证明。
42
42
 
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ // Identification never treats absence of an IDE marker as proof of CLI identity.
3
+ const path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
4
+ const HOSTS=['codebuddy','workbuddy'];
5
+ function globalBase({host='codebuddy',home=os.homedir(),env=process.env}={}){
6
+ assert.ok(HOSTS.includes(host),'Unsupported Buddy host');
7
+ const key=host==='workbuddy'?'WORKBUDDY_CONFIG_DIR':'CODEBUDDY_CONFIG_DIR';
8
+ const base=env[key]?.trim()||path.join(home,`.${host}`);
9
+ assert.ok(path.isAbsolute(base)&&!/[\x00\r\n]/.test(base),`${key} must be an absolute safe directory`);
10
+ return path.normalize(base);
11
+ }
12
+ function identify(payload,record,{home=os.homedir(),env=process.env}={}){
13
+ const clients={CodeBuddyIDE:['codebuddy','ide'],CodeBuddyCLI:['codebuddy','cli'],CodeBuddyCode:['codebuddy','cli'],WorkBuddy:['workbuddy','ide'],WorkBuddyIDE:['workbuddy','ide'],WorkBuddyDesktop:['workbuddy','ide']};
14
+ const client=clients[payload.client];
15
+ const declared=typeof payload.host==='string'?payload.host.toLowerCase():null;
16
+ let found=client&&{host:client[0],product:client[1],identitySource:'native-client'};
17
+ if(found&&declared)assert.equal(declared,found.host,'Conflicting native host identity');
18
+ const candidates=[];
19
+ if(typeof payload.transcript_path==='string'&&path.isAbsolute(payload.transcript_path)){
20
+ const file=path.normalize(payload.transcript_path);
21
+ for(const host of HOSTS){
22
+ const parent=path.join(globalBase({host,home,env}),'projects')+path.sep;
23
+ if(file.startsWith(parent)&&file.endsWith('.jsonl'))candidates.push(host);
24
+ }
25
+ }
26
+ if(found&&candidates.length)assert.ok(candidates.includes(found.host),'Conflicting client and native host log location');
27
+ if(!found&&candidates.length){
28
+ const host=candidates.length===1?candidates[0]:env.WORKBUDDY_CONFIG_DIR&&candidates.includes('workbuddy')?'workbuddy':null;
29
+ assert.ok(host,'Conflicting native host log locations');
30
+ found={host,product:host==='codebuddy'?'cli':'ide',identitySource:'native-host-log-location'};
31
+ }
32
+ // A legacy installation retains its original, explicitly weaker contract.
33
+ if(!found&&record.schemaVersion===1&&['ide','cli'].includes(record.product))found={host:'codebuddy',product:record.product,identitySource:'legacy-profile'};
34
+ assert.ok(found,'Native host/product identity is unavailable; keep invocation unverified');
35
+ if(declared)assert.equal(declared,found.host,'Conflicting native host identity');
36
+ assert.ok((record.hosts||['codebuddy']).includes(found.host),'Native host is outside the registered scope');
37
+ return {...found,productVersion:payload.client_version||payload.product_version||(found.host==='workbuddy'?(env.WORKBUDDY_APP_VERSION||env.CLIENT_INFO_PRODUCT_VERSION):null)||(record.product==='auto'?null:record.productVersion)||'unreported'};
38
+ }
39
+ function isIDE(payload){return ['CodeBuddyIDE','WorkBuddy','WorkBuddyIDE','WorkBuddyDesktop'].includes(payload.client);}
40
+ module.exports={HOSTS,globalBase,identify,isIDE};
@@ -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
- const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs');
4
- const FILES=['execution-policy.cjs','execution-evidence.cjs','codebuddy-ide-source.cjs','execution-gates.cjs','execution-assertions.cjs','execution-runtime.cjs','codebuddy-context.cjs','codebuddy-registration.cjs','codebuddy-hook.cjs','prototype-hooks.cjs','hosts/codebuddy.cjs','native-pilot.cjs','native-pilot-lib.cjs','native-delivery.cjs','native-devtools.cjs','native-files.cjs','native-build.cjs'];
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'];
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));
@@ -21,37 +21,37 @@ function bundle(scriptPath) {
21
21
  }
22
22
  function registration(base) {
23
23
  const raw=read(path.join(base,'state/i3-prototype/installation.json'));if(!raw)return null;
24
- const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.equal(record.schemaVersion,1,'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');
24
+ const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.ok([1,2].includes(record.schemaVersion),'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');
25
+ if(record.schemaVersion===2)assert.ok(record.product==='auto'&&Array.isArray(record.hosts)&&record.hosts.length&&record.hosts.every(host=>H.HOSTS.includes(host)),'Invalid automatic Hook identity');
25
26
  const settingsText=read(path.join(base,'settings.json')),settings=R.config(settingsText);
26
27
  for(const event of R.EVENTS)assert.equal((settings.hooks?.[event]||[]).filter(group=>JSON.stringify(group)===JSON.stringify(record.groups[event])).length,1,'Registered Hook was removed, duplicated, or edited');
27
28
  assert.equal(bundle(record.scriptPath),record.scriptDigest,'Audit adapter bytes changed; refresh owned registration');
28
29
  assert.ok(fs.existsSync(record.nodePath),'Registered Node executable is missing');
29
30
  return {...record,settingsDigest:E.sha(settingsText),registrationDigest:E.sha(JSON.stringify(record))};
30
31
  }
31
- function globalBase({home=os.homedir(),env=process.env}={}) {
32
- const configured=env.CODEBUDDY_CONFIG_DIR?.trim();
33
- if(!configured)return path.join(home,'.codebuddy');
34
- assert.ok(path.isAbsolute(configured)&&!/[\x00\r\n]/.test(configured),'CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
35
- return path.normalize(configured);
36
- }
37
- function select({project,product,home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
38
- const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({home,env});
32
+ const globalBase=H.globalBase;
33
+ function select({project,product,host='codebuddy',home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
34
+ assert.ok(['ide','cli'].includes(product),'Actual native product identity required');
35
+ const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({host,home,env});
39
36
  const projectText=readText(path.join(projectBase,'settings.json')),localText=readText(path.join(projectBase,'settings.local.json'));
40
37
  if(localText){const local=R.config(localText);assert.ok(!local.hooks&&!local.disableAllHooks&&!local.allowManagedHooksOnly,'Local Hook overrides are not supported by this profile');}
41
38
  const projectSettings=R.config(projectText);assert.ok(!projectSettings.disableAllHooks&&!projectSettings.allowManagedHooksOnly,'Project Hook policy prevents automatic auditing');
42
39
  const globalText=readText(path.join(userBase,'settings.json')),globalSettings=R.config(globalText);assert.ok(!globalSettings.disableAllHooks&&!globalSettings.allowManagedHooksOnly,'Global Hook policy prevents automatic auditing');
43
40
  // IDE documents project settings as an override. Do not silently fall back through a shadowing file.
44
- const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
41
+ const bases=host==='codebuddy'&&product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
45
42
  for(const base of [...new Set(bases)]) {
46
43
  const record=loadRegistration(base);if(!record)continue;
47
- assert.equal(record.product,product,'Conflicting CodeBuddy product profiles');return record;
44
+ if(record.product==='auto'){
45
+ if(!(record.hosts||[]).includes(host))continue;
46
+ }else {assert.equal(host,'codebuddy','Legacy profile is CodeBuddy only');assert.equal(record.product,product,'Conflicting CodeBuddy product profiles');}
47
+ return {...record,host,product};
48
48
  }
49
49
  return null;
50
50
  }
51
51
  function projectKey(project){return `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project'}-${E.sha(project).slice(0,16)}`;}
52
52
  function sessionDirectory(record,project,sessionId) {
53
53
  assert.ok(typeof sessionId==='string'&&sessionId,'Native session ID required');
54
- return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(sessionId).slice(0,24));
54
+ return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(record.schemaVersion===2?`${record.host}:${record.product}:${sessionId}`:sessionId).slice(0,24));
55
55
  }
56
56
  function source(payload,record,project) {
57
57
  assert.ok(typeof payload.transcript_path==='string'&&typeof payload.session_id==='string','Native transcript and session fields required');
@@ -2,30 +2,22 @@
2
2
  const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict');
3
3
  const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs'),G=require('./execution-gates.cjs');
4
4
  const X=require('./execution-runtime.cjs');
5
- function tokens(command) {
6
- if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
7
- const values=[];let value='',quote=null,started=false;
8
- for(let i=0;i<command.length;i++) {const c=command[i];if(quote){if(c===quote){quote=null;started=true;}else if(c==='\\'&&quote==='"'&&i+1<command.length)value+=command[++i];else value+=c;}
9
- else if(c==='"'||c==="'"){quote=c;started=true;}else if(c==='\\'&&i+1<command.length){value+=command[++i];started=true;}else if(/\s/.test(c)){if(started){values.push(value);value='';started=false;}}else if(/[;&|<>]/.test(c))return null;else {value+=c;started=true;}}
10
- if(quote)return null;if(started)values.push(value);return values;
11
- }
12
- function nativeCommand(tool,input,cwd) {
13
- if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
14
- const argv=tokens(input?.command);if(!argv)return null;const index=argv.findIndex(arg=>path.basename(arg)==='native-pilot.cjs');if(index<1)return null;
15
- const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
16
- return {operation,root,argv};
17
- }
5
+ const H=require('./buddy-host.cjs');
6
+ const Recovery=require('./execution-recovery.cjs');
7
+ const {tokens,nativeCommand,stageFor,completedBegin}=require('./native-command.cjs');
8
+ const {toolName}=require('./native-tool-name.cjs');
18
9
  function eventCall(payload,source) {
10
+ const callId=payload.tool_use_id||payload.call_id;
19
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;
20
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;
21
13
  const matches=source.calls.filter(call=>{
22
- if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||payload.tool_use_id&&call.toolCallId!==payload.tool_use_id)return false;
14
+ if(call.name!==toolName(payload.tool_name)||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
23
15
  if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
24
16
  if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
25
17
  // IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
26
18
  // actual callback with one pending native call in the exact generation. The
27
19
  // pending call remains unsuccessful until a native result is persisted.
28
- return source.format==='codebuddy-ide-index-v1'&&payload.client==='CodeBuddyIDE'&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
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||{});
29
21
  });
30
22
  const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
31
23
  assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
@@ -35,16 +27,16 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
35
27
  // IDE can persist the current call only AFTER returning from both Hooks. Its
36
28
  // current native prompt/generation still binds the callback. Never synthesize
37
29
  // a successful call; A checks the persisted request/result at a later stage.
38
- const attempts=toolEvent&&record.product==='ide'&&payload.client==='CodeBuddyIDE'?8:1;
30
+ const attempts=toolEvent&&record.product==='ide'&&H.isIDE(payload)?8:1;
39
31
  for(let attempt=1;attempt<=attempts;attempt++) {
40
32
  try {
41
33
  const source=readSource(payload,record,project);
42
34
  let nativeRequestStatus='not-tool-event';
43
35
  if(toolEvent) {
44
- if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&payload.client==='CodeBuddyIDE') {
36
+ if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
45
37
  const latest=source.prompts.at(-1);
46
38
  assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
47
- const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
39
+ const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===toolName(payload.tool_name));
48
40
  if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
49
41
  else nativeRequestStatus='deferred-until-native-persistence';
50
42
  }else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
@@ -53,14 +45,18 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
53
45
  }catch(error){if(attempt===attempts)throw error;pause(100);}
54
46
  }
55
47
  }
56
- function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false}) {
48
+ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy}) {
57
49
  if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
58
50
  if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
59
- const front=E.loaded(source.calls,'frontend').filter(c=>c.sessionId===binding?.sessionId&&c.requestLine>source.contextLine&&(source.stableThroughLine===undefined||c.resultLine<=source.stableThroughLine));
60
- if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine||!front.length)return {deny:true,reason:'Run execution-begin for the current task and load the frontend engineering Skill successfully in this session/context before writing prototype UI. Reading SKILL.md is insufficient.'};
51
+ const missing=Recovery.missing({source,binding,policy});
52
+ if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory})};
61
53
  }
62
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);
63
- if(event==='Stop'&&binding&&!deliveryCurrent&&!stopHookActive&&claim)return {feedback:true,reason:'This prototype run has no current verified receipt. Run execution-check and resolve the missing evidence, or report blocked / awaiting-input / progress without claiming completion.'};
55
+ const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
56
+ if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
57
+ 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})};
59
+ }
64
60
  return {continue:true};
65
61
  }
66
62
  function prototypeForWrite(project,tool,input) {
@@ -70,8 +66,9 @@ function prototypeForWrite(project,tool,input) {
70
66
  while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
71
67
  }
72
68
  function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
73
- const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/';
74
- const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
69
+ const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
70
+ 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');
75
72
  const project=real(root);
76
73
  // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
77
74
  // or session-bound native workspace above; reject all other outside cwd.
@@ -82,15 +79,30 @@ function saveIndex(directory,index) {
82
79
  F.write(directory,'session.json',index);
83
80
  }
84
81
  function toolCwd(payload,record,project) {
85
- return record.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
82
+ return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
86
83
  }
87
84
  function handle(payload,options) {
85
+ payload={...payload,tool_name:toolName(payload.tool_name)};
88
86
  assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
89
- const project=safeProject(payload,options),record=C.select({project,product:options.product});
87
+ if(options.event==='SessionStart')return {continue:true};
88
+ if(options.event==='UserPromptSubmit'){
89
+ 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。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
91
+ return {continue:true};
92
+ }
93
+ let identity,installed;
94
+ try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
95
+ const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
96
+ 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);
98
+ if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
99
+ }
100
+ throw error;
101
+ }
102
+ options={...options,...identity};
103
+ const project=safeProject(payload,options,options.env),record=C.select({project,product:identity.product,host:identity.host,home:options.home,env:options.env});
90
104
  if(!record||record.base!==options.base)return {continue:true};
91
105
  const cwd=toolCwd(payload,record,project);
92
- // SessionStart and analysis do not create persistent per-project/session state.
93
- if(['SessionStart','UserPromptSubmit'].includes(options.event))return {continue:true};
94
106
  let source;
95
107
  try {
96
108
  source=eventSource(payload,record,project);
@@ -108,30 +120,73 @@ function handle(payload,options) {
108
120
  const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
109
121
  const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
110
122
  const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
111
- if(options.event==='PreToolUse'&&command?.operation==='create'&&!command.argv.includes('--execution-policy')&&(E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text)))return {deny:true,reason:'New CodeBuddy prototypes require --host codebuddy --execution-policy codebuddy-ab-v1 and fixed --execution-assertions. Existing artifacts keep their original contracts.'};
123
+ 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
+ const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
125
+ const hostAt=command.argv.indexOf('--host');
126
+ 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
+ }
112
128
  const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
113
129
  const existing=directory?index.bindings[directory]:null;
130
+ const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
131
+ let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
132
+ const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
133
+ const eventKey=E.sha(JSON.stringify([options.event,payload.generation_id||latest.nativeRequestId||null,callId||null,payload.tool_name||null,payload.tool_input||null,callId?null:source.digest]));
134
+ const recoveryContext=target=>{
135
+ const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
136
+ const retryCall=options.event==='PostToolUse'&&callbackCall?.success&&(stagePassed||G.uiWrite({...callbackCall,cwd:callbackCall.cwd||cwd},target))?callbackCall:null;
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)})}}:{})};
141
+ };
142
+ const respond=answer=>{
143
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
144
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry without bypassing the gate');}
145
+ 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
+ finally{fs.rmdirSync(lock);}
147
+ };
114
148
  if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
115
149
  assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
116
150
  const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
151
+ 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');
117
152
  const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
118
153
  assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
119
154
  const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
120
155
  const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
121
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,
122
- operation:meta.revision==='r001'?'create':'revise',product:record.product,productVersion:record.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
157
+ 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}};
123
158
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
124
159
  try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
125
160
  try {
126
161
  const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
127
162
  saveIndex(stateDirectory,fresh);
128
- F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,product:record.product});
163
+ F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
129
164
  }finally{fs.rmdirSync(lock);}
130
165
  return {continue:true};
131
166
  }
132
167
  if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
133
- const answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd});if(answer.deny)return answer;
134
- if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)&&!existing)return {deny:true,reason:'Begin this strict prototype run with execution-begin before implementation or delivery.'};
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});
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
+ }
179
+ 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})});
181
+ const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
182
+ if(checked.status==='failed'){
183
+ 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('; ')});
185
+ }
186
+ }
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
+ }
135
190
  }
136
191
  if(!Object.keys(index.bindings).length)return {continue:true};
137
192
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -140,14 +195,13 @@ function handle(payload,options) {
140
195
  const fresh=JSON.parse(C.read(file));let changed=false;
141
196
  for(const entry of Object.values(fresh.bindings)) {
142
197
  if(entry.entryLine!==latest.line)continue;
143
- const eventKey=E.sha(JSON.stringify([options.event,payload.tool_use_id||null,payload.tool_name||null,payload.tool_input||null,source.digest]));
144
198
  if(entry.lastEventKey===eventKey)continue;
145
199
  entry.source={path:source.path,bytes:source.bytes,digest:source.digest};entry.lastEventKey=eventKey;entry.lastHook={event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus};changed=true;
146
200
  if(options.event==='Stop') {
147
201
  const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
148
202
  const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
149
- const feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
150
- if(feedback.feedback){entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return feedback;}
203
+ const fixed=X.policy(entry.directory),feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,policy:fixed,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
204
+ if(feedback.feedback){const result=Recovery.record(fresh,{...recoveryContext(entry.directory),policy:fixed},feedback);entry.recovery=result.recovery;if(fixed.id!==P.AUTO_ID)entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return result;}
151
205
  }
152
206
  }
153
207
  if(changed)saveIndex(stateDirectory,fresh);
@@ -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
  }
@@ -24,18 +24,20 @@ function config(text) {
24
24
  }
25
25
  function ownedMarker(group) { return group.hooks.some(hook => typeof hook.command === 'string' && hook.command.includes(OWNER)); }
26
26
  function plan(options) {
27
- const { action = 'keep', scope, product, base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
27
+ const { action = 'keep', scope, product = 'auto', base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
28
28
  transcriptRoots = record?.transcriptRoots || [], transcriptMode = record?.transcriptMode || (transcriptRoots.length ? 'manual' : 'auto'), productVersion = record?.productVersion || 'unreported' } = options;
29
29
  assert.ok(['keep', 'enable', 'disable'].includes(action), 'Invalid Hook action');
30
30
  assert.ok(['project', 'global'].includes(scope), 'Invalid installation scope');
31
- assert.ok(['ide', 'cli'].includes(product), 'Specify CodeBuddy product ide or cli');
31
+ assert.ok(['auto', 'ide', 'cli'].includes(product), 'Use automatic product detection, or a legacy ide/cli profile');
32
+ const hosts=[...new Set(options.hosts || [...(record?.hosts || (record ? ['codebuddy'] : [])),...(options.agents?.filter(host=>['codebuddy','workbuddy'].includes(host)) || [options.host || 'codebuddy'])])].sort();
33
+ assert.ok((hosts.length||action==='disable')&&hosts.every(host=>['codebuddy','workbuddy'].includes(host)), 'Unsupported prototype Hook host');
32
34
  for (const item of [base, project, nodePath, scriptPath]) assert.ok(typeof item === 'string' && path.isAbsolute(item) && !/[\x00\r\n]/.test(item), 'Absolute safe registration paths required');
33
35
  if (process.platform === 'win32') throw new Error('CodeBuddy A+B Windows Hook quoting is not yet verified');
34
36
  const before = config(currentText), after = structuredClone(before), activeRecord = record && record.owner === OWNER ? record : null;
35
37
  if (record) {
36
- assert.ok(activeRecord && activeRecord.schemaVersion === 1, 'Unknown registration ownership');
38
+ assert.ok(activeRecord && [1,2].includes(activeRecord.schemaVersion), 'Unknown registration ownership');
37
39
  assert.equal(activeRecord.scope, scope, 'Registration scope changed');
38
- assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
40
+ if(product!=='auto')assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
39
41
  assert.equal(activeRecord.base, base, 'Registration base changed');
40
42
  }
41
43
  const found = {};
@@ -63,7 +65,7 @@ function plan(options) {
63
65
  assert.ok(Array.isArray(transcriptRoots)&&transcriptRoots.every(root=>typeof root==='string'&&path.isAbsolute(root)&&!/[\x00\r\n]/.test(root)),'Absolute native transcript roots required');
64
66
  assert.ok(['auto','manual'].includes(transcriptMode),'Invalid native transcript mode');
65
67
  assert.ok(transcriptMode==='manual'?transcriptRoots.length>0:transcriptRoots.length===0,'Manual transcript mode requires roots; auto uses only the native callback file');
66
- recordAfter = { schemaVersion: 1, owner: OWNER, scope, product, base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
68
+ recordAfter = { schemaVersion: product==='auto'?2:1, owner: OWNER, scope, product, ...(product==='auto'?{hosts}:{}), base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
67
69
  }
68
70
  }
69
71
  const afterText = unchanged ? currentText : JSON.stringify(after, null, 2) + '\n';
@@ -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;
@@ -18,7 +19,7 @@ function nativeEvent(event) {
18
19
  if (event.type === 'message' && ['assistant', 'user'].includes(event.role)) {
19
20
  const content = Array.isArray(event.content) ? event.content.map(block =>
20
21
  ['input_text', 'output_text'].includes(block?.type) ? { ...block, type: 'text' } : block) : event.content;
21
- return { ...event, type: event.role, message: { content } };
22
+ return { ...event, type: event.role, isMeta:event.isMeta===true||event.providerData?.isMeta===true, message: { content } };
22
23
  }
23
24
  if (event.type === 'function_call') {
24
25
  let input;
@@ -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 };