atris 3.16.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +33 -7
  2. package/atris/skills/atris/SKILL.md +15 -2
  3. package/atris/skills/atris-feedback/SKILL.md +7 -0
  4. package/atris/skills/design/SKILL.md +29 -2
  5. package/atris/skills/engines/SKILL.md +44 -0
  6. package/atris/skills/flow/SKILL.md +1 -1
  7. package/atris/skills/wake/SKILL.md +37 -0
  8. package/atris/skills/youtube/SKILL.md +13 -39
  9. package/atris/team/validator/MEMBER.md +1 -0
  10. package/atris/wiki/concepts/agent-activation-contract.md +3 -3
  11. package/atris/wiki/concepts/workspace-initialization-contract.md +3 -3
  12. package/atris/wiki/index.md +1 -0
  13. package/atris.md +43 -19
  14. package/bin/atris.js +446 -43
  15. package/commands/agent-spawn.js +480 -0
  16. package/commands/analytics.js +6 -3
  17. package/commands/apps.js +11 -0
  18. package/commands/autopilot.js +466 -20
  19. package/commands/brain.js +74 -7
  20. package/commands/brainstorm.js +9 -58
  21. package/commands/clean.js +1 -4
  22. package/commands/compile.js +574 -0
  23. package/commands/console.js +8 -3
  24. package/commands/deck.js +135 -0
  25. package/commands/init.js +22 -11
  26. package/commands/lesson.js +76 -0
  27. package/commands/member.js +252 -48
  28. package/commands/mission.js +405 -13
  29. package/commands/now.js +4 -2
  30. package/commands/probe.js +444 -0
  31. package/commands/pulse.js +504 -0
  32. package/commands/radar.js +1 -0
  33. package/commands/recap.js +233 -0
  34. package/commands/run.js +615 -22
  35. package/commands/skill.js +6 -2
  36. package/commands/slop.js +173 -0
  37. package/commands/spaceship.js +39 -0
  38. package/commands/sync.js +0 -2
  39. package/commands/task.js +458 -43
  40. package/commands/verify.js +7 -3
  41. package/lib/activity-stream.js +166 -0
  42. package/lib/auto-accept-certified.js +23 -1
  43. package/lib/context-gatherer.js +170 -0
  44. package/lib/escape-regexp.js +13 -0
  45. package/lib/file-ops.js +6 -3
  46. package/lib/journal.js +1 -1
  47. package/lib/lesson-contradiction.js +113 -0
  48. package/lib/policy-lessons.js +3 -2
  49. package/lib/pulse.js +401 -0
  50. package/lib/runner-command.js +156 -0
  51. package/lib/slides-deck.js +236 -0
  52. package/lib/state-detection.js +40 -3
  53. package/lib/task-db.js +101 -4
  54. package/lib/task-proof.js +1 -1
  55. package/lib/todo-fallback.js +2 -1
  56. package/lib/todo-sections.js +33 -0
  57. package/package.json +1 -2
  58. package/utils/api.js +14 -2
  59. package/atris/atrisDev.md +0 -717
package/commands/task.js CHANGED
@@ -10,6 +10,7 @@ const os = require('os');
10
10
  const { taskProofState } = require('../lib/task-proof');
11
11
  const { evaluateAutoAccept, parseVerifyCommand } = require('../lib/auto-accept-certified');
12
12
  const { extractReceiptEvidence } = require('../lib/receipt-evidence');
13
+ const escapeRegExp = require('../lib/escape-regexp');
13
14
 
14
15
  const DEFAULT_OWNER = process.env.ATRIS_AGENT_ID
15
16
  || process.env.USER
@@ -90,7 +91,7 @@ atris task - durable local task state (SQLite, gitignored)
90
91
 
91
92
  atris task Show the task desk
92
93
  atris task new "<title>" Create a task
93
- atris task next Claim/show the next open task
94
+ atris task next [--create-next] Claim/show next open task; optionally create the generated Endgame fallback
94
95
  atris task continue-work <id> Create/reuse a certified Review follow-up task
95
96
  atris task say <id> "<message>" Add context to a task
96
97
  atris task chat <id> "<message>" [--goal "..."] Refine a task chat + working goal
@@ -143,6 +144,7 @@ atris task - durable local task state (SQLite, gitignored)
143
144
  atris task serve [--port <n>] Open local task factory board
144
145
  atris task sync --dry-run Plan cloud/Swarlo task sync writes
145
146
  atris task import <file> One-shot import from TODO.md
147
+ atris task lineage <id> [--json] Show endgame -> tasks -> commits chain
146
148
  atris task events [id] [--limit <n>] Print recent task events
147
149
  atris task events --all Print the full append-only ledger
148
150
  atris task export [--out <file>] Write web/desktop JSON projection
@@ -4122,7 +4124,14 @@ function cmdAcceptGroup(args) {
4122
4124
  const isVerified = verifiedIds.has(task.id);
4123
4125
  const proof = String(task.review?.proof || task.metadata?.latest_agent_proof || '').trim()
4124
4126
  || `Accepted via group spot-check (${groupLabel}); human ${actor} verified ${verifiedIds.size}/${group.length}.`;
4125
- const done = taskDb.doneTask(db, { id: task.id, status: 'done', actor, allowReview: true });
4127
+ const done = taskDb.doneTask(db, {
4128
+ id: task.id,
4129
+ status: 'done',
4130
+ actor,
4131
+ allowReview: true,
4132
+ action: 'accepted',
4133
+ proof,
4134
+ });
4126
4135
  if (!done.updated) { accepted.push({ id: task.id, ok: false, reason: 'not_review' }); continue; }
4127
4136
  taskDb.reviewTask(db, {
4128
4137
  id: task.id,
@@ -4425,15 +4434,26 @@ function cmdDelegate(args) {
4425
4434
  if (handoff.swarlo) console.log(`swarlo: ${handoff.swarlo.channel}/${handoff.swarlo.action}`);
4426
4435
  }
4427
4436
 
4428
- function taskDayGroups(tasks) {
4437
+ // Failed tasks older than this stop earning a daily owner-group row;
4438
+ // they collapse into one stale summary line instead (target state = clean day view).
4439
+ const DAY_STALE_FAILED_MS = 7 * 24 * 60 * 60 * 1000;
4440
+
4441
+ function taskDayGroups(tasks, { now = Date.now() } = {}) {
4429
4442
  const active = tasks.filter(task => task.status !== 'done');
4430
- const groups = new Map();
4443
+ const staleFailed = [];
4444
+ const visible = [];
4431
4445
  for (const task of active) {
4446
+ const isStaleFailed = task.status === 'failed' && (now - (task.updated_at || 0)) > DAY_STALE_FAILED_MS;
4447
+ if (isStaleFailed) staleFailed.push(task);
4448
+ else visible.push(task);
4449
+ }
4450
+ const groups = new Map();
4451
+ for (const task of visible) {
4432
4452
  const owner = taskAssignee(task) || 'unassigned';
4433
4453
  if (!groups.has(owner)) groups.set(owner, []);
4434
4454
  groups.get(owner).push(task);
4435
4455
  }
4436
- return Array.from(groups.entries())
4456
+ const grouped = Array.from(groups.entries())
4437
4457
  .sort((a, b) => {
4438
4458
  if (a[0] === 'unassigned') return 1;
4439
4459
  if (b[0] === 'unassigned') return -1;
@@ -4446,6 +4466,7 @@ function taskDayGroups(tasks) {
4446
4466
  return (statusOrder[a.status] - statusOrder[b.status]) || (b.updated_at - a.updated_at);
4447
4467
  }),
4448
4468
  }));
4469
+ return { groups: grouped, staleFailed };
4449
4470
  }
4450
4471
 
4451
4472
  function cmdDay(args) {
@@ -4453,13 +4474,15 @@ function cmdDay(args) {
4453
4474
  const taskDb = getTaskDb();
4454
4475
  const db = taskDb.open();
4455
4476
  const { projection, outPath } = writeDefaultProjection(taskDb, db, { all });
4456
- const groups = taskDayGroups(projection.tasks || []);
4477
+ const { groups, staleFailed } = taskDayGroups(projection.tasks || []);
4457
4478
  const counts = {
4458
4479
  active: groups.reduce((sum, group) => sum + group.tasks.length, 0),
4459
4480
  owners: groups.length,
4460
4481
  open: (projection.tasks || []).filter(task => task.status === 'open').length,
4461
4482
  claimed: (projection.tasks || []).filter(task => task.status === 'claimed').length,
4462
- review: (projection.tasks || []).filter(task => task.status === 'review' || task.status === 'failed' || (task.status === 'done' && task.review && task.review.reward === null)).length,
4483
+ review: (projection.tasks || []).filter(task => task.status === 'review').length,
4484
+ failed: (projection.tasks || []).filter(task => task.status === 'failed').length,
4485
+ stale_failed: staleFailed.length,
4463
4486
  };
4464
4487
  const date = new Date().toISOString().slice(0, 10);
4465
4488
  if (wantsJson(args)) {
@@ -4470,11 +4493,16 @@ function cmdDay(args) {
4470
4493
  projection_path: outPath,
4471
4494
  counts,
4472
4495
  groups,
4496
+ stale_failed: {
4497
+ count: staleFailed.length,
4498
+ refs: staleFailed.map(task => taskRef(task)),
4499
+ },
4473
4500
  });
4474
4501
  return;
4475
4502
  }
4476
4503
  console.log('TASK DAY');
4477
- console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}`);
4504
+ const failedText = counts.failed > 0 ? ` / failed ${counts.failed}` : '';
4505
+ console.log(`${date} active ${counts.active} / owners ${counts.owners} / review ${counts.review}${failedText}`);
4478
4506
  console.log('');
4479
4507
  if (!groups.length) {
4480
4508
  console.log('clear no active tasks');
@@ -4487,6 +4515,10 @@ function cmdDay(args) {
4487
4515
  console.log(` ${task.status.padEnd(7)} ${taskRef(task)}${claim}${tag} ${task.title}`);
4488
4516
  }
4489
4517
  }
4518
+ if (staleFailed.length) {
4519
+ console.log('');
4520
+ console.log(`stale ${staleFailed.length} failed >7d hidden — atris task list --status failed`);
4521
+ }
4490
4522
  console.log('');
4491
4523
  console.log('add: atris task delegate "..." --to codex --tag tasks');
4492
4524
  }
@@ -4581,6 +4613,93 @@ function cmdClaim(args) {
4581
4613
  }
4582
4614
  }
4583
4615
 
4616
+ function readEndgameAgentAction(root, owner) {
4617
+ const todoPath = path.join(root || process.cwd(), 'atris', 'TODO.md');
4618
+ if (!fs.existsSync(todoPath)) return null;
4619
+ const content = fs.readFileSync(todoPath, 'utf8');
4620
+ const section = extractTodoSectionMarkdown(content, 'Endgame');
4621
+ if (!section) return null;
4622
+ const slug = (section.match(/\*\*Slug:\*\*\s*([^\n]+)/i)?.[1] || '').trim();
4623
+ const horizon = (section.match(/\*\*Horizon:\*\*\s*([^\n]+)/i)?.[1] || '').trim();
4624
+ if (!slug && !horizon) return null;
4625
+ const member = String(owner || DEFAULT_OWNER);
4626
+ const taskSeed = buildEndgameTaskSeed({ slug, horizon, owner: member });
4627
+ return {
4628
+ kind: 'create_bounded_endgame_task',
4629
+ endgame_slug: slug || null,
4630
+ horizon: horizon || null,
4631
+ task_seed: taskSeed,
4632
+ command: `atris brain activate --member ${member} --root . --verify`,
4633
+ message: `Create the next bounded task from Endgame${slug ? ` ${slug}` : ''}${horizon ? `: ${horizon}` : ''}. Do not accept XP.`,
4634
+ };
4635
+ }
4636
+
4637
+ function shellQuoteTaskArg(value) {
4638
+ const s = String(value || '');
4639
+ if (/^[A-Za-z0-9_./:-]+$/.test(s)) return s;
4640
+ return `'${s.replace(/'/g, "'\\''")}'`;
4641
+ }
4642
+
4643
+ function buildEndgameTaskSeed({ slug, horizon, owner }) {
4644
+ const combined = `${slug || ''} ${horizon || ''}`.toLowerCase();
4645
+ const runnerEndgame = /\b(runner|heartbeat|autopilot\/run|claude -p|model retirement|runner swap)\b/.test(combined);
4646
+ const title = runnerEndgame
4647
+ ? 'Audit and close next runner-agnostic heartbeat gap'
4648
+ : `Advance Endgame ${slug || 'current horizon'}`;
4649
+ const tag = runnerEndgame ? 'runner' : 'endgame';
4650
+ const files = runnerEndgame
4651
+ ? ['commands/autopilot.js', 'commands/run.js', 'lib/runner-command.js', 'test/autopilot-runner-model.test.js']
4652
+ : ['atris/TODO.md', 'atris/MAP.md'];
4653
+ const verifier = runnerEndgame
4654
+ ? 'node --test test/autopilot-runner-model.test.js test/runner-command.test.js'
4655
+ : 'git diff --check';
4656
+ const stopRule = 'Move proof-ready work to Review; do not accept XP.';
4657
+ const goal = runnerEndgame
4658
+ ? 'Find and close one remaining runner-agnostic heartbeat gap, or record proof that the next gap is documentation/state only.'
4659
+ : `Move the Endgame forward with one bounded, verifiable slice${horizon ? `: ${horizon}` : ''}.`;
4660
+ const note = `Goal: ${goal} Files: ${files.join(', ')}. Done: one scoped Endgame slice is implemented or the audited gap is closed with proof. Check: ${verifier}; git diff --check. Stop: ${stopRule}`;
4661
+ return {
4662
+ title,
4663
+ tag,
4664
+ files,
4665
+ verifier,
4666
+ stop_rule: stopRule,
4667
+ create_command: `atris task new ${shellQuoteTaskArg(title)} --tag ${shellQuoteTaskArg(tag)}`,
4668
+ claim_command: `atris task claim <id> --as ${shellQuoteTaskArg(owner || DEFAULT_OWNER)}`,
4669
+ note,
4670
+ note_command: `atris task note <id> ${shellQuoteTaskArg(note)}`,
4671
+ };
4672
+ }
4673
+
4674
+ function createEndgameSeedTask(taskDb, db, seed, owner) {
4675
+ const taskOwner = String(owner || DEFAULT_OWNER);
4676
+ const result = taskDb.addTask(db, {
4677
+ title: seed.title,
4678
+ tag: seed.tag,
4679
+ workspaceRoot: taskDb.workspaceRoot(),
4680
+ metadata: {
4681
+ generated_from: 'task_next_endgame_seed',
4682
+ verifier: seed.verifier,
4683
+ files: seed.files,
4684
+ stop_rule: seed.stop_rule,
4685
+ },
4686
+ });
4687
+ const claim = taskDb.claimTask(db, { id: result.id, claimedBy: taskOwner });
4688
+ if (!claim.claimed) {
4689
+ return { ok: false, reason: claim.reason || 'claim_failed', task_id: result.id };
4690
+ }
4691
+ const note = taskDb.noteTask(db, { id: result.id, actor: taskOwner, content: seed.note });
4692
+ if (!note.noted) {
4693
+ return { ok: false, reason: note.reason || 'note_failed', task_id: result.id };
4694
+ }
4695
+ return {
4696
+ ok: true,
4697
+ task_id: result.id,
4698
+ inserted: result.inserted !== false,
4699
+ note_version: note.event.version,
4700
+ };
4701
+ }
4702
+
4584
4703
  function cmdNext(args) {
4585
4704
  const owner = flag(args, '--as') || DEFAULT_OWNER;
4586
4705
  const taskDb = getTaskDb();
@@ -4627,6 +4746,40 @@ function cmdNext(args) {
4627
4746
  const continueWorkCommand = handoff.next_action === 'continue_work'
4628
4747
  ? continueWorkCommandForTask(reviewTask, { owner })
4629
4748
  : null;
4749
+ const nextAgentAction = handoff.next_action === 'human_accept_waiting'
4750
+ ? readEndgameAgentAction(taskDb.workspaceRoot(), owner)
4751
+ : null;
4752
+ if (hasFlag(args, '--create-next')) {
4753
+ if (!nextAgentAction || !nextAgentAction.task_seed) {
4754
+ failTask('atris task next', 'no_create_next_seed', 'no concrete Endgame seed is available to create');
4755
+ }
4756
+ const created = createEndgameSeedTask(taskDb, db, nextAgentAction.task_seed, owner);
4757
+ if (!created.ok) {
4758
+ failTask('atris task next', created.reason || 'create_next_failed', `failed to create next task: ${created.reason || 'unknown'}`);
4759
+ }
4760
+ const { projection: createdProjection, outPath: createdOutPath } = writeDefaultProjection(taskDb, db);
4761
+ const createdTask = compactTaskFromProjection(createdProjection, created.task_id);
4762
+ if (wantsJson(args)) {
4763
+ printJson({
4764
+ ok: true,
4765
+ action: 'created_next',
4766
+ task_id: created.task_id,
4767
+ owner: String(owner),
4768
+ projection_path: createdOutPath,
4769
+ handoff,
4770
+ next_agent_action: nextAgentAction,
4771
+ note_version: created.note_version,
4772
+ task: createdTask,
4773
+ review_task: reviewTask,
4774
+ });
4775
+ return;
4776
+ }
4777
+ console.log(`created ${taskRef(createdTask)} @${owner}`);
4778
+ console.log(createdTask.title);
4779
+ console.log(`Noted v${created.note_version}. Human accept remains pending on ${taskRef(reviewTask)}.`);
4780
+ console.log(`Verify: ${nextAgentAction.task_seed.verifier}`);
4781
+ return;
4782
+ }
4630
4783
  if (wantsJson(args)) {
4631
4784
  printJson({
4632
4785
  ok: true,
@@ -4635,6 +4788,7 @@ function cmdNext(args) {
4635
4788
  owner: String(owner),
4636
4789
  projection_path: outPath,
4637
4790
  handoff,
4791
+ next_agent_action: nextAgentAction,
4638
4792
  continue_work_command: continueWorkCommand,
4639
4793
  continue_work_api: continueWorkCommand ? { method: 'POST', path: `/api/tasks/${encodeURIComponent(reviewTask.id)}/continue-work` } : null,
4640
4794
  review_task: reviewTask,
@@ -4648,8 +4802,15 @@ function cmdNext(args) {
4648
4802
  console.log(handoff.next_action === 'continue_work'
4649
4803
  ? 'Continue work elsewhere; AgentXP waits for human accept.'
4650
4804
  : handoff.next_action === 'human_accept_waiting'
4651
- ? 'No concrete next agent task is attached; AgentXP waits for human accept.'
4805
+ ? (nextAgentAction ? nextAgentAction.message : 'No concrete next agent task is attached; AgentXP waits for human accept.')
4652
4806
  : 'Review this task again before continuing.');
4807
+ if (nextAgentAction) console.log(`Command: ${nextAgentAction.command}`);
4808
+ if (nextAgentAction && nextAgentAction.task_seed) {
4809
+ console.log(`Create: ${nextAgentAction.task_seed.create_command}`);
4810
+ console.log(`Claim: ${nextAgentAction.task_seed.claim_command}`);
4811
+ console.log(`Note: ${nextAgentAction.task_seed.note_command}`);
4812
+ console.log(`Verify: ${nextAgentAction.task_seed.verifier}`);
4813
+ }
4653
4814
  if (continueWorkCommand) console.log(`Command: ${continueWorkCommand}`);
4654
4815
  return;
4655
4816
  }
@@ -5997,7 +6158,13 @@ function cmdDone(args) {
5997
6158
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task done', proof);
5998
6159
  else if (proof) requireMeaningfulTaskProof('atris task done', proof);
5999
6160
  }
6000
- const result = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done', actor });
6161
+ const result = taskDb.doneTask(db, {
6162
+ id: taskId,
6163
+ status: failed ? 'failed' : 'done',
6164
+ actor,
6165
+ action: failed ? 'failed' : 'done',
6166
+ proof,
6167
+ });
6001
6168
  if (result.updated) {
6002
6169
  const review = hasReview ? taskDb.reviewTask(db, {
6003
6170
  id: taskId,
@@ -6072,7 +6239,13 @@ function cmdFinish(args) {
6072
6239
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task finish', proof);
6073
6240
  else if (proof) requireMeaningfulTaskProof('atris task finish', proof);
6074
6241
  }
6075
- const done = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done', actor });
6242
+ const done = taskDb.doneTask(db, {
6243
+ id: taskId,
6244
+ status: failed ? 'failed' : 'done',
6245
+ actor,
6246
+ action: failed ? 'failed' : 'finished',
6247
+ proof,
6248
+ });
6076
6249
  if (!done.updated) {
6077
6250
  const detail = `finish failed: ${taskId} not in open|claimed`;
6078
6251
  if (wantsJson(args)) {
@@ -6273,7 +6446,14 @@ function cmdAccept(args) {
6273
6446
  console.error('atris task accept: reward must be a positive number');
6274
6447
  process.exit(2);
6275
6448
  }
6276
- const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor, allowReview: true });
6449
+ const done = taskDb.doneTask(db, {
6450
+ id: taskId,
6451
+ status: 'done',
6452
+ actor,
6453
+ allowReview: true,
6454
+ action: 'accepted',
6455
+ proof,
6456
+ });
6277
6457
  if (!done.updated) {
6278
6458
  console.error(`accept failed: ${taskId} not open|claimed|review`);
6279
6459
  process.exit(1);
@@ -6335,7 +6515,14 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
6335
6515
  }
6336
6516
 
6337
6517
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '' }) {
6338
- const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor, allowReview: true });
6518
+ const done = taskDb.doneTask(db, {
6519
+ id: taskId,
6520
+ status: 'done',
6521
+ actor,
6522
+ allowReview: true,
6523
+ action: 'accepted',
6524
+ proof,
6525
+ });
6339
6526
  if (!done.updated) {
6340
6527
  return { ok: false, reason: 'not_open_claimed_or_review' };
6341
6528
  }
@@ -6713,6 +6900,91 @@ function cmdEvents(args) {
6713
6900
  }
6714
6901
  }
6715
6902
 
6903
+ function cmdLineage(args) {
6904
+ const pos = positional(args);
6905
+ const id = pos[0];
6906
+ if (!id) {
6907
+ failTask('atris task lineage', 'missing_id', 'id required');
6908
+ }
6909
+ const taskDb = getTaskDb();
6910
+ const db = taskDb.open();
6911
+ const taskId = requireTaskId(taskDb, db, id, 'atris task lineage');
6912
+ const enriched = enrichTaskProjection(taskDb.taskProjection(db, { workspaceRoot: taskDb.workspaceRoot(), limit: 1000 }));
6913
+ const byId = new Map();
6914
+ for (const t of enriched.tasks) byId.set(t.id, t);
6915
+ const target = byId.get(taskId);
6916
+ if (!target) {
6917
+ console.error(`task not found: ${id}`);
6918
+ process.exit(1);
6919
+ }
6920
+
6921
+ const parents = [];
6922
+ let cursor = target;
6923
+ const seen = new Set();
6924
+ while (cursor) {
6925
+ const parentId = cursor.lineage && cursor.lineage.parent_task_id;
6926
+ if (!parentId || seen.has(parentId)) break;
6927
+ seen.add(parentId);
6928
+ const parent = byId.get(parentId);
6929
+ if (!parent) break;
6930
+ parents.unshift(parent);
6931
+ cursor = parent;
6932
+ }
6933
+
6934
+ const childIds = target.lineage && target.lineage.child_task_ids || [];
6935
+ const children = childIds.map(cid => byId.get(cid)).filter(Boolean);
6936
+
6937
+ const chain = [...parents, target, ...children];
6938
+
6939
+ let commits = [];
6940
+ try {
6941
+ const { spawnSync: sp } = require('child_process');
6942
+ const displayRefs = chain.map(t => taskRef(t)).filter(Boolean);
6943
+ const pattern = displayRefs.join('\\|');
6944
+ const result = sp('git', ['log', '--oneline', '--all', `--grep=${pattern}`], {
6945
+ encoding: 'utf8',
6946
+ timeout: 5000,
6947
+ });
6948
+ if (result.status === 0 && result.stdout) {
6949
+ commits = result.stdout.trim().split('\n').filter(Boolean);
6950
+ }
6951
+ } catch (_) {
6952
+ commits = [];
6953
+ }
6954
+
6955
+ if (wantsJson(args)) {
6956
+ printJson({
6957
+ ok: true,
6958
+ action: 'lineage',
6959
+ chain: {
6960
+ endgame: parents.length ? parents[0] : null,
6961
+ parents: parents.slice(1),
6962
+ target,
6963
+ children,
6964
+ commits,
6965
+ },
6966
+ });
6967
+ return;
6968
+ }
6969
+
6970
+ if (parents.length) {
6971
+ for (let i = 0; i < parents.length; i += 1) {
6972
+ const p = parents[i];
6973
+ console.log(`${' '.repeat(i)}${taskRef(p)} ${p.title} [${p.status}]`);
6974
+ }
6975
+ }
6976
+ const indent = ' '.repeat(parents.length);
6977
+ console.log(`${indent}${taskRef(target)} ${target.title} [${target.status}]`);
6978
+ for (const child of children) {
6979
+ console.log(`${' '.repeat(parents.length + 1)}${taskRef(child)} ${child.title} [${child.status}]`);
6980
+ }
6981
+ if (commits.length) {
6982
+ console.log('');
6983
+ console.log('commits:');
6984
+ for (const c of commits) console.log(` ${c}`);
6985
+ }
6986
+ }
6987
+
6716
6988
  function cmdExport(args) {
6717
6989
  const out = flag(args, '--out') || path.join('.atris', 'state', 'tasks.projection.json');
6718
6990
  const all = hasFlag(args, '--all');
@@ -6774,7 +7046,7 @@ function cmdSetup(args) {
6774
7046
  }
6775
7047
 
6776
7048
  function extractTodoSectionMarkdown(content, sectionName) {
6777
- const escaped = String(sectionName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7049
+ const escaped = escapeRegExp(sectionName || '');
6778
7050
  const match = String(content || '').match(new RegExp(`(?:^|\\n)(##\\s+${escaped}[^\\n]*\\n[\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
6779
7051
  return match ? match[1].trimEnd() : null;
6780
7052
  }
@@ -6978,22 +7250,54 @@ function taskBoardHtml() {
6978
7250
  <meta charset="utf-8">
6979
7251
  <meta name="viewport" content="width=device-width, initial-scale=1">
6980
7252
  <title>Atris Task Factory</title>
7253
+ <link rel="preconnect" href="https://fonts.googleapis.com">
7254
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
7255
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
6981
7256
  <style>
6982
- :root { color-scheme: dark; --bg:#101113; --panel:#17191d; --line:#292d34; --text:#f0f2f5; --muted:#9299a6; --accent:#68d391; --warn:#f6c177; --bad:#f38ba8; }
7257
+ /* aesthetic: machine-room telemetry — warm-tinted dark, mono data, calm signals (no neon) */
7258
+ :root {
7259
+ color-scheme: dark;
7260
+ --bg: oklch(18% 0.012 160);
7261
+ --panel: oklch(22% 0.014 160);
7262
+ --panel-2: oklch(25% 0.016 160);
7263
+ --line: oklch(32% 0.015 160);
7264
+ --text: oklch(93% 0.012 150);
7265
+ --muted: oklch(70% 0.018 160);
7266
+ --accent: oklch(74% 0.115 158);
7267
+ --warn: oklch(81% 0.11 80);
7268
+ --bad: oklch(69% 0.14 25);
7269
+ --info: oklch(75% 0.10 240);
7270
+ --violet: oklch(73% 0.10 300);
7271
+ --sans: 'Space Grotesk', ui-sans-serif, system-ui, -apple-system, sans-serif;
7272
+ --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
7273
+ }
6983
7274
  * { box-sizing: border-box; }
6984
- body { margin:0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }
6985
- header { height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 18px; border-bottom:1px solid var(--line); background:#121418; }
6986
- h1 { font-size:15px; margin:0; font-weight:650; letter-spacing:0; }
7275
+ body {
7276
+ margin:0; color:var(--text);
7277
+ font-family: var(--sans);
7278
+ -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility;
7279
+ background:
7280
+ radial-gradient(1100px 520px at 82% -12%, oklch(30% 0.04 160 / 0.55), transparent 62%),
7281
+ repeating-linear-gradient(0deg, oklch(32% 0.015 160 / 0.16) 0 1px, transparent 1px 34px),
7282
+ var(--bg);
7283
+ background-attachment: fixed;
7284
+ }
7285
+ header { height:60px; display:flex; align-items:center; justify-content:space-between; padding:0 22px; border-bottom:1px solid var(--line); background:linear-gradient(180deg, var(--panel-2), var(--panel)); }
7286
+ h1 { font-size:17px; margin:0; font-weight:700; letter-spacing:-0.01em; }
6987
7287
  .sub { color:var(--muted); font-size:12px; }
6988
- main { display:grid; grid-template-columns: 320px 1fr; height:calc(100vh - 56px); }
6989
- aside { border-right:1px solid var(--line); padding:14px; overflow:auto; background:#121418; }
6990
- section { min-width:0; overflow:auto; padding:14px; }
6991
- label { display:block; color:var(--muted); font-size:11px; margin:10px 0 5px; }
6992
- input, textarea, select { width:100%; border:1px solid var(--line); background:#0d0f12; color:var(--text); border-radius:7px; padding:9px 10px; font:inherit; font-size:13px; }
6993
- textarea { min-height:82px; resize:vertical; }
6994
- button { border:1px solid var(--line); background:#20242a; color:var(--text); border-radius:7px; padding:8px 10px; font:inherit; font-size:12px; cursor:pointer; }
6995
- button:hover { border-color:#3b414b; background:#252a32; }
6996
- .primary { background:#214b35; border-color:#2f684a; }
7288
+ main { display:grid; grid-template-columns: 320px 1fr; height:calc(100vh - 60px); }
7289
+ aside { border-right:1px solid var(--line); padding:16px; overflow:auto; background:var(--panel); }
7290
+ section { min-width:0; overflow:auto; padding:16px; }
7291
+ label { display:block; color:var(--muted); font-size:12px; margin:10px 0 5px; }
7292
+ input, textarea, select { width:100%; border:1px solid var(--line); background:oklch(15% 0.012 160); color:var(--text); border-radius:8px; padding:9px 11px; font:inherit; font-size:13px; transition:border-color .18s cubic-bezier(0.25,1,0.5,1); }
7293
+ input:focus, textarea:focus, select:focus { outline:2px solid var(--accent); outline-offset:1px; border-color:transparent; }
7294
+ textarea { min-height:82px; resize:vertical; font-family:var(--mono); font-size:12px; }
7295
+ button { border:1px solid var(--line); background:var(--panel-2); color:var(--text); border-radius:8px; padding:8px 12px; font:inherit; font-size:12px; cursor:pointer; transition:background .18s cubic-bezier(0.25,1,0.5,1), border-color .18s; }
7296
+ button:hover { border-color:var(--muted); background:oklch(29% 0.018 160); }
7297
+ button:focus-visible { outline:2px solid var(--accent); outline-offset:2px; }
7298
+ button:active { transform:translateY(1px); }
7299
+ .primary { background:oklch(38% 0.07 158); border-color:oklch(48% 0.09 158); color:oklch(96% 0.02 158); }
7300
+ .primary:hover { background:oklch(43% 0.085 158); border-color:var(--accent); }
6997
7301
  .grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
6998
7302
  .overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
6999
7303
  .goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
@@ -7002,35 +7306,62 @@ function taskBoardHtml() {
7002
7306
  .chainitem { display:grid; grid-template-columns:72px 1fr; gap:8px; font-size:12px; line-height:1.3; margin:5px 0; color:var(--muted); }
7003
7307
  .chainitem strong { color:var(--text); font-weight:600; }
7004
7308
  .streams { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:12px; margin-bottom:12px; }
7005
- .stream { background:#12161b; border:1px solid var(--line); border-radius:8px; padding:10px; min-height:126px; }
7006
- .stream h2 { margin:0 0 8px; font-size:12px; color:var(--text); line-height:1.25; }
7007
- .streambar { display:flex; height:7px; overflow:hidden; border-radius:999px; background:#0d0f12; border:1px solid var(--line); margin:8px 0; }
7309
+ .stream { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:126px; }
7310
+ .stream h2 { margin:0 0 8px; font-size:12px; color:var(--text); line-height:1.25; font-weight:500; }
7311
+ .streambar { display:flex; height:7px; overflow:hidden; border-radius:999px; background:oklch(15% 0.012 160); border:1px solid var(--line); margin:8px 0; }
7008
7312
  .streambar span { display:block; min-width:2px; }
7009
- .seg-open { background:#667085; }
7010
- .seg-doing { background:#68d391; }
7011
- .seg-review { background:#f6c177; }
7012
- .seg-blocked { background:#f38ba8; }
7313
+ .seg-open { background:var(--muted); }
7314
+ .seg-doing { background:var(--accent); }
7315
+ .seg-review { background:var(--warn); }
7316
+ .seg-blocked { background:var(--bad); }
7013
7317
  .streamtask { display:grid; grid-template-columns:64px 1fr; gap:8px; color:var(--muted); font-size:11px; line-height:1.25; margin-top:6px; }
7014
7318
  .streamtask strong { color:var(--text); font-weight:550; }
7015
7319
  .col { background:var(--panel); border:1px solid var(--line); border-radius:8px; min-height:160px; overflow:hidden; }
7016
7320
  .col h2 { margin:0; padding:10px 11px; font-size:12px; color:var(--muted); border-bottom:1px solid var(--line); display:flex; justify-content:space-between; }
7017
7321
  .cards { padding:8px; display:flex; flex-direction:column; gap:8px; }
7018
- .card { text-align:left; width:100%; background:#111419; border:1px solid #252a31; border-radius:8px; padding:9px; }
7019
- .card.active { border-color:#4c7a61; box-shadow:0 0 0 1px rgba(104,211,145,.2); }
7322
+ .card { text-align:left; width:100%; background:var(--panel-2); border:1px solid var(--line); border-radius:8px; padding:9px; transition:transform .18s cubic-bezier(0.25,1,0.5,1), border-color .18s, background .18s; }
7323
+ .card:hover { transform:scale(1.02); border-color:var(--muted); background:oklch(28% 0.018 160); }
7324
+ .card.active { border-color:var(--accent); box-shadow:0 0 0 1px oklch(74% 0.115 158 / 0.3); }
7020
7325
  .title { font-size:13px; line-height:1.25; }
7021
- .meta { margin-top:6px; color:var(--muted); font-size:11px; display:flex; gap:6px; flex-wrap:wrap; }
7326
+ .meta { margin-top:6px; color:var(--muted); font-size:11px; display:flex; gap:6px; flex-wrap:wrap; font-family:var(--mono); }
7022
7327
  .pill { border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
7023
7328
  .why { margin-top:7px; color:var(--muted); font-size:11px; line-height:1.25; }
7024
- .fact { margin:10px 0; background:#0d0f12; border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; line-height:1.35; }
7329
+ .fact { margin:10px 0; background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; line-height:1.35; }
7025
7330
  .fact b { color:var(--muted); font-size:11px; display:block; margin-bottom:3px; }
7026
7331
  .room { margin-top:14px; border-top:1px solid var(--line); padding-top:12px; }
7027
7332
  .room h3 { margin:0 0 4px; font-size:14px; }
7028
7333
  .thread { margin:10px 0; display:flex; flex-direction:column; gap:7px; }
7029
- .msg { background:#0d0f12; border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; }
7334
+ .msg { background:oklch(15% 0.012 160); border:1px solid var(--line); border-radius:7px; padding:8px; font-size:12px; }
7030
7335
  .msg .who { color:var(--muted); font-size:11px; margin-bottom:3px; }
7031
7336
  .actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px; }
7032
7337
  .full { grid-column:1 / -1; }
7033
7338
  .empty { color:var(--muted); font-size:12px; padding:10px; }
7339
+ /* heartbeat strip */
7340
+ .beat { display:flex; align-items:center; gap:8px; font-size:12px; color:var(--muted); font-family:var(--mono); }
7341
+ .beat .dot { width:9px; height:9px; border-radius:50%; background:var(--muted); flex:none; }
7342
+ .beat.alive .dot { background:var(--accent); box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0.6); animation:beat 2s cubic-bezier(0.25,1,0.5,1) infinite; }
7343
+ .beat.stale .dot { background:var(--bad); }
7344
+ .beat b { color:var(--accent); font-weight:600; }
7345
+ .beat .warn { color:var(--warn); }
7346
+ @keyframes beat { 0%{box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0.5)} 70%{box-shadow:0 0 0 8px oklch(74% 0.115 158 / 0)} 100%{box-shadow:0 0 0 0 oklch(74% 0.115 158 / 0)} }
7347
+ @media (prefers-reduced-motion: reduce) { .beat.alive .dot { animation:none; } *, *::before, *::after { transition:none !important; } }
7348
+ /* activity feed */
7349
+ .activity { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:0; margin-bottom:12px; max-height:46vh; overflow:auto; }
7350
+ .activity h2 { margin:0; position:sticky; top:0; background:var(--panel); padding:11px; font-size:12px; color:var(--muted); font-weight:650; border-bottom:1px solid var(--line); z-index:1; }
7351
+ .ev { display:grid; grid-template-columns:52px 64px 1fr auto; gap:10px; align-items:baseline; padding:7px 11px; border-bottom:1px solid oklch(28% 0.013 160); font-size:12px; line-height:1.3; font-family:var(--mono); transition:background .15s ease; }
7352
+ .ev:last-child { border-bottom:0; }
7353
+ .ev:hover { background:oklch(25% 0.016 160); }
7354
+ .ev .t { color:var(--muted); font-variant-numeric:tabular-nums; font-size:11px; }
7355
+ .ev .src { font-size:11px; border:1px solid var(--line); border-radius:999px; padding:1px 7px; color:var(--muted); text-align:center; }
7356
+ .ev .src.pulse { color:var(--accent); border-color:oklch(74% 0.115 158 / 0.45); }
7357
+ .ev .src.reward { color:var(--warn); border-color:oklch(81% 0.11 80 / 0.45); }
7358
+ .ev .src.xp { color:var(--info); border-color:oklch(75% 0.10 240 / 0.45); }
7359
+ .ev .src.mission { color:var(--violet); border-color:oklch(73% 0.10 300 / 0.45); }
7360
+ .ev .msg { color:var(--text); min-width:0; }
7361
+ .ev .msg .d { color:var(--muted); font-size:11px; }
7362
+ .ev.bad .msg { color:var(--bad); }
7363
+ .ev .rw { font-variant-numeric:tabular-nums; font-size:11px; color:var(--muted); }
7364
+ .ev .rw.pos { color:var(--accent); } .ev .rw.neg { color:var(--bad); }
7034
7365
  @media (max-width: 980px) { main { grid-template-columns:1fr; height:auto; } aside { border-right:0; border-bottom:1px solid var(--line); } .grid, .overview { grid-template-columns:1fr; } }
7035
7366
  </style>
7036
7367
  </head>
@@ -7038,7 +7369,7 @@ function taskBoardHtml() {
7038
7369
  <header>
7039
7370
  <div>
7040
7371
  <h1>Atris Task Factory</h1>
7041
- <div class="sub" data-smoke="hello-from-ui">hello from UI</div>
7372
+ <div class="beat" id="heartbeat"><span class="dot"></span><span>heartbeat: loading…</span></div>
7042
7373
  </div>
7043
7374
  <button id="refresh">Refresh</button>
7044
7375
  </header>
@@ -7057,6 +7388,7 @@ function taskBoardHtml() {
7057
7388
  </aside>
7058
7389
  <section>
7059
7390
  <div class="overview" id="overview"></div>
7391
+ <div class="activity" id="activity"><h2>Live Stream</h2><div class="empty">waiting for the agent…</div></div>
7060
7392
  <div class="streams" id="streams"></div>
7061
7393
  <div class="grid" id="board"></div>
7062
7394
  </section>
@@ -7102,10 +7434,61 @@ function taskBoardHtml() {
7102
7434
  return 'done';
7103
7435
  }
7104
7436
 
7437
+ function esc(s) {
7438
+ return String(s == null ? '' : s).replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
7439
+ }
7440
+
7441
+ function fmtTime(ms) {
7442
+ try { return new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); }
7443
+ catch (e) { return ''; }
7444
+ }
7445
+
7446
+ function renderHeartbeat(hb) {
7447
+ const el = $('heartbeat');
7448
+ el.className = 'beat ' + (hb.state || 'idle');
7449
+ if (hb.state === 'stale') {
7450
+ el.innerHTML = '<span class="dot"></span><span class="warn">stale</span><span>· ' + esc(hb.stale_reason || '') + ' · ' + (hb.total_ticks || 0) + ' ticks</span>';
7451
+ return;
7452
+ }
7453
+ if (!hb.total_ticks) {
7454
+ el.innerHTML = '<span class="dot"></span><span>idle · no ticks yet · run: atris pulse tick</span>';
7455
+ return;
7456
+ }
7457
+ const age = hb.last_tick_age_min == null ? '' : (hb.last_tick_age_min === 0 ? 'just now' : hb.last_tick_age_min + 'm ago');
7458
+ el.innerHTML = '<span class="dot"></span><b>alive</b>'
7459
+ + '<span>· last tick ' + age + '</span>'
7460
+ + (hb.last_what ? '<span>· ' + esc(hb.last_what) + '</span>' : '')
7461
+ + '<span>· ' + hb.total_ticks + ' ticks, reward ' + (hb.reward_sum || 0) + '</span>';
7462
+ }
7463
+
7464
+ function renderActivity(events) {
7465
+ const el = $('activity');
7466
+ if (!events.length) { el.innerHTML = '<h2>Live Stream</h2><div class="empty">no activity yet — fire a tick: atris pulse tick</div>'; return; }
7467
+ let html = '<h2>Live Stream · ' + events.length + ' events</h2>';
7468
+ for (const e of events) {
7469
+ const rwCls = e.reward == null ? '' : (e.reward > 0 ? 'pos' : (e.reward < 0 ? 'neg' : ''));
7470
+ const rw = e.reward == null ? '' : (e.reward > 0 ? '+' + e.reward : '' + e.reward);
7471
+ html += '<div class="ev ' + (e.status === 'bad' ? 'bad' : '') + '">'
7472
+ + '<span class="t">' + (e.ms ? fmtTime(e.ms) : '') + '</span>'
7473
+ + '<span class="src ' + esc(e.source) + '">' + esc(e.source) + '</span>'
7474
+ + '<span class="msg">' + esc(e.title) + (e.detail ? ' <span class="d">' + esc(e.detail) + '</span>' : '') + '</span>'
7475
+ + '<span class="rw ' + rwCls + '">' + rw + '</span>'
7476
+ + '</div>';
7477
+ }
7478
+ el.innerHTML = html;
7479
+ }
7480
+
7481
+ async function loadStream() {
7482
+ const data = await api('/api/stream');
7483
+ renderHeartbeat(data.heartbeat || {});
7484
+ renderActivity(data.events || []);
7485
+ }
7486
+
7105
7487
  async function load() {
7106
7488
  const data = await api('/api/tasks');
7107
7489
  state = data.projection;
7108
7490
  render();
7491
+ loadStream().catch(() => {});
7109
7492
  }
7110
7493
 
7111
7494
  function render() {
@@ -7320,6 +7703,24 @@ async function handleTaskApi(req, res, taskDb, db) {
7320
7703
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
7321
7704
  return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
7322
7705
  }
7706
+ if (req.method === 'GET' && url.pathname === '/api/stream') {
7707
+ // Time-ordered feed of what the agent actually did + heartbeat liveness.
7708
+ const { projection } = writeDefaultProjection(taskDb, db);
7709
+ const root = projection.workspace_root || process.cwd();
7710
+ const stateDir = path.join(root, '.atris', 'state');
7711
+ const activity = require('../lib/activity-stream');
7712
+ const { readJsonl } = require('../lib/pulse');
7713
+ const rd = (f) => readJsonl(path.join(stateDir, f));
7714
+ const pulseReceipts = rd('pulse_agi_loop_receipts.jsonl');
7715
+ const events = activity.buildActivityStream({
7716
+ pulseReceipts,
7717
+ scorecards: rd('scorecards.jsonl'),
7718
+ taskEpisodes: rd('task_episodes.jsonl').slice(-200),
7719
+ xpReceipts: rd('career_xp_receipts.jsonl').slice(-200),
7720
+ missionEvents: rd('mission_events.jsonl').slice(-200),
7721
+ }, { limit: 60 });
7722
+ return sendJson(res, 200, { ok: true, heartbeat: activity.buildHeartbeat(pulseReceipts), events });
7723
+ }
7323
7724
  if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities') {
7324
7725
  return sendJson(res, 200, {
7325
7726
  ok: true,
@@ -7636,7 +8037,13 @@ async function handleTaskApi(req, res, taskDb, db) {
7636
8037
  const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
7637
8038
  const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
7638
8039
  if (proofIssue) return sendProofIssue(res, proof, proofIssue);
7639
- const done = taskDb.doneTask(db, { id: taskId, status: failed ? 'failed' : 'done' });
8040
+ const done = taskDb.doneTask(db, {
8041
+ id: taskId,
8042
+ status: failed ? 'failed' : 'done',
8043
+ actor: String(body.actor || DEFAULT_OWNER),
8044
+ action: failed ? 'failed' : 'finished',
8045
+ proof,
8046
+ });
7640
8047
  if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
7641
8048
  let episode = null;
7642
8049
  let nextCreated = null;
@@ -7706,7 +8113,14 @@ async function handleTaskApi(req, res, taskDb, db) {
7706
8113
  if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
7707
8114
  const parsedReward = parseAcceptReward(body.reward);
7708
8115
  if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
7709
- const done = taskDb.doneTask(db, { id: taskId, status: 'done', actor: String(body.actor || DEFAULT_OWNER), allowReview: true });
8116
+ const done = taskDb.doneTask(db, {
8117
+ id: taskId,
8118
+ status: 'done',
8119
+ actor: String(body.actor || DEFAULT_OWNER),
8120
+ allowReview: true,
8121
+ action: 'accepted',
8122
+ proof,
8123
+ });
7710
8124
  if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
7711
8125
  const reviewed = taskDb.reviewTask(db, {
7712
8126
  id: taskId,
@@ -7896,6 +8310,7 @@ async function run(args) {
7896
8310
  case 'setup': return cmdSetup(rest);
7897
8311
  case 'serve': return cmdServe(rest);
7898
8312
  case 'import': return cmdImport(rest);
8313
+ case 'lineage': return cmdLineage(rest);
7899
8314
  case 'events': return cmdEvents(rest);
7900
8315
  case 'export': return cmdExport(rest);
7901
8316
  case 'render': return cmdRender(rest);
@@ -7920,4 +8335,4 @@ async function run(args) {
7920
8335
  }
7921
8336
  }
7922
8337
 
7923
- module.exports = { run };
8338
+ module.exports = { run, taskDayGroups, AGENT_ENV_MARKERS };