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