@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.
- package/LICENSE +5 -5
- package/README.md +61 -50
- package/dist/cli.js +1188 -324
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs +42 -6
- package/dist/recorder.cjs.map +1 -1
- package/library/.claude-plugin/plugin.json +5 -5
- package/library/THIRD_PARTY_NOTICES.md +123 -123
- package/library/agents/code-architect.md +67 -67
- package/library/agents/code-explorer.md +61 -61
- package/library/agents/code-reviewer.md +73 -73
- package/library/agents/code-simplifier.md +62 -62
- package/library/agents/design-critic.md +52 -52
- package/library/agents/second-opinion.md +64 -64
- package/library/agents/security-auditor.md +86 -86
- package/library/agents/silent-failure-hunter.md +70 -70
- package/library/agents/test-coverage-analyzer.md +78 -78
- package/library/agents/test-runner.md +48 -48
- package/library/agents/ui-tester.md +50 -50
- package/library/licenses/Apache-2.0.txt +202 -202
- package/library/skills/accessibility/SKILL.md +144 -144
- package/library/skills/accessibility/references/wcag-checks.md +168 -168
- package/library/skills/addressing-review-feedback/SKILL.md +109 -109
- package/library/skills/app-screens/SKILL.md +96 -96
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
- package/library/skills/app-screens/references/states-density-phone.md +54 -54
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
- package/library/skills/clean-commits/SKILL.md +85 -85
- package/library/skills/design-review/SKILL.md +107 -107
- package/library/skills/design-review/references/critique-passes.md +101 -101
- package/library/skills/design-review/references/screenshot-procedure.md +121 -121
- package/library/skills/differential-security-review/LICENSE +427 -427
- package/library/skills/differential-security-review/SKILL.md +105 -105
- package/library/skills/differential-security-review/references/adversarial.md +115 -115
- package/library/skills/differential-security-review/references/methodology.md +145 -145
- package/library/skills/differential-security-review/references/patterns.md +108 -108
- package/library/skills/false-positive-check/LICENSE +427 -427
- package/library/skills/false-positive-check/SKILL.md +95 -95
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
- package/library/skills/false-positive-check/references/deep-verification.md +113 -113
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
- package/library/skills/false-positive-check/references/standard-verification.md +80 -80
- package/library/skills/focused-implementation/SKILL.md +104 -104
- package/library/skills/implementation-plan/SKILL.md +66 -66
- package/library/skills/interface-design/SKILL.md +144 -144
- package/library/skills/interface-design/references/craft-defaults.md +130 -130
- package/library/skills/interface-design/references/new-direction.md +62 -62
- package/library/skills/interface-design/references/writing.md +52 -52
- package/library/skills/interface-polish/SKILL.md +103 -103
- package/library/skills/interface-polish/references/detail-rules.md +154 -154
- package/library/skills/interface-polish/references/worst-case-data.md +158 -158
- package/library/skills/motion-design/SKILL.md +106 -106
- package/library/skills/motion-design/references/recipes.md +243 -243
- package/library/skills/postgres-performance/SKILL.md +97 -97
- package/library/skills/postgres-performance/references/rules.md +265 -265
- package/library/skills/project-orientation/SKILL.md +77 -77
- package/library/skills/reviewing-changes/SKILL.md +183 -183
- package/library/skills/safe-database-changes/SKILL.md +111 -111
- package/library/skills/safe-database-changes/references/backfills.md +75 -75
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
- package/library/skills/security-review/SKILL.md +132 -132
- package/library/skills/security-review/references/checks-by-area.md +107 -107
- package/library/skills/security-review/references/do-not-report.md +86 -86
- package/library/skills/spec-writing/SKILL.md +144 -144
- package/library/skills/systematic-debugging/SKILL.md +130 -130
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
- package/library/skills/task-interview/SKILL.md +84 -84
- package/library/skills/test-driven-development/SKILL.md +116 -116
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
- package/library/skills/ui-testing/SKILL.md +102 -102
- package/library/skills/ui-testing/references/headless-check.md +295 -280
- package/library/skills/ui-testing/references/local-stand.md +51 -51
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
- package/library/skills/ui-testing/references/recording.md +61 -58
- package/library/skills/verification-before-completion/SKILL.md +113 -113
- package/library/skills/web-interface-guidelines/SKILL.md +202 -202
- 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
|
|
4
|
+
import { existsSync as existsSync8 } from "fs";
|
|
5
5
|
import { homedir as homedir3 } from "os";
|
|
6
|
-
import
|
|
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
|
|
37
|
-
const code = textOf(
|
|
38
|
-
const message = textOf(
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
61
|
-
|
|
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
|
|
344
|
-
import { z as
|
|
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(
|
|
581
|
+
function activityLine(tool8, input) {
|
|
405
582
|
const fields = typeof input === "object" && input !== null ? input : {};
|
|
406
|
-
if (
|
|
583
|
+
if (tool8 === "Bash" && typeof fields.command === "string") {
|
|
407
584
|
return `$ ${shorten(fields.command)}`;
|
|
408
585
|
}
|
|
409
|
-
if (FILE_TOOLS.has(
|
|
410
|
-
return `${
|
|
586
|
+
if (FILE_TOOLS.has(tool8) && typeof fields.file_path === "string") {
|
|
587
|
+
return `${tool8} ${shorten(fields.file_path)}`;
|
|
411
588
|
}
|
|
412
|
-
if (
|
|
589
|
+
if (tool8 === "Skill" && typeof fields.skill === "string") {
|
|
413
590
|
return `Skill ${shorten(fields.skill)}`;
|
|
414
591
|
}
|
|
415
|
-
if (HELPER_TOOLS.has(
|
|
592
|
+
if (HELPER_TOOLS.has(tool8) && typeof fields.subagent_type === "string") {
|
|
416
593
|
return `Agent ${shorten(fields.subagent_type)}`;
|
|
417
594
|
}
|
|
418
|
-
return
|
|
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
|
|
679
|
+
const tool8 = (block.name ?? "tool").replace(MCP_PREFIX, "");
|
|
493
680
|
const fields = fieldsOf(block.input);
|
|
494
|
-
const base = { tool:
|
|
495
|
-
if (
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
511
|
-
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1054
|
-
while (
|
|
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
|
-
|
|
1276
|
+
reply5 = await tools.check(reply5.id).catch(() => reply5);
|
|
1063
1277
|
}
|
|
1064
|
-
return
|
|
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
|
|
1086
|
-
if (typeof
|
|
1087
|
-
return deny(
|
|
1299
|
+
const reply5 = await decisionOf(tools, first, options.signal);
|
|
1300
|
+
if (typeof reply5 === "string") {
|
|
1301
|
+
return deny(reply5);
|
|
1088
1302
|
}
|
|
1089
|
-
return
|
|
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
|
|
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)}` :
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
1574
|
-
return { sha: await git(["rev-parse", "HEAD"], options), commits, ...
|
|
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
|
-
`
|
|
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
|
|
1781
|
-
note: z2.string().describe("what you found and what
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 ? [...
|
|
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
|
|
2290
|
-
import { z as
|
|
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 =
|
|
2293
|
-
id:
|
|
2294
|
-
question:
|
|
2295
|
-
whyItMatters:
|
|
2296
|
-
options:
|
|
2297
|
-
label:
|
|
2298
|
-
description:
|
|
2299
|
-
recommended:
|
|
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:
|
|
2912
|
+
multiSelect: z6.boolean().describe("true only when the options are additive and several can apply together")
|
|
2302
2913
|
});
|
|
2303
|
-
var briefSchema =
|
|
2304
|
-
findings:
|
|
2305
|
-
fact:
|
|
2306
|
-
where:
|
|
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:
|
|
2309
|
-
approach:
|
|
2310
|
-
codeMap:
|
|
2311
|
-
repo:
|
|
2312
|
-
file:
|
|
2313
|
-
role:
|
|
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:
|
|
2316
|
-
text:
|
|
2317
|
-
reason:
|
|
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:
|
|
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 =
|
|
2932
|
+
var questionsPayloadSchema = z6.object({
|
|
2322
2933
|
brief: briefSchema,
|
|
2323
|
-
questions:
|
|
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 =
|
|
2326
|
-
goal:
|
|
2327
|
-
context:
|
|
2328
|
-
inScope:
|
|
2329
|
-
outOfScope:
|
|
2330
|
-
decisions:
|
|
2331
|
-
question:
|
|
2332
|
-
answer:
|
|
2333
|
-
by:
|
|
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:
|
|
2336
|
-
repo:
|
|
2337
|
-
file:
|
|
2338
|
-
role:
|
|
2339
|
-
use:
|
|
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:
|
|
2342
|
-
repo:
|
|
2343
|
-
file:
|
|
2344
|
-
action:
|
|
2345
|
-
description:
|
|
2346
|
-
produces:
|
|
2347
|
-
consumes:
|
|
2348
|
-
acceptance:
|
|
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:
|
|
2351
|
-
id:
|
|
2352
|
-
kind:
|
|
2353
|
-
text:
|
|
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:
|
|
2356
|
-
automated:
|
|
2357
|
-
repo:
|
|
2358
|
-
command:
|
|
2359
|
-
expect:
|
|
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:
|
|
2362
|
-
seam:
|
|
2363
|
-
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:
|
|
2366
|
-
page:
|
|
2367
|
-
steps:
|
|
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:
|
|
2371
|
-
risk:
|
|
2372
|
-
mitigation:
|
|
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:
|
|
2375
|
-
assumptions:
|
|
2376
|
-
text:
|
|
2377
|
-
reason:
|
|
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:
|
|
2380
|
-
split:
|
|
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 =
|
|
2383
|
-
format:
|
|
2384
|
-
goal:
|
|
2385
|
-
context:
|
|
2386
|
-
contains:
|
|
2387
|
-
acceptance:
|
|
2388
|
-
id:
|
|
2389
|
-
text:
|
|
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:
|
|
2392
|
-
outOfScope:
|
|
2393
|
-
sources:
|
|
2394
|
-
source:
|
|
2395
|
-
use:
|
|
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:
|
|
2398
|
-
decisions:
|
|
2399
|
-
question:
|
|
2400
|
-
answer:
|
|
2401
|
-
by:
|
|
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:
|
|
2404
|
-
text:
|
|
2405
|
-
reason:
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3040
|
+
return reply4(verdict.feedback, true);
|
|
2430
3041
|
}
|
|
2431
3042
|
onSubmit(args);
|
|
2432
|
-
return
|
|
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
|
|
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
|
|
3064
|
+
return reply4(verdict.feedback, true);
|
|
2454
3065
|
}
|
|
2455
3066
|
if (!builderCheckRan() && !reminded) {
|
|
2456
3067
|
reminded = true;
|
|
2457
|
-
return
|
|
3068
|
+
return reply4(BUILDER_CHECK_FIRST, true);
|
|
2458
3069
|
}
|
|
2459
3070
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
2460
|
-
return
|
|
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
|
|
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
|
|
3090
|
+
return reply4(verdict.feedback, true);
|
|
2480
3091
|
}
|
|
2481
3092
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
2482
|
-
return
|
|
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
|
|
3194
|
+
var SERVER_NAME2 = "tamsense";
|
|
2584
3195
|
var CAPTION_FIELDS = {
|
|
2585
|
-
title:
|
|
2586
|
-
purpose:
|
|
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
|
|
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
|
|
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
|
|
3221
|
+
return tool7(
|
|
2608
3222
|
spec.name,
|
|
2609
3223
|
spec.description,
|
|
2610
|
-
{ path:
|
|
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
|
|
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:
|
|
2620
|
-
report:
|
|
2621
|
-
couldNotRun:
|
|
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
|
|
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:
|
|
2635
|
-
outcome:
|
|
2636
|
-
note:
|
|
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
|
|
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:
|
|
2654
|
-
why:
|
|
2655
|
-
question:
|
|
2656
|
-
context:
|
|
2657
|
-
tried:
|
|
2658
|
-
did:
|
|
2659
|
-
error:
|
|
2660
|
-
result:
|
|
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:
|
|
2663
|
-
label:
|
|
2664
|
-
detail:
|
|
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:
|
|
2667
|
-
assumption:
|
|
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
|
|
3291
|
+
let reply5;
|
|
2678
3292
|
try {
|
|
2679
|
-
|
|
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 =
|
|
3298
|
+
waiting.waitingFor = reply5.id;
|
|
2685
3299
|
}
|
|
2686
|
-
return { content: [{ type: "text", text: askedText(
|
|
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:
|
|
2734
|
-
allowed: tools.map((item) => `mcp__${
|
|
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: { [
|
|
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(
|
|
3456
|
+
var runClaude = claudeRunner(query3);
|
|
2840
3457
|
|
|
2841
3458
|
// src/config.ts
|
|
2842
|
-
import { mkdir as
|
|
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
|
|
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
|
|
2898
|
-
if (
|
|
2899
|
-
return { token:
|
|
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 (
|
|
3518
|
+
if (reply5.status === "denied") {
|
|
2902
3519
|
throw new Error(DENIED_TEXT);
|
|
2903
3520
|
}
|
|
2904
|
-
if (
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
4099
|
+
process6.on("SIGINT", ignoreInterrupt);
|
|
3359
4100
|
child.on("error", (error) => {
|
|
3360
|
-
|
|
4101
|
+
process6.off("SIGINT", ignoreInterrupt);
|
|
3361
4102
|
reject(error);
|
|
3362
4103
|
});
|
|
3363
4104
|
child.on("close", (code) => {
|
|
3364
|
-
|
|
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
|
|
3372
|
-
import { dirname as
|
|
3373
|
-
import
|
|
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
|
-
|
|
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
|
|
3413
|
-
import { cp, mkdir as
|
|
3414
|
-
import { join as
|
|
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
|
|
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
|
|
3434
|
-
await
|
|
3435
|
-
await cp(
|
|
3436
|
-
await cp(
|
|
3437
|
-
for (const skill of skills.filter((name) => SKILL_NAME.test(name) &&
|
|
3438
|
-
await cp(
|
|
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
|
|
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(
|
|
4275
|
+
return readMeta(join11(dirname5(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
3520
4276
|
}
|
|
3521
|
-
const folder = basename4(
|
|
3522
|
-
for (const name of await
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
3556
|
-
const replay =
|
|
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,
|
|
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), ...
|
|
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,
|
|
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), ...
|
|
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,
|
|
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,
|
|
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 =
|
|
3700
|
-
var
|
|
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
|
|
3707
|
-
var
|
|
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
|
|
4546
|
+
let reply5;
|
|
3781
4547
|
try {
|
|
3782
|
-
|
|
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 (!
|
|
4558
|
+
if (!reply5.keepGoing) {
|
|
3793
4559
|
closedOnSite();
|
|
3794
4560
|
return;
|
|
3795
4561
|
}
|
|
3796
|
-
for (const message of
|
|
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
|
|
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 (
|
|
3808
|
-
const change = { model:
|
|
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
|
|
3864
|
-
if (
|
|
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
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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
|
|
4786
|
+
await rm7(folder, { recursive: true, force: true });
|
|
4013
4787
|
}
|
|
4014
4788
|
}
|
|
4015
4789
|
function failureReport(error, signal) {
|
|
4016
|
-
return signal.reason ===
|
|
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(
|
|
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(
|
|
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
|
|
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 ===
|
|
4052
|
-
await
|
|
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
|
-
|
|
4106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4118
|
-
|
|
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
|
-
|
|
4122
|
-
|
|
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
|
-
|
|
5003
|
+
process8.stdout.write(`${line}
|
|
4140
5004
|
`);
|
|
4141
5005
|
}
|
|
4142
5006
|
function serverFor(flag, saved) {
|
|
4143
|
-
return serverOrigin(flag ??
|
|
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
|
-
|
|
5033
|
+
process8.once("SIGINT", () => {
|
|
4170
5034
|
print("Stopping\u2026 Press Ctrl+C again to quit at once.");
|
|
4171
5035
|
stop.abort();
|
|
4172
|
-
|
|
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:
|
|
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(
|
|
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
|
-
|
|
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
|
|
4210
|
-
const
|
|
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
|
-
|
|
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(
|
|
5134
|
+
await run(parseCommand(process8.argv.slice(2), process8.env));
|
|
4271
5135
|
} catch (error) {
|
|
4272
|
-
|
|
5136
|
+
process8.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
|
|
4273
5137
|
`);
|
|
4274
|
-
|
|
5138
|
+
process8.exitCode = 1;
|
|
4275
5139
|
}
|
|
4276
5140
|
//# sourceMappingURL=cli.js.map
|