toolroll 0.9.85 → 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 (79) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  4. package/dist/assignment.js +4 -3
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +31 -29
  7. package/dist/browser-workspace.d.ts +40 -0
  8. package/dist/chat-actions.d.ts +11 -1
  9. package/dist/chat-actions.js +53 -1
  10. package/dist/chat-channel.js +1 -0
  11. package/dist/chat-quiet.js +3 -2
  12. package/dist/claim.d.ts +3 -0
  13. package/dist/claim.js +4 -1
  14. package/dist/contracts/chat-actions.d.ts +9 -0
  15. package/dist/contracts/chat-actions.js +3 -0
  16. package/dist/contracts/console-api.d.ts +4 -1
  17. package/dist/contracts/console-api.js +4 -1
  18. package/dist/contracts/flow-propose.d.ts +4 -1
  19. package/dist/contracts/flow-propose.js +1 -1
  20. package/dist/contracts/flow.d.ts +41 -0
  21. package/dist/contracts/flow.js +4 -2
  22. package/dist/contracts/lead-tools.d.ts +21 -0
  23. package/dist/contracts/lead-tools.js +2 -0
  24. package/dist/contracts/store-json.d.ts +2 -0
  25. package/dist/contracts/trigger-payloads.d.ts +20 -0
  26. package/dist/contracts/trigger-payloads.js +4 -0
  27. package/dist/coordinator.js +1 -0
  28. package/dist/deploy-gate.js +4 -0
  29. package/dist/dispatch.d.ts +3 -0
  30. package/dist/dispatch.js +4 -1
  31. package/dist/flow-browser.d.ts +103 -0
  32. package/dist/flow-browser.js +204 -0
  33. package/dist/flow-code.d.ts +5 -1
  34. package/dist/flow-code.js +4 -1
  35. package/dist/flow-engine.js +16 -0
  36. package/dist/flow-live.js +9 -1
  37. package/dist/flow-pull-request.js +15 -7
  38. package/dist/flow-share.js +3 -1
  39. package/dist/flow-steps.d.ts +9 -1
  40. package/dist/flow-steps.js +55 -5
  41. package/dist/flow-triggers.js +55 -0
  42. package/dist/flows-cli.d.ts +19 -0
  43. package/dist/flows-cli.js +47 -2
  44. package/dist/flows-ui.js +11 -2
  45. package/dist/flows.js +5 -2
  46. package/dist/lead-tools.js +35 -6
  47. package/dist/lead-voice.js +16 -5
  48. package/dist/operate.d.ts +1 -1
  49. package/dist/operate.js +215 -6
  50. package/dist/project-delete.js +7 -3
  51. package/dist/pull-request-flow.js +35 -4
  52. package/dist/result-follow-ups.js +4 -0
  53. package/dist/scope.d.ts +5 -0
  54. package/dist/scope.js +7 -0
  55. package/dist/server/chat.js +4 -2
  56. package/dist/server/console.css +29 -0
  57. package/dist/server/flows.js +15 -0
  58. package/dist/server/render-pages.d.ts +13 -0
  59. package/dist/server/render-pages.js +45 -2
  60. package/dist/server/render-tasks.d.ts +8 -0
  61. package/dist/server/render-tasks.js +17 -6
  62. package/dist/server/route-table.js +11 -0
  63. package/dist/server/task-views.js +5 -0
  64. package/dist/server/tasks.d.ts +10 -0
  65. package/dist/server/tasks.js +54 -2
  66. package/dist/settings-proofs.js +1 -0
  67. package/dist/store.d.ts +71 -3
  68. package/dist/store.js +222 -20
  69. package/dist/surface.js +18 -2
  70. package/dist/task-archive.d.ts +10 -0
  71. package/dist/task-archive.js +16 -0
  72. package/dist/task-control.d.ts +1 -1
  73. package/dist/tidy.d.ts +130 -0
  74. package/dist/tidy.js +503 -0
  75. package/dist/work-index.d.ts +24 -0
  76. package/dist/work-index.js +52 -30
  77. package/dist/workspace-ui.d.ts +1 -1
  78. package/dist/workspace-ui.js +3 -2
  79. package/package.json +1 -1
package/dist/store.js CHANGED
@@ -9,6 +9,8 @@ import { isAcpProfile, isAcpProvider } from "./acp-agents.js";
9
9
  import { ACP_CONFORMANCE_VERSION, launchFromJson } from "./acp-attest.js";
10
10
  import { reclaimDatabase } from "./database-reclaim.js";
11
11
  import { BASELINE_SCHEMA, BASELINE_SCHEMA_VERSION } from "./store-baseline.js";
12
+ import { refArchived, taskArchived } from "./task-archive.js";
13
+ export { refArchived, taskArchived } from "./task-archive.js";
12
14
  import { CHAT_PROVIDERS, CHAT_SCHEMA } from "./contracts/chat-tables.js";
13
15
  import {} from "./chat-core.js";
14
16
  import { ChatMessages, TELEGRAM_HOLD_REASONS, TELEGRAM_SKIPPED_ELSEWHERE, TELEGRAM_SKIPPED_OTHER_CHAT, TELEGRAM_SKIPPED_QUIET, TELEGRAM_UNSETTLED } from "./chat-messages.js";
@@ -94,6 +96,7 @@ import { LEDGER_COLUMNS, LEDGER_SCHEMA, LEDGER_TABLE, LEDGER_V99_TABLE, installL
94
96
  import { PLAN_AUTO_SCHEMA } from "./plan-auto.js";
95
97
  import { CONSOLE_INSTANCE_SCHEMA } from "./console-instance.js";
96
98
  import { endUnattendedGrants, FLOW_UNATTENDED_SCHEMA, grantStandsSql } from "./flow-unattended.js";
99
+ import { FLOW_BROWSER_SCHEMA } from "./flow-browser.js";
97
100
  import { RECIPE_SCHEMA } from "./recipes.js";
98
101
  import { POLICY_SCHEMA, agentRefusal, approvalRefusal, policyParts, readPolicy, underCeiling, sessionCeilingRefusal } from "./policy.js";
99
102
  import { PROVIDER_AUTH_SCHEMA } from "./provider-auth.js";
@@ -1092,7 +1095,13 @@ const REGISTERED_AT_FILING = "again, as it was filed";
1092
1095
  // project's intake choice and last answer, and each request taken, with the task it became.
1093
1096
  // v143 adds, and changes no saved row: optional exact-screenshot context on result feedback
1094
1097
  // (diff_comment.image_json, V143_ADDED_COLUMNS; saved notes read NULL and keep their file/line target).
1095
- export const SCHEMA_VERSION = 143;
1098
+ // v144 adds, and changes no saved row: a person's approval of browser use in a flow's script zones (flow_browser_grant;
1099
+ // flow-browser.ts), the one sandbox rule a project check gets for Chromium; saved flows have none and get nothing.
1100
+ // v145 adds, and changes no saved row (docs/plans/task-tidy.md): put-away tasks (task_archive, one row per archived task
1101
+ // version; no row is every saved task: not archived), the completed workflow runs on a watched branch the checks
1102
+ // trigger saw (branch_check_run), and the reservations that hold work back from being put away (merge_in_flight,
1103
+ // deploy_in_flight, flow_step_task; V145_SCHEMA, new tables only).
1104
+ export const SCHEMA_VERSION = 145;
1096
1105
  /** v118: the tables merged into `credential`, in the order they move (a person's API tokens keep their ids first). */
1097
1106
  export const V118_MERGED_TABLES = Object.freeze({ api_token: "person", coordinator_credential: "coordinator", lead_credential: "lead" });
1098
1107
  /** The foreign keys that named a merged table, exactly as their DDL wrote them; each names `credential(id)` instead. */
@@ -1225,7 +1234,7 @@ const V115_DROPPED_REFERENCES = /\s+REFERENCES (?:contestant|contest|tournament_
1225
1234
  * unchanged), and adds the nullable column in V125_ADDED_COLUMNS, checked the same way. v126 only adds a new table
1226
1235
  * (task_source) and the columns in V126_ADDED_COLUMNS (null or 0), checked the same way.
1227
1236
  */
1228
- export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143]);
1237
+ export const UPDATE_SAFE_MIGRATIONS = Object.freeze([108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145]);
1229
1238
  /** v119: the columns it adds, by table, with the value every row saved before it reads (toolroll-update's HISTORY_RULES). */
1230
1239
  export const V119_ADDED_COLUMNS = Object.freeze({
1231
1240
  task_ref: { planning_generation: { definition: "INTEGER NOT NULL DEFAULT 0", saved: "0" } },
@@ -1365,6 +1374,55 @@ export const V140_ADDED_COLUMNS = Object.freeze({
1365
1374
  export const V143_ADDED_COLUMNS = Object.freeze({
1366
1375
  diff_comment: { image_json: { definition: "TEXT", saved: "NULL" } },
1367
1376
  });
1377
+ /** v145 (docs/plans/task-tidy.md): `task_archive` is a put-away task version. A row hides it from every list and count
1378
+ * and refuses every door that would dispatch, claim or approve it; restore deletes the row. Its history, results,
1379
+ * approvals and ledger stay exactly as they were. `merge_in_flight` is a direct merge between its last check and GitHub's
1380
+ * answer, owned by its token: a task can't be put away under it (a row older than 15 minutes is a crash, and no
1381
+ * longer counts). `deploy_in_flight` is a deployment whose candidate passed its gate (30 minutes, refreshed by every
1382
+ * phase that proves it again). `flow_step_task` is the tasks a flow step was admitted for, kept whatever the card does
1383
+ * next, so a step still running holds them. `branch_check_run` keeps each completed GitHub workflow run (pushed to
1384
+ * the watched branch of the watched repository) the "checks failed" trigger saw, green or red, so tidy can tell a
1385
+ * failure notice was fixed by a later commit. */
1386
+ export const V145_SCHEMA = `
1387
+ CREATE TABLE IF NOT EXISTS task_archive (
1388
+ task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
1389
+ by TEXT NOT NULL,
1390
+ by_kind TEXT NOT NULL CHECK (by_kind IN ('person','tidy')),
1391
+ rule TEXT,
1392
+ reason TEXT NOT NULL,
1393
+ at TEXT NOT NULL
1394
+ );
1395
+ CREATE TABLE IF NOT EXISTS branch_check_run (
1396
+ repo TEXT NOT NULL,
1397
+ run_id INTEGER NOT NULL,
1398
+ branch TEXT NOT NULL,
1399
+ workflow TEXT NOT NULL,
1400
+ head_sha TEXT NOT NULL,
1401
+ conclusion TEXT NOT NULL CHECK (conclusion IN ('success','failure')),
1402
+ run_created_at TEXT NOT NULL,
1403
+ observed_at TEXT NOT NULL,
1404
+ PRIMARY KEY (repo, run_id)
1405
+ );
1406
+ CREATE INDEX IF NOT EXISTS branch_check_run_by_workflow ON branch_check_run (repo, branch, workflow, run_created_at);
1407
+ CREATE TABLE IF NOT EXISTS merge_in_flight (
1408
+ publication INTEGER PRIMARY KEY REFERENCES publication(id),
1409
+ task_ref INTEGER NOT NULL REFERENCES task_ref(id),
1410
+ token TEXT NOT NULL,
1411
+ by TEXT NOT NULL,
1412
+ at TEXT NOT NULL
1413
+ );
1414
+ CREATE TABLE IF NOT EXISTS deploy_in_flight (
1415
+ task_ref INTEGER PRIMARY KEY REFERENCES task_ref(id),
1416
+ run INTEGER NOT NULL,
1417
+ at TEXT NOT NULL
1418
+ );
1419
+ CREATE TABLE IF NOT EXISTS flow_step_task (
1420
+ card INTEGER NOT NULL,
1421
+ entry INTEGER NOT NULL,
1422
+ task_id TEXT NOT NULL,
1423
+ PRIMARY KEY (card, entry, task_id)
1424
+ );
1425
+ `;
1368
1426
  /**
1369
1427
  * v135: one approval per release (docs/plans/release-once.md). `release_authority` is the single source of truth for a
1370
1428
  * release a person approved: one row per generation, changed only by compare-and-swap on `rev`, with a lease whose
@@ -4622,6 +4680,10 @@ CREATE INDEX IF NOT EXISTS run_checkpoint_by_task ON run_checkpoint (task_ref, i
4622
4680
  for (const [column, { definition }] of Object.entries(columns))
4623
4681
  addColumn(db, table, column, definition);
4624
4682
  }
4683
+ // v144: approvals of browser use in flows' script zones (a new table only).
4684
+ db.exec(FLOW_BROWSER_SCHEMA);
4685
+ // v145: put-away tasks, green checks per branch and the reservations that hold work back (new tables only).
4686
+ db.exec(V145_SCHEMA);
4625
4687
  // Keep the epoch until reclamation succeeds, including a retry after compaction already committed. A new file has
4626
4688
  // nothing to reclaim.
4627
4689
  if (!fresh)
@@ -6361,6 +6423,7 @@ export class Store {
6361
6423
  FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
6362
6424
  WHERE task.state = 'queued'
6363
6425
  AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
6426
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
6364
6427
  ORDER BY task.priority DESC, task.created_at, task_ref.id
6365
6428
  LIMIT 200`)
6366
6429
  .all(stamp, BUILT_IN, repo, repo)
@@ -6385,7 +6448,8 @@ export class Store {
6385
6448
  .prepare(`SELECT task.priority AS priority, task.created_at AS created_at, task_ref.id AS ref,
6386
6449
  task_ref.repo AS repo, task_ref.assigned_runner AS assigned
6387
6450
  FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
6388
- WHERE task.id = ? AND task.state = 'queued'`)
6451
+ WHERE task.id = ? AND task.state = 'queued'
6452
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
6389
6453
  .get(BUILT_IN, taskId);
6390
6454
  if (me === undefined)
6391
6455
  return null;
@@ -6397,7 +6461,8 @@ export class Store {
6397
6461
  OR (task.priority = ? AND task.created_at = ? AND task_ref.id < ?)
6398
6462
  THEN 1 ELSE 0 END) AS ahead
6399
6463
  FROM task JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
6400
- WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)`)
6464
+ WHERE task.state = 'queued' AND (task_ref.assigned_runner IS ?) AND (task_ref.repo IS ?)
6465
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
6401
6466
  .get(Number(me["priority"]), Number(me["priority"]), String(me["created_at"]), Number(me["priority"]), String(me["created_at"]), Number(me["ref"]), BUILT_IN, me["assigned"], me["repo"]);
6402
6467
  return {
6403
6468
  position: Number(row?.["ahead"] ?? 0) + 1,
@@ -6547,6 +6612,8 @@ export class Store {
6547
6612
  JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
6548
6613
  WHERE task.state = 'queued'
6549
6614
  ${reservation}
6615
+ -- A put-away task (v145) never dispatches.
6616
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
6550
6617
  AND NOT EXISTS (
6551
6618
  SELECT 1 FROM hold
6552
6619
  WHERE hold.task_ref = task_ref.id
@@ -7328,6 +7395,9 @@ export class Store {
7328
7395
  // road it came by.
7329
7396
  if (basis === undefined && machineActing())
7330
7397
  return false;
7398
+ // A PUT-AWAY TASK (v145, task-tidy.md) is never approved, by any road: restore it first.
7399
+ if (taskArchived(this, taskId))
7400
+ return false;
7331
7401
  // A RELEASE WAITING FOR CI (Codex review round 1, finding 4): filed while its pull request's checks still ran,
7332
7402
  // only the automatic road (which proves CI itself) seals it until the plane read exact-head CI passing.
7333
7403
  if (basis?.kind !== "auto-check" && basis?.kind !== "auto-green" && releaseCiHoldOf(this, taskId) !== null)
@@ -10587,7 +10657,7 @@ export class Store {
10587
10657
  versions.sort((a, b) => a.id === root.id ? -1 : b.id === root.id ? 1 : a.createdAt.localeCompare(b.createdAt) || a.refId - b.refId);
10588
10658
  const current = versions.at(-1);
10589
10659
  return { root, current, versions,
10590
- otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running")),
10660
+ otherActive: versions.filter(one => one.id !== current.id && (one.state === "queued" || one.state === "running") && !refArchived(this, one.refId)),
10591
10661
  problem: Number(rows[0]["broken"]) === 0 ? null : "Task history is unavailable or incomplete. This task is shown separately." };
10592
10662
  }
10593
10663
  /** Group and filter BEFORE the family limit; hydrate only that page's
@@ -10597,6 +10667,8 @@ export class Store {
10597
10667
  const order = options.order === "updated" ? "updated_at" : "created_at";
10598
10668
  const rows = this.db.prepare(`${projection.sql}, selected AS MATERIALIZED (
10599
10669
  SELECT * FROM ordered WHERE version_rank = 1
10670
+ -- A put-away family (v145) is out of every list, before the page is cut.
10671
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)
10600
10672
  ${options.states === undefined ? "" : `AND state IN (${options.states.map(() => "?").join(",")})`}
10601
10673
  ORDER BY ${order} DESC, ref_id DESC LIMIT ?
10602
10674
  ) SELECT task.*, members.repo, members.ref_id, members.root_ref, members.broken
@@ -10687,7 +10759,8 @@ export class Store {
10687
10759
  COUNT(CASE WHEN live_families.root_ref IS NOT NULL THEN 1 END) AS running,
10688
10760
  COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'queued' THEN 1 END) AS queued,
10689
10761
  COUNT(CASE WHEN live_families.root_ref IS NULL AND ordered.state = 'done' AND ordered.updated_at >= ? THEN 1 END) AS doneRecently
10690
- FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1`)
10762
+ FROM ordered LEFT JOIN live_families ON live_families.root_ref = ordered.root_ref WHERE version_rank = 1
10763
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ordered.ref_id)`)
10691
10764
  .get(...projection.params, JSON.stringify(liveTasks), new Date(now.getTime() - 24 * 60 * 60_000).toISOString());
10692
10765
  return { running: Number(row["running"]), queued: Number(row["queued"]), doneRecently: Number(row["doneRecently"]) };
10693
10766
  }
@@ -11121,6 +11194,9 @@ export class Store {
11121
11194
  if (ref === undefined)
11122
11195
  return { ok: false, reason: "unknown-task" };
11123
11196
  const taskRef = Number(ref["id"]);
11197
+ // A put-away task (v145) is restored before it runs again.
11198
+ if (refArchived(this, taskRef))
11199
+ return { ok: false, reason: "archived" };
11124
11200
  const stamp = now.toISOString();
11125
11201
  if (this.hasLiveClaim(taskRef, now))
11126
11202
  return { ok: false, reason: "claimed" };
@@ -12732,10 +12808,12 @@ export class Store {
12732
12808
  : source !== undefined && "project" in source && source.project !== "" ? "project"
12733
12809
  : source !== undefined && "installation" in source ? "installation" : "unknown";
12734
12810
  const project = scope === "task" ? String(ref["repo"]) : scope === "project" && source !== undefined && "project" in source ? source.project : null;
12811
+ // A put-away task (v145) nudges nobody: no notification about it is enqueued, whoever produced it. The check is
12812
+ // part of the insert itself, so a put away that commits first always wins.
12735
12813
  const { changes, lastInsertRowid } = this.db
12736
12814
  .prepare(`INSERT OR IGNORE INTO notification (dedupe_key, kind, subject, body, created_at, push_class, link, provenance_scope, project, task_ref, task_id, source_run, recipient)
12737
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
12738
- .run(notification.dedupeKey, notification.kind, notification.subject, notification.body, now.toISOString(), notification.pushClass ?? null, notification.link ?? null, scope, project, ref === undefined ? null : taskRef, ref === undefined ? null : String(ref["external_id"]), run?.id ?? null, notification.recipient ?? null);
12815
+ SELECT ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? WHERE NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = ?)`)
12816
+ .run(notification.dedupeKey, notification.kind, notification.subject, notification.body, now.toISOString(), notification.pushClass ?? null, notification.link ?? null, scope, project, ref === undefined ? null : taskRef, ref === undefined ? null : String(ref["external_id"]), run?.id ?? null, notification.recipient ?? null, ref === undefined ? null : Number(ref["id"]));
12739
12817
  // Whose act made this fact: pings follow it (see pingAllowed).
12740
12818
  const actor = currentActor();
12741
12819
  if (Number(changes) > 0 && actor !== null) {
@@ -14227,6 +14305,7 @@ export class Store {
14227
14305
  JOIN task_ref ON task_ref.backend = ? AND task_ref.external_id = task.id
14228
14306
  WHERE (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
14229
14307
  AND (? IS NULL OR task.state = ?)
14308
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
14230
14309
  ORDER BY task.created_at DESC, task.id DESC LIMIT ?
14231
14310
  )
14232
14311
  UNION
@@ -14395,8 +14474,10 @@ export class Store {
14395
14474
  resolvePlanRevision(id, outcome, resolvedBy, now) {
14396
14475
  const { changes } = this.db
14397
14476
  .prepare(`UPDATE plan_revision SET status = ?, resolved_at = ?, resolved_by = ?
14398
- WHERE id = ? AND status = 'blocked'`)
14399
- .run(outcome, now.toISOString(), resolvedBy, id);
14477
+ WHERE id = ? AND status = 'blocked'
14478
+ -- A put-away task's (v145) plan change is never applied; rejecting it still is.
14479
+ AND (? = 'rejected' OR NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = plan_revision.task_ref))`)
14480
+ .run(outcome, now.toISOString(), resolvedBy, id, outcome);
14400
14481
  return Number(changes) > 0;
14401
14482
  }
14402
14483
  /** Stamp the exact plan revision and authority snapshot a run's brief was
@@ -15626,6 +15707,12 @@ export class Store {
15626
15707
  if (row === undefined || String(row["state"]) !== "claimed" || Number(row["generation"]) !== generation) {
15627
15708
  return { ok: false, outcome: "raced", why: "the claim moved" };
15628
15709
  }
15710
+ // A put-away task (v145) merges nothing: the intent waits for a person, who restores it first.
15711
+ const archivedTask = this.db.prepare("SELECT 1 FROM publication p JOIN task_archive a ON a.task_ref = p.task_ref WHERE p.id = ?").get(Number(row["publication"]));
15712
+ if (archivedTask !== undefined) {
15713
+ this.settleMergeIntent(id, generation, "waiting-human", now, { error: "the task is put away", from: "claimed", bumpGeneration: true });
15714
+ return { ok: false, outcome: "waiting-human", why: "the task is put away" };
15715
+ }
15629
15716
  const supersede = (why) => {
15630
15717
  this.settleMergeIntent(id, generation, "superseded", now, { error: why, from: "claimed" });
15631
15718
  return { ok: false, outcome: "superseded", why };
@@ -17384,9 +17471,10 @@ export class Store {
17384
17471
  const page = Math.max(1, Math.min(Math.floor(limit), 50));
17385
17472
  // Roll-up admission binds BEFORE the LIMIT (Codex roll-up review,
17386
17473
  // finding 2): a repo outside the ceiling must not consume the page.
17387
- const admission = admitted === null
17474
+ // A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
17475
+ const admission = (admitted === null
17388
17476
  ? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
17389
- : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`;
17477
+ : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
17390
17478
  const params = admitted === null ? [repo, repo] : admitted;
17391
17479
  return this.db
17392
17480
  .prepare(`SELECT task.id AS task_id, task.title, task_scope.goal, task_scope.digest, task_scope.proposed_at, task_ref.repo AS task_repo
@@ -17416,9 +17504,10 @@ export class Store {
17416
17504
  */
17417
17505
  listRequeueablesScoped(repo, now, limit = 10, admitted = null) {
17418
17506
  const page = Math.max(1, Math.min(Math.floor(limit), 50));
17419
- const admission = admitted === null
17507
+ // A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
17508
+ const admission = (admitted === null
17420
17509
  ? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
17421
- : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`;
17510
+ : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
17422
17511
  const params = admitted === null ? [repo, repo] : admitted;
17423
17512
  return this.db
17424
17513
  .prepare(`WITH open_incidents AS (
@@ -17482,6 +17571,8 @@ export class Store {
17482
17571
  JOIN task_ref AS blocker_ref ON blocker_ref.backend = ? AND blocker_ref.external_id = task_edge.blocker
17483
17572
  WHERE ${admissionOf("dependent_ref")}
17484
17573
  AND ${admissionOf("blocker_ref")}
17574
+ -- Put-away tasks (v145) need no repair card, on either side.
17575
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref IN (dependent_ref.id, blocker_ref.id))
17485
17576
  GROUP BY task_edge.blocker, dependent_ref.repo, blocker_ref.repo
17486
17577
  ORDER BY task_edge.blocker LIMIT ?`)
17487
17578
  .all(BUILT_IN, BUILT_IN, ...paramsOf(), ...paramsOf(), page)
@@ -17514,6 +17605,7 @@ export class Store {
17514
17605
  WHERE task.state = 'done'
17515
17606
  AND (? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)
17516
17607
  ${admission}
17608
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)
17517
17609
  )
17518
17610
  SELECT completed.*, run.id AS run_id, run.outcome, run.handoff, run.cost_usd, run.provider, run.auth_mode, run.started_at, run.finished_at,
17519
17611
  publication.state AS pub_state, publication.pr_number, publication.pr_url,
@@ -17620,9 +17712,10 @@ export class Store {
17620
17712
  // finding 11): in the roll-up, rows outside the ceiling must not
17621
17713
  // consume the page and crowd out admitted cards. Serve still filters
17622
17714
  // per-row afterward — this is the bound, that is the law.
17623
- const admission = admitted === null
17715
+ // A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
17716
+ const admission = (admitted === null
17624
17717
  ? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
17625
- : `(task_ref.repo IS NULL OR task_ref.repo IN (${admitted.map(() => "?").join(",")}))`;
17718
+ : `(task_ref.repo IS NULL OR task_ref.repo IN (${admitted.map(() => "?").join(",")}))`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
17626
17719
  const admissionParams = admitted === null ? [repo, repo] : admitted;
17627
17720
  const columns = `
17628
17721
  SELECT task.id, task.title, task.state, task.updated_at, task.priority, task_ref.strikes, task_ref.repo AS task_repo,
@@ -17826,14 +17919,15 @@ export class Store {
17826
17919
  SUM(CASE WHEN task.state = 'queued' THEN 1 ELSE 0 END) AS queued,
17827
17920
  SUM(CASE WHEN task.state = 'running' THEN 1 ELSE 0 END) AS running
17828
17921
  FROM task JOIN task_ref ON task_ref.backend = 'built-in' AND task_ref.external_id = task.id
17829
- WHERE task_ref.repo = ?`)
17922
+ WHERE task_ref.repo = ? AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
17830
17923
  .get(repo);
17831
17924
  const since = new Date(now.getTime() - 24 * 60 * 60_000).toISOString();
17832
17925
  const done = this.db
17833
17926
  .prepare(`SELECT COUNT(*) AS n FROM run
17834
17927
  JOIN task_ref ON task_ref.id = run.task_ref
17835
17928
  WHERE task_ref.repo = ? AND run.role IN ('builder','scout')
17836
- AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >= ?`)
17929
+ AND run.outcome IN ('built','no-change') AND run.finished_at IS NOT NULL AND run.finished_at >= ?
17930
+ AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)`)
17837
17931
  .get(repo, since);
17838
17932
  return { waiting: waiting, queued: Number(q.queued ?? 0), running: Number(q.running ?? 0), doneRecently: Number(done.n) };
17839
17933
  }
@@ -17842,9 +17936,10 @@ export class Store {
17842
17936
  // review, finding 11): with repo NULL the old predicate admitted every
17843
17937
  // stored project, ceiling or not. An empty admitted list reduces to
17844
17938
  // unplaced rows only — never IN ().
17845
- const admission = admitted === null
17939
+ // A put-away task (v145, task-tidy.md) is left out here, as from every list and count.
17940
+ const admission = (admitted === null
17846
17941
  ? "(? IS NULL OR task_ref.repo IS NULL OR task_ref.repo = ?)"
17847
- : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`;
17942
+ : `(task_ref.repo IS NULL${admitted.length === 0 ? "" : ` OR task_ref.repo IN (${admitted.map(() => "?").join(",")})`})`) + " AND NOT EXISTS (SELECT 1 FROM task_archive WHERE task_archive.task_ref = task_ref.id)";
17848
17943
  const params = () => (admitted === null ? [repo, repo] : [...admitted]);
17849
17944
  const row = this.db
17850
17945
  .prepare(`SELECT COUNT(*) AS n FROM (
@@ -18697,6 +18792,8 @@ export class Store {
18697
18792
  */
18698
18793
  resumeRunStop(args, now) {
18699
18794
  return this.transact(() => {
18795
+ if (refArchived(this, args.taskRef))
18796
+ return { ok: false, reason: "archived", detail: "this task is put away — restore it first" };
18700
18797
  const stop = this.stopOf(args.runId);
18701
18798
  const run = this.getRun(args.runId);
18702
18799
  if (stop === null || run === null)
@@ -21669,3 +21766,108 @@ export function dropFeedbackImage(store, runId, commentId, actor, unusable, now)
21669
21766
  return { ok: true, id };
21670
21767
  });
21671
21768
  }
21769
+ /** The put-away record of this task, or null when it is not put away. */
21770
+ export function archiveOf(store, taskId) {
21771
+ const row = store.handle.prepare(`SELECT a.*, r.external_id FROM task_archive a JOIN task_ref r ON r.id = a.task_ref
21772
+ WHERE r.backend = 'built-in' AND r.external_id = ?`).get(taskId);
21773
+ return row === undefined ? null : { taskRef: Number(row["task_ref"]), taskId: String(row["external_id"]), by: String(row["by"]),
21774
+ byKind: row["by_kind"] === "tidy" ? "tidy" : "person", rule: row["rule"] == null ? null : String(row["rule"]), reason: String(row["reason"]), at: String(row["at"]) };
21775
+ }
21776
+ /** Every version of a task's family, by the same proven lineage the task page reads (`taskFamilyOf`: same project, each
21777
+ * revision bound to its parent's own run by its brief). A family whose lineage doesn't prove is just this task. */
21778
+ export function taskFamilyRefs(store, taskId) {
21779
+ const family = store.taskFamilyOf(taskId, null, true);
21780
+ return family === null ? [] : family.versions.map(one => ({ id: one.refId, taskId: one.id })).sort((a, b) => a.id - b.id);
21781
+ }
21782
+ /** The first family version with something still going on, or null: running, a live newest claim, an unfinished run,
21783
+ * a process Toolroll hasn't seen exit, a build whose processes were never recorded, a stop still settling, a follow-up
21784
+ * check asked for and not finished, a flow step running for its card, a release in flight (a live release approval
21785
+ * that names it, or a deploy or merge reserved, running or unknown), or a pull request merging (an intent claimed or
21786
+ * firing, or a person's merge between its last check and GitHub's answer). */
21787
+ export function liveFamilyAttempt(store, refs, now) {
21788
+ if (refs.length === 0)
21789
+ return null;
21790
+ const row = store.handle.prepare(`SELECT r.external_id FROM task_ref r JOIN task t ON t.id = r.external_id
21791
+ WHERE r.id IN (SELECT value FROM json_each(?)) AND (t.state = 'running'
21792
+ OR EXISTS (SELECT 1 FROM claim c WHERE c.task_ref = r.id AND c.released_at IS NULL AND c.expires_at > ?
21793
+ AND c.lease_generation = (SELECT MAX(n.lease_generation) FROM claim n WHERE n.task_ref = r.id))
21794
+ OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.outcome IS NULL)
21795
+ OR EXISTS (SELECT 1 FROM run_process p JOIN run x ON x.id = p.run WHERE x.task_ref = r.id AND p.exited_at IS NULL AND p.container_empty_at IS NULL)
21796
+ OR EXISTS (SELECT 1 FROM run x WHERE x.task_ref = r.id AND x.provider_started_at IS NOT NULL
21797
+ AND NOT EXISTS (SELECT 1 FROM run_process p WHERE p.run = x.id) AND NOT EXISTS (SELECT 1 FROM run_process_summary s WHERE s.run = x.id))
21798
+ OR EXISTS (SELECT 1 FROM run_stop s WHERE s.task_ref = r.id AND s.resumed_at IS NULL AND s.settled_at IS NULL)
21799
+ OR EXISTS (SELECT 1 FROM action_ledger q JOIN run x ON x.id = q.run_id WHERE x.task_ref = r.id AND q.action = 'checks requested'
21800
+ AND NOT EXISTS (SELECT 1 FROM action_ledger f WHERE f.run_id = q.run_id AND f.action = 'checks finished' AND f.outcome LIKE q.id || ':%'))
21801
+ OR EXISTS (SELECT 1 FROM flow_card c JOIN flow_step_run s ON s.card = c.id
21802
+ WHERE s.state = 'running' AND (c.task = r.external_id OR c.primary_task = r.external_id))
21803
+ OR EXISTS (SELECT 1 FROM flow_step_task t JOIN flow_step_run s ON s.card = t.card AND s.entry = t.entry
21804
+ WHERE s.state = 'running' AND t.task_id = r.external_id)
21805
+ OR EXISTS (SELECT 1 FROM deploy_in_flight d WHERE d.task_ref = r.id AND d.at > ?)
21806
+ OR EXISTS (SELECT 1 FROM release_authority a WHERE a.state IN ('open','deploying','merge-reserved') AND (a.anchor_task = r.external_id OR a.tip_task = r.external_id))
21807
+ OR EXISTS (SELECT 1 FROM release_operation o WHERE o.task_id = r.external_id AND o.state IN ('reserved','running','unknown'))
21808
+ OR EXISTS (SELECT 1 FROM merge_intent i JOIN publication p ON p.id = i.publication WHERE p.task_ref = r.id AND i.state IN ('claimed','firing'))
21809
+ OR EXISTS (SELECT 1 FROM merge_in_flight m WHERE m.task_ref = r.id AND m.at > ?))
21810
+ LIMIT 1`).get(JSON.stringify(refs), now.toISOString(), new Date(now.getTime() - 30 * 60_000).toISOString(), new Date(now.getTime() - 15 * 60_000).toISOString());
21811
+ return row === undefined ? null : String(row["external_id"]);
21812
+ }
21813
+ /** Puts a task away (v145): every version of its family, in one write transaction that re-checks it has no live
21814
+ * attempt, that its versions are exactly `expect` when the caller showed a person a family (a preview), and that
21815
+ * `guard` (the caller's own rule, re-read under the lock) still allows it. Its state, history, results, approvals and
21816
+ * ledger are untouched; its open notifications are resolved; one ledger row records who and why. */
21817
+ export function archiveTaskFamily(store, taskId, by, now, options = {}) {
21818
+ return store.transact(() => {
21819
+ const family = taskFamilyRefs(store, taskId);
21820
+ if (family.length === 0)
21821
+ return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
21822
+ const refs = family.map(one => one.id);
21823
+ if (options.expect !== undefined && [...options.expect].sort((a, b) => a - b).join(",") !== refs.join(",")) {
21824
+ return { ok: false, reason: "blocked", message: `${taskId}'s versions changed since the list was shown.` };
21825
+ }
21826
+ // Already put away, every version: nothing to do. A family a person revived with a new revision puts away the rest.
21827
+ const remaining = refs.filter(ref => !refArchived(store, ref));
21828
+ if (remaining.length === 0)
21829
+ return { ok: false, reason: "archived", message: `${taskId} is already put away.` };
21830
+ const live = liveFamilyAttempt(store, refs, now);
21831
+ if (live !== null)
21832
+ return { ok: false, reason: "live", message: `${taskId} still has work going on. Stop it or let it finish, then put it away.` };
21833
+ const blocked = options.guard?.(refs) ?? null;
21834
+ if (blocked !== null)
21835
+ return { ok: false, reason: "blocked", message: blocked };
21836
+ const reason = by.reason.replace(/[\u0000-\u001f\u007f]+/g, " ").trim().slice(0, 300) || "Put away";
21837
+ const at = now.toISOString();
21838
+ const insert = store.handle.prepare("INSERT INTO task_archive (task_ref, by, by_kind, rule, reason, at) VALUES (?, ?, ?, ?, ?, ?)");
21839
+ for (const ref of remaining)
21840
+ insert.run(ref, by.by, by.byKind, by.rule, reason, at);
21841
+ store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND task_ref IN (SELECT value FROM json_each(?))")
21842
+ .run(at, JSON.stringify(refs));
21843
+ // The family's flow cards hold while it is put away, so what they queued for people is settled too.
21844
+ store.handle.prepare(`UPDATE notification SET resolved_at = ? WHERE resolved_at IS NULL AND EXISTS (SELECT 1 FROM flow_card c
21845
+ WHERE (c.task IN (SELECT value FROM json_each(?)) OR c.primary_task IN (SELECT value FROM json_each(?)))
21846
+ AND (notification.dedupe_key LIKE 'flow:' || c.id || ':%' OR notification.dedupe_key LIKE 'flow-card:' || c.id || ':%'))`)
21847
+ .run(at, JSON.stringify(family.map(one => one.taskId)), JSON.stringify(family.map(one => one.taskId)));
21848
+ const repo = store.refById(refs[0])?.repo ?? null;
21849
+ store.recordAction({ at, actor: by.byKind === "tidy" ? "tidy" : `operator:${by.by}`, repo, taskId, runId: null, action: "task archived",
21850
+ outcome: by.rule ?? "by hand", source: "work", detail: `${reason}${refs.length > 1 ? ` (${refs.length} versions)` : ""}` });
21851
+ return { ok: true, refs };
21852
+ });
21853
+ }
21854
+ /** Brings a put-away task back (v145): every version of its family, with a ledger row. Tidy never puts a restored
21855
+ * task away again (tidy.ts reads the `task restored` row). */
21856
+ export function restoreTaskFamily(store, taskId, by, now, guard) {
21857
+ return store.transact(() => {
21858
+ const family = taskFamilyRefs(store, taskId);
21859
+ if (family.length === 0)
21860
+ return { ok: false, reason: "unknown-task", message: `No task ${taskId}.` };
21861
+ const refs = family.map(one => one.id).filter(ref => refArchived(store, ref));
21862
+ if (refs.length === 0)
21863
+ return { ok: false, reason: "not-archived", message: `${taskId} isn't put away.` };
21864
+ const blocked = guard?.(refs) ?? null;
21865
+ if (blocked !== null)
21866
+ return { ok: false, reason: "blocked", message: blocked };
21867
+ store.handle.prepare("DELETE FROM task_archive WHERE task_ref IN (SELECT value FROM json_each(?))").run(JSON.stringify(refs));
21868
+ const at = now.toISOString();
21869
+ store.recordAction({ at, actor: `operator:${by}`, repo: store.refById(refs[0])?.repo ?? null, taskId, runId: null, action: "task restored",
21870
+ outcome: "recorded", source: "work", detail: refs.length > 1 ? `${refs.length} versions` : null });
21871
+ return { ok: true, refs };
21872
+ });
21873
+ }
package/dist/surface.js CHANGED
@@ -171,7 +171,7 @@ const GUIDE_ROWS = [
171
171
  notableReasons: ["no-result"] },
172
172
  { invocation: "task list", synopsis: "a page of saved task status", audience: "agent", agentMayInvoke: true, mutation: "none",
173
173
  flags: [jsonFlag, dbFlag, { name: "state", takesValue: true, meaning: "queued|running|done|failed|cancelled" },
174
- { name: "view", takesValue: true, meaning: "all|needs-you|running|completed" },
174
+ { name: "view", takesValue: true, meaning: "all|needs-you|running|completed|archived" },
175
175
  { name: "limit", takesValue: true, meaning: "page size, 1–100; default 40" },
176
176
  { name: "cursor", takesValue: true, meaning: "nextCursor from the previous response" },
177
177
  { name: "repo", takesValue: true, meaning: "filter to one project" }] },
@@ -299,6 +299,18 @@ const GUIDE_ROWS = [
299
299
  operator("task route", "which configured agents plan, build and repair this task; phase and size changes require renewed approval"),
300
300
  operator("task reopen", "resume external work its tracker closed and has been SEEN open again"),
301
301
  operator("task stop", "stop ONE exact live attempt (--run <id>): durable before any process is signalled, answers 'stopping' until its own processes are established gone; work, branch, and evidence preserved; no strike, no retry"),
302
+ { ...operator("task archive", "put tasks away (a person's act): out of every list, count and nudge, history and approvals kept; nothing claims, approves or dispatches them until restored"),
303
+ positionals: [{ name: "id", required: true, meaning: "one or more tasks (any version: the whole family goes)" }],
304
+ flags: [jsonFlag, dbFlag, { name: "reason", takesValue: true, meaning: "why, shown on the task page" }], notableReasons: ["unknown-task", "not-an-approver"] },
305
+ { ...operator("task restore", "bring put-away tasks back exactly as they were (a person's act)"),
306
+ positionals: [{ name: "id", required: true, meaning: "one or more put-away tasks" }], flags: [jsonFlag, dbFlag], notableReasons: ["unknown-task", "not-an-approver"] },
307
+ { ...operator("task tidy", "Put away old tasks: --preview lists what would go and why (read-only), --apply --digest <d> puts away exactly that list (a person's act)"),
308
+ flags: [jsonFlag, dbFlag, { name: "preview", takesValue: false, meaning: "show the list and the digest; changes nothing" },
309
+ { name: "apply", takesValue: false, meaning: "put away exactly the previewed list (needs --digest)" },
310
+ { name: "digest", takesValue: true, meaning: "the digest --preview printed" },
311
+ { name: "days", takesValue: true, meaning: "also finished tasks untouched this many days (1–365, default 14)" },
312
+ { name: "except", takesValue: true, meaning: "comma-separated tasks from the list to leave out" }],
313
+ notableReasons: ["usage", "not-an-approver", "empty"] },
302
314
  operator("run settle", "last resort for a finished run whose process witness can't be proven either way: records your --why in the ledger; refuses while any of the run's processes is alive"),
303
315
  operator("task resume", "resume the exact stopped attempt (--run <id>): refuses until it is quiescent, lifts only that stop's hold, approves nothing — the next pass re-proves the scope and inherits the draft with fresh proof"),
304
316
  // ---- leases (runner surface) ----
@@ -443,11 +455,15 @@ const REMOTE_NO = [
443
455
  "enroll", "revoke",
444
456
  // Flow steps that fetch addresses or run checks and scripts here.
445
457
  "flows import", "flows trigger check", "flows script save",
458
+ // Lets a flow script on this machine start Chromium in its sandbox: a person here, with their own password.
459
+ "flows browser",
446
460
  // Reads this machine owner's own Claude and Codex session files (~/.claude, ~/.codex).
447
461
  "memory propose",
448
462
  ];
449
463
  const REMOTE_STEP_UP = [
450
- "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle", "knowledge apply", "memory apply",
464
+ "proposals", "decide", "task regate", "task approve", "task accept", "task merge", "run settle",
465
+ // Putting tasks away and restoring them: a person's act, in the console or on the server's machine.
466
+ "task archive", "task restore", "task tidy", "knowledge apply", "memory apply",
451
467
  "flows card approve", "flows card send-back", "flows script approve", "flows unattended",
452
468
  "approver list", "approver add", "people list", "people invite", "people projects", "people revoke",
453
469
  // A person's own API tokens: a password step-up on the server's machine, or the console. Never with a token.
@@ -0,0 +1,10 @@
1
+ /** Put-away checks (v145, docs/plans/task-tidy.md), in a leaf module so every door can ask without loading the store. */
2
+ import type { Store } from "./store.js";
3
+ /** Whether this task version is put away. Every door that dispatches, claims or approves asks this (fail closed: a
4
+ * row means no). */
5
+ export declare function refArchived(store: Store, taskRef: number): boolean;
6
+ /** Whether the built-in task with this id is put away. */
7
+ export declare function taskArchived(store: Store, taskId: string): boolean;
8
+ /** Whether a flow card's work is put away: its current task or its first build task (a later stage keeps the first
9
+ * and may clear the current one). Read from the card's own row now, so a put away that committed first holds it. */
10
+ export declare function cardTaskArchived(store: Store, cardId: number): boolean;
@@ -0,0 +1,16 @@
1
+ /** Whether this task version is put away. Every door that dispatches, claims or approves asks this (fail closed: a
2
+ * row means no). */
3
+ export function refArchived(store, taskRef) {
4
+ return store.handle.prepare("SELECT 1 FROM task_archive WHERE task_ref = ?").get(taskRef) !== undefined;
5
+ }
6
+ /** Whether the built-in task with this id is put away. */
7
+ export function taskArchived(store, taskId) {
8
+ return store.handle.prepare(`SELECT 1 FROM task_archive a JOIN task_ref r ON r.id = a.task_ref
9
+ WHERE r.backend = 'built-in' AND r.external_id = ?`).get(taskId) !== undefined;
10
+ }
11
+ /** Whether a flow card's work is put away: its current task or its first build task (a later stage keeps the first
12
+ * and may clear the current one). Read from the card's own row now, so a put away that committed first holds it. */
13
+ export function cardTaskArchived(store, cardId) {
14
+ return store.handle.prepare(`SELECT 1 FROM flow_card c JOIN task_ref r ON r.backend = 'built-in' AND r.external_id IN (c.task, c.primary_task)
15
+ JOIN task_archive a ON a.task_ref = r.id WHERE c.id = ?`).get(cardId) !== undefined;
16
+ }
@@ -102,7 +102,7 @@ export type ResumeOutcome = {
102
102
  gate: DispatchDiagnosis | null;
103
103
  } | {
104
104
  ok: false;
105
- reason: "no-task" | "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "occupied";
105
+ reason: "no-task" | "no-stop" | "wrong-task" | "stopping" | "already-resumed" | "superseded" | "busy" | "review" | "occupied" | "archived";
106
106
  detail: string;
107
107
  };
108
108
  /**