atris 3.16.1 → 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 (58) hide show
  1. package/README.md +32 -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 +400 -30
  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 +42 -18
  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 +9 -4
  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 +105 -27
  31. package/commands/pulse.js +504 -0
  32. package/commands/radar.js +1 -0
  33. package/commands/recap.js +55 -25
  34. package/commands/run.js +615 -22
  35. package/commands/slop.js +173 -0
  36. package/commands/spaceship.js +39 -0
  37. package/commands/sync.js +0 -2
  38. package/commands/task.js +429 -37
  39. package/commands/verify.js +7 -3
  40. package/lib/activity-stream.js +166 -0
  41. package/lib/auto-accept-certified.js +23 -1
  42. package/lib/context-gatherer.js +170 -0
  43. package/lib/escape-regexp.js +13 -0
  44. package/lib/file-ops.js +6 -3
  45. package/lib/journal.js +1 -1
  46. package/lib/lesson-contradiction.js +113 -0
  47. package/lib/policy-lessons.js +3 -2
  48. package/lib/pulse.js +401 -0
  49. package/lib/runner-command.js +156 -0
  50. package/lib/slides-deck.js +236 -0
  51. package/lib/state-detection.js +1 -4
  52. package/lib/task-db.js +101 -4
  53. package/lib/task-proof.js +1 -1
  54. package/lib/todo-fallback.js +2 -1
  55. package/lib/todo-sections.js +33 -0
  56. package/package.json +1 -2
  57. package/utils/api.js +14 -2
  58. 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,
@@ -4604,6 +4613,93 @@ function cmdClaim(args) {
4604
4613
  }
4605
4614
  }
4606
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
+
4607
4703
  function cmdNext(args) {
4608
4704
  const owner = flag(args, '--as') || DEFAULT_OWNER;
4609
4705
  const taskDb = getTaskDb();
@@ -4650,6 +4746,40 @@ function cmdNext(args) {
4650
4746
  const continueWorkCommand = handoff.next_action === 'continue_work'
4651
4747
  ? continueWorkCommandForTask(reviewTask, { owner })
4652
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
+ }
4653
4783
  if (wantsJson(args)) {
4654
4784
  printJson({
4655
4785
  ok: true,
@@ -4658,6 +4788,7 @@ function cmdNext(args) {
4658
4788
  owner: String(owner),
4659
4789
  projection_path: outPath,
4660
4790
  handoff,
4791
+ next_agent_action: nextAgentAction,
4661
4792
  continue_work_command: continueWorkCommand,
4662
4793
  continue_work_api: continueWorkCommand ? { method: 'POST', path: `/api/tasks/${encodeURIComponent(reviewTask.id)}/continue-work` } : null,
4663
4794
  review_task: reviewTask,
@@ -4671,8 +4802,15 @@ function cmdNext(args) {
4671
4802
  console.log(handoff.next_action === 'continue_work'
4672
4803
  ? 'Continue work elsewhere; AgentXP waits for human accept.'
4673
4804
  : handoff.next_action === 'human_accept_waiting'
4674
- ? '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.')
4675
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
+ }
4676
4814
  if (continueWorkCommand) console.log(`Command: ${continueWorkCommand}`);
4677
4815
  return;
4678
4816
  }
@@ -6020,7 +6158,13 @@ function cmdDone(args) {
6020
6158
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task done', proof);
6021
6159
  else if (proof) requireMeaningfulTaskProof('atris task done', proof);
6022
6160
  }
6023
- 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
+ });
6024
6168
  if (result.updated) {
6025
6169
  const review = hasReview ? taskDb.reviewTask(db, {
6026
6170
  id: taskId,
@@ -6095,7 +6239,13 @@ function cmdFinish(args) {
6095
6239
  if (!failed || hasReview) requireMeaningfulTaskProof('atris task finish', proof);
6096
6240
  else if (proof) requireMeaningfulTaskProof('atris task finish', proof);
6097
6241
  }
6098
- 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
+ });
6099
6249
  if (!done.updated) {
6100
6250
  const detail = `finish failed: ${taskId} not in open|claimed`;
6101
6251
  if (wantsJson(args)) {
@@ -6296,7 +6446,14 @@ function cmdAccept(args) {
6296
6446
  console.error('atris task accept: reward must be a positive number');
6297
6447
  process.exit(2);
6298
6448
  }
6299
- 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
+ });
6300
6457
  if (!done.updated) {
6301
6458
  console.error(`accept failed: ${taskId} not open|claimed|review`);
6302
6459
  process.exit(1);
@@ -6358,7 +6515,14 @@ function stampAutoAcceptMetadata(taskDb, db, taskId, actor, policy) {
6358
6515
  }
6359
6516
 
6360
6517
  function acceptReviewTask(taskDb, db, taskId, { actor, proof, reward, lesson = '', nextTask = '' }) {
6361
- 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
+ });
6362
6526
  if (!done.updated) {
6363
6527
  return { ok: false, reason: 'not_open_claimed_or_review' };
6364
6528
  }
@@ -6736,6 +6900,91 @@ function cmdEvents(args) {
6736
6900
  }
6737
6901
  }
6738
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
+
6739
6988
  function cmdExport(args) {
6740
6989
  const out = flag(args, '--out') || path.join('.atris', 'state', 'tasks.projection.json');
6741
6990
  const all = hasFlag(args, '--all');
@@ -6797,7 +7046,7 @@ function cmdSetup(args) {
6797
7046
  }
6798
7047
 
6799
7048
  function extractTodoSectionMarkdown(content, sectionName) {
6800
- const escaped = String(sectionName || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7049
+ const escaped = escapeRegExp(sectionName || '');
6801
7050
  const match = String(content || '').match(new RegExp(`(?:^|\\n)(##\\s+${escaped}[^\\n]*\\n[\\s\\S]*?)(?=\\n##(?!#)\\s+|$)`, 'i'));
6802
7051
  return match ? match[1].trimEnd() : null;
6803
7052
  }
@@ -7001,22 +7250,54 @@ function taskBoardHtml() {
7001
7250
  <meta charset="utf-8">
7002
7251
  <meta name="viewport" content="width=device-width, initial-scale=1">
7003
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">
7004
7256
  <style>
7005
- :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
+ }
7006
7274
  * { box-sizing: border-box; }
7007
- body { margin:0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; background:var(--bg); color:var(--text); }
7008
- header { height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 18px; border-bottom:1px solid var(--line); background:#121418; }
7009
- 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; }
7010
7287
  .sub { color:var(--muted); font-size:12px; }
7011
- main { display:grid; grid-template-columns: 320px 1fr; height:calc(100vh - 56px); }
7012
- aside { border-right:1px solid var(--line); padding:14px; overflow:auto; background:#121418; }
7013
- section { min-width:0; overflow:auto; padding:14px; }
7014
- label { display:block; color:var(--muted); font-size:11px; margin:10px 0 5px; }
7015
- 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; }
7016
- textarea { min-height:82px; resize:vertical; }
7017
- button { border:1px solid var(--line); background:#20242a; color:var(--text); border-radius:7px; padding:8px 10px; font:inherit; font-size:12px; cursor:pointer; }
7018
- button:hover { border-color:#3b414b; background:#252a32; }
7019
- .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); }
7020
7301
  .grid { display:grid; grid-template-columns: repeat(var(--board-columns, 6), minmax(160px, 1fr)); gap:12px; align-items:start; }
7021
7302
  .overview { display:grid; grid-template-columns: minmax(260px, 1.4fr) minmax(260px, 1fr); gap:12px; margin-bottom:12px; }
7022
7303
  .goalbox, .chainbox { background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:11px; min-height:88px; }
@@ -7025,35 +7306,62 @@ function taskBoardHtml() {
7025
7306
  .chainitem { display:grid; grid-template-columns:72px 1fr; gap:8px; font-size:12px; line-height:1.3; margin:5px 0; color:var(--muted); }
7026
7307
  .chainitem strong { color:var(--text); font-weight:600; }
7027
7308
  .streams { display:grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap:12px; margin-bottom:12px; }
7028
- .stream { background:#12161b; border:1px solid var(--line); border-radius:8px; padding:10px; min-height:126px; }
7029
- .stream h2 { margin:0 0 8px; font-size:12px; color:var(--text); line-height:1.25; }
7030
- .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; }
7031
7312
  .streambar span { display:block; min-width:2px; }
7032
- .seg-open { background:#667085; }
7033
- .seg-doing { background:#68d391; }
7034
- .seg-review { background:#f6c177; }
7035
- .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); }
7036
7317
  .streamtask { display:grid; grid-template-columns:64px 1fr; gap:8px; color:var(--muted); font-size:11px; line-height:1.25; margin-top:6px; }
7037
7318
  .streamtask strong { color:var(--text); font-weight:550; }
7038
7319
  .col { background:var(--panel); border:1px solid var(--line); border-radius:8px; min-height:160px; overflow:hidden; }
7039
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; }
7040
7321
  .cards { padding:8px; display:flex; flex-direction:column; gap:8px; }
7041
- .card { text-align:left; width:100%; background:#111419; border:1px solid #252a31; border-radius:8px; padding:9px; }
7042
- .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); }
7043
7325
  .title { font-size:13px; line-height:1.25; }
7044
- .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); }
7045
7327
  .pill { border:1px solid var(--line); border-radius:999px; padding:1px 6px; }
7046
7328
  .why { margin-top:7px; color:var(--muted); font-size:11px; line-height:1.25; }
7047
- .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; }
7048
7330
  .fact b { color:var(--muted); font-size:11px; display:block; margin-bottom:3px; }
7049
7331
  .room { margin-top:14px; border-top:1px solid var(--line); padding-top:12px; }
7050
7332
  .room h3 { margin:0 0 4px; font-size:14px; }
7051
7333
  .thread { margin:10px 0; display:flex; flex-direction:column; gap:7px; }
7052
- .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; }
7053
7335
  .msg .who { color:var(--muted); font-size:11px; margin-bottom:3px; }
7054
7336
  .actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:10px; }
7055
7337
  .full { grid-column:1 / -1; }
7056
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); }
7057
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; } }
7058
7366
  </style>
7059
7367
  </head>
@@ -7061,7 +7369,7 @@ function taskBoardHtml() {
7061
7369
  <header>
7062
7370
  <div>
7063
7371
  <h1>Atris Task Factory</h1>
7064
- <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>
7065
7373
  </div>
7066
7374
  <button id="refresh">Refresh</button>
7067
7375
  </header>
@@ -7080,6 +7388,7 @@ function taskBoardHtml() {
7080
7388
  </aside>
7081
7389
  <section>
7082
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>
7083
7392
  <div class="streams" id="streams"></div>
7084
7393
  <div class="grid" id="board"></div>
7085
7394
  </section>
@@ -7125,10 +7434,61 @@ function taskBoardHtml() {
7125
7434
  return 'done';
7126
7435
  }
7127
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
+
7128
7487
  async function load() {
7129
7488
  const data = await api('/api/tasks');
7130
7489
  state = data.projection;
7131
7490
  render();
7491
+ loadStream().catch(() => {});
7132
7492
  }
7133
7493
 
7134
7494
  function render() {
@@ -7343,6 +7703,24 @@ async function handleTaskApi(req, res, taskDb, db) {
7343
7703
  const { projection, outPath } = writeDefaultProjection(taskDb, db);
7344
7704
  return sendJson(res, 200, { ok: true, projection_path: outPath, projection });
7345
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
+ }
7346
7724
  if (req.method === 'GET' && url.pathname === '/api/tasks/capabilities') {
7347
7725
  return sendJson(res, 200, {
7348
7726
  ok: true,
@@ -7659,7 +8037,13 @@ async function handleTaskApi(req, res, taskDb, db) {
7659
8037
  const shouldReview = Boolean(body.proof || body.lesson || body.next || body.reward !== undefined);
7660
8038
  const proofIssue = meaningfulTaskProofIssue(proof, { required: !failed || shouldReview });
7661
8039
  if (proofIssue) return sendProofIssue(res, proof, proofIssue);
7662
- 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
+ });
7663
8047
  if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_or_claimed' });
7664
8048
  let episode = null;
7665
8049
  let nextCreated = null;
@@ -7729,7 +8113,14 @@ async function handleTaskApi(req, res, taskDb, db) {
7729
8113
  if (hasExplicitNext && !nextTask.trim()) clearedFields.push('next_task');
7730
8114
  const parsedReward = parseAcceptReward(body.reward);
7731
8115
  if (!parsedReward.ok) return sendJson(res, 400, { ok: false, reason: 'invalid_reward', detail: 'reward must be a positive number' });
7732
- 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
+ });
7733
8124
  if (!done.updated) return sendJson(res, 409, { ok: false, reason: 'not_open_claimed_or_review' });
7734
8125
  const reviewed = taskDb.reviewTask(db, {
7735
8126
  id: taskId,
@@ -7919,6 +8310,7 @@ async function run(args) {
7919
8310
  case 'setup': return cmdSetup(rest);
7920
8311
  case 'serve': return cmdServe(rest);
7921
8312
  case 'import': return cmdImport(rest);
8313
+ case 'lineage': return cmdLineage(rest);
7922
8314
  case 'events': return cmdEvents(rest);
7923
8315
  case 'export': return cmdExport(rest);
7924
8316
  case 'render': return cmdRender(rest);
@@ -7943,4 +8335,4 @@ async function run(args) {
7943
8335
  }
7944
8336
  }
7945
8337
 
7946
- module.exports = { run, taskDayGroups };
8338
+ module.exports = { run, taskDayGroups, AGENT_ENV_MARKERS };