@givolt/i3-composer-agent 2.3.5 → 2.3.7
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 +1 -1
- 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 +1 -1
- package/README.md +1 -1
- package/commands/i3-prototype.toml +1 -1
- 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 +23 -0
- package/hooks/i3-prototype.cjs +2 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -5
- package/scripts/ui-prototype-codebuddy-hook-test.js +8 -2
- package/scripts/ui-prototype-recovery-test.js +11 -2
- package/skills/i3-ui-prototype/SKILL.md +2 -8
- package/skills/i3-ui-prototype/references/execution-evidence.md +9 -3
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +20 -3
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +123 -36
- package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +91 -0
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +46 -2
- package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +10 -9
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +4 -3
- package/skills/i3-ui-prototype/scripts/native-command.cjs +1 -1
- 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-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
|
@@ -19,7 +19,11 @@ function eventCall(payload,source) {
|
|
|
19
19
|
// pending call remains unsuccessful until a native result is persisted.
|
|
20
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||{});
|
|
21
21
|
});
|
|
22
|
-
|
|
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;
|
|
23
27
|
assert.equal(current.length,1,'Native Hook event does not uniquely match the current transcript request');return current[0];
|
|
24
28
|
}
|
|
25
29
|
function eventSource(payload,record,project,{readSource=C.source,pause=milliseconds=>Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,milliseconds)}={}) {
|
|
@@ -27,10 +31,13 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
|
|
|
27
31
|
// IDE can persist the current call only AFTER returning from both Hooks. Its
|
|
28
32
|
// current native prompt/generation still binds the callback. Never synthesize
|
|
29
33
|
// a successful call; A checks the persisted request/result at a later stage.
|
|
30
|
-
const attempts=toolEvent
|
|
34
|
+
const attempts=toolEvent?8:1;
|
|
31
35
|
for(let attempt=1;attempt<=attempts;attempt++) {
|
|
32
36
|
try {
|
|
33
|
-
|
|
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
|
+
}
|
|
34
41
|
let nativeRequestStatus='not-tool-event';
|
|
35
42
|
if(toolEvent) {
|
|
36
43
|
if(source.format==='codebuddy-ide-index-v1'&&record.product==='ide'&&H.isIDE(payload)) {
|
|
@@ -45,30 +52,30 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
|
|
|
45
52
|
}catch(error){if(attempt===attempts)throw error;pause(100);}
|
|
46
53
|
}
|
|
47
54
|
}
|
|
48
|
-
function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy}) {
|
|
49
|
-
if(C.
|
|
55
|
+
function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy,restricted}) {
|
|
56
|
+
if(restricted??C.restriction(source.prompts).blocked)return {continue:true,reason:'user-read-only-or-stopped'};
|
|
50
57
|
if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
|
|
51
58
|
const missing=Recovery.missing({source,binding,policy});
|
|
52
|
-
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory})};
|
|
59
|
+
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
|
|
53
60
|
}
|
|
54
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);
|
|
55
62
|
const blocked=/(?:\bblocked\b|无法继续|当前阻塞|等待用户|等待授权)/i.test(final);
|
|
56
63
|
if(event==='Stop'&&binding&&!deliveryCurrent&&!blocked&&(policy?.id===P.AUTO_ID||!stopHookActive)&&claim){
|
|
57
64
|
const missing=Recovery.missing({source,binding,policy,stage:'delivery'});if(!missing.length)missing.push('execution-evidence');
|
|
58
|
-
return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory})};
|
|
65
|
+
return {feedback:true,missing,reason:'当前原型没有有效交付回执。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
|
|
59
66
|
}
|
|
60
67
|
return {continue:true};
|
|
61
68
|
}
|
|
62
|
-
function prototypeForWrite(project,tool,input) {
|
|
69
|
+
function prototypeForWrite(project,tool,input,cwd=project) {
|
|
63
70
|
if(!['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(tool))return null;
|
|
64
71
|
const file=input?.file_path||input?.filePath||input?.path||input?.TargetFile;if(typeof file!=='string')return null;
|
|
65
|
-
let current=path.dirname(path.resolve(
|
|
72
|
+
let current=path.dirname(path.resolve(cwd,file));const prototypes=path.join(project,'prototypes');
|
|
66
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;
|
|
67
74
|
}
|
|
68
75
|
function safeProject(payload,options={},env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=E.ideWorkspace) {
|
|
69
76
|
const ideRoot=options.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/';
|
|
70
77
|
const hostProject=options.host==='workbuddy'?env.WORKBUDDY_PROJECT_DIR:env.CODEBUDDY_PROJECT_DIR;
|
|
71
|
-
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');
|
|
72
79
|
const project=real(root);
|
|
73
80
|
// IDE 4.12.2 reports '/' for absolute-path tools. Use the project registration
|
|
74
81
|
// or session-bound native workspace above; reject all other outside cwd.
|
|
@@ -81,20 +88,41 @@ function saveIndex(directory,index) {
|
|
|
81
88
|
function toolCwd(payload,record,project) {
|
|
82
89
|
return record.product==='ide'&&H.isIDE(payload)&&payload.cwd==='/'?project:payload.cwd||project;
|
|
83
90
|
}
|
|
84
|
-
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.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) {
|
|
85
117
|
payload={...payload,tool_name:toolName(payload.tool_name)};
|
|
86
118
|
assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
|
|
87
119
|
if(options.event==='SessionStart')return {continue:true};
|
|
88
|
-
if(options.event==='UserPromptSubmit'){
|
|
89
|
-
const prompt=payload.prompt||'';
|
|
90
|
-
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。成功返回后再实施;收到原型门禁拒绝时按明确缺项补调并重试,不能留到下次修订。'};
|
|
91
|
-
return {continue:true};
|
|
92
|
-
}
|
|
120
|
+
if(options.event==='UserPromptSubmit')return {continue:true};
|
|
93
121
|
let identity,installed;
|
|
94
122
|
try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
|
|
95
123
|
const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
96
124
|
if(options.event==='PreToolUse'&&project&&path.isAbsolute(project)){
|
|
97
|
-
const native=nativeCommand(payload.tool_name,payload.tool_input,project),target=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
125
|
+
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);
|
|
98
126
|
if(target&&fs.existsSync(path.join(target,'prototype.json'))&&X.policy(target))return {deny:true,reason:'原型原生宿主身份尚未核验,禁止受管写入或阶段推进:'+error.message};
|
|
99
127
|
}
|
|
100
128
|
throw error;
|
|
@@ -107,26 +135,32 @@ function handle(payload,options) {
|
|
|
107
135
|
try {
|
|
108
136
|
source=eventSource(payload,record,project);
|
|
109
137
|
}catch(error){
|
|
110
|
-
|
|
111
|
-
|
|
138
|
+
// The outer diagnostic handler must persist and bound this failure as well;
|
|
139
|
+
// returning an uncounted denial here used to create an endless repair loop.
|
|
112
140
|
throw error;
|
|
113
141
|
}
|
|
114
142
|
const latest=source.prompts.at(-1);
|
|
115
|
-
if(C.
|
|
116
|
-
const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
143
|
+
if(options.restricted??C.restriction(source.prompts).blocked) {
|
|
144
|
+
const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
|
|
117
145
|
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.'};
|
|
118
146
|
return {continue:true};
|
|
119
147
|
}
|
|
120
148
|
const stateDirectory=C.sessionDirectory(record,project,payload.session_id),file=path.join(stateDirectory,'session.json');
|
|
121
|
-
|
|
149
|
+
let index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
|
|
150
|
+
if(Object.values(index.bindings).some(entry=>['required','ready-to-retry'].includes(entry.recovery?.status))) {
|
|
151
|
+
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
152
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
|
|
153
|
+
try{index=JSON.parse(C.read(file));if(settleNativeRetries(index,source,{project,sessionId:payload.session_id}))saveIndex(stateDirectory,index);}
|
|
154
|
+
finally{fs.rmdirSync(lock);}
|
|
155
|
+
}
|
|
122
156
|
const command=nativeCommand(payload.tool_name,payload.tool_input,cwd);
|
|
123
157
|
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))){
|
|
124
158
|
const at=command.argv.indexOf('--execution-policy'),expected=installed?.schemaVersion===2?P.AUTO_ID:P.ID;
|
|
125
159
|
const hostAt=command.argv.indexOf('--host');
|
|
126
160
|
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.`};
|
|
127
161
|
}
|
|
128
|
-
const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input);
|
|
129
|
-
|
|
162
|
+
const directory=command?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
|
|
163
|
+
let existing=directory?index.bindings[directory]:null;
|
|
130
164
|
const stage=options.event==='Stop'?'delivery':stageFor[command?.operation];
|
|
131
165
|
let callbackCall=null;if(['PreToolUse','PostToolUse'].includes(options.event))try{callbackCall=eventCall(payload,source);}catch{/* Deferred IDE requests cannot attest results. */}
|
|
132
166
|
const callId=payload.tool_use_id||payload.call_id||callbackCall?.toolCallId;
|
|
@@ -145,43 +179,48 @@ function handle(payload,options) {
|
|
|
145
179
|
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;}
|
|
146
180
|
finally{fs.rmdirSync(lock);}
|
|
147
181
|
};
|
|
148
|
-
|
|
182
|
+
const automatic=options.event==='PreToolUse'&&directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)&&
|
|
183
|
+
(command||G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory))&&command?.operation!=='execution-begin'&&
|
|
184
|
+
(!existing||existing.contextLine!==source.contextLine||existing.registrationDigest!==record.registrationDigest||existing.settingsDigest!==record.settingsDigest||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
|
|
185
|
+
if(options.event==='PreToolUse'&&command?.operation==='execution-begin'||automatic) {
|
|
149
186
|
assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
|
|
150
187
|
const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
|
|
151
188
|
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');
|
|
152
189
|
const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
|
|
153
|
-
assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
|
|
190
|
+
assert.ok(automatic||loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
|
|
154
191
|
const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
|
|
155
|
-
const
|
|
156
|
-
|
|
192
|
+
const reuse=existing&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine&&existing.registrationDigest===record.registrationDigest&&existing.settingsDigest===record.settingsDigest;
|
|
193
|
+
const runId=reuse?existing.runId:crypto.randomUUID();
|
|
194
|
+
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:reuse?existing.entryLine:latest.line,promptDigest:reuse?existing.promptDigest:latest.digest,contextLine:source.contextLine,
|
|
157
195
|
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}};
|
|
158
196
|
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
159
|
-
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry
|
|
197
|
+
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
|
|
160
198
|
try {
|
|
161
199
|
const fresh=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');fresh.bindings[directory]=index.bindings[directory];
|
|
162
200
|
saveIndex(stateDirectory,fresh);
|
|
163
201
|
F.write(directory,'qa/execution-context.json',{schemaVersion:1,runId,sessionId:payload.session_id,project,base:record.base,host:identity.host,product:record.product});
|
|
164
202
|
}finally{fs.rmdirSync(lock);}
|
|
165
|
-
|
|
203
|
+
existing=index.bindings[directory];
|
|
204
|
+
if(!automatic)return {continue:true};
|
|
166
205
|
}
|
|
167
206
|
if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
|
|
168
|
-
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});
|
|
207
|
+
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,restricted:options.restricted});
|
|
169
208
|
if(answer.deny)return respond(answer);
|
|
170
209
|
if(options.event==='PostToolUse'&&command&&(command.operation==='execution-begin'||stage)){
|
|
171
210
|
// IDE may not have persisted a callback's result yet. Only observed native
|
|
172
211
|
// failures produce recovery feedback; outer tool success is insufficient.
|
|
173
212
|
if(callbackCall?.resultLine&&!callbackCall.success){
|
|
174
213
|
const missing=command.operation==='execution-begin'?['execution-begin']:['execution-evidence'];
|
|
175
|
-
return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory})});
|
|
214
|
+
return respond({feedback:true,missing,reason:`原型 ${command.operation} 实际返回失败,阶段尚未通过。`+Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})});
|
|
176
215
|
}
|
|
177
216
|
if(command.operation==='execution-begin')return respond({continue:true});
|
|
178
217
|
}
|
|
179
218
|
if(options.event==='PreToolUse'&&command&&['build','record-run','record-devtools-review','finalize'].includes(command.operation)){
|
|
180
|
-
if(
|
|
219
|
+
if(existing?.association!=='automatic'&&!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
|
|
181
220
|
const checked=X.check(directory,stage,require('./native-pilot-lib.cjs'));
|
|
182
221
|
if(checked.status==='failed'){
|
|
183
222
|
const missing=Recovery.missing({source,binding:existing,policy:fixed,stage});if(!missing.length)missing.push('execution-evidence');
|
|
184
|
-
return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory})+' '+checked.errors.join('; ')});
|
|
223
|
+
return respond({deny:true,missing,reason:Recovery.instruction(missing,{directory,automatic:existing?.association==='automatic'})+' '+checked.errors.join('; ')});
|
|
185
224
|
}
|
|
186
225
|
}
|
|
187
226
|
if(G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory)||stage&&['PreToolUse','PostToolUse'].includes(options.event)){
|
|
@@ -194,13 +233,23 @@ function handle(payload,options) {
|
|
|
194
233
|
try {
|
|
195
234
|
const fresh=JSON.parse(C.read(file));let changed=false;
|
|
196
235
|
for(const entry of Object.values(fresh.bindings)) {
|
|
197
|
-
if(entry.
|
|
236
|
+
if(entry.contextLine!==source.contextLine)continue;
|
|
198
237
|
if(entry.lastEventKey===eventKey)continue;
|
|
199
238
|
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;
|
|
200
239
|
if(options.event==='Stop') {
|
|
240
|
+
// Completion of an unrelated task must not trigger delivery of every
|
|
241
|
+
// prototype previously visited in the session. Inspect actual operations
|
|
242
|
+
// in this response, without another task lifecycle or activation flag.
|
|
243
|
+
const workedOn=source.calls.some(call=>{
|
|
244
|
+
if(call.requestLine<=latest.line)return false;
|
|
245
|
+
if(G.uiWrite({...call,cwd:call.cwd||project},entry.directory))return true;
|
|
246
|
+
const native=nativeCommand(call.name,call.input,call.cwd||project);
|
|
247
|
+
return native?.root===entry.directory&&Object.hasOwn(stageFor,native.operation);
|
|
248
|
+
});
|
|
249
|
+
if(!workedOn)continue;
|
|
201
250
|
const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
|
|
202
251
|
const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
|
|
203
|
-
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});
|
|
252
|
+
const fixed=X.policy(entry.directory),feedback=decision({event:'Stop',source,binding:entry,directory:entry.directory,deliveryCurrent:complete,policy:fixed,restricted:options.restricted,stopHookActive:payload.stop_hook_active||entry.stopFeedbackPrompt===latest.digest});
|
|
204
253
|
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;}
|
|
205
254
|
}
|
|
206
255
|
}
|
|
@@ -208,4 +257,42 @@ function handle(payload,options) {
|
|
|
208
257
|
}finally{fs.rmdirSync(lock);}
|
|
209
258
|
return {continue:true};
|
|
210
259
|
}
|
|
260
|
+
function handle(payload,options){
|
|
261
|
+
const D=require('./execution-diagnostics.cjs');
|
|
262
|
+
payload={...payload,tool_name:toolName(payload.tool_name)};
|
|
263
|
+
let diagnostic;
|
|
264
|
+
try{
|
|
265
|
+
if(options.event==='UserPromptSubmit')D.remember(payload,options);
|
|
266
|
+
diagnostic=D.context(payload,options);
|
|
267
|
+
}catch{/* The authoritative dispatcher still checks registration and identity. */}
|
|
268
|
+
const toolEvent=['PreToolUse','PostToolUse'].includes(options.event);
|
|
269
|
+
const cwd=diagnostic&&toolCwd(payload,diagnostic.record,diagnostic.project);
|
|
270
|
+
const command=diagnostic&&nativeCommand(payload.tool_name,payload.tool_input,cwd);
|
|
271
|
+
const root=diagnostic&&(command?.root||prototypeForWrite(diagnostic.project,payload.tool_name,payload.tool_input,cwd));
|
|
272
|
+
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);
|
|
273
|
+
if(diagnostic&&toolEvent&&['Write','Edit','MultiEdit'].includes(payload.tool_name)&&!mutation)return {continue:true};
|
|
274
|
+
if(diagnostic?.task.restriction.blocked&&toolEvent&&mutation)return options.event==='PreToolUse'?{deny:true,reason:'The user explicitly requested read-only work or stopped implementation; no prototype mutation is authorized.'}:{continue:true};
|
|
275
|
+
try{
|
|
276
|
+
const answer=dispatch(payload,diagnostic?{...options,nativeProject:diagnostic.project,restricted:diagnostic.task.restriction.blocked}:options);
|
|
277
|
+
if(toolEvent&&!answer.deny&&!answer.feedback){if(mutation)D.recover(diagnostic);D.observed(diagnostic,payload);}
|
|
278
|
+
return answer;
|
|
279
|
+
}catch(error){
|
|
280
|
+
if(diagnostic)return mutation?D.failure(diagnostic,payload,error):{continue:true};
|
|
281
|
+
if(options.event==='PreToolUse') {
|
|
282
|
+
// A damaged/absent task source must not turn a protected write into exit 1
|
|
283
|
+
// (which CodeBuddy treats as non-blocking). This is a failure-only fallback,
|
|
284
|
+
// not a successful task association or invocation receipt.
|
|
285
|
+
const cwd=payload.cwd==='/'&&options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
286
|
+
if(cwd&&path.isAbsolute(cwd)) {
|
|
287
|
+
const parsed=nativeCommand(payload.tool_name,payload.tool_input,cwd),input=payload.tool_input||{},file=input.file_path||input.filePath||input.path||input.TargetFile;
|
|
288
|
+
let target=typeof file==='string'?path.dirname(path.resolve(cwd,file)):parsed?.root;
|
|
289
|
+
while(target&&target!==path.dirname(target)) {
|
|
290
|
+
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。'};
|
|
291
|
+
target=path.dirname(target);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
211
298
|
module.exports={tokens,nativeCommand,eventCall,eventSource,decision,safeProject,toolCwd,handle};
|
|
@@ -0,0 +1,91 @@
|
|
|
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 read=file=>fs.existsSync(file)?JSON.parse(C.read(file)):null;
|
|
6
|
+
function location(payload,options){
|
|
7
|
+
if(!payload.session_id||!path.isAbsolute(options.base||''))return null;
|
|
8
|
+
const installation=read(path.join(options.base,'state/i3-prototype/installation.json'));if(!installation)return null;
|
|
9
|
+
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`;
|
|
10
|
+
const previous=read(F.inside(options.base,pointer));
|
|
11
|
+
let project=options.scope==='project'?path.dirname(options.base):previous?.project;
|
|
12
|
+
if(!project)project=H.isIDE(payload)&&payload.cwd==='/'?E.ideWorkspace(payload.transcript_path,payload.session_id):payload.cwd;
|
|
13
|
+
if(!project||!path.isAbsolute(project))return null;project=fs.realpathSync(project);
|
|
14
|
+
if(payload.cwd&&payload.cwd!=='/'){
|
|
15
|
+
const relative=path.relative(project,fs.realpathSync(payload.cwd));if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
|
|
16
|
+
}
|
|
17
|
+
const record={...installation,...identity,base:options.base,registrationDigest:E.sha(JSON.stringify(installation)),settingsDigest:E.sha(C.read(path.join(options.base,'settings.json')))};
|
|
18
|
+
return {project,record,identity,pointer,previous,directory:C.sessionDirectory(record,project,payload.session_id)};
|
|
19
|
+
}
|
|
20
|
+
function context(payload,options,{source}={}){
|
|
21
|
+
const where=location(payload,options);if(!where)return null;
|
|
22
|
+
let native=source;if(!native)try{native=C.source(payload,where.record,where.project);}catch{/* Failure diagnostics must survive unreadable native history. */}
|
|
23
|
+
// Diagnostics identify a session/context, never the wording of the last user
|
|
24
|
+
// message. UserPromptSubmit can precede persistence: do not combine its text
|
|
25
|
+
// with the preceding native prompt's cursor or digest.
|
|
26
|
+
const contextLine=native?.contextLine??where.previous?.contextLine??0;
|
|
27
|
+
const taskKey=E.sha(JSON.stringify([where.project,where.identity.host,where.identity.product,payload.session_id,contextLine]));
|
|
28
|
+
const task={schemaVersion:2,taskKey,sessionId:payload.session_id,project:where.project,host:where.identity.host,product:where.identity.product,contextLine,
|
|
29
|
+
restriction:C.restriction(native?.prompts,where.previous?.restriction)};
|
|
30
|
+
return {...where,source:native,task};
|
|
31
|
+
}
|
|
32
|
+
function remember(payload,options){
|
|
33
|
+
const value=context(payload,options);if(!value)return;
|
|
34
|
+
const id=payload.prompt_id||payload.generation_id;
|
|
35
|
+
const submission=id?E.sha(JSON.stringify([id,payload.prompt||''])):null;
|
|
36
|
+
const previous=value.previous?.submissions||[];
|
|
37
|
+
if(submission&&previous.includes(submission))return;
|
|
38
|
+
const control=C.userRestriction(payload.prompt||'');
|
|
39
|
+
// Restrict immediately. Releasing permission waits for the corresponding
|
|
40
|
+
// native user message, so a stale or unpersisted resume cannot grant writes.
|
|
41
|
+
if(control===true)value.task.restriction.blocked=true;
|
|
42
|
+
value.task.submissions=submission?[...previous.slice(-63),submission]:previous;
|
|
43
|
+
F.write(options.base,value.pointer,value.task);
|
|
44
|
+
}
|
|
45
|
+
function state(value){return read(path.join(value.directory,'diagnostics.json'))||{schemaVersion:1,tasks:{}};}
|
|
46
|
+
function message(failure){return `原型 Hook 无法核验当前操作${failure.status==='blocked'?',已停止重复提示':''}:${failure.reason}。请简要报告未验收项,由维护入口处理;不要在原型任务中修复 Hook 或重复绑定,也不能宣称已验证。`;}
|
|
47
|
+
function failure(value,payload,error){
|
|
48
|
+
if(!value)return {continue:true};
|
|
49
|
+
return F.withLock(value.directory,'.diagnostics.lock',()=>{
|
|
50
|
+
const store=state(value),key=value.task.taskKey;
|
|
51
|
+
const phase=/JSON|transcript|source|history|日志/i.test(error.message)?'native-source':'native-request';
|
|
52
|
+
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]));
|
|
53
|
+
const item=store.tasks[key]||{schemaVersion:1,taskKey:key,total:0,counts:{},events:{},status:'required'};
|
|
54
|
+
if(!item.events[eventKey]&&item.status!=='blocked'){
|
|
55
|
+
if(item.total>=6||(item.counts[phase]||0)>=2)item.status='blocked';
|
|
56
|
+
else {item.total++;item.counts[phase]=(item.counts[phase]||0)+1;item.status='required';}
|
|
57
|
+
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,
|
|
58
|
+
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,
|
|
59
|
+
registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,transcriptPath:payload.transcript_path});
|
|
60
|
+
item.events[eventKey]=true;store.tasks[key]=item;F.write(value.directory,'diagnostics.json',store);
|
|
61
|
+
}
|
|
62
|
+
const result={reason:message(item),failure:item};
|
|
63
|
+
return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:{continue:true,failure:item,...(item.status!=='blocked'?{context:result.reason}:{})};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function recover(value){
|
|
67
|
+
if(!value)return;
|
|
68
|
+
return F.withLock(value.directory,'.diagnostics.lock',()=>{const store=state(value),item=store.tasks[value.task.taskKey];
|
|
69
|
+
if(item&&item.status!=='recovered'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function observed(value,payload){
|
|
73
|
+
if(!value)return;
|
|
74
|
+
const source=C.source(payload,value.record,value.project),at=new Date().toISOString();
|
|
75
|
+
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,
|
|
76
|
+
productVersion:value.identity.productVersion,identitySource:value.identity.identitySource,registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,
|
|
77
|
+
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])),
|
|
78
|
+
lastHook:{event:payload.hook_event_name,at}};
|
|
79
|
+
F.write(value.directory,'observation.json',entry);
|
|
80
|
+
}
|
|
81
|
+
function latest({base,project,host,record}){
|
|
82
|
+
const parent=path.join(base,'state/i3-prototype',C.projectKey(project));let found=null;
|
|
83
|
+
if(!fs.existsSync(parent))return null;
|
|
84
|
+
for(const name of fs.readdirSync(parent)){
|
|
85
|
+
if(!/^[a-f0-9]{24}$/.test(name))continue;
|
|
86
|
+
const data=read(path.join(parent,name,'diagnostics.json'));if(!data)continue;
|
|
87
|
+
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;
|
|
88
|
+
}
|
|
89
|
+
if(!found)return null;const {events,...summary}=found;return summary;
|
|
90
|
+
}
|
|
91
|
+
module.exports={context,remember,failure,recover,observed,latest};
|
|
@@ -38,11 +38,14 @@ function identity(d) {
|
|
|
38
38
|
same(d.binding.policyDigest,d.policy.digest,'Execution policy differs');same(d.binding.inputDigest,d.current.inputDigest,'Inputs changed; begin a new execution run');
|
|
39
39
|
for(const key of ['inputDigest','candidateDigest','revision'])same(d.proof[key],key==='candidateDigest'?d.current.digest:d.current[key],`Current ${key} differs`);
|
|
40
40
|
assert.ok(d.source.prompts.some(p=>p.line===d.binding.entryLine),'Native user task source missing');
|
|
41
|
-
|
|
41
|
+
if(d.binding.association==='automatic'){
|
|
42
|
+
const prompt=d.source.prompts.find(p=>p.line===d.binding.entryLine);
|
|
43
|
+
same(prompt?.digest,d.binding.promptDigest,'Automatic execution context belongs to a different native task');
|
|
44
|
+
same(d.binding.contextLine,d.source.contextLine,'Automatic execution context changed');
|
|
45
|
+
}else assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
|
|
42
46
|
if(d.policy.id===P.AUTO_ID){same(d.binding.host,d.policy.host,'Actual native host differs from the fixed policy');assert.ok(d.binding.identitySource&&d.binding.identitySource!=='legacy-profile','Automatic policy requires actual native host identity');}
|
|
43
47
|
if(d.binding.recovery){
|
|
44
48
|
const recovery=d.binding.recovery;
|
|
45
|
-
assert.notEqual(recovery.status,'blocked','Recovery budget exhausted; a new human task is required');
|
|
46
49
|
for(const ref of recovery.invocations||[]){const call=nativeRef(d,ref);same(call.skill,ref.skill,'Recovery Skill differs');same(call.requestLine,ref.requestLine,'Recovery request cursor differs');same(call.resultLine,ref.resultLine,'Recovery result cursor differs');assert.ok(call.requestLine>ref.feedbackLine,'Recovery invocation must follow its feedback');}
|
|
47
50
|
if(recovery.status==='recovered'){
|
|
48
51
|
const retry=nativeRef(d,recovery.retry);
|
|
@@ -158,8 +161,49 @@ function browser(d) {
|
|
|
158
161
|
A.compare(A.extract(A.resultData(call.result),check.pointer),check);
|
|
159
162
|
}
|
|
160
163
|
}
|
|
164
|
+
function automatic(d,stage){
|
|
165
|
+
P.validate(d.policy);
|
|
166
|
+
const prompt=d.source.prompts.find(item=>item.line===d.binding.entryLine);
|
|
167
|
+
assert.ok(prompt,'Original prototype request is missing');same(d.binding.promptDigest,prompt.digest,'Original prototype request changed');
|
|
168
|
+
same(d.binding.inputDigest,d.current.inputDigest,'Prototype inputs changed');same(d.binding.contextLine,d.source.contextLine,'Native context changed');
|
|
169
|
+
const needs=P.requirements(d.policy,stage),calls=d.source.calls.filter(c=>c.sessionId===d.binding.sessionId&&c.success&&c.resultLine&&c.requestLine>d.source.contextLine);
|
|
170
|
+
const refs={},required=needs.filter(k=>['frontend','antdv','uiux','browser'].includes(k));
|
|
171
|
+
if(needs.includes('implementation'))required.push('frontend',...(d.policy.officialCallable?['antdv']:[]),...(d.policy.mode==='professional'?['uiux']:[]));
|
|
172
|
+
const writes=calls.filter(c=>c.requestLine>=d.binding.entryLine&&uiWrite({...c,cwd:c.cwd||d.binding.project},d.binding.directory));
|
|
173
|
+
for(const key of [...new Set(required)]){
|
|
174
|
+
const loaded=calls.filter(c=>c.skill===key),firstWrite=writes.length?Math.min(...writes.map(c=>c.requestLine)):Infinity;
|
|
175
|
+
const call=['frontend','antdv','uiux'].includes(key)?loaded.find(c=>c.resultLine<firstWrite):loaded.at(-1);
|
|
176
|
+
assert.ok(call,`Actual ${key} Skill invocation required before UI implementation; invoke it and retry this operation`);
|
|
177
|
+
if(['frontend','antdv','uiux'].includes(key)&&writes.length)assert.ok(call.resultLine<Math.min(...writes.map(c=>c.requestLine)),`${key} Skill must precede business UI writes`);
|
|
178
|
+
refs[key]={toolCallId:call.toolCallId,resultDigest:call.resultDigest};
|
|
179
|
+
}
|
|
180
|
+
if(needs.includes('implementation'))assert.ok(writes.length,'Actual business UI implementation write required');
|
|
181
|
+
if(needs.includes('build')){
|
|
182
|
+
assert.ok(d.build&&d.build.candidateDigest===d.current.digest,'Current prototype build required');
|
|
183
|
+
const built=calls.find(c=>{const commandInfo=command.nativeCommand(c.name,c.input,c.cwd||d.binding.project);const result=E.resultValue(c.result);return commandInfo?.operation==='build'&&commandInfo.root===d.binding.directory&&result?.buildId===d.build.buildId&&result?.buildDigest===d.build.buildDigest;});
|
|
184
|
+
assert.ok(built,'Actual current build result required');refs.build={toolCallId:built.toolCallId,resultDigest:built.resultDigest};
|
|
185
|
+
}
|
|
186
|
+
if(needs.includes('browser-results')){
|
|
187
|
+
const loaded=calls.filter(c=>c.skill==='browser').at(-1),built=calls.find(c=>c.toolCallId===refs.build?.toolCallId);
|
|
188
|
+
const observed=calls.filter(c=>/^mcp__.*chrome.*devtools.*__/.test(c.name)&&c.requestLine>Math.max(loaded.resultLine,built.resultLine));
|
|
189
|
+
assert.ok(observed.some(c=>/__(?:new_page|navigate_page)$/.test(c.name)),'Actual browser navigation required');
|
|
190
|
+
assert.ok(observed.some(c=>c.name.endsWith('__evaluate_script')&&E.resultText(c.result).includes(d.current.digest)),'Actual browser candidate identity required');
|
|
191
|
+
for(const method of ['take_screenshot','list_console_messages','list_network_requests','take_snapshot','performance_stop_trace'])assert.ok(observed.some(c=>c.name.endsWith('__'+method)),`Actual browser ${method} required`);
|
|
192
|
+
assert.ok(d.business&&d.business.candidateDigest===d.current.digest&&d.business.buildId===d.build.buildId,'Current business browser report required');
|
|
193
|
+
const fixed=A.validate(d.assertions,d.business.journeys.map(j=>j.id));
|
|
194
|
+
for(const scenario of fixed.scenarios)for(const check of scenario.checks){
|
|
195
|
+
const observation=d.business.generationObservations?.find(o=>o.scenarioId===scenario.scenarioId);
|
|
196
|
+
const results=observed.filter(c=>c.name.endsWith('__'+check.method)&&(!observation||observation.toolCallIds.includes(c.toolCallId)));
|
|
197
|
+
assert.ok(results.some(c=>{try{A.compare(A.extract(A.resultData(c.result),check.pointer),check);return true;}catch{return false;}}),`Actual browser assertion missing: ${scenario.scenarioId} ${check.pointer}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return {status:'passed',stage,policyDigest:d.policy.digest,runId:d.binding.runId,sessionId:d.binding.sessionId,projectKey:d.binding.projectKey,host:d.policy.host,productForm:d.binding.product,productVersion:d.binding.productVersion,installationScope:d.binding.scope,
|
|
201
|
+
operation:d.binding.operation,mode:d.policy.mode,inputDigest:d.current.inputDigest,candidateDigest:d.current.digest,revision:d.current.revision,association:'automatic',skills:refs,source:{path:d.source.path,bytes:d.source.bytes,digest:d.source.digest},
|
|
202
|
+
...(d.source.callbackJournal?{callbackJournal:d.source.callbackJournal}:{}),...(d.binding.recovery?{recovery:d.binding.recovery}:{}),invocationVerification:'native-source-correlated',tamperProof:false};
|
|
203
|
+
}
|
|
161
204
|
function evaluate(data,stage) {
|
|
162
205
|
try {
|
|
206
|
+
if(data.binding?.association==='automatic')return automatic(data,stage);
|
|
163
207
|
identity(data);const needs=P.requirements(data.policy,stage);
|
|
164
208
|
if(needs.includes('frontend')||needs.includes('implementation'))frontend(data,needs.includes('implementation'));
|
|
165
209
|
if(needs.some(n=>['antdv','official-source','uiux'].includes(n)))design(data);
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
//
|
|
2
|
+
// Reminder/audit records do not authorize operations. The current native Skill
|
|
3
|
+
// results and stage predicates decide whether an operation can proceed.
|
|
3
4
|
const E=require('./execution-evidence.cjs'),P=require('./execution-policy.cjs');
|
|
4
5
|
const N=require('./native-command.cjs');
|
|
5
6
|
const names={frontend:'i3-frontend-ui-engineering',antdv:'antdv',uiux:'ui-ux-pro-max',browser:'i3-browser-testing-with-devtools'};
|
|
6
|
-
const blockedReason='
|
|
7
|
+
const blockedReason='必要调用或验收仍缺失,已停止重复催调。请报告具体缺项;取得真实成功结果后可重试原操作,不能宣称原型已通过验收。';
|
|
7
8
|
function missing({source,binding,policy,stage}){
|
|
8
|
-
const unbound
|
|
9
|
+
const unbound=binding?.association!=='automatic'&&!N.completedBegin(source,binding);
|
|
9
10
|
const required=stage&&policy?P.requirements(policy,stage).filter(key=>Object.hasOwn(names,key)):['frontend',...(policy?.id===P.AUTO_ID?[...(policy.officialCallable?['antdv']:[]),...(policy.mode==='professional'?['uiux']:[])]:[])];
|
|
10
11
|
const sessionId=binding?.sessionId||source.calls.find(call=>call.sessionId)?.sessionId;
|
|
11
12
|
return [...(unbound?['execution-begin']:[]),...required.filter(key=>!E.loaded(source.calls,key).some(call=>call.sessionId===sessionId&&call.requestLine>source.contextLine&&(source.stableThroughLine===undefined||call.resultLine<=source.stableThroughLine)))];
|
|
12
13
|
}
|
|
13
|
-
function instruction(keys,{directory}={}){
|
|
14
|
+
function instruction(keys,{directory,automatic=false}={}){
|
|
14
15
|
const skills=keys.filter(key=>names[key]).map(key=>names[key]);
|
|
15
16
|
const begin=keys.includes('execution-begin')?`先为当前任务独立执行 execution-begin${directory?':\n'+N.beginCommand(directory)+'\n':'。'}不得拼接 cd &&、管道、重定向或 head;等待 status=bound 且退出码为 0,再重试原操作。`:'';
|
|
16
|
-
return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。':''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
|
|
17
|
+
return `${begin}${skills.length?'请立即通过当前宿主的 Skill 工具调用 '+skills.join('、')+'(插件安装使用宿主显示的完整名称),等待成功返回后重试原操作。':''}${keys.includes('execution-evidence')?(automatic?'按本次构建或交互验收返回的具体缺项修正原型后重试;调用记录由 Hooks 自动采集,不检查或修复 Hooks。':'执行 execution-check,按实际原生请求和结果补齐当前阶段证据后重试。'):''}读取 SKILL.md、自动补全提示、手填调用记录不能代替实际调用。外层工具 success 不代表原型阶段成功;内层 status=failed 或非零退出必须保持阻塞。失败时报告具体原因,不得自动安装依赖或宣称完成。`;
|
|
17
18
|
}
|
|
18
19
|
function record(index,context,answer){
|
|
19
20
|
const {source,sessionId,directory,eventKey,event}=context,prompt=source.prompts.at(-1);
|
|
20
|
-
const taskKey=E.sha(JSON.stringify([sessionId,
|
|
21
|
+
const taskKey=E.sha(JSON.stringify([sessionId,source.contextLine,directory,index.bindings?.[directory]?.runId||null]));
|
|
21
22
|
if(!answer.deny&&!answer.feedback&&!index.recoveries?.[taskKey])return answer;
|
|
22
23
|
index.recoveries||={};
|
|
23
24
|
const state=index.recoveries[taskKey]||={schemaVersion:1,total:0,counts:{},events:{},pending:{},invocations:[],status:'required'};
|
|
24
25
|
state.feedbackLine??=Math.max(prompt.line,...Object.values(state.pending).map(item=>item.line));
|
|
25
26
|
const summary=()=>({taskKey,status:state.status,total:state.total,counts:state.counts,invocations:state.invocations,...(state.expectedRetry?{expectedRetry:state.expectedRetry}:{}),...(state.retry?{retry:state.retry}:{})});
|
|
26
27
|
const remember=result=>{state.events[eventKey]=result;for(const key of Object.keys(state.events).slice(0,-128))delete state.events[key];return {...result,recovery:summary()};};
|
|
27
|
-
if(state.events[eventKey])return {...
|
|
28
|
-
if(state.status==='blocked'&&!answer.deny&&!answer.feedback)return remember(event==='PreToolUse'&&context.expectedRetry?{deny:true,reason:blockedReason}:{...answer,context:blockedReason});
|
|
28
|
+
if(state.events[eventKey]&&(answer.deny||answer.feedback))return remember({...answer});
|
|
29
29
|
if(answer.deny||answer.feedback){
|
|
30
30
|
if(context.expectedRetry&&(!state.expectedRetry||state.status==='recovered'))state.expectedRetry=context.expectedRetry;
|
|
31
31
|
const keys=[...new Set(answer.missing?.length?answer.missing:['execution-evidence'])];
|
|
@@ -49,7 +49,8 @@ function record(index,context,answer){
|
|
|
49
49
|
}
|
|
50
50
|
if(!Object.keys(state.pending).length&&state.status!=='recovered'){
|
|
51
51
|
const matchingRetry=!state.expectedRetry||Object.entries(state.expectedRetry).every(([key,value])=>context.retry?.[key]===value);
|
|
52
|
-
state.
|
|
52
|
+
const afterSkills=state.invocations.every(call=>context.retry?.requestLine>call.resultLine);
|
|
53
|
+
state.status=context.retrySucceeded&&matchingRetry&&afterSkills&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
return remember({...answer});
|
|
@@ -12,7 +12,9 @@ function policy(directory) {
|
|
|
12
12
|
const acceptance=json(inside(directory,'inputs/acceptance.json'));require('./execution-assertions.cjs').validate(json(inside(directory,'inputs/execution-assertions.json')),[...acceptance.requiredScenarios,...acceptance.frameworkScenarios]);return value;
|
|
13
13
|
}
|
|
14
14
|
function binding(directory) {
|
|
15
|
-
const
|
|
15
|
+
const metadata=inside(directory,'prototype.json');
|
|
16
|
+
const automaticExpected=fs.existsSync(metadata)&&json(metadata).runtime?.executionPolicy===P.AUTO_ID;
|
|
17
|
+
const recover=automaticExpected?'Installed Hooks collect this automatically on the next prototype UI operation. Load the required Skills and continue the UI task; report an infrastructure fault through maintenance if collection fails.':Recovery.instruction(['execution-begin'],{directory});
|
|
16
18
|
const pointer=optional(directory,'qa/execution-context.json');assert.ok(pointer?.schemaVersion===1,'No native Hook task binding; '+recover);
|
|
17
19
|
const project=fs.realpathSync(pointer.project),selected=C.select({project,product:pointer.product,host:pointer.host||'codebuddy'});assert.ok(selected,'Hooks are not enabled or are shadowed');
|
|
18
20
|
assert.equal(selected.base,pointer.base,'Effective Hook scope changed');
|
|
@@ -24,8 +26,7 @@ function binding(directory) {
|
|
|
24
26
|
C.prefix(entry.source.path,entry.source.bytes,entry.source.digest);
|
|
25
27
|
const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
|
|
26
28
|
const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
|
|
27
|
-
assert.ok(!C.
|
|
28
|
-
assert.equal(entry.entryLine,source.prompts.at(-1).line,'Native task changed; '+recover);
|
|
29
|
+
assert.ok(!C.restriction(source.prompts).blocked,'The user requested read-only work or stopped implementation');
|
|
29
30
|
assert.equal(entry.contextLine,source.contextLine,'Native context changed; '+recover);
|
|
30
31
|
return {entry,source,selected};
|
|
31
32
|
}
|
|
@@ -22,7 +22,7 @@ function beginCommand(directory){
|
|
|
22
22
|
return [process.execPath,path.join(__dirname,'native-pilot.cjs'),'execution-begin',directory].map(quote).join(' ');
|
|
23
23
|
}
|
|
24
24
|
function completedBegin(source,binding){
|
|
25
|
-
if(!binding
|
|
25
|
+
if(!binding||!source.prompts.some(prompt=>prompt.line===binding.entryLine)||(binding.contextLine||0)!==source.contextLine)return null;
|
|
26
26
|
const calls=source.calls.filter(call=>call.sessionId===binding.sessionId&&call.requestLine>=binding.entryLine&&call.requestLine>source.contextLine&&(()=>{
|
|
27
27
|
const command=nativeCommand(call.name,call.input,call.cwd||binding.project||binding.directory);
|
|
28
28
|
return command?.operation==='execution-begin'&&command.root===binding.directory;
|
|
@@ -56,14 +56,15 @@ function state(directory,options,lib) {
|
|
|
56
56
|
}
|
|
57
57
|
if(build.status==='current')commands.preview=command('preview',['--build-id',build.buildId]);
|
|
58
58
|
if(devtoolsReview.required)commands.recordDevtoolsReview=command('record-devtools-review',['--report',path.join(directory,'qa/devtools-review-input.json')]);
|
|
59
|
-
|
|
59
|
+
const executionContext=read(directory,'qa/execution-context.json');
|
|
60
|
+
if(meta.runtime.executionPolicy&&executionContext){commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
|
|
60
61
|
let generation=null;
|
|
61
62
|
if(meta.runtime.generationContract===1)try{const fixed=json(inside(directory,'inputs/generation.json'));const plan=require('./generation-contract.cjs').view(fixed);generation={presentation:fixed.presentation,pendingDecisions:fixed.pendingDecisions,pendingReferenceElements:fixed.pendingReferenceElements.length,referenceReview:plan.referenceReview,implementation:require('./generation-evidence.cjs').validateImplementation(directory,fixed,json(inside(directory,'design-system/implementation.json')),{frameworkMap:json(inside(directory,'inputs/framework.json')).componentMap})};}catch(error){generation={status:'failed',errors:[error.message]};}
|
|
62
63
|
const nextAction=ownership?'wait-for-owner':structure.status!=='passed'?'repair-structure':generation&&(generation.pendingDecisions.length||generation.pendingReferenceElements)?'resolve-generation-decisions':generation?.implementation.status==='pending'?'implement-business':meta.runtime.contentReference&&meta.business?.status!=='adapted'?'implement-business':!['reviewed','no-match','skipped'].includes(design.status)?'review-design':build.status!=='current'?'build':browser.status!=='current'?(meta.runtime.contentReference?'business-browser-review':'browser'):devtoolsReview.required&&devtoolsReview.status!=='current'?'devtools-review':deliveryCurrent?'return-result':'finalize';
|
|
63
64
|
if(generation)commands.replan=command('replan',['--scope',path.join(directory,'inputs/scope.json'),'--revision',nextRevision,...expected]);
|
|
64
65
|
const fixedScope=json(inside(directory,'inputs/scope.json'));
|
|
65
66
|
const scopes=meta.runtime.generationContract===1?{sourceFile:'inputs/scope.json',schemaVersion:fixedScope.schemaVersion,title:fixedScope.title,moduleIds:fixedScope.moduleIds,pages:fixedScope.pages.map(page=>({id:page.id,title:page.title,referencePage:page.referencePage,regions:page.regions.map(region=>({id:region.id,pattern:region.pattern||region.mode,fields:region.fields?.length||0,actions:region.actions?.length||0}))})),requirements:fixedScope.requirements.map(item=>({id:item.id,pages:item.pages,source:{type:item.source.type,reference:item.source.reference}}))}:fixedScope;
|
|
66
|
-
return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction
|
|
67
|
+
return {status:structure.status==='passed'?'ready':'failed',operation:'state',readOnly:true,directory,id:meta.id,revision:current.revision,candidateDigest:current.digest,inputDigest:current.inputDigest,identity:current.identity,instanceId:meta.instanceId,scopes,structure,...(generation?{generation}:{}),design,build,browser,devtoolsReview,execution,delivery,browserRuntime:runtime,ownership,verification:meta.verification,review,nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed prototype commands; installed Hooks execute automatically.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy execution commands remain available for compatibility; prototype work does not require manual binding or Hook diagnosis.']};
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function finalize(directory,options,lib) {
|
|
@@ -85,7 +86,7 @@ function finalize(directory,options,lib) {
|
|
|
85
86
|
const delivery={schemaVersion:1,status:'complete',operation:'finalize',id:meta.id,revision:before.revision,candidateDigest:before.digest,inputDigest:before.inputDigest,framework:meta.framework,template:meta.template,expertMode:meta.expertMode,expert:meta.expert.status,verification:'passed',review:review.status,checks:{structure:'passed',acceptance:'passed',build:'current',browser:'current'},buildId:report.buildId,buildDigest:report.buildDigest,report:latest.report,executor:report.executor,tool:report.tool,handoff:path.join(directory,'HANDOFF.md'),receipt:'qa/delivery.json',openQuestions:scope.openQuestions||[],humanConfirmationRequired:review.status!=='approved'};
|
|
86
87
|
const execution=require('./execution-runtime.cjs').requireStage(directory,'delivery',lib);
|
|
87
88
|
delivery.invocationVerification=execution.invocationVerification;delivery.tamperProof=false;
|
|
88
|
-
if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));}
|
|
89
|
+
if(execution.status==='passed'){delivery.execution=execution;delivery.runId=execution.runId;delivery.policyDigest=execution.policyDigest;delivery.checks.execution='passed';delivery.executionEvidenceDigest=execution.association==='automatic'?F.hash(JSON.stringify(execution)):F.hash(fs.readFileSync(inside(directory,'qa/execution-evidence.json')));delivery.executionEvidenceSource=execution.association==='automatic'?'automatic-native-observation':'legacy-execution-evidence';}
|
|
89
90
|
delivery.browserReportDigest=F.hash(fs.readFileSync(inside(directory,latest.report)));
|
|
90
91
|
if(devtoolsReview.required){delivery.devtoolsReview=devtoolsReview.report;delivery.devtoolsReviewDigest=devtoolsReview.reportDigest;delivery.checks.devtoolsReview='current';delivery.invocationEvidence=execution.status==='passed'?'Native response IDs correlated with the configured host transcript; not tamper-proof.':'Verify actual Skill/MCP calls independently in the host transcript';}
|
|
91
92
|
const start='<!-- native-delivery:start -->',end='<!-- native-delivery:end -->';
|