toolroll 0.9.84 → 0.9.86

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 (132) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +94 -1193
  3. package/dist/agent-fence.d.ts +9 -5
  4. package/dist/agent-fence.js +6 -5
  5. package/dist/agent-onboard.d.ts +2 -0
  6. package/dist/agent-onboard.js +3 -0
  7. package/dist/assignment.js +4 -3
  8. package/dist/browser/workspace.css +1 -1
  9. package/dist/browser/workspace.js +33 -31
  10. package/dist/browser-workspace.d.ts +45 -0
  11. package/dist/chat-actions.d.ts +11 -1
  12. package/dist/chat-actions.js +53 -1
  13. package/dist/chat-channel.d.ts +10 -1
  14. package/dist/chat-channel.js +36 -5
  15. package/dist/chat-decide.d.ts +16 -4
  16. package/dist/chat-decide.js +239 -31
  17. package/dist/chat-messages.d.ts +5 -0
  18. package/dist/chat-messages.js +1 -1
  19. package/dist/chat-quiet.js +6 -4
  20. package/dist/chat-review.d.ts +4 -0
  21. package/dist/chat-review.js +13 -4
  22. package/dist/chat-terms.d.ts +9 -0
  23. package/dist/chat-terms.js +46 -0
  24. package/dist/claim.d.ts +3 -0
  25. package/dist/claim.js +4 -1
  26. package/dist/contracts/chat-actions.d.ts +9 -0
  27. package/dist/contracts/chat-actions.js +3 -0
  28. package/dist/contracts/console-api.d.ts +6 -2
  29. package/dist/contracts/console-api.js +6 -2
  30. package/dist/contracts/flow-propose.d.ts +4 -1
  31. package/dist/contracts/flow-propose.js +1 -1
  32. package/dist/contracts/flow.d.ts +41 -0
  33. package/dist/contracts/flow.js +4 -2
  34. package/dist/contracts/lead-tools.d.ts +51 -0
  35. package/dist/contracts/lead-tools.js +5 -0
  36. package/dist/contracts/store-json.d.ts +2 -0
  37. package/dist/contracts/trigger-payloads.d.ts +20 -0
  38. package/dist/contracts/trigger-payloads.js +4 -0
  39. package/dist/contracts/youiyoux-request.d.ts +248 -0
  40. package/dist/contracts/youiyoux-request.js +152 -0
  41. package/dist/coordinator.js +1 -0
  42. package/dist/deploy-gate.js +4 -0
  43. package/dist/dispatch.d.ts +3 -0
  44. package/dist/dispatch.js +4 -1
  45. package/dist/flow-browser.d.ts +103 -0
  46. package/dist/flow-browser.js +204 -0
  47. package/dist/flow-cadence.d.ts +8 -2
  48. package/dist/flow-cadence.js +16 -3
  49. package/dist/flow-card-task.d.ts +9 -0
  50. package/dist/flow-card-task.js +15 -6
  51. package/dist/flow-code.d.ts +5 -1
  52. package/dist/flow-code.js +4 -1
  53. package/dist/flow-engine.js +16 -0
  54. package/dist/flow-live.js +9 -1
  55. package/dist/flow-pull-request.js +15 -7
  56. package/dist/flow-share.js +3 -1
  57. package/dist/flow-steps.d.ts +9 -1
  58. package/dist/flow-steps.js +55 -5
  59. package/dist/flow-triggers.js +55 -0
  60. package/dist/flows-cli.d.ts +19 -0
  61. package/dist/flows-cli.js +47 -2
  62. package/dist/flows-ui.js +11 -2
  63. package/dist/flows.js +5 -2
  64. package/dist/integrations-ui.js +9 -4
  65. package/dist/integrations.d.ts +9 -1
  66. package/dist/integrations.js +43 -2
  67. package/dist/landed-cleanup.d.ts +4 -0
  68. package/dist/landed-cleanup.js +3 -2
  69. package/dist/lead-capabilities.js +2 -1
  70. package/dist/lead-cli.js +3 -1
  71. package/dist/lead-tools.d.ts +2 -0
  72. package/dist/lead-tools.js +65 -10
  73. package/dist/lead-voice.js +16 -5
  74. package/dist/lead.d.ts +2 -0
  75. package/dist/lead.js +3 -3
  76. package/dist/operate.d.ts +2 -3
  77. package/dist/operate.js +226 -21
  78. package/dist/project-activity.d.ts +20 -0
  79. package/dist/project-activity.js +23 -0
  80. package/dist/project-cli.d.ts +1 -0
  81. package/dist/project-cli.js +36 -19
  82. package/dist/project-delete.js +7 -3
  83. package/dist/project-knowledge.d.ts +3 -3
  84. package/dist/project-outcomes.d.ts +119 -0
  85. package/dist/project-outcomes.js +79 -0
  86. package/dist/pull-request-flow.js +35 -4
  87. package/dist/result-actions.d.ts +4 -0
  88. package/dist/result-actions.js +41 -9
  89. package/dist/result-follow-ups.js +4 -0
  90. package/dist/result-review.d.ts +9 -0
  91. package/dist/result-review.js +45 -5
  92. package/dist/scope.d.ts +5 -0
  93. package/dist/scope.js +7 -0
  94. package/dist/server/chat.js +4 -2
  95. package/dist/server/console.css +41 -0
  96. package/dist/server/flows.js +15 -0
  97. package/dist/server/pages.js +11 -1
  98. package/dist/server/render-chat.js +3 -2
  99. package/dist/server/render-pages.d.ts +15 -2
  100. package/dist/server/render-pages.js +52 -4
  101. package/dist/server/render-results.d.ts +2 -0
  102. package/dist/server/render-results.js +20 -7
  103. package/dist/server/render-tasks.d.ts +9 -0
  104. package/dist/server/render-tasks.js +32 -11
  105. package/dist/server/route-table.js +12 -0
  106. package/dist/server/settings.js +18 -1
  107. package/dist/server/task-views.js +11 -0
  108. package/dist/server/tasks.d.ts +10 -0
  109. package/dist/server/tasks.js +95 -7
  110. package/dist/settings-proofs.js +1 -0
  111. package/dist/store.d.ts +116 -5
  112. package/dist/store.js +385 -24
  113. package/dist/surface.js +18 -2
  114. package/dist/task-archive.d.ts +10 -0
  115. package/dist/task-archive.js +16 -0
  116. package/dist/task-control.d.ts +1 -1
  117. package/dist/task-source.d.ts +13 -1
  118. package/dist/task-source.js +17 -1
  119. package/dist/telegram-lead.js +7 -5
  120. package/dist/telegram.js +1 -1
  121. package/dist/tidy.d.ts +130 -0
  122. package/dist/tidy.js +503 -0
  123. package/dist/toolroll-update.js +2 -2
  124. package/dist/webhooks.d.ts +6 -0
  125. package/dist/webhooks.js +25 -0
  126. package/dist/work-index.d.ts +28 -0
  127. package/dist/work-index.js +82 -28
  128. package/dist/workspace-ui.d.ts +1 -1
  129. package/dist/workspace-ui.js +3 -2
  130. package/dist/youiyoux-intake.d.ts +234 -0
  131. package/dist/youiyoux-intake.js +908 -0
  132. package/package.json +11 -8
@@ -198,6 +198,12 @@ export type BrowserTasksView = {
198
198
  detail: string | null;
199
199
  problem: string | null;
200
200
  notes: string[];
201
+ /** Put away, or Restore in Archived (task-tidy.md): a POST to the task's own route; null for a viewer or live work. */
202
+ tidy?: {
203
+ action: string;
204
+ label: 'Put away' | 'Restore';
205
+ returnTo: string;
206
+ } | null;
201
207
  }[];
202
208
  empty: {
203
209
  text: string;
@@ -209,6 +215,8 @@ export type BrowserTasksView = {
209
215
  };
210
216
  tools: BrowserLink[];
211
217
  newTask: BrowserLink;
218
+ /** Put away old tasks (an approver's): the preview page. */
219
+ putAway?: BrowserLink | null;
212
220
  /** v105: subscription windows and monthly budgets, one tile each (an instance operator's; null when there are none). */
213
221
  limits: BrowserLimits | null;
214
222
  /** The stored-state filter (Any state, Queued, Running, Done, Failed, Cancelled), each a real link starting on page one. */
@@ -449,6 +457,15 @@ export type BrowserTaskView = {
449
457
  label: string;
450
458
  current: BrowserLink;
451
459
  } | null;
460
+ /** Put away (task-tidy.md): "Archived by tidy: <why>", when, and Restore for an approver. */
461
+ archived?: {
462
+ words: string;
463
+ at: string;
464
+ restore: {
465
+ action: string;
466
+ returnTo: string;
467
+ } | null;
468
+ } | null;
452
469
  /** The status card; `statusHtml` stands in when no assignment projection exists. */
453
470
  status: AssignmentCard | null;
454
471
  statusHtml: string;
@@ -624,6 +641,10 @@ export type BrowserProjectRow = {
624
641
  };
625
642
  export type BrowserProjectsView = {
626
643
  kind: 'projects';
644
+ activity?: {
645
+ project: string;
646
+ report: import('./project-outcomes.js').ProjectOutcomes | null;
647
+ };
627
648
  /** Arrived from New task: choosing where the task belongs. */
628
649
  choosing: boolean;
629
650
  problem: string | null;
@@ -711,6 +732,7 @@ export type BrowserCheckItem = {
711
732
  src: string;
712
733
  href: string;
713
734
  caption: string;
735
+ image?: import("./result-review.js").FeedbackImage;
714
736
  }[];
715
737
  };
716
738
  /** The result's one decision: what Accept is called, why it isn't plain
@@ -972,6 +994,8 @@ export type BrowserFlowStage = {
972
994
  }[] | undefined;
973
995
  /** A Build zone in another project: its path. */
974
996
  repo?: string | undefined;
997
+ /** v144: a script zone that starts a browser (Playwright's Chromium); it runs only once a person allows browser use for the flow. */
998
+ browser?: true | undefined;
975
999
  next: string | null;
976
1000
  onFail: string | null;
977
1001
  };
@@ -995,6 +1019,8 @@ export type BrowserFlowCard = {
995
1019
  stage: string;
996
1020
  state: "active" | "done" | "cancelled";
997
1021
  waiting: string | null;
1022
+ /** It waits because browser use isn't allowed for the flow (flow-browser.ts): the card offers Allow browser. */
1023
+ browserWait?: true;
998
1024
  task: {
999
1025
  id: string;
1000
1026
  href: string;
@@ -1289,6 +1315,25 @@ export type BrowserFlowView = {
1289
1315
  }[];
1290
1316
  schedulesDigest: string;
1291
1317
  };
1318
+ /** Browser use (flow-browser.ts browserViewOf): null when no zone says it starts a browser. */
1319
+ browser: {
1320
+ status: "allowed" | "ended" | "none";
1321
+ words: string;
1322
+ signedBy: string | null;
1323
+ signedAt: string | null;
1324
+ zones: {
1325
+ zone: string;
1326
+ title: string;
1327
+ script: string;
1328
+ version: number;
1329
+ digest: string;
1330
+ }[];
1331
+ terms: string[];
1332
+ digest: string | null;
1333
+ problem: string | null;
1334
+ /** Whether this person may allow it here: an approver whose account reaches every project (as the route requires). */
1335
+ canApprove: boolean;
1336
+ } | null;
1292
1337
  approvers: string[];
1293
1338
  /** The projects a Build zone may work in: the flow's own first, then others the viewer can reach. */
1294
1339
  projects?: {
@@ -153,6 +153,16 @@ export declare const CHAT_ACTIONS: {
153
153
  readonly protected: false;
154
154
  readonly password: false;
155
155
  };
156
+ readonly flow_browser: {
157
+ readonly label: "Allow browser";
158
+ readonly protected: true;
159
+ readonly password: true;
160
+ };
161
+ readonly flow_browser_off: {
162
+ readonly label: "Turn off browser";
163
+ readonly protected: false;
164
+ readonly password: false;
165
+ };
156
166
  readonly subagent_create: {
157
167
  readonly label: "Add subagent";
158
168
  readonly protected: false;
@@ -266,7 +276,7 @@ export declare function sharedActionReviewPath(id: number): string;
266
276
  export declare function mintSharedActionReview(store: Store, who: VerifiedApprover, id: number, root: string, now: Date): {
267
277
  payload: {
268
278
  version: 1;
269
- operation: "decision_record" | "decision_retire" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove";
279
+ operation: "decision_record" | "decision_retire" | "flow_browser" | "flow_browser_off" | "flow_card_add" | "flow_card_approve" | "flow_card_assign" | "flow_card_cancel" | "flow_card_choose" | "flow_card_comment" | "flow_card_move" | "flow_card_send_back" | "flow_card_watch" | "flow_create" | "flow_edit" | "flow_script_save" | "flow_starter" | "flow_trigger_add" | "flow_trigger_pause" | "flow_trigger_remove" | "flow_trigger_resume" | "kit_setup" | "knowledge_instructions" | "knowledge_remove" | "knowledge_restore" | "knowledge_save" | "lead_about_you" | "result_accept" | "scope_approve" | "skill_disable" | "skill_enable" | "skill_import" | "skill_restore" | "skill_test" | "subagent_answer" | "subagent_ask" | "subagent_create" | "subagent_memory" | "subagent_note" | "subagent_routine" | "subagent_soul" | "subagent_state" | "subagent_tools" | "subagent_undo" | "sweep_cancel_rest" | "sweep_file" | "task_cancel" | "task_resume" | "tool_add" | "tool_remove";
270
280
  request: {
271
281
  [x: string]: unknown;
272
282
  };
@@ -25,6 +25,7 @@ import { saveScript, scriptDigest, validateScript } from "./flow-scripts.js";
25
25
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowCardHref, flowCardText, flowDefinitionOf, moveCardInFlow } from "./flow-engine.js";
26
26
  import { chooseFlowCard, flowPersonOf } from "./flow-send.js";
27
27
  import { assignFlowCard, commentOnFlowCard, flowPeople, mentionsIn, watchFlowCard } from "./flow-people.js";
28
+ import { browserViewOf, liveBrowserGrant, revokeFlowBrowser, signFlowBrowser } from "./flow-browser.js";
28
29
  import { addFlowTriggerTo, describeTrigger, readLinearKey, removeFlowTrigger, takesDeliveries, triggerConfigOf, validateTriggerConfig } from "./flow-triggers.js";
29
30
  import { describeSchedule, parseSchedule } from "./flow-schedule.js";
30
31
  import { dirname } from "node:path";
@@ -109,6 +110,10 @@ export const CHAT_ACTIONS = {
109
110
  flow_trigger_pause: { label: "Pause trigger", protected: false, password: false },
110
111
  flow_trigger_resume: { label: "Turn trigger on", protected: false, password: false },
111
112
  flow_trigger_remove: { label: "Remove trigger", protected: false, password: false },
113
+ // Browser use widens a flow's sandbox (flow-browser.ts): the lead may only propose it; a person allows it with their
114
+ // own password on the secure screen, never on a chat card or a paired phone's tap. Turning it off lowers authority.
115
+ flow_browser: { label: "Allow browser", protected: true, password: true },
116
+ flow_browser_off: { label: "Turn off browser", protected: false, password: false },
112
117
  subagent_create: { label: "Add subagent", protected: false, password: false },
113
118
  subagent_soul: { label: "Update soul file", protected: false, password: false },
114
119
  subagent_state: { label: "Change subagent", protected: false, password: false },
@@ -710,6 +715,32 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
710
715
  terms.push("Only what happens from now on counts; nothing already there is added.");
711
716
  terms.push("Work a card starts still waits for your usual approvals.");
712
717
  }
718
+ else if (operation === "flow_browser" || operation === "flow_browser_off") {
719
+ const { flow } = flowTarget;
720
+ const view = browserViewOf(store, flow);
721
+ if (view === null)
722
+ throw Error(`No zone in ${flow.name} says it starts a browser. Mark its script zone first (edit the flow), then propose this.`);
723
+ // As on the flow page (route table: instance access), a project-limited account can't allow browser use.
724
+ if (operation === "flow_browser" && store.accountOf(who.name)?.projects !== null)
725
+ throw Error("Allowing browser use needs an account with access to every project.");
726
+ if (operation === "flow_browser_off") {
727
+ if (liveBrowserGrant(store, flow.id) === null)
728
+ throw Error(`Browser use isn't allowed for ${flow.name}.`);
729
+ state = { flow: flow.id, grant: liveBrowserGrant(store, flow.id).id };
730
+ title = `Turn off browser use in ${flow.name}`;
731
+ terms.push(`${flow.name}'s browser zones stop and wait until a person allows browser use again.`);
732
+ }
733
+ else {
734
+ if (view.status === "allowed")
735
+ throw Error(`${view.words}.`);
736
+ if (view.digest === null)
737
+ throw Error(view.problem ?? "This flow's browser zones can't be approved as they are.");
738
+ // The approval the person reads and the one their password signs: the digest is re-derived when they confirm.
739
+ state = { flow: flow.id, revision: flow.revision, digest: view.digest };
740
+ title = `Allow browser use in ${flow.name}`;
741
+ terms.push(`Flow: ${flow.name} · project ${project}`, ...view.zones.map(zone => `${zone.title} runs ${zone.script} v${zone.version}\nScript digest ${zone.digest}`), "Each run of these zones starts a headless Chromium.", ...view.terms, `Approval digest ${view.digest}`);
742
+ }
743
+ }
713
744
  else if (operation === "flow_trigger_pause" || operation === "flow_trigger_resume" || operation === "flow_trigger_remove") {
714
745
  const { flow, trigger } = flowTarget;
715
746
  const config = trigger === null ? null : triggerConfigOf(trigger);
@@ -1099,6 +1130,7 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1099
1130
  }
1100
1131
  // The review nonce was minted against the saved proposal's stamp: it is checked against that, whoever reviews.
1101
1132
  const savedStamp = payload.stamp;
1133
+ let personProved = null;
1102
1134
  payload = live;
1103
1135
  const config = CHAT_ACTIONS[live.operation], req = live.request;
1104
1136
  const challenged = options.via !== "web" &&
@@ -1113,10 +1145,14 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1113
1145
  // A scope approval proves its person inside approve() (one transaction with the seal, under the same freshness
1114
1146
  // rule as the task page); here an empty field is only looked at, never spent (an explicit re-check is one-use).
1115
1147
  const emptyApproval = payload.operation === "scope_approve" && options.review.password === "";
1148
+ const proved = config.password && !emptyApproval ? authenticateApprover(store, who.name, options.review.password, payload.repo) : null;
1149
+ // How the person was proved, for an approval that records it (browser use: flow-browser.ts refuses any but a person's).
1150
+ if (proved?.ok === true)
1151
+ personProved = proved.method;
1116
1152
  if (config.password &&
1117
1153
  (emptyApproval
1118
1154
  ? !passwordlessSessionFor(store, who.name) && !ssoNowAvailable(who.name) && withFreshness("approve-standard", () => freshnessFor(who.name)) === null
1119
- : !authenticateApprover(store, who.name, options.review.password, payload.repo).ok))
1155
+ : proved?.ok !== true))
1120
1156
  return refuse("needs-confirm", "Confirm with your password in the secure screen.");
1121
1157
  }
1122
1158
  try {
@@ -1156,6 +1192,22 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1156
1192
  ? {}
1157
1193
  : { restore: Number(req["restore"]) }),
1158
1194
  }, now);
1195
+ else if (payload.operation === "flow_browser") {
1196
+ // Only through the secure screen with the person's own password (protected + password above); signFlowBrowser
1197
+ // re-reads the flow, its zones and scripts in this transaction and refuses unless they are what the card showed.
1198
+ const flow = Number(payload.state["flow"]);
1199
+ // signFlowBrowser re-reads the account's generation and reach in its transaction (it may have narrowed since).
1200
+ const signed = signFlowBrowser(store, { flow, signer: actor, proof: personProved, sawDigest: String(payload.state["digest"] ?? ""), console: { generation: who.generation } }, now);
1201
+ if (!signed.ok)
1202
+ throw Error(signed.message);
1203
+ return { ok: true, taskId: null, said: "Browser allowed. Its browser zones can start Chromium now.", href: `/flows/${flow}` };
1204
+ }
1205
+ else if (payload.operation === "flow_browser_off") {
1206
+ const flow = Number(payload.state["flow"]);
1207
+ if (!revokeFlowBrowser(store, flow, actor, now))
1208
+ throw Error("Browser use was already off.");
1209
+ return { ok: true, taskId: null, said: "Browser turned off. Its browser zones wait until someone allows it again.", href: `/flows/${flow}` };
1210
+ }
1159
1211
  else if (payload.operation.startsWith("flow_")) {
1160
1212
  const done = runFlowAction(store, payload, actor, who.repos, options.root, now);
1161
1213
  return { ok: true, taskId: null, said: done.said, href: done.href };
@@ -1,3 +1,4 @@
1
+ import { type FeedbackImage } from "./result-review.js";
1
2
  import type { DoorOutcome } from "./lead-doors.js";
2
3
  import { type VerifiedApprover } from "./principal.js";
3
4
  import type { ChatConfig, LeadProposal, LeadSession, LeadThread, Store, SubscriptionChatProviderId } from "./store.js";
@@ -40,7 +41,7 @@ export declare function tooLongText(length: number): string;
40
41
  /** A reply to a message about several tasks is never guessed: it names them and says how to choose. */
41
42
  export declare function whichTaskText(titles: readonly string[]): string;
42
43
  /** The server-authored context a reply to a result message carries into the turn — the exact execution and run, never a guess. */
43
- export declare function replyContextFor(taskId: string, run: number | null): string;
44
+ export declare function replyContextFor(taskId: string, run: number | null, image?: FeedbackImage): string;
44
45
  /** The context a chosen task (/tasks, /task <name>) carries into each turn from that chat. */
45
46
  export declare function focusContextFor(taskId: string): string;
46
47
  /** No trusted https origin is configured: one honest line, no localhost, no promise. */
@@ -69,6 +70,14 @@ export declare function armedCardText(proposal: LeadProposal, previewText: strin
69
70
  /** The "yes" button's label for an armed card. */
70
71
  export declare function armedYesLabel(proposal: LeadProposal): string;
71
72
  export declare function proposalLink(store: Store, proposal: LeadProposal, repos: readonly string[], channel?: ChatChannelName | null): PhoneLink | null;
73
+ /** The personal Telegram card: the shared preview, or a handoff to the lead conversation when it cannot be shown in full. */
74
+ export declare function phoneProposalPreview(store: Store, proposal: LeadProposal, repos: readonly string[]): {
75
+ text: string;
76
+ buttons: boolean;
77
+ closed?: true;
78
+ };
79
+ /** The personal Telegram card's link: the lead conversation holding a card too long to show, else the shared link. */
80
+ export declare function phoneProposalLink(store: Store, proposal: LeadProposal, repos: readonly string[], channel?: ChatChannelName | null): PhoneLink | null;
72
81
  /**
73
82
  * The link a CONFIRMED card carries: the exact recorded task the outcome
74
83
  * names, where its remaining step lives — the approval control while the
@@ -1,6 +1,7 @@
1
+ import { parseFeedbackImage } from "./result-review.js";
1
2
  /** Shared membership-chat identity, proposal previews and result links. Transport adapters never grant authority. */
2
3
  import { FLOW_HREF } from "./flow-engine.js";
3
- import { closedQuestionWords } from "./decision.js";
4
+ import { closedQuestionWords, LIMITS } from "./decision.js";
4
5
  import { createHash } from "node:crypto";
5
6
  import { sharedActionPayload, sharedActionNeedsReview, sharedActionReviewPath, sharedActionAllowsChallenge, CHAT_ACTIONS, } from "./chat-actions.js";
6
7
  import { isDirectChatProvider, subscriptionCredentialKey } from "./converse.js";
@@ -132,12 +133,13 @@ export function whichTaskText(titles) {
132
133
  ].join("\n");
133
134
  }
134
135
  /** The server-authored context a reply to a result message carries into the turn — the exact execution and run, never a guess. */
135
- export function replyContextFor(taskId, run) {
136
+ export function replyContextFor(taskId, run, image) {
136
137
  return [
137
138
  `Current task: ${taskId}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.`,
138
139
  run === null
139
140
  ? `The operator is replying to a message about execution ${taskId}.`
140
141
  : `The operator is replying to result #${run} from execution ${taskId}. Use get_result for that exact execution and run when responding to feedback; do not substitute another result. If the operator asks for changes, use propose_review revise; use note only when they explicitly ask to save feedback without starting work.`,
142
+ ...(image === undefined ? [] : [`The reply targets screenshot ${image.artifact} on result #${image.run}. Retain image: ${image.artifact} in propose_review so the exact screenshot stays with new feedback; pass image: null only when the feedback is not about it. Read get_result_images for this result first.`]),
141
143
  ].join(" ");
142
144
  }
143
145
  /** The context a chosen task (/tasks, /task <name>) carries into each turn from that chat. */
@@ -146,6 +148,10 @@ export function focusContextFor(taskId) {
146
148
  }
147
149
  /** A step the phone cannot take itself: said once; the button (or its absence) says where. */
148
150
  const HANDOFF = "This step finishes in Toolroll.";
151
+ /** One Telegram message; a card that cannot fit is never confirmed from a shortened copy. */
152
+ const TELEGRAM_TEXT_MAX = 4_096;
153
+ const PHONE_TOO_LONG = "This is too long to show in full here. It is waiting in your Toolroll chat to review and confirm.";
154
+ const LEAD_CHAT_LINK = { label: "Open chat", path: "/chat" };
149
155
  /** No trusted https origin is configured: one honest line, no localhost, no promise. */
150
156
  export const NO_PHONE_LINK = "Phone access isn't configured. Open Toolroll on your computer.";
151
157
  /** An origin exists but the card's task is not one this phone may reach now: said as that, never as missing setup. */
@@ -229,6 +235,23 @@ export function proposalLink(store, proposal, repos, channel = null) {
229
235
  }
230
236
  return null;
231
237
  }
238
+ /** A personal Telegram card fits one message, or it waits, whole, in the person's lead conversation in Toolroll. */
239
+ function fitsPhone(preview) {
240
+ return !preview.buttons || preview.text.length <= TELEGRAM_TEXT_MAX;
241
+ }
242
+ /** The personal Telegram card: the shared preview, or a handoff to the lead conversation when it cannot be shown in full. */
243
+ export function phoneProposalPreview(store, proposal, repos) {
244
+ const preview = proposalPreview(store, proposal, repos, "telegram");
245
+ return fitsPhone(preview)
246
+ ? preview
247
+ : { text: [preview.text.split("\n")[0], PHONE_TOO_LONG, "", HANDOFF].join("\n"), buttons: false };
248
+ }
249
+ /** The personal Telegram card's link: the lead conversation holding a card too long to show, else the shared link. */
250
+ export function phoneProposalLink(store, proposal, repos, channel = null) {
251
+ return fitsPhone(proposalPreview(store, proposal, repos, "telegram"))
252
+ ? proposalLink(store, proposal, repos, channel)
253
+ : LEAD_CHAT_LINK;
254
+ }
232
255
  /**
233
256
  * The link a CONFIRMED card carries: the exact recorded task the outcome
234
257
  * names, where its remaining step lives — the approval control while the
@@ -410,20 +433,22 @@ export function proposalPreview(store, proposal, repos, channel = null) {
410
433
  ? payload["notes"]
411
434
  : [];
412
435
  const saved = (snapshot?.notes ?? []).filter((one) => selected.includes(one.id));
413
- const note = t("note", 1_000);
436
+ const note = t("note", LIMITS.note);
437
+ const image = parseFeedbackImage(payload["image"]);
414
438
  const path = t("path", 300);
415
439
  return card(`${revise ? "Request changes to" : "Save feedback on"} result #${String(payload["run"] ?? "?")} of ${taskName}`, [
416
440
  ...(note === ""
417
441
  ? []
418
442
  : [
419
443
  `Your feedback${path === "" ? "" : ` (${path}${typeof payload["line"] === "number" ? `:${payload["line"]}` : ""})`}:`,
420
- ...lines(note, 1_000),
444
+ ...(image ? [`Screenshot ${image.artifact} · result #${image.run}`] : []),
445
+ ...lines(note, LIMITS.note),
421
446
  ]),
422
447
  ...(saved.length === 0
423
448
  ? []
424
449
  : [
425
450
  "Saved notes included:",
426
- ...saved.map((one) => `• ${one.path === null ? "" : `${phoneText(one.path, 120)}${one.line === null ? "" : `:${one.line}`}: `}${phoneText(one.note, 300)}`),
451
+ ...saved.map((one) => `• ${one.image ? `Screenshot ${one.image.artifact} · result #${one.image.run}: ` : ""}${one.path === null ? "" : `${phoneText(one.path, 120)}${one.line === null ? "" : `:${one.line}`}: `}${phoneText(one.note, LIMITS.note)}`),
427
452
  ]),
428
453
  ], revise
429
454
  ? "Creates a revision of the same task. Its approval follows your settings."
@@ -484,6 +509,11 @@ export const CHAT_ACTION_PARITY = {
484
509
  how: "Reads current tasks, decisions, results and project knowledge from the local database through the shared engine, within the enrolled projects.",
485
510
  gap: "Dedicated Telegram and Slack journeys for this tool and live channel rendering remain unverified.",
486
511
  },
512
+ get_project_activity: {
513
+ support: "direct",
514
+ how: "Reads the project's Ready backlog, finished-to-decision elapsed time, explicit revisions and recorded cost for completed families in an accessible enrolled project through the shared engine.",
515
+ gap: "Dedicated Telegram and Slack journeys for this tool and live channel rendering remain unverified.",
516
+ },
487
517
  get_project_context: {
488
518
  support: "direct",
489
519
  how: "Reads bounded source excerpts or advisory import impact in an accessible enrolled project through the shared engine, with source-search fallback when its index is unavailable.",
@@ -548,6 +578,7 @@ export const CHAT_ACTION_PARITY = {
548
578
  ask_owner: { support: "direct", how: "One question with 2-4 options and Something else, drawn as buttons under the reply; the tapped option is sent as the person's next message.", gap: null },
549
579
  search_project_memory: { support: "direct", how: "Read during a turn: one search over decisions, references, lessons and the conversations the person may read.", gap: null },
550
580
  list_tasks: { support: "direct", how: "Read during a turn.", gap: null },
581
+ tidy_preview: { support: "direct", how: "Read during a turn: what Put away old tasks would put away now and why. Only a person puts tasks away, on the console or with the CLI command it names.", gap: null },
551
582
  get_task: {
552
583
  support: "direct",
553
584
  how: "Read during a turn; a reply to a result message pins the exact execution.",
@@ -4,6 +4,8 @@ import { type VerifiedApprover } from "./principal.js";
4
4
  import type { Scope } from "./scope.js";
5
5
  import { type ReleaseCard } from "./release-card.js";
6
6
  import type { Store } from "./store.js";
7
+ import { permissionPlainWords } from "./chat-terms.js";
8
+ export { permissionPlainWords };
7
9
  /** The tokens live in chat_decide_action and the open "What should change?" in chat_decide_prompt (store.ts). */
8
10
  export type DecideAct = (typeof DECIDE_ACTS)[number];
9
11
  export type DecideTarget = {
@@ -20,6 +22,11 @@ export type DecideTarget = {
20
22
  kind: "merge";
21
23
  taskId: string;
22
24
  run: number;
25
+ }
26
+ /** The tasks one youiyoux pass filed (youiyoux-intake.ts), named by any one of them. */
27
+ | {
28
+ kind: "requests";
29
+ taskId: string;
23
30
  };
24
31
  export type DecideChannel = "telegram" | "slack" | "discord" | "teams";
25
32
  /** Who is tapping, where: one paired person's binding on one channel, and the chat the card is in. */
@@ -56,11 +63,13 @@ export declare const DECIDE_LABELS: {
56
63
  readonly cancel: "Cancel";
57
64
  };
58
65
  export declare const WHAT_SHOULD_CHANGE = "What should change?";
66
+ /** `taskId`: the task the button acts on, when it isn't the card's own (a youiyoux card's one-task Approve). */
59
67
  type ActSpec = {
60
68
  act: DecideAct;
61
69
  run: number | null;
62
70
  digest: string;
63
71
  label?: string;
72
+ taskId?: string;
64
73
  };
65
74
  /** What a card offers: an optional replacement text (the plan card) and rows of acts and links; a release card also
66
75
  * sends its full terms as a file with it (`file`), where the chat app can. */
@@ -85,11 +94,11 @@ export declare function chatApproveMode(store: Store, repo: string | null, appro
85
94
  ok: false;
86
95
  why: string;
87
96
  };
88
- /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
89
- export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
90
97
  /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
91
98
  * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
92
- export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, channel: DecideChannel, root?: string): {
99
+ export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, channel: DecideChannel, root?: string,
100
+ /** A youiyoux card asking (requestsOffer): its own words are checked there, every one shown. */
101
+ from?: "requests"): {
93
102
  ok: true;
94
103
  modeDigest: string;
95
104
  source: "setting" | "mode";
@@ -109,6 +118,10 @@ export declare function mergeInChat(store: Store, taskId: string, run: number, a
109
118
  ok: false;
110
119
  why: string;
111
120
  };
121
+ /** Whether a card's words arrive as one message that shows each of them as written: within Telegram's part, Slack's and
122
+ * (after its escapes) Discord's limits, with room for the question a tap adds. Teams draws card text as Markdown, where
123
+ * a link's label hides its address, and Slack turns `:name:` into an emoji: there such words never approve. */
124
+ export declare function shownWhole(text: string, channel: DecideChannel): boolean;
112
125
  /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
113
126
  * terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
114
127
  export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string, channel?: DecideChannel): string;
@@ -260,4 +273,3 @@ export declare function decideTargetOf(row: {
260
273
  taskId: string | null;
261
274
  run: number | null;
262
275
  }): DecideTarget | null;
263
- export {};