@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
@@ -30,7 +30,7 @@ function skill(data,ref,key,before=Infinity) {
30
30
  }
31
31
  function quote(data,ref) {
32
32
  const call=nativeRef(data,ref);assert.ok(typeof ref.quote==='string'&&ref.quote.trim(),'Actual native result quote required');
33
- assert.ok(E.contentText(call.result).includes(ref.quote),'Quoted finding absent from native result');return call;
33
+ assert.ok(E.resultText(call.result).includes(ref.quote),'Quoted finding absent from native result');return call;
34
34
  }
35
35
  function identity(d) {
36
36
  P.validate(d.policy);assert.equal(d.binding?.schemaVersion,1,'Hook task binding required');assert.equal(d.proof?.schemaVersion,1,'Execution evidence required');
@@ -38,12 +38,19 @@ function identity(d) {
38
38
  same(d.binding.policyDigest,d.policy.digest,'Execution policy differs');same(d.binding.inputDigest,d.current.inputDigest,'Inputs changed; begin a new execution run');
39
39
  for(const key of ['inputDigest','candidateDigest','revision'])same(d.proof[key],key==='candidateDigest'?d.current.digest:d.current[key],`Current ${key} differs`);
40
40
  assert.ok(d.source.prompts.some(p=>p.line===d.binding.entryLine),'Native user task source missing');
41
+ if(d.binding.association==='automatic'){
42
+ const prompt=d.source.prompts.find(p=>p.line===d.binding.entryLine);
43
+ same(prompt?.digest,d.binding.promptDigest,'Automatic execution context belongs to a different native task');
44
+ same(d.binding.contextLine,d.source.contextLine,'Automatic execution context changed');
45
+ }else assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
41
46
  if(d.policy.id===P.AUTO_ID){same(d.binding.host,d.policy.host,'Actual native host differs from the fixed policy');assert.ok(d.binding.identitySource&&d.binding.identitySource!=='legacy-profile','Automatic policy requires actual native host identity');}
42
47
  if(d.binding.recovery){
43
48
  const recovery=d.binding.recovery;
49
+ assert.notEqual(recovery.status,'blocked','Recovery budget exhausted; a new human task is required');
44
50
  for(const ref of recovery.invocations||[]){const call=nativeRef(d,ref);same(call.skill,ref.skill,'Recovery Skill differs');same(call.requestLine,ref.requestLine,'Recovery request cursor differs');same(call.resultLine,ref.resultLine,'Recovery result cursor differs');assert.ok(call.requestLine>ref.feedbackLine,'Recovery invocation must follow its feedback');}
45
51
  if(recovery.status==='recovered'){
46
52
  const retry=nativeRef(d,recovery.retry);
53
+ if(recovery.expectedRetry)for(const [key,value] of Object.entries(recovery.expectedRetry))same(recovery.retry[key],value,'Recovery retry differs from the denied operation');
47
54
  if(recovery.retry.kind!==undefined)assert.ok(Number.isFinite(recovery.retry.feedbackLine)&&recovery.retry.feedbackLine>=d.binding.entryLine&&retry.requestLine>recovery.retry.feedbackLine,'Recovery retry must follow its feedback');
48
55
  if(recovery.retry.kind==='stage'){
49
56
  const parsed=command.nativeCommand(retry.name,retry.input,retry.cwd||d.binding.project||d.binding.directory);
@@ -54,6 +61,7 @@ function identity(d) {
54
61
  }else {
55
62
  assert.ok(recovery.retry.kind===undefined||recovery.retry.kind==='ui-write','Unknown recovery retry kind');
56
63
  assert.ok(uiWrite({...retry,cwd:retry.cwd||d.binding.project},d.binding.directory),'Recovery retry is not a native UI write');
64
+ if(recovery.expectedRetry?.file){const input=retry.input,file=input.file_path||input.filePath||input.path||input.TargetFile;same(path.resolve(retry.cwd||d.binding.project,file),recovery.expectedRetry.file,'Recovery wrote a different UI file');}
57
65
  }
58
66
  for(const ref of recovery.invocations||[])assert.ok(retry.requestLine>ref.resultLine,'Retry must follow successful Skill results');
59
67
  }
@@ -106,7 +114,7 @@ function design(d) {
106
114
  assert.ok(query.decisions?.length,'UIUX result decisions required');
107
115
  for(const decision of query.decisions) {
108
116
  assert.ok(['adopted','adjusted','rejected','no-match'].includes(decision.decision)&&decision.rationale&&decision.identity,'UIUX decision identity and rationale required');
109
- assert.ok(E.contentText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
117
+ assert.ok(E.resultText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
110
118
  if(['adopted','adjusted'].includes(decision.decision))assert.ok(Object.hasOwn(d.files||{},decision.implementation)&&decision.scenarioId,'Adopted UIUX result needs source and verification scenario');
111
119
  }
112
120
  }
@@ -115,7 +123,7 @@ function design(d) {
115
123
  }
116
124
  function build(d) {
117
125
  assert.ok(d.build?.buildId&&d.build?.buildDigest,'Current checked build required');
118
- const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.contentText(call.result);
126
+ const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.resultText(call.result);
119
127
  assert.ok(['Bash','execute_command','run_terminal_command'].includes(call.name)&&input.includes('native-pilot.cjs')&&/\bbuild\b/.test(input)&&input.includes(d.binding.directory),'Actual current build command required');
120
128
  assert.ok(output.includes(d.build.buildId)&&output.includes(d.build.buildDigest),'Native build result differs from current build');
121
129
  same(d.build.candidateDigest,d.current.digest,'Build candidate is stale');return call;
@@ -154,8 +162,49 @@ function browser(d) {
154
162
  A.compare(A.extract(A.resultData(call.result),check.pointer),check);
155
163
  }
156
164
  }
165
+ function automatic(d,stage){
166
+ P.validate(d.policy);
167
+ const prompt=d.source.prompts.at(-1);same(d.binding.entryLine,prompt?.line,'Current prototype task differs');same(d.binding.promptDigest,prompt?.digest,'Current prototype task changed');
168
+ same(d.binding.inputDigest,d.current.inputDigest,'Prototype inputs changed');same(d.binding.contextLine,d.source.contextLine,'Native context changed');
169
+ assert.notEqual(d.binding.recovery?.status,'blocked','Skill recovery budget exhausted');
170
+ const needs=P.requirements(d.policy,stage),calls=d.source.calls.filter(c=>c.sessionId===d.binding.sessionId&&c.success&&c.resultLine&&c.requestLine>d.source.contextLine);
171
+ const refs={},required=needs.filter(k=>['frontend','antdv','uiux','browser'].includes(k));
172
+ if(needs.includes('implementation'))required.push('frontend',...(d.policy.officialCallable?['antdv']:[]),...(d.policy.mode==='professional'?['uiux']:[]));
173
+ const writes=calls.filter(c=>c.requestLine>=d.binding.entryLine&&uiWrite({...c,cwd:c.cwd||d.binding.project},d.binding.directory));
174
+ for(const key of [...new Set(required)]){
175
+ const loaded=calls.filter(c=>c.skill===key),firstWrite=writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity;
176
+ const call=['frontend','antdv','uiux'].includes(key)?loaded.find(c=>c.resultLine<firstWrite):loaded.at(-1);
177
+ assert.ok(call,`Actual ${key} Skill invocation required before UI implementation; invoke it and retry this operation`);
178
+ if(['frontend','antdv','uiux'].includes(key)&&writes.length)assert.ok(call.resultLine<Math.min(...writes.map(c=>c.requestLine)),`${key} Skill must precede business UI writes`);
179
+ refs[key]={toolCallId:call.toolCallId,resultDigest:call.resultDigest};
180
+ }
181
+ if(needs.includes('implementation'))assert.ok(writes.length,'Actual business UI implementation write required');
182
+ if(needs.includes('build')){
183
+ assert.ok(d.build&&d.build.candidateDigest===d.current.digest,'Current prototype build required');
184
+ const built=calls.find(c=>{const commandInfo=command.nativeCommand(c.name,c.input,c.cwd||d.binding.project);const result=E.resultValue(c.result);return commandInfo?.operation==='build'&&commandInfo.root===d.binding.directory&&result?.buildId===d.build.buildId&&result?.buildDigest===d.build.buildDigest;});
185
+ assert.ok(built,'Actual current build result required');refs.build={toolCallId:built.toolCallId,resultDigest:built.resultDigest};
186
+ }
187
+ if(needs.includes('browser-results')){
188
+ const loaded=calls.filter(c=>c.skill==='browser').at(-1),built=calls.find(c=>c.toolCallId===refs.build?.toolCallId);
189
+ const observed=calls.filter(c=>/^mcp__.*chrome.*devtools.*__/.test(c.name)&&c.requestLine>Math.max(loaded.resultLine,built.resultLine));
190
+ assert.ok(observed.some(c=>/__(?:new_page|navigate_page)$/.test(c.name)),'Actual browser navigation required');
191
+ assert.ok(observed.some(c=>c.name.endsWith('__evaluate_script')&&E.resultText(c.result).includes(d.current.digest)),'Actual browser candidate identity required');
192
+ for(const method of ['take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_stop_trace'])assert.ok(observed.some(c=>c.name.endsWith('__'+method)),`Actual browser ${method} required`);
193
+ assert.ok(d.business&&d.business.candidateDigest===d.current.digest&&d.business.buildId===d.build.buildId,'Current business browser report required');
194
+ const fixed=A.validate(d.assertions,d.business.journeys.map(j=>j.id));
195
+ for(const scenario of fixed.scenarios)for(const check of scenario.checks){
196
+ const observation=d.business.generationObservations?.find(o=>o.scenarioId===scenario.scenarioId);
197
+ const results=observed.filter(c=>c.name.endsWith('__'+check.method)&&(!observation||observation.toolCallIds.includes(c.toolCallId)));
198
+ assert.ok(results.some(c=>{try{A.compare(A.extract(A.resultData(c.result),check.pointer),check);return true;}catch{return false;}}),`Actual browser assertion missing: ${scenario.scenarioId} ${check.pointer}`);
199
+ }
200
+ }
201
+ return {status:'passed',stage,policyDigest:d.policy.digest,runId:d.binding.runId,sessionId:d.binding.sessionId,projectKey:d.binding.projectKey,host:d.policy.host,productForm:d.binding.product,productVersion:d.binding.productVersion,installationScope:d.binding.scope,
202
+ operation:d.binding.operation,mode:d.policy.mode,inputDigest:d.current.inputDigest,candidateDigest:d.current.digest,revision:d.current.revision,association:'automatic',skills:refs,source:{path:d.source.path,bytes:d.source.bytes,digest:d.source.digest},
203
+ ...(d.source.callbackJournal?{callbackJournal:d.source.callbackJournal}:{}),...(d.binding.recovery?{recovery:d.binding.recovery}:{}),invocationVerification:'native-source-correlated',tamperProof:false};
204
+ }
157
205
  function evaluate(data,stage) {
158
206
  try {
207
+ if(data.binding?.association==='automatic')return automatic(data,stage);
159
208
  identity(data);const needs=P.requirements(data.policy,stage);
160
209
  if(needs.includes('frontend')||needs.includes('implementation'))frontend(data,needs.includes('implementation'));
161
210
  if(needs.some(n=>['antdv','official-source','uiux'].includes(n)))design(data);
@@ -1,16 +1,19 @@
1
1
  'use strict';
2
2
  // Deterministic recovery state; only native source results can resolve a Skill.
3
3
  const E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs');
4
+ const N=require('./native-command.cjs');
4
5
  const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
6
+ const blockedReason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
5
7
  function missing({source,binding,policy,stage}){
6
- const unbound=!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine;
8
+ const unbound=binding?.association!=='automatic'&&!N.completedBegin(source,binding);
7
9
  const required=stage&&policy?P.requirements(policy,stage).filter(key=>Object.hasOwn(names,key)):['frontend',...(policy?.id===P.AUTO_ID?[...(policy.officialCallable?['antdv']:[]),...(policy.mode==='professional'?['uiux']:[])]:[])];
8
10
  const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
9
11
  return [...(unbound?['execution-begin']:[]),...required.filter(key=>!E.loaded(source.calls,key).some(call=>call.sessionId===sessionId&&call.requestLine>source.contextLine&&(source.stableThroughLine===undefined||call.resultLine<=source.stableThroughLine)))];
10
12
  }
11
- function instruction(keys){
13
+ function instruction(keys,{directory,automatic=false}={}){
12
14
  const skills=keys.filter(key=>names[key]).map(key=>names[key]);
13
- return `${keys.includes('execution-begin')?'先为当前任务执行 execution-begin。':''}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。':''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
15
+ const begin=keys.includes('execution-begin')?`先为当前任务独立执行 execution-begin${directory?':\n'+N.beginCommand(directory)+'\n':'。'}不得拼接 cd &&、管道、重定向或 head;等待 status=bound 且退出码为 0,再重试原操作。`:'';
16
+ return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?(automatic?'按本次构建或交互验收返回的具体缺项修正原型后重试;调用记录由 Hooks 自动采集,不检查或修复 Hooks。':'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。'):''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
14
17
  }
15
18
  function record(index,context,answer){
16
19
  const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
@@ -19,16 +22,17 @@ function record(index,context,answer){
19
22
  index.recoveries||={};
20
23
  const state=index.recoveries[taskKey]||={schemaVersion:1,total:0,counts:{},events:{},pending:{},invocations:[],status:'required'};
21
24
  state.feedbackLine??=Math.max(prompt.line,...Object.values(state.pending).map(item=>item.line));
22
- const summary=()=>({taskKey,status:state.status,total:state.total,counts:state.counts,invocations:state.invocations,...(state.retry?{retry:state.retry}:{})});
25
+ const summary=()=>({taskKey,status:state.status,total:state.total,counts:state.counts,invocations:state.invocations,...(state.expectedRetry?{expectedRetry:state.expectedRetry}:{}),...(state.retry?{retry:state.retry}:{})});
23
26
  const remember=result=>{state.events[eventKey]=result;for(const key of Object.keys(state.events).slice(0,-128))delete state.events[key];return {...result,recovery:summary()};};
24
27
  if(state.events[eventKey])return {...state.events[eventKey],recovery:summary()};
28
+ if(state.status==='blocked'&&!answer.deny&&!answer.feedback)return remember(event==='PreToolUse'&&context.expectedRetry?{deny:true,reason:blockedReason}:{...answer,context:blockedReason});
25
29
  if(answer.deny||answer.feedback){
30
+ if(context.expectedRetry&&(!state.expectedRetry||state.status==='recovered'))state.expectedRetry=context.expectedRetry;
26
31
  const keys=[...new Set(answer.missing?.length?answer.missing:['execution-evidence'])];
27
32
  const limits=context.policy?.recovery||{perMissing:2,total:6};
28
33
  if(state.total>=limits.total||keys.some(key=>(state.counts[key]||0)>=limits.perMissing)){
29
34
  state.status='blocked';
30
- const reason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
31
- const result=answer.deny?{deny:true,reason,missing:keys}:{continue:true,context:reason};return remember(result);
35
+ const result=answer.deny?{deny:true,reason:blockedReason,missing:keys}:{continue:true,context:blockedReason};return remember(result);
32
36
  }
33
37
  state.total++;for(const key of keys)state.counts[key]=(state.counts[key]||0)+1;state.status='required';
34
38
  const line=Math.max(prompt.line,...source.calls.map(call=>call.resultLine||call.requestLine));
@@ -44,7 +48,9 @@ function record(index,context,answer){
44
48
  delete state.pending[key];
45
49
  }
46
50
  if(!Object.keys(state.pending).length&&state.status!=='recovered'){
47
- state.status=context.retrySucceeded&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
51
+ const matchingRetry=!state.expectedRetry||Object.entries(state.expectedRetry).every(([key,value])=>context.retry?.[key]===value);
52
+ const afterSkills=state.invocations.every(call=>context.retry?.requestLine>call.resultLine);
53
+ state.status=context.retrySucceeded&&matchingRetry&&afterSkills&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
48
54
  }
49
55
  }
50
56
  return remember({...answer});
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
  const F=require('./native-files.cjs'),{fs,path,assert,json,inside}=F;
3
3
  const P=require('./execution-policy.cjs'),E=require('./execution-evidence.cjs'),G=require('./execution-gates.cjs'),C=require('./codebuddy-context.cjs');
4
+ const Recovery=require('./execution-recovery.cjs');
4
5
  const optional=(root,file)=>fs.existsSync(inside(root,file))?json(inside(root,file)):null;
5
6
  function policy(directory) {
6
7
  const value=optional(directory,'inputs/execution-policy.json'),metaFile=inside(directory,'prototype.json');
@@ -11,7 +12,10 @@ function policy(directory) {
11
12
  const acceptance=json(inside(directory,'inputs/acceptance.json'));require('./execution-assertions.cjs').validate(json(inside(directory,'inputs/execution-assertions.json')),[...acceptance.requiredScenarios,...acceptance.frameworkScenarios]);return value;
12
13
  }
13
14
  function binding(directory) {
14
- const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; run execution-begin inside enabled CodeBuddy');
15
+ const metadata=inside(directory,'prototype.json');
16
+ const automaticExpected=fs.existsSync(metadata)&&json(metadata).runtime?.executionPolicy===P.AUTO_ID;
17
+ const recover=automaticExpected?'Installed Hooks collect this automatically on the next prototype UI operation. Load the required Skills and continue the UI task; report an infrastructure fault through maintenance if collection fails.':Recovery.instruction(['execution-begin'],{directory});
18
+ const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; '+recover);
15
19
  const project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product,host:pointer.host||'codebuddy'});assert.ok(selected,'Hooks are not enabled or are shadowed');
16
20
  assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
17
21
  const stateDirectory=C.sessionDirectory(selected,project,pointer.sessionId),indexFile=path.join(stateDirectory,'session.json');
@@ -23,6 +27,8 @@ function binding(directory) {
23
27
  const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
24
28
  const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
25
29
  assert.ok(!C.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
30
+ assert.equal(entry.entryLine,source.prompts.at(-1).line,'Native task changed; '+recover);
31
+ assert.equal(entry.contextLine,source.contextLine,'Native context changed; '+recover);
26
32
  return {entry,source,selected};
27
33
  }
28
34
  function data(directory,lib,options={}) {
@@ -33,7 +39,7 @@ function data(directory,lib,options={}) {
33
39
  const pointer=optional(directory,'last-build.json');let build=null;
34
40
  if(pointer)try{build=lib.verifyBuild(directory,pointer.buildId);}catch{/* A changed candidate must be allowed to rebuild; build/browser gates reject null. */}
35
41
  const latest=optional(directory,'qa/latest.json');
36
- const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?json(inside(directory,proof.browser.report)):null);
42
+ const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?optional(directory,proof.browser.report):null);
37
43
  const framework=json(inside(directory,'inputs/framework.json'));
38
44
  return {policy:fixed,current,binding:entry,source,proof,assertions:json(inside(directory,'inputs/execution-assertions.json')),designReview:optional(directory,'qa/design-advice.json'),scope:json(inside(directory,'inputs/scope.json')),implementation:optional(directory,'design-system/implementation.json'),files,frameworkVersion:framework.profile?.versions?.['antdv-next']||json(inside(directory,'app/package.json')).dependencies['antdv-next'].replace(/^[~^]/,''),build,business,devtools:options.devtoolsReport||optional(directory,'qa/devtools-review.json'),selected};
39
45
  }
@@ -54,6 +60,7 @@ function template(directory,lib) {
54
60
  }
55
61
  function begin(directory,lib) {
56
62
  directory=fs.realpathSync(directory);const {entry}=binding(directory);
63
+ assert.equal(entry.inputDigest,lib.manifest(directory).inputDigest,'Inputs changed; '+Recovery.instruction(['execution-begin'],{directory}));
57
64
  return {status:'bound',operation:'execution-begin',directory,runId:entry.runId,sessionId:entry.sessionId,policyDigest:entry.policyDigest,evidenceTemplate:template(directory,lib),
58
65
  next:'Load applicable Skills through CodeBuddy, implement the current scope, and bind actual native result IDs in qa/execution-evidence.json. Binding is not completion.'};
59
66
  }
@@ -3,7 +3,7 @@
3
3
  const fs=require('node:fs'),path=require('node:path');
4
4
  function format(result,event){
5
5
  if(result.deny)return {code:0,output:{hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:result.reason}}};
6
- if(result.feedback)return {code:0,output:{continue:false,reason:result.reason}};
6
+ if(result.feedback)return {code:0,output:event==='Stop'?{continue:false,reason:result.reason}:{continue:true,hookSpecificOutput:{hookEventName:event,additionalContext:result.reason}}};
7
7
  if(result.context)return {code:0,output:{continue:true,...(result.recovery?.status==='blocked'?{systemMessage:result.context}:{}),...(['SessionStart','UserPromptSubmit','PostToolUse'].includes(event)?{hookSpecificOutput:{hookEventName:event,additionalContext:result.context}}:{})}};
8
8
  return {code:0,output:{continue:true}};
9
9
  }
@@ -1,6 +1,8 @@
1
1
  'use strict';
2
2
  // One non-executing parser shared by Hook dispatch and A receipt verification.
3
- const path=require('node:path');
3
+ const path=require('node:path'),assert=require('node:assert/strict');
4
+ const {quote}=require('./codebuddy-registration.cjs');
5
+ const E=require('./execution-evidence.cjs');
4
6
  function tokens(command) {
5
7
  if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
6
8
  const values=[];let value='',quote=null,started=false;
@@ -15,4 +17,19 @@ function nativeCommand(tool,input,cwd) {
15
17
  return {operation,root,argv};
16
18
  }
17
19
  const stageFor=Object.freeze({build:'implementation','record-run':'browser-entry','record-devtools-review':'browser-entry',finalize:'delivery'});
18
- module.exports={tokens,nativeCommand,stageFor};
20
+ function beginCommand(directory){
21
+ assert.ok(typeof directory==='string'&&path.isAbsolute(directory),'Absolute prototype directory required');
22
+ return [process.execPath,path.join(__dirname,'native-pilot.cjs'),'execution-begin',directory].map(quote).join(' ');
23
+ }
24
+ function completedBegin(source,binding){
25
+ if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||(binding.contextLine||0)!==source.contextLine)return null;
26
+ const calls=source.calls.filter(call=>call.sessionId===binding.sessionId&&call.requestLine>=binding.entryLine&&call.requestLine>source.contextLine&&(()=>{
27
+ const command=nativeCommand(call.name,call.input,call.cwd||binding.project||binding.directory);
28
+ return command?.operation==='execution-begin'&&command.root===binding.directory;
29
+ })());
30
+ const latest=calls.at(-1);
31
+ if(!latest?.success||!latest.resultLine||source.stableThroughLine!==undefined&&latest.resultLine>source.stableThroughLine)return null;
32
+ const result=E.resultValue(latest.result);
33
+ return result?.status==='bound'&&result.runId===binding.runId&&result.sessionId===binding.sessionId&&result.directory===binding.directory?latest:null;
34
+ }
35
+ module.exports={tokens,nativeCommand,stageFor,beginCommand,completedBegin};
@@ -56,14 +56,15 @@ function state(directory,options,lib) {
56
56
  }
57
57
  if(build.status==='current')commands.preview=command('preview',['--build-id',build.buildId]);
58
58
  if(devtoolsReview.required)commands.recordDevtoolsReview=command('record-devtools-review',['--report',path.join(directory,'qa/devtools-review-input.json')]);
59
- if(meta.runtime.executionPolicy){commands.executionBegin=command('execution-begin');commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
59
+ const executionContext=read(directory,'qa/execution-context.json');
60
+ if(meta.runtime.executionPolicy&&executionContext){commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
60
61
  let generation=null;
61
62
  if(meta.runtime.generationContract===1)try{const fixed=json(inside(directory,'inputs/generation.json'));const plan=require('./generation-contract.cjs').view(fixed);generation={presentation:fixed.presentation,pendingDecisions:fixed.pendingDecisions,pendingReferenceElements:fixed.pendingReferenceElements.length,referenceReview:plan.referenceReview,implementation:require('./generation-evidence.cjs').validateImplementation(directory,fixed,json(inside(directory,'design-system/implementation.json')),{frameworkMap:json(inside(directory,'inputs/framework.json')).componentMap})};}catch(error){generation={status:'failed',errors:[error.message]};}
62
63
  const nextAction=ownership?'wait-for-owner':structure.status!=='passed'?'repair-structure':generation&&(generation.pendingDecisions.length||generation.pendingReferenceElements)?'resolve-generation-decisions':generation?.implementation.status==='pending'?'implement-business':meta.runtime.contentReference&&meta.business?.status!=='adapted'?'implement-business':!['reviewed','no-match','skipped'].includes(design.status)?'review-design':build.status!=='current'?'build':browser.status!=='current'?(meta.runtime.contentReference?'business-browser-review':'browser'):devtoolsReview.required&&devtoolsReview.status!=='current'?'devtools-review':deliveryCurrent?'return-result':'finalize';
63
64
  if(generation)commands.replan=command('replan',['--scope',path.join(directory,'inputs/scope.json'),'--revision',nextRevision,...expected]);
64
65
  const fixedScope=json(inside(directory,'inputs/scope.json'));
65
66
  const scopes=meta.runtime.generationContract===1?{sourceFile:'inputs/scope.json',schemaVersion:fixedScope.schemaVersion,title:fixedScope.title,moduleIds:fixedScope.moduleIds,pages:fixedScope.pages.map(page=>({id:page.id,title:page.title,referencePage:page.referencePage,regions:page.regions.map(region=>({id:region.id,pattern:region.pattern||region.mode,fields:region.fields?.length||0,actions:region.actions?.length||0}))})),requirements:fixedScope.requirements.map(item=>({id:item.id,pages:item.pages,source:{type:item.source.type,reference:item.source.reference}}))}:fixedScope;
66
- return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction:meta.runtime.executionPolicy&&execution.status==='failed'?'resolve-execution-evidence':nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed commands; no source-code inspection is required to recover flags or digests.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy invocation fields retain their original meaning. New policy checks are reported separately in execution; native-source correlation is not tamper-proof attestation.']};
67
+ return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed prototype commands; installed Hooks execute automatically.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy execution commands remain available for compatibility; prototype work does not require manual binding or Hook diagnosis.']};
67
68
  }
68
69
 
69
70
  function finalize(directory,options,lib) {
@@ -85,7 +86,7 @@ function finalize(directory,options,lib) {
85
86
  const delivery={schemaVersion:1,status:'complete',operation:'finalize',id:meta.id,revision:before.revision,candidateDigest:before.digest,inputDigest:before.inputDigest,framework:meta.framework,template:meta.template,expertMode:meta.expertMode,expert:meta.expert.status,verification:'passed',review:review.status,checks:{structure:'passed',acceptance:'passed',build:'current',browser:'current'},buildId:report.buildId,buildDigest:report.buildDigest,report:latest.report,executor:report.executor,tool:report.tool,handoff:path.join(directory,'HANDOFF.md'),receipt:'qa/delivery.json',openQuestions:scope.openQuestions||[],humanConfirmationRequired:review.status!=='approved'};
86
87
  const execution=require('./execution-runtime.cjs').requireStage(directory,'delivery',lib);
87
88
  delivery.invocationVerification=execution.invocationVerification;delivery.tamperProof=false;
88
- if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));}
89
+ if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=execution.association==='automatic'?F.hash(JSON.stringify(execution)):F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));delivery.executionEvidenceSource=execution.association==='automatic'?'automatic-native-observation':'legacy-execution-evidence';}
89
90
  delivery.browserReportDigest=F.hash(fs.readFileSync(inside(directory,latest.report)));
90
91
  if(devtoolsReview.required){delivery.devtoolsReview=devtoolsReview.report;delivery.devtoolsReviewDigest=devtoolsReview.reportDigest;delivery.checks.devtoolsReview='current';delivery.invocationEvidence=execution.status==='passed'?'Native response IDs correlated with the configured host transcript; not tamper-proof.':'Verify actual Skill/MCP calls independently in the host transcript';}
91
92
  const start='<!-- native-delivery:start -->',end='<!-- native-delivery:end -->';
@@ -28,8 +28,17 @@ function write(root,relative,value) {
28
28
  try { fs.writeFileSync(tmp,typeof value==='string'?value:JSON.stringify(value,null,2)+'\n',{flag:'wx'}); fs.renameSync(tmp,file); }
29
29
  finally { if(fs.existsSync(tmp)) fs.unlinkSync(tmp); }
30
30
  }
31
+ function withLock(root,name,work) {
32
+ fs.mkdirSync(root,{recursive:true,mode:0o700});const lock=inside(root,name);let acquired=false;
33
+ for(let attempt=0;attempt<100;attempt++) {
34
+ try{fs.mkdirSync(lock);acquired=true;break;}
35
+ catch(error){if(error.code!=='EEXIST')throw error;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}
36
+ }
37
+ assert.ok(acquired,'Concurrent native audit update; no success recorded');
38
+ try{return work();}finally{fs.rmdirSync(lock);}
39
+ }
31
40
  function entries(root,files) { return Object.fromEntries([...new Set(files)].sort().map(file=>[file,hash(fs.readFileSync(inside(root,file)))])); }
32
41
  const cacheRoots = new Set(['node_modules','dist','.vite','.git','coverage',...['app','starter'].flatMap(root=>['node_modules','dist','.vite','.git','coverage'].map(name=>`${root}/${name}`))]);
33
42
  // A source/public subdirectory named "dist", or an imported .log file, is still source.
34
43
  const excluded = (relative,name) => cacheRoots.has(relative) || name==='.DS_Store' || ['app/tsconfig.tsbuildinfo','starter/tsconfig.tsbuildinfo'].includes(relative);
35
- module.exports={fs,path,crypto,assert,hash,json,id,inside,walk,write,entries,excluded};
44
+ module.exports={fs,path,crypto,assert,hash,json,id,inside,walk,write,withLock,entries,excluded};
@@ -50,5 +50,10 @@ async function main(argv=process.argv.slice(2)) {
50
50
  default: throw new Error(`Unknown operation: ${operation}`);
51
51
  }
52
52
  }
53
- if(require.main===module) main().then(result=>{if(result!==null){process.stdout.write(JSON.stringify(result,null,2)+'\n');if(result.status==='failed')process.exitCode=1;}}).catch(error=>{process.stderr.write(JSON.stringify({status:'failed',operation:process.argv[2]||'analyze',errors:[error.message]})+'\n');process.exitCode=1;});
53
+ function capture(result,exitCode){
54
+ const args=process.argv.slice(2),at=args.indexOf('--project');let project=at>=0&&args[at+1]?path.resolve(args[at+1]):process.cwd();
55
+ const target=args[1];if(target&&path.isAbsolute(target)&&target.includes(path.sep+'prototypes'+path.sep))project=target.slice(0,target.indexOf(path.sep+'prototypes'+path.sep));
56
+ require('./ptc-callbacks.cjs').captureResult({script:__filename,argv:args,project,result,exitCode});
57
+ }
58
+ if(require.main===module) main().then(result=>{if(result!==null){const code=result.status==='failed'?1:0;capture(result,code);process.stdout.write(JSON.stringify(result,null,2)+'\n');process.exitCode=code;}}).catch(error=>{const result={status:'failed',operation:process.argv[2]||'analyze',errors:[error.message]};try{capture(result,1);}catch{/* The primary failure remains authoritative. */}process.stderr.write(JSON.stringify(result)+'\n');process.exitCode=1;});
54
59
  module.exports={main};
@@ -0,0 +1,5 @@
1
+ 'use strict';
2
+ // Observed Buddy tool aliases only. Preserve original blocks for digest checks.
3
+ const aliases=Object.freeze({use_skill:'Skill',skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'});
4
+ const toolName=name=>Object.hasOwn(aliases,name)?aliases[name]:name;
5
+ module.exports={toolName};
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Capture a real read-only query result when the host's Bash callback omits stdout.
4
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'),{spawnSync}=require('node:child_process');
5
+ const J=require('./ptc-callbacks.cjs');
6
+ function main(argv=process.argv.slice(2)){
7
+ const args=[...argv];assert.equal(args.shift(),'--project','Explicit query project required');const project=fs.realpathSync(args.shift());assert.equal(args.shift(),'--','Separate the executable and its arguments');
8
+ const executable=args.shift();assert.ok(path.isAbsolute(executable)&&fs.statSync(executable).isFile(),'Use the detected absolute local executable; this command does not install tools');
9
+ const run=spawnSync(executable,args,{cwd:project,encoding:'utf8',timeout:30000,maxBuffer:8*1024*1024});
10
+ const result={type:'execute_command_result',exitCode:run.status??1,stdout:run.stdout||'',stderr:run.stderr||'',...(run.error?{error:run.error.message}:{})};
11
+ J.captureResult({script:__filename,argv,project,result,exitCode:result.exitCode});return result;
12
+ }
13
+ module.exports={main};
14
+ if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result)+'\n');process.exitCode=result.exitCode;}catch(error){process.stderr.write(JSON.stringify({status:'failed',errors:[error.message]})+'\n');process.exitCode=1;}
@@ -99,5 +99,5 @@ async function main(argv = process.argv.slice(2)) {
99
99
  return operation === 'check' ? check(options) : operation==='query'?query(options):require('./optional-skill-install.cjs').install(options);
100
100
  }
101
101
  module.exports = { check, skillAt, antdvTool, query, parse, main };
102
- if (require.main === module) main().then(result => {process.stdout.write(JSON.stringify(result, null, 2) + '\n');if(result.status==='failed')process.exitCode=1;})
102
+ if (require.main === module) main().then(result => {const {options}=parse(process.argv.slice(2));require('./ptc-callbacks.cjs').captureResult({script:__filename,argv:process.argv.slice(2),project:path.resolve(options.project||process.cwd()),result,exitCode:result.status==='failed'?1:0});process.stdout.write(JSON.stringify(result, null, 2) + '\n');if(result.status==='failed')process.exitCode=1;})
103
103
  .catch(error => { process.stderr.write(JSON.stringify({ status: 'failed', errors: [error.message] }) + '\n'); process.exitCode = 1; });
@@ -56,8 +56,9 @@ function check(options,runtime={}) {
56
56
  const entries=runtime.entries||[];
57
57
  if(!runtime.entries&&fs.existsSync(parent))for(const name of fs.readdirSync(parent)) {
58
58
  if(!/^[a-f0-9]{24}$/.test(name))continue;
59
- const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
60
- entries.push(...Object.values(JSON.parse(raw).bindings||{}));
59
+ const raw=R.fileIO.read(path.join(parent,name,'session.json'));
60
+ if(raw)entries.push(...Object.values(JSON.parse(raw).bindings||{}));
61
+ const observed=R.fileIO.read(path.join(parent,name,'observation.json'));if(observed)entries.push(JSON.parse(observed));
61
62
  }
62
63
  for(const entry of entries)if((entry.host||'codebuddy')===host&&entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
63
64
  if(observation) {
@@ -68,9 +69,12 @@ function check(options,runtime={}) {
68
69
  result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
69
70
  const observedProduct=observation.product||(record.product!=='auto'?record.product:null);
70
71
  assert.ok(['ide','cli'].includes(observedProduct),'Observed native product identity missing');
71
- Object.assign(result,{product:observedProduct,productVersion:observation.productVersion||'unreported',effective:effectiveByProduct[observedProduct]===true,identitySource:observation.identitySource||'legacy-profile',recovery:observation.recovery||null});
72
+ Object.assign(result,{product:observedProduct,productVersion:observation.productVersion||'unreported',effective:effectiveByProduct[observedProduct]===true,identitySource:observation.identitySource||'legacy-profile',recovery:observation.recovery||null,...(observation.callMode?{callMode:observation.callMode,skills:observation.skills}:{})});
72
73
  }
73
74
  const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
75
+ const failure=require('./execution-diagnostics.cjs').latest({base:current.base,project:current.project,host,record});
76
+ if(failure&&(!observation||failure.at>=observation.lastHook.at))return {...result,status:'failed',failure,errors:[failure.reason],transcript:{...result.transcript,status:'failed',invocationVerified:false},
77
+ next:failure.status==='blocked'?'修复宿主或适配后,由用户发起新的继续任务;只读诊断不消耗恢复次数。':'按 failure.reason 检查当前宿主协议;最多两次补调,不能将注册状态当作执行成功。'};
74
78
  return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
75
79
  activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
76
80
  next:'CLI: review /hooks; desktop: reload. Verify an actual Skill invocation and missing-Skill recovery in the chosen host.'};
@@ -0,0 +1,131 @@
1
+ 'use strict';
2
+ // Native Pre/Post callbacks are the evidence. REPL source and claimed returns are never interpreted.
3
+ const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
4
+ const F=require('./native-files.cjs'),E=require('./execution-evidence.cjs'),C=require('./codebuddy-context.cjs'),P=require('./execution-policy.cjs');
5
+ const {toolName}=require('./native-tool-name.cjs');
6
+ const canonical=x=>Array.isArray(x)?x.map(canonical):x&&typeof x==='object'?Object.fromEntries(Object.keys(x).sort().map(k=>[k,canonical(x[k])])):x;
7
+ const digest=x=>E.sha(JSON.stringify(canonical(x)));
8
+ const fileFor=(record,project,sessionId)=>path.join(C.sessionDirectory(record,project,sessionId),'ptc-callbacks.json');
9
+ function load(file){
10
+ if(!fs.existsSync(file))return null;
11
+ assert.ok(!fs.lstatSync(file).isSymbolicLink()&&fs.statSync(file).size<=64*1024*1024,'Invalid PTC callback journal');return JSON.parse(fs.readFileSync(file,'utf8'));
12
+ }
13
+ function locked(file,work){
14
+ const folder=path.dirname(file);fs.mkdirSync(folder,{recursive:true,mode:0o700});const lock=file+'.lock';let acquired=false;
15
+ for(let n=0;n<40;n++){try{fs.mkdirSync(lock);acquired=true;break;}catch(error){if(error.code!=='EEXIST')throw error;Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10);}}
16
+ assert.ok(acquired,'Concurrent native PTC callback; retry without inventing evidence');
17
+ try{return work();}finally{fs.rmdirSync(lock);}
18
+ }
19
+ function validate(store,source,{record,project,sessionId}){
20
+ assert.equal(store.schemaVersion,1,'Unsupported PTC callback schema');assert.equal(store.sessionId,sessionId,'PTC callback session differs');assert.equal(store.project,project,'PTC callback project differs');
21
+ assert.equal(store.registrationDigest,record.registrationDigest,'PTC callback registration digest changed');assert.equal(store.settingsDigest,record.settingsDigest,'PTC callback settings digest changed');
22
+ assert.equal(store.sourcePath,source.path,'PTC native transcript path changed');
23
+ assert.ok(Number.isSafeInteger(store.sequence)&&store.sequence>=0&&store.sequence<900000,'Invalid native callback sequence');
24
+ const sequences=new Set();
25
+ const sequence=value=>{assert.ok(Number.isSafeInteger(value)&&value>0&&value<=store.sequence&&!sequences.has(value),'Invalid or duplicate native callback sequence');sequences.add(value);};
26
+ for(const [id,item] of Object.entries(store.calls)) {
27
+ assert.equal(item.id,id,'Native callback call ID changed');assert.equal(item.sessionId,sessionId,'Native callback call session changed');
28
+ assert.equal(digest(item.pre),digest({id:item.id,name:item.name,input:item.input,sessionId:item.sessionId,parentId:item.parentId,cwd:item.cwd}),'Native callback request changed');
29
+ sequence(item.preSequence);
30
+ if(item.post){sequence(item.postSequence);assert.ok(item.postSequence>item.preSequence,'Native callback Post must follow Pre');}
31
+ else assert.equal(item.postSequence,undefined,'Native callback result sequence without a result');
32
+ }
33
+ for(const parent of Object.values(store.parents)){
34
+ const call=source.calls.find(c=>c.toolCallId===parent.toolCallId);
35
+ assert.ok(call?.name==='REPL'&&call.sessionId===sessionId,'Native parent REPL request missing or session differs');
36
+ assert.equal(call.requestDigest,parent.requestDigest,'Native parent request digest changed');assert.equal(call.requestLine,parent.requestLine,'Native parent request cursor changed');
37
+ assert.ok(source.prompts.some(p=>p.line===parent.promptLine&&p.digest===parent.promptDigest),'Native parent task source changed');
38
+ }
39
+ }
40
+ function read(source,{record,project,sessionId}){
41
+ source={...source,calls:source.calls.filter(c=>c.evidenceSource!=='native-hook-callback')};
42
+ const store=load(fileFor(record,project,sessionId));if(!store)return source;
43
+ validate(store,source,{record,project,sessionId});
44
+ const inner=[];
45
+ for(const item of Object.values(store.calls)){
46
+ const parent=store.parents[item.parentId];assert.ok(parent,'PTC parent callback anchor missing');
47
+ const name=toolName(item.name),input=item.input,skillName=name==='Skill'?input?.skill||input?.name||input?.command:null;
48
+ assert.ok(!source.calls.some(c=>c.toolCallId===item.id),'Duplicate native inner tool call ID');
49
+ const call={toolCallId:item.id,name,input,skillName:skillName||null,skill:P.skillKey(skillName),sessionId,cwd:item.cwd,contextLine:parent.contextLine,promptLine:parent.promptLine,
50
+ requestLine:parent.requestLine+item.preSequence/1e6,requestDigest:digest(item.pre),success:false,parentToolCallId:item.parentId,evidenceSource:'native-hook-callback'};
51
+ if(item.post){
52
+ let result=item.post.response;
53
+ if(item.cliResult){
54
+ const value=item.cliResult;assert.equal(E.sha(fs.readFileSync(value.script)),value.scriptDigest,'Captured CLI script bytes changed');
55
+ assert.equal(value.inputDigest,digest(item.input),'Captured CLI input differs');
56
+ assert.equal(item.post.response?.exitCode,value.exitCode,'Native Bash exit differs from captured CLI result');
57
+ result={type:'execute_command_result',stdout:typeof value.result==='string'?value.result:JSON.stringify(value.result),exitCode:value.exitCode};
58
+ }
59
+ call.result=result;call.resultLine=parent.requestLine+item.postSequence/1e6;call.resultDigest=digest({post:item.post,cliResult:item.cliResult||null});
60
+ call.success=!item.denied&&!result?.interrupted&&!result?.sandboxDenied&&!result?.signal&&E.successful({content:result});
61
+ }
62
+ inner.push(call);
63
+ }
64
+ return {...source,calls:[...source.calls,...inner].sort((a,b)=>a.requestLine-b.requestLine),callMode:'ptc-repl',callbackJournal:{path:fileFor(record,project,sessionId),digest:digest(store),tamperProof:false}};
65
+ }
66
+ function observe({payload,record,project,source}){
67
+ const sessionId=payload.session_id,name=toolName(payload.tool_name),id=payload.tool_use_id||payload.call_id;
68
+ assert.ok(sessionId&&id,'Native PTC session and tool call ID required');
69
+ assert.ok(!payload.agent_id&&!payload.parent_agent_id,'PTC main task cannot inherit subagent callbacks');
70
+ assert.ok(source.calls.every(c=>c.sessionId===sessionId),'Native PTC callback session differs');
71
+ const task=source.prompts.at(-1);assert.ok(task,'Native PTC user task missing');
72
+ const candidates=source.calls.filter(c=>c.name==='REPL'&&!c.resultLine&&c.requestLine>task.line&&c.contextLine===source.contextLine);
73
+ const direct=source.calls.find(c=>c.toolCallId===id);
74
+ if(direct&&!direct.evidenceSource&&direct.name!=='REPL')return source;
75
+ if(name==='REPL'&&payload.hook_event_name==='PostToolUse'&&direct?.name==='REPL')return read(source,{record,project,sessionId});
76
+ assert.equal(candidates.length,1,'Native PTC callback requires a unique pending parent REPL');
77
+ const parent=candidates[0],file=fileFor(record,project,sessionId);
78
+ locked(file,()=>{
79
+ const store=load(file)||{schemaVersion:1,sessionId,project,sourcePath:source.path,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,sequence:0,parents:{},calls:{}};
80
+ validate(store,source,{record,project,sessionId});
81
+ const next=()=>{assert.ok(++store.sequence<900000,'Native PTC callback journal limit reached');return store.sequence;};
82
+ if(name==='REPL'){
83
+ assert.equal(id,parent.toolCallId,'Native outer REPL call differs');assert.equal(digest(payload.tool_input||{}),digest(parent.input),'Native outer REPL input differs');
84
+ if(payload.hook_event_name==='PreToolUse')store.parents[id]||={toolCallId:id,requestDigest:parent.requestDigest,requestLine:parent.requestLine,promptLine:task.line,promptDigest:task.digest,contextLine:source.contextLine};
85
+ }else{
86
+ assert.ok(store.parents[parent.toolCallId],'Native parent PreToolUse callback missing');
87
+ const input=payload.tool_input||{},pre={id,name,input,sessionId,parentId:parent.toolCallId,cwd:payload.cwd};
88
+ if(payload.hook_event_name==='PreToolUse'){
89
+ if(store.calls[id])assert.equal(digest(store.calls[id].pre),digest(pre),'Conflicting native PTC call input or parent changed');
90
+ else store.calls[id]={...pre,pre,preSequence:next()};
91
+ }else if(payload.hook_event_name==='PostToolUse'){
92
+ const item=store.calls[id];assert.ok(item,'Paired native PTC PreToolUse request missing');assert.equal(digest(item.pre),digest(pre),'Native PTC Post input differs');
93
+ assert.ok(Object.hasOwn(payload,'tool_response'),'Native PTC Post response missing');const post={response:payload.tool_response};
94
+ if(item.post)assert.equal(digest(item.post),digest(post),'Conflicting native PTC Post result changed');
95
+ else {item.post=post;item.postSequence=next();}
96
+ }
97
+ }
98
+ F.write(path.dirname(file),path.basename(file),store);fs.chmodSync(file,0o600);
99
+ });
100
+ return read(source,{record,project,sessionId});
101
+ }
102
+ function captureResult({script,argv,project=process.cwd(),result,exitCode}){
103
+ // Only an actually running first-party entry may capture stdout. Match its exact pending native command.
104
+ const candidates=[path.join(project,'.codebuddy')];let folder=path.dirname(script);
105
+ for(let n=0;n<5;n++,folder=path.dirname(folder))candidates.push(folder);
106
+ const global=C.globalBase();candidates.push(global);
107
+ const matches=[];
108
+ for(const base of [...new Set(candidates)]){
109
+ if(!fs.existsSync(path.join(base,'state/i3-prototype/installation.json')))continue;
110
+ const record=C.registration(base),parent=path.join(base,'state/i3-prototype',C.projectKey(project));if(!fs.existsSync(parent))continue;
111
+ for(const session of fs.readdirSync(parent)){
112
+ if(!/^[a-f0-9]{24}$/.test(session))continue;const file=path.join(parent,session,'ptc-callbacks.json'),store=load(file);if(!store)continue;
113
+ if(store.registrationDigest!==record.registrationDigest||store.project!==project)continue;
114
+ for(const item of Object.values(store.calls)){
115
+ if(item.name!=='Bash'||item.post||item.cliResult)continue;
116
+ const tokens=require('./native-command.cjs').tokens(item.input?.command);
117
+ if(!tokens||tokens.length!==argv.length+2||digest(tokens.slice(2))!==digest(argv))continue;
118
+ const cwd=item.cwd||project,real=file=>{try{return fs.realpathSync(file);}catch{return null;}};
119
+ const executable=tokens[0].includes(path.sep)?path.resolve(cwd,tokens[0]):(process.env.PATH||'').split(path.delimiter).map(dir=>path.resolve(cwd,dir,tokens[0])).find(file=>{try{fs.accessSync(file,fs.constants.X_OK);return fs.statSync(file).isFile();}catch{return false;}});
120
+ if(executable&&real(executable)===real(process.execPath)&&real(path.resolve(cwd,tokens[1]))===real(script))matches.push({file,item});
121
+ }
122
+ }
123
+ }
124
+ if(!matches.length)return false;assert.equal(matches.length,1,'Captured CLI result has multiple pending native requests');
125
+ const {file,item}=matches[0];locked(file,()=>{
126
+ const store=load(file),current=store.calls[item.id];assert.ok(current&&!current.post&&!current.cliResult,'Captured CLI callback changed');
127
+ current.cliResult={script,scriptDigest:E.sha(fs.readFileSync(script)),argv,inputDigest:digest(current.input),result,exitCode,at:new Date().toISOString()};
128
+ F.write(path.dirname(file),path.basename(file),store);
129
+ });return true;
130
+ }
131
+ module.exports={observe,read,captureResult};