@jennie-shawn/starwork 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (298) hide show
  1. package/README.md +111 -99
  2. package/adapters/README.md +34 -0
  3. package/adapters/claude-code/profile.json +55 -0
  4. package/adapters/claude-code/rules.md +13 -0
  5. package/adapters/claude-code/safety.md +6 -0
  6. package/adapters/codex/profile.json +55 -0
  7. package/adapters/codex/rules.md +12 -0
  8. package/adapters/codex/safety.md +5 -0
  9. package/adapters/contract.md +64 -0
  10. package/adapters/cursor/profile.json +60 -0
  11. package/adapters/cursor/rules.md +7 -0
  12. package/adapters/cursor/safety.md +6 -0
  13. package/adapters/trae/profile.json +61 -0
  14. package/adapters/trae/rules.md +7 -0
  15. package/adapters/trae/safety.md +7 -0
  16. package/cli/README.md +50 -9
  17. package/cli/audit-spec.md +403 -0
  18. package/cli/doctor-spec.md +109 -17
  19. package/cli/init-spec.md +81 -53
  20. package/cli/repair-spec.md +361 -0
  21. package/cli/spawn-blueprint-spec.md +18 -18
  22. package/cli/spawn-spec.md +103 -108
  23. package/cli/src/cli.js +9403 -1059
  24. package/cli/test/init.test.js +2566 -51
  25. package/cli/upgrade-spec.md +488 -0
  26. package/core/README.md +14 -3
  27. package/core/agent-lanes-session-naming-spec.md +364 -0
  28. package/core/agent-lanes-spec.md +507 -0
  29. package/core/baseline/file-boundaries.md +11 -3
  30. package/core/baseline/health-check.md +2 -2
  31. package/core/baseline/roles.md +0 -1
  32. package/core/baseline/spec.md +3 -2
  33. package/core/capabilities/README.md +5 -2
  34. package/core/capabilities/agent-lanes/capability.md +40 -0
  35. package/core/capabilities/decisions/capability.md +2 -2
  36. package/core/capabilities/knowledge/capability.md +72 -0
  37. package/core/capabilities/knowledge/skills/starworkKnowledgeProject/SKILL.md +57 -0
  38. package/core/capabilities/knowledge/skills/starworkKnowledgeProject/agents/openai.yaml +6 -0
  39. package/core/capabilities/knowledge/templates/en/README.md +20 -0
  40. package/core/capabilities/knowledge/templates/en/index.md +19 -0
  41. package/core/capabilities/knowledge/templates/en/log.md +16 -0
  42. package/core/capabilities/knowledge/templates/en/schema.md +60 -0
  43. package/core/capabilities/knowledge/templates/zh/README.md +20 -0
  44. package/core/capabilities/knowledge/templates/zh/index.md +19 -0
  45. package/core/capabilities/knowledge/templates/zh/log.md +16 -0
  46. package/core/capabilities/knowledge/templates/zh/schema.md +60 -0
  47. package/core/capabilities/main-repo-sync/capability.md +57 -34
  48. package/core/capabilities/skill-mount/capability.md +19 -5
  49. package/core/capabilities/starter-outputs/capability.md +9 -11
  50. package/core/capabilities/starter-outputs/templates/outputs/final/README.md +1 -1
  51. package/core/core-v0.1-protocol.md +23 -21
  52. package/core/kits/README.md +9 -1
  53. package/core/kits/hub/.incoming/README.md +4 -2
  54. package/core/kits/hub/.incoming/reports/.gitkeep +1 -0
  55. package/core/kits/hub/.incoming/skills/.gitkeep +1 -0
  56. package/core/kits/hub/.internal/twin-bot-merge-policy.md +5 -0
  57. package/core/kits/hub/.internal/twin-bot-writeback-protocol.md +5 -0
  58. package/core/kits/hub/.starwork/handoff/archived/.gitkeep +1 -0
  59. package/core/kits/hub/.starwork/handoff/inbox/.gitkeep +1 -0
  60. package/core/kits/hub/.starwork/handoff/outbox/.gitkeep +1 -0
  61. package/core/kits/hub/.starwork/handoff/sent/.gitkeep +1 -0
  62. package/core/kits/hub/.starwork/handoff/state.json +5 -0
  63. package/core/kits/hub/AGENTS.md +18 -12
  64. package/core/kits/hub/README.md +17 -9
  65. package/core/kits/hub/identity/README.md +2 -2
  66. package/core/kits/hub/knowledge/README.md +5 -0
  67. package/core/kits/hub/lessons/README.md +2 -2
  68. package/core/kits/hub/projects/README.md +5 -0
  69. package/core/kits/hub/projects/coordination/README.md +5 -0
  70. package/core/kits/hub/projects/coordination/messages/acknowledged/.gitkeep +1 -0
  71. package/core/kits/hub/projects/coordination/messages/closed/.gitkeep +1 -0
  72. package/core/kits/hub/projects/coordination/messages/delivered/.gitkeep +1 -0
  73. package/core/kits/hub/projects/coordination/messages/queued/.gitkeep +1 -0
  74. package/core/kits/hub/projects/coordination/reports/.gitkeep +1 -0
  75. package/core/kits/hub/skills/README.md +4 -2
  76. package/core/kits/hub/skills/registry.json +6 -0
  77. package/core/kits/hub/workspace/README.md +5 -0
  78. package/core/kits/kit-structure-reference.md +104 -191
  79. package/core/kits/project/.agents/skills/README.md +5 -0
  80. package/core/kits/project/.claude/skills/README.md +5 -0
  81. package/core/kits/project/.obsidian/README.md +5 -0
  82. package/core/kits/project/.starwork/handoff/archived/.gitkeep +1 -0
  83. package/core/kits/project/.starwork/handoff/inbox/.gitkeep +1 -0
  84. package/core/kits/project/.starwork/handoff/outbox/.gitkeep +1 -0
  85. package/core/kits/project/.starwork/handoff/sent/.gitkeep +1 -0
  86. package/core/kits/project/.starwork/handoff/state.json +5 -0
  87. package/core/kits/project/AGENTS.md +38 -0
  88. package/core/kits/project/README.md +44 -0
  89. package/core/kits/project/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/275/223/345/211/215/351/241/271/347/233/256.md +25 -0
  90. package/core/kits/project/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md +17 -0
  91. package/core/kits/project/_/347/263/273/347/273/237//346/225/231/350/256/255/README.md +17 -0
  92. package/core/kits/project/_/347/263/273/347/273/237//350/272/253/344/273/275/README.md +24 -0
  93. package/core/kits/two-kit-architecture-spec.md +695 -0
  94. package/core/legacy/README.md +16 -0
  95. package/core/legacy/kits/satellite-starter/.starwork/handoff/archived/.gitkeep +1 -0
  96. package/core/legacy/kits/satellite-starter/.starwork/handoff/inbox/.gitkeep +1 -0
  97. package/core/legacy/kits/satellite-starter/.starwork/handoff/outbox/.gitkeep +1 -0
  98. package/core/legacy/kits/satellite-starter/.starwork/handoff/sent/.gitkeep +1 -0
  99. package/core/legacy/kits/satellite-starter/.starwork/handoff/state.json +5 -0
  100. package/core/{kits → legacy/kits}/satellite-starter/AGENTS.md +7 -1
  101. package/core/{kits → legacy/kits}/satellite-starter/README.md +4 -4
  102. package/core/{kits → legacy/kits}/satellite-starter/_/347/263/273/347/273/237//344/270/273/345/272/223/345/220/214/346/255/245/README.md +3 -3
  103. package/core/legacy/kits/satellite-starter//350/276/223/345/207/272//347/241/256/350/256/244/346/210/220/346/236/234/README.md +5 -0
  104. package/core/{presets → legacy/presets}/satellite-starter.yaml +1 -1
  105. package/core/presets/README.md +17 -7
  106. package/core/presets/hub.yaml +23 -4
  107. package/core/presets/project.yaml +11 -0
  108. package/core/profiles/en/labels.json +2 -4
  109. package/core/profiles/en/paths.json +3 -4
  110. package/core/profiles/en/profile.md +7 -4
  111. package/core/profiles/en/templates/AGENTS.md +13 -9
  112. package/core/profiles/en/{reference-kits/local-starter/_system/context/project-status.md → templates/_system/context/current-project.md} +1 -1
  113. package/core/profiles/zh/labels.json +2 -4
  114. package/core/profiles/zh/paths.json +3 -5
  115. package/core/profiles/zh/profile.md +7 -4
  116. package/core/profiles/zh/templates/AGENTS.md +13 -9
  117. package/core/{kits/local-matter/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//351/241/271/347/233/256/347/212/266/346/200/201.md → profiles/zh/templates/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/275/223/345/211/215/351/241/271/347/233/256.md} +1 -1
  118. package/core/project-rules-optimization-spec.md +510 -0
  119. package/core/skill-management-spec.md +591 -0
  120. package/core/starwork-runtime-layer-spec.md +344 -0
  121. package/docs/README.md +18 -4
  122. package/docs/agent-install-guide.md +132 -0
  123. package/docs/ai-consultant-brief.md +648 -0
  124. package/docs/alpha-test-guide.md +274 -17
  125. package/docs/cli-capabilities.html +206 -115
  126. package/docs/cli-skill-registry.html +801 -0
  127. package/docs/doctor-capabilities.html +631 -0
  128. package/docs/hub-management.html +60 -59
  129. package/docs/index.html +7 -5
  130. package/docs/issue-feedback-tracking-guide.md +447 -0
  131. package/docs/m2.10-core-kit-pack-boundary-cleanup-spec.md +567 -0
  132. package/docs/m2.6-alpha-core-flows-optimization-spec.md +441 -0
  133. package/docs/m2.6-project-init-feedback/README.md +27 -0
  134. package/docs/m2.6-project-init-feedback/m2.6-project-init-feedback-01-identity-spec.md +194 -0
  135. package/docs/m2.6-project-init-feedback/m2.6-project-init-feedback-02-hub-sync-boundary-spec.md +198 -0
  136. package/docs/m2.6-project-init-feedback/m2.6-project-init-feedback-03-current-project-purity-spec.md +238 -0
  137. package/docs/m2.6-project-init-feedback/m2.6-project-init-feedback-04-system-placeholder-templates-spec.md +213 -0
  138. package/docs/m2.6-project-init-feedback/m2.6-project-init-feedback-05-agents-guardrails-spec.md +265 -0
  139. package/docs/m2.7-init-github-issues-optimization-spec.md +350 -0
  140. package/docs/m2.8-workspace-naming-optimization-spec.md +633 -0
  141. package/docs/multiagent/01-codex-session-capabilities-and-starwork-implications.md +280 -0
  142. package/docs/multiagent/02-cursor-session-management-research-instructions.md +204 -0
  143. package/docs/multiagent/03-trae-session-management-research-instructions.md +205 -0
  144. package/docs/multiagent/04-claude-code-session-management-research-instructions.md +209 -0
  145. package/docs/multiagent/05-trae-solo-session-management-research-instructions.md +222 -0
  146. package/docs/multiagent/claude-code-session-management-research-result.md +201 -0
  147. package/docs/multiagent/cursor-session-management-research-result.md +101 -0
  148. package/docs/multiagent/session-control-support-matrix.md +20 -0
  149. package/docs/multiagent/trae-session-management-research-result.md +353 -0
  150. package/docs/multiagent/trae-solo-session-management-research-result.md +467 -0
  151. package/docs/product-direction.md +17 -9
  152. package/docs/product-shape-business-model.html +1 -1
  153. package/docs/roadmap.html +34 -20
  154. package/docs/roadmap.md +185 -20
  155. package/docs/v0.1-plan.md +32 -12
  156. package/kit-skills/README.md +11 -0
  157. package/kit-skills/neat-freak/SKILL.md +32 -0
  158. package/kit-skills/neat-freak/agents/openai.yaml +2 -0
  159. package/kit-skills/starworkAudit/SKILL.md +127 -0
  160. package/kit-skills/starworkAudit/references/issue-taxonomy.md +10 -0
  161. package/kit-skills/starworkAudit/references/repair-blueprint-guide.md +54 -0
  162. package/kit-skills/starworkAudit/references/response-guide.md +34 -0
  163. package/{skills → kit-skills}/starworkSpawn/SKILL.md +36 -14
  164. package/package.json +4 -3
  165. package/packs/content-creator/languages/en.json +4 -4
  166. package/packs/content-creator/languages/zh.json +4 -4
  167. package/packs/content-creator/pack.json +5 -5
  168. package/packs/general/languages/en.json +19 -2
  169. package/packs/general/languages/zh.json +19 -2
  170. package/packs/general/pack.json +8 -3
  171. package/packs/hub-management/languages/en.json +2 -2
  172. package/packs/hub-management/languages/zh.json +11 -10
  173. package/packs/hub-management/pack.json +2 -2
  174. package/packs/hub-management/rules/en/overview.md +2 -0
  175. package/packs/hub-management/rules/en/workflow.md +2 -1
  176. package/packs/hub-management/rules/zh/overview.md +4 -2
  177. package/packs/hub-management/rules/zh/workflow.md +3 -2
  178. package/packs/hub-management/seed/en/projects/coordination/README.md +2 -2
  179. package/packs/hub-management/seed/zh/projects/coordination/README.md +5 -0
  180. package/packs/pack-structure-spec.md +4 -4
  181. package/skills/README.md +28 -3
  182. package/skills/starwork/SKILL.md +81 -0
  183. package/skills/starwork/references/install.md +64 -0
  184. package/skills/starwork/references/routing.md +44 -0
  185. package/skills/starworkAudit-spec.md +354 -0
  186. package/skills/starworkDoctor/SKILL.md +435 -0
  187. package/skills/starworkDoctor/agents/openai.yaml +7 -0
  188. package/skills/starworkDoctor/references/agent-rules-template.md +64 -0
  189. package/skills/starworkDoctor/references/hub-upgrade.md +73 -0
  190. package/skills/starworkDoctor/references/response-guide.md +57 -0
  191. package/skills/starworkDoctor/references/rules-extraction-guide.md +92 -0
  192. package/skills/starworkDoctor-spec.md +764 -0
  193. package/skills/starworkInit/SKILL.md +285 -50
  194. package/skills/starworkInit-spec.md +106 -75
  195. package/skills/starworkKnowledge/SKILL.md +195 -0
  196. package/skills/starworkKnowledge/agents/openai.yaml +3 -0
  197. package/skills/starworkMultiagent/SKILL.md +302 -0
  198. package/skills/starworkMultiagent/agents/openai.yaml +7 -0
  199. package/skills/starworkMultiagent-skill-plan.md +234 -0
  200. package/skills/starworkMultiagent-spec.md +181 -0
  201. package/core/capabilities/matter-mode/capability.md +0 -46
  202. package/core/capabilities/matter-mode/templates/matters/_matter-template/README.md +0 -17
  203. package/core/capabilities/matter-mode/templates/matters/_matter-template/handoff.md +0 -3
  204. package/core/capabilities/matter-mode/templates/matters/_matter-template/notes.md +0 -5
  205. package/core/capabilities/matter-mode/templates/matters/_matter-template/progress.md +0 -5
  206. package/core/capabilities/matter-mode/templates/matters/registry.md +0 -16
  207. package/core/kits/hub/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//351/241/271/347/233/256/347/212/266/346/200/201.md +0 -24
  208. package/core/kits/hub//347/237/245/350/257/206/README.md +0 -5
  209. package/core/kits/hub//351/241/271/347/233/256/README.md +0 -5
  210. package/core/kits/hub//351/241/271/347/233/256//350/201/224/347/273/234/README.md +0 -5
  211. package/core/kits/local-matter/AGENTS.md +0 -23
  212. package/core/kits/local-matter/README.md +0 -22
  213. package/core/kits/local-matter/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/206/263/347/255/226.md +0 -7
  214. package/core/kits/local-matter/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md +0 -17
  215. package/core/kits/local-matter/_/347/263/273/347/273/237//346/225/231/350/256/255/README.md +0 -5
  216. package/core/kits/local-matter/_/347/263/273/347/273/237//350/272/253/344/273/275/README.md +0 -5
  217. package/core/kits/local-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277/README.md +0 -17
  218. package/core/kits/local-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//344/272/244/346/216/245.md +0 -3
  219. package/core/kits/local-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//347/254/224/350/256/260.md +0 -5
  220. package/core/kits/local-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//350/277/233/345/272/246.md +0 -5
  221. package/core/kits/local-matter//344/272/213/351/241/271//346/263/250/345/206/214/350/241/250.md +0 -16
  222. package/core/kits/local-matter//350/276/223/345/207/272//347/241/256/350/256/244/346/210/220/346/236/234/README.md +0 -5
  223. package/core/kits/local-starter/AGENTS.md +0 -23
  224. package/core/kits/local-starter/README.md +0 -23
  225. package/core/kits/local-starter/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//351/241/271/347/233/256/347/212/266/346/200/201.md +0 -25
  226. package/core/kits/local-starter/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md +0 -17
  227. package/core/kits/local-starter/_/347/263/273/347/273/237//346/225/231/350/256/255/README.md +0 -5
  228. package/core/kits/local-starter/_/347/263/273/347/273/237//350/272/253/344/273/275/README.md +0 -5
  229. package/core/kits/local-starter//345/217/202/350/200/203/350/265/204/346/226/231/README.md +0 -5
  230. package/core/kits/local-starter//350/276/223/345/207/272//347/241/256/350/256/244/346/210/220/346/236/234/README.md +0 -5
  231. package/core/kits/local-starter//350/276/223/345/207/272//350/215/211/347/250/277/README.md +0 -5
  232. package/core/kits/satellite-matter/AGENTS.md +0 -27
  233. package/core/kits/satellite-matter/README.md +0 -40
  234. package/core/kits/satellite-matter/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/206/263/347/255/226.md +0 -7
  235. package/core/kits/satellite-matter/_/347/263/273/347/273/237//344/270/273/345/272/223/345/220/214/346/255/245/README.md +0 -42
  236. package/core/kits/satellite-matter/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md +0 -17
  237. package/core/kits/satellite-matter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/README.md +0 -9
  238. package/core/kits/satellite-matter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/inbox/README.md +0 -5
  239. package/core/kits/satellite-matter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/outbox/README.md +0 -5
  240. package/core/kits/satellite-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277/README.md +0 -17
  241. package/core/kits/satellite-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//344/272/244/346/216/245.md +0 -3
  242. package/core/kits/satellite-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//347/254/224/350/256/260.md +0 -5
  243. package/core/kits/satellite-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//350/277/233/345/272/246.md +0 -5
  244. package/core/kits/satellite-matter//344/272/213/351/241/271//346/263/250/345/206/214/350/241/250.md +0 -16
  245. package/core/kits/satellite-matter//345/217/202/350/200/203/350/265/204/346/226/231/README.md +0 -5
  246. package/core/kits/satellite-matter//350/276/223/345/207/272//347/241/256/350/256/244/346/210/220/346/236/234/README.md +0 -5
  247. package/core/kits/satellite-matter//350/276/223/345/207/272//350/215/211/347/250/277/README.md +0 -5
  248. package/core/kits/satellite-starter/.agents/skills/README.md +0 -5
  249. package/core/kits/satellite-starter/.claude/skills/README.md +0 -5
  250. package/core/kits/satellite-starter/.core-sync.json +0 -31
  251. package/core/kits/satellite-starter/.internal/README.md +0 -5
  252. package/core/kits/satellite-starter/.obsidian/README.md +0 -5
  253. package/core/kits/satellite-starter/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/275/223/345/211/215/351/241/271/347/233/256.md +0 -29
  254. package/core/kits/satellite-starter/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md +0 -17
  255. package/core/kits/satellite-starter/_/347/263/273/347/273/237//346/225/231/350/256/255/README.md +0 -8
  256. package/core/kits/satellite-starter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/README.md +0 -9
  257. package/core/kits/satellite-starter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/inbox/README.md +0 -5
  258. package/core/kits/satellite-starter/_/347/263/273/347/273/237//350/267/250/351/241/271/347/233/256/outbox/README.md +0 -5
  259. package/core/kits/satellite-starter/_/347/263/273/347/273/237//350/272/253/344/273/275/README.md +0 -5
  260. package/core/kits/satellite-starter//345/217/202/350/200/203/350/265/204/346/226/231/README.md +0 -5
  261. package/core/kits/satellite-starter//347/237/245/350/257/206/README.md +0 -5
  262. package/core/kits/satellite-starter//350/276/223/345/207/272//347/241/256/350/256/244/346/210/220/346/236/234/README.md +0 -5
  263. package/core/kits/satellite-starter//350/276/223/345/207/272//350/215/211/347/250/277/README.md +0 -5
  264. package/core/presets/local-matter.yaml +0 -14
  265. package/core/presets/local-starter.yaml +0 -12
  266. package/core/presets/satellite-matter.yaml +0 -17
  267. package/core/profiles/en/reference-kits/local-starter/AGENTS.md +0 -23
  268. package/core/profiles/en/reference-kits/local-starter/README.md +0 -23
  269. package/core/profiles/en/reference-kits/local-starter/_system/identity/README.md +0 -5
  270. package/core/profiles/en/reference-kits/local-starter/_system/lessons/README.md +0 -5
  271. package/core/profiles/en/reference-kits/local-starter/_system/tasks/current-work.md +0 -17
  272. package/core/profiles/en/reference-kits/local-starter/outputs/drafts/README.md +0 -5
  273. package/core/profiles/en/reference-kits/local-starter/outputs/final/README.md +0 -5
  274. package/core/profiles/en/reference-kits/local-starter/references/README.md +0 -5
  275. package/core/profiles/en/reference-presets/local-starter.yaml +0 -12
  276. package/core/profiles/en/templates/_system/context/project-status.md +0 -25
  277. package/core/profiles/zh/templates/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//351/241/271/347/233/256/347/212/266/346/200/201.md +0 -25
  278. package/packs/hub-management/seed/zh//351/241/271/347/233/256//350/201/224/347/273/234/README.md +0 -5
  279. /package/core/{capabilities/matter-mode/templates/matters/_matter-template/drafts → kits/hub/.incoming/identity}/.gitkeep +0 -0
  280. /package/core/kits/{local-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//350/215/211/347/250/277/.gitkeep" → hub/.incoming/knowledge/.gitkeep} +0 -0
  281. /package/core/kits/{satellite-matter//344/272/213/351/241/271/_/344/272/213/351/241/271/346/250/241/346/235/277//350/215/211/347/250/277/.gitkeep" → hub/.incoming/lessons/.gitkeep} +0 -0
  282. /package/core/kits/hub/{/351/241/271/347/233/256/registry.json" → projects/registry.json} +0 -0
  283. /package/core/kits/{satellite-matter → project}/CLAUDE.md +0 -0
  284. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/.agents/skills/README.md +0 -0
  285. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/.claude/skills/README.md +0 -0
  286. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/.core-sync.json +0 -0
  287. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/.internal/README.md +0 -0
  288. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/.obsidian/README.md +0 -0
  289. /package/core/{kits → legacy/kits}/satellite-starter/CLAUDE.md +0 -0
  290. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/_/347/263/273/347/273/237//344/270/212/344/270/213/346/226/207//345/275/223/345/211/215/351/241/271/347/233/256.md" +0 -0
  291. /package/core/{kits/hub → legacy/kits/satellite-starter}/_/347/263/273/347/273/237//344/273/273/345/212/241//345/275/223/345/211/215/345/267/245/344/275/234.md" +0 -0
  292. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/_/347/263/273/347/273/237//346/225/231/350/256/255/README.md" +0 -0
  293. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}/_/347/263/273/347/273/237//350/272/253/344/273/275/README.md" +0 -0
  294. /package/core/{kits/local-matter → legacy/kits/satellite-starter}//345/217/202/350/200/203/350/265/204/346/226/231/README.md" +0 -0
  295. /package/core/{kits/satellite-matter → legacy/kits/satellite-starter}//347/237/245/350/257/206/README.md" +0 -0
  296. /package/core/{kits/local-matter → legacy/kits/satellite-starter}//350/276/223/345/207/272//350/215/211/347/250/277/README.md" +0 -0
  297. /package/{skills → kit-skills}/starworkSpawn/agents/openai.yaml +0 -0
  298. /package/packs/hub-management/seed/zh/{/351/241/271/347/233/256/registry.json" → projects/registry.json} +0 -0
@@ -7,6 +7,7 @@ const { execFileSync, spawnSync } = require("node:child_process");
7
7
 
8
8
  const root = path.resolve(__dirname, "..", "..");
9
9
  const bin = path.join(root, "cli", "bin", "starwork.js");
10
+ const packageJson = require(path.join(root, "package.json"));
10
11
 
11
12
  function tempDir() {
12
13
  return fs.mkdtempSync(path.join(os.tmpdir(), "starwork-init-test-"));
@@ -26,9 +27,13 @@ function runDoctor(args) {
26
27
  });
27
28
  }
28
29
 
29
- function runCommand(args) {
30
+ function runCommand(args, options = {}) {
30
31
  return spawnSync(process.execPath, [bin, ...args], {
31
32
  cwd: root,
33
+ env: {
34
+ ...process.env,
35
+ ...(options.env || {})
36
+ },
32
37
  encoding: "utf8"
33
38
  });
34
39
  }
@@ -37,65 +42,928 @@ function readJson(file) {
37
42
  return JSON.parse(fs.readFileSync(file, "utf8"));
38
43
  }
39
44
 
45
+ function listFiles(dir) {
46
+ if (!fs.existsSync(dir)) return [];
47
+ const result = [];
48
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
49
+ const entryPath = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ result.push(...listFiles(entryPath));
52
+ } else {
53
+ result.push(entryPath);
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ function fakeCodexBin({ exitCode = 0, stderr = "", inputPath, failTurnStart = false, failThreadNameSet = false, omitFinalRead = false, omitTurnCompleted = false } = {}) {
60
+ const dir = tempDir();
61
+ const binDir = path.join(dir, "bin");
62
+ fs.mkdirSync(binDir, { recursive: true });
63
+ const codex = path.join(binDir, "codex");
64
+ fs.writeFileSync(codex, `#!/usr/bin/env node
65
+ const fs = require("fs");
66
+ const readline = require("readline");
67
+ if (${JSON.stringify(stderr)}) process.stderr.write(${JSON.stringify(stderr)});
68
+ if (${exitCode} !== 0) {
69
+ process.exit(${exitCode});
70
+ }
71
+ const rl = readline.createInterface({ input: process.stdin });
72
+ rl.on("line", (line) => {
73
+ if (process.env.STARWORK_FAKE_CODEX_INPUT) {
74
+ fs.appendFileSync(process.env.STARWORK_FAKE_CODEX_INPUT, line + "\\n");
75
+ }
76
+ const request = JSON.parse(line);
77
+ if (request.method === "thread/read") {
78
+ if (${Boolean(omitFinalRead)} && request.id >= 4) return;
79
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { thread: { id: request.params.threadId, name: "Fake Codex Thread", cwd: "/fake/project", status: "idle", turns: [{ id: "turn-1", status: "completed" }, { id: "turn-2", status: "completed" }] } } }));
80
+ } else if (request.method === "thread/name/set") {
81
+ if (${Boolean(failThreadNameSet)}) {
82
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { message: "rename failed" } }));
83
+ return;
84
+ }
85
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: {} }));
86
+ } else if (request.method === "thread/start") {
87
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { threadId: "launched-thread-1" } }));
88
+ } else if (request.method === "thread/list") {
89
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { data: [{ id: "dev-thread-2", name: "Fake Codex Thread" }] } }));
90
+ } else if (request.method === "turn/start") {
91
+ if (${Boolean(failTurnStart)}) {
92
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { message: "turn start failed" } }));
93
+ return;
94
+ }
95
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { turnId: "turn-started-1" } }));
96
+ console.log(JSON.stringify({ jsonrpc: "2.0", method: "turn/started", params: { turnId: "turn-started-1" } }));
97
+ if (!${Boolean(omitTurnCompleted)}) {
98
+ console.log(JSON.stringify({ jsonrpc: "2.0", method: "turn/completed", params: { turnId: "turn-started-1" } }));
99
+ }
100
+ } else {
101
+ console.log(JSON.stringify({ jsonrpc: "2.0", id: request.id, result: {} }));
102
+ }
103
+ });
104
+ `, "utf8");
105
+ fs.chmodSync(codex, 0o755);
106
+ return {
107
+ env: {
108
+ PATH: `${binDir}${path.delimiter}${process.env.PATH}`,
109
+ ...(inputPath ? { STARWORK_FAKE_CODEX_INPUT: inputPath } : {})
110
+ }
111
+ };
112
+ }
113
+
114
+ function fakeCursorBin({ exitCode = 0, stdout = "Logged in as fake@example.com\n", stderr = "" } = {}) {
115
+ const dir = tempDir();
116
+ const binDir = path.join(dir, "bin");
117
+ fs.mkdirSync(binDir, { recursive: true });
118
+ const cursor = path.join(binDir, "cursor");
119
+ fs.writeFileSync(cursor, `#!/usr/bin/env node
120
+ if (${JSON.stringify(stderr)}) process.stderr.write(${JSON.stringify(stderr)});
121
+ const args = process.argv.slice(2).join(" ");
122
+ if (args === "agent status") {
123
+ if (${exitCode} !== 0) process.exit(${exitCode});
124
+ process.stdout.write(${JSON.stringify(stdout)});
125
+ process.exit(0);
126
+ }
127
+ process.exit(0);
128
+ `, "utf8");
129
+ fs.chmodSync(cursor, 0o755);
130
+ return {
131
+ env: {
132
+ PATH: `${binDir}${path.delimiter}${process.env.PATH}`
133
+ }
134
+ };
135
+ }
136
+
137
+ function writeCursorTranscriptFixture(projectsDir, sessionId, lines, projectKey = "cursor-project") {
138
+ const transcriptDir = path.join(projectsDir, projectKey, "agent-transcripts", sessionId);
139
+ fs.mkdirSync(transcriptDir, { recursive: true });
140
+ const transcript = path.join(transcriptDir, `${sessionId}.jsonl`);
141
+ fs.writeFileSync(transcript, lines.join("\n") + "\n", "utf8");
142
+ return transcript;
143
+ }
144
+
145
+ function skillDescription(skillText) {
146
+ return skillText.match(/^description:\s*['"]?([\s\S]*?)['"]?\n---/m)?.[1] || "";
147
+ }
148
+
149
+ test("prints version and product-oriented help", () => {
150
+ const version = runCommand(["--version"]);
151
+ assert.equal(version.status, 0);
152
+ assert.equal(version.stdout.trim(), packageJson.version);
153
+
154
+ const help = runCommand(["--help"]);
155
+ assert.equal(help.status, 0);
156
+ assert.match(help.stdout, new RegExp(`StarWork CLI ${packageJson.version}`));
157
+ assert.match(help.stdout, /常用开始/);
158
+ assert.match(help.stdout, /starwork init --help/);
159
+ });
160
+
161
+ test("skill management v0.2 exposes main router and scoped skill layers", () => {
162
+ const systemSkillNames = fs.readdirSync(path.join(root, "skills"), { withFileTypes: true })
163
+ .filter((entry) => entry.isDirectory())
164
+ .map((entry) => entry.name)
165
+ .sort();
166
+ const kitSkillNames = fs.readdirSync(path.join(root, "kit-skills"), { withFileTypes: true })
167
+ .filter((entry) => entry.isDirectory())
168
+ .map((entry) => entry.name)
169
+ .sort();
170
+ const capabilitySkillPath = path.join(root, "core", "capabilities", "knowledge", "skills", "starworkKnowledgeProject", "SKILL.md");
171
+ const mainSkill = fs.readFileSync(path.join(root, "skills", "starwork", "SKILL.md"), "utf8");
172
+ const routing = fs.readFileSync(path.join(root, "skills", "starwork", "references", "routing.md"), "utf8");
173
+ const install = fs.readFileSync(path.join(root, "skills", "starwork", "references", "install.md"), "utf8");
174
+
175
+ assert.deepEqual(systemSkillNames, ["starwork", "starworkDoctor", "starworkInit", "starworkKnowledge", "starworkMultiagent"]);
176
+ assert.deepEqual(kitSkillNames, ["neat-freak", "starworkAudit", "starworkSpawn"]);
177
+ assert.equal(fs.existsSync(capabilitySkillPath), true);
178
+ assert.match(mainSkill, /StarWork 是给 AI 协作准备的项目工作台/);
179
+ assert.match(mainSkill, /references\/routing\.md/);
180
+ assert.match(mainSkill, /references\/install\.md/);
181
+ assert.match(mainSkill, /starworkInit/);
182
+ assert.match(mainSkill, /starworkDoctor/);
183
+ assert.match(mainSkill, /starworkKnowledge/);
184
+ assert.match(mainSkill, /starworkMultiagent/);
185
+ assert.doesNotMatch(mainSkill, /set_thread_title|create_thread|pages\/|synthesis\/|upgrade blueprint/);
186
+ assert.match(routing, /L0 主入口/);
187
+ assert.match(routing, /多 Agent[\s\S]*starworkMultiagent/);
188
+ assert.match(routing, /从项目中心创建项目[\s\S]*starworkSpawn/);
189
+ assert.match(install, /npx skills add jennie-shawn\/StarWork -g -a codex -y/);
190
+ assert.match(install, /starwork[\s\S]*starworkInit[\s\S]*starworkDoctor[\s\S]*starworkKnowledge[\s\S]*starworkMultiagent/);
191
+ assert.doesNotMatch(install, /全局安装[\s\S]*(starworkSpawn|starworkAudit|neat-freak|starworkKnowledgeProject)/);
192
+ });
193
+
194
+ test("specialist skill descriptions avoid fuzzy StarWork entrypoints", () => {
195
+ const specialists = ["starworkInit", "starworkDoctor", "starworkKnowledge", "starworkMultiagent"];
196
+ const fuzzyEntryPattern = /StarWork 是什么|怎么开始|安装 StarWork|帮我用 StarWork|能做什么/;
197
+
198
+ for (const name of specialists) {
199
+ const skill = fs.readFileSync(path.join(root, "skills", name, "SKILL.md"), "utf8");
200
+ assert.doesNotMatch(skillDescription(skill), fuzzyEntryPattern, `${name} description should not claim fuzzy entrypoints`);
201
+ assert.match(skill, /模糊|主入口|starwork` 主入口/, `${name} should point fuzzy requests back to starwork`);
202
+ }
203
+
204
+ const multiagent = fs.readFileSync(path.join(root, "skills", "starworkMultiagent", "SKILL.md"), "utf8");
205
+ assert.match(skillDescription(multiagent), /多 Agent|Agent Lanes|lane|跨会话|Codex/);
206
+ assert.match(multiagent, /多 Agent 分工/);
207
+ assert.match(multiagent, /lane/);
208
+ assert.match(multiagent, /跨会话/);
209
+ assert.match(multiagent, /Codex 标准/);
210
+ });
211
+
212
+ test("public docs describe main StarWork skill and keep kit skills out of global install", () => {
213
+ const readme = fs.readFileSync(path.join(root, "README.md"), "utf8");
214
+ const installGuide = fs.readFileSync(path.join(root, "docs", "agent-install-guide.md"), "utf8");
215
+ const alphaGuide = fs.readFileSync(path.join(root, "docs", "alpha-test-guide.md"), "utf8");
216
+ const skillsReadme = fs.readFileSync(path.join(root, "skills", "README.md"), "utf8");
217
+ const managementSpec = fs.readFileSync(path.join(root, "core", "skill-management-spec.md"), "utf8");
218
+ const registry = fs.readFileSync(path.join(root, "docs", "cli-skill-registry.html"), "utf8");
219
+ const alphaExpectedSkills = alphaGuide.match(/预期只看到:\n\n([\s\S]*?)\n\n不应看到/)?.[1] || "";
220
+
221
+ assert.match(readme, /StarWork 主入口/);
222
+ assert.match(readme, /`starwork` 主入口/);
223
+ assert.doesNotMatch(readme, /全局安装[\s\S]*(starworkSpawn|starworkAudit|neat-freak|starworkKnowledgeProject)/);
224
+ assert.match(installGuide, /StarWork 主入口和专家 Skills/);
225
+ assert.match(installGuide, /`starwork`/);
226
+ assert.match(installGuide, /`starworkInit`/);
227
+ assert.match(installGuide, /`starworkDoctor`/);
228
+ assert.match(installGuide, /`starworkKnowledge`/);
229
+ assert.match(installGuide, /`starworkMultiagent`/);
230
+ assert.doesNotMatch(installGuide, /确认能看到[\s\S]*(starworkSpawn|starworkAudit|neat-freak|starworkKnowledgeProject)/);
231
+ assert.match(alphaGuide, /L0 主入口 \+ L1 专家 Skills/);
232
+ assert.match(alphaGuide, /`starwork`/);
233
+ assert.match(alphaGuide, /`starworkInit`/);
234
+ assert.match(alphaGuide, /`starworkDoctor`/);
235
+ assert.match(alphaGuide, /`starworkKnowledge`/);
236
+ assert.match(alphaGuide, /`starworkMultiagent`/);
237
+ assert.doesNotMatch(alphaExpectedSkills, /starworkSpawn|starworkAudit|neat-freak|starworkKnowledgeProject/);
238
+ assert.match(skillsReadme, /L0 主入口/);
239
+ assert.match(skillsReadme, /L1 系统专家/);
240
+ assert.match(skillsReadme, /L2 Kit 自带/);
241
+ assert.match(skillsReadme, /L3 Capability 项目内/);
242
+ assert.match(managementSpec, /L0 主入口 Skill/);
243
+ assert.match(managementSpec, /starwork/);
244
+ assert.match(registry, /10 个可用 CLI 命令和 9 个 StarWork 自研 Skill/);
245
+ assert.match(registry, /5 个全局系统 Skill、3 个 Kit 自带 Skill、1 个 Capability 项目内 Skill/);
246
+ assert.match(registry, /starwork<\/code>/);
247
+ assert.match(registry, /L0 主入口/);
248
+ assert.match(registry, /starworkAudit<\/code>/);
249
+ assert.match(registry, /L2 Kit 自带/);
250
+ assert.match(registry, /starworkKnowledgeProject<\/code>/);
251
+ assert.match(registry, /L3 Capability 项目内/);
252
+ });
253
+
254
+ test("starworkMultiagent skill uses Codex standard session tools directly", () => {
255
+ const skill = fs.readFileSync(path.join(root, "skills", "starworkMultiagent", "SKILL.md"), "utf8");
256
+
257
+ assert.doesNotMatch(skill, /\| Host \|/);
258
+ assert.doesNotMatch(skill, /Codex app-server|app-server/);
259
+ assert.doesNotMatch(skill, /Claude Code \|/);
260
+ assert.doesNotMatch(skill, /multiagent launch --lanes/);
261
+ assert.doesNotMatch(skill, /starwork multiagent instruct|starwork multiagent launch/);
262
+ assert.doesNotMatch(skill, /multiagent message instruct|multiagent message launch/);
263
+ assert.doesNotMatch(skill, /multiagent read --host codex|multiagent status --host codex/);
264
+ assert.doesNotMatch(skill, /--session-name|--pin/);
265
+ assert.doesNotMatch(skill, /launch_status|binding_status|host_action_required|host-action complete/);
266
+ assert.doesNotMatch(skill, /thread\/start|turn\/start|thread\/resume|thread\/name\/set|thread\/read|thread\/list/);
267
+ assert.match(skill, /<职责名> Agent/);
268
+ assert.match(skill, /create_thread/);
269
+ assert.match(skill, /send_message_to_thread/);
270
+ assert.match(skill, /read_thread/);
271
+ assert.match(skill, /set_thread_title/);
272
+ assert.match(skill, /set_thread_pinned/);
273
+ assert.match(skill, /set_thread_archived/);
274
+ assert.match(skill, /multiagent status --target/);
275
+ assert.match(skill, /multiagent add/);
276
+ assert.match(skill, /multiagent bind/);
277
+ assert.match(skill, /multiagent share/);
278
+ assert.match(skill, /multiagent request record/);
279
+ assert.match(skill, /delivered_via_codex_thread_tool/);
280
+ assert.match(skill, /STARWORK:MULTIAGENT_MESSAGE v1/);
281
+ assert.match(skill, /manual_handoff_required/);
282
+ assert.match(skill, /pending_merge/);
283
+ });
284
+
285
+ test("starworkInit skill keeps existing projects in agent-docs draft mode", () => {
286
+ const skill = fs.readFileSync(path.join(root, "skills", "starworkInit", "SKILL.md"), "utf8");
287
+
288
+ assert.match(skill, /StarWork 是给 AI 协作准备的项目工作台/);
289
+ assert.match(skill, /确认这个工作台服务哪个项目/);
290
+ assert.match(skill, /预览 StarWork 准备补哪些协作文件/);
291
+ assert.match(skill, /不会直接改你的业务代码/);
292
+ assert.match(skill, /--agent-docs draft/);
293
+ assert.match(skill, /已有非空项目/);
294
+ assert.match(skill, /每次只问一个问题/);
295
+ assert.doesNotMatch(skill, /starwork init --type project --pack general --language zh --adapter codex --target <path> --yes/);
296
+ });
297
+
298
+ test("init-family skills start with user-facing capability framing", () => {
299
+ const knowledge = fs.readFileSync(path.join(root, "skills", "starworkKnowledge", "SKILL.md"), "utf8");
300
+ const multiagent = fs.readFileSync(path.join(root, "skills", "starworkMultiagent", "SKILL.md"), "utf8");
301
+ const doctor = fs.readFileSync(path.join(root, "skills", "starworkDoctor", "SKILL.md"), "utf8");
302
+ const spawn = fs.readFileSync(path.join(root, "kit-skills", "starworkSpawn", "SKILL.md"), "utf8");
303
+ const spawnFirstScreen = spawn.split("第一屏之后")[0];
304
+
305
+ assert.match(knowledge, /项目知识库是让 AI 长期记住项目稳定理解的地方/);
306
+ assert.match(knowledge, /不是原始资料文件夹/);
307
+ assert.match(knowledge, /先检查当前项目是否已经有知识库/);
308
+ assert.match(multiagent, /多 Agent 分工是把一个项目里的不同 AI 会话按职责分开协作/);
309
+ assert.match(multiagent, /先设计职责/);
310
+ assert.match(multiagent, /再创建或绑定会话/);
311
+ assert.match(doctor, /诊断是先看清当前目录的事实/);
312
+ assert.match(doctor, /升级是无损补齐 StarWork 工作台规则/);
313
+ assert.match(doctor, /不会移动、删除或覆盖你的历史文件/);
314
+ assert.match(spawnFirstScreen, /从项目中心创建项目工作台,是把一个新项目登记到项目中心/);
315
+ assert.match(spawnFirstScreen, /项目中心负责登记多个项目/);
316
+ assert.match(spawnFirstScreen, /新项目工作台负责具体项目的日常协作/);
317
+ assert.match(spawnFirstScreen, /先确认新项目是什么、要交付什么/);
318
+ assert.match(spawnFirstScreen, /先预览,不会在你确认前创建项目工作台/);
319
+ assert.doesNotMatch(spawnFirstScreen, /Spawn Blueprint 是一个小型配置包/);
320
+ });
321
+
322
+ test("init help explains preview and safe agent docs language", () => {
323
+ const result = runCommand(["init", "--help"]);
324
+
325
+ assert.equal(result.status, 0);
326
+ assert.match(result.stdout, /starwork init 会把一个目录整理成 StarWork 工作台,让 AI 能找到项目说明、当前任务、协作规则和交接记录。/);
327
+ assert.match(result.stdout, /--dry-run[\s\S]*预览将要写入的文件,不做真实改动。/);
328
+ assert.match(result.stdout, /--yes[\s\S]*确认执行,会真实写入 StarWork 工作台文件。/);
329
+ assert.match(result.stdout, /--agent-docs <draft\|skip\|write>[\s\S]*已有 AI 规则文件时,先生成待整合草稿,不覆盖原文件。/);
330
+ });
331
+
40
332
  test("dry-run does not write files", () => {
41
333
  const dir = tempDir();
42
334
  const output = runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--dry-run"]);
43
335
 
44
- assert.match(output, /初始化预览/);
336
+ assert.match(output, /创建工作台预览/);
337
+ assert.match(output, /这是预览,不会写入文件。/);
338
+ assert.match(output, new RegExp(`目标目录:${dir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
339
+ assert.match(output, /是否新建目录:否,目标目录已存在/);
340
+ assert.match(output, /日常工作会放在:输出\/草稿\//);
341
+ assert.match(output, /会创建:/);
342
+ assert.match(output, /会更新:/);
343
+ assert.match(output, /不会改动:/);
344
+ assert.match(output, /你的业务代码/);
345
+ assert.match(output, /已有非空 AI 规则文件/);
346
+ assert.match(output, /需要你确认:/);
347
+ assert.match(output, /目标路径是否正确/);
348
+ assert.match(output, /是否接受这些 StarWork 协作文件/);
45
349
  assert.equal(fs.existsSync(path.join(dir, "AGENTS.md")), false);
46
350
  assert.equal(fs.existsSync(path.join(dir, ".starwork", "workspace.json")), false);
47
351
  });
48
352
 
353
+ test("init dry-run explains existing project draft safety", () => {
354
+ const dir = tempDir();
355
+ fs.writeFileSync(path.join(dir, "README.md"), "# Existing\n", "utf8");
356
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent Rules\n", "utf8");
357
+
358
+ const output = runInit(["--type", "project", "--pack", "general", "--target", dir, "--agent-docs", "draft", "--dry-run"]);
359
+
360
+ assert.match(output, /检测到这是已有项目。/);
361
+ assert.match(output, /StarWork 会保留现有文件/);
362
+ assert.match(output, /先生成待整合草稿/);
363
+ assert.match(output, /不直接覆盖已有 AI 规则文件/);
364
+ assert.match(output, /\.starwork\/drafts\/README\.proposed\.md/);
365
+ assert.match(output, /\.starwork\/drafts\/AGENTS\.proposed\.md/);
366
+ assert.equal(fs.existsSync(path.join(dir, ".starwork")), false);
367
+ });
368
+
369
+ test("init json dry-run includes user summary for skills", () => {
370
+ const dir = tempDir();
371
+ fs.writeFileSync(path.join(dir, "README.md"), "# Existing\n", "utf8");
372
+ const result = runCommand(["init", "--type", "project", "--pack", "general", "--target", dir, "--agent-docs", "draft", "--dry-run", "--json"]);
373
+
374
+ assert.equal(result.status, 0);
375
+ const payload = JSON.parse(result.stdout);
376
+ assert.equal(payload.schema, "starwork.init.plan_result.v0.1");
377
+ assert.equal(payload.user_summary.product_purpose, "把项目整理成 AI 协作工作台");
378
+ assert.equal(payload.user_summary.mode, "preview_no_write");
379
+ assert.equal(payload.user_summary.target_kind, "existing_project");
380
+ assert.ok(payload.user_summary.will_create.includes(".starwork/workspace.json"));
381
+ assert.ok(payload.user_summary.will_not_touch.includes("你的业务代码"));
382
+ assert.ok(payload.user_summary.needs_confirmation.includes("目标路径是否正确"));
383
+ });
384
+
385
+ test("init dry-run groups rule slot writes by actual target existence", () => {
386
+ const dir = tempDir();
387
+ const rulePath = ".starwork/rules/pack.general.overview.md";
388
+ const manifestPath = ".starwork/rules/manifest.json";
389
+ const indexPath = ".starwork/rules/index.md";
390
+
391
+ const output = runInit(["--type", "project", "--pack", "general", "--target", dir, "--dry-run"]);
392
+ const createSection = output.match(/会创建:\n([\s\S]*?)\n\n会更新:/)?.[1] || "";
393
+ const updateSection = output.match(/会更新:\n([\s\S]*?)\n\n不会改动:/)?.[1] || "";
394
+
395
+ assert.match(createSection, new RegExp(rulePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
396
+ assert.match(createSection, new RegExp(manifestPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
397
+ assert.match(createSection, new RegExp(indexPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
398
+ assert.doesNotMatch(updateSection, new RegExp(rulePath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
399
+ assert.equal(fs.existsSync(path.join(dir, rulePath)), false);
400
+ });
401
+
402
+ test("init json user summary groups planned overwrites by actual target existence", () => {
403
+ const dir = tempDir();
404
+ const rulePath = ".starwork/rules/pack.general.overview.md";
405
+ const newResult = runCommand(["init", "--type", "project", "--pack", "general", "--target", dir, "--dry-run", "--json"]);
406
+
407
+ assert.equal(newResult.status, 0);
408
+ const newPayload = JSON.parse(newResult.stdout);
409
+ assert.ok(newPayload.user_summary.will_create.includes(rulePath));
410
+ assert.equal(newPayload.user_summary.will_update.includes(rulePath), false);
411
+
412
+ fs.mkdirSync(path.join(dir, ".starwork", "rules"), { recursive: true });
413
+ fs.writeFileSync(path.join(dir, rulePath), "# Existing rule\n", "utf8");
414
+
415
+ const existingResult = runCommand(["init", "--type", "project", "--pack", "general", "--target", dir, "--dry-run", "--json"]);
416
+ assert.equal(existingResult.status, 0);
417
+ const existingPayload = JSON.parse(existingResult.stdout);
418
+ assert.ok(existingPayload.user_summary.will_update.includes(rulePath));
419
+ assert.equal(existingPayload.user_summary.will_create.includes(rulePath), false);
420
+ });
421
+
422
+ test("init dry-run shows absolute target for a new folder", () => {
423
+ const parent = tempDir();
424
+ const target = path.join(parent, "review-workspace");
425
+ const output = runInit(["--type", "project", "--pack", "general", "--target", target, "--dry-run"]);
426
+
427
+ assert.match(output, new RegExp(`目标目录:${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
428
+ assert.match(output, /是否新建目录:是/);
429
+ assert.equal(fs.existsSync(target), false);
430
+ });
431
+
432
+ test("init dry-run uses the user-confirmed target instead of a suggested folder name", () => {
433
+ const parent = tempDir();
434
+ const target = path.join(parent, "my-reviewed-name");
435
+ const output = runInit(["--type", "project", "--pack", "general", "--name", "产品发布计划", "--target", target, "--dry-run"]);
436
+
437
+ assert.match(output, new RegExp(`目标目录:${target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
438
+ assert.doesNotMatch(output, /product-launch-plan/);
439
+ assert.equal(fs.existsSync(target), false);
440
+ });
441
+
442
+ test("init dry-run shows selected language", () => {
443
+ const dir = tempDir();
444
+ const output = runInit(["--type", "single-light", "--pack", "general", "--language", "en", "--target", dir, "--dry-run"]);
445
+
446
+ assert.match(output, /语言:英文/);
447
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "workspace.json")), false);
448
+ });
449
+
450
+ test("init rejects unsupported language", () => {
451
+ const dir = tempDir();
452
+ const result = runCommand(["init", "--type", "single-light", "--pack", "general", "--language", "fr", "--target", dir, "--dry-run"]);
453
+
454
+ assert.notEqual(result.status, 0);
455
+ assert.match(result.stderr, /不支持的语言:fr/);
456
+ });
457
+
49
458
  test("creates a single-light workspace with general pack", () => {
50
459
  const dir = tempDir();
51
- runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
460
+ const output = runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
52
461
 
53
462
  const state = readJson(path.join(dir, ".starwork", "workspace.json"));
54
- assert.equal(state.workspace_type, "single-light");
55
- assert.equal(state.kit, "local-starter");
463
+ const skills = readJson(path.join(dir, ".starwork", "skills.json"));
464
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
465
+ const identity = fs.readFileSync(path.join(dir, "_系统", "身份", "README.md"), "utf8");
466
+ const lessons = fs.readFileSync(path.join(dir, "_系统", "教训", "README.md"), "utf8");
467
+ const projectStatus = fs.readFileSync(path.join(dir, "_系统", "上下文", "当前项目.md"), "utf8");
468
+ const currentWork = fs.readFileSync(path.join(dir, "_系统", "任务", "当前工作.md"), "utf8");
469
+ assert.match(output, /StarWork 工作台已经创建好了。/);
470
+ assert.match(output, /这次写入的是项目协作文件,不是业务代码。/);
471
+ assert.match(output, /下一步你可以用 Codex \/ Claude Code \/ Cursor 打开这个目录/);
472
+ assert.equal(state.workspace_type, "project");
473
+ assert.equal(state.kit, "project");
56
474
  assert.equal(state.packs[0].id, "general");
475
+ assert.equal(skills.skills[0].id, "neat-freak");
476
+ assert.equal(skills.skills[0].source.kind, "kit");
477
+ assert.equal(fs.existsSync(path.join(dir, ".agents", "skills", "neat-freak", "SKILL.md")), true);
57
478
  assert.equal(fs.existsSync(path.join(dir, "AGENTS.md")), true);
58
479
  assert.equal(fs.existsSync(path.join(dir, "输出", "确认成果", "README.md")), true);
59
480
  assert.equal(fs.existsSync(path.join(dir, "_系统", "身份", "README.md")), true);
60
481
  assert.equal(fs.existsSync(path.join(dir, "_系统", "教训", "README.md")), true);
482
+ assert.equal(fs.existsSync(path.join(dir, "知识")), false);
483
+ assert.equal(fs.existsSync(path.join(dir, "知识库")), false);
484
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "主库同步")), false);
485
+ assert.equal(fs.existsSync(path.join(dir, ".core-sync.json")), false);
486
+ assert.equal(fs.existsSync(path.join(dir, ".internal")), false);
487
+ assert.match(agents, /相关时再读/);
488
+ assert.match(agents, /_系统\/身份\/README\.md/);
489
+ assert.match(agents, /_系统\/教训\/README\.md/);
490
+ assert.doesNotMatch(agents, /Folders Not Used|Initialized as|blueprint|dry-run/);
491
+ assert.match(identity, /长期背景/);
492
+ assert.match(identity, /沟通偏好/);
493
+ assert.match(identity, /稳定约束/);
494
+ assert.doesNotMatch(identity, /主库分发|初始化快照|Hub identity|satellite/i);
495
+ assert.match(lessons, /已确认教训/);
496
+ assert.match(lessons, /候选教训/);
497
+ assert.match(projectStatus, /## 目标/);
498
+ assert.match(projectStatus, /## 当前阶段/);
499
+ assert.match(projectStatus, /## 近期重点/);
500
+ assert.match(projectStatus, /## 主要事实源/);
501
+ assert.match(projectStatus, /## 风险/);
502
+ assert.doesNotMatch(projectStatus, /Initialized as|StarWork project workspace|blueprint|Folders Not Used|doctor/);
503
+ assert.match(currentWork, /## 现在/);
504
+ assert.match(currentWork, /## 给下一个 AI 的备注/);
61
505
  });
62
506
 
63
- test("creates a single-matter workspace with content creator pack", () => {
507
+ test("creates an English project workspace with standalone system templates", () => {
64
508
  const dir = tempDir();
65
- runInit(["--type", "single-matter", "--pack", "content-creator", "--target", dir, "--yes"]);
509
+ runInit(["--type", "project", "--pack", "general", "--language", "en", "--target", dir, "--yes"]);
510
+
511
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
512
+ const identity = fs.readFileSync(path.join(dir, "_system", "identity", "README.md"), "utf8");
513
+ const lessons = fs.readFileSync(path.join(dir, "_system", "lessons", "README.md"), "utf8");
514
+ const projectStatus = fs.readFileSync(path.join(dir, "_system", "context", "current-project.md"), "utf8");
515
+ const currentWork = fs.readFileSync(path.join(dir, "_system", "tasks", "current-work.md"), "utf8");
516
+ const doctor = runDoctor(["--target", dir, "--json"]);
517
+ const report = JSON.parse(doctor.stdout);
66
518
 
519
+ assert.equal(fs.existsSync(path.join(dir, "_system", "main-repo-sync")), false);
520
+ assert.equal(fs.existsSync(path.join(dir, "knowledge")), false);
521
+ assert.equal(fs.existsSync(path.join(dir, "knowledge-base")), false);
522
+ assert.equal(fs.existsSync(path.join(dir, ".core-sync.json")), false);
523
+ assert.equal(fs.existsSync(path.join(dir, ".internal")), false);
524
+ assert.match(agents, /Read When Relevant/);
525
+ assert.match(agents, /_system\/identity\/README\.md/);
526
+ assert.match(agents, /_system\/lessons\/README\.md/);
527
+ assert.doesNotMatch(agents, /Folders Not Used|Initialized as|blueprint|dry-run/);
528
+ assert.match(identity, /Durable Context/);
529
+ assert.match(identity, /Communication Preferences/);
530
+ assert.match(identity, /Stable Constraints/);
531
+ assert.doesNotMatch(identity, /Hub identity snapshot|main repo|synced main-repository|satellite/i);
532
+ assert.match(lessons, /Active Lessons/);
533
+ assert.match(lessons, /Candidate Lessons/);
534
+ assert.match(lessons, /How To Add A Lesson/);
535
+ assert.match(projectStatus, /## Goal/);
536
+ assert.match(projectStatus, /## Current Stage/);
537
+ assert.match(projectStatus, /## Focus/);
538
+ assert.match(projectStatus, /## Primary Sources/);
539
+ assert.match(projectStatus, /## Risks/);
540
+ assert.match(projectStatus, /## Next Step/);
541
+ assert.doesNotMatch(projectStatus, /Initialized as|StarWork project workspace|blueprint|Folders Not Used|doctor/);
542
+ assert.match(currentWork, /## Now/);
543
+ assert.match(currentWork, /## Notes For Next AI/);
544
+ assert.equal(doctor.status, 0);
545
+ assert.equal(report.ok, true);
546
+ });
547
+
548
+ test("knowledge init creates an optional local project knowledge base", () => {
549
+ const dir = tempDir();
550
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
551
+
552
+ const preview = runCommand(["knowledge", "init", "--target", dir, "--dry-run"]);
553
+ assert.equal(preview.status, 0);
554
+ assert.match(preview.stdout, /知识库\/schema\.md/);
555
+ assert.match(preview.stdout, /starworkKnowledgeProject\/SKILL\.md/);
556
+ assert.equal(fs.existsSync(path.join(dir, "知识库")), false);
557
+
558
+ const result = runCommand(["knowledge", "init", "--target", dir, "--yes"]);
67
559
  const state = readJson(path.join(dir, ".starwork", "workspace.json"));
560
+ const status = runCommand(["knowledge", "status", "--target", dir, "--json"]);
561
+ const report = JSON.parse(status.stdout);
562
+ const doctor = runDoctor(["--target", dir, "--json"]);
563
+ const doctorReport = JSON.parse(doctor.stdout);
564
+
565
+ assert.equal(result.status, 0);
566
+ assert.equal(state.capabilities.knowledge.enabled, true);
567
+ assert.equal(state.capabilities.knowledge.root, "知识库");
568
+ assert.equal(state.capabilities.knowledge.language, "zh");
569
+ assert.equal(state.capabilities.knowledge.version, "0.1");
570
+ assert.deepEqual(state.capabilities.knowledge.project_skill_ids, ["starworkKnowledgeProject"]);
571
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "README.md")), true);
572
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "index.md")), true);
573
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "schema.md")), true);
574
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "log.md")), true);
575
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "pages")), true);
576
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "synthesis")), true);
577
+ assert.match(fs.readFileSync(path.join(dir, "知识库", "schema.md"), "utf8"), /`pages\/` 写作规则/);
578
+ assert.match(fs.readFileSync(path.join(dir, "知识库", "schema.md"), "utf8"), /`synthesis\/` 写作规则/);
579
+ assert.equal(fs.existsSync(path.join(dir, ".agents", "skills", "starworkKnowledgeProject", "SKILL.md")), true);
580
+ assert.equal(fs.existsSync(path.join(dir, ".claude", "skills", "starworkKnowledgeProject", "SKILL.md")), true);
581
+ const skills = readJson(path.join(dir, ".starwork", "skills.json"));
582
+ assert.equal(skills.skills.some((skill) => skill.id === "starworkKnowledgeProject"), true);
583
+ assert.equal(status.status, 0);
584
+ assert.equal(report.enabled, true);
585
+ assert.equal(report.root, "知识库");
586
+ assert.equal(report.skills.project_skill_installed, true);
587
+ assert.deepEqual(report.skills.project_skill_ids, ["starworkKnowledgeProject"]);
588
+ assert.equal(Object.hasOwn(report, "next_steps"), false);
589
+ assert.equal(doctor.status, 0);
590
+ assert.equal(doctorReport.knowledge.enabled, true);
591
+ });
592
+
593
+ test("knowledge init is idempotent and preserves user-edited knowledge files", () => {
594
+ const dir = tempDir();
595
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
596
+
597
+ const first = runCommand(["knowledge", "init", "--target", dir, "--yes"]);
598
+ assert.equal(first.status, 0);
599
+ fs.writeFileSync(path.join(dir, "知识库", "schema.md"), "# Custom Schema\n", "utf8");
600
+ const second = runCommand(["knowledge", "init", "--target", dir, "--yes"]);
601
+ const check = runCommand(["knowledge", "check", "--target", dir, "--json"]);
602
+ const report = JSON.parse(check.stdout);
603
+ const noisyFiles = listFiles(dir).filter((file) => file.includes(".starwork-new"));
604
+
605
+ assert.equal(second.status, 0);
606
+ assert.deepEqual(noisyFiles, []);
607
+ assert.equal(fs.readFileSync(path.join(dir, "知识库", "schema.md"), "utf8"), "# Custom Schema\n");
608
+ assert.equal(report.ok, true);
609
+ assert.equal(report.skills.project_skill_installed, true);
610
+ });
611
+
612
+ test("knowledge status reports facts only when the capability is not enabled", () => {
613
+ const dir = tempDir();
614
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
615
+
616
+ const status = runCommand(["knowledge", "status", "--target", dir, "--json"]);
617
+ const report = JSON.parse(status.stdout);
618
+ const check = runCommand(["knowledge", "check", "--target", dir]);
619
+ const doctor = runDoctor(["--target", dir, "--json"]);
620
+
621
+ assert.equal(status.status, 0);
622
+ assert.equal(report.enabled, false);
623
+ assert.equal(report.exists, false);
624
+ assert.equal(report.skills.project_skill_installed, false);
625
+ assert.equal(Object.hasOwn(report, "next_steps"), false);
626
+ assert.equal(check.status, 0);
627
+ assert.match(check.stdout, /还没有开启知识库/);
628
+ assert.equal(doctor.status, 0);
629
+ });
630
+
631
+ test("init --knowledge creates the English knowledge-base structure", () => {
632
+ const dir = tempDir();
633
+ runInit(["--type", "project", "--pack", "general", "--language", "en", "--target", dir, "--knowledge", "--yes"]);
634
+
635
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
636
+ const status = runCommand(["knowledge", "status", "--target", dir, "--json"]);
637
+ const report = JSON.parse(status.stdout);
638
+
639
+ assert.equal(state.capabilities.knowledge.enabled, true);
640
+ assert.equal(state.capabilities.knowledge.root, "knowledge-base");
641
+ assert.equal(state.capabilities.knowledge.language, "en");
642
+ assert.deepEqual(state.capabilities.knowledge.project_skill_ids, ["starworkKnowledgeProject"]);
643
+ assert.equal(fs.existsSync(path.join(dir, "knowledge-base", "schema.md")), true);
644
+ assert.equal(fs.existsSync(path.join(dir, ".agents", "skills", "starworkKnowledgeProject", "SKILL.md")), true);
645
+ assert.match(fs.readFileSync(path.join(dir, "knowledge-base", "README.md"), "utf8"), /Project Knowledge Base/);
646
+ assert.equal(report.enabled, true);
647
+ assert.equal(report.root, "knowledge-base");
648
+ assert.equal(Object.hasOwn(report, "next_steps"), false);
649
+ });
650
+
651
+ test("knowledge apply creates structure from a blueprint without moving legacy knowledge", () => {
652
+ const dir = tempDir();
653
+ const blueprintDir = tempDir();
654
+ const blueprintPath = path.join(blueprintDir, "knowledge-blueprint.json");
655
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
656
+ fs.mkdirSync(path.join(dir, "知识"), { recursive: true });
657
+ fs.writeFileSync(path.join(dir, "知识", "old.md"), "# old\n", "utf8");
658
+ fs.writeFileSync(blueprintPath, JSON.stringify({
659
+ version: "0.1",
660
+ type: "starwork.knowledge",
661
+ language: "zh",
662
+ root: "知识库",
663
+ actions: [
664
+ { type: "create_knowledge_base", path: "知识库" },
665
+ { type: "append_agents_rule", path: "AGENTS.md", section: "知识库" },
666
+ { type: "install_project_skill" },
667
+ { type: "copy_preserved_file", from: "知识/old.md", to: "知识库/inbox/old.md", confirmed: true },
668
+ { type: "record_workspace_capability" }
669
+ ],
670
+ preserve: ["知识/"]
671
+ }, null, 2), "utf8");
672
+
673
+ const result = runCommand(["knowledge", "apply", "--target", dir, "--blueprint", blueprintPath, "--yes"]);
674
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
675
+ const status = runCommand(["knowledge", "status", "--target", dir, "--json"]);
676
+ const report = JSON.parse(status.stdout);
677
+
678
+ assert.equal(result.status, 0);
679
+ assert.equal(state.capabilities.knowledge.root, "知识库");
680
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "schema.md")), true);
681
+ assert.equal(fs.existsSync(path.join(dir, "知识", "old.md")), true);
682
+ assert.equal(fs.existsSync(path.join(dir, "知识库", "inbox", "old.md")), true);
683
+ assert.equal(fs.existsSync(path.join(dir, ".agents", "skills", "starworkKnowledgeProject", "SKILL.md")), true);
684
+ assert.deepEqual(report.legacy_candidates, ["知识"]);
685
+ });
686
+
687
+ test("knowledge blueprint rejects unsafe actions", () => {
688
+ const dir = tempDir();
689
+ const blueprintDir = tempDir();
690
+ const blueprintPath = path.join(blueprintDir, "knowledge-blueprint.json");
691
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
692
+ fs.writeFileSync(blueprintPath, JSON.stringify({
693
+ version: "0.1",
694
+ type: "starwork.knowledge",
695
+ language: "zh",
696
+ root: "知识库",
697
+ actions: [
698
+ { type: "promote_to_project_center", path: "知识库" }
699
+ ]
700
+ }, null, 2), "utf8");
701
+
702
+ const result = runCommand(["knowledge", "apply", "--target", dir, "--blueprint", blueprintPath, "--yes"]);
703
+ assert.notEqual(result.status, 0);
704
+ assert.match(result.stderr, /不允许 action\.type:promote_to_project_center/);
705
+ });
706
+
707
+ test("init creates a customized workspace from a blueprint", () => {
708
+ const dir = tempDir();
709
+ const blueprintDir = tempDir();
710
+ fs.mkdirSync(path.join(blueprintDir, "rules"), { recursive: true });
711
+ fs.writeFileSync(path.join(blueprintDir, "rules", "file-boundaries.md"), "代码放在 {{paths.drafts}},产品文档放在 {{paths.final}}。\n", "utf8");
712
+ fs.writeFileSync(path.join(blueprintDir, "rules", "workflow.md"), "推进时先读 docs/,再改 src/。\n", "utf8");
713
+ fs.writeFileSync(path.join(blueprintDir, "init-blueprint.json"), `${JSON.stringify({
714
+ schema: "starwork.init_blueprint.v0.1",
715
+ name: "AI Discussion",
716
+ workspace_type: "project",
717
+ kit: "project",
718
+ language: "en",
719
+ pack: "general",
720
+ paths: {
721
+ formal_source: "docs/",
722
+ business_work_area: "src/"
723
+ },
724
+ directories: [
725
+ {
726
+ path: "src/",
727
+ purpose: "存放代码和 AI 工作稿",
728
+ write_policy: "writable"
729
+ },
730
+ {
731
+ path: "docs/",
732
+ purpose: "存放用户确认后的产品文档",
733
+ write_policy: "confirm_before_write"
734
+ }
735
+ ],
736
+ folders: ["src/", "docs/"],
737
+ removals: ["references/", "outputs/", "参考资料/", "输出/"],
738
+ agent_rules: [
739
+ {
740
+ slot: "workspace.file_boundaries",
741
+ from: "rules/file-boundaries.md"
742
+ },
743
+ {
744
+ slot: "workspace.workflow",
745
+ from: "rules/workflow.md"
746
+ }
747
+ ]
748
+ }, null, 2)}\n`, "utf8");
749
+
750
+ const preview = runInit(["--target", dir, "--blueprint", path.join(blueprintDir, "init-blueprint.json"), "--dry-run"]);
751
+ assert.match(preview, /初始化定制单/);
752
+ assert.match(preview, /日常工作会放在:src\//);
753
+ assert.equal(fs.existsSync(path.join(dir, "src")), false);
754
+
755
+ runInit(["--target", dir, "--blueprint", path.join(blueprintDir, "init-blueprint.json"), "--yes"]);
756
+
757
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
758
+ assert.equal(state.created_by, "starwork init --blueprint");
759
+ assert.equal(state.language, "en");
760
+ assert.equal(state.paths.formal_source, "docs/");
761
+ assert.equal(state.paths.business_work_area, "src/");
762
+ assert.equal(state.customization.type, "init_blueprint");
763
+ assert.deepEqual(state.packs[0].paths, {
764
+ references: "src/",
765
+ drafts: "src/",
766
+ final: "docs/"
767
+ });
768
+ assert.equal(fs.existsSync(path.join(dir, "src")), true);
769
+ assert.equal(fs.existsSync(path.join(dir, "docs")), true);
770
+ assert.equal(fs.existsSync(path.join(dir, "references")), false);
771
+ assert.equal(fs.existsSync(path.join(dir, "outputs")), false);
772
+ assert.equal(fs.existsSync(path.join(dir, "参考资料")), false);
773
+ assert.equal(fs.existsSync(path.join(dir, "输出")), false);
774
+ assert.match(fs.readFileSync(path.join(dir, ".starwork", "rules", "workspace.file_boundaries.md"), "utf8"), /代码放在 src\//);
68
775
  const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
69
- assert.equal(state.workspace_type, "single-matter");
776
+ assert.match(agents, /Workspace Directories/);
777
+ assert.match(agents, /`src\/` \| 存放代码和 AI 工作稿/);
778
+ assert.match(agents, /`docs\/` \| 存放用户确认后的产品文档/);
779
+ assert.doesNotMatch(agents, /references\/|outputs\/|参考资料|输出\/草稿|Folders Not Used|Initialized as|blueprint|dry-run/);
780
+ assert.doesNotMatch(fs.readFileSync(path.join(dir, "_system", "context", "current-project.md"), "utf8"), /Initialized as|StarWork project workspace|blueprint|Folders Not Used|doctor/);
781
+
782
+ const report = runDoctor(["--target", dir, "--json"]);
783
+ assert.equal(report.status, 0);
784
+ const parsed = JSON.parse(report.stdout);
785
+ assert.equal(parsed.ok, true);
786
+ assert(parsed.checks.some((check) => check.id === "blueprint.schema" && check.level === "pass"));
787
+ });
788
+
789
+ test("init blueprint cannot remove StarWork mechanism files", () => {
790
+ const dir = tempDir();
791
+ const blueprintDir = tempDir();
792
+ fs.writeFileSync(path.join(blueprintDir, "init-blueprint.json"), `${JSON.stringify({
793
+ schema: "starwork.init_blueprint.v0.1",
794
+ name: "Unsafe Init",
795
+ workspace_type: "project",
796
+ kit: "project",
797
+ language: "zh",
798
+ pack: "general",
799
+ removals: [".starwork/"]
800
+ }, null, 2)}\n`, "utf8");
801
+
802
+ const result = runCommand(["init", "--target", dir, "--blueprint", path.join(blueprintDir, "init-blueprint.json"), "--dry-run"]);
803
+
804
+ assert.equal(result.status, 1);
805
+ assert.match(result.stderr, /不能跳过 StarWork 机制文件/);
806
+ });
807
+
808
+ test("creates a project workspace with content creator pack", () => {
809
+ const dir = tempDir();
810
+ runInit(["--type", "project", "--pack", "content-creator", "--target", dir, "--yes"]);
811
+
812
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
813
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
814
+ const packRule = fs.readFileSync(path.join(dir, ".starwork", "rules", "pack.content-creator.overview.md"), "utf8");
815
+ assert.equal(state.workspace_type, "project");
70
816
  assert.equal(state.packs[0].id, "content-creator");
71
817
  assert.equal(state.paths.formal_source, "发布记录/");
72
- assert.match(agents, /自媒体内容生产流/);
73
- assert.equal(fs.existsSync(path.join(dir, "事项", "注册表.md")), true);
818
+ assert.match(agents, /\.starwork\/rules\/index\.md/);
819
+ assert.doesNotMatch(agents, /StarWork Rule Slot:/);
820
+ assert.match(packRule, /自媒体内容创作场景/);
821
+ assert.equal(fs.existsSync(path.join(dir, "事项", "注册表.md")), false);
74
822
  assert.equal(fs.existsSync(path.join(dir, "发布记录", "README.md")), true);
75
823
  assert.equal(fs.existsSync(path.join(dir, ".starwork", "packs", "content-creator", "templates", "content-brief.md")), true);
76
824
  });
77
825
 
826
+ test("init rejects removed matter workspace type", () => {
827
+ const dir = tempDir();
828
+ const result = runCommand(["init", "--type", "single-matter", "--target", dir, "--yes"]);
829
+
830
+ assert.equal(result.status, 1);
831
+ assert.match(result.stderr, /不支持的工作区类型:single-matter/);
832
+ });
833
+
78
834
  test("creates a hub workspace with hub management pack", () => {
79
835
  const dir = tempDir();
80
- runInit(["--type", "hub", "--target", dir, "--yes"]);
836
+ const output = runInit(["--type", "hub", "--target", dir, "--yes"]);
81
837
 
82
838
  const state = readJson(path.join(dir, ".starwork", "workspace.json"));
839
+ const skills = readJson(path.join(dir, ".starwork", "skills.json"));
840
+ assert.match(output, /需要创建项目时,先用 starworkSpawn 设计,或直接运行 starwork spawn/);
841
+ assert.match(output, /运行 starwork audit 巡检项目中心里的项目登记/);
83
842
  assert.equal(state.workspace_type, "hub");
84
843
  assert.equal(state.kit, "hub");
85
844
  assert.equal(state.packs[0].id, "hub-management");
845
+ assert.equal(skills.skills[0].id, "starworkSpawn");
846
+ assert.equal(skills.skills[0].source.kind, "kit");
847
+ assert.equal(fs.existsSync(path.join(dir, "技能", "starworkSpawn", "SKILL.md")), true);
848
+ assert.equal(fs.existsSync(path.join(dir, "技能", "registry.json")), true);
86
849
  assert.equal(fs.existsSync(path.join(dir, "项目", "registry.json")), true);
87
850
  assert.equal(fs.existsSync(path.join(dir, "知识", "README.md")), true);
851
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "handoff", "state.json")), true);
852
+ assert.equal(fs.existsSync(path.join(dir, "_系统")), false);
853
+ assert.equal(fs.existsSync(path.join(dir, "projects")), false);
854
+ assert.equal(fs.existsSync(path.join(dir, "skills")), false);
88
855
  assert.equal(fs.existsSync(path.join(dir, ".incoming", "README.md")), true);
89
856
  });
90
857
 
858
+ test("doctor reports hub required kit skills and passes strict when complete", () => {
859
+ const dir = tempDir();
860
+ runInit(["--type", "hub", "--target", dir, "--yes"]);
861
+
862
+ const doctor = runDoctor(["--target", dir, "--strict", "--json"]);
863
+ const report = JSON.parse(doctor.stdout);
864
+ const required = report.skills.required || [];
865
+
866
+ assert.equal(doctor.status, 0);
867
+ assert.equal(report.strict_ok, true);
868
+ assert.deepEqual(required.map((skill) => skill.id).sort(), ["starworkAudit", "starworkSpawn"]);
869
+ for (const skill of required) {
870
+ assert.equal(skill.required_by, "kit:hub");
871
+ assert.equal(skill.status, "ok");
872
+ assert.equal(skill.source.status, "ok");
873
+ assert.equal(skill.manifest.status, "ok");
874
+ assert(skill.mounts.some((mount) => mount.agent === "codex" && mount.status === "ok"));
875
+ assert(skill.mounts.some((mount) => mount.agent === "claude" && mount.status === "ok"));
876
+ assert.equal(skill.frontmatter.status, "ok");
877
+ }
878
+ });
879
+
880
+ test("doctor warns about missing hub required kit skills and fails strict", () => {
881
+ const dir = tempDir();
882
+ runInit(["--type", "hub", "--target", dir, "--yes"]);
883
+ fs.writeFileSync(path.join(dir, ".starwork", "skills.json"), `${JSON.stringify({
884
+ schema: "starwork.project_skills.v0.1",
885
+ skills: []
886
+ }, null, 2)}\n`, "utf8");
887
+ for (const skillId of ["starworkSpawn", "starworkAudit"]) {
888
+ fs.rmSync(path.join(dir, "技能", skillId), { recursive: true, force: true });
889
+ fs.rmSync(path.join(dir, ".agents", "skills", skillId), { recursive: true, force: true });
890
+ fs.rmSync(path.join(dir, ".claude", "skills", skillId), { recursive: true, force: true });
891
+ }
892
+
893
+ const doctor = runDoctor(["--target", dir, "--json"]);
894
+ const report = JSON.parse(doctor.stdout);
895
+ const spawn = report.skills.required.find((skill) => skill.id === "starworkSpawn");
896
+ const text = runDoctor(["--target", dir]);
897
+ const strict = runDoctor(["--target", dir, "--strict", "--json"]);
898
+ const strictReport = JSON.parse(strict.stdout);
899
+
900
+ assert.equal(doctor.status, 0);
901
+ assert.equal(report.ok, true);
902
+ assert.equal(spawn.required_by, "kit:hub");
903
+ assert.notEqual(spawn.status, "ok");
904
+ assert.equal(spawn.source.path, "技能/starworkSpawn");
905
+ assert.equal(spawn.source.status, "missing");
906
+ assert.equal(spawn.manifest.status, "missing");
907
+ assert(spawn.mounts.some((mount) => mount.path === ".agents/skills/starworkSpawn" && mount.status === "missing"));
908
+ assert(spawn.mounts.some((mount) => mount.path === ".claude/skills/starworkSpawn" && mount.status === "missing"));
909
+ assert.match(spawn.repair_hint, /Hub Kit 自带 Skill/);
910
+ assert.doesNotMatch(spawn.repair_hint, /全局安装/);
911
+ assert.equal(text.status, 0);
912
+ assert.match(text.stdout, /缺少 Hub 自带 Skill:starworkSpawn/);
913
+ assert.match(text.stdout, /不要把它安装成全局系统 Skill/);
914
+ assert.equal(strict.status, 1);
915
+ assert.equal(strictReport.ok, true);
916
+ assert.equal(strictReport.strict_ok, false);
917
+ });
918
+
91
919
  test("does not overwrite existing user files", () => {
92
920
  const dir = tempDir();
93
921
  fs.writeFileSync(path.join(dir, "README.md"), "# Existing\n", "utf8");
922
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent Rules\n", "utf8");
94
923
 
95
- runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
924
+ const output = runInit(["--type", "project", "--pack", "general", "--adapter", "codex", "--target", dir, "--yes"]);
925
+ const plan = readJson(path.join(dir, ".starwork", "drafts", "agent-docs-plan.json"));
926
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
927
+ const doctor = runDoctor(["--target", dir, "--host", "codex", "--json"]);
928
+ const report = JSON.parse(doctor.stdout);
96
929
 
97
930
  assert.equal(fs.readFileSync(path.join(dir, "README.md"), "utf8"), "# Existing\n");
98
- assert.equal(fs.existsSync(path.join(dir, "README.starwork-new.md")), true);
931
+ assert.equal(fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8"), "# Existing Agent Rules\n");
932
+ assert.equal(fs.existsSync(path.join(dir, "README.starwork-new.md")), false);
933
+ assert.equal(fs.existsSync(path.join(dir, "AGENTS.starwork.md")), false);
934
+ assert.equal(fs.existsSync(path.join(dir, "AGENTS.starwork-new.md")), false);
935
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "drafts", "README.proposed.md")), true);
936
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "drafts", "AGENTS.proposed.md")), true);
937
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "drafts", "adapter.codex.proposed.md")), true);
938
+ assert.match(output, /AI 入口文档需要 Skill 整合后再生效/);
939
+ assert.equal(plan.status, "draft_required");
940
+ assert.ok(plan.entries.some((entry) => entry.target_path === "README.md" && entry.draft_path === ".starwork/drafts/README.proposed.md"));
941
+ assert.ok(plan.entries.some((entry) => entry.host === "codex" && entry.draft_path === ".starwork/drafts/adapter.codex.proposed.md"));
942
+ assert.equal(adaptersState.adapters.codex.enabled, false);
943
+ assert.equal(adaptersState.adapters.codex.rules_entry, "AGENTS.md");
944
+ assert.equal(adaptersState.adapters.codex.rules_entry_status, "pending_merge");
945
+ assert.equal(adaptersState.adapters.codex.draft_entry, ".starwork/drafts/adapter.codex.proposed.md");
946
+ assert.ok(report.checks.some((check) => check.id === "agent_docs.plan.pending" && check.level === "warn"));
947
+ assert.ok(report.checks.some((check) => check.id === "adapter.codex.rules.pending_merge" && check.level === "warn"));
948
+ });
949
+
950
+ test("init dry-run with adapter previews agent docs drafts and pending merge plan", () => {
951
+ const dir = tempDir();
952
+ fs.writeFileSync(path.join(dir, "README.md"), "# Existing\n", "utf8");
953
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent Rules\n", "utf8");
954
+ fs.writeFileSync(path.join(dir, "package.json"), "{\"name\":\"plain\"}\n", "utf8");
955
+
956
+ const output = runInit(["--type", "project", "--pack", "general", "--language", "zh", "--adapter", "codex", "--target", dir, "--dry-run"]);
957
+
958
+ assert.match(output, /\.starwork\/drafts\/README\.proposed\.md/);
959
+ assert.match(output, /\.starwork\/drafts\/AGENTS\.proposed\.md/);
960
+ assert.match(output, /\.starwork\/drafts\/adapter\.codex\.proposed\.md/);
961
+ assert.match(output, /\.starwork\/drafts\/agent-docs-plan\.json/);
962
+ assert.match(output, /初始化后的 AI 工具适配预览/);
963
+ assert.match(output, /pending_merge/);
964
+ assert.equal(fs.existsSync(path.join(dir, ".starwork")), false);
965
+ assert.equal(fs.existsSync(path.join(dir, "AGENTS.starwork.md")), false);
966
+ assert.equal(fs.existsSync(path.join(dir, "README.starwork-new.md")), false);
99
967
  });
100
968
 
101
969
  test("doctor passes on a single-light workspace with general pack", () => {
@@ -105,19 +973,22 @@ test("doctor passes on a single-light workspace with general pack", () => {
105
973
  const result = runDoctor(["--target", dir]);
106
974
 
107
975
  assert.equal(result.status, 0);
108
- assert.match(result.stdout, /Workspace is healthy/);
976
+ assert.match(result.stdout, /这个工作台结构完整,可以继续使用/);
977
+ assert.doesNotMatch(result.stdout, /^Kit:/m);
978
+ assert.doesNotMatch(result.stdout, /^Packs:/m);
979
+ assert.doesNotMatch(result.stdout, /Workspace is healthy/);
109
980
  });
110
981
 
111
- test("doctor passes on a single-matter workspace with content creator pack", () => {
982
+ test("doctor passes on a project workspace with content creator pack", () => {
112
983
  const dir = tempDir();
113
- runInit(["--type", "single-matter", "--pack", "content-creator", "--target", dir, "--yes"]);
984
+ runInit(["--type", "project", "--pack", "content-creator", "--target", dir, "--yes"]);
114
985
 
115
986
  const result = runDoctor(["--target", dir, "--json"]);
116
987
  const report = JSON.parse(result.stdout);
117
988
 
118
989
  assert.equal(result.status, 0);
119
990
  assert.equal(report.ok, true);
120
- assert.equal(report.workspace.workspace_type, "single-matter");
991
+ assert.equal(report.workspace.workspace_type, "project");
121
992
  assert.deepEqual(report.workspace.packs, ["content-creator"]);
122
993
  });
123
994
 
@@ -125,51 +996,1004 @@ test("doctor passes on a hub workspace", () => {
125
996
  const dir = tempDir();
126
997
  runInit(["--type", "hub", "--target", dir, "--yes"]);
127
998
 
128
- const result = runDoctor(["--target", dir]);
999
+ const result = runDoctor(["--target", dir, "--json"]);
1000
+ const report = JSON.parse(result.stdout);
1001
+ const text = runDoctor(["--target", dir]);
1002
+
1003
+ assert.equal(result.status, 0);
1004
+ assert.equal(report.ok, true);
1005
+ assert.equal(report.skills.registry.path, "技能/registry.json");
1006
+ assert.equal(report.skills.registry.path_source, "default");
1007
+ assert.equal(text.status, 0);
1008
+ assert.match(text.stdout, /这个工作台结构完整,可以继续使用/);
1009
+ });
1010
+
1011
+ test("doctor warns when hub rules mention old hub paths", () => {
1012
+ const dir = tempDir();
1013
+ runInit(["--type", "hub", "--target", dir, "--yes"]);
1014
+ fs.appendFileSync(path.join(dir, "AGENTS.md"), "\n旧路径:.starwork/projects/registry.json .starwork/coordination/ .starwork/incoming/\n", "utf8");
1015
+
1016
+ const result = runDoctor(["--target", dir, "--json"]);
1017
+ const report = JSON.parse(result.stdout);
129
1018
 
130
1019
  assert.equal(result.status, 0);
131
- assert.match(result.stdout, /Workspace is healthy/);
1020
+ assert(report.checks.some((check) => check.id === "hub.rules.agents_md.paths" && check.level === "warn"));
1021
+ });
1022
+
1023
+ test("doctor warns when a project center has duplicate semantic directories", () => {
1024
+ const dir = tempDir();
1025
+ runInit(["--type", "hub", "--target", dir, "--yes"]);
1026
+ fs.mkdirSync(path.join(dir, "knowledge"), { recursive: true });
1027
+
1028
+ const result = runDoctor(["--target", dir, "--json"]);
1029
+ const report = JSON.parse(result.stdout);
1030
+
1031
+ assert.equal(result.status, 0);
1032
+ assert(report.checks.some((check) => check.id === "hub.semantic_duplicate_dirs" && check.level === "warn"));
1033
+ });
1034
+
1035
+ test("multiagent init creates custom agent lanes without built-in defaults", () => {
1036
+ const dir = tempDir();
1037
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1038
+
1039
+ const lanes = runCommand(["multiagent", "init", "--target", dir, "--lanes", "research,writing", "--yes"]);
1040
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1041
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1042
+
1043
+ assert.equal(lanes.status, 0);
1044
+ assert.match(registry, /\| lane \| purpose \| current_session \| write_scope \| worklog \| workspace \|/);
1045
+ assert.match(registry, /\| research \| 待补充 \| unbound \| 待补充 \| lanes\/research\/worklog\.md \| lanes\/research\/workspace \|/);
1046
+ assert.match(registry, /\| writing \| 待补充 \| unbound \| 待补充 \| lanes\/writing\/worklog\.md \| lanes\/writing\/workspace \|/);
1047
+ assert.doesNotMatch(registry, /backend|frontend|test/);
1048
+ assert.match(shared, /# Shared Agent Context/);
1049
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "协作", "lanes", "research", "worklog.md")), true);
1050
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "协作", "lanes", "research", "workspace", "README.md")), true);
1051
+ });
1052
+
1053
+ test("multiagent init uses English collaboration paths for English workspaces", () => {
1054
+ const dir = tempDir();
1055
+ runInit(["--type", "project", "--pack", "general", "--language", "en", "--target", dir, "--yes"]);
1056
+
1057
+ const lanes = runCommand(["multiagent", "init", "--target", dir, "--lanes", "research", "--yes"]);
1058
+ const registry = fs.readFileSync(path.join(dir, "_system", "collaboration", "agent-lanes.md"), "utf8");
1059
+ const workspaceReadme = fs.readFileSync(path.join(dir, "_system", "collaboration", "lanes", "research", "workspace", "README.md"), "utf8");
1060
+
1061
+ assert.equal(lanes.status, 0);
1062
+ assert.match(registry, /\| research \| 待补充 \| unbound/);
1063
+ assert.match(workspaceReadme, /_system\/collaboration\/shared\.md/);
1064
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "协作")), false);
1065
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "agent-lanes", "state.json")), true);
1066
+ });
1067
+
1068
+ test("multiagent add bind share and status update markdown state", () => {
1069
+ const dir = tempDir();
1070
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1071
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1072
+
1073
+ const add = runCommand([
1074
+ "multiagent", "add", "review",
1075
+ "--purpose", "审校和风险检查",
1076
+ "--write", "reviews/**,product/docs/**",
1077
+ "--target", dir,
1078
+ "--yes"
1079
+ ]);
1080
+ const bind = runCommand([
1081
+ "multiagent", "bind", "review",
1082
+ "--session", "codex:manual-review-1",
1083
+ "--target", dir,
1084
+ "--yes"
1085
+ ]);
1086
+ const share = runCommand([
1087
+ "multiagent", "share", "review",
1088
+ "--title", "Review checklist",
1089
+ "--path", "_系统/协作/lanes/review/workspace/review-checklist.md",
1090
+ "--audience", "writing",
1091
+ "--status", "draft",
1092
+ "--target", dir,
1093
+ "--yes"
1094
+ ]);
1095
+ const status = runCommand(["multiagent", "status", "--target", dir, "--json"]);
1096
+ const report = JSON.parse(status.stdout);
1097
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1098
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1099
+
1100
+ assert.equal(add.status, 0);
1101
+ assert.equal(bind.status, 0);
1102
+ assert.equal(share.status, 0);
1103
+ assert.match(share.stdout, /其他职责位可以查看:_系统\/协作\/shared\.md/);
1104
+ assert.equal(status.status, 0);
1105
+ assert.match(registry, /\| review \| 审校和风险检查 \| codex:manual-review-1 \| reviews\/\*\*,product\/docs\/\*\* \| lanes\/review\/worklog\.md \| lanes\/review\/workspace \|/);
1106
+ assert.match(shared, /\| review \| Review checklist \| _系统\/协作\/lanes\/review\/workspace\/review-checklist\.md \| writing \| draft \|/);
1107
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "协作", "lanes", "review", "workspace", "README.md")), true);
1108
+ assert.equal(report.schema, "starwork.agent_lanes.status.v0.1");
1109
+ assert.equal(report.lanes[0].lane, "review");
1110
+ assert.equal(report.lanes[0].current_session, "codex:manual-review-1");
1111
+ assert.equal(report.lanes[0].workspace, "lanes/review/workspace");
1112
+ assert.equal(report.shared_outputs[0].title, "Review checklist");
1113
+
1114
+ const humanStatus = runCommand(["multiagent", "status", "--target", dir]);
1115
+ assert.match(humanStatus.stdout, /StarWork 多 AI 协作状态/);
1116
+ assert.match(humanStatus.stdout, /职责位:1 个;已绑定会话:1 个;共享输出:1 项/);
1117
+ });
1118
+
1119
+ test("multiagent bind records session name request without calling Codex app-server", () => {
1120
+ const dir = tempDir();
1121
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1122
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1123
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1124
+ runCommand([
1125
+ "multiagent", "add", "research",
1126
+ "--purpose", "新功能预研",
1127
+ "--write", "_系统/协作/lanes/research/**",
1128
+ "--target", dir,
1129
+ "--yes"
1130
+ ]);
1131
+
1132
+ const fakeCodex = fakeCodexBin({ inputPath });
1133
+ const bind = runCommand([
1134
+ "multiagent", "bind", "research",
1135
+ "--session", "codex:test-thread-1",
1136
+ "--session-name", "StarWork 新功能预研 Agent",
1137
+ "--target", dir,
1138
+ "--json",
1139
+ "--yes"
1140
+ ], { env: fakeCodex.env });
1141
+ const result = JSON.parse(bind.stdout);
1142
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1143
+
1144
+ assert.equal(bind.status, 0);
1145
+ assert.equal(result.session_name_sync.status, "requires_starworkMultiagent_tool");
1146
+ assert.equal(result.session_name_sync.name, "StarWork 新功能预研 Agent");
1147
+ assert.match(result.session_name_sync.warning, /set_thread_title/);
1148
+ assert.match(registry, /codex:test-thread-1/);
1149
+ assert.equal(fs.existsSync(inputPath), false);
1150
+ });
1151
+
1152
+ test("multiagent bind pure record mode does not call fake codex app-server", () => {
1153
+ const dir = tempDir();
1154
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1155
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1156
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1157
+ runCommand([
1158
+ "multiagent", "add", "maintenance",
1159
+ "--purpose", "CLI 维护",
1160
+ "--write", "product/cli/**",
1161
+ "--target", dir,
1162
+ "--yes"
1163
+ ]);
1164
+
1165
+ const fakeCodex = fakeCodexBin({ inputPath, exitCode: 1, stderr: "app-server unavailable" });
1166
+ const bind = runCommand([
1167
+ "multiagent", "bind", "maintenance",
1168
+ "--session", "codex:test-thread-2",
1169
+ "--target", dir,
1170
+ "--json",
1171
+ "--yes"
1172
+ ], { env: fakeCodex.env });
1173
+ const result = JSON.parse(bind.stdout);
1174
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1175
+
1176
+ assert.equal(bind.status, 0);
1177
+ assert.equal(result.session_name_sync.status, "not_requested");
1178
+ assert.match(registry, /codex:test-thread-2/);
1179
+ assert.equal(fs.existsSync(inputPath), false);
1180
+ });
1181
+
1182
+ test("multiagent bind --pin records host metadata without rollback when pin is unsupported", () => {
1183
+ const dir = tempDir();
1184
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1185
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1186
+ runCommand([
1187
+ "multiagent", "add", "development",
1188
+ "--purpose", "功能开发",
1189
+ "--write", "product/cli/**",
1190
+ "--target", dir,
1191
+ "--yes"
1192
+ ]);
1193
+
1194
+ const fakeCodex = fakeCodexBin();
1195
+ const bind = runCommand([
1196
+ "multiagent", "bind", "development",
1197
+ "--session", "codex:dev-thread-1",
1198
+ "--session-name", "StarWork 开发 Agent",
1199
+ "--pin",
1200
+ "--target", dir,
1201
+ "--json",
1202
+ "--yes"
1203
+ ], { env: fakeCodex.env });
1204
+ const result = JSON.parse(bind.stdout);
1205
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1206
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1207
+
1208
+ assert.equal(bind.status, 0);
1209
+ assert.equal(result.pin_sync.status, "requires_starworkMultiagent_tool");
1210
+ assert.match(result.pin_sync.warning, /set_thread_pinned/);
1211
+ assert.match(registry, /codex:dev-thread-1/);
1212
+ assert.equal(state.lanes.development.thread_id, "dev-thread-1");
1213
+ assert.equal(state.lanes.development.current_session, "codex:dev-thread-1");
1214
+ });
1215
+
1216
+ test("multiagent status --host and read route Codex observation to starworkMultiagent tools", () => {
1217
+ const dir = tempDir();
1218
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1219
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1220
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1221
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1222
+ runCommand(["multiagent", "bind", "development", "--session", "codex:dev-thread-2", "--target", dir, "--yes"], { env: fakeCodexBin().env });
1223
+
1224
+ const fakeCodex = fakeCodexBin({ inputPath });
1225
+ const status = runCommand(["multiagent", "status", "--host", "--target", dir, "--json"], { env: fakeCodex.env });
1226
+ const statusLoad = runCommand(["multiagent", "status", "--host", "--load", "--target", dir, "--json"], { env: fakeCodex.env });
1227
+ const read = runCommand(["multiagent", "read", "development", "--turns", "1", "--target", dir, "--json"], { env: fakeCodex.env });
1228
+ const report = JSON.parse(status.stdout);
1229
+ const loadReport = JSON.parse(statusLoad.stdout);
1230
+ const readReport = JSON.parse(read.stdout);
1231
+
1232
+ assert.equal(status.status, 0);
1233
+ assert.equal(statusLoad.status, 0);
1234
+ assert.equal(report.schema, "starwork.agent_lanes.host_status.v0.2");
1235
+ assert.equal(loadReport.schema, "starwork.agent_lanes.host_status.v0.2");
1236
+ assert.equal(report.lanes[0].starwork.session, "codex:dev-thread-2");
1237
+ assert.equal(report.lanes[0].host.status, "use_starworkMultiagent_tool");
1238
+ assert.match(report.lanes[0].host.warning, /read_thread/);
1239
+ assert.equal(read.status, 0);
1240
+ assert.equal(readReport.host.status, "use_starworkMultiagent_tool");
1241
+ assert.equal(fs.existsSync(inputPath), false);
1242
+ });
1243
+
1244
+ test("multiagent instruct returns manual handoff for Codex when standard send is unavailable", () => {
1245
+ const dir = tempDir();
1246
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1247
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1248
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1249
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1250
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1251
+ runCommand(["multiagent", "bind", "development", "--session", "codex:dev-thread-3", "--target", dir, "--yes"], { env: fakeCodexBin().env });
1252
+
1253
+ const fakeCodex = fakeCodexBin({ inputPath });
1254
+ const instruct = runCommand([
1255
+ "multiagent", "instruct", "development",
1256
+ "--from", "product-planning",
1257
+ "--message", "请开始实现 v0.2。",
1258
+ "--target", dir,
1259
+ "--json",
1260
+ "--yes"
1261
+ ], { env: fakeCodex.env });
1262
+ const result = JSON.parse(instruct.stdout);
1263
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1264
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1265
+
1266
+ assert.equal(instruct.status, 0);
1267
+ assert.equal(result.schema, "starwork.agent_lanes.instruct.v0.4");
1268
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
1269
+ assert.equal(result.host_delivery.mode, "manual_handoff");
1270
+ assert.match(result.host_delivery.warning, /send_message_to_thread/);
1271
+ assert.match(shared, /Cross-Lane Requests/);
1272
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.2。 \| manual_handoff_required \| manual_handoff_required/);
1273
+ assert.equal(state.requests[0].host_delivery.status, "manual_handoff_required");
1274
+ assert.equal(fs.existsSync(inputPath), false);
1275
+ });
1276
+
1277
+ test("multiagent instruct does not use low-level Codex turn APIs even with wait requested", () => {
1278
+ const dir = tempDir();
1279
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1280
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1281
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1282
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1283
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1284
+ runCommand(["multiagent", "bind", "development", "--session", "codex:dev-thread-4", "--target", dir, "--yes"], { env: fakeCodexBin().env });
1285
+
1286
+ const fakeCodex = fakeCodexBin({ inputPath, omitTurnCompleted: true });
1287
+ const instruct = runCommand([
1288
+ "multiagent", "instruct", "development",
1289
+ "--from", "product-planning",
1290
+ "--message", "请开始实现 v0.3。",
1291
+ "--target", dir,
1292
+ "--json",
1293
+ "--yes",
1294
+ "--wait-completion",
1295
+ "--timeout", "1000"
1296
+ ], { env: fakeCodex.env });
1297
+ const result = JSON.parse(instruct.stdout);
1298
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1299
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1300
+
1301
+ assert.equal(instruct.status, 0);
1302
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
1303
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.3。 \| manual_handoff_required \| manual_handoff_required/);
1304
+ assert.equal(state.requests[0].host_delivery.status, "manual_handoff_required");
1305
+ assert.equal(fs.existsSync(inputPath), false);
1306
+ });
1307
+
1308
+ test("multiagent instruct returns unbound when target lane has no session", () => {
1309
+ const dir = tempDir();
1310
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1311
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1312
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1313
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1314
+
1315
+ const instruct = runCommand([
1316
+ "multiagent", "instruct", "development",
1317
+ "--from", "product-planning",
1318
+ "--message", "请开始实现 v0.4。",
1319
+ "--target", dir,
1320
+ "--json",
1321
+ "--yes"
1322
+ ]);
1323
+ const result = JSON.parse(instruct.stdout);
1324
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1325
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1326
+
1327
+ assert.equal(instruct.status, 0);
1328
+ assert.equal(result.host_delivery.status, "unbound");
1329
+ assert.match(result.host_delivery.warning, /Target lane is not bound/);
1330
+ assert.match(shared, /product-planning \| development \| 请开始实现 v0\.4。 \| unbound \| unbound/);
1331
+ assert.equal(state.requests[0].host_delivery.status, "unbound");
1332
+ });
1333
+
1334
+ test("multiagent instruct returns needs_adapt when a non-Codex host is not adapted", () => {
1335
+ const dir = tempDir();
1336
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1337
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1338
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1339
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1340
+ runCommand(["multiagent", "bind", "development", "--session", "cursor:cursor-thread-1", "--target", dir, "--yes"]);
1341
+
1342
+ const instruct = runCommand([
1343
+ "multiagent", "instruct", "development",
1344
+ "--from", "product-planning",
1345
+ "--message", "请继续处理运行时路由。",
1346
+ "--target", dir,
1347
+ "--json",
1348
+ "--yes"
1349
+ ]);
1350
+ const result = JSON.parse(instruct.stdout);
1351
+
1352
+ assert.equal(instruct.status, 0);
1353
+ assert.equal(result.host_delivery.status, "needs_adapt");
1354
+ assert.equal(result.host.id, "cursor");
1355
+ assert.match(result.host_delivery.warning, /starwork adapt cursor/);
1356
+ });
1357
+
1358
+ test("multiagent instruct returns manual handoff when adapted host lacks standard send", () => {
1359
+ const dir = tempDir();
1360
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1361
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1362
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1363
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1364
+ runCommand(["multiagent", "bind", "development", "--session", "cursor:cursor-thread-2", "--target", dir, "--yes"]);
1365
+
1366
+ const instruct = runCommand([
1367
+ "multiagent", "instruct", "development",
1368
+ "--from", "product-planning",
1369
+ "--message", "请继续处理运行时路由。",
1370
+ "--target", dir,
1371
+ "--json",
1372
+ "--yes"
1373
+ ]);
1374
+ const result = JSON.parse(instruct.stdout);
1375
+
1376
+ assert.equal(instruct.status, 0);
1377
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
1378
+ assert.equal(result.host_delivery.mode, "manual_handoff");
1379
+ assert.match(result.host_delivery.formatted_message, /STARWORK:MULTIAGENT_MESSAGE v1/);
1380
+ });
1381
+
1382
+ test("multiagent instruct prints copyable handoff message in non-json output", () => {
1383
+ const dir = tempDir();
1384
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1385
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1386
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1387
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1388
+ runCommand(["multiagent", "bind", "development", "--session", "cursor:cursor-thread-3", "--target", dir, "--yes"]);
1389
+
1390
+ const instruct = runCommand([
1391
+ "multiagent", "instruct", "development",
1392
+ "--from", "product-planning",
1393
+ "--message", "请修复 handoff 输出。",
1394
+ "--target", dir,
1395
+ "--yes"
1396
+ ]);
1397
+
1398
+ assert.equal(instruct.status, 0);
1399
+ assert.match(instruct.stdout, /manual_handoff_required/);
1400
+ assert.match(instruct.stdout, /STARWORK:MULTIAGENT_MESSAGE v1/);
1401
+ assert.match(instruct.stdout, /请修复 handoff 输出。/);
1402
+ assert.doesNotMatch(instruct.stdout, /已通知|已发送成功/);
132
1403
  });
133
1404
 
134
- test("spawn creates a matter project from a hub", () => {
1405
+ test("multiagent bind detects Claude Code session from environment and outputs resume command", () => {
1406
+ const dir = tempDir();
1407
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1408
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1409
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "_系统/协作/lanes/research/**", "--target", dir, "--yes"]);
1410
+
1411
+ const bind = runCommand([
1412
+ "multiagent", "bind", "research",
1413
+ "--agent", "claude-code",
1414
+ "--target", dir,
1415
+ "--json",
1416
+ "--yes"
1417
+ ], { env: { CLAUDE_CODE_SESSION_ID: "claude-session-1" } });
1418
+ const result = JSON.parse(bind.stdout);
1419
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1420
+ const continued = runCommand(["multiagent", "continue", "research", "--target", dir, "--json"]);
1421
+ const continueResult = JSON.parse(continued.stdout);
1422
+
1423
+ assert.equal(bind.status, 0);
1424
+ assert.equal(result.session, "claude-code:claude-session-1");
1425
+ assert.equal(state.lanes.research.host, "claude-code");
1426
+ assert.equal(state.lanes.research.thread_id, null);
1427
+ assert.equal(continued.status, 0);
1428
+ assert.equal(continueResult.status, "manual_command");
1429
+ assert.equal(continueResult.command, "claude --resume claude-session-1");
1430
+ });
1431
+
1432
+ test("multiagent read summarizes Claude Code transcript without dumping full transcript", () => {
1433
+ const dir = tempDir();
1434
+ const transcriptDir = tempDir();
1435
+ const transcript = path.join(transcriptDir, "claude-session-2.jsonl");
1436
+ fs.writeFileSync(transcript, [
1437
+ JSON.stringify({ uuid: "u1", message: { role: "user", content: "请分析这个项目的 Host Adapter 需求。" } }),
1438
+ JSON.stringify({ uuid: "a1", message: { role: "assistant", content: [{ type: "text", text: "可以,先从宿主能力表开始,不要写私有 transcript。" }] } })
1439
+ ].join("\n") + "\n", "utf8");
1440
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1441
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1442
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "_系统/协作/lanes/research/**", "--target", dir, "--yes"]);
1443
+ runCommand(["multiagent", "bind", "research", "--session", "claude-code:claude-session-2", "--target", dir, "--yes"]);
1444
+
1445
+ const read = runCommand(["multiagent", "read", "research", "--turns", "1", "--transcript", transcript, "--target", dir, "--json"]);
1446
+ const status = runCommand(["multiagent", "status", "--host", "--transcript", transcriptDir, "--target", dir, "--json"]);
1447
+ const report = JSON.parse(read.stdout);
1448
+ const statusReport = JSON.parse(status.stdout);
1449
+
1450
+ assert.equal(read.status, 0);
1451
+ assert.equal(status.status, 0);
1452
+ assert.equal(report.host.adapter, "claude-code");
1453
+ assert.equal(report.host.readable, true);
1454
+ assert.equal(report.host.turns.length, 1);
1455
+ assert.equal(report.host.turns[0].role, "assistant");
1456
+ assert.match(report.host.turns[0].summary, /不要写私有 transcript/);
1457
+ assert.equal(statusReport.lanes[0].host.readable, true);
1458
+ assert.equal(statusReport.lanes[0].host.turn_count, 2);
1459
+ });
1460
+
1461
+ test("multiagent read summarizes Cursor transcript from agent-transcripts only", () => {
1462
+ const dir = tempDir();
1463
+ const projectsDir = tempDir();
1464
+ const sessionId = "e1717037-1b15-411b-8665-ae922b421f74";
1465
+ writeCursorTranscriptFixture(projectsDir, sessionId, [
1466
+ JSON.stringify({ type: "user", text: "请开始根据 Host Adapter v0.2 工作。" }),
1467
+ JSON.stringify({ type: "tool_call", name: "ReadFile", input: { path: "product/cli/src/cli.js" } }),
1468
+ JSON.stringify({ type: "tool_call", name: "ApplyPatch", input: { path: "product/docs/multiagent/cursor-session-management-research-result.md", patch: "x".repeat(600) } }),
1469
+ JSON.stringify({ type: "assistant", text: "已经完成只读摘要。" }),
1470
+ JSON.stringify({ type: "user", text: "若存在则输出最近用户消息。" })
1471
+ ]);
1472
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1473
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1474
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "product/docs/**", "--target", dir, "--yes"]);
1475
+ runCommand(["multiagent", "bind", "research", "--session", `cursor:${sessionId}`, "--target", dir, "--yes"]);
1476
+
1477
+ const read = runCommand(["multiagent", "read", "research", "--target", dir, "--json"], {
1478
+ env: { STARWORK_CURSOR_PROJECTS_DIR: projectsDir }
1479
+ });
1480
+ const report = JSON.parse(read.stdout);
1481
+
1482
+ assert.equal(read.status, 0);
1483
+ assert.equal(report.host.adapter, "cursor");
1484
+ assert.equal(report.host.status, "transcript_observed");
1485
+ assert.equal(report.host.session_id, sessionId);
1486
+ assert.equal(report.host.line_count, 5);
1487
+ assert.match(report.host.first_user_query, /Host Adapter v0\.2/);
1488
+ assert.equal(report.host.last_user_query, "若存在则输出最近用户消息。");
1489
+ assert.deepEqual(report.host.tool_names.sort(), ["ApplyPatch", "ReadFile"]);
1490
+ assert.ok(report.host.candidate_outputs.includes("product/docs/multiagent/cursor-session-management-research-result.md"));
1491
+ assert.doesNotMatch(JSON.stringify(report), /x{300}/);
1492
+ });
1493
+
1494
+ test("multiagent read reports Cursor missing and malformed transcript states", () => {
1495
+ const dir = tempDir();
1496
+ const projectsDir = tempDir();
1497
+ const sessionId = "bad-cursor-session";
1498
+ writeCursorTranscriptFixture(projectsDir, sessionId, [
1499
+ JSON.stringify({ type: "user", text: "可解析的用户消息" }),
1500
+ "{bad json",
1501
+ JSON.stringify({ type: "tool_call", name: "Shell", input: { command: "npm test" } })
1502
+ ]);
1503
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1504
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1505
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "product/docs/**", "--target", dir, "--yes"]);
1506
+ runCommand(["multiagent", "bind", "research", "--session", `cursor:${sessionId}`, "--target", dir, "--yes"]);
1507
+
1508
+ const malformed = runCommand(["multiagent", "read", "research", "--target", dir, "--json"], {
1509
+ env: { STARWORK_CURSOR_PROJECTS_DIR: projectsDir }
1510
+ });
1511
+ const malformedReport = JSON.parse(malformed.stdout);
1512
+ runCommand(["multiagent", "bind", "research", "--session", "cursor:missing-session", "--target", dir, "--yes"]);
1513
+ const missing = runCommand(["multiagent", "read", "research", "--target", dir, "--json"], {
1514
+ env: { STARWORK_CURSOR_PROJECTS_DIR: projectsDir }
1515
+ });
1516
+ const missingReport = JSON.parse(missing.stdout);
1517
+
1518
+ assert.equal(malformed.status, 0);
1519
+ assert.equal(malformedReport.host.status, "malformed_partial");
1520
+ assert.equal(malformedReport.host.bad_line_count, 1);
1521
+ assert.match(malformedReport.host.warning, /坏行/);
1522
+ assert.equal(missing.status, 0);
1523
+ assert.equal(missingReport.host.status, "not_found");
1524
+ assert.equal(missingReport.host.bound_transcript_exists, false);
1525
+ });
1526
+
1527
+ test("multiagent status --host reports Cursor host facts without leaking API key", () => {
1528
+ const dir = tempDir();
1529
+ const projectsDir = tempDir();
1530
+ const sessionId = "cursor-status-session";
1531
+ writeCursorTranscriptFixture(projectsDir, sessionId, [
1532
+ JSON.stringify({ type: "user", text: "状态观察" })
1533
+ ]);
1534
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1535
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1536
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "product/docs/**", "--target", dir, "--yes"]);
1537
+ runCommand(["multiagent", "bind", "research", "--session", `cursor:${sessionId}`, "--target", dir, "--yes"]);
1538
+
1539
+ const status = runCommand(["multiagent", "status", "--host", "--target", dir, "--json"], {
1540
+ env: {
1541
+ ...fakeCursorBin({ stdout: "Logged in as fake@example.com\n" }).env,
1542
+ STARWORK_CURSOR_PROJECTS_DIR: projectsDir,
1543
+ CURSOR_API_KEY: "cursor-secret-token"
1544
+ }
1545
+ });
1546
+ const report = JSON.parse(status.stdout);
1547
+ const serialized = JSON.stringify(report);
1548
+
1549
+ assert.equal(status.status, 0);
1550
+ assert.equal(report.lanes[0].host.adapter, "cursor");
1551
+ assert.equal(report.lanes[0].host.adapter_enabled, true);
1552
+ assert.equal(report.lanes[0].host.rules_entry_exists, true);
1553
+ assert.equal(report.lanes[0].host.skills_dir_exists, true);
1554
+ assert.equal(report.lanes[0].host.transcript_root_exists, true);
1555
+ assert.equal(report.lanes[0].host.bound_transcript_exists, true);
1556
+ assert.equal(report.lanes[0].host.cursor_api_key_present, true);
1557
+ assert.equal(report.lanes[0].host.cursor_agent_status, "logged_in");
1558
+ assert.doesNotMatch(serialized, /cursor-secret-token/);
1559
+ assert.doesNotMatch(serialized, /fake@example\.com/);
1560
+ });
1561
+
1562
+ test("multiagent status --host reports Cursor agent status failures without secrets", () => {
1563
+ const dir = tempDir();
1564
+ const sessionId = "cursor-status-failure-session";
1565
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1566
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1567
+ runCommand(["multiagent", "add", "research", "--purpose", "预研", "--write", "product/docs/**", "--target", dir, "--yes"]);
1568
+ runCommand(["multiagent", "bind", "research", "--session", `cursor:${sessionId}`, "--target", dir, "--yes"]);
1569
+
1570
+ const notLoggedIn = runCommand(["multiagent", "status", "--host", "--target", dir, "--json"], {
1571
+ env: {
1572
+ ...fakeCursorBin({ stdout: "Not logged in\n" }).env,
1573
+ CURSOR_API_KEY: "cursor-secret-token"
1574
+ }
1575
+ });
1576
+ const failed = runCommand(["multiagent", "status", "--host", "--target", dir, "--json"], {
1577
+ env: {
1578
+ ...fakeCursorBin({ exitCode: 2, stderr: "super-secret-error\n" }).env,
1579
+ CURSOR_API_KEY: "cursor-secret-token"
1580
+ }
1581
+ });
1582
+ const notLoggedInReport = JSON.parse(notLoggedIn.stdout);
1583
+ const failedReport = JSON.parse(failed.stdout);
1584
+ const serialized = `${notLoggedIn.stdout}\n${failed.stdout}`;
1585
+
1586
+ assert.equal(notLoggedIn.status, 0);
1587
+ assert.equal(notLoggedInReport.lanes[0].host.cursor_agent_status, "not_logged_in");
1588
+ assert.equal(failed.status, 0);
1589
+ assert.equal(failedReport.lanes[0].host.cursor_agent_status, "error");
1590
+ assert.doesNotMatch(serialized, /cursor-secret-token|super-secret-error/);
1591
+ });
1592
+
1593
+ test("multiagent instruct returns manual handoff for Trae lane instead of fake delivery", () => {
1594
+ const dir = tempDir();
1595
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "trae", "--yes"]);
1596
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1597
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1598
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1599
+ runCommand(["multiagent", "bind", "development", "--session", "trae:dev-session-1", "--target", dir, "--yes"]);
1600
+
1601
+ const instruct = runCommand([
1602
+ "multiagent", "instruct", "development",
1603
+ "--from", "product-planning",
1604
+ "--message", "请继续处理 Host Adapter。",
1605
+ "--target", dir,
1606
+ "--json",
1607
+ "--yes"
1608
+ ]);
1609
+ const result = JSON.parse(instruct.stdout);
1610
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1611
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1612
+
1613
+ assert.equal(instruct.status, 0);
1614
+ assert.equal(result.host_delivery.adapter, "trae");
1615
+ assert.equal(result.host_delivery.status, "manual_handoff_required");
1616
+ assert.match(result.host_delivery.formatted_message, /STARWORK:MULTIAGENT_MESSAGE v1/);
1617
+ assert.match(shared, /product-planning \| development \| 请继续处理 Host Adapter。 \| manual_handoff_required \| manual_handoff_required/);
1618
+ assert.equal(state.requests[0].host_delivery.status, "manual_handoff_required");
1619
+ });
1620
+
1621
+ test("Trae lane read status continue and launch stay manual without private session reads", () => {
1622
+ const dir = tempDir();
1623
+ const privateDir = path.join(tempDir(), "Trae CN");
1624
+ fs.mkdirSync(path.join(privateDir, "User", "workspaceStorage"), { recursive: true });
1625
+ fs.writeFileSync(path.join(privateDir, "database.db"), "do not read", "utf8");
1626
+ fs.writeFileSync(path.join(privateDir, "User", "workspaceStorage", "state.vscdb"), "do not read", "utf8");
1627
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--adapter", "trae", "--yes"]);
1628
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1629
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1630
+ runCommand(["multiagent", "bind", "development", "--session", "trae:dev-session-2", "--target", dir, "--yes"]);
1631
+
1632
+ const read = runCommand(["multiagent", "read", "development", "--target", dir, "--json"], {
1633
+ env: { STARWORK_TRAE_HOME: privateDir }
1634
+ });
1635
+ const status = runCommand(["multiagent", "status", "--host", "--target", dir, "--json"], {
1636
+ env: { STARWORK_TRAE_HOME: privateDir }
1637
+ });
1638
+ const continued = runCommand(["multiagent", "continue", "development", "--target", dir, "--json"], {
1639
+ env: { STARWORK_TRAE_HOME: privateDir }
1640
+ });
1641
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--host", "trae", "--json", "--yes"], {
1642
+ env: { STARWORK_TRAE_HOME: privateDir }
1643
+ });
1644
+ const readReport = JSON.parse(read.stdout);
1645
+ const statusReport = JSON.parse(status.stdout);
1646
+ const continueReport = JSON.parse(continued.stdout);
1647
+ const launchReport = JSON.parse(launch.stdout);
1648
+ const serialized = `${read.stdout}\n${status.stdout}\n${continued.stdout}\n${launch.stdout}`;
1649
+
1650
+ assert.equal(read.status, 0);
1651
+ assert.equal(readReport.host.status, "manual_handoff_required");
1652
+ assert.equal(status.status, 0);
1653
+ assert.equal(statusReport.lanes[0].host.status, "manual_host");
1654
+ assert.equal(continued.status, 0);
1655
+ assert.equal(continueReport.status, "manual_handoff_required");
1656
+ assert.equal(launch.status, 0);
1657
+ assert.equal(launchReport.launches[0].launch_status, "manual_handoff_required");
1658
+ assert.equal(launchReport.launches[0].binding_status, "unbound");
1659
+ assert.doesNotMatch(serialized, new RegExp(privateDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
1660
+ assert.doesNotMatch(serialized, /do not read/);
1661
+ });
1662
+
1663
+ test("init --adapter creates host adapter state after workspace initialization", () => {
1664
+ const dir = tempDir();
1665
+
1666
+ const init = runCommand(["init", "--type", "project", "--pack", "general", "--target", dir, "--adapter", "cursor", "--yes"]);
1667
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
1668
+
1669
+ assert.equal(init.status, 0);
1670
+ assert.equal(adaptersState.adapters.cursor.enabled, true);
1671
+ assert.equal(adaptersState.adapters.cursor.rules_entry, ".cursor/rules/starwork.mdc");
1672
+ assert.equal(fs.existsSync(path.join(dir, ".cursor", "skills")), true);
1673
+ });
1674
+
1675
+ test("multiagent launch no longer creates Codex threads from CLI", () => {
1676
+ const dir = tempDir();
1677
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1678
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1679
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1680
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1681
+
1682
+ const fakeCodex = fakeCodexBin({ inputPath });
1683
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes", "--timeout", "1000"], { env: fakeCodex.env });
1684
+ const result = JSON.parse(launch.stdout);
1685
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1686
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1687
+
1688
+ assert.equal(launch.status, 0);
1689
+ assert.equal(result.launches[0].launch_status, "manual_handoff_required");
1690
+ assert.equal(result.launches[0].binding_status, "unbound");
1691
+ assert.match(result.launches[0].instructions, /create_thread/);
1692
+ assert.match(result.launches[0].message, /StarWork MultiAgent Launch/);
1693
+ assert.match(registry, /\| development \| 功能开发 \| unbound \|/);
1694
+ assert.equal(state.lanes.development?.thread_id, undefined);
1695
+ assert.equal(fs.existsSync(inputPath), false);
1696
+ });
1697
+
1698
+ test("multiagent launch message uses short lane role names", () => {
1699
+ const dir = tempDir();
1700
+ const cases = [
1701
+ {
1702
+ lane: "data-review",
1703
+ purpose: "数据复盘: 根据用户提供的每周数据生成分析",
1704
+ expected: "数据复盘 Agent"
1705
+ },
1706
+ {
1707
+ lane: "asset-prep",
1708
+ purpose: "素材准备:根据内容脚本准备封面方案",
1709
+ expected: "素材准备 Agent"
1710
+ },
1711
+ {
1712
+ lane: "content-writing",
1713
+ purpose: "内容写作。根据已确认选题生成文稿",
1714
+ expected: "内容写作 Agent"
1715
+ },
1716
+ {
1717
+ lane: "topic-management",
1718
+ purpose: "只负责登记自媒体选题、维护选题状态",
1719
+ expected: "Topic Management Agent"
1720
+ }
1721
+ ];
1722
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1723
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1724
+ for (const item of cases) {
1725
+ runCommand([
1726
+ "multiagent", "add", item.lane,
1727
+ "--purpose", item.purpose,
1728
+ "--write", "_系统/协作/lanes/**",
1729
+ "--target", dir,
1730
+ "--yes"
1731
+ ]);
1732
+ }
1733
+
1734
+ const messageResults = cases.map((item) => {
1735
+ const launchMessage = runCommand([
1736
+ "multiagent", "message", "launch", item.lane,
1737
+ "--target", dir,
1738
+ "--json"
1739
+ ]);
1740
+ assert.equal(launchMessage.status, 0);
1741
+ return JSON.parse(launchMessage.stdout);
1742
+ });
1743
+ const launch = runCommand([
1744
+ "multiagent", "launch",
1745
+ "--lanes", cases.map((item) => item.lane).join(","),
1746
+ "--target", dir,
1747
+ "--json",
1748
+ "--yes"
1749
+ ]);
1750
+ const launchResult = JSON.parse(launch.stdout);
1751
+
1752
+ assert.deepEqual(messageResults.map((result) => result.session_name), cases.map((item) => item.expected));
1753
+ assert.equal(launch.status, 0);
1754
+ assert.deepEqual(launchResult.launches.map((result) => result.session_name), cases.map((item) => item.expected));
1755
+ assert.equal(launchResult.launches[0].rename_status, "requires_starworkMultiagent_tool");
1756
+ assert.equal(launchResult.launches[0].binding_status, "unbound");
1757
+ for (const result of launchResult.launches) {
1758
+ assert.doesNotMatch(result.session_name, /[::。]|根据|只负责|用于|\/Users|[0-9a-f]{8}-[0-9a-f]{4}/u);
1759
+ }
1760
+ });
1761
+
1762
+ test("multiagent launch does not call fake codex app-server even when available", () => {
1763
+ const dir = tempDir();
1764
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1765
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1766
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1767
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1768
+
1769
+ const fakeCodex = fakeCodexBin({ inputPath, failThreadNameSet: true });
1770
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes", "--timeout", "1000"], { env: fakeCodex.env });
1771
+ const result = JSON.parse(launch.stdout);
1772
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1773
+
1774
+ assert.equal(launch.status, 0);
1775
+ assert.equal(result.launches[0].binding_status, "unbound");
1776
+ assert.equal(result.launches[0].rename_status, "requires_starworkMultiagent_tool");
1777
+ assert.match(registry, /\| development \| 功能开发 \| unbound \|/);
1778
+ assert.equal(fs.existsSync(inputPath), false);
1779
+ });
1780
+
1781
+ test("multiagent request record supports Codex thread-tool delivery status", () => {
1782
+ const dir = tempDir();
1783
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1784
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1785
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1786
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1787
+
1788
+ const message = "<!-- STARWORK:MULTIAGENT_MESSAGE v1 -->\n\n请开始实现 v0.8。\n\n<!-- /STARWORK:MULTIAGENT_MESSAGE -->";
1789
+ const record = runCommand([
1790
+ "multiagent", "request", "record",
1791
+ "--from", "product-planning",
1792
+ "--to", "development",
1793
+ "--message", message,
1794
+ "--host-delivery", "delivered_via_codex_thread_tool",
1795
+ "--delivery-tool", "send_message_to_thread",
1796
+ "--target", dir,
1797
+ "--json",
1798
+ "--yes"
1799
+ ]);
1800
+ const recordResult = JSON.parse(record.stdout);
1801
+ const shared = fs.readFileSync(path.join(dir, "_系统", "协作", "shared.md"), "utf8");
1802
+ const state = readJson(path.join(dir, ".starwork", "agent-lanes", "state.json"));
1803
+
1804
+ assert.equal(record.status, 0);
1805
+ assert.equal(recordResult.host_delivery.status, "delivered_via_codex_thread_tool");
1806
+ assert.equal(recordResult.host_delivery.delivery_tool, "send_message_to_thread");
1807
+ assert.match(shared, /product-planning \| development \| .*请开始实现 v0\.8.* \| delivered_via_codex_thread_tool \| delivered_via_codex_thread_tool/);
1808
+ assert.equal(state.requests[0].host_delivery.status, "delivered_via_codex_thread_tool");
1809
+ assert.equal(state.requests[0].host_delivery.delivery_tool, "send_message_to_thread");
1810
+ });
1811
+
1812
+ test("multiagent request record accepts recorded-only Codex boundary status", () => {
1813
+ const dir = tempDir();
1814
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1815
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1816
+ runCommand(["multiagent", "add", "product-planning", "--purpose", "产品规划", "--write", "product/planning/**", "--target", dir, "--yes"]);
1817
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1818
+
1819
+ const record = runCommand([
1820
+ "multiagent", "request", "record",
1821
+ "--from", "product-planning",
1822
+ "--to", "development",
1823
+ "--message", "只记录,不代表自动送达。",
1824
+ "--host-delivery", "recorded_only",
1825
+ "--delivery-tool", "manual",
1826
+ "--target", dir,
1827
+ "--json",
1828
+ "--yes"
1829
+ ]);
1830
+ const recordResult = JSON.parse(record.stdout);
1831
+
1832
+ assert.equal(record.status, 0);
1833
+ assert.equal(recordResult.host_delivery.status, "recorded_only");
1834
+ assert.equal(recordResult.host_delivery.delivery_tool, "manual");
1835
+ });
1836
+
1837
+ test("multiagent launch refuses non-StarWork targets without sidecar initialization", () => {
1838
+ const dir = tempDir();
1839
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing project rules\n", "utf8");
1840
+
1841
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes"]);
1842
+
1843
+ assert.notEqual(launch.status, 0);
1844
+ assert.match(launch.stderr, /starworkInit/);
1845
+ assert.doesNotMatch(launch.stderr, /请先运行 starwork init/);
1846
+ assert.equal(fs.existsSync(path.join(dir, "AGENTS.starwork-new.md")), false);
1847
+ assert.equal(fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8"), "# Existing project rules\n");
1848
+ });
1849
+
1850
+ test("multiagent write commands stop while host agent docs are pending merge", () => {
1851
+ const dir = tempDir();
1852
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent Rules\n", "utf8");
1853
+ runInit(["--type", "project", "--pack", "general", "--adapter", "codex", "--target", dir, "--yes"]);
1854
+
1855
+ const result = runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1856
+
1857
+ assert.equal(result.status, 1);
1858
+ assert.match(result.stderr, /pending_merge/);
1859
+ assert.match(result.stderr, /starworkInit/);
1860
+ });
1861
+
1862
+ test("multiagent launch keeps lane unbound instead of using legacy final verification", () => {
1863
+ const dir = tempDir();
1864
+ const inputPath = path.join(tempDir(), "codex-input.jsonl");
1865
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1866
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1867
+ runCommand(["multiagent", "add", "development", "--purpose", "功能开发", "--write", "product/cli/**", "--target", dir, "--yes"]);
1868
+
1869
+ const fakeCodex = fakeCodexBin({ inputPath, omitFinalRead: true });
1870
+ const launch = runCommand(["multiagent", "launch", "development", "--target", dir, "--json", "--yes", "--timeout", "1000"], { env: fakeCodex.env });
1871
+ const result = JSON.parse(launch.stdout);
1872
+ const registry = fs.readFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), "utf8");
1873
+
1874
+ assert.equal(launch.status, 0);
1875
+ assert.equal(result.launches[0].status, "manual_handoff_required");
1876
+ assert.equal(result.launches[0].binding_status, "unbound");
1877
+ assert.match(registry, /\| development \| 功能开发 \| unbound \|/);
1878
+ assert.equal(fs.existsSync(inputPath), false);
1879
+ });
1880
+
1881
+ test("multiagent status infers workspace for legacy registries", () => {
1882
+ const dir = tempDir();
1883
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
1884
+ runCommand(["multiagent", "init", "--target", dir, "--yes"]);
1885
+ fs.writeFileSync(path.join(dir, "_系统", "协作", "agent-lanes.md"), `# Agent Lanes
1886
+
1887
+ ## Lanes
1888
+
1889
+ | lane | purpose | current_session | write_scope | worklog |
1890
+ |---|---|---|---|---|
1891
+ | legacy | 旧版职责 | unbound | legacy/** | lanes/legacy/worklog.md |
1892
+ `);
1893
+
1894
+ const status = runCommand(["multiagent", "status", "--target", dir, "--json"]);
1895
+ const report = JSON.parse(status.stdout);
1896
+
1897
+ assert.equal(status.status, 0);
1898
+ assert.equal(report.lanes[0].lane, "legacy");
1899
+ assert.equal(report.lanes[0].workspace, "lanes/legacy/workspace");
1900
+ });
1901
+
1902
+ test("spawn creates a project from a hub", () => {
135
1903
  const hub = tempDir();
136
1904
  const target = tempDir();
137
1905
  runInit(["--type", "hub", "--target", hub, "--yes"]);
138
1906
 
139
- const spawn = runCommand(["spawn", "--hub", hub, "--name", "Content Site", "--id", "content-site", "--target", target, "--mode", "matter", "--yes"]);
1907
+ const spawn = runCommand(["spawn", "--hub", hub, "--name", "Content Site", "--id", "content-site", "--target", target, "--mode", "project", "--yes"]);
140
1908
  const state = readJson(path.join(target, ".starwork", "workspace.json"));
141
1909
  const sync = readJson(path.join(target, ".core-sync.json"));
1910
+ const skills = readJson(path.join(target, ".starwork", "skills.json"));
142
1911
  const registry = readJson(path.join(hub, "项目", "registry.json"));
143
1912
  const doctor = runDoctor(["--target", target, "--json"]);
144
1913
  const report = JSON.parse(doctor.stdout);
145
1914
 
146
1915
  assert.equal(spawn.status, 0);
147
- assert.equal(state.workspace_type, "satellite-matter");
148
- assert.equal(state.kit, "satellite-matter");
1916
+ assert.equal(state.workspace_type, "project");
1917
+ assert.equal(state.kit, "project");
1918
+ assert.equal(state.project_center.project_id, "content-site");
1919
+ assert.equal(state.project_center.path, hub);
149
1920
  assert.equal(state.hub.project_id, "content-site");
150
1921
  assert.equal(sync.project_id, "content-site");
151
1922
  assert.equal(registry.projects[0].id, "content-site");
152
1923
  assert.equal(registry.projects[0].path, path.resolve(target));
153
- assert.equal(fs.lstatSync(path.join(target, "知识")).isSymbolicLink(), true);
154
- assert.equal(fs.lstatSync(path.join(target, ".agents", "skills")).isSymbolicLink(), true);
1924
+ assert.equal(fs.existsSync(path.join(target, "知识")), false);
1925
+ assert.equal(fs.existsSync(path.join(target, "知识库")), false);
1926
+ assert.equal(fs.existsSync(path.join(target, ".starwork", "handoff", "state.json")), true);
1927
+ assert.equal(fs.existsSync(path.join(target, "_系统", "主库同步", "README.md")), true);
1928
+ assert.match(fs.readFileSync(path.join(target, "AGENTS.md"), "utf8"), /_系统\/主库同步\/README\.md/);
1929
+ assert.match(fs.readFileSync(path.join(target, "_系统", "身份", "README.md"), "utf8"), /来自项目中心/);
1930
+ assert.match(fs.readFileSync(path.join(target, "_系统", "教训", "README.md"), "utf8"), /来自项目中心/);
1931
+ assert.equal(fs.lstatSync(path.join(target, ".agents", "skills")).isDirectory(), true);
1932
+ assert(skills.skills.some((skill) => skill.id === "neat-freak"));
1933
+ assert.equal(sync.resources.skills.mode, "selected");
1934
+ assert.equal(doctor.status, 0);
1935
+ });
1936
+
1937
+ test("spawn rejects removed matter mode", () => {
1938
+ const hub = tempDir();
1939
+ const target = tempDir();
1940
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
1941
+
1942
+ const spawn = runCommand(["spawn", "--hub", hub, "--name", "Old Matter", "--target", target, "--mode", "matter", "--yes"]);
1943
+
1944
+ assert.equal(spawn.status, 1);
1945
+ assert.match(spawn.stderr, /不支持的 spawn 模式:matter/);
1946
+ });
1947
+
1948
+ test("spawn creates a starter project from a hub", () => {
1949
+ const hub = tempDir();
1950
+ const target = tempDir();
1951
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
1952
+
1953
+ const spawn = runCommand(["spawn", "--hub", hub, "--name", "Quick Project", "--id", "quick-project", "--target", target, "--mode", "starter", "--yes"]);
1954
+ const state = readJson(path.join(target, ".starwork", "workspace.json"));
1955
+ const doctor = runDoctor(["--target", target, "--json"]);
1956
+ const report = JSON.parse(doctor.stdout);
1957
+
1958
+ assert.equal(spawn.status, 0);
1959
+ assert.equal(state.workspace_type, "project");
1960
+ assert.equal(state.kit, "project");
1961
+ assert.equal(fs.existsSync(path.join(target, "事项")), false);
155
1962
  assert.equal(doctor.status, 0);
156
1963
  });
157
1964
 
158
- test("spawn creates a starter project from a hub", () => {
1965
+ test("spawn creates an English starter satellite from a hub", () => {
159
1966
  const hub = tempDir();
160
1967
  const target = tempDir();
161
- runInit(["--type", "hub", "--target", hub, "--yes"]);
1968
+ runInit(["--type", "hub", "--language", "en", "--target", hub, "--yes"]);
162
1969
 
163
- const spawn = runCommand(["spawn", "--hub", hub, "--name", "Quick Project", "--id", "quick-project", "--target", target, "--mode", "starter", "--yes"]);
1970
+ const spawn = runCommand(["spawn", "--hub", hub, "--name", "English Project", "--id", "english-project", "--target", target, "--mode", "starter", "--language", "en", "--yes"]);
164
1971
  const state = readJson(path.join(target, ".starwork", "workspace.json"));
1972
+ const sync = readJson(path.join(target, ".core-sync.json"));
165
1973
  const doctor = runDoctor(["--target", target, "--json"]);
166
1974
  const report = JSON.parse(doctor.stdout);
167
1975
 
168
1976
  assert.equal(spawn.status, 0);
169
- assert.equal(state.workspace_type, "satellite-starter");
170
- assert.equal(state.kit, "satellite-starter");
171
- assert.equal(fs.existsSync(path.join(target, "事项")), false);
1977
+ assert.equal(state.language, "en");
1978
+ assert.equal(state.workspace_type, "project");
1979
+ assert.equal(state.paths.formal_source, "outputs/final/");
1980
+ assert.equal(state.paths.business_work_area, "outputs/drafts/");
1981
+ assert.equal(sync.resources.identity.target, "_system/identity");
1982
+ assert.equal(Object.hasOwn(sync.resources, "knowledge"), false);
1983
+ assert.equal(fs.existsSync(path.join(target, "_system", "context", "current-project.md")), true);
1984
+ assert.equal(fs.existsSync(path.join(target, "_system", "tasks", "current-work.md")), true);
1985
+ assert.equal(fs.existsSync(path.join(target, "_system", "main-repo-sync", "README.md")), true);
1986
+ assert.match(fs.readFileSync(path.join(target, "AGENTS.md"), "utf8"), /_system\/main-repo-sync\/README\.md/);
1987
+ assert.match(fs.readFileSync(path.join(target, "_system", "identity", "README.md"), "utf8"), /Project Center identity snapshots/);
1988
+ assert.equal(fs.existsSync(path.join(target, "references", "README.md")), true);
1989
+ assert.equal(fs.existsSync(path.join(target, "outputs", "final", "README.md")), true);
1990
+ assert.equal(fs.existsSync(path.join(target, "knowledge")), false);
1991
+ assert.equal(fs.existsSync(path.join(target, "knowledge-base")), false);
1992
+ assert.equal(fs.existsSync(path.join(target, ".starwork", "handoff", "state.json")), true);
1993
+ assert.equal(fs.existsSync(path.join(target, "_系统")), false);
1994
+ assert.equal(fs.existsSync(path.join(target, "知识")), false);
172
1995
  assert.equal(doctor.status, 0);
1996
+ assert.equal(report.ok, true);
173
1997
  });
174
1998
 
175
1999
  test("spawn creates a customized project from a blueprint", () => {
@@ -183,17 +2007,17 @@ test("spawn creates a customized project from a blueprint", () => {
183
2007
  fs.writeFileSync(path.join(blueprintDir, "seed", "会议纪要", "README.md"), "# 会议纪要\n\n项目:{{project.name}}\n", "utf8");
184
2008
  fs.writeFileSync(path.join(blueprintDir, "blueprint.json"), `${JSON.stringify({
185
2009
  schema: "starwork.spawn_blueprint.v0.1",
186
- name: "Blueprint Project",
187
- project_id: "blueprint-project",
188
- description: "用 blueprint 生成的定制项目。",
2010
+ name: "Custom Project",
2011
+ project_id: "custom-project",
2012
+ description: "用于整理会议纪要、资料库和交付物的定制项目。",
189
2013
  base: {
190
- mode: "matter",
191
- kit: "satellite-matter",
2014
+ mode: "project",
2015
+ kit: "project",
192
2016
  language: "zh"
193
2017
  },
194
2018
  paths: {
195
2019
  formal_source: "交付物/确认版本/",
196
- business_work_area: "事项/"
2020
+ business_work_area: "资料库/"
197
2021
  },
198
2022
  folders: [
199
2023
  "资料库/",
@@ -214,6 +2038,7 @@ test("spawn creates a customized project from a blueprint", () => {
214
2038
  const spawn = runCommand(["spawn", "--hub", hub, "--target", target, "--blueprint", path.join(blueprintDir, "blueprint.json"), "--yes"]);
215
2039
  const state = readJson(path.join(target, ".starwork", "workspace.json"));
216
2040
  const agents = fs.readFileSync(path.join(target, "AGENTS.md"), "utf8");
2041
+ const blueprintRule = fs.readFileSync(path.join(target, ".starwork", "rules", "project.file_boundaries.md"), "utf8");
217
2042
  const projectStatus = fs.readFileSync(path.join(target, "_系统", "上下文", "当前项目.md"), "utf8");
218
2043
  const seed = fs.readFileSync(path.join(target, "会议纪要", "README.md"), "utf8");
219
2044
  const registry = readJson(path.join(hub, "项目", "registry.json"));
@@ -221,22 +2046,85 @@ test("spawn creates a customized project from a blueprint", () => {
221
2046
  const report = JSON.parse(doctor.stdout);
222
2047
 
223
2048
  assert.equal(spawn.status, 0);
224
- assert.equal(state.workspace_type, "satellite-matter");
2049
+ assert.equal(state.workspace_type, "project");
225
2050
  assert.equal(state.paths.formal_source, "交付物/确认版本/");
226
2051
  assert.equal(state.customization.type, "spawn_blueprint");
227
2052
  assert.equal(state.customization.agent_rules[0].slot, "project.file_boundaries");
228
2053
  assert.equal(fs.existsSync(path.join(target, "资料库")), true);
229
2054
  assert.equal(fs.existsSync(path.join(target, "交付物", "确认版本")), true);
230
- assert.match(agents, /StarWork Blueprint: project\.file_boundaries/);
231
- assert.match(agents, /正式成果放在 交付物\/确认版本\//);
232
- assert.match(projectStatus, /工作区定制/);
233
- assert.match(seed, /项目:Blueprint Project/);
2055
+ assert.match(agents, /\.starwork\/rules\/index\.md/);
2056
+ assert.doesNotMatch(agents, /StarWork Rule Slot:/);
2057
+ assert.doesNotMatch(agents, /StarWork Blueprint:/);
2058
+ assert.match(blueprintRule, /正式成果放在 交付物\/确认版本\//);
2059
+ assert.match(projectStatus, /项目约定/);
2060
+ assert.doesNotMatch(projectStatus, /Blueprint|blueprint|starwork spawn|doctor|Initialized as|Folders Not Used/);
2061
+ assert.match(seed, /项目:Custom Project/);
234
2062
  assert.equal(registry.projects[0].customized, true);
235
2063
  assert.equal(doctor.status, 0);
236
2064
  assert(report.checks.some((check) => check.id === "blueprint.folder.exists" && check.level === "pass"));
237
2065
  assert(report.checks.some((check) => check.id === "blueprint.rule.injected" && check.level === "pass"));
238
2066
  });
239
2067
 
2068
+ test("spawn distributes selected hub-managed skills from registry", () => {
2069
+ const hub = tempDir();
2070
+ const target = tempDir();
2071
+ const blueprintDir = tempDir();
2072
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
2073
+ fs.mkdirSync(path.join(hub, "技能", "meeting-summary"), { recursive: true });
2074
+ fs.writeFileSync(path.join(hub, "技能", "meeting-summary", "SKILL.md"), "# Meeting Summary\n", "utf8");
2075
+ fs.writeFileSync(path.join(hub, "技能", "registry.json"), `${JSON.stringify({
2076
+ schema: "starwork.skill_registry.v0.1",
2077
+ owner: "hub",
2078
+ updated_at: "2026-05-20T00:00:00.000Z",
2079
+ skills: [
2080
+ {
2081
+ id: "meeting-summary",
2082
+ name: "Meeting Summary",
2083
+ type: "hub-managed",
2084
+ source: { kind: "local", path: "技能/meeting-summary" },
2085
+ ownership: "hub-owned",
2086
+ distribution: { mode: "symlink", default_for_spawn: false },
2087
+ description: "会议纪要整理。"
2088
+ }
2089
+ ]
2090
+ }, null, 2)}\n`, "utf8");
2091
+ fs.writeFileSync(path.join(blueprintDir, "blueprint.json"), `${JSON.stringify({
2092
+ schema: "starwork.spawn_blueprint.v0.1",
2093
+ name: "Skill Project",
2094
+ project_id: "skill-project",
2095
+ base: {
2096
+ mode: "starter",
2097
+ kit: "satellite-starter",
2098
+ language: "zh"
2099
+ },
2100
+ skills: [
2101
+ {
2102
+ id: "meeting-summary",
2103
+ source: "hub",
2104
+ distribution: "symlink",
2105
+ reason: "这个项目需要会议纪要整理。"
2106
+ }
2107
+ ]
2108
+ }, null, 2)}\n`, "utf8");
2109
+
2110
+ const result = runCommand(["spawn", "--hub", hub, "--target", target, "--blueprint", path.join(blueprintDir, "blueprint.json"), "--yes"]);
2111
+ const skills = readJson(path.join(target, ".starwork", "skills.json"));
2112
+ const sync = readJson(path.join(target, ".core-sync.json"));
2113
+ const doctor = runDoctor(["--target", target, "--json"]);
2114
+ const report = JSON.parse(doctor.stdout);
2115
+
2116
+ assert.equal(result.status, 0);
2117
+ const meetingSkill = skills.skills.find((skill) => skill.id === "meeting-summary");
2118
+ assert.equal(meetingSkill.source.kind, "hub");
2119
+ assert.equal(meetingSkill.distribution, "symlink");
2120
+ assert.equal(fs.lstatSync(path.join(target, ".agents", "skills")).isDirectory(), true);
2121
+ assert.equal(fs.lstatSync(path.join(target, ".agents", "skills", "meeting-summary")).isSymbolicLink(), true);
2122
+ assert.equal(fs.existsSync(path.join(target, ".agents", "skills", "starworkSpawn")), false);
2123
+ assert(sync.resources.skills.items.some((item) => item.id === "meeting-summary"));
2124
+ assert.equal(doctor.status, 0);
2125
+ assert(report.skills.mounts.some((mount) => mount.id === "meeting-summary" && mount.status === "ok"));
2126
+ });
2127
+
240
2128
  test("spawn blueprint dry-run does not write target or registry", () => {
241
2129
  const hub = tempDir();
242
2130
  const target = tempDir();
@@ -274,13 +2162,13 @@ test("spawn blueprint rejects unsafe paths", () => {
274
2162
  schema: "starwork.spawn_blueprint.v0.1",
275
2163
  name: "Unsafe Blueprint",
276
2164
  base: {
277
- mode: "matter",
278
- kit: "satellite-matter",
2165
+ mode: "project",
2166
+ kit: "project",
279
2167
  language: "zh"
280
2168
  },
281
2169
  paths: {
282
2170
  formal_source: "../escape/",
283
- business_work_area: "事项/"
2171
+ business_work_area: "参考资料/"
284
2172
  }
285
2173
  }, null, 2)}\n`, "utf8");
286
2174
  runInit(["--type", "hub", "--target", hub, "--yes"]);
@@ -312,7 +2200,7 @@ test("spawn refuses non-hub workspaces", () => {
312
2200
  const result = runCommand(["spawn", "--hub", workspace, "--name", "Nope", "--id", "nope", "--target", target, "--yes"]);
313
2201
 
314
2202
  assert.equal(result.status, 1);
315
- assert.match(result.stderr, /多项目管理中枢/);
2203
+ assert.match(result.stderr, /项目中心/);
316
2204
  });
317
2205
 
318
2206
  test("spawn refuses non-empty target directories", () => {
@@ -335,7 +2223,20 @@ test("doctor fails when AGENTS.md is missing", () => {
335
2223
  const result = runDoctor(["--target", dir]);
336
2224
 
337
2225
  assert.equal(result.status, 1);
338
- assert.match(result.stdout, /core\.entry_rules\.exists/);
2226
+ assert.match(result.stdout, /缺少 AI 入口规则 AGENTS\.md/);
2227
+ assert.doesNotMatch(result.stdout, /core\.entry_rules\.exists/);
2228
+ });
2229
+
2230
+ test("doctor warns when agent rules reference missing workspace paths", () => {
2231
+ const dir = tempDir();
2232
+ runInit(["--type", "project", "--pack", "general", "--target", dir, "--yes"]);
2233
+ fs.appendFileSync(path.join(dir, "AGENTS.md"), "\n请把草稿写到 `missing-drafts/`。\n", "utf8");
2234
+
2235
+ const result = runDoctor(["--target", dir, "--json"]);
2236
+ const report = JSON.parse(result.stdout);
2237
+
2238
+ assert.equal(result.status, 0);
2239
+ assert(report.checks.some((check) => check.id === "agents.references.existing_paths" && check.level === "warn" && check.message.includes("missing-drafts")));
339
2240
  });
340
2241
 
341
2242
  test("doctor fails when the formal source is missing", () => {
@@ -353,7 +2254,7 @@ test("doctor fails when the formal source is missing", () => {
353
2254
 
354
2255
  test("doctor fails when pack seed is missing", () => {
355
2256
  const dir = tempDir();
356
- runInit(["--type", "single-matter", "--pack", "content-creator", "--target", dir, "--yes"]);
2257
+ runInit(["--type", "project", "--pack", "content-creator", "--target", dir, "--yes"]);
357
2258
  fs.rmSync(path.join(dir, "选题池", "README.md"));
358
2259
 
359
2260
  const result = runDoctor(["--target", dir, "--json"]);
@@ -372,17 +2273,459 @@ test("doctor fails outside a StarWork workspace", () => {
372
2273
  assert.match(result.stdout, /不是 StarWork 工作台/);
373
2274
  });
374
2275
 
2276
+ test("doctor reports legacy signals for an English legacy template", () => {
2277
+ const dir = tempDir();
2278
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Legacy Agent Rules\n", "utf8");
2279
+ fs.mkdirSync(path.join(dir, "references"), { recursive: true });
2280
+ fs.mkdirSync(path.join(dir, "outputs", "drafts"), { recursive: true });
2281
+ fs.mkdirSync(path.join(dir, "outputs", "final"), { recursive: true });
2282
+
2283
+ const result = runDoctor(["--target", dir, "--json"]);
2284
+ const report = JSON.parse(result.stdout);
2285
+
2286
+ assert.equal(result.status, 1);
2287
+ assert.equal(report.upgrade.candidate, true);
2288
+ assert.equal(report.upgrade.source, "legacy-template");
2289
+ assert.equal(report.upgrade.inferred.language, "en");
2290
+ assert.equal(report.upgrade.inferred.workspace_type, "project");
2291
+ assert.equal(Object.hasOwn(report.upgrade.inferred, "pack"), false);
2292
+ assert.equal(Object.hasOwn(report.upgrade, "next_steps"), false);
2293
+ assert.deepEqual(report.upgrade.inferred.references, ["references"]);
2294
+ assert(report.upgrade.inferred.outputs.includes("outputs"));
2295
+ assert(report.upgrade.inferred.reasons.language.some((reason) => reason.includes("英文工作区信号")));
2296
+ assert(report.upgrade.inferred.reasons.outputs.some((reason) => reason.includes("outputs")));
2297
+ assert(report.checks.some((check) => check.id === "legacy.references.detected" && check.level === "info"));
2298
+ });
2299
+
2300
+ test("doctor exposes inventory and semantic signals for non-standard legacy folders", () => {
2301
+ const dir = tempDir();
2302
+ fs.writeFileSync(path.join(dir, "README.md"), "# Custom Workspace\n", "utf8");
2303
+ fs.mkdirSync(path.join(dir, "资料库", "文章"), { recursive: true });
2304
+ fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
2305
+ fs.mkdirSync(path.join(dir, "推进"), { recursive: true });
2306
+
2307
+ const result = runDoctor(["--target", dir, "--json"]);
2308
+ const report = JSON.parse(result.stdout);
2309
+
2310
+ assert.equal(result.status, 1);
2311
+ assert.equal(report.target, path.resolve(dir));
2312
+ assert(report.inventory.directories.some((item) => item.path === "资料库"));
2313
+ assert(report.inventory.directories.some((item) => item.path === "成稿"));
2314
+ assert(report.inventory.files.some((item) => item.path === "README.md"));
2315
+ assert(report.signals.possible_reference_dirs.includes("资料库"));
2316
+ assert(report.signals.possible_output_dirs.includes("成稿"));
2317
+ assert(report.signals.possible_current_work_dirs.includes("推进"));
2318
+ assert(report.signals.readonly_candidate_dirs.includes("资料库"));
2319
+ assert(report.signals.writable_candidate_dirs.includes("推进"));
2320
+ assert(report.upgrade.inferred.reasons.references.some((reason) => reason.includes("资料库")));
2321
+ assert.equal(report.upgrade.candidate, true);
2322
+ });
2323
+
2324
+ test("doctor reports legacy signals for a Chinese legacy template with matter folder", () => {
2325
+ const dir = tempDir();
2326
+ fs.mkdirSync(path.join(dir, "_系统", "身份"), { recursive: true });
2327
+ fs.mkdirSync(path.join(dir, "事项"), { recursive: true });
2328
+ fs.mkdirSync(path.join(dir, "参考资料"), { recursive: true });
2329
+ fs.mkdirSync(path.join(dir, "输出", "确认成果"), { recursive: true });
2330
+
2331
+ const result = runDoctor(["--target", dir]);
2332
+
2333
+ assert.equal(result.status, 1);
2334
+ assert.match(result.stdout, /旧目录识别/);
2335
+ assert.match(result.stdout, /推测用途:一个项目工作台/);
2336
+ assert.match(result.stdout, /推测语言:中文/);
2337
+ assert.match(result.stdout, /不会自动移动、删除或修改你的文件/);
2338
+ assert.doesNotMatch(result.stdout, /confidence/);
2339
+ assert.doesNotMatch(result.stdout, /--dry-run/);
2340
+ assert.doesNotMatch(result.stdout, /下一步/);
2341
+ });
2342
+
2343
+ test("upgrade blueprint dry-run does not write files", () => {
2344
+ const dir = tempDir();
2345
+ const blueprintDir = tempDir();
2346
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent\n", "utf8");
2347
+ fs.mkdirSync(path.join(dir, "资料库"), { recursive: true });
2348
+ fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
2349
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2350
+ schema: "starwork.upgrade_blueprint.v0.1",
2351
+ generated_by: "starworkDoctor",
2352
+ source: {
2353
+ doctor_schema: "starwork.doctor.result.v0.1",
2354
+ diagnosis: "legacy-template",
2355
+ core_fit: "medium"
2356
+ },
2357
+ base: {
2358
+ workspace_type: "single-light",
2359
+ kit: "local-starter",
2360
+ language: "zh",
2361
+ pack: "general"
2362
+ },
2363
+ strategy: "preserve-names",
2364
+ paths: {
2365
+ formal_source: "成稿/",
2366
+ business_work_area: "资料库/"
2367
+ },
2368
+ core_role_mapping: [],
2369
+ actions: [
2370
+ { type: "ensure_dir", path: ".starwork/" },
2371
+ { type: "write_workspace_state" },
2372
+ { type: "copy_kit_missing_files" }
2373
+ ]
2374
+ }, null, 2)}\n`, "utf8");
2375
+
2376
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--dry-run"]);
2377
+
2378
+ assert.equal(result.status, 0);
2379
+ assert.match(result.stdout, /升级预览/);
2380
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "workspace.json")), false);
2381
+ });
2382
+
2383
+ test("upgrade applies a blueprint and keeps existing files", () => {
2384
+ const dir = tempDir();
2385
+ const blueprintDir = tempDir();
2386
+ fs.mkdirSync(path.join(blueprintDir, "rules"), { recursive: true });
2387
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Agent\n\nKeep me.\n", "utf8");
2388
+ fs.mkdirSync(path.join(dir, "资料库"), { recursive: true });
2389
+ fs.mkdirSync(path.join(dir, "成稿"), { recursive: true });
2390
+ fs.mkdirSync(path.join(dir, "事项"), { recursive: true });
2391
+ fs.writeFileSync(path.join(blueprintDir, "rules", "core-boundaries.md"), "正式成果:{{paths.formal_source}}\n当前工作:{{paths.business_work_area}}\n", "utf8");
2392
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2393
+ schema: "starwork.upgrade_blueprint.v0.1",
2394
+ target: ".",
2395
+ generated_by: "starworkDoctor",
2396
+ source: {
2397
+ doctor_schema: "starwork.doctor.result.v0.1",
2398
+ diagnosis: "legacy-template",
2399
+ core_fit: "medium"
2400
+ },
2401
+ base: {
2402
+ workspace_type: "project",
2403
+ kit: "project",
2404
+ language: "zh",
2405
+ pack: "general"
2406
+ },
2407
+ strategy: "preserve-names",
2408
+ paths: {
2409
+ formal_source: "成稿/",
2410
+ business_work_area: "资料库/"
2411
+ },
2412
+ core_role_mapping: [
2413
+ { role: "references", path: "资料库/", confidence: "high", reason: "用户确认" },
2414
+ { role: "formal_source", path: "成稿/", confidence: "high", reason: "用户确认" }
2415
+ ],
2416
+ actions: [
2417
+ { type: "ensure_dir", path: ".starwork/" },
2418
+ { type: "write_workspace_state" },
2419
+ { type: "copy_kit_missing_files" },
2420
+ { type: "inject_agent_rules", target: "AGENTS.md", from: "rules/core-boundaries.md", slot: "upgrade.core_boundaries" }
2421
+ ],
2422
+ preserve: ["资料库/", "成稿/"],
2423
+ verification: {
2424
+ run_doctor_after: true,
2425
+ expected_workspace_type: "project"
2426
+ }
2427
+ }, null, 2)}\n`, "utf8");
2428
+
2429
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
2430
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
2431
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
2432
+ const upgradeRule = fs.readFileSync(path.join(dir, ".starwork", "rules", "upgrade.core_boundaries.md"), "utf8");
2433
+ const doctor = runDoctor(["--target", dir, "--json"]);
2434
+ const report = JSON.parse(doctor.stdout);
2435
+
2436
+ assert.equal(result.status, 0);
2437
+ assert.equal(state.workspace_type, "project");
2438
+ assert.equal(state.kit, "project");
2439
+ assert.equal(state.paths.formal_source, "成稿/");
2440
+ assert.equal(state.paths.business_work_area, "资料库/");
2441
+ assert.equal(state.upgrade.type, "upgrade_blueprint");
2442
+ assert.match(agents, /Keep me/);
2443
+ assert.match(agents, /\.starwork\/rules\/index\.md/);
2444
+ assert.doesNotMatch(agents, /StarWork Rule Slot:/);
2445
+ assert.doesNotMatch(agents, /StarWork Upgrade:/);
2446
+ assert.match(upgradeRule, /正式成果:成稿\//);
2447
+ assert.equal(fs.existsSync(path.join(dir, "_系统", "上下文", "当前项目.md")), true);
2448
+ assert.equal(doctor.status, 0);
2449
+ assert.equal(report.ok, true);
2450
+ });
2451
+
2452
+ test("hub upgrade dry-run does not create duplicate standard dirs", () => {
2453
+ const dir = tempDir();
2454
+ const blueprintDir = tempDir();
2455
+ fs.mkdirSync(path.join(dir, "projects", "coordination"), { recursive: true });
2456
+ fs.mkdirSync(path.join(dir, "knowledge"), { recursive: true });
2457
+ fs.mkdirSync(path.join(dir, "identity"), { recursive: true });
2458
+ fs.mkdirSync(path.join(dir, "lessons"), { recursive: true });
2459
+ fs.mkdirSync(path.join(dir, "skills"), { recursive: true });
2460
+ fs.mkdirSync(path.join(dir, ".incoming"), { recursive: true });
2461
+ fs.writeFileSync(path.join(dir, "README.md"), "# Main Repository\n", "utf8");
2462
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Hub Rules\n", "utf8");
2463
+ fs.writeFileSync(path.join(dir, "projects", "registry.json"), "[]\n", "utf8");
2464
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2465
+ schema: "starwork.upgrade_blueprint.v0.1",
2466
+ generated_by: "starworkDoctor",
2467
+ source: {
2468
+ doctor_schema: "starwork.doctor.result.v0.1",
2469
+ diagnosis: "hub-like-main-repository",
2470
+ core_fit: "high"
2471
+ },
2472
+ base: {
2473
+ workspace_type: "hub",
2474
+ kit: "hub",
2475
+ language: "zh",
2476
+ pack: null
2477
+ },
2478
+ strategy: "preserve-names",
2479
+ paths: {
2480
+ formal_source: "projects/",
2481
+ business_work_area: "projects/coordination/"
2482
+ },
2483
+ core_role_mapping: [
2484
+ { role: "projects", path: "projects/", confidence: "high", reason: "用户确认" },
2485
+ { role: "project_registry", path: "projects/registry.json", confidence: "high", reason: "用户确认" },
2486
+ { role: "coordination", path: "projects/coordination/", confidence: "high", reason: "用户确认" },
2487
+ { role: "incoming", path: ".incoming/", confidence: "high", reason: "用户确认" },
2488
+ { role: "knowledge", path: "knowledge/", confidence: "high", reason: "用户确认" },
2489
+ { role: "identity", path: "identity/", confidence: "high", reason: "用户确认" },
2490
+ { role: "lessons", path: "lessons/", confidence: "high", reason: "用户确认" },
2491
+ { role: "skills", path: "skills/", confidence: "high", reason: "用户确认" }
2492
+ ],
2493
+ actions: [
2494
+ { type: "ensure_dir", path: ".starwork/" },
2495
+ { type: "write_workspace_state" },
2496
+ { type: "copy_kit_missing_files" }
2497
+ ],
2498
+ preserve: ["projects/", "knowledge/", "identity/", "lessons/", "skills/", ".incoming/"]
2499
+ }, null, 2)}\n`, "utf8");
2500
+
2501
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--json", "--dry-run"]);
2502
+ const plan = JSON.parse(result.stdout);
2503
+ const plannedPaths = plan.actions.map((action) => action.path);
2504
+
2505
+ assert.equal(result.status, 0);
2506
+ assert.equal(plan.workspace_type, "hub");
2507
+ assert.equal(plan.pack, null);
2508
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "workspace.json")), false);
2509
+ assert(!plannedPaths.some((item) => item === "项目" || item.startsWith("项目/")));
2510
+ assert(!plannedPaths.some((item) => item === "知识" || item.startsWith("知识/")));
2511
+ });
2512
+
2513
+ test("upgrade applies a hub preserve-names blueprint", () => {
2514
+ const dir = tempDir();
2515
+ const blueprintDir = tempDir();
2516
+ fs.mkdirSync(path.join(blueprintDir, "rules"), { recursive: true });
2517
+ fs.mkdirSync(path.join(dir, "projects", "coordination"), { recursive: true });
2518
+ fs.mkdirSync(path.join(dir, "knowledge"), { recursive: true });
2519
+ fs.mkdirSync(path.join(dir, "identity"), { recursive: true });
2520
+ fs.mkdirSync(path.join(dir, "lessons"), { recursive: true });
2521
+ fs.mkdirSync(path.join(dir, "skills"), { recursive: true });
2522
+ fs.mkdirSync(path.join(dir, ".incoming"), { recursive: true });
2523
+ fs.writeFileSync(path.join(dir, "README.md"), "# Main Repository\n", "utf8");
2524
+ fs.writeFileSync(path.join(dir, "AGENTS.md"), "# Existing Hub Rules\n\nKeep me.\n", "utf8");
2525
+ fs.writeFileSync(path.join(dir, "projects", "registry.json"), "[]\n", "utf8");
2526
+ fs.writeFileSync(path.join(dir, "skills", "registry.json"), `${JSON.stringify({
2527
+ schema: "starwork.skill_registry.v0.1",
2528
+ owner: "hub",
2529
+ updated_at: "2026-06-13T00:00:00.000Z",
2530
+ skills: []
2531
+ }, null, 2)}\n`, "utf8");
2532
+ fs.writeFileSync(path.join(blueprintDir, "rules", "hub-boundaries.md"), "项目登记:{{paths.formal_source}}\n跨项目协调:{{paths.business_work_area}}\n", "utf8");
2533
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2534
+ schema: "starwork.upgrade_blueprint.v0.1",
2535
+ generated_by: "starworkDoctor",
2536
+ source: {
2537
+ doctor_schema: "starwork.doctor.result.v0.1",
2538
+ diagnosis: "hub-like-main-repository",
2539
+ core_fit: "high"
2540
+ },
2541
+ base: {
2542
+ workspace_type: "hub",
2543
+ kit: "hub",
2544
+ language: "zh",
2545
+ pack: null
2546
+ },
2547
+ strategy: "preserve-names",
2548
+ paths: {
2549
+ formal_source: "projects/",
2550
+ business_work_area: "projects/coordination/"
2551
+ },
2552
+ core_role_mapping: [
2553
+ { role: "projects", path: "projects/", confidence: "high", reason: "用户确认" },
2554
+ { role: "project_registry", path: "projects/registry.json", confidence: "high", reason: "用户确认" },
2555
+ { role: "coordination", path: "projects/coordination/", confidence: "high", reason: "用户确认" },
2556
+ { role: "incoming", path: ".incoming/", confidence: "high", reason: "用户确认" },
2557
+ { role: "knowledge", path: "knowledge/", confidence: "high", reason: "用户确认" },
2558
+ { role: "identity", path: "identity/", confidence: "high", reason: "用户确认" },
2559
+ { role: "lessons", path: "lessons/", confidence: "high", reason: "用户确认" },
2560
+ { role: "skills", path: "skills/", confidence: "high", reason: "用户确认" }
2561
+ ],
2562
+ actions: [
2563
+ { type: "ensure_dir", path: ".starwork/" },
2564
+ { type: "write_workspace_state" },
2565
+ { type: "copy_kit_missing_files" },
2566
+ { type: "inject_agent_rules", target: "AGENTS.md", from: "rules/hub-boundaries.md", slot: "upgrade.hub_boundaries" }
2567
+ ],
2568
+ preserve: ["projects/", "knowledge/", "identity/", "lessons/", "skills/", ".incoming/"],
2569
+ verification: {
2570
+ run_doctor_after: true,
2571
+ expected_workspace_type: "hub"
2572
+ }
2573
+ }, null, 2)}\n`, "utf8");
2574
+
2575
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
2576
+ const state = readJson(path.join(dir, ".starwork", "workspace.json"));
2577
+ const agents = fs.readFileSync(path.join(dir, "AGENTS.md"), "utf8");
2578
+ const doctor = runDoctor(["--target", dir, "--json"]);
2579
+ const report = JSON.parse(doctor.stdout);
2580
+
2581
+ assert.equal(result.status, 0);
2582
+ assert.equal(state.workspace_type, "hub");
2583
+ assert.equal(state.kit, "hub");
2584
+ assert.deepEqual(state.packs, []);
2585
+ assert.equal(state.paths.formal_source, "projects/");
2586
+ assert.equal(state.paths.business_work_area, "projects/coordination/");
2587
+ assert.equal(fs.existsSync(path.join(dir, "项目")), false);
2588
+ assert.equal(fs.existsSync(path.join(dir, "知识")), false);
2589
+ assert.match(agents, /Keep me/);
2590
+ assert.match(agents, /\.starwork\/rules\/index\.md/);
2591
+ assert.doesNotMatch(agents, /StarWork Rule Slot:/);
2592
+ assert.doesNotMatch(agents, /StarWork Upgrade:/);
2593
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "rules", "upgrade.hub_boundaries.md")), true);
2594
+ assert.equal(doctor.status, 0);
2595
+ assert.equal(report.ok, true);
2596
+ assert.equal(report.skills.registry.path, "skills/registry.json");
2597
+ assert.equal(report.skills.registry.path_source, "upgrade.core_role_mapping");
2598
+ assert(!report.checks.some((check) => check.id === "skills.registry.exists" && check.level === "warn"));
2599
+ assert(report.checks.some((check) => check.id === "upgrade.role_mapping.exists" && check.level === "pass"));
2600
+ });
2601
+
2602
+ test("upgrade refuses existing StarWork workspaces", () => {
2603
+ const dir = tempDir();
2604
+ const blueprintDir = tempDir();
2605
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2606
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2607
+ schema: "starwork.upgrade_blueprint.v0.1",
2608
+ base: {
2609
+ workspace_type: "single-light",
2610
+ kit: "local-starter",
2611
+ language: "zh",
2612
+ pack: "general"
2613
+ },
2614
+ strategy: "preserve-names",
2615
+ paths: {
2616
+ formal_source: "输出/确认成果/",
2617
+ business_work_area: "输出/草稿/"
2618
+ },
2619
+ actions: [
2620
+ { type: "ensure_dir", path: ".starwork/" },
2621
+ { type: "write_workspace_state" }
2622
+ ]
2623
+ }, null, 2)}\n`, "utf8");
2624
+
2625
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
2626
+
2627
+ assert.equal(result.status, 1);
2628
+ assert.match(result.stderr, /已经是 StarWork 工作台/);
2629
+ });
2630
+
2631
+ test("upgrade rejects unsafe blueprint paths", () => {
2632
+ const dir = tempDir();
2633
+ const blueprintDir = tempDir();
2634
+ fs.writeFileSync(path.join(blueprintDir, "upgrade-blueprint.json"), `${JSON.stringify({
2635
+ schema: "starwork.upgrade_blueprint.v0.1",
2636
+ base: {
2637
+ workspace_type: "single-light",
2638
+ kit: "local-starter",
2639
+ language: "zh",
2640
+ pack: "general"
2641
+ },
2642
+ strategy: "preserve-names",
2643
+ paths: {
2644
+ formal_source: "../escape/",
2645
+ business_work_area: "参考资料/"
2646
+ },
2647
+ actions: [
2648
+ { type: "ensure_dir", path: ".starwork/" },
2649
+ { type: "write_workspace_state" }
2650
+ ]
2651
+ }, null, 2)}\n`, "utf8");
2652
+
2653
+ const result = runCommand(["upgrade", "--target", dir, "--blueprint", path.join(blueprintDir, "upgrade-blueprint.json"), "--yes"]);
2654
+
2655
+ assert.equal(result.status, 1);
2656
+ assert.match(result.stderr, /不能跳出工作区/);
2657
+ });
2658
+
375
2659
  test("adapt creates a Claude adapter and records it in workspace state", () => {
376
2660
  const dir = tempDir();
377
2661
  runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
378
2662
 
379
2663
  const result = runCommand(["adapt", "claude", "--target", dir, "--yes"]);
380
2664
  const state = readJson(path.join(dir, ".starwork", "workspace.json"));
2665
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
381
2666
  const claude = fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8");
2667
+ const claudeAdapter = fs.readFileSync(path.join(dir, ".starwork", "drafts", "adapter.claude-code.proposed.md"), "utf8");
2668
+
2669
+ assert.equal(result.status, 0);
2670
+ assert.match(claude, /Claude 工作规则/);
2671
+ assert.match(claudeAdapter, /StarWork Adapter for Claude Code/);
2672
+ assert.match(claudeAdapter, /STARWORK:ADAPTER_ENTRY v0\.1 host=claude-code/);
2673
+ assert.equal(state.adapters["claude-code"].rules_entry, "CLAUDE.md");
2674
+ assert.equal(state.adapters["claude-code"].rules_entry_status, "pending_merge");
2675
+ assert.equal(state.adapters["claude-code"].draft_entry, ".starwork/drafts/adapter.claude-code.proposed.md");
2676
+ assert.equal(adaptersState.schema, "starwork.adapters.state.v0.1");
2677
+ assert.equal(adaptersState.adapters["claude-code"].enabled, false);
2678
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry, "CLAUDE.md");
2679
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry_status, "pending_merge");
2680
+ assert.equal(adaptersState.adapters["claude-code"].draft_entry, ".starwork/drafts/adapter.claude-code.proposed.md");
2681
+ assert.equal(adaptersState.adapters["claude-code"].capabilities["sessions.send_message"], "manual");
2682
+ assert.equal(fs.existsSync(path.join(dir, ".claude", "skills")), true);
2683
+ assert.equal(fs.existsSync(path.join(dir, "CLAUDE.starwork.md")), false);
2684
+ });
2685
+
2686
+ test("adapt does not overwrite user-authored Claude rules that mention AGENTS", () => {
2687
+ const dir = tempDir();
2688
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2689
+ const userRules = "# My Claude Rules\n\n请先阅读 AGENTS.md,但不要覆盖我。\n";
2690
+ fs.writeFileSync(path.join(dir, "CLAUDE.md"), userRules, "utf8");
2691
+
2692
+ const result = runCommand(["adapt", "claude-code", "--target", dir, "--yes"]);
2693
+ const primary = fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8");
2694
+ const draft = fs.readFileSync(path.join(dir, ".starwork", "drafts", "adapter.claude-code.proposed.md"), "utf8");
2695
+ const workspaceState = readJson(path.join(dir, ".starwork", "workspace.json"));
2696
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
2697
+ const doctor = runDoctor(["--target", dir, "--host", "claude-code", "--json"]);
2698
+ const report = JSON.parse(doctor.stdout);
2699
+
2700
+ assert.equal(result.status, 0);
2701
+ assert.equal(primary, userRules);
2702
+ assert.match(draft, /STARWORK:ADAPTER_ENTRY v0\.1 host=claude-code/);
2703
+ assert.equal(fs.existsSync(path.join(dir, "CLAUDE.starwork.md")), false);
2704
+ assert.equal(adaptersState.adapters["claude-code"].enabled, false);
2705
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry, "CLAUDE.md");
2706
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry_status, "pending_merge");
2707
+ assert.equal(adaptersState.adapters["claude-code"].draft_entry, ".starwork/drafts/adapter.claude-code.proposed.md");
2708
+ assert.deepEqual(adaptersState.adapters["claude-code"].generated_entries, [".starwork/drafts/adapter.claude-code.proposed.md"]);
2709
+ assert.equal(workspaceState.adapters["claude-code"].rules_entry, "CLAUDE.md");
2710
+ assert.equal(workspaceState.adapters["claude-code"].rules_entry_status, "pending_merge");
2711
+ assert.equal(report.adapters.checked_hosts[0].rules_entry, "CLAUDE.md");
2712
+ assert.equal(report.adapters.checked_hosts[0].rules_entry_status, "pending_merge");
2713
+ assert.ok(report.checks.some((check) => check.id === "adapter.claude-code.rules.pending_merge" && check.level === "warn" && check.path === ".starwork/drafts/adapter.claude-code.proposed.md"));
2714
+ });
2715
+
2716
+ test("adapt can update StarWork-managed Claude rules", () => {
2717
+ const dir = tempDir();
2718
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2719
+ fs.writeFileSync(path.join(dir, "CLAUDE.md"), "# Old Adapter\n\n<!-- STARWORK:ADAPTER_ENTRY v0.1 host=claude-code -->\n", "utf8");
2720
+
2721
+ const result = runCommand(["adapt", "claude-code", "--target", dir, "--yes"]);
2722
+ const primary = fs.readFileSync(path.join(dir, "CLAUDE.md"), "utf8");
2723
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
382
2724
 
383
2725
  assert.equal(result.status, 0);
384
- assert.match(claude, /StarWork Adapter for Claude Code/);
385
- assert.equal(state.adapters[0].id, "claude");
2726
+ assert.match(primary, /StarWork Adapter for Claude Code/);
2727
+ assert.equal(fs.existsSync(path.join(dir, "CLAUDE.starwork.md")), false);
2728
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry, "CLAUDE.md");
386
2729
  });
387
2730
 
388
2731
  test("adapt creates Cursor rules", () => {
@@ -395,6 +2738,101 @@ test("adapt creates Cursor rules", () => {
395
2738
  assert.equal(result.status, 0);
396
2739
  assert.match(cursorRule, /alwaysApply: true/);
397
2740
  assert.match(cursorRule, /AGENTS\.md/);
2741
+ assert.match(cursorRule, /\.starwork\/skills\.json/);
2742
+ assert.equal(fs.existsSync(path.join(dir, ".cursor", "skills")), true);
2743
+ });
2744
+
2745
+ test("adapter profiles expose valid capabilities without writing files", () => {
2746
+ const dir = tempDir();
2747
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2748
+
2749
+ const result = runCommand(["adapt", "all", "--capabilities", "--json", "--target", dir]);
2750
+ const payload = JSON.parse(result.stdout);
2751
+
2752
+ assert.equal(result.status, 0);
2753
+ assert.equal(payload.schema, "starwork.adapter.capabilities.v0.1");
2754
+ assert.deepEqual(payload.hosts.map((host) => host.host).sort(), ["claude-code", "codex", "cursor", "trae"]);
2755
+ assert.equal(payload.hosts.find((host) => host.host === "cursor").sessions.continue, "manual");
2756
+ assert.equal(payload.hosts.find((host) => host.host === "cursor").sessions.create, "unsupported");
2757
+ assert.equal(payload.hosts.find((host) => host.host === "trae").sessions.send_message, "manual");
2758
+ assert.equal(payload.hosts.find((host) => host.host === "trae").sessions.read, "unsupported");
2759
+ assert.equal(payload.hosts.find((host) => host.host === "trae").sessions.detect_current, "unsupported");
2760
+ assert.equal(payload.hosts.find((host) => host.host === "trae").sessions.list, "unsupported");
2761
+ assert.equal(payload.hosts.find((host) => host.host === "trae").sessions.create, "unsupported");
2762
+ assert.ok(payload.hosts.find((host) => host.host === "cursor").skills.project_mount_dirs.includes(".cursor/skills/"));
2763
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "adapters.json")), false);
2764
+ });
2765
+
2766
+ test("adapt supports multiple host adapter state entries", () => {
2767
+ const dir = tempDir();
2768
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2769
+
2770
+ const claude = runCommand(["adapt", "claude-code", "--target", dir, "--yes"]);
2771
+ const cursor = runCommand(["adapt", "cursor", "--target", dir, "--yes"]);
2772
+ const adaptersState = readJson(path.join(dir, ".starwork", "adapters.json"));
2773
+
2774
+ assert.equal(claude.status, 0);
2775
+ assert.equal(cursor.status, 0);
2776
+ assert.equal(adaptersState.adapters["claude-code"].enabled, false);
2777
+ assert.equal(adaptersState.adapters["claude-code"].rules_entry_status, "pending_merge");
2778
+ assert.equal(adaptersState.adapters["claude-code"].draft_entry, ".starwork/drafts/adapter.claude-code.proposed.md");
2779
+ assert.equal(adaptersState.adapters.cursor.enabled, true);
2780
+ assert.equal(adaptersState.adapters.cursor.rules_entry_status, "active");
2781
+ assert.equal(adaptersState.adapters.cursor.rules_entry, ".cursor/rules/starwork.mdc");
2782
+ });
2783
+
2784
+ test("adapt --check delegates to doctor host checks without writing new adapter state", () => {
2785
+ const dir = tempDir();
2786
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2787
+
2788
+ const check = runCommand(["adapt", "cursor", "--check", "--target", dir, "--json"]);
2789
+ const report = JSON.parse(check.stdout);
2790
+
2791
+ assert.equal(check.status, 0);
2792
+ assert.equal(report.adapters.checked_hosts[0].host, "cursor");
2793
+ assert.equal(report.adapters.checked_hosts[0].enabled, false);
2794
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "adapters.json")), false);
2795
+ });
2796
+
2797
+ test("doctor --host reports enabled adapter checks", () => {
2798
+ const dir = tempDir();
2799
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2800
+ runCommand(["adapt", "cursor", "--target", dir, "--yes"]);
2801
+
2802
+ const doctor = runDoctor(["--target", dir, "--host", "cursor", "--json"]);
2803
+ const report = JSON.parse(doctor.stdout);
2804
+
2805
+ assert.equal(doctor.status, 0);
2806
+ assert.equal(report.adapters.checked_hosts[0].host, "cursor");
2807
+ assert.ok(report.checks.some((check) => check.id === "adapter.cursor.rules.skills_manifest" && check.level === "pass"));
2808
+ assert.ok(report.checks.some((check) => check.id.startsWith("adapter.cursor.skills.mount_dir") && check.level === "pass"));
2809
+ });
2810
+
2811
+ test("doctor --host catches Trae unsafe send_message capability state", () => {
2812
+ const dir = tempDir();
2813
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2814
+ runCommand(["adapt", "trae", "--target", dir, "--yes"]);
2815
+ const statePath = path.join(dir, ".starwork", "adapters.json");
2816
+ const state = readJson(statePath);
2817
+ state.adapters.trae.capabilities["sessions.send_message"] = "supported";
2818
+ fs.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
2819
+
2820
+ const doctor = runDoctor(["--target", dir, "--host", "trae", "--json"]);
2821
+ const report = JSON.parse(doctor.stdout);
2822
+
2823
+ assert.equal(doctor.status, 1);
2824
+ assert.ok(report.checks.some((check) => check.id === "adapter.trae.capabilities.send_message" && check.level === "fail"));
2825
+ });
2826
+
2827
+ test("adapt refuses an unhealthy workspace using the same doctor checks", () => {
2828
+ const dir = tempDir();
2829
+ runInit(["--type", "single-light", "--pack", "general", "--target", dir, "--yes"]);
2830
+ fs.rmSync(path.join(dir, "AGENTS.md"));
2831
+
2832
+ const result = runCommand(["adapt", "claude", "--target", dir, "--yes"]);
2833
+
2834
+ assert.equal(result.status, 1);
2835
+ assert.match(result.stderr, /未通过 doctor 检查/);
398
2836
  });
399
2837
 
400
2838
  test("pack install adds content creator pack to an existing workspace", () => {
@@ -411,7 +2849,10 @@ test("pack install adds content creator pack to an existing workspace", () => {
411
2849
  assert.equal(state.paths.formal_source, "发布记录/");
412
2850
  assert.equal(fs.existsSync(path.join(dir, "发布记录", "README.md")), true);
413
2851
  assert.equal(fs.existsSync(path.join(dir, ".starwork", "packs", "content-creator", "templates", "content-brief.md")), true);
414
- assert.match(agents, /StarWork Pack: content-creator/);
2852
+ assert.match(agents, /\.starwork\/rules\/index\.md/);
2853
+ assert.doesNotMatch(agents, /StarWork Rule Slot:/);
2854
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "rules", "pack.general.overview.md")), true);
2855
+ assert.equal(fs.existsSync(path.join(dir, ".starwork", "rules", "pack.content-creator.overview.md")), true);
415
2856
  assert.equal(doctor.status, 0);
416
2857
  });
417
2858
 
@@ -434,3 +2875,77 @@ test("pack install skips already installed packs", () => {
434
2875
  assert.equal(result.status, 0);
435
2876
  assert.match(result.stdout, /已安装/);
436
2877
  });
2878
+
2879
+ test("audit checks a healthy hub and project satellite", () => {
2880
+ const hub = tempDir();
2881
+ const target = tempDir();
2882
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
2883
+ runCommand(["spawn", "--hub", hub, "--name", "Audit Project", "--id", "audit-project", "--target", target, "--yes"]);
2884
+
2885
+ const result = runCommand(["audit", "--hub", hub, "--json"]);
2886
+ const report = JSON.parse(result.stdout);
2887
+
2888
+ assert.equal(result.status, 0);
2889
+ assert.equal(report.schema, "starwork.audit.result.v0.1");
2890
+ assert.equal(report.ok, true);
2891
+ assert.equal(report.summary.projects_checked, 1);
2892
+ assert.equal(report.projects[0].workspace_type, "project");
2893
+ assert.equal(report.projects[0].kit, "project");
2894
+ assert.equal(report.projects[0].sync_ok, true);
2895
+ assert.equal(Object.hasOwn(report, "next_steps"), false);
2896
+
2897
+ const human = runCommand(["audit", "--hub", hub]);
2898
+ assert.equal(human.status, 0);
2899
+ assert.match(human.stdout, /StarWork 项目中心巡检结果/);
2900
+ assert.match(human.stdout, /项目检查结果/);
2901
+ assert.match(human.stdout, /这个项目中心和已登记项目目前结构完整,可以继续使用/);
2902
+ });
2903
+
2904
+ test("audit reports a missing satellite path", () => {
2905
+ const hub = tempDir();
2906
+ const target = path.join(tempDir(), "missing-project");
2907
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
2908
+ const registryPath = path.join(hub, "项目", "registry.json");
2909
+ const registry = readJson(registryPath);
2910
+ registry.projects.push({ id: "missing-project", name: "Missing Project", path: target, status: "active" });
2911
+ fs.writeFileSync(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
2912
+
2913
+ const result = runCommand(["audit", "--hub", hub, "--json"]);
2914
+ const report = JSON.parse(result.stdout);
2915
+
2916
+ assert.equal(result.status, 1);
2917
+ assert.equal(report.ok, false);
2918
+ assert.equal(report.projects[0].reachable, false);
2919
+ assert(report.projects[0].checks.some((check) => check.id === "satellite.path.exists" && check.level === "fail"));
2920
+ });
2921
+
2922
+ test("repair dry-run and apply can restore satellite handoff state", () => {
2923
+ const hub = tempDir();
2924
+ const target = tempDir();
2925
+ const blueprintDir = tempDir();
2926
+ runInit(["--type", "hub", "--target", hub, "--yes"]);
2927
+ runCommand(["spawn", "--hub", hub, "--name", "Repair Project", "--id", "repair-project", "--target", target, "--yes"]);
2928
+ fs.rmSync(path.join(target, ".starwork", "handoff"), { recursive: true, force: true });
2929
+ const blueprintPath = path.join(blueprintDir, "repair-blueprint.json");
2930
+ fs.writeFileSync(blueprintPath, `${JSON.stringify({
2931
+ schema: "starwork.repair_blueprint.v0.1",
2932
+ generated_by: "starworkAudit",
2933
+ source: { audit_schema: "starwork.audit.result.v0.1", hub },
2934
+ scope: { projects: ["repair-project"] },
2935
+ actions: [
2936
+ { type: "ensure_dir", target: "satellite", project_id: "repair-project", path: ".starwork/handoff/inbox" },
2937
+ { type: "ensure_dir", target: "satellite", project_id: "repair-project", path: ".starwork/handoff/outbox" },
2938
+ { type: "ensure_dir", target: "satellite", project_id: "repair-project", path: ".starwork/handoff/sent" },
2939
+ { type: "ensure_dir", target: "satellite", project_id: "repair-project", path: ".starwork/handoff/archived" },
2940
+ { type: "write_file_if_missing", target: "satellite", project_id: "repair-project", path: ".starwork/handoff/state.json", content: JSON.stringify({ schema: "starwork.handoff.state.v0.1" }, null, 2) + "\n" }
2941
+ ]
2942
+ }, null, 2)}\n`, "utf8");
2943
+
2944
+ const dryRun = runCommand(["repair", "--blueprint", blueprintPath, "--dry-run"]);
2945
+ assert.equal(dryRun.status, 0);
2946
+ assert.equal(fs.existsSync(path.join(target, ".starwork", "handoff", "state.json")), false);
2947
+
2948
+ const apply = runCommand(["repair", "--blueprint", blueprintPath, "--yes"]);
2949
+ assert.equal(apply.status, 0);
2950
+ assert.equal(fs.existsSync(path.join(target, ".starwork", "handoff", "state.json")), true);
2951
+ });