atris 3.38.0 → 3.41.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 (78) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +31 -6
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/computer.js +20 -0
  13. package/commands/console.js +19 -3
  14. package/commands/decide.js +166 -0
  15. package/commands/deck.js +1 -4
  16. package/commands/drill.js +14 -24
  17. package/commands/engine.js +196 -8
  18. package/commands/gm.js +8 -6
  19. package/commands/harvest.js +1 -4
  20. package/commands/init.js +23 -3
  21. package/commands/land.js +8 -14
  22. package/commands/launchpad.js +1 -14
  23. package/commands/lifecycle.js +5 -5
  24. package/commands/log.js +55 -5
  25. package/commands/member.js +558 -574
  26. package/commands/mission.js +456 -237
  27. package/commands/pack.js +2746 -164
  28. package/commands/play.js +6 -4
  29. package/commands/probe.js +2 -2
  30. package/commands/pulse.js +15 -16
  31. package/commands/release.js +10 -9
  32. package/commands/router.js +5 -4
  33. package/commands/site-deploy.js +885 -0
  34. package/commands/site.js +11 -2
  35. package/commands/slop.js +14 -2
  36. package/commands/stream.js +4 -18
  37. package/commands/task.js +880 -558
  38. package/commands/taste.js +101 -0
  39. package/commands/team.js +176 -3
  40. package/commands/vercel.js +4 -2
  41. package/commands/voice.js +195 -0
  42. package/commands/watch.js +1 -22
  43. package/commands/wiki.js +1 -4
  44. package/commands/workflow.js +2 -2
  45. package/commands/worktree.js +1 -14
  46. package/commands/xp.js +27 -24
  47. package/lib/accept-verify-gate.js +5 -1
  48. package/lib/arg-parser.js +41 -0
  49. package/lib/auto-accept-certified.js +116 -1
  50. package/lib/autoland.js +66 -0
  51. package/lib/bench/runner.js +19 -1
  52. package/lib/context-gatherer.js +7 -1
  53. package/lib/engine-registry.js +141 -20
  54. package/lib/falsifier-probe.js +84 -0
  55. package/lib/fleet.js +65 -15
  56. package/lib/git-spawn.js +15 -0
  57. package/lib/json-file.js +37 -0
  58. package/lib/known-commands.js +2 -2
  59. package/lib/lesson-preflight.js +146 -0
  60. package/lib/loop-doctor.js +0 -2
  61. package/lib/mission-human-asks.js +28 -0
  62. package/lib/mission-protected-lane.js +4 -1
  63. package/lib/official-cli-integration.js +47 -2
  64. package/lib/orb-context.js +8 -1
  65. package/lib/pack-capabilities.js +685 -0
  66. package/lib/router-brain.js +51 -1
  67. package/lib/runner-command.js +0 -6
  68. package/lib/self-drive.js +44 -13
  69. package/lib/task-db.js +137 -3
  70. package/lib/task-decision.js +50 -0
  71. package/lib/taste-lessons.js +153 -0
  72. package/lib/tool-result-encode.js +17 -1
  73. package/lib/voice-gate.js +66 -0
  74. package/lib/wish-audit.js +1 -1
  75. package/lib/wish-delegate.js +1 -1
  76. package/lib/zip.js +95 -7
  77. package/package.json +2 -1
  78. 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,
@@ -21,7 +23,7 @@ const { evaluateAcceptVerify } = require('../lib/accept-verify-gate');
21
23
  const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
22
24
  const escapeRegExp = require('../lib/escape-regexp');
23
25
  const reviewIntegrity = require('../lib/review-integrity');
24
- const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
26
+ const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord, plainLandingReason } = require('../lib/voice-gate');
25
27
  const {
26
28
  normalizeOwnerSlug,
27
29
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
@@ -35,6 +37,11 @@ const {
35
37
  inspectTextLines,
36
38
  buildInspectPayload,
37
39
  } = require('../lib/inspect-fields');
40
+ const {
41
+ isDecisionTask,
42
+ decisionMarkerFor,
43
+ DECISION_REFUSE_REASON,
44
+ } = require('../lib/task-decision');
38
45
 
39
46
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
40
47
  || process.env.USER
@@ -129,7 +136,7 @@ function taskUsageText() {
129
136
  return `
130
137
  atris task - durable local task state (SQLite, gitignored)
131
138
 
132
- golden path (zero human turns):
139
+ golden path (one tick, by cron or by hand):
133
140
  atris task delegate "fix the login bug" --to <member>
134
141
  atris task claim <id> --as <member>
135
142
  ... build ...
@@ -137,7 +144,7 @@ atris task - durable local task state (SQLite, gitignored)
137
144
  atris autoland tick # second check runs, task lands
138
145
 
139
146
  atris task Show the task desk
140
- atris task new "<title>" Create a task
147
+ atris task new "<title>" [--verify <cmd>] Create a task; weak or missing checks are marked degraded
141
148
  atris task next [--tag <tag>] [--create-next]
142
149
  Claim/show next open task; optionally create the generated Endgame fallback
143
150
  atris task continue-work <id> Create/reuse a certified Review follow-up task
@@ -169,8 +176,8 @@ atris task - durable local task state (SQLite, gitignored)
169
176
  atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
170
177
  atris task revise <id> --note "..." Send reviewed work back to Do
171
178
 
172
- atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
173
- 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
174
181
  atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
175
182
  Record a task-owned Plan stage
176
183
  atris task do <id> --as <owner> --first-move "..."
@@ -213,6 +220,7 @@ atris task - durable local task state (SQLite, gitignored)
213
220
  Sweep off-roadmap/duplicate work as archived (not failed);
214
221
  --from-failed opts in to relabel a fail-closed row (never done)
215
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
216
224
  atris task relabel-archived [--dry-run|--apply]
217
225
  One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
218
226
  atris task finish <id> --proof "..." Legacy alias for done with proof
@@ -270,10 +278,6 @@ function flag(args, name) {
270
278
  return args[i + 1] || true;
271
279
  }
272
280
 
273
- function hasFlag(args, name) {
274
- return args.indexOf(name) !== -1;
275
- }
276
-
277
281
  function taskScopeEverywhere(args = [], options = {}) {
278
282
  if (options.everywhere !== undefined) return Boolean(options.everywhere);
279
283
  return hasFlag(args, '--everywhere');
@@ -5176,13 +5180,49 @@ function evaluateReviewAutoAccept(task, root) {
5176
5180
  if (!protectedGate.ok) return { eligible: false, ref, reason: protectedGate.reason };
5177
5181
  const sizeGate = reviewAutoAcceptSizeGate(task, root);
5178
5182
  if (!sizeGate.ok) return { eligible: false, ref, reason: sizeGate.reason, size: sizeGate.stats };
5179
- 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 });
5180
5188
  if (!evaluation.eligible) {
5181
5189
  return {
5182
5190
  ...evaluation,
5183
5191
  size: sizeGate.stats,
5184
5192
  };
5185
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
+ }
5186
5226
  return {
5187
5227
  ...evaluation,
5188
5228
  eligible: true,
@@ -5605,7 +5645,7 @@ function cmdReviews(args) {
5605
5645
  if (item.accept_command) {
5606
5646
  console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
5607
5647
  } else if (item.blocked_accept_reason) {
5608
- console.log(` approve: blocked (${item.blocked_accept_reason})`);
5648
+ console.log(` approve: blocked; ${plainLandingReason(item.blocked_accept_reason)}`);
5609
5649
  console.log(` rework: ${item.revise_command}`);
5610
5650
  }
5611
5651
  });
@@ -5814,7 +5854,8 @@ function formatTaskLine(task) {
5814
5854
  const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
5815
5855
  const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
5816
5856
  const tag = task.tag ? ` #${task.tag}` : '';
5817
- 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}`;
5818
5859
  }
5819
5860
 
5820
5861
  function cmdStatus(args) {
@@ -5909,7 +5950,8 @@ function renderTaskDesk(rows, refRows = rows) {
5909
5950
  const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
5910
5951
  const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
5911
5952
  const tag = r.tag ? ` #${r.tag}` : '';
5912
- 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}`);
5913
5955
  console.log(` ${r.title}`);
5914
5956
  }
5915
5957
  if (active.length === 0) console.log('clear no active tasks');
@@ -5927,12 +5969,14 @@ function cmdAdd(args) {
5927
5969
  const tag = flag(args, '--tag');
5928
5970
  const goalId = flag(args, '--goal-id');
5929
5971
  const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
5972
+ const verify = flag(args, '--verify');
5930
5973
  const metadata = {};
5931
5974
  if (goalId && goalId !== true) metadata.goal_id = String(goalId);
5932
5975
  if (goalObjective && goalObjective !== true) {
5933
5976
  metadata.task_goal = String(goalObjective);
5934
5977
  metadata.goal_objective = String(goalObjective);
5935
5978
  }
5979
+ if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
5936
5980
  const taskDb = getTaskDb();
5937
5981
  const db = taskDb.open();
5938
5982
  const ws = taskDb.workspaceRoot();
@@ -5999,6 +6043,7 @@ function delegateTask(args, options = {}) {
5999
6043
  const note = flag(args, '--note');
6000
6044
  const goalId = flag(args, '--goal-id');
6001
6045
  const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
6046
+ const verify = flag(args, '--verify');
6002
6047
  const claimNow = hasFlag(args, '--claim');
6003
6048
  const requestedOwner = flag(args, '--to') || flag(args, '--as');
6004
6049
  const explicitExecutedBy = flag(args, '--executed-by');
@@ -6036,6 +6081,7 @@ function delegateTask(args, options = {}) {
6036
6081
  metadata.task_goal = String(goalObjective);
6037
6082
  metadata.goal_objective = String(goalObjective);
6038
6083
  }
6084
+ if (typeof verify === 'string' && verify.trim()) metadata.verify = verify.trim();
6039
6085
  const result = taskDb.addTask(db, {
6040
6086
  title,
6041
6087
  tag: typeof tag === 'string' ? tag : null,
@@ -6191,7 +6237,8 @@ function cmdDay(args) {
6191
6237
  for (const task of group.tasks) {
6192
6238
  const tag = task.tag ? ` #${task.tag}` : '';
6193
6239
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
6194
- 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)}`);
6195
6242
  }
6196
6243
  }
6197
6244
  if (textView.hiddenTasks > 0) {
@@ -6260,7 +6307,8 @@ function cmdList(args) {
6260
6307
  for (const r of displayRows) {
6261
6308
  const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
6262
6309
  const tag = r.tag ? ` #${r.tag}` : '';
6263
- 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}`);
6264
6312
  }
6265
6313
  }
6266
6314
 
@@ -6290,7 +6338,11 @@ function guardExplicitActor(command, value) {
6290
6338
  // instead of just reporting the failure, straight from the live projection
6291
6339
  // (never the rendered file), preferring the same tag when one is open.
6292
6340
  function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
6293
- 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
+ ));
6294
6346
  if (!open.length) return null;
6295
6347
  const normalizedTag = String(tag || '').trim().toLowerCase();
6296
6348
  if (normalizedTag) {
@@ -6540,13 +6592,31 @@ function cmdNext(args) {
6540
6592
  const reviewTasks = filterTasksByScope(reviewProjection.projection.tasks || [], scope)
6541
6593
  .map(compactTaskForStatus)
6542
6594
  .filter(task => task && task.review && task.review.handoff);
6543
- const open = filterTasksByScope(taskDb.listTasks(db, {
6595
+ const openAll = filterTasksByScope(taskDb.listTasks(db, {
6544
6596
  workspaceRoot: taskDb.workspaceRoot(),
6545
6597
  status: 'open',
6546
- limit: scoped ? null : 1,
6598
+ limit: scoped ? null : 50,
6547
6599
  }), scope);
6600
+ const open = openAll.filter((task) => !isDecisionTask(task));
6601
+ const skippedDecisions = openAll.length - open.length;
6548
6602
  if (!open.length) {
6549
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
+ }
6550
6620
  const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again')
6551
6621
  || reviewTasks.find(task => task.review.handoff.next_action === 'continue_work')
6552
6622
  || reviewTasks.find(task => task.review.handoff.next_action === 'human_accept_waiting');
@@ -7197,6 +7267,12 @@ function cmdShow(args) {
7197
7267
  : task.status.toUpperCase();
7198
7268
  console.log(`${statusLabel} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
7199
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
+ }
7200
7276
  if (task.review) {
7201
7277
  console.log('');
7202
7278
  printReviewLanding(task.review);
@@ -9195,6 +9271,36 @@ function cmdClearDone(args) {
9195
9271
  console.log(`cleared ${candidates.length} completed task(s).`);
9196
9272
  }
9197
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
+
9198
9304
  // One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
9199
9305
  // reset" archived ~125 certified, proof-backed tasks by writing status
9200
9306
  // 'failed' (no distinct archived status existed yet). This relabels exactly
@@ -9281,6 +9387,20 @@ function cmdReady(args) {
9281
9387
  if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
9282
9388
  process.exit(1);
9283
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
+ }
9284
9404
  const base = proof.trim();
9285
9405
  proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
9286
9406
  if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
@@ -9353,7 +9473,10 @@ function cmdReady(args) {
9353
9473
  nextTask: nextTaskInput.nextTask,
9354
9474
  });
9355
9475
  const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
9356
- 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);
9357
9480
  const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
9358
9481
  const handoff = {
9359
9482
  native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
@@ -9361,10 +9484,10 @@ function cmdReady(args) {
9361
9484
  next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
9362
9485
  rule: autolandOn
9363
9486
  ? (agentCertified
9364
- ? 'double-check complete; autoland will accept this on the next tick.'
9487
+ ? `double-check complete; autoland will accept this ${whenLands}.`
9365
9488
  : needsExternalVerifier
9366
9489
  ? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
9367
- : '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}.`)
9368
9491
  : (agentCertified
9369
9492
  ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
9370
9493
  : 'proof is ready; one more agent check before human approval. XP waits for the human.'),
@@ -9681,6 +9804,52 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
9681
9804
  `).run(JSON.stringify(metadata), Date.now(), taskId);
9682
9805
  }
9683
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
+
9684
9853
  // `atris task ready --verify "<cmd>"` already runs the command live and
9685
9854
  // gates on exit 0, but that alone leaves no machine re-runnable trace once
9686
9855
  // the proof text scrolls out of easy reach: storing the exact command on
@@ -9722,6 +9891,7 @@ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier',
9722
9891
  };
9723
9892
  }
9724
9893
  const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
9894
+ stampVerifyCacheMetadata(taskDb, db, task.id, { verify, result, actor, workspaceRoot: task.workspace_root });
9725
9895
  if (result.ok) return { ok: true, verify, result };
9726
9896
  const note = landingVerifyFailureNote(verify, result);
9727
9897
  const revised = taskDb.reviseTask(db, {
@@ -9791,8 +9961,10 @@ function cmdCertifyVerified(args, options = {}) {
9791
9961
  // "certified" alone is not landable.
9792
9962
  // strictVerify stays off in this eligibility probe: certify-verified runs
9793
9963
  // the check itself right below, and strict mode here would execute it a
9794
- // second time per row before the real run.
9795
- 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 });
9796
9968
  if (evaluation.eligible) {
9797
9969
  results.push({ ref, action: 'skipped', reason: 'already_landable' });
9798
9970
  continue;
@@ -9821,6 +9993,7 @@ function cmdCertifyVerified(args, options = {}) {
9821
9993
  continue;
9822
9994
  }
9823
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 });
9824
9997
  if (!run.ok) {
9825
9998
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
9826
9999
  continue;
@@ -10109,7 +10282,7 @@ function autoAcceptSweepDeniedReason(task) {
10109
10282
  for (const value of autoAcceptSweepLabelValues(task)) {
10110
10283
  const tokens = autoAcceptSweepNormalizedTokens(value);
10111
10284
  for (const token of tokens) {
10112
- if (token === 'needs-human' || token === 'needshuman') {
10285
+ if (token === 'needs-human' || token === 'needshuman' || token === 'decision') {
10113
10286
  return 'needs_human';
10114
10287
  }
10115
10288
  const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
@@ -11137,7 +11310,45 @@ function taskHasReview(task) {
11137
11310
  return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
11138
11311
  }
11139
11312
 
11140
- 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) {
11141
11352
  return `<!doctype html>
11142
11353
  <html lang="en">
11143
11354
  <head>
@@ -11296,7 +11507,7 @@ function taskBoardHtml() {
11296
11507
  ['blocked', 'Blocked'],
11297
11508
  ['done', 'Done']
11298
11509
  ];
11299
- const planTags = new Set(${JSON.stringify(Array.from(STATUS_PLAN_TAGS))});
11510
+ const planTags = new Set(${JSON.stringify(model.planTags)});
11300
11511
  let state = { tasks: [] };
11301
11512
  let selected = null;
11302
11513
  const $ = (id) => document.getElementById(id);
@@ -11569,6 +11780,10 @@ function taskBoardHtml() {
11569
11780
  </html>`;
11570
11781
  }
11571
11782
 
11783
+ function taskBoardHtml() {
11784
+ return taskBoardTemplate(taskBoardViewModel());
11785
+ }
11786
+
11572
11787
  function readJsonBody(req) {
11573
11788
  return new Promise((resolve, reject) => {
11574
11789
  let body = '';
@@ -11587,10 +11802,11 @@ function readJsonBody(req) {
11587
11802
  });
11588
11803
  }
11589
11804
 
11590
- function sendJson(res, status, value) {
11805
+ function sendJson(res, status, value, headers = {}) {
11591
11806
  res.writeHead(status, {
11592
11807
  'content-type': 'application/json; charset=utf-8',
11593
11808
  'access-control-allow-origin': 'http://localhost',
11809
+ ...headers,
11594
11810
  });
11595
11811
  res.end(JSON.stringify(value, null, 2));
11596
11812
  }
@@ -11600,551 +11816,655 @@ function sendHtml(res, value) {
11600
11816
  res.end(value);
11601
11817
  }
11602
11818
 
11603
- async function handleTaskApi(req, res, taskDb, db) {
11604
- const url = new URL(req.url, 'http://127.0.0.1');
11605
- if (req.method === 'OPTIONS') return sendJson(res, 200, { ok: true });
11606
- if (req.method === 'GET' && url.pathname === '/') return sendHtml(res, taskBoardHtml());
11607
- if (req.method === 'GET' && url.pathname === '/api/tasks') {
11608
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11609
- return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
11610
- }
11611
- if (req.method === 'GET' && url.pathname === '/api/stream') {
11612
- // Time-ordered feed of what the agent actually did + heartbeat liveness.
11613
- const { projection } = writeDefaultProjection(taskDb, db);
11614
- const root = projection.workspace_root || process.cwd();
11615
- const stateDir = path.join(root, '.atris', 'state');
11616
- const activity = require('../lib/activity-stream');
11617
- const { readJsonl } = require('../lib/pulse');
11618
- const rd = (f) => readJsonl(path.join(stateDir, f));
11619
- const pulseReceipts = rd('pulse_agi_loop_receipts.jsonl');
11620
- const events = activity.buildActivityStream({
11621
- pulseReceipts,
11622
- scorecards: rd('scorecards.jsonl'),
11623
- taskEpisodes: rd('task_episodes.jsonl').slice(-200),
11624
- xpReceipts: rd('career_xp_receipts.jsonl').slice(-200),
11625
- missionEvents: rd('mission_events.jsonl').slice(-200),
11626
- }, { limit: 60 });
11627
- return sendJson(res, 200, { ok: true, heartbeat: activity.buildHeartbeat(pulseReceipts), events });
11628
- }
11629
- if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities') {
11630
- return sendJson(res, 200, {
11631
- ok: true,
11632
- action: 'capabilities',
11633
- capabilities: taskCapabilitiesContract(),
11634
- safety: {
11635
- read_only: true,
11636
- claims_work: false,
11637
- human_accept: false,
11638
- xp_after_human_accept: true,
11639
- },
11640
- });
11641
- }
11642
- if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities/check') {
11643
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11644
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11645
- const limitParam = url.searchParams.get('limit');
11646
- const limit = limitParam ? Number(limitParam) : 8;
11647
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11648
- const report = taskCapabilitiesCheckReport(taskDb, db, [], {
11649
- owner,
11650
- reviewer,
11651
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11652
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11653
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11654
- scope,
11655
- });
11656
- return sendJson(res, report.ok ? 200 : 409, report);
11657
- }
11658
- if (req.method === 'GET' && url.pathname === '/api/tasks/review-lane-drain') {
11659
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11660
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11661
- const limitParam = url.searchParams.get('limit');
11662
- const limit = limitParam ? Number(limitParam) : 8;
11663
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11664
- const report = taskReviewLaneDrainReport(taskDb, db, [], {
11665
- owner,
11666
- reviewer,
11667
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11668
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11669
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11670
- scope,
11671
- });
11672
- return sendJson(res, report.ok ? 200 : 409, report);
11673
- }
11674
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-act') {
11675
- const body = await readJsonBody(req);
11676
- const result = taskReviewLaneAct(taskDb, db, taskReviewLaneActOptionsFromBody(body, url.searchParams));
11677
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11678
- }
11679
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-loop') {
11680
- const body = await readJsonBody(req);
11681
- const result = taskReviewLaneLoop(taskDb, db, taskReviewLaneLoopOptionsFromBody(body, url.searchParams));
11682
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11683
- }
11684
- if (req.method === 'POST' && url.pathname === '/api/tasks/review-lane-run') {
11685
- const body = await readJsonBody(req);
11686
- const result = taskReviewLaneRun(taskDb, db, taskReviewLaneRunOptionsFromBody(body, url.searchParams));
11687
- return sendJson(res, result.ok ? 200 : result.status || 409, result);
11688
- }
11689
- if (req.method === 'GET' && (url.pathname === '/api/tasks/current' || url.pathname === '/api/tasks/queue')) {
11690
- const owner = url.searchParams.get('owner') || url.searchParams.get('as') || DEFAULT_OWNER;
11691
- const reviewer = url.searchParams.get('reviewer') || url.searchParams.get('as_reviewer') || 'codex-review';
11692
- const limitParam = url.searchParams.get('limit');
11693
- const limit = limitParam ? Number(limitParam) : 8;
11694
- const scope = taskQueueScopeFromSearchParams(url.searchParams);
11695
- const { outPath, current } = buildTaskCurrent(taskDb, db, [], {
11696
- owner,
11697
- reviewer,
11698
- all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11699
- everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
11700
- limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
11701
- scope,
11702
- });
11703
- const action = url.pathname.endsWith('/queue') ? 'queue' : 'current';
11704
- return sendJson(res, 200, {
11705
- ok: true,
11706
- action,
11707
- projection_path: outPath,
11708
- current,
11709
- selected: current.selected,
11710
- page: current.page,
11711
- queue: current.queue,
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,
11712
11946
  });
11713
11947
  }
11714
- if (req.method === 'POST' && url.pathname === '/api/tasks/current/step') {
11715
- const body = await readJsonBody(req);
11716
- const options = taskCurrentStepOptionsFromBody(body, url.searchParams);
11717
- try {
11718
- const result = runCurrentTaskStep(taskDb, db, options);
11719
- return sendJson(res, 200, result);
11720
- } catch (error) {
11721
- const errorCurrent = error.current || null;
11722
- return sendJson(res, error.status || 409, {
11723
- ok: false,
11724
- action: 'current_step',
11725
- reason: error.reason || 'step_failed',
11726
- detail: error.message,
11727
- selected_task_id: errorCurrent ? errorCurrent.selected_task_id : null,
11728
- selected_ref: errorCurrent ? errorCurrent.selected_ref : null,
11729
- selected_next_key: selectedNextKeyFromCurrent(errorCurrent),
11730
- current: errorCurrent,
11731
- page: error.page || null,
11732
- });
11733
- }
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') });
11734
11976
  }
11735
- if (req.method === 'POST' && url.pathname === '/api/tasks') {
11736
- const body = await readJsonBody(req);
11737
- const title = String(body.title || '').trim();
11738
- if (!title) return sendJson(res, 400, { ok: false, reason: 'missing_title', detail: 'title required' });
11739
- const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
11740
- const result = taskDb.addTask(db, {
11741
- title,
11742
- tag: body.tag ? String(body.tag) : 'tasks',
11743
- workspaceRoot: taskDb.workspaceRoot(),
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,
11744
12049
  });
11745
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11746
- return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, operator_title_warning: operatorTitleWarning, projection_path: outPath, task: taskFromProjection(projection, result.id) });
11747
12050
  }
11748
- if (req.method === 'POST' && url.pathname === '/api/tasks/clear-plan') {
11749
- const body = await readJsonBody(req);
11750
- if (!body.confirm && !body.yes) {
11751
- return sendJson(res, 400, { ok: false, reason: 'confirm_required', detail: stageErrorDetail('task clear-plan', 'confirm_required') });
11752
- }
11753
- const result = taskDb.clearPlanTasks(db, {
11754
- workspaceRoot: taskDb.workspaceRoot(),
11755
- actor: String(body.actor || DEFAULT_OWNER),
11756
- reason: String(body.reason || body.note || 'clear_plan'),
11757
- tag: String(body.tag || 'capture'),
11758
- });
11759
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11760
- const taskById = new Map((projection.tasks || []).map(task => [task.id, task]));
11761
- return sendJson(res, 200, {
11762
- ok: true,
11763
- action: 'clear_plan',
11764
- cleared_count: result.cleared.length,
11765
- skipped_count: result.skipped.length,
11766
- skipped: result.skipped,
11767
- projection_path: outPath,
11768
- tasks: result.cleared.map(task => taskFromProjection(projection, task.id) || taskById.get(task.id)).filter(Boolean),
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,
11769
12064
  });
11770
12065
  }
11771
- const detailMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
11772
- if (detailMatch) {
11773
- if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11774
- const resolved = resolveTaskRef(taskDb, db, detailMatch[1]);
11775
- if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11776
- const task = taskDetail(taskDb, db, resolved.id);
11777
- if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
11778
- const { outPath } = writeDefaultProjection(taskDb, db);
11779
- return sendJson(res, 200, { ok: true, action: 'detail', task_id: resolved.id, projection_path: outPath, task, page: taskPageContract(task) });
11780
- }
11781
- const pageMatch = url.pathname.match(/^\/api\/tasks\/([^/]+)\/page$/);
11782
- if (pageMatch) {
11783
- if (req.method !== 'GET') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11784
- const resolved = resolveTaskRef(taskDb, db, pageMatch[1]);
11785
- if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11786
- const task = taskDetail(taskDb, db, resolved.id);
11787
- if (!task) return sendJson(res, 404, { ok: false, reason: 'not_found' });
11788
- const { outPath } = writeDefaultProjection(taskDb, db);
11789
- return sendJson(res, 200, { ok: true, action: 'page', task_id: resolved.id, projection_path: outPath, page: taskPageContract(task) });
11790
- }
11791
- const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|chat|step|plan|do|backlog|ready|accept|revise|finish|review|review-chat|continue-work|events)$/);
11792
- if (!match) return sendJson(res, 404, { ok: false, reason: 'not_found' });
11793
- const resolved = resolveTaskRef(taskDb, db, match[1]);
11794
- if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
11795
- const taskId = resolved.id;
11796
- const op = match[2];
11797
- if (req.method === 'GET' && op === 'events') {
11798
- const events = taskDb.listTaskEvents(db, { taskId, limit: 500 });
11799
- return sendJson(res, 200, { ok: true, events });
11800
- }
11801
- if (req.method !== 'POST') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
11802
- const body = await readJsonBody(req);
11803
- if (op === 'step') {
11804
- try {
11805
- const result = runTaskStep(taskDb, db, taskId, taskStepOptionsFromBody(body));
11806
- return sendJson(res, 200, result);
11807
- } catch (error) {
11808
- return sendJson(res, error.status || 409, {
11809
- ok: false,
11810
- action: 'step',
11811
- task_id: taskId,
11812
- reason: error.reason || 'step_failed',
11813
- detail: error.message,
11814
- page: error.page || null,
11815
- });
11816
- }
11817
- }
11818
- if (op === 'continue-work') {
11819
- try {
11820
- const result = continueWorkForReviewTask(taskDb, db, taskId, { owner: body.owner || body.actor || DEFAULT_OWNER });
11821
- return sendJson(res, 200, result);
11822
- } catch (error) {
11823
- return sendJson(res, error.status || 409, {
11824
- ok: false,
11825
- action: 'continue_work',
11826
- task_id: taskId,
11827
- reason: error.reason || 'continue_work_failed',
11828
- detail: error.message,
11829
- });
11830
- }
11831
- }
11832
- if (op === 'claim') {
11833
- const owner = String(body.owner || body.actor || DEFAULT_OWNER);
11834
- const result = taskDb.claimTask(db, { id: taskId, claimedBy: owner });
11835
- if (!result.claimed) return sendJson(res, 409, { ok: false, reason: result.reason, claimed_by: result.claimed_by || null });
11836
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11837
- return sendJson(res, 200, { ok: true, action: 'claimed', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11838
- }
11839
- if (op === 'message') {
11840
- const result = taskDb.noteTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), content: String(body.content || '') });
11841
- if (!result.noted) return sendJson(res, 404, { ok: false, reason: result.reason });
11842
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11843
- return sendJson(res, 200, { ok: true, action: 'noted', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
11844
- }
11845
- if (op === 'chat') {
11846
- const result = taskDb.chatTask(db, {
11847
- id: taskId,
11848
- actor: String(body.actor || DEFAULT_OWNER),
11849
- content: String(body.content || body.message || body.text || ''),
11850
- goal: String(body.goal || body.objective || ''),
11851
- summary: String(body.summary || ''),
11852
- });
11853
- if (!result.chatted) {
11854
- const status = result.reason === 'content_required' ? 400 : result.reason === 'not_found' ? 404 : 409;
11855
- return sendJson(res, status, { ok: false, reason: result.reason, detail: stageErrorDetail('task chat', result.reason, result) });
11856
- }
11857
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11858
- return sendJson(res, 200, {
11859
- ok: true,
11860
- action: 'chatted',
11861
- task_id: taskId,
11862
- version: result.event.version,
11863
- goal_changed: result.goal_changed,
11864
- chat_packet: result.chat_packet,
11865
- projection_path: outPath,
11866
- task: taskFromProjection(projection, taskId),
11867
- });
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) });
11868
12094
  }
11869
- if (op === 'plan') {
11870
- const actor = String(body.actor || DEFAULT_OWNER);
11871
- const goal = String(body.goal || body.objective || '');
11872
- const summary = String(body.summary || body.plan || '');
11873
- const owner = String(body.owner || body.assignee || '');
11874
- const exit = String(body.exit || body.exit_condition || '');
11875
- const firstMove = String(body.first_move || body.firstMove || body.first || '');
11876
- const task = taskDetail(taskDb, db, taskId);
11877
- const automaticPlan = buildAutomaticPlanTrace(taskDb, task, {
11878
- actor,
11879
- actorExplicit: Boolean(body.actor),
11880
- owner,
11881
- goal,
11882
- summary,
11883
- firstMove,
11884
- exit,
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),
11885
12190
  });
11886
- const result = taskDb.stageTask(db, {
11887
- id: taskId,
11888
- actor,
11889
- stage: 'plan',
11890
- goal,
11891
- summary,
11892
- owner: automaticPlan.ownerForStage || owner,
11893
- exit,
11894
- proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
11895
- firstMove,
11896
- nextButton: String(body.next_button || body.nextButton || ''),
11897
- confidence: body.confidence,
11898
- planTrace: automaticPlan.trace,
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,
11899
12198
  });
11900
- if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task plan', result.reason, result) });
11901
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11902
- 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) });
11903
12199
  }
11904
- if (op === 'do') {
11905
- const firstMove = String(body.first_move || body.firstMove || body.first || '').trim();
11906
- if (!firstMove) return sendJson(res, 400, { ok: false, reason: 'first_move_required', detail: 'task do: first_move required' });
11907
- const result = taskDb.stageTask(db, {
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, {
11908
12222
  id: taskId,
11909
12223
  actor: String(body.actor || DEFAULT_OWNER),
11910
- stage: 'do',
11911
- goal: String(body.goal || body.objective || ''),
11912
- summary: String(body.summary || ''),
11913
- owner: String(body.actor || DEFAULT_OWNER),
11914
- exit: String(body.exit || body.exit_condition || body.exitCondition || ''),
11915
- proofNeeded: String(body.proof_needed || body.proofNeeded || body.proof || body.verify || ''),
11916
- firstMove,
11917
- nextButton: String(body.next_button || body.nextButton || ''),
11918
- confidence: body.confidence,
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,
11919
12229
  });
11920
- if (!result.staged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task do', result.reason, result) });
11921
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11922
- 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) });
12230
+ episode = reviewed.episode;
12231
+ nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
12232
+ xpProjection = refreshCareerXpAfterReview(reviewed);
11923
12233
  }
11924
- if (op === 'backlog') {
11925
- const result = taskDb.backlogTask(db, {
11926
- id: taskId,
11927
- actor: String(body.actor || DEFAULT_OWNER),
11928
- reason: String(body.reason || body.note || 'clear_plan'),
11929
- tag: String(body.tag || 'capture'),
11930
- });
11931
- if (!result.backlogged) return sendJson(res, 409, { ok: false, reason: result.reason, detail: stageErrorDetail('task backlog', result.reason, result) });
11932
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11933
- return sendJson(res, 200, {
11934
- ok: true,
11935
- action: 'backlogged',
11936
- task_id: taskId,
11937
- version: result.event.version,
11938
- cleared_keys: result.cleared_keys,
11939
- projection_path: outPath,
11940
- task: taskFromProjection(projection, taskId),
11941
- });
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 });
11942
12258
  }
11943
- if (op === 'review-chat') {
11944
- try {
11945
- const result = appendTaskReviewChat(taskDb, db, taskId, {
11946
- reviewer: body.reviewer || body.actor || 'codex-review',
11947
- dryRun: Boolean(body.dryRun || body.noNote),
11948
- });
11949
- const { event, compactProjection, outPath, ...payload } = result;
11950
- return sendJson(res, 200, payload);
11951
- } catch (error) {
11952
- return sendJson(res, error.status || 409, {
11953
- ok: false,
11954
- reason: error.reason || 'review_chat_failed',
11955
- detail: error.message,
11956
- });
11957
- }
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 }) {
12419
+ const resolved = resolveTaskRef(taskDb, db, match[1]);
12420
+ if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
12421
+ const taskId = resolved.id;
12422
+ const operation = match[2];
12423
+ if (req.method === 'GET' && operation === 'events') {
12424
+ return serveTaskEvents({ res, taskDb, db, taskId });
11958
12425
  }
11959
- if (op === 'finish') {
11960
- const currentTask = taskDb.getTask(db, taskId);
11961
- const failed = Boolean(body.failed);
11962
- const proof = String(body.proof || '').trim();
11963
- const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
11964
- const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
11965
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
11966
- const done = taskDb.doneTask(db, {
11967
- id: taskId,
11968
- status: failed ? 'failed' : 'done',
11969
- actor: String(body.actor || DEFAULT_OWNER),
11970
- action: failed ? 'failed' : 'finished',
11971
- proof,
11972
- });
11973
- if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
11974
- let episode = null;
11975
- let nextCreated = null;
11976
- let xpProjection = null;
11977
- if (shouldReview) {
11978
- const reviewed = taskDb.reviewTask(db, {
11979
- id: taskId,
11980
- actor: String(body.actor || DEFAULT_OWNER),
11981
- reward: body.reward === undefined ? 1 : body.reward,
11982
- lesson: String(body.lesson || ''),
11983
- nextTask: String(body.next || ''),
11984
- proof: String(body.proof || ''),
11985
- careerXpEligible: false,
11986
- });
11987
- episode = reviewed.episode;
11988
- nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
11989
- xpProjection = refreshCareerXpAfterReview(reviewed);
12426
+ if (req.method !== 'POST') return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' });
12427
+ const body = await readJsonBody(req);
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;
11990
12439
  }
11991
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
11992
- return sendJson(res, 200, {
11993
- ok: true,
11994
- action: 'finished',
11995
- task_id: taskId,
11996
- reviewed: Boolean(episode),
11997
- episode,
11998
- xp_projection: xpProjection,
11999
- next_task_id: nextCreated ? nextCreated.id : null,
12000
- projection_path: outPath,
12001
- task: taskFromProjection(projection, taskId),
12002
- });
12440
+ const match = url.pathname.match(route.pattern);
12441
+ if (match) return { route, match };
12003
12442
  }
12004
- if (op === 'ready') {
12005
- const proof = String(body.proof || '').trim();
12006
- const proofIssue = meaningfulTaskProofIssue(proof);
12007
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12008
- const nextTaskInput = normalizeReviewNextTaskInput(body.next);
12009
- const actor = String(body.actor || DEFAULT_OWNER);
12010
- const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
12011
- if (resultText) {
12012
- const resultIssue = resultSentenceIssue(resultText);
12013
- if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
12014
- }
12015
- const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
12016
- actor,
12017
- proof,
12018
- changed: body.changed || resultText || body.done,
12019
- checked: body.checked || body.check || body.verified,
12020
- passed: body.passed || body.pass,
12021
- failed: body.failed || body.fail,
12022
- cost: body.cost,
12023
- saved: body.saved || body.savings,
12024
- tryNext: body.try_next || body.tryNext || body.try || body.handoff,
12025
- status: body.status,
12026
- files: body.files,
12027
- commands: body.commands || body.command,
12028
- });
12029
- const result = taskDb.readyTask(db, {
12030
- id: taskId,
12031
- actor,
12032
- proof,
12033
- lesson: String(body.lesson || ''),
12034
- nextTask: nextTaskInput.nextTask,
12035
- resultTrace: resultTrace && resultTrace.trace,
12036
- result: resultText,
12037
- landing: body.landing || {
12038
- happened: body.happened,
12039
- checked: body.checked,
12040
- tested: body.tested,
12041
- decision: body.decision,
12042
- },
12043
- });
12044
- if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
12045
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12046
- return sendJson(res, 200, {
12047
- ok: true,
12048
- action: 'ready',
12049
- task_id: taskId,
12050
- result_trace: resultTrace,
12051
- ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
12052
- projection_path: outPath,
12053
- task: taskFromProjection(projection, taskId),
12054
- });
12055
- }
12056
- if (op === 'accept') {
12057
- const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
12058
- const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
12059
- const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
12060
- const proofIssue = meaningfulTaskProofIssue(proof);
12061
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12062
- const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
12063
- const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
12064
- const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
12065
- const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
12066
- const clearedFields = [];
12067
- if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
12068
- if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
12069
- const parsedReward = parseAcceptReward(body.reward);
12070
- if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
12071
- const done = taskDb.doneTask(db, {
12072
- id: taskId,
12073
- status: 'done',
12074
- actor: String(body.actor || DEFAULT_OWNER),
12075
- allowReview: true,
12076
- action: 'accepted',
12077
- proof,
12078
- });
12079
- if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
12080
- const reviewed = taskDb.reviewTask(db, {
12081
- id: taskId,
12082
- actor: String(body.actor || DEFAULT_OWNER),
12083
- reward: parsedReward.value,
12084
- lesson,
12085
- nextTask,
12086
- proof,
12087
- careerXpEligible: true,
12088
- clearedFields,
12089
- });
12090
- const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12091
- const xpProjection = refreshCareerXpAfterReview(reviewed);
12092
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12093
- 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) });
12094
- }
12095
- if (op === 'revise') {
12096
- const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
12097
- if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
12098
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12099
- return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
12100
- }
12101
- if (op === 'review') {
12102
- const currentTask = taskDb.getTask(db, taskId);
12103
- const rewardValue = body.reward === undefined ? 0 : body.reward;
12104
- const proof = String(body.proof || '').trim();
12105
- const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
12106
- const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next')
12107
- || Object.prototype.hasOwnProperty.call(body, 'next_task')
12108
- || Object.prototype.hasOwnProperty.call(body, 'nextTask');
12109
- const lessonText = hasExplicitLesson ? String(body.lesson || '') : '';
12110
- const rawNext = Object.prototype.hasOwnProperty.call(body, 'next')
12111
- ? body.next
12112
- : Object.prototype.hasOwnProperty.call(body, 'next_task')
12113
- ? body.next_task
12114
- : body.nextTask;
12115
- const nextTaskInput = normalizeReviewNextTaskInput(hasExplicitNext ? rawNext : '');
12116
- const clearedFields = [];
12117
- if (hasExplicitLesson && !lessonText.trim()) clearedFields.push('lesson');
12118
- if (hasExplicitNext && !String(rawNext || '').trim()) clearedFields.push('next_task');
12119
- const proofIssue = Number(rewardValue) > 0 || proof
12120
- ? meaningfulTaskProofIssue(proof)
12121
- : null;
12122
- if (proofIssue) return sendProofIssue(res, proof, proofIssue);
12123
- const reviewed = taskDb.reviewTask(db, {
12124
- id: taskId,
12125
- actor: String(body.actor || DEFAULT_OWNER),
12126
- reward: rewardValue,
12127
- lesson: lessonText,
12128
- nextTask: nextTaskInput.nextTask,
12129
- proof,
12130
- careerXpEligible: false,
12131
- clearedFields,
12132
- });
12133
- const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
12134
- const xpProjection = refreshCareerXpAfterReview(reviewed);
12135
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
12136
- return sendJson(res, 200, {
12137
- ok: true,
12138
- action: 'reviewed',
12139
- task_id: taskId,
12140
- episode: reviewed.episode,
12141
- xp_projection: xpProjection,
12142
- next_task_id: nextCreated ? nextCreated.id : null,
12143
- ...(nextTaskInput.ignored ? { review_next_task_ignored: nextTaskInput.ignored } : {}),
12144
- projection_path: outPath,
12145
- 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) });
12146
12466
  });
12147
- }
12467
+ });
12148
12468
  }
12149
12469
 
12150
12470
  function cmdServe(args) {
@@ -12152,11 +12472,7 @@ function cmdServe(args) {
12152
12472
  const port = Number(flag(args, '--port') || process.env.PORT || 8787);
12153
12473
  const taskDb = getTaskDb();
12154
12474
  const db = taskDb.open();
12155
- const server = http.createServer((req, res) => {
12156
- handleTaskApi(req, res, taskDb, db).catch((e) => {
12157
- sendJson(res, 500, { ok: false, reason: 'server_error', detail: String(e && e.message || e) });
12158
- });
12159
- });
12475
+ const server = createTaskApiServer({ taskDb, db });
12160
12476
  return new Promise((resolve, reject) => {
12161
12477
  server.on('error', reject);
12162
12478
  server.listen(port, host, () => {
@@ -12281,6 +12597,9 @@ async function runTaskCommand(args) {
12281
12597
  case 'fail': return cmdDone([...rest, '--failed']);
12282
12598
  case 'archive': return cmdArchive(rest);
12283
12599
  case 'clear-done': return cmdClearDone(rest);
12600
+ case 'reap-mission-blockers':
12601
+ case 'reap-blockers':
12602
+ return cmdReapMissionBlockers(rest);
12284
12603
  case 'relabel-archived': return cmdRelabelArchived(rest);
12285
12604
  case 'review': return cmdReview(rest);
12286
12605
  case 'reviews':
@@ -12321,7 +12640,7 @@ const MUTATING_TASK_COMMANDS = new Set([
12321
12640
  'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
12322
12641
  'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
12323
12642
  'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
12324
- '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',
12325
12644
  'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
12326
12645
  'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
12327
12646
  ]);
@@ -12337,6 +12656,7 @@ async function run(args) {
12337
12656
 
12338
12657
  module.exports = {
12339
12658
  run,
12659
+ createTaskApiServer,
12340
12660
  taskDayGroups,
12341
12661
  taskDayTextGroups,
12342
12662
  taskDayTitle,
@@ -12347,4 +12667,6 @@ module.exports = {
12347
12667
  autoRenderTodoFromDb,
12348
12668
  projectionMissions,
12349
12669
  projectionWishes,
12670
+ taskBoardViewModel,
12671
+ taskBoardTemplate,
12350
12672
  };