@tamsensedev/runner 0.3.0 → 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/dist/cli.js CHANGED
@@ -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 tool2 } from "@anthropic-ai/claude-agent-sdk";
345
- import { z as z2 } from "zod";
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 ? `, ${person.roles.join(", ")}` : "";
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(tool3, input) {
405
+ function activityLine(tool4, input) {
406
406
  const fields = typeof input === "object" && input !== null ? input : {};
407
- if (tool3 === "Bash" && typeof fields.command === "string") {
407
+ if (tool4 === "Bash" && typeof fields.command === "string") {
408
408
  return `$ ${shorten(fields.command)}`;
409
409
  }
410
- if (FILE_TOOLS.has(tool3) && typeof fields.file_path === "string") {
411
- return `${tool3} ${shorten(fields.file_path)}`;
410
+ if (FILE_TOOLS.has(tool4) && typeof fields.file_path === "string") {
411
+ return `${tool4} ${shorten(fields.file_path)}`;
412
412
  }
413
- if (tool3 === "Skill" && typeof fields.skill === "string") {
413
+ if (tool4 === "Skill" && typeof fields.skill === "string") {
414
414
  return `Skill ${shorten(fields.skill)}`;
415
415
  }
416
- if (HELPER_TOOLS.has(tool3) && typeof fields.subagent_type === "string") {
416
+ if (HELPER_TOOLS.has(tool4) && typeof fields.subagent_type === "string") {
417
417
  return `Agent ${shorten(fields.subagent_type)}`;
418
418
  }
419
- return tool3;
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 tool3 = (block.name ?? "tool").replace(MCP_PREFIX, "");
462
+ const tool4 = (block.name ?? "tool").replace(MCP_PREFIX, "");
463
463
  const fields = fieldsOf(block.input);
464
- const base = { tool: tool3, ref: block.id ?? null, parent, failed: false };
465
- if (tool3 === "Skill" && textField(fields, "skill")) {
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(tool3)) {
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 (tool3 === "submit_verdict") {
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 (tool3 === "hand_off") {
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 (tool3 in ATTACH_LABELS) {
481
- return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool3]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
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 = tool3 === "Bash" && command ? `$ ${command}` : activityLine(tool3, block.input);
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) {
@@ -770,6 +770,204 @@ function inputChannel(options = {}) {
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);
@@ -860,8 +1058,9 @@ var SUBMIT_QUESTIONS_TOOL_NAME = "submit_questions";
860
1058
  var SUBMIT_SPEC_TOOL_NAME = "submit_spec";
861
1059
  var ASK_PERSON_TOOL_NAME = "ask_person";
862
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.`;
863
- var UNATTENDED_RULE = `${MESSAGES_RULE} Nobody answers questions or approves actions during the run, so never stop to wait for a reply: decide yourself, keep working, and explain your decisions in your report.`;
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.`;
864
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.";
865
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.";
866
1065
  var CODE_RULES = [
867
1066
  "Make the changes the task asks for, and keep them focused on it.",
@@ -927,6 +1126,9 @@ function contextText(context) {
927
1126
  return context.trim() ? `Project context from the team (follow it like the project's own rules):
928
1127
  ${context.trim()}` : null;
929
1128
  }
1129
+ function safetyText(job) {
1130
+ return job.safetyRules?.trim() || null;
1131
+ }
930
1132
  function bulletList(lines) {
931
1133
  return lines.map((line) => `- ${line}`).join("\n");
932
1134
  }
@@ -935,7 +1137,7 @@ function repositoryLines(job, workspace) {
935
1137
  return workspace.repositories.map((repository) => {
936
1138
  const source = sourceOf.get(repository.fullName);
937
1139
  const pull = source?.pullRequestUrl ? `, pull request ${source.pullRequestUrl}` : "";
938
- 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}`;
939
1141
  });
940
1142
  }
941
1143
  function stepLine(stage) {
@@ -950,11 +1152,14 @@ function peopleText(people) {
950
1152
  return null;
951
1153
  }
952
1154
  return [
953
- `People on this task you can ask with ${ASK_PERSON_TOOL_NAME} (id: name, relation, access):`,
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):`,
954
1156
  bulletList(people.map(personLine)),
955
- "Ask the assignee first; a question on a topic goes to the person whose role fits it. 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."
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.'
956
1158
  ].join("\n");
957
1159
  }
1160
+ function canProposeKnowledge(job) {
1161
+ return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
1162
+ }
958
1163
  function handoffText(targets, isCheck) {
959
1164
  if (targets.length === 0) {
960
1165
  return null;
@@ -1017,6 +1222,7 @@ ${JSON.stringify(job.spec)}` : null,
1017
1222
  const instructions = [
1018
1223
  SPEC_UNATTENDED_RULE,
1019
1224
  MACHINE_RULE,
1225
+ workplaceText(job.computer, workspace.root),
1020
1226
  stepLine(job.stage),
1021
1227
  job.stage.instructions,
1022
1228
  contextText(job.projectContext),
@@ -1025,6 +1231,8 @@ ${bulletList(repositoryLines(job, workspace))}`,
1025
1231
  READ_ONLY_RULE,
1026
1232
  `How to work:
1027
1233
  ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
1234
+ safetyText(job),
1235
+ knowledgeText(job.knowledge, job.computer, false),
1028
1236
  SPEC_STYLE_RULE
1029
1237
  ].filter(Boolean).join("\n\n");
1030
1238
  return { prompt, instructions };
@@ -1055,6 +1263,7 @@ ${job.review}`,
1055
1263
  const instructions = [
1056
1264
  canAsk ? ASKING_RULE : UNATTENDED_RULE,
1057
1265
  MACHINE_RULE,
1266
+ workplaceText(job.computer, workspace.root),
1058
1267
  stepLine(job.stage),
1059
1268
  job.stage.instructions,
1060
1269
  contextText(job.projectContext),
@@ -1062,6 +1271,9 @@ ${job.review}`,
1062
1271
  ${bulletList(repositoryLines(job, workspace))}`,
1063
1272
  `How to work:
1064
1273
  ${bulletList(rules)}`,
1274
+ safetyText(job),
1275
+ knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
1276
+ APPROVAL_RULE,
1065
1277
  peopleText(people),
1066
1278
  handoffText(job.allowedHandoffs, isCheck),
1067
1279
  options.browser && BROWSER_RULE
@@ -1070,98 +1282,98 @@ ${bulletList(rules)}`,
1070
1282
  }
1071
1283
 
1072
1284
  // src/specTools.ts
1073
- import { tool } from "@anthropic-ai/claude-agent-sdk";
1074
- import { z } from "zod";
1285
+ import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
1286
+ import { z as z2 } from "zod";
1075
1287
  var MAX_FIRST_ROUND = 4;
1076
- var questionSchema = z.object({
1077
- id: z.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
1078
- question: z.string().describe("One plain sentence the owner can answer without reading code"),
1079
- whyItMatters: z.string().describe("One line: what changes in the result depending on the answer"),
1080
- options: z.array(z.object({
1081
- label: z.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
1082
- description: z.string().describe("One sentence: what happens if this option is chosen"),
1083
- recommended: z.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
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")
1084
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'),
1085
- multiSelect: z.boolean().describe("true only when the options are additive and several can apply together")
1297
+ multiSelect: z2.boolean().describe("true only when the options are additive and several can apply together")
1086
1298
  });
1087
- var briefSchema = z.object({
1088
- findings: z.array(z.object({
1089
- fact: z.string().describe("What the code does today that matters for this task"),
1090
- where: z.string().describe("Where it is: owner/repo path:line")
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")
1091
1303
  })).describe("Findings from the code"),
1092
- dataFacts: z.array(z.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
1093
- approach: z.string().describe("The approach you would take, in 2-4 sentences"),
1094
- codeMap: z.array(z.object({
1095
- repo: z.string().describe("Repository full name (owner/name)"),
1096
- file: z.string().describe("Path relative to the repository root"),
1097
- role: z.string().describe("Why it matters: what to change or reuse there")
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")
1098
1310
  })).describe("The files the change will touch or must reuse"),
1099
- assumptions: z.array(z.object({
1100
- text: z.string().describe("The default you chose"),
1101
- reason: z.string().describe("Why it is safe or why it was chosen")
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")
1102
1314
  })).describe("Corners settled with a default instead of a question"),
1103
- pendingDecisions: z.array(z.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
1315
+ pendingDecisions: z2.array(z2.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
1104
1316
  });
1105
- var questionsPayloadSchema = z.object({
1317
+ var questionsPayloadSchema = z2.object({
1106
1318
  brief: briefSchema,
1107
- questions: z.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`)
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`)
1108
1320
  });
1109
- var specSchema = z.object({
1110
- goal: z.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
1111
- context: z.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
1112
- inScope: z.array(z.string()).describe("What this task delivers, one item each"),
1113
- outOfScope: z.array(z.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"),
1114
- decisions: z.array(z.object({
1115
- question: z.string(),
1116
- answer: z.string(),
1117
- by: z.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
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")
1118
1330
  })).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
1119
- codeMap: z.array(z.object({
1120
- repo: z.string().describe("Repository full name (owner/name)"),
1121
- file: z.string().describe("Path relative to the repository root"),
1122
- role: z.string().describe("What the coder needs to know about this file"),
1123
- use: z.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
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")
1124
1336
  })).describe("The files a coder must read before starting"),
1125
- changes: z.array(z.object({
1126
- repo: z.string().describe("Repository full name (owner/name) where the change happens"),
1127
- file: z.string().describe("Path to the file, relative to the repository root"),
1128
- action: z.enum(["create", "modify", "delete"]),
1129
- description: z.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
1130
- produces: z.array(z.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
1131
- consumes: z.array(z.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
1132
- acceptance: z.array(z.string()).describe("Ids of the criteria this change makes true, like AC-1")
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")
1133
1345
  })).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
1134
- acceptance: z.array(z.object({
1135
- id: z.string().describe("AC-1, AC-2 and so on, in order"),
1136
- kind: z.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
1137
- text: z.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
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')
1138
1350
  })).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
1139
- testPlan: z.object({
1140
- automated: z.array(z.object({
1141
- repo: z.string(),
1142
- command: z.string().describe("The exact command to run"),
1143
- expect: z.string().describe("What passing looks like")
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")
1144
1356
  })).describe("At least one command"),
1145
- newTests: z.array(z.object({
1146
- seam: z.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
1147
- covers: z.array(z.string()).describe("Ids of the criteria it 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")
1148
1360
  })),
1149
- manual: z.array(z.object({
1150
- page: z.string().describe("The page or screen"),
1151
- steps: z.string().describe("What a person does and sees")
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")
1152
1364
  }))
1153
1365
  }),
1154
- risks: z.array(z.object({
1155
- risk: z.string(),
1156
- mitigation: z.string()
1366
+ risks: z2.array(z2.object({
1367
+ risk: z2.string(),
1368
+ mitigation: z2.string()
1157
1369
  })).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
1158
- riskLevel: z.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
1159
- assumptions: z.array(z.object({
1160
- text: z.string().describe("The default you chose"),
1161
- reason: z.string().describe("Why, so the owner can confirm or correct it quickly")
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")
1162
1374
  })).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
1163
- openQuestions: z.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
1164
- split: z.array(z.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")
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")
1165
1377
  });
1166
1378
  function reply(text2, isError = false) {
1167
1379
  return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
@@ -1171,7 +1383,7 @@ function unreachable(error, toolName) {
1171
1383
  return reply(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
1172
1384
  }
1173
1385
  function questionsTool(review, onSubmit) {
1174
- return tool(
1386
+ return tool2(
1175
1387
  SUBMIT_QUESTIONS_TOOL_NAME,
1176
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.",
1177
1389
  questionsPayloadSchema.shape,
@@ -1194,7 +1406,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
1194
1406
  function specTool(review, onSubmit, builderCheckRan) {
1195
1407
  let repairs = 0;
1196
1408
  let reminded = false;
1197
- return tool(
1409
+ return tool2(
1198
1410
  SUBMIT_SPEC_TOOL_NAME,
1199
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.",
1200
1412
  specSchema.shape,
@@ -1314,14 +1526,12 @@ function usageLimitWatch() {
1314
1526
  }
1315
1527
 
1316
1528
  // 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
1529
  var NO_RESULT_TEXT = "Claude finished without a result.";
1319
1530
  var SERVER_NAME = "tamsense";
1320
1531
  var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
1321
- var denyWhenAsked = async () => ({ behavior: "deny", message: NOBODY_TO_ASK });
1322
1532
  var CAPTION_FIELDS = {
1323
- title: z2.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"'),
1324
- purpose: z2.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
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"')
1325
1535
  };
1326
1536
  var ATTACH_TOOLS = {
1327
1537
  screenshot: {
@@ -1342,21 +1552,21 @@ var ATTACH_TOOLS = {
1342
1552
  };
1343
1553
  function attachTool(kind, attach) {
1344
1554
  const spec = ATTACH_TOOLS[kind];
1345
- return tool2(
1555
+ return tool3(
1346
1556
  spec.name,
1347
1557
  spec.description,
1348
- { path: z2.string().describe(spec.path), ...CAPTION_FIELDS },
1558
+ { path: z3.string().describe(spec.path), ...CAPTION_FIELDS },
1349
1559
  async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
1350
1560
  );
1351
1561
  }
1352
1562
  function verdictTool(onVerdict) {
1353
- return tool2(
1563
+ return tool3(
1354
1564
  VERDICT_TOOL_NAME,
1355
1565
  "Hand in the result of this check. Call it exactly once, at the very end.",
1356
1566
  {
1357
- passed: z2.boolean().describe("true only if nothing must be fixed"),
1358
- report: z2.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
1359
- couldNotRun: z2.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")
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")
1360
1570
  },
1361
1571
  async (args) => {
1362
1572
  onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
@@ -1365,13 +1575,13 @@ function verdictTool(onVerdict) {
1365
1575
  );
1366
1576
  }
1367
1577
  function handoffTool(targets, onHandoff) {
1368
- return tool2(
1578
+ return tool3(
1369
1579
  HANDOFF_TOOL_NAME,
1370
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.",
1371
1581
  {
1372
- to: z2.string().describe("name or id of the step to hand the task to"),
1373
- outcome: z2.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
1374
- note: z2.string().describe("for the next agent and the owner: what was done, what to check, what is open")
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")
1375
1585
  },
1376
1586
  async (args) => {
1377
1587
  const choice = chooseHandoff(targets, args);
@@ -1384,25 +1594,25 @@ function handoffTool(targets, onHandoff) {
1384
1594
  );
1385
1595
  }
1386
1596
  function askPersonTool(asking, waiting) {
1387
- return tool2(
1597
+ return tool3(
1388
1598
  ASK_PERSON_TOOL_NAME,
1389
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.",
1390
1600
  {
1391
- to: z2.union([z2.number(), z2.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"),
1392
- why: z2.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
1393
- question: z2.string().describe("one plain question the person can answer without reading the code"),
1394
- context: z2.string().optional().describe("one line on what you saw that led to the question"),
1395
- tried: z2.array(z2.object({
1396
- did: z2.string().describe("the command or step"),
1397
- error: z2.string().optional().describe("the exact error line, if there was one"),
1398
- result: z2.string().describe("what it showed, one line")
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")
1399
1609
  })).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
1400
- options: z2.array(z2.object({
1401
- label: z2.string().describe("a short answer the person can pick"),
1402
- detail: z2.string().describe("one line on what happens if it is picked")
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")
1403
1613
  })).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
1404
- blocking: z2.boolean().describe("true when you cannot go on without the answer"),
1405
- assumption: z2.string().optional().describe("when blocking is false: what you continue with until the answer comes")
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")
1406
1616
  },
1407
1617
  async (args) => {
1408
1618
  if (waiting.waitingFor !== null) {
@@ -1435,6 +1645,16 @@ function specWorkTools(specWork, submitted) {
1435
1645
  submitted.spec = spec;
1436
1646
  }, () => submitted.helperStarted)];
1437
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
+ }
1438
1658
  function tamsenseServer(request, submitted) {
1439
1659
  const tools = [
1440
1660
  attachTool("screenshot", request.attach),
@@ -1445,6 +1665,7 @@ function tamsenseServer(request, submitted) {
1445
1665
  })] : [],
1446
1666
  ...specWorkTools(request.specWork, submitted),
1447
1667
  ...request.asking ? [askPersonTool(request.asking, submitted)] : [],
1668
+ ...knowledgeTools(request.knowledge),
1448
1669
  ...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
1449
1670
  submitted.handoff = handoff;
1450
1671
  })] : []
@@ -1483,7 +1704,8 @@ async function attempt(request, context, resume) {
1483
1704
  permissionMode: "auto",
1484
1705
  // краткий пересказ размышлений Claude идёт в журнал задачи, как в Paseo
1485
1706
  thinking: { type: "adaptive", display: "summarized" },
1486
- canUseTool: denyWhenAsked,
1707
+ // режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
1708
+ canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
1487
1709
  plugins: [{ type: "local", path: LIBRARY_PATH }],
1488
1710
  mcpServers: { [SERVER_NAME]: tamsense.server },
1489
1711
  allowedTools: tamsense.allowed,
@@ -1512,6 +1734,7 @@ async function attempt(request, context, resume) {
1512
1734
  request.onFeed(draft);
1513
1735
  if (draft.kind === "text" && draft.parent === null) {
1514
1736
  lastText = draft.text;
1737
+ context.lastWords.text = draft.text;
1515
1738
  }
1516
1739
  }
1517
1740
  if (message.type === "result") {
@@ -1538,7 +1761,7 @@ function claudeRunner(startQuery) {
1538
1761
  }, { once: true });
1539
1762
  const submitted = { verdict: null, handoff: null, questions: null, spec: null, helperStarted: false, waitingFor: null };
1540
1763
  const limit = usageLimitWatch();
1541
- const context = { startQuery, abortController, submitted, limit };
1764
+ const context = { startQuery, abortController, submitted, limit, lastWords: { text: "" } };
1542
1765
  try {
1543
1766
  let result = await attempt(request, context, request.resumeSessionId);
1544
1767
  if (result.resumeFailed) {
@@ -1608,6 +1831,12 @@ var CAPABILITIES_PATH = "/api/runner/me/capabilities";
1608
1831
  function runPath(runId, action) {
1609
1832
  return `/api/runner/runs/${runId}/${action}`;
1610
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
+ }
1611
1840
  function recordingUploadPath(runId, upload, part) {
1612
1841
  return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
1613
1842
  }
@@ -2148,10 +2377,10 @@ function keyedLock() {
2148
2377
  const previous = tails.get(key) ?? Promise.resolve();
2149
2378
  let release = () => {
2150
2379
  };
2151
- const done = new Promise((resolve) => {
2380
+ const done2 = new Promise((resolve) => {
2152
2381
  release = resolve;
2153
2382
  });
2154
- const tail = previous.then(() => done);
2383
+ const tail = previous.then(() => done2);
2155
2384
  tails.set(key, tail);
2156
2385
  await previous;
2157
2386
  try {
@@ -2263,15 +2492,15 @@ async function ledger(dir) {
2263
2492
  }
2264
2493
  }
2265
2494
  async function remember(dir, file) {
2266
- const done = await ledger(dir);
2267
- done.add(file);
2268
- await writeFile3(join5(dir, LEDGER_FILE), JSON.stringify([...done]));
2495
+ const done2 = await ledger(dir);
2496
+ done2.add(file);
2497
+ await writeFile3(join5(dir, LEDGER_FILE), JSON.stringify([...done2]));
2269
2498
  }
2270
2499
  async function nonEmpty(file) {
2271
2500
  return file !== null && ((await stat(file).catch(() => null))?.size ?? 0) > 0;
2272
2501
  }
2273
2502
  async function pendingRecordings(dir) {
2274
- const done = await ledger(dir);
2503
+ const done2 = await ledger(dir);
2275
2504
  const names = (await readdir(dir).catch(() => [])).sort();
2276
2505
  const pending = [];
2277
2506
  for (const name of names) {
@@ -2280,10 +2509,10 @@ async function pendingRecordings(dir) {
2280
2509
  }
2281
2510
  const meta = await readMeta(join5(dir, name));
2282
2511
  const replay = join5(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
2283
- if (!done.has(replay) && await nonEmpty(replay)) {
2512
+ if (!done2.has(replay) && await nonEmpty(replay)) {
2284
2513
  pending.push({ kind: "replay", file: replay, meta });
2285
2514
  }
2286
- if (meta?.video && !done.has(meta.video) && await nonEmpty(meta.video)) {
2515
+ if (meta?.video && !done2.has(meta.video) && await nonEmpty(meta.video)) {
2287
2516
  pending.push({ kind: "video", file: meta.video, meta });
2288
2517
  }
2289
2518
  }
@@ -2574,7 +2803,7 @@ var FEED_INTERVAL_MS = 1500;
2574
2803
  var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
2575
2804
  var STOPPED_HERE = "stopped_here";
2576
2805
  var CLOSED_ON_SITE = "closed_on_site";
2577
- function messageOf(error) {
2806
+ function messageOf3(error) {
2578
2807
  return error instanceof Error ? error.message : String(error);
2579
2808
  }
2580
2809
  function isFatal(error) {
@@ -2594,7 +2823,7 @@ async function attachScreenshot(api, job, screenshot, attached) {
2594
2823
  try {
2595
2824
  await api.request("POST", runPath(job.runId, "attachments"), body);
2596
2825
  } catch (error) {
2597
- return `Could not attach the screenshot: ${messageOf(error)}`;
2826
+ return `Could not attach the screenshot: ${messageOf3(error)}`;
2598
2827
  }
2599
2828
  attached.count += 1;
2600
2829
  return "Attached to the task.";
@@ -2604,7 +2833,7 @@ function sessionReporter(api, job, print2) {
2604
2833
  return {
2605
2834
  report: (session) => {
2606
2835
  sending ??= api.request("POST", runPath(job.runId, "session"), session).then(() => {
2607
- }, (error) => print2(`Could not tell TAMsense which Claude took the task: ${messageOf(error)}`));
2836
+ }, (error) => print2(`Could not tell TAMsense which Claude took the task: ${messageOf3(error)}`));
2608
2837
  },
2609
2838
  sent: async () => {
2610
2839
  await sending;
@@ -2638,7 +2867,7 @@ function inbox(job, tools, record, closedOnSite) {
2638
2867
  } catch (error) {
2639
2868
  input.restoreDelivered(ack);
2640
2869
  if (!failing) {
2641
- tools.print(`Could not check for new messages on TAMsense, trying again: ${messageOf(error)}`);
2870
+ tools.print(`Could not check for new messages on TAMsense, trying again: ${messageOf3(error)}`);
2642
2871
  }
2643
2872
  failing = true;
2644
2873
  return;
@@ -2661,7 +2890,7 @@ function inbox(job, tools, record, closedOnSite) {
2661
2890
  record({ kind: "tool", text: switchLine(change), tool: "model", ref: null, parent: null, failed: false });
2662
2891
  }
2663
2892
  },
2664
- (error) => tools.print(`Could not switch the model: ${messageOf(error)}`)
2893
+ (error) => tools.print(`Could not switch the model: ${messageOf3(error)}`)
2665
2894
  );
2666
2895
  }
2667
2896
  }
@@ -2703,6 +2932,35 @@ function askingTools(job, api) {
2703
2932
  }
2704
2933
  return { people, send: (body) => api.request("POST", runPath(job.runId, "questions"), body) };
2705
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
+ }
2706
2964
  async function executeJob(job, tools, signal, input, record) {
2707
2965
  const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
2708
2966
  tools.print("Preparing the code\u2026");
@@ -2724,6 +2982,8 @@ async function executeJob(job, tools, signal, input, record) {
2724
2982
  wantsVerdict: isCheck,
2725
2983
  specWork: specWorkTools2(job, tools.api),
2726
2984
  asking: askingTools(job, tools.api),
2985
+ permissions: permissionTools(job, tools),
2986
+ knowledge: knowledgeTools2(job, tools.api),
2727
2987
  handoffs: job.allowedHandoffs,
2728
2988
  resumeSessionId: job.resumeSessionId,
2729
2989
  model: job.model,
@@ -2795,7 +3055,7 @@ async function sendRecordings(job, tools) {
2795
3055
  }
2796
3056
  }
2797
3057
  function failureReport(error, signal) {
2798
- return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf(error)}`;
3058
+ return signal.reason === STOPPED_HERE ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
2799
3059
  }
2800
3060
  async function runJob(job, tools, stop) {
2801
3061
  tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
@@ -2834,12 +3094,12 @@ async function runJob(job, tools, stop) {
2834
3094
  tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
2835
3095
  return;
2836
3096
  }
2837
- await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf(error)}`));
3097
+ await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
2838
3098
  try {
2839
3099
  await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
2840
3100
  tools.print(resultLine(finish2));
2841
3101
  } catch (error) {
2842
- tools.print(`Could not send the result to TAMsense: ${messageOf(error)}`);
3102
+ tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
2843
3103
  }
2844
3104
  }
2845
3105
  async function reportCapabilities(tools, stop) {
@@ -2858,7 +3118,7 @@ async function reportCapabilities(tools, stop) {
2858
3118
  }
2859
3119
  } catch (error) {
2860
3120
  if (!complained && !stop.aborted) {
2861
- tools.print(`Could not tell TAMsense which Claude models this computer has: ${messageOf(error)}`);
3121
+ tools.print(`Could not tell TAMsense which Claude models this computer has: ${messageOf3(error)}`);
2862
3122
  complained = true;
2863
3123
  }
2864
3124
  }
@@ -2877,7 +3137,7 @@ async function work(tools, stop, parallel = 1) {
2877
3137
  const reporting = reportCapabilities(tools, stopAll);
2878
3138
  const start = (job) => {
2879
3139
  const runTools = jobTools(job, tools, parallel);
2880
- const task = taskLock.run(String(job.taskId), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf(error)}`)).finally(() => running.delete(task));
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));
2881
3141
  running.add(task);
2882
3142
  };
2883
3143
  try {
@@ -2894,7 +3154,7 @@ async function work(tools, stop, parallel = 1) {
2894
3154
  halt.abort();
2895
3155
  throw error;
2896
3156
  }
2897
- tools.print(`Could not reach TAMsense: ${messageOf(error)}. Trying again soon.`);
3157
+ tools.print(`Could not reach TAMsense: ${messageOf3(error)}. Trying again soon.`);
2898
3158
  await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
2899
3159
  continue;
2900
3160
  }