atris 3.37.1 → 3.40.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 (164) hide show
  1. package/AGENTS.md +25 -6
  2. package/README.md +2 -0
  3. package/atris/PERSONA.md +11 -11
  4. package/atris.md +17 -2
  5. package/ax +160 -34
  6. package/bin/atris.js +78 -6
  7. package/commands/activate.js +16 -3
  8. package/commands/agent-spawn.js +13 -18
  9. package/commands/autoland.js +76 -47
  10. package/commands/autopilot-front.js +0 -4
  11. package/commands/autopilot.js +0 -12
  12. package/commands/bench.js +10 -12
  13. package/commands/brain.js +0 -7
  14. package/commands/brief.js +0 -2
  15. package/commands/business-sync.js +0 -1
  16. package/commands/business.js +452 -12
  17. package/commands/chat-scan.js +5 -7
  18. package/commands/clean.js +31 -2
  19. package/commands/close.js +46 -11
  20. package/commands/cloud.js +0 -1
  21. package/commands/codex-goal.js +8 -12
  22. package/commands/computer.js +6 -5
  23. package/commands/console.js +64 -11
  24. package/commands/decide.js +166 -0
  25. package/commands/deck.js +30 -12
  26. package/commands/dream.js +0 -7
  27. package/commands/drill.js +14 -25
  28. package/commands/engine.js +206 -26
  29. package/commands/experiments.js +0 -4
  30. package/commands/fleet-report.js +12 -4
  31. package/commands/game.js +261 -0
  32. package/commands/gm.js +17 -7
  33. package/commands/goal.js +0 -6
  34. package/commands/harvest.js +1 -5
  35. package/commands/improve.js +29 -8
  36. package/commands/init.js +41 -37
  37. package/commands/integrations.js +0 -7
  38. package/commands/land.js +77 -20
  39. package/commands/launchpad.js +1 -16
  40. package/commands/lesson.js +17 -7
  41. package/commands/lifecycle.js +5 -5
  42. package/commands/log.js +55 -5
  43. package/commands/loop-front.js +0 -2
  44. package/commands/loops.js +6 -5
  45. package/commands/meet.js +0 -2
  46. package/commands/member.js +622 -573
  47. package/commands/mission.js +790 -314
  48. package/commands/next.js +0 -1
  49. package/commands/now.js +0 -12
  50. package/commands/one-lap.js +8 -8
  51. package/commands/orb.js +0 -4
  52. package/commands/pack-craft.js +0 -1
  53. package/commands/pack.js +3599 -424
  54. package/commands/play.js +6 -6
  55. package/commands/probe.js +12 -6
  56. package/commands/pulse.js +88 -64
  57. package/commands/push.js +0 -4
  58. package/commands/radar.js +14 -11
  59. package/commands/recover.js +340 -0
  60. package/commands/release.js +10 -9
  61. package/commands/report.js +0 -1
  62. package/commands/router.js +320 -0
  63. package/commands/run-front.js +0 -2
  64. package/commands/search.js +0 -3
  65. package/commands/site-deploy.js +870 -0
  66. package/commands/site.js +11 -2
  67. package/commands/skill.js +7 -2
  68. package/commands/slop.js +237 -7
  69. package/commands/status.js +6 -0
  70. package/commands/stream.js +4 -20
  71. package/commands/study.js +0 -3
  72. package/commands/sync-checkout.js +51 -0
  73. package/commands/sync.js +11 -40
  74. package/commands/task.js +999 -595
  75. package/commands/taste.js +101 -0
  76. package/commands/teach.js +86 -0
  77. package/commands/team.js +83 -3
  78. package/commands/unknowns.js +0 -10
  79. package/commands/update.js +0 -1
  80. package/commands/vercel.js +4 -2
  81. package/commands/visualize.js +0 -5
  82. package/commands/voice.js +195 -0
  83. package/commands/watch.js +1 -28
  84. package/commands/wiki.js +42 -15
  85. package/commands/wish.js +0 -1
  86. package/commands/workflow.js +2 -2
  87. package/commands/worktree.js +88 -28
  88. package/commands/xp.js +28 -30
  89. package/commands/youtube.js +0 -2
  90. package/lib/accept-verify-gate.js +126 -0
  91. package/lib/activity-stream.js +0 -2
  92. package/lib/arg-parser.js +41 -0
  93. package/lib/auto-accept-certified.js +261 -3
  94. package/lib/autoland.js +66 -3
  95. package/lib/ax-auto-lane.js +68 -7
  96. package/lib/bench/context.js +0 -2
  97. package/lib/bench/report.js +0 -1
  98. package/lib/bench/runner.js +18 -11
  99. package/lib/brief-ledger.js +0 -6
  100. package/lib/business-simulate.js +382 -0
  101. package/lib/chat-log-scan.js +0 -5
  102. package/lib/checkout-sync.js +52 -0
  103. package/lib/claude-boot-block.js +48 -0
  104. package/lib/cloud-mission.js +0 -6
  105. package/lib/codex-flight.js +0 -1
  106. package/lib/conductor-artifacts.js +25 -0
  107. package/lib/context-gatherer.js +7 -2
  108. package/lib/deck-compose.js +0 -1
  109. package/lib/deck-history.js +0 -1
  110. package/lib/deck-layout.js +0 -5
  111. package/lib/deck-review.js +0 -7
  112. package/lib/deck-schema.js +0 -1
  113. package/lib/default-verifier.js +0 -2
  114. package/lib/engine-registry.js +157 -28
  115. package/lib/experiments/daily.js +0 -6
  116. package/lib/falsifier-probe.js +84 -0
  117. package/lib/fleet.js +275 -42
  118. package/lib/functional-owner.js +0 -3
  119. package/lib/git-spawn.js +15 -0
  120. package/lib/inspect-fields.js +0 -1
  121. package/lib/json-file.js +37 -0
  122. package/lib/known-commands.js +2 -2
  123. package/lib/learnings.js +0 -2
  124. package/lib/lesson-preflight.js +146 -0
  125. package/lib/loop-doctor.js +11 -19
  126. package/lib/manifest.js +0 -2
  127. package/lib/member-alive.js +0 -1
  128. package/lib/member-switches.js +0 -5
  129. package/lib/mission-artifact.js +0 -1
  130. package/lib/mission-human-asks.js +28 -0
  131. package/lib/mission-protected-lane.js +296 -0
  132. package/lib/mission-room.js +0 -2
  133. package/lib/mission-runtime-loop.js +0 -1
  134. package/lib/next-moves.js +0 -18
  135. package/lib/official-cli-integration.js +47 -6
  136. package/lib/orb-context.js +14 -4
  137. package/lib/orb-scorecard.js +0 -1
  138. package/lib/outbound-send-gate.js +0 -6
  139. package/lib/pack-capabilities.js +685 -0
  140. package/lib/permission-grants.js +0 -6
  141. package/lib/policy-lessons.js +0 -2
  142. package/lib/pulse.js +68 -15
  143. package/lib/review-integrity.js +0 -2
  144. package/lib/router-brain.js +51 -3
  145. package/lib/runner-command.js +0 -10
  146. package/lib/runtime-bootstrap.js +0 -1
  147. package/lib/scorecard.js +0 -1
  148. package/lib/self-drive.js +45 -14
  149. package/lib/state-detection.js +0 -6
  150. package/lib/task-db.js +137 -7
  151. package/lib/task-decision.js +50 -0
  152. package/lib/taste-lessons.js +153 -0
  153. package/lib/team-presence.js +0 -1
  154. package/lib/tool-result-encode.js +17 -1
  155. package/lib/voice-gate.js +129 -2
  156. package/lib/wiki.js +152 -9
  157. package/lib/wish-audit.js +21 -7
  158. package/lib/wish-delegate.js +27 -21
  159. package/lib/wish-stats.js +0 -5
  160. package/lib/wish-store.js +0 -1
  161. package/lib/workspace-safety.js +0 -2
  162. package/lib/zip.js +95 -8
  163. package/package.json +2 -1
  164. package/templates/business-starter/persona.md +9 -0
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ const { addTaste, listTaste } = require('../lib/taste-lessons');
4
+
5
+ const VALID_FLAGS = new Set(['--why', '--scope', '--example']);
6
+
7
+ function usage(stdout = process.stdout) {
8
+ stdout.write([
9
+ 'usage: atris taste keep|kill|more "<subject>" --why "<reason>" [--scope writing|design|code|any] [--example path]',
10
+ ' atris taste list [--scope writing|design|code|any]',
11
+ '',
12
+ ].join('\n'));
13
+ }
14
+
15
+ function flagValue(args, flag) {
16
+ const index = args.indexOf(flag);
17
+ if (index === -1) return undefined;
18
+ const value = args[index + 1];
19
+ if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
20
+ return value;
21
+ }
22
+
23
+ function rejectUnknownArgs(args, positionalCount) {
24
+ for (let index = positionalCount; index < args.length; index += 1) {
25
+ const value = args[index];
26
+ if (!value.startsWith('--')) continue;
27
+ if (!VALID_FLAGS.has(value)) throw new Error(`unknown option: ${value}`);
28
+ index += 1;
29
+ }
30
+ }
31
+
32
+ function renderTaste(entries, stdout = process.stdout, filtered = false) {
33
+ if (!entries.length) {
34
+ stdout.write(`${filtered ? 'no taste lessons match this scope.' : 'no taste lessons have been recorded yet.'}\n`);
35
+ return;
36
+ }
37
+
38
+ for (const entry of entries) {
39
+ stdout.write(`the operator's verdict is ${entry.verdict} for "${entry.subject}".\n`);
40
+ stdout.write(`the reason is: ${entry.why}\n`);
41
+ stdout.write(`this applies to ${entry.scope}.\n`);
42
+ if (entry.example) stdout.write(`the example is ${entry.example}.\n`);
43
+ stdout.write(`this was added on ${entry.added}.\n\n`);
44
+ }
45
+ }
46
+
47
+ function tasteCommand(args = [], options = {}) {
48
+ const stdout = options.stdout || process.stdout;
49
+ const stderr = options.stderr || process.stderr;
50
+ const root = options.root || process.cwd();
51
+ const [subcommand, ...rest] = args;
52
+
53
+ if (!subcommand) {
54
+ renderTaste(listTaste({ root }), stdout);
55
+ stdout.write('\n');
56
+ usage(stdout);
57
+ return 0;
58
+ }
59
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
60
+ usage(stdout);
61
+ return 0;
62
+ }
63
+
64
+ try {
65
+ if (subcommand === 'list') {
66
+ rejectUnknownArgs(rest, 0);
67
+ const scope = flagValue(rest, '--scope');
68
+ renderTaste(listTaste({ root, scope }), stdout, !!scope);
69
+ return 0;
70
+ }
71
+
72
+ if (!['keep', 'kill', 'more'].includes(subcommand)) {
73
+ stderr.write(`unknown taste verdict: ${subcommand}.\n`);
74
+ usage(stderr);
75
+ return 2;
76
+ }
77
+
78
+ rejectUnknownArgs(rest, 1);
79
+ const subject = rest[0];
80
+ const why = flagValue(rest, '--why');
81
+ const scope = flagValue(rest, '--scope') || 'any';
82
+ const example = flagValue(rest, '--example');
83
+ const entry = addTaste({
84
+ verdict: subcommand,
85
+ subject,
86
+ why,
87
+ scope,
88
+ example,
89
+ added: new Date().toISOString().slice(0, 10),
90
+ root,
91
+ });
92
+ stdout.write(`saved the operator's ${entry.verdict} verdict for "${entry.subject}".\n`);
93
+ return 0;
94
+ } catch (error) {
95
+ stderr.write(`taste could not continue: ${error.message}.\n`);
96
+ usage(stderr);
97
+ return 2;
98
+ }
99
+ }
100
+
101
+ module.exports = { tasteCommand };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * atris teach — turn one bad turn into a permanent guard.
5
+ *
6
+ * atris teach list cases and which ones are guards
7
+ * atris teach add --id <id> --prompt "…" --require "<fragment>" [--complaint "…"]
8
+ * atris teach run [<id>] run one case (or all) and advance state
9
+ * atris teach guards rerun every promoted guard (regression suite)
10
+ *
11
+ * Thin shim over `python -m rl.teach` in the backend venv, which owns the
12
+ * red gate: a case must be observed FAILING before it can become a guard.
13
+ * Nonzero exit means a guard regressed.
14
+ */
15
+
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+ const { spawnSync } = require('child_process');
19
+
20
+ const BACKEND_ROOT = '/Users/keshavrao/arena/atrisos-backend';
21
+
22
+ function requireVenvPython() {
23
+ const venvPython = path.join(BACKEND_ROOT, 'venv/bin/python');
24
+ if (!fs.existsSync(venvPython)) {
25
+ console.error(`✗ Backend venv not found: ${venvPython}`);
26
+ process.exit(1);
27
+ }
28
+ return venvPython;
29
+ }
30
+
31
+ function usage() {
32
+ console.log('atris teach — a bad turn becomes a failing benchmark, then a guard');
33
+ console.log('');
34
+ console.log(' atris teach list cases (red = not yet fixed, guard = protected)');
35
+ console.log(' atris teach add … record a misbehavior as a red case');
36
+ console.log(' atris teach run [<id>] run a case; a red case is SUPPOSED to fail');
37
+ console.log(' atris teach guards rerun every guard; nonzero exit = regression');
38
+ console.log(' atris teach drafts corrections captured from live turns');
39
+ console.log(' atris teach fix <id> brief a fix for a reproduced case (--dispatch to run it)');
40
+ console.log(' atris teach scaffold … turn a captured correction into a red case');
41
+ console.log(' atris teach mine which recorded tick failures could become cases');
42
+ console.log('');
43
+ console.log(' add flags: --id <kebab> --prompt "…" --require "<fragment>" (repeatable)');
44
+ console.log(' --complaint "…" --file path=@localfile --forbid-changed-files');
45
+ }
46
+
47
+ /**
48
+ * Map the operator-facing verbs onto `python -m rl.teach` arguments.
49
+ * Returns null for help.
50
+ */
51
+ function buildArgs(subcommand, args) {
52
+ if (!subcommand || subcommand === 'list') return ['list'];
53
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return null;
54
+ if (subcommand === 'add') return ['add', ...args];
55
+ if (subcommand === 'guards') return ['run', '--guards', ...args];
56
+ if (subcommand === 'mine') return ['mine', ...args];
57
+ if (subcommand === 'drafts') return ['drafts', ...args];
58
+ if (subcommand === 'fix') {
59
+ // `atris teach fix <id>` -> `fix --case <id>`
60
+ if (args.length && !args[0].startsWith('-')) return ['fix', '--case', args[0], ...args.slice(1)];
61
+ return ['fix', ...args];
62
+ }
63
+ if (subcommand === 'scaffold') return ['scaffold', ...args];
64
+ if (subcommand === 'run') {
65
+ // `atris teach run <id>` -> `run --case <id>`; bare `run` runs every case.
66
+ if (args.length && !args[0].startsWith('-')) return ['run', '--case', args[0], ...args.slice(1)];
67
+ return ['run', ...args];
68
+ }
69
+ // `atris teach <id>` is shorthand for running that one case.
70
+ if (!subcommand.startsWith('-')) return ['run', '--case', subcommand, ...args];
71
+ return null;
72
+ }
73
+
74
+ module.exports = function teach(subcommand, ...args) {
75
+ const moduleArgs = buildArgs(subcommand, args);
76
+ if (moduleArgs === null) {
77
+ usage();
78
+ return;
79
+ }
80
+ const result = spawnSync(requireVenvPython(), ['-m', 'rl.teach', ...moduleArgs], {
81
+ cwd: path.join(BACKEND_ROOT, 'backend'),
82
+ stdio: 'inherit',
83
+ });
84
+ process.exit(result.status === null ? 1 : result.status);
85
+ };
86
+
package/commands/team.js CHANGED
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const path = require('path');
4
+
5
+ const { canonicalEngineName } = require('../lib/engine-registry');
3
6
  const taskDb = require('../lib/task-db');
4
7
  const { buildTeamPresence, DEFAULT_FRESHNESS_WINDOW_MS, renderTeamPresence } = require('../lib/team-presence');
5
8
  const { listMissions, listWorktreeRollupMissions } = require('./mission');
@@ -45,11 +48,79 @@ function collectTeamPresence(deps = {}) {
45
48
  });
46
49
  }
47
50
 
51
+ // One team view: member folders under atris/team/ are the who, live missions
52
+ // are the what-they-run-on. The fleet keeps no state file, so an engine
53
+ // "assignment" is read straight from missions still in flight.
54
+ const ROSTER_LIVE_MISSION_STATUSES = new Set(['running', 'planning']);
55
+
56
+ function collectMembers(root, deps = {}) {
57
+ if (Array.isArray(deps.members)) return deps.members;
58
+ const memberModule = deps.memberModule || require('./member');
59
+ return memberModule.findAllMembers(path.join(root, 'atris', 'team'));
60
+ }
61
+
62
+ // The MEMBER.md role line, made safe for a human sentence: lowercase, no em
63
+ // dashes, no repeated name prefix ("Linguist - operator language" -> "operator
64
+ // language" when the member is already named on the line).
65
+ function plainRole(member) {
66
+ // findAllMembers stamps '(no role)' on members without a role line; treat
67
+ // that placeholder as empty so the description can fill in.
68
+ const role = String(member?.role || '').trim();
69
+ const raw = (role && role !== '(no role)' ? role : String(member?.description || '')).trim();
70
+ const cleaned = raw
71
+ .replace(/[\u2013\u2014]/g, '-')
72
+ .replace(/\s+/g, ' ')
73
+ .toLowerCase()
74
+ .replace(/[.\s]+$/, '');
75
+ const name = String(member?.name || '').trim().toLowerCase();
76
+ const deduped = name && cleaned.startsWith(name)
77
+ ? cleaned.slice(name.length).replace(/^[\s:,-]+/, '')
78
+ : cleaned;
79
+ if (!deduped) return 'no role written yet';
80
+ // Keep the sentence readable: descriptions can run long, roles never should.
81
+ if (deduped.length <= 100) return deduped;
82
+ const cut = deduped.slice(0, 100);
83
+ return `${cut.slice(0, cut.lastIndexOf(' '))}`.replace(/[,;:]+$/, '');
84
+ }
85
+
86
+ function missionEngine(mission) {
87
+ return canonicalEngineName(mission?.runner) || canonicalEngineName(mission?.engine);
88
+ }
89
+
90
+ function collectTeamRoster(deps = {}) {
91
+ const root = deps.root || repoRoot(deps.cwd || process.cwd());
92
+ const engineByOwner = new Map();
93
+ for (const mission of collectMissions(root, deps)) {
94
+ if (!ROSTER_LIVE_MISSION_STATUSES.has(String(mission?.status || '').toLowerCase())) continue;
95
+ const owner = String(mission?.owner || mission?.member || '').trim().toLowerCase();
96
+ const engine = missionEngine(mission);
97
+ // Missions arrive newest-first; the first live one per owner wins.
98
+ if (owner && engine && !engineByOwner.has(owner)) engineByOwner.set(owner, engine);
99
+ }
100
+ return collectMembers(root, deps)
101
+ .map((member) => {
102
+ const name = String(member?.name || '').trim().toLowerCase();
103
+ return { name, role: plainRole(member), engine: engineByOwner.get(name) || '' };
104
+ })
105
+ .filter((entry) => entry.name)
106
+ .sort((a, b) => a.name.localeCompare(b.name));
107
+ }
108
+
109
+ function renderTeamRoster(roster) {
110
+ if (!roster.length) {
111
+ return 'no team members yet. create one with: atris member create <name> --role="..."';
112
+ }
113
+ return roster
114
+ .map((entry) => `${entry.name} - ${entry.role}${entry.engine ? `, on ${entry.engine}` : ''}.`)
115
+ .join('\n');
116
+ }
117
+
48
118
  function helpText() {
49
119
  return [
120
+ 'atris team - one team view: every member, their role, and any engine running their work',
50
121
  'atris team presence - show who is awake and what they are doing',
51
122
  '',
52
- 'usage: atris team presence [--json]',
123
+ 'usage: atris team [roster|presence] [--json]',
53
124
  ].join('\n');
54
125
  }
55
126
 
@@ -58,8 +129,17 @@ function teamCommand(args = [], deps = {}) {
58
129
  (deps.write || process.stdout.write.bind(process.stdout))(`${helpText()}\n`);
59
130
  return 0;
60
131
  }
132
+ const rosterArgs = args.filter((arg) => arg !== 'roster');
133
+ if (args[0] !== 'presence' && rosterArgs.every((arg) => arg === '--json')) {
134
+ const roster = deps.roster || collectTeamRoster(deps);
135
+ const output = rosterArgs.includes('--json')
136
+ ? JSON.stringify(roster, null, 2)
137
+ : renderTeamRoster(roster);
138
+ (deps.write || process.stdout.write.bind(process.stdout))(`${output}\n`);
139
+ return 0;
140
+ }
61
141
  if (args[0] !== 'presence' || args.some((arg, index) => index > 0 && arg !== '--json')) {
62
- (deps.error || process.stderr.write.bind(process.stderr))('usage: atris team presence [--json]\n');
142
+ (deps.error || process.stderr.write.bind(process.stderr))('usage: atris team [roster|presence] [--json]\n');
63
143
  return 2;
64
144
  }
65
145
  const presence = deps.presence || collectTeamPresence(deps);
@@ -70,4 +150,4 @@ function teamCommand(args = [], deps = {}) {
70
150
  return 0;
71
151
  }
72
152
 
73
- module.exports = { collectMissions, collectTasks, collectTeamPresence, teamCommand };
153
+ module.exports = { collectMissions, collectTasks, collectTeamPresence, collectTeamRoster, renderTeamRoster, teamCommand };
@@ -613,15 +613,5 @@ async function unknownsCommand(args = []) {
613
613
  module.exports = {
614
614
  unknownsCommand,
615
615
  showHelp,
616
- ensureUnknownsSchema,
617
- gatherTerritoryContext,
618
- buildUnknownsPrompt,
619
- analyzeWithModel,
620
- normalizeUnknowns,
621
- normalizeQuestions,
622
616
  fallbackPayload,
623
- insertUnknowns,
624
- listUnknownRows,
625
- renderUnknownsMarkdown,
626
- resolveUnknown,
627
617
  };
@@ -40,5 +40,4 @@ function updateSelf(options = {}) {
40
40
 
41
41
  module.exports = {
42
42
  updateSelf,
43
- DEFAULT_PACKAGE_ROOT,
44
43
  };
@@ -12,7 +12,8 @@ const vercelCommand = createOfficialCliCommand({
12
12
  usage: 'deploy',
13
13
  match: ['deploy'],
14
14
  forward: ['deploy'],
15
- description: 'deploy the current project',
15
+ description: 'deploy the current project (grouped output)',
16
+ summarize: true,
16
17
  },
17
18
  {
18
19
  usage: 'ls',
@@ -24,7 +25,8 @@ const vercelCommand = createOfficialCliCommand({
24
25
  usage: 'logs',
25
26
  match: ['logs'],
26
27
  forward: ['logs'],
27
- description: 'stream or inspect deployment logs',
28
+ description: 'stream or inspect deployment logs (grouped output)',
29
+ summarize: true,
28
30
  },
29
31
  {
30
32
  usage: 'inspect',
@@ -389,9 +389,4 @@ async function visualizeAtris(args = process.argv.slice(3)) {
389
389
 
390
390
  module.exports = {
391
391
  visualizeAtris,
392
- parseVisualizeArgs,
393
- buildImagePrompt,
394
- classifyArtifact,
395
- resolveOutputPath,
396
- generateVisual,
397
392
  };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+ const { spawnSync } = require('node:child_process');
7
+
8
+ const deterministicVoice = require('../scripts/det/voice');
9
+
10
+ const JUDGE_TIMEOUT_MS = 30_000;
11
+
12
+ function extractVoiceSection(repoRoot = process.cwd()) {
13
+ const candidates = [
14
+ path.join(repoRoot, 'atris.md'),
15
+ path.join(repoRoot, 'atris', 'atris.md'),
16
+ ];
17
+
18
+ for (const file of candidates) {
19
+ if (!fs.existsSync(file)) continue;
20
+ const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
21
+ const start = lines.findIndex((line) => /^## voice\s*$/i.test(line));
22
+ if (start === -1) continue;
23
+ let end = lines.length;
24
+ for (let i = start + 1; i < lines.length; i += 1) {
25
+ if (/^##\s+/.test(lines[i])) {
26
+ end = i;
27
+ break;
28
+ }
29
+ }
30
+ return lines.slice(start, end).join('\n').trim();
31
+ }
32
+
33
+ throw new Error('the voice section was not found');
34
+ }
35
+
36
+ function findExecutable(name, env = process.env) {
37
+ const pathValue = env.PATH || '';
38
+ for (const dir of pathValue.split(path.delimiter)) {
39
+ if (!dir) continue;
40
+ const candidate = path.join(dir, name);
41
+ try {
42
+ fs.accessSync(candidate, fs.constants.X_OK);
43
+ return candidate;
44
+ } catch {
45
+ // Try the next directory.
46
+ }
47
+ }
48
+ return null;
49
+ }
50
+
51
+ function judgePrompt(rubric) {
52
+ return [
53
+ 'Judge only the SHAPE and PLAINNESS of the reply provided on stdin against the rubric below.',
54
+ 'Do not judge factual correctness or technical quality.',
55
+ 'When unsure, PASS. False alarms are worse than misses.',
56
+ 'Return only strict JSON with exactly this shape: {"pass": boolean, "reasons": [strings]}.',
57
+ '',
58
+ rubric,
59
+ ].join('\n');
60
+ }
61
+
62
+ function parseVerdict(output) {
63
+ const verdict = JSON.parse(String(output || '').trim());
64
+ if (!verdict || Array.isArray(verdict) || typeof verdict !== 'object') {
65
+ throw new Error('the verdict is not an object');
66
+ }
67
+ const keys = Object.keys(verdict).sort();
68
+ if (keys.length !== 2 || keys[0] !== 'pass' || keys[1] !== 'reasons') {
69
+ throw new Error('the verdict has the wrong fields');
70
+ }
71
+ if (typeof verdict.pass !== 'boolean'
72
+ || !Array.isArray(verdict.reasons)
73
+ || !verdict.reasons.every((reason) => typeof reason === 'string')) {
74
+ throw new Error('the verdict has the wrong shape');
75
+ }
76
+ return verdict;
77
+ }
78
+
79
+ function unavailable(stderr, reason) {
80
+ const cleanReason = String(reason || 'unknown error').replace(/\s+/g, ' ').trim();
81
+ stderr.write(`voice judge unavailable: ${cleanReason}\n`);
82
+ return 0;
83
+ }
84
+
85
+ function runScan(reply, args, stdout) {
86
+ const mode = args.includes('--json') ? 'json' : 'scan';
87
+ const result = deterministicVoice.run(mode, reply);
88
+ if (result.error) return { code: 2, error: result.error };
89
+ stdout.write(`${result.text}\n`);
90
+ const passed = result.text.startsWith('PASS') || result.text.startsWith('{"pass":true');
91
+ return { code: passed ? 0 : 1 };
92
+ }
93
+
94
+ function runJudge(reply, options = {}) {
95
+ const repoRoot = options.repoRoot || process.cwd();
96
+ const env = options.env || process.env;
97
+ const stdout = options.stdout || process.stdout;
98
+ const stderr = options.stderr || process.stderr;
99
+
100
+ let rubric;
101
+ try {
102
+ rubric = extractVoiceSection(repoRoot);
103
+ } catch (error) {
104
+ return unavailable(stderr, error.message);
105
+ }
106
+
107
+ const customCommand = String(env.ATRIS_VOICE_JUDGE_CMD || '').trim();
108
+ const claudePath = customCommand ? null : findExecutable('claude', env);
109
+ if (!customCommand && !claudePath) {
110
+ return unavailable(stderr, 'no judge command was found');
111
+ }
112
+
113
+ let tempDir;
114
+ try {
115
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-voice-'));
116
+ const rubricPath = path.join(tempDir, 'rubric.md');
117
+ fs.writeFileSync(rubricPath, rubric, 'utf8');
118
+ const childEnv = { ...env, ATRIS_VOICE_RUBRIC: rubricPath };
119
+ const result = customCommand
120
+ ? spawnSync('/bin/sh', ['-c', customCommand], {
121
+ input: reply,
122
+ encoding: 'utf8',
123
+ env: childEnv,
124
+ timeout: JUDGE_TIMEOUT_MS,
125
+ maxBuffer: 1024 * 1024,
126
+ })
127
+ : spawnSync(claudePath, ['-p', '--model', 'haiku', judgePrompt(rubric)], {
128
+ input: reply,
129
+ encoding: 'utf8',
130
+ env: childEnv,
131
+ timeout: JUDGE_TIMEOUT_MS,
132
+ maxBuffer: 1024 * 1024,
133
+ });
134
+
135
+ if (result.error) return unavailable(stderr, result.error.code === 'ETIMEDOUT' ? 'the judge timed out' : 'the judge command failed');
136
+ if (result.status !== 0) return unavailable(stderr, 'the judge command failed');
137
+
138
+ let verdict;
139
+ try {
140
+ verdict = parseVerdict(result.stdout);
141
+ } catch {
142
+ return unavailable(stderr, 'the judge returned an unreadable verdict');
143
+ }
144
+
145
+ stdout.write(`${JSON.stringify(verdict)}\n`);
146
+ return verdict.pass ? 0 : 1;
147
+ } catch {
148
+ return unavailable(stderr, 'the judge command failed');
149
+ } finally {
150
+ if (tempDir) {
151
+ try {
152
+ fs.rmSync(tempDir, { recursive: true, force: true });
153
+ } catch {
154
+ // Cleanup cannot block the reply.
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ function showVoiceHelp(stdout = process.stdout) {
161
+ stdout.write([
162
+ 'usage: atris voice scan [--json]',
163
+ ' atris voice judge',
164
+ '',
165
+ 'scan checks binary voice tells. judge checks reply shape and plainness.',
166
+ '',
167
+ ].join('\n'));
168
+ }
169
+
170
+ function voiceCommand(args, options = {}) {
171
+ const subcommand = args[0];
172
+ const stdout = options.stdout || process.stdout;
173
+ const stderr = options.stderr || process.stderr;
174
+ if (!subcommand || subcommand === 'help' || subcommand === '--help' || subcommand === '-h') {
175
+ showVoiceHelp(stdout);
176
+ return 0;
177
+ }
178
+ if (!['scan', 'judge'].includes(subcommand)) {
179
+ stderr.write('usage: atris voice scan [--json] | atris voice judge\n');
180
+ return 2;
181
+ }
182
+
183
+ const reply = options.input === undefined ? fs.readFileSync(0, 'utf8') : options.input;
184
+ if (subcommand === 'scan') {
185
+ const result = runScan(reply, args.slice(1), stdout);
186
+ if (result.error) stderr.write(`${result.error}\n`);
187
+ return result.code;
188
+ }
189
+ return runJudge(reply, { ...options, stdout, stderr });
190
+ }
191
+
192
+ module.exports = {
193
+ extractVoiceSection,
194
+ voiceCommand,
195
+ };
package/commands/watch.js CHANGED
@@ -3,6 +3,7 @@
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const { spawnSync } = require('child_process');
6
+ const { hasFlag, readFlag } = require('../lib/arg-parser');
6
7
  const { startMission, listMissions } = require('./mission');
7
8
 
8
9
  const CLI_PATH = path.join(__dirname, '..', 'bin', 'atris.js');
@@ -22,28 +23,6 @@ function slugify(value) {
22
23
  .slice(0, 48) || 'watch';
23
24
  }
24
25
 
25
- function hasFlag(args, name) {
26
- return args.includes(name);
27
- }
28
-
29
- function unquote(value) {
30
- const text = String(value);
31
- if ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'"))) {
32
- return text.slice(1, -1);
33
- }
34
- return text;
35
- }
36
-
37
- function readFlag(args, name, fallback = '') {
38
- const prefix = `${name}=`;
39
- for (let i = 0; i < args.length; i += 1) {
40
- const arg = String(args[i]);
41
- if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return unquote(args[i + 1]);
42
- if (arg.startsWith(prefix)) return unquote(arg.slice(prefix.length));
43
- }
44
- return fallback;
45
- }
46
-
47
26
  function parseSentenceArgs(args) {
48
27
  const sentenceParts = [];
49
28
  let index = 0;
@@ -293,11 +272,5 @@ function watchAtris() {
293
272
 
294
273
  module.exports = {
295
274
  watchAtris,
296
- watchCommand,
297
- showWatchHelp,
298
275
  slugify,
299
- buildWatchObjective,
300
- activeWatchMissions,
301
- resolveWatchMission,
302
- watchStatePath,
303
276
  };