atris 3.47.0 → 3.48.1

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.
package/atris/CLAUDE.md CHANGED
@@ -100,6 +100,8 @@ The human approves work by reading, so how you report IS the product.
100
100
 
101
101
  ## Rules (Non‑Negotiable)
102
102
 
103
+ - YouTube link in a message: run `atris youtube notes <url>` before saying anything about the video. The transcript is the source; notes from model memory are fabrication.
104
+
103
105
  - Plan = ASCII visualization + approval gate. Do not execute during planning.
104
106
  - Execute step-by-step, verify as you go, update artifacts (`TODO.md`, `MAP.md`) when reality changes.
105
107
  - Delete completed tasks (validator cleans to target state = 0).
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: youtube
3
- description: "Process YouTube videos extract insights, answer questions, store as knowledge. 5 credits per video. Triggers on: youtube, video, process video, watch this, learn from video."
4
- version: 2.3.0
3
+ description: "A YouTube link in any message routes here. Run atris youtube notes <url> FIRST: free, about 30 seconds, quotes verified against the transcript. Never summarize a video from model memory, that is fabrication. Use atris youtube process only to store it as queryable knowledge (5 credits). Triggers on: any youtube.com or youtu.be link, youtube, video, watch this, notes on this."
4
+ version: 2.4.0
5
5
  tags:
6
6
  - youtube
7
7
  - research
@@ -15,9 +15,11 @@ Process any YouTube video through Atris transcript-first analysis. The CLI extra
15
15
 
16
16
  ## Route first: learning vs product
17
17
 
18
- Two rails process YouTube videos pick before running anything:
18
+ Two rails process YouTube videos: pick before running anything:
19
19
 
20
20
  - **Learning / work rail** → use the `alpha-learn` skill (ytnotes). Local yt-dlp + grok, zero credits, podcastnotes-style notes, tweet-feed output, `[claimable]` entries in today's journal for other agents. Use this when the goal is to LEARN from a video or mine it for Atris work.
21
+ Canonical ytnotes source is `scripts/det/ytnotes` (install: `ln -sf "$PWD/scripts/det/ytnotes" ~/.local/bin/ytnotes`).
22
+ Runs are scored by `node scripts/det/ytrail-eval.js`.
21
23
  - **Product rail** → this skill (`atris youtube process`). Credits-billed, stores knowledge in the Atris backend, customer-facing path. Use this when a customer/agent needs the video stored as Atris knowledge or answered via the API.
22
24
 
23
25
  If the user says "learn from", "notes on", "alpha", or "rabbit hole" → alpha-learn. If they say "process", "store", "add to knowledge" → this skill.
@@ -189,7 +191,7 @@ Every important insight should carry a timestamp when the transcript provides on
189
191
  | `401` | Token expired/invalid | `atris login --force` |
190
192
  | `402` | Not enough credits | Check balance, purchase at atris.ai |
191
193
  | `400` | Invalid YouTube URL | Check URL format |
192
- | `502` | Transcript or cloud processing failed | Retry credits auto-refunded when backend fails |
194
+ | `502` | Transcript or cloud processing failed | Retry; credits auto-refunded when backend fails |
193
195
 
194
196
  ---
195
197
 
package/bin/atris.js CHANGED
@@ -38,6 +38,7 @@ try {
38
38
  const {
39
39
  checkForUpdates,
40
40
  showUpdateNotification,
41
+ autoUpdate,
41
42
  inspectInstallGitState,
42
43
  formatInstallGitWarning,
43
44
  } = require('../utils/update-check');
@@ -106,6 +107,7 @@ if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'updat
106
107
  showUpdateNotification(updateInfo, {
107
108
  packageRoot: path.join(__dirname, '..'),
108
109
  });
110
+ autoUpdate(updateInfo, { packageRoot: path.join(__dirname, '..') });
109
111
  }
110
112
  return updateInfo;
111
113
  })
@@ -511,6 +513,7 @@ function showHelp() {
511
513
  console.log(' orb - Pick next moves while engine jobs work in the background');
512
514
  console.log(' activate - Load Atris context');
513
515
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
516
+ console.log(' who - Show local engines and team members working, waiting, done, or stale');
514
517
  console.log(' stream - Watch the whole team work live in one terminal');
515
518
  console.log(' team - One team view: members, roles, engine assignments (presence for live)');
516
519
  console.log(' watch - Turn one sentence into an always-on background watcher');
@@ -569,7 +572,7 @@ function showHelp() {
569
572
  console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
570
573
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
571
574
  console.log(' visualize - Generate a Slack/deck-ready visual from a prompt');
572
- console.log(' youtube - Process YouTube videos with timestamped transcript-first analysis');
575
+ console.log(' youtube - Free local notes or 5-credit cloud process for YouTube videos');
573
576
  console.log('');
574
577
  console.log('Experiments:');
575
578
  console.log(' experiments init [slug] - Prepare atris/experiments/ or scaffold a pack');
@@ -1827,6 +1830,10 @@ if (command === 'init') {
1827
1830
  Promise.resolve(require('../commands/team').teamCommand(process.argv.slice(3)))
1828
1831
  .then((code) => process.exit(code || 0))
1829
1832
  .catch((err) => { console.error(`\nerror: ${err.message || err}`); process.exit(1); });
1833
+ } else if (command === 'who') {
1834
+ Promise.resolve(require('../commands/who').whoCommand(process.argv.slice(3)))
1835
+ .then((code) => process.exit(code || 0))
1836
+ .catch((err) => { console.error(`\nerror: ${err.message || err}`); process.exit(1); });
1830
1837
  } else if (command === 'wish') {
1831
1838
  Promise.resolve(require('../commands/wish').wishCommand(process.argv.slice(3)))
1832
1839
  .then((code) => process.exit(code || 0))
package/commands/dream.js CHANGED
@@ -10,7 +10,7 @@ const {
10
10
 
11
11
  const DREAMS_FILE = ['.atris', 'state', 'dreams.jsonl'];
12
12
  const DAY_MS = 24 * 60 * 60 * 1000;
13
- const DREAM_TIMEOUT_MS = 60 * 1000;
13
+ const DREAM_TIMEOUT_MS = 5 * 60 * 1000;
14
14
  const MAX_CONTEXT_CHARS = 7000;
15
15
  const MAX_SECTION_CHARS = 2400;
16
16
 
@@ -37,10 +37,11 @@ const {
37
37
  resolveEngineForRoleRanked,
38
38
  requireEngineBin,
39
39
  engineDoctorReport,
40
+ engineFailureHealthStatus,
40
41
  setEngineOverrides,
41
42
  setEngineHealth,
42
43
  } = require('../lib/engine-registry');
43
- const { FLEET_CAPABLE, runDispatchFlight } = require('../lib/fleet');
44
+ const { DISPATCH_CAPABLE, runDispatchFlight } = require('../lib/fleet');
44
45
  const {
45
46
  buildReadOnlyEngineInvocation,
46
47
  runEngineAskCommand,
@@ -1379,12 +1380,12 @@ function parseDispatchArgs(args) {
1379
1380
  function runDispatchCommand(args, root) {
1380
1381
  const { taskIds, engine, promptFile, base, json, yolo } = parseDispatchArgs(args);
1381
1382
  if (!taskIds.length || !engine) {
1382
- console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]');
1383
+ console.error('usage: atris engine dispatch <task-id> [<task-id> ...] --engine <engine> [--prompt-file <f>] [--yolo]');
1383
1384
  return 2;
1384
1385
  }
1385
1386
  const canonical = canonicalEngineName(engine);
1386
- if (!canonical || !FLEET_CAPABLE.includes(canonical)) {
1387
- console.error(`engine dispatch: --engine must be one of ${FLEET_CAPABLE.join(', ')}`);
1387
+ if (!canonical || !DISPATCH_CAPABLE.includes(canonical)) {
1388
+ console.error(`engine dispatch: --engine must be one of ${DISPATCH_CAPABLE.join(', ')}`);
1388
1389
  return 2;
1389
1390
  }
1390
1391
  // Argument-shape errors surface before environment errors: --prompt-file
@@ -1407,7 +1408,13 @@ function runDispatchCommand(args, root) {
1407
1408
  try {
1408
1409
  requireEngineBin(canonical);
1409
1410
  } catch (err) {
1410
- console.error(`engine dispatch: ${err.message}`);
1411
+ const status = engineFailureHealthStatus({ status: 'errored', reason: err.message });
1412
+ if (status) {
1413
+ try { setEngineHealth(canonical, status, root); } catch { /* best effort */ }
1414
+ }
1415
+ console.error(canonical === 'fable'
1416
+ ? `fable handoff failed: ${err.message}`
1417
+ : `engine dispatch: ${err.message}`);
1411
1418
  return 2;
1412
1419
  }
1413
1420
  return runDispatchFlight({ root, taskIds, engine: canonical, prompt: promptOverride, yolo, ...(base ? { checkoutBase: base } : {}) }).then((flight) => {
@@ -7288,6 +7288,7 @@ function composeWakeDecision(ctx, verdict) {
7288
7288
  state: ctx.state,
7289
7289
  goal: verdict.goal,
7290
7290
  current_experiment: verdict.current_experiment,
7291
+ now_file: ctx.nowFile,
7291
7292
  };
7292
7293
  if ('autonomous_problem' in verdict) result.autonomous_problem = verdict.autonomous_problem;
7293
7294
  result.checks = ctx.checks;
@@ -7471,6 +7472,11 @@ const WAKE_DECISION_RULES = [
7471
7472
 
7472
7473
  function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeKind(name) } = {}) {
7473
7474
  const purpose = missionPurpose(paths);
7475
+ const nowPath = path.join(paths.memberDir, 'now.md');
7476
+ const nowFile = fs.existsSync(nowPath) ? {
7477
+ path: path.relative(process.cwd(), nowPath),
7478
+ excerpt: String(purpose.nowText || '').slice(0, 500),
7479
+ } : null;
7474
7480
  const steering = readSteeringMemory(paths, name);
7475
7481
  const state = loadMemberGoals(name, paths);
7476
7482
  const goal = activeGoal(state);
@@ -7519,7 +7525,7 @@ function wakeDecision(name, paths, { force = false, runtimeKind = memberRuntimeK
7519
7525
  next_command: wakeScores.selected.next_command,
7520
7526
  } : null;
7521
7527
 
7522
- const ctx = { name, force, purpose, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
7528
+ const ctx = { name, force, purpose, nowFile, steering, state, goal, current, blocked, evidence, workspace, checks, wakeScores };
7523
7529
  for (const rule of WAKE_DECISION_RULES) {
7524
7530
  const verdict = rule(ctx);
7525
7531
  if (verdict) return composeWakeDecision(ctx, verdict);
@@ -7624,6 +7630,11 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
7624
7630
  nextCommand = `atris member review ${name} ${experiment.id} --accept --proof "..." --value 4`;
7625
7631
  }
7626
7632
 
7633
+ const goals = (state.goals || []).map((item) => ({
7634
+ id: item.id || null,
7635
+ title: item.title || null,
7636
+ status: item.status || null,
7637
+ }));
7627
7638
  const receiptPayload = {
7628
7639
  schema: 'atris.member_wake.v1',
7629
7640
  created_at: now,
@@ -7636,6 +7647,8 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
7636
7647
  ask: planned.ask || null,
7637
7648
  next_command: nextCommand,
7638
7649
  mission: planned.mission,
7650
+ now_file: planned.now_file,
7651
+ goals,
7639
7652
  steering: planned.steering,
7640
7653
  evidence: planned.evidence,
7641
7654
  checks: planned.checks,
@@ -7692,6 +7705,8 @@ async function runMemberWake(name, { execute = false, confirmed = false, force =
7692
7705
  ask: planned.ask || null,
7693
7706
  next_command: nextCommand,
7694
7707
  mission: planned.mission,
7708
+ now_file: planned.now_file,
7709
+ goals,
7695
7710
  steering: planned.steering,
7696
7711
  evidence: planned.evidence,
7697
7712
  checks: planned.checks,
@@ -8972,6 +8987,25 @@ async function memberImprovements(businessId, ...args) {
8972
8987
 
8973
8988
  // --- Command Dispatcher ---
8974
8989
 
8990
+ async function memberChat(name, ...rest) {
8991
+ const paths = requireMemberDir(name);
8992
+ const flags = rest.filter((a) => String(a).startsWith('--'));
8993
+ const words = rest.filter((a) => !String(a).startsWith('--'));
8994
+ const fromFlag = flags.find((f) => f.startsWith('--from='));
8995
+ const from = fromFlag ? fromFlag.slice('--from='.length) : (process.env.USER || 'terminal');
8996
+ const text = words.join(' ').trim();
8997
+ if (!text) {
8998
+ console.error('Usage: atris member chat <name> "your message" [--from=<sender>]');
8999
+ process.exit(1);
9000
+ }
9001
+ // Append-only inbox the Atris desktop polls into the member's desk chat.
9002
+ // Contract shared with obelisk src/lib/memberChatInbox.cjs: one JSON object per line.
9003
+ const line = JSON.stringify({ at: new Date().toISOString(), from, text });
9004
+ fs.appendFileSync(path.join(paths.memberDir, 'chat-inbox.jsonl'), `${line}\n`, 'utf8');
9005
+ console.log(`Sent to ${paths.storageName || name}'s desk. It appears in Atris Desktop within about 15 seconds when the app is open.`);
9006
+ return { ok: true };
9007
+ }
9008
+
8975
9009
  async function memberCommand(subcommand, ...args) {
8976
9010
  // Subcommands that take a member name as args[0] otherwise treat `--help` as
8977
9011
  // a name and error with "Member '--help' not found". `create`/`new` handle
@@ -9003,6 +9037,8 @@ async function memberCommand(subcommand, ...args) {
9003
9037
  case 'goal-from-score':
9004
9038
  case 'score-goal':
9005
9039
  return memberGoalFromScore(args[0], ...args.slice(1));
9040
+ case 'chat':
9041
+ return memberChat(args[0], ...args.slice(1));
9006
9042
  case 'tick':
9007
9043
  return memberTick(args[0], ...args.slice(1));
9008
9044
  case 'wake':
@@ -9046,6 +9082,7 @@ async function memberCommand(subcommand, ...args) {
9046
9082
  console.log('');
9047
9083
  console.log('Subcommands:');
9048
9084
  console.log(' create <name> Scaffold a new team member (MEMBER.md + dirs) [--push]');
9085
+ console.log(' chat <name> "..." Send a message to this member\'s desk in Atris Desktop');
9049
9086
  console.log(' list Show all team members');
9050
9087
  console.log(' activate <name> Symlink member skills, show context and permissions');
9051
9088
  console.log(' upgrade <name> Convert flat file (name.md) to directory format');
@@ -23,6 +23,7 @@ const {
23
23
  } = require('../lib/runner-command');
24
24
  const {
25
25
  resolveEngineForRoleWithPreference,
26
+ engineFailureHealthStatus,
26
27
  setEngineHealth,
27
28
  } = require('../lib/engine-registry');
28
29
  const {
@@ -378,28 +379,6 @@ function resolveMissionTickRunner(mission, root = process.cwd()) {
378
379
  };
379
380
  }
380
381
 
381
- function engineFailureHealthStatus(result) {
382
- if (!result || result.status !== 'errored') return null;
383
- const signalText = [
384
- result.reason,
385
- result.model_unavailable,
386
- result.claude && result.claude.summary,
387
- result.claude && result.claude.receipt_text,
388
- result.claude && result.claude.stderr,
389
- result.rate_limit_info && JSON.stringify(result.rate_limit_info),
390
- ].filter(Boolean).join('\n').toLowerCase();
391
- if (/usage[ _-]?limit|purchase more credits|insufficient credits|credit(?:s)?[ _-]?(?:out|limit)|rate[ _-]?limit/.test(signalText)) {
392
- return 'credit_out';
393
- }
394
- if (/timeout|model-unavailable/.test(signalText)) return 'not_installed';
395
- // Any other errored tick (claude-error, no-ready-engine's sibling failures,
396
- // etc.) is still a real failure signal for the engine that ran it. Falling
397
- // through to null here left the registry showing "ready" for an engine
398
- // that had just hard-failed (e.g. a 401), so auto routing kept sending
399
- // ticks back to it. Mark it "error" instead of silently doing nothing.
400
- return 'error';
401
- }
402
-
403
382
  function recordMissionEngineTickOutcome(engineId, result, root = process.cwd()) {
404
383
  if (!engineId) return null;
405
384
  const status = result && result.status === 'ran' ? 'ready' : engineFailureHealthStatus(result);
package/commands/task.js CHANGED
@@ -6028,25 +6028,66 @@ function cmdStatus(args) {
6028
6028
  if (history) console.log(`history feed ${status.swarlo.feed.length} event${status.swarlo.feed.length === 1 ? '' : 's'}`);
6029
6029
  }
6030
6030
 
6031
+ function readProjectionFile(workspaceRoot) {
6032
+ const candidatePaths = [
6033
+ workspaceRoot ? path.join(workspaceRoot, '.atris', 'state', 'tasks.projection.json') : null,
6034
+ path.resolve('.atris', 'state', 'tasks.projection.json'),
6035
+ ].filter(Boolean);
6036
+ for (const candidate of candidatePaths) {
6037
+ try {
6038
+ if (fs.existsSync(candidate)) {
6039
+ const content = fs.readFileSync(candidate, 'utf8');
6040
+ const parsed = JSON.parse(content);
6041
+ if (parsed && Array.isArray(parsed.tasks)) return parsed;
6042
+ }
6043
+ } catch {}
6044
+ }
6045
+ return null;
6046
+ }
6047
+
6031
6048
  function resolveTaskRef(taskDb, db, ref) {
6032
6049
  const token = String(ref || '').trim();
6033
6050
  if (!token) return { ok: false, reason: 'missing' };
6034
- const exact = taskDb.getTask(db, token);
6051
+ const exact = db ? taskDb.getTask(db, token) : null;
6035
6052
  if (exact) return { ok: true, id: exact.id, row: exact };
6036
6053
  const normalized = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(token) : token.replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6037
- const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot: taskDb.workspaceRoot() }));
6038
- const seen = new Set();
6039
- const matches = rows.filter(r => {
6040
- const id = String(r.id || '').toUpperCase();
6041
- const display = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.display_id) : String(r.display_id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6042
- const legacy = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.legacy_ref) : String(r.legacy_ref || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6043
- const matched = id.startsWith(normalized) || display === normalized || legacy === normalized;
6044
- if (!matched || seen.has(r.id)) return false;
6045
- seen.add(r.id);
6046
- return true;
6047
- });
6048
- if (matches.length === 1) return { ok: true, id: matches[0].id, row: matches[0] };
6049
- if (matches.length > 1) return { ok: false, reason: 'ambiguous', matches };
6054
+ const wsRoot = taskDb.workspaceRoot ? taskDb.workspaceRoot() : process.cwd();
6055
+
6056
+ const proj = readProjectionFile(wsRoot);
6057
+ if (proj && Array.isArray(proj.tasks)) {
6058
+ const seen = new Set();
6059
+ const matches = proj.tasks.filter(r => {
6060
+ const id = String(r.id || '').toUpperCase();
6061
+ const display = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.display_id) : String(r.display_id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6062
+ const legacy = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.legacy_ref) : String(r.legacy_ref || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6063
+ const matched = id.startsWith(normalized) || (display && display === normalized) || (legacy && legacy === normalized);
6064
+ if (!matched || seen.has(r.id)) return false;
6065
+ seen.add(r.id);
6066
+ return true;
6067
+ });
6068
+ if (matches.length === 1) {
6069
+ const match = matches[0];
6070
+ const row = db ? (taskDb.getTask(db, match.id) || match) : match;
6071
+ return { ok: true, id: match.id, row };
6072
+ }
6073
+ if (matches.length > 1) return { ok: false, reason: 'ambiguous', matches };
6074
+ }
6075
+
6076
+ if (db) {
6077
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot: wsRoot }));
6078
+ const seen = new Set();
6079
+ const matches = rows.filter(r => {
6080
+ const id = String(r.id || '').toUpperCase();
6081
+ const display = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.display_id) : String(r.display_id || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6082
+ const legacy = taskDb.normalizeTaskRef ? taskDb.normalizeTaskRef(r.legacy_ref) : String(r.legacy_ref || '').replace(/[^a-zA-Z0-9]/g, '').toUpperCase();
6083
+ const matched = id.startsWith(normalized) || (display && display === normalized) || (legacy && legacy === normalized);
6084
+ if (!matched || seen.has(r.id)) return false;
6085
+ seen.add(r.id);
6086
+ return true;
6087
+ });
6088
+ if (matches.length === 1) return { ok: true, id: matches[0].id, row: matches[0] };
6089
+ if (matches.length > 1) return { ok: false, reason: 'ambiguous', matches };
6090
+ }
6050
6091
  return { ok: false, reason: 'not_found' };
6051
6092
  }
6052
6093
 
@@ -6067,7 +6108,7 @@ function workspaceRefRows(taskDb, db, options = {}) {
6067
6108
  }
6068
6109
 
6069
6110
  function renderTaskDesk(rows, refRows = rows) {
6070
- const displayRows = getTaskDb().withTaskDisplayRefs(rows, refRows);
6111
+ const displayRows = (rows && rows.length && rows[0].display_id) ? rows : getTaskDb().withTaskDisplayRefs(rows, refRows);
6071
6112
  const active = displayRows.filter(r => r.status !== 'done' && r.status !== 'archived');
6072
6113
  const done = displayRows.filter(r => r.status === 'done');
6073
6114
  if (rows.length === 0) {
@@ -6078,7 +6119,7 @@ function renderTaskDesk(rows, refRows = rows) {
6078
6119
  console.log('TASK DESK');
6079
6120
  console.log('');
6080
6121
  for (const r of active.slice(0, 12)) {
6081
- const explanation = taskExplanation(r);
6122
+ const explanation = r.explanation || taskExplanation(r);
6082
6123
  const owner = r.claimed_by ? ` @${r.claimed_by}` : '';
6083
6124
  const assigned = !r.claimed_by && taskAssignee(r) ? ` -> ${taskAssignee(r)}` : '';
6084
6125
  const tag = r.tag ? ` #${r.tag}` : '';
@@ -6415,7 +6456,17 @@ function cmdHome(args) {
6415
6456
  workspaceRoot,
6416
6457
  limit: all ? null : 200,
6417
6458
  });
6418
- const { projection, outPath } = writeDefaultProjection(taskDb, db, { all, everywhere });
6459
+ let projection;
6460
+ let outPath;
6461
+ const existingProj = readProjectionFile(workspaceRoot);
6462
+ if (rows.length === 0 && existingProj && Array.isArray(existingProj.tasks) && existingProj.tasks.length > 0) {
6463
+ projection = existingProj;
6464
+ outPath = path.resolve(path.join(workspaceRoot || '.', '.atris', 'state', 'tasks.projection.json'));
6465
+ } else {
6466
+ const written = writeDefaultProjection(taskDb, db, { all, everywhere });
6467
+ projection = written.projection;
6468
+ outPath = written.outPath;
6469
+ }
6419
6470
  if (wantsJson(args)) {
6420
6471
  printJson({
6421
6472
  ok: true,
@@ -6427,7 +6478,7 @@ function cmdHome(args) {
6427
6478
  });
6428
6479
  return;
6429
6480
  }
6430
- renderTaskDesk(rows, rows);
6481
+ renderTaskDesk(projection.tasks);
6431
6482
  }
6432
6483
 
6433
6484
  function cmdList(args) {
@@ -7382,7 +7433,14 @@ function cmdInspect(args) {
7382
7433
  const db = taskDb.open();
7383
7434
  const taskId = requireTaskId(taskDb, db, ref, 'atris task inspect');
7384
7435
  const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
7385
- const task = projection.tasks[0];
7436
+ let task = projection.tasks[0];
7437
+ if (!task) {
7438
+ const wsRoot = scopedWorkspaceRoot(taskDb, args) || process.cwd();
7439
+ const proj = readProjectionFile(wsRoot);
7440
+ if (proj && Array.isArray(proj.tasks)) {
7441
+ task = proj.tasks.find(t => t.id === taskId) || null;
7442
+ }
7443
+ }
7386
7444
  if (!task) {
7387
7445
  failTask('atris task inspect', 'not_found', `task not found: ${ref}`, 1);
7388
7446
  }
@@ -7411,7 +7469,14 @@ function cmdShow(args) {
7411
7469
  const db = taskDb.open();
7412
7470
  const taskId = requireTaskId(taskDb, db, id, 'atris task show');
7413
7471
  const projection = enrichTaskProjection(taskDb.taskProjection(db, { taskId }));
7414
- const task = projection.tasks[0];
7472
+ let task = projection.tasks[0];
7473
+ if (!task) {
7474
+ const wsRoot = scopedWorkspaceRoot(taskDb, args) || process.cwd();
7475
+ const proj = readProjectionFile(wsRoot);
7476
+ if (proj && Array.isArray(proj.tasks)) {
7477
+ task = proj.tasks.find(t => t.id === taskId) || null;
7478
+ }
7479
+ }
7415
7480
  if (!task) {
7416
7481
  console.error(`task not found: ${id}`);
7417
7482
  process.exit(1);
@@ -7550,7 +7615,14 @@ function cmdReviewChat(args) {
7550
7615
 
7551
7616
  function taskDetail(taskDb, db, taskId) {
7552
7617
  const detailedProjection = taskDb.taskProjection(db, { taskId });
7553
- const detailedTask = detailedProjection.tasks[0] || null;
7618
+ let detailedTask = detailedProjection.tasks[0] || null;
7619
+ if (!detailedTask) {
7620
+ const wsRoot = taskDb.workspaceRoot ? taskDb.workspaceRoot() : process.cwd();
7621
+ const proj = readProjectionFile(wsRoot);
7622
+ if (proj && Array.isArray(proj.tasks)) {
7623
+ detailedTask = proj.tasks.find(t => t.id === taskId) || null;
7624
+ }
7625
+ }
7554
7626
  if (!detailedTask) return null;
7555
7627
  const workspaceRoot = detailedTask.workspace_root || taskDb.workspaceRoot();
7556
7628
  const contextProjection = enrichTaskProjection(taskDb.taskProjection(db, {
@@ -7558,7 +7630,7 @@ function taskDetail(taskDb, db, taskId) {
7558
7630
  limit: 5000,
7559
7631
  }));
7560
7632
  const enrichedTask = contextProjection.tasks.find(task => task.id === detailedTask.id) || null;
7561
- if (!enrichedTask) return enrichTaskProjection(detailedProjection).tasks[0] || null;
7633
+ if (!enrichedTask) return enrichTaskProjection(detailedProjection).tasks[0] || detailedTask;
7562
7634
  return {
7563
7635
  ...enrichedTask,
7564
7636
  current_version: detailedTask.current_version,
@@ -0,0 +1,176 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { execFileSync } = require('node:child_process');
6
+
7
+ const { repoRoot } = require('./stream');
8
+ const {
9
+ buildWorkforcePresence,
10
+ isFinishedReceipt,
11
+ parsePsOutput,
12
+ receiptEngine,
13
+ renderWorkforcePresence,
14
+ } = require('../lib/workforce-presence');
15
+
16
+ function readJson(file, fsModule = fs, fallback = null) {
17
+ try {
18
+ return JSON.parse(fsModule.readFileSync(file, 'utf8'));
19
+ } catch {
20
+ return fallback;
21
+ }
22
+ }
23
+
24
+ function readJsonLines(file, fsModule = fs) {
25
+ let text;
26
+ try {
27
+ text = fsModule.readFileSync(file, 'utf8');
28
+ } catch {
29
+ return [];
30
+ }
31
+ return String(text)
32
+ .split(/\r?\n/)
33
+ .map((line) => line.trim())
34
+ .filter(Boolean)
35
+ .map((line) => {
36
+ try { return JSON.parse(line); } catch { return null; }
37
+ })
38
+ .filter(Boolean);
39
+ }
40
+
41
+ function collectTasks(root, fsModule = fs) {
42
+ const payload = readJson(path.join(root, '.atris', 'state', 'tasks.projection.json'), fsModule, {});
43
+ return Array.isArray(payload?.tasks) ? payload.tasks : [];
44
+ }
45
+
46
+ function collectMissions(root, fsModule = fs) {
47
+ const rows = readJsonLines(path.join(root, '.atris', 'state', 'missions.jsonl'), fsModule);
48
+ const latestById = new Map();
49
+ for (const row of rows) {
50
+ const id = String(row?.id || '').trim();
51
+ if (id) latestById.set(id, row);
52
+ }
53
+ return [...latestById.values()];
54
+ }
55
+
56
+ function collectReceipts(root, fsModule = fs) {
57
+ const runsDir = path.join(root, 'atris', 'runs');
58
+ let names;
59
+ try {
60
+ names = fsModule.readdirSync(runsDir).filter((name) => name.endsWith('.json')).sort();
61
+ } catch {
62
+ return [];
63
+ }
64
+ const receipts = [];
65
+ for (const name of names) {
66
+ const receiptPath = path.join(runsDir, name);
67
+ const receipt = readJson(receiptPath, fsModule);
68
+ if (!receipt || !receiptEngine(receipt)) continue;
69
+ let mtimeMs = 0;
70
+ try { mtimeMs = fsModule.statSync(receiptPath).mtimeMs; } catch {}
71
+ receipts.push({ name, path: receiptPath, mtimeMs, receipt });
72
+ }
73
+ return receipts;
74
+ }
75
+
76
+ function collectProcesses(deps = {}) {
77
+ if (Array.isArray(deps.processes)) return deps.processes;
78
+ const execFile = deps.execFile || execFileSync;
79
+ try {
80
+ const output = execFile('ps', ['-eo', 'pid=,ppid=,lstart=,command='], {
81
+ encoding: 'utf8',
82
+ stdio: ['ignore', 'pipe', 'ignore'],
83
+ });
84
+ return parsePsOutput(output);
85
+ } catch {
86
+ return [];
87
+ }
88
+ }
89
+
90
+ function collectWorkforcePresence(deps = {}) {
91
+ const fsModule = deps.fs || fs;
92
+ const root = deps.root || repoRoot(deps.cwd || process.cwd());
93
+ const receipts = Array.isArray(deps.receipts) ? deps.receipts : collectReceipts(root, fsModule);
94
+ return buildWorkforcePresence({
95
+ nowMs: typeof deps.now === 'function' ? deps.now() : Date.now(),
96
+ staleAfterMs: deps.staleAfterMs,
97
+ processes: collectProcesses(deps),
98
+ tasks: Array.isArray(deps.tasks) ? deps.tasks : collectTasks(root, fsModule),
99
+ missions: Array.isArray(deps.missions) ? deps.missions : collectMissions(root, fsModule),
100
+ receipts,
101
+ });
102
+ }
103
+
104
+ function archiveFinishedRuns(root, receipts, fsModule = fs) {
105
+ const archiveDir = path.join(root, 'atris', 'runs', 'archive');
106
+ const finished = receipts.filter((entry) => isFinishedReceipt(entry.receipt));
107
+ const summary = {
108
+ schema: 'atris.workforce_clear.v1',
109
+ archive_dir: path.relative(root, archiveDir),
110
+ archived: 0,
111
+ failed: [],
112
+ };
113
+ if (!finished.length) return summary;
114
+ fsModule.mkdirSync(archiveDir, { recursive: true });
115
+ for (const entry of finished) {
116
+ const destination = path.join(archiveDir, entry.name);
117
+ if (fsModule.existsSync(destination)) {
118
+ summary.failed.push({ receipt: entry.name, reason: 'archive destination already exists' });
119
+ continue;
120
+ }
121
+ try {
122
+ fsModule.renameSync(entry.path, destination);
123
+ summary.archived += 1;
124
+ } catch (error) {
125
+ summary.failed.push({ receipt: entry.name, reason: error.message || String(error) });
126
+ }
127
+ }
128
+ return summary;
129
+ }
130
+
131
+ function helpText() {
132
+ return [
133
+ 'atris who - show local engines and team members from live process and final run state',
134
+ '',
135
+ 'usage: atris who [--json]',
136
+ 'usage: atris who --clear [--json]',
137
+ '',
138
+ '--clear archives finished run receipts under atris/runs/archive/.',
139
+ ].join('\n');
140
+ }
141
+
142
+ function whoCommand(args = [], deps = {}) {
143
+ const write = deps.write || process.stdout.write.bind(process.stdout);
144
+ const error = deps.error || process.stderr.write.bind(process.stderr);
145
+ if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
146
+ write(`${helpText()}\n`);
147
+ return 0;
148
+ }
149
+ if (args.some((arg) => !['--json', '--clear'].includes(arg))) {
150
+ error('usage: atris who [--json] [--clear]\n');
151
+ return 2;
152
+ }
153
+
154
+ const fsModule = deps.fs || fs;
155
+ const root = deps.root || repoRoot(deps.cwd || process.cwd());
156
+ const receipts = Array.isArray(deps.receipts) ? deps.receipts : collectReceipts(root, fsModule);
157
+ if (args.includes('--clear')) {
158
+ const summary = archiveFinishedRuns(root, receipts, fsModule);
159
+ if (args.includes('--json')) write(`${JSON.stringify(summary, null, 2)}\n`);
160
+ else if (summary.failed.length) write(`archived ${summary.archived} finished runs; ${summary.failed.length} could not be archived.\n`);
161
+ else if (summary.archived) write(`archived ${summary.archived} finished run${summary.archived === 1 ? '' : 's'} in ${summary.archive_dir}.\n`);
162
+ else write('no finished runs to clear.\n');
163
+ return summary.failed.length ? 1 : 0;
164
+ }
165
+
166
+ const presence = collectWorkforcePresence({ ...deps, root, receipts });
167
+ const output = args.includes('--json')
168
+ ? JSON.stringify(presence, null, 2)
169
+ : renderWorkforcePresence(presence);
170
+ write(`${output}\n`);
171
+ return 0;
172
+ }
173
+
174
+ module.exports = {
175
+ whoCommand,
176
+ };