toolroll 0.9.42 → 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 +19 -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 +1 -1
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- 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/operate.js +53 -3
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +51 -2
- package/dist/store.d.ts +8 -1
- package/dist/store.js +42 -2
- package/dist/surface.js +4 -1
- 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/package.json +1 -1
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.",
|
package/dist/mate-tools.js
CHANGED
|
@@ -48,7 +48,9 @@ import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommand
|
|
|
48
48
|
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowFromSteps, stepsFor, withKeptSteps } from "./flows.js";
|
|
49
49
|
import { parseContract, toModelSchema } from "./contracts/contract.js";
|
|
50
50
|
import { LEAD_TOOL_INPUTS, LEAD_TOOL_READ_INPUTS, LEAD_TOOL_OPTIONS, LEAD_TOOL_OUTPUTS, reportToolOutput } from "./contracts/lead-tools.js";
|
|
51
|
-
import { flowDefinitionOf } from "./flow-engine.js";
|
|
51
|
+
import { flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
52
|
+
import { planInChat } from "./chat-decide.js";
|
|
53
|
+
import { flowDecisionAt } from "./telegram-flow.js";
|
|
52
54
|
import { flowPersonOf } from "./flow-send.js";
|
|
53
55
|
import { flowInsights } from "./flow-insights.js";
|
|
54
56
|
import { describeTrigger, FLOW_TRIGGER_KINDS, triggerConfigOf } from "./flow-triggers.js";
|
|
@@ -491,6 +493,46 @@ const MATE_TOOL_HANDLERS = {
|
|
|
491
493
|
return id === null ? tooMany() : { ok: true, body: { card: id, label: entry.label, action: "open existing control; nothing changed" } };
|
|
492
494
|
},
|
|
493
495
|
},
|
|
496
|
+
offer_approval: {
|
|
497
|
+
description: "When the owner asks to approve one thing (a plan waiting on them, or a flow card waiting at a Person decides step), send that item's own Approve and Send back buttons to their paired chat right away. Their tap there is the approval (two taps, as on any decision card); you never approve. Give task for a plan, or card for a flow card (its number from get_flows). When it can't be approved in chat, the answer says why: say that in one line and open show_control approval (a plan) or the flow, never that you can't approve.",
|
|
498
|
+
handle: (ctx, args) => {
|
|
499
|
+
const pairedHere = ctx.channel === "telegram";
|
|
500
|
+
const paired = ctx.store.handle.prepare("SELECT 1 FROM telegram_binding WHERE approver = ? AND approver_generation = ? AND revoked_at IS NULL LIMIT 1")
|
|
501
|
+
.get(ctx.who.name, ctx.store.accountOf(ctx.who.name)?.generation ?? -1) !== undefined;
|
|
502
|
+
const stamp = ctx.now.getTime();
|
|
503
|
+
if (args["card"] !== undefined) {
|
|
504
|
+
const cardId = Number(args["card"]);
|
|
505
|
+
const card = Number.isSafeInteger(cardId) ? ctx.store.getFlowCard(cardId) : null;
|
|
506
|
+
const flow = card === null ? null : ctx.store.getFlow(card.flow);
|
|
507
|
+
if (card === null || flow === null || !ctx.who.repos.includes(flow.repo))
|
|
508
|
+
return { ok: false, message: "not-found: no such flow card in your projects" };
|
|
509
|
+
const waiting = flowDecisionAt(ctx.store, card.id, card.entry);
|
|
510
|
+
const href = flowCardHref(flow.id, card.id);
|
|
511
|
+
if (waiting === null)
|
|
512
|
+
return { ok: true, body: { offered: false, why: card.state === "active" ? "That card isn't waiting for a decision now." : "That card isn't waiting any more.", open: href } };
|
|
513
|
+
const decider = deciderOf(waiting.stage, waiting.flow);
|
|
514
|
+
if (decider !== null && decider !== ctx.who.name)
|
|
515
|
+
return { ok: true, body: { offered: false, why: `Only ${decider} decides here.`, open: href } };
|
|
516
|
+
if (!pairedHere || !paired)
|
|
517
|
+
return { ok: true, body: { offered: false, why: pairedHere ? "Your chat isn't paired for decisions." : "Buttons go to your paired chat; here, approve it on the card.", open: href } };
|
|
518
|
+
ctx.store.enqueueNotification({ dedupeKey: `flow-decide:${card.id}:${card.entry}:ask:${stamp}`, kind: "flow-decision", pushClass: "attention", recipient: ctx.who.name,
|
|
519
|
+
subject: `${flow.name}: ${card.title} needs your decision`.slice(0, 200), body: `${waiting.stage.title}: approve it, or send it back with a note.`, link: href, source: { project: flow.repo } }, ctx.now);
|
|
520
|
+
return { ok: true, body: { offered: true, item: `flow card ${card.id}`, shown: "Approve and Send back buttons follow your reply in this chat; the owner's tap decides it" } };
|
|
521
|
+
}
|
|
522
|
+
const task = taskIdOf(args);
|
|
523
|
+
if (task === null || admittedRef(ctx, task) === null)
|
|
524
|
+
return notFound();
|
|
525
|
+
const current = ctx.store.taskFamilyOf(task, ctx.who.repos, false)?.current.id ?? task;
|
|
526
|
+
const plan = planInChat(ctx.store, current, ctx.who.name, ctx.now, ctx.evidenceRoot);
|
|
527
|
+
if (!plan.ok)
|
|
528
|
+
return { ok: true, body: { offered: false, task: current, why: plan.why, control: "approval" } };
|
|
529
|
+
if (!pairedHere || !paired)
|
|
530
|
+
return { ok: true, body: { offered: false, task: current, why: pairedHere ? "Your chat isn't paired for decisions." : "Buttons go to your paired chat; here, approve it on its card.", control: "approval" } };
|
|
531
|
+
ctx.store.enqueueNotification({ dedupeKey: `plan-ready:${current}:ask:${stamp}`, kind: "plan-ready", recipient: ctx.who.name, subject: `${current}: plan ready for review`,
|
|
532
|
+
body: "Approve it here, or open it to edit first.", source: { taskRef: ctx.store.lookupRef(current).id } }, ctx.now);
|
|
533
|
+
return { ok: true, body: { offered: true, item: `plan for ${current}`, task: current, shown: "Approve & start follows your reply in this chat; the owner's two taps approve it" } };
|
|
534
|
+
},
|
|
535
|
+
},
|
|
494
536
|
get_result: {
|
|
495
537
|
description: "Read exact execution/run and feedback. Use get_task currentExecution; page nextFeedbackOffset.",
|
|
496
538
|
handle: (ctx, args) => {
|
package/dist/operate.js
CHANGED
|
@@ -204,6 +204,7 @@ import { readPulls } from "./pulls.js";
|
|
|
204
204
|
import { startMaintenance } from "./maintenance.js";
|
|
205
205
|
import { livePin, modelWatchPass } from "./model-catalog.js";
|
|
206
206
|
import { runModelsCommand } from "./models-cli.js";
|
|
207
|
+
import { ALL_PROJECTS, chatApprovalSettings, chatApprovalWords, setChatApproval } from "./chat-approval.js";
|
|
207
208
|
import { updateAdmissionPaused, UPDATE_PAUSED } from "./desktop-update-gate.js";
|
|
208
209
|
import { beads } from "./beads.js";
|
|
209
210
|
import { githubIssues } from "./issues.js";
|
|
@@ -470,6 +471,8 @@ Flows — processes cards move through; the console's rules
|
|
|
470
471
|
toolroll flows script save --repo <path> --name <name> (--file <path in project> | --body <file>)
|
|
471
472
|
--about "<one line>" [--language shell|python|node] [--timeout-minutes <n>]
|
|
472
473
|
toolroll flows card add <id> --title <t> [--description <d>] [--zone <zone>]
|
|
474
|
+
toolroll flows card approve <id> <card> [--option <words|number>]
|
|
475
|
+
toolroll flows card send-back <id> <card> --note <text>
|
|
473
476
|
toolroll flows archive <id>
|
|
474
477
|
writes take --as <you> --token <t> (or the remembered login); create,
|
|
475
478
|
edit, archive and trigger add preview until --yes
|
|
@@ -639,7 +642,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
639
642
|
"max", "cap", "probe", "kind", "expires", "cmd", "since", "repair-model",
|
|
640
643
|
"choose", "note", "max-open-decisions", "max-held-sessions", "name", "days", "publication", "auto-approve", "review-auto", "entries", "port", "host", "allow-host",
|
|
641
644
|
"for", "tick-every", "bridge-every", "reconcile-every", "incarnation",
|
|
642
|
-
"say", "ceiling-usd",
|
|
645
|
+
"say", "ceiling-usd", "cap-usd",
|
|
643
646
|
"token-file", "bin", "poll", "github", "remote", "head-prefix", "password",
|
|
644
647
|
// v102: project rules.
|
|
645
648
|
"not-requester", "protect", "protect-paths",
|
|
@@ -658,7 +661,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
658
661
|
// onboard: the starter flows to switch on.
|
|
659
662
|
"starter",
|
|
660
663
|
// flows: templates, steps, scripts and cards.
|
|
661
|
-
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone",
|
|
664
|
+
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone", "option",
|
|
662
665
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
663
666
|
// pings follow responsibility: replacements.
|
|
664
667
|
"replaced-by", "replaces",
|
|
@@ -671,7 +674,7 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
671
674
|
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
672
675
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
673
676
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
674
|
-
"self-heal", "plan-auto", "chat-approve", "repair-auto", "review-retry-auto", "no-local",
|
|
677
|
+
"self-heal", "plan-auto", "chat-approve", "full-access", "repair-auto", "review-retry-auto", "no-local",
|
|
675
678
|
"html", "csv", "alerts-only",
|
|
676
679
|
// v105: the full export.
|
|
677
680
|
"zip",
|
|
@@ -1088,6 +1091,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
1088
1091
|
return proposalsCommand(positional, flags, context);
|
|
1089
1092
|
case "mode":
|
|
1090
1093
|
return modeCommand(positional, flags, context);
|
|
1094
|
+
case "chat-approval":
|
|
1095
|
+
return chatApprovalCommand(positional, flags, context);
|
|
1091
1096
|
case "people":
|
|
1092
1097
|
return peopleCommand(positional, flags, context);
|
|
1093
1098
|
case "keys":
|
|
@@ -5755,6 +5760,51 @@ async function peopleCommand(positional, flags, context) {
|
|
|
5755
5760
|
]);
|
|
5756
5761
|
}
|
|
5757
5762
|
const MODE_ACTIONS = ["set", "show", "revoke"];
|
|
5763
|
+
/** `chat-approval show | on | off [--repo <path>]`: the person's lasting choice to approve plans and merges from their
|
|
5764
|
+
* paired chat (chat-approval.ts), for all their projects or one. On (or wider limits) previews until --yes and takes
|
|
5765
|
+
* the password, like Settings; off is one step. Either way a ledger line. */
|
|
5766
|
+
async function chatApprovalCommand(positional, flags, context) {
|
|
5767
|
+
const { store, write, json, clock } = context;
|
|
5768
|
+
const action = positional[0] ?? "show";
|
|
5769
|
+
const command = `chat-approval ${action}`;
|
|
5770
|
+
if (!["show", "on", "off"].includes(action))
|
|
5771
|
+
return fail(write, json, "chat-approval", "usage", "Use chat-approval show | on | off [--repo <path>] [--full-access] [--cap-usd <n>] [--yes].", EXIT.usage);
|
|
5772
|
+
const acting = await askCredentials(flags, context);
|
|
5773
|
+
if (acting === null)
|
|
5774
|
+
return fail(write, json, command, "unauthenticated", "Approving from chat is your own setting: pass --as <you> --token <your password> (or sign in once with toolroll up).", EXIT.refused);
|
|
5775
|
+
const authed = authenticateApprover(store, acting.name, acting.token);
|
|
5776
|
+
if (!authed.ok)
|
|
5777
|
+
return fail(write, json, command, "unauthenticated", "That name and password aren't an approver's. Nothing was changed.", EXIT.refused);
|
|
5778
|
+
const given = text(flags, "repo");
|
|
5779
|
+
const scope = given === undefined ? ALL_PROJECTS : canonicalProject(given) ?? resolve(given);
|
|
5780
|
+
if (scope !== ALL_PROJECTS && !store.accountCanAccess(acting.name, scope))
|
|
5781
|
+
return fail(write, json, command, "unknown-project", "That isn't one of your projects.", EXIT.refused);
|
|
5782
|
+
const where = scope === ALL_PROJECTS ? "all your projects" : projectName(scope);
|
|
5783
|
+
if (action === "show") {
|
|
5784
|
+
const rows = chatApprovalSettings(store, acting.name).map(one => ({ scope: one.scope === ALL_PROJECTS ? "all" : one.scope, enabled: one.enabled, fullAccess: one.fullAccess, capMicrousd: one.capMicrousd, updatedAt: one.updatedAt }));
|
|
5785
|
+
return succeed(write, json, command, { settings: rows }, () => rows.length === 0 ? ["Approving from chat is off everywhere. Turn it on: toolroll chat-approval on [--repo <path>]"]
|
|
5786
|
+
: rows.map(one => `${one.scope === "all" ? "All projects" : projectName(one.scope)}: ${one.enabled ? `on · ${chatApprovalWords(one)}` : "off"}`));
|
|
5787
|
+
}
|
|
5788
|
+
const now = clock();
|
|
5789
|
+
if (action === "off") {
|
|
5790
|
+
const saved = setChatApproval(store, { approver: acting.name, scope, enabled: false, via: "the command line" }, now);
|
|
5791
|
+
return saved.ok ? succeed(write, json, command, { applied: true, scope: scope === ALL_PROJECTS ? "all" : scope }, () => [saved.said]) : fail(write, json, command, "refused", saved.message, EXIT.refused);
|
|
5792
|
+
}
|
|
5793
|
+
const usd = text(flags, "cap-usd");
|
|
5794
|
+
const cap = usd === undefined ? null : Number(usd);
|
|
5795
|
+
if (cap !== null && !(Number.isFinite(cap) && cap >= 0 && cap <= 10_000))
|
|
5796
|
+
return fail(write, json, command, "usage", "Give --cap-usd in dollars, like 5.", EXIT.usage);
|
|
5797
|
+
const limits = { fullAccess: flag(flags, "full-access"), capMicrousd: cap === null ? null : Math.round(cap * 1_000_000) };
|
|
5798
|
+
const terms = [
|
|
5799
|
+
`Your own taps in your paired chat approve plans and merges in ${where}, until you turn it off: it doesn't expire.`,
|
|
5800
|
+
`Limits: ${chatApprovalWords(limits)}.`,
|
|
5801
|
+
"Plans written for you, protected paths, two-person rules and your organisation's policy still open in Toolroll.",
|
|
5802
|
+
];
|
|
5803
|
+
if (!flag(flags, "yes"))
|
|
5804
|
+
return succeed(write, json, command, { applied: false, scope: scope === ALL_PROJECTS ? "all" : scope, terms }, () => [`Turn on approving from chat for ${where}`, ...terms.map(one => ` ${one}`), "", "Nothing has changed. Add --yes to make this change."]);
|
|
5805
|
+
const saved = setChatApproval(store, { approver: acting.name, scope, enabled: true, limits, via: "the command line" }, now);
|
|
5806
|
+
return saved.ok ? succeed(write, json, command, { applied: true, scope: scope === ALL_PROJECTS ? "all" : scope, terms }, () => [saved.said]) : fail(write, json, command, "refused", saved.message, EXIT.refused);
|
|
5807
|
+
}
|
|
5758
5808
|
async function modeCommand(positional, flags, context) {
|
|
5759
5809
|
const { store, write, json, clock } = context;
|
|
5760
5810
|
const [action] = positional;
|
|
@@ -50,7 +50,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
50
50
|
digest: string;
|
|
51
51
|
approvedAt: string;
|
|
52
52
|
approvedBy: string;
|
|
53
|
-
approvalBasis: "mode" | "password";
|
|
53
|
+
approvalBasis: "chat" | "mode" | "password";
|
|
54
54
|
modeDigest: string | null;
|
|
55
55
|
};
|
|
56
56
|
readonly command: {
|
package/dist/scope.d.ts
CHANGED
|
@@ -263,8 +263,10 @@ export type Scope = SavedScopeTerms & {
|
|
|
263
263
|
/** The digest that was actually agreed to, which may now be stale. */
|
|
264
264
|
approvedDigest: string | null;
|
|
265
265
|
/** How the approval happened (v29): 'password' = the ceremony;
|
|
266
|
-
* 'mode' = sealed by the signer's live mode (modeDigest names it)
|
|
267
|
-
|
|
266
|
+
* 'mode' = sealed by the signer's live mode (modeDigest names it);
|
|
267
|
+
* 'chat' = the owner's own two taps in their paired chat, under their chat-approval setting or a signed term
|
|
268
|
+
* (modeDigest names that authority; the yes is the person's and doesn't lapse with it). */
|
|
269
|
+
approvalBasis?: "password" | "mode" | "chat" | null;
|
|
268
270
|
modeDigest?: string | null;
|
|
269
271
|
/** v24 (optional so hand-built scopes in tests stay valid): the working
|
|
270
272
|
* execution profile, its resolution state, and the immutable snapshot
|
package/dist/serve.js
CHANGED
|
@@ -154,6 +154,7 @@ import { MODELS_CSS, modelsHtml, modelsScript } from "./models-ui.js";
|
|
|
154
154
|
import { ASSISTANTS, modelChoices, detectPreparation, previewProjectInstructions, addProjectInstructions } from "./setup-guide.js";
|
|
155
155
|
import { previewSetup, approveSetup } from "./control-setup.js";
|
|
156
156
|
import { controlSetupHtml, setupPreviewHtml, connectionHtml, connectionWords, hiddenFields } from "./control-ui.js";
|
|
157
|
+
import { ALL_PROJECTS as CHAT_APPROVAL_ALL, chatApprovalWords, setChatApproval } from "./chat-approval.js";
|
|
157
158
|
import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
|
|
158
159
|
import { listCoordinators } from "./coordinator.js";
|
|
159
160
|
import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
|
|
@@ -1721,7 +1722,7 @@ export function createDecisionServer(options) {
|
|
|
1721
1722
|
const ref = run === null ? null : store.refForId(run.taskRef);
|
|
1722
1723
|
return { repo: ref?.repo ?? null, taskId: ref?.externalId ?? null, runId, action: `${resource[1] === "d" ? "decision" : resource[1] === "i" ? "incident" : "run"} ${resource[3] ?? "view"}` };
|
|
1723
1724
|
}
|
|
1724
|
-
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/routines/add", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke"]);
|
|
1725
|
+
const known = new Set(["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/tasks/add", "/queue/move", "/queue/note", "/routines/add", "/people/invite", "/people/invite-revoke", "/people/revoke", "/people/projects", "/projects/select", "/projects/open", "/mode/confirm", "/mode/sign", "/mode/revoke", "/settings/chat-approval/save", "/settings/chat-approval/off"]);
|
|
1725
1726
|
const placed = ["/tasks/add", "/routines/add"].includes(url.pathname) ? body?.get("repo")?.trim() : null;
|
|
1726
1727
|
return { repo: url.pathname.startsWith("/people/") ? null : placed ? canonicalProject(placed) ?? placed : projectOf(who, request) ?? null, taskId: null, runId: null,
|
|
1727
1728
|
action: known.has(url.pathname) ? url.pathname.slice(1).replaceAll("/", " ") : "console request" };
|
|
@@ -1749,6 +1750,9 @@ export function createDecisionServer(options) {
|
|
|
1749
1750
|
if ((request.method === "GET" && path === "/settings/telegram") ||
|
|
1750
1751
|
(request.method === "POST" && ["/settings/telegram/pair", "/settings/telegram/unpair", "/settings/telegram/retry"].includes(path)))
|
|
1751
1752
|
return true;
|
|
1753
|
+
// A person's own chat-approval setting; the route proves each project it names.
|
|
1754
|
+
if (request.method === "POST" && ["/settings/chat-approval/confirm", "/settings/chat-approval/save", "/settings/chat-approval/off"].includes(path))
|
|
1755
|
+
return true;
|
|
1752
1756
|
// Coding routes require an instance operator, then prove saved ownership and project access.
|
|
1753
1757
|
if (path === "/code" || path.startsWith("/code/"))
|
|
1754
1758
|
return true;
|
|
@@ -7287,7 +7291,7 @@ export function createDecisionServer(options) {
|
|
|
7287
7291
|
const note = (body.get("note") ?? "").trim() || null;
|
|
7288
7292
|
if (note !== null && note.length > LIMITS.note)
|
|
7289
7293
|
return answer(400, { ok: false, said: `Keep the note to ${LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` });
|
|
7290
|
-
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft") }, now);
|
|
7294
|
+
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft"), where: "the console" }, now);
|
|
7291
7295
|
return decided.ok ? settle(decided.said) : answer(409, { ok: false, said: decided.message });
|
|
7292
7296
|
}
|
|
7293
7297
|
if (verb === "choose") {
|
|
@@ -8287,6 +8291,51 @@ export function createDecisionServer(options) {
|
|
|
8287
8291
|
store.retryTelegramReplies(who.name, now);
|
|
8288
8292
|
return redirect(response, "/settings/telegram");
|
|
8289
8293
|
}
|
|
8294
|
+
// Approving from chat, as the person's lasting setting (chat-approval.ts): for all their projects or one. Turning it
|
|
8295
|
+
// on or changing its limits is the mode's ceremony (the exact terms, a single-use nonce bound to them, the password
|
|
8296
|
+
// typed again); turning it off is one step, csrf only. Every change is a ledger line.
|
|
8297
|
+
if (url.pathname === "/settings/chat-approval/confirm" || url.pathname === "/settings/chat-approval/save" || url.pathname === "/settings/chat-approval/off") {
|
|
8298
|
+
const body = readForm(posted, CONSOLE_FORMS.chatApproval);
|
|
8299
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
8300
|
+
return refuse(response, who, 403, "An approver turns on approving from their own chat.", "/settings");
|
|
8301
|
+
if (["scope", "full-access", "cap-usd", "nonce", "digest", "token"].some(key => body.getAll(key).length > 1))
|
|
8302
|
+
return refuse(response, who, 400, "Submit one value for each field.", "/settings");
|
|
8303
|
+
const given = (body.get("scope") ?? "all").trim();
|
|
8304
|
+
const scope = given === "all" || given === "" ? CHAT_APPROVAL_ALL : canonicalProject(given) ?? given;
|
|
8305
|
+
if (scope !== CHAT_APPROVAL_ALL && !(visible(scope) && store.accountCanAccess(who.name, scope)))
|
|
8306
|
+
return refuse(response, who, 403, "That isn't one of your projects.", "/settings");
|
|
8307
|
+
const where = scope === CHAT_APPROVAL_ALL ? "all your projects" : projectName(scope);
|
|
8308
|
+
if (url.pathname.endsWith("/off")) {
|
|
8309
|
+
const saved = setChatApproval(store, { approver: who.name, scope, enabled: false, via: "the console" }, now);
|
|
8310
|
+
return saved.ok ? redirect(response, `/settings?said=${encodeURIComponent(saved.said)}`) : refuse(response, who, 400, saved.message, "/settings");
|
|
8311
|
+
}
|
|
8312
|
+
const usd = (body.get("cap-usd") ?? "").trim();
|
|
8313
|
+
const cap = usd === "" ? null : Number(usd);
|
|
8314
|
+
if (cap !== null && !(Number.isFinite(cap) && cap >= 0 && cap <= 10_000))
|
|
8315
|
+
return refuse(response, who, 400, "Give the attempt limit in dollars, like 5.", "/settings");
|
|
8316
|
+
const limits = { fullAccess: body.get("full-access") === "1", capMicrousd: cap === null ? null : Math.round(cap * 1_000_000) };
|
|
8317
|
+
const digest = createHash("sha256").update(JSON.stringify({ scope, ...limits })).digest("hex");
|
|
8318
|
+
if (url.pathname.endsWith("/confirm")) {
|
|
8319
|
+
const nonce = mintApprovalNonce(who.name, "chat-approval", digest);
|
|
8320
|
+
const html = `<h1 style="overflow-wrap:anywhere">Approve from chat in ${escape(where)}</h1>` +
|
|
8321
|
+
`<form method="post" action="/settings/chat-approval/save" class="card approve-form">` +
|
|
8322
|
+
hiddenFields({ csrf: who.session.csrf, nonce, digest, scope: scope === CHAT_APPROVAL_ALL ? "all" : scope, "full-access": limits.fullAccess ? "1" : "", "cap-usd": usd }) +
|
|
8323
|
+
`<p>Your own taps in your paired chat approve plans and merges until you turn this off. It doesn't expire.</p>` +
|
|
8324
|
+
`<p class="recap">Limits: ${escape(chatApprovalWords(limits))}.</p>` +
|
|
8325
|
+
`<p class="recap">Plans written for you, protected paths, two-person rules and your organisation's policy still open in Toolroll.</p>` +
|
|
8326
|
+
`<label>Your password<input type="password" name="token" autocomplete="current-password" required></label>` +
|
|
8327
|
+
`<div class="sticky-actions"><button type="submit">Turn on</button></div></form>` +
|
|
8328
|
+
`<p class="meta"><a href="/settings">Back</a></p>`;
|
|
8329
|
+
return sendScreen(response, 200, screen("Approve from chat", html, { chrome: chromeFor(projectOf(who, request) ?? null, "settings") }));
|
|
8330
|
+
}
|
|
8331
|
+
if (!consumeApprovalNonce(body.get("nonce") ?? "", who.name, "chat-approval", body.get("digest") ?? "") || body.get("digest") !== digest)
|
|
8332
|
+
return refuse(response, who, 409, "That form is stale. Read the terms again.", "/settings");
|
|
8333
|
+
const proved = authenticateAccount(store, who.name, body.get("token") ?? "");
|
|
8334
|
+
if (!proved.ok || proved.role !== "approver" || proved.generation !== who.session.generation)
|
|
8335
|
+
return refuse(response, who, 403, "Turning on approving from chat takes your password, typed again.", "/settings");
|
|
8336
|
+
const saved = setChatApproval(store, { approver: who.name, scope, enabled: true, limits, via: "the console" }, now);
|
|
8337
|
+
return saved.ok ? redirect(response, `/settings?said=${encodeURIComponent(saved.said)}`) : refuse(response, who, 400, saved.message, "/settings");
|
|
8338
|
+
}
|
|
8290
8339
|
if (url.pathname === "/settings/telegram/pair" || url.pathname === "/settings/telegram/unpair") {
|
|
8291
8340
|
const body = readForm(posted, CONSOLE_FORMS.telegramPair);
|
|
8292
8341
|
// The person's own pairing, under their password: a code minted for
|
package/dist/store.d.ts
CHANGED
|
@@ -65,6 +65,13 @@ export declare const MATE_ASK_SCHEMA = "\nCREATE TABLE IF NOT EXISTS mate_ask (\
|
|
|
65
65
|
* per option for one visit of one card, placed on the message it rides. A reply to that message is the note
|
|
66
66
|
* (telegram_flow_prompt, mode send-back). The chat apps keep their buttons beside their messages (chat-delivery-state.ts). */
|
|
67
67
|
export declare const FLOW_SEND_SCHEMA = "\nCREATE TABLE IF NOT EXISTS flow_send (\n card INTEGER NOT NULL REFERENCES flow_card(id),\n entry INTEGER NOT NULL,\n stage TEXT NOT NULL,\n person TEXT NOT NULL,\n content_json TEXT NOT NULL,\n created_at TEXT NOT NULL,\n PRIMARY KEY (card, entry)\n);\nCREATE TABLE IF NOT EXISTS telegram_flow_choice (\n token TEXT PRIMARY KEY,\n binding INTEGER NOT NULL REFERENCES telegram_binding(id) ON DELETE RESTRICT,\n card INTEGER NOT NULL REFERENCES flow_card(id),\n entry INTEGER NOT NULL,\n choice INTEGER NOT NULL,\n label TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n message_id TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS telegram_flow_choice_visit ON telegram_flow_choice (card, entry);\n";
|
|
68
|
+
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
69
|
+
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
70
|
+
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
71
|
+
export declare const CHAT_APPROVAL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_approval_setting (\n approver TEXT NOT NULL,\n scope TEXT NOT NULL,\n enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),\n full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),\n attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),\n generation INTEGER NOT NULL,\n digest TEXT NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n PRIMARY KEY (approver, scope)\n);\n";
|
|
72
|
+
/** The second tap on a flow decision in Telegram (telegram-flow.ts): Approve arms Yes and Cancel, bound to the binding,
|
|
73
|
+
* chat, message and exact visit of the card; Yes decides it once. */
|
|
74
|
+
export declare const TELEGRAM_FLOW_CONFIRM_SCHEMA = "\nCREATE TABLE IF NOT EXISTS telegram_flow_confirm (\n token TEXT PRIMARY KEY,\n binding INTEGER NOT NULL,\n chat_id TEXT NOT NULL,\n message_id TEXT NOT NULL,\n card INTEGER NOT NULL,\n entry INTEGER NOT NULL,\n phase TEXT NOT NULL CHECK (phase IN ('yes','cancel')),\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS telegram_flow_confirm_visit ON telegram_flow_confirm (card, entry);\n";
|
|
68
75
|
export declare const CHAT_DECIDE_SCHEMA = "\nCREATE TABLE IF NOT EXISTS chat_decide_action (\n token TEXT PRIMARY KEY,\n channel TEXT NOT NULL,\n binding INTEGER NOT NULL,\n chat TEXT NOT NULL,\n message TEXT,\n act TEXT NOT NULL CHECK (act IN ('accept','changes','retry','approve','not-now','merge')),\n phase TEXT NOT NULL CHECK (phase IN ('offer','yes','cancel')),\n task_id TEXT NOT NULL,\n run INTEGER,\n digest TEXT NOT NULL,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS chat_decide_action_message ON chat_decide_action (channel, chat, message);\nCREATE TABLE IF NOT EXISTS chat_decide_prompt (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n channel TEXT NOT NULL,\n binding INTEGER NOT NULL,\n chat TEXT NOT NULL,\n task_id TEXT NOT NULL,\n run INTEGER NOT NULL,\n digest TEXT NOT NULL,\n message TEXT,\n created_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n consumed_at TEXT\n);\nCREATE INDEX IF NOT EXISTS chat_decide_prompt_open ON chat_decide_prompt (channel, binding, consumed_at);\n";
|
|
69
76
|
export type MateAsk = {
|
|
70
77
|
turn: number;
|
|
@@ -2274,7 +2281,7 @@ export declare class Store {
|
|
|
2274
2281
|
* exactly what the signed digest was bound to. Returns false when the
|
|
2275
2282
|
* scope vanished mid-ceremony. */
|
|
2276
2283
|
sealScopeApproval(taskId: string, by: string, now: Date, mutation?: Mutation, basis?: {
|
|
2277
|
-
kind: "mode";
|
|
2284
|
+
kind: "mode" | "chat";
|
|
2278
2285
|
modeDigest: string;
|
|
2279
2286
|
}, approverKind?: ApproverKind): boolean;
|
|
2280
2287
|
/** Whether this task's scope carries a LIVE seal: approved, and the
|
package/dist/store.js
CHANGED
|
@@ -354,6 +354,40 @@ CREATE TABLE IF NOT EXISTS telegram_flow_choice (
|
|
|
354
354
|
);
|
|
355
355
|
CREATE INDEX IF NOT EXISTS telegram_flow_choice_visit ON telegram_flow_choice (card, entry);
|
|
356
356
|
`;
|
|
357
|
+
/** An owner's lasting choice to approve plans and merges from their paired chat (chat-approval.ts), for all their
|
|
358
|
+
* projects (scope '*') or one (its path), with the limits they agreed to. Off unless a row says on; a project's own row
|
|
359
|
+
* outranks the all-projects one. `generation` is the account's at the change: a reset password ends it. */
|
|
360
|
+
export const CHAT_APPROVAL_SCHEMA = `
|
|
361
|
+
CREATE TABLE IF NOT EXISTS chat_approval_setting (
|
|
362
|
+
approver TEXT NOT NULL,
|
|
363
|
+
scope TEXT NOT NULL,
|
|
364
|
+
enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
|
|
365
|
+
full_access INTEGER NOT NULL DEFAULT 0 CHECK (full_access IN (0,1)),
|
|
366
|
+
attempt_cap_microusd INTEGER CHECK (attempt_cap_microusd IS NULL OR attempt_cap_microusd >= 0),
|
|
367
|
+
generation INTEGER NOT NULL,
|
|
368
|
+
digest TEXT NOT NULL,
|
|
369
|
+
updated_by TEXT NOT NULL,
|
|
370
|
+
updated_at TEXT NOT NULL,
|
|
371
|
+
PRIMARY KEY (approver, scope)
|
|
372
|
+
);
|
|
373
|
+
`;
|
|
374
|
+
/** The second tap on a flow decision in Telegram (telegram-flow.ts): Approve arms Yes and Cancel, bound to the binding,
|
|
375
|
+
* chat, message and exact visit of the card; Yes decides it once. */
|
|
376
|
+
export const TELEGRAM_FLOW_CONFIRM_SCHEMA = `
|
|
377
|
+
CREATE TABLE IF NOT EXISTS telegram_flow_confirm (
|
|
378
|
+
token TEXT PRIMARY KEY,
|
|
379
|
+
binding INTEGER NOT NULL,
|
|
380
|
+
chat_id TEXT NOT NULL,
|
|
381
|
+
message_id TEXT NOT NULL,
|
|
382
|
+
card INTEGER NOT NULL,
|
|
383
|
+
entry INTEGER NOT NULL,
|
|
384
|
+
phase TEXT NOT NULL CHECK (phase IN ('yes','cancel')),
|
|
385
|
+
created_at TEXT NOT NULL,
|
|
386
|
+
expires_at TEXT NOT NULL,
|
|
387
|
+
consumed_at TEXT
|
|
388
|
+
);
|
|
389
|
+
CREATE INDEX IF NOT EXISTS telegram_flow_confirm_visit ON telegram_flow_confirm (card, entry);
|
|
390
|
+
`;
|
|
357
391
|
export const CHAT_DECIDE_SCHEMA = `
|
|
358
392
|
CREATE TABLE IF NOT EXISTS chat_decide_action (
|
|
359
393
|
token TEXT PRIMARY KEY,
|
|
@@ -3964,6 +3998,9 @@ function initializeStore(db, file) {
|
|
|
3964
3998
|
db.exec(MATE_ASK_SCHEMA);
|
|
3965
3999
|
// Decisions in the chat app (no version bump: additive only): result, plan and merge buttons, and Request changes.
|
|
3966
4000
|
db.exec(CHAT_DECIDE_SCHEMA);
|
|
4001
|
+
// An owner's lasting chat-approval setting (no version bump: additive only; absent means off).
|
|
4002
|
+
db.exec(CHAT_APPROVAL_SCHEMA);
|
|
4003
|
+
db.exec(TELEGRAM_FLOW_CONFIRM_SCHEMA);
|
|
3967
4004
|
// A flow's "Send to me" and "Person chooses" zones (no version bump: additive only).
|
|
3968
4005
|
db.exec(FLOW_SEND_SCHEMA);
|
|
3969
4006
|
db.exec(SPEND_SCHEMA);
|
|
@@ -8797,7 +8834,7 @@ export class Store {
|
|
|
8797
8834
|
// (setup review): the approval is what was stale, and it is new.
|
|
8798
8835
|
const ref = this.db.prepare("SELECT id FROM task_ref WHERE backend = ? AND external_id = ?").get(BUILT_IN, taskId);
|
|
8799
8836
|
if (ref !== undefined) {
|
|
8800
|
-
if (basis === undefined)
|
|
8837
|
+
if (basis === undefined || basis.kind === "chat")
|
|
8801
8838
|
this.noteTaskAct(Number(ref["id"]), "approved", now);
|
|
8802
8839
|
this.db.prepare("DELETE FROM hold WHERE owner_kind = 'backoff' AND owner_id = ?").run(`stale:${Number(ref["id"])}`);
|
|
8803
8840
|
this.db
|
|
@@ -8813,7 +8850,9 @@ export class Store {
|
|
|
8813
8850
|
subject: "Scope approved",
|
|
8814
8851
|
body: basis === undefined
|
|
8815
8852
|
? `Approved by ${currentActor()?.lead === true ? "the lead" : lifecycleWords(by, 40)}. A connected worker can take it next.`
|
|
8816
|
-
:
|
|
8853
|
+
: basis.kind === "chat"
|
|
8854
|
+
? `Approved by ${lifecycleWords(by, 40)} in chat. A connected worker can take it next.`
|
|
8855
|
+
: "Approved automatically under the operating mode. A connected worker can take it next.",
|
|
8817
8856
|
link: chatControlHref("task", taskId),
|
|
8818
8857
|
}, now);
|
|
8819
8858
|
}
|
|
@@ -18513,6 +18552,7 @@ export class Store {
|
|
|
18513
18552
|
const stamp = now.toISOString();
|
|
18514
18553
|
this.db.prepare("UPDATE telegram_flow_action SET consumed_at = ? WHERE card = ? AND entry = ? AND consumed_at IS NULL").run(stamp, card, entry);
|
|
18515
18554
|
this.db.prepare("UPDATE telegram_flow_prompt SET consumed_at = ? WHERE card = ? AND entry = ? AND consumed_at IS NULL").run(stamp, card, entry);
|
|
18555
|
+
this.db.prepare("UPDATE telegram_flow_confirm SET consumed_at = ? WHERE card = ? AND entry = ? AND consumed_at IS NULL").run(stamp, card, entry);
|
|
18516
18556
|
}
|
|
18517
18557
|
recordTelegramFlowPrompt(prompt, now, hours = 24) {
|
|
18518
18558
|
this.db.prepare("INSERT OR REPLACE INTO telegram_flow_prompt (chat_id, message_id, binding, card, entry, mode, created_at, expires_at, consumed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL)")
|
package/dist/surface.js
CHANGED
|
@@ -125,7 +125,7 @@ const GUIDE_ROWS = [
|
|
|
125
125
|
...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
|
|
126
126
|
// Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
|
|
127
127
|
...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags,
|
|
128
|
-
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
128
|
+
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : name === "card" ? "a card id from flows show" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
129
129
|
...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
|
|
130
130
|
// ---- the queue (agent surface) ----
|
|
131
131
|
{ invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
|
@@ -384,6 +384,9 @@ const GUIDE_ROWS = [
|
|
|
384
384
|
operatorRead("review show", "whether a project's finished builds get one automatic review"),
|
|
385
385
|
operator("review on", "one automatic review of each finished build whose check passes; only HIGH findings send it back, once"),
|
|
386
386
|
operator("review off", "finished builds go straight to the person, unreviewed"),
|
|
387
|
+
operatorRead("chat-approval show", "whether your own taps in your paired chat approve plans and merges: all projects and each project's own setting"),
|
|
388
|
+
operator("chat-approval on", "let your paired chat approve plans and merges in all your projects or one (--repo); lasting, with limits; previews until --yes"),
|
|
389
|
+
operator("chat-approval off", "stop approving from chat for all your projects or one (--repo); one step"),
|
|
387
390
|
operator("mode revoke", "revoke a repository's mode — one click, every act it covered falls back to its own ceremony"),
|
|
388
391
|
operator("config set", "set phase or spend configuration"),
|
|
389
392
|
operator("config clear", "clear phase or spend configuration"),
|
package/dist/teammate-work.js
CHANGED
|
@@ -206,7 +206,7 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
|
|
|
206
206
|
if (answer.action === "approve" || answer.action === "send_back") {
|
|
207
207
|
const draft = draftFor(definition, stage);
|
|
208
208
|
const decided = decideFlowCard(store, {
|
|
209
|
-
card: card.id, decision: answer.action === "approve" ? "approve" : "send-back", note: answer.action === "approve" ? answer.reason || null : note(answer.note), actor, repos, teammate: mate.handle, entry: card.entry,
|
|
209
|
+
card: card.id, decision: answer.action === "approve" ? "approve" : "send-back", note: answer.action === "approve" ? answer.reason || null : note(answer.note), actor, repos, teammate: mate.handle, entry: card.entry, where: "a teammate",
|
|
210
210
|
...(answer.action === "approve" && draft !== null && answer.text !== "" ? { draft: wholeDraft(answer.text) } : {}),
|
|
211
211
|
...(io.evidenceRoot === undefined ? {} : { evidenceRoot: io.evidenceRoot }),
|
|
212
212
|
}, now);
|
package/dist/telegram-flow.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type FlowChoiceVisit, type FlowSendContent } from "./flow-send.js";
|
|
|
2
2
|
import { type FlowStage } from "./flows.js";
|
|
3
3
|
import { type InlineButton } from "./telegram-mate.js";
|
|
4
4
|
import type { FlowCardRow, FlowRow, Store, TelegramBinding, TelegramFlowAction, TelegramFlowChoice, TelegramFlowPrompt } from "./store.js";
|
|
5
|
+
/** A flow decision's notice: one per visit, and again (`:ask:<ms>`) when the owner asks the lead to approve it. */
|
|
5
6
|
export declare const FLOW_DECIDE_KEY: RegExp;
|
|
6
7
|
type Waiting = {
|
|
7
8
|
card: FlowCardRow;
|
|
@@ -41,21 +42,60 @@ export declare function applyFlowChoiceTap(store: Store, binding: TelegramBindin
|
|
|
41
42
|
export type FlowTapEffect = {
|
|
42
43
|
kind: "ack";
|
|
43
44
|
text: string;
|
|
44
|
-
}
|
|
45
|
+
}
|
|
46
|
+
/** The message repainted; `keyboard` (absent: none) replaces its buttons, and links are paths under the console. */
|
|
47
|
+
| {
|
|
45
48
|
kind: "edit";
|
|
46
49
|
text: string;
|
|
50
|
+
keyboard?: FlowKeyboard;
|
|
51
|
+
place?: string[];
|
|
47
52
|
} | {
|
|
48
53
|
kind: "prompt";
|
|
49
54
|
text: string;
|
|
50
55
|
placeholder: string;
|
|
51
56
|
prompt: Omit<TelegramFlowPrompt, "messageId">;
|
|
52
57
|
};
|
|
58
|
+
/** A repainted message's buttons: a token, or a link to a console path. `place` names fresh flow buttons to stamp on the message. */
|
|
59
|
+
export type FlowKeyboard = Array<Array<{
|
|
60
|
+
label: string;
|
|
61
|
+
token: string;
|
|
62
|
+
} | {
|
|
63
|
+
label: string;
|
|
64
|
+
link: {
|
|
65
|
+
label: string;
|
|
66
|
+
path: string;
|
|
67
|
+
};
|
|
68
|
+
}>>;
|
|
69
|
+
/** Where a card is now, for a message whose buttons no longer act: named and linked only when it is in one of the
|
|
70
|
+
* viewer's projects, and linked only while it still waits on someone. Never a live button. */
|
|
71
|
+
export declare function flowWhereNow(store: Store, cardId: number, repos: readonly string[] | null): {
|
|
72
|
+
said: string;
|
|
73
|
+
keyboard: FlowKeyboard;
|
|
74
|
+
};
|
|
53
75
|
/** A tapped flow button, applied inside the update's transaction. */
|
|
54
76
|
export declare function applyFlowTap(store: Store, binding: TelegramBinding, action: TelegramFlowAction, message: {
|
|
55
77
|
chatId: string;
|
|
56
78
|
messageId: string;
|
|
57
79
|
text: string;
|
|
58
80
|
}, repos: readonly string[] | null, now: Date): FlowTapEffect[];
|
|
81
|
+
export type FlowConfirm = {
|
|
82
|
+
token: string;
|
|
83
|
+
binding: number;
|
|
84
|
+
chatId: string;
|
|
85
|
+
messageId: string;
|
|
86
|
+
card: number;
|
|
87
|
+
entry: number;
|
|
88
|
+
phase: "yes" | "cancel";
|
|
89
|
+
expiresAt: string;
|
|
90
|
+
consumedAt: string | null;
|
|
91
|
+
};
|
|
92
|
+
/** A Yes or Cancel this module armed, or null when the token isn't one. */
|
|
93
|
+
export declare function flowConfirmOf(store: Store, token: string): FlowConfirm | null;
|
|
94
|
+
/** The second tap, inside the update's transaction: Yes decides the card through the console's door, once; Cancel puts
|
|
95
|
+
* the card's own buttons back. The caller has proved the binding, chat and message are the ones it was armed on. */
|
|
96
|
+
export declare function applyFlowConfirm(store: Store, binding: TelegramBinding, confirm: FlowConfirm, message: {
|
|
97
|
+
text: string;
|
|
98
|
+
}, repos: readonly string[] | null, now: Date): FlowTapEffect[];
|
|
59
99
|
export type FlowReplyEffect = {
|
|
60
100
|
kind: "say";
|
|
61
101
|
text: string;
|