@tamsensedev/runner 0.13.0 → 0.19.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.
Files changed (82) hide show
  1. package/LICENSE +5 -5
  2. package/README.md +76 -57
  3. package/dist/cli.js +757 -379
  4. package/dist/cli.js.map +1 -1
  5. package/dist/recorder.cjs.map +1 -1
  6. package/library/.claude-plugin/plugin.json +5 -5
  7. package/library/THIRD_PARTY_NOTICES.md +123 -123
  8. package/library/agents/code-architect.md +67 -67
  9. package/library/agents/code-explorer.md +61 -61
  10. package/library/agents/code-reviewer.md +73 -73
  11. package/library/agents/code-simplifier.md +62 -62
  12. package/library/agents/design-critic.md +52 -52
  13. package/library/agents/second-opinion.md +64 -64
  14. package/library/agents/security-auditor.md +86 -86
  15. package/library/agents/silent-failure-hunter.md +70 -70
  16. package/library/agents/test-coverage-analyzer.md +78 -78
  17. package/library/agents/test-runner.md +48 -48
  18. package/library/agents/ui-tester.md +50 -50
  19. package/library/licenses/Apache-2.0.txt +202 -202
  20. package/library/skills/accessibility/SKILL.md +144 -144
  21. package/library/skills/accessibility/references/wcag-checks.md +168 -168
  22. package/library/skills/addressing-review-feedback/SKILL.md +109 -109
  23. package/library/skills/app-screens/SKILL.md +96 -96
  24. package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
  25. package/library/skills/app-screens/references/states-density-phone.md +54 -54
  26. package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
  27. package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
  28. package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
  29. package/library/skills/clean-commits/SKILL.md +85 -85
  30. package/library/skills/design-review/SKILL.md +107 -107
  31. package/library/skills/design-review/references/critique-passes.md +101 -101
  32. package/library/skills/design-review/references/screenshot-procedure.md +121 -121
  33. package/library/skills/differential-security-review/LICENSE +427 -427
  34. package/library/skills/differential-security-review/SKILL.md +105 -105
  35. package/library/skills/differential-security-review/references/adversarial.md +115 -115
  36. package/library/skills/differential-security-review/references/methodology.md +145 -145
  37. package/library/skills/differential-security-review/references/patterns.md +108 -108
  38. package/library/skills/false-positive-check/LICENSE +427 -427
  39. package/library/skills/false-positive-check/SKILL.md +95 -95
  40. package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
  41. package/library/skills/false-positive-check/references/deep-verification.md +113 -113
  42. package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
  43. package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
  44. package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
  45. package/library/skills/false-positive-check/references/standard-verification.md +80 -80
  46. package/library/skills/focused-implementation/SKILL.md +104 -104
  47. package/library/skills/implementation-plan/SKILL.md +66 -66
  48. package/library/skills/interface-design/SKILL.md +144 -144
  49. package/library/skills/interface-design/references/craft-defaults.md +130 -130
  50. package/library/skills/interface-design/references/new-direction.md +62 -62
  51. package/library/skills/interface-design/references/writing.md +52 -52
  52. package/library/skills/interface-polish/SKILL.md +103 -103
  53. package/library/skills/interface-polish/references/detail-rules.md +154 -154
  54. package/library/skills/interface-polish/references/worst-case-data.md +158 -158
  55. package/library/skills/motion-design/SKILL.md +106 -106
  56. package/library/skills/motion-design/references/recipes.md +243 -243
  57. package/library/skills/postgres-performance/SKILL.md +97 -97
  58. package/library/skills/postgres-performance/references/rules.md +265 -265
  59. package/library/skills/project-orientation/SKILL.md +77 -77
  60. package/library/skills/reviewing-changes/SKILL.md +183 -183
  61. package/library/skills/safe-database-changes/SKILL.md +111 -111
  62. package/library/skills/safe-database-changes/references/backfills.md +75 -75
  63. package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
  64. package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
  65. package/library/skills/security-review/SKILL.md +132 -132
  66. package/library/skills/security-review/references/checks-by-area.md +107 -107
  67. package/library/skills/security-review/references/do-not-report.md +86 -86
  68. package/library/skills/spec-writing/SKILL.md +144 -144
  69. package/library/skills/systematic-debugging/SKILL.md +130 -130
  70. package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
  71. package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
  72. package/library/skills/task-interview/SKILL.md +84 -84
  73. package/library/skills/test-driven-development/SKILL.md +116 -116
  74. package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
  75. package/library/skills/ui-testing/SKILL.md +102 -102
  76. package/library/skills/ui-testing/references/headless-check.md +295 -295
  77. package/library/skills/ui-testing/references/local-stand.md +51 -51
  78. package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
  79. package/library/skills/ui-testing/references/recording.md +61 -61
  80. package/library/skills/verification-before-completion/SKILL.md +113 -113
  81. package/library/skills/web-interface-guidelines/SKILL.md +202 -202
  82. package/package.json +46 -46
package/dist/cli.js CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { existsSync as existsSync8 } from "fs";
5
- import { homedir as homedir3 } from "os";
6
- import process7 from "process";
5
+ import { homedir as homedir2 } from "os";
6
+ import process8 from "process";
7
7
 
8
8
  // src/api.ts
9
9
  import { setTimeout as wait } from "timers/promises";
@@ -157,9 +157,9 @@ function textOf(value) {
157
157
  return typeof value === "string" && value.trim() ? value : null;
158
158
  }
159
159
  async function errorOf(response) {
160
- const reply4 = await response.json().catch(() => ({}));
161
- const code = textOf(reply4.data?.code);
162
- const message = textOf(reply4.message) ?? textOf(reply4.statusMessage) ?? `TAMsense answered with status ${response.status}`;
160
+ const reply5 = await response.json().catch(() => ({}));
161
+ const code = textOf(reply5.data?.code);
162
+ const message = textOf(reply5.message) ?? textOf(reply5.statusMessage) ?? `TAMsense answered with status ${response.status}`;
163
163
  return new ApiError(response.status, code, message);
164
164
  }
165
165
  function isFailure(outcome) {
@@ -252,6 +252,7 @@ var START_COMMAND = "npx @tamsensedev/runner@latest start";
252
252
  var BROWSER_COMMAND = "npx @tamsensedev/runner@latest browser";
253
253
  var DEFAULT_PARALLEL = 2;
254
254
  var MAX_PARALLEL = 4;
255
+ var PROFILE_NAME = /^[a-z0-9][\w-]{0,31}$/i;
255
256
  var HELP_TEXT = `Connects this computer to TAMsense so it can work on your tasks.
256
257
 
257
258
  Usage:
@@ -263,6 +264,12 @@ Usage:
263
264
  npx @tamsensedev/runner@latest status show the connection
264
265
  npx @tamsensedev/runner@latest logout disconnect this computer
265
266
 
267
+ Profiles, for several projects or accounts on one computer:
268
+ -p, --profile <name> use a separate connection with its own folders, for example:
269
+ ${LOGIN_COMMAND} -p client-a
270
+ ${START_COMMAND} -p client-a
271
+ Works with login, start, status and logout. Without it, the main connection is used.
272
+
266
273
  Login options:
267
274
  --name <name> how this computer is called in TAMsense (default: its network name)
268
275
  --server <url> TAMsense address (default: ${DEFAULT_SERVER})
@@ -270,6 +277,18 @@ Login options:
270
277
  Start options:
271
278
  --parallel <n> how many tasks to work on at once, 1 to ${MAX_PARALLEL} (default: ${DEFAULT_PARALLEL}, or TAMSENSE_PARALLEL)
272
279
  --no-browser do not install the headless browser for interface checks (start installs it when it is missing)`;
280
+ function profileFor(flag) {
281
+ if (flag === void 0) {
282
+ return null;
283
+ }
284
+ if (!PROFILE_NAME.test(flag)) {
285
+ throw new Error(`The profile name may have letters, digits, - and _, up to 32 characters, got "${flag}".`);
286
+ }
287
+ return flag.toLowerCase();
288
+ }
289
+ function withProfile(command, profile) {
290
+ return profile === null ? command : `${command} -p ${profile}`;
291
+ }
273
292
  function parallelFor(flag, env) {
274
293
  const [source, value] = flag !== void 0 ? ["--parallel", flag] : ["TAMSENSE_PARALLEL", env?.trim() || void 0];
275
294
  if (value === void 0) {
@@ -287,6 +306,8 @@ function parseCommand(argv, env = {}) {
287
306
  allowPositionals: true,
288
307
  strict: true,
289
308
  options: {
309
+ "profile": { type: "string", short: "p" },
310
+ "p": { type: "string" },
290
311
  "server": { type: "string" },
291
312
  "name": { type: "string" },
292
313
  "parallel": { type: "string" },
@@ -304,14 +325,15 @@ function parseCommand(argv, env = {}) {
304
325
  if (positionals.length !== 1) {
305
326
  return { name: "help" };
306
327
  }
328
+ const profile = profileFor(values.profile ?? values.p);
307
329
  if (name === "login") {
308
- return { name: "login", server: values.server ?? null, deviceName: values.name ?? null };
330
+ return { name: "login", profile, server: values.server ?? null, deviceName: values.name ?? null };
309
331
  }
310
332
  if (name === "start") {
311
- return { name: "start", parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL), browser: !values["no-browser"] };
333
+ return { name: "start", profile, parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL), browser: !values["no-browser"] };
312
334
  }
313
335
  if (name === "status" || name === "logout") {
314
- return { name };
336
+ return { name, profile };
315
337
  }
316
338
  return { name: "help" };
317
339
  }
@@ -517,8 +539,8 @@ async function probeCapabilities(stop) {
517
539
  }
518
540
 
519
541
  // src/claude.ts
520
- import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
521
- import { z as z5 } from "zod";
542
+ import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
543
+ import { z as z7 } from "zod";
522
544
 
523
545
  // src/askPerson.ts
524
546
  var MAX_ASK_TRIES = 5;
@@ -578,21 +600,21 @@ function shorten(text2) {
578
600
  const line = text2.replace(/\s+/g, " ").trim();
579
601
  return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
580
602
  }
581
- function activityLine(tool6, input) {
603
+ function activityLine(tool8, input) {
582
604
  const fields = typeof input === "object" && input !== null ? input : {};
583
- if (tool6 === "Bash" && typeof fields.command === "string") {
605
+ if (tool8 === "Bash" && typeof fields.command === "string") {
584
606
  return `$ ${shorten(fields.command)}`;
585
607
  }
586
- if (FILE_TOOLS.has(tool6) && typeof fields.file_path === "string") {
587
- return `${tool6} ${shorten(fields.file_path)}`;
608
+ if (FILE_TOOLS.has(tool8) && typeof fields.file_path === "string") {
609
+ return `${tool8} ${shorten(fields.file_path)}`;
588
610
  }
589
- if (tool6 === "Skill" && typeof fields.skill === "string") {
611
+ if (tool8 === "Skill" && typeof fields.skill === "string") {
590
612
  return `Skill ${shorten(fields.skill)}`;
591
613
  }
592
- if (HELPER_TOOLS.has(tool6) && typeof fields.subagent_type === "string") {
614
+ if (HELPER_TOOLS.has(tool8) && typeof fields.subagent_type === "string") {
593
615
  return `Agent ${shorten(fields.subagent_type)}`;
594
616
  }
595
- return tool6;
617
+ return tool8;
596
618
  }
597
619
  function usageOf(modelUsage) {
598
620
  const entries = Object.values(modelUsage);
@@ -650,6 +672,12 @@ function recordingUploadPath(runId, upload, part) {
650
672
  function checkPath(checkId, action) {
651
673
  return `/api/runner/checks/${checkId}/${action}`;
652
674
  }
675
+ function routinePath(checkId, action) {
676
+ return `/api/runner/checks/${checkId}/${action}`;
677
+ }
678
+ function sensePath(senseId, action) {
679
+ return `/api/runner/sense/${senseId}/${action}`;
680
+ }
653
681
 
654
682
  // src/feed.ts
655
683
  var MAX_TEXT_LENGTH = 8e3;
@@ -670,31 +698,52 @@ function textField(fields, name) {
670
698
  return typeof value === "string" && value.trim() ? value : null;
671
699
  }
672
700
  function toolDraft(block, parent) {
673
- const tool6 = (block.name ?? "tool").replace(MCP_PREFIX, "");
701
+ const tool8 = (block.name ?? "tool").replace(MCP_PREFIX, "");
674
702
  const fields = fieldsOf(block.input);
675
- const base = { tool: tool6, ref: block.id ?? null, parent, failed: false };
676
- if (tool6 === "Skill" && textField(fields, "skill")) {
703
+ const base = { tool: tool8, ref: block.id ?? null, parent, failed: false };
704
+ if (tool8 === "Skill" && textField(fields, "skill")) {
677
705
  return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
678
706
  }
679
- if (HELPER_TOOLS2.has(tool6)) {
707
+ if (HELPER_TOOLS2.has(tool8)) {
680
708
  const who = textField(fields, "subagent_type") ?? "general-purpose";
681
709
  const what = textField(fields, "description");
682
710
  return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
683
711
  }
684
- if (tool6 === "submit_verdict") {
712
+ if (tool8 === "submit_verdict") {
685
713
  const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
686
714
  return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
687
715
  }
688
- if (tool6 === "hand_off") {
716
+ if (tool8 === "hand_off") {
689
717
  return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
690
718
  }
691
- if (tool6 in ATTACH_LABELS) {
692
- return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool6]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
719
+ if (tool8 in ATTACH_LABELS) {
720
+ return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool8]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
693
721
  }
694
722
  const command = textField(fields, "command");
695
- const summary = tool6 === "Bash" && command ? `$ ${command}` : activityLine(tool6, block.input);
723
+ const summary = tool8 === "Bash" && command ? `$ ${command}` : activityLine(tool8, block.input);
696
724
  return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
697
725
  }
726
+ function sourceCalls(tools = []) {
727
+ return { tools: new Set(tools.map((tool8) => tool8.name)), refs: /* @__PURE__ */ new Set() };
728
+ }
729
+ function sourceCallDraft(block, draft) {
730
+ return { ...draft, text: clip(`${draft.tool} ${JSON.stringify(block.input ?? {})}`, MAX_TEXT_LENGTH) };
731
+ }
732
+ function fromSource(calls, block) {
733
+ return block.tool_use_id !== void 0 && calls.refs.has(block.tool_use_id);
734
+ }
735
+ function sourceSummary(text2) {
736
+ let reply5;
737
+ try {
738
+ reply5 = fieldsOf(JSON.parse(text2));
739
+ } catch {
740
+ return JSON.stringify({ ok: false });
741
+ }
742
+ if (reply5.ok === false) {
743
+ return JSON.stringify(reply5.refused === true ? { ok: false, refused: true, error: reply5.error } : { ok: false });
744
+ }
745
+ return JSON.stringify(typeof reply5.rowCount === "number" ? { ok: true, rowCount: reply5.rowCount } : { ok: true });
746
+ }
698
747
  function resultText(content) {
699
748
  if (typeof content === "string") {
700
749
  return content;
@@ -708,7 +757,7 @@ function helperEndDraft(ref, parent, end, text2) {
708
757
  const stopped = end === "stopped";
709
758
  return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
710
759
  }
711
- function feedOf(message) {
760
+ function feedOf(message, calls = sourceCalls()) {
712
761
  if (message.type !== "assistant" && message.type !== "user") {
713
762
  return [];
714
763
  }
@@ -724,9 +773,15 @@ function feedOf(message) {
724
773
  } else if (message.type === "assistant" && block.type === "text" && block.text?.trim()) {
725
774
  drafts.push({ kind: "text", text: clip(block.text, MAX_TEXT_LENGTH), tool: null, ref: null, parent, failed: false });
726
775
  } else if (message.type === "assistant" && block.type === "tool_use") {
727
- drafts.push(toolDraft(block, parent));
776
+ const draft = toolDraft(block, parent);
777
+ const isSource = draft.tool !== null && calls.tools.has(draft.tool);
778
+ if (isSource && block.id) {
779
+ calls.refs.add(block.id);
780
+ }
781
+ drafts.push(isSource ? sourceCallDraft(block, draft) : draft);
728
782
  } else if (message.type === "user" && block.type === "tool_result") {
729
- drafts.push({ kind: "result", text: clip(resultText(block.content), MAX_RESULT_LENGTH), tool: null, ref: block.tool_use_id ?? null, parent, failed: block.is_error === true });
783
+ const text2 = resultText(block.content);
784
+ drafts.push({ kind: "result", text: clip(fromSource(calls, block) ? sourceSummary(text2) : text2, MAX_RESULT_LENGTH), tool: null, ref: block.tool_use_id ?? null, parent, failed: block.is_error === true });
730
785
  }
731
786
  }
732
787
  return drafts;
@@ -1231,8 +1286,8 @@ function messageOf2(error) {
1231
1286
  async function decisionOf(tools, first, signal) {
1232
1287
  const now = tools.now ?? Date.now;
1233
1288
  const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
1234
- let reply4 = first;
1235
- while (reply4.decision === "wait") {
1289
+ let reply5 = first;
1290
+ while (reply5.decision === "wait") {
1236
1291
  await tools.sleep(CHECK_INTERVAL_MS, signal);
1237
1292
  if (signal.aborted) {
1238
1293
  return STOPPED_TEXT2;
@@ -1240,9 +1295,9 @@ async function decisionOf(tools, first, signal) {
1240
1295
  if (now() > giveUpAt) {
1241
1296
  return SITE_SILENT_TEXT;
1242
1297
  }
1243
- reply4 = await tools.check(reply4.id).catch(() => reply4);
1298
+ reply5 = await tools.check(reply5.id).catch(() => reply5);
1244
1299
  }
1245
- return reply4;
1300
+ return reply5;
1246
1301
  }
1247
1302
  function askPermission(tools, lastWords, folder) {
1248
1303
  return async (toolName, input, options) => {
@@ -1263,11 +1318,11 @@ function askPermission(tools, lastWords, folder) {
1263
1318
  } catch (error) {
1264
1319
  return deny(`Could not ask for permission on TAMsense: ${messageOf2(error)}. Do it another way, or explain in your report why it is needed.`);
1265
1320
  }
1266
- const reply4 = await decisionOf(tools, first, options.signal);
1267
- if (typeof reply4 === "string") {
1268
- return deny(reply4);
1321
+ const reply5 = await decisionOf(tools, first, options.signal);
1322
+ if (typeof reply5 === "string") {
1323
+ return deny(reply5);
1269
1324
  }
1270
- return reply4.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply4.message);
1325
+ return reply5.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply5.message);
1271
1326
  };
1272
1327
  }
1273
1328
  var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
@@ -1447,7 +1502,7 @@ async function checkFolders(tools, stop) {
1447
1502
  }
1448
1503
  }
1449
1504
  var SKIPPED_FOLDER = "node_modules";
1450
- var MORE_CODE_TEXT = "To add more code, start the program from your code folder or the folder above it, or add a path in Board settings > Code.";
1505
+ var MORE_CODE_TEXT = "To add more code, start the program from your code folder or the folder above it, or add a path in Settings, under your computer on the Account tab.";
1451
1506
  async function childRepositories(start) {
1452
1507
  const entries = await readdir(start, { withFileTypes: true }).catch(() => []);
1453
1508
  return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== SKIPPED_FOLDER).map((entry) => join2(start, entry.name)).filter((folder) => existsSync2(join2(folder, ".git"))).sort();
@@ -1515,13 +1570,14 @@ function keyedLock() {
1515
1570
  import { existsSync as existsSync3 } from "fs";
1516
1571
  import { rm as rm2 } from "fs/promises";
1517
1572
  import { join as join3, resolve as resolve2 } from "path";
1573
+ var RESULTS_FOLDER = "results";
1518
1574
  var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
1519
1575
  var cacheLock = keyedLock();
1520
1576
  function githubRemoteUrl(fullName) {
1521
1577
  return `https://github.com/${fullName}.git`;
1522
1578
  }
1523
- function tamsenseFolder(home, ...parts) {
1524
- return join3(home, ".tamsense", ...parts);
1579
+ function tamsenseFolder(dataFolder, ...parts) {
1580
+ return join3(dataFolder, ...parts);
1525
1581
  }
1526
1582
  function folderNames(repositories) {
1527
1583
  const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
@@ -1529,7 +1585,10 @@ function folderNames(repositories) {
1529
1585
  for (const repository of repositories) {
1530
1586
  counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
1531
1587
  }
1532
- return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
1588
+ return repositories.map((repository) => {
1589
+ const name = shortName(repository.fullName);
1590
+ return counts.get(name) === 1 && name !== RESULTS_FOLDER ? name : repository.fullName.replace("/", "__");
1591
+ });
1533
1592
  }
1534
1593
  async function refreshCache(cache, repository, token, tools) {
1535
1594
  if (existsSync3(join3(cache, ".git"))) {
@@ -1582,15 +1641,15 @@ async function keptBase(cache, path, job, repository) {
1582
1641
  }
1583
1642
  return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
1584
1643
  }
1585
- function taskFolder(home, job) {
1586
- return tamsenseFolder(home, "work", ...stepFolder(job));
1644
+ function taskFolder(dataFolder, job) {
1645
+ return tamsenseFolder(dataFolder, "work", ...stepFolder(job));
1587
1646
  }
1588
1647
  async function prepareWorkspace(job, token, tools) {
1589
- const root = taskFolder(tools.home, job);
1648
+ const root = taskFolder(tools.dataFolder, job);
1590
1649
  const folders = folderNames(job.repositories);
1591
1650
  const repositories = [];
1592
1651
  for (const [index, repository] of job.repositories.entries()) {
1593
- const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
1652
+ const cache = tamsenseFolder(tools.dataFolder, "repos", repository.fullName.replace("/", "__"));
1594
1653
  const folder = folders[index] ?? repository.fullName;
1595
1654
  const path = join3(root, folder);
1596
1655
  const baseSha = await cacheLock.run(repository.fullName, async () => {
@@ -1686,7 +1745,7 @@ async function baseBranchRef(source, branch, remote) {
1686
1745
  }
1687
1746
  const found = own ?? fetched;
1688
1747
  if (!found) {
1689
- throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in the board settings, on the Code tab.`);
1748
+ throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in Settings, under your computer on the Account tab.`);
1690
1749
  }
1691
1750
  return found;
1692
1751
  }
@@ -1699,15 +1758,15 @@ async function taskBranchRef(source, branch, remote, base) {
1699
1758
  async function startRef(source, job, local) {
1700
1759
  const base = await baseBranchRef(source, local.baseBranch, local.remote);
1701
1760
  const task = await taskBranchRef(source, job.branch, local.remote, base);
1702
- if (task || !isReadOnlyCheck(job)) {
1761
+ if (task || !isReadOnlyCheck(job) || job.severalBranches) {
1703
1762
  return task ?? base;
1704
1763
  }
1705
1764
  const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
1706
1765
  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.`);
1707
1766
  }
1708
- async function releaseBranch2(source, branch, keep, home) {
1767
+ async function releaseBranch2(source, branch, keep, dataFolder) {
1709
1768
  const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: source });
1710
- const work2 = tamsenseFolder(home, "work");
1769
+ const work2 = tamsenseFolder(dataFolder, "work");
1711
1770
  for (const holder of branchHolders(listed.stdout, branch).filter((path) => resolve3(path) !== resolve3(keep))) {
1712
1771
  if (!isInside(holder, work2)) {
1713
1772
  throw new Error(`Branch ${branch} is checked out in ${resolve3(holder)}. Switch that folder to another branch, then start the step again.`);
@@ -1734,7 +1793,7 @@ function sourceOf(repository) {
1734
1793
  async function prepareRepository(job, repository, path, tools) {
1735
1794
  const source = sourceOf(repository);
1736
1795
  if ((await runGit(["rev-parse", "--show-toplevel"], { cwd: source }).catch(() => null))?.code !== 0) {
1737
- 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.`);
1796
+ throw new Error(`${source} is no longer a git repository on this computer. Check the folder again in Settings, under your computer on the Account tab.`);
1738
1797
  }
1739
1798
  const local = { source, remote: await remoteOf(source), baseBranch: repository.defaultBranch };
1740
1799
  if (local.remote) {
@@ -1752,13 +1811,13 @@ async function prepareRepository(job, repository, path, tools) {
1752
1811
  if (readsOnly(job)) {
1753
1812
  await git(["worktree", "add", "--detach", path, start], { cwd: source });
1754
1813
  } else {
1755
- await releaseBranch2(source, job.branch, path, tools.home);
1814
+ await releaseBranch2(source, job.branch, path, tools.dataFolder);
1756
1815
  await git(["worktree", "add", "-B", job.branch, path, start], { cwd: source });
1757
1816
  }
1758
1817
  return { baseSha: await git(["rev-parse", "HEAD"], { cwd: path }), local };
1759
1818
  }
1760
1819
  async function prepareLocalWorkspace(job, tools) {
1761
- const root = taskFolder(tools.home, job);
1820
+ const root = taskFolder(tools.dataFolder, job);
1762
1821
  const folders = folderNames(job.repositories);
1763
1822
  const repositories = [];
1764
1823
  for (const [index, repository] of job.repositories.entries()) {
@@ -1793,8 +1852,8 @@ async function commitAndPush(repository, local, job, identity) {
1793
1852
  }
1794
1853
  const base = await baseBranchRef(repository.path, local.baseBranch, local.remote);
1795
1854
  const commits = Number(await git(["rev-list", "--count", `${base}..HEAD`], options));
1796
- const sent2 = await pushBranch(repository.path, local, job.branch);
1797
- return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent2 };
1855
+ const sent3 = await pushBranch(repository.path, local, job.branch);
1856
+ return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent3 };
1798
1857
  }
1799
1858
  async function publishLocalWorkspace(workspace, job, identity) {
1800
1859
  const pushed = [];
@@ -1830,7 +1889,6 @@ var SAVE_TABLE_TOOL_NAME = "save_result_table";
1830
1889
  var ATTACH_FILE_TOOL_NAME = "attach_result_file";
1831
1890
  var ADD_LINK_TOOL_NAME = "add_result_link";
1832
1891
  var HAND_OVER_TOOL_NAME = "hand_over";
1833
- var RESULTS_FOLDER = "results";
1834
1892
  var MAX_NAME_LENGTH = 60;
1835
1893
  var TYPES_BY_EXTENSION = {
1836
1894
  ".pdf": "application/pdf",
@@ -1997,11 +2055,11 @@ function resultToolset(tools) {
1997
2055
  function handOverTool(tools) {
1998
2056
  return tool2(
1999
2057
  HAND_OVER_TOOL_NAME,
2000
- `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.`,
2058
+ `Create a linked task on a board of the project, this board included: when part of the work belongs to another board, or when the work goes on as a new task here, such as the next wave after this one. Boards you may create tasks on: ${tools.boards.map((board) => board.name).join(", ")}. Without autopilot a person confirms it first.`,
2001
2059
  {
2002
2060
  board: z2.string().describe("name of the board"),
2003
- title: z2.string().describe("title of the new task on that board, one plain sentence"),
2004
- note: z2.string().describe("what you found and what that board should do; never names, emails or other personal data of clients"),
2061
+ title: z2.string().describe("title of the new task, one plain sentence"),
2062
+ note: z2.string().describe("what you found and what the new task should do; never names, emails or other personal data of clients"),
2005
2063
  wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
2006
2064
  },
2007
2065
  async (args) => await sent(() => tools.send(args))
@@ -2011,65 +2069,214 @@ function handOverTool(tools) {
2011
2069
  // src/schedules.ts
2012
2070
  import { existsSync as existsSync5 } from "fs";
2013
2071
  import { mkdir as mkdir3 } from "fs/promises";
2014
- import { createSdkMcpServer, query as query2, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2072
+ import { createSdkMcpServer, query as query2, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
2073
+ import { z as z5 } from "zod";
2074
+
2075
+ // src/routines.ts
2076
+ import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2015
2077
  import { z as z3 } from "zod";
2078
+ var FINISH_ROUTINE_TOOL_NAME = "finish_routine";
2079
+ var RULES = [
2080
+ "You run one run of a TAMsense routine on the user's own computer: a job that repeats on a schedule. Nobody answers during the run: decide yourself.",
2081
+ "This is the person's own computer, and their other programs may be running on it. Stop only the processes you started yourself, by their process id, and stop them before you finish. Never kill processes by name or pattern and never touch programs you did not start.",
2082
+ "One problem is one report_problem call. Use the same key for the same problem on every run, and pass the number of an open task of this routine as sameAs when it is the same problem.",
2083
+ `Finish by calling ${FINISH_ROUTINE_TOOL_NAME} exactly once, after every problem is reported.`,
2084
+ "Write in the language of the job. No em dashes and no en dashes."
2085
+ ];
2086
+ function reply2(text2) {
2087
+ return { content: [{ type: "text", text: text2 }] };
2088
+ }
2089
+ var KEY_FIELD = z3.string().describe('a short stable name of this problem, the same on every run, like "buyout-4812-card-declined"');
2090
+ async function sent2(tools, body) {
2091
+ try {
2092
+ return reply2((await tools.call(body)).text);
2093
+ } catch (error) {
2094
+ return { ...reply2(`Could not reach TAMsense: ${error instanceof Error ? error.message : String(error)}. Say so in your summary.`), isError: true };
2095
+ }
2096
+ }
2097
+ function routineToolset(tools, onFinish) {
2098
+ const problem2 = tool3("report_problem", "Hand one problem you do not fix to people: a task on the board of the routine, an addition to the open task of the same problem, or a line in the report.", {
2099
+ title: z3.string().describe("one plain sentence: what is wrong"),
2100
+ detail: z3.string().describe("the facts people need to act; never personal data of clients"),
2101
+ key: KEY_FIELD,
2102
+ sameAs: z3.number().int().nullable().describe("the number of an open task of this routine about the same problem, or null")
2103
+ }, async (args) => await sent2(tools, { tool: "problem", input: args }));
2104
+ const ask = tool3("ask_person", "Ask the person of this routine what to do with one thing you cannot decide. Do not wait for the answer: the next run reads it.", {
2105
+ question: z3.string(),
2106
+ context: z3.string().describe("the facts they need to answer without opening anything"),
2107
+ key: KEY_FIELD,
2108
+ options: z3.array(z3.string()).max(4).describe("up to four answers you suggest")
2109
+ }, async (args) => await sent2(tools, { tool: "ask", input: args }));
2110
+ const fixed = tool3("report_fixed", "Write down one fix you made in this run: what you changed and why.", {
2111
+ title: z3.string().describe('what you did, like "Cancelled buyout #4815 on Wildberries"'),
2112
+ detail: z3.string().describe("why, with the facts"),
2113
+ key: KEY_FIELD
2114
+ }, async (args) => await sent2(tools, { tool: "fixed", input: args }));
2115
+ const report = tool3("save_report", "Save the report of this run on TAMsense: a Markdown document or a CSV table with a header row.", {
2116
+ kind: z3.enum(["document", "table"]),
2117
+ title: z3.string(),
2118
+ content: z3.string().describe("the full Markdown text, or the full CSV")
2119
+ }, async (args) => await sent2(tools, { tool: "report", input: args }));
2120
+ const finish2 = tool3(FINISH_ROUTINE_TOOL_NAME, "Finish this run. Call it exactly once, at the very end.", {
2121
+ summary: z3.string().describe("one line with the numbers that matter"),
2122
+ fine: z3.number().int().min(0).describe("how many things you checked and found in order"),
2123
+ fineNote: z3.string().describe("what those are, in a few words")
2124
+ }, async (args) => {
2125
+ await sent2(tools, { tool: "fine", input: { count: args.fine, note: args.fineNote } });
2126
+ onFinish({ changed: false, summary: args.summary.trim(), note: null, conditionMet: false, callPerson: false });
2127
+ return reply2("Run finished. You can stop now.");
2128
+ });
2129
+ return [problem2, ...tools.asks ? [ask] : [], ...tools.canFix ? [fixed] : [], report, finish2];
2130
+ }
2131
+ function noteLine(note) {
2132
+ return `- ${note.at.slice(0, 16).replace("T", " ")}: ${note.summary}`;
2133
+ }
2134
+ function buildRoutinePrompt(job, folder) {
2135
+ const earlier = job.earlier.length > 0 ? `Earlier runs, the newest last:
2136
+ ${job.earlier.map(noteLine).join("\n")}` : null;
2137
+ return {
2138
+ prompt: [job.instruction, earlier].filter(Boolean).join("\n\n"),
2139
+ instructions: [RULES.join("\n"), workplaceText(job.computer, folder), job.sourcesText, job.safetyRules.trim() || null].filter(Boolean).join("\n\n")
2140
+ };
2141
+ }
2142
+ function routineRunTools(job, api, print2, sleep2) {
2143
+ const routine = job.routine;
2144
+ if (!routine) {
2145
+ return null;
2146
+ }
2147
+ const permissions = routine.canFix && !routine.dry ? {
2148
+ send: async (body) => {
2149
+ const answer = await api.request("POST", routinePath(job.checkId, "permissions"), body);
2150
+ if (answer.decision === "wait") {
2151
+ print2(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
2152
+ }
2153
+ return answer;
2154
+ },
2155
+ check: (id) => api.request("GET", routinePath(job.checkId, `permissions/${id}`)),
2156
+ sleep: sleep2
2157
+ } : null;
2158
+ return {
2159
+ call: (body) => api.request("POST", routinePath(job.checkId, "routine"), body),
2160
+ permissions,
2161
+ asks: routine.asks,
2162
+ canFix: routine.canFix
2163
+ };
2164
+ }
2165
+
2166
+ // src/sources.ts
2167
+ import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2168
+ import { z as z4 } from "zod";
2169
+ function sourceTools(tools, api, path) {
2170
+ return tools && tools.length > 0 ? { tools, call: (body) => api.request("POST", path, body) } : null;
2171
+ }
2172
+ function fieldSchema(field) {
2173
+ const value = field.type === "integer" ? z4.number().int().positive() : z4.string();
2174
+ return (field.required ? value : value.optional()).describe(field.description);
2175
+ }
2176
+ function inputShape(fields) {
2177
+ return Object.fromEntries(fields.map((field) => [field.name, fieldSchema(field)]));
2178
+ }
2179
+ function replyOf(answer) {
2180
+ return { content: [{ type: "text", text: JSON.stringify(answer) }], ...answer.ok ? {} : { isError: true } };
2181
+ }
2182
+ function sourceToolset(sources) {
2183
+ if (!sources) {
2184
+ return [];
2185
+ }
2186
+ return sources.tools.map((info) => tool4(
2187
+ info.name,
2188
+ info.description,
2189
+ inputShape(info.fields),
2190
+ async (input) => {
2191
+ try {
2192
+ return replyOf(await sources.call({ tool: info.name, input }));
2193
+ } catch (error) {
2194
+ return replyOf({ ok: false, error: `Could not read project data: ${error instanceof Error ? error.message : String(error)}` });
2195
+ }
2196
+ }
2197
+ ));
2198
+ }
2199
+
2200
+ // src/schedules.ts
2016
2201
  var PROPOSE_SCHEDULE_TOOL_NAME = "propose_schedule";
2017
2202
  var REPORT_CHECK_TOOL_NAME = "report_check";
2203
+ var WAKE_TOOL_NAME = "wake_up_later";
2018
2204
  var SCHEDULE_RULE = `Some waits are long: a local stand or a database copy that takes half an hour to come up, a slow build or import, a service that answers only after a while, logs that show a problem only over hours. Do not sleep or poll in a loop for that. Propose a schedule with the ${PROPOSE_SCHEDULE_TOOL_NAME} tool: a short check repeated on a timer by a cheap model that reads the notes of earlier runs and reports one line in the task chat. Give it a clear condition that ends it, the longest interval that still helps, and when to call a person. With wait true this run ends its turn and continues here when the schedule ends. Never use a schedule for work you can finish now, for a check you can do once, or to watch something "just in case". Without autopilot a person starts it first; on autopilot it starts at once within the project limits.`;
2019
2205
  var ALREADY_WAITING_TEXT2 = "This run already waits for something. End your turn now: the conversation continues when it comes.";
2020
- function reply2(text2, isError = false) {
2206
+ function reply3(text2, isError = false) {
2021
2207
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2022
2208
  }
2023
2209
  function proposeScheduleTool(tools, waiting) {
2024
- return tool3(
2210
+ return tool5(
2025
2211
  PROPOSE_SCHEDULE_TOOL_NAME,
2026
2212
  "Repeat a short check on a timer while the task waits for something slow. Each run is a short cheap run that reads the notes of earlier runs and reports one line in the task chat; it ends when its condition comes, its time runs out or it made its runs.",
2027
2213
  {
2028
- title: z3.string().describe('a short name, at most 80 characters: "Wait for the local stand"'),
2029
- instruction: z3.string().describe("what each run does, in plain words, as a note to yourself: what to look at and what counts as new"),
2030
- everyMinutes: z3.number().int().nullable().describe("repeat every N minutes (60 for every hour); null when dailyAt is set"),
2031
- dailyAt: z3.string().nullable().describe("repeat every day at this time, HH:MM in the project time zone; null when everyMinutes is set"),
2032
- days: z3.array(z3.number().int().min(0).max(6)).describe("with dailyAt: days of the week, 0 is Sunday; empty means every day"),
2033
- until: z3.string().describe('the condition each run checks, for example "the stand answers on localhost:3100"; when it comes, the schedule ends. Empty when it simply runs for its time'),
2034
- forMinutes: z3.number().int().nullable().describe("how long it runs at most, in minutes; null takes the longest the project allows"),
2035
- maxRuns: z3.number().int().nullable().describe("the number of runs at most, or null"),
2036
- where: z3.enum(["computer", "cloud"]).describe("computer: this computer, free, skipped while it sleeps; cloud: any time, for credits"),
2037
- callWhen: z3.string().describe('when to call a person, in plain words: "a container keeps restarting"'),
2038
- callOnTimeout: z3.boolean().describe("true: call a person when the condition did not come in time"),
2039
- keepAfterStep: z3.boolean().describe("true only when it must keep running after the task leaves this step"),
2040
- wait: z3.boolean().describe("true: this run ends its turn now and continues here when the schedule ends")
2214
+ title: z5.string().describe('a short name, at most 80 characters: "Wait for the local stand"'),
2215
+ instruction: z5.string().describe("what each run does, in plain words, as a note to yourself: what to look at and what counts as new"),
2216
+ everyMinutes: z5.number().int().nullable().describe("repeat every N minutes (60 for every hour); null when dailyAt is set"),
2217
+ dailyAt: z5.string().nullable().describe("repeat every day at this time, HH:MM in the project time zone; null when everyMinutes is set"),
2218
+ days: z5.array(z5.number().int().min(0).max(6)).describe("with dailyAt: days of the week, 0 is Sunday; empty means every day"),
2219
+ until: z5.string().describe('the condition each run checks, for example "the stand answers on localhost:3100"; when it comes, the schedule ends. Empty when it simply runs for its time'),
2220
+ forMinutes: z5.number().int().nullable().describe("how long it runs at most, in minutes; null takes the longest the project allows"),
2221
+ maxRuns: z5.number().int().nullable().describe("the number of runs at most, or null"),
2222
+ where: z5.enum(["computer", "cloud"]).describe("computer: this computer, free, skipped while it sleeps; cloud: any time, for credits"),
2223
+ callWhen: z5.string().describe('when to call a person, in plain words: "a container keeps restarting"'),
2224
+ callOnTimeout: z5.boolean().describe("true: call a person when the condition did not come in time"),
2225
+ keepAfterStep: z5.boolean().describe("true only when it must keep running after the task leaves this step"),
2226
+ wait: z5.boolean().describe("true: this run ends its turn now and continues here when the schedule ends")
2041
2227
  },
2042
2228
  async (args) => {
2043
2229
  if (args.wait && (waiting.waitingFor !== null || waiting.waitingForSchedule !== null)) {
2044
- return reply2(ALREADY_WAITING_TEXT2, true);
2230
+ return reply3(ALREADY_WAITING_TEXT2, true);
2045
2231
  }
2046
2232
  let answer;
2047
2233
  try {
2048
2234
  answer = await tools.send({ ...args, title: args.title.trim(), instruction: args.instruction.trim(), until: args.until.trim(), callWhen: args.callWhen.trim(), model: null });
2049
2235
  } catch (error) {
2050
- return reply2(`Could not send the schedule: ${error instanceof Error ? error.message : String(error)}. Wait another way or say so in your report.`, true);
2236
+ return reply3(`Could not send the schedule: ${error instanceof Error ? error.message : String(error)}. Wait another way or say so in your report.`, true);
2051
2237
  }
2052
2238
  if (answer.id === null) {
2053
- return reply2(answer.text, true);
2239
+ return reply3(answer.text, true);
2054
2240
  }
2055
2241
  if (answer.waits) {
2056
2242
  waiting.waitingForSchedule = answer.id;
2057
2243
  }
2058
- return reply2(answer.text);
2244
+ return reply3(answer.text);
2059
2245
  }
2060
2246
  );
2061
2247
  }
2062
- function scheduleTools(api, path) {
2063
- return { send: (body) => api.request("POST", path, body) };
2248
+ function scheduleTools(api, path, wakePath) {
2249
+ return {
2250
+ send: (body) => api.request("POST", path, body),
2251
+ wake: (body) => api.request("POST", wakePath, body)
2252
+ };
2253
+ }
2254
+ function wakeUpLaterTool(tools) {
2255
+ return tool5(
2256
+ WAKE_TOOL_NAME,
2257
+ "Put the task to sleep and wake it up later, once: for example 7 days after a mailing to collect its results, or 3 days after a follow-up. Nothing runs and nothing is charged while it sleeps. On waking the task moves to the step you name, along a rule of the board (a rule that only a person moves waits for that person), or this step starts again when you name none. A person moving the task meanwhile cancels the wake-up. Finish your run right after you set it.",
2258
+ {
2259
+ inHours: z5.number().min(0.1).max(2160).describe("how many hours from now, for example 168 for 7 days"),
2260
+ step: z5.string().describe("the name of the step to wake up on, empty to start this step again"),
2261
+ note: z5.string().describe("what to do on waking, in plain words")
2262
+ },
2263
+ async (args) => {
2264
+ try {
2265
+ return reply3((await tools.wake({ inHours: args.inHours, step: args.step.trim(), note: args.note.trim() })).text);
2266
+ } catch (error) {
2267
+ return reply3(`Could not set the wake-up: ${error instanceof Error ? error.message : String(error)}. Say in your report when the task should continue.`, true);
2268
+ }
2269
+ }
2270
+ );
2064
2271
  }
2065
- async function checkFolder(home, job) {
2272
+ async function checkFolder(dataFolder, job) {
2066
2273
  if (job.taskId !== null) {
2067
- const step = taskFolder(home, { taskId: job.taskId, stage: { columnId: job.stepColumnId } });
2274
+ const step = taskFolder(dataFolder, { taskId: job.taskId, stage: { columnId: job.stepColumnId } });
2068
2275
  if (existsSync5(step)) {
2069
2276
  return step;
2070
2277
  }
2071
2278
  }
2072
- const own = tamsenseFolder(home, "schedules", `schedule-${job.scheduleId}`);
2279
+ const own = tamsenseFolder(dataFolder, "schedules", `schedule-${job.scheduleId}`);
2073
2280
  await mkdir3(own, { recursive: true });
2074
2281
  return own;
2075
2282
  }
@@ -2081,7 +2288,7 @@ var CHECK_RULES = [
2081
2288
  `Finish by calling ${REPORT_CHECK_TOOL_NAME} exactly once: summary is one short line for the task chat with the numbers that matter; changed is true only when something differs from the last note; note is the full facts only when something changed; conditionMet is true when the condition of the schedule came; callPerson is true when the schedule says to call a person for what you saw.`,
2082
2289
  "Write in the language of the task. No em dashes and no en dashes."
2083
2290
  ];
2084
- function noteLine(note) {
2291
+ function noteLine2(note) {
2085
2292
  const head = `- ${note.at.slice(0, 16).replace("T", " ")} (${note.outcome}): ${note.summary}`;
2086
2293
  return note.note ? `${head}
2087
2294
  ${note.note.replace(/\n/g, "\n ")}` : head;
@@ -2094,43 +2301,56 @@ ${job.instruction}`,
2094
2301
  job.until ? `The schedule ends when: ${job.until}` : "The schedule runs for its time; there is no condition to wait for.",
2095
2302
  job.callWhen ? `Call a person when: ${job.callWhen}` : "Call a person only when something is clearly wrong.",
2096
2303
  job.earlier.length > 0 ? `Notes of the earlier runs, the newest last:
2097
- ${job.earlier.map(noteLine).join("\n")}` : "This is the first run: there are no earlier notes."
2304
+ ${job.earlier.map(noteLine2).join("\n")}` : "This is the first run: there are no earlier notes."
2098
2305
  ].join("\n\n");
2099
2306
  const instructions = [
2100
2307
  CHECK_RULES.join("\n"),
2101
2308
  workplaceText(job.computer, folder),
2309
+ job.sourcesText,
2102
2310
  job.safetyRules.trim() || null
2103
2311
  ].filter(Boolean).join("\n\n");
2104
2312
  return { prompt, instructions };
2105
2313
  }
2106
2314
  var SERVER_NAME = "tamsense";
2107
2315
  var MAX_CHECK_TURNS = 30;
2316
+ var MAX_ROUTINE_TURNS = 120;
2108
2317
  function reportCheckTool(onReport) {
2109
- return tool3(
2318
+ return tool5(
2110
2319
  REPORT_CHECK_TOOL_NAME,
2111
2320
  "Hand in the result of this check. Call it exactly once, at the very end.",
2112
2321
  {
2113
- changed: z3.boolean().describe("true when something changed since the earlier runs"),
2114
- summary: z3.string().describe("one short line for the task chat: what you saw"),
2115
- note: z3.string().nullable().describe("when something changed: the full facts, the exact lines and what they mean; null otherwise"),
2116
- conditionMet: z3.boolean().describe("true when the condition of the schedule came"),
2117
- callPerson: z3.boolean().describe("true when a person must look now, as the schedule says")
2322
+ changed: z5.boolean().describe("true when something changed since the earlier runs"),
2323
+ summary: z5.string().describe("one short line for the task chat: what you saw"),
2324
+ note: z5.string().nullable().describe("when something changed: the full facts, the exact lines and what they mean; null otherwise"),
2325
+ conditionMet: z5.boolean().describe("true when the condition of the schedule came"),
2326
+ callPerson: z5.boolean().describe("true when a person must look now, as the schedule says")
2118
2327
  },
2119
2328
  async (args) => {
2120
2329
  onReport({ changed: args.changed, summary: args.summary.trim(), note: args.note?.trim() || null, conditionMet: args.conditionMet, callPerson: args.callPerson });
2121
- return reply2("Result received. You can stop now.");
2330
+ return reply3("Result received. You can stop now.");
2122
2331
  }
2123
2332
  );
2124
2333
  }
2334
+ function textOf2(message) {
2335
+ if (message.type !== "assistant") {
2336
+ return null;
2337
+ }
2338
+ const text2 = message.message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").trim();
2339
+ return text2 || null;
2340
+ }
2125
2341
  function claudeCheckRunner(startQuery) {
2126
2342
  return async (request) => {
2127
2343
  request.signal.throwIfAborted();
2128
2344
  const abortController = new AbortController();
2129
2345
  request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
2130
2346
  let result = null;
2131
- const report = reportCheckTool((found) => {
2132
- result = found;
2133
- });
2347
+ const found = (input) => {
2348
+ result = input;
2349
+ };
2350
+ const own = request.routine ? routineToolset(request.routine, found) : [reportCheckTool(found)];
2351
+ const tools = [...own, ...sourceToolset(request.sources)];
2352
+ const lastWords = { text: "" };
2353
+ const permissions = request.routine?.permissions ?? null;
2134
2354
  const messages = startQuery({
2135
2355
  prompt: request.prompt.prompt,
2136
2356
  options: {
@@ -2138,10 +2358,10 @@ function claudeCheckRunner(startQuery) {
2138
2358
  systemPrompt: { type: "preset", preset: "claude_code", append: request.prompt.instructions },
2139
2359
  settingSources: ["user", "project", "local"],
2140
2360
  permissionMode: "auto",
2141
- canUseTool: denyWhenAsked,
2142
- mcpServers: { [SERVER_NAME]: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools: [report] }) },
2143
- allowedTools: [`mcp__${SERVER_NAME}__${REPORT_CHECK_TOOL_NAME}`],
2144
- maxTurns: MAX_CHECK_TURNS,
2361
+ canUseTool: permissions ? askPermission(permissions, () => lastWords.text, request.cwd) : denyWhenAsked,
2362
+ mcpServers: { [SERVER_NAME]: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }) },
2363
+ allowedTools: tools.map((item) => `mcp__${SERVER_NAME}__${item.name}`),
2364
+ maxTurns: request.routine ? MAX_ROUTINE_TURNS : MAX_CHECK_TURNS,
2145
2365
  abortController,
2146
2366
  env: request.env,
2147
2367
  ...request.model === null ? {} : { model: request.model }
@@ -2149,6 +2369,7 @@ function claudeCheckRunner(startQuery) {
2149
2369
  });
2150
2370
  let outcome = { completed: false, report: "Claude finished without a result.", model: null, usage: null, result: null };
2151
2371
  for await (const message of messages) {
2372
+ lastWords.text = textOf2(message) ?? lastWords.text;
2152
2373
  if (message.type === "result") {
2153
2374
  const finished = outcomeOf(message, "");
2154
2375
  outcome = { completed: finished.completed, report: finished.report, model: finished.model, usage: finished.usage, result: null };
@@ -2322,7 +2543,7 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
2322
2543
  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".';
2323
2544
  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, { notes }), where notes say in your own words what you tested in that browser and what happened: what you opened, what you clicked or typed, what you expected, and whether it passed or failed (at most 600 characters). People watching the replay read them first. 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.`;
2324
2545
  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.";
2325
- 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.";
2546
+ var FOLDER_RULE = "This run has no repositories. The current folder belongs to this task: work in it and keep your throwaway files here.";
2326
2547
  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.";
2327
2548
  var FOLDER_DO_RULES = [
2328
2549
  `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.`,
@@ -2382,7 +2603,7 @@ var SPEC_RULES = [
2382
2603
  var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
2383
2604
  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.`;
2384
2605
  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.";
2385
- 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).";
2606
+ 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 task has no repositories), 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).";
2386
2607
  var RESULT_BRIEF_RULES = [
2387
2608
  '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.',
2388
2609
  '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".',
@@ -2450,12 +2671,17 @@ function folderText(root, results) {
2450
2671
  return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
2451
2672
  ${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
2452
2673
  }
2674
+ function taskResultsText(results) {
2675
+ const lines = results ?? [];
2676
+ return lines.length > 0 ? `The task already has these documents and results, latest versions, in the ${RESULTS_FOLDER} folder next to the repositories. Read what concerns this step before you start:
2677
+ ${bulletList(lines)}` : null;
2678
+ }
2453
2679
  function handOverText(boards) {
2454
2680
  const names = (boards ?? []).map((board) => board.name);
2455
2681
  if (names.length === 0) {
2456
2682
  return null;
2457
2683
  }
2458
- 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.`;
2684
+ return `When part of the work belongs to another board, or the work goes on as a new task, such as the next wave, create a linked task with the ${HAND_OVER_TOOL_NAME} tool on: ${names.join(", ")}. Write a plain title and a note on what you found and what the new task 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.`;
2459
2685
  }
2460
2686
  function handoffText(targets, isCheck) {
2461
2687
  if (targets.length === 0) {
@@ -2531,7 +2757,7 @@ function briefGoal(job, work2) {
2531
2757
  }
2532
2758
  return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its brief: ${job.title}`;
2533
2759
  }
2534
- function buildBriefPrompt(job, work2, workspace) {
2760
+ function buildBriefPrompt(job, work2, workspace, options) {
2535
2761
  const prompt = [
2536
2762
  briefGoal(job, work2),
2537
2763
  job.description,
@@ -2550,8 +2776,9 @@ ${JSON.stringify(job.spec)}` : null,
2550
2776
  workplaceText(job.computer, workspace.root),
2551
2777
  stepLine(job.stage),
2552
2778
  job.stage.instructions,
2779
+ job.sourcesText,
2553
2780
  contextText(job.projectContext),
2554
- FOLDER_RULE,
2781
+ `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2555
2782
  RESULT_HOME_RULE,
2556
2783
  `How to work:
2557
2784
  ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
@@ -2561,7 +2788,7 @@ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work
2561
2788
  ].filter(Boolean).join("\n\n");
2562
2789
  return { prompt, instructions };
2563
2790
  }
2564
- function buildSpecPrompt(job, work2, workspace) {
2791
+ function buildSpecPrompt(job, work2, workspace, options) {
2565
2792
  const prompt = [
2566
2793
  specGoal(job, work2),
2567
2794
  job.description,
@@ -2580,10 +2807,12 @@ ${JSON.stringify(job.spec)}` : null,
2580
2807
  workplaceText(job.computer, workspace.root),
2581
2808
  stepLine(job.stage),
2582
2809
  job.stage.instructions,
2810
+ job.sourcesText,
2583
2811
  contextText(job.projectContext),
2584
2812
  `The repositories for this task are checked out in the current folder, one subfolder each:
2585
2813
  ${bulletList(repositoryLines(job, workspace))}`,
2586
2814
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2815
+ taskResultsText(options.results),
2587
2816
  READ_ONLY_RULE,
2588
2817
  `How to work:
2589
2818
  ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
@@ -2620,6 +2849,7 @@ ${job.review}`,
2620
2849
  workplaceText(job.computer, workspace.root),
2621
2850
  stepLine(job.stage),
2622
2851
  job.stage.instructions,
2852
+ job.sourcesText,
2623
2853
  contextText(job.projectContext),
2624
2854
  `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2625
2855
  `How to work:
@@ -2641,7 +2871,7 @@ function buildTaskPrompt(job, workspace, options = {}) {
2641
2871
  if (!job.specWork) {
2642
2872
  throw new Error(SPEC_WORK_MISSING);
2643
2873
  }
2644
- return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
2874
+ return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace, options) : buildSpecPrompt(job, job.specWork, workspace, options);
2645
2875
  }
2646
2876
  if (isFolderJob(job)) {
2647
2877
  return buildFolderPrompt(job, workspace, options);
@@ -2669,10 +2899,12 @@ ${job.review}`,
2669
2899
  workplaceText(job.computer, workspace.root),
2670
2900
  stepLine(job.stage),
2671
2901
  job.stage.instructions,
2902
+ job.sourcesText,
2672
2903
  contextText(job.projectContext),
2673
2904
  `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}:
2674
2905
  ${bulletList(repositoryLines(job, workspace))}`,
2675
2906
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2907
+ taskResultsText(options.results),
2676
2908
  `How to work:
2677
2909
  ${bulletList(rules)}`,
2678
2910
  safetyText(job),
@@ -2687,135 +2919,135 @@ ${bulletList(rules)}`,
2687
2919
  }
2688
2920
 
2689
2921
  // src/specTools.ts
2690
- import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2691
- import { z as z4 } from "zod";
2922
+ import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
2923
+ import { z as z6 } from "zod";
2692
2924
  var MAX_FIRST_ROUND = 4;
2693
- var questionSchema = z4.object({
2694
- id: z4.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
2695
- question: z4.string().describe("One plain sentence the owner can answer without reading code"),
2696
- whyItMatters: z4.string().describe("One line: what changes in the result depending on the answer"),
2697
- options: z4.array(z4.object({
2698
- label: z4.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
2699
- description: z4.string().describe("One sentence: what happens if this option is chosen"),
2700
- recommended: z4.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
2925
+ var questionSchema = z6.object({
2926
+ id: z6.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
2927
+ question: z6.string().describe("One plain sentence the owner can answer without reading code"),
2928
+ whyItMatters: z6.string().describe("One line: what changes in the result depending on the answer"),
2929
+ options: z6.array(z6.object({
2930
+ label: z6.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
2931
+ description: z6.string().describe("One sentence: what happens if this option is chosen"),
2932
+ recommended: z6.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
2701
2933
  })).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'),
2702
- multiSelect: z4.boolean().describe("true only when the options are additive and several can apply together")
2934
+ multiSelect: z6.boolean().describe("true only when the options are additive and several can apply together")
2703
2935
  });
2704
- var briefSchema = z4.object({
2705
- findings: z4.array(z4.object({
2706
- fact: z4.string().describe("What the code does today that matters for this task"),
2707
- where: z4.string().describe("Where it is: owner/repo path:line")
2936
+ var briefSchema = z6.object({
2937
+ findings: z6.array(z6.object({
2938
+ fact: z6.string().describe("What the code does today that matters for this task"),
2939
+ where: z6.string().describe("Where it is: owner/repo path:line")
2708
2940
  })).describe("Findings from the code"),
2709
- dataFacts: z4.array(z4.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
2710
- approach: z4.string().describe("The approach you would take, in 2-4 sentences"),
2711
- codeMap: z4.array(z4.object({
2712
- repo: z4.string().describe("Repository full name (owner/name)"),
2713
- file: z4.string().describe("Path relative to the repository root"),
2714
- role: z4.string().describe("Why it matters: what to change or reuse there")
2941
+ dataFacts: z6.array(z6.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
2942
+ approach: z6.string().describe("The approach you would take, in 2-4 sentences"),
2943
+ codeMap: z6.array(z6.object({
2944
+ repo: z6.string().describe("Repository full name (owner/name)"),
2945
+ file: z6.string().describe("Path relative to the repository root"),
2946
+ role: z6.string().describe("Why it matters: what to change or reuse there")
2715
2947
  })).describe("The files the change will touch or must reuse"),
2716
- assumptions: z4.array(z4.object({
2717
- text: z4.string().describe("The default you chose"),
2718
- reason: z4.string().describe("Why it is safe or why it was chosen")
2948
+ assumptions: z6.array(z6.object({
2949
+ text: z6.string().describe("The default you chose"),
2950
+ reason: z6.string().describe("Why it is safe or why it was chosen")
2719
2951
  })).describe("Corners settled with a default instead of a question"),
2720
- pendingDecisions: z4.array(z4.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
2952
+ pendingDecisions: z6.array(z6.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
2721
2953
  });
2722
- var questionsPayloadSchema = z4.object({
2954
+ var questionsPayloadSchema = z6.object({
2723
2955
  brief: briefSchema,
2724
- questions: z4.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`)
2956
+ questions: z6.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`)
2725
2957
  });
2726
- var specSchema = z4.object({
2727
- goal: z4.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2728
- context: z4.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2729
- inScope: z4.array(z4.string()).describe("What this task delivers, one item each"),
2730
- outOfScope: z4.array(z4.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"),
2731
- decisions: z4.array(z4.object({
2732
- question: z4.string(),
2733
- answer: z4.string(),
2734
- by: z4.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2958
+ var specSchema = z6.object({
2959
+ goal: z6.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2960
+ context: z6.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2961
+ inScope: z6.array(z6.string()).describe("What this task delivers, one item each"),
2962
+ outOfScope: z6.array(z6.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"),
2963
+ decisions: z6.array(z6.object({
2964
+ question: z6.string(),
2965
+ answer: z6.string(),
2966
+ by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2735
2967
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2736
- codeMap: z4.array(z4.object({
2737
- repo: z4.string().describe("Repository full name (owner/name)"),
2738
- file: z4.string().describe("Path relative to the repository root"),
2739
- role: z4.string().describe("What the coder needs to know about this file"),
2740
- use: z4.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
2968
+ codeMap: z6.array(z6.object({
2969
+ repo: z6.string().describe("Repository full name (owner/name)"),
2970
+ file: z6.string().describe("Path relative to the repository root"),
2971
+ role: z6.string().describe("What the coder needs to know about this file"),
2972
+ use: z6.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
2741
2973
  })).describe("The files a coder must read before starting"),
2742
- changes: z4.array(z4.object({
2743
- repo: z4.string().describe("Repository full name (owner/name) where the change happens"),
2744
- file: z4.string().describe("Path to the file, relative to the repository root"),
2745
- action: z4.enum(["create", "modify", "delete"]),
2746
- description: z4.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
2747
- produces: z4.array(z4.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2748
- consumes: z4.array(z4.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2749
- acceptance: z4.array(z4.string()).describe("Ids of the criteria this change makes true, like AC-1")
2974
+ changes: z6.array(z6.object({
2975
+ repo: z6.string().describe("Repository full name (owner/name) where the change happens"),
2976
+ file: z6.string().describe("Path to the file, relative to the repository root"),
2977
+ action: z6.enum(["create", "modify", "delete"]),
2978
+ description: z6.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
2979
+ produces: z6.array(z6.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2980
+ consumes: z6.array(z6.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2981
+ acceptance: z6.array(z6.string()).describe("Ids of the criteria this change makes true, like AC-1")
2750
2982
  })).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
2751
- acceptance: z4.array(z4.object({
2752
- id: z4.string().describe("AC-1, AC-2 and so on, in order"),
2753
- kind: z4.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
2754
- text: z4.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
2983
+ acceptance: z6.array(z6.object({
2984
+ id: z6.string().describe("AC-1, AC-2 and so on, in order"),
2985
+ kind: z6.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
2986
+ text: z6.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
2755
2987
  })).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
2756
- testPlan: z4.object({
2757
- automated: z4.array(z4.object({
2758
- repo: z4.string(),
2759
- command: z4.string().describe("The exact command to run"),
2760
- expect: z4.string().describe("What passing looks like")
2988
+ testPlan: z6.object({
2989
+ automated: z6.array(z6.object({
2990
+ repo: z6.string(),
2991
+ command: z6.string().describe("The exact command to run"),
2992
+ expect: z6.string().describe("What passing looks like")
2761
2993
  })).describe("At least one command"),
2762
- newTests: z4.array(z4.object({
2763
- seam: z4.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2764
- covers: z4.array(z4.string()).describe("Ids of the criteria it covers")
2994
+ newTests: z6.array(z6.object({
2995
+ seam: z6.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2996
+ covers: z6.array(z6.string()).describe("Ids of the criteria it covers")
2765
2997
  })),
2766
- manual: z4.array(z4.object({
2767
- page: z4.string().describe("The page or screen"),
2768
- steps: z4.string().describe("What a person does and sees")
2998
+ manual: z6.array(z6.object({
2999
+ page: z6.string().describe("The page or screen"),
3000
+ steps: z6.string().describe("What a person does and sees")
2769
3001
  }))
2770
3002
  }),
2771
- risks: z4.array(z4.object({
2772
- risk: z4.string(),
2773
- mitigation: z4.string()
3003
+ risks: z6.array(z6.object({
3004
+ risk: z6.string(),
3005
+ mitigation: z6.string()
2774
3006
  })).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
2775
- riskLevel: z4.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
2776
- assumptions: z4.array(z4.object({
2777
- text: z4.string().describe("The default you chose"),
2778
- reason: z4.string().describe("Why, so the owner can confirm or correct it quickly")
3007
+ riskLevel: z6.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
3008
+ assumptions: z6.array(z6.object({
3009
+ text: z6.string().describe("The default you chose"),
3010
+ reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
2779
3011
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2780
- openQuestions: z4.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
2781
- split: z4.array(z4.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")
3012
+ openQuestions: z6.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
3013
+ split: z6.array(z6.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")
2782
3014
  });
2783
- var resultBriefSchema = z4.object({
2784
- format: z4.literal("brief").describe('Always "brief"'),
2785
- goal: z4.string().describe("What the work is for, for whom, and the outcome people will notice"),
2786
- context: z4.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
2787
- contains: z4.array(z4.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
2788
- acceptance: z4.array(z4.object({
2789
- id: z4.string().describe("AC-1, AC-2 and so on, in order"),
2790
- text: z4.string().describe("One observable property of the result that a checker can confirm by reading it")
3015
+ var resultBriefSchema = z6.object({
3016
+ format: z6.literal("brief").describe('Always "brief"'),
3017
+ goal: z6.string().describe("What the work is for, for whom, and the outcome people will notice"),
3018
+ context: z6.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
3019
+ contains: z6.array(z6.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
3020
+ acceptance: z6.array(z6.object({
3021
+ id: z6.string().describe("AC-1, AC-2 and so on, in order"),
3022
+ text: z6.string().describe("One observable property of the result that a checker can confirm by reading it")
2791
3023
  })).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
2792
- constraints: z4.array(z4.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
2793
- outOfScope: z4.array(z4.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
2794
- sources: z4.array(z4.object({
2795
- source: z4.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
2796
- use: z4.string().describe("What to take from it")
3024
+ constraints: z6.array(z6.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
3025
+ outOfScope: z6.array(z6.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
3026
+ sources: z6.array(z6.object({
3027
+ source: z6.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
3028
+ use: z6.string().describe("What to take from it")
2797
3029
  })).describe("Sources the work must use. Empty when the task itself is enough"),
2798
- approver: z4.string().describe("Who approves the result, as the task or the board names them"),
2799
- decisions: z4.array(z4.object({
2800
- question: z4.string(),
2801
- answer: z4.string(),
2802
- by: z4.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
3030
+ approver: z6.string().describe("Who approves the result, as the task or the board names them"),
3031
+ decisions: z6.array(z6.object({
3032
+ question: z6.string(),
3033
+ answer: z6.string(),
3034
+ by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2803
3035
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2804
- assumptions: z4.array(z4.object({
2805
- text: z4.string().describe("The default you chose"),
2806
- reason: z4.string().describe("Why, so the owner can confirm or correct it quickly")
3036
+ assumptions: z6.array(z6.object({
3037
+ text: z6.string().describe("The default you chose"),
3038
+ reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
2807
3039
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2808
- openQuestions: z4.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
3040
+ openQuestions: z6.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
2809
3041
  });
2810
- function reply3(text2, isError = false) {
3042
+ function reply4(text2, isError = false) {
2811
3043
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2812
3044
  }
2813
3045
  function unreachable(error, toolName) {
2814
3046
  const message = error instanceof Error ? error.message : String(error);
2815
- return reply3(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
3047
+ return reply4(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
2816
3048
  }
2817
3049
  function questionsTool(review, onSubmit) {
2818
- return tool4(
3050
+ return tool6(
2819
3051
  SUBMIT_QUESTIONS_TOOL_NAME,
2820
3052
  "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.",
2821
3053
  questionsPayloadSchema.shape,
@@ -2827,10 +3059,10 @@ function questionsTool(review, onSubmit) {
2827
3059
  return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
2828
3060
  }
2829
3061
  if (!verdict.accepted) {
2830
- return reply3(verdict.feedback, true);
3062
+ return reply4(verdict.feedback, true);
2831
3063
  }
2832
3064
  onSubmit(args);
2833
- return reply3(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.");
3065
+ return reply4(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.");
2834
3066
  }
2835
3067
  );
2836
3068
  }
@@ -2838,7 +3070,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
2838
3070
  function specTool(review, onSubmit, builderCheckRan) {
2839
3071
  let repairs = 0;
2840
3072
  let reminded = false;
2841
- return tool4(
3073
+ return tool6(
2842
3074
  SUBMIT_SPEC_TOOL_NAME,
2843
3075
  "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.",
2844
3076
  specSchema.shape,
@@ -2851,20 +3083,20 @@ function specTool(review, onSubmit, builderCheckRan) {
2851
3083
  }
2852
3084
  if (!verdict.accepted) {
2853
3085
  repairs += 1;
2854
- return reply3(verdict.feedback, true);
3086
+ return reply4(verdict.feedback, true);
2855
3087
  }
2856
3088
  if (!builderCheckRan() && !reminded) {
2857
3089
  reminded = true;
2858
- return reply3(BUILDER_CHECK_FIRST, true);
3090
+ return reply4(BUILDER_CHECK_FIRST, true);
2859
3091
  }
2860
3092
  onSubmit({ spec: args, problems: verdict.problems });
2861
- return reply3(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
3093
+ return reply4(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2862
3094
  }
2863
3095
  );
2864
3096
  }
2865
3097
  function briefTool(review, onSubmit) {
2866
3098
  let repairs = 0;
2867
- return tool4(
3099
+ return tool6(
2868
3100
  SUBMIT_SPEC_TOOL_NAME,
2869
3101
  "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.",
2870
3102
  resultBriefSchema.shape,
@@ -2877,10 +3109,10 @@ function briefTool(review, onSubmit) {
2877
3109
  }
2878
3110
  if (!verdict.accepted) {
2879
3111
  repairs += 1;
2880
- return reply3(verdict.feedback, true);
3112
+ return reply4(verdict.feedback, true);
2881
3113
  }
2882
3114
  onSubmit({ spec: args, problems: verdict.problems });
2883
- return reply3(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
3115
+ return reply4(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
2884
3116
  }
2885
3117
  );
2886
3118
  }
@@ -2983,11 +3215,11 @@ function usageLimitWatch() {
2983
3215
  var NO_RESULT_TEXT = "Claude finished without a result.";
2984
3216
  var SERVER_NAME2 = "tamsense";
2985
3217
  var CAPTION_FIELDS = {
2986
- title: z5.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"'),
2987
- purpose: z5.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
3218
+ title: z7.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"'),
3219
+ purpose: z7.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
2988
3220
  };
2989
3221
  var NOTES_FIELD = {
2990
- notes: z5.string().optional().describe('what you tested in it and what happened, at most 600 characters: what you opened, what you clicked or typed, what you expected, and whether it passed or failed. "Opened the disposed requests on the warehouse page, filtered by December, expected 3 rows: got 3, passed. Clicked Export: the file was empty, failed."')
3222
+ notes: z7.string().optional().describe('what you tested in it and what happened, at most 600 characters: what you opened, what you clicked or typed, what you expected, and whether it passed or failed. "Opened the disposed requests on the warehouse page, filtered by December, expected 3 rows: got 3, passed. Clicked Export: the file was empty, failed."')
2991
3223
  };
2992
3224
  var ATTACH_TOOLS = {
2993
3225
  screenshot: {
@@ -3008,21 +3240,21 @@ var ATTACH_TOOLS = {
3008
3240
  };
3009
3241
  function attachTool(kind, attach) {
3010
3242
  const spec = ATTACH_TOOLS[kind];
3011
- return tool5(
3243
+ return tool7(
3012
3244
  spec.name,
3013
3245
  spec.description,
3014
- { path: z5.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
3246
+ { path: z7.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
3015
3247
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
3016
3248
  );
3017
3249
  }
3018
3250
  function verdictTool(onVerdict) {
3019
- return tool5(
3251
+ return tool7(
3020
3252
  VERDICT_TOOL_NAME,
3021
3253
  "Hand in the result of this check. Call it exactly once, at the very end.",
3022
3254
  {
3023
- passed: z5.boolean().describe("true only if nothing must be fixed"),
3024
- report: z5.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
3025
- couldNotRun: z5.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")
3255
+ passed: z7.boolean().describe("true only if nothing must be fixed"),
3256
+ report: z7.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
3257
+ couldNotRun: z7.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")
3026
3258
  },
3027
3259
  async (args) => {
3028
3260
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -3031,13 +3263,13 @@ function verdictTool(onVerdict) {
3031
3263
  );
3032
3264
  }
3033
3265
  function handoffTool(targets, onHandoff) {
3034
- return tool5(
3266
+ return tool7(
3035
3267
  HANDOFF_TOOL_NAME,
3036
3268
  "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.",
3037
3269
  {
3038
- to: z5.string().describe("name or id of the step to hand the task to"),
3039
- outcome: z5.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
3040
- note: z5.string().describe("for the next agent and the owner: what was done, what to check, what is open")
3270
+ to: z7.string().describe("name or id of the step to hand the task to"),
3271
+ outcome: z7.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
3272
+ note: z7.string().describe("for the next agent and the owner: what was done, what to check, what is open")
3041
3273
  },
3042
3274
  async (args) => {
3043
3275
  const choice = chooseHandoff(targets, args);
@@ -3050,25 +3282,25 @@ function handoffTool(targets, onHandoff) {
3050
3282
  );
3051
3283
  }
3052
3284
  function askPersonTool(asking, waiting) {
3053
- return tool5(
3285
+ return tool7(
3054
3286
  ASK_PERSON_TOOL_NAME,
3055
3287
  "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.",
3056
3288
  {
3057
- to: z5.union([z5.number(), z5.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"),
3058
- why: z5.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
3059
- question: z5.string().describe("one plain question the person can answer without reading the code"),
3060
- context: z5.string().optional().describe("one line on what you saw that led to the question"),
3061
- tried: z5.array(z5.object({
3062
- did: z5.string().describe("the command or step"),
3063
- error: z5.string().optional().describe("the exact error line, if there was one"),
3064
- result: z5.string().describe("what it showed, one line")
3289
+ to: z7.union([z7.number(), z7.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"),
3290
+ why: z7.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
3291
+ question: z7.string().describe("one plain question the person can answer without reading the code"),
3292
+ context: z7.string().optional().describe("one line on what you saw that led to the question"),
3293
+ tried: z7.array(z7.object({
3294
+ did: z7.string().describe("the command or step"),
3295
+ error: z7.string().optional().describe("the exact error line, if there was one"),
3296
+ result: z7.string().describe("what it showed, one line")
3065
3297
  })).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
3066
- options: z5.array(z5.object({
3067
- label: z5.string().describe("a short answer the person can pick"),
3068
- detail: z5.string().describe("one line on what happens if it is picked")
3298
+ options: z7.array(z7.object({
3299
+ label: z7.string().describe("a short answer the person can pick"),
3300
+ detail: z7.string().describe("one line on what happens if it is picked")
3069
3301
  })).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
3070
- blocking: z5.boolean().describe("true when you cannot go on without the answer"),
3071
- assumption: z5.string().optional().describe("when blocking is false: what you continue with until the answer comes")
3302
+ blocking: z7.boolean().describe("true when you cannot go on without the answer"),
3303
+ assumption: z7.string().optional().describe("when blocking is false: what you continue with until the answer comes")
3072
3304
  },
3073
3305
  async (args) => {
3074
3306
  if (waiting.waitingFor !== null) {
@@ -3078,16 +3310,16 @@ function askPersonTool(asking, waiting) {
3078
3310
  if ("error" in choice) {
3079
3311
  return { content: [{ type: "text", text: choice.error }], isError: true };
3080
3312
  }
3081
- let reply4;
3313
+ let reply5;
3082
3314
  try {
3083
- reply4 = await asking.send(choice.body);
3315
+ reply5 = await asking.send(choice.body);
3084
3316
  } catch (error) {
3085
3317
  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 };
3086
3318
  }
3087
3319
  if (choice.body.blocking) {
3088
- waiting.waitingFor = reply4.id;
3320
+ waiting.waitingFor = reply5.id;
3089
3321
  }
3090
- return { content: [{ type: "text", text: askedText(reply4.name, choice.body) }] };
3322
+ return { content: [{ type: "text", text: askedText(reply5.name, choice.body) }] };
3091
3323
  }
3092
3324
  );
3093
3325
  }
@@ -3132,7 +3364,8 @@ function tamsenseServer(request, submitted) {
3132
3364
  })] : [],
3133
3365
  ...request.results ? resultToolset(request.results) : [],
3134
3366
  ...request.handOver ? [handOverTool(request.handOver)] : [],
3135
- ...request.schedules ? [proposeScheduleTool(request.schedules, submitted)] : []
3367
+ ...request.schedules ? [proposeScheduleTool(request.schedules, submitted), wakeUpLaterTool(request.schedules)] : [],
3368
+ ...sourceToolset(request.sources)
3136
3369
  ];
3137
3370
  return {
3138
3371
  server: createSdkMcpServer2({ name: SERVER_NAME2, version: "1.0.0", tools }),
@@ -3159,6 +3392,7 @@ async function attempt(request, context, resume) {
3159
3392
  let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
3160
3393
  const tamsense = tamsenseServer(request, context.submitted);
3161
3394
  const shown = hiddenStepsFilter();
3395
+ const calls = sourceCalls(request.sources?.tools);
3162
3396
  const helpers = helperTasks();
3163
3397
  const messages = context.startQuery({
3164
3398
  prompt: request.input.start(request.prompt.prompt),
@@ -3193,7 +3427,7 @@ async function attempt(request, context, resume) {
3193
3427
  messages.close();
3194
3428
  return { outcome, sessionId, resumeFailed: true };
3195
3429
  }
3196
- for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
3430
+ for (const draft of [...helpers.observe(message), ...feedOf(message, calls).filter(shown).flatMap(helpers.adjust)]) {
3197
3431
  if (draft.kind === "agent" && draft.parent === null) {
3198
3432
  context.submitted.helperStarted = true;
3199
3433
  }
@@ -3245,12 +3479,16 @@ var runClaude = claudeRunner(query3);
3245
3479
 
3246
3480
  // src/config.ts
3247
3481
  import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3248
- import { homedir as homedir2 } from "os";
3249
3482
  import { dirname as dirname2, join as join6 } from "path";
3250
3483
  var OWNER_ONLY_FILE = 384;
3251
3484
  var OWNER_ONLY_FOLDER = 448;
3252
- function configPath(home = homedir2()) {
3253
- return join6(home, ".tamsense", "runner.json");
3485
+ var PROFILES_FOLDER = "profiles";
3486
+ function dataFolderFor(home, profile) {
3487
+ const shared = join6(home, ".tamsense");
3488
+ return profile === null ? shared : join6(shared, PROFILES_FOLDER, profile);
3489
+ }
3490
+ function configPath(dataFolder) {
3491
+ return join6(dataFolder, "runner.json");
3254
3492
  }
3255
3493
  function isRecord2(value) {
3256
3494
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -3266,7 +3504,7 @@ function parseConfig(value) {
3266
3504
  function isMissingFile(error) {
3267
3505
  return error instanceof Error && "code" in error && error.code === "ENOENT";
3268
3506
  }
3269
- async function readConfig(path = configPath()) {
3507
+ async function readConfig(path) {
3270
3508
  try {
3271
3509
  return parseConfig(JSON.parse(await readFile3(path, "utf8")));
3272
3510
  } catch (error) {
@@ -3276,18 +3514,18 @@ async function readConfig(path = configPath()) {
3276
3514
  throw error;
3277
3515
  }
3278
3516
  }
3279
- async function writeConfig(config, path = configPath()) {
3517
+ async function writeConfig(config, path) {
3280
3518
  await mkdir4(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
3281
3519
  await writeFile3(path, `${JSON.stringify(config, null, 4)}
3282
3520
  `, { mode: OWNER_ONLY_FILE });
3283
3521
  }
3284
- async function removeConfig(path = configPath()) {
3522
+ async function removeConfig(path) {
3285
3523
  await rm4(path, { force: true });
3286
3524
  }
3287
3525
 
3288
3526
  // src/login.ts
3289
3527
  var MS_PER_SECOND = 1e3;
3290
- var EXPIRED_TEXT = `The code expired before it was confirmed. Run ${LOGIN_COMMAND} again.`;
3528
+ var EXPIRED_TEXT = "The code expired before it was confirmed. Run the same command again.";
3291
3529
  var DENIED_TEXT = "The connection was cancelled in the browser. This computer was not connected.";
3292
3530
  async function login(tools, request) {
3293
3531
  const pairing = await tools.api.request("POST", PAIRINGS_PATH, request);
@@ -3299,14 +3537,14 @@ async function login(tools, request) {
3299
3537
  tools.openBrowser(pageUrl);
3300
3538
  while (tools.now() < expiresAt) {
3301
3539
  await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
3302
- const reply4 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
3303
- if (reply4.status === "connected") {
3304
- return { token: reply4.token, device: reply4.device };
3540
+ const reply5 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
3541
+ if (reply5.status === "connected") {
3542
+ return { token: reply5.token, device: reply5.device };
3305
3543
  }
3306
- if (reply4.status === "denied") {
3544
+ if (reply5.status === "denied") {
3307
3545
  throw new Error(DENIED_TEXT);
3308
3546
  }
3309
- if (reply4.status === "expired") {
3547
+ if (reply5.status === "expired") {
3310
3548
  break;
3311
3549
  }
3312
3550
  }
@@ -3652,8 +3890,8 @@ function bundledRecorder() {
3652
3890
  const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
3653
3891
  return existsSync6(path) ? path : null;
3654
3892
  }
3655
- function recordingsFolder(home, runId) {
3656
- return join8(home, ".tamsense", "recordings", `run-${runId}`);
3893
+ function recordingsFolder(dataFolder, runId) {
3894
+ return join8(dataFolder, "recordings", `run-${runId}`);
3657
3895
  }
3658
3896
  function nodeOptionsWithRecorder(recorder, nodeOptions) {
3659
3897
  return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
@@ -3679,6 +3917,13 @@ function describeDevice(identity) {
3679
3917
  return identity.device.team ? `a team computer of ${identity.projectName}` : `the personal computer of ${identity.email ?? "its owner"}`;
3680
3918
  }
3681
3919
 
3920
+ // src/sense.ts
3921
+ import { mkdtemp, rm as rm5 } from "fs/promises";
3922
+ import { tmpdir } from "os";
3923
+ import { dirname as dirname4, join as join9 } from "path";
3924
+ import process5 from "process";
3925
+ import { createSdkMcpServer as createSdkMcpServer3, query as query4 } from "@anthropic-ai/claude-agent-sdk";
3926
+
3682
3927
  // src/system.ts
3683
3928
  import { spawn as spawn3 } from "child_process";
3684
3929
  import { hostname } from "os";
@@ -3693,8 +3938,9 @@ var MAX_NAME_LENGTH2 = 80;
3693
3938
  function platformLabel(platform = process.platform) {
3694
3939
  return PLATFORM_LABELS[platform] ?? platform;
3695
3940
  }
3696
- function defaultDeviceName() {
3697
- return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
3941
+ function defaultDeviceName(profile = null) {
3942
+ const name = hostname() || "My computer";
3943
+ return (profile === null ? name : `${name} (${profile})`).slice(0, MAX_NAME_LENGTH2);
3698
3944
  }
3699
3945
  function browserCommand(url, platform) {
3700
3946
  if (platform === "win32") {
@@ -3723,10 +3969,104 @@ async function sleep(ms, signal) {
3723
3969
  });
3724
3970
  }
3725
3971
 
3972
+ // src/sense.ts
3973
+ var SERVER_NAME3 = "tamsense";
3974
+ var MAX_SENSE_TURNS = 3;
3975
+ var HEARTBEAT_MS = 5e3;
3976
+ var STOPPED_HERE = "stopped_here";
3977
+ var CLOSED_ON_SITE = "closed_on_site";
3978
+ var TOO_LONG = "too_long";
3979
+ var FAILED = { outcome: "failed", text: "", model: null, usage: null };
3980
+ function claudeSenseRunner(startQuery) {
3981
+ return async (request) => {
3982
+ request.signal.throwIfAborted();
3983
+ const abortController = new AbortController();
3984
+ request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3985
+ const tools = sourceToolset(request.sources);
3986
+ const limit = usageLimitWatch();
3987
+ const messages = startQuery({
3988
+ prompt: request.prompt,
3989
+ options: {
3990
+ cwd: request.cwd,
3991
+ systemPrompt: request.system,
3992
+ tools: [],
3993
+ settingSources: [],
3994
+ persistSession: false,
3995
+ strictMcpConfig: true,
3996
+ mcpServers: tools.length > 0 ? { [SERVER_NAME3]: createSdkMcpServer3({ name: SERVER_NAME3, version: "1.0.0", tools }) } : {},
3997
+ allowedTools: tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`),
3998
+ permissionMode: "auto",
3999
+ canUseTool: denyWhenAsked,
4000
+ maxTurns: MAX_SENSE_TURNS,
4001
+ outputFormat: { type: "json_schema", schema: request.schema },
4002
+ abortController,
4003
+ env: request.env,
4004
+ ...request.model === null ? {} : { model: request.model }
4005
+ }
4006
+ });
4007
+ let finish2 = FAILED;
4008
+ for await (const message of messages) {
4009
+ limit.observe(message);
4010
+ if (message.type === "result") {
4011
+ const measured = outcomeOf(message, "");
4012
+ const text2 = message.subtype !== "success" ? "" : message.structured_output === void 0 ? message.result : JSON.stringify(message.structured_output);
4013
+ finish2 = { outcome: measured.completed && text2 ? "succeeded" : "failed", text: text2, model: measured.model, usage: measured.usage };
4014
+ }
4015
+ }
4016
+ return limit.pausedUntil(finish2.outcome === "succeeded") ? { ...finish2, outcome: "usage_limit" } : finish2;
4017
+ };
4018
+ }
4019
+ function senseHeartbeat(api, job, abort) {
4020
+ return async () => {
4021
+ try {
4022
+ const answer = await api.request("POST", sensePath(job.senseId, "heartbeat"), void 0, { safeToRepeat: true });
4023
+ if (!answer.keepGoing) {
4024
+ abort();
4025
+ }
4026
+ } catch {
4027
+ }
4028
+ };
4029
+ }
4030
+ async function runSenseJob(job, runSense, tools, stop) {
4031
+ const cwd = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
4032
+ const run2 = new AbortController();
4033
+ const stopRun = () => run2.abort(STOPPED_HERE);
4034
+ if (stop.aborted) {
4035
+ stopRun();
4036
+ }
4037
+ stop.addEventListener("abort", stopRun, { once: true });
4038
+ const deadline = setTimeout(() => run2.abort(TOO_LONG), job.deadlineMs);
4039
+ const beat = senseHeartbeat(tools.api, job, () => run2.abort(CLOSED_ON_SITE));
4040
+ const beating = setInterval(() => void beat(), HEARTBEAT_MS);
4041
+ let body;
4042
+ try {
4043
+ const env = withNodeOnPath(process5.env, dirname4(process5.execPath));
4044
+ body = await runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, env, signal: run2.signal, sources: sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources")) });
4045
+ } catch {
4046
+ body = FAILED;
4047
+ } finally {
4048
+ clearTimeout(deadline);
4049
+ clearInterval(beating);
4050
+ stop.removeEventListener("abort", stopRun);
4051
+ await rm5(cwd, { recursive: true, force: true });
4052
+ }
4053
+ if (run2.signal.reason === CLOSED_ON_SITE) {
4054
+ return;
4055
+ }
4056
+ try {
4057
+ await tools.api.request("POST", sensePath(job.senseId, "finish"), run2.signal.aborted ? FAILED : body);
4058
+ if (body.outcome === "succeeded" && !run2.signal.aborted) {
4059
+ tools.print(`Sense answered on task #${job.taskId}`);
4060
+ }
4061
+ } catch {
4062
+ }
4063
+ }
4064
+ var runClaudeSense = claudeSenseRunner(query4);
4065
+
3726
4066
  // src/update.ts
3727
4067
  import { spawn as spawn4 } from "child_process";
3728
4068
  import { readFileSync as readFileSync2 } from "fs";
3729
- import process5 from "process";
4069
+ import process6 from "process";
3730
4070
  var PACKAGE_NAME = "@tamsensedev/runner";
3731
4071
  var RELAUNCHED_ENV = "TAMSENSE_RELAUNCHED";
3732
4072
  var NO_UPDATE_ENV = "TAMSENSE_NO_UPDATE";
@@ -3778,27 +4118,27 @@ function relaunchArgs(argv) {
3778
4118
  return ["--yes", `${PACKAGE_NAME}@latest`, ...argv];
3779
4119
  }
3780
4120
  function relaunch(argv, env) {
3781
- const isWindows = process5.platform === "win32";
4121
+ const isWindows = process6.platform === "win32";
3782
4122
  return new Promise((resolve5, reject) => {
3783
4123
  const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
3784
4124
  const ignoreInterrupt = () => {
3785
4125
  };
3786
- process5.on("SIGINT", ignoreInterrupt);
4126
+ process6.on("SIGINT", ignoreInterrupt);
3787
4127
  child.on("error", (error) => {
3788
- process5.off("SIGINT", ignoreInterrupt);
4128
+ process6.off("SIGINT", ignoreInterrupt);
3789
4129
  reject(error);
3790
4130
  });
3791
4131
  child.on("close", (code) => {
3792
- process5.off("SIGINT", ignoreInterrupt);
4132
+ process6.off("SIGINT", ignoreInterrupt);
3793
4133
  resolve5(code ?? 1);
3794
4134
  });
3795
4135
  });
3796
4136
  }
3797
4137
 
3798
4138
  // src/worker.ts
3799
- import { rm as rm6 } from "fs/promises";
3800
- import { dirname as dirname5 } from "path";
3801
- import process6 from "process";
4139
+ import { rm as rm7 } from "fs/promises";
4140
+ import { dirname as dirname6, join as join12 } from "path";
4141
+ import process7 from "process";
3802
4142
 
3803
4143
  // src/feedQueue.ts
3804
4144
  var MAX_BATCH = 200;
@@ -3845,8 +4185,8 @@ function feedQueue(send) {
3845
4185
 
3846
4186
  // src/library.ts
3847
4187
  import { existsSync as existsSync7 } from "fs";
3848
- import { cp, mkdir as mkdir5, rm as rm5 } from "fs/promises";
3849
- import { join as join9 } from "path";
4188
+ import { cp, mkdir as mkdir5, rm as rm6 } from "fs/promises";
4189
+ import { join as join10 } from "path";
3850
4190
  import { fileURLToPath as fileURLToPath2 } from "url";
3851
4191
  var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
3852
4192
  var SKILL_NAME = /^[a-z0-9-]+$/;
@@ -3858,19 +4198,19 @@ function allowedSkills(stage) {
3858
4198
  const skills = stage.role?.skills ?? null;
3859
4199
  return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
3860
4200
  }
3861
- function roleLibraryFolder(home, runId) {
3862
- return join9(home, ".tamsense", "libraries", `run-${runId}`);
4201
+ function roleLibraryFolder(dataFolder, runId) {
4202
+ return join10(dataFolder, "libraries", `run-${runId}`);
3863
4203
  }
3864
4204
  async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3865
4205
  if (skills === null) {
3866
4206
  return source;
3867
4207
  }
3868
- await rm5(folder, { recursive: true, force: true });
3869
- await mkdir5(join9(folder, "skills"), { recursive: true });
3870
- await cp(join9(source, ".claude-plugin"), join9(folder, ".claude-plugin"), { recursive: true });
3871
- await cp(join9(source, "agents"), join9(folder, "agents"), { recursive: true });
3872
- for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync7(join9(source, "skills", name)))) {
3873
- await cp(join9(source, "skills", skill), join9(folder, "skills", skill), { recursive: true });
4208
+ await rm6(folder, { recursive: true, force: true });
4209
+ await mkdir5(join10(folder, "skills"), { recursive: true });
4210
+ await cp(join10(source, ".claude-plugin"), join10(folder, ".claude-plugin"), { recursive: true });
4211
+ await cp(join10(source, "agents"), join10(folder, "agents"), { recursive: true });
4212
+ for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync7(join10(source, "skills", name)))) {
4213
+ await cp(join10(source, "skills", skill), join10(folder, "skills", skill), { recursive: true });
3874
4214
  }
3875
4215
  return folder;
3876
4216
  }
@@ -3879,7 +4219,7 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3879
4219
  import { randomUUID } from "crypto";
3880
4220
  import { createReadStream } from "fs";
3881
4221
  import { readdir as readdir2, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
3882
- import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, join as join10 } from "path";
4222
+ import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
3883
4223
  import { createInterface as createInterface2 } from "readline";
3884
4224
  import { Readable } from "stream";
3885
4225
  import { promisify } from "util";
@@ -3959,12 +4299,12 @@ async function readMeta(file) {
3959
4299
  }
3960
4300
  async function metaFor(kind, file) {
3961
4301
  if (kind === "replay") {
3962
- return readMeta(join10(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
4302
+ return readMeta(join11(dirname5(file), `${sessionIdOf(file)}${META_SUFFIX}`));
3963
4303
  }
3964
- const folder = basename4(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
4304
+ const folder = basename4(dirname5(file)) === "video" ? dirname5(dirname5(file)) : dirname5(file);
3965
4305
  for (const name of await readdir2(folder).catch(() => [])) {
3966
4306
  if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
3967
- const meta = await readMeta(join10(folder, name));
4307
+ const meta = await readMeta(join11(folder, name));
3968
4308
  if (meta?.video === file) {
3969
4309
  return meta;
3970
4310
  }
@@ -3974,7 +4314,7 @@ async function metaFor(kind, file) {
3974
4314
  }
3975
4315
  async function ledger(dir) {
3976
4316
  try {
3977
- return new Set(JSON.parse(await readFile4(join10(dir, LEDGER_FILE), "utf8")));
4317
+ return new Set(JSON.parse(await readFile4(join11(dir, LEDGER_FILE), "utf8")));
3978
4318
  } catch {
3979
4319
  return /* @__PURE__ */ new Set();
3980
4320
  }
@@ -3982,7 +4322,7 @@ async function ledger(dir) {
3982
4322
  async function remember(dir, file) {
3983
4323
  const done2 = await ledger(dir);
3984
4324
  done2.add(file);
3985
- await writeFile4(join10(dir, LEDGER_FILE), JSON.stringify([...done2]));
4325
+ await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
3986
4326
  }
3987
4327
  async function nonEmpty(file) {
3988
4328
  return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
@@ -3995,8 +4335,8 @@ async function pendingRecordings(dir) {
3995
4335
  if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
3996
4336
  continue;
3997
4337
  }
3998
- const meta = await readMeta(join10(dir, name));
3999
- const replay = join10(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
4338
+ const meta = await readMeta(join11(dir, name));
4339
+ const replay = join11(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
4000
4340
  if (!done2.has(replay) && await nonEmpty(replay)) {
4001
4341
  pending.push({ kind: "replay", file: replay, meta });
4002
4342
  }
@@ -4143,7 +4483,7 @@ async function readScreenshot(request) {
4143
4483
  }
4144
4484
 
4145
4485
  // src/worker.ts
4146
- var POLL_INTERVAL_MS = 5e3;
4486
+ var POLL_INTERVAL_MS = 2e3;
4147
4487
  var MAX_RETRY_WAIT_MS = 6e4;
4148
4488
  var POLL_DEADLINE_MS = 3 * 6e4;
4149
4489
  var INBOX_INTERVAL_MS = 2e3;
@@ -4154,8 +4494,10 @@ var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
4154
4494
  var MAX_CHECKS = 2;
4155
4495
  var CHECK_HEARTBEAT_MS = 3e4;
4156
4496
  var CHECK_DEADLINE_MS = 10 * 6e4;
4157
- var STOPPED_HERE = "stopped_here";
4158
- var CLOSED_ON_SITE = "closed_on_site";
4497
+ var ROUTINE_DEADLINE_MS = 30 * 6e4;
4498
+ var MAX_SENSE = 2;
4499
+ var STOPPED_HERE2 = "stopped_here";
4500
+ var CLOSED_ON_SITE2 = "closed_on_site";
4159
4501
  function messageOf3(error) {
4160
4502
  return error instanceof Error ? error.message : String(error);
4161
4503
  }
@@ -4228,9 +4570,9 @@ function inbox(job, tools, record, closedOnSite) {
4228
4570
  async function check() {
4229
4571
  const ack = input.takeDelivered();
4230
4572
  const body = { ack };
4231
- let reply4;
4573
+ let reply5;
4232
4574
  try {
4233
- reply4 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
4575
+ reply5 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
4234
4576
  } catch (error) {
4235
4577
  input.restoreDelivered(ack);
4236
4578
  if (!failing && !alreadyReported(error)) {
@@ -4240,23 +4582,23 @@ function inbox(job, tools, record, closedOnSite) {
4240
4582
  return;
4241
4583
  }
4242
4584
  failing = false;
4243
- if (!reply4.keepGoing) {
4585
+ if (!reply5.keepGoing) {
4244
4586
  closedOnSite();
4245
4587
  return;
4246
4588
  }
4247
- for (const message of reply4.messages) {
4589
+ for (const message of reply5.messages) {
4248
4590
  if (input.push(message)) {
4249
4591
  tools.print(` ${messageLine(message)}`);
4250
4592
  }
4251
4593
  }
4252
- for (const stop of reply4.helperStops ?? []) {
4594
+ for (const stop of reply5.helperStops ?? []) {
4253
4595
  if (!stopsSeen.has(stop.ref)) {
4254
4596
  stopsSeen.add(stop.ref);
4255
4597
  void stopHelper(input, stop, tools.print);
4256
4598
  }
4257
4599
  }
4258
- if (reply4.model !== null || reply4.effort !== null) {
4259
- const change = { model: reply4.model, effort: reply4.effort };
4600
+ if (reply5.model !== null || reply5.effort !== null) {
4601
+ const change = { model: reply5.model, effort: reply5.effort };
4260
4602
  void input.switchModel(change).then(
4261
4603
  (applied) => {
4262
4604
  if (applied) {
@@ -4311,11 +4653,11 @@ function permissionTools(job, tools) {
4311
4653
  }
4312
4654
  return {
4313
4655
  send: async (body) => {
4314
- const reply4 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
4315
- if (reply4.decision === "wait") {
4656
+ const reply5 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
4657
+ if (reply5.decision === "wait") {
4316
4658
  tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
4317
4659
  }
4318
- return reply4;
4660
+ return reply5;
4319
4661
  },
4320
4662
  check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
4321
4663
  sleep: tools.sleep
@@ -4340,17 +4682,18 @@ function skillsLine(skills) {
4340
4682
  async function prepare(job, tools) {
4341
4683
  if (isFolderJob(job)) {
4342
4684
  tools.print("Preparing the task folder\u2026");
4343
- const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
4685
+ const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.dataFolder, job), tools.api);
4344
4686
  return { workspace: workspace2, results, identity: {} };
4345
4687
  }
4688
+ const taskResults = (workspace2) => writeResults(join12(workspace2.root, RESULTS_FOLDER), job, tools.api);
4346
4689
  if (isLocalJob(job)) {
4347
4690
  tools.print("Preparing the code from the folders on this computer\u2026");
4348
4691
  const workspace2 = await prepareLocalWorkspace(job, tools);
4349
- return { workspace: workspace2, results: null, identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
4692
+ return { workspace: workspace2, results: await taskResults(workspace2), identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
4350
4693
  }
4351
4694
  tools.print("Preparing the code\u2026");
4352
4695
  const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
4353
- return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
4696
+ return { workspace, results: await taskResults(workspace), identity: await commitIdentityEnv(tools.home) };
4354
4697
  }
4355
4698
  async function publish(workspace, job, api, identity) {
4356
4699
  return isLocalJob(job) ? await publishLocalWorkspace(workspace, job, identity) : await publishWorkspace(workspace, job, await gitToken(api, job), identity);
@@ -4362,13 +4705,13 @@ async function executeJob(job, tools, signal, input, record) {
4362
4705
  if (skills) {
4363
4706
  tools.print(skillsLine(skills));
4364
4707
  }
4365
- const library = await prepareLibrary(roleLibraryFolder(tools.home, job.runId), skills);
4708
+ const library = await prepareLibrary(roleLibraryFolder(tools.dataFolder, job.runId), skills);
4366
4709
  signal.throwIfAborted();
4367
4710
  tools.print("Claude is working\u2026");
4368
4711
  const attached = { count: 0 };
4369
4712
  const session = sessionReporter(tools.api, job, tools.print);
4370
- const recordings = recordingsFolder(tools.home, job.runId);
4371
- const baseEnv = withNodeOnPath(process6.env, dirname5(process6.execPath));
4713
+ const recordings = recordingsFolder(tools.dataFolder, job.runId);
4714
+ const baseEnv = withNodeOnPath(process7.env, dirname6(process7.execPath));
4372
4715
  const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
4373
4716
  const outcome = await tools.runAgent({
4374
4717
  cwd: workspace.root,
@@ -4391,7 +4734,8 @@ async function executeJob(job, tools, signal, input, record) {
4391
4734
  attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
4392
4735
  results: resultTools(job, tools.api, workspace.root),
4393
4736
  handOver: handOverTools(job, tools.api),
4394
- schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules")) : null
4737
+ schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules"), runPath(job.runId, "wake")) : null,
4738
+ sources: sourceTools(job.sourceTools, tools.api, runPath(job.runId, "sources"))
4395
4739
  });
4396
4740
  await session.sent();
4397
4741
  signal.throwIfAborted();
@@ -4453,7 +4797,7 @@ function resultLine(finish2, workspace = "repositories") {
4453
4797
  return `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.`;
4454
4798
  }
4455
4799
  async function sendRecordings(job, tools) {
4456
- const folder = recordingsFolder(tools.home, job.runId);
4800
+ const folder = recordingsFolder(tools.dataFolder, job.runId);
4457
4801
  try {
4458
4802
  const report = await uploadPendingRecordings(tools.api, job.runId, folder);
4459
4803
  if (report.uploaded > 0) {
@@ -4466,16 +4810,16 @@ async function sendRecordings(job, tools) {
4466
4810
  tools.print(`Could not attach a recording: ${line}`);
4467
4811
  }
4468
4812
  } finally {
4469
- await rm6(folder, { recursive: true, force: true });
4813
+ await rm7(folder, { recursive: true, force: true });
4470
4814
  }
4471
4815
  }
4472
4816
  function failureReport(error, signal) {
4473
- return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
4817
+ return signal.reason === STOPPED_HERE2 ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
4474
4818
  }
4475
4819
  async function runJob(job, tools, stop) {
4476
4820
  tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
4477
4821
  const run2 = new AbortController();
4478
- const stopRun = () => run2.abort(STOPPED_HERE);
4822
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4479
4823
  if (stop.aborted) {
4480
4824
  stopRun();
4481
4825
  }
@@ -4483,7 +4827,7 @@ async function runJob(job, tools, stop) {
4483
4827
  const feed = feedQueue((entries) => tools.api.request("POST", runPath(job.runId, "feed"), { entries }, { safeToRepeat: true }));
4484
4828
  const feeding = setInterval(() => void feed.flush(), FEED_INTERVAL_MS);
4485
4829
  const record = journal(feed, tools.print);
4486
- const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE));
4830
+ const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE2));
4487
4831
  const { input } = messages;
4488
4832
  run2.signal.addEventListener("abort", input.close, { once: true });
4489
4833
  void messages.poll();
@@ -4494,7 +4838,7 @@ async function runJob(job, tools, stop) {
4494
4838
  } catch (error) {
4495
4839
  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 };
4496
4840
  } finally {
4497
- await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
4841
+ await rm7(roleLibraryFolder(tools.dataFolder, job.runId), { recursive: true, force: true });
4498
4842
  clearInterval(polling);
4499
4843
  clearInterval(feeding);
4500
4844
  stop.removeEventListener("abort", stopRun);
@@ -4505,8 +4849,8 @@ async function runJob(job, tools, stop) {
4505
4849
  await messages.poll();
4506
4850
  }
4507
4851
  await feed.flush();
4508
- if (run2.signal.reason === CLOSED_ON_SITE) {
4509
- await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
4852
+ if (run2.signal.reason === CLOSED_ON_SITE2) {
4853
+ await rm7(recordingsFolder(tools.dataFolder, job.runId), { recursive: true, force: true });
4510
4854
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
4511
4855
  return;
4512
4856
  }
@@ -4519,33 +4863,36 @@ async function runJob(job, tools, stop) {
4519
4863
  }
4520
4864
  }
4521
4865
  var CHECK_TOO_LONG = "check_too_long";
4522
- function checkFailure(error, signal) {
4523
- return signal.reason === CHECK_TOO_LONG ? `The check took longer than ${CHECK_DEADLINE_MS / 6e4} minutes.` : failureReport(error, signal);
4866
+ function checkFailure(error, signal, deadlineMs) {
4867
+ return signal.reason === CHECK_TOO_LONG ? `The run took longer than ${deadlineMs / 6e4} minutes.` : failureReport(error, signal);
4524
4868
  }
4525
4869
  async function runCheckJob(check, runCheck, tools, stop) {
4526
- tools.print(`Schedule check: ${check.title}${check.taskId === null ? "" : ` (task #${check.taskId})`}`);
4870
+ const routine = routineRunTools(check, tools.api, tools.print, tools.sleep);
4871
+ tools.print(routine ? `Routine: ${check.title}${check.routine?.dry ? " (test run)" : ""}` : `Schedule check: ${check.title}${check.taskId === null ? "" : ` (task #${check.taskId})`}`);
4872
+ const deadlineMs = routine ? ROUTINE_DEADLINE_MS : CHECK_DEADLINE_MS;
4527
4873
  const run2 = new AbortController();
4528
- const stopRun = () => run2.abort(STOPPED_HERE);
4874
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4529
4875
  if (stop.aborted) {
4530
4876
  stopRun();
4531
4877
  }
4532
4878
  stop.addEventListener("abort", stopRun, { once: true });
4533
- const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG), CHECK_DEADLINE_MS);
4534
- const beat = checkHeartbeat(tools.api, check, () => run2.abort(CLOSED_ON_SITE));
4879
+ const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG), deadlineMs);
4880
+ const beat = checkHeartbeat(tools.api, check, () => run2.abort(CLOSED_ON_SITE2));
4535
4881
  const beating = setInterval(() => void beat(), CHECK_HEARTBEAT_MS);
4536
4882
  let body;
4537
4883
  try {
4538
- const folder = await checkFolder(tools.home, check);
4539
- const env = withNodeOnPath(process6.env, dirname5(process6.execPath));
4540
- body = checkFinishBody(await runCheck({ cwd: folder, prompt: buildCheckPrompt(check, folder), model: check.model, env, signal: run2.signal }));
4884
+ const folder = await checkFolder(tools.dataFolder, check);
4885
+ const env = withNodeOnPath(process7.env, dirname6(process7.execPath));
4886
+ const prompt = routine ? buildRoutinePrompt(check, folder) : buildCheckPrompt(check, folder);
4887
+ body = checkFinishBody(await runCheck({ cwd: folder, prompt, model: check.model, env, signal: run2.signal, sources: sourceTools(check.sourceTools, tools.api, checkPath(check.checkId, "sources")), routine }));
4541
4888
  } catch (error) {
4542
- body = { outcome: "failed", report: checkFailure(error, run2.signal), model: null, usage: null, result: null };
4889
+ body = { outcome: "failed", report: checkFailure(error, run2.signal, deadlineMs), model: null, usage: null, result: null };
4543
4890
  } finally {
4544
4891
  clearTimeout(deadline);
4545
4892
  clearInterval(beating);
4546
4893
  stop.removeEventListener("abort", stopRun);
4547
4894
  }
4548
- if (run2.signal.reason === CLOSED_ON_SITE) {
4895
+ if (run2.signal.reason === CLOSED_ON_SITE2) {
4549
4896
  tools.print("TAMsense closed this check while it was in progress. The result was not sent.");
4550
4897
  return;
4551
4898
  }
@@ -4606,9 +4953,11 @@ async function work(tools, stop, parallel = 1) {
4606
4953
  const stopAll = AbortSignal.any([stop, halt.signal]);
4607
4954
  const running = /* @__PURE__ */ new Set();
4608
4955
  const checks = /* @__PURE__ */ new Set();
4956
+ const senses = /* @__PURE__ */ new Set();
4609
4957
  const folderLock = keyedLock();
4610
4958
  const scheduleLock = keyedLock();
4611
4959
  const runCheck = tools.version ? tools.runCheck : void 0;
4960
+ const runSense = tools.version ? tools.runSense : void 0;
4612
4961
  const reporting = reportCapabilities(tools, stopAll);
4613
4962
  const checking = tools.inspectFolder ? checkFolders({ ...tools, inspect: tools.inspectFolder }, stopAll) : Promise.resolve();
4614
4963
  if (tools.startFolder) {
@@ -4623,19 +4972,26 @@ async function work(tools, stop, parallel = 1) {
4623
4972
  const task = scheduleLock.run(`schedule-${check.scheduleId}`, () => runCheckJob(check, runner, tools, stopAll)).catch((error) => tools.print(`The check stopped: ${messageOf3(error)}`)).finally(() => checks.delete(task));
4624
4973
  checks.add(task);
4625
4974
  };
4975
+ const startSense = (job, runner) => {
4976
+ const task = runSenseJob(job, runner, tools, stopAll).catch(() => {
4977
+ }).finally(() => senses.delete(task));
4978
+ senses.add(task);
4979
+ };
4626
4980
  let failures = 0;
4627
4981
  try {
4628
4982
  while (!stopAll.aborted) {
4629
4983
  const jobsFull = running.size >= parallel;
4630
4984
  const checksFull = !runCheck || checks.size >= MAX_CHECKS;
4631
- if (jobsFull && checksFull) {
4632
- await Promise.race([...running, ...checks]);
4985
+ const senseFull = !runSense || senses.size >= MAX_SENSE;
4986
+ if (jobsFull && checksFull && senseFull) {
4987
+ await Promise.race([...running, ...checks, ...senses]);
4633
4988
  continue;
4634
4989
  }
4635
- const body = runCheck && tools.version ? { version: tools.version, jobs: !jobsFull, checks: !checksFull } : void 0;
4636
- let reply4;
4990
+ const named = tools.version && (runCheck || runSense) ? tools.version : null;
4991
+ const body = named ? { version: named, jobs: !jobsFull, checks: !checksFull, ...runSense ? { sense: !senseFull } : {} } : void 0;
4992
+ let reply5;
4637
4993
  try {
4638
- reply4 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
4994
+ reply5 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
4639
4995
  } catch (error) {
4640
4996
  if (isFatal(error)) {
4641
4997
  halt.abort();
@@ -4649,65 +5005,80 @@ async function work(tools, stop, parallel = 1) {
4649
5005
  continue;
4650
5006
  }
4651
5007
  failures = 0;
4652
- if (reply4.check && runCheck) {
4653
- startCheck(reply4.check, runCheck);
4654
- } else if (reply4.job) {
4655
- start(reply4.job);
5008
+ if (reply5.sense && runSense) {
5009
+ startSense(reply5.sense, runSense);
5010
+ } else if (reply5.check && runCheck) {
5011
+ startCheck(reply5.check, runCheck);
5012
+ } else if (reply5.job) {
5013
+ start(reply5.job);
4656
5014
  } else {
4657
5015
  await tools.sleep(POLL_INTERVAL_MS, stopAll);
4658
5016
  }
4659
5017
  }
4660
5018
  } finally {
4661
- await Promise.allSettled([...running, ...checks, reporting, checking]);
5019
+ await Promise.allSettled([...running, ...checks, ...senses, reporting, checking]);
4662
5020
  }
4663
5021
  }
4664
5022
 
4665
5023
  // src/cli.ts
4666
- var NOT_CONNECTED_TEXT = `This computer is not connected. Run ${LOGIN_COMMAND}`;
4667
- var DISCONNECTED_TEXT = `This computer was disconnected on the website. Run ${LOGIN_COMMAND}`;
4668
5024
  var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the project owner.";
4669
- var ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
5025
+ var UNAUTHORIZED_STATUS = 401;
5026
+ var FORBIDDEN_STATUS = 403;
4670
5027
  var INTERRUPTED_EXIT_CODE = 130;
5028
+ function connectionFor(profile) {
5029
+ const dataFolder = dataFolderFor(homedir2(), profile);
5030
+ return { profile, dataFolder, configFile: configPath(dataFolder) };
5031
+ }
5032
+ function notConnectedText(profile) {
5033
+ const which = profile === null ? "" : ` in the profile ${profile}`;
5034
+ return `This computer is not connected${which}. Run ${withProfile(LOGIN_COMMAND, profile)}`;
5035
+ }
4671
5036
  function print(line) {
4672
- process7.stdout.write(`${line}
5037
+ process8.stdout.write(`${line}
4673
5038
  `);
4674
5039
  }
4675
5040
  function serverFor(flag, saved) {
4676
- return serverOrigin(flag ?? process7.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
5041
+ return serverOrigin(flag ?? process8.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
4677
5042
  }
4678
- function accessProblem(error) {
4679
- return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
5043
+ function accessProblem(error, profile) {
5044
+ if (!(error instanceof ApiError)) {
5045
+ return null;
5046
+ }
5047
+ if (error.status === UNAUTHORIZED_STATUS) {
5048
+ return `This computer was disconnected on the website. Run ${withProfile(LOGIN_COMMAND, profile)}`;
5049
+ }
5050
+ return error.status === FORBIDDEN_STATUS ? NO_TASKS_ACCESS_TEXT : null;
4680
5051
  }
4681
- async function forgetDevice(config) {
5052
+ async function forgetDevice(config, connection) {
4682
5053
  await createApi(config.server, config.token).request("DELETE", IDENTITY_PATH).catch(() => {
4683
5054
  });
4684
- await removeConfig();
5055
+ await removeConfig(connection.configFile);
4685
5056
  }
4686
- async function runLogin(server, deviceName) {
4687
- const saved = await readConfig();
5057
+ async function runLogin(connection, server, deviceName) {
5058
+ const saved = await readConfig(connection.configFile);
4688
5059
  const api = createApi(serverFor(server, saved));
4689
5060
  if (saved) {
4690
- await forgetDevice(saved);
5061
+ await forgetDevice(saved, connection);
4691
5062
  }
4692
- const name = deviceName?.trim() || defaultDeviceName();
5063
+ const name = deviceName?.trim() || defaultDeviceName(connection.profile);
4693
5064
  const result = await login(
4694
5065
  { api, openBrowser, print, sleep, now: Date.now },
4695
5066
  { name, platform: platformLabel() }
4696
5067
  );
4697
- await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name });
4698
- print(`Connected: ${result.device.name}. To take your tasks, go to your code folder and run ${START_COMMAND}`);
5068
+ await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name }, connection.configFile);
5069
+ print(`Connected: ${result.device.name}. To take your tasks, go to your code folder and run ${withProfile(START_COMMAND, connection.profile)}`);
4699
5070
  }
4700
5071
  function stopOnInterrupt() {
4701
5072
  const stop = new AbortController();
4702
- process7.once("SIGINT", () => {
5073
+ process8.once("SIGINT", () => {
4703
5074
  print("Stopping\u2026 Press Ctrl+C again to quit at once.");
4704
5075
  stop.abort();
4705
- process7.once("SIGINT", () => process7.exit(INTERRUPTED_EXIT_CODE));
5076
+ process8.once("SIGINT", () => process8.exit(INTERRUPTED_EXIT_CODE));
4706
5077
  });
4707
5078
  return stop.signal;
4708
5079
  }
4709
5080
  function installTools(home) {
4710
- return { home, nodePath: process7.execPath, env: process7.env, print, run: runProcess, exists: existsSync8 };
5081
+ return { home, nodePath: process8.execPath, env: process8.env, print, run: runProcess, exists: existsSync8 };
4711
5082
  }
4712
5083
  async function browserForStart(home, install) {
4713
5084
  const installed = await installedBrowser(home);
@@ -4717,41 +5088,41 @@ async function browserForStart(home, install) {
4717
5088
  return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
4718
5089
  }
4719
5090
  async function startedNewVersion() {
4720
- const update = await updateToRun(process7.env, currentVersion(), () => latestVersion());
5091
+ const update = await updateToRun(process8.env, currentVersion(), () => latestVersion());
4721
5092
  if (!update) {
4722
5093
  return false;
4723
5094
  }
4724
5095
  print(`TAMsense runner ${update.latest} is out, this is ${update.current}. Starting the new version...`);
4725
5096
  try {
4726
- process7.exitCode = await relaunch(process7.argv.slice(2), process7.env);
5097
+ process8.exitCode = await relaunch(process8.argv.slice(2), process8.env);
4727
5098
  return true;
4728
5099
  } catch {
4729
5100
  print("Could not start the new version. Going on with this one.");
4730
5101
  return false;
4731
5102
  }
4732
5103
  }
4733
- async function runStart(parallel, installsBrowser) {
5104
+ async function runStart(connection, parallel, installsBrowser) {
4734
5105
  if (await startedNewVersion()) {
4735
5106
  return;
4736
5107
  }
4737
- const saved = await readConfig();
5108
+ const saved = await readConfig(connection.configFile);
4738
5109
  if (!saved) {
4739
- print(NOT_CONNECTED_TEXT);
5110
+ print(notConnectedText(connection.profile));
4740
5111
  return;
4741
5112
  }
4742
5113
  const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
4743
- const problem2 = teamKeyProblem(identity.device.team, process7.env);
5114
+ const problem2 = teamKeyProblem(identity.device.team, process8.env);
4744
5115
  if (problem2) {
4745
5116
  throw new Error(problem2);
4746
5117
  }
4747
5118
  print(`${identity.device.name} is connected to ${saved.server} as ${describeDevice(identity)}.`);
4748
- const home = homedir3();
5119
+ const home = homedir2();
4749
5120
  const browser = await browserForStart(home, installsBrowser);
4750
5121
  const api = createApi(saved.server, saved.token, { watch: connectionWatch(print) });
4751
- await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process7.cwd() }, stopOnInterrupt(), parallel);
5122
+ await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, runSense: runClaudeSense, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, dataFolder: connection.dataFolder, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process8.cwd() }, stopOnInterrupt(), parallel);
4752
5123
  }
4753
5124
  async function runBrowser(remove) {
4754
- const home = homedir3();
5125
+ const home = homedir2();
4755
5126
  if (remove) {
4756
5127
  print(await removeBrowser(home) ? "Removed the headless browser. The next start installs it again, unless you add --no-browser." : "No headless browser is installed.");
4757
5128
  return;
@@ -4764,46 +5135,53 @@ async function runBrowser(remove) {
4764
5135
  await installBrowser(installTools(home));
4765
5136
  print("Claude uses it for interface checks from the next start.");
4766
5137
  }
4767
- async function runStatus() {
4768
- const saved = await readConfig();
5138
+ async function runStatus(connection) {
5139
+ const saved = await readConfig(connection.configFile);
4769
5140
  if (!saved) {
4770
- print(NOT_CONNECTED_TEXT);
5141
+ print(notConnectedText(connection.profile));
4771
5142
  return;
4772
5143
  }
4773
5144
  const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
4774
5145
  print(`Connected: ${identity.device.name}, ${describeDevice(identity)}`);
4775
5146
  print(`Account: ${identity.email ?? "unknown"}, project ${identity.projectName}`);
4776
5147
  print(`Server: ${saved.server}`);
5148
+ if (connection.profile !== null) {
5149
+ print(`Profile: ${connection.profile}, folders in ${connection.dataFolder}`);
5150
+ }
4777
5151
  }
4778
- async function runLogout() {
4779
- const saved = await readConfig();
5152
+ async function runLogout(connection) {
5153
+ const saved = await readConfig(connection.configFile);
4780
5154
  if (!saved) {
4781
- print(NOT_CONNECTED_TEXT);
5155
+ print(notConnectedText(connection.profile));
4782
5156
  return;
4783
5157
  }
4784
- await forgetDevice(saved);
5158
+ await forgetDevice(saved, connection);
4785
5159
  print("Disconnected. This computer no longer takes your tasks.");
4786
5160
  }
4787
5161
  async function run(command) {
4788
5162
  if (command.name === "login") {
4789
- await runLogin(command.server, command.deviceName);
5163
+ await runLogin(connectionFor(command.profile), command.server, command.deviceName);
4790
5164
  } else if (command.name === "start") {
4791
- await runStart(command.parallel, command.browser);
5165
+ await runStart(connectionFor(command.profile), command.parallel, command.browser);
4792
5166
  } else if (command.name === "browser") {
4793
5167
  await runBrowser(command.remove);
4794
5168
  } else if (command.name === "status") {
4795
- await runStatus();
5169
+ await runStatus(connectionFor(command.profile));
4796
5170
  } else if (command.name === "logout") {
4797
- await runLogout();
5171
+ await runLogout(connectionFor(command.profile));
4798
5172
  } else {
4799
5173
  print(HELP_TEXT);
4800
5174
  }
4801
5175
  }
5176
+ function fail(error, profile) {
5177
+ process8.stderr.write(`${accessProblem(error, profile) ?? (error instanceof Error ? error.message : String(error))}
5178
+ `);
5179
+ process8.exitCode = 1;
5180
+ }
4802
5181
  try {
4803
- await run(parseCommand(process7.argv.slice(2), process7.env));
5182
+ const command = parseCommand(process8.argv.slice(2), process8.env);
5183
+ await run(command).catch((error) => fail(error, "profile" in command ? command.profile : null));
4804
5184
  } catch (error) {
4805
- process7.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
4806
- `);
4807
- process7.exitCode = 1;
5185
+ fail(error, null);
4808
5186
  }
4809
5187
  //# sourceMappingURL=cli.js.map