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.
- package/AGENTS.md +0 -2
- package/FOR_AGENTS.md +5 -3
- package/atris/skills/engines/SKILL.md +16 -7
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris.md +4 -2
- package/ax +475 -17
- package/bin/atris.js +197 -44
- package/commands/aeo.js +52 -0
- package/commands/autoland.js +313 -19
- package/commands/business.js +91 -8
- package/commands/codex-goal.js +26 -2
- package/commands/drive.js +187 -0
- package/commands/engine.js +73 -2
- package/commands/feed.js +202 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +262 -3
- package/commands/init.js +13 -1
- package/commands/integrations.js +39 -11
- package/commands/interview.js +143 -0
- package/commands/land.js +114 -13
- package/commands/lesson.js +112 -1
- package/commands/linear.js +38 -0
- package/commands/member.js +398 -42
- package/commands/mission.js +554 -64
- package/commands/now.js +25 -1
- package/commands/radar.js +259 -14
- package/commands/serve.js +54 -0
- package/commands/status.js +50 -5
- package/commands/stripe.js +38 -0
- package/commands/supabase.js +39 -0
- package/commands/task.js +935 -71
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/worktree.js +68 -10
- package/commands/write.js +399 -0
- package/lib/auto-accept-certified.js +70 -19
- package/lib/autoland.js +39 -3
- package/lib/fleet.js +250 -9
- package/lib/memory-view.js +14 -5
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/official-cli-integration.js +174 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/review-integrity.js +147 -0
- package/lib/runner-command.js +23 -0
- package/lib/task-db.js +220 -7
- package/lib/task-proof.js +20 -0
- package/lib/task-receipt.js +93 -0
- package/package.json +1 -1
- package/utils/update-check.js +27 -6
- 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
|
-
|
|
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
|
-
|
|
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
|
|
565
|
-
const
|
|
566
|
-
const
|
|
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
|
-
...
|
|
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);
|
package/commands/status.js
CHANGED
|
@@ -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
|
-
//
|
|
136
|
+
// Load task board state.
|
|
92
137
|
const todoFile = path.join(targetDir, 'TODO.md');
|
|
93
|
-
const todo =
|
|
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 };
|