@tamsensedev/runner 0.6.0 → 0.9.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 +875 -279
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync5 } from "fs";
|
|
5
5
|
import { homedir as homedir3 } from "os";
|
|
6
6
|
import process5 from "process";
|
|
7
7
|
|
|
@@ -33,9 +33,9 @@ function textOf(value) {
|
|
|
33
33
|
return typeof value === "string" && value.trim() ? value : null;
|
|
34
34
|
}
|
|
35
35
|
async function errorOf(response) {
|
|
36
|
-
const
|
|
37
|
-
const code = textOf(
|
|
38
|
-
const message = textOf(
|
|
36
|
+
const reply3 = await response.json().catch(() => ({}));
|
|
37
|
+
const code = textOf(reply3.data?.code);
|
|
38
|
+
const message = textOf(reply3.message) ?? textOf(reply3.statusMessage) ?? `TAMsense answered with status ${response.status}`;
|
|
39
39
|
return new ApiError(response.status, code, message);
|
|
40
40
|
}
|
|
41
41
|
function createApi(server, token = null, fetchImpl = fetch) {
|
|
@@ -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((resolve3, 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((resolve2, reject) => {
|
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
child.on("error", reject);
|
|
204
|
-
child.on("close", (code) =>
|
|
204
|
+
child.on("close", (code) => resolve3({ 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((resolve3) => {
|
|
318
|
+
release = resolve3;
|
|
319
319
|
});
|
|
320
320
|
const nothing = {
|
|
321
321
|
[Symbol.asyncIterator]: () => ({
|
|
@@ -340,9 +340,8 @@ async function probeCapabilities(stop) {
|
|
|
340
340
|
}
|
|
341
341
|
|
|
342
342
|
// src/claude.ts
|
|
343
|
-
import {
|
|
344
|
-
import {
|
|
345
|
-
import { z as z3 } from "zod";
|
|
343
|
+
import { createSdkMcpServer, query as query2, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
344
|
+
import { z as z4 } from "zod";
|
|
346
345
|
|
|
347
346
|
// src/askPerson.ts
|
|
348
347
|
var MAX_ASK_TRIES = 5;
|
|
@@ -402,21 +401,21 @@ function shorten(text2) {
|
|
|
402
401
|
const line = text2.replace(/\s+/g, " ").trim();
|
|
403
402
|
return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
|
|
404
403
|
}
|
|
405
|
-
function activityLine(
|
|
404
|
+
function activityLine(tool5, input) {
|
|
406
405
|
const fields = typeof input === "object" && input !== null ? input : {};
|
|
407
|
-
if (
|
|
406
|
+
if (tool5 === "Bash" && typeof fields.command === "string") {
|
|
408
407
|
return `$ ${shorten(fields.command)}`;
|
|
409
408
|
}
|
|
410
|
-
if (FILE_TOOLS.has(
|
|
411
|
-
return `${
|
|
409
|
+
if (FILE_TOOLS.has(tool5) && typeof fields.file_path === "string") {
|
|
410
|
+
return `${tool5} ${shorten(fields.file_path)}`;
|
|
412
411
|
}
|
|
413
|
-
if (
|
|
412
|
+
if (tool5 === "Skill" && typeof fields.skill === "string") {
|
|
414
413
|
return `Skill ${shorten(fields.skill)}`;
|
|
415
414
|
}
|
|
416
|
-
if (HELPER_TOOLS.has(
|
|
415
|
+
if (HELPER_TOOLS.has(tool5) && typeof fields.subagent_type === "string") {
|
|
417
416
|
return `Agent ${shorten(fields.subagent_type)}`;
|
|
418
417
|
}
|
|
419
|
-
return
|
|
418
|
+
return tool5;
|
|
420
419
|
}
|
|
421
420
|
function usageOf(modelUsage) {
|
|
422
421
|
const entries = Object.values(modelUsage);
|
|
@@ -440,6 +439,33 @@ function outcomeOf(result, lastText) {
|
|
|
440
439
|
return { completed: false, report: result.errors.join("\n") || `Claude stopped early: ${result.subtype}`, ...measured };
|
|
441
440
|
}
|
|
442
441
|
|
|
442
|
+
// src/protocol.ts
|
|
443
|
+
var MAX_RESULT_FILE_BYTES = 10 * 1024 * 1024;
|
|
444
|
+
var HELPER_STOPPED = "stopped";
|
|
445
|
+
var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
446
|
+
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
447
|
+
var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
448
|
+
var PAIRINGS_PATH = "/api/runner/pairings";
|
|
449
|
+
var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
|
|
450
|
+
var IDENTITY_PATH = "/api/runner/me";
|
|
451
|
+
var NEXT_RUN_PATH = "/api/runner/runs/next";
|
|
452
|
+
var CAPABILITIES_PATH = "/api/runner/me/capabilities";
|
|
453
|
+
function runPath(runId, action) {
|
|
454
|
+
return `/api/runner/runs/${runId}/${action}`;
|
|
455
|
+
}
|
|
456
|
+
function resultFilePath(runId, resultId) {
|
|
457
|
+
return `/api/runner/runs/${runId}/results/${resultId}`;
|
|
458
|
+
}
|
|
459
|
+
function knowledgePath(runId, entryId) {
|
|
460
|
+
return `/api/runner/runs/${runId}/knowledge/${entryId}`;
|
|
461
|
+
}
|
|
462
|
+
function permissionPath(runId, permissionId) {
|
|
463
|
+
return `/api/runner/runs/${runId}/permissions/${permissionId}`;
|
|
464
|
+
}
|
|
465
|
+
function recordingUploadPath(runId, upload, part) {
|
|
466
|
+
return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
|
|
467
|
+
}
|
|
468
|
+
|
|
443
469
|
// src/feed.ts
|
|
444
470
|
var MAX_TEXT_LENGTH = 8e3;
|
|
445
471
|
var MAX_RESULT_LENGTH = 2e3;
|
|
@@ -459,29 +485,29 @@ function textField(fields, name) {
|
|
|
459
485
|
return typeof value === "string" && value.trim() ? value : null;
|
|
460
486
|
}
|
|
461
487
|
function toolDraft(block, parent) {
|
|
462
|
-
const
|
|
488
|
+
const tool5 = (block.name ?? "tool").replace(MCP_PREFIX, "");
|
|
463
489
|
const fields = fieldsOf(block.input);
|
|
464
|
-
const base = { tool:
|
|
465
|
-
if (
|
|
490
|
+
const base = { tool: tool5, ref: block.id ?? null, parent, failed: false };
|
|
491
|
+
if (tool5 === "Skill" && textField(fields, "skill")) {
|
|
466
492
|
return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
|
|
467
493
|
}
|
|
468
|
-
if (HELPER_TOOLS2.has(
|
|
494
|
+
if (HELPER_TOOLS2.has(tool5)) {
|
|
469
495
|
const who = textField(fields, "subagent_type") ?? "general-purpose";
|
|
470
496
|
const what = textField(fields, "description");
|
|
471
497
|
return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
|
|
472
498
|
}
|
|
473
|
-
if (
|
|
499
|
+
if (tool5 === "submit_verdict") {
|
|
474
500
|
const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
|
|
475
501
|
return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
476
502
|
}
|
|
477
|
-
if (
|
|
503
|
+
if (tool5 === "hand_off") {
|
|
478
504
|
return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
479
505
|
}
|
|
480
|
-
if (
|
|
481
|
-
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[
|
|
506
|
+
if (tool5 in ATTACH_LABELS) {
|
|
507
|
+
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool5]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
482
508
|
}
|
|
483
509
|
const command = textField(fields, "command");
|
|
484
|
-
const summary =
|
|
510
|
+
const summary = tool5 === "Bash" && command ? `$ ${command}` : activityLine(tool5, block.input);
|
|
485
511
|
return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
|
|
486
512
|
}
|
|
487
513
|
function resultText(content) {
|
|
@@ -493,6 +519,10 @@ function resultText(content) {
|
|
|
493
519
|
}
|
|
494
520
|
return content.map((part) => part.type === "text" ? part.text ?? "" : part.type === "image" ? "[image]" : "").filter(Boolean).join("\n");
|
|
495
521
|
}
|
|
522
|
+
function helperEndDraft(ref, parent, end, text2) {
|
|
523
|
+
const stopped = end === "stopped";
|
|
524
|
+
return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
|
|
525
|
+
}
|
|
496
526
|
function feedOf(message) {
|
|
497
527
|
if (message.type !== "assistant" && message.type !== "user") {
|
|
498
528
|
return [];
|
|
@@ -561,6 +591,77 @@ function chooseHandoff(targets, request) {
|
|
|
561
591
|
return { handoff: { toColumnId: edge.columnId, outcome: edge.outcome, note } };
|
|
562
592
|
}
|
|
563
593
|
|
|
594
|
+
// src/helpers.ts
|
|
595
|
+
function helperTasks() {
|
|
596
|
+
const byRef = /* @__PURE__ */ new Map();
|
|
597
|
+
const refByTask = /* @__PURE__ */ new Map();
|
|
598
|
+
const parents = /* @__PURE__ */ new Map();
|
|
599
|
+
function observe(message) {
|
|
600
|
+
if (message.type !== "system") {
|
|
601
|
+
return [];
|
|
602
|
+
}
|
|
603
|
+
if (message.subtype === "task_started" && message.task_type === "local_agent" && message.tool_use_id) {
|
|
604
|
+
byRef.set(message.tool_use_id, { taskId: message.task_id, background: message.is_backgrounded === true, stopped: false, over: false });
|
|
605
|
+
refByTask.set(message.task_id, message.tool_use_id);
|
|
606
|
+
return [];
|
|
607
|
+
}
|
|
608
|
+
if (message.subtype === "task_updated" && message.patch.is_backgrounded !== void 0) {
|
|
609
|
+
const ref2 = refByTask.get(message.task_id);
|
|
610
|
+
const task2 = ref2 ? byRef.get(ref2) : void 0;
|
|
611
|
+
if (task2) {
|
|
612
|
+
task2.background = message.patch.is_backgrounded;
|
|
613
|
+
}
|
|
614
|
+
return [];
|
|
615
|
+
}
|
|
616
|
+
if (message.subtype !== "task_notification") {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
const ref = message.tool_use_id ?? refByTask.get(message.task_id);
|
|
620
|
+
const task = ref ? byRef.get(ref) : void 0;
|
|
621
|
+
if (!ref || !task || task.over) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
task.stopped = message.status === "stopped";
|
|
625
|
+
if (!task.background) {
|
|
626
|
+
return [];
|
|
627
|
+
}
|
|
628
|
+
task.over = true;
|
|
629
|
+
return [helperEndDraft(ref, parents.get(ref) ?? null, message.status, message.summary)];
|
|
630
|
+
}
|
|
631
|
+
function adjust(draft) {
|
|
632
|
+
if (draft.kind === "agent" && draft.ref) {
|
|
633
|
+
parents.set(draft.ref, draft.parent);
|
|
634
|
+
}
|
|
635
|
+
const task = draft.kind === "result" && draft.ref ? byRef.get(draft.ref) : void 0;
|
|
636
|
+
if (!task || !draft.ref) {
|
|
637
|
+
return [draft];
|
|
638
|
+
}
|
|
639
|
+
if (task.background) {
|
|
640
|
+
return [];
|
|
641
|
+
}
|
|
642
|
+
task.over = true;
|
|
643
|
+
return task.stopped ? [helperEndDraft(draft.ref, draft.parent, "stopped", "")] : [draft];
|
|
644
|
+
}
|
|
645
|
+
async function stop(target, ref) {
|
|
646
|
+
const task = byRef.get(ref);
|
|
647
|
+
if (!task) {
|
|
648
|
+
return "unknown";
|
|
649
|
+
}
|
|
650
|
+
if (task.over || task.stopped) {
|
|
651
|
+
return "finished";
|
|
652
|
+
}
|
|
653
|
+
await target.stopTask(task.taskId);
|
|
654
|
+
return "stopped";
|
|
655
|
+
}
|
|
656
|
+
return { observe, adjust, stop };
|
|
657
|
+
}
|
|
658
|
+
function helperStoppedNote(stop) {
|
|
659
|
+
return `${stop.byName} stopped your helper ${stop.name} on TAMsense. Go on without it and say in your result what it did not finish. Start it again only if a person asks.`;
|
|
660
|
+
}
|
|
661
|
+
function helperStopRequest(stop) {
|
|
662
|
+
return `${stop.byName} asked on TAMsense to stop your helper ${stop.name}. Stop it now, go on without it and say in your result what it did not finish. Start it again only if a person asks.`;
|
|
663
|
+
}
|
|
664
|
+
|
|
564
665
|
// src/input.ts
|
|
565
666
|
var DEFAULT_MODEL = "default";
|
|
566
667
|
var QUIET_MS = 1e3;
|
|
@@ -571,8 +672,11 @@ function userMessage(text2) {
|
|
|
571
672
|
function humanText(message) {
|
|
572
673
|
return `Message from ${message.authorName}: ${message.text}`;
|
|
573
674
|
}
|
|
675
|
+
function priorityOf(mode) {
|
|
676
|
+
return mode === "steer" ? "now" : "next";
|
|
677
|
+
}
|
|
574
678
|
function humanMessage(message) {
|
|
575
|
-
return { ...userMessage(humanText(message)), priority: message.mode
|
|
679
|
+
return { ...userMessage(humanText(message)), priority: priorityOf(message.mode), client_composed: true };
|
|
576
680
|
}
|
|
577
681
|
function switchLine(change) {
|
|
578
682
|
const model = change.model === DEFAULT_MODEL ? "the default model" : change.model;
|
|
@@ -601,6 +705,7 @@ function inputChannel(options = {}) {
|
|
|
601
705
|
let wake = null;
|
|
602
706
|
let timers = [];
|
|
603
707
|
let applySwitch2 = null;
|
|
708
|
+
let applyStop = null;
|
|
604
709
|
let waitingSwitch = null;
|
|
605
710
|
function wakeUp() {
|
|
606
711
|
wake?.();
|
|
@@ -653,8 +758,8 @@ function inputChannel(options = {}) {
|
|
|
653
758
|
}
|
|
654
759
|
const next = queue.shift();
|
|
655
760
|
if (!next) {
|
|
656
|
-
await new Promise((
|
|
657
|
-
wake =
|
|
761
|
+
await new Promise((resolve3) => {
|
|
762
|
+
wake = resolve3;
|
|
658
763
|
});
|
|
659
764
|
continue;
|
|
660
765
|
}
|
|
@@ -714,11 +819,14 @@ function inputChannel(options = {}) {
|
|
|
714
819
|
onSwitch(apply) {
|
|
715
820
|
applySwitch2 = apply;
|
|
716
821
|
if (waitingSwitch) {
|
|
717
|
-
const { change, resolve:
|
|
822
|
+
const { change, resolve: resolve3 } = waitingSwitch;
|
|
718
823
|
waitingSwitch = null;
|
|
719
|
-
void apply(change).then(() =>
|
|
824
|
+
void apply(change).then(() => resolve3(true), () => resolve3(false));
|
|
720
825
|
}
|
|
721
826
|
},
|
|
827
|
+
onStopHelper(stop) {
|
|
828
|
+
applyStop = stop;
|
|
829
|
+
},
|
|
722
830
|
close,
|
|
723
831
|
push(message) {
|
|
724
832
|
if (closed || seen.has(message.id)) {
|
|
@@ -729,6 +837,17 @@ function inputChannel(options = {}) {
|
|
|
729
837
|
wakeUp();
|
|
730
838
|
return true;
|
|
731
839
|
},
|
|
840
|
+
note(text2, mode) {
|
|
841
|
+
if (closed) {
|
|
842
|
+
return false;
|
|
843
|
+
}
|
|
844
|
+
queue.push({ ids: [], message: { ...userMessage(text2), priority: priorityOf(mode) } });
|
|
845
|
+
wakeUp();
|
|
846
|
+
return true;
|
|
847
|
+
},
|
|
848
|
+
async stopHelper(ref) {
|
|
849
|
+
return closed || !applyStop ? "unknown" : await applyStop(ref);
|
|
850
|
+
},
|
|
732
851
|
async checking(work2) {
|
|
733
852
|
const epoch = idle ? idleEpoch : null;
|
|
734
853
|
inFlight += 1;
|
|
@@ -763,8 +882,8 @@ function inputChannel(options = {}) {
|
|
|
763
882
|
const earlier = waitingSwitch;
|
|
764
883
|
earlier?.resolve(false);
|
|
765
884
|
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:
|
|
885
|
+
return new Promise((resolve3) => {
|
|
886
|
+
waitingSwitch = { change: merged, resolve: resolve3 };
|
|
768
887
|
});
|
|
769
888
|
}
|
|
770
889
|
};
|
|
@@ -927,8 +1046,8 @@ function messageOf2(error) {
|
|
|
927
1046
|
async function decisionOf(tools, first, signal) {
|
|
928
1047
|
const now = tools.now ?? Date.now;
|
|
929
1048
|
const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
|
|
930
|
-
let
|
|
931
|
-
while (
|
|
1049
|
+
let reply3 = first;
|
|
1050
|
+
while (reply3.decision === "wait") {
|
|
932
1051
|
await tools.sleep(CHECK_INTERVAL_MS, signal);
|
|
933
1052
|
if (signal.aborted) {
|
|
934
1053
|
return STOPPED_TEXT2;
|
|
@@ -936,9 +1055,9 @@ async function decisionOf(tools, first, signal) {
|
|
|
936
1055
|
if (now() > giveUpAt) {
|
|
937
1056
|
return SITE_SILENT_TEXT;
|
|
938
1057
|
}
|
|
939
|
-
|
|
1058
|
+
reply3 = await tools.check(reply3.id).catch(() => reply3);
|
|
940
1059
|
}
|
|
941
|
-
return
|
|
1060
|
+
return reply3;
|
|
942
1061
|
}
|
|
943
1062
|
function askPermission(tools, lastWords, folder) {
|
|
944
1063
|
return async (toolName, input, options) => {
|
|
@@ -959,15 +1078,203 @@ function askPermission(tools, lastWords, folder) {
|
|
|
959
1078
|
} catch (error) {
|
|
960
1079
|
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
1080
|
}
|
|
962
|
-
const
|
|
963
|
-
if (typeof
|
|
964
|
-
return deny(
|
|
1081
|
+
const reply3 = await decisionOf(tools, first, options.signal);
|
|
1082
|
+
if (typeof reply3 === "string") {
|
|
1083
|
+
return deny(reply3);
|
|
965
1084
|
}
|
|
966
|
-
return
|
|
1085
|
+
return reply3.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply3.message);
|
|
967
1086
|
};
|
|
968
1087
|
}
|
|
969
1088
|
var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
|
|
970
1089
|
|
|
1090
|
+
// src/results.ts
|
|
1091
|
+
import { mkdir as mkdir2, readFile as readFile2, rm as rm2, stat, writeFile as writeFile2 } from "fs/promises";
|
|
1092
|
+
import { basename, extname, isAbsolute, join as join2, resolve } from "path";
|
|
1093
|
+
import { tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
1094
|
+
import { z as z2 } from "zod";
|
|
1095
|
+
var SAVE_DOCUMENT_TOOL_NAME = "save_result_document";
|
|
1096
|
+
var SAVE_TABLE_TOOL_NAME = "save_result_table";
|
|
1097
|
+
var ATTACH_FILE_TOOL_NAME = "attach_result_file";
|
|
1098
|
+
var ADD_LINK_TOOL_NAME = "add_result_link";
|
|
1099
|
+
var HAND_OVER_TOOL_NAME = "hand_over";
|
|
1100
|
+
var RESULTS_FOLDER = "results";
|
|
1101
|
+
var MAX_NAME_LENGTH = 60;
|
|
1102
|
+
var TYPES_BY_EXTENSION = {
|
|
1103
|
+
".pdf": "application/pdf",
|
|
1104
|
+
".png": "image/png",
|
|
1105
|
+
".jpg": "image/jpeg",
|
|
1106
|
+
".jpeg": "image/jpeg",
|
|
1107
|
+
".webp": "image/webp",
|
|
1108
|
+
".gif": "image/gif",
|
|
1109
|
+
".csv": "text/csv",
|
|
1110
|
+
".txt": "text/plain",
|
|
1111
|
+
".md": "text/markdown",
|
|
1112
|
+
".json": "application/json",
|
|
1113
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1114
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1115
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1116
|
+
".zip": "application/zip"
|
|
1117
|
+
};
|
|
1118
|
+
var EXTENSIONS = { document: ".md", table: ".csv", file: "", link: "" };
|
|
1119
|
+
function isFolderJob(job) {
|
|
1120
|
+
return job.workspace === "folder";
|
|
1121
|
+
}
|
|
1122
|
+
function resultTools(job, api, root) {
|
|
1123
|
+
if (!isFolderJob(job) || job.stage.kind !== "code") {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
root,
|
|
1128
|
+
save: (body) => api.request("POST", runPath(job.runId, "results"), body),
|
|
1129
|
+
attach: (body) => api.request("POST", runPath(job.runId, "result-files"), body)
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
function handOverTools(job, api) {
|
|
1133
|
+
const boards = job.handOverBoards ?? [];
|
|
1134
|
+
if (boards.length === 0 || job.stage.kind !== "code") {
|
|
1135
|
+
return null;
|
|
1136
|
+
}
|
|
1137
|
+
return { boards, send: (body) => api.request("POST", runPath(job.runId, "hand-over"), body) };
|
|
1138
|
+
}
|
|
1139
|
+
function resultFileName(result, taken) {
|
|
1140
|
+
const stem = (result.kind === "file" && result.fileName ? result.fileName.replace(/\.[^.]*$/, "") : result.title).normalize("NFKC").replace(/[^\p{L}\p{N}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, MAX_NAME_LENGTH) || "result";
|
|
1141
|
+
const extension = result.kind === "file" ? extname(result.fileName ?? "") : EXTENSIONS[result.kind];
|
|
1142
|
+
let name = `${stem}${extension}`;
|
|
1143
|
+
for (let index = 2; taken.has(name.toLowerCase()); index += 1) {
|
|
1144
|
+
name = `${stem}-${index}${extension}`;
|
|
1145
|
+
}
|
|
1146
|
+
taken.add(name.toLowerCase());
|
|
1147
|
+
return name;
|
|
1148
|
+
}
|
|
1149
|
+
function linksText(links) {
|
|
1150
|
+
return links.map((link) => `- ${link.title}: ${link.url ?? ""}`).join("\n");
|
|
1151
|
+
}
|
|
1152
|
+
async function writeResults(folder, job, api) {
|
|
1153
|
+
const results = job.results ?? [];
|
|
1154
|
+
await rm2(folder, { recursive: true, force: true });
|
|
1155
|
+
if (results.length === 0) {
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
await mkdir2(folder, { recursive: true });
|
|
1159
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1160
|
+
const lines = [];
|
|
1161
|
+
const missing = [];
|
|
1162
|
+
for (const result of results.filter((item) => item.kind !== "link")) {
|
|
1163
|
+
const name = resultFileName(result, taken);
|
|
1164
|
+
if (result.kind === "file") {
|
|
1165
|
+
try {
|
|
1166
|
+
const file = await api.request("GET", resultFilePath(job.runId, result.id));
|
|
1167
|
+
await writeFile2(join2(folder, name), Buffer.from(file.data, "base64"));
|
|
1168
|
+
} catch (error) {
|
|
1169
|
+
missing.push(`${result.title}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
} else {
|
|
1173
|
+
await writeFile2(join2(folder, name), result.content ?? "");
|
|
1174
|
+
}
|
|
1175
|
+
lines.push(`${RESULTS_FOLDER}/${name}: ${result.title}${result.version ? `, version ${result.version}` : ""}`);
|
|
1176
|
+
}
|
|
1177
|
+
const links = results.filter((item) => item.kind === "link");
|
|
1178
|
+
if (links.length > 0) {
|
|
1179
|
+
await writeFile2(join2(folder, "links.md"), `${linksText(links)}
|
|
1180
|
+
`);
|
|
1181
|
+
lines.push(`${RESULTS_FOLDER}/links.md: ${links.length} link(s)`);
|
|
1182
|
+
}
|
|
1183
|
+
if (missing.length > 0) {
|
|
1184
|
+
await writeFile2(join2(folder, "notes.md"), `These files could not be downloaded:
|
|
1185
|
+
${missing.map((line) => `- ${line}`).join("\n")}
|
|
1186
|
+
`);
|
|
1187
|
+
}
|
|
1188
|
+
return lines;
|
|
1189
|
+
}
|
|
1190
|
+
async function prepareFolder(job, root, api) {
|
|
1191
|
+
if (!job.keepWorkspace) {
|
|
1192
|
+
await rm2(root, { recursive: true, force: true });
|
|
1193
|
+
}
|
|
1194
|
+
await mkdir2(root, { recursive: true });
|
|
1195
|
+
return { root, repositories: [], results: await writeResults(join2(root, RESULTS_FOLDER), job, api) };
|
|
1196
|
+
}
|
|
1197
|
+
async function readResultFile(root, path, title) {
|
|
1198
|
+
const full = isAbsolute(path) ? path : resolve(root, path);
|
|
1199
|
+
const info = await stat(full).catch(() => null);
|
|
1200
|
+
if (!info?.isFile()) {
|
|
1201
|
+
return `There is no file at ${full}.`;
|
|
1202
|
+
}
|
|
1203
|
+
if (info.size > MAX_RESULT_FILE_BYTES) {
|
|
1204
|
+
return `The file is larger than ${MAX_RESULT_FILE_BYTES / 1024 / 1024} MB. Attach a smaller one or split it.`;
|
|
1205
|
+
}
|
|
1206
|
+
const name = basename(full);
|
|
1207
|
+
return { name, title: title.trim() || name, contentType: TYPES_BY_EXTENSION[extname(full).toLowerCase()] ?? "application/octet-stream", data: (await readFile2(full)).toString("base64") };
|
|
1208
|
+
}
|
|
1209
|
+
function reply(text2, isError = false) {
|
|
1210
|
+
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
1211
|
+
}
|
|
1212
|
+
async function sent(send) {
|
|
1213
|
+
try {
|
|
1214
|
+
return reply((await send()).text);
|
|
1215
|
+
} catch (error) {
|
|
1216
|
+
return reply(`Could not save it on TAMsense: ${error instanceof Error ? error.message : String(error)}. Try again once; if it fails again, say so in your report.`, true);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
function resultToolset(tools) {
|
|
1220
|
+
return [
|
|
1221
|
+
tool2(
|
|
1222
|
+
SAVE_DOCUMENT_TOOL_NAME,
|
|
1223
|
+
"Save a document as the result of this step on the TAMsense task: Markdown text such as a letter, a reply, a brief or a report. Saving again with the same title makes a new version of that document; every version is kept and people compare them.",
|
|
1224
|
+
{
|
|
1225
|
+
title: z2.string().describe("short name of the document, the same for every version of it"),
|
|
1226
|
+
content: z2.string().describe("the full text in Markdown, never a diff or a part"),
|
|
1227
|
+
note: z2.string().describe('one line on what this version is or what changed: "First draft", "Fixed the prices after the check"')
|
|
1228
|
+
},
|
|
1229
|
+
async (args) => await sent(() => tools.save({ kind: "document", ...args }))
|
|
1230
|
+
),
|
|
1231
|
+
tool2(
|
|
1232
|
+
SAVE_TABLE_TOOL_NAME,
|
|
1233
|
+
"Save a table as the result of this step, as CSV with a header row. Saving again with the same title makes a new version of that table.",
|
|
1234
|
+
{
|
|
1235
|
+
title: z2.string().describe("short name of the table, the same for every version of it"),
|
|
1236
|
+
csv: z2.string().describe("the full table as CSV, the first row holds the column names"),
|
|
1237
|
+
note: z2.string().describe("one line on what this version is or what changed")
|
|
1238
|
+
},
|
|
1239
|
+
async (args) => await sent(() => tools.save({ kind: "table", title: args.title, content: args.csv, note: args.note }))
|
|
1240
|
+
),
|
|
1241
|
+
tool2(
|
|
1242
|
+
ATTACH_FILE_TOOL_NAME,
|
|
1243
|
+
`Attach a file you made in the task folder to the TAMsense task as part of the result: a PDF, a spreadsheet, an image. Up to ${MAX_RESULT_FILE_BYTES / 1024 / 1024} MB.`,
|
|
1244
|
+
{
|
|
1245
|
+
path: z2.string().describe("path of the file, from the task folder or absolute"),
|
|
1246
|
+
title: z2.string().describe("what the file is, in a few words")
|
|
1247
|
+
},
|
|
1248
|
+
async (args) => {
|
|
1249
|
+
const body = await readResultFile(tools.root, args.path, args.title);
|
|
1250
|
+
return typeof body === "string" ? reply(body, true) : await sent(() => tools.attach(body));
|
|
1251
|
+
}
|
|
1252
|
+
),
|
|
1253
|
+
tool2(
|
|
1254
|
+
ADD_LINK_TOOL_NAME,
|
|
1255
|
+
"Attach a link to the TAMsense task as part of the result: a page or a file the result relies on.",
|
|
1256
|
+
{
|
|
1257
|
+
title: z2.string().describe("what the link is, in a few words"),
|
|
1258
|
+
url: z2.string().describe("the full address starting with https://")
|
|
1259
|
+
},
|
|
1260
|
+
async (args) => await sent(() => tools.save({ kind: "link", ...args }))
|
|
1261
|
+
)
|
|
1262
|
+
];
|
|
1263
|
+
}
|
|
1264
|
+
function handOverTool(tools) {
|
|
1265
|
+
return tool2(
|
|
1266
|
+
HAND_OVER_TOOL_NAME,
|
|
1267
|
+
`Hand the task to another board of the project as a linked task, when part of the work belongs to that board. Boards you may hand to: ${tools.boards.map((board) => board.name).join(", ")}. Without autopilot a person confirms it first.`,
|
|
1268
|
+
{
|
|
1269
|
+
board: z2.string().describe("name of the board"),
|
|
1270
|
+
title: z2.string().describe("title of the new task on that board, one plain sentence"),
|
|
1271
|
+
note: z2.string().describe("what you found and what that board should do; never names, emails or other personal data of clients"),
|
|
1272
|
+
wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
|
|
1273
|
+
},
|
|
1274
|
+
async (args) => await sent(() => tools.send(args))
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
971
1278
|
// src/specText.ts
|
|
972
1279
|
function isRecord(value) {
|
|
973
1280
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1020,10 +1327,31 @@ var RISK_LEVELS = ["low", "medium", "high"];
|
|
|
1020
1327
|
function present(lines) {
|
|
1021
1328
|
return lines.filter((line) => line !== null);
|
|
1022
1329
|
}
|
|
1330
|
+
function decisionLines(spec) {
|
|
1331
|
+
return records(spec.decisions).filter((item) => text(item.question) && text(item.answer)).map((item) => `- ${text(item.question)} \u2192 ${text(item.answer)} (${item.by === "agent" ? "agent's call" : "owner"})`);
|
|
1332
|
+
}
|
|
1333
|
+
function briefPromptText(brief) {
|
|
1334
|
+
return [
|
|
1335
|
+
text(brief.goal) && `Goal: ${text(brief.goal)}`,
|
|
1336
|
+
text(brief.context) && `Summary: ${text(brief.context)}`,
|
|
1337
|
+
section("The result must contain:", texts(brief.contains).map((item) => `- ${item}`)),
|
|
1338
|
+
section("Acceptance criteria:", present(Array.isArray(brief.acceptance) ? brief.acceptance.map(criterionLine) : [])),
|
|
1339
|
+
section("Constraints:", texts(brief.constraints).map((item) => `- ${item}`)),
|
|
1340
|
+
section("Out of scope (do not make):", texts(brief.outOfScope).map((item) => `- ${item}`)),
|
|
1341
|
+
section("Sources to use:", records(brief.sources).filter((item) => text(item.source)).map((item) => `- ${text(item.source)}${text(item.use) ? `: ${text(item.use)}` : ""}`)),
|
|
1342
|
+
text(brief.approver) && `Approved by: ${text(brief.approver)}`,
|
|
1343
|
+
section("Decisions:", decisionLines(brief)),
|
|
1344
|
+
section("Assumptions:", present(Array.isArray(brief.assumptions) ? brief.assumptions.map(assumptionLine) : [])),
|
|
1345
|
+
section("Open questions (not answered yet):", records(brief.openQuestions).filter((item) => text(item.question)).map((item) => `- ${text(item.question)}`))
|
|
1346
|
+
].filter(Boolean).join("\n\n");
|
|
1347
|
+
}
|
|
1023
1348
|
function specPromptText(spec) {
|
|
1024
1349
|
if (!isRecord(spec)) {
|
|
1025
1350
|
return null;
|
|
1026
1351
|
}
|
|
1352
|
+
if (spec.format === "brief") {
|
|
1353
|
+
return briefPromptText(spec);
|
|
1354
|
+
}
|
|
1027
1355
|
const structured = Array.isArray(spec.outOfScope) || isRecord(spec.testPlan);
|
|
1028
1356
|
const plan = isRecord(spec.testPlan) ? spec.testPlan : {};
|
|
1029
1357
|
const risks = records(spec.risks).filter((item) => text(item.risk));
|
|
@@ -1033,7 +1361,7 @@ function specPromptText(spec) {
|
|
|
1033
1361
|
text(spec.context) && `${structured ? "Summary" : "Context"}: ${text(spec.context)}`,
|
|
1034
1362
|
section("In scope:", texts(spec.inScope).map((item) => `- ${item}`)),
|
|
1035
1363
|
section("Out of scope (do not build):", texts(spec.outOfScope).map((item) => `- ${item}`)),
|
|
1036
|
-
section("Decisions:",
|
|
1364
|
+
section("Decisions:", decisionLines(spec)),
|
|
1037
1365
|
section("Code map:", records(spec.codeMap).filter((item) => text(item.file)).map((item) => `- ${text(item.repo) ? `${text(item.repo)} ` : ""}${text(item.file)}: ${text(item.role)}${CODE_USE_TEXT[item.use] ? ` [${CODE_USE_TEXT[item.use]}]` : ""}`)),
|
|
1038
1366
|
section("Changes, in order:", records(spec.changes).filter((item) => text(item.file)).flatMap(changeLines)),
|
|
1039
1367
|
section("Acceptance criteria:", present(Array.isArray(spec.acceptance) ? spec.acceptance.map(criterionLine) : [])),
|
|
@@ -1050,15 +1378,15 @@ function specPromptText(spec) {
|
|
|
1050
1378
|
|
|
1051
1379
|
// src/workspace.ts
|
|
1052
1380
|
import { existsSync as existsSync2 } from "fs";
|
|
1053
|
-
import { rm as
|
|
1054
|
-
import { join as
|
|
1381
|
+
import { rm as rm3 } from "fs/promises";
|
|
1382
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1055
1383
|
|
|
1056
1384
|
// src/git.ts
|
|
1057
|
-
import { Buffer } from "buffer";
|
|
1385
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
1058
1386
|
import { spawn as spawn2 } from "child_process";
|
|
1059
1387
|
var GITHUB_ORIGIN = "https://github.com/";
|
|
1060
1388
|
function githubAuthEnv(token) {
|
|
1061
|
-
const credentials =
|
|
1389
|
+
const credentials = Buffer2.from(`x-access-token:${token}`).toString("base64");
|
|
1062
1390
|
return {
|
|
1063
1391
|
GIT_CONFIG_COUNT: "1",
|
|
1064
1392
|
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
@@ -1068,7 +1396,7 @@ function githubAuthEnv(token) {
|
|
|
1068
1396
|
}
|
|
1069
1397
|
function runGit(args, options) {
|
|
1070
1398
|
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
1071
|
-
return new Promise((
|
|
1399
|
+
return new Promise((resolve3, reject) => {
|
|
1072
1400
|
const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1073
1401
|
let stdout = "";
|
|
1074
1402
|
let stderr = "";
|
|
@@ -1079,7 +1407,7 @@ function runGit(args, options) {
|
|
|
1079
1407
|
stderr += chunk.toString();
|
|
1080
1408
|
});
|
|
1081
1409
|
child.on("error", reject);
|
|
1082
|
-
child.on("close", (code) =>
|
|
1410
|
+
child.on("close", (code) => resolve3({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
1083
1411
|
});
|
|
1084
1412
|
}
|
|
1085
1413
|
async function git(args, options) {
|
|
@@ -1098,8 +1426,8 @@ function keyedLock() {
|
|
|
1098
1426
|
const previous = tails.get(key) ?? Promise.resolve();
|
|
1099
1427
|
let release = () => {
|
|
1100
1428
|
};
|
|
1101
|
-
const done2 = new Promise((
|
|
1102
|
-
release =
|
|
1429
|
+
const done2 = new Promise((resolve3) => {
|
|
1430
|
+
release = resolve3;
|
|
1103
1431
|
});
|
|
1104
1432
|
const tail = previous.then(() => done2);
|
|
1105
1433
|
tails.set(key, tail);
|
|
@@ -1123,7 +1451,7 @@ function githubRemoteUrl(fullName) {
|
|
|
1123
1451
|
return `https://github.com/${fullName}.git`;
|
|
1124
1452
|
}
|
|
1125
1453
|
function tamsenseFolder(home, ...parts) {
|
|
1126
|
-
return
|
|
1454
|
+
return join3(home, ".tamsense", ...parts);
|
|
1127
1455
|
}
|
|
1128
1456
|
function folderNames(repositories) {
|
|
1129
1457
|
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
@@ -1134,7 +1462,7 @@ function folderNames(repositories) {
|
|
|
1134
1462
|
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
1135
1463
|
}
|
|
1136
1464
|
async function refreshCache(cache, repository, token, tools) {
|
|
1137
|
-
if (existsSync2(
|
|
1465
|
+
if (existsSync2(join3(cache, ".git"))) {
|
|
1138
1466
|
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
1139
1467
|
return;
|
|
1140
1468
|
}
|
|
@@ -1142,7 +1470,7 @@ async function refreshCache(cache, repository, token, tools) {
|
|
|
1142
1470
|
}
|
|
1143
1471
|
async function removeWorktree(cache, path) {
|
|
1144
1472
|
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
1145
|
-
await
|
|
1473
|
+
await rm3(path, { recursive: true, force: true });
|
|
1146
1474
|
await git(["worktree", "prune"], { cwd: cache });
|
|
1147
1475
|
}
|
|
1148
1476
|
function stepFolder(job) {
|
|
@@ -1166,7 +1494,7 @@ function branchHolders(porcelain, branch) {
|
|
|
1166
1494
|
}
|
|
1167
1495
|
async function releaseBranch(cache, branch, keep) {
|
|
1168
1496
|
const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
|
|
1169
|
-
for (const path of branchHolders(listed.stdout, branch).filter((holder) =>
|
|
1497
|
+
for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve2(holder) !== resolve2(keep))) {
|
|
1170
1498
|
await removeWorktree(cache, path);
|
|
1171
1499
|
}
|
|
1172
1500
|
}
|
|
@@ -1175,7 +1503,7 @@ async function baseRef(cache, job, repository) {
|
|
|
1175
1503
|
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
1176
1504
|
}
|
|
1177
1505
|
async function keptBase(cache, path, job, repository) {
|
|
1178
|
-
if (!job.keepWorkspace || !existsSync2(
|
|
1506
|
+
if (!job.keepWorkspace || !existsSync2(join3(path, ".git"))) {
|
|
1179
1507
|
return null;
|
|
1180
1508
|
}
|
|
1181
1509
|
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
@@ -1184,14 +1512,17 @@ async function keptBase(cache, path, job, repository) {
|
|
|
1184
1512
|
}
|
|
1185
1513
|
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
1186
1514
|
}
|
|
1515
|
+
function taskFolder(home, job) {
|
|
1516
|
+
return tamsenseFolder(home, "work", ...stepFolder(job));
|
|
1517
|
+
}
|
|
1187
1518
|
async function prepareWorkspace(job, token, tools) {
|
|
1188
|
-
const root =
|
|
1519
|
+
const root = taskFolder(tools.home, job);
|
|
1189
1520
|
const folders = folderNames(job.repositories);
|
|
1190
1521
|
const repositories = [];
|
|
1191
1522
|
for (const [index, repository] of job.repositories.entries()) {
|
|
1192
1523
|
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
1193
1524
|
const folder = folders[index] ?? repository.fullName;
|
|
1194
|
-
const path =
|
|
1525
|
+
const path = join3(root, folder);
|
|
1195
1526
|
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
1196
1527
|
await refreshCache(cache, repository, token, tools);
|
|
1197
1528
|
const kept = await keptBase(cache, path, job, repository);
|
|
@@ -1270,6 +1601,11 @@ var CODE_RULES = [
|
|
|
1270
1601
|
];
|
|
1271
1602
|
var REWORK_RULE = "This is a rework. The branch already has the earlier work and maybe the reviewer's own commits: treat the current branch as the source of truth, keep the reviewer's changes unless a comment asks otherwise, and address every comment.";
|
|
1272
1603
|
var CODE_REPORT_RULE = "End with a short report for the reviewer: what you changed, how you checked it, and anything left open.";
|
|
1604
|
+
var READ_ONLY_CODE_RULES = [
|
|
1605
|
+
"Your role on this step only reads. Do the work the task asks for by researching and running what helps, then report. Do not change, commit or push product code: nothing from this run is pushed. You may create throwaway files outside the repositories.",
|
|
1606
|
+
"Check what you report with the project's own tools where they exist, and say how you checked it."
|
|
1607
|
+
];
|
|
1608
|
+
var READ_ONLY_REPORT_RULE = "End with a short report: what you found, how you checked it, and what is left open.";
|
|
1273
1609
|
var CHECK_RULES = [
|
|
1274
1610
|
"The work to check is on the current branch. See it with git diff against the base branch named for each repository below, and read the code around it.",
|
|
1275
1611
|
"Do not change, commit or push product code. You may run anything and create throwaway files outside the repositories.",
|
|
@@ -1280,6 +1616,21 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
|
|
|
1280
1616
|
var NAMING_RULE = 'Name everything you attach (screenshots, replays, videos) by what it shows and why it was made: a short title such as "Design option 1", "Feature testing" or "Feature testing, attempt 2", and a one-line purpose such as "Comparing two layouts for the empty orders state". Number design variants, name test runs by their goal and attempt, and never use generic names like "screenshot1".';
|
|
1281
1617
|
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.`;
|
|
1282
1618
|
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.";
|
|
1619
|
+
var FOLDER_RULE = "This board works without code: there are no repositories. The current folder belongs to this task: work in it and keep your throwaway files here.";
|
|
1620
|
+
var RESULT_HOME_RULE = "The result lives only on the TAMsense task: never save it in outside services such as Google Drive or a shared folder. Use connected services only to read. Sending anything to real people, such as an email or a message, is an action that needs approval like any other.";
|
|
1621
|
+
var FOLDER_DO_RULES = [
|
|
1622
|
+
`Save what you make on the task with the TAMsense tools: ${SAVE_DOCUMENT_TOOL_NAME} for text in Markdown, ${SAVE_TABLE_TOOL_NAME} for rows of data as CSV, ${ATTACH_FILE_TOOL_NAME} for files you made in this folder, ${ADD_LINK_TOOL_NAME} for pages the result relies on. Keep one title per document and save every change as a new version of it.`,
|
|
1623
|
+
"Use only facts from the task, the notes, the earlier results and what you read yourself; never invent numbers, names or dates.",
|
|
1624
|
+
RESULT_HOME_RULE
|
|
1625
|
+
];
|
|
1626
|
+
var FOLDER_REWORK_RULE = "The task came back with notes. Read the earlier results in the results folder, fix what the notes ask and save new versions of the same documents.";
|
|
1627
|
+
var FOLDER_REPORT_RULE = "End with a short report for the people on the task: what you made, what you decided yourself and what is still open.";
|
|
1628
|
+
var FOLDER_CHECK_RULES = [
|
|
1629
|
+
`The result to check is in the ${RESULTS_FOLDER} folder: the latest version of each document (.md) and table (.csv), the files and links.md. Read all of it.`,
|
|
1630
|
+
`Do not change the result. You may create throwaway files outside the ${RESULTS_FOLDER} folder.`,
|
|
1631
|
+
"Judge it against the task, the notes of earlier steps and the instructions of this step. Be specific: every problem names the document, the place in it and what to change.",
|
|
1632
|
+
`Finish by calling the ${VERDICT_TOOL_NAME} tool exactly once: passed is true only if nothing must be fixed; report says what you checked and, if it failed, exactly what to fix.`
|
|
1633
|
+
];
|
|
1283
1634
|
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.";
|
|
1284
1635
|
function composeProjectName(job) {
|
|
1285
1636
|
return ["tamsense", ...stepFolder(job)].join("-");
|
|
@@ -1302,6 +1653,10 @@ var STAGE_GOALS = {
|
|
|
1302
1653
|
review: "Review the code for",
|
|
1303
1654
|
test: "Test"
|
|
1304
1655
|
};
|
|
1656
|
+
var READ_ONLY_GOAL = "Work on";
|
|
1657
|
+
function isReadOnlyCode(stage) {
|
|
1658
|
+
return stage.kind === "code" && stage.role?.access === "read";
|
|
1659
|
+
}
|
|
1305
1660
|
var SPEC_UNATTENDED_RULE = `You are preparing a TAMsense task on the user's own computer. Nobody answers you during this run: the owner answers your questions and reviews the spec on the task page after it. So never stop to wait for a reply and never ask in your messages: hand in what this stage asks for with the TAMsense tool, then finish. People on the task may send you messages during this run, each starting with "Message from <name>:". Follow them.`;
|
|
1306
1661
|
var READ_ONLY_RULE = "Read the code to research the task. Do not change, commit or push anything: nothing from this run is pushed, and the code step starts from a clean branch.";
|
|
1307
1662
|
var RESEARCH_RULE = "Start by loading the tamsense:task-interview skill with the Skill tool and follow it: it holds the question test, the corner sweep and the rules for rounds. Research before asking: read the code in the repositories here and the evidence of the linked action, if there is one. Finding facts is your job; the decisions about intent are the owner's.";
|
|
@@ -1319,12 +1674,29 @@ var SPEC_RULES = [
|
|
|
1319
1674
|
"Then reply with a short plain summary: the approach, the risk level and the open questions, if any. Do not paste the spec: the page shows it."
|
|
1320
1675
|
];
|
|
1321
1676
|
var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
|
|
1677
|
+
var BRIEF_UNATTENDED_RULE = `You are preparing a TAMsense task on the user's own computer. Nobody answers you during this run: the owner answers your questions and reviews the brief on the task page after it. So never stop to wait for a reply and never ask in your messages: hand in what this stage asks for with the TAMsense tool, then finish. People on the task may send you messages during this run, each starting with "Message from <name>:". Follow them.`;
|
|
1678
|
+
var BRIEF_RESEARCH_RULE = "Research before asking: read what the task, the project knowledge, the files in this folder and the connected services give you. Finding facts is your job; the decisions about intent are the owner's.";
|
|
1679
|
+
var RESEARCH_BRIEF_RULE = "Write the research brief as you go: findings (what the sources say that matters, each with where: the source), dataFacts (numbers and facts, each with its source; empty when none), approach (2 to 4 sentences), codeMap (always empty: this board has no code), assumptions (each corner you settled with a default, with the reason) and pendingDecisions (decisions that depend on an answer to this round; empty when none).";
|
|
1680
|
+
var RESULT_BRIEF_RULES = [
|
|
1681
|
+
'Write the brief of the result: goal, context (the summary), contains (what the result must contain), acceptance (criteria with ids AC-1, AC-2 and so on in order, each one observable property a checker confirms by reading the result), constraints, outOfScope, sources (where the facts come from: knowledge, linked items, files of the task, connected services that are read only), approver (who approves the result), decisions, assumptions and openQuestions. Set format to "brief". Never write a code map, files to touch or repositories.',
|
|
1682
|
+
'Start from the research brief and the conversation below. Apply the answers: an answer the owner gave is a decision with by: owner; an answer marked "Recommended option applied, not answered" is an assumption with the reason "not answered, the recommended option applies".',
|
|
1683
|
+
"Where the task, the answers and the project context are silent and a safe default exists, choose it and record it as an assumption with its reason. A point with no safe default that changes the result becomes an open question; keep them to 3 at most. Never invent numbers, names or dates.",
|
|
1684
|
+
`Hand in the FULL brief with ${SUBMIT_SPEC_TOOL_NAME}. TAMsense lints it; if it returns feedback, the brief was not saved: fix it and call ${SUBMIT_SPEC_TOOL_NAME} again. After two rounds of feedback the brief is saved as it is.`,
|
|
1685
|
+
"When the task already has a saved brief, the conversation holds the owner's feedback or answers to its open questions: apply them and hand in the full updated brief. An answered open question becomes a decision.",
|
|
1686
|
+
"Then reply with a short plain summary: what the result is and the open questions, if any. Do not paste the brief: the page shows it."
|
|
1687
|
+
];
|
|
1688
|
+
var BRIEF_STYLE_RULE = "Write the research brief, the questions, the brief and your reply in the language the task is written in. Replies are plain text, no Markdown. Use real names, numbers and sources; if you could not verify something, say so. No em dashes and no en dashes; use commas, colons or a plain hyphen.";
|
|
1322
1689
|
var SPEC_STYLE_RULE = "Write the brief, the questions, the spec and your reply in the language the task is written in. Replies are plain text, no Markdown. Use real file paths, function and table names; if you could not verify something, say so. No em dashes and no en dashes; use commas, colons or a plain hyphen.";
|
|
1323
1690
|
function specText(spec) {
|
|
1324
1691
|
const text2 = specPromptText(spec);
|
|
1325
1692
|
return text2 ? `Approved spec:
|
|
1326
1693
|
${text2}` : null;
|
|
1327
1694
|
}
|
|
1695
|
+
function briefText(spec) {
|
|
1696
|
+
const text2 = specPromptText(spec);
|
|
1697
|
+
return text2 ? `Approved brief:
|
|
1698
|
+
${text2}` : null;
|
|
1699
|
+
}
|
|
1328
1700
|
function contextText(context) {
|
|
1329
1701
|
return context.trim() ? `Project context from the team (follow it like the project's own rules):
|
|
1330
1702
|
${context.trim()}` : null;
|
|
@@ -1363,6 +1735,18 @@ function peopleText(people) {
|
|
|
1363
1735
|
function canProposeKnowledge(job) {
|
|
1364
1736
|
return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
|
|
1365
1737
|
}
|
|
1738
|
+
function folderText(root, results) {
|
|
1739
|
+
const lines = results ?? [];
|
|
1740
|
+
return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
|
|
1741
|
+
${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
|
|
1742
|
+
}
|
|
1743
|
+
function handOverText(boards) {
|
|
1744
|
+
const names = (boards ?? []).map((board) => board.name);
|
|
1745
|
+
if (names.length === 0) {
|
|
1746
|
+
return null;
|
|
1747
|
+
}
|
|
1748
|
+
return `When part of the work belongs to another board, hand it over with the ${HAND_OVER_TOOL_NAME} tool: ${names.join(", ")}. Write a plain title and a note on what you found and what that board should do; never put names, emails or other personal data of clients in the note. With wait true this task waits on its step until the linked task is done, then it goes on and you get its result note.`;
|
|
1749
|
+
}
|
|
1366
1750
|
function handoffText(targets, isCheck) {
|
|
1367
1751
|
if (targets.length === 0) {
|
|
1368
1752
|
return null;
|
|
@@ -1409,6 +1793,64 @@ function specGoal(job, work2) {
|
|
|
1409
1793
|
}
|
|
1410
1794
|
return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its spec: ${job.title}`;
|
|
1411
1795
|
}
|
|
1796
|
+
function briefQuestionRules(work2) {
|
|
1797
|
+
const handIn = `Call ${SUBMIT_QUESTIONS_TOOL_NAME} once with the research brief and the questions, or an empty list when none are needed. If it returns problems, fix them and call it again.`;
|
|
1798
|
+
if (work2.round === 1) {
|
|
1799
|
+
return [
|
|
1800
|
+
BRIEF_RESEARCH_RULE,
|
|
1801
|
+
RESEARCH_BRIEF_RULE,
|
|
1802
|
+
`Ask 0 to ${work2.maxQuestions} questions, ideally 2 or 3, the most important first, and only the ones research cannot settle, whose answers change the result, and where a wrong default is costly. Zero questions is a good result: the brief is then written right away.`,
|
|
1803
|
+
QUESTION_SHAPE_RULE,
|
|
1804
|
+
handIn,
|
|
1805
|
+
QUESTIONS_REPLY_RULE
|
|
1806
|
+
];
|
|
1807
|
+
}
|
|
1808
|
+
const taken = work2.takenIds.length > 0 ? ` These ids are taken by round 1: ${work2.takenIds.join(", ")}.` : "";
|
|
1809
|
+
return [
|
|
1810
|
+
`This is the follow-up round ${work2.round} of 2, and there is no round after it. Read the answers in the conversation below; do not research again what your research brief covers.`,
|
|
1811
|
+
`Ask at most ${work2.maxQuestions} more questions, and only when an answer is vague, contradicts another answer or the facts, or unlocked a decision from pendingDecisions. Otherwise hand in no questions: whatever is still open becomes an assumption.${taken}`,
|
|
1812
|
+
QUESTION_SHAPE_RULE,
|
|
1813
|
+
"Update the research brief with what the answers changed and leave pendingDecisions empty.",
|
|
1814
|
+
handIn,
|
|
1815
|
+
QUESTIONS_REPLY_RULE
|
|
1816
|
+
];
|
|
1817
|
+
}
|
|
1818
|
+
function briefGoal(job, work2) {
|
|
1819
|
+
if (work2.round === null) {
|
|
1820
|
+
return `Write the brief for TAMsense task #${job.taskId}: ${job.title}`;
|
|
1821
|
+
}
|
|
1822
|
+
return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its brief: ${job.title}`;
|
|
1823
|
+
}
|
|
1824
|
+
function buildBriefPrompt(job, work2, workspace) {
|
|
1825
|
+
const prompt = [
|
|
1826
|
+
briefGoal(job, work2),
|
|
1827
|
+
job.description,
|
|
1828
|
+
work2.linkedAction,
|
|
1829
|
+
work2.brief ? `Research brief from the clarification phase (verify only what you rely on):
|
|
1830
|
+
${JSON.stringify(work2.brief)}` : null,
|
|
1831
|
+
work2.round === null && job.spec ? `Current saved brief (hand in the full updated version):
|
|
1832
|
+
${JSON.stringify(job.spec)}` : null,
|
|
1833
|
+
job.handoffNote && `Note from another step: ${job.handoffNote}`,
|
|
1834
|
+
threadText(work2.thread),
|
|
1835
|
+
teamMessagesText(job.messages)
|
|
1836
|
+
].filter(Boolean).join("\n\n");
|
|
1837
|
+
const instructions = [
|
|
1838
|
+
BRIEF_UNATTENDED_RULE,
|
|
1839
|
+
MACHINE_RULE,
|
|
1840
|
+
workplaceText(job.computer, workspace.root),
|
|
1841
|
+
stepLine(job.stage),
|
|
1842
|
+
job.stage.instructions,
|
|
1843
|
+
contextText(job.projectContext),
|
|
1844
|
+
FOLDER_RULE,
|
|
1845
|
+
RESULT_HOME_RULE,
|
|
1846
|
+
`How to work:
|
|
1847
|
+
${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
|
|
1848
|
+
safetyText(job),
|
|
1849
|
+
knowledgeText(job.knowledge, job.computer, false),
|
|
1850
|
+
BRIEF_STYLE_RULE
|
|
1851
|
+
].filter(Boolean).join("\n\n");
|
|
1852
|
+
return { prompt, instructions };
|
|
1853
|
+
}
|
|
1412
1854
|
function buildSpecPrompt(job, work2, workspace) {
|
|
1413
1855
|
const prompt = [
|
|
1414
1856
|
specGoal(job, work2),
|
|
@@ -1440,19 +1882,63 @@ ${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
|
1440
1882
|
].filter(Boolean).join("\n\n");
|
|
1441
1883
|
return { prompt, instructions };
|
|
1442
1884
|
}
|
|
1885
|
+
var FOLDER_GOALS = {
|
|
1886
|
+
code: "Work on",
|
|
1887
|
+
review: "Check the result of",
|
|
1888
|
+
test: "Check the result of"
|
|
1889
|
+
};
|
|
1890
|
+
function buildFolderPrompt(job, workspace, options) {
|
|
1891
|
+
const kind = job.stage.kind === "review" || job.stage.kind === "test" ? job.stage.kind : "code";
|
|
1892
|
+
const isCheck = kind !== "code";
|
|
1893
|
+
const people = job.people ?? [];
|
|
1894
|
+
const canAsk = people.length > 0;
|
|
1895
|
+
const prompt = [
|
|
1896
|
+
`${FOLDER_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
1897
|
+
job.description,
|
|
1898
|
+
briefText(job.spec),
|
|
1899
|
+
job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
|
|
1900
|
+
job.review && `Notes to address:
|
|
1901
|
+
${job.review}`,
|
|
1902
|
+
teamMessagesText(job.messages)
|
|
1903
|
+
].filter(Boolean).join("\n\n");
|
|
1904
|
+
const rules = isCheck ? FOLDER_CHECK_RULES : [...FOLDER_DO_RULES, ...job.review ? [FOLDER_REWORK_RULE] : [], FOLDER_REPORT_RULE];
|
|
1905
|
+
const instructions = [
|
|
1906
|
+
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
1907
|
+
MACHINE_RULE,
|
|
1908
|
+
workplaceText(job.computer, workspace.root),
|
|
1909
|
+
stepLine(job.stage),
|
|
1910
|
+
job.stage.instructions,
|
|
1911
|
+
contextText(job.projectContext),
|
|
1912
|
+
`${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
|
|
1913
|
+
`How to work:
|
|
1914
|
+
${bulletList(rules)}`,
|
|
1915
|
+
safetyText(job),
|
|
1916
|
+
knowledgeText(job.knowledge, job.computer, canProposeKnowledge(job)),
|
|
1917
|
+
APPROVAL_RULE,
|
|
1918
|
+
peopleText(people),
|
|
1919
|
+
isCheck ? null : handOverText(job.handOverBoards),
|
|
1920
|
+
handoffText(job.allowedHandoffs, isCheck),
|
|
1921
|
+
options.browser && BROWSER_RULE
|
|
1922
|
+
].filter(Boolean).join("\n\n");
|
|
1923
|
+
return { prompt, instructions };
|
|
1924
|
+
}
|
|
1443
1925
|
function buildTaskPrompt(job, workspace, options = {}) {
|
|
1444
1926
|
const { kind } = job.stage;
|
|
1445
1927
|
if (kind === "interview" || kind === "spec") {
|
|
1446
1928
|
if (!job.specWork) {
|
|
1447
1929
|
throw new Error(SPEC_WORK_MISSING);
|
|
1448
1930
|
}
|
|
1449
|
-
return buildSpecPrompt(job, job.specWork, workspace);
|
|
1931
|
+
return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
|
|
1932
|
+
}
|
|
1933
|
+
if (isFolderJob(job)) {
|
|
1934
|
+
return buildFolderPrompt(job, workspace, options);
|
|
1450
1935
|
}
|
|
1451
1936
|
const isCheck = kind !== "code";
|
|
1937
|
+
const readOnly = isReadOnlyCode(job.stage);
|
|
1452
1938
|
const people = job.people ?? [];
|
|
1453
1939
|
const canAsk = people.length > 0;
|
|
1454
1940
|
const prompt = [
|
|
1455
|
-
`${STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
1941
|
+
`${readOnly ? READ_ONLY_GOAL : STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
1456
1942
|
job.description,
|
|
1457
1943
|
specText(job.spec),
|
|
1458
1944
|
job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
|
|
@@ -1462,7 +1948,7 @@ ${job.review}`,
|
|
|
1462
1948
|
].filter(Boolean).join("\n\n");
|
|
1463
1949
|
const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
|
|
1464
1950
|
const localApp = localAppRule(job, isCheck, canAsk);
|
|
1465
|
-
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
1951
|
+
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : readOnly ? [...READ_ONLY_CODE_RULES, DEPENDENCIES_RULE, localApp, ...evidence, READ_ONLY_REPORT_RULE] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
1466
1952
|
const instructions = [
|
|
1467
1953
|
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
1468
1954
|
MACHINE_RULE,
|
|
@@ -1485,108 +1971,135 @@ ${bulletList(rules)}`,
|
|
|
1485
1971
|
}
|
|
1486
1972
|
|
|
1487
1973
|
// src/specTools.ts
|
|
1488
|
-
import { tool as
|
|
1489
|
-
import { z as
|
|
1974
|
+
import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
1975
|
+
import { z as z3 } from "zod";
|
|
1490
1976
|
var MAX_FIRST_ROUND = 4;
|
|
1491
|
-
var questionSchema =
|
|
1492
|
-
id:
|
|
1493
|
-
question:
|
|
1494
|
-
whyItMatters:
|
|
1495
|
-
options:
|
|
1496
|
-
label:
|
|
1497
|
-
description:
|
|
1498
|
-
recommended:
|
|
1977
|
+
var questionSchema = z3.object({
|
|
1978
|
+
id: z3.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
|
|
1979
|
+
question: z3.string().describe("One plain sentence the owner can answer without reading code"),
|
|
1980
|
+
whyItMatters: z3.string().describe("One line: what changes in the result depending on the answer"),
|
|
1981
|
+
options: z3.array(z3.object({
|
|
1982
|
+
label: z3.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
|
|
1983
|
+
description: z3.string().describe("One sentence: what happens if this option is chosen"),
|
|
1984
|
+
recommended: z3.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
|
|
1499
1985
|
})).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'),
|
|
1500
|
-
multiSelect:
|
|
1986
|
+
multiSelect: z3.boolean().describe("true only when the options are additive and several can apply together")
|
|
1501
1987
|
});
|
|
1502
|
-
var briefSchema =
|
|
1503
|
-
findings:
|
|
1504
|
-
fact:
|
|
1505
|
-
where:
|
|
1988
|
+
var briefSchema = z3.object({
|
|
1989
|
+
findings: z3.array(z3.object({
|
|
1990
|
+
fact: z3.string().describe("What the code does today that matters for this task"),
|
|
1991
|
+
where: z3.string().describe("Where it is: owner/repo path:line")
|
|
1506
1992
|
})).describe("Findings from the code"),
|
|
1507
|
-
dataFacts:
|
|
1508
|
-
approach:
|
|
1509
|
-
codeMap:
|
|
1510
|
-
repo:
|
|
1511
|
-
file:
|
|
1512
|
-
role:
|
|
1993
|
+
dataFacts: z3.array(z3.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
|
|
1994
|
+
approach: z3.string().describe("The approach you would take, in 2-4 sentences"),
|
|
1995
|
+
codeMap: z3.array(z3.object({
|
|
1996
|
+
repo: z3.string().describe("Repository full name (owner/name)"),
|
|
1997
|
+
file: z3.string().describe("Path relative to the repository root"),
|
|
1998
|
+
role: z3.string().describe("Why it matters: what to change or reuse there")
|
|
1513
1999
|
})).describe("The files the change will touch or must reuse"),
|
|
1514
|
-
assumptions:
|
|
1515
|
-
text:
|
|
1516
|
-
reason:
|
|
2000
|
+
assumptions: z3.array(z3.object({
|
|
2001
|
+
text: z3.string().describe("The default you chose"),
|
|
2002
|
+
reason: z3.string().describe("Why it is safe or why it was chosen")
|
|
1517
2003
|
})).describe("Corners settled with a default instead of a question"),
|
|
1518
|
-
pendingDecisions:
|
|
2004
|
+
pendingDecisions: z3.array(z3.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
|
|
1519
2005
|
});
|
|
1520
|
-
var questionsPayloadSchema =
|
|
2006
|
+
var questionsPayloadSchema = z3.object({
|
|
1521
2007
|
brief: briefSchema,
|
|
1522
|
-
questions:
|
|
2008
|
+
questions: z3.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`)
|
|
1523
2009
|
});
|
|
1524
|
-
var specSchema =
|
|
1525
|
-
goal:
|
|
1526
|
-
context:
|
|
1527
|
-
inScope:
|
|
1528
|
-
outOfScope:
|
|
1529
|
-
decisions:
|
|
1530
|
-
question:
|
|
1531
|
-
answer:
|
|
1532
|
-
by:
|
|
2010
|
+
var specSchema = z3.object({
|
|
2011
|
+
goal: z3.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
|
|
2012
|
+
context: z3.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
|
|
2013
|
+
inScope: z3.array(z3.string()).describe("What this task delivers, one item each"),
|
|
2014
|
+
outOfScope: z3.array(z3.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"),
|
|
2015
|
+
decisions: z3.array(z3.object({
|
|
2016
|
+
question: z3.string(),
|
|
2017
|
+
answer: z3.string(),
|
|
2018
|
+
by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
1533
2019
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
1534
|
-
codeMap:
|
|
1535
|
-
repo:
|
|
1536
|
-
file:
|
|
1537
|
-
role:
|
|
1538
|
-
use:
|
|
2020
|
+
codeMap: z3.array(z3.object({
|
|
2021
|
+
repo: z3.string().describe("Repository full name (owner/name)"),
|
|
2022
|
+
file: z3.string().describe("Path relative to the repository root"),
|
|
2023
|
+
role: z3.string().describe("What the coder needs to know about this file"),
|
|
2024
|
+
use: z3.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
|
|
1539
2025
|
})).describe("The files a coder must read before starting"),
|
|
1540
|
-
changes:
|
|
1541
|
-
repo:
|
|
1542
|
-
file:
|
|
1543
|
-
action:
|
|
1544
|
-
description:
|
|
1545
|
-
produces:
|
|
1546
|
-
consumes:
|
|
1547
|
-
acceptance:
|
|
2026
|
+
changes: z3.array(z3.object({
|
|
2027
|
+
repo: z3.string().describe("Repository full name (owner/name) where the change happens"),
|
|
2028
|
+
file: z3.string().describe("Path to the file, relative to the repository root"),
|
|
2029
|
+
action: z3.enum(["create", "modify", "delete"]),
|
|
2030
|
+
description: z3.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
|
|
2031
|
+
produces: z3.array(z3.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
|
|
2032
|
+
consumes: z3.array(z3.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
|
|
2033
|
+
acceptance: z3.array(z3.string()).describe("Ids of the criteria this change makes true, like AC-1")
|
|
1548
2034
|
})).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
|
|
1549
|
-
acceptance:
|
|
1550
|
-
id:
|
|
1551
|
-
kind:
|
|
1552
|
-
text:
|
|
2035
|
+
acceptance: z3.array(z3.object({
|
|
2036
|
+
id: z3.string().describe("AC-1, AC-2 and so on, in order"),
|
|
2037
|
+
kind: z3.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
|
|
2038
|
+
text: z3.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
|
|
1553
2039
|
})).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
|
|
1554
|
-
testPlan:
|
|
1555
|
-
automated:
|
|
1556
|
-
repo:
|
|
1557
|
-
command:
|
|
1558
|
-
expect:
|
|
2040
|
+
testPlan: z3.object({
|
|
2041
|
+
automated: z3.array(z3.object({
|
|
2042
|
+
repo: z3.string(),
|
|
2043
|
+
command: z3.string().describe("The exact command to run"),
|
|
2044
|
+
expect: z3.string().describe("What passing looks like")
|
|
1559
2045
|
})).describe("At least one command"),
|
|
1560
|
-
newTests:
|
|
1561
|
-
seam:
|
|
1562
|
-
covers:
|
|
2046
|
+
newTests: z3.array(z3.object({
|
|
2047
|
+
seam: z3.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
|
|
2048
|
+
covers: z3.array(z3.string()).describe("Ids of the criteria it covers")
|
|
1563
2049
|
})),
|
|
1564
|
-
manual:
|
|
1565
|
-
page:
|
|
1566
|
-
steps:
|
|
2050
|
+
manual: z3.array(z3.object({
|
|
2051
|
+
page: z3.string().describe("The page or screen"),
|
|
2052
|
+
steps: z3.string().describe("What a person does and sees")
|
|
1567
2053
|
}))
|
|
1568
2054
|
}),
|
|
1569
|
-
risks:
|
|
1570
|
-
risk:
|
|
1571
|
-
mitigation:
|
|
2055
|
+
risks: z3.array(z3.object({
|
|
2056
|
+
risk: z3.string(),
|
|
2057
|
+
mitigation: z3.string()
|
|
1572
2058
|
})).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
|
|
1573
|
-
riskLevel:
|
|
1574
|
-
assumptions:
|
|
1575
|
-
text:
|
|
1576
|
-
reason:
|
|
2059
|
+
riskLevel: z3.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
|
|
2060
|
+
assumptions: z3.array(z3.object({
|
|
2061
|
+
text: z3.string().describe("The default you chose"),
|
|
2062
|
+
reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
1577
2063
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
1578
|
-
openQuestions:
|
|
1579
|
-
split:
|
|
2064
|
+
openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
|
|
2065
|
+
split: z3.array(z3.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")
|
|
1580
2066
|
});
|
|
1581
|
-
|
|
2067
|
+
var resultBriefSchema = z3.object({
|
|
2068
|
+
format: z3.literal("brief").describe('Always "brief"'),
|
|
2069
|
+
goal: z3.string().describe("What the work is for, for whom, and the outcome people will notice"),
|
|
2070
|
+
context: z3.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
|
|
2071
|
+
contains: z3.array(z3.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
|
|
2072
|
+
acceptance: z3.array(z3.object({
|
|
2073
|
+
id: z3.string().describe("AC-1, AC-2 and so on, in order"),
|
|
2074
|
+
text: z3.string().describe("One observable property of the result that a checker can confirm by reading it")
|
|
2075
|
+
})).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
|
|
2076
|
+
constraints: z3.array(z3.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
|
|
2077
|
+
outOfScope: z3.array(z3.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
|
|
2078
|
+
sources: z3.array(z3.object({
|
|
2079
|
+
source: z3.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
|
|
2080
|
+
use: z3.string().describe("What to take from it")
|
|
2081
|
+
})).describe("Sources the work must use. Empty when the task itself is enough"),
|
|
2082
|
+
approver: z3.string().describe("Who approves the result, as the task or the board names them"),
|
|
2083
|
+
decisions: z3.array(z3.object({
|
|
2084
|
+
question: z3.string(),
|
|
2085
|
+
answer: z3.string(),
|
|
2086
|
+
by: z3.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
2087
|
+
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
2088
|
+
assumptions: z3.array(z3.object({
|
|
2089
|
+
text: z3.string().describe("The default you chose"),
|
|
2090
|
+
reason: z3.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
2091
|
+
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
2092
|
+
openQuestions: z3.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
|
|
2093
|
+
});
|
|
2094
|
+
function reply2(text2, isError = false) {
|
|
1582
2095
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
1583
2096
|
}
|
|
1584
2097
|
function unreachable(error, toolName) {
|
|
1585
2098
|
const message = error instanceof Error ? error.message : String(error);
|
|
1586
|
-
return
|
|
2099
|
+
return reply2(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
|
|
1587
2100
|
}
|
|
1588
2101
|
function questionsTool(review, onSubmit) {
|
|
1589
|
-
return
|
|
2102
|
+
return tool3(
|
|
1590
2103
|
SUBMIT_QUESTIONS_TOOL_NAME,
|
|
1591
2104
|
"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.",
|
|
1592
2105
|
questionsPayloadSchema.shape,
|
|
@@ -1598,10 +2111,10 @@ function questionsTool(review, onSubmit) {
|
|
|
1598
2111
|
return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
|
|
1599
2112
|
}
|
|
1600
2113
|
if (!verdict.accepted) {
|
|
1601
|
-
return
|
|
2114
|
+
return reply2(verdict.feedback, true);
|
|
1602
2115
|
}
|
|
1603
2116
|
onSubmit(args);
|
|
1604
|
-
return
|
|
2117
|
+
return reply2(args.questions.length > 0 ? "Questions saved. The owner answers them on the task page after this run. Finish now with a reply of one or two sentences." : "Saved with no questions: the spec is written next. Finish now with a reply of one or two sentences.");
|
|
1605
2118
|
}
|
|
1606
2119
|
);
|
|
1607
2120
|
}
|
|
@@ -1609,7 +2122,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
|
|
|
1609
2122
|
function specTool(review, onSubmit, builderCheckRan) {
|
|
1610
2123
|
let repairs = 0;
|
|
1611
2124
|
let reminded = false;
|
|
1612
|
-
return
|
|
2125
|
+
return tool3(
|
|
1613
2126
|
SUBMIT_SPEC_TOOL_NAME,
|
|
1614
2127
|
"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.",
|
|
1615
2128
|
specSchema.shape,
|
|
@@ -1622,14 +2135,36 @@ function specTool(review, onSubmit, builderCheckRan) {
|
|
|
1622
2135
|
}
|
|
1623
2136
|
if (!verdict.accepted) {
|
|
1624
2137
|
repairs += 1;
|
|
1625
|
-
return
|
|
2138
|
+
return reply2(verdict.feedback, true);
|
|
1626
2139
|
}
|
|
1627
2140
|
if (!builderCheckRan() && !reminded) {
|
|
1628
2141
|
reminded = true;
|
|
1629
|
-
return
|
|
2142
|
+
return reply2(BUILDER_CHECK_FIRST, true);
|
|
2143
|
+
}
|
|
2144
|
+
onSubmit({ spec: args, problems: verdict.problems });
|
|
2145
|
+
return reply2(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
|
|
2146
|
+
}
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
function briefTool(review, onSubmit) {
|
|
2150
|
+
let repairs = 0;
|
|
2151
|
+
return tool3(
|
|
2152
|
+
SUBMIT_SPEC_TOOL_NAME,
|
|
2153
|
+
"Hand in the brief of the result. It replaces the previous version entirely, so always send the FULL brief. TAMsense lints it first; if it returns feedback, the brief was not saved: fix it and call submit_spec again.",
|
|
2154
|
+
resultBriefSchema.shape,
|
|
2155
|
+
async (args) => {
|
|
2156
|
+
let verdict;
|
|
2157
|
+
try {
|
|
2158
|
+
verdict = await review({ spec: args, repairs });
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
return unreachable(error, SUBMIT_SPEC_TOOL_NAME);
|
|
2161
|
+
}
|
|
2162
|
+
if (!verdict.accepted) {
|
|
2163
|
+
repairs += 1;
|
|
2164
|
+
return reply2(verdict.feedback, true);
|
|
1630
2165
|
}
|
|
1631
2166
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
1632
|
-
return
|
|
2167
|
+
return reply2(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
|
|
1633
2168
|
}
|
|
1634
2169
|
);
|
|
1635
2170
|
}
|
|
@@ -1731,10 +2266,9 @@ function usageLimitWatch() {
|
|
|
1731
2266
|
// src/claude.ts
|
|
1732
2267
|
var NO_RESULT_TEXT = "Claude finished without a result.";
|
|
1733
2268
|
var SERVER_NAME = "tamsense";
|
|
1734
|
-
var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
|
|
1735
2269
|
var CAPTION_FIELDS = {
|
|
1736
|
-
title:
|
|
1737
|
-
purpose:
|
|
2270
|
+
title: z4.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"'),
|
|
2271
|
+
purpose: z4.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
|
|
1738
2272
|
};
|
|
1739
2273
|
var ATTACH_TOOLS = {
|
|
1740
2274
|
screenshot: {
|
|
@@ -1755,21 +2289,21 @@ var ATTACH_TOOLS = {
|
|
|
1755
2289
|
};
|
|
1756
2290
|
function attachTool(kind, attach) {
|
|
1757
2291
|
const spec = ATTACH_TOOLS[kind];
|
|
1758
|
-
return
|
|
2292
|
+
return tool4(
|
|
1759
2293
|
spec.name,
|
|
1760
2294
|
spec.description,
|
|
1761
|
-
{ path:
|
|
2295
|
+
{ path: z4.string().describe(spec.path), ...CAPTION_FIELDS },
|
|
1762
2296
|
async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
|
|
1763
2297
|
);
|
|
1764
2298
|
}
|
|
1765
2299
|
function verdictTool(onVerdict) {
|
|
1766
|
-
return
|
|
2300
|
+
return tool4(
|
|
1767
2301
|
VERDICT_TOOL_NAME,
|
|
1768
2302
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
1769
2303
|
{
|
|
1770
|
-
passed:
|
|
1771
|
-
report:
|
|
1772
|
-
couldNotRun:
|
|
2304
|
+
passed: z4.boolean().describe("true only if nothing must be fixed"),
|
|
2305
|
+
report: z4.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
|
|
2306
|
+
couldNotRun: z4.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")
|
|
1773
2307
|
},
|
|
1774
2308
|
async (args) => {
|
|
1775
2309
|
onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
|
|
@@ -1778,13 +2312,13 @@ function verdictTool(onVerdict) {
|
|
|
1778
2312
|
);
|
|
1779
2313
|
}
|
|
1780
2314
|
function handoffTool(targets, onHandoff) {
|
|
1781
|
-
return
|
|
2315
|
+
return tool4(
|
|
1782
2316
|
HANDOFF_TOOL_NAME,
|
|
1783
2317
|
"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.",
|
|
1784
2318
|
{
|
|
1785
|
-
to:
|
|
1786
|
-
outcome:
|
|
1787
|
-
note:
|
|
2319
|
+
to: z4.string().describe("name or id of the step to hand the task to"),
|
|
2320
|
+
outcome: z4.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
|
|
2321
|
+
note: z4.string().describe("for the next agent and the owner: what was done, what to check, what is open")
|
|
1788
2322
|
},
|
|
1789
2323
|
async (args) => {
|
|
1790
2324
|
const choice = chooseHandoff(targets, args);
|
|
@@ -1797,25 +2331,25 @@ function handoffTool(targets, onHandoff) {
|
|
|
1797
2331
|
);
|
|
1798
2332
|
}
|
|
1799
2333
|
function askPersonTool(asking, waiting) {
|
|
1800
|
-
return
|
|
2334
|
+
return tool4(
|
|
1801
2335
|
ASK_PERSON_TOOL_NAME,
|
|
1802
2336
|
"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.",
|
|
1803
2337
|
{
|
|
1804
|
-
to:
|
|
1805
|
-
why:
|
|
1806
|
-
question:
|
|
1807
|
-
context:
|
|
1808
|
-
tried:
|
|
1809
|
-
did:
|
|
1810
|
-
error:
|
|
1811
|
-
result:
|
|
2338
|
+
to: z4.union([z4.number(), z4.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"),
|
|
2339
|
+
why: z4.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
|
|
2340
|
+
question: z4.string().describe("one plain question the person can answer without reading the code"),
|
|
2341
|
+
context: z4.string().optional().describe("one line on what you saw that led to the question"),
|
|
2342
|
+
tried: z4.array(z4.object({
|
|
2343
|
+
did: z4.string().describe("the command or step"),
|
|
2344
|
+
error: z4.string().optional().describe("the exact error line, if there was one"),
|
|
2345
|
+
result: z4.string().describe("what it showed, one line")
|
|
1812
2346
|
})).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
|
|
1813
|
-
options:
|
|
1814
|
-
label:
|
|
1815
|
-
detail:
|
|
2347
|
+
options: z4.array(z4.object({
|
|
2348
|
+
label: z4.string().describe("a short answer the person can pick"),
|
|
2349
|
+
detail: z4.string().describe("one line on what happens if it is picked")
|
|
1816
2350
|
})).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
|
|
1817
|
-
blocking:
|
|
1818
|
-
assumption:
|
|
2351
|
+
blocking: z4.boolean().describe("true when you cannot go on without the answer"),
|
|
2352
|
+
assumption: z4.string().optional().describe("when blocking is false: what you continue with until the answer comes")
|
|
1819
2353
|
},
|
|
1820
2354
|
async (args) => {
|
|
1821
2355
|
if (waiting.waitingFor !== null) {
|
|
@@ -1825,16 +2359,16 @@ function askPersonTool(asking, waiting) {
|
|
|
1825
2359
|
if ("error" in choice) {
|
|
1826
2360
|
return { content: [{ type: "text", text: choice.error }], isError: true };
|
|
1827
2361
|
}
|
|
1828
|
-
let
|
|
2362
|
+
let reply3;
|
|
1829
2363
|
try {
|
|
1830
|
-
|
|
2364
|
+
reply3 = await asking.send(choice.body);
|
|
1831
2365
|
} catch (error) {
|
|
1832
2366
|
return { content: [{ type: "text", text: `Could not send the question: ${error instanceof Error ? error.message : String(error)}. Decide yourself and say so in your report.` }], isError: true };
|
|
1833
2367
|
}
|
|
1834
2368
|
if (choice.body.blocking) {
|
|
1835
|
-
waiting.waitingFor =
|
|
2369
|
+
waiting.waitingFor = reply3.id;
|
|
1836
2370
|
}
|
|
1837
|
-
return { content: [{ type: "text", text: askedText(
|
|
2371
|
+
return { content: [{ type: "text", text: askedText(reply3.name, choice.body) }] };
|
|
1838
2372
|
}
|
|
1839
2373
|
);
|
|
1840
2374
|
}
|
|
@@ -1842,8 +2376,13 @@ function specWorkTools(specWork, submitted) {
|
|
|
1842
2376
|
if (!specWork) {
|
|
1843
2377
|
return [];
|
|
1844
2378
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
2379
|
+
if (specWork.stage === "interview") {
|
|
2380
|
+
return [questionsTool(specWork.review, (questions) => {
|
|
2381
|
+
submitted.questions = questions;
|
|
2382
|
+
})];
|
|
2383
|
+
}
|
|
2384
|
+
return specWork.brief ? [briefTool(specWork.review, (brief) => {
|
|
2385
|
+
submitted.spec = brief;
|
|
1847
2386
|
})] : [specTool(specWork.review, (spec) => {
|
|
1848
2387
|
submitted.spec = spec;
|
|
1849
2388
|
}, () => submitted.helperStarted)];
|
|
@@ -1871,7 +2410,9 @@ function tamsenseServer(request, submitted) {
|
|
|
1871
2410
|
...knowledgeTools(request.knowledge),
|
|
1872
2411
|
...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
|
|
1873
2412
|
submitted.handoff = handoff;
|
|
1874
|
-
})] : []
|
|
2413
|
+
})] : [],
|
|
2414
|
+
...request.results ? resultToolset(request.results) : [],
|
|
2415
|
+
...request.handOver ? [handOverTool(request.handOver)] : []
|
|
1875
2416
|
];
|
|
1876
2417
|
return {
|
|
1877
2418
|
server: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }),
|
|
@@ -1898,6 +2439,7 @@ async function attempt(request, context, resume) {
|
|
|
1898
2439
|
let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
|
|
1899
2440
|
const tamsense = tamsenseServer(request, context.submitted);
|
|
1900
2441
|
const shown = hiddenStepsFilter();
|
|
2442
|
+
const helpers = helperTasks();
|
|
1901
2443
|
const messages = context.startQuery({
|
|
1902
2444
|
prompt: request.input.start(request.prompt.prompt),
|
|
1903
2445
|
options: {
|
|
@@ -1909,7 +2451,7 @@ async function attempt(request, context, resume) {
|
|
|
1909
2451
|
thinking: { type: "adaptive", display: "summarized" },
|
|
1910
2452
|
// режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
|
|
1911
2453
|
canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
|
|
1912
|
-
plugins: [{ type: "local", path:
|
|
2454
|
+
plugins: [{ type: "local", path: request.library }],
|
|
1913
2455
|
mcpServers: { [SERVER_NAME]: tamsense.server },
|
|
1914
2456
|
allowedTools: tamsense.allowed,
|
|
1915
2457
|
abortController: context.abortController,
|
|
@@ -1919,6 +2461,7 @@ async function attempt(request, context, resume) {
|
|
|
1919
2461
|
}
|
|
1920
2462
|
});
|
|
1921
2463
|
request.input.onSwitch((change) => applySwitch(messages, change));
|
|
2464
|
+
request.input.onStopHelper((ref) => helpers.stop(messages, ref));
|
|
1922
2465
|
const failedToResume = () => resume !== null && sessionId === null && !request.signal.aborted;
|
|
1923
2466
|
try {
|
|
1924
2467
|
for await (const message of messages) {
|
|
@@ -1930,7 +2473,7 @@ async function attempt(request, context, resume) {
|
|
|
1930
2473
|
messages.close();
|
|
1931
2474
|
return { outcome, sessionId, resumeFailed: true };
|
|
1932
2475
|
}
|
|
1933
|
-
for (const draft of feedOf(message).filter(shown)) {
|
|
2476
|
+
for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
|
|
1934
2477
|
if (draft.kind === "agent" && draft.parent === null) {
|
|
1935
2478
|
context.submitted.helperStarted = true;
|
|
1936
2479
|
}
|
|
@@ -1981,13 +2524,13 @@ function claudeRunner(startQuery) {
|
|
|
1981
2524
|
var runClaude = claudeRunner(query2);
|
|
1982
2525
|
|
|
1983
2526
|
// src/config.ts
|
|
1984
|
-
import { mkdir as
|
|
2527
|
+
import { mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
1985
2528
|
import { homedir as homedir2 } from "os";
|
|
1986
|
-
import { dirname as dirname2, join as
|
|
2529
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1987
2530
|
var OWNER_ONLY_FILE = 384;
|
|
1988
2531
|
var OWNER_ONLY_FOLDER = 448;
|
|
1989
2532
|
function configPath(home = homedir2()) {
|
|
1990
|
-
return
|
|
2533
|
+
return join4(home, ".tamsense", "runner.json");
|
|
1991
2534
|
}
|
|
1992
2535
|
function isRecord2(value) {
|
|
1993
2536
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -2005,7 +2548,7 @@ function isMissingFile(error) {
|
|
|
2005
2548
|
}
|
|
2006
2549
|
async function readConfig(path = configPath()) {
|
|
2007
2550
|
try {
|
|
2008
|
-
return parseConfig(JSON.parse(await
|
|
2551
|
+
return parseConfig(JSON.parse(await readFile3(path, "utf8")));
|
|
2009
2552
|
} catch (error) {
|
|
2010
2553
|
if (isMissingFile(error) || error instanceof SyntaxError) {
|
|
2011
2554
|
return null;
|
|
@@ -2014,34 +2557,12 @@ async function readConfig(path = configPath()) {
|
|
|
2014
2557
|
}
|
|
2015
2558
|
}
|
|
2016
2559
|
async function writeConfig(config, path = configPath()) {
|
|
2017
|
-
await
|
|
2018
|
-
await
|
|
2560
|
+
await mkdir3(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
|
|
2561
|
+
await writeFile3(path, `${JSON.stringify(config, null, 4)}
|
|
2019
2562
|
`, { mode: OWNER_ONLY_FILE });
|
|
2020
2563
|
}
|
|
2021
2564
|
async function removeConfig(path = configPath()) {
|
|
2022
|
-
await
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// src/protocol.ts
|
|
2026
|
-
var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
2027
|
-
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
2028
|
-
var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
2029
|
-
var PAIRINGS_PATH = "/api/runner/pairings";
|
|
2030
|
-
var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
|
|
2031
|
-
var IDENTITY_PATH = "/api/runner/me";
|
|
2032
|
-
var NEXT_RUN_PATH = "/api/runner/runs/next";
|
|
2033
|
-
var CAPABILITIES_PATH = "/api/runner/me/capabilities";
|
|
2034
|
-
function runPath(runId, action) {
|
|
2035
|
-
return `/api/runner/runs/${runId}/${action}`;
|
|
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
|
-
}
|
|
2043
|
-
function recordingUploadPath(runId, upload, part) {
|
|
2044
|
-
return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
|
|
2565
|
+
await rm4(path, { force: true });
|
|
2045
2566
|
}
|
|
2046
2567
|
|
|
2047
2568
|
// src/login.ts
|
|
@@ -2058,14 +2579,14 @@ async function login(tools, request) {
|
|
|
2058
2579
|
tools.openBrowser(pageUrl);
|
|
2059
2580
|
while (tools.now() < expiresAt) {
|
|
2060
2581
|
await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
|
|
2061
|
-
const
|
|
2062
|
-
if (
|
|
2063
|
-
return { token:
|
|
2582
|
+
const reply3 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
|
|
2583
|
+
if (reply3.status === "connected") {
|
|
2584
|
+
return { token: reply3.token, device: reply3.device };
|
|
2064
2585
|
}
|
|
2065
|
-
if (
|
|
2586
|
+
if (reply3.status === "denied") {
|
|
2066
2587
|
throw new Error(DENIED_TEXT);
|
|
2067
2588
|
}
|
|
2068
|
-
if (
|
|
2589
|
+
if (reply3.status === "expired") {
|
|
2069
2590
|
break;
|
|
2070
2591
|
}
|
|
2071
2592
|
}
|
|
@@ -2074,13 +2595,13 @@ async function login(tools, request) {
|
|
|
2074
2595
|
|
|
2075
2596
|
// src/recorderEnv.ts
|
|
2076
2597
|
import { existsSync as existsSync3 } from "fs";
|
|
2077
|
-
import { join as
|
|
2078
|
-
import { fileURLToPath
|
|
2598
|
+
import { join as join6 } from "path";
|
|
2599
|
+
import { fileURLToPath } from "url";
|
|
2079
2600
|
|
|
2080
2601
|
// src/recorder.ts
|
|
2081
2602
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2082
2603
|
import Module, { createRequire } from "module";
|
|
2083
|
-
import { basename, dirname as dirname3, join as
|
|
2604
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "path";
|
|
2084
2605
|
import process2 from "process";
|
|
2085
2606
|
var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
|
|
2086
2607
|
var SERVER_ENV = "TAMSENSE_SERVER";
|
|
@@ -2153,7 +2674,7 @@ function clip2(text2, max) {
|
|
|
2153
2674
|
return text2.length > max ? `${text2.slice(0, max - 1).trimEnd()}\u2026` : text2;
|
|
2154
2675
|
}
|
|
2155
2676
|
function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
2156
|
-
const from = script2 ? `, opened by ${
|
|
2677
|
+
const from = script2 ? `, opened by ${basename2(script2)}` : "";
|
|
2157
2678
|
return {
|
|
2158
2679
|
title: clip2(env[DEFAULT_TITLE_ENV]?.trim() || "Browser session", MAX_TITLE_LENGTH),
|
|
2159
2680
|
purpose: clip2(`${env[DEFAULT_PURPOSE_ENV]?.trim() || "Recorded automatically while the agent used a browser"}${from}`, MAX_PURPOSE_LENGTH)
|
|
@@ -2161,7 +2682,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
|
2161
2682
|
}
|
|
2162
2683
|
function rrwebSource() {
|
|
2163
2684
|
const entry = createRequire(import.meta.url).resolve("rrweb");
|
|
2164
|
-
return readFileSync(
|
|
2685
|
+
return readFileSync(join5(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
|
|
2165
2686
|
}
|
|
2166
2687
|
function pageScript(rrweb) {
|
|
2167
2688
|
return `(() => {
|
|
@@ -2212,8 +2733,8 @@ var Session = class {
|
|
|
2212
2733
|
bytes = 0;
|
|
2213
2734
|
constructor(dir, recording, url) {
|
|
2214
2735
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2215
|
-
this.file =
|
|
2216
|
-
this.metaFile =
|
|
2736
|
+
this.file = join5(dir, `${id}${REPLAY_SUFFIX}`);
|
|
2737
|
+
this.metaFile = join5(dir, `${id}${META_SUFFIX}`);
|
|
2217
2738
|
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 };
|
|
2218
2739
|
writeFileSync(this.file, "");
|
|
2219
2740
|
this.save();
|
|
@@ -2385,11 +2906,11 @@ if (process2.env[RECORDINGS_DIR_ENV]) {
|
|
|
2385
2906
|
// src/recorderEnv.ts
|
|
2386
2907
|
var RECORDER_ENV = "TAMSENSE_RECORDER";
|
|
2387
2908
|
function bundledRecorder() {
|
|
2388
|
-
const path =
|
|
2909
|
+
const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
|
|
2389
2910
|
return existsSync3(path) ? path : null;
|
|
2390
2911
|
}
|
|
2391
2912
|
function recordingsFolder(home, runId) {
|
|
2392
|
-
return
|
|
2913
|
+
return join6(home, ".tamsense", "recordings", `run-${runId}`);
|
|
2393
2914
|
}
|
|
2394
2915
|
function nodeOptionsWithRecorder(recorder, nodeOptions) {
|
|
2395
2916
|
return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
|
|
@@ -2425,12 +2946,12 @@ var PLATFORM_LABELS = {
|
|
|
2425
2946
|
darwin: "macOS",
|
|
2426
2947
|
linux: "Linux"
|
|
2427
2948
|
};
|
|
2428
|
-
var
|
|
2949
|
+
var MAX_NAME_LENGTH2 = 80;
|
|
2429
2950
|
function platformLabel(platform = process.platform) {
|
|
2430
2951
|
return PLATFORM_LABELS[platform] ?? platform;
|
|
2431
2952
|
}
|
|
2432
2953
|
function defaultDeviceName() {
|
|
2433
|
-
return hostname().slice(0,
|
|
2954
|
+
return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
|
|
2434
2955
|
}
|
|
2435
2956
|
function browserCommand(url, platform) {
|
|
2436
2957
|
if (platform === "win32") {
|
|
@@ -2515,7 +3036,7 @@ function relaunchArgs(argv) {
|
|
|
2515
3036
|
}
|
|
2516
3037
|
function relaunch(argv, env) {
|
|
2517
3038
|
const isWindows = process3.platform === "win32";
|
|
2518
|
-
return new Promise((
|
|
3039
|
+
return new Promise((resolve3, reject) => {
|
|
2519
3040
|
const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
|
|
2520
3041
|
const ignoreInterrupt = () => {
|
|
2521
3042
|
};
|
|
@@ -2526,13 +3047,13 @@ function relaunch(argv, env) {
|
|
|
2526
3047
|
});
|
|
2527
3048
|
child.on("close", (code) => {
|
|
2528
3049
|
process3.off("SIGINT", ignoreInterrupt);
|
|
2529
|
-
|
|
3050
|
+
resolve3(code ?? 1);
|
|
2530
3051
|
});
|
|
2531
3052
|
});
|
|
2532
3053
|
}
|
|
2533
3054
|
|
|
2534
3055
|
// src/worker.ts
|
|
2535
|
-
import { rm as
|
|
3056
|
+
import { rm as rm6 } from "fs/promises";
|
|
2536
3057
|
import { dirname as dirname5 } from "path";
|
|
2537
3058
|
import process4 from "process";
|
|
2538
3059
|
|
|
@@ -2572,11 +3093,43 @@ function feedQueue(send) {
|
|
|
2572
3093
|
};
|
|
2573
3094
|
}
|
|
2574
3095
|
|
|
3096
|
+
// src/library.ts
|
|
3097
|
+
import { existsSync as existsSync4 } from "fs";
|
|
3098
|
+
import { cp, mkdir as mkdir4, rm as rm5 } from "fs/promises";
|
|
3099
|
+
import { join as join7 } from "path";
|
|
3100
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3101
|
+
var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
|
|
3102
|
+
var SKILL_NAME = /^[a-z0-9-]+$/;
|
|
3103
|
+
var STAGE_SKILLS = {
|
|
3104
|
+
interview: ["task-interview"],
|
|
3105
|
+
spec: ["spec-writing", "task-interview"]
|
|
3106
|
+
};
|
|
3107
|
+
function allowedSkills(stage) {
|
|
3108
|
+
const skills = stage.role?.skills ?? null;
|
|
3109
|
+
return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
|
|
3110
|
+
}
|
|
3111
|
+
function roleLibraryFolder(home, runId) {
|
|
3112
|
+
return join7(home, ".tamsense", "libraries", `run-${runId}`);
|
|
3113
|
+
}
|
|
3114
|
+
async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
|
|
3115
|
+
if (skills === null) {
|
|
3116
|
+
return source;
|
|
3117
|
+
}
|
|
3118
|
+
await rm5(folder, { recursive: true, force: true });
|
|
3119
|
+
await mkdir4(join7(folder, "skills"), { recursive: true });
|
|
3120
|
+
await cp(join7(source, ".claude-plugin"), join7(folder, ".claude-plugin"), { recursive: true });
|
|
3121
|
+
await cp(join7(source, "agents"), join7(folder, "agents"), { recursive: true });
|
|
3122
|
+
for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync4(join7(source, "skills", name)))) {
|
|
3123
|
+
await cp(join7(source, "skills", skill), join7(folder, "skills", skill), { recursive: true });
|
|
3124
|
+
}
|
|
3125
|
+
return folder;
|
|
3126
|
+
}
|
|
3127
|
+
|
|
2575
3128
|
// src/recordings.ts
|
|
2576
3129
|
import { randomUUID } from "crypto";
|
|
2577
3130
|
import { createReadStream } from "fs";
|
|
2578
|
-
import { readdir, readFile as
|
|
2579
|
-
import { basename as
|
|
3131
|
+
import { readdir, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
3132
|
+
import { basename as basename3, dirname as dirname4, extname as extname2, isAbsolute as isAbsolute2, join as join8 } from "path";
|
|
2580
3133
|
import { createInterface as createInterface2 } from "readline";
|
|
2581
3134
|
import { Readable } from "stream";
|
|
2582
3135
|
import { promisify } from "util";
|
|
@@ -2637,23 +3190,23 @@ function marksFrom(marks, startTs) {
|
|
|
2637
3190
|
return marks.map((mark) => ({ at: Math.max(0, mark.ts - startTs), label: mark.label })).sort((a, b) => a.at - b.at);
|
|
2638
3191
|
}
|
|
2639
3192
|
function sessionIdOf(file) {
|
|
2640
|
-
return
|
|
3193
|
+
return basename3(file).replace(REPLAY_SUFFIX, "");
|
|
2641
3194
|
}
|
|
2642
3195
|
async function readMeta(file) {
|
|
2643
3196
|
try {
|
|
2644
|
-
return JSON.parse(await
|
|
3197
|
+
return JSON.parse(await readFile4(file, "utf8"));
|
|
2645
3198
|
} catch {
|
|
2646
3199
|
return null;
|
|
2647
3200
|
}
|
|
2648
3201
|
}
|
|
2649
3202
|
async function metaFor(kind, file) {
|
|
2650
3203
|
if (kind === "replay") {
|
|
2651
|
-
return readMeta(
|
|
3204
|
+
return readMeta(join8(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
2652
3205
|
}
|
|
2653
|
-
const folder =
|
|
3206
|
+
const folder = basename3(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
|
|
2654
3207
|
for (const name of await readdir(folder).catch(() => [])) {
|
|
2655
3208
|
if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
|
|
2656
|
-
const meta = await readMeta(
|
|
3209
|
+
const meta = await readMeta(join8(folder, name));
|
|
2657
3210
|
if (meta?.video === file) {
|
|
2658
3211
|
return meta;
|
|
2659
3212
|
}
|
|
@@ -2663,7 +3216,7 @@ async function metaFor(kind, file) {
|
|
|
2663
3216
|
}
|
|
2664
3217
|
async function ledger(dir) {
|
|
2665
3218
|
try {
|
|
2666
|
-
return new Set(JSON.parse(await
|
|
3219
|
+
return new Set(JSON.parse(await readFile4(join8(dir, LEDGER_FILE), "utf8")));
|
|
2667
3220
|
} catch {
|
|
2668
3221
|
return /* @__PURE__ */ new Set();
|
|
2669
3222
|
}
|
|
@@ -2671,10 +3224,10 @@ async function ledger(dir) {
|
|
|
2671
3224
|
async function remember(dir, file) {
|
|
2672
3225
|
const done2 = await ledger(dir);
|
|
2673
3226
|
done2.add(file);
|
|
2674
|
-
await
|
|
3227
|
+
await writeFile4(join8(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
2675
3228
|
}
|
|
2676
3229
|
async function nonEmpty(file) {
|
|
2677
|
-
return file !== null && ((await
|
|
3230
|
+
return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
|
|
2678
3231
|
}
|
|
2679
3232
|
async function pendingRecordings(dir) {
|
|
2680
3233
|
const done2 = await ledger(dir);
|
|
@@ -2684,8 +3237,8 @@ async function pendingRecordings(dir) {
|
|
|
2684
3237
|
if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
|
|
2685
3238
|
continue;
|
|
2686
3239
|
}
|
|
2687
|
-
const meta = await readMeta(
|
|
2688
|
-
const replay =
|
|
3240
|
+
const meta = await readMeta(join8(dir, name));
|
|
3241
|
+
const replay = join8(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
2689
3242
|
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
2690
3243
|
pending.push({ kind: "replay", file: replay, meta });
|
|
2691
3244
|
}
|
|
@@ -2705,11 +3258,11 @@ async function uploadReplay(upload, file, caption) {
|
|
|
2705
3258
|
if (!scan.snapshot) {
|
|
2706
3259
|
throw new Error("This replay has no page snapshot, so it cannot be played. Was the page ever opened?");
|
|
2707
3260
|
}
|
|
2708
|
-
return finish(upload, { kind: "replay", upload: id, name:
|
|
3261
|
+
return finish(upload, { kind: "replay", upload: id, name: basename3(file), ...caption, contentType: "application/x-ndjson", durationMs: null, marks: marksFrom(scan.marks, scan.startTs) });
|
|
2709
3262
|
}
|
|
2710
3263
|
async function uploadVideo(upload, file, caption, meta) {
|
|
2711
|
-
const contentType = VIDEO_TYPES_BY_EXTENSION[
|
|
2712
|
-
const info = await
|
|
3264
|
+
const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
|
|
3265
|
+
const info = await stat2(file);
|
|
2713
3266
|
if (info.size > MAX_VIDEO_BYTES) {
|
|
2714
3267
|
throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
|
|
2715
3268
|
}
|
|
@@ -2720,16 +3273,16 @@ async function uploadVideo(upload, file, caption, meta) {
|
|
|
2720
3273
|
const endedAt = meta?.endedAt ?? info.mtimeMs;
|
|
2721
3274
|
const durationMs = Math.max(0, Math.round(endedAt - startedAt));
|
|
2722
3275
|
const marks = marksFrom(meta?.marks ?? [], startedAt).filter((mark) => mark.at <= durationMs);
|
|
2723
|
-
return finish(upload, { kind: "video", upload: id, name:
|
|
3276
|
+
return finish(upload, { kind: "video", upload: id, name: basename3(file), ...caption, contentType, durationMs, marks });
|
|
2724
3277
|
}
|
|
2725
3278
|
async function finish(upload, body) {
|
|
2726
3279
|
return (await upload.api.request("POST", runPath(upload.runId, "recordings"), body)).id;
|
|
2727
3280
|
}
|
|
2728
3281
|
async function attachRecording(api, runId, recordingsDir2, request) {
|
|
2729
|
-
if (!
|
|
3282
|
+
if (!isAbsolute2(request.path)) {
|
|
2730
3283
|
return `Give the absolute path of the ${request.kind === "replay" ? "replay (.rrweb.ndjson)" : "video (.webm or .mp4)"} file.`;
|
|
2731
3284
|
}
|
|
2732
|
-
const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) :
|
|
3285
|
+
const fitsKind = request.kind === "replay" ? request.path.endsWith(REPLAY_SUFFIX) : extname2(request.path).toLowerCase() in VIDEO_TYPES_BY_EXTENSION;
|
|
2733
3286
|
if (!fitsKind) {
|
|
2734
3287
|
return request.kind === "replay" ? `A replay is the ${REPLAY_SUFFIX} file the TAMsense recorder wrote.` : "Only WebM and MP4 videos can be attached.";
|
|
2735
3288
|
}
|
|
@@ -2794,19 +3347,19 @@ async function uploadPendingRecordings(api, runId, dir) {
|
|
|
2794
3347
|
}
|
|
2795
3348
|
|
|
2796
3349
|
// src/screenshot.ts
|
|
2797
|
-
import { readFile as
|
|
2798
|
-
import { basename as
|
|
2799
|
-
var
|
|
3350
|
+
import { readFile as readFile5, stat as stat3 } from "fs/promises";
|
|
3351
|
+
import { basename as basename4, extname as extname3, isAbsolute as isAbsolute3 } from "path";
|
|
3352
|
+
var TYPES_BY_EXTENSION2 = {
|
|
2800
3353
|
".png": "image/png",
|
|
2801
3354
|
".jpg": "image/jpeg",
|
|
2802
3355
|
".jpeg": "image/jpeg",
|
|
2803
3356
|
".webp": "image/webp"
|
|
2804
3357
|
};
|
|
2805
3358
|
async function readScreenshot(request) {
|
|
2806
|
-
if (!
|
|
3359
|
+
if (!isAbsolute3(request.path)) {
|
|
2807
3360
|
return "Give the absolute path of the image file.";
|
|
2808
3361
|
}
|
|
2809
|
-
const contentType =
|
|
3362
|
+
const contentType = TYPES_BY_EXTENSION2[extname3(request.path).toLowerCase()];
|
|
2810
3363
|
if (!contentType) {
|
|
2811
3364
|
return "Only PNG, JPEG and WebP images can be attached.";
|
|
2812
3365
|
}
|
|
@@ -2816,15 +3369,15 @@ async function readScreenshot(request) {
|
|
|
2816
3369
|
} catch (error) {
|
|
2817
3370
|
return error instanceof Error ? error.message : String(error);
|
|
2818
3371
|
}
|
|
2819
|
-
const info = await
|
|
3372
|
+
const info = await stat3(request.path).catch(() => null);
|
|
2820
3373
|
if (!info?.isFile()) {
|
|
2821
3374
|
return `There is no file at ${request.path}.`;
|
|
2822
3375
|
}
|
|
2823
3376
|
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
2824
3377
|
return "The image is larger than 5 MB. Take a smaller screenshot (viewport instead of full page, or JPEG).";
|
|
2825
3378
|
}
|
|
2826
|
-
const data = (await
|
|
2827
|
-
return { name:
|
|
3379
|
+
const data = (await readFile5(request.path)).toString("base64");
|
|
3380
|
+
return { name: basename4(request.path), ...caption, contentType, data };
|
|
2828
3381
|
}
|
|
2829
3382
|
|
|
2830
3383
|
// src/worker.ts
|
|
@@ -2887,17 +3440,31 @@ function messageLine(message) {
|
|
|
2887
3440
|
const line = `Message from ${message.authorName}: ${message.text}`.replace(/\s+/g, " ").trim();
|
|
2888
3441
|
return line.length > MAX_MESSAGE_LINE ? `${line.slice(0, MAX_MESSAGE_LINE - 1)}\u2026` : line;
|
|
2889
3442
|
}
|
|
3443
|
+
async function stopHelper(input, stop, print2) {
|
|
3444
|
+
try {
|
|
3445
|
+
const result = await input.stopHelper(stop.ref);
|
|
3446
|
+
if (result === "stopped") {
|
|
3447
|
+
input.note(helperStoppedNote(stop), "queue");
|
|
3448
|
+
print2(` ${stop.byName} stopped the helper ${stop.name}`);
|
|
3449
|
+
} else if (result === "unknown" && input.note(helperStopRequest(stop), "steer")) {
|
|
3450
|
+
print2(` ${stop.byName} asked Claude to stop the helper ${stop.name}`);
|
|
3451
|
+
}
|
|
3452
|
+
} catch (error) {
|
|
3453
|
+
print2(`Could not stop the helper ${stop.name}: ${messageOf3(error)}`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
2890
3456
|
function inbox(job, tools, record, closedOnSite) {
|
|
2891
3457
|
const input = inputChannel({ onIdle: () => void poll() });
|
|
3458
|
+
const stopsSeen = /* @__PURE__ */ new Set();
|
|
2892
3459
|
let running = null;
|
|
2893
3460
|
let again = false;
|
|
2894
3461
|
let failing = false;
|
|
2895
3462
|
async function check() {
|
|
2896
3463
|
const ack = input.takeDelivered();
|
|
2897
3464
|
const body = { ack };
|
|
2898
|
-
let
|
|
3465
|
+
let reply3;
|
|
2899
3466
|
try {
|
|
2900
|
-
|
|
3467
|
+
reply3 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
|
|
2901
3468
|
} catch (error) {
|
|
2902
3469
|
input.restoreDelivered(ack);
|
|
2903
3470
|
if (!failing) {
|
|
@@ -2907,17 +3474,23 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2907
3474
|
return;
|
|
2908
3475
|
}
|
|
2909
3476
|
failing = false;
|
|
2910
|
-
if (!
|
|
3477
|
+
if (!reply3.keepGoing) {
|
|
2911
3478
|
closedOnSite();
|
|
2912
3479
|
return;
|
|
2913
3480
|
}
|
|
2914
|
-
for (const message of
|
|
3481
|
+
for (const message of reply3.messages) {
|
|
2915
3482
|
if (input.push(message)) {
|
|
2916
3483
|
tools.print(` ${messageLine(message)}`);
|
|
2917
3484
|
}
|
|
2918
3485
|
}
|
|
2919
|
-
|
|
2920
|
-
|
|
3486
|
+
for (const stop of reply3.helperStops ?? []) {
|
|
3487
|
+
if (!stopsSeen.has(stop.ref)) {
|
|
3488
|
+
stopsSeen.add(stop.ref);
|
|
3489
|
+
void stopHelper(input, stop, tools.print);
|
|
3490
|
+
}
|
|
3491
|
+
}
|
|
3492
|
+
if (reply3.model !== null || reply3.effort !== null) {
|
|
3493
|
+
const change = { model: reply3.model, effort: reply3.effort };
|
|
2921
3494
|
void input.switchModel(change).then(
|
|
2922
3495
|
(applied) => {
|
|
2923
3496
|
if (applied) {
|
|
@@ -2957,7 +3530,7 @@ function specWorkTools2(job, api) {
|
|
|
2957
3530
|
if (kind !== "interview" && kind !== "spec") {
|
|
2958
3531
|
return null;
|
|
2959
3532
|
}
|
|
2960
|
-
return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body) };
|
|
3533
|
+
return { stage: kind, review: (body) => api.request("POST", runPath(job.runId, "review"), body), brief: isFolderJob(job) };
|
|
2961
3534
|
}
|
|
2962
3535
|
function askingTools(job, api) {
|
|
2963
3536
|
const people = job.people ?? [];
|
|
@@ -2972,11 +3545,11 @@ function permissionTools(job, tools) {
|
|
|
2972
3545
|
}
|
|
2973
3546
|
return {
|
|
2974
3547
|
send: async (body) => {
|
|
2975
|
-
const
|
|
2976
|
-
if (
|
|
3548
|
+
const reply3 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
|
|
3549
|
+
if (reply3.decision === "wait") {
|
|
2977
3550
|
tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
2978
3551
|
}
|
|
2979
|
-
return
|
|
3552
|
+
return reply3;
|
|
2980
3553
|
},
|
|
2981
3554
|
check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
|
|
2982
3555
|
sleep: tools.sleep
|
|
@@ -2995,11 +3568,27 @@ function knowledgeTools2(job, api) {
|
|
|
2995
3568
|
read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
|
|
2996
3569
|
};
|
|
2997
3570
|
}
|
|
2998
|
-
|
|
2999
|
-
|
|
3571
|
+
function skillsLine(skills) {
|
|
3572
|
+
return `This role uses ${skills.length} skill(s) from the library: ${skills.join(", ")}.`;
|
|
3573
|
+
}
|
|
3574
|
+
async function prepare(job, tools) {
|
|
3575
|
+
if (isFolderJob(job)) {
|
|
3576
|
+
tools.print("Preparing the task folder\u2026");
|
|
3577
|
+
const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
|
|
3578
|
+
return { workspace: workspace2, results, identity: {} };
|
|
3579
|
+
}
|
|
3000
3580
|
tools.print("Preparing the code\u2026");
|
|
3001
3581
|
const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
|
|
3002
|
-
|
|
3582
|
+
return { workspace, results: null, identity: await commitIdentityEnv(tools.home) };
|
|
3583
|
+
}
|
|
3584
|
+
async function executeJob(job, tools, signal, input, record) {
|
|
3585
|
+
const isCheck = job.stage.kind === "review" || job.stage.kind === "test";
|
|
3586
|
+
const { workspace, results, identity } = await prepare(job, tools);
|
|
3587
|
+
const skills = allowedSkills(job.stage);
|
|
3588
|
+
if (skills) {
|
|
3589
|
+
tools.print(skillsLine(skills));
|
|
3590
|
+
}
|
|
3591
|
+
const library = await prepareLibrary(roleLibraryFolder(tools.home, job.runId), skills);
|
|
3003
3592
|
signal.throwIfAborted();
|
|
3004
3593
|
tools.print("Claude is working\u2026");
|
|
3005
3594
|
const attached = { count: 0 };
|
|
@@ -3009,7 +3598,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
3009
3598
|
const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
|
|
3010
3599
|
const outcome = await tools.runAgent({
|
|
3011
3600
|
cwd: workspace.root,
|
|
3012
|
-
|
|
3601
|
+
library,
|
|
3602
|
+
prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder), results }),
|
|
3013
3603
|
input,
|
|
3014
3604
|
env: { ...baseEnv, ...tools.browser ? browserEnv(tools.browser) : {}, ...recording, ...identity },
|
|
3015
3605
|
signal,
|
|
@@ -3024,12 +3614,14 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
3024
3614
|
effort: job.effort,
|
|
3025
3615
|
onFeed: record,
|
|
3026
3616
|
onSession: session.report,
|
|
3027
|
-
attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request)
|
|
3617
|
+
attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
|
|
3618
|
+
results: resultTools(job, tools.api, workspace.root),
|
|
3619
|
+
handOver: handOverTools(job, tools.api)
|
|
3028
3620
|
});
|
|
3029
3621
|
await session.sent();
|
|
3030
3622
|
signal.throwIfAborted();
|
|
3031
3623
|
const waitingFor = outcome.waitingFor ?? null;
|
|
3032
|
-
const shouldPublish = job.stage.kind === "code" && outcome.completed && waitingFor === null;
|
|
3624
|
+
const shouldPublish = job.stage.kind === "code" && !isFolderJob(job) && !isReadOnlyCode(job.stage) && outcome.completed && waitingFor === null;
|
|
3033
3625
|
const pushed = shouldPublish ? await publishWorkspace(workspace, job, await gitToken(tools.api, job), identity) : [];
|
|
3034
3626
|
return {
|
|
3035
3627
|
outcome: outcome.completed ? "succeeded" : "failed",
|
|
@@ -3050,7 +3642,7 @@ function clockOf(iso) {
|
|
|
3050
3642
|
const time = new Date(iso);
|
|
3051
3643
|
return `${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
|
|
3052
3644
|
}
|
|
3053
|
-
function resultLine(finish2) {
|
|
3645
|
+
function resultLine(finish2, folder = false) {
|
|
3054
3646
|
if (finish2.pausedUntil) {
|
|
3055
3647
|
return `Paused: ${USAGE_LIMIT_REPORT}, resumes at ${clockOf(finish2.pausedUntil)}`;
|
|
3056
3648
|
}
|
|
@@ -3069,7 +3661,10 @@ function resultLine(finish2) {
|
|
|
3069
3661
|
if (finish2.spec) {
|
|
3070
3662
|
return "Done. The spec waits for the owner on TAMsense.";
|
|
3071
3663
|
}
|
|
3072
|
-
|
|
3664
|
+
if (finish2.outcome !== "succeeded") {
|
|
3665
|
+
return `Failed: ${finish2.report}`;
|
|
3666
|
+
}
|
|
3667
|
+
return folder ? "Done. The result is on the task on TAMsense." : `Done. Pushed ${finish2.pushed.length} branch(es), the pull requests open on TAMsense.`;
|
|
3073
3668
|
}
|
|
3074
3669
|
async function sendRecordings(job, tools) {
|
|
3075
3670
|
const folder = recordingsFolder(tools.home, job.runId);
|
|
@@ -3085,7 +3680,7 @@ async function sendRecordings(job, tools) {
|
|
|
3085
3680
|
tools.print(`Could not attach a recording: ${line}`);
|
|
3086
3681
|
}
|
|
3087
3682
|
} finally {
|
|
3088
|
-
await
|
|
3683
|
+
await rm6(folder, { recursive: true, force: true });
|
|
3089
3684
|
}
|
|
3090
3685
|
}
|
|
3091
3686
|
function failureReport(error, signal) {
|
|
@@ -3113,6 +3708,7 @@ async function runJob(job, tools, stop) {
|
|
|
3113
3708
|
} catch (error) {
|
|
3114
3709
|
finish2 = { outcome: "failed", report: failureReport(error, run2.signal), pushed: [], model: null, usage: null, verdict: null, handoff: null, sessionId: null, pausedUntil: null, questions: null, spec: null, waitingFor: null };
|
|
3115
3710
|
} finally {
|
|
3711
|
+
await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
3116
3712
|
clearInterval(polling);
|
|
3117
3713
|
clearInterval(feeding);
|
|
3118
3714
|
stop.removeEventListener("abort", stopRun);
|
|
@@ -3124,14 +3720,14 @@ async function runJob(job, tools, stop) {
|
|
|
3124
3720
|
}
|
|
3125
3721
|
await feed.flush();
|
|
3126
3722
|
if (run2.signal.reason === CLOSED_ON_SITE) {
|
|
3127
|
-
await
|
|
3723
|
+
await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
3128
3724
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
3129
3725
|
return;
|
|
3130
3726
|
}
|
|
3131
3727
|
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
|
|
3132
3728
|
try {
|
|
3133
3729
|
await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
|
|
3134
|
-
tools.print(resultLine(finish2));
|
|
3730
|
+
tools.print(resultLine(finish2, isFolderJob(job)));
|
|
3135
3731
|
} catch (error) {
|
|
3136
3732
|
tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
|
|
3137
3733
|
}
|
|
@@ -3180,9 +3776,9 @@ async function work(tools, stop, parallel = 1) {
|
|
|
3180
3776
|
await Promise.race(running);
|
|
3181
3777
|
continue;
|
|
3182
3778
|
}
|
|
3183
|
-
let
|
|
3779
|
+
let reply3;
|
|
3184
3780
|
try {
|
|
3185
|
-
|
|
3781
|
+
reply3 = await tools.api.request("POST", NEXT_RUN_PATH);
|
|
3186
3782
|
} catch (error) {
|
|
3187
3783
|
if (isFatal(error)) {
|
|
3188
3784
|
halt.abort();
|
|
@@ -3192,8 +3788,8 @@ async function work(tools, stop, parallel = 1) {
|
|
|
3192
3788
|
await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
|
|
3193
3789
|
continue;
|
|
3194
3790
|
}
|
|
3195
|
-
if (
|
|
3196
|
-
start(
|
|
3791
|
+
if (reply3.job) {
|
|
3792
|
+
start(reply3.job);
|
|
3197
3793
|
} else {
|
|
3198
3794
|
await tools.sleep(POLL_INTERVAL_MS, stopAll);
|
|
3199
3795
|
}
|
|
@@ -3248,7 +3844,7 @@ function stopOnInterrupt() {
|
|
|
3248
3844
|
return stop.signal;
|
|
3249
3845
|
}
|
|
3250
3846
|
function installTools(home) {
|
|
3251
|
-
return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists:
|
|
3847
|
+
return { home, nodePath: process5.execPath, env: process5.env, print, run: runProcess, exists: existsSync5 };
|
|
3252
3848
|
}
|
|
3253
3849
|
async function browserForStart(home, install) {
|
|
3254
3850
|
const installed = await installedBrowser(home);
|