@givolt/i3-composer-agent 2.3.3 → 2.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +2 -0
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +2 -0
  7. package/README.md +2 -2
  8. package/bin/i3-composer-agent.cjs +18 -15
  9. package/bin/wizard.cjs +12 -52
  10. package/commands/i3-prototype.toml +2 -0
  11. package/docs/codebuddy-setup.md +21 -7
  12. package/docs/codex-setup.md +1 -1
  13. package/docs/other-hosts.md +1 -1
  14. package/docs/workbuddy-setup.md +18 -2
  15. package/hooks/manage.cjs +22 -14
  16. package/package.json +7 -1
  17. package/plugin.json +1 -1
  18. package/scripts/ui-prototype-binding-recovery-test.js +84 -0
  19. package/scripts/ui-prototype-buddy-lifecycle-test.js +97 -0
  20. package/scripts/ui-prototype-codebuddy-hook-test.js +3 -3
  21. package/scripts/ui-prototype-dependencies-test.js +13 -0
  22. package/scripts/ui-prototype-execution-gates-test.js +27 -2
  23. package/scripts/ui-prototype-execution-policy-test.js +12 -1
  24. package/scripts/ui-prototype-recovery-test.js +72 -0
  25. package/skills/i3-ui-prototype/SKILL.md +7 -3
  26. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +10 -6
  27. package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
  28. package/skills/i3-ui-prototype/references/execution-evidence.md +31 -8
  29. package/skills/i3-ui-prototype/references/interaction-acceptance.md +1 -1
  30. package/skills/i3-ui-prototype/references/project-layout.md +1 -1
  31. package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
  32. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +14 -14
  33. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +90 -36
  34. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +3 -3
  35. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +7 -5
  36. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +1 -1
  37. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +45 -14
  38. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +30 -5
  39. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +28 -7
  40. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +57 -0
  41. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +8 -3
  42. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +9 -5
  43. package/skills/i3-ui-prototype/scripts/native-command.cjs +35 -0
  44. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +2 -4
  45. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +5 -1
  46. package/skills/i3-ui-prototype/scripts/native-tool-name.cjs +5 -0
  47. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
  48. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +13 -13
  49. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +1 -1
@@ -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) {
@@ -29,7 +30,7 @@ function skill(data,ref,key,before=Infinity) {
29
30
  }
30
31
  function quote(data,ref) {
31
32
  const call=nativeRef(data,ref);assert.ok(typeof ref.quote==='string'&&ref.quote.trim(),'Actual native result quote required');
32
- 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;
33
34
  }
34
35
  function identity(d) {
35
36
  P.validate(d.policy);assert.equal(d.binding?.schemaVersion,1,'Hook task binding required');assert.equal(d.proof?.schemaVersion,1,'Execution evidence required');
@@ -37,9 +38,33 @@ 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
+ assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
42
+ if(d.policy.id===P.AUTO_ID){same(d.binding.host,d.policy.host,'Actual native host differs from the fixed policy');assert.ok(d.binding.identitySource&&d.binding.identitySource!=='legacy-profile','Automatic policy requires actual native host identity');}
43
+ if(d.binding.recovery){
44
+ const recovery=d.binding.recovery;
45
+ assert.notEqual(recovery.status,'blocked','Recovery budget exhausted; a new human task is required');
46
+ for(const ref of recovery.invocations||[]){const call=nativeRef(d,ref);same(call.skill,ref.skill,'Recovery Skill differs');same(call.requestLine,ref.requestLine,'Recovery request cursor differs');same(call.resultLine,ref.resultLine,'Recovery result cursor differs');assert.ok(call.requestLine>ref.feedbackLine,'Recovery invocation must follow its feedback');}
47
+ if(recovery.status==='recovered'){
48
+ const retry=nativeRef(d,recovery.retry);
49
+ if(recovery.expectedRetry)for(const [key,value] of Object.entries(recovery.expectedRetry))same(recovery.retry[key],value,'Recovery retry differs from the denied operation');
50
+ if(recovery.retry.kind!==undefined)assert.ok(Number.isFinite(recovery.retry.feedbackLine)&&recovery.retry.feedbackLine>=d.binding.entryLine&&retry.requestLine>recovery.retry.feedbackLine,'Recovery retry must follow its feedback');
51
+ if(recovery.retry.kind==='stage'){
52
+ const parsed=command.nativeCommand(retry.name,retry.input,retry.cwd||d.binding.project||d.binding.directory);
53
+ assert.ok(parsed&&command.stageFor[parsed.operation],'Recovery retry is not a supported native stage command');
54
+ same(parsed.root,d.binding.directory,'Recovery stage belongs to another prototype');
55
+ same(parsed.operation,recovery.retry.operation,'Recovery stage operation differs');
56
+ same(command.stageFor[parsed.operation],recovery.retry.stage,'Recovery stage differs');
57
+ }else {
58
+ assert.ok(recovery.retry.kind===undefined||recovery.retry.kind==='ui-write','Unknown recovery retry kind');
59
+ assert.ok(uiWrite({...retry,cwd:retry.cwd||d.binding.project},d.binding.directory),'Recovery retry is not a native UI write');
60
+ if(recovery.expectedRetry?.file){const input=retry.input,file=input.file_path||input.filePath||input.path||input.TargetFile;same(path.resolve(retry.cwd||d.binding.project,file),recovery.expectedRetry.file,'Recovery wrote a different UI file');}
61
+ }
62
+ for(const ref of recovery.invocations||[])assert.ok(retry.requestLine>ref.resultLine,'Retry must follow successful Skill results');
63
+ }
64
+ }
40
65
  }
41
66
  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));
67
+ 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
68
  skill(d,d.proof.frontend,'frontend',writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity);
44
69
  if(!complete)return;
45
70
  assert.ok(writes.length,'Actual implementation write missing; metadata is not implementation');
@@ -85,7 +110,7 @@ function design(d) {
85
110
  assert.ok(query.decisions?.length,'UIUX result decisions required');
86
111
  for(const decision of query.decisions) {
87
112
  assert.ok(['adopted','adjusted','rejected','no-match'].includes(decision.decision)&&decision.rationale&&decision.identity,'UIUX decision identity and rationale required');
88
- assert.ok(E.contentText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
113
+ assert.ok(E.resultText(call.result).includes(decision.identity),'UIUX identity absent from actual result');
89
114
  if(['adopted','adjusted'].includes(decision.decision))assert.ok(Object.hasOwn(d.files||{},decision.implementation)&&decision.scenarioId,'Adopted UIUX result needs source and verification scenario');
90
115
  }
91
116
  }
@@ -94,7 +119,7 @@ function design(d) {
94
119
  }
95
120
  function build(d) {
96
121
  assert.ok(d.build?.buildId&&d.build?.buildDigest,'Current checked build required');
97
- const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.contentText(call.result);
122
+ const call=nativeRef(d,d.proof.build),input=JSON.stringify(call.input),output=E.resultText(call.result);
98
123
  assert.ok(['Bash','execute_command','run_terminal_command'].includes(call.name)&&input.includes('native-pilot.cjs')&&/\bbuild\b/.test(input)&&input.includes(d.binding.directory),'Actual current build command required');
99
124
  assert.ok(output.includes(d.build.buildId)&&output.includes(d.build.buildDigest),'Native build result differs from current build');
100
125
  same(d.build.candidateDigest,d.current.digest,'Build candidate is stale');return call;
@@ -141,7 +166,7 @@ function evaluate(data,stage) {
141
166
  if(needs.includes('build'))build(data);
142
167
  if(needs.includes('browser'))skill(data,data.proof.browser?.skill||data.devtools?.skill,'browser');
143
168
  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};
169
+ 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
170
  } catch(error){return {status:'failed',stage,errors:[error.message],invocationVerification:'unverified'};}
146
171
  }
147
172
  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,57 @@
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 N=require('./native-command.cjs');
5
+ const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
6
+ const blockedReason='原型补调已达到自动恢复上限,当前任务仍被阻塞。请如实报告缺项及失败原因;修复依赖、权限或宿主后由用户发起继续。不能取得合格交付回执。';
7
+ function missing({source,binding,policy,stage}){
8
+ const unbound=!N.completedBegin(source,binding);
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']:[])]:[])];
10
+ const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
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)))];
12
+ }
13
+ function instruction(keys,{directory}={}){
14
+ const skills=keys.filter(key=>names[key]).map(key=>names[key]);
15
+ const begin=keys.includes('execution-begin')?`先为当前任务独立执行 execution-begin${directory?':\n'+N.beginCommand(directory)+'\n':'。'}不得拼接 cd &&、管道、重定向或 head;等待 status=bound 且退出码为 0,再重试原操作。`:'';
16
+ return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。':''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
17
+ }
18
+ function record(index,context,answer){
19
+ const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
20
+ const taskKey=E.sha(JSON.stringify([sessionId,prompt.line,prompt.digest,directory]));
21
+ if(!answer.deny&&!answer.feedback&&!index.recoveries?.[taskKey])return answer;
22
+ index.recoveries||={};
23
+ const state=index.recoveries[taskKey]||={schemaVersion:1,total:0,counts:{},events:{},pending:{},invocations:[],status:'required'};
24
+ state.feedbackLine??=Math.max(prompt.line,...Object.values(state.pending).map(item=>item.line));
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}:{})});
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()};};
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});
29
+ if(answer.deny||answer.feedback){
30
+ if(context.expectedRetry&&(!state.expectedRetry||state.status==='recovered'))state.expectedRetry=context.expectedRetry;
31
+ const keys=[...new Set(answer.missing?.length?answer.missing:['execution-evidence'])];
32
+ const limits=context.policy?.recovery||{perMissing:2,total:6};
33
+ if(state.total>=limits.total||keys.some(key=>(state.counts[key]||0)>=limits.perMissing)){
34
+ state.status='blocked';
35
+ const result=answer.deny?{deny:true,reason:blockedReason,missing:keys}:{continue:true,context:blockedReason};return remember(result);
36
+ }
37
+ state.total++;for(const key of keys)state.counts[key]=(state.counts[key]||0)+1;state.status='required';
38
+ const line=Math.max(prompt.line,...source.calls.map(call=>call.resultLine||call.requestLine));
39
+ state.feedbackLine=Math.max(state.feedbackLine,line);
40
+ for(const item of keys)state.pending[item]||={line,eventKey,...(item==='execution-evidence'&&context.stage?{stage:context.stage}:{})};
41
+ }else{
42
+ for(const [key,pending] of Object.entries(state.pending)){
43
+ if(names[key]){
44
+ 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));
45
+ if(!call)continue;
46
+ 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});
47
+ }else if(!(context.resolved||[]).includes(key)||pending.stage&&pending.stage!==context.stage)continue;
48
+ delete state.pending[key];
49
+ }
50
+ if(!Object.keys(state.pending).length&&state.status!=='recovered'){
51
+ const matchingRetry=!state.expectedRetry||Object.entries(state.expectedRetry).every(([key,value])=>context.retry?.[key]===value);
52
+ state.status=context.retrySucceeded&&matchingRetry&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
53
+ }
54
+ }
55
+ return remember({...answer});
56
+ }
57
+ module.exports={missing,instruction,record};
@@ -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,8 +12,9 @@ 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 project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product});assert.ok(selected,'Hooks are not enabled or are shadowed');
15
+ const recover=Recovery.instruction(['execution-begin'],{directory});
16
+ const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; '+recover);
17
+ const project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product,host:pointer.host||'codebuddy'});assert.ok(selected,'Hooks are not enabled or are shadowed');
16
18
  assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
17
19
  const stateDirectory=C.sessionDirectory(selected,project,pointer.sessionId),indexFile=path.join(stateDirectory,'session.json');
18
20
  const index=JSON.parse(C.read(indexFile)||'null'),entry=index?.bindings?.[directory];assert.ok(entry,'Native Hook run is missing');
@@ -23,6 +25,8 @@ function binding(directory) {
23
25
  const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
24
26
  const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
25
27
  assert.ok(!C.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
28
+ assert.equal(entry.entryLine,source.prompts.at(-1).line,'Native task changed; '+recover);
29
+ assert.equal(entry.contextLine,source.contextLine,'Native context changed; '+recover);
26
30
  return {entry,source,selected};
27
31
  }
28
32
  function data(directory,lib,options={}) {
@@ -33,7 +37,7 @@ function data(directory,lib,options={}) {
33
37
  const pointer=optional(directory,'last-build.json');let build=null;
34
38
  if(pointer)try{build=lib.verifyBuild(directory,pointer.buildId);}catch{/* A changed candidate must be allowed to rebuild; build/browser gates reject null. */}
35
39
  const latest=optional(directory,'qa/latest.json');
36
- const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?json(inside(directory,proof.browser.report)):null);
40
+ const business=options.businessReport||(latest?json(inside(directory,latest.report)):proof?.browser?.report?optional(directory,proof.browser.report):null);
37
41
  const framework=json(inside(directory,'inputs/framework.json'));
38
42
  return {policy:fixed,current,binding:entry,source,proof,assertions:json(inside(directory,'inputs/execution-assertions.json')),designReview:optional(directory,'qa/design-advice.json'),scope:json(inside(directory,'inputs/scope.json')),implementation:optional(directory,'design-system/implementation.json'),files,frameworkVersion:framework.profile?.versions?.['antdv-next']||json(inside(directory,'app/package.json')).dependencies['antdv-next'].replace(/^[~^]/,''),build,business,devtools:options.devtoolsReport||optional(directory,'qa/devtools-review.json'),selected};
39
43
  }
@@ -54,6 +58,7 @@ function template(directory,lib) {
54
58
  }
55
59
  function begin(directory,lib) {
56
60
  directory=fs.realpathSync(directory);const {entry}=binding(directory);
61
+ assert.equal(entry.inputDigest,lib.manifest(directory).inputDigest,'Inputs changed; '+Recovery.instruction(['execution-begin'],{directory}));
57
62
  return {status:'bound',operation:'execution-begin',directory,runId:entry.runId,sessionId:entry.sessionId,policyDigest:entry.policyDigest,evidenceTemplate:template(directory,lib),
58
63
  next:'Load applicable Skills through CodeBuddy, implement the current scope, and bind actual native result IDs in qa/execution-evidence.json. Binding is not completion.'};
59
64
  }
@@ -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:event==='Stop'?{continue:false,reason:result.reason}:{continue:true,hookSpecificOutput:{hookEventName:event,additionalContext: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,35 @@
1
+ 'use strict';
2
+ // One non-executing parser shared by Hook dispatch and A receipt verification.
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');
6
+ function tokens(command) {
7
+ if(typeof command!=='string'||/[\x00\r\n]|\$\(|`/.test(command))return null;
8
+ const values=[];let value='',quote=null,started=false;
9
+ 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;}
10
+ 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;}}
11
+ if(quote)return null;if(started)values.push(value);return values;
12
+ }
13
+ function nativeCommand(tool,input,cwd) {
14
+ if(!['Bash','execute_command','run_terminal_command'].includes(tool))return null;
15
+ 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;
16
+ const operation=argv[index+1],root=argv[index+2]&&!argv[index+2].startsWith('--')?path.resolve(cwd,argv[index+2]):null;
17
+ return {operation,root,argv};
18
+ }
19
+ const stageFor=Object.freeze({build:'implementation','record-run':'browser-entry','record-devtools-review':'browser-entry',finalize:'delivery'});
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};
@@ -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);
@@ -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};
@@ -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; });
@@ -4,7 +4,7 @@
4
4
  const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
5
5
  const R=require('./codebuddy-registration.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs');
6
6
  function context(options) {
7
- assert.equal(options.host||'codebuddy','codebuddy','A+B supports CodeBuddy only');
7
+ const host=options.host||'codebuddy';assert.ok(['codebuddy','workbuddy'].includes(host),'A+B supports CodeBuddy and WorkBuddy');
8
8
  const project=fs.realpathSync(path.resolve(options.project||'.')),scope=options.scope;
9
9
  assert.ok(['project','global'].includes(scope),'Specify project or global installation scope');
10
10
  const base=scope==='project'?path.join(project,'.codebuddy'):C.globalBase(options);
@@ -12,7 +12,7 @@ function context(options) {
12
12
  const record=recordText?JSON.parse(recordText):null;
13
13
  const unified=path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs');
14
14
  const scriptPath=options.entryPath||(fs.existsSync(unified)?unified:path.join(base,'skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs'));
15
- return {project,scope,base,record,scriptPath};
15
+ return {project,scope,base,record,scriptPath,host};
16
16
  }
17
17
  function prepare(options,runtime={}) {
18
18
  const current=runtime.current||context(options),readText=runtime.readText||R.fileIO.read,action=options.action||'keep';
@@ -21,12 +21,8 @@ function prepare(options,runtime={}) {
21
21
  if(action==='disable'){const settings=R.config(readText(path.join(current.base,'settings.json')));assert.ok(!Object.values(settings.hooks||{}).flat().some(g=>g.hooks.some(h=>h.command?.includes(R.OWNER))),'Unknown owned-looking Hook has no ownership record; preserve it for review');}
22
22
  return {summary:{status:'not-enabled',scope:current.scope,changed:false},unchanged:true};
23
23
  }
24
- const product=options.product||current.record?.product;assert.ok(['ide','cli'].includes(product),'Use --codebuddy-product ide or cli when enabling Hooks');
25
- if(action!=='disable') {
26
- const manifest=readText(path.join(current.base,'.i3-composer-agent.json'));
27
- if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared CodeBuddy/WorkBuddy registration cannot safely identify the product; preserve settings and use an exclusive CodeBuddy scope');
28
- assert.ok(!options.agents?.includes('workbuddy')||current.scope!=='project','Do not enable CodeBuddy audit in a shared WorkBuddy project target');
29
- }
24
+ assert.ok(options.product===undefined||['auto','ide','cli'].includes(options.product),'Invalid Buddy product profile');
25
+ const product='auto'; // Legacy product arguments are hints, never a second new registration.
30
26
  const explicit=options.transcriptRoot,roots=explicit==='auto'?[]:explicit?[path.resolve(explicit)]:current.record?.transcriptRoots||[];
31
27
  const mode=explicit?(explicit==='auto'?'auto':'manual'):current.record?.transcriptMode||(roots.length?'manual':'auto');
32
28
  if(action!=='disable') {
@@ -34,7 +30,7 @@ function prepare(options,runtime={}) {
34
30
  }
35
31
  const settingsText=readText(path.join(current.base,'settings.json')),settings=R.config(settingsText);
36
32
  if(action!=='disable')assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic auditing; preserve the existing policy');
37
- return R.plan({action,...current,product,nodePath:process.execPath,scriptDigest:runtime.scriptDigest||C.bundle(options.entrySource||(path.basename(current.scriptPath)==='i3-prototype.cjs'?current.scriptPath:path.join(__dirname,'hosts/codebuddy.cjs'))),currentText:settingsText,
33
+ return R.plan({action,...current,product,hosts:options.hosts,agents:options.agents,nodePath:process.execPath,scriptDigest:runtime.scriptDigest||C.bundle(options.entrySource||(path.basename(current.scriptPath)==='i3-prototype.cjs'?current.scriptPath:path.join(__dirname,'hosts/codebuddy.cjs'))),currentText:settingsText,
38
34
  transcriptMode:mode,transcriptRoots:roots,productVersion:options.productVersion||current.record?.productVersion||'unreported'});
39
35
  }
40
36
  function configure(options) {
@@ -51,8 +47,9 @@ function check(options,runtime={}) {
51
47
  return {...result,status:result.filesPresent?'files-present':'not-enabled'};
52
48
  }
53
49
  try {
54
- const record=(runtime.registration||C.registration)(current.base),effective=(runtime.select||C.select)({project:current.project,product:record.product,home:options.home,env:options.env});
55
- Object.assign(result,{product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:effective?.base===current.base,transcriptRoots:record.transcriptRoots,
50
+ const record=(runtime.registration||C.registration)(current.base),host=options.host||'codebuddy',products=record.product==='auto'?(host==='workbuddy'?['ide']:['ide','cli']):[record.product];
51
+ const effectiveByProduct=Object.fromEntries(products.map(product=>[product,(runtime.select||C.select)({project:current.project,product,host,home:options.home,env:options.env})?.base===current.base]));
52
+ Object.assign(result,{host,product:record.product,productVersion:record.productVersion,registrationDigest:record.registrationDigest,effective:Object.values(effectiveByProduct).every(Boolean),effectiveByProduct,transcriptRoots:record.transcriptRoots,
56
53
  transcript:{mode:record.transcriptMode||(record.transcriptRoots?.length?'manual':'auto'),status:'awaiting-native-event',invocationVerified:false}});
57
54
  let observation=null;
58
55
  const parent=path.join(current.base,'state/i3-prototype',C.projectKey(current.project));
@@ -62,18 +59,21 @@ function check(options,runtime={}) {
62
59
  const raw=R.fileIO.read(path.join(parent,name,'session.json'));if(!raw)continue;
63
60
  entries.push(...Object.values(JSON.parse(raw).bindings||{}));
64
61
  }
65
- for(const entry of entries)if(entry.project===current.project&&entry.registrationDigest===record.registrationDigest&&entry.settingsDigest===record.settingsDigest&&(!observation||entry.lastHook.at>observation.lastHook.at))observation=entry;
62
+ 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;
66
63
  if(observation) {
67
64
  result.transcript.path=observation.source.path;
68
65
  C.prefix(observation.source.path,observation.source.bytes,observation.source.digest);
69
66
  C.source({session_id:observation.sessionId,transcript_path:observation.source.path},record,current.project);
70
67
  Object.assign(result.transcript,{status:'verified',invocationVerified:true});
71
68
  result.observation={sessionId:observation.sessionId,projectKey:observation.projectKey,runId:observation.runId,event:observation.lastHook.event,at:observation.lastHook.at};
69
+ const observedProduct=observation.product||(record.product!=='auto'?record.product:null);
70
+ 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
72
  }
73
73
  const recent=observation&&Date.now()-Date.parse(observation.lastHook.at)>=0&&Date.now()-Date.parse(observation.lastHook.at)<30*60*1000;
74
74
  return {...result,status:result.effective&&recent?'active':record.product==='cli'?'pending-host-review':'registered',
75
75
  activationScope:'Only the displayed project/session/configuration and adapter bytes; not all projects or business acceptance',
76
- next:record.product==='cli'?'Review /hooks in CodeBuddy Code, then use a new session.':'Reload CodeBuddy and verify a real prototype write task.'};
76
+ next:'CLI: review /hooks; desktop: reload. Verify an actual Skill invocation and missing-Skill recovery in the chosen host.'};
77
77
  }catch(error){if(result.transcript)Object.assign(result.transcript,{status:'unverified',invocationVerified:false});return {...result,status:'failed',errors:[error.message]};}
78
78
  }
79
79
  function main(argv=process.argv.slice(2)) {
@@ -12,7 +12,7 @@ function globalSkillsDir(agent, home, env) {
12
12
  let base = home;
13
13
  if (config.base === 'config') base = env.XDG_CONFIG_HOME && path.isAbsolute(env.XDG_CONFIG_HOME) ? env.XDG_CONFIG_HOME : path.join(home, '.config');
14
14
  if (config.base === 'env') base = env[config.env]?.trim() || path.join(home, config.fallback);
15
- if (config.env === 'CODEBUDDY_CONFIG_DIR' && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error('CODEBUDDY_CONFIG_DIR must be an absolute safe directory');
15
+ if (['CODEBUDDY_CONFIG_DIR','WORKBUDDY_CONFIG_DIR'].includes(config.env) && (!path.isAbsolute(base) || /[\x00\r\n]/.test(base))) throw new Error(`${config.env} must be an absolute safe directory`);
16
16
  if (config.alternatives && !fs.existsSync(path.join(home, path.dirname(config.path)))) {
17
17
  const alternative = config.alternatives.find(relative => fs.existsSync(path.join(home, path.dirname(relative))));
18
18
  if (alternative) return path.join(home, alternative);