toolroll 0.9.36 → 0.9.38
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 +77 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/browser/THIRD_PARTY_NOTICES.txt +3 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +26 -26
- package/dist/browser-crew.js +5 -0
- package/dist/browser-workspace.d.ts +2 -0
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.js +20 -9
- package/dist/chat-decide.d.ts +18 -4
- package/dist/chat-decide.js +31 -11
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +7 -2
- package/dist/contracts/contract.js +21 -12
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +5 -0
- package/dist/contracts/flow.js +5 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/desktop-update.d.ts +1 -1
- package/dist/discord-chat.js +31 -20
- package/dist/flow-engine.js +14 -17
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.js +7 -1
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-steps.js +3 -2
- package/dist/flows.d.ts +2 -4
- package/dist/flows.js +9 -5
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-progress.d.ts +26 -0
- package/dist/mate-progress.js +24 -0
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +108 -275
- package/dist/mate.js +11 -2
- package/dist/mcp-connect.d.ts +24 -4
- package/dist/mcp-connect.js +78 -15
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +23 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/task-review-brief.d.ts +1 -7
- package/dist/task-review-brief.js +6 -6
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.d.ts +9 -0
- package/dist/telegram-flow.js +44 -8
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +9 -8
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +94 -68
- package/dist/text-limits.d.ts +142 -1
- package/dist/text-limits.js +144 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/package.json +17 -12
package/dist/task-sizing.js
CHANGED
|
@@ -20,12 +20,13 @@ import { randomUUID } from "node:crypto";
|
|
|
20
20
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
21
21
|
import { tmpdir } from "node:os";
|
|
22
22
|
import { join } from "node:path";
|
|
23
|
+
import { readSizeAnswer, SIZING_MODEL_SCHEMA } from "./contracts/task-sizing.js";
|
|
23
24
|
import { strictJsonParse } from "./converse.js";
|
|
24
25
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
25
26
|
import { run, terminateOwnedProcesses } from "./exec.js";
|
|
26
27
|
import { JEV_MODEL, JEV_URL } from "./flow-sort.js";
|
|
27
28
|
import { readProviderKey } from "./keys.js";
|
|
28
|
-
import {
|
|
29
|
+
import {} from "./phase-routing.js";
|
|
29
30
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
30
31
|
/** The classifier's whole budget, from asking to answer. */
|
|
31
32
|
export const SIZING_BUDGET_MS = 5_000;
|
|
@@ -49,8 +50,11 @@ export function heuristicSizing(input, why = null) {
|
|
|
49
50
|
const clip = (text, cap) => (text.length <= cap ? text : `${text.slice(0, cap - 1)}…`);
|
|
50
51
|
/** Key-shaped lines never leave the machine. */
|
|
51
52
|
const blank = (text) => redactSecretLines(text, scanForSecrets(text));
|
|
52
|
-
/**
|
|
53
|
-
|
|
53
|
+
/** An answer read through the contract (src/contracts/task-sizing.ts), its reason on one line; null when it isn't one. */
|
|
54
|
+
function answerOf(value) {
|
|
55
|
+
const read = readSizeAnswer(value);
|
|
56
|
+
return read.ok ? read.value : null;
|
|
57
|
+
}
|
|
54
58
|
const SIZE_MEANS = {
|
|
55
59
|
small: "a contained change a fast model can make directly: copy, a style, a config value, one small function, one or two files",
|
|
56
60
|
medium: "ordinary feature or fix work across a few files that benefits from the everyday model",
|
|
@@ -66,25 +70,6 @@ export function sizingText(input) {
|
|
|
66
70
|
...(input.touches === undefined || input.touches.length === 0 ? [] : [`Paths: ${clip(input.touches.slice(0, 40).join(", "), 1500)}`]),
|
|
67
71
|
].join("\n");
|
|
68
72
|
}
|
|
69
|
-
export const SIZING_SCHEMA = {
|
|
70
|
-
type: "object",
|
|
71
|
-
properties: {
|
|
72
|
-
size: { type: "string", enum: ["small", "medium", "large"] },
|
|
73
|
-
risky: { type: "boolean" },
|
|
74
|
-
reason: { type: "string", maxLength: 160 },
|
|
75
|
-
},
|
|
76
|
-
required: ["size", "risky", "reason"],
|
|
77
|
-
additionalProperties: false,
|
|
78
|
-
};
|
|
79
|
-
/** Read an answer back: only the three sizes, a boolean, and a short reason. */
|
|
80
|
-
export function readSizeAnswer(value) {
|
|
81
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
82
|
-
return null;
|
|
83
|
-
const v = value;
|
|
84
|
-
if (!isTaskSize(v["size"]) || typeof v["risky"] !== "boolean")
|
|
85
|
-
return null;
|
|
86
|
-
return { size: v["size"], risky: v["risky"], reason: typeof v["reason"] === "string" ? oneLine(v["reason"]) : "" };
|
|
87
|
-
}
|
|
88
73
|
/** A small Claude model on this computer's sign-in: no tools, no MCP servers, no repository, credential keys stripped.
|
|
89
74
|
* The abort ends it: a late answer never keeps a command (or its process) waiting. */
|
|
90
75
|
export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs = SIZING_BUDGET_MS) {
|
|
@@ -108,7 +93,7 @@ export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs
|
|
|
108
93
|
sizingText(input),
|
|
109
94
|
].join("\n");
|
|
110
95
|
const result = await runner("claude", [
|
|
111
|
-
"-p", "--output-format", "json", "--json-schema", JSON.stringify(
|
|
96
|
+
"-p", "--output-format", "json", "--json-schema", JSON.stringify(SIZING_MODEL_SCHEMA), "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
|
|
112
97
|
"--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}', "--model", model,
|
|
113
98
|
], { cwd: dir, stdin: prompt, timeoutMs: budgetMs, maxBuffer: 256 * 1024, omitEnv: ALL_CREDENTIAL_ENV, processGroup: true, owner, beforeSpawn: () => !signal.aborted });
|
|
114
99
|
if (signal.aborted || result.notFound || result.timedOut || result.code !== 0)
|
|
@@ -117,7 +102,7 @@ export function claudeSizer(runner = run, model = SIZING_CLAUDE_MODEL, budgetMs
|
|
|
117
102
|
const body = parsed.ok && typeof parsed.value === "object" && parsed.value !== null && !Array.isArray(parsed.value) ? parsed.value : null;
|
|
118
103
|
if (body === null || body["is_error"] === true || body["subtype"] !== "success")
|
|
119
104
|
return null;
|
|
120
|
-
return
|
|
105
|
+
return answerOf(body["structured_output"]);
|
|
121
106
|
}
|
|
122
107
|
finally {
|
|
123
108
|
signal.removeEventListener("abort", stop);
|
|
@@ -149,10 +134,10 @@ export function jevSizer(fetcher, key) {
|
|
|
149
134
|
const answers = (body["answers"] ?? {});
|
|
150
135
|
const size = answers["size"]?.["choice"];
|
|
151
136
|
const risky = answers["risky"]?.["noul"];
|
|
152
|
-
if (
|
|
137
|
+
if (typeof risky !== "number" || !Number.isFinite(risky))
|
|
153
138
|
return null;
|
|
154
139
|
const sure = answers["size"]?.["confidence"];
|
|
155
|
-
return { size, risky: risky >= 0.5, reason: typeof sure === "number" && Number.isFinite(sure) ? `Jev is ${Math.round(Math.min(1, Math.max(0, sure)) * 100)}% sure` : "" };
|
|
140
|
+
return answerOf({ size, risky: risky >= 0.5, reason: typeof sure === "number" && Number.isFinite(sure) ? `Jev is ${Math.round(Math.min(1, Math.max(0, sure)) * 100)}% sure` : "" });
|
|
156
141
|
};
|
|
157
142
|
}
|
|
158
143
|
/** The owner's own classifier: Jev when an OpenRouter key is set up, otherwise Claude on this computer's sign-in. */
|
package/dist/task-text.d.ts
CHANGED
|
@@ -3,11 +3,6 @@ export declare const TASK_TEXT_LIMITS: {
|
|
|
3
3
|
readonly text: 8000;
|
|
4
4
|
readonly textBytes: 32000;
|
|
5
5
|
};
|
|
6
|
-
export declare const TASK_SCOPE_TEXT_SCHEMA: {
|
|
7
|
-
readonly type: "string";
|
|
8
|
-
readonly maxLength: 8000;
|
|
9
|
-
readonly description: `At most ${string} characters (UTF-16 code units) and ${string} UTF-8 bytes; no control or disguised text. Longer text is refused with its length, not cut: shorten it and call again.`;
|
|
10
|
-
};
|
|
11
6
|
export type TaskTextRefusal = {
|
|
12
7
|
ok: false;
|
|
13
8
|
reason: "bad-title" | "bad-goal";
|
package/dist/task-text.js
CHANGED
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
* runtime with lower limits. */
|
|
7
7
|
import { hasForbiddenControls, hasDisguisedText } from "./decision.js";
|
|
8
8
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
9
|
-
export const TASK_TEXT_LIMITS = { title:
|
|
9
|
+
export const TASK_TEXT_LIMITS = { title: TEXT_LIMITS.taskTitle, text: TEXT_LIMITS.goal, textBytes: TEXT_LIMITS.goalBytes };
|
|
10
10
|
const n = (value) => value.toLocaleString("en-US");
|
|
11
|
-
export const TASK_SCOPE_TEXT_SCHEMA = { type: "string", maxLength: TASK_TEXT_LIMITS.text,
|
|
12
|
-
description: `At most ${n(TASK_TEXT_LIMITS.text)} characters (UTF-16 code units) and ${n(TASK_TEXT_LIMITS.textBytes)} UTF-8 bytes; no control or disguised text. Longer text is refused with its length, not cut: shorten it and call again.` };
|
|
13
11
|
const dishonest = (text) => hasForbiddenControls(text) || hasDisguisedText(text);
|
|
14
12
|
const overBytes = (text, cap) => Buffer.byteLength(text, "utf8") > cap;
|
|
15
13
|
const BYTE_CAPS = { title: 800, text: TASK_TEXT_LIMITS.textBytes, path: 800 };
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* the person it was asked of. Answered anywhere, every button retires.
|
|
12
12
|
*/
|
|
13
13
|
import { randomBytes } from "node:crypto";
|
|
14
|
+
import { partContent, savedChatPart } from "./chat-delivery-state.js";
|
|
14
15
|
import { answerTeammateQuestion } from "./teammate-work.js";
|
|
15
16
|
import { labelOf } from "./teammate-admin.js";
|
|
17
|
+
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
16
18
|
/** The key notifyPeople gives a teammate's question: flow-card:<card>:teammate-q:<question>:<person>. */
|
|
17
19
|
export const TEAMMATE_Q_KEY = /^flow-card:[1-9][0-9]{0,14}:teammate-q:([1-9][0-9]{0,14}):/;
|
|
18
20
|
export const IN_WORDS = "Answer in words";
|
|
@@ -41,8 +43,8 @@ export function telegramQuestionButtons(store, binding, question, now) {
|
|
|
41
43
|
const rows = [];
|
|
42
44
|
const options = choices.filter(one => one.choice !== null);
|
|
43
45
|
for (let at = 0; at < options.length; at += 2)
|
|
44
|
-
rows.push(options.slice(at, at + 2).map(one => (
|
|
45
|
-
rows.push([
|
|
46
|
+
rows.push(options.slice(at, at + 2).map(one => telegramButton(one.label.slice(0, 60), one.token)));
|
|
47
|
+
rows.push([telegramButton(choices.at(-1).label, choices.at(-1).token)]);
|
|
46
48
|
return { keyboard: rows, tokens: choices.map(one => one.token) };
|
|
47
49
|
}
|
|
48
50
|
/** A tapped question button, inside the update's transaction. */
|
|
@@ -90,7 +92,7 @@ export function questionParts(store, notification, binding) {
|
|
|
90
92
|
/** The live buttons on one part, in the order they were minted. */
|
|
91
93
|
export function chatQuestionButtons(state, part, now) {
|
|
92
94
|
const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
|
|
93
|
-
const content = row === undefined ? null :
|
|
95
|
+
const content = row === undefined ? null : partContent(String(row["payload"]));
|
|
94
96
|
const labels = new Map((content?.question?.choices ?? []).map(one => [one.choice ?? "", one.label]));
|
|
95
97
|
return state.prepare("SELECT token,choice FROM chat_question_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
|
|
96
98
|
.map(one => ({ token: String(one.token), label: (labels.get(one.choice ?? "") ?? IN_WORDS).slice(0, 75), words: one.choice === null }));
|
|
@@ -98,9 +100,9 @@ export function chatQuestionButtons(state, part, now) {
|
|
|
98
100
|
/** Show the tapped notice again with the answer, and without its buttons. */
|
|
99
101
|
function repaint(state, part, event, line) {
|
|
100
102
|
const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
|
|
101
|
-
const before = row === undefined ? { text: "" } :
|
|
103
|
+
const before = row === undefined ? { text: "" } : partContent(String(row["payload"]));
|
|
102
104
|
const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.link === undefined ? {} : { link: before.link }), ...(before.channel === undefined ? {} : { channel: before.channel }) };
|
|
103
|
-
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(
|
|
105
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(savedChatPart(content), part);
|
|
104
106
|
}
|
|
105
107
|
/** A tapped question button, inside the action's transaction; false when the token isn't one. */
|
|
106
108
|
export function applyChatQuestionTap(options, event, binding, token, now) {
|
package/dist/teammate-work.js
CHANGED
|
@@ -111,9 +111,10 @@ export async function teammateTurn(store, flow, definition, stage, card, mate, n
|
|
|
111
111
|
store.addTeammateTurn({ teammate: mate.id, card: card.id, model, ok: reply.ok, ms: reply.ok ? reply.ms : Date.now() - asked, costUsd: reply.costUsd ?? null, tokensIn: reply.tokensIn ?? null, tokensOut: reply.tokensOut ?? null }, now);
|
|
112
112
|
if (!reply.ok)
|
|
113
113
|
return { state: "retry", said: reply.said, ...(log.length === 0 ? {} : { log: log.join("\n\n") }) };
|
|
114
|
-
const
|
|
115
|
-
if (
|
|
116
|
-
return { state: "retry", said: `${context.name}'s answer wasn't one this zone allows.`, log: [...log, JSON.stringify(reply.value).slice(0, 4000)].join("\n\n") };
|
|
114
|
+
const read = readTurn(reply.value, context);
|
|
115
|
+
if (!read.ok)
|
|
116
|
+
return { state: "retry", said: `${context.name}'s answer wasn't one this zone allows.`, log: [...log, read.issues.map(issue => issue.line).join("\n"), JSON.stringify(reply.value).slice(0, 4000)].join("\n\n") };
|
|
117
|
+
const answer = read.value;
|
|
117
118
|
const header = `${context.name} (${model}) · ${(reply.ms / 1000).toFixed(1)} s`;
|
|
118
119
|
const over = turnOverruns(answer);
|
|
119
120
|
if (over.length > 0 && !shortened) {
|
|
@@ -243,8 +244,11 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
|
|
|
243
244
|
// route: the answer names where the card goes; its text is what the next zones use.
|
|
244
245
|
const picked = handleAnswers(stage).find(one => one.answer.toLowerCase() === answer.answer.toLowerCase());
|
|
245
246
|
// What the next zones read: whole up to what a step passes on; longer is attached whole to the card, and they read a link to it.
|
|
246
|
-
if (answer.text !== "")
|
|
247
|
-
|
|
247
|
+
if (answer.text !== "") {
|
|
248
|
+
const whole = wholeDraft(answer.text);
|
|
249
|
+
const text = keptWhole(store, flow, card, actor, whole, TEXT_LIMITS.stageOutput, "a step passes on", `What ${label} wrote for the next zones`, now);
|
|
250
|
+
store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: text }, ...(text === whole ? {} : { attached: { [stage.id]: whole } }) }, now);
|
|
251
|
+
}
|
|
248
252
|
const said = `Sent “${card.title}” to ${titleOf(picked.to)}${picked.answer === CARRY_ON ? "" : ` (${picked.answer})`}${answer.reason === "" ? "" : `: ${answer.reason}`}`;
|
|
249
253
|
event("handled", said);
|
|
250
254
|
// v96: a zone that answers whoever asked sends what it wrote back to them, under its name.
|
package/dist/teammates.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { run, type ExecResult } from "./exec.js";
|
|
2
|
+
import type { ContractResult } from "./contracts/contract.js";
|
|
3
|
+
import { type TurnAnswer } from "./contracts/teammate-turn.js";
|
|
2
4
|
import { type Overrun } from "./text-limits.js";
|
|
3
5
|
export declare const SOUL_CHARS = 12000;
|
|
4
6
|
export type Soul = {
|
|
@@ -29,25 +31,11 @@ export declare const TEAMMATE_TEMPLATES: readonly {
|
|
|
29
31
|
about: string;
|
|
30
32
|
soul: string;
|
|
31
33
|
}[];
|
|
32
|
-
|
|
33
|
-
export type TurnAction = "approve" | "send_back" | "hand_off" | "route" | "ask" | "cant" | "use_tool";
|
|
34
|
-
/** v94: "use_tool" asks for one tool call (`tool`: its name, `input`: a JSON object as text); the turn goes on with its answer. */
|
|
35
|
-
export type TurnAnswer = {
|
|
36
|
-
action: TurnAction;
|
|
37
|
-
answer: string;
|
|
38
|
-
text: string;
|
|
39
|
-
note: string;
|
|
40
|
-
question: string;
|
|
41
|
-
options: string[];
|
|
42
|
-
reason: string;
|
|
43
|
-
tool: string;
|
|
44
|
-
input: string;
|
|
45
|
-
/** v95: one short fact worth keeping for later cards ("": none). */
|
|
46
|
-
remember: string;
|
|
47
|
-
};
|
|
34
|
+
export type { TurnAction, TurnAnswer } from "./contracts/teammate-turn.js";
|
|
48
35
|
/** Each field's limit, in characters: stated in the prompt before it answers (turnPrompt), and an answer over one is
|
|
49
36
|
* asked once to shorten (teammateTurn); one still over is kept whole, never cut. "text" is what the next zones read;
|
|
50
|
-
* "note" is a decision or send-back note.
|
|
37
|
+
* "note" is a decision or send-back note. Not in the turn's schema (src/contracts/teammate-turn.ts): the CLI would
|
|
38
|
+
* refuse an answer a few characters over one whole. */
|
|
51
39
|
export declare const TURN_LIMITS: {
|
|
52
40
|
readonly answer: 60;
|
|
53
41
|
readonly text: 12000;
|
|
@@ -58,49 +46,6 @@ export declare const TURN_LIMITS: {
|
|
|
58
46
|
};
|
|
59
47
|
/** The fields of an answer over their limits. */
|
|
60
48
|
export declare function turnOverruns(answer: TurnAnswer): Overrun[];
|
|
61
|
-
/** The shape Claude answers in: one flat object (a root union is refused). No length limits here: an answer
|
|
62
|
-
* a few characters over one is refused whole by the CLI, so the limits are TURN_LIMITS, checked after. */
|
|
63
|
-
export declare const TURN_SCHEMA: {
|
|
64
|
-
readonly type: "object";
|
|
65
|
-
readonly additionalProperties: false;
|
|
66
|
-
readonly required: readonly ["action", "answer", "text", "note", "question", "options", "reason", "tool", "input", "remember"];
|
|
67
|
-
readonly properties: {
|
|
68
|
-
readonly action: {
|
|
69
|
-
readonly type: "string";
|
|
70
|
-
readonly enum: readonly ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"];
|
|
71
|
-
};
|
|
72
|
-
readonly answer: {
|
|
73
|
-
readonly type: "string";
|
|
74
|
-
};
|
|
75
|
-
readonly text: {
|
|
76
|
-
readonly type: "string";
|
|
77
|
-
};
|
|
78
|
-
readonly note: {
|
|
79
|
-
readonly type: "string";
|
|
80
|
-
};
|
|
81
|
-
readonly question: {
|
|
82
|
-
readonly type: "string";
|
|
83
|
-
};
|
|
84
|
-
readonly options: {
|
|
85
|
-
readonly type: "array";
|
|
86
|
-
readonly items: {
|
|
87
|
-
readonly type: "string";
|
|
88
|
-
};
|
|
89
|
-
};
|
|
90
|
-
readonly reason: {
|
|
91
|
-
readonly type: "string";
|
|
92
|
-
};
|
|
93
|
-
readonly tool: {
|
|
94
|
-
readonly type: "string";
|
|
95
|
-
};
|
|
96
|
-
readonly input: {
|
|
97
|
-
readonly type: "string";
|
|
98
|
-
};
|
|
99
|
-
readonly remember: {
|
|
100
|
-
readonly type: "string";
|
|
101
|
-
};
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
49
|
/** Everything one turn is told: the zone, the card as data, and what its people said. */
|
|
105
50
|
export type TurnContext = {
|
|
106
51
|
soul: string;
|
|
@@ -165,8 +110,13 @@ export type TurnContext = {
|
|
|
165
110
|
};
|
|
166
111
|
};
|
|
167
112
|
export declare function turnPrompt(context: TurnContext): string;
|
|
168
|
-
/**
|
|
169
|
-
|
|
113
|
+
/**
|
|
114
|
+
* A turn's answer, read through its contract (src/contracts/teammate-turn.ts) and then checked against what this zone
|
|
115
|
+
* allows, each refusal naming its path (`action: "route" isn't one this zone allows`). Text is trimmed and kept whole
|
|
116
|
+
* (a field over its limit is asked to shorten, turnOverruns); blank options are dropped, and an ask offers at most 4,
|
|
117
|
+
* each cut to an answer's length, and a tool name is cut to 140, as they always were.
|
|
118
|
+
*/
|
|
119
|
+
export declare function readTurn(value: unknown, context: Pick<TurnContext, "kind" | "canSendBack" | "answers" | "tools">): ContractResult<TurnAnswer>;
|
|
170
120
|
export type TurnRequest = {
|
|
171
121
|
model: string;
|
|
172
122
|
prompt: string;
|
|
@@ -190,10 +140,9 @@ export type TurnReply = ({
|
|
|
190
140
|
export type TurnRunner = (request: TurnRequest) => Promise<TurnReply>;
|
|
191
141
|
export declare const TURN_TIMEOUT_MS = 180000;
|
|
192
142
|
type CommandRunner = (file: string, args: readonly string[], options: Parameters<typeof run>[2]) => Promise<ExecResult>;
|
|
193
|
-
/** The production runner: Claude through this computer's sign-in, answering in
|
|
143
|
+
/** The production runner: Claude through this computer's sign-in, answering in the turn schema (TURN_MODEL_SCHEMA), with no tools, no MCP servers and no repository. */
|
|
194
144
|
export declare function claudeTurnRunner(runner?: CommandRunner): TurnRunner;
|
|
195
145
|
/** How a teammate is named to people: "Maya · Support". */
|
|
196
146
|
export declare const teammateLabel: (soul: Pick<Soul, "name" | "role">) => string;
|
|
197
147
|
/** How its acts read in a card's history: "Maya (AI)". */
|
|
198
148
|
export declare const teammateActor: (soul: Pick<Soul, "name">) => string;
|
|
199
|
-
export {};
|
package/dist/teammates.js
CHANGED
|
@@ -24,6 +24,7 @@ import { strictJsonParse } from "./converse.js";
|
|
|
24
24
|
import { scanForSecrets } from "./evidence.js";
|
|
25
25
|
import { run } from "./exec.js";
|
|
26
26
|
import { ALL_CREDENTIAL_ENV } from "./provider.js";
|
|
27
|
+
import { readTurnAnswer, TURN_MODEL_SCHEMA } from "./contracts/teammate-turn.js";
|
|
27
28
|
import { LIMITS } from "./decision.js";
|
|
28
29
|
import { overruns, TEXT_LIMITS } from "./text-limits.js";
|
|
29
30
|
export const SOUL_CHARS = 12_000;
|
|
@@ -121,23 +122,13 @@ export const TEAMMATE_TEMPLATES = [
|
|
|
121
122
|
];
|
|
122
123
|
/** Each field's limit, in characters: stated in the prompt before it answers (turnPrompt), and an answer over one is
|
|
123
124
|
* asked once to shorten (teammateTurn); one still over is kept whole, never cut. "text" is what the next zones read;
|
|
124
|
-
* "note" is a decision or send-back note.
|
|
125
|
-
|
|
125
|
+
* "note" is a decision or send-back note. Not in the turn's schema (src/contracts/teammate-turn.ts): the CLI would
|
|
126
|
+
* refuse an answer a few characters over one whole. */
|
|
127
|
+
export const TURN_LIMITS = { answer: TEXT_LIMITS.teammateAnswer, text: TEXT_LIMITS.stageOutput, note: LIMITS.note, question: TEXT_LIMITS.teammateQuestion, reason: TEXT_LIMITS.teammateReason, remember: TEXT_LIMITS.teammateRemember };
|
|
126
128
|
/** The fields of an answer over their limits. */
|
|
127
129
|
export function turnOverruns(answer) {
|
|
128
130
|
return overruns({ answer: answer.answer, text: answer.text, note: answer.note, question: answer.question, reason: answer.reason, remember: answer.remember }, TURN_LIMITS);
|
|
129
131
|
}
|
|
130
|
-
/** The shape Claude answers in: one flat object (a root union is refused). No length limits here: an answer
|
|
131
|
-
* a few characters over one is refused whole by the CLI, so the limits are TURN_LIMITS, checked after. */
|
|
132
|
-
export const TURN_SCHEMA = {
|
|
133
|
-
type: "object", additionalProperties: false,
|
|
134
|
-
required: ["action", "answer", "text", "note", "question", "options", "reason", "tool", "input", "remember"],
|
|
135
|
-
properties: {
|
|
136
|
-
action: { type: "string", enum: ["approve", "send_back", "hand_off", "route", "ask", "cant", "use_tool"] },
|
|
137
|
-
answer: { type: "string" }, text: { type: "string" }, note: { type: "string" }, question: { type: "string" },
|
|
138
|
-
options: { type: "array", items: { type: "string" } }, reason: { type: "string" }, tool: { type: "string" }, input: { type: "string" }, remember: { type: "string" },
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
132
|
const clip = (text, cap) => text.length <= cap ? text : `${text.slice(0, cap - 1)}…`;
|
|
142
133
|
export function turnPrompt(context) {
|
|
143
134
|
const actions = context.kind === "decide"
|
|
@@ -192,41 +183,49 @@ export function turnPrompt(context) {
|
|
|
192
183
|
...(context.shorten === undefined ? [] : ["", "YOUR LAST ANSWER, WHICH IS OVER A LIMIT", context.shorten.answer, "", context.shorten.ask]),
|
|
193
184
|
].join("\n");
|
|
194
185
|
}
|
|
195
|
-
/**
|
|
186
|
+
/** How many options an ask offers, and the longest tool name and input a tool call may give. */
|
|
187
|
+
const ASK_OPTIONS = 4, TOOL_NAME_CHARS = 140, TOOL_INPUT_CHARS = 8000;
|
|
188
|
+
/**
|
|
189
|
+
* A turn's answer, read through its contract (src/contracts/teammate-turn.ts) and then checked against what this zone
|
|
190
|
+
* allows, each refusal naming its path (`action: "route" isn't one this zone allows`). Text is trimmed and kept whole
|
|
191
|
+
* (a field over its limit is asked to shorten, turnOverruns); blank options are dropped, and an ask offers at most 4,
|
|
192
|
+
* each cut to an answer's length, and a tool name is cut to 140, as they always were.
|
|
193
|
+
*/
|
|
196
194
|
export function readTurn(value, context) {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
195
|
+
const read = readTurnAnswer(value);
|
|
196
|
+
if (!read.ok)
|
|
197
|
+
return read;
|
|
198
|
+
const raw = read.value;
|
|
199
|
+
const answer = { action: raw.action, answer: raw.answer.trim(), text: raw.text.trim(), note: raw.note.trim(), question: raw.question.trim(),
|
|
200
|
+
options: raw.options.filter(one => one.trim() !== "").map(one => one.trim().slice(0, TURN_LIMITS.answer)).slice(0, ASK_OPTIONS), reason: raw.reason.trim(),
|
|
201
|
+
tool: raw.tool.trim().slice(0, TOOL_NAME_CHARS), input: raw.input.trim(), remember: raw.remember.trim() };
|
|
203
202
|
const allowed = [...(context.kind === "decide" ? ["approve", "hand_off", ...(context.canSendBack ? ["send_back"] : [])] : ["route", "ask", "cant"]),
|
|
204
203
|
...((context.tools ?? []).length > 0 ? ["use_tool"] : [])];
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
options: Array.isArray(raw["options"]) ? raw["options"].filter((one) => typeof one === "string" && one.trim() !== "").map(one => one.trim().slice(0, TURN_LIMITS.answer)).slice(0, 4) : [], reason: text("reason"),
|
|
209
|
-
tool: text("tool").slice(0, 140), input: typeof raw["input"] === "string" ? raw["input"].trim() : "", remember: text("remember") };
|
|
204
|
+
const refuse = (path, what) => ({ ok: false, issues: [{ path, kind: "bad-value", line: `${path}: ${what}` }] });
|
|
205
|
+
if (!allowed.includes(answer.action))
|
|
206
|
+
return refuse("action", `"${answer.action}" isn't one this zone allows (${allowed.map(one => `"${one}"`).join(", ")})`);
|
|
210
207
|
// A tool call is checked against the teammate's rules where it's carried out; here only that it names one and fits.
|
|
211
|
-
if (answer.action === "use_tool" &&
|
|
212
|
-
return
|
|
208
|
+
if (answer.action === "use_tool" && answer.tool === "")
|
|
209
|
+
return refuse("tool", "required for use_tool");
|
|
210
|
+
if (answer.action === "use_tool" && answer.input.length > TOOL_INPUT_CHARS)
|
|
211
|
+
return refuse("input", `at most ${TOOL_INPUT_CHARS.toLocaleString("en-US")} characters`);
|
|
213
212
|
if (answer.action === "route" && !context.answers.some(one => one.toLowerCase() === answer.answer.toLowerCase()))
|
|
214
|
-
return
|
|
213
|
+
return refuse("answer", `"${answer.answer}" isn't one of this zone's answers`);
|
|
215
214
|
if (answer.action === "send_back" && answer.note === "")
|
|
216
|
-
return
|
|
215
|
+
return refuse("note", "required for send_back");
|
|
217
216
|
if (answer.action === "ask" && answer.question === "")
|
|
218
|
-
return
|
|
219
|
-
return answer;
|
|
217
|
+
return refuse("question", "required for ask");
|
|
218
|
+
return { ok: true, value: answer };
|
|
220
219
|
}
|
|
221
220
|
export const TURN_TIMEOUT_MS = 180_000;
|
|
222
|
-
/** The production runner: Claude through this computer's sign-in, answering in
|
|
221
|
+
/** The production runner: Claude through this computer's sign-in, answering in the turn schema (TURN_MODEL_SCHEMA), with no tools, no MCP servers and no repository. */
|
|
223
222
|
export function claudeTurnRunner(runner = run) {
|
|
224
223
|
return async (request) => {
|
|
225
224
|
const dir = mkdtempSync(join(tmpdir(), "standing-orders-teammate-"));
|
|
226
225
|
const started = Date.now();
|
|
227
226
|
try {
|
|
228
227
|
const result = await runner("claude", [
|
|
229
|
-
"-p", "--output-format", "json", "--json-schema", JSON.stringify(
|
|
228
|
+
"-p", "--output-format", "json", "--json-schema", JSON.stringify(TURN_MODEL_SCHEMA), "--safe-mode", "--no-session-persistence", "--tools", "", "--permission-mode", "dontAsk",
|
|
230
229
|
"--permission-prompts", "none", "--strict-mcp-config", "--mcp-config", '{"mcpServers":{}}',
|
|
231
230
|
...(request.model === "default" ? [] : ["--model", request.model]),
|
|
232
231
|
], { cwd: dir, stdin: request.prompt, timeoutMs: request.timeoutMs, maxBuffer: 512 * 1024, omitEnv: ALL_CREDENTIAL_ENV, processGroup: true });
|
package/dist/teams-chat.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { resultShotsPruned } from "./result-shots.js";
|
|
4
4
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
5
5
|
import { chatAskButtons } from "./chat-ask.js";
|
|
6
|
-
import { ChatState, chatHash } from "./chat-delivery-state.js";
|
|
6
|
+
import { ChatDeliveryError, ChatState, chatHash, partContent } from "./chat-delivery-state.js";
|
|
7
7
|
import { channelAccess, chatObject as object, planChatNotifications, planRoomMessages, processChatEvent, splitChatText } from "./chat-delivery.js";
|
|
8
8
|
import { PLATFORM_LIMITS } from "./text-limits.js";
|
|
9
9
|
import { roomCommand } from "./chat-rooms.js";
|
|
@@ -14,6 +14,7 @@ import { chatFlowButtons } from "./chat-flow.js";
|
|
|
14
14
|
import { channelInbox } from "./chat-inbox.js";
|
|
15
15
|
import { chatResultHref } from "./chat-controls.js";
|
|
16
16
|
import { TeamsError } from "./teams-api.js";
|
|
17
|
+
import { readTeamsSubmit } from "./contracts/teams-callback.js";
|
|
17
18
|
const serviceKey = (conversation) => `serviceUrl:${conversation}`;
|
|
18
19
|
export const teamsUserId = (v) => typeof v === "string" && /^29:[A-Za-z0-9_=-]{10,200}$/.test(v);
|
|
19
20
|
/** A conversation id; a channel post's carries its thread as ";messageid=<first message>" (v89). */
|
|
@@ -91,13 +92,16 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
91
92
|
return false;
|
|
92
93
|
if (!isRoom && conversation.conversationType !== "personal")
|
|
93
94
|
return false;
|
|
94
|
-
|
|
95
|
-
|
|
95
|
+
// A card's button: its data is Toolroll's. One that can't be read is answered with why, once the tap is proved to
|
|
96
|
+
// be the paired person's below. A button carries no words of its own: other data beside words is a message, as before.
|
|
97
|
+
const read = activity.value === undefined || activity.value === null ? null : readTeamsSubmit(activity.value);
|
|
98
|
+
const button = read !== null && (read.ok || typeof activity.text !== "string") ? read : null;
|
|
99
|
+
const submitted = button?.ok === true ? button.value : null;
|
|
96
100
|
let kind, payload, eventId;
|
|
97
|
-
if (
|
|
101
|
+
if (button !== null) {
|
|
98
102
|
kind = "action";
|
|
99
|
-
payload = { token:
|
|
100
|
-
eventId = chatHash(`${identity.installation}:action:${member}:${activity.replyToId ?? id}:${submitted}`);
|
|
103
|
+
payload = button.ok ? { token: button.value } : { problem: button.issues.map(issue => issue.line).join("; ") };
|
|
104
|
+
eventId = chatHash(`${identity.installation}:action:${member}:${activity.replyToId ?? id}:${submitted ?? JSON.stringify(activity.value) ?? ""}`);
|
|
101
105
|
}
|
|
102
106
|
else {
|
|
103
107
|
if (typeof activity.text !== "string")
|
|
@@ -109,9 +113,10 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
109
113
|
...(Array.isArray(activity.attachments) && activity.attachments.length > 0 ? { unsupported: "Incoming files are not supported yet. Describe the request in a message; saved result screenshots open from their links." } : {}) };
|
|
110
114
|
eventId = chatHash(`${identity.installation}:message:${id}`);
|
|
111
115
|
}
|
|
116
|
+
const words = "text" in payload ? payload.text ?? "" : "";
|
|
112
117
|
// v89: a channel that feeds a flow takes anyone's message (as a card, with no say over anything); "flow 12" connects one.
|
|
113
|
-
const inbox = isRoom && kind === "message" ? channelInbox(state.store, "teams", identity.installation, channel,
|
|
114
|
-
const roomish = isRoom && (state.room(identity.installation, channel) !== null || (kind === "message" && roomCommand(
|
|
118
|
+
const inbox = isRoom && kind === "message" ? channelInbox(state.store, "teams", identity.installation, channel, words) : { watched: false, command: false };
|
|
119
|
+
const roomish = isRoom && (state.room(identity.installation, channel) !== null || (kind === "message" && roomCommand(words) !== null) || inbox.watched || inbox.command);
|
|
115
120
|
if (isRoom && !roomish)
|
|
116
121
|
return false;
|
|
117
122
|
const binding = state.bindingFor(identity.installation, member);
|
|
@@ -122,8 +127,8 @@ export function receiveTeams(state, identity, raw, serviceUrlClaim, now) {
|
|
|
122
127
|
state.setMeta(identity.installation, serviceKey(channel), activity.serviceUrl, now);
|
|
123
128
|
return state.enqueue({
|
|
124
129
|
id: eventId, installation: identity.installation, binding: kind === "pair" || binding === null ? null : binding.id, kind, channel, member,
|
|
125
|
-
ts:
|
|
126
|
-
payload
|
|
130
|
+
ts: kind === "action" ? String(activity.replyToId ?? id) : id, thread: kind === "action" ? String(activity.replyToId ?? id) : id,
|
|
131
|
+
payload, created: now.toISOString(),
|
|
127
132
|
});
|
|
128
133
|
});
|
|
129
134
|
}
|
|
@@ -175,7 +180,7 @@ export async function deliverTeamsPart(options) {
|
|
|
175
180
|
const repos = await channelAccess(shared, binding, session?.ceilingDigest);
|
|
176
181
|
if (event.kind === "notice" && options.canNotify?.() === false)
|
|
177
182
|
return false;
|
|
178
|
-
const content =
|
|
183
|
+
const content = partContent(row.payload);
|
|
179
184
|
const destination = content.channel ?? binding.channel;
|
|
180
185
|
const serviceUrl = state.meta(identity.installation, serviceKey(destination));
|
|
181
186
|
if (serviceUrl === null)
|
|
@@ -237,6 +242,10 @@ export async function deliverTeamsPart(options) {
|
|
|
237
242
|
return true;
|
|
238
243
|
}
|
|
239
244
|
catch (error) {
|
|
245
|
+
if (error instanceof ChatDeliveryError && error.permanent) {
|
|
246
|
+
state.prepare("UPDATE chat_part SET state='dropped',next_at=NULL,problem=? WHERE id=?").run(error.message, row.id);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
240
249
|
const problem = error instanceof TeamsError ? error : new TeamsError("Teams delivery failed", 15_000, true);
|
|
241
250
|
state.prepare("UPDATE chat_part SET attempts=attempts+1,uncertain=?,next_at=?,problem=?,state=CASE WHEN attempts>=20 THEN 'dropped' ELSE state END WHERE id=?")
|
|
242
251
|
.run(problem.uncertain ? 1 : 0, new Date(now.getTime() + problem.retryMs).toISOString(), problem.message, row.id);
|
package/dist/telegram-flow.d.ts
CHANGED
|
@@ -11,6 +11,12 @@ type Waiting = {
|
|
|
11
11
|
};
|
|
12
12
|
/** The card, if it is still at that visit, waiting at a decision. */
|
|
13
13
|
export declare function flowDecisionAt(store: Store, cardId: number, entry: number): Waiting | null;
|
|
14
|
+
/**
|
|
15
|
+
* A flow decision as Telegram shows it: the action and where each button takes the card first, then the draft (as
|
|
16
|
+
* the card holds it now, so what is approved is what is read) and why a teammate handed it over, then the flow and
|
|
17
|
+
* zone. `said` is the saved notice's body: the words before its instruction are the teammate's handoff.
|
|
18
|
+
*/
|
|
19
|
+
export declare function flowDecisionText(waiting: Waiting, said: string | null, head?: string): string;
|
|
14
20
|
/** Buttons for one visit, minted before the send; `place` stamps the message they landed on. */
|
|
15
21
|
export declare function flowButtons(store: Store, binding: TelegramBinding, waiting: Waiting, now: Date): {
|
|
16
22
|
keyboard: InlineButton[][];
|
|
@@ -20,6 +26,9 @@ export declare function flowButtons(store: Store, binding: TelegramBinding, wait
|
|
|
20
26
|
export declare function flowSentContent(store: Store, card: number, entry: number): FlowSendContent | null;
|
|
21
27
|
/** A sent visit's links as one row of buttons: Toolroll's pages under the trusted origin, and its pull request. */
|
|
22
28
|
export declare function flowSendKeyboardRow(origin: string | null, content: FlowSendContent): InlineButton[];
|
|
29
|
+
/** A choice as Telegram shows it, first: what to choose, then where each option (and a reply) takes the card. The
|
|
30
|
+
* options are the ones its buttons carry: offered when sent, and still offered. */
|
|
31
|
+
export declare function flowChoiceHead(visit: FlowChoiceVisit, content: FlowSendContent): string[];
|
|
23
32
|
/** A choice's buttons for one visit, one option to a row, minted before the send; `place` stamps the message they landed on. */
|
|
24
33
|
export declare function flowChoiceButtons(store: Store, binding: TelegramBinding, visit: FlowChoiceVisit, content: FlowSendContent, now: Date): {
|
|
25
34
|
keyboard: InlineButton[][];
|
package/dist/telegram-flow.js
CHANGED
|
@@ -24,8 +24,9 @@ import { LIMITS } from "./decision.js";
|
|
|
24
24
|
import { keptDraft } from "./flow-draft.js";
|
|
25
25
|
import { decideFlowCard, draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
26
26
|
import { chooseFlowCard, flowChoiceAt, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
27
|
-
import { deciderOf } from "./flows.js";
|
|
27
|
+
import { deciderOf, FLOW_END, replyTarget } from "./flows.js";
|
|
28
28
|
import { phoneLinkButton } from "./telegram-mate.js";
|
|
29
|
+
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
29
30
|
export const FLOW_DECIDE_KEY = /^flow-decide:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})$/;
|
|
30
31
|
const DRAFT_LIMIT = 4000;
|
|
31
32
|
/** The card, if it is still at that visit, waiting at a decision. */
|
|
@@ -40,6 +41,29 @@ export function flowDecisionAt(store, cardId, entry) {
|
|
|
40
41
|
return null;
|
|
41
42
|
return { card, flow, stage, draft: draftFor(definition, stage) };
|
|
42
43
|
}
|
|
44
|
+
/** Where Approve and Send back take the card, on one line. */
|
|
45
|
+
function decisionTerms(waiting) {
|
|
46
|
+
const approve = waiting.stage.next === null ? "Approve → done" : `Approve → ${titleOf(waiting, waiting.stage.next)}`;
|
|
47
|
+
return [approve, ...(waiting.stage.onFail === null ? [] : [`Send back → ${titleOf(waiting, waiting.stage.onFail)}, with your note`])].join(" · ");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A flow decision as Telegram shows it: the action and where each button takes the card first, then the draft (as
|
|
51
|
+
* the card holds it now, so what is approved is what is read) and why a teammate handed it over, then the flow and
|
|
52
|
+
* zone. `said` is the saved notice's body: the words before its instruction are the teammate's handoff.
|
|
53
|
+
*/
|
|
54
|
+
export function flowDecisionText(waiting, said, head = `Approve “${waiting.card.title}”`) {
|
|
55
|
+
const at = said === null ? -1 : said.indexOf(`${waiting.stage.title}: approve`);
|
|
56
|
+
const handoff = at > 0 ? said.slice(0, at).trim() : "";
|
|
57
|
+
const draft = waiting.draft === null ? undefined : waiting.card.outputs[waiting.draft.id]?.trim();
|
|
58
|
+
return [
|
|
59
|
+
head,
|
|
60
|
+
decisionTerms(waiting),
|
|
61
|
+
...(handoff === "" ? [] : ["", handoff]),
|
|
62
|
+
...(draft === undefined || draft === "" ? [] : ["", "Draft:", draft]),
|
|
63
|
+
"",
|
|
64
|
+
`${waiting.flow.name} · ${waiting.stage.title}`,
|
|
65
|
+
].join("\n");
|
|
66
|
+
}
|
|
43
67
|
/** Buttons for one visit, minted before the send; `place` stamps the message they landed on. */
|
|
44
68
|
export function flowButtons(store, binding, waiting, now) {
|
|
45
69
|
const mint = (action) => ({ action, token: randomBytes(16).toString("hex") });
|
|
@@ -48,9 +72,9 @@ export function flowButtons(store, binding, waiting, now) {
|
|
|
48
72
|
const back = waiting.stage.onFail !== null ? mint("send-back") : null;
|
|
49
73
|
const all = [approve, edit, back].filter((one) => one !== null);
|
|
50
74
|
store.createTelegramFlowActions({ binding: binding.id, chatId: binding.chatId, card: waiting.card.id, entry: waiting.card.entry }, all, now);
|
|
51
|
-
const second = [edit === null ? null :
|
|
75
|
+
const second = [edit === null ? null : telegramButton("✏️ Edit", edit.token), back === null ? null : telegramButton("↩️ Send back", back.token)]
|
|
52
76
|
.filter((one) => one !== null);
|
|
53
|
-
return { keyboard: [[
|
|
77
|
+
return { keyboard: [[telegramButton("✅ Approve", approve.token)], ...(second.length === 0 ? [] : [second])], tokens: all.map(one => one.token) };
|
|
54
78
|
}
|
|
55
79
|
/** What a "Send to me" or "Person chooses" visit sent, read back for its message. */
|
|
56
80
|
export function flowSentContent(store, card, entry) {
|
|
@@ -63,12 +87,23 @@ export function flowSendKeyboardRow(origin, content) {
|
|
|
63
87
|
const pull = content.links.flatMap(one => "url" in one ? [{ text: one.label, url: one.url }] : []);
|
|
64
88
|
return [...pages.slice(0, 1), ...pull, ...pages.slice(1)].slice(0, 3);
|
|
65
89
|
}
|
|
90
|
+
/** A choice as Telegram shows it, first: what to choose, then where each option (and a reply) takes the card. The
|
|
91
|
+
* options are the ones its buttons carry: offered when sent, and still offered. */
|
|
92
|
+
export function flowChoiceHead(visit, content) {
|
|
93
|
+
const titleIn = (id) => visit.definition.stages.find(one => one.id === id)?.title ?? id;
|
|
94
|
+
const options = (content.options ?? []).flatMap(one => {
|
|
95
|
+
const option = visit.stage.options?.[one.choice];
|
|
96
|
+
return option === undefined || option.label !== one.label ? [] : [`${one.label} → ${option.to === FLOW_END ? "closes the card" : titleIn(option.to)}`];
|
|
97
|
+
});
|
|
98
|
+
const reply = content.reply === true ? replyTarget(visit.stage) : null;
|
|
99
|
+
return [`Choose what happens to “${visit.card.title}”`, [...options, ...(reply === null ? [] : [`or reply → ${titleIn(reply)}, with your note`])].join(" · ")];
|
|
100
|
+
}
|
|
66
101
|
/** A choice's buttons for one visit, one option to a row, minted before the send; `place` stamps the message they landed on. */
|
|
67
102
|
export function flowChoiceButtons(store, binding, visit, content, now) {
|
|
68
103
|
// Only the options the zone still offers as they were sent: a flow changed since sends no stale button.
|
|
69
104
|
const options = (content.options ?? []).filter(one => visit.stage.options?.[one.choice]?.label === one.label).map(one => ({ ...one, token: randomBytes(16).toString("hex") }));
|
|
70
105
|
store.createTelegramFlowChoices({ binding: binding.id, chatId: binding.chatId, card: visit.card.id, entry: visit.card.entry }, options, now);
|
|
71
|
-
return { keyboard: options.map(one => [
|
|
106
|
+
return { keyboard: options.map(one => [telegramButton(one.label, one.token)]), tokens: options.map(one => one.token) };
|
|
72
107
|
}
|
|
73
108
|
/** A tapped option, applied inside the update's transaction. */
|
|
74
109
|
export function applyFlowChoiceTap(store, binding, choice, message, repos, now) {
|
|
@@ -87,7 +122,9 @@ export function applyFlowChoiceTap(store, binding, choice, message, repos, now)
|
|
|
87
122
|
if (!chosen.ok)
|
|
88
123
|
return [{ kind: "ack", text: chosen.message.slice(0, 190) }];
|
|
89
124
|
store.retireFlowChoices(choice.card, choice.entry, now);
|
|
90
|
-
|
|
125
|
+
// The door's words name the option already ("Ship it. Moved to Ship."); an ending says what happened instead.
|
|
126
|
+
const said = chosen.said.startsWith(`${choice.label}.`) ? chosen.said : `You chose “${choice.label}”. ${chosen.said}`;
|
|
127
|
+
return [{ kind: "ack", text: choice.label.slice(0, 190) }, { kind: "edit", text: `${message.text}\n\n✅ ${said}`.slice(0, 4000) }];
|
|
91
128
|
}
|
|
92
129
|
/** A tapped flow button, applied inside the update's transaction. */
|
|
93
130
|
export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
@@ -106,7 +143,7 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
106
143
|
if (!decided.ok)
|
|
107
144
|
return [{ kind: "ack", text: decided.message.slice(0, 190) }];
|
|
108
145
|
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
109
|
-
return [{ kind: "ack", text: "Approved" }, { kind: "edit", text: `${message.text}\n\n✅
|
|
146
|
+
return [{ kind: "ack", text: "Approved" }, { kind: "edit", text: `${message.text}\n\n✅ ${decided.said}`.slice(0, 4000) }];
|
|
110
147
|
}
|
|
111
148
|
const prompt = { chatId: binding.chatId, binding: binding.id, card: action.card, entry: action.entry, mode: action.action };
|
|
112
149
|
if (action.action === "edit") {
|
|
@@ -156,8 +193,7 @@ export function applyFlowReply(store, binding, prompt, text, repos, now) {
|
|
|
156
193
|
store.retireTelegramFlowVisit(prompt.card, prompt.entry, now);
|
|
157
194
|
const fresh = flowDecisionAt(store, prompt.card, prompt.entry);
|
|
158
195
|
const buttons = flowButtons(store, binding, fresh, now);
|
|
159
|
-
return [{ kind: "decide", keyboard: buttons.keyboard, tokens: buttons.tokens,
|
|
160
|
-
text: `${waiting.flow.name}: your version of the draft for “${waiting.card.title}”\n\n${kept}\n\nApprove to send it as written.` }];
|
|
196
|
+
return [{ kind: "decide", keyboard: buttons.keyboard, tokens: buttons.tokens, text: flowDecisionText(fresh, null, `Approve your version of “${waiting.card.title}”`) }];
|
|
161
197
|
}
|
|
162
198
|
// A note over its limit is refused with the limit, never cut.
|
|
163
199
|
if (said.length > LIMITS.note)
|