@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.
@@ -6,7 +6,7 @@
6
6
  "plugins": [
7
7
  {
8
8
  "name": "i3-composer-agent",
9
- "version": "2.3.6",
9
+ "version": "2.3.7",
10
10
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
11
11
  "source": {
12
12
  "source": "local",
@@ -8,7 +8,7 @@
8
8
  "plugins": [
9
9
  {
10
10
  "name": "i3-composer-agent",
11
- "version": "2.3.6",
11
+ "version": "2.3.7",
12
12
  "source": "./",
13
13
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
14
14
  "homepage": "https://github.com/49780522/i3-composer-agent",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "author": {
6
6
  "name": "Givolt"
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i3 Composer Agent v2.3.6
1
+ # i3 Composer Agent v2.3.7
2
2
 
3
3
  **Intelligence. Integration. Innovation.**
4
4
 
@@ -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.6`。已安装后可随时单独检测/补装,无需重装 i3:
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 .
@@ -37,6 +37,6 @@ Codex 已有原生技能路由,勿把元技能全文预加载为常驻上下
37
37
 
38
38
  ## 可选原型工具
39
39
 
40
- 原型辅助工具位于本宿主基址的 `tools/i3-prototype/`,支持项目和全局范围;旧项目 `.prototype-tools` 继续读取兼容。项目安装可使用 `npx @givolt/i3-composer-agent@2.3.6 --codex --scope project --yes --prototype-skills uiux,antdv`;全局安装将 scope 改为 global。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
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。
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## 使用本地分发目录
8
8
 
9
- 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.6` 的对应入口。
9
+ 以下命令使用完整分发包解压目录里的 CLI,适用于本地测试和离线接入。`/absolute/path/to/package` 均替换为实际解压目录;已发布版本也可使用 `npx @givolt/i3-composer-agent@2.3.7` 的对应入口。
10
10
 
11
11
  ### fx
12
12
 
@@ -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.6 --workbuddy --scope global --yes --prototype-skills uiux,antdv`;项目安装将 scope 改为 project。可选依赖默认 none,后续也可使用已安装 Skill 的 prototype-dependencies.cjs 单独检测/补装。完整用法见原型 Skill 的 references/optional-skills.md。
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
 
@@ -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
- 检查命令:`npx --yes @givolt/i3-composer-agent hooks check --host codebuddy --scope project --project .`;全局将 scope 改为 global。检查只读,不启用/修复设置。registered 表示已注册;active 表示观察到当前注册的有效运行;failure 显示已触发但处理失败的原因、会话、模式、次数和恢复状态。两次同类恢复未成功后转 blocked,不会因重试旧 begin 清零。只读维护不消耗恢复次数。
15
+ 漏调检查由当前操作和固定原型策略决定,不根据最新一句话是否包含“原型”启停采集。缺少 Skill 时拦截并提示通过宿主工具补调,真实成功后重试原操作;普通追问保持同一会话/有效上下文内的调用记录和原型运行。新会话或上下文压缩后重核 Skill,输入、实例或构建变化按原有合同重核业务证据。明确只读、暂停或取消独立限制受管变更,收到 UserPromptSubmit 即生效,不等待日志写盘;普通追问不解除,明确继续实施后解除。这个授权限制不控制回调采集,也不生成新的任务身份。
16
16
 
17
- 状态与 PTC 机器回调位于宿主基址的 state/i3-prototype/;它们是诊断/证据记录,不是业务项目的原型目录。项目/全局、宿主/会话和当前用户任务分别隔离。保留上游 Hooks、用户注册、旧 HTML 和已有原型;旧 execution-* 命令仅供兼容,不是新流程的前置。
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
@@ -444,7 +444,7 @@
444
444
  "scripts/validate-versions.js"
445
445
  ]
446
446
  },
447
- "version": "2.3.6",
447
+ "version": "2.3.7",
448
448
  "repository": {
449
449
  "type": "git",
450
450
  "url": "https://github.com/49780522/i3-composer-agent.git"
package/plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i3-composer-agent",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -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('exhausted recovery cannot silently reopen writes after late Skill loads in the same human task',t=>{
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
- const answer=f.tool('Write',{file_path:file,content:'no'},'written',()=>fs.writeFileSync(file,'no'));
71
- assert.equal(answer.decision.deny,true);assert.equal(fs.existsSync(file),false);assert.equal(f.read().bindings[f.directory].recovery.status,'blocked');
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.status,'recovered');
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
- 通过自然语言/using 选到工作 Skill,或实际 `/i3-prototype` 快捷入口后,新实例执行 `execution-begin <prototype-root>`。B 回查原生用户请求和工作 Skill/快捷命令,绑定规范化 projectKey、sessionId、runId、实例、输入、模式、策略与注册。只读或停止请求不能启动写任务。新会话/新请求/压缩后的上下文在恢复实施前重新 begin 并加载适用 Skill;用户手改保留。初建骨架不等于实际实施。
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
- 默认同一缺项两次、单原生任务总计六次,重复回调不增加预算,反复 begin 和内部 providerData.isMeta 的 Hook 消息不重置任务。达到上限时保持受管操作拒绝,允许真实 blocked 报告正常结束;不无限驱动 Stop。用户只读/暂停优先。入口、Skill 发现、加载成功、CLI 查询、重试成功、交付和用户认可分开记录。
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.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'};
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.entryLine!==source.prompts.at(-1)?.line||entry.contextLine!==source.contextLine)continue;
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.readOnly(latest.text)) {
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.entryLine!==latest.line||existing.contextLine!==source.contextLine||existing.inputDigest!==require('./native-pilot-lib.cjs').manifest(directory).inputDigest);
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)||automatic&&require('./execution-diagnostics.cjs').intent(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');
195
191
  const meta=F.json(path.join(directory,'prototype.json')),lib=require('./native-pilot-lib.cjs'),current=lib.manifest(directory);
196
- const runId=existing&&existing.entryLine===latest.line&&existing.inputDigest===current.inputDigest&&existing.contextLine===source.contextLine?existing.runId:crypto.randomUUID();
197
- index.bindings[directory]={schemaVersion:1,...(automatic?{association:'automatic'}:{}),runId,sessionId:payload.session_id,project,projectKey:C.projectKey(project),directory,instanceId:meta.instanceId,policyDigest:fixed.digest,inputDigest:current.inputDigest,entryLine:latest.line,promptDigest:latest.digest,contextLine:source.contextLine,
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.entryLine!==latest.line)continue;
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.readOnly&&toolEvent)return options.event==='PreToolUse'&&mutation?{deny:true,reason:'The latest user request is read-only or stopped; no prototype mutation is authorized.'}:{continue:true};
268
- if(diagnostic&&!diagnostic.task.active&&toolEvent&&!mutation)return {continue:true};
269
- if(diagnostic&&mutation&&!diagnostic.task.active)diagnostic.task={...diagnostic.task,active:true};
270
- const guarded=mutation?D.guard(diagnostic,payload):null;if(guarded)return guarded;
271
- if(D.blocked(diagnostic)&&toolEvent&&!mutation)return {continue:true};
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){const failure=D.failure(diagnostic,payload,error);if(!mutation&&failure.deny){const {deny,reason,...rest}=failure;return {...rest,continue:true,context:reason};}return failure;}
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,prompt}={}){
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
- const latest=native?.prompts.at(-1),text=prompt??latest?.text??where.previous?.text;
25
- if(typeof text!=='string')return null;
26
- const digest=latest?.digest||E.sha(text),entryLine=latest?.line||where.previous?.entryLine||null;
27
- const taskKey=E.sha(JSON.stringify([payload.session_id,payload.prompt_id||null,entryLine,digest]));
28
- // A callback without prompt_id reuses only the exact observed native task.
29
- const task=payload.hook_event_name!=='UserPromptSubmit'&&where.previous&&where.previous.digest===digest&&where.previous.entryLine===entryLine?where.previous:
30
- {schemaVersion:1,taskKey,sessionId:payload.session_id,project:where.project,host:where.identity.host,product:where.identity.product,text,digest,entryLine,active:intent(text),readOnly:C.readOnly(text),at:new Date().toISOString()};
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,{prompt:payload.prompt});if(!value)return;
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 current(value){return value&&state(value).tasks[value.task.taskKey];}
39
- function blocked(value){return current(value)?.status==='blocked';}
40
- function message(failure){return `原型 Hook 运行异常${failure.status==='blocked'?',已达到自动恢复上限':''}:${failure.reason}。请简要报告当前原型及未验收项,由维护入口处理;不要在原型任务中检查日志、修复 Hook 或重复绑定,也不能宣称已验证。`;}
46
+ function message(failure){return `原型 Hook 无法核验当前操作${failure.status==='blocked'?',已停止重复提示':''}:${failure.reason}。请简要报告未验收项,由维护入口处理;不要在原型任务中修复 Hook 或重复绑定,也不能宣称已验证。`;}
41
47
  function failure(value,payload,error){
42
- if(!value?.task.active)return {continue:true};
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}:payload.hook_event_name==='Stop'?{...result,feedback:true}:{...result,continue:true,context:result.reason};
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!=='blocked'){item.status='recovered';item.recoveredAt=new Date().toISOString();F.write(value.directory,'diagnostics.json',store);}
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?.task.active)return;
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={intent,context,remember,current,blocked,failure,guard,recover,observed,latest};
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.at(-1);same(d.binding.entryLine,prompt?.line,'Current prototype task differs');same(d.binding.promptDigest,prompt?.digest,'Current prototype task changed');
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
- // 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
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,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'])];
@@ -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.readOnly(source.prompts.at(-1).text),'Latest user request is read-only or stopped');
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||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;