@tamsensedev/runner 0.10.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 (83) hide show
  1. package/LICENSE +5 -5
  2. package/README.md +61 -50
  3. package/dist/cli.js +1188 -324
  4. package/dist/cli.js.map +1 -1
  5. package/dist/recorder.cjs +42 -6
  6. package/dist/recorder.cjs.map +1 -1
  7. package/library/.claude-plugin/plugin.json +5 -5
  8. package/library/THIRD_PARTY_NOTICES.md +123 -123
  9. package/library/agents/code-architect.md +67 -67
  10. package/library/agents/code-explorer.md +61 -61
  11. package/library/agents/code-reviewer.md +73 -73
  12. package/library/agents/code-simplifier.md +62 -62
  13. package/library/agents/design-critic.md +52 -52
  14. package/library/agents/second-opinion.md +64 -64
  15. package/library/agents/security-auditor.md +86 -86
  16. package/library/agents/silent-failure-hunter.md +70 -70
  17. package/library/agents/test-coverage-analyzer.md +78 -78
  18. package/library/agents/test-runner.md +48 -48
  19. package/library/agents/ui-tester.md +50 -50
  20. package/library/licenses/Apache-2.0.txt +202 -202
  21. package/library/skills/accessibility/SKILL.md +144 -144
  22. package/library/skills/accessibility/references/wcag-checks.md +168 -168
  23. package/library/skills/addressing-review-feedback/SKILL.md +109 -109
  24. package/library/skills/app-screens/SKILL.md +96 -96
  25. package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
  26. package/library/skills/app-screens/references/states-density-phone.md +54 -54
  27. package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
  28. package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
  29. package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
  30. package/library/skills/clean-commits/SKILL.md +85 -85
  31. package/library/skills/design-review/SKILL.md +107 -107
  32. package/library/skills/design-review/references/critique-passes.md +101 -101
  33. package/library/skills/design-review/references/screenshot-procedure.md +121 -121
  34. package/library/skills/differential-security-review/LICENSE +427 -427
  35. package/library/skills/differential-security-review/SKILL.md +105 -105
  36. package/library/skills/differential-security-review/references/adversarial.md +115 -115
  37. package/library/skills/differential-security-review/references/methodology.md +145 -145
  38. package/library/skills/differential-security-review/references/patterns.md +108 -108
  39. package/library/skills/false-positive-check/LICENSE +427 -427
  40. package/library/skills/false-positive-check/SKILL.md +95 -95
  41. package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
  42. package/library/skills/false-positive-check/references/deep-verification.md +113 -113
  43. package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
  44. package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
  45. package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
  46. package/library/skills/false-positive-check/references/standard-verification.md +80 -80
  47. package/library/skills/focused-implementation/SKILL.md +104 -104
  48. package/library/skills/implementation-plan/SKILL.md +66 -66
  49. package/library/skills/interface-design/SKILL.md +144 -144
  50. package/library/skills/interface-design/references/craft-defaults.md +130 -130
  51. package/library/skills/interface-design/references/new-direction.md +62 -62
  52. package/library/skills/interface-design/references/writing.md +52 -52
  53. package/library/skills/interface-polish/SKILL.md +103 -103
  54. package/library/skills/interface-polish/references/detail-rules.md +154 -154
  55. package/library/skills/interface-polish/references/worst-case-data.md +158 -158
  56. package/library/skills/motion-design/SKILL.md +106 -106
  57. package/library/skills/motion-design/references/recipes.md +243 -243
  58. package/library/skills/postgres-performance/SKILL.md +97 -97
  59. package/library/skills/postgres-performance/references/rules.md +265 -265
  60. package/library/skills/project-orientation/SKILL.md +77 -77
  61. package/library/skills/reviewing-changes/SKILL.md +183 -183
  62. package/library/skills/safe-database-changes/SKILL.md +111 -111
  63. package/library/skills/safe-database-changes/references/backfills.md +75 -75
  64. package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
  65. package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
  66. package/library/skills/security-review/SKILL.md +132 -132
  67. package/library/skills/security-review/references/checks-by-area.md +107 -107
  68. package/library/skills/security-review/references/do-not-report.md +86 -86
  69. package/library/skills/spec-writing/SKILL.md +144 -144
  70. package/library/skills/systematic-debugging/SKILL.md +130 -130
  71. package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
  72. package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
  73. package/library/skills/task-interview/SKILL.md +84 -84
  74. package/library/skills/test-driven-development/SKILL.md +116 -116
  75. package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
  76. package/library/skills/ui-testing/SKILL.md +102 -102
  77. package/library/skills/ui-testing/references/headless-check.md +295 -280
  78. package/library/skills/ui-testing/references/local-stand.md +51 -51
  79. package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
  80. package/library/skills/ui-testing/references/recording.md +61 -58
  81. package/library/skills/verification-before-completion/SKILL.md +113 -113
  82. package/library/skills/web-interface-guidelines/SKILL.md +202 -202
  83. package/package.json +46 -46
package/dist/cli.js CHANGED
@@ -1,12 +1,136 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { existsSync as existsSync7 } from "fs";
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
+
8
+ // src/api.ts
9
+ import { setTimeout as wait } from "timers/promises";
10
+
11
+ // src/connection.ts
12
+ var BUSY_STATUSES = /* @__PURE__ */ new Set([429, 502, 503, 504]);
13
+ var REFUSED_STATUSES = /* @__PURE__ */ new Set([429, 503]);
14
+ var NOT_FOUND_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "EAI_AGAIN"]);
15
+ var NO_CONNECTION_CODES = /* @__PURE__ */ new Set(["ECONNREFUSED", "ENETUNREACH", "EHOSTUNREACH", "ENETDOWN", "UND_ERR_CONNECT_TIMEOUT"]);
16
+ var CLOSED_CODES = /* @__PURE__ */ new Set(["ECONNRESET", "EPIPE", "ECONNABORTED", "UND_ERR_SOCKET", "UND_ERR_CLOSED"]);
17
+ var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ETIMEDOUT", "UND_ERR_HEADERS_TIMEOUT", "UND_ERR_BODY_TIMEOUT"]);
18
+ var CERTIFICATE_CODE = /CERT|SELF_SIGNED|UNABLE_TO_VERIFY|ERR_TLS/;
19
+ function codeOf(error) {
20
+ for (let current = error, depth = 0; current && typeof current === "object" && depth < 5; depth += 1) {
21
+ const fields = current;
22
+ if (typeof fields.code === "string") {
23
+ return fields.code;
24
+ }
25
+ current = fields.cause ?? (Array.isArray(fields.errors) ? fields.errors[0] : null);
26
+ }
27
+ return null;
28
+ }
29
+ function isTimeout(error) {
30
+ return error instanceof Error && error.name === "TimeoutError";
31
+ }
32
+ function networkFailure(error, server, timeoutMs) {
33
+ const host = new URL(server).host;
34
+ const code = codeOf(error);
35
+ if (isTimeout(error)) {
36
+ return { kind: "timeout", text: `TAMsense did not answer in ${Math.round(timeoutMs / 1e3)} seconds`, unsent: false, waitMs: null };
37
+ }
38
+ if (code && TIMEOUT_CODES.has(code)) {
39
+ return { kind: "timeout", text: `TAMsense did not answer in time (${code})`, unsent: false, waitMs: null };
40
+ }
41
+ if (code && NOT_FOUND_CODES.has(code)) {
42
+ return { kind: "no_address", text: `Could not find ${host} (${code}). Check the internet connection`, unsent: true, waitMs: null };
43
+ }
44
+ if (code && NO_CONNECTION_CODES.has(code)) {
45
+ return { kind: "no_connection", text: `Could not connect to ${host} (${code})`, unsent: true, waitMs: null };
46
+ }
47
+ if (code && CLOSED_CODES.has(code)) {
48
+ return { kind: "closed", text: `The network closed the connection to TAMsense (${code})`, unsent: false, waitMs: null };
49
+ }
50
+ if (code && CERTIFICATE_CODE.test(code)) {
51
+ return { kind: "certificate", text: `The secure connection to ${host} failed (${code}). A proxy or antivirus may be in the way`, unsent: true, waitMs: null };
52
+ }
53
+ return { kind: "other", text: `Could not reach ${server}${code ? ` (${code})` : ""}. Check your internet connection`, unsent: false, waitMs: null };
54
+ }
55
+ function retryAfterMs(header, now) {
56
+ if (!header) {
57
+ return null;
58
+ }
59
+ const seconds = Number(header);
60
+ const at = Number.isFinite(seconds) ? now + seconds * 1e3 : Date.parse(header);
61
+ return Number.isFinite(at) ? Math.max(0, at - now) : null;
62
+ }
63
+ function busyFailure(response, now = Date.now()) {
64
+ return {
65
+ kind: "busy",
66
+ text: `TAMsense is busy (${response.status})`,
67
+ unsent: REFUSED_STATUSES.has(response.status),
68
+ waitMs: retryAfterMs(response.headers.get("retry-after"), now)
69
+ };
70
+ }
71
+ function backoffMs(failures, baseMs, capMs, random = Math.random) {
72
+ const ceiling = Math.min(capMs, baseMs * 2 ** Math.max(0, failures - 1));
73
+ return Math.round(ceiling / 2 + random() * ceiling / 2);
74
+ }
75
+ function durationText(ms) {
76
+ const seconds = Math.round(ms / 1e3);
77
+ if (seconds < 60) {
78
+ return `${seconds} s`;
79
+ }
80
+ const minutes = Math.round(seconds / 60);
81
+ return minutes < 60 ? `${minutes} min` : `${Math.floor(minutes / 60)} h ${minutes % 60} min`;
82
+ }
83
+ function connectionWatch(print2, now = Date.now) {
84
+ let lostAt = null;
85
+ let failures = 0;
86
+ let told = /* @__PURE__ */ new Set();
87
+ let reconnect = new AbortController();
88
+ return {
89
+ failed(failure) {
90
+ lostAt ??= now();
91
+ failures += 1;
92
+ if (!told.has(failure.kind)) {
93
+ told.add(failure.kind);
94
+ print2(`${failure.text}. Trying again.`);
95
+ }
96
+ },
97
+ reached() {
98
+ if (lostAt === null) {
99
+ return;
100
+ }
101
+ print2(`Connected to TAMsense again after ${durationText(now() - lostAt)}, ${failures} request(s) failed meanwhile.`);
102
+ lostAt = null;
103
+ failures = 0;
104
+ told = /* @__PURE__ */ new Set();
105
+ reconnect.abort();
106
+ reconnect = new AbortController();
107
+ },
108
+ nextReconnect: () => reconnect.signal
109
+ };
110
+ }
7
111
 
8
112
  // src/api.ts
9
113
  var DEFAULT_SERVER = "https://my.tamsense.com";
114
+ var REPEATABLE_TIMEOUT_MS = 3e4;
115
+ var ACTION_TIMEOUT_MS = 2 * 6e4;
116
+ var UPLOAD_TIMEOUT_MS = 15 * 6e4;
117
+ var ATTEMPTS = 3;
118
+ var RETRY_BASE_MS = 1e3;
119
+ var RETRY_CAP_MS = 1e4;
120
+ var REPEATABLE_METHODS = /* @__PURE__ */ new Set(["GET", "PUT", "DELETE"]);
121
+ var ConnectionError = class extends Error {
122
+ kind;
123
+ reported;
124
+ constructor(failure, reported) {
125
+ super(failure.text);
126
+ this.name = "ConnectionError";
127
+ this.kind = failure.kind;
128
+ this.reported = reported;
129
+ }
130
+ };
131
+ function alreadyReported(error) {
132
+ return error instanceof ConnectionError && error.reported;
133
+ }
10
134
  var ApiError = class extends Error {
11
135
  status;
12
136
  code;
@@ -33,32 +157,84 @@ function textOf(value) {
33
157
  return typeof value === "string" && value.trim() ? value : null;
34
158
  }
35
159
  async function errorOf(response) {
36
- const reply3 = await response.json().catch(() => ({}));
37
- const code = textOf(reply3.data?.code);
38
- const message = textOf(reply3.message) ?? textOf(reply3.statusMessage) ?? `TAMsense answered with status ${response.status}`;
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}`;
39
163
  return new ApiError(response.status, code, message);
40
164
  }
41
- function createApi(server, token = null, fetchImpl = fetch) {
42
- async function send(method, path, headers, body) {
43
- const all = { accept: "application/json", ...headers };
44
- if (token) {
45
- all.authorization = `Bearer ${token}`;
46
- }
165
+ function isFailure(outcome) {
166
+ return "kind" in outcome;
167
+ }
168
+ function createApi(server, token = null, options = {}) {
169
+ const { fetch: fetchImpl = fetch, watch, timeoutMs = REPEATABLE_TIMEOUT_MS, actionTimeoutMs = ACTION_TIMEOUT_MS, sleep: sleep2 = (ms) => wait(ms) } = options;
170
+ async function once(sending) {
171
+ const { method, path, headers, body } = sending;
47
172
  let response;
48
173
  try {
49
- response = await fetchImpl(new URL(path, server), { method, headers: all, body, ...body instanceof ReadableStream ? { duplex: "half" } : {} });
174
+ response = await fetchImpl(new URL(path, server), { method, headers, body, signal: AbortSignal.timeout(sending.timeoutMs), ...body instanceof ReadableStream ? { duplex: "half" } : {} });
50
175
  } catch (error) {
51
- throw new Error(`Could not reach ${server}. Check your internet connection.`, { cause: error });
176
+ return networkFailure(error, server, sending.timeoutMs);
177
+ }
178
+ if (BUSY_STATUSES.has(response.status)) {
179
+ await response.body?.cancel().catch(() => {
180
+ });
181
+ return busyFailure(response);
52
182
  }
183
+ watch?.reached();
53
184
  if (!response.ok) {
54
185
  throw await errorOf(response);
55
186
  }
56
- return await response.json();
187
+ try {
188
+ return { reply: await response.json() };
189
+ } catch (error) {
190
+ if (error instanceof SyntaxError) {
191
+ throw error;
192
+ }
193
+ return networkFailure(error, server, sending.timeoutMs);
194
+ }
195
+ }
196
+ async function send(sending) {
197
+ const headers = { accept: "application/json", ...sending.headers };
198
+ if (token) {
199
+ headers.authorization = `Bearer ${token}`;
200
+ }
201
+ for (let attempt2 = 1; ; attempt2 += 1) {
202
+ const outcome = await once({ ...sending, headers });
203
+ if (!isFailure(outcome)) {
204
+ return outcome.reply;
205
+ }
206
+ if (attempt2 >= sending.attempts || !(outcome.unsent || sending.safeToRepeat)) {
207
+ watch?.failed(outcome);
208
+ throw new ConnectionError(outcome, Boolean(watch));
209
+ }
210
+ await sleep2(outcome.waitMs === null ? backoffMs(attempt2, RETRY_BASE_MS, RETRY_CAP_MS) : Math.min(outcome.waitMs, RETRY_CAP_MS));
211
+ }
57
212
  }
58
213
  return {
59
214
  server,
60
- request: (method, path, body) => send(method, path, body ? { "content-type": "application/json" } : {}, body && JSON.stringify(body)),
61
- upload: (method, path, body, contentType) => send(method, path, { "content-type": contentType }, body)
215
+ request: (method, path, body, requestOptions = {}) => {
216
+ const safeToRepeat = REPEATABLE_METHODS.has(method) || Boolean(requestOptions.safeToRepeat);
217
+ return send({
218
+ method,
219
+ path,
220
+ headers: body ? { "content-type": "application/json" } : {},
221
+ body: body && JSON.stringify(body),
222
+ attempts: ATTEMPTS,
223
+ timeoutMs: safeToRepeat ? timeoutMs : actionTimeoutMs,
224
+ safeToRepeat
225
+ });
226
+ },
227
+ // поток файла не перечитать, поэтому отправка файла не повторяется
228
+ upload: (method, path, body, contentType) => send({
229
+ method,
230
+ path,
231
+ headers: { "content-type": contentType },
232
+ body,
233
+ attempts: 1,
234
+ timeoutMs: UPLOAD_TIMEOUT_MS,
235
+ safeToRepeat: false
236
+ }),
237
+ ...watch ? { nextReconnect: watch.nextReconnect } : {}
62
238
  };
63
239
  }
64
240
 
@@ -80,7 +256,8 @@ var HELP_TEXT = `Connects this computer to TAMsense so it can work on your tasks
80
256
 
81
257
  Usage:
82
258
  ${LOGIN_COMMAND} connect this computer
83
- ${START_COMMAND} take your tasks and work on them until you press Ctrl+C
259
+ ${START_COMMAND} take your tasks and work on them until you press Ctrl+C;
260
+ run it from your code folder (or the folder above it) and it finds the code there
84
261
  ${BROWSER_COMMAND} install the headless browser for interface checks now, or show it
85
262
  ${BROWSER_COMMAND} remove delete that browser
86
263
  npx @tamsensedev/runner@latest status show the connection
@@ -340,8 +517,8 @@ async function probeCapabilities(stop) {
340
517
  }
341
518
 
342
519
  // src/claude.ts
343
- import { createSdkMcpServer, query as query2, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
344
- import { z as z4 } 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";
345
522
 
346
523
  // src/askPerson.ts
347
524
  var MAX_ASK_TRIES = 5;
@@ -401,21 +578,21 @@ function shorten(text2) {
401
578
  const line = text2.replace(/\s+/g, " ").trim();
402
579
  return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
403
580
  }
404
- function activityLine(tool5, input) {
581
+ function activityLine(tool8, input) {
405
582
  const fields = typeof input === "object" && input !== null ? input : {};
406
- if (tool5 === "Bash" && typeof fields.command === "string") {
583
+ if (tool8 === "Bash" && typeof fields.command === "string") {
407
584
  return `$ ${shorten(fields.command)}`;
408
585
  }
409
- if (FILE_TOOLS.has(tool5) && typeof fields.file_path === "string") {
410
- return `${tool5} ${shorten(fields.file_path)}`;
586
+ if (FILE_TOOLS.has(tool8) && typeof fields.file_path === "string") {
587
+ return `${tool8} ${shorten(fields.file_path)}`;
411
588
  }
412
- if (tool5 === "Skill" && typeof fields.skill === "string") {
589
+ if (tool8 === "Skill" && typeof fields.skill === "string") {
413
590
  return `Skill ${shorten(fields.skill)}`;
414
591
  }
415
- if (HELPER_TOOLS.has(tool5) && typeof fields.subagent_type === "string") {
592
+ if (HELPER_TOOLS.has(tool8) && typeof fields.subagent_type === "string") {
416
593
  return `Agent ${shorten(fields.subagent_type)}`;
417
594
  }
418
- return tool5;
595
+ return tool8;
419
596
  }
420
597
  function usageOf(modelUsage) {
421
598
  const entries = Object.values(modelUsage);
@@ -450,6 +627,7 @@ var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
450
627
  var IDENTITY_PATH = "/api/runner/me";
451
628
  var NEXT_RUN_PATH = "/api/runner/runs/next";
452
629
  var CAPABILITIES_PATH = "/api/runner/me/capabilities";
630
+ var FOUND_FOLDERS_PATH = "/api/runner/me/folders";
453
631
  var FOLDER_CHECKS_PATH = "/api/runner/folders/checks";
454
632
  function folderCheckPath(folderId) {
455
633
  return `/api/runner/folders/${folderId}`;
@@ -469,6 +647,15 @@ function permissionPath(runId, permissionId) {
469
647
  function recordingUploadPath(runId, upload, part) {
470
648
  return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
471
649
  }
650
+ function checkPath(checkId, action) {
651
+ return `/api/runner/checks/${checkId}/${action}`;
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
+ }
472
659
 
473
660
  // src/feed.ts
474
661
  var MAX_TEXT_LENGTH = 8e3;
@@ -489,31 +676,52 @@ function textField(fields, name) {
489
676
  return typeof value === "string" && value.trim() ? value : null;
490
677
  }
491
678
  function toolDraft(block, parent) {
492
- const tool5 = (block.name ?? "tool").replace(MCP_PREFIX, "");
679
+ const tool8 = (block.name ?? "tool").replace(MCP_PREFIX, "");
493
680
  const fields = fieldsOf(block.input);
494
- const base = { tool: tool5, ref: block.id ?? null, parent, failed: false };
495
- if (tool5 === "Skill" && textField(fields, "skill")) {
681
+ const base = { tool: tool8, ref: block.id ?? null, parent, failed: false };
682
+ if (tool8 === "Skill" && textField(fields, "skill")) {
496
683
  return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
497
684
  }
498
- if (HELPER_TOOLS2.has(tool5)) {
685
+ if (HELPER_TOOLS2.has(tool8)) {
499
686
  const who = textField(fields, "subagent_type") ?? "general-purpose";
500
687
  const what = textField(fields, "description");
501
688
  return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
502
689
  }
503
- if (tool5 === "submit_verdict") {
690
+ if (tool8 === "submit_verdict") {
504
691
  const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
505
692
  return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
506
693
  }
507
- if (tool5 === "hand_off") {
694
+ if (tool8 === "hand_off") {
508
695
  return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
509
696
  }
510
- if (tool5 in ATTACH_LABELS) {
511
- return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool5]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
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) };
512
699
  }
513
700
  const command = textField(fields, "command");
514
- const summary = tool5 === "Bash" && command ? `$ ${command}` : activityLine(tool5, block.input);
701
+ const summary = tool8 === "Bash" && command ? `$ ${command}` : activityLine(tool8, block.input);
515
702
  return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
516
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
+ }
517
725
  function resultText(content) {
518
726
  if (typeof content === "string") {
519
727
  return content;
@@ -527,7 +735,7 @@ function helperEndDraft(ref, parent, end, text2) {
527
735
  const stopped = end === "stopped";
528
736
  return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
529
737
  }
530
- function feedOf(message) {
738
+ function feedOf(message, calls = sourceCalls()) {
531
739
  if (message.type !== "assistant" && message.type !== "user") {
532
740
  return [];
533
741
  }
@@ -543,9 +751,15 @@ function feedOf(message) {
543
751
  } else if (message.type === "assistant" && block.type === "text" && block.text?.trim()) {
544
752
  drafts.push({ kind: "text", text: clip(block.text, MAX_TEXT_LENGTH), tool: null, ref: null, parent, failed: false });
545
753
  } else if (message.type === "assistant" && block.type === "tool_use") {
546
- 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);
547
760
  } else if (message.type === "user" && block.type === "tool_result") {
548
- 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 });
549
763
  }
550
764
  }
551
765
  return drafts;
@@ -1050,8 +1264,8 @@ function messageOf2(error) {
1050
1264
  async function decisionOf(tools, first, signal) {
1051
1265
  const now = tools.now ?? Date.now;
1052
1266
  const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
1053
- let reply3 = first;
1054
- while (reply3.decision === "wait") {
1267
+ let reply5 = first;
1268
+ while (reply5.decision === "wait") {
1055
1269
  await tools.sleep(CHECK_INTERVAL_MS, signal);
1056
1270
  if (signal.aborted) {
1057
1271
  return STOPPED_TEXT2;
@@ -1059,9 +1273,9 @@ async function decisionOf(tools, first, signal) {
1059
1273
  if (now() > giveUpAt) {
1060
1274
  return SITE_SILENT_TEXT;
1061
1275
  }
1062
- reply3 = await tools.check(reply3.id).catch(() => reply3);
1276
+ reply5 = await tools.check(reply5.id).catch(() => reply5);
1063
1277
  }
1064
- return reply3;
1278
+ return reply5;
1065
1279
  }
1066
1280
  function askPermission(tools, lastWords, folder) {
1067
1281
  return async (toolName, input, options) => {
@@ -1082,11 +1296,11 @@ function askPermission(tools, lastWords, folder) {
1082
1296
  } catch (error) {
1083
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.`);
1084
1298
  }
1085
- const reply3 = await decisionOf(tools, first, options.signal);
1086
- if (typeof reply3 === "string") {
1087
- return deny(reply3);
1299
+ const reply5 = await decisionOf(tools, first, options.signal);
1300
+ if (typeof reply5 === "string") {
1301
+ return deny(reply5);
1088
1302
  }
1089
- return reply3.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply3.message);
1303
+ return reply5.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply5.message);
1090
1304
  };
1091
1305
  }
1092
1306
  var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
@@ -1097,6 +1311,7 @@ import { join as join4, resolve as resolve3 } from "path";
1097
1311
 
1098
1312
  // src/folders.ts
1099
1313
  import { existsSync as existsSync2, realpathSync, statSync } from "fs";
1314
+ import { readdir } from "fs/promises";
1100
1315
  import { basename, isAbsolute, join as join2, relative, resolve } from "path";
1101
1316
  import process3 from "process";
1102
1317
 
@@ -1173,9 +1388,12 @@ function remoteWithoutSecrets(url) {
1173
1388
  return url;
1174
1389
  }
1175
1390
  }
1176
- function repositoryName(remoteUrl, folder) {
1391
+ function remoteRepository(remoteUrl) {
1177
1392
  const parts = remoteUrl && NETWORK_REMOTE.test(remoteUrl) ? remoteUrl.replace(/\/+$/, "").replace(/\.git$/, "").split(/[/:]/).filter(Boolean) : [];
1178
- return parts.length >= 3 ? `${parts.at(-2)}/${parts.at(-1)}` : basename(folder);
1393
+ return parts.length >= 3 ? `${parts.at(-2)}/${parts.at(-1)}` : null;
1394
+ }
1395
+ function repositoryName(remoteUrl, folder) {
1396
+ return remoteRepository(remoteUrl) ?? basename(folder);
1179
1397
  }
1180
1398
  async function remoteOf(folder) {
1181
1399
  const remotes = (await gitText(["remote"], folder))?.split("\n").map((line) => line.trim()).filter(Boolean) ?? [];
@@ -1242,7 +1460,7 @@ async function checkFolders(tools, stop) {
1242
1460
  let complained = false;
1243
1461
  while (!stop.aborted) {
1244
1462
  try {
1245
- const { checks } = await tools.api.request("POST", FOLDER_CHECKS_PATH);
1463
+ const { checks } = await tools.api.request("POST", FOLDER_CHECKS_PATH, void 0, { safeToRepeat: true });
1246
1464
  for (const check of checks) {
1247
1465
  const report = await tools.inspect(check.path).catch((error) => problem(error instanceof Error ? error.message : String(error)));
1248
1466
  await tools.api.request("POST", folderCheckPath(check.id), report);
@@ -1253,7 +1471,7 @@ async function checkFolders(tools, stop) {
1253
1471
  if (error instanceof ApiError && error.status === 404) {
1254
1472
  return;
1255
1473
  }
1256
- if (!complained && !stop.aborted) {
1474
+ if (!complained && !stop.aborted && !alreadyReported(error)) {
1257
1475
  tools.print(`Could not check the folders for TAMsense: ${error instanceof Error ? error.message : String(error)}`);
1258
1476
  complained = true;
1259
1477
  }
@@ -1261,6 +1479,44 @@ async function checkFolders(tools, stop) {
1261
1479
  await tools.sleep(CHECKS_INTERVAL_MS, stop);
1262
1480
  }
1263
1481
  }
1482
+ var SKIPPED_FOLDER = "node_modules";
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.";
1484
+ async function childRepositories(start) {
1485
+ const entries = await readdir(start, { withFileTypes: true }).catch(() => []);
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();
1487
+ }
1488
+ async function findCodeFolders(start, home) {
1489
+ const folder = resolve(start);
1490
+ if (!existsSync2(folder)) {
1491
+ return [];
1492
+ }
1493
+ const top = await gitText(["rev-parse", "--show-toplevel"], folder);
1494
+ const candidates = top && samePath(top, folder) ? [folder] : await childRepositories(folder);
1495
+ const found = [];
1496
+ for (const candidate of candidates) {
1497
+ const report = await inspectFolder(candidate, home);
1498
+ if (report.ok) {
1499
+ const { currentBranch, defaultBranch, remoteUrl } = report;
1500
+ found.push({ path: candidate, name: basename(candidate), repository: remoteRepository(remoteUrl), currentBranch, defaultBranch, remoteUrl });
1501
+ }
1502
+ }
1503
+ return found;
1504
+ }
1505
+ function foundCodeLines(start, found) {
1506
+ const lines = found.length > 0 ? found.map((folder) => `Found code: ${folder.name} (${folder.path})`) : [`No code found in ${resolve(start)}.`];
1507
+ return [...lines, MORE_CODE_TEXT];
1508
+ }
1509
+ async function reportFoundFolders(tools, start) {
1510
+ try {
1511
+ const found = await findCodeFolders(start, tools.home);
1512
+ foundCodeLines(start, found).forEach((line) => tools.print(line));
1513
+ await tools.api.request("PUT", FOUND_FOLDERS_PATH, { folders: found });
1514
+ } catch (error) {
1515
+ if (!(error instanceof ApiError && error.status === 404)) {
1516
+ tools.print(`Could not tell TAMsense about the code on this computer: ${error instanceof Error ? error.message : String(error)}`);
1517
+ }
1518
+ }
1519
+ }
1264
1520
 
1265
1521
  // src/lock.ts
1266
1522
  function keyedLock() {
@@ -1292,6 +1548,7 @@ function keyedLock() {
1292
1548
  import { existsSync as existsSync3 } from "fs";
1293
1549
  import { rm as rm2 } from "fs/promises";
1294
1550
  import { join as join3, resolve as resolve2 } from "path";
1551
+ var RESULTS_FOLDER = "results";
1295
1552
  var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
1296
1553
  var cacheLock = keyedLock();
1297
1554
  function githubRemoteUrl(fullName) {
@@ -1306,7 +1563,10 @@ function folderNames(repositories) {
1306
1563
  for (const repository of repositories) {
1307
1564
  counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
1308
1565
  }
1309
- 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
+ });
1310
1570
  }
1311
1571
  async function refreshCache(cache, repository, token, tools) {
1312
1572
  if (existsSync3(join3(cache, ".git"))) {
@@ -1463,7 +1723,7 @@ async function baseBranchRef(source, branch, remote) {
1463
1723
  }
1464
1724
  const found = own ?? fetched;
1465
1725
  if (!found) {
1466
- throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in the board settings, on the Code tab.`);
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.`);
1467
1727
  }
1468
1728
  return found;
1469
1729
  }
@@ -1476,7 +1736,7 @@ async function taskBranchRef(source, branch, remote, base) {
1476
1736
  async function startRef(source, job, local) {
1477
1737
  const base = await baseBranchRef(source, local.baseBranch, local.remote);
1478
1738
  const task = await taskBranchRef(source, job.branch, local.remote, base);
1479
- if (task || !isReadOnlyCheck(job)) {
1739
+ if (task || !isReadOnlyCheck(job) || job.severalBranches) {
1480
1740
  return task ?? base;
1481
1741
  }
1482
1742
  const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
@@ -1511,7 +1771,7 @@ function sourceOf(repository) {
1511
1771
  async function prepareRepository(job, repository, path, tools) {
1512
1772
  const source = sourceOf(repository);
1513
1773
  if ((await runGit(["rev-parse", "--show-toplevel"], { cwd: source }).catch(() => null))?.code !== 0) {
1514
- throw new Error(`${source} is no longer a git repository on this computer. Check the folder again in the board settings, on the Code tab.`);
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.`);
1515
1775
  }
1516
1776
  const local = { source, remote: await remoteOf(source), baseBranch: repository.defaultBranch };
1517
1777
  if (local.remote) {
@@ -1570,8 +1830,8 @@ async function commitAndPush(repository, local, job, identity) {
1570
1830
  }
1571
1831
  const base = await baseBranchRef(repository.path, local.baseBranch, local.remote);
1572
1832
  const commits = Number(await git(["rev-list", "--count", `${base}..HEAD`], options));
1573
- const sent2 = await pushBranch(repository.path, local, job.branch);
1574
- return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent2 };
1833
+ const sent3 = await pushBranch(repository.path, local, job.branch);
1834
+ return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent3 };
1575
1835
  }
1576
1836
  async function publishLocalWorkspace(workspace, job, identity) {
1577
1837
  const pushed = [];
@@ -1607,7 +1867,6 @@ var SAVE_TABLE_TOOL_NAME = "save_result_table";
1607
1867
  var ATTACH_FILE_TOOL_NAME = "attach_result_file";
1608
1868
  var ADD_LINK_TOOL_NAME = "add_result_link";
1609
1869
  var HAND_OVER_TOOL_NAME = "hand_over";
1610
- var RESULTS_FOLDER = "results";
1611
1870
  var MAX_NAME_LENGTH = 60;
1612
1871
  var TYPES_BY_EXTENSION = {
1613
1872
  ".pdf": "application/pdf",
@@ -1774,17 +2033,354 @@ function resultToolset(tools) {
1774
2033
  function handOverTool(tools) {
1775
2034
  return tool2(
1776
2035
  HAND_OVER_TOOL_NAME,
1777
- `Hand the task to another board of the project as a linked task, when part of the work belongs to that board. Boards you may hand to: ${tools.boards.map((board) => board.name).join(", ")}. Without autopilot a person confirms it first.`,
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.`,
1778
2037
  {
1779
2038
  board: z2.string().describe("name of the board"),
1780
- title: z2.string().describe("title of the new task on that board, one plain sentence"),
1781
- note: z2.string().describe("what you found and what that board should do; never names, emails or other personal data of clients"),
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"),
1782
2041
  wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
1783
2042
  },
1784
2043
  async (args) => await sent(() => tools.send(args))
1785
2044
  );
1786
2045
  }
1787
2046
 
2047
+ // src/schedules.ts
2048
+ import { existsSync as existsSync5 } from "fs";
2049
+ import { mkdir as mkdir3 } from "fs/promises";
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";
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
2179
+ var PROPOSE_SCHEDULE_TOOL_NAME = "propose_schedule";
2180
+ var REPORT_CHECK_TOOL_NAME = "report_check";
2181
+ var WAKE_TOOL_NAME = "wake_up_later";
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.`;
2183
+ var ALREADY_WAITING_TEXT2 = "This run already waits for something. End your turn now: the conversation continues when it comes.";
2184
+ function reply3(text2, isError = false) {
2185
+ return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2186
+ }
2187
+ function proposeScheduleTool(tools, waiting) {
2188
+ return tool5(
2189
+ PROPOSE_SCHEDULE_TOOL_NAME,
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.",
2191
+ {
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")
2205
+ },
2206
+ async (args) => {
2207
+ if (args.wait && (waiting.waitingFor !== null || waiting.waitingForSchedule !== null)) {
2208
+ return reply3(ALREADY_WAITING_TEXT2, true);
2209
+ }
2210
+ let answer;
2211
+ try {
2212
+ answer = await tools.send({ ...args, title: args.title.trim(), instruction: args.instruction.trim(), until: args.until.trim(), callWhen: args.callWhen.trim(), model: null });
2213
+ } catch (error) {
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);
2215
+ }
2216
+ if (answer.id === null) {
2217
+ return reply3(answer.text, true);
2218
+ }
2219
+ if (answer.waits) {
2220
+ waiting.waitingForSchedule = answer.id;
2221
+ }
2222
+ return reply3(answer.text);
2223
+ }
2224
+ );
2225
+ }
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
+ );
2249
+ }
2250
+ async function checkFolder(home, job) {
2251
+ if (job.taskId !== null) {
2252
+ const step = taskFolder(home, { taskId: job.taskId, stage: { columnId: job.stepColumnId } });
2253
+ if (existsSync5(step)) {
2254
+ return step;
2255
+ }
2256
+ }
2257
+ const own = tamsenseFolder(home, "schedules", `schedule-${job.scheduleId}`);
2258
+ await mkdir3(own, { recursive: true });
2259
+ return own;
2260
+ }
2261
+ var CHECK_RULES = [
2262
+ "You run one check of a TAMsense schedule on the user's own computer: a short check repeated on a timer while a task waits for something. Nobody answers during the run.",
2263
+ "You only look. Never change, fix, restart, stop or delete anything, never commit, and never send anything to people. Reading commands are fine: listing containers, requesting a local address, reading logs, git log.",
2264
+ "This is the person's own computer. Never touch programs you did not start, and stop the processes you started yourself by their process id before you finish.",
2265
+ "Keep it short: a few commands at most. If you cannot check something, say so in the summary instead of guessing.",
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.`,
2267
+ "Write in the language of the task. No em dashes and no en dashes."
2268
+ ];
2269
+ function noteLine2(note) {
2270
+ const head = `- ${note.at.slice(0, 16).replace("T", " ")} (${note.outcome}): ${note.summary}`;
2271
+ return note.note ? `${head}
2272
+ ${note.note.replace(/\n/g, "\n ")}` : head;
2273
+ }
2274
+ function buildCheckPrompt(job, folder) {
2275
+ const prompt = [
2276
+ `Schedule "${job.title}", run ${job.runNumber}${job.taskId === null ? "" : `, on TAMsense task #${job.taskId}: ${job.taskTitle}`}`,
2277
+ `What to check on each run:
2278
+ ${job.instruction}`,
2279
+ job.until ? `The schedule ends when: ${job.until}` : "The schedule runs for its time; there is no condition to wait for.",
2280
+ job.callWhen ? `Call a person when: ${job.callWhen}` : "Call a person only when something is clearly wrong.",
2281
+ job.earlier.length > 0 ? `Notes of the earlier runs, the newest last:
2282
+ ${job.earlier.map(noteLine2).join("\n")}` : "This is the first run: there are no earlier notes."
2283
+ ].join("\n\n");
2284
+ const instructions = [
2285
+ CHECK_RULES.join("\n"),
2286
+ workplaceText(job.computer, folder),
2287
+ job.sourcesText,
2288
+ job.safetyRules.trim() || null
2289
+ ].filter(Boolean).join("\n\n");
2290
+ return { prompt, instructions };
2291
+ }
2292
+ var SERVER_NAME = "tamsense";
2293
+ var MAX_CHECK_TURNS = 30;
2294
+ var MAX_ROUTINE_TURNS = 120;
2295
+ function reportCheckTool(onReport) {
2296
+ return tool5(
2297
+ REPORT_CHECK_TOOL_NAME,
2298
+ "Hand in the result of this check. Call it exactly once, at the very end.",
2299
+ {
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")
2305
+ },
2306
+ async (args) => {
2307
+ onReport({ changed: args.changed, summary: args.summary.trim(), note: args.note?.trim() || null, conditionMet: args.conditionMet, callPerson: args.callPerson });
2308
+ return reply3("Result received. You can stop now.");
2309
+ }
2310
+ );
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
+ }
2319
+ function claudeCheckRunner(startQuery) {
2320
+ return async (request) => {
2321
+ request.signal.throwIfAborted();
2322
+ const abortController = new AbortController();
2323
+ request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
2324
+ let result = null;
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;
2332
+ const messages = startQuery({
2333
+ prompt: request.prompt.prompt,
2334
+ options: {
2335
+ cwd: request.cwd,
2336
+ systemPrompt: { type: "preset", preset: "claude_code", append: request.prompt.instructions },
2337
+ settingSources: ["user", "project", "local"],
2338
+ permissionMode: "auto",
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,
2343
+ abortController,
2344
+ env: request.env,
2345
+ ...request.model === null ? {} : { model: request.model }
2346
+ }
2347
+ });
2348
+ let outcome = { completed: false, report: "Claude finished without a result.", model: null, usage: null, result: null };
2349
+ for await (const message of messages) {
2350
+ lastWords.text = textOf2(message) ?? lastWords.text;
2351
+ if (message.type === "result") {
2352
+ const finished = outcomeOf(message, "");
2353
+ outcome = { completed: finished.completed, report: finished.report, model: finished.model, usage: finished.usage, result: null };
2354
+ }
2355
+ }
2356
+ return { ...outcome, result };
2357
+ };
2358
+ }
2359
+ function checkHeartbeat(api, job, abort) {
2360
+ return async () => {
2361
+ try {
2362
+ const answer = await api.request("POST", checkPath(job.checkId, "heartbeat"), void 0, { safeToRepeat: true });
2363
+ if (!answer.keepGoing) {
2364
+ abort();
2365
+ }
2366
+ } catch {
2367
+ }
2368
+ };
2369
+ }
2370
+ function checkFinishBody(outcome) {
2371
+ return { outcome: outcome.completed && outcome.result ? "succeeded" : "failed", report: outcome.report, model: outcome.model, usage: outcome.usage, result: outcome.result };
2372
+ }
2373
+ function checkLine(body) {
2374
+ if (!body.result) {
2375
+ return `Check failed: ${body.report}`;
2376
+ }
2377
+ if (body.result.conditionMet) {
2378
+ return `Check: ${body.result.summary}. The condition came, the schedule ends.`;
2379
+ }
2380
+ return `Check: ${body.result.summary}`;
2381
+ }
2382
+ var runClaudeCheck = claudeCheckRunner(query2);
2383
+
1788
2384
  // src/specText.ts
1789
2385
  function isRecord(value) {
1790
2386
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1915,7 +2511,7 @@ var READ_ONLY_CODE_RULES = [
1915
2511
  "Check what you report with the project's own tools where they exist, and say how you checked it."
1916
2512
  ];
1917
2513
  var READ_ONLY_REPORT_RULE = "End with a short report: what you found, how you checked it, and what is left open.";
1918
- var CHECK_RULES = [
2514
+ var CHECK_RULES2 = [
1919
2515
  "The work to check is on the current branch. See it with git diff against the base branch named for each repository below, and read the code around it.",
1920
2516
  "Do not change, commit or push product code. You may run anything and create throwaway files outside the repositories.",
1921
2517
  "Judge against the task, the spec and its acceptance criteria, checking each criterion by its id (AC-1 and so on) when the spec has ids. Be specific: every problem gets a file, a line and what to do about it.",
@@ -1923,9 +2519,9 @@ var CHECK_RULES = [
1923
2519
  ];
1924
2520
  var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), attach the ones that show the result with the ${SCREENSHOT_TOOL_NAME} tool. People see them on the task without opening the code.`;
1925
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".';
1926
- var RECORDING_RULE = `Every browser you open with Playwright from Node is recorded for the task as a replay people can watch, requests from the tested site to the TAMsense tracker are blocked, so your runs never reach analytics, and requests other than GET, HEAD and OPTIONS to addresses outside this computer are aborted. In every browser script, load the recorder from the TAMSENSE_RECORDER environment variable (require(process.env.TAMSENSE_RECORDER), or createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) in an .mjs script), open each context with await recorder.newRecordedContext(browser, { title, purpose, context: { viewport } }), call await recorder.mark(page, 'what just happened') at every meaningful step, and end with await recorder.closeRecorded(context). Add video: true when the page has a canvas, a map, a video player or iframes from another site (a replay cannot show them), or when someone asks for a video. Recordings are attached to the task when the run ends; ${REPLAY_TOOL_NAME} and ${VIDEO_TOOL_NAME} attach one earlier with a better title.`;
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.`;
1927
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.";
1928
- var FOLDER_RULE = "This board works without code: there are no repositories. The current folder belongs to this task: work in it and keep your throwaway files here.";
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.";
1929
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.";
1930
2526
  var FOLDER_DO_RULES = [
1931
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.`,
@@ -1985,7 +2581,7 @@ var SPEC_RULES = [
1985
2581
  var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
1986
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.`;
1987
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.";
1988
- var RESEARCH_BRIEF_RULE = "Write the research brief as you go: findings (what the sources say that matters, each with where: the source), dataFacts (numbers and facts, each with its source; empty when none), approach (2 to 4 sentences), codeMap (always empty: this board has no code), assumptions (each corner you settled with a default, with the reason) and pendingDecisions (decisions that depend on an answer to this round; empty when none).";
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).";
1989
2585
  var RESULT_BRIEF_RULES = [
1990
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.',
1991
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".',
@@ -2053,12 +2649,17 @@ function folderText(root, results) {
2053
2649
  return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
2054
2650
  ${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
2055
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
+ }
2056
2657
  function handOverText(boards) {
2057
2658
  const names = (boards ?? []).map((board) => board.name);
2058
2659
  if (names.length === 0) {
2059
2660
  return null;
2060
2661
  }
2061
- return `When part of the work belongs to another board, hand it over with the ${HAND_OVER_TOOL_NAME} tool: ${names.join(", ")}. Write a plain title and a note on what you found and what that board should do; never put names, emails or other personal data of clients in the note. With wait true this task waits on its step until the linked task is done, then it goes on and you get its result note.`;
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.`;
2062
2663
  }
2063
2664
  function handoffText(targets, isCheck) {
2064
2665
  if (targets.length === 0) {
@@ -2134,7 +2735,7 @@ function briefGoal(job, work2) {
2134
2735
  }
2135
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}`;
2136
2737
  }
2137
- function buildBriefPrompt(job, work2, workspace) {
2738
+ function buildBriefPrompt(job, work2, workspace, options) {
2138
2739
  const prompt = [
2139
2740
  briefGoal(job, work2),
2140
2741
  job.description,
@@ -2153,8 +2754,9 @@ ${JSON.stringify(job.spec)}` : null,
2153
2754
  workplaceText(job.computer, workspace.root),
2154
2755
  stepLine(job.stage),
2155
2756
  job.stage.instructions,
2757
+ job.sourcesText,
2156
2758
  contextText(job.projectContext),
2157
- FOLDER_RULE,
2759
+ `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2158
2760
  RESULT_HOME_RULE,
2159
2761
  `How to work:
2160
2762
  ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
@@ -2164,7 +2766,7 @@ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work
2164
2766
  ].filter(Boolean).join("\n\n");
2165
2767
  return { prompt, instructions };
2166
2768
  }
2167
- function buildSpecPrompt(job, work2, workspace) {
2769
+ function buildSpecPrompt(job, work2, workspace, options) {
2168
2770
  const prompt = [
2169
2771
  specGoal(job, work2),
2170
2772
  job.description,
@@ -2183,10 +2785,12 @@ ${JSON.stringify(job.spec)}` : null,
2183
2785
  workplaceText(job.computer, workspace.root),
2184
2786
  stepLine(job.stage),
2185
2787
  job.stage.instructions,
2788
+ job.sourcesText,
2186
2789
  contextText(job.projectContext),
2187
2790
  `The repositories for this task are checked out in the current folder, one subfolder each:
2188
2791
  ${bulletList(repositoryLines(job, workspace))}`,
2189
2792
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2793
+ taskResultsText(options.results),
2190
2794
  READ_ONLY_RULE,
2191
2795
  `How to work:
2192
2796
  ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
@@ -2213,6 +2817,7 @@ function buildFolderPrompt(job, workspace, options) {
2213
2817
  job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
2214
2818
  job.review && `Notes to address:
2215
2819
  ${job.review}`,
2820
+ job.scheduleNote,
2216
2821
  teamMessagesText(job.messages)
2217
2822
  ].filter(Boolean).join("\n\n");
2218
2823
  const rules = isCheck ? FOLDER_CHECK_RULES : [...FOLDER_DO_RULES, ...job.review ? [FOLDER_REWORK_RULE] : [], FOLDER_REPORT_RULE];
@@ -2222,6 +2827,7 @@ ${job.review}`,
2222
2827
  workplaceText(job.computer, workspace.root),
2223
2828
  stepLine(job.stage),
2224
2829
  job.stage.instructions,
2830
+ job.sourcesText,
2225
2831
  contextText(job.projectContext),
2226
2832
  `${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
2227
2833
  `How to work:
@@ -2232,6 +2838,7 @@ ${bulletList(rules)}`,
2232
2838
  peopleText(people),
2233
2839
  isCheck ? null : handOverText(job.handOverBoards),
2234
2840
  handoffText(job.allowedHandoffs, isCheck),
2841
+ job.canSchedule && SCHEDULE_RULE,
2235
2842
  options.browser && BROWSER_RULE
2236
2843
  ].filter(Boolean).join("\n\n");
2237
2844
  return { prompt, instructions };
@@ -2242,7 +2849,7 @@ function buildTaskPrompt(job, workspace, options = {}) {
2242
2849
  if (!job.specWork) {
2243
2850
  throw new Error(SPEC_WORK_MISSING);
2244
2851
  }
2245
- 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);
2246
2853
  }
2247
2854
  if (isFolderJob(job)) {
2248
2855
  return buildFolderPrompt(job, workspace, options);
@@ -2258,21 +2865,24 @@ function buildTaskPrompt(job, workspace, options = {}) {
2258
2865
  job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
2259
2866
  job.review && `Review comments to address:
2260
2867
  ${job.review}`,
2868
+ job.scheduleNote,
2261
2869
  teamMessagesText(job.messages)
2262
2870
  ].filter(Boolean).join("\n\n");
2263
2871
  const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
2264
2872
  const localApp = localAppRule(job, isCheck, canAsk);
2265
- const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : readOnly ? [...READ_ONLY_CODE_RULES, DEPENDENCIES_RULE, localApp, ...evidence, READ_ONLY_REPORT_RULE] : [...codeRules(job), ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
2873
+ const rules = isCheck ? [...CHECK_RULES2, DEPENDENCIES_RULE, localApp, ...evidence] : readOnly ? [...READ_ONLY_CODE_RULES, DEPENDENCIES_RULE, localApp, ...evidence, READ_ONLY_REPORT_RULE] : [...codeRules(job), ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
2266
2874
  const instructions = [
2267
2875
  canAsk ? ASKING_RULE : UNATTENDED_RULE,
2268
2876
  MACHINE_RULE,
2269
2877
  workplaceText(job.computer, workspace.root),
2270
2878
  stepLine(job.stage),
2271
2879
  job.stage.instructions,
2880
+ job.sourcesText,
2272
2881
  contextText(job.projectContext),
2273
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}:
2274
2883
  ${bulletList(repositoryLines(job, workspace))}`,
2275
2884
  isLocalJob(job) && LOCAL_FOLDERS_RULE,
2885
+ taskResultsText(options.results),
2276
2886
  `How to work:
2277
2887
  ${bulletList(rules)}`,
2278
2888
  safetyText(job),
@@ -2280,141 +2890,142 @@ ${bulletList(rules)}`,
2280
2890
  APPROVAL_RULE,
2281
2891
  peopleText(people),
2282
2892
  handoffText(job.allowedHandoffs, isCheck),
2893
+ job.canSchedule && SCHEDULE_RULE,
2283
2894
  options.browser && BROWSER_RULE
2284
2895
  ].filter(Boolean).join("\n\n");
2285
2896
  return { prompt, instructions };
2286
2897
  }
2287
2898
 
2288
2899
  // src/specTools.ts
2289
- import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
2290
- import { z as z3 } from "zod";
2900
+ import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
2901
+ import { z as z6 } from "zod";
2291
2902
  var MAX_FIRST_ROUND = 4;
2292
- var questionSchema = z3.object({
2293
- id: z3.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
2294
- question: z3.string().describe("One plain sentence the owner can answer without reading code"),
2295
- whyItMatters: z3.string().describe("One line: what changes in the result depending on the answer"),
2296
- options: z3.array(z3.object({
2297
- label: z3.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
2298
- description: z3.string().describe("One sentence: what happens if this option is chosen"),
2299
- recommended: z3.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
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")
2300
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'),
2301
- multiSelect: z3.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")
2302
2913
  });
2303
- var briefSchema = z3.object({
2304
- findings: z3.array(z3.object({
2305
- fact: z3.string().describe("What the code does today that matters for this task"),
2306
- where: z3.string().describe("Where it is: owner/repo path:line")
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")
2307
2918
  })).describe("Findings from the code"),
2308
- dataFacts: z3.array(z3.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
2309
- approach: z3.string().describe("The approach you would take, in 2-4 sentences"),
2310
- codeMap: z3.array(z3.object({
2311
- repo: z3.string().describe("Repository full name (owner/name)"),
2312
- file: z3.string().describe("Path relative to the repository root"),
2313
- role: z3.string().describe("Why it matters: what to change or reuse there")
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")
2314
2925
  })).describe("The files the change will touch or must reuse"),
2315
- assumptions: z3.array(z3.object({
2316
- text: z3.string().describe("The default you chose"),
2317
- reason: z3.string().describe("Why it is safe or why it was chosen")
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")
2318
2929
  })).describe("Corners settled with a default instead of a question"),
2319
- pendingDecisions: z3.array(z3.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")
2320
2931
  });
2321
- var questionsPayloadSchema = z3.object({
2932
+ var questionsPayloadSchema = z6.object({
2322
2933
  brief: briefSchema,
2323
- questions: z3.array(questionSchema).max(MAX_FIRST_ROUND).describe(`0-${MAX_FIRST_ROUND} questions that pass the question test, most important first. An empty list when research settled everything`)
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`)
2324
2935
  });
2325
- var specSchema = z3.object({
2326
- goal: z3.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
2327
- context: z3.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
2328
- inScope: z3.array(z3.string()).describe("What this task delivers, one item each"),
2329
- outOfScope: z3.array(z3.string()).describe("What this task does not do: nearby additions a coder might be tempted to make and approaches that are ruled out. Never empty"),
2330
- decisions: z3.array(z3.object({
2331
- question: z3.string(),
2332
- answer: z3.string(),
2333
- by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
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")
2334
2945
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2335
- codeMap: z3.array(z3.object({
2336
- repo: z3.string().describe("Repository full name (owner/name)"),
2337
- file: z3.string().describe("Path relative to the repository root"),
2338
- role: z3.string().describe("What the coder needs to know about this file"),
2339
- use: z3.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
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")
2340
2951
  })).describe("The files a coder must read before starting"),
2341
- changes: z3.array(z3.object({
2342
- repo: z3.string().describe("Repository full name (owner/name) where the change happens"),
2343
- file: z3.string().describe("Path to the file, relative to the repository root"),
2344
- action: z3.enum(["create", "modify", "delete"]),
2345
- description: z3.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
2346
- produces: z3.array(z3.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
2347
- consumes: z3.array(z3.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
2348
- acceptance: z3.array(z3.string()).describe("Ids of the criteria this change makes true, like AC-1")
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")
2349
2960
  })).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
2350
- acceptance: z3.array(z3.object({
2351
- id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2352
- kind: z3.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
2353
- text: z3.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
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')
2354
2965
  })).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
2355
- testPlan: z3.object({
2356
- automated: z3.array(z3.object({
2357
- repo: z3.string(),
2358
- command: z3.string().describe("The exact command to run"),
2359
- expect: z3.string().describe("What passing looks like")
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")
2360
2971
  })).describe("At least one command"),
2361
- newTests: z3.array(z3.object({
2362
- seam: z3.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
2363
- covers: z3.array(z3.string()).describe("Ids of the criteria it covers")
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")
2364
2975
  })),
2365
- manual: z3.array(z3.object({
2366
- page: z3.string().describe("The page or screen"),
2367
- steps: z3.string().describe("What a person does and sees")
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")
2368
2979
  }))
2369
2980
  }),
2370
- risks: z3.array(z3.object({
2371
- risk: z3.string(),
2372
- mitigation: z3.string()
2981
+ risks: z6.array(z6.object({
2982
+ risk: z6.string(),
2983
+ mitigation: z6.string()
2373
2984
  })).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
2374
- riskLevel: z3.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
2375
- assumptions: z3.array(z3.object({
2376
- text: z3.string().describe("The default you chose"),
2377
- reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
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")
2378
2989
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2379
- openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
2380
- split: z3.array(z3.string()).describe("When the task holds more than one shippable goal or more than about 8 files: the separate tasks you propose, one line each. Empty otherwise")
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")
2381
2992
  });
2382
- var resultBriefSchema = z3.object({
2383
- format: z3.literal("brief").describe('Always "brief"'),
2384
- goal: z3.string().describe("What the work is for, for whom, and the outcome people will notice"),
2385
- context: z3.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
2386
- contains: z3.array(z3.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
2387
- acceptance: z3.array(z3.object({
2388
- id: z3.string().describe("AC-1, AC-2 and so on, in order"),
2389
- text: z3.string().describe("One observable property of the result that a checker can confirm by reading it")
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")
2390
3001
  })).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
2391
- constraints: z3.array(z3.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
2392
- outOfScope: z3.array(z3.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
2393
- sources: z3.array(z3.object({
2394
- source: z3.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
2395
- use: z3.string().describe("What to take from it")
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")
2396
3007
  })).describe("Sources the work must use. Empty when the task itself is enough"),
2397
- approver: z3.string().describe("Who approves the result, as the task or the board names them"),
2398
- decisions: z3.array(z3.object({
2399
- question: z3.string(),
2400
- answer: z3.string(),
2401
- by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
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")
2402
3013
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
2403
- assumptions: z3.array(z3.object({
2404
- text: z3.string().describe("The default you chose"),
2405
- reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
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")
2406
3017
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
2407
- openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
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")
2408
3019
  });
2409
- function reply2(text2, isError = false) {
3020
+ function reply4(text2, isError = false) {
2410
3021
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
2411
3022
  }
2412
3023
  function unreachable(error, toolName) {
2413
3024
  const message = error instanceof Error ? error.message : String(error);
2414
- return reply2(`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);
2415
3026
  }
2416
3027
  function questionsTool(review, onSubmit) {
2417
- return tool3(
3028
+ return tool6(
2418
3029
  SUBMIT_QUESTIONS_TOOL_NAME,
2419
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.",
2420
3031
  questionsPayloadSchema.shape,
@@ -2426,10 +3037,10 @@ function questionsTool(review, onSubmit) {
2426
3037
  return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
2427
3038
  }
2428
3039
  if (!verdict.accepted) {
2429
- return reply2(verdict.feedback, true);
3040
+ return reply4(verdict.feedback, true);
2430
3041
  }
2431
3042
  onSubmit(args);
2432
- return reply2(args.questions.length > 0 ? "Questions saved. The owner answers them on the task page after this run. Finish now with a reply of one or two sentences." : "Saved with no questions: the spec is written next. Finish now with a reply of one or two sentences.");
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.");
2433
3044
  }
2434
3045
  );
2435
3046
  }
@@ -2437,7 +3048,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
2437
3048
  function specTool(review, onSubmit, builderCheckRan) {
2438
3049
  let repairs = 0;
2439
3050
  let reminded = false;
2440
- return tool3(
3051
+ return tool6(
2441
3052
  SUBMIT_SPEC_TOOL_NAME,
2442
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.",
2443
3054
  specSchema.shape,
@@ -2450,20 +3061,20 @@ function specTool(review, onSubmit, builderCheckRan) {
2450
3061
  }
2451
3062
  if (!verdict.accepted) {
2452
3063
  repairs += 1;
2453
- return reply2(verdict.feedback, true);
3064
+ return reply4(verdict.feedback, true);
2454
3065
  }
2455
3066
  if (!builderCheckRan() && !reminded) {
2456
3067
  reminded = true;
2457
- return reply2(BUILDER_CHECK_FIRST, true);
3068
+ return reply4(BUILDER_CHECK_FIRST, true);
2458
3069
  }
2459
3070
  onSubmit({ spec: args, problems: verdict.problems });
2460
- return reply2(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
3071
+ return reply4(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
2461
3072
  }
2462
3073
  );
2463
3074
  }
2464
3075
  function briefTool(review, onSubmit) {
2465
3076
  let repairs = 0;
2466
- return tool3(
3077
+ return tool6(
2467
3078
  SUBMIT_SPEC_TOOL_NAME,
2468
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.",
2469
3080
  resultBriefSchema.shape,
@@ -2476,10 +3087,10 @@ function briefTool(review, onSubmit) {
2476
3087
  }
2477
3088
  if (!verdict.accepted) {
2478
3089
  repairs += 1;
2479
- return reply2(verdict.feedback, true);
3090
+ return reply4(verdict.feedback, true);
2480
3091
  }
2481
3092
  onSubmit({ spec: args, problems: verdict.problems });
2482
- return reply2(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.");
2483
3094
  }
2484
3095
  );
2485
3096
  }
@@ -2580,10 +3191,13 @@ function usageLimitWatch() {
2580
3191
 
2581
3192
  // src/claude.ts
2582
3193
  var NO_RESULT_TEXT = "Claude finished without a result.";
2583
- var SERVER_NAME = "tamsense";
3194
+ var SERVER_NAME2 = "tamsense";
2584
3195
  var CAPTION_FIELDS = {
2585
- title: z4.string().describe('short name of what it shows, at most 60 characters: "Design option 1", "Feature testing", "Feature testing, attempt 2". Never generic names like "screenshot1"'),
2586
- purpose: z4.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
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"')
3198
+ };
3199
+ var NOTES_FIELD = {
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."')
2587
3201
  };
2588
3202
  var ATTACH_TOOLS = {
2589
3203
  screenshot: {
@@ -2593,32 +3207,32 @@ var ATTACH_TOOLS = {
2593
3207
  },
2594
3208
  replay: {
2595
3209
  name: REPLAY_TOOL_NAME,
2596
- description: "Attach a browser replay (the .rrweb.ndjson file the TAMsense recorder wrote) to the task now, with your own title and purpose. Every recording is attached at the end of the run anyway; use this to attach one earlier or to name it better. Close the browser context first so the file is complete.",
3210
+ description: "Attach a browser replay (the .rrweb.ndjson file the TAMsense recorder wrote) to the task now, with your own title, purpose and notes on what you tested and what happened. Every recording is attached at the end of the run anyway; use this to attach one earlier or to name it better. Close the browser context first so the file is complete.",
2597
3211
  path: "absolute path of the .rrweb.ndjson replay file"
2598
3212
  },
2599
3213
  video: {
2600
3214
  name: VIDEO_TOOL_NAME,
2601
- description: "Attach a browser video (.webm or .mp4, up to 200 MB) to the task now, with your own title and purpose. Videos recorded with video: true are attached at the end of the run anyway. Close the browser context first so the file is complete.",
3215
+ description: "Attach a browser video (.webm or .mp4, up to 200 MB) to the task now, with your own title, purpose and notes on what you tested and what happened. Videos recorded with video: true are attached at the end of the run anyway. Close the browser context first so the file is complete.",
2602
3216
  path: "absolute path of the video file"
2603
3217
  }
2604
3218
  };
2605
3219
  function attachTool(kind, attach) {
2606
3220
  const spec = ATTACH_TOOLS[kind];
2607
- return tool4(
3221
+ return tool7(
2608
3222
  spec.name,
2609
3223
  spec.description,
2610
- { path: z4.string().describe(spec.path), ...CAPTION_FIELDS },
3224
+ { path: z7.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
2611
3225
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
2612
3226
  );
2613
3227
  }
2614
3228
  function verdictTool(onVerdict) {
2615
- return tool4(
3229
+ return tool7(
2616
3230
  VERDICT_TOOL_NAME,
2617
3231
  "Hand in the result of this check. Call it exactly once, at the very end.",
2618
3232
  {
2619
- passed: z4.boolean().describe("true only if nothing must be fixed"),
2620
- report: z4.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
2621
- couldNotRun: z4.boolean().optional().describe("true when the check could not run at all because of this computer: Docker not running, no local setup, or settings that point outside this computer; passed must be false and the report gives the exact reason and what a person should do")
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")
2622
3236
  },
2623
3237
  async (args) => {
2624
3238
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -2627,13 +3241,13 @@ function verdictTool(onVerdict) {
2627
3241
  );
2628
3242
  }
2629
3243
  function handoffTool(targets, onHandoff) {
2630
- return tool4(
3244
+ return tool7(
2631
3245
  HANDOFF_TOOL_NAME,
2632
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.",
2633
3247
  {
2634
- to: z4.string().describe("name or id of the step to hand the task to"),
2635
- outcome: z4.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
2636
- note: z4.string().describe("for the next agent and the owner: what was done, what to check, what is open")
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")
2637
3251
  },
2638
3252
  async (args) => {
2639
3253
  const choice = chooseHandoff(targets, args);
@@ -2646,25 +3260,25 @@ function handoffTool(targets, onHandoff) {
2646
3260
  );
2647
3261
  }
2648
3262
  function askPersonTool(asking, waiting) {
2649
- return tool4(
3263
+ return tool7(
2650
3264
  ASK_PERSON_TOOL_NAME,
2651
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.",
2652
3266
  {
2653
- to: z4.union([z4.number(), z4.string()]).describe("id of the person from the people list in your instructions: the assignee first; a topic question goes to the person whose role fits"),
2654
- why: z4.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
2655
- question: z4.string().describe("one plain question the person can answer without reading the code"),
2656
- context: z4.string().optional().describe("one line on what you saw that led to the question"),
2657
- tried: z4.array(z4.object({
2658
- did: z4.string().describe("the command or step"),
2659
- error: z4.string().optional().describe("the exact error line, if there was one"),
2660
- result: z4.string().describe("what it showed, one line")
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")
2661
3275
  })).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
2662
- options: z4.array(z4.object({
2663
- label: z4.string().describe("a short answer the person can pick"),
2664
- detail: z4.string().describe("one line on what happens if it is picked")
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")
2665
3279
  })).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
2666
- blocking: z4.boolean().describe("true when you cannot go on without the answer"),
2667
- assumption: z4.string().optional().describe("when blocking is false: what you continue with until the answer comes")
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")
2668
3282
  },
2669
3283
  async (args) => {
2670
3284
  if (waiting.waitingFor !== null) {
@@ -2674,16 +3288,16 @@ function askPersonTool(asking, waiting) {
2674
3288
  if ("error" in choice) {
2675
3289
  return { content: [{ type: "text", text: choice.error }], isError: true };
2676
3290
  }
2677
- let reply3;
3291
+ let reply5;
2678
3292
  try {
2679
- reply3 = await asking.send(choice.body);
3293
+ reply5 = await asking.send(choice.body);
2680
3294
  } catch (error) {
2681
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 };
2682
3296
  }
2683
3297
  if (choice.body.blocking) {
2684
- waiting.waitingFor = reply3.id;
3298
+ waiting.waitingFor = reply5.id;
2685
3299
  }
2686
- return { content: [{ type: "text", text: askedText(reply3.name, choice.body) }] };
3300
+ return { content: [{ type: "text", text: askedText(reply5.name, choice.body) }] };
2687
3301
  }
2688
3302
  );
2689
3303
  }
@@ -2727,11 +3341,13 @@ function tamsenseServer(request, submitted) {
2727
3341
  submitted.handoff = handoff;
2728
3342
  })] : [],
2729
3343
  ...request.results ? resultToolset(request.results) : [],
2730
- ...request.handOver ? [handOverTool(request.handOver)] : []
3344
+ ...request.handOver ? [handOverTool(request.handOver)] : [],
3345
+ ...request.schedules ? [proposeScheduleTool(request.schedules, submitted), wakeUpLaterTool(request.schedules)] : [],
3346
+ ...sourceToolset(request.sources)
2731
3347
  ];
2732
3348
  return {
2733
- server: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }),
2734
- allowed: tools.map((item) => `mcp__${SERVER_NAME}__${item.name}`)
3349
+ server: createSdkMcpServer2({ name: SERVER_NAME2, version: "1.0.0", tools }),
3350
+ allowed: tools.map((item) => `mcp__${SERVER_NAME2}__${item.name}`)
2735
3351
  };
2736
3352
  }
2737
3353
  function modelOptions(request) {
@@ -2754,6 +3370,7 @@ async function attempt(request, context, resume) {
2754
3370
  let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
2755
3371
  const tamsense = tamsenseServer(request, context.submitted);
2756
3372
  const shown = hiddenStepsFilter();
3373
+ const calls = sourceCalls(request.sources?.tools);
2757
3374
  const helpers = helperTasks();
2758
3375
  const messages = context.startQuery({
2759
3376
  prompt: request.input.start(request.prompt.prompt),
@@ -2767,7 +3384,7 @@ async function attempt(request, context, resume) {
2767
3384
  // режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
2768
3385
  canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
2769
3386
  plugins: [{ type: "local", path: request.library }],
2770
- mcpServers: { [SERVER_NAME]: tamsense.server },
3387
+ mcpServers: { [SERVER_NAME2]: tamsense.server },
2771
3388
  allowedTools: tamsense.allowed,
2772
3389
  abortController: context.abortController,
2773
3390
  env: request.env,
@@ -2788,7 +3405,7 @@ async function attempt(request, context, resume) {
2788
3405
  messages.close();
2789
3406
  return { outcome, sessionId, resumeFailed: true };
2790
3407
  }
2791
- 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)]) {
2792
3409
  if (draft.kind === "agent" && draft.parent === null) {
2793
3410
  context.submitted.helperStarted = true;
2794
3411
  }
@@ -2820,7 +3437,7 @@ function claudeRunner(startQuery) {
2820
3437
  request.input.close();
2821
3438
  abortController.abort();
2822
3439
  }, { once: true });
2823
- const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false, waitingFor: null };
3440
+ const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false, waitingFor: null, waitingForSchedule: null };
2824
3441
  const limit = usageLimitWatch();
2825
3442
  const context = { startQuery, abortController, submitted, limit, lastWords: { text: "" } };
2826
3443
  try {
@@ -2830,16 +3447,16 @@ function claudeRunner(startQuery) {
2830
3447
  }
2831
3448
  const pausedUntil = limit.pausedUntil(result.outcome.completed);
2832
3449
  const outcome = pausedUntil ? { ...result.outcome, completed: false, report: USAGE_LIMIT_REPORT } : result.outcome;
2833
- return { ...outcome, verdict: submitted.verdict, handoff: submitted.handoff, questions: submitted.questions, spec: submitted.spec, sessionId: result.sessionId, pausedUntil, waitingFor: submitted.waitingFor };
3450
+ return { ...outcome, verdict: submitted.verdict, handoff: submitted.handoff, questions: submitted.questions, spec: submitted.spec, sessionId: result.sessionId, pausedUntil, waitingFor: submitted.waitingFor, waitingForSchedule: submitted.waitingForSchedule };
2834
3451
  } finally {
2835
3452
  request.input.close();
2836
3453
  }
2837
3454
  };
2838
3455
  }
2839
- var runClaude = claudeRunner(query2);
3456
+ var runClaude = claudeRunner(query3);
2840
3457
 
2841
3458
  // src/config.ts
2842
- import { mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3459
+ import { mkdir as mkdir4, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
2843
3460
  import { homedir as homedir2 } from "os";
2844
3461
  import { dirname as dirname2, join as join6 } from "path";
2845
3462
  var OWNER_ONLY_FILE = 384;
@@ -2872,7 +3489,7 @@ async function readConfig(path = configPath()) {
2872
3489
  }
2873
3490
  }
2874
3491
  async function writeConfig(config, path = configPath()) {
2875
- await mkdir3(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
3492
+ await mkdir4(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
2876
3493
  await writeFile3(path, `${JSON.stringify(config, null, 4)}
2877
3494
  `, { mode: OWNER_ONLY_FILE });
2878
3495
  }
@@ -2894,14 +3511,14 @@ async function login(tools, request) {
2894
3511
  tools.openBrowser(pageUrl);
2895
3512
  while (tools.now() < expiresAt) {
2896
3513
  await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
2897
- const reply3 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
2898
- if (reply3.status === "connected") {
2899
- return { token: reply3.token, device: reply3.device };
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 };
2900
3517
  }
2901
- if (reply3.status === "denied") {
3518
+ if (reply5.status === "denied") {
2902
3519
  throw new Error(DENIED_TEXT);
2903
3520
  }
2904
- if (reply3.status === "expired") {
3521
+ if (reply5.status === "expired") {
2905
3522
  break;
2906
3523
  }
2907
3524
  }
@@ -2909,7 +3526,7 @@ async function login(tools, request) {
2909
3526
  }
2910
3527
 
2911
3528
  // src/recorderEnv.ts
2912
- import { existsSync as existsSync5 } from "fs";
3529
+ import { existsSync as existsSync6 } from "fs";
2913
3530
  import { join as join8 } from "path";
2914
3531
  import { fileURLToPath } from "url";
2915
3532
 
@@ -2927,6 +3544,7 @@ var REPLAY_SUFFIX = ".rrweb.ndjson";
2927
3544
  var META_SUFFIX = ".json";
2928
3545
  var MAX_TITLE_LENGTH = 60;
2929
3546
  var MAX_PURPOSE_LENGTH = 160;
3547
+ var MAX_NOTES_LENGTH = 600;
2930
3548
  var DEFAULT_TRACKER_ORIGINS = ["https://my.tamsense.com"];
2931
3549
  var READ_METHODS = ["GET", "HEAD", "OPTIONS"];
2932
3550
  var BINDING = "__tamsenseRecord";
@@ -2934,7 +3552,10 @@ var STATE_KEY = /* @__PURE__ */ Symbol.for("tamsense.recorder");
2934
3552
  var FLUSH_MS = 100;
2935
3553
  var MAX_SESSION_BYTES = 300 * 1024 * 1024;
2936
3554
  var GAP_CHECK_MS = 1500;
3555
+ var RRWEB_INCREMENTAL = 3;
2937
3556
  var RRWEB_META = 4;
3557
+ var RRWEB_SOURCE_MOUSE_INTERACTION = 2;
3558
+ var RRWEB_SOURCE_INPUT = 5;
2938
3559
  function state() {
2939
3560
  const holder = globalThis;
2940
3561
  holder[STATE_KEY] ??= { contexts: /* @__PURE__ */ new WeakMap(), pages: /* @__PURE__ */ new WeakMap(), instrumented: /* @__PURE__ */ new WeakSet(), script: null, hooked: false };
@@ -2985,6 +3606,13 @@ function checkCaption(caption) {
2985
3606
  }
2986
3607
  return { title, purpose };
2987
3608
  }
3609
+ function checkNotes(notes) {
3610
+ const text2 = typeof notes === "string" ? notes.trim() : "";
3611
+ if (text2.length > MAX_NOTES_LENGTH) {
3612
+ throw new Error(`Keep the notes within ${MAX_NOTES_LENGTH} characters: what you opened, clicked and expected, and whether it passed.`);
3613
+ }
3614
+ return text2;
3615
+ }
2988
3616
  function clip2(text2, max) {
2989
3617
  return text2.length > max ? `${text2.slice(0, max - 1).trimEnd()}\u2026` : text2;
2990
3618
  }
@@ -3015,11 +3643,22 @@ const send = (extra) => {
3015
3643
  buffer = [];
3016
3644
  return window.${BINDING}(JSON.stringify({ events, ...extra })).catch(() => {});
3017
3645
  };
3646
+ const urgent = (event) => event.type === ${RRWEB_INCREMENTAL} && (event.data.source === ${RRWEB_SOURCE_MOUSE_INTERACTION} || event.data.source === ${RRWEB_SOURCE_INPUT});
3647
+ const placeAtTarget = (interaction) => {
3648
+ if (interaction.x !== 0 || interaction.y !== 0) return;
3649
+ const node = rrweb.record.mirror.getNode(interaction.id);
3650
+ if (!node || typeof node.getBoundingClientRect !== 'function') return;
3651
+ const box = node.getBoundingClientRect();
3652
+ interaction.x = Math.round(box.left + box.width / 2);
3653
+ interaction.y = Math.round(box.top + box.height / 2);
3654
+ };
3018
3655
  rrweb.record({
3019
3656
  emit(event) {
3020
3657
  buffer.push(event);
3021
- if (!timer) timer = setTimeout(send, ${FLUSH_MS});
3658
+ if (urgent(event)) send();
3659
+ else if (!timer) timer = setTimeout(send, ${FLUSH_MS});
3022
3660
  },
3661
+ hooks: { mouseInteraction: placeAtTarget },
3023
3662
  maskInputOptions: { password: true },
3024
3663
  });
3025
3664
  const crossOrigin = (frame) => { try { return new URL(frame.src, location.href).origin !== location.origin; } catch { return false; } };
@@ -3032,6 +3671,7 @@ const checkGaps = () => {
3032
3671
  };
3033
3672
  window.__tamsenseFlush = () => send();
3034
3673
  window.__tamsenseMark = (label) => { rrweb.record.addCustomEvent('${MARK_TAG}', { label }); return send(); };
3674
+ addEventListener('beforeunload', () => { send(); });
3035
3675
  addEventListener('pagehide', () => { send(); });
3036
3676
  addEventListener('load', () => setTimeout(checkGaps, ${GAP_CHECK_MS}));
3037
3677
  })();`;
@@ -3050,7 +3690,7 @@ var Session = class {
3050
3690
  const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
3051
3691
  this.file = join7(dir, `${id}${REPLAY_SUFFIX}`);
3052
3692
  this.metaFile = join7(dir, `${id}${META_SUFFIX}`);
3053
- this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic };
3693
+ this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic, notes: "" };
3054
3694
  writeFileSync(this.file, "");
3055
3695
  this.save();
3056
3696
  }
@@ -3094,8 +3734,8 @@ var Session = class {
3094
3734
  }
3095
3735
  }
3096
3736
  summary() {
3097
- const { title, purpose, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta;
3098
- return { title, purpose, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites };
3737
+ const { title, purpose, notes, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta;
3738
+ return { title, purpose, notes, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites };
3099
3739
  }
3100
3740
  };
3101
3741
  function countBlocked(route, counter) {
@@ -3222,7 +3862,7 @@ if (process4.env[RECORDINGS_DIR_ENV]) {
3222
3862
  var RECORDER_ENV = "TAMSENSE_RECORDER";
3223
3863
  function bundledRecorder() {
3224
3864
  const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
3225
- return existsSync5(path) ? path : null;
3865
+ return existsSync6(path) ? path : null;
3226
3866
  }
3227
3867
  function recordingsFolder(home, runId) {
3228
3868
  return join8(home, ".tamsense", "recordings", `run-${runId}`);
@@ -3251,11 +3891,18 @@ function describeDevice(identity) {
3251
3891
  return identity.device.team ? `a team computer of ${identity.projectName}` : `the personal computer of ${identity.email ?? "its owner"}`;
3252
3892
  }
3253
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
+
3254
3901
  // src/system.ts
3255
3902
  import { spawn as spawn3 } from "child_process";
3256
3903
  import { hostname } from "os";
3257
3904
  import { delimiter as delimiter2 } from "path";
3258
- import { setTimeout as wait } from "timers/promises";
3905
+ import { setTimeout as wait2 } from "timers/promises";
3259
3906
  var PLATFORM_LABELS = {
3260
3907
  win32: "Windows",
3261
3908
  darwin: "macOS",
@@ -3291,14 +3938,108 @@ function withNodeOnPath(env, nodeDir, separator = delimiter2) {
3291
3938
  return { ...env, [key]: [...entries, nodeDir].join(separator) };
3292
3939
  }
3293
3940
  async function sleep(ms, signal) {
3294
- await wait(ms, void 0, { signal }).catch(() => {
3941
+ await wait2(ms, void 0, { signal }).catch(() => {
3295
3942
  });
3296
3943
  }
3297
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
+
3298
4039
  // src/update.ts
3299
4040
  import { spawn as spawn4 } from "child_process";
3300
4041
  import { readFileSync as readFileSync2 } from "fs";
3301
- import process5 from "process";
4042
+ import process6 from "process";
3302
4043
  var PACKAGE_NAME = "@tamsensedev/runner";
3303
4044
  var RELAUNCHED_ENV = "TAMSENSE_RELAUNCHED";
3304
4045
  var NO_UPDATE_ENV = "TAMSENSE_NO_UPDATE";
@@ -3350,27 +4091,27 @@ function relaunchArgs(argv) {
3350
4091
  return ["--yes", `${PACKAGE_NAME}@latest`, ...argv];
3351
4092
  }
3352
4093
  function relaunch(argv, env) {
3353
- const isWindows = process5.platform === "win32";
4094
+ const isWindows = process6.platform === "win32";
3354
4095
  return new Promise((resolve5, reject) => {
3355
4096
  const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
3356
4097
  const ignoreInterrupt = () => {
3357
4098
  };
3358
- process5.on("SIGINT", ignoreInterrupt);
4099
+ process6.on("SIGINT", ignoreInterrupt);
3359
4100
  child.on("error", (error) => {
3360
- process5.off("SIGINT", ignoreInterrupt);
4101
+ process6.off("SIGINT", ignoreInterrupt);
3361
4102
  reject(error);
3362
4103
  });
3363
4104
  child.on("close", (code) => {
3364
- process5.off("SIGINT", ignoreInterrupt);
4105
+ process6.off("SIGINT", ignoreInterrupt);
3365
4106
  resolve5(code ?? 1);
3366
4107
  });
3367
4108
  });
3368
4109
  }
3369
4110
 
3370
4111
  // src/worker.ts
3371
- import { rm as rm6 } from "fs/promises";
3372
- import { dirname as dirname5 } from "path";
3373
- 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";
3374
4115
 
3375
4116
  // src/feedQueue.ts
3376
4117
  var MAX_BATCH = 200;
@@ -3379,6 +4120,7 @@ function feedQueue(send) {
3379
4120
  let seq = 0;
3380
4121
  let waiting = [];
3381
4122
  let sending = Promise.resolve();
4123
+ let queued = false;
3382
4124
  async function drain() {
3383
4125
  while (waiting.length > 0) {
3384
4126
  const batch = waiting.slice(0, MAX_BATCH);
@@ -3402,16 +4144,22 @@ function feedQueue(send) {
3402
4144
  return entry;
3403
4145
  },
3404
4146
  flush() {
3405
- sending = sending.then(drain);
4147
+ if (!queued) {
4148
+ queued = true;
4149
+ sending = sending.then(() => {
4150
+ queued = false;
4151
+ return drain();
4152
+ });
4153
+ }
3406
4154
  return sending;
3407
4155
  }
3408
4156
  };
3409
4157
  }
3410
4158
 
3411
4159
  // src/library.ts
3412
- import { existsSync as existsSync6 } from "fs";
3413
- import { cp, mkdir as mkdir4, rm as rm5 } from "fs/promises";
3414
- import { join as join9 } from "path";
4160
+ import { existsSync as existsSync7 } from "fs";
4161
+ import { cp, mkdir as mkdir5, rm as rm6 } from "fs/promises";
4162
+ import { join as join10 } from "path";
3415
4163
  import { fileURLToPath as fileURLToPath2 } from "url";
3416
4164
  var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
3417
4165
  var SKILL_NAME = /^[a-z0-9-]+$/;
@@ -3424,18 +4172,18 @@ function allowedSkills(stage) {
3424
4172
  return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
3425
4173
  }
3426
4174
  function roleLibraryFolder(home, runId) {
3427
- return join9(home, ".tamsense", "libraries", `run-${runId}`);
4175
+ return join10(home, ".tamsense", "libraries", `run-${runId}`);
3428
4176
  }
3429
4177
  async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3430
4178
  if (skills === null) {
3431
4179
  return source;
3432
4180
  }
3433
- await rm5(folder, { recursive: true, force: true });
3434
- await mkdir4(join9(folder, "skills"), { recursive: true });
3435
- await cp(join9(source, ".claude-plugin"), join9(folder, ".claude-plugin"), { recursive: true });
3436
- await cp(join9(source, "agents"), join9(folder, "agents"), { recursive: true });
3437
- for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync6(join9(source, "skills", name)))) {
3438
- await cp(join9(source, "skills", skill), join9(folder, "skills", skill), { recursive: true });
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 });
3439
4187
  }
3440
4188
  return folder;
3441
4189
  }
@@ -3443,8 +4191,8 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
3443
4191
  // src/recordings.ts
3444
4192
  import { randomUUID } from "crypto";
3445
4193
  import { createReadStream } from "fs";
3446
- import { readdir, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
3447
- import { basename as basename4, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute3, join as join10 } from "path";
4194
+ import { readdir as readdir2, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
4195
+ import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
3448
4196
  import { createInterface as createInterface2 } from "readline";
3449
4197
  import { Readable } from "stream";
3450
4198
  import { promisify } from "util";
@@ -3501,6 +4249,14 @@ async function* replayChunks(lines, scan, targetBytes = CHUNK_TARGET_BYTES) {
3501
4249
  `;
3502
4250
  }
3503
4251
  }
4252
+ function recordingNotes(meta) {
4253
+ const notes = meta?.notes?.trim();
4254
+ if (notes) {
4255
+ return notes;
4256
+ }
4257
+ const marks = meta?.marks ?? [];
4258
+ return marks.length > 0 ? clip2(`Steps marked: ${marks.map((mark) => mark.label).join("; ")}.`, MAX_NOTES_LENGTH) : "";
4259
+ }
3504
4260
  function marksFrom(marks, startTs) {
3505
4261
  return marks.map((mark) => ({ at: Math.max(0, mark.ts - startTs), label: mark.label })).sort((a, b) => a.at - b.at);
3506
4262
  }
@@ -3516,12 +4272,12 @@ async function readMeta(file) {
3516
4272
  }
3517
4273
  async function metaFor(kind, file) {
3518
4274
  if (kind === "replay") {
3519
- return readMeta(join10(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
4275
+ return readMeta(join11(dirname5(file), `${sessionIdOf(file)}${META_SUFFIX}`));
3520
4276
  }
3521
- const folder = basename4(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
3522
- for (const name of await readdir(folder).catch(() => [])) {
4277
+ const folder = basename4(dirname5(file)) === "video" ? dirname5(dirname5(file)) : dirname5(file);
4278
+ for (const name of await readdir2(folder).catch(() => [])) {
3523
4279
  if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
3524
- const meta = await readMeta(join10(folder, name));
4280
+ const meta = await readMeta(join11(folder, name));
3525
4281
  if (meta?.video === file) {
3526
4282
  return meta;
3527
4283
  }
@@ -3531,7 +4287,7 @@ async function metaFor(kind, file) {
3531
4287
  }
3532
4288
  async function ledger(dir) {
3533
4289
  try {
3534
- 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")));
3535
4291
  } catch {
3536
4292
  return /* @__PURE__ */ new Set();
3537
4293
  }
@@ -3539,21 +4295,21 @@ async function ledger(dir) {
3539
4295
  async function remember(dir, file) {
3540
4296
  const done2 = await ledger(dir);
3541
4297
  done2.add(file);
3542
- await writeFile4(join10(dir, LEDGER_FILE), JSON.stringify([...done2]));
4298
+ await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
3543
4299
  }
3544
4300
  async function nonEmpty(file) {
3545
4301
  return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
3546
4302
  }
3547
4303
  async function pendingRecordings(dir) {
3548
4304
  const done2 = await ledger(dir);
3549
- const names = (await readdir(dir).catch(() => [])).sort();
4305
+ const names = (await readdir2(dir).catch(() => [])).sort();
3550
4306
  const pending = [];
3551
4307
  for (const name of names) {
3552
4308
  if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
3553
4309
  continue;
3554
4310
  }
3555
- const meta = await readMeta(join10(dir, name));
3556
- 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}`);
3557
4313
  if (!done2.has(replay) && await nonEmpty(replay)) {
3558
4314
  pending.push({ kind: "replay", file: replay, meta });
3559
4315
  }
@@ -3563,7 +4319,7 @@ async function pendingRecordings(dir) {
3563
4319
  }
3564
4320
  return pending;
3565
4321
  }
3566
- async function uploadReplay(upload, file, caption) {
4322
+ async function uploadReplay(upload, file, label) {
3567
4323
  const id = randomUUID();
3568
4324
  const scan = { startTs: Number.POSITIVE_INFINITY, snapshot: false, marks: [] };
3569
4325
  const lines = createInterface2({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
@@ -3573,9 +4329,9 @@ async function uploadReplay(upload, file, caption) {
3573
4329
  if (!scan.snapshot) {
3574
4330
  throw new Error("This replay has no page snapshot, so it cannot be played. Was the page ever opened?");
3575
4331
  }
3576
- return finish(upload, { kind: "replay", upload: id, name: basename4(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
4332
+ return finish(upload, { kind: "replay", upload: id, name: basename4(file), ...label, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
3577
4333
  }
3578
- async function uploadVideo(upload, file, caption, meta) {
4334
+ async function uploadVideo(upload, file, label, meta) {
3579
4335
  const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
3580
4336
  const info = await stat2(file);
3581
4337
  if (info.size > MAX_VIDEO_BYTES) {
@@ -3588,7 +4344,7 @@ async function uploadVideo(upload, file, caption, meta) {
3588
4344
  const endedAt = meta?.endedAt ?? info.mtimeMs;
3589
4345
  const durationMs = Math.max(0, Math.round(endedAt - startedAt));
3590
4346
  const marks = marksFrom(meta?.marks ?? [], startedAt).filter((mark) => mark.at <= durationMs);
3591
- return finish(upload, { kind: "video", upload: id, name: basename4(file), ...caption, contentType, durationMs, marks });
4347
+ return finish(upload, { kind: "video", upload: id, name: basename4(file), ...label, contentType, durationMs, marks });
3592
4348
  }
3593
4349
  async function finish(upload, body) {
3594
4350
  return (await upload.api.request("POST", runPath(upload.runId, "recordings"), body)).id;
@@ -3605,15 +4361,18 @@ async function attachRecording(api, runId, recordingsDir2, request) {
3605
4361
  return `There is no recording at ${request.path}. Close the browser context first (closeRecorded) so the file is complete.`;
3606
4362
  }
3607
4363
  let caption;
4364
+ let notes;
3608
4365
  try {
3609
4366
  caption = checkCaption(request);
4367
+ notes = checkNotes(request.notes);
3610
4368
  } catch (error) {
3611
4369
  return error instanceof Error ? error.message : String(error);
3612
4370
  }
3613
4371
  try {
3614
4372
  const meta = await metaFor(request.kind, request.path);
4373
+ const label = { ...caption, notes: notes || recordingNotes(meta) };
3615
4374
  const upload = { api, runId };
3616
- await (request.kind === "replay" ? uploadReplay(upload, request.path, caption) : uploadVideo(upload, request.path, caption, meta));
4375
+ await (request.kind === "replay" ? uploadReplay(upload, request.path, label) : uploadVideo(upload, request.path, label, meta));
3617
4376
  await remember(recordingsDir2, request.path);
3618
4377
  } catch (error) {
3619
4378
  return `Could not attach the ${request.kind}: ${error instanceof Error ? error.message : String(error)}`;
@@ -3645,8 +4404,9 @@ async function uploadPendingRecordings(api, runId, dir) {
3645
4404
  title: numbered(`${recording.kind}:${title}`, title, used),
3646
4405
  purpose: recording.meta?.purpose || "Recorded automatically while the agent used a browser"
3647
4406
  });
4407
+ const label = { ...caption, notes: recordingNotes(recording.meta) };
3648
4408
  try {
3649
- await (recording.kind === "replay" ? uploadReplay(upload, recording.file, caption) : uploadVideo(upload, recording.file, caption, recording.meta));
4409
+ await (recording.kind === "replay" ? uploadReplay(upload, recording.file, label) : uploadVideo(upload, recording.file, label, recording.meta));
3650
4410
  await remember(dir, recording.file);
3651
4411
  report.uploaded += 1;
3652
4412
  } catch (error) {
@@ -3696,15 +4456,21 @@ async function readScreenshot(request) {
3696
4456
  }
3697
4457
 
3698
4458
  // src/worker.ts
3699
- var POLL_INTERVAL_MS = 5e3;
3700
- var RETRY_AFTER_ERROR_MS = 15e3;
4459
+ var POLL_INTERVAL_MS = 2e3;
4460
+ var MAX_RETRY_WAIT_MS = 6e4;
4461
+ var POLL_DEADLINE_MS = 3 * 6e4;
3701
4462
  var INBOX_INTERVAL_MS = 2e3;
3702
4463
  var MAX_MESSAGE_LINE = 200;
3703
4464
  var CAPABILITIES_INTERVAL_MS = 30 * 6e4;
3704
4465
  var FEED_INTERVAL_MS = 1500;
3705
4466
  var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
3706
- var STOPPED_HERE = "stopped_here";
3707
- var CLOSED_ON_SITE = "closed_on_site";
4467
+ var MAX_CHECKS = 2;
4468
+ var CHECK_HEARTBEAT_MS = 3e4;
4469
+ var CHECK_DEADLINE_MS = 10 * 6e4;
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";
3708
4474
  function messageOf3(error) {
3709
4475
  return error instanceof Error ? error.message : String(error);
3710
4476
  }
@@ -3734,7 +4500,7 @@ function sessionReporter(api, job, print2) {
3734
4500
  let sending = null;
3735
4501
  return {
3736
4502
  report: (session) => {
3737
- sending ??= api.request("POST", runPath(job.runId, "session"), session).then(() => {
4503
+ sending ??= api.request("POST", runPath(job.runId, "session"), session, { safeToRepeat: true }).then(() => {
3738
4504
  }, (error) => print2(`Could not tell TAMsense which Claude took the task: ${messageOf3(error)}`));
3739
4505
  },
3740
4506
  sent: async () => {
@@ -3777,35 +4543,35 @@ function inbox(job, tools, record, closedOnSite) {
3777
4543
  async function check() {
3778
4544
  const ack = input.takeDelivered();
3779
4545
  const body = { ack };
3780
- let reply3;
4546
+ let reply5;
3781
4547
  try {
3782
- reply3 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
4548
+ reply5 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
3783
4549
  } catch (error) {
3784
4550
  input.restoreDelivered(ack);
3785
- if (!failing) {
4551
+ if (!failing && !alreadyReported(error)) {
3786
4552
  tools.print(`Could not check for new messages on TAMsense, trying again: ${messageOf3(error)}`);
3787
4553
  }
3788
4554
  failing = true;
3789
4555
  return;
3790
4556
  }
3791
4557
  failing = false;
3792
- if (!reply3.keepGoing) {
4558
+ if (!reply5.keepGoing) {
3793
4559
  closedOnSite();
3794
4560
  return;
3795
4561
  }
3796
- for (const message of reply3.messages) {
4562
+ for (const message of reply5.messages) {
3797
4563
  if (input.push(message)) {
3798
4564
  tools.print(` ${messageLine(message)}`);
3799
4565
  }
3800
4566
  }
3801
- for (const stop of reply3.helperStops ?? []) {
4567
+ for (const stop of reply5.helperStops ?? []) {
3802
4568
  if (!stopsSeen.has(stop.ref)) {
3803
4569
  stopsSeen.add(stop.ref);
3804
4570
  void stopHelper(input, stop, tools.print);
3805
4571
  }
3806
4572
  }
3807
- if (reply3.model !== null || reply3.effort !== null) {
3808
- const change = { model: reply3.model, effort: reply3.effort };
4573
+ if (reply5.model !== null || reply5.effort !== null) {
4574
+ const change = { model: reply5.model, effort: reply5.effort };
3809
4575
  void input.switchModel(change).then(
3810
4576
  (applied) => {
3811
4577
  if (applied) {
@@ -3860,11 +4626,11 @@ function permissionTools(job, tools) {
3860
4626
  }
3861
4627
  return {
3862
4628
  send: async (body) => {
3863
- const reply3 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
3864
- if (reply3.decision === "wait") {
4629
+ const reply5 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
4630
+ if (reply5.decision === "wait") {
3865
4631
  tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
3866
4632
  }
3867
- return reply3;
4633
+ return reply5;
3868
4634
  },
3869
4635
  check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
3870
4636
  sleep: tools.sleep
@@ -3892,14 +4658,15 @@ async function prepare(job, tools) {
3892
4658
  const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
3893
4659
  return { workspace: workspace2, results, identity: {} };
3894
4660
  }
4661
+ const taskResults = (workspace2) => writeResults(join12(workspace2.root, RESULTS_FOLDER), job, tools.api);
3895
4662
  if (isLocalJob(job)) {
3896
4663
  tools.print("Preparing the code from the folders on this computer\u2026");
3897
4664
  const workspace2 = await prepareLocalWorkspace(job, tools);
3898
- 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) };
3899
4666
  }
3900
4667
  tools.print("Preparing the code\u2026");
3901
4668
  const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
3902
- return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
4669
+ return { workspace, results: await taskResults(workspace), identity: await commitIdentityEnv(tools.home) };
3903
4670
  }
3904
4671
  async function publish(workspace, job, api, identity) {
3905
4672
  return isLocalJob(job) ? await publishLocalWorkspace(workspace, job, identity) : await publishWorkspace(workspace, job, await gitToken(api, job), identity);
@@ -3917,7 +4684,7 @@ async function executeJob(job, tools, signal, input, record) {
3917
4684
  const attached = { count: 0 };
3918
4685
  const session = sessionReporter(tools.api, job, tools.print);
3919
4686
  const recordings = recordingsFolder(tools.home, job.runId);
3920
- const baseEnv = withNodeOnPath(process6.env, dirname5(process6.execPath));
4687
+ const baseEnv = withNodeOnPath(process7.env, dirname6(process7.execPath));
3921
4688
  const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
3922
4689
  const outcome = await tools.runAgent({
3923
4690
  cwd: workspace.root,
@@ -3939,12 +4706,15 @@ async function executeJob(job, tools, signal, input, record) {
3939
4706
  onSession: session.report,
3940
4707
  attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
3941
4708
  results: resultTools(job, tools.api, workspace.root),
3942
- handOver: handOverTools(job, tools.api)
4709
+ handOver: handOverTools(job, tools.api),
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"))
3943
4712
  });
3944
4713
  await session.sent();
3945
4714
  signal.throwIfAborted();
3946
4715
  const waitingFor = outcome.waitingFor ?? null;
3947
- const shouldPublish = job.stage.kind === "code" && !isFolderJob(job) && !isReadOnlyCode(job.stage) && outcome.completed && waitingFor === null;
4716
+ const waitingForSchedule = outcome.waitingForSchedule ?? null;
4717
+ const shouldPublish = job.stage.kind === "code" && !isFolderJob(job) && !isReadOnlyCode(job.stage) && outcome.completed && waitingFor === null && waitingForSchedule === null;
3948
4718
  const pushed = shouldPublish ? await publish(workspace, job, tools.api, identity) : [];
3949
4719
  return {
3950
4720
  outcome: outcome.completed ? "succeeded" : "failed",
@@ -3958,7 +4728,8 @@ async function executeJob(job, tools, signal, input, record) {
3958
4728
  pausedUntil: outcome.pausedUntil ?? null,
3959
4729
  questions: outcome.questions ?? null,
3960
4730
  spec: outcome.spec ?? null,
3961
- waitingFor
4731
+ waitingFor,
4732
+ waitingForSchedule
3962
4733
  };
3963
4734
  }
3964
4735
  function clockOf(iso) {
@@ -3972,6 +4743,9 @@ function resultLine(finish2, workspace = "repositories") {
3972
4743
  if (finish2.waitingFor !== null) {
3973
4744
  return "Waiting for an answer on TAMsense. This computer is free for other tasks; the run continues here after the answer.";
3974
4745
  }
4746
+ if (finish2.waitingForSchedule) {
4747
+ return "Waiting for a schedule on TAMsense. This computer is free for other tasks; the run continues here when the schedule ends.";
4748
+ }
3975
4749
  if (finish2.verdict?.couldNotRun) {
3976
4750
  return `Check could not run: ${finish2.verdict.report}`;
3977
4751
  }
@@ -4009,24 +4783,24 @@ async function sendRecordings(job, tools) {
4009
4783
  tools.print(`Could not attach a recording: ${line}`);
4010
4784
  }
4011
4785
  } finally {
4012
- await rm6(folder, { recursive: true, force: true });
4786
+ await rm7(folder, { recursive: true, force: true });
4013
4787
  }
4014
4788
  }
4015
4789
  function failureReport(error, signal) {
4016
- 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)}`;
4017
4791
  }
4018
4792
  async function runJob(job, tools, stop) {
4019
4793
  tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
4020
4794
  const run2 = new AbortController();
4021
- const stopRun = () => run2.abort(STOPPED_HERE);
4795
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4022
4796
  if (stop.aborted) {
4023
4797
  stopRun();
4024
4798
  }
4025
4799
  stop.addEventListener("abort", stopRun, { once: true });
4026
- const feed = feedQueue((entries) => tools.api.request("POST", runPath(job.runId, "feed"), { entries }));
4800
+ const feed = feedQueue((entries) => tools.api.request("POST", runPath(job.runId, "feed"), { entries }, { safeToRepeat: true }));
4027
4801
  const feeding = setInterval(() => void feed.flush(), FEED_INTERVAL_MS);
4028
4802
  const record = journal(feed, tools.print);
4029
- const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE));
4803
+ const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE2));
4030
4804
  const { input } = messages;
4031
4805
  run2.signal.addEventListener("abort", input.close, { once: true });
4032
4806
  void messages.poll();
@@ -4037,7 +4811,7 @@ async function runJob(job, tools, stop) {
4037
4811
  } catch (error) {
4038
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 };
4039
4813
  } finally {
4040
- await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
4814
+ await rm7(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
4041
4815
  clearInterval(polling);
4042
4816
  clearInterval(feeding);
4043
4817
  stop.removeEventListener("abort", stopRun);
@@ -4048,8 +4822,8 @@ async function runJob(job, tools, stop) {
4048
4822
  await messages.poll();
4049
4823
  }
4050
4824
  await feed.flush();
4051
- if (run2.signal.reason === CLOSED_ON_SITE) {
4052
- 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 });
4053
4827
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
4054
4828
  return;
4055
4829
  }
@@ -4061,6 +4835,47 @@ async function runJob(job, tools, stop) {
4061
4835
  tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
4062
4836
  }
4063
4837
  }
4838
+ var CHECK_TOO_LONG = "check_too_long";
4839
+ function checkFailure(error, signal, deadlineMs) {
4840
+ return signal.reason === CHECK_TOO_LONG ? `The run took longer than ${deadlineMs / 6e4} minutes.` : failureReport(error, signal);
4841
+ }
4842
+ async function runCheckJob(check, runCheck, tools, stop) {
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;
4846
+ const run2 = new AbortController();
4847
+ const stopRun = () => run2.abort(STOPPED_HERE2);
4848
+ if (stop.aborted) {
4849
+ stopRun();
4850
+ }
4851
+ stop.addEventListener("abort", stopRun, { once: true });
4852
+ const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG), deadlineMs);
4853
+ const beat = checkHeartbeat(tools.api, check, () => run2.abort(CLOSED_ON_SITE2));
4854
+ const beating = setInterval(() => void beat(), CHECK_HEARTBEAT_MS);
4855
+ let body;
4856
+ try {
4857
+ const folder = await checkFolder(tools.home, check);
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 }));
4861
+ } catch (error) {
4862
+ body = { outcome: "failed", report: checkFailure(error, run2.signal, deadlineMs), model: null, usage: null, result: null };
4863
+ } finally {
4864
+ clearTimeout(deadline);
4865
+ clearInterval(beating);
4866
+ stop.removeEventListener("abort", stopRun);
4867
+ }
4868
+ if (run2.signal.reason === CLOSED_ON_SITE2) {
4869
+ tools.print("TAMsense closed this check while it was in progress. The result was not sent.");
4870
+ return;
4871
+ }
4872
+ try {
4873
+ await tools.api.request("POST", checkPath(check.checkId, "finish"), body);
4874
+ tools.print(checkLine(body));
4875
+ } catch (error) {
4876
+ tools.print(`Could not send the check result to TAMsense: ${messageOf3(error)}`);
4877
+ }
4878
+ }
4064
4879
  async function reportCapabilities(tools, stop) {
4065
4880
  if (!tools.capabilities) {
4066
4881
  return;
@@ -4076,7 +4891,7 @@ async function reportCapabilities(tools, stop) {
4076
4891
  printed = true;
4077
4892
  }
4078
4893
  } catch (error) {
4079
- if (!complained && !stop.aborted) {
4894
+ if (!complained && !stop.aborted && !alreadyReported(error)) {
4080
4895
  tools.print(`Could not tell TAMsense which Claude models this computer has: ${messageOf3(error)}`);
4081
4896
  complained = true;
4082
4897
  }
@@ -4084,6 +4899,24 @@ async function reportCapabilities(tools, stop) {
4084
4899
  await tools.sleep(CAPABILITIES_INTERVAL_MS, stop);
4085
4900
  }
4086
4901
  }
4902
+ async function withDeadline(work2, ms, sleep2) {
4903
+ const settled = new AbortController();
4904
+ const deadline = new Promise((_, reject) => {
4905
+ void sleep2(ms, settled.signal).then(() => {
4906
+ if (!settled.signal.aborted) {
4907
+ reject(new Error(`TAMsense did not answer for ${Math.round(ms / 6e4)} minutes`));
4908
+ }
4909
+ });
4910
+ });
4911
+ try {
4912
+ return await Promise.race([work2, deadline]);
4913
+ } finally {
4914
+ settled.abort();
4915
+ }
4916
+ }
4917
+ function untilReconnect(api, stop) {
4918
+ return api.nextReconnect ? AbortSignal.any([stop, api.nextReconnect()]) : stop;
4919
+ }
4087
4920
  function jobTools(job, tools, parallel) {
4088
4921
  return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId} ${job.stage.name}] ${line}`) } : tools;
4089
4922
  }
@@ -4092,40 +4925,71 @@ async function work(tools, stop, parallel = 1) {
4092
4925
  const halt = new AbortController();
4093
4926
  const stopAll = AbortSignal.any([stop, halt.signal]);
4094
4927
  const running = /* @__PURE__ */ new Set();
4928
+ const checks = /* @__PURE__ */ new Set();
4929
+ const senses = /* @__PURE__ */ new Set();
4095
4930
  const folderLock = keyedLock();
4931
+ const scheduleLock = keyedLock();
4932
+ const runCheck = tools.version ? tools.runCheck : void 0;
4933
+ const runSense = tools.version ? tools.runSense : void 0;
4096
4934
  const reporting = reportCapabilities(tools, stopAll);
4097
4935
  const checking = tools.inspectFolder ? checkFolders({ ...tools, inspect: tools.inspectFolder }, stopAll) : Promise.resolve();
4936
+ if (tools.startFolder) {
4937
+ await reportFoundFolders(tools, tools.startFolder);
4938
+ }
4098
4939
  const start = (job) => {
4099
4940
  const runTools = jobTools(job, tools, parallel);
4100
4941
  const task = folderLock.run(stepFolder(job).join("/"), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
4101
4942
  running.add(task);
4102
4943
  };
4944
+ const startCheck = (check, runner) => {
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));
4946
+ checks.add(task);
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
+ };
4953
+ let failures = 0;
4103
4954
  try {
4104
4955
  while (!stopAll.aborted) {
4105
- if (running.size >= parallel) {
4106
- await Promise.race(running);
4956
+ const jobsFull = running.size >= parallel;
4957
+ const checksFull = !runCheck || checks.size >= MAX_CHECKS;
4958
+ const senseFull = !runSense || senses.size >= MAX_SENSE;
4959
+ if (jobsFull && checksFull && senseFull) {
4960
+ await Promise.race([...running, ...checks, ...senses]);
4107
4961
  continue;
4108
4962
  }
4109
- let reply3;
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;
4110
4966
  try {
4111
- reply3 = await tools.api.request("POST", NEXT_RUN_PATH);
4967
+ reply5 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
4112
4968
  } catch (error) {
4113
4969
  if (isFatal(error)) {
4114
4970
  halt.abort();
4115
4971
  throw error;
4116
4972
  }
4117
- tools.print(`Could not reach TAMsense: ${messageOf3(error)}. Trying again soon.`);
4118
- await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
4973
+ failures += 1;
4974
+ if (failures === 1 && !alreadyReported(error)) {
4975
+ tools.print(`Could not ask TAMsense for tasks: ${messageOf3(error)}. Trying again.`);
4976
+ }
4977
+ await tools.sleep(backoffMs(failures, POLL_INTERVAL_MS, MAX_RETRY_WAIT_MS), untilReconnect(tools.api, stopAll));
4119
4978
  continue;
4120
4979
  }
4121
- if (reply3.job) {
4122
- start(reply3.job);
4980
+ failures = 0;
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);
4123
4987
  } else {
4124
4988
  await tools.sleep(POLL_INTERVAL_MS, stopAll);
4125
4989
  }
4126
4990
  }
4127
4991
  } finally {
4128
- await Promise.allSettled([...running, reporting, checking]);
4992
+ await Promise.allSettled([...running, ...checks, ...senses, reporting, checking]);
4129
4993
  }
4130
4994
  }
4131
4995
 
@@ -4136,11 +5000,11 @@ var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the
4136
5000
  var ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
4137
5001
  var INTERRUPTED_EXIT_CODE = 130;
4138
5002
  function print(line) {
4139
- process7.stdout.write(`${line}
5003
+ process8.stdout.write(`${line}
4140
5004
  `);
4141
5005
  }
4142
5006
  function serverFor(flag, saved) {
4143
- return serverOrigin(flag ?? process7.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
5007
+ return serverOrigin(flag ?? process8.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
4144
5008
  }
4145
5009
  function accessProblem(error) {
4146
5010
  return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
@@ -4162,19 +5026,19 @@ async function runLogin(server, deviceName) {
4162
5026
  { name, platform: platformLabel() }
4163
5027
  );
4164
5028
  await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name });
4165
- print(`Connected: ${result.device.name}. To take your tasks, run ${START_COMMAND}`);
5029
+ print(`Connected: ${result.device.name}. To take your tasks, go to your code folder and run ${START_COMMAND}`);
4166
5030
  }
4167
5031
  function stopOnInterrupt() {
4168
5032
  const stop = new AbortController();
4169
- process7.once("SIGINT", () => {
5033
+ process8.once("SIGINT", () => {
4170
5034
  print("Stopping\u2026 Press Ctrl+C again to quit at once.");
4171
5035
  stop.abort();
4172
- process7.once("SIGINT", () => process7.exit(INTERRUPTED_EXIT_CODE));
5036
+ process8.once("SIGINT", () => process8.exit(INTERRUPTED_EXIT_CODE));
4173
5037
  });
4174
5038
  return stop.signal;
4175
5039
  }
4176
5040
  function installTools(home) {
4177
- return { home, nodePath: process7.execPath, env: process7.env, print, run: runProcess, exists: existsSync7 };
5041
+ return { home, nodePath: process8.execPath, env: process8.env, print, run: runProcess, exists: existsSync8 };
4178
5042
  }
4179
5043
  async function browserForStart(home, install) {
4180
5044
  const installed = await installedBrowser(home);
@@ -4184,13 +5048,13 @@ async function browserForStart(home, install) {
4184
5048
  return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
4185
5049
  }
4186
5050
  async function startedNewVersion() {
4187
- const update = await updateToRun(process7.env, currentVersion(), () => latestVersion());
5051
+ const update = await updateToRun(process8.env, currentVersion(), () => latestVersion());
4188
5052
  if (!update) {
4189
5053
  return false;
4190
5054
  }
4191
5055
  print(`TAMsense runner ${update.latest} is out, this is ${update.current}. Starting the new version...`);
4192
5056
  try {
4193
- process7.exitCode = await relaunch(process7.argv.slice(2), process7.env);
5057
+ process8.exitCode = await relaunch(process8.argv.slice(2), process8.env);
4194
5058
  return true;
4195
5059
  } catch {
4196
5060
  print("Could not start the new version. Going on with this one.");
@@ -4206,16 +5070,16 @@ async function runStart(parallel, installsBrowser) {
4206
5070
  print(NOT_CONNECTED_TEXT);
4207
5071
  return;
4208
5072
  }
4209
- const api = createApi(saved.server, saved.token);
4210
- const identity = await api.request("GET", IDENTITY_PATH);
4211
- const problem2 = teamKeyProblem(identity.device.team, process7.env);
5073
+ const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
5074
+ const problem2 = teamKeyProblem(identity.device.team, process8.env);
4212
5075
  if (problem2) {
4213
5076
  throw new Error(problem2);
4214
5077
  }
4215
5078
  print(`${identity.device.name} is connected to ${saved.server} as ${describeDevice(identity)}.`);
4216
5079
  const home = homedir3();
4217
5080
  const browser = await browserForStart(home, installsBrowser);
4218
- await work({ api, runAgent: runClaude, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home) }, stopOnInterrupt(), parallel);
5081
+ const api = createApi(saved.server, saved.token, { watch: connectionWatch(print) });
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);
4219
5083
  }
4220
5084
  async function runBrowser(remove) {
4221
5085
  const home = homedir3();
@@ -4267,10 +5131,10 @@ async function run(command) {
4267
5131
  }
4268
5132
  }
4269
5133
  try {
4270
- await run(parseCommand(process7.argv.slice(2), process7.env));
5134
+ await run(parseCommand(process8.argv.slice(2), process8.env));
4271
5135
  } catch (error) {
4272
- 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))}
4273
5137
  `);
4274
- process7.exitCode = 1;
5138
+ process8.exitCode = 1;
4275
5139
  }
4276
5140
  //# sourceMappingURL=cli.js.map