@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.
Files changed (35) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +1 -1
  7. package/README.md +1 -1
  8. package/commands/i3-prototype.toml +1 -1
  9. package/docs/codebuddy-setup.md +1 -1
  10. package/docs/codex-setup.md +1 -1
  11. package/docs/other-hosts.md +1 -1
  12. package/docs/workbuddy-setup.md +1 -1
  13. package/hooks/PROTOTYPE.md +23 -0
  14. package/hooks/i3-prototype.cjs +2 -1
  15. package/package.json +1 -1
  16. package/plugin.json +1 -1
  17. package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -5
  18. package/scripts/ui-prototype-codebuddy-hook-test.js +8 -2
  19. package/scripts/ui-prototype-recovery-test.js +11 -2
  20. package/skills/i3-ui-prototype/SKILL.md +2 -8
  21. package/skills/i3-ui-prototype/references/execution-evidence.md +9 -3
  22. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +20 -3
  23. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +123 -36
  24. package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +91 -0
  25. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +46 -2
  26. package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +10 -9
  27. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +4 -3
  28. package/skills/i3-ui-prototype/scripts/native-command.cjs +1 -1
  29. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +4 -3
  30. package/skills/i3-ui-prototype/scripts/native-files.cjs +10 -1
  31. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +6 -1
  32. package/skills/i3-ui-prototype/scripts/native-tool-result.cjs +14 -0
  33. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +1 -1
  34. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +7 -3
  35. 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
- const current=payload.hook_event_name==='PreToolUse'?matches.filter(call=>!call.resultLine):matches;
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&&record.product==='ide'&&H.isIDE(payload)?8:1;
34
+ const attempts=toolEvent?8:1;
31
35
  for(let attempt=1;attempt<=attempts;attempt++) {
32
36
  try {
33
- const source=readSource(payload,record,project);
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.readOnly(source.prompts.at(-1)?.text||''))return {continue:true,reason:'latest-user-read-only'};
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(project,file));const prototypes=path.join(project,'prototypes');
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 handle(payload,options) {
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
- const target=options.event==='PreToolUse'?prototypeForWrite(project,payload.tool_name,payload.tool_input):null;
111
- if(target&&X.policy(target)&&G.uiWrite({name:payload.tool_name,input:payload.tool_input,cwd},target))return {deny:true,reason:`Native evidence unavailable for strict prototype UI write: ${error.message}`};
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.readOnly(latest.text)) {
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
- const index=JSON.parse(C.read(file)||'{"schemaVersion":1,"bindings":{}}');assert.equal(index.schemaVersion,1,'Unsupported audit session state');
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
- const existing=directory?index.bindings[directory]:null;
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
- if(options.event==='PreToolUse'&&command?.operation==='execution-begin') {
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 runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
156
- 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,
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 execution-begin');}
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
- return {continue:true};
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(!completedBegin(source,existing))return respond({deny:true,missing:['execution-begin'],reason:Recovery.instruction(['execution-begin'],{directory})});
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.entryLine!==latest.line)continue;
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
- assert.ok(command.completedBegin(d.source,d.binding),'Successful execution-begin for the current native task/context is required');
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
- // Deterministic recovery state; only native source results can resolve a Skill.
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=!N.completedBegin(source,binding);
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,prompt.line,prompt.digest,directory]));
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 {...state.events[eventKey],recovery:summary()};
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.status=context.retrySucceeded&&matchingRetry&&event==='PostToolUse'&&context.retry?.requestLine>state.feedbackLine?'recovered':'ready-to-retry';if(state.status==='recovered')state.retry={...context.retry,feedbackLine:state.feedbackLine};
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 recover=Recovery.instruction(['execution-begin'],{directory});
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.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
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||binding.entryLine!==source.prompts.at(-1)?.line||(binding.contextLine||0)!==source.contextLine)return null;
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
- if(meta.runtime.executionPolicy){commands.executionBegin=command('execution-begin');commands.executionTemplate=command('execution-template');commands.executionCheck=command('execution-check');}
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:meta.runtime.executionPolicy&&execution.status==='failed'?'resolve-execution-evidence':nextAction,commands,notes:['Design decisions and real provider queries remain the executing role responsibility.','Use the printed commands; no source-code inspection is required to recover flags or digests.','A missing browser provider requires an existing Playwright module path; no automatic install or global changes.','Legacy invocation fields retain their original meaning. New policy checks are reported separately in execution; native-source correlation is not tamper-proof attestation.']};
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 -->';