toolroll 0.8.2 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/agent-onboard.d.ts +24 -1
- package/dist/agent-onboard.js +53 -1
- package/dist/backup-ui.js +4 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +33 -32
- package/dist/browser-workspace.d.ts +22 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +43 -3
- package/dist/chat-delivery.js +51 -3
- package/dist/chat-quiet.d.ts +28 -0
- package/dist/chat-quiet.js +161 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/digest-times.d.ts +3 -0
- package/dist/digest-times.js +7 -0
- package/dist/dispose.js +2 -0
- package/dist/export.js +1 -1
- package/dist/flow-engine.js +13 -1
- package/dist/flow-pull-request.d.ts +11 -0
- package/dist/flow-pull-request.js +123 -0
- package/dist/flow-starters-ui.d.ts +20 -0
- package/dist/flow-starters-ui.js +35 -0
- package/dist/flow-starters.d.ts +55 -0
- package/dist/flow-starters.js +104 -0
- package/dist/flow-steps.js +14 -0
- package/dist/flows-ui.d.ts +1 -1
- package/dist/flows-ui.js +4 -3
- package/dist/flows.d.ts +30 -6
- package/dist/flows.js +110 -6
- package/dist/integrations-ui.d.ts +13 -0
- package/dist/integrations-ui.js +70 -0
- package/dist/integrations.d.ts +113 -0
- package/dist/integrations.js +526 -0
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +1 -1
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +18 -5
- package/dist/models-ui.js +3 -2
- package/dist/monitoring-ui.js +2 -1
- package/dist/operate.d.ts +5 -2
- package/dist/operate.js +169 -4
- package/dist/policy-ui.js +2 -1
- package/dist/project-delete.js +1 -0
- package/dist/publish.d.ts +11 -0
- package/dist/publish.js +9 -2
- package/dist/pull-request-flow.d.ts +172 -0
- package/dist/pull-request-flow.js +475 -0
- package/dist/pull-request-schema.d.ts +5 -0
- package/dist/pull-request-schema.js +21 -0
- package/dist/pull-request-ui.d.ts +23 -0
- package/dist/pull-request-ui.js +55 -0
- package/dist/pulls.d.ts +5 -0
- package/dist/pulls.js +11 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +371 -58
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.js +2 -1
- package/dist/store.d.ts +61 -0
- package/dist/store.js +144 -3
- package/dist/surface.js +3 -1
- package/dist/telegram-progress.d.ts +1 -0
- package/dist/telegram-progress.js +1 -1
- package/dist/telegram.js +95 -15
- package/dist/when-html.d.ts +8 -0
- package/dist/when-html.js +36 -0
- package/package.json +1 -1
package/dist/mate-tools.js
CHANGED
|
@@ -51,6 +51,7 @@ import { TEAMMATE_TEMPLATES, withSection } from "./teammates.js";
|
|
|
51
51
|
import { callOutcome, callWords, defaultRule, ruleWords } from "./teammate-tools.js";
|
|
52
52
|
import { deskOf, routinesOf } from "./teammate-desk.js";
|
|
53
53
|
import { KITS } from "./kits.js";
|
|
54
|
+
import { STARTER_IDS } from "./flow-starters.js";
|
|
54
55
|
import { undoFor, weekOf, weekWords } from "./teammate-week.js";
|
|
55
56
|
const REPO_ID = /^r[0-9]{1,3}$/;
|
|
56
57
|
const TASK_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
@@ -820,7 +821,8 @@ export const MATE_TOOLS = [
|
|
|
820
821
|
...(stage.tool === undefined ? {} : { server: stage.tool.server, tool: stage.tool.name, args: stage.tool.args.slice(0, 600) }),
|
|
821
822
|
...(stage.kind === "sort" && stage.sort !== null ? { question: stage.sort.question, answers: stage.sort.answers.map(one => ({ answer: one.answer, means: one.means, goesTo: titleOf(one.to) })),
|
|
822
823
|
sureAt: Math.round(stage.sort.sureAt * 100), ...(stage.sort.notes.length === 0 ? {} : { alsoNote: stage.sort.notes.map(one => ({ question: one.question, kind: one.kind, ...(one.levels === null ? {} : { levels: one.levels }) })) }) } : {}),
|
|
823
|
-
...(stage.wait === undefined ? {} : { waitFor: stage.wait.for, wait: durationWords(stage.wait.minutes) }),
|
|
824
|
+
...(stage.wait === undefined ? {} : stage.wait.for === "hours" ? { waitFor: "hours", from: stage.wait.from, until: stage.wait.to } : { waitFor: stage.wait.for, wait: durationWords(stage.wait.minutes) }),
|
|
825
|
+
...(stage.merge === undefined ? {} : { merge: stage.merge }),
|
|
824
826
|
...(stage.teammate === undefined ? {} : { teammate: stage.teammate }),
|
|
825
827
|
...(stage.kind === "teammate" && stage.routes !== undefined ? { routes: stage.routes.map(one => ({ answer: one.answer, goesTo: titleOf(one.to) })) } : {}),
|
|
826
828
|
...(stage.limit === undefined ? {} : { remindAfter: durationWords(stage.limit.minutes), ...(stage.limit.to === null ? {} : { thenMoveTo: titleOf(stage.limit.to) }) }),
|
|
@@ -869,10 +871,11 @@ export const MATE_TOOLS = [
|
|
|
869
871
|
},
|
|
870
872
|
{
|
|
871
873
|
name: "propose_flow",
|
|
872
|
-
description: "Draft a flow change as a card the operator confirms. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a teammate, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). AI teammates (get_teammates): an approval step with teammate (its short name) is decided by that teammate within its rules, and it hands hard ones to the step's decider; a 'teammate' step (teammate, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain)); pause_trigger, resume_trigger, remove_trigger with trigger. Read get_flows first except to create.",
|
|
874
|
+
description: "Draft a flow change as a card the operator confirms. starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a teammate, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). AI teammates (get_teammates): an approval step with teammate (its short name) is decided by that teammate within its rules, and it hands hard ones to the step's decider; a 'teammate' step (teammate, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain)); pause_trigger, resume_trigger, remove_trigger with trigger. Read get_flows first except to create.",
|
|
873
875
|
inputSchema: schema({
|
|
874
|
-
operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit"] },
|
|
876
|
+
operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit", "starter"] },
|
|
875
877
|
repo: REPO_ARG, flow: { type: "integer", minimum: 1 }, card: { type: "integer", minimum: 1 }, kit: { type: "string", enum: KITS.map(one => one.id) },
|
|
878
|
+
starter: { type: "string", enum: [...STARTER_IDS] },
|
|
876
879
|
name: { type: "string", maxLength: 80 }, template: { type: "string", enum: FLOW_TEMPLATES.map(one => one.id) },
|
|
877
880
|
steps: { type: "array", minItems: 1, maxItems: 24, items: { type: "object", additionalProperties: false, properties: {
|
|
878
881
|
id: { type: "string", maxLength: 32 }, title: { type: "string", maxLength: 60 }, kind: { type: "string", enum: [...FLOW_STAGE_KINDS] },
|
|
@@ -888,7 +891,8 @@ export const MATE_TOOLS = [
|
|
|
888
891
|
server: { type: "string", maxLength: 64 }, tool: { type: "string", maxLength: 100 }, args: { type: "object" },
|
|
889
892
|
runIn: { type: "string", enum: ["folder", "copy"] }, routes: { type: "array", maxItems: 12, items: { type: "object", additionalProperties: false, properties: { answer: { type: "string", maxLength: 40 }, goesTo: { type: "string", maxLength: 60 } } } },
|
|
890
893
|
secrets: { type: "array", maxItems: 10, items: { type: "string", maxLength: 40 } },
|
|
891
|
-
waitFor: { type: "string", enum: ["reply", "time"] }, wait: { type: "string", maxLength: 40 }, ifNoReply: { type: "string", maxLength: 60 },
|
|
894
|
+
waitFor: { type: "string", enum: ["reply", "time", "hours"] }, wait: { type: "string", maxLength: 40 }, ifNoReply: { type: "string", maxLength: 60 },
|
|
895
|
+
from: { type: "string", maxLength: 5 }, until: { type: "string", maxLength: 5 }, merge: { type: "string", enum: ["squash", "merge", "rebase"] },
|
|
892
896
|
remindAfter: { type: "string", maxLength: 40 }, thenMoveTo: { type: "string", maxLength: 60 }, teammate: { type: "string", maxLength: 40 },
|
|
893
897
|
next: { type: "string", maxLength: 60 }, ifFails: { type: "string", maxLength: 60 }, ifNotSure: { type: "string", maxLength: 60 },
|
|
894
898
|
} } },
|
|
@@ -917,6 +921,15 @@ export const MATE_TOOLS = [
|
|
|
917
921
|
let operation, input;
|
|
918
922
|
try {
|
|
919
923
|
switch (args["operation"]) {
|
|
924
|
+
case "starter": {
|
|
925
|
+
// a starter flow — its zones and its trigger, one yes.
|
|
926
|
+
const repo = repoPathOf(ctx.who, args["repo"]);
|
|
927
|
+
if (repo === null)
|
|
928
|
+
return { ok: false, message: "Choose a project from list_repos." };
|
|
929
|
+
operation = "flow_starter";
|
|
930
|
+
input = { repo, starter: args["starter"] };
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
920
933
|
case "kit": {
|
|
921
934
|
// a starter kit — a teammate, the flow it works, its buttons.
|
|
922
935
|
const repo = repoPathOf(ctx.who, args["repo"]);
|
|
@@ -937,7 +950,7 @@ export const MATE_TOOLS = [
|
|
|
937
950
|
if (name === undefined)
|
|
938
951
|
return { ok: false, message: "Name the flow." };
|
|
939
952
|
operation = "flow_create";
|
|
940
|
-
input = { repo, name, definition: template === undefined ? flowFromSteps(steps(), null) : structuredClone(template.definition) };
|
|
953
|
+
input = { repo, name, definition: template === undefined ? flowFromSteps(steps(), null) : structuredClone(template.definition), ...(template?.trigger === undefined ? {} : { trigger: template.trigger }) };
|
|
941
954
|
break;
|
|
942
955
|
}
|
|
943
956
|
case "edit": {
|
package/dist/models-ui.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { priceWords } from "./model-catalog.js";
|
|
2
|
+
import { whenUtc } from "./when-html.js";
|
|
2
3
|
const e = (s) => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
3
4
|
const hidden = (data) => Object.entries(data).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join("");
|
|
4
|
-
const when = (iso) => iso === null ? "never" :
|
|
5
|
+
const when = (iso) => iso === null ? "never" : whenUtc(iso);
|
|
5
6
|
export const MODELS_CSS = `.models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}@media(max-width:600px){.models .card{padding:14px}}`;
|
|
6
7
|
function toolRow(one, manage, csrf) {
|
|
7
8
|
const state = one.installed === null ? "Version unknown"
|
|
@@ -48,7 +49,7 @@ export function modelsHtml(view) {
|
|
|
48
49
|
`<button type="submit" class="secondary">${view.watch.enabled ? "Turn off" : "Turn on"}</button></form></details>`
|
|
49
50
|
: "";
|
|
50
51
|
return `<section class="models">${status}${problem}` +
|
|
51
|
-
`<h2>AI tools</h2>${tools}<p class="meta">Checked ${
|
|
52
|
+
`<h2>AI tools</h2>${tools}<p class="meta">Checked ${when(view.watch.checkedAt)} ${check}</p>` +
|
|
52
53
|
fresh +
|
|
53
54
|
`<h2>Default agents</h2><p class="meta">New tasks use these. Approved tasks keep the agents they were approved with.</p>` +
|
|
54
55
|
view.roles.map(role => roleForm(role, manage, view.csrf)).join("") +
|
package/dist/monitoring-ui.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { targetOf } from "./monitoring.js";
|
|
2
|
+
import { whenUtc } from "./when-html.js";
|
|
2
3
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3
|
-
const when = (at) =>
|
|
4
|
+
const when = (at) => whenUtc(at);
|
|
4
5
|
export const MONITORING_CSS = `.monitoring{max-width:720px;min-width:0}.monitoring fieldset{border:0;padding:0;margin:20px 0 0;min-width:0}` +
|
|
5
6
|
`.monitoring legend{font-weight:600;font-size:.9375rem;margin-bottom:6px}.monitoring label{display:grid;gap:4px;margin:10px 0 0;font-size:.875rem}` +
|
|
6
7
|
`.monitoring input[type=text],.monitoring input[type=url],.monitoring input[type=password]{width:100%;box-sizing:border-box}.monitoring .choice{display:flex;gap:8px;align-items:center}` +
|
package/dist/operate.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IntegrationIo } from "./integrations.js";
|
|
1
2
|
import { type OnboardIo } from "./agent-onboard.js";
|
|
2
3
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
3
4
|
import { type StepIo } from "./flow-steps.js";
|
|
@@ -55,6 +56,8 @@ export type OperateOptions = {
|
|
|
55
56
|
flowTriggerIo?: Partial<TriggerIo>;
|
|
56
57
|
/** Injected by tests: how flow check and update steps run commands and reach GitHub and Linear. */
|
|
57
58
|
flowStepIo?: Partial<StepIo>;
|
|
59
|
+
/** Injected by tests: how `integrations` checks reach services (fetch, gh, sign-in checks, mail servers). */
|
|
60
|
+
integrationIo?: Partial<IntegrationIo>;
|
|
58
61
|
/** Injected by tests: the mate's provider fetch, key environment, and stdin lines. */
|
|
59
62
|
mateSeams?: MateCliSeams;
|
|
60
63
|
/** Injected by tests: a held-session coordinator, so a `tick` exercises
|
|
@@ -72,7 +75,7 @@ export type OperateOptions = {
|
|
|
72
75
|
openBrowser?: (url: string) => void;
|
|
73
76
|
};
|
|
74
77
|
};
|
|
75
|
-
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection">>;
|
|
78
|
+
export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
|
|
76
79
|
export declare const OPERATE_HELP: string;
|
|
77
80
|
/** Parsed flags, with the positionals left over. */
|
|
78
81
|
type Args = {
|
|
@@ -89,7 +92,7 @@ type Args = {
|
|
|
89
92
|
* (surface.ts) is tested for exact equality against them.
|
|
90
93
|
*/
|
|
91
94
|
export declare const TASK_ACTIONS: readonly ["add", "list", "show", "wait", "state", "block", "unblock", "next", "steer", "assign", "reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan", "review", "accept", "repair", "route", "stop", "resume", "complete", "revise"];
|
|
92
|
-
export declare const PUBLISH_ACTIONS: readonly ["grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
95
|
+
export declare const PUBLISH_ACTIONS: readonly ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
93
96
|
export declare const CONFIG_ACTIONS: readonly ["show", "set", "clear"];
|
|
94
97
|
export declare const APPROVER_ACTIONS: readonly ["list", "add"];
|
|
95
98
|
export declare const ROUTINE_ACTIONS: readonly ["list", "add", "show", "approve", "refresh", "pause", "resume", "run-now"];
|
package/dist/operate.js
CHANGED
|
@@ -13,6 +13,7 @@ import { buildExport, exportSummary, exportZip, writeExportFolder } from "./expo
|
|
|
13
13
|
import { startBudgetAlerts } from "./budget-alerts.js";
|
|
14
14
|
import { liftAuthPause, openAuthPauses, signInGate, signInWords, startSignInProbes } from "./provider-auth.js";
|
|
15
15
|
import { createConnectionChecker } from "./provider-connection.js";
|
|
16
|
+
import { checkIntegrations, integrationsBrokenLine, integrationsNow, renderIntegrations } from "./integrations.js";
|
|
16
17
|
import { buildHandoff, handoffLines, loginAccount, runOnboard } from "./agent-onboard.js";
|
|
17
18
|
import { backupFiles, backupFolderOf, backupOwner, backupNow, restoreDatabase, startBackups } from "./backup.js";
|
|
18
19
|
import { pushLimitSink } from "./provider-limits.js";
|
|
@@ -67,7 +68,7 @@ import { runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
|
67
68
|
*/
|
|
68
69
|
import { homedir, hostname, tmpdir } from "node:os";
|
|
69
70
|
import { basename, dirname, join, resolve } from "node:path";
|
|
70
|
-
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, } from "./store.js";
|
|
71
|
+
import { openStore, openStoreNoMigrate, CONCURRENT_WRITER_WAIT_MS, isDatabaseBusy, databasePath, BUILT_IN, DEFAULT_ACTOR, parseCapabilityKey, verifiedAuthor, contestantProfileOf, isDigestTime, } from "./store.js";
|
|
71
72
|
import { randomBytes, randomInt, randomUUID } from "node:crypto";
|
|
72
73
|
import { authorizePlanUnderMode } from "./plan-auto.js";
|
|
73
74
|
import { ghDispatchAdapter, mirrorTaskId, syncPass } from "./sync.js";
|
|
@@ -136,6 +137,8 @@ import { verifyApproverByPassword } from "./principal.js";
|
|
|
136
137
|
import { authorizedProject, canonicalProject, projectName, resolveCeiling } from "./project.js";
|
|
137
138
|
import { tally, spendLine } from "./summary.js";
|
|
138
139
|
import { bodyHashOf, describePublicationGrant, observeChecks, publicationBody, publishPass, sweepMerges, } from "./publish.js";
|
|
140
|
+
import { checkPublishing, publishingOf, savePublishing, followPullRequests } from "./pull-request-flow.js";
|
|
141
|
+
import { starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
139
142
|
import { acquire, acquireFallback, acquireIfReady, completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, finalizePlanFenced, finalizePlanFailureFenced, finalizeScoutFenced, finalizeScoutFailureFenced, heartbeat, release, reap, currentClaim, DEFAULT_LEASE_MS, SYNC_MAX_AGE_MS, acquireContinuation, } from "./claim.js";
|
|
140
143
|
import { disposeBuildOutcome, holdStaleApproval, regateTask } from "./dispose.js";
|
|
141
144
|
import { attendedLivenessState } from "./liveness.js";
|
|
@@ -213,11 +216,14 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
213
216
|
toolroll storage clean [--yes] preview removing finished tasks' clean checkouts (their branches stay); --yes removes them
|
|
214
217
|
toolroll storage cleanup <when> remove a finished task's clean checkout: finished|2d|7d|never (instance operator)
|
|
215
218
|
toolroll storage discard <path> --yes throw away a checkout kept for its changes; its branch stays
|
|
219
|
+
toolroll integrations [--json] which integrations work: Connected, Not set up or Broken, and what to do (--saved: no new checks)
|
|
216
220
|
toolroll monitoring where the audit stream and traces go, and how each destination is doing
|
|
217
221
|
toolroll check-progress <run> current or final approved check progress
|
|
218
222
|
toolroll spend [--month YYYY-MM] [--csv] what agent work cost, by project, person, teammate and model
|
|
219
223
|
toolroll export --out <path> [--zip] everything Toolroll knows, in a folder or .zip (no secrets)
|
|
220
224
|
toolroll budget list|set|remove monthly budgets (--all | --project <p> | --person <name> | --teammate <id>) --usd <n> [--alerts-only]
|
|
225
|
+
toolroll notifications [quiet|all] how chats reach you: only when you're needed (the default), or every step
|
|
226
|
+
toolroll notifications digest <HH:MM>|off one evening message: what finished, what waits, what failed
|
|
221
227
|
toolroll retention show|preview how long evidence, checkout records, chat and notifications are kept; what the daily sweep would remove
|
|
222
228
|
toolroll retention set <kind> <period> evidence|checkouts|chat|notifications, 30d|1y|forever (instance operator)
|
|
223
229
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
@@ -264,6 +270,10 @@ External trackers — build what a tracker nominates, under local approvals
|
|
|
264
270
|
the dispatch grant: its own explicit
|
|
265
271
|
yes, never in any default; writes a
|
|
266
272
|
plane marker label to the repository
|
|
273
|
+
toolroll publish setup [--repo <path>] [--merge-method squash|merge|rebase]
|
|
274
|
+
[--merge-when-green] --yes one-step pull requests: checks gh sign-in
|
|
275
|
+
and push rights, then Complete offers
|
|
276
|
+
"Complete and open a pull request"
|
|
267
277
|
toolroll publish grant --github <owner/name> --allow-merge
|
|
268
278
|
--merge-method squash|merge|rebase [--merge-delete-branch] --yes
|
|
269
279
|
auto-merge this plane's own PRs —
|
|
@@ -532,7 +542,7 @@ export const TASK_ACTIONS = [
|
|
|
532
542
|
"reopen", "scope", "approve", "hold", "unhold", "require", "requeue", "regate", "plan",
|
|
533
543
|
"review", "accept", "repair", "route", "stop", "resume", "complete", "revise",
|
|
534
544
|
];
|
|
535
|
-
export const PUBLISH_ACTIONS = ["grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
545
|
+
export const PUBLISH_ACTIONS = ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
536
546
|
export const CONFIG_ACTIONS = ["show", "set", "clear"];
|
|
537
547
|
export const APPROVER_ACTIONS = ["list", "add"];
|
|
538
548
|
export const ROUTINE_ACTIONS = ["list", "add", "show", "approve", "refresh", "pause", "resume", "run-now"];
|
|
@@ -567,6 +577,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
567
577
|
"label", "reviewers", "limit", "role", "key-file", "weekly-usd", "daily-turns", "per-hour", "token-file", "race", "compare", "race-per-usd", "race-total-usd", "race-count", "race-agents", "budget-usd", "build-usd", "sync-max-age", "merge-method",
|
|
568
578
|
"phase", "risk", "tier", "clear-phase",
|
|
569
579
|
"run", "containment", "agent",
|
|
580
|
+
// onboard: the starter flows to switch on.
|
|
581
|
+
"starter",
|
|
570
582
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
571
583
|
]);
|
|
572
584
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
@@ -577,6 +589,8 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
577
589
|
"html", "csv", "alerts-only",
|
|
578
590
|
// v105: the full export.
|
|
579
591
|
"zip",
|
|
592
|
+
// Settings → Integrations: the last checks, without checking again.
|
|
593
|
+
"saved",
|
|
580
594
|
]);
|
|
581
595
|
export function parseOperateArgs(argv) {
|
|
582
596
|
const positional = [];
|
|
@@ -720,6 +734,7 @@ export async function runOperate(command, argv, write, options = {}) {
|
|
|
720
734
|
...(options.dispatchAdapter === undefined ? {} : { dispatchAdapter: options.dispatchAdapter }),
|
|
721
735
|
...(options.flowTriggerIo === undefined ? {} : { flowTriggerIo: options.flowTriggerIo }),
|
|
722
736
|
...(options.flowStepIo === undefined ? {} : { flowStepIo: options.flowStepIo }),
|
|
737
|
+
...(options.integrationIo === undefined ? {} : { integrationIo: options.integrationIo }),
|
|
723
738
|
...(options.shouldStop === undefined ? {} : { shouldStop: options.shouldStop }),
|
|
724
739
|
...(options.mateSeams === undefined ? {} : { mateSeams: options.mateSeams }),
|
|
725
740
|
...(options.heldCoordinator === undefined ? {} : { heldCoordinator: options.heldCoordinator }),
|
|
@@ -817,6 +832,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
817
832
|
return budgetCommand(positional, flags, context);
|
|
818
833
|
case "retention":
|
|
819
834
|
return retentionCommand(positional, flags, context);
|
|
835
|
+
case "notifications":
|
|
836
|
+
return notificationsCommand(positional, flags, context);
|
|
820
837
|
case "backup":
|
|
821
838
|
return backupCommand(positional, flags, context);
|
|
822
839
|
case "export":
|
|
@@ -825,6 +842,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
825
842
|
return policyCommand(positional, flags, context);
|
|
826
843
|
case "storage":
|
|
827
844
|
return storageCommand(positional, flags, context);
|
|
845
|
+
case "integrations":
|
|
846
|
+
return integrationsCommand(positional, flags, context);
|
|
828
847
|
case "monitoring": {
|
|
829
848
|
// Where the audit stream and traces go, and how each destination is doing. Settings → Monitoring changes it.
|
|
830
849
|
const settings = readMonitoring(dirname(context.databaseFile));
|
|
@@ -7230,6 +7249,14 @@ async function runWatchLoop(args) {
|
|
|
7230
7249
|
});
|
|
7231
7250
|
if (checks.failing > 0)
|
|
7232
7251
|
progress(`watch: CI is red on ${checks.failing} published PR(s) — the outbox has it`);
|
|
7252
|
+
const followed = await followPullRequests(store, checks.seen, {
|
|
7253
|
+
evidenceRoot: context.evidenceRoot,
|
|
7254
|
+
...(context.publishExec === undefined ? {} : { exec: context.publishExec }),
|
|
7255
|
+
});
|
|
7256
|
+
if (followed.revisions > 0)
|
|
7257
|
+
progress(`watch: filed ${followed.revisions} CI revision(s)`);
|
|
7258
|
+
if (followed.merged > 0)
|
|
7259
|
+
progress(`watch: merged ${followed.merged} pull request(s) whose checks passed`);
|
|
7233
7260
|
}
|
|
7234
7261
|
catch (error) {
|
|
7235
7262
|
progress(`watch: CI observation failed — ${describe(error)}; retrying on its next interval`);
|
|
@@ -7710,7 +7737,7 @@ async function onboardCommand(flags, context) {
|
|
|
7710
7737
|
const gitRun = context.gitRunner ?? ((file, args, opts) => run(file, [...args], { ...(opts?.cwd === undefined ? {} : { cwd: opts.cwd }), timeoutMs: 10_000 }));
|
|
7711
7738
|
const registryPath = registryPathOf(context);
|
|
7712
7739
|
const checker = createConnectionChecker({ home, env });
|
|
7713
|
-
|
|
7740
|
+
const code = await runOnboard({
|
|
7714
7741
|
write,
|
|
7715
7742
|
json,
|
|
7716
7743
|
yes: flag(flags, "yes"),
|
|
@@ -7750,7 +7777,38 @@ async function onboardCommand(flags, context) {
|
|
|
7750
7777
|
return { ok: true, added: "error" in before || !before.repos.includes(repo) };
|
|
7751
7778
|
},
|
|
7752
7779
|
checkConnection: seams.checkConnection ?? (agent => checker(agent)),
|
|
7780
|
+
pullRequests: seams.pullRequests ?? (async (repo) => {
|
|
7781
|
+
const publishing = publishingOf(store, repo);
|
|
7782
|
+
if (publishing.on)
|
|
7783
|
+
return `on for ${publishing.githubRepo} into ${publishing.base}`;
|
|
7784
|
+
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
7785
|
+
if (!checked.ok)
|
|
7786
|
+
return `off: ${checked.message}`;
|
|
7787
|
+
return `off: turn on with toolroll publish setup --repo ${repo} --yes (asks your password), or in Settings → Projects`;
|
|
7788
|
+
}),
|
|
7789
|
+
starterFlag: text(flags, "starter"),
|
|
7790
|
+
// Starter flows belong to a person: the flow's decisions ask them. With no account yet, none can be switched on.
|
|
7791
|
+
starters: {
|
|
7792
|
+
list: async (repo) => {
|
|
7793
|
+
const views = startersFor(store, repo);
|
|
7794
|
+
return store.listApprovers().length > 0 ? views : views.map(one => ({ ...one, blocked: one.blocked ?? "Needs your account first: run toolroll up, then onboard again." }));
|
|
7795
|
+
},
|
|
7796
|
+
switchOn: async (repo, id) => {
|
|
7797
|
+
const owner = onboardLogin(store, loginFile)?.account ?? store.listApprovers()[0]?.name ?? null;
|
|
7798
|
+
const starter = starterOf(id);
|
|
7799
|
+
if (owner === null || starter === null)
|
|
7800
|
+
return { ok: false, said: "Needs your account first: run toolroll up, then onboard again." };
|
|
7801
|
+
return switchOnStarter(store, starter, repo, owner, clock(), dirname(context.databaseFile));
|
|
7802
|
+
},
|
|
7803
|
+
},
|
|
7753
7804
|
});
|
|
7805
|
+
// One line when an integration is Broken, from the last checks only (never a new check here).
|
|
7806
|
+
if (!json) {
|
|
7807
|
+
const line = integrationsBrokenLine(await savedIntegrations(context).catch(() => []));
|
|
7808
|
+
if (line !== null)
|
|
7809
|
+
write(line);
|
|
7810
|
+
}
|
|
7811
|
+
return code;
|
|
7754
7812
|
}
|
|
7755
7813
|
async function upCommand(flags, context) {
|
|
7756
7814
|
const { store, write, json, clock } = context;
|
|
@@ -8539,6 +8597,42 @@ async function publishCommand(positional, flags, context) {
|
|
|
8539
8597
|
if (action !== undefined && !PUBLISH_ACTIONS.includes(action)) {
|
|
8540
8598
|
return fail(write, json, "publish", "usage", `unknown \`publish ${action}\` — try ${PUBLISH_ACTIONS.join(", ")}, or bare \`publish\` for the publication pass`, EXIT.usage);
|
|
8541
8599
|
}
|
|
8600
|
+
if (action === "setup") {
|
|
8601
|
+
const mergeMethod = text(flags, "merge-method") ?? "squash";
|
|
8602
|
+
if (!["squash", "merge", "rebase"].includes(mergeMethod)) {
|
|
8603
|
+
return fail(write, json, "publish setup", "usage", "--merge-method is squash, merge or rebase", EXIT.usage);
|
|
8604
|
+
}
|
|
8605
|
+
const checked = await checkPublishing(repo, context.publishExec === undefined ? {} : { exec: context.publishExec });
|
|
8606
|
+
if (!checked.ok)
|
|
8607
|
+
return fail(write, json, "publish setup", checked.reason, checked.message, EXIT.refused);
|
|
8608
|
+
const settings = { mergeMethod: mergeMethod, mergeWhenGreen: flags.has("merge-when-green") };
|
|
8609
|
+
const plan = checked.plan;
|
|
8610
|
+
const terms = [
|
|
8611
|
+
`Pull requests for ${plan.githubRepo} into ${plan.base}${plan.account === null ? "" : `, as ${plan.account}`}.`,
|
|
8612
|
+
"Complete will offer \"Complete and open a pull request\"; the pull request opens from the exact completed commit.",
|
|
8613
|
+
`Merges ${settings.mergeMethod} and delete the branch${settings.mergeWhenGreen ? ", by themselves once checks pass" : " when a person selects Merge"}.`,
|
|
8614
|
+
];
|
|
8615
|
+
if (!flags.has("yes")) {
|
|
8616
|
+
if (json) {
|
|
8617
|
+
write(envelopeJson({ ok: false, command: "publish setup", reason: "unconfirmed", proposed: { ...plan, ...settings } }));
|
|
8618
|
+
return EXIT.refused;
|
|
8619
|
+
}
|
|
8620
|
+
for (const line of terms)
|
|
8621
|
+
write(line);
|
|
8622
|
+
write("Nothing is set up yet. Repeat with --yes --as <you> --token <password> to turn it on.");
|
|
8623
|
+
return EXIT.refused;
|
|
8624
|
+
}
|
|
8625
|
+
const acting = await askCredentials(flags, context);
|
|
8626
|
+
if (acting === null) {
|
|
8627
|
+
return fail(write, json, "publish setup", "usage", "turning on pull requests takes --as <you> --token <your password>", EXIT.usage);
|
|
8628
|
+
}
|
|
8629
|
+
const authenticated = authenticateApprover(store, acting.name, acting.token, repo);
|
|
8630
|
+
if (!authenticated.ok) {
|
|
8631
|
+
return fail(write, json, "publish setup", authenticated.reason, describeApproveFailure(authenticated.reason, acting.name), EXIT.refused);
|
|
8632
|
+
}
|
|
8633
|
+
savePublishing(store, plan, acting.name, settings, clock());
|
|
8634
|
+
return succeed(write, json, "publish setup", { publishing: publishingOf(store, repo) }, () => ["Pull requests are on.", ...terms]);
|
|
8635
|
+
}
|
|
8542
8636
|
if (action === "grant") {
|
|
8543
8637
|
const github = text(flags, "github");
|
|
8544
8638
|
if (github === undefined || !/^[\w.-]+\/[\w.-]+$/.test(github)) {
|
|
@@ -9101,7 +9195,49 @@ async function statusCommand(positional, flags, context) {
|
|
|
9101
9195
|
notifySecurityRelease(context.store, release, current, method.updateCommand, context.clock());
|
|
9102
9196
|
const line = updateLine(release, current, method.updateCommand);
|
|
9103
9197
|
const update = release === null ? {} : { update: { current, latest: release.version, security: release.security, updateCommand: method.updateCommand, url: release.url } };
|
|
9104
|
-
|
|
9198
|
+
// One line when an integration is Broken, from the last checks only: status never waits on a check.
|
|
9199
|
+
const brokenIntegrations = await savedIntegrations(context).then(list => list.filter(one => one.state === "broken"), () => []);
|
|
9200
|
+
const brokenLine = integrationsBrokenLine(brokenIntegrations);
|
|
9201
|
+
const integrations = brokenIntegrations.length === 0 ? {} : { integrations: { broken: brokenIntegrations.map(one => ({ key: one.key, name: one.name, fix: one.action.kind === "fix" ? one.action.words : null })) } };
|
|
9202
|
+
return succeed(context.write, context.json, command, { ...status, ...update, ...integrations }, () => [...renderInstallationStatus(status), ...(line === null ? [] : [line]), ...(brokenLine === null ? [] : [brokenLine])]);
|
|
9203
|
+
}
|
|
9204
|
+
/** Where `integrations` looks: the installation's files beside the database, and every project it knows. */
|
|
9205
|
+
async function integrationIoFor(context) {
|
|
9206
|
+
const registered = await loadProjectRegistry(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
9207
|
+
const repos = [...new Set([...("error" in registered ? [] : registered.repos), ...context.store.listProjects().map(one => one.path)])];
|
|
9208
|
+
const seams = context.integrationIo ?? {};
|
|
9209
|
+
return {
|
|
9210
|
+
store: context.store,
|
|
9211
|
+
dir: dirname(context.databaseFile),
|
|
9212
|
+
telegramTokenFile: context.telegramTokenFile,
|
|
9213
|
+
env: process.env,
|
|
9214
|
+
repos,
|
|
9215
|
+
gh: (file, args, options) => run(file, [...args], options),
|
|
9216
|
+
checkConnection: createConnectionChecker(),
|
|
9217
|
+
clock: context.clock,
|
|
9218
|
+
...seams,
|
|
9219
|
+
};
|
|
9220
|
+
}
|
|
9221
|
+
/** The integrations from files and the last checks: no network, for `status` and `onboard`. */
|
|
9222
|
+
async function savedIntegrations(context) {
|
|
9223
|
+
return integrationsNow(await integrationIoFor(context));
|
|
9224
|
+
}
|
|
9225
|
+
/** `toolroll integrations [--json]`: check every integration now and say which work. `--saved` skips the checks. */
|
|
9226
|
+
async function integrationsCommand(positional, flags, context) {
|
|
9227
|
+
const command = "integrations";
|
|
9228
|
+
const allowed = new Set(["db", "json", "saved"]);
|
|
9229
|
+
for (const name of flags.keys()) {
|
|
9230
|
+
if (!allowed.has(name))
|
|
9231
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not an integrations option.`, EXIT.usage);
|
|
9232
|
+
}
|
|
9233
|
+
if (positional.length > 0)
|
|
9234
|
+
return fail(context.write, context.json, command, "usage", "Use `toolroll integrations [--saved] [--json]`.", EXIT.usage);
|
|
9235
|
+
const io = await integrationIoFor(context);
|
|
9236
|
+
const list = flag(flags, "saved") ? integrationsNow(io) : await checkIntegrations(io);
|
|
9237
|
+
const counts = { connected: 0, "not-set-up": 0, broken: 0 };
|
|
9238
|
+
for (const one of list)
|
|
9239
|
+
counts[one.state] += 1;
|
|
9240
|
+
return succeed(context.write, context.json, command, { integrations: list, counts }, () => renderIntegrations(list));
|
|
9105
9241
|
}
|
|
9106
9242
|
async function waitTask(positional, flags, context) {
|
|
9107
9243
|
const command = "task wait";
|
|
@@ -11270,6 +11406,35 @@ async function budgetCommand(positional, flags, context) {
|
|
|
11270
11406
|
const saved = store.setBudget({ scope: target.scope, key: target.key, limitMicrousd: Math.round(dollars) * 1_000_000, hardStop: !flags.has("alerts-only") }, acting.name, context.clock());
|
|
11271
11407
|
return succeed(context.write, context.json, command, { budget: saved }, () => [`${budgetLabel(saved).replace(/'s$/, "")}: ${spendUsd(saved.limitMicrousd)} a month${saved.hardStop ? ", API work stops at 100%" : ", alerts only"}.`]);
|
|
11272
11408
|
}
|
|
11409
|
+
/** `notifications [quiet | all | digest <HH:MM> | digest off]`: how chats reach this person — only when they are
|
|
11410
|
+
* needed (the default), every step, and an optional evening digest. Each person sets their own. */
|
|
11411
|
+
async function notificationsCommand(positional, flags, context) {
|
|
11412
|
+
const [action, value, ...extra] = positional;
|
|
11413
|
+
const command = `notifications${action === undefined ? "" : ` ${action}`}`;
|
|
11414
|
+
const store = context.store;
|
|
11415
|
+
for (const name of flags.keys())
|
|
11416
|
+
if (!["as", "token", "token-file", "token-env", "db", "json"].includes(name))
|
|
11417
|
+
return fail(context.write, context.json, command, "usage", `--${name} is not a notifications option.`, EXIT.usage);
|
|
11418
|
+
const usage = "Use notifications quiet, notifications all, or notifications digest <HH:MM>|off.";
|
|
11419
|
+
if (extra.length > 0 || (action !== undefined && !["quiet", "all", "digest"].includes(action)) || (action === "digest") !== (value !== undefined)) {
|
|
11420
|
+
return fail(context.write, context.json, command, "usage", usage, EXIT.usage);
|
|
11421
|
+
}
|
|
11422
|
+
if (action === "digest" && value !== "off" && !isDigestTime(value))
|
|
11423
|
+
return fail(context.write, context.json, command, "usage", "The digest time is HH:MM on a 24-hour clock, like 18:30, or off.", EXIT.usage);
|
|
11424
|
+
const acting = await askCredentials(flags, context);
|
|
11425
|
+
const verified = acting === null ? null : authenticateApprover(store, acting.name, acting.token);
|
|
11426
|
+
if (acting === null || verified === null || !verified.ok) {
|
|
11427
|
+
return fail(context.write, context.json, command, "refused", "Notifications are set per person: pass --as and --token (or use the remembered login).", EXIT.refused);
|
|
11428
|
+
}
|
|
11429
|
+
const now = context.clock();
|
|
11430
|
+
const preference = action === "quiet" || action === "all" ? store.setNotificationPreference(acting.name, { mode: action }, acting.name, now)
|
|
11431
|
+
: action === "digest" ? store.setNotificationPreference(acting.name, { digestAt: value === "off" ? null : value }, acting.name, now)
|
|
11432
|
+
: store.notificationPreference(acting.name);
|
|
11433
|
+
return succeed(context.write, context.json, command, { mode: preference.mode, digestAt: preference.digestAt }, () => [
|
|
11434
|
+
preference.mode === "quiet" ? "Only when you're needed: one message per task, updated as it moves, and a new one when something needs you." : "Every step: a message for each update.",
|
|
11435
|
+
preference.digestAt === null ? "No evening digest." : `Evening digest at ${preference.digestAt}: what finished, what waits and what failed.`,
|
|
11436
|
+
]);
|
|
11437
|
+
}
|
|
11273
11438
|
/** `retention show | preview | set <kind> <period>` (v105): how long each kind of data is kept, and what the daily
|
|
11274
11439
|
* sweep would remove now. Setting takes an instance operator. */
|
|
11275
11440
|
async function retentionCommand(positional, flags, context) {
|
package/dist/policy-ui.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { whenUtc } from "./when-html.js";
|
|
1
2
|
import { LEVEL_HINTS, LEVEL_NAMES, PERMISSION_LEVELS, POLICY_PROVIDERS, PROVIDER_NAMES, policyParts } from "./policy.js";
|
|
2
3
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3
4
|
export const POLICY_CSS = `.policy{max-width:720px;min-width:0}.policy dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:6px 16px;margin:12px 0 0;font-size:.875rem}` +
|
|
@@ -13,7 +14,7 @@ function historyItem(entry) {
|
|
|
13
14
|
const what = entry.action.replace(/^organisation policy: /, "");
|
|
14
15
|
const [before, after] = (entry.detail ?? "").split(" → ");
|
|
15
16
|
return `<li><span class="what">${e(what.charAt(0).toUpperCase() + what.slice(1))}: ${e(before ?? "")} → <strong>${e(after ?? "")}</strong></span>` +
|
|
16
|
-
`<span class="meta">${e(entry.actor)} ·
|
|
17
|
+
`<span class="meta">${e(entry.actor)} · ${whenUtc(entry.at)}</span></li>`;
|
|
17
18
|
}
|
|
18
19
|
export function policyHtml(view, csrf, notice) {
|
|
19
20
|
const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
|
package/dist/project-delete.js
CHANGED
|
@@ -231,6 +231,7 @@ function deleteRows(store, repo, d, now) {
|
|
|
231
231
|
del("telegram_conversation_part", `${IN("conversation")} OR ${IN("source_run")} OR ${IN("task_id")}`, conversations, R, K);
|
|
232
232
|
del("telegram_conversation", IN("id"), conversations);
|
|
233
233
|
del("telegram_outbound_message", `project = ? OR ${IN("notification")} OR ${IN("task_ref")} OR ${IN("source_run")}`, repo, notifications, T, R);
|
|
234
|
+
del("chat_card_task", IN("task_ref"), T);
|
|
234
235
|
del("push_delivery", IN("notification"), notifications);
|
|
235
236
|
del("notification_delivery", IN("notification"), notifications);
|
|
236
237
|
del("notification", IN("id"), notifications);
|
package/dist/publish.d.ts
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* @-mention in its conclusion cannot acquire GitHub semantics.
|
|
21
21
|
*/
|
|
22
22
|
import { type ExecResult, type RunOptions } from "./exec.js";
|
|
23
|
+
import { summarizeChecks } from "./pulls.js";
|
|
23
24
|
import type { Publication, PublicationGrant, Store } from "./store.js";
|
|
24
25
|
export declare const BODY_TEMPLATE_VERSION = 2;
|
|
25
26
|
/** After this many failed attempts a publication stops retrying and pages instead. */
|
|
@@ -69,6 +70,16 @@ export type CheckReport = {
|
|
|
69
70
|
failing: number;
|
|
70
71
|
resolved: number;
|
|
71
72
|
problems: string[];
|
|
73
|
+
/** Every observation that won its settle, for the pull-request follower (CI revisions, Ready to merge). */
|
|
74
|
+
seen: ObservedPullRequest[];
|
|
75
|
+
};
|
|
76
|
+
export type ObservedPullRequest = {
|
|
77
|
+
publication: Publication;
|
|
78
|
+
headOid: string;
|
|
79
|
+
state: ReturnType<typeof summarizeChecks>;
|
|
80
|
+
remoteState: "MERGED" | "CLOSED" | null;
|
|
81
|
+
rollup: unknown;
|
|
82
|
+
mergeCommit: string | null;
|
|
72
83
|
};
|
|
73
84
|
/**
|
|
74
85
|
* Read CI for every PR this control plane opened — structured rollup and the
|
package/dist/publish.js
CHANGED
|
@@ -254,7 +254,7 @@ async function openOrAdopt(store, publication, grant, exec, clock) {
|
|
|
254
254
|
export async function observeChecks(store, options) {
|
|
255
255
|
const clock = options.clock ?? (() => new Date());
|
|
256
256
|
const exec = options.exec ?? execRun;
|
|
257
|
-
const report = { observed: 0, failing: 0, resolved: 0, problems: [] };
|
|
257
|
+
const report = { observed: 0, failing: 0, resolved: 0, problems: [], seen: [] };
|
|
258
258
|
for (const publication of store.openedPublications()) {
|
|
259
259
|
if (publication.prNumber === null)
|
|
260
260
|
continue;
|
|
@@ -266,7 +266,7 @@ export async function observeChecks(store, options) {
|
|
|
266
266
|
const viewed = await exec("gh", [
|
|
267
267
|
"pr", "view", String(publication.prNumber),
|
|
268
268
|
"--repo", publication.githubRepo,
|
|
269
|
-
"--json", "statusCheckRollup,headRefOid,state",
|
|
269
|
+
"--json", "statusCheckRollup,headRefOid,state,mergeCommit",
|
|
270
270
|
], { timeoutMs: EXEC_TIMEOUT_MS });
|
|
271
271
|
if (viewed.code !== 0) {
|
|
272
272
|
report.problems.push(`PR #${publication.prNumber}: not read — ${viewed.notFound ? "gh is not on PATH" : firstLine(viewed.stderr) || `exit ${viewed.code}`}`);
|
|
@@ -290,11 +290,15 @@ export async function observeChecks(store, options) {
|
|
|
290
290
|
// remote state is recorded, its episodes resolve, and neither the
|
|
291
291
|
// review queue nor the next observation pass carries it forever.
|
|
292
292
|
const remoteState = typeof payload.state === "string" ? payload.state.toUpperCase() : null;
|
|
293
|
+
const mergeCommit = typeof payload.mergeCommit === "object" && payload.mergeCommit !== null && typeof payload.mergeCommit.oid === "string"
|
|
294
|
+
? payload.mergeCommit.oid : null;
|
|
293
295
|
if (remoteState === "MERGED" || remoteState === "CLOSED") {
|
|
296
|
+
let won = false;
|
|
294
297
|
store.transact(() => {
|
|
295
298
|
const settled = store.settleObservation({ githubRepo: publication.githubRepo, prNumber: publication.prNumber, headSha: headOid, state: "none", generation }, clock());
|
|
296
299
|
if (!settled.won)
|
|
297
300
|
return;
|
|
301
|
+
won = true;
|
|
298
302
|
store.recordPublicationRemoteState(publication.id, remoteState, clock());
|
|
299
303
|
report.resolved += store.resolveCiEpisodes(publication.githubRepo, publication.prNumber, null, clock());
|
|
300
304
|
// A closed PR moots its merge blocker and supersedes its intent —
|
|
@@ -311,6 +315,8 @@ export async function observeChecks(store, options) {
|
|
|
311
315
|
});
|
|
312
316
|
}
|
|
313
317
|
});
|
|
318
|
+
if (won)
|
|
319
|
+
report.seen.push({ publication, headOid, state: "none", remoteState, rollup: payload.statusCheckRollup, mergeCommit });
|
|
314
320
|
continue;
|
|
315
321
|
}
|
|
316
322
|
const state = summarizeChecks(payload.statusCheckRollup);
|
|
@@ -322,6 +328,7 @@ export async function observeChecks(store, options) {
|
|
|
322
328
|
continue;
|
|
323
329
|
}
|
|
324
330
|
store.recordPublicationCheckState(publication.id, state, clock());
|
|
331
|
+
report.seen.push({ publication, headOid, state, remoteState: null, rollup: payload.statusCheckRollup, mergeCommit });
|
|
325
332
|
// The episode identity carries the REPOSITORY (audit C-2): PR #55 in
|
|
326
333
|
// repo A and PR #55 in repo B are different worlds, and one's failure
|
|
327
334
|
// must never light the other's repair button.
|