@tamsensedev/runner 0.6.0 → 0.9.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/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync4 } from "fs";
4
+ import { existsSync as existsSync5 } from "fs";
5
5
  import { homedir as homedir3 } from "os";
6
6
  import process5 from "process";
7
7
 
@@ -33,9 +33,9 @@ function textOf(value) {
33
33
  return typeof value === "string" && value.trim() ? value : null;
34
34
  }
35
35
  async function errorOf(response) {
36
- const reply2 = await response.json().catch(() => ({}));
37
- const code = textOf(reply2.data?.code);
38
- const message = textOf(reply2.message) ?? textOf(reply2.statusMessage) ?? `TAMsense answered with status ${response.status}`;
36
+ const reply3 = await response.json().catch(() => ({}));
37
+ const code = textOf(reply3.data?.code);
38
+ const message = textOf(reply3.message) ?? textOf(reply3.statusMessage) ?? `TAMsense answered with status ${response.status}`;
39
39
  return new ApiError(response.status, code, message);
40
40
  }
41
41
  function createApi(server, token = null, fetchImpl = fetch) {
@@ -187,7 +187,7 @@ function findNpm(nodePath, env, exists) {
187
187
  }
188
188
  return null;
189
189
  }
190
- var runProcess = (command, args, options) => new Promise((resolve2, reject) => {
190
+ var runProcess = (command, args, options) => new Promise((resolve3, reject) => {
191
191
  const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
192
192
  const tail = [];
193
193
  for (const stream of [child.stdout, child.stderr]) {
@@ -201,7 +201,7 @@ var runProcess = (command, args, options) => new Promise((resolve2, reject) => {
201
201
  });
202
202
  }
203
203
  child.on("error", reject);
204
- child.on("close", (code) => resolve2({ code: code ?? 1, output: tail.join("\n") }));
204
+ child.on("close", (code) => resolve3({ code: code ?? 1, output: tail.join("\n") }));
205
205
  });
206
206
  function failureText(what, result) {
207
207
  if (OFFLINE_PATTERN.test(result.output)) {
@@ -314,8 +314,8 @@ async function probeCapabilities(stop) {
314
314
  stop?.throwIfAborted();
315
315
  let release = () => {
316
316
  };
317
- const released = new Promise((resolve2) => {
318
- release = resolve2;
317
+ const released = new Promise((resolve3) => {
318
+ release = resolve3;
319
319
  });
320
320
  const nothing = {
321
321
  [Symbol.asyncIterator]: () => ({
@@ -340,9 +340,8 @@ async function probeCapabilities(stop) {
340
340
  }
341
341
 
342
342
  // src/claude.ts
343
- import { fileURLToPath } from "url";
344
- import { createSdkMcpServer, query as query2, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
345
- import { z as z3 } from "zod";
343
+ import { createSdkMcpServer, query as query2, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
344
+ import { z as z4 } from "zod";
346
345
 
347
346
  // src/askPerson.ts
348
347
  var MAX_ASK_TRIES = 5;
@@ -402,21 +401,21 @@ function shorten(text2) {
402
401
  const line = text2.replace(/\s+/g, " ").trim();
403
402
  return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
404
403
  }
405
- function activityLine(tool4, input) {
404
+ function activityLine(tool5, input) {
406
405
  const fields = typeof input === "object" && input !== null ? input : {};
407
- if (tool4 === "Bash" && typeof fields.command === "string") {
406
+ if (tool5 === "Bash" && typeof fields.command === "string") {
408
407
  return `$ ${shorten(fields.command)}`;
409
408
  }
410
- if (FILE_TOOLS.has(tool4) && typeof fields.file_path === "string") {
411
- return `${tool4} ${shorten(fields.file_path)}`;
409
+ if (FILE_TOOLS.has(tool5) && typeof fields.file_path === "string") {
410
+ return `${tool5} ${shorten(fields.file_path)}`;
412
411
  }
413
- if (tool4 === "Skill" && typeof fields.skill === "string") {
412
+ if (tool5 === "Skill" && typeof fields.skill === "string") {
414
413
  return `Skill ${shorten(fields.skill)}`;
415
414
  }
416
- if (HELPER_TOOLS.has(tool4) && typeof fields.subagent_type === "string") {
415
+ if (HELPER_TOOLS.has(tool5) && typeof fields.subagent_type === "string") {
417
416
  return `Agent ${shorten(fields.subagent_type)}`;
418
417
  }
419
- return tool4;
418
+ return tool5;
420
419
  }
421
420
  function usageOf(modelUsage) {
422
421
  const entries = Object.values(modelUsage);
@@ -440,6 +439,33 @@ function outcomeOf(result, lastText) {
440
439
  return { completed: false, report: result.errors.join("\n") || `Claude stopped early: ${result.subtype}`, ...measured };
441
440
  }
442
441
 
442
+ // src/protocol.ts
443
+ var MAX_RESULT_FILE_BYTES = 10 * 1024 * 1024;
444
+ var HELPER_STOPPED = "stopped";
445
+ var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
446
+ var MAX_ATTACHMENTS_PER_RUN = 20;
447
+ var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
448
+ var PAIRINGS_PATH = "/api/runner/pairings";
449
+ var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
450
+ var IDENTITY_PATH = "/api/runner/me";
451
+ var NEXT_RUN_PATH = "/api/runner/runs/next";
452
+ var CAPABILITIES_PATH = "/api/runner/me/capabilities";
453
+ function runPath(runId, action) {
454
+ return `/api/runner/runs/${runId}/${action}`;
455
+ }
456
+ function resultFilePath(runId, resultId) {
457
+ return `/api/runner/runs/${runId}/results/${resultId}`;
458
+ }
459
+ function knowledgePath(runId, entryId) {
460
+ return `/api/runner/runs/${runId}/knowledge/${entryId}`;
461
+ }
462
+ function permissionPath(runId, permissionId) {
463
+ return `/api/runner/runs/${runId}/permissions/${permissionId}`;
464
+ }
465
+ function recordingUploadPath(runId, upload, part) {
466
+ return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
467
+ }
468
+
443
469
  // src/feed.ts
444
470
  var MAX_TEXT_LENGTH = 8e3;
445
471
  var MAX_RESULT_LENGTH = 2e3;
@@ -459,29 +485,29 @@ function textField(fields, name) {
459
485
  return typeof value === "string" && value.trim() ? value : null;
460
486
  }
461
487
  function toolDraft(block, parent) {
462
- const tool4 = (block.name ?? "tool").replace(MCP_PREFIX, "");
488
+ const tool5 = (block.name ?? "tool").replace(MCP_PREFIX, "");
463
489
  const fields = fieldsOf(block.input);
464
- const base = { tool: tool4, ref: block.id ?? null, parent, failed: false };
465
- if (tool4 === "Skill" && textField(fields, "skill")) {
490
+ const base = { tool: tool5, ref: block.id ?? null, parent, failed: false };
491
+ if (tool5 === "Skill" && textField(fields, "skill")) {
466
492
  return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
467
493
  }
468
- if (HELPER_TOOLS2.has(tool4)) {
494
+ if (HELPER_TOOLS2.has(tool5)) {
469
495
  const who = textField(fields, "subagent_type") ?? "general-purpose";
470
496
  const what = textField(fields, "description");
471
497
  return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
472
498
  }
473
- if (tool4 === "submit_verdict") {
499
+ if (tool5 === "submit_verdict") {
474
500
  const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
475
501
  return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
476
502
  }
477
- if (tool4 === "hand_off") {
503
+ if (tool5 === "hand_off") {
478
504
  return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
479
505
  }
480
- if (tool4 in ATTACH_LABELS) {
481
- return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool4]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
506
+ if (tool5 in ATTACH_LABELS) {
507
+ return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool5]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
482
508
  }
483
509
  const command = textField(fields, "command");
484
- const summary = tool4 === "Bash" && command ? `$ ${command}` : activityLine(tool4, block.input);
510
+ const summary = tool5 === "Bash" && command ? `$ ${command}` : activityLine(tool5, block.input);
485
511
  return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
486
512
  }
487
513
  function resultText(content) {
@@ -493,6 +519,10 @@ function resultText(content) {
493
519
  }
494
520
  return content.map((part) => part.type === "text" ? part.text ?? "" : part.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
495
521
  }
522
+ function helperEndDraft(ref, parent, end, text2) {
523
+ const stopped = end === "stopped";
524
+ return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
525
+ }
496
526
  function feedOf(message) {
497
527
  if (message.type !== "assistant" && message.type !== "user") {
498
528
  return [];
@@ -561,6 +591,77 @@ function chooseHandoff(targets, request) {
561
591
  return { handoff: { toColumnId: edge.columnId, outcome: edge.outcome, note } };
562
592
  }
563
593
 
594
+ // src/helpers.ts
595
+ function helperTasks() {
596
+ const byRef = /* @__PURE__ */ new Map();
597
+ const refByTask = /* @__PURE__ */ new Map();
598
+ const parents = /* @__PURE__ */ new Map();
599
+ function observe(message) {
600
+ if (message.type !== "system") {
601
+ return [];
602
+ }
603
+ if (message.subtype === "task_started" && message.task_type === "local_agent" && message.tool_use_id) {
604
+ byRef.set(message.tool_use_id, { taskId: message.task_id, background: message.is_backgrounded === true, stopped: false, over: false });
605
+ refByTask.set(message.task_id, message.tool_use_id);
606
+ return [];
607
+ }
608
+ if (message.subtype === "task_updated" && message.patch.is_backgrounded !== void 0) {
609
+ const ref2 = refByTask.get(message.task_id);
610
+ const task2 = ref2 ? byRef.get(ref2) : void 0;
611
+ if (task2) {
612
+ task2.background = message.patch.is_backgrounded;
613
+ }
614
+ return [];
615
+ }
616
+ if (message.subtype !== "task_notification") {
617
+ return [];
618
+ }
619
+ const ref = message.tool_use_id ?? refByTask.get(message.task_id);
620
+ const task = ref ? byRef.get(ref) : void 0;
621
+ if (!ref || !task || task.over) {
622
+ return [];
623
+ }
624
+ task.stopped = message.status === "stopped";
625
+ if (!task.background) {
626
+ return [];
627
+ }
628
+ task.over = true;
629
+ return [helperEndDraft(ref, parents.get(ref) ?? null, message.status, message.summary)];
630
+ }
631
+ function adjust(draft) {
632
+ if (draft.kind === "agent" && draft.ref) {
633
+ parents.set(draft.ref, draft.parent);
634
+ }
635
+ const task = draft.kind === "result" && draft.ref ? byRef.get(draft.ref) : void 0;
636
+ if (!task || !draft.ref) {
637
+ return [draft];
638
+ }
639
+ if (task.background) {
640
+ return [];
641
+ }
642
+ task.over = true;
643
+ return task.stopped ? [helperEndDraft(draft.ref, draft.parent, "stopped", "")] : [draft];
644
+ }
645
+ async function stop(target, ref) {
646
+ const task = byRef.get(ref);
647
+ if (!task) {
648
+ return "unknown";
649
+ }
650
+ if (task.over || task.stopped) {
651
+ return "finished";
652
+ }
653
+ await target.stopTask(task.taskId);
654
+ return "stopped";
655
+ }
656
+ return { observe, adjust, stop };
657
+ }
658
+ function helperStoppedNote(stop) {
659
+ return `${stop.byName} stopped your helper ${stop.name} on TAMsense. Go on without it and say in your result what it did not finish. Start it again only if a person asks.`;
660
+ }
661
+ function helperStopRequest(stop) {
662
+ return `${stop.byName} asked on TAMsense to stop your helper ${stop.name}. Stop it now, go on without it and say in your result what it did not finish. Start it again only if a person asks.`;
663
+ }
664
+
564
665
  // src/input.ts
565
666
  var DEFAULT_MODEL = "default";
566
667
  var QUIET_MS = 1e3;
@@ -571,8 +672,11 @@ function userMessage(text2) {
571
672
  function humanText(message) {
572
673
  return `Message from ${message.authorName}: ${message.text}`;
573
674
  }
675
+ function priorityOf(mode) {
676
+ return mode === "steer" ? "now" : "next";
677
+ }
574
678
  function humanMessage(message) {
575
- return { ...userMessage(humanText(message)), priority: message.mode === "steer" ? "now" : "next", client_composed: true };
679
+ return { ...userMessage(humanText(message)), priority: priorityOf(message.mode), client_composed: true };
576
680
  }
577
681
  function switchLine(change) {
578
682
  const model = change.model === DEFAULT_MODEL ? "the default model" : change.model;
@@ -601,6 +705,7 @@ function inputChannel(options = {}) {
601
705
  let wake = null;
602
706
  let timers = [];
603
707
  let applySwitch2 = null;
708
+ let applyStop = null;
604
709
  let waitingSwitch = null;
605
710
  function wakeUp() {
606
711
  wake?.();
@@ -653,8 +758,8 @@ function inputChannel(options = {}) {
653
758
  }
654
759
  const next = queue.shift();
655
760
  if (!next) {
656
- await new Promise((resolve2) => {
657
- wake = resolve2;
761
+ await new Promise((resolve3) => {
762
+ wake = resolve3;
658
763
  });
659
764
  continue;
660
765
  }
@@ -714,11 +819,14 @@ function inputChannel(options = {}) {
714
819
  onSwitch(apply) {
715
820
  applySwitch2 = apply;
716
821
  if (waitingSwitch) {
717
- const { change, resolve: resolve2 } = waitingSwitch;
822
+ const { change, resolve: resolve3 } = waitingSwitch;
718
823
  waitingSwitch = null;
719
- void apply(change).then(() => resolve2(true), () => resolve2(false));
824
+ void apply(change).then(() => resolve3(true), () => resolve3(false));
720
825
  }
721
826
  },
827
+ onStopHelper(stop) {
828
+ applyStop = stop;
829
+ },
722
830
  close,
723
831
  push(message) {
724
832
  if (closed || seen.has(message.id)) {
@@ -729,6 +837,17 @@ function inputChannel(options = {}) {
729
837
  wakeUp();
730
838
  return true;
731
839
  },
840
+ note(text2, mode) {
841
+ if (closed) {
842
+ return false;
843
+ }
844
+ queue.push({ ids: [], message: { ...userMessage(text2), priority: priorityOf(mode) } });
845
+ wakeUp();
846
+ return true;
847
+ },
848
+ async stopHelper(ref) {
849
+ return closed || !applyStop ? "unknown" : await applyStop(ref);
850
+ },
732
851
  async checking(work2) {
733
852
  const epoch = idle ? idleEpoch : null;
734
853
  inFlight += 1;
@@ -763,8 +882,8 @@ function inputChannel(options = {}) {
763
882
  const earlier = waitingSwitch;
764
883
  earlier?.resolve(false);
765
884
  const merged = { model: change.model ?? earlier?.change.model ?? null, effort: change.effort ?? earlier?.change.effort ?? null };
766
- return new Promise((resolve2) => {
767
- waitingSwitch = { change: merged, resolve: resolve2 };
885
+ return new Promise((resolve3) => {
886
+ waitingSwitch = { change: merged, resolve: resolve3 };
768
887
  });
769
888
  }
770
889
  };
@@ -927,8 +1046,8 @@ function messageOf2(error) {
927
1046
  async function decisionOf(tools, first, signal) {
928
1047
  const now = tools.now ?? Date.now;
929
1048
  const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
930
- let reply2 = first;
931
- while (reply2.decision === "wait") {
1049
+ let reply3 = first;
1050
+ while (reply3.decision === "wait") {
932
1051
  await tools.sleep(CHECK_INTERVAL_MS, signal);
933
1052
  if (signal.aborted) {
934
1053
  return STOPPED_TEXT2;
@@ -936,9 +1055,9 @@ async function decisionOf(tools, first, signal) {
936
1055
  if (now() > giveUpAt) {
937
1056
  return SITE_SILENT_TEXT;
938
1057
  }
939
- reply2 = await tools.check(reply2.id).catch(() => reply2);
1058
+ reply3 = await tools.check(reply3.id).catch(() => reply3);
940
1059
  }
941
- return reply2;
1060
+ return reply3;
942
1061
  }
943
1062
  function askPermission(tools, lastWords, folder) {
944
1063
  return async (toolName, input, options) => {
@@ -959,15 +1078,203 @@ function askPermission(tools, lastWords, folder) {
959
1078
  } catch (error) {
960
1079
  return deny(`Could not ask for permission on TAMsense: ${messageOf2(error)}. Do it another way, or explain in your report why it is needed.`);
961
1080
  }
962
- const reply2 = await decisionOf(tools, first, options.signal);
963
- if (typeof reply2 === "string") {
964
- return deny(reply2);
1081
+ const reply3 = await decisionOf(tools, first, options.signal);
1082
+ if (typeof reply3 === "string") {
1083
+ return deny(reply3);
965
1084
  }
966
- return reply2.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply2.message);
1085
+ return reply3.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply3.message);
967
1086
  };
968
1087
  }
969
1088
  var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
970
1089
 
1090
+ // src/results.ts
1091
+ import { mkdir as mkdir2, readFile as readFile2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
1092
+ import { basename, extname, isAbsolute, join as join2, resolve } from "path";
1093
+ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1094
+ import { z as z2 } from "zod";
1095
+ var SAVE_DOCUMENT_TOOL_NAME = "save_result_document";
1096
+ var SAVE_TABLE_TOOL_NAME = "save_result_table";
1097
+ var ATTACH_FILE_TOOL_NAME = "attach_result_file";
1098
+ var ADD_LINK_TOOL_NAME = "add_result_link";
1099
+ var HAND_OVER_TOOL_NAME = "hand_over";
1100
+ var RESULTS_FOLDER = "results";
1101
+ var MAX_NAME_LENGTH = 60;
1102
+ var TYPES_BY_EXTENSION = {
1103
+ ".pdf": "application/pdf",
1104
+ ".png": "image/png",
1105
+ ".jpg": "image/jpeg",
1106
+ ".jpeg": "image/jpeg",
1107
+ ".webp": "image/webp",
1108
+ ".gif": "image/gif",
1109
+ ".csv": "text/csv",
1110
+ ".txt": "text/plain",
1111
+ ".md": "text/markdown",
1112
+ ".json": "application/json",
1113
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1114
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1115
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1116
+ ".zip": "application/zip"
1117
+ };
1118
+ var EXTENSIONS = { document: ".md", table: ".csv", file: "", link: "" };
1119
+ function isFolderJob(job) {
1120
+ return job.workspace === "folder";
1121
+ }
1122
+ function resultTools(job, api, root) {
1123
+ if (!isFolderJob(job) || job.stage.kind !== "code") {
1124
+ return null;
1125
+ }
1126
+ return {
1127
+ root,
1128
+ save: (body) => api.request("POST", runPath(job.runId, "results"), body),
1129
+ attach: (body) => api.request("POST", runPath(job.runId, "result-files"), body)
1130
+ };
1131
+ }
1132
+ function handOverTools(job, api) {
1133
+ const boards = job.handOverBoards ?? [];
1134
+ if (boards.length === 0 || job.stage.kind !== "code") {
1135
+ return null;
1136
+ }
1137
+ return { boards, send: (body) => api.request("POST", runPath(job.runId, "hand-over"), body) };
1138
+ }
1139
+ function resultFileName(result, taken) {
1140
+ const stem = (result.kind === "file" && result.fileName ? result.fileName.replace(/\.[^.]*$/, "") : result.title).normalize("NFKC").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, MAX_NAME_LENGTH) || "result";
1141
+ const extension = result.kind === "file" ? extname(result.fileName ?? "") : EXTENSIONS[result.kind];
1142
+ let name = `${stem}${extension}`;
1143
+ for (let index = 2; taken.has(name.toLowerCase()); index += 1) {
1144
+ name = `${stem}-${index}${extension}`;
1145
+ }
1146
+ taken.add(name.toLowerCase());
1147
+ return name;
1148
+ }
1149
+ function linksText(links) {
1150
+ return links.map((link) => `- ${link.title}: ${link.url ?? ""}`).join("\n");
1151
+ }
1152
+ async function writeResults(folder, job, api) {
1153
+ const results = job.results ?? [];
1154
+ await rm2(folder, { recursive: true, force: true });
1155
+ if (results.length === 0) {
1156
+ return [];
1157
+ }
1158
+ await mkdir2(folder, { recursive: true });
1159
+ const taken = /* @__PURE__ */ new Set();
1160
+ const lines = [];
1161
+ const missing = [];
1162
+ for (const result of results.filter((item) => item.kind !== "link")) {
1163
+ const name = resultFileName(result, taken);
1164
+ if (result.kind === "file") {
1165
+ try {
1166
+ const file = await api.request("GET", resultFilePath(job.runId, result.id));
1167
+ await writeFile2(join2(folder, name), Buffer.from(file.data, "base64"));
1168
+ } catch (error) {
1169
+ missing.push(`${result.title}: ${error instanceof Error ? error.message : String(error)}`);
1170
+ continue;
1171
+ }
1172
+ } else {
1173
+ await writeFile2(join2(folder, name), result.content ?? "");
1174
+ }
1175
+ lines.push(`${RESULTS_FOLDER}/${name}: ${result.title}${result.version ? `, version ${result.version}` : ""}`);
1176
+ }
1177
+ const links = results.filter((item) => item.kind === "link");
1178
+ if (links.length > 0) {
1179
+ await writeFile2(join2(folder, "links.md"), `${linksText(links)}
1180
+ `);
1181
+ lines.push(`${RESULTS_FOLDER}/links.md: ${links.length} link(s)`);
1182
+ }
1183
+ if (missing.length > 0) {
1184
+ await writeFile2(join2(folder, "notes.md"), `These files could not be downloaded:
1185
+ ${missing.map((line) => `- ${line}`).join("\n")}
1186
+ `);
1187
+ }
1188
+ return lines;
1189
+ }
1190
+ async function prepareFolder(job, root, api) {
1191
+ if (!job.keepWorkspace) {
1192
+ await rm2(root, { recursive: true, force: true });
1193
+ }
1194
+ await mkdir2(root, { recursive: true });
1195
+ return { root, repositories: [], results: await writeResults(join2(root, RESULTS_FOLDER), job, api) };
1196
+ }
1197
+ async function readResultFile(root, path, title) {
1198
+ const full = isAbsolute(path) ? path : resolve(root, path);
1199
+ const info = await stat(full).catch(() => null);
1200
+ if (!info?.isFile()) {
1201
+ return `There is no file at ${full}.`;
1202
+ }
1203
+ if (info.size > MAX_RESULT_FILE_BYTES) {
1204
+ return `The file is larger than ${MAX_RESULT_FILE_BYTES / 1024 / 1024} MB. Attach a smaller one or split it.`;
1205
+ }
1206
+ const name = basename(full);
1207
+ return { name, title: title.trim() || name, contentType: TYPES_BY_EXTENSION[extname(full).toLowerCase()] ?? "application/octet-stream", data: (await readFile2(full)).toString("base64") };
1208
+ }
1209
+ function reply(text2, isError = false) {
1210
+ return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
1211
+ }
1212
+ async function sent(send) {
1213
+ try {
1214
+ return reply((await send()).text);
1215
+ } catch (error) {
1216
+ return reply(`Could not save it on TAMsense: ${error instanceof Error ? error.message : String(error)}. Try again once; if it fails again, say so in your report.`, true);
1217
+ }
1218
+ }
1219
+ function resultToolset(tools) {
1220
+ return [
1221
+ tool2(
1222
+ SAVE_DOCUMENT_TOOL_NAME,
1223
+ "Save a document as the result of this step on the TAMsense task: Markdown text such as a letter, a reply, a brief or a report. Saving again with the same title makes a new version of that document; every version is kept and people compare them.",
1224
+ {
1225
+ title: z2.string().describe("short name of the document, the same for every version of it"),
1226
+ content: z2.string().describe("the full text in Markdown, never a diff or a part"),
1227
+ note: z2.string().describe('one line on what this version is or what changed: "First draft", "Fixed the prices after the check"')
1228
+ },
1229
+ async (args) => await sent(() => tools.save({ kind: "document", ...args }))
1230
+ ),
1231
+ tool2(
1232
+ SAVE_TABLE_TOOL_NAME,
1233
+ "Save a table as the result of this step, as CSV with a header row. Saving again with the same title makes a new version of that table.",
1234
+ {
1235
+ title: z2.string().describe("short name of the table, the same for every version of it"),
1236
+ csv: z2.string().describe("the full table as CSV, the first row holds the column names"),
1237
+ note: z2.string().describe("one line on what this version is or what changed")
1238
+ },
1239
+ async (args) => await sent(() => tools.save({ kind: "table", title: args.title, content: args.csv, note: args.note }))
1240
+ ),
1241
+ tool2(
1242
+ ATTACH_FILE_TOOL_NAME,
1243
+ `Attach a file you made in the task folder to the TAMsense task as part of the result: a PDF, a spreadsheet, an image. Up to ${MAX_RESULT_FILE_BYTES / 1024 / 1024} MB.`,
1244
+ {
1245
+ path: z2.string().describe("path of the file, from the task folder or absolute"),
1246
+ title: z2.string().describe("what the file is, in a few words")
1247
+ },
1248
+ async (args) => {
1249
+ const body = await readResultFile(tools.root, args.path, args.title);
1250
+ return typeof body === "string" ? reply(body, true) : await sent(() => tools.attach(body));
1251
+ }
1252
+ ),
1253
+ tool2(
1254
+ ADD_LINK_TOOL_NAME,
1255
+ "Attach a link to the TAMsense task as part of the result: a page or a file the result relies on.",
1256
+ {
1257
+ title: z2.string().describe("what the link is, in a few words"),
1258
+ url: z2.string().describe("the full address starting with https://")
1259
+ },
1260
+ async (args) => await sent(() => tools.save({ kind: "link", ...args }))
1261
+ )
1262
+ ];
1263
+ }
1264
+ function handOverTool(tools) {
1265
+ return tool2(
1266
+ HAND_OVER_TOOL_NAME,
1267
+ `Hand the task to another board of the project as a linked task, when part of the work belongs to that board. Boards you may hand to: ${tools.boards.map((board) => board.name).join(", ")}. Without autopilot a person confirms it first.`,
1268
+ {
1269
+ board: z2.string().describe("name of the board"),
1270
+ title: z2.string().describe("title of the new task on that board, one plain sentence"),
1271
+ note: z2.string().describe("what you found and what that board should do; never names, emails or other personal data of clients"),
1272
+ wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
1273
+ },
1274
+ async (args) => await sent(() => tools.send(args))
1275
+ );
1276
+ }
1277
+
971
1278
  // src/specText.ts
972
1279
  function isRecord(value) {
973
1280
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1020,10 +1327,31 @@ var RISK_LEVELS = ["low", "medium", "high"];
1020
1327
  function present(lines) {
1021
1328
  return lines.filter((line) => line !== null);
1022
1329
  }
1330
+ function decisionLines(spec) {
1331
+ return records(spec.decisions).filter((item) => text(item.question) && text(item.answer)).map((item) => `- ${text(item.question)} \u2192 ${text(item.answer)} (${item.by === "agent" ? "agent's call" : "owner"})`);
1332
+ }
1333
+ function briefPromptText(brief) {
1334
+ return [
1335
+ text(brief.goal) && `Goal: ${text(brief.goal)}`,
1336
+ text(brief.context) && `Summary: ${text(brief.context)}`,
1337
+ section("The result must contain:", texts(brief.contains).map((item) => `- ${item}`)),
1338
+ section("Acceptance criteria:", present(Array.isArray(brief.acceptance) ? brief.acceptance.map(criterionLine) : [])),
1339
+ section("Constraints:", texts(brief.constraints).map((item) => `- ${item}`)),
1340
+ section("Out of scope (do not make):", texts(brief.outOfScope).map((item) => `- ${item}`)),
1341
+ section("Sources to use:", records(brief.sources).filter((item) => text(item.source)).map((item) => `- ${text(item.source)}${text(item.use) ? `: ${text(item.use)}` : ""}`)),
1342
+ text(brief.approver) && `Approved by: ${text(brief.approver)}`,
1343
+ section("Decisions:", decisionLines(brief)),
1344
+ section("Assumptions:", present(Array.isArray(brief.assumptions) ? brief.assumptions.map(assumptionLine) : [])),
1345
+ section("Open questions (not answered yet):", records(brief.openQuestions).filter((item) => text(item.question)).map((item) => `- ${text(item.question)}`))
1346
+ ].filter(Boolean).join("\n\n");
1347
+ }
1023
1348
  function specPromptText(spec) {
1024
1349
  if (!isRecord(spec)) {
1025
1350
  return null;
1026
1351
  }
1352
+ if (spec.format === "brief") {
1353
+ return briefPromptText(spec);
1354
+ }
1027
1355
  const structured = Array.isArray(spec.outOfScope) || isRecord(spec.testPlan);
1028
1356
  const plan = isRecord(spec.testPlan) ? spec.testPlan : {};
1029
1357
  const risks = records(spec.risks).filter((item) => text(item.risk));
@@ -1033,7 +1361,7 @@ function specPromptText(spec) {
1033
1361
  text(spec.context) && `${structured ? "Summary" : "Context"}: ${text(spec.context)}`,
1034
1362
  section("In scope:", texts(spec.inScope).map((item) => `- ${item}`)),
1035
1363
  section("Out of scope (do not build):", texts(spec.outOfScope).map((item) => `- ${item}`)),
1036
- section("Decisions:", records(spec.decisions).filter((item) => text(item.question) && text(item.answer)).map((item) => `- ${text(item.question)} \u2192 ${text(item.answer)} (${item.by === "agent" ? "agent's call" : "owner"})`)),
1364
+ section("Decisions:", decisionLines(spec)),
1037
1365
  section("Code map:", records(spec.codeMap).filter((item) => text(item.file)).map((item) => `- ${text(item.repo) ? `${text(item.repo)} ` : ""}${text(item.file)}: ${text(item.role)}${CODE_USE_TEXT[item.use] ? ` [${CODE_USE_TEXT[item.use]}]` : ""}`)),
1038
1366
  section("Changes, in order:", records(spec.changes).filter((item) => text(item.file)).flatMap(changeLines)),
1039
1367
  section("Acceptance criteria:", present(Array.isArray(spec.acceptance) ? spec.acceptance.map(criterionLine) : [])),
@@ -1050,15 +1378,15 @@ function specPromptText(spec) {
1050
1378
 
1051
1379
  // src/workspace.ts
1052
1380
  import { existsSync as existsSync2 } from "fs";
1053
- import { rm as rm2 } from "fs/promises";
1054
- import { join as join2, resolve } from "path";
1381
+ import { rm as rm3 } from "fs/promises";
1382
+ import { join as join3, resolve as resolve2 } from "path";
1055
1383
 
1056
1384
  // src/git.ts
1057
- import { Buffer } from "buffer";
1385
+ import { Buffer as Buffer2 } from "buffer";
1058
1386
  import { spawn as spawn2 } from "child_process";
1059
1387
  var GITHUB_ORIGIN = "https://github.com/";
1060
1388
  function githubAuthEnv(token) {
1061
- const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
1389
+ const credentials = Buffer2.from(`x-access-token:${token}`).toString("base64");
1062
1390
  return {
1063
1391
  GIT_CONFIG_COUNT: "1",
1064
1392
  GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
@@ -1068,7 +1396,7 @@ function githubAuthEnv(token) {
1068
1396
  }
1069
1397
  function runGit(args, options) {
1070
1398
  const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
1071
- return new Promise((resolve2, reject) => {
1399
+ return new Promise((resolve3, reject) => {
1072
1400
  const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
1073
1401
  let stdout = "";
1074
1402
  let stderr = "";
@@ -1079,7 +1407,7 @@ function runGit(args, options) {
1079
1407
  stderr += chunk.toString();
1080
1408
  });
1081
1409
  child.on("error", reject);
1082
- child.on("close", (code) => resolve2({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
1410
+ child.on("close", (code) => resolve3({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
1083
1411
  });
1084
1412
  }
1085
1413
  async function git(args, options) {
@@ -1098,8 +1426,8 @@ function keyedLock() {
1098
1426
  const previous = tails.get(key) ?? Promise.resolve();
1099
1427
  let release = () => {
1100
1428
  };
1101
- const done2 = new Promise((resolve2) => {
1102
- release = resolve2;
1429
+ const done2 = new Promise((resolve3) => {
1430
+ release = resolve3;
1103
1431
  });
1104
1432
  const tail = previous.then(() => done2);
1105
1433
  tails.set(key, tail);
@@ -1123,7 +1451,7 @@ function githubRemoteUrl(fullName) {
1123
1451
  return `https://github.com/${fullName}.git`;
1124
1452
  }
1125
1453
  function tamsenseFolder(home, ...parts) {
1126
- return join2(home, ".tamsense", ...parts);
1454
+ return join3(home, ".tamsense", ...parts);
1127
1455
  }
1128
1456
  function folderNames(repositories) {
1129
1457
  const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
@@ -1134,7 +1462,7 @@ function folderNames(repositories) {
1134
1462
  return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
1135
1463
  }
1136
1464
  async function refreshCache(cache, repository, token, tools) {
1137
- if (existsSync2(join2(cache, ".git"))) {
1465
+ if (existsSync2(join3(cache, ".git"))) {
1138
1466
  await git(["fetch", "--prune", "origin"], { cwd: cache, token });
1139
1467
  return;
1140
1468
  }
@@ -1142,7 +1470,7 @@ async function refreshCache(cache, repository, token, tools) {
1142
1470
  }
1143
1471
  async function removeWorktree(cache, path) {
1144
1472
  await runGit(["worktree", "remove", "--force", path], { cwd: cache });
1145
- await rm2(path, { recursive: true, force: true });
1473
+ await rm3(path, { recursive: true, force: true });
1146
1474
  await git(["worktree", "prune"], { cwd: cache });
1147
1475
  }
1148
1476
  function stepFolder(job) {
@@ -1166,7 +1494,7 @@ function branchHolders(porcelain, branch) {
1166
1494
  }
1167
1495
  async function releaseBranch(cache, branch, keep) {
1168
1496
  const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
1169
- for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve(holder) !== resolve(keep))) {
1497
+ for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve2(holder) !== resolve2(keep))) {
1170
1498
  await removeWorktree(cache, path);
1171
1499
  }
1172
1500
  }
@@ -1175,7 +1503,7 @@ async function baseRef(cache, job, repository) {
1175
1503
  return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
1176
1504
  }
1177
1505
  async function keptBase(cache, path, job, repository) {
1178
- if (!job.keepWorkspace || !existsSync2(join2(path, ".git"))) {
1506
+ if (!job.keepWorkspace || !existsSync2(join3(path, ".git"))) {
1179
1507
  return null;
1180
1508
  }
1181
1509
  const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
@@ -1184,14 +1512,17 @@ async function keptBase(cache, path, job, repository) {
1184
1512
  }
1185
1513
  return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
1186
1514
  }
1515
+ function taskFolder(home, job) {
1516
+ return tamsenseFolder(home, "work", ...stepFolder(job));
1517
+ }
1187
1518
  async function prepareWorkspace(job, token, tools) {
1188
- const root = tamsenseFolder(tools.home, "work", ...stepFolder(job));
1519
+ const root = taskFolder(tools.home, job);
1189
1520
  const folders = folderNames(job.repositories);
1190
1521
  const repositories = [];
1191
1522
  for (const [index, repository] of job.repositories.entries()) {
1192
1523
  const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
1193
1524
  const folder = folders[index] ?? repository.fullName;
1194
- const path = join2(root, folder);
1525
+ const path = join3(root, folder);
1195
1526
  const baseSha = await cacheLock.run(repository.fullName, async () => {
1196
1527
  await refreshCache(cache, repository, token, tools);
1197
1528
  const kept = await keptBase(cache, path, job, repository);
@@ -1270,6 +1601,11 @@ var CODE_RULES = [
1270
1601
  ];
1271
1602
  var REWORK_RULE = "This is a rework. The branch already has the earlier work and maybe the reviewer's own commits: treat the current branch as the source of truth, keep the reviewer's changes unless a comment asks otherwise, and address every comment.";
1272
1603
  var CODE_REPORT_RULE = "End with a short report for the reviewer: what you changed, how you checked it, and anything left open.";
1604
+ var READ_ONLY_CODE_RULES = [
1605
+ "Your role on this step only reads. Do the work the task asks for by researching and running what helps, then report. Do not change, commit or push product code: nothing from this run is pushed. You may create throwaway files outside the repositories.",
1606
+ "Check what you report with the project's own tools where they exist, and say how you checked it."
1607
+ ];
1608
+ var READ_ONLY_REPORT_RULE = "End with a short report: what you found, how you checked it, and what is left open.";
1273
1609
  var CHECK_RULES = [
1274
1610
  "The work to check is on the current branch. See it with git diff against the base branch named for each repository below, and read the code around it.",
1275
1611
  "Do not change, commit or push product code. You may run anything and create throwaway files outside the repositories.",
@@ -1280,6 +1616,21 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
1280
1616
  var NAMING_RULE = 'Name everything you attach (screenshots, replays, videos) by what it shows and why it was made: a short title such as "Design option 1", "Feature testing" or "Feature testing, attempt 2", and a one-line purpose such as "Comparing two layouts for the empty orders state". Number design variants, name test runs by their goal and attempt, and never use generic names like "screenshot1".';
1281
1617
  var RECORDING_RULE = `Every browser you open with Playwright from Node is recorded for the task as a replay people can watch, requests from the tested site to the TAMsense tracker are blocked, so your runs never reach analytics, and requests other than GET, HEAD and OPTIONS to addresses outside this computer are aborted. In every browser script, load the recorder from the TAMSENSE_RECORDER environment variable (require(process.env.TAMSENSE_RECORDER), or createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) in an .mjs script), open each context with await recorder.newRecordedContext(browser, { title, purpose, context: { viewport } }), call await recorder.mark(page, 'what just happened') at every meaningful step, and end with await recorder.closeRecorded(context). Add video: true when the page has a canvas, a map, a video player or iframes from another site (a replay cannot show them), or when someone asks for a video. Recordings are attached to the task when the run ends; ${REPLAY_TOOL_NAME} and ${VIDEO_TOOL_NAME} attach one earlier with a better title.`;
1282
1618
  var BROWSER_RULE = "A headless browser is installed for interface checks. If the project has no Playwright of its own, load playwright-core from the folder in the TAMSENSE_PLAYWRIGHT environment variable, with PLAYWRIGHT_BROWSERS_PATH set to the folder in TAMSENSE_PLAYWRIGHT_BROWSERS.";
1619
+ var FOLDER_RULE = "This board works without code: there are no repositories. The current folder belongs to this task: work in it and keep your throwaway files here.";
1620
+ var RESULT_HOME_RULE = "The result lives only on the TAMsense task: never save it in outside services such as Google Drive or a shared folder. Use connected services only to read. Sending anything to real people, such as an email or a message, is an action that needs approval like any other.";
1621
+ var FOLDER_DO_RULES = [
1622
+ `Save what you make on the task with the TAMsense tools: ${SAVE_DOCUMENT_TOOL_NAME} for text in Markdown, ${SAVE_TABLE_TOOL_NAME} for rows of data as CSV, ${ATTACH_FILE_TOOL_NAME} for files you made in this folder, ${ADD_LINK_TOOL_NAME} for pages the result relies on. Keep one title per document and save every change as a new version of it.`,
1623
+ "Use only facts from the task, the notes, the earlier results and what you read yourself; never invent numbers, names or dates.",
1624
+ RESULT_HOME_RULE
1625
+ ];
1626
+ var FOLDER_REWORK_RULE = "The task came back with notes. Read the earlier results in the results folder, fix what the notes ask and save new versions of the same documents.";
1627
+ var FOLDER_REPORT_RULE = "End with a short report for the people on the task: what you made, what you decided yourself and what is still open.";
1628
+ var FOLDER_CHECK_RULES = [
1629
+ `The result to check is in the ${RESULTS_FOLDER} folder: the latest version of each document (.md) and table (.csv), the files and links.md. Read all of it.`,
1630
+ `Do not change the result. You may create throwaway files outside the ${RESULTS_FOLDER} folder.`,
1631
+ "Judge it against the task, the notes of earlier steps and the instructions of this step. Be specific: every problem names the document, the place in it and what to change.",
1632
+ `Finish by calling the ${VERDICT_TOOL_NAME} tool exactly once: passed is true only if nothing must be fixed; report says what you checked and, if it failed, exactly what to fix.`
1633
+ ];
1283
1634
  var DEPENDENCIES_RULE = "The checkout may have no dependencies installed. To run the app or its tests, install the project's own dependencies from its lockfile with its package manager (pnpm install --frozen-lockfile, npm ci, yarn install --immutable and similar). Never add packages, change a lockfile or download browsers.";
1284
1635
  function composeProjectName(job) {
1285
1636
  return ["tamsense", ...stepFolder(job)].join("-");
@@ -1302,6 +1653,10 @@ var STAGE_GOALS = {
1302
1653
  review: "Review the code for",
1303
1654
  test: "Test"
1304
1655
  };
1656
+ var READ_ONLY_GOAL = "Work on";
1657
+ function isReadOnlyCode(stage) {
1658
+ return stage.kind === "code" && stage.role?.access === "read";
1659
+ }
1305
1660
  var SPEC_UNATTENDED_RULE = `You are preparing a TAMsense task on the user's own computer. Nobody answers you during this run: the owner answers your questions and reviews the spec on the task page after it. So never stop to wait for a reply and never ask in your messages: hand in what this stage asks for with the TAMsense tool, then finish. People on the task may send you messages during this run, each starting with "Message from <name>:". Follow them.`;
1306
1661
  var READ_ONLY_RULE = "Read the code to research the task. Do not change, commit or push anything: nothing from this run is pushed, and the code step starts from a clean branch.";
1307
1662
  var RESEARCH_RULE = "Start by loading the tamsense:task-interview skill with the Skill tool and follow it: it holds the question test, the corner sweep and the rules for rounds. Research before asking: read the code in the repositories here and the evidence of the linked action, if there is one. Finding facts is your job; the decisions about intent are the owner's.";
@@ -1319,12 +1674,29 @@ var SPEC_RULES = [
1319
1674
  "Then reply with a short plain summary: the approach, the risk level and the open questions, if any. Do not paste the spec: the page shows it."
1320
1675
  ];
1321
1676
  var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
1677
+ var BRIEF_UNATTENDED_RULE = `You are preparing a TAMsense task on the user's own computer. Nobody answers you during this run: the owner answers your questions and reviews the brief on the task page after it. So never stop to wait for a reply and never ask in your messages: hand in what this stage asks for with the TAMsense tool, then finish. People on the task may send you messages during this run, each starting with "Message from <name>:". Follow them.`;
1678
+ var BRIEF_RESEARCH_RULE = "Research before asking: read what the task, the project knowledge, the files in this folder and the connected services give you. Finding facts is your job; the decisions about intent are the owner's.";
1679
+ var RESEARCH_BRIEF_RULE = "Write the research brief as you go: findings (what the sources say that matters, each with where: the source), dataFacts (numbers and facts, each with its source; empty when none), approach (2 to 4 sentences), codeMap (always empty: this board has no code), assumptions (each corner you settled with a default, with the reason) and pendingDecisions (decisions that depend on an answer to this round; empty when none).";
1680
+ var RESULT_BRIEF_RULES = [
1681
+ 'Write the brief of the result: goal, context (the summary), contains (what the result must contain), acceptance (criteria with ids AC-1, AC-2 and so on in order, each one observable property a checker confirms by reading the result), constraints, outOfScope, sources (where the facts come from: knowledge, linked items, files of the task, connected services that are read only), approver (who approves the result), decisions, assumptions and openQuestions. Set format to "brief". Never write a code map, files to touch or repositories.',
1682
+ 'Start from the research brief and the conversation below. Apply the answers: an answer the owner gave is a decision with by: owner; an answer marked "Recommended option applied, not answered" is an assumption with the reason "not answered, the recommended option applies".',
1683
+ "Where the task, the answers and the project context are silent and a safe default exists, choose it and record it as an assumption with its reason. A point with no safe default that changes the result becomes an open question; keep them to 3 at most. Never invent numbers, names or dates.",
1684
+ `Hand in the FULL brief with ${SUBMIT_SPEC_TOOL_NAME}. TAMsense lints it; if it returns feedback, the brief was not saved: fix it and call ${SUBMIT_SPEC_TOOL_NAME} again. After two rounds of feedback the brief is saved as it is.`,
1685
+ "When the task already has a saved brief, the conversation holds the owner's feedback or answers to its open questions: apply them and hand in the full updated brief. An answered open question becomes a decision.",
1686
+ "Then reply with a short plain summary: what the result is and the open questions, if any. Do not paste the brief: the page shows it."
1687
+ ];
1688
+ var BRIEF_STYLE_RULE = "Write the research brief, the questions, the brief and your reply in the language the task is written in. Replies are plain text, no Markdown. Use real names, numbers and sources; if you could not verify something, say so. No em dashes and no en dashes; use commas, colons or a plain hyphen.";
1322
1689
  var SPEC_STYLE_RULE = "Write the brief, the questions, the spec and your reply in the language the task is written in. Replies are plain text, no Markdown. Use real file paths, function and table names; if you could not verify something, say so. No em dashes and no en dashes; use commas, colons or a plain hyphen.";
1323
1690
  function specText(spec) {
1324
1691
  const text2 = specPromptText(spec);
1325
1692
  return text2 ? `Approved spec:
1326
1693
  ${text2}` : null;
1327
1694
  }
1695
+ function briefText(spec) {
1696
+ const text2 = specPromptText(spec);
1697
+ return text2 ? `Approved brief:
1698
+ ${text2}` : null;
1699
+ }
1328
1700
  function contextText(context) {
1329
1701
  return context.trim() ? `Project context from the team (follow it like the project's own rules):
1330
1702
  ${context.trim()}` : null;
@@ -1363,6 +1735,18 @@ function peopleText(people) {
1363
1735
  function canProposeKnowledge(job) {
1364
1736
  return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
1365
1737
  }
1738
+ function folderText(root, results) {
1739
+ const lines = results ?? [];
1740
+ return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
1741
+ ${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
1742
+ }
1743
+ function handOverText(boards) {
1744
+ const names = (boards ?? []).map((board) => board.name);
1745
+ if (names.length === 0) {
1746
+ return null;
1747
+ }
1748
+ return `When part of the work belongs to another board, hand it over with the ${HAND_OVER_TOOL_NAME} tool: ${names.join(", ")}. Write a plain title and a note on what you found and what that board should do; never put names, emails or other personal data of clients in the note. With wait true this task waits on its step until the linked task is done, then it goes on and you get its result note.`;
1749
+ }
1366
1750
  function handoffText(targets, isCheck) {
1367
1751
  if (targets.length === 0) {
1368
1752
  return null;
@@ -1409,6 +1793,64 @@ function specGoal(job, work2) {
1409
1793
  }
1410
1794
  return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its spec: ${job.title}`;
1411
1795
  }
1796
+ function briefQuestionRules(work2) {
1797
+ const handIn = `Call ${SUBMIT_QUESTIONS_TOOL_NAME} once with the research brief and the questions, or an empty list when none are needed. If it returns problems, fix them and call it again.`;
1798
+ if (work2.round === 1) {
1799
+ return [
1800
+ BRIEF_RESEARCH_RULE,
1801
+ RESEARCH_BRIEF_RULE,
1802
+ `Ask 0 to ${work2.maxQuestions} questions, ideally 2 or 3, the most important first, and only the ones research cannot settle, whose answers change the result, and where a wrong default is costly. Zero questions is a good result: the brief is then written right away.`,
1803
+ QUESTION_SHAPE_RULE,
1804
+ handIn,
1805
+ QUESTIONS_REPLY_RULE
1806
+ ];
1807
+ }
1808
+ const taken = work2.takenIds.length > 0 ? ` These ids are taken by round 1: ${work2.takenIds.join(", ")}.` : "";
1809
+ return [
1810
+ `This is the follow-up round ${work2.round} of 2, and there is no round after it. Read the answers in the conversation below; do not research again what your research brief covers.`,
1811
+ `Ask at most ${work2.maxQuestions} more questions, and only when an answer is vague, contradicts another answer or the facts, or unlocked a decision from pendingDecisions. Otherwise hand in no questions: whatever is still open becomes an assumption.${taken}`,
1812
+ QUESTION_SHAPE_RULE,
1813
+ "Update the research brief with what the answers changed and leave pendingDecisions empty.",
1814
+ handIn,
1815
+ QUESTIONS_REPLY_RULE
1816
+ ];
1817
+ }
1818
+ function briefGoal(job, work2) {
1819
+ if (work2.round === null) {
1820
+ return `Write the brief for TAMsense task #${job.taskId}: ${job.title}`;
1821
+ }
1822
+ return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its brief: ${job.title}`;
1823
+ }
1824
+ function buildBriefPrompt(job, work2, workspace) {
1825
+ const prompt = [
1826
+ briefGoal(job, work2),
1827
+ job.description,
1828
+ work2.linkedAction,
1829
+ work2.brief ? `Research brief from the clarification phase (verify only what you rely on):
1830
+ ${JSON.stringify(work2.brief)}` : null,
1831
+ work2.round === null && job.spec ? `Current saved brief (hand in the full updated version):
1832
+ ${JSON.stringify(job.spec)}` : null,
1833
+ job.handoffNote && `Note from another step: ${job.handoffNote}`,
1834
+ threadText(work2.thread),
1835
+ teamMessagesText(job.messages)
1836
+ ].filter(Boolean).join("\n\n");
1837
+ const instructions = [
1838
+ BRIEF_UNATTENDED_RULE,
1839
+ MACHINE_RULE,
1840
+ workplaceText(job.computer, workspace.root),
1841
+ stepLine(job.stage),
1842
+ job.stage.instructions,
1843
+ contextText(job.projectContext),
1844
+ FOLDER_RULE,
1845
+ RESULT_HOME_RULE,
1846
+ `How to work:
1847
+ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
1848
+ safetyText(job),
1849
+ knowledgeText(job.knowledge, job.computer, false),
1850
+ BRIEF_STYLE_RULE
1851
+ ].filter(Boolean).join("\n\n");
1852
+ return { prompt, instructions };
1853
+ }
1412
1854
  function buildSpecPrompt(job, work2, workspace) {
1413
1855
  const prompt = [
1414
1856
  specGoal(job, work2),
@@ -1440,19 +1882,63 @@ ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
1440
1882
  ].filter(Boolean).join("\n\n");
1441
1883
  return { prompt, instructions };
1442
1884
  }
1885
+ var FOLDER_GOALS = {
1886
+ code: "Work on",
1887
+ review: "Check the result of",
1888
+ test: "Check the result of"
1889
+ };
1890
+ function buildFolderPrompt(job, workspace, options) {
1891
+ const kind = job.stage.kind === "review" || job.stage.kind === "test" ? job.stage.kind : "code";
1892
+ const isCheck = kind !== "code";
1893
+ const people = job.people ?? [];
1894
+ const canAsk = people.length > 0;
1895
+ const prompt = [
1896
+ `${FOLDER_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
1897
+ job.description,
1898
+ briefText(job.spec),
1899
+ job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
1900
+ job.review && `Notes to address:
1901
+ ${job.review}`,
1902
+ teamMessagesText(job.messages)
1903
+ ].filter(Boolean).join("\n\n");
1904
+ const rules = isCheck ? FOLDER_CHECK_RULES : [...FOLDER_DO_RULES, ...job.review ? [FOLDER_REWORK_RULE] : [], FOLDER_REPORT_RULE];
1905
+ const instructions = [
1906
+ canAsk ? ASKING_RULE : UNATTENDED_RULE,
1907
+ MACHINE_RULE,
1908
+ workplaceText(job.computer, workspace.root),
1909
+ stepLine(job.stage),
1910
+ job.stage.instructions,
1911
+ contextText(job.projectContext),
1912
+ `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
1913
+ `How to work:
1914
+ ${bulletList(rules)}`,
1915
+ safetyText(job),
1916
+ knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
1917
+ APPROVAL_RULE,
1918
+ peopleText(people),
1919
+ isCheck ? null : handOverText(job.handOverBoards),
1920
+ handoffText(job.allowedHandoffs, isCheck),
1921
+ options.browser && BROWSER_RULE
1922
+ ].filter(Boolean).join("\n\n");
1923
+ return { prompt, instructions };
1924
+ }
1443
1925
  function buildTaskPrompt(job, workspace, options = {}) {
1444
1926
  const { kind } = job.stage;
1445
1927
  if (kind === "interview" || kind === "spec") {
1446
1928
  if (!job.specWork) {
1447
1929
  throw new Error(SPEC_WORK_MISSING);
1448
1930
  }
1449
- return buildSpecPrompt(job, job.specWork, workspace);
1931
+ return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
1932
+ }
1933
+ if (isFolderJob(job)) {
1934
+ return buildFolderPrompt(job, workspace, options);
1450
1935
  }
1451
1936
  const isCheck = kind !== "code";
1937
+ const readOnly = isReadOnlyCode(job.stage);
1452
1938
  const people = job.people ?? [];
1453
1939
  const canAsk = people.length > 0;
1454
1940
  const prompt = [
1455
- `${STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
1941
+ `${readOnly ? READ_ONLY_GOAL : STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
1456
1942
  job.description,
1457
1943
  specText(job.spec),
1458
1944
  job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
@@ -1462,7 +1948,7 @@ ${job.review}`,
1462
1948
  ].filter(Boolean).join("\n\n");
1463
1949
  const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
1464
1950
  const localApp = localAppRule(job, isCheck, canAsk);
1465
- const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
1951
+ const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : readOnly ? [...READ_ONLY_CODE_RULES, DEPENDENCIES_RULE, localApp, ...evidence, READ_ONLY_REPORT_RULE] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
1466
1952
  const instructions = [
1467
1953
  canAsk ? ASKING_RULE : UNATTENDED_RULE,
1468
1954
  MACHINE_RULE,
@@ -1485,108 +1971,135 @@ ${bulletList(rules)}`,
1485
1971
  }
1486
1972
 
1487
1973
  // src/specTools.ts
1488
- import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1489
- import { z as z2 } from "zod";
1974
+ import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
1975
+ import { z as z3 } from "zod";
1490
1976
  var MAX_FIRST_ROUND = 4;
1491
- var questionSchema = z2.object({
1492
- id: z2.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
1493
- question: z2.string().describe("One plain sentence the owner can answer without reading code"),
1494
- whyItMatters: z2.string().describe("One line: what changes in the result depending on the answer"),
1495
- options: z2.array(z2.object({
1496
- label: z2.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
1497
- description: z2.string().describe("One sentence: what happens if this option is chosen"),
1498
- recommended: z2.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
1977
+ var questionSchema = z3.object({
1978
+ id: z3.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
1979
+ question: z3.string().describe("One plain sentence the owner can answer without reading code"),
1980
+ whyItMatters: z3.string().describe("One line: what changes in the result depending on the answer"),
1981
+ options: z3.array(z3.object({
1982
+ label: z3.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
1983
+ description: z3.string().describe("One sentence: what happens if this option is chosen"),
1984
+ recommended: z3.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
1499
1985
  })).min(2).max(4).describe('2-4 mutually exclusive options grounded in the code and data. The UI always adds a free-text "Other" field, so never add "Other" yourself'),
1500
- multiSelect: z2.boolean().describe("true only when the options are additive and several can apply together")
1986
+ multiSelect: z3.boolean().describe("true only when the options are additive and several can apply together")
1501
1987
  });
1502
- var briefSchema = z2.object({
1503
- findings: z2.array(z2.object({
1504
- fact: z2.string().describe("What the code does today that matters for this task"),
1505
- where: z2.string().describe("Where it is: owner/repo path:line")
1988
+ var briefSchema = z3.object({
1989
+ findings: z3.array(z3.object({
1990
+ fact: z3.string().describe("What the code does today that matters for this task"),
1991
+ where: z3.string().describe("Where it is: owner/repo path:line")
1506
1992
  })).describe("Findings from the code"),
1507
- dataFacts: z2.array(z2.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
1508
- approach: z2.string().describe("The approach you would take, in 2-4 sentences"),
1509
- codeMap: z2.array(z2.object({
1510
- repo: z2.string().describe("Repository full name (owner/name)"),
1511
- file: z2.string().describe("Path relative to the repository root"),
1512
- role: z2.string().describe("Why it matters: what to change or reuse there")
1993
+ dataFacts: z3.array(z3.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
1994
+ approach: z3.string().describe("The approach you would take, in 2-4 sentences"),
1995
+ codeMap: z3.array(z3.object({
1996
+ repo: z3.string().describe("Repository full name (owner/name)"),
1997
+ file: z3.string().describe("Path relative to the repository root"),
1998
+ role: z3.string().describe("Why it matters: what to change or reuse there")
1513
1999
  })).describe("The files the change will touch or must reuse"),
1514
- assumptions: z2.array(z2.object({
1515
- text: z2.string().describe("The default you chose"),
1516
- reason: z2.string().describe("Why it is safe or why it was chosen")
2000
+ assumptions: z3.array(z3.object({
2001
+ text: z3.string().describe("The default you chose"),
2002
+ reason: z3.string().describe("Why it is safe or why it was chosen")
1517
2003
  })).describe("Corners settled with a default instead of a question"),
1518
- pendingDecisions: z2.array(z2.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
2004
+ pendingDecisions: z3.array(z3.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
1519
2005
  });
1520
- var questionsPayloadSchema = z2.object({
2006
+ var questionsPayloadSchema = z3.object({
1521
2007
  brief: briefSchema,
1522
- questions: z2.array(questionSchema).max(MAX_FIRST_ROUND).describe(`0-${MAX_FIRST_ROUND} questions that pass the question test, most important first. An empty list when research settled everything`)
2008
+ questions: z3.array(questionSchema).max(MAX_FIRST_ROUND).describe(`0-${MAX_FIRST_ROUND} questions that pass the question test, most important first. An empty list when research settled everything`)
1523
2009
  });
1524
- var specSchema = z2.object({
1525
- goal: z2.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
1526
- context: z2.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
1527
- inScope: z2.array(z2.string()).describe("What this task delivers, one item each"),
1528
- outOfScope: z2.array(z2.string()).describe("What this task does not do: nearby additions a coder might be tempted to make and approaches that are ruled out. Never empty"),
1529
- decisions: z2.array(z2.object({
1530
- question: z2.string(),
1531
- answer: z2.string(),
1532
- by: z2.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2010
+ var specSchema = z3.object({
2011
+ goal: z3.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2012
+ context: z3.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2013
+ inScope: z3.array(z3.string()).describe("What this task delivers, one item each"),
2014
+ outOfScope: z3.array(z3.string()).describe("What this task does not do: nearby additions a coder might be tempted to make and approaches that are ruled out. Never empty"),
2015
+ decisions: z3.array(z3.object({
2016
+ question: z3.string(),
2017
+ answer: z3.string(),
2018
+ by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
1533
2019
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
1534
- codeMap: z2.array(z2.object({
1535
- repo: z2.string().describe("Repository full name (owner/name)"),
1536
- file: z2.string().describe("Path relative to the repository root"),
1537
- role: z2.string().describe("What the coder needs to know about this file"),
1538
- use: z2.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
2020
+ codeMap: z3.array(z3.object({
2021
+ repo: z3.string().describe("Repository full name (owner/name)"),
2022
+ file: z3.string().describe("Path relative to the repository root"),
2023
+ role: z3.string().describe("What the coder needs to know about this file"),
2024
+ use: z3.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
1539
2025
  })).describe("The files a coder must read before starting"),
1540
- changes: z2.array(z2.object({
1541
- repo: z2.string().describe("Repository full name (owner/name) where the change happens"),
1542
- file: z2.string().describe("Path to the file, relative to the repository root"),
1543
- action: z2.enum(["create", "modify", "delete"]),
1544
- description: z2.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
1545
- produces: z2.array(z2.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
1546
- consumes: z2.array(z2.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
1547
- acceptance: z2.array(z2.string()).describe("Ids of the criteria this change makes true, like AC-1")
2026
+ changes: z3.array(z3.object({
2027
+ repo: z3.string().describe("Repository full name (owner/name) where the change happens"),
2028
+ file: z3.string().describe("Path to the file, relative to the repository root"),
2029
+ action: z3.enum(["create", "modify", "delete"]),
2030
+ description: z3.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
2031
+ produces: z3.array(z3.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2032
+ consumes: z3.array(z3.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2033
+ acceptance: z3.array(z3.string()).describe("Ids of the criteria this change makes true, like AC-1")
1548
2034
  })).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
1549
- acceptance: z2.array(z2.object({
1550
- id: z2.string().describe("AC-1, AC-2 and so on, in order"),
1551
- kind: z2.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
1552
- text: z2.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
2035
+ acceptance: z3.array(z3.object({
2036
+ id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2037
+ kind: z3.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
2038
+ text: z3.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
1553
2039
  })).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
1554
- testPlan: z2.object({
1555
- automated: z2.array(z2.object({
1556
- repo: z2.string(),
1557
- command: z2.string().describe("The exact command to run"),
1558
- expect: z2.string().describe("What passing looks like")
2040
+ testPlan: z3.object({
2041
+ automated: z3.array(z3.object({
2042
+ repo: z3.string(),
2043
+ command: z3.string().describe("The exact command to run"),
2044
+ expect: z3.string().describe("What passing looks like")
1559
2045
  })).describe("At least one command"),
1560
- newTests: z2.array(z2.object({
1561
- seam: z2.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
1562
- covers: z2.array(z2.string()).describe("Ids of the criteria it covers")
2046
+ newTests: z3.array(z3.object({
2047
+ seam: z3.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2048
+ covers: z3.array(z3.string()).describe("Ids of the criteria it covers")
1563
2049
  })),
1564
- manual: z2.array(z2.object({
1565
- page: z2.string().describe("The page or screen"),
1566
- steps: z2.string().describe("What a person does and sees")
2050
+ manual: z3.array(z3.object({
2051
+ page: z3.string().describe("The page or screen"),
2052
+ steps: z3.string().describe("What a person does and sees")
1567
2053
  }))
1568
2054
  }),
1569
- risks: z2.array(z2.object({
1570
- risk: z2.string(),
1571
- mitigation: z2.string()
2055
+ risks: z3.array(z3.object({
2056
+ risk: z3.string(),
2057
+ mitigation: z3.string()
1572
2058
  })).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
1573
- riskLevel: z2.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
1574
- assumptions: z2.array(z2.object({
1575
- text: z2.string().describe("The default you chose"),
1576
- reason: z2.string().describe("Why, so the owner can confirm or correct it quickly")
2059
+ riskLevel: z3.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
2060
+ assumptions: z3.array(z3.object({
2061
+ text: z3.string().describe("The default you chose"),
2062
+ reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
1577
2063
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
1578
- openQuestions: z2.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
1579
- split: z2.array(z2.string()).describe("When the task holds more than one shippable goal or more than about 8 files: the separate tasks you propose, one line each. Empty otherwise")
2064
+ openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
2065
+ split: z3.array(z3.string()).describe("When the task holds more than one shippable goal or more than about 8 files: the separate tasks you propose, one line each. Empty otherwise")
1580
2066
  });
1581
- function reply(text2, isError = false) {
2067
+ var resultBriefSchema = z3.object({
2068
+ format: z3.literal("brief").describe('Always "brief"'),
2069
+ goal: z3.string().describe("What the work is for, for whom, and the outcome people will notice"),
2070
+ context: z3.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
2071
+ contains: z3.array(z3.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
2072
+ acceptance: z3.array(z3.object({
2073
+ id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2074
+ text: z3.string().describe("One observable property of the result that a checker can confirm by reading it")
2075
+ })).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
2076
+ constraints: z3.array(z3.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
2077
+ outOfScope: z3.array(z3.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
2078
+ sources: z3.array(z3.object({
2079
+ source: z3.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
2080
+ use: z3.string().describe("What to take from it")
2081
+ })).describe("Sources the work must use. Empty when the task itself is enough"),
2082
+ approver: z3.string().describe("Who approves the result, as the task or the board names them"),
2083
+ decisions: z3.array(z3.object({
2084
+ question: z3.string(),
2085
+ answer: z3.string(),
2086
+ by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2087
+ })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2088
+ assumptions: z3.array(z3.object({
2089
+ text: z3.string().describe("The default you chose"),
2090
+ reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
2091
+ })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2092
+ openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
2093
+ });
2094
+ function reply2(text2, isError = false) {
1582
2095
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
1583
2096
  }
1584
2097
  function unreachable(error, toolName) {
1585
2098
  const message = error instanceof Error ? error.message : String(error);
1586
- return reply(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
2099
+ return reply2(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
1587
2100
  }
1588
2101
  function questionsTool(review, onSubmit) {
1589
- return tool2(
2102
+ return tool3(
1590
2103
  SUBMIT_QUESTIONS_TOOL_NAME,
1591
2104
  "Hand in the brief and the clarifying questions of this round. Call it once, after the research. Pass an empty questions list when research settled everything: the spec is then written next. If it returns problems, the questions were not saved: fix them and call it again.",
1592
2105
  questionsPayloadSchema.shape,
@@ -1598,10 +2111,10 @@ function questionsTool(review, onSubmit) {
1598
2111
  return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
1599
2112
  }
1600
2113
  if (!verdict.accepted) {
1601
- return reply(verdict.feedback, true);
2114
+ return reply2(verdict.feedback, true);
1602
2115
  }
1603
2116
  onSubmit(args);
1604
- return reply(args.questions.length > 0 ? "Questions saved. The owner answers them on the task page after this run. Finish now with a reply of one or two sentences." : "Saved with no questions: the spec is written next. Finish now with a reply of one or two sentences.");
2117
+ return reply2(args.questions.length > 0 ? "Questions saved. The owner answers them on the task page after this run. Finish now with a reply of one or two sentences." : "Saved with no questions: the spec is written next. Finish now with a reply of one or two sentences.");
1605
2118
  }
1606
2119
  );
1607
2120
  }
@@ -1609,7 +2122,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
1609
2122
  function specTool(review, onSubmit, builderCheckRan) {
1610
2123
  let repairs = 0;
1611
2124
  let reminded = false;
1612
- return tool2(
2125
+ return tool3(
1613
2126
  SUBMIT_SPEC_TOOL_NAME,
1614
2127
  "Hand in the implementation spec. It replaces the previous version entirely, so always send the FULL spec. TAMsense lints it and checks the files first; if it returns feedback, the spec was not saved: fix it and call submit_spec again.",
1615
2128
  specSchema.shape,
@@ -1622,14 +2135,36 @@ function specTool(review, onSubmit, builderCheckRan) {
1622
2135
  }
1623
2136
  if (!verdict.accepted) {
1624
2137
  repairs += 1;
1625
- return reply(verdict.feedback, true);
2138
+ return reply2(verdict.feedback, true);
1626
2139
  }
1627
2140
  if (!builderCheckRan() && !reminded) {
1628
2141
  reminded = true;
1629
- return reply(BUILDER_CHECK_FIRST, true);
2142
+ return reply2(BUILDER_CHECK_FIRST, true);
2143
+ }
2144
+ onSubmit({ spec: args, problems: verdict.problems });
2145
+ return reply2(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2146
+ }
2147
+ );
2148
+ }
2149
+ function briefTool(review, onSubmit) {
2150
+ let repairs = 0;
2151
+ return tool3(
2152
+ SUBMIT_SPEC_TOOL_NAME,
2153
+ "Hand in the brief of the result. It replaces the previous version entirely, so always send the FULL brief. TAMsense lints it first; if it returns feedback, the brief was not saved: fix it and call submit_spec again.",
2154
+ resultBriefSchema.shape,
2155
+ async (args) => {
2156
+ let verdict;
2157
+ try {
2158
+ verdict = await review({ spec: args, repairs });
2159
+ } catch (error) {
2160
+ return unreachable(error, SUBMIT_SPEC_TOOL_NAME);
2161
+ }
2162
+ if (!verdict.accepted) {
2163
+ repairs += 1;
2164
+ return reply2(verdict.feedback, true);
1630
2165
  }
1631
2166
  onSubmit({ spec: args, problems: verdict.problems });
1632
- return reply(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2167
+ return reply2(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
1633
2168
  }
1634
2169
  );
1635
2170
  }
@@ -1731,10 +2266,9 @@ function usageLimitWatch() {
1731
2266
  // src/claude.ts
1732
2267
  var NO_RESULT_TEXT = "Claude finished without a result.";
1733
2268
  var SERVER_NAME = "tamsense";
1734
- var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
1735
2269
  var CAPTION_FIELDS = {
1736
- title: z3.string().describe('short name of what it shows, at most 60 characters: "Design option 1", "Feature testing", "Feature testing, attempt 2". Never generic names like "screenshot1"'),
1737
- purpose: z3.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
2270
+ title: z4.string().describe('short name of what it shows, at most 60 characters: "Design option 1", "Feature testing", "Feature testing, attempt 2". Never generic names like "screenshot1"'),
2271
+ purpose: z4.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
1738
2272
  };
1739
2273
  var ATTACH_TOOLS = {
1740
2274
  screenshot: {
@@ -1755,21 +2289,21 @@ var ATTACH_TOOLS = {
1755
2289
  };
1756
2290
  function attachTool(kind, attach) {
1757
2291
  const spec = ATTACH_TOOLS[kind];
1758
- return tool3(
2292
+ return tool4(
1759
2293
  spec.name,
1760
2294
  spec.description,
1761
- { path: z3.string().describe(spec.path), ...CAPTION_FIELDS },
2295
+ { path: z4.string().describe(spec.path), ...CAPTION_FIELDS },
1762
2296
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
1763
2297
  );
1764
2298
  }
1765
2299
  function verdictTool(onVerdict) {
1766
- return tool3(
2300
+ return tool4(
1767
2301
  VERDICT_TOOL_NAME,
1768
2302
  "Hand in the result of this check. Call it exactly once, at the very end.",
1769
2303
  {
1770
- passed: z3.boolean().describe("true only if nothing must be fixed"),
1771
- report: z3.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
1772
- couldNotRun: z3.boolean().optional().describe("true when the check could not run at all because of this computer: Docker not running, no local setup, or settings that point outside this computer; passed must be false and the report gives the exact reason and what a person should do")
2304
+ passed: z4.boolean().describe("true only if nothing must be fixed"),
2305
+ report: z4.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
2306
+ couldNotRun: z4.boolean().optional().describe("true when the check could not run at all because of this computer: Docker not running, no local setup, or settings that point outside this computer; passed must be false and the report gives the exact reason and what a person should do")
1773
2307
  },
1774
2308
  async (args) => {
1775
2309
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -1778,13 +2312,13 @@ function verdictTool(onVerdict) {
1778
2312
  );
1779
2313
  }
1780
2314
  function handoffTool(targets, onHandoff) {
1781
- return tool3(
2315
+ return tool4(
1782
2316
  HANDOFF_TOOL_NAME,
1783
2317
  "Hand the task to the next step of the TAMsense board when you finish. The next agent and the task owner read your note. Only one handoff counts: calling it again replaces the earlier one.",
1784
2318
  {
1785
- to: z3.string().describe("name or id of the step to hand the task to"),
1786
- outcome: z3.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
1787
- note: z3.string().describe("for the next agent and the owner: what was done, what to check, what is open")
2319
+ to: z4.string().describe("name or id of the step to hand the task to"),
2320
+ outcome: z4.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
2321
+ note: z4.string().describe("for the next agent and the owner: what was done, what to check, what is open")
1788
2322
  },
1789
2323
  async (args) => {
1790
2324
  const choice = chooseHandoff(targets, args);
@@ -1797,25 +2331,25 @@ function handoffTool(targets, onHandoff) {
1797
2331
  );
1798
2332
  }
1799
2333
  function askPersonTool(asking, waiting) {
1800
- return tool3(
2334
+ return tool4(
1801
2335
  ASK_PERSON_TOOL_NAME,
1802
2336
  "Ask a person on this TAMsense task when you are stuck or the decision belongs to a person: intent, design, access, or how the team runs something locally. Never ask what the code or the project knowledge already answers. With blocking true the run pauses and frees the computer until the answer comes; with blocking false you keep working on your assumption.",
1803
2337
  {
1804
- to: z3.union([z3.number(), z3.string()]).describe("id of the person from the people list in your instructions: the assignee first; a topic question goes to the person whose role fits"),
1805
- why: z3.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
1806
- question: z3.string().describe("one plain question the person can answer without reading the code"),
1807
- context: z3.string().optional().describe("one line on what you saw that led to the question"),
1808
- tried: z3.array(z3.object({
1809
- did: z3.string().describe("the command or step"),
1810
- error: z3.string().optional().describe("the exact error line, if there was one"),
1811
- result: z3.string().describe("what it showed, one line")
2338
+ to: z4.union([z4.number(), z4.string()]).describe("id of the person from the people list in your instructions: the assignee first; a topic question goes to the person whose role fits"),
2339
+ why: z4.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
2340
+ question: z4.string().describe("one plain question the person can answer without reading the code"),
2341
+ context: z4.string().optional().describe("one line on what you saw that led to the question"),
2342
+ tried: z4.array(z4.object({
2343
+ did: z4.string().describe("the command or step"),
2344
+ error: z4.string().optional().describe("the exact error line, if there was one"),
2345
+ result: z4.string().describe("what it showed, one line")
1812
2346
  })).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
1813
- options: z3.array(z3.object({
1814
- label: z3.string().describe("a short answer the person can pick"),
1815
- detail: z3.string().describe("one line on what happens if it is picked")
2347
+ options: z4.array(z4.object({
2348
+ label: z4.string().describe("a short answer the person can pick"),
2349
+ detail: z4.string().describe("one line on what happens if it is picked")
1816
2350
  })).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
1817
- blocking: z3.boolean().describe("true when you cannot go on without the answer"),
1818
- assumption: z3.string().optional().describe("when blocking is false: what you continue with until the answer comes")
2351
+ blocking: z4.boolean().describe("true when you cannot go on without the answer"),
2352
+ assumption: z4.string().optional().describe("when blocking is false: what you continue with until the answer comes")
1819
2353
  },
1820
2354
  async (args) => {
1821
2355
  if (waiting.waitingFor !== null) {
@@ -1825,16 +2359,16 @@ function askPersonTool(asking, waiting) {
1825
2359
  if ("error" in choice) {
1826
2360
  return { content: [{ type: "text", text: choice.error }], isError: true };
1827
2361
  }
1828
- let reply2;
2362
+ let reply3;
1829
2363
  try {
1830
- reply2 = await asking.send(choice.body);
2364
+ reply3 = await asking.send(choice.body);
1831
2365
  } catch (error) {
1832
2366
  return { content: [{ type: "text", text: `Could not send the question: ${error instanceof Error ? error.message : String(error)}. Decide yourself and say so in your report.` }], isError: true };
1833
2367
  }
1834
2368
  if (choice.body.blocking) {
1835
- waiting.waitingFor = reply2.id;
2369
+ waiting.waitingFor = reply3.id;
1836
2370
  }
1837
- return { content: [{ type: "text", text: askedText(reply2.name, choice.body) }] };
2371
+ return { content: [{ type: "text", text: askedText(reply3.name, choice.body) }] };
1838
2372
  }
1839
2373
  );
1840
2374
  }
@@ -1842,8 +2376,13 @@ function specWorkTools(specWork, submitted) {
1842
2376
  if (!specWork) {
1843
2377
  return [];
1844
2378
  }
1845
- return specWork.stage === "interview" ? [questionsTool(specWork.review, (questions) => {
1846
- submitted.questions = questions;
2379
+ if (specWork.stage === "interview") {
2380
+ return [questionsTool(specWork.review, (questions) => {
2381
+ submitted.questions = questions;
2382
+ })];
2383
+ }
2384
+ return specWork.brief ? [briefTool(specWork.review, (brief) => {
2385
+ submitted.spec = brief;
1847
2386
  })] : [specTool(specWork.review, (spec) => {
1848
2387
  submitted.spec = spec;
1849
2388
  }, () => submitted.helperStarted)];
@@ -1871,7 +2410,9 @@ function tamsenseServer(request, submitted) {
1871
2410
  ...knowledgeTools(request.knowledge),
1872
2411
  ...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
1873
2412
  submitted.handoff = handoff;
1874
- })] : []
2413
+ })] : [],
2414
+ ...request.results ? resultToolset(request.results) : [],
2415
+ ...request.handOver ? [handOverTool(request.handOver)] : []
1875
2416
  ];
1876
2417
  return {
1877
2418
  server: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }),
@@ -1898,6 +2439,7 @@ async function attempt(request, context, resume) {
1898
2439
  let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
1899
2440
  const tamsense = tamsenseServer(request, context.submitted);
1900
2441
  const shown = hiddenStepsFilter();
2442
+ const helpers = helperTasks();
1901
2443
  const messages = context.startQuery({
1902
2444
  prompt: request.input.start(request.prompt.prompt),
1903
2445
  options: {
@@ -1909,7 +2451,7 @@ async function attempt(request, context, resume) {
1909
2451
  thinking: { type: "adaptive", display: "summarized" },
1910
2452
  // режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
1911
2453
  canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
1912
- plugins: [{ type: "local", path: LIBRARY_PATH }],
2454
+ plugins: [{ type: "local", path: request.library }],
1913
2455
  mcpServers: { [SERVER_NAME]: tamsense.server },
1914
2456
  allowedTools: tamsense.allowed,
1915
2457
  abortController: context.abortController,
@@ -1919,6 +2461,7 @@ async function attempt(request, context, resume) {
1919
2461
  }
1920
2462
  });
1921
2463
  request.input.onSwitch((change) => applySwitch(messages, change));
2464
+ request.input.onStopHelper((ref) => helpers.stop(messages, ref));
1922
2465
  const failedToResume = () => resume !== null && sessionId === null && !request.signal.aborted;
1923
2466
  try {
1924
2467
  for await (const message of messages) {
@@ -1930,7 +2473,7 @@ async function attempt(request, context, resume) {
1930
2473
  messages.close();
1931
2474
  return { outcome, sessionId, resumeFailed: true };
1932
2475
  }
1933
- for (const draft of feedOf(message).filter(shown)) {
2476
+ for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
1934
2477
  if (draft.kind === "agent" && draft.parent === null) {
1935
2478
  context.submitted.helperStarted = true;
1936
2479
  }
@@ -1981,13 +2524,13 @@ function claudeRunner(startQuery) {
1981
2524
  var runClaude = claudeRunner(query2);
1982
2525
 
1983
2526
  // src/config.ts
1984
- import { mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
2527
+ import { mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1985
2528
  import { homedir as homedir2 } from "os";
1986
- import { dirname as dirname2, join as join3 } from "path";
2529
+ import { dirname as dirname2, join as join4 } from "path";
1987
2530
  var OWNER_ONLY_FILE = 384;
1988
2531
  var OWNER_ONLY_FOLDER = 448;
1989
2532
  function configPath(home = homedir2()) {
1990
- return join3(home, ".tamsense", "runner.json");
2533
+ return join4(home, ".tamsense", "runner.json");
1991
2534
  }
1992
2535
  function isRecord2(value) {
1993
2536
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2005,7 +2548,7 @@ function isMissingFile(error) {
2005
2548
  }
2006
2549
  async function readConfig(path = configPath()) {
2007
2550
  try {
2008
- return parseConfig(JSON.parse(await readFile2(path, "utf8")));
2551
+ return parseConfig(JSON.parse(await readFile3(path, "utf8")));
2009
2552
  } catch (error) {
2010
2553
  if (isMissingFile(error) || error instanceof SyntaxError) {
2011
2554
  return null;
@@ -2014,34 +2557,12 @@ async function readConfig(path = configPath()) {
2014
2557
  }
2015
2558
  }
2016
2559
  async function writeConfig(config, path = configPath()) {
2017
- await mkdir2(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
2018
- await writeFile2(path, `${JSON.stringify(config, null, 4)}
2560
+ await mkdir3(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
2561
+ await writeFile3(path, `${JSON.stringify(config, null, 4)}
2019
2562
  `, { mode: OWNER_ONLY_FILE });
2020
2563
  }
2021
2564
  async function removeConfig(path = configPath()) {
2022
- await rm3(path, { force: true });
2023
- }
2024
-
2025
- // src/protocol.ts
2026
- var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
2027
- var MAX_ATTACHMENTS_PER_RUN = 20;
2028
- var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
2029
- var PAIRINGS_PATH = "/api/runner/pairings";
2030
- var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
2031
- var IDENTITY_PATH = "/api/runner/me";
2032
- var NEXT_RUN_PATH = "/api/runner/runs/next";
2033
- var CAPABILITIES_PATH = "/api/runner/me/capabilities";
2034
- function runPath(runId, action) {
2035
- return `/api/runner/runs/${runId}/${action}`;
2036
- }
2037
- function knowledgePath(runId, entryId) {
2038
- return `/api/runner/runs/${runId}/knowledge/${entryId}`;
2039
- }
2040
- function permissionPath(runId, permissionId) {
2041
- return `/api/runner/runs/${runId}/permissions/${permissionId}`;
2042
- }
2043
- function recordingUploadPath(runId, upload, part) {
2044
- return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
2565
+ await rm4(path, { force: true });
2045
2566
  }
2046
2567
 
2047
2568
  // src/login.ts
@@ -2058,14 +2579,14 @@ async function login(tools, request) {
2058
2579
  tools.openBrowser(pageUrl);
2059
2580
  while (tools.now() < expiresAt) {
2060
2581
  await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
2061
- const reply2 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
2062
- if (reply2.status === "connected") {
2063
- return { token: reply2.token, device: reply2.device };
2582
+ const reply3 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
2583
+ if (reply3.status === "connected") {
2584
+ return { token: reply3.token, device: reply3.device };
2064
2585
  }
2065
- if (reply2.status === "denied") {
2586
+ if (reply3.status === "denied") {
2066
2587
  throw new Error(DENIED_TEXT);
2067
2588
  }
2068
- if (reply2.status === "expired") {
2589
+ if (reply3.status === "expired") {
2069
2590
  break;
2070
2591
  }
2071
2592
  }
@@ -2074,13 +2595,13 @@ async function login(tools, request) {
2074
2595
 
2075
2596
  // src/recorderEnv.ts
2076
2597
  import { existsSync as existsSync3 } from "fs";
2077
- import { join as join5 } from "path";
2078
- import { fileURLToPath as fileURLToPath2 } from "url";
2598
+ import { join as join6 } from "path";
2599
+ import { fileURLToPath } from "url";
2079
2600
 
2080
2601
  // src/recorder.ts
2081
2602
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2082
2603
  import Module, { createRequire } from "module";
2083
- import { basename, dirname as dirname3, join as join4 } from "path";
2604
+ import { basename as basename2, dirname as dirname3, join as join5 } from "path";
2084
2605
  import process2 from "process";
2085
2606
  var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
2086
2607
  var SERVER_ENV = "TAMSENSE_SERVER";
@@ -2153,7 +2674,7 @@ function clip2(text2, max) {
2153
2674
  return text2.length > max ? `${text2.slice(0, max - 1).trimEnd()}\u2026` : text2;
2154
2675
  }
2155
2676
  function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
2156
- const from = script2 ? `, opened by ${basename(script2)}` : "";
2677
+ const from = script2 ? `, opened by ${basename2(script2)}` : "";
2157
2678
  return {
2158
2679
  title: clip2(env[DEFAULT_TITLE_ENV]?.trim() || "Browser session", MAX_TITLE_LENGTH),
2159
2680
  purpose: clip2(`${env[DEFAULT_PURPOSE_ENV]?.trim() || "Recorded automatically while the agent used a browser"}${from}`, MAX_PURPOSE_LENGTH)
@@ -2161,7 +2682,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
2161
2682
  }
2162
2683
  function rrwebSource() {
2163
2684
  const entry = createRequire(import.meta.url).resolve("rrweb");
2164
- return readFileSync(join4(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
2685
+ return readFileSync(join5(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
2165
2686
  }
2166
2687
  function pageScript(rrweb) {
2167
2688
  return `(() => {
@@ -2212,8 +2733,8 @@ var Session = class {
2212
2733
  bytes = 0;
2213
2734
  constructor(dir, recording, url) {
2214
2735
  const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
2215
- this.file = join4(dir, `${id}${REPLAY_SUFFIX}`);
2216
- this.metaFile = join4(dir, `${id}${META_SUFFIX}`);
2736
+ this.file = join5(dir, `${id}${REPLAY_SUFFIX}`);
2737
+ this.metaFile = join5(dir, `${id}${META_SUFFIX}`);
2217
2738
  this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic };
2218
2739
  writeFileSync(this.file, "");
2219
2740
  this.save();
@@ -2385,11 +2906,11 @@ if (process2.env[RECORDINGS_DIR_ENV]) {
2385
2906
  // src/recorderEnv.ts
2386
2907
  var RECORDER_ENV = "TAMSENSE_RECORDER";
2387
2908
  function bundledRecorder() {
2388
- const path = fileURLToPath2(new URL("./recorder.cjs", import.meta.url));
2909
+ const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
2389
2910
  return existsSync3(path) ? path : null;
2390
2911
  }
2391
2912
  function recordingsFolder(home, runId) {
2392
- return join5(home, ".tamsense", "recordings", `run-${runId}`);
2913
+ return join6(home, ".tamsense", "recordings", `run-${runId}`);
2393
2914
  }
2394
2915
  function nodeOptionsWithRecorder(recorder, nodeOptions) {
2395
2916
  return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
@@ -2425,12 +2946,12 @@ var PLATFORM_LABELS = {
2425
2946
  darwin: "macOS",
2426
2947
  linux: "Linux"
2427
2948
  };
2428
- var MAX_NAME_LENGTH = 80;
2949
+ var MAX_NAME_LENGTH2 = 80;
2429
2950
  function platformLabel(platform = process.platform) {
2430
2951
  return PLATFORM_LABELS[platform] ?? platform;
2431
2952
  }
2432
2953
  function defaultDeviceName() {
2433
- return hostname().slice(0, MAX_NAME_LENGTH) || "My computer";
2954
+ return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
2434
2955
  }
2435
2956
  function browserCommand(url, platform) {
2436
2957
  if (platform === "win32") {
@@ -2515,7 +3036,7 @@ function relaunchArgs(argv) {
2515
3036
  }
2516
3037
  function relaunch(argv, env) {
2517
3038
  const isWindows = process3.platform === "win32";
2518
- return new Promise((resolve2, reject) => {
3039
+ return new Promise((resolve3, reject) => {
2519
3040
  const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
2520
3041
  const ignoreInterrupt = () => {
2521
3042
  };
@@ -2526,13 +3047,13 @@ function relaunch(argv, env) {
2526
3047
  });
2527
3048
  child.on("close", (code) => {
2528
3049
  process3.off("SIGINT", ignoreInterrupt);
2529
- resolve2(code ?? 1);
3050
+ resolve3(code ?? 1);
2530
3051
  });
2531
3052
  });
2532
3053
  }
2533
3054
 
2534
3055
  // src/worker.ts
2535
- import { rm as rm4 } from "fs/promises";
3056
+ import { rm as rm6 } from "fs/promises";
2536
3057
  import { dirname as dirname5 } from "path";
2537
3058
  import process4 from "process";
2538
3059
 
@@ -2572,11 +3093,43 @@ function feedQueue(send) {
2572
3093
  };
2573
3094
  }
2574
3095
 
3096
+ // src/library.ts
3097
+ import { existsSync as existsSync4 } from "fs";
3098
+ import { cp, mkdir as mkdir4, rm as rm5 } from "fs/promises";
3099
+ import { join as join7 } from "path";
3100
+ import { fileURLToPath as fileURLToPath2 } from "url";
3101
+ var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
3102
+ var SKILL_NAME = /^[a-z0-9-]+$/;
3103
+ var STAGE_SKILLS = {
3104
+ interview: ["task-interview"],
3105
+ spec: ["spec-writing", "task-interview"]
3106
+ };
3107
+ function allowedSkills(stage) {
3108
+ const skills = stage.role?.skills ?? null;
3109
+ return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
3110
+ }
3111
+ function roleLibraryFolder(home, runId) {
3112
+ return join7(home, ".tamsense", "libraries", `run-${runId}`);
3113
+ }
3114
+ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3115
+ if (skills === null) {
3116
+ return source;
3117
+ }
3118
+ await rm5(folder, { recursive: true, force: true });
3119
+ await mkdir4(join7(folder, "skills"), { recursive: true });
3120
+ await cp(join7(source, ".claude-plugin"), join7(folder, ".claude-plugin"), { recursive: true });
3121
+ await cp(join7(source, "agents"), join7(folder, "agents"), { recursive: true });
3122
+ for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync4(join7(source, "skills", name)))) {
3123
+ await cp(join7(source, "skills", skill), join7(folder, "skills", skill), { recursive: true });
3124
+ }
3125
+ return folder;
3126
+ }
3127
+
2575
3128
  // src/recordings.ts
2576
3129
  import { randomUUID } from "crypto";
2577
3130
  import { createReadStream } from "fs";
2578
- import { readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
2579
- import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as join6 } from "path";
3131
+ import { readdir, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
3132
+ import { basename as basename3, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute2, join as join8 } from "path";
2580
3133
  import { createInterface as createInterface2 } from "readline";
2581
3134
  import { Readable } from "stream";
2582
3135
  import { promisify } from "util";
@@ -2637,23 +3190,23 @@ function marksFrom(marks, startTs) {
2637
3190
  return marks.map((mark) => ({ at: Math.max(0, mark.ts - startTs), label: mark.label })).sort((a, b) => a.at - b.at);
2638
3191
  }
2639
3192
  function sessionIdOf(file) {
2640
- return basename2(file).replace(REPLAY_SUFFIX, "");
3193
+ return basename3(file).replace(REPLAY_SUFFIX, "");
2641
3194
  }
2642
3195
  async function readMeta(file) {
2643
3196
  try {
2644
- return JSON.parse(await readFile3(file, "utf8"));
3197
+ return JSON.parse(await readFile4(file, "utf8"));
2645
3198
  } catch {
2646
3199
  return null;
2647
3200
  }
2648
3201
  }
2649
3202
  async function metaFor(kind, file) {
2650
3203
  if (kind === "replay") {
2651
- return readMeta(join6(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
3204
+ return readMeta(join8(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
2652
3205
  }
2653
- const folder = basename2(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
3206
+ const folder = basename3(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
2654
3207
  for (const name of await readdir(folder).catch(() => [])) {
2655
3208
  if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
2656
- const meta = await readMeta(join6(folder, name));
3209
+ const meta = await readMeta(join8(folder, name));
2657
3210
  if (meta?.video === file) {
2658
3211
  return meta;
2659
3212
  }
@@ -2663,7 +3216,7 @@ async function metaFor(kind, file) {
2663
3216
  }
2664
3217
  async function ledger(dir) {
2665
3218
  try {
2666
- return new Set(JSON.parse(await readFile3(join6(dir, LEDGER_FILE), "utf8")));
3219
+ return new Set(JSON.parse(await readFile4(join8(dir, LEDGER_FILE), "utf8")));
2667
3220
  } catch {
2668
3221
  return /* @__PURE__ */ new Set();
2669
3222
  }
@@ -2671,10 +3224,10 @@ async function ledger(dir) {
2671
3224
  async function remember(dir, file) {
2672
3225
  const done2 = await ledger(dir);
2673
3226
  done2.add(file);
2674
- await writeFile3(join6(dir, LEDGER_FILE), JSON.stringify([...done2]));
3227
+ await writeFile4(join8(dir, LEDGER_FILE), JSON.stringify([...done2]));
2675
3228
  }
2676
3229
  async function nonEmpty(file) {
2677
- return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
3230
+ return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
2678
3231
  }
2679
3232
  async function pendingRecordings(dir) {
2680
3233
  const done2 = await ledger(dir);
@@ -2684,8 +3237,8 @@ async function pendingRecordings(dir) {
2684
3237
  if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
2685
3238
  continue;
2686
3239
  }
2687
- const meta = await readMeta(join6(dir, name));
2688
- const replay = join6(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
3240
+ const meta = await readMeta(join8(dir, name));
3241
+ const replay = join8(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
2689
3242
  if (!done2.has(replay) && await nonEmpty(replay)) {
2690
3243
  pending.push({ kind: "replay", file: replay, meta });
2691
3244
  }
@@ -2705,11 +3258,11 @@ async function uploadReplay(upload, file, caption) {
2705
3258
  if (!scan.snapshot) {
2706
3259
  throw new Error("This replay has no page snapshot, so it cannot be played. Was the page ever opened?");
2707
3260
  }
2708
- return finish(upload, { kind: "replay", upload: id, name: basename2(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
3261
+ return finish(upload, { kind: "replay", upload: id, name: basename3(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
2709
3262
  }
2710
3263
  async function uploadVideo(upload, file, caption, meta) {
2711
- const contentType = VIDEO_TYPES_BY_EXTENSION[extname(file).toLowerCase()];
2712
- const info = await stat(file);
3264
+ const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
3265
+ const info = await stat2(file);
2713
3266
  if (info.size > MAX_VIDEO_BYTES) {
2714
3267
  throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
2715
3268
  }
@@ -2720,16 +3273,16 @@ async function uploadVideo(upload, file, caption, meta) {
2720
3273
  const endedAt = meta?.endedAt ?? info.mtimeMs;
2721
3274
  const durationMs = Math.max(0, Math.round(endedAt - startedAt));
2722
3275
  const marks = marksFrom(meta?.marks ?? [], startedAt).filter((mark) => mark.at <= durationMs);
2723
- return finish(upload, { kind: "video", upload: id, name: basename2(file), ...caption, contentType, durationMs, marks });
3276
+ return finish(upload, { kind: "video", upload: id, name: basename3(file), ...caption, contentType, durationMs, marks });
2724
3277
  }
2725
3278
  async function finish(upload, body) {
2726
3279
  return (await upload.api.request("POST", runPath(upload.runId, "recordings"), body)).id;
2727
3280
  }
2728
3281
  async function attachRecording(api, runId, recordingsDir2, request) {
2729
- if (!isAbsolute(request.path)) {
3282
+ if (!isAbsolute2(request.path)) {
2730
3283
  return `Give the absolute path of the ${request.kind === "replay" ? "replay (.rrweb.ndjson)" : "video (.webm or .mp4)"} file.`;
2731
3284
  }
2732
- const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) : extname(request.path).toLowerCase() in VIDEO_TYPES_BY_EXTENSION;
3285
+ const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) : extname2(request.path).toLowerCase() in VIDEO_TYPES_BY_EXTENSION;
2733
3286
  if (!fitsKind) {
2734
3287
  return request.kind === "replay" ? `A replay is the ${REPLAY_SUFFIX} file the TAMsense recorder wrote.` : "Only WebM and MP4 videos can be attached.";
2735
3288
  }
@@ -2794,19 +3347,19 @@ async function uploadPendingRecordings(api, runId, dir) {
2794
3347
  }
2795
3348
 
2796
3349
  // src/screenshot.ts
2797
- import { readFile as readFile4, stat as stat2 } from "fs/promises";
2798
- import { basename as basename3, extname as extname2, isAbsolute as isAbsolute2 } from "path";
2799
- var TYPES_BY_EXTENSION = {
3350
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
3351
+ import { basename as basename4, extname as extname3, isAbsolute as isAbsolute3 } from "path";
3352
+ var TYPES_BY_EXTENSION2 = {
2800
3353
  ".png": "image/png",
2801
3354
  ".jpg": "image/jpeg",
2802
3355
  ".jpeg": "image/jpeg",
2803
3356
  ".webp": "image/webp"
2804
3357
  };
2805
3358
  async function readScreenshot(request) {
2806
- if (!isAbsolute2(request.path)) {
3359
+ if (!isAbsolute3(request.path)) {
2807
3360
  return "Give the absolute path of the image file.";
2808
3361
  }
2809
- const contentType = TYPES_BY_EXTENSION[extname2(request.path).toLowerCase()];
3362
+ const contentType = TYPES_BY_EXTENSION2[extname3(request.path).toLowerCase()];
2810
3363
  if (!contentType) {
2811
3364
  return "Only PNG, JPEG and WebP images can be attached.";
2812
3365
  }
@@ -2816,15 +3369,15 @@ async function readScreenshot(request) {
2816
3369
  } catch (error) {
2817
3370
  return error instanceof Error ? error.message : String(error);
2818
3371
  }
2819
- const info = await stat2(request.path).catch(() => null);
3372
+ const info = await stat3(request.path).catch(() => null);
2820
3373
  if (!info?.isFile()) {
2821
3374
  return `There is no file at ${request.path}.`;
2822
3375
  }
2823
3376
  if (info.size > MAX_ATTACHMENT_BYTES) {
2824
3377
  return "The image is larger than 5 MB. Take a smaller screenshot (viewport instead of full page, or JPEG).";
2825
3378
  }
2826
- const data = (await readFile4(request.path)).toString("base64");
2827
- return { name: basename3(request.path), ...caption, contentType, data };
3379
+ const data = (await readFile5(request.path)).toString("base64");
3380
+ return { name: basename4(request.path), ...caption, contentType, data };
2828
3381
  }
2829
3382
 
2830
3383
  // src/worker.ts
@@ -2887,17 +3440,31 @@ function messageLine(message) {
2887
3440
  const line = `Message from ${message.authorName}: ${message.text}`.replace(/\s+/g, " ").trim();
2888
3441
  return line.length > MAX_MESSAGE_LINE ? `${line.slice(0, MAX_MESSAGE_LINE - 1)}\u2026` : line;
2889
3442
  }
3443
+ async function stopHelper(input, stop, print2) {
3444
+ try {
3445
+ const result = await input.stopHelper(stop.ref);
3446
+ if (result === "stopped") {
3447
+ input.note(helperStoppedNote(stop), "queue");
3448
+ print2(` ${stop.byName} stopped the helper ${stop.name}`);
3449
+ } else if (result === "unknown" && input.note(helperStopRequest(stop), "steer")) {
3450
+ print2(` ${stop.byName} asked Claude to stop the helper ${stop.name}`);
3451
+ }
3452
+ } catch (error) {
3453
+ print2(`Could not stop the helper ${stop.name}: ${messageOf3(error)}`);
3454
+ }
3455
+ }
2890
3456
  function inbox(job, tools, record, closedOnSite) {
2891
3457
  const input = inputChannel({ onIdle: () => void poll() });
3458
+ const stopsSeen = /* @__PURE__ */ new Set();
2892
3459
  let running = null;
2893
3460
  let again = false;
2894
3461
  let failing = false;
2895
3462
  async function check() {
2896
3463
  const ack = input.takeDelivered();
2897
3464
  const body = { ack };
2898
- let reply2;
3465
+ let reply3;
2899
3466
  try {
2900
- reply2 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
3467
+ reply3 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
2901
3468
  } catch (error) {
2902
3469
  input.restoreDelivered(ack);
2903
3470
  if (!failing) {
@@ -2907,17 +3474,23 @@ function inbox(job, tools, record, closedOnSite) {
2907
3474
  return;
2908
3475
  }
2909
3476
  failing = false;
2910
- if (!reply2.keepGoing) {
3477
+ if (!reply3.keepGoing) {
2911
3478
  closedOnSite();
2912
3479
  return;
2913
3480
  }
2914
- for (const message of reply2.messages) {
3481
+ for (const message of reply3.messages) {
2915
3482
  if (input.push(message)) {
2916
3483
  tools.print(` ${messageLine(message)}`);
2917
3484
  }
2918
3485
  }
2919
- if (reply2.model !== null || reply2.effort !== null) {
2920
- const change = { model: reply2.model, effort: reply2.effort };
3486
+ for (const stop of reply3.helperStops ?? []) {
3487
+ if (!stopsSeen.has(stop.ref)) {
3488
+ stopsSeen.add(stop.ref);
3489
+ void stopHelper(input, stop, tools.print);
3490
+ }
3491
+ }
3492
+ if (reply3.model !== null || reply3.effort !== null) {
3493
+ const change = { model: reply3.model, effort: reply3.effort };
2921
3494
  void input.switchModel(change).then(
2922
3495
  (applied) => {
2923
3496
  if (applied) {
@@ -2957,7 +3530,7 @@ function specWorkTools2(job, api) {
2957
3530
  if (kind !== "interview" && kind !== "spec") {
2958
3531
  return null;
2959
3532
  }
2960
- return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body) };
3533
+ return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body), brief: isFolderJob(job) };
2961
3534
  }
2962
3535
  function askingTools(job, api) {
2963
3536
  const people = job.people ?? [];
@@ -2972,11 +3545,11 @@ function permissionTools(job, tools) {
2972
3545
  }
2973
3546
  return {
2974
3547
  send: async (body) => {
2975
- const reply2 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
2976
- if (reply2.decision === "wait") {
3548
+ const reply3 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
3549
+ if (reply3.decision === "wait") {
2977
3550
  tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
2978
3551
  }
2979
- return reply2;
3552
+ return reply3;
2980
3553
  },
2981
3554
  check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
2982
3555
  sleep: tools.sleep
@@ -2995,11 +3568,27 @@ function knowledgeTools2(job, api) {
2995
3568
  read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
2996
3569
  };
2997
3570
  }
2998
- async function executeJob(job, tools, signal, input, record) {
2999
- const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
3571
+ function skillsLine(skills) {
3572
+ return `This role uses ${skills.length} skill(s) from the library: ${skills.join(", ")}.`;
3573
+ }
3574
+ async function prepare(job, tools) {
3575
+ if (isFolderJob(job)) {
3576
+ tools.print("Preparing the task folder\u2026");
3577
+ const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
3578
+ return { workspace: workspace2, results, identity: {} };
3579
+ }
3000
3580
  tools.print("Preparing the code\u2026");
3001
3581
  const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
3002
- const identity = await commitIdentityEnv(tools.home);
3582
+ return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
3583
+ }
3584
+ async function executeJob(job, tools, signal, input, record) {
3585
+ const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
3586
+ const { workspace, results, identity } = await prepare(job, tools);
3587
+ const skills = allowedSkills(job.stage);
3588
+ if (skills) {
3589
+ tools.print(skillsLine(skills));
3590
+ }
3591
+ const library = await prepareLibrary(roleLibraryFolder(tools.home, job.runId), skills);
3003
3592
  signal.throwIfAborted();
3004
3593
  tools.print("Claude is working\u2026");
3005
3594
  const attached = { count: 0 };
@@ -3009,7 +3598,8 @@ async function executeJob(job, tools, signal, input, record) {
3009
3598
  const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
3010
3599
  const outcome = await tools.runAgent({
3011
3600
  cwd: workspace.root,
3012
- prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder) }),
3601
+ library,
3602
+ prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder), results }),
3013
3603
  input,
3014
3604
  env: { ...baseEnv, ...tools.browser ? browserEnv(tools.browser) : {}, ...recording, ...identity },
3015
3605
  signal,
@@ -3024,12 +3614,14 @@ async function executeJob(job, tools, signal, input, record) {
3024
3614
  effort: job.effort,
3025
3615
  onFeed: record,
3026
3616
  onSession: session.report,
3027
- attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request)
3617
+ attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
3618
+ results: resultTools(job, tools.api, workspace.root),
3619
+ handOver: handOverTools(job, tools.api)
3028
3620
  });
3029
3621
  await session.sent();
3030
3622
  signal.throwIfAborted();
3031
3623
  const waitingFor = outcome.waitingFor ?? null;
3032
- const shouldPublish = job.stage.kind === "code" && outcome.completed && waitingFor === null;
3624
+ const shouldPublish = job.stage.kind === "code" && !isFolderJob(job) && !isReadOnlyCode(job.stage) && outcome.completed && waitingFor === null;
3033
3625
  const pushed = shouldPublish ? await publishWorkspace(workspace, job, await gitToken(tools.api, job), identity) : [];
3034
3626
  return {
3035
3627
  outcome: outcome.completed ? "succeeded" : "failed",
@@ -3050,7 +3642,7 @@ function clockOf(iso) {
3050
3642
  const time = new Date(iso);
3051
3643
  return `${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
3052
3644
  }
3053
- function resultLine(finish2) {
3645
+ function resultLine(finish2, folder = false) {
3054
3646
  if (finish2.pausedUntil) {
3055
3647
  return `Paused: ${USAGE_LIMIT_REPORT}, resumes at ${clockOf(finish2.pausedUntil)}`;
3056
3648
  }
@@ -3069,7 +3661,10 @@ function resultLine(finish2) {
3069
3661
  if (finish2.spec) {
3070
3662
  return "Done. The spec waits for the owner on TAMsense.";
3071
3663
  }
3072
- return finish2.outcome === "succeeded" ? `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.` : `Failed: ${finish2.report}`;
3664
+ if (finish2.outcome !== "succeeded") {
3665
+ return `Failed: ${finish2.report}`;
3666
+ }
3667
+ return folder ? "Done. The result is on the task on TAMsense." : `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.`;
3073
3668
  }
3074
3669
  async function sendRecordings(job, tools) {
3075
3670
  const folder = recordingsFolder(tools.home, job.runId);
@@ -3085,7 +3680,7 @@ async function sendRecordings(job, tools) {
3085
3680
  tools.print(`Could not attach a recording: ${line}`);
3086
3681
  }
3087
3682
  } finally {
3088
- await rm4(folder, { recursive: true, force: true });
3683
+ await rm6(folder, { recursive: true, force: true });
3089
3684
  }
3090
3685
  }
3091
3686
  function failureReport(error, signal) {
@@ -3113,6 +3708,7 @@ async function runJob(job, tools, stop) {
3113
3708
  } catch (error) {
3114
3709
  finish2 = { outcome: "failed", report: failureReport(error, run2.signal), pushed: [], model: null, usage: null, verdict: null, handoff: null, sessionId: null, pausedUntil: null, questions: null, spec: null, waitingFor: null };
3115
3710
  } finally {
3711
+ await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
3116
3712
  clearInterval(polling);
3117
3713
  clearInterval(feeding);
3118
3714
  stop.removeEventListener("abort", stopRun);
@@ -3124,14 +3720,14 @@ async function runJob(job, tools, stop) {
3124
3720
  }
3125
3721
  await feed.flush();
3126
3722
  if (run2.signal.reason === CLOSED_ON_SITE) {
3127
- await rm4(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
3723
+ await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
3128
3724
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
3129
3725
  return;
3130
3726
  }
3131
3727
  await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
3132
3728
  try {
3133
3729
  await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
3134
- tools.print(resultLine(finish2));
3730
+ tools.print(resultLine(finish2, isFolderJob(job)));
3135
3731
  } catch (error) {
3136
3732
  tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
3137
3733
  }
@@ -3180,9 +3776,9 @@ async function work(tools, stop, parallel = 1) {
3180
3776
  await Promise.race(running);
3181
3777
  continue;
3182
3778
  }
3183
- let reply2;
3779
+ let reply3;
3184
3780
  try {
3185
- reply2 = await tools.api.request("POST", NEXT_RUN_PATH);
3781
+ reply3 = await tools.api.request("POST", NEXT_RUN_PATH);
3186
3782
  } catch (error) {
3187
3783
  if (isFatal(error)) {
3188
3784
  halt.abort();
@@ -3192,8 +3788,8 @@ async function work(tools, stop, parallel = 1) {
3192
3788
  await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
3193
3789
  continue;
3194
3790
  }
3195
- if (reply2.job) {
3196
- start(reply2.job);
3791
+ if (reply3.job) {
3792
+ start(reply3.job);
3197
3793
  } else {
3198
3794
  await tools.sleep(POLL_INTERVAL_MS, stopAll);
3199
3795
  }
@@ -3248,7 +3844,7 @@ function stopOnInterrupt() {
3248
3844
  return stop.signal;
3249
3845
  }
3250
3846
  function installTools(home) {
3251
- return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists: existsSync4 };
3847
+ return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists: existsSync5 };
3252
3848
  }
3253
3849
  async function browserForStart(home, install) {
3254
3850
  const installed = await installedBrowser(home);