@givolt/i3-composer-agent 2.3.3 → 2.3.4

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 (42) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +1 -1
  4. package/.codex-plugin/plugin.json +1 -1
  5. package/README.md +2 -2
  6. package/bin/i3-composer-agent.cjs +18 -15
  7. package/bin/wizard.cjs +12 -52
  8. package/docs/codebuddy-setup.md +21 -7
  9. package/docs/codex-setup.md +1 -1
  10. package/docs/other-hosts.md +1 -1
  11. package/docs/workbuddy-setup.md +18 -2
  12. package/hooks/manage.cjs +22 -14
  13. package/package.json +5 -1
  14. package/plugin.json +1 -1
  15. package/scripts/ui-prototype-buddy-lifecycle-test.js +89 -0
  16. package/scripts/ui-prototype-codebuddy-hook-test.js +1 -1
  17. package/scripts/ui-prototype-dependencies-test.js +13 -0
  18. package/scripts/ui-prototype-execution-gates-test.js +24 -0
  19. package/scripts/ui-prototype-execution-policy-test.js +12 -1
  20. package/scripts/ui-prototype-recovery-test.js +72 -0
  21. package/skills/i3-ui-prototype/SKILL.md +5 -3
  22. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +10 -6
  23. package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
  24. package/skills/i3-ui-prototype/references/execution-evidence.md +25 -8
  25. package/skills/i3-ui-prototype/references/interaction-acceptance.md +1 -1
  26. package/skills/i3-ui-prototype/references/project-layout.md +1 -1
  27. package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
  28. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +14 -14
  29. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +74 -35
  30. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +7 -5
  31. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +1 -1
  32. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +23 -2
  33. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +28 -7
  34. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +52 -0
  35. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +1 -1
  36. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +9 -5
  37. package/skills/i3-ui-prototype/scripts/native-command.cjs +18 -0
  38. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +2 -4
  39. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +5 -1
  40. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
  41. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +13 -13
  42. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +1 -1
@@ -2,30 +2,21 @@
2
2
  const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict');
3
3
  const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs'),G=require('./execution-gates.cjs');
4
4
  const X=require('./execution-runtime.cjs');
5
- function tokens(command) {
6
- if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
7
- const values=[];let value='',quote=null,started=false;
8
- for(let i=0;i<command.length;i++) {const c=command[i];if(quote){if(c===quote){quote=null;started=true;}else if(c==='\\'&&quote==='"'&&i+1<command.length)value+=command[++i];else value+=c;}
9
- else if(c==='"'||c==="'"){quote=c;started=true;}else if(c==='\\'&&i+1<command.length){value+=command[++i];started=true;}else if(/\s/.test(c)){if(started){values.push(value);value='';started=false;}}else if(/[;&|<>]/.test(c))return null;else {value+=c;started=true;}}
10
- if(quote)return null;if(started)values.push(value);return values;
11
- }
12
- function nativeCommand(tool,input,cwd) {
13
- if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
14
- const argv=tokens(input?.command);if(!argv)return null;const index=argv.findIndex(arg=>path.basename(arg)==='native-pilot.cjs');if(index<1)return null;
15
- const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
16
- return {operation,root,argv};
17
- }
5
+ const H=require('./buddy-host.cjs');
6
+ const Recovery=require('./execution-recovery.cjs');
7
+ const {tokens,nativeCommand,stageFor}=require('./native-command.cjs');
18
8
  function eventCall(payload,source) {
9
+ const callId=payload.tool_use_id||payload.call_id;
19
10
  const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
20
11
  const partial=(before,after)=>typeof before==='string'?typeof after==='string'&&after.startsWith(before):Array.isArray(before)?Array.isArray(after)&&before.length<=after.length&&before.every((value,i)=>partial(value,after[i])):before&&typeof before==='object'?after&&typeof after==='object'&&Object.keys(before).every(key=>Object.hasOwn(after,key)&&partial(before[key],after[key])):before===after;
21
12
  const matches=source.calls.filter(call=>{
22
- if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||payload.tool_use_id&&call.toolCallId!==payload.tool_use_id)return false;
13
+ if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
23
14
  if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
24
15
  if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
25
16
  // IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
26
17
  // actual callback with one pending native call in the exact generation. The
27
18
  // pending call remains unsuccessful until a native result is persisted.
28
- return source.format==='codebuddy-ide-index-v1'&&payload.client==='CodeBuddyIDE'&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
19
+ return source.format==='codebuddy-ide-index-v1'&&H.isIDE(payload)&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
29
20
  });
30
21
  const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
31
22
  assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
@@ -35,13 +26,13 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
35
26
  // IDE can persist the current call only AFTER returning from both Hooks. Its
36
27
  // current native prompt/generation still binds the callback. Never synthesize
37
28
  // a successful call; A checks the persisted request/result at a later stage.
38
- const attempts=toolEvent&&record.product==='ide'&&payload.client==='CodeBuddyIDE'?8:1;
29
+ const attempts=toolEvent&&record.product==='ide'&&H.isIDE(payload)?8:1;
39
30
  for(let attempt=1;attempt<=attempts;attempt++) {
40
31
  try {
41
32
  const source=readSource(payload,record,project);
42
33
  let nativeRequestStatus='not-tool-event';
43
34
  if(toolEvent) {
44
- if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&payload.client==='CodeBuddyIDE') {
35
+ if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
45
36
  const latest=source.prompts.at(-1);
46
37
  assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
47
38
  const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
@@ -53,14 +44,18 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
53
44
  }catch(error){if(attempt===attempts)throw error;pause(100);}
54
45
  }
55
46
  }
56
- function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false}) {
47
+ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy}) {
57
48
  if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
58
49
  if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
59
- const front=E.loaded(source.calls,'frontend').filter(c=>c.sessionId===binding?.sessionId&&c.requestLine>source.contextLine&&(source.stableThroughLine===undefined||c.resultLine<=source.stableThroughLine));
60
- if(!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine||!front.length)return {deny:true,reason:'Run execution-begin for the current task and load the frontend engineering Skill successfully in this session/context before writing prototype UI. Reading SKILL.md is insufficient.'};
50
+ const missing=Recovery.missing({source,binding,policy});
51
+ if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing)};
61
52
  }
62
53
  const final=source.latestAssistant||'',claim=/(?:已完成|已经完成|全部通过|可以交付|\b(?:completed|ready for review|all checks passed)\b)/i.test(final)&&!/(?:未完成|尚未|待验证|阻塞|等待|进度|blocked|awaiting|not complete|in progress)/i.test(final);
63
- if(event==='Stop'&&binding&&!deliveryCurrent&&!stopHookActive&&claim)return {feedback:true,reason:'This prototype run has no current verified receipt. Run execution-check and resolve the missing evidence, or report blocked / awaiting-input / progress without claiming completion.'};
54
+ const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
55
+ if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
56
+ const missing=Recovery.missing({source,binding,policy,stage:'delivery'});if(!missing.length)missing.push('execution-evidence');
57
+ return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing)};
58
+ }
64
59
  return {continue:true};
65
60
  }
66
61
  function prototypeForWrite(project,tool,input) {
@@ -70,8 +65,9 @@ function prototypeForWrite(project,tool,input) {
70
65
  while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
71
66
  }
72
67
  function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
73
- const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/';
74
- const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
68
+ const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
69
+ 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');
75
71
  const project=real(root);
76
72
  // IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
77
73
  // or session-bound native workspace above; reject all other outside cwd.
@@ -82,15 +78,29 @@ function saveIndex(directory,index) {
82
78
  F.write(directory,'session.json',index);
83
79
  }
84
80
  function toolCwd(payload,record,project) {
85
- return record.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
81
+ return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
86
82
  }
87
83
  function handle(payload,options) {
88
84
  assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
89
- const project=safeProject(payload,options),record=C.select({project,product:options.product});
85
+ if(options.event==='SessionStart')return {continue:true};
86
+ if(options.event==='UserPromptSubmit'){
87
+ 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。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
89
+ return {continue:true};
90
+ }
91
+ let identity,installed;
92
+ try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
93
+ const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
94
+ 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);
96
+ if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
97
+ }
98
+ throw error;
99
+ }
100
+ options={...options,...identity};
101
+ const project=safeProject(payload,options,options.env),record=C.select({project,product:identity.product,host:identity.host,home:options.home,env:options.env});
90
102
  if(!record||record.base!==options.base)return {continue:true};
91
103
  const cwd=toolCwd(payload,record,project);
92
- // SessionStart and analysis do not create persistent per-project/session state.
93
- if(['SessionStart','UserPromptSubmit'].includes(options.event))return {continue:true};
94
104
  let source;
95
105
  try {
96
106
  source=eventSource(payload,record,project);
@@ -108,30 +118,60 @@ function handle(payload,options) {
108
118
  const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
109
119
  const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
110
120
  const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
111
- if(options.event==='PreToolUse'&&command?.operation==='create'&&!command.argv.includes('--execution-policy')&&(E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text)))return {deny:true,reason:'New CodeBuddy prototypes require --host codebuddy --execution-policy codebuddy-ab-v1 and fixed --execution-assertions. Existing artifacts keep their original contracts.'};
121
+ 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
+ const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
123
+ const hostAt=command.argv.indexOf('--host');
124
+ 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
+ }
112
126
  const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
113
127
  const existing=directory?index.bindings[directory]:null;
128
+ const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
129
+ let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
130
+ const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
131
+ const eventKey=E.sha(JSON.stringify([options.event,payload.generation_id||latest.nativeRequestId||null,callId||null,payload.tool_name||null,payload.tool_input||null,callId?null:source.digest]));
132
+ const recoveryContext=target=>{
133
+ const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
134
+ const retryCall=options.event==='PostToolUse'&&callbackCall?.success&&(stagePassed||G.uiWrite({...callbackCall,cwd:callbackCall.cwd||cwd},target))?callbackCall:null;
135
+ return {source,sessionId:payload.session_id,directory:target,event:options.event,eventKey,stage,policy:X.policy(target),resolved:[...(existing?['execution-begin']:[]),...(stagePassed?['execution-evidence']:[])],retrySucceeded:!!retryCall,
136
+ ...(retryCall?{retry:{toolCallId:retryCall.toolCallId,resultDigest:retryCall.resultDigest,requestLine:retryCall.requestLine,resultLine:retryCall.resultLine,...(stagePassed?{kind:'stage',stage,operation:command.operation}:{kind:'ui-write'})}}:{})};
137
+ };
138
+ const respond=answer=>{
139
+ const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
140
+ try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry without bypassing the gate');}
141
+ 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
+ finally{fs.rmdirSync(lock);}
143
+ };
114
144
  if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
115
145
  assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
116
146
  const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
147
+ assert.equal(fixed.host,identity.host,'This prototype has a fixed contract for another host; inspect it or explicitly migrate and revalidate, do not inherit its invocation proof');
117
148
  const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
118
149
  assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
119
150
  const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
120
151
  const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
121
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,
122
- operation:meta.revision==='r001'?'create':'revise',product:record.product,productVersion:record.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
153
+ operation:meta.revision==='r001'?'create':'revise',host:identity.host,identitySource:identity.identitySource,product:record.product,productVersion:identity.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
123
154
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
124
155
  try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
125
156
  try {
126
157
  const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
127
158
  saveIndex(stateDirectory,fresh);
128
- F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,product:record.product});
159
+ F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
129
160
  }finally{fs.rmdirSync(lock);}
130
161
  return {continue:true};
131
162
  }
132
163
  if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
133
- const answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd});if(answer.deny)return answer;
134
- if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)&&!existing)return {deny:true,reason:'Begin this strict prototype run with execution-begin before implementation or delivery.'};
164
+ 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
+ if(answer.deny)return respond(answer);
166
+ 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'])});
168
+ const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
169
+ if(checked.status==='failed'){
170
+ 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('; ')});
172
+ }
173
+ }
174
+ if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event))respond({continue:true});
135
175
  }
136
176
  if(!Object.keys(index.bindings).length)return {continue:true};
137
177
  const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
@@ -140,14 +180,13 @@ function handle(payload,options) {
140
180
  const fresh=JSON.parse(C.read(file));let changed=false;
141
181
  for(const entry of Object.values(fresh.bindings)) {
142
182
  if(entry.entryLine!==latest.line)continue;
143
- const eventKey=E.sha(JSON.stringify([options.event,payload.tool_use_id||null,payload.tool_name||null,payload.tool_input||null,source.digest]));
144
183
  if(entry.lastEventKey===eventKey)continue;
145
184
  entry.source={path:source.path,bytes:source.bytes,digest:source.digest};entry.lastEventKey=eventKey;entry.lastHook={event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus};changed=true;
146
185
  if(options.event==='Stop') {
147
186
  const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
148
187
  const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
149
- const feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
150
- if(feedback.feedback){entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return feedback;}
188
+ const fixed=X.policy(entry.directory),feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,policy:fixed,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
189
+ if(feedback.feedback){const result=Recovery.record(fresh,{...recoveryContext(entry.directory),policy:fixed},feedback);entry.recovery=result.recovery;if(fixed.id!==P.AUTO_ID)entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return result;}
151
190
  }
152
191
  }
153
192
  if(changed)saveIndex(stateDirectory,fresh);
@@ -24,18 +24,20 @@ function config(text) {
24
24
  }
25
25
  function ownedMarker(group) { return group.hooks.some(hook => typeof hook.command === 'string' && hook.command.includes(OWNER)); }
26
26
  function plan(options) {
27
- const { action = 'keep', scope, product, base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
27
+ const { action = 'keep', scope, product = 'auto', base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
28
28
  transcriptRoots = record?.transcriptRoots || [], transcriptMode = record?.transcriptMode || (transcriptRoots.length ? 'manual' : 'auto'), productVersion = record?.productVersion || 'unreported' } = options;
29
29
  assert.ok(['keep', 'enable', 'disable'].includes(action), 'Invalid Hook action');
30
30
  assert.ok(['project', 'global'].includes(scope), 'Invalid installation scope');
31
- assert.ok(['ide', 'cli'].includes(product), 'Specify CodeBuddy product ide or cli');
31
+ assert.ok(['auto', 'ide', 'cli'].includes(product), 'Use automatic product detection, or a legacy ide/cli profile');
32
+ const hosts=[...new Set(options.hosts || [...(record?.hosts || (record ? ['codebuddy'] : [])),...(options.agents?.filter(host=>['codebuddy','workbuddy'].includes(host)) || [options.host || 'codebuddy'])])].sort();
33
+ assert.ok((hosts.length||action==='disable')&&hosts.every(host=>['codebuddy','workbuddy'].includes(host)), 'Unsupported prototype Hook host');
32
34
  for (const item of [base, project, nodePath, scriptPath]) assert.ok(typeof item === 'string' && path.isAbsolute(item) && !/[\x00\r\n]/.test(item), 'Absolute safe registration paths required');
33
35
  if (process.platform === 'win32') throw new Error('CodeBuddy A+B Windows Hook quoting is not yet verified');
34
36
  const before = config(currentText), after = structuredClone(before), activeRecord = record && record.owner === OWNER ? record : null;
35
37
  if (record) {
36
- assert.ok(activeRecord && activeRecord.schemaVersion === 1, 'Unknown registration ownership');
38
+ assert.ok(activeRecord && [1,2].includes(activeRecord.schemaVersion), 'Unknown registration ownership');
37
39
  assert.equal(activeRecord.scope, scope, 'Registration scope changed');
38
- assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
40
+ if(product!=='auto')assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
39
41
  assert.equal(activeRecord.base, base, 'Registration base changed');
40
42
  }
41
43
  const found = {};
@@ -63,7 +65,7 @@ function plan(options) {
63
65
  assert.ok(Array.isArray(transcriptRoots)&&transcriptRoots.every(root=>typeof root==='string'&&path.isAbsolute(root)&&!/[\x00\r\n]/.test(root)),'Absolute native transcript roots required');
64
66
  assert.ok(['auto','manual'].includes(transcriptMode),'Invalid native transcript mode');
65
67
  assert.ok(transcriptMode==='manual'?transcriptRoots.length>0:transcriptRoots.length===0,'Manual transcript mode requires roots; auto uses only the native callback file');
66
- recordAfter = { schemaVersion: 1, owner: OWNER, scope, product, base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
68
+ recordAfter = { schemaVersion: product==='auto'?2:1, owner: OWNER, scope, product, ...(product==='auto'?{hosts}:{}), base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
67
69
  }
68
70
  }
69
71
  const afterText = unchanged ? currentText : JSON.stringify(after, null, 2) + '\n';
@@ -18,7 +18,7 @@ function nativeEvent(event) {
18
18
  if (event.type === 'message' && ['assistant', 'user'].includes(event.role)) {
19
19
  const content = Array.isArray(event.content) ? event.content.map(block =>
20
20
  ['input_text', 'output_text'].includes(block?.type) ? { ...block, type: 'text' } : block) : event.content;
21
- return { ...event, type: event.role, message: { content } };
21
+ return { ...event, type: event.role, isMeta:event.isMeta===true||event.providerData?.isMeta===true, message: { content } };
22
22
  }
23
23
  if (event.type === 'function_call') {
24
24
  let input;
@@ -4,6 +4,7 @@
4
4
  const assert=require('node:assert/strict'),path=require('node:path');
5
5
  const P=require('./execution-policy.cjs'),E=require('./execution-evidence.cjs');
6
6
  const A=require('./execution-assertions.cjs');
7
+ const command=require('./native-command.cjs');
7
8
  const same=(a,b,label)=>assert.deepEqual(a,b,label);
8
9
  const exact=(a,b,label)=>same([...new Set(a)].sort(),[...new Set(b)].sort(),label);
9
10
  function uiWrite(call,directory) {
@@ -37,9 +38,29 @@ function identity(d) {
37
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');
38
39
  for(const key of ['inputDigest','candidateDigest','revision'])same(d.proof[key],key==='candidateDigest'?d.current.digest:d.current[key],`Current ${key} differs`);
39
40
  assert.ok(d.source.prompts.some(p=>p.line===d.binding.entryLine),'Native user task source missing');
41
+ 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
+ if(d.binding.recovery){
43
+ const recovery=d.binding.recovery;
44
+ 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
+ if(recovery.status==='recovered'){
46
+ const retry=nativeRef(d,recovery.retry);
47
+ 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
+ if(recovery.retry.kind==='stage'){
49
+ const parsed=command.nativeCommand(retry.name,retry.input,retry.cwd||d.binding.project||d.binding.directory);
50
+ assert.ok(parsed&&command.stageFor[parsed.operation],'Recovery retry is not a supported native stage command');
51
+ same(parsed.root,d.binding.directory,'Recovery stage belongs to another prototype');
52
+ same(parsed.operation,recovery.retry.operation,'Recovery stage operation differs');
53
+ same(command.stageFor[parsed.operation],recovery.retry.stage,'Recovery stage differs');
54
+ }else {
55
+ assert.ok(recovery.retry.kind===undefined||recovery.retry.kind==='ui-write','Unknown recovery retry kind');
56
+ assert.ok(uiWrite({...retry,cwd:retry.cwd||d.binding.project},d.binding.directory),'Recovery retry is not a native UI write');
57
+ }
58
+ for(const ref of recovery.invocations||[])assert.ok(retry.requestLine>ref.resultLine,'Retry must follow successful Skill results');
59
+ }
60
+ }
40
61
  }
41
62
  function frontend(d,complete) {
42
- const writes=d.source.calls.filter(c=>c.success&&c.requestLine>=d.binding.entryLine&&uiWrite({...c,cwd:c.cwd||(d.source.format==='codebuddy-ide-index-v1'?d.binding.project:undefined)},d.binding.directory));
63
+ const writes=d.source.calls.filter(c=>c.success&&c.requestLine>=d.binding.entryLine&&c.requestLine>(d.binding.contextLine||0)&&uiWrite({...c,cwd:c.cwd||(d.source.format==='codebuddy-ide-index-v1'?d.binding.project:undefined)},d.binding.directory));
43
64
  skill(d,d.proof.frontend,'frontend',writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity);
44
65
  if(!complete)return;
45
66
  assert.ok(writes.length,'Actual implementation write missing; metadata is not implementation');
@@ -141,7 +162,7 @@ function evaluate(data,stage) {
141
162
  if(needs.includes('build'))build(data);
142
163
  if(needs.includes('browser'))skill(data,data.proof.browser?.skill||data.devtools?.skill,'browser');
143
164
  if(needs.includes('browser-results'))browser(data);
144
- return {status:'passed',stage,policyDigest:data.policy.digest,runId:data.binding.runId,sessionId:data.binding.sessionId,projectKey:data.binding.projectKey,host:'codebuddy',productForm:data.binding.product||'unreported',productVersion:data.binding.productVersion||'unreported',installationScope:data.binding.scope||'unreported',operation:data.binding.operation||data.policy.operation,mode:data.policy.mode,inputDigest:data.current.inputDigest,candidateDigest:data.current.digest,revision:data.current.revision,source:data.source.path?{path:data.source.path,bytes:data.source.bytes,digest:data.source.digest}:undefined,invocationVerification:'native-source-correlated',tamperProof:false};
165
+ return {status:'passed',stage,policyDigest:data.policy.digest,runId:data.binding.runId,sessionId:data.binding.sessionId,projectKey:data.binding.projectKey,host:data.policy.host,productForm:data.binding.product||'unreported',productVersion:data.binding.productVersion||'unreported',installationScope:data.binding.scope||'unreported',operation:data.binding.operation||data.policy.operation,mode:data.policy.mode,inputDigest:data.current.inputDigest,candidateDigest:data.current.digest,revision:data.current.revision,source:data.source.path?{path:data.source.path,bytes:data.source.bytes,digest:data.source.digest}:undefined,...(data.binding.recovery?{recovery:data.binding.recovery}:{}),invocationVerification:'native-source-correlated',tamperProof:false};
145
166
  } catch(error){return {status:'failed',stage,errors:[error.message],invocationVerification:'unverified'};}
146
167
  }
147
168
  module.exports={evaluate,uiWrite};
@@ -2,6 +2,7 @@
2
2
  const assert = require('node:assert/strict');
3
3
  const crypto = require('node:crypto');
4
4
  const ID = 'codebuddy-ab-v1';
5
+ const AUTO_ID = 'buddy-ab-v2';
5
6
  const SKILLS = Object.freeze({
6
7
  frontend: [['frontend','ui','engineering'].join('-'), 'i3-frontend-ui-engineering'],
7
8
  browser: [['browser','testing','with','devtools'].join('-'), 'i3-browser-testing-with-devtools'],
@@ -12,16 +13,20 @@ const STAGES = ['design', 'implementation-entry', 'implementation', 'build', 'br
12
13
  function digest(value) { return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
13
14
  function create(options = {}) {
14
15
  const { host = 'codebuddy', operation = 'create', mode = 'professional', officialCallable = true } = options;
15
- assert.equal(host, 'codebuddy', 'This execution policy supports CodeBuddy only');
16
+ const policyId=options.id||(host==='workbuddy'?AUTO_ID:ID);
17
+ assert.ok([ID,AUTO_ID].includes(policyId),'Unsupported execution policy');
18
+ assert.ok(['codebuddy','workbuddy'].includes(host),'This execution policy supports CodeBuddy and WorkBuddy only');
19
+ if(policyId===ID)assert.equal(host,'codebuddy','The legacy policy is CodeBuddy only');
16
20
  assert.ok(['analyze', 'create', 'revise', 'inspect', 'confirm-baseline'].includes(operation), 'Invalid execution operation');
17
21
  assert.ok(['professional', 'template'].includes(mode), 'Invalid execution mode');
18
22
  assert.equal(typeof officialCallable, 'boolean');
19
- const value = { schemaVersion: 1, id: ID, host, operation, mode, officialCallable,
23
+ const value = { schemaVersion: policyId===AUTO_ID?2:1, id: policyId, host, operation, mode, officialCallable,
24
+ ...(policyId===AUTO_ID?{recovery:{perMissing:2,total:6}}:{}),
20
25
  readOnly: !['create', 'revise'].includes(operation), skills: SKILLS, stages: STAGES };
21
26
  return { ...value, digest: digest(value) };
22
27
  }
23
28
  function validate(policy) {
24
- assert.ok(policy && policy.id === ID, 'Unsupported execution policy');
29
+ assert.ok(policy && [ID,AUTO_ID].includes(policy.id), 'Unsupported execution policy');
25
30
  const expected = create(policy);
26
31
  assert.deepEqual(policy, expected, 'Execution policy was changed or is invalid');
27
32
  return policy;
@@ -30,12 +35,28 @@ function requirements(policy, stage) {
30
35
  validate(policy); assert.ok(STAGES.includes(stage), 'Unknown execution stage');
31
36
  if (policy.readOnly) return [];
32
37
  const design = [policy.officialCallable ? 'antdv' : 'official-source', ...(policy.mode === 'professional' ? ['uiux'] : [])];
33
- return ({ design, 'implementation-entry': ['frontend'], implementation: ['frontend', 'implementation'],
38
+ return ({ design, 'implementation-entry': ['frontend'], implementation: ['frontend', 'implementation',...(policy.id===AUTO_ID?design:[])],
34
39
  build: ['implementation', 'build'], 'browser-entry': ['browser', 'build'],
35
40
  browser: ['browser', 'build', 'browser-results'], delivery: [...design, 'frontend', 'implementation', 'build', 'browser', 'browser-results'] })[stage];
36
41
  }
37
- function skillKey(name) { return Object.keys(SKILLS).find(key => SKILLS[key].includes(name)) || null; }
42
+ function skillKey(name) {
43
+ if(typeof name!=='string')return null;
44
+ const [namespace,leaf,...rest]=name.split(':');
45
+ if(leaf!==undefined){
46
+ if(rest.length)return null;
47
+ const allowed={'i3-composer-agent':['frontend','browser','prototype'],'i3-composer-agent':['frontend','browser'],'ui-ux-pro-max':['uiux'],'antdv':['antdv'],'antdv-next':['antdv']};
48
+ return (allowed[namespace]||[]).find(key=>SKILLS[key].includes(leaf))||null;
49
+ }
50
+ return Object.keys(SKILLS).find(key => SKILLS[key].includes(name)) || null;
51
+ }
38
52
  function creationOptions(options,installed) {
39
- return installed?.name==='i3-composer-agent'&&options.host==='codebuddy'?{...options,executionPolicy:options.executionPolicy||ID}:options;
53
+ if(installed?.name!=='i3-composer-agent')return options;
54
+ let host=options.host;
55
+ if(!host&&installed.agents?.length){
56
+ const hosts=installed.agents.filter(value=>['codebuddy','workbuddy'].includes(value));
57
+ assert.ok(hosts.length<2,'Shared Buddy installation requires the actual --host');
58
+ if(hosts.length===1)host=hosts[0];
59
+ }
60
+ return ['codebuddy','workbuddy'].includes(host)?{...options,host,executionPolicy:options.executionPolicy||AUTO_ID}:options;
40
61
  }
41
- module.exports = { ID, SKILLS, STAGES, digest, create, validate, requirements, skillKey, creationOptions };
62
+ module.exports = { ID, AUTO_ID, SKILLS, STAGES, digest, create, validate, requirements, skillKey, creationOptions };
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+ // Deterministic recovery state; only native source results can resolve a Skill.
3
+ const E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs');
4
+ const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
5
+ function missing({source,binding,policy,stage}){
6
+ const unbound=!binding||binding.entryLine!==source.prompts.at(-1)?.line||binding.contextLine!==source.contextLine;
7
+ 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
+ const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
9
+ 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
+ }
11
+ function instruction(keys){
12
+ 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、自动补全提示、手填调用记录不能代替实际调用。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
14
+ }
15
+ function record(index,context,answer){
16
+ const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
17
+ const taskKey=E.sha(JSON.stringify([sessionId,prompt.line,prompt.digest,directory]));
18
+ if(!answer.deny&&!answer.feedback&&!index.recoveries?.[taskKey])return answer;
19
+ index.recoveries||={};
20
+ const state=index.recoveries[taskKey]||={schemaVersion:1,total:0,counts:{},events:{},pending:{},invocations:[],status:'required'};
21
+ 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}:{})});
23
+ 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
+ if(state.events[eventKey])return {...state.events[eventKey],recovery:summary()};
25
+ if(answer.deny||answer.feedback){
26
+ const keys=[...new Set(answer.missing?.length?answer.missing:['execution-evidence'])];
27
+ const limits=context.policy?.recovery||{perMissing:2,total:6};
28
+ if(state.total>=limits.total||keys.some(key=>(state.counts[key]||0)>=limits.perMissing)){
29
+ state.status='blocked';
30
+ const reason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
31
+ const result=answer.deny?{deny:true,reason,missing:keys}:{continue:true,context:reason};return remember(result);
32
+ }
33
+ state.total++;for(const key of keys)state.counts[key]=(state.counts[key]||0)+1;state.status='required';
34
+ const line=Math.max(prompt.line,...source.calls.map(call=>call.resultLine||call.requestLine));
35
+ state.feedbackLine=Math.max(state.feedbackLine,line);
36
+ for(const item of keys)state.pending[item]||={line,eventKey,...(item==='execution-evidence'&&context.stage?{stage:context.stage}:{})};
37
+ }else{
38
+ for(const [key,pending] of Object.entries(state.pending)){
39
+ if(names[key]){
40
+ const call=E.loaded(source.calls,key).find(call=>call.sessionId===sessionId&&call.requestLine>Math.max(pending.line,source.contextLine)&&(source.stableThroughLine===undefined||call.resultLine<=source.stableThroughLine));
41
+ if(!call)continue;
42
+ if(!state.invocations.some(item=>item.toolCallId===call.toolCallId))state.invocations.push({skill:key,toolCallId:call.toolCallId,resultDigest:call.resultDigest,requestLine:call.requestLine,resultLine:call.resultLine,feedbackLine:pending.line,afterFeedback:pending.eventKey});
43
+ }else if(!(context.resolved||[]).includes(key)||pending.stage&&pending.stage!==context.stage)continue;
44
+ delete state.pending[key];
45
+ }
46
+ 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};
48
+ }
49
+ }
50
+ return remember({...answer});
51
+ }
52
+ module.exports={missing,instruction,record};
@@ -12,7 +12,7 @@ function policy(directory) {
12
12
  }
13
13
  function binding(directory) {
14
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 project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product});assert.ok(selected,'Hooks are not enabled or are shadowed');
15
+ 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
16
  assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
17
17
  const stateDirectory=C.sessionDirectory(selected,project,pointer.sessionId),indexFile=path.join(stateDirectory,'session.json');
18
18
  const index=JSON.parse(C.read(indexFile)||'null'),entry=index?.bindings?.[directory];assert.ok(entry,'Native Hook run is missing');
@@ -1,16 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
  const fs=require('node:fs'),path=require('node:path');
4
+ function format(result,event){
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}};
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
+ return {code:0,output:{continue:true}};
9
+ }
4
10
  function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8')) {
5
11
  if(Buffer.byteLength(raw)>4*1024*1024)throw new Error('Hook input too large');
6
12
  const options={};while(argv.length){const key=argv.shift();if(!key.startsWith('--')||!argv.length)throw new Error('Invalid Hook arguments');options[key.slice(2)]=argv.shift();}
7
13
  const {OWNER,EVENTS}=require('../codebuddy-registration.cjs');
8
- if(options.registration!==OWNER||!path.isAbsolute(options.base||'')||!EVENTS.includes(options.event)||!['ide','cli'].includes(options.product))throw new Error('Invalid Hook registration');
14
+ if(options.registration!==OWNER||!path.isAbsolute(options.base||'')||!EVENTS.includes(options.event)||!['auto','ide','cli'].includes(options.product))throw new Error('Invalid Hook registration');
9
15
  const result=require('../codebuddy-hook.cjs').handle(JSON.parse(raw),options);
10
- if(result.deny)return {code:0,output:{hookSpecificOutput:{hookEventName:'PreToolUse',permissionDecision:'deny',permissionDecisionReason:result.reason}}};
11
- if(result.feedback)return {code:2,output:{decision:'block',reason:result.reason}};
12
- return {code:0,output:{continue:true}};
16
+ return format(result,options.event);
13
17
  }
14
18
  if(require.main===module)try{const result=main();process.stdout.write(JSON.stringify(result.output)+'\n');if(result.code===2)process.stderr.write(result.output.reason+'\n');process.exitCode=result.code;}
15
19
  catch(error){process.stderr.write(`i3 prototype audit unavailable: ${error.message}. Strict A gates remain unverified.\n`);process.exitCode=1;}
16
- module.exports={main};
20
+ module.exports={main,format};
@@ -0,0 +1,18 @@
1
+ 'use strict';
2
+ // One non-executing parser shared by Hook dispatch and A receipt verification.
3
+ const path=require('node:path');
4
+ function tokens(command) {
5
+ if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
6
+ const values=[];let value='',quote=null,started=false;
7
+ for(let i=0;i<command.length;i++) {const c=command[i];if(quote){if(c===quote){quote=null;started=true;}else if(c==='\\'&&quote==='"'&&i+1<command.length)value+=command[++i];else value+=c;}
8
+ else if(c==='"'||c==="'"){quote=c;started=true;}else if(c==='\\'&&i+1<command.length){value+=command[++i];started=true;}else if(/\s/.test(c)){if(started){values.push(value);value='';started=false;}}else if(/[;&|<>]/.test(c))return null;else {value+=c;started=true;}}
9
+ if(quote)return null;if(started)values.push(value);return values;
10
+ }
11
+ function nativeCommand(tool,input,cwd) {
12
+ if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
13
+ const argv=tokens(input?.command);if(!argv)return null;const index=argv.findIndex(arg=>path.basename(arg)==='native-pilot.cjs');if(index<1)return null;
14
+ const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
15
+ return {operation,root,argv};
16
+ }
17
+ const stageFor=Object.freeze({build:'implementation','record-run':'browser-entry','record-devtools-review':'browser-entry',finalize:'delivery'});
18
+ module.exports={tokens,nativeCommand,stageFor};
@@ -33,9 +33,7 @@ function defaultScope() {
33
33
  ],exemptions:[],openQuestions:[]};
34
34
  }
35
35
  function createPrototype(options) {
36
- const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
37
- if(fs.existsSync(installedFile))options=require('./execution-policy.cjs').creationOptions(options,json(installedFile));
38
- if(options.executionPolicy!==undefined){assert.equal(options.executionPolicy,require('./execution-policy.cjs').ID,'Unsupported execution policy');assert.equal(options.host,'codebuddy','Explicit CodeBuddy host required for A+B');}
36
+ if(options.executionPolicy!==undefined){const policy=require('./execution-policy.cjs');assert.ok([policy.ID,policy.AUTO_ID].includes(options.executionPolicy),'Unsupported execution policy');assert.ok(['codebuddy','workbuddy'].includes(options.host),'Explicit Buddy host required for A+B');if(options.executionPolicy===policy.ID)assert.equal(options.host,'codebuddy','Legacy policy is CodeBuddy only');}
39
37
  assert.ok(options.framework===undefined||options.framework==='vue-antdv-next','Unsupported framework / 不支持的框架');
40
38
  assert.ok(options.title===undefined||(typeof options.title==='string'&&options.title.trim()),'Title must be a nonempty string');
41
39
  const project=path.resolve(options.project||'.'); assert.ok(fs.statSync(project).isDirectory(),'Project must exist'); id(options.id);
@@ -67,7 +65,7 @@ function createPrototype(options) {
67
65
  const instanceId=crypto.randomUUID();
68
66
  const data={schemaVersion:2,id:options.id,title:options.title||content.pageTitle||'设备清单原型',revision:'r001',artifactKind:'framework-prototype',runtime:{kind:'vue-antdv-next',version:'1',...(reference?{contentReference:true}:{})},...(reference?{business:{status:'needs-adaptation',source:'inputs/scope.json'}}:{}),template:{id:template.id,version:template.version,digest:template.digest},framework:{id:'vue-antdv-next',version:profile.version||'1',digest:hash(JSON.stringify(frameworkFiles))},expertMode:mode,expert:{status:provider?'pending':'skipped',provider},verification:{status:'not-run'},review:{status:'pending-review'},instanceId};
69
67
  if(generation)data.runtime.generationContract=1;
70
- if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
68
+ if(options.executionPolicy){data.runtime.executionPolicy=options.executionPolicy;write(stage,'inputs/execution-policy.json',require('./execution-policy.cjs').create({id:options.executionPolicy,host:options.host,mode,officialCallable:!options.officialSourceFallback}));}
71
69
  write(stage,'prototype.json',data);
72
70
  for(const file of template.files) {
73
71
  assert.equal(file.encoding,'base64');
@@ -23,7 +23,11 @@ async function main(argv=process.argv.slice(2)) {
23
23
  case 'generation-plan': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template);const contract=require('./generation-contract.cjs');return contract.view(contract.compile(snapshot,JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'))),options); }
24
24
  case 'inventory': { const templates=require('./template-contract.cjs'),snapshot=root?templates.loadTemplate(root):templates.loadBundledTemplate(options.template),value=require('./template-inventory.cjs').verifySnapshot(snapshot);return options.details?value:{status:'passed',readOnly:true,template:snapshot.id,files:Object.keys(value.files).length,routes:value.routes.length,pages:value.pages.length,fragments:value.fragments.length,components:Object.keys(value.components).length,events:value.eventCount}; }
25
25
  case 'sync-generation-assets': return require('./generation-assets.cjs').refresh(path.resolve(__dirname,'..'),{check:!!options.check});
26
- case 'create': return lib.createPrototype(options);
26
+ case 'create': {
27
+ const installedFile=path.resolve(__dirname,'../../../.i3-composer-agent.json');
28
+ const effective=fs.existsSync(installedFile)?require('./execution-policy.cjs').creationOptions(options,JSON.parse(fs.readFileSync(installedFile,'utf8'))):options;
29
+ return lib.createPrototype(effective);
30
+ }
27
31
  case 'execution-begin': return require('./execution-runtime.cjs').begin(root,lib);
28
32
  case 'execution-assertions-template': return require('./execution-assertions.cjs').template(options);
29
33
  case 'execution-template': return require('./execution-runtime.cjs').template(root,lib);
@@ -31,10 +31,32 @@ function antdvTool(project, options = {}) {
31
31
  if (result.status !== 0) throw new Error(result.error?.message || result.stderr || 'CLI query failed');
32
32
  const data = JSON.parse(result.stdout);
33
33
  if (!data || data.name !== 'Table' || !Array.isArray(data.props) || data.error) throw new Error('CLI returned no Table knowledge');
34
- return { availability: 'available', root, source, entry, version: pkg.version, command: [process.execPath, ...args], knowledgeVersion: '1.5.2', componentVersion: '1.5.3', exactKnowledgeMatch: false,
34
+ return { availability: 'available', root, source, entry, nodePath:process.execPath, version: pkg.version, command: [process.execPath, ...args], invocationVerified:false, knowledgeProbe:'Table-only', knowledgeVersion: '1.5.2', componentVersion: '1.5.3', exactKnowledgeMatch: false,
35
35
  limitation: 'CLI knowledge 1.5.2 is reference only; verify target 1.5.3 package types/source.' };
36
36
  } catch (error) { return { availability: 'unusable', root, source, reason: error.message }; }
37
37
  }
38
+ function query(options={}) {
39
+ const kinds=['info','demo','token','semantic','doc','list','design.md'];
40
+ assertQuery(kinds.includes(options.kind),'Choose a read-only antDv query: '+kinds.join(', '));
41
+ const locations=resolveLocations(options),tool=antdvTool(locations.project,{...options,host:locations.host,scope:locations.scope});
42
+ assertQuery(tool.availability==='available',`Local antDv CLI ${tool.availability}: ${tool.reason||'use prototype-deps check; installation requires separate authorization'}`);
43
+ const component=options.component,version=options.version||tool.knowledgeVersion;
44
+ assertQuery(/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(version),'Explicit safe knowledge version required');
45
+ const args=[tool.entry,options.kind];
46
+ if(!['list','design.md'].includes(options.kind)){
47
+ assertQuery(typeof component==='string'&&/^[A-Za-z][A-Za-z0-9-]{0,99}$/.test(component),'Component name required');args.push(component);
48
+ }
49
+ if(options.demo!==undefined){assertQuery(options.kind==='demo'&&/^[A-Za-z0-9_-]{1,100}$/.test(options.demo),'Safe demo name required');args.push(options.demo);}
50
+ args.push('--ver',version,'--format','json');
51
+ const result=spawnSync(tool.nodePath,args,{cwd:locations.project,env:{...(options.env||process.env),ANTDV_NO_AUTO_REPORT:'1'},encoding:'utf8',timeout:15000,maxBuffer:5*1024*1024});
52
+ let data=null;try{data=JSON.parse(result.stdout);}catch{/* Some official doc/demo commands return text. */}
53
+ const stdout=result.stdout||'',stderr=result.stderr||'';
54
+ const invalid=result.status!==0||!stdout.trim()||/^\s*(?:Error\b|错误[::])/i.test(stdout)||data?.error||data?.success===false||data?.status==='failed'||options.kind==='info'&&(!data||data.name!==component||!Array.isArray(data.props));
55
+ return {status:invalid?'failed':'queried',host:locations.host,scope:locations.scope,command:[tool.nodePath,...args],source:tool.source,cliVersion:tool.version,knowledgeVersion:version,componentVersion:tool.componentVersion,exactKnowledgeMatch:version===tool.componentVersion,
56
+ component:component||null,kind:options.kind,exitCode:result.status,stdout,stderr,...(data?{data}:{}),...(result.error?{error:result.error.message}:{}),skillInvocationVerified:false,
57
+ note:'This is a CLI query only. Load the official Skill through the host before business queries; native Hooks must correlate the actual request/result. Verify target package types/source when knowledge versions differ.'};
58
+ }
59
+ function assertQuery(condition,message){if(!condition)throw new Error(message);}
38
60
  function check(options = {}) {
39
61
  const locations = resolveLocations(options), { project, host, scope } = locations;
40
62
  const roots = scopeBases(locations).map(([base]) => path.join(base, 'skills/ui-ux-pro-max'));
@@ -62,8 +84,8 @@ function check(options = {}) {
62
84
  }
63
85
  function parse(argv) {
64
86
  const operation = argv.shift() || 'check', options = {};
65
- if (!['check', 'install'].includes(operation)) throw new Error('Use check or install');
66
- const flags = new Set(['project', 'host', 'scope', 'skills', 'provider', 'python', 'tool']);
87
+ if (!['check', 'install','query'].includes(operation)) throw new Error('Use check or install or query');
88
+ const flags = new Set(['project', 'host', 'scope', 'skills', 'provider', 'python', 'tool',...(operation==='query'?['kind','component','version','demo']:[])]);
67
89
  while (argv.length) {
68
90
  const key = argv.shift();
69
91
  if (key === '--migrate-tools' && operation === 'install') { options.migrateTools = true; continue; }
@@ -74,8 +96,8 @@ function parse(argv) {
74
96
  }
75
97
  async function main(argv = process.argv.slice(2)) {
76
98
  const { operation, options } = parse([...argv]);
77
- return operation === 'check' ? check(options) : require('./optional-skill-install.cjs').install(options);
99
+ return operation === 'check' ? check(options) : operation==='query'?query(options):require('./optional-skill-install.cjs').install(options);
78
100
  }
79
- module.exports = { check, skillAt, antdvTool, parse, main };
80
- if (require.main === module) main().then(result => process.stdout.write(JSON.stringify(result, null, 2) + '\n'))
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;})
81
103
  .catch(error => { process.stderr.write(JSON.stringify({ status: 'failed', errors: [error.message] }) + '\n'); process.exitCode = 1; });