summer-engine 2.7.0 → 2.8.1

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 (153) hide show
  1. package/.claude-plugin/plugin.json +2 -1
  2. package/.codex-plugin/plugin.json +3 -2
  3. package/.cursor-plugin/plugin.json +2 -1
  4. package/.opencode/INSTALL.md +1 -1
  5. package/.opencode/plugins/summer.js +1 -1
  6. package/AGENTS.md +4 -4
  7. package/CHANGELOG.md +28 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +58 -21
  10. package/assets/autopilot/README.md +51 -0
  11. package/assets/autopilot/autopilot.gd +222 -0
  12. package/assets/autopilot/probe_base.gd +262 -0
  13. package/assets/autopilot/run.sh +78 -0
  14. package/commands/gameskill.md +11 -2
  15. package/dist/bin/summer.js +8 -0
  16. package/dist/commands/config.d.ts +2 -0
  17. package/dist/commands/config.js +53 -0
  18. package/dist/commands/create.d.ts +1 -0
  19. package/dist/commands/create.js +38 -6
  20. package/dist/commands/list.js +1 -1
  21. package/dist/commands/login.d.ts +19 -0
  22. package/dist/commands/login.js +136 -37
  23. package/dist/commands/logout.js +3 -16
  24. package/dist/commands/logs.d.ts +2 -0
  25. package/dist/commands/logs.js +19 -0
  26. package/dist/commands/mcp.js +7 -2
  27. package/dist/commands/open.js +1 -1
  28. package/dist/commands/publish.d.ts +2 -0
  29. package/dist/commands/publish.js +20 -0
  30. package/dist/commands/releases.d.ts +2 -0
  31. package/dist/commands/releases.js +20 -0
  32. package/dist/commands/run.js +1 -1
  33. package/dist/lib/api-client.d.ts +27 -8
  34. package/dist/lib/api-client.js +65 -14
  35. package/dist/lib/auth.d.ts +40 -12
  36. package/dist/lib/auth.js +173 -37
  37. package/dist/lib/cloud/engine-bridge.d.ts +17 -4
  38. package/dist/lib/cloud/engine-bridge.js +32 -6
  39. package/dist/lib/config.d.ts +20 -0
  40. package/dist/lib/config.js +142 -0
  41. package/dist/lib/creator.d.ts +93 -0
  42. package/dist/lib/creator.js +393 -0
  43. package/dist/lib/engine.d.ts +35 -2
  44. package/dist/lib/engine.js +240 -6
  45. package/dist/lib/public-surface-paths.d.ts +13 -0
  46. package/dist/lib/public-surface-paths.js +42 -0
  47. package/dist/lib/skills-registry.d.ts +8 -0
  48. package/dist/lib/skills-registry.js +9 -0
  49. package/dist/lib/store.d.ts +14 -0
  50. package/dist/lib/store.js +145 -0
  51. package/dist/lib/summer-compatibility.d.ts +6 -0
  52. package/dist/lib/summer-compatibility.js +22 -0
  53. package/dist/mcp/server.d.ts +8 -1
  54. package/dist/mcp/server.js +25 -4
  55. package/dist/mcp/tools/creator-tools.d.ts +2 -0
  56. package/dist/mcp/tools/creator-tools.js +105 -0
  57. package/dist/mcp/tools/debug-tools.d.ts +12 -0
  58. package/dist/mcp/tools/debug-tools.js +100 -1
  59. package/dist/mcp/tools/generate-tools.js +16 -6
  60. package/dist/mcp/tools/project-tools.js +131 -16
  61. package/dist/mcp/tools/scene-tools.js +166 -66
  62. package/dist/mcp/tools/visual-tools.js +32 -8
  63. package/dist/mcp/tools/with-engine.js +51 -19
  64. package/hooks/pre-commit-doctor.sh +0 -0
  65. package/hooks/run-hook.cmd +0 -0
  66. package/hooks/session-start.sh +5 -5
  67. package/package.json +3 -1
  68. package/references/collaborative-protocol.md +50 -0
  69. package/references/gd-style.md +121 -0
  70. package/references/godot-version.md +51 -0
  71. package/references/mcp-tools-reference.md +199 -0
  72. package/references/public-surface-language.md +30 -0
  73. package/references/summer-folder.md +90 -0
  74. package/references/template-registry.md +73 -0
  75. package/skills/2d-assets/character-portrait/SKILL.md +17 -12
  76. package/skills/2d-assets/concept-art/SKILL.md +10 -9
  77. package/skills/2d-assets/pixel-art/SKILL.md +36 -20
  78. package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
  79. package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
  80. package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
  81. package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
  82. package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
  83. package/skills/3d-assets/character-model/SKILL.md +34 -19
  84. package/skills/3d-assets/environment-kit/SKILL.md +11 -5
  85. package/skills/3d-assets/organic-model/SKILL.md +8 -5
  86. package/skills/3d-assets/prop-model/SKILL.md +9 -3
  87. package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
  88. package/skills/README.md +1 -1
  89. package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
  90. package/skills/animation/animation-tree/SKILL.md +39 -15
  91. package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
  92. package/skills/animation/generate-motion/SKILL.md +47 -15
  93. package/skills/animation/procedural-animation/SKILL.md +31 -13
  94. package/skills/animation/retarget/SKILL.md +127 -79
  95. package/skills/audio/adaptive-music/SKILL.md +24 -20
  96. package/skills/audio/ambient-bed/SKILL.md +22 -16
  97. package/skills/audio/audio-direction/SKILL.md +15 -10
  98. package/skills/audio/music-track/SKILL.md +18 -12
  99. package/skills/audio/sound-effect/SKILL.md +17 -12
  100. package/skills/audio/voice-line/SKILL.md +42 -42
  101. package/skills/character-controllers/fps-controller/SKILL.md +26 -16
  102. package/skills/debugging/debug/SKILL.md +52 -20
  103. package/skills/deployment/export-and-ship/SKILL.md +105 -38
  104. package/skills/deployment/remote-deploy/SKILL.md +56 -23
  105. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
  106. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
  107. package/skills/level-design/design-level/SKILL.md +22 -8
  108. package/skills/level-design/scene-to-level/SKILL.md +13 -7
  109. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
  110. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
  111. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
  112. package/skills/performance/tune-performance/SKILL.md +51 -32
  113. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
  114. package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
  115. package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
  116. package/skills/scene-and-project/make-game/SKILL.md +2 -1
  117. package/skills/scene-and-project/play/SKILL.md +10 -4
  118. package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
  119. package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
  120. package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
  121. package/skills/video/animated-loop/SKILL.md +63 -39
  122. package/skills/video/cinematic-cutscene/SKILL.md +66 -30
  123. package/skills/video/trailer-shot/SKILL.md +30 -21
  124. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
  125. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
  126. package/skills/visual-effects/game-feel/SKILL.md +51 -33
  127. package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
  128. package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
  129. package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
  130. package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
  131. package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
  132. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
  133. package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
  134. package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
  135. package/skills/workflow/brainstorming/SKILL.md +1 -1
  136. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  137. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  138. package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
  139. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
  140. package/skills/workflow/gameskill/SKILL.md +11 -2
  141. package/skills/workflow/headless-scripting/SKILL.md +407 -0
  142. package/skills/workflow/investigating-bugs/SKILL.md +4 -2
  143. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  144. package/skills/workflow/mcpupdate/SKILL.md +3 -3
  145. package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
  146. package/skills/workflow/skill-create/SKILL.md +7 -7
  147. package/skills/workflow/skill-test/SKILL.md +3 -3
  148. package/skills/workflow/using-summer/SKILL.md +12 -2
  149. package/skills/workflow/verification-before-completion/SKILL.md +2 -0
  150. package/skills/workflow/writing-plans/SKILL.md +7 -7
  151. package/skills/workflow/writing-skills/SKILL.md +7 -5
  152. package/skills/workflow/writing-skills/render-graphs.js +0 -0
  153. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
@@ -1,7 +1,20 @@
1
1
  import { z } from "zod";
2
- import { withEngine } from "./with-engine.js";
3
- import { readFile } from "fs/promises";
4
- import { join } from "path";
2
+ import { withEngine, extractOpError } from "./with-engine.js";
3
+ // Engine ops that MUST be dispatched as their own single-op request. Mirrors
4
+ // _summer_requires_single_async_dispatch (local_api_server.cpp, engine 0.5.60+):
5
+ // the engine rejects any multi-op batch containing one of these WHOLESALE —
6
+ // nothing in the batch executes, and the batch fails with per-op
7
+ // failure_reason "unsupported_transport"/"skipped". Git ops are covered by the
8
+ // prefix check below.
9
+ const SINGLE_ONLY_OPS = new Set([
10
+ "SaveScene", "InstantiateScene", "ReplaceNode",
11
+ "SimulateInput", "ViewportSnapshot", "GameSnapshot",
12
+ "RunCommand", "RunVerification", "RunEditorScript",
13
+ "ImportFromUrl", "ImportFromUrlBatch", "ExtractZipFromUrl",
14
+ ]);
15
+ function isSingleOnlyOp(kind) {
16
+ return SINGLE_ONLY_OPS.has(kind) || kind.startsWith("Git");
17
+ }
5
18
  function sceneMutationOps(ops) {
6
19
  const saveIndexes = ops
7
20
  .map((op, index) => op.op === "SaveScene" ? index : -1)
@@ -17,23 +30,72 @@ function sceneMutationOps(ops) {
17
30
  }
18
31
  return [...ops, { op: "SaveScene" }];
19
32
  }
20
- function executeSceneMutation(client, scenePath, ops, options) {
21
- return client.executeIdentityBoundOps(sceneMutationOps(ops), {
22
- ...(options ?? {}),
23
- scenePath,
24
- });
25
- }
26
- async function readMainSceneFromProject(projectPath) {
27
- if (!projectPath)
28
- return null;
29
- try {
30
- const text = await readFile(join(projectPath, "project.godot"), "utf-8");
31
- const match = text.match(/run\/main_scene="([^"]+)"/);
32
- return match?.[1] ?? null;
33
+ /** Split an op list into sequential engine requests: consecutive batchable ops
34
+ * stay grouped, each single-only op becomes its own request. Order preserved. */
35
+ function chunkOpsForDispatch(ops) {
36
+ const chunks = [];
37
+ let current = [];
38
+ for (const op of ops) {
39
+ if (isSingleOnlyOp(String(op.op ?? ""))) {
40
+ if (current.length > 0) {
41
+ chunks.push(current);
42
+ current = [];
43
+ }
44
+ chunks.push([op]);
45
+ }
46
+ else {
47
+ current.push(op);
48
+ }
33
49
  }
34
- catch {
35
- return null;
50
+ if (current.length > 0)
51
+ chunks.push(current);
52
+ return chunks;
53
+ }
54
+ /** Execute an op list as sequential engine requests honoring the single-op
55
+ * dispatch contract. Receipts from every request are preserved and merged, so
56
+ * an op that applied followed by one that failed is reported honestly instead
57
+ * of masked. */
58
+ async function executeOpsChunked(send, ops) {
59
+ const chunks = chunkOpsForDispatch(ops);
60
+ if (chunks.length === 1)
61
+ return send(chunks[0]);
62
+ const receipts = [];
63
+ const combinedResults = [];
64
+ for (let i = 0; i < chunks.length; i++) {
65
+ const receipt = await send(chunks[i]);
66
+ receipts.push(receipt);
67
+ const envelope = (receipt ?? {});
68
+ if (Array.isArray(envelope.results)) {
69
+ combinedResults.push(...envelope.results);
70
+ }
71
+ if (extractOpError(receipt)) {
72
+ const failedKind = String(chunks[i][0]?.op ?? "batch");
73
+ const appliedOps = chunks.slice(0, i).flat().map((op) => String(op.op ?? ""));
74
+ const notSent = chunks.slice(i + 1).flat().map((op) => String(op.op ?? ""));
75
+ const baseError = (typeof envelope.error === "string" && envelope.error) || `Engine request failed (${failedKind}).`;
76
+ const honestError = appliedOps.length > 0
77
+ ? `${baseError} NOTE: ${appliedOps.length} earlier op(s) already applied (${appliedOps.join(", ")})` +
78
+ (failedKind === "SaveScene"
79
+ ? " — the scene is modified in the editor but NOT saved to disk. Fix the problem, then call summer_save_scene."
80
+ : ".") +
81
+ (notSent.length > 0 ? ` Not sent: ${notSent.join(", ")}.` : "")
82
+ : baseError;
83
+ return {
84
+ ...envelope,
85
+ error: honestError,
86
+ results: combinedResults,
87
+ receipts,
88
+ };
89
+ }
36
90
  }
91
+ const last = (receipts[receipts.length - 1] ?? {});
92
+ return { ...last, results: combinedResults, requests: chunks.length, receipts };
93
+ }
94
+ /** Scene mutation entry point: appends the transaction-boundary SaveScene, then
95
+ * dispatches with the single-op contract (mutations batch together, SaveScene
96
+ * and other single-only ops travel alone). */
97
+ function executeSceneMutation(client, scenePath, ops, options) {
98
+ return executeOpsChunked((chunk) => client.executeIdentityBoundOps(chunk, { ...(options ?? {}), scenePath }), sceneMutationOps(ops));
37
99
  }
38
100
  function requireSuccessfulOps(result, context) {
39
101
  const receipt = (result ?? {});
@@ -49,61 +111,94 @@ function requireSuccessfulOps(result, context) {
49
111
  }
50
112
  return receipt;
51
113
  }
114
+ /** Compose the minimal valid .tscn text scene: header + one root node.
115
+ * `uid` and `load_steps` are optional in the engine parser
116
+ * (resource_format_text.cpp treats a missing uid as INVALID_ID); the editor
117
+ * assigns a uid on first save. */
118
+ function minimalSceneText(rootName, rootType) {
119
+ return `[gd_scene format=3]\n\n[node name="${rootName}" type="${rootType}"]\n`;
120
+ }
121
+ // Node names reject . : @ / " %; the type must be a plain class name.
122
+ // Both are interpolated into quoted .tscn fields, so validate before composing.
123
+ const VALID_ROOT_NAME = /^[A-Za-z_][A-Za-z0-9_\- ]*$/;
124
+ const VALID_ROOT_TYPE = /^[A-Za-z_][A-Za-z0-9_]*$/;
52
125
  export function registerSceneTools(server) {
53
- server.tool("summer_create_scene", `Create a new empty scene file safely.
126
+ server.tool("summer_create_scene", `Create a new empty scene file. Writes a minimal .tscn through the identity-bound engine with a create-only guard: it fails if the path already exists, and it never touches the currently open scene.
54
127
 
55
- IMPORTANT:
56
- - This tool uses a temporary mutation strategy: it opens a template scene, removes children, saves to a new path, then restores the previous scene.
57
- - To prevent accidental destructive actions, you MUST explicitly set allow_temporary_scene_mutation=true.
58
- - If you don't want this strategy, stop and ask the user for manual scene creation in the editor.
128
+ The new scene is on disk but NOT opened. Call summer_open_scene to start editing it, then summer_add_node to build it out.
59
129
 
60
130
  Recommended workflow:
61
131
  1) Call summer_get_project_context
62
- 2) Call summer_open_main_scene (optional)
63
- 3) Call summer_create_scene with a new path`, {
132
+ 2) Call summer_create_scene with a new res:// path
133
+ 3) Call summer_open_scene with that path`, {
64
134
  path: z.string().describe("New scene path, e.g. 'res://scenes/empty_level.tscn'"),
65
135
  rootName: z.string().default("Main").describe("Root node name for the new scene"),
136
+ rootType: z
137
+ .string()
138
+ .default("Node3D")
139
+ .describe("Root node type. Default 'Node3D' (3D scenes); common alternatives: 'Node2D' (2D scenes), 'Control' (UI scenes), 'Node' (logic-only)."),
66
140
  allow_temporary_scene_mutation: z
67
141
  .boolean()
68
- .default(false)
69
- .describe("Safety gate. Must be true to proceed."),
70
- }, async ({ path, rootName, allow_temporary_scene_mutation }) => withEngine(async (client) => {
71
- if (!allow_temporary_scene_mutation) {
72
- throw new Error("Refusing to create scene without explicit approval. Re-run with allow_temporary_scene_mutation=true.");
142
+ .optional()
143
+ .describe("DEPRECATED, ignored. Scene creation no longer mutates any open scene; kept only so older callers don't break."),
144
+ }, async ({ path, rootName, rootType }) => withEngine(async (client) => {
145
+ const safePath = path.trim().replace(/\\/g, "/");
146
+ if (!safePath.startsWith("res://") || safePath.includes("..")) {
147
+ throw new Error("Scene path must be a traversal-free res:// project path.");
148
+ }
149
+ if (!safePath.endsWith(".tscn")) {
150
+ throw new Error("New scenes must use the text format: the path must end in .tscn.");
151
+ }
152
+ if (!VALID_ROOT_NAME.test(rootName)) {
153
+ throw new Error(`Invalid rootName "${rootName}": use letters, digits, underscores, hyphens, or spaces, starting with a letter or underscore.`);
154
+ }
155
+ if (!VALID_ROOT_TYPE.test(rootType)) {
156
+ throw new Error(`Invalid rootType "${rootType}": must be a plain class name like 'Node3D', 'Node2D', or 'Control'.`);
73
157
  }
74
- const health = (await client.health());
75
- const currentScene = typeof health.scene === "string" && health.scene.length > 0 ? health.scene : null;
76
- const projectPath = typeof health.project_path === "string" ? health.project_path : undefined;
77
- const mainScene = await readMainSceneFromProject(projectPath);
78
- const templateScene = currentScene || mainScene;
79
- if (!templateScene) {
80
- throw new Error("No scene open and could not resolve main scene. Call summer_get_project_context first, then open a known scene.");
158
+ // Same engine-routed guarded write path summer_write_file uses:
159
+ // identity-bound, and mustNotExist makes the engine refuse (create-only)
160
+ // if the path already exists. The open scene is never touched.
161
+ const receipt = requireSuccessfulOps(await client.executeIdentityBoundOps([
162
+ {
163
+ op: "WriteFile",
164
+ path: safePath,
165
+ content: minimalSceneText(rootName, rootType),
166
+ mustNotExist: true,
167
+ },
168
+ ]), `Creating ${safePath}`);
169
+ // Post-write verification: read the file back through the engine. The
170
+ // write receipt already passed; a read-back failure is reported honestly
171
+ // instead of failing the create or being masked.
172
+ let verified = false;
173
+ let verifyError;
174
+ try {
175
+ const read = (await client.readProjectFile(safePath, 4096));
176
+ const content = typeof read?.data?.content === "string" ? read.data.content : null;
177
+ if (read?.ok === false) {
178
+ verifyError = String(read.error ?? "engine could not read the file back");
179
+ }
180
+ else if (content === null) {
181
+ verifyError = "engine returned no text content on read-back";
182
+ }
183
+ else if (!content.includes(`[node name="${rootName}"`)) {
184
+ verifyError = "read-back content does not contain the expected root node";
185
+ }
186
+ else {
187
+ verified = true;
188
+ }
81
189
  }
82
- await client.executeOps([{ op: "OpenScene", path: templateScene }]);
83
- const tree = (await client.getSceneState(templateScene));
84
- const children = tree.data?.children ?? tree.children ?? [];
85
- const removeOps = children
86
- .map((c) => c.path)
87
- .filter((p) => typeof p === "string" && p.length > 0)
88
- .map((p) => ({ op: "RemoveNode", path: `./${p}` }));
89
- const createOps = [
90
- ...removeOps,
91
- { op: "SetProp", path: ".", key: "name", value: rootName },
92
- { op: "SaveScene", path },
93
- ];
94
- const createReceipt = requireSuccessfulOps(await executeSceneMutation(client, templateScene, createOps, {
95
- groupUndo: true,
96
- }), `Creating ${path}`);
97
- if (currentScene && currentScene !== path) {
98
- requireSuccessfulOps(await client.executeOps([{ op: "OpenScene", path: currentScene }]), `Restoring ${currentScene}`);
190
+ catch (err) {
191
+ verifyError = err instanceof Error ? err.message : String(err);
99
192
  }
100
193
  return {
101
194
  ok: true,
102
- created: path,
195
+ created: safePath,
103
196
  rootName,
104
- templateScene,
105
- restoredScene: currentScene,
106
- receipt: createReceipt,
197
+ rootType,
198
+ verified,
199
+ ...(verifyError ? { verifyError } : {}),
200
+ receipt,
201
+ hint: "The new scene is on disk but not open. Call summer_open_scene to start editing it.",
107
202
  };
108
203
  }));
109
204
  server.tool("summer_add_node", `Add a new node to the scene tree.
@@ -120,7 +215,7 @@ Common node types:
120
215
  The parent path uses "./" prefix for relative paths from scene root. E.g., "./World" means the "World" child of the root node.`, {
121
216
  scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
122
217
  parent: z.string().describe("Parent node path, e.g. './World' or './World/Enemies'"),
123
- type: z.string().describe("Godot node type, e.g. 'MeshInstance3D', 'CharacterBody3D'"),
218
+ type: z.string().describe("Summer Engine node type, e.g. 'MeshInstance3D', 'CharacterBody3D'"),
124
219
  name: z.string().describe("Name for the new node, e.g. 'Player', 'MainCamera'"),
125
220
  }, async ({ scenePath, parent, type, name }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "AddNode", parent, type, name }])));
126
221
  server.tool("summer_set_prop", `Set a property on a node. This is the primary way to configure nodes after adding them.
@@ -147,7 +242,7 @@ COMMON PROPERTIES:
147
242
  scenePath: z.string().describe("Target scene path, e.g. 'res://main.tscn'"),
148
243
  path: z.string().describe("Node path, e.g. './World/Player'"),
149
244
  key: z.string().describe("Property name, e.g. 'position', 'mesh', 'visible'"),
150
- value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format for complex types, native JSON for primitives"),
245
+ value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Summer Engine variant-string format for complex types, native JSON for primitives"),
151
246
  }, async ({ scenePath, path, key, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [{ op: "SetProp", path, key, value }])));
152
247
  server.tool("summer_set_resource_property", `Set a nested property on a resource attached to a node.
153
248
 
@@ -159,7 +254,7 @@ Use when you need to modify a sub-property of a resource, like:
159
254
  nodePath: z.string().describe("Node path, e.g. './Player/CollisionShape3D'"),
160
255
  resourceProperty: z.string().describe("Resource property on the node, e.g. 'shape', 'mesh', 'material_override'"),
161
256
  subProperty: z.string().describe("Property on the resource, e.g. 'size', 'radius', 'albedo_color'"),
162
- value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Godot string format"),
257
+ value: z.union([z.string(), z.number(), z.boolean()]).describe("Value in Summer Engine variant-string format"),
163
258
  }, async ({ scenePath, nodePath, resourceProperty, subProperty, value }) => withEngine(async (client) => executeSceneMutation(client, scenePath, [
164
259
  { op: "SetResourceProperty", nodePath, resourceProperty, subProperty, value },
165
260
  ])));
@@ -260,14 +355,19 @@ Each op in the array uses the same format as the individual tools:
260
355
  - {"op": "SetProp", "path": "Floor", "key": "mesh", "value": "PlaneMesh"}
261
356
  - {"op": "SetResourceProperty", "nodePath": "Floor", "resourceProperty": "mesh", "subProperty": "size", "value": "Vector2(20, 20)"}
262
357
 
263
- RAW RUNTIME OPS (interactive verification — engine-build dependent; structured failure_reason incl "unsupported" passes through verbatim):
264
- - SimulateInput — drive the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. type is "action" | "key" | "mouse_click" | "axis".
265
- - RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "extends SummerProbeBase\\nfunc _ready(): await super._ready(); report('ok', true); finish()", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report()/save_frame()/press()/key()/finish().
358
+ RAW RUNTIME OPS (interactive verification — structured failure_reason passes through verbatim):
359
+ - RunVerification — spawn a hidden, disposable game instance that runs a GDScript probe and dies (never touches the editor): {"op": "RunVerification", "probe_source": "...", "max_seconds": 20}. Returns {ok, results, frames, out_dir}. Probe API: report(name, value) / save_frame(name) / press(action) / key(keycode) / finish(). save_frame REQUIRES a name argument — save_frame() with no args is a script error. Mount scenes deferred: get_tree().root.add_child.call_deferred(instance); await get_tree().process_frame; await settle() — a direct add_child in _ready can hit the parent-busy guard and capture a black frame.
360
+ - SimulateInput — inject an action/key/mouse/axis into the RUNNING game (summer_play first): {"op": "SimulateInput", "type": "action", "action": "jump", "pressed": true}. It MUST be sent alone (single-op batch). failure_reason "not_running" = start the game first; "unsupported" = the running game build predates the handler — fall back to RunVerification or ask the user.
266
361
 
267
362
  Do not mix OpenScene with scene mutations in one batch. OpenScene is a UI action;
268
363
  send it separately. scenePath selects every mutation target. The tool appends one
269
364
  final SaveScene when the batch mutates a scene; if supplied explicitly, SaveScene
270
- must appear exactly once and be the final operation.`, {
365
+ must appear exactly once and be the final operation. The engine requires
366
+ SaveScene, InstantiateScene, ReplaceNode, SimulateInput, and the Run*/Import*
367
+ ops to travel as their own request, so this tool automatically splits your op
368
+ list into sequential requests around them — each split chunk is its own undo
369
+ step, and if a later chunk fails the receipt reports exactly which earlier ops
370
+ already applied.`, {
271
371
  scenePath: z.string().optional().describe("Required when ops contains scene mutations; exact res:// target scene path"),
272
372
  ops: z.array(z.record(z.unknown())).describe("Array of operation objects, each with 'op' plus its parameters"),
273
373
  }, async ({ scenePath, ops }) => withEngine(async (client) => {
@@ -291,6 +391,6 @@ must appear exactly once and be the final operation.`, {
291
391
  const options = { groupUndo: true, ...(scenePath ? { scenePath } : {}) };
292
392
  return needsScenePath
293
393
  ? executeSceneMutation(client, scenePath, ops, options)
294
- : client.executeOps(ops, options);
394
+ : executeOpsChunked((chunk) => client.executeOps(chunk, options), ops);
295
395
  }));
296
396
  }
@@ -10,14 +10,15 @@ import { withEngine } from "./with-engine.js";
10
10
  export function registerVisualTools(server) {
11
11
  server.tool("summer_screenshot", `Capture a frame from Summer Engine and return it as an image you can look at directly.
12
12
 
13
- Use this to visually verify your work: scene layout, asset placement, scale, framing, lighting, materials, missing/untextured assets, or runtime gameplay state. You see the actual pixels — no description layer in between.
13
+ Use this to visually verify your work: scene layout, asset placement, scale, framing, missing/untextured assets, or runtime gameplay state. You see the actual pixels — no description layer in between. Lighting and materials are only truthfully shown by the "viewport" and "game" targets — see the note on "scene" below.
14
14
 
15
15
  target:
16
16
  "viewport" (default) — the editor's CURRENT view (whatever scene/tab is open). No game boot. Use for edit-time checks of how the scene looks right now.
17
- "scene" — an OFFSCREEN render of a scene file (no game boot; physics/particles/animations are static at t=0). Optionally pass scenePath/framing/size/nodePath. Use for a quick composed look at a scene without touching the editor's open tab.
17
+ "scene" — an OFFSCREEN render of a scene file (no game boot; physics/particles/animations are static at t=0). Optionally pass scenePath/framing/size/nodePath. Use for COMPOSITION, SCALE and FRAMING without touching the editor's open tab.
18
+ It does NOT use the scene's environment/sky, and it injects a synthetic camera and light when the scene has none. The scene's WorldEnvironment — sky, fog, tonemap, glow, SSAO, ambient — is replaced by a flat preview environment. So this target CANNOT verify lighting, mood, or any material property that depends on the environment: change them and the frame comes back identical. For those, boot the game or run a RunVerification probe, whose instance renders the real environment.
18
19
  "game" — a frame from the RUNNING game (real runtime state). Start the game first (summer_play). Not available over a plain local connection — needs the Summer desktop app bridge.
19
20
 
20
- Static frame only — one moment, not motion. If a game capture is unavailable, use "viewport" or "scene", or ask the user to share a screenshot of the running game.`, {
21
+ Static frame only — one moment, not motion. For a SEQUENCE of frames over time, or for anything lighting-dependent when the desktop bridge is unavailable, use a RunVerification probe's save_frame(name) — its instance has a real renderer.`, {
21
22
  target: z
22
23
  .enum(["viewport", "scene", "game"])
23
24
  .optional()
@@ -28,9 +29,12 @@ Static frame only — one moment, not motion. If a game capture is unavailable,
28
29
  .optional()
29
30
  .describe('target:"scene" only. Full scene path, e.g. "res://main.tscn". Omit to render the currently-open scene.'),
30
31
  framing: z
31
- .enum(["auto", "top", "front", "iso"])
32
+ .enum(["auto", "iso", "top", "front", "back", "left", "right"])
32
33
  .optional()
33
- .describe('target:"scene" only. Camera framing preset. Default: auto.'),
34
+ .describe('target:"scene" only, 3D scenes. Camera direction preset: "iso" = 3/4 diagonal view, ' +
35
+ '"top" = straight down, "front" = camera at +Z, "back" = camera at -Z, ' +
36
+ '"left" = camera at -X, "right" = camera at +X. "auto" (default) is an alias of "iso". ' +
37
+ "The result reports the resolved framing."),
34
38
  size: z
35
39
  .array(z.number().int().positive())
36
40
  .length(2)
@@ -39,7 +43,10 @@ Static frame only — one moment, not motion. If a game capture is unavailable,
39
43
  nodePath: z
40
44
  .string()
41
45
  .optional()
42
- .describe('target:"scene" only. Scene-tree node path to frame on.'),
46
+ .describe('target:"scene" only. Node path relative to the scene root (e.g. "Player/Mesh") to frame ' +
47
+ "INSTEAD of the whole scene — the camera fits that node's combined bounds (3D visual AABBs " +
48
+ "or 2D rects, children included). A bare unique name is also found recursively. " +
49
+ 'Fails with failure_reason "node_not_found" when the path does not resolve (no silent whole-scene fallback).'),
43
50
  }, async ({ target, scenePath, framing, size, nodePath }) => withEngine(async (client) => {
44
51
  if (target === "game")
45
52
  return client.gameSnapshot();
@@ -111,11 +118,28 @@ Static frame only — one moment, not motion. If a game capture is unavailable,
111
118
  if (snap.sceneHadLight === false) {
112
119
  warnings.push("WARNING: this scene has no light — lit materials may appear black when played.");
113
120
  }
121
+ // The engine ALWAYS synthesizes the preview camera (preview_ops.cpp
122
+ // sets used_synthetic_camera unconditionally) — the flag says nothing
123
+ // about the scene's own cameras. sceneHasCamera above is the
124
+ // authoritative "does this scene have a camera" answer.
114
125
  if (snap.usedSyntheticCamera) {
115
- warnings.push("NOTE: this preview used a synthetic camera the engine added just for the render — the scene itself has no camera, so it will NOT frame like this when played.");
126
+ warnings.push("NOTE: this preview is framed by a synthetic render camera, NOT the scene's own camera — the played game will not frame like this image.");
116
127
  }
117
128
  }
118
- const caption = `${label} (${dims}). Saved to ${snap.localPath ?? "n/a"}. Review the image above and describe what you actually see.` +
129
+ // Scene-preview capture details: resolved framing ("auto" -> "iso"),
130
+ // which node was framed (confirms nodePath resolved), and how many
131
+ // blank-readback retries the engine needed (0 = omitted).
132
+ const details = [];
133
+ if (target === "scene") {
134
+ if (snap.framing)
135
+ details.push(`framing: ${snap.framing}`);
136
+ if (snap.framedNode)
137
+ details.push(`framed node: ${snap.framedNode}`);
138
+ if (snap.renderRetries)
139
+ details.push(`render retries: ${snap.renderRetries}`);
140
+ }
141
+ const detailNote = details.length ? `; ${details.join(", ")}` : "";
142
+ const caption = `${label} (${dims}${detailNote}). Saved to ${snap.localPath ?? "n/a"}. Review the image above and describe what you actually see.` +
119
143
  (warnings.length ? `\n\n${warnings.join("\n")}` : "");
120
144
  return [
121
145
  { type: "image", data: snap.base64, mimeType: snap.mime || "image/jpeg" },
@@ -9,6 +9,13 @@ function attachMeta(result, meta) {
9
9
  });
10
10
  return result;
11
11
  }
12
+ function getFailureReason(value) {
13
+ return typeof value.failureReason === "string"
14
+ ? value.failureReason
15
+ : typeof value.failure_reason === "string"
16
+ ? value.failure_reason
17
+ : undefined;
18
+ }
12
19
  // 0.5.34 Block E contract (publicsummerengine src/lib/tools/contract.ts §0.1).
13
20
  // The async lifecycle (async-op-lifecycle.ts pollOpToTerminal) merges
14
21
  // terminalState/errorClass onto the apply dict it returns. ONLY these two are
@@ -36,14 +43,11 @@ function readClassifiers(result) {
36
43
  if (!result || typeof result !== "object")
37
44
  return {};
38
45
  const op = result;
46
+ const failed = op.results?.find((entry) => entry.ok === false);
39
47
  return {
40
48
  terminalState: typeof op.terminalState === "string" ? op.terminalState : undefined,
41
49
  errorClass: typeof op.errorClass === "string" ? op.errorClass : undefined,
42
- failureReason: typeof op.failureReason === "string"
43
- ? op.failureReason
44
- : typeof op.failure_reason === "string"
45
- ? op.failure_reason
46
- : undefined,
50
+ failureReason: getFailureReason(op) ?? (failed ? getFailureReason(failed) : undefined),
47
51
  };
48
52
  }
49
53
  /**
@@ -63,25 +67,53 @@ export function extractOpError(result) {
63
67
  if (!result || typeof result !== "object")
64
68
  return null;
65
69
  const op = result;
70
+ const firstFailed = op.results?.find((r) => r.ok === false);
71
+ // The engine stamps failure classifiers on the envelope OR per-op inside
72
+ // results[] (the batch gate does the latter) — read both.
73
+ const failureReason = getFailureReason(op) ?? (firstFailed ? getFailureReason(firstFailed) : undefined);
74
+ // Classified failures are rendered as JSON so callers can reliably read
75
+ // failure_reason instead of scraping a sentence. A plain-text "Hint:" line may
76
+ // follow the JSON in the final tool text. Unclassified failures stay plain.
77
+ const classify = (message) => {
78
+ if (!failureReason)
79
+ return message;
80
+ return JSON.stringify({
81
+ error: message,
82
+ failure_reason: failureReason,
83
+ ...(typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error !== message
84
+ ? { op_error: firstFailed.error }
85
+ : {}),
86
+ ...(typeof firstFailed?.op === "string" ? { op: firstFailed.op } : {}),
87
+ ...(typeof op.terminalState === "string" ? { terminalState: op.terminalState } : {}),
88
+ ...(typeof op.errorClass === "string" ? { errorClass: op.errorClass } : {}),
89
+ }, null, 2);
90
+ };
66
91
  // Failure terminalState takes precedence — it is set by the async lifecycle
67
92
  // exactly when the op did not land (timeout, backpressure, lease/identity
68
- // rejection, cancellation), often with NO results[] to inspect.
93
+ // rejection, cancellation), sometimes with NO results[] to inspect. Prefer the
94
+ // precise engine rejection (envelope error, then the failed op's error) over
95
+ // the generic terminal-state sentence.
69
96
  const ts = op.terminalState;
70
97
  if (typeof ts === "string" && ts.length > 0 && !SUCCESS_TERMINAL_STATES.has(ts)) {
71
- if (typeof op.error === "string" && op.error.length > 0)
72
- return op.error;
73
- return TERMINAL_STATE_MESSAGES[ts] ?? `Engine operation failed (terminalState: ${ts}).`;
98
+ const message = (typeof op.error === "string" && op.error.length > 0 && op.error) ||
99
+ (typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error) ||
100
+ TERMINAL_STATE_MESSAGES[ts] ||
101
+ `Engine operation failed (terminalState: ${ts}).`;
102
+ return classify(message);
74
103
  }
75
- // An explicit ok:false / status:"error" is a failure even when the engine
76
- // omitted an error string — surface it rather than mask it (matches the web
77
- // contract `isFailureSignal`).
78
- if (op.ok === false)
79
- return op.error || "Engine operation failed (ok: false).";
80
- if (op.status === "error")
81
- return op.error || "Engine operation failed (status: error).";
82
- const firstFailed = op.results?.find((r) => r.ok === false);
83
- if (firstFailed) {
84
- return firstFailed.error || `Engine op failed${firstFailed.op ? ` (${firstFailed.op})` : ""}.`;
104
+ // An explicit ok:false / status:"error" / failed op inside results[] is a
105
+ // failure even when the engine omitted an error string — surface it rather
106
+ // than mask it (matches the web contract `isFailureSignal`). The envelope
107
+ // error is kept when present; the failed op's own error backs it up.
108
+ if (op.ok === false || op.status === "error" || firstFailed) {
109
+ const message = (typeof op.error === "string" && op.error.length > 0 && op.error) ||
110
+ (typeof firstFailed?.error === "string" && firstFailed.error.length > 0 && firstFailed.error) ||
111
+ (firstFailed
112
+ ? `Engine op failed${firstFailed.op ? ` (${firstFailed.op})` : ""}.`
113
+ : op.ok === false
114
+ ? "Engine operation failed (ok: false)."
115
+ : "Engine operation failed (status: error).");
116
+ return classify(message);
85
117
  }
86
118
  return null;
87
119
  }
File without changes
File without changes
@@ -3,7 +3,7 @@
3
3
  #
4
4
  # Event: SessionStart (matcher: startup|clear|compact)
5
5
  # Purpose: Orient Claude on every fresh Summer-enabled session.
6
- # Detect Godot project + GameSoul + doctor health, surface a one-line
6
+ # Detect Summer project + GameSoul + doctor health, surface a one-line
7
7
  # Bob-style greeting as additional context.
8
8
  #
9
9
  # Input: JSON on stdin with { session_id, source, model, cwd, ... }
@@ -36,7 +36,7 @@ else
36
36
  LINES+=("Summer ready.")
37
37
  fi
38
38
 
39
- # Line 2: Godot project + GameSoul one-liner (only if project.godot exists)
39
+ # Line 2: Summer project + GameSoul one-liner (only if project.godot exists)
40
40
  if [ -f "${PROJECT_DIR}/project.godot" ]; then
41
41
  PROJECT_NAME=$(grep -E '^config/name=' "${PROJECT_DIR}/project.godot" 2>/dev/null \
42
42
  | head -1 | sed 's/^config\/name=//;s/^"//;s/"$//')
@@ -48,12 +48,12 @@ if [ -f "${PROJECT_DIR}/project.godot" ]; then
48
48
  SOUL_LINE=$(awk 'BEGIN{p=""} /^[^#[:space:]]/ && NF>0 {p=$0; exit} END{print p}' \
49
49
  "$SOUL_FILE" 2>/dev/null | cut -c1-120)
50
50
  if [ -n "$SOUL_LINE" ]; then
51
- LINES+=("Godot project: ${PROJECT_NAME}. GameSoul: ${SOUL_LINE}")
51
+ LINES+=("Summer project: ${PROJECT_NAME}. GameSoul: ${SOUL_LINE}")
52
52
  else
53
- LINES+=("Godot project: ${PROJECT_NAME}.")
53
+ LINES+=("Summer project: ${PROJECT_NAME}.")
54
54
  fi
55
55
  else
56
- LINES+=("Godot project: ${PROJECT_NAME}.")
56
+ LINES+=("Summer project: ${PROJECT_NAME}.")
57
57
  fi
58
58
  fi
59
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "summer-engine",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
4
4
  "description": "Local Summer CLI and MCP server for Summer Engine. Install and run the engine, connect Claude Code, Cursor, Codex, Gemini, and other agents, and build real games with bundled skills, hooks, and plugins.",
5
5
  "keywords": [
6
6
  "summer-engine",
@@ -41,6 +41,7 @@
41
41
  "files": [
42
42
  "dist/",
43
43
  "skills/",
44
+ "references/",
44
45
  "commands/",
45
46
  "hooks/",
46
47
  "assets/",
@@ -65,6 +66,7 @@
65
66
  "dev": "tsc --watch",
66
67
  "summer": "node dist/bin/summer.js",
67
68
  "test": "vitest run",
69
+ "test:public-language": "vitest run src/lib/public-surface-language.test.ts",
68
70
  "test:watch": "vitest",
69
71
  "prepublishOnly": "npm run build && npm test"
70
72
  },
@@ -0,0 +1,50 @@
1
+ # Collaborative Protocol — "May I write?"
2
+
3
+ > Every Summer skill must follow this. The agent works *with* the user, not around them.
4
+
5
+ ## The rule
6
+
7
+ **Before any user-visible write step, the agent must explicitly state what it is about to do and ask permission to proceed.**
8
+
9
+ This applies to:
10
+ - Creating files (`.gd`, `.tscn`, `.tres`, JSON, configs)
11
+ - Modifying existing files (especially `.gd` and `.tscn`)
12
+ - Running irreversible operations (deletion, reset, force-overwrite)
13
+ - Setting up project-wide settings (`project.godot`)
14
+ - Installing tools, libraries, or templates
15
+
16
+ Reading is unconditional. Inspection, scene-tree walks, diagnostics — never need permission.
17
+
18
+ ## Phrasing
19
+
20
+ Plain, short, action-first. Examples:
21
+
22
+ > May I create `scenes/player.tscn` and add a CharacterBody3D + Camera3D?
23
+
24
+ > I'm about to overwrite `levels/level_01.tscn`. The current version has 14 nodes — I'll preserve them and add the new lighting setup. Continue?
25
+
26
+ > May I attach `scripts/player_controller.gd` to `./World/Player`?
27
+
28
+ > I'm going to bind `move_forward`, `move_back`, `move_left`, `move_right`, `jump`. OK?
29
+
30
+ ## What this prevents
31
+
32
+ 1. **Silent overwrites.** The agent stomps the user's hand-edited script.
33
+ 2. **Cascading scope creep.** A "small fix" turns into a 30-file refactor.
34
+ 3. **Unclear state.** The user has no idea what changed when something breaks.
35
+ 4. **Trust loss.** One unauthorized edit and the user stops trusting the agent.
36
+
37
+ ## What this is NOT
38
+
39
+ - Not a confirmation dialog for every property change in a single skill execution. Group related writes into one ask: "I'm about to add the player root, attach the controller script, and bind 5 input actions. OK?"
40
+ - Not a request for design input. The skill should know what to do; the ask is for permission, not direction.
41
+ - Not a stop on simple read-only ops or single-property tweaks the user clearly directed.
42
+
43
+ ## Linter check
44
+
45
+ The `workflow/skill-test/SKILL.md` static linter looks for the phrases:
46
+ - "May I" / "May I write" / "May I create" / "May I attach"
47
+ - "I'm about to"
48
+ - "Continue?" / "OK?" / "Proceed?"
49
+
50
+ A skill that mutates files but never says any of these fails the linter.