@givolt/i3-composer-agent 2.3.4 → 2.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) 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/hooks/PROTOTYPE.md +19 -0
  14. package/hooks/i3-prototype.cjs +2 -1
  15. package/package.json +3 -1
  16. package/plugin.json +1 -1
  17. package/scripts/ui-prototype-binding-recovery-test.js +84 -0
  18. package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -2
  19. package/scripts/ui-prototype-codebuddy-hook-test.js +4 -4
  20. package/scripts/ui-prototype-execution-gates-test.js +4 -3
  21. package/skills/i3-ui-prototype/SKILL.md +2 -6
  22. package/skills/i3-ui-prototype/references/execution-evidence.md +13 -1
  23. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +3 -3
  24. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +134 -30
  25. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +3 -3
  26. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +1 -1
  27. package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +94 -0
  28. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +44 -13
  29. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +52 -3
  30. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +13 -7
  31. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +9 -2
  32. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +1 -1
  33. package/skills/i3-ui-prototype/scripts/native-command.cjs +19 -2
  34. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +4 -3
  35. package/skills/i3-ui-prototype/scripts/native-files.cjs +10 -1
  36. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +6 -1
  37. package/skills/i3-ui-prototype/scripts/native-tool-name.cjs +5 -0
  38. package/skills/i3-ui-prototype/scripts/native-tool-result.cjs +14 -0
  39. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +1 -1
  40. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +7 -3
  41. package/skills/i3-ui-prototype/scripts/ptc-callbacks.cjs +131 -0
@@ -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
@@ -18,7 +19,11 @@ function eventCall(payload,source) {
18
19
  // pending call remains unsuccessful until a native result is persisted.
19
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||{});
20
21
  });
21
- const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
22
+ // IDE can omit the call ID and invoke Post before persisting the result.
23
+ // A failed earlier attempt can have identical args; only the still-pending
24
+ // request in this exact generation can be the current callback.
25
+ const pending=matches.filter(call=>!call.resultLine);
26
+ const current=payload.hook_event_name==='PreToolUse'||source.format==='codebuddy-ide-index-v1'&&pending.length?pending:matches;
22
27
  assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
23
28
  }
24
29
  function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
@@ -26,16 +31,19 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
26
31
  // IDE can persist the current call only AFTER returning from both Hooks. Its
27
32
  // current native prompt/generation still binds the callback. Never synthesize
28
33
  // a successful call; A checks the persisted request/result at a later stage.
29
- const attempts=toolEvent&&record.product==='ide'&&H.isIDE(payload)?8:1;
34
+ const attempts=toolEvent?8:1;
30
35
  for(let attempt=1;attempt<=attempts;attempt++) {
31
36
  try {
32
- const source=readSource(payload,record,project);
37
+ let source=readSource(payload,record,project);
38
+ if(toolEvent&&record.product==='cli'&&(payload.agent_type==='ptc'||source.calls.some(c=>c.name==='REPL'&&!c.resultLine))){
39
+ source=require('./ptc-callbacks.cjs').observe({payload,record,project,source});
40
+ }
33
41
  let nativeRequestStatus='not-tool-event';
34
42
  if(toolEvent) {
35
43
  if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
36
44
  const latest=source.prompts.at(-1);
37
45
  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);
46
+ const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===toolName(payload.tool_name));
39
47
  if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
40
48
  else nativeRequestStatus='deferred-until-native-persistence';
41
49
  }else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
@@ -48,26 +56,26 @@ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent
48
56
  if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
49
57
  if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
50
58
  const missing=Recovery.missing({source,binding,policy});
51
- if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing)};
59
+ if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
52
60
  }
53
61
  const final=source.latestAssistant||'',claim=/(?:已完成|已经完成|全部通过|可以交付|\b(?:completed|ready for review|all checks passed)\b)/i.test(final)&&!/(?:未完成|尚未|待验证|阻塞|等待|进度|blocked|awaiting|not complete|in progress)/i.test(final);
54
62
  const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
55
63
  if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
56
64
  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)};
65
+ return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
58
66
  }
59
67
  return {continue:true};
60
68
  }
61
- function prototypeForWrite(project,tool,input) {
69
+ function prototypeForWrite(project,tool,input,cwd=project) {
62
70
  if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
63
71
  const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
64
- let current=path.dirname(path.resolve(project,file));const prototypes=path.join(project,'prototypes');
72
+ let current=path.dirname(path.resolve(cwd,file));const prototypes=path.join(project,'prototypes');
65
73
  while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
66
74
  }
67
75
  function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
68
76
  const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
69
77
  const hostProject=options.host==='workbuddy'?env.WORKBUDDY_PROJECT_DIR:env.CODEBUDDY_PROJECT_DIR;
70
- const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):hostProject||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
78
+ const root=options.scope==='project'?path.dirname(options.base):options.nativeProject|| (ideRoot?workspace(payload.transcript_path,payload.session_id):hostProject||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null));assert.ok(root&&path.isAbsolute(root),'Native project directory required');
71
79
  const project=real(root);
72
80
  // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
73
81
  // or session-bound native workspace above; reject all other outside cwd.
@@ -80,19 +88,45 @@ function saveIndex(directory,index) {
80
88
  function toolCwd(payload,record,project) {
81
89
  return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
82
90
  }
83
- function handle(payload,options) {
91
+ function settleNativeRetries(index,source,{project,sessionId}) {
92
+ let changed=false;
93
+ for(const entry of Object.values(index.bindings)) {
94
+ const repair=entry.recovery,expected=repair?.expectedRetry;
95
+ if(!expected||!['required','ready-to-retry'].includes(repair.status)||entry.entryLine!==source.prompts.at(-1)?.line||entry.contextLine!==source.contextLine)continue;
96
+ const matches=source.calls.filter(call=>{
97
+ if(!call.success||!call.resultLine||call.sessionId!==sessionId||call.requestLine<=entry.entryLine||source.stableThroughLine!==undefined&&call.resultLine>source.stableThroughLine)return false;
98
+ if(expected.kind==='ui-write') {
99
+ const input=call.input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
100
+ return typeof file==='string'&&path.resolve(call.cwd||project,file)===expected.file&&G.uiWrite({...call,cwd:call.cwd||project},entry.directory);
101
+ }
102
+ const parsed=nativeCommand(call.name,call.input,call.cwd||project);
103
+ return expected.kind==='stage'&&parsed?.root===entry.directory&&parsed.operation===expected.operation;
104
+ });
105
+ for(const call of matches) {
106
+ const stage=expected.kind==='stage'?expected.stage:null;
107
+ if(stage&&X.check(entry.directory,stage,require('./native-pilot-lib.cjs')).status!=='passed')continue;
108
+ const retry={...expected,toolCallId:call.toolCallId,resultDigest:call.resultDigest,requestLine:call.requestLine,resultLine:call.resultLine};
109
+ const answer=Recovery.record(index,{source,sessionId,directory:entry.directory,event:'PostToolUse',eventKey:E.sha(JSON.stringify(['persisted-native-result',call.toolCallId,call.resultDigest])),stage,policy:X.policy(entry.directory),resolved:stage?['execution-evidence']:[],retrySucceeded:true,retry},{continue:true});
110
+ if(answer.recovery&&JSON.stringify(answer.recovery)!==JSON.stringify(entry.recovery)){entry.recovery=answer.recovery;changed=true;}
111
+ if(entry.recovery?.status==='recovered')break;
112
+ }
113
+ }
114
+ return changed;
115
+ }
116
+ function dispatch(payload,options) {
117
+ payload={...payload,tool_name:toolName(payload.tool_name)};
84
118
  assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
85
119
  if(options.event==='SessionStart')return {continue:true};
86
120
  if(options.event==='UserPromptSubmit'){
87
121
  const prompt=payload.prompt||'';
88
- if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:先 execution-begin,再实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并通过 prototype-deps query 或检测出的本地 Node 入口查询组件。专业模式还须实际调用 UIUX。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
122
+ if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:界面实现前实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并查询实际组件。专业模式还须调用 UIUX。真实浏览器验收使用浏览器 Skill。缺少必要 Skill 时立即补调,成功返回后重试原操作。'};
89
123
  return {continue:true};
90
124
  }
91
125
  let identity,installed;
92
126
  try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
93
127
  const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
94
128
  if(options.event==='PreToolUse'&&project&&path.isAbsolute(project)){
95
- const native=nativeCommand(payload.tool_name,payload.tool_input,project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
129
+ const native=nativeCommand(payload.tool_name,payload.tool_input,payload.cwd||project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,payload.cwd==='/'?project:payload.cwd||project);
96
130
  if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
97
131
  }
98
132
  throw error;
@@ -105,26 +139,32 @@ function handle(payload,options) {
105
139
  try {
106
140
  source=eventSource(payload,record,project);
107
141
  }catch(error){
108
- const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
109
- if(target&&X.policy(target)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},target))return {deny:true,reason:`Native evidence unavailable for strict prototype UI write: ${error.message}`};
142
+ // The outer diagnostic handler must persist and bound this failure as well;
143
+ // returning an uncounted denial here used to create an endless repair loop.
110
144
  throw error;
111
145
  }
112
146
  const latest=source.prompts.at(-1);
113
147
  if(C.readOnly(latest.text)) {
114
- const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
148
+ const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
115
149
  if(options.event==='PreToolUse'&&(native&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(native.operation)||root&&X.policy(root)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},root)))return {deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'};
116
150
  return {continue:true};
117
151
  }
118
152
  const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
119
- const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
153
+ let index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
154
+ if(Object.values(index.bindings).some(entry=>['required','ready-to-retry'].includes(entry.recovery?.status))) {
155
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
156
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
157
+ try{index=JSON.parse(C.read(file));if(settleNativeRetries(index,source,{project,sessionId:payload.session_id}))saveIndex(stateDirectory,index);}
158
+ finally{fs.rmdirSync(lock);}
159
+ }
120
160
  const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
121
161
  if(options.event==='PreToolUse'&&command?.operation==='create'&&(installed?.schemaVersion===2||E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text))){
122
162
  const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
123
163
  const hostAt=command.argv.indexOf('--host');
124
164
  if(at<0||command.argv[at+1]!==expected||installed?.schemaVersion===2&&(hostAt<0||command.argv[hostAt+1]!==identity.host))return {deny:true,reason:`New ${identity.host} prototypes require --host ${identity.host} --execution-policy ${expected} and fixed --execution-assertions. Existing artifacts keep their original contracts.`};
125
165
  }
126
- const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
127
- const existing=directory?index.bindings[directory]:null;
166
+ const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
167
+ let existing=directory?index.bindings[directory]:null;
128
168
  const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
129
169
  let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
130
170
  const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
@@ -132,8 +172,10 @@ function handle(payload,options) {
132
172
  const recoveryContext=target=>{
133
173
  const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
134
174
  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'})}}:{})};
175
+ const input=payload.tool_input||{},uiFile=input.file_path||input.filePath||input.path||input.TargetFile;
176
+ 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;
177
+ 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,
178
+ ...(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
179
  };
138
180
  const respond=answer=>{
139
181
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -141,37 +183,52 @@ function handle(payload,options) {
141
183
  try{const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');const result=Recovery.record(fresh,recoveryContext(directory),answer);if(fresh.bindings[directory]&&result.recovery)fresh.bindings[directory].recovery=result.recovery;saveIndex(stateDirectory,fresh);return result;}
142
184
  finally{fs.rmdirSync(lock);}
143
185
  };
144
- if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
186
+ const automatic=options.event==='PreToolUse'&&directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)&&
187
+ (command||G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory))&&command?.operation!=='execution-begin'&&
188
+ (!existing||existing.entryLine!==latest.line||existing.contextLine!==source.contextLine||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
189
+ if(options.event==='PreToolUse'&&command?.operation==='execution-begin'||automatic) {
145
190
  assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
146
191
  const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
147
192
  assert.equal(fixed.host,identity.host,'This prototype has a fixed contract for another host; inspect it or explicitly migrate and revalidate, do not inherit its invocation proof');
148
193
  const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
149
- assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
194
+ assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text)||automatic&&require('./execution-diagnostics.cjs').intent(latest.text),'Native prototype Skill or actual shortcut entry required');
150
195
  const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
151
196
  const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
152
- index.bindings[directory]={schemaVersion:1,runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
197
+ index.bindings[directory]={schemaVersion:1,...(automatic?{association:'automatic'}:{}),runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
153
198
  operation:meta.revision==='r001'?'create':'revise',host:identity.host,identitySource:identity.identitySource,product:record.product,productVersion:identity.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
154
199
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
155
- try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
200
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
156
201
  try {
157
202
  const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
158
203
  saveIndex(stateDirectory,fresh);
159
204
  F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
160
205
  }finally{fs.rmdirSync(lock);}
161
- return {continue:true};
206
+ existing=index.bindings[directory];
207
+ if(!automatic)return {continue:true};
162
208
  }
163
209
  if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
164
210
  const fixed=X.policy(directory),answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd,policy:fixed});
165
211
  if(answer.deny)return respond(answer);
212
+ if(options.event==='PostToolUse'&&command&&(command.operation==='execution-begin'||stage)){
213
+ // IDE may not have persisted a callback's result yet. Only observed native
214
+ // failures produce recovery feedback; outer tool success is insufficient.
215
+ if(callbackCall?.resultLine&&!callbackCall.success){
216
+ const missing=command.operation==='execution-begin'?['execution-begin']:['execution-evidence'];
217
+ return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})});
218
+ }
219
+ if(command.operation==='execution-begin')return respond({continue:true});
220
+ }
166
221
  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'])});
222
+ if(existing?.association!=='automatic'&&!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
168
223
  const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
169
224
  if(checked.status==='failed'){
170
225
  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('; ')});
226
+ return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})+' '+checked.errors.join('; ')});
172
227
  }
173
228
  }
174
- if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event))respond({continue:true});
229
+ if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event)){
230
+ const recovery=respond({continue:true});if(recovery.deny||recovery.feedback||recovery.context)return recovery;
231
+ }
175
232
  }
176
233
  if(!Object.keys(index.bindings).length)return {continue:true};
177
234
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -193,4 +250,51 @@ function handle(payload,options) {
193
250
  }finally{fs.rmdirSync(lock);}
194
251
  return {continue:true};
195
252
  }
253
+ function handle(payload,options){
254
+ const D=require('./execution-diagnostics.cjs');
255
+ payload={...payload,tool_name:toolName(payload.tool_name)};
256
+ let diagnostic;
257
+ try{
258
+ if(options.event==='UserPromptSubmit')D.remember(payload,options);
259
+ diagnostic=D.context(payload,options);
260
+ }catch{/* The authoritative dispatcher still checks registration and identity. */}
261
+ const toolEvent=['PreToolUse','PostToolUse'].includes(options.event);
262
+ const cwd=diagnostic&&toolCwd(payload,diagnostic.record,diagnostic.project);
263
+ const command=diagnostic&&nativeCommand(payload.tool_name,payload.tool_input,cwd);
264
+ const root=diagnostic&&(command?.root||prototypeForWrite(diagnostic.project,payload.tool_name,payload.tool_input,cwd));
265
+ const mutation=command&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(command.operation)||root&&G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},root);
266
+ if(diagnostic&&toolEvent&&['Write','Edit','MultiEdit'].includes(payload.tool_name)&&!mutation)return {continue:true};
267
+ if(diagnostic?.task.readOnly&&toolEvent)return options.event==='PreToolUse'&&mutation?{deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'}:{continue:true};
268
+ if(diagnostic&&!diagnostic.task.active&&toolEvent&&!mutation)return {continue:true};
269
+ if(diagnostic&&mutation&&!diagnostic.task.active)diagnostic.task={...diagnostic.task,active:true};
270
+ const guarded=mutation?D.guard(diagnostic,payload):null;if(guarded)return guarded;
271
+ if(D.blocked(diagnostic)&&toolEvent&&!mutation)return {continue:true};
272
+ try{
273
+ const answer=dispatch(payload,diagnostic?{...options,nativeProject:diagnostic.project}:options);
274
+ if(toolEvent&&!answer.deny&&!answer.feedback)D.observed(diagnostic,payload);
275
+ if(options.event==='PostToolUse'&&command?.operation==='execution-begin'&&!answer.deny&&root&&fs.existsSync(path.join(root,'qa/execution-context.json'))){
276
+ const pointer=F.json(path.join(root,'qa/execution-context.json'));
277
+ const source=diagnostic&&C.source(payload,diagnostic.record,diagnostic.project);
278
+ if(pointer&&completedBegin(source,{...pointer,entryLine:source.prompts.at(-1).line,contextLine:source.contextLine,directory:root}))D.recover(diagnostic);
279
+ }
280
+ return answer;
281
+ }catch(error){
282
+ if(diagnostic){const failure=D.failure(diagnostic,payload,error);if(!mutation&&failure.deny){const {deny,reason,...rest}=failure;return {...rest,continue:true,context:reason};}return failure;}
283
+ if(options.event==='PreToolUse') {
284
+ // A damaged/absent task source must not turn a protected write into exit 1
285
+ // (which CodeBuddy treats as non-blocking). This is a failure-only fallback,
286
+ // not a successful task association or invocation receipt.
287
+ const cwd=payload.cwd==='/'&&options.scope==='project'?path.dirname(options.base):payload.cwd;
288
+ if(cwd&&path.isAbsolute(cwd)) {
289
+ const parsed=nativeCommand(payload.tool_name,payload.tool_input,cwd),input=payload.tool_input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
290
+ let target=typeof file==='string'?path.dirname(path.resolve(cwd,file)):parsed?.root;
291
+ while(target&&target!==path.dirname(target)) {
292
+ if(fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target)&&(parsed||G.uiWrite({name:payload.tool_name,input,cwd},target)))return {deny:true,reason:'原型 Hook 运行异常,当前操作未放行:'+error.message+'。请停止自动重试并交由维护入口处理;原型会话不排查 Hook。'};
293
+ target=path.dirname(target);
294
+ }
295
+ }
296
+ }
297
+ throw error;
298
+ }
299
+ }
196
300
  module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
@@ -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
  }
@@ -0,0 +1,94 @@
1
+ 'use strict';
2
+ // Protocol failures exist before a run binding. Never turn them into invocation proof.
3
+ const fs=require('node:fs'),path=require('node:path');
4
+ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
5
+ const intent=text=>typeof text==='string'&&!C.readOnly(text)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(text)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(text));
6
+ const read=file=>fs.existsSync(file)?JSON.parse(C.read(file)):null;
7
+ function location(payload,options){
8
+ if(!payload.session_id||!path.isAbsolute(options.base||''))return null;
9
+ const installation=read(path.join(options.base,'state/i3-prototype/installation.json'));if(!installation)return null;
10
+ const identity=H.identify(payload,installation,options),pointer=`state/i3-prototype/tasks/${E.sha(identity.host+':'+identity.product+':'+payload.session_id).slice(0,24)}.json`;
11
+ const previous=read(F.inside(options.base,pointer));
12
+ let project=options.scope==='project'?path.dirname(options.base):previous?.project;
13
+ if(!project)project=H.isIDE(payload)&&payload.cwd==='/'?E.ideWorkspace(payload.transcript_path,payload.session_id):payload.cwd;
14
+ if(!project||!path.isAbsolute(project))return null;project=fs.realpathSync(project);
15
+ if(payload.cwd&&payload.cwd!=='/'){
16
+ const relative=path.relative(project,fs.realpathSync(payload.cwd));if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
17
+ }
18
+ const record={...installation,...identity,base:options.base,registrationDigest:E.sha(JSON.stringify(installation)),settingsDigest:E.sha(C.read(path.join(options.base,'settings.json')))};
19
+ return {project,record,identity,pointer,previous,directory:C.sessionDirectory(record,project,payload.session_id)};
20
+ }
21
+ function context(payload,options,{source,prompt}={}){
22
+ const where=location(payload,options);if(!where)return null;
23
+ let native=source;if(!native)try{native=C.source(payload,where.record,where.project);}catch{/* Failure diagnostics must survive unreadable native history. */}
24
+ const latest=native?.prompts.at(-1),text=prompt??latest?.text??where.previous?.text;
25
+ if(typeof text!=='string')return null;
26
+ const digest=latest?.digest||E.sha(text),entryLine=latest?.line||where.previous?.entryLine||null;
27
+ const taskKey=E.sha(JSON.stringify([payload.session_id,payload.prompt_id||null,entryLine,digest]));
28
+ // A callback without prompt_id reuses only the exact observed native task.
29
+ const task=payload.hook_event_name!=='UserPromptSubmit'&&where.previous&&where.previous.digest===digest&&where.previous.entryLine===entryLine?where.previous:
30
+ {schemaVersion:1,taskKey,sessionId:payload.session_id,project:where.project,host:where.identity.host,product:where.identity.product,text,digest,entryLine,active:intent(text),readOnly:C.readOnly(text),at:new Date().toISOString()};
31
+ return {...where,source:native,task};
32
+ }
33
+ function remember(payload,options){
34
+ const value=context(payload,options,{prompt:payload.prompt});if(!value)return;
35
+ F.write(options.base,value.pointer,value.task);
36
+ }
37
+ function state(value){return read(path.join(value.directory,'diagnostics.json'))||{schemaVersion:1,tasks:{}};}
38
+ function current(value){return value&&state(value).tasks[value.task.taskKey];}
39
+ function blocked(value){return current(value)?.status==='blocked';}
40
+ function message(failure){return `原型 Hook 运行异常${failure.status==='blocked'?',已达到自动恢复上限':''}:${failure.reason}。请简要报告当前原型及未验收项,由维护入口处理;不要在原型任务中检查日志、修复 Hook 或重复绑定,也不能宣称已验证。`;}
41
+ function failure(value,payload,error){
42
+ if(!value?.task.active)return {continue:true};
43
+ return F.withLock(value.directory,'.diagnostics.lock',()=>{
44
+ const store=state(value),key=value.task.taskKey;
45
+ const phase=/JSON|transcript|source|history|日志/i.test(error.message)?'native-source':'native-request';
46
+ const eventKey=E.sha(JSON.stringify([key,payload.tool_use_id||payload.call_id||payload.hook_event_name,payload.tool_name||null,payload.tool_input||null,phase]));
47
+ const item=store.tasks[key]||{schemaVersion:1,taskKey:key,total:0,counts:{},events:{},status:'required'};
48
+ if(!item.events[eventKey]&&item.status!=='blocked'){
49
+ if(item.total>=6||(item.counts[phase]||0)>=2)item.status='blocked';
50
+ else {item.total++;item.counts[phase]=(item.counts[phase]||0)+1;item.status='required';}
51
+ Object.assign(item,{eventKey,reason:String(error.message).slice(0,2000),phase,at:new Date().toISOString(),host:value.identity.host,product:value.identity.product,productVersion:value.identity.productVersion,
52
+ callMode:payload.agent_type==='ptc'||value.source?.calls.some(c=>c.name==='REPL')?'ptc-repl':value.identity.product==='ide'?'native-ide':'direct',sessionId:payload.session_id,project:value.project,
53
+ registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,transcriptPath:payload.transcript_path});
54
+ item.events[eventKey]=true;store.tasks[key]=item;F.write(value.directory,'diagnostics.json',store);
55
+ }
56
+ const result={reason:message(item),failure:item};
57
+ return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:payload.hook_event_name==='Stop'?{...result,feedback:true}:{...result,continue:true,context:result.reason};
58
+ });
59
+ }
60
+ function guard(value,payload){
61
+ if(!value?.task.active||!blocked(value))return null;
62
+ const item=current(value),reason=message(item);
63
+ // Do not trap the whole host. Read tools and explicit state/check commands remain available.
64
+ const command=require('./native-command.cjs').nativeCommand(payload.tool_name,payload.tool_input,value.project);
65
+ const managed=command&&['create','execution-begin','revise','replan','build','record-run','record-devtools-review','finalize'].includes(command.operation)||['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(payload.tool_name);
66
+ if(payload.hook_event_name==='PreToolUse'&&managed)return {deny:true,reason,failure:item};
67
+ return null;
68
+ }
69
+ function recover(value){
70
+ if(!value)return;
71
+ return F.withLock(value.directory,'.diagnostics.lock',()=>{const store=state(value),item=store.tasks[value.task.taskKey];
72
+ if(item&&item.status!=='blocked'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
73
+ });
74
+ }
75
+ function observed(value,payload){
76
+ if(!value?.task.active)return;
77
+ const source=C.source(payload,value.record,value.project),at=new Date().toISOString();
78
+ const entry={schemaVersion:1,sessionId:payload.session_id,taskKey:value.task.taskKey,project:value.project,projectKey:C.projectKey(value.project),host:value.identity.host,product:value.identity.product,
79
+ productVersion:value.identity.productVersion,identitySource:value.identity.identitySource,registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,
80
+ source:{path:source.path,bytes:source.bytes,digest:source.digest},callMode:source.callMode||'direct',skills:Object.fromEntries(['prototype','frontend','antdv','uiux','browser'].map(key=>[key,E.loaded(source.calls,key).length])),
81
+ lastHook:{event:payload.hook_event_name,at}};
82
+ F.write(value.directory,'observation.json',entry);
83
+ }
84
+ function latest({base,project,host,record}){
85
+ const parent=path.join(base,'state/i3-prototype',C.projectKey(project));let found=null;
86
+ if(!fs.existsSync(parent))return null;
87
+ for(const name of fs.readdirSync(parent)){
88
+ if(!/^[a-f0-9]{24}$/.test(name))continue;
89
+ const data=read(path.join(parent,name,'diagnostics.json'));if(!data)continue;
90
+ for(const item of Object.values(data.tasks||{}))if(item.host===host&&item.project===project&&item.registrationDigest===record.registrationDigest&&item.settingsDigest===record.settingsDigest&&item.status!=='recovered'&&(!found||item.at>found.at))found=item;
91
+ }
92
+ if(!found)return null;const {events,...summary}=found;return summary;
93
+ }
94
+ module.exports={intent,context,remember,current,blocked,failure,guard,recover,observed,latest};
@@ -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 };