atris 3.74.4 → 3.74.5

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 (117) hide show
  1. package/FOR_AGENTS.md +12 -12
  2. package/atris/AGENTS.md +7 -7
  3. package/atris/CLAUDE.md +10 -10
  4. package/atris/policies/atris-backend.md +1 -1
  5. package/atris/policies/atris-design.md +71 -71
  6. package/atris/policies/design-seed.md +28 -28
  7. package/atris/policies/writing.md +1 -1
  8. package/atris/skills/apps/SKILL.md +3 -3
  9. package/atris/skills/atris-feedback/SKILL.md +2 -2
  10. package/atris/skills/autopilot/SKILL.md +10 -10
  11. package/atris/skills/blocks/SKILL.md +16 -16
  12. package/atris/skills/calendar/SKILL.md +1 -1
  13. package/atris/skills/clawhub/atris/SKILL.md +4 -4
  14. package/atris/skills/clawhub/philosophy-of-work/SKILL.md +7 -7
  15. package/atris/skills/create-app/SKILL.md +18 -18
  16. package/atris/skills/create-member/SKILL.md +11 -11
  17. package/atris/skills/design/SKILL.md +1 -1
  18. package/atris/skills/drive/SKILL.md +25 -25
  19. package/atris/skills/email-agent/SKILL.md +11 -11
  20. package/atris/skills/endgame/SKILL.md +33 -33
  21. package/atris/skills/magic-inbox/SKILL.md +15 -15
  22. package/atris/skills/notion/SKILL.md +1 -1
  23. package/atris/skills/ramp/SKILL.md +7 -7
  24. package/atris/skills/research-search/SKILL.md +7 -7
  25. package/atris/skills/slides/SKILL.md +7 -7
  26. package/atris/skills/tidy/SKILL.md +11 -11
  27. package/atris/skills/wake/SKILL.md +11 -11
  28. package/atris/skills/writing/SKILL.md +5 -5
  29. package/atris/team/_template/SOUL.md +3 -3
  30. package/atris/team/brainstormer/MEMBER.md +2 -2
  31. package/atris/team/launcher/MEMBER.md +2 -2
  32. package/atris/team/mission-lead/MISSION.md +7 -7
  33. package/atris/team/opus-overnight/MISSION.md +16 -16
  34. package/atris/team/opus-overnight/STEERING.md +5 -5
  35. package/atris.md +9 -0
  36. package/bin/atris.js +16 -3
  37. package/commands/campaign.js +17 -2
  38. package/commands/card.js +19 -12
  39. package/commands/engine.js +92 -17
  40. package/commands/gm.js +30 -0
  41. package/commands/idea.js +263 -0
  42. package/commands/init.js +11 -2
  43. package/commands/land.js +39 -18
  44. package/commands/mission.js +4 -3
  45. package/commands/ninety-nine.js +288 -0
  46. package/commands/one-lap.js +46 -27
  47. package/commands/reel.js +12 -7
  48. package/commands/sync.js +10 -2
  49. package/lib/campaign-keep.js +23 -8
  50. package/lib/campaign-loop.js +24 -5
  51. package/lib/claude-boot-block.js +7 -4
  52. package/lib/engine-registry.js +139 -1
  53. package/lib/fleet.js +11 -2
  54. package/lib/gm-bet.js +338 -34
  55. package/lib/gm-phone-page.js +325 -33
  56. package/lib/gm-phone.js +5 -1
  57. package/lib/gm-season.js +14 -0
  58. package/lib/headless-chrome.js +39 -0
  59. package/lib/idea-gate.js +33 -0
  60. package/lib/idea-inbox.js +220 -0
  61. package/lib/idea-ledger.js +196 -0
  62. package/lib/idea-tick.js +384 -0
  63. package/lib/known-commands.js +1 -1
  64. package/lib/member-engine.js +1 -0
  65. package/lib/ninety-nine-sync.js +156 -0
  66. package/lib/ninety-nine.js +247 -0
  67. package/lib/opening-lines.js +94 -0
  68. package/lib/roster-markdown.js +39 -1
  69. package/lib/roster-models.js +1 -1
  70. package/lib/roster-swap.js +353 -0
  71. package/lib/spawn-env.js +29 -2
  72. package/lib/wish-audit.js +14 -0
  73. package/lib/yes-lock.js +343 -253
  74. package/lib/zen-apply.js +5 -3
  75. package/lib/zen.js +13 -1
  76. package/package.json +1 -1
  77. package/templates/business-starter/CLAUDE.md +7 -7
  78. package/templates/business-starter/MAP.md +1 -1
  79. package/templates/business-starter/TODO.md +1 -1
  80. package/templates/business-starter/atris.md +2 -2
  81. package/templates/business-starter/context/README.md +6 -6
  82. package/templates/business-starter/context/live-workspace.md +1 -1
  83. package/templates/business-starter/goals.md +1 -1
  84. package/templates/business-starter/instructions.md +1 -1
  85. package/templates/business-starter/memory.md +4 -4
  86. package/templates/business-starter/persona.md +3 -3
  87. package/templates/business-starter/policies/LESSONS.md +1 -1
  88. package/templates/business-starter/reports/README.md +1 -1
  89. package/templates/business-starter/skills/README.md +1 -1
  90. package/templates/business-starter/team/README.md +1 -1
  91. package/templates/business-starter/team/START_HERE.md +1 -1
  92. package/templates/business-starter/team/_template/SOUL.md +3 -3
  93. package/templates/business-starter/team/comms/SOUL.md +2 -2
  94. package/templates/business-starter/team/ops/SOUL.md +4 -4
  95. package/templates/business-starter/team/research/SOUL.md +3 -3
  96. package/templates/business-starter/wiki/index.md +1 -1
  97. package/templates/research-canonical/CLAUDE.md +7 -7
  98. package/templates/research-canonical/MAP.md +1 -1
  99. package/templates/research-canonical/TODO.md +1 -1
  100. package/templates/research-canonical/atris.md +2 -2
  101. package/templates/research-canonical/context/README.md +8 -8
  102. package/templates/research-canonical/context/live-workspace.md +1 -1
  103. package/templates/research-canonical/goals.md +2 -2
  104. package/templates/research-canonical/instructions.md +1 -1
  105. package/templates/research-canonical/memory.md +4 -4
  106. package/templates/research-canonical/persona.md +1 -1
  107. package/templates/research-canonical/policies/LESSONS.md +1 -1
  108. package/templates/research-canonical/reports/README.md +1 -1
  109. package/templates/research-canonical/skills/README.md +1 -1
  110. package/templates/research-canonical/team/README.md +1 -1
  111. package/templates/research-canonical/team/eval/SOUL.md +1 -1
  112. package/templates/research-canonical/team/experiment/SOUL.md +2 -2
  113. package/templates/research-canonical/team/hypothesis/SOUL.md +3 -3
  114. package/templates/research-canonical/team/literature/SOUL.md +2 -2
  115. package/templates/research-canonical/wiki/briefs/research-program.md +1 -1
  116. package/templates/research-canonical/wiki/index.md +1 -1
  117. package/utils/auth.js +1 -0
@@ -1,10 +1,10 @@
1
- # Mission — opus-overnight
1
+ # Mission: opus-overnight
2
2
 
3
3
  <!-- Human-authored purpose file. Keep this durable; runtime state belongs in .atris/state/*.jsonl and now.md. -->
4
4
 
5
5
  ## North Star
6
6
 
7
- **AGI is the mission.** Concretely, the path is: the best model that works with Atris first (Atris-shaped, customer-trajectory-driven, vertically RL'd), then we swap the base for a better one when the loop justifies it. The loop, the data, and the published number are the moat — not the base model.
7
+ **AGI is the mission.** Concretely, the path is: the best model that works with Atris first (Atris-shaped, customer-trajectory-driven, vertically RL'd), then we swap the base for a better one when the loop justifies it. The loop, the data, and the published number are the moat, not the base model.
8
8
 
9
9
  The motto: **every tick, name the 1%.** Each tick must answer "what's 1% better than last tick, and on what axis?" If a tick can't name its 1%, log it as a no-lift tick honestly and pick a different artifact.
10
10
 
@@ -16,25 +16,25 @@ The motto: **every tick, name the 1%.** Each tick must answer "what's 1% better
16
16
 
17
17
  ALL of:
18
18
  1. One concrete artifact landed in `rl-exp2/` (script, doc, eval, dataset, comparison) on a tick-prefixed commit.
19
- 2. **The tick summary names its 1% — what got better, by how much, vs what baseline.** Examples: "+1 training mode unlocked (DPO)", "−1 ambiguity in publishable-number rubric", "+5pp clarity on which base model swap is right", "no-lift tick: tried X, didn't move forward, here's what we learned."
19
+ 2. **The tick summary names its 1%: what got better, by how much, vs what baseline.** Examples: "+1 training mode unlocked (DPO)", "−1 ambiguity in publishable-number rubric", "+5pp clarity on which base model swap is right", "no-lift tick: tried X, didn't move forward, here's what we learned."
20
20
  3. `npm test` in atris-cli still green if any atris-cli code touched (usually none).
21
21
  4. A mission-tick receipt recorded via `atris mission tick <id> --summary "Tick N: ... · 1%: <delta>"` so the operator sees the delta in `atris mission status` without reading my chat.
22
- 5. **Zero dollars spent during the tick** — no firectl create/fire/scale, no GPU rentals, no atrisos-backend master commits.
22
+ 5. **Zero dollars spent during the tick**, no firectl create/fire/scale, no GPU rentals, no atrisos-backend master commits.
23
23
 
24
24
  ## Bounded Scope
25
25
 
26
- - Code/docs in `rl-exp2/` — write freely.
27
- - Reads from `atrisos-backend/backend/rl/` and `atris/features/rl-gym/` — write findings into `rl-exp2/notes/`, never modify the backend without explicit approval.
28
- - Supabase reads via the existing trajectory_service shape — never insert/update.
29
- - `firectl list / get / whoami / show` — read-only fine; `create / scale / delete` forbidden.
26
+ - Code/docs in `rl-exp2/`: write freely.
27
+ - Reads from `atrisos-backend/backend/rl/` and `atris/features/rl-gym/`: write findings into `rl-exp2/notes/`, never modify the backend without explicit approval.
28
+ - Supabase reads via the existing trajectory_service shape: never insert/update.
29
+ - `firectl list / get / whoami / show`: read-only fine; `create / scale / delete` forbidden.
30
30
 
31
31
  ## Domains In Order
32
32
 
33
- 1. **Evaluator surface** — heuristic v1 shipped (commit `ae14e32`); LLM-judge v2 next.
34
- 2. **Corpus** — build_sft_corpus.py shipped (commit `bd171fb`); add eval-prompts companion + a dry-run sample dataset for review.
35
- 3. **Base-model swap analysis** — Kimi K2 vs Qwen3.5-30B vs GLM-4.5-Air vs GPT-OSS-20B on Fireworks: cost, throughput, RFT shape support.
36
- 4. **Strategy doc** — smallest publishable number that lets Atris claim "top RL co for vertical workspaces."
37
- 5. **rl-gym artifact survey** — 192 commits, 166 proof artifacts already in atrisos-backend. Map them so future ticks reuse instead of rebuild.
33
+ 1. **Evaluator surface**: heuristic v1 shipped (commit `ae14e32`); LLM-judge v2 next.
34
+ 2. **Corpus**: build_sft_corpus.py shipped (commit `bd171fb`); add eval-prompts companion + a dry-run sample dataset for review.
35
+ 3. **Base-model swap analysis**: Kimi K2 vs Qwen3.5-30B vs GLM-4.5-Air vs GPT-OSS-20B on Fireworks: cost, throughput, RFT shape support.
36
+ 4. **Strategy doc**: smallest publishable number that lets Atris claim "top RL co for vertical workspaces."
37
+ 5. **rl-gym artifact survey**: 192 commits, 166 proof artifacts already in atrisos-backend. Map them so future ticks reuse instead of rebuild.
38
38
 
39
39
  ## Stop Condition
40
40
 
@@ -51,11 +51,11 @@ ALL of:
51
51
 
52
52
  ## Search Discipline (anti-laziness rule)
53
53
 
54
- When the operator says "X exists in our code" or "we have X" and a grep returns 0 hits — **do NOT conclude X doesn't exist.** That conclusion is a recurring failure mode (see `atris/lessons.md` slug `narrow-grep-hides-matches`).
54
+ When the operator says "X exists in our code" or "we have X" and a grep returns 0 hits, **do NOT conclude X doesn't exist.** That conclusion is a recurring failure mode (see `atris/lessons.md` slug `narrow-grep-hides-matches`).
55
55
 
56
56
  The required search ladder:
57
- 1. `grep -rn "<term>" <repo>/` — whole repo, no subdir gating
57
+ 1. `grep -rn "<term>" <repo>/`: whole repo, no subdir gating
58
58
  2. If still nothing, try variants: case-insensitive (`-i`), alternative spellings, related terms
59
- 3. Only after BOTH (1) and (2) miss, surface "I can't find it — point me at where" rather than declaring absence
59
+ 3. Only after BOTH (1) and (2) miss, surface "I can't find it, point me at where" rather than declaring absence
60
60
 
61
61
  Operator's trust is more reliable than my regex. Trust them, then go find it.
@@ -1,4 +1,4 @@
1
- # STEERING — opus-overnight
1
+ # STEERING: opus-overnight
2
2
 
3
3
  <!-- Operator drops one-line directives here while the autonomous loop runs.
4
4
  Each tick reads this file FIRST, applies any unapplied directive as
@@ -8,7 +8,7 @@
8
8
 
9
9
  <!-- Drop one-line directives below. Format: `- YYYY-MM-DD: <directive>`.
10
10
  Examples and usage live in rl-exp2/notes/operator-steering.md.
11
- Do NOT put examples here — every `- ` line under `## Pending` is read
11
+ Do NOT put examples here: every `- ` line under `## Pending` is read
12
12
  as a real directive on the next tick. -->
13
13
 
14
14
  ## Applied
@@ -28,8 +28,8 @@ The autonomous loop runs without the operator present. They can't redirect mid-t
28
28
 
29
29
  This is the lightest layer of the four-tier feedback design:
30
30
  - **Layer 1 (this file):** taste tweaks, one-line drops
31
- - **Layer 2:** `atris brain edit <tick> "..."` — thumbs-up/down on past ticks
32
- - **Layer 3:** `atris mission steer <id> --note "..."` — directives attached to mission state (not yet implemented)
33
- - **Layer 4:** edit MISSION.md / MEMBER.md — durable contract changes
31
+ - **Layer 2:** `atris brain edit <tick> "..."`: thumbs-up/down on past ticks
32
+ - **Layer 3:** `atris mission steer <id> --note "..."`: directives attached to mission state (not yet implemented)
33
+ - **Layer 4:** edit MISSION.md / MEMBER.md: durable contract changes
34
34
 
35
35
  Use the lightest tier that fits. Most steering belongs in Layer 1.
package/atris.md CHANGED
@@ -55,6 +55,15 @@ In a project with nothing on the list yet, speak first: three things you noticed
55
55
 
56
56
  You can move fast. You do not get to move blindly.
57
57
 
58
+ We have done a lot of great work, and because of it we will do even more.
59
+
60
+ - **Acceleration.** Progress compounds. The early days are slow, then the curve bends and everything speeds up. Build on what is already done and settled; never redo it or re-ask it. Each landed piece makes the next one faster.
61
+ - **Accuracy.** A win counts when the result is validated: a test, a gate, or a real run proves it. Small fast tasks can be proven by a check alone; good and big work also gets a second reviewer. Accuracy is the floor the other three stand on, so better tests make everything faster.
62
+ - **Ambition.** Always one more level. When the goal has a clear check, go for the bigger version or the great task. We are capable of it, and we always find ways to win.
63
+ - **Agency.** Take action and own the outcome. Use every tool you have. When something is missing or broken, fix it the same turn instead of handing it back.
64
+
65
+ Acknowledge each validated win, then go for the next one.
66
+
58
67
  Scale ceremony to blast radius. Before a nontrivial change (new surface,
59
68
  multi-file, migrations, anything hard to roll back), state:
60
69
  - the goal
package/bin/atris.js CHANGED
@@ -143,8 +143,8 @@ const helpRequested = updateCommand === 'help'
143
143
  const jsonRequested = process.argv.slice(2).includes('--json');
144
144
  const dryRunRequested = updateArgs.includes('--dry-run');
145
145
  const skipUpdateCheck = Boolean(process.env.ATRIS_SKIP_UPDATE_CHECK || process.env.NO_UPDATE_NOTIFIER || helpRequested || jsonRequested);
146
- // recap promises local only: no registry call and no background install.
147
- if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update', 'mcp', 'recap'].includes(updateCommand)))) {
146
+ // Local-only commands make no registry call and start no background install.
147
+ if (!skipUpdateCheck && (!updateCommand || (updateCommand && !['version', 'update', 'mcp', 'recap', '99'].includes(updateCommand)))) {
148
148
  updateCheckPromise = checkForUpdates()
149
149
  .then((updateInfo) => {
150
150
  if (updateInfo) {
@@ -528,6 +528,7 @@ function showHelpShort() {
528
528
  console.log('');
529
529
  console.log(' atris "what do you want here?" empty folder');
530
530
  console.log(' atris later "..." remember without a room');
531
+ console.log(' atris idea "..." drop an idea; atris builds it later');
531
532
  console.log(' atris guide "..." translate your words');
532
533
  console.log(' atris do work already here');
533
534
  console.log(' atris done land what your agent built');
@@ -563,6 +564,7 @@ function showHelpAll() {
563
564
  console.log('you say what you want. already won. one next step.');
564
565
  console.log('');
565
566
  console.log(' atris later "..." remember without a room');
567
+ console.log(' atris idea "..." drop an idea; atris builds it later');
566
568
  console.log(' atris guide "..." translate your words');
567
569
  console.log(' atris do work already here');
568
570
  console.log(' atris done land what your agent built');
@@ -600,6 +602,7 @@ function showHelpAll() {
600
602
  console.log(' log - Add ideas to inbox (`atris log "note"`; `log --repl` for the editor; see also `wish`)');
601
603
  console.log(' wish - Say one plain sentence, then Atris asks only for gaps or delegates it');
602
604
  console.log(' now - Show atris/now.md, the current operating truth');
605
+ console.log(' 99 - focus, meditation, health, and optional sync');
603
606
  console.log(' goal - goal, distance, and what moved today (alias: wtf)');
604
607
  console.log(' founder - show weekly commit and completed-task improvement from receipts');
605
608
  console.log(' orb - Pick next moves while engine jobs work in the background');
@@ -608,7 +611,7 @@ function showHelpAll() {
608
611
  console.log(' who - Show local engines and team members working, waiting, done, or stale');
609
612
  console.log(' stream - Watch the whole team work live in one terminal');
610
613
  console.log(' team - One team view: members, roles, engine assignments (presence for live)');
611
- console.log(' gm - Play your team as a front office: ratings, stuck jobs, league, replay, phone');
614
+ console.log(' gm - Play your team as a front office: ratings, stuck jobs, league, replay, phone, walk');
612
615
  console.log(' host - The Host: gets to know people, makes two-yes introductions (atris member install host)');
613
616
  console.log(' watch - Turn one sentence into an always-on background watcher');
614
617
  console.log(' ctop - Show a process-first live agent CPU/memory view');
@@ -2288,6 +2291,9 @@ if (command === 'guide') {
2288
2291
  console.error(error.message || String(error));
2289
2292
  process.exit(1);
2290
2293
  }
2294
+ } else if (command === '99') {
2295
+ Promise.resolve(require('../commands/ninety-nine').ninetyNineAtris(process.argv.slice(3)))
2296
+ .then((code) => exitAfterStdoutDrain(code));
2291
2297
  } else if (command === 'now') {
2292
2298
  const args = process.argv.slice(3);
2293
2299
  const nowCode = require('../commands/now').nowAtris(args);
@@ -2298,6 +2304,13 @@ if (command === 'guide') {
2298
2304
  } else if (command === 'later') {
2299
2305
  const laterCode = require('../commands/later').laterAtris(process.argv.slice(3));
2300
2306
  process.exit(Number.isInteger(laterCode) ? laterCode : 0);
2307
+ } else if (command === 'idea') {
2308
+ Promise.resolve(require('../commands/idea').ideaAtris(process.argv.slice(3))).then((ideaCode) => {
2309
+ process.exit(Number.isInteger(ideaCode) ? ideaCode : 0);
2310
+ }, (error) => {
2311
+ console.error(`atris idea: ${error && error.message || error}`);
2312
+ process.exit(1);
2313
+ });
2301
2314
  } else if (command === 'done') {
2302
2315
  const doneCode = require('../commands/done').doneAtris(process.argv.slice(3));
2303
2316
  process.exit(Number.isInteger(doneCode) ? doneCode : 0);
@@ -33,7 +33,7 @@ function printHelp() {
33
33
  console.log(' atris campaign start "<title>" --judge "<cmd>" [--budget <dollars>] no --budget means no limit; --cloud needs one');
34
34
  console.log(' [--holdout <file>] move held-out data out of the repo; the judge reads it from $ATRIS_CAMPAIGN_HOLDOUT');
35
35
  console.log(' [--owner <member>] [--seat <member>[:<role>]]... [--bet "<bet>" [--wrong-if "<what proves it wrong>"]]');
36
- console.log(' [--standing] [--parent <id>] [--id cmp-<name>] [--cloud [--basis atris_cloud]] [--protect <path>]... [--run]');
36
+ console.log(' [--standing] [--parent <id>] [--id cmp-<name>] [--cloud [--basis atris_cloud]] [--protect <path>]... [--brief <file>] [--run]');
37
37
  console.log(' --protect saves paths agents must not touch, and the commit they start from');
38
38
  console.log(' --basis atris_cloud: Atris runs it on your Atris computer and bills its engines; the default is your own engines');
39
39
  console.log(' atris campaign show <id> bet, judge, answer, budget, seats, missions, asks');
@@ -89,7 +89,7 @@ const NO_LIMIT_BUDGET = 1e9;
89
89
  const noLimit = (view) => view.budget.total >= NO_LIMIT_BUDGET;
90
90
 
91
91
  // The flags this command understands, so positional words are what is left.
92
- const VALUE_FLAGS = ['--mission-minutes', '--basis', '--work', '--break', '--rounds', '--mission', '--parallel', '--max-steps', '--max-ticks', '--runner', '--count', '--judge', '--budget', '--owner', '--seat', '--bet', '--wrong-if', '--parent', '--id', '--role', '--add', '--data', '--timeout', '--as', '--holdout', '--protect', '--every', '--steps'];
92
+ const VALUE_FLAGS = ['--mission-minutes', '--basis', '--work', '--break', '--rounds', '--mission', '--parallel', '--max-steps', '--max-ticks', '--runner', '--count', '--judge', '--budget', '--owner', '--seat', '--bet', '--wrong-if', '--parent', '--id', '--role', '--add', '--data', '--timeout', '--as', '--holdout', '--protect', '--brief', '--every', '--steps'];
93
93
 
94
94
  function positionals(args) {
95
95
  const out = [];
@@ -389,6 +389,14 @@ async function cmdStart(args, ctx) {
389
389
  const protectPlan = protect.length ? planProtect(process.cwd(), protect) : null;
390
390
  if (protectPlan && protectPlan.error) return ctx.fail(protectPlan.error);
391
391
 
392
+ const briefFlag = readFlag(args, '--brief', '');
393
+ let briefText = null;
394
+ if (briefFlag) {
395
+ const from = path.resolve(briefFlag);
396
+ if (!fs.existsSync(from) || !fs.statSync(from).isFile()) return ctx.fail(`--brief ${briefFlag} is not a file`);
397
+ briefText = fs.readFileSync(from, 'utf8');
398
+ }
399
+
392
400
  const holdoutFlag = readFlag(args, '--holdout', '');
393
401
  let held = null;
394
402
  if (holdoutFlag) {
@@ -425,6 +433,12 @@ async function cmdStart(args, ctx) {
425
433
  if (parent) ctx.append(id, 'attached', { parent_id: parent });
426
434
  ctx.append(id, 'judge_ran', judgeRanData(reading), 'judge');
427
435
  if (protectPlan) saveProtect(ctx.campaignHome, id, protectPlan);
436
+ if (briefText !== null) {
437
+ const { briefPath } = require('../lib/campaign-loop');
438
+ const target = briefPath(process.cwd(), id);
439
+ fs.mkdirSync(path.dirname(target), { recursive: true });
440
+ fs.writeFileSync(target, briefText);
441
+ }
428
442
 
429
443
  let cloud = null;
430
444
  if (args.includes('--cloud')) {
@@ -800,6 +814,7 @@ async function driveLoop(args, ctx, verb, view) {
800
814
  log: ctx.asJson ? () => {} : (line) => console.log(line),
801
815
  judge: (command) => ctx.judge(command, JUDGE_TIMEOUT_SECONDS, view.id),
802
816
  lessons: (id) => require('../lib/campaign-cadence').readLessons(process.cwd(), id),
817
+ brief: (id) => require('../lib/campaign-loop').readBrief(process.cwd(), id),
803
818
  ...(sync ? { sync } : {}),
804
819
  ...(ctx.loopDeps || {}),
805
820
  });
package/commands/card.js CHANGED
@@ -11,6 +11,7 @@ const path = require('path');
11
11
  const os = require('os');
12
12
  const { execFileSync, spawnSync } = require('child_process');
13
13
  const { buildCard, SIZES, KINDS } = require('../lib/card');
14
+ const { makeProfileDir, removeProfileDir, headlessArgs } = require('../lib/headless-chrome');
14
15
 
15
16
  function parseFlags(argv) {
16
17
  const flags = {}; const pos = [];
@@ -46,18 +47,24 @@ function findChrome() {
46
47
 
47
48
  function renderPng(chrome, html, width, height, outPng) {
48
49
  const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-card-'));
49
- const htmlFile = path.join(tmp, 'card.html');
50
- fs.writeFileSync(htmlFile, html);
51
- const args = [
52
- '--headless=new', '--disable-gpu', '--hide-scrollbars',
53
- '--force-device-scale-factor=2',
54
- `--window-size=${width},${height}`,
55
- '--virtual-time-budget=4000',
56
- `--screenshot=${outPng}`,
57
- `file://${htmlFile}`,
58
- ];
59
- execFileSync(chrome, args, { stdio: 'ignore', timeout: 60000 });
60
- return fs.existsSync(outPng);
50
+ const profile = makeProfileDir('atris-card-chrome-');
51
+ try {
52
+ const htmlFile = path.join(tmp, 'card.html');
53
+ fs.writeFileSync(htmlFile, html);
54
+ const args = headlessArgs(profile, [
55
+ '--disable-gpu', '--hide-scrollbars',
56
+ '--force-device-scale-factor=2',
57
+ `--window-size=${width},${height}`,
58
+ '--virtual-time-budget=4000',
59
+ `--screenshot=${outPng}`,
60
+ `file://${htmlFile}`,
61
+ ]);
62
+ execFileSync(chrome, args, { stdio: 'ignore', timeout: 60000 });
63
+ return fs.existsSync(outPng);
64
+ } finally {
65
+ removeProfileDir(profile);
66
+ removeProfileDir(tmp);
67
+ }
61
68
  }
62
69
 
63
70
  function run(argv) {
@@ -11,6 +11,8 @@
11
11
  // atris engine roster confirm renew every dated worker for 30 days
12
12
  // atris engine roster session [clear] this shell's own changes
13
13
  // atris engine roster --available tools and models on this machine
14
+ // atris engine roster set <job> <tool> <model> swap a job's lead; the old lead backs it up
15
+ // atris engine roster set --member <name> <tool> <model> give one member its own model
14
16
  // atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]
15
17
  // atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]
16
18
  // jobs are open-ended: search, build, review, or your own ("small build")
@@ -35,11 +37,12 @@ const {
35
37
  const { parseScopeFlag } = require('../lib/cli-scope');
36
38
  const { isFreshWorkspace, speakFirstMinute } = require('../lib/first-minute');
37
39
  const { teamRosterView } = require('../lib/member-engine');
38
- const { engineModelText, engineRunsView, availableModels, modelLabel } = require('../lib/roster-models');
40
+ const { engineModelText, engineRunsView, modelLabel } = require('../lib/roster-models');
39
41
  const { renderTable } = require('../lib/text-table');
40
42
  const { TEAM_FOOTER } = require('../lib/team-lineup');
41
43
  const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, runView, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
42
44
  const { attachSuggestions } = require('../lib/roster-suggest');
45
+ const { swapList, swapJob, swapMember } = require('../lib/roster-swap');
43
46
  const { PIN_MAX_AGE_DAYS: PIN_STALE_DAYS, attachStaleModels, modelCatalog } = require('../lib/roster-stale');
44
47
  const {
45
48
  ENGINE_ROLES,
@@ -48,7 +51,7 @@ const {
48
51
  ROSTER_LAYERS,
49
52
  rosterDecided,
50
53
  rosterWorkerWalk,
51
- rosterToolLabel,
54
+ rosterToolName,
52
55
  rosterSessionKey,
53
56
  clearSessionRoster,
54
57
  NO_SESSION_MESSAGE,
@@ -1399,18 +1402,17 @@ function printJobRoster(root, now = new Date()) {
1399
1402
  console.log(renderRosterReport(rosterReport(root, now)));
1400
1403
  }
1401
1404
 
1402
- // Each installed tool and the models it offers, from local files only.
1403
- function availableReport(root) {
1404
- const engines = readEngineRegistry(root, { persist: false }).engines
1405
- .filter((engine) => !HIDDEN_PROFILE_NAMES.includes(engine.id))
1406
- .filter((engine) => engine.installed || (engine.health && engine.health.status === 'ready'));
1407
- return availableModels(engines).map((row) => ({ ...row, tool: rosterToolLabel(row.engine) }));
1408
- }
1409
-
1410
- function renderAvailable(rows) {
1405
+ // With the owner's picks: the swap list on top (atris engines, then the
1406
+ // picks), then every model under "more models". Without: every tool's list.
1407
+ function renderAvailable(rows, list = null) {
1411
1408
  if (!rows.length) return 'no tools found on this machine. run atris engine doctor to check again';
1412
- const width = Math.max(...rows.map((row) => row.tool.length));
1413
- return ['on this machine', ...rows.map((row) => `${row.tool.padEnd(width)} ${row.models.join(', ') || 'its own default'} (${row.source})`)].join('\n');
1409
+ const top = list && list.picked ? list.options.filter((option) => !option.more) : [];
1410
+ const width = Math.max(...rows.map((row) => row.tool.length), ...top.map((option) => option.tool.length));
1411
+ const full = rows.map((row) => `${row.tool.padEnd(width)} ${row.models.join(', ') || 'its own default'} (${row.source})`);
1412
+ if (!top.length) return ['on this machine', ...full].join('\n');
1413
+ const labels = Math.max(...top.map((option) => option.label.length));
1414
+ const picks = top.map((option) => `${option.tool.padEnd(width)} ${option.label.padEnd(labels)} ${option.atris ? 'atris' : 'pick'}`);
1415
+ return ['on this machine', ...picks, '', 'more models', ...full].join('\n');
1414
1416
  }
1415
1417
 
1416
1418
  // Only this session's changes, from its own file.
@@ -1518,7 +1520,78 @@ function runRosterRunsCommand(job, json, root, now) {
1518
1520
  return 0;
1519
1521
  }
1520
1522
 
1523
+ const ROSTER_SET_USAGE = 'usage: atris engine roster set <job> <tool> <model>, or atris engine roster set --member <name> <tool> <model>. the tools and models come from atris engine roster --available';
1524
+
1525
+ // The words after set: the job (one quoted argument, or every word before
1526
+ // the first one that names a tool), the tool (one word, or two like "claude
1527
+ // code"), then the model, which may be several words.
1528
+ function splitSetWords(words, needJob) {
1529
+ // "claude code" and "atris fast" are two words; every other tool is one.
1530
+ const toolAt = (i) => {
1531
+ const two = i + 1 < words.length ? `${words[i]} ${words[i + 1]}` : '';
1532
+ if (two && rosterToolNameOf(two)) return { at: i, size: 2 };
1533
+ return rosterToolNameOf(words[i]) ? { at: i, size: 1 } : null;
1534
+ };
1535
+ let start = 0;
1536
+ let job = '';
1537
+ if (needJob) {
1538
+ if (!words.length) return null;
1539
+ if (/\s/.test(String(words[0]).trim())) {
1540
+ job = String(words[0]).trim();
1541
+ start = 1;
1542
+ } else {
1543
+ let found = null;
1544
+ for (let i = 1; i < words.length && !found; i += 1) found = toolAt(i);
1545
+ if (!found) return null;
1546
+ job = words.slice(0, found.at).join(' ');
1547
+ start = found.at;
1548
+ }
1549
+ }
1550
+ const tool = toolAt(start);
1551
+ if (!tool) return words.length > start ? { job, tool: words[start], model: words.slice(start + 1).join(' ') } : null;
1552
+ return { job, tool: words.slice(start, start + tool.size).join(' '), model: words.slice(start + tool.size).join(' ') };
1553
+ }
1554
+
1555
+ function rosterToolNameOf(words) {
1556
+ return rosterToolName(String(words || '').trim());
1557
+ }
1558
+
1559
+ function runRosterSetCommand(args, root) {
1560
+ const json = args.includes('--json');
1561
+ const words = args.filter((arg) => arg !== '--json');
1562
+ let member = '';
1563
+ const at = words.indexOf('--member');
1564
+ if (at !== -1) {
1565
+ member = words[at + 1] || '';
1566
+ words.splice(at, 2);
1567
+ if (!member || member.startsWith('--')) {
1568
+ console.error(ROSTER_SET_USAGE);
1569
+ return 2;
1570
+ }
1571
+ }
1572
+ if (words.some((word) => String(word).startsWith('--'))) {
1573
+ console.error(ROSTER_SET_USAGE);
1574
+ return 2;
1575
+ }
1576
+ const parts = splitSetWords(words, !member);
1577
+ if (!parts || (!member && !parts.job)) {
1578
+ console.error(ROSTER_SET_USAGE);
1579
+ return 2;
1580
+ }
1581
+ try {
1582
+ const out = member
1583
+ ? swapMember(root, member, parts.tool, parts.model)
1584
+ : swapJob(root, parts.job, parts.tool, parts.model);
1585
+ console.log(json ? JSON.stringify(out, null, 2) : out.line);
1586
+ return 0;
1587
+ } catch (err) {
1588
+ console.error(err.message);
1589
+ return 2;
1590
+ }
1591
+ }
1592
+
1521
1593
  function runRosterCommand(args, root, now = new Date()) {
1594
+ if (args[0] === 'set') return runRosterSetCommand(args.slice(1), root);
1522
1595
  const json = args.includes('--json');
1523
1596
  const available = args.includes('--available');
1524
1597
  const runsAt = args.indexOf('--runs');
@@ -1536,12 +1609,14 @@ function runRosterCommand(args, root, now = new Date()) {
1536
1609
  || (rest.length === 1 && ['confirm', 'session'].includes(rest[0]))
1537
1610
  || (rest.length === 2 && rest[0] === 'session' && rest[1] === 'clear');
1538
1611
  if (!known || (available && rest.length)) {
1539
- console.error('usage: atris engine roster [confirm | session [clear] | --available | --runs [job]] [--json]');
1612
+ console.error('usage: atris engine roster [confirm | session [clear] | set <job> <tool> <model> | set --member <name> <tool> <model> | --available | --runs [job]] [--json]');
1540
1613
  return 2;
1541
1614
  }
1542
1615
  if (available) {
1543
- const rows = availableReport(root);
1544
- console.log(json ? JSON.stringify({ tools: rows }, null, 2) : renderAvailable(rows));
1616
+ // the same ordered list the phone's engine card reads
1617
+ const list = swapList(root);
1618
+ const rows = list.tools;
1619
+ console.log(json ? JSON.stringify({ tools: rows, list: list.options }, null, 2) : renderAvailable(rows, list));
1545
1620
  return 0;
1546
1621
  }
1547
1622
  if (rest[0] === 'session') return runRosterSessionCommand(rest, json, root, now);
@@ -2548,7 +2623,7 @@ function engineCommand(args = [], deps = {}) {
2548
2623
  if (sub === 'help' || args.includes('--help') || args.includes('-h')) {
2549
2624
  console.log('\n atris engine watch [<id>|latest] [--no-follow]\n follow one live transcript or list running engine work');
2550
2625
  console.log('\n long read-only asks: atris engine ask "..." --engine agy --timeout <seconds>\n quick asks default to 120 seconds; explicit jobs allow up to 3600\n follow live work with atris engine watch latest');
2551
- console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n flags a model with a newer version on this machine, or a pin not proven in 30 days\n (a landed run, or "verified 2026-09-30" on its line, proves it)\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine sweep [--apply] [--json]\n list ssh-agents that engine shells left running; --apply rechecks and stops them\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
2626
+ console.log('\n atris engine roster + current default\n atris engine roster [--json] show every job and its workers in order: search, build, review, and your own\n flags a model with a newer version on this machine, or a pin not proven in 30 days\n (a landed run, or "verified 2026-09-30" on its line, proves it)\n atris engine roster confirm renew every dated worker for 30 days\n atris engine roster session [clear]\n show or drop the roster changes for this shell only\n atris engine roster --available\n tools on this machine and the models each one offers\n atris engine roster set <job> <tool> <model>\n swap the lead of a job to a tool and model this machine offers; the old lead becomes the first backup\n atris engine roster set --member <name> <tool> <model>\n give one member its own model; every other job stays put\n atris engine roster --runs [job]\n the last 20 runs, newest first, with tool, model, time, and outcome\n atris engine assign <job> <tool> [--like <kind>] [--model <m>] [--effort <level>] [--max "20 min"] [--prep <job>] [--backup "<tool> [model]"] [--days <n>] [--add] [--session | --everywhere]\n atris engine assign <job> --promote "<tool> [model]" | --remove <tool> | --clear [--session | --everywhere]\n sets the lead worker of the job; --promote moves one of its workers to the top as written;\n --add puts one more at the end; jobs: search, build, review,\n or your own like "small build" (--like search|build|review when the name does not say);\n --prep search has that job read the task first and hand this worker a short brief;\n --everywhere is for all projects; --session is this shell only (set ATRIS_ROSTER_SESSION=<name>)\n atris engines --chart show the fleet as an org chart\n atris engine list --json [--all] full registry: default + engines with tier, roles, fallback, health (--all includes hidden engines)\n atris engine set <name> --duty leader|errands|learning [--models "a, b"]\n arrange the fleet and save its model policy\n atris engine resolve <role> or <job> [--json]\n choose the best ready engine for navigator|executor|validator or a roster job;\n notes always answers: its roster pick, else agy then haiku\n atris engine health <name> --set ready|not_installed|credit_out\n flip runtime health, for example when credits run out\n atris engine doctor [--json]\n probe which engine CLIs are installed here and sync that into health policy\n atris engine <name> make that engine the default here\n atris engine test [name] preflight: run the engine CLI headless, report pass/fail\n atris engine bench [names...] [--runs N]\n ranked latency scoreboard of engine passes\n atris engine ask "<question>" --engine <name> [--engine <name> ...]\n ask several engines in parallel without allowing edits\n atris engine ask --jobs <jobs.json>\n ask different read-only questions in parallel\n atris engine validate <receipt-path|latest> [--engine <name>]\n check ask answers with a different read-only referee\n atris engine validate scoreboard\n show pass rates by worker engine\n atris engine sweep [--apply] [--json]\n list ssh-agents that engine shells left running; --apply rechecks and stops them\n atris engine dispatch <task-id> [<task-id> ...] --engine cursor|codex [--prompt-file <f>] [--yolo]\n one-command claim, worktree, build, verify, ship, ready\n atris engine login <provider> --yes\n upload a local provider CLI login to the backend vault\n atris engine login <provider> --computer [--seat <name>]\n atris engine login <provider> --business <id> [--seat <name>]\n sign in on an Atris computer by device flow\n atris engine login --list | --remove <provider>\n list or remove vaulted provider logins\n atris engine seats show which named accounts are ready to work\n atris engine seed <provider> --business <id>|--user\n push a vaulted login onto an Atris computer\n atris engine reset back to the house default\n --engine <name> one run on that engine (mission run / autopilot / run)\n');
2552
2627
  return 0;
2553
2628
  }
2554
2629
  if (isFreshWorkspace(root)) {
package/commands/gm.js CHANGED
@@ -31,6 +31,7 @@ function showHelp() {
31
31
  console.log(' atris gm card <member> [--json] one member\'s profile page');
32
32
  console.log(' atris gm target [--landed N] [--max-abandoned N] [--max-stuck N] [--clear]');
33
33
  console.log(' atris gm phone [--port 7777] [--send] [--new-link] decision cards on your phone');
34
+ console.log(' atris gm walk [--real-copy <workspace>] [--threshold 0.8] [--no-browser] check every stage of the phone orb');
34
35
  console.log(' atris gm --json machine-readable state');
35
36
  console.log(' atris gm --watch [--interval 10]');
36
37
  console.log('');
@@ -121,6 +122,13 @@ function showHelp() {
121
122
  console.log(' tap. Every tap is logged to ~/.atris/state/gm-phone-actions.jsonl.');
122
123
  console.log(' --send texts the link to the number saved with atris autoland on --to.');
123
124
  console.log('');
125
+ console.log(' atris gm walk starts the phone page on a made-up workspace with a temp');
126
+ console.log(' home, walks every stage of the orb (the idea, an answer, the weekly');
127
+ console.log(' number, the team, a swap, the job deck, a yes card, Esc, the small and');
128
+ console.log(' big orb), and prints one row per stage: pass or fail, and why. Rules in');
129
+ console.log(' code and a fast judge model both have to agree. atris gm walk --help');
130
+ console.log(' says more. It runs from an atris checkout and never touches ~/.atris.');
131
+ console.log('');
124
132
  console.log(' The league treats every real project folder with task events in the');
125
133
  console.log(' last 30 days as a club: team rating, landed this week and in 30 days,');
126
134
  console.log(' drops, abandons, stuck claims, last task event, and best player.');
@@ -1622,6 +1630,22 @@ async function gmPhone(args = []) {
1622
1630
  return undefined;
1623
1631
  }
1624
1632
 
1633
+ // ---- walk ----------------------------------------------------------------------
1634
+
1635
+ // gm walk: every stage of the phone orb, walked and judged on a made-up
1636
+ // workspace (scripts/orb-walk.js). The published package leaves the script
1637
+ // out, so an install says so instead of failing on a missing file.
1638
+ async function gmWalk(args = []) {
1639
+ const script = path.join(__dirname, '..', 'scripts', 'orb-walk.js');
1640
+ if (!fs.existsSync(script)) {
1641
+ console.error('atris gm walk runs from an atris checkout. this install does not include the walk.');
1642
+ process.exitCode = 2;
1643
+ return undefined;
1644
+ }
1645
+ process.exitCode = await require(script).main(args);
1646
+ return undefined;
1647
+ }
1648
+
1625
1649
  // ---- player card ------------------------------------------------------------
1626
1650
 
1627
1651
  // One member's profile page as lines. Pure: card in, lines out.
@@ -1844,6 +1868,12 @@ function wakeableMember(args) {
1844
1868
  }
1845
1869
 
1846
1870
  async function gmCommand(...args) {
1871
+ // the walk has its own help
1872
+ if (positional(args)[0] === 'walk') {
1873
+ const at = args.indexOf('walk');
1874
+ return gmWalk([...args.slice(0, at), ...args.slice(at + 1)]);
1875
+ }
1876
+
1847
1877
  if (args.includes('--help') || args.includes('-h') || args[0] === 'help') {
1848
1878
  showHelp();
1849
1879
  return;