@givolt/i3-composer-agent 2.3.1 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/.agents/plugins/marketplace.json +1 -1
  2. package/.claude/commands/i3-prototype.md +5 -7
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/.claude-plugin/plugin.json +1 -1
  5. package/.codex-plugin/plugin.json +1 -1
  6. package/.gemini/commands/i3-prototype.toml +5 -7
  7. package/README.md +16 -7
  8. package/bin/agent-targets.json +4 -2
  9. package/bin/i3-composer-agent.cjs +74 -7
  10. package/bin/install.cjs +16 -1
  11. package/bin/native-layout.cjs +1 -1
  12. package/bin/native-migration.cjs +27 -3
  13. package/bin/tool-selector.cjs +24 -7
  14. package/bin/wizard.cjs +89 -15
  15. package/commands/i3-prototype.toml +5 -7
  16. package/docs/codebuddy-setup.md +91 -3
  17. package/docs/codex-setup.md +6 -0
  18. package/docs/other-hosts.md +1 -1
  19. package/docs/ui-prototype-native/MAIN-SESSION-REPORT.md +66 -0
  20. package/docs/ui-prototype-native/USAGE.md +93 -0
  21. package/docs/workbuddy-setup.md +6 -0
  22. package/evals/cases/i3-frontend-ui-engineering.json +16 -0
  23. package/evals/cases/i3-ui-prototype.json +109 -0
  24. package/evals/cases/i3-using-agent-skills.json +28 -0
  25. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css +3 -0
  26. package/evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js +8 -0
  27. package/evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json +67 -0
  28. package/evals/fixtures/i3-ui-prototype/admin-list/index.html +89 -0
  29. package/evals/fixtures/i3-ui-prototype/admin-list/readme.md +52 -0
  30. package/evals/fixtures/i3-ui-prototype/admin-list/template.json +8 -0
  31. package/evals/zh-CN/cases/i3-frontend-ui-engineering.json +17 -0
  32. package/evals/zh-CN/cases/i3-ui-prototype.json +115 -0
  33. package/evals/zh-CN/cases/i3-using-agent-skills.json +30 -0
  34. package/hooks/codebuddy-upstream.cjs +93 -0
  35. package/hooks/i3-prototype.cjs +12 -0
  36. package/hooks/manage.cjs +162 -0
  37. package/package.json +61 -1
  38. package/plugin.json +1 -1
  39. package/scripts/design-provider-test.js +7 -0
  40. package/scripts/enterprise-template-test.js +27 -1
  41. package/scripts/lib/prototype-fixture.cjs +17 -0
  42. package/scripts/prototype-cache-test.js +55 -0
  43. package/scripts/prototype-generation-entry-test.js +6 -0
  44. package/scripts/prototype-generation-evidence-test.js +10 -0
  45. package/scripts/prototype-generation-native-test.js +78 -0
  46. package/scripts/prototype-generation-test.js +99 -0
  47. package/scripts/prototype-inventory-test.js +43 -0
  48. package/scripts/prototype-locations-test.js +79 -0
  49. package/scripts/prototype-project-test.js +183 -0
  50. package/scripts/ui-prototype-codebuddy-hook-test.js +106 -0
  51. package/scripts/ui-prototype-content-test.js +123 -0
  52. package/scripts/ui-prototype-dependencies-test.js +213 -0
  53. package/scripts/ui-prototype-devtools-test.js +53 -0
  54. package/scripts/ui-prototype-execution-evidence-test.js +62 -0
  55. package/scripts/ui-prototype-execution-gates-test.js +66 -0
  56. package/scripts/ui-prototype-execution-policy-test.js +42 -0
  57. package/scripts/ui-prototype-execution-runtime-test.js +27 -0
  58. package/scripts/ui-prototype-ide-source-test.js +73 -0
  59. package/scripts/ui-prototype-native-test.js +504 -0
  60. package/scripts/ui-prototype-registration-test.js +53 -0
  61. package/scripts/ui-prototype-template-test.js +261 -0
  62. package/scripts/ui-prototype-test.js +10 -0
  63. package/scripts/ui-prototype-transcript-auto-test.js +67 -0
  64. package/skills/i3-frontend-ui-engineering/SKILL.md +1 -1
  65. package/skills/i3-incremental-implementation/SKILL.md +1 -1
  66. package/skills/i3-ui-prototype/SKILL.md +158 -19
  67. package/skills/i3-ui-prototype/assets/capabilities/generation.json +833 -0
  68. package/skills/i3-ui-prototype/assets/capabilities/management.json +294 -0
  69. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/acceptance.json +51 -0
  70. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/component-map.json +13 -0
  71. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/generation-component-map.json +300 -0
  72. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/index.html +11 -0
  73. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package-lock.json +2298 -0
  74. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/package.json +22 -0
  75. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/public/prototype-config.json +6 -0
  76. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/App.vue +136 -0
  77. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/components/DeviceEditor.vue +111 -0
  78. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/config.ts +19 -0
  79. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/content.ts +28 -0
  80. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/devices.ts +50 -0
  81. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/domain/theme.ts +25 -0
  82. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/main.ts +18 -0
  83. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/style.css +50 -0
  84. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/src/useDevices.ts +66 -0
  85. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/devices.test.ts +44 -0
  86. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tests/theme.test.ts +26 -0
  87. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/tsconfig.json +15 -0
  88. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/legacy-starter/vite.config.ts +4 -0
  89. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/profile.json +174 -0
  90. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/readme.md +61 -0
  91. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/index.html +11 -0
  92. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package-lock.json +2298 -0
  93. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/package.json +22 -0
  94. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/public/prototype-config.json +21 -0
  95. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/App.vue +62 -0
  96. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/PrototypeShell.vue +59 -0
  97. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/config.ts +23 -0
  98. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/presentation.ts +29 -0
  99. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/domain/theme.ts +25 -0
  100. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/main.ts +18 -0
  101. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/src/style.css +26 -0
  102. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/presentation.test.ts +23 -0
  103. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tests/theme.test.ts +26 -0
  104. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/tsconfig.json +15 -0
  105. package/skills/i3-ui-prototype/assets/frameworks/vue-antdv-next/starter/vite.config.ts +4 -0
  106. package/skills/i3-ui-prototype/assets/hosts/codebuddy/profile.json +13 -0
  107. package/skills/i3-ui-prototype/assets/optional-skills/host-targets.json +739 -0
  108. package/skills/i3-ui-prototype/assets/optional-skills/sources.json +27 -0
  109. package/skills/i3-ui-prototype/assets/templates/catalog.js +149 -0
  110. package/skills/i3-ui-prototype/assets/templates/catalog.json +2 -1
  111. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/capability-definition.json +294 -0
  112. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/component-inventory.json +707 -0
  113. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/generation.json +49 -0
  114. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/index.html +507 -2
  115. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline-build.json +265 -0
  116. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/offline.js +8888 -0
  117. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template-inventory.json +28791 -0
  118. package/skills/i3-ui-prototype/assets/templates/enterprise-admin/template.json +9 -0
  119. package/skills/i3-ui-prototype/assets/templates/gallery/index.html +5 -5
  120. package/skills/i3-ui-prototype/assets/templates/generation-options.js +42 -0
  121. package/skills/i3-ui-prototype/assets/templates/index.html +3 -0
  122. package/skills/i3-ui-prototype/assets/templates/presentation.html +17 -0
  123. package/skills/i3-ui-prototype/references/artifact-contract.md +26 -1
  124. package/skills/i3-ui-prototype/references/content-template.md +81 -0
  125. package/skills/i3-ui-prototype/references/enterprise-admin.md +6 -0
  126. package/skills/i3-ui-prototype/references/entry-contract.md +71 -0
  127. package/skills/i3-ui-prototype/references/execution-evidence.md +66 -0
  128. package/skills/i3-ui-prototype/references/expert-adapter.md +33 -5
  129. package/skills/i3-ui-prototype/references/framework-contract.md +76 -0
  130. package/skills/i3-ui-prototype/references/generation-contract.md +122 -0
  131. package/skills/i3-ui-prototype/references/interaction-acceptance.md +169 -0
  132. package/skills/i3-ui-prototype/references/legacy-native.md +58 -0
  133. package/skills/i3-ui-prototype/references/optional-skills.md +63 -0
  134. package/skills/i3-ui-prototype/references/project-layout.md +92 -0
  135. package/skills/i3-ui-prototype/references/template-adapters.md +7 -1
  136. package/skills/i3-ui-prototype/references/workflow-contract.md +62 -4
  137. package/skills/i3-ui-prototype/scripts/adapters/easel-html.cjs +1 -1
  138. package/skills/i3-ui-prototype/scripts/browser-workspace.cjs +72 -0
  139. package/skills/i3-ui-prototype/scripts/codebuddy-context.cjs +75 -0
  140. package/skills/i3-ui-prototype/scripts/codebuddy-hook.cjs +157 -0
  141. package/skills/i3-ui-prototype/scripts/codebuddy-ide-source.cjs +69 -0
  142. package/skills/i3-ui-prototype/scripts/codebuddy-registration.cjs +123 -0
  143. package/skills/i3-ui-prototype/scripts/content-template.cjs +98 -0
  144. package/skills/i3-ui-prototype/scripts/enterprise-template.cjs +13 -6
  145. package/skills/i3-ui-prototype/scripts/execution-assertions.cjs +43 -0
  146. package/skills/i3-ui-prototype/scripts/execution-evidence.cjs +146 -0
  147. package/skills/i3-ui-prototype/scripts/execution-gates.cjs +147 -0
  148. package/skills/i3-ui-prototype/scripts/execution-policy.cjs +41 -0
  149. package/skills/i3-ui-prototype/scripts/execution-runtime.cjs +60 -0
  150. package/skills/i3-ui-prototype/scripts/generation-assets.cjs +18 -0
  151. package/skills/i3-ui-prototype/scripts/generation-contract.cjs +214 -0
  152. package/skills/i3-ui-prototype/scripts/generation-evidence.cjs +50 -0
  153. package/skills/i3-ui-prototype/scripts/generation-replan.cjs +34 -0
  154. package/skills/i3-ui-prototype/scripts/hosts/codebuddy.cjs +16 -0
  155. package/skills/i3-ui-prototype/scripts/manage-baseline.cjs +1 -1
  156. package/skills/i3-ui-prototype/scripts/native-browser.cjs +785 -0
  157. package/skills/i3-ui-prototype/scripts/native-build.cjs +91 -0
  158. package/skills/i3-ui-prototype/scripts/native-delivery.cjs +103 -0
  159. package/skills/i3-ui-prototype/scripts/native-devtools.cjs +92 -0
  160. package/skills/i3-ui-prototype/scripts/native-files.cjs +35 -0
  161. package/skills/i3-ui-prototype/scripts/native-pilot-lib.cjs +292 -0
  162. package/skills/i3-ui-prototype/scripts/native-pilot.cjs +50 -0
  163. package/skills/i3-ui-prototype/scripts/optional-skill-install.cjs +203 -0
  164. package/skills/i3-ui-prototype/scripts/project-registry.cjs +126 -0
  165. package/skills/i3-ui-prototype/scripts/prototype-dependencies.cjs +81 -0
  166. package/skills/i3-ui-prototype/scripts/prototype-hooks.cjs +87 -0
  167. package/skills/i3-ui-prototype/scripts/prototype-lib.cjs +31 -13
  168. package/skills/i3-ui-prototype/scripts/prototype-locations.cjs +69 -0
  169. package/skills/i3-ui-prototype/scripts/sync-template.cjs +19 -0
  170. package/skills/i3-ui-prototype/scripts/template-contract.cjs +407 -0
  171. package/skills/i3-ui-prototype/scripts/template-inventory.cjs +78 -0
  172. package/skills/i3-ui-prototype/scripts/validate-result.cjs +117 -0
  173. package/skills/i3-using-agent-skills/SKILL.md +2 -5
@@ -23,6 +23,10 @@
23
23
  {
24
24
  "prompt": "为项目配置 CI/CD 流水线,自动运行构建和测试。",
25
25
  "owner": "i3-ci-cd-and-automation"
26
+ },
27
+ {
28
+ "prompt": "选择 UI 原型模板,制作一个可操作的申请审批交互原型供评审。",
29
+ "owner": "i3-ui-prototype"
26
30
  }
27
31
  ]
28
32
  },
@@ -40,6 +44,32 @@
40
44
  "Core operating behaviors (assumptions surfaced) are respected",
41
45
  "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
42
46
  ]
47
+ },
48
+ {
49
+ "id": 1002,
50
+ "kind": "dialogue",
51
+ "prompt": "用户希望根据可编辑 HTML 参考和简短业务说明制作设备管理评审原型。请在当前会话选择适用技能并说明下一步;不创建文件,不启动独立任务。",
52
+ "expected_output": "Ordinary routing to i3-ui-prototype in the main session, with operation and scope decided by the working skill",
53
+ "expectations": [
54
+ "The meta-skill selects the prototype workflow for reviewable prototype work",
55
+ "The main session uses the working skill without requiring a dedicated persona, persistent routing rule or parent-child result protocol",
56
+ "Detailed operation, framework and acceptance decisions remain in the prototype workflow rather than being copied into the meta-skill",
57
+ "The requested read-only routing explanation does not initialize a prototype or claim implementation",
58
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
59
+ ]
60
+ },
61
+ {
62
+ "id": 1003,
63
+ "kind": "dialogue",
64
+ "prompt": "分别收到两个任务:实现已认可的生产 Vue 表单组件并支持键盘操作;对照已认可基线检查评审原型。请分别路由并说明原工程工作流如何继续适用,不执行这两个任务。",
65
+ "expected_output": "Separate routing for production engineering and prototype review without changing the general lifecycle",
66
+ "expectations": [
67
+ "Production UI implementation follows the engineering workflow with the frontend implementation method",
68
+ "Prototype fidelity inspection selects the prototype workflow",
69
+ "The prototype route does not replace generic planning, implementation or review methods or impose prototype-specific delegation on them",
70
+ "The response does not invent runtime verification or human approval",
71
+ "人类可读的追问、说明、进度、报告与生成文档使用简体中文;保留代码、路径、工具参数、JSON 协议及机器状态的原始格式。"
72
+ ]
43
73
  }
44
74
  ]
45
75
  }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Original scripts stay unchanged; this adapter only maps host paths and input fields.
4
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict');
5
+ const M=require('./manage.cjs');
6
+ function projectKey(project){return (path.basename(project).replace(/[^a-zA-Z0-9_-]/g,'-').slice(0,32)||'project')+'-'+M.sha(project).slice(0,16);}
7
+ function effective({project,product,group,home=os.homedir(),env=process.env,read=file=>M.modules().R.fileIO.read(file),load=base=>registration(base)}) {
8
+ const projectBase=path.join(project,'.codebuddy'),globalBase=M.modules().P.globalBase({home,env});
9
+ const projectText=read(path.join(projectBase,'settings.json')),local=read(path.join(projectBase,'settings.local.json'));
10
+ if(local){const settings=M.config(local);assert.ok(!settings.hooks&&!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Local Hook override requires review');}
11
+ for(const base of [projectBase,globalBase]){const settings=M.config(read(path.join(base,'settings.json')));assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents Hooks');}
12
+ const bases=product==='ide'&&projectText!==null?[projectBase]:[projectBase,globalBase];
13
+ for(const base of [...new Set(bases)]){const record=load(base);if(record?.groups[group]){assert.equal(record.product,product,'Conflicting CodeBuddy products');return record;}}
14
+ return null;
15
+ }
16
+ function projectFor(options,payload,env=process.env,real=file=>fs.realpathSync(file),callbackCwd=process.cwd(),workspace=(file,session)=>require(path.join(M.modules().scripts,'execution-evidence.cjs')).ideWorkspace(file,session)) {
17
+ // CLI 2.169 omits cwd on SessionStart. Use the actual startup Hook process
18
+ // directory only for that event; other events still require native cwd.
19
+ const cwd=payload.cwd||(payload.hook_event_name==='SessionStart'?callbackCwd:null);
20
+ assert.ok(path.isAbsolute(cwd||''),'Native Hook cwd required');
21
+ const ideRoot=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&cwd==='/';
22
+ const project=real(options.scope==='project'?path.dirname(options.base):ideRoot?workspace(payload.transcript_path,payload.session_id):env.CODEBUDDY_PROJECT_DIR||env.CLAUDE_PROJECT_DIR||cwd);
23
+ const relative=path.relative(project,real(ideRoot?project:cwd));assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Hook working directory is outside the registered project');return project;
24
+ }
25
+ function invocation({group,event,project,cwd=project,base,payload},source) {
26
+ const value=structuredClone(payload),cache=path.join(base,'cache/i3-composer-agent/hooks',projectKey(project));
27
+ if(group==='sdd-cache') {
28
+ if(value.tool_name!=='WebFetch'||!['PreToolUse','PostToolUse'].includes(event))return null;
29
+ const before='${CLAUDE_PROJECT_DIR:-$PWD}/.claude/sdd-cache';assert.equal(source.split(before).length,3,'Upstream cache path changed; review the adapter');
30
+ source=source.replaceAll(before,'${I3_HOOK_CACHE}/sdd-cache');
31
+ } else if(group==='simplify-ignore') {
32
+ if(event==='Stop')value.tool_name='';
33
+ if(event!=='Stop') {
34
+ if(event==='PreToolUse'?value.tool_name!=='Read':!['Edit','Write'].includes(value.tool_name))return null;
35
+ const file=value.tool_input?.file_path||value.tool_input?.filePath;if(typeof file!=='string')return null;
36
+ const resolved=path.resolve(cwd,file),relative=path.relative(project,resolved);
37
+ if(relative==='..'||relative.startsWith('..'+path.sep)||path.isAbsolute(relative))return null;
38
+ value.tool_input.file_path=resolved;
39
+ }
40
+ const before='${CLAUDE_PROJECT_DIR:-.}/.claude/.simplify-ignore-cache';assert.equal(source.split(before).length,2,'Upstream simplify cache path changed; review the adapter');
41
+ source=source.replace(before,'${I3_HOOK_CACHE}/simplify-ignore');
42
+ }else assert.ok(group==='session-start'&&event==='SessionStart','Unknown upstream Hook event');
43
+ return {source,payload:value,cache,env:{CLAUDE_PROJECT_DIR:project,CODEBUDDY_PROJECT_DIR:project,I3_HOOK_CACHE:cache}};
44
+ }
45
+ function bundle(directory=__dirname) {
46
+ const files=['manage.cjs','codebuddy-upstream.cjs','session-start.sh','sdd-cache-pre.sh','sdd-cache-post.sh','simplify-ignore.sh'];
47
+ return M.sha(JSON.stringify(files.map(file=>{let text=fs.readFileSync(path.join(directory,file),'utf8');if(file==='session-start.sh')text=text.replace(/^SKILLS_DIR=.*$/m,'SKILLS_DIR=<native-layout>');return [file,M.sha(text)];})));
48
+ }
49
+ function registration(base) {
50
+ const io=M.modules().R.fileIO,raw=io.read(path.join(base,'state/i3-composer-agent/hooks.json'));if(raw===null)return null;
51
+ const record=JSON.parse(raw),settingsText=io.read(path.join(base,'settings.json'));
52
+ M.validateOwned(M.config(settingsText),record,base,record.scope);
53
+ for(const item of Object.values(record.groups))assert.equal(item.scriptDigest,bundle(),'Upstream Hook adapter changed; refresh registration');
54
+ return {...record,settingsDigest:M.sha(settingsText)};
55
+ }
56
+ function observe(base,project,group,payload,record,result) {
57
+ const io=M.modules().R.fileIO,file=path.join(base,'state/i3-composer-agent/hook-observations',projectKey(project),group+'.json'),unlock=io.lock(file);
58
+ try {
59
+ const before=io.read(file),old=before?JSON.parse(before):null;
60
+ const same=old?.settingsDigest===record.settingsDigest&&old?.scriptDigest===record.groups[group].scriptDigest;
61
+ const value={schemaVersion:1,group,base,project,sessionId:payload.session_id||null,event:payload.hook_event_name,at:new Date().toISOString(),
62
+ events:[...new Set([...(same?old.events:[]),payload.hook_event_name])],exitCode:result.status,settingsDigest:record.settingsDigest,scriptDigest:record.groups[group].scriptDigest,invocationVerified:false};
63
+ io.write(file,M.json(value),M.digest(before));
64
+ }finally{unlock();}
65
+ }
66
+ function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8')) {
67
+ assert.ok(Buffer.byteLength(raw)<=4*1024*1024,'Hook input too large');const options={};
68
+ while(argv.length){const flag=argv.shift();assert.ok(flag.startsWith('--')&&argv.length,'Invalid Hook argument');assert.ok(['registration','group','base','scope','product','event'].includes(flag.slice(2)),'Unknown Hook argument');options[flag.slice(2)]=argv.shift();}
69
+ assert.equal(options.registration,M.OWNER,'Invalid Hook registration');assert.ok(path.isAbsolute(options.base||''),'Absolute Hook base required');assert.ok(['project','global'].includes(options.scope),'Invalid Hook scope');
70
+ const item=M.catalog.find(x=>x.id===options.group&&x.origin==='upstream');assert.ok(item?.events.includes(options.event),'Invalid Hook group/event');
71
+ const payload=JSON.parse(raw);assert.equal(payload.hook_event_name,options.event,'Hook payload event differs');const project=projectFor(options,payload);
72
+ const expected=options.scope==='project'?path.join(project,'.codebuddy'):M.modules().P.globalBase();assert.equal(options.base,expected,'Hook installation scope differs');
73
+ const record=registration(options.base);
74
+ if(!record?.groups[options.group])return {status:0,stdout:'{"continue":true}\n',stderr:''};
75
+ assert.equal(record.product,options.product,'Hook product differs');
76
+ if(effective({project,product:options.product,group:options.group})?.base!==options.base)return {status:0,stdout:'{"continue":true}\n',stderr:''};
77
+ const filename=options.group==='session-start'?'session-start.sh':options.group==='simplify-ignore'?'simplify-ignore.sh':options.event==='PreToolUse'?'sdd-cache-pre.sh':'sdd-cache-post.sh';
78
+ const cwd=options.product==='ide'&&payload.client==='CodeBuddyIDE'&&payload.cwd==='/'?project:payload.cwd||project;
79
+ const script=path.join(__dirname,filename),prepared=invocation({...options,project,cwd,payload},fs.readFileSync(script,'utf8'));
80
+ if(!prepared)return {status:0,stdout:'{"continue":true}\n',stderr:''};
81
+ if(options.group==='simplify-ignore'&&options.event!=='Stop'&&fs.existsSync(prepared.payload.tool_input.file_path)) {
82
+ const real=fs.realpathSync(prepared.payload.tool_input.file_path),relative=path.relative(project,real);assert.ok(relative!=='..'&&!relative.startsWith('..'+path.sep)&&!path.isAbsolute(relative),'Protected file symlink escapes the project');
83
+ }
84
+ const unlock=M.modules().R.fileIO.lock(path.join(prepared.cache,'.'+options.group+'.event'));
85
+ let result;try {
86
+ result=require('node:child_process').spawnSync(record.groups[options.group].bashPath,['-c',prepared.source,script],{cwd:project,env:{...process.env,...prepared.env},input:JSON.stringify(prepared.payload),encoding:'utf8',timeout:15000,maxBuffer:8*1024*1024});
87
+ if(result.error||result.status===null)throw new Error(result.error?.message||'Upstream Hook did not complete');
88
+ }finally{unlock();}
89
+ try{observe(options.base,project,options.group,payload,record,result);}catch(error){result.stderr=(result.stderr||'')+`\ni3 Hook observation unavailable: ${error.message}\n`;}
90
+ return result;
91
+ }
92
+ module.exports={projectKey,projectFor,effective,invocation,bundle,registration,observe,main};
93
+ if(require.main===module)try{const result=main();process.stdout.write(result.stdout||'');process.stderr.write(result.stderr||'');process.exitCode=result.status;}catch(error){process.stderr.write(`i3 CodeBuddy Hook unavailable: ${error.message}\n`);process.exitCode=1;}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // Stable host Hook entry; the workflow and A+B implementation remain in the Skill.
4
+ const fs=require('node:fs'),path=require('node:path');
5
+ function main(argv=process.argv.slice(2),raw=fs.readFileSync(0,'utf8'),load=()=>require(path.join(require('./manage.cjs').modules().scripts,'hosts/codebuddy.cjs'))) {
6
+ return load().main(argv,raw);
7
+ }
8
+ module.exports={main};
9
+ if(require.main===module)try {
10
+ const result=main();process.stdout.write(JSON.stringify(result.output)+'\n');
11
+ if(result.code===2)process.stderr.write(result.output.reason+'\n');process.exitCode=result.code;
12
+ }catch(error){process.stderr.write(`i3 prototype audit unavailable: ${error.message}. Strict A gates remain unverified.\n`);process.exitCode=1;}
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // CodeBuddy settings protocol: https://www.codebuddy.cn/docs/cli/hooks
4
+ // IDE override semantics: https://www.codebuddy.cn/docs/ide/Features/Hooks
5
+ const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),crypto=require('node:crypto'),assert=require('node:assert/strict');
6
+ const OWNER='i3-composer-agent-codebuddy-v1';
7
+ const catalog=[
8
+ {id:'session-start',name:'Skill 发现与路由',origin:'upstream',files:['session-start.sh'],events:['SessionStart'],dependencies:['bash','jq'],condition:'明确 skill-routing=hook;原生路由使用 native,避免重复注入'},
9
+ {id:'sdd-cache',name:'官方文档请求缓存',origin:'upstream',files:['sdd-cache-pre.sh','sdd-cache-post.sh'],events:['PreToolUse','PostToolUse'],dependencies:['bash','jq','curl','sha256'],condition:'实际 WebFetch;仅源站 304 复用'},
10
+ {id:'simplify-ignore',name:'代码块简化保护',origin:'upstream',files:['simplify-ignore.sh'],events:['PreToolUse','PostToolUse','Stop'],dependencies:['bash','jq','sha1'],condition:'使用 simplify-ignore 标记;会在运行期间替换标记块并在 Stop 恢复'},
11
+ {id:'prototype',name:'原型执行审计',origin:'i3',events:['SessionStart','UserPromptSubmit','PreToolUse','PostToolUse','Stop'],dependencies:['node'],condition:'真实 CodeBuddy IDE/CLI;默认从原生 Hook 自动识别日志,支持手动目录限制'},
12
+ ];
13
+ const digest=value=>crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
14
+ const sha=bytes=>crypto.createHash('sha256').update(bytes).digest('hex');
15
+ const json=value=>JSON.stringify(value,null,2)+'\n';
16
+ function selection(value,action='check') {
17
+ const ids=value===undefined?(['check','list'].includes(action)?catalog.map(x=>x.id):['prototype']):value==='all'?catalog.map(x=>x.id):value.split(',');
18
+ assert.ok(ids.length&&ids.every(id=>catalog.some(item=>item.id===id)),'Unknown Hook group; choose session-start,sdd-cache,simplify-ignore,prototype or all');
19
+ return [...new Set(ids)];
20
+ }
21
+ function quote(value){assert.ok(typeof value==='string'&&!/[\x00\r\n]/.test(value),'Unsafe Hook argument');return "'"+value.replaceAll("'","'\\''")+"'";}
22
+ function config(raw){const value=raw===null?{}:JSON.parse(raw);assert.ok(value&&typeof value==='object'&&!Array.isArray(value),'Settings must be an object');assert.ok(value.hooks===undefined||value.hooks&&typeof value.hooks==='object'&&!Array.isArray(value.hooks),'Invalid hooks configuration');for(const groups of Object.values(value.hooks||{})){assert.ok(Array.isArray(groups),'Invalid Hook groups');for(const group of groups)assert.ok(group&&Array.isArray(group.hooks),'Invalid Hook group');}return value;}
23
+ function modules() {
24
+ const roots=[path.resolve(__dirname,'..'),path.resolve(__dirname,'../..')];
25
+ const candidates=roots.flatMap(root=>['i3-ui-prototype',['ui','prototype'].join('-')].map(id=>path.join(root,'skills',id,'scripts')));
26
+ const scripts=candidates.find(dir=>fs.existsSync(path.join(dir,'prototype-hooks.cjs')));assert.ok(scripts,'Prototype audit implementation is missing; install the complete package');
27
+ return {scripts,R:require(path.join(scripts,'codebuddy-registration.cjs')),P:require(path.join(scripts,'prototype-hooks.cjs'))};
28
+ }
29
+ function owned(group){return group.hooks.some(h=>typeof h.command==='string'&&h.command.includes(OWNER));}
30
+ function validateOwned(settings,record,base,scope) {
31
+ if(record){assert.equal(record.owner,OWNER,'Unknown Hook ownership');assert.equal(record.schemaVersion,1,'Unsupported Hook ownership');assert.equal(record.base,base,'Hook base differs');assert.equal(record.scope,scope,'Hook scope differs');assert.ok(record.groups&&typeof record.groups==='object'&&!Array.isArray(record.groups),'Invalid owned Hook groups');
32
+ for(const [id,item] of Object.entries(record.groups)){const known=catalog.find(x=>x.id===id&&x.origin==='upstream');assert.ok(known&&item&&item.groups,'Unknown owned Hook group');assert.deepEqual(Object.keys(item.groups).sort(),[...known.events].sort(),'Owned Hook events changed');}
33
+ }
34
+ const found=new Set();
35
+ for(const [event,groups] of Object.entries(settings.hooks||{}))for(const group of groups.filter(owned)) {
36
+ const ids=Object.keys(record?.groups||{}).filter(id=>record.groups[id].groups[event]&&group.hooks.some(h=>h.command?.includes(`--group' '${id}'`)));
37
+ assert.equal(ids.length,1,'Hook ownership is missing; preserve the unknown command');const key=ids[0]+':'+event;
38
+ assert.ok(!found.has(key),'Duplicate owned Hook');assert.deepEqual(group,record.groups[ids[0]].groups[event],'Owned Hook was modified; preserve user changes');found.add(key);
39
+ }
40
+ for(const [id,item] of Object.entries(record?.groups||{}))for(const event of Object.keys(item.groups))assert.ok(found.has(id+':'+event),'Owned Hook registration is missing');
41
+ }
42
+ function requirements(id,deps) {
43
+ for(const name of catalog.find(x=>x.id===id).dependencies){if(name==='sha256')assert.ok(deps.shasum||deps.sha256sum,'Missing shasum or sha256sum');else if(name==='sha1')assert.ok(deps.shasum||deps.sha1sum,'Missing shasum or sha1sum');else assert.ok(deps[name],`Missing ${name}; install it separately before enabling ${id}`);}
44
+ }
45
+ function planUpstream(options) {
46
+ const {base,scope,product,action,record=null,settingsText=null,nodePath,hookDirectory,scriptDigest,dependencies={},skillRouting}=options;
47
+ assert.ok(['keep','enable','disable'].includes(action),'Invalid Hook action');assert.ok(['project','global'].includes(scope),'Specify project or global scope');
48
+ const settings=config(settingsText);validateOwned(settings,record,base,scope);
49
+ const chosen=selection(options.groups,action).filter(id=>id!=='prototype'),enabled=new Set(Object.keys(record?.groups||{}));
50
+ if(action==='enable')for(const id of chosen){if(id==='session-start')assert.equal(skillRouting,'hook','session-start requires explicit skill-routing=hook; native routing must not be duplicated');requirements(id,dependencies);enabled.add(id);}
51
+ if(action==='disable')for(const id of chosen)enabled.delete(id);
52
+ if(enabled.size){assert.ok(['ide','cli'].includes(product),'Specify CodeBuddy product ide or cli');if(record)assert.equal(product,record.product,'Disable the previous product profile before changing it');assert.match(scriptDigest,/^[a-f0-9]{64}$/);for(const p of [base,nodePath,hookDirectory])assert.ok(path.isAbsolute(p),'Absolute registration paths required');}
53
+ // Remove only exact owned groups after validation. All user Hook groups survive.
54
+ for(const event of Object.keys(settings.hooks||{}))settings.hooks[event]=settings.hooks[event].filter(g=>!owned(g));
55
+ const groups={};
56
+ for(const item of catalog.filter(x=>enabled.has(x.id))) {
57
+ const entry={product,scriptDigest,nodePath,bashPath:dependencies.bash||record?.groups[item.id]?.bashPath,skillRouting:item.id==='session-start'?'hook':null,groups:{}};
58
+ for(const event of item.events) {
59
+ const matcher=item.id==='sdd-cache'?'^WebFetch$':event==='PreToolUse'?'^Read$':'^(Edit|Write)$';
60
+ const argv=[nodePath,path.join(hookDirectory,'codebuddy-upstream.cjs'),'--registration',OWNER,'--group',item.id,'--base',base,'--scope',scope,'--product',product,'--event',event];
61
+ const group={...(event.endsWith('ToolUse')?{matcher}:{}),hooks:[{type:'command',command:argv.map(quote).join(' '),timeout:item.id==='sdd-cache'?15:20}]};
62
+ entry.groups[event]=group;settings.hooks||={};(settings.hooks[event]||=[]).push(group);
63
+ }
64
+ groups[item.id]=entry;
65
+ }
66
+ const recordAfter=enabled.size?{schemaVersion:1,owner:OWNER,base,scope,product,productVersion:options.productVersion||record?.productVersion||'unreported',groups}:null;
67
+ const afterText=!record&&!chosen.length||!record&&action!=='enable'?settingsText:json(settings);
68
+ return {afterText,recordAfter};
69
+ }
70
+ function apply(change,io=modules().R.fileIO) {
71
+ const unlock=io.lock?io.lock(change.changes[0].file):()=>{},written=[];
72
+ try {
73
+ for(const entry of change.changes)assert.equal(digest(io.read(entry.file)),digest(entry.before),'Hook configuration changed after planning');
74
+ try {for(const entry of change.changes)if(entry.before!==entry.after){io.write(entry.file,entry.after,digest(entry.before));written.push(entry);}}
75
+ catch(error){for(const entry of written.reverse())if(io.read(entry.file)===entry.after)io.write(entry.file,entry.before,digest(entry.after));throw error;}
76
+ return change.summary;
77
+ }finally{unlock();}
78
+ }
79
+ function executable(name,env=process.env) {
80
+ for(const folder of (env.PATH||'').split(path.delimiter)){const file=path.resolve(folder,name);try{if(fs.statSync(file).isFile()){fs.accessSync(file,fs.constants.X_OK);return file;}}catch{}}
81
+ return null;
82
+ }
83
+ function dependencies(){return Object.fromEntries(['bash','jq','curl','shasum','sha256sum','sha1sum'].map(name=>[name,executable(name)]));}
84
+ function location(options) {
85
+ assert.equal(options.host||'codebuddy','codebuddy','Unified Hooks currently support CodeBuddy only');assert.ok(['project','global'].includes(options.scope),'Specify project or global scope');
86
+ const project=fs.realpathSync(path.resolve(options.project||'.'));assert.ok(fs.statSync(project).isDirectory(),'Project must be a directory');
87
+ return {project,base:options.scope==='project'?path.join(project,'.codebuddy'):modules().P.globalBase(options)};
88
+ }
89
+ function unmanaged(settings) {
90
+ const files={ 'session-start':['session-start.sh'],'sdd-cache':['sdd-cache-pre.sh','sdd-cache-post.sh'],'simplify-ignore':['simplify-ignore.sh'] },result=[];
91
+ for(const [event,groups] of Object.entries(settings.hooks||{}))for(const group of groups)for(const hook of group.hooks) {
92
+ if(typeof hook.command!=='string'||hook.command.includes(OWNER))continue;
93
+ for(const [id,names] of Object.entries(files))if(names.some(name=>hook.command.includes(name)))result.push({id,event,command:hook.command});
94
+ }
95
+ return result;
96
+ }
97
+ function pendingSimplify(base) {
98
+ const parent=path.join(base,'cache/i3-composer-agent/hooks');if(!fs.existsSync(parent))return [];
99
+ return fs.readdirSync(parent,{withFileTypes:true}).filter(entry=>entry.isDirectory()&&!entry.isSymbolicLink()).flatMap(entry=>{
100
+ const dir=path.join(parent,entry.name,'simplify-ignore');if(!fs.existsSync(dir))return [];
101
+ assert.ok(!fs.lstatSync(dir).isSymbolicLink(),'Simplify cache symlink requires review');return fs.readdirSync(dir).filter(file=>file.endsWith('.bak')).map(file=>path.join(dir,file));
102
+ });
103
+ }
104
+ function prepare(options,runtime={}) {
105
+ const {P,R}=runtime.modules||modules(),{project,base}=runtime.location||location(options),io=R.fileIO,action=options.action||'keep',ids=selection(options.groups,action);
106
+ const settingsPath=path.join(base,'settings.json'),recordPath=path.join(base,'state/i3-composer-agent/hooks.json'),prototypePath=path.join(base,'state/i3-prototype/installation.json');
107
+ const before=io.read(settingsPath),recordText=io.read(recordPath),record=recordText?JSON.parse(recordText):null,prototypeText=io.read(prototypePath),prototype=prototypeText?JSON.parse(prototypeText):null;
108
+ if(action!=='disable'&&(action!=='keep'||record||prototype))assert.ok(process.platform!=='win32','CodeBuddy Hook quoting on Windows has not been validated; default installation without Hooks remains available');
109
+ const product=options.product||record?.product||prototype?.product,settings=config(before);
110
+ if(action!=='disable'&&(action!=='keep'||record||prototype)) {
111
+ assert.ok(!settings.disableAllHooks&&!settings.allowManagedHooksOnly,'Host policy prevents automatic Hooks; preserve the policy');
112
+ const manifest=io.read(path.join(base,'.i3-composer-agent.json'));if(manifest)assert.ok(!JSON.parse(manifest).agents?.includes('workbuddy'),'Shared WorkBuddy scope cannot identify CodeBuddy safely');
113
+ assert.ok(!options.agents?.includes('workbuddy'),'Use an exclusive CodeBuddy scope for Hooks');
114
+ }
115
+ if(action==='enable')for(const id of ids)assert.ok(!unmanaged(settings).some(item=>item.id===id),`Unmanaged ${id} Hook already exists; preserve it and resolve the duplicate before enabling`);
116
+ if(ids.includes('simplify-ignore')&&['enable','disable'].includes(action)) {
117
+ for(const candidate of [...new Set([base,path.join(project,'.codebuddy'),P.globalBase?P.globalBase(options):path.join(options.home||os.homedir(),'.codebuddy')])])assert.equal((runtime.pendingSimplify||pendingSimplify)(candidate).length,0,'Simplify protection is still active; finish the session and restore blocks before changing Hooks');
118
+ }
119
+ const prototypeAction=ids.includes('prototype')?action:'keep';
120
+ const prototypeChange=action==='disable'&&!ids.includes('prototype')?{unchanged:true}:P.prepare({...options,project,product,action:prototypeAction,entryPath:path.join(base,'i3-composer-agent/hooks/i3-prototype.cjs'),entrySource:path.join(__dirname,'i3-prototype.cjs')});
121
+ const upstream=planUpstream({...options,base,product,action,record,settingsText:prototypeChange.unchanged?before:prototypeChange.afterText,nodePath:process.execPath,
122
+ hookDirectory:path.join(base,'i3-composer-agent/hooks'),scriptDigest:runtime.scriptDigest||require('./codebuddy-upstream.cjs').bundle(),dependencies:runtime.dependencies||dependencies()});
123
+ const changes=[{file:settingsPath,before,after:upstream.afterText},{file:recordPath,before:recordText,after:upstream.recordAfter?json(upstream.recordAfter):null},
124
+ {file:prototypePath,before:prototypeText,after:prototypeChange.unchanged?prototypeText:prototypeChange.recordAfter?json(prototypeChange.recordAfter):null}];
125
+ return {changes,summary:{host:'codebuddy',scope:options.scope,base,product:product||null,action,groups:ids,changed:changes.some(item=>item.before!==item.after),
126
+ enabledGroups:[...Object.keys(upstream.recordAfter?.groups||{}),...((prototypeChange.unchanged?prototype:prototypeChange.recordAfter)?['prototype']:[])],
127
+ ...(prototypeChange.summary?.transcript?{transcript:prototypeChange.summary.transcript}:{}),
128
+ next:product==='cli'?'Review /hooks in CodeBuddy Code and start a new session.':'Reload CodeBuddy and verify actual Hook events. Registration is not activation.'}};
129
+ }
130
+ function check(options) {
131
+ const {P,R}=modules(),{project,base}=location(options),io=R.fileIO,ids=selection(options.groups,options.action||'check'),settingsText=io.read(path.join(base,'settings.json')),settings=config(settingsText),deps=dependencies(),items=[];
132
+ let record=null,error=null;try{record=require('./codebuddy-upstream.cjs').registration(base);}catch(e){error=e.message;}
133
+ for(const id of ids) {
134
+ const item=catalog.find(x=>x.id===id),entry=path.join(base,'i3-composer-agent/hooks',id==='prototype'?'i3-prototype.cjs':'codebuddy-upstream.cjs');
135
+ if(id==='prototype'){try{const result=P.check({...options,project});items.push({...item,...result,...(result.effective===false&&result.status!=='failed'?{status:'shadowed'}:{}),entryPath:entry});}catch(e){items.push({...item,status:'failed',reason:e.message,entryPath:entry});}continue;}
136
+ const missingFiles=[entry,...item.files.map(file=>path.join(base,'i3-composer-agent/hooks',file))].filter(file=>!fs.existsSync(file));
137
+ const row={...item,entryPath:entry,filesPresent:missingFiles.length===0,missingFiles,status:missingFiles.length?'not-installed':'files-present',invocationVerified:false,nativeValidation:'pending-user'};
138
+ try{requirements(id,deps);}catch(e){row.dependencyIssue=e.message;}
139
+ if(error){row.status='failed';row.reason=error;}
140
+ else if(record?.groups[id])try {
141
+ const U=require('./codebuddy-upstream.cjs'),selected=U.effective({project,product:record.product,group:id,home:options.home,env:options.env});row.effective=selected?.base===base;
142
+ row.status=!row.effective?'shadowed':record.product==='cli'?'pending-host-review':'registered';
143
+ const raw=io.read(path.join(base,'state/i3-composer-agent/hook-observations',U.projectKey(project),id+'.json'));
144
+ if(raw){const observation=JSON.parse(raw);if(observation.settingsDigest===record.settingsDigest&&observation.scriptDigest===record.groups[id].scriptDigest){row.observation=observation;row.status=row.effective?'triggered':row.status;}}
145
+ row.product=record.product;row.productVersion=record.productVersion;
146
+ }catch(e){row.status='failed';row.reason=e.message;}
147
+ items.push(row);
148
+ }
149
+ return {host:'codebuddy',scope:options.scope,base,readOnly:true,changed:false,status:items.some(item=>item.status==='failed')?'failed':'checked',hooks:items,unmanagedHooks:unmanaged(settings),
150
+ next:'files-present is not enabled; registered is not triggered; triggered is not business acceptance. CLI: review /hooks; IDE: reload.'};
151
+ }
152
+ function configure(options){const change=prepare(options);return options.preview?change.summary:apply(change);}
153
+ function main(argv=process.argv.slice(2)) {
154
+ if(argv.includes('--help'))return {usage:'node manage.cjs list|check|enable|disable --host codebuddy --scope project|global --project <directory> [--groups <ids>|all] [--product ide|cli] [--skill-routing hook] [--transcript-root <native-directory>|auto]',hooks:catalog,default:'keep; enable/disable default prototype; list/check default all; transcripts auto for new registrations',nativeValidation:'pending-user'};
155
+ const options={action:argv.shift()||'list',host:'codebuddy',project:process.cwd()};
156
+ assert.ok(['list','check','enable','disable','keep'].includes(options.action),'Choose list, check, enable or disable');
157
+ while(argv.length){const arg=argv.shift();assert.ok(arg.startsWith('--')&&argv[0]&&!argv[0].startsWith('--'),'Hook option needs a value');const key=arg.slice(2).replace(/-([a-z])/g,(_,c)=>c.toUpperCase());assert.ok(['host','scope','project','product','productVersion','transcriptRoot','groups','skillRouting'].includes(key),'Unknown Hook option');options[key]=argv.shift();}
158
+ if(options.skillRouting!==undefined)assert.ok(['native','hook'].includes(options.skillRouting),'skill-routing must be native or hook');
159
+ return ['list','check'].includes(options.action)?check(options):configure(options);
160
+ }
161
+ module.exports={OWNER,catalog,digest,sha,json,quote,config,selection,modules,validateOwned,requirements,planUpstream,apply,dependencies,location,unmanaged,pendingSimplify,prepare,check,configure,main};
162
+ if(require.main===module)try{const result=main();process.stdout.write(json(result));if(result.status==='failed')process.exitCode=1;}catch(error){process.stderr.write(error.message+'\n');process.exitCode=1;}
package/package.json CHANGED
@@ -100,6 +100,12 @@
100
100
  "evals/fixtures/i3-test-driven-development/package.json",
101
101
  "evals/fixtures/i3-test-driven-development/src/split.js",
102
102
  "evals/fixtures/i3-test-driven-development/test/split.test.js",
103
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css",
104
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js",
105
+ "evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json",
106
+ "evals/fixtures/i3-ui-prototype/admin-list/index.html",
107
+ "evals/fixtures/i3-ui-prototype/admin-list/readme.md",
108
+ "evals/fixtures/i3-ui-prototype/admin-list/template.json",
103
109
  "evals/fixtures/i3-ui-prototype/business-brief.md",
104
110
  "evals/fixtures/i3-using-agent-skills/incident.md",
105
111
  "evals/fixtures/incremental-implementation-pressure/draft-export.js",
@@ -163,6 +169,8 @@
163
169
  "docs/getting-started.md",
164
170
  "docs/opencode-setup.md",
165
171
  "docs/other-hosts.md",
172
+ "docs/ui-prototype-native/MAIN-SESSION-REPORT.md",
173
+ "docs/ui-prototype-native/USAGE.md",
166
174
  "docs/windsurf-setup.md",
167
175
  "docs/workbuddy-setup.md",
168
176
  "plugin.json",
@@ -172,13 +180,35 @@
172
180
  "scripts/design-provider-test.js",
173
181
  "scripts/enterprise-template-test.js",
174
182
  "scripts/floor-guard-reference-test.js",
183
+ "scripts/lib/prototype-fixture.cjs",
175
184
  "scripts/lib/skill-lint-test.js",
176
185
  "scripts/lib/skill-lint.js",
186
+ "scripts/prototype-cache-test.js",
187
+ "scripts/prototype-generation-entry-test.js",
188
+ "scripts/prototype-generation-evidence-test.js",
189
+ "scripts/prototype-generation-native-test.js",
190
+ "scripts/prototype-generation-test.js",
191
+ "scripts/prototype-inventory-test.js",
192
+ "scripts/prototype-locations-test.js",
193
+ "scripts/prototype-project-test.js",
177
194
  "scripts/run-evals-test.js",
178
195
  "scripts/run-evals.js",
179
196
  "scripts/run-regressions.js",
180
197
  "scripts/template-adapter-test.js",
198
+ "scripts/ui-prototype-codebuddy-hook-test.js",
199
+ "scripts/ui-prototype-content-test.js",
200
+ "scripts/ui-prototype-dependencies-test.js",
201
+ "scripts/ui-prototype-devtools-test.js",
202
+ "scripts/ui-prototype-execution-evidence-test.js",
203
+ "scripts/ui-prototype-execution-gates-test.js",
204
+ "scripts/ui-prototype-execution-policy-test.js",
205
+ "scripts/ui-prototype-execution-runtime-test.js",
206
+ "scripts/ui-prototype-ide-source-test.js",
207
+ "scripts/ui-prototype-native-test.js",
208
+ "scripts/ui-prototype-registration-test.js",
209
+ "scripts/ui-prototype-template-test.js",
181
210
  "scripts/ui-prototype-test.js",
211
+ "scripts/ui-prototype-transcript-auto-test.js",
182
212
  "scripts/validate-artifact-paths-test.js",
183
213
  "scripts/validate-artifact-paths.js",
184
214
  "scripts/validate-commands-test.js",
@@ -287,6 +317,12 @@
287
317
  "evals/fixtures/i3-test-driven-development/package.json",
288
318
  "evals/fixtures/i3-test-driven-development/src/split.js",
289
319
  "evals/fixtures/i3-test-driven-development/test/split.test.js",
320
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.css",
321
+ "evals/fixtures/i3-ui-prototype/admin-list/assets/reference.js",
322
+ "evals/fixtures/i3-ui-prototype/admin-list/capability-definition.json",
323
+ "evals/fixtures/i3-ui-prototype/admin-list/index.html",
324
+ "evals/fixtures/i3-ui-prototype/admin-list/readme.md",
325
+ "evals/fixtures/i3-ui-prototype/admin-list/template.json",
290
326
  "evals/fixtures/i3-ui-prototype/business-brief.md",
291
327
  "evals/fixtures/i3-using-agent-skills/incident.md",
292
328
  "evals/fixtures/incremental-implementation-pressure/draft-export.js",
@@ -350,6 +386,8 @@
350
386
  "docs/getting-started.md",
351
387
  "docs/opencode-setup.md",
352
388
  "docs/other-hosts.md",
389
+ "docs/ui-prototype-native/MAIN-SESSION-REPORT.md",
390
+ "docs/ui-prototype-native/USAGE.md",
353
391
  "docs/windsurf-setup.md",
354
392
  "docs/workbuddy-setup.md",
355
393
  "plugin.json",
@@ -360,13 +398,35 @@
360
398
  "scripts/design-provider-test.js",
361
399
  "scripts/enterprise-template-test.js",
362
400
  "scripts/floor-guard-reference-test.js",
401
+ "scripts/lib/prototype-fixture.cjs",
363
402
  "scripts/lib/skill-lint-test.js",
364
403
  "scripts/lib/skill-lint.js",
404
+ "scripts/prototype-cache-test.js",
405
+ "scripts/prototype-generation-entry-test.js",
406
+ "scripts/prototype-generation-evidence-test.js",
407
+ "scripts/prototype-generation-native-test.js",
408
+ "scripts/prototype-generation-test.js",
409
+ "scripts/prototype-inventory-test.js",
410
+ "scripts/prototype-locations-test.js",
411
+ "scripts/prototype-project-test.js",
365
412
  "scripts/run-evals-test.js",
366
413
  "scripts/run-evals.js",
367
414
  "scripts/run-regressions.js",
368
415
  "scripts/template-adapter-test.js",
416
+ "scripts/ui-prototype-codebuddy-hook-test.js",
417
+ "scripts/ui-prototype-content-test.js",
418
+ "scripts/ui-prototype-dependencies-test.js",
419
+ "scripts/ui-prototype-devtools-test.js",
420
+ "scripts/ui-prototype-execution-evidence-test.js",
421
+ "scripts/ui-prototype-execution-gates-test.js",
422
+ "scripts/ui-prototype-execution-policy-test.js",
423
+ "scripts/ui-prototype-execution-runtime-test.js",
424
+ "scripts/ui-prototype-ide-source-test.js",
425
+ "scripts/ui-prototype-native-test.js",
426
+ "scripts/ui-prototype-registration-test.js",
427
+ "scripts/ui-prototype-template-test.js",
369
428
  "scripts/ui-prototype-test.js",
429
+ "scripts/ui-prototype-transcript-auto-test.js",
370
430
  "scripts/validate-artifact-paths-test.js",
371
431
  "scripts/validate-artifact-paths.js",
372
432
  "scripts/validate-commands-test.js",
@@ -378,7 +438,7 @@
378
438
  "scripts/validate-versions.js"
379
439
  ]
380
440
  },
381
- "version": "2.3.1",
441
+ "version": "2.3.3",
382
442
  "repository": {
383
443
  "type": "git",
384
444
  "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.1",
3
+ "version": "2.3.3",
4
4
  "description": "覆盖需求、计划、开发、测试、审查与发布的软件工程技能,默认使用简体中文。",
5
5
  "homepage": "https://github.com/49780522/i3-composer-agent",
6
6
  "repository": "https://github.com/49780522/i3-composer-agent"
@@ -66,3 +66,10 @@ test('all unusable providers preserve their individual failure reasons without a
66
66
  assert.ok(!result.choices.some(choice => choice.id === 'install'));
67
67
  assert.equal(fs.existsSync(path.join(home, 'prototypes')), false);
68
68
  });
69
+ test('a business package version cannot masquerade as the later installed UIUX version', t => {
70
+ const home = fixture(t), root = provider(home, 'manual-provider');
71
+ fs.writeFileSync(path.join(home, 'package.json'), JSON.stringify({ name: 'human-business-project', version: '99.88.77' }));
72
+ assert.equal(lib.checkDesignProvider({ provider: root }).version, 'unversioned');
73
+ fs.writeFileSync(path.join(root, 'UPSTREAM-ORIGIN.json'), JSON.stringify({ packageName: 'ui-ux-pro-max-cli', version: '2.15.0' }));
74
+ assert.equal(lib.checkDesignProvider({ provider: root }).version, '2.15.0');
75
+ });
@@ -104,11 +104,37 @@ test('generated enterprise instances isolate their local demo storage and retain
104
104
  const source = fs.readFileSync(path.join(dir, 'app/src/shared/session.js'), 'utf8');
105
105
  assert.match(source, /i3-enterprise-enterprise-demo-/);
106
106
  assert.doesNotMatch(source, /i3-prototype-antdv-/);
107
- assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /enterprise-bootstrap\.mjs/);
107
+ assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /src="offline\.js"/);
108
+ assert.ok(fs.existsSync(path.join(dir, 'app/enterprise-bootstrap.mjs')), 'Retain the historical module bootstrap for compatibility');
108
109
  assert.match(fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8'), /tokens\.css/);
109
110
  const other = initialize(t);
110
111
  assert.notEqual(read(dir, 'prototype.json').runtime.storagePrefix, read(other, 'prototype.json').runtime.storagePrefix);
111
112
  });
113
+ test('offline entry carries every editable HTML fragment and routes without changing the reference', t => {
114
+ const root = path.join(skill, 'assets/templates/enterprise-admin');
115
+ const build = read(root, 'offline-build.json');
116
+ for (const [file, expected] of Object.entries(build.files)) {
117
+ assert.equal(crypto.createHash('sha256').update(fs.readFileSync(path.join(root, file))).digest('hex'), expected, file);
118
+ }
119
+ const html = fs.readFileSync(path.join(root, 'index.html'), 'utf8');
120
+ assert.equal([...html.matchAll(/<template data-source=/g)].length, build.htmlFragmentCount);
121
+ const routes = JSON.parse(html.match(/id="prototype-offline-routes">([\s\S]*?)<\/script>/)[1]);
122
+ assert.deepEqual(routes, read(root, 'reference/routes.json'));
123
+ for (const component of new Set(routes.map(route => route.component).filter(Boolean))) {
124
+ const source = fs.readFileSync(path.resolve(root, 'reference', component), 'utf8');
125
+ const fragment = source.match(/template: new URL\('\.\/([^']+\.html)'/)[1];
126
+ assert.ok(html.includes(`data-source="reference/${path.posix.dirname(component).replace(/^\.\//, '')}/${fragment}"`), component);
127
+ }
128
+ assert.doesNotMatch(html, /type="module"/);
129
+ assert.doesNotMatch(fs.readFileSync(path.join(root, 'offline.js'), 'utf8'), /\bfetch\s*\(|\bimport\s*\(|import\.meta/);
130
+ const dir = initialize(t, { title: '离线配置 <安全标题>' });
131
+ const instance = fs.readFileSync(path.join(dir, 'app/index.html'), 'utf8');
132
+ assert.equal(JSON.parse(instance.match(/id="prototype-offline-config">([\s\S]*?)<\/script>/)[1]).title, '离线配置 <安全标题>');
133
+ assert.equal(read(dir, 'prototype.json').runtime.offlineVersion, 1);
134
+ assert.equal(lib.checkPrototype(dir).status, 'passed');
135
+ fs.unlinkSync(path.join(dir, 'app/offline.js'));
136
+ assert.equal(lib.checkPrototype(dir).status, 'failed', 'A new offline instance must require its runtime');
137
+ });
112
138
  test('missing enterprise runtime imports are rejected while declarative data-bind attributes remain valid', t => {
113
139
  const dir = initialize(t);
114
140
  assert.equal(lib.checkPrototype(dir).status, 'passed');
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+ // Historical admin-list input for regression tests only; no public template entry.
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ function prepareTemplate(contract, { project, id = 'admin-list' }) {
6
+ if (!/^[a-z][a-z0-9-]{0,63}$/.test(id)) throw new Error('Invalid identifier');
7
+ const directory = path.resolve(project, 'ui-templates', id);
8
+ if (fs.existsSync(path.join(project, 'ui-templates')) && fs.lstatSync(path.join(project, 'ui-templates')).isSymbolicLink()) throw new Error('Refusing symlink');
9
+ if (fs.existsSync(directory)) return { directory, created: false, index: contract.loadTemplate(directory).index };
10
+ fs.cpSync(path.resolve(__dirname, '../../evals/fixtures/i3-ui-prototype/admin-list'), directory, { recursive: true });
11
+ const file = path.join(directory, 'template.json');
12
+ const manifest = JSON.parse(fs.readFileSync(file));
13
+ manifest.id = id;
14
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + '\n');
15
+ return { directory, created: true, index: contract.syncTemplate(directory) };
16
+ }
17
+ module.exports = { prepareTemplate };
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+ const test = require('node:test');
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const installation = path.join(__dirname, 'lib/installation.js');
8
+ const skills = fs.existsSync(installation) ? require(installation).loadInstallation().skillsDirectory : path.resolve(__dirname, '../skills');
9
+ const scripts = path.join(skills, 'i3-ui-prototype/scripts'), cache = require(path.join(scripts, 'browser-workspace.cjs'));
10
+ function fixture(t, scope = 'project') {
11
+ const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'i3-browser-output-'))), project = path.join(root, '中文 项目'), home = path.join(root, 'isolated-home');
12
+ fs.mkdirSync(project); fs.mkdirSync(home); t.after(() => fs.rmSync(root, { recursive: true, force: true }));
13
+ return { root, project, home, host: 'codebuddy', scope, runId: 'output-test', env: {} };
14
+ }
15
+ for (const scope of ['project', 'global']) test(`${scope} browser output is prepared only under its host cache and can be reused without overwriting files`, t => {
16
+ const options = fixture(t, scope), result = cache.prepare(options);
17
+ assert.equal(result.status, 'prepared');
18
+ assert.ok(result.directory.startsWith(scope === 'global' ? options.home : path.join(options.project, '.codebuddy')));
19
+ assert.equal(fs.existsSync(path.join(options.project, '.playwright-cli')), false);
20
+ assert.equal(fs.existsSync(path.join(options.project, 'prototypes')), false);
21
+ const config = JSON.parse(fs.readFileSync(result.configFile));
22
+ assert.equal(config.outputDir, result.directory); assert.equal(config.outputMode, 'file');
23
+ fs.writeFileSync(path.join(result.directory, 'keep.txt'), 'keep');
24
+ assert.equal(cache.prepare(options).directory, result.directory);
25
+ assert.equal(fs.readFileSync(path.join(result.directory, 'keep.txt'), 'utf8'), 'keep');
26
+ if (scope === 'global') assert.deepEqual(fs.readdirSync(options.project), []);
27
+ });
28
+ test('CLI wrapper uses an existing explicit Node entry, cache cwd and configured output without installing anything', t => {
29
+ const options = fixture(t), entry = path.join(options.root, 'fixture-cli.cjs');
30
+ fs.writeFileSync(entry, 'const f=require("node:fs"),p=require("node:path");const c=JSON.parse(f.readFileSync(process.argv[process.argv.indexOf("--config")+1]));if(c.outputDir!==process.cwd())process.exit(5);f.writeFileSync(p.join(c.outputDir,"snapshot.yml"),"fixture snapshot");console.log("fixture command executed");');
31
+ const result = cache.runCli({ ...options, cli: entry, command: ['snapshot'] });
32
+ assert.equal(result.status, 'executed'); assert.equal(result.exitCode, 0);
33
+ assert.ok(fs.existsSync(path.join(result.directory, 'snapshot.yml')));
34
+ assert.equal(fs.existsSync(path.join(options.project, '.playwright-cli')), false);
35
+ assert.throws(() => cache.runCli({ ...options, cli: entry, command: ['--config=other.json', 'snapshot'] }), /config/i);
36
+ });
37
+ test('explicit evidence archiving preserves candidate identity and survives cache removal', t => {
38
+ const options = fixture(t), output = cache.prepare(options), native = require(path.join(scripts, 'native-pilot-lib.cjs'));
39
+ const scope = { schemaVersion: 2, moduleIds: ['assets'], title: '资产', pages: [{ id: 'assets', title: '资产台账', referencePage: 'models-page', regions: [{ id: 'list', mode: 'management-list' }] }], requirements: [{ id: 'A-01', text: '资产操作', pages: ['assets'], source: { type: 'prd', reference: 'fixture/A-01', statement: 'Synthetic fixture, not real acceptance.' } }], exemptions: [] };
40
+ const file = path.join(options.root, 'scope.json'); fs.writeFileSync(file, JSON.stringify(scope));
41
+ const prototype = native.createPrototype({ project: options.project, id: 'assets', scope: file, expertMode: 'template' }).directory, before = native.manifest(prototype);
42
+ fs.writeFileSync(path.join(output.directory, 'page.yml'), 'synthetic snapshot, not acceptance');
43
+ const archived = cache.archive({ ...options, id: 'assets', files: ['page.yml'] });
44
+ assert.equal(archived.status, 'archived'); assert.equal(archived.verification, 'not-assessed');
45
+ assert.deepEqual(native.manifest(prototype), before);
46
+ fs.rmSync(output.directory, { recursive: true });
47
+ assert.equal(fs.readFileSync(path.join(archived.directory, 'page.yml'), 'utf8'), 'synthetic snapshot, not acceptance');
48
+ assert.throws(() => cache.archive({ ...options, id: 'assets', files: ['../scope.json'] }), /path|cache|exist/i);
49
+ });
50
+ test('unsafe run identifiers and absent CLI entries fail before cache creation', t => {
51
+ const options = fixture(t);
52
+ assert.throws(() => cache.prepare({ ...options, runId: '../outside' }), /identifier|path/i);
53
+ assert.throws(() => cache.runCli({ ...options, cli: path.join(options.root, 'missing.cjs'), command: ['snapshot'] }));
54
+ assert.deepEqual(fs.readdirSync(options.project), []);
55
+ });
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+ const test=require('node:test'),assert=require('node:assert/strict'),fs=require('node:fs'),path=require('node:path'),vm=require('node:vm');
3
+ const install=path.join(__dirname,'lib/installation.js'),skills=fs.existsSync(install)?require(install).loadInstallation().skillsDirectory:path.resolve(__dirname,'../skills');const root=path.join(skills,'i3-ui-prototype'),lib=require(path.join(root,'scripts/native-pilot.cjs'));
4
+ const hash=()=>require('node:crypto').createHash('sha256').update(fs.readFileSync(path.join(root,'assets/templates/enterprise-admin/template-inventory.json'))).digest('hex');
5
+ test('template discovery and inventory commands remain read-only and expose real supported choices',async()=>{const before=hash();const options=await lib.main(['generation-options','--template','enterprise-admin']);assert.equal(options.readOnly,true);assert.equal(Object.keys(options.choices.layout).length,4);assert.equal(Object.keys(options.choices.style).length,3);assert.ok(options.patterns['tree-table']);const inventory=await lib.main(['inventory']);assert.equal(inventory.events,595);assert.equal(inventory.components,116);assert.equal(hash(),before);await assert.rejects(()=>lib.main(['generation-options','--template','../bad']));});
6
+ test('offline preview uses metadata-derived local classic scripts and has valid JavaScript',()=>{const templates=path.join(root,'assets/templates'),context={};vm.runInNewContext(fs.readFileSync(path.join(templates,'generation-options.js'),'utf8'),context);assert.equal(context.I3_GENERATION_OPTIONS[0].id,'enterprise-admin');const html=fs.readFileSync(path.join(templates,'presentation.html'),'utf8');assert.ok(!/type="module"|https?:\/\/|fetch\(/.test(html));for(const block of html.matchAll(/<script(?:\s[^>]*)?>([\s\S]*?)<\/script>/g))new vm.Script(block[1]);assert.equal(require(path.join(root,'scripts/generation-assets.cjs')).refresh(root,{check:true}).status,'passed');});