@playcraft/cli 0.0.53 → 0.0.56

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 (242) hide show
  1. package/README.md +21 -0
  2. package/REMIX.md +103 -19
  3. package/dist/atom-tree/fusion-hints.js +8 -3
  4. package/dist/atom-tree/fusion-intent.js +62 -0
  5. package/dist/atom-tree/selection.js +3 -1
  6. package/dist/commands/create.js +439 -159
  7. package/dist/commands/image.js +79 -69
  8. package/dist/commands/levels.js +29 -0
  9. package/dist/commands/patterns.js +75 -0
  10. package/dist/commands/remix.js +47 -0
  11. package/dist/commands/skills.js +48 -17
  12. package/dist/index.js +13 -6
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +255 -0
  15. package/dist/scaffold/templates.js +35 -0
  16. package/dist/utils/agent-api-client.js +10 -3
  17. package/dist/utils/remix-image-attachments.js +4 -32
  18. package/dist/utils/sandbox-path.js +20 -0
  19. package/package.json +3 -3
  20. package/project-template/.claude/agents/creative-director.md +114 -0
  21. package/project-template/.claude/agents/developer.md +5 -2
  22. package/project-template/.claude/agents/game-designer.md +41 -20
  23. package/project-template/.claude/agents/refs/README.md +8 -0
  24. package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
  25. package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  26. package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
  27. package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
  28. package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
  29. package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  30. package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
  31. package/project-template/.claude/agents/reviewer.md +11 -0
  32. package/project-template/.claude/hooks/README.md +12 -12
  33. package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
  34. package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
  35. package/project-template/.claude/hooks/user-prompt.mjs +3 -13
  36. package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
  37. package/project-template/.claude/settings.json +2 -2
  38. package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  39. package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
  40. package/project-template/.claude/snapshot.mjs +0 -1
  41. package/project-template/.codebuddy/settings.json +1 -1
  42. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  43. package/project-template/CLAUDE.md +9 -9
  44. package/project-template/docs/project-state.md +1 -1
  45. package/project-template/docs/team/agent-conduct.md +18 -18
  46. package/project-template/docs/team/collaboration.md +19 -7
  47. package/project-template/docs/team/workflow-changelog.md +15 -0
  48. package/project-template/fusion-patterns/catalog.json +77 -0
  49. package/project-template/package.json +2 -0
  50. package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
  51. package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
  52. package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
  53. package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
  54. package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  55. package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  56. package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
  57. package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
  58. package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
  59. package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  60. package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  61. package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  62. package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
  63. package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
  64. package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  65. package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  66. package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
  67. package/project-template/templates/design-brief.template.md +34 -0
  68. package/project-template/templates/design-methodology.md +1 -2
  69. package/project-template/templates/design.template.md +65 -0
  70. package/project-template/templates/developer-log.template.md +12 -1
  71. package/project-template/templates/five-axis-framework.md +12 -0
  72. package/project-template/templates/game-designer-creative-process.template.md +99 -0
  73. package/project-template/templates/level-sketch.template.json +56 -0
  74. package/project-template/templates/north-star.template.md +43 -0
  75. package/project-template/templates/project-state.template.md +1 -1
  76. package/project-template/vite.config.ts +4 -5
  77. package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
  78. package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
  79. package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
  80. package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  81. package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
  82. package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
  83. package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
  84. package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  85. package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
  86. package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  87. package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
  88. package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
  89. package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  90. package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  91. package/project-templates/phaser/CLAUDE.md +88 -54
  92. package/project-templates/phaser/docs/DESIGN.md +25 -0
  93. package/project-templates/phaser/docs/project-state.md +1 -1
  94. package/project-templates/phaser/docs/team/collaboration.md +19 -7
  95. package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
  96. package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
  97. package/project-templates/phaser/package.json +3 -1
  98. package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
  99. package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
  100. package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
  101. package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  102. package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  103. package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
  104. package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
  105. package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  106. package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  107. package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  108. package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
  109. package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
  110. package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  111. package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  112. package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
  113. package/project-templates/phaser/templates/design-brief.template.md +42 -11
  114. package/project-templates/phaser/templates/design-methodology.md +1 -2
  115. package/project-templates/phaser/templates/design.template.md +77 -0
  116. package/project-templates/phaser/templates/five-axis-framework.md +50 -1
  117. package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
  118. package/project-templates/phaser/templates/level-sketch.template.json +56 -0
  119. package/project-templates/phaser/templates/north-star.template.md +43 -0
  120. package/project-templates/phaser/templates/project-state.template.md +1 -1
  121. package/project-templates/phaser/vite.config.ts +4 -5
  122. package/project-templates/playable-template-phaser/.playcraftignore +10 -0
  123. package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
  124. package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
  125. package/project-templates/playable-template-phaser/README.md +381 -0
  126. package/project-templates/playable-template-phaser/assets/brain.png +0 -0
  127. package/project-templates/playable-template-phaser/assets/button.png +0 -0
  128. package/project-templates/playable-template-phaser/assets/interact.png +0 -0
  129. package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
  130. package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
  131. package/project-templates/playable-template-phaser/builds.config.js +12 -0
  132. package/project-templates/playable-template-phaser/globals.d.ts +2 -0
  133. package/project-templates/playable-template-phaser/index.html +12 -0
  134. package/project-templates/playable-template-phaser/package.json +45 -0
  135. package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
  136. package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
  137. package/project-templates/playable-template-phaser/src/Game.ts +154 -0
  138. package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
  139. package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
  140. package/project-templates/playable-template-phaser/src/index.css +18 -0
  141. package/project-templates/playable-template-phaser/src/index.html +9 -0
  142. package/project-templates/playable-template-phaser/src/index.ts +30 -0
  143. package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
  144. package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
  145. package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
  146. package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
  147. package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
  148. package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
  149. package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
  150. package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
  151. package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
  152. package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
  153. package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
  154. package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
  155. package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
  156. package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
  157. package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
  158. package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
  159. package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
  160. package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
  161. package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
  162. package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
  163. package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
  164. package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
  165. package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
  166. package/project-templates/playable-template-phaser/tsconfig.json +26 -0
  167. package/project-templates/playable-template-phaser/vite.config.ts +64 -0
  168. package/project-templates/playable-template-phaser//346/226/260/346/211/213/345/274/200/345/217/221/346/214/207/345/215/227.md +209 -0
  169. package/project-templates/playable-template-three/README.md +795 -0
  170. package/project-templates/playable-template-three/assets/Stork.glb +0 -0
  171. package/project-templates/playable-template-three/assets/button.png +0 -0
  172. package/project-templates/playable-template-three/assets/logo.webp +0 -0
  173. package/project-templates/playable-template-three/build.json +7 -0
  174. package/project-templates/playable-template-three/builds.config.example.js +151 -0
  175. package/project-templates/playable-template-three/builds.config.js +158 -0
  176. package/project-templates/playable-template-three/package.json +36 -0
  177. package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
  178. package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
  179. package/project-templates/playable-template-three/src/Game.ts +215 -0
  180. package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
  181. package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
  182. package/project-templates/playable-template-three/src/global.d.ts +108 -0
  183. package/project-templates/playable-template-three/src/index.css +18 -0
  184. package/project-templates/playable-template-three/src/index.html +9 -0
  185. package/project-templates/playable-template-three/src/index.ts +20 -0
  186. package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
  187. package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
  188. package/project-templates/playable-template-three/src/theme/README.md +225 -0
  189. package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
  190. package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
  191. package/project-templates/playable-template-three/src/theme/current.ts +12 -0
  192. package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
  193. package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
  194. package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
  195. package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
  196. package/project-templates/playable-template-three/tsconfig.json +24 -0
  197. package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
  198. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  199. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  200. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  201. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  202. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  203. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  204. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  205. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  206. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  207. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  208. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  209. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  210. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  211. package/project-templates/threejs/CLAUDE.md +88 -54
  212. package/project-templates/threejs/docs/DESIGN.md +25 -0
  213. package/project-templates/threejs/docs/project-state.md +1 -1
  214. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  215. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  216. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  217. package/project-templates/threejs/package.json +3 -1
  218. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  219. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  220. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  221. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  222. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  223. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  224. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  225. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  226. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  227. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  228. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  229. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  230. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  231. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  232. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  233. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  234. package/project-templates/threejs/templates/design-methodology.md +1 -2
  235. package/project-templates/threejs/templates/design.template.md +77 -0
  236. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  237. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  238. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  239. package/project-templates/threejs/templates/north-star.template.md +43 -0
  240. package/project-templates/threejs/templates/project-state.template.md +1 -1
  241. package/project-templates/threejs/vite.config.ts +4 -5
  242. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Fusion intent detection — shared by verify:fusion and verify:design-consistency.
3
+ */
4
+ import { existsSync, readFileSync } from 'node:fs';
5
+ import { join } from 'node:path';
6
+
7
+ const FUSION_KEYWORDS = [
8
+ '融合',
9
+ 'fusion',
10
+ 'hybrid',
11
+ 'combine',
12
+ '组合',
13
+ 'mix',
14
+ 'mixed',
15
+ 'plus',
16
+ '+',
17
+ '和',
18
+ '与',
19
+ '加',
20
+ ];
21
+
22
+ const ELIMINATION_KEYWORDS = ['消除', 'elimination', 'eliminate', 'match-3', 'match3', '三消', 'match'];
23
+ const ARROW_PATH_KEYWORDS = ['箭头', 'arrow', 'path', '路径', 'slide-out', 'slide'];
24
+
25
+ /** @param {string[]} intentTags @param {string} [rawIntent] */
26
+ export function detectFusionIntent(intentTags, rawIntent = '') {
27
+ const blob = `${rawIntent} ${(intentTags ?? []).join(' ')}`.toLowerCase();
28
+ if (FUSION_KEYWORDS.some((k) => blob.includes(k.toLowerCase()))) return true;
29
+ const hasElim = ELIMINATION_KEYWORDS.some((k) => blob.includes(k.toLowerCase()));
30
+ const hasArrow = ARROW_PATH_KEYWORDS.some((k) => blob.includes(k.toLowerCase()));
31
+ return hasElim && hasArrow;
32
+ }
33
+
34
+ /**
35
+ * Collect fusion-intent signals from project artifacts.
36
+ * @param {string} root
37
+ * @returns {{ intentTags: string[], rawIntent: string }}
38
+ */
39
+ export function readFusionIntentFromProject(root) {
40
+ const tags = [];
41
+ let raw = '';
42
+
43
+ const configPath = join(root, 'config.json');
44
+ if (existsSync(configPath)) {
45
+ try {
46
+ const cfg = JSON.parse(readFileSync(configPath, 'utf8'));
47
+ if (cfg.prompt) raw += ` ${cfg.prompt}`;
48
+ } catch {
49
+ // ignore
50
+ }
51
+ }
52
+
53
+ const atomTreePath = join(root, 'docs/atom-tree.json');
54
+ if (existsSync(atomTreePath)) {
55
+ try {
56
+ const tree = JSON.parse(readFileSync(atomTreePath, 'utf8'));
57
+ const intent = tree?.meta?.skillsMatch?.intent;
58
+ if (Array.isArray(intent)) tags.push(...intent.map(String));
59
+ else if (typeof intent === 'string') tags.push(intent);
60
+ } catch {
61
+ // ignore
62
+ }
63
+ }
64
+
65
+ const statePath = join(root, 'docs/project-state.json');
66
+ if (existsSync(statePath)) {
67
+ try {
68
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
69
+ if (Array.isArray(state.intentTags)) tags.push(...state.intentTags.map(String));
70
+ if (typeof state.rawIntent === 'string') raw += ` ${state.rawIntent}`;
71
+ } catch {
72
+ // ignore
73
+ }
74
+ }
75
+
76
+ const briefPath = join(root, 'docs/design-brief.md');
77
+ if (existsSync(briefPath)) {
78
+ try {
79
+ const brief = readFileSync(briefPath, 'utf8');
80
+ const userMatch = brief.match(/\[USER\][^\n]*/i);
81
+ if (userMatch) raw += ` ${userMatch[0]}`;
82
+ } catch {
83
+ // ignore
84
+ }
85
+ }
86
+
87
+ return { intentTags: tags, rawIntent: raw.trim() };
88
+ }
@@ -0,0 +1,185 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Mechanism fidelity — DESIGN + north-star vs primary gameplay skill PGS.
4
+ * Authority: SKILL.md + ref/pgs-schema.json (no mechanism-spine.md).
5
+ */
6
+ import { existsSync, readFileSync } from 'node:fs';
7
+ import { join, dirname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+
12
+ /** @param {string} root */
13
+ function readJson(root, rel) {
14
+ const p = join(root, rel);
15
+ if (!existsSync(p)) return null;
16
+ try {
17
+ return JSON.parse(readFileSync(p, 'utf8'));
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ /** @param {string} root @param {object} [handoffContext] */
24
+ export function resolvePrimarySkillId(root, handoffContext = {}) {
25
+ const fromCtx = String(handoffContext.primaryGameplaySkill ?? '').trim();
26
+ if (fromCtx && fromCtx !== 'unset') return fromCtx;
27
+
28
+ const state = readJson(root, 'docs/project-state.json');
29
+ const fromState = String(state?.handoffContext?.primaryGameplaySkill ?? '').trim();
30
+ if (fromState && fromState !== 'unset') return fromState;
31
+
32
+ const tree = readJson(root, 'docs/atom-tree.json');
33
+ const items = tree?.meta?.skillsMatch?.items ?? [];
34
+ const gameplay =
35
+ items.find((i) => (i.role ?? []).some((r) => /gameplay|core|mechanic/i.test(String(r)))) ??
36
+ items.find((i) => String(i.atomId ?? '').includes('.aigameplay')) ??
37
+ items[0];
38
+ return String(gameplay?.atomId ?? '').trim() || null;
39
+ }
40
+
41
+ /** @param {string} fromRoot */
42
+ function monorepoSkillCandidates(fromRoot, skillId) {
43
+ const roots = [
44
+ join(fromRoot, '.claude/skills', skillId),
45
+ join(fromRoot, 'skills', skillId),
46
+ join(fromRoot, 'node_modules/@playcraft/skills/skills', skillId),
47
+ join(fromRoot, '../../../skills/skills', skillId),
48
+ join(fromRoot, '../../../../skills/skills', skillId),
49
+ join(fromRoot, '../../../../../skills/skills', skillId),
50
+ ];
51
+ const envRoot = String(process.env.PLAYCRAFT_SKILLS_ROOT ?? '').trim();
52
+ if (envRoot) roots.unshift(join(envRoot, skillId));
53
+ return roots;
54
+ }
55
+
56
+ /** @param {string} root @param {string} skillId */
57
+ export function resolveSkillPackageDir(root, skillId) {
58
+ if (!skillId) return null;
59
+ for (const dir of monorepoSkillCandidates(root, skillId)) {
60
+ if (existsSync(join(dir, 'SKILL.md'))) return dir;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /** @param {string} text */
66
+ function extractPayoffBlob(text) {
67
+ const section = text.match(/##\s*0[\s\S]*?(?=\n##\s|$)/i)?.[0] ?? text;
68
+ const payoff =
69
+ section.match(/\*\*Payoff image\*\*:\s*(.+)/i)?.[1] ??
70
+ section.match(/\*\*Payoff\*\*:\s*(.+)/i)?.[1] ??
71
+ '';
72
+ return payoff;
73
+ }
74
+
75
+ const ROTATE_CORE_RE =
76
+ /(?:rotate|rotation|rotating|clockwise|counter-?clockwise|90\s*°|90\s*度|顺时针|逆时针|旋转(?:瞄准|箭头|方向)?(?:作为|为核心|为主|核心操作|主要操作))/i;
77
+
78
+ const PGS_REQUIRED_GROUPS = [
79
+ {
80
+ id: 'path',
81
+ label: 'path tap / click',
82
+ re: /path|路径|pathclick|path_click|点击路径|点路径|折线/i,
83
+ },
84
+ {
85
+ id: 'slide',
86
+ label: 'slide out',
87
+ re: /slide\s*out|slide_out|滑出|推出|沿方向滑/i,
88
+ },
89
+ {
90
+ id: 'tray',
91
+ label: 'tray / fly to tray',
92
+ re: /tray|托盘|fly\s*to\s*tray|flytotray|飞入.*托|篮子/i,
93
+ },
94
+ ];
95
+
96
+ const PAYOFF_SLIDE_TRAY_RE = /slide|滑出|推出|tray|托盘|飞入|篮子|burst|爆/i;
97
+ const CASCADE_ONLY_PAYOFF_RE = /(?:board|棋盘).{0,40}(?:cascade|连锁|清盘)|(?:cascade|连锁).{0,40}(?:board|棋盘)/i;
98
+
99
+ /** @param {string} blob @param {string} scope */
100
+ function missingPgsGroups(blob, scope) {
101
+ const missing = [];
102
+ for (const g of PGS_REQUIRED_GROUPS) {
103
+ if (!g.re.test(blob)) missing.push(`${scope}: missing ${g.label}`);
104
+ }
105
+ return missing;
106
+ }
107
+
108
+ /**
109
+ * @param {string} root
110
+ * @param {{ design?: string, northStarMd?: string, handoffContext?: object, payoffOnly?: boolean }} [opts]
111
+ */
112
+ export function validateSkillFidelity(root, opts = {}) {
113
+ const driftReasons = [];
114
+ const primarySkillId = resolvePrimarySkillId(root, opts.handoffContext ?? {});
115
+ if (!primarySkillId) {
116
+ return { pass: true, primarySkillId: null, driftReasons: [], skipped: true, reason: 'no primary skill' };
117
+ }
118
+
119
+ const skillDir = resolveSkillPackageDir(root, primarySkillId);
120
+ if (!skillDir) {
121
+ return {
122
+ pass: false,
123
+ primarySkillId,
124
+ driftReasons: [`skill package not found for ${primarySkillId}`],
125
+ skipped: false,
126
+ };
127
+ }
128
+
129
+ const design = opts.design ?? (existsSync(join(root, 'docs/DESIGN.md')) ? readFileSync(join(root, 'docs/DESIGN.md'), 'utf8') : '');
130
+ const northStarMd =
131
+ opts.northStarMd ??
132
+ (existsSync(join(root, 'docs/north-star.md')) ? readFileSync(join(root, 'docs/north-star.md'), 'utf8') : '');
133
+
134
+ const designBlob = design.toLowerCase();
135
+ const northBlob = northStarMd.toLowerCase();
136
+ const payoffBlob = extractPayoffBlob(northStarMd || design).toLowerCase();
137
+
138
+ driftReasons.push(...missingPgsGroups(designBlob, 'DESIGN'));
139
+ if (!opts.payoffOnly) {
140
+ driftReasons.push(...missingPgsGroups(northBlob, 'north-star'));
141
+ }
142
+
143
+ const coreActionSection =
144
+ design.match(/##\s*7[\s\S]*?(?=\n##\s|$)/i)?.[0] ??
145
+ design.match(/##\s*1[\s\S]*?(?=\n##\s|$)/i)?.[0] ??
146
+ design;
147
+
148
+ if (ROTATE_CORE_RE.test(coreActionSection)) {
149
+ driftReasons.push('DESIGN: rotate / clockwise rotation appears as core player action (PGS drift)');
150
+ }
151
+
152
+ if (payoffBlob) {
153
+ if (!PAYOFF_SLIDE_TRAY_RE.test(payoffBlob)) {
154
+ driftReasons.push('Payoff: must reference slide-out or tray moment (not abstract only)');
155
+ }
156
+ if (CASCADE_ONLY_PAYOFF_RE.test(payoffBlob) && !PAYOFF_SLIDE_TRAY_RE.test(payoffBlob)) {
157
+ driftReasons.push('Payoff: board cascade alone does not satisfy tray/slide payoff for arrow_pick_match PGS');
158
+ }
159
+ } else if (!opts.payoffOnly) {
160
+ driftReasons.push('Payoff: missing Payoff image field for PGS fidelity check');
161
+ }
162
+
163
+ const pgsPath = join(skillDir, 'ref/pgs-schema.json');
164
+ if (existsSync(pgsPath)) {
165
+ try {
166
+ const pgs = JSON.parse(readFileSync(pgsPath, 'utf8'));
167
+ const gameplayType = String(pgs.gameplayType ?? '');
168
+ if (gameplayType === 'arrow_pick_match' && ROTATE_CORE_RE.test(designBlob)) {
169
+ if (!driftReasons.some((d) => d.includes('rotate'))) {
170
+ driftReasons.push('DESIGN: rotate-as-core conflicts with arrow_pick_match PGS (tap path → slide → tray)');
171
+ }
172
+ }
173
+ } catch {
174
+ driftReasons.push(`PGS schema parse failed: ${pgsPath}`);
175
+ }
176
+ }
177
+
178
+ return {
179
+ pass: driftReasons.length === 0,
180
+ primarySkillId,
181
+ driftReasons,
182
+ skipped: false,
183
+ skillDir,
184
+ };
185
+ }
@@ -10,12 +10,24 @@
10
10
  * node skills/_shared/scripts/state-get.mjs --root=. --json iteration loop
11
11
  */
12
12
 
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+
13
16
  import { DOD_LABELS, parseRingDoDFlow, ringDoDKeysForIteration } from './ring-config.mjs';
14
17
  import {
15
18
  disclosureForProjectState,
16
19
  recommendedActionForDispatch,
17
20
  } from './disclosure-config.mjs';
18
- import { flatHandoff, getActiveSlice, readState, serializeRingDoD, serializeArchivedRings, stateExists } from './state-store.mjs';
21
+ import {
22
+ evaluateSliceQueueForRingGate,
23
+ flatHandoff,
24
+ getActiveSlice,
25
+ readState,
26
+ serializeRingDoD,
27
+ serializeArchivedRings,
28
+ stateExists,
29
+ normalizeRequiredContext,
30
+ } from './state-store.mjs';
19
31
 
20
32
  function nullishValue(value) {
21
33
  if (value == null) return null;
@@ -40,11 +52,25 @@ function parseArgs(argv) {
40
52
  return opts;
41
53
  }
42
54
 
55
+ function isRingGatePending(handoff, state) {
56
+ const ringGate = String(nullishValue(handoff.ringGate) || 'pending').toLowerCase();
57
+ if (ringGate === 'passed') return false;
58
+ const verdict = String(state.ringGateRunner?.ringGateVerdict || '').toLowerCase();
59
+ return verdict !== 'pass';
60
+ }
61
+
62
+ function shouldRouteRingGate({ handoff, state }) {
63
+ if (!isRingGatePending(handoff, state)) return false;
64
+ const ss = state.sliceState || {};
65
+ if (ss.activeSliceId && ss.activeSliceId !== 'none') return false;
66
+ const gate = evaluateSliceQueueForRingGate(ss);
67
+ return gate.applicable && gate.ok;
68
+ }
69
+
43
70
  function recommendedNextAction({ handoff, state, disclosure, iteration, dispatchPacket }) {
44
71
  if (dispatchPacket) {
45
72
  return recommendedActionForDispatch(dispatchPacket);
46
73
  }
47
- const explicit = nullishValue(handoff.next_orchestrator_action);
48
74
  const runner = state.ringGateRunner || {};
49
75
  const routeTo = nullishValue(runner.ringGateRouteTo);
50
76
  const canAsk = String(runner.ringGateCanAskUser || '').toLowerCase() === 'yes';
@@ -54,16 +80,24 @@ function recommendedNextAction({ handoff, state, disclosure, iteration, dispatch
54
80
  if (verdict === 'fail' && routeTo) {
55
81
  return `Route ring gate failure to ${routeTo}: ${runner.ringGateFailReason || 'see ring:gate output'}`;
56
82
  }
83
+ if (shouldRouteRingGate({ handoff, state })) {
84
+ return `npm run ring:gate -- --iteration ${iteration}`;
85
+ }
86
+ const explicit = nullishValue(handoff.next_orchestrator_action);
57
87
  if (explicit) return explicit;
58
88
  return disclosure.nextCommands?.[0] ?? null;
59
89
  }
60
90
 
91
+ function requiredContextMustRead(requiredContext) {
92
+ return normalizeRequiredContext(requiredContext).mustRead;
93
+ }
94
+
61
95
  function buildContextPacket({ state, disclosure, iteration, loop, dispatchPacket }) {
62
96
  const context = state.handoffContext || {};
63
97
  const nextRole = context.nextRole || {};
64
98
  const dispatchRequired = dispatchPacket
65
99
  ? [
66
- ...(Array.isArray(dispatchPacket.requiredContext) ? dispatchPacket.requiredContext : []),
100
+ ...requiredContextMustRead(dispatchPacket.requiredContext),
67
101
  ...(Array.isArray(dispatchPacket.allowedFiles) ? dispatchPacket.allowedFiles : []),
68
102
  ]
69
103
  : [];
@@ -111,9 +145,79 @@ function buildContextPacket({ state, disclosure, iteration, loop, dispatchPacket
111
145
  * single dispatch entry — agents read this instead of inferring ring/loop scope.
112
146
  * Returns null when no slice is active (harness falls back to ring:loop disclosure).
113
147
  */
114
- function buildDispatchPacket(state) {
148
+ function readAtomTreeDisclosure(root) {
149
+ const jsonPath = path.join(path.resolve(root), 'docs', 'atom-tree.json');
150
+ if (!fs.existsSync(jsonPath)) {
151
+ return {
152
+ mustRead: [],
153
+ atomTreeFocus: null,
154
+ selectedAtoms: [],
155
+ };
156
+ }
157
+ try {
158
+ const tree = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
159
+ const selectedAtoms = Array.isArray(tree?.meta?.selectedAtoms)
160
+ ? tree.meta.selectedAtoms
161
+ .map((atom) => (typeof atom === 'string' ? atom.trim() : ''))
162
+ .filter((atom) => atom && !atom.startsWith('{{') && !atom.startsWith('playcraft-'))
163
+ : [];
164
+ return {
165
+ mustRead: [
166
+ 'docs/atom-tree.json',
167
+ ...selectedAtoms.map((atom) => `.claude/skills/${atom}/SKILL.md`),
168
+ ],
169
+ atomTreeFocus: {
170
+ readFields: [
171
+ 'meta.engineDecision',
172
+ 'meta.selectedAtoms',
173
+ 'meta.skillsMatch.mediaGroups',
174
+ 'meta.rings',
175
+ ],
176
+ selectedAtomsPolicy: 'read-all-selected-skills-before-game-edit',
177
+ selectedAtoms,
178
+ sliceSkillRefsPolicy: 'skillRefs narrow implementation focus; selectedAtoms remain mandatory preflight context',
179
+ },
180
+ selectedAtoms,
181
+ };
182
+ } catch {
183
+ return {
184
+ mustRead: ['docs/atom-tree.json'],
185
+ atomTreeFocus: {
186
+ readFields: ['meta.engineDecision', 'meta.selectedAtoms', 'meta.skillsMatch.mediaGroups', 'meta.rings'],
187
+ selectedAtomsPolicy: 'read-all-selected-skills-before-game-edit',
188
+ selectedAtoms: [],
189
+ parseError: true,
190
+ },
191
+ selectedAtoms: [],
192
+ };
193
+ }
194
+ }
195
+
196
+ function mergeRequiredContext(sliceContext, atomTreeDisclosure) {
197
+ const normalized = normalizeRequiredContext(sliceContext);
198
+ const atomTreeFocus = {
199
+ ...(atomTreeDisclosure.atomTreeFocus || {}),
200
+ ...(normalized.atomTreeFocus || {}),
201
+ };
202
+ return {
203
+ ...normalized,
204
+ mustRead: [
205
+ ...new Set([
206
+ ...atomTreeDisclosure.mustRead,
207
+ ...normalized.mustRead,
208
+ ]),
209
+ ],
210
+ atomTreeFocus: Object.keys(atomTreeFocus).length ? atomTreeFocus : null,
211
+ };
212
+ }
213
+
214
+ function buildDispatchPacket(state, { root = process.cwd() } = {}) {
115
215
  const active = getActiveSlice(state);
116
216
  if (!active) return null;
217
+ const atomTreeDisclosure = active.role === 'developer'
218
+ ? readAtomTreeDisclosure(root)
219
+ : { mustRead: [], atomTreeFocus: null, selectedAtoms: [] };
220
+ const requiredContext = mergeRequiredContext(active.requiredContext, atomTreeDisclosure);
117
221
  const queue = (state.sliceState.slices || []).map((s) => ({
118
222
  sliceId: s.sliceId,
119
223
  status: s.status,
@@ -125,11 +229,15 @@ function buildDispatchPacket(state) {
125
229
  doWhat: active.doWhat,
126
230
  flow: active.flow,
127
231
  hypothesis: active.hypothesis,
128
- requiredContext: active.requiredContext,
232
+ requiredContext,
129
233
  allowedFiles: active.allowedFiles,
130
234
  forbiddenTasks: active.forbiddenTasks,
131
235
  acceptance: active.acceptance,
132
236
  evidenceTier: active.evidenceTier,
237
+ skillRefs: active.skillRefs,
238
+ skillUse: active.skillUse,
239
+ fromScratchReason: active.fromScratchReason,
240
+ selectedAtoms: atomTreeDisclosure.selectedAtoms,
133
241
  verifier: active.verifier,
134
242
  fallbackRoute: active.fallbackRoute,
135
243
  status: active.status,
@@ -149,8 +257,16 @@ function main() {
149
257
  const handoff = flatHandoff(state);
150
258
  const iteration = Number.parseInt(String(handoff.iteration || '1'), 10) || 1;
151
259
  const loop = nullishValue(handoff.loop);
152
- const dispatchPacket = buildDispatchPacket(state);
153
- const disclosure = disclosureForProjectState({ iteration, loop, handoff, dispatchPacket });
260
+ const dispatchPacket = buildDispatchPacket(state, { root: opts.root });
261
+ const disclosure = disclosureForProjectState({
262
+ iteration,
263
+ loop,
264
+ handoff,
265
+ dispatchPacket,
266
+ sliceState: state.sliceState,
267
+ projectRoot: opts.root,
268
+ handoffContext: state.handoffContext || {},
269
+ });
154
270
  const ringDoD = parseRingDoDFlow(serializeRingDoD(state.ringDoD));
155
271
  const contextPacket = buildContextPacket({ state, disclosure, iteration, loop, dispatchPacket });
156
272
  const dev = state.devPreview || {};
@@ -170,6 +286,7 @@ function main() {
170
286
  nextRole: disclosure.nextRole,
171
287
  artifactRefs: disclosure.artifactRefs,
172
288
  requiredContextFields: disclosure.requiredContextFields,
289
+ referenceReads: disclosure.referenceReads || [],
173
290
  evidencePaths: disclosure.evidencePaths,
174
291
  contextPacket,
175
292
  currentRingVerifiers: ringDoDKeysForIteration(iteration, opts.root).map((key) => ({
@@ -143,6 +143,7 @@ export function defaultState() {
143
143
  assetContractRefs: [],
144
144
  },
145
145
  openQuestions: [],
146
+ primaryGameplaySkill: 'unset',
146
147
  },
147
148
  ringArtifacts: {},
148
149
  archivedRings: {},
@@ -183,6 +184,26 @@ export function defaultState() {
183
184
  };
184
185
  }
185
186
 
187
+ /** Default disclosure shape for one slice. */
188
+ export function defaultRequiredContext() {
189
+ return {
190
+ mustRead: [],
191
+ atomTreeFocus: null,
192
+ };
193
+ }
194
+
195
+ export function normalizeRequiredContext(value) {
196
+ if (Array.isArray(value) || typeof value === 'string') {
197
+ return { ...defaultRequiredContext(), mustRead: parseStringList(value) };
198
+ }
199
+ if (!isPlainObject(value)) return defaultRequiredContext();
200
+ return {
201
+ ...value,
202
+ mustRead: parseStringList(value.mustRead),
203
+ recommendedRead: parseStringList(value.recommendedRead),
204
+ };
205
+ }
206
+
186
207
  /** A single slice's full default shape (dispatchPacket fields + runtime state). */
187
208
  export function defaultSlice() {
188
209
  return {
@@ -191,7 +212,7 @@ export function defaultSlice() {
191
212
  doWhat: '',
192
213
  flow: 'implement',
193
214
  hypothesis: '',
194
- requiredContext: [],
215
+ requiredContext: defaultRequiredContext(),
195
216
  allowedFiles: [],
196
217
  forbiddenTasks: [],
197
218
  acceptance: [],
@@ -291,7 +312,7 @@ export function normalizeSliceState(sliceState) {
291
312
  .filter((s) => isPlainObject(s) && String(s.sliceId || '').trim())
292
313
  .map((s) => {
293
314
  const slice = { ...defaultSlice(), ...s };
294
- slice.requiredContext = parseStringList(slice.requiredContext);
315
+ slice.requiredContext = normalizeRequiredContext(slice.requiredContext);
295
316
  slice.allowedFiles = parseStringList(slice.allowedFiles);
296
317
  slice.forbiddenTasks = parseStringList(slice.forbiddenTasks);
297
318
  slice.acceptance = parseStringList(slice.acceptance);
@@ -625,6 +646,8 @@ export function applyStateUpdates(root, updates) {
625
646
  state.handoffContext.sharedDecisions.semanticPlaceholders = parseStringList(value);
626
647
  } else if (key === 'contextAssetContractRefs') {
627
648
  state.handoffContext.sharedDecisions.assetContractRefs = parseStringList(value);
649
+ } else if (key === 'contextPrimaryGameplaySkill' || key === 'primaryGameplaySkill') {
650
+ state.handoffContext.primaryGameplaySkill = value;
628
651
  } else {
629
652
  state.handoff[key] = value;
630
653
  }