castle-web-cli 0.4.80 → 0.4.82
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/dist/agent-prompts.d.ts +0 -3
- package/dist/agent-prompts.js +3 -8
- package/dist/agent.d.ts +0 -2
- package/dist/agent.js +4 -148
- package/dist/castle-host/host.js +28 -0
- package/dist/init.js +1 -1
- package/dist/native/loop.js +10 -28
- package/dist/native/playtest-browser.d.ts +3 -0
- package/dist/native/playtest-browser.js +135 -6
- package/dist/native/tools.d.ts +0 -1
- package/dist/native/tools.js +3 -79
- package/dist/native/types.d.ts +0 -1
- package/dist/native/types.js +3 -3
- package/dist/shell/assets/{index-D3unT7do.js → index-ByhgiJoP.js} +1 -1
- package/dist/shell/assets/{index-RZrw5gQ2.css → index-D6hM_VlW.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/kits/basic-2d/CLAUDE.md +2 -1
- package/kits/basic-2d/engine/ScenePlayer.jsx +5 -1
- package/kits/basic-2d/engine/blueprint.js +74 -3
- package/package.json +1 -1
- package/kits/basic-2d/pnpm-workspace.yaml +0 -3
package/dist/agent-prompts.d.ts
CHANGED
|
@@ -9,15 +9,12 @@ export interface PromptTask {
|
|
|
9
9
|
status: string;
|
|
10
10
|
progress: number;
|
|
11
11
|
notes: string;
|
|
12
|
-
files?: string[];
|
|
13
12
|
error?: string;
|
|
14
13
|
blockedBy?: string[];
|
|
15
|
-
suspectNoChanges?: boolean;
|
|
16
14
|
}
|
|
17
15
|
export interface PromptSibling {
|
|
18
16
|
title: string;
|
|
19
17
|
status: string;
|
|
20
|
-
files?: string[];
|
|
21
18
|
}
|
|
22
19
|
export declare function buildRouterPrompt(opts: {
|
|
23
20
|
deckLabel: string;
|
package/dist/agent-prompts.js
CHANGED
|
@@ -48,7 +48,6 @@ comma-separated active-task titles or ids, or \`all\`
|
|
|
48
48
|
- Never claim the board is cleared without actually emitting the fence.
|
|
49
49
|
- Tasks are one-and-done -- when the user gives feedback on a finished task, spawn a new fix task (and \`castle-done\` the old row) rather than reopening it.
|
|
50
50
|
- Task agents are capable coding agents working in this same deck directory, but they know nothing about this conversation beyond your prompt.
|
|
51
|
-
- Board rows may include \`files:\` for finished work. Use those touched-file lists to aim follow-up/fix tasks and to keep shared names consistent without rereading the deck.
|
|
52
51
|
|
|
53
52
|
Asking with options (the \`\`\`ask block). When you need the user to settle a few choices at once, emit ONE fenced block tagged \`ask\` containing JSON -- it renders inline in the chat as grouped options they tap and submit together (far better than stacking questions they can only half-answer). Reach for it to pin a direction fast when their ask is vague ("make me a game" -> ask what kind), NOT to interrogate. Options only, no free text.
|
|
54
53
|
|
|
@@ -88,15 +87,11 @@ function renderTasks(tasks) {
|
|
|
88
87
|
return tasks
|
|
89
88
|
.map((t) => {
|
|
90
89
|
const notes = t.notes.trim() ? ` -- notes: ${t.notes.trim()}` : "";
|
|
91
|
-
const files = t.files && t.files.length > 0 ? ` -- files: ${t.files.join(", ")}` : "";
|
|
92
90
|
const error = t.error ? ` -- error: ${t.error}` : "";
|
|
93
91
|
const blockedBy = t.blockedBy && t.blockedBy.length > 0
|
|
94
92
|
? ` -- blocked by: ${t.blockedBy.join(", ")}`
|
|
95
93
|
: "";
|
|
96
|
-
|
|
97
|
-
? " -- caution: done but touched no tracked files (bash side effects aren't tracked); verify the work actually landed"
|
|
98
|
-
: "";
|
|
99
|
-
return `- [${t.status} ${t.progress}%] ${t.title} (${t.id})${notes}${files}${error}${blockedBy}${suspect}`;
|
|
94
|
+
return `- [${t.status} ${t.progress}%] ${t.title} (${t.id})${notes}${error}${blockedBy}`;
|
|
100
95
|
})
|
|
101
96
|
.join("\n");
|
|
102
97
|
}
|
|
@@ -163,10 +158,10 @@ export function buildTaskPrompt(opts) {
|
|
|
163
158
|
? `\n\nThis task waited on earlier tasks:\n${opts.depsSummary}\n`
|
|
164
159
|
: "";
|
|
165
160
|
const siblingRows = (opts.siblings ?? [])
|
|
166
|
-
.map((s) => `- [${s.status}] ${s.title}
|
|
161
|
+
.map((s) => `- [${s.status}] ${s.title}`)
|
|
167
162
|
.join("\n");
|
|
168
163
|
const siblings = siblingRows
|
|
169
|
-
? `\n\nOther tasks on this deck's board (snapshot at your start):\n${siblingRows}\nRunning and waiting rows are sibling agents working in this same directory. If a sibling plainly owns something your prompt only references (art, a scene, a behavior), leave it to them -- create only what YOUR prompt says to create
|
|
164
|
+
? `\n\nOther tasks on this deck's board (snapshot at your start):\n${siblingRows}\nRunning and waiting rows are sibling agents working in this same directory. If a sibling plainly owns something your prompt only references (art, a scene, a behavior), leave it to them -- create only what YOUR prompt says to create.\n`
|
|
170
165
|
: "";
|
|
171
166
|
// Same "say there are no docs" reasoning as buildRouterPrompt's
|
|
172
167
|
// quickReference above -- a task agent wastes turns the same way a router
|
package/dist/agent.d.ts
CHANGED
|
@@ -20,7 +20,6 @@ interface TaskRecord {
|
|
|
20
20
|
finishedAt?: string;
|
|
21
21
|
pid?: number;
|
|
22
22
|
originMessageId?: string;
|
|
23
|
-
files?: string[];
|
|
24
23
|
playtestFrames?: string[];
|
|
25
24
|
resultSummary?: string;
|
|
26
25
|
avatar?: string;
|
|
@@ -28,7 +27,6 @@ interface TaskRecord {
|
|
|
28
27
|
acknowledged?: boolean;
|
|
29
28
|
rejected?: boolean;
|
|
30
29
|
blockedBy?: string[];
|
|
31
|
-
suspectNoChanges?: boolean;
|
|
32
30
|
}
|
|
33
31
|
export interface DepsState {
|
|
34
32
|
kind: "ready" | "waiting" | "blocked";
|
package/dist/agent.js
CHANGED
|
@@ -787,137 +787,12 @@ function readQuickReference(deckDir) {
|
|
|
787
787
|
function readWelcomeMessage(deckDir) {
|
|
788
788
|
return readClaudeSection(deckDir, "Welcome message");
|
|
789
789
|
}
|
|
790
|
-
const TOUCHED_FILE_LIMIT = 10;
|
|
791
|
-
function collectStrings(value, out) {
|
|
792
|
-
if (typeof value === "string") {
|
|
793
|
-
out.push(value);
|
|
794
|
-
}
|
|
795
|
-
else if (Array.isArray(value)) {
|
|
796
|
-
for (const item of value)
|
|
797
|
-
collectStrings(item, out);
|
|
798
|
-
}
|
|
799
|
-
else if (value && typeof value === "object") {
|
|
800
|
-
for (const item of Object.values(value)) {
|
|
801
|
-
collectStrings(item, out);
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
function toolWritesFiles(name) {
|
|
806
|
-
const kind = name.toLowerCase();
|
|
807
|
-
return ["edit", "write", "notebookedit", "multiedit", "delete"].some((p) => kind.startsWith(p));
|
|
808
|
-
}
|
|
809
|
-
function toolRunsShell(name) {
|
|
810
|
-
const kind = name.toLowerCase();
|
|
811
|
-
return (kind.startsWith("bash") ||
|
|
812
|
-
kind.startsWith("shell") ||
|
|
813
|
-
kind.includes("terminal"));
|
|
814
|
-
}
|
|
815
|
-
function drawingPathForDrawArg(raw) {
|
|
816
|
-
const name = raw.replace(/^['"]|['"]$/g, "").trim();
|
|
817
|
-
if (!name || name.startsWith("-") || name.includes("\n"))
|
|
818
|
-
return null;
|
|
819
|
-
if (name.startsWith("drawings/")) {
|
|
820
|
-
return name.endsWith(".pxart") ? name : `${name}.pxart`;
|
|
821
|
-
}
|
|
822
|
-
return `drawings/${name.endsWith(".pxart") ? name : `${name}.pxart`}`;
|
|
823
|
-
}
|
|
824
|
-
function shellTouchedCandidates(command) {
|
|
825
|
-
const out = [];
|
|
826
|
-
const redirectRe = /(?:^|[\s;|])(?:\d*)>>?\s*(?!&)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/g;
|
|
827
|
-
for (const match of command.matchAll(redirectRe)) {
|
|
828
|
-
const target = match[1] ?? match[2] ?? match[3];
|
|
829
|
-
if (target)
|
|
830
|
-
out.push(target);
|
|
831
|
-
}
|
|
832
|
-
const drawRe = /npm\s+run\s+draw\s+--\s+([^\s;&|]+)/g;
|
|
833
|
-
for (const match of command.matchAll(drawRe)) {
|
|
834
|
-
const drawing = drawingPathForDrawArg(match[1] ?? "");
|
|
835
|
-
if (drawing)
|
|
836
|
-
out.push(drawing);
|
|
837
|
-
}
|
|
838
|
-
return out;
|
|
839
|
-
}
|
|
840
|
-
// Guards every touched-file candidate (shell redirects AND tool file-path
|
|
841
|
-
// args) against junk that isn't plausibly a path. Added because
|
|
842
|
-
// shellTouchedCandidates' redirect regex treats any `>`-plus-token as a
|
|
843
|
-
// write target, so a command merely CONTAINING `>=` (e.g. a numeric
|
|
844
|
-
// comparison inside a quoted inline JS/awk script) false-matches as a
|
|
845
|
-
// redirect to "=" (or "=5" with no space around the `>=`) -- neither looks
|
|
846
|
-
// like a real file. A leading "-" is rejected too, mirroring
|
|
847
|
-
// drawingPathForDrawArg's flag guard above.
|
|
848
|
-
function looksLikeTouchedPath(raw) {
|
|
849
|
-
return /[a-zA-Z0-9]/.test(raw) && raw[0] !== "-" && raw[0] !== "=";
|
|
850
|
-
}
|
|
851
|
-
function normalizeTouchedPath(cwd, raw) {
|
|
852
|
-
if (!raw || raw.includes("\n") || !looksLikeTouchedPath(raw))
|
|
853
|
-
return null;
|
|
854
|
-
const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
855
|
-
const rel = path.relative(cwd, abs);
|
|
856
|
-
if (!rel || rel.startsWith("..") || path.isAbsolute(rel))
|
|
857
|
-
return null;
|
|
858
|
-
const normalized = rel.split(path.sep).join("/");
|
|
859
|
-
if (normalized.startsWith(".castle/") || PROGRESS_FILE_RE.test(normalized)) {
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
return normalized;
|
|
863
|
-
}
|
|
864
|
-
function addTouchedFiles(files, cwd, toolName, input) {
|
|
865
|
-
const candidates = [];
|
|
866
|
-
if (toolWritesFiles(toolName)) {
|
|
867
|
-
collectStrings([
|
|
868
|
-
input.file_path,
|
|
869
|
-
input.path,
|
|
870
|
-
input.notebook_path,
|
|
871
|
-
input.old_path,
|
|
872
|
-
input.new_path,
|
|
873
|
-
], candidates);
|
|
874
|
-
}
|
|
875
|
-
else if (toolRunsShell(toolName) && typeof input.command === "string") {
|
|
876
|
-
candidates.push(...shellTouchedCandidates(input.command));
|
|
877
|
-
}
|
|
878
|
-
else {
|
|
879
|
-
return;
|
|
880
|
-
}
|
|
881
|
-
for (const candidate of candidates) {
|
|
882
|
-
const normalized = normalizeTouchedPath(cwd, candidate);
|
|
883
|
-
if (normalized)
|
|
884
|
-
files.add(normalized);
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
function cursorToolNameAndInput(ev) {
|
|
888
|
-
const call = ev.tool_call;
|
|
889
|
-
const key = call ? Object.keys(call).find((k) => k.endsWith("ToolCall")) : undefined;
|
|
890
|
-
if (!call || !key)
|
|
891
|
-
return null;
|
|
892
|
-
const input = call[key];
|
|
893
|
-
const args = input && typeof input === "object"
|
|
894
|
-
? input.args
|
|
895
|
-
: undefined;
|
|
896
|
-
return {
|
|
897
|
-
name: key.slice(0, -"ToolCall".length),
|
|
898
|
-
input: args && typeof args === "object"
|
|
899
|
-
? args
|
|
900
|
-
: input && typeof input === "object"
|
|
901
|
-
? input
|
|
902
|
-
: {},
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
function touchedFileList(files) {
|
|
906
|
-
const sorted = [...files].sort();
|
|
907
|
-
if (sorted.length <= TOUCHED_FILE_LIMIT)
|
|
908
|
-
return sorted;
|
|
909
|
-
return [
|
|
910
|
-
...sorted.slice(0, TOUCHED_FILE_LIMIT),
|
|
911
|
-
`+${sorted.length - TOUCHED_FILE_LIMIT} more`,
|
|
912
|
-
];
|
|
913
|
-
}
|
|
914
790
|
function createAgentStreamState() {
|
|
915
791
|
return {
|
|
916
792
|
accumulated: "",
|
|
917
793
|
finalText: "",
|
|
918
794
|
resultIsError: false,
|
|
919
795
|
usage: undefined,
|
|
920
|
-
filesTouched: new Set(),
|
|
921
796
|
sawResult: false,
|
|
922
797
|
segmentText: "",
|
|
923
798
|
needsGap: false,
|
|
@@ -1043,7 +918,6 @@ function makeAgentEventHandler(opts, state) {
|
|
|
1043
918
|
catch {
|
|
1044
919
|
/* input JSON arrived partial -- fall back to a generic label */
|
|
1045
920
|
}
|
|
1046
|
-
addTouchedFiles(state.filesTouched, opts.cwd, pending.name, input);
|
|
1047
921
|
const label = claudeToolFeedLabel(pending.name, input);
|
|
1048
922
|
if (label)
|
|
1049
923
|
opts.onActivity?.(label);
|
|
@@ -1078,9 +952,6 @@ function makeAgentEventHandler(opts, state) {
|
|
|
1078
952
|
else if (ev.type === "tool_call") {
|
|
1079
953
|
state.segmentText = "";
|
|
1080
954
|
state.needsGap = true;
|
|
1081
|
-
const tool = cursorToolNameAndInput(ev);
|
|
1082
|
-
if (tool)
|
|
1083
|
-
addTouchedFiles(state.filesTouched, opts.cwd, tool.name, tool.input);
|
|
1084
955
|
if (ev.subtype === "started")
|
|
1085
956
|
opts.onActivity?.(toolActivityLabel(ev));
|
|
1086
957
|
}
|
|
@@ -1140,7 +1011,6 @@ function runAgentCli(opts) {
|
|
|
1140
1011
|
finalText: state.finalText || state.accumulated,
|
|
1141
1012
|
error: "agent run timed out",
|
|
1142
1013
|
usage: state.usage,
|
|
1143
|
-
filesTouched: touchedFileList(state.filesTouched),
|
|
1144
1014
|
});
|
|
1145
1015
|
}, opts.timeoutMs);
|
|
1146
1016
|
const handleEvent = makeAgentEventHandler(opts, state);
|
|
@@ -1178,7 +1048,6 @@ function runAgentCli(opts) {
|
|
|
1178
1048
|
ok,
|
|
1179
1049
|
finalText: state.finalText || state.accumulated,
|
|
1180
1050
|
usage: state.usage,
|
|
1181
|
-
filesTouched: touchedFileList(state.filesTouched),
|
|
1182
1051
|
crashed: !state.sawResult,
|
|
1183
1052
|
error: ok
|
|
1184
1053
|
? undefined
|
|
@@ -1189,9 +1058,9 @@ function runAgentCli(opts) {
|
|
|
1189
1058
|
}
|
|
1190
1059
|
// One smith (native castle agent) run, adapted to runAgentCli's contract so
|
|
1191
1060
|
// every caller downstream of runAgentTurn is backend-agnostic:
|
|
1192
|
-
// - NativeRunResult.text -> finalText; error/usage/
|
|
1193
|
-
//
|
|
1194
|
-
//
|
|
1061
|
+
// - NativeRunResult.text -> finalText; error/usage/crashed pass through by
|
|
1062
|
+
// name. `ok` is derived as !error && !crashed -- there is no process exit
|
|
1063
|
+
// code; those two fields are the whole story.
|
|
1195
1064
|
// - Cancellation: one AbortController per run, registered in the same
|
|
1196
1065
|
// `children` set the CLI runs use, via a handle whose kill() aborts it
|
|
1197
1066
|
// (see AgentRunHandle). Interrupts (killRouterChildren), task halts
|
|
@@ -1242,7 +1111,6 @@ async function runAgentSmith(opts) {
|
|
|
1242
1111
|
finalText: result.text,
|
|
1243
1112
|
error: result.error,
|
|
1244
1113
|
usage: result.usage,
|
|
1245
|
-
filesTouched: result.filesTouched,
|
|
1246
1114
|
playtestFrames: result.playtestFrames,
|
|
1247
1115
|
crashed: result.crashed,
|
|
1248
1116
|
};
|
|
@@ -1407,10 +1275,6 @@ function depsSummaryFor(tasks, task) {
|
|
|
1407
1275
|
.filter((dep) => !!dep)
|
|
1408
1276
|
.map((dep) => {
|
|
1409
1277
|
const parts = [`- "${dep.title}" finished ${dep.status}`];
|
|
1410
|
-
if (dep.files && dep.files.length > 0)
|
|
1411
|
-
parts.push(` files it touched: ${dep.files.join(", ")}`);
|
|
1412
|
-
if (dep.suspectNoChanges)
|
|
1413
|
-
parts.push(" caution: it finished without touching any tracked files (bash side effects aren't tracked) -- verify its work actually landed before building on it");
|
|
1414
1278
|
// The agent's own closing prose is the real handoff -- names it created,
|
|
1415
1279
|
// what it wired, what it left undone. The notes file is player-facing
|
|
1416
1280
|
// and deliberately stripped of that detail.
|
|
@@ -1598,7 +1462,7 @@ function startTask(ctx, task) {
|
|
|
1598
1462
|
siblings: ctx
|
|
1599
1463
|
.sorted()
|
|
1600
1464
|
.filter((t) => t.id !== task.id && !(t.acknowledged && isTerminal(t.status)))
|
|
1601
|
-
.map((t) => ({ title: t.title, status: t.status
|
|
1465
|
+
.map((t) => ({ title: t.title, status: t.status })),
|
|
1602
1466
|
onFeed: (entry) => ctx.onFeed(task, entry),
|
|
1603
1467
|
onRetry: (attempt) => ctx.onRetry(task, attempt),
|
|
1604
1468
|
onSignal: (signal) => {
|
|
@@ -1638,12 +1502,7 @@ function startTask(ctx, task) {
|
|
|
1638
1502
|
task.acknowledged = true;
|
|
1639
1503
|
if (result.ok && !wasStopped)
|
|
1640
1504
|
task.progress = 100;
|
|
1641
|
-
task.files = result.filesTouched ?? [];
|
|
1642
1505
|
task.playtestFrames = result.playtestFrames ?? [];
|
|
1643
|
-
// Flag, don't fail: see the TaskRecord.suspectNoChanges comment.
|
|
1644
|
-
if (task.status === "done" && task.files.length === 0) {
|
|
1645
|
-
task.suspectNoChanges = true;
|
|
1646
|
-
}
|
|
1647
1506
|
task.finishedAt = nowIso();
|
|
1648
1507
|
task.resultSummary = wasStopped
|
|
1649
1508
|
? "stopped by the router"
|
|
@@ -1983,10 +1842,8 @@ function asPromptTask(task) {
|
|
|
1983
1842
|
status: task.rejected ? "rejected by user" : task.status,
|
|
1984
1843
|
progress: task.progress,
|
|
1985
1844
|
notes: task.notes,
|
|
1986
|
-
files: task.files,
|
|
1987
1845
|
error: task.status === "failed" ? firstErrorLine(task.resultSummary) : undefined,
|
|
1988
1846
|
blockedBy: task.status === "blocked" ? task.blockedBy : undefined,
|
|
1989
|
-
suspectNoChanges: task.suspectNoChanges,
|
|
1990
1847
|
};
|
|
1991
1848
|
}
|
|
1992
1849
|
function asClientTask(task) {
|
|
@@ -2004,7 +1861,6 @@ function asClientTask(task) {
|
|
|
2004
1861
|
phase: task.phase,
|
|
2005
1862
|
acknowledged: task.acknowledged,
|
|
2006
1863
|
rejected: task.rejected,
|
|
2007
|
-
suspectNoChanges: task.suspectNoChanges,
|
|
2008
1864
|
playtestFrames: (task.playtestFrames ?? []).map((rel) => `${AGENT_PLAYTEST_PREFIX}${task.id}/${path.basename(rel)}`),
|
|
2009
1865
|
};
|
|
2010
1866
|
}
|
package/dist/castle-host/host.js
CHANGED
|
@@ -28,6 +28,7 @@ const COMMAND_NAMES = [
|
|
|
28
28
|
"pass.offer",
|
|
29
29
|
"portal.open",
|
|
30
30
|
"portal.prefetch",
|
|
31
|
+
"haptics.play",
|
|
31
32
|
];
|
|
32
33
|
// Platform/capability commands: NOT serviced by graphqlFetch. They're dispatched
|
|
33
34
|
// to the host's optional platformHandler (mobile renders native UI; web shows an
|
|
@@ -38,6 +39,7 @@ const PLATFORM_COMMAND_NAMES = [
|
|
|
38
39
|
"pass.offer",
|
|
39
40
|
"portal.open",
|
|
40
41
|
"portal.prefetch",
|
|
42
|
+
"haptics.play",
|
|
41
43
|
];
|
|
42
44
|
function isCommandName(value) {
|
|
43
45
|
return (typeof value === "string" &&
|
|
@@ -106,6 +108,7 @@ function runCommand(ctx, command, params, caps) {
|
|
|
106
108
|
case "pass.offer":
|
|
107
109
|
case "portal.open":
|
|
108
110
|
case "portal.prefetch":
|
|
111
|
+
case "haptics.play":
|
|
109
112
|
return runPlatformCommand(ctx, command, params, caps);
|
|
110
113
|
}
|
|
111
114
|
}
|
|
@@ -122,6 +125,8 @@ async function runPlatformCommand(ctx, command, params, caps) {
|
|
|
122
125
|
return portalOpen(ctx, params, caps);
|
|
123
126
|
case "portal.prefetch":
|
|
124
127
|
return portalPrefetch(ctx, params, caps);
|
|
128
|
+
case "haptics.play":
|
|
129
|
+
return hapticsPlay(ctx, params, caps);
|
|
125
130
|
default:
|
|
126
131
|
return unavailableOutcome();
|
|
127
132
|
}
|
|
@@ -212,6 +217,29 @@ function normalizePortalPrefetchOutcome(value) {
|
|
|
212
217
|
}
|
|
213
218
|
return { status: "unavailable" };
|
|
214
219
|
}
|
|
220
|
+
// A haptic is a device effect, not deck-scoped state, so — unlike pass/portal —
|
|
221
|
+
// no deckId is required; the style is validated and handed straight to the
|
|
222
|
+
// host's platformHandler. Hosts that can't play a haptic (dev CLI — no handler;
|
|
223
|
+
// a browser with no vibration API) get a normalized `unavailable`, never an
|
|
224
|
+
// error.
|
|
225
|
+
async function hapticsPlay(ctx, params, caps) {
|
|
226
|
+
const style = asString(params.style, "style", "haptics.play");
|
|
227
|
+
if (!caps.platformHandler)
|
|
228
|
+
return { status: "unavailable" };
|
|
229
|
+
const outcome = await caps.platformHandler("haptics.play", { style }, ctx);
|
|
230
|
+
return normalizeHapticsOutcome(outcome);
|
|
231
|
+
}
|
|
232
|
+
function normalizeHapticsOutcome(value) {
|
|
233
|
+
const record = typeof value === "object" && value !== null
|
|
234
|
+
? value
|
|
235
|
+
: {};
|
|
236
|
+
const status = record.status;
|
|
237
|
+
const valid = ["triggered", "unavailable"];
|
|
238
|
+
if (typeof status === "string" && valid.includes(status)) {
|
|
239
|
+
return { status: status };
|
|
240
|
+
}
|
|
241
|
+
return { status: "unavailable" };
|
|
242
|
+
}
|
|
215
243
|
function unavailableOutcome() {
|
|
216
244
|
return { status: "unavailable" };
|
|
217
245
|
}
|
package/dist/init.js
CHANGED
|
@@ -35,7 +35,7 @@ const DEFAULT_KIT = "basic-2d";
|
|
|
35
35
|
// Registry version of castle-web-sdk to inject when scaffolding from a
|
|
36
36
|
// globally-installed castle-web (not from inside the workspace). Bumped
|
|
37
37
|
// alongside cli/sdk version bumps.
|
|
38
|
-
const PUBLISHED_SDK_VERSION = "0.4.
|
|
38
|
+
const PUBLISHED_SDK_VERSION = "0.4.10";
|
|
39
39
|
// Never copied into a fresh deck: build/dependency junk. castle.json IS copied
|
|
40
40
|
// (the kit ships a config-only one with the editor layout / file filters), but
|
|
41
41
|
// `scaffoldFromKit` strips any identity fields off it first -- a fresh deck has
|
package/dist/native/loop.js
CHANGED
|
@@ -21,16 +21,9 @@ import { toolSchemasForRole, executeTool, activityLabelForCall, } from "./tools.
|
|
|
21
21
|
// Safety valve against a model that never stops calling tools -- distinct
|
|
22
22
|
// from timeoutMs, which bounds wall-clock time regardless of iteration count.
|
|
23
23
|
const MAX_ITERATIONS = 40;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (sorted.length <= TOUCHED_FILE_LIMIT)
|
|
28
|
-
return sorted;
|
|
29
|
-
return [...sorted.slice(0, TOUCHED_FILE_LIMIT), `+${sorted.length - TOUCHED_FILE_LIMIT} more`];
|
|
30
|
-
}
|
|
31
|
-
// playtest frames never hit the TOUCHED_FILE_LIMIT truncation above (a run
|
|
32
|
-
// is capped at PLAYTEST_MAX_CALLS_PER_RUN calls x PLAYTEST_MAX_SHOTS frames
|
|
33
|
-
// each -- at most 24 -- small enough to list in full for the task card).
|
|
24
|
+
// playtest frames are capped at PLAYTEST_MAX_CALLS_PER_RUN calls x
|
|
25
|
+
// PLAYTEST_MAX_SHOTS frames each -- at most 24 -- small enough to list in
|
|
26
|
+
// full for the task card, with no truncation needed.
|
|
34
27
|
function playtestFrameList(frames) {
|
|
35
28
|
return [...frames].sort();
|
|
36
29
|
}
|
|
@@ -399,11 +392,11 @@ function groupToolCalls(toolCalls) {
|
|
|
399
392
|
// Executes one tool call and reports its own activity label -- everything
|
|
400
393
|
// error handling and result-shape wise is identical to the old sequential
|
|
401
394
|
// loop; only the caller (runToolCalls) changed, to run several of these
|
|
402
|
-
// concurrently within a group. Bookkeeping shared across calls (
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
395
|
+
// concurrently within a group. Bookkeeping shared across calls (the labels
|
|
396
|
+
// map, the log) is intentionally NOT touched in here -- the caller applies
|
|
397
|
+
// it after Promise.all resolves, walking the group in its ORIGINAL order, so
|
|
398
|
+
// concurrent completion order never affects what lands in the message array
|
|
399
|
+
// or the log.
|
|
407
400
|
async function runOneToolCall(call, role, ctx, onActivity) {
|
|
408
401
|
const name = call.function?.name ?? "";
|
|
409
402
|
const { args, error } = parseToolArgs(call.function?.arguments ?? "");
|
|
@@ -418,7 +411,7 @@ async function runOneToolCall(call, role, ctx, onActivity) {
|
|
|
418
411
|
onActivity?.(null);
|
|
419
412
|
return { call, name, args, result };
|
|
420
413
|
}
|
|
421
|
-
async function runToolCalls(toolCalls, role, ctx,
|
|
414
|
+
async function runToolCalls(toolCalls, role, ctx, playtestFrames, labels, imageLabels, log, onActivity) {
|
|
422
415
|
const results = [];
|
|
423
416
|
// Images a call produced this batch (view_image, playtest, ...). Delivered
|
|
424
417
|
// as synthetic role:"user" messages AFTER all the batch's tool results --
|
|
@@ -435,9 +428,6 @@ async function runToolCalls(toolCalls, role, ctx, filesTouched, playtestFrames,
|
|
|
435
428
|
const resolved = await Promise.all(group.map((call) => runOneToolCall(call, role, ctx, onActivity)));
|
|
436
429
|
for (const { call, name, args, result } of resolved) {
|
|
437
430
|
labels.set(call.id, toolCallLabel(name, args));
|
|
438
|
-
if (result.filesTouched)
|
|
439
|
-
for (const f of result.filesTouched)
|
|
440
|
-
filesTouched.add(f);
|
|
441
431
|
if (result.playtestFrames)
|
|
442
432
|
for (const f of result.playtestFrames)
|
|
443
433
|
playtestFrames.add(f);
|
|
@@ -502,9 +492,6 @@ export async function runAgentNative(opts) {
|
|
|
502
492
|
...(result.error ? { error: result.error } : {}),
|
|
503
493
|
...(result.crashed ? { crashed: true } : {}),
|
|
504
494
|
...(result.usage ? { usage: result.usage } : {}),
|
|
505
|
-
...(result.filesTouched && result.filesTouched.length > 0
|
|
506
|
-
? { filesTouched: result.filesTouched }
|
|
507
|
-
: {}),
|
|
508
495
|
...(result.playtestFrames && result.playtestFrames.length > 0
|
|
509
496
|
? { playtestFrames: result.playtestFrames }
|
|
510
497
|
: {}),
|
|
@@ -571,7 +558,6 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
571
558
|
}
|
|
572
559
|
: undefined,
|
|
573
560
|
};
|
|
574
|
-
const filesTouched = new Set();
|
|
575
561
|
const playtestFrames = new Set();
|
|
576
562
|
const toolLabels = new Map();
|
|
577
563
|
// Synthetic view_image carrier messages, by identity -- see runToolCalls
|
|
@@ -590,7 +576,6 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
590
576
|
text: finalText,
|
|
591
577
|
error: timeoutFired ? "agent run timed out" : "agent run stopped",
|
|
592
578
|
usage: totalUsage,
|
|
593
|
-
filesTouched: touchedFileList(filesTouched),
|
|
594
579
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
595
580
|
};
|
|
596
581
|
};
|
|
@@ -647,7 +632,6 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
647
632
|
text: finalText,
|
|
648
633
|
error: streamResult.error ?? "openrouter stream ended without a final response",
|
|
649
634
|
usage: totalUsage,
|
|
650
|
-
filesTouched: touchedFileList(filesTouched),
|
|
651
635
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
652
636
|
crashed: streamResult.crashed,
|
|
653
637
|
};
|
|
@@ -672,7 +656,6 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
672
656
|
return {
|
|
673
657
|
text: finalText,
|
|
674
658
|
usage: totalUsage,
|
|
675
|
-
filesTouched: touchedFileList(filesTouched),
|
|
676
659
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
677
660
|
};
|
|
678
661
|
}
|
|
@@ -681,7 +664,7 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
681
664
|
content: streamResult.message.content || null,
|
|
682
665
|
tool_calls: toolCalls,
|
|
683
666
|
});
|
|
684
|
-
const toolResults = await runToolCalls(toolCalls, opts.role, toolCtx,
|
|
667
|
+
const toolResults = await runToolCalls(toolCalls, opts.role, toolCtx, playtestFrames, toolLabels, imageLabels, log, opts.onActivity);
|
|
685
668
|
messages.push(...toolResults);
|
|
686
669
|
}
|
|
687
670
|
}
|
|
@@ -692,7 +675,6 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
692
675
|
text: finalText,
|
|
693
676
|
error: "agent exceeded the maximum number of tool-call iterations",
|
|
694
677
|
usage: totalUsage,
|
|
695
|
-
filesTouched: touchedFileList(filesTouched),
|
|
696
678
|
playtestFrames: playtestFrameList(playtestFrames),
|
|
697
679
|
};
|
|
698
680
|
}
|
|
@@ -2,6 +2,7 @@ import type { Browser } from "playwright-core";
|
|
|
2
2
|
import type { PlaytestInstallEvent } from "./playtest.js";
|
|
3
3
|
export declare const INSTALL_START_LABEL = "Downloading playtest browser (one-time, ~250MB)\u2026";
|
|
4
4
|
export declare const INSTALL_WAIT_LABEL = "Waiting for browser download (shared)\u2026";
|
|
5
|
+
export declare const DEPS_REPAIR_LABEL = "Installing missing browser system libraries (one-time)\u2026";
|
|
5
6
|
export interface PlaywrightChromiumLike {
|
|
6
7
|
executablePath(): string;
|
|
7
8
|
launch(options?: {
|
|
@@ -14,6 +15,8 @@ export interface PlaywrightModuleLike {
|
|
|
14
15
|
export interface PlaytestBrowserSeams {
|
|
15
16
|
loadPlaywright?: () => Promise<PlaywrightModuleLike>;
|
|
16
17
|
runInstall?: (onLine: (line: string) => void) => Promise<void>;
|
|
18
|
+
runInstallDeps?: () => Promise<void>;
|
|
19
|
+
platform?: NodeJS.Platform;
|
|
17
20
|
}
|
|
18
21
|
export interface BrowserInstallHooks {
|
|
19
22
|
onProgress?: (label: string) => void;
|