toolroll 0.9.0 → 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 +18 -0
- package/dist/agent-onboard.d.ts +21 -1
- package/dist/agent-onboard.js +50 -1
- package/dist/backup-ui.js +4 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +12 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +43 -3
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/desktop-update.d.ts +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.js +9 -4
- 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.js +18 -0
- package/dist/policy-ui.js +2 -1
- package/dist/pull-request-flow.d.ts +32 -0
- package/dist/pull-request-flow.js +43 -8
- package/dist/pull-request-ui.js +2 -2
- package/dist/serve.js +108 -43
- package/dist/skills-ui.js +2 -1
- package/dist/store.js +2 -0
- package/dist/surface.js +1 -1
- package/dist/when-html.d.ts +8 -0
- package/dist/when-html.js +36 -0
- package/package.json +1 -1
package/dist/flows-ui.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* (the React view's data, plus a plain fallback page the view replaces). */
|
|
3
3
|
import type { BrowserFlowView } from "./browser-workspace.js";
|
|
4
4
|
import type { FlowRow, Store } from "./store.js";
|
|
5
|
-
export declare const FLOWS_CSS = ".flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}@media(max-width:600px){.flows input,.flows select{font-size:16px}}";
|
|
5
|
+
export declare const FLOWS_CSS = ".flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:\"\";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}";
|
|
6
6
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
7
7
|
export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], csrf: string, canCreate: boolean, problem: string | null): string;
|
|
8
8
|
/** One flow's canvas for one person. */
|
package/dist/flows-ui.js
CHANGED
|
@@ -13,7 +13,7 @@ import { callWords, receiptWords } from "./teammate-tools.js";
|
|
|
13
13
|
import { undoFor } from "./teammate-week.js";
|
|
14
14
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
15
15
|
const projectName = (repo) => repo.split(/[\\/]/).filter(Boolean).pop() ?? repo;
|
|
16
|
-
export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}@media(max-width:600px){.flows input,.flows select{font-size:16px}}`;
|
|
16
|
+
export const FLOWS_CSS = `.flows{max-width:880px;min-width:0}.flows .card{padding:16px 18px;margin:12px 0}.flows .flow-row{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap}.flows .flow-row h2{font-size:1.05rem;margin:0}.flows .flow-counts{font-size:.85rem;color:var(--so-muted)}.flows form{display:grid;gap:10px;margin:0}.flows label{display:grid;gap:6px}.flows input,.flows select{box-sizing:border-box;width:100%;max-width:100%}.flows button{justify-self:start;min-height:44px}.flows .flow-fallback ol{padding-left:20px}.flows summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.flows .flow-buttons{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0 0}@media(max-width:600px){.flows input,.flows select{font-size:16px}}@media(max-width:760px){.flows .card{padding:10px 12px;margin:8px 0;position:relative}.flows .flow-row{row-gap:2px}.flows .flow-row h2 a::after{content:"";position:absolute;inset:0;border-radius:inherit}.flows .flow-counts{line-height:1.35}}`;
|
|
17
17
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
18
18
|
export function flowsListHtml(store, flows, projects, csrf, canCreate, problem) {
|
|
19
19
|
const rows = flows.map(flow => {
|
|
@@ -71,11 +71,12 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
71
71
|
const decision = parseSortDecision(decisions.get(card.id)?.decisionJson ?? null);
|
|
72
72
|
const shown = stage?.kind === "approval" && definition !== null ? draftFor(definition, stage) : null;
|
|
73
73
|
// v91: when a Wait zone gives up (or moves on), or a zone's time limit comes.
|
|
74
|
-
|
|
74
|
+
// An "hours" wait has no deadline of its own: it goes on when the clock reaches its hours.
|
|
75
|
+
const clock = stage?.wait?.for === "hours" ? stage.limit : stage?.wait ?? stage?.limit;
|
|
75
76
|
const until = card.state !== "active" || stage === undefined || clock === undefined ? null : new Date(Date.parse(store.flowCardEnteredAt(card.id) ?? card.updatedAt) + clock.minutes * 60_000);
|
|
76
77
|
const deadline = until === null || until.getTime() <= Date.now() ? null : {
|
|
77
78
|
at: until.toISOString(),
|
|
78
|
-
label: stage.wait !== undefined ? (stage.wait.for === "reply" ? "No reply by" : "Moves on at") : stage.limit.to !== null ? `Moves to ${title(stage.limit.to)} at` : "Reminder at",
|
|
79
|
+
label: stage.wait !== undefined && stage.wait.for !== "hours" ? (stage.wait.for === "reply" ? "No reply by" : "Moves on at") : stage.limit.to !== null ? `Moves to ${title(stage.limit.to)} at` : "Reminder at",
|
|
79
80
|
};
|
|
80
81
|
// Its open question: one of its own, or (v94) a tool call waiting for approval.
|
|
81
82
|
const asked = card.state === "active" ? store.openTeammateQuestionOn(card.id, card.entry) : null;
|
package/dist/flows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const FLOW_STAGE_KINDS: readonly ["inbox", "task", "report", "approval", "check", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
1
|
+
export declare const FLOW_STAGE_KINDS: readonly ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
2
2
|
export type FlowStageKind = (typeof FLOW_STAGE_KINDS)[number];
|
|
3
3
|
export declare const FLOW_COLORS: readonly ["slate", "blue", "violet", "amber", "green", "rose"];
|
|
4
4
|
export type FlowColor = (typeof FLOW_COLORS)[number];
|
|
@@ -48,11 +48,18 @@ export type FlowTool = {
|
|
|
48
48
|
name: string;
|
|
49
49
|
args: string;
|
|
50
50
|
};
|
|
51
|
-
/** wait (v91): for a reply to the card's email (next: replied, onFail: no reply in time), or for a set time (then next).
|
|
51
|
+
/** wait (v91): for a reply to the card's email (next: replied, onFail: no reply in time), or for a set time (then next).
|
|
52
|
+
* "hours": until the clock is between `from` and `to` (like 22:00–06:00), in the computer's time zone unless one is named. */
|
|
52
53
|
export type FlowWait = {
|
|
53
|
-
for: "reply" | "time";
|
|
54
|
+
for: "reply" | "time" | "hours";
|
|
54
55
|
minutes: number;
|
|
56
|
+
from?: string;
|
|
57
|
+
to?: string;
|
|
58
|
+
timeZone?: string;
|
|
55
59
|
};
|
|
60
|
+
/** How a Pull request zone merges once checks pass. */
|
|
61
|
+
export declare const FLOW_MERGE_METHODS: readonly ["squash", "merge", "rebase"];
|
|
62
|
+
export type FlowMergeMethod = (typeof FLOW_MERGE_METHODS)[number];
|
|
56
63
|
/** A time limit on a zone (v91): after this long, the person it waits on is reminded, and a Holding or
|
|
57
64
|
* "Person decides" zone can move the card on (`to`). */
|
|
58
65
|
export type FlowLimit = {
|
|
@@ -102,6 +109,8 @@ export type FlowStage = {
|
|
|
102
109
|
teammate?: string;
|
|
103
110
|
/** v96: a "Teammate handles it" zone sends what the teammate writes back to whoever asked (the person who added the card, its chat thread, or the teammate's manager). */
|
|
104
111
|
reply?: boolean;
|
|
112
|
+
/** pull-request: merge once checks pass, this way. Only allowed after a "Person decides" zone, and a card merges only when a person approved it after it was built. */
|
|
113
|
+
merge?: FlowMergeMethod;
|
|
105
114
|
/** Where a card goes when this zone's step succeeds, and when it fails or is sent back (sort: when it isn't sure). */
|
|
106
115
|
next: string | null;
|
|
107
116
|
onFail: string | null;
|
|
@@ -132,12 +141,18 @@ export declare const LONGEST_WAIT_MINUTES: number;
|
|
|
132
141
|
export declare function durationMinutes(value: unknown): number | null;
|
|
133
142
|
/** Minutes in the words people use: "3 days", "1 day 4 hours", "45 minutes". */
|
|
134
143
|
export declare function durationWords(minutes: number): string;
|
|
144
|
+
/** "22:00", "9:30" as "HH:MM"; null when it isn't a time of day. */
|
|
145
|
+
export declare function clockTime(value: unknown): string | null;
|
|
146
|
+
/** Whether a time falls inside an "hours" wait (22:00–06:00 runs past midnight), in its time zone or this computer's. */
|
|
147
|
+
export declare function withinHours(wait: Pick<FlowWait, "from" | "to" | "timeZone">, now: Date): boolean;
|
|
135
148
|
/** Who decides at an approval zone: its named person, the flow's owner, or null for anyone who approves on the project. */
|
|
136
149
|
export declare function deciderOf(stage: Pick<FlowStage, "approver" | "toOwner">, flow: {
|
|
137
150
|
owner: string;
|
|
138
151
|
}): string | null;
|
|
139
152
|
/** A flow as drawn on the canvas, checked whole. Throws in plain words. */
|
|
140
153
|
export declare function validateFlowDefinition(input: unknown): FlowDefinition;
|
|
154
|
+
/** The zones a card can reach from `start` without passing through a zone `stop` picks (those zones aren't included). */
|
|
155
|
+
export declare function reachableWithout(stages: readonly FlowStage[], start: string, stop: (stage: FlowStage) => boolean): Set<string>;
|
|
141
156
|
/** What a card's work is held to: the zones' steps and paths, never where they sit on the canvas. */
|
|
142
157
|
export declare function flowDigest(definition: FlowDefinition): string;
|
|
143
158
|
/** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
|
|
@@ -194,10 +209,15 @@ export type FlowStepInput = {
|
|
|
194
209
|
server?: string;
|
|
195
210
|
tool?: string;
|
|
196
211
|
args?: Record<string, unknown> | string;
|
|
197
|
-
/** wait (v91): for a reply (the default) or a set time, and how long ("3 days"); where a card goes when no reply comes.
|
|
198
|
-
|
|
212
|
+
/** wait (v91): for a reply (the default) or a set time, and how long ("3 days"); where a card goes when no reply comes.
|
|
213
|
+
* "hours": until the clock is between `from` and `until` ("22:00", "06:00"). */
|
|
214
|
+
waitFor?: "reply" | "time" | "hours";
|
|
199
215
|
wait?: string | number;
|
|
200
216
|
ifNoReply?: string;
|
|
217
|
+
from?: string;
|
|
218
|
+
until?: string;
|
|
219
|
+
/** pull-request: merge once checks pass (true is squash). Needs a decision before it. */
|
|
220
|
+
merge?: boolean | FlowMergeMethod;
|
|
201
221
|
/** Any step but wait and done (v91): remind after this long ("2 days"; "none" removes it), and on holding and approval steps, move the card to this step then. */
|
|
202
222
|
remindAfter?: string | number;
|
|
203
223
|
thenMoveTo?: string;
|
|
@@ -220,10 +240,14 @@ export declare const DRAFT_DEFAULT = "Write a short, friendly reply to the perso
|
|
|
220
240
|
export declare function flowFromSteps(input: unknown, previous?: FlowDefinition | null): FlowDefinition;
|
|
221
241
|
/** What confirming a new or changed flow means, in the canvas's words: each step's job and path, what changed, and that the usual approvals still apply. */
|
|
222
242
|
export declare function flowTerms(definition: FlowDefinition, previous: FlowDefinition | null): string[];
|
|
223
|
-
/**
|
|
243
|
+
/** The label an issue gets to become work: what the Issues to PRs template and the issue starter flow watch for. */
|
|
244
|
+
export declare const ISSUE_LABEL = "toolroll";
|
|
245
|
+
/** Ready-made flows: the coding flow is the whole business process around a change. A template's `trigger`
|
|
246
|
+
* (a trigger's settings, as flow-triggers.ts reads them) is added with it. */
|
|
224
247
|
export declare const FLOW_TEMPLATES: readonly {
|
|
225
248
|
id: string;
|
|
226
249
|
label: string;
|
|
227
250
|
about: string;
|
|
228
251
|
definition: FlowDefinition;
|
|
252
|
+
trigger?: Record<string, unknown>;
|
|
229
253
|
}[];
|
package/dist/flows.js
CHANGED
|
@@ -30,14 +30,21 @@
|
|
|
30
30
|
* {{card.email}} is the first address the card mentions.
|
|
31
31
|
* - tool — (v87) calls one tool of one of the project's MCP servers
|
|
32
32
|
* (Settings → Tools), with arguments filled from the card.
|
|
33
|
+
* - pull-request — opens a pull request for the card's built result under
|
|
34
|
+
* the project's pull request setup (flow-pull-request.ts),
|
|
35
|
+
* waits for CI, and moves on when it passes or down the
|
|
36
|
+
* failure path, naming the failing check, when it fails. It
|
|
37
|
+
* can merge once checks pass, only after a person approved.
|
|
33
38
|
* - done — the end.
|
|
34
39
|
*
|
|
35
40
|
* The engine is deterministic and model-free: it runs in the worker's pass
|
|
36
41
|
* beside routines, and no card ever skips an approval the task itself needs.
|
|
37
42
|
*/
|
|
38
43
|
import { createHash } from "node:crypto";
|
|
39
|
-
export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
44
|
+
export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "done"];
|
|
40
45
|
export const FLOW_COLORS = ["slate", "blue", "violet", "amber", "green", "rose"];
|
|
46
|
+
/** How a Pull request zone merges once checks pass. */
|
|
47
|
+
export const FLOW_MERGE_METHODS = ["squash", "merge", "rebase"];
|
|
41
48
|
/** What each kind is called and does, in the words the canvas uses. */
|
|
42
49
|
export const FLOW_KIND_WORDS = {
|
|
43
50
|
inbox: { label: "Holding", about: "Cards wait here until someone moves them." },
|
|
@@ -45,6 +52,7 @@ export const FLOW_KIND_WORDS = {
|
|
|
45
52
|
report: { label: "Research", about: "An agent investigates and writes a report. No code changes." },
|
|
46
53
|
approval: { label: "Person decides", about: "Someone approves, or sends it back with a note." },
|
|
47
54
|
check: { label: "Run a script", about: "Runs one of the project's scripts (shell, Python or Node) with no AI. It gets the card; what it prints is passed on, and it can pick where the card goes next. If it fails, the card takes its failure path." },
|
|
55
|
+
"pull-request": { label: "Pull request", about: "Opens a pull request for the card's built result and waits for CI. Green moves it on; red takes the failure path with the failing check named. It can merge once checks pass, only after a person approved." },
|
|
48
56
|
update: { label: "Update where it came from", about: "Comments on the GitHub or Linear issue the card came from (and can close it), or answers in the chat thread it came from. Other cards pass straight through." },
|
|
49
57
|
notify: { label: "Message", about: "Posts a message to the project's chat, then moves on." },
|
|
50
58
|
request: { label: "Web request", about: "Calls an address on the web, like an API, with the card's details, and keeps what it answers. If it fails, the card takes its failure path." },
|
|
@@ -209,11 +217,42 @@ export function durationWords(minutes) {
|
|
|
209
217
|
/** A Wait zone's settings, checked. */
|
|
210
218
|
function validateWait(input, title) {
|
|
211
219
|
const raw = (input ?? {});
|
|
220
|
+
if (raw["for"] === "hours") {
|
|
221
|
+
const from = clockTime(raw["from"]), to = clockTime(raw["to"]);
|
|
222
|
+
if (from === null || to === null || from === to)
|
|
223
|
+
throw new Error(`Zone ${title}: say the hours it waits for, like 22:00 to 06:00.`);
|
|
224
|
+
const zone = typeof raw["timeZone"] === "string" && raw["timeZone"].trim() !== "" ? raw["timeZone"].trim() : null;
|
|
225
|
+
if (zone !== null && !knownTimeZone(zone))
|
|
226
|
+
throw new Error(`Zone ${title}: ${zone} isn't a time zone, like Europe/London.`);
|
|
227
|
+
return { for: "hours", minutes: 0, from, to, ...(zone === null ? {} : { timeZone: zone }) };
|
|
228
|
+
}
|
|
212
229
|
const minutes = durationMinutes(raw["minutes"]);
|
|
213
230
|
if (minutes === null)
|
|
214
231
|
throw new Error(`Zone ${title}: say how long it waits, from 1 minute to 30 days.`);
|
|
215
232
|
return { for: raw["for"] === "time" ? "time" : "reply", minutes };
|
|
216
233
|
}
|
|
234
|
+
/** "22:00", "9:30" as "HH:MM"; null when it isn't a time of day. */
|
|
235
|
+
export function clockTime(value) {
|
|
236
|
+
const match = typeof value === "string" ? /^\s*([01]?[0-9]|2[0-3]):([0-5][0-9])\s*$/.exec(value) : null;
|
|
237
|
+
return match === null ? null : `${match[1].padStart(2, "0")}:${match[2]}`;
|
|
238
|
+
}
|
|
239
|
+
function knownTimeZone(zone) {
|
|
240
|
+
try {
|
|
241
|
+
new Intl.DateTimeFormat("en-GB", { timeZone: zone });
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
/** Whether a time falls inside an "hours" wait (22:00–06:00 runs past midnight), in its time zone or this computer's. */
|
|
249
|
+
export function withinHours(wait, now) {
|
|
250
|
+
const parts = new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hourCycle: "h23", ...(wait.timeZone === undefined ? {} : { timeZone: wait.timeZone }) }).formatToParts(now);
|
|
251
|
+
const at = Number(parts.find(one => one.type === "hour")?.value ?? 0) * 60 + Number(parts.find(one => one.type === "minute")?.value ?? 0);
|
|
252
|
+
const minutesOf = (time) => { const [h = "0", m = "0"] = (time ?? "00:00").split(":"); return Number(h) * 60 + Number(m); };
|
|
253
|
+
const from = minutesOf(wait.from), to = minutesOf(wait.to);
|
|
254
|
+
return from < to ? at >= from && at < to : at >= from || at < to;
|
|
255
|
+
}
|
|
217
256
|
/** A zone's time limit, checked; null when it has none. Only Holding and "Person decides" zones move a card on. */
|
|
218
257
|
function validateLimit(input, kind, title) {
|
|
219
258
|
if (input === undefined || input === null)
|
|
@@ -270,6 +309,7 @@ export function validateFlowDefinition(input) {
|
|
|
270
309
|
...(kind === "email" ? { email: validateEmail(stage["email"], title) } : {}),
|
|
271
310
|
...(kind === "tool" ? { tool: validateTool(stage["tool"], title) } : {}),
|
|
272
311
|
...(kind === "wait" ? { wait: validateWait(stage["wait"], title) } : {}),
|
|
312
|
+
...(kind === "pull-request" && stage["merge"] !== undefined && stage["merge"] !== null && stage["merge"] !== false ? { merge: validateMerge(stage["merge"], title) } : {}),
|
|
273
313
|
...((kind === "approval" || kind === "teammate") && stage["teammate"] !== undefined && stage["teammate"] !== null && stage["teammate"] !== "" ? { teammate: validateTeammate(stage["teammate"], title) } : {}),
|
|
274
314
|
...(kind === "teammate" ? validateRoutes(stage, title) : {}),
|
|
275
315
|
...(kind === "teammate" && stage["reply"] === true ? { reply: true } : {}),
|
|
@@ -308,8 +348,36 @@ export function validateFlowDefinition(input) {
|
|
|
308
348
|
throw new Error(`Zone ${stage.title}: choose which teammate handles it.`);
|
|
309
349
|
}
|
|
310
350
|
const start = typeof raw.start === "string" && ids.has(raw.start) ? raw.start : stages[0].id;
|
|
351
|
+
// A zone that merges comes after a person's decision on every path to it: never merged without one.
|
|
352
|
+
const unapproved = reachableWithout(stages, start, one => one.kind === "approval");
|
|
353
|
+
for (const stage of stages) {
|
|
354
|
+
if (stage.merge !== undefined && unapproved.has(stage.id))
|
|
355
|
+
throw new Error(`Zone ${stage.title} merges, so a “Person decides” zone must come before it on every path.`);
|
|
356
|
+
}
|
|
311
357
|
return { version: 1, start, stages };
|
|
312
358
|
}
|
|
359
|
+
/** A Pull request zone's merge: true is squash, the default. */
|
|
360
|
+
function validateMerge(value, title) {
|
|
361
|
+
if (value === true)
|
|
362
|
+
return "squash";
|
|
363
|
+
if (typeof value === "string" && FLOW_MERGE_METHODS.includes(value))
|
|
364
|
+
return value;
|
|
365
|
+
throw new Error(`Zone ${title}: merge by squash, merge or rebase.`);
|
|
366
|
+
}
|
|
367
|
+
/** The zones a card can reach from `start` without passing through a zone `stop` picks (those zones aren't included). */
|
|
368
|
+
export function reachableWithout(stages, start, stop) {
|
|
369
|
+
const seen = new Set();
|
|
370
|
+
const queue = [start];
|
|
371
|
+
while (queue.length > 0) {
|
|
372
|
+
const id = queue.shift();
|
|
373
|
+
const stage = stages.find(one => one.id === id);
|
|
374
|
+
if (stage === undefined || seen.has(stage.id) || stop(stage))
|
|
375
|
+
continue;
|
|
376
|
+
seen.add(stage.id);
|
|
377
|
+
queue.push(...[stage.next, stage.onFail, stage.limit?.to ?? null, ...(stage.sort?.answers.map(one => one.to) ?? []), ...(stage.routes?.map(one => one.to) ?? [])].filter((one) => one !== null));
|
|
378
|
+
}
|
|
379
|
+
return seen;
|
|
380
|
+
}
|
|
313
381
|
/** A teammate's handle on a zone (v92): which teammate works it. Whether it exists is the project's to say, when a card arrives. */
|
|
314
382
|
function validateTeammate(value, title) {
|
|
315
383
|
if (typeof value !== "string" || !/^[a-z0-9][a-z0-9-]{0,31}$/.test(value))
|
|
@@ -387,13 +455,19 @@ function fillOne(key, stage, card) {
|
|
|
387
455
|
return stage === undefined ? "" : card.outputs[stage] ?? "";
|
|
388
456
|
}
|
|
389
457
|
}
|
|
390
|
-
const KIND_COLORS = { inbox: "slate", task: "blue", report: "violet", approval: "amber", check: "blue", update: "green", notify: "green", sort: "violet", draft: "violet", request: "blue", email: "green", tool: "blue", wait: "slate", teammate: "violet", done: "green" };
|
|
458
|
+
const KIND_COLORS = { inbox: "slate", task: "blue", report: "violet", approval: "amber", check: "blue", "pull-request": "blue", update: "green", notify: "green", sort: "violet", draft: "violet", request: "blue", email: "green", tool: "blue", wait: "slate", teammate: "violet", done: "green" };
|
|
391
459
|
/** A step id as the lead may write it (sort_by_hand, Sort-By-Hand) in the one form zones use. */
|
|
392
460
|
const idOf = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32);
|
|
393
461
|
const slugOf = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 28) || "zone";
|
|
394
462
|
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
|
|
395
463
|
/** A wait step's settings: what it waits for and how long (3 days when neither the step nor the zone it keeps says). */
|
|
396
464
|
function waitFromStep(step, old, title) {
|
|
465
|
+
if ((step.waitFor ?? old?.for) === "hours") {
|
|
466
|
+
const from = clockTime(step.from ?? old?.from ?? "22:00"), to = clockTime(step.until ?? old?.to ?? "06:00");
|
|
467
|
+
if (from === null || to === null)
|
|
468
|
+
throw new Error(`Step ${title}: say the hours it waits for, like from 22:00 until 06:00.`);
|
|
469
|
+
return { for: "hours", minutes: 0, from, to, ...(old?.timeZone === undefined ? {} : { timeZone: old.timeZone }) };
|
|
470
|
+
}
|
|
397
471
|
const minutes = step.wait === undefined ? old?.minutes ?? 3 * 24 * 60 : durationMinutes(step.wait);
|
|
398
472
|
if (minutes === null)
|
|
399
473
|
throw new Error(`Step ${title}: say how long it waits, like "3 days" or "4 hours" (up to 30 days).`);
|
|
@@ -479,7 +553,9 @@ export function flowFromSteps(input, previous = null) {
|
|
|
479
553
|
: kind === "wait" && typeof step.ifNoReply === "string" && step.ifNoReply.trim() !== "" ? step.ifNoReply : step.ifFails;
|
|
480
554
|
const onFail = kind === "done" ? null
|
|
481
555
|
: typeof notSure === "string" && notSure.trim() !== "" ? find(notSure, title)
|
|
482
|
-
: keptFail ?? (kind === "approval" ? worker?.id ?? (drafts[0].id === id ? null : drafts[0].id)
|
|
556
|
+
: keptFail ?? (kind === "approval" ? worker?.id ?? (drafts[0].id === id ? null : drafts[0].id)
|
|
557
|
+
// Red CI goes back to the build before it, carrying the failing check.
|
|
558
|
+
: kind === "pull-request" ? [...earlier].reverse().find(one => one.kind === "task")?.id ?? null : null);
|
|
483
559
|
// "owner": whoever owns the flow when the card arrives.
|
|
484
560
|
const toOwner = kind === "approval" && (step.decider === undefined ? old?.toOwner === true : typeof step.decider === "string" && /^(owner|the owner|flow owner|the flow owner|the flow's owner)$/i.test(step.decider.trim()));
|
|
485
561
|
const approver = kind !== "approval" || toOwner ? null
|
|
@@ -512,6 +588,7 @@ export function flowFromSteps(input, previous = null) {
|
|
|
512
588
|
...(kind === "email" ? { email: { to: step.to ?? old?.email?.to ?? "{{card.email}}", subject: step.subject ?? old?.email?.subject ?? "Re: {{card.title}}", body: step.body ?? old?.email?.body ?? "" } } : {}),
|
|
513
589
|
...(kind === "tool" ? { tool: { server: step.server ?? old?.tool?.server ?? "", name: step.tool ?? old?.tool?.name ?? "", args: typeof step.args === "string" ? step.args : step.args !== undefined ? JSON.stringify(step.args) : old?.tool?.args ?? "{}" } } : {}),
|
|
514
590
|
...(kind === "wait" ? { wait: waitFromStep(step, old?.wait ?? null, title) } : {}),
|
|
591
|
+
...(kind === "pull-request" && (step.merge === undefined ? old?.merge !== undefined : step.merge !== false) ? { merge: step.merge === undefined || step.merge === true || step.merge === false ? old?.merge ?? "squash" : step.merge } : {}),
|
|
515
592
|
// v92: "nobody" (or "none") takes a teammate off an approval step.
|
|
516
593
|
...((kind === "approval" || kind === "teammate") && (step.teammate === undefined ? old?.teammate !== undefined : !/^(nobody|none|no one)$/i.test(step.teammate.trim())) ? { teammate: idOf(step.teammate ?? old.teammate) } : {}),
|
|
517
594
|
...(kind === "teammate" ? step.routes !== undefined ? step.routes.length === 0 ? {} : { routes: step.routes.map(one => ({ answer: String(one.answer ?? "").trim(), to: find(String(one.goesTo ?? ""), title) })) } : old?.routes === undefined ? {} : { routes: old.routes } : {}),
|
|
@@ -641,11 +718,16 @@ export function flowTerms(definition, previous) {
|
|
|
641
718
|
if (stage.sort.notes.length > 0)
|
|
642
719
|
lines.push(`Also notes: ${stage.sort.notes.map(one => one.kind === "score" ? `${one.question} (${one.levels?.join(" / ")})` : `${one.question} (yes or no)`).join("; ")}`);
|
|
643
720
|
}
|
|
721
|
+
else if (stage.kind === "pull-request") {
|
|
722
|
+
const approval = [...definition.stages.slice(0, index)].reverse().find(one => one.kind === "approval");
|
|
723
|
+
lines.push("Opens a pull request for the card's built result, then waits for CI.", `Checks pass → ${stage.merge === undefined ? to(stage.next) : `merges it (${stage.merge}) and deletes its branch, only when a person approved it${approval === undefined ? "" : ` at ${approval.title}`} since it was built. Then → ${to(stage.next)}`}`, `Checks fail → ${stage.onFail === null ? "waits there, naming the failing check." : `${to(stage.onFail)} The failing check is named on the card.`}`);
|
|
724
|
+
}
|
|
644
725
|
else if (stage.kind === "check")
|
|
645
726
|
lines.push(`Runs the project's script “${stage.script}” with no AI.`, `Passes → ${to(stage.next)}${stage.onFail === null ? " Fails → waits there." : ` Fails → ${to(stage.onFail)}`}`);
|
|
646
727
|
else if (stage.kind === "wait" && stage.wait !== undefined)
|
|
647
|
-
lines.push(stage.wait.for === "
|
|
648
|
-
:
|
|
728
|
+
lines.push(stage.wait.for === "hours" ? `Waits until it's between ${stage.wait.from} and ${stage.wait.to}${stage.wait.timeZone === undefined ? "" : ` (${stage.wait.timeZone})`}; cards that arrive then go straight on. Then → ${to(stage.next)}`
|
|
729
|
+
: stage.wait.for === "time" ? `Waits ${durationWords(stage.wait.minutes)}. Then → ${to(stage.next)}`
|
|
730
|
+
: `Waits up to ${durationWords(stage.wait.minutes)} for a reply to the card's email, from someone it was sent to. Reply → ${to(stage.next)} No reply → ${stage.onFail === null ? "stays there for a person." : to(stage.onFail)}`);
|
|
649
731
|
else if (stage.next !== null)
|
|
650
732
|
lines.push(`Then → ${to(stage.next)}${stage.onFail === null ? "" : ` If it fails → ${to(stage.onFail)}`}`);
|
|
651
733
|
if (stage.limit !== undefined)
|
|
@@ -676,6 +758,8 @@ export function flowTerms(definition, previous) {
|
|
|
676
758
|
terms.push("Sort steps send each card's title, details and earlier notes to Jev through your OpenRouter account.");
|
|
677
759
|
if (definition.stages.some(one => one.kind === "request" || one.kind === "email" || one.kind === "tool"))
|
|
678
760
|
terms.push("Web request, email and tool steps send what they're given outside this computer, with no one checking unless a decision comes before them.");
|
|
761
|
+
if (definition.stages.some(one => one.kind === "pull-request"))
|
|
762
|
+
terms.push(`Pull request steps push the card's branch to GitHub and open a pull request under this project's pull request setup.${definition.stages.some(one => one.merge !== undefined) ? " A merge happens only after a person approves the card; nothing merges without one." : " Nothing merges on its own."}`);
|
|
679
763
|
if (definition.stages.some(one => one.kind === "draft"))
|
|
680
764
|
terms.push("Draft steps send each card's text to Claude through the lead chat's sign-in. Nothing a draft writes is sent until a later step sends it.");
|
|
681
765
|
if (definition.stages.some(one => one.teammate !== undefined))
|
|
@@ -684,7 +768,10 @@ export function flowTerms(definition, previous) {
|
|
|
684
768
|
}
|
|
685
769
|
const zone = (x, y, color, h = 300) => ({ x, y, w: 260, h, color });
|
|
686
770
|
const stage = (id, title, kind, at, rest = {}) => ({ id, title, kind, zone: at, instructions: null, planning: kind === "task" ? "auto" : null, approver: null, message: null, close: kind === "update" ? true : null, script: null, sort: null, next: null, onFail: null, ...rest });
|
|
687
|
-
/**
|
|
771
|
+
/** The label an issue gets to become work: what the Issues to PRs template and the issue starter flow watch for. */
|
|
772
|
+
export const ISSUE_LABEL = "toolroll";
|
|
773
|
+
/** Ready-made flows: the coding flow is the whole business process around a change. A template's `trigger`
|
|
774
|
+
* (a trigger's settings, as flow-triggers.ts reads them) is added with it. */
|
|
688
775
|
export const FLOW_TEMPLATES = [
|
|
689
776
|
{
|
|
690
777
|
id: "coding",
|
|
@@ -710,6 +797,23 @@ export const FLOW_TEMPLATES = [
|
|
|
710
797
|
],
|
|
711
798
|
},
|
|
712
799
|
},
|
|
800
|
+
{
|
|
801
|
+
id: "issues-to-prs",
|
|
802
|
+
label: "Issues to PRs",
|
|
803
|
+
about: `GitHub issues labelled “${ISSUE_LABEL}” are built, a person approves, a pull request opens and waits for CI, then the issue gets a comment and is closed. Nothing merges on its own.`,
|
|
804
|
+
trigger: { kind: "github", watch: "issues", label: ISSUE_LABEL },
|
|
805
|
+
definition: {
|
|
806
|
+
version: 1,
|
|
807
|
+
start: "build",
|
|
808
|
+
stages: [
|
|
809
|
+
stage("build", "Build", "task", zone(0, 0, "blue"), { instructions: "{{card.title}}\n\n{{card.description}}\n\nChanges asked for (if any): {{note}}", next: "approve" }),
|
|
810
|
+
stage("approve", "Approve", "approval", zone(300, 0, "amber"), { toOwner: true, next: "pull-request", onFail: "build" }),
|
|
811
|
+
stage("pull-request", "Pull request", "pull-request", zone(600, 0, "blue"), { next: "update-issue", onFail: "build" }),
|
|
812
|
+
stage("update-issue", "Update the issue", "update", zone(900, 0, "green", 220), { message: "Done: {{card.title}}. {{stage.pull-request}}", next: "done" }),
|
|
813
|
+
stage("done", "Done", "done", zone(900, 280, "green", 220)),
|
|
814
|
+
],
|
|
815
|
+
},
|
|
816
|
+
},
|
|
713
817
|
{
|
|
714
818
|
id: "research",
|
|
715
819
|
label: "Research flow",
|
package/dist/integrations-ui.js
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* render never waits on one (see integrations.ts).
|
|
6
6
|
*/
|
|
7
7
|
import { STATE_WORDS } from "./integrations.js";
|
|
8
|
+
import { whenUtc } from "./when-html.js";
|
|
8
9
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
9
|
-
const when = (at) =>
|
|
10
|
+
const when = (at) => whenUtc(at);
|
|
10
11
|
export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.integrations h2{margin:24px 0 6px;font-size:.9375rem}` +
|
|
11
12
|
`.integrations .integration{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:12px 0;border-top:1px solid var(--so-line);min-width:0}` +
|
|
12
|
-
`.integrations .integration>:not(.integration-action){grid-column:1}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
|
|
13
|
+
`.integrations .integration>:not(.integration-action),.integrations .integration-body>*{grid-column:1}.integrations .integration-body{display:contents}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
|
|
13
14
|
`.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
|
|
14
15
|
`.integrations .integration-action{grid-column:2;grid-row:1 / span 3;align-self:center;margin:0}.integrations .integration-action button,.integrations .integration-action .button-link{min-height:44px;white-space:nowrap}` +
|
|
15
16
|
`.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
|
|
@@ -18,7 +19,11 @@ export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.inte
|
|
|
18
19
|
`.integration-state i{width:6px;height:6px;border-radius:50%;background:currentColor}` +
|
|
19
20
|
`.integration-state--connected{color:var(--so-success);background:var(--so-success-soft)}.integration-state--broken{color:var(--so-danger);background:var(--so-danger-soft)}` +
|
|
20
21
|
`.integration-state--not-set-up,.integration-state--checking{color:var(--so-muted);background:var(--so-neutral-soft)}` +
|
|
21
|
-
|
|
22
|
+
// A phone: the action sits at the right of the name line, and the facts under it run on as one line.
|
|
23
|
+
`@media (max-width:760px){.integrations h2{margin:16px 0 0}.integrations .integration{gap:0 12px;padding:6px 0 8px}.integrations .integration-head{min-height:44px}` +
|
|
24
|
+
`.integrations .integration-action{grid-row:1;align-self:center}.integrations .integration-body{display:block;grid-column:1 / -1;line-height:1.35}` +
|
|
25
|
+
`.integrations .integration-body>p{margin:0}.integrations .integration-body>p.meta{display:inline}.integrations .integration-body>p.meta+p.meta::before{content:" · "}` +
|
|
26
|
+
`.integrations .integration-body>:not(.meta){margin-top:4px}.integrations .integration-body>:not(.meta)+p.meta{display:block;margin-top:2px}.integrations details{margin-top:2px}}`;
|
|
22
27
|
const GROUPS = [
|
|
23
28
|
["chat", "Chat"], ["code", "Code and issues"], ["mail", "Email"], ["tools", "MCP tools"], ["monitoring", "Monitoring"], ["agents", "Agents"],
|
|
24
29
|
];
|
|
@@ -50,7 +55,7 @@ function row(one, csrf) {
|
|
|
50
55
|
return `<div class="integration" data-integration="${e(one.key)}" data-state="${one.state}">` +
|
|
51
56
|
`<p class="integration-head"><strong>${e(one.name)}</strong> ${badge}</p>` +
|
|
52
57
|
action(one, csrf) +
|
|
53
|
-
|
|
58
|
+
`<div class="integration-body">${facts === "" ? "" : `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
|
|
54
59
|
}
|
|
55
60
|
export function integrationsHtml(list, csrf, notice) {
|
|
56
61
|
const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
|
package/dist/lead-context.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ import { type AssignmentCatchUp } from './assignment-brief.js';
|
|
|
5
5
|
export declare function leadContext(store: Store, repos: readonly string[], now: Date, evidenceRoot?: string): string;
|
|
6
6
|
/** The same saved brief is useful before chat spending is authorized. */
|
|
7
7
|
export declare function leadBriefHtml(brief: AssignmentCatchUp): string;
|
|
8
|
-
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}";
|
|
8
|
+
export declare const LEAD_CONTEXT_CSS = ".lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}";
|
package/dist/lead-context.js
CHANGED
|
@@ -53,4 +53,4 @@ export function leadBriefHtml(brief) {
|
|
|
53
53
|
});
|
|
54
54
|
return `<section class="lead-brief" aria-label="Project catch-up"><h2>Catch up</h2>${sections.length ? sections.join('') : '<p class="meta">Nothing needs you right now.</p>'}${brief.assignments.length > 3 || brief.omissions.candidateScanLimited || brief.omissions.assignments > 0 ? '<a href="/work">See all tasks</a>' : ''}</section>`;
|
|
55
55
|
}
|
|
56
|
-
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}';
|
|
56
|
+
export const LEAD_CONTEXT_CSS = '.lead-brief{margin:1rem 0;min-width:0}.lead-brief h2{font-size:1.1rem;margin:0 0 .8rem}.repository-context input[name=q]{display:block;box-sizing:border-box;min-height:44px;width:100%;margin:.4rem 0 .75rem;padding:.6rem .75rem;border:1px solid var(--border);border-radius:8px;background:var(--surface);color:inherit;font:inherit}.repository-context form{margin:.5rem 0 1rem}.repository-context summary{min-height:44px;padding:.75rem 0;overflow-wrap:anywhere}.lead-brief h3{font-size:.85rem;margin:1rem 0 .4rem;color:var(--muted-foreground)}.lead-brief ul{list-style:none;margin:0;padding:0}.lead-brief li{display:grid;grid-template-columns:minmax(0,1fr) auto;column-gap:.75rem;align-items:center;padding:.35rem 0;border-bottom:1px solid var(--border);min-width:0}.lead-brief li a{display:flex;min-height:44px;align-items:center;font-weight:500;overflow-wrap:anywhere;text-decoration:none}.lead-brief li a:hover{text-decoration:underline}.lead-brief-state{font-size:.75rem;font-weight:600;padding:.15rem .5rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink);white-space:nowrap}.lead-brief-state--needs-decision,.lead-brief-state--ready-to-check{background:var(--so-attention-soft);color:var(--so-attention)}.lead-brief-state--working,.lead-brief-state--checking{background:var(--so-info-soft);color:var(--so-info)}.lead-brief-state--complete{background:var(--so-success-soft);color:var(--so-success)}.lead-brief-detail{grid-column:1/-1;font-size:.85rem;color:var(--muted-foreground);overflow-wrap:anywhere;padding-bottom:.35rem}@media(max-width:760px){.lead-brief{margin:.5rem 0}.lead-brief h2{margin:0 0 .25rem}.lead-brief h3{margin:.5rem 0 0}.lead-brief li{padding:0 0 .375rem}.lead-brief-detail{margin-top:-.375rem;padding-bottom:0;line-height:1.35;pointer-events:none}}';
|
package/dist/mate-contract.js
CHANGED
|
@@ -19,7 +19,7 @@ export const MATE_CONTRACT = [
|
|
|
19
19
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
20
20
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
|
21
21
|
"Tools are MCP servers a project's builds get, and builds get nothing else: read get_project_tools. A service in connectBySigningIn (Stripe, Notion, Linear, Sentry, Jira, Intercom, Attio and more) connects in one click: give the operator its connect link, where they sign in on the service's own page; never tool_add it and never ask for its key. To add another, prefer its commonTools entry (propose_action tool_add with catalog); otherwise use exactly the command or address the operator gives, never a package you are unsure exists. A tool reaches work approved after it was added. Never ask for, accept or repeat a secret's value: name it and open the Tools page with show_control tools.",
|
|
22
|
-
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_flow create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, propose_flow edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it up to a limit on a number; propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
22
|
+
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_flow create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, propose_flow edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it up to a limit on a number; propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). A 'pull-request' step opens a pull request for a Build's result and follows its CI (green moves on, red goes back to the build naming the failing check); it merges only when set to, and only after a person approved the card. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight), offer the matching starter flow with propose_flow starter; each is one yes, and none merges without a person. For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
23
23
|
"For skills read get_skills and exact instructions. Enabled means supplied, not proven used or connected. Manage/test through get_actions/propose_action with project/version; require a receipt before claiming deployment/test start.",
|
|
24
24
|
"To see what a result changed, read get_diff: the file list, then one file's changes. To see why checks failed, read get_check_log: its end, or search for the error. When the operator asks for a change, read the relevant file's diff first, then propose_review revise with the exact path and line and one precise instruction in their words.",
|
|
25
25
|
"Each task also has its own chat with the operator (its Ask panel and phone replies). When they refer to what was said or asked about a task, read get_task_conversation. Cards you draft about a task are recorded in that task's chat once confirmed.",
|
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}` +
|