atris 3.15.13 → 3.15.22

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 (93) hide show
  1. package/AGENTS.md +84 -8
  2. package/README.md +5 -1
  3. package/atris/AGENTS.md +46 -1
  4. package/atris/CLAUDE.md +36 -1
  5. package/atris/GEMINI.md +14 -1
  6. package/atris/atris.md +12 -1
  7. package/atris/atrisDev.md +3 -2
  8. package/atris/context/README.md +11 -0
  9. package/atris/features/company-brain-sync/validate.md +5 -5
  10. package/atris/learnings.jsonl +1 -0
  11. package/atris/policies/atris-design.md +2 -0
  12. package/atris/skills/aeo/SKILL.md +2 -2
  13. package/atris/skills/atris/SKILL.md +15 -62
  14. package/atris/skills/design/SKILL.md +2 -0
  15. package/atris/skills/imessage/SKILL.md +19 -2
  16. package/atris/skills/loop/SKILL.md +6 -5
  17. package/atris/skills/magic-inbox/SKILL.md +1 -1
  18. package/atris/team/_template/MEMBER.md +23 -1
  19. package/atris/team/brainstormer/START_HERE.md +6 -0
  20. package/atris/team/executor/MEMBER.md +13 -0
  21. package/atris/team/executor/START_HERE.md +6 -0
  22. package/atris/team/launcher/START_HERE.md +6 -0
  23. package/atris/team/mission-lead/MEMBER.md +39 -0
  24. package/atris/team/mission-lead/MISSION.md +33 -0
  25. package/atris/team/mission-lead/START_HERE.md +6 -0
  26. package/atris/team/navigator/MEMBER.md +11 -0
  27. package/atris/team/navigator/START_HERE.md +6 -0
  28. package/atris/team/opus-overnight/MEMBER.md +39 -0
  29. package/atris/team/opus-overnight/MISSION.md +61 -0
  30. package/atris/team/opus-overnight/START_HERE.md +6 -0
  31. package/atris/team/opus-overnight/STEERING.md +35 -0
  32. package/atris/team/researcher/START_HERE.md +6 -0
  33. package/atris/team/validator/MEMBER.md +26 -6
  34. package/atris/team/validator/START_HERE.md +6 -0
  35. package/atris/wiki/concepts/agent-activation-contract.md +79 -0
  36. package/atris/wiki/concepts/workspace-initialization-contract.md +73 -0
  37. package/atris/wiki/index.md +27 -0
  38. package/atris/wiki/sources/atris-labs-2026-05-10.txt +17 -0
  39. package/atris/wiki/sources/atris-labs-goals-2026-05-10.txt +15 -0
  40. package/atris/wiki/sources/atrisos-generative-ui-product-surface-2026-05-10.txt +10 -0
  41. package/atris/wiki/sources/jack-dorsey-2026-05-10.txt +12 -0
  42. package/atris.md +49 -13
  43. package/bin/atris.js +660 -22
  44. package/commands/activate.js +12 -3
  45. package/commands/aeo.js +1 -1
  46. package/commands/align.js +10 -10
  47. package/commands/analytics.js +9 -4
  48. package/commands/app.js +2 -0
  49. package/commands/apps.js +276 -0
  50. package/commands/auth.js +1 -1
  51. package/commands/autopilot.js +74 -5
  52. package/commands/brain.js +536 -61
  53. package/commands/brainstorm.js +12 -12
  54. package/commands/business-sync.js +142 -24
  55. package/commands/clean.js +9 -6
  56. package/commands/codex-goal.js +311 -0
  57. package/commands/errors.js +11 -1
  58. package/commands/feedback.js +55 -17
  59. package/commands/fork.js +2 -2
  60. package/commands/gm.js +376 -0
  61. package/commands/init.js +80 -3
  62. package/commands/integrations.js +524 -0
  63. package/commands/learn.js +25 -16
  64. package/commands/lesson.js +41 -0
  65. package/commands/lifecycle.js +2 -2
  66. package/commands/member.js +2416 -9
  67. package/commands/mission.js +1776 -0
  68. package/commands/now.js +48 -7
  69. package/commands/play.js +425 -0
  70. package/commands/publish.js +2 -1
  71. package/commands/pull.js +72 -29
  72. package/commands/push.js +199 -17
  73. package/commands/review.js +51 -13
  74. package/commands/skill.js +2 -2
  75. package/commands/soul.js +19 -13
  76. package/commands/status.js +6 -1
  77. package/commands/sync.js +5 -4
  78. package/commands/task.js +1041 -147
  79. package/commands/terminal.js +5 -5
  80. package/commands/verify.js +7 -5
  81. package/commands/visualize.js +7 -0
  82. package/commands/wiki.js +53 -16
  83. package/commands/workflow.js +298 -54
  84. package/commands/workspace-clean.js +1 -1
  85. package/commands/worktree.js +468 -0
  86. package/commands/xp.js +1608 -0
  87. package/lib/manifest.js +34 -4
  88. package/lib/scorecard.js +3 -2
  89. package/lib/task-db.js +408 -27
  90. package/lib/todo-fallback.js +28 -2
  91. package/lib/todo.js +5 -3
  92. package/package.json +23 -2
  93. package/utils/update-check.js +51 -1
package/commands/task.js CHANGED
@@ -12,6 +12,28 @@ const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
12
12
  || process.env.USER
13
13
  || os.userInfo().username
14
14
  || 'unknown';
15
+ const AGENT_CERTIFICATION_REVIEW_PASSES = 2;
16
+
17
+ const STATUS_PLAN_TAGS = new Set([
18
+ 'agent',
19
+ 'autopilot',
20
+ 'cron',
21
+ 'endgame',
22
+ 'execute',
23
+ 'explore',
24
+ 'feature',
25
+ 'goal',
26
+ 'goal-step',
27
+ 'loop',
28
+ 'plan',
29
+ 'planned',
30
+ 'schedule',
31
+ 'scheduled',
32
+ 'shape',
33
+ 'shaping',
34
+ 'ui',
35
+ 'ux',
36
+ ]);
15
37
 
16
38
  let taskDbModule = null;
17
39
 
@@ -35,43 +57,64 @@ function getTaskDb() {
35
57
  }
36
58
  }
37
59
 
38
- function help() {
39
- console.log(`
60
+ function taskUsageText() {
61
+ return `
40
62
  atris task - durable local task state (SQLite, gitignored)
41
63
 
42
64
  atris task Show the task desk
43
65
  atris task new "<title>" Create a task
44
66
  atris task next Claim/show the next open task
45
67
  atris task say <id> "<message>" Add context to a task
46
- atris task finish <id> [--proof "..."] Complete, optionally review
68
+ atris task ready <id> --proof "..." Agent proof ready; native goal can complete
69
+ atris task accept <id> [--proof "..."] Human accepts proof, marks done
70
+ atris task revise <id> --note "..." Send reviewed work back to Do
47
71
 
48
- atris task add "<title>" [--tag <tag>] Create a task
72
+ atris task add "<title>" [--tag <tag>] [--goal-id <id>] Create a task
49
73
  atris task delegate "<title>" --to <id> Create an assigned task
50
74
  atris task day [--json] Show today's owner-grouped task list
51
75
  atris task list [--all] [--status <s>] List tasks (default: this workspace)
52
76
  atris task claim <id> [--as <owner>] Atomic claim
53
77
  atris task note <id> "<message>" Append dialogue/context to a task
54
78
  atris task show <id> [--json] Show a task card + dialogue
55
- atris task done <id> [--failed] Mark complete (or failed)
79
+ atris task done <id> [--failed] [--proof "..."] Mark complete (or failed), optionally reviewed
80
+ atris task finish <id> [--proof "..."] Legacy alias for done
56
81
  atris task review <id> --reward <n> Write review event + RSI episode
57
- atris task status [--json] Compact live status for web/Swarlo
82
+ atris task status [--json] [--history] Compact live status for web/Swarlo
58
83
  atris task setup [--import-todo] Create/refresh task projection
59
84
  atris task serve [--port <n>] Open local task factory board
60
85
  atris task sync --dry-run Plan cloud/Swarlo task sync writes
61
86
  atris task import <file> One-shot import from TODO.md
62
- atris task events [id] Print append-only task events
87
+ atris task events [id] [--limit <n>] Print recent task events
88
+ atris task events --all Print the full append-only ledger
63
89
  atris task export [--out <file>] Write web/desktop JSON projection
64
- atris task render [--out <file>] Regenerate TODO.md view from state
90
+ atris task render [--out <file>] Regenerate compact TODO.md view from state
65
91
  atris task where Print db path + workspace scope
66
92
  atris task help This help
67
93
 
94
+ Confidence Gate:
95
+ Before plan/do/review advances, find loopholes, patch them with proof,
96
+ verifier, owner, rollback, or name the residual risk.
97
+
68
98
  Env:
69
99
  ATRIS_TASKS_DB Override db path (default ~/.atris/tasks.db)
70
100
  ATRIS_AGENT_ID Owner id for claim/done (default: $USER)
71
101
 
102
+ Refs:
103
+ Human views use semantic refs like OBL-18. Commands accept OBL-18,
104
+ OBL18, full 26-char task IDs, and any unique legacy prefix. JSON/API
105
+ keep the full id as canonical and also expose display_id + legacy_ref.
106
+
72
107
  Headless:
73
108
  Add --json to task commands for machine-readable output and stable automation.
74
- `.trim());
109
+ `.trim();
110
+ }
111
+
112
+ function taskUsageLines() {
113
+ return taskUsageText().split('\n');
114
+ }
115
+
116
+ function help() {
117
+ console.log(taskUsageText());
75
118
  }
76
119
 
77
120
  function flag(args, name) {
@@ -84,12 +127,56 @@ function hasFlag(args, name) {
84
127
  return args.indexOf(name) !== -1;
85
128
  }
86
129
 
130
+ function hasEmptyFlagValue(args, name) {
131
+ const i = args.indexOf(name);
132
+ return i !== -1 && args[i + 1] === '';
133
+ }
134
+
87
135
  function wantsJson(args) {
88
136
  return hasFlag(args, '--json');
89
137
  }
90
138
 
139
+ function parseAcceptReward(value, { defaultValue = 1 } = {}) {
140
+ if (value === undefined || value === null || value === true) return { ok: true, value: defaultValue };
141
+ const numeric = Number(value);
142
+ if (!Number.isFinite(numeric) || numeric <= 0) {
143
+ return { ok: false, reason: 'invalid_reward' };
144
+ }
145
+ return { ok: true, value: numeric };
146
+ }
147
+
91
148
  function printJson(value) {
92
- console.log(JSON.stringify(value, null, 2));
149
+ const buffer = Buffer.from(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
150
+ const retryWait = new Int32Array(new SharedArrayBuffer(4));
151
+ let offset = 0;
152
+ while (offset < buffer.length) {
153
+ try {
154
+ offset += fs.writeSync(process.stdout.fd, buffer, offset, buffer.length - offset);
155
+ } catch (err) {
156
+ if (err && err.code === 'EAGAIN') {
157
+ Atomics.wait(retryWait, 0, 0, 10);
158
+ continue;
159
+ }
160
+ throw err;
161
+ }
162
+ }
163
+ }
164
+
165
+ function refreshCareerXpProjection(workspaceRoot) {
166
+ if (!workspaceRoot) return null;
167
+ try {
168
+ const { collectLocalXpProjection } = require('../commands/xp');
169
+ return collectLocalXpProjection(['--workspace', workspaceRoot]);
170
+ } catch (error) {
171
+ return {
172
+ ok: false,
173
+ error: error && error.message ? error.message : String(error),
174
+ };
175
+ }
176
+ }
177
+
178
+ function refreshCareerXpAfterReview(reviewed) {
179
+ return refreshCareerXpProjection(reviewed?.episode?.workspace_root);
93
180
  }
94
181
 
95
182
  function jsonModeActive() {
@@ -98,7 +185,7 @@ function jsonModeActive() {
98
185
 
99
186
  function failTask(label, reason, detail, exitCode = 2) {
100
187
  if (jsonModeActive()) {
101
- console.error(JSON.stringify({
188
+ console.log(JSON.stringify({
102
189
  ok: false,
103
190
  command: label,
104
191
  reason,
@@ -133,6 +220,28 @@ function taskFromProjection(projection, id) {
133
220
  return projection.tasks.find(t => t.id === id) || null;
134
221
  }
135
222
 
223
+ function taskRef(taskOrId) {
224
+ if (!taskOrId) return 'TASK';
225
+ if (typeof taskOrId === 'string') return taskOrId.replace(/[^a-zA-Z0-9]/g, '').toUpperCase().slice(0, 8);
226
+ return taskOrId.display_id || taskOrId.legacy_ref || taskRef(taskOrId.id);
227
+ }
228
+
229
+ function normalizeTaskLookupRef(value) {
230
+ return String(value || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
231
+ }
232
+
233
+ function taskLookupRefs(task) {
234
+ if (!task) return [];
235
+ return [task.id, task.display_id, task.legacy_ref, taskRef(task)]
236
+ .map(normalizeTaskLookupRef)
237
+ .filter(Boolean);
238
+ }
239
+
240
+ function resolveProjectionTaskRef(ref, taskByRef) {
241
+ const key = normalizeTaskLookupRef(ref);
242
+ return key ? taskByRef.get(key) || null : null;
243
+ }
244
+
136
245
  function createNextTaskIfRequested(taskDb, db, args, currentTask, title) {
137
246
  const nextTitle = String(title || '').trim();
138
247
  if (!hasFlag(args, '--create-next') || !nextTitle) return null;
@@ -196,14 +305,95 @@ function readGoalSources(root = process.cwd()) {
196
305
  return { path: null, goals: [] };
197
306
  }
198
307
 
308
+ function reviewSummary(task, payload = {}) {
309
+ const metadata = task.metadata || {};
310
+ const explicit = payload.summary
311
+ || payload.meaning
312
+ || metadata.review_summary
313
+ || metadata.review_meaning
314
+ || metadata.plain_language_summary
315
+ || metadata.human_summary;
316
+ if (explicit) return clipStatusText(explicit, 240);
317
+
318
+ const title = String(task.title || 'this task').replace(/\s+/g, ' ').trim();
319
+ const plainTitle = title ? title.charAt(0).toLowerCase() + title.slice(1) : 'this task';
320
+ const careerText = [
321
+ task.tag,
322
+ metadata.goal_id,
323
+ metadata.goal_objective,
324
+ metadata.review_goal,
325
+ ].filter(Boolean).join(' ').toLowerCase();
326
+ if (
327
+ careerText.includes('career-xp')
328
+ || careerText.includes('career xp')
329
+ || careerText.includes('agent-xp')
330
+ || careerText.includes('agent xp')
331
+ ) {
332
+ if (task.status === 'done') {
333
+ return `This is accepted AgentXP work: ${plainTitle} is done and has a proof receipt.`;
334
+ }
335
+ if (task.status === 'review') {
336
+ return `This is AgentXP review: ${plainTitle} is agent-complete; accept only if the proof is real.`;
337
+ }
338
+ return `This explains what accepting ${plainTitle} would make real for AgentXP.`;
339
+ }
340
+ if (task.status === 'done') {
341
+ return `This is the accepted outcome: ${plainTitle} is done and counted as real work.`;
342
+ }
343
+ if (task.status === 'review') {
344
+ return `This is the human checkpoint: ${plainTitle} is agent-complete and needs acceptance before it counts as done.`;
345
+ }
346
+ return `This explains what accepting ${plainTitle} would make real.`;
347
+ }
348
+
199
349
  function taskReviewSummary(task) {
200
- const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed');
350
+ const reviewed = (task.events || []).slice().reverse().find(e => e.event_type === 'reviewed' || e.event_type === 'proof_ready' || e.event_type === 'revision_requested');
201
351
  const payload = reviewed && reviewed.payload || {};
352
+ const metadata = task.metadata || {};
353
+ if (!reviewed && !metadata.approval_status && !metadata.agent_review_pass_count && !metadata.human_revision_count && !metadata.agent_certified) return null;
354
+ if (reviewed && reviewed.event_type === 'revision_requested') {
355
+ return {
356
+ summary: reviewSummary(task, payload),
357
+ reward: null,
358
+ proof: null,
359
+ lesson: null,
360
+ next_task: null,
361
+ approval_status: metadata.approval_status || payload.approval_status || 'revise',
362
+ agent_review_pass_count: null,
363
+ agent_certified: false,
364
+ agent_certification_policy: null,
365
+ human_revision_count: metadata.human_revision_count || payload.revision_count || null,
366
+ human_revision_note: metadata.human_revision_note || payload.note || null,
367
+ };
368
+ }
369
+ const reviewPassCount = Number(metadata.agent_review_pass_count || payload.review_pass_count || 0);
370
+ const agentCertified = metadata.agent_certified === true
371
+ || payload.agent_certified === true
372
+ || reviewPassCount >= AGENT_CERTIFICATION_REVIEW_PASSES;
373
+ const reviewedEventHas = (key) => reviewed && reviewed.event_type === 'reviewed'
374
+ && Object.prototype.hasOwnProperty.call(payload, key);
375
+ const clearedReviewFields = new Set(Array.isArray(payload.cleared_review_fields) ? payload.cleared_review_fields : []);
376
+ const readyField = (key, metadataKey) => {
377
+ if (reviewedEventHas(key)) {
378
+ if (payload[key]) return payload[key];
379
+ if (key === 'proof' || !clearedReviewFields.has(key)) return metadata[metadataKey] || null;
380
+ return null;
381
+ }
382
+ return payload[key] || metadata[metadataKey] || null;
383
+ };
202
384
  return {
203
- reward: payload.reward === undefined ? null : payload.reward,
204
- proof: payload.proof || null,
205
- lesson: payload.lesson || null,
206
- next_task: payload.next_task || null,
385
+ summary: reviewSummary(task, payload),
386
+ reward: reviewed && reviewed.event_type === 'reviewed' && payload.reward !== undefined ? payload.reward : null,
387
+ proof: readyField('proof', 'latest_agent_proof'),
388
+ lesson: readyField('lesson', 'latest_agent_lesson'),
389
+ next_task: readyField('next_task', 'latest_agent_next_task'),
390
+ approval_status: metadata.approval_status || (task.status === 'review' ? 'pending' : null),
391
+ agent_review_pass_count: reviewPassCount || null,
392
+ agent_certified: agentCertified,
393
+ agent_certification_policy: metadata.agent_certification_policy
394
+ || payload.agent_certification_policy
395
+ || (agentCertified ? `${AGENT_CERTIFICATION_REVIEW_PASSES}_agent_review_passes` : null),
396
+ human_revision_count: metadata.human_revision_count || null,
207
397
  };
208
398
  }
209
399
 
@@ -212,10 +402,26 @@ function taskAssignee(task) {
212
402
  return metadata.assigned_to || task.claimed_by || null;
213
403
  }
214
404
 
405
+ const GOAL_MATCH_STOPWORDS = new Set([
406
+ 'daily',
407
+ 'goal',
408
+ 'goals',
409
+ 'loop',
410
+ 'loops',
411
+ 'make',
412
+ 'task',
413
+ 'tasks',
414
+ 'work',
415
+ ]);
416
+
215
417
  function scoreGoalMatch(task, goal) {
216
418
  const haystack = `${task.title} ${task.tag || ''}`.toLowerCase();
217
- const words = String(goal || '').toLowerCase().match(/[a-z0-9]{4,}/g) || [];
218
- return words.reduce((score, word) => score + (haystack.includes(word) ? 1 : 0), 0);
419
+ const words = (String(goal || '').toLowerCase().match(/[a-z0-9]{4,}/g) || [])
420
+ .filter(word => !GOAL_MATCH_STOPWORDS.has(word));
421
+ return words.reduce((score, word) => {
422
+ const singular = word.endsWith('s') && word.length > 4 ? word.slice(0, -1) : word;
423
+ return score + (haystack.includes(word) || haystack.includes(singular) ? 1 : 0);
424
+ }, 0);
219
425
  }
220
426
 
221
427
  function pickTaskGoal(task, goals) {
@@ -229,7 +435,27 @@ function pickTaskGoal(task, goals) {
229
435
  bestScore = score;
230
436
  }
231
437
  }
232
- return best;
438
+ return bestScore > 0 ? best : null;
439
+ }
440
+
441
+ function taskBaseObjective(task, goals) {
442
+ const metadata = task && task.metadata || {};
443
+ return task.objective
444
+ || metadata.goal_objective
445
+ || metadata.objective
446
+ || pickTaskGoal(task, goals);
447
+ }
448
+
449
+ function taskObjective(task, parent, goals, { parentLinkType = null, baseObjectives = new Map() } = {}) {
450
+ const metadata = task && task.metadata || {};
451
+ const explicit = task.objective || metadata.goal_objective || metadata.objective;
452
+ if (explicit) return explicit;
453
+ if (parent) {
454
+ if (parentLinkType === 'parent_task_id') return baseObjectives.get(parent.id) || parent.title;
455
+ if (parentLinkType === 'goal_id') return parent.title;
456
+ return baseObjectives.get(parent.id) || parent.title;
457
+ }
458
+ return pickTaskGoal(task, goals);
233
459
  }
234
460
 
235
461
  function buildTaskStreams(tasks, goals) {
@@ -288,29 +514,42 @@ function buildTaskStreams(tasks, goals) {
288
514
  function enrichTaskProjection(projection) {
289
515
  const root = projection.workspace_root || process.cwd();
290
516
  const goalSource = readGoalSources(root);
291
- const byId = new Map((projection.tasks || []).map(task => [task.id, task]));
517
+ const byRef = new Map();
518
+ for (const task of projection.tasks || []) {
519
+ for (const ref of taskLookupRefs(task)) byRef.set(ref, task);
520
+ }
521
+ const baseObjectives = new Map();
522
+ for (const task of projection.tasks || []) {
523
+ const objective = taskBaseObjective(task, goalSource.goals);
524
+ if (objective) baseObjectives.set(task.id, objective);
525
+ }
292
526
  const children = new Map();
293
527
  for (const task of projection.tasks || []) {
294
- const parentId = task.metadata && task.metadata.parent_task_id;
295
- if (!parentId) continue;
296
- if (!children.has(parentId)) children.set(parentId, []);
297
- children.get(parentId).push(task);
528
+ const metadata = task.metadata || {};
529
+ const parent = resolveProjectionTaskRef(metadata.parent_task_id, byRef) || resolveProjectionTaskRef(metadata.goal_id, byRef);
530
+ if (!parent) continue;
531
+ if (!children.has(parent.id)) children.set(parent.id, []);
532
+ children.get(parent.id).push(task);
298
533
  }
299
534
  const enrichedTasks = (projection.tasks || []).map(task => {
300
- const parentId = task.metadata && task.metadata.parent_task_id || null;
301
- const parent = parentId ? byId.get(parentId) : null;
535
+ const metadata = task.metadata || {};
536
+ const parentFromParentId = resolveProjectionTaskRef(metadata.parent_task_id, byRef);
537
+ const parentFromGoalId = resolveProjectionTaskRef(metadata.goal_id, byRef);
538
+ const parent = parentFromParentId || parentFromGoalId;
539
+ const parentLinkType = parentFromParentId ? 'parent_task_id' : parentFromGoalId ? 'goal_id' : null;
540
+ const parentId = parent ? parent.id : metadata.parent_task_id || null;
302
541
  const childTasks = children.get(task.id) || [];
303
542
  const review = taskReviewSummary(task);
304
543
  return {
305
544
  ...task,
306
- objective: pickTaskGoal(task, goalSource.goals),
545
+ objective: taskObjective(task, parent, goalSource.goals, { parentLinkType, baseObjectives }),
307
546
  review,
308
547
  lineage: {
309
548
  parent_task_id: parentId,
310
549
  parent_title: parent ? parent.title : null,
311
550
  child_task_ids: childTasks.map(child => child.id),
312
551
  child_titles: childTasks.map(child => child.title),
313
- next_task_suggestion: review.next_task,
552
+ next_task_suggestion: review ? review.next_task : null,
314
553
  },
315
554
  };
316
555
  });
@@ -334,12 +573,19 @@ function taskTypeForCloud(task) {
334
573
  }
335
574
 
336
575
  function taskStateForCloud(task) {
576
+ if (task.status === 'review') return 'doing';
337
577
  if (task.status === 'claimed') return 'doing';
578
+ if (task.status === 'failed' && taskHasReview(task)) return 'done';
338
579
  if (task.status === 'failed') return 'blocked';
339
580
  if (task.status === 'done') return 'done';
340
581
  return 'open';
341
582
  }
342
583
 
584
+ function taskNeedsApprovalForCloud(task) {
585
+ const approvalStatus = task?.review?.approval_status || task?.metadata?.approval_status || null;
586
+ return task?.status === 'review' || approvalStatus === 'pending';
587
+ }
588
+
343
589
  function ownerMemberIdForCloud(task) {
344
590
  const ownerValue = task.claimed_by || taskAssignee(task);
345
591
  if (!ownerValue) return null;
@@ -366,6 +612,10 @@ function taskDescriptionForCloud(task) {
366
612
  if (reviewed.payload.proof) lines.push('', `Proof: ${reviewed.payload.proof}`);
367
613
  if (reviewed.payload.lesson) lines.push(`Lesson: ${reviewed.payload.lesson}`);
368
614
  if (reviewed.payload.next_task) lines.push(`Next: ${reviewed.payload.next_task}`);
615
+ } else if (task.review && task.review.proof) {
616
+ lines.push('', `Proof: ${task.review.proof}`);
617
+ if (task.review.lesson) lines.push(`Lesson: ${task.review.lesson}`);
618
+ if (task.review.next_task) lines.push(`Next: ${task.review.next_task}`);
369
619
  }
370
620
  return lines.join('\n').slice(0, 5000);
371
621
  }
@@ -378,7 +628,7 @@ function cloudPayloadForTask(task, businessId) {
378
628
  title: String(task.title || '').slice(0, 200),
379
629
  description: taskDescriptionForCloud(task),
380
630
  owner_member_id: ownerMemberIdForCloud(task),
381
- needs_approval: false,
631
+ needs_approval: taskNeedsApprovalForCloud(task),
382
632
  metadata: {
383
633
  ...metadata,
384
634
  source: 'atris_cli_task',
@@ -439,9 +689,106 @@ function latestTaskEvent(task) {
439
689
  return events.length ? events[events.length - 1] : null;
440
690
  }
441
691
 
442
- function taskStatusSummary(projection) {
692
+ function reviewHandoffForTask(task) {
693
+ const review = task && task.review || {};
694
+ if (task && task.status !== 'review') return null;
695
+ if (review.approval_status !== 'pending') return null;
696
+ const agentCertified = review.agent_certified === true;
697
+ return {
698
+ native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
699
+ career_xp_status: 'pending_human_accept',
700
+ next_action: agentCertified ? 'continue_work' : 'agent_review_again',
701
+ };
702
+ }
703
+
704
+ function compactTaskForStatus(task) {
705
+ if (!task) return null;
706
+ const metadata = task.metadata || {};
707
+ const out = {
708
+ id: task.id,
709
+ display_id: task.display_id || null,
710
+ legacy_ref: task.legacy_ref || taskRef(task.id),
711
+ title: clipStatusText(task.title, 140),
712
+ status: task.status,
713
+ updated_at: task.updated_at,
714
+ };
715
+ if (task.tag) out.tag = task.tag;
716
+ if (task.claimed_by) out.claimed_by = task.claimed_by;
717
+ const assignedTo = taskAssignee(task);
718
+ if (assignedTo) out.assigned_to = assignedTo;
719
+ if (task.latest_event_type) out.latest_event_type = task.latest_event_type;
720
+ if (task.objective) out.objective = clipStatusText(task.objective, 180);
721
+ if (task.review) {
722
+ const review = {};
723
+ if (typeof task.review.reward === 'number') review.reward = task.review.reward;
724
+ else if (task.review.reward === null) review.reward = null;
725
+ if (task.review.summary) review.summary = clipStatusText(task.review.summary, 240);
726
+ if (task.review.proof) review.proof = clipStatusText(task.review.proof, 180);
727
+ if (task.review.lesson) review.lesson = clipStatusText(task.review.lesson, 180);
728
+ if (task.review.next_task) review.next_task = clipStatusText(task.review.next_task, 140);
729
+ if (task.review.approval_status) review.approval_status = task.review.approval_status;
730
+ if (task.review.agent_review_pass_count) review.agent_review_pass_count = task.review.agent_review_pass_count;
731
+ if (task.review.agent_certified) review.agent_certified = task.review.agent_certified;
732
+ if (task.review.agent_certification_policy) review.agent_certification_policy = task.review.agent_certification_policy;
733
+ if (task.review.human_revision_count) review.human_revision_count = task.review.human_revision_count;
734
+ const handoff = reviewHandoffForTask(task);
735
+ if (handoff) review.handoff = handoff;
736
+ if (Object.keys(review).length) out.review = review;
737
+ }
738
+ if (task.lineage) {
739
+ const lineage = {};
740
+ if (task.lineage.parent_task_id) lineage.parent_task_id = task.lineage.parent_task_id;
741
+ if (task.lineage.parent_title) lineage.parent_title = clipStatusText(task.lineage.parent_title, 140);
742
+ if (task.lineage.child_task_ids && task.lineage.child_task_ids.length) lineage.child_task_ids = task.lineage.child_task_ids;
743
+ if (task.lineage.next_task_suggestion) lineage.next_task_suggestion = clipStatusText(task.lineage.next_task_suggestion, 140);
744
+ if (Object.keys(lineage).length) out.lineage = lineage;
745
+ }
746
+ const compactMetadata = {};
747
+ for (const key of ['todo_id', 'stage', 'verify', 'delegate_via', 'goal_id', 'goal_objective', 'approval_status', 'agent_review_pass_count', 'agent_certified', 'agent_certification_policy', 'human_revision_count', 'human_revision_note']) {
748
+ if (metadata[key]) compactMetadata[key] = key === 'verify' ? clipStatusText(metadata[key], 180) : metadata[key];
749
+ }
750
+ if (Object.keys(compactMetadata).length) out.metadata = compactMetadata;
751
+ return out;
752
+ }
753
+
754
+ function compactTaskFromProjection(projection, id) {
755
+ return compactTaskForStatus(taskFromProjection(projection, id));
756
+ }
757
+
758
+ function compactEventPayload(payload) {
759
+ if (!payload || typeof payload !== 'object') return null;
760
+ const out = {};
761
+ for (const key of ['title', 'status', 'tag', 'content', 'proof', 'lesson', 'reward', 'next_task']) {
762
+ if (payload[key] !== undefined && payload[key] !== null && payload[key] !== '') out[key] = payload[key];
763
+ }
764
+ return Object.keys(out).length ? out : null;
765
+ }
766
+
767
+ function compactTaskEvent(event) {
768
+ if (!event) return null;
769
+ return {
770
+ event_id: event.event_id,
771
+ task_id: event.task_id,
772
+ version: event.version,
773
+ actor: event.actor || null,
774
+ event_type: event.event_type,
775
+ created_at: event.created_at,
776
+ payload: compactEventPayload(event.payload),
777
+ };
778
+ }
779
+
780
+ function clipStatusText(value, max = 180) {
781
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
782
+ if (text.length <= max) return text;
783
+ return `${text.slice(0, max - 1)}…`;
784
+ }
785
+
786
+ function taskStatusSummary(projection, { history = false } = {}) {
443
787
  const tasks = projection.tasks || [];
788
+ const hiddenDoneCount = Math.max(0, Number(projection.surface && projection.surface.hidden_done_count || 0));
789
+ const fullTaskCount = Math.max(tasks.length + hiddenDoneCount, Number(projection.surface && projection.surface.full_task_count || 0));
444
790
  const columns = {
791
+ backlog: tasks.filter(task => taskColumn(task) === 'backlog'),
445
792
  plan: tasks.filter(task => taskColumn(task) === 'open'),
446
793
  do: tasks.filter(task => taskColumn(task) === 'doing'),
447
794
  review: tasks.filter(task => taskColumn(task) === 'review' || taskColumn(task) === 'blocked'),
@@ -449,10 +796,10 @@ function taskStatusSummary(projection) {
449
796
  };
450
797
  const active = [...columns.do, ...columns.review, ...columns.plan];
451
798
  const lastUpdated = tasks.reduce((max, task) => Math.max(max, Number(task.updated_at || 0)), 0);
452
- const swarloFeed = tasks
799
+ const swarloFeed = history ? tasks
453
800
  .flatMap(task => (task.events || []).map(event => ({
454
801
  task_id: task.id,
455
- task_title: task.title,
802
+ task_title: clipStatusText(task.title, 120),
456
803
  actor: event.actor || task.claimed_by || null,
457
804
  kind: event.event_type === 'claimed'
458
805
  ? 'claim'
@@ -460,8 +807,11 @@ function taskStatusSummary(projection) {
460
807
  ? 'result'
461
808
  : 'note',
462
809
  channel: task.tag || 'tasks',
463
- content: event.payload && (event.payload.content || event.payload.proof || event.payload.lesson)
464
- || humanEventType(event.event_type),
810
+ content: clipStatusText(
811
+ event.payload && (event.payload.content || event.payload.proof || event.payload.lesson)
812
+ || humanEventType(event.event_type),
813
+ 180,
814
+ ),
465
815
  created_at: event.created_at,
466
816
  metadata: {
467
817
  swarlo: {
@@ -472,23 +822,24 @@ function taskStatusSummary(projection) {
472
822
  },
473
823
  })))
474
824
  .sort((a, b) => b.created_at - a.created_at)
475
- .slice(0, 12);
476
- return {
825
+ .slice(0, 12) : [];
826
+ const status = {
477
827
  schema: 'atris.task_status.v1',
478
828
  generated_at: projection.generated_at,
479
829
  workspace_root: projection.workspace_root,
480
830
  goals: projection.goals || { source_path: null, items: [] },
481
831
  counts: {
482
- total: tasks.length,
483
- active: tasks.filter(task => task.status !== 'done').length,
832
+ total: fullTaskCount,
833
+ active: columns.plan.length + columns.do.length + columns.review.length,
834
+ backlog: columns.backlog.length,
484
835
  plan: columns.plan.length,
485
836
  do: columns.do.length,
486
837
  review: columns.review.length,
487
- done: columns.done.length,
838
+ done: tasks.filter(task => task.status === 'done' || (task.status === 'failed' && taskHasReview(task))).length + hiddenDoneCount,
488
839
  },
489
- current: columns.do[0] || columns.review[0] || null,
490
- next: columns.plan[0] || null,
491
- needs_review: columns.review.slice(0, 5),
840
+ current: compactTaskForStatus(columns.do[0] || columns.review[0] || null),
841
+ next: compactTaskForStatus(columns.plan[0] || null),
842
+ needs_review: columns.review.slice(0, 5).map(compactTaskForStatus),
492
843
  streams: (projection.streams || []).slice(0, 8).map(stream => ({
493
844
  objective: stream.objective,
494
845
  active_count: stream.active_count,
@@ -498,38 +849,75 @@ function taskStatusSummary(projection) {
498
849
  review_count: stream.review_count,
499
850
  blocked_count: stream.blocked_count,
500
851
  })),
501
- last_event: active.map(task => ({ task, event: latestTaskEvent(task) })).filter(row => row.event)
502
- .sort((a, b) => b.event.created_at - a.event.created_at)[0] || null,
503
852
  last_updated_at: lastUpdated ? new Date(lastUpdated).toISOString() : null,
504
- swarlo: {
853
+ };
854
+ if (history) {
855
+ status.last_event = active.map(task => ({ task: compactTaskForStatus(task), event: compactTaskEvent(latestTaskEvent(task)) })).filter(row => row.event)
856
+ .sort((a, b) => b.event.created_at - a.event.created_at)[0] || null;
857
+ status.swarlo = {
505
858
  feed: swarloFeed,
506
859
  realtime_contract: {
507
860
  claim: 'Swarlo claim -> canonical task state=doing + lease metadata',
508
861
  report_done: 'Swarlo report(done) -> canonical task state=done + proof metadata',
509
862
  web: 'atrisos-web reads canonical tasks through /api/agent/:id/tasks or /api/business/* and live activity through public business/Swarlo posts',
510
863
  },
511
- },
512
- };
864
+ };
865
+ }
866
+ return status;
513
867
  }
514
868
 
515
869
  function humanEventType(type) {
516
870
  return String(type || 'event').replace(/_/g, ' ');
517
871
  }
518
872
 
873
+ function taskEventSummary(event) {
874
+ const payload = event && event.payload || {};
875
+ const raw = payload.content || payload.proof || payload.lesson || payload.title || payload.status || humanEventType(event && event.event_type);
876
+ return clipStatusText(raw, 140);
877
+ }
878
+
879
+ function formatTaskEventCompact(event, refById = new Map()) {
880
+ const actor = event.actor ? ` @${event.actor}` : '';
881
+ const when = event.created_at ? new Date(Number(event.created_at)).toISOString() : '';
882
+ return `${when}\t${event.event_type.padEnd(9)}\t${refById.get(event.task_id) || taskRef(event.task_id)}${actor}\t${taskEventSummary(event)}`;
883
+ }
884
+
885
+ function normalizedStatusPart(value) {
886
+ return String(value || '').trim().toLowerCase().replace(/\s+/g, '-');
887
+ }
888
+
889
+ function taskIsPlannedOpen(task) {
890
+ const metadata = task && task.metadata || {};
891
+ const tag = normalizedStatusPart(task && task.tag);
892
+ const stage = normalizedStatusPart(metadata.stage);
893
+ return STATUS_PLAN_TAGS.has(tag)
894
+ || STATUS_PLAN_TAGS.has(stage)
895
+ || Boolean(metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at);
896
+ }
897
+
519
898
  function formatTaskLine(task) {
520
899
  if (!task) return 'none';
521
900
  const owner = task.claimed_by ? ` @${task.claimed_by}` : '';
522
901
  const assigned = !task.claimed_by && taskAssignee(task) ? ` -> ${taskAssignee(task)}` : '';
523
902
  const tag = task.tag ? ` #${task.tag}` : '';
524
- return `${task.id.slice(0, 8)}${owner}${assigned}${tag} ${task.title}`;
903
+ return `${taskRef(task)}${owner}${assigned}${tag} ${task.title}`;
525
904
  }
526
905
 
527
906
  function cmdStatus(args) {
528
907
  const all = hasFlag(args, '--all');
908
+ const history = hasFlag(args, '--history');
529
909
  const taskDb = getTaskDb();
530
910
  const db = taskDb.open();
531
- const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
532
- const status = taskStatusSummary(projection);
911
+ const compact = writeDefaultProjection(taskDb, db, { all });
912
+ const projection = history
913
+ ? enrichTaskProjection(taskDb.taskProjection(db, {
914
+ workspaceRoot: all ? null : taskDb.workspaceRoot(),
915
+ limit: 500,
916
+ includeHistory: true,
917
+ }))
918
+ : compact.projection;
919
+ const outPath = compact.outPath;
920
+ const status = taskStatusSummary(projection, { history });
533
921
  if (wantsJson(args)) {
534
922
  printJson({
535
923
  ok: true,
@@ -541,14 +929,14 @@ function cmdStatus(args) {
541
929
  }
542
930
  console.log('TASK STATUS');
543
931
  console.log(`workspace ${status.workspace_root || '(all)'}`);
544
- console.log(`plan ${status.counts.plan} / do ${status.counts.do} / review ${status.counts.review} / done ${status.counts.done}`);
932
+ console.log(`plan ${status.counts.plan} / do ${status.counts.do} / review ${status.counts.review} / backlog ${status.counts.backlog} / done ${status.counts.done}`);
545
933
  console.log(`current ${formatTaskLine(status.current)}`);
546
934
  console.log(`next ${formatTaskLine(status.next)}`);
547
935
  if (status.needs_review.length) {
548
936
  console.log('review');
549
937
  for (const task of status.needs_review.slice(0, 3)) console.log(` ${formatTaskLine(task)}`);
550
938
  }
551
- console.log(`swarlo feed ${status.swarlo.feed.length} event${status.swarlo.feed.length === 1 ? '' : 's'}`);
939
+ if (history) console.log(`history feed ${status.swarlo.feed.length} event${status.swarlo.feed.length === 1 ? '' : 's'}`);
552
940
  }
553
941
 
554
942
  function resolveTaskRef(taskDb, db, ref) {
@@ -556,8 +944,18 @@ function resolveTaskRef(taskDb, db, ref) {
556
944
  if (!token) return { ok: false, reason: 'missing' };
557
945
  const exact = taskDb.getTask(db, token);
558
946
  if (exact) return { ok: true, id: exact.id, row: exact };
559
- const rows = taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot(), limit: 500 });
560
- const matches = rows.filter(r => r.id.startsWith(token));
947
+ const normalized = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(token) : token.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
948
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot() }));
949
+ const seen = new Set();
950
+ const matches = rows.filter(r => {
951
+ const id = String(r.id || '').toUpperCase();
952
+ const display = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.display_id) : String(r.display_id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
953
+ const legacy = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.legacy_ref) : String(r.legacy_ref || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
954
+ const matched = id.startsWith(normalized) || display === normalized || legacy === normalized;
955
+ if (!matched || seen.has(r.id)) return false;
956
+ seen.add(r.id);
957
+ return true;
958
+ });
561
959
  if (matches.length === 1) return { ok: true, id: matches[0].id, row: matches[0] };
562
960
  if (matches.length > 1) return { ok: false, reason: 'ambiguous', matches };
563
961
  return { ok: false, reason: 'not_found' };
@@ -575,9 +973,14 @@ function requireTaskId(taskDb, db, ref, label) {
575
973
  }
576
974
  }
577
975
 
578
- function renderTaskDesk(rows) {
579
- const active = rows.filter(r => r.status !== 'done');
580
- const done = rows.filter(r => r.status === 'done');
976
+ function workspaceRefRows(taskDb, db, all = false) {
977
+ return taskDb.listTasks(db, { workspaceRoot: all ? null : taskDb.workspaceRoot() });
978
+ }
979
+
980
+ function renderTaskDesk(rows, refRows = rows) {
981
+ const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
982
+ const active = displayRows.filter(r => r.status !== 'done');
983
+ const done = displayRows.filter(r => r.status === 'done');
581
984
  if (rows.length === 0) {
582
985
  console.log('No tasks yet.');
583
986
  console.log('Start with: atris task new "Ship the smallest useful thing"');
@@ -589,7 +992,7 @@ function renderTaskDesk(rows) {
589
992
  const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
590
993
  const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
591
994
  const tag = r.tag ? ` #${r.tag}` : '';
592
- console.log(`${r.status.padEnd(7)} ${r.id.slice(0, 8)}${owner}${assigned}${tag}`);
995
+ console.log(`${r.status.padEnd(7)} ${taskRef(r)}${owner}${assigned}${tag}`);
593
996
  console.log(` ${r.title}`);
594
997
  }
595
998
  if (active.length === 0) console.log('clear no active tasks');
@@ -602,10 +1005,14 @@ function cmdAdd(args) {
602
1005
  const pos = positional(args);
603
1006
  const title = pos.join(' ').trim();
604
1007
  if (!title) {
605
- console.error('atris task add: title required');
606
- process.exit(2);
1008
+ failTask('atris task add', 'missing_title', 'title required');
607
1009
  }
608
1010
  const tag = flag(args, '--tag');
1011
+ const goalId = flag(args, '--goal-id');
1012
+ const goalObjective = flag(args, '--goal-objective') || flag(args, '--goal');
1013
+ const metadata = {};
1014
+ if (goalId && goalId !== true) metadata.goal_id = String(goalId);
1015
+ if (goalObjective && goalObjective !== true) metadata.goal_objective = String(goalObjective);
609
1016
  const taskDb = getTaskDb();
610
1017
  const db = taskDb.open();
611
1018
  const ws = taskDb.workspaceRoot();
@@ -613,8 +1020,10 @@ function cmdAdd(args) {
613
1020
  title,
614
1021
  tag: typeof tag === 'string' ? tag : null,
615
1022
  workspaceRoot: ws,
1023
+ metadata: Object.keys(metadata).length ? metadata : null,
616
1024
  });
617
1025
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1026
+ const task = compactTaskFromProjection(projection, result.id);
618
1027
  if (wantsJson(args)) {
619
1028
  printJson({
620
1029
  ok: true,
@@ -622,21 +1031,21 @@ function cmdAdd(args) {
622
1031
  task_id: result.id,
623
1032
  inserted: result.inserted !== false,
624
1033
  projection_path: outPath,
625
- task: taskFromProjection(projection, result.id),
1034
+ task,
626
1035
  });
627
1036
  return;
628
1037
  }
629
- console.log(`${result.id}\t${title}`);
1038
+ console.log(`${taskRef(task)}\t${title}`);
630
1039
  }
631
1040
 
632
- function delegateHandoff(taskId, owner, via, tag) {
633
- const shortId = taskId.slice(0, 8);
1041
+ function delegateHandoff(task, owner, via, tag) {
1042
+ const ref = taskRef(task);
634
1043
  const handoff = {
635
- command: `atris task claim ${shortId} --as ${owner}`,
1044
+ command: `atris task claim ${ref} --as ${owner}`,
636
1045
  };
637
1046
  if (via === 'swarlo') {
638
1047
  handoff.swarlo = {
639
- task_key: taskId,
1048
+ task_key: task.id,
640
1049
  action: 'claim',
641
1050
  channel: tag || 'tasks',
642
1051
  assignee: owner,
@@ -650,12 +1059,10 @@ function cmdDelegate(args) {
650
1059
  const title = pos.join(' ').trim();
651
1060
  const owner = flag(args, '--to') || flag(args, '--as');
652
1061
  if (!title) {
653
- console.error('atris task delegate: title required');
654
- process.exit(2);
1062
+ failTask('atris task delegate', 'missing_title', 'title required');
655
1063
  }
656
1064
  if (!owner || owner === true) {
657
- console.error('atris task delegate: --to <owner> required');
658
- process.exit(2);
1065
+ failTask('atris task delegate', 'missing_owner', '--to <owner> required');
659
1066
  }
660
1067
  const viaFlag = flag(args, '--via');
661
1068
  const via = viaFlag === 'swarlo' ? 'swarlo' : 'local';
@@ -683,8 +1090,8 @@ function cmdDelegate(args) {
683
1090
  taskDb.noteTask(db, { id: result.id, actor: DEFAULT_OWNER, content: note });
684
1091
  }
685
1092
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
686
- const task = taskFromProjection(projection, result.id);
687
- const handoff = delegateHandoff(result.id, String(owner), via, typeof tag === 'string' ? tag : null);
1093
+ const task = compactTaskFromProjection(projection, result.id);
1094
+ const handoff = delegateHandoff(task, String(owner), via, typeof tag === 'string' ? tag : null);
688
1095
  if (wantsJson(args)) {
689
1096
  printJson({
690
1097
  ok: true,
@@ -700,7 +1107,7 @@ function cmdDelegate(args) {
700
1107
  return;
701
1108
  }
702
1109
  const tagText = tag && tag !== true ? ` #${tag}` : '';
703
- console.log(`delegated ${result.id.slice(0, 8)} -> ${owner}${tagText} via=${via}`);
1110
+ console.log(`delegated ${taskRef(task)} -> ${owner}${tagText} via=${via}`);
704
1111
  console.log(`claim: ${handoff.command}`);
705
1112
  if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
706
1113
  }
@@ -739,7 +1146,7 @@ function cmdDay(args) {
739
1146
  owners: groups.length,
740
1147
  open: (projection.tasks || []).filter(task => task.status === 'open').length,
741
1148
  claimed: (projection.tasks || []).filter(task => task.status === 'claimed').length,
742
- review: (projection.tasks || []).filter(task => task.status === 'failed' || (task.status === 'done' && task.review && task.review.reward === null)).length,
1149
+ review: (projection.tasks || []).filter(task => task.status === 'review' || task.status === 'failed' || (task.status === 'done' && task.review && task.review.reward === null)).length,
743
1150
  };
744
1151
  const date = new Date().toISOString().slice(0, 10);
745
1152
  if (wantsJson(args)) {
@@ -764,7 +1171,7 @@ function cmdDay(args) {
764
1171
  for (const task of group.tasks.slice(0, 8)) {
765
1172
  const tag = task.tag ? ` #${task.tag}` : '';
766
1173
  const claim = task.claimed_by ? ` @${task.claimed_by}` : '';
767
- console.log(` ${task.status.padEnd(7)} ${task.id.slice(0, 8)}${claim}${tag} ${task.title}`);
1174
+ console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
768
1175
  }
769
1176
  }
770
1177
  console.log('');
@@ -791,7 +1198,7 @@ function cmdHome(args) {
791
1198
  });
792
1199
  return;
793
1200
  }
794
- renderTaskDesk(rows);
1201
+ renderTaskDesk(rows, rows);
795
1202
  }
796
1203
 
797
1204
  function cmdList(args) {
@@ -804,18 +1211,19 @@ function cmdList(args) {
804
1211
  status: typeof status === 'string' ? status : null,
805
1212
  limit: 200,
806
1213
  });
1214
+ const displayRows = taskDb.withTaskDisplayRefs(rows, workspaceRefRows(taskDb, db, all));
807
1215
  if (wantsJson(args)) {
808
- printJson({ ok: true, action: 'list', tasks: rows });
1216
+ printJson({ ok: true, action: 'list', tasks: displayRows });
809
1217
  return;
810
1218
  }
811
1219
  if (rows.length === 0) {
812
1220
  console.log('(no tasks)');
813
1221
  return;
814
1222
  }
815
- for (const r of rows) {
1223
+ for (const r of displayRows) {
816
1224
  const claim = r.claimed_by ? ` [${r.claimed_by}]` : '';
817
1225
  const tag = r.tag ? ` #${r.tag}` : '';
818
- console.log(`${r.status.padEnd(8)} ${r.id}${claim}${tag}\t${r.title}`);
1226
+ console.log(`${r.status.padEnd(8)} ${taskRef(r)}${claim}${tag}\t${r.title}`);
819
1227
  }
820
1228
  }
821
1229
 
@@ -823,8 +1231,7 @@ function cmdClaim(args) {
823
1231
  const pos = positional(args);
824
1232
  const id = pos[0];
825
1233
  if (!id) {
826
- console.error('atris task claim: id required');
827
- process.exit(2);
1234
+ failTask('atris task claim', 'missing_id', 'id required');
828
1235
  }
829
1236
  const owner = flag(args, '--as') || DEFAULT_OWNER;
830
1237
  const taskDb = getTaskDb();
@@ -840,12 +1247,22 @@ function cmdClaim(args) {
840
1247
  task_id: taskId,
841
1248
  owner: String(owner),
842
1249
  projection_path: outPath,
843
- task: taskFromProjection(projection, taskId),
1250
+ task: compactTaskFromProjection(projection, taskId),
844
1251
  });
845
1252
  return;
846
1253
  }
847
- console.log(`claimed ${taskId} as ${owner}`);
1254
+ console.log(`claimed ${taskRef(compactTaskFromProjection(projection, taskId))} as ${owner}`);
848
1255
  } else {
1256
+ if (wantsJson(args)) {
1257
+ printJson({
1258
+ ok: false,
1259
+ command: 'atris task claim',
1260
+ reason: result.reason,
1261
+ claimed_by: result.claimed_by || null,
1262
+ detail: `claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`,
1263
+ });
1264
+ process.exit(1);
1265
+ }
849
1266
  console.error(`claim failed: ${result.reason}${result.claimed_by ? ` (held by ${result.claimed_by})` : ''}`);
850
1267
  process.exit(1);
851
1268
  }
@@ -870,21 +1287,68 @@ function cmdNext(args) {
870
1287
  task_id: claimed[0].id,
871
1288
  owner: String(owner),
872
1289
  projection_path: outPath,
873
- task: taskFromProjection(projection, claimed[0].id),
1290
+ task: compactTaskFromProjection(projection, claimed[0].id),
874
1291
  });
875
1292
  return;
876
1293
  }
877
- console.log(`current ${claimed[0].id.slice(0, 8)} @${owner}`);
1294
+ console.log(`current ${taskRef(compactTaskFromProjection(projection, claimed[0].id))} @${owner}`);
878
1295
  console.log(claimed[0].title);
879
1296
  return;
880
1297
  }
1298
+ const reviewProjection = writeDefaultProjection(taskDb, db);
1299
+ const reviewTasks = (reviewProjection.projection.tasks || [])
1300
+ .map(compactTaskForStatus)
1301
+ .filter(task => task && task.review && task.review.handoff);
1302
+ const secondReviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'agent_review_again');
1303
+ if (secondReviewTask) {
1304
+ const handoff = secondReviewTask.review.handoff;
1305
+ if (wantsJson(args)) {
1306
+ printJson({
1307
+ ok: true,
1308
+ action: handoff.next_action,
1309
+ task_id: secondReviewTask.id,
1310
+ owner: String(owner),
1311
+ projection_path: reviewProjection.outPath,
1312
+ handoff,
1313
+ review_task: secondReviewTask,
1314
+ });
1315
+ return;
1316
+ }
1317
+ console.log(`${taskRef(secondReviewTask)} needs one more agent review before continuation.`);
1318
+ console.log('Review this task again before claiming new work.');
1319
+ return;
1320
+ }
881
1321
  const open = taskDb.listTasks(db, {
882
1322
  workspaceRoot: taskDb.workspaceRoot(),
883
1323
  status: 'open',
884
1324
  limit: 1,
885
1325
  });
886
1326
  if (!open.length) {
887
- const { outPath } = writeDefaultProjection(taskDb, db);
1327
+ const { projection, outPath } = reviewProjection;
1328
+ const reviewTask = reviewTasks.find(task => task.review.handoff.next_action === 'continue_work');
1329
+ if (reviewTask) {
1330
+ const handoff = reviewTask.review.handoff;
1331
+ if (wantsJson(args)) {
1332
+ printJson({
1333
+ ok: true,
1334
+ action: handoff.next_action,
1335
+ task_id: handoff.next_action === 'agent_review_again' ? reviewTask.id : null,
1336
+ owner: String(owner),
1337
+ projection_path: outPath,
1338
+ handoff,
1339
+ review_task: reviewTask,
1340
+ });
1341
+ return;
1342
+ }
1343
+ console.log('No open tasks.');
1344
+ console.log(handoff.next_action === 'continue_work'
1345
+ ? `${taskRef(reviewTask)} is agent-certified and waiting for human accept.`
1346
+ : `${taskRef(reviewTask)} needs one more agent review before continuation.`);
1347
+ console.log(handoff.next_action === 'continue_work'
1348
+ ? 'Continue work elsewhere; AgentXP waits for human accept.'
1349
+ : 'Review this task again before continuing.');
1350
+ return;
1351
+ }
888
1352
  if (wantsJson(args)) {
889
1353
  printJson({
890
1354
  ok: true,
@@ -912,11 +1376,11 @@ function cmdNext(args) {
912
1376
  task_id: open[0].id,
913
1377
  owner: String(owner),
914
1378
  projection_path: outPath,
915
- task: taskFromProjection(projection, open[0].id),
1379
+ task: compactTaskFromProjection(projection, open[0].id),
916
1380
  });
917
1381
  return;
918
1382
  }
919
- console.log(`next ${open[0].id.slice(0, 8)} @${owner}`);
1383
+ console.log(`next ${taskRef(compactTaskFromProjection(projection, open[0].id))} @${owner}`);
920
1384
  console.log(open[0].title);
921
1385
  }
922
1386
 
@@ -925,8 +1389,7 @@ function cmdNote(args) {
925
1389
  const id = pos[0];
926
1390
  const content = pos.slice(1).join(' ').trim();
927
1391
  if (!id || !content) {
928
- console.error('atris task note: id and message required');
929
- process.exit(2);
1392
+ failTask('atris task note', 'missing_args', 'id and message required');
930
1393
  }
931
1394
  const actor = flag(args, '--as') || DEFAULT_OWNER;
932
1395
  const taskDb = getTaskDb();
@@ -945,37 +1408,45 @@ function cmdNote(args) {
945
1408
  task_id: taskId,
946
1409
  version: result.event.version,
947
1410
  projection_path: outPath,
948
- task: taskFromProjection(projection, taskId),
1411
+ task: compactTaskFromProjection(projection, taskId),
949
1412
  });
950
1413
  return;
951
1414
  }
952
- console.log(`noted ${taskId} v${result.event.version}`);
1415
+ console.log(`noted ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
953
1416
  }
954
1417
 
955
1418
  function cmdShow(args) {
956
1419
  const pos = positional(args);
957
1420
  const id = pos[0];
958
1421
  if (!id) {
959
- console.error('atris task show: id required');
960
- process.exit(2);
1422
+ failTask('atris task show', 'missing_id', 'id required');
961
1423
  }
962
1424
  const taskDb = getTaskDb();
963
1425
  const db = taskDb.open();
964
1426
  const taskId = requireTaskId(taskDb, db, id, 'atris task show');
965
- const projection = taskDb.taskProjection(db, { taskId });
1427
+ const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
966
1428
  const task = projection.tasks[0];
967
1429
  if (!task) {
968
1430
  console.error(`task not found: ${id}`);
969
1431
  process.exit(1);
970
1432
  }
971
1433
  if (hasFlag(args, '--json')) {
972
- console.log(JSON.stringify(task, null, 2));
1434
+ printJson(task);
973
1435
  return;
974
1436
  }
975
1437
  const owner = task.claimed_by ? ` / ${task.claimed_by}` : '';
976
1438
  const tag = task.tag ? ` #${task.tag}` : '';
977
- console.log(`${task.status.toUpperCase()} ${task.id} v${task.current_version}${owner}${tag}`);
1439
+ console.log(`${task.status.toUpperCase()} ${taskRef(task)} v${task.current_version}${owner}${tag}`);
978
1440
  console.log(task.title);
1441
+ if (task.review) {
1442
+ console.log('');
1443
+ if (task.review.summary) console.log(`Summary: ${task.review.summary}`);
1444
+ if (task.review.proof) console.log(`Proof: ${task.review.proof}`);
1445
+ if (task.review.lesson) console.log(`Lesson: ${task.review.lesson}`);
1446
+ if (task.review.next_task) console.log(`Next: ${task.review.next_task}`);
1447
+ if (task.review.approval_status) console.log(`Approval: ${task.review.approval_status}`);
1448
+ if (task.review.agent_certified) console.log(`Agent certified: yes (${task.review.agent_review_pass_count || AGENT_CERTIFICATION_REVIEW_PASSES} reviews)`);
1449
+ }
979
1450
  if (task.messages.length) {
980
1451
  console.log('');
981
1452
  console.log('Dialogue:');
@@ -990,29 +1461,60 @@ function cmdDone(args) {
990
1461
  const pos = positional(args);
991
1462
  const id = pos[0];
992
1463
  if (!id) {
993
- console.error('atris task done: id required');
994
- process.exit(2);
1464
+ failTask('atris task done', 'missing_id', 'id required');
995
1465
  }
996
1466
  const failed = hasFlag(args, '--failed');
997
1467
  const taskDb = getTaskDb();
998
1468
  const db = taskDb.open();
999
1469
  const taskId = requireTaskId(taskDb, db, id, 'atris task done');
1000
- const result = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done' });
1470
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
1471
+ const result = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done', actor });
1001
1472
  if (result.updated) {
1473
+ const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
1474
+ const review = hasReview ? taskDb.reviewTask(db, {
1475
+ id: taskId,
1476
+ actor,
1477
+ reward: flag(args, '--reward') || (failed ? 0 : 1),
1478
+ lesson: typeof flag(args, '--lesson') === 'string' ? flag(args, '--lesson') : '',
1479
+ nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
1480
+ proof: typeof flag(args, '--proof') === 'string' ? flag(args, '--proof') : '',
1481
+ careerXpEligible: false,
1482
+ }) : null;
1483
+ const xpProjection = refreshCareerXpAfterReview(review);
1002
1484
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1003
1485
  if (wantsJson(args)) {
1004
1486
  printJson({
1005
1487
  ok: true,
1006
1488
  action: failed ? 'failed' : 'done',
1007
1489
  task_id: taskId,
1490
+ reviewed: Boolean(review && review.reviewed),
1491
+ reward: review && review.episode ? review.episode.reward.value : null,
1492
+ episode: review && review.episode || null,
1493
+ xp_projection: xpProjection,
1008
1494
  projection_path: outPath,
1009
- task: taskFromProjection(projection, taskId),
1495
+ task: compactTaskFromProjection(projection, taskId),
1010
1496
  });
1011
1497
  return;
1012
1498
  }
1013
- console.log(`${failed ? 'failed' : 'done'} ${taskId}`);
1499
+ const task = compactTaskFromProjection(projection, taskId);
1500
+ if (review && review.reviewed) {
1501
+ console.log(`${failed ? 'failed' : 'done'} ${taskRef(task)} reward=${review.episode.reward.value}`);
1502
+ } else {
1503
+ console.log(`${failed ? 'failed' : 'done'} ${taskRef(task)}`);
1504
+ }
1014
1505
  } else {
1015
- console.error(`done failed: ${taskId} not in open|claimed`);
1506
+ const detail = `done failed: ${taskId} not in open|claimed`;
1507
+ if (wantsJson(args)) {
1508
+ printJson({
1509
+ ok: false,
1510
+ command: 'atris task done',
1511
+ reason: 'not_open_or_claimed',
1512
+ task_id: taskId,
1513
+ detail,
1514
+ });
1515
+ process.exit(1);
1516
+ }
1517
+ console.error(detail);
1016
1518
  process.exit(1);
1017
1519
  }
1018
1520
  }
@@ -1021,29 +1523,42 @@ function cmdFinish(args) {
1021
1523
  const pos = positional(args);
1022
1524
  const id = pos[0];
1023
1525
  if (!id) {
1024
- console.error('atris task finish: id required');
1025
- process.exit(2);
1526
+ failTask('atris task finish', 'missing_id', 'id required');
1026
1527
  }
1027
1528
  const taskDb = getTaskDb();
1028
1529
  const db = taskDb.open();
1029
1530
  const taskId = requireTaskId(taskDb, db, id, 'atris task finish');
1030
1531
  const currentTask = taskDb.getTask(db, taskId);
1031
- const done = taskDb.doneTask(db, { id: taskId, status: hasFlag(args, '--failed') ? 'failed' : 'done' });
1532
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
1533
+ const done = taskDb.doneTask(db, { id: taskId, status: hasFlag(args, '--failed') ? 'failed' : 'done', actor });
1032
1534
  if (!done.updated) {
1033
- console.error(`finish failed: ${taskId} not in open|claimed`);
1535
+ const detail = `finish failed: ${taskId} not in open|claimed`;
1536
+ if (wantsJson(args)) {
1537
+ printJson({
1538
+ ok: false,
1539
+ command: 'atris task finish',
1540
+ reason: 'not_open_or_claimed',
1541
+ task_id: taskId,
1542
+ detail,
1543
+ });
1544
+ process.exit(1);
1545
+ }
1546
+ console.error(detail);
1034
1547
  process.exit(1);
1035
1548
  }
1036
1549
  const hasReview = hasFlag(args, '--review') || flag(args, '--lesson') || flag(args, '--next') || flag(args, '--proof') || flag(args, '--reward');
1037
1550
  if (hasReview) {
1038
1551
  const result = taskDb.reviewTask(db, {
1039
1552
  id: taskId,
1040
- actor: String(flag(args, '--as') || DEFAULT_OWNER),
1553
+ actor,
1041
1554
  reward: flag(args, '--reward') || 1,
1042
1555
  lesson: typeof flag(args, '--lesson') === 'string' ? flag(args, '--lesson') : '',
1043
1556
  nextTask: typeof flag(args, '--next') === 'string' ? flag(args, '--next') : '',
1044
1557
  proof: typeof flag(args, '--proof') === 'string' ? flag(args, '--proof') : '',
1558
+ careerXpEligible: false,
1045
1559
  });
1046
1560
  const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
1561
+ const xpProjection = refreshCareerXpAfterReview(result);
1047
1562
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1048
1563
  if (wantsJson(args)) {
1049
1564
  printJson({
@@ -1053,16 +1568,17 @@ function cmdFinish(args) {
1053
1568
  reviewed: true,
1054
1569
  reward: result.episode.reward.value,
1055
1570
  episode: result.episode,
1571
+ xp_projection: xpProjection,
1056
1572
  next_task_id: nextCreated ? nextCreated.id : null,
1057
1573
  projection_path: outPath,
1058
- projection,
1059
- task: taskFromProjection(projection, taskId),
1574
+ task: compactTaskFromProjection(projection, taskId),
1575
+ next_task: nextCreated ? compactTaskFromProjection(projection, nextCreated.id) : null,
1060
1576
  });
1061
1577
  return;
1062
1578
  }
1063
- console.log(`finished ${taskId} reward=${result.episode.reward.value}`);
1579
+ console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))} reward=${result.episode.reward.value}`);
1064
1580
  if (result.episode.next_task_suggestion) console.log(`next: ${result.episode.next_task_suggestion}`);
1065
- if (nextCreated) console.log(`created next ${nextCreated.id}`);
1581
+ if (nextCreated) console.log(`created next ${taskRef(compactTaskFromProjection(projection, nextCreated.id))}`);
1066
1582
  return;
1067
1583
  }
1068
1584
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
@@ -1073,20 +1589,195 @@ function cmdFinish(args) {
1073
1589
  task_id: taskId,
1074
1590
  reviewed: false,
1075
1591
  projection_path: outPath,
1076
- task: taskFromProjection(projection, taskId),
1592
+ task: compactTaskFromProjection(projection, taskId),
1077
1593
  });
1078
1594
  return;
1079
1595
  }
1080
- console.log(`finished ${taskId}`);
1596
+ console.log(`finished ${taskRef(compactTaskFromProjection(projection, taskId))}`);
1081
1597
  }
1082
1598
 
1083
- function cmdReview(args) {
1599
+ function cmdReady(args) {
1600
+ const pos = positional(args);
1601
+ const id = pos[0];
1602
+ if (!id) {
1603
+ console.error('atris task ready: id required');
1604
+ process.exit(2);
1605
+ }
1606
+ const proof = flag(args, '--proof');
1607
+ if (!proof || proof === true) {
1608
+ console.error('atris task ready: --proof required');
1609
+ process.exit(2);
1610
+ }
1611
+ const lesson = flag(args, '--lesson') || '';
1612
+ const nextTask = flag(args, '--next') || '';
1613
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
1614
+ const taskDb = getTaskDb();
1615
+ const db = taskDb.open();
1616
+ const taskId = requireTaskId(taskDb, db, id, 'atris task ready');
1617
+ const result = taskDb.readyTask(db, {
1618
+ id: taskId,
1619
+ actor,
1620
+ proof: String(proof),
1621
+ lesson: typeof lesson === 'string' ? lesson : '',
1622
+ nextTask: typeof nextTask === 'string' ? nextTask : '',
1623
+ });
1624
+ if (!result.ready) {
1625
+ console.error(`ready failed: ${result.reason}`);
1626
+ process.exit(1);
1627
+ }
1628
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
1629
+ const agentCertified = result.event.payload.agent_certified === true;
1630
+ const handoff = {
1631
+ native_goal_status: agentCertified ? 'agent_certified' : 'needs_second_agent_review',
1632
+ career_xp_status: 'pending_human_accept',
1633
+ next_action: agentCertified ? 'continue_work' : 'agent_review_again',
1634
+ rule: agentCertified
1635
+ ? 'Agent double-check complete; continue work. AgentXP waits for human accept.'
1636
+ : 'Proof is in Review; one more agent review pass certifies continuation. AgentXP waits for human accept.',
1637
+ };
1638
+ if (wantsJson(args)) {
1639
+ printJson({
1640
+ ok: true,
1641
+ action: 'ready',
1642
+ task_id: taskId,
1643
+ version: result.event.version,
1644
+ approval_status: 'pending',
1645
+ review_pass_count: result.event.payload.review_pass_count,
1646
+ agent_certified: agentCertified,
1647
+ handoff,
1648
+ projection_path: outPath,
1649
+ task: compactTaskFromProjection(projection, taskId),
1650
+ });
1651
+ return;
1652
+ }
1653
+ console.log(`ready ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version} pending approval`);
1654
+ console.log(handoff.rule);
1655
+ }
1656
+
1657
+ function cmdAccept(args) {
1084
1658
  const pos = positional(args);
1085
1659
  const id = pos[0];
1086
1660
  if (!id) {
1087
- console.error('atris task review: id required');
1661
+ console.error('atris task accept: id required');
1088
1662
  process.exit(2);
1089
1663
  }
1664
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
1665
+ const reward = flag(args, '--reward');
1666
+ const lessonFlag = flag(args, '--lesson');
1667
+ const nextTaskFlag = flag(args, '--next');
1668
+ const taskDb = getTaskDb();
1669
+ const db = taskDb.open();
1670
+ const taskId = requireTaskId(taskDb, db, id, 'atris task accept');
1671
+ const beforeProjection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
1672
+ const beforeTask = beforeProjection.tasks[0] || null;
1673
+ const proofFlag = flag(args, '--proof');
1674
+ const hasExplicitProof = typeof proofFlag === 'string';
1675
+ const proof = hasExplicitProof
1676
+ ? proofFlag
1677
+ : String(beforeTask?.metadata?.latest_agent_proof || '').trim();
1678
+ if (!proof) {
1679
+ console.error('atris task accept: proof required or task must already have fresh proof_ready proof');
1680
+ process.exit(2);
1681
+ }
1682
+ const readyReview = beforeTask?.review || {};
1683
+ const clearLesson = hasEmptyFlagValue(args, '--lesson');
1684
+ const clearNextTask = hasEmptyFlagValue(args, '--next');
1685
+ const lesson = clearLesson
1686
+ ? ''
1687
+ : typeof lessonFlag === 'string'
1688
+ ? lessonFlag
1689
+ : String(readyReview.lesson || beforeTask?.metadata?.latest_agent_lesson || '');
1690
+ const nextTask = clearNextTask
1691
+ ? ''
1692
+ : typeof nextTaskFlag === 'string'
1693
+ ? nextTaskFlag
1694
+ : String(readyReview.next_task || beforeTask?.metadata?.latest_agent_next_task || '');
1695
+ const clearedFields = [];
1696
+ if (clearLesson || (typeof lessonFlag === 'string' && !String(lessonFlag).trim())) clearedFields.push('lesson');
1697
+ if (clearNextTask || (typeof nextTaskFlag === 'string' && !String(nextTaskFlag).trim())) clearedFields.push('next_task');
1698
+ const parsedReward = parseAcceptReward(reward);
1699
+ if (!parsedReward.ok) {
1700
+ console.error('atris task accept: reward must be a positive number');
1701
+ process.exit(2);
1702
+ }
1703
+ const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor, allowReview: true });
1704
+ if (!done.updated) {
1705
+ console.error(`accept failed: ${taskId} not open|claimed|review`);
1706
+ process.exit(1);
1707
+ }
1708
+ const reviewed = taskDb.reviewTask(db, {
1709
+ id: taskId,
1710
+ actor,
1711
+ reward: parsedReward.value,
1712
+ lesson,
1713
+ nextTask,
1714
+ proof,
1715
+ careerXpEligible: true,
1716
+ clearedFields,
1717
+ });
1718
+ const xpProjection = refreshCareerXpAfterReview(reviewed);
1719
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
1720
+ if (wantsJson(args)) {
1721
+ printJson({
1722
+ ok: true,
1723
+ action: 'accepted',
1724
+ task_id: taskId,
1725
+ reviewed: true,
1726
+ reward: reviewed.episode.reward.value,
1727
+ episode: reviewed.episode,
1728
+ xp_projection: xpProjection,
1729
+ projection_path: outPath,
1730
+ task: compactTaskFromProjection(projection, taskId),
1731
+ });
1732
+ return;
1733
+ }
1734
+ console.log(`accepted ${taskRef(compactTaskFromProjection(projection, taskId))} reward=${reviewed.episode.reward.value}`);
1735
+ }
1736
+
1737
+ function cmdRevise(args) {
1738
+ const pos = positional(args);
1739
+ const id = pos[0];
1740
+ if (!id) {
1741
+ console.error('atris task revise: id required');
1742
+ process.exit(2);
1743
+ }
1744
+ const note = flag(args, '--note') || flag(args, '--reason') || pos.slice(1).join(' ');
1745
+ if (!note || note === true) {
1746
+ console.error('atris task revise: --note required');
1747
+ process.exit(2);
1748
+ }
1749
+ const actor = String(flag(args, '--as') || DEFAULT_OWNER);
1750
+ const taskDb = getTaskDb();
1751
+ const db = taskDb.open();
1752
+ const taskId = requireTaskId(taskDb, db, id, 'atris task revise');
1753
+ const result = taskDb.reviseTask(db, { id: taskId, actor, note: String(note) });
1754
+ if (!result.revised) {
1755
+ console.error(`revise failed: ${result.reason}`);
1756
+ process.exit(1);
1757
+ }
1758
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
1759
+ if (wantsJson(args)) {
1760
+ printJson({
1761
+ ok: true,
1762
+ action: 'revise',
1763
+ task_id: taskId,
1764
+ version: result.event.version,
1765
+ approval_status: 'revise',
1766
+ revision_count: result.event.payload.revision_count,
1767
+ projection_path: outPath,
1768
+ task: compactTaskFromProjection(projection, taskId),
1769
+ });
1770
+ return;
1771
+ }
1772
+ console.log(`revise ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version}`);
1773
+ }
1774
+
1775
+ function cmdReview(args) {
1776
+ const pos = positional(args);
1777
+ const id = pos[0];
1778
+ if (!id) {
1779
+ failTask('atris task review', 'missing_id', 'id required');
1780
+ }
1090
1781
  const reward = flag(args, '--reward');
1091
1782
  const lesson = flag(args, '--lesson') || '';
1092
1783
  const nextTask = flag(args, '--next') || '';
@@ -1103,12 +1794,14 @@ function cmdReview(args) {
1103
1794
  lesson: typeof lesson === 'string' ? lesson : '',
1104
1795
  nextTask: typeof nextTask === 'string' ? nextTask : '',
1105
1796
  proof: typeof proof === 'string' ? proof : '',
1797
+ careerXpEligible: false,
1106
1798
  });
1107
1799
  if (!result.reviewed) {
1108
1800
  console.error(`review failed: ${result.reason}`);
1109
1801
  process.exit(1);
1110
1802
  }
1111
1803
  const nextCreated = createNextTaskIfRequested(taskDb, db, args, currentTask, result.episode.next_task_suggestion);
1804
+ const xpProjection = refreshCareerXpAfterReview(result);
1112
1805
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1113
1806
  if (wantsJson(args)) {
1114
1807
  printJson({
@@ -1118,16 +1811,17 @@ function cmdReview(args) {
1118
1811
  version: result.event.version,
1119
1812
  reward: result.episode.reward.value,
1120
1813
  episode: result.episode,
1814
+ xp_projection: xpProjection,
1121
1815
  next_task_id: nextCreated ? nextCreated.id : null,
1122
1816
  projection_path: outPath,
1123
- projection,
1124
- task: taskFromProjection(projection, taskId),
1817
+ task: compactTaskFromProjection(projection, taskId),
1818
+ next_task: nextCreated ? compactTaskFromProjection(projection, nextCreated.id) : null,
1125
1819
  });
1126
1820
  return;
1127
1821
  }
1128
- console.log(`reviewed ${taskId} v${result.event.version} reward=${result.episode.reward.value}`);
1822
+ console.log(`reviewed ${taskRef(compactTaskFromProjection(projection, taskId))} v${result.event.version} reward=${result.episode.reward.value}`);
1129
1823
  if (result.episode.next_task_suggestion) console.log(`next: ${result.episode.next_task_suggestion}`);
1130
- if (nextCreated) console.log(`created next ${nextCreated.id}`);
1824
+ if (nextCreated) console.log(`created next ${taskRef(compactTaskFromProjection(projection, nextCreated.id))}`);
1131
1825
  }
1132
1826
 
1133
1827
  function importTodoFile(taskDb, db, target) {
@@ -1141,6 +1835,7 @@ function importTodoFile(taskDb, db, target) {
1141
1835
  const all = [
1142
1836
  ...parsed.backlog.map(t => ({ ...t, importStatus: 'open' })),
1143
1837
  ...parsed.inProgress.map(t => ({ ...t, importStatus: 'claimed' })),
1838
+ ...(parsed.review || []).map(t => ({ ...t, importStatus: 'review' })),
1144
1839
  ];
1145
1840
  let inserted = 0;
1146
1841
  let skipped = 0;
@@ -1154,7 +1849,7 @@ function importTodoFile(taskDb, db, target) {
1154
1849
  sourceKey: sk,
1155
1850
  status: t.importStatus,
1156
1851
  claimedBy: t.claimed || null,
1157
- metadata: { todo_id: t.id, claimed: t.claimed, stage: t.stage, verify: t.verify },
1852
+ metadata: { todo_id: t.id, todo_tags: t.tags || [], claimed: t.claimed, stage: t.stage, verify: t.verify },
1158
1853
  });
1159
1854
  if (result.inserted) inserted++; else skipped++;
1160
1855
  }
@@ -1207,25 +1902,48 @@ function cmdEvents(args) {
1207
1902
  const pos = positional(args);
1208
1903
  let taskId = pos[0] || null;
1209
1904
  const all = hasFlag(args, '--all');
1905
+ const rawLimit = flag(args, '--limit');
1906
+ const explicitLimit = rawLimit && rawLimit !== true ? Number(rawLimit) : null;
1907
+ const defaultRecentLimit = 24;
1908
+ const limit = explicitLimit || (taskId ? 500 : (all ? null : defaultRecentLimit));
1210
1909
  const taskDb = getTaskDb();
1211
1910
  const db = taskDb.open();
1212
1911
  if (taskId) taskId = requireTaskId(taskDb, db, taskId, 'atris task events');
1213
1912
  const events = taskDb.listTaskEvents(db, {
1214
1913
  taskId,
1215
1914
  workspaceRoot: all || taskId ? null : taskDb.workspaceRoot(),
1216
- limit: 500,
1915
+ limit,
1916
+ order: taskId || all ? 'asc' : 'desc',
1917
+ });
1918
+ const refRows = taskDb.listTasks(db, {
1919
+ workspaceRoot: all ? null : (taskId ? (taskDb.getTask(db, taskId) || {}).workspace_root : taskDb.workspaceRoot()),
1217
1920
  });
1921
+ const refById = taskDb.taskDisplayRefMap(refRows);
1218
1922
  if (wantsJson(args)) {
1219
- printJson({ ok: true, action: 'events', events });
1923
+ printJson({
1924
+ ok: true,
1925
+ action: 'events',
1926
+ task_id: taskId,
1927
+ mode: taskId ? 'task' : (all ? 'ledger' : 'recent'),
1928
+ limit,
1929
+ events,
1930
+ });
1220
1931
  return;
1221
1932
  }
1222
1933
  if (events.length === 0) {
1223
1934
  console.log('(no task events)');
1224
1935
  return;
1225
1936
  }
1937
+ if (!taskId && !all) {
1938
+ console.log('TASK EVENTS');
1939
+ console.log(`recent ${events.length} event${events.length === 1 ? '' : 's'} (use --all for the full ledger, --limit N to adjust)`);
1940
+ console.log('');
1941
+ for (const e of events) console.log(formatTaskEventCompact(e, refById));
1942
+ return;
1943
+ }
1226
1944
  for (const e of events) {
1227
1945
  const actor = e.actor ? ` actor=${e.actor}` : '';
1228
- console.log(`${e.version}\t${e.event_type}\t${e.task_id}${actor}\t${JSON.stringify(e.payload || {})}`);
1946
+ console.log(`${e.version}\t${e.event_type}\t${refById.get(e.task_id) || taskRef(e.task_id)}${actor}\t${JSON.stringify(e.payload || {})}`);
1229
1947
  }
1230
1948
  }
1231
1949
 
@@ -1289,29 +2007,102 @@ function cmdSetup(args) {
1289
2007
  }
1290
2008
  }
1291
2009
 
2010
+ function extractTodoSectionMarkdown(content, sectionName) {
2011
+ const escaped = String(sectionName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2012
+ const match = String(content || '').match(new RegExp(`(?:^|\\n)(##\\s+${escaped}[^\\n]*\\n[\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
2013
+ return match ? match[1].trimEnd() : null;
2014
+ }
2015
+
2016
+ function markdownRowsForRender(taskDb, existingTodoPath, rows, refRows) {
2017
+ if (!existingTodoPath || !fs.existsSync(existingTodoPath)) return [];
2018
+ const { parseTodoFile } = require('../lib/todo-fallback');
2019
+ const parsed = parseTodoFile(existingTodoPath);
2020
+ const ws = taskDb.workspaceRoot();
2021
+ const existingSourceKeys = new Set(
2022
+ (Array.isArray(refRows) ? refRows : [])
2023
+ .map(row => row && row.source_key)
2024
+ .filter(Boolean)
2025
+ );
2026
+ const existingTitles = new Set(
2027
+ [...(Array.isArray(rows) ? rows : []), ...(Array.isArray(refRows) ? refRows : [])]
2028
+ .map(row => taskDb.normalizeTitle(row && row.title))
2029
+ .filter(Boolean)
2030
+ );
2031
+ const sections = [
2032
+ ['backlog', 'open'],
2033
+ ['inProgress', 'claimed'],
2034
+ ['review', 'review'],
2035
+ ['completed', 'done'],
2036
+ ];
2037
+ const out = [];
2038
+ let index = 0;
2039
+ for (const [bucket, status] of sections) {
2040
+ for (const task of parsed[bucket] || []) {
2041
+ if (!task.title) continue;
2042
+ const sk = taskDb.sourceKey(existingTodoPath, task.title);
2043
+ const normalizedTitle = taskDb.normalizeTitle(task.title);
2044
+ if ((sk && existingSourceKeys.has(sk)) || existingTitles.has(normalizedTitle)) continue;
2045
+ out.push({
2046
+ id: `markdown:${status}:${task.id || index}:${sk ? sk.slice(0, 10) : index}`,
2047
+ title: task.title,
2048
+ status,
2049
+ tag: task.tag || null,
2050
+ workspace_root: ws,
2051
+ claimed_by: status === 'claimed' ? (task.claimed || null) : null,
2052
+ created_at: index,
2053
+ updated_at: index,
2054
+ done_at: null,
2055
+ metadata: {
2056
+ todo_id: task.id || null,
2057
+ todo_tags: task.tags || [],
2058
+ claimed: task.claimed || null,
2059
+ stage: task.stage || null,
2060
+ verify: task.verify || null,
2061
+ markdown_source: existingTodoPath,
2062
+ },
2063
+ });
2064
+ if (sk) existingSourceKeys.add(sk);
2065
+ existingTitles.add(normalizedTitle);
2066
+ index += 1;
2067
+ }
2068
+ }
2069
+ return out;
2070
+ }
2071
+
1292
2072
  function cmdRender(args) {
1293
2073
  const out = flag(args, '--out') || path.join('atris', 'TODO.md');
1294
2074
  const all = hasFlag(args, '--all');
2075
+ const doneLimitRaw = flag(args, '--done-limit');
2076
+ const doneLimit = doneLimitRaw && doneLimitRaw !== true ? Number(doneLimitRaw) : undefined;
1295
2077
  const taskDb = getTaskDb();
1296
2078
  const db = taskDb.open();
1297
2079
  const rows = taskDb.listTasks(db, {
1298
2080
  workspaceRoot: all ? null : taskDb.workspaceRoot(),
1299
2081
  limit: 500,
1300
2082
  });
1301
- const markdown = taskDb.renderTodoMarkdown(rows);
2083
+ const refRows = taskDb.listTasks(db, {
2084
+ workspaceRoot: all ? null : taskDb.workspaceRoot(),
2085
+ });
1302
2086
  const outPath = path.resolve(String(out));
2087
+ const existingTodo = fs.existsSync(outPath) ? fs.readFileSync(outPath, 'utf8') : '';
2088
+ const preservedSections = [];
2089
+ const endgameSection = extractTodoSectionMarkdown(existingTodo, 'Endgame');
2090
+ if (endgameSection) preservedSections.push(endgameSection);
2091
+ const markdownRows = markdownRowsForRender(taskDb, outPath, rows, refRows);
2092
+ const rowsToRender = [...rows, ...markdownRows];
2093
+ const markdown = taskDb.renderTodoMarkdown(rowsToRender, { doneLimit, refRows, preservedSections });
1303
2094
  fs.mkdirSync(path.dirname(outPath), { recursive: true });
1304
2095
  fs.writeFileSync(outPath, markdown, 'utf8');
1305
2096
  if (wantsJson(args)) {
1306
2097
  printJson({
1307
2098
  ok: true,
1308
2099
  action: 'rendered',
1309
- count: rows.length,
2100
+ count: rowsToRender.length,
1310
2101
  path: outPath,
1311
2102
  });
1312
2103
  return;
1313
2104
  }
1314
- console.log(`rendered ${rows.length} task${rows.length === 1 ? '' : 's'} -> ${outPath}`);
2105
+ console.log(`rendered ${rowsToRender.length} task${rowsToRender.length === 1 ? '' : 's'} -> ${outPath}`);
1315
2106
  }
1316
2107
 
1317
2108
  function cmdSync(args) {
@@ -1358,8 +2149,9 @@ function cmdSync(args) {
1358
2149
 
1359
2150
  console.log(`task sync dry-run: ${plan.length} planned write${plan.length === 1 ? '' : 's'}`);
1360
2151
  console.log(`business: ${businessId}`);
2152
+ const refById = taskDb.taskDisplayRefMap(projection.tasks || []);
1361
2153
  for (const item of plan) {
1362
- console.log(`${item.method.padEnd(5)} ${item.endpoint} <= ${item.local_task_id.slice(0, 8)} ${item.body.title}`);
2154
+ console.log(`${item.method.padEnd(5)} ${item.endpoint} <= ${refById.get(item.local_task_id) || taskRef(item.local_task_id)} ${item.body.title}`);
1363
2155
  for (const followup of item.after_create || []) {
1364
2156
  console.log(` then ${followup.method} ${followup.endpoint} state=${followup.body.state}`);
1365
2157
  }
@@ -1367,13 +2159,21 @@ function cmdSync(args) {
1367
2159
  }
1368
2160
 
1369
2161
  function taskColumn(task) {
1370
- if (task.status === 'open') return 'open';
2162
+ if (task.status === 'open') return taskIsPlannedOpen(task) ? 'open' : 'backlog';
1371
2163
  if (task.status === 'claimed') return 'doing';
2164
+ if (task.status === 'review') return 'review';
2165
+ if (task.status === 'failed' && taskHasReview(task)) return 'done';
1372
2166
  if (task.status === 'failed') return 'blocked';
1373
- if (task.status === 'done' && task.latest_event_type !== 'reviewed') return 'review';
2167
+ if (task.status === 'done' && !taskHasReview(task)) return 'review';
1374
2168
  return 'done';
1375
2169
  }
1376
2170
 
2171
+ function taskHasReview(task) {
2172
+ if (task.latest_event_type === 'reviewed') return true;
2173
+ const review = task.review || {};
2174
+ return review.reward != null || Boolean(review.proof || review.lesson || review.next_task);
2175
+ }
2176
+
1377
2177
  function taskBoardHtml() {
1378
2178
  return `<!doctype html>
1379
2179
  <html lang="en">
@@ -1397,7 +2197,7 @@ function taskBoardHtml() {
1397
2197
  button { border:1px solid var(--line); background:#20242a; color:var(--text); border-radius:7px; padding:8px 10px; font:inherit; font-size:12px; cursor:pointer; }
1398
2198
  button:hover { border-color:#3b414b; background:#252a32; }
1399
2199
  .primary { background:#214b35; border-color:#2f684a; }
1400
- .grid { display:grid; grid-template-columns: repeat(5, minmax(180px, 1fr)); gap:12px; align-items:start; }
2200
+ .grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
1401
2201
  .overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
1402
2202
  .goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
1403
2203
  .goalbox h2, .chainbox h2 { margin:0 0 8px; color:var(--muted); font-size:12px; font-weight:650; }
@@ -1441,7 +2241,7 @@ function taskBoardHtml() {
1441
2241
  <header>
1442
2242
  <div>
1443
2243
  <h1>Atris Task Factory</h1>
1444
- <div class="sub">local durable tasks / Swarlo-ready event stream</div>
2244
+ <div class="sub" data-smoke="hello-from-ui">hello from UI</div>
1445
2245
  </div>
1446
2246
  <button id="refresh">Refresh</button>
1447
2247
  </header>
@@ -1466,12 +2266,14 @@ function taskBoardHtml() {
1466
2266
  </main>
1467
2267
  <script>
1468
2268
  const columns = [
2269
+ ['backlog', 'Backlog'],
1469
2270
  ['open', 'Open'],
1470
2271
  ['doing', 'Doing'],
1471
2272
  ['review', 'Review'],
1472
2273
  ['blocked', 'Blocked'],
1473
2274
  ['done', 'Done']
1474
2275
  ];
2276
+ const planTags = new Set(${JSON.stringify(Array.from(STATUS_PLAN_TAGS))});
1475
2277
  let state = { tasks: [] };
1476
2278
  let selected = null;
1477
2279
  const $ = (id) => document.getElementById(id);
@@ -1487,10 +2289,19 @@ function taskBoardHtml() {
1487
2289
  }
1488
2290
 
1489
2291
  function taskColumn(task) {
1490
- if (task.status === 'open') return 'open';
2292
+ if (task.status === 'open') {
2293
+ const metadata = task.metadata || {};
2294
+ const tag = String(task.tag || '').trim().toLowerCase().replace(/\\s+/g, '-');
2295
+ const stage = String(metadata.stage || '').trim().toLowerCase().replace(/\\s+/g, '-');
2296
+ const planned = planTags.has(tag) || planTags.has(stage) || metadata.verify || metadata.goal || metadata.loop || metadata.cron || metadata.next_run_at;
2297
+ return planned ? 'open' : 'backlog';
2298
+ }
1491
2299
  if (task.status === 'claimed') return 'doing';
2300
+ if (task.status === 'review') return 'review';
2301
+ const reviewed = task.latest_event_type === 'reviewed' || !!(task.review && (task.review.reward != null || task.review.proof || task.review.lesson || task.review.next_task));
2302
+ if (task.status === 'failed' && reviewed) return 'done';
1492
2303
  if (task.status === 'failed') return 'blocked';
1493
- if (task.status === 'done' && task.latest_event_type !== 'reviewed') return 'review';
2304
+ if (task.status === 'done' && !reviewed) return 'review';
1494
2305
  return 'done';
1495
2306
  }
1496
2307
 
@@ -1504,6 +2315,7 @@ function taskBoardHtml() {
1504
2315
  renderOverview();
1505
2316
  renderStreams();
1506
2317
  const board = $('board');
2318
+ board.style.setProperty('--board-columns', columns.length);
1507
2319
  board.innerHTML = '';
1508
2320
  for (const [key, label] of columns) {
1509
2321
  const tasks = state.tasks.filter((task) => taskColumn(task) === key);
@@ -1531,7 +2343,7 @@ function taskBoardHtml() {
1531
2343
  : '<div class="empty">No atris/goals.md found. Add goals to give tasks a north star.</div>';
1532
2344
  const latest = reviewed.slice(0, 3);
1533
2345
  const chainHtml = latest.length
1534
- ? latest.map((task) => '<div class="chainitem"><span>' + task.id.slice(0, 8) + '</span><strong></strong></div>').join('')
2346
+ ? latest.map((task) => '<div class="chainitem"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
1535
2347
  : '<div class="empty">Complete a task with proof to start the chain.</div>';
1536
2348
  $('overview').innerHTML = [
1537
2349
  '<div class="goalbox"><h2>Goals</h2>' + goalHtml + '</div>',
@@ -1562,7 +2374,7 @@ function taskBoardHtml() {
1562
2374
  };
1563
2375
  const tasks = stream.tasks.filter((task) => task.status !== 'done').slice(0, 3);
1564
2376
  const taskHtml = tasks.length
1565
- ? tasks.map((task) => '<div class="streamtask"><span>' + task.id.slice(0, 8) + '</span><strong></strong></div>').join('')
2377
+ ? tasks.map((task) => '<div class="streamtask"><span>' + (task.display_id || task.id.slice(0, 8)) + '</span><strong></strong></div>').join('')
1566
2378
  : '<div class="empty">No active tasks in this stream.</div>';
1567
2379
  return [
1568
2380
  '<div class="stream">',
@@ -1588,7 +2400,7 @@ function taskBoardHtml() {
1588
2400
  btn.innerHTML = '<div class="title"></div><div class="meta"><span class="pill"></span><span class="pill"></span><span class="pill"></span></div><div class="why"></div>';
1589
2401
  btn.querySelector('.title').textContent = task.title;
1590
2402
  const pills = btn.querySelectorAll('.pill');
1591
- pills[0].textContent = task.id.slice(0, 8);
2403
+ pills[0].textContent = task.display_id || task.id.slice(0, 8);
1592
2404
  pills[1].textContent = owner;
1593
2405
  pills[2].textContent = 'v' + task.current_version;
1594
2406
  const why = task.objective || (task.lineage && task.lineage.parent_title) || (task.review && task.review.proof) || '';
@@ -1611,30 +2423,43 @@ function taskBoardHtml() {
1611
2423
  '<div class="meta"><span class="pill">' + task.status + '</span><span class="pill">' + (task.claimed_by || 'unowned') + '</span><span class="pill">v' + task.current_version + '</span></div>',
1612
2424
  '<div class="fact"><b>Goal</b><div id="taskGoal"></div></div>',
1613
2425
  '<div class="fact"><b>Lineage</b><div id="taskLineage"></div></div>',
2426
+ '<div class="fact"><b>Summary</b><div id="taskSummary"></div></div>',
1614
2427
  '<div class="fact"><b>Proof / lesson</b><div id="taskProof"></div></div>',
1615
2428
  '<div class="thread">' + (messages || '<div class="empty">No thread yet.</div>') + '</div>',
1616
2429
  '<label>Add context</label><textarea id="note" placeholder="Decision, blocker, context, update..."></textarea>',
1617
2430
  '<label>Proof</label><input id="proof" placeholder="npm test, PR link, screenshot, blocked reason...">',
1618
2431
  '<label>Lesson</label><textarea id="lesson" placeholder="What did this task teach us?"></textarea>',
1619
2432
  '<label>Next task</label><input id="nextTask" placeholder="Optional next sharper task">',
1620
- '<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="finish" class="primary full">Finish + review</button></div>'
2433
+ '<div class="actions"><button id="claim">Claim</button><button id="saveNote">Say</button><button id="finish" class="primary full"></button></div>'
1621
2434
  ].join('');
1622
2435
  room.querySelector('h3').textContent = task.title;
1623
2436
  $('taskGoal').textContent = task.objective || 'No matching goal yet.';
1624
2437
  $('taskLineage').textContent = 'parent: ' + parent + ' / next: ' + children;
2438
+ $('taskSummary').textContent = task.review && task.review.summary
2439
+ ? task.review.summary
2440
+ : 'No review summary yet.';
1625
2441
  $('taskProof').textContent = task.review && (task.review.proof || task.review.lesson)
1626
2442
  ? ((task.review.proof || 'no proof') + ' / ' + (task.review.lesson || 'no lesson'))
1627
2443
  : 'No proof yet.';
1628
2444
  room.querySelectorAll('.msg div:last-child').forEach((el, i) => { el.textContent = task.messages[i].content; });
2445
+ $('finish').textContent = task.status === 'review' ? 'Accept proof' : 'Move to Review';
1629
2446
  $('claim').onclick = () => mutate('/api/tasks/' + task.id + '/claim', { owner: 'operator' });
1630
2447
  $('saveNote').onclick = () => mutate('/api/tasks/' + task.id + '/message', { actor: 'operator', content: $('note').value });
1631
- $('finish').onclick = () => mutate('/api/tasks/' + task.id + '/finish', {
1632
- actor: 'operator',
1633
- proof: $('proof').value,
1634
- lesson: $('lesson').value,
1635
- next: $('nextTask').value,
1636
- createNext: Boolean($('nextTask').value.trim())
1637
- });
2448
+ $('finish').onclick = () => {
2449
+ const proof = $('proof').value.trim();
2450
+ const lesson = $('lesson').value.trim();
2451
+ const nextTask = $('nextTask').value.trim();
2452
+ const payload = { actor: 'operator' };
2453
+ if (proof) payload.proof = proof;
2454
+ if (lesson) payload.lesson = lesson;
2455
+ if (nextTask) payload.next = nextTask;
2456
+ if (task.status === 'review') {
2457
+ payload.createNext = Boolean(nextTask || (task.review && task.review.next_task));
2458
+ mutate('/api/tasks/' + task.id + '/accept', payload);
2459
+ } else {
2460
+ mutate('/api/tasks/' + task.id + '/ready', payload);
2461
+ }
2462
+ };
1638
2463
  }
1639
2464
 
1640
2465
  async function mutate(path, body) {
@@ -1710,7 +2535,7 @@ async function handleTaskApi(req, res, taskDb, db) {
1710
2535
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1711
2536
  return sendJson(res, 200, { ok: true, action: 'created', task_id: result.id, projection_path: outPath, task: taskFromProjection(projection, result.id) });
1712
2537
  }
1713
- const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|finish|review|events)$/);
2538
+ const match = url.pathname.match(/^\/api\/tasks\/([^/]+)\/(claim|message|ready|accept|revise|finish|review|events)$/);
1714
2539
  if (!match) return sendJson(res, 404, { ok: false, reason: 'not_found' });
1715
2540
  const resolved = resolveTaskRef(taskDb, db, match[1]);
1716
2541
  if (!resolved.ok) return sendJson(res, resolved.reason === 'ambiguous' ? 409 : 404, { ok: false, reason: resolved.reason });
@@ -1742,6 +2567,7 @@ async function handleTaskApi(req, res, taskDb, db) {
1742
2567
  const shouldReview = body.proof || body.lesson || body.next || body.reward !== undefined;
1743
2568
  let episode = null;
1744
2569
  let nextCreated = null;
2570
+ let xpProjection = null;
1745
2571
  if (shouldReview) {
1746
2572
  const reviewed = taskDb.reviewTask(db, {
1747
2573
  id: taskId,
@@ -1750,9 +2576,11 @@ async function handleTaskApi(req, res, taskDb, db) {
1750
2576
  lesson: String(body.lesson || ''),
1751
2577
  nextTask: String(body.next || ''),
1752
2578
  proof: String(body.proof || ''),
2579
+ careerXpEligible: false,
1753
2580
  });
1754
2581
  episode = reviewed.episode;
1755
2582
  nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, episode.next_task_suggestion) : null;
2583
+ xpProjection = refreshCareerXpAfterReview(reviewed);
1756
2584
  }
1757
2585
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1758
2586
  return sendJson(res, 200, {
@@ -1761,11 +2589,63 @@ async function handleTaskApi(req, res, taskDb, db) {
1761
2589
  task_id: taskId,
1762
2590
  reviewed: Boolean(episode),
1763
2591
  episode,
2592
+ xp_projection: xpProjection,
1764
2593
  next_task_id: nextCreated ? nextCreated.id : null,
1765
2594
  projection_path: outPath,
1766
2595
  task: taskFromProjection(projection, taskId),
1767
2596
  });
1768
2597
  }
2598
+ if (op === 'ready') {
2599
+ const proof = String(body.proof || '').trim();
2600
+ if (!proof) return sendJson(res, 400, { ok: false, reason: 'proof_required' });
2601
+ const result = taskDb.readyTask(db, {
2602
+ id: taskId,
2603
+ actor: String(body.actor || DEFAULT_OWNER),
2604
+ proof,
2605
+ lesson: String(body.lesson || ''),
2606
+ nextTask: String(body.next || ''),
2607
+ });
2608
+ if (!result.ready) return sendJson(res, 409, { ok: false, reason: result.reason });
2609
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
2610
+ return sendJson(res, 200, { ok: true, action: 'ready', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
2611
+ }
2612
+ if (op === 'accept') {
2613
+ const currentTask = enrichTaskProjection(taskDb.taskProjection(db, { taskId })).tasks[0] || null;
2614
+ const hasExplicitProof = Object.prototype.hasOwnProperty.call(body, 'proof');
2615
+ const proof = String(hasExplicitProof ? body.proof : currentTask?.metadata?.latest_agent_proof || '').trim();
2616
+ if (!proof) return sendJson(res, 400, { ok: false, reason: 'proof_required' });
2617
+ const hasExplicitLesson = Object.prototype.hasOwnProperty.call(body, 'lesson');
2618
+ const hasExplicitNext = Object.prototype.hasOwnProperty.call(body, 'next');
2619
+ const lesson = hasExplicitLesson ? String(body.lesson || '') : String(currentTask?.review?.lesson || currentTask?.metadata?.latest_agent_lesson || '');
2620
+ const nextTask = hasExplicitNext ? String(body.next || '') : String(currentTask?.review?.next_task || currentTask?.metadata?.latest_agent_next_task || '');
2621
+ const clearedFields = [];
2622
+ if (hasExplicitLesson && !lesson.trim()) clearedFields.push('lesson');
2623
+ if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
2624
+ const parsedReward = parseAcceptReward(body.reward);
2625
+ if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
2626
+ const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor: String(body.actor || DEFAULT_OWNER), allowReview: true });
2627
+ if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
2628
+ const reviewed = taskDb.reviewTask(db, {
2629
+ id: taskId,
2630
+ actor: String(body.actor || DEFAULT_OWNER),
2631
+ reward: parsedReward.value,
2632
+ lesson,
2633
+ nextTask,
2634
+ proof,
2635
+ careerXpEligible: true,
2636
+ clearedFields,
2637
+ });
2638
+ const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
2639
+ const xpProjection = refreshCareerXpAfterReview(reviewed);
2640
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
2641
+ return sendJson(res, 200, { ok: true, action: 'accepted', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
2642
+ }
2643
+ if (op === 'revise') {
2644
+ const result = taskDb.reviseTask(db, { id: taskId, actor: String(body.actor || DEFAULT_OWNER), note: String(body.note || body.reason || '') });
2645
+ if (!result.revised) return sendJson(res, 409, { ok: false, reason: result.reason });
2646
+ const { projection, outPath } = writeDefaultProjection(taskDb, db);
2647
+ return sendJson(res, 200, { ok: true, action: 'revise', task_id: taskId, projection_path: outPath, task: taskFromProjection(projection, taskId) });
2648
+ }
1769
2649
  if (op === 'review') {
1770
2650
  const currentTask = taskDb.getTask(db, taskId);
1771
2651
  const reviewed = taskDb.reviewTask(db, {
@@ -1775,10 +2655,12 @@ async function handleTaskApi(req, res, taskDb, db) {
1775
2655
  lesson: String(body.lesson || ''),
1776
2656
  nextTask: String(body.next || ''),
1777
2657
  proof: String(body.proof || ''),
2658
+ careerXpEligible: false,
1778
2659
  });
1779
2660
  const nextCreated = body.createNext ? createNextTaskIfRequested(taskDb, db, ['--create-next'], currentTask, reviewed.episode.next_task_suggestion) : null;
2661
+ const xpProjection = refreshCareerXpAfterReview(reviewed);
1780
2662
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
1781
- return sendJson(res, 200, { ok: true, action: 'reviewed', task_id: taskId, episode: reviewed.episode, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
2663
+ return sendJson(res, 200, { ok: true, action: 'reviewed', task_id: taskId, episode: reviewed.episode, xp_projection: xpProjection, next_task_id: nextCreated ? nextCreated.id : null, projection_path: outPath, task: taskFromProjection(projection, taskId) });
1782
2664
  }
1783
2665
  }
1784
2666
 
@@ -1811,6 +2693,7 @@ function cmdServe(args) {
1811
2693
 
1812
2694
  async function run(args) {
1813
2695
  const raw = args || [];
2696
+ if (raw.includes('--help') || raw.includes('-h')) return help();
1814
2697
  const first = raw[0];
1815
2698
  const sub = !first || first.startsWith('--') ? 'desk' : first;
1816
2699
  const rest = !first || first.startsWith('--') ? raw : raw.slice(1);
@@ -1830,6 +2713,9 @@ async function run(args) {
1830
2713
  case 'note': return cmdNote(rest);
1831
2714
  case 'say': return cmdNote(rest);
1832
2715
  case 'show': return cmdShow(rest);
2716
+ case 'ready': return cmdReady(rest);
2717
+ case 'accept': return cmdAccept(rest);
2718
+ case 'revise': return cmdRevise(rest);
1833
2719
  case 'done': return cmdDone(rest);
1834
2720
  case 'finish': return cmdFinish(rest);
1835
2721
  case 'fail': return cmdDone([...rest, '--failed']);
@@ -1848,6 +2734,14 @@ async function run(args) {
1848
2734
  case '-h':
1849
2735
  return help();
1850
2736
  default:
2737
+ if (wantsJson(raw)) {
2738
+ printJson({
2739
+ ok: false,
2740
+ error: `unknown task subcommand: ${sub}`,
2741
+ usage: taskUsageLines(),
2742
+ });
2743
+ process.exit(2);
2744
+ }
1851
2745
  console.error(`atris task: unknown subcommand "${sub}"`);
1852
2746
  help();
1853
2747
  process.exit(2);