@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.
- package/README.md +21 -0
- package/REMIX.md +103 -19
- package/dist/atom-tree/fusion-hints.js +8 -3
- package/dist/atom-tree/fusion-intent.js +62 -0
- package/dist/atom-tree/selection.js +3 -1
- package/dist/commands/create.js +439 -159
- package/dist/commands/image.js +79 -69
- package/dist/commands/levels.js +29 -0
- package/dist/commands/patterns.js +75 -0
- package/dist/commands/remix.js +47 -0
- package/dist/commands/skills.js +48 -17
- package/dist/index.js +13 -6
- package/dist/remix/clone-source.js +208 -0
- package/dist/remix/clone.js +255 -0
- package/dist/scaffold/templates.js +35 -0
- package/dist/utils/agent-api-client.js +10 -3
- package/dist/utils/remix-image-attachments.js +4 -32
- package/dist/utils/sandbox-path.js +20 -0
- package/package.json +3 -3
- package/project-template/.claude/agents/creative-director.md +114 -0
- package/project-template/.claude/agents/developer.md +5 -2
- package/project-template/.claude/agents/game-designer.md +41 -20
- package/project-template/.claude/agents/refs/README.md +8 -0
- package/project-template/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-template/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-template/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-template/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-template/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-template/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-template/.claude/agents/refs/game-designer-gameplay-sufficiency.md +4 -4
- package/project-template/.claude/agents/reviewer.md +11 -0
- package/project-template/.claude/hooks/README.md +12 -12
- package/project-template/.claude/hooks/block-developer-preflight.mjs +49 -13
- package/project-template/.claude/hooks/block-ring1-tasklist.mjs +1 -1
- package/project-template/.claude/hooks/user-prompt.mjs +3 -13
- package/project-template/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-template/.claude/settings.json +2 -2
- package/project-template/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-template/.claude/skills/session-analyzer/scripts/resolve-session.mjs +0 -1
- package/project-template/.claude/snapshot.mjs +0 -1
- package/project-template/.codebuddy/settings.json +1 -1
- package/project-template/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-template/CLAUDE.md +9 -9
- package/project-template/docs/project-state.md +1 -1
- package/project-template/docs/team/agent-conduct.md +18 -18
- package/project-template/docs/team/collaboration.md +19 -7
- package/project-template/docs/team/workflow-changelog.md +15 -0
- package/project-template/fusion-patterns/catalog.json +77 -0
- package/project-template/package.json +2 -0
- package/project-template/skills/_shared/scripts/create-playable.mjs +31 -11
- package/project-template/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-template/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-template/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-template/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-template/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-template/skills/_shared/scripts/slice-set.mjs +29 -2
- package/project-template/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-template/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-template/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-template/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-template/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-template/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-template/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-template/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-template/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-template/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-template/templates/design-brief.template.md +34 -0
- package/project-template/templates/design-methodology.md +1 -2
- package/project-template/templates/design.template.md +65 -0
- package/project-template/templates/developer-log.template.md +12 -1
- package/project-template/templates/five-axis-framework.md +12 -0
- package/project-template/templates/game-designer-creative-process.template.md +99 -0
- package/project-template/templates/level-sketch.template.json +56 -0
- package/project-template/templates/north-star.template.md +43 -0
- package/project-template/templates/project-state.template.md +1 -1
- package/project-template/vite.config.ts +4 -5
- package/project-templates/phaser/.claude/agents/creative-director.md +114 -0
- package/project-templates/phaser/.claude/agents/game-designer.md +41 -20
- package/project-templates/phaser/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-templates/phaser/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-templates/phaser/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-templates/phaser/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-templates/phaser/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-templates/phaser/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-templates/phaser/.claude/agents/reviewer.md +11 -0
- package/project-templates/phaser/.claude/hooks/block-ring1-tasklist.mjs +2 -2
- package/project-templates/phaser/.claude/hooks/user-prompt.mjs +17 -18
- package/project-templates/phaser/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-templates/phaser/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-templates/phaser/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-templates/phaser/CLAUDE.md +88 -54
- package/project-templates/phaser/docs/DESIGN.md +25 -0
- package/project-templates/phaser/docs/project-state.md +1 -1
- package/project-templates/phaser/docs/team/collaboration.md +19 -7
- package/project-templates/phaser/docs/team/workflow-changelog.md +14 -0
- package/project-templates/phaser/fusion-patterns/catalog.json +77 -0
- package/project-templates/phaser/package.json +3 -1
- package/project-templates/phaser/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-templates/phaser/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-templates/phaser/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-templates/phaser/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-templates/phaser/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-templates/phaser/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-templates/phaser/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-templates/phaser/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-templates/phaser/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-templates/phaser/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-templates/phaser/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-templates/phaser/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-templates/phaser/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-templates/phaser/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-templates/phaser/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-templates/phaser/templates/design-brief.template.md +42 -11
- package/project-templates/phaser/templates/design-methodology.md +1 -2
- package/project-templates/phaser/templates/design.template.md +77 -0
- package/project-templates/phaser/templates/five-axis-framework.md +50 -1
- package/project-templates/phaser/templates/game-designer-creative-process.template.md +99 -0
- package/project-templates/phaser/templates/level-sketch.template.json +56 -0
- package/project-templates/phaser/templates/north-star.template.md +43 -0
- package/project-templates/phaser/templates/project-state.template.md +1 -1
- package/project-templates/phaser/vite.config.ts +4 -5
- package/project-templates/playable-template-phaser/.playcraftignore +10 -0
- package/project-templates/playable-template-phaser/CLAUDE.md +178 -0
- package/project-templates/playable-template-phaser/PLAYCRAFT-MIGRATION.md +328 -0
- package/project-templates/playable-template-phaser/README.md +381 -0
- package/project-templates/playable-template-phaser/assets/brain.png +0 -0
- package/project-templates/playable-template-phaser/assets/button.png +0 -0
- package/project-templates/playable-template-phaser/assets/interact.png +0 -0
- package/project-templates/playable-template-phaser/assets/logo.webp +0 -0
- package/project-templates/playable-template-phaser/assets/warn_brain.png +0 -0
- package/project-templates/playable-template-phaser/builds.config.js +12 -0
- package/project-templates/playable-template-phaser/globals.d.ts +2 -0
- package/project-templates/playable-template-phaser/index.html +12 -0
- package/project-templates/playable-template-phaser/package.json +45 -0
- package/project-templates/playable-template-phaser/scripts/base64-to-mp3.js +340 -0
- package/project-templates/playable-template-phaser/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-phaser/src/Game.ts +154 -0
- package/project-templates/playable-template-phaser/src/config/LoadingConfig.ts +40 -0
- package/project-templates/playable-template-phaser/src/config/ThemeConfig.ts +81 -0
- package/project-templates/playable-template-phaser/src/index.css +18 -0
- package/project-templates/playable-template-phaser/src/index.html +9 -0
- package/project-templates/playable-template-phaser/src/index.ts +30 -0
- package/project-templates/playable-template-phaser/src/json5.d.ts +4 -0
- package/project-templates/playable-template-phaser/src/managers/AudioManager.ts +559 -0
- package/project-templates/playable-template-phaser/src/scenes/MainScene.ts +301 -0
- package/project-templates/playable-template-phaser/src/scenes/PreloadScene.ts +256 -0
- package/project-templates/playable-template-phaser/src/theme/README.md +194 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/assets/.gitkeep +24 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/index.ts +27 -0
- package/project-templates/playable-template-phaser/src/theme/blue-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/green-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/index.ts +1 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/index.ts +20 -0
- package/project-templates/playable-template-phaser/src/theme/red-theme/theme.data.json5 +19 -0
- package/project-templates/playable-template-phaser/src/theme/theme.metadata.md +60 -0
- package/project-templates/playable-template-phaser/src/theme/theme.schema.json5 +149 -0
- package/project-templates/playable-template-phaser/src/ui/BrainAgeUI.ts +681 -0
- package/project-templates/playable-template-phaser/src/ui/CountdownUI.ts +565 -0
- package/project-templates/playable-template-phaser/src/ui/PlayButton.ts +51 -0
- package/project-templates/playable-template-phaser/src/ui/ScoreUI.ts +207 -0
- package/project-templates/playable-template-phaser/src/ui/SuccessUI.ts +43 -0
- package/project-templates/playable-template-phaser/src/ui/index.ts +15 -0
- package/project-templates/playable-template-phaser/tsconfig.json +26 -0
- package/project-templates/playable-template-phaser/vite.config.ts +64 -0
- 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
- package/project-templates/playable-template-three/README.md +795 -0
- package/project-templates/playable-template-three/assets/Stork.glb +0 -0
- package/project-templates/playable-template-three/assets/button.png +0 -0
- package/project-templates/playable-template-three/assets/logo.webp +0 -0
- package/project-templates/playable-template-three/build.json +7 -0
- package/project-templates/playable-template-three/builds.config.example.js +151 -0
- package/project-templates/playable-template-three/builds.config.js +158 -0
- package/project-templates/playable-template-three/package.json +36 -0
- package/project-templates/playable-template-three/scripts/convert-to-webp.js +370 -0
- package/project-templates/playable-template-three/scripts/theme-builds.js +514 -0
- package/project-templates/playable-template-three/src/Game.ts +215 -0
- package/project-templates/playable-template-three/src/config/LoadingConfig.ts +27 -0
- package/project-templates/playable-template-three/src/config/ThemeConfig.ts +36 -0
- package/project-templates/playable-template-three/src/global.d.ts +108 -0
- package/project-templates/playable-template-three/src/index.css +18 -0
- package/project-templates/playable-template-three/src/index.html +9 -0
- package/project-templates/playable-template-three/src/index.ts +20 -0
- package/project-templates/playable-template-three/src/managers/AudioManager.ts +538 -0
- package/project-templates/playable-template-three/src/scenes/PreloadScene.ts +305 -0
- package/project-templates/playable-template-three/src/theme/README.md +225 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/assets/.gitkeep +28 -0
- package/project-templates/playable-template-three/src/theme/blue-theme/index.ts +45 -0
- package/project-templates/playable-template-three/src/theme/current.ts +12 -0
- package/project-templates/playable-template-three/src/theme/green-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/green-theme/index.ts +34 -0
- package/project-templates/playable-template-three/src/theme/red-theme/assets/.gitkeep +3 -0
- package/project-templates/playable-template-three/src/theme/red-theme/index.ts +34 -0
- package/project-templates/playable-template-three/tsconfig.json +24 -0
- package/project-templates/threejs/.claude/agents/creative-director.md +114 -0
- package/project-templates/threejs/.claude/agents/game-designer.md +41 -20
- package/project-templates/threejs/.claude/agents/refs/creative-director-guide.md +35 -0
- package/project-templates/threejs/.claude/agents/refs/creative-director-kickoff-guide.md +52 -0
- package/project-templates/threejs/.claude/agents/refs/creative-discovery-guide.md +76 -0
- package/project-templates/threejs/.claude/agents/refs/creative-north-star-examples.md +82 -0
- package/project-templates/threejs/.claude/agents/refs/developer-dev-handoff.md +1 -1
- package/project-templates/threejs/.claude/agents/refs/game-designer-fusion-depth-playbook.md +28 -0
- package/project-templates/threejs/.claude/agents/reviewer.md +11 -0
- package/project-templates/threejs/.claude/hooks/block-ring1-tasklist.mjs +2 -2
- package/project-templates/threejs/.claude/hooks/user-prompt.mjs +17 -18
- package/project-templates/threejs/.claude/hooks/validate-workflow-stop.mjs +209 -12
- package/project-templates/threejs/.claude/skills/playcraft-playable-optimization/SKILL.md +20 -20
- package/project-templates/threejs/.cursor/rules/playcraft-orchestrator.mdc +1 -1
- package/project-templates/threejs/CLAUDE.md +88 -54
- package/project-templates/threejs/docs/DESIGN.md +25 -0
- package/project-templates/threejs/docs/project-state.md +1 -1
- package/project-templates/threejs/docs/team/collaboration.md +19 -7
- package/project-templates/threejs/docs/team/workflow-changelog.md +14 -0
- package/project-templates/threejs/fusion-patterns/catalog.json +77 -0
- package/project-templates/threejs/package.json +3 -1
- package/project-templates/threejs/skills/_shared/scripts/dev-start.mjs +3 -3
- package/project-templates/threejs/skills/_shared/scripts/disclosure-config.mjs +65 -39
- package/project-templates/threejs/skills/_shared/scripts/harness-smoke.mjs +3 -1
- package/project-templates/threejs/skills/_shared/scripts/lib/fusion-intent.mjs +88 -0
- package/project-templates/threejs/skills/_shared/scripts/lib/skill-fidelity.mjs +185 -0
- package/project-templates/threejs/skills/_shared/scripts/state-get.mjs +124 -7
- package/project-templates/threejs/skills/_shared/scripts/state-store.mjs +25 -2
- package/project-templates/threejs/skills/_shared/scripts/validate-level-sketch.mjs +444 -0
- package/project-templates/threejs/skills/_shared/scripts/verify-design-consistency.mjs +728 -0
- package/project-templates/threejs/skills/playable-record/scripts/lib/dev-server.mjs +46 -11
- package/project-templates/threejs/skills/playable-record/scripts/record-playable.mjs +2 -1
- package/project-templates/threejs/skills/playable-record/scripts/verify-contract.mjs +4 -4
- package/project-templates/threejs/skills/playable-record/scripts/verify-firstwin.mjs +3 -3
- package/project-templates/threejs/skills/playable-record/scripts/verify-fusion.mjs +37 -2
- package/project-templates/threejs/skills/playable-record/scripts/verify-slice.mjs +2 -2
- package/project-templates/threejs/templates/design-brief.template.md +42 -11
- package/project-templates/threejs/templates/design-methodology.md +1 -2
- package/project-templates/threejs/templates/design.template.md +77 -0
- package/project-templates/threejs/templates/five-axis-framework.md +50 -1
- package/project-templates/threejs/templates/game-designer-creative-process.template.md +99 -0
- package/project-templates/threejs/templates/level-sketch.template.json +56 -0
- package/project-templates/threejs/templates/north-star.template.md +43 -0
- package/project-templates/threejs/templates/project-state.template.md +1 -1
- package/project-templates/threejs/vite.config.ts +4 -5
- package/project-template/docs/DESIGN.md +0 -65
|
@@ -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
|
+
}
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
* See docs/harness/slice-axis-design.md.
|
|
15
15
|
*/
|
|
16
16
|
import { resolve } from 'node:path';
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
applyStateUpdates,
|
|
19
|
+
flatHandoff,
|
|
20
|
+
getActiveSlice,
|
|
21
|
+
readState,
|
|
22
|
+
updateActiveSlice,
|
|
23
|
+
} from './state-store.mjs';
|
|
18
24
|
import { validateBuildArtifactForSlice } from '../../playable-record/scripts/lib/build-artifact.mjs';
|
|
19
25
|
|
|
20
26
|
function parseArgs(argv) {
|
|
@@ -71,7 +77,28 @@ function main() {
|
|
|
71
77
|
if (slice.verification.length) console.log(` verification: ${slice.verification[slice.verification.length - 1]}`);
|
|
72
78
|
if (slice.routeTo) console.log(` routeTo: ${slice.routeTo.role} — ${slice.routeTo.reason}`);
|
|
73
79
|
if (promotedTo) console.log(` ✓ auto-promoted: activeSliceId now ${promotedTo}`);
|
|
74
|
-
else if (slice.status === 'verified')
|
|
80
|
+
else if (slice.status === 'verified') {
|
|
81
|
+
console.log(' (no further slice to promote — queue drained)');
|
|
82
|
+
const state = readState(root);
|
|
83
|
+
const handoff = flatHandoff(state);
|
|
84
|
+
const iteration = Number.parseInt(String(handoff.iteration || '1'), 10) || 1;
|
|
85
|
+
const ringGate = String(nullishValue(handoff.ringGate) || 'pending').toLowerCase();
|
|
86
|
+
if (iteration === 1 && ringGate === 'pending') {
|
|
87
|
+
applyStateUpdates(root, {
|
|
88
|
+
loop: 'verify',
|
|
89
|
+
next_orchestrator_action: `npm run ring:gate -- --iteration ${iteration}`,
|
|
90
|
+
waiting_for: 'orchestrator',
|
|
91
|
+
subagent_stop: 'false',
|
|
92
|
+
});
|
|
93
|
+
console.log(` ✓ handoff updated: loop=verify, next_orchestrator_action=ring:gate --iteration ${iteration}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nullishValue(value) {
|
|
99
|
+
if (value == null) return null;
|
|
100
|
+
const s = String(value).trim().toLowerCase();
|
|
101
|
+
return s === '' || s === 'null' || s === 'none' ? null : value;
|
|
75
102
|
}
|
|
76
103
|
|
|
77
104
|
main();
|
|
@@ -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 {
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
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({
|
|
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 =
|
|
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
|
}
|