@givolt/i3-composer-agent 2.3.2 → 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 +5 -1
- package/bin/agent-targets.json +4 -2
- package/bin/i3-composer-agent.cjs +48 -9
- package/bin/install.cjs +2 -1
- package/bin/native-layout.cjs +1 -1
- package/bin/tool-selector.cjs +24 -7
- package/bin/wizard.cjs +49 -15
- package/docs/codebuddy-setup.md +43 -4
- package/docs/codex-setup.md +3 -1
- package/docs/other-hosts.md +1 -1
- package/docs/ui-prototype-native/USAGE.md +4 -2
- package/docs/workbuddy-setup.md +20 -2
- package/hooks/codebuddy-upstream.cjs +93 -0
- package/hooks/i3-prototype.cjs +12 -0
- package/hooks/manage.cjs +170 -0
- package/package.json +35 -1
- package/plugin.json +1 -1
- package/scripts/prototype-cache-test.js +55 -0
- package/scripts/prototype-generation-entry-test.js +6 -0
- package/scripts/prototype-generation-evidence-test.js +10 -0
- package/scripts/prototype-generation-native-test.js +78 -0
- package/scripts/prototype-generation-test.js +99 -0
- package/scripts/prototype-inventory-test.js +43 -0
- package/scripts/prototype-locations-test.js +21 -0
- package/scripts/prototype-project-test.js +183 -0
- package/scripts/ui-prototype-buddy-lifecycle-test.js +89 -0
- package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
- package/scripts/ui-prototype-dependencies-test.js +13 -0
- package/scripts/ui-prototype-execution-evidence-test.js +62 -0
- package/scripts/ui-prototype-execution-gates-test.js +90 -0
- package/scripts/ui-prototype-execution-policy-test.js +53 -0
- package/scripts/ui-prototype-execution-runtime-test.js +27 -0
- package/scripts/ui-prototype-ide-source-test.js +73 -0
- package/scripts/ui-prototype-recovery-test.js +72 -0
- package/scripts/ui-prototype-registration-test.js +53 -0
- package/scripts/ui-prototype-transcript-auto-test.js +67 -0
- package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
- package/skills/i3-incremental-implementation/SKILL.md +1 -1
- package/skills/i3-ui-prototype/SKILL.md +29 -7
- package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +14 -3
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +6 -1
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +13 -0
- package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
- package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +17 -0
- package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +4 -2
- package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
- package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
- package/skills/i3-ui-prototype/assets/templates/gallery/index.html +1 -1
- package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
- package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
- package/skills/i3-ui-prototype/references/artifact-contract.md +1 -1
- package/skills/i3-ui-prototype/references/content-template.md +9 -4
- package/skills/i3-ui-prototype/references/entry-contract.md +1 -1
- package/skills/i3-ui-prototype/references/execution-evidence.md +83 -0
- package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
- package/skills/i3-ui-prototype/references/interaction-acceptance.md +5 -1
- package/skills/i3-ui-prototype/references/optional-skills.md +4 -0
- package/skills/i3-ui-prototype/references/project-layout.md +92 -0
- package/skills/i3-ui-prototype/references/template-adapters.md +6 -0
- package/skills/i3-ui-prototype/references/workflow-contract.md +4 -0
- package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
- package/skills/i3-ui-prototype/scripts/buddy-host.cjs +40 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +196 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
- package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +125 -0
- package/skills/i3-ui-prototype/scripts/content-template.cjs +2 -2
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +168 -0
- package/skills/i3-ui-prototype/scripts/execution-policy.cjs +62 -0
- package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +52 -0
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
- package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
- package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
- package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +20 -0
- package/skills/i3-ui-prototype/scripts/native-command.cjs +18 -0
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +17 -5
- package/skills/i3-ui-prototype/scripts/native-devtools.cjs +1 -0
- package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +64 -18
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +23 -3
- package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +28 -6
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
- package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +5 -7
- package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +5 -1
- package/skills/i3-ui-prototype/scripts/template-contract.cjs +11 -2
- package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const E = require(prototypeScripts+'/execution-evidence.cjs');
|
|
6
|
+
const use = (id, name, input) => ({ type: 'assistant', message: { role: 'assistant', content: [{ type: 'tool_use', id, name, input }] } });
|
|
7
|
+
const result = (id, content, is_error = false) => ({ type: 'user', message: { role: 'user', content: [{ type: 'tool_result', tool_use_id: id, content, is_error }] } });
|
|
8
|
+
const raw = rows => rows.map(row => JSON.stringify(row)).join('\n') + '\n';
|
|
9
|
+
test('paired native results distinguish loaded skills from attempts and failures', () => {
|
|
10
|
+
const stream = raw([use('f1', 'Skill', { skill: 'i3-frontend-ui-engineering' }), result('f1', 'Full skill instructions'),
|
|
11
|
+
use('f2', 'Skill', { skill: 'antdv' }), result('f2', 'Not available', true), use('f3', 'Skill', { skill: 'ui-ux-pro-max' })]);
|
|
12
|
+
const calls = E.parse(stream);
|
|
13
|
+
assert.equal(calls[0].skill, 'frontend'); assert.equal(calls[0].success, true);
|
|
14
|
+
assert.equal(calls[1].success, false); assert.equal(calls[2].success, false);
|
|
15
|
+
assert.equal(E.loaded(calls, 'frontend').length, 1); assert.equal(E.loaded(calls, 'antdv').length, 0);
|
|
16
|
+
});
|
|
17
|
+
test('Read, forged names, zero-exit errors and duplicate IDs do not become valid skill loads', () => {
|
|
18
|
+
const calls = E.parse(raw([use('r1', 'Read', { file_path: 'SKILL.md' }), result('r1', 'instructions'),
|
|
19
|
+
use('b1', 'Bash', { command: 'node antdv info' }), result('b1', JSON.stringify({ exitCode: 0, stdout: 'Error: missing knowledge' }))]));
|
|
20
|
+
assert.equal(E.loaded(calls, 'frontend').length, 0); assert.equal(calls[1].success, false);
|
|
21
|
+
assert.throws(() => E.parse(raw([use('a', 'Skill', { skill: 'antdv' }), use('a', 'Skill', { skill: 'antdv' })])), /Duplicate/);
|
|
22
|
+
assert.throws(() => E.parse(raw([result('unknown', 'success')])), /unmatched|Unmatched/);
|
|
23
|
+
});
|
|
24
|
+
test('evidence references must resolve to the actual response and retain original order', () => {
|
|
25
|
+
const calls = E.parse(raw([use('a', 'Skill', { skill: 'antdv' }), result('a', 'official skill'), use('b', 'Bash', { command: 'node cli info Drawer' }), result('b', 'Drawer API') ]));
|
|
26
|
+
assert.equal(E.reference(calls, { toolCallId: 'b', resultDigest: calls[1].resultDigest }).name, 'Bash');
|
|
27
|
+
assert.throws(() => E.reference(calls, { toolCallId: 'b', resultDigest: '0'.repeat(64) }), /digest/);
|
|
28
|
+
assert.throws(() => E.reference(calls, { toolCallId: 'not-real' }), /missing/);
|
|
29
|
+
assert.ok(calls[0].resultLine < calls[1].requestLine);
|
|
30
|
+
});
|
|
31
|
+
test('malformed streams and model-written audit JSON are not accepted as native transcripts', () => {
|
|
32
|
+
assert.throws(() => E.parse('{broken\n'), /JSON/);
|
|
33
|
+
assert.throws(() => E.parse(JSON.stringify({ skill: 'frontend', used: true, passed: true })), /native|Native/);
|
|
34
|
+
});
|
|
35
|
+
// Structure captured from a real CodeBuddy Code 2.169.0 installation smoke run.
|
|
36
|
+
// Skill instruction bodies are replaced; identities, pairing and status remain.
|
|
37
|
+
const modern = (id='native-1', status='completed') => [
|
|
38
|
+
{type:'message',role:'user',sessionId:'session-169',content:[{type:'input_text',text:'请验收安装并加载前端 Skill'}]},
|
|
39
|
+
{type:'function_call',name:'Skill',callId:id,arguments:'{"skill":"i3-frontend-ui-engineering"}',sessionId:'session-169',message:{usage:{}}},
|
|
40
|
+
{type:'function_call_result',name:'Skill',callId:id,status,output:{type:'text',text:'Full frontend Skill instructions'},sessionId:'session-169'},
|
|
41
|
+
{type:'message',role:'assistant',sessionId:'session-169',content:[{type:'output_text',text:'安装检查已完成'}]},
|
|
42
|
+
];
|
|
43
|
+
test('CodeBuddy 2.169 flat messages and function calls retain native ordering and session evidence', () => {
|
|
44
|
+
const text=raw(modern()),calls=E.parse(text),description=E.describe(text);
|
|
45
|
+
assert.equal(E.loaded(calls,'frontend').length,1);
|
|
46
|
+
assert.equal(calls[0].sessionId,'session-169');
|
|
47
|
+
assert.equal(calls[0].requestLine,2);assert.equal(calls[0].resultLine,3);
|
|
48
|
+
assert.equal(description.prompts[0].text,'请验收安装并加载前端 Skill');
|
|
49
|
+
assert.equal(description.latestAssistant,'安装检查已完成');
|
|
50
|
+
});
|
|
51
|
+
test('flat failed, pending, empty and Read results cannot satisfy a frontend Skill gate', () => {
|
|
52
|
+
for(const status of ['failed','pending','cancelled'])assert.equal(E.loaded(E.parse(raw(modern('a',status))),'frontend').length,0);
|
|
53
|
+
const empty=modern();empty[2].output.text='';assert.equal(E.loaded(E.parse(raw(empty)),'frontend').length,0);
|
|
54
|
+
const read=modern();read[1].name=read[2].name='Read';read[1].arguments='{"file_path":"SKILL.md"}';
|
|
55
|
+
assert.equal(E.loaded(E.parse(raw(read)),'frontend').length,0);
|
|
56
|
+
});
|
|
57
|
+
test('flat native calls reject mismatched identity, malformed arguments and cross-session results', () => {
|
|
58
|
+
const wrong=modern();wrong[2].name='Read';assert.throws(()=>E.parse(raw(wrong)),/name|identity/i);
|
|
59
|
+
const bad=modern();bad[1].arguments='{broken';assert.throws(()=>E.parse(raw(bad)),/argument|JSON/i);
|
|
60
|
+
const session=modern();session[2].sessionId='other';assert.throws(()=>E.parse(raw(session)),/session/i);
|
|
61
|
+
const duplicate=modern();duplicate.splice(2,0,duplicate[1]);assert.throws(()=>E.parse(raw(duplicate)),/Duplicate/);
|
|
62
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test=require('node:test'),assert=require('node:assert/strict');
|
|
4
|
+
const P=require(prototypeScripts+'/execution-policy.cjs');
|
|
5
|
+
const E=require(prototypeScripts+'/execution-evidence.cjs');
|
|
6
|
+
const G=require(prototypeScripts+'/execution-gates.cjs');
|
|
7
|
+
test('native UI writes resolve relative paths from the actual tool cwd',()=>{
|
|
8
|
+
assert.equal(G.uiWrite({name:'Write',cwd:'/work',input:{file_path:'prototypes/orders/app/src/Probe.vue'}},'/work/prototypes/orders'),true);
|
|
9
|
+
assert.equal(G.uiWrite({name:'Write',cwd:'/work/prototypes/orders/app',input:{file_path:'src/Probe.vue'}},'/work/prototypes/orders'),true);
|
|
10
|
+
assert.equal(G.uiWrite({name:'Write',cwd:'/work',input:{file_path:'other/app/src/Probe.vue'}},'/work/prototypes/orders'),false);
|
|
11
|
+
assert.equal(G.uiWrite({name:'Write',input:{filePath:'/work/prototypes/orders/app/src/Probe.vue'}},'/work/prototypes/orders'),true);
|
|
12
|
+
});
|
|
13
|
+
function fixture() {
|
|
14
|
+
const file='/work/prototypes/orders/app/src/Orders.vue',bytes='<Table />';
|
|
15
|
+
const call=(toolCallId,name,input,result,line,skill)=>({toolCallId,name,input,result,requestLine:line,resultLine:line+1,success:true,resultDigest:E.sha(result),sessionId:'s1',contextLine:0,skill});
|
|
16
|
+
const calls=[call('front','Skill',{},'frontend instructions',3,'frontend'),call('ant','Skill',{},'antdv instructions',5,'antdv'),call('query','Bash',{command:'antdv info Table'},'Table API: rowSelection',7),call('version','Read',{file_path:'/work/app/node_modules/antdv-next/package.json'},'{"version":"1.5.3"}',9),call('write','Write',{file_path:file},'saved',11)];
|
|
17
|
+
const policy=P.create({mode:'template'}),current={digest:'candidate',inputDigest:'input',revision:'r001'};
|
|
18
|
+
const binding={schemaVersion:1,runId:'r1',sessionId:'s1',projectKey:'p1',directory:'/work/prototypes/orders',instanceId:'instance',policyDigest:policy.digest,inputDigest:'input',entryLine:1};
|
|
19
|
+
const proof={schemaVersion:1,runId:'r1',sessionId:'s1',projectKey:'p1',policyDigest:policy.digest,candidateDigest:'candidate',inputDigest:'input',revision:'r001',frontend:{toolCallId:'front',files:[{path:'app/src/Orders.vue',sha256:E.sha(bytes)}],requirementIds:['R1']},official:{skill:{toolCallId:'ant'},version:{toolCallId:'version',quote:'"version":"1.5.3"'},queries:[{component:'Table',toolCallId:'query',quote:'Table API: rowSelection'}]},templateChoice:{quote:'template mode'}};
|
|
20
|
+
return {policy,current,binding,proof,source:{calls,contextLine:0,prompts:[{line:1,text:'generate prototype in template mode',digest:'prompt'}]},scope:{requirements:[{id:'R1'}]},files:{'app/src/Orders.vue':bytes},implementation:{bindings:[{status:'implemented',components:['Table'],files:[{path:'app/src/Orders.vue'}]}]},frameworkVersion:'1.5.3'};
|
|
21
|
+
}
|
|
22
|
+
test('a native frontend load is required before the first actual UI write in both modes',()=>{
|
|
23
|
+
const f=fixture();assert.equal(G.evaluate(f,'implementation').status,'passed');
|
|
24
|
+
f.source.calls[0].success=false;assert.equal(G.evaluate(f,'implementation').status,'failed');
|
|
25
|
+
f.source.calls[0].success=true;f.source.calls[0].resultLine=12;assert.match(G.evaluate(f,'implementation').errors.join(),/before/);
|
|
26
|
+
});
|
|
27
|
+
test('IDE relative write evidence uses its bound workspace while legacy evidence keeps its contract',()=>{
|
|
28
|
+
const f=fixture();f.source.format='codebuddy-ide-index-v1';f.binding.project='/work';f.source.calls.find(c=>c.name==='Write').input.file_path='prototypes/orders/app/src/Orders.vue';
|
|
29
|
+
assert.equal(G.evaluate(f,'implementation').status,'passed');
|
|
30
|
+
});
|
|
31
|
+
test('current context, task and artifact identities reject replay and changed inputs',()=>{
|
|
32
|
+
for(const mutate of [f=>f.proof.runId='other',f=>f.proof.inputDigest='old',f=>f.source.contextLine=10,f=>f.source.calls[0].sessionId='s2',f=>f.binding.entryLine=20]){const f=fixture();mutate(f);assert.equal(G.evaluate(f,'implementation').status,'failed');}
|
|
33
|
+
});
|
|
34
|
+
test('reading a skill, adapted metadata, or incomplete review mappings cannot pass',()=>{
|
|
35
|
+
const f=fixture();f.source.calls[0].skill=null;assert.equal(G.evaluate(f,'implementation').status,'failed');
|
|
36
|
+
const g=fixture();g.proof.frontend.files=[];assert.equal(G.evaluate(g,'implementation').status,'failed');
|
|
37
|
+
const h=fixture();h.proof.frontend.requirementIds=[];assert.equal(G.evaluate(h,'implementation').status,'failed');
|
|
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
|
+
});
|
|
63
|
+
test('official query references must be successful, relevant, and version checked',()=>{
|
|
64
|
+
const f=fixture();assert.equal(G.evaluate(f,'design').status,'passed');
|
|
65
|
+
f.proof.official.queries[0].quote='invented';assert.equal(G.evaluate(f,'design').status,'failed');
|
|
66
|
+
const g=fixture();g.source.calls[2].input.command='antdv probe';assert.equal(G.evaluate(g,'design').status,'failed');
|
|
67
|
+
const h=fixture();h.source.calls[3].result='{"version":"1.5.2"}';assert.equal(G.evaluate(h,'design').status,'failed');
|
|
68
|
+
});
|
|
69
|
+
test('professional requires actual UIUX execution; explicit template choice cannot be invented',()=>{
|
|
70
|
+
const f=fixture();f.policy=P.create({mode:'professional'});f.binding.policyDigest=f.proof.policyDigest=f.policy.digest;assert.match(G.evaluate(f,'design').errors.join(),/UIUX/);
|
|
71
|
+
const g=fixture();g.proof.templateChoice.quote='use template now';assert.equal(G.evaluate(g,'design').status,'failed');
|
|
72
|
+
const h=fixture();h.proof.templateChoice.quote='template';h.source.prompts[0].text='参考 template 制作专业增强原型';assert.equal(G.evaluate(h,'design').status,'failed');
|
|
73
|
+
});
|
|
74
|
+
test('missing current build, fake browser IDs, and self-written passed booleans cannot finish',()=>{
|
|
75
|
+
const f=fixture();f.proof.build={toolCallId:'query'};assert.equal(G.evaluate(f,'build').status,'failed');
|
|
76
|
+
f.proof.browser={passed:true};assert.equal(G.evaluate(f,'browser').status,'failed');assert.equal(G.evaluate(f,'delivery').status,'failed');
|
|
77
|
+
});
|
|
78
|
+
test('the shared gate can correlate a complete synthetic chain and rejects unrelated native facts',()=>{
|
|
79
|
+
const f=fixture(),call=(toolCallId,name,input,result,line,skill)=>({toolCallId,name,input,result,requestLine:line,resultLine:line+1,success:true,resultDigest:E.sha(result),sessionId:'s1',contextLine:0,skill});
|
|
80
|
+
f.build={buildId:'b1',buildDigest:'bytes',candidateDigest:'candidate'};
|
|
81
|
+
f.source.calls.push(call('build','Bash',{command:'node /skills/native-pilot.cjs build /work/prototypes/orders'},'{"buildId":"b1","buildDigest":"bytes"}',15),call('browser','Skill',{},'browser instructions',17,'browser'),call('preview','mcp__chrome_devtools__navigate_page',{url:'http://127.0.0.1:4173/'},'opened',19),call('fact','mcp__chrome_devtools__evaluate_script',{function:'() => document.title'},'{"candidate":"candidate","saved":true}',21));
|
|
82
|
+
f.proof.build={toolCallId:'build'};f.business={previewBaseURL:'http://127.0.0.1:4173/',journeys:[{id:'save'}]};
|
|
83
|
+
const methods={dom:'evaluate_script',screenshot:'take_screenshot',console:'list_console_messages',network:'list_network_requests',accessibility:'take_snapshot',performance:'performance_stop_trace'};
|
|
84
|
+
const refs=Object.fromEntries(Object.entries(methods).map(([kind,method],i)=>{const id=kind==='dom'?'fact':kind;if(kind!=='dom')f.source.calls.push(call(id,`mcp__chrome_devtools__${method}`,{},'synthetic observation',24+i*2));return [kind,{toolCallId:id,quote:kind==='dom'?'candidate':'synthetic observation'}];}));
|
|
85
|
+
f.devtools={skill:{toolCallId:'browser'},mcp:{calls:Object.entries(methods).map(([kind,method])=>({toolCallId:refs[kind].toolCallId,tool:`mcp__chrome_devtools__${method}`}))}};
|
|
86
|
+
f.proof.browser={skill:{toolCallId:'browser'},preview:{toolCallId:'preview'},identity:{toolCallId:'fact',quote:'candidate'},observations:refs,facts:[{scenarioId:'save',checkIndex:0,toolCallId:'fact',quote:'"saved":true'}]};
|
|
87
|
+
f.assertions={schemaVersion:1,scenarios:[{scenarioId:'save',checks:[{method:'evaluate_script',pointer:'/saved',operator:'equals',expected:true}]}]};
|
|
88
|
+
assert.equal(G.evaluate(f,'delivery').status,'passed');
|
|
89
|
+
f.source.calls.find(c=>c.toolCallId==='fact').result='{"candidate":"candidate","saved":false}';f.proof.browser.facts[0].quote='"saved":false';assert.equal(G.evaluate(f,'delivery').status,'failed');
|
|
90
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const P = require(prototypeScripts+'/execution-policy.cjs');
|
|
6
|
+
test('CodeBuddy policy defaults never confuse a PRD scope file with project/global installation scope',()=>{
|
|
7
|
+
const options={host:'codebuddy',scope:'/work/prototypes/.preparation/orders/scope.json'};
|
|
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.AUTO_ID);
|
|
10
|
+
assert.equal(P.creationOptions({host:'codex'},{name:'i3-composer-agent'}).executionPolicy,undefined);
|
|
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/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('analysis and inspection never acquire implementation requirements', () => {
|
|
24
|
+
for (const operation of ['analyze', 'inspect', 'confirm-baseline']) {
|
|
25
|
+
const policy = P.create({ operation });
|
|
26
|
+
assert.deepEqual(P.requirements(policy, 'implementation-entry'), []);
|
|
27
|
+
assert.equal(policy.readOnly, true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
test('both generation modes require frontend loading before implementation', () => {
|
|
31
|
+
for (const operation of ['create', 'revise']) for (const mode of ['professional', 'template']) {
|
|
32
|
+
const policy = P.create({ operation, mode });
|
|
33
|
+
assert.ok(P.requirements(policy, 'implementation-entry').includes('frontend'));
|
|
34
|
+
assert.ok(P.requirements(policy, 'browser-entry').includes('browser'));
|
|
35
|
+
assert.equal(P.requirements(policy, 'design').includes('uiux'), mode === 'professional');
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
test('official framework fallback is explicit and cannot invent a native invocation', () => {
|
|
39
|
+
assert.ok(P.requirements(P.create({ officialCallable: true }), 'design').includes('antdv'));
|
|
40
|
+
assert.ok(P.requirements(P.create({ officialCallable: false }), 'design').includes('official-source'));
|
|
41
|
+
assert.equal(P.skillKey('fake-i3-frontend-ui-engineering'), null);
|
|
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);
|
|
45
|
+
});
|
|
46
|
+
test('policy changes are detected and unsupported hosts cannot silently acquire CodeBuddy support', () => {
|
|
47
|
+
const policy = P.create({ mode: 'template' });
|
|
48
|
+
assert.equal(P.validate(policy).digest, policy.digest);
|
|
49
|
+
assert.throws(() => P.validate({ ...policy, mode: 'professional' }), /policy|Policy/);
|
|
50
|
+
assert.throws(() => P.create({ host: 'codex' }), /CodeBuddy/);
|
|
51
|
+
assert.throws(() => P.create({ operation: 'write-anything' }), /operation/);
|
|
52
|
+
assert.throws(() => P.create({ mode: 'basic' }), /mode/);
|
|
53
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),os=require('node:os');
|
|
4
|
+
const lib=require(prototypeScripts+'/native-pilot-lib.cjs'),X=require(prototypeScripts+'/execution-runtime.cjs'),A=require(prototypeScripts+'/execution-assertions.cjs');
|
|
5
|
+
function fixture(t){const project=fs.mkdtempSync(path.join(os.tmpdir(),'i3-execution-source-fixture-'));t.after(()=>fs.rmSync(project,{recursive:true,force:true}));const scope={schemaVersion:1,title:'Synthetic orders fixture',pages:[{id:'orders',title:'Orders',referencePage:'models-page',regions:[{id:'list',mode:'management-list'},{id:'editor',mode:'edit-form'}]}],requirements:[{id:'R1',text:'Synthetic source contract only',pages:['orders'],source:{type:'prd',reference:'fixture/R1',statement:'Synthetic fixture, not actual browser acceptance'}}],exemptions:[]};const scopeFile=path.join(project,'scope.json');fs.writeFileSync(scopeFile,JSON.stringify(scope));return {project,scope:scopeFile,expertMode:'template'};}
|
|
6
|
+
test('new execution inputs freeze complete scenario expectations without changing legacy candidates',t=>{
|
|
7
|
+
const options=fixture(t),legacy=lib.createPrototype({...options,id:'old'}),before=lib.manifest(legacy.directory);
|
|
8
|
+
assert.equal(X.policy(legacy.directory),null);assert.equal(X.check(legacy.directory,'delivery',lib).status,'not-required');
|
|
9
|
+
const assertions=A.template(options);assert.ok(assertions.scenarios.length>20);for(const scenario of assertions.scenarios)scenario.checks=[{method:'evaluate_script',pointer:'/synthetic',operator:'equals',expected:true}];
|
|
10
|
+
const file=path.join(options.project,'assertions.json');fs.writeFileSync(file,JSON.stringify(assertions));
|
|
11
|
+
const fresh=lib.createPrototype({...options,id:'strict',host:'codebuddy',executionPolicy:'codebuddy-ab-v1',executionAssertions:file});
|
|
12
|
+
assert.equal(X.policy(fresh.directory).id,'codebuddy-ab-v1');assert.equal(lib.inspect(fresh.directory).status,'passed');
|
|
13
|
+
assert.equal(X.check(fresh.directory,'delivery',lib).status,'failed');assert.throws(()=>X.requireStage(fresh.directory,'implementation',lib),/binding|execution-begin/);
|
|
14
|
+
assert.deepEqual(lib.manifest(legacy.directory),before);assert.deepEqual(fs.readdirSync(options.project).sort(),['assertions.json','prototypes','scope.json']);
|
|
15
|
+
});
|
|
16
|
+
test('missing or incomplete fixed facts cannot silently create a strict artifact or affect legacy state',t=>{
|
|
17
|
+
const options=fixture(t);assert.throws(()=>lib.createPrototype({...options,id:'missing',host:'codebuddy',executionPolicy:'codebuddy-ab-v1'}),/execution-assertions/);
|
|
18
|
+
assert.equal(fs.existsSync(path.join(options.project,'prototypes/missing')),false);
|
|
19
|
+
const assertions=A.template(options);assert.throws(()=>A.validate(assertions,assertions.scenarios.map(s=>s.scenarioId)),/actual result/);
|
|
20
|
+
});
|
|
21
|
+
test('native values are extracted and compared with fixed inputs instead of a self-written passed flag',()=>{
|
|
22
|
+
const check={method:'evaluate_script',pointer:'/form/errors',operator:'equals',expected:1};
|
|
23
|
+
A.compare(A.extract(A.resultData('### Result\n```json\n{"form":{"errors":1}}\n```'),check.pointer),check);
|
|
24
|
+
assert.throws(()=>A.compare(A.extract(A.resultData('{"passed":true,"form":{"errors":0}}'),check.pointer),check),/differs/);
|
|
25
|
+
assert.throws(()=>A.extract({passed:true},check.pointer),/absent/);
|
|
26
|
+
assert.throws(()=>A.resultData('passed'),/JSON/);
|
|
27
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),os=require('node:os');
|
|
4
|
+
const E=require(prototypeScripts+'/execution-evidence.cjs'),C=require(prototypeScripts+'/codebuddy-context.cjs');
|
|
5
|
+
function fixture(t){
|
|
6
|
+
const root=fs.mkdtempSync(path.join(os.tmpdir(),'i3-ide-index-'));t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
|
|
7
|
+
const sessionId='a'.repeat(32),requestId='b'.repeat(32),directory=path.join(root,sessionId);fs.mkdirSync(path.join(directory,'messages'),{recursive:true});
|
|
8
|
+
const rows=[
|
|
9
|
+
{id:'1'.repeat(32),role:'user',message:JSON.stringify({role:'user',content:[{type:'text',text:'injected context'}]}),extra:JSON.stringify({requestId,isHelperMessage:false,sourceContentBlocks:[{type:'text',text:'请实现原型'}]})},
|
|
10
|
+
{id:'2'.repeat(32),role:'assistant',message:JSON.stringify({role:'assistant',content:[{type:'tool-call',toolCallId:'native-skill',toolName:'use_skill',args:{command:'i3-frontend-ui-engineering'}}]}),extra:JSON.stringify({requestId})},
|
|
11
|
+
{id:'3'.repeat(32),role:'tool',message:JSON.stringify({role:'tool',content:[{type:'tool-result',toolCallId:'native-skill',toolName:'use_skill',result:{status:'success',success:true,result:{message:'Actual Skill instructions'}},isError:false}]}),extra:JSON.stringify({requestId})},
|
|
12
|
+
{id:'4'.repeat(32),role:'assistant',message:JSON.stringify({role:'assistant',content:[{type:'text',text:'已完成'}]}),extra:JSON.stringify({requestId})},
|
|
13
|
+
];
|
|
14
|
+
const index={messages:rows.map(row=>({id:row.id,role:row.role,type:'text',isComplete:false})),requests:[{id:requestId,type:'craft',messages:rows.map(row=>row.id),state:'running'}]};
|
|
15
|
+
const file=path.join(directory,'index.json');
|
|
16
|
+
const save=()=>{fs.writeFileSync(file,JSON.stringify(index));for(const row of rows)fs.writeFileSync(path.join(directory,'messages',row.id+'.json'),JSON.stringify(row));};save();
|
|
17
|
+
return {file,root,sessionId,rows,index,save,read:()=>E.readSource(file,[root])};
|
|
18
|
+
}
|
|
19
|
+
test('observed IDE index resolves only referenced messages and pairs a real use_skill result',t=>{
|
|
20
|
+
const f=fixture(t),s=f.read();assert.equal(s.format,'codebuddy-ide-index-v1');
|
|
21
|
+
assert.equal(E.loaded(s.calls,'frontend').length,1);assert.equal(s.calls[0].sessionId,f.sessionId);
|
|
22
|
+
assert.equal(s.calls[0].name,'Skill');assert.deepEqual(s.calls[0].input,{command:'i3-frontend-ui-engineering'});
|
|
23
|
+
assert.equal(s.prompts[0].text,'请实现原型');assert.equal(s.latestAssistant,'已完成');
|
|
24
|
+
fs.writeFileSync(path.join(path.dirname(f.file),'messages','unreferenced.json'),'{broken');assert.equal(E.loaded(f.read().calls,'frontend').length,1);
|
|
25
|
+
});
|
|
26
|
+
test('IDE stable evidence cursor survives streaming text and state updates but detects changed facts',t=>{
|
|
27
|
+
const f=fixture(t),s=f.read();f.index.requests[0].state='completed';
|
|
28
|
+
f.rows[3].message=JSON.stringify({role:'assistant',content:[{type:'text',text:'已完成验收'}]});f.save();
|
|
29
|
+
C.prefix(f.file,s.bytes,s.digest);
|
|
30
|
+
const message=JSON.parse(f.rows[2].message);message.content[0].result.result.message='edited';f.rows[2].message=JSON.stringify(message);f.save();
|
|
31
|
+
assert.throws(()=>C.prefix(f.file,s.bytes,s.digest),/prefix changed|truncated/);
|
|
32
|
+
});
|
|
33
|
+
test('IDE failed use_skill and successful reads do not become frontend invocation evidence',t=>{
|
|
34
|
+
const f=fixture(t),message=JSON.parse(f.rows[2].message);message.content[0].result.success=false;f.rows[2].message=JSON.stringify(message);f.save();
|
|
35
|
+
assert.equal(E.loaded(f.read().calls,'frontend').length,0);
|
|
36
|
+
const g=fixture(t);for(const i of [1,2]){const row=JSON.parse(g.rows[i].message);row.content[0].toolName='read_file';g.rows[i].message=JSON.stringify(row);}g.save();
|
|
37
|
+
assert.equal(E.loaded(g.read().calls,'frontend').length,0);
|
|
38
|
+
});
|
|
39
|
+
test('IDE index traversal, missing files, duplicate references and wrong request identity fail closed',t=>{
|
|
40
|
+
const traversal=fixture(t);traversal.index.messages[0].id='../outside';traversal.save();assert.throws(()=>traversal.read(),/ID/);
|
|
41
|
+
const missing=fixture(t);fs.unlinkSync(path.join(path.dirname(missing.file),'messages',missing.rows[1].id+'.json'));assert.throws(()=>missing.read(),/ENOENT/);
|
|
42
|
+
const duplicate=fixture(t);duplicate.index.messages.push(duplicate.index.messages[0]);duplicate.save();assert.throws(()=>duplicate.read(),/Duplicate/);
|
|
43
|
+
const wrong=fixture(t);wrong.rows[1].extra=JSON.stringify({requestId:'f'.repeat(32)});wrong.save();assert.throws(()=>wrong.read(),/request identity/);
|
|
44
|
+
});
|
|
45
|
+
test('IDE symlinked message files and session mismatch cannot satisfy source verification',t=>{
|
|
46
|
+
const f=fixture(t),file=path.join(path.dirname(f.file),'messages',f.rows[1].id+'.json'),bytes=fs.readFileSync(file),other=path.join(f.root,'other.json');
|
|
47
|
+
fs.writeFileSync(other,bytes);fs.unlinkSync(file);fs.symlinkSync(other,file);assert.throws(()=>f.read(),/history file/);
|
|
48
|
+
const g=fixture(t);assert.throws(()=>C.source({transcript_path:g.file,session_id:'different'},{transcriptRoots:[g.root]},'/business'),/session identity/);
|
|
49
|
+
});
|
|
50
|
+
test('pending IDE argument serialization is excluded from the immutable completed prefix',t=>{
|
|
51
|
+
const f=fixture(t),result=f.rows.splice(2,1)[0];
|
|
52
|
+
const changeIndex=()=>{f.index.messages=f.rows.map(row=>({id:row.id,role:row.role,type:'text',isComplete:false}));f.index.requests[0].messages=f.rows.map(row=>row.id);f.save();};
|
|
53
|
+
f.rows[1].message=JSON.stringify({role:'assistant',content:[{type:'tool-call',toolCallId:'native-skill',toolName:'use_skill',args:{command:'i3-front'}}]});changeIndex();
|
|
54
|
+
const pending=f.read();assert.equal(pending.stableThroughLine,1);assert.equal(E.loaded(pending.calls,'frontend').length,0);
|
|
55
|
+
assert.equal(pending.calls[0].nativeRequestId,'b'.repeat(32));
|
|
56
|
+
f.rows[1].message=JSON.stringify({role:'assistant',content:[{type:'tool-call',toolCallId:'native-skill',toolName:'use_skill',args:{command:'i3-frontend-ui-engineering'}}]});f.rows.splice(2,0,result);changeIndex();
|
|
57
|
+
C.prefix(f.file,pending.bytes,pending.digest);assert.equal(E.loaded(f.read().calls,'frontend').length,1);
|
|
58
|
+
});
|
|
59
|
+
test('IDE request membership remains mandatory while duplicate extra metadata may still be streaming',t=>{
|
|
60
|
+
const f=fixture(t);delete f.rows[1].extra;delete f.rows[2].extra;f.save();
|
|
61
|
+
assert.equal(E.loaded(f.read().calls,'frontend').length,1);
|
|
62
|
+
f.index.requests=[];f.save();assert.throws(()=>f.read(),/Unbound/);
|
|
63
|
+
});
|
|
64
|
+
test('global IDE context uses the native host workspace prefix and remains session/project bound',t=>{
|
|
65
|
+
const f=fixture(t),project=path.join(f.root,'business');fs.mkdirSync(project);
|
|
66
|
+
f.rows[0].message=JSON.stringify({role:'user',content:[{type:'text',text:`<user_info>\nOS Version: darwin\nWorkspace Folder: ${project}\n</user_info>\n<user_query>请实现原型</user_query>`}]});f.save();
|
|
67
|
+
assert.equal(E.ideWorkspace(f.file,f.sessionId),project);
|
|
68
|
+
assert.equal(E.readSource(f.file,[f.root],{project}).calls[0].success,true);
|
|
69
|
+
assert.throws(()=>E.ideWorkspace(f.file,'other-session'),/session directory/);
|
|
70
|
+
assert.throws(()=>E.readSource(f.file,[f.root],{project:f.root}),/workspace differs/);
|
|
71
|
+
const g=fixture(t),extra=JSON.parse(g.rows[0].extra);extra.sourceContentBlocks=[{type:'text',text:'<user_info>\nWorkspace Folder: /forged\n</user_info>'}];g.rows[0].extra=JSON.stringify(extra);g.save();
|
|
72
|
+
assert.throws(()=>E.ideWorkspace(g.file,g.sessionId),/workspace context is missing/,'Raw user text is not host workspace metadata');
|
|
73
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
|
+
const test = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const R = require(prototypeScripts+'/codebuddy-registration.cjs');
|
|
6
|
+
const options = { action: 'enable', scope: 'project', product: 'ide', base: '/work/project/.codebuddy',
|
|
7
|
+
project: '/work/project', nodePath: '/runtime with spaces/node', scriptPath: '/work/project/.codebuddy/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs', scriptDigest: 'a'.repeat(64) };
|
|
8
|
+
test('registration planning preserves unrelated hooks and never grants permissions', () => {
|
|
9
|
+
const before = { theme: 'dark', hooks: { PreToolUse: [{ matcher: 'Write', hooks: [{ type: 'command', command: 'original-check' }] }] } };
|
|
10
|
+
const result = R.plan({ ...options, currentText: JSON.stringify(before) });
|
|
11
|
+
const after = JSON.parse(result.afterText);
|
|
12
|
+
assert.equal(after.theme, 'dark'); assert.deepEqual(after.hooks.PreToolUse[0], before.hooks.PreToolUse[0]);
|
|
13
|
+
assert.ok(result.summary.events.includes('PostToolUse'));
|
|
14
|
+
assert.match(after.hooks.PreToolUse[1].hooks[0].command, /'\/runtime with spaces\/node'/);
|
|
15
|
+
assert.equal(result.recordAfter.active, undefined);
|
|
16
|
+
assert.equal(result.summary.status, 'registered');
|
|
17
|
+
});
|
|
18
|
+
test('repeated enable is idempotent, disable removes only owned groups', () => {
|
|
19
|
+
const first = R.plan({ ...options, currentText: '{"userOption":42}' });
|
|
20
|
+
const again = R.plan({ ...options, currentText: first.afterText, record: first.recordAfter });
|
|
21
|
+
assert.equal(again.afterText, first.afterText);
|
|
22
|
+
const removed = R.plan({ ...options, action: 'disable', currentText: again.afterText, record: again.recordAfter });
|
|
23
|
+
assert.equal(JSON.parse(removed.afterText).userOption, 42);
|
|
24
|
+
assert.equal(Object.values(JSON.parse(removed.afterText).hooks).flat().length, 0);
|
|
25
|
+
});
|
|
26
|
+
test('missing ownership and modified owned hooks cannot be force adopted', () => {
|
|
27
|
+
const first = R.plan({ ...options, currentText: '{}' });
|
|
28
|
+
assert.throws(() => R.plan({ ...options, currentText: first.afterText }), /ownership|Ownership/);
|
|
29
|
+
const edited = JSON.parse(first.afterText); edited.hooks.Stop[0].hooks[0].timeout = 9;
|
|
30
|
+
assert.throws(() => R.plan({ ...options, currentText: JSON.stringify(edited), record: first.recordAfter }), /modified|changed/);
|
|
31
|
+
assert.throws(() => R.plan({ ...options, currentText: '{broken' }), /JSON/);
|
|
32
|
+
});
|
|
33
|
+
test('global CLI registration and defaults remain separate from actual activation', () => {
|
|
34
|
+
const keep = R.plan({ ...options, action: 'keep', currentText: null });
|
|
35
|
+
assert.equal(keep.afterText, null); assert.equal(keep.summary.status, 'not-enabled');
|
|
36
|
+
const global = R.plan({ ...options, scope: 'global', base: '/user/.codebuddy', product: 'cli', currentText: '{}' });
|
|
37
|
+
assert.equal(global.summary.status, 'pending-host-review');
|
|
38
|
+
assert.match(global.recordAfter.groups.Stop.hooks[0].command, /--scope' 'global/);
|
|
39
|
+
assert.throws(() => R.plan({ ...options, product: 'workbuddy' }), /product/);
|
|
40
|
+
});
|
|
41
|
+
test('configuration transaction refuses stale previews and restores only its own failed write in memory', () => {
|
|
42
|
+
const change = R.plan({ ...options, currentText: '{}' });
|
|
43
|
+
const files = new Map([[change.settingsPath, '{}']]); let fail = true;
|
|
44
|
+
const io = { read: file => files.get(file) ?? null, write(file, text) {
|
|
45
|
+
if (file === change.recordPath && fail) { fail = false; throw new Error('record write failed'); }
|
|
46
|
+
if (text === null) files.delete(file); else files.set(file, text);
|
|
47
|
+
} };
|
|
48
|
+
assert.throws(() => R.apply(change, io), /record write failed/);
|
|
49
|
+
assert.equal(files.get(change.settingsPath), '{}');
|
|
50
|
+
files.set(change.settingsPath, '{"userEdit":true}');
|
|
51
|
+
assert.throws(() => R.apply(change, io), /changed after preview/);
|
|
52
|
+
assert.equal(files.get(change.settingsPath), '{"userEdit":true}');
|
|
53
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Synthetic files and in-memory configuration only; never installs or launches a host.
|
|
3
|
+
const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),test=require('node:test'),assert=require('node:assert/strict');
|
|
4
|
+
const prototypeScripts=(()=>{const file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
5
|
+
const E=require(prototypeScripts+'/execution-evidence.cjs'),C=require(prototypeScripts+'/codebuddy-context.cjs'),R=require(prototypeScripts+'/codebuddy-registration.cjs');
|
|
6
|
+
function fixture(t) {
|
|
7
|
+
const root=fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(),'i3-transcript-contract-'))),project=path.join(root,'business'),native=path.join(root,'native');
|
|
8
|
+
fs.mkdirSync(project);fs.mkdirSync(native);t.after(()=>fs.rmSync(root,{recursive:true,force:true}));
|
|
9
|
+
const rows=[{type:'user',sessionId:'session-a',message:{content:'生成当前原型'}},{type:'assistant',sessionId:'session-a',message:{content:[{type:'tool_use',id:'front-1',name:'Skill',input:{skill:'i3-frontend-ui-engineering'}}]}},{type:'user',sessionId:'session-a',message:{content:[{type:'tool_result',tool_use_id:'front-1',content:'Native fixture instructions'}]}}];
|
|
10
|
+
const file=path.join(native,'session.txt');fs.writeFileSync(file,rows.map(JSON.stringify).join('\n')+'\n');return {root,project,native,file,rows};
|
|
11
|
+
}
|
|
12
|
+
test('new IDE and CLI registrations default to automatic transcripts in both scopes',()=>{
|
|
13
|
+
for(const product of ['ide','cli'])for(const scope of ['project','global']) {
|
|
14
|
+
const base=scope==='project'?'/work/p/.codebuddy':'/users/u/.codebuddy';
|
|
15
|
+
const change=R.plan({action:'enable',product,scope,base,project:'/work/p',nodePath:'/node',scriptPath:base+'/hooks/i3-prototype.cjs',scriptDigest:'a'.repeat(64)});
|
|
16
|
+
assert.equal(change.recordAfter.transcriptMode,'auto');assert.deepEqual(change.recordAfter.transcriptRoots,[]);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
test('manual and legacy root restrictions are retained; empty manual policy is rejected',()=>{
|
|
20
|
+
const options={action:'enable',product:'ide',scope:'project',base:'/work/p/.codebuddy',project:'/work/p',nodePath:'/node',scriptPath:'/fixed/hook.cjs',scriptDigest:'a'.repeat(64),transcriptRoots:['/native/logs']};
|
|
21
|
+
const first=R.plan(options);assert.equal(first.recordAfter.transcriptMode,'manual');
|
|
22
|
+
const legacy=structuredClone(first.recordAfter);delete legacy.transcriptMode;
|
|
23
|
+
const kept=R.plan({...options,action:'keep',transcriptRoots:undefined,currentText:first.afterText,record:legacy});
|
|
24
|
+
assert.equal(kept.recordAfter.transcriptMode,'manual');assert.deepEqual(kept.recordAfter.transcriptRoots,['/native/logs']);
|
|
25
|
+
assert.throws(()=>R.plan({...options,transcriptMode:'manual',transcriptRoots:[]}),/manual|roots/i);
|
|
26
|
+
});
|
|
27
|
+
test('automatic source uses the exact callback file regardless of filename suffix',t=>{
|
|
28
|
+
const f=fixture(t),payload={transcript_path:f.file,session_id:'session-a'};
|
|
29
|
+
const source=C.source(payload,{transcriptMode:'auto',transcriptRoots:[]},f.project);
|
|
30
|
+
assert.equal(source.path,f.file);assert.equal(E.loaded(source.calls,'frontend').length,1);assert.equal(source.tamperProof,false);
|
|
31
|
+
assert.equal(source.sourceVerification,'native-hook-transcript');
|
|
32
|
+
});
|
|
33
|
+
test('automatic discovery never admits business files, leaf symlinks or missing files',t=>{
|
|
34
|
+
const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]},load=file=>C.source({transcript_path:file,session_id:'session-a'},record,f.project);
|
|
35
|
+
const fake=path.join(f.project,'qa.jsonl');fs.copyFileSync(f.file,fake);assert.throws(()=>load(fake),/business project/i);
|
|
36
|
+
const alias=path.join(f.native,'alias.jsonl');fs.symlinkSync(f.file,alias);assert.throws(()=>load(alias),/symlink|native transcript/i);
|
|
37
|
+
assert.throws(()=>load(path.join(f.native,'missing.jsonl')),/not.*available|ENOENT/i);
|
|
38
|
+
assert.throws(()=>load('relative.jsonl'),/absolute|native transcript/i);
|
|
39
|
+
});
|
|
40
|
+
test('automatic source still rejects malformed streams, forged declarations and mismatched sessions',t=>{
|
|
41
|
+
const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]},payload={transcript_path:f.file,session_id:'different'};
|
|
42
|
+
assert.throws(()=>C.source(payload,record,f.project),/session/i);
|
|
43
|
+
payload.session_id='session-a';
|
|
44
|
+
for(const content of ['plain transcript text','{"skill":"frontend","passed":true}\n','{"type":"assistant","message":{"content":[]}}\n']) {
|
|
45
|
+
fs.writeFileSync(f.file,content);assert.throws(()=>C.source(payload,record,f.project),/JSONL|native|Native|user task/);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
test('manual roots remain enforced even when a valid callback points elsewhere',t=>{
|
|
49
|
+
const f=fixture(t),record={transcriptMode:'manual',transcriptRoots:[path.join(f.root,'other')]};
|
|
50
|
+
assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},record,f.project),/outside.*roots/);
|
|
51
|
+
record.transcriptRoots=[f.native];assert.equal(C.source({transcript_path:f.file,session_id:'session-a'},record,f.project).path,f.file);
|
|
52
|
+
});
|
|
53
|
+
test('automatic reading refuses oversized native files without parsing their contents',t=>{
|
|
54
|
+
const f=fixture(t);fs.truncateSync(f.file,64*1024*1024+1);
|
|
55
|
+
assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{transcriptMode:'auto',transcriptRoots:[]},f.project),/oversized|native transcript/i);
|
|
56
|
+
});
|
|
57
|
+
test('unreadable native transcripts cannot become valid evidence',t=>{
|
|
58
|
+
const f=fixture(t);fs.chmodSync(f.file,0);
|
|
59
|
+
try{assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{transcriptMode:'auto',transcriptRoots:[]},f.project),/EACCES|EPERM/);}
|
|
60
|
+
finally{fs.chmodSync(f.file,0o600);}
|
|
61
|
+
});
|
|
62
|
+
test('missing callback identity and contradictory policy cannot fall back to another log',t=>{
|
|
63
|
+
const f=fixture(t),record={transcriptMode:'auto',transcriptRoots:[]};
|
|
64
|
+
assert.throws(()=>C.source({session_id:'session-a'},record,f.project),/fields required/);
|
|
65
|
+
assert.throws(()=>C.source({transcript_path:f.file},record,f.project),/fields required/);
|
|
66
|
+
assert.throws(()=>C.source({transcript_path:f.file,session_id:'session-a'},{...record,transcriptRoots:[f.native]},f.project),/manual roots/);
|
|
67
|
+
});
|
|
@@ -31,7 +31,7 @@ Build production-quality user interfaces that are accessible, performant, and vi
|
|
|
31
31
|
|
|
32
32
|
## Applicable Prototype Baselines
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
Read the project prototype index at `prototypes/index.json`, falling back to legacy `.i3-ui-prototypes.json` when no new index exists. Select the entry whose `appliesTo` contains the explicit business module ID; an empty array applies to no module, and conflicting indices or multiple matches require resolution before selecting a baseline. New associations are derived from fixed scope.moduleIds by `i3-ui-prototype`, not inferred from a PRD filename. Then read its `baseline.json`, `design-system/MASTER.md`, tokens and page rules. Preserve approved design and interaction decisions; explain conflicts with new requirements instead of silently changing them. New pages reuse the selected template and component conventions. Changes to global tokens, navigation, component behavior or business states go through the `i3-ui-prototype` workflow and corresponding review. HTML review prototypes need explicit component mapping when implemented in another framework. Verify affected production pages in the browser; a prototype manifest check alone does not prove production fidelity. Projects without an applicable baseline retain this skill's existing workflow.
|
|
35
35
|
|
|
36
36
|
## Component Architecture
|
|
37
37
|
|
|
@@ -194,7 +194,7 @@ Each increment should be independently revertable:
|
|
|
194
194
|
|
|
195
195
|
## UI Slices with Prototype Baselines
|
|
196
196
|
|
|
197
|
-
For a UI slice, check `.i3-ui-prototypes.json` for an applicable approved prototype. Read its design contract, business mapping and baseline before implementation, and include affected interaction/visual checks in the slice acceptance. Preserve prior snapshots; coordinate contract changes through `i3-ui-prototype`. Backend-only slices and projects without a baseline continue the existing increment workflow.
|
|
197
|
+
For a UI slice, check `prototypes/index.json` (legacy `.i3-ui-prototypes.json` fallback) for an applicable approved prototype. Match explicit module IDs against appliesTo; an empty array is unbound. Resolve conflicting indices or multiple matches instead of applying a prototype globally. New appliesTo values come from fixed scope.moduleIds through `i3-ui-prototype`. Read its design contract, business mapping and baseline before implementation, and include affected interaction/visual checks in the slice acceptance. Preserve prior snapshots; coordinate contract changes through `i3-ui-prototype`. Backend-only slices and projects without a baseline continue the existing increment workflow.
|
|
198
198
|
|
|
199
199
|
## Working with Agents
|
|
200
200
|
|