toolroll 0.9.102 → 0.9.104

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 (62) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +10 -8
  3. package/dist/assignment.d.ts +32 -0
  4. package/dist/assignment.js +140 -4
  5. package/dist/browser/workspace.js +25 -25
  6. package/dist/browser-workspace.d.ts +21 -5
  7. package/dist/contracts/console-api.d.ts +3 -2
  8. package/dist/contracts/console-api.js +4 -2
  9. package/dist/desktop-update.d.ts +2 -2
  10. package/dist/flow-engine.d.ts +3 -0
  11. package/dist/flow-engine.js +4 -1
  12. package/dist/flows-ui.d.ts +31 -1
  13. package/dist/flows-ui.js +40 -12
  14. package/dist/keymap.d.ts +3 -1
  15. package/dist/keymap.js +3 -0
  16. package/dist/local-owner-acts.d.ts +83 -0
  17. package/dist/local-owner-acts.js +221 -0
  18. package/dist/local-owner.d.ts +12 -1
  19. package/dist/local-owner.js +26 -0
  20. package/dist/operate-remote-arguments.js +4 -2
  21. package/dist/operate.js +76 -1
  22. package/dist/plan-auto.js +5 -0
  23. package/dist/project-knowledge.d.ts +6 -6
  24. package/dist/public-access.d.ts +9 -0
  25. package/dist/public-access.js +15 -0
  26. package/dist/pull-request-flow.js +9 -0
  27. package/dist/release-approval.d.ts +9 -20
  28. package/dist/release-approval.js +14 -53
  29. package/dist/release-card.d.ts +15 -1
  30. package/dist/release-card.js +35 -12
  31. package/dist/release-refusal.d.ts +32 -0
  32. package/dist/release-refusal.js +73 -0
  33. package/dist/result-completion.d.ts +6 -2
  34. package/dist/result-completion.js +13 -7
  35. package/dist/scope.d.ts +32 -1
  36. package/dist/scope.js +116 -25
  37. package/dist/serve.js +13 -14
  38. package/dist/server/guards.d.ts +1 -0
  39. package/dist/server/guards.js +11 -1
  40. package/dist/server/home-asks.d.ts +45 -0
  41. package/dist/server/home-asks.js +108 -0
  42. package/dist/server/home.d.ts +15 -5
  43. package/dist/server/home.js +53 -18
  44. package/dist/server/pages.js +15 -3
  45. package/dist/server/remote.d.ts +1 -0
  46. package/dist/server/render-results.d.ts +4 -0
  47. package/dist/server/render-tasks.d.ts +9 -0
  48. package/dist/server/render-tasks.js +4 -4
  49. package/dist/server/route-table.js +4 -0
  50. package/dist/server/runtime.d.ts +1 -0
  51. package/dist/server/task-views.js +3 -0
  52. package/dist/server/tasks.d.ts +25 -0
  53. package/dist/server/tasks.js +46 -6
  54. package/dist/surface.js +12 -2
  55. package/dist/task-outcome-cli.d.ts +18 -0
  56. package/dist/task-outcome-cli.js +60 -2
  57. package/dist/tui-contract.d.ts +61 -3
  58. package/dist/tui-source.js +91 -20
  59. package/dist/tui.js +54 -54
  60. package/dist/work-index.d.ts +10 -5
  61. package/dist/work-index.js +23 -13
  62. package/package.json +2 -2
@@ -392,8 +392,13 @@ export type BrowserTaskView = {
392
392
  run: number;
393
393
  } | null;
394
394
  /** Build again, in place, when the status asks for it (a result built to an earlier plan). */
395
+ /** `pins`: the build and approved plan the page read, which a local owner's Retry binds to (local-owner slice 3). */
395
396
  rebuild?: {
396
397
  action: string;
398
+ pins?: {
399
+ run: number;
400
+ digest: string;
401
+ } | null;
397
402
  } | null;
398
403
  /** What a failed task missed, in one line (the requirement it missed, the failing check's error line, or the stop
399
404
  * reason), the evidence line behind it, one suggestion of what to change, and where to see it (its build's result
@@ -402,6 +407,10 @@ export type BrowserTaskView = {
402
407
  retry?: {
403
408
  action: string;
404
409
  note?: string;
410
+ pins?: {
411
+ run: number;
412
+ digest: string;
413
+ } | null;
405
414
  } | null;
406
415
  /** Run checks in place, on a status row that offers it: posts and comes back to this page. */
407
416
  runChecks?: BrowserRunChecks | null;
@@ -867,6 +876,10 @@ export type BrowserResultView = {
867
876
  retry: {
868
877
  action: string;
869
878
  note: string;
879
+ pins?: {
880
+ run: number;
881
+ digest: string;
882
+ } | null;
870
883
  } | null;
871
884
  /** A failed task's delivered result, accepted only in outline with a reason (the task's accept-proof). */
872
885
  acceptAnyway?: {
@@ -1725,8 +1738,9 @@ export type BrowserHomeView = {
1725
1738
  value: number;
1726
1739
  href: string;
1727
1740
  }[];
1728
- /** What needs you, by what is asked, in this order: approve, answer, wrong, review, other (anything none of the four
1729
- * fits). `count` is the exact total for that ask, past the rows listed. */
1741
+ /** What needs you, by what is asked, in this order: release, approve, answer, flow, wrong, review, setup, other
1742
+ * (anything none of the others fits; docs/plans/home-other-asks.md). `count` is the exact total for that ask, past the
1743
+ * rows listed. */
1730
1744
  groups: {
1731
1745
  key: BrowserHomeAsk;
1732
1746
  label: string;
@@ -1761,9 +1775,10 @@ export type BrowserHomeView = {
1761
1775
  /** While the first run is open (no first result yet): the welcome's address, linked as Finish setup. */
1762
1776
  setup?: string | null;
1763
1777
  };
1764
- /** What a waiting row asks (work-index.ts WorkAskGroup). */
1765
- export type BrowserHomeAsk = 'approve' | 'answer' | 'wrong' | 'review' | 'other';
1778
+ /** What a waiting row asks (work-index.ts WorkAskGroup), plus a flow card waiting on you and a first-run step. */
1779
+ export type BrowserHomeAsk = 'release' | 'approve' | 'answer' | 'flow' | 'wrong' | 'review' | 'setup' | 'other';
1766
1780
  export type BrowserHomeRow = {
1781
+ /** A task row's family id; a row that isn't a task is namespaced (`release:<task>`, `flow:<card>`, `setup:<step>`). */
1767
1782
  id: string;
1768
1783
  title: string;
1769
1784
  href: string;
@@ -1777,7 +1792,8 @@ export type BrowserHomeRow = {
1777
1792
  reason: string | null;
1778
1793
  /** One line: what it asks, or what the agent is doing now. */
1779
1794
  line: string | null;
1780
- /** The one act that resolves it (a link), or null. */
1795
+ /** The one act that resolves it (a link), or null. A release, a flow card or a setup step is never acted on at
1796
+ * Home: its one link opens its own page. */
1781
1797
  action: {
1782
1798
  label: string;
1783
1799
  href: string;
@@ -202,7 +202,8 @@ export declare const CONSOLE_FORMS: {
202
202
  readonly taskRepairDependency: FormContract<"blocker" | "operation" | "replacement">;
203
203
  readonly taskNext: FormContract<"undo">;
204
204
  readonly taskReopen: FormContract<"token">;
205
- readonly taskRequeue: FormContract<"note" | "return">;
205
+ readonly taskRequeue: FormContract<"digest" | "note" | "return" | "run">;
206
+ readonly taskPlan: FormContract<"source">;
206
207
  readonly taskFollowUp: FormContract<"index">;
207
208
  readonly taskPlanEdit: FormContract<"plan-document" | "saw-plan">;
208
209
  readonly taskRevision: FormContract<"revision-id" | "token">;
@@ -218,7 +219,7 @@ export declare const CONSOLE_FORMS: {
218
219
  };
219
220
  export type ConsoleFormName = keyof typeof CONSOLE_FORMS;
220
221
  /** The console POSTs whose handlers read no field (sign-out not even the shared guard): their contract is the empty form. */
221
- export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/plan", "POST /t/<task>/retry-review", "POST /settings/updates/check", "POST /settings/storage/check", "POST /projects/github", "POST /code/<session>/ship/preview"];
222
+ export declare const BODILESS_POSTS: readonly ["POST /settings/models/<unknown action>", "POST /r/<run>/draft-repair", "POST /ledger/checkpoint", "POST /settings/backups/now", "POST /mode/revoke", "POST /settings/telegram/retry", "POST /providers/<provider>/resume", "POST /logout", "POST /t/<task>/unhold", "POST /t/<task>/stop-carrying", "POST /t/<task>/retry-review", "POST /settings/updates/check", "POST /settings/storage/check", "POST /projects/github", "POST /code/<session>/ship/preview"];
222
223
  /** The field names a console form's handler may read. */
223
224
  export type FormFieldOf<N extends ConsoleFormName> = (typeof CONSOLE_FORMS)[N] extends FormContract<infer K> ? K : never;
224
225
  /** `GET /ledger?format=json`: the chain's state, a page of entries (newest first) and the cursor for the next. */
@@ -220,7 +220,10 @@ export const CONSOLE_FORMS = {
220
220
  taskRepairDependency: formContract("POST /t/<task>/repair-dependency", ["blocker", "operation", "replacement"]),
221
221
  taskNext: formContract("POST /t/<task>/next", ["undo"]),
222
222
  taskReopen: formContract("POST /t/<task>/reopen", ["token"]),
223
- taskRequeue: formContract("POST /t/<task>/requeue", ["note", "return"]),
223
+ // Local-owner slice 3: the build and approved plan the page read (`run`, `digest`), read only for the laptop's local owner.
224
+ taskRequeue: formContract("POST /t/<task>/requeue", ["note", "return", "run", "digest"]),
225
+ // Local-owner slice 2: the planning source the page read (planner-source.ts), read only for the laptop's local owner.
226
+ taskPlan: formContract("POST /t/<task>/plan", ["source"]),
224
227
  taskFollowUp: formContract("POST /t/<task>/follow-up", ["index"]),
225
228
  taskPlanEdit: formContract("POST /t/<task>/plan-edit", ["saw-plan", "plan-document"]),
226
229
  taskRevision: formContract("POST /t/<task>/accept-revision|reject-revision", ["revision-id", "token"]),
@@ -248,7 +251,6 @@ export const BODILESS_POSTS = [
248
251
  "POST /logout",
249
252
  "POST /t/<task>/unhold",
250
253
  "POST /t/<task>/stop-carrying",
251
- "POST /t/<task>/plan",
252
254
  "POST /t/<task>/retry-review",
253
255
  // Check now and its kin (docs/plans/get-side-effects.md): the form token only, no fields read.
254
256
  "POST /settings/updates/check",
@@ -97,20 +97,20 @@ export declare function sqliteLock(file: string, busyTimeoutMs?: number): Databa
97
97
  export declare function lockHeld(file: string): boolean;
98
98
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
99
99
  export declare function desktopUpdateStatus(stateDir: string): {
100
- error?: never;
100
+ version?: never;
101
101
  active: boolean;
102
102
  phase: string;
103
103
  detail: string;
104
104
  running: boolean;
105
105
  canResume: boolean;
106
106
  canCancel: boolean;
107
+ error?: never;
107
108
  automaticRecovery?: never;
108
109
  recoveryAttempts?: never;
109
110
  recoveryRepairs?: never;
110
111
  wasRunning?: never;
111
112
  backupPath?: never;
112
113
  updateId?: never;
113
- version?: never;
114
114
  buildId?: never;
115
115
  workDir?: never;
116
116
  } | {
@@ -9,6 +9,9 @@ export type FlowAdvance = {
9
9
  export declare function flowDefinitionOf(flow: FlowRow): FlowDefinition | null;
10
10
  /** Where a card's link opens: the flow's canvas with the card selected. */
11
11
  export declare const flowCardHref: (flow: number, card: number) => string;
12
+ /** A reply wait that timed out with nowhere to send the card asks its owner (or the flow's owner) to move it on: the
13
+ * card's waiting words end with this (flows-ui.ts flowCardWaitsOn reads it). */
14
+ export declare const NO_REPLY_MOVE_ON = "Move the card on when you're ready.";
12
15
  /** Where a confirmed flow action may point: a flow's canvas, maybe with one card open. */
13
16
  export declare const FLOW_HREF: RegExp;
14
17
  /** Advance every active card in one project's flows. */
@@ -36,6 +36,9 @@ export function flowDefinitionOf(flow) {
36
36
  }
37
37
  /** Where a card's link opens: the flow's canvas with the card selected. */
38
38
  export const flowCardHref = (flow, card) => `/flows/${flow}?card=${card}`;
39
+ /** A reply wait that timed out with nowhere to send the card asks its owner (or the flow's owner) to move it on: the
40
+ * card's waiting words end with this (flows-ui.ts flowCardWaitsOn reads it). */
41
+ export const NO_REPLY_MOVE_ON = "Move the card on when you're ready.";
39
42
  /** Where a confirmed flow action may point: a flow's canvas, maybe with one card open. */
40
43
  export const FLOW_HREF = /^\/flows\/[1-9][0-9]{0,9}(\?card=[1-9][0-9]{0,9})?$/;
41
44
  const ACCEPTANCE = FLOW_ACCEPTANCE;
@@ -173,7 +176,7 @@ function advanceCard(store, flow, definition, card, now, options, outcome) {
173
176
  outcome.moved++;
174
177
  return;
175
178
  }
176
- const waiting = `No reply after ${words}. Move the card on when you're ready.`;
179
+ const waiting = `No reply after ${words}. ${NO_REPLY_MOVE_ON}`;
177
180
  if (card.waiting !== waiting) {
178
181
  store.updateFlowCard(card.id, { waiting }, now);
179
182
  notifyPeople(store, card, [card.owner ?? flow.owner], null, { key: `no-reply:${card.entry}`, subject: `No reply to “${card.title}” after ${words}`, body: `It's waiting in ${stage.title}.`, attention: true }, now);
@@ -1,5 +1,6 @@
1
1
  import type { BrowserFlowView } from "./browser-workspace.js";
2
- import type { FlowRow, Store } from "./store.js";
2
+ import { type FlowStage } from "./flows.js";
3
+ import type { FlowCardRow, FlowRow, Store, SubagentQuestionRow } from "./store.js";
3
4
  import { type FlowFile, type FlowImportPlan } from "./flow-share.js";
4
5
  import { type Html } from "./html.js";
5
6
  /** The flow page's fallback rules, plus the Flows list and the import preview (kit pieces; only layout here). */
@@ -24,6 +25,35 @@ export declare function flowImportHtml(input: {
24
25
  previewed: string;
25
26
  problem: string | null;
26
27
  }): Html;
28
+ /** Why a card waits on this person now (docs/plans/home-other-asks.md part 4, v2.3): the one rule the flow page's
29
+ * Decide and Home's Flow decisions both read, so they never disagree. Each needs approver standing:
30
+ * - `decide`: an approval zone it is theirs to decide (the zone's decider, the flow's owner for "to owner", any approver
31
+ * when none is named), and no subagent of the zone deciding it first;
32
+ * - `choose`: a "Person chooses" zone waiting on them (flow-send.ts flowChoiceAt);
33
+ * - `answer`: its subagent's open question or tool-call request asked of them;
34
+ * - `browser`: a check zone waiting for browser use, which only an approver with every project may allow;
35
+ * - `reply`: a reply wait that ran out with nowhere to go, asking the card's owner (else the flow's) to move it on.
36
+ * `reads` passes what the caller already read (a missing key is read here). */
37
+ export type FlowCardWait = {
38
+ decide: boolean;
39
+ choose: boolean;
40
+ answer: boolean;
41
+ browser: boolean;
42
+ reply: boolean;
43
+ };
44
+ export declare function flowCardWaitsOn(store: Store, flow: FlowRow, card: FlowCardRow, viewer: {
45
+ name: string;
46
+ approver: boolean;
47
+ }, reads?: {
48
+ stage?: FlowStage | undefined;
49
+ asked?: SubagentQuestionRow | null;
50
+ choosing?: {
51
+ person: string;
52
+ } | null;
53
+ browser?: {
54
+ status: string;
55
+ } | null;
56
+ }): FlowCardWait;
27
57
  /** One flow's canvas for one person. */
28
58
  export declare function flowView(store: Store, flow: FlowRow, viewer: {
29
59
  name: string;
package/dist/flows-ui.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /** Flows in the console: the list of a person's flows, and one flow's canvas
2
2
  * (the React view's data, plus a plain fallback page the view replaces). */
3
3
  import { liveAttemptOf, steerControlsFor } from "./steer-controls.js";
4
- import { draftFor, flowDefinitionOf } from "./flow-engine.js";
4
+ import { draftFor, flowDefinitionOf, NO_REPLY_MOVE_ON } from "./flow-engine.js";
5
5
  import { describeStandingOrder } from "./flow-schedule.js";
6
6
  import { describeTrigger, triggerHeadline, FLOW_TRIGGER_KINDS, FLOW_TRIGGER_WORDS, githubRepoOf, HOOK_PATH, hookReady, planeStatusLink, readHooksBase, readLinearKey, senderOf, takesDeliveries, triggerConfigOf } from "./flow-triggers.js";
7
7
  import { SENDER_WORDS } from "./webhook-signing.js";
@@ -14,7 +14,7 @@ import { parseSortDecision, sortChip } from "./flow-sort.js";
14
14
  import { flowSecretNames, readEmailSettings, sendingReady } from "./flow-actions.js";
15
15
  import { projectToolsOf, secretsSetFor, toolStanding } from "./project-tools.js";
16
16
  import { flowFingerprint } from "./flow-live.js";
17
- import { flowChoiceAt, readFlowSend } from "./flow-send.js";
17
+ import { flowChoiceAt, flowPersonOf, readFlowSend } from "./flow-send.js";
18
18
  import { itemCaption, whyLines } from "./flow-items.js";
19
19
  import { FLOW_FILE_MAX_BYTES } from "./flow-share.js";
20
20
  import { googleConnected } from "./google-mail.js";
@@ -224,6 +224,37 @@ const historyText = (event, title, sorts = new Set()) => {
224
224
  default: return `Moved to ${title(event.toStage)}${who}${note}`;
225
225
  }
226
226
  };
227
+ export function flowCardWaitsOn(store, flow, card, viewer, reads = {}) {
228
+ const none = { decide: false, choose: false, answer: false, browser: false, reply: false };
229
+ if (!viewer.approver || card.state !== "active")
230
+ return none;
231
+ const stage = "stage" in reads ? reads.stage : flowDefinitionOf(flow)?.stages.find(one => one.id === card.stage);
232
+ // Its subagent's open question is the person's to answer whatever zone it is in, even one since removed from the flow
233
+ // (the flow page shows it and takes the answer).
234
+ const asked = "asked" in reads ? reads.asked ?? null : store.openSubagentQuestionOn(card.id, card.entry);
235
+ const answer = asked !== null && asked.state === "open" && asked.askedOf === viewer.name && store.getSubagent(asked.subagent) !== null;
236
+ if (stage === undefined)
237
+ return { ...none, answer };
238
+ const decider = stage.kind === "approval" ? deciderOf(stage, flow) : null;
239
+ // While its subagent is deciding (v92), the card isn't anyone else's to decide yet.
240
+ const subagentDeciding = stage.subagent !== undefined && card.waiting !== null && card.waiting.endsWith(" is deciding");
241
+ const choosing = stage.kind !== "choose" ? null : "choosing" in reads ? reads.choosing ?? null : flowChoiceAt(store, card.id, card.entry);
242
+ const browser = stage.kind !== "check" || stage.browser !== true ? null : "browser" in reads ? reads.browser ?? null : browserViewOf(store, flow);
243
+ const wait = stage.wait ?? { for: "reply" };
244
+ return {
245
+ decide: stage.kind === "approval" && (decider === null || decider === viewer.name) && !subagentDeciding,
246
+ choose: choosing !== null && choosing.person === viewer.name,
247
+ answer,
248
+ browser: browserWaitOn(card, stage, browser) && store.accountOf(viewer.name)?.projects === null,
249
+ reply: stage.kind === "wait" && wait.for === "reply" && card.waiting !== null && card.waiting.startsWith("No reply after ") && card.waiting.endsWith(NO_REPLY_MOVE_ON)
250
+ && flowPersonOf(card, flow) === viewer.name,
251
+ };
252
+ }
253
+ /** A check zone's card waiting for browser use (flow-browser.ts): the flow's browser isn't allowed and the card says so. */
254
+ function browserWaitOn(card, stage, browser) {
255
+ return card.state === "active" && stage?.kind === "check" && stage.browser === true && browser !== null && browser.status !== "allowed"
256
+ && card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
257
+ }
227
258
  /** One flow's canvas for one person. */
228
259
  export function flowView(store, flow, viewer, selectedCard, setup = { dir: null, repos: [] }) {
229
260
  const definition = flowDefinitionOf(flow);
@@ -249,10 +280,12 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
249
280
  const canReadTask = taskRepo !== null && (taskRepo === flow.repo || (setup.repos.includes(taskRepo) && store.accountCanAccess(viewer.name, taskRepo)));
250
281
  const discussion = store.flowComments(card.id);
251
282
  const watchers = store.flowCardWatchers(card.id);
252
- const decider = stage?.kind === "approval" ? deciderOf(stage, flow) : null;
253
- // While its subagent is deciding (v92), the card isn't anyone else's to decide yet.
254
- const subagentDeciding = stage?.subagent !== undefined && card.waiting !== null && card.waiting.endsWith(" is deciding");
255
- const canDecide = viewer.approver && card.state === "active" && stage?.kind === "approval" && (decider === null || decider === viewer.name) && !subagentDeciding;
283
+ // Its open question: one of its own, or (v94) a tool call waiting for approval.
284
+ const asked = card.state === "active" ? store.openSubagentQuestionOn(card.id, card.entry) : null;
285
+ // "Person chooses" (flow-send.ts): what was sent, and the options, for the person it waits on; the latest send otherwise.
286
+ const choosing = card.state === "active" && stage?.kind === "choose" ? flowChoiceAt(store, card.id, card.entry) : null;
287
+ // Who may decide it here: the one rule Home reads too (flowCardWaitsOn).
288
+ const canDecide = flowCardWaitsOn(store, flow, card, viewer, { stage, asked, choosing, browser: browserNow }).decide;
256
289
  // History reads moves and ownership together, newest first.
257
290
  const owned = discussion.filter(one => one.kind === "owner").map(one => ({ text: one.body === "" ? `${one.author} left it without an owner` : one.body === one.author ? `${one.author} took it on` : `${one.author} made ${one.body} the owner`, at: one.at }));
258
291
  const moves = store.flowEvents(card.id).map(event => ({ text: historyText(event, title, sortZones), at: event.at }));
@@ -266,8 +299,6 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
266
299
  at: until.toISOString(),
267
300
  label: stage.wait !== undefined && stage.wait.for !== "hours" ? (stage.wait.for === "reply" ? "No reply by" : "Moves on at") : stage.limit.to !== null ? `Moves to ${title(stage.limit.to)} at` : "Reminder at",
268
301
  };
269
- // Its open question: one of its own, or (v94) a tool call waiting for approval.
270
- const asked = card.state === "active" ? store.openSubagentQuestionOn(card.id, card.entry) : null;
271
302
  const pending = asked?.toolCall == null ? null : store.subagentCall(asked.toolCall);
272
303
  // A decision its subagent handed over carries what the subagent said (v92).
273
304
  const turn = card.state === "active" && stage?.kind === "approval" && stage.subagent !== undefined ? store.flowStepRun(card.id, card.entry) : null;
@@ -284,8 +315,6 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
284
315
  }
285
316
  }
286
317
  const asker = asked === null || asked.state !== "open" ? null : store.getSubagent(asked.subagent);
287
- // "Person chooses" (flow-send.ts): what was sent, and the options, for the person it waits on; the latest send otherwise.
288
- const choosing = card.state === "active" && stage?.kind === "choose" ? flowChoiceAt(store, card.id, card.entry) : null;
289
318
  const latest = store.flowSends(card.id)[0] ?? null;
290
319
  const sentContent = latest === null ? null : readFlowSend(latest.contentJson);
291
320
  // On the card itself: where it came from (the card's title is right above), and only the links the panel doesn't already have.
@@ -295,8 +324,7 @@ export function flowView(store, flow, viewer, selectedCard, setup = { dir: null,
295
324
  // After research: the same numbered items the message carried, each beside its screenshot.
296
325
  ...(content.items === undefined ? {} : { items: content.items.map((item, index) => ({ number: index + 1, title: item.title, lines: whyLines(item.why), source: item.source, url: item.url,
297
326
  image: item.shot === null || content.shots === null ? null : { src: `/r/${content.shots.run}/evidence/${item.shot}`, caption: itemCaption(index + 1, item) } })) }) });
298
- const browserWait = card.state === "active" && stage?.kind === "check" && stage.browser === true && browser !== null && browser.status !== "allowed"
299
- && card.waiting !== null && card.waiting.startsWith(`${stage.title} starts a browser`);
327
+ const browserWait = browserWaitOn(card, stage, browser);
300
328
  return {
301
329
  id: card.id, title: card.title, description: card.description, stage: card.stage, state: card.state,
302
330
  waiting: card.state === "active" && card.task !== null && (stage?.kind === "task" || stage?.kind === "report")
package/dist/keymap.d.ts CHANGED
@@ -15,7 +15,7 @@ export type WebShortcut = {
15
15
  kind: "help";
16
16
  };
17
17
  /** What a key does in the terminal UI, or null with the reason it has none there (a named gap). */
18
- export type TuiShortcut = "new" | "ink" | "send-back" | "retry" | "list" | "help";
18
+ export type TuiShortcut = "new" | "ink" | "send-back" | "retry" | "plan" | "list" | "help";
19
19
  export type Shortcut = {
20
20
  keys: string;
21
21
  says: string;
@@ -30,6 +30,8 @@ export declare const INK_ACT_SELECTORS: readonly ["[data-ink-approve]", '[data-i
30
30
  export declare const SEND_BACK_SELECTORS: readonly ['[data-act="request-changes"]', '[data-act="revise"]', 'a.result-feedback-link[href="#request-changes"]'];
31
31
  /** Retry: the result's Retry, or the failed box's own Retry form. */
32
32
  export declare const RETRY_SELECTORS: readonly ['[data-act="retry"]', 'form[data-retry] button[type="submit"]', 'form[data-rebuild] button[type="submit"]'];
33
+ /** Plan it: the task page's own plan button (local-owner slice 2). */
34
+ export declare const PLAN_SELECTORS: readonly ['form[action$="/plan"] button[type="submit"]'];
33
35
  export declare const SHORTCUTS: readonly Shortcut[];
34
36
  /** The keys typed, as the help list shows them: "g h" reads "g then h". */
35
37
  export declare const keyWords: (keys: string) => string;
package/dist/keymap.js CHANGED
@@ -8,11 +8,14 @@ export const INK_ACT_SELECTORS = ["[data-ink-approve]", '[data-ink-act="accept"]
8
8
  export const SEND_BACK_SELECTORS = ['[data-act="request-changes"]', '[data-act="revise"]', 'a.result-feedback-link[href="#request-changes"]'];
9
9
  /** Retry: the result's Retry, or the failed box's own Retry form. */
10
10
  export const RETRY_SELECTORS = ['[data-act="retry"]', 'form[data-retry] button[type="submit"]', 'form[data-rebuild] button[type="submit"]'];
11
+ /** Plan it: the task page's own plan button (local-owner slice 2). */
12
+ export const PLAN_SELECTORS = ['form[action$="/plan"] button[type="submit"]'];
11
13
  export const SHORTCUTS = [
12
14
  { keys: "n", says: "new task", web: { kind: "go", href: "/tasks/new" }, tui: "new" },
13
15
  { keys: "a", says: "Approve or Accept (presses the button on the page)", web: { kind: "press", selectors: INK_ACT_SELECTORS }, tui: "ink" },
14
16
  { keys: "b", says: "Send back (opens its note)", web: { kind: "press", selectors: SEND_BACK_SELECTORS }, tui: "send-back" },
15
17
  { keys: "r", says: "Retry (presses the button on the page)", web: { kind: "press", selectors: RETRY_SELECTORS }, tui: "retry" },
18
+ { keys: "p", says: "Ask for a plan (presses the button on the page)", web: { kind: "press", selectors: PLAN_SELECTORS }, tui: "plan" },
16
19
  { keys: "/", says: "search", web: { kind: "search" }, tui: { none: "The terminal has no search; press w to open the console." } },
17
20
  { keys: "g h", says: "go Home", web: { kind: "go", href: "/" }, tui: "list" },
18
21
  { keys: "g t", says: "go to Tasks", web: { kind: "go", href: "/tasks" }, tui: "list" },
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The laptop's local owner's password-free acts beyond approving and accepting (docs/plans/local-owner-acts.md, slices
3
+ * 2 and 3): asking for a plan, and retrying its own approved build. One writer per act, shared by the terminal (the
4
+ * CLI rows `task plan` and `task requeue`, run as the local owner) and the browser (the task page's plan button and
5
+ * Retry, posted by a local owner's session). Each writer proves everything inside its own transaction: the local owner
6
+ * acting now as exactly this act (localActNow, never `proveAccountNow` saying yes to any act), the owner's current reach
7
+ * into the project, the one work rule (scope.ts localWorkEligible), the exact thing the screen read, and the act's own
8
+ * guards. Neither is ever chosen for anyone else: a password session, an API token, the lead, a chat bot or a machine
9
+ * keeps the road it had.
10
+ */
11
+ import type { Store } from "./store.js";
12
+ /** What a refused local act says, and a reason the surfaces branch on. */
13
+ export type LocalActRefusal = {
14
+ ok: false;
15
+ reason: string;
16
+ message: string;
17
+ };
18
+ /** The ledger act a local owner's plan request is kept under: the planning source it was bound to, and its transport. */
19
+ export declare const LOCAL_PLAN_ACTION = "task plan (local owner)";
20
+ /** What a stale screen hears: the task is not what it read. */
21
+ export declare const LOCAL_TASK_CHANGED = "This task changed. Look at it again.";
22
+ /**
23
+ * The planning source a screen binds a plan request to (planner-source.ts): the digest over everything the planner
24
+ * would be given, the filed scope when there is one and the title alone when there is none. Null when it can't be read.
25
+ */
26
+ export declare function planSourceOf(store: Store, taskId: string): string | null;
27
+ /**
28
+ * Ask for a plan, as the laptop's local owner, with no password (local-owner slice 2). In one transaction: the owner
29
+ * acting now as exactly `request-plan`; its current reach into the task's project; the task not put away; the one work
30
+ * rule for planning (no release, protected, two-person, escalated, coordinator, imported or unsandboxed work); no plan
31
+ * pins saved on the task (a local request never plans with an agent someone chose; it refuses rather than ignoring or
32
+ * clearing them, so what plans is exactly what the screen's terms say); and the planning source still the one the
33
+ * screen read (`source`). A request already made, with that source unchanged, answers "already" and writes nothing.
34
+ * Otherwise any unchanged-plan pre-authorization saved on the task is withdrawn (with its audit row), so the plan this
35
+ * asks for can only be approved by a person's own yes; then requestPlan's own guards (queued, not planned, not
36
+ * approved, no live claim); then the request's ledger row. It approves nothing, pins nothing and pre-authorizes nothing.
37
+ */
38
+ export declare function requestPlanAsLocalOwner(store: Store, taskId: string, source: string, now: Date): {
39
+ ok: true;
40
+ replayed: boolean;
41
+ } | LocalActRefusal;
42
+ /** The ledger act a local owner's retry is kept under: the build it retried, the exact seal it spent, and its transport. */
43
+ export declare const LOCAL_RETRY_ACTION = "task requeue (local owner)";
44
+ /** Local retries per task and exact seal: then a note (Send back) or a password. */
45
+ export declare const LOCAL_RETRY_LIMIT = 3;
46
+ /** The fourth local retry of one approval. */
47
+ export declare const LOCAL_RETRY_SPENT = "You've retried this 3 times. Send it back with a note, or sign in with your password to retry again.";
48
+ /** What a retry hears while Toolroll can't yet prove the last build stopped. */
49
+ export declare const LOCAL_RETRY_STOPPING = "The last build is still stopping. Try again in a moment.";
50
+ /**
51
+ * What a Retry binds to (local-owner slice 3): the task's latest finished build and its approved scope digest, when the
52
+ * task failed or finished and its scope is approved as it stands. What a screen offers only; the writer decides.
53
+ */
54
+ export declare function retryPinsOf(store: Store, taskId: string): {
55
+ run: number;
56
+ digest: string;
57
+ } | null;
58
+ /**
59
+ * Retry the local owner's own approved build after it stopped (a console restart, a model error), with no password
60
+ * (local-owner slice 3). One transaction, in this order:
61
+ * - the owner acting now as exactly `retry-build`, and its current reach into the task's project;
62
+ * - the task's current approval is this owner's own person yes on the exact seal (personApprovalOf: the ceremony or a
63
+ * chat tap, a person's proof for those bytes — never a mode, grant, schedule, automatic or carried seal, the lead, or
64
+ * another person), still standing for dispatch, and on work the owner could approve again now (localWorkEligible);
65
+ * - bound to what the screen read: `digest` is the approved scope digest, and `run` the latest finished build;
66
+ * - the same request again (this owner's retry of `run` under this seal) answers "already" and writes nothing while
67
+ * that build is still the latest one;
68
+ * - its own stop proof: no unfinished run of the task, no run after `run`, Toolroll's durable stop facts settled for
69
+ * every run (an expired claim alone proves nothing), no final result (accepted or published) in any task state, and a
70
+ * failed task or a finished one whose result wasn't accepted (requeueEligibility re-reads the claim and archive);
71
+ * - at most three local retries of this task under this exact seal (a fresh approval, even of the same bytes, is a new
72
+ * seal), then the words that lead to a note or a password;
73
+ * - then the requeue (incidents resolved, strikes cleared, as the console's Retry) and its ledger row.
74
+ * Nothing about the scope, route, model, chain, budget or permissions is written.
75
+ */
76
+ export declare function retryAsLocalOwner(store: Store, taskId: string, pins: {
77
+ run: number;
78
+ digest: string;
79
+ }, now: Date): {
80
+ ok: true;
81
+ replayed: boolean;
82
+ retries: number;
83
+ } | LocalActRefusal;