@givolt/i3-composer-agent 2.3.6 → 2.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/docs/codebuddy-setup.md +1 -1
- package/docs/codex-setup.md +1 -1
- package/docs/other-hosts.md +1 -1
- package/docs/workbuddy-setup.md +1 -1
- package/hooks/PROTOTYPE.md +6 -2
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/ui-prototype-buddy-lifecycle-test.js +10 -5
- package/scripts/ui-prototype-codebuddy-hook-test.js +6 -0
- package/scripts/ui-prototype-recovery-test.js +11 -2
- 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
package/README.md
CHANGED
package/docs/codebuddy-setup.md
CHANGED
|
@@ -48,7 +48,7 @@ CodeBuddy 与 WorkBuddy 项目共用 `.codebuddy/`;同版本追加客户端会
|
|
|
48
48
|
npx @givolt/i3-composer-agent --codebuddy --scope project --yes --prototype-skills uiux,antdv
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
值为 none、uiux、antdv 或 uiux,antdv;默认 none。可选工具跟随宿主和项目/全局范围,位于基址下 `tools/i3-prototype/`;旧项目 `.prototype-tools` 保持读取兼容。需要固定本次版本时使用 `npx @givolt/i3-composer-agent@2.3.
|
|
51
|
+
值为 none、uiux、antdv 或 uiux,antdv;默认 none。可选工具跟随宿主和项目/全局范围,位于基址下 `tools/i3-prototype/`;旧项目 `.prototype-tools` 保持读取兼容。需要固定本次版本时使用 `npx @givolt/i3-composer-agent@2.3.7`。已安装后可随时单独检测/补装,无需重装 i3:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
54
|
node .codebuddy/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs check --host codebuddy --scope project --project .
|
package/docs/codex-setup.md
CHANGED
|
@@ -37,6 +37,6 @@ Codex 已有原生技能路由,勿把元技能全文预加载为常驻上下
|
|
|
37
37
|
|
|
38
38
|
## 可选原型工具
|
|
39
39
|
|
|
40
|
-
原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。项目安装可使用 `npx @givolt/i3-composer-agent@2.3.
|
|
40
|
+
原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。项目安装可使用 `npx @givolt/i3-composer-agent@2.3.7 --codex --scope project --yes --prototype-skills uiux,antdv`;全局安装将 scope 改为 global。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
|
|
41
41
|
|
|
42
42
|
原型共享资料统一在项目 prototypes/:新索引 prototypes/index.json,准备范围 prototypes/.preparation/<id>/scope.json。新原生 scope.moduleIds 自动生成 appliesTo;旧索引、输入和实例保持兼容,安装升级不自动迁移。工具及运行缓存使用所选宿主基址;全局浏览器缓存按业务项目身份隔离。详细规则和只读查询/显式迁移命令见 i3-ui-prototype/references/project-layout.md。
|
package/docs/other-hosts.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## 使用本地分发目录
|
|
8
8
|
|
|
9
|
-
以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.
|
|
9
|
+
以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.7` 的对应入口。
|
|
10
10
|
|
|
11
11
|
### fx
|
|
12
12
|
|
package/docs/workbuddy-setup.md
CHANGED
|
@@ -85,7 +85,7 @@ npx @givolt/i3-composer-agent --workbuddy --scope global --yes --force
|
|
|
85
85
|
|
|
86
86
|
## 可选原型工具
|
|
87
87
|
|
|
88
|
-
原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。推荐使用 `npx @givolt/i3-composer-agent@2.3.
|
|
88
|
+
原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。推荐使用 `npx @givolt/i3-composer-agent@2.3.7 --workbuddy --scope global --yes --prototype-skills uiux,antdv`;项目安装将 scope 改为 project。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
|
|
89
89
|
|
|
90
90
|
原型共享资料统一在项目 prototypes/:新索引 prototypes/index.json,准备范围 prototypes/.preparation/<id>/scope.json。新原生 scope.moduleIds 自动生成 appliesTo;旧索引、输入和实例保持兼容,安装升级不自动迁移。工具及运行缓存使用所选宿主基址;全局浏览器缓存按业务项目身份隔离。详细规则和只读查询/显式迁移命令见 i3-ui-prototype/references/project-layout.md。
|
|
91
91
|
|
package/hooks/PROTOTYPE.md
CHANGED
|
@@ -12,8 +12,12 @@
|
|
|
12
12
|
|
|
13
13
|
安装器在 CodeBuddy/WorkBuddy 项目或全局范围注册 prototype 组。宿主自动触发;不要求原型使用者额外执行 execution-begin 或确认任务绑定。必要 Skill 的真实调用由原生 Pre/Post 回调记录,PTC/REPL 的内部工具分别记录;REPL 脚本文本不能代替调用成功。Bash 不提供 stdout 时由本包第一方入口捕获实际结果;其他只读查询可使用 native-tool-result.cjs 的本地绝对入口,不安装依赖。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
漏调检查由当前操作和固定原型策略决定,不根据最新一句话是否包含“原型”启停采集。缺少 Skill 时拦截并提示通过宿主工具补调,真实成功后重试原操作;普通追问保持同一会话/有效上下文内的调用记录和原型运行。新会话或上下文压缩后重核 Skill,输入、实例或构建变化按原有合同重核业务证据。明确只读、暂停或取消独立限制受管变更,收到 UserPromptSubmit 即生效,不等待日志写盘;普通追问不解除,明确继续实施后解除。这个授权限制不控制回调采集,也不生成新的任务身份。
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
Stop 只对本轮实际操作过的原型检查完成声明;无关文件任务不会被历史原型的验收接管。finalize 仍核验当前原型的真实调用、构建和浏览器证据。
|
|
18
|
+
|
|
19
|
+
检查命令:`npx --yes @givolt/i3-composer-agent hooks check --host codebuddy --scope project --project .`;全局将 scope 改为 global。检查只读,不启用/修复设置。registered 表示已注册;active 表示观察到当前注册的有效运行;failure 显示当前无法核验的操作、会话和原因。次数只限制重复催调/提示,不构成永久操作锁;新的真实成功结果可恢复。普通追问不清零计数,原型会话不承担 Hook 排障。
|
|
20
|
+
|
|
21
|
+
诊断、补调审计与 PTC 机器回调位于宿主基址的 state/i3-prototype/;项目/全局、宿主/会话/有效上下文和原型实例分别隔离。required、ready-to-retry、recovered、blocked 为兼容的审计显示,放行始终重新核对当前真实结果,不把状态名当额外前置。记录请求、失败或只读 SKILL.md 均不能代替成功调用;recovered 只表示观察到补调及重试,不代表业务验收。保留上游 Hooks、用户注册、旧 HTML 和已有原型;旧 execution-* 命令仅供兼容,不是新流程的前置。
|
|
18
22
|
|
|
19
23
|
CodeBuddy 项目/全局 × IDE/CLI(CLI direct/PTC)真实注册、触发、必要 Skill 调用、缺失补调和失败展示必须分别验收。自动化回放不能冒充真实宿主;正式发布门禁在仓库维护工具中执行,不放到业务原型 Skill 内。
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -54,6 +54,8 @@ for(const [host,client] of [['codebuddy','CodeBuddyCLI'],['codebuddy','CodeBuddy
|
|
|
54
54
|
});
|
|
55
55
|
test('Stop asks for missing Skills twice, then reports blocked without an endless loop',t=>{
|
|
56
56
|
const f=fixture(t,'codebuddy','project','CodeBuddyCLI');
|
|
57
|
+
const input={file_path:path.join(f.directory,'app/src/Stopped.vue'),content:'fixture'};
|
|
58
|
+
const attempted=f.request('Write',input);f.result(attempted,'Synthetic operation was not completed',true);
|
|
57
59
|
for(let n=0;n<3;n++){
|
|
58
60
|
f.append({type:'assistant',message:{content:'已完成,可以交付 '+n}});
|
|
59
61
|
const answer=f.hook('Stop');
|
|
@@ -62,13 +64,16 @@ test('Stop asks for missing Skills twice, then reports blocked without an endles
|
|
|
62
64
|
f.append({type:'user',message:{content:'先分析,不改代码'}});
|
|
63
65
|
const answer=f.tool('Write',{file_path:path.join(f.directory,'app/src/NoWrite.vue'),content:'no'});assert.equal(answer.decision.deny,true);assert.match(answer.decision.reason,/read-only/);
|
|
64
66
|
});
|
|
65
|
-
test('
|
|
67
|
+
test('late successful Skills release writes after the reminder limit without granting delivery',t=>{
|
|
66
68
|
const f=fixture(t,'codebuddy','project','CodeBuddyCLI');
|
|
67
|
-
for(let n=0;n<3;n++){f.append({type:'assistant',message:{content:'已完成,可以交付 '+n}});f.hook('Stop');}
|
|
68
|
-
f.tool('Skill',{skill:'i3-frontend-ui-engineering'});f.tool('Skill',{skill:'antdv'});
|
|
69
69
|
const file=path.join(f.directory,'app/src/AfterBudget.vue');
|
|
70
|
-
|
|
71
|
-
assert.equal(
|
|
70
|
+
for(let n=0;n<3;n++)assert.equal(f.tool('Write',{file_path:file,content:'fixture'}).decision.deny,true);
|
|
71
|
+
assert.equal(fs.existsSync(file),false);assert.equal(f.read().bindings[f.directory].recovery.status,'blocked');
|
|
72
|
+
f.tool('Skill',{skill:'i3-frontend-ui-engineering'});f.tool('Skill',{skill:'antdv'});
|
|
73
|
+
const answer=f.tool('Write',{file_path:file,content:'fixture'},'written',()=>fs.writeFileSync(file,'fixture'));
|
|
74
|
+
assert.notEqual(answer.decision.deny,true);assert.equal(fs.readFileSync(file,'utf8'),'fixture');assert.equal(f.read().bindings[f.directory].recovery.status,'recovered');
|
|
75
|
+
assert.equal(f.read().bindings[f.directory].recovery.total,2,'Keep reminder counts as audit history');
|
|
76
|
+
assert.equal(X.check(f.directory,'delivery',lib).status,'failed','Successful loading and writing do not replace browser acceptance');
|
|
72
77
|
});
|
|
73
78
|
test('a duplicated native callback does not spend recovery budget when the transcript grows',t=>{
|
|
74
79
|
const f=fixture(t,'workbuddy','project',null),input={file_path:path.join(f.directory,'app/src/Duplicate.vue'),content:'fixture'};
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
const prototypeScripts=(()=>{const fs=require('node:fs'),path=require('node:path'),file=path.join(__dirname,'lib/installation.js');return path.join(fs.existsSync(file)?require(file).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills'),'i3-ui-prototype/scripts');})();
|
|
3
3
|
const test=require('node:test'),assert=require('node:assert/strict');
|
|
4
4
|
const H=require(prototypeScripts+'/codebuddy-hook.cjs'),E=require(prototypeScripts+'/execution-evidence.cjs');
|
|
5
|
+
test('only explicit implementation instructions release a user restriction in Chinese and English',()=>{
|
|
6
|
+
const C=require(prototypeScripts+'/codebuddy-context.cjs');
|
|
7
|
+
for(const prompt of ['继续实施当前原型','继续吧','那就按这版方案实施吧,先保存任务清单','请帮我生成原型','continue','Please resume implementation'])assert.equal(C.userRestriction(prompt),false,prompt);
|
|
8
|
+
for(const prompt of ['你调用了哪三个skill','继续实施需要哪些技能?','继续解释错误','继续原型方案讨论','continue the prototype discussion','resume the prototype analysis','What skills did you use?'])assert.equal(C.userRestriction(prompt),null,prompt);
|
|
9
|
+
for(const prompt of ['先分析,不改代码','暂停当前原型实施','cancel current implementation'])assert.equal(C.userRestriction(prompt),true,prompt);
|
|
10
|
+
});
|
|
5
11
|
test('native command parsing handles quoted paths without evaluating shell substitutions or chains',()=>{
|
|
6
12
|
const c=H.nativeCommand('Bash',{command:"'/node path/node' '/skills path/native-pilot.cjs' execution-begin '/work/prototypes/a b'"},'/work');
|
|
7
13
|
assert.equal(c.root,'/work/prototypes/a b');assert.equal(c.operation,'execution-begin');
|
|
@@ -21,16 +21,25 @@ test('recovery correlates a later successful native Skill and a successful retry
|
|
|
21
21
|
const recovered=R().record(index,{...context,event:'PostToolUse',eventKey:'retry-2-result',retrySucceeded:true,retry:{toolCallId:'retry-write',requestLine:4,resultLine:5}},{continue:true});
|
|
22
22
|
assert.equal(recovered.recovery.status,'recovered');assert.equal(recovered.recovery.invocations[0].toolCallId,'real-front');
|
|
23
23
|
});
|
|
24
|
+
test('duplicate callback counts stay idempotent without replaying a stale denial after a successful Skill',()=>{
|
|
25
|
+
const index={bindings:{}},s=source(),context={source:s,sessionId:'s1',directory:'/p/prototypes/a',event:'PreToolUse',eventKey:'same-callback'};
|
|
26
|
+
R().record(index,context,{deny:true,missing:['frontend'],reason:'missing frontend'});
|
|
27
|
+
s.calls=[{skill:'frontend',sessionId:'s1',toolCallId:'front',requestLine:2,resultLine:3,success:true}];
|
|
28
|
+
const current=R().record(index,context,{continue:true});
|
|
29
|
+
assert.notEqual(current.deny,true,'Return the current gate decision, not its cached historical denial');
|
|
30
|
+
assert.equal(current.recovery.total,1);assert.equal(current.recovery.status,'ready-to-retry');
|
|
31
|
+
});
|
|
24
32
|
test('failed and stale Skill results do not recover and Stop correction is bounded across begin calls',()=>{
|
|
25
33
|
const index={bindings:{}},s=source(),context={source:s,sessionId:'s1',directory:'/p/prototypes/a',event:'Stop'};
|
|
26
34
|
s.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:false}];
|
|
27
35
|
const feedback={feedback:true,missing:['frontend'],reason:'load frontend'};
|
|
28
36
|
for(let n=1;n<=2;n++)assert.equal(R().record(index,{...context,eventKey:'stop-'+n},feedback).feedback,true);
|
|
29
37
|
index.bindings={};
|
|
30
|
-
const exhausted=R().record(index,{...context,eventKey:'stop-3'},feedback);assert.equal(exhausted.feedback,undefined);assert.equal(exhausted.recovery.status,'blocked');assert.match(exhausted.context
|
|
38
|
+
const exhausted=R().record(index,{...context,eventKey:'stop-3'},feedback);assert.equal(exhausted.feedback,undefined);assert.equal(exhausted.recovery.status,'blocked');assert.match(exhausted.context,/停止重复催调/);
|
|
31
39
|
const denied=R().record(index,{...context,event:'PreToolUse',eventKey:'write-3'},{deny:true,missing:['frontend'],reason:'load frontend'});assert.equal(denied.deny,true);
|
|
32
40
|
s.contextLine=10;s.calls=[{skill:'frontend',sessionId:'s1',requestLine:2,resultLine:3,success:true}];
|
|
33
|
-
assert.notEqual(R().record(index,{...context,eventKey:'old-load'},{continue:true}).recovery
|
|
41
|
+
assert.notEqual(R().record(index,{...context,eventKey:'old-load'},{continue:true}).recovery?.status,'recovered');
|
|
42
|
+
assert.ok(R().missing({source:s,binding:{sessionId:'s1',association:'automatic'},policy:P.create({id:P.AUTO_ID,mode:'template'})}).includes('frontend'),'Pre-compaction Skill success remains invalid in the new context');
|
|
34
43
|
});
|
|
35
44
|
test('flat native Hook feedback remains metadata and cannot reset the human task or recovery budget',()=>{
|
|
36
45
|
const rows=[{type:'message',role:'user',sessionId:'s1',content:[{type:'input_text',text:'实现原型'}]},{type:'message',role:'user',sessionId:'s1',providerData:{isMeta:true},content:[{type:'input_text',text:'补调 frontend 后继续'}]}];
|
|
@@ -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;
|