@tamsensedev/runner 0.13.0 → 0.18.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 +61 -57
  3. package/dist/cli.js +646 -315
  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
@@ -3,7 +3,7 @@
3
3
  // src/cli.ts
4
4
  import { existsSync as existsSync8 } from "fs";
5
5
  import { homedir as homedir3 } from "os";
6
- import process7 from "process";
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) {
@@ -517,8 +517,8 @@ async function probeCapabilities(stop) {
517
517
  }
518
518
 
519
519
  // 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";
520
+ import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
521
+ import { z as z7 } from "zod";
522
522
 
523
523
  // src/askPerson.ts
524
524
  var MAX_ASK_TRIES = 5;
@@ -578,21 +578,21 @@ function shorten(text2) {
578
578
  const line = text2.replace(/\s+/g, " ").trim();
579
579
  return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
580
580
  }
581
- function activityLine(tool6, input) {
581
+ function activityLine(tool8, input) {
582
582
  const fields = typeof input === "object" && input !== null ? input : {};
583
- if (tool6 === "Bash" && typeof fields.command === "string") {
583
+ if (tool8 === "Bash" && typeof fields.command === "string") {
584
584
  return `$ ${shorten(fields.command)}`;
585
585
  }
586
- if (FILE_TOOLS.has(tool6) && typeof fields.file_path === "string") {
587
- return `${tool6} ${shorten(fields.file_path)}`;
586
+ if (FILE_TOOLS.has(tool8) && typeof fields.file_path === "string") {
587
+ return `${tool8} ${shorten(fields.file_path)}`;
588
588
  }
589
- if (tool6 === "Skill" && typeof fields.skill === "string") {
589
+ if (tool8 === "Skill" && typeof fields.skill === "string") {
590
590
  return `Skill ${shorten(fields.skill)}`;
591
591
  }
592
- if (HELPER_TOOLS.has(tool6) && typeof fields.subagent_type === "string") {
592
+ if (HELPER_TOOLS.has(tool8) && typeof fields.subagent_type === "string") {
593
593
  return `Agent ${shorten(fields.subagent_type)}`;
594
594
  }
595
- return tool6;
595
+ return tool8;
596
596
  }
597
597
  function usageOf(modelUsage) {
598
598
  const entries = Object.values(modelUsage);
@@ -650,6 +650,12 @@ function recordingUploadPath(runId, upload, part) {
650
650
  function checkPath(checkId, action) {
651
651
  return `/api/runner/checks/${checkId}/${action}`;
652
652
  }
653
+ function routinePath(checkId, action) {
654
+ return `/api/runner/checks/${checkId}/${action}`;
655
+ }
656
+ function sensePath(senseId, action) {
657
+ return `/api/runner/sense/${senseId}/${action}`;
658
+ }
653
659
 
654
660
  // src/feed.ts
655
661
  var MAX_TEXT_LENGTH = 8e3;
@@ -670,31 +676,52 @@ function textField(fields, name) {
670
676
  return typeof value === "string" && value.trim() ? value : null;
671
677
  }
672
678
  function toolDraft(block, parent) {
673
- const tool6 = (block.name ?? "tool").replace(MCP_PREFIX, "");
679
+ const tool8 = (block.name ?? "tool").replace(MCP_PREFIX, "");
674
680
  const fields = fieldsOf(block.input);
675
- const base = { tool: tool6, ref: block.id ?? null, parent, failed: false };
676
- if (tool6 === "Skill" && textField(fields, "skill")) {
681
+ const base = { tool: tool8, ref: block.id ?? null, parent, failed: false };
682
+ if (tool8 === "Skill" && textField(fields, "skill")) {
677
683
  return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
678
684
  }
679
- if (HELPER_TOOLS2.has(tool6)) {
685
+ if (HELPER_TOOLS2.has(tool8)) {
680
686
  const who = textField(fields, "subagent_type") ?? "general-purpose";
681
687
  const what = textField(fields, "description");
682
688
  return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
683
689
  }
684
- if (tool6 === "submit_verdict") {
690
+ if (tool8 === "submit_verdict") {
685
691
  const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
686
692
  return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
687
693
  }
688
- if (tool6 === "hand_off") {
694
+ if (tool8 === "hand_off") {
689
695
  return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
690
696
  }
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) };
697
+ if (tool8 in ATTACH_LABELS) {
698
+ return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool8]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
693
699
  }
694
700
  const command = textField(fields, "command");
695
- const summary = tool6 === "Bash" && command ? `$ ${command}` : activityLine(tool6, block.input);
701
+ const summary = tool8 === "Bash" && command ? `$ ${command}` : activityLine(tool8, block.input);
696
702
  return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
697
703
  }
704
+ function sourceCalls(tools = []) {
705
+ return { tools: new Set(tools.map((tool8) => tool8.name)), refs: /* @__PURE__ */ new Set() };
706
+ }
707
+ function sourceCallDraft(block, draft) {
708
+ return { ...draft, text: clip(`${draft.tool} ${JSON.stringify(block.input ?? {})}`, MAX_TEXT_LENGTH) };
709
+ }
710
+ function fromSource(calls, block) {
711
+ return block.tool_use_id !== void 0 && calls.refs.has(block.tool_use_id);
712
+ }
713
+ function sourceSummary(text2) {
714
+ let reply5;
715
+ try {
716
+ reply5 = fieldsOf(JSON.parse(text2));
717
+ } catch {
718
+ return JSON.stringify({ ok: false });
719
+ }
720
+ if (reply5.ok === false) {
721
+ return JSON.stringify(reply5.refused === true ? { ok: false, refused: true, error: reply5.error } : { ok: false });
722
+ }
723
+ return JSON.stringify(typeof reply5.rowCount === "number" ? { ok: true, rowCount: reply5.rowCount } : { ok: true });
724
+ }
698
725
  function resultText(content) {
699
726
  if (typeof content === "string") {
700
727
  return content;
@@ -708,7 +735,7 @@ function helperEndDraft(ref, parent, end, text2) {
708
735
  const stopped = end === "stopped";
709
736
  return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
710
737
  }
711
- function feedOf(message) {
738
+ function feedOf(message, calls = sourceCalls()) {
712
739
  if (message.type !== "assistant" && message.type !== "user") {
713
740
  return [];
714
741
  }
@@ -724,9 +751,15 @@ function feedOf(message) {
724
751
  } else if (message.type === "assistant" && block.type === "text" && block.text?.trim()) {
725
752
  drafts.push({ kind: "text", text: clip(block.text, MAX_TEXT_LENGTH), tool: null, ref: null, parent, failed: false });
726
753
  } else if (message.type === "assistant" && block.type === "tool_use") {
727
- drafts.push(toolDraft(block, parent));
754
+ const draft = toolDraft(block, parent);
755
+ const isSource = draft.tool !== null && calls.tools.has(draft.tool);
756
+ if (isSource && block.id) {
757
+ calls.refs.add(block.id);
758
+ }
759
+ drafts.push(isSource ? sourceCallDraft(block, draft) : draft);
728
760
  } 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 });
761
+ const text2 = resultText(block.content);
762
+ 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
763
  }
731
764
  }
732
765
  return drafts;
@@ -1231,8 +1264,8 @@ function messageOf2(error) {
1231
1264
  async function decisionOf(tools, first, signal) {
1232
1265
  const now = tools.now ?? Date.now;
1233
1266
  const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
1234
- let reply4 = first;
1235
- while (reply4.decision === "wait") {
1267
+ let reply5 = first;
1268
+ while (reply5.decision === "wait") {
1236
1269
  await tools.sleep(CHECK_INTERVAL_MS, signal);
1237
1270
  if (signal.aborted) {
1238
1271
  return STOPPED_TEXT2;
@@ -1240,9 +1273,9 @@ async function decisionOf(tools, first, signal) {
1240
1273
  if (now() > giveUpAt) {
1241
1274
  return SITE_SILENT_TEXT;
1242
1275
  }
1243
- reply4 = await tools.check(reply4.id).catch(() => reply4);
1276
+ reply5 = await tools.check(reply5.id).catch(() => reply5);
1244
1277
  }
1245
- return reply4;
1278
+ return reply5;
1246
1279
  }
1247
1280
  function askPermission(tools, lastWords, folder) {
1248
1281
  return async (toolName, input, options) => {
@@ -1263,11 +1296,11 @@ function askPermission(tools, lastWords, folder) {
1263
1296
  } catch (error) {
1264
1297
  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
1298
  }
1266
- const reply4 = await decisionOf(tools, first, options.signal);
1267
- if (typeof reply4 === "string") {
1268
- return deny(reply4);
1299
+ const reply5 = await decisionOf(tools, first, options.signal);
1300
+ if (typeof reply5 === "string") {
1301
+ return deny(reply5);
1269
1302
  }
1270
- return reply4.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply4.message);
1303
+ return reply5.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply5.message);
1271
1304
  };
1272
1305
  }
1273
1306
  var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
@@ -1447,7 +1480,7 @@ async function checkFolders(tools, stop) {
1447
1480
  }
1448
1481
  }
1449
1482
  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.";
1483
+ 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
1484
  async function childRepositories(start) {
1452
1485
  const entries = await readdir(start, { withFileTypes: true }).catch(() => []);
1453
1486
  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,6 +1548,7 @@ function keyedLock() {
1515
1548
  import { existsSync as existsSync3 } from "fs";
1516
1549
  import { rm as rm2 } from "fs/promises";
1517
1550
  import { join as join3, resolve as resolve2 } from "path";
1551
+ var RESULTS_FOLDER = "results";
1518
1552
  var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
1519
1553
  var cacheLock = keyedLock();
1520
1554
  function githubRemoteUrl(fullName) {
@@ -1529,7 +1563,10 @@ function folderNames(repositories) {
1529
1563
  for (const repository of repositories) {
1530
1564
  counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
1531
1565
  }
1532
- return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
1566
+ return repositories.map((repository) => {
1567
+ const name = shortName(repository.fullName);
1568
+ return counts.get(name) === 1 && name !== RESULTS_FOLDER ? name : repository.fullName.replace("/", "__");
1569
+ });
1533
1570
  }
1534
1571
  async function refreshCache(cache, repository, token, tools) {
1535
1572
  if (existsSync3(join3(cache, ".git"))) {
@@ -1686,7 +1723,7 @@ async function baseBranchRef(source, branch, remote) {
1686
1723
  }
1687
1724
  const found = own ?? fetched;
1688
1725
  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.`);
1726
+ 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
1727
  }
1691
1728
  return found;
1692
1729
  }
@@ -1699,7 +1736,7 @@ async function taskBranchRef(source, branch, remote, base) {
1699
1736
  async function startRef(source, job, local) {
1700
1737
  const base = await baseBranchRef(source, local.baseBranch, local.remote);
1701
1738
  const task = await taskBranchRef(source, job.branch, local.remote, base);
1702
- if (task || !isReadOnlyCheck(job)) {
1739
+ if (task || !isReadOnlyCheck(job) || job.severalBranches) {
1703
1740
  return task ?? base;
1704
1741
  }
1705
1742
  const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
@@ -1734,7 +1771,7 @@ function sourceOf(repository) {
1734
1771
  async function prepareRepository(job, repository, path, tools) {
1735
1772
  const source = sourceOf(repository);
1736
1773
  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.`);
1774
+ 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
1775
  }
1739
1776
  const local = { source, remote: await remoteOf(source), baseBranch: repository.defaultBranch };
1740
1777
  if (local.remote) {
@@ -1793,8 +1830,8 @@ async function commitAndPush(repository, local, job, identity) {
1793
1830
  }
1794
1831
  const base = await baseBranchRef(repository.path, local.baseBranch, local.remote);
1795
1832
  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 };
1833
+ const sent3 = await pushBranch(repository.path, local, job.branch);
1834
+ return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent3 };
1798
1835
  }
1799
1836
  async function publishLocalWorkspace(workspace, job, identity) {
1800
1837
  const pushed = [];
@@ -1830,7 +1867,6 @@ var SAVE_TABLE_TOOL_NAME = "save_result_table";
1830
1867
  var ATTACH_FILE_TOOL_NAME = "attach_result_file";
1831
1868
  var ADD_LINK_TOOL_NAME = "add_result_link";
1832
1869
  var HAND_OVER_TOOL_NAME = "hand_over";
1833
- var RESULTS_FOLDER = "results";
1834
1870
  var MAX_NAME_LENGTH = 60;
1835
1871
  var TYPES_BY_EXTENSION = {
1836
1872
  ".pdf": "application/pdf",
@@ -1997,11 +2033,11 @@ function resultToolset(tools) {
1997
2033
  function handOverTool(tools) {
1998
2034
  return tool2(
1999
2035
  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.`,
2036
+ `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
2037
  {
2002
2038
  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"),
2039
+ title: z2.string().describe("title of the new task, one plain sentence"),
2040
+ note: z2.string().describe("what you found and what the new task should do; never names, emails or other personal data of clients"),
2005
2041
  wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
2006
2042
  },
2007
2043
  async (args) => await sent(() => tools.send(args))
@@ -2011,56 +2047,205 @@ function handOverTool(tools) {
2011
2047
  // src/schedules.ts
2012
2048
  import { existsSync as existsSync5 } from "fs";
2013
2049
  import { mkdir as mkdir3 } from "fs/promises";
2014
- import { createSdkMcpServer, query as query2, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2050
+ import { createSdkMcpServer, query as query2, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
2051
+ import { z as z5 } from "zod";
2052
+
2053
+ // src/routines.ts
2054
+ import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2015
2055
  import { z as z3 } from "zod";
2056
+ var FINISH_ROUTINE_TOOL_NAME = "finish_routine";
2057
+ var RULES = [
2058
+ "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.",
2059
+ "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.",
2060
+ "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.",
2061
+ `Finish by calling ${FINISH_ROUTINE_TOOL_NAME} exactly once, after every problem is reported.`,
2062
+ "Write in the language of the job. No em dashes and no en dashes."
2063
+ ];
2064
+ function reply2(text2) {
2065
+ return { content: [{ type: "text", text: text2 }] };
2066
+ }
2067
+ var KEY_FIELD = z3.string().describe('a short stable name of this problem, the same on every run, like "buyout-4812-card-declined"');
2068
+ async function sent2(tools, body) {
2069
+ try {
2070
+ return reply2((await tools.call(body)).text);
2071
+ } catch (error) {
2072
+ return { ...reply2(`Could not reach TAMsense: ${error instanceof Error ? error.message : String(error)}. Say so in your summary.`), isError: true };
2073
+ }
2074
+ }
2075
+ function routineToolset(tools, onFinish) {
2076
+ 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.", {
2077
+ title: z3.string().describe("one plain sentence: what is wrong"),
2078
+ detail: z3.string().describe("the facts people need to act; never personal data of clients"),
2079
+ key: KEY_FIELD,
2080
+ sameAs: z3.number().int().nullable().describe("the number of an open task of this routine about the same problem, or null")
2081
+ }, async (args) => await sent2(tools, { tool: "problem", input: args }));
2082
+ 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.", {
2083
+ question: z3.string(),
2084
+ context: z3.string().describe("the facts they need to answer without opening anything"),
2085
+ key: KEY_FIELD,
2086
+ options: z3.array(z3.string()).max(4).describe("up to four answers you suggest")
2087
+ }, async (args) => await sent2(tools, { tool: "ask", input: args }));
2088
+ const fixed = tool3("report_fixed", "Write down one fix you made in this run: what you changed and why.", {
2089
+ title: z3.string().describe('what you did, like "Cancelled buyout #4815 on Wildberries"'),
2090
+ detail: z3.string().describe("why, with the facts"),
2091
+ key: KEY_FIELD
2092
+ }, async (args) => await sent2(tools, { tool: "fixed", input: args }));
2093
+ const report = tool3("save_report", "Save the report of this run on TAMsense: a Markdown document or a CSV table with a header row.", {
2094
+ kind: z3.enum(["document", "table"]),
2095
+ title: z3.string(),
2096
+ content: z3.string().describe("the full Markdown text, or the full CSV")
2097
+ }, async (args) => await sent2(tools, { tool: "report", input: args }));
2098
+ const finish2 = tool3(FINISH_ROUTINE_TOOL_NAME, "Finish this run. Call it exactly once, at the very end.", {
2099
+ summary: z3.string().describe("one line with the numbers that matter"),
2100
+ fine: z3.number().int().min(0).describe("how many things you checked and found in order"),
2101
+ fineNote: z3.string().describe("what those are, in a few words")
2102
+ }, async (args) => {
2103
+ await sent2(tools, { tool: "fine", input: { count: args.fine, note: args.fineNote } });
2104
+ onFinish({ changed: false, summary: args.summary.trim(), note: null, conditionMet: false, callPerson: false });
2105
+ return reply2("Run finished. You can stop now.");
2106
+ });
2107
+ return [problem2, ...tools.asks ? [ask] : [], ...tools.canFix ? [fixed] : [], report, finish2];
2108
+ }
2109
+ function noteLine(note) {
2110
+ return `- ${note.at.slice(0, 16).replace("T", " ")}: ${note.summary}`;
2111
+ }
2112
+ function buildRoutinePrompt(job, folder) {
2113
+ const earlier = job.earlier.length > 0 ? `Earlier runs, the newest last:
2114
+ ${job.earlier.map(noteLine).join("\n")}` : null;
2115
+ return {
2116
+ prompt: [job.instruction, earlier].filter(Boolean).join("\n\n"),
2117
+ instructions: [RULES.join("\n"), workplaceText(job.computer, folder), job.sourcesText, job.safetyRules.trim() || null].filter(Boolean).join("\n\n")
2118
+ };
2119
+ }
2120
+ function routineRunTools(job, api, print2, sleep2) {
2121
+ const routine = job.routine;
2122
+ if (!routine) {
2123
+ return null;
2124
+ }
2125
+ const permissions = routine.canFix && !routine.dry ? {
2126
+ send: async (body) => {
2127
+ const answer = await api.request("POST", routinePath(job.checkId, "permissions"), body);
2128
+ if (answer.decision === "wait") {
2129
+ print2(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
2130
+ }
2131
+ return answer;
2132
+ },
2133
+ check: (id) => api.request("GET", routinePath(job.checkId, `permissions/${id}`)),
2134
+ sleep: sleep2
2135
+ } : null;
2136
+ return {
2137
+ call: (body) => api.request("POST", routinePath(job.checkId, "routine"), body),
2138
+ permissions,
2139
+ asks: routine.asks,
2140
+ canFix: routine.canFix
2141
+ };
2142
+ }
2143
+
2144
+ // src/sources.ts
2145
+ import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2146
+ import { z as z4 } from "zod";
2147
+ function sourceTools(tools, api, path) {
2148
+ return tools && tools.length > 0 ? { tools, call: (body) => api.request("POST", path, body) } : null;
2149
+ }
2150
+ function fieldSchema(field) {
2151
+ const value = field.type === "integer" ? z4.number().int().positive() : z4.string();
2152
+ return (field.required ? value : value.optional()).describe(field.description);
2153
+ }
2154
+ function inputShape(fields) {
2155
+ return Object.fromEntries(fields.map((field) => [field.name, fieldSchema(field)]));
2156
+ }
2157
+ function replyOf(answer) {
2158
+ return { content: [{ type: "text", text: JSON.stringify(answer) }], ...answer.ok ? {} : { isError: true } };
2159
+ }
2160
+ function sourceToolset(sources) {
2161
+ if (!sources) {
2162
+ return [];
2163
+ }
2164
+ return sources.tools.map((info) => tool4(
2165
+ info.name,
2166
+ info.description,
2167
+ inputShape(info.fields),
2168
+ async (input) => {
2169
+ try {
2170
+ return replyOf(await sources.call({ tool: info.name, input }));
2171
+ } catch (error) {
2172
+ return replyOf({ ok: false, error: `Could not read project data: ${error instanceof Error ? error.message : String(error)}` });
2173
+ }
2174
+ }
2175
+ ));
2176
+ }
2177
+
2178
+ // src/schedules.ts
2016
2179
  var PROPOSE_SCHEDULE_TOOL_NAME = "propose_schedule";
2017
2180
  var REPORT_CHECK_TOOL_NAME = "report_check";
2181
+ var WAKE_TOOL_NAME = "wake_up_later";
2018
2182
  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
2183
  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) {
2184
+ function reply3(text2, isError = false) {
2021
2185
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2022
2186
  }
2023
2187
  function proposeScheduleTool(tools, waiting) {
2024
- return tool3(
2188
+ return tool5(
2025
2189
  PROPOSE_SCHEDULE_TOOL_NAME,
2026
2190
  "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
2191
  {
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")
2192
+ title: z5.string().describe('a short name, at most 80 characters: "Wait for the local stand"'),
2193
+ 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"),
2194
+ everyMinutes: z5.number().int().nullable().describe("repeat every N minutes (60 for every hour); null when dailyAt is set"),
2195
+ dailyAt: z5.string().nullable().describe("repeat every day at this time, HH:MM in the project time zone; null when everyMinutes is set"),
2196
+ days: z5.array(z5.number().int().min(0).max(6)).describe("with dailyAt: days of the week, 0 is Sunday; empty means every day"),
2197
+ 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'),
2198
+ forMinutes: z5.number().int().nullable().describe("how long it runs at most, in minutes; null takes the longest the project allows"),
2199
+ maxRuns: z5.number().int().nullable().describe("the number of runs at most, or null"),
2200
+ where: z5.enum(["computer", "cloud"]).describe("computer: this computer, free, skipped while it sleeps; cloud: any time, for credits"),
2201
+ callWhen: z5.string().describe('when to call a person, in plain words: "a container keeps restarting"'),
2202
+ callOnTimeout: z5.boolean().describe("true: call a person when the condition did not come in time"),
2203
+ keepAfterStep: z5.boolean().describe("true only when it must keep running after the task leaves this step"),
2204
+ wait: z5.boolean().describe("true: this run ends its turn now and continues here when the schedule ends")
2041
2205
  },
2042
2206
  async (args) => {
2043
2207
  if (args.wait && (waiting.waitingFor !== null || waiting.waitingForSchedule !== null)) {
2044
- return reply2(ALREADY_WAITING_TEXT2, true);
2208
+ return reply3(ALREADY_WAITING_TEXT2, true);
2045
2209
  }
2046
2210
  let answer;
2047
2211
  try {
2048
2212
  answer = await tools.send({ ...args, title: args.title.trim(), instruction: args.instruction.trim(), until: args.until.trim(), callWhen: args.callWhen.trim(), model: null });
2049
2213
  } 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);
2214
+ 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
2215
  }
2052
2216
  if (answer.id === null) {
2053
- return reply2(answer.text, true);
2217
+ return reply3(answer.text, true);
2054
2218
  }
2055
2219
  if (answer.waits) {
2056
2220
  waiting.waitingForSchedule = answer.id;
2057
2221
  }
2058
- return reply2(answer.text);
2222
+ return reply3(answer.text);
2059
2223
  }
2060
2224
  );
2061
2225
  }
2062
- function scheduleTools(api, path) {
2063
- return { send: (body) => api.request("POST", path, body) };
2226
+ function scheduleTools(api, path, wakePath) {
2227
+ return {
2228
+ send: (body) => api.request("POST", path, body),
2229
+ wake: (body) => api.request("POST", wakePath, body)
2230
+ };
2231
+ }
2232
+ function wakeUpLaterTool(tools) {
2233
+ return tool5(
2234
+ WAKE_TOOL_NAME,
2235
+ "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.",
2236
+ {
2237
+ inHours: z5.number().min(0.1).max(2160).describe("how many hours from now, for example 168 for 7 days"),
2238
+ step: z5.string().describe("the name of the step to wake up on, empty to start this step again"),
2239
+ note: z5.string().describe("what to do on waking, in plain words")
2240
+ },
2241
+ async (args) => {
2242
+ try {
2243
+ return reply3((await tools.wake({ inHours: args.inHours, step: args.step.trim(), note: args.note.trim() })).text);
2244
+ } catch (error) {
2245
+ 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);
2246
+ }
2247
+ }
2248
+ );
2064
2249
  }
2065
2250
  async function checkFolder(home, job) {
2066
2251
  if (job.taskId !== null) {
@@ -2081,7 +2266,7 @@ var CHECK_RULES = [
2081
2266
  `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
2267
  "Write in the language of the task. No em dashes and no en dashes."
2083
2268
  ];
2084
- function noteLine(note) {
2269
+ function noteLine2(note) {
2085
2270
  const head = `- ${note.at.slice(0, 16).replace("T", " ")} (${note.outcome}): ${note.summary}`;
2086
2271
  return note.note ? `${head}
2087
2272
  ${note.note.replace(/\n/g, "\n ")}` : head;
@@ -2094,43 +2279,56 @@ ${job.instruction}`,
2094
2279
  job.until ? `The schedule ends when: ${job.until}` : "The schedule runs for its time; there is no condition to wait for.",
2095
2280
  job.callWhen ? `Call a person when: ${job.callWhen}` : "Call a person only when something is clearly wrong.",
2096
2281
  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."
2282
+ ${job.earlier.map(noteLine2).join("\n")}` : "This is the first run: there are no earlier notes."
2098
2283
  ].join("\n\n");
2099
2284
  const instructions = [
2100
2285
  CHECK_RULES.join("\n"),
2101
2286
  workplaceText(job.computer, folder),
2287
+ job.sourcesText,
2102
2288
  job.safetyRules.trim() || null
2103
2289
  ].filter(Boolean).join("\n\n");
2104
2290
  return { prompt, instructions };
2105
2291
  }
2106
2292
  var SERVER_NAME = "tamsense";
2107
2293
  var MAX_CHECK_TURNS = 30;
2294
+ var MAX_ROUTINE_TURNS = 120;
2108
2295
  function reportCheckTool(onReport) {
2109
- return tool3(
2296
+ return tool5(
2110
2297
  REPORT_CHECK_TOOL_NAME,
2111
2298
  "Hand in the result of this check. Call it exactly once, at the very end.",
2112
2299
  {
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")
2300
+ changed: z5.boolean().describe("true when something changed since the earlier runs"),
2301
+ summary: z5.string().describe("one short line for the task chat: what you saw"),
2302
+ note: z5.string().nullable().describe("when something changed: the full facts, the exact lines and what they mean; null otherwise"),
2303
+ conditionMet: z5.boolean().describe("true when the condition of the schedule came"),
2304
+ callPerson: z5.boolean().describe("true when a person must look now, as the schedule says")
2118
2305
  },
2119
2306
  async (args) => {
2120
2307
  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.");
2308
+ return reply3("Result received. You can stop now.");
2122
2309
  }
2123
2310
  );
2124
2311
  }
2312
+ function textOf2(message) {
2313
+ if (message.type !== "assistant") {
2314
+ return null;
2315
+ }
2316
+ const text2 = message.message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").trim();
2317
+ return text2 || null;
2318
+ }
2125
2319
  function claudeCheckRunner(startQuery) {
2126
2320
  return async (request) => {
2127
2321
  request.signal.throwIfAborted();
2128
2322
  const abortController = new AbortController();
2129
2323
  request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
2130
2324
  let result = null;
2131
- const report = reportCheckTool((found) => {
2132
- result = found;
2133
- });
2325
+ const found = (input) => {
2326
+ result = input;
2327
+ };
2328
+ const own = request.routine ? routineToolset(request.routine, found) : [reportCheckTool(found)];
2329
+ const tools = [...own, ...sourceToolset(request.sources)];
2330
+ const lastWords = { text: "" };
2331
+ const permissions = request.routine?.permissions ?? null;
2134
2332
  const messages = startQuery({
2135
2333
  prompt: request.prompt.prompt,
2136
2334
  options: {
@@ -2138,10 +2336,10 @@ function claudeCheckRunner(startQuery) {
2138
2336
  systemPrompt: { type: "preset", preset: "claude_code", append: request.prompt.instructions },
2139
2337
  settingSources: ["user", "project", "local"],
2140
2338
  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,
2339
+ canUseTool: permissions ? askPermission(permissions, () => lastWords.text, request.cwd) : denyWhenAsked,
2340
+ mcpServers: { [SERVER_NAME]: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }) },
2341
+ allowedTools: tools.map((item) => `mcp__${SERVER_NAME}__${item.name}`),
2342
+ maxTurns: request.routine ? MAX_ROUTINE_TURNS : MAX_CHECK_TURNS,
2145
2343
  abortController,
2146
2344
  env: request.env,
2147
2345
  ...request.model === null ? {} : { model: request.model }
@@ -2149,6 +2347,7 @@ function claudeCheckRunner(startQuery) {
2149
2347
  });
2150
2348
  let outcome = { completed: false, report: "Claude finished without a result.", model: null, usage: null, result: null };
2151
2349
  for await (const message of messages) {
2350
+ lastWords.text = textOf2(message) ?? lastWords.text;
2152
2351
  if (message.type === "result") {
2153
2352
  const finished = outcomeOf(message, "");
2154
2353
  outcome = { completed: finished.completed, report: finished.report, model: finished.model, usage: finished.usage, result: null };
@@ -2322,7 +2521,7 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
2322
2521
  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
2522
  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
2523
  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.";
2524
+ 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
2525
  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
2526
  var FOLDER_DO_RULES = [
2328
2527
  `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 +2581,7 @@ var SPEC_RULES = [
2382
2581
  var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
2383
2582
  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
2583
  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).";
2584
+ 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
2585
  var RESULT_BRIEF_RULES = [
2387
2586
  '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
2587
  '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 +2649,17 @@ function folderText(root, results) {
2450
2649
  return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
2451
2650
  ${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
2452
2651
  }
2652
+ function taskResultsText(results) {
2653
+ const lines = results ?? [];
2654
+ 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:
2655
+ ${bulletList(lines)}` : null;
2656
+ }
2453
2657
  function handOverText(boards) {
2454
2658
  const names = (boards ?? []).map((board) => board.name);
2455
2659
  if (names.length === 0) {
2456
2660
  return null;
2457
2661
  }
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.`;
2662
+ 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
2663
  }
2460
2664
  function handoffText(targets, isCheck) {
2461
2665
  if (targets.length === 0) {
@@ -2531,7 +2735,7 @@ function briefGoal(job, work2) {
2531
2735
  }
2532
2736
  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
2737
  }
2534
- function buildBriefPrompt(job, work2, workspace) {
2738
+ function buildBriefPrompt(job, work2, workspace, options) {
2535
2739
  const prompt = [
2536
2740
  briefGoal(job, work2),
2537
2741
  job.description,
@@ -2550,8 +2754,9 @@ ${JSON.stringify(job.spec)}` : null,
2550
2754
  workplaceText(job.computer, workspace.root),
2551
2755
  stepLine(job.stage),
2552
2756
  job.stage.instructions,
2757
+ job.sourcesText,
2553
2758
  contextText(job.projectContext),
2554
- FOLDER_RULE,
2759
+ `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2555
2760
  RESULT_HOME_RULE,
2556
2761
  `How to work:
2557
2762
  ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
@@ -2561,7 +2766,7 @@ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work
2561
2766
  ].filter(Boolean).join("\n\n");
2562
2767
  return { prompt, instructions };
2563
2768
  }
2564
- function buildSpecPrompt(job, work2, workspace) {
2769
+ function buildSpecPrompt(job, work2, workspace, options) {
2565
2770
  const prompt = [
2566
2771
  specGoal(job, work2),
2567
2772
  job.description,
@@ -2580,10 +2785,12 @@ ${JSON.stringify(job.spec)}` : null,
2580
2785
  workplaceText(job.computer, workspace.root),
2581
2786
  stepLine(job.stage),
2582
2787
  job.stage.instructions,
2788
+ job.sourcesText,
2583
2789
  contextText(job.projectContext),
2584
2790
  `The repositories for this task are checked out in the current folder, one subfolder each:
2585
2791
  ${bulletList(repositoryLines(job, workspace))}`,
2586
2792
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2793
+ taskResultsText(options.results),
2587
2794
  READ_ONLY_RULE,
2588
2795
  `How to work:
2589
2796
  ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
@@ -2620,6 +2827,7 @@ ${job.review}`,
2620
2827
  workplaceText(job.computer, workspace.root),
2621
2828
  stepLine(job.stage),
2622
2829
  job.stage.instructions,
2830
+ job.sourcesText,
2623
2831
  contextText(job.projectContext),
2624
2832
  `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2625
2833
  `How to work:
@@ -2641,7 +2849,7 @@ function buildTaskPrompt(job, workspace, options = {}) {
2641
2849
  if (!job.specWork) {
2642
2850
  throw new Error(SPEC_WORK_MISSING);
2643
2851
  }
2644
- return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
2852
+ return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace, options) : buildSpecPrompt(job, job.specWork, workspace, options);
2645
2853
  }
2646
2854
  if (isFolderJob(job)) {
2647
2855
  return buildFolderPrompt(job, workspace, options);
@@ -2669,10 +2877,12 @@ ${job.review}`,
2669
2877
  workplaceText(job.computer, workspace.root),
2670
2878
  stepLine(job.stage),
2671
2879
  job.stage.instructions,
2880
+ job.sourcesText,
2672
2881
  contextText(job.projectContext),
2673
2882
  `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
2883
  ${bulletList(repositoryLines(job, workspace))}`,
2675
2884
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2885
+ taskResultsText(options.results),
2676
2886
  `How to work:
2677
2887
  ${bulletList(rules)}`,
2678
2888
  safetyText(job),
@@ -2687,135 +2897,135 @@ ${bulletList(rules)}`,
2687
2897
  }
2688
2898
 
2689
2899
  // src/specTools.ts
2690
- import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2691
- import { z as z4 } from "zod";
2900
+ import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
2901
+ import { z as z6 } from "zod";
2692
2902
  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")
2903
+ var questionSchema = z6.object({
2904
+ id: z6.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
2905
+ question: z6.string().describe("One plain sentence the owner can answer without reading code"),
2906
+ whyItMatters: z6.string().describe("One line: what changes in the result depending on the answer"),
2907
+ options: z6.array(z6.object({
2908
+ label: z6.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
2909
+ description: z6.string().describe("One sentence: what happens if this option is chosen"),
2910
+ recommended: z6.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
2701
2911
  })).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")
2912
+ multiSelect: z6.boolean().describe("true only when the options are additive and several can apply together")
2703
2913
  });
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")
2914
+ var briefSchema = z6.object({
2915
+ findings: z6.array(z6.object({
2916
+ fact: z6.string().describe("What the code does today that matters for this task"),
2917
+ where: z6.string().describe("Where it is: owner/repo path:line")
2708
2918
  })).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")
2919
+ 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"),
2920
+ approach: z6.string().describe("The approach you would take, in 2-4 sentences"),
2921
+ codeMap: z6.array(z6.object({
2922
+ repo: z6.string().describe("Repository full name (owner/name)"),
2923
+ file: z6.string().describe("Path relative to the repository root"),
2924
+ role: z6.string().describe("Why it matters: what to change or reuse there")
2715
2925
  })).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")
2926
+ assumptions: z6.array(z6.object({
2927
+ text: z6.string().describe("The default you chose"),
2928
+ reason: z6.string().describe("Why it is safe or why it was chosen")
2719
2929
  })).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")
2930
+ pendingDecisions: z6.array(z6.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
2721
2931
  });
2722
- var questionsPayloadSchema = z4.object({
2932
+ var questionsPayloadSchema = z6.object({
2723
2933
  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`)
2934
+ 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
2935
  });
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")
2936
+ var specSchema = z6.object({
2937
+ goal: z6.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2938
+ context: z6.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2939
+ inScope: z6.array(z6.string()).describe("What this task delivers, one item each"),
2940
+ 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"),
2941
+ decisions: z6.array(z6.object({
2942
+ question: z6.string(),
2943
+ answer: z6.string(),
2944
+ by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2735
2945
  })).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")
2946
+ codeMap: z6.array(z6.object({
2947
+ repo: z6.string().describe("Repository full name (owner/name)"),
2948
+ file: z6.string().describe("Path relative to the repository root"),
2949
+ role: z6.string().describe("What the coder needs to know about this file"),
2950
+ use: z6.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
2741
2951
  })).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")
2952
+ changes: z6.array(z6.object({
2953
+ repo: z6.string().describe("Repository full name (owner/name) where the change happens"),
2954
+ file: z6.string().describe("Path to the file, relative to the repository root"),
2955
+ action: z6.enum(["create", "modify", "delete"]),
2956
+ 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"),
2957
+ produces: z6.array(z6.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2958
+ consumes: z6.array(z6.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2959
+ acceptance: z6.array(z6.string()).describe("Ids of the criteria this change makes true, like AC-1")
2750
2960
  })).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')
2961
+ acceptance: z6.array(z6.object({
2962
+ id: z6.string().describe("AC-1, AC-2 and so on, in order"),
2963
+ 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"),
2964
+ text: z6.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
2755
2965
  })).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")
2966
+ testPlan: z6.object({
2967
+ automated: z6.array(z6.object({
2968
+ repo: z6.string(),
2969
+ command: z6.string().describe("The exact command to run"),
2970
+ expect: z6.string().describe("What passing looks like")
2761
2971
  })).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")
2972
+ newTests: z6.array(z6.object({
2973
+ seam: z6.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2974
+ covers: z6.array(z6.string()).describe("Ids of the criteria it covers")
2765
2975
  })),
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")
2976
+ manual: z6.array(z6.object({
2977
+ page: z6.string().describe("The page or screen"),
2978
+ steps: z6.string().describe("What a person does and sees")
2769
2979
  }))
2770
2980
  }),
2771
- risks: z4.array(z4.object({
2772
- risk: z4.string(),
2773
- mitigation: z4.string()
2981
+ risks: z6.array(z6.object({
2982
+ risk: z6.string(),
2983
+ mitigation: z6.string()
2774
2984
  })).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")
2985
+ riskLevel: z6.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
2986
+ assumptions: z6.array(z6.object({
2987
+ text: z6.string().describe("The default you chose"),
2988
+ reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
2779
2989
  })).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")
2990
+ 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"),
2991
+ 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
2992
  });
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")
2993
+ var resultBriefSchema = z6.object({
2994
+ format: z6.literal("brief").describe('Always "brief"'),
2995
+ goal: z6.string().describe("What the work is for, for whom, and the outcome people will notice"),
2996
+ context: z6.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
2997
+ contains: z6.array(z6.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
2998
+ acceptance: z6.array(z6.object({
2999
+ id: z6.string().describe("AC-1, AC-2 and so on, in order"),
3000
+ text: z6.string().describe("One observable property of the result that a checker can confirm by reading it")
2791
3001
  })).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")
3002
+ constraints: z6.array(z6.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
3003
+ 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"),
3004
+ sources: z6.array(z6.object({
3005
+ 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"),
3006
+ use: z6.string().describe("What to take from it")
2797
3007
  })).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")
3008
+ approver: z6.string().describe("Who approves the result, as the task or the board names them"),
3009
+ decisions: z6.array(z6.object({
3010
+ question: z6.string(),
3011
+ answer: z6.string(),
3012
+ by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
2803
3013
  })).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")
3014
+ assumptions: z6.array(z6.object({
3015
+ text: z6.string().describe("The default you chose"),
3016
+ reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
2807
3017
  })).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")
3018
+ 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
3019
  });
2810
- function reply3(text2, isError = false) {
3020
+ function reply4(text2, isError = false) {
2811
3021
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2812
3022
  }
2813
3023
  function unreachable(error, toolName) {
2814
3024
  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);
3025
+ return reply4(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
2816
3026
  }
2817
3027
  function questionsTool(review, onSubmit) {
2818
- return tool4(
3028
+ return tool6(
2819
3029
  SUBMIT_QUESTIONS_TOOL_NAME,
2820
3030
  "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
3031
  questionsPayloadSchema.shape,
@@ -2827,10 +3037,10 @@ function questionsTool(review, onSubmit) {
2827
3037
  return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
2828
3038
  }
2829
3039
  if (!verdict.accepted) {
2830
- return reply3(verdict.feedback, true);
3040
+ return reply4(verdict.feedback, true);
2831
3041
  }
2832
3042
  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.");
3043
+ 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
3044
  }
2835
3045
  );
2836
3046
  }
@@ -2838,7 +3048,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
2838
3048
  function specTool(review, onSubmit, builderCheckRan) {
2839
3049
  let repairs = 0;
2840
3050
  let reminded = false;
2841
- return tool4(
3051
+ return tool6(
2842
3052
  SUBMIT_SPEC_TOOL_NAME,
2843
3053
  "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
3054
  specSchema.shape,
@@ -2851,20 +3061,20 @@ function specTool(review, onSubmit, builderCheckRan) {
2851
3061
  }
2852
3062
  if (!verdict.accepted) {
2853
3063
  repairs += 1;
2854
- return reply3(verdict.feedback, true);
3064
+ return reply4(verdict.feedback, true);
2855
3065
  }
2856
3066
  if (!builderCheckRan() && !reminded) {
2857
3067
  reminded = true;
2858
- return reply3(BUILDER_CHECK_FIRST, true);
3068
+ return reply4(BUILDER_CHECK_FIRST, true);
2859
3069
  }
2860
3070
  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.");
3071
+ return reply4(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2862
3072
  }
2863
3073
  );
2864
3074
  }
2865
3075
  function briefTool(review, onSubmit) {
2866
3076
  let repairs = 0;
2867
- return tool4(
3077
+ return tool6(
2868
3078
  SUBMIT_SPEC_TOOL_NAME,
2869
3079
  "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
3080
  resultBriefSchema.shape,
@@ -2877,10 +3087,10 @@ function briefTool(review, onSubmit) {
2877
3087
  }
2878
3088
  if (!verdict.accepted) {
2879
3089
  repairs += 1;
2880
- return reply3(verdict.feedback, true);
3090
+ return reply4(verdict.feedback, true);
2881
3091
  }
2882
3092
  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.");
3093
+ return reply4(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
2884
3094
  }
2885
3095
  );
2886
3096
  }
@@ -2983,11 +3193,11 @@ function usageLimitWatch() {
2983
3193
  var NO_RESULT_TEXT = "Claude finished without a result.";
2984
3194
  var SERVER_NAME2 = "tamsense";
2985
3195
  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"')
3196
+ 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"'),
3197
+ purpose: z7.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
2988
3198
  };
2989
3199
  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."')
3200
+ 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
3201
  };
2992
3202
  var ATTACH_TOOLS = {
2993
3203
  screenshot: {
@@ -3008,21 +3218,21 @@ var ATTACH_TOOLS = {
3008
3218
  };
3009
3219
  function attachTool(kind, attach) {
3010
3220
  const spec = ATTACH_TOOLS[kind];
3011
- return tool5(
3221
+ return tool7(
3012
3222
  spec.name,
3013
3223
  spec.description,
3014
- { path: z5.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
3224
+ { path: z7.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
3015
3225
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
3016
3226
  );
3017
3227
  }
3018
3228
  function verdictTool(onVerdict) {
3019
- return tool5(
3229
+ return tool7(
3020
3230
  VERDICT_TOOL_NAME,
3021
3231
  "Hand in the result of this check. Call it exactly once, at the very end.",
3022
3232
  {
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")
3233
+ passed: z7.boolean().describe("true only if nothing must be fixed"),
3234
+ report: z7.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
3235
+ 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
3236
  },
3027
3237
  async (args) => {
3028
3238
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -3031,13 +3241,13 @@ function verdictTool(onVerdict) {
3031
3241
  );
3032
3242
  }
3033
3243
  function handoffTool(targets, onHandoff) {
3034
- return tool5(
3244
+ return tool7(
3035
3245
  HANDOFF_TOOL_NAME,
3036
3246
  "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
3247
  {
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")
3248
+ to: z7.string().describe("name or id of the step to hand the task to"),
3249
+ outcome: z7.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
3250
+ note: z7.string().describe("for the next agent and the owner: what was done, what to check, what is open")
3041
3251
  },
3042
3252
  async (args) => {
3043
3253
  const choice = chooseHandoff(targets, args);
@@ -3050,25 +3260,25 @@ function handoffTool(targets, onHandoff) {
3050
3260
  );
3051
3261
  }
3052
3262
  function askPersonTool(asking, waiting) {
3053
- return tool5(
3263
+ return tool7(
3054
3264
  ASK_PERSON_TOOL_NAME,
3055
3265
  "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
3266
  {
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")
3267
+ 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"),
3268
+ why: z7.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
3269
+ question: z7.string().describe("one plain question the person can answer without reading the code"),
3270
+ context: z7.string().optional().describe("one line on what you saw that led to the question"),
3271
+ tried: z7.array(z7.object({
3272
+ did: z7.string().describe("the command or step"),
3273
+ error: z7.string().optional().describe("the exact error line, if there was one"),
3274
+ result: z7.string().describe("what it showed, one line")
3065
3275
  })).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")
3276
+ options: z7.array(z7.object({
3277
+ label: z7.string().describe("a short answer the person can pick"),
3278
+ detail: z7.string().describe("one line on what happens if it is picked")
3069
3279
  })).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")
3280
+ blocking: z7.boolean().describe("true when you cannot go on without the answer"),
3281
+ assumption: z7.string().optional().describe("when blocking is false: what you continue with until the answer comes")
3072
3282
  },
3073
3283
  async (args) => {
3074
3284
  if (waiting.waitingFor !== null) {
@@ -3078,16 +3288,16 @@ function askPersonTool(asking, waiting) {
3078
3288
  if ("error" in choice) {
3079
3289
  return { content: [{ type: "text", text: choice.error }], isError: true };
3080
3290
  }
3081
- let reply4;
3291
+ let reply5;
3082
3292
  try {
3083
- reply4 = await asking.send(choice.body);
3293
+ reply5 = await asking.send(choice.body);
3084
3294
  } catch (error) {
3085
3295
  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
3296
  }
3087
3297
  if (choice.body.blocking) {
3088
- waiting.waitingFor = reply4.id;
3298
+ waiting.waitingFor = reply5.id;
3089
3299
  }
3090
- return { content: [{ type: "text", text: askedText(reply4.name, choice.body) }] };
3300
+ return { content: [{ type: "text", text: askedText(reply5.name, choice.body) }] };
3091
3301
  }
3092
3302
  );
3093
3303
  }
@@ -3132,7 +3342,8 @@ function tamsenseServer(request, submitted) {
3132
3342
  })] : [],
3133
3343
  ...request.results ? resultToolset(request.results) : [],
3134
3344
  ...request.handOver ? [handOverTool(request.handOver)] : [],
3135
- ...request.schedules ? [proposeScheduleTool(request.schedules, submitted)] : []
3345
+ ...request.schedules ? [proposeScheduleTool(request.schedules, submitted), wakeUpLaterTool(request.schedules)] : [],
3346
+ ...sourceToolset(request.sources)
3136
3347
  ];
3137
3348
  return {
3138
3349
  server: createSdkMcpServer2({ name: SERVER_NAME2, version: "1.0.0", tools }),
@@ -3159,6 +3370,7 @@ async function attempt(request, context, resume) {
3159
3370
  let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
3160
3371
  const tamsense = tamsenseServer(request, context.submitted);
3161
3372
  const shown = hiddenStepsFilter();
3373
+ const calls = sourceCalls(request.sources?.tools);
3162
3374
  const helpers = helperTasks();
3163
3375
  const messages = context.startQuery({
3164
3376
  prompt: request.input.start(request.prompt.prompt),
@@ -3193,7 +3405,7 @@ async function attempt(request, context, resume) {
3193
3405
  messages.close();
3194
3406
  return { outcome, sessionId, resumeFailed: true };
3195
3407
  }
3196
- for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
3408
+ for (const draft of [...helpers.observe(message), ...feedOf(message, calls).filter(shown).flatMap(helpers.adjust)]) {
3197
3409
  if (draft.kind === "agent" && draft.parent === null) {
3198
3410
  context.submitted.helperStarted = true;
3199
3411
  }
@@ -3299,14 +3511,14 @@ async function login(tools, request) {
3299
3511
  tools.openBrowser(pageUrl);
3300
3512
  while (tools.now() < expiresAt) {
3301
3513
  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 };
3514
+ const reply5 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
3515
+ if (reply5.status === "connected") {
3516
+ return { token: reply5.token, device: reply5.device };
3305
3517
  }
3306
- if (reply4.status === "denied") {
3518
+ if (reply5.status === "denied") {
3307
3519
  throw new Error(DENIED_TEXT);
3308
3520
  }
3309
- if (reply4.status === "expired") {
3521
+ if (reply5.status === "expired") {
3310
3522
  break;
3311
3523
  }
3312
3524
  }
@@ -3679,6 +3891,13 @@ function describeDevice(identity) {
3679
3891
  return identity.device.team ? `a team computer of ${identity.projectName}` : `the personal computer of ${identity.email ?? "its owner"}`;
3680
3892
  }
3681
3893
 
3894
+ // src/sense.ts
3895
+ import { mkdtemp, rm as rm5 } from "fs/promises";
3896
+ import { tmpdir } from "os";
3897
+ import { dirname as dirname4, join as join9 } from "path";
3898
+ import process5 from "process";
3899
+ import { createSdkMcpServer as createSdkMcpServer3, query as query4 } from "@anthropic-ai/claude-agent-sdk";
3900
+
3682
3901
  // src/system.ts
3683
3902
  import { spawn as spawn3 } from "child_process";
3684
3903
  import { hostname } from "os";
@@ -3723,10 +3942,104 @@ async function sleep(ms, signal) {
3723
3942
  });
3724
3943
  }
3725
3944
 
3945
+ // src/sense.ts
3946
+ var SERVER_NAME3 = "tamsense";
3947
+ var MAX_SENSE_TURNS = 3;
3948
+ var HEARTBEAT_MS = 5e3;
3949
+ var STOPPED_HERE = "stopped_here";
3950
+ var CLOSED_ON_SITE = "closed_on_site";
3951
+ var TOO_LONG = "too_long";
3952
+ var FAILED = { outcome: "failed", text: "", model: null, usage: null };
3953
+ function claudeSenseRunner(startQuery) {
3954
+ return async (request) => {
3955
+ request.signal.throwIfAborted();
3956
+ const abortController = new AbortController();
3957
+ request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
3958
+ const tools = sourceToolset(request.sources);
3959
+ const limit = usageLimitWatch();
3960
+ const messages = startQuery({
3961
+ prompt: request.prompt,
3962
+ options: {
3963
+ cwd: request.cwd,
3964
+ systemPrompt: request.system,
3965
+ tools: [],
3966
+ settingSources: [],
3967
+ persistSession: false,
3968
+ strictMcpConfig: true,
3969
+ mcpServers: tools.length > 0 ? { [SERVER_NAME3]: createSdkMcpServer3({ name: SERVER_NAME3, version: "1.0.0", tools }) } : {},
3970
+ allowedTools: tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`),
3971
+ permissionMode: "auto",
3972
+ canUseTool: denyWhenAsked,
3973
+ maxTurns: MAX_SENSE_TURNS,
3974
+ outputFormat: { type: "json_schema", schema: request.schema },
3975
+ abortController,
3976
+ env: request.env,
3977
+ ...request.model === null ? {} : { model: request.model }
3978
+ }
3979
+ });
3980
+ let finish2 = FAILED;
3981
+ for await (const message of messages) {
3982
+ limit.observe(message);
3983
+ if (message.type === "result") {
3984
+ const measured = outcomeOf(message, "");
3985
+ const text2 = message.subtype !== "success" ? "" : message.structured_output === void 0 ? message.result : JSON.stringify(message.structured_output);
3986
+ finish2 = { outcome: measured.completed && text2 ? "succeeded" : "failed", text: text2, model: measured.model, usage: measured.usage };
3987
+ }
3988
+ }
3989
+ return limit.pausedUntil(finish2.outcome === "succeeded") ? { ...finish2, outcome: "usage_limit" } : finish2;
3990
+ };
3991
+ }
3992
+ function senseHeartbeat(api, job, abort) {
3993
+ return async () => {
3994
+ try {
3995
+ const answer = await api.request("POST", sensePath(job.senseId, "heartbeat"), void 0, { safeToRepeat: true });
3996
+ if (!answer.keepGoing) {
3997
+ abort();
3998
+ }
3999
+ } catch {
4000
+ }
4001
+ };
4002
+ }
4003
+ async function runSenseJob(job, runSense, tools, stop) {
4004
+ const cwd = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
4005
+ const run2 = new AbortController();
4006
+ const stopRun = () => run2.abort(STOPPED_HERE);
4007
+ if (stop.aborted) {
4008
+ stopRun();
4009
+ }
4010
+ stop.addEventListener("abort", stopRun, { once: true });
4011
+ const deadline = setTimeout(() => run2.abort(TOO_LONG), job.deadlineMs);
4012
+ const beat = senseHeartbeat(tools.api, job, () => run2.abort(CLOSED_ON_SITE));
4013
+ const beating = setInterval(() => void beat(), HEARTBEAT_MS);
4014
+ let body;
4015
+ try {
4016
+ const env = withNodeOnPath(process5.env, dirname4(process5.execPath));
4017
+ 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")) });
4018
+ } catch {
4019
+ body = FAILED;
4020
+ } finally {
4021
+ clearTimeout(deadline);
4022
+ clearInterval(beating);
4023
+ stop.removeEventListener("abort", stopRun);
4024
+ await rm5(cwd, { recursive: true, force: true });
4025
+ }
4026
+ if (run2.signal.reason === CLOSED_ON_SITE) {
4027
+ return;
4028
+ }
4029
+ try {
4030
+ await tools.api.request("POST", sensePath(job.senseId, "finish"), run2.signal.aborted ? FAILED : body);
4031
+ if (body.outcome === "succeeded" && !run2.signal.aborted) {
4032
+ tools.print(`Sense answered on task #${job.taskId}`);
4033
+ }
4034
+ } catch {
4035
+ }
4036
+ }
4037
+ var runClaudeSense = claudeSenseRunner(query4);
4038
+
3726
4039
  // src/update.ts
3727
4040
  import { spawn as spawn4 } from "child_process";
3728
4041
  import { readFileSync as readFileSync2 } from "fs";
3729
- import process5 from "process";
4042
+ import process6 from "process";
3730
4043
  var PACKAGE_NAME = "@tamsensedev/runner";
3731
4044
  var RELAUNCHED_ENV = "TAMSENSE_RELAUNCHED";
3732
4045
  var NO_UPDATE_ENV = "TAMSENSE_NO_UPDATE";
@@ -3778,27 +4091,27 @@ function relaunchArgs(argv) {
3778
4091
  return ["--yes", `${PACKAGE_NAME}@latest`, ...argv];
3779
4092
  }
3780
4093
  function relaunch(argv, env) {
3781
- const isWindows = process5.platform === "win32";
4094
+ const isWindows = process6.platform === "win32";
3782
4095
  return new Promise((resolve5, reject) => {
3783
4096
  const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
3784
4097
  const ignoreInterrupt = () => {
3785
4098
  };
3786
- process5.on("SIGINT", ignoreInterrupt);
4099
+ process6.on("SIGINT", ignoreInterrupt);
3787
4100
  child.on("error", (error) => {
3788
- process5.off("SIGINT", ignoreInterrupt);
4101
+ process6.off("SIGINT", ignoreInterrupt);
3789
4102
  reject(error);
3790
4103
  });
3791
4104
  child.on("close", (code) => {
3792
- process5.off("SIGINT", ignoreInterrupt);
4105
+ process6.off("SIGINT", ignoreInterrupt);
3793
4106
  resolve5(code ?? 1);
3794
4107
  });
3795
4108
  });
3796
4109
  }
3797
4110
 
3798
4111
  // src/worker.ts
3799
- import { rm as rm6 } from "fs/promises";
3800
- import { dirname as dirname5 } from "path";
3801
- import process6 from "process";
4112
+ import { rm as rm7 } from "fs/promises";
4113
+ import { dirname as dirname6, join as join12 } from "path";
4114
+ import process7 from "process";
3802
4115
 
3803
4116
  // src/feedQueue.ts
3804
4117
  var MAX_BATCH = 200;
@@ -3845,8 +4158,8 @@ function feedQueue(send) {
3845
4158
 
3846
4159
  // src/library.ts
3847
4160
  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";
4161
+ import { cp, mkdir as mkdir5, rm as rm6 } from "fs/promises";
4162
+ import { join as join10 } from "path";
3850
4163
  import { fileURLToPath as fileURLToPath2 } from "url";
3851
4164
  var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
3852
4165
  var SKILL_NAME = /^[a-z0-9-]+$/;
@@ -3859,18 +4172,18 @@ function allowedSkills(stage) {
3859
4172
  return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
3860
4173
  }
3861
4174
  function roleLibraryFolder(home, runId) {
3862
- return join9(home, ".tamsense", "libraries", `run-${runId}`);
4175
+ return join10(home, ".tamsense", "libraries", `run-${runId}`);
3863
4176
  }
3864
4177
  async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3865
4178
  if (skills === null) {
3866
4179
  return source;
3867
4180
  }
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 });
4181
+ await rm6(folder, { recursive: true, force: true });
4182
+ await mkdir5(join10(folder, "skills"), { recursive: true });
4183
+ await cp(join10(source, ".claude-plugin"), join10(folder, ".claude-plugin"), { recursive: true });
4184
+ await cp(join10(source, "agents"), join10(folder, "agents"), { recursive: true });
4185
+ for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync7(join10(source, "skills", name)))) {
4186
+ await cp(join10(source, "skills", skill), join10(folder, "skills", skill), { recursive: true });
3874
4187
  }
3875
4188
  return folder;
3876
4189
  }
@@ -3879,7 +4192,7 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3879
4192
  import { randomUUID } from "crypto";
3880
4193
  import { createReadStream } from "fs";
3881
4194
  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";
4195
+ import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
3883
4196
  import { createInterface as createInterface2 } from "readline";
3884
4197
  import { Readable } from "stream";
3885
4198
  import { promisify } from "util";
@@ -3959,12 +4272,12 @@ async function readMeta(file) {
3959
4272
  }
3960
4273
  async function metaFor(kind, file) {
3961
4274
  if (kind === "replay") {
3962
- return readMeta(join10(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
4275
+ return readMeta(join11(dirname5(file), `${sessionIdOf(file)}${META_SUFFIX}`));
3963
4276
  }
3964
- const folder = basename4(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
4277
+ const folder = basename4(dirname5(file)) === "video" ? dirname5(dirname5(file)) : dirname5(file);
3965
4278
  for (const name of await readdir2(folder).catch(() => [])) {
3966
4279
  if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
3967
- const meta = await readMeta(join10(folder, name));
4280
+ const meta = await readMeta(join11(folder, name));
3968
4281
  if (meta?.video === file) {
3969
4282
  return meta;
3970
4283
  }
@@ -3974,7 +4287,7 @@ async function metaFor(kind, file) {
3974
4287
  }
3975
4288
  async function ledger(dir) {
3976
4289
  try {
3977
- return new Set(JSON.parse(await readFile4(join10(dir, LEDGER_FILE), "utf8")));
4290
+ return new Set(JSON.parse(await readFile4(join11(dir, LEDGER_FILE), "utf8")));
3978
4291
  } catch {
3979
4292
  return /* @__PURE__ */ new Set();
3980
4293
  }
@@ -3982,7 +4295,7 @@ async function ledger(dir) {
3982
4295
  async function remember(dir, file) {
3983
4296
  const done2 = await ledger(dir);
3984
4297
  done2.add(file);
3985
- await writeFile4(join10(dir, LEDGER_FILE), JSON.stringify([...done2]));
4298
+ await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
3986
4299
  }
3987
4300
  async function nonEmpty(file) {
3988
4301
  return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
@@ -3995,8 +4308,8 @@ async function pendingRecordings(dir) {
3995
4308
  if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
3996
4309
  continue;
3997
4310
  }
3998
- const meta = await readMeta(join10(dir, name));
3999
- const replay = join10(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
4311
+ const meta = await readMeta(join11(dir, name));
4312
+ const replay = join11(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
4000
4313
  if (!done2.has(replay) && await nonEmpty(replay)) {
4001
4314
  pending.push({ kind: "replay", file: replay, meta });
4002
4315
  }
@@ -4143,7 +4456,7 @@ async function readScreenshot(request) {
4143
4456
  }
4144
4457
 
4145
4458
  // src/worker.ts
4146
- var POLL_INTERVAL_MS = 5e3;
4459
+ var POLL_INTERVAL_MS = 2e3;
4147
4460
  var MAX_RETRY_WAIT_MS = 6e4;
4148
4461
  var POLL_DEADLINE_MS = 3 * 6e4;
4149
4462
  var INBOX_INTERVAL_MS = 2e3;
@@ -4154,8 +4467,10 @@ var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
4154
4467
  var MAX_CHECKS = 2;
4155
4468
  var CHECK_HEARTBEAT_MS = 3e4;
4156
4469
  var CHECK_DEADLINE_MS = 10 * 6e4;
4157
- var STOPPED_HERE = "stopped_here";
4158
- var CLOSED_ON_SITE = "closed_on_site";
4470
+ var ROUTINE_DEADLINE_MS = 30 * 6e4;
4471
+ var MAX_SENSE = 2;
4472
+ var STOPPED_HERE2 = "stopped_here";
4473
+ var CLOSED_ON_SITE2 = "closed_on_site";
4159
4474
  function messageOf3(error) {
4160
4475
  return error instanceof Error ? error.message : String(error);
4161
4476
  }
@@ -4228,9 +4543,9 @@ function inbox(job, tools, record, closedOnSite) {
4228
4543
  async function check() {
4229
4544
  const ack = input.takeDelivered();
4230
4545
  const body = { ack };
4231
- let reply4;
4546
+ let reply5;
4232
4547
  try {
4233
- reply4 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
4548
+ reply5 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
4234
4549
  } catch (error) {
4235
4550
  input.restoreDelivered(ack);
4236
4551
  if (!failing && !alreadyReported(error)) {
@@ -4240,23 +4555,23 @@ function inbox(job, tools, record, closedOnSite) {
4240
4555
  return;
4241
4556
  }
4242
4557
  failing = false;
4243
- if (!reply4.keepGoing) {
4558
+ if (!reply5.keepGoing) {
4244
4559
  closedOnSite();
4245
4560
  return;
4246
4561
  }
4247
- for (const message of reply4.messages) {
4562
+ for (const message of reply5.messages) {
4248
4563
  if (input.push(message)) {
4249
4564
  tools.print(` ${messageLine(message)}`);
4250
4565
  }
4251
4566
  }
4252
- for (const stop of reply4.helperStops ?? []) {
4567
+ for (const stop of reply5.helperStops ?? []) {
4253
4568
  if (!stopsSeen.has(stop.ref)) {
4254
4569
  stopsSeen.add(stop.ref);
4255
4570
  void stopHelper(input, stop, tools.print);
4256
4571
  }
4257
4572
  }
4258
- if (reply4.model !== null || reply4.effort !== null) {
4259
- const change = { model: reply4.model, effort: reply4.effort };
4573
+ if (reply5.model !== null || reply5.effort !== null) {
4574
+ const change = { model: reply5.model, effort: reply5.effort };
4260
4575
  void input.switchModel(change).then(
4261
4576
  (applied) => {
4262
4577
  if (applied) {
@@ -4311,11 +4626,11 @@ function permissionTools(job, tools) {
4311
4626
  }
4312
4627
  return {
4313
4628
  send: async (body) => {
4314
- const reply4 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
4315
- if (reply4.decision === "wait") {
4629
+ const reply5 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
4630
+ if (reply5.decision === "wait") {
4316
4631
  tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
4317
4632
  }
4318
- return reply4;
4633
+ return reply5;
4319
4634
  },
4320
4635
  check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
4321
4636
  sleep: tools.sleep
@@ -4343,14 +4658,15 @@ async function prepare(job, tools) {
4343
4658
  const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
4344
4659
  return { workspace: workspace2, results, identity: {} };
4345
4660
  }
4661
+ const taskResults = (workspace2) => writeResults(join12(workspace2.root, RESULTS_FOLDER), job, tools.api);
4346
4662
  if (isLocalJob(job)) {
4347
4663
  tools.print("Preparing the code from the folders on this computer\u2026");
4348
4664
  const workspace2 = await prepareLocalWorkspace(job, tools);
4349
- return { workspace: workspace2, results: null, identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
4665
+ return { workspace: workspace2, results: await taskResults(workspace2), identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
4350
4666
  }
4351
4667
  tools.print("Preparing the code\u2026");
4352
4668
  const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
4353
- return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
4669
+ return { workspace, results: await taskResults(workspace), identity: await commitIdentityEnv(tools.home) };
4354
4670
  }
4355
4671
  async function publish(workspace, job, api, identity) {
4356
4672
  return isLocalJob(job) ? await publishLocalWorkspace(workspace, job, identity) : await publishWorkspace(workspace, job, await gitToken(api, job), identity);
@@ -4368,7 +4684,7 @@ async function executeJob(job, tools, signal, input, record) {
4368
4684
  const attached = { count: 0 };
4369
4685
  const session = sessionReporter(tools.api, job, tools.print);
4370
4686
  const recordings = recordingsFolder(tools.home, job.runId);
4371
- const baseEnv = withNodeOnPath(process6.env, dirname5(process6.execPath));
4687
+ const baseEnv = withNodeOnPath(process7.env, dirname6(process7.execPath));
4372
4688
  const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
4373
4689
  const outcome = await tools.runAgent({
4374
4690
  cwd: workspace.root,
@@ -4391,7 +4707,8 @@ async function executeJob(job, tools, signal, input, record) {
4391
4707
  attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
4392
4708
  results: resultTools(job, tools.api, workspace.root),
4393
4709
  handOver: handOverTools(job, tools.api),
4394
- schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules")) : null
4710
+ schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules"), runPath(job.runId, "wake")) : null,
4711
+ sources: sourceTools(job.sourceTools, tools.api, runPath(job.runId, "sources"))
4395
4712
  });
4396
4713
  await session.sent();
4397
4714
  signal.throwIfAborted();
@@ -4466,16 +4783,16 @@ async function sendRecordings(job, tools) {
4466
4783
  tools.print(`Could not attach a recording: ${line}`);
4467
4784
  }
4468
4785
  } finally {
4469
- await rm6(folder, { recursive: true, force: true });
4786
+ await rm7(folder, { recursive: true, force: true });
4470
4787
  }
4471
4788
  }
4472
4789
  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)}`;
4790
+ return signal.reason === STOPPED_HERE2 ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
4474
4791
  }
4475
4792
  async function runJob(job, tools, stop) {
4476
4793
  tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
4477
4794
  const run2 = new AbortController();
4478
- const stopRun = () => run2.abort(STOPPED_HERE);
4795
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4479
4796
  if (stop.aborted) {
4480
4797
  stopRun();
4481
4798
  }
@@ -4483,7 +4800,7 @@ async function runJob(job, tools, stop) {
4483
4800
  const feed = feedQueue((entries) => tools.api.request("POST", runPath(job.runId, "feed"), { entries }, { safeToRepeat: true }));
4484
4801
  const feeding = setInterval(() => void feed.flush(), FEED_INTERVAL_MS);
4485
4802
  const record = journal(feed, tools.print);
4486
- const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE));
4803
+ const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE2));
4487
4804
  const { input } = messages;
4488
4805
  run2.signal.addEventListener("abort", input.close, { once: true });
4489
4806
  void messages.poll();
@@ -4494,7 +4811,7 @@ async function runJob(job, tools, stop) {
4494
4811
  } catch (error) {
4495
4812
  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
4813
  } finally {
4497
- await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
4814
+ await rm7(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
4498
4815
  clearInterval(polling);
4499
4816
  clearInterval(feeding);
4500
4817
  stop.removeEventListener("abort", stopRun);
@@ -4505,8 +4822,8 @@ async function runJob(job, tools, stop) {
4505
4822
  await messages.poll();
4506
4823
  }
4507
4824
  await feed.flush();
4508
- if (run2.signal.reason === CLOSED_ON_SITE) {
4509
- await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
4825
+ if (run2.signal.reason === CLOSED_ON_SITE2) {
4826
+ await rm7(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
4510
4827
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
4511
4828
  return;
4512
4829
  }
@@ -4519,33 +4836,36 @@ async function runJob(job, tools, stop) {
4519
4836
  }
4520
4837
  }
4521
4838
  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);
4839
+ function checkFailure(error, signal, deadlineMs) {
4840
+ return signal.reason === CHECK_TOO_LONG ? `The run took longer than ${deadlineMs / 6e4} minutes.` : failureReport(error, signal);
4524
4841
  }
4525
4842
  async function runCheckJob(check, runCheck, tools, stop) {
4526
- tools.print(`Schedule check: ${check.title}${check.taskId === null ? "" : ` (task #${check.taskId})`}`);
4843
+ const routine = routineRunTools(check, tools.api, tools.print, tools.sleep);
4844
+ tools.print(routine ? `Routine: ${check.title}${check.routine?.dry ? " (test run)" : ""}` : `Schedule check: ${check.title}${check.taskId === null ? "" : ` (task #${check.taskId})`}`);
4845
+ const deadlineMs = routine ? ROUTINE_DEADLINE_MS : CHECK_DEADLINE_MS;
4527
4846
  const run2 = new AbortController();
4528
- const stopRun = () => run2.abort(STOPPED_HERE);
4847
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4529
4848
  if (stop.aborted) {
4530
4849
  stopRun();
4531
4850
  }
4532
4851
  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));
4852
+ const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG), deadlineMs);
4853
+ const beat = checkHeartbeat(tools.api, check, () => run2.abort(CLOSED_ON_SITE2));
4535
4854
  const beating = setInterval(() => void beat(), CHECK_HEARTBEAT_MS);
4536
4855
  let body;
4537
4856
  try {
4538
4857
  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 }));
4858
+ const env = withNodeOnPath(process7.env, dirname6(process7.execPath));
4859
+ const prompt = routine ? buildRoutinePrompt(check, folder) : buildCheckPrompt(check, folder);
4860
+ 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
4861
  } catch (error) {
4542
- body = { outcome: "failed", report: checkFailure(error, run2.signal), model: null, usage: null, result: null };
4862
+ body = { outcome: "failed", report: checkFailure(error, run2.signal, deadlineMs), model: null, usage: null, result: null };
4543
4863
  } finally {
4544
4864
  clearTimeout(deadline);
4545
4865
  clearInterval(beating);
4546
4866
  stop.removeEventListener("abort", stopRun);
4547
4867
  }
4548
- if (run2.signal.reason === CLOSED_ON_SITE) {
4868
+ if (run2.signal.reason === CLOSED_ON_SITE2) {
4549
4869
  tools.print("TAMsense closed this check while it was in progress. The result was not sent.");
4550
4870
  return;
4551
4871
  }
@@ -4606,9 +4926,11 @@ async function work(tools, stop, parallel = 1) {
4606
4926
  const stopAll = AbortSignal.any([stop, halt.signal]);
4607
4927
  const running = /* @__PURE__ */ new Set();
4608
4928
  const checks = /* @__PURE__ */ new Set();
4929
+ const senses = /* @__PURE__ */ new Set();
4609
4930
  const folderLock = keyedLock();
4610
4931
  const scheduleLock = keyedLock();
4611
4932
  const runCheck = tools.version ? tools.runCheck : void 0;
4933
+ const runSense = tools.version ? tools.runSense : void 0;
4612
4934
  const reporting = reportCapabilities(tools, stopAll);
4613
4935
  const checking = tools.inspectFolder ? checkFolders({ ...tools, inspect: tools.inspectFolder }, stopAll) : Promise.resolve();
4614
4936
  if (tools.startFolder) {
@@ -4623,19 +4945,26 @@ async function work(tools, stop, parallel = 1) {
4623
4945
  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
4946
  checks.add(task);
4625
4947
  };
4948
+ const startSense = (job, runner) => {
4949
+ const task = runSenseJob(job, runner, tools, stopAll).catch(() => {
4950
+ }).finally(() => senses.delete(task));
4951
+ senses.add(task);
4952
+ };
4626
4953
  let failures = 0;
4627
4954
  try {
4628
4955
  while (!stopAll.aborted) {
4629
4956
  const jobsFull = running.size >= parallel;
4630
4957
  const checksFull = !runCheck || checks.size >= MAX_CHECKS;
4631
- if (jobsFull && checksFull) {
4632
- await Promise.race([...running, ...checks]);
4958
+ const senseFull = !runSense || senses.size >= MAX_SENSE;
4959
+ if (jobsFull && checksFull && senseFull) {
4960
+ await Promise.race([...running, ...checks, ...senses]);
4633
4961
  continue;
4634
4962
  }
4635
- const body = runCheck && tools.version ? { version: tools.version, jobs: !jobsFull, checks: !checksFull } : void 0;
4636
- let reply4;
4963
+ const named = tools.version && (runCheck || runSense) ? tools.version : null;
4964
+ const body = named ? { version: named, jobs: !jobsFull, checks: !checksFull, ...runSense ? { sense: !senseFull } : {} } : void 0;
4965
+ let reply5;
4637
4966
  try {
4638
- reply4 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
4967
+ reply5 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
4639
4968
  } catch (error) {
4640
4969
  if (isFatal(error)) {
4641
4970
  halt.abort();
@@ -4649,16 +4978,18 @@ async function work(tools, stop, parallel = 1) {
4649
4978
  continue;
4650
4979
  }
4651
4980
  failures = 0;
4652
- if (reply4.check && runCheck) {
4653
- startCheck(reply4.check, runCheck);
4654
- } else if (reply4.job) {
4655
- start(reply4.job);
4981
+ if (reply5.sense && runSense) {
4982
+ startSense(reply5.sense, runSense);
4983
+ } else if (reply5.check && runCheck) {
4984
+ startCheck(reply5.check, runCheck);
4985
+ } else if (reply5.job) {
4986
+ start(reply5.job);
4656
4987
  } else {
4657
4988
  await tools.sleep(POLL_INTERVAL_MS, stopAll);
4658
4989
  }
4659
4990
  }
4660
4991
  } finally {
4661
- await Promise.allSettled([...running, ...checks, reporting, checking]);
4992
+ await Promise.allSettled([...running, ...checks, ...senses, reporting, checking]);
4662
4993
  }
4663
4994
  }
4664
4995
 
@@ -4669,11 +5000,11 @@ var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the
4669
5000
  var ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
4670
5001
  var INTERRUPTED_EXIT_CODE = 130;
4671
5002
  function print(line) {
4672
- process7.stdout.write(`${line}
5003
+ process8.stdout.write(`${line}
4673
5004
  `);
4674
5005
  }
4675
5006
  function serverFor(flag, saved) {
4676
- return serverOrigin(flag ?? process7.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
5007
+ return serverOrigin(flag ?? process8.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
4677
5008
  }
4678
5009
  function accessProblem(error) {
4679
5010
  return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
@@ -4699,15 +5030,15 @@ async function runLogin(server, deviceName) {
4699
5030
  }
4700
5031
  function stopOnInterrupt() {
4701
5032
  const stop = new AbortController();
4702
- process7.once("SIGINT", () => {
5033
+ process8.once("SIGINT", () => {
4703
5034
  print("Stopping\u2026 Press Ctrl+C again to quit at once.");
4704
5035
  stop.abort();
4705
- process7.once("SIGINT", () => process7.exit(INTERRUPTED_EXIT_CODE));
5036
+ process8.once("SIGINT", () => process8.exit(INTERRUPTED_EXIT_CODE));
4706
5037
  });
4707
5038
  return stop.signal;
4708
5039
  }
4709
5040
  function installTools(home) {
4710
- return { home, nodePath: process7.execPath, env: process7.env, print, run: runProcess, exists: existsSync8 };
5041
+ return { home, nodePath: process8.execPath, env: process8.env, print, run: runProcess, exists: existsSync8 };
4711
5042
  }
4712
5043
  async function browserForStart(home, install) {
4713
5044
  const installed = await installedBrowser(home);
@@ -4717,13 +5048,13 @@ async function browserForStart(home, install) {
4717
5048
  return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
4718
5049
  }
4719
5050
  async function startedNewVersion() {
4720
- const update = await updateToRun(process7.env, currentVersion(), () => latestVersion());
5051
+ const update = await updateToRun(process8.env, currentVersion(), () => latestVersion());
4721
5052
  if (!update) {
4722
5053
  return false;
4723
5054
  }
4724
5055
  print(`TAMsense runner ${update.latest} is out, this is ${update.current}. Starting the new version...`);
4725
5056
  try {
4726
- process7.exitCode = await relaunch(process7.argv.slice(2), process7.env);
5057
+ process8.exitCode = await relaunch(process8.argv.slice(2), process8.env);
4727
5058
  return true;
4728
5059
  } catch {
4729
5060
  print("Could not start the new version. Going on with this one.");
@@ -4740,7 +5071,7 @@ async function runStart(parallel, installsBrowser) {
4740
5071
  return;
4741
5072
  }
4742
5073
  const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
4743
- const problem2 = teamKeyProblem(identity.device.team, process7.env);
5074
+ const problem2 = teamKeyProblem(identity.device.team, process8.env);
4744
5075
  if (problem2) {
4745
5076
  throw new Error(problem2);
4746
5077
  }
@@ -4748,7 +5079,7 @@ async function runStart(parallel, installsBrowser) {
4748
5079
  const home = homedir3();
4749
5080
  const browser = await browserForStart(home, installsBrowser);
4750
5081
  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);
5082
+ await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, runSense: runClaudeSense, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process8.cwd() }, stopOnInterrupt(), parallel);
4752
5083
  }
4753
5084
  async function runBrowser(remove) {
4754
5085
  const home = homedir3();
@@ -4800,10 +5131,10 @@ async function run(command) {
4800
5131
  }
4801
5132
  }
4802
5133
  try {
4803
- await run(parseCommand(process7.argv.slice(2), process7.env));
5134
+ await run(parseCommand(process8.argv.slice(2), process8.env));
4804
5135
  } catch (error) {
4805
- process7.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
5136
+ process8.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
4806
5137
  `);
4807
- process7.exitCode = 1;
5138
+ process8.exitCode = 1;
4808
5139
  }
4809
5140
  //# sourceMappingURL=cli.js.map