@playcraft/cli 0.0.55 → 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 (167) 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 +299 -85
  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 +9 -5
  13. package/dist/remix/clone-source.js +208 -0
  14. package/dist/remix/clone.js +255 -0
  15. package/dist/scaffold/templates.js +1 -1
  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/threejs/.claude/agents/creative-director.md +114 -0
  123. package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
  124. package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
  125. package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
  126. package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
  127. package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
  128. package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
  129. package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
  130. package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
  131. package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
  132. package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
  133. package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
  134. package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
  135. package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
  136. package/project-templates/threejs/CLAUDE.md +88 -54
  137. package/project-templates/threejs/docs/DESIGN.md +25 -0
  138. package/project-templates/threejs/docs/project-state.md +1 -1
  139. package/project-templates/threejs/docs/team/collaboration.md +19 -7
  140. package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
  141. package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
  142. package/project-templates/threejs/package.json +3 -1
  143. package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
  144. package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
  145. package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
  146. package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
  147. package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
  148. package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
  149. package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
  150. package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
  151. package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
  152. package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
  153. package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
  154. package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
  155. package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
  156. package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
  157. package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
  158. package/project-templates/threejs/templates/design-brief.template.md +42 -11
  159. package/project-templates/threejs/templates/design-methodology.md +1 -2
  160. package/project-templates/threejs/templates/design.template.md +77 -0
  161. package/project-templates/threejs/templates/five-axis-framework.md +50 -1
  162. package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
  163. package/project-templates/threejs/templates/level-sketch.template.json +56 -0
  164. package/project-templates/threejs/templates/north-star.template.md +43 -0
  165. package/project-templates/threejs/templates/project-state.template.md +1 -1
  166. package/project-templates/threejs/vite.config.ts +4 -5
  167. package/project-template/docs/DESIGN.md +0 -65
@@ -0,0 +1,77 @@
1
+ {
2
+ "schemaVersion": 2,
3
+ "patterns": [
4
+ {
5
+ "id": "arrow_pick_match_fusion",
6
+ "intentTags": ["arrow", "elimination", "fusion", "match-3", "消除", "箭头", "融合"],
7
+ "title": "Arrow slide-out + tray match-3",
8
+ "mechanics": "Paths slide into a tray; match-3 clears tray slots. Combines path puzzle dependency with elimination payoff.",
9
+ "playerFantasy": "Tap an arrow path — it slides out. Fill the tray strategically; match-3 clears space before overflow.",
10
+ "decisionMoment": "Choosing which blocked path to unlock first vs filling the tray for a combo clear.",
11
+ "skillRef": "arrow_pick_match_rules.aigameplay",
12
+ "recommendedProfile": "standard",
13
+ "depthHint": "不要写「箭头+三消」就停;写清:棋盘哪一块先解锁、托盘峰值如何迫使颜色顺序、blocked 误点教什么。",
14
+ "antiPatternRef": "8×8 but ≤4 paths or all-free opening — no puzzle identity"
15
+ },
16
+ {
17
+ "id": "path_elimination_puzzle",
18
+ "intentTags": ["arrow", "path", "puzzle", "elimination", "箭头", "路径"],
19
+ "title": "Pure path elimination puzzle",
20
+ "mechanics": "Click arrow heads; paths slide out when unobstructed. Order matters — blocked paths teach dependency.",
21
+ "playerFantasy": "Read arrow directions, find the unlock sequence, clear the board.",
22
+ "decisionMoment": "First tap on a blocked path — shake/highlight feedback — then finding the correct unlock order.",
23
+ "skillRef": "path_elimination_rules.aigameplay",
24
+ "recommendedProfile": "standard",
25
+ "depthHint": "用折线密度与依赖层数定难度,固定 8×8;至少 2 层解锁波次,禁止开局全部 FREE。",
26
+ "antiPatternRef": "All paths FREE at start — zero decision"
27
+ },
28
+ {
29
+ "id": "match3_board",
30
+ "intentTags": ["match-3", "match3", "swap", "三消", "消除"],
31
+ "title": "Classic match-3 board",
32
+ "mechanics": "Swap adjacent tiles; match-3 removes; gravity refills.",
33
+ "playerFantasy": "Swap to create matches; cascades and combos build excitement.",
34
+ "decisionMoment": "Choosing between an obvious match vs setting up a cascade.",
35
+ "skillRef": "match3_core.aigameplay",
36
+ "recommendedProfile": "standard",
37
+ "depthHint": "若与 path 融合,说明主手势是 swap 还是 tap-path,避免异基底双主手势。",
38
+ "antiPatternRef": "Auto-clear board with no player swap"
39
+ },
40
+ {
41
+ "id": "board_phase_unlock",
42
+ "intentTags": ["phase", "unlock", "mask", "分区", "解锁", "board", "fusion"],
43
+ "title": "Board phase unlock (same-grid mask)",
44
+ "mechanics": "Same 8×8 board; masked regions unlock after match clears or path milestones. New paths appear in unlocked zones — board changes via mask, not resize.",
45
+ "playerFantasy": "Clear the first zone to reveal more puzzle — the board grows in complexity without shrinking cells.",
46
+ "decisionMoment": "Spend a tray clear to unlock the next zone vs hoarding colors for a safer combo.",
47
+ "skillRef": "arrow_pick_match_rules.aigameplay",
48
+ "recommendedProfile": "standard",
49
+ "depthHint": "写清每 phase 解锁条件与新增 path 条数;难度靠追加折线而非缩小棋盘。",
50
+ "antiPatternRef": "Resize board to 4×4 for difficulty — use path count instead"
51
+ },
52
+ {
53
+ "id": "match3_tray_puzzle_hybrid",
54
+ "intentTags": ["tray", "match-3", "puzzle", "hybrid", "托盘", "三消", "拼图"],
55
+ "title": "Tray-only match-3 + board path puzzle",
56
+ "mechanics": "Primary gesture: path tap on board. Match-3 happens only inside the tray. Board side is pure dependency puzzle — no swap on grid.",
57
+ "playerFantasy": "Solve the arrow maze; the tray is your pressure valve — match-3 is the relief, not the main puzzle.",
58
+ "decisionMoment": "Push a risky color into a nearly-full tray hoping the next two paths complete the triple.",
59
+ "skillRef": "arrow_pick_match_rules.aigameplay",
60
+ "recommendedProfile": "ring1_min",
61
+ "depthHint": "声明三消仅在托盘;棋盘侧禁止 swap。写托盘峰值曲线与颜色推出顺序。",
62
+ "antiPatternRef": "Board swap match-3 + path tap without interaction contract"
63
+ },
64
+ {
65
+ "id": "match3_board_phase",
66
+ "intentTags": ["match3", "phase", "path", "hybrid", "sequential", "双区"],
67
+ "title": "Match-3 main + path lock zone (high risk)",
68
+ "mechanics": "Primary zone: swap match-3. Secondary zone: path puzzle locked until match milestone. C-Sequential — document pseudo-fusion reducer.",
69
+ "playerFantasy": "Clear tiles to unlock the arrow zone — two systems, one win condition.",
70
+ "decisionMoment": "Finish the match-3 objective vs peeking at the locked path zone.",
71
+ "skillRef": "match3_core.aigameplay",
72
+ "recommendedProfile": "hard",
73
+ "depthHint": "Ring 2+ only.必须写 pseudo-fusion 与单 reducer,避免双主手势抢输入。",
74
+ "antiPatternRef": "Two equal primary gestures with no documented interaction contract"
75
+ }
76
+ ]
77
+ }
@@ -54,6 +54,8 @@
54
54
  "placeholder:gen-audio": "python3 skills/_shared/scripts/gen-placeholder-audio.py",
55
55
  "verify:placeholders": "node skills/_shared/scripts/verify-placeholders.mjs --root=.",
56
56
  "verify:fusion": "node skills/playable-record/scripts/verify-fusion.mjs --root=. --use-dev",
57
+ "verify:design": "node skills/_shared/scripts/verify-design-consistency.mjs --root=.",
58
+ "levels:validate": "node skills/_shared/scripts/validate-level-sketch.mjs --root=.",
57
59
  "verify:visual-ring1": "node skills/playable-record/scripts/verify-visual-ring1.mjs --root=. --use-dev",
58
60
  "verify:visual-ring2": "node skills/playable-record/scripts/verify-visual-ring.mjs --root=. --ring=2 --use-dev",
59
61
  "verify:visual-ring3": "node skills/playable-record/scripts/verify-visual-ring.mjs --root=. --ring=3 --use-dev",
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { access, cp, mkdir, readFile, writeFile } from 'node:fs/promises';
11
- import { basename, dirname, join, resolve } from 'node:path';
11
+ import { basename, dirname, join, resolve, sep } from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { spawnSync } from 'node:child_process';
14
14
 
@@ -24,17 +24,27 @@ const COPY_EXCLUDES = new Set([
24
24
  'dist',
25
25
  'package-lock.json',
26
26
  'playable.ui.profile.yaml',
27
- 'playground',
28
- 'recordings',
29
- 'validate',
30
27
  ]);
31
28
 
29
+ /**
30
+ * 仅在「模板根直接子目录」时才排除的运行时产物目录。
31
+ * 这些名字也会出现在 skills 内部(如 skills/playable-focus/templates/playground 与
32
+ * skills/playable-focus/templates/focus-lab),故只在顶层排除,避免误伤 skills 的模板素材
33
+ * (否则 bootstrap-playground.mjs 会因找不到 registry.template.json 而 ENOENT 退出)。
34
+ * 对齐 create.ts 的 shouldCopyTemplateEntry 行为。
35
+ */
36
+ const TEMPLATE_ROOTLEVEL_EXCLUDES = new Set(['playground', 'recordings', 'validate']);
37
+
32
38
  function shouldCopy(src) {
33
39
  const name = basename(src);
34
40
  if (COPY_EXCLUDES.has(name)) return false;
35
- if (src.includes(`${join('ta-workspace', 'logs')}`)) return name === '.gitkeep';
36
- if (src.includes(`${join('ta-workspace', 'tmp')}`)) return name === '.gitkeep';
37
- if (src.includes(`${join('focus-lab')}`)) return name === 'focus-lab' || name === '.gitkeep';
41
+ // 运行时产物目录只排除模板根这一层;skills 内的同名目录是模板素材,必须复制
42
+ if (TEMPLATE_ROOTLEVEL_EXCLUDES.has(name) && dirname(src) === TEMPLATE_ROOT) return false;
43
+ // 顶层 focus-lab/ 是运行时 lab 目录,只保留 .gitkeep 占位;
44
+ // 但 skills 内部(如 skills/playable-focus/templates/focus-lab)是模板素材,需正常复制
45
+ if (src.startsWith(join(TEMPLATE_ROOT, 'focus-lab') + sep)) return name === '.gitkeep';
46
+ if (src.includes(`${sep}ta-workspace${sep}logs${sep}`)) return name === '.gitkeep';
47
+ if (src.includes(`${sep}ta-workspace${sep}tmp${sep}`)) return name === '.gitkeep';
38
48
  return true;
39
49
  }
40
50
 
@@ -107,9 +117,10 @@ async function main() {
107
117
  });
108
118
  ensureResult = { status: r.status, stdout: r.stdout, stderr: r.stderr };
109
119
  }
120
+ const ensureFailed = opts.ensure && ensureResult?.status !== 0;
110
121
 
111
122
  const result = {
112
- ok: true,
123
+ ok: !ensureFailed,
113
124
  projectRoot: destRoot,
114
125
  templateRoot: TEMPLATE_ROOT,
115
126
  skillsRoot: SKILLS_ROOT,
@@ -125,13 +136,22 @@ async function main() {
125
136
  console.log(JSON.stringify(result, null, 2));
126
137
  } else {
127
138
  console.log(`[create-playable] created ${destRoot}`);
128
- if (ensureResult?.status === 0) {
139
+ if (!opts.ensure) {
140
+ // 跳过 ensure,无事可报
141
+ } else if (!ensureFailed) {
129
142
  console.log('[create-playable] ensure-project OK');
130
- } else if (opts.ensure) {
131
- console.warn('[create-playable] ensure-project had issues — run npm run ensure:project');
143
+ } else {
144
+ // ensure-project 失败时透出子进程真实输出(如 ENOENT、缺失模板),
145
+ // 否则只会看到无意义的 "node exit 1",无法定位漏文件根因。
146
+ console.error('[create-playable] ensure-project failed:');
147
+ if (ensureResult?.stderr) console.error(ensureResult.stderr.trimEnd());
148
+ if (ensureResult?.stdout) console.error(ensureResult.stdout.trimEnd());
149
+ console.error(' → 项目模板可能不完整,运行 npm run ensure:project 排查');
132
150
  }
133
151
  for (const step of result.next) console.log(` → ${step}`);
134
152
  }
153
+
154
+ if (ensureFailed) process.exit(1);
135
155
  }
136
156
 
137
157
  main().catch((err) => {
@@ -3,7 +3,7 @@
3
3
  * Start (or stop) the persistent preview dev server and record the real devUrl.
4
4
  *
5
5
  * This removes the LLM from port handling entirely (docs/harness/experiment-lessons.md §9.2):
6
- * Vite runs with strictPort:false, so when 3000 is taken it auto-increments; this
6
+ * Vite runs with strictPort:false, so when 5173 is taken it auto-increments; this
7
7
  * script parses Vite's actual `Local:` URL from the log, appends `?debug=1`, and
8
8
  * writes it back to project-state via the same layer `state:set` uses. The agent
9
9
  * never reads or records a port by hand.
@@ -20,11 +20,11 @@ import { openSync, closeSync, existsSync, readFileSync, writeFileSync, rmSync }
20
20
  import { spawn } from 'node:child_process';
21
21
  import { join, resolve } from 'node:path';
22
22
  import { applyHandoffUpdates, resolveProjectStatePath } from './state-handoff.mjs';
23
- import { parseLocalUrl, waitForHttp } from '../../playable-record/scripts/lib/dev-server.mjs';
23
+ import { parseLocalUrl, waitForHttp, DEFAULT_DEV_URL } from '../../playable-record/scripts/lib/dev-server.mjs';
24
24
 
25
25
  const PID_FILE = '.playcraft-dev.pid';
26
26
  const LOG_FILE = '.playcraft-dev.log';
27
- const PREFERRED_URL = 'http://127.0.0.1:3000/';
27
+ const PREFERRED_URL = DEFAULT_DEV_URL;
28
28
 
29
29
  function parseArgs(argv) {
30
30
  const opts = { root: process.cwd(), stop: false, timeout: 30000 };
@@ -7,6 +7,11 @@
7
7
  * for that surface.
8
8
  */
9
9
 
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { evaluateSliceQueueForRingGate } from './state-store.mjs';
13
+ import { resolvePrimarySkillId } from './lib/skill-fidelity.mjs';
14
+
10
15
  const COMMON_REQUIRED = ['CLAUDE.md'];
11
16
 
12
17
  export const FUTURE_RING_FILES = [
@@ -22,51 +27,43 @@ export const FUTURE_RING_FILES = [
22
27
  const DISCLOSURE_BY_RING_LOOP = {
23
28
  '1:clarify': {
24
29
  currentAction:
25
- 'Orchestrator: invoke @game-designer for Ring 1 clarify — design-brief discovery + DESIGN seed + playcraft skills match compact refs + minimal developer slice queue (not layout/style/CTA/full plan).',
26
- requiredFiles: [...COMMON_REQUIRED, 'docs/DESIGN.md'],
27
- deferredFiles: FUTURE_RING_FILES.filter((f) => f !== 'docs/design-brief.md'),
30
+ 'Orchestrator: invoke @game-designer with only the user\'s original intent. Do not add design decisions, engine hints, platform constraints, or workflow steps — designer reads its own role card.',
31
+ requiredFiles: [...COMMON_REQUIRED],
32
+ deferredFiles: FUTURE_RING_FILES,
28
33
  nextRole: 'game-designer',
29
- artifactRefs: ['docs/DESIGN.md', 'docs/design-brief.md', 'docs/atom-tree.json', 'sliceState'],
30
- requiredContextFields: ['ring.goal', 'lastRole.summary', 'sharedDecisions.coreAction', 'sharedDecisions.semanticPlaceholders'],
34
+ artifactRefs: [],
35
+ requiredContextFields: [],
31
36
  allowedTasks: [
32
- 'Invoke @game-designer for ring 1 clarify',
33
- 'Game Designer: design-brief discovery + DESIGN seed + playcraft skills match + sliceState queue',
34
- 'Game Designer: run playcraft skills match and write meta.skillsMatch + compact selectedAtoms',
35
- 'Record fromScratchReason when core mechanic is novel',
36
- 'Define first-level win strategy and semantic placeholders (Game Designer subagent)',
37
+ 'Invoke @game-designer with user intent only',
37
38
  ],
38
39
  forbiddenTasks: [
39
40
  'Do not write docs/DESIGN.md as orchestrator',
40
41
  'Do not implement game/ as orchestrator during clarify',
41
- 'Do not require layout-spec or full implementation-plan in Ring 1',
42
- 'Do not build a full atom graph in Ring 1 clarify — compact skillsMatch refs only',
43
- 'Do not design CTA polish or production assets in Ring 1',
44
- 'Do not claim skillRefs implement core mechanic without skills read',
45
- 'Do not run standalone record:mp4 as a Ring 1 blocker',
46
- 'Do not run verify:ui as Ring 1 blockers',
42
+ 'Do not add design decisions, engine hints, or workflow steps to the designer dispatch',
43
+ 'Do not describe what designer should produce — its role card defines outputs',
44
+ 'Do not list designer CLI commands — its role card defines the construction chain',
47
45
  ],
48
46
  nextCommands: [
49
- 'Invoke @game-designer — ring 1 clarify (skills match + design-brief + DESIGN seed + slice queue)',
47
+ 'Invoke @game-designer — pass only the user\'s original intent, nothing more',
50
48
  ],
51
49
  },
52
50
  '1:select': {
53
- currentAction: 'Orchestrator: after Game Designer STOP, spawn a temporary ref-flow atom-selection subagent for compact Ring 1 atom-tree selection.',
51
+ currentAction: 'Orchestrator: after Game Designer STOP, spawn a temporary ref-flow atom-selection subagent.',
54
52
  requiredFiles: [...COMMON_REQUIRED, 'docs/DESIGN.md'],
55
53
  deferredFiles: FUTURE_RING_FILES,
56
54
  nextRole: 'temporary-ref-flow',
57
55
  artifactRefs: ['docs/DESIGN.md', 'docs/atom-tree.json'],
58
- requiredContextFields: ['lastRole.summary', 'lastRole.decisions', 'sharedDecisions.coreAction', 'sharedDecisions.semanticPlaceholders'],
56
+ requiredContextFields: ['lastRole.summary', 'sharedDecisions.coreAction'],
59
57
  allowedTasks: [
60
- 'Spawn temporary ref-flow atom-selection subagent for ring 1 atom-tree selection',
61
- 'Pass DESIGN.md gameplay keywords and constraints to the temporary selector',
58
+ 'Spawn temporary ref-flow atom-selection subagent',
62
59
  ],
63
60
  forbiddenTasks: [
64
61
  'Do not select atoms inline as orchestrator',
65
62
  'Do not run init-template before atom-selection STOP',
66
- 'Do not implement gameplay logic as orchestrator',
63
+ 'Do not add engine or gameplay-trunk hints to the selector dispatch',
67
64
  ],
68
65
  nextCommands: [
69
- 'Spawn temporary ref-flow atom-selection subagent — ring 1 atom-tree selection (engine + compact gameplay trunk)',
66
+ 'Spawn temporary ref-flow atom-selection subagent — read docs/DESIGN.md and follow skill-selector-guide.md',
70
67
  ],
71
68
  },
72
69
  '1:scaffold': {
@@ -95,29 +92,21 @@ const DISCLOSURE_BY_RING_LOOP = {
95
92
  ],
96
93
  },
97
94
  '1:build': {
98
- currentAction: 'Orchestrator: invoke @developer for Ring 1 build. Developer: implement playable spine to first-level win.',
95
+ currentAction: 'Orchestrator: invoke @developer for Ring 1 build.',
99
96
  requiredFiles: [...COMMON_REQUIRED, 'docs/DESIGN.md', 'docs/atom-tree.json', 'docs/scaffold-manifest.md', 'game/'],
100
97
  deferredFiles: FUTURE_RING_FILES.filter((file) => file !== 'logs/review-report.md'),
101
98
  nextRole: 'developer',
102
99
  artifactRefs: ['docs/DESIGN.md', 'docs/atom-tree.json', 'logs/developer-log.md', 'game/'],
103
- requiredContextFields: ['lastRole.summary', 'lastRole.decisions', 'nextRole.mustKnow', 'sharedDecisions.engine', 'sharedDecisions.coreAction', 'sharedDecisions.semanticPlaceholders'],
100
+ requiredContextFields: ['lastRole.summary', 'sharedDecisions.engine', 'sharedDecisions.coreAction', 'sharedDecisions.semanticPlaceholders'],
104
101
  allowedTasks: [
105
102
  'Invoke @developer for ring 1 build',
106
- 'Apply engine scaffold if needed (orchestrator only, before developer)',
107
- 'Implement core gameplay loop (Developer subagent)',
108
- 'Wire window.__playable runtime contract (Developer subagent)',
109
- 'Make autoPlay follow player-reachable actions (Developer subagent)',
110
- 'Record devUrl when ready (Developer subagent)',
111
103
  ],
112
104
  forbiddenTasks: [
113
105
  'Do not implement game/ as orchestrator when @developer should own build',
114
- 'Do not polish HUD',
115
- 'Do not implement CTA fallback as a Ring 1 blocker',
116
- 'Do not run full MP4 as a Ring 1 blocker',
117
- 'Do not produce production assets',
106
+ 'Do not add implementation hints to the developer dispatch — developer reads its dispatchPacket + role card',
118
107
  ],
119
108
  nextCommands: [
120
- 'Invoke @developer — ring 1 build (placeholder art + core loop)',
109
+ 'Invoke @developer — pass the active slice from sliceState, nothing more',
121
110
  'npm run placeholder:gen',
122
111
  'npm run verify:contract',
123
112
  'npm run verify:lifecycle',
@@ -349,6 +338,21 @@ function normalizeIteration(iteration) {
349
338
  return Number.isFinite(n) && n >= 1 && n <= 4 ? n : 1;
350
339
  }
351
340
 
341
+ /** Compatibility export for STOP hook mechanism-phase detection. */
342
+ export function isMechanismLocked(projectRoot, handoffContext = {}) {
343
+ const skill = resolvePrimarySkillId(projectRoot, handoffContext);
344
+ if (!skill) return false;
345
+ const treePath = join(projectRoot, 'docs/atom-tree.json');
346
+ if (!existsSync(treePath)) return false;
347
+ try {
348
+ const tree = JSON.parse(readFileSync(treePath, 'utf8'));
349
+ const items = tree?.meta?.skillsMatch?.items ?? [];
350
+ return items.length > 0;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+
352
356
  function resolveDisclosureKey({ iteration = 1, loop = 'clarify', handoff = {} } = {}) {
353
357
  const ring = normalizeIteration(iteration);
354
358
  const normalizedLoop = normalizeLoop(loop);
@@ -407,6 +411,11 @@ export function disclosureForSlice(dispatchPacket, { iteration = 1 } = {}) {
407
411
  const status = String(dispatchPacket.status || 'pending').trim();
408
412
  const allowedFiles = Array.isArray(dispatchPacket.allowedFiles) ? dispatchPacket.allowedFiles : [];
409
413
  const packetForbidden = Array.isArray(dispatchPacket.forbiddenTasks) ? dispatchPacket.forbiddenTasks : [];
414
+ const packetRequired = Array.isArray(dispatchPacket.requiredContext?.mustRead)
415
+ ? dispatchPacket.requiredContext.mustRead
416
+ : Array.isArray(dispatchPacket.requiredContext)
417
+ ? dispatchPacket.requiredContext
418
+ : [];
410
419
 
411
420
  const currentAction =
412
421
  `Orchestrator: invoke @${role} for slice "${sliceId}" — ${doWhat} (flow: ${flow}). ` +
@@ -429,7 +438,7 @@ export function disclosureForSlice(dispatchPacket, { iteration = 1 } = {}) {
429
438
  ring,
430
439
  loop: flow,
431
440
  currentAction,
432
- currentRingRequiredFiles: [...COMMON_REQUIRED, ...allowedFiles],
441
+ currentRingRequiredFiles: [...new Set([...COMMON_REQUIRED, ...packetRequired, ...allowedFiles])],
433
442
  deferredFiles: FUTURE_RING_FILES.filter((f) => !allowedFiles.includes(f)),
434
443
  allowedTasks: [
435
444
  `Invoke @${role} with dispatchPacket only (sliceId=${sliceId}, flow=${flow})`,
@@ -443,7 +452,7 @@ export function disclosureForSlice(dispatchPacket, { iteration = 1 } = {}) {
443
452
  ],
444
453
  nextCommands,
445
454
  nextRole: role,
446
- artifactRefs: allowedFiles.length ? allowedFiles : ['dispatchPacket'],
455
+ artifactRefs: [...new Set(['dispatchPacket', ...packetRequired, ...allowedFiles])],
447
456
  requiredContextFields: [
448
457
  'dispatchPacket.sliceId',
449
458
  'dispatchPacket.role',
@@ -455,11 +464,28 @@ export function disclosureForSlice(dispatchPacket, { iteration = 1 } = {}) {
455
464
  };
456
465
  }
457
466
 
458
- /** Unified entry: active dispatchPacket wins; otherwise legacy ring:loop disclosure. */
459
- export function disclosureForProjectState({ iteration = 1, loop = 'clarify', handoff = {}, dispatchPacket = null } = {}) {
467
+ /** Unified entry: active dispatchPacket wins; drained queue promotes to verify + ring:gate. */
468
+ export function disclosureForProjectState({
469
+ iteration = 1,
470
+ loop = 'clarify',
471
+ handoff = {},
472
+ dispatchPacket = null,
473
+ sliceState = null,
474
+ } = {}) {
460
475
  if (dispatchPacket) {
461
476
  return disclosureForSlice(dispatchPacket, { iteration });
462
477
  }
478
+ const ss = sliceState || {};
479
+ const gate = evaluateSliceQueueForRingGate(ss);
480
+ const ringGatePending = String(handoff.ringGate || 'pending').toLowerCase() !== 'passed';
481
+ if (
482
+ ss.activeSliceId === 'none'
483
+ && gate.applicable
484
+ && gate.ok
485
+ && ringGatePending
486
+ ) {
487
+ return disclosureForState({ iteration, loop: 'verify', handoff });
488
+ }
463
489
  return disclosureForState({ iteration, loop, handoff });
464
490
  }
465
491
 
@@ -25,6 +25,8 @@ import { createRequire } from 'node:module';
25
25
  import { dirname, join, resolve } from 'node:path';
26
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
 
28
+ import { DEFAULT_DEV_URL } from '../../playable-record/scripts/lib/dev-server.mjs';
29
+
28
30
  const require = createRequire(import.meta.url);
29
31
  const __dirname = dirname(fileURLToPath(import.meta.url));
30
32
  const SKILLS_ROOT = resolve(__dirname, '../..');
@@ -171,7 +173,7 @@ async function checkContractLive(projectRoot) {
171
173
  const { runContractProbe } = await import(pathToFileURL(probePath).href);
172
174
  const res = await runContractProbe({
173
175
  root: projectRoot,
174
- url: isVite ? 'http://localhost:3000/' : null,
176
+ url: isVite ? DEFAULT_DEV_URL : null,
175
177
  useDev: isVite,
176
178
  port: 8080,
177
179
  scene: 'MainScene',
@@ -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
+ }