@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.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +2 -2
- package/bin/i3-composer-agent.cjs +18 -15
- package/bin/wizard.cjs +12 -52
- package/docs/codebuddy-setup.md +21 -7
- package/docs/codex-setup.md +1 -1
- package/docs/other-hosts.md +1 -1
- package/docs/workbuddy-setup.md +18 -2
- package/hooks/manage.cjs +22 -14
- package/package.json +5 -1
- package/plugin.json +1 -1
- package/scripts/ui-prototype-buddy-lifecycle-test.js +89 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +1 -1
- package/scripts/ui-prototype-dependencies-test.js +13 -0
- package/scripts/ui-prototype-execution-gates-test.js +24 -0
- package/scripts/ui-prototype-execution-policy-test.js +12 -1
- package/scripts/ui-prototype-recovery-test.js +72 -0
- package/skills/i3-ui-prototype/SKILL.md +5 -3
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +10 -6
- package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
- package/skills/i3-ui-prototype/references/execution-evidence.md +25 -8
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +1 -1
- package/skills/i3-ui-prototype/references/project-layout.md +1 -1
- package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +14 -14
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +74 -35
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +7 -5
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +23 -2
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +28 -7
- package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +52 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +9 -5
- package/skills/i3-ui-prototype/scripts/native-command.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +2 -4
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +5 -1
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +13 -13
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +1 -1
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Real filesystem/Hook handlers with explicitly synthetic native events, not host acceptance.
|
|
3
|
+
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),os=require('node:os'),path=require('node:path');
|
|
4
|
+
const installed=path.join(__dirname,'lib/installation.js');
|
|
5
|
+
const scripts=path.join(fs.existsSync(installed)?require(installed).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');
|
|
6
|
+
const H=require(path.join(scripts,'codebuddy-hook.cjs')),R=require(path.join(scripts,'codebuddy-registration.cjs')),C=require(path.join(scripts,'codebuddy-context.cjs'));
|
|
7
|
+
const P=require(path.join(scripts,'execution-policy.cjs')),A=require(path.join(scripts,'execution-assertions.cjs')),lib=require(path.join(scripts,'native-pilot-lib.cjs')),X=require(path.join(scripts,'execution-runtime.cjs'));
|
|
8
|
+
function fixture(t,host,scope,client){
|
|
9
|
+
const root=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'i3-buddy-lifecycle-fixture-')));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
|
|
10
|
+
const project=path.join(root,'business'),home=path.join(root,'isolated-home');fs.mkdirSync(project);fs.mkdirSync(home);
|
|
11
|
+
const scopeFile=path.join(root,'scope.json');fs.writeFileSync(scopeFile,JSON.stringify({schemaVersion:1,title:'Synthetic fixture',pages:[{id:'orders',title:'Orders',referencePage:'models-page',regions:[{id:'list',mode:'management-list'}]}],requirements:[{id:'R1',text:'Synthetic contract only',pages:['orders'],source:{type:'prd',reference:'fixture',statement:'Not native host or business acceptance'}}],exemptions:[]}));
|
|
12
|
+
const opts={project,scope:scopeFile,expertMode:'template'};
|
|
13
|
+
const facts=A.template(opts);for(const item of facts.scenarios)item.checks=[{method:'evaluate_script',pointer:'/fixture',operator:'equals',expected:true}];
|
|
14
|
+
const assertions=path.join(root,'facts.json');fs.writeFileSync(assertions,JSON.stringify(facts));
|
|
15
|
+
const created=lib.createPrototype({...opts,id:'orders',host,executionPolicy:P.AUTO_ID,executionAssertions:assertions});
|
|
16
|
+
const base=scope==='project'?path.join(project,'.codebuddy'):path.join(home,`.${host}`),entry=path.join(scripts,'hosts/codebuddy.cjs');
|
|
17
|
+
const registered=R.plan({action:'enable',scope,host,base,project,nodePath:process.execPath,scriptPath:entry,scriptDigest:C.bundle(entry)});R.apply(registered);
|
|
18
|
+
const file=client?path.join(root,'native.jsonl'):path.join(home,`.${host}`,'projects','business','fixture-native-session.jsonl'),sessionId='fixture-native-session',rows=[];let number=0;
|
|
19
|
+
fs.mkdirSync(path.dirname(file),{recursive:true});
|
|
20
|
+
function append(row){
|
|
21
|
+
if(!client){
|
|
22
|
+
const block=Array.isArray(row.message?.content)?row.message.content[0]:null;
|
|
23
|
+
if(block?.type==='tool_use')row={type:'function_call',callId:block.id,name:block.name,arguments:JSON.stringify(block.input)};
|
|
24
|
+
else if(block?.type==='tool_result')row={type:'function_call_result',callId:block.tool_use_id,output:block.content,status:block.is_error?'failed':'completed'};
|
|
25
|
+
else row={type:'message',role:row.type,content:[{type:row.type==='user'?'input_text':'output_text',text:row.message?.content||''}]};
|
|
26
|
+
}
|
|
27
|
+
rows.push({...row,sessionId,cwd:project});fs.writeFileSync(file,rows.map(value=>JSON.stringify(value)).join('\n')+'\n');
|
|
28
|
+
}
|
|
29
|
+
function hook(event,name,input,id){return H.handle({client,hook_event_name:event,session_id:sessionId,transcript_path:file,cwd:project,tool_name:name,tool_input:input,tool_use_id:id},{event,base,scope,product:'auto',home,env:{}});}
|
|
30
|
+
function request(name,input){const id='fixture-call-'+(++number);append({type:'assistant',message:{content:[{type:'tool_use',id,name,input}]}});return id;}
|
|
31
|
+
function result(id,content,failed=false){append({type:'user',message:{content:[{type:'tool_result',tool_use_id:id,content,is_error:failed}]}});}
|
|
32
|
+
function tool(name,input,content='success',action=()=>{}){
|
|
33
|
+
const id=request(name,input),decision=hook('PreToolUse',name,input,id);
|
|
34
|
+
if(decision.deny){result(id,decision.reason,true);return {id,decision};}
|
|
35
|
+
action();result(id,content);return {id,decision,post:hook('PostToolUse',name,input,id)};
|
|
36
|
+
}
|
|
37
|
+
append({type:'user',message:{content:'实现原型,使用模板模式。Synthetic fixture only.'}});
|
|
38
|
+
tool('Skill',{skill:'i3-ui-prototype'},'Prototype Skill loaded');
|
|
39
|
+
const begin=()=>tool('Bash',{command:`node '${path.join(scripts,'native-pilot.cjs')}' execution-begin '${created.directory}'`},'bound');begin();
|
|
40
|
+
return {root,project,base,directory:created.directory,append,hook,tool,begin,result,request,read(){const pointer=JSON.parse(fs.readFileSync(path.join(created.directory,'qa/execution-context.json')));const selected=C.select({project,product:client==='CodeBuddyCLI'||!client&&host==='codebuddy'?'cli':'ide',host,home,env:{}});return JSON.parse(fs.readFileSync(path.join(C.sessionDirectory(selected,project,pointer.sessionId),'session.json')));}};
|
|
41
|
+
}
|
|
42
|
+
for(const [host,client] of [['codebuddy','CodeBuddyCLI'],['codebuddy','CodeBuddyIDE'],['workbuddy','WorkBuddy'],['codebuddy',null],['workbuddy',null]])for(const scope of ['project','global'])test(`${host} ${client||'native-flat-log'} ${scope}: deny, correlate successful Skills, retry and retain budget`,t=>{
|
|
43
|
+
const f=fixture(t,host,scope,client),file=path.join(f.directory,'app/src/RecoveryFixture.vue');
|
|
44
|
+
const denied=f.tool('Write',{file_path:file,content:'fixture'});assert.equal(denied.decision.deny,true);assert.equal(fs.existsSync(file),false);
|
|
45
|
+
assert.match(denied.decision.reason,/i3-frontend-ui-engineering/);
|
|
46
|
+
const before=Object.values(f.read().recoveries)[0].total;f.begin();assert.equal(Object.values(f.read().recoveries)[0].total,before);
|
|
47
|
+
const frontend=f.tool('Skill',{skill:'i3-frontend-ui-engineering'},'Frontend Skill successfully loaded');
|
|
48
|
+
const official=f.tool('Skill',{skill:'antdv'},'Official Skill successfully loaded');
|
|
49
|
+
const retry=f.tool('Write',{file_path:file,content:'fixture'},'written',()=>fs.writeFileSync(file,'fixture'));
|
|
50
|
+
assert.equal(retry.decision.continue,true);assert.equal(fs.readFileSync(file,'utf8'),'fixture');
|
|
51
|
+
const recovery=f.read().bindings[f.directory].recovery;assert.equal(recovery.status,'recovered');assert.equal(recovery.retry.toolCallId,retry.id);
|
|
52
|
+
assert.deepEqual(recovery.invocations.map(item=>item.toolCallId).sort(),[frontend.id,official.id].sort());
|
|
53
|
+
assert.equal(X.check(f.directory,'delivery',lib).status,'failed','Recovery is not business acceptance or a delivery receipt');
|
|
54
|
+
});
|
|
55
|
+
test('Stop asks for missing Skills twice, then reports blocked without an endless loop',t=>{
|
|
56
|
+
const f=fixture(t,'codebuddy','project','CodeBuddyCLI');
|
|
57
|
+
for(let n=0;n<3;n++){
|
|
58
|
+
f.append({type:'assistant',message:{content:'已完成,可以交付 '+n}});
|
|
59
|
+
const answer=f.hook('Stop');
|
|
60
|
+
if(n<2)assert.equal(answer.feedback,true);else {assert.equal(answer.continue,true);assert.equal(answer.recovery.status,'blocked');}
|
|
61
|
+
}
|
|
62
|
+
f.append({type:'user',message:{content:'先分析,不改代码'}});
|
|
63
|
+
const answer=f.tool('Write',{file_path:path.join(f.directory,'app/src/NoWrite.vue'),content:'no'});assert.equal(answer.decision.deny,true);assert.match(answer.decision.reason,/read-only/);
|
|
64
|
+
});
|
|
65
|
+
test('a duplicated native callback does not spend recovery budget when the transcript grows',t=>{
|
|
66
|
+
const f=fixture(t,'workbuddy','project',null),input={file_path:path.join(f.directory,'app/src/Duplicate.vue'),content:'fixture'};
|
|
67
|
+
const id=f.request('Write',input);assert.equal(f.hook('PreToolUse','Write',input,id).deny,true);
|
|
68
|
+
f.append({type:'assistant',message:{content:'native transcript metadata was persisted'}});
|
|
69
|
+
assert.equal(f.hook('PreToolUse','Write',input,id).deny,true);
|
|
70
|
+
assert.equal(Object.values(f.read().recoveries)[0].total,1);
|
|
71
|
+
});
|
|
72
|
+
test('stage recovery waits for its shared gate and the matching successful command result',t=>{
|
|
73
|
+
const f=fixture(t,'codebuddy','project','CodeBuddyCLI');
|
|
74
|
+
f.tool('Skill',{skill:'i3-frontend-ui-engineering'});f.tool('Skill',{skill:'antdv'});
|
|
75
|
+
// This test covers the Hook/recovery boundary; execution-runtime tests exercise
|
|
76
|
+
// the real shared predicate with genuine source/evidence fixtures separately.
|
|
77
|
+
const original=X.check;t.after(()=>{X.check=original;});
|
|
78
|
+
X.check=()=>({status:'failed',errors:['Synthetic implementation evidence missing']});
|
|
79
|
+
const command=`node '${path.join(scripts,'native-pilot.cjs')}' build '${f.directory}'`;
|
|
80
|
+
assert.equal(f.tool('Bash',{command}).decision.deny,true);
|
|
81
|
+
X.check=()=>({status:'passed'});
|
|
82
|
+
const file=path.join(f.directory,'app/src/Unrelated.vue');
|
|
83
|
+
f.tool('Write',{file_path:file,content:'unrelated'},'written',()=>fs.writeFileSync(file,'unrelated'));
|
|
84
|
+
assert.equal(f.read().bindings[f.directory].recovery.status,'required','An unrelated successful UI edit cannot resolve a stage failure');
|
|
85
|
+
const retry=f.tool('Bash',{command},'successful native build command');
|
|
86
|
+
const recovery=f.read().bindings[f.directory].recovery;
|
|
87
|
+
assert.equal(recovery.status,'recovered');assert.equal(recovery.retry.toolCallId,retry.id);
|
|
88
|
+
assert.equal(recovery.retry.kind,'stage');assert.equal(recovery.retry.stage,'implementation');assert.equal(recovery.retry.operation,'build');
|
|
89
|
+
});
|
|
@@ -33,7 +33,7 @@ test('native compact boundaries invalidate loads and mixed-session results are r
|
|
|
33
33
|
test('effective binding differs by IDE override and CLI merge without writing any settings',()=>{
|
|
34
34
|
const C=require(prototypeScripts+'/codebuddy-context.cjs'),path=require('node:path');
|
|
35
35
|
const texts=new Map(),records=new Map(),project='/work/p',home='/home/user',projectBase=path.join(project,'.codebuddy'),globalBase=path.join(home,'.codebuddy');
|
|
36
|
-
const args={project,home,readText:file=>texts.get(file)||null,loadRegistration:base=>records.get(base)||null};
|
|
36
|
+
const args={project,home,env:{},readText:file=>texts.get(file)||null,loadRegistration:base=>records.get(base)||null};
|
|
37
37
|
records.set(globalBase,{base:globalBase,product:'ide'});texts.set(path.join(projectBase,'settings.json'),'{}');
|
|
38
38
|
assert.equal(C.select({...args,product:'ide'}),null,'IDE cannot fall back through shadowing project settings');
|
|
39
39
|
records.set(globalBase,{base:globalBase,product:'cli'});assert.equal(C.select({...args,product:'cli'}).base,globalBase);
|
|
@@ -131,6 +131,19 @@ test('antDv error text or error JSON with exit zero is not successful knowledge'
|
|
|
131
131
|
assert.equal(deps.check(options).antdv.tool.availability, 'unusable');
|
|
132
132
|
}
|
|
133
133
|
});
|
|
134
|
+
test('local CLI handoff uses the real Node entry with no PATH command and rejects failed business queries',async t=>{
|
|
135
|
+
const options={...workspace(t),host:'workbuddy'};options.env.PATH='';
|
|
136
|
+
await installer.install({...options,skills:'antdv'},{prepare:officialFixture,progress(){}});
|
|
137
|
+
const detected=deps.check(options).antdv.tool;
|
|
138
|
+
assert.equal(detected.nodePath,process.execPath);assert.equal(detected.invocationVerified,false);
|
|
139
|
+
const result=deps.query({...options,kind:'info',component:'Table'});
|
|
140
|
+
assert.equal(result.status,'queried');assert.equal(result.command[0],process.execPath);assert.equal(result.command[1],detected.entry);
|
|
141
|
+
assert.equal(result.data.name,'Table');assert.equal(result.skillInvocationVerified,false);
|
|
142
|
+
const file=path.join(resultTool(options),'node_modules/@antdv-next/cli/cli.cjs');
|
|
143
|
+
fs.writeFileSync(file,'const v=process.argv[process.argv.indexOf("--ver")+1];process.stdout.write(v==="1.5.3"?"Error: missing target knowledge":JSON.stringify({name:"Table",props:[]}));');
|
|
144
|
+
assert.equal(deps.query({...options,kind:'info',component:'Table',version:'1.5.3'}).status,'failed');
|
|
145
|
+
assert.throws(()=>deps.query({...options,kind:'upgrade'}),/read-only/);
|
|
146
|
+
});
|
|
134
147
|
function resultTool(options) { return path.join(deps.check(options).locations.toolsDirectory, 'antdv-cli'); }
|
|
135
148
|
for (const host of ['codebuddy', 'workbuddy', 'codex', 'claude-code']) for (const scope of ['project', 'global']) {
|
|
136
149
|
test(`${host} ${scope} optional installation keeps all tools inside the selected host root`, async t => {
|
|
@@ -36,6 +36,30 @@ test('reading a skill, adapted metadata, or incomplete review mappings cannot pa
|
|
|
36
36
|
const g=fixture();g.proof.frontend.files=[];assert.equal(G.evaluate(g,'implementation').status,'failed');
|
|
37
37
|
const h=fixture();h.proof.frontend.requirementIds=[];assert.equal(G.evaluate(h,'implementation').status,'failed');
|
|
38
38
|
});
|
|
39
|
+
test('new WorkBuddy implementation uses the shared design gate and rechecks recovery references',()=>{
|
|
40
|
+
const f=fixture();f.policy=P.create({id:P.AUTO_ID,host:'workbuddy',mode:'template'});
|
|
41
|
+
Object.assign(f.binding,{host:'workbuddy',identitySource:'native-client',policyDigest:f.policy.digest});f.proof.policyDigest=f.policy.digest;
|
|
42
|
+
assert.equal(G.evaluate(f,'implementation').status,'passed');
|
|
43
|
+
f.binding.recovery={status:'recovered',invocations:[{skill:'frontend',toolCallId:'front',resultDigest:f.source.calls[0].resultDigest,requestLine:3,resultLine:4,feedbackLine:1}],retry:{toolCallId:'write'}};
|
|
44
|
+
assert.equal(G.evaluate(f,'implementation').status,'passed');
|
|
45
|
+
f.binding.recovery.retry.toolCallId='invented';assert.equal(G.evaluate(f,'implementation').status,'failed');
|
|
46
|
+
delete f.binding.recovery;f.source.calls.find(call=>call.skill==='antdv').success=false;
|
|
47
|
+
assert.equal(G.evaluate(f,'implementation').status,'failed','Official loading cannot wait until finalization in the new policy');
|
|
48
|
+
});
|
|
49
|
+
function stageRecoveryFixture(){
|
|
50
|
+
const f=fixture(),result='native build succeeded';
|
|
51
|
+
f.source.calls.push({toolCallId:'retry-build',name:'Bash',input:{command:'node /skills/native-pilot.cjs build /work/prototypes/orders'},result,requestLine:13,resultLine:14,success:true,resultDigest:E.sha(result),sessionId:'s1'});
|
|
52
|
+
f.binding.recovery={status:'recovered',invocations:[],retry:{toolCallId:'retry-build',resultDigest:E.sha(result),kind:'stage',stage:'implementation',operation:'build',feedbackLine:12}};
|
|
53
|
+
return f;
|
|
54
|
+
}
|
|
55
|
+
test('the shared gate accepts an actual matching successful stage retry after recovery',()=>{
|
|
56
|
+
const f=stageRecoveryFixture();const result=G.evaluate(f,'implementation');assert.equal(result.status,'passed',result.errors?.join('; '));
|
|
57
|
+
});
|
|
58
|
+
test('stage recovery rejects a wrong operation, stage, root, unsafe command or failed native result',()=>{
|
|
59
|
+
for(const change of [f=>f.binding.recovery.retry.operation='finalize',f=>f.binding.recovery.retry.stage='delivery',f=>f.source.calls.at(-1).input.command='node /skills/native-pilot.cjs build /other/prototypes/orders',f=>f.source.calls.at(-1).input.command+='; echo fake',f=>f.source.calls.at(-1).success=false,f=>f.binding.recovery.retry.kind='ui-write',f=>f.binding.recovery.retry.feedbackLine=14]){
|
|
60
|
+
const f=stageRecoveryFixture();change(f);assert.equal(G.evaluate(f,'implementation').status,'failed');
|
|
61
|
+
}
|
|
62
|
+
});
|
|
39
63
|
test('official query references must be successful, relevant, and version checked',()=>{
|
|
40
64
|
const f=fixture();assert.equal(G.evaluate(f,'design').status,'passed');
|
|
41
65
|
f.proof.official.queries[0].quote='invented';assert.equal(G.evaluate(f,'design').status,'failed');
|
|
@@ -6,9 +6,18 @@ const P = require(prototypeScripts+'/execution-policy.cjs');
|
|
|
6
6
|
test('CodeBuddy policy defaults never confuse a PRD scope file with project/global installation scope',()=>{
|
|
7
7
|
const options={host:'codebuddy',scope:'/work/prototypes/.preparation/orders/scope.json'};
|
|
8
8
|
const result=P.creationOptions(options,{name:'i3-composer-agent',scope:'global',agents:['codebuddy']});
|
|
9
|
-
assert.equal(result.scope,options.scope);assert.equal(result.executionPolicy,P.
|
|
9
|
+
assert.equal(result.scope,options.scope);assert.equal(result.executionPolicy,P.AUTO_ID);
|
|
10
10
|
assert.equal(P.creationOptions({host:'codex'},{name:'i3-composer-agent'}).executionPolicy,undefined);
|
|
11
11
|
assert.equal(P.creationOptions({},null).executionPolicy,undefined);
|
|
12
|
+
assert.equal(P.creationOptions({},{name:'i3-composer-agent',agents:['workbuddy']}).executionPolicy,P.AUTO_ID);
|
|
13
|
+
assert.throws(()=>P.creationOptions({},{name:'i3-composer-agent',agents:['codebuddy','workbuddy']}),/actual --host/);
|
|
14
|
+
});
|
|
15
|
+
test('new Buddy policy supports WorkBuddy without changing the legacy policy',()=>{
|
|
16
|
+
const old=P.create();assert.equal(old.id,'codebuddy-ab-v1');assert.equal(old.schemaVersion,1);
|
|
17
|
+
const policy=P.create({host:'workbuddy',id:P.AUTO_ID});assert.equal(policy.host,'workbuddy');
|
|
18
|
+
assert.equal(P.validate(policy).id,'buddy-ab-v2');assert.deepEqual(policy.recovery,{perMissing:2,total:6});
|
|
19
|
+
assert.ok(P.requirements(policy,'implementation').includes('antdv'));
|
|
20
|
+
assert.throws(()=>P.create({host:'workbuddy',id:P.ID}),/legacy|CodeBuddy/);
|
|
12
21
|
});
|
|
13
22
|
|
|
14
23
|
test('analysis and inspection never acquire implementation requirements', () => {
|
|
@@ -31,6 +40,8 @@ test('official framework fallback is explicit and cannot invent a native invocat
|
|
|
31
40
|
assert.ok(P.requirements(P.create({ officialCallable: false }), 'design').includes('official-source'));
|
|
32
41
|
assert.equal(P.skillKey('fake-i3-frontend-ui-engineering'), null);
|
|
33
42
|
assert.equal(P.skillKey('i3-frontend-ui-engineering'), 'frontend');
|
|
43
|
+
assert.equal(P.skillKey('ui-ux-pro-max:ui-ux-pro-max'),'uiux');
|
|
44
|
+
assert.equal(P.skillKey('arbitrary:ui-ux-pro-max'),null);
|
|
34
45
|
});
|
|
35
46
|
test('policy changes are detected and unsupported hosts cannot silently acquire CodeBuddy support', () => {
|
|
36
47
|
const policy = P.create({ mode: 'template' });
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const test=require('node:test'),assert=require('node:assert/strict'),path=require('node:path'),fs=require('node:fs');
|
|
3
|
+
const installation=path.join(__dirname,'lib/installation.js');
|
|
4
|
+
const scripts=path.join(fs.existsSync(installation)?require(installation).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');
|
|
5
|
+
const E=require(path.join(scripts,'execution-evidence.cjs')),P=require(path.join(scripts,'execution-policy.cjs'));
|
|
6
|
+
const R=()=>require(path.join(scripts,'execution-recovery.cjs'));
|
|
7
|
+
const source=()=>({prompts:[{line:1,digest:'actual-user-task',text:'实现原型'}],calls:[],contextLine:0});
|
|
8
|
+
test('missing frontend returns the actual Skill and concrete retry instruction',()=>{
|
|
9
|
+
const missing=R().missing({source:source(),binding:{sessionId:'s1',entryLine:1,contextLine:0},policy:P.create({id:P.AUTO_ID,mode:'template'})});
|
|
10
|
+
assert.ok(missing.includes('frontend'));assert.ok(missing.includes('antdv'));
|
|
11
|
+
const message=R().instruction(missing);assert.match(message,/i3-frontend-ui-engineering/);assert.match(message,/成功|success/);assert.match(message,/重试|retry/);
|
|
12
|
+
});
|
|
13
|
+
test('recovery correlates a later successful native Skill and a successful retry',()=>{
|
|
14
|
+
const index={bindings:{}},s=source(),context={source:s,sessionId:'s1',directory:'/p/prototypes/a',event:'PreToolUse',eventKey:'denied-write-1'};
|
|
15
|
+
const denied={deny:true,missing:['frontend'],reason:'missing frontend'};
|
|
16
|
+
const first=R().record(index,context,denied);assert.equal(first.recovery.status,'required');
|
|
17
|
+
R().record(index,context,denied);assert.equal(Object.values(index.recoveries)[0].total,1,'Duplicate callbacks do not spend another attempt');
|
|
18
|
+
const rows=[{type:'user',sessionId:'s1',message:{content:'实现原型'}},{type:'assistant',sessionId:'s1',message:{content:[{type:'tool_use',id:'real-front',name:'Skill',input:{skill:'i3-frontend-ui-engineering'}}]}},{type:'user',sessionId:'s1',message:{content:[{type:'tool_result',tool_use_id:'real-front',content:'Skill loaded successfully'}]}}];
|
|
19
|
+
s.calls=E.parse(rows.map(row=>JSON.stringify(row)).join('\n'));
|
|
20
|
+
const ready=R().record(index,{...context,eventKey:'retry-2'},{continue:true});assert.equal(ready.recovery.status,'ready-to-retry');
|
|
21
|
+
const recovered=R().record(index,{...context,event:'PostToolUse',eventKey:'retry-2-result',retrySucceeded:true,retry:{toolCallId:'retry-write',requestLine:4,resultLine:5}},{continue:true});
|
|
22
|
+
assert.equal(recovered.recovery.status,'recovered');assert.equal(recovered.recovery.invocations[0].toolCallId,'real-front');
|
|
23
|
+
});
|
|
24
|
+
test('failed and stale Skill results do not recover and Stop correction is bounded across begin calls',()=>{
|
|
25
|
+
const index={bindings:{}},s=source(),context={source:s,sessionId:'s1',directory:'/p/prototypes/a',event:'Stop'};
|
|
26
|
+
s.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:false}];
|
|
27
|
+
const feedback={feedback:true,missing:['frontend'],reason:'load frontend'};
|
|
28
|
+
for(let n=1;n<=2;n++)assert.equal(R().record(index,{...context,eventKey:'stop-'+n},feedback).feedback,true);
|
|
29
|
+
index.bindings={};
|
|
30
|
+
const exhausted=R().record(index,{...context,eventKey:'stop-3'},feedback);assert.equal(exhausted.feedback,undefined);assert.equal(exhausted.recovery.status,'blocked');assert.match(exhausted.context,/阻塞/);
|
|
31
|
+
const denied=R().record(index,{...context,event:'PreToolUse',eventKey:'write-3'},{deny:true,missing:['frontend'],reason:'load frontend'});assert.equal(denied.deny,true);
|
|
32
|
+
s.contextLine=10;s.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:true}];
|
|
33
|
+
assert.notEqual(R().record(index,{...context,eventKey:'old-load'},{continue:true}).recovery.status,'recovered');
|
|
34
|
+
});
|
|
35
|
+
test('flat native Hook feedback remains metadata and cannot reset the human task or recovery budget',()=>{
|
|
36
|
+
const rows=[{type:'message',role:'user',sessionId:'s1',content:[{type:'input_text',text:'实现原型'}]},{type:'message',role:'user',sessionId:'s1',providerData:{isMeta:true},content:[{type:'input_text',text:'补调 frontend 后继续'}]}];
|
|
37
|
+
const text=rows.map(row=>JSON.stringify(row)).join('\n');
|
|
38
|
+
assert.equal(E.describe(text).prompts.length,1);assert.equal(E.describe(text).prompts[0].text,'实现原型');
|
|
39
|
+
});
|
|
40
|
+
test('a missing Skill has one budget even when the set of other missing Skills changes',()=>{
|
|
41
|
+
const index={},context={source:source(),sessionId:'s1',directory:'/p/prototypes/a',event:'Stop'};
|
|
42
|
+
for(const [n,missing] of [['1',['frontend','antdv']],['2',['frontend']]])assert.equal(R().record(index,{...context,eventKey:n},{feedback:true,missing}).feedback,true);
|
|
43
|
+
const third=R().record(index,{...context,eventKey:'3'},{feedback:true,missing:['frontend','uiux']});
|
|
44
|
+
assert.equal(third.recovery.status,'blocked');assert.equal(third.feedback,undefined);assert.equal(third.recovery.counts.frontend,2);
|
|
45
|
+
});
|
|
46
|
+
test('native output resumes Stop for recovery and shows the user an exhausted-budget warning',()=>{
|
|
47
|
+
const format=require(path.join(scripts,'hosts/codebuddy.cjs')).format;
|
|
48
|
+
assert.deepEqual(format({feedback:true,reason:'补调'},'Stop').output,{continue:false,reason:'补调'});
|
|
49
|
+
const blocked=format({context:'当前仍被阻塞',recovery:{status:'blocked'}},'Stop').output;
|
|
50
|
+
assert.equal(blocked.continue,true);assert.equal(blocked.systemMessage,'当前仍被阻塞');
|
|
51
|
+
assert.equal(format({deny:true,reason:'缺 Skill'},'PreToolUse').output.hookSpecificOutput.permissionDecision,'deny');
|
|
52
|
+
assert.equal(format({continue:true,context:'实施前加载'},'UserPromptSubmit').output.hookSpecificOutput.additionalContext,'实施前加载');
|
|
53
|
+
});
|
|
54
|
+
test('a stage evidence recovery requires the same verified stage and its successful native retry',()=>{
|
|
55
|
+
const index={},context={source:source(),sessionId:'s1',directory:'/p/prototypes/a',event:'PreToolUse',eventKey:'denied-build',stage:'implementation'};
|
|
56
|
+
R().record(index,context,{deny:true,missing:['execution-evidence']});
|
|
57
|
+
const other=R().record(index,{...context,eventKey:'another-stage',stage:'browser-entry',resolved:['execution-evidence'],retrySucceeded:true,event:'PostToolUse'},{continue:true});
|
|
58
|
+
assert.equal(other.recovery.status,'required');
|
|
59
|
+
const ready=R().record(index,{...context,eventKey:'verified-build',resolved:['execution-evidence']},{continue:true});
|
|
60
|
+
assert.equal(ready.recovery.status,'ready-to-retry');
|
|
61
|
+
const retry=R().record(index,{...context,eventKey:'successful-build',resolved:['execution-evidence'],retrySucceeded:true,event:'PostToolUse',retry:{toolCallId:'actual-build-result',requestLine:4,resultLine:5}},{continue:true});
|
|
62
|
+
assert.equal(retry.recovery.status,'recovered');assert.equal(retry.recovery.retry.toolCallId,'actual-build-result');
|
|
63
|
+
});
|
|
64
|
+
test('an old successful callback cannot recover a later stage denial',()=>{
|
|
65
|
+
const s=source();s.calls=[{toolCallId:'old',requestLine:2,resultLine:3,success:true}];
|
|
66
|
+
const index={},context={source:s,sessionId:'s1',directory:'/p/prototypes/a',stage:'implementation'};
|
|
67
|
+
R().record(index,{...context,event:'PreToolUse',eventKey:'deny'},{deny:true,missing:['execution-evidence']});
|
|
68
|
+
const stale=R().record(index,{...context,event:'PostToolUse',eventKey:'old-result',resolved:['execution-evidence'],retrySucceeded:true,retry:s.calls[0]},{continue:true});
|
|
69
|
+
assert.equal(stale.recovery.status,'ready-to-retry');
|
|
70
|
+
const current=R().record(index,{...context,event:'PostToolUse',eventKey:'new-result',resolved:['execution-evidence'],retrySucceeded:true,retry:{toolCallId:'new',requestLine:4,resultLine:5}},{continue:true});
|
|
71
|
+
assert.equal(current.recovery.status,'recovered');assert.equal(current.recovery.retry.feedbackLine,3);
|
|
72
|
+
});
|
|
@@ -61,9 +61,9 @@ CodeBuddy 新建原型采用 A+B 执行合同,实际接口见 [execution-evi
|
|
|
61
61
|
|
|
62
62
|
项目/全局 IDE/CLI 新启用默认从真实 Hook 的 `transcript_path` 自动取得当前会话文件;不让模型搜索/填写日志、不选择最近文件。`transcript.status=awaiting-native-event` 表示尚未观察,不阻止启动已授权任务及 `execution-begin`;进入制作和交付仍必须有可解析的原生证据。已有手动目录限制保持,可由用户显式切回 auto;缺字段、不可读、格式不支持或会话不符时报告未核验,不能用手写记录补齐。
|
|
63
63
|
|
|
64
|
-
CodeBuddy create 前,用 `native-pilot.cjs execution-assertions-template --scope <scope-file>` 取得完整场景,按本次 PRD 与能力参数填充每项预期 JSON 事实,存于该原型 `.preparation` 目录。create 增加 `--host codebuddy --execution-policy
|
|
64
|
+
CodeBuddy/WorkBuddy 新 create 前,用 `native-pilot.cjs execution-assertions-template --scope <scope-file>` 取得完整场景,按本次 PRD 与能力参数填充每项预期 JSON 事实,存于该原型 `.preparation` 目录。create 增加 `--host <codebuddy|workbuddy> --execution-policy buddy-ab-v2 --execution-assertions <assertions-file>`;官方 Skill 真实不可用时才显式 `--official-source-fallback`,保留其失败调用定位。随后在实际所选宿主主会话执行 `execution-begin <prototype-root>`,以原生 Skill 工具加载当前上下文需要的前端 Skill 后再写业务界面。旧 codebuddy-ab-v1 及未采用 executionPolicy 的实例保留原合同。其他宿主尚未适配此 Hook 合同,按原流程披露调用核验边界。
|
|
65
65
|
|
|
66
|
-
`execution-template <prototype-root>` 返回当前身份与已观察到的真实加载引用;以实际查询、实现复核及浏览器结果补齐 `qa/execution-evidence.json`,不能补造调用 ID。构建、record-run、record-devtools-review、finalize 与 `execution-check --stage <stage>` 共用 A;B
|
|
66
|
+
`execution-template <prototype-root>` 返回当前身份与已观察到的真实加载引用;以实际查询、实现复核及浏览器结果补齐 `qa/execution-evidence.json`,不能补造调用 ID。构建、record-run、record-devtools-review、finalize 与 `execution-check --stage <stage>` 共用 A;B 在可识别的写入前检查,反馈明确 Skill 名与下一动作,成功补调后重试;Stop 按同缺项两次、当前原生任务总计六次的预算继续纠正,到上限如实报告阻塞,反复 begin 不清零。只读分析、进度、阻塞及用户停止可正常结束。未通过门禁时交付候选和缺项;新收据的 `native-source-correlated` 不代表不可篡改认证或用户认可。
|
|
67
67
|
|
|
68
68
|
### 2. 完整内容模板与需求能力
|
|
69
69
|
|
|
@@ -102,6 +102,8 @@ UIUX 和框架官方 Skill 独立可选安装,不随本技能默认复制。cr
|
|
|
102
102
|
|
|
103
103
|
加载框架官方 Skill 或对应版本官方文档、类型和源码,先查 API、读示例、再实现。先完成上文工具预检,使用已准备的固定本地 CLI;缺项不因官方 Skill 的安装建议而扩大权限。固定项目依赖锁,记录请求与实际知识版本。映射与官方来源见 [framework-contract.md](references/framework-contract.md)。
|
|
104
104
|
|
|
105
|
+
antDv Skill 成功加载后,优先使用只读命令 `node <skill-root>/scripts/prototype-dependencies.cjs query --host <host> --scope project --project <project-root> --kind info --component <Component> --version <knowledge-version>`,它解析当前宿主本地 Node/CLI 入口,不依赖全局 `antdv` PATH。也可使用检测返回的绝对 Node 和 CLI 入口执行官方只读查询。query 返回不代表 Skill 调用;组件版本与知识版本分别记录,不足时核对目标包类型/源码。官方 Skill 的 allowed-tools/原生权限若不允许实际入口,按原生权限流程处理,不编辑官方正文绕过。
|
|
106
|
+
|
|
105
107
|
宿主已发现可调用的官方框架 Skill 时,先通过真实 Skill 工具加载它;读取 SKILL.md 或运行 CLI 查询不能冒充宿主 Skill 调用。只有该 Skill 在当前执行者中不可调用时,才使用版本匹配的官方文档/类型/源码等效渠道,并明确记录官方 Skill 未唤醒及实际资料来源。UIUX 的 Skill 调用、真实查询、建议采纳与效果验证也分别记录,查询成功不自动记 used。
|
|
106
108
|
|
|
107
109
|
```sh
|
|
@@ -110,7 +112,7 @@ node <skill-root>/scripts/native-pilot.cjs create --project <project-root> --id
|
|
|
110
112
|
node <skill-root>/scripts/native-pilot.cjs create --project <project-root> --id business-demo --scope <project-root>/prototypes/.preparation/business-demo/scope.json --expert-mode template
|
|
111
113
|
```
|
|
112
114
|
|
|
113
|
-
目标存在则 revise
|
|
115
|
+
目标存在则 revise,不重新初始化。首次业务 UI 写入前必须通过宿主 Skill 工具成功调用 `i3-frontend-ui-engineering`;不得仅凭当前 Skill 中的摘要直接实现。收到门禁反馈时立即补调、等待成功返回并重试原操作,失败则报告具体阻塞,不能推迟到下次修订。Follow the `i3-frontend-ui-engineering` skill,使用真实 Table/Form/Drawer/反馈组件及 ConfigProvider。初始化只产生需求对应的框架骨架和四主题,business.status 为 needs-adaptation;这不是已完成业务。实现固定输入中的实际字段、列表和流程,逐个填写 `design-system/implementation.json` 的实际组件、源文件/定位符和固定场景,再核对需求映射、模拟数据并更新 business.status 为 adapted。UIUX 采用规则落实到 design-system、主题和页面;模板样式不锁定生成外观。
|
|
114
116
|
|
|
115
117
|
目标存在但没有 prototype.json 时属于未识别的准备目录,不能当作有效实例执行 revise。先核对来源并保留文件;只有从本次工具记录确认整个目标目录均由当前任务的准备操作产生,才可将该整个目录移到独立准备位置,确保目标路径不存在后重试原子 create。仅移动查询文件仍会留下空目录并阻止 create。来源不明或包含用户已有文件时保留原目录并报告冲突,不得覆盖或清空。
|
|
116
118
|
|
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
3
|
-
"id": "
|
|
2
|
+
"schemaVersion": 2,
|
|
3
|
+
"id": "buddy-command-auto-v2",
|
|
4
4
|
"nativeValidation": "pending-user",
|
|
5
5
|
"platforms": ["darwin", "linux"],
|
|
6
|
+
"hosts": ["codebuddy", "workbuddy"],
|
|
6
7
|
"events": ["SessionStart", "UserPromptSubmit", "PreToolUse", "PostToolUse", "Stop"],
|
|
7
|
-
"transcriptFormat": "assistant-user-tool-use-jsonl-v1",
|
|
8
|
+
"transcriptFormat": ["assistant-user-tool-use-jsonl-v1", "flat-message-function-call-jsonl", "codebuddy-ide-index-v1"],
|
|
8
9
|
"products": {
|
|
9
|
-
"ide": {"settings": "settings.json", "configuration": "project-overrides-global", "transcriptRoot": "native-hook-transcript-path", "
|
|
10
|
-
"cli": {"settings": "settings.json", "configuration": "merged-project-first-binding", "transcriptRoot": "native-hook-transcript-path", "
|
|
10
|
+
"ide": {"settings": "settings.json", "configuration": "codebuddy-project-overrides-global; workbuddy-merged", "transcriptRoot": "native-hook-transcript-path", "nativeReview": "reload-and-verify"},
|
|
11
|
+
"cli": {"settings": "settings.json", "configuration": "merged-project-first-binding", "transcriptRoot": "native-hook-transcript-path", "nativeReview": "/hooks"}
|
|
11
12
|
},
|
|
12
|
-
"
|
|
13
|
+
"identification": "Explicit native client, or a callback log in a configured native host projects directory; unknown/conflicting identity remains unverified.",
|
|
14
|
+
"recovery": {"perMissing": 2, "total": 6, "doesNotForceNextTool": true},
|
|
15
|
+
"sources": ["https://www.codebuddy.cn/docs/ide/Features/Hooks", "https://www.codebuddy.cn/docs/cli/hooks", "https://www.codebuddy.cn/docs/workbuddy/From-Beginner-to-Expert-Guide/Function-Description/Plug-In"],
|
|
16
|
+
"localSourceInspection": {"workbuddy": "5.8.3 bundled CLI: shared command Hook protocol, config/projects/session JSONL, providerData.isMeta; native runtime acceptance pending"}
|
|
13
17
|
}
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
原生 create/revise 仍按工作流程完成业务适配、设计取舍、构建、全部适用的真实验收和 finalize。交付前只读核对 state 中 `delivery.current`、revision、candidateDigest 与实际 `qa/delivery.json` 收据一致。旧构建、旧 QA 或过期收据不能作为当前成功证据。
|
|
52
52
|
|
|
53
|
-
本轮 CodeBuddy 新原生流程仍要求主会话实际调用 `i3-browser-testing-with-devtools`,通过 Chrome DevTools MCP 取得当前候选/构建的 DOM/计算样式、截图、console、network
|
|
53
|
+
本轮 CodeBuddy 新原生流程仍要求主会话实际调用 `i3-browser-testing-with-devtools`,通过 Chrome DevTools MCP 取得当前候选/构建的 DOM/计算样式、截图、console、network、可访问性和性能观测,历史设备合同另外执行其固定 Playwright 专项回归;新完整业务按固定场景执行并 record-run。缺 Skill、MCP 或必需观测时整链未完成。脚本收据和 state 不证明真实调用;原始宿主记录与实际观测另核。其他宿主结果及旧 v1/已有 v2 的固定合同按事实单列,不隐式扩展历史验收。
|
|
54
54
|
|
|
55
55
|
通过本地验证的候选保持 pending-review。只有真实用户对同一 revision/digest 的明确认可,且适用验收仍当前有效,才能确认基线。原型认可不自动批准工程 Plan、生产开发或发布。Legacy HTML 保留原摘要与运行合同。
|
|
56
56
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Buddy A+B 执行与补调合同
|
|
2
2
|
|
|
3
3
|
<!-- i3:zh-guide:start -->
|
|
4
4
|
## 中文使用说明
|
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
默认使用简体中文解释、追问及生成报告;用户明确要求其他语言时可切换。原方法正文、引用、阈值和审批条件完整保留。代码标识、路径、命令、工具参数、JSON 键/枚举与原文引用保持原样。
|
|
9
9
|
<!-- i3:zh-guide:end -->
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
本合同适配 CodeBuddy IDE/CLI 和 WorkBuddy 桌面 command Hooks;各宿主项目/全局的真实调用、补调及业务效果仍须逐项手测,profile 保持 pending-user。不是新增专责 Agent,不改 using 的发现/路由、上游前端/浏览器方法或上游 Hooks。A 是确定性门禁,B 采集与绑定原生来源;没有 C 提示词对照表方案。
|
|
12
12
|
|
|
13
13
|
## 安装范围与原生生效
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
安装器对 CodeBuddy/WorkBuddy 默认注册 prototype 组,不再询问是否启用、组或 IDE/CLI;非交互同默认。其他原版组仍交付脚本,但不新增注册。已有启用组保留,明确禁用由 state/i3-composer-agent/hooks.json 的 prototypeDisabledHosts 记忆;只有显式 enable 撤销禁用。显式 --hooks keep 保留状态,--hooks enable|list|check|disable 和 --hook-groups 继续作为管理参数;旧 --prototype-hooks 仅管理原型组。新注册 product=auto、schemaVersion=2;旧 ide/cli 注册和参数兼容,可安全升级到 auto,手改和未知归属不接管。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
每次仅使用原生 Hook 的 transcript_path 具体文件,校验 session_id、实际项目和工具请求/结果,不扫描最新日志。真实 client 或已配置宿主原生日志位置用于身份识别,身份缺失/冲突不能默认 CLI。WorkBuddy 5.8.3 安装程序源代码表明其桌面使用 CLI 后端协议和原生项目会话 JSONL;本地源代码核对不替代实际回调。--codebuddy-transcript-root/--transcript-root 可设置手动限制,已有 manual roots 不被默认升级放宽;显式 auto 才切回自动。
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
原型 Hook 的新注册入口是 `<host-base>/i3-composer-agent/hooks/i3-prototype.cjs`,委托本 Skill 的 `scripts/hosts/codebuddy.cjs`;旧入口继续兼容。统一管理入口为同目录 `manage.cjs`,原版三组由 `codebuddy-upstream.cjs` 包装,原脚本不改。项目设置在 `.codebuddy/settings.json`;全局 CodeBuddy 为 CODEBUDDY_CONFIG_DIR 或 ~/.codebuddy,全局 WorkBuddy 为 WORKBUDDY_CONFIG_DIR 或 ~/.workbuddy。原型归属在 <host-base>/state/i3-prototype/installation.json,通用归属在 <host-base>/state/i3-composer-agent/hooks.json。项目注册引用项目副本,全局注册引用全局副本。保留原设置、权限和其他 Hook;无效 JSON、被手改的自有注册或未知配置归属拒绝合并;共享 Buddy 项目只注册一个入口,hosts 分别记录与禁用。Windows 命令引用尚未验证,此 B profile 不宣称支持。
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
独立统一管理可调用 `node <host-base>/i3-composer-agent/hooks/manage.cjs list|check|enable|disable --host codebuddy|workbuddy --scope project|global --project <project-root> --groups prototype`。旧 `node <skill-root>/scripts/prototype-hooks.cjs check|enable|disable` 参数保持且只针对 prototype。无需指定 product;旧 `--product ide|cli` 兼容,`--product-version <实际版本>` 可记录安装信息;可选 `--transcript-root <目录|auto>`。全局配置根按所选宿主遵循绝对 CODEBUDDY_CONFIG_DIR/WORKBUDDY_CONFIG_DIR,默认 ~/.codebuddy/~/.workbuddy,工具、注册、状态和运行时选择使用同一根。check 不执行安装、不写配置或缓存。首次没有实际日志时 `transcript.status=awaiting-native-event`,配置可注册但证据尚未核验;不是安装失败,也不算 active。CLI 注册后需用户在原生 `/hooks` 审核,然后新建会话;IDE 需重载。状态为 files-present/registered/pending-host-review/active/failed 等,active 只指展示的项目/会话和当前注册/脚本字节的近期事件。它不代表所有项目已激活或业务已通过。原版三组的 triggered 仅表示包装回调观察,仍是未核验等级。
|
|
22
|
+
|
|
23
|
+
CodeBuddy IDE 对存在的项目设置保守按覆盖处理,不能穿透到全局冒称生效;CodeBuddy CLI/WorkBuddy 在项目/全局合并来源中优先选自有项目绑定,否则全局。未选中的回调退出,选中状态用锁和原生来源游标去重。配置、脚本或 profile 改动使旧观察失效;重复启用不增加注册。disable 仅撤销自有配置,不删除原型与 QA。卸载核心前先 disable,避免悬空自有命令。
|
|
22
24
|
|
|
23
25
|
## 新实例的固定预期
|
|
24
26
|
|
|
@@ -28,7 +30,7 @@ IDE 对存在的项目设置保守按覆盖处理,不能穿透到全局冒称
|
|
|
28
30
|
node <skill-root>/scripts/native-pilot.cjs execution-assertions-template --scope <scope-file>
|
|
29
31
|
```
|
|
30
32
|
|
|
31
|
-
返回所有适用业务/框架场景及 `checks: []`。依据 PRD、绑定参数、框架场景填写每项检查,再传 `create --host codebuddy --execution-policy
|
|
33
|
+
返回所有适用业务/框架场景及 `checks: []`。依据 PRD、绑定参数、框架场景填写每项检查,再传 `create --host <codebuddy|workbuddy> --execution-policy buddy-ab-v2 --execution-assertions <file>`。这个读取命令没有写入副作用。文件置于 `prototypes/.preparation/<id>/`;不更改随包 HTML 模板。每个场景至少一条,不能漏场景或用 `passed` 作实际事实;预期随其他 inputs 固定,不在验收后改到与结果一致。
|
|
32
34
|
|
|
33
35
|
```json
|
|
34
36
|
{"scenarioId":"orders/list/form-validation","checks":[
|
|
@@ -59,8 +61,23 @@ node <skill-root>/scripts/native-pilot.cjs execution-assertions-template --scope
|
|
|
59
61
|
|
|
60
62
|
`execution-check --stage design|implementation-entry|implementation|build|browser-entry|browser|delivery`、build、record-run、record-devtools-review、finalize 使用同一谓词。state 展示 execution 缺项及恢复命令,finalize 成功收据包含 runId/policyDigest、证据摘要和调用等级;HANDOFF 显示同一结果。修改源码/输入/设计/QA 会使相应检查失效,旧收据不能复用。
|
|
61
63
|
|
|
62
|
-
B 的 PreToolUse 只拒绝可识别的缺前置 UI 写入或明确阶段命令,不返回强制 allow,不替换原生结果。它不是通用 Shell 文件隔离器;未知 Shell
|
|
64
|
+
B 的 PreToolUse 只拒绝可识别的缺前置 UI 写入或明确阶段命令,不返回强制 allow,不替换原生结果。它不是通用 Shell 文件隔离器;未知 Shell 写法和所有自然语言完成方式不保证被拦截。新 buddy-ab-v2 的 Stop 对已纳管当前任务、可识别的完成声明且缺有效收据时按有限预算反馈;旧 codebuddy-ab-v1 保留原单次反馈合同。进度、blocked、awaiting-input、用户停止正常结束,不能无限循环。崩溃/超时不生成成功记录,A 拒绝无有效来源的严格完成。
|
|
63
65
|
|
|
64
66
|
本地原文、索引与配置同属用户权限域,`native-source-correlated` 表示受配置来源的逐项回查,不是防篡改签名。文件摘要、chmod、机器落盘或模型自述均不能单独证明可信。若日志本身缺独立可信来源,结果继续披露 unverified,需用户检查真实宿主调用。本测试包原生 profile 状态 pending-user;源码/合成/打包通过不改变它。
|
|
65
67
|
|
|
66
68
|
旧 HTML/未采用 executionPolicy 的原生实例继续原摘要、输入、场景与基线,不自动迁移,新等级为 unverified。四主题、完整离线 HTML、业务共享索引和 moduleIds→appliesTo 不变;跨宿主可读共享原型,不继承 CodeBuddy 的调用证明。
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
## 补调与恢复验收
|
|
72
|
+
|
|
73
|
+
首次业务 UI 写入前,当前有效上下文须成功加载前端 Skill;新 buddy-ab-v2 还检查可调用的 antDv Skill 和专业模式 UIUX。阶段命令使用同一 A 谓词,缺 Skill 时反馈准确名称,已有加载但证据缺项时要求 execution-check 和真实引用,不要求重复加载全部 Skill。
|
|
74
|
+
|
|
75
|
+
B 拒绝后主会话立即补调,等待成功返回,再重试被拒操作;普通 Read 或另开会话不算本主会话 Skill 调用。缺项、反馈事件、后续成功 toolCallId/resultDigest 及重试结果在宿主会话状态落盘。required → ready-to-retry → recovered 分别表示缺项、已可重试、观察到成功重试;recovered 不表示业务验收通过。A 再回查补调及重试的原生引用和时序。
|
|
76
|
+
|
|
77
|
+
重试按 kind 区分 ui-write 和 stage;阶段重试绑定实际 operation、stage 与同一原型目录。两类新记录都以 feedbackLine 核对请求发生在反馈之后,旧成功回调不能恢复后续拒绝;旧无 kind 的记录继续仅按页面写入合同核验。
|
|
78
|
+
|
|
79
|
+
默认同一缺项两次、单原生任务总计六次,重复回调不增加预算,反复 begin 和内部 providerData.isMeta 的 Hook 消息不重置任务。达到上限时保持受管操作拒绝,允许真实 blocked 报告正常结束;不无限驱动 Stop。用户只读/暂停优先。入口、Skill 发现、加载成功、CLI 查询、重试成功、交付和用户认可分开记录。
|
|
80
|
+
|
|
81
|
+
每宿主/范围实测:故意漏调 → 写入被拒且文件未被该操作修改 → 本主会话实际 Skill 成功 → 原操作重试成功;失败调用、旧上下文、假 ID、旧会话及当前构建缺项不能放行。持续缺项须在预算耗尽后报告阻塞。源测试里的合成日志只能验证解析和门禁逻辑,不证明模型会补调。原生 Hooks 没有指定下一工具的执行接口,不能承诺必然调用,也不是任意 Shell 安全隔离。
|
|
82
|
+
|
|
83
|
+
antDv 加载与本地 CLI 查询分别留证:`prototype-deps query --host <host> --scope project --project <project> --kind info --component Table --version 1.5.2` 经解析的绝对 Node/入口执行,仅引用知识 1.5.2;目标 1.5.3 仍核对包类型/源码。probe 不是业务查询,退出零的错误内容仍失败,不自动安装。
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
固定工具的输入事件也须实际验证:本轮 Chrome DevTools MCP 1.11.0 内捆的短文本 fill 路径对空字符串直接设置 DOM value 后结束,没有发送 input/change,可能使界面为空而查询或校验仍使用旧绑定值。保存原始失败观测。独立真实 MCP 实测可用原生 fill 单个空格使输入活跃,再只读取得实际 value 长度和 selectionStart/selectionEnd;空格可能追加到恢复的旧值,不能假定只有一字。确认光标位于末尾后,按实际文本长度逐次原生 press_key Backspace,核对空值,再查询/保存并核对绑定结果。本轮 headless 的 Meta+A/Control+A 未全选,快捷键须按实际结果核对。不能只看 DOM value 或工具成功文本,也不修改组件库或用 evaluate_script 修补事件。其他版本按实际行为判断。该工具事件限制、宿主 PreToolUse 参数兼容、原型业务缺陷分别诊断,不以其中一项通过代替其他项。
|
|
41
41
|
|
|
42
|
-
`native-pilot.cjs browser` 的 Playwright 执行器保留原型专项回归,包括确定性能力、故障与隔离检查;它是补充证据,不能代替浏览器 Skill、MCP
|
|
42
|
+
`native-pilot.cjs browser` 的 Playwright 执行器保留原型专项回归,包括确定性能力、故障与隔离检查;它是补充证据,不能代替浏览器 Skill、MCP 调用或上述实际观测。新完整业务的 CodeBuddy/WorkBuddy 严格链必须满足 Skill、DevTools 和固定业务场景实测;Playwright 执行器仅适用于原设备列表固定合同,缺可调用 Skill 或 MCP 时明确 blocked/not-run,最终结果为 partial/blocked。只有补充回归通过时,不将整链写为 complete。
|
|
43
43
|
|
|
44
44
|
新 create 将 `browserReview` 要求固定到 inputs/acceptance.json;字段缺失的旧 v2 维持原行为,v1 不迁移。现有实例需明确接受完整验收输入变更后才启用,不在 inspect 中追随全局策略。其他宿主的等效真实浏览器渠道如实单列,不能借其结果宣称这条 CodeBuddy 指定链已通过。
|
|
45
45
|
|
|
@@ -36,7 +36,7 @@ paths 返回的 scope 是安装范围,scopeFile 是准备区 scope.json 的完
|
|
|
36
36
|
|
|
37
37
|
## scope.moduleIds 与 appliesTo
|
|
38
38
|
|
|
39
|
-
新原生 scope 使用 schemaVersion=
|
|
39
|
+
新原生 scope 使用 schemaVersion=3,moduleIds 为数组,例如 `["asset-management", "work-order-management"]`。标识须来自实际 PRD 或已确定项目模块:小写字母开头,使用字母、数字和单个连字符分隔,总长不超过 128;不把文档名、需求全文、代码路径或通配符当模块标识。模块身份不清楚时填 [],不编造标识。
|
|
40
40
|
|
|
41
41
|
需求的 source.reference 保留具体 PRD、版本与需求编号,statement 保留真实依据。多个 PRD 同模块去重;不同模块取本原型 scope 范围内并集。需求 ID 在本 scope 内唯一,文档冲突明确留作待确认项;模块关联不能代替需求覆盖或完成证明。
|
|
42
42
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Identification never treats absence of an IDE marker as proof of CLI identity.
|
|
3
|
+
const path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
|
|
4
|
+
const HOSTS=['codebuddy','workbuddy'];
|
|
5
|
+
function globalBase({host='codebuddy',home=os.homedir(),env=process.env}={}){
|
|
6
|
+
assert.ok(HOSTS.includes(host),'Unsupported Buddy host');
|
|
7
|
+
const key=host==='workbuddy'?'WORKBUDDY_CONFIG_DIR':'CODEBUDDY_CONFIG_DIR';
|
|
8
|
+
const base=env[key]?.trim()||path.join(home,`.${host}`);
|
|
9
|
+
assert.ok(path.isAbsolute(base)&&!/[\x00\r\n]/.test(base),`${key} must be an absolute safe directory`);
|
|
10
|
+
return path.normalize(base);
|
|
11
|
+
}
|
|
12
|
+
function identify(payload,record,{home=os.homedir(),env=process.env}={}){
|
|
13
|
+
const clients={CodeBuddyIDE:['codebuddy','ide'],CodeBuddyCLI:['codebuddy','cli'],CodeBuddyCode:['codebuddy','cli'],WorkBuddy:['workbuddy','ide'],WorkBuddyIDE:['workbuddy','ide'],WorkBuddyDesktop:['workbuddy','ide']};
|
|
14
|
+
const client=clients[payload.client];
|
|
15
|
+
const declared=typeof payload.host==='string'?payload.host.toLowerCase():null;
|
|
16
|
+
let found=client&&{host:client[0],product:client[1],identitySource:'native-client'};
|
|
17
|
+
if(found&&declared)assert.equal(declared,found.host,'Conflicting native host identity');
|
|
18
|
+
const candidates=[];
|
|
19
|
+
if(typeof payload.transcript_path==='string'&&path.isAbsolute(payload.transcript_path)){
|
|
20
|
+
const file=path.normalize(payload.transcript_path);
|
|
21
|
+
for(const host of HOSTS){
|
|
22
|
+
const parent=path.join(globalBase({host,home,env}),'projects')+path.sep;
|
|
23
|
+
if(file.startsWith(parent)&&file.endsWith('.jsonl'))candidates.push(host);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if(found&&candidates.length)assert.ok(candidates.includes(found.host),'Conflicting client and native host log location');
|
|
27
|
+
if(!found&&candidates.length){
|
|
28
|
+
const host=candidates.length===1?candidates[0]:env.WORKBUDDY_CONFIG_DIR&&candidates.includes('workbuddy')?'workbuddy':null;
|
|
29
|
+
assert.ok(host,'Conflicting native host log locations');
|
|
30
|
+
found={host,product:host==='codebuddy'?'cli':'ide',identitySource:'native-host-log-location'};
|
|
31
|
+
}
|
|
32
|
+
// A legacy installation retains its original, explicitly weaker contract.
|
|
33
|
+
if(!found&&record.schemaVersion===1&&['ide','cli'].includes(record.product))found={host:'codebuddy',product:record.product,identitySource:'legacy-profile'};
|
|
34
|
+
assert.ok(found,'Native host/product identity is unavailable; keep invocation unverified');
|
|
35
|
+
if(declared)assert.equal(declared,found.host,'Conflicting native host identity');
|
|
36
|
+
assert.ok((record.hosts||['codebuddy']).includes(found.host),'Native host is outside the registered scope');
|
|
37
|
+
return {...found,productVersion:payload.client_version||payload.product_version||(found.host==='workbuddy'?(env.WORKBUDDY_APP_VERSION||env.CLIENT_INFO_PRODUCT_VERSION):null)||(record.product==='auto'?null:record.productVersion)||'unreported'};
|
|
38
|
+
}
|
|
39
|
+
function isIDE(payload){return ['CodeBuddyIDE','WorkBuddy','WorkBuddyIDE','WorkBuddyDesktop'].includes(payload.client);}
|
|
40
|
+
module.exports={HOSTS,globalBase,identify,isIDE};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
|
|
3
|
-
const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs');
|
|
4
|
-
const FILES=['execution-policy.cjs','execution-evidence.cjs','codebuddy-ide-source.cjs','execution-gates.cjs','execution-assertions.cjs','execution-runtime.cjs','codebuddy-context.cjs','codebuddy-registration.cjs','codebuddy-hook.cjs','prototype-hooks.cjs','hosts/codebuddy.cjs','native-pilot.cjs','native-pilot-lib.cjs','native-delivery.cjs','native-devtools.cjs','native-files.cjs','native-build.cjs'];
|
|
3
|
+
const R=require('./codebuddy-registration.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
|
|
4
|
+
const FILES=['native-command.cjs','buddy-host.cjs','execution-recovery.cjs','execution-policy.cjs','execution-evidence.cjs','codebuddy-ide-source.cjs','execution-gates.cjs','execution-assertions.cjs','execution-runtime.cjs','codebuddy-context.cjs','codebuddy-registration.cjs','codebuddy-hook.cjs','prototype-hooks.cjs','hosts/codebuddy.cjs','native-pilot.cjs','native-pilot-lib.cjs','native-delivery.cjs','native-devtools.cjs','native-files.cjs','native-build.cjs'];
|
|
5
5
|
const read=file=>R.fileIO.read(file);
|
|
6
6
|
function scriptsForEntry(scriptPath) {
|
|
7
7
|
if(path.basename(scriptPath)!=='i3-prototype.cjs')return path.dirname(path.dirname(scriptPath));
|
|
@@ -21,37 +21,37 @@ function bundle(scriptPath) {
|
|
|
21
21
|
}
|
|
22
22
|
function registration(base) {
|
|
23
23
|
const raw=read(path.join(base,'state/i3-prototype/installation.json'));if(!raw)return null;
|
|
24
|
-
const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.
|
|
24
|
+
const record=JSON.parse(raw);assert.equal(record.owner,R.OWNER,'Unknown Hook ownership');assert.ok([1,2].includes(record.schemaVersion),'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');
|
|
25
|
+
if(record.schemaVersion===2)assert.ok(record.product==='auto'&&Array.isArray(record.hosts)&&record.hosts.length&&record.hosts.every(host=>H.HOSTS.includes(host)),'Invalid automatic Hook identity');
|
|
25
26
|
const settingsText=read(path.join(base,'settings.json')),settings=R.config(settingsText);
|
|
26
27
|
for(const event of R.EVENTS)assert.equal((settings.hooks?.[event]||[]).filter(group=>JSON.stringify(group)===JSON.stringify(record.groups[event])).length,1,'Registered Hook was removed, duplicated, or edited');
|
|
27
28
|
assert.equal(bundle(record.scriptPath),record.scriptDigest,'Audit adapter bytes changed; refresh owned registration');
|
|
28
29
|
assert.ok(fs.existsSync(record.nodePath),'Registered Node executable is missing');
|
|
29
30
|
return {...record,settingsDigest:E.sha(settingsText),registrationDigest:E.sha(JSON.stringify(record))};
|
|
30
31
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return path.normalize(configured);
|
|
36
|
-
}
|
|
37
|
-
function select({project,product,home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
|
|
38
|
-
const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({home,env});
|
|
32
|
+
const globalBase=H.globalBase;
|
|
33
|
+
function select({project,product,host='codebuddy',home=os.homedir(),env=process.env,readText=read,loadRegistration=registration}) {
|
|
34
|
+
assert.ok(['ide','cli'].includes(product),'Actual native product identity required');
|
|
35
|
+
const projectBase=path.join(project,'.codebuddy'),userBase=globalBase({host,home,env});
|
|
39
36
|
const projectText=readText(path.join(projectBase,'settings.json')),localText=readText(path.join(projectBase,'settings.local.json'));
|
|
40
37
|
if(localText){const local=R.config(localText);assert.ok(!local.hooks&&!local.disableAllHooks&&!local.allowManagedHooksOnly,'Local Hook overrides are not supported by this profile');}
|
|
41
38
|
const projectSettings=R.config(projectText);assert.ok(!projectSettings.disableAllHooks&&!projectSettings.allowManagedHooksOnly,'Project Hook policy prevents automatic auditing');
|
|
42
39
|
const globalText=readText(path.join(userBase,'settings.json')),globalSettings=R.config(globalText);assert.ok(!globalSettings.disableAllHooks&&!globalSettings.allowManagedHooksOnly,'Global Hook policy prevents automatic auditing');
|
|
43
40
|
// IDE documents project settings as an override. Do not silently fall back through a shadowing file.
|
|
44
|
-
const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
|
|
41
|
+
const bases=host==='codebuddy'&&product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
|
|
45
42
|
for(const base of [...new Set(bases)]) {
|
|
46
43
|
const record=loadRegistration(base);if(!record)continue;
|
|
47
|
-
|
|
44
|
+
if(record.product==='auto'){
|
|
45
|
+
if(!(record.hosts||[]).includes(host))continue;
|
|
46
|
+
}else {assert.equal(host,'codebuddy','Legacy profile is CodeBuddy only');assert.equal(record.product,product,'Conflicting CodeBuddy product profiles');}
|
|
47
|
+
return {...record,host,product};
|
|
48
48
|
}
|
|
49
49
|
return null;
|
|
50
50
|
}
|
|
51
51
|
function projectKey(project){return `${path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project'}-${E.sha(project).slice(0,16)}`;}
|
|
52
52
|
function sessionDirectory(record,project,sessionId) {
|
|
53
53
|
assert.ok(typeof sessionId==='string'&&sessionId,'Native session ID required');
|
|
54
|
-
return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(sessionId).slice(0,24));
|
|
54
|
+
return path.join(record.base,'state/i3-prototype',projectKey(project),E.sha(record.schemaVersion===2?`${record.host}:${record.product}:${sessionId}`:sessionId).slice(0,24));
|
|
55
55
|
}
|
|
56
56
|
function source(payload,record,project) {
|
|
57
57
|
assert.ok(typeof payload.transcript_path==='string'&&typeof payload.session_id==='string','Native transcript and session fields required');
|