toolroll 0.9.16 → 0.9.17
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/CHANGELOG.md +17 -0
- package/dist/assignment-brief.js +11 -4
- package/dist/chat-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.js +29 -15
- package/dist/chat-channel.js +3 -1
- package/dist/chat-delivery-state.d.ts +2 -0
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +31 -16
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/discord-api.d.ts +1 -1
- package/dist/discord-api.js +1 -1
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +18 -3
- package/dist/lead-about.d.ts +35 -0
- package/dist/lead-about.js +62 -0
- package/dist/lead-capabilities.d.ts +64 -0
- package/dist/lead-capabilities.js +199 -0
- package/dist/lead-commitments.js +13 -7
- package/dist/lead-context.d.ts +5 -3
- package/dist/lead-context.js +29 -7
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +3 -2
- package/dist/lead-identity.js +1 -1
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +3 -2
- package/dist/lead-voice.js +4 -3
- package/dist/mate-cli.d.ts +3 -0
- package/dist/mate-cli.js +15 -14
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +6 -4
- package/dist/mate-progress.js +2 -0
- package/dist/mate-tools.d.ts +8 -0
- package/dist/mate-tools.js +112 -6
- package/dist/mate.d.ts +26 -1
- package/dist/mate.js +89 -25
- package/dist/operate.js +2 -0
- package/dist/phase-routing.d.ts +1 -1
- package/dist/provider-auth.d.ts +4 -0
- package/dist/provider-auth.js +1 -1
- package/dist/reply-shape.d.ts +96 -0
- package/dist/reply-shape.js +452 -0
- package/dist/serve.js +53 -28
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +20 -3
- package/dist/store.d.ts +12 -3
- package/dist/store.js +44 -9
- package/dist/team-runtime.js +1 -1
- package/dist/teams-chat.js +12 -1
- package/dist/telegram-mate.d.ts +2 -0
- package/dist/telegram-mate.js +27 -11
- package/dist/telegram-team.js +6 -3
- package/dist/telegram.js +1 -1
- package/dist/test-temp.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { scanForSecrets } from "./evidence.js";
|
|
2
|
+
export const ABOUT_YOU_MAX_LINES = 20;
|
|
3
|
+
/** Each line is under 200 characters. */
|
|
4
|
+
export const ABOUT_YOU_LINE_MAX = 199;
|
|
5
|
+
/** This person's confirmed lines; an older store reads as none. */
|
|
6
|
+
export function aboutYouOf(store, account) {
|
|
7
|
+
if (account == null || account === "")
|
|
8
|
+
return [];
|
|
9
|
+
try {
|
|
10
|
+
return store.leadAbout(account);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** One line as it is saved, or the problem with it. */
|
|
17
|
+
export function checkAboutYouLine(value) {
|
|
18
|
+
const line = typeof value === "string" ? value.replace(/^\s*[-*•]\s*/, "").replace(/\s+/g, " ").trim() : "";
|
|
19
|
+
if (line.length < 2)
|
|
20
|
+
return { ok: false, message: "Say it in a few words." };
|
|
21
|
+
if (line.length > ABOUT_YOU_LINE_MAX)
|
|
22
|
+
return { ok: false, message: `Keep each line under ${ABOUT_YOU_LINE_MAX + 1} characters.` };
|
|
23
|
+
if (/[\u0000-\u001f\u007f---�]/.test(line) || scanForSecrets(line).length > 0)
|
|
24
|
+
return { ok: false, message: "Use plain words, with no passwords or keys." };
|
|
25
|
+
return { ok: true, line };
|
|
26
|
+
}
|
|
27
|
+
/** The Settings form's text, one line each: the saved list, or the problem with it. Blank lines and repeats go. */
|
|
28
|
+
export function checkAboutYou(text) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
// A problem names the line as the person sees it in the box, blank lines and repeats counted.
|
|
31
|
+
for (const [index, raw] of text.replace(/\r\n?/g, "\n").split("\n").entries()) {
|
|
32
|
+
if (raw.trim() === "")
|
|
33
|
+
continue;
|
|
34
|
+
const checked = checkAboutYouLine(raw);
|
|
35
|
+
if (!checked.ok)
|
|
36
|
+
return { ok: false, message: `Line ${index + 1}: ${checked.message}` };
|
|
37
|
+
if (!lines.some(one => one.toLowerCase() === checked.line.toLowerCase()))
|
|
38
|
+
lines.push(checked.line);
|
|
39
|
+
}
|
|
40
|
+
if (lines.length > ABOUT_YOU_MAX_LINES)
|
|
41
|
+
return { ok: false, message: `Keep it to ${ABOUT_YOU_MAX_LINES} lines.` };
|
|
42
|
+
return { ok: true, lines };
|
|
43
|
+
}
|
|
44
|
+
/** Save the whole list for this person (already checked). Their lead's name and persona are left as they are. */
|
|
45
|
+
export function saveAboutYou(store, account, lines, now) {
|
|
46
|
+
store.setLeadAbout(account, lines, now);
|
|
47
|
+
}
|
|
48
|
+
/** The list once a confirmed line is added, or once it replaces line `replaces` (1-based). */
|
|
49
|
+
export function withAboutYouLine(lines, line, replaces) {
|
|
50
|
+
if (replaces > 0) {
|
|
51
|
+
if (replaces > lines.length)
|
|
52
|
+
return { ok: false, message: "That line is no longer in what your lead knows about you." };
|
|
53
|
+
const next = [...lines];
|
|
54
|
+
next[replaces - 1] = line;
|
|
55
|
+
return { ok: true, lines: next.filter((one, index) => index === replaces - 1 || one.toLowerCase() !== line.toLowerCase()) };
|
|
56
|
+
}
|
|
57
|
+
if (lines.some(one => one.toLowerCase() === line.toLowerCase()))
|
|
58
|
+
return { ok: false, message: "Your lead already knows this." };
|
|
59
|
+
if (lines.length >= ABOUT_YOU_MAX_LINES)
|
|
60
|
+
return { ok: false, message: `What your lead knows about you is full (${ABOUT_YOU_MAX_LINES} lines). Replace a line, or tidy it in Settings → Lead.` };
|
|
61
|
+
return { ok: true, lines: [...lines, line] };
|
|
62
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lead can rely on right now, in one read (get_capabilities): the agents a project's route would run, the
|
|
3
|
+
* workers that would run them, the project's tools and skills, the integrations, and the project's checks. Every entry
|
|
4
|
+
* says what it lets the lead do and, when it can't, the one next step and the settings control (show_control) that
|
|
5
|
+
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
|
+
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
|
+
*/
|
|
8
|
+
import type { Store } from "./store.js";
|
|
9
|
+
import type { ChatControl } from "./chat-controls.js";
|
|
10
|
+
import type { ProviderId } from "./provider.js";
|
|
11
|
+
import { type AuthMode } from "./keys.js";
|
|
12
|
+
import { type Integration } from "./integrations.js";
|
|
13
|
+
/** One thing the lead may depend on: its state in a few words, what it lets the lead do, and when it can't, the next step. */
|
|
14
|
+
export type Capability = {
|
|
15
|
+
name: string;
|
|
16
|
+
state: string;
|
|
17
|
+
ok: boolean;
|
|
18
|
+
lets: string;
|
|
19
|
+
next: string | null;
|
|
20
|
+
link: ChatControl | null;
|
|
21
|
+
};
|
|
22
|
+
export type ProjectCapabilities = {
|
|
23
|
+
repo: string;
|
|
24
|
+
agents: (Capability & {
|
|
25
|
+
provider: string;
|
|
26
|
+
model: string;
|
|
27
|
+
roles: string[];
|
|
28
|
+
})[];
|
|
29
|
+
agentsProblem: Capability | null;
|
|
30
|
+
tools: Capability[];
|
|
31
|
+
skills: Capability[];
|
|
32
|
+
checks: Capability & {
|
|
33
|
+
level: string;
|
|
34
|
+
quickCheck: boolean;
|
|
35
|
+
releaseCheck: boolean;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export type Capabilities = {
|
|
39
|
+
projects: ProjectCapabilities[];
|
|
40
|
+
workers: (Capability & {
|
|
41
|
+
online: boolean;
|
|
42
|
+
capacity: number;
|
|
43
|
+
running: string[];
|
|
44
|
+
})[];
|
|
45
|
+
integrations: (Capability & {
|
|
46
|
+
kind: string;
|
|
47
|
+
account: string | null;
|
|
48
|
+
usedBy: string[];
|
|
49
|
+
lastError: string | null;
|
|
50
|
+
})[];
|
|
51
|
+
};
|
|
52
|
+
/** The integrations exactly as get_integrations reads them, with what each lets the lead do and the next step. */
|
|
53
|
+
export declare function integrationCapabilities(list: readonly Integration[]): Capabilities["integrations"];
|
|
54
|
+
/** Every capability, for one project or every admitted one. Read-only and spends nothing. */
|
|
55
|
+
export declare function capabilitiesOf(store: Store, input: {
|
|
56
|
+
repos: readonly string[];
|
|
57
|
+
admitted: readonly string[];
|
|
58
|
+
actor: string;
|
|
59
|
+
integrations: readonly Integration[];
|
|
60
|
+
authMode?: (provider: ProviderId) => AuthMode;
|
|
61
|
+
now: Date;
|
|
62
|
+
}): Capabilities;
|
|
63
|
+
export declare function needsCapabilities(name: string, args: Record<string, unknown>, channel: string | undefined): boolean;
|
|
64
|
+
export declare const CAPABILITIES_UNREAD = "This depends on an agent, a worker or an integration. Read get_capabilities first, then propose or promise in a later step; if something it needs can't work, say so plainly with its next step and show_control link instead.";
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { resolveRouteCandidates } from "./agentconfig.js";
|
|
2
|
+
import { authPauseOf, messageCommand, providerName } from "./provider-auth.js";
|
|
3
|
+
import { readAuthMode, SUBSCRIPTION_CAPABLE } from "./keys.js";
|
|
4
|
+
import { DEFAULT_LIVENESS_MS } from "./runner.js";
|
|
5
|
+
import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
|
|
6
|
+
import { conversationSkills } from "./project-skills.js";
|
|
7
|
+
import { CHECK_LEVEL_WORDS, liveQuickCommand, projectCheckLevel } from "./check-levels.js";
|
|
8
|
+
import { STATE_WORDS } from "./integrations.js";
|
|
9
|
+
const ROLE_WORDS = { plan: "plan", build: "build", repair: "fix failed checks", review: "review" };
|
|
10
|
+
const MAX_LISTED = 12;
|
|
11
|
+
/** The exact provider and model pairs a project's route would use, with the roles each would play. */
|
|
12
|
+
function routePairs(store, repo) {
|
|
13
|
+
const pairs = new Map();
|
|
14
|
+
const resolved = resolveRouteCandidates(store, repo);
|
|
15
|
+
if (!resolved.ok)
|
|
16
|
+
return { pairs, problem: resolved.problem };
|
|
17
|
+
const add = (phase, candidate) => {
|
|
18
|
+
if (candidate === null)
|
|
19
|
+
return;
|
|
20
|
+
const key = `${candidate.provider} ${candidate.model}`;
|
|
21
|
+
const pair = pairs.get(key) ?? { provider: candidate.provider, model: candidate.model, roles: [] };
|
|
22
|
+
if (!pair.roles.includes(ROLE_WORDS[phase]))
|
|
23
|
+
pair.roles.push(ROLE_WORDS[phase]);
|
|
24
|
+
pairs.set(key, pair);
|
|
25
|
+
};
|
|
26
|
+
for (const phase of ["plan", "build", "repair", "review"]) {
|
|
27
|
+
const leg = resolved.candidates[phase];
|
|
28
|
+
add(phase, leg.routine ?? (phase === "repair" ? resolved.candidates.build.routine : null));
|
|
29
|
+
add(phase, leg.strong);
|
|
30
|
+
}
|
|
31
|
+
return { pairs, problem: null };
|
|
32
|
+
}
|
|
33
|
+
/** A provider's plan or quota running out, from the last readings (provider_limit and the runners' quota rows). */
|
|
34
|
+
function outOfBudget(store, provider, now) {
|
|
35
|
+
const stamp = now.toISOString();
|
|
36
|
+
const window = store.providerLimits().find(one => one.provider === provider && one.reached && (one.resetsAt === null || one.resetsAt > stamp));
|
|
37
|
+
if (window !== undefined)
|
|
38
|
+
return { until: window.resetsAt };
|
|
39
|
+
const quota = store.handle.prepare("SELECT reset_at FROM quota WHERE provider = ? AND state = 'exhausted' AND (reset_at IS NULL OR reset_at > ?) ORDER BY reset_at DESC LIMIT 1").get(provider, stamp);
|
|
40
|
+
return quota === undefined ? null : { until: quota["reset_at"] == null ? null : String(quota["reset_at"]) };
|
|
41
|
+
}
|
|
42
|
+
function agentsOf(store, repo, now, authMode) {
|
|
43
|
+
const { pairs, problem } = routePairs(store, repo);
|
|
44
|
+
if (problem !== null) {
|
|
45
|
+
return { agents: [], agentsProblem: { name: "Agents", state: "Not set up", ok: false, lets: "plan and build this project's tasks", next: `Choose an exact model for each role: ${problem}`, link: "providers" } };
|
|
46
|
+
}
|
|
47
|
+
const readiness = store.readinessLookupFor(repo, null, now);
|
|
48
|
+
const agents = [...pairs.values()].map(pair => {
|
|
49
|
+
const name = `${providerName(pair.provider)} · ${pair.model}`;
|
|
50
|
+
const lets = `${pair.roles.join(", ")} this project's tasks`;
|
|
51
|
+
const base = { provider: pair.provider, model: pair.model, roles: pair.roles, name, lets };
|
|
52
|
+
// Signed out only on a real sign-in failure: a sign-in pause from a run, the worker's login check saying not logged
|
|
53
|
+
// in, or the key a key-only provider needs missing. A provider on an API key gets the key step, never the sign-in.
|
|
54
|
+
const pause = authPauseOf(store, pair.provider);
|
|
55
|
+
const mode = pause?.authMode ?? (SUBSCRIPTION_CAPABLE[pair.provider] ? authMode(pair.provider) : "api-key");
|
|
56
|
+
const key = mode === "api-key";
|
|
57
|
+
const outWords = key ? "Needs a working API key" : "Signed out";
|
|
58
|
+
const outNext = (detail) => key ? `Save a working API key: run \`${messageCommand({ provider: pair.provider, authMode: "api-key" })}\` on this computer${detail}.` : `Sign in: run \`${messageCommand({ provider: pair.provider, authMode: "subscription" })}\` on this computer${detail}.`;
|
|
59
|
+
if (pause !== null)
|
|
60
|
+
return { ...base, state: outWords, ok: false, next: outNext("; its tasks start again on their own"), link: "providers" };
|
|
61
|
+
const seen = readiness(pair.provider);
|
|
62
|
+
const authFailed = seen?.state === "unavailable" && (seen.probe === "key" || (seen.probe === "identity" && !key));
|
|
63
|
+
if (authFailed)
|
|
64
|
+
return { ...base, state: outWords, ok: false, next: outNext(""), link: "providers" };
|
|
65
|
+
if (seen?.state === "unavailable" && seen.probe !== "identity") {
|
|
66
|
+
return { ...base, state: `Not available: ${seen.reason}`, ok: false, next: `Fix it on the worker: ${seen.reason}.`, link: "providers" };
|
|
67
|
+
}
|
|
68
|
+
const spent = outOfBudget(store, pair.provider, now);
|
|
69
|
+
if (spent !== null)
|
|
70
|
+
return { ...base, state: "Out of plan budget", ok: false, next: spent.until === null ? "Wait for the plan to reset, or choose another agent." : `Wait until ${spent.until}, when the plan resets, or choose another agent.`, link: "providers" };
|
|
71
|
+
if (seen === null)
|
|
72
|
+
return { ...base, state: "Not checked yet", ok: false, next: "No worker has reported this agent yet: start a worker for this project.", link: "workers" };
|
|
73
|
+
// On an API key, only a key check speaks for it: a CLI login check (passed or not) says nothing about the key.
|
|
74
|
+
if (seen.state !== "ready" || (key && seen.probe !== "key")) {
|
|
75
|
+
return { ...base, state: "Not checked yet", ok: false, next: key ? "Its API key has not been checked without spending; the first task shows whether it works." : "A worker could not check it without spending; the first task shows whether it works.", link: "providers" };
|
|
76
|
+
}
|
|
77
|
+
return { ...base, state: key ? "API key set" : "Signed in", ok: true, next: null, link: null };
|
|
78
|
+
});
|
|
79
|
+
return { agents, agentsProblem: null };
|
|
80
|
+
}
|
|
81
|
+
function toolsOf(store, repo) {
|
|
82
|
+
return projectToolsOf(store, repo).slice(0, MAX_LISTED).map(tool => {
|
|
83
|
+
let set = [];
|
|
84
|
+
try {
|
|
85
|
+
set = secretsSetFor(repo, tool.spec);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
set = [];
|
|
89
|
+
}
|
|
90
|
+
const standing = toolStanding(tool, set);
|
|
91
|
+
const state = standing.words;
|
|
92
|
+
const lets = `builds in this project can use ${tool.name}${tool.spec.about ? ` (${tool.spec.about})` : ""}`;
|
|
93
|
+
if (state.startsWith("Needs"))
|
|
94
|
+
return { name: tool.name, state, ok: false, lets, next: `Add ${state.slice("Needs ".length)} on the Tools page; never in chat.`, link: "tools" };
|
|
95
|
+
if (state === "Last test failed")
|
|
96
|
+
return { name: tool.name, state, ok: false, lets, next: `Fix it and test it again on the Tools page${tool.lastTest?.problem ? `: ${tool.lastTest.problem}` : ""}.`, link: "tools" };
|
|
97
|
+
if (state === "Not tested yet")
|
|
98
|
+
return { name: tool.name, state, ok: true, lets, next: "Test it on the Tools page to be sure it works.", link: "tools" };
|
|
99
|
+
return { name: tool.name, state: "Working", ok: true, lets, next: null, link: null };
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** The newest test of one skill version in a project: passed, failed or still running; null when never tested. */
|
|
103
|
+
function lastSkillTest(store, repo, sha) {
|
|
104
|
+
const row = store.handle.prepare("SELECT skill_test.task_ref AS ref FROM skill_test JOIN task_ref ON task_ref.id = skill_test.task_ref WHERE task_ref.repo = ? AND skill_test.package = ? ORDER BY skill_test.task_ref DESC LIMIT 1").get(repo, sha);
|
|
105
|
+
if (row === undefined)
|
|
106
|
+
return null;
|
|
107
|
+
const run = store.runsFor(Number(row["ref"]))[0];
|
|
108
|
+
if (run === undefined || run.outcome === null)
|
|
109
|
+
return "running";
|
|
110
|
+
return run.outcome === "built" || run.outcome === "no-change" ? "passed" : "failed";
|
|
111
|
+
}
|
|
112
|
+
function skillsOf(store, repo, actor) {
|
|
113
|
+
let skills;
|
|
114
|
+
try {
|
|
115
|
+
skills = conversationSkills(store, repo, actor).skills;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
return skills.filter(one => one.enabled).slice(0, MAX_LISTED).map(skill => {
|
|
121
|
+
const lets = `builds in this project are given the ${skill.name} skill${skill.description ? ` (${skill.description.slice(0, 120)})` : ""}`;
|
|
122
|
+
const test = lastSkillTest(store, repo, skill.sha);
|
|
123
|
+
if (test === "failed")
|
|
124
|
+
return { name: skill.name, state: "Last test failed", ok: false, lets, next: "Read it with get_skills, then fix and test it again on the Skills page.", link: "skills" };
|
|
125
|
+
const needs = skill.requirements.trim();
|
|
126
|
+
if (needs !== "" && test !== "passed")
|
|
127
|
+
return { name: skill.name, state: `Needs ${needs.slice(0, 120)}`, ok: false, lets, next: "Make sure what it needs is set up, then test it on the Skills page.", link: "skills" };
|
|
128
|
+
return { name: skill.name, state: test === "running" ? "Working · test running" : "Working", ok: true, lets, next: null, link: null };
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function checksOf(store, repo) {
|
|
132
|
+
const level = projectCheckLevel(store, repo).level;
|
|
133
|
+
const releaseCheck = store.liveVerifyCommand(repo) !== null, quickCheck = liveQuickCommand(store, repo) !== null;
|
|
134
|
+
const base = { name: "Checks", level: CHECK_LEVEL_WORDS[level], quickCheck, releaseCheck, lets: "say whether a result's checks passed before you call it done" };
|
|
135
|
+
if (level === "off")
|
|
136
|
+
return { ...base, state: "Off", ok: false, next: "Builds are not checked. Turn checks on for the project if results need them.", link: "projects" };
|
|
137
|
+
if (!releaseCheck)
|
|
138
|
+
return { ...base, state: `${CHECK_LEVEL_WORDS[level]} · no release check`, ok: false, next: "No release check is set up, so no result can show a passing check. Set one for the project.", link: "projects" };
|
|
139
|
+
return { ...base, state: level === "quick" && !quickCheck ? "Quick · runs the release check" : `${CHECK_LEVEL_WORDS[level]} · release check set`, ok: true, next: null, link: null };
|
|
140
|
+
}
|
|
141
|
+
function workersOf(store, repos, now) {
|
|
142
|
+
const live = store.handle.prepare(`SELECT ref.external_id AS task, ref.repo AS repo FROM claim JOIN task_ref AS ref ON ref.id = claim.task_ref
|
|
143
|
+
WHERE claim.runner = ? AND claim.released_at IS NULL AND claim.expires_at > ?
|
|
144
|
+
AND claim.lease_generation = (SELECT MAX(newest.lease_generation) FROM claim AS newest WHERE newest.task_ref = claim.task_ref)`);
|
|
145
|
+
const workers = store.listRunners()
|
|
146
|
+
.filter(one => one.retiredAt === null && one.repos.some(repo => repos.includes(repo)))
|
|
147
|
+
.slice(0, MAX_LISTED)
|
|
148
|
+
.map(runner => {
|
|
149
|
+
const online = now.getTime() - Date.parse(runner.heartbeatAt) <= DEFAULT_LIVENESS_MS;
|
|
150
|
+
const held = live.all(runner.name, now.toISOString());
|
|
151
|
+
const running = held.filter(row => repos.includes(String(row["repo"]))).map(row => String(row["task"]));
|
|
152
|
+
const busy = store.liveClaimCount(runner.name, now);
|
|
153
|
+
const lets = `run tasks for ${runner.repos.filter(repo => repos.includes(repo)).length} of your projects, ${runner.capacity} at a time`;
|
|
154
|
+
if (!online)
|
|
155
|
+
return { name: runner.name, state: `Offline since ${runner.heartbeatAt}`, ok: false, online, capacity: runner.capacity, running, lets,
|
|
156
|
+
next: `Start that computer, or reinstall its background worker there: \`toolroll daemon install --runner ${runner.name}\`.`, link: "workers" };
|
|
157
|
+
const full = busy >= runner.capacity;
|
|
158
|
+
return { name: runner.name, state: full ? `Online · full (${busy} of ${runner.capacity})` : `Online · ${busy} of ${runner.capacity} busy`, ok: true, online, capacity: runner.capacity, running, lets,
|
|
159
|
+
next: full ? "New work waits for a free slot; raise its capacity on the Workers page if it should run more at once." : null, link: full ? "workers" : null };
|
|
160
|
+
});
|
|
161
|
+
if (workers.length === 0) {
|
|
162
|
+
return [{ name: "No worker", state: "None for your projects", ok: false, online: false, capacity: 0, running: [], lets: "run any task",
|
|
163
|
+
next: "Add a worker for the project: run `toolroll daemon install` on the computer that should do the work.", link: "workers" }];
|
|
164
|
+
}
|
|
165
|
+
return workers;
|
|
166
|
+
}
|
|
167
|
+
const INTEGRATION_LINK = { slack: "slack", discord: "discord" };
|
|
168
|
+
/** The integrations exactly as get_integrations reads them, with what each lets the lead do and the next step. */
|
|
169
|
+
export function integrationCapabilities(list) {
|
|
170
|
+
return list.map(one => {
|
|
171
|
+
const link = one.group === "agents" ? "providers" : one.group === "tools" ? "tools" : INTEGRATION_LINK[one.key.replace(/^.*:/, "").toLowerCase()] ?? INTEGRATION_LINK[one.name.toLowerCase()] ?? "integrations";
|
|
172
|
+
const lets = one.usedBy.length > 0 ? `use ${one.name} for ${one.usedBy.join(", ")}` : `use ${one.name}`;
|
|
173
|
+
const next = one.action.kind === "setup" ? `Not set up: set up ${one.name} in Settings` : one.action.kind === "fix" ? `Fix: ${one.action.words}` : null;
|
|
174
|
+
return { name: one.name, kind: one.group, state: STATE_WORDS[one.state], ok: one.state === "connected", account: one.account, usedBy: one.usedBy, lastError: one.lastError, lets, next, link: next === null ? null : link };
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
/** Every capability, for one project or every admitted one. Read-only and spends nothing. */
|
|
178
|
+
export function capabilitiesOf(store, input) {
|
|
179
|
+
const authMode = input.authMode ?? (provider => readAuthMode(provider));
|
|
180
|
+
return {
|
|
181
|
+
projects: input.repos.map(repo => ({ repo, ...agentsOf(store, repo, input.now, authMode), tools: toolsOf(store, repo), skills: skillsOf(store, repo, input.actor), checks: checksOf(store, repo) })),
|
|
182
|
+
workers: workersOf(store, input.admitted, input.now),
|
|
183
|
+
integrations: integrationCapabilities(input.integrations),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
/** Whether a call depends on an agent, a worker or an integration, and so needs get_capabilities read first this turn:
|
|
187
|
+
* a task always does (a worker runs it with an agent); a flow change that sets up or starts work does; a promise to
|
|
188
|
+
* report on a task, attempt or check does (crew work must run), and a timed one does when it is reported on a chat app. */
|
|
189
|
+
const FLOW_WORK = new Set(["create", "edit", "kit", "starter", "add_trigger", "resume_trigger", "add_card"]);
|
|
190
|
+
export function needsCapabilities(name, args, channel) {
|
|
191
|
+
if (name === "propose_task")
|
|
192
|
+
return true;
|
|
193
|
+
if (name === "propose_flow")
|
|
194
|
+
return FLOW_WORK.has(String(args["operation"]));
|
|
195
|
+
if (name === "commit_to")
|
|
196
|
+
return args["when"] === "time" ? channel !== undefined && channel !== "chat" : true;
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
export const CAPABILITIES_UNREAD = "This depends on an agent, a worker or an integration. Read get_capabilities first, then propose or promise in a later step; if something it needs can't work, say so plainly with its next step and show_control link instead.";
|
package/dist/lead-commitments.js
CHANGED
|
@@ -87,9 +87,13 @@ export function getCommitment(store, id) {
|
|
|
87
87
|
const row = store.handle.prepare('SELECT * FROM lead_commitment WHERE id = ?').get(id);
|
|
88
88
|
return row === undefined ? null : readCommitment(row);
|
|
89
89
|
}
|
|
90
|
-
/**
|
|
91
|
-
*
|
|
92
|
-
const
|
|
90
|
+
/** The reply that made a promise was shown: its turn is over, and it was answered or its text reached the conversation
|
|
91
|
+
* before it failed. A turn still running has shown nothing yet, whatever it has written so far. */
|
|
92
|
+
const SHOWN = (turn) => `EXISTS (SELECT 1 FROM mate_turn t WHERE t.id = ${turn} AND (t.state = 'answered'
|
|
93
|
+
OR t.state = 'failed' AND EXISTS (SELECT 1 FROM mate_message m WHERE m.turn = t.id AND m.role = 'assistant')))`;
|
|
94
|
+
/** A promise only counts once the reply that made it was shown: one from a reply still being written, or one that
|
|
95
|
+
* failed without reaching the owner, was never heard. */
|
|
96
|
+
const HEARD = `(c.turn IS NULL OR ${SHOWN('c.turn')})`;
|
|
93
97
|
/** The owner's open promises that they heard, oldest first. */
|
|
94
98
|
export function openCommitments(store, owner, limit = 20) {
|
|
95
99
|
return store.handle.prepare(`SELECT c.* FROM lead_commitment c WHERE c.owner = ? AND c.state = 'open' AND ${HEARD} ORDER BY c.id LIMIT ?`).all(owner, limit)
|
|
@@ -153,10 +157,12 @@ export function checkLeadCommitments(store, now, root) {
|
|
|
153
157
|
const at = now.toISOString();
|
|
154
158
|
let reported = 0;
|
|
155
159
|
store.handle.prepare("UPDATE lead_commitment SET state = 'expired', closed_at = ?, outcome = 'Expired after 7 days without the condition being met.' WHERE state = 'open' AND expires_at <= ?").run(at, at);
|
|
156
|
-
// A promise made in a reply that was not delivered (it failed, or its turn is gone) was
|
|
157
|
-
// whenever it was next due, so nothing waits on it.
|
|
160
|
+
// A promise made in a reply that was not delivered (it failed before any text was shown, or its turn is gone) was
|
|
161
|
+
// never heard: drop it now, whenever it was next due, so nothing waits on it. An interrupted reply whose text was
|
|
162
|
+
// shown keeps its promises.
|
|
158
163
|
store.handle.prepare(`UPDATE lead_commitment SET state = 'cancelled', closed_at = ?, closed_by = 'lead', outcome = 'The reply that made this promise was not delivered.'
|
|
159
|
-
WHERE state = 'open' AND turn IS NOT NULL AND NOT EXISTS (SELECT 1 FROM mate_turn t WHERE t.id = lead_commitment.turn AND t.state IN ('queued', 'running'
|
|
164
|
+
WHERE state = 'open' AND turn IS NOT NULL AND NOT EXISTS (SELECT 1 FROM mate_turn t WHERE t.id = lead_commitment.turn AND t.state IN ('queued', 'running'))
|
|
165
|
+
AND NOT ${SHOWN('lead_commitment.turn')}`).run(at);
|
|
160
166
|
const due = store.handle.prepare("SELECT * FROM lead_commitment WHERE state = 'open' AND check_at <= ? ORDER BY check_at, id LIMIT ?").all(at, PASS_LIMIT);
|
|
161
167
|
for (const row of due) {
|
|
162
168
|
const one = readCommitment(row);
|
|
@@ -168,7 +174,7 @@ export function checkLeadCommitments(store, now, root) {
|
|
|
168
174
|
}
|
|
169
175
|
// A reply still being written may yet be delivered; one that was not is dropped above.
|
|
170
176
|
const turn = one.turn === null ? null : store.getMateTurn(one.turn);
|
|
171
|
-
if (turn !== null && turn.state
|
|
177
|
+
if (turn !== null && (turn.state === 'queued' || turn.state === 'running'))
|
|
172
178
|
continue;
|
|
173
179
|
const account = store.accountOf(one.owner);
|
|
174
180
|
if (account === null || account.revokedAt !== null || one.repo !== null && !store.accountCanAccess(one.owner, one.repo)) {
|
package/dist/lead-context.d.ts
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
* repository scan, mutation, or hidden approval happens while catching up.
|
|
3
3
|
*
|
|
4
4
|
* The bundle is ordered by importance and built fresh each turn: who the lead
|
|
5
|
-
* is (name, persona), who it is talking to (first name, time zone, today),
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* is (name, persona), who it is talking to (first name, time zone, today), what
|
|
6
|
+
* it knows about them (their confirmed lines), the people, AI teammates and team
|
|
7
|
+
* chats it works with (one line each), the channel, what needs them now,
|
|
8
|
+
* projects by name with their active decisions, then the rest. Over 8 KB, the
|
|
9
|
+
* least important goes first: the rest, then people, then decisions. */
|
|
8
10
|
import type { Store } from './store.js';
|
|
9
11
|
import { type AssignmentCatchUp } from './assignment-brief.js';
|
|
10
12
|
export declare const LEAD_CONTEXT_MAX_BYTES = 8000;
|
package/dist/lead-context.js
CHANGED
|
@@ -2,7 +2,9 @@ import { activeDecisionsOf, assignmentCatchUp } from './assignment-brief.js';
|
|
|
2
2
|
import { publicChatText } from './chat-display.js';
|
|
3
3
|
import { leadIdentityOf } from './lead-identity.js';
|
|
4
4
|
import { conditionWords, openCommitments } from './lead-commitments.js';
|
|
5
|
-
|
|
5
|
+
import { aboutYouOf } from './lead-about.js';
|
|
6
|
+
import { peopleIndexOf, peopleLines, teamRoomOf } from './lead-people.js';
|
|
7
|
+
const REMEMBERED = new Set(['decision_record', 'knowledge_instructions', 'lead_about_you']);
|
|
6
8
|
/** The lead's own follow-through for one conversation: its open promises, and corrections the operator confirmed
|
|
7
9
|
* since its last reply (with the cards still open in this conversation, which a correction may affect). */
|
|
8
10
|
function followThrough(store, owner, thread) {
|
|
@@ -16,7 +18,9 @@ function followThrough(store, owner, thread) {
|
|
|
16
18
|
// A changed instruction is added at the end, so that is the part to show.
|
|
17
19
|
const instructions = payload?.request?.instructions;
|
|
18
20
|
const change = payload?.operation === 'knowledge_instructions' && typeof instructions === 'string'
|
|
19
|
-
? `Project instructions now end: ${instructions.slice(-240)}`
|
|
21
|
+
? `Project instructions now end: ${instructions.slice(-240)}`
|
|
22
|
+
: payload?.operation === 'lead_about_you' && typeof payload?.request?.line === 'string' ? `About you: ${payload.request.line}`
|
|
23
|
+
: Array.isArray(payload?.terms) ? payload.terms[0] : null;
|
|
20
24
|
if (REMEMBERED.has(payload?.operation) && typeof change === 'string')
|
|
21
25
|
corrections.push({ proposal: Number(row['id']), change: change.slice(0, 300) });
|
|
22
26
|
}
|
|
@@ -94,26 +98,40 @@ export function leadContext(store, repos, now, options = {}) {
|
|
|
94
98
|
sources: one.knowledge.sources.map(source => ({ id: source.id, title: source.title })) }));
|
|
95
99
|
const omissions = { ...brief.omissions, projects: Math.max(brief.omissions.projects, repos.length - 8), notes: [...brief.omissions.notes] };
|
|
96
100
|
const firstName = options.owner === undefined ? null : firstNameOf(options.owner);
|
|
101
|
+
const redact = options.redact ?? (text => text);
|
|
102
|
+
// A team chat's own lead speaks for the room, so the owner's own note stays with their own lead, and the people
|
|
103
|
+
// index is that room and its members, no one else.
|
|
104
|
+
const room = teamRoomOf(store, options.thread);
|
|
105
|
+
const aboutYou = options.leadName === undefined && room === null ? aboutYouOf(store, options.owner) : [];
|
|
106
|
+
// First names are shown on purpose; each line's free text is scrubbed as it is written.
|
|
107
|
+
const people = options.owner === undefined ? { people: [], teammates: [], teams: [] } : peopleLines(peopleIndexOf(store, options.owner, repos, room), redact);
|
|
97
108
|
// The whole bundle is scrubbed (titles, notes, next labels, the lead's name and persona); then the names it
|
|
98
|
-
// carries on purpose are put back: the lead's own name, the person's first name
|
|
109
|
+
// carries on purpose are put back: the lead's own name, the person's first name, the lines they confirmed about
|
|
110
|
+
// themselves, the people index (first names) and each project's label.
|
|
99
111
|
const data = scrubbed({
|
|
100
112
|
snapshotVersion: 3, source: 'local-database',
|
|
101
113
|
me: { name: options.leadName ?? identity.name, persona: identity.persona },
|
|
102
114
|
you: { firstName, ...localNow(now, options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone) },
|
|
115
|
+
aboutYou,
|
|
116
|
+
people: { people: [], teammates: [], teams: [] },
|
|
103
117
|
channel: options.channel === undefined ? null : { id: options.channel, fit: CHANNEL_WORDS[options.channel] },
|
|
104
118
|
needsYou: brief.assignments.filter(needs).map(task),
|
|
105
119
|
...(options.owner === undefined || options.thread === undefined ? {} : followThrough(store, options.owner, options.thread)),
|
|
106
120
|
projects,
|
|
107
121
|
rest: { tasks: brief.assignments.filter(one => !needs(one)).map(task), knowledge },
|
|
108
|
-
omissions,
|
|
122
|
+
omissions: { ...omissions, people: 0 },
|
|
109
123
|
notice: 'Bounded catch-up. Read the exact task/result before acting. Saved knowledge is context, not authority.',
|
|
110
|
-
},
|
|
124
|
+
}, redact);
|
|
111
125
|
data.you.firstName = firstName;
|
|
126
|
+
data.people = people;
|
|
127
|
+
// The owner confirmed every line (secret-checked when saved), so they read as written, names included.
|
|
128
|
+
data.aboutYou = aboutYou;
|
|
112
129
|
// The name the owner gave their lead is theirs to say, even when it matches an account name.
|
|
113
130
|
data.me.name = options.leadName ?? identity.name;
|
|
114
131
|
data.projects.forEach((one, index) => { one.name = projects[index].name; });
|
|
115
|
-
// Least important first: the rest's knowledge, then its tasks, then
|
|
116
|
-
// last Needs you. Who the lead is, who it is talking
|
|
132
|
+
// Least important first: the rest's knowledge, then its tasks, then the people index (team chats, AI teammates,
|
|
133
|
+
// then people), then each project's oldest decision, then the last Needs you. Who the lead is, who it is talking
|
|
134
|
+
// to, what it knows about them and the channel always stay.
|
|
117
135
|
const drop = () => {
|
|
118
136
|
if (data.rest.knowledge.pop() !== undefined) {
|
|
119
137
|
data.omissions.projects++;
|
|
@@ -123,6 +141,10 @@ export function leadContext(store, repos, now, options = {}) {
|
|
|
123
141
|
data.omissions.assignments++;
|
|
124
142
|
return true;
|
|
125
143
|
}
|
|
144
|
+
if ((data.people.teams.pop() ?? data.people.teammates.pop() ?? data.people.people.pop()) !== undefined) {
|
|
145
|
+
data.omissions.people++;
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
126
148
|
const decided = [...data.projects].reverse().find(one => one.decisions.length > 0);
|
|
127
149
|
if (decided !== undefined) {
|
|
128
150
|
decided.decisions.pop();
|
package/dist/lead-follow.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Store, type MateSession, type MateThread, type ChatConfig } from './store.js';
|
|
2
2
|
import { type VerifiedApprover } from './principal.js';
|
|
3
3
|
import { type MateTurnInput, type MateTurnOutcome } from './mate.js';
|
|
4
4
|
export declare function configureLeadFollow(store: Store, who: VerifiedApprover, session: MateSession, thread: MateThread, enabled: boolean, now: Date): boolean;
|
package/dist/lead-follow.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Durable crew updates for the built-in lead. Idle scans use no model.
|
|
2
2
|
* Delivery replays reuse the saved mate request; they never dispatch a task. */
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { LEAD_FOLLOW_MESSAGE } from './store.js';
|
|
4
5
|
import { isVerifiedApprover, reproveApprover, verifyApproverStanding } from './principal.js';
|
|
5
6
|
import { assignmentOf } from './assignment.js';
|
|
6
7
|
import { runMateTurn } from './mate.js';
|
|
@@ -141,8 +142,8 @@ export async function runLeadFollowPass(input) {
|
|
|
141
142
|
if (!provider)
|
|
142
143
|
continue;
|
|
143
144
|
const result = await (input.runTurn ?? runMateTurn)({ store, ...access, ...provider, evidenceRoot: input.evidenceRoot, clock,
|
|
144
|
-
requestId: batch.request, message:
|
|
145
|
-
context: batch.context, revalidate: async () => !updateAdmissionPaused(store.raw()) && authorized(store, grant, input.repos()) !== null ? { ok: true } : { ok: false, reason: '
|
|
145
|
+
requestId: batch.request, message: LEAD_FOLLOW_MESSAGE,
|
|
146
|
+
context: batch.context, revalidate: async () => !updateAdmissionPaused(store.raw()) && authorized(store, grant, input.repos()) !== null ? { ok: true } : { ok: false, reason: 'access-changed' },
|
|
146
147
|
...(input.fetcher ? { fetcher: input.fetcher } : {}), ...(input.subscriptionRunner ? { subscriptionRunner: input.subscriptionRunner } : {}) });
|
|
147
148
|
if (!result.ok && 'refused' in result) {
|
|
148
149
|
const status = `Crew updates are waiting: ${result.message}`;
|
package/dist/lead-identity.js
CHANGED
|
@@ -9,7 +9,7 @@ export function leadIdentityOf(store, account) {
|
|
|
9
9
|
try {
|
|
10
10
|
const saved = store.leadConfig(account);
|
|
11
11
|
if (saved !== null)
|
|
12
|
-
return { name: saved.name, persona: saved.persona };
|
|
12
|
+
return { name: saved.name || DEFAULT_LEAD_NAME, persona: saved.persona || DEFAULT_LEAD_PERSONA };
|
|
13
13
|
}
|
|
14
14
|
catch { /* no lead_config yet: the defaults */ }
|
|
15
15
|
return { name: DEFAULT_LEAD_NAME, persona: DEFAULT_LEAD_PERSONA };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Store, TeammateRow } from "./store.js";
|
|
2
|
+
export type PersonEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
account: string;
|
|
5
|
+
name: string;
|
|
6
|
+
role: string;
|
|
7
|
+
projects: string[];
|
|
8
|
+
profile: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type MateEntry = {
|
|
11
|
+
id: string;
|
|
12
|
+
mate: TeammateRow;
|
|
13
|
+
name: string;
|
|
14
|
+
role: string;
|
|
15
|
+
project: string;
|
|
16
|
+
};
|
|
17
|
+
export type TeamEntry = {
|
|
18
|
+
id: string;
|
|
19
|
+
conversation: string;
|
|
20
|
+
name: string;
|
|
21
|
+
members: string[];
|
|
22
|
+
purpose: string;
|
|
23
|
+
projects: string[];
|
|
24
|
+
};
|
|
25
|
+
export type PeopleIndex = {
|
|
26
|
+
people: PersonEntry[];
|
|
27
|
+
teammates: MateEntry[];
|
|
28
|
+
teams: TeamEntry[];
|
|
29
|
+
};
|
|
30
|
+
/** get_person's id shapes. */
|
|
31
|
+
export declare const PERSON_ID: RegExp;
|
|
32
|
+
/** The team chat this conversation is, or null when it is the person's own chat with their lead. */
|
|
33
|
+
export declare function teamRoomOf(store: Store, thread: number | undefined): string | null;
|
|
34
|
+
/** Everyone the owner's lead works with, in order; in a team chat (`room`), only that room and its members. */
|
|
35
|
+
export declare function peopleIndexOf(store: Store, owner: string, repos: readonly string[], room?: string | null): PeopleIndex;
|
|
36
|
+
/** One line each, as the bundle carries them. Free text goes through `redact`; names and project ids do not. */
|
|
37
|
+
export declare function peopleLines(index: PeopleIndex, redact: (text: string) => string): {
|
|
38
|
+
people: string[];
|
|
39
|
+
teammates: string[];
|
|
40
|
+
teams: string[];
|
|
41
|
+
};
|
|
42
|
+
/** One entry in full by its id (p3fa91c2e, t5, c07b1d9a4) or by name; the candidates when a name fits several, null when
|
|
43
|
+
* none. In a team chat (`room`), only that room and its members. */
|
|
44
|
+
export declare function personEntry(store: Store, owner: string, repos: readonly string[], ask: {
|
|
45
|
+
id?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
}, room?: string | null): {
|
|
48
|
+
found: Record<string, unknown>;
|
|
49
|
+
} | {
|
|
50
|
+
several: {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
kind: string;
|
|
54
|
+
}[];
|
|
55
|
+
} | null;
|