toolroll 0.9.41 → 0.9.43
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 +37 -0
- package/dist/chat-actions.js +1 -1
- package/dist/chat-approval.d.ts +45 -0
- package/dist/chat-approval.js +81 -0
- package/dist/chat-channel.js +5 -0
- package/dist/chat-decide.d.ts +18 -6
- package/dist/chat-decide.js +54 -20
- package/dist/chat-flow.js +2 -2
- package/dist/cli.js +4 -2
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +484 -0
- package/dist/contracts/console-api.js +274 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- package/dist/flow-triggers.js +61 -60
- package/dist/flows-cli.d.ts +28 -0
- package/dist/flows-cli.js +43 -1
- package/dist/guides.js +2 -0
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +43 -1
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +55 -4
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +193 -30
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +11 -16
- package/dist/store.js +122 -160
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +10 -2
- package/dist/teammate-work.js +1 -1
- package/dist/telegram-flow.d.ts +41 -1
- package/dist/telegram-flow.js +96 -12
- package/dist/telegram.js +27 -3
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/desktop-update.js
CHANGED
|
@@ -12,6 +12,7 @@ import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate
|
|
|
12
12
|
import { currentDesktopAccess } from "./desktop-access.js";
|
|
13
13
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner } from "./coding-update.js";
|
|
14
14
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
15
|
+
import { readDesktopRecoveryView, readDesktopUpdateJournal, supervisorPidsOf, updateRequestIs } from "./contracts/update-journal.js";
|
|
15
16
|
/** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
|
|
16
17
|
* and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
|
|
17
18
|
function sqlite() {
|
|
@@ -60,13 +61,40 @@ function privateFile(file) {
|
|
|
60
61
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
61
62
|
throw Error("The update record must be an owner-only regular file.");
|
|
62
63
|
}
|
|
64
|
+
/** The refusal the hand-written reader gave for the field a saved journal first gets wrong, so callers and people read
|
|
65
|
+
* the same words; fields it never checked read as an invalid record. */
|
|
66
|
+
function journalRefusal(issue) {
|
|
67
|
+
const [field, inner] = issue.path.split(/[.[]/);
|
|
68
|
+
switch (field) {
|
|
69
|
+
case "old":
|
|
70
|
+
if (inner === undefined || inner === "path")
|
|
71
|
+
return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
72
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
73
|
+
case "next":
|
|
74
|
+
case "databaseFile":
|
|
75
|
+
case "configHash":
|
|
76
|
+
case "wasRunning":
|
|
77
|
+
case "backupPath":
|
|
78
|
+
case "backupHash":
|
|
79
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
80
|
+
case "replacementOccurred": return "The recorded replacement state is invalid. Nothing was changed.";
|
|
81
|
+
case "codingCatalogExpected": return "The recorded coding catalog presence is invalid. Nothing was changed.";
|
|
82
|
+
case "stoppedPids": return "The recorded service processes are invalid. Nothing was changed.";
|
|
83
|
+
case "codingBackupPath":
|
|
84
|
+
case "codingBackupHash": return "The retained coding backup paths or identity are invalid. Nothing was changed.";
|
|
85
|
+
default: return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
export function readUpdateJournal(stateDir, retainedReceipt) {
|
|
64
89
|
const file = retainedReceipt ?? journalPath(stateDir);
|
|
65
90
|
if (!existsSync(file))
|
|
66
91
|
return null;
|
|
67
92
|
privateFile(file);
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
93
|
+
const read = readDesktopUpdateJournal(JSON.parse(readFileSync(file, "utf8")));
|
|
94
|
+
if (!read.ok)
|
|
95
|
+
throw Error(`${journalRefusal(read.issues[0])} (${read.issues.map(issue => issue.line).join("; ")})`);
|
|
96
|
+
const j = read.value;
|
|
97
|
+
if (!/^[a-f0-9-]{36}$/.test(j.id) || j.stateDir !== resolve(stateDir) || !isAbsolute(j.old?.path ?? "") || j.workDir !== join(dirname(j.old.path), ".standing-orders-updates", j.id) || !/^[a-zA-Z0-9.-]+$/.test(j.label) || !["install", "restore"].includes(j.intended) || !["prepared", "draining", "backing-up", "stopping", "installing", "verifying", "rolling-back", "releasing", "complete", "restored", "cancelled", "needs-attention"].includes(j.phase))
|
|
70
98
|
throw Error("The saved update record is invalid. Preserve it and the app backups; nothing was changed.");
|
|
71
99
|
if (retainedReceipt && retainedReceipt !== join(j.workDir, "receipt.json"))
|
|
72
100
|
throw Error("The retained recovery receipt is at the wrong path.");
|
|
@@ -374,7 +402,7 @@ export function desktopUpdateStatus(stateDir) {
|
|
|
374
402
|
let recovery = null;
|
|
375
403
|
try {
|
|
376
404
|
privateFile(join(j.workDir, "recovery.json"));
|
|
377
|
-
recovery = JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8"));
|
|
405
|
+
recovery = readDesktopRecoveryView(JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8")));
|
|
378
406
|
}
|
|
379
407
|
catch { /* A legacy/manual update has no guardian. */ }
|
|
380
408
|
const wantsAutomatic = recovery?.id === j.id && ["armed", "running", "backoff"].includes(recovery.state ?? "") && !terminal(j.phase);
|
|
@@ -389,8 +417,7 @@ export function updateStopRequested(j) {
|
|
|
389
417
|
if (!existsSync(file))
|
|
390
418
|
return false;
|
|
391
419
|
privateFile(file);
|
|
392
|
-
|
|
393
|
-
if (request.id !== j.id || request.action !== "stop")
|
|
420
|
+
if (!updateRequestIs(JSON.parse(readFileSync(file, "utf8")), j.id, "stop"))
|
|
394
421
|
throw Error("The saved stop request is invalid. Automatic starts are refused.");
|
|
395
422
|
return true;
|
|
396
423
|
}
|
|
@@ -410,7 +437,9 @@ export function requestUpdateRestore(stateDir) {
|
|
|
410
437
|
function restoreRequested(j) {
|
|
411
438
|
try {
|
|
412
439
|
const request = JSON.parse(readFileSync(join(j.workDir, "request.json"), "utf8"));
|
|
413
|
-
|
|
440
|
+
if (request === null)
|
|
441
|
+
throw Error("The saved restore request is invalid.");
|
|
442
|
+
return updateRequestIs(request, j.id, "restore");
|
|
414
443
|
}
|
|
415
444
|
catch (error) {
|
|
416
445
|
if (error.code === "ENOENT")
|
|
@@ -543,8 +572,7 @@ function assertDesktopCodingStopped(j, db) {
|
|
|
543
572
|
/** The service's own processes, from its supervisor's status file: read before the stop, never after. */
|
|
544
573
|
function servicePids(stateDir) {
|
|
545
574
|
try {
|
|
546
|
-
|
|
547
|
-
return [status.supervisorPid, status.controllerPid].filter((pid) => Number.isSafeInteger(pid) && Number(pid) > 1);
|
|
575
|
+
return supervisorPidsOf(JSON.parse(readFileSync(join(stateDir, "controller-supervisor.json"), "utf8")));
|
|
548
576
|
}
|
|
549
577
|
catch {
|
|
550
578
|
return [];
|
package/dist/evidence.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { type ParsedReport, type ReportImage } from "./scout-report.js";
|
|
17
17
|
import { type ParsedProof } from "./proof.js";
|
|
18
18
|
import { type HandoffArtifact } from "./contracts/handoff.js";
|
|
19
|
+
import type { DiffStat } from "./contracts/diff-stat.js";
|
|
19
20
|
export type { HandoffArtifact } from "./contracts/handoff.js";
|
|
20
21
|
import type { Artifact, Store } from "./store.js";
|
|
21
22
|
import type { ExecResult } from "./exec.js";
|
|
@@ -229,25 +230,8 @@ export declare function quarantineMailboxes(worktree: string, root: string | nul
|
|
|
229
230
|
export declare function captureParkEvidence(store: Store, git: (file: string, args: readonly string[], options?: {
|
|
230
231
|
cwd?: string;
|
|
231
232
|
}) => Promise<ExecResult>, worktree: string, baseRevision: string | null, root: string, runId: number, now: Date): Promise<number[]>;
|
|
232
|
-
/**
|
|
233
|
-
export type DiffStatFile
|
|
234
|
-
path: string;
|
|
235
|
-
additions: number | null;
|
|
236
|
-
deletions: number | null;
|
|
237
|
-
renamedFrom?: string;
|
|
238
|
-
};
|
|
239
|
-
export type DiffStat = {
|
|
240
|
-
schema: 1;
|
|
241
|
-
base: string;
|
|
242
|
-
head: string;
|
|
243
|
-
fileCount: number;
|
|
244
|
-
additions: number;
|
|
245
|
-
deletions: number;
|
|
246
|
-
binaryCount: number;
|
|
247
|
-
files: DiffStatFile[];
|
|
248
|
-
/** True when the file list was cut to fit the cap — counts stay complete. */
|
|
249
|
-
filesTruncated: boolean;
|
|
250
|
-
};
|
|
233
|
+
/** The terminal diff-stat and its file rows: src/contracts/diff-stat.ts. */
|
|
234
|
+
export type { DiffStat, DiffStatFile } from "./contracts/diff-stat.js";
|
|
251
235
|
/**
|
|
252
236
|
* Parse `git diff --numstat -z` output. NUL-delimited, so filenames are never
|
|
253
237
|
* guessed out of patch headers (Codex roadmap review, scope cut A.3): plain
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -74,4 +74,6 @@ export declare function decideFlowCard(store: Store, input: {
|
|
|
74
74
|
entry?: number;
|
|
75
75
|
/** v92: the AI teammate (by handle) deciding a zone it staffs; `actor` is then how it reads in history. */
|
|
76
76
|
teammate?: string;
|
|
77
|
+
/** Where the decision was made, for its ledger line: "the console", "the command line", "Telegram". */
|
|
78
|
+
where?: string;
|
|
77
79
|
}, now: Date): FlowDecision;
|
package/dist/flow-engine.js
CHANGED
|
@@ -485,6 +485,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
485
485
|
// v102: a protected project's decisions are a person's, never an AI teammate's.
|
|
486
486
|
if (input.teammate !== undefined && store.approvalRules(flow.repo).protectProject)
|
|
487
487
|
return { ok: false, message: "This project is protected: a person decides here." };
|
|
488
|
+
// Every decision is one ledger line, the same wherever it was made; only `via` says where.
|
|
489
|
+
const ledger = (outcome) => store.recordAction({ at: now.toISOString(), actor: input.actor, repo: flow.repo, taskId: card.primaryTask, runId: null,
|
|
490
|
+
action: "flow decision", outcome, source: "request", detail: `${flow.name} · card ${card.id} · ${stage.title}${input.teammate === undefined ? "" : ` · teammate ${input.teammate}`} · via ${input.where ?? "Toolroll"}` });
|
|
488
491
|
if (input.decision === "approve") {
|
|
489
492
|
// An edited draft replaces the one Claude wrote, so the steps after this send what the person approved.
|
|
490
493
|
const draft = draftFor(definition, stage);
|
|
@@ -498,9 +501,12 @@ export function decideFlowCard(store, input, now) {
|
|
|
498
501
|
}
|
|
499
502
|
if (stage.next === null) {
|
|
500
503
|
store.updateFlowCard(card.id, { state: "done", waiting: null }, now);
|
|
504
|
+
ledger("approved");
|
|
501
505
|
return { ok: true, said: "Approved. The card is done." };
|
|
502
506
|
}
|
|
503
|
-
store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now)
|
|
507
|
+
if (!store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now))
|
|
508
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
509
|
+
ledger("approved");
|
|
504
510
|
const title = definition.stages.find(one => one.id === stage.next)?.title ?? stage.next;
|
|
505
511
|
if (card.owner !== null)
|
|
506
512
|
notifyPeople(store, card, [card.owner], input.actor, { key: `approved:${card.entry}`, subject: `${input.actor} approved “${card.title}”`, body: `${stage.title}: approved. It moves to ${title}.${input.note === null || input.note.trim() === "" ? "" : `\n\n${input.note.trim()}`}` }, now);
|
|
@@ -512,7 +518,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
512
518
|
return { ok: false, message: "Say what should change." };
|
|
513
519
|
const target = definition.stages.find(one => one.id === stage.onFail);
|
|
514
520
|
const revision = revisionWithNote(store, card, target, input.note.trim(), input.actor, input.repos, input.evidenceRoot, now);
|
|
515
|
-
store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now)
|
|
521
|
+
if (!store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now))
|
|
522
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
523
|
+
ledger("sent-back");
|
|
516
524
|
if (card.owner !== null)
|
|
517
525
|
notifyPeople(store, card, [card.owner], input.actor, { key: `sent-back:${card.entry}`, subject: `${input.actor} sent “${card.title}” back`, body: `Sent back to ${target?.title ?? stage.onFail}:\n\n${input.note.trim()}`, attention: true }, now);
|
|
518
526
|
return { ok: true, said: revision === null ? `Sent back to ${target?.title ?? stage.onFail} with your note.` : `Sent back to ${target?.title ?? stage.onFail}: a revision was made with your note.` };
|
package/dist/flow-triggers.js
CHANGED
|
@@ -42,6 +42,7 @@ import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./
|
|
|
42
42
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
43
43
|
import { tmpdir } from "node:os";
|
|
44
44
|
import { envValue } from "./names.js";
|
|
45
|
+
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
45
46
|
export { FLOW_TRIGGER_KINDS };
|
|
46
47
|
export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
|
|
47
48
|
/** What each kind is called on the canvas. */
|
|
@@ -771,7 +772,7 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
771
772
|
if (!read.ok)
|
|
772
773
|
return { ok: false, problem: read.said };
|
|
773
774
|
const own = signed.address.toLowerCase();
|
|
774
|
-
const items = read.mails.filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
|
+
const items = read.mails.map(readInboundMail).filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
776
|
const key = `mail:${read.at.validity}:${mail.uid}`, at = mailCursorText({ validity: read.at.validity, uid: mail.uid });
|
|
776
777
|
const label = `Email from ${mail.from || "an unknown sender"}`.slice(0, 120);
|
|
777
778
|
// Machines and this account itself never start cards: a flow that answers mail mustn't answer them.
|
|
@@ -793,44 +794,42 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
793
794
|
: read.renumbered ? "The mail server renumbered this folder; watching it from now." : undefined;
|
|
794
795
|
return { ok: true, items, cursor: mailCursorText(read.at), more: read.more, ...(said === undefined ? {} : { said }) };
|
|
795
796
|
}
|
|
796
|
-
const text = (value) => typeof value === "string" ? value : "";
|
|
797
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
798
797
|
/** A GitHub issue or pull request as a card, or why it is left out; null when it doesn't match. */
|
|
799
798
|
export function githubItem(config, raw, what) {
|
|
800
|
-
const item =
|
|
801
|
-
const number = Number(item
|
|
799
|
+
const item = readGithubIssue(raw);
|
|
800
|
+
const number = Number(item.number);
|
|
802
801
|
if (!Number.isSafeInteger(number))
|
|
803
802
|
return null;
|
|
804
|
-
if (what === "issue" && item
|
|
803
|
+
if (what === "issue" && item.pull_request !== undefined)
|
|
805
804
|
return null;
|
|
806
|
-
if (what === "pull" && item
|
|
805
|
+
if (what === "pull" && item.draft === true)
|
|
807
806
|
return null;
|
|
808
|
-
const labels =
|
|
807
|
+
const labels = (item.labels ?? []).map(one => (one.name ?? "").toLowerCase());
|
|
809
808
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
810
809
|
return null;
|
|
811
810
|
const label = what === "issue" ? `GitHub issue #${number}` : `Pull request #${number}`;
|
|
812
|
-
const key = `${what}:${number}`, at =
|
|
813
|
-
if (config.from === "team" && !TEAM.has(
|
|
811
|
+
const key = `${what}:${number}`, at = item.updated_at || null;
|
|
812
|
+
if (config.from === "team" && !TEAM.has(item.author_association ?? ""))
|
|
814
813
|
return { key, skip: "opened by someone without write access", label, at };
|
|
815
|
-
const login =
|
|
816
|
-
const branches = what === "pull" ? ` (${
|
|
817
|
-
return { key, at, title:
|
|
818
|
-
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${
|
|
819
|
-
source: { kind: "github", label, url:
|
|
814
|
+
const login = item.user?.login || "someone";
|
|
815
|
+
const branches = what === "pull" ? ` (${item.head?.ref ?? ""} → ${item.base?.ref ?? ""})` : "";
|
|
816
|
+
return { key, at, title: item.title || label,
|
|
817
|
+
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${item.body ?? ""}`.trim().slice(0, 4000),
|
|
818
|
+
source: { kind: "github", label, url: item.html_url || null } };
|
|
820
819
|
}
|
|
821
820
|
/** A failed workflow run as a card: one card per failing commit on the watched branch. */
|
|
822
821
|
export function githubRunItem(config, raw) {
|
|
823
|
-
const run =
|
|
824
|
-
if (
|
|
822
|
+
const run = readGithubRun(raw);
|
|
823
|
+
if (run.conclusion !== "failure" || (config.branch !== null && (run.head_branch ?? "") !== config.branch))
|
|
825
824
|
return null;
|
|
826
|
-
const sha =
|
|
825
|
+
const sha = run.head_sha ?? "";
|
|
827
826
|
if (sha === "")
|
|
828
827
|
return null;
|
|
829
|
-
const name =
|
|
830
|
-
const commit =
|
|
831
|
-
return { key: `checks:${sha}`, at:
|
|
832
|
-
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${
|
|
833
|
-
source: { kind: "github", label: `Failed check on ${config.branch}`, url:
|
|
828
|
+
const name = run.name || "Checks";
|
|
829
|
+
const commit = run.display_title ?? "";
|
|
830
|
+
return { key: `checks:${sha}`, at: run.created_at || null, title: `Checks failed on ${config.branch}: ${name}`,
|
|
831
|
+
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${run.html_url ?? ""}`,
|
|
832
|
+
source: { kind: "github", label: `Failed check on ${config.branch}`, url: run.html_url || null } };
|
|
834
833
|
}
|
|
835
834
|
async function fetchGitHub(config, trigger, gh) {
|
|
836
835
|
const since = trigger.cursor ?? trigger.createdAt;
|
|
@@ -861,20 +860,19 @@ async function fetchGitHub(config, trigger, gh) {
|
|
|
861
860
|
}
|
|
862
861
|
const created = Date.parse(trigger.createdAt);
|
|
863
862
|
if (config.watch === "checks") {
|
|
864
|
-
const runs = (
|
|
863
|
+
const runs = (readGithubRunList(body).workflow_runs ?? []).filter(one => Date.parse(one.created_at ?? "") > created);
|
|
865
864
|
const items = runs.reverse().map(one => githubRunItem(config, one)).filter((one) => one !== null);
|
|
866
865
|
return { ok: true, items: unique(items), cursor: trigger.cursor };
|
|
867
866
|
}
|
|
868
|
-
const list = Array.isArray(body) ? body : [];
|
|
869
867
|
if (labelEvents) {
|
|
870
|
-
const labeled =
|
|
871
|
-
&& Date.parse(
|
|
872
|
-
const items = labeled.map(one => { const item = githubItem(config,
|
|
873
|
-
return { ok: true, items: unique(items), cursor: labeled.map(one =>
|
|
868
|
+
const labeled = readGithubIssueEventList(body).filter(one => one.event === "labeled" && (one.label?.name ?? "").toLowerCase() === config.label.toLowerCase()
|
|
869
|
+
&& Date.parse(one.created_at ?? "") > Date.parse(since)).reverse();
|
|
870
|
+
const items = labeled.map(one => { const item = githubItem(config, one.issue, "issue"); return item === null ? null : { ...item, at: one.created_at || null }; }).filter((one) => one !== null);
|
|
871
|
+
return { ok: true, items: unique(items), cursor: labeled.map(one => one.created_at ?? "").at(-1) ?? trigger.cursor };
|
|
874
872
|
}
|
|
875
873
|
// New issues and pull requests: opened after the trigger was added, oldest first; the key keeps each to one card.
|
|
876
874
|
const what = config.watch === "pulls" ? "pull" : "issue";
|
|
877
|
-
const items =
|
|
875
|
+
const items = readGithubIssueList(body).filter(one => Date.parse(one.created_at ?? "") > created).reverse().map(one => githubItem(config, one, what)).filter((one) => one !== null);
|
|
878
876
|
return { ok: true, items, cursor: trigger.cursor };
|
|
879
877
|
}
|
|
880
878
|
const unique = (items) => items.filter((item, index) => items.findIndex(one => one.key === item.key) === index);
|
|
@@ -882,21 +880,20 @@ export const LINEAR_URL = "https://api.linear.app/graphql";
|
|
|
882
880
|
export const LINEAR_QUERY = "query FlowTrigger($filter: IssueFilter) { issues(filter: $filter, first: 50, orderBy: updatedAt) { nodes { identifier title description url updatedAt state { name } team { key } labels { nodes { name } } } } }";
|
|
883
881
|
/** A Linear issue as a card; null when it doesn't match the trigger. */
|
|
884
882
|
export function linearItem(config, raw) {
|
|
885
|
-
const issue =
|
|
886
|
-
const identifier =
|
|
883
|
+
const issue = readLinearIssue(raw);
|
|
884
|
+
const identifier = issue.identifier ?? "";
|
|
887
885
|
if (identifier === "")
|
|
888
886
|
return null;
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
if (config.team !== null && text(record(issue["team"])["key"]).toUpperCase() !== config.team)
|
|
887
|
+
const labels = linearLabelNames(issue).map(one => one.toLowerCase());
|
|
888
|
+
if (config.team !== null && (issue.team?.key ?? "").toUpperCase() !== config.team)
|
|
892
889
|
return null;
|
|
893
|
-
if (config.state !== null &&
|
|
890
|
+
if (config.state !== null && (issue.state?.name ?? "").toLowerCase() !== config.state.toLowerCase())
|
|
894
891
|
return null;
|
|
895
892
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
896
893
|
return null;
|
|
897
|
-
return { key: `linear:${identifier}`, at:
|
|
898
|
-
description: `From Linear ${identifier}:\n\n${
|
|
899
|
-
source: { kind: "linear", label: `Linear ${identifier}`, url:
|
|
894
|
+
return { key: `linear:${identifier}`, at: issue.updatedAt || null, title: issue.title || identifier,
|
|
895
|
+
description: `From Linear ${identifier}:\n\n${issue.description ?? ""}`.trim().slice(0, 4000),
|
|
896
|
+
source: { kind: "linear", label: `Linear ${identifier}`, url: issue.url || null } };
|
|
900
897
|
}
|
|
901
898
|
async function fetchLinear(config, trigger, io) {
|
|
902
899
|
const key = readLinearKey(io.dir);
|
|
@@ -917,22 +914,22 @@ async function fetchLinear(config, trigger, io) {
|
|
|
917
914
|
catch {
|
|
918
915
|
return { ok: false, problem: "Couldn't reach Linear." };
|
|
919
916
|
}
|
|
920
|
-
let
|
|
917
|
+
let raw;
|
|
921
918
|
try {
|
|
922
|
-
|
|
919
|
+
raw = await response.json();
|
|
923
920
|
}
|
|
924
921
|
catch {
|
|
925
922
|
return { ok: false, problem: `Linear answered ${response.status} and it couldn't be read.` };
|
|
926
923
|
}
|
|
927
|
-
const
|
|
924
|
+
const body = readLinearAnswer(raw);
|
|
925
|
+
const errors = (body.errors ?? []).map(one => one.message ?? "");
|
|
928
926
|
if (response.status === 401 || errors.some(one => /authenticat|api key/i.test(one)))
|
|
929
927
|
return { ok: false, problem: "Linear didn't accept the key. Add a new one on this flow's Triggers panel." };
|
|
930
928
|
if (!response.ok || errors.length > 0)
|
|
931
929
|
return { ok: false, problem: `Linear said: ${(errors[0] ?? `status ${response.status}`).slice(0, 160)}` };
|
|
932
|
-
const
|
|
933
|
-
const issues = (Array.isArray(nodes) ? nodes : []).sort((a, b) => text(record(a)["updatedAt"]).localeCompare(text(record(b)["updatedAt"])));
|
|
930
|
+
const issues = (body.data?.issues?.nodes ?? []).sort((a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? ""));
|
|
934
931
|
const items = issues.map(one => linearItem(config, one)).filter((one) => one !== null);
|
|
935
|
-
return { ok: true, items, cursor: issues.map(one =>
|
|
932
|
+
return { ok: true, items, cursor: issues.map(one => one.updatedAt ?? "").filter(one => one !== "").at(-1) ?? trigger.cursor };
|
|
936
933
|
}
|
|
937
934
|
const header = (delivery, name) => { const value = delivery.headers[name]; return Array.isArray(value) ? value[0] ?? "" : value ?? ""; };
|
|
938
935
|
function sameHex(expected, given) {
|
|
@@ -963,8 +960,8 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
963
960
|
const signature = header(delivery, "x-hub-signature-256");
|
|
964
961
|
if (secret === null || !sameHex(`sha256=${createHmac("sha256", secret).update(delivery.body).digest("hex")}`, signature))
|
|
965
962
|
return { status: 401, said: "Signature doesn't match." };
|
|
966
|
-
const event = header(delivery, "x-github-event"), body =
|
|
967
|
-
const repository =
|
|
963
|
+
const event = header(delivery, "x-github-event"), body = readGithubEvent(payload), action = body.action ?? "";
|
|
964
|
+
const repository = body.repository?.full_name ?? "";
|
|
968
965
|
if (event === "ping") {
|
|
969
966
|
note("GitHub connected.");
|
|
970
967
|
return { status: 200, said: "Connected." };
|
|
@@ -972,10 +969,10 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
972
969
|
if (repository.toLowerCase() !== config.repo.toLowerCase())
|
|
973
970
|
return { status: 202, said: "Not this repository." };
|
|
974
971
|
// A label trigger takes the moment its label is added (or an issue opened with it); a plain one takes new issues.
|
|
975
|
-
const labeledNow = action === "labeled" && config.label !== null &&
|
|
976
|
-
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body
|
|
977
|
-
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body
|
|
978
|
-
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body
|
|
972
|
+
const labeledNow = action === "labeled" && config.label !== null && (body.label?.name ?? "").toLowerCase() === config.label.toLowerCase();
|
|
973
|
+
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body.issue, "issue")
|
|
974
|
+
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
|
|
975
|
+
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
|
|
979
976
|
items = found === null ? [] : [found];
|
|
980
977
|
}
|
|
981
978
|
else if (config.kind === "linear") {
|
|
@@ -984,23 +981,26 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
984
981
|
return { status: 401, said: "Paste Linear's signing secret on the flow's Triggers panel first." };
|
|
985
982
|
if (!sameHex(createHmac("sha256", secret).update(delivery.body).digest("hex"), header(delivery, "linear-signature")))
|
|
986
983
|
return { status: 401, said: "Signature doesn't match." };
|
|
987
|
-
const body =
|
|
984
|
+
const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
|
|
988
985
|
if (!Number.isFinite(sent) || Math.abs(now.getTime() - sent) > 5 * 60_000)
|
|
989
986
|
return { status: 401, said: "Too old." };
|
|
990
|
-
if (
|
|
987
|
+
if (body.type !== "Issue" || !["create", "update"].includes(body.action ?? ""))
|
|
991
988
|
return { status: 202, said: "Ignored." };
|
|
992
989
|
// A state trigger fires when the issue arrives in that state, not on every later edit there.
|
|
993
|
-
const moved =
|
|
994
|
-
const found = moved ? linearItem(config, body
|
|
990
|
+
const moved = body.action === "create" || body.updatedFrom?.stateId !== undefined || config.state === null;
|
|
991
|
+
const found = moved ? linearItem(config, body.data) : null;
|
|
995
992
|
items = found === null ? [] : [found];
|
|
996
993
|
}
|
|
997
994
|
else if (config.kind === "webhook") {
|
|
998
|
-
|
|
995
|
+
// Any JSON, kept as sent: the trigger's own paths are picked from it, and it is shown whole when they find no text.
|
|
996
|
+
const body = readWebhookPayload(payload);
|
|
997
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
998
|
+
const pick = (path) => path === null ? null : path.split(".").reduce((node, part) => record(node)[part], body);
|
|
999
999
|
const title = pick(config.titleField ?? "title") ?? pick("summary") ?? pick("message");
|
|
1000
1000
|
const bodyText = pick(config.bodyField ?? "description") ?? pick("body") ?? pick("text");
|
|
1001
1001
|
const id = header(delivery, "x-request-id") || header(delivery, "x-delivery-id") || header(delivery, "idempotency-key") || createHash("sha256").update(delivery.body).digest("hex");
|
|
1002
1002
|
items = [{ key: `hook:${id.slice(0, 120)}`, at: null, title: typeof title === "string" && title.trim() !== "" ? title : config.title,
|
|
1003
|
-
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(
|
|
1003
|
+
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(body, null, 2)}`.slice(0, 4000),
|
|
1004
1004
|
source: { kind: "webhook", label: "Webhook", url: null } }];
|
|
1005
1005
|
}
|
|
1006
1006
|
if (items.length === 0)
|
|
@@ -1081,13 +1081,14 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1081
1081
|
const again = `<p><a href="${esc(FORM_PATH + token)}">Send another</a></p>`;
|
|
1082
1082
|
const thanks = formHtml(config.label, `<p>Thanks — it's been sent.</p>${again}`);
|
|
1083
1083
|
// A filled trap, or a page posted faster than a person could, gets thanks and makes nothing.
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1084
|
+
const form = readFormSubmission(fields, config.questions.length);
|
|
1085
|
+
const shown = Number(form.t);
|
|
1086
|
+
if ((form.website ?? "") !== "" || !Number.isFinite(shown) || now.getTime() - shown < 2000 || now.getTime() - shown > 86_400_000)
|
|
1086
1087
|
return { status: 200, html: thanks };
|
|
1087
1088
|
const recent = store.handle.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND key LIKE 'form:%' AND at > ?").get(trigger.id, new Date(now.getTime() - 3_600_000).toISOString());
|
|
1088
1089
|
if (Number(recent?.["n"] ?? 0) >= FORM_CARDS_PER_HOUR)
|
|
1089
1090
|
return { status: 429, html: formHtml(config.label, `<p>Too many sent in the last hour. Try again later.</p>`) };
|
|
1090
|
-
const answers =
|
|
1091
|
+
const answers = form.answers.map((answer, index) => (answer ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, index === 0 ? 200 : 3000));
|
|
1091
1092
|
if (answers[0] === "")
|
|
1092
1093
|
return { status: 400, html: formHtml(config.label, `<p>Answer “${esc(config.questions[0])}” first.</p>${again}`) };
|
|
1093
1094
|
const details = config.questions.slice(1).map((question, index) => answers[index + 1] === "" ? null : `${question}\n${answers[index + 1]}`).filter(one => one !== null).join("\n\n");
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -188,6 +188,34 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
188
188
|
readonly takesValue: true;
|
|
189
189
|
readonly meaning: 'a zone id or name';
|
|
190
190
|
}];
|
|
191
|
+
}, {
|
|
192
|
+
readonly action: 'card approve';
|
|
193
|
+
readonly synopsis: "approve a card waiting at a Person decides step (or pick --option at a Person chooses step), as the console's Approve does";
|
|
194
|
+
readonly mutation: 'identity-idempotent';
|
|
195
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
196
|
+
readonly flags: readonly [...{
|
|
197
|
+
name: string;
|
|
198
|
+
takesValue: boolean;
|
|
199
|
+
meaning: string;
|
|
200
|
+
}[], {
|
|
201
|
+
readonly name: 'option';
|
|
202
|
+
readonly takesValue: true;
|
|
203
|
+
readonly meaning: "at a Person chooses step: the option's words or its number";
|
|
204
|
+
}];
|
|
205
|
+
}, {
|
|
206
|
+
readonly action: 'card send-back';
|
|
207
|
+
readonly synopsis: "send a waiting card back with a note, as the console's Send back (or a reply at a Person chooses step) does";
|
|
208
|
+
readonly mutation: 'identity-idempotent';
|
|
209
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
210
|
+
readonly flags: readonly [...{
|
|
211
|
+
name: string;
|
|
212
|
+
takesValue: boolean;
|
|
213
|
+
meaning: string;
|
|
214
|
+
}[], {
|
|
215
|
+
readonly name: 'note';
|
|
216
|
+
readonly takesValue: true;
|
|
217
|
+
readonly meaning: 'what should change';
|
|
218
|
+
}];
|
|
191
219
|
}, {
|
|
192
220
|
readonly action: 'export';
|
|
193
221
|
readonly synopsis: 'write a flow as a *.toolroll-flow.json file: zones, paths, trigger settings and scripts; never secrets, webhook addresses, names or cards';
|
package/dist/flows-cli.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
11
11
|
import { resolve } from 'node:path';
|
|
12
12
|
import { envelopeJson } from './envelope.js';
|
|
13
|
-
import { addCardToFlow, advanceFlows, crossProjectProblem, flowDefinitionOf } from './flow-engine.js';
|
|
13
|
+
import { addCardToFlow, advanceFlows, crossProjectProblem, decideFlowCard, flowDefinitionOf } from './flow-engine.js';
|
|
14
|
+
import { chooseFlowCard } from './flow-send.js';
|
|
14
15
|
import { saveScript } from './flow-scripts.js';
|
|
15
16
|
import { exportFlow, fetchFlowFile, FLOW_FILE_MAX_BYTES, FlowFileError, importFlow, parseFlowFile, planFlowImport } from './flow-share.js';
|
|
16
17
|
import { STARTER_FLOWS, starterFlowOf, starterOf, startersFor, starterTerms, switchOnStarter } from './flow-starters.js';
|
|
@@ -51,6 +52,10 @@ export const FLOWS_DESCRIPTORS = [
|
|
|
51
52
|
{ name: 'title', takesValue: true, meaning: "the card's title" },
|
|
52
53
|
{ name: 'description', takesValue: true, meaning: 'its details' },
|
|
53
54
|
{ name: 'zone', takesValue: true, meaning: 'a zone id or name' }] },
|
|
55
|
+
{ action: 'card approve', synopsis: "approve a card waiting at a Person decides step (or pick --option at a Person chooses step), as the console's Approve does", mutation: 'identity-idempotent', positionals: ['flow', 'card'], flags: [...write,
|
|
56
|
+
{ name: 'option', takesValue: true, meaning: "at a Person chooses step: the option's words or its number" }] },
|
|
57
|
+
{ action: 'card send-back', synopsis: "send a waiting card back with a note, as the console's Send back (or a reply at a Person chooses step) does", mutation: 'identity-idempotent', positionals: ['flow', 'card'], flags: [...write,
|
|
58
|
+
{ name: 'note', takesValue: true, meaning: 'what should change' }] },
|
|
54
59
|
{ 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
60
|
{ name: 'out', takesValue: true, meaning: 'write the file here (default: print it)' }] },
|
|
56
61
|
{ 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,
|
|
@@ -407,6 +412,43 @@ export async function runFlowsCommand(positional, flags, context) {
|
|
|
407
412
|
const card = store.getFlowCard(added.card);
|
|
408
413
|
return ok({ applied: true, card: card === null ? { id: added.card } : cardOf(definition, card), said: added.said }, [`${added.said} Card #${added.card}.`]);
|
|
409
414
|
}
|
|
415
|
+
if (action === 'card approve' || action === 'card send-back') {
|
|
416
|
+
// The console's own doors (decideFlowCard, chooseFlowCard): the same decider, visit and project checks, and the same card history.
|
|
417
|
+
const approving = action === 'card approve';
|
|
418
|
+
const note = text('note')?.trim() ?? null;
|
|
419
|
+
if (args[1] === undefined || (!approving && (note === null || note === '')))
|
|
420
|
+
return fail('usage', approving ? 'Use flows card approve <flow> <card> [--option <words|number>].' : 'Use flows card send-back <flow> <card> --note "<what should change>".', EXIT.usage);
|
|
421
|
+
const card = /^[1-9][0-9]{0,9}$/.test(args[1]) ? store.getFlowCard(Number(args[1])) : null;
|
|
422
|
+
if (card === null || card.flow !== flow.id)
|
|
423
|
+
return refuse(flow.repo, 'unknown-card', `Flow #${flow.id} has no card #${args[1]}. toolroll flows show ${flow.id} lists its cards.`);
|
|
424
|
+
const stage = definition.stages.find(one => one.id === card.stage);
|
|
425
|
+
let decided;
|
|
426
|
+
if (stage?.kind === 'choose') {
|
|
427
|
+
if (approving) {
|
|
428
|
+
const given = text('option')?.trim() ?? '';
|
|
429
|
+
const options = stage.options ?? [];
|
|
430
|
+
const choice = /^[1-9]$/.test(given) && Number(given) <= options.length ? Number(given) - 1 : options.findIndex(one => one.label.toLowerCase() === given.toLowerCase());
|
|
431
|
+
if (given === '' || choice < 0)
|
|
432
|
+
return refuse(flow.repo, 'choose-option', `${stage.title} asks for a choice: give --option with one of ${options.map((one, at) => `${at + 1} “${one.label}”`).join(', ')}.`, EXIT.usage);
|
|
433
|
+
decided = chooseFlowCard(store, { card: card.id, entry: card.entry, choice, label: options[choice].label, note: null, actor: who, where: 'the command line', repos: reachable, evidenceRoot: context.evidenceRoot }, now);
|
|
434
|
+
}
|
|
435
|
+
else
|
|
436
|
+
decided = chooseFlowCard(store, { card: card.id, entry: card.entry, choice: null, note, actor: who, where: 'the command line', repos: reachable, evidenceRoot: context.evidenceRoot }, now);
|
|
437
|
+
}
|
|
438
|
+
else if (stage?.kind === 'approval') {
|
|
439
|
+
if (text('option') !== null)
|
|
440
|
+
return fail('usage', '--option is for a Person chooses step; this card waits for a decision.', EXIT.usage);
|
|
441
|
+
decided = decideFlowCard(store, { card: card.id, decision: approving ? 'approve' : 'send-back', note, actor: who, repos: reachable, evidenceRoot: context.evidenceRoot, entry: card.entry, where: 'the command line' }, now);
|
|
442
|
+
}
|
|
443
|
+
else
|
|
444
|
+
return refuse(flow.repo, 'not-waiting', card.state === 'active' ? `Card #${card.id} isn't waiting for a decision; it's in ${stage?.title ?? card.stage}.` : `Card #${card.id} is ${card.state}; nothing is waiting on it.`);
|
|
445
|
+
if (!decided.ok)
|
|
446
|
+
return refuse(flow.repo, 'refused', decided.message);
|
|
447
|
+
record(flow.repo, 'accepted', `flow #${flow.id} card #${card.id} · ${stage.title}`);
|
|
448
|
+
settle(flow.repo);
|
|
449
|
+
const after = store.getFlowCard(card.id);
|
|
450
|
+
return ok({ applied: true, card: after === null ? { id: card.id } : cardOf(definition, after), said: decided.said }, [decided.said]);
|
|
451
|
+
}
|
|
410
452
|
if (action === 'trigger add') {
|
|
411
453
|
if (args[1] === undefined)
|
|
412
454
|
return fail('usage', 'Use flows trigger add <flow> <json|file|->, like \'{"kind":"schedule","schedule":"daily 09:00","title":"Standup"}\'.', EXIT.usage);
|
package/dist/guides.js
CHANGED
|
@@ -141,6 +141,8 @@ applies the console's rules exactly.
|
|
|
141
141
|
\`flows trigger pause|resume|remove|check <id> <trigger>\`; \`flows script
|
|
142
142
|
save --repo PATH --name <n> (--file <path in project> | --body <file>)
|
|
143
143
|
--about "<line>"\`; \`flows card add <id> --title <t> [--zone <z>]\`;
|
|
144
|
+
\`flows card approve <id> <card>\` and \`flows card send-back <id> <card>
|
|
145
|
+
--note <text>\` only when the person asked for that decision;
|
|
144
146
|
\`flows archive <id>\`.
|
|
145
147
|
- create, edit, archive and trigger add answer with a preview
|
|
146
148
|
(\`applied: false\`, \`terms\`) until \`--yes\`. Show the person the terms
|
package/dist/mate-contract.js
CHANGED
|
@@ -16,7 +16,7 @@ export const MATE_CONTRACT = [
|
|
|
16
16
|
"Investigate before reporting a limitation: when a tool refuses or something looks unsupported, read get_capabilities and the relevant skill (get_skills, then that skill's instructions) before telling the owner it can't be done. Then say what you checked, what is missing, and the one next step.",
|
|
17
17
|
"Ask rarely: only when the answer changes the work and you cannot settle it from tools or project memory. Then use ask_owner: one question, 2-4 short options (Something else is added), shown as buttons after your reply; the tapped option comes back as the owner's next message. Keep your reply to one short line leading into it, and do not repeat the options in your text.",
|
|
18
18
|
"propose_* drafts, never completes. Confirmation and execution approvals still apply. Never request credentials in chat. Read get_task currentExecution before acting; revisions retain task identity and old cards retain exact targets.",
|
|
19
|
-
"The operator talks plainly and names work by what it is about ('the login page', 'that payout fix'), rarely by id. Use the current task when there is one; otherwise find it with list_tasks search, and if several fit, ask which by title. When they want work moved forward, read get_task and draft its next step: changes to a result still waiting on them → propose_review revise; guidance for work in progress → propose_steer; stopped → resume and failed → retry with propose_task_action; waiting for approval → show_control approval; new work, or more on something already completed → propose_task in the same project, naming the earlier task in its goal.",
|
|
19
|
+
"The operator talks plainly and names work by what it is about ('the login page', 'that payout fix'), rarely by id. Use the current task when there is one; otherwise find it with list_tasks search, and if several fit, ask which by title. When they want work moved forward, read get_task and draft its next step: changes to a result still waiting on them → propose_review revise; guidance for work in progress → propose_steer; stopped → resume and failed → retry with propose_task_action; waiting for approval, or 'approve X' → offer_approval for exactly that plan or flow card, which sends its own Approve buttons to the owner's chat (their tap approves it; you never do), and only when it says why not, say that in one line and show_control approval; new work, or more on something already completed → propose_task in the same project, naming the earlier task in its goal.",
|
|
20
20
|
"Talk like a colleague in everyday words: task titles, 'the changes', 'the checks', 'try again'. Never say execution, run, scope, digest, dispatch or lane to the operator; say what happens next and what they can do.",
|
|
21
21
|
"Read get_result for the exact execution/run; page nextFeedbackOffset. propose_review revise requests changes; note saves feedback. Use read notes only; clarify ambiguous intent. Ready is a saved result, not a reviewer stage. Historical missing assessments never require rerunning work. Failed checks and missing files remain visible; completion never deploys or changes checks.",
|
|
22
22
|
"For screenshots, call get_result_images for that exact execution and run. On Telegram say they follow, never that they were delivered; elsewhere name result and image count. Limit 8/reply. If nextImageOffset exists, name remaining count; call it again with that offset or with the image ids it listed when asked. Select a newer revision explicitly; never describe unselected images or expose paths.",
|