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.
Files changed (53) hide show
  1. package/README.md +37 -5
  2. package/atris/features/README.md +8 -0
  3. package/atris/skills/README.md +1 -0
  4. package/atris/skills/commerce/SKILL.md +96 -0
  5. package/atris/skills/engines/SKILL.md +9 -9
  6. package/bin/atris.js +12 -0
  7. package/commands/autoland.js +36 -5
  8. package/commands/autopilot.js +105 -6
  9. package/commands/buy.js +242 -0
  10. package/commands/engine.js +96 -21
  11. package/commands/host.js +56 -4
  12. package/commands/land.js +129 -8
  13. package/commands/member.js +68 -14
  14. package/commands/mission.js +116 -4
  15. package/commands/one-lap.js +14 -10
  16. package/commands/stream.js +56 -22
  17. package/commands/task.js +7 -0
  18. package/commands/team.js +82 -10
  19. package/commands/transactions.js +115 -0
  20. package/commands/worktree.js +39 -0
  21. package/commands/youtube.js +58 -26
  22. package/lib/commerce.js +75 -0
  23. package/lib/engine-ask.js +112 -9
  24. package/lib/engine-registry.js +300 -39
  25. package/lib/fleet.js +444 -26
  26. package/lib/functional-owner.js +15 -6
  27. package/lib/host.js +296 -170
  28. package/lib/known-commands.js +1 -1
  29. package/lib/member-engine.js +25 -1
  30. package/lib/member-park.js +138 -0
  31. package/lib/member-scaffold.js +39 -0
  32. package/lib/member-type.js +27 -0
  33. package/lib/roster-markdown.js +1 -1
  34. package/lib/roster-prep.js +289 -0
  35. package/lib/roster-runs.js +389 -0
  36. package/lib/roster-suggest.js +145 -0
  37. package/lib/router-brain.js +111 -3
  38. package/lib/runner-command.js +37 -2
  39. package/lib/self-drive.js +26 -2
  40. package/lib/state-detection.js +3 -3
  41. package/lib/task-db.js +38 -3
  42. package/lib/team-lineup.js +7 -2
  43. package/lib/todo.js +1 -1
  44. package/lib/wish-audit.js +1 -1
  45. package/package.json +1 -1
  46. package/scripts/det/cpu-hotspots.js +216 -0
  47. package/scripts/det/ytnotes +169 -23
  48. package/scripts/det/ytquote-repair.js +18 -4
  49. package/scripts/det/ytrail-bench.js +214 -0
  50. package/scripts/det/ytrail-eval.js +128 -56
  51. package/templates/members/host/MEMBER.md +5 -3
  52. package/templates/members/host/MISSION.md +1 -1
  53. package/templates/members/host/skills/host/SKILL.md +64 -16
@@ -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.health || selected.health.status !== 'ready') throw new Error(`engine ${selected.id} is not ready`);
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.health && engine.health.status === 'ready')
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.health && engine.health.status === 'ready');
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 ['roster_model', 'roster_effort', 'roster_max_seconds']) {
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, and
217
- // time cap for the builder, and each reviewer's for that reviewer only. A
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.health && entry.health.status === 'ready')
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,
@@ -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 { collectBoard } = require('./land');
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: (root, args) => spawnGit(args, { cwd: root, check: false }),
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
- const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-owner$'], deps);
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
- const result = runGit(root, ['config', '--get-regexp', '^branch\\..*\\.atris-task$'], deps);
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 = collectBoard(root);
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
- function collectSnapshot({ root = process.cwd(), deps = defaultDeps() } = {}) {
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
- try {
672
- const board = collectBoard(root);
673
- landingWait = Number(board?.summary?.due || 0);
674
- } catch {}
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 stream = deps.stream || collectSnapshot({ root, deps: deps.streamDeps });
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
- const presence = deps.presence || collectTeamPresence(deps);
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
- function renderTeamRoster(rosterRows, deps = {}) {
232
- if (!rosterRows.length) {
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(rosterRows, meta = {}) {
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 };