summer-engine 2.6.6 → 2.7.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +3 -1
- package/.codex-plugin/plugin.json +5 -2
- package/.cursor-plugin/plugin.json +4 -1
- package/.opencode/plugins/summer.js +1 -1
- package/AGENTS.md +8 -4
- package/CHANGELOG.md +30 -0
- package/GEMINI.md +5 -4
- package/README.md +7 -4
- package/_persona/summer/SKILL.md +1 -0
- package/commands/summer.md +13 -0
- package/dist/bin/summer.js +4 -0
- package/dist/commands/debug.d.ts +2 -0
- package/dist/commands/debug.js +44 -0
- package/dist/commands/doctor.js +112 -8
- package/dist/commands/skills.js +1 -1
- package/dist/lib/api-client.d.ts +11 -2
- package/dist/lib/api-client.js +110 -9
- package/dist/lib/async-op-lifecycle.d.ts +7 -2
- package/dist/lib/async-op-lifecycle.js +32 -9
- package/dist/lib/debug-report.d.ts +50 -0
- package/dist/lib/debug-report.js +308 -0
- package/dist/lib/game-task-plan.js +4 -4
- package/dist/lib/mcp-boot-notice.d.ts +3 -0
- package/dist/lib/mcp-boot-notice.js +7 -0
- package/dist/lib/mcp-log.d.ts +3 -0
- package/dist/lib/mcp-log.js +72 -0
- package/dist/lib/skills-registry.d.ts +25 -1
- package/dist/lib/skills-registry.js +46 -0
- package/dist/mcp/server.d.ts +2 -2
- package/dist/mcp/server.js +79 -7
- package/dist/mcp/tools/asset-tools.js +33 -12
- package/dist/mcp/tools/cloud-tools.js +15 -15
- package/dist/mcp/tools/debug-tools.js +44 -1
- package/dist/mcp/tools/file-tools.d.ts +2 -0
- package/dist/mcp/tools/file-tools.js +154 -0
- package/dist/mcp/tools/generate-tools.js +143 -5
- package/dist/mcp/tools/project-tools.js +25 -20
- package/dist/mcp/tools/scene-tools.js +101 -24
- package/dist/mcp/tools/with-engine.js +4 -4
- package/hooks/pre-commit-doctor.sh +0 -0
- package/hooks/run-hook.cmd +0 -0
- package/hooks/session-start.sh +0 -0
- package/package.json +1 -1
- package/skills/2d-assets/create-asset-sheet/SKILL.md +17 -15
- package/skills/debugging/debug/SKILL.md +23 -1
- package/skills/deployment/remote-deploy/SKILL.md +107 -0
- package/skills/scene-and-project/brainstorm-game/SKILL.md +4 -4
- package/skills/scene-and-project/summer-cloud/SKILL.md +92 -0
- package/skills/scripting-patterns/gdscript-patterns/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/investigating-bugs/find-polluter.sh +0 -0
- package/skills/workflow/mcpupdate/SKILL.md +6 -6
- package/skills/workflow/using-summer/SKILL.md +20 -1
- package/skills/workflow/writing-skills/render-graphs.js +0 -0
package/dist/lib/api-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, writeFile } from "fs/promises";
|
|
2
2
|
import { tmpdir } from "os";
|
|
3
3
|
import { join } from "path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
4
5
|
import { getApiToken, getApiPort, checkEngineHealth } from "./engine.js";
|
|
5
6
|
import { classifyOpsResponse, pollOpToTerminal, } from "./async-op-lifecycle.js";
|
|
6
7
|
function asRecord(value) {
|
|
@@ -131,6 +132,13 @@ export class EngineApiClient {
|
|
|
131
132
|
const projectIdHash = this.targetIdentity.projectIdHash;
|
|
132
133
|
return projectIdHash ? { projectIdHash } : {};
|
|
133
134
|
}
|
|
135
|
+
requireBoundIdentity() {
|
|
136
|
+
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
137
|
+
if (!instanceId || !projectId || !projectIdHash) {
|
|
138
|
+
throw new Error("Safe project mutation requires a complete engine/project identity. Call summer_get_project_context to bind this MCP session, then retry. Nothing was written.");
|
|
139
|
+
}
|
|
140
|
+
return { projectIdHash };
|
|
141
|
+
}
|
|
134
142
|
targetUrl(path) {
|
|
135
143
|
const url = new URL(path, `http://127.0.0.1:${this.port}`);
|
|
136
144
|
const { instanceId, projectId, projectIdHash } = this.targetIdentity;
|
|
@@ -190,24 +198,98 @@ export class EngineApiClient {
|
|
|
190
198
|
* result (HTTP 200 — dormant/older engine) OR the async lifecycle (202
|
|
191
199
|
* {requestId} -> long-poll /api/ops/result until terminal). Compatible with
|
|
192
200
|
* both — inspects the HTTP status, not a flag. `timeoutMs` is the poll loop's
|
|
193
|
-
*
|
|
201
|
+
* client wait budget; expiry preserves the requestId and last known lifecycle
|
|
202
|
+
* state instead of pretending the accepted operation did not apply.
|
|
194
203
|
*/
|
|
195
204
|
async _requestQueued(method, path, body, timeoutMs) {
|
|
196
|
-
|
|
205
|
+
// Mint the request ID before submission. If the acknowledgement or a later
|
|
206
|
+
// poll is lost, callers retain the exact identity needed to reconcile the
|
|
207
|
+
// accepted lifecycle instead of issuing an unsafe blind retry.
|
|
208
|
+
let clientRequestId;
|
|
209
|
+
let requestBody = body;
|
|
210
|
+
if (method === "POST") {
|
|
211
|
+
const bodyRecord = asRecord(body) ?? {};
|
|
212
|
+
const existingOptions = asRecord(bodyRecord.options) ?? {};
|
|
213
|
+
clientRequestId =
|
|
214
|
+
stringFrom(existingOptions.requestId) ?? `mcp-${randomUUID()}`;
|
|
215
|
+
requestBody = {
|
|
216
|
+
...bodyRecord,
|
|
217
|
+
options: {
|
|
218
|
+
...existingOptions,
|
|
219
|
+
requestId: clientRequestId,
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
let res;
|
|
224
|
+
try {
|
|
225
|
+
res = await this._fetchRaw(method, path, requestBody, timeoutMs);
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
if (!clientRequestId)
|
|
229
|
+
throw error;
|
|
230
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
231
|
+
throw new Error(`Summer Engine did not acknowledge request ${clientRequestId}; dispatch state is unknown. Do not retry blindly. ${detail}`);
|
|
232
|
+
}
|
|
197
233
|
// 202 (queued) + 429 (backpressure, errorClass in body) are handled below;
|
|
198
234
|
// any other non-2xx is a real transport error.
|
|
199
235
|
if (!res.ok && res.status !== 202 && res.status !== 429) {
|
|
200
236
|
const text = await res.text().catch(() => "");
|
|
201
|
-
|
|
237
|
+
const suffix = clientRequestId ? ` (requestId ${clientRequestId})` : "";
|
|
238
|
+
throw new Error(`Engine API error ${res.status}${suffix}: ${text.slice(0, 200)}`);
|
|
202
239
|
}
|
|
203
240
|
const respBody = await res.json().catch(() => ({}));
|
|
204
241
|
const classified = classifyOpsResponse(res.status, respBody);
|
|
205
242
|
if (classified.mode === "legacy") {
|
|
243
|
+
if (res.status === 429) {
|
|
244
|
+
return {
|
|
245
|
+
...(asRecord(respBody) ?? {}),
|
|
246
|
+
requestId: clientRequestId,
|
|
247
|
+
terminalState: stringFrom(asRecord(respBody)?.terminalState) ?? "queue_full",
|
|
248
|
+
dispatchState: "not_sent",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
206
251
|
return respBody;
|
|
207
252
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
253
|
+
// The acknowledgement is authoritative for polling. Current engines echo
|
|
254
|
+
// the client-generated ID, while older engines may ignore options.requestId
|
|
255
|
+
// and mint their own; polling the submitted ID against those engines would
|
|
256
|
+
// wait forever on an unknown lifecycle.
|
|
257
|
+
const acceptedRequestId = classified.requestId || clientRequestId;
|
|
258
|
+
if (!acceptedRequestId) {
|
|
259
|
+
return {
|
|
260
|
+
status: "error",
|
|
261
|
+
error: "Summer Engine accepted an operation without returning a requestId; final state is uncertain.",
|
|
262
|
+
terminalState: "uncertain",
|
|
263
|
+
errorClass: "transient",
|
|
264
|
+
dispatchState: "uncertain",
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const terminal = await pollOpToTerminal((waitMs) => this._pollResult(acceptedRequestId, waitMs), {
|
|
269
|
+
totalTimeoutMs: timeoutMs,
|
|
270
|
+
requestId: acceptedRequestId,
|
|
271
|
+
});
|
|
272
|
+
if (clientRequestId && clientRequestId !== acceptedRequestId) {
|
|
273
|
+
terminal.submissionRequestId = clientRequestId;
|
|
274
|
+
}
|
|
275
|
+
return terminal;
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
279
|
+
return {
|
|
280
|
+
status: "error",
|
|
281
|
+
error: `Summer Engine accepted request ${acceptedRequestId}, but its final receipt could not be retrieved. ` +
|
|
282
|
+
`The operation may still be running or may already have applied; inspect the target before retrying. ${detail}`,
|
|
283
|
+
terminalState: "uncertain",
|
|
284
|
+
errorClass: "transient",
|
|
285
|
+
dispatchState: "uncertain",
|
|
286
|
+
requestId: acceptedRequestId,
|
|
287
|
+
...(clientRequestId && clientRequestId !== acceptedRequestId
|
|
288
|
+
? { submissionRequestId: clientRequestId }
|
|
289
|
+
: {}),
|
|
290
|
+
lastKnownStatus: "unknown",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
211
293
|
}
|
|
212
294
|
async health() {
|
|
213
295
|
return this.request("GET", "/api/health");
|
|
@@ -218,12 +300,31 @@ export class EngineApiClient {
|
|
|
218
300
|
// async-202 (Block E).
|
|
219
301
|
// Attach the bound project identity so the engine rejects a write aimed at a
|
|
220
302
|
// different project (identity_mismatch, atomic — before any op applies).
|
|
221
|
-
const merged = { ...
|
|
303
|
+
const merged = { ...(options ?? {}), ...this.identityOptions() };
|
|
222
304
|
const body = Object.keys(merged).length ? { ops, options: merged } : { ops };
|
|
223
305
|
return this._requestQueued("POST", "/api/ops", body, 120_000);
|
|
224
306
|
}
|
|
225
|
-
|
|
226
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Mutation path for MCP file tools. Unlike legacy executeOps callers, this
|
|
309
|
+
* fails closed when health did not provide a complete target identity. The
|
|
310
|
+
* bound hash is stamped last so a caller cannot override it in options.
|
|
311
|
+
*/
|
|
312
|
+
async executeIdentityBoundOps(ops, options) {
|
|
313
|
+
const identity = this.requireBoundIdentity();
|
|
314
|
+
return this.executeOps(ops, { ...(options ?? {}), ...identity });
|
|
315
|
+
}
|
|
316
|
+
async readProjectFile(path, maxBytes = 200_000) {
|
|
317
|
+
const query = new URLSearchParams({
|
|
318
|
+
path,
|
|
319
|
+
maxBytes: String(maxBytes),
|
|
320
|
+
});
|
|
321
|
+
return this.request("GET", `/api/state/read-file?${query.toString()}`);
|
|
322
|
+
}
|
|
323
|
+
async getSceneState(scenePath) {
|
|
324
|
+
const query = scenePath
|
|
325
|
+
? `?scene=${encodeURIComponent(scenePath)}`
|
|
326
|
+
: "";
|
|
327
|
+
return this.request("GET", `/api/state/scene${query}`);
|
|
227
328
|
}
|
|
228
329
|
async getProjectState() {
|
|
229
330
|
return this.request("GET", "/api/state/project");
|
|
@@ -40,6 +40,10 @@ export declare function classifyOpsResponse(httpStatus: number, body: unknown):
|
|
|
40
40
|
export interface PollOpts {
|
|
41
41
|
/** Hard wall-clock cap on the whole poll loop (ms). The per-tool budget. */
|
|
42
42
|
totalTimeoutMs: number;
|
|
43
|
+
/** Stable client-generated correlation ID. Preserved in non-terminal timeout
|
|
44
|
+
* receipts so callers can reconcile the accepted operation instead of
|
|
45
|
+
* blindly retrying it. */
|
|
46
|
+
requestId?: string;
|
|
43
47
|
/** Give up if the op hasn't advanced for this long (ms). Default = totalTimeoutMs. */
|
|
44
48
|
noProgressTimeoutMs?: number;
|
|
45
49
|
/** Server long-poll wait per GET (ms). Default 5000 — the engine holds the
|
|
@@ -54,7 +58,8 @@ export interface PollOpts {
|
|
|
54
58
|
/**
|
|
55
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
56
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
57
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
58
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
59
64
|
*/
|
|
60
65
|
export declare function pollOpToTerminal(pollOnce: (waitMs: number) => Promise<OpResultEnvelope>, opts: PollOpts): Promise<Record<string, unknown>>;
|
|
@@ -30,17 +30,37 @@ export function classifyOpsResponse(httpStatus, body) {
|
|
|
30
30
|
}
|
|
31
31
|
return { mode: "legacy", legacyResult: body };
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
function incompleteResult(requestId, lastStatus) {
|
|
34
|
+
if (lastStatus === "queued") {
|
|
35
|
+
return {
|
|
36
|
+
status: "error",
|
|
37
|
+
error: `Summer Engine accepted request ${requestId ?? "(unknown)"}, but it was still queued when this tool stopped waiting. ` +
|
|
38
|
+
"It may still run; do not retry blindly.",
|
|
39
|
+
terminalState: "still_queued",
|
|
40
|
+
errorClass: "transient",
|
|
41
|
+
dispatchState: "accepted",
|
|
42
|
+
requestId,
|
|
43
|
+
lastKnownStatus: "queued",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
const wasRunning = lastStatus === "running";
|
|
47
|
+
return {
|
|
48
|
+
status: "error",
|
|
49
|
+
error: `Summer Engine did not provide a final receipt for request ${requestId ?? "(unknown)"}. ` +
|
|
50
|
+
`${wasRunning ? "The operation was still running" : "Its dispatch state is uncertain"}; inspect the target before retrying.`,
|
|
51
|
+
terminalState: wasRunning ? "still_running" : "uncertain",
|
|
52
|
+
errorClass: "transient",
|
|
53
|
+
dispatchState: wasRunning ? "sent" : "uncertain",
|
|
54
|
+
requestId,
|
|
55
|
+
lastKnownStatus: lastStatus ?? "unknown",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
39
58
|
/**
|
|
40
59
|
* Drive an async op to a terminal result by polling. `pollOnce(waitMs)` performs
|
|
41
60
|
* one GET /api/ops/result (long-poll up to waitMs). Returns the engine's apply
|
|
42
|
-
* dict with terminalState/errorClass/appliedSeq merged on top, or a
|
|
43
|
-
* `
|
|
61
|
+
* dict with terminalState/errorClass/appliedSeq merged on top, or a truthful
|
|
62
|
+
* non-terminal `still_queued` / `still_running` / `uncertain` receipt when the
|
|
63
|
+
* client budget expires before the engine publishes a terminal.
|
|
44
64
|
*/
|
|
45
65
|
export async function pollOpToTerminal(pollOnce, opts) {
|
|
46
66
|
const now = opts.now ?? (() => Date.now());
|
|
@@ -57,6 +77,9 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
57
77
|
const env = await pollOnce(pollWaitMs);
|
|
58
78
|
if (isTerminalStatus(env.status)) {
|
|
59
79
|
const out = { ...(env.result ?? {}) };
|
|
80
|
+
const requestId = opts.requestId ?? env.requestId;
|
|
81
|
+
if (requestId)
|
|
82
|
+
out.requestId = requestId;
|
|
60
83
|
if (env.terminalState)
|
|
61
84
|
out.terminalState = env.terminalState;
|
|
62
85
|
if (env.errorClass)
|
|
@@ -76,7 +99,7 @@ export async function pollOpToTerminal(pollOnce, opts) {
|
|
|
76
99
|
lastProgress = progressJson;
|
|
77
100
|
}
|
|
78
101
|
if (t - start >= opts.totalTimeoutMs || t - lastAdvanceAt >= noProgressMs) {
|
|
79
|
-
return
|
|
102
|
+
return incompleteResult(opts.requestId ?? env.requestId, env.status);
|
|
80
103
|
}
|
|
81
104
|
await sleep(pacingMs);
|
|
82
105
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type DoctorResult } from "../commands/doctor.js";
|
|
2
|
+
export interface DebugReportOptions {
|
|
3
|
+
issue?: string;
|
|
4
|
+
outputPath?: string;
|
|
5
|
+
includePlaySession?: boolean;
|
|
6
|
+
playWaitMs?: number;
|
|
7
|
+
maxConsoleLines?: number;
|
|
8
|
+
maxDebuggerEntries?: number;
|
|
9
|
+
includeDoctor?: boolean;
|
|
10
|
+
baseDir?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface DebugReportArtifact {
|
|
13
|
+
path: string;
|
|
14
|
+
markdown: string;
|
|
15
|
+
report: DebugReport;
|
|
16
|
+
}
|
|
17
|
+
export interface DebugReport {
|
|
18
|
+
generatedAt: string;
|
|
19
|
+
issue: string | null;
|
|
20
|
+
environment: {
|
|
21
|
+
cliVersion: string;
|
|
22
|
+
nodeVersion: string;
|
|
23
|
+
platform: string;
|
|
24
|
+
release: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
hostname: string;
|
|
27
|
+
};
|
|
28
|
+
doctor: DoctorResult | null;
|
|
29
|
+
mcp: {
|
|
30
|
+
logPath: string;
|
|
31
|
+
recentLog: string[];
|
|
32
|
+
};
|
|
33
|
+
engine: {
|
|
34
|
+
connected: boolean;
|
|
35
|
+
error?: string;
|
|
36
|
+
health?: unknown;
|
|
37
|
+
project?: unknown;
|
|
38
|
+
scene?: unknown;
|
|
39
|
+
diagnostics?: unknown;
|
|
40
|
+
console?: unknown;
|
|
41
|
+
debuggerErrors?: unknown;
|
|
42
|
+
debuggerWarnings?: unknown;
|
|
43
|
+
playSession?: unknown;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export declare function defaultDebugReportFilename(date?: Date): string;
|
|
47
|
+
export declare function redactDebugReport(report: DebugReport): DebugReport;
|
|
48
|
+
export declare function renderDebugReportMarkdown(report: DebugReport): string;
|
|
49
|
+
export declare function collectDebugReport(options?: DebugReportOptions): Promise<DebugReport>;
|
|
50
|
+
export declare function createDebugReportArtifact(options?: DebugReportOptions): Promise<DebugReportArtifact>;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { hostname, platform, release } from "node:os";
|
|
4
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { EngineApiClient } from "./api-client.js";
|
|
6
|
+
import { shapeEngineLogResponse } from "./log-filters.js";
|
|
7
|
+
import { tildeify } from "./format.js";
|
|
8
|
+
import { getMcpLogPath, readRecentMcpLogLines } from "./mcp-log.js";
|
|
9
|
+
import { runDoctor } from "../commands/doctor.js";
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const { version } = require("../../package.json");
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
14
|
+
}
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function timestampForFile(date) {
|
|
19
|
+
return date.toISOString().replace(/[:.]/g, "-");
|
|
20
|
+
}
|
|
21
|
+
export function defaultDebugReportFilename(date = new Date()) {
|
|
22
|
+
return `summer-debug-report-${timestampForFile(date)}.md`;
|
|
23
|
+
}
|
|
24
|
+
function asRecord(value) {
|
|
25
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
26
|
+
? value
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
function pickString(value, keys) {
|
|
30
|
+
const record = asRecord(value);
|
|
31
|
+
if (!record)
|
|
32
|
+
return null;
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const candidate = record[key];
|
|
35
|
+
if (typeof candidate === "string" && candidate.length > 0)
|
|
36
|
+
return candidate;
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function projectPathFromReport(report) {
|
|
41
|
+
return pickString(report.engine.health, ["project_path", "projectPath", "projectRoot", "project_root"]);
|
|
42
|
+
}
|
|
43
|
+
function resolveOutputPath(report, options) {
|
|
44
|
+
if (options.outputPath) {
|
|
45
|
+
return isAbsolute(options.outputPath)
|
|
46
|
+
? options.outputPath
|
|
47
|
+
: resolve(process.cwd(), options.outputPath);
|
|
48
|
+
}
|
|
49
|
+
const baseDir = options.baseDir ?? projectPathFromReport(report) ?? process.cwd();
|
|
50
|
+
return join(baseDir, defaultDebugReportFilename(new Date(report.generatedAt)));
|
|
51
|
+
}
|
|
52
|
+
async function writeMarkdown(path, markdown) {
|
|
53
|
+
await mkdir(dirname(path), { recursive: true });
|
|
54
|
+
await writeFile(path, markdown, "utf-8");
|
|
55
|
+
}
|
|
56
|
+
async function safeCollect(label, fn) {
|
|
57
|
+
try {
|
|
58
|
+
return await fn();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
label,
|
|
64
|
+
error: errorMessage(error),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function firstOpsResult(value) {
|
|
69
|
+
const root = asRecord(value);
|
|
70
|
+
const results = root?.results;
|
|
71
|
+
if (Array.isArray(results) && results.length > 0) {
|
|
72
|
+
return asRecord(results[0]);
|
|
73
|
+
}
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
76
|
+
function isGamePlaying(value) {
|
|
77
|
+
return firstOpsResult(value)?.playing === true;
|
|
78
|
+
}
|
|
79
|
+
async function collectConsole(client, maxConsoleLines) {
|
|
80
|
+
const raw = await client.executeOps([
|
|
81
|
+
{ op: "GetConsoleOutput", max_lines: maxConsoleLines },
|
|
82
|
+
]);
|
|
83
|
+
return shapeEngineLogResponse(raw, {
|
|
84
|
+
errorsOnly: false,
|
|
85
|
+
maxEntries: maxConsoleLines,
|
|
86
|
+
}).result;
|
|
87
|
+
}
|
|
88
|
+
async function collectDebugger(client, type, maxEntries) {
|
|
89
|
+
const op = {
|
|
90
|
+
op: "GetDebuggerErrors",
|
|
91
|
+
max_errors: maxEntries,
|
|
92
|
+
include_stack: true,
|
|
93
|
+
};
|
|
94
|
+
if (type === "warning")
|
|
95
|
+
op.type = "warning";
|
|
96
|
+
const raw = await client.executeOps([op]);
|
|
97
|
+
return shapeEngineLogResponse(raw, { maxEntries }).result;
|
|
98
|
+
}
|
|
99
|
+
async function collectPlaySession(client, options) {
|
|
100
|
+
const session = {
|
|
101
|
+
requested: true,
|
|
102
|
+
waitMs: options.playWaitMs,
|
|
103
|
+
};
|
|
104
|
+
session.before = await safeCollect("is-running-before", () => client.executeOps([{ op: "IsGameRunning" }]));
|
|
105
|
+
const wasPlaying = isGamePlaying(session.before);
|
|
106
|
+
session.wasPlayingBefore = wasPlaying;
|
|
107
|
+
if (wasPlaying) {
|
|
108
|
+
session.note =
|
|
109
|
+
"Game was already running. Collected current runtime diagnostics without clearing console, launching, or stopping.";
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
session.clearConsole = await safeCollect("clear-console", () => client.executeOps([{ op: "ClearConsoleOutput" }]));
|
|
113
|
+
session.play = await safeCollect("play", () => client.play());
|
|
114
|
+
}
|
|
115
|
+
await sleep(options.playWaitMs);
|
|
116
|
+
session.diagnostics = await safeCollect("diagnostics-after-play", () => client.getDiagnostics());
|
|
117
|
+
session.console = await safeCollect("console-after-play", () => collectConsole(client, options.maxConsoleLines));
|
|
118
|
+
session.debuggerErrors = await safeCollect("debugger-errors-after-play", () => collectDebugger(client, "error", options.maxDebuggerEntries));
|
|
119
|
+
session.debuggerWarnings = await safeCollect("debugger-warnings-after-play", () => collectDebugger(client, "warning", options.maxDebuggerEntries));
|
|
120
|
+
if (!wasPlaying) {
|
|
121
|
+
session.stop = await safeCollect("stop", () => client.stop());
|
|
122
|
+
}
|
|
123
|
+
return session;
|
|
124
|
+
}
|
|
125
|
+
function redact(value) {
|
|
126
|
+
if (Array.isArray(value))
|
|
127
|
+
return value.map((item) => redact(item));
|
|
128
|
+
if (!value || typeof value !== "object")
|
|
129
|
+
return value;
|
|
130
|
+
const out = {};
|
|
131
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
132
|
+
if (/token|secret|password|authorization|cookie|api[-_]?key|hostname/i.test(key)) {
|
|
133
|
+
out[key] = "[REDACTED]";
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
out[key] = redact(nested);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
export function redactDebugReport(report) {
|
|
142
|
+
return redact(report);
|
|
143
|
+
}
|
|
144
|
+
function jsonBlock(value) {
|
|
145
|
+
return `\`\`\`json\n${JSON.stringify(redact(value), null, 2)}\n\`\`\``;
|
|
146
|
+
}
|
|
147
|
+
function doctorSummary(doctor) {
|
|
148
|
+
if (!doctor)
|
|
149
|
+
return "Doctor was skipped.";
|
|
150
|
+
return `${doctor.summary.failures} failures, ${doctor.summary.warnings} warnings, ${doctor.summary.ok} OK.`;
|
|
151
|
+
}
|
|
152
|
+
function issueText(issue) {
|
|
153
|
+
return issue && issue.trim().length > 0 ? issue.trim() : "No issue text provided.";
|
|
154
|
+
}
|
|
155
|
+
export function renderDebugReportMarkdown(report) {
|
|
156
|
+
const health = asRecord(report.engine.health);
|
|
157
|
+
const projectName = pickString(health, ["project_name", "projectName"]) ?? "unknown";
|
|
158
|
+
const projectPath = pickString(health, ["project_path", "projectPath"]) ?? "unknown";
|
|
159
|
+
const scene = pickString(health, ["scene", "scenePath", "scene_path"]) ?? "unknown";
|
|
160
|
+
return `# Summer Debug Report
|
|
161
|
+
|
|
162
|
+
Generated: ${report.generatedAt}
|
|
163
|
+
|
|
164
|
+
## Before Sending
|
|
165
|
+
|
|
166
|
+
This report is meant for Summer Engine support or an assisting AI agent. It does not include auth tokens or project file contents, but it may include local paths, project names, console output, and stack traces. Review it before sending.
|
|
167
|
+
|
|
168
|
+
## User Issue
|
|
169
|
+
|
|
170
|
+
${issueText(report.issue)}
|
|
171
|
+
|
|
172
|
+
## Quick Summary
|
|
173
|
+
|
|
174
|
+
- Doctor: ${doctorSummary(report.doctor)}
|
|
175
|
+
- Engine connected: ${report.engine.connected ? "yes" : "no"}
|
|
176
|
+
- Project: ${projectName}
|
|
177
|
+
- Project path: ${tildeify(projectPath)}
|
|
178
|
+
- Scene: ${scene}
|
|
179
|
+
|
|
180
|
+
## Environment
|
|
181
|
+
|
|
182
|
+
${jsonBlock({
|
|
183
|
+
...report.environment,
|
|
184
|
+
cwd: tildeify(report.environment.cwd),
|
|
185
|
+
hostname: report.environment.hostname ? "[local-hostname-present]" : "",
|
|
186
|
+
})}
|
|
187
|
+
|
|
188
|
+
## Doctor Checks
|
|
189
|
+
|
|
190
|
+
${report.doctor ? jsonBlock(report.doctor) : "Doctor was skipped."}
|
|
191
|
+
|
|
192
|
+
## MCP Lifecycle Log
|
|
193
|
+
|
|
194
|
+
Log path: ${tildeify(report.mcp.logPath)}
|
|
195
|
+
|
|
196
|
+
${report.mcp.recentLog.length > 0 ? `\`\`\`jsonl\n${report.mcp.recentLog.join("\n")}\n\`\`\`` : "No MCP lifecycle log entries found."}
|
|
197
|
+
|
|
198
|
+
## Engine Health
|
|
199
|
+
|
|
200
|
+
${report.engine.health ? jsonBlock(report.engine.health) : report.engine.error ?? "Engine was not reachable."}
|
|
201
|
+
|
|
202
|
+
## Project State
|
|
203
|
+
|
|
204
|
+
${report.engine.project ? jsonBlock(report.engine.project) : "No project state collected."}
|
|
205
|
+
|
|
206
|
+
## Scene State
|
|
207
|
+
|
|
208
|
+
${report.engine.scene ? jsonBlock(report.engine.scene) : "No scene state collected."}
|
|
209
|
+
|
|
210
|
+
## Diagnostics
|
|
211
|
+
|
|
212
|
+
${report.engine.diagnostics ? jsonBlock(report.engine.diagnostics) : "No diagnostics collected."}
|
|
213
|
+
|
|
214
|
+
## Console
|
|
215
|
+
|
|
216
|
+
${report.engine.console ? jsonBlock(report.engine.console) : "No console output collected."}
|
|
217
|
+
|
|
218
|
+
## Debugger Errors
|
|
219
|
+
|
|
220
|
+
${report.engine.debuggerErrors ? jsonBlock(report.engine.debuggerErrors) : "No debugger errors collected."}
|
|
221
|
+
|
|
222
|
+
## Debugger Warnings
|
|
223
|
+
|
|
224
|
+
${report.engine.debuggerWarnings ? jsonBlock(report.engine.debuggerWarnings) : "No debugger warnings collected."}
|
|
225
|
+
|
|
226
|
+
## Play Session
|
|
227
|
+
|
|
228
|
+
${report.engine.playSession ? jsonBlock(report.engine.playSession) : "No play session requested. Re-run with --play if the issue only appears while the game is running."}
|
|
229
|
+
|
|
230
|
+
## Agent Handoff Prompt
|
|
231
|
+
|
|
232
|
+
\`\`\`text
|
|
233
|
+
You are debugging a Summer Engine project. Use the Summer debug skill and this report as the starting evidence.
|
|
234
|
+
|
|
235
|
+
User issue:
|
|
236
|
+
${issueText(report.issue)}
|
|
237
|
+
|
|
238
|
+
Process:
|
|
239
|
+
1. Start with the collected diagnostics, console output, and debugger errors in this report.
|
|
240
|
+
2. Form one specific hypothesis before editing.
|
|
241
|
+
3. Ask before making project changes.
|
|
242
|
+
4. After a fix, re-run the diagnostic that found the issue and update the report or final answer with the exact evidence.
|
|
243
|
+
\`\`\`
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
export async function collectDebugReport(options = {}) {
|
|
247
|
+
const generatedAt = new Date().toISOString();
|
|
248
|
+
const includeDoctor = options.includeDoctor ?? true;
|
|
249
|
+
const report = {
|
|
250
|
+
generatedAt,
|
|
251
|
+
issue: options.issue?.trim() || null,
|
|
252
|
+
environment: {
|
|
253
|
+
cliVersion: version,
|
|
254
|
+
nodeVersion: process.version,
|
|
255
|
+
platform: platform(),
|
|
256
|
+
release: release(),
|
|
257
|
+
cwd: process.cwd(),
|
|
258
|
+
hostname: hostname(),
|
|
259
|
+
},
|
|
260
|
+
doctor: null,
|
|
261
|
+
mcp: {
|
|
262
|
+
logPath: getMcpLogPath(),
|
|
263
|
+
recentLog: [],
|
|
264
|
+
},
|
|
265
|
+
engine: {
|
|
266
|
+
connected: false,
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
if (includeDoctor) {
|
|
270
|
+
report.doctor = await runDoctor({ quiet: true });
|
|
271
|
+
}
|
|
272
|
+
report.mcp.recentLog = await readRecentMcpLogLines();
|
|
273
|
+
let client;
|
|
274
|
+
try {
|
|
275
|
+
client = await EngineApiClient.connect();
|
|
276
|
+
report.engine.connected = true;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
report.engine.error = errorMessage(error);
|
|
280
|
+
report.mcp.recentLog = await readRecentMcpLogLines();
|
|
281
|
+
return report;
|
|
282
|
+
}
|
|
283
|
+
const maxConsoleLines = options.maxConsoleLines ?? 200;
|
|
284
|
+
const maxDebuggerEntries = options.maxDebuggerEntries ?? 100;
|
|
285
|
+
report.engine.health = await safeCollect("health", () => client.health());
|
|
286
|
+
report.engine.project = await safeCollect("project-state", () => client.getProjectState());
|
|
287
|
+
report.engine.scene = await safeCollect("scene-state", () => client.getSceneState());
|
|
288
|
+
report.engine.diagnostics = await safeCollect("diagnostics", () => client.getDiagnostics());
|
|
289
|
+
report.engine.console = await safeCollect("console", () => collectConsole(client, maxConsoleLines));
|
|
290
|
+
report.engine.debuggerErrors = await safeCollect("debugger-errors", () => collectDebugger(client, "error", maxDebuggerEntries));
|
|
291
|
+
report.engine.debuggerWarnings = await safeCollect("debugger-warnings", () => collectDebugger(client, "warning", maxDebuggerEntries));
|
|
292
|
+
if (options.includePlaySession) {
|
|
293
|
+
report.engine.playSession = await collectPlaySession(client, {
|
|
294
|
+
playWaitMs: options.playWaitMs ?? 2500,
|
|
295
|
+
maxConsoleLines,
|
|
296
|
+
maxDebuggerEntries,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
report.mcp.recentLog = await readRecentMcpLogLines();
|
|
300
|
+
return report;
|
|
301
|
+
}
|
|
302
|
+
export async function createDebugReportArtifact(options = {}) {
|
|
303
|
+
const report = await collectDebugReport(options);
|
|
304
|
+
const markdown = renderDebugReportMarkdown(report);
|
|
305
|
+
const path = resolveOutputPath(report, options);
|
|
306
|
+
await writeMarkdown(path, markdown);
|
|
307
|
+
return { path, markdown, report };
|
|
308
|
+
}
|
|
@@ -249,7 +249,7 @@ function inferSkills(mode, target, goal) {
|
|
|
249
249
|
addSkill(routes, "design-mechanic", "Implement the core shooting/combat loop.", 230);
|
|
250
250
|
addSkill(routes, "design-level", "Shape the arena, spawn space, cover, and pacing.", 210);
|
|
251
251
|
}
|
|
252
|
-
addSkill(routes, "gdscript-patterns", "Use
|
|
252
|
+
addSkill(routes, "gdscript-patterns", "Use guarded Summer file tools for scripts, then verify through MCP.", 90);
|
|
253
253
|
addSkill(routes, "verification-before-completion", "Do not finish without a clean verification story.", 80);
|
|
254
254
|
return routes
|
|
255
255
|
.sort((a, b) => b.weight - a.weight)
|
|
@@ -350,7 +350,7 @@ export function buildGameTaskPlan(options) {
|
|
|
350
350
|
verification,
|
|
351
351
|
principles: [
|
|
352
352
|
"The agent should build a playable game slice, not a static demo.",
|
|
353
|
-
"Use
|
|
353
|
+
"Use identity-bound Summer file tools for code/data/docs and Summer scene tools for live hierarchy/inspector changes.",
|
|
354
354
|
"Reuse/import assets before paid generation unless the user explicitly wants custom output.",
|
|
355
355
|
"Prefer exact IDs and structured tool results over search guesses after asset creation.",
|
|
356
356
|
"Verify through the engine before claiming completion.",
|
|
@@ -358,8 +358,8 @@ export function buildGameTaskPlan(options) {
|
|
|
358
358
|
recommendedSkills: inferSkills(mode, target, goal),
|
|
359
359
|
mcpToolPlan: buildMcpToolPlan(mode, target, assetPolicy, verification, goal),
|
|
360
360
|
hostFileWork: [
|
|
361
|
-
"
|
|
362
|
-
"Use
|
|
361
|
+
"Read and mutate project text through summer_read_file, summer_replace_text, and guarded summer_write_file.",
|
|
362
|
+
"Use create_only:true for new files and an engine sha256 receipt for overwrites.",
|
|
363
363
|
"Read relevant .summer memory files when project context reports them.",
|
|
364
364
|
],
|
|
365
365
|
userGates: buildUserGates(mode, assetPolicy),
|