@tamsensedev/runner 0.3.0 → 0.6.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/dist/cli.js +649 -355
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -187,7 +187,7 @@ function findNpm(nodePath, env, exists) {
|
|
|
187
187
|
}
|
|
188
188
|
return null;
|
|
189
189
|
}
|
|
190
|
-
var runProcess = (command, args, options) => new Promise((
|
|
190
|
+
var runProcess = (command, args, options) => new Promise((resolve2, reject) => {
|
|
191
191
|
const child = spawn(command, args, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
192
192
|
const tail = [];
|
|
193
193
|
for (const stream of [child.stdout, child.stderr]) {
|
|
@@ -201,7 +201,7 @@ var runProcess = (command, args, options) => new Promise((resolve, reject) => {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
child.on("error", reject);
|
|
204
|
-
child.on("close", (code) =>
|
|
204
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, output: tail.join("\n") }));
|
|
205
205
|
});
|
|
206
206
|
function failureText(what, result) {
|
|
207
207
|
if (OFFLINE_PATTERN.test(result.output)) {
|
|
@@ -314,8 +314,8 @@ async function probeCapabilities(stop) {
|
|
|
314
314
|
stop?.throwIfAborted();
|
|
315
315
|
let release = () => {
|
|
316
316
|
};
|
|
317
|
-
const released = new Promise((
|
|
318
|
-
release =
|
|
317
|
+
const released = new Promise((resolve2) => {
|
|
318
|
+
release = resolve2;
|
|
319
319
|
});
|
|
320
320
|
const nothing = {
|
|
321
321
|
[Symbol.asyncIterator]: () => ({
|
|
@@ -341,15 +341,15 @@ async function probeCapabilities(stop) {
|
|
|
341
341
|
|
|
342
342
|
// src/claude.ts
|
|
343
343
|
import { fileURLToPath } from "url";
|
|
344
|
-
import { createSdkMcpServer, query as query2, tool as
|
|
345
|
-
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
346
|
|
|
347
347
|
// src/askPerson.ts
|
|
348
348
|
var MAX_ASK_TRIES = 5;
|
|
349
349
|
var MAX_ASK_OPTIONS = 4;
|
|
350
350
|
function personLine(person) {
|
|
351
|
-
const roles = person.roles.length > 0 ?
|
|
352
|
-
return `${person.id}: ${person.name} (${person.relation}, ${person.access} access${roles}
|
|
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
353
|
}
|
|
354
354
|
function findPerson(people, to) {
|
|
355
355
|
const key = String(to).trim().toLowerCase();
|
|
@@ -402,21 +402,21 @@ function shorten(text2) {
|
|
|
402
402
|
const line = text2.replace(/\s+/g, " ").trim();
|
|
403
403
|
return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
|
|
404
404
|
}
|
|
405
|
-
function activityLine(
|
|
405
|
+
function activityLine(tool4, input) {
|
|
406
406
|
const fields = typeof input === "object" && input !== null ? input : {};
|
|
407
|
-
if (
|
|
407
|
+
if (tool4 === "Bash" && typeof fields.command === "string") {
|
|
408
408
|
return `$ ${shorten(fields.command)}`;
|
|
409
409
|
}
|
|
410
|
-
if (FILE_TOOLS.has(
|
|
411
|
-
return `${
|
|
410
|
+
if (FILE_TOOLS.has(tool4) && typeof fields.file_path === "string") {
|
|
411
|
+
return `${tool4} ${shorten(fields.file_path)}`;
|
|
412
412
|
}
|
|
413
|
-
if (
|
|
413
|
+
if (tool4 === "Skill" && typeof fields.skill === "string") {
|
|
414
414
|
return `Skill ${shorten(fields.skill)}`;
|
|
415
415
|
}
|
|
416
|
-
if (HELPER_TOOLS.has(
|
|
416
|
+
if (HELPER_TOOLS.has(tool4) && typeof fields.subagent_type === "string") {
|
|
417
417
|
return `Agent ${shorten(fields.subagent_type)}`;
|
|
418
418
|
}
|
|
419
|
-
return
|
|
419
|
+
return tool4;
|
|
420
420
|
}
|
|
421
421
|
function usageOf(modelUsage) {
|
|
422
422
|
const entries = Object.values(modelUsage);
|
|
@@ -459,29 +459,29 @@ function textField(fields, name) {
|
|
|
459
459
|
return typeof value === "string" && value.trim() ? value : null;
|
|
460
460
|
}
|
|
461
461
|
function toolDraft(block, parent) {
|
|
462
|
-
const
|
|
462
|
+
const tool4 = (block.name ?? "tool").replace(MCP_PREFIX, "");
|
|
463
463
|
const fields = fieldsOf(block.input);
|
|
464
|
-
const base = { tool:
|
|
465
|
-
if (
|
|
464
|
+
const base = { tool: tool4, ref: block.id ?? null, parent, failed: false };
|
|
465
|
+
if (tool4 === "Skill" && textField(fields, "skill")) {
|
|
466
466
|
return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
|
|
467
467
|
}
|
|
468
|
-
if (HELPER_TOOLS2.has(
|
|
468
|
+
if (HELPER_TOOLS2.has(tool4)) {
|
|
469
469
|
const who = textField(fields, "subagent_type") ?? "general-purpose";
|
|
470
470
|
const what = textField(fields, "description");
|
|
471
471
|
return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
|
|
472
472
|
}
|
|
473
|
-
if (
|
|
473
|
+
if (tool4 === "submit_verdict") {
|
|
474
474
|
const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
|
|
475
475
|
return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
476
476
|
}
|
|
477
|
-
if (
|
|
477
|
+
if (tool4 === "hand_off") {
|
|
478
478
|
return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
479
479
|
}
|
|
480
|
-
if (
|
|
481
|
-
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) };
|
|
482
482
|
}
|
|
483
483
|
const command = textField(fields, "command");
|
|
484
|
-
const summary =
|
|
484
|
+
const summary = tool4 === "Bash" && command ? `$ ${command}` : activityLine(tool4, block.input);
|
|
485
485
|
return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
|
|
486
486
|
}
|
|
487
487
|
function resultText(content) {
|
|
@@ -653,8 +653,8 @@ function inputChannel(options = {}) {
|
|
|
653
653
|
}
|
|
654
654
|
const next = queue.shift();
|
|
655
655
|
if (!next) {
|
|
656
|
-
await new Promise((
|
|
657
|
-
wake =
|
|
656
|
+
await new Promise((resolve2) => {
|
|
657
|
+
wake = resolve2;
|
|
658
658
|
});
|
|
659
659
|
continue;
|
|
660
660
|
}
|
|
@@ -714,9 +714,9 @@ function inputChannel(options = {}) {
|
|
|
714
714
|
onSwitch(apply) {
|
|
715
715
|
applySwitch2 = apply;
|
|
716
716
|
if (waitingSwitch) {
|
|
717
|
-
const { change, resolve } = waitingSwitch;
|
|
717
|
+
const { change, resolve: resolve2 } = waitingSwitch;
|
|
718
718
|
waitingSwitch = null;
|
|
719
|
-
void apply(change).then(() =>
|
|
719
|
+
void apply(change).then(() => resolve2(true), () => resolve2(false));
|
|
720
720
|
}
|
|
721
721
|
},
|
|
722
722
|
close,
|
|
@@ -763,13 +763,211 @@ function inputChannel(options = {}) {
|
|
|
763
763
|
const earlier = waitingSwitch;
|
|
764
764
|
earlier?.resolve(false);
|
|
765
765
|
const merged = { model: change.model ?? earlier?.change.model ?? null, effort: change.effort ?? earlier?.change.effort ?? null };
|
|
766
|
-
return new Promise((
|
|
767
|
-
waitingSwitch = { change: merged, resolve };
|
|
766
|
+
return new Promise((resolve2) => {
|
|
767
|
+
waitingSwitch = { change: merged, resolve: resolve2 };
|
|
768
768
|
});
|
|
769
769
|
}
|
|
770
770
|
};
|
|
771
771
|
}
|
|
772
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
|
+
|
|
773
971
|
// src/specText.ts
|
|
774
972
|
function isRecord(value) {
|
|
775
973
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -850,6 +1048,206 @@ function specPromptText(spec) {
|
|
|
850
1048
|
].filter(Boolean).join("\n\n");
|
|
851
1049
|
}
|
|
852
1050
|
|
|
1051
|
+
// src/workspace.ts
|
|
1052
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1053
|
+
import { rm as rm2 } from "fs/promises";
|
|
1054
|
+
import { join as join2, resolve } from "path";
|
|
1055
|
+
|
|
1056
|
+
// src/git.ts
|
|
1057
|
+
import { Buffer } from "buffer";
|
|
1058
|
+
import { spawn as spawn2 } from "child_process";
|
|
1059
|
+
var GITHUB_ORIGIN = "https://github.com/";
|
|
1060
|
+
function githubAuthEnv(token) {
|
|
1061
|
+
const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
1062
|
+
return {
|
|
1063
|
+
GIT_CONFIG_COUNT: "1",
|
|
1064
|
+
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
1065
|
+
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
1066
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
function runGit(args, options) {
|
|
1070
|
+
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
1071
|
+
return new Promise((resolve2, reject) => {
|
|
1072
|
+
const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1073
|
+
let stdout = "";
|
|
1074
|
+
let stderr = "";
|
|
1075
|
+
child.stdout.on("data", (chunk) => {
|
|
1076
|
+
stdout += chunk.toString();
|
|
1077
|
+
});
|
|
1078
|
+
child.stderr.on("data", (chunk) => {
|
|
1079
|
+
stderr += chunk.toString();
|
|
1080
|
+
});
|
|
1081
|
+
child.on("error", reject);
|
|
1082
|
+
child.on("close", (code) => resolve2({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
async function git(args, options) {
|
|
1086
|
+
const result = await runGit(args, options);
|
|
1087
|
+
if (result.code !== 0) {
|
|
1088
|
+
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
1089
|
+
}
|
|
1090
|
+
return result.stdout;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// src/lock.ts
|
|
1094
|
+
function keyedLock() {
|
|
1095
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1096
|
+
return {
|
|
1097
|
+
async run(key, work2) {
|
|
1098
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
1099
|
+
let release = () => {
|
|
1100
|
+
};
|
|
1101
|
+
const done2 = new Promise((resolve2) => {
|
|
1102
|
+
release = resolve2;
|
|
1103
|
+
});
|
|
1104
|
+
const tail = previous.then(() => done2);
|
|
1105
|
+
tails.set(key, tail);
|
|
1106
|
+
await previous;
|
|
1107
|
+
try {
|
|
1108
|
+
return await work2();
|
|
1109
|
+
} finally {
|
|
1110
|
+
release();
|
|
1111
|
+
if (tails.get(key) === tail) {
|
|
1112
|
+
tails.delete(key);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// src/workspace.ts
|
|
1120
|
+
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
1121
|
+
var cacheLock = keyedLock();
|
|
1122
|
+
function githubRemoteUrl(fullName) {
|
|
1123
|
+
return `https://github.com/${fullName}.git`;
|
|
1124
|
+
}
|
|
1125
|
+
function tamsenseFolder(home, ...parts) {
|
|
1126
|
+
return join2(home, ".tamsense", ...parts);
|
|
1127
|
+
}
|
|
1128
|
+
function folderNames(repositories) {
|
|
1129
|
+
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
1130
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1131
|
+
for (const repository of repositories) {
|
|
1132
|
+
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
1133
|
+
}
|
|
1134
|
+
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
1135
|
+
}
|
|
1136
|
+
async function refreshCache(cache, repository, token, tools) {
|
|
1137
|
+
if (existsSync2(join2(cache, ".git"))) {
|
|
1138
|
+
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
1142
|
+
}
|
|
1143
|
+
async function removeWorktree(cache, path) {
|
|
1144
|
+
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
1145
|
+
await rm2(path, { recursive: true, force: true });
|
|
1146
|
+
await git(["worktree", "prune"], { cwd: cache });
|
|
1147
|
+
}
|
|
1148
|
+
function stepFolder(job) {
|
|
1149
|
+
const task = `task-${job.taskId}`;
|
|
1150
|
+
return typeof job.stage.columnId === "number" ? [task, `step-${job.stage.columnId}`] : [task];
|
|
1151
|
+
}
|
|
1152
|
+
function isReadOnlyCheck(job) {
|
|
1153
|
+
return job.stage.kind === "review" || job.stage.kind === "test";
|
|
1154
|
+
}
|
|
1155
|
+
function branchHolders(porcelain, branch) {
|
|
1156
|
+
const holders = [];
|
|
1157
|
+
let path = null;
|
|
1158
|
+
for (const line of porcelain.split("\n").map((text2) => text2.trim())) {
|
|
1159
|
+
if (line.startsWith("worktree ")) {
|
|
1160
|
+
path = line.slice("worktree ".length);
|
|
1161
|
+
} else if (path && line === `branch refs/heads/${branch}`) {
|
|
1162
|
+
holders.push(path);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
return holders;
|
|
1166
|
+
}
|
|
1167
|
+
async function releaseBranch(cache, branch, keep) {
|
|
1168
|
+
const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
|
|
1169
|
+
for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve(holder) !== resolve(keep))) {
|
|
1170
|
+
await removeWorktree(cache, path);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
async function baseRef(cache, job, repository) {
|
|
1174
|
+
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
1175
|
+
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
1176
|
+
}
|
|
1177
|
+
async function keptBase(cache, path, job, repository) {
|
|
1178
|
+
if (!job.keepWorkspace || !existsSync2(join2(path, ".git"))) {
|
|
1179
|
+
return null;
|
|
1180
|
+
}
|
|
1181
|
+
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
1182
|
+
if (branch.code !== 0 || branch.stdout !== (isReadOnlyCheck(job) ? "HEAD" : job.branch)) {
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
1186
|
+
}
|
|
1187
|
+
async function prepareWorkspace(job, token, tools) {
|
|
1188
|
+
const root = tamsenseFolder(tools.home, "work", ...stepFolder(job));
|
|
1189
|
+
const folders = folderNames(job.repositories);
|
|
1190
|
+
const repositories = [];
|
|
1191
|
+
for (const [index, repository] of job.repositories.entries()) {
|
|
1192
|
+
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
1193
|
+
const folder = folders[index] ?? repository.fullName;
|
|
1194
|
+
const path = join2(root, folder);
|
|
1195
|
+
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
1196
|
+
await refreshCache(cache, repository, token, tools);
|
|
1197
|
+
const kept = await keptBase(cache, path, job, repository);
|
|
1198
|
+
if (kept) {
|
|
1199
|
+
return kept;
|
|
1200
|
+
}
|
|
1201
|
+
await removeWorktree(cache, path);
|
|
1202
|
+
const base = await baseRef(cache, job, repository);
|
|
1203
|
+
if (isReadOnlyCheck(job)) {
|
|
1204
|
+
await git(["worktree", "add", "--force", "--detach", path, base], { cwd: cache });
|
|
1205
|
+
} else {
|
|
1206
|
+
await releaseBranch(cache, job.branch, path);
|
|
1207
|
+
await git(["worktree", "add", "--force", "-B", job.branch, path, base], { cwd: cache });
|
|
1208
|
+
}
|
|
1209
|
+
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
1210
|
+
});
|
|
1211
|
+
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
1212
|
+
}
|
|
1213
|
+
return { root, repositories };
|
|
1214
|
+
}
|
|
1215
|
+
async function commitIdentityEnv(home) {
|
|
1216
|
+
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
1217
|
+
if (configured.code === 0 && configured.stdout) {
|
|
1218
|
+
return {};
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
1222
|
+
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
1223
|
+
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
1224
|
+
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
async function publishWorkspace(workspace, job, token, identity) {
|
|
1228
|
+
const pushed = [];
|
|
1229
|
+
for (const repository of workspace.repositories) {
|
|
1230
|
+
const options = { cwd: repository.path, env: identity };
|
|
1231
|
+
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
1232
|
+
await git(["add", "--all"], options);
|
|
1233
|
+
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
1234
|
+
if (hasStaged) {
|
|
1235
|
+
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
1236
|
+
}
|
|
1237
|
+
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
1238
|
+
if (newCommits === 0) {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
1242
|
+
return git(["rev-parse", "HEAD"], options);
|
|
1243
|
+
});
|
|
1244
|
+
if (sha) {
|
|
1245
|
+
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
return pushed;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
853
1251
|
// src/prompt.ts
|
|
854
1252
|
var VERDICT_TOOL_NAME = "submit_verdict";
|
|
855
1253
|
var SCREENSHOT_TOOL_NAME = "attach_screenshot";
|
|
@@ -860,8 +1258,9 @@ var SUBMIT_QUESTIONS_TOOL_NAME = "submit_questions";
|
|
|
860
1258
|
var SUBMIT_SPEC_TOOL_NAME = "submit_spec";
|
|
861
1259
|
var ASK_PERSON_TOOL_NAME = "ask_person";
|
|
862
1260
|
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.`;
|
|
863
|
-
var UNATTENDED_RULE = `${MESSAGES_RULE} Nobody answers questions
|
|
1261
|
+
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.`;
|
|
864
1262
|
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.`;
|
|
1263
|
+
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.";
|
|
865
1264
|
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.";
|
|
866
1265
|
var CODE_RULES = [
|
|
867
1266
|
"Make the changes the task asks for, and keep them focused on it.",
|
|
@@ -882,8 +1281,11 @@ var NAMING_RULE = 'Name everything you attach (screenshots, replays, videos) by
|
|
|
882
1281
|
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.`;
|
|
883
1282
|
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.";
|
|
884
1283
|
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.";
|
|
885
|
-
function
|
|
886
|
-
|
|
1284
|
+
function composeProjectName(job) {
|
|
1285
|
+
return ["tamsense", ...stepFolder(job)].join("-");
|
|
1286
|
+
}
|
|
1287
|
+
function localAppRule(job, isCheck, canAsk) {
|
|
1288
|
+
const project = composeProjectName(job);
|
|
887
1289
|
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.";
|
|
888
1290
|
return [
|
|
889
1291
|
"When you need the running app, start a local copy from this task folder only.",
|
|
@@ -927,6 +1329,9 @@ function contextText(context) {
|
|
|
927
1329
|
return context.trim() ? `Project context from the team (follow it like the project's own rules):
|
|
928
1330
|
${context.trim()}` : null;
|
|
929
1331
|
}
|
|
1332
|
+
function safetyText(job) {
|
|
1333
|
+
return job.safetyRules?.trim() || null;
|
|
1334
|
+
}
|
|
930
1335
|
function bulletList(lines) {
|
|
931
1336
|
return lines.map((line) => `- ${line}`).join("\n");
|
|
932
1337
|
}
|
|
@@ -935,7 +1340,7 @@ function repositoryLines(job, workspace) {
|
|
|
935
1340
|
return workspace.repositories.map((repository) => {
|
|
936
1341
|
const source = sourceOf.get(repository.fullName);
|
|
937
1342
|
const pull = source?.pullRequestUrl ? `, pull request ${source.pullRequestUrl}` : "";
|
|
938
|
-
return `${repository.folder} (${repository.fullName}), base branch origin/${source?.defaultBranch ?? "main"}${pull}`;
|
|
1343
|
+
return `${repository.folder} (${repository.fullName}) at commit ${repository.baseSha.slice(0, 7)}, base branch origin/${source?.defaultBranch ?? "main"}${pull}`;
|
|
939
1344
|
});
|
|
940
1345
|
}
|
|
941
1346
|
function stepLine(stage) {
|
|
@@ -950,11 +1355,14 @@ function peopleText(people) {
|
|
|
950
1355
|
return null;
|
|
951
1356
|
}
|
|
952
1357
|
return [
|
|
953
|
-
`People on this task you can ask with ${ASK_PERSON_TOOL_NAME} (id: name, relation, access):`,
|
|
1358
|
+
`People on this task you can ask with ${ASK_PERSON_TOOL_NAME} (id: name, relation, access, then their team roles with what each owns):`,
|
|
954
1359
|
bulletList(people.map(personLine)),
|
|
955
|
-
|
|
1360
|
+
'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.'
|
|
956
1361
|
].join("\n");
|
|
957
1362
|
}
|
|
1363
|
+
function canProposeKnowledge(job) {
|
|
1364
|
+
return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
|
|
1365
|
+
}
|
|
958
1366
|
function handoffText(targets, isCheck) {
|
|
959
1367
|
if (targets.length === 0) {
|
|
960
1368
|
return null;
|
|
@@ -1017,6 +1425,7 @@ ${JSON.stringify(job.spec)}` : null,
|
|
|
1017
1425
|
const instructions = [
|
|
1018
1426
|
SPEC_UNATTENDED_RULE,
|
|
1019
1427
|
MACHINE_RULE,
|
|
1428
|
+
workplaceText(job.computer, workspace.root),
|
|
1020
1429
|
stepLine(job.stage),
|
|
1021
1430
|
job.stage.instructions,
|
|
1022
1431
|
contextText(job.projectContext),
|
|
@@ -1025,6 +1434,8 @@ ${bulletList(repositoryLines(job, workspace))}`,
|
|
|
1025
1434
|
READ_ONLY_RULE,
|
|
1026
1435
|
`How to work:
|
|
1027
1436
|
${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
1437
|
+
safetyText(job),
|
|
1438
|
+
knowledgeText(job.knowledge, job.computer, false),
|
|
1028
1439
|
SPEC_STYLE_RULE
|
|
1029
1440
|
].filter(Boolean).join("\n\n");
|
|
1030
1441
|
return { prompt, instructions };
|
|
@@ -1050,18 +1461,22 @@ ${job.review}`,
|
|
|
1050
1461
|
teamMessagesText(job.messages)
|
|
1051
1462
|
].filter(Boolean).join("\n\n");
|
|
1052
1463
|
const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
|
|
1053
|
-
const localApp = localAppRule(job
|
|
1464
|
+
const localApp = localAppRule(job, isCheck, canAsk);
|
|
1054
1465
|
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
1055
1466
|
const instructions = [
|
|
1056
1467
|
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
1057
1468
|
MACHINE_RULE,
|
|
1469
|
+
workplaceText(job.computer, workspace.root),
|
|
1058
1470
|
stepLine(job.stage),
|
|
1059
1471
|
job.stage.instructions,
|
|
1060
1472
|
contextText(job.projectContext),
|
|
1061
|
-
`The repositories for this task are checked out in the current folder, one subfolder each, on branch ${job.branch}:
|
|
1473
|
+
`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}:
|
|
1062
1474
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
1063
1475
|
`How to work:
|
|
1064
1476
|
${bulletList(rules)}`,
|
|
1477
|
+
safetyText(job),
|
|
1478
|
+
knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
|
|
1479
|
+
APPROVAL_RULE,
|
|
1065
1480
|
peopleText(people),
|
|
1066
1481
|
handoffText(job.allowedHandoffs, isCheck),
|
|
1067
1482
|
options.browser && BROWSER_RULE
|
|
@@ -1070,98 +1485,98 @@ ${bulletList(rules)}`,
|
|
|
1070
1485
|
}
|
|
1071
1486
|
|
|
1072
1487
|
// src/specTools.ts
|
|
1073
|
-
import { tool } from "@anthropic-ai/claude-agent-sdk";
|
|
1074
|
-
import { z } from "zod";
|
|
1488
|
+
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1489
|
+
import { z as z2 } from "zod";
|
|
1075
1490
|
var MAX_FIRST_ROUND = 4;
|
|
1076
|
-
var questionSchema =
|
|
1077
|
-
id:
|
|
1078
|
-
question:
|
|
1079
|
-
whyItMatters:
|
|
1080
|
-
options:
|
|
1081
|
-
label:
|
|
1082
|
-
description:
|
|
1083
|
-
recommended:
|
|
1491
|
+
var questionSchema = z2.object({
|
|
1492
|
+
id: z2.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
|
|
1493
|
+
question: z2.string().describe("One plain sentence the owner can answer without reading code"),
|
|
1494
|
+
whyItMatters: z2.string().describe("One line: what changes in the result depending on the answer"),
|
|
1495
|
+
options: z2.array(z2.object({
|
|
1496
|
+
label: z2.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
|
|
1497
|
+
description: z2.string().describe("One sentence: what happens if this option is chosen"),
|
|
1498
|
+
recommended: z2.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
|
|
1084
1499
|
})).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'),
|
|
1085
|
-
multiSelect:
|
|
1500
|
+
multiSelect: z2.boolean().describe("true only when the options are additive and several can apply together")
|
|
1086
1501
|
});
|
|
1087
|
-
var briefSchema =
|
|
1088
|
-
findings:
|
|
1089
|
-
fact:
|
|
1090
|
-
where:
|
|
1502
|
+
var briefSchema = z2.object({
|
|
1503
|
+
findings: z2.array(z2.object({
|
|
1504
|
+
fact: z2.string().describe("What the code does today that matters for this task"),
|
|
1505
|
+
where: z2.string().describe("Where it is: owner/repo path:line")
|
|
1091
1506
|
})).describe("Findings from the code"),
|
|
1092
|
-
dataFacts:
|
|
1093
|
-
approach:
|
|
1094
|
-
codeMap:
|
|
1095
|
-
repo:
|
|
1096
|
-
file:
|
|
1097
|
-
role:
|
|
1507
|
+
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"),
|
|
1508
|
+
approach: z2.string().describe("The approach you would take, in 2-4 sentences"),
|
|
1509
|
+
codeMap: z2.array(z2.object({
|
|
1510
|
+
repo: z2.string().describe("Repository full name (owner/name)"),
|
|
1511
|
+
file: z2.string().describe("Path relative to the repository root"),
|
|
1512
|
+
role: z2.string().describe("Why it matters: what to change or reuse there")
|
|
1098
1513
|
})).describe("The files the change will touch or must reuse"),
|
|
1099
|
-
assumptions:
|
|
1100
|
-
text:
|
|
1101
|
-
reason:
|
|
1514
|
+
assumptions: z2.array(z2.object({
|
|
1515
|
+
text: z2.string().describe("The default you chose"),
|
|
1516
|
+
reason: z2.string().describe("Why it is safe or why it was chosen")
|
|
1102
1517
|
})).describe("Corners settled with a default instead of a question"),
|
|
1103
|
-
pendingDecisions:
|
|
1518
|
+
pendingDecisions: z2.array(z2.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
|
|
1104
1519
|
});
|
|
1105
|
-
var questionsPayloadSchema =
|
|
1520
|
+
var questionsPayloadSchema = z2.object({
|
|
1106
1521
|
brief: briefSchema,
|
|
1107
|
-
questions:
|
|
1522
|
+
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`)
|
|
1108
1523
|
});
|
|
1109
|
-
var specSchema =
|
|
1110
|
-
goal:
|
|
1111
|
-
context:
|
|
1112
|
-
inScope:
|
|
1113
|
-
outOfScope:
|
|
1114
|
-
decisions:
|
|
1115
|
-
question:
|
|
1116
|
-
answer:
|
|
1117
|
-
by:
|
|
1524
|
+
var specSchema = z2.object({
|
|
1525
|
+
goal: z2.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
|
|
1526
|
+
context: z2.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
|
|
1527
|
+
inScope: z2.array(z2.string()).describe("What this task delivers, one item each"),
|
|
1528
|
+
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"),
|
|
1529
|
+
decisions: z2.array(z2.object({
|
|
1530
|
+
question: z2.string(),
|
|
1531
|
+
answer: z2.string(),
|
|
1532
|
+
by: z2.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
1118
1533
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
1119
|
-
codeMap:
|
|
1120
|
-
repo:
|
|
1121
|
-
file:
|
|
1122
|
-
role:
|
|
1123
|
-
use:
|
|
1534
|
+
codeMap: z2.array(z2.object({
|
|
1535
|
+
repo: z2.string().describe("Repository full name (owner/name)"),
|
|
1536
|
+
file: z2.string().describe("Path relative to the repository root"),
|
|
1537
|
+
role: z2.string().describe("What the coder needs to know about this file"),
|
|
1538
|
+
use: z2.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
|
|
1124
1539
|
})).describe("The files a coder must read before starting"),
|
|
1125
|
-
changes:
|
|
1126
|
-
repo:
|
|
1127
|
-
file:
|
|
1128
|
-
action:
|
|
1129
|
-
description:
|
|
1130
|
-
produces:
|
|
1131
|
-
consumes:
|
|
1132
|
-
acceptance:
|
|
1540
|
+
changes: z2.array(z2.object({
|
|
1541
|
+
repo: z2.string().describe("Repository full name (owner/name) where the change happens"),
|
|
1542
|
+
file: z2.string().describe("Path to the file, relative to the repository root"),
|
|
1543
|
+
action: z2.enum(["create", "modify", "delete"]),
|
|
1544
|
+
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"),
|
|
1545
|
+
produces: z2.array(z2.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
|
|
1546
|
+
consumes: z2.array(z2.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
|
|
1547
|
+
acceptance: z2.array(z2.string()).describe("Ids of the criteria this change makes true, like AC-1")
|
|
1133
1548
|
})).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
|
|
1134
|
-
acceptance:
|
|
1135
|
-
id:
|
|
1136
|
-
kind:
|
|
1137
|
-
text:
|
|
1549
|
+
acceptance: z2.array(z2.object({
|
|
1550
|
+
id: z2.string().describe("AC-1, AC-2 and so on, in order"),
|
|
1551
|
+
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"),
|
|
1552
|
+
text: z2.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
|
|
1138
1553
|
})).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
|
|
1139
|
-
testPlan:
|
|
1140
|
-
automated:
|
|
1141
|
-
repo:
|
|
1142
|
-
command:
|
|
1143
|
-
expect:
|
|
1554
|
+
testPlan: z2.object({
|
|
1555
|
+
automated: z2.array(z2.object({
|
|
1556
|
+
repo: z2.string(),
|
|
1557
|
+
command: z2.string().describe("The exact command to run"),
|
|
1558
|
+
expect: z2.string().describe("What passing looks like")
|
|
1144
1559
|
})).describe("At least one command"),
|
|
1145
|
-
newTests:
|
|
1146
|
-
seam:
|
|
1147
|
-
covers:
|
|
1560
|
+
newTests: z2.array(z2.object({
|
|
1561
|
+
seam: z2.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
|
|
1562
|
+
covers: z2.array(z2.string()).describe("Ids of the criteria it covers")
|
|
1148
1563
|
})),
|
|
1149
|
-
manual:
|
|
1150
|
-
page:
|
|
1151
|
-
steps:
|
|
1564
|
+
manual: z2.array(z2.object({
|
|
1565
|
+
page: z2.string().describe("The page or screen"),
|
|
1566
|
+
steps: z2.string().describe("What a person does and sees")
|
|
1152
1567
|
}))
|
|
1153
1568
|
}),
|
|
1154
|
-
risks:
|
|
1155
|
-
risk:
|
|
1156
|
-
mitigation:
|
|
1569
|
+
risks: z2.array(z2.object({
|
|
1570
|
+
risk: z2.string(),
|
|
1571
|
+
mitigation: z2.string()
|
|
1157
1572
|
})).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
|
|
1158
|
-
riskLevel:
|
|
1159
|
-
assumptions:
|
|
1160
|
-
text:
|
|
1161
|
-
reason:
|
|
1573
|
+
riskLevel: z2.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
|
|
1574
|
+
assumptions: z2.array(z2.object({
|
|
1575
|
+
text: z2.string().describe("The default you chose"),
|
|
1576
|
+
reason: z2.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
1162
1577
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
1163
|
-
openQuestions:
|
|
1164
|
-
split:
|
|
1578
|
+
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"),
|
|
1579
|
+
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")
|
|
1165
1580
|
});
|
|
1166
1581
|
function reply(text2, isError = false) {
|
|
1167
1582
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
@@ -1171,7 +1586,7 @@ function unreachable(error, toolName) {
|
|
|
1171
1586
|
return reply(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
|
|
1172
1587
|
}
|
|
1173
1588
|
function questionsTool(review, onSubmit) {
|
|
1174
|
-
return
|
|
1589
|
+
return tool2(
|
|
1175
1590
|
SUBMIT_QUESTIONS_TOOL_NAME,
|
|
1176
1591
|
"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.",
|
|
1177
1592
|
questionsPayloadSchema.shape,
|
|
@@ -1194,7 +1609,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
|
|
|
1194
1609
|
function specTool(review, onSubmit, builderCheckRan) {
|
|
1195
1610
|
let repairs = 0;
|
|
1196
1611
|
let reminded = false;
|
|
1197
|
-
return
|
|
1612
|
+
return tool2(
|
|
1198
1613
|
SUBMIT_SPEC_TOOL_NAME,
|
|
1199
1614
|
"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.",
|
|
1200
1615
|
specSchema.shape,
|
|
@@ -1314,14 +1729,12 @@ function usageLimitWatch() {
|
|
|
1314
1729
|
}
|
|
1315
1730
|
|
|
1316
1731
|
// src/claude.ts
|
|
1317
|
-
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.";
|
|
1318
1732
|
var NO_RESULT_TEXT = "Claude finished without a result.";
|
|
1319
1733
|
var SERVER_NAME = "tamsense";
|
|
1320
1734
|
var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
|
|
1321
|
-
var denyWhenAsked = async () => ({ behavior: "deny", message: NOBODY_TO_ASK });
|
|
1322
1735
|
var CAPTION_FIELDS = {
|
|
1323
|
-
title:
|
|
1324
|
-
purpose:
|
|
1736
|
+
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"'),
|
|
1737
|
+
purpose: z3.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
|
|
1325
1738
|
};
|
|
1326
1739
|
var ATTACH_TOOLS = {
|
|
1327
1740
|
screenshot: {
|
|
@@ -1342,21 +1755,21 @@ var ATTACH_TOOLS = {
|
|
|
1342
1755
|
};
|
|
1343
1756
|
function attachTool(kind, attach) {
|
|
1344
1757
|
const spec = ATTACH_TOOLS[kind];
|
|
1345
|
-
return
|
|
1758
|
+
return tool3(
|
|
1346
1759
|
spec.name,
|
|
1347
1760
|
spec.description,
|
|
1348
|
-
{ path:
|
|
1761
|
+
{ path: z3.string().describe(spec.path), ...CAPTION_FIELDS },
|
|
1349
1762
|
async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
|
|
1350
1763
|
);
|
|
1351
1764
|
}
|
|
1352
1765
|
function verdictTool(onVerdict) {
|
|
1353
|
-
return
|
|
1766
|
+
return tool3(
|
|
1354
1767
|
VERDICT_TOOL_NAME,
|
|
1355
1768
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
1356
1769
|
{
|
|
1357
|
-
passed:
|
|
1358
|
-
report:
|
|
1359
|
-
couldNotRun:
|
|
1770
|
+
passed: z3.boolean().describe("true only if nothing must be fixed"),
|
|
1771
|
+
report: z3.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
|
|
1772
|
+
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")
|
|
1360
1773
|
},
|
|
1361
1774
|
async (args) => {
|
|
1362
1775
|
onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
|
|
@@ -1365,13 +1778,13 @@ function verdictTool(onVerdict) {
|
|
|
1365
1778
|
);
|
|
1366
1779
|
}
|
|
1367
1780
|
function handoffTool(targets, onHandoff) {
|
|
1368
|
-
return
|
|
1781
|
+
return tool3(
|
|
1369
1782
|
HANDOFF_TOOL_NAME,
|
|
1370
1783
|
"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.",
|
|
1371
1784
|
{
|
|
1372
|
-
to:
|
|
1373
|
-
outcome:
|
|
1374
|
-
note:
|
|
1785
|
+
to: z3.string().describe("name or id of the step to hand the task to"),
|
|
1786
|
+
outcome: z3.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
|
|
1787
|
+
note: z3.string().describe("for the next agent and the owner: what was done, what to check, what is open")
|
|
1375
1788
|
},
|
|
1376
1789
|
async (args) => {
|
|
1377
1790
|
const choice = chooseHandoff(targets, args);
|
|
@@ -1384,25 +1797,25 @@ function handoffTool(targets, onHandoff) {
|
|
|
1384
1797
|
);
|
|
1385
1798
|
}
|
|
1386
1799
|
function askPersonTool(asking, waiting) {
|
|
1387
|
-
return
|
|
1800
|
+
return tool3(
|
|
1388
1801
|
ASK_PERSON_TOOL_NAME,
|
|
1389
1802
|
"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.",
|
|
1390
1803
|
{
|
|
1391
|
-
to:
|
|
1392
|
-
why:
|
|
1393
|
-
question:
|
|
1394
|
-
context:
|
|
1395
|
-
tried:
|
|
1396
|
-
did:
|
|
1397
|
-
error:
|
|
1398
|
-
result:
|
|
1804
|
+
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"),
|
|
1805
|
+
why: z3.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
|
|
1806
|
+
question: z3.string().describe("one plain question the person can answer without reading the code"),
|
|
1807
|
+
context: z3.string().optional().describe("one line on what you saw that led to the question"),
|
|
1808
|
+
tried: z3.array(z3.object({
|
|
1809
|
+
did: z3.string().describe("the command or step"),
|
|
1810
|
+
error: z3.string().optional().describe("the exact error line, if there was one"),
|
|
1811
|
+
result: z3.string().describe("what it showed, one line")
|
|
1399
1812
|
})).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
|
|
1400
|
-
options:
|
|
1401
|
-
label:
|
|
1402
|
-
detail:
|
|
1813
|
+
options: z3.array(z3.object({
|
|
1814
|
+
label: z3.string().describe("a short answer the person can pick"),
|
|
1815
|
+
detail: z3.string().describe("one line on what happens if it is picked")
|
|
1403
1816
|
})).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
|
|
1404
|
-
blocking:
|
|
1405
|
-
assumption:
|
|
1817
|
+
blocking: z3.boolean().describe("true when you cannot go on without the answer"),
|
|
1818
|
+
assumption: z3.string().optional().describe("when blocking is false: what you continue with until the answer comes")
|
|
1406
1819
|
},
|
|
1407
1820
|
async (args) => {
|
|
1408
1821
|
if (waiting.waitingFor !== null) {
|
|
@@ -1435,6 +1848,16 @@ function specWorkTools(specWork, submitted) {
|
|
|
1435
1848
|
submitted.spec = spec;
|
|
1436
1849
|
}, () => submitted.helperStarted)];
|
|
1437
1850
|
}
|
|
1851
|
+
function knowledgeTools(knowledge) {
|
|
1852
|
+
if (!knowledge) {
|
|
1853
|
+
return [];
|
|
1854
|
+
}
|
|
1855
|
+
return [
|
|
1856
|
+
...knowledge.entryIds.length > 0 ? [updateKnowledgeTool(knowledge)] : [],
|
|
1857
|
+
...knowledge.moreIds.length > 0 ? [readKnowledgeTool(knowledge)] : [],
|
|
1858
|
+
...knowledge.canPropose ? [proposeKnowledgeTool(knowledge)] : []
|
|
1859
|
+
];
|
|
1860
|
+
}
|
|
1438
1861
|
function tamsenseServer(request, submitted) {
|
|
1439
1862
|
const tools = [
|
|
1440
1863
|
attachTool("screenshot", request.attach),
|
|
@@ -1445,6 +1868,7 @@ function tamsenseServer(request, submitted) {
|
|
|
1445
1868
|
})] : [],
|
|
1446
1869
|
...specWorkTools(request.specWork, submitted),
|
|
1447
1870
|
...request.asking ? [askPersonTool(request.asking, submitted)] : [],
|
|
1871
|
+
...knowledgeTools(request.knowledge),
|
|
1448
1872
|
...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
|
|
1449
1873
|
submitted.handoff = handoff;
|
|
1450
1874
|
})] : []
|
|
@@ -1483,7 +1907,8 @@ async function attempt(request, context, resume) {
|
|
|
1483
1907
|
permissionMode: "auto",
|
|
1484
1908
|
// краткий пересказ размышлений Claude идёт в журнал задачи, как в Paseo
|
|
1485
1909
|
thinking: { type: "adaptive", display: "summarized" },
|
|
1486
|
-
|
|
1910
|
+
// режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
|
|
1911
|
+
canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
|
|
1487
1912
|
plugins: [{ type: "local", path: LIBRARY_PATH }],
|
|
1488
1913
|
mcpServers: { [SERVER_NAME]: tamsense.server },
|
|
1489
1914
|
allowedTools: tamsense.allowed,
|
|
@@ -1512,6 +1937,7 @@ async function attempt(request, context, resume) {
|
|
|
1512
1937
|
request.onFeed(draft);
|
|
1513
1938
|
if (draft.kind === "text" && draft.parent === null) {
|
|
1514
1939
|
lastText = draft.text;
|
|
1940
|
+
context.lastWords.text = draft.text;
|
|
1515
1941
|
}
|
|
1516
1942
|
}
|
|
1517
1943
|
if (message.type === "result") {
|
|
@@ -1538,7 +1964,7 @@ function claudeRunner(startQuery) {
|
|
|
1538
1964
|
}, { once: true });
|
|
1539
1965
|
const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false, waitingFor: null };
|
|
1540
1966
|
const limit = usageLimitWatch();
|
|
1541
|
-
const context = { startQuery, abortController, submitted, limit };
|
|
1967
|
+
const context = { startQuery, abortController, submitted, limit, lastWords: { text: "" } };
|
|
1542
1968
|
try {
|
|
1543
1969
|
let result = await attempt(request, context, request.resumeSessionId);
|
|
1544
1970
|
if (result.resumeFailed) {
|
|
@@ -1555,13 +1981,13 @@ function claudeRunner(startQuery) {
|
|
|
1555
1981
|
var runClaude = claudeRunner(query2);
|
|
1556
1982
|
|
|
1557
1983
|
// src/config.ts
|
|
1558
|
-
import { mkdir as mkdir2, readFile as readFile2, rm as
|
|
1984
|
+
import { mkdir as mkdir2, readFile as readFile2, rm as rm3, writeFile as writeFile2 } from "fs/promises";
|
|
1559
1985
|
import { homedir as homedir2 } from "os";
|
|
1560
|
-
import { dirname as dirname2, join as
|
|
1986
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1561
1987
|
var OWNER_ONLY_FILE = 384;
|
|
1562
1988
|
var OWNER_ONLY_FOLDER = 448;
|
|
1563
1989
|
function configPath(home = homedir2()) {
|
|
1564
|
-
return
|
|
1990
|
+
return join3(home, ".tamsense", "runner.json");
|
|
1565
1991
|
}
|
|
1566
1992
|
function isRecord2(value) {
|
|
1567
1993
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1593,7 +2019,7 @@ async function writeConfig(config, path = configPath()) {
|
|
|
1593
2019
|
`, { mode: OWNER_ONLY_FILE });
|
|
1594
2020
|
}
|
|
1595
2021
|
async function removeConfig(path = configPath()) {
|
|
1596
|
-
await
|
|
2022
|
+
await rm3(path, { force: true });
|
|
1597
2023
|
}
|
|
1598
2024
|
|
|
1599
2025
|
// src/protocol.ts
|
|
@@ -1608,6 +2034,12 @@ var CAPABILITIES_PATH = "/api/runner/me/capabilities";
|
|
|
1608
2034
|
function runPath(runId, action) {
|
|
1609
2035
|
return `/api/runner/runs/${runId}/${action}`;
|
|
1610
2036
|
}
|
|
2037
|
+
function knowledgePath(runId, entryId) {
|
|
2038
|
+
return `/api/runner/runs/${runId}/knowledge/${entryId}`;
|
|
2039
|
+
}
|
|
2040
|
+
function permissionPath(runId, permissionId) {
|
|
2041
|
+
return `/api/runner/runs/${runId}/permissions/${permissionId}`;
|
|
2042
|
+
}
|
|
1611
2043
|
function recordingUploadPath(runId, upload, part) {
|
|
1612
2044
|
return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
|
|
1613
2045
|
}
|
|
@@ -1641,14 +2073,14 @@ async function login(tools, request) {
|
|
|
1641
2073
|
}
|
|
1642
2074
|
|
|
1643
2075
|
// src/recorderEnv.ts
|
|
1644
|
-
import { existsSync as
|
|
1645
|
-
import { join as
|
|
2076
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2077
|
+
import { join as join5 } from "path";
|
|
1646
2078
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1647
2079
|
|
|
1648
2080
|
// src/recorder.ts
|
|
1649
2081
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1650
2082
|
import Module, { createRequire } from "module";
|
|
1651
|
-
import { basename, dirname as dirname3, join as
|
|
2083
|
+
import { basename, dirname as dirname3, join as join4 } from "path";
|
|
1652
2084
|
import process2 from "process";
|
|
1653
2085
|
var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
|
|
1654
2086
|
var SERVER_ENV = "TAMSENSE_SERVER";
|
|
@@ -1729,7 +2161,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
|
1729
2161
|
}
|
|
1730
2162
|
function rrwebSource() {
|
|
1731
2163
|
const entry = createRequire(import.meta.url).resolve("rrweb");
|
|
1732
|
-
return readFileSync(
|
|
2164
|
+
return readFileSync(join4(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
|
|
1733
2165
|
}
|
|
1734
2166
|
function pageScript(rrweb) {
|
|
1735
2167
|
return `(() => {
|
|
@@ -1780,8 +2212,8 @@ var Session = class {
|
|
|
1780
2212
|
bytes = 0;
|
|
1781
2213
|
constructor(dir, recording, url) {
|
|
1782
2214
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
1783
|
-
this.file =
|
|
1784
|
-
this.metaFile =
|
|
2215
|
+
this.file = join4(dir, `${id}${REPLAY_SUFFIX}`);
|
|
2216
|
+
this.metaFile = join4(dir, `${id}${META_SUFFIX}`);
|
|
1785
2217
|
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 };
|
|
1786
2218
|
writeFileSync(this.file, "");
|
|
1787
2219
|
this.save();
|
|
@@ -1954,10 +2386,10 @@ if (process2.env[RECORDINGS_DIR_ENV]) {
|
|
|
1954
2386
|
var RECORDER_ENV = "TAMSENSE_RECORDER";
|
|
1955
2387
|
function bundledRecorder() {
|
|
1956
2388
|
const path = fileURLToPath2(new URL("./recorder.cjs", import.meta.url));
|
|
1957
|
-
return
|
|
2389
|
+
return existsSync3(path) ? path : null;
|
|
1958
2390
|
}
|
|
1959
2391
|
function recordingsFolder(home, runId) {
|
|
1960
|
-
return
|
|
2392
|
+
return join5(home, ".tamsense", "recordings", `run-${runId}`);
|
|
1961
2393
|
}
|
|
1962
2394
|
function nodeOptionsWithRecorder(recorder, nodeOptions) {
|
|
1963
2395
|
return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
|
|
@@ -1984,7 +2416,7 @@ function describeDevice(identity) {
|
|
|
1984
2416
|
}
|
|
1985
2417
|
|
|
1986
2418
|
// src/system.ts
|
|
1987
|
-
import { spawn as
|
|
2419
|
+
import { spawn as spawn3 } from "child_process";
|
|
1988
2420
|
import { hostname } from "os";
|
|
1989
2421
|
import { delimiter as delimiter2 } from "path";
|
|
1990
2422
|
import { setTimeout as wait } from "timers/promises";
|
|
@@ -2008,7 +2440,7 @@ function browserCommand(url, platform) {
|
|
|
2008
2440
|
}
|
|
2009
2441
|
function openBrowser(url, platform = process.platform) {
|
|
2010
2442
|
const [command, args] = browserCommand(url, platform);
|
|
2011
|
-
const child =
|
|
2443
|
+
const child = spawn3(command, args, { detached: true, stdio: "ignore" });
|
|
2012
2444
|
child.on("error", () => {
|
|
2013
2445
|
});
|
|
2014
2446
|
child.unref();
|
|
@@ -2028,7 +2460,7 @@ async function sleep(ms, signal) {
|
|
|
2028
2460
|
}
|
|
2029
2461
|
|
|
2030
2462
|
// src/update.ts
|
|
2031
|
-
import { spawn as
|
|
2463
|
+
import { spawn as spawn4 } from "child_process";
|
|
2032
2464
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2033
2465
|
import process3 from "process";
|
|
2034
2466
|
var PACKAGE_NAME = "@tamsensedev/runner";
|
|
@@ -2083,8 +2515,8 @@ function relaunchArgs(argv) {
|
|
|
2083
2515
|
}
|
|
2084
2516
|
function relaunch(argv, env) {
|
|
2085
2517
|
const isWindows = process3.platform === "win32";
|
|
2086
|
-
return new Promise((
|
|
2087
|
-
const child =
|
|
2518
|
+
return new Promise((resolve2, reject) => {
|
|
2519
|
+
const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
|
|
2088
2520
|
const ignoreInterrupt = () => {
|
|
2089
2521
|
};
|
|
2090
2522
|
process3.on("SIGINT", ignoreInterrupt);
|
|
@@ -2094,7 +2526,7 @@ function relaunch(argv, env) {
|
|
|
2094
2526
|
});
|
|
2095
2527
|
child.on("close", (code) => {
|
|
2096
2528
|
process3.off("SIGINT", ignoreInterrupt);
|
|
2097
|
-
|
|
2529
|
+
resolve2(code ?? 1);
|
|
2098
2530
|
});
|
|
2099
2531
|
});
|
|
2100
2532
|
}
|
|
@@ -2140,37 +2572,11 @@ function feedQueue(send) {
|
|
|
2140
2572
|
};
|
|
2141
2573
|
}
|
|
2142
2574
|
|
|
2143
|
-
// src/lock.ts
|
|
2144
|
-
function keyedLock() {
|
|
2145
|
-
const tails = /* @__PURE__ */ new Map();
|
|
2146
|
-
return {
|
|
2147
|
-
async run(key, work2) {
|
|
2148
|
-
const previous = tails.get(key) ?? Promise.resolve();
|
|
2149
|
-
let release = () => {
|
|
2150
|
-
};
|
|
2151
|
-
const done = new Promise((resolve) => {
|
|
2152
|
-
release = resolve;
|
|
2153
|
-
});
|
|
2154
|
-
const tail = previous.then(() => done);
|
|
2155
|
-
tails.set(key, tail);
|
|
2156
|
-
await previous;
|
|
2157
|
-
try {
|
|
2158
|
-
return await work2();
|
|
2159
|
-
} finally {
|
|
2160
|
-
release();
|
|
2161
|
-
if (tails.get(key) === tail) {
|
|
2162
|
-
tails.delete(key);
|
|
2163
|
-
}
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
2575
|
// src/recordings.ts
|
|
2170
2576
|
import { randomUUID } from "crypto";
|
|
2171
2577
|
import { createReadStream } from "fs";
|
|
2172
2578
|
import { readdir, readFile as readFile3, stat, writeFile as writeFile3 } from "fs/promises";
|
|
2173
|
-
import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as
|
|
2579
|
+
import { basename as basename2, dirname as dirname4, extname, isAbsolute, join as join6 } from "path";
|
|
2174
2580
|
import { createInterface as createInterface2 } from "readline";
|
|
2175
2581
|
import { Readable } from "stream";
|
|
2176
2582
|
import { promisify } from "util";
|
|
@@ -2242,12 +2648,12 @@ async function readMeta(file) {
|
|
|
2242
2648
|
}
|
|
2243
2649
|
async function metaFor(kind, file) {
|
|
2244
2650
|
if (kind === "replay") {
|
|
2245
|
-
return readMeta(
|
|
2651
|
+
return readMeta(join6(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
2246
2652
|
}
|
|
2247
2653
|
const folder = basename2(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
|
|
2248
2654
|
for (const name of await readdir(folder).catch(() => [])) {
|
|
2249
2655
|
if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
|
|
2250
|
-
const meta = await readMeta(
|
|
2656
|
+
const meta = await readMeta(join6(folder, name));
|
|
2251
2657
|
if (meta?.video === file) {
|
|
2252
2658
|
return meta;
|
|
2253
2659
|
}
|
|
@@ -2257,33 +2663,33 @@ async function metaFor(kind, file) {
|
|
|
2257
2663
|
}
|
|
2258
2664
|
async function ledger(dir) {
|
|
2259
2665
|
try {
|
|
2260
|
-
return new Set(JSON.parse(await readFile3(
|
|
2666
|
+
return new Set(JSON.parse(await readFile3(join6(dir, LEDGER_FILE), "utf8")));
|
|
2261
2667
|
} catch {
|
|
2262
2668
|
return /* @__PURE__ */ new Set();
|
|
2263
2669
|
}
|
|
2264
2670
|
}
|
|
2265
2671
|
async function remember(dir, file) {
|
|
2266
|
-
const
|
|
2267
|
-
|
|
2268
|
-
await writeFile3(
|
|
2672
|
+
const done2 = await ledger(dir);
|
|
2673
|
+
done2.add(file);
|
|
2674
|
+
await writeFile3(join6(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
2269
2675
|
}
|
|
2270
2676
|
async function nonEmpty(file) {
|
|
2271
2677
|
return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
|
|
2272
2678
|
}
|
|
2273
2679
|
async function pendingRecordings(dir) {
|
|
2274
|
-
const
|
|
2680
|
+
const done2 = await ledger(dir);
|
|
2275
2681
|
const names = (await readdir(dir).catch(() => [])).sort();
|
|
2276
2682
|
const pending = [];
|
|
2277
2683
|
for (const name of names) {
|
|
2278
2684
|
if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
|
|
2279
2685
|
continue;
|
|
2280
2686
|
}
|
|
2281
|
-
const meta = await readMeta(
|
|
2282
|
-
const replay =
|
|
2283
|
-
if (!
|
|
2687
|
+
const meta = await readMeta(join6(dir, name));
|
|
2688
|
+
const replay = join6(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
2689
|
+
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
2284
2690
|
pending.push({ kind: "replay", file: replay, meta });
|
|
2285
2691
|
}
|
|
2286
|
-
if (meta?.video && !
|
|
2692
|
+
if (meta?.video && !done2.has(meta.video) && await nonEmpty(meta.video)) {
|
|
2287
2693
|
pending.push({ kind: "video", file: meta.video, meta });
|
|
2288
2694
|
}
|
|
2289
2695
|
}
|
|
@@ -2421,149 +2827,6 @@ async function readScreenshot(request) {
|
|
|
2421
2827
|
return { name: basename3(request.path), ...caption, contentType, data };
|
|
2422
2828
|
}
|
|
2423
2829
|
|
|
2424
|
-
// src/workspace.ts
|
|
2425
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2426
|
-
import { rm as rm3 } from "fs/promises";
|
|
2427
|
-
import { join as join6 } from "path";
|
|
2428
|
-
|
|
2429
|
-
// src/git.ts
|
|
2430
|
-
import { Buffer } from "buffer";
|
|
2431
|
-
import { spawn as spawn4 } from "child_process";
|
|
2432
|
-
var GITHUB_ORIGIN = "https://github.com/";
|
|
2433
|
-
function githubAuthEnv(token) {
|
|
2434
|
-
const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
2435
|
-
return {
|
|
2436
|
-
GIT_CONFIG_COUNT: "1",
|
|
2437
|
-
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
2438
|
-
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
2439
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
2440
|
-
};
|
|
2441
|
-
}
|
|
2442
|
-
function runGit(args, options) {
|
|
2443
|
-
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
2444
|
-
return new Promise((resolve, reject) => {
|
|
2445
|
-
const child = spawn4("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2446
|
-
let stdout = "";
|
|
2447
|
-
let stderr = "";
|
|
2448
|
-
child.stdout.on("data", (chunk) => {
|
|
2449
|
-
stdout += chunk.toString();
|
|
2450
|
-
});
|
|
2451
|
-
child.stderr.on("data", (chunk) => {
|
|
2452
|
-
stderr += chunk.toString();
|
|
2453
|
-
});
|
|
2454
|
-
child.on("error", reject);
|
|
2455
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
2456
|
-
});
|
|
2457
|
-
}
|
|
2458
|
-
async function git(args, options) {
|
|
2459
|
-
const result = await runGit(args, options);
|
|
2460
|
-
if (result.code !== 0) {
|
|
2461
|
-
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
2462
|
-
}
|
|
2463
|
-
return result.stdout;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
// src/workspace.ts
|
|
2467
|
-
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
2468
|
-
var cacheLock = keyedLock();
|
|
2469
|
-
function githubRemoteUrl(fullName) {
|
|
2470
|
-
return `https://github.com/${fullName}.git`;
|
|
2471
|
-
}
|
|
2472
|
-
function tamsenseFolder(home, ...parts) {
|
|
2473
|
-
return join6(home, ".tamsense", ...parts);
|
|
2474
|
-
}
|
|
2475
|
-
function folderNames(repositories) {
|
|
2476
|
-
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
2477
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2478
|
-
for (const repository of repositories) {
|
|
2479
|
-
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
2480
|
-
}
|
|
2481
|
-
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
2482
|
-
}
|
|
2483
|
-
async function refreshCache(cache, repository, token, tools) {
|
|
2484
|
-
if (existsSync3(join6(cache, ".git"))) {
|
|
2485
|
-
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
2486
|
-
return;
|
|
2487
|
-
}
|
|
2488
|
-
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
2489
|
-
}
|
|
2490
|
-
async function removeWorktree(cache, path) {
|
|
2491
|
-
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
2492
|
-
await rm3(path, { recursive: true, force: true });
|
|
2493
|
-
await git(["worktree", "prune"], { cwd: cache });
|
|
2494
|
-
}
|
|
2495
|
-
async function baseRef(cache, job, repository) {
|
|
2496
|
-
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
2497
|
-
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
2498
|
-
}
|
|
2499
|
-
async function keptBase(cache, path, job, repository) {
|
|
2500
|
-
if (!job.keepWorkspace || !existsSync3(join6(path, ".git"))) {
|
|
2501
|
-
return null;
|
|
2502
|
-
}
|
|
2503
|
-
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
2504
|
-
if (branch.code !== 0 || branch.stdout !== job.branch) {
|
|
2505
|
-
return null;
|
|
2506
|
-
}
|
|
2507
|
-
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
2508
|
-
}
|
|
2509
|
-
async function prepareWorkspace(job, token, tools) {
|
|
2510
|
-
const root = tamsenseFolder(tools.home, "work", `task-${job.taskId}`);
|
|
2511
|
-
const folders = folderNames(job.repositories);
|
|
2512
|
-
const repositories = [];
|
|
2513
|
-
for (const [index, repository] of job.repositories.entries()) {
|
|
2514
|
-
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
2515
|
-
const folder = folders[index] ?? repository.fullName;
|
|
2516
|
-
const path = join6(root, folder);
|
|
2517
|
-
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
2518
|
-
await refreshCache(cache, repository, token, tools);
|
|
2519
|
-
const kept = await keptBase(cache, path, job, repository);
|
|
2520
|
-
if (kept) {
|
|
2521
|
-
return kept;
|
|
2522
|
-
}
|
|
2523
|
-
await removeWorktree(cache, path);
|
|
2524
|
-
await git(["worktree", "add", "--force", "-B", job.branch, path, await baseRef(cache, job, repository)], { cwd: cache });
|
|
2525
|
-
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
2526
|
-
});
|
|
2527
|
-
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
2528
|
-
}
|
|
2529
|
-
return { root, repositories };
|
|
2530
|
-
}
|
|
2531
|
-
async function commitIdentityEnv(home) {
|
|
2532
|
-
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
2533
|
-
if (configured.code === 0 && configured.stdout) {
|
|
2534
|
-
return {};
|
|
2535
|
-
}
|
|
2536
|
-
return {
|
|
2537
|
-
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
2538
|
-
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
2539
|
-
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
2540
|
-
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
2541
|
-
};
|
|
2542
|
-
}
|
|
2543
|
-
async function publishWorkspace(workspace, job, token, identity) {
|
|
2544
|
-
const pushed = [];
|
|
2545
|
-
for (const repository of workspace.repositories) {
|
|
2546
|
-
const options = { cwd: repository.path, env: identity };
|
|
2547
|
-
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
2548
|
-
await git(["add", "--all"], options);
|
|
2549
|
-
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
2550
|
-
if (hasStaged) {
|
|
2551
|
-
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
2552
|
-
}
|
|
2553
|
-
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
2554
|
-
if (newCommits === 0) {
|
|
2555
|
-
return null;
|
|
2556
|
-
}
|
|
2557
|
-
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
2558
|
-
return git(["rev-parse", "HEAD"], options);
|
|
2559
|
-
});
|
|
2560
|
-
if (sha) {
|
|
2561
|
-
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
2562
|
-
}
|
|
2563
|
-
}
|
|
2564
|
-
return pushed;
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
2830
|
// src/worker.ts
|
|
2568
2831
|
var POLL_INTERVAL_MS = 5e3;
|
|
2569
2832
|
var RETRY_AFTER_ERROR_MS = 15e3;
|
|
@@ -2574,7 +2837,7 @@ var FEED_INTERVAL_MS = 1500;
|
|
|
2574
2837
|
var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
2575
2838
|
var STOPPED_HERE = "stopped_here";
|
|
2576
2839
|
var CLOSED_ON_SITE = "closed_on_site";
|
|
2577
|
-
function
|
|
2840
|
+
function messageOf3(error) {
|
|
2578
2841
|
return error instanceof Error ? error.message : String(error);
|
|
2579
2842
|
}
|
|
2580
2843
|
function isFatal(error) {
|
|
@@ -2594,7 +2857,7 @@ async function attachScreenshot(api, job, screenshot, attached) {
|
|
|
2594
2857
|
try {
|
|
2595
2858
|
await api.request("POST", runPath(job.runId, "attachments"), body);
|
|
2596
2859
|
} catch (error) {
|
|
2597
|
-
return `Could not attach the screenshot: ${
|
|
2860
|
+
return `Could not attach the screenshot: ${messageOf3(error)}`;
|
|
2598
2861
|
}
|
|
2599
2862
|
attached.count += 1;
|
|
2600
2863
|
return "Attached to the task.";
|
|
@@ -2604,7 +2867,7 @@ function sessionReporter(api, job, print2) {
|
|
|
2604
2867
|
return {
|
|
2605
2868
|
report: (session) => {
|
|
2606
2869
|
sending ??= api.request("POST", runPath(job.runId, "session"), session).then(() => {
|
|
2607
|
-
}, (error) => print2(`Could not tell TAMsense which Claude took the task: ${
|
|
2870
|
+
}, (error) => print2(`Could not tell TAMsense which Claude took the task: ${messageOf3(error)}`));
|
|
2608
2871
|
},
|
|
2609
2872
|
sent: async () => {
|
|
2610
2873
|
await sending;
|
|
@@ -2638,7 +2901,7 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2638
2901
|
} catch (error) {
|
|
2639
2902
|
input.restoreDelivered(ack);
|
|
2640
2903
|
if (!failing) {
|
|
2641
|
-
tools.print(`Could not check for new messages on TAMsense, trying again: ${
|
|
2904
|
+
tools.print(`Could not check for new messages on TAMsense, trying again: ${messageOf3(error)}`);
|
|
2642
2905
|
}
|
|
2643
2906
|
failing = true;
|
|
2644
2907
|
return;
|
|
@@ -2661,7 +2924,7 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2661
2924
|
record({ kind: "tool", text: switchLine(change), tool: "model", ref: null, parent: null, failed: false });
|
|
2662
2925
|
}
|
|
2663
2926
|
},
|
|
2664
|
-
(error) => tools.print(`Could not switch the model: ${
|
|
2927
|
+
(error) => tools.print(`Could not switch the model: ${messageOf3(error)}`)
|
|
2665
2928
|
);
|
|
2666
2929
|
}
|
|
2667
2930
|
}
|
|
@@ -2703,6 +2966,35 @@ function askingTools(job, api) {
|
|
|
2703
2966
|
}
|
|
2704
2967
|
return { people, send: (body) => api.request("POST", runPath(job.runId, "questions"), body) };
|
|
2705
2968
|
}
|
|
2969
|
+
function permissionTools(job, tools) {
|
|
2970
|
+
if (job.stage.kind === "interview" || job.stage.kind === "spec") {
|
|
2971
|
+
return null;
|
|
2972
|
+
}
|
|
2973
|
+
return {
|
|
2974
|
+
send: async (body) => {
|
|
2975
|
+
const reply2 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
|
|
2976
|
+
if (reply2.decision === "wait") {
|
|
2977
|
+
tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
2978
|
+
}
|
|
2979
|
+
return reply2;
|
|
2980
|
+
},
|
|
2981
|
+
check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
|
|
2982
|
+
sleep: tools.sleep
|
|
2983
|
+
};
|
|
2984
|
+
}
|
|
2985
|
+
function knowledgeTools2(job, api) {
|
|
2986
|
+
if (!job.knowledge) {
|
|
2987
|
+
return null;
|
|
2988
|
+
}
|
|
2989
|
+
return {
|
|
2990
|
+
entryIds: knowledgeIds(job.knowledge),
|
|
2991
|
+
moreIds: job.knowledge.more.map((entry) => entry.id),
|
|
2992
|
+
canPropose: canProposeKnowledge(job),
|
|
2993
|
+
propose: (body) => api.request("POST", runPath(job.runId, "knowledge"), body),
|
|
2994
|
+
update: (entryId, body) => api.request("POST", knowledgePath(job.runId, entryId), body),
|
|
2995
|
+
read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
|
|
2996
|
+
};
|
|
2997
|
+
}
|
|
2706
2998
|
async function executeJob(job, tools, signal, input, record) {
|
|
2707
2999
|
const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
|
|
2708
3000
|
tools.print("Preparing the code\u2026");
|
|
@@ -2724,6 +3016,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2724
3016
|
wantsVerdict: isCheck,
|
|
2725
3017
|
specWork: specWorkTools2(job, tools.api),
|
|
2726
3018
|
asking: askingTools(job, tools.api),
|
|
3019
|
+
permissions: permissionTools(job, tools),
|
|
3020
|
+
knowledge: knowledgeTools2(job, tools.api),
|
|
2727
3021
|
handoffs: job.allowedHandoffs,
|
|
2728
3022
|
resumeSessionId: job.resumeSessionId,
|
|
2729
3023
|
model: job.model,
|
|
@@ -2795,7 +3089,7 @@ async function sendRecordings(job, tools) {
|
|
|
2795
3089
|
}
|
|
2796
3090
|
}
|
|
2797
3091
|
function failureReport(error, signal) {
|
|
2798
|
-
return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${
|
|
3092
|
+
return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
|
|
2799
3093
|
}
|
|
2800
3094
|
async function runJob(job, tools, stop) {
|
|
2801
3095
|
tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
|
|
@@ -2834,12 +3128,12 @@ async function runJob(job, tools, stop) {
|
|
|
2834
3128
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
2835
3129
|
return;
|
|
2836
3130
|
}
|
|
2837
|
-
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${
|
|
3131
|
+
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
|
|
2838
3132
|
try {
|
|
2839
3133
|
await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
|
|
2840
3134
|
tools.print(resultLine(finish2));
|
|
2841
3135
|
} catch (error) {
|
|
2842
|
-
tools.print(`Could not send the result to TAMsense: ${
|
|
3136
|
+
tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
|
|
2843
3137
|
}
|
|
2844
3138
|
}
|
|
2845
3139
|
async function reportCapabilities(tools, stop) {
|
|
@@ -2858,7 +3152,7 @@ async function reportCapabilities(tools, stop) {
|
|
|
2858
3152
|
}
|
|
2859
3153
|
} catch (error) {
|
|
2860
3154
|
if (!complained && !stop.aborted) {
|
|
2861
|
-
tools.print(`Could not tell TAMsense which Claude models this computer has: ${
|
|
3155
|
+
tools.print(`Could not tell TAMsense which Claude models this computer has: ${messageOf3(error)}`);
|
|
2862
3156
|
complained = true;
|
|
2863
3157
|
}
|
|
2864
3158
|
}
|
|
@@ -2866,18 +3160,18 @@ async function reportCapabilities(tools, stop) {
|
|
|
2866
3160
|
}
|
|
2867
3161
|
}
|
|
2868
3162
|
function jobTools(job, tools, parallel) {
|
|
2869
|
-
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId}] ${line}`) } : tools;
|
|
3163
|
+
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId} ${job.stage.name}] ${line}`) } : tools;
|
|
2870
3164
|
}
|
|
2871
3165
|
async function work(tools, stop, parallel = 1) {
|
|
2872
3166
|
tools.print(parallel > 1 ? `Waiting for tasks, up to ${parallel} at once. Press Ctrl+C to stop.` : "Waiting for tasks. Press Ctrl+C to stop.");
|
|
2873
3167
|
const halt = new AbortController();
|
|
2874
3168
|
const stopAll = AbortSignal.any([stop, halt.signal]);
|
|
2875
3169
|
const running = /* @__PURE__ */ new Set();
|
|
2876
|
-
const
|
|
3170
|
+
const folderLock = keyedLock();
|
|
2877
3171
|
const reporting = reportCapabilities(tools, stopAll);
|
|
2878
3172
|
const start = (job) => {
|
|
2879
3173
|
const runTools = jobTools(job, tools, parallel);
|
|
2880
|
-
const task =
|
|
3174
|
+
const task = folderLock.run(stepFolder(job).join("/"), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
|
|
2881
3175
|
running.add(task);
|
|
2882
3176
|
};
|
|
2883
3177
|
try {
|
|
@@ -2894,7 +3188,7 @@ async function work(tools, stop, parallel = 1) {
|
|
|
2894
3188
|
halt.abort();
|
|
2895
3189
|
throw error;
|
|
2896
3190
|
}
|
|
2897
|
-
tools.print(`Could not reach TAMsense: ${
|
|
3191
|
+
tools.print(`Could not reach TAMsense: ${messageOf3(error)}. Trying again soon.`);
|
|
2898
3192
|
await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
|
|
2899
3193
|
continue;
|
|
2900
3194
|
}
|