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.
- package/.claude-plugin/plugin.json +2 -1
- package/.codex-plugin/plugin.json +3 -2
- package/.cursor-plugin/plugin.json +2 -1
- package/.opencode/INSTALL.md +1 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +4 -4
- package/CHANGELOG.md +28 -0
- package/GEMINI.md +5 -4
- package/README.md +58 -21
- package/assets/autopilot/README.md +51 -0
- package/assets/autopilot/autopilot.gd +222 -0
- package/assets/autopilot/probe_base.gd +262 -0
- package/assets/autopilot/run.sh +78 -0
- package/commands/gameskill.md +11 -2
- package/dist/bin/summer.js +8 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/create.d.ts +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/list.js +1 -1
- package/dist/commands/login.d.ts +19 -0
- package/dist/commands/login.js +136 -37
- package/dist/commands/logout.js +3 -16
- package/dist/commands/logs.d.ts +2 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/mcp.js +7 -2
- package/dist/commands/open.js +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +20 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +20 -0
- package/dist/commands/run.js +1 -1
- package/dist/lib/api-client.d.ts +27 -8
- package/dist/lib/api-client.js +65 -14
- package/dist/lib/auth.d.ts +40 -12
- package/dist/lib/auth.js +173 -37
- package/dist/lib/cloud/engine-bridge.d.ts +17 -4
- package/dist/lib/cloud/engine-bridge.js +32 -6
- package/dist/lib/config.d.ts +20 -0
- package/dist/lib/config.js +142 -0
- package/dist/lib/creator.d.ts +93 -0
- package/dist/lib/creator.js +393 -0
- package/dist/lib/engine.d.ts +35 -2
- package/dist/lib/engine.js +240 -6
- package/dist/lib/public-surface-paths.d.ts +13 -0
- package/dist/lib/public-surface-paths.js +42 -0
- package/dist/lib/skills-registry.d.ts +8 -0
- package/dist/lib/skills-registry.js +9 -0
- package/dist/lib/store.d.ts +14 -0
- package/dist/lib/store.js +145 -0
- package/dist/lib/summer-compatibility.d.ts +6 -0
- package/dist/lib/summer-compatibility.js +22 -0
- package/dist/mcp/server.d.ts +8 -1
- package/dist/mcp/server.js +25 -4
- package/dist/mcp/tools/creator-tools.d.ts +2 -0
- package/dist/mcp/tools/creator-tools.js +105 -0
- package/dist/mcp/tools/debug-tools.d.ts +12 -0
- package/dist/mcp/tools/debug-tools.js +100 -1
- package/dist/mcp/tools/generate-tools.js +16 -6
- package/dist/mcp/tools/project-tools.js +131 -16
- package/dist/mcp/tools/scene-tools.js +166 -66
- package/dist/mcp/tools/visual-tools.js +32 -8
- package/dist/mcp/tools/with-engine.js +51 -19
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +5 -5
- package/package.json +3 -1
- package/references/collaborative-protocol.md +50 -0
- package/references/gd-style.md +121 -0
- package/references/godot-version.md +51 -0
- package/references/mcp-tools-reference.md +199 -0
- package/references/public-surface-language.md +30 -0
- package/references/summer-folder.md +90 -0
- package/references/template-registry.md +73 -0
- package/skills/2d-assets/character-portrait/SKILL.md +17 -12
- package/skills/2d-assets/concept-art/SKILL.md +10 -9
- package/skills/2d-assets/pixel-art/SKILL.md +36 -20
- package/skills/2d-assets/skybox-panorama/SKILL.md +32 -37
- package/skills/2d-assets/sprite-sheet/SKILL.md +40 -35
- package/skills/2d-assets/tileable-texture/SKILL.md +28 -22
- package/skills/2d-assets/ui-graphics/SKILL.md +46 -32
- package/skills/2d-assets/use-widget-asset/SKILL.md +8 -6
- package/skills/3d-assets/character-model/SKILL.md +34 -19
- package/skills/3d-assets/environment-kit/SKILL.md +11 -5
- package/skills/3d-assets/organic-model/SKILL.md +8 -5
- package/skills/3d-assets/prop-model/SKILL.md +9 -3
- package/skills/3d-assets/vehicle-model/SKILL.md +32 -19
- package/skills/README.md +1 -1
- package/skills/ai-and-npcs/design-npc/SKILL.md +31 -13
- package/skills/animation/animation-tree/SKILL.md +39 -15
- package/skills/animation/facial-and-lipsync/SKILL.md +25 -8
- package/skills/animation/generate-motion/SKILL.md +47 -15
- package/skills/animation/procedural-animation/SKILL.md +31 -13
- package/skills/animation/retarget/SKILL.md +127 -79
- package/skills/audio/adaptive-music/SKILL.md +24 -20
- package/skills/audio/ambient-bed/SKILL.md +22 -16
- package/skills/audio/audio-direction/SKILL.md +15 -10
- package/skills/audio/music-track/SKILL.md +18 -12
- package/skills/audio/sound-effect/SKILL.md +17 -12
- package/skills/audio/voice-line/SKILL.md +42 -42
- package/skills/character-controllers/fps-controller/SKILL.md +26 -16
- package/skills/debugging/debug/SKILL.md +52 -20
- package/skills/deployment/export-and-ship/SKILL.md +105 -38
- package/skills/deployment/remote-deploy/SKILL.md +56 -23
- package/skills/gameplay-mechanics/auto-fire-targeting/SKILL.md +26 -1
- package/skills/gameplay-mechanics/design-mechanic/SKILL.md +23 -15
- package/skills/level-design/design-level/SKILL.md +22 -8
- package/skills/level-design/scene-to-level/SKILL.md +13 -7
- package/skills/multiplayer-and-networking/host-authoritative-state/SKILL.md +8 -8
- package/skills/multiplayer-and-networking/peer-to-peer-multiplayer/SKILL.md +2 -7
- package/skills/multiplayer-and-networking/setup-multiplayer/SKILL.md +26 -18
- package/skills/performance/tune-performance/SKILL.md +51 -32
- package/skills/rendering-and-lighting/3d-lighting/SKILL.md +34 -5
- package/skills/rendering-and-lighting/art-direction/SKILL.md +6 -8
- package/skills/scene-and-project/brainstorm-game/SKILL.md +3 -5
- package/skills/scene-and-project/make-game/SKILL.md +2 -1
- package/skills/scene-and-project/play/SKILL.md +10 -4
- package/skills/scene-and-project/scene-composition/SKILL.md +21 -8
- package/skills/scene-and-project/summer-cloud/SKILL.md +1 -1
- package/skills/ui-and-ux/ui-basics/SKILL.md +16 -7
- package/skills/video/animated-loop/SKILL.md +63 -39
- package/skills/video/cinematic-cutscene/SKILL.md +66 -30
- package/skills/video/trailer-shot/SKILL.md +30 -21
- package/skills/visual-effects/_building-blocks/additive-billboard-particles.md +25 -16
- package/skills/visual-effects/_building-blocks/trauma-shake-snippet.md +5 -3
- package/skills/visual-effects/game-feel/SKILL.md +51 -33
- package/skills/visual-effects/recipes/dissolve/SKILL.md +51 -9
- package/skills/visual-effects/recipes/fire/SKILL.md +136 -49
- package/skills/visual-effects/recipes/hit-spark/SKILL.md +45 -8
- package/skills/visual-effects/recipes/lightning/SKILL.md +59 -27
- package/skills/visual-effects/recipes/magic-glow/SKILL.md +59 -26
- package/skills/visual-effects/recipes/muzzle-flash/SKILL.md +68 -25
- package/skills/visual-effects/recipes/smoke/SKILL.md +89 -28
- package/skills/visual-effects/recipes/water-ripple/SKILL.md +83 -23
- package/skills/workflow/brainstorming/SKILL.md +1 -1
- package/skills/workflow/brainstorming/scripts/start-server.sh +0 -0
- package/skills/workflow/brainstorming/scripts/stop-server.sh +0 -0
- package/skills/workflow/debugging-game-feel/SKILL.md +29 -9
- package/skills/workflow/diagnosing-perf-regressions/SKILL.md +46 -21
- package/skills/workflow/gameskill/SKILL.md +11 -2
- package/skills/workflow/headless-scripting/SKILL.md +407 -0
- package/skills/workflow/investigating-bugs/SKILL.md +4 -2
- package/skills/workflow/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +3 -3
- package/skills/workflow/playtesting-a-feature/SKILL.md +94 -21
- package/skills/workflow/skill-create/SKILL.md +7 -7
- package/skills/workflow/skill-test/SKILL.md +3 -3
- package/skills/workflow/using-summer/SKILL.md +12 -2
- package/skills/workflow/verification-before-completion/SKILL.md +2 -0
- package/skills/workflow/writing-plans/SKILL.md +7 -7
- package/skills/workflow/writing-skills/SKILL.md +7 -5
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
- 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
|
-
|
|
4
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
63
|
-
3) Call
|
|
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
|
-
.
|
|
69
|
-
.describe("
|
|
70
|
-
}, async ({ path, rootName,
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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:
|
|
195
|
+
created: safePath,
|
|
103
196
|
rootName,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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("
|
|
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
|
|
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
|
|
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 —
|
|
264
|
-
-
|
|
265
|
-
-
|
|
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(
|
|
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,
|
|
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
|
|
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.
|
|
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", "
|
|
32
|
+
.enum(["auto", "iso", "top", "front", "back", "left", "right"])
|
|
32
33
|
.optional()
|
|
33
|
-
.describe('target:"scene" only. Camera
|
|
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.
|
|
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
|
|
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
|
-
|
|
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:
|
|
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),
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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"
|
|
76
|
-
// omitted an error string — surface it rather
|
|
77
|
-
// contract `isFailureSignal`).
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
package/hooks/run-hook.cmd
CHANGED
|
File without changes
|
package/hooks/session-start.sh
CHANGED
|
@@ -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
|
|
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:
|
|
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+=("
|
|
51
|
+
LINES+=("Summer project: ${PROJECT_NAME}. GameSoul: ${SOUL_LINE}")
|
|
52
52
|
else
|
|
53
|
-
LINES+=("
|
|
53
|
+
LINES+=("Summer project: ${PROJECT_NAME}.")
|
|
54
54
|
fi
|
|
55
55
|
else
|
|
56
|
-
LINES+=("
|
|
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.
|
|
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.
|