atris 3.34.0 → 3.36.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 +35 -0
- package/FOR_AGENTS.md +5 -3
- package/README.md +5 -3
- package/atris/GETTING_STARTED.md +1 -1
- package/atris/atris.md +3 -0
- package/atris/policies/day-loop-voice.md +102 -0
- package/atris/policies/outbound-artifact-gate.md +2 -0
- package/atris/skills/design/SKILL.md +56 -32
- package/atris/skills/endgame/SKILL.md +12 -6
- package/atris/skills/engines/SKILL.md +35 -8
- package/atris/skills/fable-method/SKILL.md +66 -0
- package/atris/skills/improve/SKILL.md +65 -45
- package/atris/skills/render-cli/SKILL.md +88 -0
- package/atris/skills/youtube/SKILL.md +10 -1
- package/atris.md +6 -2
- package/ax +617 -31
- package/bin/atris.js +719 -266
- package/commands/activate.js +194 -88
- package/commands/aeo.js +52 -0
- package/commands/agents.js +166 -0
- package/commands/autoland.js +718 -72
- package/commands/autopilot-front.js +20 -2
- package/commands/autopilot.js +118 -2
- package/commands/avail.js +407 -0
- package/commands/bench.js +188 -0
- package/commands/brain.js +3 -0
- package/commands/brief.js +651 -0
- package/commands/business-sync.js +192 -6
- package/commands/business.js +91 -8
- package/commands/clean.js +50 -24
- package/commands/close.js +1083 -0
- package/commands/cloud.js +245 -0
- package/commands/codex-goal.js +26 -2
- package/commands/compile.js +292 -1
- package/commands/computer.js +150 -3
- package/commands/dream.js +365 -0
- package/commands/drill.js +371 -0
- package/commands/drive.js +187 -0
- package/commands/engine.js +1061 -29
- package/commands/experiments.js +28 -0
- package/commands/feed.js +202 -0
- package/commands/feedback.js +34 -12
- package/commands/fleet-report.js +206 -0
- package/commands/github.js +38 -0
- package/commands/gm.js +285 -3
- package/commands/goal.js +247 -0
- package/commands/improve.js +642 -26
- package/commands/init.js +83 -43
- package/commands/integrations.js +39 -11
- package/commands/interview.js +209 -0
- package/commands/land.js +253 -52
- package/commands/lesson.js +112 -1
- package/commands/lifecycle.js +39 -3
- package/commands/linear.js +38 -0
- package/commands/log.js +84 -1
- package/commands/loops.js +220 -16
- package/commands/meet.js +220 -0
- package/commands/member.js +899 -66
- package/commands/mission.js +3512 -332
- package/commands/next.js +137 -0
- package/commands/now.js +240 -21
- package/commands/one-lap.js +776 -0
- package/commands/orb.js +314 -0
- package/commands/pack-craft.js +179 -0
- package/commands/pack.js +823 -0
- package/commands/play.js +3 -2
- package/commands/probe.js +30 -3
- package/commands/pulse.js +241 -46
- package/commands/push.js +260 -82
- package/commands/radar.js +259 -14
- package/commands/rainmaker.js +49 -0
- package/commands/report.js +415 -0
- package/commands/scout.js +147 -0
- package/commands/search.js +363 -0
- package/commands/serve.js +54 -0
- package/commands/skill.js +47 -3
- package/commands/slop.js +50 -2
- package/commands/soul.js +1 -1
- package/commands/status.js +50 -5
- package/commands/stream.js +861 -0
- package/commands/stripe.js +38 -0
- package/commands/study.js +693 -0
- package/commands/supabase.js +39 -0
- package/commands/sync.js +67 -54
- package/commands/task.js +2275 -182
- package/commands/team.js +73 -0
- package/commands/truth.js +29 -3
- package/commands/unknowns.js +627 -0
- package/commands/update.js +44 -0
- package/commands/vercel.js +38 -0
- package/commands/verify.js +96 -0
- package/commands/watch.js +303 -0
- package/commands/wish.js +500 -0
- package/commands/workflow.js +11 -5
- package/commands/worktree.js +299 -20
- package/commands/write.js +399 -0
- package/commands/xp.js +29 -11
- package/lib/auto-accept-certified.js +391 -43
- package/lib/autoland.js +353 -52
- package/lib/ax-auto-lane.js +79 -0
- package/lib/bench/context.js +147 -0
- package/lib/bench/engines.js +141 -0
- package/lib/bench/report.js +140 -0
- package/lib/bench/runner.js +512 -0
- package/lib/brief-ledger.js +350 -0
- package/lib/cloud-mission.js +259 -0
- package/lib/codex-flight.js +154 -0
- package/lib/default-runner.js +45 -0
- package/lib/default-verifier.js +70 -0
- package/lib/engine-registry.js +232 -0
- package/lib/experiments/daily.js +640 -0
- package/lib/fleet.js +2431 -38
- package/lib/improve-vitals-html.js +171 -0
- package/lib/known-commands.js +58 -0
- package/lib/loop-doctor.js +416 -0
- package/lib/member-switches.js +144 -0
- package/lib/memory-view.js +14 -5
- package/lib/mission-room.js +1 -0
- package/lib/mission-root.js +52 -0
- package/lib/mission-runtime-loop.js +7 -0
- package/lib/next-moves.js +327 -10
- package/lib/official-cli-integration.js +174 -0
- package/lib/one-lap-validator.js +60 -0
- package/lib/orb-context.js +477 -0
- package/lib/orb-scorecard.js +224 -0
- package/lib/outbound-send-gate.js +165 -0
- package/lib/permission-grants.js +293 -0
- package/lib/policy-lessons.js +52 -1
- package/lib/pulse.js +277 -3
- package/lib/receipt-block.js +168 -0
- package/lib/receipt-evidence.js +65 -4
- package/lib/review-integrity.js +147 -0
- package/lib/router-brain.js +352 -0
- package/lib/runner-command.js +33 -0
- package/lib/self-drive.js +258 -0
- package/lib/short-name.js +103 -0
- package/lib/spawn-env.js +18 -0
- package/lib/state-detection.js +56 -1
- package/lib/sync-status.js +59 -0
- package/lib/task-db.js +319 -27
- package/lib/task-proof.js +43 -1
- package/lib/task-receipt.js +93 -0
- package/lib/team-presence.js +260 -0
- package/lib/tool-result-encode.js +7 -0
- package/lib/trust-tiers.js +90 -0
- package/lib/usage.js +107 -0
- package/lib/voice-gate.js +163 -0
- package/lib/wish-audit.js +1368 -0
- package/lib/wish-delegate.js +1840 -0
- package/lib/wish-design.js +110 -0
- package/lib/wish-stats.js +183 -0
- package/lib/wish-store.js +354 -0
- package/lib/zip.js +221 -0
- package/package.json +3 -1
- package/templates/loops/atris/loops/LOOPS.md +55 -0
- package/templates/loops/atris/loops/TICK.md +24 -0
- package/templates/loops/atris/loops/feedback.md +22 -0
- package/templates/loops/atris/loops/quality.md +22 -0
- package/templates/loops/atris/wiki/systems/loops.md +41 -0
- package/utils/api.js +5 -1
- package/utils/auth.js +57 -21
- package/utils/update-check.js +27 -6
- package/atris/learnings.jsonl +0 -1
package/ax
CHANGED
|
@@ -8,8 +8,11 @@ const path = require('path');
|
|
|
8
8
|
const readline = require('readline');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const { Readable } = require('stream');
|
|
11
|
+
const permissionGrants = require(path.join(__dirname, 'lib', 'permission-grants.js'));
|
|
11
12
|
const { loadCredentials } = require('./utils/auth');
|
|
13
|
+
const { apiRequestJson, getApiBaseUrl } = require('./utils/api');
|
|
12
14
|
const missionRuntime = require('./lib/mission-runtime-loop');
|
|
15
|
+
const { pickLane } = require('./lib/ax-auto-lane');
|
|
13
16
|
|
|
14
17
|
const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
|
|
15
18
|
const BACKEND = {
|
|
@@ -29,7 +32,12 @@ const CONNECTION_CAPABILITIES_PATH = '/api/atris2/connection-capabilities';
|
|
|
29
32
|
const ATRIS2_CONNECTION_STATUS_PATH = '/api/atris2/connection-status';
|
|
30
33
|
const ATRIS2_HEALTH_PATH = '/api/atris2/health';
|
|
31
34
|
const APPROVAL_EXECUTE_PATH = '/api/atris2/approvals/execute';
|
|
35
|
+
const BACKEND_API_TOOL_NAME = 'backend_api';
|
|
36
|
+
const BACKEND_API_TOOL_RESULT_PATH = '/api/atris2/turn/tool-result';
|
|
32
37
|
const DEFAULT_APPROVAL_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
38
|
+
const DEFAULT_TURN_TIMEOUT_MS = 60000;
|
|
39
|
+
const PRO_TURN_TIMEOUT_MS = 180000;
|
|
40
|
+
const MAX_TURN_TIMEOUT_MS = 300000;
|
|
33
41
|
const CONNECTOR_NAMES = {
|
|
34
42
|
gmail: 'Gmail',
|
|
35
43
|
google_calendar: 'Google Calendar',
|
|
@@ -71,6 +79,18 @@ const ANSI = {
|
|
|
71
79
|
|
|
72
80
|
const TIER_COLORS = { fast: '\x1b[32m', pro: '\x1b[36m', max: '\x1b[35m' };
|
|
73
81
|
|
|
82
|
+
// Extend backend_api by adding one row here. Keep mutating endpoints narrow;
|
|
83
|
+
// any non-get method is approval-gated before the HTTP request is made.
|
|
84
|
+
const BACKEND_API_ALLOWLIST = [
|
|
85
|
+
{ method: 'GET', endpoint: '/api/atris2/health', description: 'Atris2 health' },
|
|
86
|
+
{ method: 'GET', endpoint: '/api/atris2/connection-status', description: 'connection status' },
|
|
87
|
+
{ method: 'GET', endpoint: '/api/atris2/connection-capabilities', description: 'connection capabilities' },
|
|
88
|
+
{ method: 'GET', endpoint: '/api/integrations/status', description: 'integration status' },
|
|
89
|
+
{ method: 'GET', endpoint: '/api/ai-computer/user/status', description: 'personal computer status' },
|
|
90
|
+
{ method: 'GET', endpoint: '/api/business', description: 'business list' },
|
|
91
|
+
{ method: 'POST', endpoint: '/api/improve', description: 'run one improve tick' },
|
|
92
|
+
];
|
|
93
|
+
|
|
74
94
|
function tierColor(mode) {
|
|
75
95
|
return TIER_COLORS[mode] || ANSI.accent;
|
|
76
96
|
}
|
|
@@ -81,6 +101,33 @@ function modelForMode(mode) {
|
|
|
81
101
|
return mode === 'fast' ? 'atris:fast' : 'atris:pro';
|
|
82
102
|
}
|
|
83
103
|
|
|
104
|
+
function payloadCanRunTools(payload = {}, options = {}) {
|
|
105
|
+
return Boolean(
|
|
106
|
+
options.business
|
|
107
|
+
|| options.local
|
|
108
|
+
|| payload.local_executor
|
|
109
|
+
|| (Array.isArray(payload.local_tools) && payload.local_tools.length > 0)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function postTurnTimeoutMs(payload = {}, options = {}) {
|
|
114
|
+
const model = String(payload.model || '');
|
|
115
|
+
let timeoutMs = DEFAULT_TURN_TIMEOUT_MS;
|
|
116
|
+
if (model === 'atris:max') timeoutMs = MAX_TURN_TIMEOUT_MS;
|
|
117
|
+
else if (model === 'atris:pro') timeoutMs = PRO_TURN_TIMEOUT_MS;
|
|
118
|
+
|
|
119
|
+
// AX postTurn is the tool-capable Atris2 path. Fast tool turns need the same
|
|
120
|
+
// headroom as pro because the backend can spend long stretches inside tools
|
|
121
|
+
// without SSE traffic. Plain tool-free one-shot fast chat stays at 60s.
|
|
122
|
+
if (model === 'atris:fast' && payloadCanRunTools(payload, options)) {
|
|
123
|
+
timeoutMs = Math.max(timeoutMs, PRO_TURN_TIMEOUT_MS);
|
|
124
|
+
}
|
|
125
|
+
if (options.business || options.local) {
|
|
126
|
+
timeoutMs = Math.max(timeoutMs, PRO_TURN_TIMEOUT_MS);
|
|
127
|
+
}
|
|
128
|
+
return timeoutMs;
|
|
129
|
+
}
|
|
130
|
+
|
|
84
131
|
function formatDuration(ms) {
|
|
85
132
|
const value = Number(ms) || 0;
|
|
86
133
|
if (value < 1000) return `${Math.max(0, Math.round(value))}ms`;
|
|
@@ -115,23 +162,30 @@ function formatUsage() {
|
|
|
115
162
|
'ax - Atris local/code agent',
|
|
116
163
|
'',
|
|
117
164
|
'Usage:',
|
|
118
|
-
' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] <message>',
|
|
119
|
-
' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] --
|
|
165
|
+
' ax [--auto|--max|--pro|--fast|--code-fast] [--local|--cloud] <message>',
|
|
166
|
+
' ax [--auto|--max|--pro|--fast|--code-fast] [--local|--cloud] --print <message>',
|
|
167
|
+
' ax [--auto|--max|--pro|--fast|--code-fast] [--local|--cloud] --chat',
|
|
120
168
|
' ax [--max|--pro|--fast] --business <slug> [<message>|--chat]',
|
|
121
169
|
' ax [--max|--pro|--fast|--code-fast] --doctor',
|
|
122
170
|
' ax --approvals',
|
|
123
171
|
' ax --approve <approval-id>',
|
|
124
172
|
' ax --deny <approval-id>',
|
|
173
|
+
' ax --grant <approval-id> approve and remember this exact command (this workspace)',
|
|
174
|
+
' ax --grants list active permission grants',
|
|
175
|
+
' ax --sync-grants push/pull grants with atrisos-web',
|
|
176
|
+
' ax --revoke-grant <grant-id>',
|
|
125
177
|
' ax --self-test',
|
|
126
178
|
' ax [--max|--fast] --benchmark',
|
|
127
179
|
'',
|
|
128
180
|
'Modes:',
|
|
181
|
+
' --auto pick a lane for each message and say why',
|
|
129
182
|
' --max hosted Atris 2, highest reasoning, slowest turns',
|
|
130
183
|
' --pro hosted Atris 2, deeper tool loop',
|
|
131
184
|
' --fast hosted Atris 2, faster low-latency turns',
|
|
132
185
|
' --code-fast Atris Code Fast public lane',
|
|
133
186
|
' --local opt into local backend/workspace tools',
|
|
134
187
|
' --cloud force authenticated cloud connectors/chat',
|
|
188
|
+
' --print headless JSON result, including auto_lane and auto_reason with --auto',
|
|
135
189
|
' --business <slug> run tools on that business cloud workspace (EC2)',
|
|
136
190
|
' --verify <cmd> gate the turn on this command passing (default: no verifier)',
|
|
137
191
|
'',
|
|
@@ -177,10 +231,29 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
|
|
|
177
231
|
].join('\n'));
|
|
178
232
|
|
|
179
233
|
let redactedChars = 0;
|
|
234
|
+
let logBuffer = '';
|
|
235
|
+
let logFlushTimer = null;
|
|
236
|
+
const flushLogBuffer = () => {
|
|
237
|
+
if (logFlushTimer) {
|
|
238
|
+
clearTimeout(logFlushTimer);
|
|
239
|
+
logFlushTimer = null;
|
|
240
|
+
}
|
|
241
|
+
if (!logBuffer) return;
|
|
242
|
+
fs.appendFileSync(logPath, logBuffer);
|
|
243
|
+
logBuffer = '';
|
|
244
|
+
};
|
|
245
|
+
const scheduleLogFlush = () => {
|
|
246
|
+
if (logFlushTimer) return;
|
|
247
|
+
logFlushTimer = setTimeout(() => {
|
|
248
|
+
logFlushTimer = null;
|
|
249
|
+
flushLogBuffer();
|
|
250
|
+
}, 500);
|
|
251
|
+
};
|
|
180
252
|
const writeLog = (chunk) => {
|
|
181
253
|
const text = stripAnsi(chunk);
|
|
182
254
|
if (fullTranscript) {
|
|
183
|
-
|
|
255
|
+
logBuffer += text;
|
|
256
|
+
scheduleLogFlush();
|
|
184
257
|
return;
|
|
185
258
|
}
|
|
186
259
|
redactedChars += text.length;
|
|
@@ -200,6 +273,7 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
|
|
|
200
273
|
output: teeOutput,
|
|
201
274
|
write: writeLog,
|
|
202
275
|
close(exitCode = 0) {
|
|
276
|
+
flushLogBuffer();
|
|
203
277
|
if (!fullTranscript) {
|
|
204
278
|
fs.appendFileSync(logPath, `redacted_chars: ${redactedChars}\n`);
|
|
205
279
|
fs.appendFileSync(logPath, 'note: full transcript disabled by default; set AX_LOG_FULL=1 to opt in.\n');
|
|
@@ -207,6 +281,7 @@ function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', out
|
|
|
207
281
|
return;
|
|
208
282
|
}
|
|
209
283
|
writeLog(`\nexit_code: ${exitCode}\nfinished_at: ${new Date().toISOString()}\n`);
|
|
284
|
+
flushLogBuffer();
|
|
210
285
|
}
|
|
211
286
|
};
|
|
212
287
|
}
|
|
@@ -495,6 +570,201 @@ function postJson(pathname, body, { token = authToken(), timeoutMs = 30000, rout
|
|
|
495
570
|
});
|
|
496
571
|
}
|
|
497
572
|
|
|
573
|
+
function compactSingleLine(value) {
|
|
574
|
+
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function truncateSingleLine(value, limit = 120) {
|
|
578
|
+
const text = compactSingleLine(value);
|
|
579
|
+
if (text.length <= limit) return text;
|
|
580
|
+
if (limit <= 3) return text.slice(0, limit);
|
|
581
|
+
return `${text.slice(0, limit - 3)}...`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function canonicalBackendApiPath(pathname) {
|
|
585
|
+
let value = String(pathname || '').trim();
|
|
586
|
+
if (!value) return '';
|
|
587
|
+
if (!value.startsWith('/')) value = `/${value}`;
|
|
588
|
+
value = value.replace(/\/{2,}/g, '/');
|
|
589
|
+
if (value !== '/' && value.endsWith('/')) value = value.replace(/\/+$/, '');
|
|
590
|
+
return value;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function normalizeBackendApiMethod(method) {
|
|
594
|
+
return String(method || 'GET').trim().toUpperCase() || 'GET';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function normalizeBackendApiEndpoint(endpoint) {
|
|
598
|
+
const raw = String(endpoint || '').trim();
|
|
599
|
+
if (!raw || /^[a-z][a-z0-9+.-]*:\/\//i.test(raw)) return null;
|
|
600
|
+
const parsed = new URL(raw.startsWith('/') ? raw : `/${raw}`, 'https://ax.local');
|
|
601
|
+
const pathOnly = canonicalBackendApiPath(parsed.pathname);
|
|
602
|
+
const displayPath = pathOnly === '/api' || pathOnly.startsWith('/api/')
|
|
603
|
+
? pathOnly
|
|
604
|
+
: canonicalBackendApiPath(`/api${pathOnly}`);
|
|
605
|
+
return {
|
|
606
|
+
endpoint: `${displayPath}${parsed.search}`,
|
|
607
|
+
pathname: displayPath,
|
|
608
|
+
search: parsed.search,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function backendApiAllowlistEntry(method, endpointPathname) {
|
|
613
|
+
const normalizedMethod = normalizeBackendApiMethod(method);
|
|
614
|
+
const normalizedPath = canonicalBackendApiPath(endpointPathname);
|
|
615
|
+
return BACKEND_API_ALLOWLIST.find(row =>
|
|
616
|
+
row.method === normalizedMethod && canonicalBackendApiPath(row.endpoint) === normalizedPath
|
|
617
|
+
) || null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function apiRequestPathForBackendEndpoint(displayEndpoint, baseUrl = getApiBaseUrl()) {
|
|
621
|
+
const parsed = new URL(displayEndpoint, 'https://ax.local');
|
|
622
|
+
let pathname = canonicalBackendApiPath(parsed.pathname);
|
|
623
|
+
const base = String(baseUrl || '').replace(/\/+$/, '');
|
|
624
|
+
if (base.endsWith('/api') && pathname.startsWith('/api/')) {
|
|
625
|
+
pathname = pathname.slice('/api'.length) || '/';
|
|
626
|
+
}
|
|
627
|
+
return `${pathname}${parsed.search}`;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function summarizeBackendApiPayload(payload) {
|
|
631
|
+
if (payload === undefined || payload === null) return 'none';
|
|
632
|
+
if (typeof payload === 'string') return truncateSingleLine(payload, 120);
|
|
633
|
+
try {
|
|
634
|
+
return truncateSingleLine(JSON.stringify(payload), 120);
|
|
635
|
+
} catch (_) {
|
|
636
|
+
return truncateSingleLine(String(payload), 120);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function summarizeBackendApiResponse(response) {
|
|
641
|
+
if (!response) return '';
|
|
642
|
+
if (response.data !== undefined && response.data !== null) {
|
|
643
|
+
try {
|
|
644
|
+
return truncateSingleLine(JSON.stringify(response.data), 120);
|
|
645
|
+
} catch (_) {
|
|
646
|
+
return truncateSingleLine(String(response.data), 120);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return truncateSingleLine(response.text || response.error || '', 120);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function formatBackendApiReceipt(call) {
|
|
653
|
+
return `backend_api ${String(call.method || 'get').toLowerCase()} ${call.endpoint} status ${call.status}: ${truncateSingleLine(call.summary, 120)}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatBackendApiApprovalPrompt(call) {
|
|
657
|
+
return `approve backend_api ${String(call.method || 'post').toLowerCase()} ${call.endpoint} payload ${summarizeBackendApiPayload(call.payload)}? yes/no: `;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function messageApprovesBackendApi(value) {
|
|
661
|
+
const normalized = String(value || '').trim().toLowerCase().replace(/[.!?]+$/g, '');
|
|
662
|
+
return normalized === 'yes' || normalized === 'y';
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
async function requestBackendApiApproval(call, options = {}) {
|
|
666
|
+
const prompt = formatBackendApiApprovalPrompt(call);
|
|
667
|
+
if (typeof options.approveBackendApi === 'function') {
|
|
668
|
+
if (options.output && typeof options.output.write === 'function') {
|
|
669
|
+
options.output.write(`${prompt}\n`);
|
|
670
|
+
}
|
|
671
|
+
const decision = await options.approveBackendApi({ ...call, prompt });
|
|
672
|
+
return decision === true || messageApprovesBackendApi(decision);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
const input = options.input || process.stdin;
|
|
676
|
+
const output = options.output || process.stdout;
|
|
677
|
+
if (!input || !input.isTTY) return false;
|
|
678
|
+
return new Promise((resolve) => {
|
|
679
|
+
const rl = readline.createInterface({ input, output });
|
|
680
|
+
rl.question(prompt, (answer) => {
|
|
681
|
+
rl.close();
|
|
682
|
+
resolve(messageApprovesBackendApi(answer));
|
|
683
|
+
});
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function backendApiToolDescriptor() {
|
|
688
|
+
return {
|
|
689
|
+
name: BACKEND_API_TOOL_NAME,
|
|
690
|
+
description: 'Call allowlisted AtrisOS backend API endpoints through ax. get calls run directly; non-get calls require explicit user approval.',
|
|
691
|
+
input_schema: {
|
|
692
|
+
type: 'object',
|
|
693
|
+
properties: {
|
|
694
|
+
method: { type: 'string', description: 'HTTP method, for example get or post' },
|
|
695
|
+
endpoint: { type: 'string', description: 'Allowlisted endpoint such as /api/atris2/health or /api/improve' },
|
|
696
|
+
payload: { type: 'object', description: 'JSON payload for non-get calls' },
|
|
697
|
+
},
|
|
698
|
+
required: ['endpoint'],
|
|
699
|
+
},
|
|
700
|
+
allowlist: BACKEND_API_ALLOWLIST.map(row => ({ method: row.method, endpoint: row.endpoint, description: row.description })),
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async function executeBackendApiTool(args = {}, options = {}) {
|
|
705
|
+
const method = normalizeBackendApiMethod(args.method || args.verb);
|
|
706
|
+
const normalizedEndpoint = normalizeBackendApiEndpoint(args.endpoint || args.path || args.url);
|
|
707
|
+
if (!normalizedEndpoint) {
|
|
708
|
+
return { status: 'error', error: 'backend_api refused: missing or invalid endpoint' };
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const allowlisted = backendApiAllowlistEntry(method, normalizedEndpoint.pathname);
|
|
712
|
+
if (!allowlisted) {
|
|
713
|
+
return {
|
|
714
|
+
status: 'error',
|
|
715
|
+
error: `backend_api refused: ${method.toLowerCase()} ${normalizedEndpoint.endpoint} is not allowlisted`,
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const payload = args.payload !== undefined ? args.payload : args.body;
|
|
720
|
+
const call = { method, endpoint: normalizedEndpoint.endpoint, payload };
|
|
721
|
+
if (method !== 'GET') {
|
|
722
|
+
const approved = await requestBackendApiApproval(call, options);
|
|
723
|
+
if (!approved) {
|
|
724
|
+
return {
|
|
725
|
+
status: 'error',
|
|
726
|
+
error: `backend_api refused: ${method.toLowerCase()} ${normalizedEndpoint.endpoint} was not approved`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const loadCreds = options.loadCredentials || loadCredentials;
|
|
732
|
+
const credentials = loadCreds();
|
|
733
|
+
const token = credentials && credentials.token ? credentials.token : '';
|
|
734
|
+
if (!token) {
|
|
735
|
+
return { status: 'error', error: 'backend_api refused: not logged in' };
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const apiFn = options.apiRequestJson || apiRequestJson;
|
|
739
|
+
const baseFn = options.getApiBaseUrl || getApiBaseUrl;
|
|
740
|
+
const requestPath = apiRequestPathForBackendEndpoint(normalizedEndpoint.endpoint, baseFn());
|
|
741
|
+
const response = await apiFn(requestPath, {
|
|
742
|
+
method,
|
|
743
|
+
token,
|
|
744
|
+
...(method === 'GET' ? {} : { body: payload === undefined ? {} : payload }),
|
|
745
|
+
timeoutMs: options.timeoutMs || 30000,
|
|
746
|
+
});
|
|
747
|
+
const summary = summarizeBackendApiResponse(response);
|
|
748
|
+
const receipt = formatBackendApiReceipt({
|
|
749
|
+
method,
|
|
750
|
+
endpoint: normalizedEndpoint.endpoint,
|
|
751
|
+
status: response && typeof response.status === 'number' ? response.status : 0,
|
|
752
|
+
summary,
|
|
753
|
+
});
|
|
754
|
+
return {
|
|
755
|
+
status: response && response.ok ? 'ok' : 'error',
|
|
756
|
+
method: method.toLowerCase(),
|
|
757
|
+
endpoint: normalizedEndpoint.endpoint,
|
|
758
|
+
status_code: response && typeof response.status === 'number' ? response.status : 0,
|
|
759
|
+
summary,
|
|
760
|
+
receipt,
|
|
761
|
+
response: response && response.data !== undefined && response.data !== null
|
|
762
|
+
? response.data
|
|
763
|
+
: truncateSingleLine(response && response.text, 4000),
|
|
764
|
+
...(response && response.ok ? {} : { error: (response && response.error) || summary || 'request failed' }),
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
|
|
498
768
|
function defaultAuthority(id) {
|
|
499
769
|
const key = canonicalConnectorId(id);
|
|
500
770
|
if (key === 'gmail') return { list_messages: 'read_only', get_message: 'read_only', send_message: 'approval_required' };
|
|
@@ -687,10 +957,38 @@ function normalizeMode(mode) {
|
|
|
687
957
|
return mode === 'fast' ? 'fast' : 'pro';
|
|
688
958
|
}
|
|
689
959
|
|
|
960
|
+
function appendAutoPick(message, picked, options = {}) {
|
|
961
|
+
try {
|
|
962
|
+
const dir = path.join(os.homedir(), '.atris');
|
|
963
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
964
|
+
fs.appendFileSync(path.join(dir, 'ax-auto-picks.jsonl'), `${JSON.stringify({
|
|
965
|
+
at: new Date().toISOString(),
|
|
966
|
+
lane: picked.lane,
|
|
967
|
+
reason: picked.reason,
|
|
968
|
+
message_chars: String(message || '').length,
|
|
969
|
+
print: Boolean(options.print),
|
|
970
|
+
})}\n`);
|
|
971
|
+
} catch (_) {
|
|
972
|
+
// Lane telemetry is best-effort and must never block a turn.
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function autoLaneForMessage(message, options = {}) {
|
|
977
|
+
const picked = pickLane(message);
|
|
978
|
+
appendAutoPick(message, picked, options);
|
|
979
|
+
const errorOutput = options.errorOutput || process.stderr;
|
|
980
|
+
errorOutput.write(`${picked.reason}\n`);
|
|
981
|
+
return picked;
|
|
982
|
+
}
|
|
983
|
+
|
|
690
984
|
function formatPrompt(mode, options = {}) {
|
|
691
985
|
if (!mode) return '› ';
|
|
692
986
|
const tier = normalizeMode(mode);
|
|
693
|
-
|
|
987
|
+
const label = paint(tier, [ANSI.bold, tierColor(tier)], options);
|
|
988
|
+
if (options.approveMode === 'auto') {
|
|
989
|
+
return `${label} ${paint('[auto-approve]', [ANSI.muted], options)} › `;
|
|
990
|
+
}
|
|
991
|
+
return `${label} › `;
|
|
694
992
|
}
|
|
695
993
|
|
|
696
994
|
const TIER_COMMANDS = new Map([
|
|
@@ -707,6 +1005,8 @@ const CHAT_COMMANDS = [
|
|
|
707
1005
|
['/fast', 'quick answers, lowest latency'],
|
|
708
1006
|
['/pro', 'deeper tool loop for real work'],
|
|
709
1007
|
['/max', 'highest reasoning for the hardest jobs'],
|
|
1008
|
+
['/clear', 'wipe chat history and reset conversation id'],
|
|
1009
|
+
['/context', 'show turn count and rough token estimate'],
|
|
710
1010
|
['/help', 'show this menu'],
|
|
711
1011
|
['exit', 'leave chat'],
|
|
712
1012
|
];
|
|
@@ -867,6 +1167,8 @@ function formatGoalStatusLine(goal, options = {}) {
|
|
|
867
1167
|
const status = String(goal.mission_status || '').toLowerCase();
|
|
868
1168
|
if (!['active', 'running', 'planning', 'in_progress', 'ready'].includes(status)) return '';
|
|
869
1169
|
const elapsed = goalElapsedSeconds(goal);
|
|
1170
|
+
// A goal "running" for >24h is stale state, not a live goal — hide it.
|
|
1171
|
+
if (elapsed !== null && elapsed > 86400) return '';
|
|
870
1172
|
const objective = String(goal.objective).length > 64
|
|
871
1173
|
? `${String(goal.objective).slice(0, 61)}...`
|
|
872
1174
|
: String(goal.objective);
|
|
@@ -1768,12 +2070,58 @@ async function approveWorkspaceApproval(ref, options = {}) {
|
|
|
1768
2070
|
return { response, executed };
|
|
1769
2071
|
}
|
|
1770
2072
|
|
|
2073
|
+
// A persistent grant (ax --grant) lets an exact approved command pattern
|
|
2074
|
+
// redeem itself on later turns instead of re-asking; anything unmatched
|
|
2075
|
+
// falls through to the normal hint. Store: ~/.atris/permission-grants.json.
|
|
2076
|
+
async function autoRedeemGrantedApprovals(cwd, sinceMs, options = {}) {
|
|
2077
|
+
const redeemed = [];
|
|
2078
|
+
for (const item of pendingWorkspaceApprovalsSince(cwd, sinceMs)) {
|
|
2079
|
+
const record = item.record;
|
|
2080
|
+
if (String(record.action_type || '') !== 'local_command') continue;
|
|
2081
|
+
const command = record.payload && record.payload.command;
|
|
2082
|
+
const grant = permissionGrants.matchGrant({ command, workspaceRoot: cwd });
|
|
2083
|
+
if (!grant) continue;
|
|
2084
|
+
const output = options.output || process.stdout;
|
|
2085
|
+
output.write(`${formatAuxRow('grant', `auto-approved via ${grant.grant_id}: ${grant.pattern.display}`, output)}\n`);
|
|
2086
|
+
try {
|
|
2087
|
+
await approveWorkspaceApproval(record.approval_id, { ...options, cwd, output });
|
|
2088
|
+
permissionGrants.recordUse(grant.grant_id);
|
|
2089
|
+
redeemed.push(record.approval_id);
|
|
2090
|
+
} catch {
|
|
2091
|
+
// Redemption failed: leave the artifact pending so the hint still shows.
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
return redeemed;
|
|
2095
|
+
}
|
|
2096
|
+
|
|
1771
2097
|
function writeWorkspaceApprovalHints(cwd, sinceMs, output) {
|
|
1772
2098
|
for (const item of pendingWorkspaceApprovalsSince(cwd, sinceMs)) {
|
|
1773
2099
|
output.write(`${formatAuxRow('approve', formatApprovalCommand(item.record.approval_id), output)}\n`);
|
|
1774
2100
|
}
|
|
1775
2101
|
}
|
|
1776
2102
|
|
|
2103
|
+
function deferWriteWorkspaceApprovalHints(cwd, sinceMs, output) {
|
|
2104
|
+
setImmediate(() => {
|
|
2105
|
+
Promise.resolve()
|
|
2106
|
+
.then(() => writeWorkspaceApprovalHints(cwd, sinceMs, output))
|
|
2107
|
+
.catch(() => {});
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
function deferPersistPendingApproval(receipt, approvalRequest, options = {}) {
|
|
2112
|
+
setImmediate(() => {
|
|
2113
|
+
Promise.resolve()
|
|
2114
|
+
.then(() => persistPendingApproval(receipt, approvalRequest, options))
|
|
2115
|
+
.catch(() => {});
|
|
2116
|
+
});
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function estimateHistoryContext(history = []) {
|
|
2120
|
+
const turns = history.filter(row => row && row.role === 'user').length;
|
|
2121
|
+
const chars = history.reduce((total, row) => total + String((row && row.content) || '').length, 0);
|
|
2122
|
+
return { turns, chars, tokens: Math.ceil(chars / 4) };
|
|
2123
|
+
}
|
|
2124
|
+
|
|
1777
2125
|
function formatStoredApprovals(store, options = {}) {
|
|
1778
2126
|
const approvals = Array.isArray(store && store.approvals) ? store.approvals : [];
|
|
1779
2127
|
if (!approvals.length) return 'No pending approvals.';
|
|
@@ -2187,8 +2535,9 @@ function buildPayload(message, options = {}) {
|
|
|
2187
2535
|
const payload = {
|
|
2188
2536
|
message: buildMessage(message, options.history || []),
|
|
2189
2537
|
model: modelForMode(mode),
|
|
2190
|
-
max_turns: local ? (mode === 'fast' ?
|
|
2191
|
-
verify_command: verifyCommand || 'true'
|
|
2538
|
+
max_turns: local ? (mode === 'fast' ? 16 : 24) : 1,
|
|
2539
|
+
verify_command: verifyCommand || 'true',
|
|
2540
|
+
local_tools: [backendApiToolDescriptor()]
|
|
2192
2541
|
};
|
|
2193
2542
|
if (previousMessages.length) {
|
|
2194
2543
|
payload.previous_messages = previousMessages;
|
|
@@ -2317,7 +2666,15 @@ function handleEvent(event, state, output) {
|
|
|
2317
2666
|
state.relay.chain = state.relay.chain
|
|
2318
2667
|
.then(() => state.relay.execute(event.name, args))
|
|
2319
2668
|
.catch((err) => ({ status: 'error', error: String(err.message || err).slice(0, 500) }))
|
|
2320
|
-
.then((result) =>
|
|
2669
|
+
.then((result) => {
|
|
2670
|
+
if (result && result.receipt) {
|
|
2671
|
+
flushPendingText(state, output);
|
|
2672
|
+
writeAuxLine(state, output, formatAuxRow('receipt', result.receipt, output));
|
|
2673
|
+
state.lastAux = 'receipt';
|
|
2674
|
+
}
|
|
2675
|
+
return result;
|
|
2676
|
+
})
|
|
2677
|
+
.then((result) => state.relay.post(event.call_id || event.id, result))
|
|
2321
2678
|
.catch((err) => state.errors.push(`tool relay failed: ${err.message}`));
|
|
2322
2679
|
}
|
|
2323
2680
|
return;
|
|
@@ -2351,11 +2708,22 @@ function handleEvent(event, state, output) {
|
|
|
2351
2708
|
return;
|
|
2352
2709
|
}
|
|
2353
2710
|
|
|
2354
|
-
if (event.type === 'result' && event.result
|
|
2711
|
+
if (event.type === 'result' && event.result) {
|
|
2355
2712
|
const content = String(event.result || '');
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2713
|
+
// `result` is the backend's authoritative final answer for the turn.
|
|
2714
|
+
// Local tool-loop turns stream throwaway narration via text_delta before
|
|
2715
|
+
// the model actually does the work ("Acknowledged, let me check that...")
|
|
2716
|
+
// — remember the true final text separately so callers (runHeadlessTurn's
|
|
2717
|
+
// JSON `output`, and chat()'s conversation history) get the real answer
|
|
2718
|
+
// instead of whichever fragment happened to stream first. Terminal
|
|
2719
|
+
// rendering keeps its existing de-dupe: only print `result` live when
|
|
2720
|
+
// nothing was already streamed (avoids double-printing a duplicate).
|
|
2721
|
+
state.resultOutput = content;
|
|
2722
|
+
if (!state.output) {
|
|
2723
|
+
state.output += content;
|
|
2724
|
+
if (output && output.isTTY) writeStreamingText(state, output, content);
|
|
2725
|
+
else state.pendingText += content;
|
|
2726
|
+
}
|
|
2359
2727
|
return;
|
|
2360
2728
|
}
|
|
2361
2729
|
|
|
@@ -2385,14 +2753,7 @@ async function postTurn(message, options = {}) {
|
|
|
2385
2753
|
const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId, turnId });
|
|
2386
2754
|
const postData = JSON.stringify(payload);
|
|
2387
2755
|
const output = options.output || process.stdout;
|
|
2388
|
-
|
|
2389
|
-
// SSE traffic in between, so the socket-idle timeout needs more headroom.
|
|
2390
|
-
const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
|
|
2391
|
-
let timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
|
|
2392
|
-
// Local workspace tool loops (max_turns 8/14) legitimately run past the fast
|
|
2393
|
-
// lane's 60s chat wall — SwapBench 2026-07-02: three tool-loop tasks died at
|
|
2394
|
-
// ~60s as "Atris cloud did not respond". Same headroom as business relays.
|
|
2395
|
-
if (local) timeoutMs = Math.max(timeoutMs, 180000);
|
|
2756
|
+
const timeoutMs = postTurnTimeoutMs(payload, { business: options.business, local });
|
|
2396
2757
|
const turnUrl = new URL(backendUrl({ route: endpointRoute }));
|
|
2397
2758
|
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
2398
2759
|
const state = {
|
|
@@ -2411,11 +2772,34 @@ async function postTurn(message, options = {}) {
|
|
|
2411
2772
|
markdownMode: 'normal',
|
|
2412
2773
|
markdownBuffer: '',
|
|
2413
2774
|
markdownCarry: '',
|
|
2414
|
-
relay:
|
|
2775
|
+
relay: {
|
|
2415
2776
|
chain: Promise.resolve(),
|
|
2416
|
-
execute:
|
|
2417
|
-
|
|
2418
|
-
|
|
2777
|
+
execute: async (name, args) => {
|
|
2778
|
+
if (name === BACKEND_API_TOOL_NAME) {
|
|
2779
|
+
return executeBackendApiTool(args, {
|
|
2780
|
+
input: options.input,
|
|
2781
|
+
output,
|
|
2782
|
+
approveBackendApi: options.approveBackendApi,
|
|
2783
|
+
apiRequestJson: options.apiRequestJson,
|
|
2784
|
+
loadCredentials: options.loadCredentials,
|
|
2785
|
+
getApiBaseUrl: options.getApiBaseUrl,
|
|
2786
|
+
timeoutMs: options.backendApiTimeoutMs,
|
|
2787
|
+
});
|
|
2788
|
+
}
|
|
2789
|
+
if (options.business) return options.business.executor(name, args);
|
|
2790
|
+
return { status: 'error', error: `unsupported relayed tool: ${name || '?'}` };
|
|
2791
|
+
},
|
|
2792
|
+
post: (callId, result) => {
|
|
2793
|
+
if (options.business) {
|
|
2794
|
+
return options.business.postToolResult(callId, result, backendBaseUrl({ route: 'cloud' }));
|
|
2795
|
+
}
|
|
2796
|
+
return postJson(BACKEND_API_TOOL_RESULT_PATH, { call_id: callId, result }, {
|
|
2797
|
+
token,
|
|
2798
|
+
route: endpointRoute,
|
|
2799
|
+
timeoutMs: 30000,
|
|
2800
|
+
});
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2419
2803
|
};
|
|
2420
2804
|
|
|
2421
2805
|
return new Promise((resolve, reject) => {
|
|
@@ -2495,6 +2879,11 @@ async function postTurn(message, options = {}) {
|
|
|
2495
2879
|
}
|
|
2496
2880
|
|
|
2497
2881
|
flushPendingText(state, output);
|
|
2882
|
+
// Terminal display already streamed/de-duped above; the RETURNED
|
|
2883
|
+
// output must be the backend's authoritative final answer, not
|
|
2884
|
+
// whichever text_delta fragment happened to arrive first (see the
|
|
2885
|
+
// `result` branch in handleEvent).
|
|
2886
|
+
if (state.resultOutput) state.output = state.resultOutput;
|
|
2498
2887
|
finish(null, state);
|
|
2499
2888
|
});
|
|
2500
2889
|
});
|
|
@@ -2622,6 +3011,44 @@ function turnFunctionForMode(mode) {
|
|
|
2622
3011
|
return normalizeMode(mode) === 'code-fast' ? postCodeFastTurn : postTurn;
|
|
2623
3012
|
}
|
|
2624
3013
|
|
|
3014
|
+
async function runHeadlessTurn(message, options = {}) {
|
|
3015
|
+
const mode = normalizeMode(options.mode || 'fast');
|
|
3016
|
+
const startedAt = Date.now();
|
|
3017
|
+
const sink = options.output || bufferedOutput();
|
|
3018
|
+
const turnFunction = options.turnFunction || turnFunctionForMode(mode);
|
|
3019
|
+
const autoFields = options.autoLane && options.autoReason
|
|
3020
|
+
? { auto_lane: normalizeMode(options.autoLane), auto_reason: String(options.autoReason) }
|
|
3021
|
+
: {};
|
|
3022
|
+
|
|
3023
|
+
try {
|
|
3024
|
+
const result = await turnFunction(message, {
|
|
3025
|
+
mode,
|
|
3026
|
+
cwd: options.cwd || process.cwd(),
|
|
3027
|
+
route: options.route,
|
|
3028
|
+
business: options.business,
|
|
3029
|
+
verify: options.verify,
|
|
3030
|
+
output: sink,
|
|
3031
|
+
showProgress: false,
|
|
3032
|
+
});
|
|
3033
|
+
return {
|
|
3034
|
+
ok: true,
|
|
3035
|
+
model: modelForMode(mode),
|
|
3036
|
+
output: String((result && result.output) || '').trim(),
|
|
3037
|
+
durationMs: Number((result && result.durationMs) || 0) || (Date.now() - startedAt),
|
|
3038
|
+
...autoFields,
|
|
3039
|
+
};
|
|
3040
|
+
} catch (error) {
|
|
3041
|
+
return {
|
|
3042
|
+
ok: false,
|
|
3043
|
+
model: modelForMode(mode),
|
|
3044
|
+
output: '',
|
|
3045
|
+
durationMs: Date.now() - startedAt,
|
|
3046
|
+
error: String((error && error.message) || error || 'headless turn failed'),
|
|
3047
|
+
...autoFields,
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
|
|
2625
3052
|
async function chat(options = {}) {
|
|
2626
3053
|
let mode = normalizeMode(options.mode);
|
|
2627
3054
|
const cwd = options.cwd || process.cwd();
|
|
@@ -2631,7 +3058,9 @@ async function chat(options = {}) {
|
|
|
2631
3058
|
const output = logger ? logger.output : baseOutput;
|
|
2632
3059
|
const history = [];
|
|
2633
3060
|
let lastCompactionNotice = 0;
|
|
2634
|
-
|
|
3061
|
+
let conversationId = options.conversationId || `ax-${process.pid}-${Date.now().toString(36)}`;
|
|
3062
|
+
const session = { approveMode: 'stage' };
|
|
3063
|
+
let turnInFlight = false;
|
|
2635
3064
|
|
|
2636
3065
|
output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
|
|
2637
3066
|
if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
|
|
@@ -2678,11 +3107,31 @@ async function chat(options = {}) {
|
|
|
2678
3107
|
return false;
|
|
2679
3108
|
}
|
|
2680
3109
|
|
|
3110
|
+
if (trimmed === '/clear') {
|
|
3111
|
+
history.length = 0;
|
|
3112
|
+
conversationId = `ax-${process.pid}-${Date.now().toString(36)}`;
|
|
3113
|
+
output.write(`${paint('context cleared', [ANSI.muted], output)}\n\n`);
|
|
3114
|
+
return false;
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
if (trimmed === '/context') {
|
|
3118
|
+
const ctx = estimateHistoryContext(history);
|
|
3119
|
+
output.write(`${paint(`· ${ctx.turns} turn${ctx.turns === 1 ? '' : 's'} · ~${ctx.chars} chars · ~${ctx.tokens} tokens`, [ANSI.muted], output)}\n\n`);
|
|
3120
|
+
return false;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
2681
3123
|
if (trimmed.startsWith('/')) {
|
|
2682
3124
|
output.write(`${chatMenu(output)}\n\n`);
|
|
2683
3125
|
return false;
|
|
2684
3126
|
}
|
|
2685
3127
|
|
|
3128
|
+
if (options.auto) {
|
|
3129
|
+
mode = autoLaneForMessage(trimmed, {
|
|
3130
|
+
errorOutput: options.errorOutput,
|
|
3131
|
+
print: false,
|
|
3132
|
+
}).lane;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
2686
3135
|
if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
|
|
2687
3136
|
output.write('\n');
|
|
2688
3137
|
|
|
@@ -2781,7 +3230,22 @@ async function chat(options = {}) {
|
|
|
2781
3230
|
lastCompactionNotice = compactedTurns;
|
|
2782
3231
|
output.write(`${paint(`· context: ${compactedTurns} earlier turn${compactedTurns === 1 ? '' : 's'} compacted into a digest`, [ANSI.muted], output)}\n`);
|
|
2783
3232
|
}
|
|
2784
|
-
const result = await turnFunction(trimmed, {
|
|
3233
|
+
const result = await turnFunction(trimmed, {
|
|
3234
|
+
mode,
|
|
3235
|
+
cwd,
|
|
3236
|
+
history,
|
|
3237
|
+
input,
|
|
3238
|
+
output,
|
|
3239
|
+
route,
|
|
3240
|
+
business: options.business,
|
|
3241
|
+
verify: options.verify,
|
|
3242
|
+
conversationId,
|
|
3243
|
+
turnId,
|
|
3244
|
+
approveBackendApi: options.approveBackendApi,
|
|
3245
|
+
apiRequestJson: options.apiRequestJson,
|
|
3246
|
+
loadCredentials: options.loadCredentials,
|
|
3247
|
+
getApiBaseUrl: options.getApiBaseUrl,
|
|
3248
|
+
});
|
|
2785
3249
|
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2786
3250
|
const approvalExecution = normalizeMode(mode) === 'code-fast'
|
|
2787
3251
|
? null
|
|
@@ -3595,11 +4059,22 @@ async function main() {
|
|
|
3595
4059
|
args.splice(verifyIdx, 2);
|
|
3596
4060
|
}
|
|
3597
4061
|
|
|
3598
|
-
const
|
|
4062
|
+
const explicitLane = args.includes('--code-fast') || args.includes('--code')
|
|
4063
|
+
? 'code-fast'
|
|
4064
|
+
: args.includes('--max')
|
|
4065
|
+
? 'max'
|
|
4066
|
+
: args.includes('--fast')
|
|
4067
|
+
? 'fast'
|
|
4068
|
+
: args.includes('--pro')
|
|
4069
|
+
? 'pro'
|
|
4070
|
+
: null;
|
|
4071
|
+
const autoEnabled = args.includes('--auto') && !explicitLane;
|
|
4072
|
+
let mode = explicitLane || 'pro';
|
|
3599
4073
|
const doctor = args.includes('--doctor');
|
|
3600
4074
|
const selfTest = args.includes('--self-test');
|
|
3601
4075
|
const benchmark = args.includes('--benchmark');
|
|
3602
4076
|
const approvals = args.includes('--approvals');
|
|
4077
|
+
const printMode = args.includes('--print') || args.includes('--headless');
|
|
3603
4078
|
const forceCloud = args.includes('--cloud');
|
|
3604
4079
|
const forceLocal = args.includes('--local');
|
|
3605
4080
|
|
|
@@ -3614,6 +4089,34 @@ async function main() {
|
|
|
3614
4089
|
args.splice(approveIdx, 2);
|
|
3615
4090
|
}
|
|
3616
4091
|
|
|
4092
|
+
let grantId = '';
|
|
4093
|
+
const grantIdx = args.indexOf('--grant');
|
|
4094
|
+
if (grantIdx !== -1) {
|
|
4095
|
+
grantId = String(args[grantIdx + 1] || '').trim();
|
|
4096
|
+
if (!grantId) {
|
|
4097
|
+
console.error('Usage: ax --grant <approval-id>');
|
|
4098
|
+
process.exit(1);
|
|
4099
|
+
}
|
|
4100
|
+
args.splice(grantIdx, 2);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
const listGrants = args.includes('--grants');
|
|
4104
|
+
if (listGrants) args.splice(args.indexOf('--grants'), 1);
|
|
4105
|
+
|
|
4106
|
+
const syncGrantsFlag = args.includes('--sync-grants');
|
|
4107
|
+
if (syncGrantsFlag) args.splice(args.indexOf('--sync-grants'), 1);
|
|
4108
|
+
|
|
4109
|
+
let revokeGrantId = '';
|
|
4110
|
+
const revokeGrantIdx = args.indexOf('--revoke-grant');
|
|
4111
|
+
if (revokeGrantIdx !== -1) {
|
|
4112
|
+
revokeGrantId = String(args[revokeGrantIdx + 1] || '').trim();
|
|
4113
|
+
if (!revokeGrantId) {
|
|
4114
|
+
console.error('Usage: ax --revoke-grant <grant-id>');
|
|
4115
|
+
process.exit(1);
|
|
4116
|
+
}
|
|
4117
|
+
args.splice(revokeGrantIdx, 2);
|
|
4118
|
+
}
|
|
4119
|
+
|
|
3617
4120
|
let denyId = '';
|
|
3618
4121
|
const denyIdx = args.indexOf('--deny');
|
|
3619
4122
|
if (denyIdx !== -1) {
|
|
@@ -3627,11 +4130,11 @@ async function main() {
|
|
|
3627
4130
|
|
|
3628
4131
|
const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
|
|
3629
4132
|
const prompt = args
|
|
3630
|
-
.filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--approvals', '--self-test', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
4133
|
+
.filter(arg => !['--auto', '--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--approvals', '--self-test', '--benchmark', '--print', '--headless', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
3631
4134
|
.join(' ')
|
|
3632
4135
|
.trim();
|
|
3633
4136
|
|
|
3634
|
-
const missionIntent = missionRunIntentFromMessage(prompt);
|
|
4137
|
+
const missionIntent = printMode ? null : missionRunIntentFromMessage(prompt);
|
|
3635
4138
|
if (missionIntent) {
|
|
3636
4139
|
const result = await runLocalMission(missionIntent, {
|
|
3637
4140
|
cwd: process.cwd(),
|
|
@@ -3649,6 +4152,12 @@ async function main() {
|
|
|
3649
4152
|
return;
|
|
3650
4153
|
}
|
|
3651
4154
|
|
|
4155
|
+
const startsChat = !printMode && (!prompt || args.includes('--chat'));
|
|
4156
|
+
const autoPick = autoEnabled && prompt && !startsChat
|
|
4157
|
+
? autoLaneForMessage(prompt, { print: printMode })
|
|
4158
|
+
: null;
|
|
4159
|
+
if (autoPick) mode = autoPick.lane;
|
|
4160
|
+
|
|
3652
4161
|
let business = null;
|
|
3653
4162
|
if (businessSlug) {
|
|
3654
4163
|
if (normalizeMode(mode) === 'code-fast') {
|
|
@@ -3677,7 +4186,7 @@ async function main() {
|
|
|
3677
4186
|
executor: makeCloudExecutor({ token: creds.token, businessId: biz.businessId, workspaceId: biz.workspaceId, slug: businessSlug }),
|
|
3678
4187
|
postToolResult
|
|
3679
4188
|
};
|
|
3680
|
-
console.log(`cloud workspace: ${biz.businessName || businessSlug}`);
|
|
4189
|
+
if (!printMode) console.log(`cloud workspace: ${biz.businessName || businessSlug}`);
|
|
3681
4190
|
}
|
|
3682
4191
|
|
|
3683
4192
|
try {
|
|
@@ -3720,8 +4229,75 @@ async function main() {
|
|
|
3720
4229
|
return;
|
|
3721
4230
|
}
|
|
3722
4231
|
|
|
4232
|
+
if (grantId) {
|
|
4233
|
+
const found = findWorkspaceApproval(grantId, process.cwd());
|
|
4234
|
+
if (!found) {
|
|
4235
|
+
console.error(`Pending approval not found: ${grantId}`);
|
|
4236
|
+
process.exit(1);
|
|
4237
|
+
}
|
|
4238
|
+
const command = found.record.payload && found.record.payload.command;
|
|
4239
|
+
const added = permissionGrants.addGrant({ command, workspaceRoot: process.cwd() });
|
|
4240
|
+
if (!added.ok) {
|
|
4241
|
+
console.error(`Not grantable: ${added.reason}`);
|
|
4242
|
+
process.exit(1);
|
|
4243
|
+
}
|
|
4244
|
+
console.log(`granted ${added.grant.grant_id}: ${added.grant.pattern.display} (this workspace, expires ${added.grant.constraints.expires_at.slice(0, 10)})`);
|
|
4245
|
+
await approveWorkspaceApproval(grantId, { cwd: process.cwd(), mode, output: process.stdout });
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
|
|
4249
|
+
if (listGrants) {
|
|
4250
|
+
const store = permissionGrants.loadGrants();
|
|
4251
|
+
const rows = store.grants.filter(g => g.status === 'active');
|
|
4252
|
+
if (!rows.length) {
|
|
4253
|
+
console.log('no active grants');
|
|
4254
|
+
} else {
|
|
4255
|
+
for (const g of rows) {
|
|
4256
|
+
console.log(`${g.grant_id} ${g.pattern.display} ${g.scope.workspace_root} uses:${g.audit?.use_count || 0} expires:${String(g.constraints?.expires_at || '').slice(0, 10)}`);
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
if (revokeGrantId) {
|
|
4263
|
+
const revoked = permissionGrants.revokeGrant(revokeGrantId);
|
|
4264
|
+
if (!revoked.ok) {
|
|
4265
|
+
console.error(revoked.reason);
|
|
4266
|
+
process.exit(1);
|
|
4267
|
+
}
|
|
4268
|
+
console.log(`revoked ${revoked.grant.grant_id}: ${revoked.grant.pattern.display}`);
|
|
4269
|
+
return;
|
|
4270
|
+
}
|
|
4271
|
+
|
|
4272
|
+
if (syncGrantsFlag) {
|
|
4273
|
+
const { apiRequestJson } = require('./utils/api');
|
|
4274
|
+
const result = await permissionGrants.syncGrants({ apiRequestJson, token: authToken() });
|
|
4275
|
+
if (!result.ok) {
|
|
4276
|
+
console.error(`grant sync failed: ${result.reason}`);
|
|
4277
|
+
process.exit(1);
|
|
4278
|
+
}
|
|
4279
|
+
console.log(`synced grants: pushed:${result.pushed} pulled:${result.pulled} revoked:${result.revoked}${result.rejected ? ` rejected:${result.rejected}` : ''}`);
|
|
4280
|
+
return;
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
if (printMode) {
|
|
4284
|
+
const payload = prompt
|
|
4285
|
+
? await runHeadlessTurn(prompt, {
|
|
4286
|
+
mode,
|
|
4287
|
+
cwd: process.cwd(),
|
|
4288
|
+
route: route === 'auto' ? undefined : route,
|
|
4289
|
+
business,
|
|
4290
|
+
verify,
|
|
4291
|
+
autoLane: autoPick && autoPick.lane,
|
|
4292
|
+
autoReason: autoPick && autoPick.reason,
|
|
4293
|
+
})
|
|
4294
|
+
: { ok: false, model: modelForMode(mode), output: '', durationMs: 0, error: 'missing prompt' };
|
|
4295
|
+
console.log(JSON.stringify(payload));
|
|
4296
|
+
process.exit(payload.ok ? 0 : 1);
|
|
4297
|
+
}
|
|
4298
|
+
|
|
3723
4299
|
if (!prompt || args.includes('--chat')) {
|
|
3724
|
-
await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
|
|
4300
|
+
await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify, auto: autoEnabled });
|
|
3725
4301
|
return;
|
|
3726
4302
|
}
|
|
3727
4303
|
|
|
@@ -3737,7 +4313,9 @@ async function main() {
|
|
|
3737
4313
|
: null;
|
|
3738
4314
|
if (approvalRecord) console.log(formatAuxRow('approve', formatApprovalCommand(approvalRecord.id), process.stdout));
|
|
3739
4315
|
// A one-shot has no next turn, so "reply yes to approve" is a dead end;
|
|
3740
|
-
//
|
|
4316
|
+
// granted patterns redeem themselves, everything else gets the exact
|
|
4317
|
+
// redeem command for what the turn staged on disk.
|
|
4318
|
+
await autoRedeemGrantedApprovals(process.cwd(), turnStartedAt, { mode, output: process.stdout });
|
|
3741
4319
|
writeWorkspaceApprovalHints(process.cwd(), turnStartedAt, process.stdout);
|
|
3742
4320
|
console.log('');
|
|
3743
4321
|
console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
|
|
@@ -3767,10 +4345,13 @@ module.exports = {
|
|
|
3767
4345
|
approvalStorePath,
|
|
3768
4346
|
approveStoredApproval,
|
|
3769
4347
|
approveWorkspaceApproval,
|
|
4348
|
+
autoRedeemGrantedApprovals,
|
|
3770
4349
|
findWorkspaceApproval,
|
|
3771
4350
|
listWorkspaceApprovals,
|
|
3772
4351
|
pendingWorkspaceApprovalsSince,
|
|
3773
4352
|
backendBaseUrl,
|
|
4353
|
+
backendApiAllowlistEntry,
|
|
4354
|
+
backendApiToolDescriptor,
|
|
3774
4355
|
backendStartCommand,
|
|
3775
4356
|
backendUrl,
|
|
3776
4357
|
buildCodeFastPayload,
|
|
@@ -3793,6 +4374,9 @@ module.exports = {
|
|
|
3793
4374
|
denyStoredApproval,
|
|
3794
4375
|
executeApprovalRequest,
|
|
3795
4376
|
executeApprovalFromState,
|
|
4377
|
+
executeBackendApiTool,
|
|
4378
|
+
formatBackendApiApprovalPrompt,
|
|
4379
|
+
formatBackendApiReceipt,
|
|
3796
4380
|
formatDoneLine,
|
|
3797
4381
|
formatDuration,
|
|
3798
4382
|
formatApprovalExecutionResult,
|
|
@@ -3819,11 +4403,13 @@ module.exports = {
|
|
|
3819
4403
|
postApprovalExecution,
|
|
3820
4404
|
postCodeFastTurn,
|
|
3821
4405
|
postTurn,
|
|
4406
|
+
postTurnTimeoutMs,
|
|
3822
4407
|
readApprovalStore,
|
|
3823
4408
|
removeStoredApproval,
|
|
3824
4409
|
renderStreamingMarkdown,
|
|
3825
4410
|
renderTerminalMarkdown,
|
|
3826
4411
|
resolveRoute,
|
|
4412
|
+
runHeadlessTurn,
|
|
3827
4413
|
runBenchmark,
|
|
3828
4414
|
runSelfTest,
|
|
3829
4415
|
runtimeReadyForChat,
|