castle-web-cli 0.4.81 → 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.
@@ -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;
@@ -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
- const suspect = t.suspectNoChanges
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}${s.files && s.files.length > 0 ? ` -- files: ${s.files.join(", ")}` : ""}`)
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. Finished rows' file lists show where recent work landed.\n`
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/filesTouched/crashed pass
1193
- // through by name. `ok` is derived as !error && !crashed -- there is no
1194
- // process exit code; those two fields are the whole story.
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, files: t.files })),
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
  }
@@ -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.9";
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
@@ -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
- const TOUCHED_FILE_LIMIT = 10;
25
- function touchedFileList(files) {
26
- const sorted = [...files].sort();
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 (filesTouched,
403
- // the labels map, the log) is intentionally NOT touched in here -- the
404
- // caller applies it after Promise.all resolves, walking the group in its
405
- // ORIGINAL order, so concurrent completion order never affects what lands in
406
- // the message array or the log.
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, filesTouched, playtestFrames, labels, imageLabels, log, onActivity) {
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, filesTouched, playtestFrames, toolLabels, imageLabels, log, opts.onActivity);
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
  }
@@ -18,7 +18,6 @@ export interface ToolExecContext {
18
18
  export interface ToolCallResult {
19
19
  ok: boolean;
20
20
  output: string;
21
- filesTouched?: string[];
22
21
  images?: Array<{
23
22
  label: string;
24
23
  dataUrl: string;
@@ -21,17 +21,6 @@ import { PLAYTEST_TOOL_DESCRIPTION, PLAYTEST_TOOL_PARAMETERS, runPlaytest, } fro
21
21
  function err(message) {
22
22
  return { ok: false, output: `Error: ${message}` };
23
23
  }
24
- // Mirrors PROGRESS_FILE_RE / the .castle/ exclusion in agent.ts's
25
- // normalizeTouchedPath, so a native task's filesTouched reads the same as a
26
- // CLI task's once wired together.
27
- const PROGRESS_FILE_RE = /\.castle\/agent\/tasks\/[^/]+\/progress$/;
28
- function isTrackedTouch(rel) {
29
- if (rel.startsWith(".castle/"))
30
- return false;
31
- if (PROGRESS_FILE_RE.test(rel))
32
- return false;
33
- return true;
34
- }
35
24
  // Mirrors DECK_TREE_EXCLUDE in agent.ts.
36
25
  const IGNORED_DIRS = new Set(["node_modules", ".castle", ".git", "dist", ".DS_Store"]);
37
26
  const MAX_WALK_FILES = 20_000;
@@ -41,8 +30,7 @@ function baseName(p) {
41
30
  }
42
31
  // Resolves a tool-supplied path against the deck dir and rejects any escape
43
32
  // (absolute paths outside it, `..` traversal). Returns both the absolute path
44
- // and a deck-root-relative path (forward-slashed, for display and for
45
- // filesTouched entries).
33
+ // and a deck-root-relative path (forward-slashed, for display in tool output).
46
34
  function resolveInDeck(deckDir, rawPath) {
47
35
  if (typeof rawPath !== "string" || rawPath.trim() === "")
48
36
  return null;
@@ -169,8 +157,8 @@ const VIEW_IMAGE_SIZE_CAP = 4 * 1024 * 1024;
169
157
  //
170
158
  // Path confinement is the same resolveInDeck as every file tool; note that
171
159
  // user attachments live at .castle/agent/attachments/ INSIDE the deck, so
172
- // they are reachable here (the .castle/ exclusions elsewhere apply to
173
- // filesTouched tracking and tree walks, never to reads).
160
+ // they are reachable here (the .castle/ exclusions elsewhere apply to tree
161
+ // walks, never to reads).
174
162
  function viewImageRun(args, ctx) {
175
163
  const resolved = resolveInDeck(ctx.deckDir, args.path);
176
164
  if (!resolved)
@@ -218,11 +206,9 @@ function writeFileRun(args, ctx) {
218
206
  catch (e) {
219
207
  return err(`could not write ${resolved.rel}: ${e instanceof Error ? e.message : String(e)}`);
220
208
  }
221
- const filesTouched = isTrackedTouch(resolved.rel) ? [resolved.rel] : [];
222
209
  return {
223
210
  ok: true,
224
211
  output: `Wrote ${resolved.rel} (${Buffer.byteLength(args.content, "utf8")} bytes).`,
225
- filesTouched,
226
212
  };
227
213
  }
228
214
  // -- edit_file ------------------------------------------------------------------
@@ -264,11 +250,9 @@ function editFileRun(args, ctx) {
264
250
  catch (e) {
265
251
  return err(`could not write ${resolved.rel}: ${e instanceof Error ? e.message : String(e)}`);
266
252
  }
267
- const filesTouched = isTrackedTouch(resolved.rel) ? [resolved.rel] : [];
268
253
  return {
269
254
  ok: true,
270
255
  output: `Edited ${resolved.rel}${replaceAll ? ` (${count} replacements)` : ""}.`,
271
- filesTouched,
272
256
  };
273
257
  }
274
258
  // -- list_files -----------------------------------------------------------------
@@ -366,61 +350,6 @@ function grepRun(args, ctx) {
366
350
  output: results.join("\n") + (truncated ? `\n... (capped at ${GREP_MAX_RESULTS} matches)` : ""),
367
351
  };
368
352
  }
369
- // -- bash filesTouched heuristic ---------------------------------------------
370
- // Mirrors shellTouchedCandidates/drawingPathForDrawArg/looksLikeTouchedPath in
371
- // agent.ts's CLI stream parser, so a smith task's bash-redirect writes are
372
- // tracked the same way a cursor/claude task's shell-tool writes are. Before
373
- // this, bash had NO filesTouched signal at all here -- a smith task that
374
- // wrote its result via a shell redirect (heredoc, `>`, `npm run draw --`,
375
- // etc.) instead of write_file/edit_file still landed with an empty
376
- // filesTouched and tripped the false "no changes" caution despite genuinely
377
- // writing to disk. (This is the gap agent-prompts.ts's renderTasks used to
378
- // paper over with a "bash side effects aren't tracked" caveat.)
379
- const SHELL_REDIRECT_RE = /(?:^|[\s;|])(?:\d*)>>?\s*(?!&)(?:"([^"]+)"|'([^']+)'|([^\s;&|]+))/g;
380
- const DRAW_RE = /npm\s+run\s+draw\s+--\s+([^\s;&|]+)/g;
381
- function drawingPathForDrawArg(raw) {
382
- const name = raw.replace(/^['"]|['"]$/g, "").trim();
383
- if (!name || name.startsWith("-") || name.includes("\n"))
384
- return null;
385
- if (name.startsWith("drawings/")) {
386
- return name.endsWith(".pxart") ? name : `${name}.pxart`;
387
- }
388
- return `drawings/${name.endsWith(".pxart") ? name : `${name}.pxart`}`;
389
- }
390
- // Guards every redirect candidate against junk that isn't plausibly a path --
391
- // the redirect regex treats any `>`-plus-token as a write target, so e.g. a
392
- // numeric comparison inside a quoted inline script (`>=5`) false-matches.
393
- function looksLikeTouchedPath(raw) {
394
- return /[a-zA-Z0-9]/.test(raw) && raw[0] !== "-" && raw[0] !== "=";
395
- }
396
- function shellTouchedCandidates(command) {
397
- const out = [];
398
- for (const match of command.matchAll(SHELL_REDIRECT_RE)) {
399
- const target = match[1] ?? match[2] ?? match[3];
400
- if (target)
401
- out.push(target);
402
- }
403
- for (const match of command.matchAll(DRAW_RE)) {
404
- const drawing = drawingPathForDrawArg(match[1] ?? "");
405
- if (drawing)
406
- out.push(drawing);
407
- }
408
- return out;
409
- }
410
- // Resolves each shell-redirect candidate against the deck dir the same way
411
- // write_file/edit_file do (path escapes rejected, .castle/ and the progress
412
- // file excluded) -- see resolveInDeck/isTrackedTouch above.
413
- function bashFilesTouched(deckDir, command) {
414
- const out = [];
415
- for (const candidate of shellTouchedCandidates(command)) {
416
- if (!looksLikeTouchedPath(candidate) || candidate.includes("\n"))
417
- continue;
418
- const resolved = resolveInDeck(deckDir, candidate);
419
- if (resolved && isTrackedTouch(resolved.rel))
420
- out.push(resolved.rel);
421
- }
422
- return out;
423
- }
424
353
  // -- bash -------------------------------------------------------------------
425
354
  // Full shell, trusted -- matches today's --force trust level for task agents
426
355
  // (ratified; not revisited here). cwd is always the deck dir; per-call
@@ -480,14 +409,9 @@ function bashRun(args, ctx) {
480
409
  // the assistant's own tool_call (which, unlike this result, is never
481
410
  // evicted from context -- see evictOldToolResults in loop.ts), so
482
411
  // repeating it would just be the same bytes twice on every later turn.
483
- // filesTouched is derived from the command text itself (not gated on
484
- // `ok`): a shell redirect creates/truncates its target as soon as the
485
- // shell sets it up, before the command even runs, so the write already
486
- // landed even if the command that followed the redirect then failed.
487
412
  resolve({
488
413
  ok,
489
414
  output: `(exit ${code ?? "null"})\n${capped}${timedOutNote}`,
490
- filesTouched: bashFilesTouched(ctx.deckDir, command),
491
415
  });
492
416
  });
493
417
  });
@@ -34,7 +34,6 @@ export interface NativeRunResult {
34
34
  text: string;
35
35
  error?: string;
36
36
  usage?: NativeUsage;
37
- filesTouched?: string[];
38
37
  playtestFrames?: string[];
39
38
  crashed?: boolean;
40
39
  }