atris 3.37.1 → 3.40.0

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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
package/commands/task.js CHANGED
@@ -5,8 +5,10 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const http = require('http');
8
+ const { execSync } = require('node:child_process');
8
9
  const path = require('path');
9
10
  const os = require('os');
11
+ const { hasFlag } = require('../lib/arg-parser');
10
12
  const { taskProofState } = require('../lib/task-proof');
11
13
  const {
12
14
  evaluateAutoAccept,
@@ -17,10 +19,11 @@ const {
17
19
  runVerifyCommandCached,
18
20
  DENIED_TAGS,
19
21
  } = require('../lib/auto-accept-certified');
22
+ const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
20
23
  const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
21
24
  const escapeRegExp = require('../lib/escape-regexp');
22
25
  const reviewIntegrity = require('../lib/review-integrity');
23
- const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
26
+ const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord, plainLandingReason } = require('../lib/voice-gate');
24
27
  const {
25
28
  normalizeOwnerSlug,
26
29
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
@@ -34,6 +37,11 @@ const {
34
37
  inspectTextLines,
35
38
  buildInspectPayload,
36
39
  } = require('../lib/inspect-fields');
40
+ const {
41
+ isDecisionTask,
42
+ decisionMarkerFor,
43
+ DECISION_REFUSE_REASON,
44
+ } = require('../lib/task-decision');
37
45
 
38
46
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
39
47
  || process.env.USER
@@ -128,7 +136,7 @@ function taskUsageText() {
128
136
  return `
129
137
  atris task - durable local task state (SQLite, gitignored)
130
138
 
131
- golden path (zero human turns):
139
+ golden path (one tick, by cron or by hand):
132
140
  atris task delegate "fix the login bug" --to <member>
133
141
  atris task claim <id> --as <member>
134
142
  ... build ...
@@ -136,7 +144,7 @@ atris task - durable local task state (SQLite, gitignored)
136
144
  atris autoland tick # second check runs, task lands
137
145
 
138
146
  atris task Show the task desk
139
- atris task new "<title>" Create a task
147
+ atris task new "<title>" [--verify <cmd>] Create a task; weak or missing checks are marked degraded
140
148
  atris task next [--tag <tag>] [--create-next]
141
149
  Claim/show next open task; optionally create the generated Endgame fallback
142
150
  atris task continue-work <id> Create/reuse a certified Review follow-up task
@@ -168,8 +176,8 @@ atris task - durable local task state (SQLite, gitignored)
168
176
  atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
169
177
  atris task revise <id> --note "..." Send reviewed work back to Do
170
178
 
171
- atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
172
- atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] Create assigned work
179
+ atris task add "<title>" [--tag <tag>] [--goal-id <id>] [--verify <cmd>] Create a task
180
+ atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] [--verify <cmd>] Create assigned work
173
181
  atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
174
182
  Record a task-owned Plan stage
175
183
  atris task do <id> --as <owner> --first-move "..."
@@ -212,6 +220,7 @@ atris task - durable local task state (SQLite, gitignored)
212
220
  Sweep off-roadmap/duplicate work as archived (not failed);
213
221
  --from-failed opts in to relabel a fail-closed row (never done)
214
222
  atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
223
+ atris task reap-mission-blockers [--json] Close blocker rows whose missions are complete or stopped
215
224
  atris task relabel-archived [--dry-run|--apply]
216
225
  One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
217
226
  atris task finish <id> --proof "..." Legacy alias for done with proof
@@ -269,10 +278,6 @@ function flag(args, name) {
269
278
  return args[i + 1] || true;
270
279
  }
271
280
 
272
- function hasFlag(args, name) {
273
- return args.indexOf(name) !== -1;
274
- }
275
-
276
281
  function taskScopeEverywhere(args = [], options = {}) {
277
282
  if (options.everywhere !== undefined) return Boolean(options.everywhere);
278
283
  return hasFlag(args, '--everywhere');
@@ -880,10 +885,15 @@ function certifiedReviewNextAction(nextTaskTitle) {
880
885
  function proofBoundaryBlockedEvaluation(task) {
881
886
  const missionXpBoundary = missionXpProofBoundaryEvaluation(task);
882
887
  if (missionXpBoundary) return missionXpBoundary;
883
- // strictVerify stays off here: this is a render-path probe for the boundary
884
- // reason only, and the default-true strict mode would spawn the verify
885
- // subprocess for every certified row just to draw the desk.
886
- const evaluation = evaluateAutoAccept(task, { strictVerify: false, minPasses: 0 });
888
+ // This is a render-path probe for the boundary reason only. strictVerify
889
+ // alone is not enough to keep it read-only because probationary actors
890
+ // still require strict verification. Never execute a stored verifier while
891
+ // drawing task reviews, status, queues, or projections.
892
+ const evaluation = evaluateAutoAccept(task, {
893
+ strictVerify: false,
894
+ minPasses: 0,
895
+ executeVerify: false,
896
+ });
887
897
  return evaluation && evaluation.reason === 'proof_unmerged_or_draft_pr_boundary'
888
898
  ? evaluation
889
899
  : null;
@@ -1583,9 +1593,74 @@ function enrichTaskProjection(projection) {
1583
1593
  },
1584
1594
  streams: buildTaskStreams(enrichedTasks, goalSource.goals),
1585
1595
  tasks: enrichedTasks,
1596
+ missions: projectionMissions(root),
1597
+ wishes: projectionWishes(root),
1586
1598
  };
1587
1599
  }
1588
1600
 
1601
+ // The desktop app renders projection.missions but the writer never filled it,
1602
+ // so live workers were invisible on every dashboard. Best-effort: a broken
1603
+ // mission or wish store must never take the task projection down with it.
1604
+ const PROJECTION_MISSION_LIMIT = 50;
1605
+ const PROJECTION_WISH_LIMIT = 30;
1606
+ const PROJECTION_CLIP = 240;
1607
+
1608
+ function clipForProjection(value) {
1609
+ const text = String(value || '');
1610
+ return text.length > PROJECTION_CLIP ? `${text.slice(0, PROJECTION_CLIP - 3)}...` : text;
1611
+ }
1612
+
1613
+ function projectionMissions(root) {
1614
+ try {
1615
+ const { listMissions } = require('./mission.js');
1616
+ const recentCutoff = Date.now() - 48 * 60 * 60 * 1000;
1617
+ return listMissions(root)
1618
+ .filter(mission => {
1619
+ const status = String(mission.status || '');
1620
+ if (!['complete', 'stopped'].includes(status)) return true;
1621
+ const updated = Date.parse(mission.updated_at || mission.created_at || '') || 0;
1622
+ return updated >= recentCutoff;
1623
+ })
1624
+ .slice(0, PROJECTION_MISSION_LIMIT)
1625
+ .map(mission => ({
1626
+ id: mission.id,
1627
+ objective: clipForProjection(String(mission.objective || '')),
1628
+ status: mission.status || null,
1629
+ owner: mission.owner || null,
1630
+ runner: mission.runner || null,
1631
+ cadence: mission.cadence || null,
1632
+ lane: mission.lane || null,
1633
+ next_action: clipForProjection(String(mission.next_action || '')),
1634
+ last_tick_at: mission.last_tick_at || null,
1635
+ last_tick_status: mission.last_tick_status || null,
1636
+ receipt_path: mission.receipt_path || null,
1637
+ wish_id: mission.wish_id || null,
1638
+ created_at: mission.created_at || null,
1639
+ updated_at: mission.updated_at || null,
1640
+ }));
1641
+ } catch {
1642
+ return [];
1643
+ }
1644
+ }
1645
+
1646
+ function projectionWishes(root) {
1647
+ try {
1648
+ const { readWishes } = require('../lib/wish-store');
1649
+ return readWishes(root)
1650
+ .filter(wish => String(wish.status || '') !== 'closed')
1651
+ .slice(-PROJECTION_WISH_LIMIT)
1652
+ .map(wish => ({
1653
+ id: wish.id,
1654
+ n: wish.n ?? null,
1655
+ text: clipForProjection(String(wish.text || '')),
1656
+ status: wish.status || null,
1657
+ created_at: wish.first_ts || wish.ts || null,
1658
+ }));
1659
+ } catch {
1660
+ return [];
1661
+ }
1662
+ }
1663
+
1589
1664
  function taskTypeForCloud(task) {
1590
1665
  const tag = String(task.tag || '').toLowerCase();
1591
1666
  if (['inbound', 'outbound', 'creative', 'improvement'].includes(tag)) return tag;
@@ -5105,13 +5180,49 @@ function evaluateReviewAutoAccept(task, root) {
5105
5180
  if (!protectedGate.ok) return { eligible: false, ref, reason: protectedGate.reason };
5106
5181
  const sizeGate = reviewAutoAcceptSizeGate(task, root);
5107
5182
  if (!sizeGate.ok) return { eligible: false, ref, reason: sizeGate.reason, size: sizeGate.stats };
5108
- const evaluation = evaluateAutoAccept(task, { strictVerify: true });
5183
+ // executeVerify: false — this is a READ path. The verdict comes from
5184
+ // metadata.verify_cache, stamped by the two lanes allowed to spawn
5185
+ // checks (certify-verified, autoland landing re-check). Spawning here is
5186
+ // what turned `atris task reviews` into a fork-bomb multiplier.
5187
+ const evaluation = evaluateAutoAccept(task, { strictVerify: true, executeVerify: false });
5109
5188
  if (!evaluation.eligible) {
5110
5189
  return {
5111
5190
  ...evaluation,
5112
5191
  size: sizeGate.stats,
5113
5192
  };
5114
5193
  }
5194
+ if (evaluation.verification_pending) {
5195
+ const verdict = cachedVerifyVerdict(task, root);
5196
+ if (!verdict.fresh) {
5197
+ return {
5198
+ eligible: false,
5199
+ ref,
5200
+ reason: 'verification_pending',
5201
+ verify: verdict.command || null,
5202
+ next_action: 'no fresh executed verify on record; `atris task certify-verified` or the autoland tick runs the stored check and stamps metadata.verify_cache',
5203
+ size: sizeGate.stats,
5204
+ };
5205
+ }
5206
+ if (!verdict.ok) {
5207
+ return {
5208
+ eligible: false,
5209
+ ref,
5210
+ reason: verdict.reason || 'verify_failed',
5211
+ verify: verdict.command,
5212
+ verify_cache: verdict,
5213
+ size: sizeGate.stats,
5214
+ };
5215
+ }
5216
+ return {
5217
+ ...evaluation,
5218
+ verification_pending: false,
5219
+ verify_cache: verdict,
5220
+ eligible: true,
5221
+ reason: 'certified_small',
5222
+ policy: REVIEW_AUTO_ACCEPT_POLICY,
5223
+ size: sizeGate.stats,
5224
+ };
5225
+ }
5115
5226
  return {
5116
5227
  ...evaluation,
5117
5228
  eligible: true,
@@ -5442,19 +5553,17 @@ function taskReviewLandingLines(item) {
5442
5553
  function cmdReviews(args) {
5443
5554
  const taskDb = getTaskDb();
5444
5555
  const db = taskDb.open();
5445
- let { projection, outPath } = writeDefaultProjection(taskDb, db);
5446
- const workspaceRoot = projection.workspace_root || process.cwd();
5447
- const previousAutoAcceptState = readReviewAutoAcceptState(workspaceRoot);
5448
- const autoAcceptRun = autoAcceptCertifiedSmallReviews(taskDb, db, projection);
5449
- if (autoAcceptRun.changed) {
5450
- ({ projection, outPath } = writeDefaultProjection(taskDb, db));
5451
- }
5452
- const autoAcceptRollup = reviewAutoAcceptRollup(taskDb, db, workspaceRoot, previousAutoAcceptState, autoAcceptRun);
5453
- writeReviewAutoAcceptState(workspaceRoot, {
5454
- ...previousAutoAcceptState,
5455
- last_look_at: new Date().toISOString(),
5456
- last_count: autoAcceptRollup.count,
5457
- });
5556
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
5557
+ // Reviews is a view, not an execution lane. Certification and landing are
5558
+ // persisted by certify-verified, explicit receipt/ready commands, sweep,
5559
+ // and autoland; this command only reports that stored state.
5560
+ const autoAcceptView = {
5561
+ enabled: false,
5562
+ read_only: true,
5563
+ accepted_now: 0,
5564
+ accepted_since_last_look: 0,
5565
+ results: [],
5566
+ };
5458
5567
  const groupByRaw = flag(args, '--group-by');
5459
5568
  if (groupByRaw) {
5460
5569
  const key = reviewGroupKey(groupByRaw);
@@ -5464,20 +5573,12 @@ function cmdReviews(args) {
5464
5573
  ok: true,
5465
5574
  action: 'review_groups',
5466
5575
  projection_path: outPath,
5467
- autoaccept: {
5468
- enabled: autoAcceptRun.enabled,
5469
- accepted_now: autoAcceptRun.accepted,
5470
- accepted_since_last_look: autoAcceptRollup.count,
5471
- results: autoAcceptRun.results,
5472
- },
5576
+ autoaccept: autoAcceptView,
5473
5577
  groups,
5474
5578
  });
5475
5579
  return;
5476
5580
  }
5477
5581
  console.log(gateForHuman(`${numberWord(groups.total_certified)} finished things are waiting, grouped by ${key}.`).text);
5478
- if (autoAcceptRollup.count > 0) {
5479
- console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5480
- }
5481
5582
  const visibleGroups = groups.groups.slice(0, reviewGroupTextLimit(args, groups.groups.length));
5482
5583
  visibleGroups.forEach((g, index) => {
5483
5584
  console.log('');
@@ -5498,12 +5599,7 @@ function cmdReviews(args) {
5498
5599
  ok: true,
5499
5600
  action: 'review_queue',
5500
5601
  projection_path: outPath,
5501
- autoaccept: {
5502
- enabled: autoAcceptRun.enabled,
5503
- accepted_now: autoAcceptRun.accepted,
5504
- accepted_since_last_look: autoAcceptRollup.count,
5505
- results: autoAcceptRun.results,
5506
- },
5602
+ autoaccept: autoAcceptView,
5507
5603
  queue,
5508
5604
  });
5509
5605
  return;
@@ -5512,9 +5608,6 @@ function cmdReviews(args) {
5512
5608
  const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
5513
5609
  if (!approvalItems.length && !blockedItems.length) {
5514
5610
  console.log('nothing is waiting on you. everything that finished has already landed.');
5515
- if (autoAcceptRollup.count > 0) {
5516
- console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5517
- }
5518
5611
  return;
5519
5612
  }
5520
5613
  if (queue.counts.certified > 0) {
@@ -5528,9 +5621,6 @@ function cmdReviews(args) {
5528
5621
  ? 'one more is almost ready; a second check is still running.'
5529
5622
  : `${numberWord(queue.counts.blocking)} more are almost ready; second checks are still running.`).text);
5530
5623
  }
5531
- if (autoAcceptRollup.count > 0) {
5532
- console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5533
- }
5534
5624
  approvalItems.forEach((item, index) => {
5535
5625
  if (index > 0) console.log('');
5536
5626
  const ref = item.display_id || taskRef(item.id);
@@ -5555,7 +5645,7 @@ function cmdReviews(args) {
5555
5645
  if (item.accept_command) {
5556
5646
  console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
5557
5647
  } else if (item.blocked_accept_reason) {
5558
- console.log(` approve: blocked (${item.blocked_accept_reason})`);
5648
+ console.log(` approve: blocked; ${plainLandingReason(item.blocked_accept_reason)}`);
5559
5649
  console.log(` rework: ${item.revise_command}`);
5560
5650
  }
5561
5651
  });
@@ -5764,7 +5854,8 @@ function formatTaskLine(task) {
5764
5854
  const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
5765
5855
  const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
5766
5856
  const tag = task.tag ? ` #${task.tag}` : '';
5767
- return `${taskRef(task)}${owner}${assigned}${tag} ${task.title}`;
5857
+ const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
5858
+ return `${taskRef(task)}${owner}${assigned}${tag}${decision} ${task.title}`;
5768
5859
  }
5769
5860
 
5770
5861
  function cmdStatus(args) {
@@ -5859,7 +5950,8 @@ function renderTaskDesk(rows, refRows = rows) {
5859
5950
  const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
5860
5951
  const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
5861
5952
  const tag = r.tag ? ` #${r.tag}` : '';
5862
- console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}`);
5953
+ const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
5954
+ console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}${decision}`);
5863
5955
  console.log(` ${r.title}`);
5864
5956
  }
5865
5957
  if (active.length === 0) console.log('clear no active tasks');
@@ -5877,12 +5969,14 @@ function cmdAdd(args) {
5877
5969
  const tag = flag(args, '--tag');
5878
5970
  const goalId = flag(args, '--goal-id');
5879
5971
  const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
5972
+ const verify = flag(args, '--verify');
5880
5973
  const metadata = {};
5881
5974
  if (goalId && goalId !== true) metadata.goal_id = String(goalId);
5882
5975
  if (goalObjective && goalObjective !== true) {
5883
5976
  metadata.task_goal = String(goalObjective);
5884
5977
  metadata.goal_objective = String(goalObjective);
5885
5978
  }
5979
+ if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
5886
5980
  const taskDb = getTaskDb();
5887
5981
  const db = taskDb.open();
5888
5982
  const ws = taskDb.workspaceRoot();
@@ -5949,6 +6043,7 @@ function delegateTask(args, options = {}) {
5949
6043
  const note = flag(args, '--note');
5950
6044
  const goalId = flag(args, '--goal-id');
5951
6045
  const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
6046
+ const verify = flag(args, '--verify');
5952
6047
  const claimNow = hasFlag(args, '--claim');
5953
6048
  const requestedOwner = flag(args, '--to') || flag(args, '--as');
5954
6049
  const explicitExecutedBy = flag(args, '--executed-by');
@@ -5986,6 +6081,7 @@ function delegateTask(args, options = {}) {
5986
6081
  metadata.task_goal = String(goalObjective);
5987
6082
  metadata.goal_objective = String(goalObjective);
5988
6083
  }
6084
+ if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
5989
6085
  const result = taskDb.addTask(db, {
5990
6086
  title,
5991
6087
  tag: typeof tag === 'string' ? tag : null,
@@ -6141,7 +6237,8 @@ function cmdDay(args) {
6141
6237
  for (const task of group.tasks) {
6142
6238
  const tag = task.tag ? ` #${task.tag}` : '';
6143
6239
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
6144
- console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${taskDayTitle(task.title)}`);
6240
+ const decision = decisionMarkerFor(task) ? ` ${decisionMarkerFor(task)}` : '';
6241
+ console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag}${decision} ${taskDayTitle(task.title)}`);
6145
6242
  }
6146
6243
  }
6147
6244
  if (textView.hiddenTasks > 0) {
@@ -6210,7 +6307,8 @@ function cmdList(args) {
6210
6307
  for (const r of displayRows) {
6211
6308
  const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
6212
6309
  const tag = r.tag ? ` #${r.tag}` : '';
6213
- console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}\t${r.title}`);
6310
+ const decision = decisionMarkerFor(r) ? ` ${decisionMarkerFor(r)}` : '';
6311
+ console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}${decision}\t${r.title}`);
6214
6312
  }
6215
6313
  }
6216
6314
 
@@ -6240,7 +6338,11 @@ function guardExplicitActor(command, value) {
6240
6338
  // instead of just reporting the failure, straight from the live projection
6241
6339
  // (never the rendered file), preferring the same tag when one is open.
6242
6340
  function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
6243
- const open = (projection && projection.tasks || []).filter((t) => t && t.status === 'open' && t.id !== excludeId);
6341
+ // Decision rows (needs-human / decision) belong to a human. Autonomous
6342
+ // recovery after already_done must not hand an agent a judgment call.
6343
+ const open = (projection && projection.tasks || []).filter((t) => (
6344
+ t && t.status === 'open' && t.id !== excludeId && !isDecisionTask(t)
6345
+ ));
6244
6346
  if (!open.length) return null;
6245
6347
  const normalizedTag = String(tag || '').trim().toLowerCase();
6246
6348
  if (normalizedTag) {
@@ -6490,13 +6592,31 @@ function cmdNext(args) {
6490
6592
  const reviewTasks = filterTasksByScope(reviewProjection.projection.tasks || [], scope)
6491
6593
  .map(compactTaskForStatus)
6492
6594
  .filter(task => task && task.review && task.review.handoff);
6493
- const open = filterTasksByScope(taskDb.listTasks(db, {
6595
+ const openAll = filterTasksByScope(taskDb.listTasks(db, {
6494
6596
  workspaceRoot: taskDb.workspaceRoot(),
6495
6597
  status: 'open',
6496
- limit: scoped ? null : 1,
6598
+ limit: scoped ? null : 50,
6497
6599
  }), scope);
6600
+ const open = openAll.filter((task) => !isDecisionTask(task));
6601
+ const skippedDecisions = openAll.length - open.length;
6498
6602
  if (!open.length) {
6499
6603
  const { projection, outPath } = reviewProjection;
6604
+ if (skippedDecisions > 0 && !reviewTasks.length) {
6605
+ if (wantsJson(args)) {
6606
+ printJson({
6607
+ ok: false,
6608
+ action: 'refused',
6609
+ reason: DECISION_REFUSE_REASON,
6610
+ skipped_decision_count: skippedDecisions,
6611
+ owner: String(owner),
6612
+ scope: normalizeTaskQueueScope(scope),
6613
+ projection_path: outPath,
6614
+ });
6615
+ process.exit(1);
6616
+ }
6617
+ console.error(DECISION_REFUSE_REASON);
6618
+ process.exit(1);
6619
+ }
6500
6620
  const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
6501
6621
  || reviewTasks.find(task => task.review.handoff.next_action === 'continue_work')
6502
6622
  || reviewTasks.find(task => task.review.handoff.next_action === 'human_accept_waiting');
@@ -7147,6 +7267,12 @@ function cmdShow(args) {
7147
7267
  : task.status.toUpperCase();
7148
7268
  console.log(`${statusLabel} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
7149
7269
  console.log(task.title);
7270
+ if (task.metadata?.verification_status === 'degraded') {
7271
+ const reason = task.metadata.verification_degraded_reason === 'diff_only_verify'
7272
+ ? 'diff-only verify command'
7273
+ : 'missing verify command';
7274
+ console.log(`verification: degraded (${reason})`);
7275
+ }
7150
7276
  if (task.review) {
7151
7277
  console.log('');
7152
7278
  printReviewLanding(task.review);
@@ -9145,6 +9271,36 @@ function cmdClearDone(args) {
9145
9271
  console.log(`cleared ${candidates.length} completed task(s).`);
9146
9272
  }
9147
9273
 
9274
+ function cmdReapMissionBlockers(args) {
9275
+ const taskDb = getTaskDb();
9276
+ const db = taskDb.open();
9277
+ const workspaceRoot = taskDb.workspaceRoot();
9278
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
9279
+ const missions = require('./mission').listMissions(workspaceRoot);
9280
+ const result = taskDb.reapMissionBlockerTasks(db, { workspaceRoot, missions, actor });
9281
+ const { outPath } = writeDefaultProjection(taskDb, db);
9282
+ const taskById = new Map(taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot, limit: null }))
9283
+ .map(task => [task.id, task]));
9284
+ const closed = result.closed.map(row => ({
9285
+ ...row,
9286
+ task_ref: taskRef(taskById.get(row.task_id)),
9287
+ }));
9288
+ if (wantsJson(args)) {
9289
+ printJson({
9290
+ ok: true,
9291
+ action: 'reaped_mission_blockers',
9292
+ closed_count: closed.length,
9293
+ closed,
9294
+ projection_path: outPath,
9295
+ });
9296
+ return;
9297
+ }
9298
+ console.log(`closed ${closed.length} mission blocker task(s).`);
9299
+ for (const row of closed) {
9300
+ console.log(`closed ${row.task_ref} because its mission is ${row.mission_status}.`);
9301
+ }
9302
+ }
9303
+
9148
9304
  // One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
9149
9305
  // reset" archived ~125 certified, proof-backed tasks by writing status
9150
9306
  // 'failed' (no distinct archived status existed yet). This relabels exactly
@@ -9231,6 +9387,20 @@ function cmdReady(args) {
9231
9387
  if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
9232
9388
  process.exit(1);
9233
9389
  }
9390
+ // Exit 0 says the check passed. It does not say the check could have
9391
+ // failed. Run it again with none of the work present: a check anchored to
9392
+ // this codebase fails there, and one that passes anywhere passes there too.
9393
+ if (!hasFlag(args, '--no-falsify-check')) {
9394
+ const { probeVerifierCanFail } = require('../lib/falsifier-probe');
9395
+ const probe = probeVerifierCanFail({ command: verifyFlag });
9396
+ if (probe.probed && probe.canFail === false) {
9397
+ console.error(`atris task ready: this check cannot fail — ${probe.reason}`);
9398
+ console.error(` command: ${verifyFlag}`);
9399
+ console.error(' give a check that fails when the work is missing, or pass --no-falsify-check to accept anyway.');
9400
+ process.exit(1);
9401
+ }
9402
+ if (!probe.probed && !wantsJson(args)) console.log(` ${probe.reason}`);
9403
+ }
9234
9404
  const base = proof.trim();
9235
9405
  proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
9236
9406
  if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
@@ -9303,7 +9473,10 @@ function cmdReady(args) {
9303
9473
  nextTask: nextTaskInput.nextTask,
9304
9474
  });
9305
9475
  const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
9306
- const autolandOn = require('../lib/autoland').liveAcceptAuthorization(taskDb.workspaceRoot()).ok;
9476
+ const autolandLib = require('../lib/autoland');
9477
+ const workspaceRoot = taskDb.workspaceRoot();
9478
+ const autolandOn = autolandLib.liveAcceptAuthorization(workspaceRoot).ok;
9479
+ const whenLands = autolandLib.whenAutolandLands(workspaceRoot);
9307
9480
  const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
9308
9481
  const handoff = {
9309
9482
  native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
@@ -9311,10 +9484,10 @@ function cmdReady(args) {
9311
9484
  next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
9312
9485
  rule: autolandOn
9313
9486
  ? (agentCertified
9314
- ? 'double-check complete; autoland will accept this on the next tick.'
9487
+ ? `double-check complete; autoland will accept this ${whenLands}.`
9315
9488
  : needsExternalVerifier
9316
9489
  ? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
9317
- : 'proof is ready; autoland runs the second check and lands it on the next tick.')
9490
+ : `proof is ready; autoland runs the second check and lands it ${whenLands}.`)
9318
9491
  : (agentCertified
9319
9492
  ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
9320
9493
  : 'proof is ready; one more agent check before human approval. XP waits for the human.'),
@@ -9440,6 +9613,28 @@ async function cmdAccept(args) {
9440
9613
  if (missionXpIssue) {
9441
9614
  failTask('atris task accept', MISSION_XP_END_TO_END_REASON, missionXpIssue);
9442
9615
  }
9616
+ // The proof must be able to fail. Run the stored verify command and refuse the
9617
+ // accept if it does not parse, does not run, or does not pass. Prose describing a
9618
+ // check is not a check.
9619
+ const acceptVerify = evaluateAcceptVerify(beforeTask, taskDb.workspaceRoot());
9620
+ const verifyOverride = hasFlag(args, '--accept-unverified');
9621
+ const overrideReason = String(flag(args, '--reason') || '').trim();
9622
+ if (!acceptVerify.ok && !verifyOverride) {
9623
+ failTask(
9624
+ 'atris task accept',
9625
+ acceptVerify.reason,
9626
+ `${acceptVerify.detail}. Stored verify: ${acceptVerify.command || '(none)'}. `
9627
+ + 'Fix the verify command and re-run, or accept deliberately with '
9628
+ + '--accept-unverified --reason "why this cannot be machine-checked".',
9629
+ );
9630
+ }
9631
+ if (!acceptVerify.ok && verifyOverride && !overrideReason) {
9632
+ failTask(
9633
+ 'atris task accept',
9634
+ 'unverified_accept_needs_reason',
9635
+ '--accept-unverified records an unfalsifiable accept in the ledger, so it requires --reason "<why>".',
9636
+ );
9637
+ }
9443
9638
  const readyReview = beforeTask?.review || {};
9444
9639
  const clearLesson = hasEmptyFlagValue(args, '--lesson');
9445
9640
  const clearNextTask = hasEmptyFlagValue(args, '--next');
@@ -9609,6 +9804,52 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
9609
9804
  `).run(JSON.stringify(metadata), Date.now(), taskId);
9610
9805
  }
9611
9806
 
9807
+ function currentHeadSha(workspaceRoot) {
9808
+ try {
9809
+ return execSync('git rev-parse HEAD', {
9810
+ cwd: workspaceRoot || process.cwd(),
9811
+ stdio: ['ignore', 'pipe', 'pipe'],
9812
+ encoding: 'utf8',
9813
+ }).trim();
9814
+ } catch {
9815
+ return null;
9816
+ }
9817
+ }
9818
+
9819
+ // metadata.verify_cache is the read path's only verify verdict: `reviews`
9820
+ // must never spawn a check (that read-side execution fork-bombed the fleet
9821
+ // on 2026-07-29), so the two lanes that legitimately execute verifies —
9822
+ // certify-verified and the autoland landing re-check — persist the outcome
9823
+ // here for the read path to consult.
9824
+ function stampVerifyCacheMetadata(taskDb, db, taskId, { verify, result, actor, workspaceRoot }) {
9825
+ const row = taskDb.getTask(db, taskId);
9826
+ if (!row) return;
9827
+ const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
9828
+ metadata.verify_cache = {
9829
+ command: verify,
9830
+ ok: result && result.ok === true,
9831
+ reason: (result && result.reason) || null,
9832
+ ran_at: new Date().toISOString(),
9833
+ ran_by: actor,
9834
+ code_sha: currentHeadSha(workspaceRoot),
9835
+ };
9836
+ db.prepare(`
9837
+ UPDATE tasks
9838
+ SET metadata = ?,
9839
+ updated_at = ?
9840
+ WHERE id = ?
9841
+ `).run(JSON.stringify(metadata), Date.now(), taskId);
9842
+ }
9843
+
9844
+ function cachedVerifyVerdict(task, root) {
9845
+ const metadata = (task && task.metadata) || {};
9846
+ const cache = metadata.verify_cache;
9847
+ if (!cache || typeof cache !== 'object') return { fresh: false, command: metadata.verify || null };
9848
+ const fresh = cache.command === metadata.verify
9849
+ && (!cache.code_sha || cache.code_sha === currentHeadSha(task.workspace_root || root));
9850
+ return { ...cache, fresh: Boolean(fresh) };
9851
+ }
9852
+
9612
9853
  // `atris task ready --verify "<cmd>"` already runs the command live and
9613
9854
  // gates on exit 0, but that alone leaves no machine re-runnable trace once
9614
9855
  // the proof text scrolls out of easy reach: storing the exact command on
@@ -9650,6 +9891,7 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
9650
9891
  };
9651
9892
  }
9652
9893
  const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
9894
+ stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result, actor, workspaceRoot: task.workspace_root });
9653
9895
  if (result.ok) return { ok: true, verify, result };
9654
9896
  const note = landingVerifyFailureNote(verify, result);
9655
9897
  const revised = taskDb.reviseTask(db, {
@@ -9719,8 +9961,10 @@ function cmdCertifyVerified(args, options = {}) {
9719
9961
  // "certified" alone is not landable.
9720
9962
  // strictVerify stays off in this eligibility probe: certify-verified runs
9721
9963
  // the check itself right below, and strict mode here would execute it a
9722
- // second time per row before the real run.
9723
- const evaluation = evaluateAutoAccept(task, { strictVerify: false });
9964
+ // second time per row before the real run. executeVerify: false as well —
9965
+ // an untrusted trust tier re-enables the strict block internally even
9966
+ // with strictVerify off, which is how this probe spawned a verify per row.
9967
+ const evaluation = evaluateAutoAccept(task, { strictVerify: false, executeVerify: false });
9724
9968
  if (evaluation.eligible) {
9725
9969
  results.push({ ref, action: 'skipped', reason: 'already_landable' });
9726
9970
  continue;
@@ -9749,6 +9993,7 @@ function cmdCertifyVerified(args, options = {}) {
9749
9993
  continue;
9750
9994
  }
9751
9995
  const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
9996
+ stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result: run, actor, workspaceRoot: task.workspace_root });
9752
9997
  if (!run.ok) {
9753
9998
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
9754
9999
  continue;
@@ -9927,11 +10172,19 @@ function cmdAutoAcceptCertified(args) {
9927
10172
  }
9928
10173
  const landingVerify = reverifyBeforeLanding(taskDb, db, task, { verifyCache });
9929
10174
  if (!landingVerify.ok) {
10175
+ // A refused landing whose revise also failed is the worst state in the
10176
+ // loop: the work does not land AND nobody is told to fix it, so the row
10177
+ // sits in review forever being re-refused every hour. Mark it so the
10178
+ // heartbeat can raise it instead of counting it as routine.
9930
10179
  results.push({
9931
10180
  ...evaluation,
9932
10181
  eligible: false,
9933
10182
  action: landingVerify.revised ? 'revised' : 'revise_failed',
9934
10183
  reason: landingVerify.result.reason || landingVerify.revise_reason || 'verify_failed',
10184
+ ...(landingVerify.result.unrunnable_cause
10185
+ ? { unrunnable_cause: landingVerify.result.unrunnable_cause }
10186
+ : {}),
10187
+ alarm: landingVerify.revised !== true || landingVerify.result.reason === 'verify_unrunnable',
9935
10188
  verify: landingVerify.verify,
9936
10189
  exit_code: landingVerify.result.status,
9937
10190
  revision_note: landingVerify.note,
@@ -10029,7 +10282,7 @@ function autoAcceptSweepDeniedReason(task) {
10029
10282
  for (const value of autoAcceptSweepLabelValues(task)) {
10030
10283
  const tokens = autoAcceptSweepNormalizedTokens(value);
10031
10284
  for (const token of tokens) {
10032
- if (token === 'needs-human' || token === 'needshuman') {
10285
+ if (token === 'needs-human' || token === 'needshuman' || token === 'decision') {
10033
10286
  return 'needs_human';
10034
10287
  }
10035
10288
  const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
@@ -11057,7 +11310,45 @@ function taskHasReview(task) {
11057
11310
  return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
11058
11311
  }
11059
11312
 
11060
- function taskBoardHtml() {
11313
+ const TASK_BOARD_COLUMNS = [
11314
+ ['backlog', 'Backlog'],
11315
+ ['open', 'Open'],
11316
+ ['doing', 'Doing'],
11317
+ ['review', 'Review'],
11318
+ ['blocked', 'Blocked'],
11319
+ ['done', 'Done'],
11320
+ ];
11321
+
11322
+ // Pure state shaping: projection in, board view model out. No markup here.
11323
+ function taskBoardViewModel(projection = {}) {
11324
+ const tasks = Array.isArray(projection.tasks) ? projection.tasks : [];
11325
+ const rows = tasks.map((task) => ({
11326
+ id: task.id,
11327
+ task,
11328
+ column: taskColumn(task),
11329
+ decision: taskHasReview(task),
11330
+ }));
11331
+ const columns = TASK_BOARD_COLUMNS.map(([key, label]) => ({
11332
+ key,
11333
+ label,
11334
+ rows: rows.filter((row) => row.column === key),
11335
+ }));
11336
+ const counts = {};
11337
+ for (const column of columns) counts[column.key] = column.rows.length;
11338
+ return {
11339
+ columns,
11340
+ counts,
11341
+ rows,
11342
+ total: rows.length,
11343
+ planTags: Array.from(STATUS_PLAN_TAGS),
11344
+ };
11345
+ }
11346
+
11347
+ // Markup only: view model in, board page out. The page hydrates itself from
11348
+ // /api/tasks in the browser, so the template bakes in just the shared
11349
+ // plan-tag list; the column keys and labels must stay in step with
11350
+ // TASK_BOARD_COLUMNS above.
11351
+ function taskBoardTemplate(model) {
11061
11352
  return `<!doctype html>
11062
11353
  <html lang="en">
11063
11354
  <head>
@@ -11216,7 +11507,7 @@ function taskBoardHtml() {
11216
11507
  ['blocked', 'Blocked'],
11217
11508
  ['done', 'Done']
11218
11509
  ];
11219
- const planTags = new Set(${JSON.stringify(Array.from(STATUS_PLAN_TAGS))});
11510
+ const planTags = new Set(${JSON.stringify(model.planTags)});
11220
11511
  let state = { tasks: [] };
11221
11512
  let selected = null;
11222
11513
  const $ = (id) => document.getElementById(id);
@@ -11489,6 +11780,10 @@ function taskBoardHtml() {
11489
11780
  </html>`;
11490
11781
  }
11491
11782
 
11783
+ function taskBoardHtml() {
11784
+ return taskBoardTemplate(taskBoardViewModel());
11785
+ }
11786
+
11492
11787
  function readJsonBody(req) {
11493
11788
  return new Promise((resolve, reject) => {
11494
11789
  let body = '';
@@ -11507,10 +11802,11 @@ function readJsonBody(req) {
11507
11802
  });
11508
11803
  }
11509
11804
 
11510
- function sendJson(res, status, value) {
11805
+ function sendJson(res, status, value, headers = {}) {
11511
11806
  res.writeHead(status, {
11512
11807
  'content-type': 'application/json; charset=utf-8',
11513
11808
  'access-control-allow-origin': 'http://localhost',
11809
+ ...headers,
11514
11810
  });
11515
11811
  res.end(JSON.stringify(value, null, 2));
11516
11812
  }
@@ -11520,551 +11816,655 @@ function sendHtml(res, value) {
11520
11816
  res.end(value);
11521
11817
  }
11522
11818
 
11523
- async function handleTaskApi(req, res, taskDb, db) {
11524
- const url = new URL(req.url, 'http://127.0.0.1');
11525
- if (req.method === 'OPTIONS') return sendJson(res, 200, { ok: true });
11526
- if (req.method === 'GET' && url.pathname === '/') return sendHtml(res, taskBoardHtml());
11527
- if (req.method === 'GET' && url.pathname === '/api/tasks') {
11528
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11529
- return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
11530
- }
11531
- if (req.method === 'GET' && url.pathname === '/api/stream') {
11532
- // Time-ordered feed of what the agent actually did + heartbeat liveness.
11533
- const { projection } = writeDefaultProjection(taskDb, db);
11534
- const root = projection.workspace_root || process.cwd();
11535
- const stateDir = path.join(root, '.atris', 'state');
11536
- const activity = require('../lib/activity-stream');
11537
- const { readJsonl } = require('../lib/pulse');
11538
- const rd = (f) => readJsonl(path.join(stateDir, f));
11539
- const pulseReceipts = rd('pulse_agi_loop_receipts.jsonl');
11540
- const events = activity.buildActivityStream({
11541
- pulseReceipts,
11542
- scorecards: rd('scorecards.jsonl'),
11543
- taskEpisodes: rd('task_episodes.jsonl').slice(-200),
11544
- xpReceipts: rd('career_xp_receipts.jsonl').slice(-200),
11545
- missionEvents: rd('mission_events.jsonl').slice(-200),
11546
- }, { limit: 60 });
11547
- return sendJson(res, 200, { ok: true, heartbeat: activity.buildHeartbeat(pulseReceipts), events });
11548
- }
11549
- if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities') {
11550
- return sendJson(res, 200, {
11551
- ok: true,
11552
- action: 'capabilities',
11553
- capabilities: taskCapabilitiesContract(),
11554
- safety: {
11555
- read_only: true,
11556
- claims_work: false,
11557
- human_accept: false,
11558
- xp_after_human_accept: true,
11559
- },
11819
+ function taskApiQueueOptions(url) {
11820
+ const limitParam = url.searchParams.get('limit');
11821
+ const limit = limitParam ? Number(limitParam) : 8;
11822
+ return {
11823
+ owner: url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER,
11824
+ reviewer: url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review',
11825
+ all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11826
+ everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11827
+ limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11828
+ scope: taskQueueScopeFromSearchParams(url.searchParams),
11829
+ };
11830
+ }
11831
+
11832
+ function serveTaskApiOptions({ res }) {
11833
+ return sendJson(res, 200, { ok: true });
11834
+ }
11835
+
11836
+ function serveTaskBoardPage({ res }) {
11837
+ return sendHtml(res, taskBoardHtml());
11838
+ }
11839
+
11840
+ function serveTaskList({ res, taskDb, db }) {
11841
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
11842
+ return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
11843
+ }
11844
+
11845
+ function serveTaskActivityStream({ res, taskDb, db }) {
11846
+ // Time-ordered feed of what the agent actually did + heartbeat liveness.
11847
+ const { projection } = writeDefaultProjection(taskDb, db);
11848
+ const root = projection.workspace_root || process.cwd();
11849
+ const stateDir = path.join(root, '.atris', 'state');
11850
+ const activity = require('../lib/activity-stream');
11851
+ const { readJsonl } = require('../lib/pulse');
11852
+ const readStateRows = (file) => readJsonl(path.join(stateDir, file));
11853
+ const pulseReceipts = readStateRows('pulse_agi_loop_receipts.jsonl');
11854
+ const events = activity.buildActivityStream({
11855
+ pulseReceipts,
11856
+ scorecards: readStateRows('scorecards.jsonl'),
11857
+ taskEpisodes: readStateRows('task_episodes.jsonl').slice(-200),
11858
+ xpReceipts: readStateRows('career_xp_receipts.jsonl').slice(-200),
11859
+ missionEvents: readStateRows('mission_events.jsonl').slice(-200),
11860
+ }, { limit: 60 });
11861
+ return sendJson(res, 200, {
11862
+ ok: true,
11863
+ heartbeat: activity.buildHeartbeat(pulseReceipts),
11864
+ events,
11865
+ }, {
11866
+ 'content-type': 'text/event-stream; charset=utf-8',
11867
+ 'cache-control': 'no-cache',
11868
+ connection: 'keep-alive',
11869
+ });
11870
+ }
11871
+
11872
+ function serveTaskCapabilities({ res }) {
11873
+ return sendJson(res, 200, {
11874
+ ok: true,
11875
+ action: 'capabilities',
11876
+ capabilities: taskCapabilitiesContract(),
11877
+ safety: {
11878
+ read_only: true,
11879
+ claims_work: false,
11880
+ human_accept: false,
11881
+ xp_after_human_accept: true,
11882
+ },
11883
+ });
11884
+ }
11885
+
11886
+ function serveTaskCapabilitiesCheck({ res, taskDb, db, url }) {
11887
+ const report = taskCapabilitiesCheckReport(taskDb, db, [], taskApiQueueOptions(url));
11888
+ return sendJson(res, report.ok ? 200 : 409, report);
11889
+ }
11890
+
11891
+ function serveTaskReviewLaneDrain({ res, taskDb, db, url }) {
11892
+ const report = taskReviewLaneDrainReport(taskDb, db, [], taskApiQueueOptions(url));
11893
+ return sendJson(res, report.ok ? 200 : 409, report);
11894
+ }
11895
+
11896
+ async function postTaskReviewLaneAct({ req, res, taskDb, db, url }) {
11897
+ const body = await readJsonBody(req);
11898
+ const result = taskReviewLaneAct(taskDb, db, taskReviewLaneActOptionsFromBody(body, url.searchParams));
11899
+ return sendJson(res, result.ok ? 200 : result.status || 409, result);
11900
+ }
11901
+
11902
+ async function postTaskReviewLaneLoop({ req, res, taskDb, db, url }) {
11903
+ const body = await readJsonBody(req);
11904
+ const result = taskReviewLaneLoop(taskDb, db, taskReviewLaneLoopOptionsFromBody(body, url.searchParams));
11905
+ return sendJson(res, result.ok ? 200 : result.status || 409, result);
11906
+ }
11907
+
11908
+ async function postTaskReviewLaneRun({ req, res, taskDb, db, url }) {
11909
+ const body = await readJsonBody(req);
11910
+ const result = taskReviewLaneRun(taskDb, db, taskReviewLaneRunOptionsFromBody(body, url.searchParams));
11911
+ return sendJson(res, result.ok ? 200 : result.status || 409, result);
11912
+ }
11913
+
11914
+ function serveTaskCurrent({ res, taskDb, db, url }) {
11915
+ const { outPath, current } = buildTaskCurrent(taskDb, db, [], taskApiQueueOptions(url));
11916
+ const action = url.pathname.endsWith('/queue') ? 'queue' : 'current';
11917
+ return sendJson(res, 200, {
11918
+ ok: true,
11919
+ action,
11920
+ projection_path: outPath,
11921
+ current,
11922
+ selected: current.selected,
11923
+ page: current.page,
11924
+ queue: current.queue,
11925
+ });
11926
+ }
11927
+
11928
+ async function postCurrentTaskStep({ req, res, taskDb, db, url }) {
11929
+ const body = await readJsonBody(req);
11930
+ const options = taskCurrentStepOptionsFromBody(body, url.searchParams);
11931
+ try {
11932
+ const result = runCurrentTaskStep(taskDb, db, options);
11933
+ return sendJson(res, 200, result);
11934
+ } catch (error) {
11935
+ const errorCurrent = error.current || null;
11936
+ return sendJson(res, error.status || 409, {
11937
+ ok: false,
11938
+ action: 'current_step',
11939
+ reason: error.reason || 'step_failed',
11940
+ detail: error.message,
11941
+ selected_task_id: errorCurrent ? errorCurrent.selected_task_id : null,
11942
+ selected_ref: errorCurrent ? errorCurrent.selected_ref : null,
11943
+ selected_next_key: selectedNextKeyFromCurrent(errorCurrent),
11944
+ current: errorCurrent,
11945
+ page: error.page || null,
11560
11946
  });
11561
11947
  }
11562
- if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities/check') {
11563
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11564
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11565
- const limitParam = url.searchParams.get('limit');
11566
- const limit = limitParam ? Number(limitParam) : 8;
11567
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11568
- const report = taskCapabilitiesCheckReport(taskDb, db, [], {
11569
- owner,
11570
- reviewer,
11571
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11572
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11573
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11574
- scope,
11575
- });
11576
- return sendJson(res, report.ok ? 200 : 409, report);
11577
- }
11578
- if (req.method === 'GET' && url.pathname === '/api/tasks/review-lane-drain') {
11579
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11580
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11581
- const limitParam = url.searchParams.get('limit');
11582
- const limit = limitParam ? Number(limitParam) : 8;
11583
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11584
- const report = taskReviewLaneDrainReport(taskDb, db, [], {
11585
- owner,
11586
- reviewer,
11587
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11588
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11589
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11590
- scope,
11591
- });
11592
- return sendJson(res, report.ok ? 200 : 409, report);
11593
- }
11594
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-act') {
11595
- const body = await readJsonBody(req);
11596
- const result = taskReviewLaneAct(taskDb, db, taskReviewLaneActOptionsFromBody(body, url.searchParams));
11597
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11598
- }
11599
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-loop') {
11600
- const body = await readJsonBody(req);
11601
- const result = taskReviewLaneLoop(taskDb, db, taskReviewLaneLoopOptionsFromBody(body, url.searchParams));
11602
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11603
- }
11604
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-run') {
11605
- const body = await readJsonBody(req);
11606
- const result = taskReviewLaneRun(taskDb, db, taskReviewLaneRunOptionsFromBody(body, url.searchParams));
11607
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11608
- }
11609
- if (req.method === 'GET' && (url.pathname === '/api/tasks/current' || url.pathname === '/api/tasks/queue')) {
11610
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11611
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11612
- const limitParam = url.searchParams.get('limit');
11613
- const limit = limitParam ? Number(limitParam) : 8;
11614
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11615
- const { outPath, current } = buildTaskCurrent(taskDb, db, [], {
11616
- owner,
11617
- reviewer,
11618
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11619
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11620
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11621
- scope,
11948
+ }
11949
+
11950
+ async function postTaskCreate({ req, res, taskDb, db }) {
11951
+ const body = await readJsonBody(req);
11952
+ const title = String(body.title || '').trim();
11953
+ if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
11954
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
11955
+ const result = taskDb.addTask(db, {
11956
+ title,
11957
+ tag: body.tag ? String(body.tag) : 'tasks',
11958
+ workspaceRoot: taskDb.workspaceRoot(),
11959
+ metadata: body.verify ? { verify: String(body.verify).trim() } : null,
11960
+ });
11961
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
11962
+ return sendJson(res, 200, {
11963
+ ok: true,
11964
+ action: 'created',
11965
+ task_id: result.id,
11966
+ operator_title_warning: operatorTitleWarning,
11967
+ projection_path: outPath,
11968
+ task: taskFromProjection(projection, result.id),
11969
+ });
11970
+ }
11971
+
11972
+ async function postTaskClearPlan({ req, res, taskDb, db }) {
11973
+ const body = await readJsonBody(req);
11974
+ if (!body.confirm && !body.yes) {
11975
+ return sendJson(res, 400, { ok: false, reason: 'confirm_required', detail: stageErrorDetail('task clear-plan', 'confirm_required') });
11976
+ }
11977
+ const result = taskDb.clearPlanTasks(db, {
11978
+ workspaceRoot: taskDb.workspaceRoot(),
11979
+ actor: String(body.actor || DEFAULT_OWNER),
11980
+ reason: String(body.reason || body.note || 'clear_plan'),
11981
+ tag: String(body.tag || 'capture'),
11982
+ });
11983
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
11984
+ const taskById = new Map((projection.tasks || []).map(task => [task.id, task]));
11985
+ return sendJson(res, 200, {
11986
+ ok: true,
11987
+ action: 'clear_plan',
11988
+ cleared_count: result.cleared.length,
11989
+ skipped_count: result.skipped.length,
11990
+ skipped: result.skipped,
11991
+ projection_path: outPath,
11992
+ tasks: result.cleared.map(task => taskFromProjection(projection, task.id) || taskById.get(task.id)).filter(Boolean),
11993
+ });
11994
+ }
11995
+
11996
+ const TASK_API_ROUTES = [
11997
+ { method: 'OPTIONS', pathname: null, serve: serveTaskApiOptions },
11998
+ { method: 'GET', pathname: '/', serve: serveTaskBoardPage },
11999
+ { method: 'GET', pathname: '/api/tasks', serve: serveTaskList },
12000
+ { method: 'GET', pathname: '/api/stream', serve: serveTaskActivityStream },
12001
+ { method: 'GET', pathname: '/api/tasks/capabilities', serve: serveTaskCapabilities },
12002
+ { method: 'GET', pathname: '/api/tasks/capabilities/check', serve: serveTaskCapabilitiesCheck },
12003
+ { method: 'GET', pathname: '/api/tasks/review-lane-drain', serve: serveTaskReviewLaneDrain },
12004
+ { method: 'POST', pathname: '/api/tasks/review-lane-act', serve: postTaskReviewLaneAct },
12005
+ { method: 'POST', pathname: '/api/tasks/review-lane-loop', serve: postTaskReviewLaneLoop },
12006
+ { method: 'POST', pathname: '/api/tasks/review-lane-run', serve: postTaskReviewLaneRun },
12007
+ { method: 'GET', pathname: '/api/tasks/current', serve: serveTaskCurrent },
12008
+ { method: 'GET', pathname: '/api/tasks/queue', serve: serveTaskCurrent },
12009
+ { method: 'POST', pathname: '/api/tasks/current/step', serve: postCurrentTaskStep },
12010
+ { method: 'POST', pathname: '/api/tasks', serve: postTaskCreate },
12011
+ { method: 'POST', pathname: '/api/tasks/clear-plan', serve: postTaskClearPlan },
12012
+ { pattern: /^\/api\/tasks\/([^/]+)$/, serve: serveTaskDetail },
12013
+ { pattern: /^\/api\/tasks\/([^/]+)\/page$/, serve: serveTaskPage },
12014
+ { pattern: /^\/api\/tasks\/([^/]+)\/(claim|message|chat|step|plan|do|backlog|ready|accept|revise|finish|review|review-chat|continue-work|events)$/, serve: serveTaskOperation },
12015
+ ];
12016
+
12017
+ function serveTaskDetail({ req, res, taskDb, db, match }) {
12018
+ if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
12019
+ const resolved = resolveTaskRef(taskDb, db, match[1]);
12020
+ if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
12021
+ const task = taskDetail(taskDb, db, resolved.id);
12022
+ if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
12023
+ const { outPath } = writeDefaultProjection(taskDb, db);
12024
+ return sendJson(res, 200, { ok: true, action: 'detail', task_id: resolved.id, projection_path: outPath, task, page: taskPageContract(task) });
12025
+ }
12026
+
12027
+ function serveTaskPage({ req, res, taskDb, db, match }) {
12028
+ if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
12029
+ const resolved = resolveTaskRef(taskDb, db, match[1]);
12030
+ if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
12031
+ const task = taskDetail(taskDb, db, resolved.id);
12032
+ if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
12033
+ const { outPath } = writeDefaultProjection(taskDb, db);
12034
+ return sendJson(res, 200, { ok: true, action: 'page', task_id: resolved.id, projection_path: outPath, page: taskPageContract(task) });
12035
+ }
12036
+
12037
+ function postTaskApiStep({ res, taskDb, db, taskId, body }) {
12038
+ try {
12039
+ const result = runTaskStep(taskDb, db, taskId, taskStepOptionsFromBody(body));
12040
+ return sendJson(res, 200, result);
12041
+ } catch (error) {
12042
+ return sendJson(res, error.status || 409, {
12043
+ ok: false,
12044
+ action: 'step',
12045
+ task_id: taskId,
12046
+ reason: error.reason || 'step_failed',
12047
+ detail: error.message,
12048
+ page: error.page || null,
11622
12049
  });
11623
- const action = url.pathname.endsWith('/queue') ? 'queue' : 'current';
11624
- return sendJson(res, 200, {
11625
- ok: true,
11626
- action,
11627
- projection_path: outPath,
11628
- current,
11629
- selected: current.selected,
11630
- page: current.page,
11631
- queue: current.queue,
12050
+ }
12051
+ }
12052
+
12053
+ function postTaskApiContinueWork({ res, taskDb, db, taskId, body }) {
12054
+ try {
12055
+ const result = continueWorkForReviewTask(taskDb, db, taskId, { owner: body.owner || body.actor || DEFAULT_OWNER });
12056
+ return sendJson(res, 200, result);
12057
+ } catch (error) {
12058
+ return sendJson(res, error.status || 409, {
12059
+ ok: false,
12060
+ action: 'continue_work',
12061
+ task_id: taskId,
12062
+ reason: error.reason || 'continue_work_failed',
12063
+ detail: error.message,
11632
12064
  });
11633
12065
  }
11634
- if (req.method === 'POST' && url.pathname === '/api/tasks/current/step') {
11635
- const body = await readJsonBody(req);
11636
- const options = taskCurrentStepOptionsFromBody(body, url.searchParams);
11637
- try {
11638
- const result = runCurrentTaskStep(taskDb, db, options);
11639
- return sendJson(res, 200, result);
11640
- } catch (error) {
11641
- const errorCurrent = error.current || null;
11642
- return sendJson(res, error.status || 409, {
11643
- ok: false,
11644
- action: 'current_step',
11645
- reason: error.reason || 'step_failed',
11646
- detail: error.message,
11647
- selected_task_id: errorCurrent ? errorCurrent.selected_task_id : null,
11648
- selected_ref: errorCurrent ? errorCurrent.selected_ref : null,
11649
- selected_next_key: selectedNextKeyFromCurrent(errorCurrent),
11650
- current: errorCurrent,
11651
- page: error.page || null,
11652
- });
11653
- }
12066
+ }
12067
+
12068
+ function postTaskApiClaim({ res, taskDb, db, taskId, body }) {
12069
+ const owner = String(body.owner || body.actor || DEFAULT_OWNER);
12070
+ const result = taskDb.claimTask(db, { id: taskId, claimedBy: owner });
12071
+ if (!result.claimed) return sendJson(res, 409, { ok: false, reason: result.reason, claimed_by: result.claimed_by || null });
12072
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12073
+ return sendJson(res, 200, { ok: true, action: 'claimed', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12074
+ }
12075
+
12076
+ function postTaskApiMessage({ res, taskDb, db, taskId, body }) {
12077
+ const result = taskDb.noteTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), content: String(body.content || '') });
12078
+ if (!result.noted) return sendJson(res, 404, { ok: false, reason: result.reason });
12079
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12080
+ return sendJson(res, 200, { ok: true, action: 'noted', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12081
+ }
12082
+
12083
+ function postTaskApiChat({ res, taskDb, db, taskId, body }) {
12084
+ const result = taskDb.chatTask(db, {
12085
+ id: taskId,
12086
+ actor: String(body.actor || DEFAULT_OWNER),
12087
+ content: String(body.content || body.message || body.text || ''),
12088
+ goal: String(body.goal || body.objective || ''),
12089
+ summary: String(body.summary || ''),
12090
+ });
12091
+ if (!result.chatted) {
12092
+ const status = result.reason === 'content_required' ? 400 : result.reason === 'not_found' ? 404 : 409;
12093
+ return sendJson(res, status, { ok: false, reason: result.reason, detail: stageErrorDetail('task chat', result.reason, result) });
11654
12094
  }
11655
- if (req.method === 'POST' && url.pathname === '/api/tasks') {
11656
- const body = await readJsonBody(req);
11657
- const title = String(body.title || '').trim();
11658
- if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
11659
- const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
11660
- const result = taskDb.addTask(db, {
11661
- title,
11662
- tag: body.tag ? String(body.tag) : 'tasks',
11663
- workspaceRoot: taskDb.workspaceRoot(),
12095
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12096
+ return sendJson(res, 200, {
12097
+ ok: true,
12098
+ action: 'chatted',
12099
+ task_id: taskId,
12100
+ version: result.event.version,
12101
+ goal_changed: result.goal_changed,
12102
+ chat_packet: result.chat_packet,
12103
+ projection_path: outPath,
12104
+ task: taskFromProjection(projection, taskId),
12105
+ });
12106
+ }
12107
+
12108
+ function postTaskApiPlan({ res, taskDb, db, taskId, body }) {
12109
+ const actor = String(body.actor || DEFAULT_OWNER);
12110
+ const goal = String(body.goal || body.objective || '');
12111
+ const summary = String(body.summary || body.plan || '');
12112
+ const owner = String(body.owner || body.assignee || '');
12113
+ const exit = String(body.exit || body.exit_condition || '');
12114
+ const firstMove = String(body.first_move || body.firstMove || body.first || '');
12115
+ const task = taskDetail(taskDb, db, taskId);
12116
+ const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
12117
+ actor,
12118
+ actorExplicit: Boolean(body.actor),
12119
+ owner,
12120
+ goal,
12121
+ summary,
12122
+ firstMove,
12123
+ exit,
12124
+ });
12125
+ const result = taskDb.stageTask(db, {
12126
+ id: taskId,
12127
+ actor,
12128
+ stage: 'plan',
12129
+ goal,
12130
+ summary,
12131
+ owner: automaticPlan.ownerForStage || owner,
12132
+ exit,
12133
+ proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
12134
+ firstMove,
12135
+ nextButton: String(body.next_button || body.nextButton || ''),
12136
+ confidence: body.confidence,
12137
+ planTrace: automaticPlan.trace,
12138
+ });
12139
+ if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
12140
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12141
+ return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, plan_trace: automaticPlan.trace, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12142
+ }
12143
+
12144
+ function postTaskApiDo({ res, taskDb, db, taskId, body }) {
12145
+ const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
12146
+ if (!firstMove) return sendJson(res, 400, { ok: false, reason: 'first_move_required', detail: 'task do: first_move required' });
12147
+ const result = taskDb.stageTask(db, {
12148
+ id: taskId,
12149
+ actor: String(body.actor || DEFAULT_OWNER),
12150
+ stage: 'do',
12151
+ goal: String(body.goal || body.objective || ''),
12152
+ summary: String(body.summary || ''),
12153
+ owner: String(body.actor || DEFAULT_OWNER),
12154
+ exit: String(body.exit || body.exit_condition || body.exitCondition || ''),
12155
+ proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
12156
+ firstMove,
12157
+ nextButton: String(body.next_button || body.nextButton || ''),
12158
+ confidence: body.confidence,
12159
+ });
12160
+ if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task do', result.reason, result) });
12161
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12162
+ return sendJson(res, 200, { ok: true, action: 'doing', task_id: taskId, version: result.event.version, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12163
+ }
12164
+
12165
+ function postTaskApiBacklog({ res, taskDb, db, taskId, body }) {
12166
+ const result = taskDb.backlogTask(db, {
12167
+ id: taskId,
12168
+ actor: String(body.actor || DEFAULT_OWNER),
12169
+ reason: String(body.reason || body.note || 'clear_plan'),
12170
+ tag: String(body.tag || 'capture'),
12171
+ });
12172
+ if (!result.backlogged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task backlog', result.reason, result) });
12173
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12174
+ return sendJson(res, 200, {
12175
+ ok: true,
12176
+ action: 'backlogged',
12177
+ task_id: taskId,
12178
+ version: result.event.version,
12179
+ cleared_keys: result.cleared_keys,
12180
+ projection_path: outPath,
12181
+ task: taskFromProjection(projection, taskId),
12182
+ });
12183
+ }
12184
+
12185
+ function postTaskApiReviewChat({ res, taskDb, db, taskId, body }) {
12186
+ try {
12187
+ const result = appendTaskReviewChat(taskDb, db, taskId, {
12188
+ reviewer: body.reviewer || body.actor || 'codex-review',
12189
+ dryRun: Boolean(body.dryRun || body.noNote),
12190
+ });
12191
+ const { event, compactProjection, outPath, ...payload } = result;
12192
+ return sendJson(res, 200, payload);
12193
+ } catch (error) {
12194
+ return sendJson(res, error.status || 409, {
12195
+ ok: false,
12196
+ reason: error.reason || 'review_chat_failed',
12197
+ detail: error.message,
11664
12198
  });
11665
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11666
- return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, operator_title_warning: operatorTitleWarning, projection_path: outPath, task: taskFromProjection(projection, result.id) });
11667
12199
  }
11668
- if (req.method === 'POST' && url.pathname === '/api/tasks/clear-plan') {
11669
- const body = await readJsonBody(req);
11670
- if (!body.confirm && !body.yes) {
11671
- return sendJson(res, 400, { ok: false, reason: 'confirm_required', detail: stageErrorDetail('task clear-plan', 'confirm_required') });
11672
- }
11673
- const result = taskDb.clearPlanTasks(db, {
11674
- workspaceRoot: taskDb.workspaceRoot(),
12200
+ }
12201
+
12202
+ function postTaskApiFinish({ res, taskDb, db, taskId, body }) {
12203
+ const currentTask = taskDb.getTask(db, taskId);
12204
+ const failed = Boolean(body.failed);
12205
+ const proof = String(body.proof || '').trim();
12206
+ const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
12207
+ const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
12208
+ if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12209
+ const done = taskDb.doneTask(db, {
12210
+ id: taskId,
12211
+ status: failed ? 'failed' : 'done',
12212
+ actor: String(body.actor || DEFAULT_OWNER),
12213
+ action: failed ? 'failed' : 'finished',
12214
+ proof,
12215
+ });
12216
+ if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
12217
+ let episode = null;
12218
+ let nextCreated = null;
12219
+ let xpProjection = null;
12220
+ if (shouldReview) {
12221
+ const reviewed = taskDb.reviewTask(db, {
12222
+ id: taskId,
11675
12223
  actor: String(body.actor || DEFAULT_OWNER),
11676
- reason: String(body.reason || body.note || 'clear_plan'),
11677
- tag: String(body.tag || 'capture'),
11678
- });
11679
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11680
- const taskById = new Map((projection.tasks || []).map(task => [task.id, task]));
11681
- return sendJson(res, 200, {
11682
- ok: true,
11683
- action: 'clear_plan',
11684
- cleared_count: result.cleared.length,
11685
- skipped_count: result.skipped.length,
11686
- skipped: result.skipped,
11687
- projection_path: outPath,
11688
- tasks: result.cleared.map(task => taskFromProjection(projection, task.id) || taskById.get(task.id)).filter(Boolean),
12224
+ reward: body.reward === undefined ? 1 : body.reward,
12225
+ lesson: String(body.lesson || ''),
12226
+ nextTask: String(body.next || ''),
12227
+ proof: String(body.proof || ''),
12228
+ careerXpEligible: false,
11689
12229
  });
12230
+ episode = reviewed.episode;
12231
+ nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
12232
+ xpProjection = refreshCareerXpAfterReview(reviewed);
11690
12233
  }
11691
- const detailMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
11692
- if (detailMatch) {
11693
- if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11694
- const resolved = resolveTaskRef(taskDb, db, detailMatch[1]);
11695
- if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11696
- const task = taskDetail(taskDb, db, resolved.id);
11697
- if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
11698
- const { outPath } = writeDefaultProjection(taskDb, db);
11699
- return sendJson(res, 200, { ok: true, action: 'detail', task_id: resolved.id, projection_path: outPath, task, page: taskPageContract(task) });
11700
- }
11701
- const pageMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/page$/);
11702
- if (pageMatch) {
11703
- if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11704
- const resolved = resolveTaskRef(taskDb, db, pageMatch[1]);
11705
- if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11706
- const task = taskDetail(taskDb, db, resolved.id);
11707
- if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
11708
- const { outPath } = writeDefaultProjection(taskDb, db);
11709
- return sendJson(res, 200, { ok: true, action: 'page', task_id: resolved.id, projection_path: outPath, page: taskPageContract(task) });
12234
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12235
+ return sendJson(res, 200, {
12236
+ ok: true,
12237
+ action: 'finished',
12238
+ task_id: taskId,
12239
+ reviewed: Boolean(episode),
12240
+ episode,
12241
+ xp_projection: xpProjection,
12242
+ next_task_id: nextCreated ? nextCreated.id : null,
12243
+ projection_path: outPath,
12244
+ task: taskFromProjection(projection, taskId),
12245
+ });
12246
+ }
12247
+
12248
+ function postTaskApiReady({ res, taskDb, db, taskId, body }) {
12249
+ const proof = String(body.proof || '').trim();
12250
+ const proofIssue = meaningfulTaskProofIssue(proof);
12251
+ if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12252
+ const nextTaskInput = normalizeReviewNextTaskInput(body.next);
12253
+ const actor = String(body.actor || DEFAULT_OWNER);
12254
+ const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
12255
+ if (resultText) {
12256
+ const resultIssue = resultSentenceIssue(resultText);
12257
+ if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
11710
12258
  }
11711
- const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|chat|step|plan|do|backlog|ready|accept|revise|finish|review|review-chat|continue-work|events)$/);
11712
- if (!match) return sendJson(res, 404, { ok: false, reason: 'not_found' });
12259
+ const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
12260
+ actor,
12261
+ proof,
12262
+ changed: body.changed || resultText || body.done,
12263
+ checked: body.checked || body.check || body.verified,
12264
+ passed: body.passed || body.pass,
12265
+ failed: body.failed || body.fail,
12266
+ cost: body.cost,
12267
+ saved: body.saved || body.savings,
12268
+ tryNext: body.try_next || body.tryNext || body.try || body.handoff,
12269
+ status: body.status,
12270
+ files: body.files,
12271
+ commands: body.commands || body.command,
12272
+ });
12273
+ const result = taskDb.readyTask(db, {
12274
+ id: taskId,
12275
+ actor,
12276
+ proof,
12277
+ lesson: String(body.lesson || ''),
12278
+ nextTask: nextTaskInput.nextTask,
12279
+ resultTrace: resultTrace && resultTrace.trace,
12280
+ result: resultText,
12281
+ landing: body.landing || {
12282
+ happened: body.happened,
12283
+ checked: body.checked,
12284
+ tested: body.tested,
12285
+ decision: body.decision,
12286
+ },
12287
+ });
12288
+ if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
12289
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12290
+ return sendJson(res, 200, {
12291
+ ok: true,
12292
+ action: 'ready',
12293
+ task_id: taskId,
12294
+ result_trace: resultTrace,
12295
+ ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
12296
+ projection_path: outPath,
12297
+ task: taskFromProjection(projection, taskId),
12298
+ });
12299
+ }
12300
+
12301
+ function postTaskApiAccept({ res, taskDb, db, taskId, body }) {
12302
+ const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
12303
+ const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
12304
+ const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
12305
+ const proofIssue = meaningfulTaskProofIssue(proof);
12306
+ if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12307
+ const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
12308
+ const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
12309
+ const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
12310
+ const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
12311
+ const clearedFields = [];
12312
+ if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
12313
+ if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
12314
+ const parsedReward = parseAcceptReward(body.reward);
12315
+ if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
12316
+ const done = taskDb.doneTask(db, {
12317
+ id: taskId,
12318
+ status: 'done',
12319
+ actor: String(body.actor || DEFAULT_OWNER),
12320
+ allowReview: true,
12321
+ action: 'accepted',
12322
+ proof,
12323
+ });
12324
+ if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
12325
+ const reviewed = taskDb.reviewTask(db, {
12326
+ id: taskId,
12327
+ actor: String(body.actor || DEFAULT_OWNER),
12328
+ reward: parsedReward.value,
12329
+ lesson,
12330
+ nextTask,
12331
+ proof,
12332
+ careerXpEligible: true,
12333
+ clearedFields,
12334
+ });
12335
+ const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12336
+ const xpProjection = refreshCareerXpAfterReview(reviewed);
12337
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12338
+ return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12339
+ }
12340
+
12341
+ function postTaskApiRevise({ res, taskDb, db, taskId, body }) {
12342
+ const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
12343
+ if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
12344
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12345
+ return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12346
+ }
12347
+
12348
+ function postTaskApiReview({ res, taskDb, db, taskId, body }) {
12349
+ const currentTask = taskDb.getTask(db, taskId);
12350
+ const rewardValue = body.reward === undefined ? 0 : body.reward;
12351
+ const proof = String(body.proof || '').trim();
12352
+ const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
12353
+ const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next')
12354
+ || Object.prototype.hasOwnProperty.call(body, 'next_task')
12355
+ || Object.prototype.hasOwnProperty.call(body, 'nextTask');
12356
+ const lessonText = hasExplicitLesson ? String(body.lesson || '') : '';
12357
+ const rawNext = Object.prototype.hasOwnProperty.call(body, 'next')
12358
+ ? body.next
12359
+ : Object.prototype.hasOwnProperty.call(body, 'next_task')
12360
+ ? body.next_task
12361
+ : body.nextTask;
12362
+ const nextTaskInput = normalizeReviewNextTaskInput(hasExplicitNext ? rawNext : '');
12363
+ const clearedFields = [];
12364
+ if (hasExplicitLesson && !lessonText.trim()) clearedFields.push('lesson');
12365
+ if (hasExplicitNext && !String(rawNext || '').trim()) clearedFields.push('next_task');
12366
+ const proofIssue = Number(rewardValue) > 0 || proof
12367
+ ? meaningfulTaskProofIssue(proof)
12368
+ : null;
12369
+ if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12370
+ const reviewed = taskDb.reviewTask(db, {
12371
+ id: taskId,
12372
+ actor: String(body.actor || DEFAULT_OWNER),
12373
+ reward: rewardValue,
12374
+ lesson: lessonText,
12375
+ nextTask: nextTaskInput.nextTask,
12376
+ proof,
12377
+ careerXpEligible: false,
12378
+ clearedFields,
12379
+ });
12380
+ const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12381
+ const xpProjection = refreshCareerXpAfterReview(reviewed);
12382
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
12383
+ return sendJson(res, 200, {
12384
+ ok: true,
12385
+ action: 'reviewed',
12386
+ task_id: taskId,
12387
+ episode: reviewed.episode,
12388
+ xp_projection: xpProjection,
12389
+ next_task_id: nextCreated ? nextCreated.id : null,
12390
+ ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
12391
+ projection_path: outPath,
12392
+ task: taskFromProjection(projection, taskId),
12393
+ });
12394
+ }
12395
+
12396
+ const TASK_API_POST_OPERATIONS = {
12397
+ step: postTaskApiStep,
12398
+ 'continue-work': postTaskApiContinueWork,
12399
+ claim: postTaskApiClaim,
12400
+ message: postTaskApiMessage,
12401
+ chat: postTaskApiChat,
12402
+ plan: postTaskApiPlan,
12403
+ do: postTaskApiDo,
12404
+ backlog: postTaskApiBacklog,
12405
+ 'review-chat': postTaskApiReviewChat,
12406
+ finish: postTaskApiFinish,
12407
+ ready: postTaskApiReady,
12408
+ accept: postTaskApiAccept,
12409
+ revise: postTaskApiRevise,
12410
+ review: postTaskApiReview,
12411
+ };
12412
+
12413
+ function serveTaskEvents({ res, taskDb, db, taskId }) {
12414
+ const events = taskDb.listTaskEvents(db, { taskId, limit: 500 });
12415
+ return sendJson(res, 200, { ok: true, events });
12416
+ }
12417
+
12418
+ async function serveTaskOperation({ req, res, taskDb, db, match }) {
11713
12419
  const resolved = resolveTaskRef(taskDb, db, match[1]);
11714
12420
  if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11715
12421
  const taskId = resolved.id;
11716
- const op = match[2];
11717
- if (req.method === 'GET' && op === 'events') {
11718
- const events = taskDb.listTaskEvents(db, { taskId, limit: 500 });
11719
- return sendJson(res, 200, { ok: true, events });
12422
+ const operation = match[2];
12423
+ if (req.method === 'GET' && operation === 'events') {
12424
+ return serveTaskEvents({ res, taskDb, db, taskId });
11720
12425
  }
11721
12426
  if (req.method !== 'POST') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11722
12427
  const body = await readJsonBody(req);
11723
- if (op === 'step') {
11724
- try {
11725
- const result = runTaskStep(taskDb, db, taskId, taskStepOptionsFromBody(body));
11726
- return sendJson(res, 200, result);
11727
- } catch (error) {
11728
- return sendJson(res, error.status || 409, {
11729
- ok: false,
11730
- action: 'step',
11731
- task_id: taskId,
11732
- reason: error.reason || 'step_failed',
11733
- detail: error.message,
11734
- page: error.page || null,
11735
- });
11736
- }
11737
- }
11738
- if (op === 'continue-work') {
11739
- try {
11740
- const result = continueWorkForReviewTask(taskDb, db, taskId, { owner: body.owner || body.actor || DEFAULT_OWNER });
11741
- return sendJson(res, 200, result);
11742
- } catch (error) {
11743
- return sendJson(res, error.status || 409, {
11744
- ok: false,
11745
- action: 'continue_work',
11746
- task_id: taskId,
11747
- reason: error.reason || 'continue_work_failed',
11748
- detail: error.message,
11749
- });
11750
- }
11751
- }
11752
- if (op === 'claim') {
11753
- const owner = String(body.owner || body.actor || DEFAULT_OWNER);
11754
- const result = taskDb.claimTask(db, { id: taskId, claimedBy: owner });
11755
- if (!result.claimed) return sendJson(res, 409, { ok: false, reason: result.reason, claimed_by: result.claimed_by || null });
11756
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11757
- return sendJson(res, 200, { ok: true, action: 'claimed', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11758
- }
11759
- if (op === 'message') {
11760
- const result = taskDb.noteTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), content: String(body.content || '') });
11761
- if (!result.noted) return sendJson(res, 404, { ok: false, reason: result.reason });
11762
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11763
- return sendJson(res, 200, { ok: true, action: 'noted', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11764
- }
11765
- if (op === 'chat') {
11766
- const result = taskDb.chatTask(db, {
11767
- id: taskId,
11768
- actor: String(body.actor || DEFAULT_OWNER),
11769
- content: String(body.content || body.message || body.text || ''),
11770
- goal: String(body.goal || body.objective || ''),
11771
- summary: String(body.summary || ''),
11772
- });
11773
- if (!result.chatted) {
11774
- const status = result.reason === 'content_required' ? 400 : result.reason === 'not_found' ? 404 : 409;
11775
- return sendJson(res, status, { ok: false, reason: result.reason, detail: stageErrorDetail('task chat', result.reason, result) });
11776
- }
11777
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11778
- return sendJson(res, 200, {
11779
- ok: true,
11780
- action: 'chatted',
11781
- task_id: taskId,
11782
- version: result.event.version,
11783
- goal_changed: result.goal_changed,
11784
- chat_packet: result.chat_packet,
11785
- projection_path: outPath,
11786
- task: taskFromProjection(projection, taskId),
11787
- });
11788
- }
11789
- if (op === 'plan') {
11790
- const actor = String(body.actor || DEFAULT_OWNER);
11791
- const goal = String(body.goal || body.objective || '');
11792
- const summary = String(body.summary || body.plan || '');
11793
- const owner = String(body.owner || body.assignee || '');
11794
- const exit = String(body.exit || body.exit_condition || '');
11795
- const firstMove = String(body.first_move || body.firstMove || body.first || '');
11796
- const task = taskDetail(taskDb, db, taskId);
11797
- const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
11798
- actor,
11799
- actorExplicit: Boolean(body.actor),
11800
- owner,
11801
- goal,
11802
- summary,
11803
- firstMove,
11804
- exit,
11805
- });
11806
- const result = taskDb.stageTask(db, {
11807
- id: taskId,
11808
- actor,
11809
- stage: 'plan',
11810
- goal,
11811
- summary,
11812
- owner: automaticPlan.ownerForStage || owner,
11813
- exit,
11814
- proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
11815
- firstMove,
11816
- nextButton: String(body.next_button || body.nextButton || ''),
11817
- confidence: body.confidence,
11818
- planTrace: automaticPlan.trace,
11819
- });
11820
- if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
11821
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11822
- return sendJson(res, 200, { ok: true, action: 'planned', task_id: taskId, version: result.event.version, plan_trace: automaticPlan.trace, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11823
- }
11824
- if (op === 'do') {
11825
- const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
11826
- if (!firstMove) return sendJson(res, 400, { ok: false, reason: 'first_move_required', detail: 'task do: first_move required' });
11827
- const result = taskDb.stageTask(db, {
11828
- id: taskId,
11829
- actor: String(body.actor || DEFAULT_OWNER),
11830
- stage: 'do',
11831
- goal: String(body.goal || body.objective || ''),
11832
- summary: String(body.summary || ''),
11833
- owner: String(body.actor || DEFAULT_OWNER),
11834
- exit: String(body.exit || body.exit_condition || body.exitCondition || ''),
11835
- proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
11836
- firstMove,
11837
- nextButton: String(body.next_button || body.nextButton || ''),
11838
- confidence: body.confidence,
11839
- });
11840
- if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task do', result.reason, result) });
11841
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11842
- return sendJson(res, 200, { ok: true, action: 'doing', task_id: taskId, version: result.event.version, stage_packet: result.stage_packet, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11843
- }
11844
- if (op === 'backlog') {
11845
- const result = taskDb.backlogTask(db, {
11846
- id: taskId,
11847
- actor: String(body.actor || DEFAULT_OWNER),
11848
- reason: String(body.reason || body.note || 'clear_plan'),
11849
- tag: String(body.tag || 'capture'),
11850
- });
11851
- if (!result.backlogged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task backlog', result.reason, result) });
11852
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11853
- return sendJson(res, 200, {
11854
- ok: true,
11855
- action: 'backlogged',
11856
- task_id: taskId,
11857
- version: result.event.version,
11858
- cleared_keys: result.cleared_keys,
11859
- projection_path: outPath,
11860
- task: taskFromProjection(projection, taskId),
11861
- });
11862
- }
11863
- if (op === 'review-chat') {
11864
- try {
11865
- const result = appendTaskReviewChat(taskDb, db, taskId, {
11866
- reviewer: body.reviewer || body.actor || 'codex-review',
11867
- dryRun: Boolean(body.dryRun || body.noNote),
11868
- });
11869
- const { event, compactProjection, outPath, ...payload } = result;
11870
- return sendJson(res, 200, payload);
11871
- } catch (error) {
11872
- return sendJson(res, error.status || 409, {
11873
- ok: false,
11874
- reason: error.reason || 'review_chat_failed',
11875
- detail: error.message,
11876
- });
12428
+ const postTaskOperation = TASK_API_POST_OPERATIONS[operation];
12429
+ if (!postTaskOperation) return;
12430
+ return postTaskOperation({ res, taskDb, db, taskId, body });
12431
+ }
12432
+
12433
+ function matchTaskApiRoute(req, url) {
12434
+ for (const route of TASK_API_ROUTES) {
12435
+ if (route.method && route.method !== req.method) continue;
12436
+ if (Object.prototype.hasOwnProperty.call(route, 'pathname')) {
12437
+ if (route.pathname === null || route.pathname === url.pathname) return { route, match: null };
12438
+ continue;
11877
12439
  }
12440
+ const match = url.pathname.match(route.pattern);
12441
+ if (match) return { route, match };
11878
12442
  }
11879
- if (op === 'finish') {
11880
- const currentTask = taskDb.getTask(db, taskId);
11881
- const failed = Boolean(body.failed);
11882
- const proof = String(body.proof || '').trim();
11883
- const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
11884
- const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
11885
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
11886
- const done = taskDb.doneTask(db, {
11887
- id: taskId,
11888
- status: failed ? 'failed' : 'done',
11889
- actor: String(body.actor || DEFAULT_OWNER),
11890
- action: failed ? 'failed' : 'finished',
11891
- proof,
11892
- });
11893
- if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
11894
- let episode = null;
11895
- let nextCreated = null;
11896
- let xpProjection = null;
11897
- if (shouldReview) {
11898
- const reviewed = taskDb.reviewTask(db, {
11899
- id: taskId,
11900
- actor: String(body.actor || DEFAULT_OWNER),
11901
- reward: body.reward === undefined ? 1 : body.reward,
11902
- lesson: String(body.lesson || ''),
11903
- nextTask: String(body.next || ''),
11904
- proof: String(body.proof || ''),
11905
- careerXpEligible: false,
11906
- });
11907
- episode = reviewed.episode;
11908
- nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
11909
- xpProjection = refreshCareerXpAfterReview(reviewed);
11910
- }
11911
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11912
- return sendJson(res, 200, {
11913
- ok: true,
11914
- action: 'finished',
11915
- task_id: taskId,
11916
- reviewed: Boolean(episode),
11917
- episode,
11918
- xp_projection: xpProjection,
11919
- next_task_id: nextCreated ? nextCreated.id : null,
11920
- projection_path: outPath,
11921
- task: taskFromProjection(projection, taskId),
11922
- });
11923
- }
11924
- if (op === 'ready') {
11925
- const proof = String(body.proof || '').trim();
11926
- const proofIssue = meaningfulTaskProofIssue(proof);
11927
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
11928
- const nextTaskInput = normalizeReviewNextTaskInput(body.next);
11929
- const actor = String(body.actor || DEFAULT_OWNER);
11930
- const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
11931
- if (resultText) {
11932
- const resultIssue = resultSentenceIssue(resultText);
11933
- if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
11934
- }
11935
- const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
11936
- actor,
11937
- proof,
11938
- changed: body.changed || resultText || body.done,
11939
- checked: body.checked || body.check || body.verified,
11940
- passed: body.passed || body.pass,
11941
- failed: body.failed || body.fail,
11942
- cost: body.cost,
11943
- saved: body.saved || body.savings,
11944
- tryNext: body.try_next || body.tryNext || body.try || body.handoff,
11945
- status: body.status,
11946
- files: body.files,
11947
- commands: body.commands || body.command,
11948
- });
11949
- const result = taskDb.readyTask(db, {
11950
- id: taskId,
11951
- actor,
11952
- proof,
11953
- lesson: String(body.lesson || ''),
11954
- nextTask: nextTaskInput.nextTask,
11955
- resultTrace: resultTrace && resultTrace.trace,
11956
- result: resultText,
11957
- landing: body.landing || {
11958
- happened: body.happened,
11959
- checked: body.checked,
11960
- tested: body.tested,
11961
- decision: body.decision,
11962
- },
11963
- });
11964
- if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
11965
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11966
- return sendJson(res, 200, {
11967
- ok: true,
11968
- action: 'ready',
11969
- task_id: taskId,
11970
- result_trace: resultTrace,
11971
- ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
11972
- projection_path: outPath,
11973
- task: taskFromProjection(projection, taskId),
11974
- });
11975
- }
11976
- if (op === 'accept') {
11977
- const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
11978
- const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
11979
- const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
11980
- const proofIssue = meaningfulTaskProofIssue(proof);
11981
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
11982
- const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
11983
- const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
11984
- const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
11985
- const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
11986
- const clearedFields = [];
11987
- if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
11988
- if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
11989
- const parsedReward = parseAcceptReward(body.reward);
11990
- if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
11991
- const done = taskDb.doneTask(db, {
11992
- id: taskId,
11993
- status: 'done',
11994
- actor: String(body.actor || DEFAULT_OWNER),
11995
- allowReview: true,
11996
- action: 'accepted',
11997
- proof,
11998
- });
11999
- if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
12000
- const reviewed = taskDb.reviewTask(db, {
12001
- id: taskId,
12002
- actor: String(body.actor || DEFAULT_OWNER),
12003
- reward: parsedReward.value,
12004
- lesson,
12005
- nextTask,
12006
- proof,
12007
- careerXpEligible: true,
12008
- clearedFields,
12009
- });
12010
- const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12011
- const xpProjection = refreshCareerXpAfterReview(reviewed);
12012
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12013
- return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12014
- }
12015
- if (op === 'revise') {
12016
- const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
12017
- if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
12018
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12019
- return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12020
- }
12021
- if (op === 'review') {
12022
- const currentTask = taskDb.getTask(db, taskId);
12023
- const rewardValue = body.reward === undefined ? 0 : body.reward;
12024
- const proof = String(body.proof || '').trim();
12025
- const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
12026
- const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next')
12027
- || Object.prototype.hasOwnProperty.call(body, 'next_task')
12028
- || Object.prototype.hasOwnProperty.call(body, 'nextTask');
12029
- const lessonText = hasExplicitLesson ? String(body.lesson || '') : '';
12030
- const rawNext = Object.prototype.hasOwnProperty.call(body, 'next')
12031
- ? body.next
12032
- : Object.prototype.hasOwnProperty.call(body, 'next_task')
12033
- ? body.next_task
12034
- : body.nextTask;
12035
- const nextTaskInput = normalizeReviewNextTaskInput(hasExplicitNext ? rawNext : '');
12036
- const clearedFields = [];
12037
- if (hasExplicitLesson && !lessonText.trim()) clearedFields.push('lesson');
12038
- if (hasExplicitNext && !String(rawNext || '').trim()) clearedFields.push('next_task');
12039
- const proofIssue = Number(rewardValue) > 0 || proof
12040
- ? meaningfulTaskProofIssue(proof)
12041
- : null;
12042
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12043
- const reviewed = taskDb.reviewTask(db, {
12044
- id: taskId,
12045
- actor: String(body.actor || DEFAULT_OWNER),
12046
- reward: rewardValue,
12047
- lesson: lessonText,
12048
- nextTask: nextTaskInput.nextTask,
12049
- proof,
12050
- careerXpEligible: false,
12051
- clearedFields,
12052
- });
12053
- const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12054
- const xpProjection = refreshCareerXpAfterReview(reviewed);
12055
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12056
- return sendJson(res, 200, {
12057
- ok: true,
12058
- action: 'reviewed',
12059
- task_id: taskId,
12060
- episode: reviewed.episode,
12061
- xp_projection: xpProjection,
12062
- next_task_id: nextCreated ? nextCreated.id : null,
12063
- ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
12064
- projection_path: outPath,
12065
- task: taskFromProjection(projection, taskId),
12443
+ return null;
12444
+ }
12445
+
12446
+ async function handleTaskApi(req, res, taskDb, db) {
12447
+ const url = new URL(req.url, 'http://127.0.0.1');
12448
+ const matchedRoute = matchTaskApiRoute(req, url);
12449
+ if (!matchedRoute) return sendJson(res, 404, { ok: false, reason: 'not_found' });
12450
+ return matchedRoute.route.serve({
12451
+ req,
12452
+ res,
12453
+ taskDb,
12454
+ db,
12455
+ url,
12456
+ match: matchedRoute.match,
12457
+ });
12458
+ }
12459
+
12460
+ function createTaskApiServer(taskApi = {}) {
12461
+ const taskDb = taskApi.taskDb || getTaskDb();
12462
+ const db = taskApi.db || taskDb.open();
12463
+ return http.createServer((req, res) => {
12464
+ handleTaskApi(req, res, taskDb, db).catch((error) => {
12465
+ sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(error && error.message || error) });
12066
12466
  });
12067
- }
12467
+ });
12068
12468
  }
12069
12469
 
12070
12470
  function cmdServe(args) {
@@ -12072,11 +12472,7 @@ function cmdServe(args) {
12072
12472
  const port = Number(flag(args, '--port') || process.env.PORT || 8787);
12073
12473
  const taskDb = getTaskDb();
12074
12474
  const db = taskDb.open();
12075
- const server = http.createServer((req, res) => {
12076
- handleTaskApi(req, res, taskDb, db).catch((e) => {
12077
- sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(e && e.message || e) });
12078
- });
12079
- });
12475
+ const server = createTaskApiServer({ taskDb, db });
12080
12476
  return new Promise((resolve, reject) => {
12081
12477
  server.on('error', reject);
12082
12478
  server.listen(port, host, () => {
@@ -12201,6 +12597,9 @@ async function runTaskCommand(args) {
12201
12597
  case 'fail': return cmdDone([...rest, '--failed']);
12202
12598
  case 'archive': return cmdArchive(rest);
12203
12599
  case 'clear-done': return cmdClearDone(rest);
12600
+ case 'reap-mission-blockers':
12601
+ case 'reap-blockers':
12602
+ return cmdReapMissionBlockers(rest);
12204
12603
  case 'relabel-archived': return cmdRelabelArchived(rest);
12205
12604
  case 'review': return cmdReview(rest);
12206
12605
  case 'reviews':
@@ -12241,7 +12640,7 @@ const MUTATING_TASK_COMMANDS = new Set([
12241
12640
  'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
12242
12641
  'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
12243
12642
  'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
12244
- 'done', 'finish', 'fail', 'archive', 'clear-done', 'relabel-archived', 'review', 'import',
12643
+ 'done', 'finish', 'fail', 'archive', 'clear-done', 'reap-mission-blockers', 'reap-blockers', 'relabel-archived', 'review', 'import',
12245
12644
  'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
12246
12645
  'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
12247
12646
  ]);
@@ -12257,6 +12656,7 @@ async function run(args) {
12257
12656
 
12258
12657
  module.exports = {
12259
12658
  run,
12659
+ createTaskApiServer,
12260
12660
  taskDayGroups,
12261
12661
  taskDayTextGroups,
12262
12662
  taskDayTitle,
@@ -12265,4 +12665,8 @@ module.exports = {
12265
12665
  delegateTask,
12266
12666
  AGENT_ENV_MARKERS,
12267
12667
  autoRenderTodoFromDb,
12668
+ projectionMissions,
12669
+ projectionWishes,
12670
+ taskBoardViewModel,
12671
+ taskBoardTemplate,
12268
12672
  };