toolroll 0.9.26 → 0.9.28

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/THIRD_PARTY_NOTICES.md +41 -0
  3. package/dist/agentconfig.js +71 -3
  4. package/dist/brand-icons.d.ts +168 -0
  5. package/dist/brand-icons.js +32 -0
  6. package/dist/brand-mark.d.ts +27 -0
  7. package/dist/brand-mark.js +49 -0
  8. package/dist/browser/THIRD_PARTY_NOTICES.txt +41 -0
  9. package/dist/browser/workspace.css +1 -1
  10. package/dist/browser/workspace.js +21 -23
  11. package/dist/browser-workspace.d.ts +17 -0
  12. package/dist/build-review.js +3 -2
  13. package/dist/builder.js +10 -8
  14. package/dist/chat-actions.js +7 -4
  15. package/dist/chat-delivery.js +3 -3
  16. package/dist/chat-flow.d.ts +4 -2
  17. package/dist/chat-flow.js +36 -10
  18. package/dist/converse.js +3 -2
  19. package/dist/coordinator-proposals.js +2 -2
  20. package/dist/decision.d.ts +3 -3
  21. package/dist/decision.js +4 -3
  22. package/dist/desktop-update.d.ts +2 -2
  23. package/dist/evidence.js +1 -1
  24. package/dist/flow-actions.d.ts +1 -0
  25. package/dist/flow-actions.js +6 -7
  26. package/dist/flow-code.d.ts +3 -2
  27. package/dist/flow-code.js +6 -4
  28. package/dist/flow-draft.d.ts +10 -4
  29. package/dist/flow-draft.js +19 -8
  30. package/dist/flow-engine.d.ts +0 -3
  31. package/dist/flow-engine.js +17 -10
  32. package/dist/flow-items.d.ts +46 -0
  33. package/dist/flow-items.js +152 -0
  34. package/dist/flow-people.d.ts +0 -1
  35. package/dist/flow-people.js +9 -1
  36. package/dist/flow-send.d.ts +5 -0
  37. package/dist/flow-send.js +33 -6
  38. package/dist/flow-steps.js +33 -10
  39. package/dist/flows-ui.js +5 -1
  40. package/dist/flows.d.ts +23 -8
  41. package/dist/flows.js +106 -52
  42. package/dist/guides.js +1 -1
  43. package/dist/integrations-ui.js +10 -3
  44. package/dist/integrations.d.ts +2 -0
  45. package/dist/integrations.js +2 -2
  46. package/dist/lead-context.d.ts +3 -9
  47. package/dist/lead-context.js +21 -5
  48. package/dist/mate-doors.js +8 -2
  49. package/dist/mate-tools.js +27 -10
  50. package/dist/mate.js +13 -2
  51. package/dist/operate.d.ts +3 -0
  52. package/dist/operate.js +122 -8
  53. package/dist/phase-routing.d.ts +86 -5
  54. package/dist/phase-routing.js +228 -16
  55. package/dist/project-skills.d.ts +10 -0
  56. package/dist/proposal.d.ts +10 -2
  57. package/dist/proposal.js +25 -10
  58. package/dist/recipe-ui.js +2 -1
  59. package/dist/result-shots.d.ts +5 -1
  60. package/dist/result-shots.js +33 -3
  61. package/dist/scout-report.d.ts +10 -4
  62. package/dist/scout-report.js +23 -6
  63. package/dist/scout.d.ts +2 -0
  64. package/dist/scout.js +84 -11
  65. package/dist/serve.d.ts +2 -0
  66. package/dist/serve.js +76 -23
  67. package/dist/store.d.ts +64 -5
  68. package/dist/store.js +192 -7
  69. package/dist/task-sizing.d.ts +78 -0
  70. package/dist/task-sizing.js +228 -0
  71. package/dist/task-text.d.ts +4 -4
  72. package/dist/task-text.js +10 -6
  73. package/dist/teammate-work.js +31 -7
  74. package/dist/teammates.d.ts +20 -1
  75. package/dist/teammates.js +18 -5
  76. package/dist/teams-chat.d.ts +4 -1
  77. package/dist/teams-chat.js +17 -6
  78. package/dist/telegram-flow.js +6 -2
  79. package/dist/telegram.js +37 -6
  80. package/dist/test-temp.js +1 -1
  81. package/dist/text-limits.d.ts +67 -0
  82. package/dist/text-limits.js +73 -0
  83. package/dist/tier-report.d.ts +44 -0
  84. package/dist/tier-report.js +71 -0
  85. package/dist/tools-ui.d.ts +1 -3
  86. package/dist/tools-ui.js +5 -4
  87. package/package.json +3 -1
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
- instructions: text(stage["instructions"], 4000),
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 under it. */
502
- export const FLOW_GOAL_LIMIT = 2000;
503
- /** What stands in for the middle of a filled-in value the goal had no room for. */
504
- export const FLOW_CUT = "… (cut; the full text is on the card)";
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
- /** A value kept to `room` characters: its start and end, with the cut mark between. */
527
- function cutToRoom(value, room) {
528
- if (value.length <= room)
529
- return value;
530
- const keep = room - FLOW_CUT.length;
531
- let head = Math.ceil(keep / 2), from = value.length - (keep - head);
532
- if (/[\uD800-\uDBFF]/.test(value[head - 1] ?? ""))
533
- head--;
534
- if (/[\uDC00-\uDFFF]/.test(value[from] ?? ""))
535
- from++;
536
- return value.slice(0, head) + FLOW_CUT + value.slice(from);
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 under FLOW_GOAL_LIMIT: the zone's own words stay whole, and only the
539
- * filled-in values are shortened (the longest first), each keeping its start and end. Null when the words alone leave no
540
- * room, which a saved flow can't have (validateFlowDefinition). */
541
- export function fitFlowText(template, card, limit = FLOW_GOAL_LIMIT) {
542
- const values = [...template.matchAll(FILLED)].map(match => goalSafe(fillOne(match[1], match[2], card)));
543
- const room = limit - 1 - template.replace(FILLED, "").length;
544
- let each = Math.max(0, ...values.map(one => one.length));
545
- const used = (cap) => values.reduce((sum, one) => sum + Math.min(one.length, cap), 0);
546
- if (used(each) > room) {
547
- // The widest cap every value can share: the shortest values stay whole, the longest are cut to it.
548
- let low = 0, high = each;
549
- while (low < high) {
550
- const mid = Math.ceil((low + high) / 2);
551
- if (used(mid) <= room)
552
- low = mid;
553
- else
554
- high = mid - 1;
555
- }
556
- each = low;
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
- let at = 0;
561
- return template.replace(FILLED, () => cutToRoom(values[at++], each)).trim();
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 short note (≤500 characters) for the
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
@@ -6,12 +6,13 @@
6
6
  */
7
7
  import { STATE_WORDS } from "./integrations.js";
8
8
  import { whenUtc } from "./when-html.js";
9
+ import { brandMarkHtml } from "./brand-mark.js";
9
10
  const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
10
11
  const when = (at) => whenUtc(at);
11
12
  export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.integrations h2{margin:24px 0 6px;font-size:.9375rem}` +
12
13
  `.integrations .integration{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:12px 0;border-top:1px solid var(--so-line);min-width:0}` +
13
14
  `.integrations .integration>:not(.integration-action),.integrations .integration-body>*{grid-column:1}.integrations .integration-body{display:contents}.integrations .integration:first-of-type{border-top:0}.integrations .integration-head{margin:0;display:flex;flex-wrap:wrap;gap:6px 10px;align-items:center;min-width:0}` +
14
- `.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
15
+ `.integrations .integration .integration-body>*{margin-left:calc(var(--brand-mark) + 10px)}.integrations .integration-head strong{overflow-wrap:anywhere}.integrations .integration p{margin:2px 0 0;min-width:0;overflow-wrap:anywhere}` +
15
16
  `.integrations .integration-action{grid-column:2;grid-row:1 / span 3;align-self:center;margin:0}.integrations .integration-action button,.integrations .integration-action .button-link{min-height:44px;white-space:nowrap}` +
16
17
  `.integrations .integration .integration-action button{width:auto}.integrations .integration .integration-action a.integration-quiet{background:var(--so-paper);color:var(--so-ink);border:1px solid var(--so-input-line)}` +
17
18
  `.integrations .integration-fix{color:var(--so-danger)}.integrations code{overflow-wrap:anywhere}.integrations details{margin-top:4px;font-size:.8125rem}.integrations summary{cursor:pointer;padding:4px 0}` +
@@ -21,7 +22,7 @@ export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.inte
21
22
  `.integration-state--not-set-up,.integration-state--checking{color:var(--so-muted);background:var(--so-neutral-soft)}` +
22
23
  // A phone: the action sits at the right of the name line, and the facts under it run on as one line.
23
24
  `@media (max-width:760px){.integrations h2{margin:16px 0 0}.integrations .integration{gap:0 12px;padding:6px 0 8px}.integrations .integration-head{min-height:44px}` +
24
- `.integrations .integration-action{grid-row:1;align-self:center}.integrations .integration-body{display:block;grid-column:1 / -1;line-height:1.35}` +
25
+ `.integrations .integration-action{grid-row:1;align-self:center}.integrations .integration-body{display:block;grid-column:1 / -1;line-height:1.35;padding-left:calc(var(--brand-mark) + 10px)}.integrations .integration .integration-body>*{margin-left:0}` +
25
26
  `.integrations .integration-body>p{margin:0}.integrations .integration-body>p.meta{display:inline}.integrations .integration-body>p.meta+p.meta::before{content:" · "}` +
26
27
  `.integrations .integration-body>:not(.meta){margin-top:4px}.integrations .integration-body>:not(.meta)+p.meta{display:block;margin-top:2px}.integrations details{margin-top:2px}}`;
27
28
  const GROUPS = [
@@ -39,6 +40,12 @@ function action(one, csrf) {
39
40
  return `<p class="integration-action"><a class="button-link${a.kind === "setup" ? " integration-quiet" : ""}" href="${e(a.href)}">${a.label}</a></p>`;
40
41
  return "";
41
42
  }
43
+ /** Which mark a row shows: email is Gmail's only when the account is a Gmail address; a custom tool is a letter. */
44
+ function markFor(one) {
45
+ if (one.custom)
46
+ return null;
47
+ return one.key === "email" && /@(gmail|googlemail)\.com$/i.test(one.account ?? "") ? "gmail" : one.key;
48
+ }
42
49
  function row(one, csrf) {
43
50
  const badge = !one.checked && one.state === "connected"
44
51
  ? `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
@@ -53,7 +60,7 @@ function row(one, csrf) {
53
60
  ? `<p class="meta">Failed ${when(one.lastErrorAt ?? "")}</p>`
54
61
  : `<details><summary>Last error ${one.lastErrorAt === null ? "" : when(one.lastErrorAt)}</summary><p class="meta">${e(one.lastError)}</p></details>`;
55
62
  return `<div class="integration" data-integration="${e(one.key)}" data-state="${one.state}">` +
56
- `<p class="integration-head"><strong>${e(one.name)}</strong> ${badge}</p>` +
63
+ `<p class="integration-head">${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<strong>${e(one.name)}</strong> ${badge}</p>` +
57
64
  action(one, csrf) +
58
65
  `<div class="integration-body">${facts === "" ? "" : `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
59
66
  }
@@ -36,6 +36,8 @@ export type Integration = {
36
36
  lastErrorAt: string | null;
37
37
  usedBy: string[];
38
38
  action: IntegrationAction;
39
+ /** A project's own MCP tool: it shows a letter, never a logo matched by name. */
40
+ custom?: true;
39
41
  };
40
42
  export declare const STATE_WORDS: Record<IntegrationState, string>;
41
43
  export type ProbeResult = {
@@ -383,7 +383,7 @@ function targets(io) {
383
383
  const href = `/settings/tools?repo=${encodeURIComponent(repo)}#tool-${encodeURIComponent(tool.name)}`;
384
384
  const test = tool.lastTest;
385
385
  list.push({
386
- key: `mcp:${repo}:${tool.name}`, group: "tools", name: tool.name, configured: true, account: basename(repo),
386
+ key: `mcp:${repo}:${tool.name}`, group: "tools", name: tool.name, configured: true, account: basename(repo), ...(tool.source === "custom" ? { custom: true } : {}),
387
387
  setup: place(href), fix: place(href), usedBy: [`Tasks in ${basename(repo)}`],
388
388
  activity: test === null ? NO_ACTIVITY : test.ok ? { okAt: test.at, error: null, errorAt: null, failing: false } : { okAt: null, error: test.problem, errorAt: test.at, failing: false },
389
389
  probe: async (check) => {
@@ -432,7 +432,7 @@ function targets(io) {
432
432
  }
433
433
  // ---------------------------------------------------------------- the list
434
434
  function combine(target, row) {
435
- const base = { key: target.key, group: target.group, name: target.name, usedBy: target.usedBy };
435
+ const base = { key: target.key, group: target.group, name: target.name, usedBy: target.usedBy, ...(target.custom ? { custom: true } : {}) };
436
436
  const checked = target.probe === null || row !== undefined;
437
437
  const lastSuccessAt = later(row?.okAt ?? null, target.activity.okAt);
438
438
  const checkError = row?.error ?? null, checkErrorAt = row?.errorAt ?? null;
@@ -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). */
@@ -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: 'Telegram on their phone: a few short lines, the most important first.',
44
- slack: 'A Slack thread: a few short lines; teammates may read it.',
45
- discord: 'A Discord thread: a few short lines; teammates may read it.',
46
- teams: 'A Microsoft Teams thread: a few short lines; teammates may read it.',
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,
@@ -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 {
@@ -1,3 +1,4 @@
1
+ import { TEXT_LIMITS } from "./text-limits.js";
1
2
  import { connectionsOf, oneClickOf } from "./mcp-connect.js";
2
3
  import { checkLevelFromWords, isCheckLevel } from "./check-levels.js";
3
4
  import { policyParts } from "./policy.js";
@@ -42,7 +43,7 @@ import { parseAcceptanceCriteria, ACCEPTANCE_LIMITS, EVIDENCE_KINDS } from "./sc
42
43
  import { diagnoseTaskDispatch, withDispatchDiagnoses } from "./dispatch.js";
43
44
  import { agentChoicesFor, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.js";
44
45
  import { isNewModel, modelWords, priceWords, runtimeStates, seenModels } from "./model-catalog.js";
45
- import { agentsSummary, chosenWords, isRiskLevel, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, specWords } from "./phase-routing.js";
46
+ import { agentsSummary, chosenWords, isRiskLevel, isTaskSize, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, sizeSourceWords, sizeWords, specWords } from "./phase-routing.js";
46
47
  import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommandLine, toolStanding } from "./project-tools.js";
47
48
  import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps, stepsFor } from "./flows.js";
48
49
  import { flowDefinitionOf } from "./flow-engine.js";
@@ -359,6 +360,7 @@ export function agentsOver(store, taskId, now) {
359
360
  const base = {
360
361
  task: taskId,
361
362
  risk: { level: risk, title: riskTitle(risk), consequence: riskConsequence(risk) },
363
+ size: ref.sizing == null ? null : { size: ref.sizing.size, risky: ref.sizing.risky, reason: ref.sizing.reason, source: sizeSourceWords(ref.sizing.source) },
362
364
  riskChoices: RISK_CHOICES.map(one => ({ risk: one.risk, title: one.title, consequence: one.consequence })),
363
365
  editable,
364
366
  editableWhy,
@@ -378,6 +380,7 @@ export function agentsOver(store, taskId, now) {
378
380
  standing: routed.source === "approved" ? "approved" : routed.source === "proposed" ? "awaiting approval" : "recommended",
379
381
  summary: agentsSummary(route),
380
382
  posture: postureWords(route),
383
+ sizeWords: sizeWords(route),
381
384
  demands: route.demands.filter(reason => !/review/i.test(reason)),
382
385
  agents: route.legs.filter(leg => leg.phase !== "review").map(leg => ({ role: ROLE_WORD[leg.phase], provider: leg.provider, model: leg.model, chosen: chosenWords(leg), reasons: leg.reasons.map(reason => reason.replace("builder and reviewer", "builder")), problem: leg.problem })),
383
386
  problems: routeProblems(route),
@@ -433,7 +436,7 @@ export const MATE_TOOLS = [
433
436
  {
434
437
  name: "propose_action",
435
438
  description: "Read get_actions and relevant skills/evidence first. Save an exact-state proposal only; protected or long terms require full secure review.",
436
- inputSchema: schema({ operation: { type: 'string', enum: Object.keys(CHAT_ACTIONS).filter(one => !one.startsWith('flow_') && !one.startsWith('teammate_')) }, repo: { type: 'string' }, task: TASK_ARG, version: { type: 'string' }, restore: { type: 'integer', minimum: 1 }, sample: { type: 'string', maxLength: 800 }, content: { type: 'string', maxLength: 12000 }, instructions: { type: 'string', maxLength: 4000 }, title: { type: 'string', maxLength: 120 }, id: { type: 'string' }, run: { type: 'integer', minimum: 1 }, note: { type: 'string', maxLength: 2000 }, catalog: { type: 'string', maxLength: 40 }, name: { type: 'string', maxLength: 40 }, command: { type: 'string', maxLength: 400 }, args: { type: 'array', items: { type: 'string', maxLength: 400 }, maxItems: 40 }, url: { type: 'string', maxLength: 500 }, secrets: { type: 'array', items: { type: 'string', maxLength: 64 }, maxItems: 12 }, about: { type: 'string', maxLength: 240 } }, ['operation']),
439
+ inputSchema: schema({ operation: { type: 'string', enum: Object.keys(CHAT_ACTIONS).filter(one => !one.startsWith('flow_') && !one.startsWith('teammate_')) }, repo: { type: 'string' }, task: TASK_ARG, version: { type: 'string' }, restore: { type: 'integer', minimum: 1 }, sample: { type: 'string', maxLength: 800 }, content: { type: 'string', maxLength: 12000 }, instructions: { type: 'string', maxLength: TEXT_LIMITS.flowInstructions }, title: { type: 'string', maxLength: 120 }, id: { type: 'string' }, run: { type: 'integer', minimum: 1 }, note: { type: 'string', maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` }, catalog: { type: 'string', maxLength: 40 }, name: { type: 'string', maxLength: 40 }, command: { type: 'string', maxLength: 400 }, args: { type: 'array', items: { type: 'string', maxLength: 400 }, maxItems: 40 }, url: { type: 'string', maxLength: 500 }, secrets: { type: 'array', items: { type: 'string', maxLength: 64 }, maxItems: 12 }, about: { type: 'string', maxLength: 240 } }, ['operation']),
437
440
  handle: (ctx, args) => {
438
441
  const operation = args['operation'];
439
442
  if (!isChatAction(operation))
@@ -894,7 +897,7 @@ export const MATE_TOOLS = [
894
897
  name: { type: "string", maxLength: 80 }, template: { type: "string", enum: FLOW_TEMPLATES.map(one => one.id) },
895
898
  steps: { type: "array", minItems: 1, maxItems: 24, items: { type: "object", additionalProperties: false, properties: {
896
899
  id: { type: "string", maxLength: 32 }, title: { type: "string", maxLength: 60 }, kind: { type: "string", enum: [...FLOW_STAGE_KINDS] },
897
- instructions: { type: "string", maxLength: 4000 }, planning: { type: "string", enum: ["auto", "required", "skip"] },
900
+ instructions: { type: "string", maxLength: TEXT_LIMITS.flowInstructions }, planning: { type: "string", enum: ["auto", "required", "skip"] },
898
901
  decider: { type: "string", maxLength: 64 }, message: { type: "string", maxLength: 1000 },
899
902
  script: { type: "string", maxLength: 40 }, close: { type: "boolean" },
900
903
  question: { type: "string", maxLength: 300 }, sureAt: { type: "integer", minimum: 50, maximum: 99 },
@@ -913,7 +916,7 @@ export const MATE_TOOLS = [
913
916
  ifReplied: { type: "string", maxLength: 60 }, repo: REPO_ARG,
914
917
  next: { type: "string", maxLength: 60 }, ifFails: { type: "string", maxLength: 60 }, ifNotSure: { type: "string", maxLength: 60 },
915
918
  } } },
916
- title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength: 2000 },
919
+ title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` },
917
920
  choice: { type: "integer", minimum: 1, maximum: 4 },
918
921
  trigger: { type: "integer", minimum: 1 }, owner: { type: "string", maxLength: 64 },
919
922
  script: { type: "object", additionalProperties: false, properties: { name: { type: "string", maxLength: 40 }, about: { type: "string", maxLength: 160 }, body: { type: "string", maxLength: 1600 }, timeoutMinutes: { type: "integer", minimum: 1, maximum: 60 },
@@ -1829,15 +1832,17 @@ export const MATE_TOOLS = [
1829
1832
  {
1830
1833
  name: "propose_steer",
1831
1834
  description: "Guide the next attempt within existing scope; never interrupt work.",
1832
- inputSchema: schema({ task: TASK_ARG, note: { type: "string", maxLength: 2_000 } }, ["task", "note"]),
1835
+ inputSchema: schema({ task: TASK_ARG, note: { type: "string", maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` } }, ["task", "note"]),
1833
1836
  handle: (ctx, args) => {
1834
1837
  const taskId = taskIdOf(args);
1835
1838
  const ref = taskId === null ? null : admittedRef(ctx, taskId);
1836
1839
  const task = taskId === null ? null : ctx.store.getTask(taskId);
1837
1840
  if (taskId === null || ref === null || task === null)
1838
1841
  return notFound();
1839
- if (!honest(args["note"], 2_000))
1840
- return { ok: false, message: "guidance is plain text up to 2,000 characters" };
1842
+ if (typeof args["note"] === "string" && args["note"].length > LIMITS.note)
1843
+ return { ok: false, message: `guidance is ${args["note"].length} characters; the limit is ${LIMITS.note}. Shorten it and propose again.` };
1844
+ if (!honest(args["note"], LIMITS.note))
1845
+ return { ok: false, message: `guidance is plain text up to ${LIMITS.note} characters` };
1841
1846
  if (task.state === "done" || task.state === "cancelled")
1842
1847
  return { ok: false, message: "that task is finished, so guidance has no next attempt to reach" };
1843
1848
  if (ctx.store.openContestFor(ref.id) !== null)
@@ -1945,10 +1950,12 @@ export const MATE_TOOLS = [
1945
1950
  },
1946
1951
  {
1947
1952
  name: "propose_agents",
1948
- description: "Read get_agents. Change risk/configured role model or clear override; stales approval, refuses running work.",
1953
+ description: "Read get_agents. Change risk, size (small: fast model, no plan; large or risky: strongest agents), configured role model, or clear override; stales approval, refuses running work.",
1949
1954
  inputSchema: schema({
1950
1955
  task: TASK_ARG,
1951
1956
  risk: { type: "string", enum: ["routine", "elevated", "high"] },
1957
+ size: { type: "string", enum: ["small", "medium", "large"] },
1958
+ risky: { type: "boolean" },
1952
1959
  role: { type: "string", enum: ["planner", "builder", "repair"] },
1953
1960
  agent: schema({ provider: { type: "string", maxLength: 20 }, model: { type: "string", maxLength: 120 } }, ["provider", "model"]),
1954
1961
  clear: { type: "boolean" },
@@ -1969,8 +1976,16 @@ export const MATE_TOOLS = [
1969
1976
  return { ok: false, message: "role is planner, builder, or repair" };
1970
1977
  const clear = args["clear"] === true;
1971
1978
  const agent = args["agent"];
1972
- if (risk === undefined && phase === null)
1973
- return { ok: false, message: "say what changes: a risk, or a role with an agent (or clear: true)" };
1979
+ const size = args["size"];
1980
+ const risky = args["risky"];
1981
+ if (size !== undefined && !isTaskSize(size))
1982
+ return { ok: false, message: "size is small, medium, or large" };
1983
+ if (risky !== undefined && typeof risky !== "boolean")
1984
+ return { ok: false, message: "risky is true or false" };
1985
+ const sized = ctx.store.refForId(ref.id)?.sizing ?? null;
1986
+ const sizeChange = size === undefined && risky === undefined ? null : { size: size ?? sized?.size ?? "medium", risky: risky ?? sized?.risky ?? false };
1987
+ if (risk === undefined && phase === null && sizeChange === null)
1988
+ return { ok: false, message: "say what changes: a risk, a size, or a role with an agent (or clear: true)" };
1974
1989
  if (phase !== null && !clear && (agent === null || typeof agent !== "object"))
1975
1990
  return { ok: false, message: "a role change names an agent from get_agents, or clear: true" };
1976
1991
  if (phase === null && (clear || agent !== undefined))
@@ -2014,6 +2029,7 @@ export const MATE_TOOLS = [
2014
2029
  taskTitle: task.title,
2015
2030
  repoId: ref.repoId,
2016
2031
  ...(risk === undefined ? {} : { risk, riskConsequence: riskConsequence(risk) }),
2032
+ ...(sizeChange === null ? {} : { size: sizeChange.size, risky: sizeChange.risky }),
2017
2033
  ...(phase === null ? {} : { phase, role: ROLE_WORD[phase] }),
2018
2034
  ...(chosen === null ? {} : { provider: chosen.provider, model: chosen.model }),
2019
2035
  ...(clear ? { clear: true } : {}),
@@ -2031,6 +2047,7 @@ export const MATE_TOOLS = [
2031
2047
  kind: "agents",
2032
2048
  task: taskId,
2033
2049
  ...(risk === undefined ? {} : { risk }),
2050
+ ...(sizeChange === null ? {} : { size: sizeChange.size, risky: sizeChange.risky }),
2034
2051
  ...(phase === null ? {} : { role: ROLE_WORD[phase], ...(chosen === null ? { clear: true } : { agent: chosen }) }),
2035
2052
  awaiting: view["approval"] === "approved" ? "the operator's confirmation — the current approval will then need renewing" : "the operator's confirmation",
2036
2053
  },
package/dist/mate.js CHANGED
@@ -9,7 +9,8 @@ import { redactSecretAssignments } from "./builder.js";
9
9
  import { MATE_CONTRACT } from "./mate-contract.js";
10
10
  import { MATE_MAX_PROPOSALS_PER_TURN, MATE_TOOL_SCHEMAS, executeMateTool, isMateTool, mateViewContextFor, projectLabelForMate, redactForMate, toolResultBytes } from "./mate-tools.js";
11
11
  import { composeSubscriptionMatePrompt, performSubscriptionMateRequest } from "./subscription-chat.js";
12
- import { leadContext } from './lead-context.js';
12
+ import { LEAD_REPLY_LIMITS, leadContext } from './lead-context.js';
13
+ import { shortenAsk } from './text-limits.js';
13
14
  import { envValue } from "./names.js";
14
15
  import { deliverableClaim, deliverableRepair, dropDeliverableClaims, replyCarriesDeliverable } from "./reply-shape.js";
15
16
  export const MATE_MESSAGE_MAX_CHARS = 2_000;
@@ -317,6 +318,9 @@ export async function runMateTurn(input) {
317
318
  let shownControl = false;
318
319
  /** The turn's one repair step for a reply that claims an attachment it does not carry. */
319
320
  let repaired = false;
321
+ /** The turn's one shorten step for a reply over its channel's message limit (LEAD_REPLY_LIMITS); still over, it is split. */
322
+ let shortened = false;
323
+ const replyLimit = input.channel === undefined ? null : LEAD_REPLY_LIMITS[input.channel];
320
324
  const unbacked = (text) => {
321
325
  const claim = deliverableClaim(text);
322
326
  if (claim === null || shownControl || replyCarriesDeliverable(text) || store.listMateTurnEvidence(turnId).length > 0)
@@ -447,7 +451,14 @@ export async function runMateTurn(input) {
447
451
  history.push({ role: "operator", text: deliverableRepair(claim) });
448
452
  continue;
449
453
  }
450
- reply = claim === null ? answer.text : dropDeliverableClaims(answer.text);
454
+ const candidate = claim === null ? answer.text : dropDeliverableClaims(answer.text);
455
+ if (replyLimit !== null && candidate.length > replyLimit && !shortened && steps < maxSteps) {
456
+ shortened = true;
457
+ history.push({ role: "assistant", text: answer.text, calls: [] });
458
+ history.push({ role: "operator", text: shortenAsk([{ field: "Your reply", limit: replyLimit, length: candidate.length }]) });
459
+ continue;
460
+ }
461
+ reply = candidate;
451
462
  break;
452
463
  }
453
464
  if (answer.calls.length > MATE_MAX_CALLS_PER_STEP)
package/dist/operate.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type IntegrationIo } from "./integrations.js";
2
2
  import { type OnboardIo } from "./agent-onboard.js";
3
+ import { type Sizer } from "./task-sizing.js";
3
4
  import { type TriggerIo } from "./flow-triggers.js";
4
5
  import type { StepIo } from "./flow-steps.js";
5
6
  import type { FetchLike } from "./flow-share.js";
@@ -83,6 +84,8 @@ export type OperateOptions = {
83
84
  env?: Record<string, string | undefined>;
84
85
  openBrowser?: (url: string) => void;
85
86
  };
87
+ /** Injected by tests: the classifier that sizes filed tasks (none by default under test). */
88
+ filingSizer?: Sizer;
86
89
  };
87
90
  export type OnboardSeams = Partial<Pick<OnboardIo, "home" | "env" | "cwd" | "interactive" | "confirm" | "findRepo" | "checkConnection" | "pullRequests">>;
88
91
  export declare const OPERATE_HELP: string;