atris 3.34.0 → 3.35.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 (53) hide show
  1. package/AGENTS.md +0 -2
  2. package/FOR_AGENTS.md +5 -3
  3. package/atris/skills/engines/SKILL.md +16 -7
  4. package/atris/skills/render-cli/SKILL.md +88 -0
  5. package/atris.md +4 -2
  6. package/ax +475 -17
  7. package/bin/atris.js +197 -44
  8. package/commands/aeo.js +52 -0
  9. package/commands/autoland.js +313 -19
  10. package/commands/business.js +91 -8
  11. package/commands/codex-goal.js +26 -2
  12. package/commands/drive.js +187 -0
  13. package/commands/engine.js +73 -2
  14. package/commands/feed.js +202 -0
  15. package/commands/github.js +38 -0
  16. package/commands/gm.js +262 -3
  17. package/commands/init.js +13 -1
  18. package/commands/integrations.js +39 -11
  19. package/commands/interview.js +143 -0
  20. package/commands/land.js +114 -13
  21. package/commands/lesson.js +112 -1
  22. package/commands/linear.js +38 -0
  23. package/commands/member.js +398 -42
  24. package/commands/mission.js +554 -64
  25. package/commands/now.js +25 -1
  26. package/commands/radar.js +259 -14
  27. package/commands/serve.js +54 -0
  28. package/commands/status.js +50 -5
  29. package/commands/stripe.js +38 -0
  30. package/commands/supabase.js +39 -0
  31. package/commands/task.js +935 -71
  32. package/commands/truth.js +29 -3
  33. package/commands/unknowns.js +627 -0
  34. package/commands/update.js +44 -0
  35. package/commands/vercel.js +38 -0
  36. package/commands/worktree.js +68 -10
  37. package/commands/write.js +399 -0
  38. package/lib/auto-accept-certified.js +70 -19
  39. package/lib/autoland.js +39 -3
  40. package/lib/fleet.js +250 -9
  41. package/lib/memory-view.js +14 -5
  42. package/lib/mission-runtime-loop.js +7 -0
  43. package/lib/official-cli-integration.js +174 -0
  44. package/lib/outbound-send-gate.js +165 -0
  45. package/lib/permission-grants.js +293 -0
  46. package/lib/review-integrity.js +147 -0
  47. package/lib/runner-command.js +23 -0
  48. package/lib/task-db.js +220 -7
  49. package/lib/task-proof.js +20 -0
  50. package/lib/task-receipt.js +93 -0
  51. package/package.json +1 -1
  52. package/utils/update-check.js +27 -6
  53. package/atris/learnings.jsonl +0 -1
package/commands/now.js CHANGED
@@ -201,6 +201,25 @@ function countTaskReceiptsToday(root = process.cwd(), date = new Date()) {
201
201
  return seen.size;
202
202
  }
203
203
 
204
+ function compactLine(value) {
205
+ return String(value || '').replace(/\s+/g, ' ').trim();
206
+ }
207
+
208
+ function currentMissionMoveLine(root = process.cwd()) {
209
+ try {
210
+ const { selectCodexGoalMission, codexGoalNextCommand } = require('./mission');
211
+ const selected = selectCodexGoalMission(root);
212
+ const mission = selected?.mission || null;
213
+ if (!mission) return null;
214
+ const objective = compactLine(mission.objective);
215
+ const next = compactLine(codexGoalNextCommand(mission));
216
+ if (!objective || !next) return null;
217
+ return `The move: ${objective} — next: ${next}`;
218
+ } catch {
219
+ return null;
220
+ }
221
+ }
222
+
204
223
  function currentJournalPath(root = process.cwd()) {
205
224
  const now = new Date();
206
225
  const year = String(now.getFullYear());
@@ -218,6 +237,10 @@ function renderDefaultNow(root = process.cwd()) {
218
237
  const taskReceiptCount = countTaskReceiptsToday(root);
219
238
  const completedCount = taskReceiptCount || countJournalCompletedReceipts(journalPath);
220
239
  const generated = todayIso();
240
+ const moveLine = currentMissionMoveLine(root);
241
+ const whatMattersNow = moveLine
242
+ ? `${moveLine}\n\n- Decide the next useful move before opening more context.`
243
+ : '- Decide the next useful move before opening more context.';
221
244
 
222
245
  return `# now
223
246
 
@@ -228,7 +251,7 @@ Last updated: ${generated}
228
251
 
229
252
  ## What Matters Now
230
253
 
231
- - Decide the next useful move before opening more context.
254
+ ${whatMattersNow}
232
255
 
233
256
  ## Current Priority
234
257
 
@@ -425,6 +448,7 @@ module.exports = {
425
448
  countOpenWorkItems,
426
449
  countOpenTodoItems,
427
450
  countTaskReceiptsToday,
451
+ currentMissionMoveLine,
428
452
  findChildWorkspaces,
429
453
  isGeneratedNowFile,
430
454
  nowAtris,
package/commands/radar.js CHANGED
@@ -100,6 +100,7 @@ function collectAgents(deps) {
100
100
  return {
101
101
  pid: row.pid,
102
102
  agent: row.agent,
103
+ command: row.command,
103
104
  status: row.stat.includes('Z') ? 'zombie' : row.stat.includes('T') ? 'stopped' : 'active',
104
105
  cwd,
105
106
  repo: repoLabel(cwd),
@@ -148,7 +149,8 @@ function loadTasksCached(root, deps, cache) {
148
149
  return cache.get(root);
149
150
  }
150
151
 
151
- function untaskedReason(agent, taskWorkspaceRoot, tasks) {
152
+ function untaskedReason(agent, taskWorkspaceRoot, tasks, binding = null) {
153
+ if (binding?.interactive) return 'interactive session';
152
154
  if (!agent.cwd) return 'cwd unknown';
153
155
  if (!taskWorkspaceRoot) return 'no task projection';
154
156
  if (!tasks.length) return 'empty task projection';
@@ -159,10 +161,11 @@ function shellQuote(value) {
159
161
  return `'${String(value || '').replace(/'/g, "'\\''")}'`;
160
162
  }
161
163
 
162
- function untaskedAction(agent, taskWorkspaceRoot, tasks) {
163
- const reason = untaskedReason(agent, taskWorkspaceRoot, tasks);
164
+ function untaskedAction(agent, taskWorkspaceRoot, tasks, binding = null) {
165
+ const reason = untaskedReason(agent, taskWorkspaceRoot, tasks, binding);
164
166
  const pid = agent.pid || '?';
165
167
  const actor = agent.agent || 'agent';
168
+ if (reason === 'interactive session') return `inspect pid ${pid} interactive claude session; bind via member lock, worktree sidecar, or branch identity before assuming a repo task`;
166
169
  if (reason === 'cwd unknown') return `inspect pid ${pid} cwd with lsof`;
167
170
  if (reason === 'no active task') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task next --as ${actor}`;
168
171
  if (reason === 'empty task projection') return `cd ${shellQuote(taskWorkspaceRoot)} && atris task new "<small concrete title>" --tag ops`;
@@ -410,6 +413,213 @@ function taskRef(task) {
410
413
  return task ? (task.display_id || task.legacy_ref || task.id || '-') : '-';
411
414
  }
412
415
 
416
+ function isInteractiveClaudeCommand(command) {
417
+ const cmd = String(command || '');
418
+ if (!/(^|\s|\/)claude(\s|$)/.test(cmd) || /Claude\.app/.test(cmd)) return false;
419
+ return !/(^|\s)-p(\s|$)/.test(cmd) && !/(^|\s)--print(\s|$)/.test(cmd);
420
+ }
421
+
422
+ function parseSessionIdFromCommand(command) {
423
+ const parts = String(command || '').split(/\s+/);
424
+ for (let i = 0; i < parts.length; i += 1) {
425
+ if ((parts[i] === '--session-id' || parts[i] === '--resume') && parts[i + 1]) return parts[i + 1];
426
+ const inline = parts[i].match(/^--session-id=(.+)$/);
427
+ if (inline) return inline[1];
428
+ }
429
+ return null;
430
+ }
431
+
432
+ function loadAgentWorktreeSidecar(cwd, deps) {
433
+ if (!cwd) return null;
434
+ let current = path.resolve(cwd);
435
+ for (let depth = 0; depth < 8; depth += 1) {
436
+ const payload = readJsonFile(path.join(current, '.atris', 'agent-worktree.json'), deps, null);
437
+ if (payload) return payload;
438
+ const parent = path.dirname(current);
439
+ if (!parent || parent === current) break;
440
+ current = parent;
441
+ }
442
+ return null;
443
+ }
444
+
445
+ function branchIdentityAtCwd(cwd, execFile) {
446
+ if (!cwd) return null;
447
+ try {
448
+ const branch = String(execFile('git', ['-C', cwd, 'branch', '--show-current'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
449
+ if (!branch) return null;
450
+ const owner = String(execFile('git', ['-C', cwd, 'config', '--get', `branch.${branch}.atris-owner`], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
451
+ const taskHint = String(execFile('git', ['-C', cwd, 'config', '--get', `branch.${branch}.atris-task`], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] })).trim();
452
+ if (!owner) return null;
453
+ return { owner, task_hint: taskHint || null, branch };
454
+ } catch {
455
+ return null;
456
+ }
457
+ }
458
+
459
+ function loadMissionLocks(stateDir, deps) {
460
+ const locks = [];
461
+ for (const name of listNames(stateDir, deps)) {
462
+ const match = name.match(/^mission-(.+)\.lock$/);
463
+ if (!match) continue;
464
+ const payload = readJsonFile(path.join(stateDir, name), deps, null);
465
+ if (!payload || payload.pid == null) continue;
466
+ locks.push({
467
+ ...payload,
468
+ mission_id: payload.mission_id || match[1],
469
+ lock_file: name,
470
+ });
471
+ }
472
+ return locks;
473
+ }
474
+
475
+ function loadMemberLoopLocks(stateDir, deps) {
476
+ const dir = path.join(stateDir, 'member-loops');
477
+ const locks = [];
478
+ for (const name of listNames(dir, deps)) {
479
+ if (!name.endsWith('.lock.json')) continue;
480
+ const payload = readJsonFile(path.join(dir, name), deps, null);
481
+ if (!payload || payload.pid == null) continue;
482
+ locks.push({
483
+ ...payload,
484
+ member: payload.member || name.replace(/\.lock\.json$/, ''),
485
+ });
486
+ }
487
+ return locks;
488
+ }
489
+
490
+ function loadWorkspaceIdentity(root, deps, cache, nowMs) {
491
+ if (!root) return { missions: [], missionLocks: [], memberLocks: [] };
492
+ if (cache.has(root)) return cache.get(root);
493
+ const stateDir = path.join(root, '.atris', 'state');
494
+ const bundle = {
495
+ missions: loadMissions(root, deps, nowMs),
496
+ missionLocks: loadMissionLocks(stateDir, deps),
497
+ memberLocks: loadMemberLoopLocks(stateDir, deps),
498
+ };
499
+ cache.set(root, bundle);
500
+ return bundle;
501
+ }
502
+
503
+ function taskOwnerValues(task) {
504
+ return [
505
+ task?.assigned_to,
506
+ task?.claimed_by,
507
+ task?.metadata?.assigned_to,
508
+ task?.metadata?.claimed_by,
509
+ task?.metadata?.owner,
510
+ ].filter(Boolean).map(value => String(value));
511
+ }
512
+
513
+ function taskMatchesOwner(task, owner) {
514
+ if (!task || !owner) return false;
515
+ const want = String(owner).toLowerCase();
516
+ return taskOwnerValues(task).some(value => value.toLowerCase() === want);
517
+ }
518
+
519
+ function findTaskByRef(tasks, ref) {
520
+ if (!ref) return null;
521
+ return tasks.find(task => taskRef(task) === ref) || null;
522
+ }
523
+
524
+ function taskForOwner(tasks, owner, statuses = ['claimed', 'open', 'review']) {
525
+ if (!owner) return null;
526
+ for (const status of statuses) {
527
+ const matches = tasks.filter(task => task.status === status && taskMatchesOwner(task, owner));
528
+ if (matches.length === 1) return matches[0];
529
+ if (matches.length > 1) return matches[0];
530
+ }
531
+ return null;
532
+ }
533
+
534
+ function resolveAgentTaskBinding(context) {
535
+ const {
536
+ agent,
537
+ tasks,
538
+ missions,
539
+ missionLocks,
540
+ memberLocks,
541
+ sidecar,
542
+ branchIdentity,
543
+ sessionId,
544
+ interactive,
545
+ taskWorkspaceRoot,
546
+ } = context;
547
+ const pid = String(agent?.pid || '');
548
+
549
+ const missionLock = missionLocks.find(row => String(row.pid) === pid);
550
+ if (missionLock) {
551
+ const mission = missions.find(row => row.id === missionLock.mission_id);
552
+ const task = findTaskByRef(tasks, mission?.task_spine?.task_ref) || taskForOwner(tasks, mission?.owner);
553
+ return {
554
+ task,
555
+ owner: mission?.owner || ownerForTask(task),
556
+ member: mission?.owner || sidecar?.member || null,
557
+ session_id: mission?.claude_session_id || sessionId || null,
558
+ mission_id: mission?.id || missionLock.mission_id,
559
+ ...taskBinding('mission_lock', 'process'),
560
+ };
561
+ }
562
+
563
+ const memberLock = memberLocks.find(row => String(row.pid) === pid);
564
+ if (memberLock) {
565
+ const task = taskForOwner(tasks, memberLock.member);
566
+ return {
567
+ task,
568
+ owner: memberLock.member,
569
+ member: memberLock.member,
570
+ session_id: sessionId || null,
571
+ ...taskBinding('member_loop_lock', 'process'),
572
+ };
573
+ }
574
+
575
+ if (sessionId) {
576
+ const mission = missions.find(row => row.claude_session_id === sessionId || row.pending_session_id === sessionId);
577
+ if (mission) {
578
+ const task = findTaskByRef(tasks, mission.task_spine?.task_ref) || taskForOwner(tasks, mission.owner);
579
+ return {
580
+ task,
581
+ owner: mission.owner || ownerForTask(task),
582
+ member: mission.owner || sidecar?.member || null,
583
+ session_id: sessionId,
584
+ mission_id: mission.id,
585
+ ...taskBinding('claude_session', 'process'),
586
+ };
587
+ }
588
+ }
589
+
590
+ const ownerHint = sidecar?.owner || sidecar?.member || branchIdentity?.owner || null;
591
+ if (ownerHint) {
592
+ const task = taskForOwner(tasks, ownerHint);
593
+ return {
594
+ task,
595
+ owner: ownerHint,
596
+ member: sidecar?.member || ownerHint,
597
+ session_id: sessionId || null,
598
+ ...taskBinding(sidecar ? 'agent_worktree' : 'branch_identity', 'process'),
599
+ };
600
+ }
601
+
602
+ if (interactive) {
603
+ return {
604
+ task: null,
605
+ owner: 'interactive',
606
+ member: null,
607
+ session_id: sessionId || null,
608
+ interactive: true,
609
+ ...taskBinding('interactive_session', 'process'),
610
+ };
611
+ }
612
+
613
+ const fallbackTask = taskForCwd(tasks, agent?.cwd, taskWorkspaceRoot, agent);
614
+ return {
615
+ task: fallbackTask,
616
+ owner: ownerForTask(fallbackTask),
617
+ member: null,
618
+ session_id: sessionId || null,
619
+ ...(fallbackTask ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null)),
620
+ };
621
+ }
622
+
413
623
  function taskOwnerMatchesAgent(task, agent) {
414
624
  const actor = String(agent?.agent || '').toLowerCase();
415
625
  if (!actor) return false;
@@ -438,15 +648,22 @@ function taskForCwd(tasks, cwd, workspaceRoot = cwd, agent = null) {
438
648
  || null;
439
649
  }
440
650
 
651
+ function taskBinding(source, scope = null) {
652
+ if (!source) return { task_source: null, task_scope: null };
653
+ return { task_source: source, task_scope: scope };
654
+ }
655
+
441
656
  function taskBindingForProjection(task) {
442
- if (!task) return { task_source: null, task_scope: null };
443
- return {
444
- task_source: 'repo_task_projection',
445
- task_scope: 'repo',
446
- };
657
+ return task ? taskBinding('repo_task_projection', 'repo') : taskBinding(null, null);
447
658
  }
448
659
 
449
660
  function taskSourceLabel(sources = []) {
661
+ if (sources.includes('member_loop_lock')) return 'member loop lock';
662
+ if (sources.includes('mission_lock')) return 'mission lock';
663
+ if (sources.includes('claude_session')) return 'claude session id';
664
+ if (sources.includes('agent_worktree')) return 'agent worktree sidecar';
665
+ if (sources.includes('branch_identity')) return 'branch identity';
666
+ if (sources.includes('interactive_session')) return 'interactive session';
450
667
  if (sources.includes('repo_task_projection')) return 'repo projection; verify ownership';
451
668
  return sources.filter(Boolean).join(', ');
452
669
  }
@@ -556,23 +773,45 @@ function collectRadar(options = {}) {
556
773
  const nowMs = options.nowMs || Date.now();
557
774
  const tasks = loadTasks(root, deps);
558
775
  const taskCache = new Map([[root, tasks]]);
776
+ const identityCache = new Map();
559
777
  const missions = loadMissions(root, deps, nowMs);
778
+ identityCache.set(root, { missions, missionLocks: loadMissionLocks(path.join(root, '.atris', 'state'), deps), memberLocks: loadMemberLoopLocks(path.join(root, '.atris', 'state'), deps) });
560
779
  const worktrees = loadWorktrees(root, deps);
561
780
  const agents = collectAgents(deps).map(agent => {
562
781
  const taskWorkspaceRoot = findTaskWorkspaceRoot(agent.cwd, deps);
563
782
  const agentTasks = taskWorkspaceRoot ? loadTasksCached(taskWorkspaceRoot, deps, taskCache) : [];
564
- const task = taskForCwd(agentTasks, agent.cwd, taskWorkspaceRoot, agent);
565
- const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks);
566
- const taskBinding = taskBindingForProjection(task);
783
+ const identity = taskWorkspaceRoot ? loadWorkspaceIdentity(taskWorkspaceRoot, deps, identityCache, nowMs) : { missions: [], missionLocks: [], memberLocks: [] };
784
+ const sidecar = loadAgentWorktreeSidecar(agent.cwd, deps);
785
+ const branchIdentity = branchIdentityAtCwd(agent.cwd, deps.execFile);
786
+ const sessionId = parseSessionIdFromCommand(agent.command);
787
+ const interactive = agent.agent === 'claude' && isInteractiveClaudeCommand(agent.command);
788
+ const resolved = resolveAgentTaskBinding({
789
+ agent,
790
+ tasks: agentTasks,
791
+ missions: identity.missions,
792
+ missionLocks: identity.missionLocks,
793
+ memberLocks: identity.memberLocks,
794
+ sidecar,
795
+ branchIdentity,
796
+ sessionId,
797
+ interactive,
798
+ taskWorkspaceRoot,
799
+ });
800
+ const task = resolved.task;
801
+ const taskReason = task ? taskSessionReason(task) : untaskedReason(agent, taskWorkspaceRoot, agentTasks, resolved);
802
+ const binding = { task_source: resolved.task_source, task_scope: resolved.task_scope };
567
803
  return {
568
804
  ...agent,
569
805
  task: taskRef(task),
570
806
  task_status: task?.status || null,
571
- owner: ownerForTask(task),
807
+ owner: resolved.owner || ownerForTask(task),
808
+ member: resolved.member || null,
809
+ session_id: resolved.session_id || null,
810
+ mission_id: resolved.mission_id || null,
572
811
  task_workspace: taskWorkspaceRoot ? repoLabel(taskWorkspaceRoot) : null,
573
- ...taskBinding,
812
+ ...binding,
574
813
  task_reason: taskReason,
575
- task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks),
814
+ task_action: task ? taskSessionAction(agent, task, taskWorkspaceRoot) : untaskedAction(agent, taskWorkspaceRoot, agentTasks, resolved),
576
815
  };
577
816
  });
578
817
  const osState = {
@@ -949,10 +1188,16 @@ function radarCommand(args = [], options = {}) {
949
1188
  module.exports = {
950
1189
  agentTypeForCommand,
951
1190
  agentTopPayload,
1191
+ branchIdentityAtCwd,
952
1192
  collectRadar,
1193
+ isInteractiveClaudeCommand,
1194
+ loadAgentWorktreeSidecar,
953
1195
  parsePsOutput,
1196
+ parseSessionIdFromCommand,
954
1197
  parseWorktrees,
955
1198
  radarCommand,
956
1199
  renderAgentTop,
957
1200
  renderRadar,
1201
+ resolveAgentTaskBinding,
1202
+ taskForOwner,
958
1203
  };
package/commands/serve.js CHANGED
@@ -208,8 +208,39 @@ function streamSession(token, sessionId, workingDir) {
208
208
 
209
209
  console.log(`● Bridge active — listening for ops on session ${sessionId.slice(0, 8)}...`);
210
210
 
211
+ // Ping watchdog: the server pings every ~30s. Silence past 3 intervals
212
+ // means the socket died without a FIN (server redeploy, NAT drop) —
213
+ // kill it so the outer loop reconnects/re-registers instead of zombieing.
214
+ let lastSeen = Date.now();
215
+ const watchdog = setInterval(() => {
216
+ if (Date.now() - lastSeen > HEARTBEAT_INTERVAL_MS * 3) {
217
+ clearInterval(watchdog);
218
+ res.destroy(new Error('stream silent >90s, assuming dead socket'));
219
+ }
220
+ }, HEARTBEAT_INTERVAL_MS);
221
+ res.on('close', () => clearInterval(watchdog));
222
+
223
+ // Registry check: a rolling deploy can leave this stream alive on an
224
+ // old instance while new instances answer API calls from an empty
225
+ // in-memory session store — pings keep flowing so the silence watchdog
226
+ // never fires, but dispatchers can't find us. Ask the registry
227
+ // directly; if it forgot this session, reconnect and re-register.
228
+ const registryCheck = setInterval(async () => {
229
+ try {
230
+ const result = await apiRequestJson('/cli/sessions', { method: 'GET', token });
231
+ if (!result.ok) return; // transient API errors don't kill a live stream
232
+ const sessions = (result.data && result.data.sessions) || [];
233
+ if (!sessions.some((s) => s.id === sessionId)) {
234
+ clearInterval(registryCheck);
235
+ res.destroy(new Error('session missing from server registry, re-registering'));
236
+ }
237
+ } catch { /* network blips are the silence watchdog's job */ }
238
+ }, HEARTBEAT_INTERVAL_MS * 4);
239
+ res.on('close', () => clearInterval(registryCheck));
240
+
211
241
  let buffer = '';
212
242
  res.on('data', async (chunk) => {
243
+ lastSeen = Date.now();
213
244
  buffer += chunk.toString();
214
245
  const messages = buffer.split('\n\n');
215
246
  buffer = messages.pop() || '';
@@ -345,6 +376,29 @@ async function serveAtris(options = {}) {
345
376
  reconnectDelay = RECONNECT_DELAY_MS; // reset on clean disconnect
346
377
  } catch (err) {
347
378
  if (shuttingDown) break;
379
+ // Server restarts wipe the in-memory session store — a dead session id
380
+ // 404s forever, so re-register instead of retrying the corpse.
381
+ if (/session not found|404/i.test(err.message || '')) {
382
+ try {
383
+ const result = await apiRequestJson('/cli/sessions', {
384
+ method: 'POST',
385
+ token,
386
+ body: {
387
+ working_directory: workingDir,
388
+ agent_id: agentId,
389
+ allow_bash: allowBash,
390
+ },
391
+ });
392
+ if (result.ok && result.data && result.data.session_id) {
393
+ session = result.data;
394
+ console.log(` ✓ Session expired server-side — re-registered as ${session.session_id}`);
395
+ reconnectDelay = RECONNECT_DELAY_MS;
396
+ continue;
397
+ }
398
+ } catch {
399
+ // fall through to normal backoff
400
+ }
401
+ }
348
402
  console.error(` ⚠ Stream error: ${err.message}, reconnecting in ${reconnectDelay / 1000}s...`);
349
403
  await new Promise((r) => setTimeout(r, reconnectDelay));
350
404
  reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { getLogPath, ensureLogDirectory, createLogFile } = require('../lib/journal');
4
4
  const { parseTodo, getTeamActivity } = require('../lib/todo');
5
+ const { clarify } = require('../lib/autoland');
5
6
 
6
7
  // Box drawing helpers
7
8
  const W = 64; // inner width
@@ -76,6 +77,50 @@ function readEndgameMeta(todoFile) {
76
77
  return { slug, horizon };
77
78
  }
78
79
 
80
+ function todoItemFromTaskRow(row) {
81
+ const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
82
+ return {
83
+ id: row.display_id || row.id,
84
+ title: row.title,
85
+ tag: row.tag || null,
86
+ tags: row.tag ? [row.tag] : [],
87
+ claimed: row.claimed_by || metadata.claimed || null,
88
+ stage: row.status === 'claimed' ? 'in_progress' : (metadata.stage || null),
89
+ verify: typeof metadata.verify === 'string' && metadata.verify.trim()
90
+ ? metadata.verify.trim()
91
+ : null,
92
+ _source: 'db',
93
+ };
94
+ }
95
+
96
+ function parseStatusTodo(todoFile) {
97
+ try {
98
+ const taskDb = require('../lib/task-db');
99
+ const { DatabaseSync } = require('node:sqlite');
100
+ const dbPath = taskDb.getDbPath();
101
+ if (!dbPath || !fs.existsSync(dbPath)) return parseTodo(todoFile);
102
+ const db = new DatabaseSync(dbPath, { readOnly: true });
103
+ try {
104
+ const workspaceRoot = taskDb.workspaceRoot();
105
+ const rows = taskDb.withTaskDisplayRefs(taskDb.listTasks(db, { workspaceRoot, limit: 500 }));
106
+ const todo = { backlog: [], inProgress: [], review: [], completed: [] };
107
+ for (const row of rows) {
108
+ const item = todoItemFromTaskRow(row);
109
+ if (row.status === 'open') todo.backlog.push(item);
110
+ else if (row.status === 'claimed') todo.inProgress.push(item);
111
+ else if (row.status === 'review') todo.review.push(item);
112
+ else if (row.status === 'done') todo.completed.push(item);
113
+ }
114
+ return todo;
115
+ } finally {
116
+ db.close();
117
+ }
118
+ } catch (e) {
119
+ if (process.env.ATRIS_DEBUG) console.error('[status] task db read failed:', e.message);
120
+ return parseTodo(todoFile);
121
+ }
122
+ }
123
+
79
124
  function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
80
125
  const targetDir = path.join(process.cwd(), 'atris');
81
126
 
@@ -88,9 +133,9 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
88
133
  process.exit(1);
89
134
  }
90
135
 
91
- // Parse TODO.md
136
+ // Load task board state.
92
137
  const todoFile = path.join(targetDir, 'TODO.md');
93
- const todo = parseTodo(todoFile);
138
+ const todo = parseStatusTodo(todoFile);
94
139
 
95
140
  // Read journal for inbox and completions
96
141
  const { logFile, dateFormatted } = getLogPath();
@@ -184,7 +229,7 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
184
229
  if (endgame.slug) {
185
230
  where.push(`The active horizon is ${endgame.slug}.`);
186
231
  if (endgame.horizon) {
187
- where.push(...compactWrappedText(endgame.horizon, 74, 2));
232
+ where.push(...compactWrappedText(clarify(endgame.horizon), 74, 2));
188
233
  }
189
234
  } else {
190
235
  where.push('No active endgame is set.');
@@ -198,12 +243,12 @@ function statusAtris(isQuick = false, jsonMode = false, verbose = false) {
198
243
 
199
244
  const queueParts = [];
200
245
  if (todo.inProgress[0]) {
201
- queueParts.push(...compactWrappedText(`In progress: ${todo.inProgress[0].title}.`, 74, 2));
246
+ queueParts.push(...compactWrappedText(`In progress: ${clarify(todo.inProgress[0].title)}.`, 74, 2));
202
247
  } else {
203
248
  queueParts.push('In progress: none.');
204
249
  }
205
250
  if (todo.backlog[0]) {
206
- queueParts.push(...compactWrappedText(`Next backlog item: ${todo.backlog[0].title}.`, 74, 2));
251
+ queueParts.push(...compactWrappedText(`Next backlog item: ${clarify(todo.backlog[0].title)}.`, 74, 2));
207
252
  } else {
208
253
  queueParts.push('Next backlog item: none.');
209
254
  }
@@ -0,0 +1,38 @@
1
+ const { createOfficialCliCommand } = require('../lib/official-cli-integration');
2
+
3
+ const stripeCommand = createOfficialCliCommand({
4
+ name: 'stripe',
5
+ binary: 'stripe',
6
+ versionArgs: ['--version'],
7
+ authArgs: ['config', '--list'],
8
+ installHint: 'https://docs.stripe.com/stripe-cli',
9
+ loginHint: 'stripe login',
10
+ commands: [
11
+ {
12
+ usage: 'listen',
13
+ match: ['listen'],
14
+ forward: ['listen'],
15
+ description: 'listen for webhook events',
16
+ },
17
+ {
18
+ usage: 'trigger',
19
+ match: ['trigger'],
20
+ forward: ['trigger'],
21
+ description: 'trigger test webhook events',
22
+ },
23
+ {
24
+ usage: 'products list',
25
+ match: ['products', 'list'],
26
+ forward: ['products', 'list'],
27
+ description: 'list products',
28
+ },
29
+ {
30
+ usage: 'products create',
31
+ match: ['products', 'create'],
32
+ forward: ['products', 'create'],
33
+ description: 'create a product',
34
+ },
35
+ ],
36
+ });
37
+
38
+ module.exports = { stripeCommand };
@@ -0,0 +1,39 @@
1
+ const { createOfficialCliCommand } = require('../lib/official-cli-integration');
2
+
3
+ const supabaseCommand = createOfficialCliCommand({
4
+ name: 'supabase',
5
+ binary: 'supabase',
6
+ versionArgs: ['--version'],
7
+ authArgs: ['projects', 'list'],
8
+ doctorAliases: ['doctor'],
9
+ installHint: 'https://supabase.com/docs/guides/cli',
10
+ loginHint: 'supabase login',
11
+ commands: [
12
+ {
13
+ usage: 'status',
14
+ match: ['status'],
15
+ forward: ['status'],
16
+ description: 'show local project status',
17
+ },
18
+ {
19
+ usage: 'db push',
20
+ match: ['db', 'push'],
21
+ forward: ['db', 'push'],
22
+ description: 'push local database migrations',
23
+ },
24
+ {
25
+ usage: 'functions list',
26
+ match: ['functions', 'list'],
27
+ forward: ['functions', 'list'],
28
+ description: 'list edge functions',
29
+ },
30
+ {
31
+ usage: 'functions deploy',
32
+ match: ['functions', 'deploy'],
33
+ forward: ['functions', 'deploy'],
34
+ description: 'deploy an edge function',
35
+ },
36
+ ],
37
+ });
38
+
39
+ module.exports = { supabaseCommand };