toolroll 0.9.6 → 0.9.7
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 +38 -0
- package/dist/actor.d.ts +21 -0
- package/dist/actor.js +48 -0
- package/dist/assignment-adapters.d.ts +1 -0
- package/dist/assignment-ui.js +3 -1
- package/dist/assignment.d.ts +9 -0
- package/dist/assignment.js +62 -13
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +28 -24
- package/dist/browser-workspace.d.ts +117 -2
- package/dist/builder.d.ts +1 -0
- package/dist/builder.js +33 -13
- package/dist/chat-actions.js +2 -1
- package/dist/chat-delivery.js +2 -0
- package/dist/chat-quiet.js +11 -1
- package/dist/chat-task-actions.d.ts +13 -0
- package/dist/chat-task-actions.js +19 -1
- package/dist/check-levels-ui.d.ts +21 -0
- package/dist/check-levels-ui.js +70 -0
- package/dist/check-levels.d.ts +67 -0
- package/dist/check-levels.js +171 -0
- package/dist/claim.d.ts +22 -0
- package/dist/claim.js +43 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +4 -0
- package/dist/demo-chat.js +4 -1
- package/dist/dispatch.js +7 -2
- package/dist/dispose.d.ts +8 -0
- package/dist/dispose.js +7 -1
- package/dist/export.js +3 -2
- package/dist/first-run.d.ts +34 -0
- package/dist/first-run.js +37 -1
- package/dist/flow-gallery-ui.d.ts +30 -0
- package/dist/flow-gallery-ui.js +85 -0
- package/dist/flow-gallery.d.ts +111 -0
- package/dist/flow-gallery.js +445 -0
- package/dist/flow-share.d.ts +102 -0
- package/dist/flow-share.js +614 -0
- package/dist/flow-starters.d.ts +2 -0
- package/dist/flow-starters.js +1 -1
- package/dist/flow-steps.js +7 -0
- package/dist/flow-triggers.js +2 -0
- package/dist/flows-cli.d.ts +56 -0
- package/dist/flows-cli.js +109 -3
- package/dist/flows-ui.d.ts +15 -1
- package/dist/flows-ui.js +31 -6
- package/dist/guides.js +27 -2
- package/dist/lead-context.js +6 -2
- package/dist/mate-doors.d.ts +0 -15
- package/dist/mate-doors.js +5 -1
- package/dist/mate-tools.js +9 -4
- package/dist/models-ui.js +1 -1
- package/dist/operate.d.ts +4 -1
- package/dist/operate.js +601 -85
- package/dist/project-concurrency.d.ts +62 -0
- package/dist/project-concurrency.js +116 -0
- package/dist/project-delete-ui.d.ts +7 -0
- package/dist/project-delete-ui.js +19 -2
- package/dist/project-delete.js +4 -0
- package/dist/pull-request-flow.d.ts +9 -0
- package/dist/pull-request-flow.js +21 -3
- package/dist/result-follow-ups.d.ts +101 -0
- package/dist/result-follow-ups.js +333 -0
- package/dist/scope.js +11 -0
- package/dist/serve.d.ts +211 -2
- package/dist/serve.js +1009 -141
- package/dist/store.d.ts +76 -3
- package/dist/store.js +311 -22
- package/dist/surface.js +31 -8
- package/dist/tailnet.d.ts +11 -0
- package/dist/tailnet.js +47 -0
- package/dist/task-outcome-cli.js +2 -2
- package/dist/task-status.d.ts +8 -1
- package/dist/task-status.js +28 -10
- package/dist/team-cli.js +3 -0
- package/dist/telegram-status.js +2 -2
- package/dist/verification-evidence.js +21 -1
- package/dist/webhooks.js +5 -0
- package/dist/work-index.js +39 -6
- package/dist/work-summary.d.ts +0 -4
- package/dist/work-summary.js +2 -0
- package/dist/workspace-ui.d.ts +3 -0
- package/dist/workspace-ui.js +5 -3
- package/package.json +1 -1
package/dist/flow-triggers.js
CHANGED
|
@@ -660,6 +660,8 @@ async function fireScript(store, trigger, config, now, io, scheduled) {
|
|
|
660
660
|
const script = flow === null ? null : store.flowScript(flow.repo, config.script);
|
|
661
661
|
if (flow === null || script === null)
|
|
662
662
|
return done(false, `There's no script called ${config.script} in this project any more.`);
|
|
663
|
+
if (script.held !== null)
|
|
664
|
+
return done(false, `The ${script.name} script came with an imported flow. Approve it on the Scripts panel to run it.`);
|
|
663
665
|
const saved = readFlowSecrets(io.dir, flow.repo);
|
|
664
666
|
const missing = (config.secrets ?? []).filter(name => saved[name] === undefined);
|
|
665
667
|
if (missing.length > 0)
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FetchLike } from './flow-share.js';
|
|
1
2
|
import { type TriggerIo } from './flow-triggers.js';
|
|
2
3
|
import type { Store } from './store.js';
|
|
3
4
|
export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
@@ -187,6 +188,59 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
187
188
|
readonly takesValue: true;
|
|
188
189
|
readonly meaning: 'a zone id or name';
|
|
189
190
|
}];
|
|
191
|
+
}, {
|
|
192
|
+
readonly action: 'export';
|
|
193
|
+
readonly synopsis: 'write a flow as a *.toolroll-flow.json file: zones, paths, trigger settings and scripts; never secrets, webhook addresses, names or cards';
|
|
194
|
+
readonly mutation: 'none';
|
|
195
|
+
readonly positionals: readonly ['flow'];
|
|
196
|
+
readonly flags: readonly [...{
|
|
197
|
+
name: string;
|
|
198
|
+
takesValue: boolean;
|
|
199
|
+
meaning: string;
|
|
200
|
+
}[], {
|
|
201
|
+
readonly name: 'out';
|
|
202
|
+
readonly takesValue: true;
|
|
203
|
+
readonly meaning: 'write the file here (default: print it)';
|
|
204
|
+
}];
|
|
205
|
+
}, {
|
|
206
|
+
readonly action: 'import';
|
|
207
|
+
readonly synopsis: 'make a flow from a flow file or a gist/GitHub https address; previews in plain words until --yes; triggers arrive off and scripts wait for approval';
|
|
208
|
+
readonly mutation: 'unkeyed';
|
|
209
|
+
readonly positionals: readonly ['file'];
|
|
210
|
+
readonly flags: readonly [...{
|
|
211
|
+
name: string;
|
|
212
|
+
takesValue: boolean;
|
|
213
|
+
meaning: string;
|
|
214
|
+
}[], {
|
|
215
|
+
name: string;
|
|
216
|
+
takesValue: boolean;
|
|
217
|
+
meaning: string;
|
|
218
|
+
}, {
|
|
219
|
+
name: string;
|
|
220
|
+
takesValue: boolean;
|
|
221
|
+
meaning: string;
|
|
222
|
+
}, {
|
|
223
|
+
readonly name: 'param';
|
|
224
|
+
readonly takesValue: true;
|
|
225
|
+
readonly meaning: 'a value the file asks for, as name=value (repeat for more)';
|
|
226
|
+
}];
|
|
227
|
+
}, {
|
|
228
|
+
readonly action: 'script approve';
|
|
229
|
+
readonly synopsis: 'approve a script an imported flow brought, so it runs';
|
|
230
|
+
readonly mutation: 'identity-idempotent';
|
|
231
|
+
readonly flags: readonly [...{
|
|
232
|
+
name: string;
|
|
233
|
+
takesValue: boolean;
|
|
234
|
+
meaning: string;
|
|
235
|
+
}[], {
|
|
236
|
+
name: string;
|
|
237
|
+
takesValue: boolean;
|
|
238
|
+
meaning: string;
|
|
239
|
+
}, {
|
|
240
|
+
readonly name: 'name';
|
|
241
|
+
readonly takesValue: true;
|
|
242
|
+
readonly meaning: "the script's name";
|
|
243
|
+
}];
|
|
190
244
|
}, {
|
|
191
245
|
readonly action: 'archive';
|
|
192
246
|
readonly synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes';
|
|
@@ -222,6 +276,8 @@ export type FlowsCliContext = {
|
|
|
222
276
|
triggerIo: TriggerIo;
|
|
223
277
|
/** Reads a file, or stdin for "-". */
|
|
224
278
|
readInput?: (path: string) => string;
|
|
279
|
+
/** Fetches a flow file's address (tests stand in for the network). */
|
|
280
|
+
fetchFlow?: FetchLike;
|
|
225
281
|
};
|
|
226
282
|
type Flags = Map<string, string | true>;
|
|
227
283
|
export declare function runFlowsCommand(positional: readonly string[], flags: Flags, context: FlowsCliContext): Promise<number>;
|
package/dist/flows-cli.js
CHANGED
|
@@ -4,12 +4,15 @@
|
|
|
4
4
|
* addCardToFlow — so a flow made here is checked exactly as one drawn there.
|
|
5
5
|
* Reads need no login; every write is an approver's, proved by --as/--token
|
|
6
6
|
* (or the remembered login), and lands in the ledger as a console request
|
|
7
|
-
* does. create, edit, archive and trigger add preview until --yes.
|
|
8
|
-
import
|
|
7
|
+
* does. create, edit, archive, import and trigger add preview until --yes.
|
|
8
|
+
* export writes a flow file (flow-share.ts); import reads one, from a path
|
|
9
|
+
* or a gist/GitHub address, and previews it in plain words first. */
|
|
10
|
+
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
9
11
|
import { resolve } from 'node:path';
|
|
10
12
|
import { envelopeJson } from './envelope.js';
|
|
11
13
|
import { addCardToFlow, advanceFlows, flowDefinitionOf } from './flow-engine.js';
|
|
12
14
|
import { saveScript } from './flow-scripts.js';
|
|
15
|
+
import { exportFlow, fetchFlowFile, FLOW_FILE_MAX_BYTES, FlowFileError, importFlow, parseFlowFile, planFlowImport } from './flow-share.js';
|
|
13
16
|
import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, switchOnStarter } from './flow-starters.js';
|
|
14
17
|
import { addFlowTriggerTo, checkFlowTriggerNow, describeTrigger, removeFlowTrigger, triggerConfigOf, validateTriggerConfig } from './flow-triggers.js';
|
|
15
18
|
import { deciderOf, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowDigest, flowFromSteps, flowTerms, stepsFor } from './flows.js';
|
|
@@ -48,10 +51,15 @@ export const FLOWS_DESCRIPTORS = [
|
|
|
48
51
|
{ name: 'title', takesValue: true, meaning: "the card's title" },
|
|
49
52
|
{ name: 'description', takesValue: true, meaning: 'its details' },
|
|
50
53
|
{ name: 'zone', takesValue: true, meaning: 'a zone id or name' }] },
|
|
54
|
+
{ action: 'export', synopsis: 'write a flow as a *.toolroll-flow.json file: zones, paths, trigger settings and scripts; never secrets, webhook addresses, names or cards', mutation: 'none', positionals: ['flow'], flags: [...read,
|
|
55
|
+
{ name: 'out', takesValue: true, meaning: 'write the file here (default: print it)' }] },
|
|
56
|
+
{ action: 'import', synopsis: 'make a flow from a flow file or a gist/GitHub https address; previews in plain words until --yes; triggers arrive off and scripts wait for approval', mutation: 'unkeyed', positionals: ['file'], flags: [...write, yes, repo,
|
|
57
|
+
{ name: 'param', takesValue: true, meaning: 'a value the file asks for, as name=value (repeat for more)' }] },
|
|
58
|
+
{ action: 'script approve', synopsis: 'approve a script an imported flow brought, so it runs', mutation: 'identity-idempotent', flags: [...write, repo, { name: 'name', takesValue: true, meaning: "the script's name" }] },
|
|
51
59
|
{ action: 'archive', synopsis: 'archive a flow: its cards stop moving and its triggers stop; previews until --yes', mutation: 'identity-idempotent', positionals: ['flow'], flags: [...write, yes] },
|
|
52
60
|
];
|
|
53
61
|
/** `--file` is a value here (a path in the project), where elsewhere it is a switch. */
|
|
54
|
-
export const FLOWS_VALUE_FLAGS = new Set(['file']);
|
|
62
|
+
export const FLOWS_VALUE_FLAGS = new Set(['file', 'param']);
|
|
55
63
|
export async function runFlowsCommand(positional, flags, context) {
|
|
56
64
|
const [first, second] = positional;
|
|
57
65
|
const action = first === 'trigger' || first === 'script' || first === 'card' ? `${first} ${second ?? ''}`.trim() : first ?? 'list';
|
|
@@ -93,6 +101,33 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
93
101
|
const shown = describeFlow(store, flow);
|
|
94
102
|
return ok({ flow: shown }, showLines(shown));
|
|
95
103
|
}
|
|
104
|
+
if (action === 'export') {
|
|
105
|
+
const flow = flowOf(args[0], context.projects);
|
|
106
|
+
if (flow === null)
|
|
107
|
+
return fail('unknown-flow', 'No such flow in your projects. toolroll flows list names them.');
|
|
108
|
+
let exported;
|
|
109
|
+
try {
|
|
110
|
+
exported = exportFlow(store, flow, context.configDir);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return fail('unreadable', error instanceof Error ? error.message : 'This flow can\'t be exported.');
|
|
114
|
+
}
|
|
115
|
+
const left = exported.left.length === 0 ? [] : [`Left out: ${exported.left.join('; ')}.`];
|
|
116
|
+
const out = text('out');
|
|
117
|
+
if (out === null) {
|
|
118
|
+
if (context.json)
|
|
119
|
+
return ok({ file: exported.file, fileName: exported.fileName, left: exported.left }, []);
|
|
120
|
+
context.write(exported.json.trimEnd());
|
|
121
|
+
return EXIT.ok;
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
writeFileSync(resolve(out), exported.json);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return fail('unwritable', `Couldn't write ${out}.`, EXIT.failed);
|
|
128
|
+
}
|
|
129
|
+
return ok({ path: resolve(out), fileName: exported.fileName, left: exported.left }, [`Wrote ${flow.name} to ${resolve(out)}.`, ...left]);
|
|
130
|
+
}
|
|
96
131
|
// ---- writes: an approver's, on a project they can reach
|
|
97
132
|
if (context.credentials === null)
|
|
98
133
|
return fail('unauthenticated', 'Changing a flow needs an approver: pass --as <you> --token <your password> (or sign in once with toolroll up).');
|
|
@@ -232,6 +267,77 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
232
267
|
settle(project);
|
|
233
268
|
return ok({ applied: true, repo: project, script: store.flowScript(project, String(text('name')).trim().toLowerCase()), said: saved.said }, [saved.said]);
|
|
234
269
|
}
|
|
270
|
+
if (action === 'import') {
|
|
271
|
+
const given = text('repo');
|
|
272
|
+
if (given === null || args[0] === undefined)
|
|
273
|
+
return fail('usage', 'Use flows import <file|https address> --repo <path> [--param name=value …] [--yes].', EXIT.usage);
|
|
274
|
+
const project = projectOf(given);
|
|
275
|
+
if (project === null || !reachable.includes(project))
|
|
276
|
+
return refuse(null, 'unknown-project', 'That isn\'t one of your projects. toolroll repos lists them.');
|
|
277
|
+
const source = args[0];
|
|
278
|
+
const params = {};
|
|
279
|
+
for (const line of (text('param') ?? '').split('\n').filter(one => one.trim() !== '')) {
|
|
280
|
+
const equals = line.indexOf('=');
|
|
281
|
+
if (equals < 1)
|
|
282
|
+
return fail('usage', `Give --param as name=value, not ${line}.`, EXIT.usage);
|
|
283
|
+
params[line.slice(0, equals).trim()] = line.slice(equals + 1);
|
|
284
|
+
}
|
|
285
|
+
let plan;
|
|
286
|
+
try {
|
|
287
|
+
let raw;
|
|
288
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(source))
|
|
289
|
+
raw = await fetchFlowFile(source, context.fetchFlow);
|
|
290
|
+
else {
|
|
291
|
+
let size = 0;
|
|
292
|
+
try {
|
|
293
|
+
size = source === '-' ? 0 : statSync(resolve(source)).size;
|
|
294
|
+
}
|
|
295
|
+
catch { /* readInput says it can't be read */ }
|
|
296
|
+
if (size > FLOW_FILE_MAX_BYTES)
|
|
297
|
+
throw new FlowFileError(`That file is too big: a flow file is at most ${FLOW_FILE_MAX_BYTES / 1024} KB.`);
|
|
298
|
+
try {
|
|
299
|
+
raw = readInput(source);
|
|
300
|
+
}
|
|
301
|
+
catch {
|
|
302
|
+
throw new FlowFileError(`${source} couldn't be read.`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
plan = planFlowImport(store, project, parseFlowFile(raw), params, who);
|
|
306
|
+
}
|
|
307
|
+
catch (error) {
|
|
308
|
+
if (!(error instanceof FlowFileError))
|
|
309
|
+
throw error;
|
|
310
|
+
return refuse(project, 'invalid-file', error.message);
|
|
311
|
+
}
|
|
312
|
+
if (!flags.has('yes'))
|
|
313
|
+
return preview(project, plan.title, plan.terms, { name: plan.name, definition: plan.definition, parameters: plan.values });
|
|
314
|
+
let made;
|
|
315
|
+
try {
|
|
316
|
+
made = importFlow(store, plan, who, now, context.configDir);
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
return refuse(project, 'invalid-file', error instanceof Error ? error.message : 'That flow couldn\'t be imported.');
|
|
320
|
+
}
|
|
321
|
+
record(project, 'accepted', `flow #${made.id}`);
|
|
322
|
+
return ok({ applied: true, flow: describeFlow(store, store.getFlow(made.id)), said: made.said }, [`${made.said} Flow #${made.id}.`]);
|
|
323
|
+
}
|
|
324
|
+
if (action === 'script approve') {
|
|
325
|
+
const given = text('repo'), name = text('name');
|
|
326
|
+
if (given === null || name === null)
|
|
327
|
+
return fail('usage', 'Use flows script approve --repo <path> --name <name>.', EXIT.usage);
|
|
328
|
+
const project = projectOf(given);
|
|
329
|
+
if (project === null || !reachable.includes(project))
|
|
330
|
+
return refuse(null, 'unknown-project', 'That isn\'t one of your projects. toolroll repos lists them.');
|
|
331
|
+
const script = store.flowScript(project, name.trim().toLowerCase());
|
|
332
|
+
if (script === null)
|
|
333
|
+
return refuse(project, 'unknown-script', `There's no script called ${name} in ${projectName(project)}.`);
|
|
334
|
+
if (script.held === null)
|
|
335
|
+
return ok({ applied: false, repo: project, said: `${script.name} already runs.` }, [`${script.name} already runs.`]);
|
|
336
|
+
store.approveFlowScript(project, script.name);
|
|
337
|
+
record(project, 'accepted', `script ${script.name} approved`);
|
|
338
|
+
settle(project);
|
|
339
|
+
return ok({ applied: true, repo: project, said: `Approved ${script.name}. Zones that run it go on.` }, [`Approved ${script.name}. Zones that run it go on.`]);
|
|
340
|
+
}
|
|
235
341
|
// Everything else names a flow in one of this person's projects.
|
|
236
342
|
const flow = flowOf(args[0], reachable);
|
|
237
343
|
if (flow === null)
|
package/dist/flows-ui.d.ts
CHANGED
|
@@ -2,9 +2,23 @@
|
|
|
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
|
-
|
|
5
|
+
import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
|
|
6
|
+
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;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@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
7
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
7
8
|
export declare function flowsListHtml(store: Store, flows: readonly FlowRow[], projects: readonly string[], csrf: string, canCreate: boolean, problem: string | null): string;
|
|
9
|
+
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made). */
|
|
10
|
+
export declare const FLOW_IMPORT_SCRIPT = "(function(){var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(!file){reading=null;return;}if(file.size>262144){say('That file is too big: a flow file is at most 256 KB.');input.value='';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();";
|
|
11
|
+
/** An import's preview: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
12
|
+
export declare function flowImportHtml(input: {
|
|
13
|
+
plan: FlowImportPlan | null;
|
|
14
|
+
file: FlowFile;
|
|
15
|
+
document: string;
|
|
16
|
+
repo: string;
|
|
17
|
+
csrf: string;
|
|
18
|
+
values: Record<string, string>;
|
|
19
|
+
previewed: string;
|
|
20
|
+
problem: string | null;
|
|
21
|
+
}): string;
|
|
8
22
|
/** One flow's canvas for one person. */
|
|
9
23
|
export declare function flowView(store: Store, flow: FlowRow, viewer: {
|
|
10
24
|
name: string;
|
package/dist/flows-ui.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
2
2
|
import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, readHooksBase, readLinearKey, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
|
|
3
|
-
import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS
|
|
3
|
+
import { deciderOf, FLOW_COLORS, FLOW_KIND_WORDS, FLOW_STAGE_KINDS } from "./flows.js";
|
|
4
4
|
import { flowInsights, troubleWords } from "./flow-insights.js";
|
|
5
5
|
import { parseSortDecision, sortChip } from "./flow-sort.js";
|
|
6
6
|
import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
|
|
7
7
|
import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
|
|
8
8
|
import { flowFingerprint } from "./flow-live.js";
|
|
9
|
+
import { FLOW_FILE_MAX_BYTES } from "./flow-share.js";
|
|
9
10
|
import { googleConnected } from "./google-mail.js";
|
|
10
11
|
import { mailboxReady } from "./mailbox.js";
|
|
11
12
|
import { labelOf, nameOf } from "./teammate-admin.js";
|
|
@@ -13,7 +14,7 @@ import { callWords, receiptWords } from "./teammate-tools.js";
|
|
|
13
14
|
import { undoFor } from "./teammate-week.js";
|
|
14
15
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
15
16
|
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}}@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
|
+
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;align-items:center;gap:8px;margin:10px 0 0}.flows .flow-import{margin-top:18px;padding-top:14px;border-top:1px solid var(--so-line)}.flows .flow-import h3{font-size:.95rem;margin:0}.flows .flow-terms{list-style:none;padding:0;margin:8px 0 16px;display:grid;gap:10px}.flows .flow-terms li{white-space:pre-line;overflow-wrap:anywhere}.flows .flow-terms pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:.8rem;margin:6px 0 0}.flows .flow-import-preview h2{font-size:1rem;margin:20px 0 4px}.flows .flow-untrusted{font-weight:600}.flows button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary)}.flows fieldset{border:0;padding:0;margin:0;display:grid;gap:10px;min-width:0}.flows legend{font-weight:600;padding:0;margin-bottom:6px}.flows textarea[hidden]{display:none}.flows button.secondary{background:transparent;color:var(--foreground);border:1px solid var(--border)}@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
18
|
/** The flows a person can open, with how many cards wait in each, and the new-flow form. */
|
|
18
19
|
export function flowsListHtml(store, flows, projects, csrf, canCreate, problem) {
|
|
19
20
|
const rows = flows.map(flow => {
|
|
@@ -25,15 +26,39 @@ export function flowsListHtml(store, flows, projects, csrf, canCreate, problem)
|
|
|
25
26
|
return `<article class="card"><div class="flow-row"><h2><a href="/flows/${flow.id}">${e(flow.name)}</a></h2><span class="flow-counts">${e(projectName(flow.repo))} · ${cards.length} card${cards.length === 1 ? "" : "s"} in progress${waiting > 0 ? ` · ${waiting} waiting for a decision` : ""}${trouble === null ? "" : ` · ${e(trouble)}`}</span></div>` +
|
|
26
27
|
(buttons.length === 0 ? "" : `<p class="flow-buttons">${buttons.map(one => `<a class="button-link" href="/flows/${flow.id}?start=${one.id}">${e(one.label)}</a>`).join(" ")}</p>`) + `</article>`;
|
|
27
28
|
}).join("");
|
|
28
|
-
const
|
|
29
|
+
const projectField = projects.length === 1 ? `<input type="hidden" name="repo" value="${e(projects[0])}">` : `<label>Project<select name="repo">${projects.map(repo => `<option value="${e(repo)}">${e(projectName(repo))}</option>`).join("")}</select></label>`;
|
|
30
|
+
// A flow file (flow-share.ts): chosen here or fetched from a gist, previewed in plain words before anything is made.
|
|
31
|
+
const importForm = `<form method="post" action="/flows/import" class="flow-import" data-flow-import><input type="hidden" name="csrf" value="${e(csrf)}"><h3>Or import a flow file</h3>${projectField}<label>File<input type="file" accept=".json,application/json" data-flow-file></label><textarea name="document" hidden data-flow-document></textarea><label>Or its address<input name="url" type="url" inputmode="url" placeholder="https://gist.github.com/…"></label><p class="problem" role="alert" data-flow-file-note hidden></p><button>Preview import</button></form>`;
|
|
32
|
+
// A new flow starts from the gallery (Flows → New): templates grouped by what they're for.
|
|
33
|
+
const create = canCreate && projects.length > 0 ? `<p class="flow-buttons"><a class="button-link" href="/flows/new">New flow</a></p>${importForm}` : "";
|
|
29
34
|
// No flows yet: one click makes a working example with a sample question in it.
|
|
30
35
|
const example = flows.length === 0 && canCreate && projects.length > 0
|
|
31
36
|
? `<form method="post" action="/flows/example" class="card flow-example"><input type="hidden" name="csrf" value="${e(csrf)}"><h2>See a flow work</h2><p class="meta">Claude drafts a reply to a sample customer question; you approve it here or in your chat app.</p>${projects.length === 1 ? `<input type="hidden" name="repo" value="${e(projects[0])}">` : `<label>Project<select name="repo">${projects.map(repo => `<option value="${e(repo)}">${e(projectName(repo))}</option>`).join("")}</select></label>`}<button>Try an example</button></form>`
|
|
32
37
|
: "";
|
|
33
38
|
const intro = `<p class="meta">A flow is your process drawn as zones. Cards move through them: agents do the work, people approve, and the team hears about it.</p>` +
|
|
34
|
-
(canCreate && projects.length > 0 ? `<p class="flow-chat">Describe how work should move and your lead drafts the flow for you to confirm, or start from a template
|
|
39
|
+
(canCreate && projects.length > 0 ? `<p class="flow-chat">Describe how work should move and your lead drafts the flow for you to confirm, or start from a template. <a href="/chat?draft=${encodeURIComponent("Make a flow for ")}">Describe it in chat</a></p>` : "") +
|
|
35
40
|
`<p class="flow-chat">AI teammates can decide and handle cards for you, within rules you write. <a href="/teammates">Teammates</a> · or set one up with its flow in one click: <a href="/kits">Starter kits</a></p>`;
|
|
36
|
-
return `<section class="flows">${problem === null ? "" : `<p class="problem" role="alert">${e(problem)}</p>`}${intro}${rows || example || '<p class="meta">No flows yet.</p>'}
|
|
41
|
+
return `<section class="flows">${problem === null ? "" : `<p class="problem" role="alert">${e(problem)}</p>`}${intro}${create}${rows || example || '<p class="meta">No flows yet.</p>'}</section>`;
|
|
42
|
+
}
|
|
43
|
+
/** Reads the chosen flow file into the form (the server reads it again, whole, before anything is made). */
|
|
44
|
+
export const FLOW_IMPORT_SCRIPT = `(function(){var form=document.querySelector('[data-flow-import]');if(!form)return;var input=form.querySelector('[data-flow-file]'),text=form.querySelector('[data-flow-document]'),note=form.querySelector('[data-flow-file-note]'),address=form.querySelector('[name=url]');function say(words){note.textContent=words;note.hidden=words==='';}var reading=null;input.addEventListener('change',function(){text.value='';say('');var file=input.files&&input.files[0];if(!file){reading=null;return;}if(file.size>${FLOW_FILE_MAX_BYTES}){say('That file is too big: a flow file is at most ${FLOW_FILE_MAX_BYTES / 1024} KB.');input.value='';reading=null;return;}reading=file.text().then(function(value){text.value=value;});});form.addEventListener('submit',function(event){if(reading!==null&&text.value===''){event.preventDefault();reading.then(function(){form.requestSubmit();});return;}if(text.value===''&&address.value.trim()===''){event.preventDefault();say('Choose a flow file or give its address.');}});})();`;
|
|
45
|
+
/** An import's preview: the plan in plain words, what it asks for, and one Import button after the terms. */
|
|
46
|
+
export function flowImportHtml(input) {
|
|
47
|
+
const { plan, file } = input;
|
|
48
|
+
const fields = file.parameters.map(one => `<label><span>${e(one.about)}${one.optional === true ? ' <span class="meta">(optional)</span>' : ""}</span><input name="param.${e(one.id)}" value="${e(input.values[one.id] ?? plan?.values[one.id] ?? one.default ?? "")}" maxlength="200" autocomplete="off"></label>`).join("");
|
|
49
|
+
const list = (items) => items.length === 0 ? "" : `<ul class="flow-terms">${items.map(term => `<li>${e(term)}</li>`).join("")}</ul>`;
|
|
50
|
+
// The steps, what they mean, the triggers and the scripts; each script's code one tap away, as on the Scripts panel.
|
|
51
|
+
const terms = plan === null ? "" : (plan.parts.about === null ? "" : `<p>${e(plan.parts.about)}</p>`) +
|
|
52
|
+
`<p class="flow-untrusted">Its instructions come from the file, not from you: read what each step is asked before you import.</p>` +
|
|
53
|
+
list(plan.parts.steps) +
|
|
54
|
+
(plan.parts.triggers.length === 0 ? "" : `<h2>Triggers</h2>${list(plan.parts.triggers)}`) +
|
|
55
|
+
(plan.parts.scripts.length + plan.parts.kept.length === 0 ? "" : `<h2>Scripts</h2><ul class="flow-terms">${plan.scripts.add.map((one, index) => `<li>${e(plan.parts.scripts[index])}${one.body === undefined ? "" : `<details><summary>Show the script</summary><pre>${e(one.body)}</pre></details>`}</li>`).join("")}${plan.parts.kept.map(one => `<li>${e(one)}</li>`).join("")}</ul>`) +
|
|
56
|
+
`<h2>Good to know</h2>${list([...plan.parts.notes, ...(plan.parts.needs === null ? [] : [plan.parts.needs])])}`;
|
|
57
|
+
return `<section class="flows flow-import-preview">${input.problem === null ? "" : `<p class="problem" role="alert">${e(input.problem)}</p>`}` +
|
|
58
|
+
`<p class="meta">Into ${e(projectName(input.repo))}. Nothing is made until you import it.</p>${terms}` +
|
|
59
|
+
`<form method="post" action="/flows/import" class="card"><input type="hidden" name="csrf" value="${e(input.csrf)}"><input type="hidden" name="repo" value="${e(input.repo)}"><input type="hidden" name="previewed" value="${e(input.previewed)}"><textarea name="document" hidden>${e(input.document)}</textarea>` +
|
|
60
|
+
(fields === "" ? "" : `<fieldset class="flow-import-fields"><legend>Fill in</legend>${fields}</fieldset>`) +
|
|
61
|
+
`<p class="flow-buttons">${plan === null ? "" : '<button name="confirm" value="yes" class="primary">Import flow</button>'}${fields === "" ? "" : `<button name="confirm" value="" class="${plan === null ? "" : "secondary"}">Update preview</button>`}<a href="/flows">Cancel</a></p></form></section>`;
|
|
37
62
|
}
|
|
38
63
|
const historyText = (event, title, sorts = new Set()) => {
|
|
39
64
|
const who = event.actor === "flow" ? "" : ` by ${event.actor}`;
|
|
@@ -154,7 +179,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
|
|
|
154
179
|
tools: projectToolsOf(store, flow.repo).map(tool => ({ name: tool.name, about: tool.spec.about, functions: tool.lastTest?.ok === true ? tool.lastTest.tools : [],
|
|
155
180
|
ready: toolStanding(tool, secretsSetFor(flow.repo, tool.spec, setup.toolHome)).ready })),
|
|
156
181
|
scripts: store.flowScripts(flow.repo).map(script => ({ name: script.name, about: script.about, body: script.body, timeoutMinutes: script.timeoutMinutes, version: script.version, savedBy: script.savedBy, savedAt: script.savedAt,
|
|
157
|
-
language: script.language, file: script.file,
|
|
182
|
+
language: script.language, file: script.file, held: script.held !== null,
|
|
158
183
|
usedHere: stages.filter(stage => stage.kind === "check" && stage.script === script.name).map(stage => stage.title) })),
|
|
159
184
|
start: definition?.start ?? stages[0]?.id ?? "",
|
|
160
185
|
stages,
|
package/dist/guides.js
CHANGED
|
@@ -79,6 +79,27 @@ ${AUTHORITY_LINE}
|
|
|
79
79
|
detail says why; \`toolroll sync\` refreshes trackers.
|
|
80
80
|
- \`contest-open\`: a tournament is running on the task; a person picks.
|
|
81
81
|
|
|
82
|
+
## Acting as the lead
|
|
83
|
+
|
|
84
|
+
When you are the person's lead, act under your own identity, never their
|
|
85
|
+
login, so your housekeeping never pings them.
|
|
86
|
+
|
|
87
|
+
- The person mints it once, behind their password: \`toolroll lead token\`
|
|
88
|
+
(\`--revoke\` ends it). Pass it on every task, assignment and status
|
|
89
|
+
command as \`--token <lead token>\`, or set \`TOOLROLL_LEAD_TOKEN\`. It
|
|
90
|
+
signs in as them; the ledger records you as "lead for <owner>", and the
|
|
91
|
+
console shows "by the lead".
|
|
92
|
+
- Your work pings nobody: tasks you file, approve, cancel or complete stay
|
|
93
|
+
on the console and in the evening digest. A person hears about them only
|
|
94
|
+
when you hand one over, when it fails with nothing left for you to try,
|
|
95
|
+
or for a security alert.
|
|
96
|
+
- Hand a task to a person: \`task ask <id> --person <name> --why "<what
|
|
97
|
+
they need to do>"\` — one message, and the task is theirs from then on.
|
|
98
|
+
- Re-filing (a release check re-gated as \`release-<name>b\`): \`task add
|
|
99
|
+
"<title>" --id <new> --replaces <old>\`, or \`task state <old> cancelled
|
|
100
|
+
--replaced-by <new>\`. The old task reads "Replaced by <new>", never
|
|
101
|
+
"Cancelled".
|
|
102
|
+
|
|
82
103
|
## Complete, pull request, merge
|
|
83
104
|
|
|
84
105
|
The console's Complete and Merge, from a terminal. Both are a person's
|
|
@@ -122,7 +143,9 @@ applies the console's rules exactly.
|
|
|
122
143
|
## What you may never do
|
|
123
144
|
|
|
124
145
|
- Never approve scopes, answer decisions, or acquire approver tokens —
|
|
125
|
-
those acts belong to a person, through their own ceremony.
|
|
146
|
+
those acts belong to a person, through their own ceremony. A lead
|
|
147
|
+
approves only under the lead token its person minted for it, and never
|
|
148
|
+
with the person's own password or remembered login.
|
|
126
149
|
- Never steer a task: \`task steer\` takes the operator's credential,
|
|
127
150
|
because a steering note speaks in an agent's brief WITH THE OPERATOR'S
|
|
128
151
|
VOICE (see the \`steering\` guide).
|
|
@@ -311,7 +334,9 @@ ${AUTHORITY_LINE}
|
|
|
311
334
|
their cards appear under "proposed by coordinators".
|
|
312
335
|
- **settings** (\`/settings\`): alerts to this device, provider keys,
|
|
313
336
|
which messaging service pages, the Telegram bot token, and the Telegram
|
|
314
|
-
digest cadence (away mode).
|
|
337
|
+
digest cadence (away mode). Notifications → Projects mutes one project's
|
|
338
|
+
pings (\`toolroll notifications mute|unmute --repo <p>\`); it still shows
|
|
339
|
+
in Tasks and the evening digest.
|
|
315
340
|
- The rail is four rows — inbox · board · builds · projects — and a
|
|
316
341
|
**more** group: portfolio, task list, fleet, routines, system,
|
|
317
342
|
requirements, people, operating mode, settings. On a phone the same
|
package/dist/lead-context.js
CHANGED
|
@@ -46,9 +46,13 @@ export function leadBriefHtml(brief) {
|
|
|
46
46
|
// it says what the person or the crew is doing next.
|
|
47
47
|
const quiet = group.title === 'Finished';
|
|
48
48
|
// A state chip that repeats its group's heading says nothing new.
|
|
49
|
-
const stateChip = (state) => {
|
|
49
|
+
const stateChip = (state, detail) => {
|
|
50
|
+
// Replaced, never "Cancelled".
|
|
51
|
+
const words = state === 'cancelled' && /^Replaced by \S+/.test(detail ?? '') ? detail.replace(/\.$/, '') : STATE_WORDS[state] ?? state;
|
|
52
|
+
return words === group.title ? '' : `<span class="lead-brief-state lead-brief-state--${state}">${escape(words)}</span>`;
|
|
53
|
+
};
|
|
50
54
|
return [`<section><h3>${group.title}</h3><ul>${entries.map(one => `<li><a href="/chat?task=${encodeURIComponent(one.rootId)}">${escape(one.title)}</a>` +
|
|
51
|
-
stateChip(one.state) +
|
|
55
|
+
stateChip(one.state, one.detail) +
|
|
52
56
|
(quiet ? '' : `<span class="lead-brief-detail">${escape(publicChatText(one.detail || one.outcome || '', 160))}</span>`) + `</li>`).join('')}</ul></section>`];
|
|
53
57
|
});
|
|
54
58
|
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>`;
|
package/dist/mate-doors.d.ts
CHANGED
|
@@ -1,19 +1,4 @@
|
|
|
1
1
|
import { type SharedActionOptions } from './chat-actions.js';
|
|
2
|
-
/**
|
|
3
|
-
* The confirm doors (mate arc §2, ruling 7; v3 §9): a pending proposal —
|
|
4
|
-
* the mate's, or a coordinator's over the gateway — becomes an act only
|
|
5
|
-
* here, as the operator's own act through the plane's existing
|
|
6
|
-
* primitives, in ONE transaction that re-proves everything the card was
|
|
7
|
-
* rendered under. The console and the CLI both call these; the HTTP edge
|
|
8
|
-
* adds csrf and the cookie, the CLI adds the password — neither adds
|
|
9
|
-
* authority the door does not check itself.
|
|
10
|
-
*
|
|
11
|
-
* The door's answer is typed and rendered on the card: `done` with the
|
|
12
|
-
* plain words of what happened, or `refused` with the reason the primitive
|
|
13
|
-
* gave. A stale proposal (the queue moved, the hold changed hands, the
|
|
14
|
-
* scope was rewritten, the decision answered) is a refusal, never a
|
|
15
|
-
* silent re-read.
|
|
16
|
-
*/
|
|
17
2
|
import { type CoordinatorProposal, type MateProposal, type Store } from "./store.js";
|
|
18
3
|
import type { VerifiedApprover } from "./principal.js";
|
|
19
4
|
export type ProposalKind = MateProposal["kind"];
|
package/dist/mate-doors.js
CHANGED
|
@@ -14,6 +14,7 @@ import { executeSharedAction, sharedActionPayload, sharedActionAllowsChallenge,
|
|
|
14
14
|
* scope was rewritten, the decision answered) is a refusal, never a
|
|
15
15
|
* silent re-read.
|
|
16
16
|
*/
|
|
17
|
+
import { isCheckLevel, setTaskCheckLevel } from "./check-levels.js";
|
|
17
18
|
import { verifiedAuthor } from "./store.js";
|
|
18
19
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
19
20
|
import { TeamLeads } from './team-leads.js';
|
|
@@ -253,6 +254,9 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
253
254
|
}, now);
|
|
254
255
|
if (!filed.ok)
|
|
255
256
|
return refuse("refused", `not filed: ${filed.message}`);
|
|
257
|
+
const checks = payload["checks"];
|
|
258
|
+
if (isCheckLevel(checks))
|
|
259
|
+
setTaskCheckLevel(store, filed.id, checks, actor.name, now);
|
|
256
260
|
return {
|
|
257
261
|
ok: true,
|
|
258
262
|
kind,
|
|
@@ -302,7 +306,7 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
302
306
|
if (kind === "task_action") {
|
|
303
307
|
// An attended surface (the console, the paired phone) honours the
|
|
304
308
|
// operator's own automatic-approval mode; the CLI keeps its ceremony.
|
|
305
|
-
const result = applyChatTaskAction(store, actor, payload, now, options.via !== "cli", { held: options.held, deferSignal: options.deferSignal, via: options.via });
|
|
309
|
+
const result = applyChatTaskAction(store, actor, payload, now, options.via !== "cli", { held: options.held, deferSignal: options.deferSignal, via: options.via, ...(options.evidenceRoot === undefined ? {} : { evidenceRoot: options.evidenceRoot }) });
|
|
306
310
|
return result.ok ? { ok: true, kind, taskId: result.taskId, said: result.said } : refuse("stale", result.message);
|
|
307
311
|
}
|
|
308
312
|
if (kind === "review") {
|
package/dist/mate-tools.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { connectionsOf, oneClickOf } from "./mcp-connect.js";
|
|
2
|
+
import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
|
|
2
3
|
import { policyParts } from "./policy.js";
|
|
3
4
|
import { randomUUID } from 'node:crypto';
|
|
4
5
|
import { dirname, join } from 'node:path';
|
|
@@ -451,7 +452,7 @@ export const MATE_TOOLS = [
|
|
|
451
452
|
},
|
|
452
453
|
{
|
|
453
454
|
name: "propose_task_action",
|
|
454
|
-
description: "Propose stop/resume/retry/plan/wait_for/stop_waiting for currentExecution. Stop/resume needs get_task control.run; resume requires password review.",
|
|
455
|
+
description: "Propose stop/resume/retry/plan/wait_for/stop_waiting/run_checks/run_full_checks/add_tests for currentExecution. Stop/resume needs get_task control.run; resume requires password review. run_checks and run_full_checks check the latest result's exact commit; add_tests files a task to write tests for it.",
|
|
455
456
|
inputSchema: schema({ task: TASK_ARG, operation: { type: "string", enum: Object.keys(CHAT_TASK_ACTIONS) }, dependency: TASK_ARG, run: { type: "integer", minimum: 1 } }, ["task", "operation"]),
|
|
456
457
|
handle: (ctx, args) => {
|
|
457
458
|
const task = taskIdOf(args), operation = args["operation"];
|
|
@@ -1439,8 +1440,8 @@ export const MATE_TOOLS = [
|
|
|
1439
1440
|
},
|
|
1440
1441
|
{
|
|
1441
1442
|
name: "propose_task",
|
|
1442
|
-
description: "Draft inferred title/goal/criteria. report:true investigates without code; planning required plans first, skip needs explicit direct-build request, auto is default.",
|
|
1443
|
-
inputSchema: schema({ repo: REPO_ARG, title: { type: "string", maxLength: 200 }, goal: TASK_SCOPE_TEXT_SCHEMA, not: TASK_SCOPE_TEXT_SCHEMA, touches: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 50 }, acceptance: ACCEPTANCE_ARG_SCHEMA, planning: { type: "string", enum: ["auto", "required", "skip"] }, report: { type: "boolean" } }, ["repo", "title", "goal", "acceptance"]),
|
|
1443
|
+
description: "Draft inferred title/goal/criteria. report:true investigates without code; planning required plans first, skip needs explicit direct-build request, auto is default. checks only when the person asked: off for \"skip the tests\", full for \"run the full checks\", quick for quick checks; omit to use the project's setting.",
|
|
1444
|
+
inputSchema: schema({ repo: REPO_ARG, title: { type: "string", maxLength: 200 }, goal: TASK_SCOPE_TEXT_SCHEMA, not: TASK_SCOPE_TEXT_SCHEMA, touches: { type: "array", items: { type: "string", maxLength: 200 }, maxItems: 50 }, acceptance: ACCEPTANCE_ARG_SCHEMA, planning: { type: "string", enum: ["auto", "required", "skip"] }, report: { type: "boolean" }, checks: { type: "string", maxLength: 80 } }, ["repo", "title", "goal", "acceptance"]),
|
|
1444
1445
|
handle: (ctx, args) => {
|
|
1445
1446
|
const repo = repoPathOf(ctx.who, args["repo"]);
|
|
1446
1447
|
if (repo === null)
|
|
@@ -1467,7 +1468,11 @@ export const MATE_TOOLS = [
|
|
|
1467
1468
|
if (args["report"] !== undefined && typeof args["report"] !== "boolean")
|
|
1468
1469
|
return { ok: false, message: "report is true or false" };
|
|
1469
1470
|
const report = args["report"] === true;
|
|
1470
|
-
|
|
1471
|
+
// A level, or the person's own words ("skip the tests", "run the full checks").
|
|
1472
|
+
const checks = args["checks"] === undefined ? null : isCheckLevel(args["checks"]) ? args["checks"] : typeof args["checks"] === "string" ? checkLevelFromWords(args["checks"]) : null;
|
|
1473
|
+
if (args["checks"] !== undefined && checks === null)
|
|
1474
|
+
return { ok: false, message: "checks is quick, full or off" };
|
|
1475
|
+
const id = ctx.draft("task", { repo, repoId: args["repo"], title: args["title"], goal: args["goal"], not, touches, acceptance, planning, report, ...(checks === null || report ? {} : { checks }) });
|
|
1471
1476
|
if (id === null)
|
|
1472
1477
|
return tooMany();
|
|
1473
1478
|
return { ok: true, body: { proposal: id, kind: "task", repo: args["repo"], deliverable: report ? "report" : "branch", planning: report ? "not needed for a scout" : planning, awaiting: "the operator's confirmation" } };
|
package/dist/models-ui.js
CHANGED
|
@@ -53,7 +53,7 @@ export function modelsHtml(view) {
|
|
|
53
53
|
fresh +
|
|
54
54
|
`<h2>Default agents</h2><p class="meta">New tasks use these. Approved tasks keep the agents they were approved with.</p>` +
|
|
55
55
|
view.roles.map(role => roleForm(role, manage, view.csrf)).join("") +
|
|
56
|
-
(view.chat === null ? "" : `<h2>Chat</h2><p>${e(view.chat.words)} · <a href="/
|
|
56
|
+
(view.chat === null ? "" : `<h2>Chat</h2><p>${e(view.chat.words)} · <a href="/settings/lead">Change</a></p>`) +
|
|
57
57
|
watch + `</section>`;
|
|
58
58
|
}
|
|
59
59
|
/** Filters one long picker as you type; the saved choice always stays listed. */
|
package/dist/operate.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type IntegrationIo } from "./integrations.js";
|
|
|
2
2
|
import { type OnboardIo } from "./agent-onboard.js";
|
|
3
3
|
import { type TriggerIo } from "./flow-triggers.js";
|
|
4
4
|
import { type StepIo } from "./flow-steps.js";
|
|
5
|
+
import type { FetchLike } from "./flow-share.js";
|
|
5
6
|
import { type Store } from "./store.js";
|
|
6
7
|
import { type DispatchAdapter } from "./sync.js";
|
|
7
8
|
import { type TelegramTransport } from "./telegram.js";
|
|
@@ -56,6 +57,8 @@ export type OperateOptions = {
|
|
|
56
57
|
flowTriggerIo?: Partial<TriggerIo>;
|
|
57
58
|
/** Injected by tests: how flow check and update steps run commands and reach GitHub and Linear. */
|
|
58
59
|
flowStepIo?: Partial<StepIo>;
|
|
60
|
+
/** Injected by tests: how `flows import` fetches a flow file's address. */
|
|
61
|
+
flowFetch?: FetchLike;
|
|
59
62
|
/** Injected by tests: how `integrations` checks reach services (fetch, gh, sign-in checks, mail servers). */
|
|
60
63
|
integrationIo?: Partial<IntegrationIo>;
|
|
61
64
|
/** Injected by tests: the mate's provider fetch, key environment, and stdin lines. */
|
|
@@ -91,7 +94,7 @@ type Args = {
|
|
|
91
94
|
* exports are behavior, not commentary — and the declared command guide
|
|
92
95
|
* (surface.ts) is tested for exact equality against them.
|
|
93
96
|
*/
|
|
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", "merge"];
|
|
97
|
+
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", "merge", "ask", "checks", "add-tests"];
|
|
95
98
|
export declare const PUBLISH_ACTIONS: readonly ["setup", "grant", "revoke", "status", "unblock", "rearm", "merge", "refire"];
|
|
96
99
|
export declare const CONFIG_ACTIONS: readonly ["show", "set", "clear"];
|
|
97
100
|
export declare const APPROVER_ACTIONS: readonly ["list", "add"];
|