@givolt/i3-composer-agent 2.3.4 → 2.3.6
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/commands/i3-prototype.md +2 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.gemini/commands/i3-prototype.toml +2 -0
- package/README.md +1 -1
- package/commands/i3-prototype.toml +2 -0
- package/docs/codebuddy-setup.md +1 -1
- package/docs/codex-setup.md +1 -1
- package/docs/other-hosts.md +1 -1
- package/docs/workbuddy-setup.md +1 -1
- package/hooks/PROTOTYPE.md +19 -0
- package/hooks/i3-prototype.cjs +2 -1
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/scripts/ui-prototype-binding-recovery-test.js +84 -0
- package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -2
- package/scripts/ui-prototype-codebuddy-hook-test.js +4 -4
- package/scripts/ui-prototype-execution-gates-test.js +4 -3
- package/skills/i3-ui-prototype/SKILL.md +2 -6
- package/skills/i3-ui-prototype/references/execution-evidence.md +13 -1
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +3 -3
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +134 -30
- package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +3 -3
- package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +94 -0
- package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +44 -13
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +52 -3
- package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +13 -7
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +9 -2
- package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/native-command.cjs +19 -2
- package/skills/i3-ui-prototype/scripts/native-delivery.cjs +4 -3
- package/skills/i3-ui-prototype/scripts/native-files.cjs +10 -1
- package/skills/i3-ui-prototype/scripts/native-pilot.cjs +6 -1
- package/skills/i3-ui-prototype/scripts/native-tool-name.cjs +5 -0
- package/skills/i3-ui-prototype/scripts/native-tool-result.cjs +14 -0
- package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +1 -1
- package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +7 -3
- package/skills/i3-ui-prototype/scripts/ptc-callbacks.cjs +131 -0
|
@@ -4,13 +4,14 @@ const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=req
|
|
|
4
4
|
const X=require('./execution-runtime.cjs');
|
|
5
5
|
const H=require('./buddy-host.cjs');
|
|
6
6
|
const Recovery=require('./execution-recovery.cjs');
|
|
7
|
-
const {tokens,nativeCommand,stageFor}=require('./native-command.cjs');
|
|
7
|
+
const {tokens,nativeCommand,stageFor,completedBegin}=require('./native-command.cjs');
|
|
8
|
+
const {toolName}=require('./native-tool-name.cjs');
|
|
8
9
|
function eventCall(payload,source) {
|
|
9
10
|
const callId=payload.tool_use_id||payload.call_id;
|
|
10
11
|
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
12
|
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
13
|
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(call.name!==toolName(payload.tool_name)||call.requestLine<source.prompts.at(-1).line||callId&&call.toolCallId!==callId)return false;
|
|
14
15
|
if(source.format==='codebuddy-ide-index-v1'&&payload.generation_id&&call.nativeRequestId!==payload.generation_id)return false;
|
|
15
16
|
if(JSON.stringify(canonical(call.input))===JSON.stringify(canonical(payload.tool_input||{})))return true;
|
|
16
17
|
// IDE 4.12 streams incomplete args to disk before PreToolUse. Correlate its
|
|
@@ -18,7 +19,11 @@ function eventCall(payload,source) {
|
|
|
18
19
|
// pending call remains unsuccessful until a native result is persisted.
|
|
19
20
|
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
|
});
|
|
21
|
-
|
|
22
|
+
// IDE can omit the call ID and invoke Post before persisting the result.
|
|
23
|
+
// A failed earlier attempt can have identical args; only the still-pending
|
|
24
|
+
// request in this exact generation can be the current callback.
|
|
25
|
+
const pending=matches.filter(call=>!call.resultLine);
|
|
26
|
+
const current=payload.hook_event_name==='PreToolUse'||source.format==='codebuddy-ide-index-v1'&&pending.length?pending:matches;
|
|
22
27
|
assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
|
|
23
28
|
}
|
|
24
29
|
function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
|
|
@@ -26,16 +31,19 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
|
|
|
26
31
|
// IDE can persist the current call only AFTER returning from both Hooks. Its
|
|
27
32
|
// current native prompt/generation still binds the callback. Never synthesize
|
|
28
33
|
// a successful call; A checks the persisted request/result at a later stage.
|
|
29
|
-
const attempts=toolEvent
|
|
34
|
+
const attempts=toolEvent?8:1;
|
|
30
35
|
for(let attempt=1;attempt<=attempts;attempt++) {
|
|
31
36
|
try {
|
|
32
|
-
|
|
37
|
+
let source=readSource(payload,record,project);
|
|
38
|
+
if(toolEvent&&record.product==='cli'&&(payload.agent_type==='ptc'||source.calls.some(c=>c.name==='REPL'&&!c.resultLine))){
|
|
39
|
+
source=require('./ptc-callbacks.cjs').observe({payload,record,project,source});
|
|
40
|
+
}
|
|
33
41
|
let nativeRequestStatus='not-tool-event';
|
|
34
42
|
if(toolEvent) {
|
|
35
43
|
if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
|
|
36
44
|
const latest=source.prompts.at(-1);
|
|
37
45
|
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);
|
|
46
|
+
const pending=source.calls.filter(call=>!call.resultLine&&call.requestLine>latest.line&&call.nativeRequestId===payload.generation_id&&call.name===toolName(payload.tool_name));
|
|
39
47
|
if(pending.length) {eventCall(payload,source);nativeRequestStatus='pending-native-request';}
|
|
40
48
|
else nativeRequestStatus='deferred-until-native-persistence';
|
|
41
49
|
}else {eventCall(payload,source);nativeRequestStatus='matched-native-request';}
|
|
@@ -48,26 +56,26 @@ function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent
|
|
|
48
56
|
if(C.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
|
|
49
57
|
if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
|
|
50
58
|
const missing=Recovery.missing({source,binding,policy});
|
|
51
|
-
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing)};
|
|
59
|
+
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
|
|
52
60
|
}
|
|
53
61
|
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
62
|
const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
|
|
55
63
|
if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
|
|
56
64
|
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)};
|
|
65
|
+
return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
|
|
58
66
|
}
|
|
59
67
|
return {continue:true};
|
|
60
68
|
}
|
|
61
|
-
function prototypeForWrite(project,tool,input) {
|
|
69
|
+
function prototypeForWrite(project,tool,input,cwd=project) {
|
|
62
70
|
if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
|
|
63
71
|
const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
|
|
64
|
-
let current=path.dirname(path.resolve(
|
|
72
|
+
let current=path.dirname(path.resolve(cwd,file));const prototypes=path.join(project,'prototypes');
|
|
65
73
|
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
74
|
}
|
|
67
75
|
function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
|
|
68
76
|
const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
|
|
69
77
|
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');
|
|
78
|
+
const root=options.scope==='project'?path.dirname(options.base):options.nativeProject|| (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
79
|
const project=real(root);
|
|
72
80
|
// IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
|
|
73
81
|
// or session-bound native workspace above; reject all other outside cwd.
|
|
@@ -80,19 +88,45 @@ function saveIndex(directory,index) {
|
|
|
80
88
|
function toolCwd(payload,record,project) {
|
|
81
89
|
return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
|
|
82
90
|
}
|
|
83
|
-
function
|
|
91
|
+
function settleNativeRetries(index,source,{project,sessionId}) {
|
|
92
|
+
let changed=false;
|
|
93
|
+
for(const entry of Object.values(index.bindings)) {
|
|
94
|
+
const repair=entry.recovery,expected=repair?.expectedRetry;
|
|
95
|
+
if(!expected||!['required','ready-to-retry'].includes(repair.status)||entry.entryLine!==source.prompts.at(-1)?.line||entry.contextLine!==source.contextLine)continue;
|
|
96
|
+
const matches=source.calls.filter(call=>{
|
|
97
|
+
if(!call.success||!call.resultLine||call.sessionId!==sessionId||call.requestLine<=entry.entryLine||source.stableThroughLine!==undefined&&call.resultLine>source.stableThroughLine)return false;
|
|
98
|
+
if(expected.kind==='ui-write') {
|
|
99
|
+
const input=call.input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
|
|
100
|
+
return typeof file==='string'&&path.resolve(call.cwd||project,file)===expected.file&&G.uiWrite({...call,cwd:call.cwd||project},entry.directory);
|
|
101
|
+
}
|
|
102
|
+
const parsed=nativeCommand(call.name,call.input,call.cwd||project);
|
|
103
|
+
return expected.kind==='stage'&&parsed?.root===entry.directory&&parsed.operation===expected.operation;
|
|
104
|
+
});
|
|
105
|
+
for(const call of matches) {
|
|
106
|
+
const stage=expected.kind==='stage'?expected.stage:null;
|
|
107
|
+
if(stage&&X.check(entry.directory,stage,require('./native-pilot-lib.cjs')).status!=='passed')continue;
|
|
108
|
+
const retry={...expected,toolCallId:call.toolCallId,resultDigest:call.resultDigest,requestLine:call.requestLine,resultLine:call.resultLine};
|
|
109
|
+
const answer=Recovery.record(index,{source,sessionId,directory:entry.directory,event:'PostToolUse',eventKey:E.sha(JSON.stringify(['persisted-native-result',call.toolCallId,call.resultDigest])),stage,policy:X.policy(entry.directory),resolved:stage?['execution-evidence']:[],retrySucceeded:true,retry},{continue:true});
|
|
110
|
+
if(answer.recovery&&JSON.stringify(answer.recovery)!==JSON.stringify(entry.recovery)){entry.recovery=answer.recovery;changed=true;}
|
|
111
|
+
if(entry.recovery?.status==='recovered')break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return changed;
|
|
115
|
+
}
|
|
116
|
+
function dispatch(payload,options) {
|
|
117
|
+
payload={...payload,tool_name:toolName(payload.tool_name)};
|
|
84
118
|
assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
|
|
85
119
|
if(options.event==='SessionStart')return {continue:true};
|
|
86
120
|
if(options.event==='UserPromptSubmit'){
|
|
87
121
|
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:'
|
|
122
|
+
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:'原型实施:界面实现前实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并查询实际组件。专业模式还须调用 UIUX。真实浏览器验收使用浏览器 Skill。缺少必要 Skill 时立即补调,成功返回后重试原操作。'};
|
|
89
123
|
return {continue:true};
|
|
90
124
|
}
|
|
91
125
|
let identity,installed;
|
|
92
126
|
try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
|
|
93
127
|
const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
94
128
|
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);
|
|
129
|
+
const native=nativeCommand(payload.tool_name,payload.tool_input,payload.cwd||project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,payload.cwd==='/'?project:payload.cwd||project);
|
|
96
130
|
if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
|
|
97
131
|
}
|
|
98
132
|
throw error;
|
|
@@ -105,26 +139,32 @@ function handle(payload,options) {
|
|
|
105
139
|
try {
|
|
106
140
|
source=eventSource(payload,record,project);
|
|
107
141
|
}catch(error){
|
|
108
|
-
|
|
109
|
-
|
|
142
|
+
// The outer diagnostic handler must persist and bound this failure as well;
|
|
143
|
+
// returning an uncounted denial here used to create an endless repair loop.
|
|
110
144
|
throw error;
|
|
111
145
|
}
|
|
112
146
|
const latest=source.prompts.at(-1);
|
|
113
147
|
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);
|
|
148
|
+
const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
|
|
115
149
|
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
150
|
return {continue:true};
|
|
117
151
|
}
|
|
118
152
|
const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
|
|
119
|
-
|
|
153
|
+
let index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
|
|
154
|
+
if(Object.values(index.bindings).some(entry=>['required','ready-to-retry'].includes(entry.recovery?.status))) {
|
|
155
|
+
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
156
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
|
|
157
|
+
try{index=JSON.parse(C.read(file));if(settleNativeRetries(index,source,{project,sessionId:payload.session_id}))saveIndex(stateDirectory,index);}
|
|
158
|
+
finally{fs.rmdirSync(lock);}
|
|
159
|
+
}
|
|
120
160
|
const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
|
|
121
161
|
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
162
|
const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
|
|
123
163
|
const hostAt=command.argv.indexOf('--host');
|
|
124
164
|
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
165
|
}
|
|
126
|
-
const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
127
|
-
|
|
166
|
+
const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
|
|
167
|
+
let existing=directory?index.bindings[directory]:null;
|
|
128
168
|
const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
|
|
129
169
|
let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
|
|
130
170
|
const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
|
|
@@ -132,8 +172,10 @@ function handle(payload,options) {
|
|
|
132
172
|
const recoveryContext=target=>{
|
|
133
173
|
const stagePassed=stage&&options.event!=='Stop'&&X.check(target,stage,require('./native-pilot-lib.cjs')).status==='passed';
|
|
134
174
|
const retryCall=options.event==='PostToolUse'&&callbackCall?.success&&(stagePassed||G.uiWrite({...callbackCall,cwd:callbackCall.cwd||cwd},target))?callbackCall:null;
|
|
135
|
-
|
|
136
|
-
|
|
175
|
+
const input=payload.tool_input||{},uiFile=input.file_path||input.filePath||input.path||input.TargetFile;
|
|
176
|
+
const expectedRetry=stage?{kind:'stage',stage,operation:options.event==='Stop'?'finalize':command.operation}:G.uiWrite({name:payload.tool_name,input,cwd},target)?{kind:'ui-write',file:path.resolve(cwd,uiFile)}:null;
|
|
177
|
+
return {source,sessionId:payload.session_id,directory:target,event:options.event,eventKey,stage,expectedRetry,policy:X.policy(target),resolved:[...(completedBegin(source,existing)?['execution-begin']:[]),...(stagePassed?['execution-evidence']:[])],retrySucceeded:!!retryCall,
|
|
178
|
+
...(retryCall?{retry:{toolCallId:retryCall.toolCallId,resultDigest:retryCall.resultDigest,requestLine:retryCall.requestLine,resultLine:retryCall.resultLine,...(stagePassed?{kind:'stage',stage,operation:command.operation}:{kind:'ui-write',file:path.resolve(cwd,uiFile)})}}:{})};
|
|
137
179
|
};
|
|
138
180
|
const respond=answer=>{
|
|
139
181
|
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
@@ -141,37 +183,52 @@ function handle(payload,options) {
|
|
|
141
183
|
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
184
|
finally{fs.rmdirSync(lock);}
|
|
143
185
|
};
|
|
144
|
-
|
|
186
|
+
const automatic=options.event==='PreToolUse'&&directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)&&
|
|
187
|
+
(command||G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory))&&command?.operation!=='execution-begin'&&
|
|
188
|
+
(!existing||existing.entryLine!==latest.line||existing.contextLine!==source.contextLine||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
|
|
189
|
+
if(options.event==='PreToolUse'&&command?.operation==='execution-begin'||automatic) {
|
|
145
190
|
assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
|
|
146
191
|
const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
|
|
147
192
|
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
193
|
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');
|
|
194
|
+
assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text)||automatic&&require('./execution-diagnostics.cjs').intent(latest.text),'Native prototype Skill or actual shortcut entry required');
|
|
150
195
|
const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
|
|
151
196
|
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,
|
|
197
|
+
index.bindings[directory]={schemaVersion:1,...(automatic?{association:'automatic'}:{}),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
198
|
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
199
|
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
|
|
200
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
|
|
156
201
|
try {
|
|
157
202
|
const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
|
|
158
203
|
saveIndex(stateDirectory,fresh);
|
|
159
204
|
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
205
|
}finally{fs.rmdirSync(lock);}
|
|
161
|
-
|
|
206
|
+
existing=index.bindings[directory];
|
|
207
|
+
if(!automatic)return {continue:true};
|
|
162
208
|
}
|
|
163
209
|
if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
|
|
164
210
|
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
211
|
if(answer.deny)return respond(answer);
|
|
212
|
+
if(options.event==='PostToolUse'&&command&&(command.operation==='execution-begin'||stage)){
|
|
213
|
+
// IDE may not have persisted a callback's result yet. Only observed native
|
|
214
|
+
// failures produce recovery feedback; outer tool success is insufficient.
|
|
215
|
+
if(callbackCall?.resultLine&&!callbackCall.success){
|
|
216
|
+
const missing=command.operation==='execution-begin'?['execution-begin']:['execution-evidence'];
|
|
217
|
+
return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})});
|
|
218
|
+
}
|
|
219
|
+
if(command.operation==='execution-begin')return respond({continue:true});
|
|
220
|
+
}
|
|
166
221
|
if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)){
|
|
167
|
-
if(
|
|
222
|
+
if(existing?.association!=='automatic'&&!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
|
|
168
223
|
const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
|
|
169
224
|
if(checked.status==='failed'){
|
|
170
225
|
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('; ')});
|
|
226
|
+
return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})+' '+checked.errors.join('; ')});
|
|
172
227
|
}
|
|
173
228
|
}
|
|
174
|
-
if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event))
|
|
229
|
+
if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event)){
|
|
230
|
+
const recovery=respond({continue:true});if(recovery.deny||recovery.feedback||recovery.context)return recovery;
|
|
231
|
+
}
|
|
175
232
|
}
|
|
176
233
|
if(!Object.keys(index.bindings).length)return {continue:true};
|
|
177
234
|
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
@@ -193,4 +250,51 @@ function handle(payload,options) {
|
|
|
193
250
|
}finally{fs.rmdirSync(lock);}
|
|
194
251
|
return {continue:true};
|
|
195
252
|
}
|
|
253
|
+
function handle(payload,options){
|
|
254
|
+
const D=require('./execution-diagnostics.cjs');
|
|
255
|
+
payload={...payload,tool_name:toolName(payload.tool_name)};
|
|
256
|
+
let diagnostic;
|
|
257
|
+
try{
|
|
258
|
+
if(options.event==='UserPromptSubmit')D.remember(payload,options);
|
|
259
|
+
diagnostic=D.context(payload,options);
|
|
260
|
+
}catch{/* The authoritative dispatcher still checks registration and identity. */}
|
|
261
|
+
const toolEvent=['PreToolUse','PostToolUse'].includes(options.event);
|
|
262
|
+
const cwd=diagnostic&&toolCwd(payload,diagnostic.record,diagnostic.project);
|
|
263
|
+
const command=diagnostic&&nativeCommand(payload.tool_name,payload.tool_input,cwd);
|
|
264
|
+
const root=diagnostic&&(command?.root||prototypeForWrite(diagnostic.project,payload.tool_name,payload.tool_input,cwd));
|
|
265
|
+
const mutation=command&&['create','execution-begin','build','revise','replan','record-run','record-devtools-review','finalize'].includes(command.operation)||root&&G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},root);
|
|
266
|
+
if(diagnostic&&toolEvent&&['Write','Edit','MultiEdit'].includes(payload.tool_name)&&!mutation)return {continue:true};
|
|
267
|
+
if(diagnostic?.task.readOnly&&toolEvent)return options.event==='PreToolUse'&&mutation?{deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'}:{continue:true};
|
|
268
|
+
if(diagnostic&&!diagnostic.task.active&&toolEvent&&!mutation)return {continue:true};
|
|
269
|
+
if(diagnostic&&mutation&&!diagnostic.task.active)diagnostic.task={...diagnostic.task,active:true};
|
|
270
|
+
const guarded=mutation?D.guard(diagnostic,payload):null;if(guarded)return guarded;
|
|
271
|
+
if(D.blocked(diagnostic)&&toolEvent&&!mutation)return {continue:true};
|
|
272
|
+
try{
|
|
273
|
+
const answer=dispatch(payload,diagnostic?{...options,nativeProject:diagnostic.project}:options);
|
|
274
|
+
if(toolEvent&&!answer.deny&&!answer.feedback)D.observed(diagnostic,payload);
|
|
275
|
+
if(options.event==='PostToolUse'&&command?.operation==='execution-begin'&&!answer.deny&&root&&fs.existsSync(path.join(root,'qa/execution-context.json'))){
|
|
276
|
+
const pointer=F.json(path.join(root,'qa/execution-context.json'));
|
|
277
|
+
const source=diagnostic&&C.source(payload,diagnostic.record,diagnostic.project);
|
|
278
|
+
if(pointer&&completedBegin(source,{...pointer,entryLine:source.prompts.at(-1).line,contextLine:source.contextLine,directory:root}))D.recover(diagnostic);
|
|
279
|
+
}
|
|
280
|
+
return answer;
|
|
281
|
+
}catch(error){
|
|
282
|
+
if(diagnostic){const failure=D.failure(diagnostic,payload,error);if(!mutation&&failure.deny){const {deny,reason,...rest}=failure;return {...rest,continue:true,context:reason};}return failure;}
|
|
283
|
+
if(options.event==='PreToolUse') {
|
|
284
|
+
// A damaged/absent task source must not turn a protected write into exit 1
|
|
285
|
+
// (which CodeBuddy treats as non-blocking). This is a failure-only fallback,
|
|
286
|
+
// not a successful task association or invocation receipt.
|
|
287
|
+
const cwd=payload.cwd==='/'&&options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
288
|
+
if(cwd&&path.isAbsolute(cwd)) {
|
|
289
|
+
const parsed=nativeCommand(payload.tool_name,payload.tool_input,cwd),input=payload.tool_input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
|
|
290
|
+
let target=typeof file==='string'?path.dirname(path.resolve(cwd,file)):parsed?.root;
|
|
291
|
+
while(target&&target!==path.dirname(target)) {
|
|
292
|
+
if(fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target)&&(parsed||G.uiWrite({name:payload.tool_name,input,cwd},target)))return {deny:true,reason:'原型 Hook 运行异常,当前操作未放行:'+error.message+'。请停止自动重试并交由维护入口处理;原型会话不排查 Hook。'};
|
|
293
|
+
target=path.dirname(target);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
196
300
|
module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// UUID message files explicitly referenced by that index. No directory scan.
|
|
4
4
|
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
|
5
5
|
const ID=/^[a-f0-9]{32}$/;
|
|
6
|
-
const
|
|
6
|
+
const {toolName}=require('./native-tool-name.cjs');
|
|
7
7
|
function read(file) {
|
|
8
8
|
assert.equal(path.basename(file),'index.json','Expected native IDE index');
|
|
9
9
|
const directory=path.dirname(file),sessionId=path.basename(directory);
|
|
@@ -52,13 +52,13 @@ function read(file) {
|
|
|
52
52
|
if(text)latestAssistant=text;
|
|
53
53
|
for(const block of message.content.filter(b=>b.type==='tool-call')){
|
|
54
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:
|
|
55
|
+
events.push({...base,type:'assistant',message:{content:[{type:'tool_use',id:block.toolCallId,name:toolName(block.toolName),input:block.args}]}});
|
|
56
56
|
}
|
|
57
57
|
}else if(row.role==='tool'){
|
|
58
58
|
for(const block of message.content.filter(b=>b.type==='tool-result')){
|
|
59
59
|
const result=block.result,success=!block.isError&&result?.success===true&&result.status==='success';
|
|
60
60
|
events.push({...base,type:'user',message:{content:[{type:'tool_result',tool_use_id:block.toolCallId,
|
|
61
|
-
tool_name:
|
|
61
|
+
tool_name:toolName(block.toolName),content:result,is_error:!success}]}});
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
}
|
|
@@ -17,7 +17,7 @@ function validate(value,scenarioIds) {
|
|
|
17
17
|
return value;
|
|
18
18
|
}
|
|
19
19
|
function resultData(result) {
|
|
20
|
-
const text=E.
|
|
20
|
+
const text=E.resultText(result).trim();let value;
|
|
21
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
22
|
assert.ok(value&&typeof value==='object','Native JSON fact result required');return value;
|
|
23
23
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Protocol failures exist before a run binding. Never turn them into invocation proof.
|
|
3
|
+
const fs=require('node:fs'),path=require('node:path');
|
|
4
|
+
const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
|
|
5
|
+
const intent=text=>typeof text==='string'&&!C.readOnly(text)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(text)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(text));
|
|
6
|
+
const read=file=>fs.existsSync(file)?JSON.parse(C.read(file)):null;
|
|
7
|
+
function location(payload,options){
|
|
8
|
+
if(!payload.session_id||!path.isAbsolute(options.base||''))return null;
|
|
9
|
+
const installation=read(path.join(options.base,'state/i3-prototype/installation.json'));if(!installation)return null;
|
|
10
|
+
const identity=H.identify(payload,installation,options),pointer=`state/i3-prototype/tasks/${E.sha(identity.host+':'+identity.product+':'+payload.session_id).slice(0,24)}.json`;
|
|
11
|
+
const previous=read(F.inside(options.base,pointer));
|
|
12
|
+
let project=options.scope==='project'?path.dirname(options.base):previous?.project;
|
|
13
|
+
if(!project)project=H.isIDE(payload)&&payload.cwd==='/'?E.ideWorkspace(payload.transcript_path,payload.session_id):payload.cwd;
|
|
14
|
+
if(!project||!path.isAbsolute(project))return null;project=fs.realpathSync(project);
|
|
15
|
+
if(payload.cwd&&payload.cwd!=='/'){
|
|
16
|
+
const relative=path.relative(project,fs.realpathSync(payload.cwd));if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
|
|
17
|
+
}
|
|
18
|
+
const record={...installation,...identity,base:options.base,registrationDigest:E.sha(JSON.stringify(installation)),settingsDigest:E.sha(C.read(path.join(options.base,'settings.json')))};
|
|
19
|
+
return {project,record,identity,pointer,previous,directory:C.sessionDirectory(record,project,payload.session_id)};
|
|
20
|
+
}
|
|
21
|
+
function context(payload,options,{source,prompt}={}){
|
|
22
|
+
const where=location(payload,options);if(!where)return null;
|
|
23
|
+
let native=source;if(!native)try{native=C.source(payload,where.record,where.project);}catch{/* Failure diagnostics must survive unreadable native history. */}
|
|
24
|
+
const latest=native?.prompts.at(-1),text=prompt??latest?.text??where.previous?.text;
|
|
25
|
+
if(typeof text!=='string')return null;
|
|
26
|
+
const digest=latest?.digest||E.sha(text),entryLine=latest?.line||where.previous?.entryLine||null;
|
|
27
|
+
const taskKey=E.sha(JSON.stringify([payload.session_id,payload.prompt_id||null,entryLine,digest]));
|
|
28
|
+
// A callback without prompt_id reuses only the exact observed native task.
|
|
29
|
+
const task=payload.hook_event_name!=='UserPromptSubmit'&&where.previous&&where.previous.digest===digest&&where.previous.entryLine===entryLine?where.previous:
|
|
30
|
+
{schemaVersion:1,taskKey,sessionId:payload.session_id,project:where.project,host:where.identity.host,product:where.identity.product,text,digest,entryLine,active:intent(text),readOnly:C.readOnly(text),at:new Date().toISOString()};
|
|
31
|
+
return {...where,source:native,task};
|
|
32
|
+
}
|
|
33
|
+
function remember(payload,options){
|
|
34
|
+
const value=context(payload,options,{prompt:payload.prompt});if(!value)return;
|
|
35
|
+
F.write(options.base,value.pointer,value.task);
|
|
36
|
+
}
|
|
37
|
+
function state(value){return read(path.join(value.directory,'diagnostics.json'))||{schemaVersion:1,tasks:{}};}
|
|
38
|
+
function current(value){return value&&state(value).tasks[value.task.taskKey];}
|
|
39
|
+
function blocked(value){return current(value)?.status==='blocked';}
|
|
40
|
+
function message(failure){return `原型 Hook 运行异常${failure.status==='blocked'?',已达到自动恢复上限':''}:${failure.reason}。请简要报告当前原型及未验收项,由维护入口处理;不要在原型任务中检查日志、修复 Hook 或重复绑定,也不能宣称已验证。`;}
|
|
41
|
+
function failure(value,payload,error){
|
|
42
|
+
if(!value?.task.active)return {continue:true};
|
|
43
|
+
return F.withLock(value.directory,'.diagnostics.lock',()=>{
|
|
44
|
+
const store=state(value),key=value.task.taskKey;
|
|
45
|
+
const phase=/JSON|transcript|source|history|日志/i.test(error.message)?'native-source':'native-request';
|
|
46
|
+
const eventKey=E.sha(JSON.stringify([key,payload.tool_use_id||payload.call_id||payload.hook_event_name,payload.tool_name||null,payload.tool_input||null,phase]));
|
|
47
|
+
const item=store.tasks[key]||{schemaVersion:1,taskKey:key,total:0,counts:{},events:{},status:'required'};
|
|
48
|
+
if(!item.events[eventKey]&&item.status!=='blocked'){
|
|
49
|
+
if(item.total>=6||(item.counts[phase]||0)>=2)item.status='blocked';
|
|
50
|
+
else {item.total++;item.counts[phase]=(item.counts[phase]||0)+1;item.status='required';}
|
|
51
|
+
Object.assign(item,{eventKey,reason:String(error.message).slice(0,2000),phase,at:new Date().toISOString(),host:value.identity.host,product:value.identity.product,productVersion:value.identity.productVersion,
|
|
52
|
+
callMode:payload.agent_type==='ptc'||value.source?.calls.some(c=>c.name==='REPL')?'ptc-repl':value.identity.product==='ide'?'native-ide':'direct',sessionId:payload.session_id,project:value.project,
|
|
53
|
+
registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,transcriptPath:payload.transcript_path});
|
|
54
|
+
item.events[eventKey]=true;store.tasks[key]=item;F.write(value.directory,'diagnostics.json',store);
|
|
55
|
+
}
|
|
56
|
+
const result={reason:message(item),failure:item};
|
|
57
|
+
return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:payload.hook_event_name==='Stop'?{...result,feedback:true}:{...result,continue:true,context:result.reason};
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function guard(value,payload){
|
|
61
|
+
if(!value?.task.active||!blocked(value))return null;
|
|
62
|
+
const item=current(value),reason=message(item);
|
|
63
|
+
// Do not trap the whole host. Read tools and explicit state/check commands remain available.
|
|
64
|
+
const command=require('./native-command.cjs').nativeCommand(payload.tool_name,payload.tool_input,value.project);
|
|
65
|
+
const managed=command&&['create','execution-begin','revise','replan','build','record-run','record-devtools-review','finalize'].includes(command.operation)||['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(payload.tool_name);
|
|
66
|
+
if(payload.hook_event_name==='PreToolUse'&&managed)return {deny:true,reason,failure:item};
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function recover(value){
|
|
70
|
+
if(!value)return;
|
|
71
|
+
return F.withLock(value.directory,'.diagnostics.lock',()=>{const store=state(value),item=store.tasks[value.task.taskKey];
|
|
72
|
+
if(item&&item.status!=='blocked'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function observed(value,payload){
|
|
76
|
+
if(!value?.task.active)return;
|
|
77
|
+
const source=C.source(payload,value.record,value.project),at=new Date().toISOString();
|
|
78
|
+
const entry={schemaVersion:1,sessionId:payload.session_id,taskKey:value.task.taskKey,project:value.project,projectKey:C.projectKey(value.project),host:value.identity.host,product:value.identity.product,
|
|
79
|
+
productVersion:value.identity.productVersion,identitySource:value.identity.identitySource,registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,
|
|
80
|
+
source:{path:source.path,bytes:source.bytes,digest:source.digest},callMode:source.callMode||'direct',skills:Object.fromEntries(['prototype','frontend','antdv','uiux','browser'].map(key=>[key,E.loaded(source.calls,key).length])),
|
|
81
|
+
lastHook:{event:payload.hook_event_name,at}};
|
|
82
|
+
F.write(value.directory,'observation.json',entry);
|
|
83
|
+
}
|
|
84
|
+
function latest({base,project,host,record}){
|
|
85
|
+
const parent=path.join(base,'state/i3-prototype',C.projectKey(project));let found=null;
|
|
86
|
+
if(!fs.existsSync(parent))return null;
|
|
87
|
+
for(const name of fs.readdirSync(parent)){
|
|
88
|
+
if(!/^[a-f0-9]{24}$/.test(name))continue;
|
|
89
|
+
const data=read(path.join(parent,name,'diagnostics.json'));if(!data)continue;
|
|
90
|
+
for(const item of Object.values(data.tasks||{}))if(item.host===host&&item.project===project&&item.registrationDigest===record.registrationDigest&&item.settingsDigest===record.settingsDigest&&item.status!=='recovered'&&(!found||item.at>found.at))found=item;
|
|
91
|
+
}
|
|
92
|
+
if(!found)return null;const {events,...summary}=found;return summary;
|
|
93
|
+
}
|
|
94
|
+
module.exports={intent,context,remember,current,blocked,failure,guard,recover,observed,latest};
|
|
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
|
|
|
4
4
|
const fs = require('node:fs');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const P = require('./execution-policy.cjs');
|
|
7
|
+
const {toolName}=require('./native-tool-name.cjs');
|
|
7
8
|
const sha = value => crypto.createHash('sha256').update(value).digest('hex');
|
|
8
9
|
function contentText(content) {
|
|
9
10
|
if (typeof content === 'string') return content;
|
|
@@ -36,17 +37,47 @@ function nativeEvent(event) {
|
|
|
36
37
|
function successful(block) {
|
|
37
38
|
if (block.is_error || block.isError || block.success === false || block.error) return false;
|
|
38
39
|
const text = contentText(block.content ?? block.result ?? block.tool_response);
|
|
39
|
-
if (!text.trim()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
if (typeof value.stdout === 'string' && /^\s*(?:Error:|Error\b|错误[::])/i.test(value.stdout)) return false;
|
|
40
|
+
if (!text.trim()) return false;
|
|
41
|
+
function failed(value,depth=0) {
|
|
42
|
+
if(depth>8)return true;
|
|
43
|
+
if(typeof value==='string'){
|
|
44
|
+
if(/^\s*(?:Error:|Error\b|错误[::]|Unknown skill\b|Skill[^\n]*not (?:found|available))/i.test(value))return true;
|
|
45
|
+
try{return failed(JSON.parse(value),depth+1);}catch{return false;}
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if(!value||typeof value!=='object')return false;
|
|
48
|
+
if(value.type==='text')return failed(value.text,depth+1);
|
|
49
|
+
if(value.isError||value.is_error||value.success===false||value.error||['failed','error','cancelled'].includes(value.status))return true;
|
|
50
|
+
if(typeof value.exitCode==='number'&&value.exitCode!==0)return true;
|
|
51
|
+
if(typeof value.stdout==='string'&&failed(value.stdout,depth+1))return true;
|
|
52
|
+
// Only descend through native execution envelopes, not arbitrary observed
|
|
53
|
+
// application data such as {result:{status:'failed'}} from a browser check.
|
|
54
|
+
const envelope=typeof value.success==='boolean'||['success','error'].includes(value.status)||value.type==='execute_command_result';
|
|
55
|
+
return !!(envelope&&value.result!==undefined&&failed(value.result,depth+1));
|
|
56
|
+
}
|
|
57
|
+
return !failed(text);
|
|
58
|
+
}
|
|
59
|
+
function resultContent(content){
|
|
60
|
+
let value=content;
|
|
61
|
+
for(let depth=0;depth<8;depth++){
|
|
62
|
+
if(typeof value==='string'){
|
|
63
|
+
try{const parsed=JSON.parse(value);if(parsed&&typeof parsed==='object'&&(parsed.type==='text'||parsed.type==='execute_command_result'||typeof parsed.success==='boolean')){value=parsed;continue;}}catch{/* Plain text and fenced MCP facts remain text. */}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
if(Array.isArray(value)){value=contentText(value);continue;}
|
|
67
|
+
if(value?.type==='text'){value=value.text;continue;}
|
|
68
|
+
if(!value||typeof value!=='object')return value;
|
|
69
|
+
if((typeof value.success==='boolean'||['success','error'].includes(value.status)||value.type==='execute_command_result')&&value.result!==undefined){value=value.result;continue;}
|
|
70
|
+
if(typeof value.stdout==='string'){value=value.stdout;continue;}
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
function resultValue(content){
|
|
76
|
+
const value=resultContent(content);
|
|
77
|
+
if(typeof value==='string'){try{return JSON.parse(value);}catch{return null;}}
|
|
78
|
+
return value;
|
|
49
79
|
}
|
|
80
|
+
const resultText=content=>contentText(resultContent(content));
|
|
50
81
|
function parse(text) {
|
|
51
82
|
assert.ok(typeof text === 'string' && Buffer.byteLength(text) <= 64 * 1024 * 1024, 'Native transcript is too large');
|
|
52
83
|
const calls = [], requests = new Map(); let nativeMessages = 0, contextLine = 0, promptLine = 0;
|
|
@@ -65,16 +96,16 @@ function parse(text) {
|
|
|
65
96
|
if (event.type === 'assistant' && block.type === 'tool_use') {
|
|
66
97
|
assert.ok(typeof block.id === 'string' && block.id && typeof block.name === 'string', 'Native request identity missing');
|
|
67
98
|
assert.ok(!requests.has(block.id), 'Duplicate native tool call ID');
|
|
68
|
-
const input = block.input || {}, skillName =
|
|
99
|
+
const name=toolName(block.name),input = block.input || {}, skillName = name==='Skill' ? input.skill || input.name || input.command : null;
|
|
69
100
|
const normalized = typeof skillName === 'string' ? skillName.replace(/^i3-composer-agent:/, '') : null;
|
|
70
|
-
const call = { toolCallId: block.id, name
|
|
101
|
+
const call = { toolCallId: block.id, name, ...(name!==block.name?{rawName:block.name}:{}), input, skillName: normalized, skill: P.skillKey(normalized),
|
|
71
102
|
sessionId:event.sessionId||event.session_id||null, cwd:event.cwd||null, nativeRequestId:event.nativeRequestId||null, contextLine, promptLine,
|
|
72
103
|
requestLine: lineIndex + 1, requestDigest: sha(JSON.stringify(block)), success: false };
|
|
73
104
|
requests.set(block.id, call); calls.push(call);
|
|
74
105
|
} else if (event.type === 'user' && block.type === 'tool_result') {
|
|
75
106
|
const call = requests.get(block.tool_use_id);
|
|
76
107
|
assert.ok(call && !call.resultLine, 'Unmatched or duplicate native tool result');
|
|
77
|
-
if (block.tool_name !== undefined) assert.equal(block.tool_name, call.name, 'Native tool result name differs');
|
|
108
|
+
if (block.tool_name !== undefined) assert.equal(toolName(block.tool_name), call.name, 'Native tool result name differs');
|
|
78
109
|
if (call.sessionId && (event.sessionId||event.session_id)) assert.equal(call.sessionId,event.sessionId||event.session_id,'Cross-session native result');
|
|
79
110
|
Object.assign(call, { resultLine: lineIndex + 1, resultDigest: sha(JSON.stringify(block)),
|
|
80
111
|
result: block.content, success: successful(block) });
|
|
@@ -143,4 +174,4 @@ function readSource(file, roots, {mode='manual',project}={}) {
|
|
|
143
174
|
...(data.latestAssistant!==undefined?{latestAssistant:data.latestAssistant}:{}),
|
|
144
175
|
sourceVerification: mode==='auto'?'native-hook-transcript':'configured-host-transcript', tamperProof: false };
|
|
145
176
|
}
|
|
146
|
-
module.exports = { sha, contentText, successful, parse, loaded, reference, describe, readSource, cursorBytes, ideWorkspace };
|
|
177
|
+
module.exports = { sha, contentText, successful, resultContent, resultText, resultValue, parse, loaded, reference, describe, readSource, cursorBytes, ideWorkspace };
|