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
@@ -27,7 +27,7 @@ export type WorkIndexItem = {
27
27
  ask: Ask | null;
28
28
  /** The row's chip under its group: the specific ask (needs-you.ts askChipOf), or none. */
29
29
  chip: AskChip | null;
30
- /** What a Needs you row asks, in Home's five groups (ASK_GROUP_SQL); null for every other row. */
30
+ /** What a Needs you row asks, in Home's six groups (ASK_GROUP_SQL); null for every other row. */
31
31
  askGroup: WorkAskGroup | null;
32
32
  primaryAction: WorkAction | null;
33
33
  completion: {
@@ -65,18 +65,23 @@ export type WorkIndexOptions = {
65
65
  /** How the list is grouped: the Needs you rows by their ask, then building, then the rest. */
66
66
  export type WorkIndexGroup = Ask | 'building' | 'rest';
67
67
  export type WorkIndexGroupCounts = Record<WorkIndexGroup, number>;
68
- /** What a Needs you row asks, as Home groups it (docs/plans/live-pages-and-home.md B1.2): a plan to approve, a question
69
- * to answer, something that went wrong, a result to review, or anything else that needs the person. */
70
- export type WorkAskGroup = 'approve' | 'answer' | 'wrong' | 'review' | 'other';
68
+ /** What a Needs you row asks, as Home groups it (docs/plans/live-pages-and-home.md B1.2): a release to approve, a plan
69
+ * to approve, a question to answer, something that went wrong, a result to review, or anything else that needs the
70
+ * person. A release is a plan to approve whose active task the plane's sealed state names a release (release-approval.ts
71
+ * releaseTaskOf, docs/plans/home-other-asks.md v2.1); a failed release is still something that went wrong and a
72
+ * release's result still a result to review. */
73
+ export type WorkAskGroup = 'release' | 'approve' | 'answer' | 'wrong' | 'review' | 'other';
71
74
  export declare const WORK_ASK_GROUPS: readonly WorkAskGroup[];
72
75
  /** `asks`: the exact Needs you totals per ask group, over every row in view (never only the page). `completedSince`:
73
- * how many the Completed view lists from `options.completedSince` on (0 without it). */
76
+ * how many the Completed view lists from `options.completedSince` on (0 without it). `builderWaits`: how many Needs you
77
+ * rows wait for a builder to connect or start (no worker registered or none online). */
74
78
  export type WorkIndexPage = {
75
79
  items: WorkIndexItem[];
76
80
  totals: WorkIndexCounts;
77
81
  groups: WorkIndexGroupCounts;
78
82
  asks: Record<WorkAskGroup, number>;
79
83
  completedSince: number;
84
+ builderWaits: number;
80
85
  projects: WorkProjectCounts[];
81
86
  nextCursor: string | null;
82
87
  limit: number;
@@ -21,13 +21,14 @@ import { withFollowUps } from './result-follow-ups.js';
21
21
  import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
22
22
  import { ASKS, CODE_READING, STORAGE_HOLD_PREFIX, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, isWaitCode, processNeedOf, resultHoldUpSentence } from './needs-you.js';
23
23
  import { retryWhenWords } from './dispatch.js';
24
+ import { releaseTaskOf } from './release-approval.js';
24
25
  import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
25
- export const WORK_ASK_GROUPS = ['approve', 'answer', 'wrong', 'review', 'other'];
26
+ export const WORK_ASK_GROUPS = ['release', 'approve', 'answer', 'wrong', 'review', 'other'];
26
27
  export const WORK_INDEX_PAGE_LIMIT = 40;
27
28
  export const WORK_INDEX_MAX_LIMIT = 100;
28
29
  const EMPTY = { all: 0, 'needs-you': 0, ready: 0, running: 0, queued: 0, completed: 0, archived: 0 };
29
30
  const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
30
- const NO_ASKS = { approve: 0, answer: 0, wrong: 0, review: 0, other: 0 };
31
+ const NO_ASKS = { release: 0, approve: 0, answer: 0, wrong: 0, review: 0, other: 0 };
31
32
  const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
32
33
  const VIEWS = ['all', 'needs-you', 'ready', 'running', 'queued', 'completed', 'archived'];
33
34
  /** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
@@ -75,7 +76,7 @@ const ASK_RANK = `CASE WHEN code IN (${codesOf('decide')}) THEN 0 WHEN code IN (
75
76
  /** Home's ask group of a row, by its code alone, so a row's group and its group's count are one rule: a plan waiting
76
77
  * for its yes, a question, a build that failed or stopped unexpectedly, a saved result to review (ASK_RANK's Review),
77
78
  * and everything else that waits on a person. Read on Needs you rows only. */
78
- const ASK_GROUP_SQL = `CASE WHEN code='needs-approval' THEN 'approve' WHEN code IN ('waiting-decision','decision-queue') THEN 'answer'
79
+ const ASK_GROUP_SQL = `CASE WHEN code='needs-approval' AND c.id IN (SELECT value FROM json_each($releaseTasks)) THEN 'release' WHEN code='needs-approval' THEN 'approve' WHEN code IN ('waiting-decision','decision-queue') THEN 'answer'
79
80
  WHEN code IN ('failed','waiting-incident','vanished-run') THEN 'wrong' WHEN code IN (${codesOf('review')}) AND NOT custody_unresolved THEN 'review' ELSE 'other' END`;
80
81
  /** Where a row sits in the list: Decide, Review, Unblock, then building, then the rest by recency. */
81
82
  const ORDER = `(CASE WHEN rank=0 THEN ask_rank WHEN rank=1 THEN 3 ELSE 4 END)`;
@@ -348,9 +349,17 @@ function custodyReadings(store) {
348
349
  }
349
350
  return { readings, workspace: new Set(noted) };
350
351
  }
351
- function parameters(now, access, options, custody) {
352
+ /** The list can't read a release's sealed state in SQL: each queued task (the only state a plan waiting for a yes is in)
353
+ * whose scope carries a release form, kept only where release-approval.ts releaseTaskOf names it a release, the one
354
+ * rule the release sheet reads (never a title or a task id). */
355
+ function releaseTaskIds(store) {
356
+ const sealed = store.handle.prepare(`SELECT s.task_id FROM task_scope s JOIN task t ON t.id=s.task_id
357
+ WHERE t.state='queued' AND (s.release_terms_json IS NOT NULL OR s.release_check_json IS NOT NULL)`).all().map(row => String(row['task_id']));
358
+ return JSON.stringify(sealed.filter(taskId => releaseTaskOf(store, taskId) !== null));
359
+ }
360
+ function parameters(store, now, access, options, custody) {
352
361
  const ids = (keep) => JSON.stringify([...custody.readings].filter(([ref, fact]) => keep(ref, fact)).map(([ref]) => ref));
353
- return { $settledCustody: ids((_, fact) => fact === null), $waitingCustody: ids((_, fact) => fact !== null && 'wait' in (processNeedOf(fact) ?? {})),
362
+ return { $releaseTasks: releaseTaskIds(store), $settledCustody: ids((_, fact) => fact === null), $waitingCustody: ids((_, fact) => fact !== null && 'wait' in (processNeedOf(fact) ?? {})),
354
363
  $workspaceCustody: ids((ref, fact) => fact !== null && custody.workspace.has(ref)),
355
364
  $now: now.toISOString(), $alive: new Date(now.getTime() - 180_000).toISOString(),
356
365
  $all: access.repos === null ? 1 : 0, $repos: JSON.stringify(access.repos ?? []),
@@ -373,7 +382,8 @@ const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) deci
373
382
  + `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
374
383
  /** The exact Needs you totals per ask group, and what was completed since the page's `completedSince`: page reads only. */
375
384
  const ASK_TOTALS = WORK_ASK_GROUPS.map(group => `COALESCE(SUM(needs AND ask_group='${group}'),0) ask_${group}_count`).join(',')
376
- + `,COALESCE(SUM(code='complete' AND archived=0 AND $completedSince IS NOT NULL AND sort_at>=$completedSince),0) completed_since_count`;
385
+ + `,COALESCE(SUM(code='complete' AND archived=0 AND $completedSince IS NOT NULL AND sort_at>=$completedSince),0) completed_since_count`
386
+ + `,COALESCE(SUM(needs AND code IN ('no-worker-registered','no-worker-online')),0) builder_wait_count`;
377
387
  function askCounts(row) {
378
388
  return Object.fromEntries(WORK_ASK_GROUPS.map(group => [group, n(row, `ask_${group}_count`)]));
379
389
  }
@@ -438,7 +448,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
438
448
  )
439
449
  SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,'ready_count',ready_count,
440
450
  'running_count',running_count,'queued_count',queued_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
441
- UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,${WORK_ASK_GROUPS.map(() => '0,').join('')}0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
451
+ UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,0,${WORK_ASK_GROUPS.map(() => '0,').join('')}0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
442
452
  'archived_kind',(SELECT by_kind FROM task_archive WHERE task_ref=page.ref_id),'archived_reason',(SELECT reason FROM task_archive WHERE task_ref=page.ref_id),
443
453
  'archived_at',(SELECT at FROM task_archive WHERE task_ref=page.ref_id),'root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
444
454
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
@@ -458,7 +468,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
458
468
  'agent_ask_run',(SELECT run FROM agent_question WHERE id=page.agent_ask),'agent_ask_text',(SELECT substr(question,1,300) FROM agent_question WHERE id=page.agent_ask),'replaced_by',(SELECT successor FROM task_replacement WHERE task_ref=page.ref_id),
459
469
  'completed_by_lead',(SELECT lead FROM task_act WHERE task_ref=page.root_ref AND act='completed' ORDER BY id DESC LIMIT 1),'machine_check',COALESCE(machine_check,0),
460
470
  'question_task',(SELECT r.external_id FROM decision d JOIN run ON run.id=d.run JOIN task_ref r ON r.id=run.task_ref WHERE d.id=page.question_id)) FROM page`)
461
- .all({ ...parameters(now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $completedSince: options.completedSince ?? null, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
471
+ .all({ ...parameters(store, now, access, options, custody), $taskId: taskId, $roots: options.roots === undefined ? null : JSON.stringify(options.roots), $cursorRoot: cursor?.root ?? 0, $cursorRank: cursor?.rank ?? 0, $cursorAt: cursor?.at ?? '', $limit: limit + 1, $completedSince: options.completedSince ?? null, $recent: new Date(now.getTime() - 86_400_000).toISOString() });
462
472
  const selected = rows.slice(1).map(row => JSON.parse(String(row['row_json'])));
463
473
  const page = selected.slice(0, limit), last = page.at(-1);
464
474
  const nextCursor = selected.length > limit && last !== undefined ? Buffer.from(JSON.stringify({ version: 2, scope,
@@ -477,13 +487,13 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
477
487
  const readiness = (row, checks) => resultReadinessFacts(store, now, options.root, row, checks, memo);
478
488
  return { items: page.map(row => itemOf(row, access.principal, probe, leadName, readiness, options.root !== undefined, now)), projects, totals: rows[0] === undefined ? { ...EMPTY } : counts(rows[0]),
479
489
  groups: rows[0] === undefined ? { ...NO_GROUPS } : groupCounts(rows[0]), asks: rows[0] === undefined ? { ...NO_ASKS } : askCounts(rows[0]),
480
- completedSince: rows[0] === undefined ? 0 : n(rows[0], 'completed_since_count'), nextCursor, limit, view };
490
+ completedSince: rows[0] === undefined ? 0 : n(rows[0], 'completed_since_count'), builderWaits: rows[0] === undefined ? 0 : n(rows[0], 'builder_wait_count'), nextCursor, limit, view };
481
491
  }
482
492
  export function workCountsByProject(store, now, access) {
483
493
  const projection = registerValidators(store);
484
494
  return store.handle.prepare(`${projection} SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
485
495
  SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo ORDER BY repo`)
486
- .all({ ...parameters(now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
496
+ .all({ ...parameters(store, now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
487
497
  .map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
488
498
  }
489
499
  export function workStateRows(store, now, access) {
@@ -495,7 +505,7 @@ export function workStateRows(store, now, access) {
495
505
  COALESCE((SELECT held_at FROM hold WHERE id=hold_id),''),COALESCE((SELECT requested_at FROM run_stop WHERE run=stop_run),'')),''),sort_at) ask_at,
496
506
  COALESCE(live_run,(SELECT run FROM decision WHERE id=question_id),(SELECT run FROM agent_question WHERE id=agent_ask)) ask_run,
497
507
  (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members FROM ranked WHERE archived=0`)
498
- .all(parameters(now, access, {}, custodyReadings(store)))
508
+ .all(parameters(store, now, access, {}, custodyReadings(store)))
499
509
  .map(row => ({ repo: s(row, 'repo'), rootId: String(row['root_id']), code: String(row['code']), needs: n(row, 'needs') === 1, personNeed: n(row, 'person_need') === 1,
500
510
  leadClaim: n(row, 'lead_claim') === 1, liveRun: row['live_run'] == null ? null : n(row, 'live_run'), unfinished: row['unfinished'] == null ? null : n(row, 'unfinished'),
501
511
  resultRun: row['result_id'] == null ? null : n(row, 'result_id'), waits: Number(row['rank']) === 2, version: String(row['version']), at: String(row['ask_at'] ?? ''), askRun: row['ask_run'] == null ? null : n(row, 'ask_run'), members: JSON.parse(String(row['members'] ?? '[]')).map(Number) }));
@@ -530,7 +540,7 @@ export function workOutcomeRows(store, now, access, project, since, limit) {
530
540
  (SELECT status FROM run_check WHERE run=f.result_id) check_status,
531
541
  COALESCE(a.runs,0) runs,COALESCE(a.api_runs,0) api_runs,a.api_microusd,COALESCE(a.subscription_runs,0) subscription_runs,a.subscription_usd
532
542
  FROM selected f LEFT JOIN attempts a ON a.root_ref=f.root_ref ORDER BY f.sort_at DESC,f.root_ref DESC`)
533
- .all({ ...parameters(now, access, { project }, custodyReadings(store)), $since: since, $outcomeLimit: limit + 1 });
543
+ .all({ ...parameters(store, now, access, { project }, custodyReadings(store)), $since: since, $outcomeLimit: limit + 1 });
534
544
  }
535
545
  /** Families whose headline is Ready for review, newest first: the Tasks
536
546
  * list's own rows, so a completed family (root or any revision), a failed
@@ -783,7 +793,7 @@ export function workTidyRows(store, now, access) {
783
793
  (family_running OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved OR stop_run IS NOT NULL AND stop_settled IS NULL OR state='running') live,
784
794
  (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members,
785
795
  (SELECT json_group_array(m.id) FROM members m WHERE m.root_ref=ranked.root_ref) member_ids FROM ranked`)
786
- .all(parameters(now, access, {}, custodyReadings(store)))
796
+ .all(parameters(store, now, access, {}, custodyReadings(store)))
787
797
  .map(row => ({ rootId: String(row['root_id']), currentId: String(row['id']), title: String(row['root_title']), repo: s(row, 'repo'),
788
798
  state: String(row['state']), code: String(row['code']), live: n(row, 'live') === 1, archived: n(row, 'archived') === 1, broken: n(row, 'broken') === 1,
789
799
  updatedAt: String(row['family_updated'] ?? ''), members: JSON.parse(String(row['members'] ?? '[]')).map(Number),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.102",
3
+ "version": "0.9.104",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -137,7 +137,7 @@
137
137
  "@slack/socket-mode": "3.1.1",
138
138
  "@xterm/addon-serialize": "0.14.0",
139
139
  "@xterm/headless": "6.0.0",
140
- "discord.js": "14.28.0",
140
+ "discord.js": "14.28.1",
141
141
  "imapflow": "2.10.0",
142
142
  "mailparser": "3.14.1",
143
143
  "nodemailer": "10.3.1",