@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,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};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use strict';
|
|
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'),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
|
+
const read=file=>R.fileIO.read(file);
|
|
6
|
+
function scriptsForEntry(scriptPath) {
|
|
7
|
+
if(path.basename(scriptPath)!=='i3-prototype.cjs')return path.dirname(path.dirname(scriptPath));
|
|
8
|
+
const root=path.resolve(path.dirname(scriptPath),'..');
|
|
9
|
+
const candidates=[root,path.dirname(root)].flatMap(directory=>['i3-ui-prototype',['ui','prototype'].join('-')].map(id=>path.join(directory,'skills',id,'scripts')));
|
|
10
|
+
const scripts=candidates.find(dir=>fs.existsSync(path.join(dir,'hosts/codebuddy.cjs')));assert.ok(scripts,'Unified Hook entry has no prototype Skill');return path.resolve(scripts);
|
|
11
|
+
}
|
|
12
|
+
function bundle(scriptPath) {
|
|
13
|
+
const scripts=scriptsForEntry(scriptPath);
|
|
14
|
+
const files=FILES.map(file=>[file,E.sha(fs.readFileSync(path.join(scripts,file)))]);
|
|
15
|
+
files.push(['profile',E.sha(fs.readFileSync(path.join(scripts,'../assets/hosts/codebuddy/profile.json')))]);
|
|
16
|
+
if(path.basename(scriptPath)==='i3-prototype.cjs') {
|
|
17
|
+
files.push(['unified-entry',E.sha(fs.readFileSync(scriptPath))]);
|
|
18
|
+
files.push(['unified-manager',E.sha(fs.readFileSync(path.join(path.dirname(scriptPath),'manage.cjs')))]);
|
|
19
|
+
}
|
|
20
|
+
return E.sha(JSON.stringify(files));
|
|
21
|
+
}
|
|
22
|
+
function registration(base) {
|
|
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.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');
|
|
26
|
+
const settingsText=read(path.join(base,'settings.json')),settings=R.config(settingsText);
|
|
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');
|
|
28
|
+
assert.equal(bundle(record.scriptPath),record.scriptDigest,'Audit adapter bytes changed; refresh owned registration');
|
|
29
|
+
assert.ok(fs.existsSync(record.nodePath),'Registered Node executable is missing');
|
|
30
|
+
return {...record,settingsDigest:E.sha(settingsText),registrationDigest:E.sha(JSON.stringify(record))};
|
|
31
|
+
}
|
|
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});
|
|
36
|
+
const projectText=readText(path.join(projectBase,'settings.json')),localText=readText(path.join(projectBase,'settings.local.json'));
|
|
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');}
|
|
38
|
+
const projectSettings=R.config(projectText);assert.ok(!projectSettings.disableAllHooks&&!projectSettings.allowManagedHooksOnly,'Project Hook policy prevents automatic auditing');
|
|
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');
|
|
40
|
+
// IDE documents project settings as an override. Do not silently fall back through a shadowing file.
|
|
41
|
+
const bases=host==='codebuddy'&&product==='ide'&&projectText!==null?[projectBase]:[projectBase,userBase];
|
|
42
|
+
for(const base of [...new Set(bases)]) {
|
|
43
|
+
const record=loadRegistration(base);if(!record)continue;
|
|
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
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
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
|
+
function sessionDirectory(record,project,sessionId) {
|
|
53
|
+
assert.ok(typeof sessionId==='string'&&sessionId,'Native session ID required');
|
|
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
|
+
}
|
|
56
|
+
function source(payload,record,project) {
|
|
57
|
+
assert.ok(typeof payload.transcript_path==='string'&&typeof payload.session_id==='string','Native transcript and session fields required');
|
|
58
|
+
const roots=record.transcriptRoots||[],mode=record.transcriptMode||(roots.length?'manual':'auto');
|
|
59
|
+
const value=E.readSource(payload.transcript_path,roots,{mode,project});
|
|
60
|
+
assert.ok(value.prompts.length,'Original user task is absent from native transcript');
|
|
61
|
+
assert.ok(value.calls.every(call=>call.sessionId===payload.session_id),'Native session identity missing or mismatched');
|
|
62
|
+
assert.ok(value.prompts.every(prompt=>!prompt.sessionId||prompt.sessionId===payload.session_id),'Native prompt session differs');
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
function readOnly(prompt) {
|
|
66
|
+
if(/不(?:要)?(?:改代码|修改(?:任何)?代码|实施|生成原型)|\b(?:analysis only|do not (?:edit|modify|implement))\b/i.test(prompt))return true;
|
|
67
|
+
if(/(?:只|仅).{0,6}(?:分析|讨论|梳理|检查|解释)/.test(prompt))return true;
|
|
68
|
+
if(/先.{0,4}(?:分析|讨论|梳理|检查|解释)/.test(prompt)&&!/(?:然后|再|后).{0,8}(?:实现|实施|修改|继续)/.test(prompt))return true;
|
|
69
|
+
return /^\s*(?:请|先)?(?:暂停|停止|取消)(?:[,。!\s]|(?:这|当前|实施|工作|修改|生成))|^\s*(?:stop|cancel|pause)\b/i.test(prompt);
|
|
70
|
+
}
|
|
71
|
+
function prefix(file,bytes,digest) {
|
|
72
|
+
assert.ok(Number.isSafeInteger(bytes)&&bytes>0&&bytes<=64*1024*1024,'Invalid source cursor');
|
|
73
|
+
const raw=E.cursorBytes(file);assert.ok(raw.length>=bytes,'Native transcript was truncated');assert.equal(E.sha(raw.subarray(0,bytes)),digest,'Native transcript prefix changed');
|
|
74
|
+
}
|
|
75
|
+
module.exports={read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'),assert=require('node:assert/strict');
|
|
3
|
+
const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs'),G=require('./execution-gates.cjs');
|
|
4
|
+
const X=require('./execution-runtime.cjs');
|
|
5
|
+
const H=require('./buddy-host.cjs');
|
|
6
|
+
const Recovery=require('./execution-recovery.cjs');
|
|
7
|
+
const {tokens,nativeCommand,stageFor}=require('./native-command.cjs');
|
|
8
|
+
function eventCall(payload,source) {
|
|
9
|
+
const callId=payload.tool_use_id||payload.call_id;
|
|
10
|
+
const canonical=value=>Array.isArray(value)?value.map(canonical):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(k=>[k,canonical(value[k])])):value;
|
|
11
|
+
const partial=(before,after)=>typeof before==='string'?typeof after==='string'&&after.startsWith(before):Array.isArray(before)?Array.isArray(after)&&before.length<=after.length&&before.every((value,i)=>partial(value,after[i])):before&&typeof before==='object'?after&&typeof after==='object'&&Object.keys(before).every(key=>Object.hasOwn(after,key)&&partial(before[key],after[key])):before===after;
|
|
12
|
+
const matches=source.calls.filter(call=>{
|
|
13
|
+
if(call.name!==payload.tool_name||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
|
|
14
|
+
if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
|
|
15
|
+
if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
|
|
16
|
+
// IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
|
|
17
|
+
// actual callback with one pending native call in the exact generation. The
|
|
18
|
+
// pending call remains unsuccessful until a native result is persisted.
|
|
19
|
+
return source.format==='codebuddy-ide-index-v1'&&H.isIDE(payload)&&typeof payload.generation_id==='string'&&call.nativeRequestId===payload.generation_id&&!call.resultLine&&partial(call.input,payload.tool_input||{});
|
|
20
|
+
});
|
|
21
|
+
const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
|
|
22
|
+
assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
|
|
23
|
+
}
|
|
24
|
+
function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
|
|
25
|
+
const toolEvent=['PreToolUse','PostToolUse'].includes(payload.hook_event_name);
|
|
26
|
+
// IDE can persist the current call only AFTER returning from both Hooks. Its
|
|
27
|
+
// current native prompt/generation still binds the callback. Never synthesize
|
|
28
|
+
// a successful call; A checks the persisted request/result at a later stage.
|
|
29
|
+
const attempts=toolEvent&&record.product==='ide'&&H.isIDE(payload)?8:1;
|
|
30
|
+
for(let attempt=1;attempt<=attempts;attempt++) {
|
|
31
|
+
try {
|
|
32
|
+
const source=readSource(payload,record,project);
|
|
33
|
+
let nativeRequestStatus='not-tool-event';
|
|
34
|
+
if(toolEvent) {
|
|
35
|
+
if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
|
|
36
|
+
const latest=source.prompts.at(-1);
|
|
37
|
+
assert.ok(typeof payload.generation_id==='string'&&latest?.nativeRequestId===payload.generation_id,'IDE callback differs from the current native task generation');
|
|
38
|
+
const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===payload.tool_name);
|
|
39
|
+
if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
|
|
40
|
+
else nativeRequestStatus='deferred-until-native-persistence';
|
|
41
|
+
}else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
|
|
42
|
+
}
|
|
43
|
+
return {...source,nativeReadAttempts:attempt,nativeRequestStatus};
|
|
44
|
+
}catch(error){if(attempt===attempts)throw error;pause(100);}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy}) {
|
|
48
|
+
if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
|
|
49
|
+
if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
|
|
50
|
+
const missing=Recovery.missing({source,binding,policy});
|
|
51
|
+
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing)};
|
|
52
|
+
}
|
|
53
|
+
const final=source.latestAssistant||'',claim=/(?:已完成|已经完成|全部通过|可以交付|\b(?:completed|ready for review|all checks passed)\b)/i.test(final)&&!/(?:未完成|尚未|待验证|阻塞|等待|进度|blocked|awaiting|not complete|in progress)/i.test(final);
|
|
54
|
+
const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
|
|
55
|
+
if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
|
|
56
|
+
const missing=Recovery.missing({source,binding,policy,stage:'delivery'});if(!missing.length)missing.push('execution-evidence');
|
|
57
|
+
return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing)};
|
|
58
|
+
}
|
|
59
|
+
return {continue:true};
|
|
60
|
+
}
|
|
61
|
+
function prototypeForWrite(project,tool,input) {
|
|
62
|
+
if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
|
|
63
|
+
const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
|
|
64
|
+
let current=path.dirname(path.resolve(project,file));const prototypes=path.join(project,'prototypes');
|
|
65
|
+
while(current.startsWith(prototypes+path.sep)){if(fs.existsSync(path.join(current,'prototype.json')))return fs.realpathSync(current);current=path.dirname(current);}return null;
|
|
66
|
+
}
|
|
67
|
+
function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
|
|
68
|
+
const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
|
|
69
|
+
const hostProject=options.host==='workbuddy'?env.WORKBUDDY_PROJECT_DIR:env.CODEBUDDY_PROJECT_DIR;
|
|
70
|
+
const root=options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):hostProject||env.CLAUDE_PROJECT_DIR||payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);assert.ok(root&&path.isAbsolute(root),'Native project directory required');
|
|
71
|
+
const project=real(root);
|
|
72
|
+
// IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
|
|
73
|
+
// or session-bound native workspace above; reject all other outside cwd.
|
|
74
|
+
if(payload.cwd&&!ideRoot){const relative=path.relative(project,real(payload.cwd));assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Hook cwd is outside project');}return project;
|
|
75
|
+
}
|
|
76
|
+
function saveIndex(directory,index) {
|
|
77
|
+
for(const entry of Object.values(index.bindings))F.write(path.dirname(directory),`runs/${entry.runId}/binding.json`,entry);
|
|
78
|
+
F.write(directory,'session.json',index);
|
|
79
|
+
}
|
|
80
|
+
function toolCwd(payload,record,project) {
|
|
81
|
+
return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
|
|
82
|
+
}
|
|
83
|
+
function handle(payload,options) {
|
|
84
|
+
assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
|
|
85
|
+
if(options.event==='SessionStart')return {continue:true};
|
|
86
|
+
if(options.event==='UserPromptSubmit'){
|
|
87
|
+
const prompt=payload.prompt||'';
|
|
88
|
+
if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:先 execution-begin,再实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并通过 prototype-deps query 或检测出的本地 Node 入口查询组件。专业模式还须实际调用 UIUX。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
|
|
89
|
+
return {continue:true};
|
|
90
|
+
}
|
|
91
|
+
let identity,installed;
|
|
92
|
+
try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
|
|
93
|
+
const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
94
|
+
if(options.event==='PreToolUse'&&project&&path.isAbsolute(project)){
|
|
95
|
+
const native=nativeCommand(payload.tool_name,payload.tool_input,project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
96
|
+
if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
options={...options,...identity};
|
|
101
|
+
const project=safeProject(payload,options,options.env),record=C.select({project,product:identity.product,host:identity.host,home:options.home,env:options.env});
|
|
102
|
+
if(!record||record.base!==options.base)return {continue:true};
|
|
103
|
+
const cwd=toolCwd(payload,record,project);
|
|
104
|
+
let source;
|
|
105
|
+
try {
|
|
106
|
+
source=eventSource(payload,record,project);
|
|
107
|
+
}catch(error){
|
|
108
|
+
const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
|
|
109
|
+
if(target&&X.policy(target)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},target))return {deny:true,reason:`Native evidence unavailable for strict prototype UI write: ${error.message}`};
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
const latest=source.prompts.at(-1);
|
|
113
|
+
if(C.readOnly(latest.text)) {
|
|
114
|
+
const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
115
|
+
if(options.event==='PreToolUse'&&(native&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(native.operation)||root&&X.policy(root)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},root)))return {deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'};
|
|
116
|
+
return {continue:true};
|
|
117
|
+
}
|
|
118
|
+
const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
|
|
119
|
+
const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
|
|
120
|
+
const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
|
|
121
|
+
if(options.event==='PreToolUse'&&command?.operation==='create'&&(installed?.schemaVersion===2||E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine)||/^\s*\/i3-prototype\b/.test(latest.text))){
|
|
122
|
+
const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
|
|
123
|
+
const hostAt=command.argv.indexOf('--host');
|
|
124
|
+
if(at<0||command.argv[at+1]!==expected||installed?.schemaVersion===2&&(hostAt<0||command.argv[hostAt+1]!==identity.host))return {deny:true,reason:`New ${identity.host} prototypes require --host ${identity.host} --execution-policy ${expected} and fixed --execution-assertions. Existing artifacts keep their original contracts.`};
|
|
125
|
+
}
|
|
126
|
+
const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
127
|
+
const existing=directory?index.bindings[directory]:null;
|
|
128
|
+
const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
|
|
129
|
+
let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
|
|
130
|
+
const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
|
|
131
|
+
const eventKey=E.sha(JSON.stringify([options.event,payload.generation_id||latest.nativeRequestId||null,callId||null,payload.tool_name||null,payload.tool_input||null,callId?null:source.digest]));
|
|
132
|
+
const recoveryContext=target=>{
|
|
133
|
+
const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
|
|
134
|
+
const retryCall=options.event==='PostToolUse'&&callbackCall?.success&&(stagePassed||G.uiWrite({...callbackCall,cwd:callbackCall.cwd||cwd},target))?callbackCall:null;
|
|
135
|
+
return {source,sessionId:payload.session_id,directory:target,event:options.event,eventKey,stage,policy:X.policy(target),resolved:[...(existing?['execution-begin']:[]),...(stagePassed?['execution-evidence']:[])],retrySucceeded:!!retryCall,
|
|
136
|
+
...(retryCall?{retry:{toolCallId:retryCall.toolCallId,resultDigest:retryCall.resultDigest,requestLine:retryCall.requestLine,resultLine:retryCall.resultLine,...(stagePassed?{kind:'stage',stage,operation:command.operation}:{kind:'ui-write'})}}:{})};
|
|
137
|
+
};
|
|
138
|
+
const respond=answer=>{
|
|
139
|
+
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
140
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry without bypassing the gate');}
|
|
141
|
+
try{const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');const result=Recovery.record(fresh,recoveryContext(directory),answer);if(fresh.bindings[directory]&&result.recovery)fresh.bindings[directory].recovery=result.recovery;saveIndex(stateDirectory,fresh);return result;}
|
|
142
|
+
finally{fs.rmdirSync(lock);}
|
|
143
|
+
};
|
|
144
|
+
if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
|
|
145
|
+
assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
|
|
146
|
+
const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
|
|
147
|
+
assert.equal(fixed.host,identity.host,'This prototype has a fixed contract for another host; inspect it or explicitly migrate and revalidate, do not inherit its invocation proof');
|
|
148
|
+
const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
|
|
149
|
+
assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
|
|
150
|
+
const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
|
|
151
|
+
const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
|
|
152
|
+
index.bindings[directory]={schemaVersion:1,runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
|
|
153
|
+
operation:meta.revision==='r001'?'create':'revise',host:identity.host,identitySource:identity.identitySource,product:record.product,productVersion:identity.productVersion,scope:record.scope,registrationDigest:record.registrationDigest,settingsDigest:record.settingsDigest,source:{path:source.path,bytes:source.bytes,digest:source.digest},lastHook:{event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus}};
|
|
154
|
+
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
155
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry execution-begin');}
|
|
156
|
+
try {
|
|
157
|
+
const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
|
|
158
|
+
saveIndex(stateDirectory,fresh);
|
|
159
|
+
F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
|
|
160
|
+
}finally{fs.rmdirSync(lock);}
|
|
161
|
+
return {continue:true};
|
|
162
|
+
}
|
|
163
|
+
if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
|
|
164
|
+
const fixed=X.policy(directory),answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd,policy:fixed});
|
|
165
|
+
if(answer.deny)return respond(answer);
|
|
166
|
+
if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)){
|
|
167
|
+
if(!existing)return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'])});
|
|
168
|
+
const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
|
|
169
|
+
if(checked.status==='failed'){
|
|
170
|
+
const missing=Recovery.missing({source,binding:existing,policy:fixed,stage});if(!missing.length)missing.push('execution-evidence');
|
|
171
|
+
return respond({deny:true,missing,reason:Recovery.instruction(missing)+' '+checked.errors.join('; ')});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event))respond({continue:true});
|
|
175
|
+
}
|
|
176
|
+
if(!Object.keys(index.bindings).length)return {continue:true};
|
|
177
|
+
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
178
|
+
try{fs.mkdirSync(lock);}catch(error){if(error.code==='EEXIST')throw new Error('Concurrent audit callback; no success evidence recorded. Retry the stage.');throw error;}
|
|
179
|
+
try {
|
|
180
|
+
const fresh=JSON.parse(C.read(file));let changed=false;
|
|
181
|
+
for(const entry of Object.values(fresh.bindings)) {
|
|
182
|
+
if(entry.entryLine!==latest.line)continue;
|
|
183
|
+
if(entry.lastEventKey===eventKey)continue;
|
|
184
|
+
entry.source={path:source.path,bytes:source.bytes,digest:source.digest};entry.lastEventKey=eventKey;entry.lastHook={event:options.event,at:new Date().toISOString(),nativeReadAttempts:source.nativeReadAttempts,nativeRequestStatus:source.nativeRequestStatus};changed=true;
|
|
185
|
+
if(options.event==='Stop') {
|
|
186
|
+
const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
|
|
187
|
+
const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
|
|
188
|
+
const fixed=X.policy(entry.directory),feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,policy:fixed,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
|
|
189
|
+
if(feedback.feedback){const result=Recovery.record(fresh,{...recoveryContext(entry.directory),policy:fixed},feedback);entry.recovery=result.recovery;if(fixed.id!==P.AUTO_ID)entry.stopFeedbackPrompt=latest.digest;saveIndex(stateDirectory,fresh);return result;}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if(changed)saveIndex(stateDirectory,fresh);
|
|
193
|
+
}finally{fs.rmdirSync(lock);}
|
|
194
|
+
return {continue:true};
|
|
195
|
+
}
|
|
196
|
+
module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Observed CodeBuddy IDE history format: Hook-provided index.json and only the
|
|
3
|
+
// UUID message files explicitly referenced by that index. No directory scan.
|
|
4
|
+
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
|
5
|
+
const ID=/^[a-f0-9]{32}$/;
|
|
6
|
+
const aliases={use_skill:'Skill',execute_command:'Bash',write_to_file:'Write',read_file:'Read',replace_in_file:'Edit'};
|
|
7
|
+
function read(file) {
|
|
8
|
+
assert.equal(path.basename(file),'index.json','Expected native IDE index');
|
|
9
|
+
const directory=path.dirname(file),sessionId=path.basename(directory);
|
|
10
|
+
assert.match(sessionId,ID,'Invalid IDE session directory');
|
|
11
|
+
let total=0;
|
|
12
|
+
const load=target=>{
|
|
13
|
+
const stat=fs.lstatSync(target);assert.ok(stat.isFile()&&!stat.isSymbolicLink(),'Invalid IDE history file');
|
|
14
|
+
total+=stat.size;assert.ok(total<=64*1024*1024,'IDE history exceeds size limit');
|
|
15
|
+
return JSON.parse(fs.readFileSync(target,'utf8'));
|
|
16
|
+
};
|
|
17
|
+
const index=load(file);assert.ok(Array.isArray(index.messages)&&Array.isArray(index.requests),'Unsupported IDE index');
|
|
18
|
+
assert.ok(index.messages.length<=10000&&index.requests.length<=10000,'IDE history index exceeds limit');
|
|
19
|
+
const folder=path.join(directory,'messages');assert.ok(fs.lstatSync(folder).isDirectory()&&!fs.lstatSync(folder).isSymbolicLink(),'Invalid IDE message directory');
|
|
20
|
+
const requestIds=new Map(),seen=new Set(),events=[];let latestAssistant='',workspace;
|
|
21
|
+
for(const request of index.requests){
|
|
22
|
+
assert.match(request.id,ID,'Invalid IDE request ID');assert.ok(Array.isArray(request.messages),'Invalid IDE request messages');
|
|
23
|
+
for(const id of request.messages){assert.match(id,ID,'Invalid IDE message reference');assert.ok(!requestIds.has(id),'Duplicate IDE message request');requestIds.set(id,request.id);}
|
|
24
|
+
}
|
|
25
|
+
for(const item of index.messages){
|
|
26
|
+
assert.match(item.id,ID,'Invalid IDE message ID');assert.ok(!seen.has(item.id),'Duplicate IDE message ID');seen.add(item.id);
|
|
27
|
+
const row=load(path.join(folder,item.id+'.json'));
|
|
28
|
+
assert.equal(row.id,item.id,'IDE message identity differs');assert.equal(row.role,item.role,'IDE message role differs');
|
|
29
|
+
const message=JSON.parse(row.message),extra=JSON.parse(row.extra||'{}');
|
|
30
|
+
assert.equal(message.role,row.role,'IDE serialized message role differs');
|
|
31
|
+
assert.ok(requestIds.has(item.id),'Unbound IDE message');
|
|
32
|
+
// The index is the primary request membership. During execution the SDK can
|
|
33
|
+
// write message content before its duplicate extra.requestId metadata.
|
|
34
|
+
const requestId=requestIds.get(item.id);
|
|
35
|
+
if(extra.requestId!==undefined)assert.equal(extra.requestId,requestId,'IDE request identity differs');
|
|
36
|
+
assert.ok(Array.isArray(message.content),'Invalid IDE message content');
|
|
37
|
+
const base={sessionId,nativeMessageId:item.id,nativeRequestId:requestId};
|
|
38
|
+
if(row.role==='user'&&!extra.isHelperMessage){
|
|
39
|
+
const hostText=message.content[0]?.type==='text'?message.content[0].text:'';
|
|
40
|
+
const context=typeof hostText==='string'?hostText.match(/^<user_info>\s*([\s\S]*?)<\/user_info>/):null;
|
|
41
|
+
const nativeFolder=context?.[1].match(/^Workspace Folder:\s*(\/[^\r\n]+)$/m)?.[1]?.trim();
|
|
42
|
+
if(nativeFolder){
|
|
43
|
+
assert.ok(path.isAbsolute(nativeFolder)&&!/[\x00\r\n]/.test(nativeFolder),'Invalid native IDE workspace');
|
|
44
|
+
if(workspace)assert.equal(workspace,nativeFolder,'Native IDE workspace changed within one session');
|
|
45
|
+
workspace=nativeFolder;
|
|
46
|
+
}
|
|
47
|
+
assert.ok(Array.isArray(extra.sourceContentBlocks),'Original IDE user task is missing');
|
|
48
|
+
const text=extra.sourceContentBlocks.filter(b=>b.type==='text'&&typeof b.text==='string').map(b=>b.text).join('\n');
|
|
49
|
+
if(text.trim())events.push({...base,type:'user',message:{content:[{type:'text',text}]}});
|
|
50
|
+
}else if(row.role==='assistant'){
|
|
51
|
+
const text=message.content.filter(b=>b.type==='text').map(b=>b.text||'').join('\n');
|
|
52
|
+
if(text)latestAssistant=text;
|
|
53
|
+
for(const block of message.content.filter(b=>b.type==='tool-call')){
|
|
54
|
+
assert.ok(block.args&&typeof block.args==='object'&&!Array.isArray(block.args),'Invalid IDE tool arguments');
|
|
55
|
+
events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:aliases[block.toolName]||block.toolName,input:block.args}]}});
|
|
56
|
+
}
|
|
57
|
+
}else if(row.role==='tool'){
|
|
58
|
+
for(const block of message.content.filter(b=>b.type==='tool-result')){
|
|
59
|
+
const result=block.result,success=!block.isError&&result?.success===true&&result.status==='success';
|
|
60
|
+
events.push({...base,type:'user',message:{content:[{type:'tool_result',tool_use_id:block.toolCallId,
|
|
61
|
+
tool_name:aliases[block.toolName]||block.toolName,content:result,is_error:!success}]}});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Only stable prompt/call/result facts form the prefix cursor. Streaming text,
|
|
66
|
+
// reasoning, request state and mutable toolStatus do not invalidate old facts.
|
|
67
|
+
return {text:events.map(row=>JSON.stringify(row)).join('\n')+'\n',latestAssistant,workspace,format:'codebuddy-ide-index-v1'};
|
|
68
|
+
}
|
|
69
|
+
module.exports={read};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure configuration planning. This module does not install or activate a host.
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const crypto = require('node:crypto');
|
|
7
|
+
const { digest } = require('./execution-policy.cjs');
|
|
8
|
+
const OWNER = 'i3-prototype-codebuddy-v1';
|
|
9
|
+
const EVENTS = ['SessionStart', 'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'Stop'];
|
|
10
|
+
function quote(value) {
|
|
11
|
+
assert.ok(typeof value === 'string' && !/[\x00\r\n]/.test(value), 'Unsafe Hook argument');
|
|
12
|
+
return "'" + value.replaceAll("'", "'\\''") + "'";
|
|
13
|
+
}
|
|
14
|
+
function config(text) {
|
|
15
|
+
if (text === null) return {};
|
|
16
|
+
let value; try { value = JSON.parse(text); } catch { throw new Error('Settings must be valid JSON; preserve the original file'); }
|
|
17
|
+
assert.ok(value && !Array.isArray(value) && typeof value === 'object', 'Settings must be a JSON object');
|
|
18
|
+
assert.ok(!value.hooks || (typeof value.hooks === 'object' && !Array.isArray(value.hooks)), 'Invalid hooks configuration');
|
|
19
|
+
for (const groups of Object.values(value.hooks || {})) {
|
|
20
|
+
assert.ok(Array.isArray(groups), 'Hook groups must be arrays');
|
|
21
|
+
for (const group of groups) assert.ok(group && Array.isArray(group.hooks), 'Invalid Hook group');
|
|
22
|
+
}
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function ownedMarker(group) { return group.hooks.some(hook => typeof hook.command === 'string' && hook.command.includes(OWNER)); }
|
|
26
|
+
function plan(options) {
|
|
27
|
+
const { action = 'keep', scope, product = 'auto', base, project, nodePath, scriptPath, scriptDigest, currentText = null, record = null,
|
|
28
|
+
transcriptRoots = record?.transcriptRoots || [], transcriptMode = record?.transcriptMode || (transcriptRoots.length ? 'manual' : 'auto'), productVersion = record?.productVersion || 'unreported' } = options;
|
|
29
|
+
assert.ok(['keep', 'enable', 'disable'].includes(action), 'Invalid Hook action');
|
|
30
|
+
assert.ok(['project', 'global'].includes(scope), 'Invalid installation scope');
|
|
31
|
+
assert.ok(['auto', 'ide', 'cli'].includes(product), 'Use automatic product detection, or a legacy ide/cli profile');
|
|
32
|
+
const hosts=[...new Set(options.hosts || [...(record?.hosts || (record ? ['codebuddy'] : [])),...(options.agents?.filter(host=>['codebuddy','workbuddy'].includes(host)) || [options.host || 'codebuddy'])])].sort();
|
|
33
|
+
assert.ok((hosts.length||action==='disable')&&hosts.every(host=>['codebuddy','workbuddy'].includes(host)), 'Unsupported prototype Hook host');
|
|
34
|
+
for (const item of [base, project, nodePath, scriptPath]) assert.ok(typeof item === 'string' && path.isAbsolute(item) && !/[\x00\r\n]/.test(item), 'Absolute safe registration paths required');
|
|
35
|
+
if (process.platform === 'win32') throw new Error('CodeBuddy A+B Windows Hook quoting is not yet verified');
|
|
36
|
+
const before = config(currentText), after = structuredClone(before), activeRecord = record && record.owner === OWNER ? record : null;
|
|
37
|
+
if (record) {
|
|
38
|
+
assert.ok(activeRecord && [1,2].includes(activeRecord.schemaVersion), 'Unknown registration ownership');
|
|
39
|
+
assert.equal(activeRecord.scope, scope, 'Registration scope changed');
|
|
40
|
+
if(product!=='auto')assert.equal(activeRecord.product, product, 'Disable the previous product profile before changing it');
|
|
41
|
+
assert.equal(activeRecord.base, base, 'Registration base changed');
|
|
42
|
+
}
|
|
43
|
+
const found = {};
|
|
44
|
+
for (const [event, groups] of Object.entries(after.hooks || {})) {
|
|
45
|
+
for (const group of groups.filter(ownedMarker)) {
|
|
46
|
+
assert.ok(activeRecord?.groups[event], 'Hook ownership is missing; do not adopt an unknown command');
|
|
47
|
+
assert.deepEqual(group, activeRecord.groups[event], 'Owned Hook was modified; preserve the user changes');
|
|
48
|
+
assert.ok(!found[event], 'Duplicate owned Hook registration'); found[event] = group;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (activeRecord) for (const event of Object.keys(activeRecord.groups)) assert.ok(found[event], 'Owned Hook registration changed or is missing');
|
|
52
|
+
const unchanged = action === 'keep' && !activeRecord;
|
|
53
|
+
let recordAfter = null;
|
|
54
|
+
if (!unchanged) {
|
|
55
|
+
after.hooks ||= {};
|
|
56
|
+
for (const event of Object.keys(found)) after.hooks[event] = after.hooks[event].filter(group => group !== found[event]);
|
|
57
|
+
if (action !== 'disable') {
|
|
58
|
+
assert.match(scriptDigest || '', /^[a-f0-9]{64}$/, 'Hook script digest required');
|
|
59
|
+
const groups = {};
|
|
60
|
+
for (const event of EVENTS) {
|
|
61
|
+
const argv = [nodePath, scriptPath, '--registration', OWNER, '--base', base, '--scope', scope, '--product', product, '--event', event];
|
|
62
|
+
const group = { ...(event.endsWith('ToolUse') ? { matcher: '' } : {}), hooks: [{ type: 'command', command: argv.map(quote).join(' '), timeout: 10 }] };
|
|
63
|
+
groups[event] = group; (after.hooks[event] ||= []).push(group);
|
|
64
|
+
}
|
|
65
|
+
assert.ok(Array.isArray(transcriptRoots)&&transcriptRoots.every(root=>typeof root==='string'&&path.isAbsolute(root)&&!/[\x00\r\n]/.test(root)),'Absolute native transcript roots required');
|
|
66
|
+
assert.ok(['auto','manual'].includes(transcriptMode),'Invalid native transcript mode');
|
|
67
|
+
assert.ok(transcriptMode==='manual'?transcriptRoots.length>0:transcriptRoots.length===0,'Manual transcript mode requires roots; auto uses only the native callback file');
|
|
68
|
+
recordAfter = { schemaVersion: product==='auto'?2:1, owner: OWNER, scope, product, ...(product==='auto'?{hosts}:{}), base, scriptPath, scriptDigest, nodePath, groups, transcriptMode, transcriptRoots, productVersion };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const afterText = unchanged ? currentText : JSON.stringify(after, null, 2) + '\n';
|
|
72
|
+
return { action, scope, product, base, project, settingsPath: path.join(base, 'settings.json'),
|
|
73
|
+
recordPath: path.join(base, 'state/i3-prototype/installation.json'), beforeDigest: digest(currentText), beforeRecordDigest: digest(record), afterText, recordAfter,
|
|
74
|
+
summary: { status: unchanged || action === 'disable' ? 'not-enabled' : product === 'cli' ? 'pending-host-review' : 'registered',
|
|
75
|
+
...(recordAfter?{transcript:{mode:transcriptMode,status:'awaiting-native-event',invocationVerified:false}}:{}),
|
|
76
|
+
scope, product, settingsPath: path.join(base, 'settings.json'), events: recordAfter ? EVENTS : [], changed: afterText !== currentText || digest(recordAfter)!==digest(record),
|
|
77
|
+
next: product === 'cli' && recordAfter ? 'Review the external settings change in CodeBuddy /hooks; then start a new session.' : 'Actual Hook invocation must be checked in the host; registration is not activation.' } };
|
|
78
|
+
}
|
|
79
|
+
function safePath(file) {
|
|
80
|
+
let current = path.parse(path.resolve(file)).root;
|
|
81
|
+
for (const part of path.resolve(file).slice(current.length).split(path.sep)) {
|
|
82
|
+
current = path.join(current, part);
|
|
83
|
+
if (fs.existsSync(current)) assert.ok(!fs.lstatSync(current).isSymbolicLink(), 'Hook settings symlink refused');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const fileIO = {
|
|
87
|
+
lock(file) {safePath(file);fs.mkdirSync(path.dirname(file),{recursive:true});const lock=file+'.i3-prototype.lock';try{fs.mkdirSync(lock);}catch{throw new Error('Hook configuration is being updated; retry without overwriting the other change');}return ()=>fs.rmdirSync(lock);},
|
|
88
|
+
read(file) {
|
|
89
|
+
safePath(file); if (!fs.existsSync(file)) return null;
|
|
90
|
+
assert.ok(fs.statSync(file).isFile() && fs.statSync(file).size <= 4 * 1024 * 1024, 'Invalid or oversized Hook configuration');
|
|
91
|
+
return fs.readFileSync(file, 'utf8');
|
|
92
|
+
},
|
|
93
|
+
write(file, text, expected) {
|
|
94
|
+
assert.equal(digest(this.read(file)), expected, 'Hook configuration changed concurrently');
|
|
95
|
+
if (text === null) { if (fs.existsSync(file)) fs.unlinkSync(file); return; }
|
|
96
|
+
fs.mkdirSync(path.dirname(file), { recursive: true }); safePath(file);
|
|
97
|
+
const staging = file + '.' + crypto.randomUUID() + '.new';
|
|
98
|
+
try {
|
|
99
|
+
fs.writeFileSync(staging, text, { flag: 'wx', mode: fs.existsSync(file) ? fs.statSync(file).mode & 0o777 : 0o600 });
|
|
100
|
+
assert.equal(digest(this.read(file)), expected, 'Hook configuration changed before publication');
|
|
101
|
+
fs.renameSync(staging, file);
|
|
102
|
+
} finally { if (fs.existsSync(staging)) fs.unlinkSync(staging); }
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
function apply(change, io = fileIO) {
|
|
106
|
+
const unlock=io.lock?io.lock(change.settingsPath):()=>{};
|
|
107
|
+
try {return applyLocked(change,io);}finally{unlock();}
|
|
108
|
+
}
|
|
109
|
+
function applyLocked(change, io) {
|
|
110
|
+
const before = io.read(change.settingsPath), recordBefore = io.read(change.recordPath);
|
|
111
|
+
assert.equal(digest(before), change.beforeDigest, 'Hook configuration changed after preview');
|
|
112
|
+
assert.equal(digest(recordBefore === null ? null : JSON.parse(recordBefore)), change.beforeRecordDigest, 'Hook ownership changed after preview');
|
|
113
|
+
const recordAfter = change.recordAfter ? JSON.stringify(change.recordAfter, null, 2) + '\n' : null;
|
|
114
|
+
if (change.afterText === before && recordAfter === recordBefore) return change.summary;
|
|
115
|
+
let settingsWritten = false;
|
|
116
|
+
try {
|
|
117
|
+
if (change.afterText !== before) { io.write(change.settingsPath, change.afterText, digest(before)); settingsWritten = true; }
|
|
118
|
+
if (recordAfter !== recordBefore) io.write(change.recordPath, recordAfter, digest(recordBefore));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (settingsWritten && io.read(change.settingsPath) === change.afterText) io.write(change.settingsPath, before, digest(change.afterText));
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
return change.summary;
|
|
124
|
+
}
|
|
125
|
+
module.exports = { OWNER, EVENTS, quote, config, plan, apply, fileIO };
|
|
@@ -56,8 +56,8 @@ function scopeInputs(snapshot, scope, contract) {
|
|
|
56
56
|
assert.ok(page.regions?.length, `Declare the actual region modes: ${page.id}`);
|
|
57
57
|
for (const region of page.regions) {
|
|
58
58
|
assert.match(region.id || '', /^[a-z][a-z0-9-]{0,63}$/);
|
|
59
|
-
assert.ok(['management-list', 'edit-form', 'content'].includes(region.mode), `Unknown business region mode: ${region.mode}`);
|
|
60
|
-
regions.push({ pageId: page.id, id: region.id, mode: region.mode });
|
|
59
|
+
assert.ok(['management-list', 'edit-form', 'content'].includes(region.mode) || (scope.schemaVersion === 3 && require('./generation-contract.cjs').resource(snapshot, 'generation.json').patterns.includes(region.pattern || region.mode)), `Unknown business region mode: ${region.mode}`);
|
|
60
|
+
regions.push({ pageId: page.id, id: region.id, mode: scope.schemaVersion === 3 ? 'content' : region.mode });
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
for (const region of regions) for (const capability of snapshot.definition.capabilities) {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const assert=require('node:assert/strict'),E=require('./execution-evidence.cjs');
|
|
3
|
+
const METHODS=['evaluate_script','take_snapshot','list_console_messages','list_network_requests','performance_stop_trace'];
|
|
4
|
+
function validate(value,scenarioIds) {
|
|
5
|
+
assert.equal(value?.schemaVersion,1,'Fixed native assertions schema required');assert.ok(Array.isArray(value.scenarios),'Fixed native assertion scenarios required');
|
|
6
|
+
assert.deepEqual(value.scenarios.map(s=>s.scenarioId).sort(),[...scenarioIds].sort(),'Fixed native assertion scenario coverage differs');
|
|
7
|
+
for(const scenario of value.scenarios) {
|
|
8
|
+
assert.ok(scenario.checks?.length,'Each fixed scenario needs actual result assertions before implementation');
|
|
9
|
+
for(const check of scenario.checks) {
|
|
10
|
+
assert.ok(METHODS.includes(check.method),'Unsupported native assertion method');
|
|
11
|
+
assert.ok(typeof check.pointer==='string'&&/^\/(?:[^~]|~[01])*$/.test(check.pointer)&&!/(?:__proto__|constructor|prototype)/.test(check.pointer),'Safe JSON pointer required');
|
|
12
|
+
assert.ok(['equals','includes','gte','lte'].includes(check.operator),'Unsupported native assertion operator');
|
|
13
|
+
assert.ok(Object.hasOwn(check,'expected')&&check.expected!==null,'Explicit fixed expected fact required');
|
|
14
|
+
if(['gte','lte'].includes(check.operator))assert.equal(typeof check.expected,'number','Numeric fixed expectation required');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
function resultData(result) {
|
|
20
|
+
const text=E.contentText(result).trim();let value;
|
|
21
|
+
try{value=JSON.parse(text);}catch{const blocks=[...text.matchAll(/```(?:json)?\s*\n([\s\S]*?)\n```/g)];assert.equal(blocks.length,1,'One actual JSON result or JSON code block required');value=JSON.parse(blocks[0][1]);}
|
|
22
|
+
assert.ok(value&&typeof value==='object','Native JSON fact result required');return value;
|
|
23
|
+
}
|
|
24
|
+
function extract(value,pointer) {
|
|
25
|
+
for(const part of pointer.slice(1).split('/').map(p=>p.replaceAll('~1','/').replaceAll('~0','~'))) {assert.ok(value&&Object.hasOwn(value,part),'Native expected fact is absent');value=value[part];}return value;
|
|
26
|
+
}
|
|
27
|
+
function compare(actual,check) {
|
|
28
|
+
if(check.operator==='equals')assert.deepEqual(actual,check.expected,'Native fact differs from fixed expectation');
|
|
29
|
+
else if(check.operator==='includes')assert.ok((typeof actual==='string'||Array.isArray(actual))&&actual.includes(check.expected),'Native fact does not include fixed expectation');
|
|
30
|
+
else {assert.equal(typeof actual,'number','Native numeric fact required');assert.ok(check.operator==='gte'?actual>=check.expected:actual<=check.expected,'Native numeric fact differs from fixed expectation');}
|
|
31
|
+
}
|
|
32
|
+
function template(options) {
|
|
33
|
+
const fs=require('node:fs'),path=require('node:path'),T=require('./template-contract.cjs');
|
|
34
|
+
assert.ok(options.scope,'Sourced scope file required');const scope=JSON.parse(fs.readFileSync(path.resolve(options.scope),'utf8'));
|
|
35
|
+
const snapshot=options.templateRoot?T.loadTemplate(path.resolve(options.templateRoot)):T.loadBundledTemplate(options.template);
|
|
36
|
+
const selection=snapshot.kind==='content-reference'?require('./content-template.cjs').scopeInputs(snapshot,scope,T):null;
|
|
37
|
+
const generation=scope.schemaVersion===3?require('./generation-contract.cjs').compile(snapshot,scope):null;
|
|
38
|
+
const scenarios=[...(selection?.result.requiredScenarios||snapshot.index.requiredScenarios),...(generation?.scenarios||[])];
|
|
39
|
+
const drawer=selection&&(selection.fixed.regions.some(r=>r.mode==='edit-form')||generation?.bindings.some(b=>b.capabilityId==='dialog'&&b.parameters.presentation==='drawer'));
|
|
40
|
+
const framework=selection?['framework-responsive','framework-instance-isolation','template-content-projection',...(drawer?['framework-drawer-motion','framework-mask-blur','framework-focus-scroll']:[])]:['framework-drawer-motion','framework-mask-blur','framework-focus-scroll','framework-states','framework-responsive','framework-instance-isolation','template-content-projection'];
|
|
41
|
+
return {schemaVersion:1,scopeDigest:E.sha(JSON.stringify(scope)),templateDigest:snapshot.digest,scenarios:[...scenarios.map(s=>({scenarioId:typeof s==='string'?s:s.id,expectedSummary:s.expected||null,checks:[]})),...framework.map(scenarioId=>({scenarioId,checks:[]}))]};
|
|
42
|
+
}
|
|
43
|
+
module.exports={validate,resultData,extract,compare,template};
|