atris 3.38.0 → 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 (77) 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 +30 -5
  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/console.js +19 -3
  13. package/commands/decide.js +166 -0
  14. package/commands/deck.js +1 -4
  15. package/commands/drill.js +14 -24
  16. package/commands/engine.js +196 -8
  17. package/commands/gm.js +8 -6
  18. package/commands/harvest.js +1 -4
  19. package/commands/init.js +23 -3
  20. package/commands/land.js +8 -14
  21. package/commands/launchpad.js +1 -14
  22. package/commands/lifecycle.js +5 -5
  23. package/commands/log.js +55 -5
  24. package/commands/member.js +558 -574
  25. package/commands/mission.js +456 -237
  26. package/commands/pack.js +2746 -164
  27. package/commands/play.js +6 -4
  28. package/commands/probe.js +2 -2
  29. package/commands/pulse.js +15 -16
  30. package/commands/release.js +10 -9
  31. package/commands/router.js +5 -4
  32. package/commands/site-deploy.js +870 -0
  33. package/commands/site.js +11 -2
  34. package/commands/slop.js +14 -2
  35. package/commands/stream.js +4 -18
  36. package/commands/task.js +880 -558
  37. package/commands/taste.js +101 -0
  38. package/commands/team.js +83 -3
  39. package/commands/vercel.js +4 -2
  40. package/commands/voice.js +195 -0
  41. package/commands/watch.js +1 -22
  42. package/commands/wiki.js +1 -4
  43. package/commands/workflow.js +2 -2
  44. package/commands/worktree.js +1 -14
  45. package/commands/xp.js +27 -24
  46. package/lib/accept-verify-gate.js +5 -1
  47. package/lib/arg-parser.js +41 -0
  48. package/lib/auto-accept-certified.js +116 -1
  49. package/lib/autoland.js +66 -0
  50. package/lib/bench/runner.js +19 -1
  51. package/lib/context-gatherer.js +7 -1
  52. package/lib/engine-registry.js +141 -20
  53. package/lib/falsifier-probe.js +84 -0
  54. package/lib/fleet.js +65 -15
  55. package/lib/git-spawn.js +15 -0
  56. package/lib/json-file.js +37 -0
  57. package/lib/known-commands.js +2 -2
  58. package/lib/lesson-preflight.js +146 -0
  59. package/lib/loop-doctor.js +0 -2
  60. package/lib/mission-human-asks.js +28 -0
  61. package/lib/mission-protected-lane.js +4 -1
  62. package/lib/official-cli-integration.js +47 -2
  63. package/lib/orb-context.js +8 -1
  64. package/lib/pack-capabilities.js +685 -0
  65. package/lib/router-brain.js +51 -1
  66. package/lib/runner-command.js +0 -6
  67. package/lib/self-drive.js +44 -13
  68. package/lib/task-db.js +137 -3
  69. package/lib/task-decision.js +50 -0
  70. package/lib/taste-lessons.js +153 -0
  71. package/lib/tool-result-encode.js +17 -1
  72. package/lib/voice-gate.js +66 -0
  73. package/lib/wish-audit.js +1 -1
  74. package/lib/wish-delegate.js +1 -1
  75. package/lib/zip.js +95 -7
  76. package/package.json +2 -1
  77. 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 };
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 };
@@ -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',
@@ -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;
package/commands/wiki.js CHANGED
@@ -1,5 +1,6 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
+ const { hasFlag } = require('../lib/arg-parser');
3
4
  const { loadCredentials } = require('../utils/auth');
4
5
  const { apiRequestJson } = require('../utils/api');
5
6
  const { loadBusinesses, saveBusinesses } = require('./business');
@@ -223,10 +224,6 @@ function printWikiHelp(scope = null) {
223
224
  console.log('');
224
225
  }
225
226
 
226
- function hasFlag(args, name) {
227
- return args.includes(name);
228
- }
229
-
230
227
  function optionValue(args, name, fallback = null) {
231
228
  const index = args.indexOf(name);
232
229
  if (index === -1 || index + 1 >= args.length) return fallback;
@@ -1,7 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const { getLogPath } = require('../lib/journal');
4
- const { encodeToolResult } = require('../lib/tool-result-encode');
4
+ const { buildToolResultBody } = require('../lib/tool-result-encode');
5
5
 
6
6
  function wrapWorkflowText(text, width = 76) {
7
7
  const normalized = String(text || '').replace(/\s+/g, ' ').trim();
@@ -140,7 +140,7 @@ function postToolResult(callId, result, base = 'http://127.0.0.1:8000') {
140
140
  const url = new URL('/api/atris2/turn/tool-result', base);
141
141
  const transport = url.protocol === 'https:' ? require('https') : require('http');
142
142
  return new Promise((resolve, reject) => {
143
- const postData = JSON.stringify({ call_id: callId, result_base64: encodeToolResult(result) });
143
+ const postData = JSON.stringify(buildToolResultBody(callId, result));
144
144
  const req = transport.request({
145
145
  hostname: url.hostname,
146
146
  port: url.port || (url.protocol === 'https:' ? 443 : 80),
@@ -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 { stampLatestOpenBriefForWorktree } = require('../lib/brief-ledger');
7
8
  const { isConductorArtifact } = require('../lib/conductor-artifacts');
8
9
 
@@ -321,20 +322,6 @@ function restoreRegeneratedAdapterChurn(root, message, { dryRun = false } = {})
321
322
  return skipped;
322
323
  }
323
324
 
324
- function readFlag(args, name, fallback = '') {
325
- const prefix = `${name}=`;
326
- for (let i = 0; i < args.length; i += 1) {
327
- const arg = String(args[i]);
328
- if (arg === name && args[i + 1] && !String(args[i + 1]).startsWith('--')) return String(args[i + 1]);
329
- if (arg.startsWith(prefix)) return arg.slice(prefix.length);
330
- }
331
- return fallback;
332
- }
333
-
334
- function hasFlag(args, name) {
335
- return args.includes(name);
336
- }
337
-
338
325
  function swarloClaim(root, { channel, taskKey, content }) {
339
326
  const script = path.join(root, 'scripts', 'swarlo.py');
340
327
  if (!fs.existsSync(script)) return 'skip: scripts/swarlo.py not found';