atris 3.35.0 → 3.36.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 (133) hide show
  1. package/AGENTS.md +37 -0
  2. package/README.md +5 -3
  3. package/atris/GETTING_STARTED.md +1 -1
  4. package/atris/atris.md +3 -0
  5. package/atris/policies/day-loop-voice.md +102 -0
  6. package/atris/policies/outbound-artifact-gate.md +2 -0
  7. package/atris/skills/design/SKILL.md +56 -32
  8. package/atris/skills/endgame/SKILL.md +12 -6
  9. package/atris/skills/engines/SKILL.md +22 -4
  10. package/atris/skills/fable-method/SKILL.md +66 -0
  11. package/atris/skills/improve/SKILL.md +65 -45
  12. package/atris/skills/youtube/SKILL.md +10 -1
  13. package/atris.md +2 -0
  14. package/ax +147 -19
  15. package/bin/atris.js +565 -265
  16. package/commands/activate.js +194 -88
  17. package/commands/agents.js +166 -0
  18. package/commands/autoland.js +459 -107
  19. package/commands/autopilot-front.js +20 -2
  20. package/commands/autopilot.js +118 -2
  21. package/commands/avail.js +407 -0
  22. package/commands/bench.js +188 -0
  23. package/commands/brain.js +3 -0
  24. package/commands/brief.js +651 -0
  25. package/commands/business-sync.js +192 -6
  26. package/commands/clean.js +50 -24
  27. package/commands/close.js +1083 -0
  28. package/commands/cloud.js +245 -0
  29. package/commands/compile.js +292 -1
  30. package/commands/computer.js +150 -3
  31. package/commands/dream.js +365 -0
  32. package/commands/drill.js +371 -0
  33. package/commands/engine.js +993 -32
  34. package/commands/experiments.js +28 -0
  35. package/commands/feedback.js +34 -12
  36. package/commands/fleet-report.js +206 -0
  37. package/commands/gm.js +23 -0
  38. package/commands/goal.js +247 -0
  39. package/commands/improve.js +642 -26
  40. package/commands/init.js +72 -44
  41. package/commands/interview.js +67 -1
  42. package/commands/land.js +152 -52
  43. package/commands/lifecycle.js +39 -3
  44. package/commands/log.js +84 -1
  45. package/commands/loops.js +220 -16
  46. package/commands/meet.js +220 -0
  47. package/commands/member.js +511 -34
  48. package/commands/mission.js +3029 -339
  49. package/commands/next.js +137 -0
  50. package/commands/now.js +220 -25
  51. package/commands/one-lap.js +776 -0
  52. package/commands/orb.js +314 -0
  53. package/commands/pack-craft.js +179 -0
  54. package/commands/pack.js +823 -0
  55. package/commands/play.js +3 -2
  56. package/commands/probe.js +30 -3
  57. package/commands/pulse.js +241 -46
  58. package/commands/push.js +260 -82
  59. package/commands/rainmaker.js +49 -0
  60. package/commands/report.js +415 -0
  61. package/commands/scout.js +147 -0
  62. package/commands/search.js +363 -0
  63. package/commands/skill.js +47 -3
  64. package/commands/slop.js +50 -2
  65. package/commands/soul.js +1 -1
  66. package/commands/stream.js +861 -0
  67. package/commands/study.js +693 -0
  68. package/commands/sync.js +67 -54
  69. package/commands/task.js +1346 -117
  70. package/commands/team.js +73 -0
  71. package/commands/verify.js +96 -0
  72. package/commands/watch.js +303 -0
  73. package/commands/wish.js +500 -0
  74. package/commands/workflow.js +11 -5
  75. package/commands/worktree.js +234 -13
  76. package/commands/xp.js +29 -11
  77. package/lib/auto-accept-certified.js +331 -34
  78. package/lib/autoland.js +319 -54
  79. package/lib/ax-auto-lane.js +79 -0
  80. package/lib/bench/context.js +147 -0
  81. package/lib/bench/engines.js +141 -0
  82. package/lib/bench/report.js +140 -0
  83. package/lib/bench/runner.js +512 -0
  84. package/lib/brief-ledger.js +350 -0
  85. package/lib/cloud-mission.js +259 -0
  86. package/lib/codex-flight.js +154 -0
  87. package/lib/default-runner.js +45 -0
  88. package/lib/default-verifier.js +70 -0
  89. package/lib/engine-registry.js +232 -0
  90. package/lib/experiments/daily.js +640 -0
  91. package/lib/fleet.js +2219 -67
  92. package/lib/improve-vitals-html.js +171 -0
  93. package/lib/known-commands.js +58 -0
  94. package/lib/loop-doctor.js +416 -0
  95. package/lib/member-switches.js +144 -0
  96. package/lib/mission-room.js +1 -0
  97. package/lib/mission-root.js +52 -0
  98. package/lib/next-moves.js +327 -10
  99. package/lib/one-lap-validator.js +60 -0
  100. package/lib/orb-context.js +477 -0
  101. package/lib/orb-scorecard.js +224 -0
  102. package/lib/policy-lessons.js +52 -1
  103. package/lib/pulse.js +277 -3
  104. package/lib/receipt-block.js +168 -0
  105. package/lib/receipt-evidence.js +65 -4
  106. package/lib/router-brain.js +352 -0
  107. package/lib/runner-command.js +10 -0
  108. package/lib/self-drive.js +258 -0
  109. package/lib/short-name.js +103 -0
  110. package/lib/spawn-env.js +18 -0
  111. package/lib/state-detection.js +56 -1
  112. package/lib/sync-status.js +59 -0
  113. package/lib/task-db.js +108 -29
  114. package/lib/task-proof.js +23 -1
  115. package/lib/team-presence.js +260 -0
  116. package/lib/tool-result-encode.js +7 -0
  117. package/lib/trust-tiers.js +90 -0
  118. package/lib/usage.js +107 -0
  119. package/lib/voice-gate.js +163 -0
  120. package/lib/wish-audit.js +1368 -0
  121. package/lib/wish-delegate.js +1840 -0
  122. package/lib/wish-design.js +110 -0
  123. package/lib/wish-stats.js +183 -0
  124. package/lib/wish-store.js +354 -0
  125. package/lib/zip.js +221 -0
  126. package/package.json +3 -1
  127. package/templates/loops/atris/loops/LOOPS.md +55 -0
  128. package/templates/loops/atris/loops/TICK.md +24 -0
  129. package/templates/loops/atris/loops/feedback.md +22 -0
  130. package/templates/loops/atris/loops/quality.md +22 -0
  131. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  132. package/utils/api.js +5 -1
  133. package/utils/auth.js +57 -21
package/commands/task.js CHANGED
@@ -14,16 +14,18 @@ const {
14
14
  isAutoCertifyVerifyCommandAllowed,
15
15
  parseVerifyCommand,
16
16
  runVerifyCommand,
17
+ runVerifyCommandCached,
17
18
  DENIED_TAGS,
18
19
  } = require('../lib/auto-accept-certified');
19
- const { extractReceiptEvidence } = require('../lib/receipt-evidence');
20
+ const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
20
21
  const escapeRegExp = require('../lib/escape-regexp');
21
22
  const reviewIntegrity = require('../lib/review-integrity');
23
+ const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
22
24
  const {
23
25
  normalizeOwnerSlug,
24
26
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
25
27
  } = require('../lib/functional-owner');
26
- const { operatorReady, hasAgentJargon } = require('./autoland');
28
+ const { operatorReady, hasAgentJargon, explainResult } = require('./autoland');
27
29
  const {
28
30
  TASK_INSPECT_FIELDS,
29
31
  readFieldsFlag,
@@ -46,6 +48,13 @@ const REVIEW_LANE_RUN_MAX_RUNS = 20;
46
48
  const PENDING_REVIEW_CHAT_STOP_REASON = 'pending_review_chat_waiting_for_agent_review';
47
49
  const PROOF_BOUNDARY_BLOCKED_ACTION = 'proof_boundary_blocked';
48
50
  const PROOF_BOUNDARY_BLOCKED_REASON = 'proof_boundary_blocked_requires_revision';
51
+ const MISSION_XP_END_TO_END_REASON = 'mission_xp_requires_end_to_end_receipt';
52
+ const MISSION_XP_END_TO_END_DETAIL = 'mission XP proof must name a zero-papercut end-to-end fresh-laptop pass through install, init, first mission, and first self-landed task; generic mission/tick receipts are not enough';
53
+ const READY_RESULT_TEACHING = 'ready needs --result: one plain sentence someone new to the project can understand. say what someone can do now and why it matters. no ids, no paths, no commands. example: operators can now read the whole team day on one page instead of scrolling raw logs.';
54
+ const REVIEW_AUTO_ACCEPT_ACTOR = 'auto (certified, small)';
55
+ const REVIEW_AUTO_ACCEPT_POLICY = 'review_autoaccept_certified_small';
56
+ const REVIEW_AUTO_ACCEPT_FILE_LIMIT = 10;
57
+ const REVIEW_AUTO_ACCEPT_LINE_LIMIT = 300;
49
58
 
50
59
  const STATUS_PLAN_TAGS = new Set([
51
60
  'agent',
@@ -107,11 +116,11 @@ function getTaskDb() {
107
116
  }
108
117
  }
109
118
 
110
- function warnIfTaskTitleNeedsOperatorWhy(title) {
119
+ function warnIfTaskTitleNeedsOperatorWhy(title, options = {}) {
111
120
  const text = String(title || '').trim();
112
121
  if (!text || operatorReady(text)) return null;
113
- const warning = 'Warning: add the why in plain words to this task title: what it buys or costs, who benefits, and no flags or identifiers.';
114
- console.error(warning);
122
+ const warning = 'Warning: put the why in this task title in plain words: what it buys or costs, and who benefits. Drop flags and ids.';
123
+ if (options.print !== false) console.error(warning);
115
124
  return warning;
116
125
  }
117
126
 
@@ -133,16 +142,19 @@ atris task - durable local task state (SQLite, gitignored)
133
142
  atris task continue-work <id> Create/reuse a certified Review follow-up task
134
143
  atris task say <id> "<message>" Add context to a task
135
144
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
136
- atris task ready <id> --proof "..." Agent proof ready; native goal can complete
137
- atris task ready <id> --verify "<cmd>" Run <cmd>; only ready if it exits 0 (executed proof)
145
+ atris task ready <id> --proof "..." --result "<sentence>"
146
+ Agent proof ready; native goal can complete
147
+ atris task ready <id> --verify "<cmd>" --result "<sentence>"
148
+ Run <cmd>; only ready if it exits 0 (executed proof)
138
149
  Writes atris/runs/ receipt (pass or fail), folds path into proof
139
150
  atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
140
151
  atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
141
152
  Show the plain Plan before work starts
142
- atris task ready <id> --proof "..." [--changed "..." --checked "..." --saved "..." --try "..."]
153
+ atris task ready <id> --proof "..." --result "<sentence>" [--changed "..." --checked "..." --saved "..." --try "..."]
143
154
  Agent proof ready; records Result if needed
144
- atris task ready <id> --proof "..." [--happened "..." --checked "..." --tested "..." --decision "..."]
155
+ atris task ready <id> --proof "..." --result "<sentence>" [--happened "..." --checked "..." --tested "..." --decision "..."]
145
156
  Agent proof ready; writes the human result receipt
157
+ atris task result <id> "<sentence>" Set or replace the day-one PM result sentence
146
158
  atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
147
159
  Show the plain Result and store trace on the task
148
160
  atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
@@ -153,6 +165,7 @@ atris task - durable local task state (SQLite, gitignored)
153
165
  atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
154
166
  Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
155
167
  atris task sweep --auto-accept [--json] Auto-accept verified Review rows; protected lanes wait for human
168
+ atris task audit [--limit <n>] [--revise] re-run stored verifies for newest accepted tasks; report-only unless --revise
156
169
  atris task revise <id> --note "..." Send reviewed work back to Do
157
170
 
158
171
  atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
@@ -163,7 +176,8 @@ atris task - durable local task state (SQLite, gitignored)
163
176
  Start task-owned Do work from the plan
164
177
  atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
165
178
  atris task clear-plan --yes Move all planned open tasks back to Backlog
166
- atris task day [--all] [--everywhere] [--json] show today's owner-grouped task list
179
+ atris task day [--full] [--all] [--everywhere] [--json] show today's owner-grouped task list
180
+ text shows eight current rows; --full shows every active row
167
181
  --all stays in this workspace; --everywhere spans workspaces
168
182
  atris task list [--all] [--everywhere] [--status <s>]
169
183
  list tasks in this workspace; --everywhere spans workspaces
@@ -183,6 +197,7 @@ atris task - durable local task state (SQLite, gitignored)
183
197
  Advance the scoped current task one safe step
184
198
  review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
185
199
  atris task note <id> "<message>" Append dialogue/context to a task
200
+ atris task retitle <id> "<new title>" Rename a task and preserve the old title in dialogue
186
201
  atris task tag <id> --add <tag> [--remove <tag>]
187
202
  Update tags on an existing task (e.g. --add needs-human to hold it
188
203
  from sweep + fleet staffing); logs a task_tags_updated event
@@ -196,6 +211,7 @@ atris task - durable local task state (SQLite, gitignored)
196
211
  atris task archive <id> --reason "..." [--from-failed]
197
212
  Sweep off-roadmap/duplicate work as archived (not failed);
198
213
  --from-failed opts in to relabel a fail-closed row (never done)
214
+ atris task clear-done [--before <days>] [--dry-run] [--json] Archive completed rows, oldest first
199
215
  atris task relabel-archived [--dry-run|--apply]
200
216
  One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
201
217
  atris task finish <id> --proof "..." Legacy alias for done with proof
@@ -427,6 +443,22 @@ function proofFlagValue(args) {
427
443
  return typeof proof === 'string' ? proof.trim() : '';
428
444
  }
429
445
 
446
+ function resultSentenceIssue(value) {
447
+ const check = explainResult(value);
448
+ return check.ok ? null : check.reason;
449
+ }
450
+
451
+ function readyResultDetail(reason) {
452
+ return reason ? `${READY_RESULT_TEACHING}\n${reason}` : READY_RESULT_TEACHING;
453
+ }
454
+
455
+ function requireResultSentence(label, value, { ready = false } = {}) {
456
+ const issue = resultSentenceIssue(value);
457
+ if (!issue) return String(value || '').replace(/\s+/g, ' ').trim();
458
+ const detail = ready ? readyResultDetail(issue) : issue;
459
+ failTask(label, 'weak_result', detail);
460
+ }
461
+
430
462
  function textFlag(args, names) {
431
463
  for (const name of names) {
432
464
  const value = flag(args, name);
@@ -456,17 +488,27 @@ function landingNeedsDayOnePm(sentence, title) {
456
488
  const text = normalizedLandingSentence(sentence);
457
489
  if (!text) return true;
458
490
  if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
459
- return hasAgentJargon(text) || !operatorReady(text);
491
+ return hasAgentJargon(text) || /\bas\s+exists?\b/i.test(text) || !operatorReady(text);
460
492
  }
461
493
 
462
494
  function warnIfLandingNeedsDayOnePm(landing, title) {
463
495
  const sentence = landing && typeof landing === 'object' ? landing.happened : '';
464
496
  if (!landingNeedsDayOnePm(sentence, title)) return null;
465
- const warning = 'Advisory: add --landing with one capability sentence a day-one PM could read, with the result in plain words and no flags or identifiers.';
497
+ const warning = 'Advisory: add --landing with one plain sentence saying what someone can do now, in words a new teammate would get. No flags, no ids.';
466
498
  console.error(warning);
467
499
  return warning;
468
500
  }
469
501
 
502
+ function requireExplicitLandingDayOnePm(label, landing, title) {
503
+ const sentence = landing && typeof landing === 'object' ? normalizedLandingSentence(landing.happened) : '';
504
+ if (!sentence || !landingNeedsDayOnePm(sentence, title)) return;
505
+ failTask(
506
+ label,
507
+ 'weak_landing',
508
+ 'landing needs one plain sentence saying what someone can do now and why it matters, in words a new teammate would understand. no flags, ids, or unnamed filters.',
509
+ );
510
+ }
511
+
470
512
  function numericFlag(args, name) {
471
513
  const value = flag(args, name);
472
514
  if (value === null || value === true || value === undefined) return null;
@@ -481,6 +523,102 @@ function meaningfulTaskProofIssue(proof, { required = true } = {}) {
481
523
  return state.ok ? null : state.reason;
482
524
  }
483
525
 
526
+ function goldenPathMissionXpTask(task) {
527
+ if (!task) return false;
528
+ const metadata = task.metadata || {};
529
+ const text = [
530
+ task.title,
531
+ task.tag,
532
+ metadata.goal_id,
533
+ metadata.goalId,
534
+ metadata.mission_id,
535
+ metadata.stop_condition,
536
+ metadata.goal_objective,
537
+ metadata.objective,
538
+ ].filter(Boolean).join(' ');
539
+ const missionXp = String(task.title || '').trim().toLowerCase().startsWith('mission xp:')
540
+ || String(task.tag || '').toLowerCase() === 'agent-xp';
541
+ return missionXp
542
+ && /\b(?:golden[- ]path|zero[- ]knowledge|zero[- ]papercuts?|fresh[- ](?:laptop|environment|install|home)|self[- ]landed)\b/i.test(text);
543
+ }
544
+
545
+ function receiptTextForProof(proof, root = process.cwd()) {
546
+ const chunks = [];
547
+ const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
548
+ let match;
549
+ while ((match = pattern.exec(String(proof || ''))) && chunks.length < 3) {
550
+ const rel = match[1];
551
+ if (!rel || rel.includes('*')) continue;
552
+ try {
553
+ const raw = fs.readFileSync(path.resolve(root, rel), 'utf8');
554
+ const parsed = JSON.parse(raw);
555
+ chunks.push(JSON.stringify({
556
+ schema: parsed.schema || null,
557
+ mission_id: parsed.mission_id || null,
558
+ result: parsed.result || null,
559
+ landing: parsed.landing || null,
560
+ last_landing: parsed.last_landing || null,
561
+ summary: parsed.summary || null,
562
+ }).slice(0, 12000));
563
+ } catch {}
564
+ }
565
+ return chunks.join(' ');
566
+ }
567
+
568
+ // Mission ticks already compose a real landing sentence into the run receipt.
569
+ // When a mission-bridged task lands with only a receipt proof, lift that
570
+ // sentence so the review queue shows the work instead of echoing the title.
571
+ function missionReceiptResultForProof(task, proof, root = process.cwd()) {
572
+ const metadata = task?.metadata || {};
573
+ if (!metadata.mission_id && !metadata.goal_id) return null;
574
+ const pattern = new RegExp(RECEIPT_PATH_PATTERN.source, 'g');
575
+ let match;
576
+ while ((match = pattern.exec(String(proof || '')))) {
577
+ const rel = match[1];
578
+ if (!rel || rel.includes('*')) continue;
579
+ let landing = null;
580
+ try {
581
+ const parsed = JSON.parse(fs.readFileSync(path.resolve(root, rel), 'utf8'));
582
+ landing = parsed?.result?.landing || parsed?.landing || parsed?.last_landing || null;
583
+ } catch { continue; }
584
+ const changed = String(landing?.changed || landing?.happened || '').replace(/\s+/g, ' ').trim();
585
+ if (!changed) continue;
586
+ if (/\brecorded tick \d+\.?$/i.test(changed) || /^recorded a proof heartbeat\b/i.test(changed)) continue;
587
+ const reason = String(landing?.reason || landing?.why || '').replace(/\s+/g, ' ').trim();
588
+ return { changed, reason: reason && !isRetiredFillerReason(reason) ? reason : null };
589
+ }
590
+ return null;
591
+ }
592
+
593
+ function missionXpEndToEndProofIssue(task, proof, root = process.cwd()) {
594
+ if (!goldenPathMissionXpTask(task)) return null;
595
+ const corpus = `${String(proof || '')} ${receiptTextForProof(proof, root)}`
596
+ .replace(/\s+/g, ' ')
597
+ .trim();
598
+ const hasZeroPapercut = /\b(?:zero|0|no)\s+(?:new\s+)?papercuts?\b|\bzero[- ]papercut\b/i.test(corpus);
599
+ const hasEndToEnd = /\bend[- ]to[- ]end\b|\bfull\s+(?:fresh[- ](?:laptop|environment)\s+)?pass\b|\bfresh[- ](?:laptop|environment|install)\b|\bclean\s+temp\s+home\b|\bnpm\s+pack\b/i.test(corpus);
600
+ const hasSelfLanded = /\bself[- ]landed\b|\bfirst\s+self[- ]landed\s+task\b|\btask\s+reaches\s+done\b|\binstall\b.{0,120}\binit\b.{0,120}\bmission\b.{0,120}\b(?:self[- ]landed|task)\b/i.test(corpus);
601
+ return hasZeroPapercut && hasEndToEnd && hasSelfLanded ? null : MISSION_XP_END_TO_END_DETAIL;
602
+ }
603
+
604
+ function missionXpProofBoundaryEvaluation(task, proofOverride = null) {
605
+ if (!goldenPathMissionXpTask(task)) return null;
606
+ const metadata = task.metadata || {};
607
+ const review = task.review || {};
608
+ const proof = proofOverride === null
609
+ ? String(review.proof || metadata.latest_agent_proof || '').trim()
610
+ : String(proofOverride || '').trim();
611
+ const issue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
612
+ if (!issue) return null;
613
+ return {
614
+ eligible: false,
615
+ ref: taskRef(task),
616
+ reason: MISSION_XP_END_TO_END_REASON,
617
+ next_action: 'attach the zero-papercut end-to-end fresh-laptop receipt, then resubmit Mission XP proof',
618
+ proof,
619
+ };
620
+ }
621
+
484
622
  function requireMeaningfulTaskProof(label, proof, { required = true } = {}) {
485
623
  const issue = meaningfulTaskProofIssue(proof, { required });
486
624
  if (issue) failTask(label, 'weak_proof', `meaningful proof required: ${issue}`);
@@ -509,8 +647,15 @@ function writeDefaultProjection(taskDb, db, options = {}) {
509
647
  limit: options.all ? null : 500,
510
648
  }));
511
649
  const outPath = path.resolve(path.join('.atris', 'state', 'tasks.projection.json'));
650
+ const output = JSON.stringify(projection, null, 2) + '\n';
512
651
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
513
- fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
652
+ let shouldWrite = true;
653
+ try {
654
+ shouldWrite = fs.readFileSync(outPath, 'utf8') !== output;
655
+ } catch {
656
+ shouldWrite = true;
657
+ }
658
+ if (shouldWrite) fs.writeFileSync(outPath, output, 'utf8');
514
659
  return { projection, outPath };
515
660
  }
516
661
 
@@ -733,6 +878,8 @@ function certifiedReviewNextAction(nextTaskTitle) {
733
878
  }
734
879
 
735
880
  function proofBoundaryBlockedEvaluation(task) {
881
+ const missionXpBoundary = missionXpProofBoundaryEvaluation(task);
882
+ if (missionXpBoundary) return missionXpBoundary;
736
883
  // strictVerify stays off here: this is a render-path probe for the boundary
737
884
  // reason only, and the default-true strict mode would spawn the verify
738
885
  // subprocess for every certified row just to draw the desk.
@@ -926,26 +1073,11 @@ function proofToReasonText(proof) {
926
1073
  return /[.!?]$/.test(section) ? section : `${section}.`;
927
1074
  }
928
1075
 
929
- function titleToReasonText(task, proof = '') {
930
- const title = String(task?.title || '').replace(/\s+/g, ' ').trim();
931
- const text = `${title} ${proof || ''}`.toLowerCase();
932
- if (/\b(priv(?:ate|acy)|secret|payload|leak|redact)\b/.test(text)) {
933
- return 'It keeps private data out of the fast human decision screen.';
934
- }
935
- if (/\bapprove\b/.test(text) && /\b(command|exact|preview|ux)\b/.test(text)) {
936
- return 'It lets the operator see the next command without hunting.';
937
- }
938
- if (/\b(stale|expire|expired)\b/.test(text) && /\bapproval/.test(text)) {
939
- return 'It stops old approvals from running after their context has gone stale.';
940
- }
941
- if (/\bapproval|approve|permission\b/.test(text)) {
942
- return 'It keeps real-world side effects behind a clear human decision.';
943
- }
944
- if (/\btest|self-test|harness|verifier|proof\b/.test(text)) {
945
- return 'It gives the human a repeatable check before approval.';
946
- }
947
- return 'It turns the task title into a concrete result the human can approve.';
948
- }
1076
+ // A landing sentence written for --result already carries its own why
1077
+ // ("..., so operators keep deciding instead of waiting"). Reuse that clause
1078
+ // as the reason instead of inventing one; with no clause, stay silent.
1079
+ // landingWhyClause lives in lib/voice-gate.js so every human-bound landing
1080
+ // composer (task reviews, mission receipts) shares the same why extraction.
949
1081
 
950
1082
  function proofToHumanCheck(proof) {
951
1083
  const text = String(proof || '').replace(/\s+/g, ' ').trim();
@@ -1055,17 +1187,30 @@ function taskReviewLanding(task, review = {}, payload = {}) {
1055
1187
  const agentCertified = review.agent_certified === true || metadata.agent_certified === true;
1056
1188
  const approvalStatus = review.approval_status || metadata.approval_status || null;
1057
1189
  const explicitHappened = landingPayloadValue(payload, metadata, 'happened')
1058
- || payload.changed || metadata.result_changed || metadata.human_changed || metadata.changed;
1190
+ || payload.result || metadata.result || payload.changed || metadata.result_changed || metadata.human_changed || metadata.changed;
1059
1191
  const explicitChecked = landingPayloadValue(payload, metadata, 'checked')
1060
1192
  || payload.checked || metadata.result_checked || metadata.human_checked || metadata.checked;
1061
1193
  const explicitTested = landingPayloadValue(payload, metadata, 'tested');
1062
1194
  const explicitDecision = landingPayloadValue(payload, metadata, 'decision');
1063
- const explicitReason = landingPayloadValue(payload, metadata, 'reason')
1195
+ const explicitReasonRaw = landingPayloadValue(payload, metadata, 'reason')
1064
1196
  || landingPayloadValue(payload, metadata, 'why')
1065
1197
  || payload.reason || payload.why || metadata.result_reason || metadata.review_reason || metadata.why_it_matters;
1198
+ const explicitReason = explicitReasonRaw && !isRetiredFillerReason(explicitReasonRaw) ? explicitReasonRaw : null;
1199
+ const missionLift = explicitHappened
1200
+ ? null
1201
+ : missionReceiptResultForProof(task, proof, task.workspace_root || process.cwd());
1202
+ let happened = clipStatusText(explicitHappened || (missionLift && missionLift.changed) || titleToResultText(task.title), 220);
1203
+ let reason = clipStatusText(explicitReason || proofToReasonText(proof) || (missionLift && missionLift.reason) || '', 220);
1204
+ if (!reason) {
1205
+ const clause = landingWhyClause(happened);
1206
+ if (clause) {
1207
+ happened = clipStatusText(clause.change, 220);
1208
+ reason = clipStatusText(clause.why, 220);
1209
+ }
1210
+ }
1066
1211
  return {
1067
- happened: clipStatusText(explicitHappened || titleToResultText(task.title), 220),
1068
- reason: clipStatusText(explicitReason || proofToReasonText(proof) || titleToReasonText(task, proof), 220),
1212
+ happened,
1213
+ reason,
1069
1214
  checked: clipStatusText(explicitChecked || proofToHumanCheck(proof), 220),
1070
1215
  tested: clipStatusText(explicitTested || taskReviewLandingTested(proof), 260),
1071
1216
  decision: clipStatusText(explicitDecision || (task.status === 'done'
@@ -1658,7 +1803,10 @@ function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExist
1658
1803
  if (proofBoundary) {
1659
1804
  handoff.reason = proofBoundary.reason;
1660
1805
  handoff.next_action_detail = proofBoundary.next_action || null;
1661
- handoff.revise_command = `atris task revise ${taskRef(task)} --note "<replace stale PR proof with merged proof or move back to Do>"`;
1806
+ const note = proofBoundary.reason === MISSION_XP_END_TO_END_REASON
1807
+ ? '<attach zero-papercut end-to-end fresh-laptop receipt or move back to Do>'
1808
+ : '<replace stale PR proof with merged proof or move back to Do>';
1809
+ handoff.revise_command = `atris task revise ${taskRef(task)} --note "${note}"`;
1662
1810
  } else if (!agentCertified) {
1663
1811
  const blocker = reviewBlockerForTask(task);
1664
1812
  handoff.reason = blocker.reason;
@@ -2102,7 +2250,8 @@ function compactTaskForStatus(task) {
2102
2250
  id: task.id,
2103
2251
  display_id: task.display_id || null,
2104
2252
  legacy_ref: task.legacy_ref || taskRef(task.id),
2105
- title: clipStatusText(task.title, 140),
2253
+ title: clipStatusTitle(task.title, 140),
2254
+ result: clipStatusText(task.result || metadata.result, 180) || null,
2106
2255
  status: task.status,
2107
2256
  updated_at: task.updated_at,
2108
2257
  };
@@ -2156,7 +2305,7 @@ function compactTaskFromProjection(projection, id) {
2156
2305
  function compactEventPayload(payload) {
2157
2306
  if (!payload || typeof payload !== 'object') return null;
2158
2307
  const out = {};
2159
- for (const key of ['title', 'status', 'tag', 'content', 'goal', 'summary', 'proof', 'lesson', 'reward', 'next_task']) {
2308
+ for (const key of ['title', 'status', 'tag', 'content', 'goal', 'summary', 'proof', 'lesson', 'reward', 'next_task', 'result']) {
2160
2309
  if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') out[key] = payload[key];
2161
2310
  }
2162
2311
  return Object.keys(out).length ? out : null;
@@ -2181,6 +2330,19 @@ function clipStatusText(value, max = 180) {
2181
2330
  return `${text.slice(0, max - 1)}…`;
2182
2331
  }
2183
2332
 
2333
+ function clipStatusTitle(value, max = 140) {
2334
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
2335
+ if (text.length <= max) return text;
2336
+ const cut = text.slice(0, max + 1);
2337
+ const boundaries = [...cut.matchAll(/[.!?;](?=\s|$)/g)];
2338
+ const boundary = boundaries.map((match) => match.index).filter((index) => index >= max * 0.45).pop();
2339
+ if (Number.isInteger(boundary)) {
2340
+ return cut.slice(0, boundary + 1).replace(/[;:]+$/, '').trim();
2341
+ }
2342
+ const wholeWords = text.slice(0, max).replace(/\s+\S*$/, '').trim();
2343
+ return `${wholeWords || text.slice(0, max).trim()}...`;
2344
+ }
2345
+
2184
2346
  function compactReviewActionRef(task, { hasExistingReviewFollowUp = null } = {}) {
2185
2347
  if (!task) return null;
2186
2348
  const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp }) || {};
@@ -4577,6 +4739,483 @@ function cmdReviewLaneRun(args) {
4577
4739
  if (!result.ok) process.exit(1);
4578
4740
  }
4579
4741
 
4742
+ function configValueDisabled(value) {
4743
+ if (value === false) return true;
4744
+ const text = String(value === undefined || value === null ? '' : value).trim().toLowerCase();
4745
+ return ['0', 'false', 'off', 'no'].includes(text);
4746
+ }
4747
+
4748
+ function reviewAutoAcceptEnabled() {
4749
+ try {
4750
+ const { loadConfig } = require('../utils/config');
4751
+ const config = loadConfig();
4752
+ const value = Object.prototype.hasOwnProperty.call(config, 'autoaccept')
4753
+ ? config.autoaccept
4754
+ : Object.prototype.hasOwnProperty.call(config, 'review_autoaccept')
4755
+ ? config.review_autoaccept
4756
+ : config.reviewAutoaccept;
4757
+ return !configValueDisabled(value);
4758
+ } catch {
4759
+ return true;
4760
+ }
4761
+ }
4762
+
4763
+ function reviewAutoAcceptStatePath(root = process.cwd()) {
4764
+ return path.join(root, '.atris', 'state', 'review-autoaccept.json');
4765
+ }
4766
+
4767
+ function readReviewAutoAcceptState(root = process.cwd()) {
4768
+ const file = reviewAutoAcceptStatePath(root);
4769
+ try {
4770
+ if (!fs.existsSync(file)) return {};
4771
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
4772
+ return parsed && typeof parsed === 'object' ? parsed : {};
4773
+ } catch {
4774
+ return {};
4775
+ }
4776
+ }
4777
+
4778
+ function writeReviewAutoAcceptState(root = process.cwd(), state = {}) {
4779
+ const file = reviewAutoAcceptStatePath(root);
4780
+ fs.mkdirSync(path.dirname(file), { recursive: true });
4781
+ fs.writeFileSync(file, JSON.stringify(state, null, 2) + '\n', 'utf8');
4782
+ }
4783
+
4784
+ function finiteNumber(value) {
4785
+ const number = Number(value);
4786
+ return Number.isFinite(number) ? number : null;
4787
+ }
4788
+
4789
+ function firstNumberFromObject(source, keys) {
4790
+ if (!source || typeof source !== 'object') return null;
4791
+ for (const key of keys) {
4792
+ if (!Object.prototype.hasOwnProperty.call(source, key)) continue;
4793
+ const value = source[key];
4794
+ if (Array.isArray(value)) return value.length;
4795
+ if (typeof value === 'string' && value.includes(',')) {
4796
+ const parts = value.split(',').map(part => part.trim()).filter(Boolean);
4797
+ if (parts.length > 1) return parts.length;
4798
+ }
4799
+ const number = finiteNumber(value);
4800
+ if (number !== null) return number;
4801
+ }
4802
+ return null;
4803
+ }
4804
+
4805
+ function arrayFromPathValue(value) {
4806
+ if (!value) return [];
4807
+ if (Array.isArray(value)) return value.flatMap(arrayFromPathValue);
4808
+ if (typeof value === 'object') {
4809
+ if (typeof value.path === 'string') return [value.path];
4810
+ if (typeof value.file === 'string') return [value.file];
4811
+ if (typeof value.filename === 'string') return [value.filename];
4812
+ return [];
4813
+ }
4814
+ return String(value)
4815
+ .split(/[\n,]+/)
4816
+ .map(part => part.trim())
4817
+ .filter(Boolean);
4818
+ }
4819
+
4820
+ function collectRecordedDiffPaths(source) {
4821
+ if (!source || typeof source !== 'object') return [];
4822
+ const keys = [
4823
+ 'files',
4824
+ 'paths',
4825
+ 'touched_files',
4826
+ 'touchedFiles',
4827
+ 'changed_files',
4828
+ 'changedFiles',
4829
+ 'modified_files',
4830
+ 'modifiedFiles',
4831
+ ];
4832
+ const out = [];
4833
+ for (const key of keys) out.push(...arrayFromPathValue(source[key]));
4834
+ return out;
4835
+ }
4836
+
4837
+ function normalizeRecordedDiffStats(source, label = 'recorded') {
4838
+ if (!source || typeof source !== 'object') return null;
4839
+ const fileKeys = [
4840
+ 'files_touched',
4841
+ 'filesTouched',
4842
+ 'changed_files_count',
4843
+ 'changedFilesCount',
4844
+ 'files_changed',
4845
+ 'filesChanged',
4846
+ 'file_count',
4847
+ 'fileCount',
4848
+ 'files',
4849
+ 'changed_files',
4850
+ 'changedFiles',
4851
+ ];
4852
+ const changedLineKeys = [
4853
+ 'changed_lines',
4854
+ 'changedLines',
4855
+ 'lines_changed',
4856
+ 'linesChanged',
4857
+ 'changed_line_count',
4858
+ 'changedLineCount',
4859
+ 'total_changed_lines',
4860
+ 'totalChangedLines',
4861
+ 'line_count',
4862
+ 'lineCount',
4863
+ ];
4864
+ const insertions = firstNumberFromObject(source, ['insertions', 'added', 'additions', 'lines_added', 'linesAdded']);
4865
+ const deletions = firstNumberFromObject(source, ['deletions', 'deleted', 'removals', 'lines_deleted', 'linesDeleted']);
4866
+ const paths = collectRecordedDiffPaths(source);
4867
+ const filesTouched = firstNumberFromObject(source, fileKeys) ?? (paths.length ? paths.length : null);
4868
+ const changedLines = firstNumberFromObject(source, changedLineKeys)
4869
+ ?? (insertions !== null || deletions !== null ? Number(insertions || 0) + Number(deletions || 0) : null);
4870
+ if (filesTouched === null && changedLines === null && !paths.length) return null;
4871
+ return {
4872
+ source: label,
4873
+ files_touched: filesTouched,
4874
+ changed_lines: changedLines,
4875
+ paths,
4876
+ };
4877
+ }
4878
+
4879
+ function recordedDiffStatsForTask(task) {
4880
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
4881
+ const candidates = [
4882
+ metadata.diff_stats,
4883
+ metadata.diffStats,
4884
+ metadata.git_diff_stats,
4885
+ metadata.gitDiffStats,
4886
+ metadata.change_stats,
4887
+ metadata.changeStats,
4888
+ metadata.stats,
4889
+ metadata,
4890
+ ];
4891
+ for (const candidate of candidates) {
4892
+ const stats = normalizeRecordedDiffStats(candidate, 'recorded');
4893
+ if (stats) return stats;
4894
+ }
4895
+ return null;
4896
+ }
4897
+
4898
+ function proofDiffStats(proof) {
4899
+ const text = String(proof || '').replace(/\s+/g, ' ').trim();
4900
+ if (!text) return null;
4901
+ let filesTouched = null;
4902
+ let changedLines = null;
4903
+ const gitStat = text.match(/(\d+)\s+files?\s+changed(?:,\s*(\d+)\s+insertions?\(\+\))?(?:,\s*(\d+)\s+deletions?\(-\))?/i);
4904
+ if (gitStat) {
4905
+ filesTouched = Number(gitStat[1]);
4906
+ const insertions = gitStat[2] ? Number(gitStat[2]) : 0;
4907
+ const deletions = gitStat[3] ? Number(gitStat[3]) : 0;
4908
+ if (gitStat[2] || gitStat[3]) changedLines = insertions + deletions;
4909
+ }
4910
+ const filePatterns = [
4911
+ /files?\s+(?:touched|changed|modified)\s*[:=]\s*(\d+)/i,
4912
+ /(\d+)\s+files?\s+(?:touched|changed|modified)/i,
4913
+ ];
4914
+ for (const pattern of filePatterns) {
4915
+ const match = text.match(pattern);
4916
+ if (match) filesTouched = Number(match[1]);
4917
+ }
4918
+ const linePatterns = [
4919
+ /changed\s+lines?\s*[:=]\s*(\d+)/i,
4920
+ /(\d+)\s+changed\s+lines?/i,
4921
+ /lines?\s+changed\s*[:=]\s*(\d+)/i,
4922
+ ];
4923
+ for (const pattern of linePatterns) {
4924
+ const match = text.match(pattern);
4925
+ if (match) changedLines = Number(match[1]);
4926
+ }
4927
+ if (filesTouched === null && changedLines === null) return null;
4928
+ return {
4929
+ source: 'proof',
4930
+ files_touched: filesTouched,
4931
+ changed_lines: changedLines,
4932
+ paths: [],
4933
+ };
4934
+ }
4935
+
4936
+ function safeGitRef(value) {
4937
+ const text = String(value || '').trim();
4938
+ if (!text || text.startsWith('-') || text.includes('..')) return null;
4939
+ if (!/^[a-zA-Z0-9_./@=+~^-]+$/.test(text)) return null;
4940
+ return text;
4941
+ }
4942
+
4943
+ function runGitForAutoAccept(root, args) {
4944
+ try {
4945
+ const { spawnSync } = require('child_process');
4946
+ return spawnSync('git', args, {
4947
+ cwd: root || process.cwd(),
4948
+ encoding: 'utf8',
4949
+ timeout: 10000,
4950
+ });
4951
+ } catch {
4952
+ return { status: 1, stdout: '', stderr: '' };
4953
+ }
4954
+ }
4955
+
4956
+ function gitRefExists(root, ref) {
4957
+ const result = runGitForAutoAccept(root, ['rev-parse', '--verify', `${ref}^{commit}`]);
4958
+ return result.status === 0;
4959
+ }
4960
+
4961
+ function defaultDiffBase(root) {
4962
+ for (const ref of ['origin/master', 'origin/main', 'master', 'main']) {
4963
+ if (gitRefExists(root, ref)) return ref;
4964
+ }
4965
+ return null;
4966
+ }
4967
+
4968
+ function branchDiffStatsForTask(task, root) {
4969
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
4970
+ const branch = safeGitRef(
4971
+ metadata.branch
4972
+ || metadata.worktree_branch
4973
+ || metadata.git_branch
4974
+ || metadata.pr_branch
4975
+ || metadata.head_branch
4976
+ || metadata.worktree?.branch
4977
+ );
4978
+ if (!branch) return null;
4979
+ const base = safeGitRef(
4980
+ metadata.base
4981
+ || metadata.base_ref
4982
+ || metadata.target_ref
4983
+ || metadata.target_branch
4984
+ || metadata.worktree?.base
4985
+ ) || defaultDiffBase(root);
4986
+ if (!base) return null;
4987
+ const result = runGitForAutoAccept(root, ['diff', '--numstat', `${base}...${branch}`]);
4988
+ if (result.status !== 0) return null;
4989
+ const lines = String(result.stdout || '').split(/\r?\n/).filter(Boolean);
4990
+ if (!lines.length) return { source: 'branch', files_touched: 0, changed_lines: 0, paths: [] };
4991
+ let changedLines = 0;
4992
+ let unknownLines = false;
4993
+ const paths = [];
4994
+ for (const line of lines) {
4995
+ const parts = line.split(/\t+/);
4996
+ const added = Number(parts[0]);
4997
+ const deleted = Number(parts[1]);
4998
+ if (!Number.isFinite(added) || !Number.isFinite(deleted)) unknownLines = true;
4999
+ else changedLines += added + deleted;
5000
+ if (parts[2]) paths.push(parts.slice(2).join('\t'));
5001
+ }
5002
+ return {
5003
+ source: 'branch',
5004
+ files_touched: lines.length,
5005
+ changed_lines: unknownLines ? null : changedLines,
5006
+ paths,
5007
+ };
5008
+ }
5009
+
5010
+ function reviewAutoAcceptDiffStats(task, root) {
5011
+ const recorded = recordedDiffStatsForTask(task);
5012
+ if (recorded) return recorded;
5013
+ const branch = branchDiffStatsForTask(task, root);
5014
+ if (branch) return branch;
5015
+ const proof = String(task?.review?.proof || task?.metadata?.latest_agent_proof || '');
5016
+ return proofDiffStats(proof);
5017
+ }
5018
+
5019
+ function reviewAutoAcceptBigTitle(task) {
5020
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
5021
+ const text = [
5022
+ task && task.title,
5023
+ metadata.kind,
5024
+ metadata.type,
5025
+ metadata.category,
5026
+ metadata.report_type,
5027
+ ].map(value => String(value || '')).join(' ').toLowerCase();
5028
+ const match = text.match(/\b(daily\s+update|summary|report|digest|retro|retrospective)\b/i);
5029
+ if (!match) return null;
5030
+ return {
5031
+ ok: false,
5032
+ reason: `big_title_${match[1].toLowerCase().replace(/\s+/g, '_')}`,
5033
+ };
5034
+ }
5035
+
5036
+ function reviewAutoAcceptSizeGate(task, root) {
5037
+ const titleGate = reviewAutoAcceptBigTitle(task);
5038
+ if (titleGate) return titleGate;
5039
+ const stats = reviewAutoAcceptDiffStats(task, root);
5040
+ if (!stats) return { ok: false, reason: 'size_unknown', stats: null };
5041
+ const filesTouched = finiteNumber(stats.files_touched);
5042
+ const changedLines = finiteNumber(stats.changed_lines);
5043
+ if (filesTouched !== null && filesTouched > REVIEW_AUTO_ACCEPT_FILE_LIMIT) {
5044
+ return { ok: false, reason: 'big_files', stats };
5045
+ }
5046
+ if (changedLines !== null && changedLines > REVIEW_AUTO_ACCEPT_LINE_LIMIT) {
5047
+ return { ok: false, reason: 'big_changed_lines', stats };
5048
+ }
5049
+ if (filesTouched === null || changedLines === null) {
5050
+ return { ok: false, reason: 'size_unknown', stats };
5051
+ }
5052
+ return { ok: true, reason: 'small_change', stats };
5053
+ }
5054
+
5055
+ function reviewAutoAcceptMetadataText(task) {
5056
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
5057
+ return [
5058
+ task && task.title,
5059
+ task && task.tag,
5060
+ task && task.source_key,
5061
+ metadata.kind,
5062
+ metadata.type,
5063
+ metadata.category,
5064
+ metadata.lane,
5065
+ metadata.stage,
5066
+ metadata.area,
5067
+ JSON.stringify(metadata),
5068
+ ].map(value => String(value || '')).join(' ').toLowerCase();
5069
+ }
5070
+
5071
+ function reviewAutoAcceptTouchedPaths(task) {
5072
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
5073
+ const stats = recordedDiffStatsForTask(task);
5074
+ const proof = String(task?.review?.proof || metadata.latest_agent_proof || '');
5075
+ return [
5076
+ ...(stats && stats.paths ? stats.paths : []),
5077
+ ...collectRecordedDiffPaths(metadata),
5078
+ ...taskReviewEvidencePaths(proof, 50),
5079
+ ].map(value => String(value || '').trim()).filter(Boolean);
5080
+ }
5081
+
5082
+ function reviewProtectedMatch(task) {
5083
+ const text = reviewAutoAcceptMetadataText(task);
5084
+ const paths = reviewAutoAcceptTouchedPaths(task).join(' ').toLowerCase();
5085
+ const combined = `${text} ${paths}`;
5086
+ const checks = [
5087
+ ['auth', /\b(auth|authentication|authorization|oauth|login|session)\b/i],
5088
+ ['credentials', /\b(credentials?|secrets?|passwords?|api[-_ ]?keys?|tokens?)\b/i],
5089
+ ['csp', /\b(csp|content[-_ ]security[-_ ]policy)\b/i],
5090
+ ['sandbox', /\b(sandbox|allow-scripts|allow-same-origin)\b/i],
5091
+ ['billing', /\b(billing|invoice|invoices|subscription|subscriptions)\b/i],
5092
+ ['payments', /\b(payments?|stripe|checkout|refunds?)\b/i],
5093
+ ['outbound_sends', /\b(outbound|send|sending|email|sms|webhook|notification|notifications)\b/i],
5094
+ ];
5095
+ for (const [key, pattern] of checks) {
5096
+ if (pattern.test(combined)) return { ok: false, reason: `protected_${key}` };
5097
+ }
5098
+ return { ok: true };
5099
+ }
5100
+
5101
+ function evaluateReviewAutoAccept(task, root) {
5102
+ const ref = taskRef(task);
5103
+ if (!task) return { eligible: false, ref, reason: 'task_not_found' };
5104
+ const protectedGate = reviewProtectedMatch(task);
5105
+ if (!protectedGate.ok) return { eligible: false, ref, reason: protectedGate.reason };
5106
+ const sizeGate = reviewAutoAcceptSizeGate(task, root);
5107
+ if (!sizeGate.ok) return { eligible: false, ref, reason: sizeGate.reason, size: sizeGate.stats };
5108
+ const evaluation = evaluateAutoAccept(task, { strictVerify: true });
5109
+ if (!evaluation.eligible) {
5110
+ return {
5111
+ ...evaluation,
5112
+ size: sizeGate.stats,
5113
+ };
5114
+ }
5115
+ return {
5116
+ ...evaluation,
5117
+ eligible: true,
5118
+ reason: 'certified_small',
5119
+ policy: REVIEW_AUTO_ACCEPT_POLICY,
5120
+ size: sizeGate.stats,
5121
+ };
5122
+ }
5123
+
5124
+ function autoAcceptCertifiedSmallReviews(taskDb, db, projection) {
5125
+ const enabled = reviewAutoAcceptEnabled();
5126
+ const root = projection.workspace_root || process.cwd();
5127
+ const results = [];
5128
+ if (!enabled) {
5129
+ return {
5130
+ enabled,
5131
+ scanned: 0,
5132
+ accepted: 0,
5133
+ changed: false,
5134
+ results,
5135
+ };
5136
+ }
5137
+ const certified = certifiedPendingReviewTasks(projection);
5138
+ for (const item of certified) {
5139
+ const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
5140
+ const task = fullProjection.tasks[0] || null;
5141
+ const evaluation = evaluateReviewAutoAccept(task, root);
5142
+ if (!evaluation.eligible) {
5143
+ results.push({ ...evaluation, action: 'queued', task_id: task?.id || item.id || null });
5144
+ continue;
5145
+ }
5146
+ const accepted = acceptReviewTask(taskDb, db, task.id, {
5147
+ actor: REVIEW_AUTO_ACCEPT_ACTOR,
5148
+ proof: evaluation.proof,
5149
+ reward: 1,
5150
+ lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
5151
+ nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
5152
+ autoAccepted: true,
5153
+ });
5154
+ if (!accepted.ok) {
5155
+ results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
5156
+ continue;
5157
+ }
5158
+ stampAutoAcceptMetadata(taskDb, db, task.id, REVIEW_AUTO_ACCEPT_ACTOR, REVIEW_AUTO_ACCEPT_POLICY);
5159
+ refreshCareerXpAfterReview(accepted.reviewed);
5160
+ results.push({
5161
+ ...evaluation,
5162
+ action: 'accepted',
5163
+ task_id: task.id,
5164
+ reward: accepted.reviewed.episode.reward.value,
5165
+ });
5166
+ }
5167
+ const acceptedCount = results.filter(row => row.action === 'accepted').length;
5168
+ return {
5169
+ enabled,
5170
+ scanned: certified.length,
5171
+ accepted: acceptedCount,
5172
+ changed: acceptedCount > 0,
5173
+ results,
5174
+ };
5175
+ }
5176
+
5177
+ function autoAcceptedReviewRowsSince(taskDb, db, workspaceRoot, sinceIso, acceptedNowIds = []) {
5178
+ const sinceMs = Date.parse(sinceIso || '');
5179
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot }));
5180
+ const acceptedNow = new Set(acceptedNowIds.filter(Boolean));
5181
+ const seen = new Set();
5182
+ const out = [];
5183
+ for (const row of rows) {
5184
+ const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
5185
+ const acceptedAt = Date.parse(metadata.auto_accepted_at || metadata.accepted_at || '');
5186
+ const policyMatch = metadata.auto_accept_policy === REVIEW_AUTO_ACCEPT_POLICY
5187
+ || metadata.auto_accepted_by === REVIEW_AUTO_ACCEPT_ACTOR
5188
+ || metadata.accepted_by === REVIEW_AUTO_ACCEPT_ACTOR;
5189
+ const isCurrent = acceptedNow.has(row.id);
5190
+ const isSince = Number.isFinite(sinceMs) && Number.isFinite(acceptedAt) && acceptedAt > sinceMs;
5191
+ const firstLook = !Number.isFinite(sinceMs) && Number.isFinite(acceptedAt);
5192
+ if (row.status !== 'done' || !policyMatch || (!isCurrent && !isSince && !firstLook)) continue;
5193
+ if (seen.has(row.id)) continue;
5194
+ seen.add(row.id);
5195
+ out.push({
5196
+ id: row.id,
5197
+ ref: row.display_id || taskRef(row),
5198
+ title: row.title,
5199
+ accepted_at: metadata.auto_accepted_at || metadata.accepted_at || null,
5200
+ });
5201
+ }
5202
+ out.sort((a, b) => String(b.accepted_at || '').localeCompare(String(a.accepted_at || '')));
5203
+ return out;
5204
+ }
5205
+
5206
+ function reviewAutoAcceptRollup(taskDb, db, workspaceRoot, previousState, autoAcceptRun) {
5207
+ const acceptedNowIds = (autoAcceptRun.results || [])
5208
+ .filter(row => row.action === 'accepted')
5209
+ .map(row => row.task_id)
5210
+ .filter(Boolean);
5211
+ const rows = autoAcceptedReviewRowsSince(taskDb, db, workspaceRoot, previousState.last_look_at, acceptedNowIds);
5212
+ return {
5213
+ count: rows.length,
5214
+ items: rows,
5215
+ since: previousState.last_look_at || null,
5216
+ };
5217
+ }
5218
+
4580
5219
  function reviewQueueLimit(args, total) {
4581
5220
  if (hasFlag(args, '--all')) return total;
4582
5221
  const raw = flag(args, '--limit');
@@ -4595,6 +5234,17 @@ function reviewGroupTextLimit(args, total) {
4595
5234
  return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 10;
4596
5235
  }
4597
5236
 
5237
+ function plainReviewBlockerMessage(item) {
5238
+ const ref = item.display_id || taskRef(item.id);
5239
+ if (item.reason === 'verify_command_not_allowed') {
5240
+ return `${ref} used a check command the system does not allow; rerun with an approved check`;
5241
+ }
5242
+ if (item.reason === 'needs_second_actor_review') {
5243
+ return `${ref} needs a second reviewer before it can land`;
5244
+ }
5245
+ return `${ref} needs another check before it can land`;
5246
+ }
5247
+
4598
5248
  // Risk order for human attention: named receipts that are missing/failing (0)
4599
5249
  // beat prose-only proofs (1) beat fully validated green evidence (2).
4600
5250
  function evidenceRiskRank(evidence) {
@@ -4772,30 +5422,72 @@ function taskReviewGroups(projection, key) {
4772
5422
  };
4773
5423
  }
4774
5424
 
5425
+ function taskReviewLandingLines(item) {
5426
+ if (!item?.landing) return [];
5427
+ const clean = value => value
5428
+ ? gateForHuman(value, { title: item.title }).text
5429
+ : '';
5430
+ const happened = clean(item.landing.happened);
5431
+ const reason = clean(item.landing.reason);
5432
+ const unperiod = value => value.replace(/\.$/, '');
5433
+ const checked = unperiod(clean(item.landing.checked));
5434
+ const tested = unperiod(clean(item.landing.tested));
5435
+ return [
5436
+ happened ? ` what's new: ${happened}` : '',
5437
+ reason ? ` why it matters: ${reason}` : '',
5438
+ checked ? ` checked: ${checked}${tested && tested !== checked ? `; tested: ${tested}` : ''}.` : '',
5439
+ ].filter(Boolean);
5440
+ }
5441
+
4775
5442
  function cmdReviews(args) {
4776
5443
  const taskDb = getTaskDb();
4777
5444
  const db = taskDb.open();
4778
- const { projection, outPath } = writeDefaultProjection(taskDb, db);
5445
+ let { projection, outPath } = writeDefaultProjection(taskDb, db);
5446
+ const workspaceRoot = projection.workspace_root || process.cwd();
5447
+ const previousAutoAcceptState = readReviewAutoAcceptState(workspaceRoot);
5448
+ const autoAcceptRun = autoAcceptCertifiedSmallReviews(taskDb, db, projection);
5449
+ if (autoAcceptRun.changed) {
5450
+ ({ projection, outPath } = writeDefaultProjection(taskDb, db));
5451
+ }
5452
+ const autoAcceptRollup = reviewAutoAcceptRollup(taskDb, db, workspaceRoot, previousAutoAcceptState, autoAcceptRun);
5453
+ writeReviewAutoAcceptState(workspaceRoot, {
5454
+ ...previousAutoAcceptState,
5455
+ last_look_at: new Date().toISOString(),
5456
+ last_count: autoAcceptRollup.count,
5457
+ });
4779
5458
  const groupByRaw = flag(args, '--group-by');
4780
5459
  if (groupByRaw) {
4781
5460
  const key = reviewGroupKey(groupByRaw);
4782
5461
  const groups = taskReviewGroups(projection, key);
4783
5462
  if (wantsJson(args)) {
4784
- printJson({ ok: true, action: 'review_groups', projection_path: outPath, groups });
5463
+ printJson({
5464
+ ok: true,
5465
+ action: 'review_groups',
5466
+ projection_path: outPath,
5467
+ autoaccept: {
5468
+ enabled: autoAcceptRun.enabled,
5469
+ accepted_now: autoAcceptRun.accepted,
5470
+ accepted_since_last_look: autoAcceptRollup.count,
5471
+ results: autoAcceptRun.results,
5472
+ },
5473
+ groups,
5474
+ });
4785
5475
  return;
4786
5476
  }
4787
- console.log(`READY FOR APPROVAL — grouped by ${key}`);
4788
- console.log(`${groups.total_certified} ready for approval across ${groups.group_count} ${key} group(s)`);
5477
+ console.log(gateForHuman(`${numberWord(groups.total_certified)} finished things are waiting, grouped by ${key}.`).text);
5478
+ if (autoAcceptRollup.count > 0) {
5479
+ console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5480
+ }
4789
5481
  const visibleGroups = groups.groups.slice(0, reviewGroupTextLimit(args, groups.groups.length));
4790
5482
  visibleGroups.forEach((g, index) => {
4791
5483
  console.log('');
4792
- console.log(`${index + 1}. ${g.value} — ${g.count} task${g.count === 1 ? '' : 's'}`);
4793
- g.sample_titles.forEach(title => console.log(` • ${title}`));
5484
+ console.log(`${index + 1}. ${g.value} - ${numberWord(g.count)} task${g.count === 1 ? '' : 's'}`);
5485
+ g.sample_titles.forEach(title => console.log(` • ${gateForHuman(title, { title }).text}`));
4794
5486
  console.log(` approve this group: ${g.accept_group_command} --confirm-human-accept --as <you>`);
4795
5487
  });
4796
5488
  if (visibleGroups.length < groups.groups.length) {
4797
5489
  console.log('');
4798
- console.log(`Showing ${visibleGroups.length}/${groups.groups.length} groups; rerun with --all for every group or --limit N to adjust.`);
5490
+ console.log(`showing ${numberWord(visibleGroups.length)} of ${numberWord(groups.groups.length)} groups; rerun with --all for every group or --limit N to adjust.`);
4799
5491
  }
4800
5492
  return;
4801
5493
  }
@@ -4806,32 +5498,45 @@ function cmdReviews(args) {
4806
5498
  ok: true,
4807
5499
  action: 'review_queue',
4808
5500
  projection_path: outPath,
5501
+ autoaccept: {
5502
+ enabled: autoAcceptRun.enabled,
5503
+ accepted_now: autoAcceptRun.accepted,
5504
+ accepted_since_last_look: autoAcceptRollup.count,
5505
+ results: autoAcceptRun.results,
5506
+ },
4809
5507
  queue,
4810
5508
  });
4811
5509
  return;
4812
5510
  }
4813
- console.log('READY FOR APPROVAL');
4814
- console.log(`${queue.counts.certified} ready for approval / ${queue.counts.blocking} need one more check / ${queue.counts.review} total waiting`);
4815
5511
  const approvalItems = queue.items.filter(item => item.queue_role !== 'blocked');
4816
5512
  const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
4817
5513
  if (!approvalItems.length && !blockedItems.length) {
4818
- console.log('Nothing is ready for approval.');
5514
+ console.log('nothing is waiting on you. everything that finished has already landed.');
5515
+ if (autoAcceptRollup.count > 0) {
5516
+ console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5517
+ }
4819
5518
  return;
4820
5519
  }
5520
+ if (queue.counts.certified > 0) {
5521
+ const header = queue.counts.certified === 1
5522
+ ? 'one finished thing is waiting for your ok. it passed both checks.'
5523
+ : `${numberWord(queue.counts.certified)} finished things are waiting for your ok. all of them passed both checks.`;
5524
+ console.log(gateForHuman(header).text);
5525
+ }
5526
+ if (queue.counts.blocking > 0) {
5527
+ console.log(gateForHuman(queue.counts.blocking === 1
5528
+ ? 'one more is almost ready; a second check is still running.'
5529
+ : `${numberWord(queue.counts.blocking)} more are almost ready; second checks are still running.`).text);
5530
+ }
5531
+ if (autoAcceptRollup.count > 0) {
5532
+ console.log(gateForHuman(`${numberWord(autoAcceptRollup.count)} landed on their own since you last looked.`).text);
5533
+ }
4821
5534
  approvalItems.forEach((item, index) => {
4822
- const tag = item.tag ? ` [${item.tag}]` : '';
4823
- const passes = item.review_pass_count ? ` (${item.review_pass_count} reviews)` : '';
4824
- const badge = item.evidence?.all_passing ? ' [evidence:passing]' : '';
4825
- console.log('');
4826
- console.log(`${index + 1}. ${item.display_id || taskRef(item.id)}${tag}${passes}: ${item.title}${badge}`);
5535
+ if (index > 0) console.log('');
5536
+ console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text}`);
4827
5537
  if (item.landing) {
4828
- console.log(' Result:');
4829
- if (item.landing.happened) console.log(` What happened: ${item.landing.happened}`);
4830
- if (item.landing.reason) console.log(` Why it matters: ${item.landing.reason}`);
4831
- if (item.landing.checked) console.log(` How I checked: ${item.landing.checked}`);
4832
- if (item.landing.tested) console.log(` What I tested: ${item.landing.tested}`);
4833
- if (item.result?.saved) console.log(` Saved: ${item.result.saved}`);
4834
- if (item.landing.decision) console.log(` Decision: ${item.landing.decision}`);
5538
+ taskReviewLandingLines(item).forEach(line => console.log(line));
5539
+ if (verbose && item.result?.saved) console.log(` saved: ${item.result.saved}`);
4835
5540
  }
4836
5541
  if (verbose && item.proof) console.log(` details: ${item.proof}`);
4837
5542
  if (verbose && item.evidence) {
@@ -4843,18 +5548,23 @@ function cmdReviews(args) {
4843
5548
  item.evidence.missing.forEach((missingPath) => console.log(` receipt: ${missingPath} MISSING`));
4844
5549
  }
4845
5550
  if (verbose && item.review_chat_command) console.log(` /codex: ${item.review_chat_command}`);
4846
- if (item.continue_work_command) console.log(` continue: ${item.continue_work_command}`);
4847
- if (item.accept_command) console.log(` approve: ${item.accept_command}`);
4848
- else if (item.blocked_accept_reason) console.log(` approve: blocked (${item.blocked_accept_reason})`);
4849
- console.log(` rework: ${item.revise_command}`);
5551
+ if (item.accept_command) {
5552
+ console.log(` say yes: atris task accept ${item.display_id || taskRef(item.id)}`);
5553
+ } else if (item.blocked_accept_reason) {
5554
+ console.log(` approve: blocked (${item.blocked_accept_reason})`);
5555
+ console.log(` rework: ${item.revise_command}`);
5556
+ }
4850
5557
  });
4851
- for (const item of blockedItems) {
4852
- console.log('');
4853
- console.log(`blocked: ${item.display_id || taskRef(item.id)}: ${item.reason}; next: ${item.next_command}`);
5558
+ if (blockedItems.length > 0) {
5559
+ if (approvalItems.length > 0) console.log('');
5560
+ console.log('still being checked:');
5561
+ for (const item of blockedItems) {
5562
+ console.log(`${plainReviewBlockerMessage(item)}; next: ${item.next_command}`);
5563
+ }
4854
5564
  }
4855
5565
  if (queue.counts.shown < queue.counts.certified) {
4856
5566
  console.log('');
4857
- console.log(`Showing ${queue.counts.shown}/${queue.counts.certified}; rerun with --all for every row or --verbose for proof details.`);
5567
+ console.log(`showing ${numberWord(queue.counts.shown)} of ${numberWord(queue.counts.certified)}; rerun with --all for every row or --verbose for proof details.`);
4858
5568
  }
4859
5569
  }
4860
5570
 
@@ -4969,6 +5679,11 @@ function cmdAcceptGroup(args) {
4969
5679
  const isVerified = verifiedIds.has(task.id);
4970
5680
  const proof = String(task.review?.proof || task.metadata?.latest_agent_proof || '').trim()
4971
5681
  || `Accepted via group spot-check (${groupLabel}); human ${actor} verified ${verifiedIds.size}/${group.length}.`;
5682
+ const missionXpIssue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || taskDb.workspaceRoot());
5683
+ if (missionXpIssue) {
5684
+ accepted.push({ id: task.id, ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue });
5685
+ continue;
5686
+ }
4972
5687
  const done = taskDb.doneTask(db, {
4973
5688
  id: task.id,
4974
5689
  status: 'done',
@@ -5218,7 +5933,7 @@ function delegateHandoff(task, owner, via, tag) {
5218
5933
  return handoff;
5219
5934
  }
5220
5935
 
5221
- function cmdDelegate(args) {
5936
+ function delegateTask(args, options = {}) {
5222
5937
  const pos = positional(args);
5223
5938
  const title = pos.join(' ').trim();
5224
5939
  if (!title) {
@@ -5236,7 +5951,7 @@ function cmdDelegate(args) {
5236
5951
  const taskDb = getTaskDb();
5237
5952
  const db = taskDb.open();
5238
5953
  const ws = taskDb.workspaceRoot();
5239
- const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
5954
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title, { print: options.warnOperatorTitle !== false });
5240
5955
  const ownerResolution = resolveFunctionalTaskOwner({
5241
5956
  requestedOwner: requestedOwner && requestedOwner !== true ? requestedOwner : null,
5242
5957
  title,
@@ -5281,34 +5996,69 @@ function cmdDelegate(args) {
5281
5996
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
5282
5997
  const task = compactTaskFromProjection(projection, result.id);
5283
5998
  const handoff = delegateHandoff(task, owner, via, typeof tag === 'string' ? tag : null);
5999
+ return {
6000
+ ok: true,
6001
+ action: 'delegated',
6002
+ task_id: result.id,
6003
+ inserted: result.inserted !== false,
6004
+ owner,
6005
+ owner_resolution: ownerResolution,
6006
+ executed_by: executedBy || null,
6007
+ via,
6008
+ tag: typeof tag === 'string' ? tag : null,
6009
+ handoff,
6010
+ proposed_member_command: metadata.proposed_member_command || null,
6011
+ operator_title_warning: operatorTitleWarning,
6012
+ projection_path: outPath,
6013
+ task,
6014
+ };
6015
+ }
6016
+
6017
+ function cmdDelegate(args) {
6018
+ const payload = delegateTask(args);
5284
6019
  if (wantsJson(args)) {
5285
- printJson({
5286
- ok: true,
5287
- action: 'delegated',
5288
- task_id: result.id,
5289
- inserted: result.inserted !== false,
5290
- owner,
5291
- owner_resolution: ownerResolution,
5292
- executed_by: executedBy || null,
5293
- via,
5294
- handoff,
5295
- operator_title_warning: operatorTitleWarning,
5296
- projection_path: outPath,
5297
- task,
5298
- });
6020
+ printJson(payload);
5299
6021
  return;
5300
6022
  }
5301
- const tagText = tag && tag !== true ? ` #${tag}` : '';
5302
- console.log(`delegated ${taskRef(task)} -> ${owner}${tagText} via=${via}`);
5303
- if (executedBy) console.log(`executed_by: ${executedBy}`);
5304
- if (ownerResolution.proposed_member) console.log(`member: ${metadata.proposed_member_command}`);
5305
- console.log(`claim: ${handoff.command}`);
5306
- if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
6023
+ const tagText = payload.tag ? ` #${payload.tag}` : '';
6024
+ console.log(`delegated ${taskRef(payload.task)} -> ${payload.owner}${tagText} via=${payload.via}`);
6025
+ if (payload.executed_by) console.log(`executed_by: ${payload.executed_by}`);
6026
+ if (payload.proposed_member_command) console.log(`member: ${payload.proposed_member_command}`);
6027
+ console.log(`claim: ${payload.handoff.command}`);
6028
+ if (payload.handoff.swarlo) console.log(`swarlo: ${payload.handoff.swarlo.channel}/${payload.handoff.swarlo.action}`);
5307
6029
  }
5308
6030
 
5309
6031
  // Failed tasks older than this stop earning a daily owner-group row;
5310
6032
  // they collapse into one stale summary line instead (target state = clean day view).
5311
6033
  const DAY_STALE_FAILED_MS = 7 * 24 * 60 * 60 * 1000;
6034
+ const DAY_TEXT_TASK_LIMIT = 8;
6035
+
6036
+ function taskDayTextGroups(groups, { full = false } = {}) {
6037
+ if (full) return { groups, hiddenTasks: 0, hiddenOwners: 0 };
6038
+ const statusOrder = { claimed: 0, open: 1, review: 2, failed: 3, done: 4 };
6039
+ const ranked = groups
6040
+ .flatMap((group) => group.tasks)
6041
+ .sort((a, b) => ((statusOrder[a.status] ?? 5) - (statusOrder[b.status] ?? 5))
6042
+ || ((b.updated_at || 0) - (a.updated_at || 0)));
6043
+ const selected = new Set(ranked.slice(0, DAY_TEXT_TASK_LIMIT));
6044
+ const visibleGroups = groups
6045
+ .map((group) => ({ ...group, tasks: group.tasks.filter((task) => selected.has(task)) }))
6046
+ .filter((group) => group.tasks.length > 0);
6047
+ return {
6048
+ groups: visibleGroups,
6049
+ hiddenTasks: Math.max(0, ranked.length - selected.size),
6050
+ hiddenOwners: Math.max(0, groups.length - visibleGroups.length),
6051
+ };
6052
+ }
6053
+
6054
+ function taskDayTitle(title, maxLength = 120) {
6055
+ const text = String(title || '').replace(/\s*[—–]\s*/g, ' - ').replace(/\s+/g, ' ').trim();
6056
+ if (text.length <= maxLength) return text;
6057
+ const sentence = text.slice(0, maxLength + 1).match(/^(.{48,}?[.!?])(?:\s|$)/);
6058
+ if (sentence) return sentence[1];
6059
+ const clipped = text.slice(0, maxLength).replace(/\s+\S*$/, '').trim();
6060
+ return `${clipped || text.slice(0, maxLength).trim()}...`;
6061
+ }
5312
6062
 
5313
6063
  function taskDayGroups(tasks, { now = Date.now() } = {}) {
5314
6064
  const active = tasks.filter(task => task.status !== 'done');
@@ -5343,6 +6093,7 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
5343
6093
 
5344
6094
  function cmdDay(args) {
5345
6095
  const all = hasFlag(args, '--all');
6096
+ const full = hasFlag(args, '--full');
5346
6097
  const everywhere = taskScopeEverywhere(args);
5347
6098
  const taskDb = getTaskDb();
5348
6099
  const db = taskDb.open();
@@ -5373,24 +6124,30 @@ function cmdDay(args) {
5373
6124
  });
5374
6125
  return;
5375
6126
  }
5376
- console.log('TASK DAY');
6127
+ const textView = taskDayTextGroups(groups, { full });
6128
+ console.log('task day');
5377
6129
  const failedText = counts.failed > 0 ? ` / failed ${counts.failed}` : '';
5378
6130
  console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}${failedText}`);
5379
6131
  console.log('');
5380
6132
  if (!groups.length) {
5381
6133
  console.log('clear no active tasks');
5382
6134
  }
5383
- for (const group of groups) {
6135
+ for (const group of textView.groups) {
5384
6136
  console.log(`${group.owner}`);
5385
- for (const task of group.tasks.slice(0, 8)) {
6137
+ for (const task of group.tasks) {
5386
6138
  const tag = task.tag ? ` #${task.tag}` : '';
5387
6139
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
5388
- console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
6140
+ console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${taskDayTitle(task.title)}`);
5389
6141
  }
5390
6142
  }
6143
+ if (textView.hiddenTasks > 0) {
6144
+ console.log('');
6145
+ const ownerText = textView.hiddenOwners > 0 ? `, ${textView.hiddenOwners} owners not shown` : '';
6146
+ console.log(`more ${textView.hiddenTasks} active rows hidden${ownerText} - atris task day --full`);
6147
+ }
5391
6148
  if (staleFailed.length) {
5392
6149
  console.log('');
5393
- console.log(`stale ${staleFailed.length} failed >7d hidden — atris task list --status failed`);
6150
+ console.log(`stale ${staleFailed.length} failed >7d hidden - atris task list --status failed`);
5394
6151
  }
5395
6152
  console.log('');
5396
6153
  console.log('add: atris task delegate "..." --to task-planner --tag tasks');
@@ -5514,7 +6271,11 @@ function cmdClaim(args) {
5514
6271
  });
5515
6272
  return;
5516
6273
  }
5517
- console.log(`claimed ${taskRef(compactTaskFromProjection(projection, taskId))} as ${owner}`);
6274
+ const ref = taskRef(compactTaskFromProjection(projection, taskId));
6275
+ console.log(`claimed ${ref} as ${owner}`);
6276
+ console.log(`Next: make the change, then run: atris task ready ${ref} --verify "git diff --check" --result "<who can do what now and why>" --landing "<what someone can do now>"`);
6277
+ console.log('Use a different verifier only if autoland can rerun it, such as `node --test <file>`.');
6278
+ console.log('Then: atris autoland tick');
5518
6279
  } else {
5519
6280
  const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
5520
6281
  ? `atris task release ${id} --as ${result.claimed_by}`
@@ -5740,7 +6501,7 @@ function cmdNext(args) {
5740
6501
  const continueWorkCommand = handoff.next_action === 'continue_work'
5741
6502
  ? continueWorkCommandForTask(reviewTask, { owner })
5742
6503
  : null;
5743
- const nextAgentAction = handoff.next_action === 'human_accept_waiting'
6504
+ const nextAgentAction = handoff.next_action === 'human_accept_waiting' && !scoped
5744
6505
  ? readEndgameAgentAction(taskDb.workspaceRoot(), owner, { tasks: projection.tasks || [] })
5745
6506
  : null;
5746
6507
  if (hasFlag(args, '--create-next')) {
@@ -5961,6 +6722,58 @@ function cmdNote(args) {
5961
6722
  console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
5962
6723
  }
5963
6724
 
6725
+ function cmdRetitle(args) {
6726
+ const pos = positional(args);
6727
+ const id = pos[0];
6728
+ const title = pos.slice(1).join(' ').trim();
6729
+ if (!id) failTask('atris task retitle', 'missing_id', 'task id required');
6730
+ if (!title) failTask('atris task retitle', 'missing_title', 'new title required');
6731
+ warnIfTaskTitleNeedsOperatorWhy(title);
6732
+
6733
+ const actor = flag(args, '--as') || DEFAULT_OWNER;
6734
+ const taskDb = getTaskDb();
6735
+ const db = taskDb.open();
6736
+ const taskId = requireTaskId(taskDb, db, id, 'atris task retitle');
6737
+ const current = taskDb.getTask(db, taskId);
6738
+ const oldTitle = String(current.title || '');
6739
+ const now = Math.max(Date.now(), Number(current.updated_at || 0) + 1);
6740
+ const updated = db.prepare(`
6741
+ UPDATE tasks
6742
+ SET title = ?,
6743
+ updated_at = ?
6744
+ WHERE id = ?
6745
+ AND updated_at = ?
6746
+ `).run(title, now, taskId, current.updated_at);
6747
+ if (updated.changes !== 1) {
6748
+ failTask('atris task retitle', 'stale_task_state', 'retitle failed: stale task state', 1);
6749
+ }
6750
+ const history = taskDb.noteTask(db, {
6751
+ id: taskId,
6752
+ actor: String(actor),
6753
+ content: `previous title: ${oldTitle}`,
6754
+ });
6755
+ if (!history.noted) {
6756
+ failTask('atris task retitle', history.reason || 'history_failed', `retitle history failed: ${history.reason || 'unknown'}`, 1);
6757
+ }
6758
+
6759
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
6760
+ const task = compactTaskFromProjection(projection, taskId);
6761
+ if (wantsJson(args)) {
6762
+ printJson({
6763
+ ok: true,
6764
+ action: 'retitled',
6765
+ task_id: taskId,
6766
+ old_title: oldTitle,
6767
+ title,
6768
+ version: history.event.version,
6769
+ projection_path: outPath,
6770
+ task,
6771
+ });
6772
+ return;
6773
+ }
6774
+ console.log(`retitled ${taskRef(task)}: ${title}`);
6775
+ }
6776
+
5964
6777
  // Collect EVERY value for a repeatable flag (flag() only returns the first),
5965
6778
  // so `--add a --add b` and `--add a,b` both work.
5966
6779
  function collectFlagValues(args, name) {
@@ -6324,7 +7137,7 @@ function cmdShow(args) {
6324
7137
  const owner = task.claimed_by ? ` / ${task.claimed_by}` : '';
6325
7138
  const tag = task.tag ? ` #${task.tag}` : '';
6326
7139
  const statusLabel = task.status === 'review'
6327
- ? 'READY FOR APPROVAL'
7140
+ ? 'ready for approval'
6328
7141
  : task.status === 'done'
6329
7142
  ? 'DONE'
6330
7143
  : task.status.toUpperCase();
@@ -7084,9 +7897,38 @@ function cmdResult(args) {
7084
7897
  const pos = positional(args);
7085
7898
  const id = pos[0];
7086
7899
  if (!id) failTask('atris task result', 'missing_id', 'id required');
7900
+ const sentence = pos.slice(1).join(' ').trim();
7901
+ if (sentence) {
7902
+ const resultSentence = requireResultSentence('atris task result', sentence);
7903
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
7904
+ const taskDb = getTaskDb();
7905
+ const db = taskDb.open();
7906
+ const taskId = requireTaskId(taskDb, db, id, 'atris task result');
7907
+ const saved = taskDb.setTaskResult(db, {
7908
+ id: taskId,
7909
+ actor,
7910
+ result: resultSentence,
7911
+ });
7912
+ if (!saved.saved) failTask('atris task result', saved.reason || 'result_failed', `result failed: ${saved.reason || 'result_failed'}`, 1);
7913
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
7914
+ if (wantsJson(args)) {
7915
+ printJson({
7916
+ ok: true,
7917
+ action: 'result',
7918
+ task_id: taskId,
7919
+ version: saved.event.version,
7920
+ result: resultSentence,
7921
+ projection_path: outPath,
7922
+ task: compactTaskFromProjection(projection, taskId),
7923
+ });
7924
+ return;
7925
+ }
7926
+ console.log(`result saved ${taskRef(compactTaskFromProjection(projection, taskId))}: ${resultSentence}`);
7927
+ return;
7928
+ }
7087
7929
  const fields = {
7088
7930
  purpose: textFlag(args, ['--purpose', '--goal', '--objective']),
7089
- changed: textFlag(args, ['--changed', '--result', '--done']),
7931
+ changed: textFlag(args, ['--changed', '--done']),
7090
7932
  checked: textFlag(args, ['--checked', '--check', '--verified']),
7091
7933
  passed: textFlag(args, ['--passed', '--pass']),
7092
7934
  failed: textFlag(args, ['--failed', '--fail']),
@@ -7196,7 +8038,7 @@ function taskPageActions(task, { reviewer = 'codex-review', hasExistingReviewFol
7196
8038
  note_command: `atris task note ${ref} "<context>" --as ${owner}`,
7197
8039
  plan_command: `atris task plan ${ref} --goal ${taskCommandQuote(goal)} --exit "<exit condition>" --proof-needed "<verification command>" --first-move "<first move>"`,
7198
8040
  do_command: `atris task do ${ref} --as ${owner} --first-move "<first move>"`,
7199
- ready_command: `atris task ready ${ref} --as ${owner} --proof "<specific proof command/result>" --happened "<what happened>" --checked "<how you know>" --tested "<what you ran or inspected>" --decision "<accept/rework guidance>"`,
8041
+ ready_command: `atris task ready ${ref} --as ${owner} --proof "<specific proof command/result>" --result "<one day-one PM sentence>" --happened "<what happened>" --checked "<how you know>" --tested "<what you ran or inspected>" --decision "<accept/rework guidance>"`,
7200
8042
  review_command: `atris task review ${ref} --reward 0 --as ${actor} --proof "<specific proof command/result>" --verify "<safe verifier command>"`,
7201
8043
  };
7202
8044
  if (task && task.status === 'review') {
@@ -7632,9 +8474,14 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7632
8474
  if (proofIssue) {
7633
8475
  throw taskStepError(proof ? 'weak_proof' : 'proof_required', `meaningful proof required: ${proofIssue}`, { status: 400, exitCode: 2, page: actionPage });
7634
8476
  }
8477
+ const missionXpIssue = missionXpEndToEndProofIssue(task, proof, task.workspace_root || process.cwd());
8478
+ if (missionXpIssue) {
8479
+ throw taskStepError(MISSION_XP_END_TO_END_REASON, missionXpIssue, { status: 409, exitCode: 1, page: actionPage });
8480
+ }
7635
8481
  const lesson = String(options.lesson || '');
7636
8482
  const nextTask = String(options.nextTask || '');
7637
8483
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, { actor, proof });
8484
+ const missionResult = missionReceiptResultForProof(task, proof, task.workspace_root || process.cwd());
7638
8485
  const ready = taskDb.readyTask(db, {
7639
8486
  id: taskId,
7640
8487
  actor,
@@ -7642,6 +8489,8 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7642
8489
  lesson,
7643
8490
  nextTask,
7644
8491
  resultTrace: resultTrace && resultTrace.trace,
8492
+ result: missionResult ? missionResult.changed : undefined,
8493
+ reason: missionResult ? missionResult.reason : undefined,
7645
8494
  });
7646
8495
  if (!ready.ready) taskStepFailure('atris task step', ready, actionPage);
7647
8496
  task = taskDetail(taskDb, db, taskId) || task;
@@ -7651,7 +8500,10 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7651
8500
  } else if (current === 'review' && task.status === 'review') {
7652
8501
  const handoffState = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
7653
8502
  if (handoffState && handoffState.next_action === PROOF_BOUNDARY_BLOCKED_ACTION) {
7654
- throw taskStepError(PROOF_BOUNDARY_BLOCKED_REASON, 'atris task step: Review proof cites an open/draft/unmerged PR boundary; revise the row before further stepping', { status: 409, exitCode: 1, page: actionPage });
8503
+ const detail = handoffState.reason === MISSION_XP_END_TO_END_REASON
8504
+ ? `atris task step: ${MISSION_XP_END_TO_END_DETAIL}`
8505
+ : 'atris task step: Review proof cites an open/draft/unmerged PR boundary; revise the row before further stepping';
8506
+ throw taskStepError(PROOF_BOUNDARY_BLOCKED_REASON, detail, { status: 409, exitCode: 1, page: actionPage });
7655
8507
  }
7656
8508
  if (handoffState && (handoffState.next_action === 'continue_work' || handoffState.next_action === 'human_accept_waiting')) {
7657
8509
  const reason = handoffState.next_action === 'continue_work'
@@ -7776,9 +8628,13 @@ function runCurrentTaskStep(taskDb, db, { owner = DEFAULT_OWNER, reviewer = 'cod
7776
8628
  throw error;
7777
8629
  }
7778
8630
  if (nextActionKey === PROOF_BOUNDARY_BLOCKED_ACTION) {
8631
+ const boundaryReason = current.page?.review?.handoff?.reason || current.selected?.review?.handoff?.reason || '';
8632
+ const detail = boundaryReason === MISSION_XP_END_TO_END_REASON
8633
+ ? `atris task current-step: ${MISSION_XP_END_TO_END_DETAIL}`
8634
+ : 'atris task current-step: selected Review row has stale/open/draft/unmerged PR proof; revise it instead of accepting or auto-stepping';
7779
8635
  const error = taskStepError(
7780
8636
  PROOF_BOUNDARY_BLOCKED_REASON,
7781
- 'atris task current-step: selected Review row has stale/open/draft/unmerged PR proof; revise it instead of accepting or auto-stepping',
8637
+ detail,
7782
8638
  {
7783
8639
  status: 409,
7784
8640
  exitCode: 1,
@@ -7985,7 +8841,7 @@ function cmdDone(args) {
7985
8841
  if (agentProofOnlyMode() && !failed) {
7986
8842
  failAgentProofOnly(
7987
8843
  'atris task done',
7988
- 'Agent proof-only mode cannot mark tasks done. Use `atris task ready <id> --proof "..."` or `atris task review <id> --reward 0 --proof "..."`.',
8844
+ 'Agent proof-only mode cannot mark tasks done. Use `atris task ready <id> --proof "..." --result "<day-one PM sentence>"` or `atris task review <id> --reward 0 --proof "..."`.',
7989
8845
  );
7990
8846
  }
7991
8847
  const canComplete = beforeTask && (beforeTask.status === 'open' || beforeTask.status === 'claimed');
@@ -8067,13 +8923,14 @@ function cmdFinish(args) {
8067
8923
  if (agentProofOnlyMode() && !failed) {
8068
8924
  failAgentProofOnly(
8069
8925
  'atris task finish',
8070
- 'Agent proof-only mode cannot finish tasks. Use `atris task ready <id> --proof "..."` or `atris task review <id> --reward 0 --proof "..."`.',
8926
+ 'Agent proof-only mode cannot finish tasks. Use `atris task ready <id> --proof "..." --result "<day-one PM sentence>"` or `atris task review <id> --reward 0 --proof "..."`.',
8071
8927
  );
8072
8928
  }
8073
8929
  const canComplete = currentTask && (currentTask.status === 'open' || currentTask.status === 'claimed');
8074
8930
  if (canComplete) {
8075
8931
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task finish', proof);
8076
8932
  else if (proof) requireMeaningfulTaskProof('atris task finish', proof);
8933
+ if (!failed && hasReview) requireExplicitLandingDayOnePm('atris task finish', landing, currentTask.title);
8077
8934
  }
8078
8935
  const done = taskDb.doneTask(db, {
8079
8936
  id: taskId,
@@ -8204,6 +9061,86 @@ function cmdArchive(args) {
8204
9061
  }
8205
9062
  }
8206
9063
 
9064
+ function taskCompletionTime(row) {
9065
+ const doneAt = Number(row && row.done_at);
9066
+ if (Number.isFinite(doneAt) && doneAt > 0) return doneAt;
9067
+ const acceptedAt = Date.parse(String(row && row.metadata && row.metadata.accepted_at || ''));
9068
+ if (Number.isFinite(acceptedAt)) return acceptedAt;
9069
+ return Number(row && (row.updated_at || row.created_at) || 0);
9070
+ }
9071
+
9072
+ function cmdClearDone(args) {
9073
+ const beforeRaw = flag(args, '--before');
9074
+ let beforeDays = null;
9075
+ if (beforeRaw !== null) {
9076
+ beforeDays = Number(beforeRaw);
9077
+ if (beforeRaw === true || !Number.isFinite(beforeDays) || beforeDays < 0) {
9078
+ failTask('atris task clear-done', 'invalid_before', '--before requires a non-negative number of days');
9079
+ }
9080
+ }
9081
+
9082
+ const dryRun = hasFlag(args, '--dry-run');
9083
+ const taskDb = getTaskDb();
9084
+ const db = taskDb.open();
9085
+ const workspaceRoot = taskDb.workspaceRoot();
9086
+ const cutoff = beforeDays === null ? null : Date.now() - (beforeDays * 24 * 60 * 60 * 1000);
9087
+ const candidates = taskDb.listTasks(db, { workspaceRoot, status: 'done', limit: null })
9088
+ .filter(row => cutoff === null || taskCompletionTime(row) < cutoff)
9089
+ .sort((a, b) => taskCompletionTime(a) - taskCompletionTime(b) || String(a.id).localeCompare(String(b.id)));
9090
+ const sample = candidates.slice(0, 5).map(row => ({
9091
+ task_id: row.id,
9092
+ title: row.title,
9093
+ completed_at: new Date(taskCompletionTime(row)).toISOString(),
9094
+ }));
9095
+
9096
+ if (dryRun) {
9097
+ if (wantsJson(args)) {
9098
+ printJson({
9099
+ ok: true,
9100
+ action: 'clear-done',
9101
+ dry_run: true,
9102
+ before_days: beforeDays,
9103
+ count: candidates.length,
9104
+ sample,
9105
+ });
9106
+ return;
9107
+ }
9108
+ console.log(`clear-done dry-run: ${candidates.length} completed task(s) would be archived.`);
9109
+ for (const row of sample) console.log(` - ${row.title}`);
9110
+ if (candidates.length > sample.length) console.log(` ...and ${candidates.length - sample.length} more`);
9111
+ return;
9112
+ }
9113
+
9114
+ const reason = 'cleared by clear-done sweep';
9115
+ for (const row of candidates) {
9116
+ const result = taskDb.archiveTask(db, {
9117
+ id: row.id,
9118
+ actor: String(flag(args, '--as') || DEFAULT_OWNER),
9119
+ reason,
9120
+ fromDone: true,
9121
+ });
9122
+ if (!result.archived) {
9123
+ failTask('atris task clear-done', result.reason, `clear-done failed: ${row.id} ${result.reason}`, 1);
9124
+ }
9125
+ }
9126
+ const { outPath } = writeDefaultProjection(taskDb, db);
9127
+
9128
+ if (wantsJson(args)) {
9129
+ printJson({
9130
+ ok: true,
9131
+ action: 'clear-done',
9132
+ dry_run: false,
9133
+ before_days: beforeDays,
9134
+ count: candidates.length,
9135
+ reason,
9136
+ sample,
9137
+ projection_path: outPath,
9138
+ });
9139
+ return;
9140
+ }
9141
+ console.log(`cleared ${candidates.length} completed task(s).`);
9142
+ }
9143
+
8207
9144
  // One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
8208
9145
  // reset" archived ~125 certified, proof-backed tasks by writing status
8209
9146
  // 'failed' (no distinct archived status existed yet). This relabels exactly
@@ -8272,6 +9209,7 @@ function cmdReady(args) {
8272
9209
  // turning a claim into executed evidence. --verify can carry an optional --proof note.
8273
9210
  const proofFlag = flag(args, '--proof');
8274
9211
  const verifyFlag = flag(args, '--verify');
9212
+ const resultSentence = requireResultSentence('atris task ready', textFlag(args, ['--result']), { ready: true });
8275
9213
  const usedVerify = typeof verifyFlag === 'string' ? verifyFlag.trim() : '';
8276
9214
  let proof = typeof proofFlag === 'string' ? proofFlag : '';
8277
9215
  const verifyAutoCertifyAllowed = !usedVerify || isAutoCertifyVerifyCommandAllowed(verifyFlag);
@@ -8304,7 +9242,7 @@ function cmdReady(args) {
8304
9242
  guardExplicitActor('atris task ready', flag(args, '--as'));
8305
9243
  const actor = String(flag(args, '--as') || DEFAULT_OWNER);
8306
9244
  const resultFields = {
8307
- changed: textFlag(args, ['--changed', '--result', '--done']),
9245
+ changed: textFlag(args, ['--changed', '--done']),
8308
9246
  checked: textFlag(args, ['--checked', '--check', '--verified']),
8309
9247
  passed: textFlag(args, ['--passed', '--pass']),
8310
9248
  failed: textFlag(args, ['--failed', '--fail']),
@@ -8318,10 +9256,17 @@ function cmdReady(args) {
8318
9256
  const taskDb = getTaskDb();
8319
9257
  const db = taskDb.open();
8320
9258
  const taskId = requireTaskId(taskDb, db, id, 'atris task ready');
9259
+ const beforeTask = taskDetail(taskDb, db, taskId);
9260
+ requireExplicitLandingDayOnePm('atris task ready', landing, beforeTask && beforeTask.title);
9261
+ const missionXpIssue = missionXpEndToEndProofIssue(beforeTask, proof, taskDb.workspaceRoot());
9262
+ if (missionXpIssue) {
9263
+ failTask('atris task ready', MISSION_XP_END_TO_END_REASON, missionXpIssue);
9264
+ }
8321
9265
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
8322
9266
  actor,
8323
9267
  proof: String(proof),
8324
9268
  ...resultFields,
9269
+ changed: resultFields.changed || resultSentence,
8325
9270
  });
8326
9271
  const result = taskDb.readyTask(db, {
8327
9272
  id: taskId,
@@ -8331,6 +9276,7 @@ function cmdReady(args) {
8331
9276
  nextTask: nextTaskInput.nextTask,
8332
9277
  resultTrace: resultTrace && resultTrace.trace,
8333
9278
  landing,
9279
+ result: resultSentence,
8334
9280
  });
8335
9281
  if (!result.ready) {
8336
9282
  console.error(`ready failed: ${result.reason}`);
@@ -8354,6 +9300,7 @@ function cmdReady(args) {
8354
9300
  });
8355
9301
  const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
8356
9302
  const autolandOn = require('../lib/autoland').liveAcceptAuthorization(taskDb.workspaceRoot()).ok;
9303
+ const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
8357
9304
  const handoff = {
8358
9305
  native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
8359
9306
  career_xp_status: 'pending_human_accept',
@@ -8361,6 +9308,8 @@ function cmdReady(args) {
8361
9308
  rule: autolandOn
8362
9309
  ? (agentCertified
8363
9310
  ? 'double-check complete; autoland will accept this on the next tick.'
9311
+ : needsExternalVerifier
9312
+ ? 'proof is ready; this verifier needs a second agent review because autoland cannot rerun it.'
8364
9313
  : 'proof is ready; autoland runs the second check and lands it on the next tick.')
8365
9314
  : (agentCertified
8366
9315
  ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
@@ -8443,7 +9392,7 @@ function cmdTaskReceipt(args) {
8443
9392
  }
8444
9393
  if (receipt.passed) {
8445
9394
  console.log(`receipt written: ${receipt.receiptPath} (exit 0)`);
8446
- console.log(`use: atris task ready ${taskId} --proof "Receipt: ${receipt.receiptPath}"`);
9395
+ console.log(`use: atris task ready ${taskId} --proof "Receipt: ${receipt.receiptPath}" --result "<what someone can do now and why it matters>"`);
8447
9396
  } else {
8448
9397
  console.error(`verifier failed (exit ${receipt.exit}); receipt written: ${receipt.receiptPath}`);
8449
9398
  if (receipt.output) console.error(receipt.output);
@@ -8483,6 +9432,10 @@ async function cmdAccept(args) {
8483
9432
  process.exit(2);
8484
9433
  }
8485
9434
  requireMeaningfulTaskProof('atris task accept', proof);
9435
+ const missionXpIssue = missionXpEndToEndProofIssue(beforeTask, proof, taskDb.workspaceRoot());
9436
+ if (missionXpIssue) {
9437
+ failTask('atris task accept', MISSION_XP_END_TO_END_REASON, missionXpIssue);
9438
+ }
8486
9439
  const readyReview = beforeTask?.review || {};
8487
9440
  const clearLesson = hasEmptyFlagValue(args, '--lesson');
8488
9441
  const clearNextTask = hasEmptyFlagValue(args, '--next');
@@ -8583,6 +9536,11 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
8583
9536
  }
8584
9537
 
8585
9538
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '', autoAccepted = false }) {
9539
+ const task = taskDetail(taskDb, db, taskId);
9540
+ const missionXpIssue = missionXpEndToEndProofIssue(task, proof, taskDb.workspaceRoot());
9541
+ if (missionXpIssue) {
9542
+ return { ok: false, reason: MISSION_XP_END_TO_END_REASON, detail: missionXpIssue };
9543
+ }
8586
9544
  const done = taskDb.doneTask(db, {
8587
9545
  id: taskId,
8588
9546
  status: 'done',
@@ -8665,9 +9623,51 @@ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
8665
9623
  `).run(JSON.stringify(metadata), Date.now(), taskId);
8666
9624
  }
8667
9625
 
8668
- function cmdCertifyVerified(args) {
9626
+ function landingVerifyFailureNote(verify, result) {
9627
+ const exit = result && result.status != null ? result.status : 'unknown';
9628
+ return `Autoland re-ran allowlisted verify at landing and it failed: ${verify} (exit ${exit}).`;
9629
+ }
9630
+
9631
+ function reverifyBeforeLanding(taskDb, db, task, { actor = 'autoland-verifier', verifyCache = null } = {}) {
9632
+ const verify = certifyVerifyCandidate(task);
9633
+ if (!verify) {
9634
+ // No runnable check on record. Landing anyway converts "never verified"
9635
+ // into "accepted with XP" — the exact signal poisoning this gate exists
9636
+ // to prevent — so bounce the task back for a recorded verify instead.
9637
+ const note = 'Autoland refused to land without a runnable verify command: record one with `atris task ready --verify "<cmd>" --result "<sentence>"`.';
9638
+ const revised = taskDb.reviseTask(db, { id: task.id, actor, note });
9639
+ return {
9640
+ ok: false,
9641
+ verify: null,
9642
+ result: { reason: 'strict_verify_missing', status: null },
9643
+ note,
9644
+ revised: revised.revised === true,
9645
+ revise_reason: revised.reason || null,
9646
+ };
9647
+ }
9648
+ const result = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
9649
+ if (result.ok) return { ok: true, verify, result };
9650
+ const note = landingVerifyFailureNote(verify, result);
9651
+ const revised = taskDb.reviseTask(db, {
9652
+ id: task.id,
9653
+ actor,
9654
+ note,
9655
+ });
9656
+ return {
9657
+ ok: false,
9658
+ verify,
9659
+ result,
9660
+ note,
9661
+ revised: revised.revised === true,
9662
+ revise_reason: revised.reason || null,
9663
+ };
9664
+ }
9665
+
9666
+ function cmdCertifyVerified(args, options = {}) {
8669
9667
  const dryRun = hasFlag(args, '--dry-run');
8670
9668
  const asJson = wantsJson(args);
9669
+ const silent = options.silent === true;
9670
+ const verifyCache = options.verifyCache || null;
8671
9671
  const actor = String(flag(args, '--as') || 'autoland-verifier');
8672
9672
  const limitRaw = flag(args, '--limit');
8673
9673
  const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
@@ -8704,6 +9704,11 @@ function cmdCertifyVerified(args) {
8704
9704
  results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
8705
9705
  continue;
8706
9706
  }
9707
+ const proofBoundary = proofBoundaryBlockedEvaluation(task);
9708
+ if (proofBoundary) {
9709
+ results.push({ ref, action: 'skipped', reason: proofBoundary.reason });
9710
+ continue;
9711
+ }
8707
9712
  // Skip only rows the accept lane can already land, or rows blocked by
8708
9713
  // something an executed second-actor check cannot cure. A row with two
8709
9714
  // passes from ONE actor is exactly what this command exists to cure —
@@ -8739,7 +9744,7 @@ function cmdCertifyVerified(args) {
8739
9744
  results.push({ ref, action: 'would_certify', verify });
8740
9745
  continue;
8741
9746
  }
8742
- const run = runVerifyCommand(verify, task.workspace_root || process.cwd());
9747
+ const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
8743
9748
  if (!run.ok) {
8744
9749
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
8745
9750
  continue;
@@ -8773,6 +9778,9 @@ function cmdCertifyVerified(args) {
8773
9778
  results,
8774
9779
  projection_path: outPath,
8775
9780
  };
9781
+ if (silent) {
9782
+ return payload;
9783
+ }
8776
9784
  if (asJson) {
8777
9785
  console.log(JSON.stringify(payload, null, 2));
8778
9786
  } else if (results.length === 0) {
@@ -8805,6 +9813,7 @@ function cmdLanding(args) {
8805
9813
  function cmdAutoAcceptCertified(args) {
8806
9814
  const dryRun = hasFlag(args, '--dry-run');
8807
9815
  const acceptAll = hasFlag(args, '--all');
9816
+ const certifyFirst = hasFlag(args, '--certify-first');
8808
9817
  const strictVerify = !hasFlag(args, '--no-strict-verify') && !acceptAll;
8809
9818
  const actorFlag = flag(args, '--as');
8810
9819
  const hasHumanActor = validHumanActorFlag(actorFlag);
@@ -8863,6 +9872,14 @@ function cmdAutoAcceptCertified(args) {
8863
9872
  );
8864
9873
  }
8865
9874
 
9875
+ // A heartbeat certifies and lands in one process so its live verifier result
9876
+ // remains available to the landing gate. The cache is process-local and is
9877
+ // never persisted: a later heartbeat must prove the checkout again.
9878
+ const verifyCache = new Map();
9879
+ const certification = certifyFirst && !dryRun
9880
+ ? cmdCertifyVerified([], { verifyCache, silent: true })
9881
+ : null;
9882
+
8866
9883
  const taskDb = getTaskDb();
8867
9884
  const db = taskDb.open();
8868
9885
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
@@ -8890,7 +9907,12 @@ function cmdAutoAcceptCertified(args) {
8890
9907
  results.push({ ref: item.display_id || item.id, eligible: false, reason: 'task_not_found', action: 'skipped' });
8891
9908
  continue;
8892
9909
  }
8893
- const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll });
9910
+ const proofBoundary = proofBoundaryBlockedEvaluation(task);
9911
+ if (proofBoundary) {
9912
+ results.push({ ...proofBoundary, action: 'skipped' });
9913
+ continue;
9914
+ }
9915
+ const evaluation = evaluateAutoAccept(task, { strictVerify, acceptAll, verifyCache });
8894
9916
  if (!evaluation.eligible) {
8895
9917
  results.push({ ...evaluation, action: 'skipped' });
8896
9918
  continue;
@@ -8899,6 +9921,20 @@ function cmdAutoAcceptCertified(args) {
8899
9921
  results.push({ ...evaluation, action: 'would_accept', reward: parsedReward.value });
8900
9922
  continue;
8901
9923
  }
9924
+ const landingVerify = reverifyBeforeLanding(taskDb, db, task, { verifyCache });
9925
+ if (!landingVerify.ok) {
9926
+ results.push({
9927
+ ...evaluation,
9928
+ eligible: false,
9929
+ action: landingVerify.revised ? 'revised' : 'revise_failed',
9930
+ reason: landingVerify.result.reason || landingVerify.revise_reason || 'verify_failed',
9931
+ verify: landingVerify.verify,
9932
+ exit_code: landingVerify.result.status,
9933
+ revision_note: landingVerify.note,
9934
+ task_id: task.id,
9935
+ });
9936
+ continue;
9937
+ }
8902
9938
  const accepted = acceptReviewTask(taskDb, db, task.id, {
8903
9939
  actor,
8904
9940
  proof: evaluation.proof,
@@ -8927,8 +9963,9 @@ function cmdAutoAcceptCertified(args) {
8927
9963
  scanned: pool.length,
8928
9964
  accepted: results.filter(row => row.action === 'accepted').length,
8929
9965
  would_accept: results.filter(row => row.action === 'would_accept').length,
9966
+ revised: results.filter(row => row.action === 'revised').length,
8930
9967
  skipped: results.filter(row => row.action === 'skipped').length,
8931
- failed: results.filter(row => row.action === 'accept_failed').length,
9968
+ failed: results.filter(row => row.action === 'accept_failed' || row.action === 'revise_failed').length,
8932
9969
  // Visible undercount flag: true only if the pool was cut short by `max`
8933
9970
  // while certified rows still existed beyond it. --all uses a high safety
8934
9971
  // cap (AUTO_ACCEPT_ALL_SWEEP_CAP), so this should stay false in practice;
@@ -8944,18 +9981,20 @@ function cmdAutoAcceptCertified(args) {
8944
9981
  summary,
8945
9982
  ...summary,
8946
9983
  results,
9984
+ certification,
8947
9985
  projection_path: finalPath,
8948
9986
  queue,
8949
9987
  });
8950
- return;
9988
+ return { ...summary, results, certification, projection_path: finalPath, queue };
8951
9989
  }
8952
9990
  console.log(`AUTO-ACCEPT CERTIFIED (${dryRun ? 'dry-run' : 'execute'})`);
8953
- console.log(`${summary.certified} certified, ${summary.scanned} scanned, ${summary.accepted || summary.would_accept} accepted, ${summary.skipped} skipped${summary.failed ? `, ${summary.failed} failed` : ''}${summary.undercounted ? ' (UNDERCOUNTED — raise --limit or the sweep cap)' : ''}`);
9991
+ console.log(`${summary.certified} certified, ${summary.scanned} scanned, ${summary.accepted || summary.would_accept} accepted, ${summary.skipped} skipped${summary.revised ? `, ${summary.revised} revised` : ''}${summary.failed ? `, ${summary.failed} failed` : ''}${summary.undercounted ? ' (UNDERCOUNTED — raise --limit or the sweep cap)' : ''}`);
8954
9992
  for (const row of results) {
8955
9993
  const nextAction = row.next_action ? ` next_action=${row.next_action}` : '';
8956
9994
  const reviewChat = row.review_chat_command ? ` review_chat=${row.review_chat_command}` : '';
8957
9995
  console.log(`${row.action.toUpperCase()} ${row.ref}: ${row.reason}${row.reward ? ` reward=${row.reward}` : ''}${nextAction}${reviewChat}`);
8958
9996
  }
9997
+ return { ...summary, results, certification, projection_path: finalPath, queue };
8959
9998
  }
8960
9999
 
8961
10000
  const SWEEP_AUTO_ACCEPT_PROTECTED = new Set([
@@ -9237,6 +10276,138 @@ function cmdRevise(args) {
9237
10276
  console.log(`revise ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
9238
10277
  }
9239
10278
 
10279
+ function taskAuditTimestamp(task) {
10280
+ const acceptedAt = Date.parse(String(task?.metadata?.accepted_at || ''));
10281
+ if (Number.isFinite(acceptedAt)) return acceptedAt;
10282
+ return Number(task?.done_at || task?.updated_at || task?.created_at || 0);
10283
+ }
10284
+
10285
+ function taskAuditOutput(value, max = 800) {
10286
+ const text = String(value || '').trim();
10287
+ if (text.length <= max) return text;
10288
+ return `...${text.slice(-max)}`;
10289
+ }
10290
+
10291
+ function taskAuditReceiptPath(at) {
10292
+ const safeTime = at.replace(/[:.]/g, '-');
10293
+ return path.join('atris', 'runs', `task-audit-${safeTime}.json`);
10294
+ }
10295
+
10296
+ function runTaskAuditVerify(task, verify) {
10297
+ const { spawnSync } = require('child_process');
10298
+ const result = spawnSync('bash', ['-c', verify], {
10299
+ cwd: task.workspace_root,
10300
+ encoding: 'utf8',
10301
+ timeout: 120000,
10302
+ });
10303
+ const passed = !result.error && result.status === 0;
10304
+ return {
10305
+ passed,
10306
+ exit: result.status,
10307
+ signal: result.signal || null,
10308
+ error: result.error ? result.error.message : null,
10309
+ output: taskAuditOutput(`${result.stdout || ''}${result.stderr || ''}`),
10310
+ };
10311
+ }
10312
+
10313
+ function cmdAudit(args) {
10314
+ const limitRaw = flag(args, '--limit');
10315
+ const limit = limitRaw === null ? 20 : Number(limitRaw);
10316
+ if (!Number.isInteger(limit) || limit < 1) {
10317
+ console.error('atris task audit: --limit must be a positive integer');
10318
+ process.exit(2);
10319
+ }
10320
+
10321
+ const revise = hasFlag(args, '--revise');
10322
+ const actor = String(flag(args, '--as') || 'task-audit');
10323
+ const taskDb = getTaskDb();
10324
+ const db = taskDb.open();
10325
+ const workspaceRoot = taskDb.workspaceRoot();
10326
+ const allRows = taskDb.listTasks(db, { workspaceRoot });
10327
+ const accepted = taskDb.withTaskDisplayRefs(
10328
+ allRows
10329
+ .filter(task => task.status === 'done' && task.metadata?.approval_status === 'accepted')
10330
+ .sort((a, b) => taskAuditTimestamp(b) - taskAuditTimestamp(a))
10331
+ .slice(0, limit),
10332
+ allRows,
10333
+ );
10334
+ const at = new Date().toISOString();
10335
+ const receiptPath = taskAuditReceiptPath(at);
10336
+ const results = accepted.map(task => {
10337
+ const verify = typeof task.metadata?.verify === 'string' ? task.metadata.verify : '';
10338
+ if (!verify.trim()) {
10339
+ return {
10340
+ task_id: task.id,
10341
+ ref: task.display_id || task.legacy_ref || taskRef(task),
10342
+ status: 'skipped-no-verify',
10343
+ verify: null,
10344
+ };
10345
+ }
10346
+ const run = runTaskAuditVerify(task, verify);
10347
+ return {
10348
+ task_id: task.id,
10349
+ ref: task.display_id || task.legacy_ref || taskRef(task),
10350
+ status: run.passed ? 'passed' : 'failed',
10351
+ verify,
10352
+ exit: run.exit,
10353
+ signal: run.signal,
10354
+ error: run.error,
10355
+ output: run.output,
10356
+ };
10357
+ });
10358
+
10359
+ const failing = results.filter(row => row.status === 'failed');
10360
+ if (revise) {
10361
+ const note = `task audit re-ran the stored verify and it failed; see ${receiptPath}`;
10362
+ for (const row of failing) {
10363
+ const revised = taskDb.reviseTask(db, {
10364
+ id: row.task_id,
10365
+ actor,
10366
+ note,
10367
+ allowDone: true,
10368
+ });
10369
+ row.revised = revised.revised === true;
10370
+ row.revise_reason = revised.revised ? null : revised.reason;
10371
+ }
10372
+ if (failing.some(row => row.revised)) writeDefaultProjection(taskDb, db);
10373
+ }
10374
+
10375
+ const summary = {
10376
+ sampled: results.length,
10377
+ passed: results.filter(row => row.status === 'passed').length,
10378
+ failed: failing.length,
10379
+ 'skipped-no-verify': results.filter(row => row.status === 'skipped-no-verify').length,
10380
+ revised: results.filter(row => row.revised === true).length,
10381
+ };
10382
+ const receipt = {
10383
+ schema: 'atris.task_audit_receipt.v1',
10384
+ at,
10385
+ workspace_root: workspaceRoot,
10386
+ limit,
10387
+ revise,
10388
+ summary,
10389
+ failing_task_ids: failing.map(row => row.task_id),
10390
+ results,
10391
+ };
10392
+ const receiptFile = path.join(workspaceRoot, receiptPath);
10393
+ fs.mkdirSync(path.dirname(receiptFile), { recursive: true });
10394
+ fs.writeFileSync(receiptFile, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
10395
+
10396
+ if (wantsJson(args)) {
10397
+ printJson({
10398
+ ok: true,
10399
+ action: 'task_audit',
10400
+ receipt_path: receiptPath,
10401
+ ...receipt,
10402
+ });
10403
+ return;
10404
+ }
10405
+ console.log(`task audit: sampled ${summary.sampled}, passed ${summary.passed}, failed ${summary.failed}, skipped-no-verify ${summary['skipped-no-verify']}`);
10406
+ console.log(`failing task ids: ${failing.length ? failing.map(row => row.ref).join(', ') : 'none'}`);
10407
+ if (revise) console.log(`revised: ${summary.revised}`);
10408
+ console.log(`receipt: ${receiptPath}`);
10409
+ }
10410
+
9240
10411
  function cmdReview(args) {
9241
10412
  const pos = positional(args);
9242
10413
  const id = pos[0];
@@ -9750,6 +10921,25 @@ function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
9750
10921
  return outPath;
9751
10922
  }
9752
10923
 
10924
+ // TODO.md is a projection of task-db state. Keep this best-effort and silent
10925
+ // because it runs after every successful mutating task command.
10926
+ function autoRenderTodoFromDb(cwd = process.cwd()) {
10927
+ try {
10928
+ const atrisDir = path.join(cwd, 'atris');
10929
+ if (!fs.existsSync(atrisDir)) return null;
10930
+ const taskDb = getTaskDb();
10931
+ const db = taskDb.open();
10932
+ const outPath = path.join(atrisDir, 'TODO.md');
10933
+ if (fs.existsSync(outPath)) return refreshExistingTodoMarkdown(taskDb, db, cwd);
10934
+ const rows = taskDb.listTasks(db, { workspaceRoot: cwd, limit: 500 });
10935
+ const refRows = taskDb.listTasks(db, { workspaceRoot: cwd });
10936
+ fs.writeFileSync(outPath, taskDb.renderTodoMarkdown(rows, { refRows }), 'utf8');
10937
+ return outPath;
10938
+ } catch {
10939
+ return null;
10940
+ }
10941
+ }
10942
+
9753
10943
  function cmdRender(args) {
9754
10944
  const out = flag(args, '--out') || path.join('atris', 'TODO.md');
9755
10945
  const all = hasFlag(args, '--all');
@@ -10733,10 +11923,15 @@ async function handleTaskApi(req, res, taskDb, db) {
10733
11923
  if (proofIssue) return sendProofIssue(res, proof, proofIssue);
10734
11924
  const nextTaskInput = normalizeReviewNextTaskInput(body.next);
10735
11925
  const actor = String(body.actor || DEFAULT_OWNER);
11926
+ const resultText = String(body.result || '').replace(/\s+/g, ' ').trim();
11927
+ if (resultText) {
11928
+ const resultIssue = resultSentenceIssue(resultText);
11929
+ if (resultIssue) return sendJson(res, 400, { ok: false, reason: 'weak_result', detail: resultIssue });
11930
+ }
10736
11931
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
10737
11932
  actor,
10738
11933
  proof,
10739
- changed: body.changed || body.result || body.done,
11934
+ changed: body.changed || resultText || body.done,
10740
11935
  checked: body.checked || body.check || body.verified,
10741
11936
  passed: body.passed || body.pass,
10742
11937
  failed: body.failed || body.fail,
@@ -10754,6 +11949,7 @@ async function handleTaskApi(req, res, taskDb, db) {
10754
11949
  lesson: String(body.lesson || ''),
10755
11950
  nextTask: nextTaskInput.nextTask,
10756
11951
  resultTrace: resultTrace && resultTrace.trace,
11952
+ result: resultText,
10757
11953
  landing: body.landing || {
10758
11954
  happened: body.happened,
10759
11955
  checked: body.checked,
@@ -10894,7 +12090,7 @@ function cmdServe(args) {
10894
12090
  });
10895
12091
  }
10896
12092
 
10897
- async function run(args) {
12093
+ async function runTaskCommand(args) {
10898
12094
  const raw = args || [];
10899
12095
  if (raw.includes('--help') || raw.includes('-h')) return help();
10900
12096
  const first = raw[0];
@@ -10967,6 +12163,7 @@ async function run(args) {
10967
12163
  return cmdPlanPreview(rest);
10968
12164
  case 'note': return cmdNote(rest);
10969
12165
  case 'say': return cmdNote(rest);
12166
+ case 'retitle': return cmdRetitle(rest);
10970
12167
  case 'tag':
10971
12168
  case 'tags':
10972
12169
  return cmdTag(rest);
@@ -10989,6 +12186,7 @@ async function run(args) {
10989
12186
  return cmdAutoAcceptCertified(rest);
10990
12187
  case 'sweep':
10991
12188
  return cmdSweep(rest);
12189
+ case 'audit': return cmdAudit(rest);
10992
12190
  case 'certify-verified':
10993
12191
  return cmdCertifyVerified(rest);
10994
12192
  case 'accept-group':
@@ -10998,6 +12196,7 @@ async function run(args) {
10998
12196
  case 'finish': return cmdFinish(rest);
10999
12197
  case 'fail': return cmdDone([...rest, '--failed']);
11000
12198
  case 'archive': return cmdArchive(rest);
12199
+ case 'clear-done': return cmdClearDone(rest);
11001
12200
  case 'relabel-archived': return cmdRelabelArchived(rest);
11002
12201
  case 'review': return cmdReview(rest);
11003
12202
  case 'reviews':
@@ -11032,4 +12231,34 @@ async function run(args) {
11032
12231
  }
11033
12232
  }
11034
12233
 
11035
- module.exports = { run, taskDayGroups, AGENT_ENV_MARKERS };
12234
+ const MUTATING_TASK_COMMANDS = new Set([
12235
+ 'add', 'new', 'delegate', 'assign', 'plan', 'do', 'backlog', 'unplan',
12236
+ 'clear-plan', 'clearplan', 'claim', 'start', 'release', 'unclaim', 'next',
12237
+ 'continue-work', 'continue', 'chat', 'note', 'say', 'retitle', 'tag', 'tags', 'step',
12238
+ 'ready', 'result', 'accept', 'landing', 'land-review', 'auto-accept-certified',
12239
+ 'auto-accept', 'sweep', 'audit', 'certify-verified', 'accept-group', 'revise',
12240
+ 'done', 'finish', 'fail', 'archive', 'clear-done', 'relabel-archived', 'review', 'import',
12241
+ 'setup', 'review-lane-act', 'review-act', 'act-review', 'review-lane-loop',
12242
+ 'review-loop', 'loop-review', 'review-lane-run', 'review-run', 'run-review',
12243
+ ]);
12244
+
12245
+ async function run(args) {
12246
+ const raw = args || [];
12247
+ const sub = !raw[0] || raw[0].startsWith('--') ? 'desk' : raw[0];
12248
+ const result = await runTaskCommand(raw);
12249
+ const skipsRender = sub === 'clear-done' && hasFlag(raw, '--dry-run');
12250
+ if (MUTATING_TASK_COMMANDS.has(sub) && !skipsRender) autoRenderTodoFromDb();
12251
+ return result;
12252
+ }
12253
+
12254
+ module.exports = {
12255
+ run,
12256
+ taskDayGroups,
12257
+ taskDayTextGroups,
12258
+ taskDayTitle,
12259
+ taskReviewLanding,
12260
+ taskReviewLandingLines,
12261
+ delegateTask,
12262
+ AGENT_ENV_MARKERS,
12263
+ autoRenderTodoFromDb,
12264
+ };