atris 3.38.0 → 3.41.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 (78) hide show
  1. package/AGENTS.md +25 -6
  2. package/atris/PERSONA.md +8 -4
  3. package/atris.md +7 -0
  4. package/ax +2 -1
  5. package/bin/atris.js +31 -6
  6. package/commands/agent-spawn.js +13 -11
  7. package/commands/autoland.js +28 -77
  8. package/commands/bench.js +10 -12
  9. package/commands/business.js +345 -0
  10. package/commands/chat-scan.js +5 -7
  11. package/commands/codex-goal.js +8 -10
  12. package/commands/computer.js +20 -0
  13. package/commands/console.js +19 -3
  14. package/commands/decide.js +166 -0
  15. package/commands/deck.js +1 -4
  16. package/commands/drill.js +14 -24
  17. package/commands/engine.js +196 -8
  18. package/commands/gm.js +8 -6
  19. package/commands/harvest.js +1 -4
  20. package/commands/init.js +23 -3
  21. package/commands/land.js +8 -14
  22. package/commands/launchpad.js +1 -14
  23. package/commands/lifecycle.js +5 -5
  24. package/commands/log.js +55 -5
  25. package/commands/member.js +558 -574
  26. package/commands/mission.js +456 -237
  27. package/commands/pack.js +2746 -164
  28. package/commands/play.js +6 -4
  29. package/commands/probe.js +2 -2
  30. package/commands/pulse.js +15 -16
  31. package/commands/release.js +10 -9
  32. package/commands/router.js +5 -4
  33. package/commands/site-deploy.js +885 -0
  34. package/commands/site.js +11 -2
  35. package/commands/slop.js +14 -2
  36. package/commands/stream.js +4 -18
  37. package/commands/task.js +880 -558
  38. package/commands/taste.js +101 -0
  39. package/commands/team.js +176 -3
  40. package/commands/vercel.js +4 -2
  41. package/commands/voice.js +195 -0
  42. package/commands/watch.js +1 -22
  43. package/commands/wiki.js +1 -4
  44. package/commands/workflow.js +2 -2
  45. package/commands/worktree.js +1 -14
  46. package/commands/xp.js +27 -24
  47. package/lib/accept-verify-gate.js +5 -1
  48. package/lib/arg-parser.js +41 -0
  49. package/lib/auto-accept-certified.js +116 -1
  50. package/lib/autoland.js +66 -0
  51. package/lib/bench/runner.js +19 -1
  52. package/lib/context-gatherer.js +7 -1
  53. package/lib/engine-registry.js +141 -20
  54. package/lib/falsifier-probe.js +84 -0
  55. package/lib/fleet.js +65 -15
  56. package/lib/git-spawn.js +15 -0
  57. package/lib/json-file.js +37 -0
  58. package/lib/known-commands.js +2 -2
  59. package/lib/lesson-preflight.js +146 -0
  60. package/lib/loop-doctor.js +0 -2
  61. package/lib/mission-human-asks.js +28 -0
  62. package/lib/mission-protected-lane.js +4 -1
  63. package/lib/official-cli-integration.js +47 -2
  64. package/lib/orb-context.js +8 -1
  65. package/lib/pack-capabilities.js +685 -0
  66. package/lib/router-brain.js +51 -1
  67. package/lib/runner-command.js +0 -6
  68. package/lib/self-drive.js +44 -13
  69. package/lib/task-db.js +137 -3
  70. package/lib/task-decision.js +50 -0
  71. package/lib/taste-lessons.js +153 -0
  72. package/lib/tool-result-encode.js +17 -1
  73. package/lib/voice-gate.js +66 -0
  74. package/lib/wish-audit.js +1 -1
  75. package/lib/wish-delegate.js +1 -1
  76. package/lib/zip.js +95 -7
  77. package/package.json +2 -1
  78. package/templates/business-starter/persona.md +9 -0
package/AGENTS.md CHANGED
@@ -1,9 +1,28 @@
1
- # AGENTS.md — Universal Agent Instructions
1
+ # AGENTS.md: Universal Agent Instructions
2
2
 
3
- > **New here?** Read [`FOR_AGENTS.md`](FOR_AGENTS.md) — open letter from an agent who tried it.
3
+ > **New here?** Read [`FOR_AGENTS.md`](FOR_AGENTS.md): open letter from an agent who tried it.
4
4
 
5
5
  > Works with: Claude Code, Cursor, Codex, OpenClaw, Windsurf, and any AI coding agent.
6
6
 
7
+ ## The way of talking (every reply, no exceptions)
8
+
9
+ Talk like a person. This governs every message you send the operator: boot, status, summaries, everything.
10
+
11
+ - Plain words. Say what happened and what it means for the reader, cause and effect. Not the machinery.
12
+ - No task codes, branch names, commit hashes, PR numbers, or system nouns (worktree, verifier, projection, tick) in the message body. If the reader needs a command, put ONE copyable line at the end.
13
+ - No insider terms without defining them in the same breath. A sharp non-engineer must follow it on first read.
14
+ - Lead with the outcome. Complete sentences, one or two per paragraph, blank line between. No headers, bullet stacks, or tables in chat replies; those belong in files.
15
+ - Detail lives in files and receipts. Offer "want the detail?" instead of dumping it.
16
+ - No em dashes anywhere; use a comma, colon, or period.
17
+ - The test before sending: read it fried at 2am. If decoding takes work, rewrite it.
18
+
19
+ copy these shapes:
20
+ - **Status:** "The reply check is built. I am running the final checks now, so the result is not ready yet."
21
+ - **Landing:** "Replies now get a plain-language check before they reach you. The checks passed, and the change is ready."
22
+ - **Failure:** "The plain-language check could not run because its model was unavailable. Your reply still went through, so no work was blocked."
23
+
24
+ Canonical source: `atris/atris.md` `## voice`. This block is a copy for tools that only read `AGENTS.md`; if they differ, atris.md wins.
25
+
7
26
  ## Quick Start
8
27
 
9
28
  ```bash
@@ -31,7 +50,7 @@ task truth, proof, review, and backend/cloud sync all flow through Atris.
31
50
  |------|---------|
32
51
  | `atris/atris.md` | Protocol/backbone for this workspace |
33
52
  | `atris/PERSONA.md` | Communication style (read first) |
34
- | `atris task` | Current tasks, claims, dialogue, proof — deep dive: [`tasks.md`](tasks.md) |
53
+ | `atris task` | Current tasks, claims, dialogue, proof; deep dive: [`tasks.md`](tasks.md) |
35
54
  | `.atris/state/tasks.projection.json` | Readable task projection for UIs/agents |
36
55
  | `atris/TODO.md` | Rendered/legacy task view only |
37
56
  | `atris/MAP.md` | Navigation (where is X?) |
@@ -125,13 +144,13 @@ member -> mission start --verify -> status --status active -> one bounded step -
125
144
  - Close: if the verifier passes, run `atris mission complete <id> --proof "<receipt_path>"`; if current-agent work should keep going, repeat status -> step -> tick.
126
145
  - Rollout: `atrisos-backend` and `atrisos-web` agents must check active missions before picking work; if no active mission exists and autonomy was requested, create one with owner, verifier, lane, and stop condition first.
127
146
 
128
- ## Build Craft — what decides acceptance
147
+ ## Build Craft: what decides acceptance
129
148
 
130
149
  Mined from this repo's receipts (803 receipts, 1711 episodes): proofs naming a
131
150
  runnable verify command were accepted 670/674 at the gate; 24 of 28 bounces
132
151
  named none. These rules are the difference, in priority order:
133
152
 
134
- 1. **Name a runnable verify command in every proof**, and run it bare — never
153
+ 1. **Name a runnable verify command in every proof**, and run it bare; never
135
154
  `test | tail` or `| grep`; a pipe replaces your exit code with the filter's.
136
155
  2. **A task naming a spec file (`atris/features/<name>/idea.md`) is a contract.**
137
156
  Read it before writing code; build the named slice only; use its verify
@@ -157,7 +176,7 @@ named none. These rules are the difference, in priority order:
157
176
  reproduces it against the real runtime, not a mock that can stay green
158
177
  through breakage.
159
178
  10. **When your engine dies mid-build (credits, limits), that is a staffing
160
- event, not a failure** — leave the worktree intact with a note; the
179
+ event, not a failure**; leave the worktree intact with a note; the
161
180
  conductor restaffs it.
162
181
 
163
182
  ## Rules
package/atris/PERSONA.md CHANGED
@@ -1,4 +1,4 @@
1
- # PERSONA.md — Atris Agent Personality
1
+ # PERSONA.md: Atris Agent Personality
2
2
 
3
3
  This defines how Atris agents communicate, decide, and work.
4
4
 
@@ -28,7 +28,7 @@ This defines how Atris agents communicate, decide, and work.
28
28
 
29
29
  ## Core Workflow
30
30
 
31
- **Read before you act.** Before planning or building, read the relevant files. Understand the current state. Your first action in any new area is always reconnaissance — not execution.
31
+ **Read before you act.** Before planning or building, read the relevant files. Understand the current state. Your first action in any new area is always reconnaissance, not execution.
32
32
 
33
33
  **Ask for intent when it's fuzzy or the blast radius is real.** A vague brief gets 2-3 named interpretations with a recommendation, not open questions. Small, reversible, clearly-scoped work: act, then report. Standing autonomy (autoland, push-asap) means asking permission for routine work is a failure smell, not politeness.
34
34
 
@@ -52,9 +52,13 @@ We can always add layer by layer.
52
52
 
53
53
  ## Communication Style
54
54
 
55
- **3-4 sentences max.** No verbose explanations. Get to the point.
55
+ **Talk like a person.** Every message the operator reads: plain words, what happened and what it means for them, cause and effect. Not the machinery.
56
56
 
57
- Direct and casual tone. No corporate speak.
57
+ **No codes in the message body.** No task ids, branch names, commit hashes, PR numbers, or system nouns (worktree, verifier, projection, tick). If the reader needs a command, ONE copyable line at the end. Insider terms get defined in the same breath or cut.
58
+
59
+ **Lead with the outcome.** Complete sentences, one or two per paragraph, blank line between. No headers, bullet stacks, or tables in chat replies. Detail lives in files; offer "want the detail?" instead of dumping it.
60
+
61
+ **The test before sending:** read it fried at 2am. If decoding takes work, rewrite it.
58
62
 
59
63
  If something is slop, call it out. Optimize ruthlessly.
60
64
 
package/atris.md CHANGED
@@ -83,6 +83,13 @@ What you ship should not read as generated. The test: if someone said "an AI mad
83
83
 
84
84
  The same discipline for words. Output stays sharp no matter how bloated the context. A full context is not license to ramble.
85
85
 
86
+ - **The way of talking.** Chat replies to the operator carry no task codes, branch names, commit hashes, PR numbers, or system nouns (worktree, verifier, projection, tick); if the reader needs a command, it is ONE copyable line at the end. Insider terms get defined in the same breath or cut. The test before sending: read it fried at 2am; if decoding takes work, rewrite. This is the canonical statement; `AGENTS.md` and other adapters carry a copy of it, never a variant.
87
+
88
+ copy these shapes:
89
+ - **Status:** "The reply check is built. I am running the final checks now, so the result is not ready yet."
90
+ - **Landing:** "Replies now get a plain-language check before they reach you. The checks passed, and the change is ready."
91
+ - **Failure:** "The plain-language check could not run because its model was unavailable. Your reply still went through, so no work was blocked."
92
+
86
93
  - **Lead with the move.** Answer first, support after. No preamble, no agreement reflex ("great question", "you're absolutely right").
87
94
  - **Specific over buzzy.** Name the exact thing. If you can't, you don't understand it yet; go look, don't hedge.
88
95
  - **Cut filler.** Drop "it's worth noting", "in order to", "leverage", "seamless", "robust", "delve", stacked hedges, and em dashes. `atris slop` flags the prose tells (em-dash, hype-copy) too.
package/ax CHANGED
@@ -13,6 +13,7 @@ const { loadCredentials } = require('./utils/auth');
13
13
  const { apiRequestJson, getApiBaseUrl } = require('./utils/api');
14
14
  const missionRuntime = require('./lib/mission-runtime-loop');
15
15
  const { loadOverrides, pickLane } = require('./lib/ax-auto-lane');
16
+ const { buildToolResultBody } = require('./lib/tool-result-encode');
16
17
 
17
18
  const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
18
19
  const BACKEND = {
@@ -2871,7 +2872,7 @@ async function postTurn(message, options = {}) {
2871
2872
  if (options.business) {
2872
2873
  return options.business.postToolResult(callId, result, backendBaseUrl({ route: 'cloud' }));
2873
2874
  }
2874
- return postJson(BACKEND_API_TOOL_RESULT_PATH, { call_id: callId, result }, {
2875
+ return postJson(BACKEND_API_TOOL_RESULT_PATH, buildToolResultBody(callId, result), {
2875
2876
  token,
2876
2877
  route: endpointRoute,
2877
2878
  timeoutMs: 30000,
package/bin/atris.js CHANGED
@@ -504,7 +504,7 @@ function showHelp() {
504
504
  console.log(' activate - Load Atris context');
505
505
  console.log(' radar - Show live agents joined with tasks, missions, and worktrees');
506
506
  console.log(' stream - Watch the whole team work live in one terminal');
507
- console.log(' team - Show who is awake, what they are doing, and running loops');
507
+ console.log(' team - One team view: members, roles, engine assignments (presence for live)');
508
508
  console.log(' watch - Turn one sentence into an always-on background watcher');
509
509
  console.log(' ctop - Show a process-first live agent CPU/memory view');
510
510
  console.log(' launchpad - Show the next action from local brain, task, mission, and proof state');
@@ -520,15 +520,17 @@ function showHelp() {
520
520
  console.log(' harvest - Find bugs and next actions from receipts, run logs, and thinking');
521
521
  console.log(' verify - Validate work is done (tests, MAP.md, changes)');
522
522
  console.log(' slop - Deterministic slop detector: frontend/prose tells, dead code (slop dead --exports)');
523
+ console.log(' voice - Scan binary voice tells or judge reply shape and plainness');
523
524
  console.log(' recover - Explain stopped missions; --apply performs safe recovery only');
524
525
  console.log(' task - Local agent task plane (atomic claims, TODO import)');
525
- console.log(' golden path (zero human turns):');
526
+ console.log(' golden path (one tick, by cron or by hand):');
526
527
  console.log(' atris task delegate "fix the login bug" --to <member>');
527
528
  console.log(' atris task claim <id> --as <member>');
528
529
  console.log(' ... build ...');
529
530
  console.log(' atris task ready <id> --verify');
530
531
  console.log(' atris autoland tick # second check runs, task lands');
531
532
  console.log(' mission - Goal + loop + member owner + verifier + receipt; --budget quick|long|deep sets bounded tiers');
533
+ console.log(' decide - Answer open mission asks and send the decision to the next tick');
532
534
  console.log(' release - Tag release, bump version, create GitHub release, draft /launch');
533
535
  console.log(' learn - Project learnings (patterns, pitfalls, preferences)');
534
536
  console.log(' study - On-demand learning feed: ingest topic, start server, open browser');
@@ -537,6 +539,7 @@ function showHelp() {
537
539
  console.log(' avail - Booking availability (/book/{username} weekly windows)');
538
540
  console.log(' brain - Compile MAP/TODO/wiki/state into a loadable agent brain');
539
541
  console.log(' lesson - Append a one-line lesson to atris/lessons.md (mine: distill receipts/episodes/scorecards into policy lessons)');
542
+ console.log(' taste - Record the operator\'s keep, kill, and more creative verdicts');
540
543
  console.log(' teach - Turn a bad turn into a failing benchmark, then a permanent guard (red gate: no promotion without a proven failure)');
541
544
  console.log(' ingest - Local-first wiki ingest into atris/wiki/');
542
545
  console.log(' query - Local-first wiki query against atris/wiki/');
@@ -551,7 +554,7 @@ function showHelp() {
551
554
  console.log(' worktree - Isolated Git worktrees plus guarded ship/merge for parallel agents');
552
555
  console.log(' land - The landing: what is actually done vs still in the air; --reap backs up + clears overdue');
553
556
  console.log(' drive - One self-driving tick: mission doctor -> auto-fix -> count disengagements');
554
- console.log(' autoland - Approve the policy once; certified work lands itself, you keep irreversible calls');
557
+ console.log(` autoland - Approve the policy once; ${require('../lib/autoland').certifiedWorkLandsPhrase(process.cwd())}, you keep irreversible calls`);
555
558
  console.log(' engine - Engine registry: list/resolve roles, health flips, default engine, `engine test`, and dispatch flights');
556
559
  console.log(' router - inspect ax lane outcomes and promote gated reflex overrides');
557
560
  console.log(' sign - Co-author trailer on every commit in an atris workspace (on/off/status)');
@@ -588,7 +591,7 @@ function showHelp() {
588
591
  console.log(' browse [query] - Discover workspace templates');
589
592
  console.log(' fork <template> - Clone a template into a new workspace');
590
593
  console.log(' publish - Share your workspace as a template');
591
- console.log(' pack - Publish or install an Atris brain zip');
594
+ console.log(' pack - Craft, publish, share, browse, or install an Atris brain zip');
592
595
  console.log(' sleep [business|member] [--loop id] - Pause compute or flip a member switch');
593
596
  console.log(' wake [business|member] [--loop id] - Resume compute or flip a member switch');
594
597
  console.log('');
@@ -597,6 +600,8 @@ function showHelp() {
597
600
  console.log(' business onboard - Onboard from sparse input (--name, --website, --contact)');
598
601
  console.log(' business add <slug> - Connect a business');
599
602
  console.log(' business list - Show connected businesses');
603
+ console.log(' business room [slug] - Show the live room from the production API');
604
+ console.log(' business store on - Enable the room store with its current catalog');
600
605
  console.log(' business remove <slug> - Disconnect a business');
601
606
  console.log(' business team [slug] - Show members, roles, and admin access');
602
607
  console.log(' business health <slug> - Health report (members, workspace, issues)');
@@ -1815,6 +1820,18 @@ if (command === 'init') {
1815
1820
  } else if (command === 'router') {
1816
1821
  const code = require('../commands/router').routerCommand(process.argv.slice(3));
1817
1822
  if (Number.isInteger(code) && code !== 0) process.exit(code);
1823
+ } else if (command === 'decide') {
1824
+ // process.exit() can outrun a piped stdout: writes beyond the 64KB pipe
1825
+ // buffer are async, so large --json payloads truncate at 64KB multiples.
1826
+ // Queue an empty write and exit from its callback — it fires only after
1827
+ // every earlier buffered write has drained (BCK-1306).
1828
+ const exitAfterStdoutDrain = (code) => {
1829
+ if (process.stdout.writableLength === 0) process.exit(code);
1830
+ else process.stdout.write('', () => process.exit(code));
1831
+ };
1832
+ Promise.resolve(require('../commands/decide').decideCommand(process.argv.slice(3)))
1833
+ .then(() => exitAfterStdoutDrain(process.exitCode || 0))
1834
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); exitAfterStdoutDrain(1); });
1818
1835
  } else if (command === 'mission') {
1819
1836
  // process.exit() can outrun a piped stdout: writes beyond the 64KB pipe
1820
1837
  // buffer are async, so large --json payloads truncate at 64KB multiples.
@@ -1962,7 +1979,7 @@ if (command === 'init') {
1962
1979
  if (logsArgs.includes('--help') || logsArgs.includes('-h') || logsArgs[0] === 'help') {
1963
1980
  console.log('Usage: atris logs [--date YYYY-MM-DD] [--json]');
1964
1981
  console.log('');
1965
- console.log(' atris logs Show today\'s workspace + team journal digest');
1982
+ console.log(' atris logs Show today\'s journal entries, completed receipts, and inbox in one view');
1966
1983
  console.log(' atris logs --date <day> Show the digest for a specific day');
1967
1984
  console.log(' atris logs --json Emit the digest as JSON');
1968
1985
  process.exit(0);
@@ -2633,6 +2650,10 @@ if (command === 'init') {
2633
2650
  const subcommand = process.argv[3];
2634
2651
  const args = process.argv.slice(4);
2635
2652
  require('../commands/lesson')(subcommand, ...args);
2653
+ } else if (command === 'taste') {
2654
+ Promise.resolve(require('../commands/taste').tasteCommand(process.argv.slice(3)))
2655
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2656
+ .catch((err) => { console.error(`taste could not continue: ${err.message || err}.`); process.exit(1); });
2636
2657
  } else if (command === 'teach') {
2637
2658
  const subcommand = process.argv[3];
2638
2659
  const args = process.argv.slice(4);
@@ -2711,7 +2732,7 @@ if (command === 'init') {
2711
2732
  const subcommand = process.argv[3];
2712
2733
  const args = process.argv.slice(4);
2713
2734
  require('../commands/business').businessCommand(subcommand, ...args)
2714
- .then(() => process.exit(0))
2735
+ .then(() => process.exit(process.exitCode || 0))
2715
2736
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2716
2737
  } else if (command === 'soul') {
2717
2738
  const args = process.argv.slice(3);
@@ -2806,6 +2827,10 @@ if (command === 'init') {
2806
2827
  Promise.resolve(require('../commands/slop').slopCommand(process.argv.slice(3)))
2807
2828
  .then((code) => process.exit(typeof code === 'number' ? code : 0))
2808
2829
  .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2830
+ } else if (command === 'voice') {
2831
+ Promise.resolve(require('../commands/voice').voiceCommand(process.argv.slice(3)))
2832
+ .then((code) => process.exit(typeof code === 'number' ? code : 0))
2833
+ .catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
2809
2834
  } else if (command === 'write') {
2810
2835
  // Write: guided writing sessions — human types every word, atris structures + reviews (plan-do-review for prose).
2811
2836
  Promise.resolve(require('../commands/write').writeCommand(process.argv.slice(3)))
@@ -2,6 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const crypto = require('crypto');
4
4
  const { spawnSync } = require('child_process');
5
+ const { hasFlag: hasSingleFlag } = require('../lib/arg-parser');
5
6
 
6
7
  const SPAWN_SCHEMA = 'atris.agent_spawn.v1';
7
8
  const DOGFOOD_SCHEMA = 'atris.agent_cli_dogfood.v1';
@@ -63,8 +64,9 @@ function flagValue(args, names) {
63
64
  return null;
64
65
  }
65
66
 
66
- function hasFlag(args, names) {
67
- return names.some(name => args.includes(name));
67
+ // This command accepts alias lists, while the shared helper checks one name.
68
+ function hasAnyFlag(args, names) {
69
+ return names.some(name => hasSingleFlag(args, name));
68
70
  }
69
71
 
70
72
  function positionalArgs(args) {
@@ -148,11 +150,11 @@ function commandForEngine(request) {
148
150
  }
149
151
 
150
152
  function parseSpawnArgs(args = []) {
151
- const help = args.length === 0 || hasFlag(args, ['--help', '-h']) || args[0] === 'help';
153
+ const help = args.length === 0 || hasAnyFlag(args, ['--help', '-h']) || args[0] === 'help';
152
154
  if (help) return { help: true };
153
155
 
154
- const json = hasFlag(args, ['--json']);
155
- const dryRun = hasFlag(args, ['--dry-run']);
156
+ const json = hasAnyFlag(args, ['--json']);
157
+ const dryRun = hasAnyFlag(args, ['--dry-run']);
156
158
  const roleFlag = flagValue(args, ['--role']);
157
159
  const engine = String(flagValue(args, ['--engine']) || 'manual').toLowerCase();
158
160
  if (!ALLOWED_ENGINES.has(engine)) throw new Error(`Unknown engine: ${engine}`);
@@ -209,10 +211,10 @@ function showSpawnHelp(output = console.log, commandName = 'atris agent spawn')
209
211
  }
210
212
 
211
213
  function parseDogfoodArgs(args = []) {
212
- const help = hasFlag(args, ['--help', '-h']) || args[0] === 'help';
213
- const json = hasFlag(args, ['--json']);
214
- const live = hasFlag(args, ['--live']);
215
- const noWrite = hasFlag(args, ['--no-write']);
214
+ const help = hasAnyFlag(args, ['--help', '-h']) || args[0] === 'help';
215
+ const json = hasAnyFlag(args, ['--json']);
216
+ const live = hasAnyFlag(args, ['--live']);
217
+ const noWrite = hasAnyFlag(args, ['--no-write']);
216
218
  const model = flagValue(args, ['--model']) || 'glm-5.2';
217
219
  const timeoutRaw = Number(flagValue(args, ['--timeout']) || 45);
218
220
  const timeoutMs = Math.max(5, Math.min(300, timeoutRaw)) * 1000;
@@ -431,7 +433,7 @@ function agentSpawnCommand(args = [], deps = {}) {
431
433
  function agentSpawnListCommand(args = [], deps = {}) {
432
434
  const root = deps.root || process.cwd();
433
435
  const output = deps.output || ((line = '') => console.log(line));
434
- const json = hasFlag(args, ['--json']);
436
+ const json = hasAnyFlag(args, ['--json']);
435
437
  const requests = loadSpawnRequests(root).reverse();
436
438
  const payload = { ok: true, action: 'spawn_list', requests, state_path: spawnStatePath(root) };
437
439
  if (json) {
@@ -451,7 +453,7 @@ function agentSpawnListCommand(args = [], deps = {}) {
451
453
  function agentSpawnStatusCommand(args = [], deps = {}) {
452
454
  const root = deps.root || process.cwd();
453
455
  const output = deps.output || ((line = '') => console.log(line));
454
- const json = hasFlag(args, ['--json']);
456
+ const json = hasAnyFlag(args, ['--json']);
455
457
  const id = positionalArgs(args)[0];
456
458
  if (!id) throw new Error('Missing spawn id. Usage: atris agent spawn-status <id>');
457
459
  const request = loadSpawnRequests(root).find(req => req.id === id);
@@ -6,7 +6,7 @@ const os = require('os');
6
6
  const { spawnSync } = require('child_process');
7
7
 
8
8
  const autoland = require('../lib/autoland');
9
- const { gateForHuman } = require('../lib/voice-gate');
9
+ const { gateForHuman, plainLandingReason } = require('../lib/voice-gate');
10
10
  const { evaluateAutoAccept } = require('../lib/auto-accept-certified');
11
11
  const { operatorReady, hasAgentJargon, explainResult } = autoland;
12
12
  const MISSION_AUTO_VERIFY_STATUSES = new Set(['planning', 'paused', 'ready']);
@@ -436,26 +436,9 @@ function protectedReviewWaiting(root, policy = {}, waiting = []) {
436
436
  .filter((row) => !alreadyWaiting.has(row.ref));
437
437
  }
438
438
 
439
- function plainReason(reason) {
440
- const map = {
441
- denied_tag_billing: 'money: human decision required',
442
- denied_tag_deploy: 'a deploy: human decision required',
443
- denied_tag_security: 'security: human decision required',
444
- denied_tag_customer: 'customer-facing: human decision required',
445
- denied_tag_external: 'outward-facing: human decision required',
446
- denied_tag_feedback: 'customer feedback: human decision required',
447
- needs_second_reviewer_or_third_pass: 'needs one more independent check first',
448
- needs_independent_reviewer: 'built and judged by the same actor, needs an independent check',
449
- verifier_is_builder: 'the re-check actor built this row, another actor must re-check',
450
- judge_equals_worker: 'built and judged by the same actor, hand the review to someone else',
451
- not_agent_certified: 'not certified yet',
452
- insufficient_review_passes: 'not enough review passes yet',
453
- strict_verify_missing: 'no recorded check command to re-run',
454
- verify_failed: 'its check command failed on re-run',
455
- proof_unmerged_or_draft_pr_boundary: 'its proof points at an unmerged draft',
456
- };
457
- return map[reason] || reason.replace(/_/g, ' ');
458
- }
439
+ // One reason table for every human-facing landing refusal lives in
440
+ // lib/voice-gate.js (plainLandingReason); this alias keeps call sites short.
441
+ const plainReason = plainLandingReason;
459
442
 
460
443
  function showStatus(root, args) {
461
444
  const json = args.includes('--json');
@@ -475,7 +458,8 @@ function showStatus(root, args) {
475
458
  // running hourly for weeks, and would keep reporting "running" for one
476
459
  // someone deleted. A loop that cannot confirm its own liveness from
477
460
  // evidence cannot be trusted to say it is healthy.
478
- const heartbeatInstalled = heartbeatLiveness(root, policy);
461
+ const heartbeatInstalled = autoland.heartbeatLiveness(root, policy);
462
+ const heartbeatLive = autoland.heartbeatIsLive(root);
479
463
 
480
464
  if (json) {
481
465
  console.log(JSON.stringify({
@@ -491,7 +475,7 @@ function showStatus(root, args) {
491
475
  }
492
476
 
493
477
  console.log('');
494
- console.log('autoland: certified work lands itself; you keep the irreversible calls');
478
+ console.log(`autoland: ${autoland.certifiedWorkLandsPhrase(root)}; you keep the irreversible calls`);
495
479
  console.log('');
496
480
  const policyOwner = String(policy?.enabled_by || 'unknown').trim() || 'unknown';
497
481
  const policyText = enabled
@@ -501,26 +485,33 @@ function showStatus(root, args) {
501
485
  : 'off - everything waits for you';
502
486
  console.log(` policy: ${policyText}`);
503
487
  if (enabled && acceptAll) console.log(' bar: everything lands except the protected lanes (money, deploys, security, customer, outward)');
504
- console.log(` heartbeat: ${heartbeatStatusText(root, policy)}`);
488
+ console.log(` heartbeat: ${autoland.heartbeatStatusText(root, policy)}`);
505
489
  if (policy && policy.imessage_to) console.log(` daily message: ${policy.imessage_to} at ${policy.digest_hour ?? autoland.DEFAULT_DIGEST_HOUR}:00`);
506
490
  const reapTrouble = autoland.readState(root).last_reap_error;
507
491
  if (reapTrouble) console.log(` cleanup trouble: landing sweep failed on ${reapTrouble.date} (${reapTrouble.error}) - run: atris land --reap`);
508
492
  console.log('');
509
- if (readyForRecheck.length > 0) {
510
- console.log(` ready for heartbeat recheck: ${readyForRecheck.length}`);
511
- for (const r of readyForRecheck.slice(0, 10)) console.log(` rechecks then lands ${r.ref}`);
512
- } else {
513
- console.log(' nothing is ready to land on its own right now.');
514
- }
515
493
  const humanOnly = blocked.filter((r) => String(r.reason || '').startsWith('denied_tag_'));
516
494
  const needsWork = blocked.filter((r) => !String(r.reason || '').startsWith('denied_tag_'));
517
495
  if (humanOnly.length > 0) {
518
- console.log(` protected reviews waiting on you: ${humanOnly.length}`);
519
- for (const r of humanOnly.slice(0, 10)) console.log(` waits for you ${r.ref} - ${plainReason(r.reason)}`);
496
+ console.log(` yours to decide first: ${humanOnly.length} protected ${humanOnly.length === 1 ? 'review' : 'reviews'}.`);
497
+ for (const r of humanOnly.slice(0, 10)) console.log(` ${r.ref} waits for you; ${plainReason(r.reason)}.`);
498
+ }
499
+ if (readyForRecheck.length > 0) {
500
+ if (heartbeatLive) {
501
+ console.log(` landing on their own after the hourly recheck: ${readyForRecheck.length}.`);
502
+ for (const r of readyForRecheck.slice(0, 10)) console.log(` ${r.ref} passes the recheck, then lands itself.`);
503
+ } else {
504
+ console.log(` ready to land ${autoland.STALE_HEARTBEAT_LANDING}: ${readyForRecheck.length}.`);
505
+ for (const r of readyForRecheck.slice(0, 10)) {
506
+ console.log(` ${r.ref} passes the recheck, then lands ${autoland.STALE_HEARTBEAT_LANDING}.`);
507
+ }
508
+ }
509
+ } else {
510
+ console.log(' nothing is ready to land on its own right now.');
520
511
  }
521
512
  if (needsWork.length > 0) {
522
- console.log(` not ready yet: ${needsWork.length}`);
523
- for (const r of needsWork.slice(0, 10)) console.log(` held back ${r.ref} - ${plainReason(String(r.reason || ''))}`);
513
+ console.log(` still needs work before it can land: ${needsWork.length}.`);
514
+ for (const r of needsWork.slice(0, 10)) console.log(` ${r.ref} stays put; ${plainReason(String(r.reason || ''))}.`);
524
515
  }
525
516
  if (waiting.length > 0) {
526
517
  console.log('');
@@ -699,48 +690,8 @@ function persistTickReceipt(root, receipt) {
699
690
  // loud, and call out the two states that mean the loop is wedged rather than
700
691
  // idle: work blocked behind a check the harness could not execute, and work
701
692
  // that passed every gate and still did not land.
702
- // Installed and alive are different claims, and only one of them is worth
703
- // printing. Deliberately does NOT shell out to crontab: `autoland status` is
704
- // read-only and must stay fast (a stalled crontab would block it for the full
705
- // 10s read timeout — see 'autoland status never probes a stalled crontab').
706
- // Tick receipts are local file reads and are stronger evidence anyway: a
707
- // crontab line proves configuration, a recent receipt proves the loop ran.
708
- function heartbeatLiveness(root, policy) {
709
- return typeof policy?.heartbeat_installed === 'boolean' ? policy.heartbeat_installed : null;
710
- }
711
-
712
- function lastTickAgeHours(root) {
713
- try {
714
- const runsDir = path.join(root, 'atris', 'runs');
715
- const newest = fs.readdirSync(runsDir)
716
- .filter((f) => f.startsWith('autoland-tick-') && f.endsWith('.json'))
717
- .sort()
718
- .pop();
719
- if (!newest) return null;
720
- const stamp = fs.statSync(path.join(runsDir, newest)).mtimeMs;
721
- return (Date.now() - stamp) / 3_600_000;
722
- } catch {
723
- return null;
724
- }
725
- }
726
-
727
- function heartbeatStatusText(root, policy) {
728
- const ageHours = lastTickAgeHours(root);
729
- const ageText = ageHours === null
730
- ? null
731
- : ageHours < 1 ? 'under an hour' : `${Math.floor(ageHours)}h`;
732
- // Evidence first: a receipt from the last couple of hours proves the loop is
733
- // alive no matter what the policy file remembers. The hourly cron makes two
734
- // missed hours an outage rather than jitter — and a heartbeat that has gone
735
- // quiet is the single most useful thing this line can say, because that is
736
- // the state nobody notices.
737
- if (ageHours !== null && ageHours <= 2) return `running hourly (last tick ${ageText} ago)`;
738
- if (ageHours !== null) return `SILENT - last tick ${ageText} ago; run atris autoland tick`;
739
- const installed = heartbeatLiveness(root, policy);
740
- if (installed === false) return 'not installed - run atris autoland on';
741
- if (installed === true) return 'installed, but no tick has ever run - run atris autoland tick';
742
- return 'unknown - run atris autoland on to check and repair';
743
- }
693
+ // Heartbeat liveness helpers live in lib/autoland.js so task ready copy and
694
+ // status share one gate (recent tick receipt within two hours).
744
695
 
745
696
  function stuckBacklogNote(receipt) {
746
697
  const parts = [];
@@ -1049,7 +1000,7 @@ function runTickBody(root, { json, policy, receipt }) {
1049
1000
 
1050
1001
  function showHelp() {
1051
1002
  console.log('');
1052
- console.log('atris autoland: you approve the policy once; certified work lands itself');
1003
+ console.log(`atris autoland: you approve the policy once; ${autoland.certifiedWorkLandsPhrase(process.cwd())}`);
1053
1004
  console.log('');
1054
1005
  console.log('finished work that passed its checks and two independent reviews lands');
1055
1006
  console.log('automatically with a receipt. money, deploys, security, customer, and');
package/commands/bench.js CHANGED
@@ -8,12 +8,10 @@ const {
8
8
  taskMetadata,
9
9
  } = require('../lib/bench/runner');
10
10
  const { buildBenchReport, renderBenchReportText } = require('../lib/bench/report');
11
+ const { hasFlag } = require('../lib/arg-parser');
11
12
 
12
- function hasFlag(args, name) {
13
- return args.includes(name);
14
- }
15
-
16
- function readFlag(args, name) {
13
+ // Preserve the existing rule that any inline value wins over a split value.
14
+ function readInlineFirstFlag(args, name) {
17
15
  const prefix = `${name}=`;
18
16
  const inline = args.find((arg) => String(arg).startsWith(prefix));
19
17
  if (inline) return String(inline).slice(prefix.length);
@@ -58,11 +56,11 @@ async function runCommand(args) {
58
56
  const asJson = hasFlag(args, '--json');
59
57
  try {
60
58
  const result = await runBench({
61
- pack: readFlag(args, '--pack') || undefined,
62
- engine: readFlag(args, '--engine') || undefined,
59
+ pack: readInlineFirstFlag(args, '--pack') || undefined,
60
+ engine: readInlineFirstFlag(args, '--engine') || undefined,
63
61
  taskIds: readRepeatedFlag(args, '--task'),
64
- label: readFlag(args, '--label'),
65
- experiment: readFlag(args, '--experiment'),
62
+ label: readInlineFirstFlag(args, '--label'),
63
+ experiment: readInlineFirstFlag(args, '--experiment'),
66
64
  updateBaseline: hasFlag(args, '--update-baseline'),
67
65
  stateRoot: process.cwd(),
68
66
  });
@@ -87,7 +85,7 @@ async function runCommand(args) {
87
85
  }
88
86
 
89
87
  function tasksCommand(args) {
90
- const pack = readFlag(args, '--pack') || undefined;
88
+ const pack = readInlineFirstFlag(args, '--pack') || undefined;
91
89
  const tasks = taskMetadata({ pack });
92
90
  if (hasFlag(args, '--json')) {
93
91
  console.log(JSON.stringify({
@@ -121,7 +119,7 @@ function packsCommand(args) {
121
119
  }
122
120
 
123
121
  function resultsCommand(args) {
124
- const last = Number(readFlag(args, '--last') || 0);
122
+ const last = Number(readInlineFirstFlag(args, '--last') || 0);
125
123
  const results = readResultRecords({ stateRoot: process.cwd(), last });
126
124
  if (hasFlag(args, '--json')) {
127
125
  console.log(JSON.stringify({
@@ -143,7 +141,7 @@ function resultsCommand(args) {
143
141
  }
144
142
 
145
143
  function reportCommand(args) {
146
- const pack = readFlag(args, '--pack') || undefined;
144
+ const pack = readInlineFirstFlag(args, '--pack') || undefined;
147
145
  const asJson = hasFlag(args, '--json');
148
146
  try {
149
147
  const report = buildBenchReport({ pack, stateRoot: process.cwd() });