@tamsensedev/runner 0.6.0 → 0.10.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,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync4 } from "fs";
4
+ import { existsSync as existsSync7 } from "fs";
5
5
  import { homedir as homedir3 } from "os";
6
- import process5 from "process";
6
+ import process7 from "process";
7
7
 
8
8
  // src/api.ts
9
9
  var DEFAULT_SERVER = "https://my.tamsense.com";
@@ -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((resolve5, 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) => resolve5({ 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((resolve5) => {
318
+ release = resolve5;
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,37 @@ 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
+ var FOLDER_CHECKS_PATH = "/api/runner/folders/checks";
454
+ function folderCheckPath(folderId) {
455
+ return `/api/runner/folders/${folderId}`;
456
+ }
457
+ function runPath(runId, action) {
458
+ return `/api/runner/runs/${runId}/${action}`;
459
+ }
460
+ function resultFilePath(runId, resultId) {
461
+ return `/api/runner/runs/${runId}/results/${resultId}`;
462
+ }
463
+ function knowledgePath(runId, entryId) {
464
+ return `/api/runner/runs/${runId}/knowledge/${entryId}`;
465
+ }
466
+ function permissionPath(runId, permissionId) {
467
+ return `/api/runner/runs/${runId}/permissions/${permissionId}`;
468
+ }
469
+ function recordingUploadPath(runId, upload, part) {
470
+ return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
471
+ }
472
+
443
473
  // src/feed.ts
444
474
  var MAX_TEXT_LENGTH = 8e3;
445
475
  var MAX_RESULT_LENGTH = 2e3;
@@ -459,29 +489,29 @@ function textField(fields, name) {
459
489
  return typeof value === "string" && value.trim() ? value : null;
460
490
  }
461
491
  function toolDraft(block, parent) {
462
- const tool4 = (block.name ?? "tool").replace(MCP_PREFIX, "");
492
+ const tool5 = (block.name ?? "tool").replace(MCP_PREFIX, "");
463
493
  const fields = fieldsOf(block.input);
464
- const base = { tool: tool4, ref: block.id ?? null, parent, failed: false };
465
- if (tool4 === "Skill" && textField(fields, "skill")) {
494
+ const base = { tool: tool5, ref: block.id ?? null, parent, failed: false };
495
+ if (tool5 === "Skill" && textField(fields, "skill")) {
466
496
  return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
467
497
  }
468
- if (HELPER_TOOLS2.has(tool4)) {
498
+ if (HELPER_TOOLS2.has(tool5)) {
469
499
  const who = textField(fields, "subagent_type") ?? "general-purpose";
470
500
  const what = textField(fields, "description");
471
501
  return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
472
502
  }
473
- if (tool4 === "submit_verdict") {
503
+ if (tool5 === "submit_verdict") {
474
504
  const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
475
505
  return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
476
506
  }
477
- if (tool4 === "hand_off") {
507
+ if (tool5 === "hand_off") {
478
508
  return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
479
509
  }
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) };
510
+ if (tool5 in ATTACH_LABELS) {
511
+ return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool5]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
482
512
  }
483
513
  const command = textField(fields, "command");
484
- const summary = tool4 === "Bash" && command ? `$ ${command}` : activityLine(tool4, block.input);
514
+ const summary = tool5 === "Bash" && command ? `$ ${command}` : activityLine(tool5, block.input);
485
515
  return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
486
516
  }
487
517
  function resultText(content) {
@@ -493,6 +523,10 @@ function resultText(content) {
493
523
  }
494
524
  return content.map((part) => part.type === "text" ? part.text ?? "" : part.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
495
525
  }
526
+ function helperEndDraft(ref, parent, end, text2) {
527
+ const stopped = end === "stopped";
528
+ return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
529
+ }
496
530
  function feedOf(message) {
497
531
  if (message.type !== "assistant" && message.type !== "user") {
498
532
  return [];
@@ -561,6 +595,77 @@ function chooseHandoff(targets, request) {
561
595
  return { handoff: { toColumnId: edge.columnId, outcome: edge.outcome, note } };
562
596
  }
563
597
 
598
+ // src/helpers.ts
599
+ function helperTasks() {
600
+ const byRef = /* @__PURE__ */ new Map();
601
+ const refByTask = /* @__PURE__ */ new Map();
602
+ const parents = /* @__PURE__ */ new Map();
603
+ function observe(message) {
604
+ if (message.type !== "system") {
605
+ return [];
606
+ }
607
+ if (message.subtype === "task_started" && message.task_type === "local_agent" && message.tool_use_id) {
608
+ byRef.set(message.tool_use_id, { taskId: message.task_id, background: message.is_backgrounded === true, stopped: false, over: false });
609
+ refByTask.set(message.task_id, message.tool_use_id);
610
+ return [];
611
+ }
612
+ if (message.subtype === "task_updated" && message.patch.is_backgrounded !== void 0) {
613
+ const ref2 = refByTask.get(message.task_id);
614
+ const task2 = ref2 ? byRef.get(ref2) : void 0;
615
+ if (task2) {
616
+ task2.background = message.patch.is_backgrounded;
617
+ }
618
+ return [];
619
+ }
620
+ if (message.subtype !== "task_notification") {
621
+ return [];
622
+ }
623
+ const ref = message.tool_use_id ?? refByTask.get(message.task_id);
624
+ const task = ref ? byRef.get(ref) : void 0;
625
+ if (!ref || !task || task.over) {
626
+ return [];
627
+ }
628
+ task.stopped = message.status === "stopped";
629
+ if (!task.background) {
630
+ return [];
631
+ }
632
+ task.over = true;
633
+ return [helperEndDraft(ref, parents.get(ref) ?? null, message.status, message.summary)];
634
+ }
635
+ function adjust(draft) {
636
+ if (draft.kind === "agent" && draft.ref) {
637
+ parents.set(draft.ref, draft.parent);
638
+ }
639
+ const task = draft.kind === "result" && draft.ref ? byRef.get(draft.ref) : void 0;
640
+ if (!task || !draft.ref) {
641
+ return [draft];
642
+ }
643
+ if (task.background) {
644
+ return [];
645
+ }
646
+ task.over = true;
647
+ return task.stopped ? [helperEndDraft(draft.ref, draft.parent, "stopped", "")] : [draft];
648
+ }
649
+ async function stop(target, ref) {
650
+ const task = byRef.get(ref);
651
+ if (!task) {
652
+ return "unknown";
653
+ }
654
+ if (task.over || task.stopped) {
655
+ return "finished";
656
+ }
657
+ await target.stopTask(task.taskId);
658
+ return "stopped";
659
+ }
660
+ return { observe, adjust, stop };
661
+ }
662
+ function helperStoppedNote(stop) {
663
+ 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.`;
664
+ }
665
+ function helperStopRequest(stop) {
666
+ 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.`;
667
+ }
668
+
564
669
  // src/input.ts
565
670
  var DEFAULT_MODEL = "default";
566
671
  var QUIET_MS = 1e3;
@@ -571,8 +676,11 @@ function userMessage(text2) {
571
676
  function humanText(message) {
572
677
  return `Message from ${message.authorName}: ${message.text}`;
573
678
  }
679
+ function priorityOf(mode) {
680
+ return mode === "steer" ? "now" : "next";
681
+ }
574
682
  function humanMessage(message) {
575
- return { ...userMessage(humanText(message)), priority: message.mode === "steer" ? "now" : "next", client_composed: true };
683
+ return { ...userMessage(humanText(message)), priority: priorityOf(message.mode), client_composed: true };
576
684
  }
577
685
  function switchLine(change) {
578
686
  const model = change.model === DEFAULT_MODEL ? "the default model" : change.model;
@@ -601,6 +709,7 @@ function inputChannel(options = {}) {
601
709
  let wake = null;
602
710
  let timers = [];
603
711
  let applySwitch2 = null;
712
+ let applyStop = null;
604
713
  let waitingSwitch = null;
605
714
  function wakeUp() {
606
715
  wake?.();
@@ -653,8 +762,8 @@ function inputChannel(options = {}) {
653
762
  }
654
763
  const next = queue.shift();
655
764
  if (!next) {
656
- await new Promise((resolve2) => {
657
- wake = resolve2;
765
+ await new Promise((resolve5) => {
766
+ wake = resolve5;
658
767
  });
659
768
  continue;
660
769
  }
@@ -714,11 +823,14 @@ function inputChannel(options = {}) {
714
823
  onSwitch(apply) {
715
824
  applySwitch2 = apply;
716
825
  if (waitingSwitch) {
717
- const { change, resolve: resolve2 } = waitingSwitch;
826
+ const { change, resolve: resolve5 } = waitingSwitch;
718
827
  waitingSwitch = null;
719
- void apply(change).then(() => resolve2(true), () => resolve2(false));
828
+ void apply(change).then(() => resolve5(true), () => resolve5(false));
720
829
  }
721
830
  },
831
+ onStopHelper(stop) {
832
+ applyStop = stop;
833
+ },
722
834
  close,
723
835
  push(message) {
724
836
  if (closed || seen.has(message.id)) {
@@ -729,6 +841,17 @@ function inputChannel(options = {}) {
729
841
  wakeUp();
730
842
  return true;
731
843
  },
844
+ note(text2, mode) {
845
+ if (closed) {
846
+ return false;
847
+ }
848
+ queue.push({ ids: [], message: { ...userMessage(text2), priority: priorityOf(mode) } });
849
+ wakeUp();
850
+ return true;
851
+ },
852
+ async stopHelper(ref) {
853
+ return closed || !applyStop ? "unknown" : await applyStop(ref);
854
+ },
732
855
  async checking(work2) {
733
856
  const epoch = idle ? idleEpoch : null;
734
857
  inFlight += 1;
@@ -763,8 +886,8 @@ function inputChannel(options = {}) {
763
886
  const earlier = waitingSwitch;
764
887
  earlier?.resolve(false);
765
888
  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 };
889
+ return new Promise((resolve5) => {
890
+ waitingSwitch = { change: merged, resolve: resolve5 };
768
891
  });
769
892
  }
770
893
  };
@@ -927,8 +1050,8 @@ function messageOf2(error) {
927
1050
  async function decisionOf(tools, first, signal) {
928
1051
  const now = tools.now ?? Date.now;
929
1052
  const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
930
- let reply2 = first;
931
- while (reply2.decision === "wait") {
1053
+ let reply3 = first;
1054
+ while (reply3.decision === "wait") {
932
1055
  await tools.sleep(CHECK_INTERVAL_MS, signal);
933
1056
  if (signal.aborted) {
934
1057
  return STOPPED_TEXT2;
@@ -936,9 +1059,9 @@ async function decisionOf(tools, first, signal) {
936
1059
  if (now() > giveUpAt) {
937
1060
  return SITE_SILENT_TEXT;
938
1061
  }
939
- reply2 = await tools.check(reply2.id).catch(() => reply2);
1062
+ reply3 = await tools.check(reply3.id).catch(() => reply3);
940
1063
  }
941
- return reply2;
1064
+ return reply3;
942
1065
  }
943
1066
  function askPermission(tools, lastWords, folder) {
944
1067
  return async (toolName, input, options) => {
@@ -959,106 +1082,31 @@ function askPermission(tools, lastWords, folder) {
959
1082
  } catch (error) {
960
1083
  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
1084
  }
962
- const reply2 = await decisionOf(tools, first, options.signal);
963
- if (typeof reply2 === "string") {
964
- return deny(reply2);
1085
+ const reply3 = await decisionOf(tools, first, options.signal);
1086
+ if (typeof reply3 === "string") {
1087
+ return deny(reply3);
965
1088
  }
966
- return reply2.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply2.message);
1089
+ return reply3.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply3.message);
967
1090
  };
968
1091
  }
969
1092
  var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
970
1093
 
971
- // src/specText.ts
972
- function isRecord(value) {
973
- return typeof value === "object" && value !== null && !Array.isArray(value);
974
- }
975
- function text(value) {
976
- return typeof value === "string" ? value.trim() : "";
977
- }
978
- function texts(value) {
979
- return Array.isArray(value) ? value.map(text).filter(Boolean) : [];
980
- }
981
- function records(value) {
982
- return Array.isArray(value) ? value.filter(isRecord) : [];
983
- }
984
- function section(title, lines) {
985
- return lines.length > 0 ? `${title}
986
- ${lines.join("\n")}` : null;
987
- }
988
- function criterionLine(value) {
989
- if (typeof value === "string") {
990
- return value.trim() ? `- ${value.trim()}` : null;
991
- }
992
- if (!isRecord(value) || !text(value.text)) {
993
- return null;
994
- }
995
- const kind = ["main", "edge", "keep"].includes(value.kind) ? `(${value.kind})` : "";
996
- const head = [text(value.id), kind].filter(Boolean).join(" ");
997
- return `- ${head ? `${head}: ` : ""}${text(value.text)}`;
998
- }
999
- function assumptionLine(value) {
1000
- if (typeof value === "string") {
1001
- return value.trim() ? `- ${value.trim()}` : null;
1002
- }
1003
- if (!isRecord(value) || !text(value.text)) {
1004
- return null;
1005
- }
1006
- return `- ${text(value.text)}${text(value.reason) ? ` (${text(value.reason)})` : ""}`;
1007
- }
1008
- function changeLines(change, index) {
1009
- const action = ["create", "modify", "delete"].includes(change.action) ? ` (${change.action})` : "";
1010
- const [produces, consumes, acceptance] = [texts(change.produces), texts(change.consumes), texts(change.acceptance)];
1011
- return [
1012
- `${index + 1}. ${text(change.repo) ? `${text(change.repo)} ` : ""}${text(change.file)}${action}: ${text(change.description)}`,
1013
- ...produces.length > 0 ? [` Produces: ${produces.join("; ")}`] : [],
1014
- ...consumes.length > 0 ? [` Consumes: ${consumes.join("; ")}`] : [],
1015
- ...acceptance.length > 0 ? [` Makes true: ${acceptance.join(", ")}`] : []
1016
- ];
1017
- }
1018
- var CODE_USE_TEXT = { reuse: "reuse", change: "change", keep: "do not touch" };
1019
- var RISK_LEVELS = ["low", "medium", "high"];
1020
- function present(lines) {
1021
- return lines.filter((line) => line !== null);
1022
- }
1023
- function specPromptText(spec) {
1024
- if (!isRecord(spec)) {
1025
- return null;
1026
- }
1027
- const structured = Array.isArray(spec.outOfScope) || isRecord(spec.testPlan);
1028
- const plan = isRecord(spec.testPlan) ? spec.testPlan : {};
1029
- const risks = records(spec.risks).filter((item) => text(item.risk));
1030
- const riskLevel = RISK_LEVELS.includes(spec.riskLevel) ? spec.riskLevel : null;
1031
- return [
1032
- text(spec.goal) && `Goal: ${text(spec.goal)}`,
1033
- text(spec.context) && `${structured ? "Summary" : "Context"}: ${text(spec.context)}`,
1034
- section("In scope:", texts(spec.inScope).map((item) => `- ${item}`)),
1035
- 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"})`)),
1037
- 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
- section("Changes, in order:", records(spec.changes).filter((item) => text(item.file)).flatMap(changeLines)),
1039
- section("Acceptance criteria:", present(Array.isArray(spec.acceptance) ? spec.acceptance.map(criterionLine) : [])),
1040
- section("Test plan:", [
1041
- ...records(plan.automated).filter((item) => text(item.command)).map((item) => `- Run in ${text(item.repo) || "the repository"}: ${text(item.command)}${text(item.expect) ? `. Passing: ${text(item.expect)}` : ""}`),
1042
- ...records(plan.newTests).filter((item) => text(item.seam)).map((item) => `- New test at ${text(item.seam)}${texts(item.covers).length > 0 ? `, covers ${texts(item.covers).join(", ")}` : ""}`),
1043
- ...records(plan.manual).filter((item) => text(item.steps)).map((item) => `- By hand${text(item.page) ? ` on ${text(item.page)}` : ""}: ${text(item.steps)}`)
1044
- ]),
1045
- risks.length > 0 ? section(`Risks${riskLevel ? ` (risk level ${riskLevel})` : ""}:`, risks.map((item) => `- ${text(item.risk)}${text(item.mitigation) ? `. Mitigation: ${text(item.mitigation)}` : ""}`)) : riskLevel && `Risk level: ${riskLevel}`,
1046
- section("Assumptions:", present(Array.isArray(spec.assumptions) ? spec.assumptions.map(assumptionLine) : [])),
1047
- section("Open questions (not answered yet):", records(spec.openQuestions).filter((item) => text(item.question)).map((item) => `- ${text(item.question)}`))
1048
- ].filter(Boolean).join("\n\n");
1049
- }
1094
+ // src/localWorkspace.ts
1095
+ import { existsSync as existsSync4 } from "fs";
1096
+ import { join as join4, resolve as resolve3 } from "path";
1050
1097
 
1051
- // src/workspace.ts
1052
- import { existsSync as existsSync2 } from "fs";
1053
- import { rm as rm2 } from "fs/promises";
1054
- import { join as join2, resolve } from "path";
1098
+ // src/folders.ts
1099
+ import { existsSync as existsSync2, realpathSync, statSync } from "fs";
1100
+ import { basename, isAbsolute, join as join2, relative, resolve } from "path";
1101
+ import process3 from "process";
1055
1102
 
1056
1103
  // src/git.ts
1057
- import { Buffer } from "buffer";
1104
+ import { Buffer as Buffer2 } from "buffer";
1058
1105
  import { spawn as spawn2 } from "child_process";
1106
+ import process2 from "process";
1059
1107
  var GITHUB_ORIGIN = "https://github.com/";
1060
1108
  function githubAuthEnv(token) {
1061
- const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
1109
+ const credentials = Buffer2.from(`x-access-token:${token}`).toString("base64");
1062
1110
  return {
1063
1111
  GIT_CONFIG_COUNT: "1",
1064
1112
  GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
@@ -1066,10 +1114,13 @@ function githubAuthEnv(token) {
1066
1114
  GIT_TERMINAL_PROMPT: "0"
1067
1115
  };
1068
1116
  }
1117
+ function personalGitEnv(env = process2.env) {
1118
+ return { GIT_TERMINAL_PROMPT: "0", ...env.GIT_SSH_COMMAND ? {} : { GIT_SSH_COMMAND: "ssh -o BatchMode=yes" } };
1119
+ }
1069
1120
  function runGit(args, options) {
1070
- const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
1071
- return new Promise((resolve2, reject) => {
1072
- const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
1121
+ const env = { ...process2.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
1122
+ return new Promise((resolve5, reject) => {
1123
+ const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"], ...options.timeoutMs ? { timeout: options.timeoutMs } : {} });
1073
1124
  let stdout = "";
1074
1125
  let stderr = "";
1075
1126
  child.stdout.on("data", (chunk) => {
@@ -1079,7 +1130,7 @@ function runGit(args, options) {
1079
1130
  stderr += chunk.toString();
1080
1131
  });
1081
1132
  child.on("error", reject);
1082
- child.on("close", (code) => resolve2({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
1133
+ child.on("close", (code) => resolve5({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
1083
1134
  });
1084
1135
  }
1085
1136
  async function git(args, options) {
@@ -1090,6 +1141,127 @@ async function git(args, options) {
1090
1141
  return result.stdout;
1091
1142
  }
1092
1143
 
1144
+ // src/folders.ts
1145
+ var FIRST_REMOTE = "origin";
1146
+ var USUAL_BASES = ["main", "master"];
1147
+ var CHECKS_INTERVAL_MS = 5e3;
1148
+ var WEB_REMOTE = /^[a-z][\w+.-]*:\/\//i;
1149
+ var NETWORK_REMOTE = /^(?!file:)[a-z][\w+.-]*:\/\/|^[^/@]+@[^/:]+:/i;
1150
+ function problem(text2) {
1151
+ return { ok: false, problem: text2 };
1152
+ }
1153
+ async function gitText(args, cwd) {
1154
+ const result = await runGit(args, { cwd });
1155
+ return result.code === 0 && result.stdout ? result.stdout : null;
1156
+ }
1157
+ function expandHome(path, home) {
1158
+ const trimmed = path.trim();
1159
+ return trimmed === "~" || trimmed.startsWith("~/") || trimmed.startsWith("~\\") ? join2(home, trimmed.slice(1)) : trimmed;
1160
+ }
1161
+ function remoteWithoutSecrets(url) {
1162
+ if (!WEB_REMOTE.test(url)) {
1163
+ return url;
1164
+ }
1165
+ try {
1166
+ const parsed = new URL(url);
1167
+ if (parsed.username || parsed.password) {
1168
+ parsed.username = "";
1169
+ parsed.password = "";
1170
+ }
1171
+ return parsed.toString().replace(/\/$/, "");
1172
+ } catch {
1173
+ return url;
1174
+ }
1175
+ }
1176
+ function repositoryName(remoteUrl, folder) {
1177
+ const parts = remoteUrl && NETWORK_REMOTE.test(remoteUrl) ? remoteUrl.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).filter(Boolean) : [];
1178
+ return parts.length >= 3 ? `${parts.at(-2)}/${parts.at(-1)}` : basename(folder);
1179
+ }
1180
+ async function remoteOf(folder) {
1181
+ const remotes = (await gitText(["remote"], folder))?.split("\n").map((line) => line.trim()).filter(Boolean) ?? [];
1182
+ return remotes.includes(FIRST_REMOTE) ? FIRST_REMOTE : remotes[0] ?? null;
1183
+ }
1184
+ async function hasRef(folder, ref) {
1185
+ return (await runGit(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], { cwd: folder })).code === 0;
1186
+ }
1187
+ async function baseBranchOf(folder, remote, current) {
1188
+ const remoteHead = remote ? await gitText(["symbolic-ref", "--quiet", "--short", `refs/remotes/${remote}/HEAD`], folder) : null;
1189
+ if (remoteHead?.startsWith(`${remote}/`)) {
1190
+ return remoteHead.slice(remote.length + 1);
1191
+ }
1192
+ for (const name of USUAL_BASES) {
1193
+ if (await hasRef(folder, `refs/heads/${name}`) || remote && await hasRef(folder, `refs/remotes/${remote}/${name}`)) {
1194
+ return name;
1195
+ }
1196
+ }
1197
+ return current;
1198
+ }
1199
+ function samePath(first, second) {
1200
+ const [a, b] = [realpathSync.native(first), realpathSync.native(second)];
1201
+ return process3.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
1202
+ }
1203
+ function isInside(path, folder) {
1204
+ const between = relative(resolve(folder), resolve(path));
1205
+ return between === "" || !between.startsWith("..") && !isAbsolute(between);
1206
+ }
1207
+ async function inspectFolder(path, home) {
1208
+ const folder = expandHome(path, home);
1209
+ if (!isAbsolute(folder)) {
1210
+ return problem("Enter the full path of the folder, for example /Users/anna/code/shop or C:\\code\\shop.");
1211
+ }
1212
+ if (!existsSync2(folder) || !statSync(folder).isDirectory()) {
1213
+ return problem(`There is no folder ${folder} on this computer.`);
1214
+ }
1215
+ if (isInside(folder, join2(home, ".tamsense"))) {
1216
+ return problem("This is a TAMsense work folder. Choose your own copy of the repository.");
1217
+ }
1218
+ const top = await gitText(["rev-parse", "--show-toplevel"], folder);
1219
+ if (!top) {
1220
+ return problem(`${folder} is not a git repository. Choose the folder of a cloned repository, or run git init there.`);
1221
+ }
1222
+ if (!samePath(top, folder)) {
1223
+ return problem(`${folder} is inside the repository ${resolve(top)}. Add the top folder of the repository instead.`);
1224
+ }
1225
+ if (!await hasRef(folder, "HEAD")) {
1226
+ return problem("The repository has no commits yet. Make the first commit, then check the folder again.");
1227
+ }
1228
+ const remote = await remoteOf(folder);
1229
+ const remoteUrl = remote ? await gitText(["remote", "get-url", remote], folder) : null;
1230
+ const currentBranch = await gitText(["symbolic-ref", "--quiet", "--short", "HEAD"], folder);
1231
+ const defaultBranch = await baseBranchOf(folder, remote, currentBranch);
1232
+ if (!defaultBranch) {
1233
+ return problem("Could not tell the main branch of the repository. Check out the main branch in this folder, then check it again.");
1234
+ }
1235
+ const safeUrl = remoteUrl && remoteWithoutSecrets(remoteUrl);
1236
+ return { ok: true, repository: repositoryName(safeUrl, folder), currentBranch, defaultBranch, remoteUrl: safeUrl };
1237
+ }
1238
+ function reportLine(path, report) {
1239
+ return report.ok ? `Checked the folder ${path}: ${report.repository}, main branch ${report.defaultBranch}.` : `The folder ${path} cannot be used: ${report.problem}`;
1240
+ }
1241
+ async function checkFolders(tools, stop) {
1242
+ let complained = false;
1243
+ while (!stop.aborted) {
1244
+ try {
1245
+ const { checks } = await tools.api.request("POST", FOLDER_CHECKS_PATH);
1246
+ for (const check of checks) {
1247
+ const report = await tools.inspect(check.path).catch((error) => problem(error instanceof Error ? error.message : String(error)));
1248
+ await tools.api.request("POST", folderCheckPath(check.id), report);
1249
+ tools.print(reportLine(check.path, report));
1250
+ }
1251
+ complained = false;
1252
+ } catch (error) {
1253
+ if (error instanceof ApiError && error.status === 404) {
1254
+ return;
1255
+ }
1256
+ if (!complained && !stop.aborted) {
1257
+ tools.print(`Could not check the folders for TAMsense: ${error instanceof Error ? error.message : String(error)}`);
1258
+ complained = true;
1259
+ }
1260
+ }
1261
+ await tools.sleep(CHECKS_INTERVAL_MS, stop);
1262
+ }
1263
+ }
1264
+
1093
1265
  // src/lock.ts
1094
1266
  function keyedLock() {
1095
1267
  const tails = /* @__PURE__ */ new Map();
@@ -1098,8 +1270,8 @@ function keyedLock() {
1098
1270
  const previous = tails.get(key) ?? Promise.resolve();
1099
1271
  let release = () => {
1100
1272
  };
1101
- const done2 = new Promise((resolve2) => {
1102
- release = resolve2;
1273
+ const done2 = new Promise((resolve5) => {
1274
+ release = resolve5;
1103
1275
  });
1104
1276
  const tail = previous.then(() => done2);
1105
1277
  tails.set(key, tail);
@@ -1117,13 +1289,16 @@ function keyedLock() {
1117
1289
  }
1118
1290
 
1119
1291
  // src/workspace.ts
1292
+ import { existsSync as existsSync3 } from "fs";
1293
+ import { rm as rm2 } from "fs/promises";
1294
+ import { join as join3, resolve as resolve2 } from "path";
1120
1295
  var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
1121
1296
  var cacheLock = keyedLock();
1122
1297
  function githubRemoteUrl(fullName) {
1123
1298
  return `https://github.com/${fullName}.git`;
1124
1299
  }
1125
1300
  function tamsenseFolder(home, ...parts) {
1126
- return join2(home, ".tamsense", ...parts);
1301
+ return join3(home, ".tamsense", ...parts);
1127
1302
  }
1128
1303
  function folderNames(repositories) {
1129
1304
  const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
@@ -1134,7 +1309,7 @@ function folderNames(repositories) {
1134
1309
  return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
1135
1310
  }
1136
1311
  async function refreshCache(cache, repository, token, tools) {
1137
- if (existsSync2(join2(cache, ".git"))) {
1312
+ if (existsSync3(join3(cache, ".git"))) {
1138
1313
  await git(["fetch", "--prune", "origin"], { cwd: cache, token });
1139
1314
  return;
1140
1315
  }
@@ -1166,7 +1341,7 @@ function branchHolders(porcelain, branch) {
1166
1341
  }
1167
1342
  async function releaseBranch(cache, branch, keep) {
1168
1343
  const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
1169
- for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve(holder) !== resolve(keep))) {
1344
+ for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve2(holder) !== resolve2(keep))) {
1170
1345
  await removeWorktree(cache, path);
1171
1346
  }
1172
1347
  }
@@ -1175,7 +1350,7 @@ async function baseRef(cache, job, repository) {
1175
1350
  return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
1176
1351
  }
1177
1352
  async function keptBase(cache, path, job, repository) {
1178
- if (!job.keepWorkspace || !existsSync2(join2(path, ".git"))) {
1353
+ if (!job.keepWorkspace || !existsSync3(join3(path, ".git"))) {
1179
1354
  return null;
1180
1355
  }
1181
1356
  const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
@@ -1184,14 +1359,17 @@ async function keptBase(cache, path, job, repository) {
1184
1359
  }
1185
1360
  return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
1186
1361
  }
1362
+ function taskFolder(home, job) {
1363
+ return tamsenseFolder(home, "work", ...stepFolder(job));
1364
+ }
1187
1365
  async function prepareWorkspace(job, token, tools) {
1188
- const root = tamsenseFolder(tools.home, "work", ...stepFolder(job));
1366
+ const root = taskFolder(tools.home, job);
1189
1367
  const folders = folderNames(job.repositories);
1190
1368
  const repositories = [];
1191
1369
  for (const [index, repository] of job.repositories.entries()) {
1192
1370
  const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
1193
1371
  const folder = folders[index] ?? repository.fullName;
1194
- const path = join2(root, folder);
1372
+ const path = join3(root, folder);
1195
1373
  const baseSha = await cacheLock.run(repository.fullName, async () => {
1196
1374
  await refreshCache(cache, repository, token, tools);
1197
1375
  const kept = await keptBase(cache, path, job, repository);
@@ -1212,40 +1390,500 @@ async function prepareWorkspace(job, token, tools) {
1212
1390
  }
1213
1391
  return { root, repositories };
1214
1392
  }
1215
- async function commitIdentityEnv(home) {
1216
- const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
1217
- if (configured.code === 0 && configured.stdout) {
1218
- return {};
1393
+ async function commitIdentityEnv(home) {
1394
+ const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
1395
+ if (configured.code === 0 && configured.stdout) {
1396
+ return {};
1397
+ }
1398
+ return {
1399
+ GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
1400
+ GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
1401
+ GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
1402
+ GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
1403
+ };
1404
+ }
1405
+ async function publishWorkspace(workspace, job, token, identity) {
1406
+ const pushed = [];
1407
+ for (const repository of workspace.repositories) {
1408
+ const options = { cwd: repository.path, env: identity };
1409
+ const sha = await cacheLock.run(repository.fullName, async () => {
1410
+ await git(["add", "--all"], options);
1411
+ const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
1412
+ if (hasStaged) {
1413
+ await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
1414
+ }
1415
+ const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
1416
+ if (newCommits === 0) {
1417
+ return null;
1418
+ }
1419
+ await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
1420
+ return git(["rev-parse", "HEAD"], options);
1421
+ });
1422
+ if (sha) {
1423
+ pushed.push({ repository: repository.fullName, branch: job.branch, sha });
1424
+ }
1425
+ }
1426
+ return pushed;
1427
+ }
1428
+
1429
+ // src/localWorkspace.ts
1430
+ var NETWORK_TIMEOUT_MS = 12e4;
1431
+ var sourceLock = keyedLock();
1432
+ function isLocalJob(job) {
1433
+ return job.workspace === "local";
1434
+ }
1435
+ function localBranchLine(branch) {
1436
+ const local = branch.local;
1437
+ if (!local) {
1438
+ return `Pushed ${branch.branch} in ${branch.repository}.`;
1439
+ }
1440
+ const where = `Branch ${branch.branch} has ${local.commits} commit(s) in ${local.folder}`;
1441
+ if (local.pushProblem) {
1442
+ return `${where}. It was not pushed: ${local.pushProblem}`;
1443
+ }
1444
+ return local.remoteUrl ? `${where}, pushed to ${local.remoteUrl}.` : `${where}.`;
1445
+ }
1446
+ function readsOnly(job) {
1447
+ return isReadOnlyCheck(job) || job.stage.kind === "interview" || job.stage.kind === "spec";
1448
+ }
1449
+ async function isAncestor(folder, older, newer) {
1450
+ return (await runGit(["merge-base", "--is-ancestor", older, newer], { cwd: folder })).code === 0;
1451
+ }
1452
+ async function fetchRemote(source, remote, print2) {
1453
+ const fetched = await runGit(["fetch", "--prune", remote], { cwd: source, env: personalGitEnv(), timeoutMs: NETWORK_TIMEOUT_MS });
1454
+ if (fetched.code !== 0) {
1455
+ print2(`Could not fetch from ${remote} in ${source}, going on with what the folder has: ${fetched.stderr || `exit code ${fetched.code}`}`);
1456
+ }
1457
+ }
1458
+ async function baseBranchRef(source, branch, remote) {
1459
+ const own = await hasRef(source, `refs/heads/${branch}`) ? branch : null;
1460
+ const fetched = remote && await hasRef(source, `refs/remotes/${remote}/${branch}`) ? `${remote}/${branch}` : null;
1461
+ if (own && fetched) {
1462
+ return await isAncestor(source, own, fetched) ? fetched : own;
1463
+ }
1464
+ const found = own ?? fetched;
1465
+ if (!found) {
1466
+ throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in the board settings, on the Code tab.`);
1467
+ }
1468
+ return found;
1469
+ }
1470
+ async function taskBranchRef(source, branch, remote, base) {
1471
+ if (await hasRef(source, `refs/heads/${branch}`) && Number(await git(["rev-list", "--count", `${base}..${branch}`], { cwd: source })) > 0) {
1472
+ return branch;
1473
+ }
1474
+ return remote && await hasRef(source, `refs/remotes/${remote}/${branch}`) ? `${remote}/${branch}` : null;
1475
+ }
1476
+ async function startRef(source, job, local) {
1477
+ const base = await baseBranchRef(source, local.baseBranch, local.remote);
1478
+ const task = await taskBranchRef(source, job.branch, local.remote, base);
1479
+ if (task || !isReadOnlyCheck(job)) {
1480
+ return task ?? base;
1481
+ }
1482
+ const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
1483
+ throw new Error(`Branch ${job.branch} is not in ${where}. It was made on another computer: push it to a remote this folder can fetch, or run this check on that computer.`);
1484
+ }
1485
+ async function releaseBranch2(source, branch, keep, home) {
1486
+ const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: source });
1487
+ const work2 = tamsenseFolder(home, "work");
1488
+ for (const holder of branchHolders(listed.stdout, branch).filter((path) => resolve3(path) !== resolve3(keep))) {
1489
+ if (!isInside(holder, work2)) {
1490
+ throw new Error(`Branch ${branch} is checked out in ${resolve3(holder)}. Switch that folder to another branch, then start the step again.`);
1491
+ }
1492
+ await removeWorktree(source, holder);
1493
+ }
1494
+ }
1495
+ async function keptBase2(path, job) {
1496
+ if (!job.keepWorkspace || !existsSync4(join4(path, ".git"))) {
1497
+ return null;
1498
+ }
1499
+ const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
1500
+ if (branch.code !== 0 || branch.stdout !== (readsOnly(job) ? "HEAD" : job.branch)) {
1501
+ return null;
1502
+ }
1503
+ return git(["rev-parse", "HEAD"], { cwd: path });
1504
+ }
1505
+ function sourceOf(repository) {
1506
+ if (!repository.folder) {
1507
+ throw new Error(`TAMsense did not say which folder holds ${repository.fullName} on this computer.`);
1508
+ }
1509
+ return repository.folder;
1510
+ }
1511
+ async function prepareRepository(job, repository, path, tools) {
1512
+ const source = sourceOf(repository);
1513
+ if ((await runGit(["rev-parse", "--show-toplevel"], { cwd: source }).catch(() => null))?.code !== 0) {
1514
+ throw new Error(`${source} is no longer a git repository on this computer. Check the folder again in the board settings, on the Code tab.`);
1515
+ }
1516
+ const local = { source, remote: await remoteOf(source), baseBranch: repository.defaultBranch };
1517
+ if (local.remote) {
1518
+ await fetchRemote(source, local.remote, tools.print);
1519
+ }
1520
+ const kept = await keptBase2(path, job);
1521
+ if (kept) {
1522
+ return { baseSha: kept, local };
1523
+ }
1524
+ await runGit(["worktree", "prune"], { cwd: source });
1525
+ if (existsSync4(path)) {
1526
+ await removeWorktree(source, path);
1527
+ }
1528
+ const start = await startRef(source, job, local);
1529
+ if (readsOnly(job)) {
1530
+ await git(["worktree", "add", "--detach", path, start], { cwd: source });
1531
+ } else {
1532
+ await releaseBranch2(source, job.branch, path, tools.home);
1533
+ await git(["worktree", "add", "-B", job.branch, path, start], { cwd: source });
1534
+ }
1535
+ return { baseSha: await git(["rev-parse", "HEAD"], { cwd: path }), local };
1536
+ }
1537
+ async function prepareLocalWorkspace(job, tools) {
1538
+ const root = taskFolder(tools.home, job);
1539
+ const folders = folderNames(job.repositories);
1540
+ const repositories = [];
1541
+ for (const [index, repository] of job.repositories.entries()) {
1542
+ const folder = folders[index] ?? repository.fullName;
1543
+ const path = join4(root, folder);
1544
+ const prepared = await sourceLock.run(resolve3(sourceOf(repository)), () => prepareRepository(job, repository, path, tools));
1545
+ repositories.push({ fullName: repository.fullName, folder, path, baseSha: prepared.baseSha, local: prepared.local });
1546
+ }
1547
+ return { root, repositories };
1548
+ }
1549
+ async function pushBranch(path, local, branch) {
1550
+ if (!local.remote) {
1551
+ return { remoteUrl: null, pushProblem: null };
1552
+ }
1553
+ if (branch === local.baseBranch) {
1554
+ return { remoteUrl: null, pushProblem: `TAMsense never pushes the main branch ${branch}.` };
1555
+ }
1556
+ const pushed = await runGit(["push", local.remote, `HEAD:refs/heads/${branch}`], { cwd: path, env: personalGitEnv(), timeoutMs: NETWORK_TIMEOUT_MS });
1557
+ if (pushed.code !== 0) {
1558
+ return { remoteUrl: null, pushProblem: pushed.stderr || `exit code ${pushed.code}` };
1559
+ }
1560
+ return { remoteUrl: await git(["remote", "get-url", local.remote], { cwd: path }), pushProblem: null };
1561
+ }
1562
+ async function commitAndPush(repository, local, job, identity) {
1563
+ const options = { cwd: repository.path, env: identity };
1564
+ await git(["add", "--all"], options);
1565
+ if ((await runGit(["diff", "--cached", "--quiet"], options)).code === 1) {
1566
+ await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
1567
+ }
1568
+ if (Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options)) === 0) {
1569
+ return null;
1570
+ }
1571
+ const base = await baseBranchRef(repository.path, local.baseBranch, local.remote);
1572
+ const commits = Number(await git(["rev-list", "--count", `${base}..HEAD`], options));
1573
+ const sent2 = await pushBranch(repository.path, local, job.branch);
1574
+ return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent2 };
1575
+ }
1576
+ async function publishLocalWorkspace(workspace, job, identity) {
1577
+ const pushed = [];
1578
+ for (const repository of workspace.repositories) {
1579
+ const local = repository.local;
1580
+ if (!local) {
1581
+ continue;
1582
+ }
1583
+ const branch = await sourceLock.run(resolve3(local.source), async () => {
1584
+ const published = await commitAndPush(repository, local, job, identity);
1585
+ await runGit(["checkout", "--quiet", "--detach"], { cwd: repository.path });
1586
+ return published;
1587
+ });
1588
+ if (branch) {
1589
+ pushed.push({
1590
+ repository: repository.fullName,
1591
+ branch: job.branch,
1592
+ sha: branch.sha,
1593
+ local: { folder: local.source, baseBranch: local.baseBranch, commits: branch.commits, remoteUrl: branch.remoteUrl && remoteWithoutSecrets(branch.remoteUrl), pushProblem: branch.pushProblem }
1594
+ });
1595
+ }
1596
+ }
1597
+ return pushed;
1598
+ }
1599
+
1600
+ // src/results.ts
1601
+ import { mkdir as mkdir2, readFile as readFile2, rm as rm3, stat, writeFile as writeFile2 } from "fs/promises";
1602
+ import { basename as basename2, extname, isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "path";
1603
+ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1604
+ import { z as z2 } from "zod";
1605
+ var SAVE_DOCUMENT_TOOL_NAME = "save_result_document";
1606
+ var SAVE_TABLE_TOOL_NAME = "save_result_table";
1607
+ var ATTACH_FILE_TOOL_NAME = "attach_result_file";
1608
+ var ADD_LINK_TOOL_NAME = "add_result_link";
1609
+ var HAND_OVER_TOOL_NAME = "hand_over";
1610
+ var RESULTS_FOLDER = "results";
1611
+ var MAX_NAME_LENGTH = 60;
1612
+ var TYPES_BY_EXTENSION = {
1613
+ ".pdf": "application/pdf",
1614
+ ".png": "image/png",
1615
+ ".jpg": "image/jpeg",
1616
+ ".jpeg": "image/jpeg",
1617
+ ".webp": "image/webp",
1618
+ ".gif": "image/gif",
1619
+ ".csv": "text/csv",
1620
+ ".txt": "text/plain",
1621
+ ".md": "text/markdown",
1622
+ ".json": "application/json",
1623
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1624
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1625
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1626
+ ".zip": "application/zip"
1627
+ };
1628
+ var EXTENSIONS = { document: ".md", table: ".csv", file: "", link: "" };
1629
+ function isFolderJob(job) {
1630
+ return job.workspace === "folder";
1631
+ }
1632
+ function resultTools(job, api, root) {
1633
+ if (!isFolderJob(job) || job.stage.kind !== "code") {
1634
+ return null;
1635
+ }
1636
+ return {
1637
+ root,
1638
+ save: (body) => api.request("POST", runPath(job.runId, "results"), body),
1639
+ attach: (body) => api.request("POST", runPath(job.runId, "result-files"), body)
1640
+ };
1641
+ }
1642
+ function handOverTools(job, api) {
1643
+ const boards = job.handOverBoards ?? [];
1644
+ if (boards.length === 0 || job.stage.kind !== "code") {
1645
+ return null;
1646
+ }
1647
+ return { boards, send: (body) => api.request("POST", runPath(job.runId, "hand-over"), body) };
1648
+ }
1649
+ function resultFileName(result, taken) {
1650
+ 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";
1651
+ const extension = result.kind === "file" ? extname(result.fileName ?? "") : EXTENSIONS[result.kind];
1652
+ let name = `${stem}${extension}`;
1653
+ for (let index = 2; taken.has(name.toLowerCase()); index += 1) {
1654
+ name = `${stem}-${index}${extension}`;
1655
+ }
1656
+ taken.add(name.toLowerCase());
1657
+ return name;
1658
+ }
1659
+ function linksText(links) {
1660
+ return links.map((link) => `- ${link.title}: ${link.url ?? ""}`).join("\n");
1661
+ }
1662
+ async function writeResults(folder, job, api) {
1663
+ const results = job.results ?? [];
1664
+ await rm3(folder, { recursive: true, force: true });
1665
+ if (results.length === 0) {
1666
+ return [];
1667
+ }
1668
+ await mkdir2(folder, { recursive: true });
1669
+ const taken = /* @__PURE__ */ new Set();
1670
+ const lines = [];
1671
+ const missing = [];
1672
+ for (const result of results.filter((item) => item.kind !== "link")) {
1673
+ const name = resultFileName(result, taken);
1674
+ if (result.kind === "file") {
1675
+ try {
1676
+ const file = await api.request("GET", resultFilePath(job.runId, result.id));
1677
+ await writeFile2(join5(folder, name), Buffer.from(file.data, "base64"));
1678
+ } catch (error) {
1679
+ missing.push(`${result.title}: ${error instanceof Error ? error.message : String(error)}`);
1680
+ continue;
1681
+ }
1682
+ } else {
1683
+ await writeFile2(join5(folder, name), result.content ?? "");
1684
+ }
1685
+ lines.push(`${RESULTS_FOLDER}/${name}: ${result.title}${result.version ? `, version ${result.version}` : ""}`);
1686
+ }
1687
+ const links = results.filter((item) => item.kind === "link");
1688
+ if (links.length > 0) {
1689
+ await writeFile2(join5(folder, "links.md"), `${linksText(links)}
1690
+ `);
1691
+ lines.push(`${RESULTS_FOLDER}/links.md: ${links.length} link(s)`);
1692
+ }
1693
+ if (missing.length > 0) {
1694
+ await writeFile2(join5(folder, "notes.md"), `These files could not be downloaded:
1695
+ ${missing.map((line) => `- ${line}`).join("\n")}
1696
+ `);
1697
+ }
1698
+ return lines;
1699
+ }
1700
+ async function prepareFolder(job, root, api) {
1701
+ if (!job.keepWorkspace) {
1702
+ await rm3(root, { recursive: true, force: true });
1703
+ }
1704
+ await mkdir2(root, { recursive: true });
1705
+ return { root, repositories: [], results: await writeResults(join5(root, RESULTS_FOLDER), job, api) };
1706
+ }
1707
+ async function readResultFile(root, path, title) {
1708
+ const full = isAbsolute2(path) ? path : resolve4(root, path);
1709
+ const info = await stat(full).catch(() => null);
1710
+ if (!info?.isFile()) {
1711
+ return `There is no file at ${full}.`;
1712
+ }
1713
+ if (info.size > MAX_RESULT_FILE_BYTES) {
1714
+ return `The file is larger than ${MAX_RESULT_FILE_BYTES / 1024 / 1024} MB. Attach a smaller one or split it.`;
1715
+ }
1716
+ const name = basename2(full);
1717
+ return { name, title: title.trim() || name, contentType: TYPES_BY_EXTENSION[extname(full).toLowerCase()] ?? "application/octet-stream", data: (await readFile2(full)).toString("base64") };
1718
+ }
1719
+ function reply(text2, isError = false) {
1720
+ return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
1721
+ }
1722
+ async function sent(send) {
1723
+ try {
1724
+ return reply((await send()).text);
1725
+ } catch (error) {
1726
+ 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);
1727
+ }
1728
+ }
1729
+ function resultToolset(tools) {
1730
+ return [
1731
+ tool2(
1732
+ SAVE_DOCUMENT_TOOL_NAME,
1733
+ "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.",
1734
+ {
1735
+ title: z2.string().describe("short name of the document, the same for every version of it"),
1736
+ content: z2.string().describe("the full text in Markdown, never a diff or a part"),
1737
+ note: z2.string().describe('one line on what this version is or what changed: "First draft", "Fixed the prices after the check"')
1738
+ },
1739
+ async (args) => await sent(() => tools.save({ kind: "document", ...args }))
1740
+ ),
1741
+ tool2(
1742
+ SAVE_TABLE_TOOL_NAME,
1743
+ "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.",
1744
+ {
1745
+ title: z2.string().describe("short name of the table, the same for every version of it"),
1746
+ csv: z2.string().describe("the full table as CSV, the first row holds the column names"),
1747
+ note: z2.string().describe("one line on what this version is or what changed")
1748
+ },
1749
+ async (args) => await sent(() => tools.save({ kind: "table", title: args.title, content: args.csv, note: args.note }))
1750
+ ),
1751
+ tool2(
1752
+ ATTACH_FILE_TOOL_NAME,
1753
+ `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.`,
1754
+ {
1755
+ path: z2.string().describe("path of the file, from the task folder or absolute"),
1756
+ title: z2.string().describe("what the file is, in a few words")
1757
+ },
1758
+ async (args) => {
1759
+ const body = await readResultFile(tools.root, args.path, args.title);
1760
+ return typeof body === "string" ? reply(body, true) : await sent(() => tools.attach(body));
1761
+ }
1762
+ ),
1763
+ tool2(
1764
+ ADD_LINK_TOOL_NAME,
1765
+ "Attach a link to the TAMsense task as part of the result: a page or a file the result relies on.",
1766
+ {
1767
+ title: z2.string().describe("what the link is, in a few words"),
1768
+ url: z2.string().describe("the full address starting with https://")
1769
+ },
1770
+ async (args) => await sent(() => tools.save({ kind: "link", ...args }))
1771
+ )
1772
+ ];
1773
+ }
1774
+ function handOverTool(tools) {
1775
+ return tool2(
1776
+ HAND_OVER_TOOL_NAME,
1777
+ `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.`,
1778
+ {
1779
+ board: z2.string().describe("name of the board"),
1780
+ title: z2.string().describe("title of the new task on that board, one plain sentence"),
1781
+ note: z2.string().describe("what you found and what that board should do; never names, emails or other personal data of clients"),
1782
+ wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
1783
+ },
1784
+ async (args) => await sent(() => tools.send(args))
1785
+ );
1786
+ }
1787
+
1788
+ // src/specText.ts
1789
+ function isRecord(value) {
1790
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1791
+ }
1792
+ function text(value) {
1793
+ return typeof value === "string" ? value.trim() : "";
1794
+ }
1795
+ function texts(value) {
1796
+ return Array.isArray(value) ? value.map(text).filter(Boolean) : [];
1797
+ }
1798
+ function records(value) {
1799
+ return Array.isArray(value) ? value.filter(isRecord) : [];
1800
+ }
1801
+ function section(title, lines) {
1802
+ return lines.length > 0 ? `${title}
1803
+ ${lines.join("\n")}` : null;
1804
+ }
1805
+ function criterionLine(value) {
1806
+ if (typeof value === "string") {
1807
+ return value.trim() ? `- ${value.trim()}` : null;
1808
+ }
1809
+ if (!isRecord(value) || !text(value.text)) {
1810
+ return null;
1811
+ }
1812
+ const kind = ["main", "edge", "keep"].includes(value.kind) ? `(${value.kind})` : "";
1813
+ const head = [text(value.id), kind].filter(Boolean).join(" ");
1814
+ return `- ${head ? `${head}: ` : ""}${text(value.text)}`;
1815
+ }
1816
+ function assumptionLine(value) {
1817
+ if (typeof value === "string") {
1818
+ return value.trim() ? `- ${value.trim()}` : null;
1219
1819
  }
1220
- return {
1221
- GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
1222
- GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
1223
- GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
1224
- GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
1225
- };
1820
+ if (!isRecord(value) || !text(value.text)) {
1821
+ return null;
1822
+ }
1823
+ return `- ${text(value.text)}${text(value.reason) ? ` (${text(value.reason)})` : ""}`;
1226
1824
  }
1227
- async function publishWorkspace(workspace, job, token, identity) {
1228
- const pushed = [];
1229
- for (const repository of workspace.repositories) {
1230
- const options = { cwd: repository.path, env: identity };
1231
- const sha = await cacheLock.run(repository.fullName, async () => {
1232
- await git(["add", "--all"], options);
1233
- const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
1234
- if (hasStaged) {
1235
- await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
1236
- }
1237
- const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
1238
- if (newCommits === 0) {
1239
- return null;
1240
- }
1241
- await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
1242
- return git(["rev-parse", "HEAD"], options);
1243
- });
1244
- if (sha) {
1245
- pushed.push({ repository: repository.fullName, branch: job.branch, sha });
1246
- }
1825
+ function changeLines(change, index) {
1826
+ const action = ["create", "modify", "delete"].includes(change.action) ? ` (${change.action})` : "";
1827
+ const [produces, consumes, acceptance] = [texts(change.produces), texts(change.consumes), texts(change.acceptance)];
1828
+ return [
1829
+ `${index + 1}. ${text(change.repo) ? `${text(change.repo)} ` : ""}${text(change.file)}${action}: ${text(change.description)}`,
1830
+ ...produces.length > 0 ? [` Produces: ${produces.join("; ")}`] : [],
1831
+ ...consumes.length > 0 ? [` Consumes: ${consumes.join("; ")}`] : [],
1832
+ ...acceptance.length > 0 ? [` Makes true: ${acceptance.join(", ")}`] : []
1833
+ ];
1834
+ }
1835
+ var CODE_USE_TEXT = { reuse: "reuse", change: "change", keep: "do not touch" };
1836
+ var RISK_LEVELS = ["low", "medium", "high"];
1837
+ function present(lines) {
1838
+ return lines.filter((line) => line !== null);
1839
+ }
1840
+ function decisionLines(spec) {
1841
+ 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"})`);
1842
+ }
1843
+ function briefPromptText(brief) {
1844
+ return [
1845
+ text(brief.goal) && `Goal: ${text(brief.goal)}`,
1846
+ text(brief.context) && `Summary: ${text(brief.context)}`,
1847
+ section("The result must contain:", texts(brief.contains).map((item) => `- ${item}`)),
1848
+ section("Acceptance criteria:", present(Array.isArray(brief.acceptance) ? brief.acceptance.map(criterionLine) : [])),
1849
+ section("Constraints:", texts(brief.constraints).map((item) => `- ${item}`)),
1850
+ section("Out of scope (do not make):", texts(brief.outOfScope).map((item) => `- ${item}`)),
1851
+ section("Sources to use:", records(brief.sources).filter((item) => text(item.source)).map((item) => `- ${text(item.source)}${text(item.use) ? `: ${text(item.use)}` : ""}`)),
1852
+ text(brief.approver) && `Approved by: ${text(brief.approver)}`,
1853
+ section("Decisions:", decisionLines(brief)),
1854
+ section("Assumptions:", present(Array.isArray(brief.assumptions) ? brief.assumptions.map(assumptionLine) : [])),
1855
+ section("Open questions (not answered yet):", records(brief.openQuestions).filter((item) => text(item.question)).map((item) => `- ${text(item.question)}`))
1856
+ ].filter(Boolean).join("\n\n");
1857
+ }
1858
+ function specPromptText(spec) {
1859
+ if (!isRecord(spec)) {
1860
+ return null;
1247
1861
  }
1248
- return pushed;
1862
+ if (spec.format === "brief") {
1863
+ return briefPromptText(spec);
1864
+ }
1865
+ const structured = Array.isArray(spec.outOfScope) || isRecord(spec.testPlan);
1866
+ const plan = isRecord(spec.testPlan) ? spec.testPlan : {};
1867
+ const risks = records(spec.risks).filter((item) => text(item.risk));
1868
+ const riskLevel = RISK_LEVELS.includes(spec.riskLevel) ? spec.riskLevel : null;
1869
+ return [
1870
+ text(spec.goal) && `Goal: ${text(spec.goal)}`,
1871
+ text(spec.context) && `${structured ? "Summary" : "Context"}: ${text(spec.context)}`,
1872
+ section("In scope:", texts(spec.inScope).map((item) => `- ${item}`)),
1873
+ section("Out of scope (do not build):", texts(spec.outOfScope).map((item) => `- ${item}`)),
1874
+ section("Decisions:", decisionLines(spec)),
1875
+ 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]}]` : ""}`)),
1876
+ section("Changes, in order:", records(spec.changes).filter((item) => text(item.file)).flatMap(changeLines)),
1877
+ section("Acceptance criteria:", present(Array.isArray(spec.acceptance) ? spec.acceptance.map(criterionLine) : [])),
1878
+ section("Test plan:", [
1879
+ ...records(plan.automated).filter((item) => text(item.command)).map((item) => `- Run in ${text(item.repo) || "the repository"}: ${text(item.command)}${text(item.expect) ? `. Passing: ${text(item.expect)}` : ""}`),
1880
+ ...records(plan.newTests).filter((item) => text(item.seam)).map((item) => `- New test at ${text(item.seam)}${texts(item.covers).length > 0 ? `, covers ${texts(item.covers).join(", ")}` : ""}`),
1881
+ ...records(plan.manual).filter((item) => text(item.steps)).map((item) => `- By hand${text(item.page) ? ` on ${text(item.page)}` : ""}: ${text(item.steps)}`)
1882
+ ]),
1883
+ risks.length > 0 ? section(`Risks${riskLevel ? ` (risk level ${riskLevel})` : ""}:`, risks.map((item) => `- ${text(item.risk)}${text(item.mitigation) ? `. Mitigation: ${text(item.mitigation)}` : ""}`)) : riskLevel && `Risk level: ${riskLevel}`,
1884
+ section("Assumptions:", present(Array.isArray(spec.assumptions) ? spec.assumptions.map(assumptionLine) : [])),
1885
+ section("Open questions (not answered yet):", records(spec.openQuestions).filter((item) => text(item.question)).map((item) => `- ${text(item.question)}`))
1886
+ ].filter(Boolean).join("\n\n");
1249
1887
  }
1250
1888
 
1251
1889
  // src/prompt.ts
@@ -1268,8 +1906,15 @@ var CODE_RULES = [
1268
1906
  "Check your work with the project's own tools: run the tests, type checks and linters that exist, and fix what you broke.",
1269
1907
  "Commit your work in every repository you changed, on the current branch. Do not push, do not switch or create branches, and do not rewrite history: TAMsense pushes the branch and opens the pull request."
1270
1908
  ];
1909
+ var LOCAL_COMMIT_RULE = "Commit your work in every repository you changed, on the current branch. Do not push, do not switch or create branches, and do not rewrite history: TAMsense pushes the branch when the folder has a remote, and the person merges it.";
1910
+ var LOCAL_FOLDERS_RULE = "Each repository here is a git worktree of the person's own folder on this computer. Work only here: never open, change or check out branches in the person's original folder.";
1271
1911
  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
1912
  var CODE_REPORT_RULE = "End with a short report for the reviewer: what you changed, how you checked it, and anything left open.";
1913
+ var READ_ONLY_CODE_RULES = [
1914
+ "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.",
1915
+ "Check what you report with the project's own tools where they exist, and say how you checked it."
1916
+ ];
1917
+ var READ_ONLY_REPORT_RULE = "End with a short report: what you found, how you checked it, and what is left open.";
1273
1918
  var CHECK_RULES = [
1274
1919
  "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
1920
  "Do not change, commit or push product code. You may run anything and create throwaway files outside the repositories.",
@@ -1280,6 +1925,21 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
1280
1925
  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
1926
  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
1927
  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.";
1928
+ 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.";
1929
+ 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.";
1930
+ var FOLDER_DO_RULES = [
1931
+ `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.`,
1932
+ "Use only facts from the task, the notes, the earlier results and what you read yourself; never invent numbers, names or dates.",
1933
+ RESULT_HOME_RULE
1934
+ ];
1935
+ 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.";
1936
+ 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.";
1937
+ var FOLDER_CHECK_RULES = [
1938
+ `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.`,
1939
+ `Do not change the result. You may create throwaway files outside the ${RESULTS_FOLDER} folder.`,
1940
+ "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.",
1941
+ `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.`
1942
+ ];
1283
1943
  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
1944
  function composeProjectName(job) {
1285
1945
  return ["tamsense", ...stepFolder(job)].join("-");
@@ -1302,6 +1962,10 @@ var STAGE_GOALS = {
1302
1962
  review: "Review the code for",
1303
1963
  test: "Test"
1304
1964
  };
1965
+ var READ_ONLY_GOAL = "Work on";
1966
+ function isReadOnlyCode(stage) {
1967
+ return stage.kind === "code" && stage.role?.access === "read";
1968
+ }
1305
1969
  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
1970
  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
1971
  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 +1983,29 @@ var SPEC_RULES = [
1319
1983
  "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
1984
  ];
1321
1985
  var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
1986
+ 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.`;
1987
+ 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.";
1988
+ 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).";
1989
+ var RESULT_BRIEF_RULES = [
1990
+ '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.',
1991
+ '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".',
1992
+ "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.",
1993
+ `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.`,
1994
+ "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.",
1995
+ "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."
1996
+ ];
1997
+ 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
1998
  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
1999
  function specText(spec) {
1324
2000
  const text2 = specPromptText(spec);
1325
2001
  return text2 ? `Approved spec:
1326
2002
  ${text2}` : null;
1327
2003
  }
2004
+ function briefText(spec) {
2005
+ const text2 = specPromptText(spec);
2006
+ return text2 ? `Approved brief:
2007
+ ${text2}` : null;
2008
+ }
1328
2009
  function contextText(context) {
1329
2010
  return context.trim() ? `Project context from the team (follow it like the project's own rules):
1330
2011
  ${context.trim()}` : null;
@@ -1336,13 +2017,17 @@ function bulletList(lines) {
1336
2017
  return lines.map((line) => `- ${line}`).join("\n");
1337
2018
  }
1338
2019
  function repositoryLines(job, workspace) {
1339
- const sourceOf = new Map(job.repositories.map((repository) => [repository.fullName, repository]));
2020
+ const sourceOf2 = new Map(job.repositories.map((repository) => [repository.fullName, repository]));
1340
2021
  return workspace.repositories.map((repository) => {
1341
- const source = sourceOf.get(repository.fullName);
2022
+ const source = sourceOf2.get(repository.fullName);
1342
2023
  const pull = source?.pullRequestUrl ? `, pull request ${source.pullRequestUrl}` : "";
1343
- return `${repository.folder} (${repository.fullName}) at commit ${repository.baseSha.slice(0, 7)}, base branch origin/${source?.defaultBranch ?? "main"}${pull}`;
2024
+ const base = repository.local ? repository.local.baseBranch : `origin/${source?.defaultBranch ?? "main"}`;
2025
+ return `${repository.folder} (${repository.fullName}) at commit ${repository.baseSha.slice(0, 7)}, base branch ${base}${pull}`;
1344
2026
  });
1345
2027
  }
2028
+ function codeRules(job) {
2029
+ return isLocalJob(job) ? [...CODE_RULES.slice(0, -1), LOCAL_COMMIT_RULE] : CODE_RULES;
2030
+ }
1346
2031
  function stepLine(stage) {
1347
2032
  return `This step of the board is "${stage.name}"${stage.helper ? `, and you work as the ${stage.helper}` : ""}.`;
1348
2033
  }
@@ -1363,6 +2048,18 @@ function peopleText(people) {
1363
2048
  function canProposeKnowledge(job) {
1364
2049
  return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
1365
2050
  }
2051
+ function folderText(root, results) {
2052
+ const lines = results ?? [];
2053
+ return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
2054
+ ${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
2055
+ }
2056
+ function handOverText(boards) {
2057
+ const names = (boards ?? []).map((board) => board.name);
2058
+ if (names.length === 0) {
2059
+ return null;
2060
+ }
2061
+ 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.`;
2062
+ }
1366
2063
  function handoffText(targets, isCheck) {
1367
2064
  if (targets.length === 0) {
1368
2065
  return null;
@@ -1409,6 +2106,64 @@ function specGoal(job, work2) {
1409
2106
  }
1410
2107
  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
2108
  }
2109
+ function briefQuestionRules(work2) {
2110
+ 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.`;
2111
+ if (work2.round === 1) {
2112
+ return [
2113
+ BRIEF_RESEARCH_RULE,
2114
+ RESEARCH_BRIEF_RULE,
2115
+ `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.`,
2116
+ QUESTION_SHAPE_RULE,
2117
+ handIn,
2118
+ QUESTIONS_REPLY_RULE
2119
+ ];
2120
+ }
2121
+ const taken = work2.takenIds.length > 0 ? ` These ids are taken by round 1: ${work2.takenIds.join(", ")}.` : "";
2122
+ return [
2123
+ `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.`,
2124
+ `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}`,
2125
+ QUESTION_SHAPE_RULE,
2126
+ "Update the research brief with what the answers changed and leave pendingDecisions empty.",
2127
+ handIn,
2128
+ QUESTIONS_REPLY_RULE
2129
+ ];
2130
+ }
2131
+ function briefGoal(job, work2) {
2132
+ if (work2.round === null) {
2133
+ return `Write the brief for TAMsense task #${job.taskId}: ${job.title}`;
2134
+ }
2135
+ return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its brief: ${job.title}`;
2136
+ }
2137
+ function buildBriefPrompt(job, work2, workspace) {
2138
+ const prompt = [
2139
+ briefGoal(job, work2),
2140
+ job.description,
2141
+ work2.linkedAction,
2142
+ work2.brief ? `Research brief from the clarification phase (verify only what you rely on):
2143
+ ${JSON.stringify(work2.brief)}` : null,
2144
+ work2.round === null && job.spec ? `Current saved brief (hand in the full updated version):
2145
+ ${JSON.stringify(job.spec)}` : null,
2146
+ job.handoffNote && `Note from another step: ${job.handoffNote}`,
2147
+ threadText(work2.thread),
2148
+ teamMessagesText(job.messages)
2149
+ ].filter(Boolean).join("\n\n");
2150
+ const instructions = [
2151
+ BRIEF_UNATTENDED_RULE,
2152
+ MACHINE_RULE,
2153
+ workplaceText(job.computer, workspace.root),
2154
+ stepLine(job.stage),
2155
+ job.stage.instructions,
2156
+ contextText(job.projectContext),
2157
+ FOLDER_RULE,
2158
+ RESULT_HOME_RULE,
2159
+ `How to work:
2160
+ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
2161
+ safetyText(job),
2162
+ knowledgeText(job.knowledge, job.computer, false),
2163
+ BRIEF_STYLE_RULE
2164
+ ].filter(Boolean).join("\n\n");
2165
+ return { prompt, instructions };
2166
+ }
1412
2167
  function buildSpecPrompt(job, work2, workspace) {
1413
2168
  const prompt = [
1414
2169
  specGoal(job, work2),
@@ -1431,6 +2186,7 @@ ${JSON.stringify(job.spec)}` : null,
1431
2186
  contextText(job.projectContext),
1432
2187
  `The repositories for this task are checked out in the current folder, one subfolder each:
1433
2188
  ${bulletList(repositoryLines(job, workspace))}`,
2189
+ isLocalJob(job) && LOCAL_FOLDERS_RULE,
1434
2190
  READ_ONLY_RULE,
1435
2191
  `How to work:
1436
2192
  ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
@@ -1440,19 +2196,63 @@ ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
1440
2196
  ].filter(Boolean).join("\n\n");
1441
2197
  return { prompt, instructions };
1442
2198
  }
2199
+ var FOLDER_GOALS = {
2200
+ code: "Work on",
2201
+ review: "Check the result of",
2202
+ test: "Check the result of"
2203
+ };
2204
+ function buildFolderPrompt(job, workspace, options) {
2205
+ const kind = job.stage.kind === "review" || job.stage.kind === "test" ? job.stage.kind : "code";
2206
+ const isCheck = kind !== "code";
2207
+ const people = job.people ?? [];
2208
+ const canAsk = people.length > 0;
2209
+ const prompt = [
2210
+ `${FOLDER_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
2211
+ job.description,
2212
+ briefText(job.spec),
2213
+ job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
2214
+ job.review && `Notes to address:
2215
+ ${job.review}`,
2216
+ teamMessagesText(job.messages)
2217
+ ].filter(Boolean).join("\n\n");
2218
+ const rules = isCheck ? FOLDER_CHECK_RULES : [...FOLDER_DO_RULES, ...job.review ? [FOLDER_REWORK_RULE] : [], FOLDER_REPORT_RULE];
2219
+ const instructions = [
2220
+ canAsk ? ASKING_RULE : UNATTENDED_RULE,
2221
+ MACHINE_RULE,
2222
+ workplaceText(job.computer, workspace.root),
2223
+ stepLine(job.stage),
2224
+ job.stage.instructions,
2225
+ contextText(job.projectContext),
2226
+ `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2227
+ `How to work:
2228
+ ${bulletList(rules)}`,
2229
+ safetyText(job),
2230
+ knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
2231
+ APPROVAL_RULE,
2232
+ peopleText(people),
2233
+ isCheck ? null : handOverText(job.handOverBoards),
2234
+ handoffText(job.allowedHandoffs, isCheck),
2235
+ options.browser && BROWSER_RULE
2236
+ ].filter(Boolean).join("\n\n");
2237
+ return { prompt, instructions };
2238
+ }
1443
2239
  function buildTaskPrompt(job, workspace, options = {}) {
1444
2240
  const { kind } = job.stage;
1445
2241
  if (kind === "interview" || kind === "spec") {
1446
2242
  if (!job.specWork) {
1447
2243
  throw new Error(SPEC_WORK_MISSING);
1448
2244
  }
1449
- return buildSpecPrompt(job, job.specWork, workspace);
2245
+ return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
2246
+ }
2247
+ if (isFolderJob(job)) {
2248
+ return buildFolderPrompt(job, workspace, options);
1450
2249
  }
1451
2250
  const isCheck = kind !== "code";
2251
+ const readOnly = isReadOnlyCode(job.stage);
1452
2252
  const people = job.people ?? [];
1453
2253
  const canAsk = people.length > 0;
1454
2254
  const prompt = [
1455
- `${STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
2255
+ `${readOnly ? READ_ONLY_GOAL : STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
1456
2256
  job.description,
1457
2257
  specText(job.spec),
1458
2258
  job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
@@ -1462,7 +2262,7 @@ ${job.review}`,
1462
2262
  ].filter(Boolean).join("\n\n");
1463
2263
  const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
1464
2264
  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];
2265
+ const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : readOnly ? [...READ_ONLY_CODE_RULES, DEPENDENCIES_RULE, localApp, ...evidence, READ_ONLY_REPORT_RULE] : [...codeRules(job), ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
1466
2266
  const instructions = [
1467
2267
  canAsk ? ASKING_RULE : UNATTENDED_RULE,
1468
2268
  MACHINE_RULE,
@@ -1472,6 +2272,7 @@ ${job.review}`,
1472
2272
  contextText(job.projectContext),
1473
2273
  `The repositories for this task are checked out in the current folder, one subfolder each, ${isCheck ? "at the latest commit of branch" : "on branch"} ${job.branch}:
1474
2274
  ${bulletList(repositoryLines(job, workspace))}`,
2275
+ isLocalJob(job) && LOCAL_FOLDERS_RULE,
1475
2276
  `How to work:
1476
2277
  ${bulletList(rules)}`,
1477
2278
  safetyText(job),
@@ -1485,108 +2286,135 @@ ${bulletList(rules)}`,
1485
2286
  }
1486
2287
 
1487
2288
  // src/specTools.ts
1488
- import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1489
- import { z as z2 } from "zod";
2289
+ import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2290
+ import { z as z3 } from "zod";
1490
2291
  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")
2292
+ var questionSchema = z3.object({
2293
+ id: z3.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
2294
+ question: z3.string().describe("One plain sentence the owner can answer without reading code"),
2295
+ whyItMatters: z3.string().describe("One line: what changes in the result depending on the answer"),
2296
+ options: z3.array(z3.object({
2297
+ label: z3.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
2298
+ description: z3.string().describe("One sentence: what happens if this option is chosen"),
2299
+ recommended: z3.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
1499
2300
  })).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")
2301
+ multiSelect: z3.boolean().describe("true only when the options are additive and several can apply together")
1501
2302
  });
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")
2303
+ var briefSchema = z3.object({
2304
+ findings: z3.array(z3.object({
2305
+ fact: z3.string().describe("What the code does today that matters for this task"),
2306
+ where: z3.string().describe("Where it is: owner/repo path:line")
1506
2307
  })).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")
2308
+ 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"),
2309
+ approach: z3.string().describe("The approach you would take, in 2-4 sentences"),
2310
+ codeMap: z3.array(z3.object({
2311
+ repo: z3.string().describe("Repository full name (owner/name)"),
2312
+ file: z3.string().describe("Path relative to the repository root"),
2313
+ role: z3.string().describe("Why it matters: what to change or reuse there")
1513
2314
  })).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")
2315
+ assumptions: z3.array(z3.object({
2316
+ text: z3.string().describe("The default you chose"),
2317
+ reason: z3.string().describe("Why it is safe or why it was chosen")
1517
2318
  })).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")
2319
+ pendingDecisions: z3.array(z3.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
1519
2320
  });
1520
- var questionsPayloadSchema = z2.object({
2321
+ var questionsPayloadSchema = z3.object({
1521
2322
  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`)
2323
+ 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
2324
  });
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")
2325
+ var specSchema = z3.object({
2326
+ goal: z3.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2327
+ context: z3.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2328
+ inScope: z3.array(z3.string()).describe("What this task delivers, one item each"),
2329
+ 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"),
2330
+ decisions: z3.array(z3.object({
2331
+ question: z3.string(),
2332
+ answer: z3.string(),
2333
+ by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
1533
2334
  })).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")
2335
+ codeMap: z3.array(z3.object({
2336
+ repo: z3.string().describe("Repository full name (owner/name)"),
2337
+ file: z3.string().describe("Path relative to the repository root"),
2338
+ role: z3.string().describe("What the coder needs to know about this file"),
2339
+ use: z3.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
1539
2340
  })).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")
2341
+ changes: z3.array(z3.object({
2342
+ repo: z3.string().describe("Repository full name (owner/name) where the change happens"),
2343
+ file: z3.string().describe("Path to the file, relative to the repository root"),
2344
+ action: z3.enum(["create", "modify", "delete"]),
2345
+ 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"),
2346
+ produces: z3.array(z3.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2347
+ consumes: z3.array(z3.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2348
+ acceptance: z3.array(z3.string()).describe("Ids of the criteria this change makes true, like AC-1")
1548
2349
  })).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')
2350
+ acceptance: z3.array(z3.object({
2351
+ id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2352
+ 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"),
2353
+ text: z3.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
1553
2354
  })).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")
2355
+ testPlan: z3.object({
2356
+ automated: z3.array(z3.object({
2357
+ repo: z3.string(),
2358
+ command: z3.string().describe("The exact command to run"),
2359
+ expect: z3.string().describe("What passing looks like")
1559
2360
  })).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")
2361
+ newTests: z3.array(z3.object({
2362
+ seam: z3.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2363
+ covers: z3.array(z3.string()).describe("Ids of the criteria it covers")
1563
2364
  })),
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")
2365
+ manual: z3.array(z3.object({
2366
+ page: z3.string().describe("The page or screen"),
2367
+ steps: z3.string().describe("What a person does and sees")
1567
2368
  }))
1568
2369
  }),
1569
- risks: z2.array(z2.object({
1570
- risk: z2.string(),
1571
- mitigation: z2.string()
2370
+ risks: z3.array(z3.object({
2371
+ risk: z3.string(),
2372
+ mitigation: z3.string()
1572
2373
  })).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")
2374
+ riskLevel: z3.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
2375
+ assumptions: z3.array(z3.object({
2376
+ text: z3.string().describe("The default you chose"),
2377
+ reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
1577
2378
  })).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")
2379
+ 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"),
2380
+ 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
2381
  });
1581
- function reply(text2, isError = false) {
2382
+ var resultBriefSchema = z3.object({
2383
+ format: z3.literal("brief").describe('Always "brief"'),
2384
+ goal: z3.string().describe("What the work is for, for whom, and the outcome people will notice"),
2385
+ context: z3.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
2386
+ contains: z3.array(z3.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
2387
+ acceptance: z3.array(z3.object({
2388
+ id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2389
+ text: z3.string().describe("One observable property of the result that a checker can confirm by reading it")
2390
+ })).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
2391
+ constraints: z3.array(z3.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
2392
+ 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"),
2393
+ sources: z3.array(z3.object({
2394
+ 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"),
2395
+ use: z3.string().describe("What to take from it")
2396
+ })).describe("Sources the work must use. Empty when the task itself is enough"),
2397
+ approver: z3.string().describe("Who approves the result, as the task or the board names them"),
2398
+ decisions: z3.array(z3.object({
2399
+ question: z3.string(),
2400
+ answer: z3.string(),
2401
+ by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2402
+ })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2403
+ assumptions: z3.array(z3.object({
2404
+ text: z3.string().describe("The default you chose"),
2405
+ reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
2406
+ })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2407
+ 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")
2408
+ });
2409
+ function reply2(text2, isError = false) {
1582
2410
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
1583
2411
  }
1584
2412
  function unreachable(error, toolName) {
1585
2413
  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);
2414
+ return reply2(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
1587
2415
  }
1588
2416
  function questionsTool(review, onSubmit) {
1589
- return tool2(
2417
+ return tool3(
1590
2418
  SUBMIT_QUESTIONS_TOOL_NAME,
1591
2419
  "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
2420
  questionsPayloadSchema.shape,
@@ -1598,10 +2426,10 @@ function questionsTool(review, onSubmit) {
1598
2426
  return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
1599
2427
  }
1600
2428
  if (!verdict.accepted) {
1601
- return reply(verdict.feedback, true);
2429
+ return reply2(verdict.feedback, true);
1602
2430
  }
1603
2431
  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.");
2432
+ 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
2433
  }
1606
2434
  );
1607
2435
  }
@@ -1609,7 +2437,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
1609
2437
  function specTool(review, onSubmit, builderCheckRan) {
1610
2438
  let repairs = 0;
1611
2439
  let reminded = false;
1612
- return tool2(
2440
+ return tool3(
1613
2441
  SUBMIT_SPEC_TOOL_NAME,
1614
2442
  "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
2443
  specSchema.shape,
@@ -1622,14 +2450,36 @@ function specTool(review, onSubmit, builderCheckRan) {
1622
2450
  }
1623
2451
  if (!verdict.accepted) {
1624
2452
  repairs += 1;
1625
- return reply(verdict.feedback, true);
2453
+ return reply2(verdict.feedback, true);
1626
2454
  }
1627
2455
  if (!builderCheckRan() && !reminded) {
1628
2456
  reminded = true;
1629
- return reply(BUILDER_CHECK_FIRST, true);
2457
+ return reply2(BUILDER_CHECK_FIRST, true);
2458
+ }
2459
+ onSubmit({ spec: args, problems: verdict.problems });
2460
+ return reply2(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2461
+ }
2462
+ );
2463
+ }
2464
+ function briefTool(review, onSubmit) {
2465
+ let repairs = 0;
2466
+ return tool3(
2467
+ SUBMIT_SPEC_TOOL_NAME,
2468
+ "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.",
2469
+ resultBriefSchema.shape,
2470
+ async (args) => {
2471
+ let verdict;
2472
+ try {
2473
+ verdict = await review({ spec: args, repairs });
2474
+ } catch (error) {
2475
+ return unreachable(error, SUBMIT_SPEC_TOOL_NAME);
2476
+ }
2477
+ if (!verdict.accepted) {
2478
+ repairs += 1;
2479
+ return reply2(verdict.feedback, true);
1630
2480
  }
1631
2481
  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.");
2482
+ return reply2(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
1633
2483
  }
1634
2484
  );
1635
2485
  }
@@ -1731,10 +2581,9 @@ function usageLimitWatch() {
1731
2581
  // src/claude.ts
1732
2582
  var NO_RESULT_TEXT = "Claude finished without a result.";
1733
2583
  var SERVER_NAME = "tamsense";
1734
- var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
1735
2584
  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"')
2585
+ 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"'),
2586
+ purpose: z4.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
1738
2587
  };
1739
2588
  var ATTACH_TOOLS = {
1740
2589
  screenshot: {
@@ -1755,21 +2604,21 @@ var ATTACH_TOOLS = {
1755
2604
  };
1756
2605
  function attachTool(kind, attach) {
1757
2606
  const spec = ATTACH_TOOLS[kind];
1758
- return tool3(
2607
+ return tool4(
1759
2608
  spec.name,
1760
2609
  spec.description,
1761
- { path: z3.string().describe(spec.path), ...CAPTION_FIELDS },
2610
+ { path: z4.string().describe(spec.path), ...CAPTION_FIELDS },
1762
2611
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
1763
2612
  );
1764
2613
  }
1765
2614
  function verdictTool(onVerdict) {
1766
- return tool3(
2615
+ return tool4(
1767
2616
  VERDICT_TOOL_NAME,
1768
2617
  "Hand in the result of this check. Call it exactly once, at the very end.",
1769
2618
  {
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")
2619
+ passed: z4.boolean().describe("true only if nothing must be fixed"),
2620
+ report: z4.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
2621
+ 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
2622
  },
1774
2623
  async (args) => {
1775
2624
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -1778,13 +2627,13 @@ function verdictTool(onVerdict) {
1778
2627
  );
1779
2628
  }
1780
2629
  function handoffTool(targets, onHandoff) {
1781
- return tool3(
2630
+ return tool4(
1782
2631
  HANDOFF_TOOL_NAME,
1783
2632
  "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
2633
  {
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")
2634
+ to: z4.string().describe("name or id of the step to hand the task to"),
2635
+ outcome: z4.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
2636
+ note: z4.string().describe("for the next agent and the owner: what was done, what to check, what is open")
1788
2637
  },
1789
2638
  async (args) => {
1790
2639
  const choice = chooseHandoff(targets, args);
@@ -1797,25 +2646,25 @@ function handoffTool(targets, onHandoff) {
1797
2646
  );
1798
2647
  }
1799
2648
  function askPersonTool(asking, waiting) {
1800
- return tool3(
2649
+ return tool4(
1801
2650
  ASK_PERSON_TOOL_NAME,
1802
2651
  "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
2652
  {
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")
2653
+ 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"),
2654
+ why: z4.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
2655
+ question: z4.string().describe("one plain question the person can answer without reading the code"),
2656
+ context: z4.string().optional().describe("one line on what you saw that led to the question"),
2657
+ tried: z4.array(z4.object({
2658
+ did: z4.string().describe("the command or step"),
2659
+ error: z4.string().optional().describe("the exact error line, if there was one"),
2660
+ result: z4.string().describe("what it showed, one line")
1812
2661
  })).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")
2662
+ options: z4.array(z4.object({
2663
+ label: z4.string().describe("a short answer the person can pick"),
2664
+ detail: z4.string().describe("one line on what happens if it is picked")
1816
2665
  })).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")
2666
+ blocking: z4.boolean().describe("true when you cannot go on without the answer"),
2667
+ assumption: z4.string().optional().describe("when blocking is false: what you continue with until the answer comes")
1819
2668
  },
1820
2669
  async (args) => {
1821
2670
  if (waiting.waitingFor !== null) {
@@ -1825,16 +2674,16 @@ function askPersonTool(asking, waiting) {
1825
2674
  if ("error" in choice) {
1826
2675
  return { content: [{ type: "text", text: choice.error }], isError: true };
1827
2676
  }
1828
- let reply2;
2677
+ let reply3;
1829
2678
  try {
1830
- reply2 = await asking.send(choice.body);
2679
+ reply3 = await asking.send(choice.body);
1831
2680
  } catch (error) {
1832
2681
  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
2682
  }
1834
2683
  if (choice.body.blocking) {
1835
- waiting.waitingFor = reply2.id;
2684
+ waiting.waitingFor = reply3.id;
1836
2685
  }
1837
- return { content: [{ type: "text", text: askedText(reply2.name, choice.body) }] };
2686
+ return { content: [{ type: "text", text: askedText(reply3.name, choice.body) }] };
1838
2687
  }
1839
2688
  );
1840
2689
  }
@@ -1842,8 +2691,13 @@ function specWorkTools(specWork, submitted) {
1842
2691
  if (!specWork) {
1843
2692
  return [];
1844
2693
  }
1845
- return specWork.stage === "interview" ? [questionsTool(specWork.review, (questions) => {
1846
- submitted.questions = questions;
2694
+ if (specWork.stage === "interview") {
2695
+ return [questionsTool(specWork.review, (questions) => {
2696
+ submitted.questions = questions;
2697
+ })];
2698
+ }
2699
+ return specWork.brief ? [briefTool(specWork.review, (brief) => {
2700
+ submitted.spec = brief;
1847
2701
  })] : [specTool(specWork.review, (spec) => {
1848
2702
  submitted.spec = spec;
1849
2703
  }, () => submitted.helperStarted)];
@@ -1871,7 +2725,9 @@ function tamsenseServer(request, submitted) {
1871
2725
  ...knowledgeTools(request.knowledge),
1872
2726
  ...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
1873
2727
  submitted.handoff = handoff;
1874
- })] : []
2728
+ })] : [],
2729
+ ...request.results ? resultToolset(request.results) : [],
2730
+ ...request.handOver ? [handOverTool(request.handOver)] : []
1875
2731
  ];
1876
2732
  return {
1877
2733
  server: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }),
@@ -1898,6 +2754,7 @@ async function attempt(request, context, resume) {
1898
2754
  let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
1899
2755
  const tamsense = tamsenseServer(request, context.submitted);
1900
2756
  const shown = hiddenStepsFilter();
2757
+ const helpers = helperTasks();
1901
2758
  const messages = context.startQuery({
1902
2759
  prompt: request.input.start(request.prompt.prompt),
1903
2760
  options: {
@@ -1909,7 +2766,7 @@ async function attempt(request, context, resume) {
1909
2766
  thinking: { type: "adaptive", display: "summarized" },
1910
2767
  // режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
1911
2768
  canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
1912
- plugins: [{ type: "local", path: LIBRARY_PATH }],
2769
+ plugins: [{ type: "local", path: request.library }],
1913
2770
  mcpServers: { [SERVER_NAME]: tamsense.server },
1914
2771
  allowedTools: tamsense.allowed,
1915
2772
  abortController: context.abortController,
@@ -1919,6 +2776,7 @@ async function attempt(request, context, resume) {
1919
2776
  }
1920
2777
  });
1921
2778
  request.input.onSwitch((change) => applySwitch(messages, change));
2779
+ request.input.onStopHelper((ref) => helpers.stop(messages, ref));
1922
2780
  const failedToResume = () => resume !== null && sessionId === null && !request.signal.aborted;
1923
2781
  try {
1924
2782
  for await (const message of messages) {
@@ -1930,7 +2788,7 @@ async function attempt(request, context, resume) {
1930
2788
  messages.close();
1931
2789
  return { outcome, sessionId, resumeFailed: true };
1932
2790
  }
1933
- for (const draft of feedOf(message).filter(shown)) {
2791
+ for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
1934
2792
  if (draft.kind === "agent" && draft.parent === null) {
1935
2793
  context.submitted.helperStarted = true;
1936
2794
  }
@@ -1981,13 +2839,13 @@ function claudeRunner(startQuery) {
1981
2839
  var runClaude = claudeRunner(query2);
1982
2840
 
1983
2841
  // src/config.ts
1984
- import { mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
2842
+ import { mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
1985
2843
  import { homedir as homedir2 } from "os";
1986
- import { dirname as dirname2, join as join3 } from "path";
2844
+ import { dirname as dirname2, join as join6 } from "path";
1987
2845
  var OWNER_ONLY_FILE = 384;
1988
2846
  var OWNER_ONLY_FOLDER = 448;
1989
2847
  function configPath(home = homedir2()) {
1990
- return join3(home, ".tamsense", "runner.json");
2848
+ return join6(home, ".tamsense", "runner.json");
1991
2849
  }
1992
2850
  function isRecord2(value) {
1993
2851
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2005,7 +2863,7 @@ function isMissingFile(error) {
2005
2863
  }
2006
2864
  async function readConfig(path = configPath()) {
2007
2865
  try {
2008
- return parseConfig(JSON.parse(await readFile2(path, "utf8")));
2866
+ return parseConfig(JSON.parse(await readFile3(path, "utf8")));
2009
2867
  } catch (error) {
2010
2868
  if (isMissingFile(error) || error instanceof SyntaxError) {
2011
2869
  return null;
@@ -2014,34 +2872,12 @@ async function readConfig(path = configPath()) {
2014
2872
  }
2015
2873
  }
2016
2874
  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)}
2875
+ await mkdir3(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
2876
+ await writeFile3(path, `${JSON.stringify(config, null, 4)}
2019
2877
  `, { mode: OWNER_ONLY_FILE });
2020
2878
  }
2021
2879
  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}`;
2880
+ await rm4(path, { force: true });
2045
2881
  }
2046
2882
 
2047
2883
  // src/login.ts
@@ -2058,14 +2894,14 @@ async function login(tools, request) {
2058
2894
  tools.openBrowser(pageUrl);
2059
2895
  while (tools.now() < expiresAt) {
2060
2896
  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 };
2897
+ const reply3 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
2898
+ if (reply3.status === "connected") {
2899
+ return { token: reply3.token, device: reply3.device };
2064
2900
  }
2065
- if (reply2.status === "denied") {
2901
+ if (reply3.status === "denied") {
2066
2902
  throw new Error(DENIED_TEXT);
2067
2903
  }
2068
- if (reply2.status === "expired") {
2904
+ if (reply3.status === "expired") {
2069
2905
  break;
2070
2906
  }
2071
2907
  }
@@ -2073,15 +2909,15 @@ async function login(tools, request) {
2073
2909
  }
2074
2910
 
2075
2911
  // src/recorderEnv.ts
2076
- import { existsSync as existsSync3 } from "fs";
2077
- import { join as join5 } from "path";
2078
- import { fileURLToPath as fileURLToPath2 } from "url";
2912
+ import { existsSync as existsSync5 } from "fs";
2913
+ import { join as join8 } from "path";
2914
+ import { fileURLToPath } from "url";
2079
2915
 
2080
2916
  // src/recorder.ts
2081
2917
  import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
2082
2918
  import Module, { createRequire } from "module";
2083
- import { basename, dirname as dirname3, join as join4 } from "path";
2084
- import process2 from "process";
2919
+ import { basename as basename3, dirname as dirname3, join as join7 } from "path";
2920
+ import process4 from "process";
2085
2921
  var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
2086
2922
  var SERVER_ENV = "TAMSENSE_SERVER";
2087
2923
  var DEFAULT_TITLE_ENV = "TAMSENSE_RECORDING_TITLE";
@@ -2105,14 +2941,14 @@ function state() {
2105
2941
  return holder[STATE_KEY];
2106
2942
  }
2107
2943
  function recordingsDir() {
2108
- const dir = process2.env[RECORDINGS_DIR_ENV];
2944
+ const dir = process4.env[RECORDINGS_DIR_ENV];
2109
2945
  if (!dir) {
2110
2946
  throw new Error(`${RECORDINGS_DIR_ENV} is not set: recordings work only inside a TAMsense run`);
2111
2947
  }
2112
2948
  return dir;
2113
2949
  }
2114
2950
  function warn(text2) {
2115
- process2.stderr.write(`[tamsense recorder] ${text2}
2951
+ process4.stderr.write(`[tamsense recorder] ${text2}
2116
2952
  `);
2117
2953
  }
2118
2954
  function isTrackerIngest(url, tamsenseOrigins) {
@@ -2128,7 +2964,7 @@ function isLocalHost(hostname2) {
2128
2964
  function isExternalWrite(method, url) {
2129
2965
  return !READ_METHODS.includes(method.toUpperCase()) && !isLocalHost(url.hostname);
2130
2966
  }
2131
- function trackerOrigins(env = process2.env) {
2967
+ function trackerOrigins(env = process4.env) {
2132
2968
  const origins = new Set(DEFAULT_TRACKER_ORIGINS);
2133
2969
  try {
2134
2970
  if (env[SERVER_ENV]) {
@@ -2152,8 +2988,8 @@ function checkCaption(caption) {
2152
2988
  function clip2(text2, max) {
2153
2989
  return text2.length > max ? `${text2.slice(0, max - 1).trimEnd()}\u2026` : text2;
2154
2990
  }
2155
- function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
2156
- const from = script2 ? `, opened by ${basename(script2)}` : "";
2991
+ function automaticCaption(env = process4.env, script2 = process4.argv[1]) {
2992
+ const from = script2 ? `, opened by ${basename3(script2)}` : "";
2157
2993
  return {
2158
2994
  title: clip2(env[DEFAULT_TITLE_ENV]?.trim() || "Browser session", MAX_TITLE_LENGTH),
2159
2995
  purpose: clip2(`${env[DEFAULT_PURPOSE_ENV]?.trim() || "Recorded automatically while the agent used a browser"}${from}`, MAX_PURPOSE_LENGTH)
@@ -2161,7 +2997,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
2161
2997
  }
2162
2998
  function rrwebSource() {
2163
2999
  const entry = createRequire(import.meta.url).resolve("rrweb");
2164
- return readFileSync(join4(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
3000
+ return readFileSync(join7(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
2165
3001
  }
2166
3002
  function pageScript(rrweb) {
2167
3003
  return `(() => {
@@ -2212,8 +3048,8 @@ var Session = class {
2212
3048
  bytes = 0;
2213
3049
  constructor(dir, recording, url) {
2214
3050
  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}`);
3051
+ this.file = join7(dir, `${id}${REPLAY_SUFFIX}`);
3052
+ this.metaFile = join7(dir, `${id}${META_SUFFIX}`);
2217
3053
  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
3054
  writeFileSync(this.file, "");
2219
3055
  this.save();
@@ -2378,18 +3214,18 @@ function installAutoRecording() {
2378
3214
  return exports;
2379
3215
  };
2380
3216
  }
2381
- if (process2.env[RECORDINGS_DIR_ENV]) {
3217
+ if (process4.env[RECORDINGS_DIR_ENV]) {
2382
3218
  installAutoRecording();
2383
3219
  }
2384
3220
 
2385
3221
  // src/recorderEnv.ts
2386
3222
  var RECORDER_ENV = "TAMSENSE_RECORDER";
2387
3223
  function bundledRecorder() {
2388
- const path = fileURLToPath2(new URL("./recorder.cjs", import.meta.url));
2389
- return existsSync3(path) ? path : null;
3224
+ const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
3225
+ return existsSync5(path) ? path : null;
2390
3226
  }
2391
3227
  function recordingsFolder(home, runId) {
2392
- return join5(home, ".tamsense", "recordings", `run-${runId}`);
3228
+ return join8(home, ".tamsense", "recordings", `run-${runId}`);
2393
3229
  }
2394
3230
  function nodeOptionsWithRecorder(recorder, nodeOptions) {
2395
3231
  return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
@@ -2425,12 +3261,12 @@ var PLATFORM_LABELS = {
2425
3261
  darwin: "macOS",
2426
3262
  linux: "Linux"
2427
3263
  };
2428
- var MAX_NAME_LENGTH = 80;
3264
+ var MAX_NAME_LENGTH2 = 80;
2429
3265
  function platformLabel(platform = process.platform) {
2430
3266
  return PLATFORM_LABELS[platform] ?? platform;
2431
3267
  }
2432
3268
  function defaultDeviceName() {
2433
- return hostname().slice(0, MAX_NAME_LENGTH) || "My computer";
3269
+ return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
2434
3270
  }
2435
3271
  function browserCommand(url, platform) {
2436
3272
  if (platform === "win32") {
@@ -2462,7 +3298,7 @@ async function sleep(ms, signal) {
2462
3298
  // src/update.ts
2463
3299
  import { spawn as spawn4 } from "child_process";
2464
3300
  import { readFileSync as readFileSync2 } from "fs";
2465
- import process3 from "process";
3301
+ import process5 from "process";
2466
3302
  var PACKAGE_NAME = "@tamsensedev/runner";
2467
3303
  var RELAUNCHED_ENV = "TAMSENSE_RELAUNCHED";
2468
3304
  var NO_UPDATE_ENV = "TAMSENSE_NO_UPDATE";
@@ -2514,27 +3350,27 @@ function relaunchArgs(argv) {
2514
3350
  return ["--yes", `${PACKAGE_NAME}@latest`, ...argv];
2515
3351
  }
2516
3352
  function relaunch(argv, env) {
2517
- const isWindows = process3.platform === "win32";
2518
- return new Promise((resolve2, reject) => {
3353
+ const isWindows = process5.platform === "win32";
3354
+ return new Promise((resolve5, reject) => {
2519
3355
  const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
2520
3356
  const ignoreInterrupt = () => {
2521
3357
  };
2522
- process3.on("SIGINT", ignoreInterrupt);
3358
+ process5.on("SIGINT", ignoreInterrupt);
2523
3359
  child.on("error", (error) => {
2524
- process3.off("SIGINT", ignoreInterrupt);
3360
+ process5.off("SIGINT", ignoreInterrupt);
2525
3361
  reject(error);
2526
3362
  });
2527
3363
  child.on("close", (code) => {
2528
- process3.off("SIGINT", ignoreInterrupt);
2529
- resolve2(code ?? 1);
3364
+ process5.off("SIGINT", ignoreInterrupt);
3365
+ resolve5(code ?? 1);
2530
3366
  });
2531
3367
  });
2532
3368
  }
2533
3369
 
2534
3370
  // src/worker.ts
2535
- import { rm as rm4 } from "fs/promises";
3371
+ import { rm as rm6 } from "fs/promises";
2536
3372
  import { dirname as dirname5 } from "path";
2537
- import process4 from "process";
3373
+ import process6 from "process";
2538
3374
 
2539
3375
  // src/feedQueue.ts
2540
3376
  var MAX_BATCH = 200;
@@ -2572,11 +3408,43 @@ function feedQueue(send) {
2572
3408
  };
2573
3409
  }
2574
3410
 
3411
+ // src/library.ts
3412
+ import { existsSync as existsSync6 } from "fs";
3413
+ import { cp, mkdir as mkdir4, rm as rm5 } from "fs/promises";
3414
+ import { join as join9 } from "path";
3415
+ import { fileURLToPath as fileURLToPath2 } from "url";
3416
+ var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
3417
+ var SKILL_NAME = /^[a-z0-9-]+$/;
3418
+ var STAGE_SKILLS = {
3419
+ interview: ["task-interview"],
3420
+ spec: ["spec-writing", "task-interview"]
3421
+ };
3422
+ function allowedSkills(stage) {
3423
+ const skills = stage.role?.skills ?? null;
3424
+ return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
3425
+ }
3426
+ function roleLibraryFolder(home, runId) {
3427
+ return join9(home, ".tamsense", "libraries", `run-${runId}`);
3428
+ }
3429
+ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3430
+ if (skills === null) {
3431
+ return source;
3432
+ }
3433
+ await rm5(folder, { recursive: true, force: true });
3434
+ await mkdir4(join9(folder, "skills"), { recursive: true });
3435
+ await cp(join9(source, ".claude-plugin"), join9(folder, ".claude-plugin"), { recursive: true });
3436
+ await cp(join9(source, "agents"), join9(folder, "agents"), { recursive: true });
3437
+ for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync6(join9(source, "skills", name)))) {
3438
+ await cp(join9(source, "skills", skill), join9(folder, "skills", skill), { recursive: true });
3439
+ }
3440
+ return folder;
3441
+ }
3442
+
2575
3443
  // src/recordings.ts
2576
3444
  import { randomUUID } from "crypto";
2577
3445
  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";
3446
+ import { readdir, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
3447
+ import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, join as join10 } from "path";
2580
3448
  import { createInterface as createInterface2 } from "readline";
2581
3449
  import { Readable } from "stream";
2582
3450
  import { promisify } from "util";
@@ -2637,23 +3505,23 @@ function marksFrom(marks, startTs) {
2637
3505
  return marks.map((mark) => ({ at: Math.max(0, mark.ts - startTs), label: mark.label })).sort((a, b) => a.at - b.at);
2638
3506
  }
2639
3507
  function sessionIdOf(file) {
2640
- return basename2(file).replace(REPLAY_SUFFIX, "");
3508
+ return basename4(file).replace(REPLAY_SUFFIX, "");
2641
3509
  }
2642
3510
  async function readMeta(file) {
2643
3511
  try {
2644
- return JSON.parse(await readFile3(file, "utf8"));
3512
+ return JSON.parse(await readFile4(file, "utf8"));
2645
3513
  } catch {
2646
3514
  return null;
2647
3515
  }
2648
3516
  }
2649
3517
  async function metaFor(kind, file) {
2650
3518
  if (kind === "replay") {
2651
- return readMeta(join6(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
3519
+ return readMeta(join10(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
2652
3520
  }
2653
- const folder = basename2(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
3521
+ const folder = basename4(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
2654
3522
  for (const name of await readdir(folder).catch(() => [])) {
2655
3523
  if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
2656
- const meta = await readMeta(join6(folder, name));
3524
+ const meta = await readMeta(join10(folder, name));
2657
3525
  if (meta?.video === file) {
2658
3526
  return meta;
2659
3527
  }
@@ -2663,7 +3531,7 @@ async function metaFor(kind, file) {
2663
3531
  }
2664
3532
  async function ledger(dir) {
2665
3533
  try {
2666
- return new Set(JSON.parse(await readFile3(join6(dir, LEDGER_FILE), "utf8")));
3534
+ return new Set(JSON.parse(await readFile4(join10(dir, LEDGER_FILE), "utf8")));
2667
3535
  } catch {
2668
3536
  return /* @__PURE__ */ new Set();
2669
3537
  }
@@ -2671,10 +3539,10 @@ async function ledger(dir) {
2671
3539
  async function remember(dir, file) {
2672
3540
  const done2 = await ledger(dir);
2673
3541
  done2.add(file);
2674
- await writeFile3(join6(dir, LEDGER_FILE), JSON.stringify([...done2]));
3542
+ await writeFile4(join10(dir, LEDGER_FILE), JSON.stringify([...done2]));
2675
3543
  }
2676
3544
  async function nonEmpty(file) {
2677
- return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
3545
+ return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
2678
3546
  }
2679
3547
  async function pendingRecordings(dir) {
2680
3548
  const done2 = await ledger(dir);
@@ -2684,8 +3552,8 @@ async function pendingRecordings(dir) {
2684
3552
  if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
2685
3553
  continue;
2686
3554
  }
2687
- const meta = await readMeta(join6(dir, name));
2688
- const replay = join6(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
3555
+ const meta = await readMeta(join10(dir, name));
3556
+ const replay = join10(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
2689
3557
  if (!done2.has(replay) && await nonEmpty(replay)) {
2690
3558
  pending.push({ kind: "replay", file: replay, meta });
2691
3559
  }
@@ -2705,11 +3573,11 @@ async function uploadReplay(upload, file, caption) {
2705
3573
  if (!scan.snapshot) {
2706
3574
  throw new Error("This replay has no page snapshot, so it cannot be played. Was the page ever opened?");
2707
3575
  }
2708
- return finish(upload, { kind: "replay", upload: id, name: basename2(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
3576
+ return finish(upload, { kind: "replay", upload: id, name: basename4(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
2709
3577
  }
2710
3578
  async function uploadVideo(upload, file, caption, meta) {
2711
- const contentType = VIDEO_TYPES_BY_EXTENSION[extname(file).toLowerCase()];
2712
- const info = await stat(file);
3579
+ const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
3580
+ const info = await stat2(file);
2713
3581
  if (info.size > MAX_VIDEO_BYTES) {
2714
3582
  throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
2715
3583
  }
@@ -2720,16 +3588,16 @@ async function uploadVideo(upload, file, caption, meta) {
2720
3588
  const endedAt = meta?.endedAt ?? info.mtimeMs;
2721
3589
  const durationMs = Math.max(0, Math.round(endedAt - startedAt));
2722
3590
  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 });
3591
+ return finish(upload, { kind: "video", upload: id, name: basename4(file), ...caption, contentType, durationMs, marks });
2724
3592
  }
2725
3593
  async function finish(upload, body) {
2726
3594
  return (await upload.api.request("POST", runPath(upload.runId, "recordings"), body)).id;
2727
3595
  }
2728
3596
  async function attachRecording(api, runId, recordingsDir2, request) {
2729
- if (!isAbsolute(request.path)) {
3597
+ if (!isAbsolute3(request.path)) {
2730
3598
  return `Give the absolute path of the ${request.kind === "replay" ? "replay (.rrweb.ndjson)" : "video (.webm or .mp4)"} file.`;
2731
3599
  }
2732
- const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) : extname(request.path).toLowerCase() in VIDEO_TYPES_BY_EXTENSION;
3600
+ const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) : extname2(request.path).toLowerCase() in VIDEO_TYPES_BY_EXTENSION;
2733
3601
  if (!fitsKind) {
2734
3602
  return request.kind === "replay" ? `A replay is the ${REPLAY_SUFFIX} file the TAMsense recorder wrote.` : "Only WebM and MP4 videos can be attached.";
2735
3603
  }
@@ -2794,19 +3662,19 @@ async function uploadPendingRecordings(api, runId, dir) {
2794
3662
  }
2795
3663
 
2796
3664
  // 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 = {
3665
+ import { readFile as readFile5, stat as stat3 } from "fs/promises";
3666
+ import { basename as basename5, extname as extname3, isAbsolute as isAbsolute4 } from "path";
3667
+ var TYPES_BY_EXTENSION2 = {
2800
3668
  ".png": "image/png",
2801
3669
  ".jpg": "image/jpeg",
2802
3670
  ".jpeg": "image/jpeg",
2803
3671
  ".webp": "image/webp"
2804
3672
  };
2805
3673
  async function readScreenshot(request) {
2806
- if (!isAbsolute2(request.path)) {
3674
+ if (!isAbsolute4(request.path)) {
2807
3675
  return "Give the absolute path of the image file.";
2808
3676
  }
2809
- const contentType = TYPES_BY_EXTENSION[extname2(request.path).toLowerCase()];
3677
+ const contentType = TYPES_BY_EXTENSION2[extname3(request.path).toLowerCase()];
2810
3678
  if (!contentType) {
2811
3679
  return "Only PNG, JPEG and WebP images can be attached.";
2812
3680
  }
@@ -2816,15 +3684,15 @@ async function readScreenshot(request) {
2816
3684
  } catch (error) {
2817
3685
  return error instanceof Error ? error.message : String(error);
2818
3686
  }
2819
- const info = await stat2(request.path).catch(() => null);
3687
+ const info = await stat3(request.path).catch(() => null);
2820
3688
  if (!info?.isFile()) {
2821
3689
  return `There is no file at ${request.path}.`;
2822
3690
  }
2823
3691
  if (info.size > MAX_ATTACHMENT_BYTES) {
2824
3692
  return "The image is larger than 5 MB. Take a smaller screenshot (viewport instead of full page, or JPEG).";
2825
3693
  }
2826
- const data = (await readFile4(request.path)).toString("base64");
2827
- return { name: basename3(request.path), ...caption, contentType, data };
3694
+ const data = (await readFile5(request.path)).toString("base64");
3695
+ return { name: basename5(request.path), ...caption, contentType, data };
2828
3696
  }
2829
3697
 
2830
3698
  // src/worker.ts
@@ -2887,17 +3755,31 @@ function messageLine(message) {
2887
3755
  const line = `Message from ${message.authorName}: ${message.text}`.replace(/\s+/g, " ").trim();
2888
3756
  return line.length > MAX_MESSAGE_LINE ? `${line.slice(0, MAX_MESSAGE_LINE - 1)}\u2026` : line;
2889
3757
  }
3758
+ async function stopHelper(input, stop, print2) {
3759
+ try {
3760
+ const result = await input.stopHelper(stop.ref);
3761
+ if (result === "stopped") {
3762
+ input.note(helperStoppedNote(stop), "queue");
3763
+ print2(` ${stop.byName} stopped the helper ${stop.name}`);
3764
+ } else if (result === "unknown" && input.note(helperStopRequest(stop), "steer")) {
3765
+ print2(` ${stop.byName} asked Claude to stop the helper ${stop.name}`);
3766
+ }
3767
+ } catch (error) {
3768
+ print2(`Could not stop the helper ${stop.name}: ${messageOf3(error)}`);
3769
+ }
3770
+ }
2890
3771
  function inbox(job, tools, record, closedOnSite) {
2891
3772
  const input = inputChannel({ onIdle: () => void poll() });
3773
+ const stopsSeen = /* @__PURE__ */ new Set();
2892
3774
  let running = null;
2893
3775
  let again = false;
2894
3776
  let failing = false;
2895
3777
  async function check() {
2896
3778
  const ack = input.takeDelivered();
2897
3779
  const body = { ack };
2898
- let reply2;
3780
+ let reply3;
2899
3781
  try {
2900
- reply2 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
3782
+ reply3 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
2901
3783
  } catch (error) {
2902
3784
  input.restoreDelivered(ack);
2903
3785
  if (!failing) {
@@ -2907,17 +3789,23 @@ function inbox(job, tools, record, closedOnSite) {
2907
3789
  return;
2908
3790
  }
2909
3791
  failing = false;
2910
- if (!reply2.keepGoing) {
3792
+ if (!reply3.keepGoing) {
2911
3793
  closedOnSite();
2912
3794
  return;
2913
3795
  }
2914
- for (const message of reply2.messages) {
3796
+ for (const message of reply3.messages) {
2915
3797
  if (input.push(message)) {
2916
3798
  tools.print(` ${messageLine(message)}`);
2917
3799
  }
2918
3800
  }
2919
- if (reply2.model !== null || reply2.effort !== null) {
2920
- const change = { model: reply2.model, effort: reply2.effort };
3801
+ for (const stop of reply3.helperStops ?? []) {
3802
+ if (!stopsSeen.has(stop.ref)) {
3803
+ stopsSeen.add(stop.ref);
3804
+ void stopHelper(input, stop, tools.print);
3805
+ }
3806
+ }
3807
+ if (reply3.model !== null || reply3.effort !== null) {
3808
+ const change = { model: reply3.model, effort: reply3.effort };
2921
3809
  void input.switchModel(change).then(
2922
3810
  (applied) => {
2923
3811
  if (applied) {
@@ -2957,7 +3845,7 @@ function specWorkTools2(job, api) {
2957
3845
  if (kind !== "interview" && kind !== "spec") {
2958
3846
  return null;
2959
3847
  }
2960
- return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body) };
3848
+ return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body), brief: isFolderJob(job) };
2961
3849
  }
2962
3850
  function askingTools(job, api) {
2963
3851
  const people = job.people ?? [];
@@ -2972,11 +3860,11 @@ function permissionTools(job, tools) {
2972
3860
  }
2973
3861
  return {
2974
3862
  send: async (body) => {
2975
- const reply2 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
2976
- if (reply2.decision === "wait") {
3863
+ const reply3 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
3864
+ if (reply3.decision === "wait") {
2977
3865
  tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
2978
3866
  }
2979
- return reply2;
3867
+ return reply3;
2980
3868
  },
2981
3869
  check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
2982
3870
  sleep: tools.sleep
@@ -2995,21 +3883,46 @@ function knowledgeTools2(job, api) {
2995
3883
  read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
2996
3884
  };
2997
3885
  }
2998
- async function executeJob(job, tools, signal, input, record) {
2999
- const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
3886
+ function skillsLine(skills) {
3887
+ return `This role uses ${skills.length} skill(s) from the library: ${skills.join(", ")}.`;
3888
+ }
3889
+ async function prepare(job, tools) {
3890
+ if (isFolderJob(job)) {
3891
+ tools.print("Preparing the task folder\u2026");
3892
+ const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
3893
+ return { workspace: workspace2, results, identity: {} };
3894
+ }
3895
+ if (isLocalJob(job)) {
3896
+ tools.print("Preparing the code from the folders on this computer\u2026");
3897
+ const workspace2 = await prepareLocalWorkspace(job, tools);
3898
+ return { workspace: workspace2, results: null, identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
3899
+ }
3000
3900
  tools.print("Preparing the code\u2026");
3001
3901
  const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
3002
- const identity = await commitIdentityEnv(tools.home);
3902
+ return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
3903
+ }
3904
+ async function publish(workspace, job, api, identity) {
3905
+ return isLocalJob(job) ? await publishLocalWorkspace(workspace, job, identity) : await publishWorkspace(workspace, job, await gitToken(api, job), identity);
3906
+ }
3907
+ async function executeJob(job, tools, signal, input, record) {
3908
+ const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
3909
+ const { workspace, results, identity } = await prepare(job, tools);
3910
+ const skills = allowedSkills(job.stage);
3911
+ if (skills) {
3912
+ tools.print(skillsLine(skills));
3913
+ }
3914
+ const library = await prepareLibrary(roleLibraryFolder(tools.home, job.runId), skills);
3003
3915
  signal.throwIfAborted();
3004
3916
  tools.print("Claude is working\u2026");
3005
3917
  const attached = { count: 0 };
3006
3918
  const session = sessionReporter(tools.api, job, tools.print);
3007
3919
  const recordings = recordingsFolder(tools.home, job.runId);
3008
- const baseEnv = withNodeOnPath(process4.env, dirname5(process4.execPath));
3920
+ const baseEnv = withNodeOnPath(process6.env, dirname5(process6.execPath));
3009
3921
  const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
3010
3922
  const outcome = await tools.runAgent({
3011
3923
  cwd: workspace.root,
3012
- prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder) }),
3924
+ library,
3925
+ prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder), results }),
3013
3926
  input,
3014
3927
  env: { ...baseEnv, ...tools.browser ? browserEnv(tools.browser) : {}, ...recording, ...identity },
3015
3928
  signal,
@@ -3024,13 +3937,15 @@ async function executeJob(job, tools, signal, input, record) {
3024
3937
  effort: job.effort,
3025
3938
  onFeed: record,
3026
3939
  onSession: session.report,
3027
- attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request)
3940
+ attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
3941
+ results: resultTools(job, tools.api, workspace.root),
3942
+ handOver: handOverTools(job, tools.api)
3028
3943
  });
3029
3944
  await session.sent();
3030
3945
  signal.throwIfAborted();
3031
3946
  const waitingFor = outcome.waitingFor ?? null;
3032
- const shouldPublish = job.stage.kind === "code" && outcome.completed && waitingFor === null;
3033
- const pushed = shouldPublish ? await publishWorkspace(workspace, job, await gitToken(tools.api, job), identity) : [];
3947
+ const shouldPublish = job.stage.kind === "code" && !isFolderJob(job) && !isReadOnlyCode(job.stage) && outcome.completed && waitingFor === null;
3948
+ const pushed = shouldPublish ? await publish(workspace, job, tools.api, identity) : [];
3034
3949
  return {
3035
3950
  outcome: outcome.completed ? "succeeded" : "failed",
3036
3951
  report: outcome.report,
@@ -3050,7 +3965,7 @@ function clockOf(iso) {
3050
3965
  const time = new Date(iso);
3051
3966
  return `${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
3052
3967
  }
3053
- function resultLine(finish2) {
3968
+ function resultLine(finish2, workspace = "repositories") {
3054
3969
  if (finish2.pausedUntil) {
3055
3970
  return `Paused: ${USAGE_LIMIT_REPORT}, resumes at ${clockOf(finish2.pausedUntil)}`;
3056
3971
  }
@@ -3069,7 +3984,16 @@ function resultLine(finish2) {
3069
3984
  if (finish2.spec) {
3070
3985
  return "Done. The spec waits for the owner on TAMsense.";
3071
3986
  }
3072
- return finish2.outcome === "succeeded" ? `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.` : `Failed: ${finish2.report}`;
3987
+ if (finish2.outcome !== "succeeded") {
3988
+ return `Failed: ${finish2.report}`;
3989
+ }
3990
+ if (workspace === "folder") {
3991
+ return "Done. The result is on the task on TAMsense.";
3992
+ }
3993
+ if (workspace === "local") {
3994
+ return finish2.pushed.length > 0 ? `Done. ${finish2.pushed.map(localBranchLine).join(" ")}` : "Done. Nothing new to commit.";
3995
+ }
3996
+ return `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.`;
3073
3997
  }
3074
3998
  async function sendRecordings(job, tools) {
3075
3999
  const folder = recordingsFolder(tools.home, job.runId);
@@ -3085,7 +4009,7 @@ async function sendRecordings(job, tools) {
3085
4009
  tools.print(`Could not attach a recording: ${line}`);
3086
4010
  }
3087
4011
  } finally {
3088
- await rm4(folder, { recursive: true, force: true });
4012
+ await rm6(folder, { recursive: true, force: true });
3089
4013
  }
3090
4014
  }
3091
4015
  function failureReport(error, signal) {
@@ -3113,6 +4037,7 @@ async function runJob(job, tools, stop) {
3113
4037
  } catch (error) {
3114
4038
  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
4039
  } finally {
4040
+ await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
3116
4041
  clearInterval(polling);
3117
4042
  clearInterval(feeding);
3118
4043
  stop.removeEventListener("abort", stopRun);
@@ -3124,14 +4049,14 @@ async function runJob(job, tools, stop) {
3124
4049
  }
3125
4050
  await feed.flush();
3126
4051
  if (run2.signal.reason === CLOSED_ON_SITE) {
3127
- await rm4(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
4052
+ await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
3128
4053
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
3129
4054
  return;
3130
4055
  }
3131
4056
  await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
3132
4057
  try {
3133
4058
  await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
3134
- tools.print(resultLine(finish2));
4059
+ tools.print(resultLine(finish2, job.workspace));
3135
4060
  } catch (error) {
3136
4061
  tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
3137
4062
  }
@@ -3169,6 +4094,7 @@ async function work(tools, stop, parallel = 1) {
3169
4094
  const running = /* @__PURE__ */ new Set();
3170
4095
  const folderLock = keyedLock();
3171
4096
  const reporting = reportCapabilities(tools, stopAll);
4097
+ const checking = tools.inspectFolder ? checkFolders({ ...tools, inspect: tools.inspectFolder }, stopAll) : Promise.resolve();
3172
4098
  const start = (job) => {
3173
4099
  const runTools = jobTools(job, tools, parallel);
3174
4100
  const task = folderLock.run(stepFolder(job).join("/"), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
@@ -3180,9 +4106,9 @@ async function work(tools, stop, parallel = 1) {
3180
4106
  await Promise.race(running);
3181
4107
  continue;
3182
4108
  }
3183
- let reply2;
4109
+ let reply3;
3184
4110
  try {
3185
- reply2 = await tools.api.request("POST", NEXT_RUN_PATH);
4111
+ reply3 = await tools.api.request("POST", NEXT_RUN_PATH);
3186
4112
  } catch (error) {
3187
4113
  if (isFatal(error)) {
3188
4114
  halt.abort();
@@ -3192,14 +4118,14 @@ async function work(tools, stop, parallel = 1) {
3192
4118
  await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
3193
4119
  continue;
3194
4120
  }
3195
- if (reply2.job) {
3196
- start(reply2.job);
4121
+ if (reply3.job) {
4122
+ start(reply3.job);
3197
4123
  } else {
3198
4124
  await tools.sleep(POLL_INTERVAL_MS, stopAll);
3199
4125
  }
3200
4126
  }
3201
4127
  } finally {
3202
- await Promise.allSettled([...running, reporting]);
4128
+ await Promise.allSettled([...running, reporting, checking]);
3203
4129
  }
3204
4130
  }
3205
4131
 
@@ -3210,11 +4136,11 @@ var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the
3210
4136
  var ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
3211
4137
  var INTERRUPTED_EXIT_CODE = 130;
3212
4138
  function print(line) {
3213
- process5.stdout.write(`${line}
4139
+ process7.stdout.write(`${line}
3214
4140
  `);
3215
4141
  }
3216
4142
  function serverFor(flag, saved) {
3217
- return serverOrigin(flag ?? process5.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
4143
+ return serverOrigin(flag ?? process7.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
3218
4144
  }
3219
4145
  function accessProblem(error) {
3220
4146
  return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
@@ -3240,15 +4166,15 @@ async function runLogin(server, deviceName) {
3240
4166
  }
3241
4167
  function stopOnInterrupt() {
3242
4168
  const stop = new AbortController();
3243
- process5.once("SIGINT", () => {
4169
+ process7.once("SIGINT", () => {
3244
4170
  print("Stopping\u2026 Press Ctrl+C again to quit at once.");
3245
4171
  stop.abort();
3246
- process5.once("SIGINT", () => process5.exit(INTERRUPTED_EXIT_CODE));
4172
+ process7.once("SIGINT", () => process7.exit(INTERRUPTED_EXIT_CODE));
3247
4173
  });
3248
4174
  return stop.signal;
3249
4175
  }
3250
4176
  function installTools(home) {
3251
- return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists: existsSync4 };
4177
+ return { home, nodePath: process7.execPath, env: process7.env, print, run: runProcess, exists: existsSync7 };
3252
4178
  }
3253
4179
  async function browserForStart(home, install) {
3254
4180
  const installed = await installedBrowser(home);
@@ -3258,13 +4184,13 @@ async function browserForStart(home, install) {
3258
4184
  return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
3259
4185
  }
3260
4186
  async function startedNewVersion() {
3261
- const update = await updateToRun(process5.env, currentVersion(), () => latestVersion());
4187
+ const update = await updateToRun(process7.env, currentVersion(), () => latestVersion());
3262
4188
  if (!update) {
3263
4189
  return false;
3264
4190
  }
3265
4191
  print(`TAMsense runner ${update.latest} is out, this is ${update.current}. Starting the new version...`);
3266
4192
  try {
3267
- process5.exitCode = await relaunch(process5.argv.slice(2), process5.env);
4193
+ process7.exitCode = await relaunch(process7.argv.slice(2), process7.env);
3268
4194
  return true;
3269
4195
  } catch {
3270
4196
  print("Could not start the new version. Going on with this one.");
@@ -3282,14 +4208,14 @@ async function runStart(parallel, installsBrowser) {
3282
4208
  }
3283
4209
  const api = createApi(saved.server, saved.token);
3284
4210
  const identity = await api.request("GET", IDENTITY_PATH);
3285
- const problem = teamKeyProblem(identity.device.team, process5.env);
3286
- if (problem) {
3287
- throw new Error(problem);
4211
+ const problem2 = teamKeyProblem(identity.device.team, process7.env);
4212
+ if (problem2) {
4213
+ throw new Error(problem2);
3288
4214
  }
3289
4215
  print(`${identity.device.name} is connected to ${saved.server} as ${describeDevice(identity)}.`);
3290
4216
  const home = homedir3();
3291
4217
  const browser = await browserForStart(home, installsBrowser);
3292
- await work({ api, runAgent: runClaude, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder() }, stopOnInterrupt(), parallel);
4218
+ await work({ api, runAgent: runClaude, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home) }, stopOnInterrupt(), parallel);
3293
4219
  }
3294
4220
  async function runBrowser(remove) {
3295
4221
  const home = homedir3();
@@ -3341,10 +4267,10 @@ async function run(command) {
3341
4267
  }
3342
4268
  }
3343
4269
  try {
3344
- await run(parseCommand(process5.argv.slice(2), process5.env));
4270
+ await run(parseCommand(process7.argv.slice(2), process7.env));
3345
4271
  } catch (error) {
3346
- process5.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
4272
+ process7.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
3347
4273
  `);
3348
- process5.exitCode = 1;
4274
+ process7.exitCode = 1;
3349
4275
  }
3350
4276
  //# sourceMappingURL=cli.js.map