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 +2 -0
- package/atris/skills/youtube/SKILL.md +6 -4
- package/bin/atris.js +8 -1
- package/commands/dream.js +1 -1
- package/commands/engine.js +12 -5
- package/commands/member.js +38 -1
- package/commands/mission.js +1 -22
- package/commands/task.js +94 -22
- package/commands/who.js +176 -0
- package/commands/worktree.js +7 -5
- package/commands/youtube.js +31 -1
- package/lib/engine-ask.js +15 -1
- package/lib/engine-registry.js +23 -0
- package/lib/fleet.js +123 -45
- package/lib/known-commands.js +1 -1
- package/lib/task-db.js +2 -2
- package/lib/workforce-presence.js +448 -0
- package/package.json +2 -1
- package/scripts/det/README.md +162 -0
- package/scripts/det/ax-lane-eval.js +116 -0
- package/scripts/det/changelog.js +148 -0
- package/scripts/det/codex-watchdog.js +217 -0
- package/scripts/det/commit-msg.js +153 -0
- package/scripts/det/data/ax-lane-gold.jsonl +81 -0
- package/scripts/det/data/ax-lane-holdout.jsonl +20 -0
- package/scripts/det/date.js +91 -0
- package/scripts/det/det.js +149 -0
- package/scripts/det/extract.js +93 -0
- package/scripts/det/hash.js +79 -0
- package/scripts/det/hunk-filter.js +73 -0
- package/scripts/det/json.js +120 -0
- package/scripts/det/pr-description.js +213 -0
- package/scripts/det/test.js +296 -0
- package/scripts/det/text.js +102 -0
- package/scripts/det/voice.js +76 -0
- package/scripts/det/ytnotes +196 -0
- package/scripts/det/ytquote-repair.js +181 -0
- package/scripts/det/ytrail-eval.js +124 -0
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: "
|
|
4
|
-
version: 2.
|
|
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
|
|
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
|
|
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 -
|
|
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
|
|
package/commands/engine.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 || !
|
|
1387
|
-
console.error(`engine dispatch: --engine must be one of ${
|
|
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
|
-
|
|
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) => {
|
package/commands/member.js
CHANGED
|
@@ -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');
|
package/commands/mission.js
CHANGED
|
@@ -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
|
|
6038
|
-
|
|
6039
|
-
const
|
|
6040
|
-
|
|
6041
|
-
const
|
|
6042
|
-
const
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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] ||
|
|
7633
|
+
if (!enrichedTask) return enrichTaskProjection(detailedProjection).tasks[0] || detailedTask;
|
|
7562
7634
|
return {
|
|
7563
7635
|
...enrichedTask,
|
|
7564
7636
|
current_version: detailedTask.current_version,
|
package/commands/who.js
ADDED
|
@@ -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
|
+
};
|