@tamsensedev/runner 0.2.1 → 0.5.1
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/README.md +3 -1
- package/dist/cli.js +616 -214
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs +39 -11
- package/dist/recorder.cjs.map +1 -1
- package/library/agents/design-critic.md +1 -1
- package/library/agents/ui-tester.md +3 -2
- package/library/skills/ui-testing/SKILL.md +5 -5
- package/library/skills/ui-testing/references/local-stand.md +51 -0
- package/library/skills/ui-testing/references/recording.md +1 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -81,7 +81,7 @@ var HELP_TEXT = `Connects this computer to TAMsense so it can work on your tasks
|
|
|
81
81
|
Usage:
|
|
82
82
|
${LOGIN_COMMAND} connect this computer
|
|
83
83
|
${START_COMMAND} take your tasks and work on them until you press Ctrl+C
|
|
84
|
-
${BROWSER_COMMAND} install the headless browser for interface checks, or show it
|
|
84
|
+
${BROWSER_COMMAND} install the headless browser for interface checks now, or show it
|
|
85
85
|
${BROWSER_COMMAND} remove delete that browser
|
|
86
86
|
npx @tamsensedev/runner@latest status show the connection
|
|
87
87
|
npx @tamsensedev/runner@latest logout disconnect this computer
|
|
@@ -91,7 +91,8 @@ Login options:
|
|
|
91
91
|
--server <url> TAMsense address (default: ${DEFAULT_SERVER})
|
|
92
92
|
|
|
93
93
|
Start options:
|
|
94
|
-
--parallel <n> how many tasks to work on at once, 1 to ${MAX_PARALLEL} (default: ${DEFAULT_PARALLEL}, or TAMSENSE_PARALLEL)
|
|
94
|
+
--parallel <n> how many tasks to work on at once, 1 to ${MAX_PARALLEL} (default: ${DEFAULT_PARALLEL}, or TAMSENSE_PARALLEL)
|
|
95
|
+
--no-browser do not install the headless browser for interface checks (start installs it when it is missing)`;
|
|
95
96
|
function parallelFor(flag, env) {
|
|
96
97
|
const [source, value] = flag !== void 0 ? ["--parallel", flag] : ["TAMSENSE_PARALLEL", env?.trim() || void 0];
|
|
97
98
|
if (value === void 0) {
|
|
@@ -109,10 +110,11 @@ function parseCommand(argv, env = {}) {
|
|
|
109
110
|
allowPositionals: true,
|
|
110
111
|
strict: true,
|
|
111
112
|
options: {
|
|
112
|
-
server: { type: "string" },
|
|
113
|
-
name: { type: "string" },
|
|
114
|
-
parallel: { type: "string" },
|
|
115
|
-
|
|
113
|
+
"server": { type: "string" },
|
|
114
|
+
"name": { type: "string" },
|
|
115
|
+
"parallel": { type: "string" },
|
|
116
|
+
"no-browser": { type: "boolean" },
|
|
117
|
+
"help": { type: "boolean" }
|
|
116
118
|
}
|
|
117
119
|
});
|
|
118
120
|
const [name, detail] = positionals;
|
|
@@ -129,7 +131,7 @@ function parseCommand(argv, env = {}) {
|
|
|
129
131
|
return { name: "login", server: values.server ?? null, deviceName: values.name ?? null };
|
|
130
132
|
}
|
|
131
133
|
if (name === "start") {
|
|
132
|
-
return { name: "start", parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL) };
|
|
134
|
+
return { name: "start", parallel: parallelFor(values.parallel, env.TAMSENSE_PARALLEL), browser: !values["no-browser"] };
|
|
133
135
|
}
|
|
134
136
|
if (name === "status" || name === "logout") {
|
|
135
137
|
return { name };
|
|
@@ -145,8 +147,7 @@ var OUTPUT_TAIL_LINES = 40;
|
|
|
145
147
|
var ERROR_TAIL_LINES = 3;
|
|
146
148
|
var ANSI_PATTERN = /\u001B\[[\d;]*m/g;
|
|
147
149
|
var OFFLINE_PATTERN = /ENOTFOUND|EAI_AGAIN|ETIMEDOUT|ECONNREFUSED|ECONNRESET|ENETUNREACH|getaddrinfo/i;
|
|
148
|
-
var
|
|
149
|
-
var BROWSER_HINT = `Interface checks can use a headless browser. To install it (about 150 MB), run ${BROWSER_COMMAND}`;
|
|
150
|
+
var INSTALLING_TEXT = "Interface checks need a headless browser. Installing it once into ~/.tamsense, about 150 MB. To start without it, add --no-browser.";
|
|
150
151
|
var OFFLINE_TEXT = `No connection to download it. Check your internet connection and run ${BROWSER_COMMAND} again.`;
|
|
151
152
|
function browserFolders(home) {
|
|
152
153
|
const prefix = join(home, ".tamsense", "playwright");
|
|
@@ -248,24 +249,13 @@ async function removeBrowser(home) {
|
|
|
248
249
|
await rm(folders.browsers, { recursive: true, force: true });
|
|
249
250
|
return present2;
|
|
250
251
|
}
|
|
251
|
-
async function
|
|
252
|
-
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
if (!tools.interactive) {
|
|
256
|
-
tools.print(BROWSER_HINT);
|
|
257
|
-
return null;
|
|
258
|
-
}
|
|
259
|
-
if (!await tools.ask(BROWSER_QUESTION)) {
|
|
260
|
-
await tools.rememberDecline();
|
|
261
|
-
tools.print(`Not installed. To install it later, run ${BROWSER_COMMAND}`);
|
|
262
|
-
return null;
|
|
263
|
-
}
|
|
252
|
+
async function ensureBrowser(tools) {
|
|
253
|
+
tools.print(INSTALLING_TEXT);
|
|
264
254
|
try {
|
|
265
255
|
return await tools.install();
|
|
266
256
|
} catch (error) {
|
|
267
257
|
tools.print(`Could not install the browser: ${error instanceof Error ? error.message : String(error)}`);
|
|
268
|
-
tools.print(
|
|
258
|
+
tools.print(`Tasks still run without it. To try again, run ${BROWSER_COMMAND}`);
|
|
269
259
|
return null;
|
|
270
260
|
}
|
|
271
261
|
}
|
|
@@ -351,8 +341,58 @@ async function probeCapabilities(stop) {
|
|
|
351
341
|
|
|
352
342
|
// src/claude.ts
|
|
353
343
|
import { fileURLToPath } from "url";
|
|
354
|
-
import { createSdkMcpServer, query as query2, tool as
|
|
355
|
-
import { z as
|
|
344
|
+
import { createSdkMcpServer, query as query2, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
345
|
+
import { z as z3 } from "zod";
|
|
346
|
+
|
|
347
|
+
// src/askPerson.ts
|
|
348
|
+
var MAX_ASK_TRIES = 5;
|
|
349
|
+
var MAX_ASK_OPTIONS = 4;
|
|
350
|
+
function personLine(person) {
|
|
351
|
+
const roles = person.roles.length > 0 ? `. Team roles: ${person.roles.join("; ")}` : "";
|
|
352
|
+
return `${person.id}: ${person.name} (${person.relation}, ${person.access} access)${roles}`;
|
|
353
|
+
}
|
|
354
|
+
function findPerson(people, to) {
|
|
355
|
+
const key = String(to).trim().toLowerCase();
|
|
356
|
+
return people.find((person) => String(person.id) === key || person.name.toLowerCase() === key) ?? null;
|
|
357
|
+
}
|
|
358
|
+
function cleanTry(item) {
|
|
359
|
+
return { did: item.did.trim(), error: item.error?.trim() || null, result: item.result.trim() };
|
|
360
|
+
}
|
|
361
|
+
function checkAsk(people, request) {
|
|
362
|
+
const question = request.question.trim();
|
|
363
|
+
const why = request.why.trim();
|
|
364
|
+
const assumption = request.assumption?.trim() ?? "";
|
|
365
|
+
if (!question) {
|
|
366
|
+
return { error: "Write the question: one plain sentence the person can answer without reading the code." };
|
|
367
|
+
}
|
|
368
|
+
const person = findPerson(people, request.to);
|
|
369
|
+
if (!person) {
|
|
370
|
+
return { error: `"${request.to}" is not a person on this task. Ask one of them by id:
|
|
371
|
+
${people.map(personLine).join("\n")}` };
|
|
372
|
+
}
|
|
373
|
+
if (!why) {
|
|
374
|
+
return { error: `Say in one line why ${person.name} is the right person to ask.` };
|
|
375
|
+
}
|
|
376
|
+
if (!request.blocking && !assumption) {
|
|
377
|
+
return { error: "A question you do not wait for needs the assumption you continue with until the answer comes." };
|
|
378
|
+
}
|
|
379
|
+
const tried = (request.tried ?? []).map(cleanTry).filter((item) => item.did);
|
|
380
|
+
const options = (request.options ?? []).map((option) => ({ label: option.label.trim(), detail: option.detail.trim() })).filter((option) => option.label);
|
|
381
|
+
if (tried.length > MAX_ASK_TRIES || options.length > MAX_ASK_OPTIONS) {
|
|
382
|
+
return { error: `Keep it short: at most ${MAX_ASK_TRIES} things you tried and at most ${MAX_ASK_OPTIONS} suggested answers.` };
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
person,
|
|
386
|
+
body: { to: person.id, why, question, context: request.context?.trim() ?? "", tried, options, blocking: request.blocking, assumption: request.blocking ? null : assumption }
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function askedText(name, body) {
|
|
390
|
+
if (body.blocking) {
|
|
391
|
+
return `Sent to ${name}. This run now waits for the answer and frees the computer. Write one short line saying what you wait for, then end your turn without doing more work. When ${name} answers, this conversation continues here with the answer.`;
|
|
392
|
+
}
|
|
393
|
+
return `Sent to ${name}. Keep working on your assumption: ${body.assumption}. The answer comes as a message from ${name} while you work, or with the next run if this one has ended. If it differs, change what you did.`;
|
|
394
|
+
}
|
|
395
|
+
var ALREADY_WAITING_TEXT = "You already asked a question this run waits for. End your turn now: the conversation continues when the answer comes.";
|
|
356
396
|
|
|
357
397
|
// src/claudeResult.ts
|
|
358
398
|
var MAX_ACTIVITY_LENGTH = 120;
|
|
@@ -362,21 +402,21 @@ function shorten(text2) {
|
|
|
362
402
|
const line = text2.replace(/\s+/g, " ").trim();
|
|
363
403
|
return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
|
|
364
404
|
}
|
|
365
|
-
function activityLine(
|
|
405
|
+
function activityLine(tool4, input) {
|
|
366
406
|
const fields = typeof input === "object" && input !== null ? input : {};
|
|
367
|
-
if (
|
|
407
|
+
if (tool4 === "Bash" && typeof fields.command === "string") {
|
|
368
408
|
return `$ ${shorten(fields.command)}`;
|
|
369
409
|
}
|
|
370
|
-
if (FILE_TOOLS.has(
|
|
371
|
-
return `${
|
|
410
|
+
if (FILE_TOOLS.has(tool4) && typeof fields.file_path === "string") {
|
|
411
|
+
return `${tool4} ${shorten(fields.file_path)}`;
|
|
372
412
|
}
|
|
373
|
-
if (
|
|
413
|
+
if (tool4 === "Skill" && typeof fields.skill === "string") {
|
|
374
414
|
return `Skill ${shorten(fields.skill)}`;
|
|
375
415
|
}
|
|
376
|
-
if (HELPER_TOOLS.has(
|
|
416
|
+
if (HELPER_TOOLS.has(tool4) && typeof fields.subagent_type === "string") {
|
|
377
417
|
return `Agent ${shorten(fields.subagent_type)}`;
|
|
378
418
|
}
|
|
379
|
-
return
|
|
419
|
+
return tool4;
|
|
380
420
|
}
|
|
381
421
|
function usageOf(modelUsage) {
|
|
382
422
|
const entries = Object.values(modelUsage);
|
|
@@ -419,29 +459,29 @@ function textField(fields, name) {
|
|
|
419
459
|
return typeof value === "string" && value.trim() ? value : null;
|
|
420
460
|
}
|
|
421
461
|
function toolDraft(block, parent) {
|
|
422
|
-
const
|
|
462
|
+
const tool4 = (block.name ?? "tool").replace(MCP_PREFIX, "");
|
|
423
463
|
const fields = fieldsOf(block.input);
|
|
424
|
-
const base = { tool:
|
|
425
|
-
if (
|
|
464
|
+
const base = { tool: tool4, ref: block.id ?? null, parent, failed: false };
|
|
465
|
+
if (tool4 === "Skill" && textField(fields, "skill")) {
|
|
426
466
|
return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
|
|
427
467
|
}
|
|
428
|
-
if (HELPER_TOOLS2.has(
|
|
468
|
+
if (HELPER_TOOLS2.has(tool4)) {
|
|
429
469
|
const who = textField(fields, "subagent_type") ?? "general-purpose";
|
|
430
470
|
const what = textField(fields, "description");
|
|
431
471
|
return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
|
|
432
472
|
}
|
|
433
|
-
if (
|
|
434
|
-
const verdict = fields.passed === true ? "Passed" : "Did not pass";
|
|
473
|
+
if (tool4 === "submit_verdict") {
|
|
474
|
+
const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
|
|
435
475
|
return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
436
476
|
}
|
|
437
|
-
if (
|
|
477
|
+
if (tool4 === "hand_off") {
|
|
438
478
|
return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
439
479
|
}
|
|
440
|
-
if (
|
|
441
|
-
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[
|
|
480
|
+
if (tool4 in ATTACH_LABELS) {
|
|
481
|
+
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool4]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
442
482
|
}
|
|
443
483
|
const command = textField(fields, "command");
|
|
444
|
-
const summary =
|
|
484
|
+
const summary = tool4 === "Bash" && command ? `$ ${command}` : activityLine(tool4, block.input);
|
|
445
485
|
return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
|
|
446
486
|
}
|
|
447
487
|
function resultText(content) {
|
|
@@ -730,6 +770,204 @@ function inputChannel(options = {}) {
|
|
|
730
770
|
};
|
|
731
771
|
}
|
|
732
772
|
|
|
773
|
+
// src/knowledge.ts
|
|
774
|
+
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
775
|
+
import { z } from "zod";
|
|
776
|
+
var PROPOSE_KNOWLEDGE_TOOL_NAME = "propose_knowledge";
|
|
777
|
+
var UPDATE_KNOWLEDGE_TOOL_NAME = "update_knowledge";
|
|
778
|
+
var READ_KNOWLEDGE_TOOL_NAME = "read_knowledge";
|
|
779
|
+
var MAX_KNOWLEDGE_TITLE = 80;
|
|
780
|
+
var MAX_KNOWLEDGE_TEXT = 600;
|
|
781
|
+
var UPDATE_RULE = `Rely on this knowledge instead of asking people again. When you relied on an entry and found it true, call ${UPDATE_KNOWLEDGE_TOOL_NAME} with stillTrue true. When the code shows an entry is out of date (a file moved, a command or a port changed), fix it yourself at once: ${UPDATE_KNOWLEDGE_TOOL_NAME} with stillTrue false, the full new text and the evidence (the commit, the file or the command output that shows it). The change applies right away and people can revert it. Never ask a person whether an entry is still right, and never move facts of this computer into project knowledge.`;
|
|
782
|
+
var PROPOSE_RULE = `When a person's answer to your question teaches something later runs need (how the team runs the local stand, where something lives, a convention), propose one short note with ${PROPOSE_KNOWLEDGE_TOOL_NAME} and the question number from the answer. Scope computer is for what is true only on this computer (folders, ports, installed tools, local quirks); scope project is for what comes from the repository and works on every computer. Never propose secrets, personal data or decisions about this one task. A person approves every note; keep working while they decide.`;
|
|
783
|
+
function dateOf(iso) {
|
|
784
|
+
return iso ? `checked ${iso.slice(0, 10)}` : "not checked yet";
|
|
785
|
+
}
|
|
786
|
+
function entryText(entry) {
|
|
787
|
+
return `[${entry.id}] ${entry.title}: ${entry.text.replace(/\s+/g, " ").trim()} (${dateOf(entry.checkedAt)})`;
|
|
788
|
+
}
|
|
789
|
+
function entryLine(entry) {
|
|
790
|
+
return `- ${entryText(entry)}`;
|
|
791
|
+
}
|
|
792
|
+
function workplaceText(computer, folder) {
|
|
793
|
+
if (!computer) {
|
|
794
|
+
return null;
|
|
795
|
+
}
|
|
796
|
+
return `You run on ${computer.name}, the ${computer.platform} computer of ${computer.ownerName}. Your task folder is ${folder}; everything outside it belongs to ${computer.ownerName}.`;
|
|
797
|
+
}
|
|
798
|
+
function hasKnowledge(knowledge) {
|
|
799
|
+
return Boolean(knowledge && (knowledge.project.length > 0 || knowledge.computer.length > 0 || knowledge.more.length > 0));
|
|
800
|
+
}
|
|
801
|
+
function knowledgeText(knowledge, computer, canPropose) {
|
|
802
|
+
const rules = canPropose ? [PROPOSE_RULE] : [];
|
|
803
|
+
if (!hasKnowledge(knowledge)) {
|
|
804
|
+
return rules.length > 0 ? `Knowledge of this project is empty so far. ${rules.join(" ")}` : null;
|
|
805
|
+
}
|
|
806
|
+
const here = computer ? computer.name : "this computer";
|
|
807
|
+
const sections = [
|
|
808
|
+
knowledge.project.length > 0 ? `Project knowledge (approved by the team, true on every computer):
|
|
809
|
+
${knowledge.project.map(entryLine).join("\n")}` : null,
|
|
810
|
+
knowledge.computer.length > 0 ? `Knowledge for ${here} only (true here, not on other computers):
|
|
811
|
+
${knowledge.computer.map(entryLine).join("\n")}` : null,
|
|
812
|
+
knowledge.more.length > 0 ? `More entries, titles only to keep this short: ${knowledge.more.map((entry) => `[${entry.id}] ${entry.title} (${entry.scope})`).join("; ")}. When a title touches your work, read the entry with ${READ_KNOWLEDGE_TOOL_NAME} and its number.` : null
|
|
813
|
+
];
|
|
814
|
+
return [...sections, UPDATE_RULE, ...rules].filter(Boolean).join("\n\n");
|
|
815
|
+
}
|
|
816
|
+
function knowledgeIds(knowledge) {
|
|
817
|
+
return knowledge ? [...knowledge.project, ...knowledge.computer, ...knowledge.more].map((entry) => entry.id) : [];
|
|
818
|
+
}
|
|
819
|
+
function failed(text2) {
|
|
820
|
+
return { content: [{ type: "text", text: text2 }], isError: true };
|
|
821
|
+
}
|
|
822
|
+
function done(text2) {
|
|
823
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
824
|
+
}
|
|
825
|
+
function unknownEntry(tools, entry) {
|
|
826
|
+
return failed(`There is no entry ${entry} in this run. Use a number in square brackets from your instructions: ${tools.entryIds.join(", ") || "none"}.`);
|
|
827
|
+
}
|
|
828
|
+
function messageOf(error) {
|
|
829
|
+
return error instanceof Error ? error.message : String(error);
|
|
830
|
+
}
|
|
831
|
+
function proposeKnowledgeTool(tools) {
|
|
832
|
+
return tool(
|
|
833
|
+
PROPOSE_KNOWLEDGE_TOOL_NAME,
|
|
834
|
+
"Propose a short note for the TAMsense project knowledge after a person answered your question, so later runs do not ask again. A person on the task approves it: for the whole project, for this computer only, or not at all.",
|
|
835
|
+
{
|
|
836
|
+
title: z.string().describe(`a few words, at most ${MAX_KNOWLEDGE_TITLE} characters: "Local stand and database"`),
|
|
837
|
+
text: z.string().describe(`the fact in one to three plain sentences, at most ${MAX_KNOWLEDGE_TEXT} characters, with exact commands and paths in backticks`),
|
|
838
|
+
scope: z.enum(["project", "computer"]).describe("computer: true only on this computer (folders, ports, installed tools); project: comes from the repository and works everywhere"),
|
|
839
|
+
question: z.number().optional().describe('the number of the question whose answer this comes from, as in "(question 31)"')
|
|
840
|
+
},
|
|
841
|
+
async (args) => {
|
|
842
|
+
const title = args.title.trim();
|
|
843
|
+
const text2 = args.text.trim();
|
|
844
|
+
if (!title || !text2 || title.length > MAX_KNOWLEDGE_TITLE || text2.length > MAX_KNOWLEDGE_TEXT) {
|
|
845
|
+
return failed(`Write a title of at most ${MAX_KNOWLEDGE_TITLE} characters and a text of at most ${MAX_KNOWLEDGE_TEXT}.`);
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
await tools.propose({ title, text: text2, scope: args.scope, question: args.question ?? null });
|
|
849
|
+
} catch (error) {
|
|
850
|
+
return failed(`The note was not suggested: ${messageOf(error)}`);
|
|
851
|
+
}
|
|
852
|
+
return done("Suggested. A person on the task decides whether to save it; keep working.");
|
|
853
|
+
}
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
function updateKnowledgeTool(tools) {
|
|
857
|
+
return tool(
|
|
858
|
+
UPDATE_KNOWLEDGE_TOOL_NAME,
|
|
859
|
+
"Keep the TAMsense knowledge current. stillTrue true: you relied on the entry and it holds. stillTrue false: the code shows it is out of date; the new text applies at once and people see your evidence.",
|
|
860
|
+
{
|
|
861
|
+
entry: z.number().describe("the entry number in square brackets from your instructions"),
|
|
862
|
+
stillTrue: z.boolean(),
|
|
863
|
+
text: z.string().optional().describe(`when stillTrue is false: the full new text, at most ${MAX_KNOWLEDGE_TEXT} characters`),
|
|
864
|
+
evidence: z.string().optional().describe("when stillTrue is false: one line on what shows the old text is wrong, such as a commit, a file or a command output")
|
|
865
|
+
},
|
|
866
|
+
async (args) => {
|
|
867
|
+
if (!tools.entryIds.includes(args.entry)) {
|
|
868
|
+
return unknownEntry(tools, args.entry);
|
|
869
|
+
}
|
|
870
|
+
const text2 = args.text?.trim() ?? "";
|
|
871
|
+
const evidence = args.evidence?.trim() ?? "";
|
|
872
|
+
if (!args.stillTrue && (!text2 || !evidence || text2.length > MAX_KNOWLEDGE_TEXT)) {
|
|
873
|
+
return failed(`An update needs the full new text (at most ${MAX_KNOWLEDGE_TEXT} characters) and the evidence.`);
|
|
874
|
+
}
|
|
875
|
+
try {
|
|
876
|
+
await tools.update(args.entry, { stillTrue: args.stillTrue, text: text2, evidence });
|
|
877
|
+
} catch (error) {
|
|
878
|
+
return failed(`The entry was not updated: ${messageOf(error)}`);
|
|
879
|
+
}
|
|
880
|
+
return done(args.stillTrue ? "Marked as checked today." : "Updated. People see the old text, your evidence and can revert it.");
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
function readKnowledgeTool(tools) {
|
|
885
|
+
return tool(
|
|
886
|
+
READ_KNOWLEDGE_TOOL_NAME,
|
|
887
|
+
"Read a TAMsense knowledge entry that your instructions list by title only.",
|
|
888
|
+
{
|
|
889
|
+
entry: z.number().describe("the entry number in square brackets from your instructions")
|
|
890
|
+
},
|
|
891
|
+
async (args) => {
|
|
892
|
+
if (!tools.entryIds.includes(args.entry)) {
|
|
893
|
+
return unknownEntry(tools, args.entry);
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
return done(entryText(await tools.read(args.entry)));
|
|
897
|
+
} catch (error) {
|
|
898
|
+
return failed(`The entry was not read: ${messageOf(error)}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/permissions.ts
|
|
905
|
+
var NOBODY_TO_ASK = "Nobody is at the computer to approve this during a TAMsense run. Do it another way, or explain in your report why it is needed.";
|
|
906
|
+
var CHECK_INTERVAL_MS = 5e3;
|
|
907
|
+
var SITE_SILENCE_MS = 6e4;
|
|
908
|
+
var MAX_ACTION_LENGTH = 4e3;
|
|
909
|
+
var STOPPED_TEXT2 = "The run is stopping, so this was not done.";
|
|
910
|
+
var SITE_SILENT_TEXT = "TAMsense did not answer about this action, so it was not done. Do it another way, or explain in your report why it is needed.";
|
|
911
|
+
var NEVER_SENT = /* @__PURE__ */ new Set(["AskUserQuestion", "ExitPlanMode"]);
|
|
912
|
+
var ACTION_FIELDS = ["command", "file_path", "notebook_path", "url", "query", "path"];
|
|
913
|
+
function actionOf(input) {
|
|
914
|
+
const field = ACTION_FIELDS.map((key) => input[key]).find((value) => typeof value === "string" && value.trim());
|
|
915
|
+
return (typeof field === "string" ? field.trim() : JSON.stringify(input)).slice(0, MAX_ACTION_LENGTH);
|
|
916
|
+
}
|
|
917
|
+
function titleOf(input, options) {
|
|
918
|
+
const described = typeof input.description === "string" ? input.description.trim() : "";
|
|
919
|
+
return described || options.title?.trim() || options.displayName?.trim() || "";
|
|
920
|
+
}
|
|
921
|
+
function deny(message) {
|
|
922
|
+
return { behavior: "deny", message };
|
|
923
|
+
}
|
|
924
|
+
function messageOf2(error) {
|
|
925
|
+
return error instanceof Error ? error.message : String(error);
|
|
926
|
+
}
|
|
927
|
+
async function decisionOf(tools, first, signal) {
|
|
928
|
+
const now = tools.now ?? Date.now;
|
|
929
|
+
const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
|
|
930
|
+
let reply2 = first;
|
|
931
|
+
while (reply2.decision === "wait") {
|
|
932
|
+
await tools.sleep(CHECK_INTERVAL_MS, signal);
|
|
933
|
+
if (signal.aborted) {
|
|
934
|
+
return STOPPED_TEXT2;
|
|
935
|
+
}
|
|
936
|
+
if (now() > giveUpAt) {
|
|
937
|
+
return SITE_SILENT_TEXT;
|
|
938
|
+
}
|
|
939
|
+
reply2 = await tools.check(reply2.id).catch(() => reply2);
|
|
940
|
+
}
|
|
941
|
+
return reply2;
|
|
942
|
+
}
|
|
943
|
+
function askPermission(tools, lastWords, folder) {
|
|
944
|
+
return async (toolName, input, options) => {
|
|
945
|
+
if (NEVER_SENT.has(toolName)) {
|
|
946
|
+
return deny(NOBODY_TO_ASK);
|
|
947
|
+
}
|
|
948
|
+
const body = {
|
|
949
|
+
tool: toolName,
|
|
950
|
+
action: actionOf(input),
|
|
951
|
+
title: titleOf(input, options),
|
|
952
|
+
reason: lastWords() || options.decisionReason || "",
|
|
953
|
+
folder,
|
|
954
|
+
outsideFolder: Boolean(options.blockedPath)
|
|
955
|
+
};
|
|
956
|
+
let first;
|
|
957
|
+
try {
|
|
958
|
+
first = await tools.send(body);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
return deny(`Could not ask for permission on TAMsense: ${messageOf2(error)}. Do it another way, or explain in your report why it is needed.`);
|
|
961
|
+
}
|
|
962
|
+
const reply2 = await decisionOf(tools, first, options.signal);
|
|
963
|
+
if (typeof reply2 === "string") {
|
|
964
|
+
return deny(reply2);
|
|
965
|
+
}
|
|
966
|
+
return reply2.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply2.message);
|
|
967
|
+
};
|
|
968
|
+
}
|
|
969
|
+
var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
|
|
970
|
+
|
|
733
971
|
// src/specText.ts
|
|
734
972
|
function isRecord(value) {
|
|
735
973
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -818,7 +1056,11 @@ var VIDEO_TOOL_NAME = "attach_video";
|
|
|
818
1056
|
var HANDOFF_TOOL_NAME = "hand_off";
|
|
819
1057
|
var SUBMIT_QUESTIONS_TOOL_NAME = "submit_questions";
|
|
820
1058
|
var SUBMIT_SPEC_TOOL_NAME = "submit_spec";
|
|
821
|
-
var
|
|
1059
|
+
var ASK_PERSON_TOOL_NAME = "ask_person";
|
|
1060
|
+
var MESSAGES_RULE = `You are working on a TAMsense task on the user's own computer. People on the task may send you messages during this run, each starting with "Message from <name>:". Follow them: they take priority over the original task where the two disagree.`;
|
|
1061
|
+
var UNATTENDED_RULE = `${MESSAGES_RULE} Nobody answers questions during the run, so never stop to wait for a reply: decide yourself, keep working, and explain your decisions in your report.`;
|
|
1062
|
+
var ASKING_RULE = `${MESSAGES_RULE} Decide yourself when you reasonably can, keep working, and explain your decisions in your report. When you are stuck, or the decision belongs to a person (intent, design, access, how the team runs something locally), ask one of the people on the task with the ${ASK_PERSON_TOOL_NAME} tool instead of guessing, and say what you tried. Never ask what the code or the project knowledge already answers. Nobody watches your messages, so ask only with the tool.`;
|
|
1063
|
+
var APPROVAL_RULE = "Some actions need a person's approval. When Claude Code would ask for one, TAMsense checks the project safety rules: a Never action is refused at once, an Allowed one goes ahead, and anything else goes to a person on the task while your tool call waits up to 15 minutes. Before such an action, write one short line: why you need it and what you will do instead if it is denied. Never get around a refused action with another command that has the same effect.";
|
|
822
1064
|
var MACHINE_RULE = "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 (taskkill /IM, pkill, killall, Stop-Process -Name) and never touch programs you did not start.";
|
|
823
1065
|
var CODE_RULES = [
|
|
824
1066
|
"Make the changes the task asks for, and keep them focused on it.",
|
|
@@ -836,8 +1078,22 @@ var CHECK_RULES = [
|
|
|
836
1078
|
];
|
|
837
1079
|
var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), attach the ones that show the result with the ${SCREENSHOT_TOOL_NAME} tool. People see them on the task without opening the code.`;
|
|
838
1080
|
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".';
|
|
839
|
-
var RECORDING_RULE = `Every browser you open with Playwright from Node is recorded for the task as a replay people can watch,
|
|
840
|
-
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.
|
|
1081
|
+
var RECORDING_RULE = `Every browser you open with Playwright from Node is recorded for the task as a replay people can watch, requests from the tested site to the TAMsense tracker are blocked, so your runs never reach analytics, and requests other than GET, HEAD and OPTIONS to addresses outside this computer are aborted. In every browser script, load the recorder from the TAMSENSE_RECORDER environment variable (require(process.env.TAMSENSE_RECORDER), or createRequire(import.meta.url)(process.env.TAMSENSE_RECORDER) in an .mjs script), open each context with await recorder.newRecordedContext(browser, { title, purpose, context: { viewport } }), call await recorder.mark(page, 'what just happened') at every meaningful step, and end with await recorder.closeRecorded(context). Add video: true when the page has a canvas, a map, a video player or iframes from another site (a replay cannot show them), or when someone asks for a video. Recordings are attached to the task when the run ends; ${REPLAY_TOOL_NAME} and ${VIDEO_TOOL_NAME} attach one earlier with a better title.`;
|
|
1082
|
+
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.";
|
|
1083
|
+
var DEPENDENCIES_RULE = "The checkout may have no dependencies installed. To run the app or its tests, install the project's own dependencies from its lockfile with its package manager (pnpm install --frozen-lockfile, npm ci, yarn install --immutable and similar). Never add packages, change a lockfile or download browsers.";
|
|
1084
|
+
function localAppRule(taskId, isCheck, canAsk) {
|
|
1085
|
+
const project = `tamsense-task-${taskId}`;
|
|
1086
|
+
const cannotRun = isCheck ? `call ${VERDICT_TOOL_NAME} with passed false and couldNotRun true, write the exact reason and what a person should do in the report, and do not hand off: the task waits on this step for a person.` : "say so in your report with the exact reason and what a person should do.";
|
|
1087
|
+
return [
|
|
1088
|
+
"When you need the running app, start a local copy from this task folder only.",
|
|
1089
|
+
"Find how the project runs locally yourself: the README, compose files named local or dev, .env.example and local env files, package scripts.",
|
|
1090
|
+
canAsk ? `If you still cannot tell how the team prepares it, for example how the local database gets its tables, ask with ${ASK_PERSON_TOOL_NAME} instead of guessing.` : null,
|
|
1091
|
+
"Use only settings meant for a local run that are in this checkout. Never read or copy .env files from elsewhere on this computer, including the person's own checkout, and never use staging, shared dev or production addresses.",
|
|
1092
|
+
"Before starting, check every address in the settings you use: databases, caches, queues and APIs must point at localhost or at containers you started, and keys of outside services stay empty.",
|
|
1093
|
+
`Start containers as a separate compose project named ${project} on free host ports (an override file in a temp folder), never stop or change containers you did not start, and remove yours with their volumes at the end (docker compose -p ${project} down -v).`,
|
|
1094
|
+
`If Docker is not running, there is no local setup, or the settings point outside this computer, do not start anything: ${cannotRun}`
|
|
1095
|
+
].filter(Boolean).join(" ");
|
|
1096
|
+
}
|
|
841
1097
|
var STAGE_GOALS = {
|
|
842
1098
|
code: "Implement",
|
|
843
1099
|
review: "Review the code for",
|
|
@@ -870,6 +1126,9 @@ function contextText(context) {
|
|
|
870
1126
|
return context.trim() ? `Project context from the team (follow it like the project's own rules):
|
|
871
1127
|
${context.trim()}` : null;
|
|
872
1128
|
}
|
|
1129
|
+
function safetyText(job) {
|
|
1130
|
+
return job.safetyRules?.trim() || null;
|
|
1131
|
+
}
|
|
873
1132
|
function bulletList(lines) {
|
|
874
1133
|
return lines.map((line) => `- ${line}`).join("\n");
|
|
875
1134
|
}
|
|
@@ -878,7 +1137,7 @@ function repositoryLines(job, workspace) {
|
|
|
878
1137
|
return workspace.repositories.map((repository) => {
|
|
879
1138
|
const source = sourceOf.get(repository.fullName);
|
|
880
1139
|
const pull = source?.pullRequestUrl ? `, pull request ${source.pullRequestUrl}` : "";
|
|
881
|
-
return `${repository.folder} (${repository.fullName}), base branch origin/${source?.defaultBranch ?? "main"}${pull}`;
|
|
1140
|
+
return `${repository.folder} (${repository.fullName}) at commit ${repository.baseSha.slice(0, 7)}, base branch origin/${source?.defaultBranch ?? "main"}${pull}`;
|
|
882
1141
|
});
|
|
883
1142
|
}
|
|
884
1143
|
function stepLine(stage) {
|
|
@@ -888,6 +1147,19 @@ function teamMessagesText(messages) {
|
|
|
888
1147
|
return messages.length > 0 ? `Messages from the team before this run:
|
|
889
1148
|
${messages.map((message) => `${message.authorName}: ${message.text}`).join("\n")}` : null;
|
|
890
1149
|
}
|
|
1150
|
+
function peopleText(people) {
|
|
1151
|
+
if (people.length === 0) {
|
|
1152
|
+
return null;
|
|
1153
|
+
}
|
|
1154
|
+
return [
|
|
1155
|
+
`People on this task you can ask with ${ASK_PERSON_TOOL_NAME} (id: name, relation, access, then their team roles with what each owns):`,
|
|
1156
|
+
bulletList(people.map(personLine)),
|
|
1157
|
+
'Ask the assignee first. A question on a topic goes to the person whose team role fits it, and then why names that role and what it owns, for example "Designer, owns empty states". Set blocking only when you cannot go on without the answer: the run then pauses and the computer is freed for other tasks. Otherwise give the assumption you continue with.'
|
|
1158
|
+
].join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
function canProposeKnowledge(job) {
|
|
1161
|
+
return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
|
|
1162
|
+
}
|
|
891
1163
|
function handoffText(targets, isCheck) {
|
|
892
1164
|
if (targets.length === 0) {
|
|
893
1165
|
return null;
|
|
@@ -950,6 +1222,7 @@ ${JSON.stringify(job.spec)}` : null,
|
|
|
950
1222
|
const instructions = [
|
|
951
1223
|
SPEC_UNATTENDED_RULE,
|
|
952
1224
|
MACHINE_RULE,
|
|
1225
|
+
workplaceText(job.computer, workspace.root),
|
|
953
1226
|
stepLine(job.stage),
|
|
954
1227
|
job.stage.instructions,
|
|
955
1228
|
contextText(job.projectContext),
|
|
@@ -958,6 +1231,8 @@ ${bulletList(repositoryLines(job, workspace))}`,
|
|
|
958
1231
|
READ_ONLY_RULE,
|
|
959
1232
|
`How to work:
|
|
960
1233
|
${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
1234
|
+
safetyText(job),
|
|
1235
|
+
knowledgeText(job.knowledge, job.computer, false),
|
|
961
1236
|
SPEC_STYLE_RULE
|
|
962
1237
|
].filter(Boolean).join("\n\n");
|
|
963
1238
|
return { prompt, instructions };
|
|
@@ -971,6 +1246,8 @@ function buildTaskPrompt(job, workspace, options = {}) {
|
|
|
971
1246
|
return buildSpecPrompt(job, job.specWork, workspace);
|
|
972
1247
|
}
|
|
973
1248
|
const isCheck = kind !== "code";
|
|
1249
|
+
const people = job.people ?? [];
|
|
1250
|
+
const canAsk = people.length > 0;
|
|
974
1251
|
const prompt = [
|
|
975
1252
|
`${STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
976
1253
|
job.description,
|
|
@@ -981,10 +1258,12 @@ ${job.review}`,
|
|
|
981
1258
|
teamMessagesText(job.messages)
|
|
982
1259
|
].filter(Boolean).join("\n\n");
|
|
983
1260
|
const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
|
|
984
|
-
const
|
|
1261
|
+
const localApp = localAppRule(job.taskId, isCheck, canAsk);
|
|
1262
|
+
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
985
1263
|
const instructions = [
|
|
986
|
-
UNATTENDED_RULE,
|
|
1264
|
+
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
987
1265
|
MACHINE_RULE,
|
|
1266
|
+
workplaceText(job.computer, workspace.root),
|
|
988
1267
|
stepLine(job.stage),
|
|
989
1268
|
job.stage.instructions,
|
|
990
1269
|
contextText(job.projectContext),
|
|
@@ -992,6 +1271,10 @@ ${job.review}`,
|
|
|
992
1271
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
993
1272
|
`How to work:
|
|
994
1273
|
${bulletList(rules)}`,
|
|
1274
|
+
safetyText(job),
|
|
1275
|
+
knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
|
|
1276
|
+
APPROVAL_RULE,
|
|
1277
|
+
peopleText(people),
|
|
995
1278
|
handoffText(job.allowedHandoffs, isCheck),
|
|
996
1279
|
options.browser && BROWSER_RULE
|
|
997
1280
|
].filter(Boolean).join("\n\n");
|
|
@@ -999,98 +1282,98 @@ ${bulletList(rules)}`,
|
|
|
999
1282
|
}
|
|
1000
1283
|
|
|
1001
1284
|
// src/specTools.ts
|
|
1002
|
-
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
1003
|
-
import { z } from "zod";
|
|
1285
|
+
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1286
|
+
import { z as z2 } from "zod";
|
|
1004
1287
|
var MAX_FIRST_ROUND = 4;
|
|
1005
|
-
var questionSchema =
|
|
1006
|
-
id:
|
|
1007
|
-
question:
|
|
1008
|
-
whyItMatters:
|
|
1009
|
-
options:
|
|
1010
|
-
label:
|
|
1011
|
-
description:
|
|
1012
|
-
recommended:
|
|
1288
|
+
var questionSchema = z2.object({
|
|
1289
|
+
id: z2.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
|
|
1290
|
+
question: z2.string().describe("One plain sentence the owner can answer without reading code"),
|
|
1291
|
+
whyItMatters: z2.string().describe("One line: what changes in the result depending on the answer"),
|
|
1292
|
+
options: z2.array(z2.object({
|
|
1293
|
+
label: z2.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
|
|
1294
|
+
description: z2.string().describe("One sentence: what happens if this option is chosen"),
|
|
1295
|
+
recommended: z2.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
|
|
1013
1296
|
})).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'),
|
|
1014
|
-
multiSelect:
|
|
1297
|
+
multiSelect: z2.boolean().describe("true only when the options are additive and several can apply together")
|
|
1015
1298
|
});
|
|
1016
|
-
var briefSchema =
|
|
1017
|
-
findings:
|
|
1018
|
-
fact:
|
|
1019
|
-
where:
|
|
1299
|
+
var briefSchema = z2.object({
|
|
1300
|
+
findings: z2.array(z2.object({
|
|
1301
|
+
fact: z2.string().describe("What the code does today that matters for this task"),
|
|
1302
|
+
where: z2.string().describe("Where it is: owner/repo path:line")
|
|
1020
1303
|
})).describe("Findings from the code"),
|
|
1021
|
-
dataFacts:
|
|
1022
|
-
approach:
|
|
1023
|
-
codeMap:
|
|
1024
|
-
repo:
|
|
1025
|
-
file:
|
|
1026
|
-
role:
|
|
1304
|
+
dataFacts: z2.array(z2.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
|
|
1305
|
+
approach: z2.string().describe("The approach you would take, in 2-4 sentences"),
|
|
1306
|
+
codeMap: z2.array(z2.object({
|
|
1307
|
+
repo: z2.string().describe("Repository full name (owner/name)"),
|
|
1308
|
+
file: z2.string().describe("Path relative to the repository root"),
|
|
1309
|
+
role: z2.string().describe("Why it matters: what to change or reuse there")
|
|
1027
1310
|
})).describe("The files the change will touch or must reuse"),
|
|
1028
|
-
assumptions:
|
|
1029
|
-
text:
|
|
1030
|
-
reason:
|
|
1311
|
+
assumptions: z2.array(z2.object({
|
|
1312
|
+
text: z2.string().describe("The default you chose"),
|
|
1313
|
+
reason: z2.string().describe("Why it is safe or why it was chosen")
|
|
1031
1314
|
})).describe("Corners settled with a default instead of a question"),
|
|
1032
|
-
pendingDecisions:
|
|
1315
|
+
pendingDecisions: z2.array(z2.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
|
|
1033
1316
|
});
|
|
1034
|
-
var questionsPayloadSchema =
|
|
1317
|
+
var questionsPayloadSchema = z2.object({
|
|
1035
1318
|
brief: briefSchema,
|
|
1036
|
-
questions:
|
|
1319
|
+
questions: z2.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`)
|
|
1037
1320
|
});
|
|
1038
|
-
var specSchema =
|
|
1039
|
-
goal:
|
|
1040
|
-
context:
|
|
1041
|
-
inScope:
|
|
1042
|
-
outOfScope:
|
|
1043
|
-
decisions:
|
|
1044
|
-
question:
|
|
1045
|
-
answer:
|
|
1046
|
-
by:
|
|
1321
|
+
var specSchema = z2.object({
|
|
1322
|
+
goal: z2.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
|
|
1323
|
+
context: z2.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
|
|
1324
|
+
inScope: z2.array(z2.string()).describe("What this task delivers, one item each"),
|
|
1325
|
+
outOfScope: z2.array(z2.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"),
|
|
1326
|
+
decisions: z2.array(z2.object({
|
|
1327
|
+
question: z2.string(),
|
|
1328
|
+
answer: z2.string(),
|
|
1329
|
+
by: z2.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
1047
1330
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
1048
|
-
codeMap:
|
|
1049
|
-
repo:
|
|
1050
|
-
file:
|
|
1051
|
-
role:
|
|
1052
|
-
use:
|
|
1331
|
+
codeMap: z2.array(z2.object({
|
|
1332
|
+
repo: z2.string().describe("Repository full name (owner/name)"),
|
|
1333
|
+
file: z2.string().describe("Path relative to the repository root"),
|
|
1334
|
+
role: z2.string().describe("What the coder needs to know about this file"),
|
|
1335
|
+
use: z2.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
|
|
1053
1336
|
})).describe("The files a coder must read before starting"),
|
|
1054
|
-
changes:
|
|
1055
|
-
repo:
|
|
1056
|
-
file:
|
|
1057
|
-
action:
|
|
1058
|
-
description:
|
|
1059
|
-
produces:
|
|
1060
|
-
consumes:
|
|
1061
|
-
acceptance:
|
|
1337
|
+
changes: z2.array(z2.object({
|
|
1338
|
+
repo: z2.string().describe("Repository full name (owner/name) where the change happens"),
|
|
1339
|
+
file: z2.string().describe("Path to the file, relative to the repository root"),
|
|
1340
|
+
action: z2.enum(["create", "modify", "delete"]),
|
|
1341
|
+
description: z2.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
|
|
1342
|
+
produces: z2.array(z2.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
|
|
1343
|
+
consumes: z2.array(z2.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
|
|
1344
|
+
acceptance: z2.array(z2.string()).describe("Ids of the criteria this change makes true, like AC-1")
|
|
1062
1345
|
})).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
|
|
1063
|
-
acceptance:
|
|
1064
|
-
id:
|
|
1065
|
-
kind:
|
|
1066
|
-
text:
|
|
1346
|
+
acceptance: z2.array(z2.object({
|
|
1347
|
+
id: z2.string().describe("AC-1, AC-2 and so on, in order"),
|
|
1348
|
+
kind: z2.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
|
|
1349
|
+
text: z2.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
|
|
1067
1350
|
})).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
|
|
1068
|
-
testPlan:
|
|
1069
|
-
automated:
|
|
1070
|
-
repo:
|
|
1071
|
-
command:
|
|
1072
|
-
expect:
|
|
1351
|
+
testPlan: z2.object({
|
|
1352
|
+
automated: z2.array(z2.object({
|
|
1353
|
+
repo: z2.string(),
|
|
1354
|
+
command: z2.string().describe("The exact command to run"),
|
|
1355
|
+
expect: z2.string().describe("What passing looks like")
|
|
1073
1356
|
})).describe("At least one command"),
|
|
1074
|
-
newTests:
|
|
1075
|
-
seam:
|
|
1076
|
-
covers:
|
|
1357
|
+
newTests: z2.array(z2.object({
|
|
1358
|
+
seam: z2.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
|
|
1359
|
+
covers: z2.array(z2.string()).describe("Ids of the criteria it covers")
|
|
1077
1360
|
})),
|
|
1078
|
-
manual:
|
|
1079
|
-
page:
|
|
1080
|
-
steps:
|
|
1361
|
+
manual: z2.array(z2.object({
|
|
1362
|
+
page: z2.string().describe("The page or screen"),
|
|
1363
|
+
steps: z2.string().describe("What a person does and sees")
|
|
1081
1364
|
}))
|
|
1082
1365
|
}),
|
|
1083
|
-
risks:
|
|
1084
|
-
risk:
|
|
1085
|
-
mitigation:
|
|
1366
|
+
risks: z2.array(z2.object({
|
|
1367
|
+
risk: z2.string(),
|
|
1368
|
+
mitigation: z2.string()
|
|
1086
1369
|
})).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
|
|
1087
|
-
riskLevel:
|
|
1088
|
-
assumptions:
|
|
1089
|
-
text:
|
|
1090
|
-
reason:
|
|
1370
|
+
riskLevel: z2.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
|
|
1371
|
+
assumptions: z2.array(z2.object({
|
|
1372
|
+
text: z2.string().describe("The default you chose"),
|
|
1373
|
+
reason: z2.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
1091
1374
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
1092
|
-
openQuestions:
|
|
1093
|
-
split:
|
|
1375
|
+
openQuestions: z2.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
|
|
1376
|
+
split: z2.array(z2.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")
|
|
1094
1377
|
});
|
|
1095
1378
|
function reply(text2, isError = false) {
|
|
1096
1379
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
@@ -1100,7 +1383,7 @@ function unreachable(error, toolName) {
|
|
|
1100
1383
|
return reply(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
|
|
1101
1384
|
}
|
|
1102
1385
|
function questionsTool(review, onSubmit) {
|
|
1103
|
-
return
|
|
1386
|
+
return tool2(
|
|
1104
1387
|
SUBMIT_QUESTIONS_TOOL_NAME,
|
|
1105
1388
|
"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.",
|
|
1106
1389
|
questionsPayloadSchema.shape,
|
|
@@ -1123,7 +1406,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
|
|
|
1123
1406
|
function specTool(review, onSubmit, builderCheckRan) {
|
|
1124
1407
|
let repairs = 0;
|
|
1125
1408
|
let reminded = false;
|
|
1126
|
-
return
|
|
1409
|
+
return tool2(
|
|
1127
1410
|
SUBMIT_SPEC_TOOL_NAME,
|
|
1128
1411
|
"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.",
|
|
1129
1412
|
specSchema.shape,
|
|
@@ -1243,14 +1526,12 @@ function usageLimitWatch() {
|
|
|
1243
1526
|
}
|
|
1244
1527
|
|
|
1245
1528
|
// src/claude.ts
|
|
1246
|
-
var NOBODY_TO_ASK = "Nobody is at the computer to approve this during a TAMsense run. Do it another way, or explain in your report why it is needed.";
|
|
1247
1529
|
var NO_RESULT_TEXT = "Claude finished without a result.";
|
|
1248
1530
|
var SERVER_NAME = "tamsense";
|
|
1249
1531
|
var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
|
|
1250
|
-
var denyWhenAsked = async () => ({ behavior: "deny", message: NOBODY_TO_ASK });
|
|
1251
1532
|
var CAPTION_FIELDS = {
|
|
1252
|
-
title:
|
|
1253
|
-
purpose:
|
|
1533
|
+
title: z3.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"'),
|
|
1534
|
+
purpose: z3.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
|
|
1254
1535
|
};
|
|
1255
1536
|
var ATTACH_TOOLS = {
|
|
1256
1537
|
screenshot: {
|
|
@@ -1271,35 +1552,36 @@ var ATTACH_TOOLS = {
|
|
|
1271
1552
|
};
|
|
1272
1553
|
function attachTool(kind, attach) {
|
|
1273
1554
|
const spec = ATTACH_TOOLS[kind];
|
|
1274
|
-
return
|
|
1555
|
+
return tool3(
|
|
1275
1556
|
spec.name,
|
|
1276
1557
|
spec.description,
|
|
1277
|
-
{ path:
|
|
1558
|
+
{ path: z3.string().describe(spec.path), ...CAPTION_FIELDS },
|
|
1278
1559
|
async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
|
|
1279
1560
|
);
|
|
1280
1561
|
}
|
|
1281
1562
|
function verdictTool(onVerdict) {
|
|
1282
|
-
return
|
|
1563
|
+
return tool3(
|
|
1283
1564
|
VERDICT_TOOL_NAME,
|
|
1284
1565
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
1285
1566
|
{
|
|
1286
|
-
passed:
|
|
1287
|
-
report:
|
|
1567
|
+
passed: z3.boolean().describe("true only if nothing must be fixed"),
|
|
1568
|
+
report: z3.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
|
|
1569
|
+
couldNotRun: z3.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")
|
|
1288
1570
|
},
|
|
1289
1571
|
async (args) => {
|
|
1290
|
-
onVerdict({ passed: args.passed, report: args.report });
|
|
1572
|
+
onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
|
|
1291
1573
|
return { content: [{ type: "text", text: "Verdict received. You can stop now." }] };
|
|
1292
1574
|
}
|
|
1293
1575
|
);
|
|
1294
1576
|
}
|
|
1295
1577
|
function handoffTool(targets, onHandoff) {
|
|
1296
|
-
return
|
|
1578
|
+
return tool3(
|
|
1297
1579
|
HANDOFF_TOOL_NAME,
|
|
1298
1580
|
"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.",
|
|
1299
1581
|
{
|
|
1300
|
-
to:
|
|
1301
|
-
outcome:
|
|
1302
|
-
note:
|
|
1582
|
+
to: z3.string().describe("name or id of the step to hand the task to"),
|
|
1583
|
+
outcome: z3.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
|
|
1584
|
+
note: z3.string().describe("for the next agent and the owner: what was done, what to check, what is open")
|
|
1303
1585
|
},
|
|
1304
1586
|
async (args) => {
|
|
1305
1587
|
const choice = chooseHandoff(targets, args);
|
|
@@ -1311,6 +1593,48 @@ function handoffTool(targets, onHandoff) {
|
|
|
1311
1593
|
}
|
|
1312
1594
|
);
|
|
1313
1595
|
}
|
|
1596
|
+
function askPersonTool(asking, waiting) {
|
|
1597
|
+
return tool3(
|
|
1598
|
+
ASK_PERSON_TOOL_NAME,
|
|
1599
|
+
"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.",
|
|
1600
|
+
{
|
|
1601
|
+
to: z3.union([z3.number(), z3.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"),
|
|
1602
|
+
why: z3.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
|
|
1603
|
+
question: z3.string().describe("one plain question the person can answer without reading the code"),
|
|
1604
|
+
context: z3.string().optional().describe("one line on what you saw that led to the question"),
|
|
1605
|
+
tried: z3.array(z3.object({
|
|
1606
|
+
did: z3.string().describe("the command or step"),
|
|
1607
|
+
error: z3.string().optional().describe("the exact error line, if there was one"),
|
|
1608
|
+
result: z3.string().describe("what it showed, one line")
|
|
1609
|
+
})).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
|
|
1610
|
+
options: z3.array(z3.object({
|
|
1611
|
+
label: z3.string().describe("a short answer the person can pick"),
|
|
1612
|
+
detail: z3.string().describe("one line on what happens if it is picked")
|
|
1613
|
+
})).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
|
|
1614
|
+
blocking: z3.boolean().describe("true when you cannot go on without the answer"),
|
|
1615
|
+
assumption: z3.string().optional().describe("when blocking is false: what you continue with until the answer comes")
|
|
1616
|
+
},
|
|
1617
|
+
async (args) => {
|
|
1618
|
+
if (waiting.waitingFor !== null) {
|
|
1619
|
+
return { content: [{ type: "text", text: ALREADY_WAITING_TEXT }], isError: true };
|
|
1620
|
+
}
|
|
1621
|
+
const choice = checkAsk(asking.people, args);
|
|
1622
|
+
if ("error" in choice) {
|
|
1623
|
+
return { content: [{ type: "text", text: choice.error }], isError: true };
|
|
1624
|
+
}
|
|
1625
|
+
let reply2;
|
|
1626
|
+
try {
|
|
1627
|
+
reply2 = await asking.send(choice.body);
|
|
1628
|
+
} catch (error) {
|
|
1629
|
+
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 };
|
|
1630
|
+
}
|
|
1631
|
+
if (choice.body.blocking) {
|
|
1632
|
+
waiting.waitingFor = reply2.id;
|
|
1633
|
+
}
|
|
1634
|
+
return { content: [{ type: "text", text: askedText(reply2.name, choice.body) }] };
|
|
1635
|
+
}
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1314
1638
|
function specWorkTools(specWork, submitted) {
|
|
1315
1639
|
if (!specWork) {
|
|
1316
1640
|
return [];
|
|
@@ -1321,6 +1645,16 @@ function specWorkTools(specWork, submitted) {
|
|
|
1321
1645
|
submitted.spec = spec;
|
|
1322
1646
|
}, () => submitted.helperStarted)];
|
|
1323
1647
|
}
|
|
1648
|
+
function knowledgeTools(knowledge) {
|
|
1649
|
+
if (!knowledge) {
|
|
1650
|
+
return [];
|
|
1651
|
+
}
|
|
1652
|
+
return [
|
|
1653
|
+
...knowledge.entryIds.length > 0 ? [updateKnowledgeTool(knowledge)] : [],
|
|
1654
|
+
...knowledge.moreIds.length > 0 ? [readKnowledgeTool(knowledge)] : [],
|
|
1655
|
+
...knowledge.canPropose ? [proposeKnowledgeTool(knowledge)] : []
|
|
1656
|
+
];
|
|
1657
|
+
}
|
|
1324
1658
|
function tamsenseServer(request, submitted) {
|
|
1325
1659
|
const tools = [
|
|
1326
1660
|
attachTool("screenshot", request.attach),
|
|
@@ -1330,6 +1664,8 @@ function tamsenseServer(request, submitted) {
|
|
|
1330
1664
|
submitted.verdict = verdict;
|
|
1331
1665
|
})] : [],
|
|
1332
1666
|
...specWorkTools(request.specWork, submitted),
|
|
1667
|
+
...request.asking ? [askPersonTool(request.asking, submitted)] : [],
|
|
1668
|
+
...knowledgeTools(request.knowledge),
|
|
1333
1669
|
...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
|
|
1334
1670
|
submitted.handoff = handoff;
|
|
1335
1671
|
})] : []
|
|
@@ -1368,7 +1704,8 @@ async function attempt(request, context, resume) {
|
|
|
1368
1704
|
permissionMode: "auto",
|
|
1369
1705
|
// краткий пересказ размышлений Claude идёт в журнал задачи, как в Paseo
|
|
1370
1706
|
thinking: { type: "adaptive", display: "summarized" },
|
|
1371
|
-
|
|
1707
|
+
// режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
|
|
1708
|
+
canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
|
|
1372
1709
|
plugins: [{ type: "local", path: LIBRARY_PATH }],
|
|
1373
1710
|
mcpServers: { [SERVER_NAME]: tamsense.server },
|
|
1374
1711
|
allowedTools: tamsense.allowed,
|
|
@@ -1397,6 +1734,7 @@ async function attempt(request, context, resume) {
|
|
|
1397
1734
|
request.onFeed(draft);
|
|
1398
1735
|
if (draft.kind === "text" && draft.parent === null) {
|
|
1399
1736
|
lastText = draft.text;
|
|
1737
|
+
context.lastWords.text = draft.text;
|
|
1400
1738
|
}
|
|
1401
1739
|
}
|
|
1402
1740
|
if (message.type === "result") {
|
|
@@ -1421,9 +1759,9 @@ function claudeRunner(startQuery) {
|
|
|
1421
1759
|
request.input.close();
|
|
1422
1760
|
abortController.abort();
|
|
1423
1761
|
}, { once: true });
|
|
1424
|
-
const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false };
|
|
1762
|
+
const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false, waitingFor: null };
|
|
1425
1763
|
const limit = usageLimitWatch();
|
|
1426
|
-
const context = { startQuery, abortController, submitted, limit };
|
|
1764
|
+
const context = { startQuery, abortController, submitted, limit, lastWords: { text: "" } };
|
|
1427
1765
|
try {
|
|
1428
1766
|
let result = await attempt(request, context, request.resumeSessionId);
|
|
1429
1767
|
if (result.resumeFailed) {
|
|
@@ -1431,7 +1769,7 @@ function claudeRunner(startQuery) {
|
|
|
1431
1769
|
}
|
|
1432
1770
|
const pausedUntil = limit.pausedUntil(result.outcome.completed);
|
|
1433
1771
|
const outcome = pausedUntil ? { ...result.outcome, completed: false, report: USAGE_LIMIT_REPORT } : result.outcome;
|
|
1434
|
-
return { ...outcome, verdict: submitted.verdict, handoff: submitted.handoff, questions: submitted.questions, spec: submitted.spec, sessionId: result.sessionId, pausedUntil };
|
|
1772
|
+
return { ...outcome, verdict: submitted.verdict, handoff: submitted.handoff, questions: submitted.questions, spec: submitted.spec, sessionId: result.sessionId, pausedUntil, waitingFor: submitted.waitingFor };
|
|
1435
1773
|
} finally {
|
|
1436
1774
|
request.input.close();
|
|
1437
1775
|
}
|
|
@@ -1455,9 +1793,9 @@ function parseConfig(value) {
|
|
|
1455
1793
|
if (!isRecord2(value)) {
|
|
1456
1794
|
return null;
|
|
1457
1795
|
}
|
|
1458
|
-
const { server, token, deviceId, deviceName
|
|
1796
|
+
const { server, token, deviceId, deviceName } = value;
|
|
1459
1797
|
const isValid = typeof server === "string" && typeof token === "string" && typeof deviceId === "number" && typeof deviceName === "string";
|
|
1460
|
-
return isValid ? { server, token, deviceId, deviceName
|
|
1798
|
+
return isValid ? { server, token, deviceId, deviceName } : null;
|
|
1461
1799
|
}
|
|
1462
1800
|
function isMissingFile(error) {
|
|
1463
1801
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
@@ -1493,6 +1831,12 @@ var CAPABILITIES_PATH = "/api/runner/me/capabilities";
|
|
|
1493
1831
|
function runPath(runId, action) {
|
|
1494
1832
|
return `/api/runner/runs/${runId}/${action}`;
|
|
1495
1833
|
}
|
|
1834
|
+
function knowledgePath(runId, entryId) {
|
|
1835
|
+
return `/api/runner/runs/${runId}/knowledge/${entryId}`;
|
|
1836
|
+
}
|
|
1837
|
+
function permissionPath(runId, permissionId) {
|
|
1838
|
+
return `/api/runner/runs/${runId}/permissions/${permissionId}`;
|
|
1839
|
+
}
|
|
1496
1840
|
function recordingUploadPath(runId, upload, part) {
|
|
1497
1841
|
return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
|
|
1498
1842
|
}
|
|
@@ -1545,6 +1889,7 @@ var META_SUFFIX = ".json";
|
|
|
1545
1889
|
var MAX_TITLE_LENGTH = 60;
|
|
1546
1890
|
var MAX_PURPOSE_LENGTH = 160;
|
|
1547
1891
|
var DEFAULT_TRACKER_ORIGINS = ["https://my.tamsense.com"];
|
|
1892
|
+
var READ_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
1548
1893
|
var BINDING = "__tamsenseRecord";
|
|
1549
1894
|
var STATE_KEY = /* @__PURE__ */ Symbol.for("tamsense.recorder");
|
|
1550
1895
|
var FLUSH_MS = 100;
|
|
@@ -1573,6 +1918,13 @@ function isTrackerIngest(url, tamsenseOrigins) {
|
|
|
1573
1918
|
}
|
|
1574
1919
|
return url.searchParams.has("key") || tamsenseOrigins.includes(url.origin);
|
|
1575
1920
|
}
|
|
1921
|
+
function isLocalHost(hostname2) {
|
|
1922
|
+
const host = hostname2.toLowerCase().replace(/^\[|\]$/g, "").replace(/\.$/, "");
|
|
1923
|
+
return host === "localhost" || host.endsWith(".localhost") || host === "::1" || host === "0.0.0.0" || /^127(?:\.\d{1,3}){3}$/.test(host);
|
|
1924
|
+
}
|
|
1925
|
+
function isExternalWrite(method, url) {
|
|
1926
|
+
return !READ_METHODS.includes(method.toUpperCase()) && !isLocalHost(url.hostname);
|
|
1927
|
+
}
|
|
1576
1928
|
function trackerOrigins(env = process2.env) {
|
|
1577
1929
|
const origins = new Set(DEFAULT_TRACKER_ORIGINS);
|
|
1578
1930
|
try {
|
|
@@ -1659,7 +2011,7 @@ var Session = class {
|
|
|
1659
2011
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
1660
2012
|
this.file = join3(dir, `${id}${REPLAY_SUFFIX}`);
|
|
1661
2013
|
this.metaFile = join3(dir, `${id}${META_SUFFIX}`);
|
|
1662
|
-
this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, truncated: false, automatic: recording.automatic };
|
|
2014
|
+
this.meta = { version: 1, id, ...recording.caption, startedAt: Date.now(), endedAt: null, url, marks: [], video: null, gaps: [], blockedTrackerRequests: 0, blockedExternalWrites: 0, truncated: false, automatic: recording.automatic };
|
|
1663
2015
|
writeFileSync(this.file, "");
|
|
1664
2016
|
this.save();
|
|
1665
2017
|
}
|
|
@@ -1703,10 +2055,20 @@ var Session = class {
|
|
|
1703
2055
|
}
|
|
1704
2056
|
}
|
|
1705
2057
|
summary() {
|
|
1706
|
-
const { title, purpose, video, marks, gaps, blockedTrackerRequests } = this.meta;
|
|
1707
|
-
return { title, purpose, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests };
|
|
2058
|
+
const { title, purpose, video, marks, gaps, blockedTrackerRequests, blockedExternalWrites } = this.meta;
|
|
2059
|
+
return { title, purpose, replay: this.file, video, marks: marks.length, gaps, blockedTrackerRequests, blockedExternalWrites };
|
|
1708
2060
|
}
|
|
1709
2061
|
};
|
|
2062
|
+
function countBlocked(route, counter) {
|
|
2063
|
+
try {
|
|
2064
|
+
const session = state().pages.get(route.request().frame().page());
|
|
2065
|
+
if (session) {
|
|
2066
|
+
session.meta[counter] += 1;
|
|
2067
|
+
session.save();
|
|
2068
|
+
}
|
|
2069
|
+
} catch {
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
1710
2072
|
async function startSession(recording, page, injectNow) {
|
|
1711
2073
|
const current = state();
|
|
1712
2074
|
if (current.pages.has(page)) {
|
|
@@ -1748,15 +2110,19 @@ async function recordContext(context, caption, automatic = false) {
|
|
|
1748
2110
|
const recording = { caption: checked, automatic, sessions: [] };
|
|
1749
2111
|
current.contexts.set(context, recording);
|
|
1750
2112
|
const origins = trackerOrigins();
|
|
1751
|
-
await context.route((url) =>
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
}
|
|
1758
|
-
} catch {
|
|
2113
|
+
await context.route((url) => !isLocalHost(url.hostname), async (route) => {
|
|
2114
|
+
const request = route.request();
|
|
2115
|
+
if (!isExternalWrite(request.method(), new URL(request.url()))) {
|
|
2116
|
+
await route.continue().catch(() => {
|
|
2117
|
+
});
|
|
2118
|
+
return;
|
|
1759
2119
|
}
|
|
2120
|
+
countBlocked(route, "blockedExternalWrites");
|
|
2121
|
+
await route.abort("blockedbyclient").catch(() => {
|
|
2122
|
+
});
|
|
2123
|
+
});
|
|
2124
|
+
await context.route((url) => isTrackerIngest(url, origins), async (route) => {
|
|
2125
|
+
countBlocked(route, "blockedTrackerRequests");
|
|
1760
2126
|
await route.fulfill({ status: 200, contentType: "application/json", body: '{"ok":true}' });
|
|
1761
2127
|
});
|
|
1762
2128
|
await context.exposeBinding(BINDING, ({ page }, payload) => current.pages.get(page)?.receive(payload));
|
|
@@ -1850,7 +2216,6 @@ function describeDevice(identity) {
|
|
|
1850
2216
|
import { spawn as spawn2 } from "child_process";
|
|
1851
2217
|
import { hostname } from "os";
|
|
1852
2218
|
import { delimiter as delimiter2 } from "path";
|
|
1853
|
-
import { createInterface as createInterface2 } from "readline/promises";
|
|
1854
2219
|
import { setTimeout as wait } from "timers/promises";
|
|
1855
2220
|
var PLATFORM_LABELS = {
|
|
1856
2221
|
win32: "Windows",
|
|
@@ -1886,22 +2251,6 @@ function withNodeOnPath(env, nodeDir, separator = delimiter2) {
|
|
|
1886
2251
|
}
|
|
1887
2252
|
return { ...env, [key]: [...entries, nodeDir].join(separator) };
|
|
1888
2253
|
}
|
|
1889
|
-
async function askYesNo(question) {
|
|
1890
|
-
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
1891
|
-
try {
|
|
1892
|
-
for (; ; ) {
|
|
1893
|
-
const answer = (await prompt.question(question)).trim().toLowerCase();
|
|
1894
|
-
if (answer === "" || answer === "y" || answer === "yes") {
|
|
1895
|
-
return true;
|
|
1896
|
-
}
|
|
1897
|
-
if (answer === "n" || answer === "no") {
|
|
1898
|
-
return false;
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
} finally {
|
|
1902
|
-
prompt.close();
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
2254
|
async function sleep(ms, signal) {
|
|
1906
2255
|
await wait(ms, void 0, { signal }).catch(() => {
|
|
1907
2256
|
});
|
|
@@ -2028,10 +2377,10 @@ function keyedLock() {
|
|
|
2028
2377
|
const previous = tails.get(key) ?? Promise.resolve();
|
|
2029
2378
|
let release = () => {
|
|
2030
2379
|
};
|
|
2031
|
-
const
|
|
2380
|
+
const done2 = new Promise((resolve) => {
|
|
2032
2381
|
release = resolve;
|
|
2033
2382
|
});
|
|
2034
|
-
const tail = previous.then(() =>
|
|
2383
|
+
const tail = previous.then(() => done2);
|
|
2035
2384
|
tails.set(key, tail);
|
|
2036
2385
|
await previous;
|
|
2037
2386
|
try {
|
|
@@ -2051,7 +2400,7 @@ import { randomUUID } from "crypto";
|
|
|
2051
2400
|
import { createReadStream } from "fs";
|
|
2052
2401
|
import { readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
|
|
2053
2402
|
import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as join5 } from "path";
|
|
2054
|
-
import { createInterface as
|
|
2403
|
+
import { createInterface as createInterface2 } from "readline";
|
|
2055
2404
|
import { Readable } from "stream";
|
|
2056
2405
|
import { promisify } from "util";
|
|
2057
2406
|
import { gzip } from "zlib";
|
|
@@ -2143,15 +2492,15 @@ async function ledger(dir) {
|
|
|
2143
2492
|
}
|
|
2144
2493
|
}
|
|
2145
2494
|
async function remember(dir, file) {
|
|
2146
|
-
const
|
|
2147
|
-
|
|
2148
|
-
await writeFile3(join5(dir, LEDGER_FILE), JSON.stringify([...
|
|
2495
|
+
const done2 = await ledger(dir);
|
|
2496
|
+
done2.add(file);
|
|
2497
|
+
await writeFile3(join5(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
2149
2498
|
}
|
|
2150
2499
|
async function nonEmpty(file) {
|
|
2151
2500
|
return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
|
|
2152
2501
|
}
|
|
2153
2502
|
async function pendingRecordings(dir) {
|
|
2154
|
-
const
|
|
2503
|
+
const done2 = await ledger(dir);
|
|
2155
2504
|
const names = (await readdir(dir).catch(() => [])).sort();
|
|
2156
2505
|
const pending = [];
|
|
2157
2506
|
for (const name of names) {
|
|
@@ -2160,10 +2509,10 @@ async function pendingRecordings(dir) {
|
|
|
2160
2509
|
}
|
|
2161
2510
|
const meta = await readMeta(join5(dir, name));
|
|
2162
2511
|
const replay = join5(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
2163
|
-
if (!
|
|
2512
|
+
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
2164
2513
|
pending.push({ kind: "replay", file: replay, meta });
|
|
2165
2514
|
}
|
|
2166
|
-
if (meta?.video && !
|
|
2515
|
+
if (meta?.video && !done2.has(meta.video) && await nonEmpty(meta.video)) {
|
|
2167
2516
|
pending.push({ kind: "video", file: meta.video, meta });
|
|
2168
2517
|
}
|
|
2169
2518
|
}
|
|
@@ -2172,7 +2521,7 @@ async function pendingRecordings(dir) {
|
|
|
2172
2521
|
async function uploadReplay(upload, file, caption) {
|
|
2173
2522
|
const id = randomUUID();
|
|
2174
2523
|
const scan = { startTs: Number.POSITIVE_INFINITY, snapshot: false, marks: [] };
|
|
2175
|
-
const lines =
|
|
2524
|
+
const lines = createInterface2({ input: createReadStream(file, "utf8"), crlfDelay: Infinity });
|
|
2176
2525
|
for await (const chunk of replayChunks(lines, scan)) {
|
|
2177
2526
|
await upload.api.upload("POST", recordingUploadPath(upload.runId, id, "chunks"), new Uint8Array(await gzipAsync(chunk)), "application/gzip");
|
|
2178
2527
|
}
|
|
@@ -2376,6 +2725,16 @@ async function baseRef(cache, job, repository) {
|
|
|
2376
2725
|
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
2377
2726
|
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
2378
2727
|
}
|
|
2728
|
+
async function keptBase(cache, path, job, repository) {
|
|
2729
|
+
if (!job.keepWorkspace || !existsSync3(join6(path, ".git"))) {
|
|
2730
|
+
return null;
|
|
2731
|
+
}
|
|
2732
|
+
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
2733
|
+
if (branch.code !== 0 || branch.stdout !== job.branch) {
|
|
2734
|
+
return null;
|
|
2735
|
+
}
|
|
2736
|
+
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
2737
|
+
}
|
|
2379
2738
|
async function prepareWorkspace(job, token, tools) {
|
|
2380
2739
|
const root = tamsenseFolder(tools.home, "work", `task-${job.taskId}`);
|
|
2381
2740
|
const folders = folderNames(job.repositories);
|
|
@@ -2386,6 +2745,10 @@ async function prepareWorkspace(job, token, tools) {
|
|
|
2386
2745
|
const path = join6(root, folder);
|
|
2387
2746
|
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
2388
2747
|
await refreshCache(cache, repository, token, tools);
|
|
2748
|
+
const kept = await keptBase(cache, path, job, repository);
|
|
2749
|
+
if (kept) {
|
|
2750
|
+
return kept;
|
|
2751
|
+
}
|
|
2389
2752
|
await removeWorktree(cache, path);
|
|
2390
2753
|
await git(["worktree", "add", "--force", "-B", job.branch, path, await baseRef(cache, job, repository)], { cwd: cache });
|
|
2391
2754
|
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
@@ -2440,7 +2803,7 @@ var FEED_INTERVAL_MS = 1500;
|
|
|
2440
2803
|
var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
2441
2804
|
var STOPPED_HERE = "stopped_here";
|
|
2442
2805
|
var CLOSED_ON_SITE = "closed_on_site";
|
|
2443
|
-
function
|
|
2806
|
+
function messageOf3(error) {
|
|
2444
2807
|
return error instanceof Error ? error.message : String(error);
|
|
2445
2808
|
}
|
|
2446
2809
|
function isFatal(error) {
|
|
@@ -2460,7 +2823,7 @@ async function attachScreenshot(api, job, screenshot, attached) {
|
|
|
2460
2823
|
try {
|
|
2461
2824
|
await api.request("POST", runPath(job.runId, "attachments"), body);
|
|
2462
2825
|
} catch (error) {
|
|
2463
|
-
return `Could not attach the screenshot: ${
|
|
2826
|
+
return `Could not attach the screenshot: ${messageOf3(error)}`;
|
|
2464
2827
|
}
|
|
2465
2828
|
attached.count += 1;
|
|
2466
2829
|
return "Attached to the task.";
|
|
@@ -2470,7 +2833,7 @@ function sessionReporter(api, job, print2) {
|
|
|
2470
2833
|
return {
|
|
2471
2834
|
report: (session) => {
|
|
2472
2835
|
sending ??= api.request("POST", runPath(job.runId, "session"), session).then(() => {
|
|
2473
|
-
}, (error) => print2(`Could not tell TAMsense which Claude took the task: ${
|
|
2836
|
+
}, (error) => print2(`Could not tell TAMsense which Claude took the task: ${messageOf3(error)}`));
|
|
2474
2837
|
},
|
|
2475
2838
|
sent: async () => {
|
|
2476
2839
|
await sending;
|
|
@@ -2504,7 +2867,7 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2504
2867
|
} catch (error) {
|
|
2505
2868
|
input.restoreDelivered(ack);
|
|
2506
2869
|
if (!failing) {
|
|
2507
|
-
tools.print(`Could not check for new messages on TAMsense, trying again: ${
|
|
2870
|
+
tools.print(`Could not check for new messages on TAMsense, trying again: ${messageOf3(error)}`);
|
|
2508
2871
|
}
|
|
2509
2872
|
failing = true;
|
|
2510
2873
|
return;
|
|
@@ -2527,7 +2890,7 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2527
2890
|
record({ kind: "tool", text: switchLine(change), tool: "model", ref: null, parent: null, failed: false });
|
|
2528
2891
|
}
|
|
2529
2892
|
},
|
|
2530
|
-
(error) => tools.print(`Could not switch the model: ${
|
|
2893
|
+
(error) => tools.print(`Could not switch the model: ${messageOf3(error)}`)
|
|
2531
2894
|
);
|
|
2532
2895
|
}
|
|
2533
2896
|
}
|
|
@@ -2562,6 +2925,42 @@ function specWorkTools2(job, api) {
|
|
|
2562
2925
|
}
|
|
2563
2926
|
return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body) };
|
|
2564
2927
|
}
|
|
2928
|
+
function askingTools(job, api) {
|
|
2929
|
+
const people = job.people ?? [];
|
|
2930
|
+
if (people.length === 0 || job.stage.kind === "interview" || job.stage.kind === "spec") {
|
|
2931
|
+
return null;
|
|
2932
|
+
}
|
|
2933
|
+
return { people, send: (body) => api.request("POST", runPath(job.runId, "questions"), body) };
|
|
2934
|
+
}
|
|
2935
|
+
function permissionTools(job, tools) {
|
|
2936
|
+
if (job.stage.kind === "interview" || job.stage.kind === "spec") {
|
|
2937
|
+
return null;
|
|
2938
|
+
}
|
|
2939
|
+
return {
|
|
2940
|
+
send: async (body) => {
|
|
2941
|
+
const reply2 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
|
|
2942
|
+
if (reply2.decision === "wait") {
|
|
2943
|
+
tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
2944
|
+
}
|
|
2945
|
+
return reply2;
|
|
2946
|
+
},
|
|
2947
|
+
check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
|
|
2948
|
+
sleep: tools.sleep
|
|
2949
|
+
};
|
|
2950
|
+
}
|
|
2951
|
+
function knowledgeTools2(job, api) {
|
|
2952
|
+
if (!job.knowledge) {
|
|
2953
|
+
return null;
|
|
2954
|
+
}
|
|
2955
|
+
return {
|
|
2956
|
+
entryIds: knowledgeIds(job.knowledge),
|
|
2957
|
+
moreIds: job.knowledge.more.map((entry) => entry.id),
|
|
2958
|
+
canPropose: canProposeKnowledge(job),
|
|
2959
|
+
propose: (body) => api.request("POST", runPath(job.runId, "knowledge"), body),
|
|
2960
|
+
update: (entryId, body) => api.request("POST", knowledgePath(job.runId, entryId), body),
|
|
2961
|
+
read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2565
2964
|
async function executeJob(job, tools, signal, input, record) {
|
|
2566
2965
|
const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
|
|
2567
2966
|
tools.print("Preparing the code\u2026");
|
|
@@ -2582,6 +2981,9 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2582
2981
|
signal,
|
|
2583
2982
|
wantsVerdict: isCheck,
|
|
2584
2983
|
specWork: specWorkTools2(job, tools.api),
|
|
2984
|
+
asking: askingTools(job, tools.api),
|
|
2985
|
+
permissions: permissionTools(job, tools),
|
|
2986
|
+
knowledge: knowledgeTools2(job, tools.api),
|
|
2585
2987
|
handoffs: job.allowedHandoffs,
|
|
2586
2988
|
resumeSessionId: job.resumeSessionId,
|
|
2587
2989
|
model: job.model,
|
|
@@ -2592,7 +2994,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2592
2994
|
});
|
|
2593
2995
|
await session.sent();
|
|
2594
2996
|
signal.throwIfAborted();
|
|
2595
|
-
const
|
|
2997
|
+
const waitingFor = outcome.waitingFor ?? null;
|
|
2998
|
+
const shouldPublish = job.stage.kind === "code" && outcome.completed && waitingFor === null;
|
|
2596
2999
|
const pushed = shouldPublish ? await publishWorkspace(workspace, job, await gitToken(tools.api, job), identity) : [];
|
|
2597
3000
|
return {
|
|
2598
3001
|
outcome: outcome.completed ? "succeeded" : "failed",
|
|
@@ -2605,7 +3008,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2605
3008
|
sessionId: outcome.sessionId ?? null,
|
|
2606
3009
|
pausedUntil: outcome.pausedUntil ?? null,
|
|
2607
3010
|
questions: outcome.questions ?? null,
|
|
2608
|
-
spec: outcome.spec ?? null
|
|
3011
|
+
spec: outcome.spec ?? null,
|
|
3012
|
+
waitingFor
|
|
2609
3013
|
};
|
|
2610
3014
|
}
|
|
2611
3015
|
function clockOf(iso) {
|
|
@@ -2616,6 +3020,12 @@ function resultLine(finish2) {
|
|
|
2616
3020
|
if (finish2.pausedUntil) {
|
|
2617
3021
|
return `Paused: ${USAGE_LIMIT_REPORT}, resumes at ${clockOf(finish2.pausedUntil)}`;
|
|
2618
3022
|
}
|
|
3023
|
+
if (finish2.waitingFor !== null) {
|
|
3024
|
+
return "Waiting for an answer on TAMsense. This computer is free for other tasks; the run continues here after the answer.";
|
|
3025
|
+
}
|
|
3026
|
+
if (finish2.verdict?.couldNotRun) {
|
|
3027
|
+
return `Check could not run: ${finish2.verdict.report}`;
|
|
3028
|
+
}
|
|
2619
3029
|
if (finish2.verdict) {
|
|
2620
3030
|
return finish2.verdict.passed ? "Check passed." : `Check did not pass: ${finish2.verdict.report}`;
|
|
2621
3031
|
}
|
|
@@ -2645,7 +3055,7 @@ async function sendRecordings(job, tools) {
|
|
|
2645
3055
|
}
|
|
2646
3056
|
}
|
|
2647
3057
|
function failureReport(error, signal) {
|
|
2648
|
-
return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${
|
|
3058
|
+
return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
|
|
2649
3059
|
}
|
|
2650
3060
|
async function runJob(job, tools, stop) {
|
|
2651
3061
|
tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
|
|
@@ -2667,7 +3077,7 @@ async function runJob(job, tools, stop) {
|
|
|
2667
3077
|
try {
|
|
2668
3078
|
finish2 = await executeJob(job, tools, run2.signal, input, record);
|
|
2669
3079
|
} catch (error) {
|
|
2670
|
-
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 };
|
|
3080
|
+
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 };
|
|
2671
3081
|
} finally {
|
|
2672
3082
|
clearInterval(polling);
|
|
2673
3083
|
clearInterval(feeding);
|
|
@@ -2684,12 +3094,12 @@ async function runJob(job, tools, stop) {
|
|
|
2684
3094
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
2685
3095
|
return;
|
|
2686
3096
|
}
|
|
2687
|
-
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${
|
|
3097
|
+
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
|
|
2688
3098
|
try {
|
|
2689
3099
|
await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
|
|
2690
3100
|
tools.print(resultLine(finish2));
|
|
2691
3101
|
} catch (error) {
|
|
2692
|
-
tools.print(`Could not send the result to TAMsense: ${
|
|
3102
|
+
tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
|
|
2693
3103
|
}
|
|
2694
3104
|
}
|
|
2695
3105
|
async function reportCapabilities(tools, stop) {
|
|
@@ -2708,7 +3118,7 @@ async function reportCapabilities(tools, stop) {
|
|
|
2708
3118
|
}
|
|
2709
3119
|
} catch (error) {
|
|
2710
3120
|
if (!complained && !stop.aborted) {
|
|
2711
|
-
tools.print(`Could not tell TAMsense which Claude models this computer has: ${
|
|
3121
|
+
tools.print(`Could not tell TAMsense which Claude models this computer has: ${messageOf3(error)}`);
|
|
2712
3122
|
complained = true;
|
|
2713
3123
|
}
|
|
2714
3124
|
}
|
|
@@ -2727,7 +3137,7 @@ async function work(tools, stop, parallel = 1) {
|
|
|
2727
3137
|
const reporting = reportCapabilities(tools, stopAll);
|
|
2728
3138
|
const start = (job) => {
|
|
2729
3139
|
const runTools = jobTools(job, tools, parallel);
|
|
2730
|
-
const task = taskLock.run(String(job.taskId), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${
|
|
3140
|
+
const task = taskLock.run(String(job.taskId), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
|
|
2731
3141
|
running.add(task);
|
|
2732
3142
|
};
|
|
2733
3143
|
try {
|
|
@@ -2744,7 +3154,7 @@ async function work(tools, stop, parallel = 1) {
|
|
|
2744
3154
|
halt.abort();
|
|
2745
3155
|
throw error;
|
|
2746
3156
|
}
|
|
2747
|
-
tools.print(`Could not reach TAMsense: ${
|
|
3157
|
+
tools.print(`Could not reach TAMsense: ${messageOf3(error)}. Trying again soon.`);
|
|
2748
3158
|
await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
|
|
2749
3159
|
continue;
|
|
2750
3160
|
}
|
|
@@ -2791,8 +3201,7 @@ async function runLogin(server, deviceName) {
|
|
|
2791
3201
|
{ api, openBrowser, print, sleep, now: Date.now },
|
|
2792
3202
|
{ name, platform: platformLabel() }
|
|
2793
3203
|
);
|
|
2794
|
-
|
|
2795
|
-
await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name, ...browserDeclined });
|
|
3204
|
+
await writeConfig({ server: api.server, token: result.token, deviceId: result.device.id, deviceName: result.device.name });
|
|
2796
3205
|
print(`Connected: ${result.device.name}. To take your tasks, run ${START_COMMAND}`);
|
|
2797
3206
|
}
|
|
2798
3207
|
function stopOnInterrupt() {
|
|
@@ -2807,15 +3216,12 @@ function stopOnInterrupt() {
|
|
|
2807
3216
|
function installTools(home) {
|
|
2808
3217
|
return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists: existsSync4 };
|
|
2809
3218
|
}
|
|
2810
|
-
async function browserForStart(home,
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
install: () => installBrowser(installTools(home)),
|
|
2817
|
-
rememberDecline: async () => writeConfig({ ...saved, browserDeclined: true })
|
|
2818
|
-
});
|
|
3219
|
+
async function browserForStart(home, install) {
|
|
3220
|
+
const installed = await installedBrowser(home);
|
|
3221
|
+
if (installed || !install) {
|
|
3222
|
+
return installed;
|
|
3223
|
+
}
|
|
3224
|
+
return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
|
|
2819
3225
|
}
|
|
2820
3226
|
async function startedNewVersion() {
|
|
2821
3227
|
const update = await updateToRun(process5.env, currentVersion(), () => latestVersion());
|
|
@@ -2831,7 +3237,7 @@ async function startedNewVersion() {
|
|
|
2831
3237
|
return false;
|
|
2832
3238
|
}
|
|
2833
3239
|
}
|
|
2834
|
-
async function runStart(parallel) {
|
|
3240
|
+
async function runStart(parallel, installsBrowser) {
|
|
2835
3241
|
if (await startedNewVersion()) {
|
|
2836
3242
|
return;
|
|
2837
3243
|
}
|
|
@@ -2848,17 +3254,13 @@ async function runStart(parallel) {
|
|
|
2848
3254
|
}
|
|
2849
3255
|
print(`${identity.device.name} is connected to ${saved.server} as ${describeDevice(identity)}.`);
|
|
2850
3256
|
const home = homedir3();
|
|
2851
|
-
const browser = await browserForStart(home,
|
|
3257
|
+
const browser = await browserForStart(home, installsBrowser);
|
|
2852
3258
|
await work({ api, runAgent: runClaude, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder() }, stopOnInterrupt(), parallel);
|
|
2853
3259
|
}
|
|
2854
3260
|
async function runBrowser(remove) {
|
|
2855
3261
|
const home = homedir3();
|
|
2856
3262
|
if (remove) {
|
|
2857
|
-
print(await removeBrowser(home) ? "Removed the headless browser." : "No headless browser is installed.");
|
|
2858
|
-
const saved = await readConfig();
|
|
2859
|
-
if (saved) {
|
|
2860
|
-
await writeConfig({ ...saved, browserDeclined: true });
|
|
2861
|
-
}
|
|
3263
|
+
print(await removeBrowser(home) ? "Removed the headless browser. The next start installs it again, unless you add --no-browser." : "No headless browser is installed.");
|
|
2862
3264
|
return;
|
|
2863
3265
|
}
|
|
2864
3266
|
const installed = await installedBrowser(home);
|
|
@@ -2893,7 +3295,7 @@ async function run(command) {
|
|
|
2893
3295
|
if (command.name === "login") {
|
|
2894
3296
|
await runLogin(command.server, command.deviceName);
|
|
2895
3297
|
} else if (command.name === "start") {
|
|
2896
|
-
await runStart(command.parallel);
|
|
3298
|
+
await runStart(command.parallel, command.browser);
|
|
2897
3299
|
} else if (command.name === "browser") {
|
|
2898
3300
|
await runBrowser(command.remove);
|
|
2899
3301
|
} else if (command.name === "status") {
|