@playcraft/cli 0.0.39 → 0.0.41

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 (121) hide show
  1. package/README.md +66 -3
  2. package/dist/atom-plan/validate-atom-plan.js +298 -0
  3. package/dist/cli-root-help.js +1 -1
  4. package/dist/commands/3d.js +363 -0
  5. package/dist/commands/create.js +337 -0
  6. package/dist/commands/fix-ids.js +17 -3
  7. package/dist/commands/fix-ids.test.js +264 -0
  8. package/dist/commands/image.js +1337 -43
  9. package/dist/commands/login.js +60 -2
  10. package/dist/commands/recommend.js +1 -1
  11. package/dist/commands/remix.js +213 -0
  12. package/dist/commands/skills.js +1379 -0
  13. package/dist/commands/tools-3d.js +473 -0
  14. package/dist/commands/tools-generation.js +454 -0
  15. package/dist/commands/tools-project.js +400 -0
  16. package/dist/commands/tools-research.js +37 -0
  17. package/dist/commands/tools-research.test.js +216 -0
  18. package/dist/commands/tools-utils.js +164 -0
  19. package/dist/commands/tools.js +7 -616
  20. package/dist/config.js +2 -0
  21. package/dist/index.js +20 -2
  22. package/dist/utils/agent-api-client.js +52 -16
  23. package/package.json +9 -3
  24. package/project-template/.claude/agents/designer.md +116 -0
  25. package/project-template/.claude/agents/developer.md +133 -0
  26. package/project-template/.claude/agents/pm.md +164 -0
  27. package/project-template/.claude/agents/refs/README.md +67 -0
  28. package/project-template/.claude/agents/refs/designer-art-style-catalog.md +533 -0
  29. package/project-template/.claude/agents/refs/designer-color-audio-recipes.md +153 -0
  30. package/project-template/.claude/agents/refs/designer-deliverable-spec.md +167 -0
  31. package/project-template/.claude/agents/refs/designer-dimension-axis.md +27 -0
  32. package/project-template/.claude/agents/refs/designer-handoff-v2-checklist.md +68 -0
  33. package/project-template/.claude/agents/refs/designer-master-composite-recipes.md +216 -0
  34. package/project-template/.claude/agents/refs/designer-style-exploration-flow.md +37 -0
  35. package/project-template/.claude/agents/refs/developer-dev-handoff.md +109 -0
  36. package/project-template/.claude/agents/refs/developer-impl-cookbook.md +134 -0
  37. package/project-template/.claude/agents/refs/developer-phase1-flow.md +211 -0
  38. package/project-template/.claude/agents/refs/pm-workflow-detail.md +545 -0
  39. package/project-template/.claude/agents/refs/reviewer-six-dimension-eval.md +286 -0
  40. package/project-template/.claude/agents/refs/ta-3d-flip-recipe.md +85 -0
  41. package/project-template/.claude/agents/refs/ta-atlas-deliverable-standard.md +46 -0
  42. package/project-template/.claude/agents/refs/ta-batch-pipeline-recipes.md +120 -0
  43. package/project-template/.claude/agents/refs/ta-image-generation-detail.md +356 -0
  44. package/project-template/.claude/agents/refs/ta-image-ops-reference.md +495 -0
  45. package/project-template/.claude/agents/refs/ta-pipeline-cookbook.md +699 -0
  46. package/project-template/.claude/agents/refs/ta-tools-reference.md +111 -0
  47. package/project-template/.claude/agents/refs/ta-vfx-preset-catalog.md +365 -0
  48. package/project-template/.claude/agents/reviewer.md +103 -0
  49. package/project-template/.claude/agents/technical-artist.md +111 -0
  50. package/project-template/.claude/hooks/README.md +36 -0
  51. package/project-template/.claude/hooks/validate-atom-plan.mjs +224 -0
  52. package/project-template/.claude/hooks/validate-workflow-stop.mjs +258 -0
  53. package/project-template/.claude/settings.json +32 -0
  54. package/project-template/.claude/settings.local.json +4 -0
  55. package/project-template/.claude/skills/playcraft-ad-psychology/SKILL.md +182 -0
  56. package/project-template/.claude/skills/playcraft-art-style-guide/SKILL.md +123 -0
  57. package/project-template/.claude/skills/playcraft-asset-state-sheet/SKILL.md +141 -0
  58. package/project-template/.claude/skills/playcraft-audio-generation/SKILL.md +280 -0
  59. package/project-template/.claude/skills/playcraft-batch-pipeline/SKILL.md +184 -0
  60. package/project-template/.claude/skills/playcraft-build-optimizer/SKILL.md +306 -0
  61. package/project-template/.claude/skills/playcraft-image-generation/SKILL.md +229 -0
  62. package/project-template/.claude/skills/playcraft-image-generation/reference/build-sprite-sheet.template.mjs +123 -0
  63. package/project-template/.claude/skills/playcraft-image-generation/reference/compare-style.template.mjs +254 -0
  64. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch-sprite.template.mjs +235 -0
  65. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-batch.template.mjs +97 -0
  66. package/project-template/.claude/skills/playcraft-image-generation/reference/gen-edit-variants.template.mjs +118 -0
  67. package/project-template/.claude/skills/playcraft-image-generation/reference/process-batch.template.mjs +137 -0
  68. package/project-template/.claude/skills/playcraft-image-generation/reference/prompt-cookbook.md +397 -0
  69. package/project-template/.claude/skills/playcraft-image-generation/reference/validate-sprite-sheet.template.mjs +296 -0
  70. package/project-template/.claude/skills/playcraft-image-ops/SKILL.md +122 -0
  71. package/project-template/.claude/skills/playcraft-masking/SKILL.md +373 -0
  72. package/project-template/.claude/skills/playcraft-research/SKILL.md +212 -0
  73. package/project-template/.claude/skills/playcraft-sprite-generation/SKILL.md +423 -0
  74. package/project-template/.claude/skills/playcraft-storyboard/SKILL.md +148 -0
  75. package/project-template/.claude/skills/playcraft-style-qa/SKILL.md +270 -0
  76. package/project-template/.claude/skills/playcraft-text-rendering/SKILL.md +236 -0
  77. package/project-template/.claude/skills/playcraft-vfx-animation/SKILL.md +130 -0
  78. package/project-template/.claude/skills/playcraft-workflow/SKILL.md +396 -0
  79. package/project-template/.cursor/hooks.json +17 -0
  80. package/project-template/.cursor/rules/playcraft-orchestrator.mdc +87 -0
  81. package/project-template/.cursor/rules/playcraft-subagent-boundary.mdc +18 -0
  82. package/project-template/CLAUDE.md +240 -0
  83. package/project-template/assets/audio/bgm/.gitkeep +0 -0
  84. package/project-template/assets/audio/sfx/.gitkeep +0 -0
  85. package/project-template/assets/bundles/.gitkeep +0 -0
  86. package/project-template/assets/images/bg/.gitkeep +0 -0
  87. package/project-template/assets/images/reference/.gitkeep +0 -0
  88. package/project-template/assets/images/storyboard/.gitkeep +0 -0
  89. package/project-template/assets/images/tiles/.gitkeep +0 -0
  90. package/project-template/assets/images/ui/.gitkeep +0 -0
  91. package/project-template/assets/images/vfx/.gitkeep +0 -0
  92. package/project-template/assets/models/.gitkeep +0 -0
  93. package/project-template/docs/team/agent-conduct.md +105 -0
  94. package/project-template/docs/team/agent-runtime-matrix.md +62 -0
  95. package/project-template/docs/team/atom-plan-format.md +74 -0
  96. package/project-template/docs/team/collaboration.md +288 -0
  97. package/project-template/docs/team/core-model.md +50 -0
  98. package/project-template/docs/team/platform-capabilities.md +15 -0
  99. package/project-template/docs/team/workflow-changelog.md +51 -0
  100. package/project-template/docs/team/workflow-consistency-checklist.md +128 -0
  101. package/project-template/game/config/.gitkeep +0 -0
  102. package/project-template/game/gameplay/.gitkeep +0 -0
  103. package/project-template/game/scenes/.gitkeep +0 -0
  104. package/project-template/logs/.gitkeep +0 -0
  105. package/project-template/ta-workspace/logs/.gitkeep +0 -0
  106. package/project-template/ta-workspace/scripts/.gitkeep +0 -0
  107. package/project-template/ta-workspace/tmp/.gitkeep +0 -0
  108. package/project-template/templates/atom-plan.template.json +26 -0
  109. package/project-template/templates/atom-plan.template.md +76 -0
  110. package/project-template/templates/design-brief.template.md +195 -0
  111. package/project-template/templates/design-lens-checklist.reference.md +117 -0
  112. package/project-template/templates/design-methodology.md +99 -0
  113. package/project-template/templates/designer-log.template.md +98 -0
  114. package/project-template/templates/developer-log.template.md +140 -0
  115. package/project-template/templates/five-axis-framework.md +186 -0
  116. package/project-template/templates/intent-clarifications.template.md +58 -0
  117. package/project-template/templates/layout-spec.template.md +132 -0
  118. package/project-template/templates/project-state.template.md +219 -0
  119. package/project-template/templates/review-report.template.md +166 -0
  120. package/project-template/templates/style-exploration.template.md +93 -0
  121. package/project-template/templates/ta-log.template.md +205 -0
@@ -0,0 +1,111 @@
1
+ ---
2
+ description: "TA: (1) style-faithful mass production from MC/ASR, (2) 100% assetMapping, (3) production-grade assets for Developer — not gameplay or build."
3
+ allowedTools:
4
+ - "Read"
5
+ - "Grep"
6
+ - "Glob"
7
+ - "Write"
8
+ - "Edit"
9
+ - "Skill"
10
+ - "Bash(ls:*)"
11
+ - "Bash(cat:*)"
12
+ - "Bash(find:*)"
13
+ - "Bash(mkdir:*)"
14
+ - "Bash(cp:*)"
15
+ - "Bash(mv:*)"
16
+ - "Bash(rm:*)"
17
+ - "Bash(playcraft image:*)"
18
+ - "Bash(playcraft 3d:*)"
19
+ - "Bash(playcraft tools:*)"
20
+ - "Bash(playcraft audio:*)"
21
+ - "Bash(playcraft skills:*)"
22
+ - "Bash(node:*)"
23
+ ---
24
+
25
+ > **First Step**: Read `docs/project-state.md` → **`## Agent handoff`** → **## Runtime** → branch; read `refs/` only when Runtime says so. STOP: [STOP sync checklist](../../docs/team/collaboration.md#stop-sync-checklist).
26
+
27
+ # Technical Artist — Playable Ads Production Agent
28
+
29
+ ## Agent Conduct
30
+
31
+ > Full: [docs/team/agent-conduct.md](../../docs/team/agent-conduct.md). **On invoke, follow ## Runtime**; Spec Quick-Check at production entry; orchestrator owns `stage`.
32
+
33
+ ## Runtime (on invoke)
34
+
35
+ 1. Read `docs/project-state.md` → parse `## Agent handoff` YAML.
36
+ 2. If `subagent_stop: true` and `subagent: technical-artist` and `waiting_for: orchestrator` → **Already STOPPED — waiting for orchestrator**.
37
+ 3. Branch:
38
+
39
+ | Condition | This round only | On STOP |
40
+ | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
41
+ | `stage: production`, Wave 1 done | Confirm Production Pipeline Wave 1 = done → Spec Quick-Check → Style Interpretation → bulk → Compliance; ≤5 files → CLI, >5 → `ta-workspace/scripts/` | When all TA atoms `done`: `next_orchestrator_action: "Set stage=integration, invoke @developer"` |
42
+ | `devStatus: blocked_upstream`, routeTo TA | Fix listed paths only → ta-log | `next_orchestrator_action: "Re-invoke @developer"` |
43
+ | `stage: rework`, Action Items routeTo TA | Fix report items at contract paths | `next_orchestrator_action: "Re-invoke @developer after integration"` |
44
+
45
+ 4. **Do not** change `stage`. Update Production Pipeline Wave 2 + handoff + `--- PLAYCRAFT_STOP ---` (`role: technical-artist`).
46
+
47
+ **Track done:** all `assignTo: TA` atoms `done` + Compliance Gate green + atlas/WebP + sidecars per **`refs/ta-atlas-deliverable-standard.md`** + `ta-log.md` manifest complete.
48
+
49
+ ## Mission
50
+
51
+ > Team mission: see [CLAUDE.md](../../CLAUDE.md#mission). Stage 链路与上下游: [CLAUDE.md § Stage Model](../../CLAUDE.md#stage-model).
52
+
53
+ ## Goals
54
+
55
+ **Top 3 (in order):** (1) **Style-faithful mass production** from MC / ASR — **do not redefine art direction**. (2) **100% `assetMapping`** — every path, dims/format per `layout-spec`. (3) **Production-grade handoff** — no placeholders; `ta-log.md` + sprite/atlas params complete.
56
+
57
+ **Success:** Step 0 pass or `spec-gap` filed; Style Interpretation + micro-batch ≥3 before bulk; Compliance green; Developer can verify every path with `playcraft image|audio info`.
58
+
59
+ **Non-goals:** gameplay design, first-level tuning, **file-size optimization** (build stage), MC/storyboard (Designer), `npm run dev` (Developer).
60
+
61
+ ## Step 0 (one-liner)
62
+
63
+ Before generation: complete **`ta-log.md` § Upstream Intake** (read 四件套 + `style-exploration` + `designer-log` per [`agent-conduct.md`](../../docs/team/agent-conduct.md)); confirm **every `assetMapping` row** has explicit size/format + **`logs/designer-log.md`** has composite + Style Intent Notes — else `spec-gap` / `blocked`. Then execute **[`refs/ta-pipeline-cookbook.md`](refs/ta-pipeline-cookbook.md)** (**Step 0a → 0 → 1 → 2**).
64
+
65
+ ## Execute (L2)
66
+
67
+ | Ref | When |
68
+ | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
69
+ | **[`refs/ta-pipeline-cookbook.md`](refs/ta-pipeline-cookbook.md)** | Step 0b–D, pipeline model, ASR extraction rules, stages A–D, **full Compliance Gate**, reference-image matrix, DAG edits |
70
+ | **[`refs/ta-atlas-deliverable-standard.md`](refs/ta-atlas-deliverable-standard.md)** | **WebP** runtime images, atlas `.webp` + `.json`, digit strips, production/conversion commands |
71
+
72
+ **CLI depth:** [`refs/ta-tools-reference.md`](refs/ta-tools-reference.md). **Skills** (as cookbook triggers): `playcraft-masking`, `playcraft-sprite-generation`, `playcraft-vfx-animation`, `playcraft-style-qa`, `playcraft-text-rendering`, `playcraft-image-generation`, `playcraft-image-ops`.
73
+
74
+ ## File Access
75
+
76
+ **Read:** `docs/project-state.md`, `design-brief.md`, `layout-spec.md` (`assetMapping`), `atom-plan.json` (`assignTo: TA`), `atom-plan.md` (TA Skill Context), `style-exploration.md`, `logs/designer-log.md`.
77
+
78
+ **Write:** `logs/ta-log.md`; `atom-plan.json` (`atoms[].status`); `atom-plan.md` (TA Skill Context); `docs/project-state.md` (tracking); `assets/images/`, `assets/audio/`; `ta-workspace/scripts/`.
79
+
80
+ ## Compliance Gate (condensed)
81
+
82
+ | Check | Pass |
83
+ | ----------------- | -------------------------------------------------------------------------------------- |
84
+ | Atlas group table | `layout-spec.md` atlas grouping table exists → else `spec-gap` routeTo PM |
85
+ | Coverage | Every `assetMapping` path exists; dimensions match spec |
86
+ | Format | **WebP** for runtime images / atlases (see atlas ref); **MP3** after TA audio pipeline |
87
+ | Atlases | Per `layout-spec`: grouped assets → one `.webp` + `.json` where required |
88
+ | Params | `ta-log.md` records cols, frameW, frameH, frameCount for every sheet/atlas |
89
+ | Process | Style interpretation table in `ta-log.md`; no open TA questions in ICP |
90
+ | Isolation | No undeclared external deps on deliverables |
91
+
92
+ > TA ignores playable **file-size budget**; optimize at Developer `playcraft build`.
93
+
94
+ ## Developer upstream rework
95
+
96
+ When Developer blocks with `routeTo: TA` (or `devStatus: blocked_upstream`): read `intent-clarifications.md` + blocker table; fix assets at **contract paths**; re-run Compliance for affected items; update `ta-log.md`. Done when Dev self-check can pass — not merely files on disk.
97
+
98
+ ## Identity
99
+
100
+ **Designer = 生 (creation)**; **TA = 产 (production)** — extract → batch → comply. **Working style:** ≤5 files → CLI; >5 → `ta-workspace/scripts/` (Node.js).
101
+
102
+ ## Important Rules
103
+
104
+ 1. **Extract first** — Step 0b references before batch generation.
105
+ 2. **Batch multi-element sets** — `playcraft-sprite-generation` (not one-off loops).
106
+ 3. **`assetMapping` is law** — zero missing or off-path deliveries.
107
+ 4. **Compliance before done** — `playcraft image info` / `audio info` on every deliverable path.
108
+ 5. **WebP + atlas sidecars** per `ta-atlas-deliverable-standard` unless `layout-spec` explicitly excepts.
109
+ 6. **Never modify `game/`** — Developer's domain.
110
+ 7. **Pipeline order** — E→A→B incremental; C₁ earliest; D parallel; update `project-state` / atom-plan after batches.
111
+ 8. **Prompt quality = output quality** — five-section prompt + 9-item review before major generates (see sprite skill).
@@ -0,0 +1,36 @@
1
+ # PlayCraft workflow hooks
2
+
3
+ Shared validator for **Claude Code** (`.claude/settings.json` → `SubagentStop`) and **Cursor** (`.cursor/hooks.json` → `subagentStop`).
4
+
5
+ ## `validate-atom-plan.mjs`
6
+
7
+ When **PM** subagent stops, validates **`docs/atom-plan.json`**:
8
+
9
+ - Every non-empty `skillRef` exists in the skills package (`@playcraft/skills`)
10
+ - `skillRef` must be `*.aigameplay` or `*.aiconfig` (no `playcraft-*`)
11
+ - `skillRef` must appear in `skillsMatch.items` when snapshot is present
12
+
13
+ Runs `playcraft skills validate-atom-plan` when CLI is on `PATH`; otherwise embedded check.
14
+
15
+ On failure: exit `2` — PM must fix atom-plan before STOP.
16
+
17
+ ## `validate-workflow-stop.mjs`
18
+
19
+ When a **Technical Artist** or **Developer** subagent stops, checks `logs/ta-log.md` or `logs/developer-log.md` § **Upstream Intake**:
20
+
21
+ - Every required doc row has Read ✓
22
+ - Every takeaway is filled (no `{{placeholders}}`, min 8 chars)
23
+
24
+ On failure:
25
+
26
+ - **Claude Code**: exit `2` + JSON `{"decision":"block","reason":"..."}` — subagent must fix intake before stopping
27
+ - **Cursor**: exit `2` — blocks subagent stop; stderr shows the same reason
28
+
29
+ Role detection: `agent_type` / `PLAYCRAFT_STOP` footer `role:` in the last message. Other agents (PM, Designer, Reviewer) are skipped.
30
+
31
+ ## Enable
32
+
33
+ - **Claude Code**: project hooks load from `.claude/settings.json` automatically (see `/hooks` menu)
34
+ - **Cursor**: reload after editing `.cursor/hooks.json`; verify in **Hooks** output channel
35
+
36
+ Requires **Node.js** on `PATH` for the hook process.
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PM SubagentStop — 校验 docs/atom-plan.json 中 skillRef 均存在于 skills 库。
4
+ * 优先调用 `playcraft skills validate-atom-plan`;若 CLI 不可用则内嵌最小校验。
5
+ *
6
+ * Exit 0 = pass
7
+ * Exit 2 = block (Claude / Cursor subagent stop)
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+
17
+ const SKILL_REF_EMPTY = new Set(['', '—', '-', '–']);
18
+ const SKILL_REF_SUFFIX_RE = /\.(aigameplay|aiconfig)$/;
19
+
20
+ /** @param {string} raw */
21
+ function parseHookInput(raw) {
22
+ if (!raw.trim()) return {};
23
+ try {
24
+ return JSON.parse(raw);
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ /** @param {string} s */
31
+ function normalizeRole(s) {
32
+ return String(s || '')
33
+ .toLowerCase()
34
+ .replace(/\s+/g, '-')
35
+ .replace(/_/g, '-');
36
+ }
37
+
38
+ /**
39
+ * @param {Record<string, unknown>} input
40
+ * @returns {'pm' | null}
41
+ */
42
+ function detectRole(input) {
43
+ const candidates = [
44
+ input.agent_type,
45
+ input.agent_name,
46
+ input.subagent_name,
47
+ input.subagent_type,
48
+ input.subagent,
49
+ input.role,
50
+ ].filter(Boolean);
51
+
52
+ for (const c of candidates) {
53
+ const n = normalizeRole(String(c));
54
+ if (n === 'pm' || n.includes('project-manager')) return 'pm';
55
+ }
56
+
57
+ const blobs = [input.last_assistant_message, input.last_message, input.message, input.output]
58
+ .filter(Boolean)
59
+ .join('\n');
60
+ if (/---\s*PLAYCRAFT_STOP\s*---/i.test(blobs) && /role:\s*pm\b/i.test(blobs)) return 'pm';
61
+ return null;
62
+ }
63
+
64
+ /** @param {string} projectDir */
65
+ function loadSkillAtomIds(projectDir) {
66
+ const dirs = [];
67
+ const envPaths = process.env.AGENT_SKILLS_PATHS?.split(',').map((p) => p.trim()).filter(Boolean) ?? [];
68
+ dirs.push(...envPaths);
69
+
70
+ const nm = path.join(projectDir, 'node_modules', '@playcraft', 'skills', 'skills');
71
+ if (fs.existsSync(nm)) dirs.push(nm);
72
+
73
+ const ids = new Set();
74
+ for (const dir of dirs) {
75
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) continue;
76
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
77
+ if (!ent.isDirectory()) continue;
78
+ const mp = path.join(dir, ent.name, 'manifest.json');
79
+ if (!fs.existsSync(mp)) continue;
80
+ try {
81
+ const m = JSON.parse(fs.readFileSync(mp, 'utf8'));
82
+ if (m.atomId) ids.add(m.atomId);
83
+ } catch {
84
+ // skip
85
+ }
86
+ }
87
+ }
88
+ return ids;
89
+ }
90
+
91
+ /**
92
+ * @param {string} projectDir
93
+ * @returns {{ errors: string[], warnings: string[] }}
94
+ */
95
+ function validateInline(projectDir) {
96
+ const jsonPath = path.join(projectDir, 'docs', 'atom-plan.json');
97
+ const errors = [];
98
+ const warnings = [];
99
+
100
+ if (!fs.existsSync(jsonPath)) {
101
+ errors.push('缺少 docs/atom-plan.json(从 templates/atom-plan.template.json 创建)');
102
+ return { errors, warnings };
103
+ }
104
+
105
+ let plan;
106
+ try {
107
+ plan = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
108
+ } catch (e) {
109
+ errors.push(`atom-plan.json 解析失败:${e.message}`);
110
+ return { errors, warnings };
111
+ }
112
+
113
+ if (plan.schemaVersion !== 1) {
114
+ errors.push(`schemaVersion 须为 1,当前:${plan.schemaVersion}`);
115
+ }
116
+ if (!Array.isArray(plan.atoms)) {
117
+ errors.push('atoms 须为数组');
118
+ return { errors, warnings };
119
+ }
120
+
121
+ const skillIndex = loadSkillAtomIds(projectDir);
122
+ if (skillIndex.size === 0) {
123
+ errors.push('未找到 skills 库(请 npm install @playcraft/skills 或设置 AGENT_SKILLS_PATHS)');
124
+ return { errors, warnings };
125
+ }
126
+
127
+ const snapshotIds = new Set((plan.skillsMatch?.items ?? []).map((i) => i.atomId).filter(Boolean));
128
+
129
+ for (const atom of plan.atoms) {
130
+ const ref = atom.skillRef == null ? '' : String(atom.skillRef).trim();
131
+ if (!ref || SKILL_REF_EMPTY.has(ref) || ref.startsWith('{{')) continue;
132
+
133
+ if (!SKILL_REF_SUFFIX_RE.test(ref)) {
134
+ errors.push(`${atom.atomId}: skillRef 须为 *.aigameplay 或 *.aiconfig:${ref}`);
135
+ } else if (ref.startsWith('playcraft-')) {
136
+ errors.push(`${atom.atomId}: 禁止 playcraft-* 平台 skill:${ref}`);
137
+ } else if (!skillIndex.has(ref)) {
138
+ errors.push(`${atom.atomId}: skillRef 不在 skills 库:${ref}`);
139
+ } else if (snapshotIds.size > 0 && !snapshotIds.has(ref)) {
140
+ errors.push(`${atom.atomId}: skillRef ${ref} 不在 skillsMatch.items 中`);
141
+ }
142
+
143
+ if (atom.type !== 'GameplayAtom' && atom.type !== 'ConfigAtom') {
144
+ errors.push(`${atom.atomId}: 仅 GameplayAtom/ConfigAtom 可有 skillRef`);
145
+ }
146
+ }
147
+
148
+ if (!(plan.skillsMatch?.items?.length > 0)) {
149
+ warnings.push('skillsMatch.items 为空 — 请先 playcraft skills match --json 并写入 skillsMatch');
150
+ }
151
+
152
+ return { errors, warnings };
153
+ }
154
+
155
+ /** @param {string} projectDir */
156
+ function runCliValidator(projectDir) {
157
+ const r = spawnSync('playcraft', ['skills', 'validate-atom-plan', '--project-dir', projectDir], {
158
+ encoding: 'utf8',
159
+ cwd: projectDir,
160
+ });
161
+ return { status: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
162
+ }
163
+
164
+ function emitBlock(reason) {
165
+ const payload = { decision: 'block', reason };
166
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
167
+ process.stderr.write(`[playcraft-atom-plan] ${reason}\n`);
168
+ process.exit(2);
169
+ }
170
+
171
+ async function readStdin() {
172
+ const chunks = [];
173
+ for await (const chunk of process.stdin) {
174
+ chunks.push(chunk);
175
+ }
176
+ return Buffer.concat(chunks).toString('utf8');
177
+ }
178
+
179
+ export { detectRole, validateInline };
180
+
181
+ async function main() {
182
+ const stdin = await readStdin();
183
+ const input = parseHookInput(stdin);
184
+ const role = detectRole(input);
185
+
186
+ if (role !== 'pm') {
187
+ process.exit(0);
188
+ }
189
+
190
+ const projectDir =
191
+ process.env.CLAUDE_PROJECT_DIR ||
192
+ process.env.CURSOR_PROJECT_DIR ||
193
+ process.cwd();
194
+
195
+ const statePath = path.join(projectDir, 'docs', 'project-state.md');
196
+ if (!fs.existsSync(statePath)) {
197
+ process.exit(0);
198
+ }
199
+
200
+ let cli = runCliValidator(projectDir);
201
+ if (cli.status !== 0 && /ENOENT|not found/i.test(cli.stderr)) {
202
+ const { errors, warnings } = validateInline(projectDir);
203
+ for (const w of warnings) process.stderr.write(`[playcraft-atom-plan] warn: ${w}\n`);
204
+ if (errors.length > 0) {
205
+ emitBlock(`PM STOP blocked — atom-plan skillRef 校验失败:\n- ${errors.join('\n- ')}`);
206
+ }
207
+ process.exit(0);
208
+ }
209
+
210
+ if (cli.status !== 0) {
211
+ const detail = (cli.stderr || cli.stdout).trim() || 'playcraft skills validate-atom-plan failed';
212
+ emitBlock(`PM STOP blocked — ${detail}`);
213
+ }
214
+
215
+ process.exit(0);
216
+ }
217
+
218
+ const isMain = process.argv[1] && path.resolve(process.argv[1]) === __filename;
219
+ if (isMain) {
220
+ main().catch((err) => {
221
+ process.stderr.write(`[playcraft-atom-plan] ${err.message}\n`);
222
+ process.exit(1);
223
+ });
224
+ }
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PlayCraft workflow STOP validator — shared by Claude Code (SubagentStop) and Cursor (subagentStop).
4
+ * Ensures TA / Developer filled logs/<role>-log.md § Upstream Intake before subagent may stop.
5
+ *
6
+ * Exit 0 = pass
7
+ * Exit 2 = block (Claude: decision block; Cursor: block stop)
8
+ * stdout (Claude): {"decision":"block","reason":"..."} on failure
9
+ */
10
+
11
+ import fs from 'node:fs';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+
17
+ const INTAKE_HEADING = '## Upstream Intake';
18
+ const PLACEHOLDER_RE = /\{\{[^}]+\}\}/;
19
+ const MIN_TAKEAWAY_LEN = 8;
20
+
21
+ const TA_REQUIRED_DOCS = [
22
+ 'docs/project-state.md',
23
+ 'docs/design-brief.md',
24
+ 'docs/layout-spec.md',
25
+ 'docs/atom-plan.json',
26
+ 'docs/atom-plan.md',
27
+ 'docs/style-exploration.md',
28
+ 'logs/designer-log.md',
29
+ ];
30
+
31
+ const DEV_REQUIRED_DOCS = [...TA_REQUIRED_DOCS, 'logs/ta-log.md'];
32
+
33
+ const ROLE_LOG = {
34
+ 'technical-artist': { log: 'logs/ta-log.md', docs: TA_REQUIRED_DOCS, label: 'Technical Artist' },
35
+ developer: { log: 'logs/developer-log.md', docs: DEV_REQUIRED_DOCS, label: 'Developer' },
36
+ };
37
+
38
+ /** @param {string} raw */
39
+ function parseHookInput(raw) {
40
+ if (!raw.trim()) return {};
41
+ try {
42
+ return JSON.parse(raw);
43
+ } catch {
44
+ return {};
45
+ }
46
+ }
47
+
48
+ /** @param {string} s */
49
+ function normalizeRole(s) {
50
+ return String(s || '')
51
+ .toLowerCase()
52
+ .replace(/\s+/g, '-')
53
+ .replace(/_/g, '-');
54
+ }
55
+
56
+ /**
57
+ * @param {Record<string, unknown>} input
58
+ * @returns {'technical-artist' | 'developer' | null}
59
+ */
60
+ function detectRole(input) {
61
+ const candidates = [
62
+ input.agent_type,
63
+ input.agent_name,
64
+ input.subagent_name,
65
+ input.subagent_type,
66
+ input.subagent,
67
+ input.role,
68
+ ].filter(Boolean);
69
+
70
+ for (const c of candidates) {
71
+ const n = normalizeRole(String(c));
72
+ if (n.includes('technical-artist') || n === 'ta') return 'technical-artist';
73
+ if (n === 'developer' || n === 'dev') return 'developer';
74
+ }
75
+
76
+ const blobs = [
77
+ input.last_assistant_message,
78
+ input.last_message,
79
+ input.message,
80
+ input.output,
81
+ ]
82
+ .filter((v) => typeof v === 'string')
83
+ .join('\n');
84
+
85
+ const stopMatch = blobs.match(/---\s*PLAYCRAFT_STOP\s*---[\s\S]*?role:\s*([^\s\n]+)/i);
86
+ if (stopMatch) {
87
+ const r = normalizeRole(stopMatch[1]);
88
+ if (r in ROLE_LOG) return /** @type {keyof ROLE_LOG} */ (r);
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ /** @param {string} cell */
95
+ function isReadChecked(cell) {
96
+ const t = cell.trim();
97
+ if (/[✓✔☑]/.test(t)) return true;
98
+ if (/\[x\]/i.test(t)) return true;
99
+ if (/^(yes|done|true|read)$/i.test(t)) return true;
100
+ return false;
101
+ }
102
+
103
+ /**
104
+ * @param {string} content
105
+ * @returns {{ rows: { doc: string, read: string, takeaway: string }[], errors: string[] }}
106
+ */
107
+ function parseUpstreamIntake(content) {
108
+ const errors = [];
109
+ const start = content.indexOf(INTAKE_HEADING);
110
+ if (start === -1) {
111
+ return { rows: [], errors: [`Missing "${INTAKE_HEADING}" section`] };
112
+ }
113
+
114
+ const after = content.slice(start + INTAKE_HEADING.length);
115
+ const nextHeading = after.search(/\n##\s+/);
116
+ const section = nextHeading === -1 ? after : after.slice(0, nextHeading);
117
+
118
+ const rows = [];
119
+ for (const line of section.split('\n')) {
120
+ const trimmed = line.trim();
121
+ if (!trimmed.startsWith('|')) continue;
122
+ if (/^\|\s*---/.test(trimmed)) continue;
123
+ if (/^\|\s*Doc\s*\|/i.test(trimmed)) continue;
124
+
125
+ const parts = trimmed
126
+ .split('|')
127
+ .map((p) => p.trim())
128
+ .filter((_, i, arr) => i > 0 && i < arr.length - 1);
129
+
130
+ if (parts.length < 3) continue;
131
+
132
+ const doc = parts[0].replace(/^`/, '').replace(/`$/, '').trim();
133
+ if (!doc.startsWith('docs/') && !doc.startsWith('logs/')) continue;
134
+
135
+ rows.push({ doc, read: parts[1], takeaway: parts[2] });
136
+ }
137
+
138
+ if (rows.length === 0) {
139
+ errors.push('Upstream Intake table has no data rows');
140
+ }
141
+
142
+ return { rows, errors };
143
+ }
144
+
145
+ /**
146
+ * @param {string} content
147
+ * @param {string[]} requiredDocs
148
+ */
149
+ function validateIntakeContent(content, requiredDocs) {
150
+ const { rows, errors } = parseUpstreamIntake(content);
151
+ const byDoc = new Map(rows.map((r) => [r.doc, r]));
152
+
153
+ for (const doc of requiredDocs) {
154
+ const row = byDoc.get(doc);
155
+ if (!row) {
156
+ errors.push(`Missing row for ${doc}`);
157
+ continue;
158
+ }
159
+ if (!isReadChecked(row.read)) {
160
+ errors.push(`${doc}: mark Read column (✓) after reading`);
161
+ }
162
+ const takeaway = row.takeaway.trim();
163
+ if (!takeaway || PLACEHOLDER_RE.test(takeaway) || takeaway.length < MIN_TAKEAWAY_LEN) {
164
+ errors.push(`${doc}: add a concrete one-line takeaway (≥${MIN_TAKEAWAY_LEN} chars, no {{placeholders}})`);
165
+ }
166
+ }
167
+
168
+ return errors;
169
+ }
170
+
171
+ /**
172
+ * @param {string} projectDir
173
+ * @param {'technical-artist' | 'developer'} role
174
+ */
175
+ function validateRole(projectDir, role) {
176
+ const cfg = ROLE_LOG[role];
177
+ const logPath = path.join(projectDir, cfg.log);
178
+
179
+ if (!fs.existsSync(logPath)) {
180
+ return [
181
+ `${cfg.label}: create ${cfg.log} from templates/${path.basename(cfg.log).replace('.md', '.template.md')} and complete § Upstream Intake before STOP.`,
182
+ ];
183
+ }
184
+
185
+ const content = fs.readFileSync(logPath, 'utf8');
186
+ return validateIntakeContent(content, cfg.docs);
187
+ }
188
+
189
+ /** @param {string} projectDir */
190
+ function resolveProjectDir(projectDir) {
191
+ const statePath = path.join(projectDir, 'docs/project-state.md');
192
+ if (!fs.existsSync(statePath)) {
193
+ return { skip: true, reason: 'no docs/project-state.md' };
194
+ }
195
+ return { skip: false };
196
+ }
197
+
198
+ function emitBlock(reason) {
199
+ const payload = { decision: 'block', reason };
200
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
201
+ process.stderr.write(`[playcraft-workflow-stop] ${reason}\n`);
202
+ process.exit(2);
203
+ }
204
+
205
+ async function readStdin() {
206
+ const chunks = [];
207
+ for await (const chunk of process.stdin) {
208
+ chunks.push(chunk);
209
+ }
210
+ return Buffer.concat(chunks).toString('utf8');
211
+ }
212
+
213
+ export {
214
+ detectRole,
215
+ parseUpstreamIntake,
216
+ validateIntakeContent,
217
+ validateRole,
218
+ TA_REQUIRED_DOCS,
219
+ DEV_REQUIRED_DOCS,
220
+ };
221
+
222
+ async function main() {
223
+ const stdin = await readStdin();
224
+ const input = parseHookInput(stdin);
225
+ const role = detectRole(input);
226
+
227
+ if (!role) {
228
+ process.exit(0);
229
+ }
230
+
231
+ const projectDir =
232
+ process.env.CLAUDE_PROJECT_DIR ||
233
+ process.env.CURSOR_PROJECT_DIR ||
234
+ process.cwd();
235
+
236
+ const { skip } = resolveProjectDir(projectDir);
237
+ if (skip) {
238
+ process.exit(0);
239
+ }
240
+
241
+ const errors = validateRole(projectDir, role);
242
+ if (errors.length > 0) {
243
+ const reason = `${ROLE_LOG[role].label} STOP blocked — fix logs/${role === 'technical-artist' ? 'ta' : 'developer'}-log.md § Upstream Intake:\n- ${errors.join('\n- ')}`;
244
+ emitBlock(reason);
245
+ }
246
+
247
+ process.exit(0);
248
+ }
249
+
250
+ const isMain =
251
+ process.argv[1] && path.resolve(process.argv[1]) === __filename;
252
+
253
+ if (isMain) {
254
+ main().catch((err) => {
255
+ process.stderr.write(`[playcraft-workflow-stop] validator error: ${err.message}\n`);
256
+ process.exit(1);
257
+ });
258
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "hooks": {
3
+ "SubagentStop": [
4
+ {
5
+ "matcher": "pm",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node",
10
+ "args": [
11
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-atom-plan.mjs"
12
+ ],
13
+ "timeout": 30
14
+ }
15
+ ]
16
+ },
17
+ {
18
+ "matcher": "technical-artist|developer",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node",
23
+ "args": [
24
+ "${CLAUDE_PROJECT_DIR}/.claude/hooks/validate-workflow-stop.mjs"
25
+ ],
26
+ "timeout": 30
27
+ }
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "_comment": "PlayCraft project template — local permission hints. Sub-agents PM/Designer must not use AskUserQuestion; gates are orchestrator-only. See docs/team/agent-conduct.md."
4
+ }