atris 3.15.56 → 3.16.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 (44) hide show
  1. package/AGENTS.md +2 -2
  2. package/GETTING_STARTED.md +1 -1
  3. package/PERSONA.md +4 -4
  4. package/README.md +11 -11
  5. package/atris/skills/copy-editor/SKILL.md +30 -4
  6. package/atris/skills/improve/SKILL.md +18 -20
  7. package/atris/wiki/concepts/agent-activation-contract.md +5 -3
  8. package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
  9. package/atris/wiki/index.md +1 -0
  10. package/ax +522 -73
  11. package/bin/atris.js +32 -31
  12. package/commands/align.js +0 -14
  13. package/commands/apps.js +102 -1
  14. package/commands/autopilot.js +197 -22
  15. package/commands/brain.js +219 -34
  16. package/commands/brainstorm.js +0 -829
  17. package/commands/computer.js +45 -83
  18. package/commands/improve.js +501 -0
  19. package/commands/integrations.js +228 -0
  20. package/commands/lesson.js +44 -0
  21. package/commands/member.js +4498 -226
  22. package/commands/mission.js +302 -27
  23. package/commands/now.js +89 -1
  24. package/commands/radar.js +181 -56
  25. package/commands/skill.js +37 -6
  26. package/commands/soul.js +0 -4
  27. package/commands/task.js +5582 -517
  28. package/commands/terminal.js +14 -10
  29. package/commands/wiki.js +87 -1
  30. package/commands/workflow.js +288 -73
  31. package/commands/worktree.js +52 -15
  32. package/commands/xp.js +41 -65
  33. package/lib/auto-accept-certified.js +294 -0
  34. package/lib/file-ops.js +0 -184
  35. package/lib/member-alive.js +232 -0
  36. package/lib/policy-lessons.js +280 -0
  37. package/lib/receipt-evidence.js +64 -0
  38. package/lib/state-detection.js +34 -0
  39. package/lib/task-db.js +568 -16
  40. package/lib/task-proof.js +43 -0
  41. package/package.json +1 -1
  42. package/utils/auth.js +13 -4
  43. package/commands/research.js +0 -52
  44. package/lib/section-merge.js +0 -196
@@ -81,6 +81,18 @@ async function resolveBusiness(token, slug) {
81
81
  return null;
82
82
  }
83
83
 
84
+ async function runTerminalCommand(token, businessId, workspaceId, command, timeoutSec = 30) {
85
+ return apiRequestJson(
86
+ `/business/${businessId}/workspaces/${workspaceId}/terminal`,
87
+ {
88
+ method: 'POST',
89
+ token,
90
+ body: { command, timeout: timeoutSec },
91
+ timeoutMs: (timeoutSec + 10) * 1000,
92
+ }
93
+ );
94
+ }
95
+
84
96
  async function terminalAtris() {
85
97
  // Parse args. Three forms:
86
98
  // atris terminal <business> <command...>
@@ -162,15 +174,7 @@ async function terminalAtris() {
162
174
  }
163
175
 
164
176
  // Execute the command
165
- const result = await apiRequestJson(
166
- `/business/${biz.businessId}/workspaces/${biz.workspaceId}/terminal`,
167
- {
168
- method: 'POST',
169
- token: creds.token,
170
- body: { command, timeout: timeoutSec },
171
- timeoutMs: (timeoutSec + 10) * 1000,
172
- }
173
- );
177
+ const result = await runTerminalCommand(creds.token, biz.businessId, biz.workspaceId, command, timeoutSec);
174
178
 
175
179
  if (!result.ok) {
176
180
  console.error(`\n✗ Terminal call failed: ${result.errorMessage || result.error || result.status}`);
@@ -198,4 +202,4 @@ async function terminalAtris() {
198
202
  process.exit(typeof exitCode === 'number' ? exitCode : 0);
199
203
  }
200
204
 
201
- module.exports = { terminalAtris };
205
+ module.exports = { terminalAtris, resolveBusiness, ensureAwake, runTerminalCommand };
package/commands/wiki.js CHANGED
@@ -192,7 +192,7 @@ function printWikiHelp(scope = null) {
192
192
  console.log('');
193
193
  console.log('Build a local or cloud wiki lint prompt.');
194
194
  } else {
195
- console.log('Usage: atris wiki <ingest|query|lint|search|log|loop|verify> [business] [args]');
195
+ console.log('Usage: atris wiki <ingest|query|lint|search|log|loop|verify|entities|related> [business] [args]');
196
196
  console.log('');
197
197
  console.log(' ingest <path> Local-first ingest into atris/wiki/');
198
198
  console.log(' query "question" Local-first query against atris/wiki/');
@@ -201,6 +201,8 @@ function printWikiHelp(scope = null) {
201
201
  console.log(' log [business] [N] Show recent atris/wiki/log.md entries');
202
202
  console.log(' loop Run local wiki upkeep analysis and refresh STATUS/log');
203
203
  console.log(' verify Check agent-readable source/verification metadata');
204
+ console.log(' entities [--type T] [--json] List extracted graph entities');
205
+ console.log(' related <entity> [--json] List graph relationships touching entity');
204
206
  }
205
207
  console.log('');
206
208
  console.log('Flags:');
@@ -213,6 +215,80 @@ function printWikiHelp(scope = null) {
213
215
  console.log('');
214
216
  }
215
217
 
218
+ function hasFlag(args, name) {
219
+ return args.includes(name);
220
+ }
221
+
222
+ function optionValue(args, name, fallback = null) {
223
+ const index = args.indexOf(name);
224
+ if (index === -1 || index + 1 >= args.length) return fallback;
225
+ return args[index + 1];
226
+ }
227
+
228
+ function printJsonOrText(payload, lines, asJson) {
229
+ if (asJson) {
230
+ console.log(JSON.stringify(payload, null, 2));
231
+ return;
232
+ }
233
+ lines.forEach((line) => console.log(line));
234
+ }
235
+
236
+ function readWikiGraph(root = process.cwd()) {
237
+ const graphPath = path.join(root, 'atris', 'wiki', '.graph.json');
238
+ if (!fs.existsSync(graphPath)) {
239
+ return { graphPath, graph: { schema: 'atris.wiki_graph.v1', entities: [], relationships: [] } };
240
+ }
241
+ try {
242
+ const parsed = JSON.parse(fs.readFileSync(graphPath, 'utf8'));
243
+ return {
244
+ graphPath,
245
+ graph: {
246
+ schema: parsed.schema || 'atris.wiki_graph.v1',
247
+ updated_at: parsed.updated_at || null,
248
+ entities: Array.isArray(parsed.entities) ? parsed.entities : [],
249
+ relationships: Array.isArray(parsed.relationships) ? parsed.relationships : [],
250
+ },
251
+ };
252
+ } catch {
253
+ return { graphPath, graph: { schema: 'atris.wiki_graph.v1', entities: [], relationships: [] } };
254
+ }
255
+ }
256
+
257
+ function wikiEntities(args = []) {
258
+ const asJson = hasFlag(args, '--json');
259
+ const type = optionValue(args, '--type', null);
260
+ const { graphPath, graph } = readWikiGraph();
261
+ const entities = type ? graph.entities.filter((entity) => entity.type === type) : graph.entities;
262
+ printJsonOrText(
263
+ { ok: true, action: 'entities', graph_path: graphPath, type: type || null, entities },
264
+ entities.length
265
+ ? entities.map((entity) => `${entity.type || 'concept'}\t${entity.name}`)
266
+ : ['No wiki graph entities found. Run: atris member wake wiki-miner --execute'],
267
+ asJson,
268
+ );
269
+ }
270
+
271
+ function wikiRelated(args = []) {
272
+ const asJson = hasFlag(args, '--json');
273
+ const entity = args.filter((arg) => arg !== '--json')[0] || '';
274
+ if (!entity) {
275
+ console.error('Usage: atris wiki related <entity>');
276
+ process.exit(1);
277
+ }
278
+ const wanted = entity.toLowerCase();
279
+ const { graphPath, graph } = readWikiGraph();
280
+ const related = graph.relationships
281
+ .filter((relationship) => String(relationship.from || '').toLowerCase() === wanted || String(relationship.to || '').toLowerCase() === wanted)
282
+ .slice(0, 5);
283
+ printJsonOrText(
284
+ { ok: true, action: 'related', graph_path: graphPath, entity, relationships: related },
285
+ related.length
286
+ ? related.map((relationship) => `${relationship.from} -[${relationship.type}]-> ${relationship.to}`)
287
+ : [`No wiki graph relationships found for "${entity}".`],
288
+ asJson,
289
+ );
290
+ }
291
+
216
292
  async function wikiIngest(mode, slug, sourceValue) {
217
293
  if (!sourceValue) {
218
294
  console.error('Usage: atris wiki ingest [business] <path>');
@@ -414,6 +490,14 @@ async function wikiCommand(subcommand, ...args) {
414
490
  const { mode, args: cleanArgs } = parseModeArgs(args);
415
491
 
416
492
  switch (subcommand) {
493
+ case 'entities': {
494
+ wikiEntities(cleanArgs);
495
+ break;
496
+ }
497
+ case 'related': {
498
+ wikiRelated(cleanArgs);
499
+ break;
500
+ }
417
501
  case 'ingest': {
418
502
  const [slug, sourceValue] = mode === 'cloud' ? parseCloudArgs(cleanArgs) : [null, cleanArgs.join(' ')];
419
503
  await wikiIngest(mode, slug, sourceValue);
@@ -494,4 +578,6 @@ module.exports = {
494
578
  wikiSearch,
495
579
  wikiLog,
496
580
  wikiVerify,
581
+ wikiEntities,
582
+ wikiRelated,
497
583
  };
@@ -63,101 +63,294 @@ function confidenceGatePrompt(stage) {
63
63
  ].join('\n');
64
64
  }
65
65
 
66
- function printAtris2Result(response) {
67
- if (!response || !Array.isArray(response.result)) {
68
- console.log(JSON.stringify(response, null, 2));
69
- return;
66
+ // Translate one relayed local_file_op call into a single bash command that runs
67
+ // on the business cloud workspace, mirroring the backend handler's result shapes.
68
+ // Content for write/edit travels base64 so shell quoting can't corrupt it.
69
+ function cloudFileOpCommand(args) {
70
+ const q = (s) => `'${String(s).replace(/'/g, `'\\''`)}'`;
71
+ const b64 = (s) => Buffer.from(String(s), 'utf8').toString('base64');
72
+ const op = String(args.type || '').toLowerCase();
73
+ const rawPath = String(args.path || '.');
74
+ if (rawPath.split('/').includes('..')) return null;
75
+ const p = q(rawPath);
76
+
77
+ if (op === 'bash') return `cd /workspace && ( ${args.command || 'true'} )`;
78
+ if (op === 'list') return `cd /workspace && find ${p} -maxdepth 3 -not -path '*/node_modules/*' -not -path '*/.git/*' | head -200`;
79
+ if (op === 'search') {
80
+ const query = q(String(args.query || args.pattern || ''));
81
+ return `cd /workspace && grep -rn -m 50 ${query} ${p} 2>/dev/null | head -50`;
82
+ }
83
+ if (op === 'read') return `cd /workspace && { [ -d ${p} ] && ls -p ${p} | head -200 || head -c 12000 ${p}; }`;
84
+ if (op === 'write') {
85
+ return `cd /workspace && mkdir -p "$(dirname ${p})" && echo ${q(b64(args.content || ''))} | base64 -d > ${p} && echo WROTE ${p}`;
86
+ }
87
+ if (op === 'edit') {
88
+ const py = [
89
+ 'import base64,sys',
90
+ `p=base64.b64decode('${b64(rawPath)}').decode()`,
91
+ `f=base64.b64decode('${b64(args.find || '')}').decode()`,
92
+ `r=base64.b64decode('${b64(args.replace || '')}').decode()`,
93
+ 's=open(p).read()',
94
+ "sys.exit('find text not found') if f not in s else open(p,'w').write(s.replace(f,r,1))",
95
+ ].join('; ');
96
+ return `cd /workspace && python3 -c ${q(py)} && echo EDITED ${p}`;
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function cloudFileOpResult(args, term) {
102
+ const op = String(args.type || '').toLowerCase();
103
+ const body = (term && term.data) || {};
104
+ const stdout = body.stdout || '';
105
+ const stderr = body.stderr || '';
106
+ const exitCode = body.exit_code !== undefined ? body.exit_code : null;
107
+ if (!term.ok) {
108
+ return { status: 'error', error: term.errorMessage || term.error || `terminal HTTP ${term.status}` };
109
+ }
110
+ if (exitCode !== 0 && exitCode !== null) {
111
+ return { status: 'error', error: (stderr || stdout || 'command failed').slice(0, 2000), exit_code: exitCode };
70
112
  }
113
+ if (op === 'bash') return { status: 'ok', stdout, stderr, exit_code: exitCode };
114
+ if (op === 'read') return { status: 'ok', path: args.path || '.', content: stdout.slice(0, 12000) };
115
+ if (op === 'write' || op === 'edit') return { status: 'ok', path: args.path };
116
+ return { status: 'ok', stdout: stdout.slice(0, 12000) };
117
+ }
118
+
119
+ function makeCloudExecutor({ token, businessId, workspaceId, slug }) {
120
+ const { runTerminalCommand } = require('./terminal');
121
+ return async function executeToolCall(name, args) {
122
+ if (name !== 'local_file_op') {
123
+ return { status: 'error', error: `unsupported relayed tool: ${name}` };
124
+ }
125
+ const command = cloudFileOpCommand(args || {});
126
+ if (!command) {
127
+ return { status: 'error', error: `unsupported op or unsafe path on cloud workspace ${slug}` };
128
+ }
129
+ try {
130
+ const term = await runTerminalCommand(token, businessId, workspaceId, command, 60);
131
+ return cloudFileOpResult(args || {}, term);
132
+ } catch (err) {
133
+ return { status: 'error', error: String(err.message || err).slice(0, 500) };
134
+ }
135
+ };
136
+ }
137
+
138
+ function postToolResult(callId, result, base = 'http://127.0.0.1:8000') {
139
+ const url = new URL('/api/atris2/turn/tool-result', base);
140
+ const transport = url.protocol === 'https:' ? require('https') : require('http');
141
+ return new Promise((resolve, reject) => {
142
+ const postData = JSON.stringify({ call_id: callId, result });
143
+ const req = transport.request({
144
+ hostname: url.hostname,
145
+ port: url.port || (url.protocol === 'https:' ? 443 : 80),
146
+ path: url.pathname,
147
+ method: 'POST',
148
+ headers: {
149
+ 'Content-Type': 'application/json',
150
+ 'Content-Length': Buffer.byteLength(postData),
151
+ Origin: 'http://localhost:8000'
152
+ }
153
+ }, (res) => {
154
+ let data = '';
155
+ res.on('data', (c) => data += c);
156
+ res.on('end', () => res.statusCode === 200 ? resolve() : reject(new Error(`tool-result HTTP ${res.statusCode}: ${data.slice(0, 200)}`)));
157
+ });
158
+ req.on('error', reject);
159
+ req.write(postData);
160
+ req.end();
161
+ });
162
+ }
163
+
164
+ function atris2TurnRequest(payload, executeToolCall = null) {
165
+ const http = require('http');
166
+ return new Promise((resolve, reject) => {
167
+ const postData = JSON.stringify(payload);
168
+ const req = http.request({
169
+ hostname: '127.0.0.1',
170
+ port: 8000,
171
+ path: '/api/atris2/turn',
172
+ method: 'POST',
173
+ headers: {
174
+ 'Content-Type': 'application/json',
175
+ 'Content-Length': Buffer.byteLength(postData),
176
+ // Local-desktop auth: the backend treats localhost requests with a
177
+ // localhost Origin as the free local-desktop user.
178
+ Origin: 'http://localhost:8000'
179
+ }
180
+ }, (res) => {
181
+ if (res.statusCode < 200 || res.statusCode >= 300) {
182
+ let data = '';
183
+ res.on('data', (chunk) => data += chunk);
184
+ res.on('end', () => {
185
+ let detail = data;
186
+ try { detail = JSON.parse(data).detail || data; } catch (e) { /* raw body */ }
187
+ const err = new Error(`HTTP ${res.statusCode}: ${detail}`.slice(0, 400));
188
+ err.statusCode = res.statusCode;
189
+ reject(err);
190
+ });
191
+ return;
192
+ }
71
193
 
72
- let wroteText = false;
73
- for (const event of response.result) {
74
- if (event && event.type === 'text' && event.content) {
75
- process.stdout.write(event.content);
76
- wroteText = true;
77
- } else if (event && event.type === 'assistant' && Array.isArray(event.content)) {
78
- for (const block of event.content) {
79
- if (block && block.type === 'text' && block.text) {
80
- process.stdout.write(block.text);
194
+ // SSE stream: print text deltas live, surface tool calls, capture result.
195
+ let buffer = '';
196
+ let finalResult = null;
197
+ let streamError = null;
198
+ let wroteText = false;
199
+ let idleTimer = null;
200
+ const IDLE_MS = 120000;
201
+ const resetIdle = () => {
202
+ if (idleTimer) clearTimeout(idleTimer);
203
+ idleTimer = setTimeout(() => {
204
+ req.destroy();
205
+ reject(new Error(`Stream stalled: no events for ${IDLE_MS / 1000}s`));
206
+ }, IDLE_MS);
207
+ };
208
+ resetIdle();
209
+
210
+ // Relayed tool calls run sequentially: the backend awaits each result
211
+ // before continuing the loop, so a promise chain preserves order.
212
+ let toolChain = Promise.resolve();
213
+ const handleEvent = (event) => {
214
+ if (!event || typeof event !== 'object') return;
215
+ if (event.type === 'text_delta' && event.content) {
216
+ process.stdout.write(event.content);
81
217
  wroteText = true;
218
+ } else if (event.type === 'tool_call_request' && executeToolCall) {
219
+ const { call_id: callId, name, args } = event;
220
+ const label = (args && args.type) || name || 'tool';
221
+ console.log(`\n⚙ cloud:${label}${args && args.command ? ` $ ${String(args.command).slice(0, 80)}` : ''}${args && args.path ? ` ${args.path}` : ''}`);
222
+ toolChain = toolChain
223
+ .then(() => executeToolCall(name, args))
224
+ .catch((err) => ({ status: 'error', error: String(err.message || err).slice(0, 500) }))
225
+ .then((result) => postToolResult(callId, result))
226
+ .then(() => resetIdle())
227
+ .catch((err) => console.error(`✗ tool relay failed: ${err.message}`));
228
+ } else if (event.type === 'tool_call') {
229
+ const name = event.tool || (event.input && event.input.tool) || 'tool';
230
+ console.log(`\n⚙ ${name}...`);
231
+ } else if (event.type === 'error') {
232
+ streamError = event.error || 'Atris 2 returned an error.';
233
+ } else if (event.type === 'result' && typeof event.result === 'string') {
234
+ finalResult = event.result;
82
235
  }
83
- }
84
- }
85
- }
236
+ };
86
237
 
87
- if (wroteText) {
88
- console.log('');
89
- } else {
90
- console.log(JSON.stringify(response.result, null, 2));
91
- }
238
+ res.setEncoding('utf8');
239
+ res.on('data', (chunk) => {
240
+ resetIdle();
241
+ buffer += chunk;
242
+ let idx;
243
+ while ((idx = buffer.indexOf('\n\n')) !== -1) {
244
+ const frame = buffer.slice(0, idx);
245
+ buffer = buffer.slice(idx + 2);
246
+ for (const line of frame.split('\n')) {
247
+ if (!line.startsWith('data: ')) continue;
248
+ try {
249
+ handleEvent(JSON.parse(line.slice(6)));
250
+ } catch (e) { /* ignore malformed frame */ }
251
+ }
252
+ }
253
+ });
254
+ res.on('end', () => {
255
+ if (idleTimer) clearTimeout(idleTimer);
256
+ if (streamError) {
257
+ reject(new Error(streamError));
258
+ return;
259
+ }
260
+ resolve({ finalResult, wroteText });
261
+ });
262
+ res.on('error', (err) => {
263
+ if (idleTimer) clearTimeout(idleTimer);
264
+ reject(err);
265
+ });
266
+ });
267
+
268
+ req.on('error', reject);
269
+ req.write(postData);
270
+ req.end();
271
+ });
92
272
  }
93
273
 
94
274
  async function runAtris2Local(userInput, atris2Mode) {
95
- console.log(`🚀 EXECUTING VIA ATRIS 2 ${atris2Mode.toUpperCase()}`);
275
+ let actualCommand = String(userInput || '').trim().replace(/^2\s+(fast|pro)\b/i, '').trim();
276
+
277
+ // --business <slug>: run the turn against that business's cloud workspace.
278
+ // The model loop stays on the backend; every file/bash tool call is relayed
279
+ // here and executed on the business EC2 via the /terminal endpoint.
280
+ let businessSlug = null;
281
+ const bizMatch = actualCommand.match(/(?:^|\s)--business[= ]([a-z0-9-]+)/i);
282
+ if (bizMatch) {
283
+ businessSlug = bizMatch[1].toLowerCase();
284
+ actualCommand = actualCommand.replace(bizMatch[0], ' ').replace(/\s+/g, ' ').trim();
285
+ }
286
+
287
+ console.log(`🚀 EXECUTING VIA ATRIS 2 ${atris2Mode.toUpperCase()}${businessSlug ? ` → cloud workspace ${businessSlug}` : ''}`);
96
288
  console.log('');
97
289
 
98
- const actualCommand = String(userInput || '').trim().replace(/^2\s+(fast|pro)\b/i, '').trim();
99
290
  if (!actualCommand) {
100
291
  console.log(`⚠ No command provided after "2 ${atris2Mode}"`);
101
- console.log(`Usage: atris 2 ${atris2Mode} <your command>`);
292
+ console.log(`Usage: atris 2 ${atris2Mode} [--business <slug>] <your command>`);
102
293
  process.exit(1);
103
294
  }
104
295
 
105
296
  console.log(`Running: ${actualCommand}`);
106
297
  console.log('');
107
298
 
108
- try {
109
- const http = require('http');
110
- const response = await new Promise((resolve, reject) => {
111
- const postData = JSON.stringify({
112
- message: actualCommand,
113
- workspace_path: process.cwd(),
114
- model: `atris-2-${atris2Mode}`
115
- });
116
-
117
- const options = {
118
- hostname: '127.0.0.1',
119
- port: 8000,
120
- path: '/api/agent-sdk/fast',
121
- method: 'POST',
122
- headers: {
123
- 'Content-Type': 'application/json',
124
- 'Content-Length': Buffer.byteLength(postData)
125
- }
126
- };
127
-
128
- const req = http.request(options, (res) => {
129
- let data = '';
130
- res.on('data', (chunk) => data += chunk);
131
- res.on('end', () => {
132
- try {
133
- const parsed = JSON.parse(data);
134
- if (res.statusCode < 200 || res.statusCode >= 300) {
135
- reject(new Error(parsed.detail || parsed.error || `HTTP ${res.statusCode}`));
136
- return;
137
- }
138
- resolve(parsed);
139
- } catch (e) {
140
- reject(new Error(`Failed to parse response: ${data}`));
141
- }
142
- });
143
- });
144
-
145
- req.on('error', reject);
146
- const timeoutMs = atris2Mode === 'pro' ? 30000 : 10000;
147
- req.setTimeout(timeoutMs, () => {
148
- req.destroy();
149
- reject(new Error(`Request timeout after ${timeoutMs / 1000}s`));
150
- });
151
- req.write(postData);
152
- req.end();
299
+ let executeToolCall = null;
300
+ const payload = {
301
+ message: actualCommand,
302
+ workspace_path: process.cwd(),
303
+ model: `atris:${atris2Mode}`
304
+ };
305
+
306
+ if (businessSlug) {
307
+ const { loadCredentials } = require('../utils/auth');
308
+ const { resolveBusiness, ensureAwake } = require('./terminal');
309
+ const creds = loadCredentials();
310
+ if (!creds || !creds.token) {
311
+ console.error('Not logged in. Run: atris login');
312
+ process.exit(1);
313
+ }
314
+ const biz = await resolveBusiness(creds.token, businessSlug);
315
+ if (!biz || !biz.workspaceId) {
316
+ console.error(`Business "${businessSlug}" not found or has no workspace.`);
317
+ process.exit(1);
318
+ }
319
+ const awake = await ensureAwake(creds.token, biz.businessId);
320
+ if (!awake) {
321
+ console.error('Cloud computer did not become ready in time.');
322
+ process.exit(1);
323
+ }
324
+ executeToolCall = makeCloudExecutor({
325
+ token: creds.token,
326
+ businessId: biz.businessId,
327
+ workspaceId: biz.workspaceId,
328
+ slug: businessSlug,
153
329
  });
330
+ payload.local_executor = true;
331
+ payload.workspace_path = `/workspace/${businessSlug}`;
332
+ }
154
333
 
155
- if (response.error) {
156
- throw new Error(response.error);
334
+ try {
335
+ let outcome;
336
+ try {
337
+ outcome = await atris2TurnRequest(payload, executeToolCall);
338
+ } catch (error) {
339
+ // Backends without local workspace access (prod config) reject the path;
340
+ // retry the same prompt as plain cloud chat. Never silently downgrade a
341
+ // cloud-workspace run.
342
+ if (!businessSlug && error.statusCode === 403 && /workspace/i.test(error.message)) {
343
+ outcome = await atris2TurnRequest({ ...payload, workspace_path: null });
344
+ } else {
345
+ throw error;
346
+ }
157
347
  }
158
348
 
349
+ if (!outcome.wroteText && outcome.finalResult) {
350
+ process.stdout.write(outcome.finalResult);
351
+ }
352
+ console.log('');
159
353
  console.log(`✅ Atris 2 ${atris2Mode} completed`);
160
- printAtris2Result(response);
161
354
  } catch (error) {
162
355
  console.error(`✗ Error: ${error.message}`);
163
356
  console.error(`Atris 2 ${atris2Mode} failed before completion.`);
@@ -868,6 +1061,26 @@ async function reviewAtris() {
868
1061
  const args = process.argv.slice(3);
869
1062
  const executeFlag = args.includes('--execute');
870
1063
  const showFull = args.includes('--full') || args.includes('--verbose');
1064
+ const wantsTaskJson = args.includes('--json');
1065
+
1066
+ if (!executeFlag && !showFull) {
1067
+ const forwarded = ['reviews', ...args.filter(arg => !['--execute', '--full', '--verbose'].includes(arg))];
1068
+ const { run: runTaskCommand } = require('./task');
1069
+ if (!wantsTaskJson) {
1070
+ printWorkflowBrief([
1071
+ 'Atris Review is the human checkpoint for proof-ready work.',
1072
+ 'Accept only when the proof is real; revise when the claim is vague, stale, or too narrow.',
1073
+ 'Agents can add review proof here, but XP waits for human accept.',
1074
+ ]);
1075
+ }
1076
+ await runTaskCommand(forwarded);
1077
+ if (!wantsTaskJson) {
1078
+ printWorkflowBrief([
1079
+ 'Need the legacy Validator prompt? Run `atris review --verbose`.',
1080
+ ]);
1081
+ }
1082
+ return;
1083
+ }
871
1084
 
872
1085
  const config = loadConfig();
873
1086
  const executionMode = executeFlag ? 'agent' : (config.execution_mode || 'prompt');
@@ -1423,5 +1636,7 @@ module.exports = {
1423
1636
  planAtris,
1424
1637
  doAtris,
1425
1638
  reviewAtris,
1426
- executeAgentSDKFast
1639
+ executeAgentSDKFast,
1640
+ makeCloudExecutor,
1641
+ postToolResult
1427
1642
  };
@@ -209,6 +209,40 @@ function printStatus() {
209
209
  }
210
210
  }
211
211
 
212
+ // Programmatic core shared by `atris worktree start` and `atris mission start
213
+ // --worktree`: creates the branch + isolated checkout + identity sidecar and
214
+ // returns the facts. Throws on failure; callers own messaging and next steps.
215
+ function createAgentWorktree({ root = repoRoot(), member = '', agent = '', task, branch: branchOverride, path: pathOverride, base: baseOverride, now = new Date() } = {}) {
216
+ const owner = member || agent;
217
+ if (!owner || !task) throw new Error('createAgentWorktree: owner (member/agent) and task required');
218
+ const branch = branchOverride || branchName(owner, task, now);
219
+ const target = path.resolve(pathOverride || defaultWorktreePath(root, owner, task, now));
220
+ const base = normalizeTargetRef(root, baseOverride || defaultStartBase(root));
221
+ if (fs.existsSync(target)) throw new Error(`worktree path already exists: ${target}`);
222
+ fs.mkdirSync(path.dirname(target), { recursive: true });
223
+ refreshRemoteRef(root, base);
224
+ runGit(['worktree', 'add', '-b', branch, target, base], { cwd: root });
225
+ runGit(['config', `branch.${branch}.atris-base`, base], { cwd: target, check: false });
226
+ runGit(['config', `branch.${branch}.atris-owner`, owner], { cwd: target, check: false });
227
+ runGit(['config', `branch.${branch}.atris-task`, task], { cwd: target, check: false });
228
+ fs.mkdirSync(path.join(target, '.atris'), { recursive: true });
229
+ fs.writeFileSync(
230
+ path.join(target, '.atris', 'agent-worktree.json'),
231
+ JSON.stringify({
232
+ agent: agent || null,
233
+ member: member || null,
234
+ owner,
235
+ task,
236
+ branch,
237
+ base,
238
+ workspace_root: root,
239
+ created_at: now.toISOString(),
240
+ }, null, 2) + '\n',
241
+ 'utf8'
242
+ );
243
+ return { path: target, branch, base, owner };
244
+ }
245
+
212
246
  function startWorktree(args) {
213
247
  const root = repoRoot();
214
248
  const member = readFlag(args, '--member');
@@ -219,25 +253,26 @@ function startWorktree(args) {
219
253
  console.error('Usage: atris worktree start --member <member>|--agent <name> --task "<short task>" [--claim]');
220
254
  return 2;
221
255
  }
222
- const now = new Date();
223
- const branch = readFlag(args, '--branch') || branchName(owner, task, now);
224
- const target = path.resolve(readFlag(args, '--path') || defaultWorktreePath(root, owner, task, now));
225
- const base = normalizeTargetRef(root, readFlag(args, '--base') || readFlag(args, '--target') || defaultStartBase(root));
226
256
  const memberFile = member ? path.join(root, 'atris', 'team', member, 'MEMBER.md') : '';
227
-
228
- if (fs.existsSync(target)) {
229
- console.error(`refusing: worktree path already exists: ${target}`);
230
- return 2;
231
- }
232
257
  if (memberFile && !fs.existsSync(memberFile)) {
233
258
  console.error(`warning: no member persona at ${path.relative(root, memberFile)}`);
234
259
  }
235
- fs.mkdirSync(path.dirname(target), { recursive: true });
236
- refreshRemoteRef(root, base);
237
- runGit(['worktree', 'add', '-b', branch, target, base], { cwd: root });
238
- runGit(['config', `branch.${branch}.atris-base`, base], { cwd: target, check: false });
239
- runGit(['config', `branch.${branch}.atris-owner`, owner], { cwd: target, check: false });
240
- runGit(['config', `branch.${branch}.atris-task`, task], { cwd: target, check: false });
260
+ let created;
261
+ try {
262
+ created = createAgentWorktree({
263
+ root,
264
+ member,
265
+ agent,
266
+ task,
267
+ branch: readFlag(args, '--branch'),
268
+ path: readFlag(args, '--path'),
269
+ base: readFlag(args, '--base') || readFlag(args, '--target'),
270
+ });
271
+ } catch (e) {
272
+ console.error(`refusing: ${e.message}`);
273
+ return 2;
274
+ }
275
+ const { path: target, branch, base } = created;
241
276
 
242
277
  const counts = statusCounts(root);
243
278
  if (counts && (counts.staged || counts.unstaged || counts.untracked)) {
@@ -474,9 +509,11 @@ function worktreeCommand(args = []) {
474
509
 
475
510
  module.exports = {
476
511
  branchName,
512
+ createAgentWorktree,
477
513
  defaultShipTarget,
478
514
  defaultStartBase,
479
515
  defaultWorktreePath,
516
+ listWorktrees,
480
517
  parseWorktrees,
481
518
  normalizeTargetRef,
482
519
  prMergeRef,