@tamsensedev/runner 0.5.1 → 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 +1077 -447
- 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((resolve, 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 } = waitingSwitch;
|
|
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) : [])),
|
|
@@ -1048,6 +1376,209 @@ function specPromptText(spec) {
|
|
|
1048
1376
|
].filter(Boolean).join("\n\n");
|
|
1049
1377
|
}
|
|
1050
1378
|
|
|
1379
|
+
// src/workspace.ts
|
|
1380
|
+
import { existsSync as existsSync2 } from "fs";
|
|
1381
|
+
import { rm as rm3 } from "fs/promises";
|
|
1382
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
1383
|
+
|
|
1384
|
+
// src/git.ts
|
|
1385
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
1386
|
+
import { spawn as spawn2 } from "child_process";
|
|
1387
|
+
var GITHUB_ORIGIN = "https://github.com/";
|
|
1388
|
+
function githubAuthEnv(token) {
|
|
1389
|
+
const credentials = Buffer2.from(`x-access-token:${token}`).toString("base64");
|
|
1390
|
+
return {
|
|
1391
|
+
GIT_CONFIG_COUNT: "1",
|
|
1392
|
+
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
1393
|
+
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
1394
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
function runGit(args, options) {
|
|
1398
|
+
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
1399
|
+
return new Promise((resolve3, reject) => {
|
|
1400
|
+
const child = spawn2("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
1401
|
+
let stdout = "";
|
|
1402
|
+
let stderr = "";
|
|
1403
|
+
child.stdout.on("data", (chunk) => {
|
|
1404
|
+
stdout += chunk.toString();
|
|
1405
|
+
});
|
|
1406
|
+
child.stderr.on("data", (chunk) => {
|
|
1407
|
+
stderr += chunk.toString();
|
|
1408
|
+
});
|
|
1409
|
+
child.on("error", reject);
|
|
1410
|
+
child.on("close", (code) => resolve3({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
async function git(args, options) {
|
|
1414
|
+
const result = await runGit(args, options);
|
|
1415
|
+
if (result.code !== 0) {
|
|
1416
|
+
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
1417
|
+
}
|
|
1418
|
+
return result.stdout;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/lock.ts
|
|
1422
|
+
function keyedLock() {
|
|
1423
|
+
const tails = /* @__PURE__ */ new Map();
|
|
1424
|
+
return {
|
|
1425
|
+
async run(key, work2) {
|
|
1426
|
+
const previous = tails.get(key) ?? Promise.resolve();
|
|
1427
|
+
let release = () => {
|
|
1428
|
+
};
|
|
1429
|
+
const done2 = new Promise((resolve3) => {
|
|
1430
|
+
release = resolve3;
|
|
1431
|
+
});
|
|
1432
|
+
const tail = previous.then(() => done2);
|
|
1433
|
+
tails.set(key, tail);
|
|
1434
|
+
await previous;
|
|
1435
|
+
try {
|
|
1436
|
+
return await work2();
|
|
1437
|
+
} finally {
|
|
1438
|
+
release();
|
|
1439
|
+
if (tails.get(key) === tail) {
|
|
1440
|
+
tails.delete(key);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// src/workspace.ts
|
|
1448
|
+
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
1449
|
+
var cacheLock = keyedLock();
|
|
1450
|
+
function githubRemoteUrl(fullName) {
|
|
1451
|
+
return `https://github.com/${fullName}.git`;
|
|
1452
|
+
}
|
|
1453
|
+
function tamsenseFolder(home, ...parts) {
|
|
1454
|
+
return join3(home, ".tamsense", ...parts);
|
|
1455
|
+
}
|
|
1456
|
+
function folderNames(repositories) {
|
|
1457
|
+
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
1458
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1459
|
+
for (const repository of repositories) {
|
|
1460
|
+
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
1461
|
+
}
|
|
1462
|
+
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
1463
|
+
}
|
|
1464
|
+
async function refreshCache(cache, repository, token, tools) {
|
|
1465
|
+
if (existsSync2(join3(cache, ".git"))) {
|
|
1466
|
+
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1469
|
+
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
1470
|
+
}
|
|
1471
|
+
async function removeWorktree(cache, path) {
|
|
1472
|
+
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
1473
|
+
await rm3(path, { recursive: true, force: true });
|
|
1474
|
+
await git(["worktree", "prune"], { cwd: cache });
|
|
1475
|
+
}
|
|
1476
|
+
function stepFolder(job) {
|
|
1477
|
+
const task = `task-${job.taskId}`;
|
|
1478
|
+
return typeof job.stage.columnId === "number" ? [task, `step-${job.stage.columnId}`] : [task];
|
|
1479
|
+
}
|
|
1480
|
+
function isReadOnlyCheck(job) {
|
|
1481
|
+
return job.stage.kind === "review" || job.stage.kind === "test";
|
|
1482
|
+
}
|
|
1483
|
+
function branchHolders(porcelain, branch) {
|
|
1484
|
+
const holders = [];
|
|
1485
|
+
let path = null;
|
|
1486
|
+
for (const line of porcelain.split("\n").map((text2) => text2.trim())) {
|
|
1487
|
+
if (line.startsWith("worktree ")) {
|
|
1488
|
+
path = line.slice("worktree ".length);
|
|
1489
|
+
} else if (path && line === `branch refs/heads/${branch}`) {
|
|
1490
|
+
holders.push(path);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
return holders;
|
|
1494
|
+
}
|
|
1495
|
+
async function releaseBranch(cache, branch, keep) {
|
|
1496
|
+
const listed = await runGit(["worktree", "list", "--porcelain"], { cwd: cache });
|
|
1497
|
+
for (const path of branchHolders(listed.stdout, branch).filter((holder) => resolve2(holder) !== resolve2(keep))) {
|
|
1498
|
+
await removeWorktree(cache, path);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
async function baseRef(cache, job, repository) {
|
|
1502
|
+
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
1503
|
+
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
1504
|
+
}
|
|
1505
|
+
async function keptBase(cache, path, job, repository) {
|
|
1506
|
+
if (!job.keepWorkspace || !existsSync2(join3(path, ".git"))) {
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
1510
|
+
if (branch.code !== 0 || branch.stdout !== (isReadOnlyCheck(job) ? "HEAD" : job.branch)) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
1514
|
+
}
|
|
1515
|
+
function taskFolder(home, job) {
|
|
1516
|
+
return tamsenseFolder(home, "work", ...stepFolder(job));
|
|
1517
|
+
}
|
|
1518
|
+
async function prepareWorkspace(job, token, tools) {
|
|
1519
|
+
const root = taskFolder(tools.home, job);
|
|
1520
|
+
const folders = folderNames(job.repositories);
|
|
1521
|
+
const repositories = [];
|
|
1522
|
+
for (const [index, repository] of job.repositories.entries()) {
|
|
1523
|
+
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
1524
|
+
const folder = folders[index] ?? repository.fullName;
|
|
1525
|
+
const path = join3(root, folder);
|
|
1526
|
+
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
1527
|
+
await refreshCache(cache, repository, token, tools);
|
|
1528
|
+
const kept = await keptBase(cache, path, job, repository);
|
|
1529
|
+
if (kept) {
|
|
1530
|
+
return kept;
|
|
1531
|
+
}
|
|
1532
|
+
await removeWorktree(cache, path);
|
|
1533
|
+
const base = await baseRef(cache, job, repository);
|
|
1534
|
+
if (isReadOnlyCheck(job)) {
|
|
1535
|
+
await git(["worktree", "add", "--force", "--detach", path, base], { cwd: cache });
|
|
1536
|
+
} else {
|
|
1537
|
+
await releaseBranch(cache, job.branch, path);
|
|
1538
|
+
await git(["worktree", "add", "--force", "-B", job.branch, path, base], { cwd: cache });
|
|
1539
|
+
}
|
|
1540
|
+
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
1541
|
+
});
|
|
1542
|
+
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
1543
|
+
}
|
|
1544
|
+
return { root, repositories };
|
|
1545
|
+
}
|
|
1546
|
+
async function commitIdentityEnv(home) {
|
|
1547
|
+
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
1548
|
+
if (configured.code === 0 && configured.stdout) {
|
|
1549
|
+
return {};
|
|
1550
|
+
}
|
|
1551
|
+
return {
|
|
1552
|
+
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
1553
|
+
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
1554
|
+
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
1555
|
+
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
async function publishWorkspace(workspace, job, token, identity) {
|
|
1559
|
+
const pushed = [];
|
|
1560
|
+
for (const repository of workspace.repositories) {
|
|
1561
|
+
const options = { cwd: repository.path, env: identity };
|
|
1562
|
+
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
1563
|
+
await git(["add", "--all"], options);
|
|
1564
|
+
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
1565
|
+
if (hasStaged) {
|
|
1566
|
+
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
1567
|
+
}
|
|
1568
|
+
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
1569
|
+
if (newCommits === 0) {
|
|
1570
|
+
return null;
|
|
1571
|
+
}
|
|
1572
|
+
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
1573
|
+
return git(["rev-parse", "HEAD"], options);
|
|
1574
|
+
});
|
|
1575
|
+
if (sha) {
|
|
1576
|
+
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return pushed;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1051
1582
|
// src/prompt.ts
|
|
1052
1583
|
var VERDICT_TOOL_NAME = "submit_verdict";
|
|
1053
1584
|
var SCREENSHOT_TOOL_NAME = "attach_screenshot";
|
|
@@ -1070,6 +1601,11 @@ var CODE_RULES = [
|
|
|
1070
1601
|
];
|
|
1071
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.";
|
|
1072
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.";
|
|
1073
1609
|
var CHECK_RULES = [
|
|
1074
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.",
|
|
1075
1611
|
"Do not change, commit or push product code. You may run anything and create throwaway files outside the repositories.",
|
|
@@ -1080,9 +1616,27 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
|
|
|
1080
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".';
|
|
1081
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.`;
|
|
1082
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
|
+
];
|
|
1083
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.";
|
|
1084
|
-
function
|
|
1085
|
-
|
|
1635
|
+
function composeProjectName(job) {
|
|
1636
|
+
return ["tamsense", ...stepFolder(job)].join("-");
|
|
1637
|
+
}
|
|
1638
|
+
function localAppRule(job, isCheck, canAsk) {
|
|
1639
|
+
const project = composeProjectName(job);
|
|
1086
1640
|
const cannotRun = isCheck ? `call ${VERDICT_TOOL_NAME} with passed false and couldNotRun true, write the exact reason and what a person should do in the report, and do not hand off: the task waits on this step for a person.` : "say so in your report with the exact reason and what a person should do.";
|
|
1087
1641
|
return [
|
|
1088
1642
|
"When you need the running app, start a local copy from this task folder only.",
|
|
@@ -1099,6 +1653,10 @@ var STAGE_GOALS = {
|
|
|
1099
1653
|
review: "Review the code for",
|
|
1100
1654
|
test: "Test"
|
|
1101
1655
|
};
|
|
1656
|
+
var READ_ONLY_GOAL = "Work on";
|
|
1657
|
+
function isReadOnlyCode(stage) {
|
|
1658
|
+
return stage.kind === "code" && stage.role?.access === "read";
|
|
1659
|
+
}
|
|
1102
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.`;
|
|
1103
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.";
|
|
1104
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.";
|
|
@@ -1116,12 +1674,29 @@ var SPEC_RULES = [
|
|
|
1116
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."
|
|
1117
1675
|
];
|
|
1118
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.";
|
|
1119
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.";
|
|
1120
1690
|
function specText(spec) {
|
|
1121
1691
|
const text2 = specPromptText(spec);
|
|
1122
1692
|
return text2 ? `Approved spec:
|
|
1123
1693
|
${text2}` : null;
|
|
1124
1694
|
}
|
|
1695
|
+
function briefText(spec) {
|
|
1696
|
+
const text2 = specPromptText(spec);
|
|
1697
|
+
return text2 ? `Approved brief:
|
|
1698
|
+
${text2}` : null;
|
|
1699
|
+
}
|
|
1125
1700
|
function contextText(context) {
|
|
1126
1701
|
return context.trim() ? `Project context from the team (follow it like the project's own rules):
|
|
1127
1702
|
${context.trim()}` : null;
|
|
@@ -1160,6 +1735,18 @@ function peopleText(people) {
|
|
|
1160
1735
|
function canProposeKnowledge(job) {
|
|
1161
1736
|
return job.knowledge !== void 0 && (job.people ?? []).length > 0 && job.stage.kind !== "interview" && job.stage.kind !== "spec";
|
|
1162
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
|
+
}
|
|
1163
1750
|
function handoffText(targets, isCheck) {
|
|
1164
1751
|
if (targets.length === 0) {
|
|
1165
1752
|
return null;
|
|
@@ -1206,6 +1793,64 @@ function specGoal(job, work2) {
|
|
|
1206
1793
|
}
|
|
1207
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}`;
|
|
1208
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
|
+
}
|
|
1209
1854
|
function buildSpecPrompt(job, work2, workspace) {
|
|
1210
1855
|
const prompt = [
|
|
1211
1856
|
specGoal(job, work2),
|
|
@@ -1230,10 +1875,50 @@ ${JSON.stringify(job.spec)}` : null,
|
|
|
1230
1875
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
1231
1876
|
READ_ONLY_RULE,
|
|
1232
1877
|
`How to work:
|
|
1233
|
-
${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
1878
|
+
${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
1879
|
+
safetyText(job),
|
|
1880
|
+
knowledgeText(job.knowledge, job.computer, false),
|
|
1881
|
+
SPEC_STYLE_RULE
|
|
1882
|
+
].filter(Boolean).join("\n\n");
|
|
1883
|
+
return { prompt, instructions };
|
|
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)}`,
|
|
1234
1915
|
safetyText(job),
|
|
1235
|
-
knowledgeText(job.knowledge, job.computer,
|
|
1236
|
-
|
|
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
|
|
1237
1922
|
].filter(Boolean).join("\n\n");
|
|
1238
1923
|
return { prompt, instructions };
|
|
1239
1924
|
}
|
|
@@ -1243,13 +1928,17 @@ function buildTaskPrompt(job, workspace, options = {}) {
|
|
|
1243
1928
|
if (!job.specWork) {
|
|
1244
1929
|
throw new Error(SPEC_WORK_MISSING);
|
|
1245
1930
|
}
|
|
1246
|
-
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);
|
|
1247
1935
|
}
|
|
1248
1936
|
const isCheck = kind !== "code";
|
|
1937
|
+
const readOnly = isReadOnlyCode(job.stage);
|
|
1249
1938
|
const people = job.people ?? [];
|
|
1250
1939
|
const canAsk = people.length > 0;
|
|
1251
1940
|
const prompt = [
|
|
1252
|
-
`${STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
1941
|
+
`${readOnly ? READ_ONLY_GOAL : STAGE_GOALS[kind]} TAMsense task #${job.taskId}: ${job.title}`,
|
|
1253
1942
|
job.description,
|
|
1254
1943
|
specText(job.spec),
|
|
1255
1944
|
job.handoffNote && `Note from the previous step: ${job.handoffNote}`,
|
|
@@ -1258,8 +1947,8 @@ ${job.review}`,
|
|
|
1258
1947
|
teamMessagesText(job.messages)
|
|
1259
1948
|
].filter(Boolean).join("\n\n");
|
|
1260
1949
|
const evidence = [SCREENSHOT_RULE, ...options.recorder ? [RECORDING_RULE] : [], NAMING_RULE];
|
|
1261
|
-
const localApp = localAppRule(job
|
|
1262
|
-
const rules = isCheck ? [...CHECK_RULES, DEPENDENCIES_RULE, localApp, ...evidence] : [...CODE_RULES, ...job.review ? [REWORK_RULE] : [], DEPENDENCIES_RULE, localApp, ...evidence, CODE_REPORT_RULE];
|
|
1950
|
+
const localApp = localAppRule(job, isCheck, canAsk);
|
|
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];
|
|
1263
1952
|
const instructions = [
|
|
1264
1953
|
canAsk ? ASKING_RULE : UNATTENDED_RULE,
|
|
1265
1954
|
MACHINE_RULE,
|
|
@@ -1267,7 +1956,7 @@ ${job.review}`,
|
|
|
1267
1956
|
stepLine(job.stage),
|
|
1268
1957
|
job.stage.instructions,
|
|
1269
1958
|
contextText(job.projectContext),
|
|
1270
|
-
`The repositories for this task are checked out in the current folder, one subfolder each, on branch ${job.branch}:
|
|
1959
|
+
`The repositories for this task are checked out in the current folder, one subfolder each, ${isCheck ? "at the latest commit of branch" : "on branch"} ${job.branch}:
|
|
1271
1960
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
1272
1961
|
`How to work:
|
|
1273
1962
|
${bulletList(rules)}`,
|
|
@@ -1282,108 +1971,135 @@ ${bulletList(rules)}`,
|
|
|
1282
1971
|
}
|
|
1283
1972
|
|
|
1284
1973
|
// src/specTools.ts
|
|
1285
|
-
import { tool as
|
|
1286
|
-
import { z as
|
|
1974
|
+
import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
1975
|
+
import { z as z3 } from "zod";
|
|
1287
1976
|
var MAX_FIRST_ROUND = 4;
|
|
1288
|
-
var questionSchema =
|
|
1289
|
-
id:
|
|
1290
|
-
question:
|
|
1291
|
-
whyItMatters:
|
|
1292
|
-
options:
|
|
1293
|
-
label:
|
|
1294
|
-
description:
|
|
1295
|
-
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")
|
|
1296
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'),
|
|
1297
|
-
multiSelect:
|
|
1986
|
+
multiSelect: z3.boolean().describe("true only when the options are additive and several can apply together")
|
|
1298
1987
|
});
|
|
1299
|
-
var briefSchema =
|
|
1300
|
-
findings:
|
|
1301
|
-
fact:
|
|
1302
|
-
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")
|
|
1303
1992
|
})).describe("Findings from the code"),
|
|
1304
|
-
dataFacts:
|
|
1305
|
-
approach:
|
|
1306
|
-
codeMap:
|
|
1307
|
-
repo:
|
|
1308
|
-
file:
|
|
1309
|
-
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")
|
|
1310
1999
|
})).describe("The files the change will touch or must reuse"),
|
|
1311
|
-
assumptions:
|
|
1312
|
-
text:
|
|
1313
|
-
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")
|
|
1314
2003
|
})).describe("Corners settled with a default instead of a question"),
|
|
1315
|
-
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")
|
|
1316
2005
|
});
|
|
1317
|
-
var questionsPayloadSchema =
|
|
2006
|
+
var questionsPayloadSchema = z3.object({
|
|
1318
2007
|
brief: briefSchema,
|
|
1319
|
-
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`)
|
|
1320
2009
|
});
|
|
1321
|
-
var specSchema =
|
|
1322
|
-
goal:
|
|
1323
|
-
context:
|
|
1324
|
-
inScope:
|
|
1325
|
-
outOfScope:
|
|
1326
|
-
decisions:
|
|
1327
|
-
question:
|
|
1328
|
-
answer:
|
|
1329
|
-
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")
|
|
1330
2019
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
1331
|
-
codeMap:
|
|
1332
|
-
repo:
|
|
1333
|
-
file:
|
|
1334
|
-
role:
|
|
1335
|
-
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")
|
|
1336
2025
|
})).describe("The files a coder must read before starting"),
|
|
1337
|
-
changes:
|
|
1338
|
-
repo:
|
|
1339
|
-
file:
|
|
1340
|
-
action:
|
|
1341
|
-
description:
|
|
1342
|
-
produces:
|
|
1343
|
-
consumes:
|
|
1344
|
-
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")
|
|
1345
2034
|
})).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
|
|
1346
|
-
acceptance:
|
|
1347
|
-
id:
|
|
1348
|
-
kind:
|
|
1349
|
-
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')
|
|
1350
2039
|
})).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
|
|
1351
|
-
testPlan:
|
|
1352
|
-
automated:
|
|
1353
|
-
repo:
|
|
1354
|
-
command:
|
|
1355
|
-
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")
|
|
1356
2045
|
})).describe("At least one command"),
|
|
1357
|
-
newTests:
|
|
1358
|
-
seam:
|
|
1359
|
-
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")
|
|
1360
2049
|
})),
|
|
1361
|
-
manual:
|
|
1362
|
-
page:
|
|
1363
|
-
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")
|
|
1364
2053
|
}))
|
|
1365
2054
|
}),
|
|
1366
|
-
risks:
|
|
1367
|
-
risk:
|
|
1368
|
-
mitigation:
|
|
2055
|
+
risks: z3.array(z3.object({
|
|
2056
|
+
risk: z3.string(),
|
|
2057
|
+
mitigation: z3.string()
|
|
1369
2058
|
})).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
|
|
1370
|
-
riskLevel:
|
|
1371
|
-
assumptions:
|
|
1372
|
-
text:
|
|
1373
|
-
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")
|
|
1374
2063
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
1375
|
-
openQuestions:
|
|
1376
|
-
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")
|
|
1377
2066
|
});
|
|
1378
|
-
|
|
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) {
|
|
1379
2095
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
1380
2096
|
}
|
|
1381
2097
|
function unreachable(error, toolName) {
|
|
1382
2098
|
const message = error instanceof Error ? error.message : String(error);
|
|
1383
|
-
return
|
|
2099
|
+
return reply2(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
|
|
1384
2100
|
}
|
|
1385
2101
|
function questionsTool(review, onSubmit) {
|
|
1386
|
-
return
|
|
2102
|
+
return tool3(
|
|
1387
2103
|
SUBMIT_QUESTIONS_TOOL_NAME,
|
|
1388
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.",
|
|
1389
2105
|
questionsPayloadSchema.shape,
|
|
@@ -1395,10 +2111,10 @@ function questionsTool(review, onSubmit) {
|
|
|
1395
2111
|
return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
|
|
1396
2112
|
}
|
|
1397
2113
|
if (!verdict.accepted) {
|
|
1398
|
-
return
|
|
2114
|
+
return reply2(verdict.feedback, true);
|
|
1399
2115
|
}
|
|
1400
2116
|
onSubmit(args);
|
|
1401
|
-
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.");
|
|
1402
2118
|
}
|
|
1403
2119
|
);
|
|
1404
2120
|
}
|
|
@@ -1406,7 +2122,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
|
|
|
1406
2122
|
function specTool(review, onSubmit, builderCheckRan) {
|
|
1407
2123
|
let repairs = 0;
|
|
1408
2124
|
let reminded = false;
|
|
1409
|
-
return
|
|
2125
|
+
return tool3(
|
|
1410
2126
|
SUBMIT_SPEC_TOOL_NAME,
|
|
1411
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.",
|
|
1412
2128
|
specSchema.shape,
|
|
@@ -1419,14 +2135,36 @@ function specTool(review, onSubmit, builderCheckRan) {
|
|
|
1419
2135
|
}
|
|
1420
2136
|
if (!verdict.accepted) {
|
|
1421
2137
|
repairs += 1;
|
|
1422
|
-
return
|
|
2138
|
+
return reply2(verdict.feedback, true);
|
|
1423
2139
|
}
|
|
1424
2140
|
if (!builderCheckRan() && !reminded) {
|
|
1425
2141
|
reminded = true;
|
|
1426
|
-
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);
|
|
1427
2165
|
}
|
|
1428
2166
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
1429
|
-
return
|
|
2167
|
+
return reply2(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
|
|
1430
2168
|
}
|
|
1431
2169
|
);
|
|
1432
2170
|
}
|
|
@@ -1528,10 +2266,9 @@ function usageLimitWatch() {
|
|
|
1528
2266
|
// src/claude.ts
|
|
1529
2267
|
var NO_RESULT_TEXT = "Claude finished without a result.";
|
|
1530
2268
|
var SERVER_NAME = "tamsense";
|
|
1531
|
-
var LIBRARY_PATH = fileURLToPath(new URL("../library", import.meta.url));
|
|
1532
2269
|
var CAPTION_FIELDS = {
|
|
1533
|
-
title:
|
|
1534
|
-
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"')
|
|
1535
2272
|
};
|
|
1536
2273
|
var ATTACH_TOOLS = {
|
|
1537
2274
|
screenshot: {
|
|
@@ -1552,21 +2289,21 @@ var ATTACH_TOOLS = {
|
|
|
1552
2289
|
};
|
|
1553
2290
|
function attachTool(kind, attach) {
|
|
1554
2291
|
const spec = ATTACH_TOOLS[kind];
|
|
1555
|
-
return
|
|
2292
|
+
return tool4(
|
|
1556
2293
|
spec.name,
|
|
1557
2294
|
spec.description,
|
|
1558
|
-
{ path:
|
|
2295
|
+
{ path: z4.string().describe(spec.path), ...CAPTION_FIELDS },
|
|
1559
2296
|
async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
|
|
1560
2297
|
);
|
|
1561
2298
|
}
|
|
1562
2299
|
function verdictTool(onVerdict) {
|
|
1563
|
-
return
|
|
2300
|
+
return tool4(
|
|
1564
2301
|
VERDICT_TOOL_NAME,
|
|
1565
2302
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
1566
2303
|
{
|
|
1567
|
-
passed:
|
|
1568
|
-
report:
|
|
1569
|
-
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")
|
|
1570
2307
|
},
|
|
1571
2308
|
async (args) => {
|
|
1572
2309
|
onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
|
|
@@ -1575,13 +2312,13 @@ function verdictTool(onVerdict) {
|
|
|
1575
2312
|
);
|
|
1576
2313
|
}
|
|
1577
2314
|
function handoffTool(targets, onHandoff) {
|
|
1578
|
-
return
|
|
2315
|
+
return tool4(
|
|
1579
2316
|
HANDOFF_TOOL_NAME,
|
|
1580
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.",
|
|
1581
2318
|
{
|
|
1582
|
-
to:
|
|
1583
|
-
outcome:
|
|
1584
|
-
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")
|
|
1585
2322
|
},
|
|
1586
2323
|
async (args) => {
|
|
1587
2324
|
const choice = chooseHandoff(targets, args);
|
|
@@ -1594,25 +2331,25 @@ function handoffTool(targets, onHandoff) {
|
|
|
1594
2331
|
);
|
|
1595
2332
|
}
|
|
1596
2333
|
function askPersonTool(asking, waiting) {
|
|
1597
|
-
return
|
|
2334
|
+
return tool4(
|
|
1598
2335
|
ASK_PERSON_TOOL_NAME,
|
|
1599
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.",
|
|
1600
2337
|
{
|
|
1601
|
-
to:
|
|
1602
|
-
why:
|
|
1603
|
-
question:
|
|
1604
|
-
context:
|
|
1605
|
-
tried:
|
|
1606
|
-
did:
|
|
1607
|
-
error:
|
|
1608
|
-
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")
|
|
1609
2346
|
})).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
|
|
1610
|
-
options:
|
|
1611
|
-
label:
|
|
1612
|
-
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")
|
|
1613
2350
|
})).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
|
|
1614
|
-
blocking:
|
|
1615
|
-
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")
|
|
1616
2353
|
},
|
|
1617
2354
|
async (args) => {
|
|
1618
2355
|
if (waiting.waitingFor !== null) {
|
|
@@ -1622,16 +2359,16 @@ function askPersonTool(asking, waiting) {
|
|
|
1622
2359
|
if ("error" in choice) {
|
|
1623
2360
|
return { content: [{ type: "text", text: choice.error }], isError: true };
|
|
1624
2361
|
}
|
|
1625
|
-
let
|
|
2362
|
+
let reply3;
|
|
1626
2363
|
try {
|
|
1627
|
-
|
|
2364
|
+
reply3 = await asking.send(choice.body);
|
|
1628
2365
|
} catch (error) {
|
|
1629
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 };
|
|
1630
2367
|
}
|
|
1631
2368
|
if (choice.body.blocking) {
|
|
1632
|
-
waiting.waitingFor =
|
|
2369
|
+
waiting.waitingFor = reply3.id;
|
|
1633
2370
|
}
|
|
1634
|
-
return { content: [{ type: "text", text: askedText(
|
|
2371
|
+
return { content: [{ type: "text", text: askedText(reply3.name, choice.body) }] };
|
|
1635
2372
|
}
|
|
1636
2373
|
);
|
|
1637
2374
|
}
|
|
@@ -1639,8 +2376,13 @@ function specWorkTools(specWork, submitted) {
|
|
|
1639
2376
|
if (!specWork) {
|
|
1640
2377
|
return [];
|
|
1641
2378
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
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;
|
|
1644
2386
|
})] : [specTool(specWork.review, (spec) => {
|
|
1645
2387
|
submitted.spec = spec;
|
|
1646
2388
|
}, () => submitted.helperStarted)];
|
|
@@ -1668,7 +2410,9 @@ function tamsenseServer(request, submitted) {
|
|
|
1668
2410
|
...knowledgeTools(request.knowledge),
|
|
1669
2411
|
...request.handoffs.length > 0 ? [handoffTool(request.handoffs, (handoff) => {
|
|
1670
2412
|
submitted.handoff = handoff;
|
|
1671
|
-
})] : []
|
|
2413
|
+
})] : [],
|
|
2414
|
+
...request.results ? resultToolset(request.results) : [],
|
|
2415
|
+
...request.handOver ? [handOverTool(request.handOver)] : []
|
|
1672
2416
|
];
|
|
1673
2417
|
return {
|
|
1674
2418
|
server: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }),
|
|
@@ -1695,6 +2439,7 @@ async function attempt(request, context, resume) {
|
|
|
1695
2439
|
let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
|
|
1696
2440
|
const tamsense = tamsenseServer(request, context.submitted);
|
|
1697
2441
|
const shown = hiddenStepsFilter();
|
|
2442
|
+
const helpers = helperTasks();
|
|
1698
2443
|
const messages = context.startQuery({
|
|
1699
2444
|
prompt: request.input.start(request.prompt.prompt),
|
|
1700
2445
|
options: {
|
|
@@ -1706,7 +2451,7 @@ async function attempt(request, context, resume) {
|
|
|
1706
2451
|
thinking: { type: "adaptive", display: "summarized" },
|
|
1707
2452
|
// режим auto сам решает, что можно; о чём он спросил бы человека, решают правила проекта или люди задачи
|
|
1708
2453
|
canUseTool: request.permissions ? askPermission(request.permissions, () => context.lastWords.text, request.cwd) : denyWhenAsked,
|
|
1709
|
-
plugins: [{ type: "local", path:
|
|
2454
|
+
plugins: [{ type: "local", path: request.library }],
|
|
1710
2455
|
mcpServers: { [SERVER_NAME]: tamsense.server },
|
|
1711
2456
|
allowedTools: tamsense.allowed,
|
|
1712
2457
|
abortController: context.abortController,
|
|
@@ -1716,6 +2461,7 @@ async function attempt(request, context, resume) {
|
|
|
1716
2461
|
}
|
|
1717
2462
|
});
|
|
1718
2463
|
request.input.onSwitch((change) => applySwitch(messages, change));
|
|
2464
|
+
request.input.onStopHelper((ref) => helpers.stop(messages, ref));
|
|
1719
2465
|
const failedToResume = () => resume !== null && sessionId === null && !request.signal.aborted;
|
|
1720
2466
|
try {
|
|
1721
2467
|
for await (const message of messages) {
|
|
@@ -1727,7 +2473,7 @@ async function attempt(request, context, resume) {
|
|
|
1727
2473
|
messages.close();
|
|
1728
2474
|
return { outcome, sessionId, resumeFailed: true };
|
|
1729
2475
|
}
|
|
1730
|
-
for (const draft of feedOf(message).filter(shown)) {
|
|
2476
|
+
for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
|
|
1731
2477
|
if (draft.kind === "agent" && draft.parent === null) {
|
|
1732
2478
|
context.submitted.helperStarted = true;
|
|
1733
2479
|
}
|
|
@@ -1778,13 +2524,13 @@ function claudeRunner(startQuery) {
|
|
|
1778
2524
|
var runClaude = claudeRunner(query2);
|
|
1779
2525
|
|
|
1780
2526
|
// src/config.ts
|
|
1781
|
-
import { mkdir as
|
|
2527
|
+
import { mkdir as mkdir3, readFile as readFile3, rm as rm4, writeFile as writeFile3 } from "fs/promises";
|
|
1782
2528
|
import { homedir as homedir2 } from "os";
|
|
1783
|
-
import { dirname as dirname2, join as
|
|
2529
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
1784
2530
|
var OWNER_ONLY_FILE = 384;
|
|
1785
2531
|
var OWNER_ONLY_FOLDER = 448;
|
|
1786
2532
|
function configPath(home = homedir2()) {
|
|
1787
|
-
return
|
|
2533
|
+
return join4(home, ".tamsense", "runner.json");
|
|
1788
2534
|
}
|
|
1789
2535
|
function isRecord2(value) {
|
|
1790
2536
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -1802,7 +2548,7 @@ function isMissingFile(error) {
|
|
|
1802
2548
|
}
|
|
1803
2549
|
async function readConfig(path = configPath()) {
|
|
1804
2550
|
try {
|
|
1805
|
-
return parseConfig(JSON.parse(await
|
|
2551
|
+
return parseConfig(JSON.parse(await readFile3(path, "utf8")));
|
|
1806
2552
|
} catch (error) {
|
|
1807
2553
|
if (isMissingFile(error) || error instanceof SyntaxError) {
|
|
1808
2554
|
return null;
|
|
@@ -1811,34 +2557,12 @@ async function readConfig(path = configPath()) {
|
|
|
1811
2557
|
}
|
|
1812
2558
|
}
|
|
1813
2559
|
async function writeConfig(config, path = configPath()) {
|
|
1814
|
-
await
|
|
1815
|
-
await
|
|
2560
|
+
await mkdir3(dirname2(path), { recursive: true, mode: OWNER_ONLY_FOLDER });
|
|
2561
|
+
await writeFile3(path, `${JSON.stringify(config, null, 4)}
|
|
1816
2562
|
`, { mode: OWNER_ONLY_FILE });
|
|
1817
2563
|
}
|
|
1818
2564
|
async function removeConfig(path = configPath()) {
|
|
1819
|
-
await
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
// src/protocol.ts
|
|
1823
|
-
var MAX_ATTACHMENT_BYTES = 5 * 1024 * 1024;
|
|
1824
|
-
var MAX_ATTACHMENTS_PER_RUN = 20;
|
|
1825
|
-
var MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
1826
|
-
var PAIRINGS_PATH = "/api/runner/pairings";
|
|
1827
|
-
var PAIRING_TOKEN_PATH = "/api/runner/pairings/token";
|
|
1828
|
-
var IDENTITY_PATH = "/api/runner/me";
|
|
1829
|
-
var NEXT_RUN_PATH = "/api/runner/runs/next";
|
|
1830
|
-
var CAPABILITIES_PATH = "/api/runner/me/capabilities";
|
|
1831
|
-
function runPath(runId, action) {
|
|
1832
|
-
return `/api/runner/runs/${runId}/${action}`;
|
|
1833
|
-
}
|
|
1834
|
-
function knowledgePath(runId, entryId) {
|
|
1835
|
-
return `/api/runner/runs/${runId}/knowledge/${entryId}`;
|
|
1836
|
-
}
|
|
1837
|
-
function permissionPath(runId, permissionId) {
|
|
1838
|
-
return `/api/runner/runs/${runId}/permissions/${permissionId}`;
|
|
1839
|
-
}
|
|
1840
|
-
function recordingUploadPath(runId, upload, part) {
|
|
1841
|
-
return `/api/runner/runs/${runId}/recordings/${upload}/${part}`;
|
|
2565
|
+
await rm4(path, { force: true });
|
|
1842
2566
|
}
|
|
1843
2567
|
|
|
1844
2568
|
// src/login.ts
|
|
@@ -1855,14 +2579,14 @@ async function login(tools, request) {
|
|
|
1855
2579
|
tools.openBrowser(pageUrl);
|
|
1856
2580
|
while (tools.now() < expiresAt) {
|
|
1857
2581
|
await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
|
|
1858
|
-
const
|
|
1859
|
-
if (
|
|
1860
|
-
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 };
|
|
1861
2585
|
}
|
|
1862
|
-
if (
|
|
2586
|
+
if (reply3.status === "denied") {
|
|
1863
2587
|
throw new Error(DENIED_TEXT);
|
|
1864
2588
|
}
|
|
1865
|
-
if (
|
|
2589
|
+
if (reply3.status === "expired") {
|
|
1866
2590
|
break;
|
|
1867
2591
|
}
|
|
1868
2592
|
}
|
|
@@ -1870,14 +2594,14 @@ async function login(tools, request) {
|
|
|
1870
2594
|
}
|
|
1871
2595
|
|
|
1872
2596
|
// src/recorderEnv.ts
|
|
1873
|
-
import { existsSync as
|
|
1874
|
-
import { join as
|
|
1875
|
-
import { fileURLToPath
|
|
2597
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2598
|
+
import { join as join6 } from "path";
|
|
2599
|
+
import { fileURLToPath } from "url";
|
|
1876
2600
|
|
|
1877
2601
|
// src/recorder.ts
|
|
1878
2602
|
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
1879
2603
|
import Module, { createRequire } from "module";
|
|
1880
|
-
import { basename, dirname as dirname3, join as
|
|
2604
|
+
import { basename as basename2, dirname as dirname3, join as join5 } from "path";
|
|
1881
2605
|
import process2 from "process";
|
|
1882
2606
|
var RECORDINGS_DIR_ENV = "TAMSENSE_RECORDINGS_DIR";
|
|
1883
2607
|
var SERVER_ENV = "TAMSENSE_SERVER";
|
|
@@ -1950,7 +2674,7 @@ function clip2(text2, max) {
|
|
|
1950
2674
|
return text2.length > max ? `${text2.slice(0, max - 1).trimEnd()}\u2026` : text2;
|
|
1951
2675
|
}
|
|
1952
2676
|
function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
1953
|
-
const from = script2 ? `, opened by ${
|
|
2677
|
+
const from = script2 ? `, opened by ${basename2(script2)}` : "";
|
|
1954
2678
|
return {
|
|
1955
2679
|
title: clip2(env[DEFAULT_TITLE_ENV]?.trim() || "Browser session", MAX_TITLE_LENGTH),
|
|
1956
2680
|
purpose: clip2(`${env[DEFAULT_PURPOSE_ENV]?.trim() || "Recorded automatically while the agent used a browser"}${from}`, MAX_PURPOSE_LENGTH)
|
|
@@ -1958,7 +2682,7 @@ function automaticCaption(env = process2.env, script2 = process2.argv[1]) {
|
|
|
1958
2682
|
}
|
|
1959
2683
|
function rrwebSource() {
|
|
1960
2684
|
const entry = createRequire(import.meta.url).resolve("rrweb");
|
|
1961
|
-
return readFileSync(
|
|
2685
|
+
return readFileSync(join5(dirname3(entry), "rrweb.umd.min.cjs"), "utf8");
|
|
1962
2686
|
}
|
|
1963
2687
|
function pageScript(rrweb) {
|
|
1964
2688
|
return `(() => {
|
|
@@ -2009,8 +2733,8 @@ var Session = class {
|
|
|
2009
2733
|
bytes = 0;
|
|
2010
2734
|
constructor(dir, recording, url) {
|
|
2011
2735
|
const id = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
|
|
2012
|
-
this.file =
|
|
2013
|
-
this.metaFile =
|
|
2736
|
+
this.file = join5(dir, `${id}${REPLAY_SUFFIX}`);
|
|
2737
|
+
this.metaFile = join5(dir, `${id}${META_SUFFIX}`);
|
|
2014
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 };
|
|
2015
2739
|
writeFileSync(this.file, "");
|
|
2016
2740
|
this.save();
|
|
@@ -2182,11 +2906,11 @@ if (process2.env[RECORDINGS_DIR_ENV]) {
|
|
|
2182
2906
|
// src/recorderEnv.ts
|
|
2183
2907
|
var RECORDER_ENV = "TAMSENSE_RECORDER";
|
|
2184
2908
|
function bundledRecorder() {
|
|
2185
|
-
const path =
|
|
2186
|
-
return
|
|
2909
|
+
const path = fileURLToPath(new URL("./recorder.cjs", import.meta.url));
|
|
2910
|
+
return existsSync3(path) ? path : null;
|
|
2187
2911
|
}
|
|
2188
2912
|
function recordingsFolder(home, runId) {
|
|
2189
|
-
return
|
|
2913
|
+
return join6(home, ".tamsense", "recordings", `run-${runId}`);
|
|
2190
2914
|
}
|
|
2191
2915
|
function nodeOptionsWithRecorder(recorder, nodeOptions) {
|
|
2192
2916
|
return [nodeOptions?.trim(), `--require "${recorder.replaceAll("\\", "/")}"`].filter(Boolean).join(" ");
|
|
@@ -2213,7 +2937,7 @@ function describeDevice(identity) {
|
|
|
2213
2937
|
}
|
|
2214
2938
|
|
|
2215
2939
|
// src/system.ts
|
|
2216
|
-
import { spawn as
|
|
2940
|
+
import { spawn as spawn3 } from "child_process";
|
|
2217
2941
|
import { hostname } from "os";
|
|
2218
2942
|
import { delimiter as delimiter2 } from "path";
|
|
2219
2943
|
import { setTimeout as wait } from "timers/promises";
|
|
@@ -2222,12 +2946,12 @@ var PLATFORM_LABELS = {
|
|
|
2222
2946
|
darwin: "macOS",
|
|
2223
2947
|
linux: "Linux"
|
|
2224
2948
|
};
|
|
2225
|
-
var
|
|
2949
|
+
var MAX_NAME_LENGTH2 = 80;
|
|
2226
2950
|
function platformLabel(platform = process.platform) {
|
|
2227
2951
|
return PLATFORM_LABELS[platform] ?? platform;
|
|
2228
2952
|
}
|
|
2229
2953
|
function defaultDeviceName() {
|
|
2230
|
-
return hostname().slice(0,
|
|
2954
|
+
return hostname().slice(0, MAX_NAME_LENGTH2) || "My computer";
|
|
2231
2955
|
}
|
|
2232
2956
|
function browserCommand(url, platform) {
|
|
2233
2957
|
if (platform === "win32") {
|
|
@@ -2237,7 +2961,7 @@ function browserCommand(url, platform) {
|
|
|
2237
2961
|
}
|
|
2238
2962
|
function openBrowser(url, platform = process.platform) {
|
|
2239
2963
|
const [command, args] = browserCommand(url, platform);
|
|
2240
|
-
const child =
|
|
2964
|
+
const child = spawn3(command, args, { detached: true, stdio: "ignore" });
|
|
2241
2965
|
child.on("error", () => {
|
|
2242
2966
|
});
|
|
2243
2967
|
child.unref();
|
|
@@ -2257,7 +2981,7 @@ async function sleep(ms, signal) {
|
|
|
2257
2981
|
}
|
|
2258
2982
|
|
|
2259
2983
|
// src/update.ts
|
|
2260
|
-
import { spawn as
|
|
2984
|
+
import { spawn as spawn4 } from "child_process";
|
|
2261
2985
|
import { readFileSync as readFileSync2 } from "fs";
|
|
2262
2986
|
import process3 from "process";
|
|
2263
2987
|
var PACKAGE_NAME = "@tamsensedev/runner";
|
|
@@ -2312,8 +3036,8 @@ function relaunchArgs(argv) {
|
|
|
2312
3036
|
}
|
|
2313
3037
|
function relaunch(argv, env) {
|
|
2314
3038
|
const isWindows = process3.platform === "win32";
|
|
2315
|
-
return new Promise((
|
|
2316
|
-
const child =
|
|
3039
|
+
return new Promise((resolve3, reject) => {
|
|
3040
|
+
const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
|
|
2317
3041
|
const ignoreInterrupt = () => {
|
|
2318
3042
|
};
|
|
2319
3043
|
process3.on("SIGINT", ignoreInterrupt);
|
|
@@ -2323,13 +3047,13 @@ function relaunch(argv, env) {
|
|
|
2323
3047
|
});
|
|
2324
3048
|
child.on("close", (code) => {
|
|
2325
3049
|
process3.off("SIGINT", ignoreInterrupt);
|
|
2326
|
-
|
|
3050
|
+
resolve3(code ?? 1);
|
|
2327
3051
|
});
|
|
2328
3052
|
});
|
|
2329
3053
|
}
|
|
2330
3054
|
|
|
2331
3055
|
// src/worker.ts
|
|
2332
|
-
import { rm as
|
|
3056
|
+
import { rm as rm6 } from "fs/promises";
|
|
2333
3057
|
import { dirname as dirname5 } from "path";
|
|
2334
3058
|
import process4 from "process";
|
|
2335
3059
|
|
|
@@ -2369,37 +3093,43 @@ function feedQueue(send) {
|
|
|
2369
3093
|
};
|
|
2370
3094
|
}
|
|
2371
3095
|
|
|
2372
|
-
// src/
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
};
|
|
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;
|
|
2396
3126
|
}
|
|
2397
3127
|
|
|
2398
3128
|
// src/recordings.ts
|
|
2399
3129
|
import { randomUUID } from "crypto";
|
|
2400
3130
|
import { createReadStream } from "fs";
|
|
2401
|
-
import { readdir, readFile as
|
|
2402
|
-
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";
|
|
2403
3133
|
import { createInterface as createInterface2 } from "readline";
|
|
2404
3134
|
import { Readable } from "stream";
|
|
2405
3135
|
import { promisify } from "util";
|
|
@@ -2460,23 +3190,23 @@ function marksFrom(marks, startTs) {
|
|
|
2460
3190
|
return marks.map((mark) => ({ at: Math.max(0, mark.ts - startTs), label: mark.label })).sort((a, b) => a.at - b.at);
|
|
2461
3191
|
}
|
|
2462
3192
|
function sessionIdOf(file) {
|
|
2463
|
-
return
|
|
3193
|
+
return basename3(file).replace(REPLAY_SUFFIX, "");
|
|
2464
3194
|
}
|
|
2465
3195
|
async function readMeta(file) {
|
|
2466
3196
|
try {
|
|
2467
|
-
return JSON.parse(await
|
|
3197
|
+
return JSON.parse(await readFile4(file, "utf8"));
|
|
2468
3198
|
} catch {
|
|
2469
3199
|
return null;
|
|
2470
3200
|
}
|
|
2471
3201
|
}
|
|
2472
3202
|
async function metaFor(kind, file) {
|
|
2473
3203
|
if (kind === "replay") {
|
|
2474
|
-
return readMeta(
|
|
3204
|
+
return readMeta(join8(dirname4(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
2475
3205
|
}
|
|
2476
|
-
const folder =
|
|
3206
|
+
const folder = basename3(dirname4(file)) === "video" ? dirname4(dirname4(file)) : dirname4(file);
|
|
2477
3207
|
for (const name of await readdir(folder).catch(() => [])) {
|
|
2478
3208
|
if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
|
|
2479
|
-
const meta = await readMeta(
|
|
3209
|
+
const meta = await readMeta(join8(folder, name));
|
|
2480
3210
|
if (meta?.video === file) {
|
|
2481
3211
|
return meta;
|
|
2482
3212
|
}
|
|
@@ -2486,7 +3216,7 @@ async function metaFor(kind, file) {
|
|
|
2486
3216
|
}
|
|
2487
3217
|
async function ledger(dir) {
|
|
2488
3218
|
try {
|
|
2489
|
-
return new Set(JSON.parse(await
|
|
3219
|
+
return new Set(JSON.parse(await readFile4(join8(dir, LEDGER_FILE), "utf8")));
|
|
2490
3220
|
} catch {
|
|
2491
3221
|
return /* @__PURE__ */ new Set();
|
|
2492
3222
|
}
|
|
@@ -2494,10 +3224,10 @@ async function ledger(dir) {
|
|
|
2494
3224
|
async function remember(dir, file) {
|
|
2495
3225
|
const done2 = await ledger(dir);
|
|
2496
3226
|
done2.add(file);
|
|
2497
|
-
await
|
|
3227
|
+
await writeFile4(join8(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
2498
3228
|
}
|
|
2499
3229
|
async function nonEmpty(file) {
|
|
2500
|
-
return file !== null && ((await
|
|
3230
|
+
return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
|
|
2501
3231
|
}
|
|
2502
3232
|
async function pendingRecordings(dir) {
|
|
2503
3233
|
const done2 = await ledger(dir);
|
|
@@ -2507,8 +3237,8 @@ async function pendingRecordings(dir) {
|
|
|
2507
3237
|
if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
|
|
2508
3238
|
continue;
|
|
2509
3239
|
}
|
|
2510
|
-
const meta = await readMeta(
|
|
2511
|
-
const replay =
|
|
3240
|
+
const meta = await readMeta(join8(dir, name));
|
|
3241
|
+
const replay = join8(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
2512
3242
|
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
2513
3243
|
pending.push({ kind: "replay", file: replay, meta });
|
|
2514
3244
|
}
|
|
@@ -2528,11 +3258,11 @@ async function uploadReplay(upload, file, caption) {
|
|
|
2528
3258
|
if (!scan.snapshot) {
|
|
2529
3259
|
throw new Error("This replay has no page snapshot, so it cannot be played. Was the page ever opened?");
|
|
2530
3260
|
}
|
|
2531
|
-
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) });
|
|
2532
3262
|
}
|
|
2533
3263
|
async function uploadVideo(upload, file, caption, meta) {
|
|
2534
|
-
const contentType = VIDEO_TYPES_BY_EXTENSION[
|
|
2535
|
-
const info = await
|
|
3264
|
+
const contentType = VIDEO_TYPES_BY_EXTENSION[extname2(file).toLowerCase()];
|
|
3265
|
+
const info = await stat2(file);
|
|
2536
3266
|
if (info.size > MAX_VIDEO_BYTES) {
|
|
2537
3267
|
throw new Error(`The video is larger than ${MAX_VIDEO_BYTES / 1024 / 1024} MB. Record a shorter one.`);
|
|
2538
3268
|
}
|
|
@@ -2543,16 +3273,16 @@ async function uploadVideo(upload, file, caption, meta) {
|
|
|
2543
3273
|
const endedAt = meta?.endedAt ?? info.mtimeMs;
|
|
2544
3274
|
const durationMs = Math.max(0, Math.round(endedAt - startedAt));
|
|
2545
3275
|
const marks = marksFrom(meta?.marks ?? [], startedAt).filter((mark) => mark.at <= durationMs);
|
|
2546
|
-
return finish(upload, { kind: "video", upload: id, name:
|
|
3276
|
+
return finish(upload, { kind: "video", upload: id, name: basename3(file), ...caption, contentType, durationMs, marks });
|
|
2547
3277
|
}
|
|
2548
3278
|
async function finish(upload, body) {
|
|
2549
3279
|
return (await upload.api.request("POST", runPath(upload.runId, "recordings"), body)).id;
|
|
2550
3280
|
}
|
|
2551
3281
|
async function attachRecording(api, runId, recordingsDir2, request) {
|
|
2552
|
-
if (!
|
|
3282
|
+
if (!isAbsolute2(request.path)) {
|
|
2553
3283
|
return `Give the absolute path of the ${request.kind === "replay" ? "replay (.rrweb.ndjson)" : "video (.webm or .mp4)"} file.`;
|
|
2554
3284
|
}
|
|
2555
|
-
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;
|
|
2556
3286
|
if (!fitsKind) {
|
|
2557
3287
|
return request.kind === "replay" ? `A replay is the ${REPLAY_SUFFIX} file the TAMsense recorder wrote.` : "Only WebM and MP4 videos can be attached.";
|
|
2558
3288
|
}
|
|
@@ -2617,19 +3347,19 @@ async function uploadPendingRecordings(api, runId, dir) {
|
|
|
2617
3347
|
}
|
|
2618
3348
|
|
|
2619
3349
|
// src/screenshot.ts
|
|
2620
|
-
import { readFile as
|
|
2621
|
-
import { basename as
|
|
2622
|
-
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 = {
|
|
2623
3353
|
".png": "image/png",
|
|
2624
3354
|
".jpg": "image/jpeg",
|
|
2625
3355
|
".jpeg": "image/jpeg",
|
|
2626
3356
|
".webp": "image/webp"
|
|
2627
3357
|
};
|
|
2628
3358
|
async function readScreenshot(request) {
|
|
2629
|
-
if (!
|
|
3359
|
+
if (!isAbsolute3(request.path)) {
|
|
2630
3360
|
return "Give the absolute path of the image file.";
|
|
2631
3361
|
}
|
|
2632
|
-
const contentType =
|
|
3362
|
+
const contentType = TYPES_BY_EXTENSION2[extname3(request.path).toLowerCase()];
|
|
2633
3363
|
if (!contentType) {
|
|
2634
3364
|
return "Only PNG, JPEG and WebP images can be attached.";
|
|
2635
3365
|
}
|
|
@@ -2639,158 +3369,15 @@ async function readScreenshot(request) {
|
|
|
2639
3369
|
} catch (error) {
|
|
2640
3370
|
return error instanceof Error ? error.message : String(error);
|
|
2641
3371
|
}
|
|
2642
|
-
const info = await
|
|
3372
|
+
const info = await stat3(request.path).catch(() => null);
|
|
2643
3373
|
if (!info?.isFile()) {
|
|
2644
3374
|
return `There is no file at ${request.path}.`;
|
|
2645
3375
|
}
|
|
2646
3376
|
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
2647
3377
|
return "The image is larger than 5 MB. Take a smaller screenshot (viewport instead of full page, or JPEG).";
|
|
2648
3378
|
}
|
|
2649
|
-
const data = (await
|
|
2650
|
-
return { name:
|
|
2651
|
-
}
|
|
2652
|
-
|
|
2653
|
-
// src/workspace.ts
|
|
2654
|
-
import { existsSync as existsSync3 } from "fs";
|
|
2655
|
-
import { rm as rm3 } from "fs/promises";
|
|
2656
|
-
import { join as join6 } from "path";
|
|
2657
|
-
|
|
2658
|
-
// src/git.ts
|
|
2659
|
-
import { Buffer } from "buffer";
|
|
2660
|
-
import { spawn as spawn4 } from "child_process";
|
|
2661
|
-
var GITHUB_ORIGIN = "https://github.com/";
|
|
2662
|
-
function githubAuthEnv(token) {
|
|
2663
|
-
const credentials = Buffer.from(`x-access-token:${token}`).toString("base64");
|
|
2664
|
-
return {
|
|
2665
|
-
GIT_CONFIG_COUNT: "1",
|
|
2666
|
-
GIT_CONFIG_KEY_0: `http.${GITHUB_ORIGIN}.extraheader`,
|
|
2667
|
-
GIT_CONFIG_VALUE_0: `AUTHORIZATION: basic ${credentials}`,
|
|
2668
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
function runGit(args, options) {
|
|
2672
|
-
const env = { ...process.env, ...options.env, ...options.token ? githubAuthEnv(options.token) : {} };
|
|
2673
|
-
return new Promise((resolve, reject) => {
|
|
2674
|
-
const child = spawn4("git", args, { cwd: options.cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
2675
|
-
let stdout = "";
|
|
2676
|
-
let stderr = "";
|
|
2677
|
-
child.stdout.on("data", (chunk) => {
|
|
2678
|
-
stdout += chunk.toString();
|
|
2679
|
-
});
|
|
2680
|
-
child.stderr.on("data", (chunk) => {
|
|
2681
|
-
stderr += chunk.toString();
|
|
2682
|
-
});
|
|
2683
|
-
child.on("error", reject);
|
|
2684
|
-
child.on("close", (code) => resolve({ code: code ?? 1, stdout: stdout.trim(), stderr: stderr.trim() }));
|
|
2685
|
-
});
|
|
2686
|
-
}
|
|
2687
|
-
async function git(args, options) {
|
|
2688
|
-
const result = await runGit(args, options);
|
|
2689
|
-
if (result.code !== 0) {
|
|
2690
|
-
throw new Error(`git ${args[0]} failed: ${result.stderr || result.stdout || `exit code ${result.code}`}`);
|
|
2691
|
-
}
|
|
2692
|
-
return result.stdout;
|
|
2693
|
-
}
|
|
2694
|
-
|
|
2695
|
-
// src/workspace.ts
|
|
2696
|
-
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
2697
|
-
var cacheLock = keyedLock();
|
|
2698
|
-
function githubRemoteUrl(fullName) {
|
|
2699
|
-
return `https://github.com/${fullName}.git`;
|
|
2700
|
-
}
|
|
2701
|
-
function tamsenseFolder(home, ...parts) {
|
|
2702
|
-
return join6(home, ".tamsense", ...parts);
|
|
2703
|
-
}
|
|
2704
|
-
function folderNames(repositories) {
|
|
2705
|
-
const shortName = (fullName) => fullName.split("/").pop() ?? fullName;
|
|
2706
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2707
|
-
for (const repository of repositories) {
|
|
2708
|
-
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
2709
|
-
}
|
|
2710
|
-
return repositories.map((repository) => counts.get(shortName(repository.fullName)) === 1 ? shortName(repository.fullName) : repository.fullName.replace("/", "__"));
|
|
2711
|
-
}
|
|
2712
|
-
async function refreshCache(cache, repository, token, tools) {
|
|
2713
|
-
if (existsSync3(join6(cache, ".git"))) {
|
|
2714
|
-
await git(["fetch", "--prune", "origin"], { cwd: cache, token });
|
|
2715
|
-
return;
|
|
2716
|
-
}
|
|
2717
|
-
await git(["clone", "--no-checkout", tools.remoteUrl(repository.fullName), cache], { cwd: tools.home, token });
|
|
2718
|
-
}
|
|
2719
|
-
async function removeWorktree(cache, path) {
|
|
2720
|
-
await runGit(["worktree", "remove", "--force", path], { cwd: cache });
|
|
2721
|
-
await rm3(path, { recursive: true, force: true });
|
|
2722
|
-
await git(["worktree", "prune"], { cwd: cache });
|
|
2723
|
-
}
|
|
2724
|
-
async function baseRef(cache, job, repository) {
|
|
2725
|
-
const pushed = await runGit(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${job.branch}`], { cwd: cache });
|
|
2726
|
-
return pushed.code === 0 ? `origin/${job.branch}` : `origin/${repository.defaultBranch}`;
|
|
2727
|
-
}
|
|
2728
|
-
async function keptBase(cache, path, job, repository) {
|
|
2729
|
-
if (!job.keepWorkspace || !existsSync3(join6(path, ".git"))) {
|
|
2730
|
-
return null;
|
|
2731
|
-
}
|
|
2732
|
-
const branch = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path });
|
|
2733
|
-
if (branch.code !== 0 || branch.stdout !== job.branch) {
|
|
2734
|
-
return null;
|
|
2735
|
-
}
|
|
2736
|
-
return git(["rev-parse", await baseRef(cache, job, repository)], { cwd: cache });
|
|
2737
|
-
}
|
|
2738
|
-
async function prepareWorkspace(job, token, tools) {
|
|
2739
|
-
const root = tamsenseFolder(tools.home, "work", `task-${job.taskId}`);
|
|
2740
|
-
const folders = folderNames(job.repositories);
|
|
2741
|
-
const repositories = [];
|
|
2742
|
-
for (const [index, repository] of job.repositories.entries()) {
|
|
2743
|
-
const cache = tamsenseFolder(tools.home, "repos", repository.fullName.replace("/", "__"));
|
|
2744
|
-
const folder = folders[index] ?? repository.fullName;
|
|
2745
|
-
const path = join6(root, folder);
|
|
2746
|
-
const baseSha = await cacheLock.run(repository.fullName, async () => {
|
|
2747
|
-
await refreshCache(cache, repository, token, tools);
|
|
2748
|
-
const kept = await keptBase(cache, path, job, repository);
|
|
2749
|
-
if (kept) {
|
|
2750
|
-
return kept;
|
|
2751
|
-
}
|
|
2752
|
-
await removeWorktree(cache, path);
|
|
2753
|
-
await git(["worktree", "add", "--force", "-B", job.branch, path, await baseRef(cache, job, repository)], { cwd: cache });
|
|
2754
|
-
return git(["rev-parse", "HEAD"], { cwd: path });
|
|
2755
|
-
});
|
|
2756
|
-
repositories.push({ fullName: repository.fullName, folder, path, baseSha });
|
|
2757
|
-
}
|
|
2758
|
-
return { root, repositories };
|
|
2759
|
-
}
|
|
2760
|
-
async function commitIdentityEnv(home) {
|
|
2761
|
-
const configured = await runGit(["config", "--get", "user.email"], { cwd: home });
|
|
2762
|
-
if (configured.code === 0 && configured.stdout) {
|
|
2763
|
-
return {};
|
|
2764
|
-
}
|
|
2765
|
-
return {
|
|
2766
|
-
GIT_AUTHOR_NAME: FALLBACK_IDENTITY.name,
|
|
2767
|
-
GIT_AUTHOR_EMAIL: FALLBACK_IDENTITY.email,
|
|
2768
|
-
GIT_COMMITTER_NAME: FALLBACK_IDENTITY.name,
|
|
2769
|
-
GIT_COMMITTER_EMAIL: FALLBACK_IDENTITY.email
|
|
2770
|
-
};
|
|
2771
|
-
}
|
|
2772
|
-
async function publishWorkspace(workspace, job, token, identity) {
|
|
2773
|
-
const pushed = [];
|
|
2774
|
-
for (const repository of workspace.repositories) {
|
|
2775
|
-
const options = { cwd: repository.path, env: identity };
|
|
2776
|
-
const sha = await cacheLock.run(repository.fullName, async () => {
|
|
2777
|
-
await git(["add", "--all"], options);
|
|
2778
|
-
const hasStaged = (await runGit(["diff", "--cached", "--quiet"], options)).code === 1;
|
|
2779
|
-
if (hasStaged) {
|
|
2780
|
-
await git(["commit", "--message", `Task #${job.taskId}: ${job.title}`], options);
|
|
2781
|
-
}
|
|
2782
|
-
const newCommits = Number(await git(["rev-list", "--count", `${repository.baseSha}..HEAD`], options));
|
|
2783
|
-
if (newCommits === 0) {
|
|
2784
|
-
return null;
|
|
2785
|
-
}
|
|
2786
|
-
await git(["push", "origin", `HEAD:refs/heads/${job.branch}`], { ...options, token });
|
|
2787
|
-
return git(["rev-parse", "HEAD"], options);
|
|
2788
|
-
});
|
|
2789
|
-
if (sha) {
|
|
2790
|
-
pushed.push({ repository: repository.fullName, branch: job.branch, sha });
|
|
2791
|
-
}
|
|
2792
|
-
}
|
|
2793
|
-
return pushed;
|
|
3379
|
+
const data = (await readFile5(request.path)).toString("base64");
|
|
3380
|
+
return { name: basename4(request.path), ...caption, contentType, data };
|
|
2794
3381
|
}
|
|
2795
3382
|
|
|
2796
3383
|
// src/worker.ts
|
|
@@ -2853,17 +3440,31 @@ function messageLine(message) {
|
|
|
2853
3440
|
const line = `Message from ${message.authorName}: ${message.text}`.replace(/\s+/g, " ").trim();
|
|
2854
3441
|
return line.length > MAX_MESSAGE_LINE ? `${line.slice(0, MAX_MESSAGE_LINE - 1)}\u2026` : line;
|
|
2855
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
|
+
}
|
|
2856
3456
|
function inbox(job, tools, record, closedOnSite) {
|
|
2857
3457
|
const input = inputChannel({ onIdle: () => void poll() });
|
|
3458
|
+
const stopsSeen = /* @__PURE__ */ new Set();
|
|
2858
3459
|
let running = null;
|
|
2859
3460
|
let again = false;
|
|
2860
3461
|
let failing = false;
|
|
2861
3462
|
async function check() {
|
|
2862
3463
|
const ack = input.takeDelivered();
|
|
2863
3464
|
const body = { ack };
|
|
2864
|
-
let
|
|
3465
|
+
let reply3;
|
|
2865
3466
|
try {
|
|
2866
|
-
|
|
3467
|
+
reply3 = await tools.api.request("POST", runPath(job.runId, "inbox"), body);
|
|
2867
3468
|
} catch (error) {
|
|
2868
3469
|
input.restoreDelivered(ack);
|
|
2869
3470
|
if (!failing) {
|
|
@@ -2873,17 +3474,23 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
2873
3474
|
return;
|
|
2874
3475
|
}
|
|
2875
3476
|
failing = false;
|
|
2876
|
-
if (!
|
|
3477
|
+
if (!reply3.keepGoing) {
|
|
2877
3478
|
closedOnSite();
|
|
2878
3479
|
return;
|
|
2879
3480
|
}
|
|
2880
|
-
for (const message of
|
|
3481
|
+
for (const message of reply3.messages) {
|
|
2881
3482
|
if (input.push(message)) {
|
|
2882
3483
|
tools.print(` ${messageLine(message)}`);
|
|
2883
3484
|
}
|
|
2884
3485
|
}
|
|
2885
|
-
|
|
2886
|
-
|
|
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 };
|
|
2887
3494
|
void input.switchModel(change).then(
|
|
2888
3495
|
(applied) => {
|
|
2889
3496
|
if (applied) {
|
|
@@ -2923,7 +3530,7 @@ function specWorkTools2(job, api) {
|
|
|
2923
3530
|
if (kind !== "interview" && kind !== "spec") {
|
|
2924
3531
|
return null;
|
|
2925
3532
|
}
|
|
2926
|
-
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) };
|
|
2927
3534
|
}
|
|
2928
3535
|
function askingTools(job, api) {
|
|
2929
3536
|
const people = job.people ?? [];
|
|
@@ -2938,11 +3545,11 @@ function permissionTools(job, tools) {
|
|
|
2938
3545
|
}
|
|
2939
3546
|
return {
|
|
2940
3547
|
send: async (body) => {
|
|
2941
|
-
const
|
|
2942
|
-
if (
|
|
3548
|
+
const reply3 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
|
|
3549
|
+
if (reply3.decision === "wait") {
|
|
2943
3550
|
tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
2944
3551
|
}
|
|
2945
|
-
return
|
|
3552
|
+
return reply3;
|
|
2946
3553
|
},
|
|
2947
3554
|
check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
|
|
2948
3555
|
sleep: tools.sleep
|
|
@@ -2961,11 +3568,27 @@ function knowledgeTools2(job, api) {
|
|
|
2961
3568
|
read: (entryId) => api.request("GET", knowledgePath(job.runId, entryId))
|
|
2962
3569
|
};
|
|
2963
3570
|
}
|
|
2964
|
-
|
|
2965
|
-
|
|
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
|
+
}
|
|
2966
3580
|
tools.print("Preparing the code\u2026");
|
|
2967
3581
|
const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
|
|
2968
|
-
|
|
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);
|
|
2969
3592
|
signal.throwIfAborted();
|
|
2970
3593
|
tools.print("Claude is working\u2026");
|
|
2971
3594
|
const attached = { count: 0 };
|
|
@@ -2975,7 +3598,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2975
3598
|
const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
|
|
2976
3599
|
const outcome = await tools.runAgent({
|
|
2977
3600
|
cwd: workspace.root,
|
|
2978
|
-
|
|
3601
|
+
library,
|
|
3602
|
+
prompt: buildTaskPrompt(job, workspace, { browser: Boolean(tools.browser), recorder: Boolean(tools.recorder), results }),
|
|
2979
3603
|
input,
|
|
2980
3604
|
env: { ...baseEnv, ...tools.browser ? browserEnv(tools.browser) : {}, ...recording, ...identity },
|
|
2981
3605
|
signal,
|
|
@@ -2990,12 +3614,14 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
2990
3614
|
effort: job.effort,
|
|
2991
3615
|
onFeed: record,
|
|
2992
3616
|
onSession: session.report,
|
|
2993
|
-
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)
|
|
2994
3620
|
});
|
|
2995
3621
|
await session.sent();
|
|
2996
3622
|
signal.throwIfAborted();
|
|
2997
3623
|
const waitingFor = outcome.waitingFor ?? null;
|
|
2998
|
-
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;
|
|
2999
3625
|
const pushed = shouldPublish ? await publishWorkspace(workspace, job, await gitToken(tools.api, job), identity) : [];
|
|
3000
3626
|
return {
|
|
3001
3627
|
outcome: outcome.completed ? "succeeded" : "failed",
|
|
@@ -3016,7 +3642,7 @@ function clockOf(iso) {
|
|
|
3016
3642
|
const time = new Date(iso);
|
|
3017
3643
|
return `${String(time.getHours()).padStart(2, "0")}:${String(time.getMinutes()).padStart(2, "0")}`;
|
|
3018
3644
|
}
|
|
3019
|
-
function resultLine(finish2) {
|
|
3645
|
+
function resultLine(finish2, folder = false) {
|
|
3020
3646
|
if (finish2.pausedUntil) {
|
|
3021
3647
|
return `Paused: ${USAGE_LIMIT_REPORT}, resumes at ${clockOf(finish2.pausedUntil)}`;
|
|
3022
3648
|
}
|
|
@@ -3035,7 +3661,10 @@ function resultLine(finish2) {
|
|
|
3035
3661
|
if (finish2.spec) {
|
|
3036
3662
|
return "Done. The spec waits for the owner on TAMsense.";
|
|
3037
3663
|
}
|
|
3038
|
-
|
|
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.`;
|
|
3039
3668
|
}
|
|
3040
3669
|
async function sendRecordings(job, tools) {
|
|
3041
3670
|
const folder = recordingsFolder(tools.home, job.runId);
|
|
@@ -3051,7 +3680,7 @@ async function sendRecordings(job, tools) {
|
|
|
3051
3680
|
tools.print(`Could not attach a recording: ${line}`);
|
|
3052
3681
|
}
|
|
3053
3682
|
} finally {
|
|
3054
|
-
await
|
|
3683
|
+
await rm6(folder, { recursive: true, force: true });
|
|
3055
3684
|
}
|
|
3056
3685
|
}
|
|
3057
3686
|
function failureReport(error, signal) {
|
|
@@ -3079,6 +3708,7 @@ async function runJob(job, tools, stop) {
|
|
|
3079
3708
|
} catch (error) {
|
|
3080
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 };
|
|
3081
3710
|
} finally {
|
|
3711
|
+
await rm6(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
3082
3712
|
clearInterval(polling);
|
|
3083
3713
|
clearInterval(feeding);
|
|
3084
3714
|
stop.removeEventListener("abort", stopRun);
|
|
@@ -3090,14 +3720,14 @@ async function runJob(job, tools, stop) {
|
|
|
3090
3720
|
}
|
|
3091
3721
|
await feed.flush();
|
|
3092
3722
|
if (run2.signal.reason === CLOSED_ON_SITE) {
|
|
3093
|
-
await
|
|
3723
|
+
await rm6(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
3094
3724
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
3095
3725
|
return;
|
|
3096
3726
|
}
|
|
3097
3727
|
await sendRecordings(job, tools).catch((error) => tools.print(`Could not attach the browser recordings: ${messageOf3(error)}`));
|
|
3098
3728
|
try {
|
|
3099
3729
|
await tools.api.request("POST", runPath(job.runId, "finish"), finish2);
|
|
3100
|
-
tools.print(resultLine(finish2));
|
|
3730
|
+
tools.print(resultLine(finish2, isFolderJob(job)));
|
|
3101
3731
|
} catch (error) {
|
|
3102
3732
|
tools.print(`Could not send the result to TAMsense: ${messageOf3(error)}`);
|
|
3103
3733
|
}
|
|
@@ -3126,18 +3756,18 @@ async function reportCapabilities(tools, stop) {
|
|
|
3126
3756
|
}
|
|
3127
3757
|
}
|
|
3128
3758
|
function jobTools(job, tools, parallel) {
|
|
3129
|
-
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId}] ${line}`) } : tools;
|
|
3759
|
+
return parallel > 1 ? { ...tools, print: (line) => tools.print(`[#${job.taskId} ${job.stage.name}] ${line}`) } : tools;
|
|
3130
3760
|
}
|
|
3131
3761
|
async function work(tools, stop, parallel = 1) {
|
|
3132
3762
|
tools.print(parallel > 1 ? `Waiting for tasks, up to ${parallel} at once. Press Ctrl+C to stop.` : "Waiting for tasks. Press Ctrl+C to stop.");
|
|
3133
3763
|
const halt = new AbortController();
|
|
3134
3764
|
const stopAll = AbortSignal.any([stop, halt.signal]);
|
|
3135
3765
|
const running = /* @__PURE__ */ new Set();
|
|
3136
|
-
const
|
|
3766
|
+
const folderLock = keyedLock();
|
|
3137
3767
|
const reporting = reportCapabilities(tools, stopAll);
|
|
3138
3768
|
const start = (job) => {
|
|
3139
3769
|
const runTools = jobTools(job, tools, parallel);
|
|
3140
|
-
const task =
|
|
3770
|
+
const task = folderLock.run(stepFolder(job).join("/"), () => runJob(job, runTools, stopAll)).catch((error) => runTools.print(`The run stopped: ${messageOf3(error)}`)).finally(() => running.delete(task));
|
|
3141
3771
|
running.add(task);
|
|
3142
3772
|
};
|
|
3143
3773
|
try {
|
|
@@ -3146,9 +3776,9 @@ async function work(tools, stop, parallel = 1) {
|
|
|
3146
3776
|
await Promise.race(running);
|
|
3147
3777
|
continue;
|
|
3148
3778
|
}
|
|
3149
|
-
let
|
|
3779
|
+
let reply3;
|
|
3150
3780
|
try {
|
|
3151
|
-
|
|
3781
|
+
reply3 = await tools.api.request("POST", NEXT_RUN_PATH);
|
|
3152
3782
|
} catch (error) {
|
|
3153
3783
|
if (isFatal(error)) {
|
|
3154
3784
|
halt.abort();
|
|
@@ -3158,8 +3788,8 @@ async function work(tools, stop, parallel = 1) {
|
|
|
3158
3788
|
await tools.sleep(RETRY_AFTER_ERROR_MS, stopAll);
|
|
3159
3789
|
continue;
|
|
3160
3790
|
}
|
|
3161
|
-
if (
|
|
3162
|
-
start(
|
|
3791
|
+
if (reply3.job) {
|
|
3792
|
+
start(reply3.job);
|
|
3163
3793
|
} else {
|
|
3164
3794
|
await tools.sleep(POLL_INTERVAL_MS, stopAll);
|
|
3165
3795
|
}
|
|
@@ -3214,7 +3844,7 @@ function stopOnInterrupt() {
|
|
|
3214
3844
|
return stop.signal;
|
|
3215
3845
|
}
|
|
3216
3846
|
function installTools(home) {
|
|
3217
|
-
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 };
|
|
3218
3848
|
}
|
|
3219
3849
|
async function browserForStart(home, install) {
|
|
3220
3850
|
const installed = await installedBrowser(home);
|