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.
- package/AGENTS.md +2 -2
- package/GETTING_STARTED.md +1 -1
- package/PERSONA.md +4 -4
- package/README.md +11 -11
- package/atris/skills/copy-editor/SKILL.md +30 -4
- package/atris/skills/improve/SKILL.md +18 -20
- package/atris/wiki/concepts/agent-activation-contract.md +5 -3
- package/atris/wiki/concepts/workspace-initialization-contract.md +4 -4
- package/atris/wiki/index.md +1 -0
- package/ax +522 -73
- package/bin/atris.js +32 -31
- package/commands/align.js +0 -14
- package/commands/apps.js +102 -1
- package/commands/autopilot.js +197 -22
- package/commands/brain.js +219 -34
- package/commands/brainstorm.js +0 -829
- package/commands/computer.js +45 -83
- package/commands/improve.js +501 -0
- package/commands/integrations.js +228 -0
- package/commands/lesson.js +44 -0
- package/commands/member.js +4498 -226
- package/commands/mission.js +302 -27
- package/commands/now.js +89 -1
- package/commands/radar.js +181 -56
- package/commands/skill.js +37 -6
- package/commands/soul.js +0 -4
- package/commands/task.js +5582 -517
- package/commands/terminal.js +14 -10
- package/commands/wiki.js +87 -1
- package/commands/workflow.js +288 -73
- package/commands/worktree.js +52 -15
- package/commands/xp.js +41 -65
- package/lib/auto-accept-certified.js +294 -0
- package/lib/file-ops.js +0 -184
- package/lib/member-alive.js +232 -0
- package/lib/policy-lessons.js +280 -0
- package/lib/receipt-evidence.js +64 -0
- package/lib/state-detection.js +34 -0
- package/lib/task-db.js +568 -16
- package/lib/task-proof.js +43 -0
- package/package.json +1 -1
- package/utils/auth.js +13 -4
- package/commands/research.js +0 -52
- package/lib/section-merge.js +0 -196
package/commands/terminal.js
CHANGED
|
@@ -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
|
|
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
|
};
|
package/commands/workflow.js
CHANGED
|
@@ -63,101 +63,294 @@ function confidenceGatePrompt(stage) {
|
|
|
63
63
|
].join('\n');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
wroteText =
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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
|
};
|
package/commands/worktree.js
CHANGED
|
@@ -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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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,
|