atris 3.34.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 (163) hide show
  1. package/AGENTS.md +35 -0
  2. package/FOR_AGENTS.md +5 -3
  3. package/README.md +5 -3
  4. package/atris/GETTING_STARTED.md +1 -1
  5. package/atris/atris.md +3 -0
  6. package/atris/policies/day-loop-voice.md +102 -0
  7. package/atris/policies/outbound-artifact-gate.md +2 -0
  8. package/atris/skills/design/SKILL.md +56 -32
  9. package/atris/skills/endgame/SKILL.md +12 -6
  10. package/atris/skills/engines/SKILL.md +35 -8
  11. package/atris/skills/fable-method/SKILL.md +66 -0
  12. package/atris/skills/improve/SKILL.md +65 -45
  13. package/atris/skills/render-cli/SKILL.md +88 -0
  14. package/atris/skills/youtube/SKILL.md +10 -1
  15. package/atris.md +6 -2
  16. package/ax +617 -31
  17. package/bin/atris.js +719 -266
  18. package/commands/activate.js +194 -88
  19. package/commands/aeo.js +52 -0
  20. package/commands/agents.js +166 -0
  21. package/commands/autoland.js +718 -72
  22. package/commands/autopilot-front.js +20 -2
  23. package/commands/autopilot.js +118 -2
  24. package/commands/avail.js +407 -0
  25. package/commands/bench.js +188 -0
  26. package/commands/brain.js +3 -0
  27. package/commands/brief.js +651 -0
  28. package/commands/business-sync.js +192 -6
  29. package/commands/business.js +91 -8
  30. package/commands/clean.js +50 -24
  31. package/commands/close.js +1083 -0
  32. package/commands/cloud.js +245 -0
  33. package/commands/codex-goal.js +26 -2
  34. package/commands/compile.js +292 -1
  35. package/commands/computer.js +150 -3
  36. package/commands/dream.js +365 -0
  37. package/commands/drill.js +371 -0
  38. package/commands/drive.js +187 -0
  39. package/commands/engine.js +1061 -29
  40. package/commands/experiments.js +28 -0
  41. package/commands/feed.js +202 -0
  42. package/commands/feedback.js +34 -12
  43. package/commands/fleet-report.js +206 -0
  44. package/commands/github.js +38 -0
  45. package/commands/gm.js +285 -3
  46. package/commands/goal.js +247 -0
  47. package/commands/improve.js +642 -26
  48. package/commands/init.js +83 -43
  49. package/commands/integrations.js +39 -11
  50. package/commands/interview.js +209 -0
  51. package/commands/land.js +253 -52
  52. package/commands/lesson.js +112 -1
  53. package/commands/lifecycle.js +39 -3
  54. package/commands/linear.js +38 -0
  55. package/commands/log.js +84 -1
  56. package/commands/loops.js +220 -16
  57. package/commands/meet.js +220 -0
  58. package/commands/member.js +899 -66
  59. package/commands/mission.js +3512 -332
  60. package/commands/next.js +137 -0
  61. package/commands/now.js +240 -21
  62. package/commands/one-lap.js +776 -0
  63. package/commands/orb.js +314 -0
  64. package/commands/pack-craft.js +179 -0
  65. package/commands/pack.js +823 -0
  66. package/commands/play.js +3 -2
  67. package/commands/probe.js +30 -3
  68. package/commands/pulse.js +241 -46
  69. package/commands/push.js +260 -82
  70. package/commands/radar.js +259 -14
  71. package/commands/rainmaker.js +49 -0
  72. package/commands/report.js +415 -0
  73. package/commands/scout.js +147 -0
  74. package/commands/search.js +363 -0
  75. package/commands/serve.js +54 -0
  76. package/commands/skill.js +47 -3
  77. package/commands/slop.js +50 -2
  78. package/commands/soul.js +1 -1
  79. package/commands/status.js +50 -5
  80. package/commands/stream.js +861 -0
  81. package/commands/stripe.js +38 -0
  82. package/commands/study.js +693 -0
  83. package/commands/supabase.js +39 -0
  84. package/commands/sync.js +67 -54
  85. package/commands/task.js +2275 -182
  86. package/commands/team.js +73 -0
  87. package/commands/truth.js +29 -3
  88. package/commands/unknowns.js +627 -0
  89. package/commands/update.js +44 -0
  90. package/commands/vercel.js +38 -0
  91. package/commands/verify.js +96 -0
  92. package/commands/watch.js +303 -0
  93. package/commands/wish.js +500 -0
  94. package/commands/workflow.js +11 -5
  95. package/commands/worktree.js +299 -20
  96. package/commands/write.js +399 -0
  97. package/commands/xp.js +29 -11
  98. package/lib/auto-accept-certified.js +391 -43
  99. package/lib/autoland.js +353 -52
  100. package/lib/ax-auto-lane.js +79 -0
  101. package/lib/bench/context.js +147 -0
  102. package/lib/bench/engines.js +141 -0
  103. package/lib/bench/report.js +140 -0
  104. package/lib/bench/runner.js +512 -0
  105. package/lib/brief-ledger.js +350 -0
  106. package/lib/cloud-mission.js +259 -0
  107. package/lib/codex-flight.js +154 -0
  108. package/lib/default-runner.js +45 -0
  109. package/lib/default-verifier.js +70 -0
  110. package/lib/engine-registry.js +232 -0
  111. package/lib/experiments/daily.js +640 -0
  112. package/lib/fleet.js +2431 -38
  113. package/lib/improve-vitals-html.js +171 -0
  114. package/lib/known-commands.js +58 -0
  115. package/lib/loop-doctor.js +416 -0
  116. package/lib/member-switches.js +144 -0
  117. package/lib/memory-view.js +14 -5
  118. package/lib/mission-room.js +1 -0
  119. package/lib/mission-root.js +52 -0
  120. package/lib/mission-runtime-loop.js +7 -0
  121. package/lib/next-moves.js +327 -10
  122. package/lib/official-cli-integration.js +174 -0
  123. package/lib/one-lap-validator.js +60 -0
  124. package/lib/orb-context.js +477 -0
  125. package/lib/orb-scorecard.js +224 -0
  126. package/lib/outbound-send-gate.js +165 -0
  127. package/lib/permission-grants.js +293 -0
  128. package/lib/policy-lessons.js +52 -1
  129. package/lib/pulse.js +277 -3
  130. package/lib/receipt-block.js +168 -0
  131. package/lib/receipt-evidence.js +65 -4
  132. package/lib/review-integrity.js +147 -0
  133. package/lib/router-brain.js +352 -0
  134. package/lib/runner-command.js +33 -0
  135. package/lib/self-drive.js +258 -0
  136. package/lib/short-name.js +103 -0
  137. package/lib/spawn-env.js +18 -0
  138. package/lib/state-detection.js +56 -1
  139. package/lib/sync-status.js +59 -0
  140. package/lib/task-db.js +319 -27
  141. package/lib/task-proof.js +43 -1
  142. package/lib/task-receipt.js +93 -0
  143. package/lib/team-presence.js +260 -0
  144. package/lib/tool-result-encode.js +7 -0
  145. package/lib/trust-tiers.js +90 -0
  146. package/lib/usage.js +107 -0
  147. package/lib/voice-gate.js +163 -0
  148. package/lib/wish-audit.js +1368 -0
  149. package/lib/wish-delegate.js +1840 -0
  150. package/lib/wish-design.js +110 -0
  151. package/lib/wish-stats.js +183 -0
  152. package/lib/wish-store.js +354 -0
  153. package/lib/zip.js +221 -0
  154. package/package.json +3 -1
  155. package/templates/loops/atris/loops/LOOPS.md +55 -0
  156. package/templates/loops/atris/loops/TICK.md +24 -0
  157. package/templates/loops/atris/loops/feedback.md +22 -0
  158. package/templates/loops/atris/loops/quality.md +22 -0
  159. package/templates/loops/atris/wiki/systems/loops.md +41 -0
  160. package/utils/api.js +5 -1
  161. package/utils/auth.js +57 -21
  162. package/utils/update-check.js +27 -6
  163. package/atris/learnings.jsonl +0 -1
package/commands/task.js CHANGED
@@ -7,15 +7,25 @@ const fs = require('fs');
7
7
  const http = require('http');
8
8
  const path = require('path');
9
9
  const os = require('os');
10
- const { taskProofState, buildVerifiedProof } = require('../lib/task-proof');
11
- const { evaluateAutoAccept, parseVerifyCommand, runVerifyCommand, DENIED_TAGS } = require('../lib/auto-accept-certified');
12
- const { extractReceiptEvidence } = require('../lib/receipt-evidence');
10
+ const { taskProofState } = require('../lib/task-proof');
11
+ const {
12
+ evaluateAutoAccept,
13
+ isAgentCertified,
14
+ isAutoCertifyVerifyCommandAllowed,
15
+ parseVerifyCommand,
16
+ runVerifyCommand,
17
+ runVerifyCommandCached,
18
+ DENIED_TAGS,
19
+ } = require('../lib/auto-accept-certified');
20
+ const { extractReceiptEvidence, RECEIPT_PATH_PATTERN } = require('../lib/receipt-evidence');
13
21
  const escapeRegExp = require('../lib/escape-regexp');
22
+ const reviewIntegrity = require('../lib/review-integrity');
23
+ const { gateForHuman, isRetiredFillerReason, landingWhyClause, numberWord } = require('../lib/voice-gate');
14
24
  const {
15
25
  normalizeOwnerSlug,
16
26
  resolveFunctionalOwner: resolveFunctionalTaskOwner,
17
27
  } = require('../lib/functional-owner');
18
- const { operatorReady, hasAgentJargon } = require('./autoland');
28
+ const { operatorReady, hasAgentJargon, explainResult } = require('./autoland');
19
29
  const {
20
30
  TASK_INSPECT_FIELDS,
21
31
  readFieldsFlag,
@@ -38,6 +48,13 @@ const REVIEW_LANE_RUN_MAX_RUNS = 20;
38
48
  const PENDING_REVIEW_CHAT_STOP_REASON = 'pending_review_chat_waiting_for_agent_review';
39
49
  const PROOF_BOUNDARY_BLOCKED_ACTION = 'proof_boundary_blocked';
40
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;
41
58
 
42
59
  const STATUS_PLAN_TAGS = new Set([
43
60
  'agent',
@@ -99,11 +116,11 @@ function getTaskDb() {
99
116
  }
100
117
  }
101
118
 
102
- function warnIfTaskTitleNeedsOperatorWhy(title) {
119
+ function warnIfTaskTitleNeedsOperatorWhy(title, options = {}) {
103
120
  const text = String(title || '').trim();
104
121
  if (!text || operatorReady(text)) return null;
105
- 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.';
106
- 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);
107
124
  return warning;
108
125
  }
109
126
 
@@ -111,6 +128,13 @@ function taskUsageText() {
111
128
  return `
112
129
  atris task - durable local task state (SQLite, gitignored)
113
130
 
131
+ golden path (zero human turns):
132
+ atris task delegate "fix the login bug" --to <member>
133
+ atris task claim <id> --as <member>
134
+ ... build ...
135
+ atris task ready <id> --verify
136
+ atris autoland tick # second check runs, task lands
137
+
114
138
  atris task Show the task desk
115
139
  atris task new "<title>" Create a task
116
140
  atris task next [--tag <tag>] [--create-next]
@@ -118,14 +142,19 @@ atris task - durable local task state (SQLite, gitignored)
118
142
  atris task continue-work <id> Create/reuse a certified Review follow-up task
119
143
  atris task say <id> "<message>" Add context to a task
120
144
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
121
- atris task ready <id> --proof "..." Agent proof ready; native goal can complete
122
- 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)
149
+ Writes atris/runs/ receipt (pass or fail), folds path into proof
150
+ atris task receipt <id> --verify "<cmd>" Run <cmd> and write an atris/runs/ receipt without going to ready
123
151
  atris task plan-preview "<purpose>" [--tag <tag>] [--owner <member>] [--task <id>]
124
152
  Show the plain Plan before work starts
125
- atris task ready <id> --proof "..." [--changed "..." --checked "..." --saved "..." --try "..."]
153
+ atris task ready <id> --proof "..." --result "<sentence>" [--changed "..." --checked "..." --saved "..." --try "..."]
126
154
  Agent proof ready; records Result if needed
127
- atris task ready <id> --proof "..." [--happened "..." --checked "..." --tested "..." --decision "..."]
155
+ atris task ready <id> --proof "..." --result "<sentence>" [--happened "..." --checked "..." --tested "..." --decision "..."]
128
156
  Agent proof ready; writes the human result receipt
157
+ atris task result <id> "<sentence>" Set or replace the day-one PM result sentence
129
158
  atris task result <id> --changed "..." --checked "..." [--saved "..."] [--try "..."]
130
159
  Show the plain Result and store trace on the task
131
160
  atris task review-chat <id> [--as <owner>] Start a task-owned /codex verification chat
@@ -133,20 +162,25 @@ atris task - durable local task state (SQLite, gitignored)
133
162
  Human accepts proof, marks done; --public also publishes AgentXP
134
163
  atris task certify-verified [--dry-run] [--limit <n>] [--as <actor>]
135
164
  Re-run the runnable check named in each Review proof as a second actor; passing rows certify (denied lanes and check-less rows wait for a human)
136
- atris task auto-accept-certified --dry-run [--strict-verify] [--limit <n>]
165
+ atris task auto-accept-certified --dry-run [--strict-verify] [--all] [--limit <n>]
137
166
  Preview certified Review rows; live accept needs --confirm-human-accept --as <human>
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
138
169
  atris task revise <id> --note "..." Send reviewed work back to Do
139
170
 
140
171
  atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
141
- atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] Create assigned work
172
+ atris task delegate "<title>" [--to <member>] [--executed-by <engine>] [--goal-id <id>] [--tag <tag>] Create assigned work
142
173
  atris task plan <id> --goal "..." --exit "..." --proof-needed "..."
143
174
  Record a task-owned Plan stage
144
175
  atris task do <id> --as <owner> --first-move "..."
145
176
  Start task-owned Do work from the plan
146
177
  atris task backlog <id> [--reason "..."] Move a planned open task back to Backlog
147
178
  atris task clear-plan --yes Move all planned open tasks back to Backlog
148
- atris task day [--json] Show today's owner-grouped task list
149
- atris task list [--all] [--status <s>] List tasks (default: this workspace)
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
181
+ --all stays in this workspace; --everywhere spans workspaces
182
+ atris task list [--all] [--everywhere] [--status <s>]
183
+ list tasks in this workspace; --everywhere spans workspaces
150
184
  atris task claim <id> [--as <owner>] Atomic claim
151
185
  atris task release <id> [--as <owner>] Release your own mistaken claim back to open
152
186
  atris task capabilities [--json] Read-only task CLI/API capability contract
@@ -163,6 +197,10 @@ atris task - durable local task state (SQLite, gitignored)
163
197
  Advance the scoped current task one safe step
164
198
  review-state lanes: needs-agent, continue-work, human-accept-waiting, certified
165
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
201
+ atris task tag <id> --add <tag> [--remove <tag>]
202
+ Update tags on an existing task (e.g. --add needs-human to hold it
203
+ from sweep + fleet staffing); logs a task_tags_updated event
166
204
  atris task show <id> [--json] Show a task card + dialogue
167
205
  atris task inspect <id> --fields review,status,title [--json]
168
206
  Field-selectable task state (review metadata, status, title, owner, tag)
@@ -170,6 +208,12 @@ atris task - durable local task state (SQLite, gitignored)
170
208
  atris task step <id> [--json] Refine chat, then advance one safe Plan/Do/Review step
171
209
  atris task done <id> --proof "..." Mark complete with proof
172
210
  atris task done <id> --failed [--proof "..."] Mark failed, optionally reviewed
211
+ atris task archive <id> --reason "..." [--from-failed]
212
+ Sweep off-roadmap/duplicate work as archived (not failed);
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
215
+ atris task relabel-archived [--dry-run|--apply]
216
+ One-time OBL-1622 migration: relabel June-10 backlog-reset rows failed -> archived
173
217
  atris task finish <id> --proof "..." Legacy alias for done with proof
174
218
  atris task review <id> --reward <n> [--verify "<cmd>"]
175
219
  Write review event + RSI episode
@@ -183,10 +227,13 @@ atris task - durable local task state (SQLite, gitignored)
183
227
  atris task sync --dry-run Plan cloud/Swarlo task sync writes
184
228
  atris task import <file> One-shot import from TODO.md
185
229
  atris task lineage <id> [--json] Show endgame -> tasks -> commits chain
186
- atris task events [id] [--limit <n>] Print recent task events
187
- atris task events --all Print the full append-only ledger
188
- atris task export [--out <file>] Write web/desktop JSON projection
189
- atris task render [--out <file>] Regenerate compact TODO.md view from state
230
+ atris task events [id] [--limit <n>] print recent task events
231
+ atris task events --all print the full current-workspace ledger
232
+ atris task events --everywhere print the full ledger across workspaces
233
+ atris task export [--all] [--everywhere] [--out <file>]
234
+ write web/desktop JSON projection
235
+ atris task render [--all] [--everywhere] [--out <file>]
236
+ regenerate compact TODO.md view from state
190
237
  atris task where Print db path + workspace scope
191
238
  atris task help This help
192
239
 
@@ -226,6 +273,15 @@ function hasFlag(args, name) {
226
273
  return args.indexOf(name) !== -1;
227
274
  }
228
275
 
276
+ function taskScopeEverywhere(args = [], options = {}) {
277
+ if (options.everywhere !== undefined) return Boolean(options.everywhere);
278
+ return hasFlag(args, '--everywhere');
279
+ }
280
+
281
+ function scopedWorkspaceRoot(taskDb, args = [], options = {}) {
282
+ return taskScopeEverywhere(args, options) ? null : taskDb.workspaceRoot();
283
+ }
284
+
229
285
  function hasEmptyFlagValue(args, name) {
230
286
  const i = args.indexOf(name);
231
287
  return i !== -1 && args[i + 1] === '';
@@ -387,6 +443,22 @@ function proofFlagValue(args) {
387
443
  return typeof proof === 'string' ? proof.trim() : '';
388
444
  }
389
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
+
390
462
  function textFlag(args, names) {
391
463
  for (const name of names) {
392
464
  const value = flag(args, name);
@@ -416,17 +488,27 @@ function landingNeedsDayOnePm(sentence, title) {
416
488
  const text = normalizedLandingSentence(sentence);
417
489
  if (!text) return true;
418
490
  if (text.toLowerCase() === normalizedLandingSentence(defaultLandingSentenceForTitle(title)).toLowerCase()) return true;
419
- return hasAgentJargon(text) || !operatorReady(text);
491
+ return hasAgentJargon(text) || /\bas\s+exists?\b/i.test(text) || !operatorReady(text);
420
492
  }
421
493
 
422
494
  function warnIfLandingNeedsDayOnePm(landing, title) {
423
495
  const sentence = landing && typeof landing === 'object' ? landing.happened : '';
424
496
  if (!landingNeedsDayOnePm(sentence, title)) return null;
425
- 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.';
426
498
  console.error(warning);
427
499
  return warning;
428
500
  }
429
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
+
430
512
  function numericFlag(args, name) {
431
513
  const value = flag(args, name);
432
514
  if (value === null || value === true || value === undefined) return null;
@@ -441,6 +523,102 @@ function meaningfulTaskProofIssue(proof, { required = true } = {}) {
441
523
  return state.ok ? null : state.reason;
442
524
  }
443
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
+
444
622
  function requireMeaningfulTaskProof(label, proof, { required = true } = {}) {
445
623
  const issue = meaningfulTaskProofIssue(proof, { required });
446
624
  if (issue) failTask(label, 'weak_proof', `meaningful proof required: ${issue}`);
@@ -462,14 +640,22 @@ function positional(args) {
462
640
  });
463
641
  }
464
642
 
465
- function writeDefaultProjection(taskDb, db, { all = false } = {}) {
643
+ function writeDefaultProjection(taskDb, db, options = {}) {
644
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, [], options);
466
645
  const projection = enrichTaskProjection(taskDb.taskProjection(db, {
467
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
468
- limit: 500,
646
+ workspaceRoot,
647
+ limit: options.all ? null : 500,
469
648
  }));
470
649
  const outPath = path.resolve(path.join('.atris', 'state', 'tasks.projection.json'));
650
+ const output = JSON.stringify(projection, null, 2) + '\n';
471
651
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
472
- 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');
473
659
  return { projection, outPath };
474
660
  }
475
661
 
@@ -692,6 +878,11 @@ function certifiedReviewNextAction(nextTaskTitle) {
692
878
  }
693
879
 
694
880
  function proofBoundaryBlockedEvaluation(task) {
881
+ const missionXpBoundary = missionXpProofBoundaryEvaluation(task);
882
+ if (missionXpBoundary) return missionXpBoundary;
883
+ // strictVerify stays off here: this is a render-path probe for the boundary
884
+ // reason only, and the default-true strict mode would spawn the verify
885
+ // subprocess for every certified row just to draw the desk.
695
886
  const evaluation = evaluateAutoAccept(task, { strictVerify: false, minPasses: 0 });
696
887
  return evaluation && evaluation.reason === 'proof_unmerged_or_draft_pr_boundary'
697
888
  ? evaluation
@@ -882,26 +1073,11 @@ function proofToReasonText(proof) {
882
1073
  return /[.!?]$/.test(section) ? section : `${section}.`;
883
1074
  }
884
1075
 
885
- function titleToReasonText(task, proof = '') {
886
- const title = String(task?.title || '').replace(/\s+/g, ' ').trim();
887
- const text = `${title} ${proof || ''}`.toLowerCase();
888
- if (/\b(priv(?:ate|acy)|secret|payload|leak|redact)\b/.test(text)) {
889
- return 'It keeps private data out of the fast human decision screen.';
890
- }
891
- if (/\bapprove\b/.test(text) && /\b(command|exact|preview|ux)\b/.test(text)) {
892
- return 'It lets the operator see the next command without hunting.';
893
- }
894
- if (/\b(stale|expire|expired)\b/.test(text) && /\bapproval/.test(text)) {
895
- return 'It stops old approvals from running after their context has gone stale.';
896
- }
897
- if (/\bapproval|approve|permission\b/.test(text)) {
898
- return 'It keeps real-world side effects behind a clear human decision.';
899
- }
900
- if (/\btest|self-test|harness|verifier|proof\b/.test(text)) {
901
- return 'It gives the human a repeatable check before approval.';
902
- }
903
- return 'It turns the task title into a concrete result the human can approve.';
904
- }
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.
905
1081
 
906
1082
  function proofToHumanCheck(proof) {
907
1083
  const text = String(proof || '').replace(/\s+/g, ' ').trim();
@@ -1011,17 +1187,30 @@ function taskReviewLanding(task, review = {}, payload = {}) {
1011
1187
  const agentCertified = review.agent_certified === true || metadata.agent_certified === true;
1012
1188
  const approvalStatus = review.approval_status || metadata.approval_status || null;
1013
1189
  const explicitHappened = landingPayloadValue(payload, metadata, 'happened')
1014
- || 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;
1015
1191
  const explicitChecked = landingPayloadValue(payload, metadata, 'checked')
1016
1192
  || payload.checked || metadata.result_checked || metadata.human_checked || metadata.checked;
1017
1193
  const explicitTested = landingPayloadValue(payload, metadata, 'tested');
1018
1194
  const explicitDecision = landingPayloadValue(payload, metadata, 'decision');
1019
- const explicitReason = landingPayloadValue(payload, metadata, 'reason')
1195
+ const explicitReasonRaw = landingPayloadValue(payload, metadata, 'reason')
1020
1196
  || landingPayloadValue(payload, metadata, 'why')
1021
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
+ }
1022
1211
  return {
1023
- happened: clipStatusText(explicitHappened || titleToResultText(task.title), 220),
1024
- reason: clipStatusText(explicitReason || proofToReasonText(proof) || titleToReasonText(task, proof), 220),
1212
+ happened,
1213
+ reason,
1025
1214
  checked: clipStatusText(explicitChecked || proofToHumanCheck(proof), 220),
1026
1215
  tested: clipStatusText(explicitTested || taskReviewLandingTested(proof), 260),
1027
1216
  decision: clipStatusText(explicitDecision || (task.status === 'done'
@@ -1057,11 +1246,65 @@ function taskReviewSavedText(task, review = {}, ref = taskRef(task)) {
1057
1246
  return `Work record saved as ${ref}.`;
1058
1247
  }
1059
1248
 
1249
+ function cleanReviewProofText(value) {
1250
+ const text = String(value || '').trim();
1251
+ return text || null;
1252
+ }
1253
+
1254
+ function reviewMessageLooksLikeProof(value) {
1255
+ const text = cleanReviewProofText(value);
1256
+ return Boolean(text && /\b(?:proof|verified|verifier|passed|receipt|git diff --check|node --test|npm test|pnpm test|yarn test|pytest|typecheck)\b/i.test(text));
1257
+ }
1258
+
1259
+ function taskReviewEventProof(task) {
1260
+ const events = Array.isArray(task.events) ? task.events : [];
1261
+ for (const event of events.slice().reverse()) {
1262
+ const payload = event && event.payload && typeof event.payload === 'object' ? event.payload : {};
1263
+ const explicit = cleanReviewProofText(payload.proof || payload.verify);
1264
+ if (explicit) return explicit;
1265
+ const message = cleanReviewProofText(payload.content || payload.chat_packet || payload.stage_packet);
1266
+ if (message && reviewMessageLooksLikeProof(message)) return message;
1267
+ }
1268
+ const messages = Array.isArray(task.messages) ? task.messages : [];
1269
+ for (const message of messages.slice().reverse()) {
1270
+ const content = cleanReviewProofText(message && message.content);
1271
+ if (content && reviewMessageLooksLikeProof(content)) return content;
1272
+ }
1273
+ return null;
1274
+ }
1275
+
1276
+ function taskReviewProofFallback(task, payload = {}) {
1277
+ if (!task || task.status !== 'review') return null;
1278
+ const metadata = task.metadata || {};
1279
+ return cleanReviewProofText(metadata.latest_agent_proof)
1280
+ || cleanReviewProofText(payload.proof)
1281
+ || cleanReviewProofText(metadata.proof)
1282
+ || cleanReviewProofText(metadata.verify)
1283
+ || cleanReviewProofText(metadata.latest_agent_verify)
1284
+ || cleanReviewProofText(payload.verify)
1285
+ || taskReviewEventProof(task);
1286
+ }
1287
+
1288
+ function reviewReceiptPath(proofText, root) {
1289
+ const evidence = extractReceiptEvidence(proofText, root);
1290
+ if (!evidence) return null;
1291
+ return evidence.receipts?.[0]?.path || evidence.missing?.[0] || null;
1292
+ }
1293
+
1294
+ function withReviewReceiptPath(review, root) {
1295
+ if (!review) return null;
1296
+ return {
1297
+ ...review,
1298
+ receipt_path: reviewReceiptPath(review.proof, root),
1299
+ };
1300
+ }
1301
+
1060
1302
  function taskReviewSummary(task) {
1061
1303
  const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed' || e.event_type === 'proof_ready' || e.event_type === 'revision_requested');
1062
1304
  const payload = reviewed && reviewed.payload || {};
1063
1305
  const metadata = task.metadata || {};
1064
- if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified) return null;
1306
+ const fallbackProof = taskReviewProofFallback(task, payload);
1307
+ if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified && !fallbackProof) return null;
1065
1308
  if (reviewed && reviewed.event_type === 'revision_requested') {
1066
1309
  const review = {
1067
1310
  summary: reviewSummary(task, payload),
@@ -1101,7 +1344,7 @@ function taskReviewSummary(task) {
1101
1344
  const review = {
1102
1345
  summary: reviewSummary(task, payload),
1103
1346
  reward: reviewed && reviewed.event_type === 'reviewed' && payload.reward !== undefined ? payload.reward : null,
1104
- proof: readyField('proof', 'latest_agent_proof'),
1347
+ proof: readyField('proof', 'latest_agent_proof') || fallbackProof,
1105
1348
  lesson: readyField('lesson', 'latest_agent_lesson'),
1106
1349
  next_task: readyField('next_task', 'latest_agent_next_task'),
1107
1350
  approval_status: metadata.approval_status || (task.status === 'review' ? 'pending' : null),
@@ -1132,6 +1375,7 @@ function taskReviewInspectMetadata(task) {
1132
1375
  human_revision_count: review.human_revision_count || null,
1133
1376
  human_revision_note: review.human_revision_note || null,
1134
1377
  reward: review.reward ?? null,
1378
+ receipt_path: review.receipt_path || null,
1135
1379
  };
1136
1380
  }
1137
1381
 
@@ -1317,7 +1561,7 @@ function enrichTaskProjection(projection) {
1317
1561
  const parentLinkType = parentFromParentId ? 'parent_task_id' : parentFromGoalId ? 'goal_id' : null;
1318
1562
  const parentId = parent ? parent.id : metadata.parent_task_id || null;
1319
1563
  const childTasks = children.get(task.id) || [];
1320
- const review = taskReviewSummary(task);
1564
+ const review = withReviewReceiptPath(taskReviewSummary(task), root);
1321
1565
  return {
1322
1566
  ...task,
1323
1567
  objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
@@ -1502,6 +1746,44 @@ function latestTaskEvent(task) {
1502
1746
  return events.length ? events[events.length - 1] : null;
1503
1747
  }
1504
1748
 
1749
+ function verifiedProofCommand(proof) {
1750
+ const match = String(proof || '').match(/\[verified\]\s+`([^`]+)`\s+passed\s+\(exit 0\)/i);
1751
+ return match ? String(match[1] || '').trim() : '';
1752
+ }
1753
+
1754
+ function autoCertifyCommandCandidatesForTask(task) {
1755
+ const metadata = task && task.metadata || {};
1756
+ const review = task && task.review || {};
1757
+ const proof = String(review.proof || metadata.latest_agent_proof || '');
1758
+ const candidates = [
1759
+ metadata.verify,
1760
+ metadata.latest_agent_verify,
1761
+ verifiedProofCommand(review.proof),
1762
+ verifiedProofCommand(metadata.latest_agent_proof),
1763
+ ...taskReviewEvidenceCommands(proof),
1764
+ ].map(value => String(value || '').trim()).filter(Boolean);
1765
+ const seen = new Set();
1766
+ return candidates.filter((candidate) => {
1767
+ const key = candidate.toLowerCase();
1768
+ if (seen.has(key)) return false;
1769
+ seen.add(key);
1770
+ return true;
1771
+ });
1772
+ }
1773
+
1774
+ function reviewBlockerForTask(task) {
1775
+ const ref = taskRef(task);
1776
+ const candidates = autoCertifyCommandCandidatesForTask(task);
1777
+ const safe = candidates.find(command => isAutoCertifyVerifyCommandAllowed(command));
1778
+ const unsafe = candidates.find(command => !isAutoCertifyVerifyCommandAllowed(command));
1779
+ const reason = !safe && unsafe ? 'verify_command_not_allowed' : 'needs_second_actor_review';
1780
+ return {
1781
+ reason,
1782
+ verify_command: (!safe && unsafe) ? unsafe : (safe || candidates[0] || null),
1783
+ next_command: `atris task review-chat ${ref} --as codex-review`,
1784
+ };
1785
+ }
1786
+
1505
1787
  function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExistingReviewFollowUp = null } = {}) {
1506
1788
  const review = task && task.review || {};
1507
1789
  if (task && task.status !== 'review') return null;
@@ -1521,7 +1803,15 @@ function reviewHandoffForTask(task, { suppressExistingFollowUp = false, hasExist
1521
1803
  if (proofBoundary) {
1522
1804
  handoff.reason = proofBoundary.reason;
1523
1805
  handoff.next_action_detail = proofBoundary.next_action || null;
1524
- 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}"`;
1810
+ } else if (!agentCertified) {
1811
+ const blocker = reviewBlockerForTask(task);
1812
+ handoff.reason = blocker.reason;
1813
+ handoff.next_action_detail = blocker.verify_command || null;
1814
+ handoff.review_chat_command = blocker.next_command;
1525
1815
  } else if (agentCertified && nextTask && !hasExistingFollowUp) {
1526
1816
  handoff.next_task = nextTask;
1527
1817
  handoff.continue_work_command = continueWorkCommandForTask(task);
@@ -1960,7 +2250,8 @@ function compactTaskForStatus(task) {
1960
2250
  id: task.id,
1961
2251
  display_id: task.display_id || null,
1962
2252
  legacy_ref: task.legacy_ref || taskRef(task.id),
1963
- title: clipStatusText(task.title, 140),
2253
+ title: clipStatusTitle(task.title, 140),
2254
+ result: clipStatusText(task.result || metadata.result, 180) || null,
1964
2255
  status: task.status,
1965
2256
  updated_at: task.updated_at,
1966
2257
  };
@@ -1980,6 +2271,7 @@ function compactTaskForStatus(task) {
1980
2271
  if (task.review.proof) review.proof = clipStatusText(task.review.proof, 180);
1981
2272
  if (task.review.lesson) review.lesson = clipStatusText(task.review.lesson, 180);
1982
2273
  if (task.review.next_task) review.next_task = clipStatusText(task.review.next_task, 140);
2274
+ if (Object.prototype.hasOwnProperty.call(task.review, 'receipt_path')) review.receipt_path = task.review.receipt_path || null;
1983
2275
  if (task.review.approval_status) review.approval_status = task.review.approval_status;
1984
2276
  if (task.review.agent_review_pass_count) review.agent_review_pass_count = task.review.agent_review_pass_count;
1985
2277
  if (task.review.agent_certified) review.agent_certified = task.review.agent_certified;
@@ -2013,7 +2305,7 @@ function compactTaskFromProjection(projection, id) {
2013
2305
  function compactEventPayload(payload) {
2014
2306
  if (!payload || typeof payload !== 'object') return null;
2015
2307
  const out = {};
2016
- 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']) {
2017
2309
  if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') out[key] = payload[key];
2018
2310
  }
2019
2311
  return Object.keys(out).length ? out : null;
@@ -2038,6 +2330,19 @@ function clipStatusText(value, max = 180) {
2038
2330
  return `${text.slice(0, max - 1)}…`;
2039
2331
  }
2040
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
+
2041
2346
  function compactReviewActionRef(task, { hasExistingReviewFollowUp = null } = {}) {
2042
2347
  if (!task) return null;
2043
2348
  const handoff = reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp }) || {};
@@ -3055,6 +3360,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
3055
3360
  const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
3056
3361
  const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
3057
3362
  const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
3363
+ const everywhere = taskScopeEverywhere(args, options);
3058
3364
  const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
3059
3365
  const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
3060
3366
  const standalone = taskCapabilitiesContract();
@@ -3062,6 +3368,7 @@ function taskCapabilitiesCheckReport(taskDb, db, args = [], options = {}) {
3062
3368
  owner,
3063
3369
  reviewer,
3064
3370
  all,
3371
+ everywhere,
3065
3372
  limit,
3066
3373
  scope,
3067
3374
  });
@@ -3326,7 +3633,11 @@ function selectTaskForCurrent(projection, { owner = DEFAULT_OWNER, scope = {}, h
3326
3633
  if (reviewNeedsAgent) return { task: reviewNeedsAgent, reason: 'review_needs_agent_verification' };
3327
3634
  const reviewProofBoundaryBlocked = columns.review.find(task => reviewHandoffForTask(task, { suppressExistingFollowUp: true, hasExistingReviewFollowUp })?.next_action === PROOF_BOUNDARY_BLOCKED_ACTION);
3328
3635
  if (reviewProofBoundaryBlocked) return { task: reviewProofBoundaryBlocked, reason: 'review_proof_boundary_blocked' };
3329
- const planQueue = normalizedScope.goal_id ? sortTasksOldestFirst(columns.plan) : columns.plan;
3636
+ // Scoped selection (goal_id, tag, status, or review_state) implies a sequenced
3637
+ // work stream (e.g. golden-path "pass 1a" before "pass 2"): earlier-created
3638
+ // tasks must win over newer ones, not the newest-first default used for the
3639
+ // unscoped desk view.
3640
+ const planQueue = !taskQueueScopeIsEmpty(normalizedScope) ? sortTasksOldestFirst(columns.plan) : columns.plan;
3330
3641
  const planReady = planQueue[0];
3331
3642
  if (planReady) return { task: planReady, reason: 'plan_ready' };
3332
3643
  const backlogIdea = columns.backlog[0];
@@ -3392,18 +3703,20 @@ function buildTaskCurrent(taskDb, db, args = [], options = {}) {
3392
3703
  const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
3393
3704
  const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
3394
3705
  const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
3706
+ const everywhere = taskScopeEverywhere(args, options);
3707
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
3395
3708
  const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
3396
3709
  const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
3397
- const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
3710
+ const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
3398
3711
  const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(
3399
3712
  taskDb,
3400
3713
  db,
3401
- all ? null : taskDb.workspaceRoot(),
3714
+ workspaceRoot,
3402
3715
  );
3403
3716
  const hasPendingReviewChat = buildPendingReviewChatPredicate(
3404
3717
  taskDb,
3405
3718
  db,
3406
- all ? null : taskDb.workspaceRoot(),
3719
+ workspaceRoot,
3407
3720
  );
3408
3721
  return {
3409
3722
  projection,
@@ -3676,12 +3989,14 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
3676
3989
  const owner = options.owner || flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER;
3677
3990
  const reviewer = reviewActor(options.reviewer || flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review');
3678
3991
  const all = options.all !== undefined ? Boolean(options.all) : hasFlag(args, '--all');
3992
+ const everywhere = taskScopeEverywhere(args, options);
3679
3993
  const limit = options.limit !== undefined ? options.limit : taskQueueLimit(args);
3680
3994
  const scope = normalizeTaskQueueScope(options.scope || taskQueueScopeFromArgs(args));
3681
3995
  const capabilitiesCheck = taskCapabilitiesCheckReport(taskDb, db, [], {
3682
3996
  owner,
3683
3997
  reviewer,
3684
3998
  all,
3999
+ everywhere,
3685
4000
  limit,
3686
4001
  scope,
3687
4002
  });
@@ -3689,6 +4004,7 @@ function taskReviewLaneDrainReport(taskDb, db, args = [], options = {}) {
3689
4004
  owner,
3690
4005
  reviewer,
3691
4006
  all,
4007
+ everywhere,
3692
4008
  limit,
3693
4009
  scope,
3694
4010
  excludeTaskIds: options.excludeTaskIds,
@@ -3749,6 +4065,7 @@ function taskReviewLaneActOptionsFromArgs(args = []) {
3749
4065
  owner: flag(args, '--as') || flag(args, '--owner') || DEFAULT_OWNER,
3750
4066
  reviewer: reviewActor(flag(args, '--reviewer') || flag(args, '--as-reviewer') || 'codex-review'),
3751
4067
  all: hasFlag(args, '--all'),
4068
+ everywhere: hasFlag(args, '--everywhere'),
3752
4069
  limit: taskQueueLimit(args),
3753
4070
  scope: taskQueueScopeFromArgs(args),
3754
4071
  dryRun: hasFlag(args, '--dry-run'),
@@ -3766,6 +4083,7 @@ function taskReviewLaneActOptionsFromBody(body = {}, searchParams = new URLSearc
3766
4083
  owner: String(queryOwner || body.owner || body.as || body.actor || DEFAULT_OWNER),
3767
4084
  reviewer: reviewActor(queryReviewer || body.reviewer || body.review_actor || body.reviewActor || 'codex-review'),
3768
4085
  all: searchParams.get('all') === '1' || searchParams.get('all') === 'true' || Boolean(body.all),
4086
+ everywhere: searchParams.get('everywhere') === '1' || searchParams.get('everywhere') === 'true' || Boolean(body.everywhere),
3769
4087
  limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
3770
4088
  scope: mergeTaskQueueScopes(queryScope, bodyScope),
3771
4089
  dryRun: searchParams.get('dry_run') === '1'
@@ -3831,6 +4149,7 @@ function taskReviewLaneAct(taskDb, db, options = {}) {
3831
4149
  owner,
3832
4150
  reviewer,
3833
4151
  all: Boolean(options.all),
4152
+ everywhere: Boolean(options.everywhere),
3834
4153
  limit: options.limit !== undefined ? options.limit : 8,
3835
4154
  scope,
3836
4155
  excludeTaskIds: options.excludeTaskIds,
@@ -4069,6 +4388,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
4069
4388
  owner,
4070
4389
  reviewer,
4071
4390
  all: Boolean(options.all),
4391
+ everywhere: Boolean(options.everywhere),
4072
4392
  limit: options.limit !== undefined ? options.limit : 8,
4073
4393
  scope,
4074
4394
  excludeTaskIds,
@@ -4121,6 +4441,7 @@ function taskReviewLaneLoop(taskDb, db, options = {}) {
4121
4441
  owner,
4122
4442
  reviewer,
4123
4443
  all: Boolean(options.all),
4444
+ everywhere: Boolean(options.everywhere),
4124
4445
  limit: options.limit !== undefined ? options.limit : 8,
4125
4446
  scope,
4126
4447
  excludeTaskIds,
@@ -4308,6 +4629,7 @@ function taskReviewLaneRun(taskDb, db, options = {}) {
4308
4629
  owner,
4309
4630
  reviewer,
4310
4631
  all: Boolean(options.all),
4632
+ everywhere: Boolean(options.everywhere),
4311
4633
  limit: options.limit !== undefined ? options.limit : 8,
4312
4634
  scope,
4313
4635
  excludeTaskIds,
@@ -4417,6 +4739,483 @@ function cmdReviewLaneRun(args) {
4417
4739
  if (!result.ok) process.exit(1);
4418
4740
  }
4419
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
+
4420
5219
  function reviewQueueLimit(args, total) {
4421
5220
  if (hasFlag(args, '--all')) return total;
4422
5221
  const raw = flag(args, '--limit');
@@ -4435,6 +5234,17 @@ function reviewGroupTextLimit(args, total) {
4435
5234
  return Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 10;
4436
5235
  }
4437
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
+
4438
5248
  // Risk order for human attention: named receipts that are missing/failing (0)
4439
5249
  // beat prose-only proofs (1) beat fully validated green evidence (2).
4440
5250
  function evidenceRiskRank(evidence) {
@@ -4488,7 +5298,20 @@ function reviewQueueItem(task, root = process.cwd(), evidence = undefined) {
4488
5298
  return item;
4489
5299
  }
4490
5300
 
4491
- function reviewQueueHygiene(tasks) {
5301
+ function blockedReviewQueueItem(task, root = process.cwd()) {
5302
+ const item = reviewQueueItem(task, root);
5303
+ const blocker = reviewBlockerForTask(task);
5304
+ item.queue_role = 'blocked';
5305
+ item.reason = blocker.reason;
5306
+ item.blocked_reason = blocker.reason;
5307
+ item.next_command = blocker.next_command;
5308
+ item.verify_command = blocker.verify_command;
5309
+ item.accept_command = null;
5310
+ item.land_command = null;
5311
+ return item;
5312
+ }
5313
+
5314
+ function reviewQueueHygiene(tasks) {
4492
5315
  const genericContinuations = (tasks || []).map(task => {
4493
5316
  const issues = genericContinuationIssues(task);
4494
5317
  if (!issues.length) return null;
@@ -4526,7 +5349,10 @@ function taskReviewQueue(projection, args = []) {
4526
5349
  const ordered = [...certified].sort((a, b) =>
4527
5350
  evidenceRiskRank(evidenceByTaskId.get(b.id)) - evidenceRiskRank(evidenceByTaskId.get(a.id))
4528
5351
  || Number(b.updated_at || 0) - Number(a.updated_at || 0));
4529
- const items = ordered.slice(0, limit).map((task) => reviewQueueItem(task, root, evidenceByTaskId.get(task.id)));
5352
+ const certifiedItems = ordered.slice(0, limit).map((task) => reviewQueueItem(task, root, evidenceByTaskId.get(task.id)));
5353
+ const blockedLimit = reviewQueueLimit(args, blocking.length);
5354
+ const blockedItems = blocking.slice(0, blockedLimit).map((task) => blockedReviewQueueItem(task, root));
5355
+ const items = [...certifiedItems, ...blockedItems];
4530
5356
  return {
4531
5357
  schema: 'atris.task_review_queue.v1',
4532
5358
  generated_at: projection.generated_at,
@@ -4537,7 +5363,8 @@ function taskReviewQueue(projection, args = []) {
4537
5363
  evidence_passing: certified.filter((task) => evidenceByTaskId.get(task.id)?.all_passing).length,
4538
5364
  blocking: blocking.length,
4539
5365
  proof_boundary_blocked: proofBoundaryBlocked.length,
4540
- shown: items.length,
5366
+ shown: certifiedItems.length,
5367
+ blocking_shown: blockedItems.length,
4541
5368
  },
4542
5369
  hygiene: reviewQueueHygiene(reviewTasks),
4543
5370
  items,
@@ -4595,30 +5422,72 @@ function taskReviewGroups(projection, key) {
4595
5422
  };
4596
5423
  }
4597
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
+
4598
5442
  function cmdReviews(args) {
4599
5443
  const taskDb = getTaskDb();
4600
5444
  const db = taskDb.open();
4601
- 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
+ });
4602
5458
  const groupByRaw = flag(args, '--group-by');
4603
5459
  if (groupByRaw) {
4604
5460
  const key = reviewGroupKey(groupByRaw);
4605
5461
  const groups = taskReviewGroups(projection, key);
4606
5462
  if (wantsJson(args)) {
4607
- 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
+ });
4608
5475
  return;
4609
5476
  }
4610
- console.log(`READY FOR APPROVAL — grouped by ${key}`);
4611
- 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
+ }
4612
5481
  const visibleGroups = groups.groups.slice(0, reviewGroupTextLimit(args, groups.groups.length));
4613
5482
  visibleGroups.forEach((g, index) => {
4614
5483
  console.log('');
4615
- console.log(`${index + 1}. ${g.value} — ${g.count} task${g.count === 1 ? '' : 's'}`);
4616
- 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}`));
4617
5486
  console.log(` approve this group: ${g.accept_group_command} --confirm-human-accept --as <you>`);
4618
5487
  });
4619
5488
  if (visibleGroups.length < groups.groups.length) {
4620
5489
  console.log('');
4621
- 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.`);
4622
5491
  }
4623
5492
  return;
4624
5493
  }
@@ -4629,30 +5498,45 @@ function cmdReviews(args) {
4629
5498
  ok: true,
4630
5499
  action: 'review_queue',
4631
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
+ },
4632
5507
  queue,
4633
5508
  });
4634
5509
  return;
4635
5510
  }
4636
- console.log('READY FOR APPROVAL');
4637
- console.log(`${queue.counts.certified} ready for approval / ${queue.counts.blocking} need one more check / ${queue.counts.review} total waiting`);
4638
- if (!queue.items.length) {
4639
- console.log('Nothing is ready for approval.');
5511
+ const approvalItems = queue.items.filter(item => item.queue_role !== 'blocked');
5512
+ const blockedItems = queue.items.filter(item => item.queue_role === 'blocked');
5513
+ if (!approvalItems.length && !blockedItems.length) {
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
+ }
4640
5518
  return;
4641
5519
  }
4642
- queue.items.forEach((item, index) => {
4643
- const tag = item.tag ? ` [${item.tag}]` : '';
4644
- const passes = item.review_pass_count ? ` (${item.review_pass_count} reviews)` : '';
4645
- const badge = item.evidence?.all_passing ? ' [evidence:passing]' : '';
4646
- console.log('');
4647
- console.log(`${index + 1}. ${item.display_id || taskRef(item.id)}${tag}${passes}: ${item.title}${badge}`);
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
+ }
5534
+ approvalItems.forEach((item, index) => {
5535
+ if (index > 0) console.log('');
5536
+ console.log(`${index + 1}. ${gateForHuman(item.title, { title: item.title }).text}`);
4648
5537
  if (item.landing) {
4649
- console.log(' Result:');
4650
- if (item.landing.happened) console.log(` What happened: ${item.landing.happened}`);
4651
- if (item.landing.reason) console.log(` Why it matters: ${item.landing.reason}`);
4652
- if (item.landing.checked) console.log(` How I checked: ${item.landing.checked}`);
4653
- if (item.landing.tested) console.log(` What I tested: ${item.landing.tested}`);
4654
- if (item.result?.saved) console.log(` Saved: ${item.result.saved}`);
4655
- 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}`);
4656
5540
  }
4657
5541
  if (verbose && item.proof) console.log(` details: ${item.proof}`);
4658
5542
  if (verbose && item.evidence) {
@@ -4664,14 +5548,23 @@ function cmdReviews(args) {
4664
5548
  item.evidence.missing.forEach((missingPath) => console.log(` receipt: ${missingPath} MISSING`));
4665
5549
  }
4666
5550
  if (verbose && item.review_chat_command) console.log(` /codex: ${item.review_chat_command}`);
4667
- if (item.continue_work_command) console.log(` continue: ${item.continue_work_command}`);
4668
- if (item.accept_command) console.log(` approve: ${item.accept_command}`);
4669
- else if (item.blocked_accept_reason) console.log(` approve: blocked (${item.blocked_accept_reason})`);
4670
- 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
+ }
4671
5557
  });
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
+ }
5564
+ }
4672
5565
  if (queue.counts.shown < queue.counts.certified) {
4673
5566
  console.log('');
4674
- 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.`);
4675
5568
  }
4676
5569
  }
4677
5570
 
@@ -4786,6 +5679,11 @@ function cmdAcceptGroup(args) {
4786
5679
  const isVerified = verifiedIds.has(task.id);
4787
5680
  const proof = String(task.review?.proof || task.metadata?.latest_agent_proof || '').trim()
4788
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
+ }
4789
5687
  const done = taskDb.doneTask(db, {
4790
5688
  id: task.id,
4791
5689
  status: 'done',
@@ -4867,19 +5765,21 @@ function formatTaskLine(task) {
4867
5765
 
4868
5766
  function cmdStatus(args) {
4869
5767
  const all = hasFlag(args, '--all');
5768
+ const everywhere = taskScopeEverywhere(args);
4870
5769
  const history = hasFlag(args, '--history');
4871
5770
  const taskDb = getTaskDb();
4872
5771
  const db = taskDb.open();
4873
- const compact = writeDefaultProjection(taskDb, db, { all });
5772
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
5773
+ const compact = writeDefaultProjection(taskDb, db, { all, everywhere });
4874
5774
  const projection = history
4875
5775
  ? enrichTaskProjection(taskDb.taskProjection(db, {
4876
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
4877
- limit: 500,
5776
+ workspaceRoot,
5777
+ limit: all ? null : 500,
4878
5778
  includeHistory: true,
4879
5779
  }))
4880
5780
  : compact.projection;
4881
5781
  const outPath = compact.outPath;
4882
- const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db, all ? null : taskDb.workspaceRoot());
5782
+ const hasExistingReviewFollowUp = buildReviewFollowUpChildPredicate(taskDb, db, workspaceRoot);
4883
5783
  const status = taskStatusSummary(projection, { history, hasExistingReviewFollowUp });
4884
5784
  if (wantsJson(args)) {
4885
5785
  printJson({
@@ -4936,13 +5836,13 @@ function requireTaskId(taskDb, db, ref, label) {
4936
5836
  }
4937
5837
  }
4938
5838
 
4939
- function workspaceRefRows(taskDb, db, all = false) {
4940
- return taskDb.listTasks(db, { workspaceRoot: all ? null : taskDb.workspaceRoot() });
5839
+ function workspaceRefRows(taskDb, db, options = {}) {
5840
+ return taskDb.listTasks(db, { workspaceRoot: scopedWorkspaceRoot(taskDb, [], options) });
4941
5841
  }
4942
5842
 
4943
5843
  function renderTaskDesk(rows, refRows = rows) {
4944
5844
  const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
4945
- const active = displayRows.filter(r => r.status !== 'done');
5845
+ const active = displayRows.filter(r => r.status !== 'done' && r.status !== 'archived');
4946
5846
  const done = displayRows.filter(r => r.status === 'done');
4947
5847
  if (rows.length === 0) {
4948
5848
  console.log('No tasks yet.');
@@ -5033,7 +5933,7 @@ function delegateHandoff(task, owner, via, tag) {
5033
5933
  return handoff;
5034
5934
  }
5035
5935
 
5036
- function cmdDelegate(args) {
5936
+ function delegateTask(args, options = {}) {
5037
5937
  const pos = positional(args);
5038
5938
  const title = pos.join(' ').trim();
5039
5939
  if (!title) {
@@ -5051,7 +5951,7 @@ function cmdDelegate(args) {
5051
5951
  const taskDb = getTaskDb();
5052
5952
  const db = taskDb.open();
5053
5953
  const ws = taskDb.workspaceRoot();
5054
- const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title);
5954
+ const operatorTitleWarning = warnIfTaskTitleNeedsOperatorWhy(title, { print: options.warnOperatorTitle !== false });
5055
5955
  const ownerResolution = resolveFunctionalTaskOwner({
5056
5956
  requestedOwner: requestedOwner && requestedOwner !== true ? requestedOwner : null,
5057
5957
  title,
@@ -5096,34 +5996,69 @@ function cmdDelegate(args) {
5096
5996
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
5097
5997
  const task = compactTaskFromProjection(projection, result.id);
5098
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);
5099
6019
  if (wantsJson(args)) {
5100
- printJson({
5101
- ok: true,
5102
- action: 'delegated',
5103
- task_id: result.id,
5104
- inserted: result.inserted !== false,
5105
- owner,
5106
- owner_resolution: ownerResolution,
5107
- executed_by: executedBy || null,
5108
- via,
5109
- handoff,
5110
- operator_title_warning: operatorTitleWarning,
5111
- projection_path: outPath,
5112
- task,
5113
- });
6020
+ printJson(payload);
5114
6021
  return;
5115
6022
  }
5116
- const tagText = tag && tag !== true ? ` #${tag}` : '';
5117
- console.log(`delegated ${taskRef(task)} -> ${owner}${tagText} via=${via}`);
5118
- if (executedBy) console.log(`executed_by: ${executedBy}`);
5119
- if (ownerResolution.proposed_member) console.log(`member: ${metadata.proposed_member_command}`);
5120
- console.log(`claim: ${handoff.command}`);
5121
- 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}`);
5122
6029
  }
5123
6030
 
5124
6031
  // Failed tasks older than this stop earning a daily owner-group row;
5125
6032
  // they collapse into one stale summary line instead (target state = clean day view).
5126
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
+ }
5127
6062
 
5128
6063
  function taskDayGroups(tasks, { now = Date.now() } = {}) {
5129
6064
  const active = tasks.filter(task => task.status !== 'done');
@@ -5158,9 +6093,11 @@ function taskDayGroups(tasks, { now = Date.now() } = {}) {
5158
6093
 
5159
6094
  function cmdDay(args) {
5160
6095
  const all = hasFlag(args, '--all');
6096
+ const full = hasFlag(args, '--full');
6097
+ const everywhere = taskScopeEverywhere(args);
5161
6098
  const taskDb = getTaskDb();
5162
6099
  const db = taskDb.open();
5163
- const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
6100
+ const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
5164
6101
  const { groups, staleFailed } = taskDayGroups(projection.tasks || []);
5165
6102
  const counts = {
5166
6103
  active: groups.reduce((sum, group) => sum + group.tasks.length, 0),
@@ -5187,24 +6124,30 @@ function cmdDay(args) {
5187
6124
  });
5188
6125
  return;
5189
6126
  }
5190
- console.log('TASK DAY');
6127
+ const textView = taskDayTextGroups(groups, { full });
6128
+ console.log('task day');
5191
6129
  const failedText = counts.failed > 0 ? ` / failed ${counts.failed}` : '';
5192
6130
  console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}${failedText}`);
5193
6131
  console.log('');
5194
6132
  if (!groups.length) {
5195
6133
  console.log('clear no active tasks');
5196
6134
  }
5197
- for (const group of groups) {
6135
+ for (const group of textView.groups) {
5198
6136
  console.log(`${group.owner}`);
5199
- for (const task of group.tasks.slice(0, 8)) {
6137
+ for (const task of group.tasks) {
5200
6138
  const tag = task.tag ? ` #${task.tag}` : '';
5201
6139
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
5202
- 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)}`);
5203
6141
  }
5204
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
+ }
5205
6148
  if (staleFailed.length) {
5206
6149
  console.log('');
5207
- 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`);
5208
6151
  }
5209
6152
  console.log('');
5210
6153
  console.log('add: atris task delegate "..." --to task-planner --tag tasks');
@@ -5212,13 +6155,15 @@ function cmdDay(args) {
5212
6155
 
5213
6156
  function cmdHome(args) {
5214
6157
  const all = hasFlag(args, '--all');
6158
+ const everywhere = taskScopeEverywhere(args);
5215
6159
  const taskDb = getTaskDb();
5216
6160
  const db = taskDb.open();
6161
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
5217
6162
  const rows = taskDb.listTasks(db, {
5218
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
5219
- limit: 200,
6163
+ workspaceRoot,
6164
+ limit: all ? null : 200,
5220
6165
  });
5221
- const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
6166
+ const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
5222
6167
  if (wantsJson(args)) {
5223
6168
  printJson({
5224
6169
  ok: true,
@@ -5235,18 +6180,20 @@ function cmdHome(args) {
5235
6180
 
5236
6181
  function cmdList(args) {
5237
6182
  const all = hasFlag(args, '--all');
6183
+ const everywhere = taskScopeEverywhere(args);
5238
6184
  const status = flag(args, '--status');
5239
6185
  const scope = taskQueueScopeFromArgs(args);
5240
6186
  const scoped = !taskQueueScopeIsEmpty(scope);
5241
6187
  const taskDb = getTaskDb();
5242
6188
  const db = taskDb.open();
6189
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
5243
6190
  const rawRows = taskDb.listTasks(db, {
5244
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
6191
+ workspaceRoot,
5245
6192
  status: typeof status === 'string' ? status : null,
5246
- limit: scoped ? null : 200,
6193
+ limit: scoped || all ? null : 200,
5247
6194
  });
5248
6195
  const rows = filterTasksByScope(rawRows, scope);
5249
- const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, all));
6196
+ const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, { everywhere }));
5250
6197
  if (wantsJson(args)) {
5251
6198
  printJson({ ok: true, action: 'list', scope: normalizeTaskQueueScope(scope), tasks: displayRows });
5252
6199
  return;
@@ -5263,12 +6210,49 @@ function cmdList(args) {
5263
6210
  }
5264
6211
  }
5265
6212
 
6213
+ // judge != worker support: reserved system names (autoland-verifier and co)
6214
+ // can never be assumed via --as, and unknown names warn by default or fail
6215
+ // under ATRIS_ACTOR_VALIDATION=enforce. Only explicit --as values are
6216
+ // checked; the DEFAULT_OWNER fallback stays silent.
6217
+ function guardExplicitActor(command, value) {
6218
+ if (typeof value !== 'string' || !value.trim()) return;
6219
+ const check = reviewIntegrity.validateActor(value, { root: process.cwd() });
6220
+ if (!check.ok) {
6221
+ if (check.reason === 'reserved_actor') {
6222
+ console.error(`${command}: reserved_actor: '${value}' is a system actor and cannot be used with --as`);
6223
+ } else {
6224
+ console.error(`${command}: actor_not_on_roster: '${value}' is not a workspace member or engine (actor validation is enforced)`);
6225
+ }
6226
+ process.exit(1);
6227
+ }
6228
+ if (check.reason === 'actor_not_on_roster' && check.mode === 'warn') {
6229
+ console.error(`Warning: '${value}' is not a workspace member or engine; reviews under unknown names weaken the audit trail.`);
6230
+ }
6231
+ }
6232
+
6233
+ // A claim against an already-done task burns a whole dispatch when a
6234
+ // rendered view (atris/TODO.md) is stale: the agent claims, builds, then
6235
+ // discovers the work was already done. Point straight at a real open task
6236
+ // instead of just reporting the failure, straight from the live projection
6237
+ // (never the rendered file), preferring the same tag when one is open.
6238
+ function suggestNextClaimableTask(projection, { excludeId = null, tag = '' } = {}) {
6239
+ const open = (projection && projection.tasks || []).filter((t) => t && t.status === 'open' && t.id !== excludeId);
6240
+ if (!open.length) return null;
6241
+ const normalizedTag = String(tag || '').trim().toLowerCase();
6242
+ if (normalizedTag) {
6243
+ const sameTag = open.find((t) => String(t.tag || '').trim().toLowerCase() === normalizedTag);
6244
+ if (sameTag) return sameTag;
6245
+ }
6246
+ return open[0];
6247
+ }
6248
+
5266
6249
  function cmdClaim(args) {
5267
6250
  const pos = positional(args);
5268
6251
  const id = pos[0];
5269
6252
  if (!id) {
5270
6253
  failTask('atris task claim', 'missing_id', 'id required');
5271
6254
  }
6255
+ guardExplicitActor('atris task claim', flag(args, '--as'));
5272
6256
  const owner = flag(args, '--as') || DEFAULT_OWNER;
5273
6257
  const taskDb = getTaskDb();
5274
6258
  const db = taskDb.open();
@@ -5287,11 +6271,21 @@ function cmdClaim(args) {
5287
6271
  });
5288
6272
  return;
5289
6273
  }
5290
- 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');
5291
6279
  } else {
5292
6280
  const recoveryCommand = result.reason === 'already_claimed' && result.claimed_by
5293
6281
  ? `atris task release ${id} --as ${result.claimed_by}`
5294
6282
  : null;
6283
+ let nextClaimable = null;
6284
+ if (result.reason === 'already_done') {
6285
+ const doneRow = taskDb.getTask(db, taskId);
6286
+ const { projection } = writeDefaultProjection(taskDb, db);
6287
+ nextClaimable = suggestNextClaimableTask(projection, { excludeId: taskId, tag: doneRow && doneRow.tag });
6288
+ }
5295
6289
  if (wantsJson(args)) {
5296
6290
  printJson({
5297
6291
  ok: false,
@@ -5299,12 +6293,16 @@ function cmdClaim(args) {
5299
6293
  reason: result.reason,
5300
6294
  claimed_by: result.claimed_by || null,
5301
6295
  recovery_command: recoveryCommand,
6296
+ next_claimable: nextClaimable ? { id: nextClaimable.id, ref: taskRef(nextClaimable), tag: nextClaimable.tag || null, title: nextClaimable.title } : null,
5302
6297
  detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
5303
6298
  });
5304
6299
  process.exit(1);
5305
6300
  }
5306
6301
  console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
5307
6302
  if (recoveryCommand) console.error(`Recovery: ${recoveryCommand}`);
6303
+ if (nextClaimable) {
6304
+ console.error(`next claimable: ${taskRef(nextClaimable)} ${String(nextClaimable.title || '').slice(0, 80)} (atris task claim ${taskRef(nextClaimable)} --as ${owner})`);
6305
+ }
5308
6306
  process.exit(1);
5309
6307
  }
5310
6308
  }
@@ -5503,7 +6501,7 @@ function cmdNext(args) {
5503
6501
  const continueWorkCommand = handoff.next_action === 'continue_work'
5504
6502
  ? continueWorkCommandForTask(reviewTask, { owner })
5505
6503
  : null;
5506
- const nextAgentAction = handoff.next_action === 'human_accept_waiting'
6504
+ const nextAgentAction = handoff.next_action === 'human_accept_waiting' && !scoped
5507
6505
  ? readEndgameAgentAction(taskDb.workspaceRoot(), owner, { tasks: projection.tasks || [] })
5508
6506
  : null;
5509
6507
  if (hasFlag(args, '--create-next')) {
@@ -5724,6 +6722,131 @@ function cmdNote(args) {
5724
6722
  console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
5725
6723
  }
5726
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
+
6777
+ // Collect EVERY value for a repeatable flag (flag() only returns the first),
6778
+ // so `--add a --add b` and `--add a,b` both work.
6779
+ function collectFlagValues(args, name) {
6780
+ const values = [];
6781
+ for (let i = 0; i < args.length; i += 1) {
6782
+ if (args[i] === name && i + 1 < args.length && !String(args[i + 1]).startsWith('--')) {
6783
+ values.push(args[i + 1]);
6784
+ }
6785
+ }
6786
+ return values
6787
+ .flatMap((value) => String(value).split(','))
6788
+ .map((value) => value.trim())
6789
+ .filter(Boolean);
6790
+ }
6791
+
6792
+ function cmdTag(args) {
6793
+ const pos = positional(args);
6794
+ const id = pos[0];
6795
+ if (!id) failTask('atris task tag', 'missing_id', 'task id required');
6796
+ const add = collectFlagValues(args, '--add');
6797
+ const remove = collectFlagValues(args, '--remove');
6798
+ if (!add.length && !remove.length) {
6799
+ failTask('atris task tag', 'missing_tags', 'at least one --add <tag> or --remove <tag> required');
6800
+ }
6801
+ const actor = flag(args, '--as') || DEFAULT_OWNER;
6802
+ const taskDb = getTaskDb();
6803
+ const db = taskDb.open();
6804
+ const taskId = requireTaskId(taskDb, db, id, 'atris task tag');
6805
+ const result = taskDb.tagTask(db, { id: taskId, actor: String(actor), add, remove });
6806
+ if (!result.tagged) {
6807
+ if (result.reason === 'no_changes') {
6808
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
6809
+ const task = compactTaskFromProjection(projection, taskId);
6810
+ if (wantsJson(args)) {
6811
+ printJson({
6812
+ ok: true,
6813
+ action: 'unchanged',
6814
+ task_id: taskId,
6815
+ added: [],
6816
+ removed: [],
6817
+ tags: result.tags || [],
6818
+ projection_path: outPath,
6819
+ task,
6820
+ });
6821
+ return;
6822
+ }
6823
+ console.log(`no change ${taskRef(task)} tags [${(result.tags || []).join(', ')}]`);
6824
+ return;
6825
+ }
6826
+ failTask('atris task tag', result.reason || 'tag_failed', `tag failed: ${result.reason || 'unknown'}`, 1);
6827
+ }
6828
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
6829
+ const task = compactTaskFromProjection(projection, taskId);
6830
+ if (wantsJson(args)) {
6831
+ printJson({
6832
+ ok: true,
6833
+ action: 'tagged',
6834
+ task_id: taskId,
6835
+ added: result.added,
6836
+ removed: result.removed,
6837
+ tags: result.tags,
6838
+ version: result.event.version,
6839
+ projection_path: outPath,
6840
+ task,
6841
+ });
6842
+ return;
6843
+ }
6844
+ const parts = [];
6845
+ if (result.added.length) parts.push(`+${result.added.join(' +')}`);
6846
+ if (result.removed.length) parts.push(`-${result.removed.join(' -')}`);
6847
+ console.log(`tagged ${taskRef(task)} ${parts.join(' ')} -> [${result.tags.join(', ')}] v${result.event.version}`);
6848
+ }
6849
+
5727
6850
  function cmdChat(args) {
5728
6851
  const pos = positional(args);
5729
6852
  const id = pos[0];
@@ -6014,7 +7137,7 @@ function cmdShow(args) {
6014
7137
  const owner = task.claimed_by ? ` / ${task.claimed_by}` : '';
6015
7138
  const tag = task.tag ? ` #${task.tag}` : '';
6016
7139
  const statusLabel = task.status === 'review'
6017
- ? 'READY FOR APPROVAL'
7140
+ ? 'ready for approval'
6018
7141
  : task.status === 'done'
6019
7142
  ? 'DONE'
6020
7143
  : task.status.toUpperCase();
@@ -6774,9 +7897,38 @@ function cmdResult(args) {
6774
7897
  const pos = positional(args);
6775
7898
  const id = pos[0];
6776
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
+ }
6777
7929
  const fields = {
6778
7930
  purpose: textFlag(args, ['--purpose', '--goal', '--objective']),
6779
- changed: textFlag(args, ['--changed', '--result', '--done']),
7931
+ changed: textFlag(args, ['--changed', '--done']),
6780
7932
  checked: textFlag(args, ['--checked', '--check', '--verified']),
6781
7933
  passed: textFlag(args, ['--passed', '--pass']),
6782
7934
  failed: textFlag(args, ['--failed', '--fail']),
@@ -6886,7 +8038,7 @@ function taskPageActions(task, { reviewer = 'codex-review', hasExistingReviewFol
6886
8038
  note_command: `atris task note ${ref} "<context>" --as ${owner}`,
6887
8039
  plan_command: `atris task plan ${ref} --goal ${taskCommandQuote(goal)} --exit "<exit condition>" --proof-needed "<verification command>" --first-move "<first move>"`,
6888
8040
  do_command: `atris task do ${ref} --as ${owner} --first-move "<first move>"`,
6889
- 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>"`,
6890
8042
  review_command: `atris task review ${ref} --reward 0 --as ${actor} --proof "<specific proof command/result>" --verify "<safe verifier command>"`,
6891
8043
  };
6892
8044
  if (task && task.status === 'review') {
@@ -7322,9 +8474,14 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7322
8474
  if (proofIssue) {
7323
8475
  throw taskStepError(proof ? 'weak_proof' : 'proof_required', `meaningful proof required: ${proofIssue}`, { status: 400, exitCode: 2, page: actionPage });
7324
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
+ }
7325
8481
  const lesson = String(options.lesson || '');
7326
8482
  const nextTask = String(options.nextTask || '');
7327
8483
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, { actor, proof });
8484
+ const missionResult = missionReceiptResultForProof(task, proof, task.workspace_root || process.cwd());
7328
8485
  const ready = taskDb.readyTask(db, {
7329
8486
  id: taskId,
7330
8487
  actor,
@@ -7332,6 +8489,8 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7332
8489
  lesson,
7333
8490
  nextTask,
7334
8491
  resultTrace: resultTrace && resultTrace.trace,
8492
+ result: missionResult ? missionResult.changed : undefined,
8493
+ reason: missionResult ? missionResult.reason : undefined,
7335
8494
  });
7336
8495
  if (!ready.ready) taskStepFailure('atris task step', ready, actionPage);
7337
8496
  task = taskDetail(taskDb, db, taskId) || task;
@@ -7341,7 +8500,10 @@ function runTaskStep(taskDb, db, taskId, options = {}) {
7341
8500
  } else if (current === 'review' && task.status === 'review') {
7342
8501
  const handoffState = reviewHandoffForTask(task, { suppressExistingFollowUp: true });
7343
8502
  if (handoffState && handoffState.next_action === PROOF_BOUNDARY_BLOCKED_ACTION) {
7344
- 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 });
7345
8507
  }
7346
8508
  if (handoffState && (handoffState.next_action === 'continue_work' || handoffState.next_action === 'human_accept_waiting')) {
7347
8509
  const reason = handoffState.next_action === 'continue_work'
@@ -7466,9 +8628,13 @@ function runCurrentTaskStep(taskDb, db, { owner = DEFAULT_OWNER, reviewer = 'cod
7466
8628
  throw error;
7467
8629
  }
7468
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';
7469
8635
  const error = taskStepError(
7470
8636
  PROOF_BOUNDARY_BLOCKED_REASON,
7471
- 'atris task current-step: selected Review row has stale/open/draft/unmerged PR proof; revise it instead of accepting or auto-stepping',
8637
+ detail,
7472
8638
  {
7473
8639
  status: 409,
7474
8640
  exitCode: 1,
@@ -7675,7 +8841,7 @@ function cmdDone(args) {
7675
8841
  if (agentProofOnlyMode() && !failed) {
7676
8842
  failAgentProofOnly(
7677
8843
  'atris task done',
7678
- '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 "..."`.',
7679
8845
  );
7680
8846
  }
7681
8847
  const canComplete = beforeTask && (beforeTask.status === 'open' || beforeTask.status === 'claimed');
@@ -7757,13 +8923,14 @@ function cmdFinish(args) {
7757
8923
  if (agentProofOnlyMode() && !failed) {
7758
8924
  failAgentProofOnly(
7759
8925
  'atris task finish',
7760
- '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 "..."`.',
7761
8927
  );
7762
8928
  }
7763
8929
  const canComplete = currentTask && (currentTask.status === 'open' || currentTask.status === 'claimed');
7764
8930
  if (canComplete) {
7765
8931
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task finish', proof);
7766
8932
  else if (proof) requireMeaningfulTaskProof('atris task finish', proof);
8933
+ if (!failed && hasReview) requireExplicitLandingDayOnePm('atris task finish', landing, currentTask.title);
7767
8934
  }
7768
8935
  const done = taskDb.doneTask(db, {
7769
8936
  id: taskId,
@@ -7839,6 +9006,197 @@ function cmdFinish(args) {
7839
9006
  console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))}`);
7840
9007
  }
7841
9008
 
9009
+ // Distinct from `done --failed`: a bulk sweep of duplicates/off-roadmap work
9010
+ // closes the task without ever claiming it did or didn't succeed. Writing
9011
+ // 'failed' here would corrupt the reward signal readers rely on (see
9012
+ // atris/reports/failed-tasks-analysis-2026-07-03.md, cluster 2, OBL-1622).
9013
+ function cmdArchive(args) {
9014
+ const pos = positional(args);
9015
+ const id = pos[0];
9016
+ if (!id) {
9017
+ failTask('atris task archive', 'missing_id', 'id required');
9018
+ }
9019
+ const reason = textFlag(args, ['--reason']);
9020
+ if (!reason) {
9021
+ failTask('atris task archive', 'missing_reason', 'atris task archive requires --reason "<why this is being swept, not failed>"');
9022
+ }
9023
+ const taskDb = getTaskDb();
9024
+ const db = taskDb.open();
9025
+ const taskId = requireTaskId(taskDb, db, id, 'atris task archive');
9026
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
9027
+ // Explicit opt-in for sanctioned failed→archived cleanup (e.g. duplicate
9028
+ // loop-tick orphans fail-closed before 'archived' existed). Without the
9029
+ // flag, failed rows stay failed; done rows are never archivable.
9030
+ const fromFailed = hasFlag(args, '--from-failed');
9031
+ const result = taskDb.archiveTask(db, { id: taskId, actor, reason, fromFailed });
9032
+ if (result.archived) {
9033
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
9034
+ if (wantsJson(args)) {
9035
+ printJson({
9036
+ ok: true,
9037
+ action: 'archived',
9038
+ task_id: taskId,
9039
+ reason,
9040
+ archived_from: result.row && result.row.metadata && result.row.metadata.archived_from || null,
9041
+ projection_path: outPath,
9042
+ task: compactTaskFromProjection(projection, taskId),
9043
+ });
9044
+ return;
9045
+ }
9046
+ const fromNote = result.row && result.row.metadata && result.row.metadata.archived_from
9047
+ ? ` (was ${result.row.metadata.archived_from})`
9048
+ : '';
9049
+ console.log(`archived ${taskRef(compactTaskFromProjection(projection, taskId))}${fromNote}: ${reason}`);
9050
+ } else {
9051
+ const hint = result.reason === 'already_failed'
9052
+ ? ' (use --from-failed to archive a fail-closed duplicate/off-roadmap row)'
9053
+ : '';
9054
+ const detail = `archive failed: ${taskId} ${result.reason}${hint}`;
9055
+ if (wantsJson(args)) {
9056
+ printJson({ ok: false, command: 'atris task archive', reason: result.reason, task_id: taskId, detail });
9057
+ process.exit(1);
9058
+ }
9059
+ console.error(detail);
9060
+ process.exit(1);
9061
+ }
9062
+ }
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
+
9144
+ // One-time migration for OBL-1622: the 2026-06-10 "first-principles backlog
9145
+ // reset" archived ~125 certified, proof-backed tasks by writing status
9146
+ // 'failed' (no distinct archived status existed yet). This relabels exactly
9147
+ // the rows that carry that reset's metadata marker, using the same
9148
+ // UPDATE+appendTaskEvent write path as every other status transition in
9149
+ // lib/task-db.js — never a raw projection-JSON edit.
9150
+ function cmdRelabelArchived(args) {
9151
+ const apply = hasFlag(args, '--apply');
9152
+ const taskDb = getTaskDb();
9153
+ const db = taskDb.open();
9154
+ const workspaceRoot = taskDb.workspaceRoot();
9155
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
9156
+ const result = taskDb.relabelArchivedTasks(db, { workspaceRoot, apply, actor });
9157
+ if (apply && result.count > 0) {
9158
+ appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count: result.count, ids: result.ids });
9159
+ }
9160
+ if (wantsJson(args)) {
9161
+ printJson({
9162
+ ok: true,
9163
+ action: apply ? 'relabeled' : 'preview',
9164
+ dry_run: !apply,
9165
+ workspace_root: workspaceRoot,
9166
+ ...result,
9167
+ });
9168
+ return;
9169
+ }
9170
+ if (!apply) {
9171
+ console.log(`relabel-archived (dry-run): ${result.count} failed task(s) match the June 10 backlog-reset marker.`);
9172
+ for (const s of result.sample) console.log(` - ${s.id} ${s.title}`);
9173
+ if (result.count > result.sample.length) console.log(` ...and ${result.count - result.sample.length} more`);
9174
+ console.log('Run with --apply --as <you> to relabel these failed -> archived.');
9175
+ return;
9176
+ }
9177
+ console.log(`relabeled ${result.count} task(s) failed -> archived (June 10 backlog reset, OBL-1622).`);
9178
+ }
9179
+
9180
+ function appendRelabelArchivedJournalReceipt(workspaceRoot, { actor, count, ids }) {
9181
+ if (!workspaceRoot || !fs.existsSync(path.join(workspaceRoot, 'atris'))) return null;
9182
+ const now = new Date();
9183
+ const logName = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}.md`;
9184
+ const stamp = now.toTimeString().slice(0, 5);
9185
+ const projectDir = path.join(workspaceRoot, 'atris', 'logs', logName.slice(0, 4));
9186
+ fs.mkdirSync(projectDir, { recursive: true });
9187
+ const logPath = path.join(projectDir, logName);
9188
+ const idPreview = ids.slice(0, 10).join(', ') + (ids.length > 10 ? `, ...(+${ids.length - 10} more)` : '');
9189
+ fs.appendFileSync(logPath, [
9190
+ `## ${stamp} · Task relabel: failed -> archived (OBL-1622)`,
9191
+ `- count: ${count}`,
9192
+ `- reason: June 10 backlog-reset rows mislabeled failed; relabeled to archived`,
9193
+ `- actor: ${actor}`,
9194
+ `- ids: ${idPreview}`,
9195
+ '',
9196
+ ].join('\n'), 'utf8');
9197
+ return logPath;
9198
+ }
9199
+
7842
9200
  function cmdReady(args) {
7843
9201
  const pos = positional(args);
7844
9202
  const id = pos[0];
@@ -7851,18 +9209,27 @@ function cmdReady(args) {
7851
9209
  // turning a claim into executed evidence. --verify can carry an optional --proof note.
7852
9210
  const proofFlag = flag(args, '--proof');
7853
9211
  const verifyFlag = flag(args, '--verify');
9212
+ const resultSentence = requireResultSentence('atris task ready', textFlag(args, ['--result']), { ready: true });
9213
+ const usedVerify = typeof verifyFlag === 'string' ? verifyFlag.trim() : '';
7854
9214
  let proof = typeof proofFlag === 'string' ? proofFlag : '';
7855
- if (typeof verifyFlag === 'string' && verifyFlag.trim()) {
7856
- const verified = buildVerifiedProof(verifyFlag, proof, undefined, { cwd: process.cwd() });
7857
- if (!verified.ok) {
7858
- const detail = verified.exit != null ? ` (exit ${verified.exit})` : (verified.signal ? ` (signal ${verified.signal})` : '');
9215
+ const verifyAutoCertifyAllowed = !usedVerify || isAutoCertifyVerifyCommandAllowed(verifyFlag);
9216
+ if (usedVerify) {
9217
+ // Run the verifier once and write a receipt (pass or fail) so the review
9218
+ // gate in lib/receipt-evidence.js can validate the exact path named in
9219
+ // the proof, not just trust the prose.
9220
+ const { writeTaskReceipt } = require('../lib/task-receipt');
9221
+ const receipt = writeTaskReceipt({ taskId: id, command: verifyFlag, root: process.cwd() });
9222
+ if (!receipt.passed) {
9223
+ const detail = receipt.exit != null ? ` (exit ${receipt.exit})` : (receipt.signal ? ` (signal ${receipt.signal})` : '');
7859
9224
  console.error(`atris task ready: verifier failed${detail}: ${verifyFlag}`);
7860
- if (verified.output) console.error(verified.output);
7861
- else if (verified.error) console.error(verified.error);
9225
+ if (receipt.output) console.error(receipt.output);
9226
+ else if (receipt.error) console.error(receipt.error);
9227
+ if (receipt.receiptPath) console.error(`receipt: ${receipt.receiptPath}`);
7862
9228
  process.exit(1);
7863
9229
  }
7864
- proof = verified.proof;
7865
- if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0`);
9230
+ const base = proof.trim();
9231
+ proof = `[verified] \`${verifyFlag}\` passed (exit 0)${base ? ` — ${base}` : ''}${receipt.output ? `\n${receipt.output}` : ''}\nReceipt: ${receipt.receiptPath}`;
9232
+ if (!wantsJson(args)) console.log(`✓ verified: \`${verifyFlag}\` exited 0 (receipt ${receipt.receiptPath})`);
7866
9233
  }
7867
9234
  if (!proof) {
7868
9235
  console.error('atris task ready: --proof or --verify required');
@@ -7872,9 +9239,10 @@ function cmdReady(args) {
7872
9239
  const lesson = flag(args, '--lesson') || '';
7873
9240
  const nextTaskInput = normalizeReviewNextTaskInput(typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '');
7874
9241
  const landing = landingFlags(args);
9242
+ guardExplicitActor('atris task ready', flag(args, '--as'));
7875
9243
  const actor = String(flag(args, '--as') || DEFAULT_OWNER);
7876
9244
  const resultFields = {
7877
- changed: textFlag(args, ['--changed', '--result', '--done']),
9245
+ changed: textFlag(args, ['--changed', '--done']),
7878
9246
  checked: textFlag(args, ['--checked', '--check', '--verified']),
7879
9247
  passed: textFlag(args, ['--passed', '--pass']),
7880
9248
  failed: textFlag(args, ['--failed', '--fail']),
@@ -7888,10 +9256,17 @@ function cmdReady(args) {
7888
9256
  const taskDb = getTaskDb();
7889
9257
  const db = taskDb.open();
7890
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
+ }
7891
9265
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
7892
9266
  actor,
7893
9267
  proof: String(proof),
7894
9268
  ...resultFields,
9269
+ changed: resultFields.changed || resultSentence,
7895
9270
  });
7896
9271
  const result = taskDb.readyTask(db, {
7897
9272
  id: taskId,
@@ -7901,11 +9276,17 @@ function cmdReady(args) {
7901
9276
  nextTask: nextTaskInput.nextTask,
7902
9277
  resultTrace: resultTrace && resultTrace.trace,
7903
9278
  landing,
9279
+ result: resultSentence,
7904
9280
  });
7905
9281
  if (!result.ready) {
7906
9282
  console.error(`ready failed: ${result.reason}`);
7907
9283
  process.exit(1);
7908
9284
  }
9285
+ // Store the exact verifier command on the task itself (not just baked into
9286
+ // proof prose) so sweep --auto-accept and certify-verified can re-run it
9287
+ // live against the current checkout later, instead of re-deriving it from
9288
+ // text or trusting a receipt file that may no longer exist.
9289
+ if (usedVerify) stampReadyVerifyMetadata(taskDb, db, taskId, usedVerify);
7909
9290
  const landingAdvisory = warnIfLandingNeedsDayOnePm(landing, result.row && result.row.title);
7910
9291
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
7911
9292
  const agentCertified = result.event.payload.agent_certified === true;
@@ -7918,13 +9299,21 @@ function cmdReady(args) {
7918
9299
  nextTask: nextTaskInput.nextTask,
7919
9300
  });
7920
9301
  const reviewChat = taskReviewChatHandoff(verifierTask, { reviewer: 'codex-review' });
9302
+ const autolandOn = require('../lib/autoland').liveAcceptAuthorization(taskDb.workspaceRoot()).ok;
9303
+ const needsExternalVerifier = Boolean(usedVerify && !verifyAutoCertifyAllowed);
7921
9304
  const handoff = {
7922
9305
  native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
7923
9306
  career_xp_status: 'pending_human_accept',
7924
9307
  next_action: agentCertified ? certifiedReviewNextAction(nextTaskInput.nextTask) : 'agent_review_again',
7925
- rule: agentCertified
7926
- ? 'Double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
7927
- : 'Proof is ready; one more agent check before human approval. XP waits for the human.',
9308
+ rule: autolandOn
9309
+ ? (agentCertified
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.'
9313
+ : 'proof is ready; autoland runs the second check and lands it on the next tick.')
9314
+ : (agentCertified
9315
+ ? 'double-check complete; ready to keep moving. XP is awarded only after the human approves the task.'
9316
+ : 'proof is ready; one more agent check before human approval. XP waits for the human.'),
7928
9317
  };
7929
9318
  if (reviewChat) {
7930
9319
  handoff.review_chat_command = reviewChat.command;
@@ -7958,11 +9347,59 @@ function cmdReady(args) {
7958
9347
  console.log(`ready for approval ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
7959
9348
  if (resultTrace) console.log('Result trace recorded.');
7960
9349
  console.log(handoff.rule);
9350
+ if (!verifyAutoCertifyAllowed) {
9351
+ const ref = taskRef(compactTaskFromProjection(projection, taskId) || result.row || taskId);
9352
+ console.log(`note: this verify command is outside the auto-certify allowlist, so autoland cannot run the second check itself. use a test command like node --test <file> or git diff --check, or have a second agent run: atris task review-chat ${ref} --as <reviewer>`);
9353
+ }
7961
9354
  for (const hint of policyHints) {
7962
9355
  console.log(`policy (${hint.id}): ${hint.hint}`);
7963
9356
  }
7964
9357
  }
7965
9358
 
9359
+ // Standalone receipt writer: runs a verifier for a task and writes atris/runs/
9360
+ // evidence without moving the task to ready. Useful when you want a receipt
9361
+ // on record before or independent of a ready call, or to record a failed
9362
+ // verifier run for the audit trail. `atris task ready --verify` calls the
9363
+ // same writer inline and folds the resulting path into the proof.
9364
+ function cmdTaskReceipt(args) {
9365
+ const pos = positional(args);
9366
+ const id = pos[0];
9367
+ if (!id) {
9368
+ console.error('atris task receipt: id required');
9369
+ process.exit(2);
9370
+ }
9371
+ const verifyFlag = flag(args, '--verify');
9372
+ if (typeof verifyFlag !== 'string' || !verifyFlag.trim()) {
9373
+ console.error('atris task receipt: --verify "<cmd>" required');
9374
+ process.exit(2);
9375
+ }
9376
+ const taskDb = getTaskDb();
9377
+ const db = taskDb.open();
9378
+ const taskId = requireTaskId(taskDb, db, id, 'atris task receipt');
9379
+ const { writeTaskReceipt } = require('../lib/task-receipt');
9380
+ const receipt = writeTaskReceipt({ taskId, command: verifyFlag, root: process.cwd() });
9381
+ if (wantsJson(args)) {
9382
+ printJson({
9383
+ ok: receipt.passed,
9384
+ task_id: taskId,
9385
+ command: verifyFlag,
9386
+ receipt_path: receipt.receiptPath,
9387
+ exit: receipt.exit,
9388
+ passed: receipt.passed,
9389
+ });
9390
+ if (!receipt.passed) process.exit(1);
9391
+ return;
9392
+ }
9393
+ if (receipt.passed) {
9394
+ console.log(`receipt written: ${receipt.receiptPath} (exit 0)`);
9395
+ console.log(`use: atris task ready ${taskId} --proof "Receipt: ${receipt.receiptPath}" --result "<what someone can do now and why it matters>"`);
9396
+ } else {
9397
+ console.error(`verifier failed (exit ${receipt.exit}); receipt written: ${receipt.receiptPath}`);
9398
+ if (receipt.output) console.error(receipt.output);
9399
+ process.exit(1);
9400
+ }
9401
+ }
9402
+
7966
9403
  async function cmdAccept(args) {
7967
9404
  const pos = positional(args);
7968
9405
  const id = pos[0];
@@ -7995,6 +9432,10 @@ async function cmdAccept(args) {
7995
9432
  process.exit(2);
7996
9433
  }
7997
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
+ }
7998
9439
  const readyReview = beforeTask?.review || {};
7999
9440
  const clearLesson = hasEmptyFlagValue(args, '--lesson');
8000
9441
  const clearNextTask = hasEmptyFlagValue(args, '--next');
@@ -8041,6 +9482,7 @@ async function cmdAccept(args) {
8041
9482
  const xpProjection = refreshCareerXpAfterReview(reviewed);
8042
9483
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
8043
9484
  const workspaceRoot = projection.workspace_root || process.cwd();
9485
+ refreshExistingTodoMarkdown(taskDb, db, workspaceRoot);
8044
9486
  const brainScorecards = refreshBrainScorecardsAfterAccept(workspaceRoot);
8045
9487
  const nextMissionRoute = nextMissionRouteAfterAccept(workspaceRoot);
8046
9488
  // Inform the gate, never block it: show what the receipts named in the proof
@@ -8093,7 +9535,12 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
8093
9535
  `).run(JSON.stringify(metadata), Date.now(), taskId);
8094
9536
  }
8095
9537
 
8096
- function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '' }) {
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
+ }
8097
9544
  const done = taskDb.doneTask(db, {
8098
9545
  id: taskId,
8099
9546
  status: 'done',
@@ -8101,6 +9548,7 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
8101
9548
  allowReview: true,
8102
9549
  action: 'accepted',
8103
9550
  proof,
9551
+ autoAccepted,
8104
9552
  });
8105
9553
  if (!done.updated) {
8106
9554
  return { ok: false, reason: 'not_open_claimed_or_review' };
@@ -8113,7 +9561,10 @@ function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '
8113
9561
  nextTask,
8114
9562
  proof,
8115
9563
  careerXpEligible: true,
9564
+ autoAccepted,
8116
9565
  });
9566
+ const acceptedRow = taskDb.getTask(db, taskId);
9567
+ refreshExistingTodoMarkdown(taskDb, db, acceptedRow && acceptedRow.workspace_root);
8117
9568
  return { ok: true, reviewed };
8118
9569
  }
8119
9570
 
@@ -8145,6 +9596,7 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
8145
9596
  metadata.verify = metadata.verify || verify;
8146
9597
  metadata.certified_verified_at = new Date().toISOString();
8147
9598
  metadata.certified_verified_by = actor;
9599
+ metadata.machine_verified = true;
8148
9600
  db.prepare(`
8149
9601
  UPDATE tasks
8150
9602
  SET metadata = ?,
@@ -8153,9 +9605,69 @@ function stampCertifyVerifyMetadata(taskDb, db, taskId, actor, verify) {
8153
9605
  `).run(JSON.stringify(metadata), Date.now(), taskId);
8154
9606
  }
8155
9607
 
8156
- function cmdCertifyVerified(args) {
9608
+ // `atris task ready --verify "<cmd>"` already runs the command live and
9609
+ // gates on exit 0, but that alone leaves no machine re-runnable trace once
9610
+ // the proof text scrolls out of easy reach: storing the exact command on
9611
+ // metadata.verify is what lets sweep --auto-accept (and certify-verified)
9612
+ // re-run it later against the current checkout instead of re-parsing prose.
9613
+ function stampReadyVerifyMetadata(taskDb, db, taskId, verify) {
9614
+ const row = taskDb.getTask(db, taskId);
9615
+ if (!row) return;
9616
+ const metadata = row.metadata && typeof row.metadata === 'object' ? { ...row.metadata } : {};
9617
+ metadata.verify = verify;
9618
+ db.prepare(`
9619
+ UPDATE tasks
9620
+ SET metadata = ?,
9621
+ updated_at = ?
9622
+ WHERE id = ?
9623
+ `).run(JSON.stringify(metadata), Date.now(), taskId);
9624
+ }
9625
+
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 = {}) {
8157
9667
  const dryRun = hasFlag(args, '--dry-run');
8158
9668
  const asJson = wantsJson(args);
9669
+ const silent = options.silent === true;
9670
+ const verifyCache = options.verifyCache || null;
8159
9671
  const actor = String(flag(args, '--as') || 'autoland-verifier');
8160
9672
  const limitRaw = flag(args, '--limit');
8161
9673
  const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 6) : 6;
@@ -8192,20 +9704,37 @@ function cmdCertifyVerified(args) {
8192
9704
  results.push({ ref, action: 'skipped', reason: `denied_tag_${tag}` });
8193
9705
  continue;
8194
9706
  }
9707
+ const proofBoundary = proofBoundaryBlockedEvaluation(task);
9708
+ if (proofBoundary) {
9709
+ results.push({ ref, action: 'skipped', reason: proofBoundary.reason });
9710
+ continue;
9711
+ }
8195
9712
  // Skip only rows the accept lane can already land, or rows blocked by
8196
9713
  // something an executed second-actor check cannot cure. A row with two
8197
9714
  // passes from ONE actor is exactly what this command exists to cure —
8198
9715
  // "certified" alone is not landable.
9716
+ // strictVerify stays off in this eligibility probe: certify-verified runs
9717
+ // the check itself right below, and strict mode here would execute it a
9718
+ // second time per row before the real run.
8199
9719
  const evaluation = evaluateAutoAccept(task, { strictVerify: false });
8200
9720
  if (evaluation.eligible) {
8201
9721
  results.push({ ref, action: 'skipped', reason: 'already_landable' });
8202
9722
  continue;
8203
9723
  }
8204
- const curable = ['not_agent_certified', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes'];
9724
+ // proof_not_executed is curable by definition: this command re-runs the
9725
+ // named check and replaces the free-text claim with executed evidence.
9726
+ const curable = ['not_agent_certified', 'needs_independent_reviewer', 'needs_second_reviewer_or_third_pass', 'insufficient_review_passes', 'proof_not_executed'];
8205
9727
  if (!curable.includes(evaluation.reason)) {
8206
9728
  results.push({ ref, action: 'skipped', reason: evaluation.reason });
8207
9729
  continue;
8208
9730
  }
9731
+ // judge != worker: the re-run only counts as an independent check when
9732
+ // its actor is not the builder of the row it is judging.
9733
+ const builder = reviewIntegrity.taskBuilder(task);
9734
+ if (builder && reviewIntegrity.normalizeActor(actor) === builder) {
9735
+ results.push({ ref, action: 'skipped', reason: 'verifier_is_builder' });
9736
+ continue;
9737
+ }
8209
9738
  const verify = certifyVerifyCandidate(task);
8210
9739
  if (!verify) {
8211
9740
  results.push({ ref, action: 'skipped', reason: 'no_runnable_check_in_proof' });
@@ -8215,7 +9744,7 @@ function cmdCertifyVerified(args) {
8215
9744
  results.push({ ref, action: 'would_certify', verify });
8216
9745
  continue;
8217
9746
  }
8218
- const run = runVerifyCommand(verify, task.workspace_root || process.cwd());
9747
+ const run = runVerifyCommandCached(verify, task.workspace_root || process.cwd(), verifyCache);
8219
9748
  if (!run.ok) {
8220
9749
  results.push({ ref, action: 'verify_failed', reason: run.reason, verify });
8221
9750
  continue;
@@ -8249,6 +9778,9 @@ function cmdCertifyVerified(args) {
8249
9778
  results,
8250
9779
  projection_path: outPath,
8251
9780
  };
9781
+ if (silent) {
9782
+ return payload;
9783
+ }
8252
9784
  if (asJson) {
8253
9785
  console.log(JSON.stringify(payload, null, 2));
8254
9786
  } else if (results.length === 0) {
@@ -8262,9 +9794,27 @@ function cmdCertifyVerified(args) {
8262
9794
  return payload;
8263
9795
  }
8264
9796
 
9797
+ // The landing: everything certified, one summary, one human gesture.
9798
+ // Review-by-N-pastes was the operator pain; this is the batch gate that
9799
+ // keeps human accept as the one gate without making it N gates.
9800
+ function cmdLanding(args) {
9801
+ if (hasFlag(args, '--accept')) {
9802
+ const passthrough = args.filter(arg => arg !== '--accept');
9803
+ if (!passthrough.includes('--confirm-human-accept')) passthrough.push('--confirm-human-accept');
9804
+ return cmdAutoAcceptCertified(passthrough);
9805
+ }
9806
+ cmdReviews(args);
9807
+ console.log('');
9808
+ console.log('land everything certified above in one gesture:');
9809
+ console.log(' atris task landing --accept --as <you>');
9810
+ console.log('(items needing one more check stay in review; only certified work lands)');
9811
+ }
9812
+
8265
9813
  function cmdAutoAcceptCertified(args) {
8266
9814
  const dryRun = hasFlag(args, '--dry-run');
8267
- const strictVerify = hasFlag(args, '--strict-verify');
9815
+ const acceptAll = hasFlag(args, '--all');
9816
+ const certifyFirst = hasFlag(args, '--certify-first');
9817
+ const strictVerify = !hasFlag(args, '--no-strict-verify') && !acceptAll;
8268
9818
  const actorFlag = flag(args, '--as');
8269
9819
  const hasHumanActor = validHumanActorFlag(actorFlag);
8270
9820
  const confirmedHumanAccept = hasFlag(args, '--confirm-human-accept');
@@ -8277,7 +9827,17 @@ function cmdAutoAcceptCertified(args) {
8277
9827
  : { ok: false };
8278
9828
  const actor = String(actorFlag || (policyAuth.ok ? policyAuth.actor : 'auto-accept-certified'));
8279
9829
  const limitRaw = flag(args, '--limit');
8280
- const max = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 12) : 12;
9830
+ const hasExplicitLimit = Boolean(limitRaw) && limitRaw !== true;
9831
+ // --all means sweep the full certified backlog, not just the first page of
9832
+ // it. Before this fix `max` was hard-capped at 12 even under --all, so a
9833
+ // real backlog (78 certified rows observed live) only ever drained 12/run —
9834
+ // an invisible undercount the autoland heartbeat repeated every hour.
9835
+ // AUTO_ACCEPT_ALL_SWEEP_CAP is a safety ceiling, not a target: a well-formed
9836
+ // --all run should always scan fewer rows than this.
9837
+ const AUTO_ACCEPT_ALL_SWEEP_CAP = 500;
9838
+ const max = hasExplicitLimit
9839
+ ? Math.max(1, Number(limitRaw) || 12)
9840
+ : (acceptAll ? AUTO_ACCEPT_ALL_SWEEP_CAP : 12);
8281
9841
  const parsedReward = parseAcceptReward(flag(args, '--reward'));
8282
9842
  if (!parsedReward.ok) {
8283
9843
  console.error('atris task auto-accept-certified: reward must be a positive number');
@@ -8297,27 +9857,62 @@ function cmdAutoAcceptCertified(args) {
8297
9857
  'live auto-accept requires --as <human> so XP has an explicit human acceptance actor',
8298
9858
  );
8299
9859
  }
8300
- if (agentProofOnlyMode() && !dryRun) {
9860
+ // The standing autoland policy clears this gate too, same as the two
9861
+ // per-run human gates above: the owner flipped the policy, accepts run as
9862
+ // that owner, and the cron tick would land the same rows an hour later
9863
+ // anyway. Without this exception `atris autoland tick` was blind whenever
9864
+ // invoked from an agent session (CLAUDECODE etc. in env): the spawned
9865
+ // sweep failTask'd with no summary and the tick receipt showed nulls.
9866
+ // A per-run --confirm-human-accept claim from an agent is still refused —
9867
+ // policyAuth is only consulted when no per-run confirmation is passed.
9868
+ if (agentProofOnlyMode() && !dryRun && !policyAuth.ok) {
8301
9869
  failAgentProofOnly(
8302
9870
  'atris task auto-accept-certified',
8303
- 'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them.',
9871
+ 'Agent proof-only mode can preview certified rows with --dry-run, but cannot live-accept them without the standing autoland policy.',
8304
9872
  );
8305
9873
  }
8306
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
+
8307
9883
  const taskDb = getTaskDb();
8308
9884
  const db = taskDb.open();
8309
9885
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
8310
9886
  const queue = taskReviewQueue(projection, ['--limit', String(max)]);
9887
+ const pendingReview = (projection.tasks || [])
9888
+ .filter((t) => t && t.status === 'review' && t.review && t.review.approval_status === 'pending');
9889
+ // Honest denominator for the summary line below: how many rows the
9890
+ // projection actually certified, independent of any scan cap. If `scanned`
9891
+ // ever comes in under `certified`, the undercount is visible instead of
9892
+ // silent (the exact failure mode this fix closes).
9893
+ const certifiedTotal = pendingReview.filter((t) => isAgentCertified(t)).length;
9894
+ // The review queue only surfaces certified rows. Under --all the bar is
9895
+ // the protected lanes, not certification, so scan every pending review.
9896
+ const pool = acceptAll
9897
+ ? pendingReview
9898
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0))
9899
+ .slice(0, max)
9900
+ : queue.items.filter(item => item.queue_role !== 'blocked');
8311
9901
  const results = [];
8312
9902
 
8313
- for (const item of queue.items) {
9903
+ for (const item of pool) {
8314
9904
  const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
8315
9905
  const task = fullProjection.tasks[0] || null;
8316
9906
  if (!task) {
8317
9907
  results.push({ ref: item.display_id || item.id, eligible: false, reason: 'task_not_found', action: 'skipped' });
8318
9908
  continue;
8319
9909
  }
8320
- const evaluation = evaluateAutoAccept(task, { strictVerify });
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 });
8321
9916
  if (!evaluation.eligible) {
8322
9917
  results.push({ ...evaluation, action: 'skipped' });
8323
9918
  continue;
@@ -8326,12 +9921,27 @@ function cmdAutoAcceptCertified(args) {
8326
9921
  results.push({ ...evaluation, action: 'would_accept', reward: parsedReward.value });
8327
9922
  continue;
8328
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
+ }
8329
9938
  const accepted = acceptReviewTask(taskDb, db, task.id, {
8330
9939
  actor,
8331
9940
  proof: evaluation.proof,
8332
9941
  reward: parsedReward.value,
8333
9942
  lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
8334
9943
  nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
9944
+ autoAccepted: true,
8335
9945
  });
8336
9946
  if (!accepted.ok) {
8337
9947
  results.push({ ...evaluation, action: 'accept_failed', reason: accepted.reason });
@@ -8349,32 +9959,282 @@ function cmdAutoAcceptCertified(args) {
8349
9959
 
8350
9960
  const { projection: finalProjection, outPath: finalPath } = writeDefaultProjection(taskDb, db);
8351
9961
  const summary = {
8352
- scanned: queue.items.length,
9962
+ certified: certifiedTotal,
9963
+ scanned: pool.length,
8353
9964
  accepted: results.filter(row => row.action === 'accepted').length,
8354
9965
  would_accept: results.filter(row => row.action === 'would_accept').length,
9966
+ revised: results.filter(row => row.action === 'revised').length,
8355
9967
  skipped: results.filter(row => row.action === 'skipped').length,
8356
- failed: results.filter(row => row.action === 'accept_failed').length,
9968
+ failed: results.filter(row => row.action === 'accept_failed' || row.action === 'revise_failed').length,
9969
+ // Visible undercount flag: true only if the pool was cut short by `max`
9970
+ // while certified rows still existed beyond it. --all uses a high safety
9971
+ // cap (AUTO_ACCEPT_ALL_SWEEP_CAP), so this should stay false in practice;
9972
+ // if it ever flips true, the cap itself needs raising, not silence.
9973
+ undercounted: certifiedTotal > pool.length,
8357
9974
  };
8358
9975
  if (wantsJson(args)) {
8359
9976
  printJson({
8360
9977
  ok: true,
8361
9978
  action: dryRun ? 'auto_accept_certified_dry_run' : 'auto_accept_certified',
8362
9979
  strict_verify: strictVerify,
9980
+ accept_all: acceptAll,
8363
9981
  summary,
8364
9982
  ...summary,
8365
9983
  results,
9984
+ certification,
8366
9985
  projection_path: finalPath,
8367
9986
  queue,
8368
9987
  });
8369
- return;
9988
+ return { ...summary, results, certification, projection_path: finalPath, queue };
8370
9989
  }
8371
9990
  console.log(`AUTO-ACCEPT CERTIFIED (${dryRun ? 'dry-run' : 'execute'})`);
8372
- console.log(`${summary.accepted || summary.would_accept} accepted / ${summary.skipped} skipped / ${summary.failed} failed / ${summary.scanned} scanned`);
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)' : ''}`);
8373
9992
  for (const row of results) {
8374
9993
  const nextAction = row.next_action ? ` next_action=${row.next_action}` : '';
8375
9994
  const reviewChat = row.review_chat_command ? ` review_chat=${row.review_chat_command}` : '';
8376
9995
  console.log(`${row.action.toUpperCase()} ${row.ref}: ${row.reason}${row.reward ? ` reward=${row.reward}` : ''}${nextAction}${reviewChat}`);
8377
9996
  }
9997
+ return { ...summary, results, certification, projection_path: finalPath, queue };
9998
+ }
9999
+
10000
+ const SWEEP_AUTO_ACCEPT_PROTECTED = new Set([
10001
+ 'money', 'deploy', 'release', 'publish', 'security', 'customer', 'outward',
10002
+ ]);
10003
+
10004
+ function autoAcceptSweepLabelValues(task) {
10005
+ const metadata = task && task.metadata && typeof task.metadata === 'object' ? task.metadata : {};
10006
+ const tags = Array.isArray(metadata.tags) ? metadata.tags : [];
10007
+ return [
10008
+ task && task.tag,
10009
+ task && task.lane,
10010
+ metadata.tag,
10011
+ metadata.lane,
10012
+ metadata.mission_lane,
10013
+ metadata.stage,
10014
+ ...tags,
10015
+ ].filter(value => value !== undefined && value !== null && String(value).trim());
10016
+ }
10017
+
10018
+ function autoAcceptSweepNormalizedTokens(value) {
10019
+ const normalized = String(value || '').trim().toLowerCase().replace(/_/g, '-');
10020
+ if (!normalized) return [];
10021
+ return [normalized, ...normalized.split(/[^a-z0-9-]+/).filter(Boolean)];
10022
+ }
10023
+
10024
+ function autoAcceptSweepDeniedReason(task) {
10025
+ for (const value of autoAcceptSweepLabelValues(task)) {
10026
+ const tokens = autoAcceptSweepNormalizedTokens(value);
10027
+ for (const token of tokens) {
10028
+ if (token === 'needs-human' || token === 'needshuman') {
10029
+ return 'needs_human';
10030
+ }
10031
+ const protectedLane = [...SWEEP_AUTO_ACCEPT_PROTECTED].find((denied) =>
10032
+ token === denied || token.replace(/s$/, '') === denied
10033
+ );
10034
+ if (protectedLane) return 'protected_lane';
10035
+ }
10036
+ }
10037
+ return null;
10038
+ }
10039
+
10040
+ function autoAcceptSweepLatestProof(task) {
10041
+ const metadata = task && task.metadata || {};
10042
+ const review = task && task.review || {};
10043
+ return String(review.proof || metadata.latest_agent_proof || '').trim();
10044
+ }
10045
+
10046
+ function autoAcceptSweepVerifierEvidence(proof, root) {
10047
+ const evidence = extractReceiptEvidence(proof, root);
10048
+ if (!evidence || !evidence.receipts || !evidence.receipts.length) {
10049
+ return { ok: false, reason: 'no_passing_verifier', evidence: evidence || null };
10050
+ }
10051
+ if (evidence.missing && evidence.missing.length) {
10052
+ return { ok: false, reason: 'receipt_missing', evidence };
10053
+ }
10054
+ const passing = evidence.receipts.filter((receipt) => receipt.verifier_passed === true);
10055
+ if (!passing.length) {
10056
+ return { ok: false, reason: 'no_passing_verifier', evidence };
10057
+ }
10058
+ const notPassed = evidence.receipts.find((receipt) => receipt.verifier_passed !== true);
10059
+ if (notPassed) {
10060
+ return { ok: false, reason: 'receipt_verifier_not_passed', evidence };
10061
+ }
10062
+ return {
10063
+ ok: true,
10064
+ evidence,
10065
+ passing,
10066
+ proved_by: passing.map((receipt) => `${receipt.path} verifier_passed=true`),
10067
+ };
10068
+ }
10069
+
10070
+ function autoAcceptSweepHappened(task) {
10071
+ const review = task && task.review || {};
10072
+ return clipStatusText(
10073
+ review.landing?.happened
10074
+ || review.result?.changed
10075
+ || task?.title
10076
+ || 'accepted verified task',
10077
+ 180,
10078
+ );
10079
+ }
10080
+
10081
+ function evaluateSweepAutoAccept(task, root) {
10082
+ const ref = taskRef(task);
10083
+ if (!task) return { eligible: false, ref, reason: 'task_not_found' };
10084
+ if (task.status !== 'review') return { eligible: false, ref, reason: 'not_in_review' };
10085
+ const metadata = task.metadata || {};
10086
+ const review = task.review || {};
10087
+ const approval = String(review.approval_status || metadata.approval_status || 'pending').toLowerCase();
10088
+ if (approval !== 'pending') return { eligible: false, ref, reason: `approval_${approval}` };
10089
+ if (metadata.auto_accepted_at) return { eligible: false, ref, reason: 'already_auto_accepted' };
10090
+ const denied = autoAcceptSweepDeniedReason(task);
10091
+ if (denied) return { eligible: false, ref, reason: denied };
10092
+ const proof = autoAcceptSweepLatestProof(task);
10093
+ if (!proof) return { eligible: false, ref, reason: 'no_proof' };
10094
+
10095
+ // 1. An explicit, stored verifier (`atris task ready --verify`, or a prior
10096
+ // certify-verified stamp) is the strongest signal: re-run it live, right
10097
+ // now, against the current checkout, and let it decide outright. This is
10098
+ // what unblocks CLI-762/CLI-861-shaped proofs: a real green test cited (or
10099
+ // executed) in the proof, but the older receipt-path check below could not
10100
+ // find file evidence for it once `ready --verify` stopped writing a
10101
+ // receipt file and started embedding the executed result into proof text.
10102
+ const storedVerify = typeof metadata.verify === 'string' ? metadata.verify.trim() : '';
10103
+ if (storedVerify) {
10104
+ const result = runVerifyCommand(storedVerify, root);
10105
+ if (!result.ok) return { eligible: false, ref, reason: result.reason, verify: storedVerify };
10106
+ return {
10107
+ eligible: true,
10108
+ ref,
10109
+ reason: 'verified_command',
10110
+ policy: 'sweep_auto_accept_verified_command',
10111
+ proof,
10112
+ verify: storedVerify,
10113
+ proved_by: [`${storedVerify} exited 0`],
10114
+ happened: autoAcceptSweepHappened(task),
10115
+ };
10116
+ }
10117
+
10118
+ // 2. Legacy path: a receipt file explicitly named in proof text.
10119
+ const verifier = autoAcceptSweepVerifierEvidence(proof, root);
10120
+ if (verifier.ok) {
10121
+ return {
10122
+ eligible: true,
10123
+ ref,
10124
+ reason: 'verified_receipt',
10125
+ policy: 'sweep_auto_accept_verified',
10126
+ proof,
10127
+ evidence: verifier.evidence,
10128
+ proved_by: verifier.proved_by,
10129
+ happened: autoAcceptSweepHappened(task),
10130
+ };
10131
+ }
10132
+
10133
+ // 3. No stored verifier and no receipt was even cited: try deriving a
10134
+ // safe, runnable command straight from the proof text itself (same
10135
+ // extractor certify-verified uses) and re-run it live. Only reached when
10136
+ // there is nothing else to go on, so a proof that legitimately cites a
10137
+ // real receipt keeps taking the receipt path above rather than racing an
10138
+ // unrelated command mentioned in the same sentence.
10139
+ if (verifier.reason === 'no_passing_verifier') {
10140
+ const derived = certifyVerifyCandidate(task);
10141
+ if (derived) {
10142
+ const result = runVerifyCommand(derived, root);
10143
+ if (result.ok) {
10144
+ return {
10145
+ eligible: true,
10146
+ ref,
10147
+ reason: 'verified_derived_command',
10148
+ policy: 'sweep_auto_accept_verified_derived',
10149
+ proof,
10150
+ verify: derived,
10151
+ proved_by: [`${derived} exited 0`],
10152
+ happened: autoAcceptSweepHappened(task),
10153
+ };
10154
+ }
10155
+ return { eligible: false, ref, reason: result.reason, verify: derived };
10156
+ }
10157
+ }
10158
+
10159
+ return { eligible: false, ref, reason: verifier.reason, evidence: verifier.evidence };
10160
+ }
10161
+
10162
+ function cmdSweep(args) {
10163
+ if (!hasFlag(args, '--auto-accept')) {
10164
+ failTask(
10165
+ 'atris task sweep',
10166
+ 'missing_auto_accept',
10167
+ 'atris task sweep currently requires --auto-accept for the explicit verified accept policy',
10168
+ );
10169
+ }
10170
+ const actor = String(flag(args, '--as') || 'orb-autoaccept');
10171
+ const parsedReward = parseAcceptReward(flag(args, '--reward'));
10172
+ if (!parsedReward.ok) {
10173
+ failTask('atris task sweep', 'invalid_reward', 'atris task sweep --auto-accept reward must be a positive number');
10174
+ }
10175
+ const limitRaw = flag(args, '--limit');
10176
+ const explicitLimit = limitRaw && limitRaw !== true ? Math.max(1, Number(limitRaw) || 0) : null;
10177
+ const taskDb = getTaskDb();
10178
+ const db = taskDb.open();
10179
+ const { projection } = writeDefaultProjection(taskDb, db);
10180
+ const root = projection.workspace_root || process.cwd();
10181
+ let pendingReview = (projection.tasks || [])
10182
+ .filter((task) => task && task.status === 'review' && task.review && task.review.approval_status === 'pending')
10183
+ .sort((a, b) => Number(b.updated_at || 0) - Number(a.updated_at || 0));
10184
+ if (explicitLimit) pendingReview = pendingReview.slice(0, explicitLimit);
10185
+ const results = [];
10186
+ for (const item of pendingReview) {
10187
+ const fullProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId: item.id }));
10188
+ const task = fullProjection.tasks[0] || null;
10189
+ const evaluation = evaluateSweepAutoAccept(task, root);
10190
+ if (!evaluation.eligible) {
10191
+ results.push({ ...evaluation, action: 'skipped', task_id: task?.id || item.id || null });
10192
+ continue;
10193
+ }
10194
+ const accepted = acceptReviewTask(taskDb, db, task.id, {
10195
+ actor,
10196
+ proof: evaluation.proof,
10197
+ reward: parsedReward.value,
10198
+ lesson: String(task.review?.lesson || task.metadata?.latest_agent_lesson || ''),
10199
+ nextTask: String(task.review?.next_task || task.metadata?.latest_agent_next_task || ''),
10200
+ autoAccepted: true,
10201
+ });
10202
+ if (!accepted.ok) {
10203
+ results.push({ ...evaluation, action: 'accept_failed', task_id: task.id, reason: accepted.reason });
10204
+ continue;
10205
+ }
10206
+ stampAutoAcceptMetadata(taskDb, db, task.id, actor, evaluation.policy);
10207
+ refreshCareerXpAfterReview(accepted.reviewed);
10208
+ results.push({
10209
+ ...evaluation,
10210
+ action: 'accepted',
10211
+ task_id: task.id,
10212
+ reward: accepted.reviewed.episode.reward.value,
10213
+ });
10214
+ }
10215
+ const { outPath } = writeDefaultProjection(taskDb, db);
10216
+ const summary = {
10217
+ scanned: pendingReview.length,
10218
+ accepted: results.filter((row) => row.action === 'accepted').length,
10219
+ skipped: results.filter((row) => row.action === 'skipped').length,
10220
+ failed: results.filter((row) => row.action === 'accept_failed').length,
10221
+ };
10222
+ if (wantsJson(args)) {
10223
+ printJson({
10224
+ ok: true,
10225
+ action: 'sweep_auto_accept',
10226
+ actor,
10227
+ summary,
10228
+ ...summary,
10229
+ results,
10230
+ projection_path: outPath,
10231
+ });
10232
+ return;
10233
+ }
10234
+ console.log(`TASK SWEEP AUTO-ACCEPT: ${summary.accepted} accepted / ${summary.scanned} scanned / ${summary.skipped} skipped${summary.failed ? ` / ${summary.failed} failed` : ''}`);
10235
+ for (const row of results.filter((item) => item.action === 'accepted')) {
10236
+ console.log(`ACCEPTED ${row.ref}: ${row.happened} | proved by ${row.proved_by.join(', ')}`);
10237
+ }
8378
10238
  }
8379
10239
 
8380
10240
  function cmdRevise(args) {
@@ -8416,6 +10276,138 @@ function cmdRevise(args) {
8416
10276
  console.log(`revise ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
8417
10277
  }
8418
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
+
8419
10411
  function cmdReview(args) {
8420
10412
  const pos = positional(args);
8421
10413
  const id = pos[0];
@@ -8444,6 +10436,7 @@ function cmdReview(args) {
8444
10436
  if (clearNextTask || (typeof nextTaskFlag === 'string' && !String(nextTaskFlag).trim())) clearedFields.push('next_task');
8445
10437
  const proof = proofFlagValue(args);
8446
10438
  const verify = textFlag(args, ['--verify']);
10439
+ guardExplicitActor('atris task review', flag(args, '--as'));
8447
10440
  const actor = flag(args, '--as') || DEFAULT_OWNER;
8448
10441
  const rewardValue = reward === true || reward === null ? 0 : reward;
8449
10442
  if (agentProofOnlyMode() && Number(rewardValue) > 0) {
@@ -8481,7 +10474,11 @@ function cmdReview(args) {
8481
10474
  clearedFields,
8482
10475
  });
8483
10476
  if (!result.reviewed) {
8484
- console.error(`review failed: ${result.reason}`);
10477
+ if (result.reason === 'judge_equals_worker') {
10478
+ console.error(`review failed: judge_equals_worker: ${result.builder} built this task and cannot judge it. Hand off: atris task review ${id} --reward 1 --as <another member>`);
10479
+ } else {
10480
+ console.error(`review failed: ${result.reason}`);
10481
+ }
8485
10482
  process.exit(1);
8486
10483
  }
8487
10484
  const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
@@ -8587,21 +10584,23 @@ function cmdEvents(args) {
8587
10584
  const pos = positional(args);
8588
10585
  let taskId = pos[0] || null;
8589
10586
  const all = hasFlag(args, '--all');
10587
+ const everywhere = taskScopeEverywhere(args);
8590
10588
  const rawLimit = flag(args, '--limit');
8591
10589
  const explicitLimit = rawLimit && rawLimit !== true ? Number(rawLimit) : null;
8592
10590
  const defaultRecentLimit = 24;
8593
- const limit = explicitLimit || (taskId ? 500 : (all ? null : defaultRecentLimit));
10591
+ const limit = explicitLimit || (taskId ? 500 : (all || everywhere ? null : defaultRecentLimit));
8594
10592
  const taskDb = getTaskDb();
8595
10593
  const db = taskDb.open();
8596
10594
  if (taskId) taskId = requireTaskId(taskDb, db, taskId, 'atris task events');
10595
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
8597
10596
  const events = taskDb.listTaskEvents(db, {
8598
10597
  taskId,
8599
- workspaceRoot: all || taskId ? null : taskDb.workspaceRoot(),
10598
+ workspaceRoot: taskId ? null : workspaceRoot,
8600
10599
  limit,
8601
- order: taskId || all ? 'asc' : 'desc',
10600
+ order: taskId || all || everywhere ? 'asc' : 'desc',
8602
10601
  });
8603
10602
  const refRows = taskDb.listTasks(db, {
8604
- workspaceRoot: all ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : taskDb.workspaceRoot()),
10603
+ workspaceRoot: everywhere ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : workspaceRoot),
8605
10604
  });
8606
10605
  const refById = taskDb.taskDisplayRefMap(refRows);
8607
10606
  if (wantsJson(args)) {
@@ -8609,7 +10608,7 @@ function cmdEvents(args) {
8609
10608
  ok: true,
8610
10609
  action: 'events',
8611
10610
  task_id: taskId,
8612
- mode: taskId ? 'task' : (all ? 'ledger' : 'recent'),
10611
+ mode: taskId ? 'task' : (everywhere ? 'ledger_everywhere' : (all ? 'ledger' : 'recent')),
8613
10612
  limit,
8614
10613
  events,
8615
10614
  });
@@ -8619,7 +10618,7 @@ function cmdEvents(args) {
8619
10618
  console.log('(no task events)');
8620
10619
  return;
8621
10620
  }
8622
- if (!taskId && !all) {
10621
+ if (!taskId && !all && !everywhere) {
8623
10622
  console.log('TASK EVENTS');
8624
10623
  console.log(`recent ${events.length} event${events.length === 1 ? '' : 's'} (use --all for the full ledger, --limit N to adjust)`);
8625
10624
  console.log('');
@@ -8720,12 +10719,13 @@ function cmdLineage(args) {
8720
10719
  function cmdExport(args) {
8721
10720
  const out = flag(args, '--out') || path.join('.atris', 'state', 'tasks.projection.json');
8722
10721
  const all = hasFlag(args, '--all');
10722
+ const everywhere = taskScopeEverywhere(args);
8723
10723
  const taskDb = getTaskDb();
8724
10724
  const db = taskDb.open();
8725
10725
  const outPath = path.resolve(String(out));
8726
10726
  const projection = enrichTaskProjection(taskDb.taskProjection(db, {
8727
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
8728
- limit: 500,
10727
+ workspaceRoot: scopedWorkspaceRoot(taskDb, args, { everywhere }),
10728
+ limit: all ? null : 500,
8729
10729
  }));
8730
10730
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
8731
10731
  fs.writeFileSync(outPath, JSON.stringify(projection, null, 2) + '\n', 'utf8');
@@ -8902,21 +10902,61 @@ function taskRenderSummaryLine(counts) {
8902
10902
  ].join('; ');
8903
10903
  }
8904
10904
 
10905
+ function refreshExistingTodoMarkdown(taskDb, db, workspaceRoot) {
10906
+ const ws = workspaceRoot || taskDb.workspaceRoot();
10907
+ const outPath = path.join(ws, 'atris', 'TODO.md');
10908
+ if (!fs.existsSync(outPath)) return null;
10909
+ const rows = taskDb.listTasks(db, { workspaceRoot: ws, limit: 500 });
10910
+ const refRows = taskDb.listTasks(db, { workspaceRoot: ws });
10911
+ const existingTodo = fs.readFileSync(outPath, 'utf8');
10912
+ const preservedSections = [];
10913
+ const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
10914
+ if (endgameSection) preservedSections.push(endgameSection);
10915
+ const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
10916
+ const markdown = taskDb.renderTodoMarkdown([...rows, ...markdownRows], {
10917
+ refRows,
10918
+ preservedSections,
10919
+ });
10920
+ fs.writeFileSync(outPath, markdown, 'utf8');
10921
+ return outPath;
10922
+ }
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
+
8905
10943
  function cmdRender(args) {
8906
10944
  const out = flag(args, '--out') || path.join('atris', 'TODO.md');
8907
10945
  const all = hasFlag(args, '--all');
10946
+ const everywhere = taskScopeEverywhere(args);
8908
10947
  const doneLimitRaw = flag(args, '--done-limit');
8909
10948
  const doneLimit = doneLimitRaw && doneLimitRaw !== true ? Number(doneLimitRaw) : undefined;
8910
10949
  const failedLimitRaw = flag(args, '--failed-limit');
8911
10950
  const failedLimit = failedLimitRaw && failedLimitRaw !== true ? Number(failedLimitRaw) : undefined;
8912
10951
  const taskDb = getTaskDb();
8913
10952
  const db = taskDb.open();
10953
+ const workspaceRoot = scopedWorkspaceRoot(taskDb, args, { everywhere });
8914
10954
  const rows = taskDb.listTasks(db, {
8915
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
8916
- limit: 500,
10955
+ workspaceRoot,
10956
+ limit: all ? null : 500,
8917
10957
  });
8918
10958
  const refRows = taskDb.listTasks(db, {
8919
- workspaceRoot: all ? null : taskDb.workspaceRoot(),
10959
+ workspaceRoot,
8920
10960
  });
8921
10961
  const outPath = path.resolve(String(out));
8922
10962
  const existingTodo = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
@@ -9525,6 +11565,7 @@ async function handleTaskApi(req, res, taskDb, db) {
9525
11565
  owner,
9526
11566
  reviewer,
9527
11567
  all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11568
+ everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
9528
11569
  limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
9529
11570
  scope,
9530
11571
  });
@@ -9540,6 +11581,7 @@ async function handleTaskApi(req, res, taskDb, db) {
9540
11581
  owner,
9541
11582
  reviewer,
9542
11583
  all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11584
+ everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
9543
11585
  limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
9544
11586
  scope,
9545
11587
  });
@@ -9570,6 +11612,7 @@ async function handleTaskApi(req, res, taskDb, db) {
9570
11612
  owner,
9571
11613
  reviewer,
9572
11614
  all: url.searchParams.get('all') === '1' || url.searchParams.get('all') === 'true',
11615
+ everywhere: url.searchParams.get('everywhere') === '1' || url.searchParams.get('everywhere') === 'true',
9573
11616
  limit: Number.isFinite(limit) && limit > 0 ? Math.floor(limit) : 8,
9574
11617
  scope,
9575
11618
  });
@@ -9880,10 +11923,15 @@ async function handleTaskApi(req, res, taskDb, db) {
9880
11923
  if (proofIssue) return sendProofIssue(res, proof, proofIssue);
9881
11924
  const nextTaskInput = normalizeReviewNextTaskInput(body.next);
9882
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
+ }
9883
11931
  const resultTrace = buildAutomaticResultTrace(taskDb, db, taskId, {
9884
11932
  actor,
9885
11933
  proof,
9886
- changed: body.changed || body.result || body.done,
11934
+ changed: body.changed || resultText || body.done,
9887
11935
  checked: body.checked || body.check || body.verified,
9888
11936
  passed: body.passed || body.pass,
9889
11937
  failed: body.failed || body.fail,
@@ -9901,6 +11949,7 @@ async function handleTaskApi(req, res, taskDb, db) {
9901
11949
  lesson: String(body.lesson || ''),
9902
11950
  nextTask: nextTaskInput.nextTask,
9903
11951
  resultTrace: resultTrace && resultTrace.trace,
11952
+ result: resultText,
9904
11953
  landing: body.landing || {
9905
11954
  happened: body.happened,
9906
11955
  checked: body.checked,
@@ -10041,7 +12090,7 @@ function cmdServe(args) {
10041
12090
  });
10042
12091
  }
10043
12092
 
10044
- async function run(args) {
12093
+ async function runTaskCommand(args) {
10045
12094
  const raw = args || [];
10046
12095
  if (raw.includes('--help') || raw.includes('-h')) return help();
10047
12096
  const first = raw[0];
@@ -10114,6 +12163,10 @@ async function run(args) {
10114
12163
  return cmdPlanPreview(rest);
10115
12164
  case 'note': return cmdNote(rest);
10116
12165
  case 'say': return cmdNote(rest);
12166
+ case 'retitle': return cmdRetitle(rest);
12167
+ case 'tag':
12168
+ case 'tags':
12169
+ return cmdTag(rest);
10117
12170
  case 'show': return cmdShow(rest);
10118
12171
  case 'inspect': return cmdInspect(rest);
10119
12172
  case 'page': return cmdPage(rest);
@@ -10122,11 +12175,18 @@ async function run(args) {
10122
12175
  case 'chat-review':
10123
12176
  return cmdReviewChat(rest);
10124
12177
  case 'ready': return cmdReady(rest);
12178
+ case 'receipt': return cmdTaskReceipt(rest);
10125
12179
  case 'result': return cmdResult(rest);
10126
12180
  case 'accept': return cmdAccept(rest);
12181
+ case 'landing':
12182
+ case 'land-review':
12183
+ return cmdLanding(rest);
10127
12184
  case 'auto-accept-certified':
10128
12185
  case 'auto-accept':
10129
12186
  return cmdAutoAcceptCertified(rest);
12187
+ case 'sweep':
12188
+ return cmdSweep(rest);
12189
+ case 'audit': return cmdAudit(rest);
10130
12190
  case 'certify-verified':
10131
12191
  return cmdCertifyVerified(rest);
10132
12192
  case 'accept-group':
@@ -10135,6 +12195,9 @@ async function run(args) {
10135
12195
  case 'done': return cmdDone(rest);
10136
12196
  case 'finish': return cmdFinish(rest);
10137
12197
  case 'fail': return cmdDone([...rest, '--failed']);
12198
+ case 'archive': return cmdArchive(rest);
12199
+ case 'clear-done': return cmdClearDone(rest);
12200
+ case 'relabel-archived': return cmdRelabelArchived(rest);
10138
12201
  case 'review': return cmdReview(rest);
10139
12202
  case 'reviews':
10140
12203
  case 'review-queue':
@@ -10168,4 +12231,34 @@ async function run(args) {
10168
12231
  }
10169
12232
  }
10170
12233
 
10171
- 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
+ };