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
@@ -22,10 +22,12 @@ import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf,
22
22
  import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
23
23
  export const WORK_INDEX_PAGE_LIMIT = 40;
24
24
  export const WORK_INDEX_MAX_LIMIT = 100;
25
- const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
25
+ const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0, archived: 0 };
26
26
  const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
27
27
  const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
28
- const VIEWS = ['all', 'needs-you', 'running', 'completed'];
28
+ const VIEWS = ['all', 'needs-you', 'running', 'completed', 'archived'];
29
+ /** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
30
+ const ARCHIVED = `EXISTS(SELECT 1 FROM task_archive ta WHERE ta.task_ref=c.ref_id)`;
29
31
  const n = (row, key) => Number(row[key] ?? 0);
30
32
  const s = (row, key) => row[key] == null ? null : String(row[key]);
31
33
  /** The running agent's oldest open question (agent-board.ts), from its open newest attempt only. */
@@ -93,24 +95,25 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
93
95
  (SELECT id FROM run WHERE task_ref=a.ref_id AND outcome IS NULL ORDER BY id DESC LIMIT 1) unfinished,
94
96
  CASE WHEN a.state='done' THEN (SELECT id FROM run WHERE task_ref=a.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1) END result_id,
95
97
  (SELECT h.id FROM hold h WHERE h.task_ref=a.ref_id AND (h.until IS NULL OR h.until>$now) ORDER BY h.held_at,h.id LIMIT 1) hold_id,
96
- q.decision_id,${AGENT_ASK} agent_ask,(cu.task_ref IS NOT NULL OR a.ref_id IN (SELECT value FROM json_each($workspaceCustody)))
98
+ q.decision_id,${AGENT_ASK} agent_ask,EXISTS(SELECT 1 FROM task_archive ta WHERE ta.task_ref=a.ref_id) member_archived,(cu.task_ref IS NOT NULL OR a.ref_id IN (SELECT value FROM json_each($workspaceCustody)))
97
99
  AND a.ref_id NOT IN (SELECT value FROM json_each($settledCustody)) custody_unresolved
98
100
  FROM admitted a LEFT JOIN lineage l ON l.ref_id=a.ref_id
99
101
  LEFT JOIN questions q ON q.task_ref=a.ref_id LEFT JOIN custody cu ON cu.task_ref=a.ref_id
100
102
  ), family_heads AS MATERIALIZED (
101
103
  SELECT root_ref,COUNT(*) version_count,MAX(updated_at) family_updated,
102
104
  CAST(substr(MAX((CASE WHEN ref_id=root_ref THEN '0' ELSE '1' END)||created_at||printf('%020d',ref_id)),-20) AS INTEGER) current_ref,
103
- SUM(state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved) active_count,
104
- MIN(CASE WHEN state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved THEN ref_id END) first_active,
105
- MAX(CASE WHEN state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved THEN ref_id END) last_active,
106
- MAX(live_lease IS NOT NULL) family_running,MIN(decision_id) question_id
105
+ -- A put-away version (v145) is history: it never counts as an earlier active version or brings its question along.
106
+ SUM(NOT member_archived AND (state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved)) active_count,
107
+ MIN(CASE WHEN NOT member_archived AND (state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved) THEN ref_id END) first_active,
108
+ MAX(CASE WHEN NOT member_archived AND (state IN ('queued','running') OR live_lease IS NOT NULL OR unfinished IS NOT NULL OR custody_unresolved) THEN ref_id END) last_active,
109
+ MAX(live_lease IS NOT NULL) family_running,MIN(CASE WHEN NOT member_archived THEN decision_id END) question_id
107
110
  FROM members GROUP BY root_ref
108
111
  ), ordered AS MATERIALIZED (
109
112
  SELECT m.*,CASE WHEN m.ref_id=f.current_ref THEN 1 ELSE 2 END version_rank
110
113
  FROM members m JOIN family_heads f USING(root_ref)
111
114
  ), families AS MATERIALIZED (
112
115
  SELECT f.root_ref,f.version_count,f.family_updated,f.family_running,
113
- f.active_count-(c.state IN ('queued','running') OR c.live_lease IS NOT NULL OR c.unfinished IS NOT NULL OR c.custody_unresolved) earlier_active,
116
+ f.active_count-(NOT c.member_archived AND (c.state IN ('queued','running') OR c.live_lease IS NOT NULL OR c.unfinished IS NOT NULL OR c.custody_unresolved)) earlier_active,
114
117
  earlier.id earlier_id,COALESCE(c.decision_id,f.question_id) question_id
115
118
  FROM family_heads f JOIN members c ON c.ref_id=f.current_ref
116
119
  LEFT JOIN admitted earlier ON earlier.ref_id=CASE WHEN f.first_active=f.current_ref THEN f.last_active ELSE f.first_active END
@@ -236,8 +239,9 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
236
239
  SELECT c.*,CASE WHEN code IN ('cancelled','complete') THEN 0 ELSE ${LEAD_CLAIM_SQL} END lead_claim FROM classified c
237
240
  ), ranked AS MATERIALIZED (
238
241
  -- A task the person's lead is on (lead-voice.ts) waits on nobody: it leaves Needs you until the claim ends or lapses.
239
- SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} AND lead_claim<>1 needs,
240
- code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} person_need,
242
+ SELECT c.*,code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} AND lead_claim<>1 AND NOT ${ARCHIVED} needs,
243
+ code NOT IN ('cancelled','complete','running','updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') AND NOT ${CUSTODY_WAITS} AND NOT ${ARCHIVED} person_need,
244
+ ${ARCHIVED} archived,
241
245
  CASE WHEN code='complete' THEN 3 WHEN code='cancelled' THEN 4 WHEN code='running' THEN 1
242
246
  WHEN ${CUSTODY_WAITS} OR code IN ('updating','retry-scheduled','waiting-dependency','worker-at-capacity','planning-ready','scouting-ready','queued') THEN 2 ELSE 0 END rank,
243
247
  ${ASK_RANK} ask_rank,
@@ -329,11 +333,13 @@ function parameters(now, access, options, custody) {
329
333
  $leadOf: access.principal === 'operator' && access.viewer != null && access.viewer !== '' ? `lead for ${access.viewer}` : null };
330
334
  }
331
335
  function counts(row) {
332
- return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), running: n(row, 'running_count'), completed: n(row, 'completed_count') };
336
+ return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), running: n(row, 'running_count'), completed: n(row, 'completed_count'), archived: n(row, 'archived_count') };
333
337
  }
334
- const TOTALS = `COUNT(*) all_count,COALESCE(SUM(needs),0) needs_count,COALESCE(SUM(family_running),0) running_count,COALESCE(SUM(code='complete'),0) completed_count`;
335
- const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2),0) unblock_count,`
336
- + `COALESCE(SUM(rank=1),0) building_count,COALESCE(SUM(rank>1),0) rest_count`;
338
+ // Put-away families (v145) count only as archived.
339
+ const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) needs_count,COALESCE(SUM(family_running AND archived=0),0) running_count,`
340
+ + `COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
341
+ const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1 AND archived=0),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2 AND archived=0),0) unblock_count,`
342
+ + `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
337
343
  function groupCounts(row) {
338
344
  return { decide: n(row, 'decide_count'), review: n(row, 'review_count'), unblock: n(row, 'unblock_count'), building: n(row, 'building_count'), rest: n(row, 'rest_count') };
339
345
  }
@@ -383,18 +389,20 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
383
389
  const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
384
390
  const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
385
391
  const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
386
- const filter = view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1' : view === 'completed' ? "code='complete'" : '1';
392
+ const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1 AND archived=0' : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
387
393
  const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
388
- SELECT * FROM ranked WHERE ${filter} AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
394
+ SELECT * FROM ranked WHERE ($taskId IS NOT NULL OR ${filter}) AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
389
395
  ${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
390
396
  ORDER BY ${ORDER},sort_at DESC,root_ref DESC LIMIT $limit
391
397
  ), page AS (SELECT p.*,COALESCE(p.result_id,(SELECT id FROM run WHERE task_ref=p.ref_id AND finished_at IS NOT NULL AND role IN ('builder','scout') ORDER BY id DESC LIMIT 1)) page_result_id FROM selected_page p), totals AS (SELECT ${TOTALS},${GROUP_TOTALS} FROM ranked), project_totals AS (
392
- SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0) queued,
393
- SUM(state='done' AND family_running=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
398
+ SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
399
+ SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
394
400
  )
395
401
  SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,
396
- 'running_count',running_count,'completed_count',completed_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
397
- UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,json_object('root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
402
+ 'running_count',running_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
403
+ UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
404
+ '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),
405
+ '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,
398
406
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
399
407
  'earlier_id',earlier_id,'broken',broken,'live_run',live_run,'result_id',page_result_id,'result_outcome',(SELECT outcome FROM run WHERE id=page_result_id),'attempt_reason',(SELECT substr(reason,1,1000) FROM run WHERE task_ref IN (SELECT ref_id FROM members WHERE root_ref=page.root_ref) AND (finished_at IS NOT NULL OR outcome IS NOT NULL) AND role NOT IN ('planner','reviewer') ORDER BY id DESC LIMIT 1),
400
408
  'publication_url',(SELECT pr_url FROM publication WHERE run=page_result_id),
@@ -434,8 +442,8 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
434
442
  }
435
443
  export function workCountsByProject(store, now, access) {
436
444
  const projection = registerValidators(store);
437
- return store.handle.prepare(`${projection} SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0) queued,
438
- SUM(state='done' AND family_running=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo ORDER BY repo`)
445
+ return store.handle.prepare(`${projection} SELECT repo,${TOTALS},SUM(state='queued' AND family_running=0 AND archived=0) queued,
446
+ SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo ORDER BY repo`)
439
447
  .all({ ...parameters(now, access, {}, custodyReadings(store)), $recent: new Date(now.getTime() - 86_400_000).toISOString() })
440
448
  .map(row => ({ repo: s(row, 'repo'), totals: counts(row), queued: n(row, 'queued'), doneRecently: n(row, 'done_recently') }));
441
449
  }
@@ -447,12 +455,44 @@ export function workStateRows(store, now, access) {
447
455
  COALESCE((SELECT created_at FROM decision WHERE id=question_id),''),COALESCE((SELECT asked_at FROM agent_question WHERE id=agent_ask),''),
448
456
  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,
449
457
  COALESCE(live_run,(SELECT run FROM decision WHERE id=question_id),(SELECT run FROM agent_question WHERE id=agent_ask)) ask_run,
450
- (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members FROM ranked`)
458
+ (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members FROM ranked WHERE archived=0`)
451
459
  .all(parameters(now, access, {}, custodyReadings(store)))
452
460
  .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,
453
461
  leadClaim: n(row, 'lead_claim') === 1, liveRun: row['live_run'] == null ? null : n(row, 'live_run'), unfinished: row['unfinished'] == null ? null : n(row, 'unfinished'),
454
462
  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) }));
455
463
  }
464
+ /** Activity uses the Tasks list's exact family admission and saved completion rules.
465
+ * Only the requested project enters the projection. The extra row signals truncation;
466
+ * attempts are aggregated in SQLite, never loaded as an unbounded history. */
467
+ export function workOutcomeRows(store, now, access, project, since, limit) {
468
+ return store.handle.prepare(`${registerValidators(store)}, selected AS MATERIALIZED (
469
+ SELECT * FROM ranked WHERE archived=0 AND (code='ready-to-check' OR code='history-problem'
470
+ OR (code='complete' AND checked_at >= $since AND checked_at <= $now))
471
+ ORDER BY sort_at DESC,root_ref DESC LIMIT $outcomeLimit
472
+ ), amounts AS (
473
+ -- reported-usage.ts runAmountOf, row for row: only evidenced billing on a run whose cost was reported is known;
474
+ -- an API run counts its priced spend as dollars (a safe integer), a subscription run its reported cost as usage,
475
+ -- never dollars. TOTAL sums as a float, so no family's sum can overflow.
476
+ SELECT m.root_ref,r.cost_usd,s.microusd,
477
+ CASE WHEN r.billing_basis IS NOT 'evidence' OR COALESCE(r.cost_unknown,1)<>0 THEN 'unknown'
478
+ WHEN s.billing='subscription' THEN 'subscription'
479
+ WHEN s.billing='api-key' AND typeof(s.microusd)='integer' AND s.microusd>=0 AND s.microusd<=9007199254740991 THEN 'api'
480
+ ELSE 'unknown' END amount
481
+ FROM selected f JOIN members m ON m.root_ref=f.root_ref JOIN run r ON r.task_ref=m.ref_id LEFT JOIN run_spend s ON s.run=r.id
482
+ ), attempts AS (
483
+ SELECT root_ref,COUNT(*) runs,COUNT(CASE WHEN amount='api' THEN 1 END) api_runs,
484
+ TOTAL(CASE WHEN amount='api' THEN microusd END) api_microusd,
485
+ COUNT(CASE WHEN amount='subscription' THEN 1 END) subscription_runs,
486
+ SUM(CASE WHEN amount='subscription' AND typeof(cost_usd) IN ('integer','real') AND cost_usd>=0 THEN cost_usd END) subscription_usd
487
+ FROM amounts GROUP BY root_ref
488
+ ) SELECT f.root_id,f.id task_id,f.root_title title,f.code,f.finished_at,f.result_id,f.result_role,
489
+ f.version_count,f.checked_at,f.checked_actor,f.checked_detail,
490
+ (SELECT lead FROM task_act WHERE task_ref=f.root_ref AND act='completed' AND at=f.checked_at ORDER BY id DESC LIMIT 1) completed_by_lead,
491
+ (SELECT status FROM run_check WHERE run=f.result_id) check_status,
492
+ 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
493
+ FROM selected f LEFT JOIN attempts a ON a.root_ref=f.root_ref ORDER BY f.sort_at DESC,f.root_ref DESC`)
494
+ .all({ ...parameters(now, access, { project }, custodyReadings(store)), $since: since, $outcomeLimit: limit + 1 });
495
+ }
456
496
  /** Families whose headline is Ready for review, newest first: the Tasks
457
497
  * list's own rows, so a completed family (root or any revision), a failed
458
498
  * check and a superseded version never count. */
@@ -669,12 +709,14 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
669
709
  if (asked !== null)
670
710
  detail = `The agent asks: ${asked}`;
671
711
  }
672
- const views = ['all'];
673
- if (need)
712
+ const archived = n(row, 'archived') === 1 ? { by: String(row['archived_by'] ?? ''), byKind: row['archived_kind'] === 'tidy' ? 'tidy' : 'person',
713
+ reason: String(row['archived_reason'] ?? ''), at: String(row['archived_at'] ?? '') } : null;
714
+ const views = archived !== null ? ['archived'] : ['all'];
715
+ if (archived === null && need)
674
716
  views.push('needs-you');
675
- if (running)
717
+ if (archived === null && running)
676
718
  views.push('running');
677
- if (code === 'complete')
719
+ if (archived === null && code === 'complete')
678
720
  views.push('completed');
679
721
  return { rootId: String(row['root_id']), activeTaskId: id, title: String(row['root_title']), repo: s(row, 'repo'),
680
722
  state: String(row['state']), assignmentState, createdAt: String(row['created_at']), updatedAt: String(row['sort_at']),
@@ -686,5 +728,17 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
686
728
  ask: need ? ASKS[n(row, 'ask_rank')] ?? 'unblock' : null,
687
729
  chip: need ? askChipOf({ headline: shared.headline, need: shared.need?.key ?? null, planChanged, mismatch: verdict === 'refuted' && n(row, 'proof_failed_check') !== 1, ask: ASKS[n(row, 'ask_rank')] ?? 'unblock' }) : null,
688
730
  primaryAction, completion: code === 'complete' ? { actor: String(row['checked_actor']), at: String(row['checked_at']), digest: String(row['checked_digest']), ...(landed === null ? {} : { landed }) } : null,
689
- evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}) };
731
+ evidence: 'recorded', ...(lead === 'on-it' ? { lead: leadOnIt(leadName) } : {}), ...(archived === null ? {} : { archived }) };
732
+ }
733
+ export function workTidyRows(store, now, access) {
734
+ const projection = registerValidators(store);
735
+ return store.handle.prepare(`${projection} SELECT root_id,id,root_title,repo,state,code,archived,broken,family_updated,
736
+ (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,
737
+ (SELECT json_group_array(m.ref_id) FROM members m WHERE m.root_ref=ranked.root_ref) members,
738
+ (SELECT json_group_array(m.id) FROM members m WHERE m.root_ref=ranked.root_ref) member_ids FROM ranked`)
739
+ .all(parameters(now, access, {}, custodyReadings(store)))
740
+ .map(row => ({ rootId: String(row['root_id']), currentId: String(row['id']), title: String(row['root_title']), repo: s(row, 'repo'),
741
+ state: String(row['state']), code: String(row['code']), live: n(row, 'live') === 1, archived: n(row, 'archived') === 1, broken: n(row, 'broken') === 1,
742
+ updatedAt: String(row['family_updated'] ?? ''), members: JSON.parse(String(row['members'] ?? '[]')).map(Number),
743
+ memberIds: JSON.parse(String(row['member_ids'] ?? '[]')).map(String) }));
690
744
  }
@@ -26,7 +26,7 @@ import { type ChecksFact } from "./task-status.js";
26
26
  import { type Html } from "./html.js";
27
27
  /** The Work destination's views — shortcuts over the same rows, never a
28
28
  * persisted state. All is the default. */
29
- export type WorkView = "all" | "needs-you" | "running" | "completed";
29
+ export type WorkView = "all" | "needs-you" | "running" | "completed" | "archived";
30
30
  export declare const WORK_VIEWS: readonly {
31
31
  key: WorkView;
32
32
  label: string;
@@ -27,9 +27,10 @@ export const WORK_VIEWS = [
27
27
  { key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
28
28
  { key: "running", label: "Building", hint: "Attempts a builder owns right now.", empty: "Nothing is building right now. Approved tasks start when a builder with capacity is connected." },
29
29
  { key: "completed", label: "Complete", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "No tasks have been marked complete in this view." },
30
+ { key: "archived", label: "Archived", hint: "Tasks put away by you or by tidy. Their history and results are kept.", empty: "Nothing is put away." },
30
31
  ];
31
32
  export function parseWorkView(raw) {
32
- return raw === "needs-you" || raw === "running" || raw === "completed" ? raw : "all";
33
+ return raw === "needs-you" || raw === "running" || raw === "completed" || raw === "archived" ? raw : "all";
33
34
  }
34
35
  /** The review facts from the store's bounded retry projection — the same
35
36
  * reading `diagnoseTaskDispatch` makes, so every surface starts from one
@@ -525,7 +526,7 @@ function workStatusWordsOf(facts, resultDisplay) {
525
526
  }
526
527
  /** The counts each view tab wears — from the same rows the page lists. */
527
528
  export function workCounts(rows) {
528
- const counts = { all: 0, "needs-you": 0, running: 0, completed: 0 };
529
+ const counts = { all: 0, "needs-you": 0, running: 0, completed: 0, archived: 0 };
529
530
  for (const row of rows)
530
531
  for (const view of row.views)
531
532
  counts[view] += 1;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * youiyoux change and options requests become tasks (docs/guide/getting-started.md, "Change requests from youiyoux").
3
+ *
4
+ * youiyoux is a design-system tool: in its hub a person points at a component on a screenshot of their app and asks
5
+ * for a change, and it saves the request as `<project>/.youi/requests/<id>.json` (`youi.request/v1`). A project that
6
+ * uses youiyoux (a `youi.project.json` at its root, or a `.youi/requests` folder) has this intake on with no step of
7
+ * its own, unless a person turned it off in Settings → Integrations.
8
+ *
9
+ * On the worker's flow pass this reads the project's main checkout — the path from Toolroll's own records, resolved
10
+ * with realpath — and files each open change request with a task exactly once, keyed by the project and the request
11
+ * id in `youiyoux_request`: never again, whatever the file says later. Everything in the folder is outside text,
12
+ * written by whoever can write the project's folder:
13
+ *
14
+ * - the folders and files must be real (never a link), a file a single link to itself, at most 64 KB, read without
15
+ * following a link or waiting on a pipe, and at most 200 are read in one pass;
16
+ * - a file whose name, schema or id is wrong is counted (the integration says so) and never filed;
17
+ * - text that looks like a key or password is left out (recorded once, never filed), as flow triggers do;
18
+ * - a path the task may touch is kept only when youiyoux knew them all and every one is a plain relative path;
19
+ * - the task is filed through the one filing door (proposal.ts) by the "youiyoux" automation, unplanned, and waits
20
+ * for a person: its provenance (`filed_via = 'youiyoux'`) keeps every seal no person looked at off it (store.ts,
21
+ * the outside-request quarantine), so no operating mode, grant, schedule or automatic seal approves it. A person
22
+ * approves it in Toolroll, or with their own two taps in their paired chat where they turned approving from chat
23
+ * on (chat-decide.ts), bound to the exact scope the card showed.
24
+ *
25
+ * Beside each request it writes `<id>.task.json` (`youi.request-task/v1`): the task and its status, and, when the
26
+ * console has an address (the one the installation names, else this machine's own console where `up` serves it, on
27
+ * loopback), the task's link and `home`, where a person approves all waiting work. It is written when filed, whenever
28
+ * the status changes, and once more when that address changes. Beside an options request's options file (below), it is
29
+ * the only file it writes in the checkout: written whole (a temporary file in the same folder, then renamed over),
30
+ * refused when the folder or the target is a link, never the request itself. The status is also kept in the store;
31
+ * nothing Toolroll decides reads the file back.
32
+ *
33
+ * Status words (task-status.ts headline → youiyoux): Queued → queued; Planning, Building and Waiting (work going on
34
+ * by itself) → building; Needs you → needs-you; Ready for review → ready; Complete → done; Failed → failed;
35
+ * Stopped → stopped.
36
+ *
37
+ * An options request (`kind: "options"`) asks an agent for two to four options for one component, which youiyoux shows
38
+ * on its Screens canvas. It is filed the same way, once, waiting for a person, and only as youiyoux's contract has it:
39
+ * its offer names `p-<its own hex>`, that proposal's file `.youi/proposals/p-<hex>.json`, and the task may touch that
40
+ * one file (anything else is counted as a file that couldn't be read, and never filed). The proposal it names is kept
41
+ * when filed; the request is never read for it again. Then (runYouiyoux, the worker's pass):
42
+ *
43
+ * - When the task is Ready for review (or Complete), its result is read: the file at the result's commit, the commit
44
+ * Toolroll recorded for the build (never the checkout the agent had, which it controls and the pool hands on), read
45
+ * with the hardened git (teardown-git.ts). It must be a plain file in that commit (never a link), at most 64 KB, a
46
+ * `youi.proposal/v1` file whose id is the proposal, which answers this request, still open (an agent proposes; only a
47
+ * person decides), by an agent, with no key-like text. Then it is copied, byte for byte, into the project's main
48
+ * checkout as `.youi/proposals/<id>.json` the way the status file is written (folders real, never through a link;
49
+ * a temporary file, then linked into place, which never replaces a file already there: a person may have decided on
50
+ * it). A file missing from the result, or one that isn't safe, is said in the status file and never copied; a newer
51
+ * result is read again.
52
+ * - Only a file Toolroll copied is read for a decision: one already at that name is left as it was (a person may
53
+ * have decided on it) and never read for one, unless it is byte for byte the result's (an earlier pass's copy).
54
+ * - When that copy says a person chose an option or dismissed them all (youiyoux's hub writes that), and it is still
55
+ * the proposal copied (only its status, choice, decidedBy, decidedAt, request and updatedAt may differ), decided by
56
+ * a person, at a time, the task is marked complete by Toolroll, as landed work is (result-completion.ts LANDED_ACTOR), without
57
+ * merging or publishing its branch: its deliverable was the options, and youiyoux has used them. Only the exact
58
+ * result whose file was copied, and only through every guard a person's completion passes (landed-cleanup.ts
59
+ * completionProblem): otherwise it stays Ready for review and the ledger says why. The ledger keeps who decided
60
+ * what, as youiyoux wrote it (outside text, cleaned). A decision never approves, publishes or merges anything.
61
+ */
62
+ import type { Store } from "./store.js";
63
+ import type { Headline } from "./task-status.js";
64
+ import { type Git } from "./landed-cleanup.js";
65
+ import { YOUIYOUX_FILED_VIA, type YouiyouxRequest, type YouiyouxRequestTask, type YouiyouxTaskStatus } from "./contracts/youiyoux-request.js";
66
+ export { YOUIYOUX_FILED_VIA };
67
+ /** Who files the tasks: an automation, like a flow trigger. */
68
+ export declare const YOUIYOUX_ACTOR = "youiyoux";
69
+ export declare const REQUEST_BYTES: number;
70
+ export declare const REQUESTS_PER_PASS = 200;
71
+ export declare const FILED_PER_PASS = 10;
72
+ /** The ledger's word for a decision youiyoux's hub made on a task's options. */
73
+ export declare const OPTIONS_DECIDED_ACTION = "youiyoux options decided";
74
+ export declare const OPTIONS_COPIED_ACTION = "youiyoux options copied";
75
+ /** The flow pass runs every 15 seconds; the folder is read at most this often. */
76
+ export declare const INTAKE_EVERY_MS = 30000;
77
+ export declare const STATUS_OF_HEADLINE: Readonly<Record<Headline, YouiyouxTaskStatus>>;
78
+ /** Whether a project uses youiyoux: a `youi.project.json` at its root, or a `.youi` folder. Only their presence is
79
+ * read here (a link counts, so the integration can say it is Broken); requestsFolder decides what may be read. */
80
+ export declare function usesYouiyoux(repo: string): boolean;
81
+ /** The paths a request may touch: every one a plain relative path inside the project, or none at all. */
82
+ export declare function safeTouches(touches: readonly string[]): string[] | null;
83
+ export type RequestTask = {
84
+ title: string;
85
+ goal: string;
86
+ outOfScope: string | null;
87
+ touches: string[];
88
+ acceptance: {
89
+ id: string;
90
+ statement: string;
91
+ how: null;
92
+ evidence: string[];
93
+ }[];
94
+ /** Where the person pointed, for the task page: the route, the component, and `file:line` when youiyoux knew a plain one. */
95
+ pointed: {
96
+ route: string;
97
+ component: string;
98
+ at: string | null;
99
+ };
100
+ };
101
+ /** The task a request asks for, in Toolroll's terms. Text is cleaned here; whether it may be filed is decided by the caller. */
102
+ export declare function requestTask(request: YouiyouxRequest): RequestTask;
103
+ /** A person turns the intake off or back on for a project; the ledger keeps who. */
104
+ export declare function setYouiyouxIntake(store: Store, repo: string, on: boolean, by: string, now: Date): void;
105
+ export type YouiyouxState = {
106
+ /** The project uses youiyoux, as the worker's last pass found. */
107
+ uses: boolean;
108
+ /** Requests become tasks: it uses youiyoux and nobody turned it off. */
109
+ on: boolean;
110
+ /** A person turned it off. */
111
+ off: boolean;
112
+ /** Filed tasks not yet done, failed or stopped. */
113
+ queued: number;
114
+ filed: number;
115
+ invalid: number;
116
+ problem: string | null;
117
+ checkedAt: string | null;
118
+ };
119
+ /** How the intake stands for one project, from the store alone: a page reads it on every render, so it never reads the
120
+ * project's folder (one that doesn't answer must not hold the console). Whether it uses youiyoux is the last pass's. */
121
+ export declare function youiyouxState(store: Store, repo: string): YouiyouxState;
122
+ /** The one line `toolroll onboard` says for a project that uses youiyoux; null when it doesn't. */
123
+ export declare function youiyouxOnboardLine(store: Store, repo: string): string | null;
124
+ /** The console the links name: the address the installation names (CONSOLE_URL or the console-url file), else, with
125
+ * none named, this machine's own console where `up` serves it (loopback http only, never a guessed host); null without
126
+ * either. Either way http(s) with no credentials, query or fragment. */
127
+ export declare function consoleBase(env: Record<string, string | undefined>, dir: string | null): string | null;
128
+ /** The task's page in that console; null without one. */
129
+ export declare function taskLink(env: Record<string, string | undefined>, dir: string | null, taskId: string): string | null;
130
+ /**
131
+ * Write `<id>.task.json` beside its request, whole. The folders are checked again now (a link is refused), the request
132
+ * must still be a plain file, the target must be a plain file or absent, and the content goes to a fresh temporary file
133
+ * in the same folder (created exclusively, never through a link) that is then renamed over the target.
134
+ */
135
+ export declare function writeStatusFile(repo: string, body: YouiyouxRequestTask): {
136
+ ok: true;
137
+ } | {
138
+ ok: false;
139
+ problem: string;
140
+ };
141
+ /**
142
+ * Put an options file into the project's main checkout as `.youi/proposals/<id>.json`, whole, and never over a file
143
+ * already there (a person may have decided on it). The folders are checked as the status file's are (`.youi/proposals`
144
+ * is made, as a real folder, when missing), the content goes to a fresh temporary file in the same folder (created
145
+ * exclusively, never through a link), the folders are checked again, and the temporary file is linked into place, which
146
+ * fails rather than replace anything at that name, a link included. `same`: that exact content was already there;
147
+ * `taken`: something else was, and is left as it was.
148
+ */
149
+ export declare function writeProposalFile(repo: string, id: string, text: string): {
150
+ ok: true;
151
+ made: "written" | "same" | "taken";
152
+ } | {
153
+ ok: false;
154
+ problem: string;
155
+ };
156
+ /** `git`: the hardened git an options request's result is read with (landed-cleanup.ts gitOf), and `evidenceRoot`, where
157
+ * results' saved evidence is (a completion is bound to the receipt read with it). Without either, options wait. */
158
+ export type YouiyouxIo = {
159
+ dir: string | null;
160
+ env: Record<string, string | undefined>;
161
+ git?: Git;
162
+ evidenceRoot?: string | null;
163
+ };
164
+ /** `copied`: options files that went to the hub; `decided`: decisions in the hub Toolroll acted on. */
165
+ export type YouiyouxPass = {
166
+ filed: number;
167
+ skipped: number;
168
+ invalid: number;
169
+ updated: number;
170
+ copied: number;
171
+ decided: number;
172
+ problems: string[];
173
+ };
174
+ export declare const NO_YOUIYOUX_PASS: Readonly<YouiyouxPass>;
175
+ /** File one request's task, exactly once: the claim and the filing are one transaction (two passes, in two processes
176
+ * or one, make one task). */
177
+ export declare function fileYouiyouxRequest(store: Store, repo: string, request: YouiyouxRequest, now: Date): {
178
+ made: "filed";
179
+ taskId: string;
180
+ } | {
181
+ made: "seen";
182
+ } | {
183
+ made: "skipped";
184
+ note: string;
185
+ };
186
+ /**
187
+ * One message for what a pass filed, however many: the person's paired chats draw it as one card (chat-decide.ts) that
188
+ * lists each new task, with Approve all and each one's own Approve and Open, and a phone push links to what needs
189
+ * them. Keyed by the pass's first task, so a pass says it once; the tasks are the ones filed with it (one project, one
190
+ * moment: requestPassOf). Titles are cleaned outside text, as filed.
191
+ */
192
+ export declare function noteYouiyouxFiled(store: Store, taskIds: readonly string[], now: Date): void;
193
+ /** What a pass's tasks were asked for, in a word: "change request", "options request", or "request" for both. */
194
+ export declare function requestKindWord(store: Store, taskIds: readonly string[]): string;
195
+ /** The tasks one pass filed beside this one (one project, one moment), in request order; empty for any other task. */
196
+ export declare function requestPassOf(store: Store, taskId: string): string[];
197
+ /**
198
+ * One project's pass: read new requests and file them, then write each filed task's status where it changed (no
199
+ * options moved: runYouiyoux does that too). The folder read is the project in Toolroll's records that `given`
200
+ * resolves to; anything else is left alone.
201
+ */
202
+ export declare function runYouiyouxIntake(store: Store, given: string, now: Date, io: YouiyouxIo, options?: {
203
+ force?: boolean;
204
+ }): YouiyouxPass;
205
+ /** The worker's pass for one project (flow-cadence.ts): the intake, then each options request's options, both ways. */
206
+ export declare function runYouiyoux(store: Store, given: string, now: Date, io: YouiyouxIo, options?: {
207
+ force?: boolean;
208
+ }): Promise<YouiyouxPass>;
209
+ export type OptionsRow = {
210
+ request: string;
211
+ taskId: string;
212
+ proposal: string;
213
+ state: string | null;
214
+ run: number | null;
215
+ digest: string | null;
216
+ };
217
+ /** The digest of an options file apart from what a decision changes: the proposal itself, every other key included. */
218
+ export declare function proposalIdentity(raw: unknown): string | null;
219
+ /** Why an agent's options file can't go to the hub as it stands, or null. Toolroll checks what makes it safe to copy
220
+ * and this request's; youiyoux checks each option against the component's contract when it reads it. */
221
+ export declare function proposalProblem(text: string, proposal: string, request: string): string | null;
222
+ export type Decision = {
223
+ status: "chosen" | "dismissed";
224
+ choice: string | null;
225
+ label: string | null;
226
+ by: string | null;
227
+ at: string | null;
228
+ };
229
+ /**
230
+ * A person decided in the hub: mark the task complete as landed work is, bound to the exact result whose options were
231
+ * copied, through every guard a person's completion passes, or leave it Ready for review and say why. Once per request,
232
+ * in one transaction with the read. Waits (false) while the task isn't Ready, Complete or cancelled.
233
+ */
234
+ export declare function actOnDecision(store: Store, repo: string, row: OptionsRow, decision: Decision, now: Date, root: string): boolean;