@tamsensedev/runner 0.13.0 → 0.18.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/LICENSE +5 -5
- package/README.md +61 -57
- package/dist/cli.js +646 -315
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/library/.claude-plugin/plugin.json +5 -5
- package/library/THIRD_PARTY_NOTICES.md +123 -123
- package/library/agents/code-architect.md +67 -67
- package/library/agents/code-explorer.md +61 -61
- package/library/agents/code-reviewer.md +73 -73
- package/library/agents/code-simplifier.md +62 -62
- package/library/agents/design-critic.md +52 -52
- package/library/agents/second-opinion.md +64 -64
- package/library/agents/security-auditor.md +86 -86
- package/library/agents/silent-failure-hunter.md +70 -70
- package/library/agents/test-coverage-analyzer.md +78 -78
- package/library/agents/test-runner.md +48 -48
- package/library/agents/ui-tester.md +50 -50
- package/library/licenses/Apache-2.0.txt +202 -202
- package/library/skills/accessibility/SKILL.md +144 -144
- package/library/skills/accessibility/references/wcag-checks.md +168 -168
- package/library/skills/addressing-review-feedback/SKILL.md +109 -109
- package/library/skills/app-screens/SKILL.md +96 -96
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
- package/library/skills/app-screens/references/states-density-phone.md +54 -54
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
- package/library/skills/clean-commits/SKILL.md +85 -85
- package/library/skills/design-review/SKILL.md +107 -107
- package/library/skills/design-review/references/critique-passes.md +101 -101
- package/library/skills/design-review/references/screenshot-procedure.md +121 -121
- package/library/skills/differential-security-review/LICENSE +427 -427
- package/library/skills/differential-security-review/SKILL.md +105 -105
- package/library/skills/differential-security-review/references/adversarial.md +115 -115
- package/library/skills/differential-security-review/references/methodology.md +145 -145
- package/library/skills/differential-security-review/references/patterns.md +108 -108
- package/library/skills/false-positive-check/LICENSE +427 -427
- package/library/skills/false-positive-check/SKILL.md +95 -95
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
- package/library/skills/false-positive-check/references/deep-verification.md +113 -113
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
- package/library/skills/false-positive-check/references/standard-verification.md +80 -80
- package/library/skills/focused-implementation/SKILL.md +104 -104
- package/library/skills/implementation-plan/SKILL.md +66 -66
- package/library/skills/interface-design/SKILL.md +144 -144
- package/library/skills/interface-design/references/craft-defaults.md +130 -130
- package/library/skills/interface-design/references/new-direction.md +62 -62
- package/library/skills/interface-design/references/writing.md +52 -52
- package/library/skills/interface-polish/SKILL.md +103 -103
- package/library/skills/interface-polish/references/detail-rules.md +154 -154
- package/library/skills/interface-polish/references/worst-case-data.md +158 -158
- package/library/skills/motion-design/SKILL.md +106 -106
- package/library/skills/motion-design/references/recipes.md +243 -243
- package/library/skills/postgres-performance/SKILL.md +97 -97
- package/library/skills/postgres-performance/references/rules.md +265 -265
- package/library/skills/project-orientation/SKILL.md +77 -77
- package/library/skills/reviewing-changes/SKILL.md +183 -183
- package/library/skills/safe-database-changes/SKILL.md +111 -111
- package/library/skills/safe-database-changes/references/backfills.md +75 -75
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
- package/library/skills/security-review/SKILL.md +132 -132
- package/library/skills/security-review/references/checks-by-area.md +107 -107
- package/library/skills/security-review/references/do-not-report.md +86 -86
- package/library/skills/spec-writing/SKILL.md +144 -144
- package/library/skills/systematic-debugging/SKILL.md +130 -130
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
- package/library/skills/task-interview/SKILL.md +84 -84
- package/library/skills/test-driven-development/SKILL.md +116 -116
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
- package/library/skills/ui-testing/SKILL.md +102 -102
- package/library/skills/ui-testing/references/headless-check.md +295 -295
- package/library/skills/ui-testing/references/local-stand.md +51 -51
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
- package/library/skills/ui-testing/references/recording.md +61 -61
- package/library/skills/verification-before-completion/SKILL.md +113 -113
- package/library/skills/web-interface-guidelines/SKILL.md +202 -202
- package/package.json +46 -46
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { existsSync as existsSync8 } from "fs";
|
|
5
5
|
import { homedir as homedir3 } from "os";
|
|
6
|
-
import
|
|
6
|
+
import process8 from "process";
|
|
7
7
|
|
|
8
8
|
// src/api.ts
|
|
9
9
|
import { setTimeout as wait } from "timers/promises";
|
|
@@ -157,9 +157,9 @@ function textOf(value) {
|
|
|
157
157
|
return typeof value === "string" && value.trim() ? value : null;
|
|
158
158
|
}
|
|
159
159
|
async function errorOf(response) {
|
|
160
|
-
const
|
|
161
|
-
const code = textOf(
|
|
162
|
-
const message = textOf(
|
|
160
|
+
const reply5 = await response.json().catch(() => ({}));
|
|
161
|
+
const code = textOf(reply5.data?.code);
|
|
162
|
+
const message = textOf(reply5.message) ?? textOf(reply5.statusMessage) ?? `TAMsense answered with status ${response.status}`;
|
|
163
163
|
return new ApiError(response.status, code, message);
|
|
164
164
|
}
|
|
165
165
|
function isFailure(outcome) {
|
|
@@ -517,8 +517,8 @@ async function probeCapabilities(stop) {
|
|
|
517
517
|
}
|
|
518
518
|
|
|
519
519
|
// src/claude.ts
|
|
520
|
-
import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as
|
|
521
|
-
import { z as
|
|
520
|
+
import { createSdkMcpServer as createSdkMcpServer2, query as query3, tool as tool7 } from "@anthropic-ai/claude-agent-sdk";
|
|
521
|
+
import { z as z7 } from "zod";
|
|
522
522
|
|
|
523
523
|
// src/askPerson.ts
|
|
524
524
|
var MAX_ASK_TRIES = 5;
|
|
@@ -578,21 +578,21 @@ function shorten(text2) {
|
|
|
578
578
|
const line = text2.replace(/\s+/g, " ").trim();
|
|
579
579
|
return line.length > MAX_ACTIVITY_LENGTH ? `${line.slice(0, MAX_ACTIVITY_LENGTH - 1)}\u2026` : line;
|
|
580
580
|
}
|
|
581
|
-
function activityLine(
|
|
581
|
+
function activityLine(tool8, input) {
|
|
582
582
|
const fields = typeof input === "object" && input !== null ? input : {};
|
|
583
|
-
if (
|
|
583
|
+
if (tool8 === "Bash" && typeof fields.command === "string") {
|
|
584
584
|
return `$ ${shorten(fields.command)}`;
|
|
585
585
|
}
|
|
586
|
-
if (FILE_TOOLS.has(
|
|
587
|
-
return `${
|
|
586
|
+
if (FILE_TOOLS.has(tool8) && typeof fields.file_path === "string") {
|
|
587
|
+
return `${tool8} ${shorten(fields.file_path)}`;
|
|
588
588
|
}
|
|
589
|
-
if (
|
|
589
|
+
if (tool8 === "Skill" && typeof fields.skill === "string") {
|
|
590
590
|
return `Skill ${shorten(fields.skill)}`;
|
|
591
591
|
}
|
|
592
|
-
if (HELPER_TOOLS.has(
|
|
592
|
+
if (HELPER_TOOLS.has(tool8) && typeof fields.subagent_type === "string") {
|
|
593
593
|
return `Agent ${shorten(fields.subagent_type)}`;
|
|
594
594
|
}
|
|
595
|
-
return
|
|
595
|
+
return tool8;
|
|
596
596
|
}
|
|
597
597
|
function usageOf(modelUsage) {
|
|
598
598
|
const entries = Object.values(modelUsage);
|
|
@@ -650,6 +650,12 @@ function recordingUploadPath(runId, upload, part) {
|
|
|
650
650
|
function checkPath(checkId, action) {
|
|
651
651
|
return `/api/runner/checks/${checkId}/${action}`;
|
|
652
652
|
}
|
|
653
|
+
function routinePath(checkId, action) {
|
|
654
|
+
return `/api/runner/checks/${checkId}/${action}`;
|
|
655
|
+
}
|
|
656
|
+
function sensePath(senseId, action) {
|
|
657
|
+
return `/api/runner/sense/${senseId}/${action}`;
|
|
658
|
+
}
|
|
653
659
|
|
|
654
660
|
// src/feed.ts
|
|
655
661
|
var MAX_TEXT_LENGTH = 8e3;
|
|
@@ -670,31 +676,52 @@ function textField(fields, name) {
|
|
|
670
676
|
return typeof value === "string" && value.trim() ? value : null;
|
|
671
677
|
}
|
|
672
678
|
function toolDraft(block, parent) {
|
|
673
|
-
const
|
|
679
|
+
const tool8 = (block.name ?? "tool").replace(MCP_PREFIX, "");
|
|
674
680
|
const fields = fieldsOf(block.input);
|
|
675
|
-
const base = { tool:
|
|
676
|
-
if (
|
|
681
|
+
const base = { tool: tool8, ref: block.id ?? null, parent, failed: false };
|
|
682
|
+
if (tool8 === "Skill" && textField(fields, "skill")) {
|
|
677
683
|
return { ...base, kind: "skill", text: clip(textField(fields, "skill"), MAX_SUMMARY_LENGTH) };
|
|
678
684
|
}
|
|
679
|
-
if (HELPER_TOOLS2.has(
|
|
685
|
+
if (HELPER_TOOLS2.has(tool8)) {
|
|
680
686
|
const who = textField(fields, "subagent_type") ?? "general-purpose";
|
|
681
687
|
const what = textField(fields, "description");
|
|
682
688
|
return { ...base, kind: "agent", text: clip(what ? `${who}: ${what}` : who, MAX_SUMMARY_LENGTH) };
|
|
683
689
|
}
|
|
684
|
-
if (
|
|
690
|
+
if (tool8 === "submit_verdict") {
|
|
685
691
|
const verdict = fields.couldNotRun === true ? "Could not run" : fields.passed === true ? "Passed" : "Did not pass";
|
|
686
692
|
return { ...base, kind: "tool", text: clip(`${verdict}. ${textField(fields, "report") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
687
693
|
}
|
|
688
|
-
if (
|
|
694
|
+
if (tool8 === "hand_off") {
|
|
689
695
|
return { ...base, kind: "tool", text: clip(`Hand off to ${textField(fields, "to") ?? "?"}: ${textField(fields, "note") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
690
696
|
}
|
|
691
|
-
if (
|
|
692
|
-
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[
|
|
697
|
+
if (tool8 in ATTACH_LABELS) {
|
|
698
|
+
return { ...base, kind: "tool", text: clip(`${ATTACH_LABELS[tool8]}: ${textField(fields, "title") ?? textField(fields, "path") ?? ""}`, MAX_SUMMARY_LENGTH) };
|
|
693
699
|
}
|
|
694
700
|
const command = textField(fields, "command");
|
|
695
|
-
const summary =
|
|
701
|
+
const summary = tool8 === "Bash" && command ? `$ ${command}` : activityLine(tool8, block.input);
|
|
696
702
|
return { ...base, kind: "tool", text: clip(summary, MAX_SUMMARY_LENGTH) };
|
|
697
703
|
}
|
|
704
|
+
function sourceCalls(tools = []) {
|
|
705
|
+
return { tools: new Set(tools.map((tool8) => tool8.name)), refs: /* @__PURE__ */ new Set() };
|
|
706
|
+
}
|
|
707
|
+
function sourceCallDraft(block, draft) {
|
|
708
|
+
return { ...draft, text: clip(`${draft.tool} ${JSON.stringify(block.input ?? {})}`, MAX_TEXT_LENGTH) };
|
|
709
|
+
}
|
|
710
|
+
function fromSource(calls, block) {
|
|
711
|
+
return block.tool_use_id !== void 0 && calls.refs.has(block.tool_use_id);
|
|
712
|
+
}
|
|
713
|
+
function sourceSummary(text2) {
|
|
714
|
+
let reply5;
|
|
715
|
+
try {
|
|
716
|
+
reply5 = fieldsOf(JSON.parse(text2));
|
|
717
|
+
} catch {
|
|
718
|
+
return JSON.stringify({ ok: false });
|
|
719
|
+
}
|
|
720
|
+
if (reply5.ok === false) {
|
|
721
|
+
return JSON.stringify(reply5.refused === true ? { ok: false, refused: true, error: reply5.error } : { ok: false });
|
|
722
|
+
}
|
|
723
|
+
return JSON.stringify(typeof reply5.rowCount === "number" ? { ok: true, rowCount: reply5.rowCount } : { ok: true });
|
|
724
|
+
}
|
|
698
725
|
function resultText(content) {
|
|
699
726
|
if (typeof content === "string") {
|
|
700
727
|
return content;
|
|
@@ -708,7 +735,7 @@ function helperEndDraft(ref, parent, end, text2) {
|
|
|
708
735
|
const stopped = end === "stopped";
|
|
709
736
|
return { kind: "result", text: stopped ? "" : clip(text2, MAX_RESULT_LENGTH), tool: stopped ? HELPER_STOPPED : null, ref, parent, failed: end === "failed" };
|
|
710
737
|
}
|
|
711
|
-
function feedOf(message) {
|
|
738
|
+
function feedOf(message, calls = sourceCalls()) {
|
|
712
739
|
if (message.type !== "assistant" && message.type !== "user") {
|
|
713
740
|
return [];
|
|
714
741
|
}
|
|
@@ -724,9 +751,15 @@ function feedOf(message) {
|
|
|
724
751
|
} else if (message.type === "assistant" && block.type === "text" && block.text?.trim()) {
|
|
725
752
|
drafts.push({ kind: "text", text: clip(block.text, MAX_TEXT_LENGTH), tool: null, ref: null, parent, failed: false });
|
|
726
753
|
} else if (message.type === "assistant" && block.type === "tool_use") {
|
|
727
|
-
|
|
754
|
+
const draft = toolDraft(block, parent);
|
|
755
|
+
const isSource = draft.tool !== null && calls.tools.has(draft.tool);
|
|
756
|
+
if (isSource && block.id) {
|
|
757
|
+
calls.refs.add(block.id);
|
|
758
|
+
}
|
|
759
|
+
drafts.push(isSource ? sourceCallDraft(block, draft) : draft);
|
|
728
760
|
} else if (message.type === "user" && block.type === "tool_result") {
|
|
729
|
-
|
|
761
|
+
const text2 = resultText(block.content);
|
|
762
|
+
drafts.push({ kind: "result", text: clip(fromSource(calls, block) ? sourceSummary(text2) : text2, MAX_RESULT_LENGTH), tool: null, ref: block.tool_use_id ?? null, parent, failed: block.is_error === true });
|
|
730
763
|
}
|
|
731
764
|
}
|
|
732
765
|
return drafts;
|
|
@@ -1231,8 +1264,8 @@ function messageOf2(error) {
|
|
|
1231
1264
|
async function decisionOf(tools, first, signal) {
|
|
1232
1265
|
const now = tools.now ?? Date.now;
|
|
1233
1266
|
const giveUpAt = Date.parse(first.waitUntil) + SITE_SILENCE_MS;
|
|
1234
|
-
let
|
|
1235
|
-
while (
|
|
1267
|
+
let reply5 = first;
|
|
1268
|
+
while (reply5.decision === "wait") {
|
|
1236
1269
|
await tools.sleep(CHECK_INTERVAL_MS, signal);
|
|
1237
1270
|
if (signal.aborted) {
|
|
1238
1271
|
return STOPPED_TEXT2;
|
|
@@ -1240,9 +1273,9 @@ async function decisionOf(tools, first, signal) {
|
|
|
1240
1273
|
if (now() > giveUpAt) {
|
|
1241
1274
|
return SITE_SILENT_TEXT;
|
|
1242
1275
|
}
|
|
1243
|
-
|
|
1276
|
+
reply5 = await tools.check(reply5.id).catch(() => reply5);
|
|
1244
1277
|
}
|
|
1245
|
-
return
|
|
1278
|
+
return reply5;
|
|
1246
1279
|
}
|
|
1247
1280
|
function askPermission(tools, lastWords, folder) {
|
|
1248
1281
|
return async (toolName, input, options) => {
|
|
@@ -1263,11 +1296,11 @@ function askPermission(tools, lastWords, folder) {
|
|
|
1263
1296
|
} catch (error) {
|
|
1264
1297
|
return deny(`Could not ask for permission on TAMsense: ${messageOf2(error)}. Do it another way, or explain in your report why it is needed.`);
|
|
1265
1298
|
}
|
|
1266
|
-
const
|
|
1267
|
-
if (typeof
|
|
1268
|
-
return deny(
|
|
1299
|
+
const reply5 = await decisionOf(tools, first, options.signal);
|
|
1300
|
+
if (typeof reply5 === "string") {
|
|
1301
|
+
return deny(reply5);
|
|
1269
1302
|
}
|
|
1270
|
-
return
|
|
1303
|
+
return reply5.decision === "allow" ? { behavior: "allow", updatedInput: input } : deny(reply5.message);
|
|
1271
1304
|
};
|
|
1272
1305
|
}
|
|
1273
1306
|
var denyWhenAsked = async () => deny(NOBODY_TO_ASK);
|
|
@@ -1447,7 +1480,7 @@ async function checkFolders(tools, stop) {
|
|
|
1447
1480
|
}
|
|
1448
1481
|
}
|
|
1449
1482
|
var SKIPPED_FOLDER = "node_modules";
|
|
1450
|
-
var MORE_CODE_TEXT = "To add more code, start the program from your code folder or the folder above it, or add a path in
|
|
1483
|
+
var MORE_CODE_TEXT = "To add more code, start the program from your code folder or the folder above it, or add a path in Settings, under your computer on the Account tab.";
|
|
1451
1484
|
async function childRepositories(start) {
|
|
1452
1485
|
const entries = await readdir(start, { withFileTypes: true }).catch(() => []);
|
|
1453
1486
|
return entries.filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== SKIPPED_FOLDER).map((entry) => join2(start, entry.name)).filter((folder) => existsSync2(join2(folder, ".git"))).sort();
|
|
@@ -1515,6 +1548,7 @@ function keyedLock() {
|
|
|
1515
1548
|
import { existsSync as existsSync3 } from "fs";
|
|
1516
1549
|
import { rm as rm2 } from "fs/promises";
|
|
1517
1550
|
import { join as join3, resolve as resolve2 } from "path";
|
|
1551
|
+
var RESULTS_FOLDER = "results";
|
|
1518
1552
|
var FALLBACK_IDENTITY = { name: "TAMsense", email: "noreply@tamsense.com" };
|
|
1519
1553
|
var cacheLock = keyedLock();
|
|
1520
1554
|
function githubRemoteUrl(fullName) {
|
|
@@ -1529,7 +1563,10 @@ function folderNames(repositories) {
|
|
|
1529
1563
|
for (const repository of repositories) {
|
|
1530
1564
|
counts.set(shortName(repository.fullName), (counts.get(shortName(repository.fullName)) ?? 0) + 1);
|
|
1531
1565
|
}
|
|
1532
|
-
return repositories.map((repository) =>
|
|
1566
|
+
return repositories.map((repository) => {
|
|
1567
|
+
const name = shortName(repository.fullName);
|
|
1568
|
+
return counts.get(name) === 1 && name !== RESULTS_FOLDER ? name : repository.fullName.replace("/", "__");
|
|
1569
|
+
});
|
|
1533
1570
|
}
|
|
1534
1571
|
async function refreshCache(cache, repository, token, tools) {
|
|
1535
1572
|
if (existsSync3(join3(cache, ".git"))) {
|
|
@@ -1686,7 +1723,7 @@ async function baseBranchRef(source, branch, remote) {
|
|
|
1686
1723
|
}
|
|
1687
1724
|
const found = own ?? fetched;
|
|
1688
1725
|
if (!found) {
|
|
1689
|
-
throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in
|
|
1726
|
+
throw new Error(`The main branch ${branch} is not in ${source}. Check the folder again in Settings, under your computer on the Account tab.`);
|
|
1690
1727
|
}
|
|
1691
1728
|
return found;
|
|
1692
1729
|
}
|
|
@@ -1699,7 +1736,7 @@ async function taskBranchRef(source, branch, remote, base) {
|
|
|
1699
1736
|
async function startRef(source, job, local) {
|
|
1700
1737
|
const base = await baseBranchRef(source, local.baseBranch, local.remote);
|
|
1701
1738
|
const task = await taskBranchRef(source, job.branch, local.remote, base);
|
|
1702
|
-
if (task || !isReadOnlyCheck(job)) {
|
|
1739
|
+
if (task || !isReadOnlyCheck(job) || job.severalBranches) {
|
|
1703
1740
|
return task ?? base;
|
|
1704
1741
|
}
|
|
1705
1742
|
const where = local.remote ? `${source} or on its remote ${local.remote}` : source;
|
|
@@ -1734,7 +1771,7 @@ function sourceOf(repository) {
|
|
|
1734
1771
|
async function prepareRepository(job, repository, path, tools) {
|
|
1735
1772
|
const source = sourceOf(repository);
|
|
1736
1773
|
if ((await runGit(["rev-parse", "--show-toplevel"], { cwd: source }).catch(() => null))?.code !== 0) {
|
|
1737
|
-
throw new Error(`${source} is no longer a git repository on this computer. Check the folder again in
|
|
1774
|
+
throw new Error(`${source} is no longer a git repository on this computer. Check the folder again in Settings, under your computer on the Account tab.`);
|
|
1738
1775
|
}
|
|
1739
1776
|
const local = { source, remote: await remoteOf(source), baseBranch: repository.defaultBranch };
|
|
1740
1777
|
if (local.remote) {
|
|
@@ -1793,8 +1830,8 @@ async function commitAndPush(repository, local, job, identity) {
|
|
|
1793
1830
|
}
|
|
1794
1831
|
const base = await baseBranchRef(repository.path, local.baseBranch, local.remote);
|
|
1795
1832
|
const commits = Number(await git(["rev-list", "--count", `${base}..HEAD`], options));
|
|
1796
|
-
const
|
|
1797
|
-
return { sha: await git(["rev-parse", "HEAD"], options), commits, ...
|
|
1833
|
+
const sent3 = await pushBranch(repository.path, local, job.branch);
|
|
1834
|
+
return { sha: await git(["rev-parse", "HEAD"], options), commits, ...sent3 };
|
|
1798
1835
|
}
|
|
1799
1836
|
async function publishLocalWorkspace(workspace, job, identity) {
|
|
1800
1837
|
const pushed = [];
|
|
@@ -1830,7 +1867,6 @@ var SAVE_TABLE_TOOL_NAME = "save_result_table";
|
|
|
1830
1867
|
var ATTACH_FILE_TOOL_NAME = "attach_result_file";
|
|
1831
1868
|
var ADD_LINK_TOOL_NAME = "add_result_link";
|
|
1832
1869
|
var HAND_OVER_TOOL_NAME = "hand_over";
|
|
1833
|
-
var RESULTS_FOLDER = "results";
|
|
1834
1870
|
var MAX_NAME_LENGTH = 60;
|
|
1835
1871
|
var TYPES_BY_EXTENSION = {
|
|
1836
1872
|
".pdf": "application/pdf",
|
|
@@ -1997,11 +2033,11 @@ function resultToolset(tools) {
|
|
|
1997
2033
|
function handOverTool(tools) {
|
|
1998
2034
|
return tool2(
|
|
1999
2035
|
HAND_OVER_TOOL_NAME,
|
|
2000
|
-
`
|
|
2036
|
+
`Create a linked task on a board of the project, this board included: when part of the work belongs to another board, or when the work goes on as a new task here, such as the next wave after this one. Boards you may create tasks on: ${tools.boards.map((board) => board.name).join(", ")}. Without autopilot a person confirms it first.`,
|
|
2001
2037
|
{
|
|
2002
2038
|
board: z2.string().describe("name of the board"),
|
|
2003
|
-
title: z2.string().describe("title of the new task
|
|
2004
|
-
note: z2.string().describe("what you found and what
|
|
2039
|
+
title: z2.string().describe("title of the new task, one plain sentence"),
|
|
2040
|
+
note: z2.string().describe("what you found and what the new task should do; never names, emails or other personal data of clients"),
|
|
2005
2041
|
wait: z2.boolean().describe("true when this task should wait on its step until the linked task is done")
|
|
2006
2042
|
},
|
|
2007
2043
|
async (args) => await sent(() => tools.send(args))
|
|
@@ -2011,56 +2047,205 @@ function handOverTool(tools) {
|
|
|
2011
2047
|
// src/schedules.ts
|
|
2012
2048
|
import { existsSync as existsSync5 } from "fs";
|
|
2013
2049
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
2014
|
-
import { createSdkMcpServer, query as query2, tool as
|
|
2050
|
+
import { createSdkMcpServer, query as query2, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
|
|
2051
|
+
import { z as z5 } from "zod";
|
|
2052
|
+
|
|
2053
|
+
// src/routines.ts
|
|
2054
|
+
import { tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
|
|
2015
2055
|
import { z as z3 } from "zod";
|
|
2056
|
+
var FINISH_ROUTINE_TOOL_NAME = "finish_routine";
|
|
2057
|
+
var RULES = [
|
|
2058
|
+
"You run one run of a TAMsense routine on the user's own computer: a job that repeats on a schedule. Nobody answers during the run: decide yourself.",
|
|
2059
|
+
"This is the person's own computer, and their other programs may be running on it. Stop only the processes you started yourself, by their process id, and stop them before you finish. Never kill processes by name or pattern and never touch programs you did not start.",
|
|
2060
|
+
"One problem is one report_problem call. Use the same key for the same problem on every run, and pass the number of an open task of this routine as sameAs when it is the same problem.",
|
|
2061
|
+
`Finish by calling ${FINISH_ROUTINE_TOOL_NAME} exactly once, after every problem is reported.`,
|
|
2062
|
+
"Write in the language of the job. No em dashes and no en dashes."
|
|
2063
|
+
];
|
|
2064
|
+
function reply2(text2) {
|
|
2065
|
+
return { content: [{ type: "text", text: text2 }] };
|
|
2066
|
+
}
|
|
2067
|
+
var KEY_FIELD = z3.string().describe('a short stable name of this problem, the same on every run, like "buyout-4812-card-declined"');
|
|
2068
|
+
async function sent2(tools, body) {
|
|
2069
|
+
try {
|
|
2070
|
+
return reply2((await tools.call(body)).text);
|
|
2071
|
+
} catch (error) {
|
|
2072
|
+
return { ...reply2(`Could not reach TAMsense: ${error instanceof Error ? error.message : String(error)}. Say so in your summary.`), isError: true };
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function routineToolset(tools, onFinish) {
|
|
2076
|
+
const problem2 = tool3("report_problem", "Hand one problem you do not fix to people: a task on the board of the routine, an addition to the open task of the same problem, or a line in the report.", {
|
|
2077
|
+
title: z3.string().describe("one plain sentence: what is wrong"),
|
|
2078
|
+
detail: z3.string().describe("the facts people need to act; never personal data of clients"),
|
|
2079
|
+
key: KEY_FIELD,
|
|
2080
|
+
sameAs: z3.number().int().nullable().describe("the number of an open task of this routine about the same problem, or null")
|
|
2081
|
+
}, async (args) => await sent2(tools, { tool: "problem", input: args }));
|
|
2082
|
+
const ask = tool3("ask_person", "Ask the person of this routine what to do with one thing you cannot decide. Do not wait for the answer: the next run reads it.", {
|
|
2083
|
+
question: z3.string(),
|
|
2084
|
+
context: z3.string().describe("the facts they need to answer without opening anything"),
|
|
2085
|
+
key: KEY_FIELD,
|
|
2086
|
+
options: z3.array(z3.string()).max(4).describe("up to four answers you suggest")
|
|
2087
|
+
}, async (args) => await sent2(tools, { tool: "ask", input: args }));
|
|
2088
|
+
const fixed = tool3("report_fixed", "Write down one fix you made in this run: what you changed and why.", {
|
|
2089
|
+
title: z3.string().describe('what you did, like "Cancelled buyout #4815 on Wildberries"'),
|
|
2090
|
+
detail: z3.string().describe("why, with the facts"),
|
|
2091
|
+
key: KEY_FIELD
|
|
2092
|
+
}, async (args) => await sent2(tools, { tool: "fixed", input: args }));
|
|
2093
|
+
const report = tool3("save_report", "Save the report of this run on TAMsense: a Markdown document or a CSV table with a header row.", {
|
|
2094
|
+
kind: z3.enum(["document", "table"]),
|
|
2095
|
+
title: z3.string(),
|
|
2096
|
+
content: z3.string().describe("the full Markdown text, or the full CSV")
|
|
2097
|
+
}, async (args) => await sent2(tools, { tool: "report", input: args }));
|
|
2098
|
+
const finish2 = tool3(FINISH_ROUTINE_TOOL_NAME, "Finish this run. Call it exactly once, at the very end.", {
|
|
2099
|
+
summary: z3.string().describe("one line with the numbers that matter"),
|
|
2100
|
+
fine: z3.number().int().min(0).describe("how many things you checked and found in order"),
|
|
2101
|
+
fineNote: z3.string().describe("what those are, in a few words")
|
|
2102
|
+
}, async (args) => {
|
|
2103
|
+
await sent2(tools, { tool: "fine", input: { count: args.fine, note: args.fineNote } });
|
|
2104
|
+
onFinish({ changed: false, summary: args.summary.trim(), note: null, conditionMet: false, callPerson: false });
|
|
2105
|
+
return reply2("Run finished. You can stop now.");
|
|
2106
|
+
});
|
|
2107
|
+
return [problem2, ...tools.asks ? [ask] : [], ...tools.canFix ? [fixed] : [], report, finish2];
|
|
2108
|
+
}
|
|
2109
|
+
function noteLine(note) {
|
|
2110
|
+
return `- ${note.at.slice(0, 16).replace("T", " ")}: ${note.summary}`;
|
|
2111
|
+
}
|
|
2112
|
+
function buildRoutinePrompt(job, folder) {
|
|
2113
|
+
const earlier = job.earlier.length > 0 ? `Earlier runs, the newest last:
|
|
2114
|
+
${job.earlier.map(noteLine).join("\n")}` : null;
|
|
2115
|
+
return {
|
|
2116
|
+
prompt: [job.instruction, earlier].filter(Boolean).join("\n\n"),
|
|
2117
|
+
instructions: [RULES.join("\n"), workplaceText(job.computer, folder), job.sourcesText, job.safetyRules.trim() || null].filter(Boolean).join("\n\n")
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
function routineRunTools(job, api, print2, sleep2) {
|
|
2121
|
+
const routine = job.routine;
|
|
2122
|
+
if (!routine) {
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
const permissions = routine.canFix && !routine.dry ? {
|
|
2126
|
+
send: async (body) => {
|
|
2127
|
+
const answer = await api.request("POST", routinePath(job.checkId, "permissions"), body);
|
|
2128
|
+
if (answer.decision === "wait") {
|
|
2129
|
+
print2(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
2130
|
+
}
|
|
2131
|
+
return answer;
|
|
2132
|
+
},
|
|
2133
|
+
check: (id) => api.request("GET", routinePath(job.checkId, `permissions/${id}`)),
|
|
2134
|
+
sleep: sleep2
|
|
2135
|
+
} : null;
|
|
2136
|
+
return {
|
|
2137
|
+
call: (body) => api.request("POST", routinePath(job.checkId, "routine"), body),
|
|
2138
|
+
permissions,
|
|
2139
|
+
asks: routine.asks,
|
|
2140
|
+
canFix: routine.canFix
|
|
2141
|
+
};
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
// src/sources.ts
|
|
2145
|
+
import { tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
|
|
2146
|
+
import { z as z4 } from "zod";
|
|
2147
|
+
function sourceTools(tools, api, path) {
|
|
2148
|
+
return tools && tools.length > 0 ? { tools, call: (body) => api.request("POST", path, body) } : null;
|
|
2149
|
+
}
|
|
2150
|
+
function fieldSchema(field) {
|
|
2151
|
+
const value = field.type === "integer" ? z4.number().int().positive() : z4.string();
|
|
2152
|
+
return (field.required ? value : value.optional()).describe(field.description);
|
|
2153
|
+
}
|
|
2154
|
+
function inputShape(fields) {
|
|
2155
|
+
return Object.fromEntries(fields.map((field) => [field.name, fieldSchema(field)]));
|
|
2156
|
+
}
|
|
2157
|
+
function replyOf(answer) {
|
|
2158
|
+
return { content: [{ type: "text", text: JSON.stringify(answer) }], ...answer.ok ? {} : { isError: true } };
|
|
2159
|
+
}
|
|
2160
|
+
function sourceToolset(sources) {
|
|
2161
|
+
if (!sources) {
|
|
2162
|
+
return [];
|
|
2163
|
+
}
|
|
2164
|
+
return sources.tools.map((info) => tool4(
|
|
2165
|
+
info.name,
|
|
2166
|
+
info.description,
|
|
2167
|
+
inputShape(info.fields),
|
|
2168
|
+
async (input) => {
|
|
2169
|
+
try {
|
|
2170
|
+
return replyOf(await sources.call({ tool: info.name, input }));
|
|
2171
|
+
} catch (error) {
|
|
2172
|
+
return replyOf({ ok: false, error: `Could not read project data: ${error instanceof Error ? error.message : String(error)}` });
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
));
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
// src/schedules.ts
|
|
2016
2179
|
var PROPOSE_SCHEDULE_TOOL_NAME = "propose_schedule";
|
|
2017
2180
|
var REPORT_CHECK_TOOL_NAME = "report_check";
|
|
2181
|
+
var WAKE_TOOL_NAME = "wake_up_later";
|
|
2018
2182
|
var SCHEDULE_RULE = `Some waits are long: a local stand or a database copy that takes half an hour to come up, a slow build or import, a service that answers only after a while, logs that show a problem only over hours. Do not sleep or poll in a loop for that. Propose a schedule with the ${PROPOSE_SCHEDULE_TOOL_NAME} tool: a short check repeated on a timer by a cheap model that reads the notes of earlier runs and reports one line in the task chat. Give it a clear condition that ends it, the longest interval that still helps, and when to call a person. With wait true this run ends its turn and continues here when the schedule ends. Never use a schedule for work you can finish now, for a check you can do once, or to watch something "just in case". Without autopilot a person starts it first; on autopilot it starts at once within the project limits.`;
|
|
2019
2183
|
var ALREADY_WAITING_TEXT2 = "This run already waits for something. End your turn now: the conversation continues when it comes.";
|
|
2020
|
-
function
|
|
2184
|
+
function reply3(text2, isError = false) {
|
|
2021
2185
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
2022
2186
|
}
|
|
2023
2187
|
function proposeScheduleTool(tools, waiting) {
|
|
2024
|
-
return
|
|
2188
|
+
return tool5(
|
|
2025
2189
|
PROPOSE_SCHEDULE_TOOL_NAME,
|
|
2026
2190
|
"Repeat a short check on a timer while the task waits for something slow. Each run is a short cheap run that reads the notes of earlier runs and reports one line in the task chat; it ends when its condition comes, its time runs out or it made its runs.",
|
|
2027
2191
|
{
|
|
2028
|
-
title:
|
|
2029
|
-
instruction:
|
|
2030
|
-
everyMinutes:
|
|
2031
|
-
dailyAt:
|
|
2032
|
-
days:
|
|
2033
|
-
until:
|
|
2034
|
-
forMinutes:
|
|
2035
|
-
maxRuns:
|
|
2036
|
-
where:
|
|
2037
|
-
callWhen:
|
|
2038
|
-
callOnTimeout:
|
|
2039
|
-
keepAfterStep:
|
|
2040
|
-
wait:
|
|
2192
|
+
title: z5.string().describe('a short name, at most 80 characters: "Wait for the local stand"'),
|
|
2193
|
+
instruction: z5.string().describe("what each run does, in plain words, as a note to yourself: what to look at and what counts as new"),
|
|
2194
|
+
everyMinutes: z5.number().int().nullable().describe("repeat every N minutes (60 for every hour); null when dailyAt is set"),
|
|
2195
|
+
dailyAt: z5.string().nullable().describe("repeat every day at this time, HH:MM in the project time zone; null when everyMinutes is set"),
|
|
2196
|
+
days: z5.array(z5.number().int().min(0).max(6)).describe("with dailyAt: days of the week, 0 is Sunday; empty means every day"),
|
|
2197
|
+
until: z5.string().describe('the condition each run checks, for example "the stand answers on localhost:3100"; when it comes, the schedule ends. Empty when it simply runs for its time'),
|
|
2198
|
+
forMinutes: z5.number().int().nullable().describe("how long it runs at most, in minutes; null takes the longest the project allows"),
|
|
2199
|
+
maxRuns: z5.number().int().nullable().describe("the number of runs at most, or null"),
|
|
2200
|
+
where: z5.enum(["computer", "cloud"]).describe("computer: this computer, free, skipped while it sleeps; cloud: any time, for credits"),
|
|
2201
|
+
callWhen: z5.string().describe('when to call a person, in plain words: "a container keeps restarting"'),
|
|
2202
|
+
callOnTimeout: z5.boolean().describe("true: call a person when the condition did not come in time"),
|
|
2203
|
+
keepAfterStep: z5.boolean().describe("true only when it must keep running after the task leaves this step"),
|
|
2204
|
+
wait: z5.boolean().describe("true: this run ends its turn now and continues here when the schedule ends")
|
|
2041
2205
|
},
|
|
2042
2206
|
async (args) => {
|
|
2043
2207
|
if (args.wait && (waiting.waitingFor !== null || waiting.waitingForSchedule !== null)) {
|
|
2044
|
-
return
|
|
2208
|
+
return reply3(ALREADY_WAITING_TEXT2, true);
|
|
2045
2209
|
}
|
|
2046
2210
|
let answer;
|
|
2047
2211
|
try {
|
|
2048
2212
|
answer = await tools.send({ ...args, title: args.title.trim(), instruction: args.instruction.trim(), until: args.until.trim(), callWhen: args.callWhen.trim(), model: null });
|
|
2049
2213
|
} catch (error) {
|
|
2050
|
-
return
|
|
2214
|
+
return reply3(`Could not send the schedule: ${error instanceof Error ? error.message : String(error)}. Wait another way or say so in your report.`, true);
|
|
2051
2215
|
}
|
|
2052
2216
|
if (answer.id === null) {
|
|
2053
|
-
return
|
|
2217
|
+
return reply3(answer.text, true);
|
|
2054
2218
|
}
|
|
2055
2219
|
if (answer.waits) {
|
|
2056
2220
|
waiting.waitingForSchedule = answer.id;
|
|
2057
2221
|
}
|
|
2058
|
-
return
|
|
2222
|
+
return reply3(answer.text);
|
|
2059
2223
|
}
|
|
2060
2224
|
);
|
|
2061
2225
|
}
|
|
2062
|
-
function scheduleTools(api, path) {
|
|
2063
|
-
return {
|
|
2226
|
+
function scheduleTools(api, path, wakePath) {
|
|
2227
|
+
return {
|
|
2228
|
+
send: (body) => api.request("POST", path, body),
|
|
2229
|
+
wake: (body) => api.request("POST", wakePath, body)
|
|
2230
|
+
};
|
|
2231
|
+
}
|
|
2232
|
+
function wakeUpLaterTool(tools) {
|
|
2233
|
+
return tool5(
|
|
2234
|
+
WAKE_TOOL_NAME,
|
|
2235
|
+
"Put the task to sleep and wake it up later, once: for example 7 days after a mailing to collect its results, or 3 days after a follow-up. Nothing runs and nothing is charged while it sleeps. On waking the task moves to the step you name, along a rule of the board (a rule that only a person moves waits for that person), or this step starts again when you name none. A person moving the task meanwhile cancels the wake-up. Finish your run right after you set it.",
|
|
2236
|
+
{
|
|
2237
|
+
inHours: z5.number().min(0.1).max(2160).describe("how many hours from now, for example 168 for 7 days"),
|
|
2238
|
+
step: z5.string().describe("the name of the step to wake up on, empty to start this step again"),
|
|
2239
|
+
note: z5.string().describe("what to do on waking, in plain words")
|
|
2240
|
+
},
|
|
2241
|
+
async (args) => {
|
|
2242
|
+
try {
|
|
2243
|
+
return reply3((await tools.wake({ inHours: args.inHours, step: args.step.trim(), note: args.note.trim() })).text);
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
return reply3(`Could not set the wake-up: ${error instanceof Error ? error.message : String(error)}. Say in your report when the task should continue.`, true);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
);
|
|
2064
2249
|
}
|
|
2065
2250
|
async function checkFolder(home, job) {
|
|
2066
2251
|
if (job.taskId !== null) {
|
|
@@ -2081,7 +2266,7 @@ var CHECK_RULES = [
|
|
|
2081
2266
|
`Finish by calling ${REPORT_CHECK_TOOL_NAME} exactly once: summary is one short line for the task chat with the numbers that matter; changed is true only when something differs from the last note; note is the full facts only when something changed; conditionMet is true when the condition of the schedule came; callPerson is true when the schedule says to call a person for what you saw.`,
|
|
2082
2267
|
"Write in the language of the task. No em dashes and no en dashes."
|
|
2083
2268
|
];
|
|
2084
|
-
function
|
|
2269
|
+
function noteLine2(note) {
|
|
2085
2270
|
const head = `- ${note.at.slice(0, 16).replace("T", " ")} (${note.outcome}): ${note.summary}`;
|
|
2086
2271
|
return note.note ? `${head}
|
|
2087
2272
|
${note.note.replace(/\n/g, "\n ")}` : head;
|
|
@@ -2094,43 +2279,56 @@ ${job.instruction}`,
|
|
|
2094
2279
|
job.until ? `The schedule ends when: ${job.until}` : "The schedule runs for its time; there is no condition to wait for.",
|
|
2095
2280
|
job.callWhen ? `Call a person when: ${job.callWhen}` : "Call a person only when something is clearly wrong.",
|
|
2096
2281
|
job.earlier.length > 0 ? `Notes of the earlier runs, the newest last:
|
|
2097
|
-
${job.earlier.map(
|
|
2282
|
+
${job.earlier.map(noteLine2).join("\n")}` : "This is the first run: there are no earlier notes."
|
|
2098
2283
|
].join("\n\n");
|
|
2099
2284
|
const instructions = [
|
|
2100
2285
|
CHECK_RULES.join("\n"),
|
|
2101
2286
|
workplaceText(job.computer, folder),
|
|
2287
|
+
job.sourcesText,
|
|
2102
2288
|
job.safetyRules.trim() || null
|
|
2103
2289
|
].filter(Boolean).join("\n\n");
|
|
2104
2290
|
return { prompt, instructions };
|
|
2105
2291
|
}
|
|
2106
2292
|
var SERVER_NAME = "tamsense";
|
|
2107
2293
|
var MAX_CHECK_TURNS = 30;
|
|
2294
|
+
var MAX_ROUTINE_TURNS = 120;
|
|
2108
2295
|
function reportCheckTool(onReport) {
|
|
2109
|
-
return
|
|
2296
|
+
return tool5(
|
|
2110
2297
|
REPORT_CHECK_TOOL_NAME,
|
|
2111
2298
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
2112
2299
|
{
|
|
2113
|
-
changed:
|
|
2114
|
-
summary:
|
|
2115
|
-
note:
|
|
2116
|
-
conditionMet:
|
|
2117
|
-
callPerson:
|
|
2300
|
+
changed: z5.boolean().describe("true when something changed since the earlier runs"),
|
|
2301
|
+
summary: z5.string().describe("one short line for the task chat: what you saw"),
|
|
2302
|
+
note: z5.string().nullable().describe("when something changed: the full facts, the exact lines and what they mean; null otherwise"),
|
|
2303
|
+
conditionMet: z5.boolean().describe("true when the condition of the schedule came"),
|
|
2304
|
+
callPerson: z5.boolean().describe("true when a person must look now, as the schedule says")
|
|
2118
2305
|
},
|
|
2119
2306
|
async (args) => {
|
|
2120
2307
|
onReport({ changed: args.changed, summary: args.summary.trim(), note: args.note?.trim() || null, conditionMet: args.conditionMet, callPerson: args.callPerson });
|
|
2121
|
-
return
|
|
2308
|
+
return reply3("Result received. You can stop now.");
|
|
2122
2309
|
}
|
|
2123
2310
|
);
|
|
2124
2311
|
}
|
|
2312
|
+
function textOf2(message) {
|
|
2313
|
+
if (message.type !== "assistant") {
|
|
2314
|
+
return null;
|
|
2315
|
+
}
|
|
2316
|
+
const text2 = message.message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("\n").trim();
|
|
2317
|
+
return text2 || null;
|
|
2318
|
+
}
|
|
2125
2319
|
function claudeCheckRunner(startQuery) {
|
|
2126
2320
|
return async (request) => {
|
|
2127
2321
|
request.signal.throwIfAborted();
|
|
2128
2322
|
const abortController = new AbortController();
|
|
2129
2323
|
request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
2130
2324
|
let result = null;
|
|
2131
|
-
const
|
|
2132
|
-
result =
|
|
2133
|
-
}
|
|
2325
|
+
const found = (input) => {
|
|
2326
|
+
result = input;
|
|
2327
|
+
};
|
|
2328
|
+
const own = request.routine ? routineToolset(request.routine, found) : [reportCheckTool(found)];
|
|
2329
|
+
const tools = [...own, ...sourceToolset(request.sources)];
|
|
2330
|
+
const lastWords = { text: "" };
|
|
2331
|
+
const permissions = request.routine?.permissions ?? null;
|
|
2134
2332
|
const messages = startQuery({
|
|
2135
2333
|
prompt: request.prompt.prompt,
|
|
2136
2334
|
options: {
|
|
@@ -2138,10 +2336,10 @@ function claudeCheckRunner(startQuery) {
|
|
|
2138
2336
|
systemPrompt: { type: "preset", preset: "claude_code", append: request.prompt.instructions },
|
|
2139
2337
|
settingSources: ["user", "project", "local"],
|
|
2140
2338
|
permissionMode: "auto",
|
|
2141
|
-
canUseTool: denyWhenAsked,
|
|
2142
|
-
mcpServers: { [SERVER_NAME]: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools
|
|
2143
|
-
allowedTools:
|
|
2144
|
-
maxTurns: MAX_CHECK_TURNS,
|
|
2339
|
+
canUseTool: permissions ? askPermission(permissions, () => lastWords.text, request.cwd) : denyWhenAsked,
|
|
2340
|
+
mcpServers: { [SERVER_NAME]: createSdkMcpServer({ name: SERVER_NAME, version: "1.0.0", tools }) },
|
|
2341
|
+
allowedTools: tools.map((item) => `mcp__${SERVER_NAME}__${item.name}`),
|
|
2342
|
+
maxTurns: request.routine ? MAX_ROUTINE_TURNS : MAX_CHECK_TURNS,
|
|
2145
2343
|
abortController,
|
|
2146
2344
|
env: request.env,
|
|
2147
2345
|
...request.model === null ? {} : { model: request.model }
|
|
@@ -2149,6 +2347,7 @@ function claudeCheckRunner(startQuery) {
|
|
|
2149
2347
|
});
|
|
2150
2348
|
let outcome = { completed: false, report: "Claude finished without a result.", model: null, usage: null, result: null };
|
|
2151
2349
|
for await (const message of messages) {
|
|
2350
|
+
lastWords.text = textOf2(message) ?? lastWords.text;
|
|
2152
2351
|
if (message.type === "result") {
|
|
2153
2352
|
const finished = outcomeOf(message, "");
|
|
2154
2353
|
outcome = { completed: finished.completed, report: finished.report, model: finished.model, usage: finished.usage, result: null };
|
|
@@ -2322,7 +2521,7 @@ var SCREENSHOT_RULE = `If you render screens (screenshots of the interface), att
|
|
|
2322
2521
|
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".';
|
|
2323
2522
|
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, { notes }), where notes say in your own words what you tested in that browser and what happened: what you opened, what you clicked or typed, what you expected, and whether it passed or failed (at most 600 characters). People watching the replay read them first. 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.`;
|
|
2324
2523
|
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.";
|
|
2325
|
-
var FOLDER_RULE = "This
|
|
2524
|
+
var FOLDER_RULE = "This run has no repositories. The current folder belongs to this task: work in it and keep your throwaway files here.";
|
|
2326
2525
|
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.";
|
|
2327
2526
|
var FOLDER_DO_RULES = [
|
|
2328
2527
|
`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.`,
|
|
@@ -2382,7 +2581,7 @@ var SPEC_RULES = [
|
|
|
2382
2581
|
var SPEC_WORK_MISSING = "TAMsense sent a spec step without its round and brief.";
|
|
2383
2582
|
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.`;
|
|
2384
2583
|
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.";
|
|
2385
|
-
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
|
|
2584
|
+
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 task has no repositories), 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).";
|
|
2386
2585
|
var RESULT_BRIEF_RULES = [
|
|
2387
2586
|
'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.',
|
|
2388
2587
|
'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".',
|
|
@@ -2450,12 +2649,17 @@ function folderText(root, results) {
|
|
|
2450
2649
|
return lines.length > 0 ? `The task folder is ${root}. The results of the task so far, latest versions:
|
|
2451
2650
|
${bulletList(lines)}` : `The task folder is ${root}. The task has no result yet.`;
|
|
2452
2651
|
}
|
|
2652
|
+
function taskResultsText(results) {
|
|
2653
|
+
const lines = results ?? [];
|
|
2654
|
+
return lines.length > 0 ? `The task already has these documents and results, latest versions, in the ${RESULTS_FOLDER} folder next to the repositories. Read what concerns this step before you start:
|
|
2655
|
+
${bulletList(lines)}` : null;
|
|
2656
|
+
}
|
|
2453
2657
|
function handOverText(boards) {
|
|
2454
2658
|
const names = (boards ?? []).map((board) => board.name);
|
|
2455
2659
|
if (names.length === 0) {
|
|
2456
2660
|
return null;
|
|
2457
2661
|
}
|
|
2458
|
-
return `When part of the work belongs to another board,
|
|
2662
|
+
return `When part of the work belongs to another board, or the work goes on as a new task, such as the next wave, create a linked task with the ${HAND_OVER_TOOL_NAME} tool on: ${names.join(", ")}. Write a plain title and a note on what you found and what the new task 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.`;
|
|
2459
2663
|
}
|
|
2460
2664
|
function handoffText(targets, isCheck) {
|
|
2461
2665
|
if (targets.length === 0) {
|
|
@@ -2531,7 +2735,7 @@ function briefGoal(job, work2) {
|
|
|
2531
2735
|
}
|
|
2532
2736
|
return work2.round > 1 ? `Follow-up questions for TAMsense task #${job.taskId}: ${job.title}` : `Prepare TAMsense task #${job.taskId} for its brief: ${job.title}`;
|
|
2533
2737
|
}
|
|
2534
|
-
function buildBriefPrompt(job, work2, workspace) {
|
|
2738
|
+
function buildBriefPrompt(job, work2, workspace, options) {
|
|
2535
2739
|
const prompt = [
|
|
2536
2740
|
briefGoal(job, work2),
|
|
2537
2741
|
job.description,
|
|
@@ -2550,8 +2754,9 @@ ${JSON.stringify(job.spec)}` : null,
|
|
|
2550
2754
|
workplaceText(job.computer, workspace.root),
|
|
2551
2755
|
stepLine(job.stage),
|
|
2552
2756
|
job.stage.instructions,
|
|
2757
|
+
job.sourcesText,
|
|
2553
2758
|
contextText(job.projectContext),
|
|
2554
|
-
FOLDER_RULE,
|
|
2759
|
+
`${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
|
|
2555
2760
|
RESULT_HOME_RULE,
|
|
2556
2761
|
`How to work:
|
|
2557
2762
|
${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work2))}`,
|
|
@@ -2561,7 +2766,7 @@ ${bulletList(work2.round === null ? RESULT_BRIEF_RULES : briefQuestionRules(work
|
|
|
2561
2766
|
].filter(Boolean).join("\n\n");
|
|
2562
2767
|
return { prompt, instructions };
|
|
2563
2768
|
}
|
|
2564
|
-
function buildSpecPrompt(job, work2, workspace) {
|
|
2769
|
+
function buildSpecPrompt(job, work2, workspace, options) {
|
|
2565
2770
|
const prompt = [
|
|
2566
2771
|
specGoal(job, work2),
|
|
2567
2772
|
job.description,
|
|
@@ -2580,10 +2785,12 @@ ${JSON.stringify(job.spec)}` : null,
|
|
|
2580
2785
|
workplaceText(job.computer, workspace.root),
|
|
2581
2786
|
stepLine(job.stage),
|
|
2582
2787
|
job.stage.instructions,
|
|
2788
|
+
job.sourcesText,
|
|
2583
2789
|
contextText(job.projectContext),
|
|
2584
2790
|
`The repositories for this task are checked out in the current folder, one subfolder each:
|
|
2585
2791
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
2586
2792
|
isLocalJob(job) && LOCAL_FOLDERS_RULE,
|
|
2793
|
+
taskResultsText(options.results),
|
|
2587
2794
|
READ_ONLY_RULE,
|
|
2588
2795
|
`How to work:
|
|
2589
2796
|
${bulletList(work2.round === null ? SPEC_RULES : questionRules(work2))}`,
|
|
@@ -2620,6 +2827,7 @@ ${job.review}`,
|
|
|
2620
2827
|
workplaceText(job.computer, workspace.root),
|
|
2621
2828
|
stepLine(job.stage),
|
|
2622
2829
|
job.stage.instructions,
|
|
2830
|
+
job.sourcesText,
|
|
2623
2831
|
contextText(job.projectContext),
|
|
2624
2832
|
`${FOLDER_RULE} ${folderText(workspace.root, options.results)}`,
|
|
2625
2833
|
`How to work:
|
|
@@ -2641,7 +2849,7 @@ function buildTaskPrompt(job, workspace, options = {}) {
|
|
|
2641
2849
|
if (!job.specWork) {
|
|
2642
2850
|
throw new Error(SPEC_WORK_MISSING);
|
|
2643
2851
|
}
|
|
2644
|
-
return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace) : buildSpecPrompt(job, job.specWork, workspace);
|
|
2852
|
+
return isFolderJob(job) ? buildBriefPrompt(job, job.specWork, workspace, options) : buildSpecPrompt(job, job.specWork, workspace, options);
|
|
2645
2853
|
}
|
|
2646
2854
|
if (isFolderJob(job)) {
|
|
2647
2855
|
return buildFolderPrompt(job, workspace, options);
|
|
@@ -2669,10 +2877,12 @@ ${job.review}`,
|
|
|
2669
2877
|
workplaceText(job.computer, workspace.root),
|
|
2670
2878
|
stepLine(job.stage),
|
|
2671
2879
|
job.stage.instructions,
|
|
2880
|
+
job.sourcesText,
|
|
2672
2881
|
contextText(job.projectContext),
|
|
2673
2882
|
`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}:
|
|
2674
2883
|
${bulletList(repositoryLines(job, workspace))}`,
|
|
2675
2884
|
isLocalJob(job) && LOCAL_FOLDERS_RULE,
|
|
2885
|
+
taskResultsText(options.results),
|
|
2676
2886
|
`How to work:
|
|
2677
2887
|
${bulletList(rules)}`,
|
|
2678
2888
|
safetyText(job),
|
|
@@ -2687,135 +2897,135 @@ ${bulletList(rules)}`,
|
|
|
2687
2897
|
}
|
|
2688
2898
|
|
|
2689
2899
|
// src/specTools.ts
|
|
2690
|
-
import { tool as
|
|
2691
|
-
import { z as
|
|
2900
|
+
import { tool as tool6 } from "@anthropic-ai/claude-agent-sdk";
|
|
2901
|
+
import { z as z6 } from "zod";
|
|
2692
2902
|
var MAX_FIRST_ROUND = 4;
|
|
2693
|
-
var questionSchema =
|
|
2694
|
-
id:
|
|
2695
|
-
question:
|
|
2696
|
-
whyItMatters:
|
|
2697
|
-
options:
|
|
2698
|
-
label:
|
|
2699
|
-
description:
|
|
2700
|
-
recommended:
|
|
2903
|
+
var questionSchema = z6.object({
|
|
2904
|
+
id: z6.string().describe('Stable snake_case id, unique in the task, for example "empty_state"'),
|
|
2905
|
+
question: z6.string().describe("One plain sentence the owner can answer without reading code"),
|
|
2906
|
+
whyItMatters: z6.string().describe("One line: what changes in the result depending on the answer"),
|
|
2907
|
+
options: z6.array(z6.object({
|
|
2908
|
+
label: z6.string().describe('Short, concrete answer (1-6 words), without "(Recommended)": use the recommended flag'),
|
|
2909
|
+
description: z6.string().describe("One sentence: what happens if this option is chosen"),
|
|
2910
|
+
recommended: z6.boolean().describe("true on the option you recommend (in a multiSelect question, on every option you recommend). Recommended options come first")
|
|
2701
2911
|
})).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'),
|
|
2702
|
-
multiSelect:
|
|
2912
|
+
multiSelect: z6.boolean().describe("true only when the options are additive and several can apply together")
|
|
2703
2913
|
});
|
|
2704
|
-
var briefSchema =
|
|
2705
|
-
findings:
|
|
2706
|
-
fact:
|
|
2707
|
-
where:
|
|
2914
|
+
var briefSchema = z6.object({
|
|
2915
|
+
findings: z6.array(z6.object({
|
|
2916
|
+
fact: z6.string().describe("What the code does today that matters for this task"),
|
|
2917
|
+
where: z6.string().describe("Where it is: owner/repo path:line")
|
|
2708
2918
|
})).describe("Findings from the code"),
|
|
2709
|
-
dataFacts:
|
|
2710
|
-
approach:
|
|
2711
|
-
codeMap:
|
|
2712
|
-
repo:
|
|
2713
|
-
file:
|
|
2714
|
-
role:
|
|
2919
|
+
dataFacts: z6.array(z6.string()).describe("Numbers and facts from the events or the business data, each with its source. Empty when the task needs no data"),
|
|
2920
|
+
approach: z6.string().describe("The approach you would take, in 2-4 sentences"),
|
|
2921
|
+
codeMap: z6.array(z6.object({
|
|
2922
|
+
repo: z6.string().describe("Repository full name (owner/name)"),
|
|
2923
|
+
file: z6.string().describe("Path relative to the repository root"),
|
|
2924
|
+
role: z6.string().describe("Why it matters: what to change or reuse there")
|
|
2715
2925
|
})).describe("The files the change will touch or must reuse"),
|
|
2716
|
-
assumptions:
|
|
2717
|
-
text:
|
|
2718
|
-
reason:
|
|
2926
|
+
assumptions: z6.array(z6.object({
|
|
2927
|
+
text: z6.string().describe("The default you chose"),
|
|
2928
|
+
reason: z6.string().describe("Why it is safe or why it was chosen")
|
|
2719
2929
|
})).describe("Corners settled with a default instead of a question"),
|
|
2720
|
-
pendingDecisions:
|
|
2930
|
+
pendingDecisions: z6.array(z6.string()).describe("Decisions that depend on an answer to this round and cannot be asked yet. Empty when none")
|
|
2721
2931
|
});
|
|
2722
|
-
var questionsPayloadSchema =
|
|
2932
|
+
var questionsPayloadSchema = z6.object({
|
|
2723
2933
|
brief: briefSchema,
|
|
2724
|
-
questions:
|
|
2934
|
+
questions: z6.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`)
|
|
2725
2935
|
});
|
|
2726
|
-
var specSchema =
|
|
2727
|
-
goal:
|
|
2728
|
-
context:
|
|
2729
|
-
inScope:
|
|
2730
|
-
outOfScope:
|
|
2731
|
-
decisions:
|
|
2732
|
-
question:
|
|
2733
|
-
answer:
|
|
2734
|
-
by:
|
|
2936
|
+
var specSchema = z6.object({
|
|
2937
|
+
goal: z6.string().describe("The problem solved, for whom, and the outcome a user or the business will notice"),
|
|
2938
|
+
context: z6.string().describe("Summary: the problem in 1-2 sentences, then the chosen approach in 1-2 sentences"),
|
|
2939
|
+
inScope: z6.array(z6.string()).describe("What this task delivers, one item each"),
|
|
2940
|
+
outOfScope: z6.array(z6.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"),
|
|
2941
|
+
decisions: z6.array(z6.object({
|
|
2942
|
+
question: z6.string(),
|
|
2943
|
+
answer: z6.string(),
|
|
2944
|
+
by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
2735
2945
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
2736
|
-
codeMap:
|
|
2737
|
-
repo:
|
|
2738
|
-
file:
|
|
2739
|
-
role:
|
|
2740
|
-
use:
|
|
2946
|
+
codeMap: z6.array(z6.object({
|
|
2947
|
+
repo: z6.string().describe("Repository full name (owner/name)"),
|
|
2948
|
+
file: z6.string().describe("Path relative to the repository root"),
|
|
2949
|
+
role: z6.string().describe("What the coder needs to know about this file"),
|
|
2950
|
+
use: z6.enum(["reuse", "change", "keep"]).describe("reuse: build on it; change: it is in changes; keep: do not touch")
|
|
2741
2951
|
})).describe("The files a coder must read before starting"),
|
|
2742
|
-
changes:
|
|
2743
|
-
repo:
|
|
2744
|
-
file:
|
|
2745
|
-
action:
|
|
2746
|
-
description:
|
|
2747
|
-
produces:
|
|
2748
|
-
consumes:
|
|
2749
|
-
acceptance:
|
|
2952
|
+
changes: z6.array(z6.object({
|
|
2953
|
+
repo: z6.string().describe("Repository full name (owner/name) where the change happens"),
|
|
2954
|
+
file: z6.string().describe("Path to the file, relative to the repository root"),
|
|
2955
|
+
action: z6.enum(["create", "modify", "delete"]),
|
|
2956
|
+
description: z6.string().describe("The behaviour the file has after the change and which existing functions or patterns to extend. No code snippets or diffs"),
|
|
2957
|
+
produces: z6.array(z6.string()).describe("Names with signatures or data shapes this change creates or changes for other changes. Empty when none"),
|
|
2958
|
+
consumes: z6.array(z6.string()).describe("Names with signatures or data shapes this change uses from earlier changes. Empty when none"),
|
|
2959
|
+
acceptance: z6.array(z6.string()).describe("Ids of the criteria this change makes true, like AC-1")
|
|
2750
2960
|
})).min(1).describe("File changes in dependency order: each uses only what earlier changes or the code already provide"),
|
|
2751
|
-
acceptance:
|
|
2752
|
-
id:
|
|
2753
|
-
kind:
|
|
2754
|
-
text:
|
|
2961
|
+
acceptance: z6.array(z6.object({
|
|
2962
|
+
id: z6.string().describe("AC-1, AC-2 and so on, in order"),
|
|
2963
|
+
kind: z6.enum(["main", "edge", "keep"]).describe("main: the main path; edge: an error, empty or limit case; keep: existing behaviour that must keep working"),
|
|
2964
|
+
text: z6.string().describe('"Given <state>, when <action>, then <observable result>", saying where to look. No file paths or function names')
|
|
2755
2965
|
})).min(1).describe("Acceptance criteria: main path first, then edge cases, then what must keep working"),
|
|
2756
|
-
testPlan:
|
|
2757
|
-
automated:
|
|
2758
|
-
repo:
|
|
2759
|
-
command:
|
|
2760
|
-
expect:
|
|
2966
|
+
testPlan: z6.object({
|
|
2967
|
+
automated: z6.array(z6.object({
|
|
2968
|
+
repo: z6.string(),
|
|
2969
|
+
command: z6.string().describe("The exact command to run"),
|
|
2970
|
+
expect: z6.string().describe("What passing looks like")
|
|
2761
2971
|
})).describe("At least one command"),
|
|
2762
|
-
newTests:
|
|
2763
|
-
seam:
|
|
2764
|
-
covers:
|
|
2972
|
+
newTests: z6.array(z6.object({
|
|
2973
|
+
seam: z6.string().describe("Where the test goes: the highest seam that covers the behaviour (an endpoint, a page, a public function)"),
|
|
2974
|
+
covers: z6.array(z6.string()).describe("Ids of the criteria it covers")
|
|
2765
2975
|
})),
|
|
2766
|
-
manual:
|
|
2767
|
-
page:
|
|
2768
|
-
steps:
|
|
2976
|
+
manual: z6.array(z6.object({
|
|
2977
|
+
page: z6.string().describe("The page or screen"),
|
|
2978
|
+
steps: z6.string().describe("What a person does and sees")
|
|
2769
2979
|
}))
|
|
2770
2980
|
}),
|
|
2771
|
-
risks:
|
|
2772
|
-
risk:
|
|
2773
|
-
mitigation:
|
|
2981
|
+
risks: z6.array(z6.object({
|
|
2982
|
+
risk: z6.string(),
|
|
2983
|
+
mitigation: z6.string()
|
|
2774
2984
|
})).describe("What could go wrong: migrations, existing data, access, performance, compatibility"),
|
|
2775
|
-
riskLevel:
|
|
2776
|
-
assumptions:
|
|
2777
|
-
text:
|
|
2778
|
-
reason:
|
|
2985
|
+
riskLevel: z6.enum(["low", "medium", "high"]).describe("How closely a person should review the result"),
|
|
2986
|
+
assumptions: z6.array(z6.object({
|
|
2987
|
+
text: z6.string().describe("The default you chose"),
|
|
2988
|
+
reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
2779
2989
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
2780
|
-
openQuestions:
|
|
2781
|
-
split:
|
|
2990
|
+
openQuestions: z6.array(questionSchema).describe("Questions only the owner can settle; the spec cannot be approved until each is answered or dismissed. Empty when none"),
|
|
2991
|
+
split: z6.array(z6.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")
|
|
2782
2992
|
});
|
|
2783
|
-
var resultBriefSchema =
|
|
2784
|
-
format:
|
|
2785
|
-
goal:
|
|
2786
|
-
context:
|
|
2787
|
-
contains:
|
|
2788
|
-
acceptance:
|
|
2789
|
-
id:
|
|
2790
|
-
text:
|
|
2993
|
+
var resultBriefSchema = z6.object({
|
|
2994
|
+
format: z6.literal("brief").describe('Always "brief"'),
|
|
2995
|
+
goal: z6.string().describe("What the work is for, for whom, and the outcome people will notice"),
|
|
2996
|
+
context: z6.string().describe("Summary: the situation in 1-2 sentences, then how the work will be done in 1-2 sentences"),
|
|
2997
|
+
contains: z6.array(z6.string()).min(1).describe("What the result must contain, one item each: the parts, facts and attachments a reader expects"),
|
|
2998
|
+
acceptance: z6.array(z6.object({
|
|
2999
|
+
id: z6.string().describe("AC-1, AC-2 and so on, in order"),
|
|
3000
|
+
text: z6.string().describe("One observable property of the result that a checker can confirm by reading it")
|
|
2791
3001
|
})).min(1).describe("Acceptance criteria the checks judge the result by, the most important first"),
|
|
2792
|
-
constraints:
|
|
2793
|
-
outOfScope:
|
|
2794
|
-
sources:
|
|
2795
|
-
source:
|
|
2796
|
-
use:
|
|
3002
|
+
constraints: z6.array(z6.string()).describe("Rules the result must keep: length, tone, language, deadlines, what must not be promised. Empty when none"),
|
|
3003
|
+
outOfScope: z6.array(z6.string()).describe("What this work does not do: nearby things someone might be tempted to add. Empty when nothing is tempting"),
|
|
3004
|
+
sources: z6.array(z6.object({
|
|
3005
|
+
source: z6.string().describe("Where the facts come from: a project knowledge note, a linked item, a file of the task, a connected service read only"),
|
|
3006
|
+
use: z6.string().describe("What to take from it")
|
|
2797
3007
|
})).describe("Sources the work must use. Empty when the task itself is enough"),
|
|
2798
|
-
approver:
|
|
2799
|
-
decisions:
|
|
2800
|
-
question:
|
|
2801
|
-
answer:
|
|
2802
|
-
by:
|
|
3008
|
+
approver: z6.string().describe("Who approves the result, as the task or the board names them"),
|
|
3009
|
+
decisions: z6.array(z6.object({
|
|
3010
|
+
question: z6.string(),
|
|
3011
|
+
answer: z6.string(),
|
|
3012
|
+
by: z6.enum(["owner", "agent"]).describe("owner: answered by a person; agent: your own call")
|
|
2803
3013
|
})).describe("Every answered question with by: owner, and your own calls that matter with by: agent"),
|
|
2804
|
-
assumptions:
|
|
2805
|
-
text:
|
|
2806
|
-
reason:
|
|
3014
|
+
assumptions: z6.array(z6.object({
|
|
3015
|
+
text: z6.string().describe("The default you chose"),
|
|
3016
|
+
reason: z6.string().describe("Why, so the owner can confirm or correct it quickly")
|
|
2807
3017
|
})).describe("Defaults chosen where the task was silent. Empty when nothing was assumed"),
|
|
2808
|
-
openQuestions:
|
|
3018
|
+
openQuestions: z6.array(questionSchema).describe("Questions only the owner can settle; the brief cannot be approved until each is answered or dismissed. Empty when none")
|
|
2809
3019
|
});
|
|
2810
|
-
function
|
|
3020
|
+
function reply4(text2, isError = false) {
|
|
2811
3021
|
return { content: [{ type: "text", text: text2 }], ...isError ? { isError: true } : {} };
|
|
2812
3022
|
}
|
|
2813
3023
|
function unreachable(error, toolName) {
|
|
2814
3024
|
const message = error instanceof Error ? error.message : String(error);
|
|
2815
|
-
return
|
|
3025
|
+
return reply4(`Could not reach TAMsense to check this: ${message}. Call ${toolName} again in a moment.`, true);
|
|
2816
3026
|
}
|
|
2817
3027
|
function questionsTool(review, onSubmit) {
|
|
2818
|
-
return
|
|
3028
|
+
return tool6(
|
|
2819
3029
|
SUBMIT_QUESTIONS_TOOL_NAME,
|
|
2820
3030
|
"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.",
|
|
2821
3031
|
questionsPayloadSchema.shape,
|
|
@@ -2827,10 +3037,10 @@ function questionsTool(review, onSubmit) {
|
|
|
2827
3037
|
return unreachable(error, SUBMIT_QUESTIONS_TOOL_NAME);
|
|
2828
3038
|
}
|
|
2829
3039
|
if (!verdict.accepted) {
|
|
2830
|
-
return
|
|
3040
|
+
return reply4(verdict.feedback, true);
|
|
2831
3041
|
}
|
|
2832
3042
|
onSubmit(args);
|
|
2833
|
-
return
|
|
3043
|
+
return reply4(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.");
|
|
2834
3044
|
}
|
|
2835
3045
|
);
|
|
2836
3046
|
}
|
|
@@ -2838,7 +3048,7 @@ var BUILDER_CHECK_FIRST = "The spec passed the lint but was not saved yet: the b
|
|
|
2838
3048
|
function specTool(review, onSubmit, builderCheckRan) {
|
|
2839
3049
|
let repairs = 0;
|
|
2840
3050
|
let reminded = false;
|
|
2841
|
-
return
|
|
3051
|
+
return tool6(
|
|
2842
3052
|
SUBMIT_SPEC_TOOL_NAME,
|
|
2843
3053
|
"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.",
|
|
2844
3054
|
specSchema.shape,
|
|
@@ -2851,20 +3061,20 @@ function specTool(review, onSubmit, builderCheckRan) {
|
|
|
2851
3061
|
}
|
|
2852
3062
|
if (!verdict.accepted) {
|
|
2853
3063
|
repairs += 1;
|
|
2854
|
-
return
|
|
3064
|
+
return reply4(verdict.feedback, true);
|
|
2855
3065
|
}
|
|
2856
3066
|
if (!builderCheckRan() && !reminded) {
|
|
2857
3067
|
reminded = true;
|
|
2858
|
-
return
|
|
3068
|
+
return reply4(BUILDER_CHECK_FIRST, true);
|
|
2859
3069
|
}
|
|
2860
3070
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
2861
|
-
return
|
|
3071
|
+
return reply4(verdict.problems.length > 0 ? `Spec saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Spec saved.");
|
|
2862
3072
|
}
|
|
2863
3073
|
);
|
|
2864
3074
|
}
|
|
2865
3075
|
function briefTool(review, onSubmit) {
|
|
2866
3076
|
let repairs = 0;
|
|
2867
|
-
return
|
|
3077
|
+
return tool6(
|
|
2868
3078
|
SUBMIT_SPEC_TOOL_NAME,
|
|
2869
3079
|
"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.",
|
|
2870
3080
|
resultBriefSchema.shape,
|
|
@@ -2877,10 +3087,10 @@ function briefTool(review, onSubmit) {
|
|
|
2877
3087
|
}
|
|
2878
3088
|
if (!verdict.accepted) {
|
|
2879
3089
|
repairs += 1;
|
|
2880
|
-
return
|
|
3090
|
+
return reply4(verdict.feedback, true);
|
|
2881
3091
|
}
|
|
2882
3092
|
onSubmit({ spec: args, problems: verdict.problems });
|
|
2883
|
-
return
|
|
3093
|
+
return reply4(verdict.problems.length > 0 ? `Brief saved with these problems left: ${verdict.problems.join("; ")}. Mention them in your reply.` : "Brief saved.");
|
|
2884
3094
|
}
|
|
2885
3095
|
);
|
|
2886
3096
|
}
|
|
@@ -2983,11 +3193,11 @@ function usageLimitWatch() {
|
|
|
2983
3193
|
var NO_RESULT_TEXT = "Claude finished without a result.";
|
|
2984
3194
|
var SERVER_NAME2 = "tamsense";
|
|
2985
3195
|
var CAPTION_FIELDS = {
|
|
2986
|
-
title:
|
|
2987
|
-
purpose:
|
|
3196
|
+
title: z7.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"'),
|
|
3197
|
+
purpose: z7.string().describe('one line, at most 160 characters, on why it was made: "Comparing two layouts for the empty orders state"')
|
|
2988
3198
|
};
|
|
2989
3199
|
var NOTES_FIELD = {
|
|
2990
|
-
notes:
|
|
3200
|
+
notes: z7.string().optional().describe('what you tested in it and what happened, at most 600 characters: what you opened, what you clicked or typed, what you expected, and whether it passed or failed. "Opened the disposed requests on the warehouse page, filtered by December, expected 3 rows: got 3, passed. Clicked Export: the file was empty, failed."')
|
|
2991
3201
|
};
|
|
2992
3202
|
var ATTACH_TOOLS = {
|
|
2993
3203
|
screenshot: {
|
|
@@ -3008,21 +3218,21 @@ var ATTACH_TOOLS = {
|
|
|
3008
3218
|
};
|
|
3009
3219
|
function attachTool(kind, attach) {
|
|
3010
3220
|
const spec = ATTACH_TOOLS[kind];
|
|
3011
|
-
return
|
|
3221
|
+
return tool7(
|
|
3012
3222
|
spec.name,
|
|
3013
3223
|
spec.description,
|
|
3014
|
-
{ path:
|
|
3224
|
+
{ path: z7.string().describe(spec.path), ...CAPTION_FIELDS, ...kind === "screenshot" ? {} : NOTES_FIELD },
|
|
3015
3225
|
async (args) => ({ content: [{ type: "text", text: await attach({ kind, ...args }) }] })
|
|
3016
3226
|
);
|
|
3017
3227
|
}
|
|
3018
3228
|
function verdictTool(onVerdict) {
|
|
3019
|
-
return
|
|
3229
|
+
return tool7(
|
|
3020
3230
|
VERDICT_TOOL_NAME,
|
|
3021
3231
|
"Hand in the result of this check. Call it exactly once, at the very end.",
|
|
3022
3232
|
{
|
|
3023
|
-
passed:
|
|
3024
|
-
report:
|
|
3025
|
-
couldNotRun:
|
|
3233
|
+
passed: z7.boolean().describe("true only if nothing must be fixed"),
|
|
3234
|
+
report: z7.string().describe("what you checked and, if it failed, exactly what to fix with file and line"),
|
|
3235
|
+
couldNotRun: z7.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")
|
|
3026
3236
|
},
|
|
3027
3237
|
async (args) => {
|
|
3028
3238
|
onVerdict(args.couldNotRun ? { passed: false, report: args.report, couldNotRun: true } : { passed: args.passed, report: args.report });
|
|
@@ -3031,13 +3241,13 @@ function verdictTool(onVerdict) {
|
|
|
3031
3241
|
);
|
|
3032
3242
|
}
|
|
3033
3243
|
function handoffTool(targets, onHandoff) {
|
|
3034
|
-
return
|
|
3244
|
+
return tool7(
|
|
3035
3245
|
HANDOFF_TOOL_NAME,
|
|
3036
3246
|
"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.",
|
|
3037
3247
|
{
|
|
3038
|
-
to:
|
|
3039
|
-
outcome:
|
|
3040
|
-
note:
|
|
3248
|
+
to: z7.string().describe("name or id of the step to hand the task to"),
|
|
3249
|
+
outcome: z7.enum(HANDOFF_OUTCOMES).describe("pass: the work is done; problems: something must be fixed; ask: a question for that step"),
|
|
3250
|
+
note: z7.string().describe("for the next agent and the owner: what was done, what to check, what is open")
|
|
3041
3251
|
},
|
|
3042
3252
|
async (args) => {
|
|
3043
3253
|
const choice = chooseHandoff(targets, args);
|
|
@@ -3050,25 +3260,25 @@ function handoffTool(targets, onHandoff) {
|
|
|
3050
3260
|
);
|
|
3051
3261
|
}
|
|
3052
3262
|
function askPersonTool(asking, waiting) {
|
|
3053
|
-
return
|
|
3263
|
+
return tool7(
|
|
3054
3264
|
ASK_PERSON_TOOL_NAME,
|
|
3055
3265
|
"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.",
|
|
3056
3266
|
{
|
|
3057
|
-
to:
|
|
3058
|
-
why:
|
|
3059
|
-
question:
|
|
3060
|
-
context:
|
|
3061
|
-
tried:
|
|
3062
|
-
did:
|
|
3063
|
-
error:
|
|
3064
|
-
result:
|
|
3267
|
+
to: z7.union([z7.number(), z7.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"),
|
|
3268
|
+
why: z7.string().describe('one line on why this person: "assignee, and the run is on her computer", "Designer, owns empty states"'),
|
|
3269
|
+
question: z7.string().describe("one plain question the person can answer without reading the code"),
|
|
3270
|
+
context: z7.string().optional().describe("one line on what you saw that led to the question"),
|
|
3271
|
+
tried: z7.array(z7.object({
|
|
3272
|
+
did: z7.string().describe("the command or step"),
|
|
3273
|
+
error: z7.string().optional().describe("the exact error line, if there was one"),
|
|
3274
|
+
result: z7.string().describe("what it showed, one line")
|
|
3065
3275
|
})).max(MAX_ASK_TRIES).optional().describe("what you already tried, the most telling first"),
|
|
3066
|
-
options:
|
|
3067
|
-
label:
|
|
3068
|
-
detail:
|
|
3276
|
+
options: z7.array(z7.object({
|
|
3277
|
+
label: z7.string().describe("a short answer the person can pick"),
|
|
3278
|
+
detail: z7.string().describe("one line on what happens if it is picked")
|
|
3069
3279
|
})).max(MAX_ASK_OPTIONS).optional().describe("2 to 4 answers you suggest, the one you would pick first"),
|
|
3070
|
-
blocking:
|
|
3071
|
-
assumption:
|
|
3280
|
+
blocking: z7.boolean().describe("true when you cannot go on without the answer"),
|
|
3281
|
+
assumption: z7.string().optional().describe("when blocking is false: what you continue with until the answer comes")
|
|
3072
3282
|
},
|
|
3073
3283
|
async (args) => {
|
|
3074
3284
|
if (waiting.waitingFor !== null) {
|
|
@@ -3078,16 +3288,16 @@ function askPersonTool(asking, waiting) {
|
|
|
3078
3288
|
if ("error" in choice) {
|
|
3079
3289
|
return { content: [{ type: "text", text: choice.error }], isError: true };
|
|
3080
3290
|
}
|
|
3081
|
-
let
|
|
3291
|
+
let reply5;
|
|
3082
3292
|
try {
|
|
3083
|
-
|
|
3293
|
+
reply5 = await asking.send(choice.body);
|
|
3084
3294
|
} catch (error) {
|
|
3085
3295
|
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 };
|
|
3086
3296
|
}
|
|
3087
3297
|
if (choice.body.blocking) {
|
|
3088
|
-
waiting.waitingFor =
|
|
3298
|
+
waiting.waitingFor = reply5.id;
|
|
3089
3299
|
}
|
|
3090
|
-
return { content: [{ type: "text", text: askedText(
|
|
3300
|
+
return { content: [{ type: "text", text: askedText(reply5.name, choice.body) }] };
|
|
3091
3301
|
}
|
|
3092
3302
|
);
|
|
3093
3303
|
}
|
|
@@ -3132,7 +3342,8 @@ function tamsenseServer(request, submitted) {
|
|
|
3132
3342
|
})] : [],
|
|
3133
3343
|
...request.results ? resultToolset(request.results) : [],
|
|
3134
3344
|
...request.handOver ? [handOverTool(request.handOver)] : [],
|
|
3135
|
-
...request.schedules ? [proposeScheduleTool(request.schedules, submitted)] : []
|
|
3345
|
+
...request.schedules ? [proposeScheduleTool(request.schedules, submitted), wakeUpLaterTool(request.schedules)] : [],
|
|
3346
|
+
...sourceToolset(request.sources)
|
|
3136
3347
|
];
|
|
3137
3348
|
return {
|
|
3138
3349
|
server: createSdkMcpServer2({ name: SERVER_NAME2, version: "1.0.0", tools }),
|
|
@@ -3159,6 +3370,7 @@ async function attempt(request, context, resume) {
|
|
|
3159
3370
|
let outcome = { completed: false, report: NO_RESULT_TEXT, model: null, usage: null, verdict: null };
|
|
3160
3371
|
const tamsense = tamsenseServer(request, context.submitted);
|
|
3161
3372
|
const shown = hiddenStepsFilter();
|
|
3373
|
+
const calls = sourceCalls(request.sources?.tools);
|
|
3162
3374
|
const helpers = helperTasks();
|
|
3163
3375
|
const messages = context.startQuery({
|
|
3164
3376
|
prompt: request.input.start(request.prompt.prompt),
|
|
@@ -3193,7 +3405,7 @@ async function attempt(request, context, resume) {
|
|
|
3193
3405
|
messages.close();
|
|
3194
3406
|
return { outcome, sessionId, resumeFailed: true };
|
|
3195
3407
|
}
|
|
3196
|
-
for (const draft of [...helpers.observe(message), ...feedOf(message).filter(shown).flatMap(helpers.adjust)]) {
|
|
3408
|
+
for (const draft of [...helpers.observe(message), ...feedOf(message, calls).filter(shown).flatMap(helpers.adjust)]) {
|
|
3197
3409
|
if (draft.kind === "agent" && draft.parent === null) {
|
|
3198
3410
|
context.submitted.helperStarted = true;
|
|
3199
3411
|
}
|
|
@@ -3299,14 +3511,14 @@ async function login(tools, request) {
|
|
|
3299
3511
|
tools.openBrowser(pageUrl);
|
|
3300
3512
|
while (tools.now() < expiresAt) {
|
|
3301
3513
|
await tools.sleep(pairing.intervalSeconds * MS_PER_SECOND);
|
|
3302
|
-
const
|
|
3303
|
-
if (
|
|
3304
|
-
return { token:
|
|
3514
|
+
const reply5 = await tools.api.request("POST", PAIRING_TOKEN_PATH, { deviceCode: pairing.deviceCode });
|
|
3515
|
+
if (reply5.status === "connected") {
|
|
3516
|
+
return { token: reply5.token, device: reply5.device };
|
|
3305
3517
|
}
|
|
3306
|
-
if (
|
|
3518
|
+
if (reply5.status === "denied") {
|
|
3307
3519
|
throw new Error(DENIED_TEXT);
|
|
3308
3520
|
}
|
|
3309
|
-
if (
|
|
3521
|
+
if (reply5.status === "expired") {
|
|
3310
3522
|
break;
|
|
3311
3523
|
}
|
|
3312
3524
|
}
|
|
@@ -3679,6 +3891,13 @@ function describeDevice(identity) {
|
|
|
3679
3891
|
return identity.device.team ? `a team computer of ${identity.projectName}` : `the personal computer of ${identity.email ?? "its owner"}`;
|
|
3680
3892
|
}
|
|
3681
3893
|
|
|
3894
|
+
// src/sense.ts
|
|
3895
|
+
import { mkdtemp, rm as rm5 } from "fs/promises";
|
|
3896
|
+
import { tmpdir } from "os";
|
|
3897
|
+
import { dirname as dirname4, join as join9 } from "path";
|
|
3898
|
+
import process5 from "process";
|
|
3899
|
+
import { createSdkMcpServer as createSdkMcpServer3, query as query4 } from "@anthropic-ai/claude-agent-sdk";
|
|
3900
|
+
|
|
3682
3901
|
// src/system.ts
|
|
3683
3902
|
import { spawn as spawn3 } from "child_process";
|
|
3684
3903
|
import { hostname } from "os";
|
|
@@ -3723,10 +3942,104 @@ async function sleep(ms, signal) {
|
|
|
3723
3942
|
});
|
|
3724
3943
|
}
|
|
3725
3944
|
|
|
3945
|
+
// src/sense.ts
|
|
3946
|
+
var SERVER_NAME3 = "tamsense";
|
|
3947
|
+
var MAX_SENSE_TURNS = 3;
|
|
3948
|
+
var HEARTBEAT_MS = 5e3;
|
|
3949
|
+
var STOPPED_HERE = "stopped_here";
|
|
3950
|
+
var CLOSED_ON_SITE = "closed_on_site";
|
|
3951
|
+
var TOO_LONG = "too_long";
|
|
3952
|
+
var FAILED = { outcome: "failed", text: "", model: null, usage: null };
|
|
3953
|
+
function claudeSenseRunner(startQuery) {
|
|
3954
|
+
return async (request) => {
|
|
3955
|
+
request.signal.throwIfAborted();
|
|
3956
|
+
const abortController = new AbortController();
|
|
3957
|
+
request.signal.addEventListener("abort", () => abortController.abort(), { once: true });
|
|
3958
|
+
const tools = sourceToolset(request.sources);
|
|
3959
|
+
const limit = usageLimitWatch();
|
|
3960
|
+
const messages = startQuery({
|
|
3961
|
+
prompt: request.prompt,
|
|
3962
|
+
options: {
|
|
3963
|
+
cwd: request.cwd,
|
|
3964
|
+
systemPrompt: request.system,
|
|
3965
|
+
tools: [],
|
|
3966
|
+
settingSources: [],
|
|
3967
|
+
persistSession: false,
|
|
3968
|
+
strictMcpConfig: true,
|
|
3969
|
+
mcpServers: tools.length > 0 ? { [SERVER_NAME3]: createSdkMcpServer3({ name: SERVER_NAME3, version: "1.0.0", tools }) } : {},
|
|
3970
|
+
allowedTools: tools.map((item) => `mcp__${SERVER_NAME3}__${item.name}`),
|
|
3971
|
+
permissionMode: "auto",
|
|
3972
|
+
canUseTool: denyWhenAsked,
|
|
3973
|
+
maxTurns: MAX_SENSE_TURNS,
|
|
3974
|
+
outputFormat: { type: "json_schema", schema: request.schema },
|
|
3975
|
+
abortController,
|
|
3976
|
+
env: request.env,
|
|
3977
|
+
...request.model === null ? {} : { model: request.model }
|
|
3978
|
+
}
|
|
3979
|
+
});
|
|
3980
|
+
let finish2 = FAILED;
|
|
3981
|
+
for await (const message of messages) {
|
|
3982
|
+
limit.observe(message);
|
|
3983
|
+
if (message.type === "result") {
|
|
3984
|
+
const measured = outcomeOf(message, "");
|
|
3985
|
+
const text2 = message.subtype !== "success" ? "" : message.structured_output === void 0 ? message.result : JSON.stringify(message.structured_output);
|
|
3986
|
+
finish2 = { outcome: measured.completed && text2 ? "succeeded" : "failed", text: text2, model: measured.model, usage: measured.usage };
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
return limit.pausedUntil(finish2.outcome === "succeeded") ? { ...finish2, outcome: "usage_limit" } : finish2;
|
|
3990
|
+
};
|
|
3991
|
+
}
|
|
3992
|
+
function senseHeartbeat(api, job, abort) {
|
|
3993
|
+
return async () => {
|
|
3994
|
+
try {
|
|
3995
|
+
const answer = await api.request("POST", sensePath(job.senseId, "heartbeat"), void 0, { safeToRepeat: true });
|
|
3996
|
+
if (!answer.keepGoing) {
|
|
3997
|
+
abort();
|
|
3998
|
+
}
|
|
3999
|
+
} catch {
|
|
4000
|
+
}
|
|
4001
|
+
};
|
|
4002
|
+
}
|
|
4003
|
+
async function runSenseJob(job, runSense, tools, stop) {
|
|
4004
|
+
const cwd = await mkdtemp(join9(tmpdir(), "tamsense-sense-"));
|
|
4005
|
+
const run2 = new AbortController();
|
|
4006
|
+
const stopRun = () => run2.abort(STOPPED_HERE);
|
|
4007
|
+
if (stop.aborted) {
|
|
4008
|
+
stopRun();
|
|
4009
|
+
}
|
|
4010
|
+
stop.addEventListener("abort", stopRun, { once: true });
|
|
4011
|
+
const deadline = setTimeout(() => run2.abort(TOO_LONG), job.deadlineMs);
|
|
4012
|
+
const beat = senseHeartbeat(tools.api, job, () => run2.abort(CLOSED_ON_SITE));
|
|
4013
|
+
const beating = setInterval(() => void beat(), HEARTBEAT_MS);
|
|
4014
|
+
let body;
|
|
4015
|
+
try {
|
|
4016
|
+
const env = withNodeOnPath(process5.env, dirname4(process5.execPath));
|
|
4017
|
+
body = await runSense({ system: job.system, prompt: job.prompt, schema: job.schema, model: job.model, cwd, env, signal: run2.signal, sources: sourceTools(job.tools, tools.api, sensePath(job.senseId, "sources")) });
|
|
4018
|
+
} catch {
|
|
4019
|
+
body = FAILED;
|
|
4020
|
+
} finally {
|
|
4021
|
+
clearTimeout(deadline);
|
|
4022
|
+
clearInterval(beating);
|
|
4023
|
+
stop.removeEventListener("abort", stopRun);
|
|
4024
|
+
await rm5(cwd, { recursive: true, force: true });
|
|
4025
|
+
}
|
|
4026
|
+
if (run2.signal.reason === CLOSED_ON_SITE) {
|
|
4027
|
+
return;
|
|
4028
|
+
}
|
|
4029
|
+
try {
|
|
4030
|
+
await tools.api.request("POST", sensePath(job.senseId, "finish"), run2.signal.aborted ? FAILED : body);
|
|
4031
|
+
if (body.outcome === "succeeded" && !run2.signal.aborted) {
|
|
4032
|
+
tools.print(`Sense answered on task #${job.taskId}`);
|
|
4033
|
+
}
|
|
4034
|
+
} catch {
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
var runClaudeSense = claudeSenseRunner(query4);
|
|
4038
|
+
|
|
3726
4039
|
// src/update.ts
|
|
3727
4040
|
import { spawn as spawn4 } from "child_process";
|
|
3728
4041
|
import { readFileSync as readFileSync2 } from "fs";
|
|
3729
|
-
import
|
|
4042
|
+
import process6 from "process";
|
|
3730
4043
|
var PACKAGE_NAME = "@tamsensedev/runner";
|
|
3731
4044
|
var RELAUNCHED_ENV = "TAMSENSE_RELAUNCHED";
|
|
3732
4045
|
var NO_UPDATE_ENV = "TAMSENSE_NO_UPDATE";
|
|
@@ -3778,27 +4091,27 @@ function relaunchArgs(argv) {
|
|
|
3778
4091
|
return ["--yes", `${PACKAGE_NAME}@latest`, ...argv];
|
|
3779
4092
|
}
|
|
3780
4093
|
function relaunch(argv, env) {
|
|
3781
|
-
const isWindows =
|
|
4094
|
+
const isWindows = process6.platform === "win32";
|
|
3782
4095
|
return new Promise((resolve5, reject) => {
|
|
3783
4096
|
const child = spawn4(isWindows ? "npx.cmd" : "npx", relaunchArgs(argv), { stdio: "inherit", shell: isWindows, env: { ...env, [RELAUNCHED_ENV]: "1" } });
|
|
3784
4097
|
const ignoreInterrupt = () => {
|
|
3785
4098
|
};
|
|
3786
|
-
|
|
4099
|
+
process6.on("SIGINT", ignoreInterrupt);
|
|
3787
4100
|
child.on("error", (error) => {
|
|
3788
|
-
|
|
4101
|
+
process6.off("SIGINT", ignoreInterrupt);
|
|
3789
4102
|
reject(error);
|
|
3790
4103
|
});
|
|
3791
4104
|
child.on("close", (code) => {
|
|
3792
|
-
|
|
4105
|
+
process6.off("SIGINT", ignoreInterrupt);
|
|
3793
4106
|
resolve5(code ?? 1);
|
|
3794
4107
|
});
|
|
3795
4108
|
});
|
|
3796
4109
|
}
|
|
3797
4110
|
|
|
3798
4111
|
// src/worker.ts
|
|
3799
|
-
import { rm as
|
|
3800
|
-
import { dirname as
|
|
3801
|
-
import
|
|
4112
|
+
import { rm as rm7 } from "fs/promises";
|
|
4113
|
+
import { dirname as dirname6, join as join12 } from "path";
|
|
4114
|
+
import process7 from "process";
|
|
3802
4115
|
|
|
3803
4116
|
// src/feedQueue.ts
|
|
3804
4117
|
var MAX_BATCH = 200;
|
|
@@ -3845,8 +4158,8 @@ function feedQueue(send) {
|
|
|
3845
4158
|
|
|
3846
4159
|
// src/library.ts
|
|
3847
4160
|
import { existsSync as existsSync7 } from "fs";
|
|
3848
|
-
import { cp, mkdir as mkdir5, rm as
|
|
3849
|
-
import { join as
|
|
4161
|
+
import { cp, mkdir as mkdir5, rm as rm6 } from "fs/promises";
|
|
4162
|
+
import { join as join10 } from "path";
|
|
3850
4163
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3851
4164
|
var LIBRARY_PATH = fileURLToPath2(new URL("../library", import.meta.url));
|
|
3852
4165
|
var SKILL_NAME = /^[a-z0-9-]+$/;
|
|
@@ -3859,18 +4172,18 @@ function allowedSkills(stage) {
|
|
|
3859
4172
|
return skills && [.../* @__PURE__ */ new Set([...skills, ...STAGE_SKILLS[stage.kind] ?? []])].filter((name) => SKILL_NAME.test(name));
|
|
3860
4173
|
}
|
|
3861
4174
|
function roleLibraryFolder(home, runId) {
|
|
3862
|
-
return
|
|
4175
|
+
return join10(home, ".tamsense", "libraries", `run-${runId}`);
|
|
3863
4176
|
}
|
|
3864
4177
|
async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
|
|
3865
4178
|
if (skills === null) {
|
|
3866
4179
|
return source;
|
|
3867
4180
|
}
|
|
3868
|
-
await
|
|
3869
|
-
await mkdir5(
|
|
3870
|
-
await cp(
|
|
3871
|
-
await cp(
|
|
3872
|
-
for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync7(
|
|
3873
|
-
await cp(
|
|
4181
|
+
await rm6(folder, { recursive: true, force: true });
|
|
4182
|
+
await mkdir5(join10(folder, "skills"), { recursive: true });
|
|
4183
|
+
await cp(join10(source, ".claude-plugin"), join10(folder, ".claude-plugin"), { recursive: true });
|
|
4184
|
+
await cp(join10(source, "agents"), join10(folder, "agents"), { recursive: true });
|
|
4185
|
+
for (const skill of skills.filter((name) => SKILL_NAME.test(name) && existsSync7(join10(source, "skills", name)))) {
|
|
4186
|
+
await cp(join10(source, "skills", skill), join10(folder, "skills", skill), { recursive: true });
|
|
3874
4187
|
}
|
|
3875
4188
|
return folder;
|
|
3876
4189
|
}
|
|
@@ -3879,7 +4192,7 @@ async function prepareLibrary(folder, skills, source = LIBRARY_PATH) {
|
|
|
3879
4192
|
import { randomUUID } from "crypto";
|
|
3880
4193
|
import { createReadStream } from "fs";
|
|
3881
4194
|
import { readdir as readdir2, readFile as readFile4, stat as stat2, writeFile as writeFile4 } from "fs/promises";
|
|
3882
|
-
import { basename as basename4, dirname as
|
|
4195
|
+
import { basename as basename4, dirname as dirname5, extname as extname2, isAbsolute as isAbsolute3, join as join11 } from "path";
|
|
3883
4196
|
import { createInterface as createInterface2 } from "readline";
|
|
3884
4197
|
import { Readable } from "stream";
|
|
3885
4198
|
import { promisify } from "util";
|
|
@@ -3959,12 +4272,12 @@ async function readMeta(file) {
|
|
|
3959
4272
|
}
|
|
3960
4273
|
async function metaFor(kind, file) {
|
|
3961
4274
|
if (kind === "replay") {
|
|
3962
|
-
return readMeta(
|
|
4275
|
+
return readMeta(join11(dirname5(file), `${sessionIdOf(file)}${META_SUFFIX}`));
|
|
3963
4276
|
}
|
|
3964
|
-
const folder = basename4(
|
|
4277
|
+
const folder = basename4(dirname5(file)) === "video" ? dirname5(dirname5(file)) : dirname5(file);
|
|
3965
4278
|
for (const name of await readdir2(folder).catch(() => [])) {
|
|
3966
4279
|
if (name.endsWith(META_SUFFIX) && name !== LEDGER_FILE) {
|
|
3967
|
-
const meta = await readMeta(
|
|
4280
|
+
const meta = await readMeta(join11(folder, name));
|
|
3968
4281
|
if (meta?.video === file) {
|
|
3969
4282
|
return meta;
|
|
3970
4283
|
}
|
|
@@ -3974,7 +4287,7 @@ async function metaFor(kind, file) {
|
|
|
3974
4287
|
}
|
|
3975
4288
|
async function ledger(dir) {
|
|
3976
4289
|
try {
|
|
3977
|
-
return new Set(JSON.parse(await readFile4(
|
|
4290
|
+
return new Set(JSON.parse(await readFile4(join11(dir, LEDGER_FILE), "utf8")));
|
|
3978
4291
|
} catch {
|
|
3979
4292
|
return /* @__PURE__ */ new Set();
|
|
3980
4293
|
}
|
|
@@ -3982,7 +4295,7 @@ async function ledger(dir) {
|
|
|
3982
4295
|
async function remember(dir, file) {
|
|
3983
4296
|
const done2 = await ledger(dir);
|
|
3984
4297
|
done2.add(file);
|
|
3985
|
-
await writeFile4(
|
|
4298
|
+
await writeFile4(join11(dir, LEDGER_FILE), JSON.stringify([...done2]));
|
|
3986
4299
|
}
|
|
3987
4300
|
async function nonEmpty(file) {
|
|
3988
4301
|
return file !== null && ((await stat2(file).catch(() => null))?.size ?? 0) > 0;
|
|
@@ -3995,8 +4308,8 @@ async function pendingRecordings(dir) {
|
|
|
3995
4308
|
if (!name.endsWith(META_SUFFIX) || name === LEDGER_FILE) {
|
|
3996
4309
|
continue;
|
|
3997
4310
|
}
|
|
3998
|
-
const meta = await readMeta(
|
|
3999
|
-
const replay =
|
|
4311
|
+
const meta = await readMeta(join11(dir, name));
|
|
4312
|
+
const replay = join11(dir, `${name.slice(0, -META_SUFFIX.length)}${REPLAY_SUFFIX}`);
|
|
4000
4313
|
if (!done2.has(replay) && await nonEmpty(replay)) {
|
|
4001
4314
|
pending.push({ kind: "replay", file: replay, meta });
|
|
4002
4315
|
}
|
|
@@ -4143,7 +4456,7 @@ async function readScreenshot(request) {
|
|
|
4143
4456
|
}
|
|
4144
4457
|
|
|
4145
4458
|
// src/worker.ts
|
|
4146
|
-
var POLL_INTERVAL_MS =
|
|
4459
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
4147
4460
|
var MAX_RETRY_WAIT_MS = 6e4;
|
|
4148
4461
|
var POLL_DEADLINE_MS = 3 * 6e4;
|
|
4149
4462
|
var INBOX_INTERVAL_MS = 2e3;
|
|
@@ -4154,8 +4467,10 @@ var FATAL_STATUSES = /* @__PURE__ */ new Set([401, 403]);
|
|
|
4154
4467
|
var MAX_CHECKS = 2;
|
|
4155
4468
|
var CHECK_HEARTBEAT_MS = 3e4;
|
|
4156
4469
|
var CHECK_DEADLINE_MS = 10 * 6e4;
|
|
4157
|
-
var
|
|
4158
|
-
var
|
|
4470
|
+
var ROUTINE_DEADLINE_MS = 30 * 6e4;
|
|
4471
|
+
var MAX_SENSE = 2;
|
|
4472
|
+
var STOPPED_HERE2 = "stopped_here";
|
|
4473
|
+
var CLOSED_ON_SITE2 = "closed_on_site";
|
|
4159
4474
|
function messageOf3(error) {
|
|
4160
4475
|
return error instanceof Error ? error.message : String(error);
|
|
4161
4476
|
}
|
|
@@ -4228,9 +4543,9 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
4228
4543
|
async function check() {
|
|
4229
4544
|
const ack = input.takeDelivered();
|
|
4230
4545
|
const body = { ack };
|
|
4231
|
-
let
|
|
4546
|
+
let reply5;
|
|
4232
4547
|
try {
|
|
4233
|
-
|
|
4548
|
+
reply5 = await tools.api.request("POST", runPath(job.runId, "inbox"), body, { safeToRepeat: true });
|
|
4234
4549
|
} catch (error) {
|
|
4235
4550
|
input.restoreDelivered(ack);
|
|
4236
4551
|
if (!failing && !alreadyReported(error)) {
|
|
@@ -4240,23 +4555,23 @@ function inbox(job, tools, record, closedOnSite) {
|
|
|
4240
4555
|
return;
|
|
4241
4556
|
}
|
|
4242
4557
|
failing = false;
|
|
4243
|
-
if (!
|
|
4558
|
+
if (!reply5.keepGoing) {
|
|
4244
4559
|
closedOnSite();
|
|
4245
4560
|
return;
|
|
4246
4561
|
}
|
|
4247
|
-
for (const message of
|
|
4562
|
+
for (const message of reply5.messages) {
|
|
4248
4563
|
if (input.push(message)) {
|
|
4249
4564
|
tools.print(` ${messageLine(message)}`);
|
|
4250
4565
|
}
|
|
4251
4566
|
}
|
|
4252
|
-
for (const stop of
|
|
4567
|
+
for (const stop of reply5.helperStops ?? []) {
|
|
4253
4568
|
if (!stopsSeen.has(stop.ref)) {
|
|
4254
4569
|
stopsSeen.add(stop.ref);
|
|
4255
4570
|
void stopHelper(input, stop, tools.print);
|
|
4256
4571
|
}
|
|
4257
4572
|
}
|
|
4258
|
-
if (
|
|
4259
|
-
const change = { model:
|
|
4573
|
+
if (reply5.model !== null || reply5.effort !== null) {
|
|
4574
|
+
const change = { model: reply5.model, effort: reply5.effort };
|
|
4260
4575
|
void input.switchModel(change).then(
|
|
4261
4576
|
(applied) => {
|
|
4262
4577
|
if (applied) {
|
|
@@ -4311,11 +4626,11 @@ function permissionTools(job, tools) {
|
|
|
4311
4626
|
}
|
|
4312
4627
|
return {
|
|
4313
4628
|
send: async (body) => {
|
|
4314
|
-
const
|
|
4315
|
-
if (
|
|
4629
|
+
const reply5 = await tools.api.request("POST", runPath(job.runId, "permissions"), body);
|
|
4630
|
+
if (reply5.decision === "wait") {
|
|
4316
4631
|
tools.print(` Waiting for a person on TAMsense to allow: ${body.title || body.action}`);
|
|
4317
4632
|
}
|
|
4318
|
-
return
|
|
4633
|
+
return reply5;
|
|
4319
4634
|
},
|
|
4320
4635
|
check: (id) => tools.api.request("GET", permissionPath(job.runId, id)),
|
|
4321
4636
|
sleep: tools.sleep
|
|
@@ -4343,14 +4658,15 @@ async function prepare(job, tools) {
|
|
|
4343
4658
|
const { results, ...workspace2 } = await prepareFolder(job, taskFolder(tools.home, job), tools.api);
|
|
4344
4659
|
return { workspace: workspace2, results, identity: {} };
|
|
4345
4660
|
}
|
|
4661
|
+
const taskResults = (workspace2) => writeResults(join12(workspace2.root, RESULTS_FOLDER), job, tools.api);
|
|
4346
4662
|
if (isLocalJob(job)) {
|
|
4347
4663
|
tools.print("Preparing the code from the folders on this computer\u2026");
|
|
4348
4664
|
const workspace2 = await prepareLocalWorkspace(job, tools);
|
|
4349
|
-
return { workspace: workspace2, results:
|
|
4665
|
+
return { workspace: workspace2, results: await taskResults(workspace2), identity: await commitIdentityEnv(job.repositories[0]?.folder ?? tools.home) };
|
|
4350
4666
|
}
|
|
4351
4667
|
tools.print("Preparing the code\u2026");
|
|
4352
4668
|
const workspace = await prepareWorkspace(job, await gitToken(tools.api, job), tools);
|
|
4353
|
-
return { workspace, results:
|
|
4669
|
+
return { workspace, results: await taskResults(workspace), identity: await commitIdentityEnv(tools.home) };
|
|
4354
4670
|
}
|
|
4355
4671
|
async function publish(workspace, job, api, identity) {
|
|
4356
4672
|
return isLocalJob(job) ? await publishLocalWorkspace(workspace, job, identity) : await publishWorkspace(workspace, job, await gitToken(api, job), identity);
|
|
@@ -4368,7 +4684,7 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
4368
4684
|
const attached = { count: 0 };
|
|
4369
4685
|
const session = sessionReporter(tools.api, job, tools.print);
|
|
4370
4686
|
const recordings = recordingsFolder(tools.home, job.runId);
|
|
4371
|
-
const baseEnv = withNodeOnPath(
|
|
4687
|
+
const baseEnv = withNodeOnPath(process7.env, dirname6(process7.execPath));
|
|
4372
4688
|
const recording = tools.recorder ? recorderEnv({ recorder: tools.recorder, folder: recordings, server: tools.api.server, job, nodeOptions: baseEnv.NODE_OPTIONS }) : {};
|
|
4373
4689
|
const outcome = await tools.runAgent({
|
|
4374
4690
|
cwd: workspace.root,
|
|
@@ -4391,7 +4707,8 @@ async function executeJob(job, tools, signal, input, record) {
|
|
|
4391
4707
|
attach: (request) => request.kind === "screenshot" ? attachScreenshot(tools.api, job, request, attached) : attachRecording(tools.api, job.runId, recordings, request),
|
|
4392
4708
|
results: resultTools(job, tools.api, workspace.root),
|
|
4393
4709
|
handOver: handOverTools(job, tools.api),
|
|
4394
|
-
schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules")) : null
|
|
4710
|
+
schedules: job.canSchedule ? scheduleTools(tools.api, runPath(job.runId, "schedules"), runPath(job.runId, "wake")) : null,
|
|
4711
|
+
sources: sourceTools(job.sourceTools, tools.api, runPath(job.runId, "sources"))
|
|
4395
4712
|
});
|
|
4396
4713
|
await session.sent();
|
|
4397
4714
|
signal.throwIfAborted();
|
|
@@ -4466,16 +4783,16 @@ async function sendRecordings(job, tools) {
|
|
|
4466
4783
|
tools.print(`Could not attach a recording: ${line}`);
|
|
4467
4784
|
}
|
|
4468
4785
|
} finally {
|
|
4469
|
-
await
|
|
4786
|
+
await rm7(folder, { recursive: true, force: true });
|
|
4470
4787
|
}
|
|
4471
4788
|
}
|
|
4472
4789
|
function failureReport(error, signal) {
|
|
4473
|
-
return signal.reason ===
|
|
4790
|
+
return signal.reason === STOPPED_HERE2 ? "The run was stopped on the computer." : `The run stopped on the computer: ${messageOf3(error)}`;
|
|
4474
4791
|
}
|
|
4475
4792
|
async function runJob(job, tools, stop) {
|
|
4476
4793
|
tools.print(`Task #${job.taskId}: ${job.title} (${job.stage.name})`);
|
|
4477
4794
|
const run2 = new AbortController();
|
|
4478
|
-
const stopRun = () => run2.abort(
|
|
4795
|
+
const stopRun = () => run2.abort(STOPPED_HERE2);
|
|
4479
4796
|
if (stop.aborted) {
|
|
4480
4797
|
stopRun();
|
|
4481
4798
|
}
|
|
@@ -4483,7 +4800,7 @@ async function runJob(job, tools, stop) {
|
|
|
4483
4800
|
const feed = feedQueue((entries) => tools.api.request("POST", runPath(job.runId, "feed"), { entries }, { safeToRepeat: true }));
|
|
4484
4801
|
const feeding = setInterval(() => void feed.flush(), FEED_INTERVAL_MS);
|
|
4485
4802
|
const record = journal(feed, tools.print);
|
|
4486
|
-
const messages = inbox(job, tools, record, () => run2.abort(
|
|
4803
|
+
const messages = inbox(job, tools, record, () => run2.abort(CLOSED_ON_SITE2));
|
|
4487
4804
|
const { input } = messages;
|
|
4488
4805
|
run2.signal.addEventListener("abort", input.close, { once: true });
|
|
4489
4806
|
void messages.poll();
|
|
@@ -4494,7 +4811,7 @@ async function runJob(job, tools, stop) {
|
|
|
4494
4811
|
} catch (error) {
|
|
4495
4812
|
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 };
|
|
4496
4813
|
} finally {
|
|
4497
|
-
await
|
|
4814
|
+
await rm7(roleLibraryFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
4498
4815
|
clearInterval(polling);
|
|
4499
4816
|
clearInterval(feeding);
|
|
4500
4817
|
stop.removeEventListener("abort", stopRun);
|
|
@@ -4505,8 +4822,8 @@ async function runJob(job, tools, stop) {
|
|
|
4505
4822
|
await messages.poll();
|
|
4506
4823
|
}
|
|
4507
4824
|
await feed.flush();
|
|
4508
|
-
if (run2.signal.reason ===
|
|
4509
|
-
await
|
|
4825
|
+
if (run2.signal.reason === CLOSED_ON_SITE2) {
|
|
4826
|
+
await rm7(recordingsFolder(tools.home, job.runId), { recursive: true, force: true });
|
|
4510
4827
|
tools.print("TAMsense closed this run while it was in progress. The result was not sent.");
|
|
4511
4828
|
return;
|
|
4512
4829
|
}
|
|
@@ -4519,33 +4836,36 @@ async function runJob(job, tools, stop) {
|
|
|
4519
4836
|
}
|
|
4520
4837
|
}
|
|
4521
4838
|
var CHECK_TOO_LONG = "check_too_long";
|
|
4522
|
-
function checkFailure(error, signal) {
|
|
4523
|
-
return signal.reason === CHECK_TOO_LONG ? `The
|
|
4839
|
+
function checkFailure(error, signal, deadlineMs) {
|
|
4840
|
+
return signal.reason === CHECK_TOO_LONG ? `The run took longer than ${deadlineMs / 6e4} minutes.` : failureReport(error, signal);
|
|
4524
4841
|
}
|
|
4525
4842
|
async function runCheckJob(check, runCheck, tools, stop) {
|
|
4526
|
-
|
|
4843
|
+
const routine = routineRunTools(check, tools.api, tools.print, tools.sleep);
|
|
4844
|
+
tools.print(routine ? `Routine: ${check.title}${check.routine?.dry ? " (test run)" : ""}` : `Schedule check: ${check.title}${check.taskId === null ? "" : ` (task #${check.taskId})`}`);
|
|
4845
|
+
const deadlineMs = routine ? ROUTINE_DEADLINE_MS : CHECK_DEADLINE_MS;
|
|
4527
4846
|
const run2 = new AbortController();
|
|
4528
|
-
const stopRun = () => run2.abort(
|
|
4847
|
+
const stopRun = () => run2.abort(STOPPED_HERE2);
|
|
4529
4848
|
if (stop.aborted) {
|
|
4530
4849
|
stopRun();
|
|
4531
4850
|
}
|
|
4532
4851
|
stop.addEventListener("abort", stopRun, { once: true });
|
|
4533
|
-
const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG),
|
|
4534
|
-
const beat = checkHeartbeat(tools.api, check, () => run2.abort(
|
|
4852
|
+
const deadline = setTimeout(() => run2.abort(CHECK_TOO_LONG), deadlineMs);
|
|
4853
|
+
const beat = checkHeartbeat(tools.api, check, () => run2.abort(CLOSED_ON_SITE2));
|
|
4535
4854
|
const beating = setInterval(() => void beat(), CHECK_HEARTBEAT_MS);
|
|
4536
4855
|
let body;
|
|
4537
4856
|
try {
|
|
4538
4857
|
const folder = await checkFolder(tools.home, check);
|
|
4539
|
-
const env = withNodeOnPath(
|
|
4540
|
-
|
|
4858
|
+
const env = withNodeOnPath(process7.env, dirname6(process7.execPath));
|
|
4859
|
+
const prompt = routine ? buildRoutinePrompt(check, folder) : buildCheckPrompt(check, folder);
|
|
4860
|
+
body = checkFinishBody(await runCheck({ cwd: folder, prompt, model: check.model, env, signal: run2.signal, sources: sourceTools(check.sourceTools, tools.api, checkPath(check.checkId, "sources")), routine }));
|
|
4541
4861
|
} catch (error) {
|
|
4542
|
-
body = { outcome: "failed", report: checkFailure(error, run2.signal), model: null, usage: null, result: null };
|
|
4862
|
+
body = { outcome: "failed", report: checkFailure(error, run2.signal, deadlineMs), model: null, usage: null, result: null };
|
|
4543
4863
|
} finally {
|
|
4544
4864
|
clearTimeout(deadline);
|
|
4545
4865
|
clearInterval(beating);
|
|
4546
4866
|
stop.removeEventListener("abort", stopRun);
|
|
4547
4867
|
}
|
|
4548
|
-
if (run2.signal.reason ===
|
|
4868
|
+
if (run2.signal.reason === CLOSED_ON_SITE2) {
|
|
4549
4869
|
tools.print("TAMsense closed this check while it was in progress. The result was not sent.");
|
|
4550
4870
|
return;
|
|
4551
4871
|
}
|
|
@@ -4606,9 +4926,11 @@ async function work(tools, stop, parallel = 1) {
|
|
|
4606
4926
|
const stopAll = AbortSignal.any([stop, halt.signal]);
|
|
4607
4927
|
const running = /* @__PURE__ */ new Set();
|
|
4608
4928
|
const checks = /* @__PURE__ */ new Set();
|
|
4929
|
+
const senses = /* @__PURE__ */ new Set();
|
|
4609
4930
|
const folderLock = keyedLock();
|
|
4610
4931
|
const scheduleLock = keyedLock();
|
|
4611
4932
|
const runCheck = tools.version ? tools.runCheck : void 0;
|
|
4933
|
+
const runSense = tools.version ? tools.runSense : void 0;
|
|
4612
4934
|
const reporting = reportCapabilities(tools, stopAll);
|
|
4613
4935
|
const checking = tools.inspectFolder ? checkFolders({ ...tools, inspect: tools.inspectFolder }, stopAll) : Promise.resolve();
|
|
4614
4936
|
if (tools.startFolder) {
|
|
@@ -4623,19 +4945,26 @@ async function work(tools, stop, parallel = 1) {
|
|
|
4623
4945
|
const task = scheduleLock.run(`schedule-${check.scheduleId}`, () => runCheckJob(check, runner, tools, stopAll)).catch((error) => tools.print(`The check stopped: ${messageOf3(error)}`)).finally(() => checks.delete(task));
|
|
4624
4946
|
checks.add(task);
|
|
4625
4947
|
};
|
|
4948
|
+
const startSense = (job, runner) => {
|
|
4949
|
+
const task = runSenseJob(job, runner, tools, stopAll).catch(() => {
|
|
4950
|
+
}).finally(() => senses.delete(task));
|
|
4951
|
+
senses.add(task);
|
|
4952
|
+
};
|
|
4626
4953
|
let failures = 0;
|
|
4627
4954
|
try {
|
|
4628
4955
|
while (!stopAll.aborted) {
|
|
4629
4956
|
const jobsFull = running.size >= parallel;
|
|
4630
4957
|
const checksFull = !runCheck || checks.size >= MAX_CHECKS;
|
|
4631
|
-
|
|
4632
|
-
|
|
4958
|
+
const senseFull = !runSense || senses.size >= MAX_SENSE;
|
|
4959
|
+
if (jobsFull && checksFull && senseFull) {
|
|
4960
|
+
await Promise.race([...running, ...checks, ...senses]);
|
|
4633
4961
|
continue;
|
|
4634
4962
|
}
|
|
4635
|
-
const
|
|
4636
|
-
|
|
4963
|
+
const named = tools.version && (runCheck || runSense) ? tools.version : null;
|
|
4964
|
+
const body = named ? { version: named, jobs: !jobsFull, checks: !checksFull, ...runSense ? { sense: !senseFull } : {} } : void 0;
|
|
4965
|
+
let reply5;
|
|
4637
4966
|
try {
|
|
4638
|
-
|
|
4967
|
+
reply5 = await withDeadline(tools.api.request("POST", NEXT_RUN_PATH, body, { safeToRepeat: true }), POLL_DEADLINE_MS, tools.sleep);
|
|
4639
4968
|
} catch (error) {
|
|
4640
4969
|
if (isFatal(error)) {
|
|
4641
4970
|
halt.abort();
|
|
@@ -4649,16 +4978,18 @@ async function work(tools, stop, parallel = 1) {
|
|
|
4649
4978
|
continue;
|
|
4650
4979
|
}
|
|
4651
4980
|
failures = 0;
|
|
4652
|
-
if (
|
|
4653
|
-
|
|
4654
|
-
} else if (
|
|
4655
|
-
|
|
4981
|
+
if (reply5.sense && runSense) {
|
|
4982
|
+
startSense(reply5.sense, runSense);
|
|
4983
|
+
} else if (reply5.check && runCheck) {
|
|
4984
|
+
startCheck(reply5.check, runCheck);
|
|
4985
|
+
} else if (reply5.job) {
|
|
4986
|
+
start(reply5.job);
|
|
4656
4987
|
} else {
|
|
4657
4988
|
await tools.sleep(POLL_INTERVAL_MS, stopAll);
|
|
4658
4989
|
}
|
|
4659
4990
|
}
|
|
4660
4991
|
} finally {
|
|
4661
|
-
await Promise.allSettled([...running, ...checks, reporting, checking]);
|
|
4992
|
+
await Promise.allSettled([...running, ...checks, ...senses, reporting, checking]);
|
|
4662
4993
|
}
|
|
4663
4994
|
}
|
|
4664
4995
|
|
|
@@ -4669,11 +5000,11 @@ var NO_TASKS_ACCESS_TEXT = "Your account no longer has access to Tasks. Ask the
|
|
|
4669
5000
|
var ACCESS_PROBLEMS = { 401: DISCONNECTED_TEXT, 403: NO_TASKS_ACCESS_TEXT };
|
|
4670
5001
|
var INTERRUPTED_EXIT_CODE = 130;
|
|
4671
5002
|
function print(line) {
|
|
4672
|
-
|
|
5003
|
+
process8.stdout.write(`${line}
|
|
4673
5004
|
`);
|
|
4674
5005
|
}
|
|
4675
5006
|
function serverFor(flag, saved) {
|
|
4676
|
-
return serverOrigin(flag ??
|
|
5007
|
+
return serverOrigin(flag ?? process8.env.TAMSENSE_URL ?? saved?.server ?? DEFAULT_SERVER);
|
|
4677
5008
|
}
|
|
4678
5009
|
function accessProblem(error) {
|
|
4679
5010
|
return error instanceof ApiError ? ACCESS_PROBLEMS[error.status] ?? null : null;
|
|
@@ -4699,15 +5030,15 @@ async function runLogin(server, deviceName) {
|
|
|
4699
5030
|
}
|
|
4700
5031
|
function stopOnInterrupt() {
|
|
4701
5032
|
const stop = new AbortController();
|
|
4702
|
-
|
|
5033
|
+
process8.once("SIGINT", () => {
|
|
4703
5034
|
print("Stopping\u2026 Press Ctrl+C again to quit at once.");
|
|
4704
5035
|
stop.abort();
|
|
4705
|
-
|
|
5036
|
+
process8.once("SIGINT", () => process8.exit(INTERRUPTED_EXIT_CODE));
|
|
4706
5037
|
});
|
|
4707
5038
|
return stop.signal;
|
|
4708
5039
|
}
|
|
4709
5040
|
function installTools(home) {
|
|
4710
|
-
return { home, nodePath:
|
|
5041
|
+
return { home, nodePath: process8.execPath, env: process8.env, print, run: runProcess, exists: existsSync8 };
|
|
4711
5042
|
}
|
|
4712
5043
|
async function browserForStart(home, install) {
|
|
4713
5044
|
const installed = await installedBrowser(home);
|
|
@@ -4717,13 +5048,13 @@ async function browserForStart(home, install) {
|
|
|
4717
5048
|
return ensureBrowser({ print, install: () => installBrowser(installTools(home)) });
|
|
4718
5049
|
}
|
|
4719
5050
|
async function startedNewVersion() {
|
|
4720
|
-
const update = await updateToRun(
|
|
5051
|
+
const update = await updateToRun(process8.env, currentVersion(), () => latestVersion());
|
|
4721
5052
|
if (!update) {
|
|
4722
5053
|
return false;
|
|
4723
5054
|
}
|
|
4724
5055
|
print(`TAMsense runner ${update.latest} is out, this is ${update.current}. Starting the new version...`);
|
|
4725
5056
|
try {
|
|
4726
|
-
|
|
5057
|
+
process8.exitCode = await relaunch(process8.argv.slice(2), process8.env);
|
|
4727
5058
|
return true;
|
|
4728
5059
|
} catch {
|
|
4729
5060
|
print("Could not start the new version. Going on with this one.");
|
|
@@ -4740,7 +5071,7 @@ async function runStart(parallel, installsBrowser) {
|
|
|
4740
5071
|
return;
|
|
4741
5072
|
}
|
|
4742
5073
|
const identity = await createApi(saved.server, saved.token).request("GET", IDENTITY_PATH);
|
|
4743
|
-
const problem2 = teamKeyProblem(identity.device.team,
|
|
5074
|
+
const problem2 = teamKeyProblem(identity.device.team, process8.env);
|
|
4744
5075
|
if (problem2) {
|
|
4745
5076
|
throw new Error(problem2);
|
|
4746
5077
|
}
|
|
@@ -4748,7 +5079,7 @@ async function runStart(parallel, installsBrowser) {
|
|
|
4748
5079
|
const home = homedir3();
|
|
4749
5080
|
const browser = await browserForStart(home, installsBrowser);
|
|
4750
5081
|
const api = createApi(saved.server, saved.token, { watch: connectionWatch(print) });
|
|
4751
|
-
await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder:
|
|
5082
|
+
await work({ api, runAgent: runClaude, runCheck: runClaudeCheck, runSense: runClaudeSense, version: currentVersion() ?? void 0, capabilities: probeCapabilities, print, sleep, home, remoteUrl: githubRemoteUrl, browser, recorder: bundledRecorder(), inspectFolder: (path) => inspectFolder(path, home), startFolder: process8.cwd() }, stopOnInterrupt(), parallel);
|
|
4752
5083
|
}
|
|
4753
5084
|
async function runBrowser(remove) {
|
|
4754
5085
|
const home = homedir3();
|
|
@@ -4800,10 +5131,10 @@ async function run(command) {
|
|
|
4800
5131
|
}
|
|
4801
5132
|
}
|
|
4802
5133
|
try {
|
|
4803
|
-
await run(parseCommand(
|
|
5134
|
+
await run(parseCommand(process8.argv.slice(2), process8.env));
|
|
4804
5135
|
} catch (error) {
|
|
4805
|
-
|
|
5136
|
+
process8.stderr.write(`${accessProblem(error) ?? (error instanceof Error ? error.message : String(error))}
|
|
4806
5137
|
`);
|
|
4807
|
-
|
|
5138
|
+
process8.exitCode = 1;
|
|
4808
5139
|
}
|
|
4809
5140
|
//# sourceMappingURL=cli.js.map
|