toolroll 0.9.27 → 0.9.29
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 +28 -0
- package/dist/agentconfig.js +71 -3
- package/dist/browser/workspace.js +3 -3
- package/dist/build-review.js +3 -2
- package/dist/builder.js +10 -8
- package/dist/chat-actions.js +7 -4
- package/dist/chat-flow.js +10 -6
- package/dist/converse.js +3 -2
- package/dist/coordinator-proposals.js +2 -2
- package/dist/decision.d.ts +3 -3
- package/dist/decision.js +4 -3
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.js +1 -1
- package/dist/flow-actions.d.ts +1 -0
- package/dist/flow-actions.js +6 -7
- package/dist/flow-code.d.ts +3 -2
- package/dist/flow-code.js +6 -4
- package/dist/flow-draft.d.ts +10 -4
- package/dist/flow-draft.js +19 -8
- package/dist/flow-engine.d.ts +0 -3
- package/dist/flow-engine.js +17 -10
- package/dist/flow-people.d.ts +0 -1
- package/dist/flow-people.js +9 -1
- package/dist/flow-send.js +3 -2
- package/dist/flow-steps.js +33 -10
- package/dist/flows.d.ts +23 -8
- package/dist/flows.js +106 -52
- package/dist/guides.js +1 -1
- package/dist/lead-context.d.ts +3 -9
- package/dist/lead-context.js +21 -5
- package/dist/lead-status.js +17 -12
- package/dist/mate-doors.js +8 -2
- package/dist/mate-tools.js +27 -10
- package/dist/mate.js +13 -2
- package/dist/needs-you.d.ts +2 -2
- package/dist/needs-you.js +3 -0
- package/dist/operate.d.ts +3 -0
- package/dist/operate.js +144 -9
- package/dist/phase-routing.d.ts +86 -5
- package/dist/phase-routing.js +229 -17
- package/dist/process-liveness.d.ts +1 -0
- package/dist/process-liveness.js +5 -0
- package/dist/project-skills.d.ts +10 -0
- package/dist/proposal.d.ts +10 -2
- package/dist/proposal.js +25 -10
- package/dist/recipe-ui.js +2 -1
- package/dist/scout-report.d.ts +10 -4
- package/dist/scout-report.js +23 -6
- package/dist/scout.d.ts +2 -0
- package/dist/scout.js +83 -10
- package/dist/serve.js +71 -15
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +72 -8
- package/dist/store.js +230 -29
- package/dist/task-sizing.d.ts +78 -0
- package/dist/task-sizing.js +228 -0
- package/dist/task-text.d.ts +4 -4
- package/dist/task-text.js +10 -6
- package/dist/teammate-work.js +31 -7
- package/dist/teammates.d.ts +20 -1
- package/dist/teammates.js +18 -5
- package/dist/teams-chat.d.ts +4 -1
- package/dist/teams-chat.js +17 -6
- package/dist/telegram-flow.js +6 -2
- package/dist/test-temp.js +1 -1
- package/dist/text-limits.d.ts +67 -0
- package/dist/text-limits.js +73 -0
- package/dist/tier-report.d.ts +44 -0
- package/dist/tier-report.js +71 -0
- package/package.json +1 -1
package/dist/flow-steps.js
CHANGED
|
@@ -27,12 +27,13 @@ import { join } from "node:path";
|
|
|
27
27
|
import { approvedCommandShell, redactSecretAssignments, SETUP_ENV_ALLOWLIST, SETUP_ENV_DENYLIST } from "./builder.js";
|
|
28
28
|
import { assignmentOf } from "./assignment.js";
|
|
29
29
|
import { redactSecretLines, scanForSecrets } from "./evidence.js";
|
|
30
|
-
import { flowDefinitionOf } from "./flow-engine.js";
|
|
30
|
+
import { flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
31
31
|
import { cardFollowers, notifyPeople } from "./flow-people.js";
|
|
32
32
|
import { LINEAR_URL, readLinearKey } from "./flow-triggers.js";
|
|
33
33
|
import { cardEmailOf, fillFlowText } from "./flows.js";
|
|
34
34
|
import { JEV_MODEL, askJev, readJevAnswers, sortLog, sortRequest, sortState, sortWords } from "./flow-sort.js";
|
|
35
|
-
import { claudeDraftRunner, DRAFT_TIMEOUT, draftPrompt,
|
|
35
|
+
import { claudeDraftRunner, DRAFT_CHARS, DRAFT_TIMEOUT, draftPrompt, wholeDraft } from "./flow-draft.js";
|
|
36
|
+
import { overruns, passOn, TEXT_LIMITS, writeWithin } from "./text-limits.js";
|
|
36
37
|
import { readFlowSecrets, sendingReady, runRequest, sendEmail, toolWaiting, useTool } from "./flow-actions.js";
|
|
37
38
|
import { cleanFolder, runCode } from "./flow-code.js";
|
|
38
39
|
import { agentFence } from "./agent-fence.js";
|
|
@@ -246,7 +247,13 @@ function settle(store, definition, stage, card, outcome, now, durationMs) {
|
|
|
246
247
|
outcome = { state: "failed", said: `${outcome.said} It didn't work after three tries.` };
|
|
247
248
|
}
|
|
248
249
|
store.finishFlowStep(card.id, card.entry, { state: outcome.state === "passed" ? "passed" : "failed", result: outcome.said, ...kept }, now);
|
|
249
|
-
|
|
250
|
+
// What the steps after it read: whole up to TEXT_LIMITS.stageOutput; longer is attached whole to the card's discussion,
|
|
251
|
+
// and they read a link to it. Never cut.
|
|
252
|
+
const whole = outcome.output ?? outcome.said;
|
|
253
|
+
const passed = passOn(whole, TEXT_LIMITS.stageOutput, { label: "the card's discussion", href: flowCardHref(card.flow, card.id) });
|
|
254
|
+
if (passed.kept)
|
|
255
|
+
store.addFlowComment({ card: card.id, author: "flow", body: `What ${stage.title} produced, in full (${whole.length.toLocaleString("en-US")} characters):\n\n${whole}`, mentions: [] }, now);
|
|
256
|
+
store.updateFlowCard(card.id, { outputs: { ...card.outputs, [stage.id]: passed.text }, waiting: null }, now);
|
|
250
257
|
const titleOf = (id) => definition.stages.find(one => one.id === id)?.title ?? "another zone";
|
|
251
258
|
if (outcome.state === "passed") {
|
|
252
259
|
// A sort names where the card goes; one it isn't sure about is a person's to place.
|
|
@@ -289,16 +296,32 @@ function draftModel(store) {
|
|
|
289
296
|
/** A draft: Claude writes what the zone asks, from the card, with the lead chat's Claude model; the text stays on the card. */
|
|
290
297
|
async function draftCard(store, definition, stage, card, io, repo, now) {
|
|
291
298
|
const model = draftModel(store);
|
|
292
|
-
const
|
|
299
|
+
const runner = io.draft ?? claudeDraftRunner();
|
|
300
|
+
// Claude is told the limit before it writes; a draft over it is asked once to shorten, and one still over is kept whole
|
|
301
|
+
// in the step's log and attached to the card's discussion, linked from what the next steps read (settle): never cut.
|
|
302
|
+
let first = null;
|
|
303
|
+
const written = await writeWithin(async (shorten) => {
|
|
304
|
+
const answer = await runner({ model, prompt: draftPrompt(stage, card, definition, shorten === null || first === null ? null : { draft: first, ask: shorten }), timeoutMs: DRAFT_TIMEOUT });
|
|
305
|
+
// v105: what it cost, billed as this computer's Claude does (a plan's is $0).
|
|
306
|
+
if (answer.ok) {
|
|
307
|
+
store.recordDraftSpend({ repo, model: model === "default" ? null : model, costUsd: answer.costUsd ?? null }, now);
|
|
308
|
+
first ??= wholeDraft(answer.text);
|
|
309
|
+
}
|
|
310
|
+
return answer;
|
|
311
|
+
}, answer => answer.ok ? overruns({ draft: wholeDraft(answer.text) }, { draft: DRAFT_CHARS }) : []);
|
|
312
|
+
const answer = written.answer;
|
|
293
313
|
if (!answer.ok)
|
|
294
|
-
return { state: "retry", said: answer.said };
|
|
295
|
-
|
|
296
|
-
store.recordDraftSpend({ repo, model: model === "default" ? null : model, costUsd: answer.costUsd ?? null }, now);
|
|
297
|
-
const text = keptDraft(answer.text);
|
|
314
|
+
return written.repaired && first !== null ? draftOutcome(first, model, null, true) : { state: "retry", said: answer.said };
|
|
315
|
+
const text = wholeDraft(answer.text);
|
|
298
316
|
if (text === "")
|
|
299
|
-
return { state: "retry", said: "Claude's draft came back empty." };
|
|
317
|
+
return written.repaired && first !== null ? draftOutcome(first, model, answer.ms, true) : { state: "retry", said: "Claude's draft came back empty." };
|
|
318
|
+
return draftOutcome(text, model, answer.ms, written.repaired);
|
|
319
|
+
}
|
|
320
|
+
/** A draft step's result: the draft whole, on the card and in its log. */
|
|
321
|
+
function draftOutcome(text, model, ms, shortened) {
|
|
300
322
|
const words = text.split(/\s+/).filter(Boolean).length;
|
|
301
|
-
|
|
323
|
+
const over = text.length > DRAFT_CHARS ? ` It is still over the ${DRAFT_CHARS.toLocaleString("en-US")}-character limit after one ask to shorten, so it is kept whole.` : shortened ? " Shortened once to fit." : "";
|
|
324
|
+
return { state: "passed", said: `Drafted ${words} word${words === 1 ? "" : "s"}.${over}`, output: text, log: `Claude (${model})${ms === null ? "" : ` · ${(ms / 1000).toFixed(1)} s`}\n\n${text}` };
|
|
302
325
|
}
|
|
303
326
|
/** A sort: Jev picks one of the zone's answers for the card; the answer (or the not-sure path) says where it goes. */
|
|
304
327
|
async function sortCard(definition, stage, card, key, io) {
|
package/dist/flows.d.ts
CHANGED
|
@@ -167,6 +167,14 @@ export declare function deciderOf(stage: Pick<FlowStage, "approver" | "toOwner">
|
|
|
167
167
|
export declare function validateFlowDefinition(input: unknown, options?: {
|
|
168
168
|
stored?: boolean;
|
|
169
169
|
}): FlowDefinition;
|
|
170
|
+
/** The instructions 0.9.26 and earlier read from a saved zone: they re-check this length on every read, and a flow with a
|
|
171
|
+
* longer one won't load. A saved zone keeps its first part under it, and the rest in `instructionsMore`, which they ignore. */
|
|
172
|
+
export declare const FLOW_INSTRUCTIONS_STORED = 4000;
|
|
173
|
+
/** A flow's definition as the store saves it: any zone's instructions over FLOW_INSTRUCTIONS_STORED are split, so a
|
|
174
|
+
* person who goes back a version still opens the flow (with its instructions' first part). Unchanged otherwise. */
|
|
175
|
+
export declare function flowDefinitionForStore(json: string): string;
|
|
176
|
+
/** A saved flow's definition with every zone's instructions whole again (flowDefinitionForStore). */
|
|
177
|
+
export declare function flowDefinitionFromStore(json: string): string;
|
|
170
178
|
/** The zones a card can reach from `start` without passing through a zone `stop` picks (those zones aren't included). */
|
|
171
179
|
export declare function reachableWithout(stages: readonly FlowStage[], start: string, stop: (stage: FlowStage) => boolean): Set<string>;
|
|
172
180
|
/** The zones a choose zone's options lead to (an option that ends the card leads to none). */
|
|
@@ -183,20 +191,27 @@ type FlowFillCard = {
|
|
|
183
191
|
};
|
|
184
192
|
/** Fill a zone's text from the card: title, description, the latest note and earlier zones' reports. */
|
|
185
193
|
export declare function fillFlowText(template: string, card: FlowFillCard, encode?: (value: string) => string): string;
|
|
186
|
-
/** The task door's goal limit (task-text.ts). A work zone's goal is kept
|
|
187
|
-
export declare const FLOW_GOAL_LIMIT
|
|
188
|
-
/** What stands in
|
|
189
|
-
export declare const
|
|
194
|
+
/** The task door's goal limit (task-text.ts). A work zone's goal is kept within it. */
|
|
195
|
+
export declare const FLOW_GOAL_LIMIT: 8000;
|
|
196
|
+
/** What stands in a goal for a filled-in value too long to hold whole: the agent is given it whole, attached (flowGoalCuts). */
|
|
197
|
+
export declare const flowAttachedMark: (label: string) => string;
|
|
190
198
|
/** A work zone's words, plus the card and any send-back note when the words leave them out. "Fix the bug on the card"
|
|
191
199
|
* says nothing on its own (found in the real e2e: the builder saw only the branch name). */
|
|
192
200
|
export declare function flowWorkTemplate(instructions: string, card: {
|
|
193
201
|
description: string | null;
|
|
194
202
|
note: string | null;
|
|
195
203
|
}): string;
|
|
196
|
-
/**
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
204
|
+
/** How a filled-in value is named where it is attached: the same words flowGoalCuts labels it with. */
|
|
205
|
+
export declare function flowValueLabel(key: string, stageTitle?: (id: string) => string): string;
|
|
206
|
+
/** A work zone's goal, filled from the card and kept within `limit`: the zone's own words stay whole, and every filled-in
|
|
207
|
+
* value goes in whole. When they don't all fit, the longest values are attached instead (each named in the goal by
|
|
208
|
+
* flowAttachedMark, and given to the agent whole by flowGoalCuts) until the rest fits: nothing is cut. Words that fill
|
|
209
|
+
* the limit to the last character leave attached values a shorter mark, then none (they are still attached). Null only
|
|
210
|
+
* when the template's own words are over the limit: the caller then files the zone's words alone (flowGoal). */
|
|
211
|
+
export declare function fitFlowText(template: string, card: FlowFillCard, limit?: 8000, stageTitle?: (id: string) => string): string | null;
|
|
212
|
+
/** A work zone's goal for a card: its words, the card and any note, fitted (fitFlowText); when the zone's words leave no
|
|
213
|
+
* room for the card's own lines, its words alone, with the card attached. Null only for words over the limit. */
|
|
214
|
+
export declare function flowGoal(instructions: string, card: FlowFillCard, stageTitle?: (id: string) => string): string | null;
|
|
200
215
|
/** The first email address a card mentions, or "" — what {{card.email}} holds. */
|
|
201
216
|
export declare function cardEmailOf(card: {
|
|
202
217
|
title: string;
|
package/dist/flows.js
CHANGED
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
* The engine is deterministic and model-free: it runs in the worker's pass
|
|
48
48
|
* beside routines, and no card ever skips an approval the task itself needs.
|
|
49
49
|
*/
|
|
50
|
+
import { TEXT_LIMITS } from "./text-limits.js";
|
|
50
51
|
import { createHash } from "node:crypto";
|
|
51
52
|
export const FLOW_STAGE_KINDS = ["inbox", "task", "report", "approval", "check", "pull-request", "update", "notify", "sort", "draft", "request", "email", "tool", "wait", "teammate", "send", "choose", "done"];
|
|
52
53
|
export const FLOW_COLORS = ["slate", "blue", "violet", "amber", "green", "rose"];
|
|
@@ -310,7 +311,8 @@ export function validateFlowDefinition(input, options = {}) {
|
|
|
310
311
|
return {
|
|
311
312
|
id, title, kind: kind,
|
|
312
313
|
zone: { x: coordinate(zone["x"], -20000, 20000, index * 320), y: coordinate(zone["y"], -20000, 20000, 0), w: coordinate(zone["w"], 220, 1200, 280), h: coordinate(zone["h"], 160, 1600, 360), color },
|
|
313
|
-
|
|
314
|
+
// Reading a saved flow never re-checks length: a limit is for writing.
|
|
315
|
+
instructions: text(stage["instructions"], options.stored ? Number.POSITIVE_INFINITY : TEXT_LIMITS.flowInstructions),
|
|
314
316
|
planning: kind === "task" ? planning ?? "auto" : null,
|
|
315
317
|
approver: kind === "approval" && stage["toOwner"] !== true ? text(stage["approver"], 64) : null,
|
|
316
318
|
...(kind === "approval" && stage["toOwner"] === true ? { toOwner: true } : {}),
|
|
@@ -356,11 +358,6 @@ export function validateFlowDefinition(input, options = {}) {
|
|
|
356
358
|
throw new Error(`Zone ${stage.title}: an answer can't send cards back into the same zone.`);
|
|
357
359
|
if ((stage.kind === "task" || stage.kind === "report") && stage.instructions === null)
|
|
358
360
|
throw new Error(`Zone ${stage.title}: say what the agent should do.`);
|
|
359
|
-
// A work zone's goal must fit a task: its own words whole, with room left for the card's details. Refused here, when the flow
|
|
360
|
-
// is saved, never when a card arrives. A flow saved before this rule is still read, and its card says why it can't file.
|
|
361
|
-
const over = !options.stored && (stage.kind === "task" || stage.kind === "report") && stage.instructions !== null ? flowGoalOverflow(stage.instructions) : 0;
|
|
362
|
-
if (over > 0)
|
|
363
|
-
throw new Error(`Zone ${stage.title}: its instructions leave no room for the card's details. Shorten them by ${over} characters.`);
|
|
364
361
|
if (stage.kind === "draft" && stage.instructions === null)
|
|
365
362
|
throw new Error(`Zone ${stage.title}: say what Claude should write.`);
|
|
366
363
|
if (stage.kind === "notify" && stage.message === null)
|
|
@@ -381,6 +378,59 @@ export function validateFlowDefinition(input, options = {}) {
|
|
|
381
378
|
}
|
|
382
379
|
return { version: 1, start, stages };
|
|
383
380
|
}
|
|
381
|
+
/** The instructions 0.9.26 and earlier read from a saved zone: they re-check this length on every read, and a flow with a
|
|
382
|
+
* longer one won't load. A saved zone keeps its first part under it, and the rest in `instructionsMore`, which they ignore. */
|
|
383
|
+
export const FLOW_INSTRUCTIONS_STORED = 4000;
|
|
384
|
+
/** A flow's definition as the store saves it: any zone's instructions over FLOW_INSTRUCTIONS_STORED are split, so a
|
|
385
|
+
* person who goes back a version still opens the flow (with its instructions' first part). Unchanged otherwise. */
|
|
386
|
+
export function flowDefinitionForStore(json) {
|
|
387
|
+
let raw;
|
|
388
|
+
try {
|
|
389
|
+
raw = JSON.parse(json);
|
|
390
|
+
}
|
|
391
|
+
catch {
|
|
392
|
+
return json;
|
|
393
|
+
}
|
|
394
|
+
const stages = raw?.stages;
|
|
395
|
+
if (!Array.isArray(stages))
|
|
396
|
+
return json;
|
|
397
|
+
let split = false;
|
|
398
|
+
const kept = stages.map(one => {
|
|
399
|
+
const stage = one;
|
|
400
|
+
const words = stage?.["instructions"];
|
|
401
|
+
if (stage === null || typeof stage !== "object" || typeof words !== "string" || words.length <= FLOW_INSTRUCTIONS_STORED)
|
|
402
|
+
return one;
|
|
403
|
+
let at = FLOW_INSTRUCTIONS_STORED;
|
|
404
|
+
if (/[\uD800-\uDBFF]/.test(words[at - 1]))
|
|
405
|
+
at--;
|
|
406
|
+
split = true;
|
|
407
|
+
return { ...stage, instructions: words.slice(0, at), instructionsMore: words.slice(at) };
|
|
408
|
+
});
|
|
409
|
+
return split ? JSON.stringify({ ...raw, stages: kept }) : json;
|
|
410
|
+
}
|
|
411
|
+
/** A saved flow's definition with every zone's instructions whole again (flowDefinitionForStore). */
|
|
412
|
+
export function flowDefinitionFromStore(json) {
|
|
413
|
+
if (!json.includes("\"instructionsMore\""))
|
|
414
|
+
return json;
|
|
415
|
+
let raw;
|
|
416
|
+
try {
|
|
417
|
+
raw = JSON.parse(json);
|
|
418
|
+
}
|
|
419
|
+
catch {
|
|
420
|
+
return json;
|
|
421
|
+
}
|
|
422
|
+
const stages = raw?.stages;
|
|
423
|
+
if (!Array.isArray(stages))
|
|
424
|
+
return json;
|
|
425
|
+
const whole = stages.map(one => {
|
|
426
|
+
const stage = one;
|
|
427
|
+
if (stage === null || typeof stage !== "object" || typeof stage["instructionsMore"] !== "string")
|
|
428
|
+
return one;
|
|
429
|
+
const { instructionsMore, ...rest } = stage;
|
|
430
|
+
return { ...rest, instructions: `${typeof stage["instructions"] === "string" ? stage["instructions"] : ""}${instructionsMore}` };
|
|
431
|
+
});
|
|
432
|
+
return JSON.stringify({ ...raw, stages: whole });
|
|
433
|
+
}
|
|
384
434
|
/** A Pull request zone's merge: true is squash, the default. */
|
|
385
435
|
function validateMerge(value, title) {
|
|
386
436
|
if (value === true)
|
|
@@ -498,12 +548,10 @@ const FILLED = /\{\{\s*(card\.title|card\.description|card\.email|note|stage\.([
|
|
|
498
548
|
export function fillFlowText(template, card, encode = value => value) {
|
|
499
549
|
return template.replace(FILLED, (_match, key, stage) => encode(fillOne(key, stage, card))).trim();
|
|
500
550
|
}
|
|
501
|
-
/** The task door's goal limit (task-text.ts). A work zone's goal is kept
|
|
502
|
-
export const FLOW_GOAL_LIMIT =
|
|
503
|
-
/** What stands in
|
|
504
|
-
export const
|
|
505
|
-
/** The least room each filled-in value keeps: the cut mark and 20 characters from each end. */
|
|
506
|
-
const VALUE_ROOM = FLOW_CUT.length + 40;
|
|
551
|
+
/** The task door's goal limit (task-text.ts). A work zone's goal is kept within it. */
|
|
552
|
+
export const FLOW_GOAL_LIMIT = TEXT_LIMITS.goal;
|
|
553
|
+
/** What stands in a goal for a filled-in value too long to hold whole: the agent is given it whole, attached (flowGoalCuts). */
|
|
554
|
+
export const flowAttachedMark = (label) => `[${label}: attached in full below]`;
|
|
507
555
|
/** A work zone's words, plus the card and any send-back note when the words leave them out. "Fix the bug on the card"
|
|
508
556
|
* says nothing on its own (found in the real e2e: the builder saw only the branch name). */
|
|
509
557
|
export function flowWorkTemplate(instructions, card) {
|
|
@@ -515,50 +563,56 @@ export function flowWorkTemplate(instructions, card) {
|
|
|
515
563
|
];
|
|
516
564
|
return [instructions, ...extra].join("\n\n");
|
|
517
565
|
}
|
|
518
|
-
/** Characters a work zone's instructions leave no room for, with every filled-in value kept to its least room. 0 when they fit. */
|
|
519
|
-
function flowGoalOverflow(instructions) {
|
|
520
|
-
const widest = flowWorkTemplate(instructions, { description: "x", note: "x" });
|
|
521
|
-
const values = [...widest.matchAll(FILLED)].length;
|
|
522
|
-
return Math.max(0, widest.replace(FILLED, "").length + values * VALUE_ROOM - (FLOW_GOAL_LIMIT - 1));
|
|
523
|
-
}
|
|
524
566
|
/** Script output can carry carriage returns, colour codes and hidden marks a task goal refuses; the goal drops them. */
|
|
525
567
|
const goalSafe = (value) => value.replace(/\r\n?/g, "\n").replace(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F\u2028\u2029\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, "");
|
|
526
|
-
/**
|
|
527
|
-
function
|
|
528
|
-
if (
|
|
529
|
-
return
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
if (
|
|
535
|
-
|
|
536
|
-
|
|
568
|
+
/** How a filled-in value is named where it is attached: the same words flowGoalCuts labels it with. */
|
|
569
|
+
export function flowValueLabel(key, stageTitle = id => id) {
|
|
570
|
+
if (key === "card.title")
|
|
571
|
+
return "The card's title";
|
|
572
|
+
if (key === "card.description")
|
|
573
|
+
return "The card's description";
|
|
574
|
+
if (key === "card.email")
|
|
575
|
+
return "The card's email address";
|
|
576
|
+
if (key === "note")
|
|
577
|
+
return "The note it was sent back with";
|
|
578
|
+
const match = /^stage\.([a-z0-9-]+)(?:\.(items|report))?$/.exec(key);
|
|
579
|
+
if (match === null)
|
|
580
|
+
return key;
|
|
581
|
+
return `What ${stageTitle(match[1])} ${match[2] === "items" ? "listed" : match[2] === "report" ? "reported in full" : "found"}`;
|
|
537
582
|
}
|
|
538
|
-
/** A work zone's goal, filled from the card and kept
|
|
539
|
-
*
|
|
540
|
-
*
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
if (each < FLOW_CUT.length + 2)
|
|
558
|
-
return null;
|
|
583
|
+
/** A work zone's goal, filled from the card and kept within `limit`: the zone's own words stay whole, and every filled-in
|
|
584
|
+
* value goes in whole. When they don't all fit, the longest values are attached instead (each named in the goal by
|
|
585
|
+
* flowAttachedMark, and given to the agent whole by flowGoalCuts) until the rest fits: nothing is cut. Words that fill
|
|
586
|
+
* the limit to the last character leave attached values a shorter mark, then none (they are still attached). Null only
|
|
587
|
+
* when the template's own words are over the limit: the caller then files the zone's words alone (flowGoal). */
|
|
588
|
+
export function fitFlowText(template, card, limit = FLOW_GOAL_LIMIT, stageTitle) {
|
|
589
|
+
const matches = [...template.matchAll(FILLED)];
|
|
590
|
+
const values = matches.map(match => goalSafe(fillOne(match[1], match[2], card)));
|
|
591
|
+
const marks = matches.map(match => flowAttachedMark(flowValueLabel(match[1], stageTitle)));
|
|
592
|
+
const attached = new Set();
|
|
593
|
+
const build = () => { let at = 0; return template.replace(FILLED, () => { const i = at++; return attached.has(i) ? marks[i] : values[i]; }).trim(); };
|
|
594
|
+
let goal = build();
|
|
595
|
+
// The longest first, so the fewest values leave the goal.
|
|
596
|
+
const order = values.map((value, i) => i).filter(i => values[i].length > marks[i].length).sort((a, b) => values[b].length - values[a].length);
|
|
597
|
+
for (const i of order) {
|
|
598
|
+
if (goal.length <= limit)
|
|
599
|
+
break;
|
|
600
|
+
attached.add(i);
|
|
601
|
+
goal = build();
|
|
559
602
|
}
|
|
560
|
-
|
|
561
|
-
|
|
603
|
+
for (const shorter of ["[attached]", ""]) {
|
|
604
|
+
if (goal.length <= limit)
|
|
605
|
+
break;
|
|
606
|
+
for (const i of attached)
|
|
607
|
+
marks[i] = shorter;
|
|
608
|
+
goal = build();
|
|
609
|
+
}
|
|
610
|
+
return goal.length <= limit ? goal : null;
|
|
611
|
+
}
|
|
612
|
+
/** A work zone's goal for a card: its words, the card and any note, fitted (fitFlowText); when the zone's words leave no
|
|
613
|
+
* room for the card's own lines, its words alone, with the card attached. Null only for words over the limit. */
|
|
614
|
+
export function flowGoal(instructions, card, stageTitle) {
|
|
615
|
+
return fitFlowText(flowWorkTemplate(instructions, card), card, FLOW_GOAL_LIMIT, stageTitle) ?? fitFlowText(instructions, card, FLOW_GOAL_LIMIT, stageTitle);
|
|
562
616
|
}
|
|
563
617
|
/** The first email address a card mentions, or "" — what {{card.email}} holds. */
|
|
564
618
|
export function cardEmailOf(card) {
|
package/dist/guides.js
CHANGED
|
@@ -207,7 +207,7 @@ const steering = {
|
|
|
207
207
|
${AUTHORITY_LINE}
|
|
208
208
|
|
|
209
209
|
Steering is an OPERATOR ceremony. \`task steer <id> --note "..." --as
|
|
210
|
-
<you> --token <t>\` files one
|
|
210
|
+
<you> --token <t>\` files one note (≤4000 characters) for the
|
|
211
211
|
task's NEXT attempt, and the credential is the point: the note appears
|
|
212
212
|
in the agent's brief under an OPERATOR STEERING heading, so only a
|
|
213
213
|
verified person may put words there. Anonymous or agent-authored notes
|
package/dist/lead-context.d.ts
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
/** Bounded DB memory shared by browser and terminal lead turns. No provider,
|
|
2
|
-
* repository scan, mutation, or hidden approval happens while catching up.
|
|
3
|
-
*
|
|
4
|
-
* The bundle is ordered by importance and built fresh each turn: who the lead
|
|
5
|
-
* is (name, persona), who it is talking to (first name, time zone, today), what
|
|
6
|
-
* it knows about them (their confirmed lines), the people, AI teammates and team
|
|
7
|
-
* chats it works with (one line each), the channel, what needs them now,
|
|
8
|
-
* projects by name with their active decisions, then the rest. Over 8 KB, the
|
|
9
|
-
* least important goes first: the rest, then people, then decisions. */
|
|
10
1
|
import type { Store } from './store.js';
|
|
11
2
|
import { type AssignmentCatchUp } from './assignment-brief.js';
|
|
12
3
|
export declare const LEAD_CONTEXT_MAX_BYTES = 8000;
|
|
13
4
|
/** Where this turn's conversation happens. */
|
|
14
5
|
export type LeadChannel = 'console' | 'terminal' | 'telegram' | 'slack' | 'discord' | 'teams';
|
|
6
|
+
/** One message on the channel, in characters: the lead writes a reply within it (it is told before it writes and asked
|
|
7
|
+
* once to shorten one over it); a longer reply is split across messages, never cut. Null: no platform limit. */
|
|
8
|
+
export declare const LEAD_REPLY_LIMITS: Record<LeadChannel, number | null>;
|
|
15
9
|
export type LeadContextOptions = {
|
|
16
10
|
evidenceRoot?: string;
|
|
17
11
|
/** The person this turn talks to (their account name). */
|
package/dist/lead-context.js
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/** Bounded DB memory shared by browser and terminal lead turns. No provider,
|
|
2
|
+
* repository scan, mutation, or hidden approval happens while catching up.
|
|
3
|
+
*
|
|
4
|
+
* The bundle is ordered by importance and built fresh each turn: who the lead
|
|
5
|
+
* is (name, persona), who it is talking to (first name, time zone, today), what
|
|
6
|
+
* it knows about them (their confirmed lines), the people, AI teammates and team
|
|
7
|
+
* chats it works with (one line each), the channel, what needs them now,
|
|
8
|
+
* projects by name with their active decisions, then the rest. Over 8 KB, the
|
|
9
|
+
* least important goes first: the rest, then people, then decisions. */
|
|
10
|
+
import { PLATFORM_LIMITS } from './text-limits.js';
|
|
1
11
|
import { activeDecisionsOf, assignmentCatchUp } from './assignment-brief.js';
|
|
2
12
|
import { publicChatText } from './chat-display.js';
|
|
3
13
|
import { leadIdentityOf } from './lead-identity.js';
|
|
@@ -37,13 +47,19 @@ function followThrough(store, owner, thread) {
|
|
|
37
47
|
...(corrections.length === 0 ? {} : { followThrough: 'The operator confirmed these corrections since your last reply. Re-check the open proposals and promises listed here; release or replace any they affect and say in one line what you changed.' }) };
|
|
38
48
|
}
|
|
39
49
|
export const LEAD_CONTEXT_MAX_BYTES = 8_000;
|
|
50
|
+
/** One message on the channel, in characters: the lead writes a reply within it (it is told before it writes and asked
|
|
51
|
+
* once to shorten one over it); a longer reply is split across messages, never cut. Null: no platform limit. */
|
|
52
|
+
export const LEAD_REPLY_LIMITS = {
|
|
53
|
+
console: null, terminal: null, telegram: PLATFORM_LIMITS.telegram, slack: PLATFORM_LIMITS.slack, discord: PLATFORM_LIMITS.discord, teams: PLATFORM_LIMITS.teams,
|
|
54
|
+
};
|
|
55
|
+
const within = (channel) => { const limit = LEAD_REPLY_LIMITS[channel]; return limit === null ? '' : ` Keep each reply within ${limit.toLocaleString('en-US')} characters, one message; a longer one is split across messages.`; };
|
|
40
56
|
const CHANNEL_WORDS = {
|
|
41
57
|
console: 'The Toolroll console in a browser: cards and links show beside your reply.',
|
|
42
58
|
terminal: 'The Toolroll CLI in a terminal: plain text only.',
|
|
43
|
-
telegram:
|
|
44
|
-
slack:
|
|
45
|
-
discord:
|
|
46
|
-
teams:
|
|
59
|
+
telegram: `Telegram on their phone: a few short lines, the most important first.${within('telegram')}`,
|
|
60
|
+
slack: `A Slack thread: a few short lines; teammates may read it.${within('slack')}`,
|
|
61
|
+
discord: `A Discord thread: a few short lines; teammates may read it.${within('discord')}`,
|
|
62
|
+
teams: `A Microsoft Teams thread: a few short lines; teammates may read it.${within('teams')}`,
|
|
47
63
|
};
|
|
48
64
|
/** "alex.pelletier@example.com" → "Alex". */
|
|
49
65
|
export function firstNameOf(account) {
|
|
@@ -114,7 +130,7 @@ export function leadContext(store, repos, now, options = {}) {
|
|
|
114
130
|
you: { firstName, ...localNow(now, options.timeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone) },
|
|
115
131
|
aboutYou,
|
|
116
132
|
people: { people: [], teammates: [], teams: [] },
|
|
117
|
-
channel: options.channel === undefined ? null : { id: options.channel, fit: CHANNEL_WORDS[options.channel] },
|
|
133
|
+
channel: options.channel === undefined ? null : { id: options.channel, fit: CHANNEL_WORDS[options.channel], replyLimit: LEAD_REPLY_LIMITS[options.channel] },
|
|
118
134
|
needsYou: brief.assignments.filter(needs).map(task),
|
|
119
135
|
...(options.owner === undefined || options.thread === undefined ? {} : followThrough(store, options.owner, options.thread)),
|
|
120
136
|
projects,
|
package/dist/lead-status.js
CHANGED
|
@@ -114,19 +114,21 @@ export function taskWaitSnapshot(store, taskId, now, watchedRun) {
|
|
|
114
114
|
const next = holdKind === "operator" ? "Remove hold" : holdKind === "revision" ? "Review plan" : holdKind === "stop" ? "Resume or close attempt" : holdKind === "contest" ? "Choose a result" : "Review task";
|
|
115
115
|
return answer("Needs a person", next, true, 1, "needs-person");
|
|
116
116
|
}
|
|
117
|
-
if (task["plan"] === "drafted")
|
|
118
|
-
return answer("Needs a person", "Approve plan", true, 1, "needs-person");
|
|
119
|
-
if (Number(task["strikes"] ?? 0) >= 3)
|
|
120
|
-
return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
|
|
121
117
|
// An admitted attempt is already the work being watched. Its immutable
|
|
122
118
|
// approval snapshot lives on the run, so a damaged or later-edited current
|
|
123
119
|
// scope must not make `wait` abandon that attempt before it settles.
|
|
124
120
|
if (attempt !== undefined)
|
|
125
121
|
return answer("Running", phase === null ? "Wait for attempt" : `Wait — ${phase}`, false, null, null);
|
|
126
|
-
const scope = store.handle.prepare(
|
|
122
|
+
const scope = store.handle.prepare(`SELECT digest, approved_digest, profile_state,
|
|
123
|
+
(approved_at IS NOT NULL AND approved_by IS NOT NULL AND approved_digest = digest) AS approved
|
|
124
|
+
FROM task_scope WHERE task_id = ?`).get(taskId);
|
|
125
|
+
if (task["plan"] === "drafted" && Number(scope?.["approved"]) !== 1)
|
|
126
|
+
return answer("Needs a person", "Approve plan", true, 1, "needs-person");
|
|
127
|
+
if (Number(task["strikes"] ?? 0) >= 3)
|
|
128
|
+
return answer("Needs a person", "Review repeated failures", true, 1, "needs-person");
|
|
127
129
|
if (scope === undefined)
|
|
128
130
|
return answer("Needs a person", "Add scope", true, 1, "needs-person");
|
|
129
|
-
if (scope["
|
|
131
|
+
if (Number(scope["approved"]) !== 1 || scope["profile_state"] !== "resolved")
|
|
130
132
|
return answer("Needs a person", "Approve task", true, 1, "needs-person");
|
|
131
133
|
if (state === "running" && attempt === undefined) {
|
|
132
134
|
const liveClaim = store.handle.prepare(`SELECT 1 AS hit FROM claim INDEXED BY claim_by_task
|
|
@@ -157,7 +159,7 @@ function queueReason(row) {
|
|
|
157
159
|
return "needs a result choice";
|
|
158
160
|
if (hold === "backoff")
|
|
159
161
|
return "retrying later";
|
|
160
|
-
if (row["plan"] === "drafted")
|
|
162
|
+
if (row["plan"] === "drafted" && Number(row["approved"]) !== 1)
|
|
161
163
|
return "needs plan review";
|
|
162
164
|
if (Number(row["strikes"] ?? 0) >= 3)
|
|
163
165
|
return "stalled after failures";
|
|
@@ -165,7 +167,7 @@ function queueReason(row) {
|
|
|
165
167
|
return "waiting for another task";
|
|
166
168
|
if (row["scope_digest"] == null)
|
|
167
169
|
return "needs a scope";
|
|
168
|
-
if (row["
|
|
170
|
+
if (Number(row["approved"]) !== 1 || row["profile_state"] !== "resolved")
|
|
169
171
|
return "needs approval";
|
|
170
172
|
return "ready for a worker";
|
|
171
173
|
}
|
|
@@ -181,7 +183,7 @@ export function repairStaleStatuses(store, now) {
|
|
|
181
183
|
}
|
|
182
184
|
/** `viewer`: the person asking (the lead asks as its person); null when nobody is known, which shows no lead. */
|
|
183
185
|
export function installationStatus(store, now, viewer = currentActor()?.account ?? null) {
|
|
184
|
-
const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.external_id AS task
|
|
186
|
+
const runningRows = store.handle.prepare(`SELECT run.id, run.role, run.phase, ref.id AS task_ref, ref.external_id AS task
|
|
185
187
|
FROM run INDEXED BY work_unfinished
|
|
186
188
|
JOIN task_ref AS ref ON ref.id = run.task_ref
|
|
187
189
|
JOIN claim ON claim.lease_id = run.lease_id
|
|
@@ -194,8 +196,10 @@ export function installationStatus(store, now, viewer = currentActor()?.account
|
|
|
194
196
|
phases.set(phase, (phases.get(phase) ?? 0) + 1);
|
|
195
197
|
return { task: String(row["task"]), run: Number(row["id"]), phase };
|
|
196
198
|
});
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
+
const runningRefs = new Set(runningRows.map(row => Number(row["task_ref"])));
|
|
200
|
+
const queuedRows = store.handle.prepare(`SELECT task.id, ref.id AS task_ref, ref.plan, ref.strikes, scope.digest AS scope_digest,
|
|
201
|
+
scope.profile_state,
|
|
202
|
+
(scope.approved_at IS NOT NULL AND scope.approved_by IS NOT NULL AND scope.approved_digest = scope.digest) AS approved,
|
|
199
203
|
EXISTS (SELECT 1 FROM task_edge WHERE task_edge.blocked = task.id
|
|
200
204
|
AND EXISTS (SELECT 1 FROM task AS blocker WHERE blocker.id = task_edge.blocker AND blocker.state <> 'done')) AS blocked,
|
|
201
205
|
(SELECT owner_kind FROM hold INDEXED BY hold_by_task WHERE hold.task_ref = ref.id AND (hold.until IS NULL OR hold.until > ?)
|
|
@@ -203,7 +207,8 @@ export function installationStatus(store, now, viewer = currentActor()?.account
|
|
|
203
207
|
FROM task INDEXED BY task_by_state
|
|
204
208
|
JOIN task_ref AS ref INDEXED BY sqlite_autoindex_task_ref_1 ON ref.backend = ? AND ref.external_id = task.id
|
|
205
209
|
LEFT JOIN task_scope AS scope ON scope.task_id = task.id
|
|
206
|
-
WHERE task.state = 'queued'`).all(now.toISOString(), BUILT_IN)
|
|
210
|
+
WHERE task.state = 'queued'`).all(now.toISOString(), BUILT_IN)
|
|
211
|
+
.filter(row => !runningRefs.has(Number(row["task_ref"])));
|
|
207
212
|
const reasons = new Map();
|
|
208
213
|
const queuedTasks = [];
|
|
209
214
|
const pauses = openAuthPauses(store);
|
package/dist/mate-doors.js
CHANGED
|
@@ -20,7 +20,7 @@ import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
|
20
20
|
import { TeamLeads } from './team-leads.js';
|
|
21
21
|
import { fileTaskProposal } from "./proposal.js";
|
|
22
22
|
import { proposeGuarded } from "./scope.js";
|
|
23
|
-
import { isRiskLevel, PHASES, riskTitle, specWords } from "./phase-routing.js";
|
|
23
|
+
import { isRiskLevel, isTaskSize, PHASES, riskTitle, specWords } from "./phase-routing.js";
|
|
24
24
|
import { isProviderId } from "./provider.js";
|
|
25
25
|
import { applyChatReview } from "./chat-review.js";
|
|
26
26
|
import { applyChatTaskAction } from "./chat-task-actions.js";
|
|
@@ -454,11 +454,15 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
454
454
|
const clear = payload["clear"] === true;
|
|
455
455
|
const provider = payloadString(payload, "provider");
|
|
456
456
|
const model = payloadString(payload, "model");
|
|
457
|
+
const size = payload["size"];
|
|
458
|
+
const risky = payload["risky"];
|
|
457
459
|
if (risk !== undefined && !isRiskLevel(risk))
|
|
458
460
|
return refuse("refused", "this proposal carries an unknown risk level");
|
|
461
|
+
if (size !== undefined && (!isTaskSize(size) || typeof risky !== "boolean"))
|
|
462
|
+
return refuse("refused", "this proposal carries an unknown size");
|
|
459
463
|
if (phase !== undefined && (typeof phase !== "string" || !PHASES.includes(phase)))
|
|
460
464
|
return refuse("refused", "this proposal names an unknown role");
|
|
461
|
-
if (risk === undefined && phase === undefined)
|
|
465
|
+
if (risk === undefined && phase === undefined && size === undefined)
|
|
462
466
|
return refuse("refused", "this proposal changes nothing about the agents");
|
|
463
467
|
let override;
|
|
464
468
|
if (typeof phase === "string") {
|
|
@@ -478,6 +482,7 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
478
482
|
return standing.ok ? { ok: true } : { ok: false, reason: `your approver standing changed (${standing.reason}) — sign in again` };
|
|
479
483
|
},
|
|
480
484
|
...(isRiskLevel(risk) ? { risk } : {}),
|
|
485
|
+
...(isTaskSize(size) && typeof risky === "boolean" ? { size: { size, risky } } : {}),
|
|
481
486
|
...(override === undefined ? {} : { override }),
|
|
482
487
|
expectDigest: payloadString(payload, "sawDigest"),
|
|
483
488
|
// The pair is re-proved against the role's configured choices
|
|
@@ -508,6 +513,7 @@ function executeProposal(store, actor, kind, payload, now, options) {
|
|
|
508
513
|
const roleWord = typeof phase === "string" ? { plan: "planner", build: "builder", repair: "repair", review: "reviewer" }[phase] ?? phase : null;
|
|
509
514
|
const changed = [
|
|
510
515
|
...(isRiskLevel(risk) ? [`risk is now ${riskTitle(risk).toLowerCase()}`] : []),
|
|
516
|
+
...(isTaskSize(size) ? [`it is now a ${size}${risky === true ? ", risky" : ""} change`] : []),
|
|
511
517
|
...(roleWord === null ? [] : clear ? [`the ${roleWord} choice was cleared — the recommendation stands again`] : [`the ${roleWord} is now ${specWords({ provider: provider, model: model })}`]),
|
|
512
518
|
];
|
|
513
519
|
return {
|