@givolt/i3-composer-agent 2.3.6 → 3.0.0
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-sop.md +19 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.gemini/commands/i3-sop.toml +19 -0
- package/README.md +37 -3
- package/commands/i3-sop.toml +19 -0
- package/docs/codebuddy-setup.md +3 -3
- package/docs/codex-setup.md +2 -2
- package/docs/other-hosts.md +2 -2
- package/docs/workbuddy-setup.md +3 -3
- package/evals/cases/i3-enterprise-sop.json +64 -0
- package/evals/fixtures/i3-enterprise-sop/brief.md +11 -0
- package/evals/zh-CN/cases/i3-enterprise-sop.json +61 -0
- package/hooks/PROTOTYPE.md +6 -2
- package/package.json +7 -1
- package/plugin.json +1 -1
- package/references/engineering-skeleton-resources/README.md +52 -0
- package/references/engineering-skeleton-resources/adoption.md +200 -0
- package/references/engineering-skeleton-resources/catalog.json +17 -0
- package/references/engineering-skeleton-resources/directory-standard.md +119 -0
- package/references/enterprise-sop-resources/README.md +165 -0
- package/references/enterprise-sop-resources/ad/README.md +70 -0
- package/references/enterprise-sop-resources/ad/assets-manifest.json +106 -0
- package/references/enterprise-sop-resources/ad/prompts/01-generate.md +58 -0
- package/references/enterprise-sop-resources/ad/prompts/02-review.md +54 -0
- package/references/enterprise-sop-resources/ad/prompts/03-revise.md +54 -0
- package/references/enterprise-sop-resources/ad/prompts/04-recheck.md +54 -0
- package/references/enterprise-sop-resources/ad/templates/AD.md +337 -0
- package/references/enterprise-sop-resources/assets-manifest.json +199 -0
- package/references/enterprise-sop-resources/brd/README.md +50 -0
- package/references/enterprise-sop-resources/brd/assets-manifest.json +94 -0
- package/references/enterprise-sop-resources/brd/prompts/01-generate.md +51 -0
- package/references/enterprise-sop-resources/brd/prompts/02-review.md +47 -0
- package/references/enterprise-sop-resources/brd/prompts/03-revise.md +47 -0
- package/references/enterprise-sop-resources/brd/prompts/04-recheck.md +47 -0
- package/references/enterprise-sop-resources/brd/templates/BRD.md +169 -0
- package/references/enterprise-sop-resources/codebuddy-project-guide.md +34 -0
- package/references/enterprise-sop-resources/controlled-handoff.md +41 -0
- package/references/enterprise-sop-resources/controlled_handoff.py +334 -0
- package/references/enterprise-sop-resources/handoff-contract.md +196 -0
- package/references/enterprise-sop-resources/handoff.py +277 -0
- package/references/enterprise-sop-resources/lifecycle-contract.md +91 -0
- package/references/enterprise-sop-resources/plan-handoff.md +73 -0
- package/references/enterprise-sop-resources/prd/README.md +137 -0
- package/references/enterprise-sop-resources/prd/assets-manifest.json +115 -0
- package/references/enterprise-sop-resources/prd/prompts/01-generate.md +64 -0
- package/references/enterprise-sop-resources/prd/prompts/02-review.md +49 -0
- package/references/enterprise-sop-resources/prd/prompts/03-revise.md +48 -0
- package/references/enterprise-sop-resources/prd/prompts/04-recheck.md +47 -0
- package/references/enterprise-sop-resources/prd/template-contracts.md +39 -0
- package/references/enterprise-sop-resources/prd/templates/AD.md +17 -0
- package/references/enterprise-sop-resources/prd/templates/PRD.md +257 -0
- package/references/enterprise-sop-resources/recovery-contract.md +65 -0
- package/references/enterprise-sop-resources/review-handoff.md +105 -0
- package/references/enterprise-sop-resources/selection-handoff.md +55 -0
- package/references/enterprise-sop-resources/selection_handoff.py +225 -0
- package/references/enterprise-sop-resources/stages.json +262 -0
- package/references/enterprise-sop-resources/technical-baseline/README.md +132 -0
- package/references/enterprise-sop-resources/technical-baseline/assets-manifest.json +101 -0
- package/references/enterprise-sop-resources/technical-baseline/baselines/PROVENANCE.md +40 -0
- package/references/enterprise-sop-resources/technical-baseline/baselines//344/274/201/344/270/232/345/272/224/347/224/250-/346/212/200/346/234/257/350/247/204/347/272/246_v0.0.2.md +295 -0
- package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/adoption.md +112 -0
- package/references/enterprise-sop-resources/technical-baseline/references/engineering-directory-v0.2.0/directory-standard.md +119 -0
- package/references/enterprise-sop-resources/technical-baseline/templates/TECHNICAL-BASELINE.md +133 -0
- package/references/enterprise-sop-resources/template-contracts.md +142 -0
- package/references/enterprise-sop-resources/testcase/README.md +61 -0
- package/references/enterprise-sop-resources/testcase/assets-manifest.json +111 -0
- package/references/enterprise-sop-resources/testcase/prompts/01-generate.md +53 -0
- package/references/enterprise-sop-resources/testcase/prompts/02-review.md +51 -0
- package/references/enterprise-sop-resources/testcase/prompts/03-revise.md +49 -0
- package/references/enterprise-sop-resources/testcase/prompts/04-recheck.md +51 -0
- package/references/enterprise-sop-resources/testcase/templates/TestCase.md +269 -0
- package/references/enterprise-sop-resources/verification/README.md +149 -0
- package/references/enterprise-sop-resources/verification/check_execution.py +185 -0
- package/references/enterprise-sop-resources/verification/check_native_execution.py +282 -0
- package/references/enterprise-sop-resources/verification/check_reads.py +348 -0
- package/references/enterprise-sop-resources/verification/review-output.schema.json +155 -0
- package/references/enterprise-sop-resources/verification/review_handoff.py +329 -0
- package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -5
- package/scripts/ui-prototype-codebuddy-hook-test.js +6 -0
- package/scripts/ui-prototype-recovery-test.js +11 -2
- package/skills/i3-enterprise-sop/SKILL.md +173 -0
- package/skills/i3-enterprise-sop/agents/openai.yaml +2 -0
- package/skills/i3-ui-prototype/SKILL.md +1 -1
- package/skills/i3-ui-prototype/references/execution-evidence.md +2 -2
- package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +18 -1
- package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +27 -29
- package/skills/i3-ui-prototype/scripts/execution-diagnostics.cjs +24 -27
- package/skills/i3-ui-prototype/scripts/execution-gates.cjs +2 -3
- package/skills/i3-ui-prototype/scripts/execution-recovery.cjs +5 -5
- package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +1 -2
- package/skills/i3-ui-prototype/scripts/native-command.cjs +1 -1
|
@@ -57,7 +57,7 @@ analyze 的交付是需求/能力覆盖、模板与框架选型、交互验收
|
|
|
57
57
|
|
|
58
58
|
已有 v2 实例先运行一次 `node <skill-root>/scripts/native-pilot.cjs state <prototype-root>`,从聚合结果恢复身份、固定需求、设计/构建/实际QA、负责人和下一步的精确命令。不要为寻找参数反复遍历全仓库、node_modules或阅读控制器内部实现。这里只读检查,不安装、启动浏览器或更新状态。
|
|
59
59
|
|
|
60
|
-
制作前实际调用所需的前端/框架/设计 Skill,真实浏览器验收使用浏览器 Skill;不能以读取 SKILL.md 代替调用。安装器注册的 Hooks 自动运行。收到缺少 Skill 的反馈时,补调该 Skill
|
|
60
|
+
制作前实际调用所需的前端/框架/设计 Skill,真实浏览器验收使用浏览器 Skill;不能以读取 SKILL.md 代替调用。安装器注册的 Hooks 自动运行。收到缺少 Skill 的反馈时,补调该 Skill、等待成功后重试原操作。同一有效上下文内的成功加载可复用,普通追问后继续原任务,不重新绑定或重复加载全部 Skill;基础设施异常只简要报告未验收项,由独立维护入口处理,不在原型任务中排查。
|
|
61
61
|
|
|
62
62
|
新 create 使用实际 `--host <codebuddy|workbuddy>`。按 PRD 和固定能力制定交互验收场景,使用 `native-pilot.cjs execution-assertions-template --scope <scope-file>` 取得场景骨架,填充预期 JSON 事实并传 `--execution-assertions <assertions-file>`。这是原型交互验收,不要求手动绑定会话或填写 Hook 调用记录。旧 HTML 和既有 execution-* 入口保持兼容。
|
|
63
63
|
|
|
@@ -48,7 +48,7 @@ node <skill-root>/scripts/native-pilot.cjs execution-assertions-template --scope
|
|
|
48
48
|
|
|
49
49
|
## 会话绑定与阶段
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
以下显式入口仅供已有使用者兼容:通过自然语言/using 选到工作 Skill,或实际 `/i3-prototype` 快捷入口后,执行 `execution-begin <prototype-root>`。B 回查原生来源,关联 projectKey、sessionId、runId、实例、输入、模式、策略与注册。只读或停止请求不能启动写任务。新会话/压缩后的上下文需重新核验适用 Skill;普通追问延续原运行,不要求重新 begin。用户手改保留。初建骨架不等于实际实施。
|
|
52
52
|
|
|
53
53
|
使用 `state` 中的 `commands.executionBegin.shell` 或拒绝反馈中的绝对路径命令,作为一次独立 Shell 调用;不加 `cd &&`、管道、重定向或 `head`。严格解析器不解释组合 Shell。成功须同时满足进程退出 0 和内层 `status=bound`;外层 `status=success`/`success:true` 不足以证明阶段成功。缺绑定、任务切换、上下文变化或输入变化时重新执行,不能由 CLI 自行写入原生绑定;A 和写入门禁回查当前任务最新一次 begin 的成功结果,失败/待返回不可复用旧成功。
|
|
54
54
|
|
|
@@ -88,7 +88,7 @@ PostToolUse 观察到已落盘的 begin/阶段失败时,通过该事件的 a
|
|
|
88
88
|
|
|
89
89
|
重试按 kind 区分 ui-write 和 stage;阶段重试绑定实际 operation、stage 与同一原型目录。两类新记录都以 feedbackLine 核对请求发生在反馈之后,旧成功回调不能恢复后续拒绝;旧无 kind 的记录继续仅按页面写入合同核验。
|
|
90
90
|
|
|
91
|
-
|
|
91
|
+
同一会话/有效上下文/原型运行默认同一缺项提示两次、累计六次;重复回调、普通追问、反复 begin 和内部 providerData.isMeta 消息不重置计数。达到上限只停止重复催调,仍缺项则拒绝当前操作,取得真实成功结果后重新按事实放行。required/ready-to-retry/recovered/blocked 保留为兼容审计显示,不构成额外许可或永久锁。允许如实报告阻塞,不无限驱动 Stop。用户只读/暂停优先。入口、Skill 发现、加载成功、CLI 查询、重试成功、交付和用户认可分开记录。
|
|
92
92
|
|
|
93
93
|
每宿主/范围实测:故意漏调 → 写入被拒且文件未被该操作修改 → 本主会话实际 Skill 成功 → 原操作重试成功;失败调用、旧上下文、假 ID、旧会话及当前构建缺项不能放行。持续缺项须在预算耗尽后报告阻塞。源测试里的合成日志只能验证解析和门禁逻辑,不证明模型会补调。原生 Hooks 没有指定下一工具的执行接口,不能承诺必然调用,也不是任意 Shell 安全隔离。
|
|
94
94
|
|
|
@@ -68,8 +68,25 @@ function readOnly(prompt) {
|
|
|
68
68
|
if(/先.{0,4}(?:分析|讨论|梳理|检查|解释)/.test(prompt)&&!/(?:然后|再|后).{0,8}(?:实现|实施|修改|继续)/.test(prompt))return true;
|
|
69
69
|
return /^\s*(?:请|先)?(?:暂停|停止|取消)(?:[,。!\s]|(?:这|当前|实施|工作|修改|生成))|^\s*(?:stop|cancel|pause)\b/i.test(prompt);
|
|
70
70
|
}
|
|
71
|
+
function userRestriction(prompt) {
|
|
72
|
+
if(readOnly(prompt))return true;
|
|
73
|
+
// Only an explicit request to resume implementation releases a restriction.
|
|
74
|
+
// Ordinary questions never turn a pause into permission to write.
|
|
75
|
+
if(typeof prompt!=='string'||/[??]|(?:为什么|如何|是否|哪些|什么|是不是|能不能|吗)/.test(prompt))return null;
|
|
76
|
+
const text=prompt.trim().replace(/^(?:(?:那就|好的?|现在|请|可以)[,,\s]*)+/,'').replace(/^please\s+/i,'');
|
|
77
|
+
const resume=/^(?:继续|恢复)(?:[。!!\s]*$|吧[。!!\s]*$|实施|实现|开发|修改|执行|生成|修订|(?:当前|本次)?原型(?:的)?(?:实施|实现|开发|修改|制作|生成|修订))/.test(text)||/^(?:开始实施|开始实现|实施吧|执行吧)/.test(text)||/^(?:按|按照|依照)[^\n]{0,40}(?:方案|计划)[,,\s]*(?:开始)?(?:实施|实现|执行)(?:吧)?(?:[,,。!!\s]|$)/.test(text)||/^(?:帮我)?(?:生成|创建|实现|修订|修改|制作)[^\n]{0,20}原型/.test(text)||/^\/i3-prototype\b[^\n]*(?:生成|实现|创建|修订|修改|继续)/.test(text)||/^(?:continue|resume)(?:[.!\s]*$|\s+(?:the prototype\s+)?(?:implementation|implementing|development|coding)\b)|^(?:proceed|start implementation)[.!\s]*$/i.test(text);
|
|
78
|
+
return resume?false:null;
|
|
79
|
+
}
|
|
80
|
+
function restriction(prompts=[],previous) {
|
|
81
|
+
const value={blocked:previous?.blocked===true,throughLine:previous?.throughLine||0};
|
|
82
|
+
for(const prompt of prompts)if(prompt.line>value.throughLine){
|
|
83
|
+
const control=userRestriction(prompt.text);if(control!==null)value.blocked=control;
|
|
84
|
+
value.throughLine=prompt.line;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
71
88
|
function prefix(file,bytes,digest) {
|
|
72
89
|
assert.ok(Number.isSafeInteger(bytes)&&bytes>0&&bytes<=64*1024*1024,'Invalid source cursor');
|
|
73
90
|
const raw=E.cursorBytes(file);assert.ok(raw.length>=bytes,'Native transcript was truncated');assert.equal(E.sha(raw.subarray(0,bytes)),digest,'Native transcript prefix changed');
|
|
74
91
|
}
|
|
75
|
-
module.exports={FILES:Object.freeze(FILES),read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,prefix};
|
|
92
|
+
module.exports={FILES:Object.freeze(FILES),read,scriptsForEntry,bundle,registration,globalBase,select,projectKey,sessionDirectory,source,readOnly,userRestriction,restriction,prefix};
|
|
@@ -52,8 +52,8 @@ function eventSource(payload,record,project,{readSource=C.source,pause=milliseco
|
|
|
52
52
|
}catch(error){if(attempt===attempts)throw error;pause(100);}
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
function decision({event,source,binding,tool,input,directory,cwd,deliveryCurrent,stopHookActive=false,policy}) {
|
|
56
|
-
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'};
|
|
57
57
|
if(event==='PreToolUse'&&G.uiWrite({name:tool,input:input||{},cwd},directory)) {
|
|
58
58
|
const missing=Recovery.missing({source,binding,policy});
|
|
59
59
|
if(missing.length)return {deny:true,missing,reason:'原型 UI 写入已被阻止。'+Recovery.instruction(missing,{directory,automatic:binding?.association==='automatic'})};
|
|
@@ -92,7 +92,7 @@ function settleNativeRetries(index,source,{project,sessionId}) {
|
|
|
92
92
|
let changed=false;
|
|
93
93
|
for(const entry of Object.values(index.bindings)) {
|
|
94
94
|
const repair=entry.recovery,expected=repair?.expectedRetry;
|
|
95
|
-
if(!expected||!['required','ready-to-retry'].includes(repair.status)||entry.
|
|
95
|
+
if(!expected||!['required','ready-to-retry'].includes(repair.status)||entry.contextLine!==source.contextLine)continue;
|
|
96
96
|
const matches=source.calls.filter(call=>{
|
|
97
97
|
if(!call.success||!call.resultLine||call.sessionId!==sessionId||call.requestLine<=entry.entryLine||source.stableThroughLine!==undefined&&call.resultLine>source.stableThroughLine)return false;
|
|
98
98
|
if(expected.kind==='ui-write') {
|
|
@@ -117,11 +117,7 @@ function dispatch(payload,options) {
|
|
|
117
117
|
payload={...payload,tool_name:toolName(payload.tool_name)};
|
|
118
118
|
assert.equal(payload.hook_event_name,options.event,'Hook event mismatch');
|
|
119
119
|
if(options.event==='SessionStart')return {continue:true};
|
|
120
|
-
if(options.event==='UserPromptSubmit'){
|
|
121
|
-
const prompt=payload.prompt||'';
|
|
122
|
-
if(!C.readOnly(prompt)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(prompt)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(prompt)))return {continue:true,context:'原型实施:界面实现前实际调用 i3-frontend-ui-engineering;使用 antDv 时加载 antdv 并查询实际组件。专业模式还须调用 UIUX。真实浏览器验收使用浏览器 Skill。缺少必要 Skill 时立即补调,成功返回后重试原操作。'};
|
|
123
|
-
return {continue:true};
|
|
124
|
-
}
|
|
120
|
+
if(options.event==='UserPromptSubmit')return {continue:true};
|
|
125
121
|
let identity,installed;
|
|
126
122
|
try{installed=options.product==='auto'?C.registration(options.base):null;identity=H.identify(payload,installed||{schemaVersion:1,product:options.product},options);}catch(error){
|
|
127
123
|
const project=options.scope==='project'?path.dirname(options.base):payload.cwd;
|
|
@@ -144,7 +140,7 @@ function dispatch(payload,options) {
|
|
|
144
140
|
throw error;
|
|
145
141
|
}
|
|
146
142
|
const latest=source.prompts.at(-1);
|
|
147
|
-
if(C.
|
|
143
|
+
if(options.restricted??C.restriction(source.prompts).blocked) {
|
|
148
144
|
const native=nativeCommand(payload.tool_name,payload.tool_input,cwd),root=native?.root||prototypeForWrite(project,payload.tool_name,payload.tool_input,cwd);
|
|
149
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.'};
|
|
150
146
|
return {continue:true};
|
|
@@ -185,16 +181,17 @@ function dispatch(payload,options) {
|
|
|
185
181
|
};
|
|
186
182
|
const automatic=options.event==='PreToolUse'&&directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)&&
|
|
187
183
|
(command||G.uiWrite({name:payload.tool_name,input:payload.tool_input||{},cwd},directory))&&command?.operation!=='execution-begin'&&
|
|
188
|
-
(!existing||existing.
|
|
184
|
+
(!existing||existing.contextLine!==source.contextLine||existing.registrationDigest!==record.registrationDigest||existing.settingsDigest!==record.settingsDigest||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
|
|
189
185
|
if(options.event==='PreToolUse'&&command?.operation==='execution-begin'||automatic) {
|
|
190
186
|
assert.ok(directory&&directory.startsWith(path.join(project,'prototypes')+path.sep),'Prototype must be in this project');
|
|
191
187
|
const fixed=X.policy(directory);assert.ok(fixed,'Existing prototype retains its original contract; do not rewrite it implicitly');
|
|
192
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');
|
|
193
189
|
const loaded=E.loaded(source.calls,'prototype').some(c=>c.requestLine>source.contextLine);
|
|
194
|
-
assert.ok(loaded||/^\s*\/i3-prototype\b/.test(latest.text)
|
|
190
|
+
assert.ok(automatic||loaded||/^\s*\/i3-prototype\b/.test(latest.text),'Native prototype Skill or actual shortcut entry required');
|
|
195
191
|
const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
|
|
196
|
-
const
|
|
197
|
-
|
|
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,
|
|
198
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}};
|
|
199
196
|
const lock=path.join(stateDirectory,'.audit.lock');fs.mkdirSync(stateDirectory,{recursive:true});
|
|
200
197
|
try{fs.mkdirSync(lock);}catch{throw new Error('Concurrent audit callback; retry the original operation');}
|
|
@@ -207,7 +204,7 @@ function dispatch(payload,options) {
|
|
|
207
204
|
if(!automatic)return {continue:true};
|
|
208
205
|
}
|
|
209
206
|
if(directory&&fs.existsSync(path.join(directory,'prototype.json'))&&X.policy(directory)) {
|
|
210
|
-
const fixed=X.policy(directory),answer=decision({event:options.event,source,binding:existing,tool:payload.tool_name,input:payload.tool_input,directory,cwd,policy:fixed});
|
|
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});
|
|
211
208
|
if(answer.deny)return respond(answer);
|
|
212
209
|
if(options.event==='PostToolUse'&&command&&(command.operation==='execution-begin'||stage)){
|
|
213
210
|
// IDE may not have persisted a callback's result yet. Only observed native
|
|
@@ -236,13 +233,23 @@ function dispatch(payload,options) {
|
|
|
236
233
|
try {
|
|
237
234
|
const fresh=JSON.parse(C.read(file));let changed=false;
|
|
238
235
|
for(const entry of Object.values(fresh.bindings)) {
|
|
239
|
-
if(entry.
|
|
236
|
+
if(entry.contextLine!==source.contextLine)continue;
|
|
240
237
|
if(entry.lastEventKey===eventKey)continue;
|
|
241
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;
|
|
242
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;
|
|
243
250
|
const lib=require('./native-pilot-lib.cjs'),state=lib.state(entry.directory);
|
|
244
251
|
const complete=state.execution.status==='passed'&&state.delivery?.status==='current'&&state.delivery.result.runId===entry.runId;
|
|
245
|
-
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});
|
|
246
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;}
|
|
247
254
|
}
|
|
248
255
|
}
|
|
@@ -264,22 +271,13 @@ function handle(payload,options){
|
|
|
264
271
|
const root=diagnostic&&(command?.root||prototypeForWrite(diagnostic.project,payload.tool_name,payload.tool_input,cwd));
|
|
265
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);
|
|
266
273
|
if(diagnostic&&toolEvent&&['Write','Edit','MultiEdit'].includes(payload.tool_name)&&!mutation)return {continue:true};
|
|
267
|
-
if(diagnostic?.task.
|
|
268
|
-
if(diagnostic&&!diagnostic.task.active&&toolEvent&&!mutation)return {continue:true};
|
|
269
|
-
if(diagnostic&&mutation&&!diagnostic.task.active)diagnostic.task={...diagnostic.task,active:true};
|
|
270
|
-
const guarded=mutation?D.guard(diagnostic,payload):null;if(guarded)return guarded;
|
|
271
|
-
if(D.blocked(diagnostic)&&toolEvent&&!mutation)return {continue:true};
|
|
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};
|
|
272
275
|
try{
|
|
273
|
-
const answer=dispatch(payload,diagnostic?{...options,nativeProject:diagnostic.project}:options);
|
|
274
|
-
if(toolEvent&&!answer.deny&&!answer.feedback)D.observed(diagnostic,payload);
|
|
275
|
-
if(options.event==='PostToolUse'&&command?.operation==='execution-begin'&&!answer.deny&&root&&fs.existsSync(path.join(root,'qa/execution-context.json'))){
|
|
276
|
-
const pointer=F.json(path.join(root,'qa/execution-context.json'));
|
|
277
|
-
const source=diagnostic&&C.source(payload,diagnostic.record,diagnostic.project);
|
|
278
|
-
if(pointer&&completedBegin(source,{...pointer,entryLine:source.prompts.at(-1).line,contextLine:source.contextLine,directory:root}))D.recover(diagnostic);
|
|
279
|
-
}
|
|
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);}
|
|
280
278
|
return answer;
|
|
281
279
|
}catch(error){
|
|
282
|
-
if(diagnostic)
|
|
280
|
+
if(diagnostic)return mutation?D.failure(diagnostic,payload,error):{continue:true};
|
|
283
281
|
if(options.event==='PreToolUse') {
|
|
284
282
|
// A damaged/absent task source must not turn a protected write into exit 1
|
|
285
283
|
// (which CodeBuddy treats as non-blocking). This is a failure-only fallback,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Protocol failures exist before a run binding. Never turn them into invocation proof.
|
|
3
3
|
const fs=require('node:fs'),path=require('node:path');
|
|
4
4
|
const F=require('./native-files.cjs'),C=require('./codebuddy-context.cjs'),E=require('./execution-evidence.cjs'),H=require('./buddy-host.cjs');
|
|
5
|
-
const intent=text=>typeof text==='string'&&!C.readOnly(text)&&(/\/i3-prototype\b(?!\s+(?:analyze|inspect|confirm-baseline))/.test(text)||/(?:生成|实现|创建|修订|修改|继续).{0,16}原型|(?:create|revise|implement).{0,20}prototype/i.test(text));
|
|
6
5
|
const read=file=>fs.existsSync(file)?JSON.parse(C.read(file)):null;
|
|
7
6
|
function location(payload,options){
|
|
8
7
|
if(!payload.session_id||!path.isAbsolute(options.base||''))return null;
|
|
@@ -18,28 +17,35 @@ function location(payload,options){
|
|
|
18
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')))};
|
|
19
18
|
return {project,record,identity,pointer,previous,directory:C.sessionDirectory(record,project,payload.session_id)};
|
|
20
19
|
}
|
|
21
|
-
function context(payload,options,{source
|
|
20
|
+
function context(payload,options,{source}={}){
|
|
22
21
|
const where=location(payload,options);if(!where)return null;
|
|
23
22
|
let native=source;if(!native)try{native=C.source(payload,where.record,where.project);}catch{/* Failure diagnostics must survive unreadable native history. */}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const task=payload.
|
|
30
|
-
|
|
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)};
|
|
31
30
|
return {...where,source:native,task};
|
|
32
31
|
}
|
|
33
32
|
function remember(payload,options){
|
|
34
|
-
const value=context(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;
|
|
35
43
|
F.write(options.base,value.pointer,value.task);
|
|
36
44
|
}
|
|
37
45
|
function state(value){return read(path.join(value.directory,'diagnostics.json'))||{schemaVersion:1,tasks:{}};}
|
|
38
|
-
function
|
|
39
|
-
function blocked(value){return current(value)?.status==='blocked';}
|
|
40
|
-
function message(failure){return `原型 Hook 运行异常${failure.status==='blocked'?',已达到自动恢复上限':''}:${failure.reason}。请简要报告当前原型及未验收项,由维护入口处理;不要在原型任务中检查日志、修复 Hook 或重复绑定,也不能宣称已验证。`;}
|
|
46
|
+
function message(failure){return `原型 Hook 无法核验当前操作${failure.status==='blocked'?',已停止重复提示':''}:${failure.reason}。请简要报告未验收项,由维护入口处理;不要在原型任务中修复 Hook 或重复绑定,也不能宣称已验证。`;}
|
|
41
47
|
function failure(value,payload,error){
|
|
42
|
-
if(!value
|
|
48
|
+
if(!value)return {continue:true};
|
|
43
49
|
return F.withLock(value.directory,'.diagnostics.lock',()=>{
|
|
44
50
|
const store=state(value),key=value.task.taskKey;
|
|
45
51
|
const phase=/JSON|transcript|source|history|日志/i.test(error.message)?'native-source':'native-request';
|
|
@@ -54,26 +60,17 @@ function failure(value,payload,error){
|
|
|
54
60
|
item.events[eventKey]=true;store.tasks[key]=item;F.write(value.directory,'diagnostics.json',store);
|
|
55
61
|
}
|
|
56
62
|
const result={reason:message(item),failure:item};
|
|
57
|
-
return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:
|
|
63
|
+
return payload.hook_event_name==='PreToolUse'?{...result,deny:true}:{continue:true,failure:item,...(item.status!=='blocked'?{context:result.reason}:{})};
|
|
58
64
|
});
|
|
59
65
|
}
|
|
60
|
-
function guard(value,payload){
|
|
61
|
-
if(!value?.task.active||!blocked(value))return null;
|
|
62
|
-
const item=current(value),reason=message(item);
|
|
63
|
-
// Do not trap the whole host. Read tools and explicit state/check commands remain available.
|
|
64
|
-
const command=require('./native-command.cjs').nativeCommand(payload.tool_name,payload.tool_input,value.project);
|
|
65
|
-
const managed=command&&['create','execution-begin','revise','replan','build','record-run','record-devtools-review','finalize'].includes(command.operation)||['Write','Edit','MultiEdit','write_to_file','replace_in_file'].includes(payload.tool_name);
|
|
66
|
-
if(payload.hook_event_name==='PreToolUse'&&managed)return {deny:true,reason,failure:item};
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
66
|
function recover(value){
|
|
70
67
|
if(!value)return;
|
|
71
68
|
return F.withLock(value.directory,'.diagnostics.lock',()=>{const store=state(value),item=store.tasks[value.task.taskKey];
|
|
72
|
-
if(item&&item.status!=='
|
|
69
|
+
if(item&&item.status!=='recovered'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
|
|
73
70
|
});
|
|
74
71
|
}
|
|
75
72
|
function observed(value,payload){
|
|
76
|
-
if(!value
|
|
73
|
+
if(!value)return;
|
|
77
74
|
const source=C.source(payload,value.record,value.project),at=new Date().toISOString();
|
|
78
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,
|
|
79
76
|
productVersion:value.identity.productVersion,identitySource:value.identity.identitySource,registrationDigest:value.record.registrationDigest,settingsDigest:value.record.settingsDigest,
|
|
@@ -91,4 +88,4 @@ function latest({base,project,host,record}){
|
|
|
91
88
|
}
|
|
92
89
|
if(!found)return null;const {events,...summary}=found;return summary;
|
|
93
90
|
}
|
|
94
|
-
module.exports={
|
|
91
|
+
module.exports={context,remember,failure,recover,observed,latest};
|
|
@@ -46,7 +46,6 @@ function identity(d) {
|
|
|
46
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');}
|
|
47
47
|
if(d.binding.recovery){
|
|
48
48
|
const recovery=d.binding.recovery;
|
|
49
|
-
assert.notEqual(recovery.status,'blocked','Recovery budget exhausted; a new human task is required');
|
|
50
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');}
|
|
51
50
|
if(recovery.status==='recovered'){
|
|
52
51
|
const retry=nativeRef(d,recovery.retry);
|
|
@@ -164,9 +163,9 @@ function browser(d) {
|
|
|
164
163
|
}
|
|
165
164
|
function automatic(d,stage){
|
|
166
165
|
P.validate(d.policy);
|
|
167
|
-
const prompt=d.source.prompts.
|
|
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
168
|
same(d.binding.inputDigest,d.current.inputDigest,'Prototype inputs changed');same(d.binding.contextLine,d.source.contextLine,'Native context changed');
|
|
169
|
-
assert.notEqual(d.binding.recovery?.status,'blocked','Skill recovery budget exhausted');
|
|
170
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);
|
|
171
170
|
const refs={},required=needs.filter(k=>['frontend','antdv','uiux','browser'].includes(k));
|
|
172
171
|
if(needs.includes('implementation'))required.push('frontend',...(d.policy.officialCallable?['antdv']:[]),...(d.policy.mode==='professional'?['uiux']:[]));
|
|
@@ -1,9 +1,10 @@
|
|
|
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
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']:[])]:[])];
|
|
@@ -17,15 +18,14 @@ function instruction(keys,{directory,automatic=false}={}){
|
|
|
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'])];
|
|
@@ -26,8 +26,7 @@ function binding(directory) {
|
|
|
26
26
|
C.prefix(entry.source.path,entry.source.bytes,entry.source.digest);
|
|
27
27
|
const source=C.source({session_id:entry.sessionId,transcript_path:entry.source.path},selected,project);
|
|
28
28
|
const prompt=source.prompts.find(p=>p.line===entry.entryLine);assert.equal(prompt?.digest,entry.promptDigest,'Original user task changed');
|
|
29
|
-
assert.ok(!C.
|
|
30
|
-
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');
|
|
31
30
|
assert.equal(entry.contextLine,source.contextLine,'Native context changed; '+recover);
|
|
32
31
|
return {entry,source,selected};
|
|
33
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;
|