summer-engine 2.7.0 → 2.8.0

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 (150) 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 +10 -0
  8. package/GEMINI.md +5 -4
  9. package/README.md +58 -19
  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 +123 -36
  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 +4 -2
  34. package/dist/lib/api-client.js +28 -4
  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/generate-tools.js +16 -6
  58. package/dist/mcp/tools/project-tools.js +12 -7
  59. package/dist/mcp/tools/scene-tools.js +6 -6
  60. package/dist/mcp/tools/visual-tools.js +4 -3
  61. package/hooks/pre-commit-doctor.sh +0 -0
  62. package/hooks/run-hook.cmd +0 -0
  63. package/hooks/session-start.sh +5 -5
  64. package/package.json +3 -1
  65. package/references/collaborative-protocol.md +50 -0
  66. package/references/gd-style.md +121 -0
  67. package/references/godot-version.md +51 -0
  68. package/references/mcp-tools-reference.md +199 -0
  69. package/references/public-surface-language.md +30 -0
  70. package/references/summer-folder.md +90 -0
  71. package/references/template-registry.md +73 -0
  72. package/skills/2d-assets/character-portrait/SKILL.md +17 -12
  73. package/skills/2d-assets/concept-art/SKILL.md +10 -9
  74. package/skills/2d-assets/pixel-art/SKILL.md +36 -20
  75. package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
  76. package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
  77. package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
  78. package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
  79. package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
  80. package/skills/3d-assets/character-model/SKILL.md +34 -19
  81. package/skills/3d-assets/environment-kit/SKILL.md +11 -5
  82. package/skills/3d-assets/organic-model/SKILL.md +8 -5
  83. package/skills/3d-assets/prop-model/SKILL.md +9 -3
  84. package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
  85. package/skills/README.md +1 -1
  86. package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
  87. package/skills/animation/animation-tree/SKILL.md +39 -15
  88. package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
  89. package/skills/animation/generate-motion/SKILL.md +47 -15
  90. package/skills/animation/procedural-animation/SKILL.md +31 -13
  91. package/skills/animation/retarget/SKILL.md +127 -79
  92. package/skills/audio/adaptive-music/SKILL.md +24 -20
  93. package/skills/audio/ambient-bed/SKILL.md +22 -16
  94. package/skills/audio/audio-direction/SKILL.md +15 -10
  95. package/skills/audio/music-track/SKILL.md +18 -12
  96. package/skills/audio/sound-effect/SKILL.md +17 -12
  97. package/skills/audio/voice-line/SKILL.md +42 -42
  98. package/skills/character-controllers/fps-controller/SKILL.md +26 -16
  99. package/skills/debugging/debug/SKILL.md +52 -20
  100. package/skills/deployment/export-and-ship/SKILL.md +105 -38
  101. package/skills/deployment/remote-deploy/SKILL.md +56 -23
  102. package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
  103. package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
  104. package/skills/level-design/design-level/SKILL.md +22 -8
  105. package/skills/level-design/scene-to-level/SKILL.md +13 -7
  106. package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
  107. package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
  108. package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
  109. package/skills/performance/tune-performance/SKILL.md +51 -32
  110. package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
  111. package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
  112. package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
  113. package/skills/scene-and-project/make-game/SKILL.md +2 -1
  114. package/skills/scene-and-project/play/SKILL.md +10 -4
  115. package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
  116. package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
  117. package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
  118. package/skills/video/animated-loop/SKILL.md +63 -39
  119. package/skills/video/cinematic-cutscene/SKILL.md +66 -30
  120. package/skills/video/trailer-shot/SKILL.md +30 -21
  121. package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
  122. package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
  123. package/skills/visual-effects/game-feel/SKILL.md +51 -33
  124. package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
  125. package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
  126. package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
  127. package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
  128. package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
  129. package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
  130. package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
  131. package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
  132. package/skills/workflow/brainstorming/SKILL.md +1 -1
  133. package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
  134. package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
  135. package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
  136. package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
  137. package/skills/workflow/gameskill/SKILL.md +11 -2
  138. package/skills/workflow/headless-scripting/SKILL.md +407 -0
  139. package/skills/workflow/investigating-bugs/SKILL.md +4 -2
  140. package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
  141. package/skills/workflow/mcpupdate/SKILL.md +3 -3
  142. package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
  143. package/skills/workflow/skill-create/SKILL.md +7 -7
  144. package/skills/workflow/skill-test/SKILL.md +3 -3
  145. package/skills/workflow/using-summer/SKILL.md +12 -2
  146. package/skills/workflow/verification-before-completion/SKILL.md +2 -0
  147. package/skills/workflow/writing-plans/SKILL.md +7 -7
  148. package/skills/workflow/writing-skills/SKILL.md +7 -5
  149. package/skills/workflow/writing-skills/render-graphs.js +0 -0
  150. package/skills/workflow/writing-skills/testing-skills-with-subagents.md +1 -1
@@ -0,0 +1,262 @@
1
+ # SUMMER - Verification Runner v1: probe base class.
2
+ #
3
+ # An AI-authored probe extends this base. The engine injects the probe as a singleton
4
+ # named "SummerProbe" into a disposable, invisible --summer-verify game instance. The
5
+ # base provides the runtime API (report / save_frame / dump_tree / press / key / finish)
6
+ # and guarantees a results.json is written even if the probe hangs (at-exit backstop +
7
+ # the frames-based quit-after ceiling in main.cpp).
8
+ #
9
+ # The engine sets two members on the instance before _ready() via `set()`:
10
+ # summer_out_dir : String - absolute temp dir to write results/frames into
11
+ # summer_max_seconds : int - soft budget; base auto-finishes at this ceiling
12
+ #
13
+ # Contract of <out_dir>/results.json:
14
+ # { "reports": {..}, "frames": [names], "errors_seen": [strings],
15
+ # "frame_warnings": [strings], "duration_ms": int, "finished": bool }
16
+ #
17
+ # PERCEPTION NOTES (measured against the shipped 0.5.55 binary, reproducible 3/3):
18
+ # * This instance is NOT --headless. Headless has no renderer, so
19
+ # viewport.get_texture().get_image() returns null and NO frame can ever be
20
+ # captured. The verify child is a real windowed instance parked offscreen at
21
+ # (-32000,-32000) precisely so capture works. Never add --headless here.
22
+ # * The viewport does not hold your scene yet during _ready(). Measured, with a
23
+ # red box on screen, sampling distinct colours in the captured image:
24
+ # in _ready() frames_drawn=0 -> 1 colour (clear colour only)
25
+ # after 1 process_frame frames_drawn=0 -> 1 colour (still empty)
26
+ # after 2 process_frames frames_drawn=1 -> 2 colours (the box is there)
27
+ # So `await settle()` before your FIRST save_frame(). save_frame() records a
28
+ # frame_warnings entry rather than lying if you skip it.
29
+ # * RenderingServer.force_draw() is NOT a shortcut. Measured: it fills the image
30
+ # with the clear colour (1 colour, all non-black) but the scene geometry is
31
+ # still missing. A plausible-but-wrong frame is worse than a blank one.
32
+ # * Audio IS assertable even though this instance forces the Dummy driver
33
+ # (main.cpp, summer_verify block). AudioServer still mixes, so measured:
34
+ # silent scene -> AudioServer.get_bus_peak_volume_left_db(0,0) == -200.0
35
+ # playing sound -> ... == -1.938
36
+ # * Performance monitors work here and only here. Measured in this instance:
37
+ # draw_calls=1, fps=46, objects=1, primitives=12. The same probe under
38
+ # --headless reports draw_calls=0, fps=1, objects=0 -- all meaningless.
39
+ extends Node
40
+
41
+ var summer_out_dir: String = ""
42
+ var summer_max_seconds: int = 20
43
+
44
+ var _reports: Dictionary = {}
45
+ var _frames: Array = []
46
+ var _frame_warnings: Array = []
47
+ var _start_ms: int = 0
48
+ var _finished: bool = false
49
+
50
+ func _enter_tree() -> void:
51
+ _start_ms = Time.get_ticks_msec()
52
+
53
+ func _ready() -> void:
54
+ # Hard soft-ceiling: if the probe body never calls finish(), auto-finish just before
55
+ # the main.cpp frames ceiling would kill us, so we still flush a partial results.json.
56
+ var t := Timer.new()
57
+ t.one_shot = true
58
+ t.wait_time = float(summer_max_seconds)
59
+ t.timeout.connect(_on_deadline)
60
+ add_child(t)
61
+ t.start()
62
+
63
+ func _on_deadline() -> void:
64
+ if not _finished:
65
+ report("_timeout", true)
66
+ finish()
67
+
68
+ # --- Runtime API for the AI-authored probe --------------------------------------------
69
+
70
+ func report(key: String, value) -> void:
71
+ _reports[key] = value
72
+
73
+ func settle(frames: int = 1) -> void:
74
+ # Await until the renderer has actually PRESENTED `frames` more frames. Call this
75
+ # before your first save_frame(), and after any change you want to see on screen.
76
+ #
77
+ # await settle() # first capture is now trustworthy
78
+ # save_frame("start")
79
+ #
80
+ # Counting presented frames (Engine.get_frames_drawn()) rather than awaiting
81
+ # process_frame is deliberate: a process frame can complete without the renderer
82
+ # having presented anything, which is exactly the blank-capture bug. See the
83
+ # PERCEPTION NOTES at the top for the measurements behind this.
84
+ #
85
+ # But that counter only advances when there IS a renderer. Under --headless it is
86
+ # 0 forever -- measured: 438 process frames awaited over 3005ms, frames_drawn
87
+ # still 0 -- so waiting on it there would spin until the probe budget killed the
88
+ # run. This base is normally injected into the windowed verify child, but the
89
+ # headless-scripting path runs it under `--headless -s`, where that would hang.
90
+ # So: fall back to process frames when there is no renderer, and bound the wait
91
+ # either way so settle() can never be the reason a probe fails to finish.
92
+ if not has_renderer():
93
+ # +1 because content lands a frame later than the naive count suggests.
94
+ for _i in range(maxi(1, frames) + 1):
95
+ await get_tree().process_frame
96
+ return
97
+ var target := Engine.get_frames_drawn() + maxi(1, frames)
98
+ var guard := 0
99
+ while Engine.get_frames_drawn() < target and guard < 600:
100
+ guard += 1
101
+ await get_tree().process_frame
102
+ if guard >= 600:
103
+ report("_settle_timeout", true)
104
+
105
+ func has_renderer() -> bool:
106
+ # True when this instance can actually draw. Use this, NOT OS.has_feature("headless"),
107
+ # which is measurably FALSE under --headless and will mislead you. The rendering
108
+ # method setting lies too -- it still reads "forward_plus" with no renderer behind it.
109
+ # DisplayServer's own name is the honest signal.
110
+ return DisplayServer.get_name() != "headless"
111
+
112
+ func settle_physics(frames: int = 1) -> void:
113
+ # Await N physics frames. Call this after building a scene and BEFORE asserting
114
+ # anything physical. A body added this frame has no space yet, so the natural
115
+ # "build the scene, immediately assert physics" probe throws on frame 0:
116
+ # Parameter "body->get_space()" is null
117
+ # at: godot_physics_server_2d.cpp:997
118
+ # One physics frame after add_child() is the minimum; raycasts and contacts
119
+ # generally want two.
120
+ for _i in range(maxi(1, frames)):
121
+ await get_tree().physics_frame
122
+
123
+ func settle_navigation(max_frames: int = 60) -> bool:
124
+ # Await until the navigation map is actually queryable, and report whether it got
125
+ # there. Returns true on success, false if it timed out.
126
+ #
127
+ # This exists because the failure is SILENT. NavigationServer3D.map_get_path()
128
+ # returns an EMPTY array with no error until the map has synchronized TWICE --
129
+ # iteration_id == 1 is not enough. Measured here, awaiting physics_frame:
130
+ # frame 0-1 iter=0 path_pts=0
131
+ # frame 2-3 iter=1 path_pts=0 <- still empty, still no error
132
+ # frame 4+ iter=2 path_pts=2 <- queryable
133
+ # So the obvious probe returns [] and reads as "navigation is broken" when
134
+ # nothing is wrong. (map_get_closest_point() does error; map_get_path() does not.)
135
+ # process_frame does NOT advance the iteration id as quickly -- await physics.
136
+ var map := get_viewport().world_3d.navigation_map
137
+ for _i in range(maxi(1, max_frames)):
138
+ if NavigationServer3D.map_get_iteration_id(map) >= 2:
139
+ return true
140
+ await get_tree().physics_frame
141
+ report("_navigation_settle_timeout", true)
142
+ return false
143
+
144
+ func save_frame(name: String) -> String:
145
+ # Capture the current viewport framebuffer to <out_dir>/<name> (jpg). Returns the
146
+ # absolute path written, or "" on failure.
147
+ #
148
+ # If no frame has been presented yet, the file still lands (so the web side can look
149
+ # at it) but a frame_warnings entry is recorded, because the image will be the flat
150
+ # clear colour rather than your scene. Fix that by `await settle()` first -- do not
151
+ # ignore the warning, the capture is not evidence of anything.
152
+ if not has_renderer():
153
+ var hw := "save_frame(\"%s\") called under --headless: there is NO renderer, get_image() returns null and no frame can ever be captured. Use the windowed verify path (--summer-verify), not --headless." % name
154
+ if not _frame_warnings.has(hw):
155
+ _frame_warnings.append(hw)
156
+ elif Engine.get_frames_drawn() < 1:
157
+ var w := "save_frame(\"%s\") ran before any frame was presented - the image is the clear colour, not the scene. `await settle()` first." % name
158
+ if not _frame_warnings.has(w):
159
+ _frame_warnings.append(w)
160
+ var vp := get_viewport()
161
+ if vp == null:
162
+ return ""
163
+ var tex := vp.get_texture()
164
+ if tex == null:
165
+ return ""
166
+ var img := tex.get_image()
167
+ if img == null:
168
+ return ""
169
+ var fname := name if name.ends_with(".jpg") else name + ".jpg"
170
+ var path := summer_out_dir.path_join(fname)
171
+ var err := img.save_jpg(path, 0.75)
172
+ if err == OK:
173
+ if not _frames.has(fname):
174
+ _frames.append(fname)
175
+ return path
176
+ return ""
177
+
178
+ func dump_tree(max_depth: int = 6) -> void:
179
+ # Summarize the live scene tree (names + classes) into the report under "tree".
180
+ var root := get_tree().root
181
+ var lines: Array = []
182
+ _dump_node(root, 0, max_depth, lines)
183
+ report("tree", lines)
184
+
185
+ func _dump_node(n: Node, depth: int, max_depth: int, lines: Array) -> void:
186
+ if depth > max_depth:
187
+ return
188
+ var indent := " ".repeat(depth)
189
+ lines.append("%s%s (%s)" % [indent, n.name, n.get_class()])
190
+ for c in n.get_children():
191
+ _dump_node(c, depth + 1, max_depth, lines)
192
+
193
+ func press(action: String, hold_ms: int = 100) -> void:
194
+ # Inject an input action press+release from INSIDE the process (no debugger).
195
+ var ev := InputEventAction.new()
196
+ ev.action = action
197
+ ev.pressed = true
198
+ Input.parse_input_event(ev)
199
+ await get_tree().create_timer(hold_ms / 1000.0).timeout
200
+ var up := InputEventAction.new()
201
+ up.action = action
202
+ up.pressed = false
203
+ Input.parse_input_event(up)
204
+
205
+ func key(keycode: int, hold_ms: int = 100) -> void:
206
+ # Inject a physical key press+release by keycode (e.g. KEY_SPACE).
207
+ var ev := InputEventKey.new()
208
+ ev.keycode = keycode
209
+ ev.physical_keycode = keycode
210
+ ev.pressed = true
211
+ Input.parse_input_event(ev)
212
+ await get_tree().create_timer(hold_ms / 1000.0).timeout
213
+ var up := InputEventKey.new()
214
+ up.keycode = keycode
215
+ up.physical_keycode = keycode
216
+ up.pressed = false
217
+ Input.parse_input_event(up)
218
+
219
+ func finish() -> void:
220
+ # Flush results.json and terminate the disposable instance.
221
+ if _finished:
222
+ return
223
+ _finished = true
224
+ _write_results(true)
225
+ get_tree().quit()
226
+
227
+ # --- Results writing ------------------------------------------------------------------
228
+
229
+ func _collect_errors() -> Array:
230
+ # The C++ SummerVerifyLogger appends error-level engine/script errors to errors.log.
231
+ var errors: Array = []
232
+ var p := summer_out_dir.path_join("errors.log")
233
+ if FileAccess.file_exists(p):
234
+ var f := FileAccess.open(p, FileAccess.READ)
235
+ if f != null:
236
+ while not f.eof_reached():
237
+ var line := f.get_line()
238
+ if line.strip_edges() != "":
239
+ errors.append(line)
240
+ return errors
241
+
242
+ func _write_results(finished: bool) -> void:
243
+ var out := {
244
+ "reports": _reports,
245
+ "frames": _frames,
246
+ "frame_warnings": _frame_warnings,
247
+ "errors_seen": _collect_errors(),
248
+ "duration_ms": Time.get_ticks_msec() - _start_ms,
249
+ "finished": finished,
250
+ }
251
+ var path := summer_out_dir.path_join("results.json")
252
+ var f := FileAccess.open(path, FileAccess.WRITE)
253
+ if f != null:
254
+ f.store_string(JSON.stringify(out, " "))
255
+ f.flush()
256
+ f.close()
257
+
258
+ func _exit_tree() -> void:
259
+ # At-exit backstop: if finish() was never called (quit-after ceiling in main.cpp
260
+ # killed us, or the probe crashed), still leave a partial results.json behind.
261
+ if not _finished:
262
+ _write_results(false)
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env bash
2
+ # Run the autopilot against this project and print the results.
3
+ #
4
+ # bash tests/autopilot/run.sh # runs autopilot.gd
5
+ # bash tests/autopilot/run.sh my_probe.gd # runs a different probe
6
+ #
7
+ # No editor required. This spawns the engine's offscreen verify instance, which
8
+ # has a REAL renderer — unlike --headless, which produces no pixels at all.
9
+ set -euo pipefail
10
+
11
+ PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
12
+ PROBE="${1:-$PROJECT_DIR/tests/autopilot/autopilot.gd}"
13
+ OUT="$PROJECT_DIR/tests/autopilot/out"
14
+ MAX_SECONDS="${MAX_SECONDS:-40}"
15
+
16
+ # Find the engine. Override with SUMMER_BIN=/path/to/engine if it lives elsewhere.
17
+ # There is no `godot` binary on a Summer install — do not substitute one.
18
+ if [[ -n "${SUMMER_BIN:-}" ]]; then
19
+ ENGINE="$SUMMER_BIN"
20
+ elif [[ -x "/Applications/Summer.app/Contents/MacOS/Summer" ]]; then
21
+ ENGINE="/Applications/Summer.app/Contents/MacOS/Summer"
22
+ elif [[ -x "${LOCALAPPDATA:-}/Summer/current/Summer.exe" ]]; then
23
+ ENGINE="${LOCALAPPDATA}/Summer/current/Summer.exe"
24
+ else
25
+ echo "Could not find the Summer engine binary." >&2
26
+ echo "Set SUMMER_BIN, or read engineBinaryPath from summer_get_project_context." >&2
27
+ exit 1
28
+ fi
29
+
30
+ rm -rf "$OUT"
31
+ mkdir -p "$OUT"
32
+
33
+ # --disable-crash-handler: Summer's handler popen()s atos from inside a signal
34
+ # handler, which turns a clean failure into a hang. Never arm it on an
35
+ # agent-driven run. Note there is deliberately no --headless here: headless has
36
+ # no renderer, so save_frame() would produce nothing.
37
+ "$ENGINE" \
38
+ --disable-crash-handler \
39
+ --path "$PROJECT_DIR" \
40
+ --summer-verify "$PROBE" \
41
+ --summer-verify-out "$OUT" \
42
+ --summer-verify-max "$MAX_SECONDS" \
43
+ >"$OUT/engine.log" 2>&1 || true
44
+
45
+ if [[ ! -f "$OUT/results.json" ]]; then
46
+ echo "No results.json — the probe never started. Engine output:" >&2
47
+ tail -30 "$OUT/engine.log" >&2
48
+ exit 1
49
+ fi
50
+
51
+ cat "$OUT/results.json"
52
+ echo
53
+ echo "Frames and full log: $OUT"
54
+
55
+ # finished:false means the probe hit its time ceiling before calling finish().
56
+ # That is a failure, not a pass — exit non-zero so CI notices.
57
+ if command -v python3 >/dev/null 2>&1; then
58
+ python3 - "$OUT/results.json" <<'PY'
59
+ import json, sys
60
+ d = json.load(open(sys.argv[1]))
61
+ reports = d.get("reports", {})
62
+ if not d.get("finished", False):
63
+ print("FAILED: probe did not finish (hit --summer-verify-max)", file=sys.stderr)
64
+ sys.exit(1)
65
+ if d.get("errors_seen"):
66
+ print(f"FAILED: {len(d['errors_seen'])} engine error(s) during the run", file=sys.stderr)
67
+ sys.exit(1)
68
+ if reports.get("error"):
69
+ print(f"FAILED: {reports['error']}", file=sys.stderr)
70
+ print("Edit the CONFIG block at the top of tests/autopilot/autopilot.gd.", file=sys.stderr)
71
+ sys.exit(1)
72
+ # Any waypoint the autopilot could not reach is a failure, whatever else passed.
73
+ missed = [k for k, v in reports.items() if k.endswith("_reached") and v is False]
74
+ if missed:
75
+ print(f"FAILED: waypoints not reached: {', '.join(sorted(missed))}", file=sys.stderr)
76
+ sys.exit(1)
77
+ PY
78
+ fi
@@ -6,9 +6,18 @@ This is the meta-skill: the loop that turns ad-hoc fixes into durable, reusable
6
6
 
7
7
  Three repos, one ecosystem:
8
8
 
9
- 1. **`~/development/SummerEngine`** — the desktop engine (Godot fork in C++) + the canonical CLI. The engine source lives at `editor/`, `core/`, and the Summer-specific module at `modules/1summer_engine/`. The Summer CLI lives at `tools/summer-cli/`. It ships as `npm i -g summer-engine`. It does install/login/create/run for the engine, plus `summer skills install --as-claude-skill` which writes a curated subset of skills into `~/.claude/skills/` for any agent to auto-discover.
9
+ 1. **`~/development/SummerEngine`** — the Summer Engine desktop source and the
10
+ canonical CLI. The C++ engine is maintained against the upstream Godot
11
+ Engine codebase; the Summer-owned module lives at `modules/1summer_engine/`.
12
+ The Summer CLI lives at `tools/summer-cli/` and is normally invoked with
13
+ `npx -y summer-engine@latest`. It installs, signs in, creates, and runs Summer
14
+ projects and can install the current Summer skill bundle for supported
15
+ agents.
10
16
  2. **`~/development/PublicSummerEngine`** — the web app at summerengine.com. Has its own AI skill stores at `src/lib/ai/skills/bundled/` and `public/knowledge/summer/skills/` for the in-browser chat agent. ALSO has its own MCP-style consumers of the engine API (`src/lib/bridge/direct-executor.ts`, `src/lib/ai/tools/run-and-verify.ts`) which call the same engine endpoints the CLI does.
11
- 3. **The active game project** — usually `~/development/<game-name>` (Godot 4.6) or whatever game we were just working on. Real working code in this repo is the gold standard for examples.
17
+ 3. **The active Summer game project** — usually
18
+ `~/development/<game-name>` or whatever game we were just working on. The
19
+ live compatibility line comes from Summer Engine, not this prompt. Real
20
+ working code in this repo is the gold standard for examples.
12
21
 
13
22
  The skill system is the value flywheel: every game we ship teaches lessons → the lessons become skills → the next user (or AI) starts smarter. Your job here is to close that loop for what we just did.
14
23
 
@@ -18,6 +18,10 @@ import { doctorCommand } from "../commands/doctor.js";
18
18
  import { planCommand } from "../commands/plan.js";
19
19
  import { cloudCommand } from "../commands/cloud.js";
20
20
  import { debugCommand } from "../commands/debug.js";
21
+ import { configCommand } from "../commands/config.js";
22
+ import { publishCommand } from "../commands/publish.js";
23
+ import { releasesCommand } from "../commands/releases.js";
24
+ import { logsCommand } from "../commands/logs.js";
21
25
  import { getBanner } from "../lib/banner.js";
22
26
  import { c, sym } from "../lib/format.js";
23
27
  const require = createRequire(import.meta.url);
@@ -47,6 +51,10 @@ program.addCommand(debugCommand);
47
51
  program.addCommand(planCommand);
48
52
  program.addCommand(orchestratorCommand);
49
53
  program.addCommand(cloudCommand);
54
+ program.addCommand(configCommand);
55
+ program.addCommand(publishCommand);
56
+ program.addCommand(releasesCommand);
57
+ program.addCommand(logsCommand);
50
58
  program.parseAsync().catch((err) => {
51
59
  console.error(err instanceof Error ? err.message : String(err));
52
60
  process.exit(1);
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare const configCommand: Command;
@@ -0,0 +1,53 @@
1
+ import { Command } from "commander";
2
+ import { CONFIG_KEYS, getConfigValue, isConfigKey, readSummerConfig, setConfigValue, unsetConfigValue, } from "../lib/config.js";
3
+ function requireKey(value) {
4
+ if (isConfigKey(value))
5
+ return value;
6
+ throw new Error(`Unknown config key "${value}". Recovery: use one of ${CONFIG_KEYS.join(", ")}.`);
7
+ }
8
+ async function printConfig(json) {
9
+ const config = await readSummerConfig();
10
+ const values = Object.fromEntries(CONFIG_KEYS.map((key) => [key, getConfigValue(config, key) ?? null]));
11
+ if (json) {
12
+ console.log(JSON.stringify(values, null, 2));
13
+ return;
14
+ }
15
+ for (const [key, value] of Object.entries(values)) {
16
+ console.log(`${key}=${value ?? "(not set)"}`);
17
+ }
18
+ }
19
+ export const configCommand = new Command("config")
20
+ .description("Read or update the shared ~/.summer configuration")
21
+ .option("--json", "Print all configuration as JSON")
22
+ .action(async (opts) => printConfig(Boolean(opts.json)));
23
+ configCommand
24
+ .command("get")
25
+ .description("Read one configuration value")
26
+ .argument("<key>", `One of: ${CONFIG_KEYS.join(", ")}`)
27
+ .action(async (rawKey) => {
28
+ const key = requireKey(rawKey);
29
+ const value = getConfigValue(await readSummerConfig(), key);
30
+ if (value === undefined) {
31
+ throw new Error(`${key} is not set. Recovery: run "summer config set ${key} <value>", or rely on the documented default.`);
32
+ }
33
+ console.log(value);
34
+ });
35
+ configCommand
36
+ .command("set")
37
+ .description("Set one non-secret configuration value")
38
+ .argument("<key>", `One of: ${CONFIG_KEYS.join(", ")}`)
39
+ .argument("<value>")
40
+ .action(async (rawKey, value) => {
41
+ const key = requireKey(rawKey);
42
+ await setConfigValue(key, value);
43
+ console.log(`Set ${key}.`);
44
+ });
45
+ configCommand
46
+ .command("unset")
47
+ .description("Remove one configuration override")
48
+ .argument("<key>", `One of: ${CONFIG_KEYS.join(", ")}`)
49
+ .action(async (rawKey) => {
50
+ const key = requireKey(rawKey);
51
+ await unsetConfigValue(key);
52
+ console.log(`Unset ${key}.`);
53
+ });
@@ -1,2 +1,3 @@
1
1
  import { Command } from "commander";
2
2
  export declare const createCommand: Command;
3
+ export declare function renderProjectSettings(name: string, mainScene: string): string;
@@ -1,14 +1,16 @@
1
1
  import { Command } from "commander";
2
- import { existsSync, mkdirSync, writeFileSync } from "fs";
3
- import { join, resolve } from "path";
2
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from "fs";
3
+ import { dirname, join, resolve } from "path";
4
+ import { fileURLToPath } from "url";
4
5
  import { fetchRemoteTemplates, cloneTemplate, matchTemplate } from "../lib/remote-templates.js";
6
+ import { SUMMER_ENGINE_COMPATIBILITY } from "../lib/summer-compatibility.js";
5
7
  const BUILTIN_TEMPLATES = [
6
8
  {
7
9
  name: "empty",
8
10
  description: "Empty 3D project with just a root node",
9
11
  builtin: true,
10
12
  generate: (dir, name) => {
11
- writeFileSync(join(dir, "project.godot"), projectGodot(name, "res://main.tscn"));
13
+ writeFileSync(join(dir, "project.godot"), renderProjectSettings(name, "res://main.tscn"));
12
14
  writeFileSync(join(dir, "main.tscn"), emptyScene());
13
15
  },
14
16
  },
@@ -17,11 +19,34 @@ const BUILTIN_TEMPLATES = [
17
19
  description: "3D scene with camera, light, and floor",
18
20
  builtin: true,
19
21
  generate: (dir, name) => {
20
- writeFileSync(join(dir, "project.godot"), projectGodot(name, "res://main.tscn"));
22
+ writeFileSync(join(dir, "project.godot"), renderProjectSettings(name, "res://main.tscn"));
21
23
  writeFileSync(join(dir, "main.tscn"), basicScene3D());
22
24
  },
23
25
  },
24
26
  ];
27
+ /**
28
+ * Copy the autopilot verification scaffold into a fresh project.
29
+ *
30
+ * Every project gets a working example of how to prove its own gameplay: a probe
31
+ * that drives the player with real input, saves rendered frames, asserts, and
32
+ * exits. Without it every agent reinvents verification from scratch, and most
33
+ * settle for asking the user to play the game instead.
34
+ *
35
+ * Never overwrites — an existing tests/autopilot/ is the user's, not ours.
36
+ */
37
+ function scaffoldAutopilot(projectDir) {
38
+ const here = dirname(fileURLToPath(import.meta.url));
39
+ // dist/commands/create.js -> package root
40
+ const source = resolve(here, "..", "..", "assets", "autopilot");
41
+ const target = join(projectDir, "tests", "autopilot");
42
+ if (!existsSync(source) || existsSync(target))
43
+ return false;
44
+ mkdirSync(target, { recursive: true });
45
+ for (const entry of readdirSync(source)) {
46
+ copyFileSync(join(source, entry), join(target, entry));
47
+ }
48
+ return true;
49
+ }
25
50
  export const createCommand = new Command("create")
26
51
  .description("Create a new Summer Engine project. Built-in templates work offline; community templates clone from github.com/SummerEngine/template-*.")
27
52
  .argument("<template>", `Template slug. Built-in: ${BUILTIN_TEMPLATES.map((t) => t.name).join(", ")}. Or any community slug — see "summer list templates".`)
@@ -39,9 +64,12 @@ export const createCommand = new Command("create")
39
64
  console.log(`Creating project from built-in '${templateName}' template...`);
40
65
  mkdirSync(fullPath, { recursive: true });
41
66
  builtin.generate(fullPath, dirName);
67
+ const scaffolded = scaffoldAutopilot(fullPath);
42
68
  console.log(`\nProject created at ${fullPath}`);
43
69
  console.log("\nNext steps:");
44
70
  console.log(` summer run ${dirName}`);
71
+ if (scaffolded)
72
+ console.log(` bash ${dirName}/tests/autopilot/run.sh (verify the game without opening it)`);
45
73
  console.log(" Ask your agent to run summer:brainstorm-game to create .summer/GameSoul.md");
46
74
  return;
47
75
  }
@@ -83,21 +111,25 @@ export const createCommand = new Command("create")
83
111
  console.error(err.message);
84
112
  process.exit(1);
85
113
  }
114
+ const scaffolded = scaffoldAutopilot(fullPath);
86
115
  console.log(`\nProject created at ${fullPath}`);
87
116
  console.log("Source: " + remote.url);
88
117
  console.log("\nNext steps:");
89
118
  console.log(` summer run ${dirName}`);
119
+ if (scaffolded)
120
+ console.log(` bash ${dirName}/tests/autopilot/run.sh (verify the game without opening it)`);
90
121
  console.log(" Ask your agent to run summer:brainstorm-game to create .summer/GameSoul.md");
91
122
  });
92
- function projectGodot(name, mainScene) {
123
+ export function renderProjectSettings(name, mainScene) {
93
124
  return `; Summer Engine Project
94
125
  ; Generated by summer-engine CLI
126
+ ; Technical base ${SUMMER_ENGINE_COMPATIBILITY.currentTechnicalBaseVersion}; Summer follows upstream continuously
95
127
 
96
128
  [application]
97
129
 
98
130
  config/name="${name}"
99
131
  run/main_scene="${mainScene}"
100
- config/features=PackedStringArray("4.5")
132
+ config/features=PackedStringArray("${SUMMER_ENGINE_COMPATIBILITY.projectFeatureTag}")
101
133
 
102
134
  [rendering]
103
135
 
@@ -55,7 +55,7 @@ function listProjects() {
55
55
  }
56
56
  }
57
57
  if (projects.length === 0) {
58
- console.log("No Summer Engine / Godot projects found in current directory.");
58
+ console.log("No Summer Engine projects found in current directory.");
59
59
  console.log("\nCreate one: summer create 3d-basic my-game");
60
60
  return;
61
61
  }
@@ -1,2 +1,21 @@
1
1
  import { Command } from "commander";
2
2
  export declare const loginCommand: Command;
3
+ export interface CreatorLoginDependencies {
4
+ openUrl: (url: string) => Promise<unknown>;
5
+ readSecret: (prompt: string) => Promise<string>;
6
+ log: (message: string) => void;
7
+ }
8
+ /**
9
+ * The browser session mints the token; the token is pasted once into a hidden
10
+ * prompt. This deliberately does not replace the core Summer login JWT.
11
+ */
12
+ export declare function runCreatorLogin(overrides?: Partial<CreatorLoginDependencies>): Promise<void>;
13
+ export interface LoginDependencies {
14
+ fetch: typeof fetch;
15
+ openUrl: (url: string) => Promise<unknown>;
16
+ randomId: () => string;
17
+ sleep: (ms: number) => Promise<void>;
18
+ now: () => number;
19
+ log: (message: string) => void;
20
+ }
21
+ export declare function runLogin(overrides?: Partial<LoginDependencies>): Promise<void>;