atris 3.61.0 → 3.63.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/README.md +37 -5
- package/atris/features/README.md +8 -0
- package/atris/skills/README.md +1 -0
- package/atris/skills/commerce/SKILL.md +96 -0
- package/atris/skills/engines/SKILL.md +9 -9
- package/bin/atris.js +12 -0
- package/commands/autoland.js +36 -5
- package/commands/autopilot.js +105 -6
- package/commands/buy.js +242 -0
- package/commands/engine.js +96 -21
- package/commands/host.js +56 -4
- package/commands/land.js +129 -8
- package/commands/member.js +68 -14
- package/commands/mission.js +116 -4
- package/commands/one-lap.js +14 -10
- package/commands/stream.js +56 -22
- package/commands/task.js +7 -0
- package/commands/team.js +82 -10
- package/commands/transactions.js +115 -0
- package/commands/worktree.js +39 -0
- package/commands/youtube.js +58 -26
- package/lib/commerce.js +75 -0
- package/lib/engine-ask.js +112 -9
- package/lib/engine-registry.js +300 -39
- package/lib/fleet.js +444 -26
- package/lib/functional-owner.js +15 -6
- package/lib/host.js +296 -170
- package/lib/known-commands.js +1 -1
- package/lib/member-engine.js +25 -1
- package/lib/member-park.js +138 -0
- package/lib/member-scaffold.js +39 -0
- package/lib/member-type.js +27 -0
- package/lib/roster-markdown.js +1 -1
- package/lib/roster-prep.js +289 -0
- package/lib/roster-runs.js +389 -0
- package/lib/roster-suggest.js +145 -0
- package/lib/router-brain.js +111 -3
- package/lib/runner-command.js +37 -2
- package/lib/self-drive.js +26 -2
- package/lib/state-detection.js +3 -3
- package/lib/task-db.js +38 -3
- package/lib/team-lineup.js +7 -2
- package/lib/todo.js +1 -1
- package/lib/wish-audit.js +1 -1
- package/package.json +1 -1
- package/scripts/det/cpu-hotspots.js +216 -0
- package/scripts/det/ytnotes +169 -23
- package/scripts/det/ytquote-repair.js +18 -4
- package/scripts/det/ytrail-bench.js +214 -0
- package/scripts/det/ytrail-eval.js +128 -56
- package/templates/members/host/MEMBER.md +5 -3
- package/templates/members/host/MISSION.md +1 -1
- package/templates/members/host/skills/host/SKILL.md +64 -16
package/commands/one-lap.js
CHANGED
|
@@ -7,11 +7,13 @@ const { spawnSync } = require('child_process');
|
|
|
7
7
|
const { freezeMissionVerifier, listMissions, markMissionReviewReady } = require('./mission');
|
|
8
8
|
const { readWishes } = require('../lib/wish-store');
|
|
9
9
|
const {
|
|
10
|
+
engineReadyAt,
|
|
10
11
|
engineRegistryView,
|
|
11
12
|
resolveEngineForRole,
|
|
12
13
|
resolveEngineForRoleRanked,
|
|
13
14
|
resolveRegisteredEngine,
|
|
14
15
|
rosterPinFor,
|
|
16
|
+
ROSTER_PIN_FIELDS,
|
|
15
17
|
rosterDecided,
|
|
16
18
|
} = require('../lib/engine-registry');
|
|
17
19
|
const { parseVerifyCommand } = require('../lib/auto-accept-certified');
|
|
@@ -156,7 +158,7 @@ function readyExecutor(root, preferred = '') {
|
|
|
156
158
|
if (preferred) {
|
|
157
159
|
const selected = resolveRegisteredEngine(preferred, root);
|
|
158
160
|
if (!selected.roles.includes('executor')) throw new Error(`engine ${selected.id} is not an executor`);
|
|
159
|
-
if (!selected
|
|
161
|
+
if (!engineReadyAt(selected)) throw new Error(`engine ${selected.id} is not ready`);
|
|
160
162
|
if (!fleet.FLEET_CAPABLE.includes(selected.id)) throw new Error(`engine ${selected.id} cannot build headlessly`);
|
|
161
163
|
const pin = rosterPinFor('executor', selected.id, root);
|
|
162
164
|
return Object.keys(pin).length ? { ...selected, ...pin } : selected;
|
|
@@ -165,7 +167,7 @@ function readyExecutor(root, preferred = '') {
|
|
|
165
167
|
if (routed && fleet.FLEET_CAPABLE.includes(routed.id)) return routed;
|
|
166
168
|
return engineRegistryView(root)
|
|
167
169
|
.filter((engine) => engine.roles.includes('executor'))
|
|
168
|
-
.filter((engine) => engine
|
|
170
|
+
.filter((engine) => engineReadyAt(engine))
|
|
169
171
|
.filter((engine) => fleet.FLEET_CAPABLE.includes(engine.id))
|
|
170
172
|
.sort((a, b) => Number(a.fallback_order) - Number(b.fallback_order))[0] || null;
|
|
171
173
|
}
|
|
@@ -178,7 +180,7 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
178
180
|
? rosterRanked.ranked.map((engine) => engine.id)
|
|
179
181
|
: [];
|
|
180
182
|
const pinned = fromRoster && rosterRanked.engine
|
|
181
|
-
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds)
|
|
183
|
+
&& (rosterRanked.engine.roster_model || rosterRanked.engine.roster_effort || rosterRanked.engine.roster_max_seconds || rosterRanked.engine.roster_prep)
|
|
182
184
|
? rosterRanked.engine
|
|
183
185
|
: null;
|
|
184
186
|
// A roster pick can name an engine that reviews only when picked (codex),
|
|
@@ -196,7 +198,7 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
196
198
|
|| String(a.id).localeCompare(String(b.id));
|
|
197
199
|
})
|
|
198
200
|
.map((engine) => (pinned && engine.id === pinned.id ? { ...engine, ...pinFields(pinned) } : engine));
|
|
199
|
-
const ready = candidates.filter((engine) => engine
|
|
201
|
+
const ready = candidates.filter((engine) => engineReadyAt(engine));
|
|
200
202
|
// Route-time determinism: which validator binaries exist on this machine
|
|
201
203
|
// must not change the lap's route. When none are ready, dispatch with the
|
|
202
204
|
// policy-ordered distinct validators; the flight still enforces judge !=
|
|
@@ -207,24 +209,25 @@ function readyValidators(root, preferred = '', exclude = '') {
|
|
|
207
209
|
|
|
208
210
|
function pinFields(engine) {
|
|
209
211
|
const pin = {};
|
|
210
|
-
for (const field of
|
|
212
|
+
for (const field of ROSTER_PIN_FIELDS) {
|
|
211
213
|
if (engine && engine[field]) pin[field] = engine[field];
|
|
212
214
|
}
|
|
213
215
|
return pin;
|
|
214
216
|
}
|
|
215
217
|
|
|
216
|
-
// The roster's pins ride into the flight: the builder's model, effort,
|
|
217
|
-
//
|
|
218
|
-
// reviewer with only a model keeps the plain model string.
|
|
218
|
+
// The roster's pins ride into the flight: the builder's model, effort, time
|
|
219
|
+
// cap, and prep job for the builder, and each reviewer's for that reviewer
|
|
220
|
+
// only. A reviewer with only a model keeps the plain model string.
|
|
219
221
|
function lapModelPins(executor, validators = []) {
|
|
220
222
|
const validatorModels = {};
|
|
221
223
|
for (const engine of validators) {
|
|
222
224
|
if (!engine) continue;
|
|
223
|
-
if (engine.roster_effort || engine.roster_max_seconds) {
|
|
225
|
+
if (engine.roster_effort || engine.roster_max_seconds || engine.roster_prep) {
|
|
224
226
|
validatorModels[engine.id] = {
|
|
225
227
|
model: engine.roster_model || '',
|
|
226
228
|
effort: engine.roster_effort || '',
|
|
227
229
|
max_seconds: engine.roster_max_seconds || 0,
|
|
230
|
+
...(engine.roster_prep ? { prep: engine.roster_prep } : {}),
|
|
228
231
|
};
|
|
229
232
|
} else if (engine.roster_model) {
|
|
230
233
|
validatorModels[engine.id] = engine.roster_model;
|
|
@@ -234,6 +237,7 @@ function lapModelPins(executor, validators = []) {
|
|
|
234
237
|
model: executor && executor.roster_model || '',
|
|
235
238
|
...(executor && executor.roster_effort ? { effort: executor.roster_effort } : {}),
|
|
236
239
|
...(executor && executor.roster_max_seconds ? { maxSeconds: executor.roster_max_seconds } : {}),
|
|
240
|
+
...(executor && executor.roster_prep ? { prep: executor.roster_prep } : {}),
|
|
237
241
|
validatorModels: Object.keys(validatorModels).length ? validatorModels : null,
|
|
238
242
|
};
|
|
239
243
|
}
|
|
@@ -710,7 +714,7 @@ async function runOneLap(ask, options = {}) {
|
|
|
710
714
|
...lapModelPins(executor, validators),
|
|
711
715
|
actor: mission.owner || 'mission-lead',
|
|
712
716
|
installedEngines: engineRegistryView(root)
|
|
713
|
-
.filter((entry) => entry
|
|
717
|
+
.filter((entry) => engineReadyAt(entry))
|
|
714
718
|
.filter((entry) => fleet.FLEET_CAPABLE.includes(entry.id))
|
|
715
719
|
.map((entry) => entry.id),
|
|
716
720
|
ownCli: runCli,
|
package/commands/stream.js
CHANGED
|
@@ -6,7 +6,7 @@ const { hasFlag, readFlag } = require('../lib/arg-parser');
|
|
|
6
6
|
const { isNonInteractive } = require('../lib/noninteractive');
|
|
7
7
|
const escapeRegExp = require('../lib/escape-regexp');
|
|
8
8
|
const { runGit: spawnGit } = require('../lib/git-spawn');
|
|
9
|
-
const {
|
|
9
|
+
const { clearBoardMemo, sharedBoard } = require('./land');
|
|
10
10
|
const { listWorktrees } = require('./worktree');
|
|
11
11
|
|
|
12
12
|
const DEFAULT_INTERVAL_MS = 3000;
|
|
@@ -15,6 +15,11 @@ const MAX_SUMMARY = 180;
|
|
|
15
15
|
const ACTIVE_MISSION_STATES = new Set(['planning', 'active', 'running', 'ready']);
|
|
16
16
|
const ACTIVE_TASK_STATES = new Set(['claimed', 'do', 'doing', 'in_progress', 'review']);
|
|
17
17
|
|
|
18
|
+
// One stable default runner so per-process memos can key on it.
|
|
19
|
+
function defaultRunGit(root, args) {
|
|
20
|
+
return spawnGit(args, { cwd: root, check: false });
|
|
21
|
+
}
|
|
22
|
+
|
|
18
23
|
function defaultDeps() {
|
|
19
24
|
return {
|
|
20
25
|
existsSync: fs.existsSync,
|
|
@@ -22,7 +27,7 @@ function defaultDeps() {
|
|
|
22
27
|
readdirSync: fs.readdirSync,
|
|
23
28
|
statSync: fs.statSync,
|
|
24
29
|
watch: fs.watch,
|
|
25
|
-
runGit:
|
|
30
|
+
runGit: defaultRunGit,
|
|
26
31
|
now: () => Date.now(),
|
|
27
32
|
};
|
|
28
33
|
}
|
|
@@ -540,26 +545,48 @@ function collectWorkspaceRoots(root, deps) {
|
|
|
540
545
|
return [...out.values()];
|
|
541
546
|
}
|
|
542
547
|
|
|
548
|
+
// Owner and task come from one `git config` read per root, shared for a few
|
|
549
|
+
// seconds across the snapshot and the event scan of the same command.
|
|
550
|
+
const WORKTREE_CONFIG_MAX_AGE_MS = 10000;
|
|
551
|
+
const worktreeConfigMemo = new WeakMap();
|
|
552
|
+
|
|
553
|
+
function worktreeConfig(root, deps) {
|
|
554
|
+
const key = typeof deps.runGit === 'function' ? deps.runGit : null;
|
|
555
|
+
let byRoot = key ? worktreeConfigMemo.get(key) : null;
|
|
556
|
+
if (key && !byRoot) {
|
|
557
|
+
byRoot = new Map();
|
|
558
|
+
worktreeConfigMemo.set(key, byRoot);
|
|
559
|
+
}
|
|
560
|
+
const hit = byRoot && byRoot.get(root);
|
|
561
|
+
if (hit && Date.now() - hit.at < WORKTREE_CONFIG_MAX_AGE_MS) return hit;
|
|
562
|
+
const found = { owner: '', task: '', at: Date.now() };
|
|
563
|
+
const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-(owner|task)$'], deps);
|
|
564
|
+
if (result.status === 0) {
|
|
565
|
+
for (const line of String(result.stdout || '').split(/\r?\n/).filter(Boolean)) {
|
|
566
|
+
const [name, ...rest] = line.split(/\s+/);
|
|
567
|
+
const value = rest.join(' ');
|
|
568
|
+
if (!found.owner && /\.atris-owner$/i.test(name)) found.owner = value;
|
|
569
|
+
if (!found.task && /\.atris-task$/i.test(name)) found.task = value;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (byRoot) byRoot.set(root, found);
|
|
573
|
+
return found;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function clearWorktreeConfigMemo(deps) {
|
|
577
|
+
if (deps && typeof deps.runGit === 'function') worktreeConfigMemo.delete(deps.runGit);
|
|
578
|
+
}
|
|
579
|
+
|
|
543
580
|
function worktreeOwner(root, deps) {
|
|
544
581
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
545
582
|
if (sidecar) return sidecar.owner || sidecar.member || sidecar.agent || '';
|
|
546
|
-
|
|
547
|
-
if (result.status === 0) {
|
|
548
|
-
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
549
|
-
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
550
|
-
}
|
|
551
|
-
return '';
|
|
583
|
+
return worktreeConfig(root, deps).owner;
|
|
552
584
|
}
|
|
553
585
|
|
|
554
586
|
function worktreeTask(root, deps) {
|
|
555
587
|
const sidecar = readJson(statePaths(root).sidecar, deps, null);
|
|
556
588
|
if (sidecar && sidecar.task) return sidecar.task;
|
|
557
|
-
|
|
558
|
-
if (result.status === 0) {
|
|
559
|
-
const line = String(result.stdout || '').split(/\r?\n/).find(Boolean);
|
|
560
|
-
if (line) return line.split(/\s+/).slice(1).join(' ');
|
|
561
|
-
}
|
|
562
|
-
return '';
|
|
589
|
+
return worktreeConfig(root, deps).task;
|
|
563
590
|
}
|
|
564
591
|
|
|
565
592
|
function collectWorktreeActivityEvents(root, deps, events) {
|
|
@@ -584,7 +611,7 @@ function collectWorktreeActivityEvents(root, deps, events) {
|
|
|
584
611
|
|
|
585
612
|
function collectLandingStateEvent(root, deps, events, nowMs) {
|
|
586
613
|
try {
|
|
587
|
-
const board =
|
|
614
|
+
const board = sharedBoard(root);
|
|
588
615
|
const summary = board && board.summary || {};
|
|
589
616
|
const active = Number(summary.active || 0);
|
|
590
617
|
const due = Number(summary.due || 0);
|
|
@@ -610,7 +637,7 @@ function collectLandingStateEvent(root, deps, events, nowMs) {
|
|
|
610
637
|
}
|
|
611
638
|
}
|
|
612
639
|
|
|
613
|
-
function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', nowMs = Date.now(), deps = defaultDeps() } = {}) {
|
|
640
|
+
function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', nowMs = Date.now(), deps = defaultDeps(), skipLanding = false } = {}) {
|
|
614
641
|
const allDeps = { ...defaultDeps(), ...deps };
|
|
615
642
|
const events = [];
|
|
616
643
|
const roots = collectWorkspaceRoots(root, allDeps);
|
|
@@ -622,7 +649,7 @@ function collectStreamEvents({ root = process.cwd(), sinceMs = 0, agent = '', no
|
|
|
622
649
|
collectScorecardEvents(item.root, allDeps, events);
|
|
623
650
|
}
|
|
624
651
|
collectWorktreeActivityEvents(root, allDeps, events);
|
|
625
|
-
collectLandingStateEvent(root, allDeps, events, nowMs);
|
|
652
|
+
if (!skipLanding) collectLandingStateEvent(root, allDeps, events, nowMs);
|
|
626
653
|
const wantedAgent = agent ? sanitizeAgent(agent).toLowerCase() : '';
|
|
627
654
|
const byKey = new Map();
|
|
628
655
|
for (const event of events) {
|
|
@@ -648,7 +675,9 @@ function waitingOnOperator(tasks) {
|
|
|
648
675
|
});
|
|
649
676
|
}
|
|
650
677
|
|
|
651
|
-
|
|
678
|
+
// skipLanding: callers that never show the landing wait (the team roster)
|
|
679
|
+
// skip the landing board, the most expensive git work here.
|
|
680
|
+
function collectSnapshot({ root = process.cwd(), deps = defaultDeps(), skipLanding = false } = {}) {
|
|
652
681
|
const allDeps = { ...defaultDeps(), ...deps };
|
|
653
682
|
const active = new Map();
|
|
654
683
|
const waiting = [];
|
|
@@ -668,10 +697,13 @@ function collectSnapshot({ root = process.cwd(), deps = defaultDeps() } = {}) {
|
|
|
668
697
|
if (owner) active.set(sanitizeAgent(owner), clip(worktreeTask(item.root, allDeps) || 'worktree changes', 90));
|
|
669
698
|
}
|
|
670
699
|
let landingWait = 0;
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
700
|
+
if (!skipLanding) {
|
|
701
|
+
try {
|
|
702
|
+
// Same options as the landing-state event so both share one board.
|
|
703
|
+
const board = sharedBoard(root);
|
|
704
|
+
landingWait = Number(board?.summary?.due || 0);
|
|
705
|
+
} catch {}
|
|
706
|
+
}
|
|
675
707
|
const activeRows = [...active.entries()];
|
|
676
708
|
const lines = [];
|
|
677
709
|
lines.push(`Team stream: ${activeRows.length} active agent${activeRows.length === 1 ? '' : 's'}.`);
|
|
@@ -838,6 +870,8 @@ function streamCommand(args = [], deps = defaultDeps()) {
|
|
|
838
870
|
if (closed || polling) return;
|
|
839
871
|
polling = true;
|
|
840
872
|
try {
|
|
873
|
+
clearBoardMemo();
|
|
874
|
+
clearWorktreeConfigMemo(allDeps);
|
|
841
875
|
const fresh = pollStreamOnce(state, { root, sinceMs: opts.sinceMs || (nowMs - 24 * 60 * 60 * 1000), agent: opts.agent, deps: allDeps, nowMs: allDeps.now() });
|
|
842
876
|
if (fresh.length) console.log(renderRecords(fresh, opts));
|
|
843
877
|
} finally {
|
package/commands/task.js
CHANGED
|
@@ -7922,6 +7922,8 @@ function publicWords(value) {
|
|
|
7922
7922
|
]).has(word));
|
|
7923
7923
|
}
|
|
7924
7924
|
|
|
7925
|
+
const { memberTypeOf } = require('../lib/member-type');
|
|
7926
|
+
|
|
7925
7927
|
function parseMemberFrontmatter(text) {
|
|
7926
7928
|
const source = String(text || '');
|
|
7927
7929
|
if (!source.startsWith('---')) return {};
|
|
@@ -7952,6 +7954,7 @@ function readTeamMembers(root = process.cwd()) {
|
|
|
7952
7954
|
slug,
|
|
7953
7955
|
role: cleanPublicText(frontmatter.role || slug.replace(/[-_]/g, ' '), 120),
|
|
7954
7956
|
description: cleanPublicText(frontmatter.description || '', 240),
|
|
7957
|
+
type: memberTypeOf(frontmatter).type,
|
|
7955
7958
|
path: memberPath,
|
|
7956
7959
|
};
|
|
7957
7960
|
})
|
|
@@ -8018,6 +8021,8 @@ function chooseTaskOwner({ purpose, tag, requestedOwner, root = process.cwd() }
|
|
|
8018
8021
|
const words = publicWords(`${purpose || ''} ${tag || ''}`);
|
|
8019
8022
|
let best = null;
|
|
8020
8023
|
for (const member of members) {
|
|
8024
|
+
// A person can be named as an owner, but routing never hands them work as if they were an AI.
|
|
8025
|
+
if (member.type !== 'ai') continue;
|
|
8021
8026
|
const score = scoreTeamMember(member, words, tag);
|
|
8022
8027
|
if (!best || score > best.score) best = { member, score };
|
|
8023
8028
|
}
|
|
@@ -13465,6 +13470,8 @@ async function run(args) {
|
|
|
13465
13470
|
|
|
13466
13471
|
module.exports = {
|
|
13467
13472
|
run,
|
|
13473
|
+
chooseTaskOwner,
|
|
13474
|
+
readTeamMembers,
|
|
13468
13475
|
createTaskApiServer,
|
|
13469
13476
|
taskDayGroups,
|
|
13470
13477
|
taskDayTextGroups,
|
package/commands/team.js
CHANGED
|
@@ -7,6 +7,7 @@ const { canonicalEngineName } = require('../lib/engine-registry');
|
|
|
7
7
|
const taskDb = require('../lib/task-db');
|
|
8
8
|
const { buildTeamPresence, DEFAULT_FRESHNESS_WINDOW_MS, renderTeamPresence } = require('../lib/team-presence');
|
|
9
9
|
const { LINEUP_UNREADABLE, memberLineup, readLineupSafe, renderLineup } = require('../lib/team-lineup');
|
|
10
|
+
const { isMemberParked, isParkedFrontmatter, setMemberParked } = require('../lib/member-park');
|
|
10
11
|
const { readEngineRegistry } = require('./engine');
|
|
11
12
|
const { listMissions, listWorktreeRollupMissions } = require('./mission');
|
|
12
13
|
const { collectSnapshot, collectStreamEvents, repoRoot } = require('./stream');
|
|
@@ -34,12 +35,14 @@ function collectTeamPresence(deps = {}) {
|
|
|
34
35
|
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
35
36
|
const nowMs = typeof deps.now === 'function' ? deps.now() : Date.now();
|
|
36
37
|
const freshnessWindowMs = deps.freshnessWindowMs || DEFAULT_FRESHNESS_WINDOW_MS;
|
|
37
|
-
const
|
|
38
|
+
const skipLanding = Boolean(deps.skipLanding);
|
|
39
|
+
const stream = deps.stream || collectSnapshot({ root, deps: deps.streamDeps, skipLanding });
|
|
38
40
|
const streamEvents = deps.streamEvents || collectStreamEvents({
|
|
39
41
|
root,
|
|
40
42
|
sinceMs: nowMs - freshnessWindowMs,
|
|
41
43
|
nowMs,
|
|
42
44
|
deps: deps.streamDeps,
|
|
45
|
+
skipLanding,
|
|
43
46
|
});
|
|
44
47
|
return buildTeamPresence({
|
|
45
48
|
nowMs,
|
|
@@ -174,7 +177,9 @@ function escapeHtml(text) {
|
|
|
174
177
|
|
|
175
178
|
function collectTeamRoster(deps = {}) {
|
|
176
179
|
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
177
|
-
|
|
180
|
+
// The roster only reads who is awake, never the landing wait, so it skips
|
|
181
|
+
// the landing board entirely.
|
|
182
|
+
const presence = deps.presence || collectTeamPresence({ ...deps, skipLanding: true });
|
|
178
183
|
const awake = new Set(presence.members.map((member) => String(member.name || '').trim().toLowerCase()));
|
|
179
184
|
const engineRoster = deps.engineRoster || readEngineRegistry(root, { persist: false }).engines;
|
|
180
185
|
const engineByOwner = new Map();
|
|
@@ -206,6 +211,7 @@ function collectTeamRoster(deps = {}) {
|
|
|
206
211
|
now: rawNow,
|
|
207
212
|
focus,
|
|
208
213
|
active,
|
|
214
|
+
parked: isParkedFrontmatter(member?.frontmatter),
|
|
209
215
|
};
|
|
210
216
|
})
|
|
211
217
|
.filter((entry) => entry.name)
|
|
@@ -228,10 +234,30 @@ function wrapCommaNames(names, width = 80) {
|
|
|
228
234
|
return lines.join('\n');
|
|
229
235
|
}
|
|
230
236
|
|
|
231
|
-
|
|
232
|
-
|
|
237
|
+
// Parked members leave the lists and come back as one line, unless --all
|
|
238
|
+
// asks for them in place.
|
|
239
|
+
function parkedSummaryLine(parkedRows, width = 80) {
|
|
240
|
+
if (!parkedRows.length) return '';
|
|
241
|
+
const names = parkedRows.map((entry) => entry.name);
|
|
242
|
+
names[0] = `parked (${parkedRows.length}): ${names[0]}`;
|
|
243
|
+
names[names.length - 1] = `${names[names.length - 1]} · atris team --all to show them`;
|
|
244
|
+
return wrapCommaNames(names, width);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function withParkedLabels(rosterRows, all) {
|
|
248
|
+
const parkedRows = rosterRows.filter((entry) => entry.parked);
|
|
249
|
+
if (!all) return { rows: rosterRows.filter((entry) => !entry.parked), parkedRows };
|
|
250
|
+
return {
|
|
251
|
+
rows: rosterRows.map((entry) => (entry.parked ? { ...entry, name: `${entry.name} (parked)` } : entry)),
|
|
252
|
+
parkedRows: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function renderTeamRoster(allRows, deps = {}) {
|
|
257
|
+
if (!allRows.length) {
|
|
233
258
|
return 'no team members yet. create one with: atris member create <name> --role="..."';
|
|
234
259
|
}
|
|
260
|
+
const { rows: rosterRows, parkedRows } = withParkedLabels(allRows, Boolean(deps.all));
|
|
235
261
|
const activeRows = rosterRows.filter((entry) => entry.active);
|
|
236
262
|
const restRows = rosterRows.filter((entry) => !entry.active);
|
|
237
263
|
const termWidth = deps.termWidth || process.stdout.columns || 80;
|
|
@@ -260,6 +286,11 @@ function renderTeamRoster(rosterRows, deps = {}) {
|
|
|
260
286
|
} else {
|
|
261
287
|
lines.push('(none)');
|
|
262
288
|
}
|
|
289
|
+
const parkedLine = parkedSummaryLine(parkedRows, termWidth);
|
|
290
|
+
if (parkedLine) {
|
|
291
|
+
lines.push('');
|
|
292
|
+
lines.push(parkedLine);
|
|
293
|
+
}
|
|
263
294
|
return lines.join('\n');
|
|
264
295
|
}
|
|
265
296
|
|
|
@@ -273,7 +304,8 @@ function renderTeamWithLineup(rosterRows, lineup, deps = {}) {
|
|
|
273
304
|
return block ? `${block}\n\n${today}` : today;
|
|
274
305
|
}
|
|
275
306
|
|
|
276
|
-
function renderTeamRosterHtml(
|
|
307
|
+
function renderTeamRosterHtml(allRows, meta = {}) {
|
|
308
|
+
const rosterRows = withParkedLabels(allRows, Boolean(meta.all)).rows;
|
|
277
309
|
const activeRows = rosterRows.filter((entry) => entry.active);
|
|
278
310
|
const restRows = rosterRows.filter((entry) => !entry.active);
|
|
279
311
|
const generatedAt = meta.generatedAt || new Date().toISOString();
|
|
@@ -418,6 +450,7 @@ function writeTeamBoardHtml(rosterRows, deps = {}) {
|
|
|
418
450
|
const outPath = path.join(workspace, 'atris', 'team', 'team-board.html');
|
|
419
451
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
420
452
|
const html = renderTeamRosterHtml(rosterRows, {
|
|
453
|
+
all: Boolean(deps.all),
|
|
421
454
|
workspace,
|
|
422
455
|
generatedAt: deps.generatedAt || new Date().toISOString(),
|
|
423
456
|
});
|
|
@@ -465,6 +498,8 @@ function collectTeamPrune(deps = {}) {
|
|
|
465
498
|
for (const member of collectMembers(root, deps)) {
|
|
466
499
|
const name = String(member?.name || '').trim().toLowerCase();
|
|
467
500
|
if (!name) continue;
|
|
501
|
+
// A parked member is already set aside on purpose; flagging it again is noise.
|
|
502
|
+
if (isMemberParked(root, name)) continue;
|
|
468
503
|
const signalMs = newestSignalMs(member);
|
|
469
504
|
if (activeOwners.has(name) || (signalMs && nowMs - signalMs < days * DAY_MS)) {
|
|
470
505
|
activeCount += 1;
|
|
@@ -497,14 +532,47 @@ function renderTeamPrune(report, days = DEFAULT_PRUNE_DAYS) {
|
|
|
497
532
|
function helpText() {
|
|
498
533
|
return [
|
|
499
534
|
'atris team - who does each job, with its tool and model, then active members and the rest',
|
|
535
|
+
'atris team --all - also list parked members in place',
|
|
500
536
|
'atris team presence - show who is awake and what they are doing',
|
|
501
537
|
'atris team prune - flag members with no recent activity; deletes nothing',
|
|
538
|
+
'atris team park <name> [--note "<reason>"] - hide a member from the team views; it still runs by name',
|
|
539
|
+
'atris team unpark <name> - bring a parked member back',
|
|
502
540
|
'',
|
|
503
|
-
'usage: atris team [roster|presence] [--json] [--html]',
|
|
541
|
+
'usage: atris team [roster|presence] [--all] [--json] [--html]',
|
|
504
542
|
'usage: atris team prune [--days N] [--json]',
|
|
543
|
+
'usage: atris team park <name> [--note "<reason>"] | atris team unpark <name>',
|
|
505
544
|
].join('\n');
|
|
506
545
|
}
|
|
507
546
|
|
|
547
|
+
const PARK_USAGE = 'usage: atris team park <name> [--note "<reason>"] | atris team unpark <name>';
|
|
548
|
+
|
|
549
|
+
function teamParkCommand(parked, rest, deps = {}) {
|
|
550
|
+
const write = deps.write || process.stdout.write.bind(process.stdout);
|
|
551
|
+
const error = deps.error || process.stderr.write.bind(process.stderr);
|
|
552
|
+
let name = '';
|
|
553
|
+
let note = '';
|
|
554
|
+
let bad = false;
|
|
555
|
+
for (let i = 0; i < rest.length; i += 1) {
|
|
556
|
+
const arg = rest[i];
|
|
557
|
+
if (parked && arg === '--note') { i += 1; note = rest[i] || ''; if (!note) bad = true; continue; }
|
|
558
|
+
if (parked && arg.startsWith('--note=')) { note = arg.slice('--note='.length); continue; }
|
|
559
|
+
if (arg.startsWith('-') || name) { bad = true; continue; }
|
|
560
|
+
name = arg;
|
|
561
|
+
}
|
|
562
|
+
if (bad || !name) {
|
|
563
|
+
error(`${PARK_USAGE}\n`);
|
|
564
|
+
return 2;
|
|
565
|
+
}
|
|
566
|
+
const root = deps.root || repoRoot(deps.cwd || process.cwd());
|
|
567
|
+
const result = setMemberParked(root, name, { parked, note, now: deps.now ? new Date(deps.now()) : new Date() });
|
|
568
|
+
if (!result.ok) {
|
|
569
|
+
error(`${result.error}\n`);
|
|
570
|
+
return 1;
|
|
571
|
+
}
|
|
572
|
+
write(`${result.message}\n`);
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
508
576
|
function teamCommand(args = [], deps = {}) {
|
|
509
577
|
if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
|
|
510
578
|
(deps.write || process.stdout.write.bind(process.stdout))(`${helpText()}\n`);
|
|
@@ -531,18 +599,22 @@ function teamCommand(args = [], deps = {}) {
|
|
|
531
599
|
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
532
600
|
return 0;
|
|
533
601
|
}
|
|
602
|
+
if (args[0] === 'park' || args[0] === 'unpark') {
|
|
603
|
+
return teamParkCommand(args[0] === 'park', args.slice(1), deps);
|
|
604
|
+
}
|
|
534
605
|
const rosterArgs = args.filter((arg) => arg !== 'roster');
|
|
535
|
-
const rosterFlags = new Set(['--json', '--html']);
|
|
606
|
+
const rosterFlags = new Set(['--json', '--html', '--all']);
|
|
536
607
|
if (args[0] !== 'presence' && rosterArgs.every((arg) => rosterFlags.has(arg))) {
|
|
537
608
|
const html = rosterArgs.includes('--html');
|
|
538
609
|
const json = rosterArgs.includes('--json');
|
|
610
|
+
const all = rosterArgs.includes('--all');
|
|
539
611
|
if (html && json) {
|
|
540
612
|
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [--json] [--html] (not both)\n');
|
|
541
613
|
return 2;
|
|
542
614
|
}
|
|
543
615
|
const roster = deps.roster || collectTeamRoster(deps);
|
|
544
616
|
if (html) {
|
|
545
|
-
const outPath = writeTeamBoardHtml(roster, deps);
|
|
617
|
+
const outPath = writeTeamBoardHtml(roster, { ...deps, all });
|
|
546
618
|
(deps.write || process.stdout.write.bind(process.stdout))(`${outPath}\n`);
|
|
547
619
|
return 0;
|
|
548
620
|
}
|
|
@@ -554,12 +626,12 @@ function teamCommand(args = [], deps = {}) {
|
|
|
554
626
|
: readLineupSafe(deps.root || repoRoot(deps.cwd || process.cwd()), deps.lineupNow || new Date());
|
|
555
627
|
const output = json
|
|
556
628
|
? JSON.stringify(roster.map((entry) => ({ ...entry, lineup: memberLineup(lineup, entry.name) })), null, 2)
|
|
557
|
-
: renderTeamWithLineup(roster, lineup, deps);
|
|
629
|
+
: renderTeamWithLineup(roster, lineup, { ...deps, all });
|
|
558
630
|
(deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
|
|
559
631
|
return 0;
|
|
560
632
|
}
|
|
561
633
|
if (args[0] !== 'presence' || args.some((arg, index) => index > 0 && arg !== '--json')) {
|
|
562
|
-
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence|prune] [--json] [--html]\n');
|
|
634
|
+
(deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence|prune|park|unpark] [--all] [--json] [--html]\n');
|
|
563
635
|
return 2;
|
|
564
636
|
}
|
|
565
637
|
const presence = deps.presence || collectTeamPresence(deps);
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// atris transactions: every order, fee, trade, and wallet send, newest first.
|
|
4
|
+
// Read only. Backend: GET /api/transactions.
|
|
5
|
+
|
|
6
|
+
const { apiRequestJson } = require('../utils/api');
|
|
7
|
+
const { loadCredentials } = require('../utils/auth');
|
|
8
|
+
const { NOT_LOGGED_IN, tokenFrom, oneLine, printResult } = require('../lib/developer-api');
|
|
9
|
+
const { parseFlags, formatCents, errorFrom } = require('../lib/commerce');
|
|
10
|
+
|
|
11
|
+
const TX_KINDS = ['ticket', 'shop', 'flight', 'test_purchase', 'fee', 'trade', 'wallet_send'];
|
|
12
|
+
|
|
13
|
+
function showTransactionsHelp(log = console.log) {
|
|
14
|
+
log(`usage: atris transactions [--since YYYY-MM-DD] [--kind k[,k]] [--limit n] [--include-quotes] [--json]
|
|
15
|
+
|
|
16
|
+
list what you bought, paid in fees, traded, or sent, newest first. read only.
|
|
17
|
+
|
|
18
|
+
--since only rows on or after this date, like 2026-09-01
|
|
19
|
+
--kind ${TX_KINDS.join(', ')}
|
|
20
|
+
--limit 1 to 200 (default 50)
|
|
21
|
+
--include-quotes also list quotes nobody approved`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function clip(text, width) {
|
|
25
|
+
const s = String(text == null ? '' : text).replace(/\s+/g, ' ').trim();
|
|
26
|
+
return s.length > width ? `${s.slice(0, width - 1)}…` : s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function whenText(value) {
|
|
30
|
+
const s = String(value || '');
|
|
31
|
+
const m = s.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2})/);
|
|
32
|
+
return m ? `${m[1]} ${m[2]}` : clip(s, 16);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function renderTransactions(data = {}) {
|
|
36
|
+
const rows = Array.isArray(data.transactions) ? data.transactions : [];
|
|
37
|
+
const lines = [];
|
|
38
|
+
if (!rows.length) lines.push('No transactions yet.');
|
|
39
|
+
else {
|
|
40
|
+
const table = rows.map((r) => [
|
|
41
|
+
whenText(r.when),
|
|
42
|
+
clip(r.kind, 12),
|
|
43
|
+
clip(r.what, 40),
|
|
44
|
+
formatCents(r.amount_cents, r.currency),
|
|
45
|
+
clip(r.state, 16),
|
|
46
|
+
]);
|
|
47
|
+
const head = ['WHEN', 'KIND', 'WHAT', 'AMOUNT', 'STATE'];
|
|
48
|
+
const widths = head.map((h, i) => Math.max(h.length, ...table.map((row) => row[i].length)));
|
|
49
|
+
const fmt = (row) => row.map((cell, i) => (i === 3 ? cell.padStart(widths[i]) : cell.padEnd(widths[i])))
|
|
50
|
+
.join(' ').trimEnd();
|
|
51
|
+
lines.push(fmt(head));
|
|
52
|
+
for (const row of table) lines.push(fmt(row));
|
|
53
|
+
}
|
|
54
|
+
const down = Object.entries(data.sources || {}).filter(([, v]) => v !== 'ok').map(([k]) => k);
|
|
55
|
+
if (down.length) lines.push('', `Partial list: could not load ${down.join(', ')}.`);
|
|
56
|
+
return lines.join('\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function buildQuery(flags) {
|
|
60
|
+
const params = new URLSearchParams();
|
|
61
|
+
if (flags.since != null) {
|
|
62
|
+
const since = String(flags.since).trim();
|
|
63
|
+
if (!/^\d{4}-\d{2}-\d{2}/.test(since)) return { ok: false, error: '--since must be a date like 2026-09-01' };
|
|
64
|
+
params.set('since', since);
|
|
65
|
+
}
|
|
66
|
+
const rawKind = flags.kind != null ? flags.kind : flags.kinds;
|
|
67
|
+
if (rawKind != null) {
|
|
68
|
+
const kinds = String(rawKind).split(',').map((k) => k.trim().toLowerCase()).filter(Boolean);
|
|
69
|
+
const bad = kinds.filter((k) => !TX_KINDS.includes(k));
|
|
70
|
+
if (bad.length || !kinds.length) return { ok: false, error: `--kind must be one of ${TX_KINDS.join(', ')}` };
|
|
71
|
+
params.set('kinds', kinds.join(','));
|
|
72
|
+
}
|
|
73
|
+
if (flags.limit != null) {
|
|
74
|
+
const limit = Number(flags.limit);
|
|
75
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) return { ok: false, error: '--limit must be a number from 1 to 200' };
|
|
76
|
+
params.set('limit', String(limit));
|
|
77
|
+
}
|
|
78
|
+
if (flags.include_quotes === true) params.set('include_quotes', 'true');
|
|
79
|
+
const qs = params.toString();
|
|
80
|
+
return { ok: true, path: qs ? `/transactions?${qs}` : '/transactions' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function transactionsCommand(args = [], deps = {}) {
|
|
84
|
+
const io = { log: deps.log || console.log, err: deps.err || console.error };
|
|
85
|
+
const { flags, pos } = parseFlags(args);
|
|
86
|
+
if (flags.help || pos[0] === 'help') {
|
|
87
|
+
showTransactionsHelp(io.log);
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
const json = flags.json === true;
|
|
91
|
+
const fail = (error, status) => {
|
|
92
|
+
printResult({ json, ok: false, error, payload: { ok: false, error, ...(status ? { status } : {}) } }, io);
|
|
93
|
+
return 1;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const query = buildQuery(flags);
|
|
97
|
+
if (!query.ok) return fail(query.error);
|
|
98
|
+
|
|
99
|
+
const load = deps.loadCredentials || loadCredentials;
|
|
100
|
+
const request = deps.apiRequestJson || apiRequestJson;
|
|
101
|
+
const token = tokenFrom(load());
|
|
102
|
+
if (!token) return fail(NOT_LOGGED_IN);
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await request(query.path, { method: 'GET', token });
|
|
106
|
+
if (!result || !result.ok) return fail(errorFrom(result), result && result.status);
|
|
107
|
+
const data = result.data || {};
|
|
108
|
+
printResult({ json, ok: true, text: renderTransactions(data), payload: { ok: true, ...data } }, io);
|
|
109
|
+
return 0;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
return fail(oneLine(error && error.message));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { transactionsCommand };
|