atris 3.30.0 → 3.30.2
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 +6 -0
- package/atris/AGENTS.md +11 -0
- package/atris/CLAUDE.md +5 -0
- package/atris/atris.md +19 -4
- package/atris/policies/atris-design.md +71 -0
- package/atris/policies/design-seed.md +187 -0
- package/atris/skills/atris/SKILL.md +26 -3
- package/atris/skills/design/SKILL.md +3 -2
- package/atris/skills/loop/SKILL.md +5 -3
- package/atris/team/_template/MEMBER.md +19 -0
- package/atris.md +63 -23
- package/ax +1434 -1698
- package/bin/atris.js +5 -1
- package/commands/agent-spawn.js +2 -2
- package/commands/brain.js +92 -7
- package/commands/brainstorm.js +62 -22
- package/commands/business-sync.js +92 -8
- package/commands/business.js +13 -7
- package/commands/chat-scan.js +102 -0
- package/commands/computer.js +758 -15
- package/commands/deck.js +505 -105
- package/commands/init.js +6 -1
- package/commands/launchpad.js +638 -0
- package/commands/log-sync.js +44 -13
- package/commands/loop-front.js +290 -0
- package/commands/member.js +124 -3
- package/commands/mission.js +653 -30
- package/commands/pull.js +37 -28
- package/commands/pulse.js +11 -8
- package/commands/run.js +79 -39
- package/commands/sync.js +36 -17
- package/commands/task.js +1072 -89
- package/commands/workflow.js +6 -6
- package/commands/xp.js +13 -1
- package/commands/youtube.js +221 -7
- package/decks/README.md +89 -0
- package/decks/archetype-catalog.json +180 -0
- package/decks/atris-antislop-pitch.json +48 -0
- package/decks/atris-archetypes-v2.json +83 -0
- package/decks/atris-one-loop-pitch.json +80 -0
- package/decks/atris-seed-pitch-v3.json +118 -0
- package/decks/atris-seed-pitch-v4-skeleton.json +106 -0
- package/decks/atris-seed-pitch-v5.json +109 -0
- package/decks/atris-seed-pitch-v6.json +137 -0
- package/decks/atris-seed-pitch-v7.json +133 -0
- package/decks/atris-single-shot-proof.json +74 -0
- package/decks/mark-pincus-narrative.json +102 -0
- package/decks/mark-pincus-sourcery.json +94 -0
- package/decks/yash-applied-compute-detailed.json +150 -0
- package/decks/yash-applied-compute-generalist.json +82 -0
- package/decks/yash-applied-compute-narrative.json +54 -0
- package/lib/ax-prefs.js +5 -12
- package/lib/chat-log-scan.js +377 -0
- package/lib/context-gatherer.js +35 -1
- package/lib/deck-compose.js +145 -0
- package/lib/deck-history.js +64 -0
- package/lib/deck-layout.js +169 -0
- package/lib/deck-review.js +431 -0
- package/lib/deck-schema.js +154 -0
- package/lib/file-ops.js +3 -3
- package/lib/functional-owner.js +189 -0
- package/lib/journal.js +7 -73
- package/lib/slides-deck.js +512 -58
- package/lib/task-db.js +128 -11
- package/package.json +2 -1
- package/templates/business-starter/team/START_HERE.md +12 -8
- package/utils/auth.js +4 -0
- package/utils/config.js +4 -0
- package/atris/atrisDev.md +0 -717
package/ax
CHANGED
|
@@ -6,46 +6,15 @@ const https = require('https');
|
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const readline = require('readline');
|
|
9
|
-
const
|
|
9
|
+
const crypto = require('crypto');
|
|
10
10
|
const { Readable } = require('stream');
|
|
11
11
|
const { loadCredentials } = require('./utils/auth');
|
|
12
|
-
const {
|
|
13
|
-
MEMBER_SLUG,
|
|
14
|
-
loadAxPrefs,
|
|
15
|
-
setBypassPermissions,
|
|
16
|
-
resolveBypassPermissions,
|
|
17
|
-
permissionsLabel,
|
|
18
|
-
} = require('./lib/ax-prefs');
|
|
19
|
-
const {
|
|
20
|
-
renderShimmerText,
|
|
21
|
-
} = require('./lib/ax-shimmer');
|
|
22
|
-
const {
|
|
23
|
-
attachMultilineChatInput,
|
|
24
|
-
stripMultilineCsiText,
|
|
25
|
-
} = require('./lib/ax-chat-input');
|
|
26
|
-
const {
|
|
27
|
-
accumulateGoalUsage,
|
|
28
|
-
buildGoalDirective,
|
|
29
|
-
clearGoalState,
|
|
30
|
-
createGoalState,
|
|
31
|
-
evaluateGoalTurn,
|
|
32
|
-
finishGoalAchieved,
|
|
33
|
-
formatGoalAchieved,
|
|
34
|
-
formatGoalActiveBanner,
|
|
35
|
-
formatGoalContinue,
|
|
36
|
-
formatGoalStatus,
|
|
37
|
-
formatGoalStopped,
|
|
38
|
-
goalBudgetExceeded,
|
|
39
|
-
goalTurnLimitReached,
|
|
40
|
-
parseGoalCommand,
|
|
41
|
-
} = require('./lib/ax-goal');
|
|
42
12
|
|
|
43
13
|
const EXIT_WORDS = new Set(['exit', 'quit', ':q']);
|
|
44
14
|
const BACKEND = {
|
|
45
15
|
host: '127.0.0.1',
|
|
46
16
|
port: 8000,
|
|
47
|
-
path: '/api/atris2/turn'
|
|
48
|
-
publicBase: 'https://api.atris.ai'
|
|
17
|
+
path: '/api/atris2/turn'
|
|
49
18
|
};
|
|
50
19
|
const DEFAULT_BACKEND_BASE = `http://${BACKEND.host}:${BACKEND.port}`;
|
|
51
20
|
const CODE_FAST = {
|
|
@@ -56,6 +25,9 @@ const CODE_FAST = {
|
|
|
56
25
|
const CONNECTION_STATUS_PATH = '/api/integrations/status';
|
|
57
26
|
const CONNECTION_CAPABILITIES_PATH = '/api/atris2/connection-capabilities';
|
|
58
27
|
const ATRIS2_CONNECTION_STATUS_PATH = '/api/atris2/connection-status';
|
|
28
|
+
const ATRIS2_HEALTH_PATH = '/api/atris2/health';
|
|
29
|
+
const APPROVAL_EXECUTE_PATH = '/api/atris2/approvals/execute';
|
|
30
|
+
const DEFAULT_APPROVAL_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
59
31
|
const CONNECTOR_NAMES = {
|
|
60
32
|
gmail: 'Gmail',
|
|
61
33
|
google_calendar: 'Google Calendar',
|
|
@@ -88,22 +60,14 @@ const CONNECTOR_SCOPES = {
|
|
|
88
60
|
const ANSI = {
|
|
89
61
|
reset: '\x1b[0m',
|
|
90
62
|
bold: '\x1b[1m',
|
|
91
|
-
italic: '\x1b[3m',
|
|
92
63
|
dim: '\x1b[2m',
|
|
93
64
|
muted: '\x1b[90m',
|
|
94
65
|
accent: '\x1b[36m',
|
|
95
66
|
ok: '\x1b[32m',
|
|
96
|
-
|
|
97
|
-
warn: '\x1b[33m',
|
|
98
|
-
magenta: '\x1b[35m',
|
|
99
|
-
underline: '\x1b[4m',
|
|
100
|
-
strike: '\x1b[9m',
|
|
67
|
+
magenta: '\x1b[35m'
|
|
101
68
|
};
|
|
102
69
|
|
|
103
70
|
const TIER_COLORS = { fast: '\x1b[32m', pro: '\x1b[36m', max: '\x1b[35m' };
|
|
104
|
-
const PROGRESS_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
105
|
-
const PROGRESS_START_DELAY_MS = 350;
|
|
106
|
-
const PROGRESS_FRAME_MS = 100;
|
|
107
71
|
|
|
108
72
|
function tierColor(mode) {
|
|
109
73
|
return TIER_COLORS[mode] || ANSI.accent;
|
|
@@ -136,23 +100,11 @@ function tierLabel(mode) {
|
|
|
136
100
|
return 'Atris 2 Pro';
|
|
137
101
|
}
|
|
138
102
|
|
|
139
|
-
function formatHeader({ mode = '
|
|
140
|
-
const goalLine = chat && goal ? formatGoalActiveBanner(goal, {
|
|
141
|
-
paint: (text, codes) => paint(text, codes, options),
|
|
142
|
-
bold: ANSI.bold,
|
|
143
|
-
magenta: ANSI.magenta,
|
|
144
|
-
muted: ANSI.muted,
|
|
145
|
-
accent: ANSI.accent,
|
|
146
|
-
...options,
|
|
147
|
-
}) : '';
|
|
148
|
-
const home = os.homedir();
|
|
149
|
-
const shortCwd = cwd && home && (cwd === home || cwd.startsWith(`${home}/`)) ? `~${cwd.slice(home.length)}` : cwd;
|
|
150
|
-
const title = paint(`${tierLabel(mode)}${chat ? ' chat' : ''}`, [ANSI.bold, tierColor(normalizeMode(mode))], options);
|
|
151
|
-
const titleLine = shortCwd ? `${title}${paint(` · ${shortCwd}`, [ANSI.muted], options)}` : title;
|
|
103
|
+
function formatHeader({ mode = 'pro', cwd = process.cwd(), chat = false } = {}, options = {}) {
|
|
152
104
|
return [
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
chat ? paint('
|
|
105
|
+
paint(`${tierLabel(mode)}${chat ? ' chat' : ''}`, [ANSI.bold, tierColor(normalizeMode(mode))], options),
|
|
106
|
+
cwd,
|
|
107
|
+
chat ? paint('type / for the menu · /fast /pro /max swap tiers · exit to leave', [ANSI.muted], options) : '',
|
|
156
108
|
].filter(Boolean).join('\n');
|
|
157
109
|
}
|
|
158
110
|
|
|
@@ -161,57 +113,34 @@ function formatUsage() {
|
|
|
161
113
|
'ax - Atris local/code agent',
|
|
162
114
|
'',
|
|
163
115
|
'Usage:',
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
' ax chat [ax] [--fast|--pro|--max]',
|
|
167
|
-
' ax spawn <role> --task "..." [--engine manual|codex|claude|cursor|devin]',
|
|
168
|
-
' ax spawns [--json]',
|
|
169
|
-
' ax youtube <url> [--query "..."] [--json]',
|
|
170
|
-
' ax --dogfood-chat [--loops 25] [--json]',
|
|
171
|
-
' ax --dogfood-status [--json]',
|
|
116
|
+
' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] <message>',
|
|
117
|
+
' ax [--max|--pro|--fast|--code-fast] [--local|--cloud] --chat',
|
|
172
118
|
' ax [--max|--pro|--fast] --business <slug> [<message>|--chat]',
|
|
173
119
|
' ax [--max|--pro|--fast|--code-fast] --doctor',
|
|
120
|
+
' ax --approvals',
|
|
121
|
+
' ax --approve <approval-id>',
|
|
122
|
+
' ax --deny <approval-id>',
|
|
123
|
+
' ax --self-test',
|
|
174
124
|
' ax [--max|--fast] --benchmark',
|
|
175
125
|
'',
|
|
176
126
|
'Modes:',
|
|
177
|
-
' --max
|
|
178
|
-
' --pro
|
|
179
|
-
' --fast
|
|
127
|
+
' --max local workspace agent, highest reasoning, slowest turns',
|
|
128
|
+
' --pro local workspace agent, deeper tool loop',
|
|
129
|
+
' --fast local workspace agent, faster low-latency turns',
|
|
180
130
|
' --code-fast Atris Code Fast public lane',
|
|
181
131
|
' --local force local workspace tools',
|
|
182
132
|
' --cloud force authenticated cloud connectors/chat',
|
|
183
133
|
' --business <slug> run tools on that business cloud workspace (EC2)',
|
|
184
134
|
' --verify <cmd> gate the turn on this command passing (default: no verifier)',
|
|
185
|
-
' --bypass allow external side effects for this run (default: safe / approval-only)',
|
|
186
|
-
' --safe force approval-only mode for this run',
|
|
187
|
-
' --dogfood-chat run the safe ax fast chat checklist harness',
|
|
188
|
-
' --dogfood-status summarize the overnight dogfood mission progress',
|
|
189
|
-
'',
|
|
190
|
-
'Chat:',
|
|
191
|
-
' /goal <condition> work autonomously until the condition is met',
|
|
192
|
-
' /goal show active goal, turns, and evaluator reason',
|
|
193
|
-
' /goal clear stop the active goal',
|
|
194
|
-
' /bypass persist bypass mode (external actions allowed when backend supports it)',
|
|
195
|
-
' /safe persist safe mode (default; approval-only external actions)',
|
|
196
|
-
'',
|
|
197
|
-
'Delegation:',
|
|
198
|
-
' ax spawn <role> --task "..." create a worker request from ax',
|
|
199
|
-
' atris agent spawn <role> --task "..." same request ledger from the Atris CLI',
|
|
200
|
-
' ax youtube <url> process a YouTube video through Atris',
|
|
201
|
-
' ax --dogfood-chat --loops 25 exercise chat UI and routing without spending credits',
|
|
202
|
-
' ax --dogfood-status show overnight dogfood proof and remaining loops',
|
|
203
135
|
'',
|
|
204
136
|
'Examples:',
|
|
205
|
-
' ax spawn worker --task "Fix the failing smoke test" --engine codex',
|
|
206
|
-
' ax youtube https://www.youtube.com/watch?v=VIDEO_ID',
|
|
207
|
-
' ax --dogfood-chat --loops 25',
|
|
208
|
-
' ax --dogfood-status',
|
|
209
137
|
' ax --pro find the config file and explain it',
|
|
210
138
|
' ax --fast what files are here',
|
|
211
139
|
' ax --max refactor this module and verify the tests',
|
|
212
140
|
' ax --max --verify "npm test" fix the failing suite',
|
|
213
141
|
' ax --code-fast explain this error',
|
|
214
142
|
' ax --fast what is on my calendar today',
|
|
143
|
+
' ax --approvals',
|
|
215
144
|
' ax --pro --chat',
|
|
216
145
|
].join('\n');
|
|
217
146
|
}
|
|
@@ -226,9 +155,10 @@ function stripAnsi(value) {
|
|
|
226
155
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, '');
|
|
227
156
|
}
|
|
228
157
|
|
|
229
|
-
function createRunLogger({ cwd = process.cwd(), mode = '
|
|
158
|
+
function createRunLogger({ cwd = process.cwd(), mode = 'pro', kind = 'play', output = process.stdout } = {}) {
|
|
230
159
|
if (process.env.AX_AUTO_LOG === '0') return null;
|
|
231
160
|
if (output !== process.stdout && output !== process.stderr && !output.isTTY) return null;
|
|
161
|
+
const fullTranscript = process.env.AX_LOG_FULL === '1';
|
|
232
162
|
|
|
233
163
|
const baseDir = fs.existsSync(path.join(cwd, 'atris'))
|
|
234
164
|
? path.join(cwd, 'atris', 'runs')
|
|
@@ -236,15 +166,22 @@ function createRunLogger({ cwd = process.cwd(), mode = 'fast', kind = 'play', ou
|
|
|
236
166
|
fs.mkdirSync(baseDir, { recursive: true });
|
|
237
167
|
const logPath = path.join(baseDir, `ax-${kind}-${timestampForFile()}.log`);
|
|
238
168
|
fs.appendFileSync(logPath, [
|
|
239
|
-
`command: ${process.argv.join(' ')}`,
|
|
169
|
+
`command: ${fullTranscript ? process.argv.join(' ') : 'ax <redacted>'}`,
|
|
240
170
|
`cwd: ${cwd}`,
|
|
241
171
|
`mode: ${mode}`,
|
|
172
|
+
`transcript: ${fullTranscript ? 'full' : 'redacted'}`,
|
|
242
173
|
`started_at: ${new Date().toISOString()}`,
|
|
243
174
|
'',
|
|
244
175
|
].join('\n'));
|
|
245
176
|
|
|
177
|
+
let redactedChars = 0;
|
|
246
178
|
const writeLog = (chunk) => {
|
|
247
|
-
|
|
179
|
+
const text = stripAnsi(chunk);
|
|
180
|
+
if (fullTranscript) {
|
|
181
|
+
fs.appendFileSync(logPath, text);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
redactedChars += text.length;
|
|
248
185
|
};
|
|
249
186
|
const teeOutput = {
|
|
250
187
|
isTTY: Boolean(output && output.isTTY),
|
|
@@ -261,56 +198,41 @@ function createRunLogger({ cwd = process.cwd(), mode = 'fast', kind = 'play', ou
|
|
|
261
198
|
output: teeOutput,
|
|
262
199
|
write: writeLog,
|
|
263
200
|
close(exitCode = 0) {
|
|
201
|
+
if (!fullTranscript) {
|
|
202
|
+
fs.appendFileSync(logPath, `redacted_chars: ${redactedChars}\n`);
|
|
203
|
+
fs.appendFileSync(logPath, 'note: full transcript disabled by default; set AX_LOG_FULL=1 to opt in.\n');
|
|
204
|
+
fs.appendFileSync(logPath, `exit_code: ${exitCode}\nfinished_at: ${new Date().toISOString()}\n`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
264
207
|
writeLog(`\nexit_code: ${exitCode}\nfinished_at: ${new Date().toISOString()}\n`);
|
|
265
208
|
}
|
|
266
209
|
};
|
|
267
210
|
}
|
|
268
211
|
|
|
269
|
-
function backendBaseUrl(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
: options.route === 'cloud' || options.forceCloud
|
|
273
|
-
? 'cloud'
|
|
274
|
-
: 'auto';
|
|
275
|
-
const localOverride = process.env.AX_BACKEND_URL || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL;
|
|
276
|
-
const cloudOverride = process.env.OBELISK_ATRIS2_BACKEND_URL || process.env.ATRIS_API_BASE || BACKEND.publicBase;
|
|
277
|
-
if (route === 'cloud') {
|
|
278
|
-
if (localOverride && !isLoopbackUrl(localOverride)) return localOverride.replace(/\/$/, '');
|
|
279
|
-
return cloudOverride.replace(/\/$/, '');
|
|
280
|
-
}
|
|
281
|
-
if (route === 'auto' && !localOverride) {
|
|
282
|
-
return cloudOverride.replace(/\/$/, '');
|
|
283
|
-
}
|
|
284
|
-
return (localOverride
|
|
212
|
+
function backendBaseUrl() {
|
|
213
|
+
return (process.env.AX_BACKEND_URL
|
|
214
|
+
|| process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL
|
|
285
215
|
|| process.env.OBELISK_ATRIS2_BACKEND_URL
|
|
286
216
|
|| DEFAULT_BACKEND_BASE).replace(/\/$/, '');
|
|
287
217
|
}
|
|
288
218
|
|
|
289
|
-
function backendUrl(
|
|
290
|
-
return new URL(BACKEND.path, backendBaseUrl(
|
|
219
|
+
function backendUrl() {
|
|
220
|
+
return new URL(BACKEND.path, backendBaseUrl()).toString();
|
|
291
221
|
}
|
|
292
222
|
|
|
293
|
-
function backendPathUrl(pathname
|
|
294
|
-
return new URL(pathname, backendBaseUrl(
|
|
223
|
+
function backendPathUrl(pathname) {
|
|
224
|
+
return new URL(pathname, backendBaseUrl()).toString();
|
|
295
225
|
}
|
|
296
226
|
|
|
297
|
-
function codeFastBaseUrl(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
: 'auto';
|
|
303
|
-
const backendOverride = process.env.AX_CODE_FAST_BACKEND_URL;
|
|
304
|
-
const useBackendOverride = backendOverride && !(route === 'cloud' && isLoopbackUrl(backendOverride));
|
|
305
|
-
return (useBackendOverride
|
|
306
|
-
? backendOverride
|
|
307
|
-
: process.env.AX_CODE_FAST_API_BASE
|
|
308
|
-
|| process.env.ATRIS_API_BASE
|
|
309
|
-
|| CODE_FAST.publicBase).replace(/\/$/, '');
|
|
227
|
+
function codeFastBaseUrl() {
|
|
228
|
+
return (process.env.AX_CODE_FAST_BACKEND_URL
|
|
229
|
+
|| process.env.AX_CODE_FAST_API_BASE
|
|
230
|
+
|| process.env.ATRIS_API_BASE
|
|
231
|
+
|| CODE_FAST.publicBase).replace(/\/$/, '');
|
|
310
232
|
}
|
|
311
233
|
|
|
312
|
-
function codeFastUrl(
|
|
313
|
-
return new URL(CODE_FAST.path, codeFastBaseUrl(
|
|
234
|
+
function codeFastUrl() {
|
|
235
|
+
return new URL(CODE_FAST.path, codeFastBaseUrl()).toString();
|
|
314
236
|
}
|
|
315
237
|
|
|
316
238
|
function isLoopbackUrl(value) {
|
|
@@ -322,15 +244,6 @@ function isLoopbackUrl(value) {
|
|
|
322
244
|
}
|
|
323
245
|
}
|
|
324
246
|
|
|
325
|
-
function configuredLocalBackendUrl() {
|
|
326
|
-
return process.env.AX_BACKEND_URL || process.env.OBELISK_LOCAL_ATRIS2_BACKEND_URL || '';
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function hasLocalWorkspaceBackend(options = {}) {
|
|
330
|
-
if (options.localWorkspaceBackend === true) return true;
|
|
331
|
-
return Boolean(configuredLocalBackendUrl());
|
|
332
|
-
}
|
|
333
|
-
|
|
334
247
|
function isCodeFastLocal(options = {}) {
|
|
335
248
|
const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
|
|
336
249
|
return route === 'local' && isLoopbackUrl(options.codeFastBaseUrl || codeFastBaseUrl());
|
|
@@ -342,44 +255,39 @@ function buildRunProfile(options = {}) {
|
|
|
342
255
|
if (mode === 'code-fast') {
|
|
343
256
|
const route = options.route === 'local' || options.forceLocal ? 'local' : 'cloud';
|
|
344
257
|
return {
|
|
345
|
-
endpoint: codeFastUrl(
|
|
258
|
+
endpoint: codeFastUrl(),
|
|
346
259
|
mode,
|
|
347
260
|
route,
|
|
348
261
|
model: CODE_FAST.model,
|
|
349
262
|
workspace_path: isCodeFastLocal(options) ? cwd : 'cloud scratch',
|
|
350
263
|
max_turns: 1,
|
|
351
264
|
streaming: false,
|
|
352
|
-
runtime: isCodeFastLocal(options) ? 'local Cursor SDK
|
|
265
|
+
runtime: isCodeFastLocal(options) ? 'local Cursor SDK through backend' : 'authenticated Code Fast cloud scratch',
|
|
353
266
|
reasoning: 'Composer 2.5 fast lane; charges 10 credits per public turn'
|
|
354
267
|
};
|
|
355
268
|
}
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
const payload = buildPayload(profileMessage, { mode, cwd, route, bypassPermissions: options.bypassPermissions });
|
|
269
|
+
const route = resolveRoute(options.message || 'doctor', options);
|
|
270
|
+
const payload = buildPayload(options.message || 'doctor', { mode, cwd, route });
|
|
359
271
|
return {
|
|
360
|
-
endpoint: backendUrl(
|
|
272
|
+
endpoint: backendUrl(),
|
|
361
273
|
mode,
|
|
362
274
|
route,
|
|
363
275
|
model: payload.model,
|
|
364
276
|
workspace_path: payload.workspace_path || 'cloud',
|
|
365
277
|
max_turns: payload.max_turns,
|
|
366
|
-
member_slug: payload.member_slug,
|
|
367
|
-
bypass_permissions: payload.bypass_permissions,
|
|
368
278
|
streaming: true,
|
|
369
279
|
runtime: route === 'cloud' ? 'authenticated cloud connectors/chat' : 'local workspace',
|
|
370
280
|
reasoning: mode === 'max'
|
|
371
|
-
? '
|
|
281
|
+
? 'backend reports run row; Max workspace tool loop uses high reasoning effort'
|
|
372
282
|
: mode === 'pro'
|
|
373
|
-
? '
|
|
374
|
-
: '
|
|
283
|
+
? 'backend reports run row; Pro workspace tool loop uses API default medium'
|
|
284
|
+
: 'backend reports run row; Fast workspace tool loop uses provider default'
|
|
375
285
|
};
|
|
376
286
|
}
|
|
377
287
|
|
|
378
288
|
function formatRunProfile(profile, options = {}) {
|
|
379
289
|
const rows = [
|
|
380
290
|
['mode', profile.mode],
|
|
381
|
-
['agent', profile.member_slug || MEMBER_SLUG],
|
|
382
|
-
['permissions', profile.bypass_permissions ? 'bypass' : 'safe'],
|
|
383
291
|
['endpoint', profile.endpoint],
|
|
384
292
|
['route', profile.route || 'auto'],
|
|
385
293
|
['workspace', formatPathSubject(profile.workspace_path, options)],
|
|
@@ -391,6 +299,53 @@ function formatRunProfile(profile, options = {}) {
|
|
|
391
299
|
return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
|
|
392
300
|
}
|
|
393
301
|
|
|
302
|
+
async function buildRuntimeHealth(options = {}) {
|
|
303
|
+
const healthRes = options.healthRes
|
|
304
|
+
? await Promise.resolve(options.healthRes)
|
|
305
|
+
: await requestJson(ATRIS2_HEALTH_PATH, { token: '', timeoutMs: options.timeoutMs || 1500 });
|
|
306
|
+
const data = healthRes.ok && healthRes.data && typeof healthRes.data === 'object' ? healthRes.data : {};
|
|
307
|
+
const models = Array.isArray(data.models) ? data.models : [];
|
|
308
|
+
const fast = models.find(row => row && row.id === 'atris:fast') || null;
|
|
309
|
+
return {
|
|
310
|
+
schema: 'ax.runtime_health.v1',
|
|
311
|
+
backend: {
|
|
312
|
+
ready: Boolean(healthRes.ok && data.ready),
|
|
313
|
+
reachable: Boolean(healthRes.ok),
|
|
314
|
+
status: healthRes.status || 0,
|
|
315
|
+
error: healthRes.error || '',
|
|
316
|
+
},
|
|
317
|
+
fast: {
|
|
318
|
+
ready: Boolean(fast && fast.ready),
|
|
319
|
+
route_ready: Boolean(fast && fast.ready),
|
|
320
|
+
},
|
|
321
|
+
permissions: {
|
|
322
|
+
approval_execution_path: APPROVAL_EXECUTE_PATH,
|
|
323
|
+
ready: Boolean(healthRes.ok),
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function formatRuntimeHealth(health, options = {}) {
|
|
329
|
+
const backendReady = health && health.backend && health.backend.ready;
|
|
330
|
+
const backendReachable = health && health.backend && health.backend.reachable;
|
|
331
|
+
const fastReady = health && health.fast && health.fast.ready;
|
|
332
|
+
const permissionReady = health && health.permissions && health.permissions.ready;
|
|
333
|
+
const rows = [
|
|
334
|
+
['backend', backendReady ? 'ready' : backendReachable ? 'not ready' : 'offline'],
|
|
335
|
+
['fast', fastReady ? 'ready' : 'not ready'],
|
|
336
|
+
['approvals', permissionReady ? 'ready' : 'offline'],
|
|
337
|
+
];
|
|
338
|
+
if (!backendReachable) rows.push(['fix', 'start local backend, then rerun ax --doctor']);
|
|
339
|
+
return rows.map(([label, value]) => formatAuxRow(label, value, options)).join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function runtimeReadyForChat(health, mode = 'fast') {
|
|
343
|
+
if (!health || !health.backend || !health.permissions) return false;
|
|
344
|
+
if (!health.backend.ready || !health.permissions.ready) return false;
|
|
345
|
+
if (normalizeMode(mode) === 'fast' && (!health.fast || health.fast.ready === false)) return false;
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
|
|
394
349
|
function canonicalConnectorId(value) {
|
|
395
350
|
return String(value || '').trim().toLowerCase().replace(/-/g, '_');
|
|
396
351
|
}
|
|
@@ -423,9 +378,9 @@ function authUserId() {
|
|
|
423
378
|
}
|
|
424
379
|
}
|
|
425
380
|
|
|
426
|
-
function isLoopbackBackend(
|
|
381
|
+
function isLoopbackBackend() {
|
|
427
382
|
try {
|
|
428
|
-
const parsed = new URL(backendBaseUrl(
|
|
383
|
+
const parsed = new URL(backendBaseUrl());
|
|
429
384
|
return ['127.0.0.1', 'localhost', '::1'].includes(parsed.hostname);
|
|
430
385
|
} catch (_) {
|
|
431
386
|
return false;
|
|
@@ -467,6 +422,48 @@ function requestJson(pathname, { token = authToken(), timeoutMs = 6000 } = {}) {
|
|
|
467
422
|
});
|
|
468
423
|
}
|
|
469
424
|
|
|
425
|
+
function postJson(pathname, body, { token = authToken(), timeoutMs = 30000 } = {}) {
|
|
426
|
+
const url = backendPathUrl(pathname);
|
|
427
|
+
const parsed = new URL(url);
|
|
428
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
429
|
+
const postData = JSON.stringify(body || {});
|
|
430
|
+
return new Promise((resolve) => {
|
|
431
|
+
const req = transport.request({
|
|
432
|
+
method: 'POST',
|
|
433
|
+
hostname: parsed.hostname,
|
|
434
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
435
|
+
path: `${parsed.pathname}${parsed.search}`,
|
|
436
|
+
headers: {
|
|
437
|
+
'Content-Type': 'application/json',
|
|
438
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
439
|
+
Accept: 'application/json',
|
|
440
|
+
...(token ? { Authorization: `Bearer ${token}` } : {})
|
|
441
|
+
}
|
|
442
|
+
}, (res) => {
|
|
443
|
+
const chunks = [];
|
|
444
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
445
|
+
res.on('end', () => {
|
|
446
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
447
|
+
let data = null;
|
|
448
|
+
try {
|
|
449
|
+
data = text ? JSON.parse(text) : null;
|
|
450
|
+
} catch (_) {
|
|
451
|
+
resolve({ ok: false, status: res.statusCode, data: null, text });
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, data, text });
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
req.on('error', error => resolve({ ok: false, status: 0, data: null, text: '', error: error.message }));
|
|
458
|
+
req.setTimeout(timeoutMs, () => {
|
|
459
|
+
req.destroy();
|
|
460
|
+
resolve({ ok: false, status: 0, data: null, text: '', error: `timeout after ${timeoutMs}ms` });
|
|
461
|
+
});
|
|
462
|
+
req.write(postData);
|
|
463
|
+
req.end();
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
470
467
|
function defaultAuthority(id) {
|
|
471
468
|
const key = canonicalConnectorId(id);
|
|
472
469
|
if (key === 'gmail') return { list_messages: 'read_only', get_message: 'read_only', send_message: 'approval_required' };
|
|
@@ -594,7 +591,7 @@ function mentionsConnector(message) {
|
|
|
594
591
|
}
|
|
595
592
|
|
|
596
593
|
function connectorWriteIntent(message) {
|
|
597
|
-
return mentionsConnector(message) && /\b(send|post|dm|message|reply|draft|compose|schedule|book|create|update|delete|archive|move|share|comment|invite)\b/i.test(message || '');
|
|
594
|
+
return mentionsConnector(message) && /\b(add|send|post|dm|message|reply|draft|compose|schedule|book|create|update|delete|archive|move|share|comment|invite)\b/i.test(message || '');
|
|
598
595
|
}
|
|
599
596
|
|
|
600
597
|
function workspaceIntent(message) {
|
|
@@ -607,220 +604,24 @@ function githubWorkspaceIntent(message) {
|
|
|
607
604
|
return /\b(push|commit|commits?|branch|branches|checkout|merge|rebase|tag|release|pr|pull request|pull-request|repo change|code change|small change)\b/i.test(text);
|
|
608
605
|
}
|
|
609
606
|
|
|
610
|
-
function casualChatIntent(message) {
|
|
611
|
-
const text = String(message || '').trim();
|
|
612
|
-
if (!text) return false;
|
|
613
|
-
if (workspaceIntent(text) || mentionsConnector(text) || githubWorkspaceIntent(text)) return false;
|
|
614
|
-
if (/^(hi+|hello+|hey+|yo+|sup|gm|good morning|good afternoon|good evening|thanks?|thank you|ok+|okay|k|cool|nice|lol|lmao|haha|why\??|what\??|huh\??|yes|no|nah|yep|nope)[.!?]*$/i.test(text)) return true;
|
|
615
|
-
if (/^(hi|hello|hey)\b/i.test(text)) return true;
|
|
616
|
-
return /^[a-z]{1,8}[.!?]*$/i.test(text);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
607
|
function resolveRoute(message, options = {}) {
|
|
620
608
|
if (options.route === 'local' || options.forceLocal) return 'local';
|
|
621
609
|
if (options.route === 'cloud' || options.forceCloud) return 'cloud';
|
|
622
|
-
if (
|
|
623
|
-
const localWorkspace = hasLocalWorkspaceBackend(options);
|
|
624
|
-
if (githubWorkspaceIntent(message) && localWorkspace) return 'local';
|
|
610
|
+
if (githubWorkspaceIntent(message)) return 'local';
|
|
625
611
|
if (mentionsConnector(message) && !workspaceIntent(message)) return 'cloud';
|
|
626
|
-
if (!localWorkspace) return 'cloud';
|
|
627
612
|
return 'local';
|
|
628
613
|
}
|
|
629
614
|
|
|
630
615
|
function normalizeMode(mode) {
|
|
631
616
|
if (mode === 'code-fast' || mode === 'code') return 'code-fast';
|
|
632
617
|
if (mode === 'max') return 'max';
|
|
633
|
-
|
|
634
|
-
return 'fast';
|
|
618
|
+
return mode === 'fast' ? 'fast' : 'pro';
|
|
635
619
|
}
|
|
636
620
|
|
|
637
621
|
function formatPrompt(mode, options = {}) {
|
|
622
|
+
if (!mode) return '› ';
|
|
638
623
|
const tier = normalizeMode(mode);
|
|
639
|
-
|
|
640
|
-
? `${paint('◎ ', [ANSI.bold, ANSI.magenta], options)}`
|
|
641
|
-
: '';
|
|
642
|
-
if (!mode) return `${goalHint}› `;
|
|
643
|
-
return `${goalHint}${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `;
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
function terminalWidth(options = {}) {
|
|
647
|
-
const cols = Number(options.columns || process.stdout.columns || 0);
|
|
648
|
-
if (Number.isFinite(cols) && cols >= 48) return cols;
|
|
649
|
-
return 72;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
function inputBoxPlainWidth(options = {}) {
|
|
653
|
-
const cols = Number(options.columns || process.stdout.columns || 0);
|
|
654
|
-
if (Number.isFinite(cols) && cols >= 48) return cols - 1;
|
|
655
|
-
return 71;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
function inputBoxLayoutOptions(stream, overrides = {}) {
|
|
659
|
-
const cols = Number(stream?.columns || process.stdout.columns || 0);
|
|
660
|
-
return {
|
|
661
|
-
...overrides,
|
|
662
|
-
columns: Number.isFinite(cols) && cols >= 48 ? cols - 1 : 71,
|
|
663
|
-
isTTY: stream?.isTTY ?? process.stdout.isTTY,
|
|
664
|
-
};
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
function buildInputBoxTopPlain(options = {}) {
|
|
668
|
-
const width = inputBoxPlainWidth(options);
|
|
669
|
-
return `╭${'─'.repeat(Math.max(0, width - 2))}╮`;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// The mode + newline affordance lives on its own line BELOW the box (claude
|
|
673
|
-
// style), so the rules above/below stay clean and we never say "enter send".
|
|
674
|
-
function buildInputBoxStatusPlain(options = {}) {
|
|
675
|
-
return ` ${formatPermissionModeBrief(options)} · shift+enter`;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
function buildInputBoxBottomPlain(options = {}) {
|
|
679
|
-
const width = inputBoxPlainWidth(options);
|
|
680
|
-
return `╰${'─'.repeat(Math.max(0, width - 2))}╯`;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function formatInputBoxInputRow(prompt, line, options = {}) {
|
|
684
|
-
const width = inputBoxPlainWidth(options);
|
|
685
|
-
const promptPlain = stripAnsi(prompt);
|
|
686
|
-
const linePlain = stripAnsi(String(line || ''));
|
|
687
|
-
const rightBar = paint('│', [ANSI.dim, ANSI.muted], options);
|
|
688
|
-
const rightPlain = '│';
|
|
689
|
-
const pad = Math.max(0, width - promptPlain.length - linePlain.length - rightPlain.length);
|
|
690
|
-
return `${prompt}${line}${' '.repeat(pad)}${rightBar}`;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function formatPermissionModeBrief(options = {}) {
|
|
694
|
-
return resolveBypassPermissions(options) ? 'full access' : 'approve';
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
function permissionAccentCodes(options = {}) {
|
|
698
|
-
return resolveBypassPermissions(options) ? [ANSI.dim, ANSI.warn] : [ANSI.dim, ANSI.safe];
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function formatPermissionToggleMessage(bypassPermissions, options = {}) {
|
|
702
|
-
const persistPrefs = options.persistPrefs !== false;
|
|
703
|
-
const suffix = persistPrefs ? ' (saved)' : ' (session)';
|
|
704
|
-
if (bypassPermissions) {
|
|
705
|
-
return paint(`· Full access${suffix} — external actions can run without approval`, [ANSI.warn], options);
|
|
706
|
-
}
|
|
707
|
-
return paint(`· Approve mode${suffix} — external actions ask before they run`, [ANSI.safe], options);
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function isPermissionToggleKey(key) {
|
|
711
|
-
if (!key) return false;
|
|
712
|
-
if (key.shift && key.name === 'tab') return true;
|
|
713
|
-
const sequence = String(key.sequence || '');
|
|
714
|
-
return sequence === '\x1b[Z' || sequence === '\x1b[1;2Z';
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function isMultilineInsertKey(str, key) {
|
|
718
|
-
if (!key) return str === '\n';
|
|
719
|
-
if (key.shift && (key.name === 'return' || key.name === 'enter')) return true;
|
|
720
|
-
if (key.meta && (key.name === 'return' || key.name === 'enter')) return true;
|
|
721
|
-
if (str === '\n' && key.name !== 'return') return true;
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function insertMultilineBreak(rl) {
|
|
726
|
-
if (!rl) return;
|
|
727
|
-
// Insert a literal newline at the cursor. rl.write('\n') would SUBMIT the
|
|
728
|
-
// line (readline treats it as Enter) and also re-enter our _ttyWrite hook.
|
|
729
|
-
if (typeof rl._insertString === 'function') {
|
|
730
|
-
rl._insertString('\n');
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
if (typeof rl.line === 'string') {
|
|
734
|
-
const cursor = Number.isFinite(rl.cursor) ? rl.cursor : rl.line.length;
|
|
735
|
-
rl.line = rl.line.slice(0, cursor) + '\n' + rl.line.slice(cursor);
|
|
736
|
-
rl.cursor = cursor + 1;
|
|
737
|
-
if (typeof rl._refreshLine === 'function') rl._refreshLine();
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function formatInputBoxTop(options = {}) {
|
|
742
|
-
return paint(buildInputBoxTopPlain(options), [ANSI.dim, ANSI.muted], options);
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
function formatInputBoxBottom(options = {}) {
|
|
746
|
-
return paint(buildInputBoxBottomPlain(options), [ANSI.dim, ANSI.muted], options);
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
// Status line shown below the box: permission mode + the shift+enter newline
|
|
750
|
-
// affordance, mode-colored. Drawn by the repaint, cleared on submit.
|
|
751
|
-
function formatInputBoxStatus(options = {}) {
|
|
752
|
-
const plain = buildInputBoxStatusPlain(options);
|
|
753
|
-
const modeLabel = formatPermissionModeBrief(options);
|
|
754
|
-
const idx = plain.indexOf(modeLabel);
|
|
755
|
-
if (idx === -1) return paint(plain, [ANSI.dim, ANSI.muted], options);
|
|
756
|
-
const modePainted = paint(modeLabel, permissionAccentCodes(options), options);
|
|
757
|
-
const dim = [ANSI.dim, ANSI.muted];
|
|
758
|
-
return `${paint(plain.slice(0, idx), dim, options)}${modePainted}${paint(plain.slice(idx + modeLabel.length), dim, options)}`;
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function closeInputBox(output, options = {}) {
|
|
762
|
-
// On submit the cursor sits on the bottom-rule row: redraw the rule, then
|
|
763
|
-
// clear the status line the repaint left below it and leave the cursor on a
|
|
764
|
-
// fresh row for the turn output.
|
|
765
|
-
output.write(`\r\x1b[2K${formatInputBoxBottom(options)}\n\x1b[2K\r`);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
// Keep the input box closed while the user is typing: after each readline
|
|
769
|
-
// render, repaint the bottom rule one row below the (possibly wrapped/multiline)
|
|
770
|
-
// input plus a status line below that, then return the cursor. readline clears
|
|
771
|
-
// to end-of-display on every refresh, so both must be redrawn each time. Writes
|
|
772
|
-
// to the raw stream readline renders to (not the logger) since these are
|
|
773
|
-
// transient cursor moves.
|
|
774
|
-
function repaintInputBoxBottom(rl, output, options = {}) {
|
|
775
|
-
if (!rl || !output || typeof output.write !== 'function') return;
|
|
776
|
-
if (typeof rl._getDisplayPos !== 'function' || typeof rl.getCursorPos !== 'function') return;
|
|
777
|
-
let total;
|
|
778
|
-
let cur;
|
|
779
|
-
try {
|
|
780
|
-
total = rl._getDisplayPos(`${rl._prompt || ''}${rl.line || ''}`);
|
|
781
|
-
cur = rl.getCursorPos();
|
|
782
|
-
} catch {
|
|
783
|
-
return;
|
|
784
|
-
}
|
|
785
|
-
if (!total || !cur) return;
|
|
786
|
-
// When the input is taller than the viewport the terminal scrolls and the
|
|
787
|
-
// relative cursor math below can't land the rule reliably (it would corrupt
|
|
788
|
-
// the scrollback). Degrade gracefully: skip the closing rule for a giant
|
|
789
|
-
// input rather than scramble it. readline still renders the text correctly.
|
|
790
|
-
const rows = Number(output.rows || (typeof process !== 'undefined' && process.stdout && process.stdout.rows) || 0);
|
|
791
|
-
if (rows && total.rows + 3 >= rows) return;
|
|
792
|
-
const down = Math.max(1, (total.rows - cur.rows) + 1);
|
|
793
|
-
let seq = `\r\x1b[${down}B\x1b[2K${formatInputBoxBottom(options)}`;
|
|
794
|
-
seq += `\r\x1b[1B\x1b[2K${formatInputBoxStatus(options)}`;
|
|
795
|
-
seq += `\x1b[${down + 1}A\r`;
|
|
796
|
-
if (cur.cols > 0) seq += `\x1b[${cur.cols}C`;
|
|
797
|
-
output.write(seq);
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
function formatChatInputInnerPrefix(mode, options = {}) {
|
|
801
|
-
const tier = normalizeMode(mode);
|
|
802
|
-
const goalHint = options.goal && options.goal.active
|
|
803
|
-
? `${paint('◎ ', [ANSI.bold, ANSI.magenta], options)}`
|
|
804
|
-
: '';
|
|
805
|
-
const tierPart = mode
|
|
806
|
-
? `${goalHint}${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `
|
|
807
|
-
: `${goalHint}› `;
|
|
808
|
-
return `${paint('→ ', [ANSI.muted], options)}${tierPart}`;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
function formatChatInputPrompt(mode, options = {}) {
|
|
812
|
-
// Two-space indent (no left bar) to match the plain rules and the status line.
|
|
813
|
-
return ` ${formatChatInputInnerPrefix(mode, options)}`;
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const PERMISSION_COMMANDS = new Map([
|
|
817
|
-
['/bypass', true],
|
|
818
|
-
['/safe', false],
|
|
819
|
-
]);
|
|
820
|
-
|
|
821
|
-
function chatPermissionCommand(line) {
|
|
822
|
-
const key = String(line || '').trim().toLowerCase();
|
|
823
|
-
return PERMISSION_COMMANDS.has(key) ? PERMISSION_COMMANDS.get(key) : null;
|
|
624
|
+
return `${paint(tier, [ANSI.bold, tierColor(tier)], options)} › `;
|
|
824
625
|
}
|
|
825
626
|
|
|
826
627
|
const TIER_COMMANDS = new Map([
|
|
@@ -834,70 +635,21 @@ function chatTierCommand(line) {
|
|
|
834
635
|
}
|
|
835
636
|
|
|
836
637
|
const CHAT_COMMANDS = [
|
|
837
|
-
['/goal', 'set/show autonomous completion condition'],
|
|
838
638
|
['/fast', 'quick answers, lowest latency'],
|
|
839
639
|
['/pro', 'deeper tool loop for real work'],
|
|
840
640
|
['/max', 'highest reasoning for the hardest jobs'],
|
|
841
|
-
['/bypass', 'allow external side effects (persisted)'],
|
|
842
|
-
['/safe', 'approval-only external actions (default, persisted)'],
|
|
843
641
|
['/help', 'show this menu'],
|
|
844
642
|
['exit', 'leave chat'],
|
|
845
643
|
];
|
|
846
644
|
|
|
847
|
-
const CHAT_SHORTCUTS = [
|
|
848
|
-
['shift+tab', 'toggle approve ↔ full access'],
|
|
849
|
-
['shift+enter', 'new line (enter sends)'],
|
|
850
|
-
];
|
|
851
|
-
|
|
852
|
-
const CHAT_DOGFOOD_BASE_CASES = [
|
|
853
|
-
{ input: '/help', kind: 'command', coverage: 'menu' },
|
|
854
|
-
{ input: '/fast', kind: 'command', coverage: 'tier_fast' },
|
|
855
|
-
{ input: 'hi', kind: 'turn', expectRoute: 'cloud', coverage: 'small_talk' },
|
|
856
|
-
{ input: 'pop', kind: 'turn', expectRoute: 'cloud', coverage: 'short_noise' },
|
|
857
|
-
{ input: 'hello why', kind: 'turn', expectRoute: 'cloud', coverage: 'greeting_phrase' },
|
|
858
|
-
{ input: 'what files are here?', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_read' },
|
|
859
|
-
{ input: 'search src for the input component', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_search' },
|
|
860
|
-
{ input: 'fix the xp game tests', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_fix' },
|
|
861
|
-
{ input: 'what is on my calendar today?', kind: 'turn', expectRoute: 'cloud', coverage: 'connector_read' },
|
|
862
|
-
{ input: 'which integrations are connected?', kind: 'turn', expectRoute: 'cloud', coverage: 'connector_status' },
|
|
863
|
-
{ input: 'what github repos do I have?', kind: 'turn', expectRoute: 'cloud', coverage: 'github_read' },
|
|
864
|
-
{ input: 'push something to github', kind: 'turn', expectRoute: 'cloud', coverage: 'github_workspace_mutation' },
|
|
865
|
-
{ input: 'https://www.youtube.com/watch?v=gBukk9LIklc', kind: 'youtube', coverage: 'youtube_url' },
|
|
866
|
-
{ input: '/max', kind: 'command', coverage: 'tier_max' },
|
|
867
|
-
{ input: 'refactor this module and verify the tests', kind: 'turn', expectRoute: 'cloud', expectMode: 'max', coverage: 'max_cloud' },
|
|
868
|
-
{ input: '/pro', kind: 'command', coverage: 'tier_pro' },
|
|
869
|
-
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectMode: 'pro', coverage: 'connector_write_safe' },
|
|
870
|
-
{ input: '/bypass', kind: 'command', coverage: 'permission_bypass' },
|
|
871
|
-
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectBypass: true, coverage: 'connector_write_bypass' },
|
|
872
|
-
{ input: '/safe', kind: 'command', coverage: 'permission_safe' },
|
|
873
|
-
{ input: 'send a slack message to the team', kind: 'turn', expectRoute: 'cloud', expectBypass: false, coverage: 'connector_write_resafe' },
|
|
874
|
-
{ input: '/wat', kind: 'command', coverage: 'unknown_slash' },
|
|
875
|
-
{ input: '/goal', kind: 'command', coverage: 'goal_status' },
|
|
876
|
-
{ input: 'oj', kind: 'turn', expectRoute: 'cloud', coverage: 'short_noise_repeat' },
|
|
877
|
-
{ input: 'read MAP.md and do not edit', kind: 'turn', expectRoute: 'cloud', coverage: 'workspace_read_specific' },
|
|
878
|
-
];
|
|
879
|
-
|
|
880
|
-
function buildChatDogfoodCases(loopCount = 25) {
|
|
881
|
-
const count = Math.max(1, Number(loopCount) || 25);
|
|
882
|
-
const cases = [];
|
|
883
|
-
for (let i = 0; i < count; i += 1) {
|
|
884
|
-
cases.push({ ...CHAT_DOGFOOD_BASE_CASES[i % CHAT_DOGFOOD_BASE_CASES.length], index: i + 1 });
|
|
885
|
-
}
|
|
886
|
-
return cases;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
645
|
function chatMenu(options = {}) {
|
|
890
|
-
|
|
646
|
+
return CHAT_COMMANDS
|
|
891
647
|
.map(([name, desc]) => {
|
|
892
648
|
const tier = TIER_COMMANDS.get(name);
|
|
893
649
|
const color = tier ? tierColor(tier) : ANSI.accent;
|
|
894
650
|
return ` ${paint(name.padEnd(6), [color], options)} ${paint(desc, [ANSI.muted], options)}`;
|
|
895
651
|
})
|
|
896
652
|
.join('\n');
|
|
897
|
-
const shortcuts = CHAT_SHORTCUTS
|
|
898
|
-
.map(([name, desc]) => ` ${paint(name.padEnd(6), [ANSI.ok], options)} ${paint(desc, [ANSI.muted], options)}`)
|
|
899
|
-
.join('\n');
|
|
900
|
-
return `${commands}\n${shortcuts}`;
|
|
901
653
|
}
|
|
902
654
|
|
|
903
655
|
function chatCompleter(line) {
|
|
@@ -908,132 +660,10 @@ function chatCompleter(line) {
|
|
|
908
660
|
return [hits.length ? hits : names, trimmed];
|
|
909
661
|
}
|
|
910
662
|
|
|
911
|
-
function
|
|
912
|
-
let bypassPermissions;
|
|
913
|
-
if (args.includes('--bypass')) bypassPermissions = true;
|
|
914
|
-
if (args.includes('--safe')) bypassPermissions = false;
|
|
915
|
-
return bypassPermissions;
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
function stripPermissionFlags(args) {
|
|
919
|
-
return args.filter(arg => arg !== '--bypass' && arg !== '--safe');
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
function normalizeChatCommandArgs(args = []) {
|
|
923
|
-
const normalized = [...args];
|
|
924
|
-
let firstPositional = -1;
|
|
925
|
-
const flagsWithValue = new Set(['--business', '--verify', '--loops']);
|
|
926
|
-
|
|
927
|
-
for (let i = 0; i < normalized.length; i += 1) {
|
|
928
|
-
const arg = String(normalized[i] || '');
|
|
929
|
-
if (flagsWithValue.has(arg)) {
|
|
930
|
-
i += 1;
|
|
931
|
-
continue;
|
|
932
|
-
}
|
|
933
|
-
if (arg.startsWith('-')) continue;
|
|
934
|
-
firstPositional = i;
|
|
935
|
-
break;
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
let chatRequested = normalized.includes('--chat');
|
|
939
|
-
if (firstPositional !== -1 && normalized[firstPositional] === 'chat') {
|
|
940
|
-
chatRequested = true;
|
|
941
|
-
normalized.splice(firstPositional, 1, '--chat');
|
|
942
|
-
const target = String(normalized[firstPositional + 1] || '').toLowerCase();
|
|
943
|
-
if (target === MEMBER_SLUG || target === 'ax') {
|
|
944
|
-
normalized.splice(firstPositional + 1, 1);
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
return { args: normalized, chatRequested };
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
function isAxSpawnCommand(command) {
|
|
952
|
-
return ['spawn', 'spawns', 'spawn-list', 'list-spawns', 'spawn-status', 'spawn-show'].includes(String(command || ''));
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
function runAxSpawnCommand(args = [], deps = {}) {
|
|
956
|
-
const [command, ...rest] = args;
|
|
957
|
-
const root = deps.root || process.cwd();
|
|
958
|
-
const output = deps.output || ((line = '') => console.log(line));
|
|
959
|
-
const {
|
|
960
|
-
agentSpawnCommand,
|
|
961
|
-
agentSpawnListCommand,
|
|
962
|
-
agentSpawnStatusCommand,
|
|
963
|
-
} = require('./commands/agent-spawn');
|
|
964
|
-
|
|
965
|
-
if (command === 'spawn') return agentSpawnCommand(rest, { root, output, commandName: 'ax spawn' });
|
|
966
|
-
if (command === 'spawns' || command === 'spawn-list' || command === 'list-spawns') {
|
|
967
|
-
return agentSpawnListCommand(rest, { root, output });
|
|
968
|
-
}
|
|
969
|
-
if (command === 'spawn-status' || command === 'spawn-show') {
|
|
970
|
-
return agentSpawnStatusCommand(rest, { root, output });
|
|
971
|
-
}
|
|
972
|
-
throw new Error('Unknown ax spawn command');
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const YOUTUBE_URL_PATTERN = /\bhttps?:\/\/(?:www\.)?(?:youtube\.com\/watch\?[^\s]+|youtu\.be\/[^\s]+|youtube\.com\/shorts\/[^\s]+)/i;
|
|
976
|
-
|
|
977
|
-
function cleanYoutubeUrl(url) {
|
|
978
|
-
return String(url || '').replace(/[),.;\]]+$/g, '');
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function extractYoutubeUrl(text) {
|
|
982
|
-
const match = String(text || '').match(YOUTUBE_URL_PATTERN);
|
|
983
|
-
return match ? cleanYoutubeUrl(match[0]) : null;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function youtubeQueryFromPrompt(prompt, youtubeUrl) {
|
|
987
|
-
const withoutUrl = String(prompt || '').replace(youtubeUrl, '').trim();
|
|
988
|
-
return withoutUrl.replace(/^[-:,\s]+|[-:,\s]+$/g, '').trim();
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
function buildAxYoutubeArgs(prompt, extraArgs = []) {
|
|
992
|
-
const text = Array.isArray(prompt) ? prompt.join(' ') : String(prompt || '');
|
|
993
|
-
const youtubeUrl = extractYoutubeUrl(text);
|
|
994
|
-
if (!youtubeUrl) return null;
|
|
995
|
-
const query = youtubeQueryFromPrompt(text, youtubeUrl);
|
|
996
|
-
const args = [youtubeUrl];
|
|
997
|
-
if (query) args.push('--query', query);
|
|
998
|
-
return args.concat(extraArgs || []);
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
async function runAxYoutubeCommand(args = [], deps = {}) {
|
|
1002
|
-
const output = deps.output || ((line = '') => console.log(line));
|
|
1003
|
-
const commandArgs = args[0] === 'youtube' ? args.slice(1) : args;
|
|
1004
|
-
const inferred = args[0] === 'youtube' ? null : buildAxYoutubeArgs(commandArgs);
|
|
1005
|
-
const finalArgs = inferred || commandArgs;
|
|
1006
|
-
const youtubeCommand = deps.youtubeCommand || require('./commands/youtube').youtubeCommand;
|
|
1007
|
-
const commandName = args[0] === 'youtube' ? 'ax youtube' : deps.commandName;
|
|
1008
|
-
return youtubeCommand(finalArgs, { ...deps, output, commandName });
|
|
1009
|
-
}
|
|
1010
|
-
|
|
1011
|
-
function normalizeWorkingOptions(frameOrOptions, maybeOptions = {}) {
|
|
1012
|
-
if (typeof frameOrOptions === 'string') {
|
|
1013
|
-
return { ...maybeOptions, frameChar: frameOrOptions };
|
|
1014
|
-
}
|
|
1015
|
-
if (frameOrOptions && typeof frameOrOptions === 'object') {
|
|
1016
|
-
return frameOrOptions;
|
|
1017
|
-
}
|
|
1018
|
-
return maybeOptions;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
function formatWorkingLine(ms, verb, frameOrOptions, maybeOptions = {}) {
|
|
1022
|
-
const options = normalizeWorkingOptions(frameOrOptions, maybeOptions);
|
|
663
|
+
function formatWorkingLine(ms, verb, frame) {
|
|
1023
664
|
const totalSeconds = Math.max(1, Math.round((Number(ms) || 0) / 1000));
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
const meta = paint(
|
|
1027
|
-
`(${formatSeconds(totalSeconds)} · ctrl-c to interrupt)`,
|
|
1028
|
-
[ANSI.dim, ANSI.muted],
|
|
1029
|
-
options
|
|
1030
|
-
);
|
|
1031
|
-
const label = tick === null
|
|
1032
|
-
? paint(labelVerb, [ANSI.muted], options)
|
|
1033
|
-
: renderShimmerText(labelVerb, tick ?? 0, options);
|
|
1034
|
-
const suffix = paint('…', [ANSI.dim, ANSI.muted], options);
|
|
1035
|
-
const prefix = paint('•', [ANSI.muted], options);
|
|
1036
|
-
return `${prefix} ${label}${suffix} ${meta}`;
|
|
665
|
+
if (verb) return `${frame || '•'} ${verb}… (${formatSeconds(totalSeconds)} · ctrl-c to interrupt)`;
|
|
666
|
+
return `• Working (${formatSeconds(totalSeconds)} • ctrl-c to interrupt)`;
|
|
1037
667
|
}
|
|
1038
668
|
|
|
1039
669
|
function verbForTool(tool) {
|
|
@@ -1042,8 +672,7 @@ function verbForTool(tool) {
|
|
|
1042
672
|
if (name.includes('grep') || name.includes('glob') || name.includes('search')) return 'Searching';
|
|
1043
673
|
if (name.includes('bash') || name.includes('command') || name.includes('run')) return 'Running';
|
|
1044
674
|
if (name.includes('write') || name.includes('edit')) return 'Editing';
|
|
1045
|
-
|
|
1046
|
-
return 'Thinking';
|
|
675
|
+
return 'Working';
|
|
1047
676
|
}
|
|
1048
677
|
|
|
1049
678
|
function formatDoneLine(ms, credits) {
|
|
@@ -1065,18 +694,7 @@ function paint(text, codes, options = {}) {
|
|
|
1065
694
|
return `${codes.join('')}${text}${ANSI.reset}`;
|
|
1066
695
|
}
|
|
1067
696
|
|
|
1068
|
-
function
|
|
1069
|
-
const display = String(label || '').trim();
|
|
1070
|
-
const href = String(url || '').trim();
|
|
1071
|
-
if (!href) return display;
|
|
1072
|
-
const colored = paint(display, [ANSI.accent, ANSI.underline], options);
|
|
1073
|
-
if (useColor(options) && (/^(https?:\/\/|file:\/\/)/i.test(href) || href.startsWith('/'))) {
|
|
1074
|
-
return hyperlinkPath(colored, href, options);
|
|
1075
|
-
}
|
|
1076
|
-
return `${display} (${href})`;
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function renderTerminalInline(text, options = {}) {
|
|
697
|
+
function renderTerminalMarkdown(text, options = {}) {
|
|
1080
698
|
let rendered = String(text || '');
|
|
1081
699
|
const codeSpans = [];
|
|
1082
700
|
rendered = rendered.replace(/`([^`\n]+)`/g, (_, code) => {
|
|
@@ -1084,131 +702,23 @@ function renderTerminalInline(text, options = {}) {
|
|
|
1084
702
|
codeSpans.push(code);
|
|
1085
703
|
return token;
|
|
1086
704
|
});
|
|
1087
|
-
rendered = rendered.replace(
|
|
1088
|
-
rendered = rendered.replace(/\*\*\*([^*\n]+)\*\*\*/g, (_, value) => paint(value, [ANSI.bold, ANSI.italic], options));
|
|
705
|
+
rendered = rendered.replace(/^#{1,6}\s+(.+)$/gm, (_, title) => paint(title, [ANSI.bold], options));
|
|
1089
706
|
rendered = rendered.replace(/\*\*([^*\n]+)\*\*/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
1090
707
|
rendered = rendered.replace(/__([^_\n]+)__/g, (_, value) => paint(value, [ANSI.bold], options));
|
|
1091
|
-
rendered = rendered.replace(/~~([^~\n]+)~~/g, (_, value) => paint(value, [ANSI.strike], options));
|
|
1092
|
-
rendered = rendered.replace(/\*([^*\n]+)\*/g, (_, value) => paint(value, [ANSI.italic], options));
|
|
1093
|
-
rendered = rendered.replace(/_([^_\n]+)_/g, (_, value) => paint(value, [ANSI.italic], options));
|
|
1094
708
|
rendered = rendered.replace(/\u0000CODE(\d+)\u0000/g, (_, index) => paint(codeSpans[Number(index)] || '', [ANSI.accent], options));
|
|
1095
709
|
return rendered;
|
|
1096
710
|
}
|
|
1097
711
|
|
|
1098
|
-
function renderTerminalBlockLine(line, options = {}) {
|
|
1099
|
-
const header = line.match(/^(#{1,6})\s+(.+)$/);
|
|
1100
|
-
if (header) return paint(header[2], [ANSI.bold], options);
|
|
1101
|
-
|
|
1102
|
-
const list = line.match(/^(\s*)(?:[*+-]|\d+\.)\s+(.*)$/);
|
|
1103
|
-
if (list) {
|
|
1104
|
-
const bullet = paint('•', [ANSI.accent], options);
|
|
1105
|
-
return `${list[1]}${bullet} ${renderTerminalInline(list[2], options)}`;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
const quote = line.match(/^>\s?(.*)$/);
|
|
1109
|
-
if (quote) {
|
|
1110
|
-
return `${paint('▏ ', [ANSI.muted], options)}${renderTerminalInline(quote[1], options)}`;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (/^(\*{3,}|-{3,}|_{3,})$/.test(line.trim())) {
|
|
1114
|
-
const width = Math.min(Math.max(16, terminalWidth(options) - 4), 48);
|
|
1115
|
-
return paint('─'.repeat(width), [ANSI.dim, ANSI.muted], options);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
return renderTerminalInline(line, options);
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
function renderTerminalMarkdown(text, options = {}) {
|
|
1122
|
-
const source = String(text || '');
|
|
1123
|
-
const parts = [];
|
|
1124
|
-
const fence = /```[^\n]*\n([\s\S]*?)```/g;
|
|
1125
|
-
let last = 0;
|
|
1126
|
-
let match = fence.exec(source);
|
|
1127
|
-
while (match) {
|
|
1128
|
-
if (match.index > last) {
|
|
1129
|
-
parts.push({ kind: 'text', value: source.slice(last, match.index) });
|
|
1130
|
-
}
|
|
1131
|
-
parts.push({ kind: 'code', value: match[1] || '' });
|
|
1132
|
-
last = match.index + match[0].length;
|
|
1133
|
-
match = fence.exec(source);
|
|
1134
|
-
}
|
|
1135
|
-
if (last < source.length) parts.push({ kind: 'text', value: source.slice(last) });
|
|
1136
|
-
|
|
1137
|
-
return parts.map((part) => {
|
|
1138
|
-
if (part.kind === 'code') {
|
|
1139
|
-
return part.value
|
|
1140
|
-
.replace(/\n$/, '')
|
|
1141
|
-
.split('\n')
|
|
1142
|
-
.map((line) => paint(` ${line}`, [ANSI.muted], options))
|
|
1143
|
-
.join('\n');
|
|
1144
|
-
}
|
|
1145
|
-
return part.value
|
|
1146
|
-
.split('\n')
|
|
1147
|
-
.map((line) => renderTerminalBlockLine(line, options))
|
|
1148
|
-
.join('\n');
|
|
1149
|
-
}).join('\n');
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function renderFenceBuffer(buffer, options = {}) {
|
|
1153
|
-
return String(buffer || '')
|
|
1154
|
-
.replace(/\n$/, '')
|
|
1155
|
-
.split('\n')
|
|
1156
|
-
.map((line) => paint(` ${line}`, [ANSI.muted], options))
|
|
1157
|
-
.join('\n');
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
712
|
function resetMarkdownState(state) {
|
|
1161
713
|
state.markdownMode = 'normal';
|
|
1162
714
|
state.markdownBuffer = '';
|
|
1163
715
|
state.markdownCarry = '';
|
|
1164
|
-
state.markdownDelim = '';
|
|
1165
|
-
state.atLineStart = true;
|
|
1166
716
|
}
|
|
1167
717
|
|
|
1168
718
|
function ensureMarkdownState(state) {
|
|
1169
719
|
if (!state.markdownMode) state.markdownMode = 'normal';
|
|
1170
720
|
if (typeof state.markdownBuffer !== 'string') state.markdownBuffer = '';
|
|
1171
721
|
if (typeof state.markdownCarry !== 'string') state.markdownCarry = '';
|
|
1172
|
-
if (typeof state.markdownDelim !== 'string') state.markdownDelim = '';
|
|
1173
|
-
if (typeof state.atLineStart !== 'boolean') state.atLineStart = true;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
function streamingListMarkerAt(input, index) {
|
|
1177
|
-
const rest = input.slice(index);
|
|
1178
|
-
const match = rest.match(/^(\s*)(?:[*+-]|\d+\.)\s+/);
|
|
1179
|
-
if (!match) return null;
|
|
1180
|
-
return { length: match[0].length, indent: match[1] || '' };
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function streamingHeadingAt(input, index) {
|
|
1184
|
-
const rest = input.slice(index);
|
|
1185
|
-
const match = rest.match(/^#{1,6}\s+([^\n]*)/);
|
|
1186
|
-
if (!match) return null;
|
|
1187
|
-
return { length: match[0].length, title: match[1] || '' };
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
function streamingBlockquoteAt(input, index) {
|
|
1191
|
-
const rest = input.slice(index);
|
|
1192
|
-
const match = rest.match(/^>\s?/);
|
|
1193
|
-
if (!match) return null;
|
|
1194
|
-
return { length: match[0].length };
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function streamingHorizontalRuleAt(input, index) {
|
|
1198
|
-
const rest = input.slice(index);
|
|
1199
|
-
const match = rest.match(/^(-{3,}|\*{3,}|_{3,})(?=\n|$)/);
|
|
1200
|
-
if (!match) return null;
|
|
1201
|
-
return { length: match[0].length };
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
function streamingLinkAt(input, index, options) {
|
|
1205
|
-
const rest = input.slice(index);
|
|
1206
|
-
const match = rest.match(/^\[([^\]]+)\]\(([^)]+)\)/);
|
|
1207
|
-
if (!match) return null;
|
|
1208
|
-
return {
|
|
1209
|
-
length: match[0].length,
|
|
1210
|
-
rendered: formatMarkdownLink(match[1], match[2], options),
|
|
1211
|
-
};
|
|
1212
722
|
}
|
|
1213
723
|
|
|
1214
724
|
function renderStreamingMarkdown(state, text, options = {}) {
|
|
@@ -1218,239 +728,43 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
1218
728
|
let out = '';
|
|
1219
729
|
|
|
1220
730
|
for (let i = 0; i < input.length;) {
|
|
1221
|
-
if (state.markdownMode === 'fence') {
|
|
1222
|
-
if (state.atLineStart) {
|
|
1223
|
-
const rest = input.slice(i);
|
|
1224
|
-
if (/^```/.test(rest)) {
|
|
1225
|
-
out += renderFenceBuffer(state.markdownBuffer, options);
|
|
1226
|
-
state.markdownBuffer = '';
|
|
1227
|
-
state.markdownMode = 'normal';
|
|
1228
|
-
const nl = rest.indexOf('\n');
|
|
1229
|
-
if (nl === -1) { i = input.length; state.atLineStart = true; continue; }
|
|
1230
|
-
out += '\n';
|
|
1231
|
-
i += nl + 1;
|
|
1232
|
-
state.atLineStart = true;
|
|
1233
|
-
continue;
|
|
1234
|
-
}
|
|
1235
|
-
if (/^`{1,3}$/.test(rest)) { state.markdownCarry = rest; break; }
|
|
1236
|
-
}
|
|
1237
|
-
const c = input[i];
|
|
1238
|
-
state.markdownBuffer += c;
|
|
1239
|
-
state.atLineStart = c === '\n';
|
|
1240
|
-
i += 1;
|
|
1241
|
-
continue;
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
if (state.markdownMode === 'normal' && state.atLineStart) {
|
|
1245
|
-
const rest = input.slice(i);
|
|
1246
|
-
// Code fence ```lang … ``` — wait for the full opening line, then stream
|
|
1247
|
-
// the body as an indented block (handled by the 'fence' branch above).
|
|
1248
|
-
if (/^```/.test(rest)) {
|
|
1249
|
-
const nl = rest.indexOf('\n');
|
|
1250
|
-
if (nl === -1) { state.markdownCarry = rest; break; }
|
|
1251
|
-
state.markdownMode = 'fence';
|
|
1252
|
-
state.markdownBuffer = '';
|
|
1253
|
-
state.atLineStart = true;
|
|
1254
|
-
i += nl + 1;
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
// Incomplete block marker at the end of this delta (heading ##, fence ```,
|
|
1258
|
-
// bullet - / +, numbered 1.) — carry it so the next delta completes the
|
|
1259
|
-
// marker instead of leaking it as plain text.
|
|
1260
|
-
if (/^(?:#{1,6}|`{1,3}|[-+]|\d{1,9}\.)$/.test(rest)) { state.markdownCarry = rest; break; }
|
|
1261
|
-
|
|
1262
|
-
const heading = streamingHeadingAt(input, i);
|
|
1263
|
-
if (heading) {
|
|
1264
|
-
out += paint(heading.title, [ANSI.bold], options);
|
|
1265
|
-
i += heading.length;
|
|
1266
|
-
if (input[i] === '\n') {
|
|
1267
|
-
out += '\n';
|
|
1268
|
-
i += 1;
|
|
1269
|
-
state.atLineStart = true;
|
|
1270
|
-
} else {
|
|
1271
|
-
state.atLineStart = false;
|
|
1272
|
-
}
|
|
1273
|
-
continue;
|
|
1274
|
-
}
|
|
1275
|
-
const list = streamingListMarkerAt(input, i);
|
|
1276
|
-
if (list) {
|
|
1277
|
-
out += `${list.indent}${paint('•', [ANSI.accent], options)} `;
|
|
1278
|
-
i += list.length;
|
|
1279
|
-
state.atLineStart = false;
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
const quote = streamingBlockquoteAt(input, i);
|
|
1283
|
-
if (quote) {
|
|
1284
|
-
out += paint('▏ ', [ANSI.muted], options);
|
|
1285
|
-
i += quote.length;
|
|
1286
|
-
state.atLineStart = false;
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
const hr = streamingHorizontalRuleAt(input, i);
|
|
1290
|
-
if (hr) {
|
|
1291
|
-
const width = Math.min(Math.max(16, terminalWidth(options) - 4), 48);
|
|
1292
|
-
out += paint('─'.repeat(width), [ANSI.dim, ANSI.muted], options);
|
|
1293
|
-
i += hr.length;
|
|
1294
|
-
if (input[i] === '\n') {
|
|
1295
|
-
out += '\n';
|
|
1296
|
-
i += 1;
|
|
1297
|
-
state.atLineStart = true;
|
|
1298
|
-
} else {
|
|
1299
|
-
state.atLineStart = false;
|
|
1300
|
-
}
|
|
1301
|
-
continue;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
731
|
const char = input[i];
|
|
1306
732
|
const next = input[i + 1];
|
|
1307
|
-
const third = input[i + 2];
|
|
1308
733
|
|
|
1309
734
|
if (state.markdownMode === 'normal') {
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
out += link.rendered;
|
|
1313
|
-
i += link.length;
|
|
1314
|
-
state.atLineStart = false;
|
|
1315
|
-
continue;
|
|
1316
|
-
}
|
|
1317
|
-
if (char === '[') {
|
|
1318
|
-
state.markdownCarry = '[';
|
|
735
|
+
if (char === '*' && next === undefined) {
|
|
736
|
+
state.markdownCarry = '*';
|
|
1319
737
|
break;
|
|
1320
738
|
}
|
|
1321
|
-
if (char === '*' && next === '*' && third === '*') {
|
|
1322
|
-
state.markdownMode = 'bold_italic';
|
|
1323
|
-
state.markdownBuffer = '';
|
|
1324
|
-
state.markdownDelim = '***';
|
|
1325
|
-
i += 3;
|
|
1326
|
-
continue;
|
|
1327
|
-
}
|
|
1328
739
|
if (char === '*' && next === '*') {
|
|
1329
740
|
state.markdownMode = 'bold';
|
|
1330
741
|
state.markdownBuffer = '';
|
|
1331
|
-
state.markdownDelim = '**';
|
|
1332
|
-
i += 2;
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
if (char === '*') {
|
|
1336
|
-
if (next === undefined) {
|
|
1337
|
-
state.markdownCarry = '*';
|
|
1338
|
-
break;
|
|
1339
|
-
}
|
|
1340
|
-
state.markdownMode = 'italic';
|
|
1341
|
-
state.markdownBuffer = '';
|
|
1342
|
-
state.markdownDelim = '*';
|
|
1343
|
-
i += 1;
|
|
1344
|
-
continue;
|
|
1345
|
-
}
|
|
1346
|
-
if (char === '_' && next === '_') {
|
|
1347
|
-
state.markdownMode = 'bold';
|
|
1348
|
-
state.markdownBuffer = '';
|
|
1349
|
-
state.markdownDelim = '__';
|
|
1350
|
-
i += 2;
|
|
1351
|
-
continue;
|
|
1352
|
-
}
|
|
1353
|
-
if (char === '_') {
|
|
1354
|
-
if (next === undefined) {
|
|
1355
|
-
state.markdownCarry = '_';
|
|
1356
|
-
break;
|
|
1357
|
-
}
|
|
1358
|
-
state.markdownMode = 'italic';
|
|
1359
|
-
state.markdownBuffer = '';
|
|
1360
|
-
state.markdownDelim = '_';
|
|
1361
|
-
i += 1;
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1364
|
-
if (char === '~' && next === '~') {
|
|
1365
|
-
state.markdownMode = 'strike';
|
|
1366
|
-
state.markdownBuffer = '';
|
|
1367
|
-
state.markdownDelim = '~~';
|
|
1368
742
|
i += 2;
|
|
1369
743
|
continue;
|
|
1370
744
|
}
|
|
1371
745
|
if (char === '`') {
|
|
1372
746
|
state.markdownMode = 'code';
|
|
1373
747
|
state.markdownBuffer = '';
|
|
1374
|
-
state.markdownDelim = '`';
|
|
1375
748
|
i += 1;
|
|
1376
749
|
continue;
|
|
1377
750
|
}
|
|
1378
751
|
out += char;
|
|
1379
|
-
if (char === '\n') state.atLineStart = true;
|
|
1380
|
-
else state.atLineStart = false;
|
|
1381
752
|
i += 1;
|
|
1382
753
|
continue;
|
|
1383
754
|
}
|
|
1384
755
|
|
|
1385
|
-
if (state.markdownMode === '
|
|
1386
|
-
if (char === '*' && next === '*' && third === '*') {
|
|
1387
|
-
out += paint(state.markdownBuffer, [ANSI.bold, ANSI.italic], options);
|
|
1388
|
-
state.markdownMode = 'normal';
|
|
1389
|
-
state.markdownBuffer = '';
|
|
1390
|
-
state.markdownDelim = '';
|
|
1391
|
-
i += 3;
|
|
1392
|
-
continue;
|
|
1393
|
-
}
|
|
756
|
+
if (state.markdownMode === 'bold') {
|
|
1394
757
|
if (char === '*' && next === undefined) {
|
|
1395
758
|
state.markdownCarry = '*';
|
|
1396
759
|
break;
|
|
1397
760
|
}
|
|
1398
|
-
|
|
1399
|
-
i += 1;
|
|
1400
|
-
continue;
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
if (state.markdownMode === 'bold') {
|
|
1404
|
-
const close = state.markdownDelim === '__' ? (char === '_' && next === '_') : (char === '*' && next === '*');
|
|
1405
|
-
if (close) {
|
|
761
|
+
if (char === '*' && next === '*') {
|
|
1406
762
|
out += paint(state.markdownBuffer, [ANSI.bold], options);
|
|
1407
763
|
state.markdownMode = 'normal';
|
|
1408
764
|
state.markdownBuffer = '';
|
|
1409
|
-
state.markdownDelim = '';
|
|
1410
765
|
i += 2;
|
|
1411
766
|
continue;
|
|
1412
767
|
}
|
|
1413
|
-
if ((char === '*' || char === '_') && next === undefined) {
|
|
1414
|
-
state.markdownCarry = char;
|
|
1415
|
-
break;
|
|
1416
|
-
}
|
|
1417
|
-
state.markdownBuffer += char;
|
|
1418
|
-
i += 1;
|
|
1419
|
-
continue;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
if (state.markdownMode === 'italic') {
|
|
1423
|
-
const close = char === state.markdownDelim && next !== state.markdownDelim;
|
|
1424
|
-
if (close) {
|
|
1425
|
-
out += paint(state.markdownBuffer, [ANSI.italic], options);
|
|
1426
|
-
state.markdownMode = 'normal';
|
|
1427
|
-
state.markdownBuffer = '';
|
|
1428
|
-
state.markdownDelim = '';
|
|
1429
|
-
i += 1;
|
|
1430
|
-
continue;
|
|
1431
|
-
}
|
|
1432
|
-
if (char === state.markdownDelim && next === undefined) {
|
|
1433
|
-
state.markdownCarry = char;
|
|
1434
|
-
break;
|
|
1435
|
-
}
|
|
1436
|
-
state.markdownBuffer += char;
|
|
1437
|
-
i += 1;
|
|
1438
|
-
continue;
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
if (state.markdownMode === 'strike') {
|
|
1442
|
-
if (char === '~' && next === '~') {
|
|
1443
|
-
out += paint(state.markdownBuffer, [ANSI.strike], options);
|
|
1444
|
-
state.markdownMode = 'normal';
|
|
1445
|
-
state.markdownBuffer = '';
|
|
1446
|
-
state.markdownDelim = '';
|
|
1447
|
-
i += 2;
|
|
1448
|
-
continue;
|
|
1449
|
-
}
|
|
1450
|
-
if (char === '~' && next === undefined) {
|
|
1451
|
-
state.markdownCarry = '~';
|
|
1452
|
-
break;
|
|
1453
|
-
}
|
|
1454
768
|
state.markdownBuffer += char;
|
|
1455
769
|
i += 1;
|
|
1456
770
|
continue;
|
|
@@ -1476,19 +790,10 @@ function renderStreamingMarkdown(state, text, options = {}) {
|
|
|
1476
790
|
function flushStreamingMarkdown(state, output) {
|
|
1477
791
|
ensureMarkdownState(state);
|
|
1478
792
|
let out = state.markdownCarry || '';
|
|
1479
|
-
state.
|
|
1480
|
-
if (state.markdownMode === 'bold_italic' && state.markdownBuffer) {
|
|
1481
|
-
out += paint(state.markdownBuffer, [ANSI.bold, ANSI.italic], output);
|
|
1482
|
-
} else if (state.markdownMode === 'bold' && state.markdownBuffer) {
|
|
793
|
+
if (state.markdownMode === 'bold' && state.markdownBuffer) {
|
|
1483
794
|
out += paint(state.markdownBuffer, [ANSI.bold], output);
|
|
1484
|
-
} else if (state.markdownMode === 'italic' && state.markdownBuffer) {
|
|
1485
|
-
out += paint(state.markdownBuffer, [ANSI.italic], output);
|
|
1486
|
-
} else if (state.markdownMode === 'strike' && state.markdownBuffer) {
|
|
1487
|
-
out += paint(state.markdownBuffer, [ANSI.strike], output);
|
|
1488
795
|
} else if (state.markdownMode === 'code' && state.markdownBuffer) {
|
|
1489
796
|
out += paint(state.markdownBuffer, [ANSI.accent], output);
|
|
1490
|
-
} else if (state.markdownMode === 'fence' && state.markdownBuffer) {
|
|
1491
|
-
out += renderFenceBuffer(state.markdownBuffer, output);
|
|
1492
797
|
}
|
|
1493
798
|
resetMarkdownState(state);
|
|
1494
799
|
if (out) output.write(out);
|
|
@@ -1538,26 +843,21 @@ function createProgressReporter(output, options = {}) {
|
|
|
1538
843
|
const enabled = options.showProgress !== false;
|
|
1539
844
|
const isTty = Boolean(output && output.isTTY);
|
|
1540
845
|
const startedAt = Date.now();
|
|
846
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
1541
847
|
let frameIndex = 0;
|
|
1542
|
-
let verb = '
|
|
848
|
+
let verb = 'Working';
|
|
1543
849
|
let interval = null;
|
|
1544
850
|
let timeout = null;
|
|
1545
851
|
let shown = false;
|
|
1546
852
|
let closed = false;
|
|
1547
|
-
const renderOptions = () => ({
|
|
1548
|
-
...output,
|
|
1549
|
-
color: options.color,
|
|
1550
|
-
tick: frameIndex,
|
|
1551
|
-
frameIndex,
|
|
1552
|
-
});
|
|
1553
853
|
|
|
1554
854
|
const render = () => {
|
|
1555
855
|
if (!enabled || closed) return;
|
|
1556
856
|
if (isTty) {
|
|
1557
|
-
|
|
1558
|
-
|
|
857
|
+
const frame = paint(frames[frameIndex++ % frames.length], [ANSI.accent], output);
|
|
858
|
+
output.write(`\r${formatWorkingLine(Date.now() - startedAt, verb, frame)}\x1b[K`);
|
|
1559
859
|
} else if (!shown) {
|
|
1560
|
-
output.write(`${formatWorkingLine(Date.now() - startedAt
|
|
860
|
+
output.write(`${formatWorkingLine(Date.now() - startedAt)}\n`);
|
|
1561
861
|
}
|
|
1562
862
|
shown = true;
|
|
1563
863
|
};
|
|
@@ -1567,11 +867,11 @@ function createProgressReporter(output, options = {}) {
|
|
|
1567
867
|
if (!enabled) return;
|
|
1568
868
|
timeout = setTimeout(() => {
|
|
1569
869
|
render();
|
|
1570
|
-
if (isTty) interval = setInterval(render,
|
|
1571
|
-
},
|
|
870
|
+
if (isTty) interval = setInterval(render, 120);
|
|
871
|
+
}, 700);
|
|
1572
872
|
},
|
|
1573
873
|
setVerb(next) {
|
|
1574
|
-
verb = next || '
|
|
874
|
+
verb = next || 'Working';
|
|
1575
875
|
},
|
|
1576
876
|
clear() {
|
|
1577
877
|
if (!isTty || !shown || closed) return;
|
|
@@ -1588,17 +888,6 @@ function createProgressReporter(output, options = {}) {
|
|
|
1588
888
|
};
|
|
1589
889
|
}
|
|
1590
890
|
|
|
1591
|
-
function manageTurnInterrupt(onInterrupt, options = {}) {
|
|
1592
|
-
const trigger = () => onInterrupt();
|
|
1593
|
-
const register = options.registerAbort;
|
|
1594
|
-
if (typeof register === 'function') {
|
|
1595
|
-
register(trigger);
|
|
1596
|
-
return () => register(null);
|
|
1597
|
-
}
|
|
1598
|
-
process.once('SIGINT', trigger);
|
|
1599
|
-
return () => process.removeListener('SIGINT', trigger);
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
891
|
function parseSseBlock(block) {
|
|
1603
892
|
const data = String(block || '')
|
|
1604
893
|
.split(/\r?\n/)
|
|
@@ -1695,6 +984,428 @@ function formatSystemInit(event, options = {}) {
|
|
|
1695
984
|
return parts.length ? parts.join(' ') : null;
|
|
1696
985
|
}
|
|
1697
986
|
|
|
987
|
+
function formatApprovalTime(value) {
|
|
988
|
+
const match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/);
|
|
989
|
+
if (!match) return '';
|
|
990
|
+
const [, year, month, day, rawHour, minute] = match;
|
|
991
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
992
|
+
const hour24 = Number(rawHour);
|
|
993
|
+
const hour12 = hour24 % 12 || 12;
|
|
994
|
+
const ampm = hour24 >= 12 ? 'PM' : 'AM';
|
|
995
|
+
return `${months[Number(month) - 1] || month} ${Number(day)}, ${year} at ${hour12}:${minute} ${ampm}`;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function formatApprovalReceipt(receipt) {
|
|
999
|
+
if (!receipt || typeof receipt !== 'object') return null;
|
|
1000
|
+
const events = Array.isArray(receipt.tool_events) ? receipt.tool_events : [];
|
|
1001
|
+
const approvalEvent = events.find(event => event && event.approval_request);
|
|
1002
|
+
const request = approvalEvent && approvalEvent.approval_request;
|
|
1003
|
+
if (!request || request.status !== 'approval_required') return null;
|
|
1004
|
+
|
|
1005
|
+
const accept = receipt.task_accept_receipt || {};
|
|
1006
|
+
const actionType = request.executor_action_type || request.action_type || '';
|
|
1007
|
+
if (actionType === 'google_calendar_create_event' || accept.task === 'calendar.create_event') {
|
|
1008
|
+
const payload = request.payload || {};
|
|
1009
|
+
const start = accept.start || (payload.start && payload.start.dateTime) || (approvalEvent.preview && approvalEvent.preview.start);
|
|
1010
|
+
const when = formatApprovalTime(start);
|
|
1011
|
+
const title = taskPreviewTitle(receipt) || accept.title || (approvalEvent.preview && approvalEvent.preview.summary) || 'calendar event';
|
|
1012
|
+
return `Approval needed before creation: calendar event "${String(title).slice(0, 80)}"${when ? ` ${when}` : ''}`;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const connector = String(request.connector || approvalEvent.tool || 'connector').replace(/_/g, ' ');
|
|
1016
|
+
const action = String(request.action || actionType || 'action').replace(/_/g, ' ');
|
|
1017
|
+
return `Approval needed before ${action}: ${connector}`;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function taskPreviewApprovalEvent(receipt) {
|
|
1021
|
+
const events = Array.isArray(receipt && receipt.tool_events) ? receipt.tool_events : [];
|
|
1022
|
+
return events.find(event => event && event.approval_request) || null;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function taskPreviewTitle(receipt) {
|
|
1026
|
+
const taskPreview = receipt && receipt.task_preview && typeof receipt.task_preview === 'object' ? receipt.task_preview : {};
|
|
1027
|
+
const slots = taskPreview.slots && typeof taskPreview.slots === 'object' ? taskPreview.slots : {};
|
|
1028
|
+
const accept = receipt && receipt.task_accept_receipt && typeof receipt.task_accept_receipt === 'object' ? receipt.task_accept_receipt : {};
|
|
1029
|
+
const approvalEvent = taskPreviewApprovalEvent(receipt);
|
|
1030
|
+
return String(
|
|
1031
|
+
slots.title
|
|
1032
|
+
|| accept.title
|
|
1033
|
+
|| (approvalEvent && approvalEvent.preview && approvalEvent.preview.summary)
|
|
1034
|
+
|| ''
|
|
1035
|
+
).trim();
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function formatTaskPreviewPlan(receipt) {
|
|
1039
|
+
const taskPreview = receipt && receipt.task_preview && typeof receipt.task_preview === 'object' ? receipt.task_preview : null;
|
|
1040
|
+
if (!taskPreview) return null;
|
|
1041
|
+
const task = String(taskPreview.task || '').trim();
|
|
1042
|
+
const slots = taskPreview.slots && typeof taskPreview.slots === 'object' ? taskPreview.slots : {};
|
|
1043
|
+
if (task === 'calendar.create_event') {
|
|
1044
|
+
const title = taskPreviewTitle(receipt);
|
|
1045
|
+
const start = slots.start || (receipt.task_accept_receipt && receipt.task_accept_receipt.start);
|
|
1046
|
+
const when = formatApprovalTime(start);
|
|
1047
|
+
return ['create calendar event', title ? `"${title.slice(0, 80)}"` : '', when].filter(Boolean).join(' ');
|
|
1048
|
+
}
|
|
1049
|
+
return task ? task.replace(/[._-]/g, ' ') : null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function formatTaskPreviewCheck(receipt) {
|
|
1053
|
+
const taskPreview = receipt && receipt.task_preview && typeof receipt.task_preview === 'object' ? receipt.task_preview : null;
|
|
1054
|
+
if (!taskPreview) return null;
|
|
1055
|
+
const status = String(taskPreview.status || '').trim();
|
|
1056
|
+
const missing = Array.isArray(taskPreview.missing)
|
|
1057
|
+
? taskPreview.missing.map(item => String(item || '').trim()).filter(Boolean)
|
|
1058
|
+
: [];
|
|
1059
|
+
if (missing.length) return `needs ${missing.join(', ')}`;
|
|
1060
|
+
if (status === 'approval_required') return 'approval required';
|
|
1061
|
+
if (status === 'needs_input') return 'needs input';
|
|
1062
|
+
return status ? status.replace(/_/g, ' ') : null;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function formatTaskPreviewRows(receipt, options = {}) {
|
|
1066
|
+
const taskPreview = receipt && receipt.task_preview && typeof receipt.task_preview === 'object' ? receipt.task_preview : null;
|
|
1067
|
+
if (!taskPreview) return [];
|
|
1068
|
+
const owner = String(taskPreview.owner_member || taskPreview.owner || '').trim();
|
|
1069
|
+
const plan = formatTaskPreviewPlan(receipt);
|
|
1070
|
+
const check = formatTaskPreviewCheck(receipt);
|
|
1071
|
+
return [
|
|
1072
|
+
owner ? formatAuxRow('owner', owner, options) : null,
|
|
1073
|
+
plan ? formatAuxRow('plan', plan, options) : null,
|
|
1074
|
+
check ? formatAuxRow('check', check, options) : null,
|
|
1075
|
+
].filter(Boolean);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function approvalStorePath(options = {}) {
|
|
1079
|
+
return options.storePath || process.env.AX_APPROVAL_STORE || path.join(os.homedir(), '.atris', 'ax-approvals.json');
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
function approvalMaxAgeMs(options = {}) {
|
|
1083
|
+
const value = Number(options.maxAgeMs);
|
|
1084
|
+
return Number.isFinite(value) && value > 0 ? value : DEFAULT_APPROVAL_MAX_AGE_MS;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function approvalRecordAgeMs(record, options = {}) {
|
|
1088
|
+
const created = Date.parse(record && record.created_at ? record.created_at : '');
|
|
1089
|
+
if (!Number.isFinite(created)) return Infinity;
|
|
1090
|
+
const now = options.now instanceof Date ? options.now.getTime() : Number(options.now || Date.now());
|
|
1091
|
+
return Math.max(0, now - created);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
function storedApprovalIsStale(record, options = {}) {
|
|
1095
|
+
return approvalRecordAgeMs(record, options) > approvalMaxAgeMs(options);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function emptyApprovalStore() {
|
|
1099
|
+
return { schema: 'ax.pending_approvals.v1', approvals: [] };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
function readApprovalStore(options = {}) {
|
|
1103
|
+
const file = approvalStorePath(options);
|
|
1104
|
+
try {
|
|
1105
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
1106
|
+
const approvals = Array.isArray(parsed.approvals) ? parsed.approvals.filter(row => row && row.id && row.approval_request) : [];
|
|
1107
|
+
return { schema: 'ax.pending_approvals.v1', approvals };
|
|
1108
|
+
} catch (_) {
|
|
1109
|
+
return emptyApprovalStore();
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function writeApprovalStore(store, options = {}) {
|
|
1114
|
+
const file = approvalStorePath(options);
|
|
1115
|
+
const dir = path.dirname(file);
|
|
1116
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
1117
|
+
try {
|
|
1118
|
+
fs.chmodSync(dir, 0o700);
|
|
1119
|
+
} catch (_) {}
|
|
1120
|
+
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
1121
|
+
const data = JSON.stringify({
|
|
1122
|
+
schema: 'ax.pending_approvals.v1',
|
|
1123
|
+
approvals: Array.isArray(store && store.approvals) ? store.approvals : [],
|
|
1124
|
+
}, null, 2);
|
|
1125
|
+
fs.writeFileSync(tmp, data, { mode: 0o600 });
|
|
1126
|
+
fs.renameSync(tmp, file);
|
|
1127
|
+
try {
|
|
1128
|
+
fs.chmodSync(file, 0o600);
|
|
1129
|
+
} catch (_) {}
|
|
1130
|
+
return file;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function approvalRecordFromReceipt(receipt, approvalRequest, options = {}) {
|
|
1134
|
+
const request = approvalRequestWithStableId(approvalRequest);
|
|
1135
|
+
const id = request && (request.approval_request_id || request.approval_id);
|
|
1136
|
+
if (!id) return null;
|
|
1137
|
+
const approvalLine = formatApprovalReceipt({
|
|
1138
|
+
...receipt,
|
|
1139
|
+
tool_events: [{
|
|
1140
|
+
...(taskPreviewApprovalEvent(receipt) || {}),
|
|
1141
|
+
approval_request: request,
|
|
1142
|
+
}],
|
|
1143
|
+
});
|
|
1144
|
+
return {
|
|
1145
|
+
id,
|
|
1146
|
+
created_at: options.createdAt || new Date().toISOString(),
|
|
1147
|
+
updated_at: options.updatedAt || new Date().toISOString(),
|
|
1148
|
+
cwd: options.cwd || '',
|
|
1149
|
+
mode: normalizeMode(options.mode || 'fast'),
|
|
1150
|
+
approval_request: request,
|
|
1151
|
+
display_rows: [
|
|
1152
|
+
...formatTaskPreviewRows(receipt, { color: false }),
|
|
1153
|
+
approvalLine ? formatAuxRow('wait', approvalLine, { color: false }) : null,
|
|
1154
|
+
].filter(Boolean),
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function persistPendingApproval(receipt, approvalRequest, options = {}) {
|
|
1159
|
+
if (options.disableApprovalStore) return null;
|
|
1160
|
+
const record = approvalRecordFromReceipt(receipt || {}, approvalRequest, options);
|
|
1161
|
+
if (!record) return null;
|
|
1162
|
+
const store = readApprovalStore(options);
|
|
1163
|
+
const existing = store.approvals.find(row => row.id === record.id);
|
|
1164
|
+
if (existing && existing.created_at) record.created_at = existing.created_at;
|
|
1165
|
+
store.approvals = [
|
|
1166
|
+
record,
|
|
1167
|
+
...store.approvals.filter(row => row.id !== record.id),
|
|
1168
|
+
].slice(0, 50);
|
|
1169
|
+
writeApprovalStore(store, options);
|
|
1170
|
+
return record;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function findStoredApproval(ref, options = {}) {
|
|
1174
|
+
const target = String(ref || '').trim();
|
|
1175
|
+
if (!target) return null;
|
|
1176
|
+
const store = readApprovalStore(options);
|
|
1177
|
+
const matches = store.approvals.filter(row => String(row.id || '').startsWith(target));
|
|
1178
|
+
if (matches.length !== 1) return null;
|
|
1179
|
+
return matches[0];
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
function removeStoredApproval(ref, options = {}) {
|
|
1183
|
+
const record = findStoredApproval(ref, options);
|
|
1184
|
+
if (!record) return null;
|
|
1185
|
+
const store = readApprovalStore(options);
|
|
1186
|
+
store.approvals = store.approvals.filter(row => row.id !== record.id);
|
|
1187
|
+
writeApprovalStore(store, options);
|
|
1188
|
+
return record;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function formatStoredApprovals(store, options = {}) {
|
|
1192
|
+
const approvals = Array.isArray(store && store.approvals) ? store.approvals : [];
|
|
1193
|
+
if (!approvals.length) return 'No pending approvals.';
|
|
1194
|
+
const blocks = ['Pending approvals'];
|
|
1195
|
+
for (const approval of approvals) {
|
|
1196
|
+
blocks.push(formatAuxRow('id', approval.id, options));
|
|
1197
|
+
blocks.push(formatAuxRow('approve', formatApprovalCommand(approval.id), options));
|
|
1198
|
+
for (const row of Array.isArray(approval.display_rows) ? approval.display_rows : []) {
|
|
1199
|
+
blocks.push(row);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
return blocks.join('\n');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
function formatApprovalCommand(id) {
|
|
1206
|
+
return `ax --approve ${String(id || '').trim()}`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
function stableStringify(value) {
|
|
1210
|
+
if (value === null || typeof value !== 'object') return JSON.stringify(value);
|
|
1211
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
1212
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(',')}}`;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function approvalRequestWithStableId(request) {
|
|
1216
|
+
if (!request || typeof request !== 'object') return null;
|
|
1217
|
+
const copy = JSON.parse(JSON.stringify(request));
|
|
1218
|
+
if (!copy.approval_request_id && !copy.approval_id) {
|
|
1219
|
+
const fingerprint = crypto
|
|
1220
|
+
.createHash('sha256')
|
|
1221
|
+
.update(stableStringify({
|
|
1222
|
+
action_type: copy.executor_action_type || copy.action_type || copy.action || '',
|
|
1223
|
+
connector: copy.connector || '',
|
|
1224
|
+
payload: copy.payload || null,
|
|
1225
|
+
}))
|
|
1226
|
+
.digest('hex')
|
|
1227
|
+
.slice(0, 20);
|
|
1228
|
+
copy.approval_request_id = `apreq_ax_${fingerprint}`;
|
|
1229
|
+
}
|
|
1230
|
+
return copy;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function approvalRequestFromReceipt(receipt) {
|
|
1234
|
+
if (!receipt || typeof receipt !== 'object') return null;
|
|
1235
|
+
const events = Array.isArray(receipt.tool_events) ? receipt.tool_events : [];
|
|
1236
|
+
const approvalEvent = events.find(event => event && event.approval_request);
|
|
1237
|
+
const request = approvalEvent && approvalEvent.approval_request;
|
|
1238
|
+
if (!request || request.status !== 'approval_required') return null;
|
|
1239
|
+
return approvalRequestWithStableId(request);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function approvalExecutionRequestFromReceipt(receipt) {
|
|
1243
|
+
if (!receipt || typeof receipt !== 'object') return null;
|
|
1244
|
+
const accepted = receipt.task_accept_receipt || {};
|
|
1245
|
+
if (accepted.accepted !== true) return null;
|
|
1246
|
+
if (accepted.execution_endpoint && accepted.execution_endpoint !== APPROVAL_EXECUTE_PATH) return null;
|
|
1247
|
+
if (accepted.execution_status && accepted.execution_status !== 'pending_approval_execution') return null;
|
|
1248
|
+
return approvalRequestFromReceipt(receipt);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function approvalExecutionRequestFromState(state) {
|
|
1252
|
+
return approvalExecutionRequestFromReceipt(receiptFromState(state));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function approvalActionLabel(actionType) {
|
|
1256
|
+
if (actionType === 'google_calendar_create_event') return 'calendar event';
|
|
1257
|
+
if (actionType === 'gmail_send') return 'email';
|
|
1258
|
+
if (actionType === 'slack_post_message') return 'Slack message';
|
|
1259
|
+
if (actionType === 'github_create_issue') return 'GitHub issue';
|
|
1260
|
+
if (actionType === 'google_drive_create_file') return 'Drive file';
|
|
1261
|
+
if (actionType === 'notion_create_page') return 'Notion page';
|
|
1262
|
+
return 'approved action';
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function capitalize(value) {
|
|
1266
|
+
const text = String(value || '');
|
|
1267
|
+
return text ? `${text[0].toUpperCase()}${text.slice(1)}` : text;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function approvalSuccessMessage(actionType) {
|
|
1271
|
+
const actionLabel = approvalActionLabel(actionType);
|
|
1272
|
+
if (actionType === 'gmail_send') return 'Sent email.';
|
|
1273
|
+
if (actionType === 'slack_post_message') return 'Sent Slack message.';
|
|
1274
|
+
return `Created ${actionLabel}.`;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function approvalNotDoneMessage(actionType, reason) {
|
|
1278
|
+
const actionLabel = approvalActionLabel(actionType);
|
|
1279
|
+
const verb = actionType === 'gmail_send' || actionType === 'slack_post_message' ? 'sent' : 'created';
|
|
1280
|
+
return `${capitalize(actionLabel)} was not ${verb} because ${reason}.`;
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function formatApprovalExecutionResult(response) {
|
|
1284
|
+
if (!response) return null;
|
|
1285
|
+
const data = response.data && typeof response.data === 'object' ? response.data : {};
|
|
1286
|
+
const detail = data.detail && typeof data.detail === 'object' ? data.detail : {};
|
|
1287
|
+
const execution = data.execution && typeof data.execution === 'object' ? data.execution : {};
|
|
1288
|
+
const actionType = data.action_type || execution.action_type || detail.action_type || '';
|
|
1289
|
+
const actionLabel = approvalActionLabel(actionType);
|
|
1290
|
+
const status = data.status || execution.status || detail.error || '';
|
|
1291
|
+
|
|
1292
|
+
if (!response.ok) {
|
|
1293
|
+
if (detail.error === 'paid_cloud_computer_required') return 'Approval execution needs a paid cloud computer.';
|
|
1294
|
+
if (detail.error === 'approval_signing_key_required') return 'Approval execution is not configured in this runtime.';
|
|
1295
|
+
if (detail.error === 'approval_already_executed') return `Already executed this ${actionLabel}.`;
|
|
1296
|
+
if (detail.user_message) return String(detail.user_message);
|
|
1297
|
+
if (detail.message) return String(detail.message);
|
|
1298
|
+
return response.error || `Approval execution failed (${response.status || 'unknown'}).`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
if (execution.executed || status === 'executed') return approvalSuccessMessage(actionType);
|
|
1302
|
+
if (status === 'blocked_connector_not_connected') return approvalNotDoneMessage(actionType, 'the connector is not connected');
|
|
1303
|
+
if (status === 'execution_failed') return approvalNotDoneMessage(actionType, 'execution failed');
|
|
1304
|
+
if (execution.would_execute) return `${actionLabel[0].toUpperCase()}${actionLabel.slice(1)} was approved but not confirmed as created.`;
|
|
1305
|
+
return `Approval handled for ${actionLabel}.`;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function approvalExecutionShouldRemainPending(response) {
|
|
1309
|
+
if (!response) return true;
|
|
1310
|
+
const data = response.data && typeof response.data === 'object' ? response.data : {};
|
|
1311
|
+
const detail = data.detail && typeof data.detail === 'object' ? data.detail : {};
|
|
1312
|
+
const execution = data.execution && typeof data.execution === 'object' ? data.execution : {};
|
|
1313
|
+
const status = data.status || execution.status || detail.error || '';
|
|
1314
|
+
if (execution.executed || status === 'executed') return false;
|
|
1315
|
+
if (detail.error === 'approval_already_executed') return false;
|
|
1316
|
+
return true;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function creditsFromApprovalExecution(response) {
|
|
1320
|
+
const billing = response && response.data && response.data.billing;
|
|
1321
|
+
if (!billing || billing.billed === false) return null;
|
|
1322
|
+
const credits = Number(billing.credits_charged);
|
|
1323
|
+
return Number.isFinite(credits) && credits > 0 ? credits : null;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
async function postApprovalExecution(approvalRequest, { model = 'atris:fast', token = authToken() } = {}) {
|
|
1327
|
+
return postJson(APPROVAL_EXECUTE_PATH, {
|
|
1328
|
+
model,
|
|
1329
|
+
approval_request: approvalRequest,
|
|
1330
|
+
}, { token });
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async function executeApprovalRequest(approvalRequest, {
|
|
1334
|
+
model = 'atris:fast',
|
|
1335
|
+
output = process.stdout,
|
|
1336
|
+
postApproval = postApprovalExecution,
|
|
1337
|
+
state = null,
|
|
1338
|
+
} = {}) {
|
|
1339
|
+
if (!approvalRequest) return null;
|
|
1340
|
+
const localState = state || {
|
|
1341
|
+
events: [],
|
|
1342
|
+
errors: [],
|
|
1343
|
+
output: '',
|
|
1344
|
+
pendingText: '',
|
|
1345
|
+
wroteText: false,
|
|
1346
|
+
wroteActivity: false,
|
|
1347
|
+
lastChar: '\n',
|
|
1348
|
+
progress: null,
|
|
1349
|
+
inAuxBlock: false,
|
|
1350
|
+
needsBullet: true,
|
|
1351
|
+
};
|
|
1352
|
+
writeAuxLine(localState, output, formatAuxRow('wait', 'Sending approved action to Atris cloud...', output));
|
|
1353
|
+
const response = await postApproval(approvalRequest, { model });
|
|
1354
|
+
const message = formatApprovalExecutionResult(response);
|
|
1355
|
+
if (message) {
|
|
1356
|
+
const execution = response && response.data && response.data.execution;
|
|
1357
|
+
const label = response && response.ok && execution && execution.executed ? 'done' : 'wait';
|
|
1358
|
+
writeAuxLine(localState, output, formatAuxRow(label, message, output));
|
|
1359
|
+
}
|
|
1360
|
+
return { approval_request: approvalRequest, response, message };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
async function executeApprovalFromState(state, options = {}) {
|
|
1364
|
+
const approvalRequest = approvalExecutionRequestFromState(state);
|
|
1365
|
+
if (!approvalRequest) return null;
|
|
1366
|
+
return executeApprovalRequest(approvalRequest, { ...options, state });
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
async function approveStoredApproval(ref, options = {}) {
|
|
1370
|
+
const record = findStoredApproval(ref, options);
|
|
1371
|
+
if (!record) throw new Error(`Pending approval not found: ${ref}`);
|
|
1372
|
+
const output = options.output || process.stdout;
|
|
1373
|
+
const startedAt = Date.now();
|
|
1374
|
+
output.write(`${formatAuxRow('id', record.id, output)}\n`);
|
|
1375
|
+
for (const row of Array.isArray(record.display_rows) ? record.display_rows : []) {
|
|
1376
|
+
output.write(`${row}\n`);
|
|
1377
|
+
}
|
|
1378
|
+
if (storedApprovalIsStale(record, options)) {
|
|
1379
|
+
removeStoredApproval(record.id, options);
|
|
1380
|
+
const message = 'Approval expired. Recreate the preview before approving.';
|
|
1381
|
+
output.write(`${formatAuxRow('wait', message, output)}\n`);
|
|
1382
|
+
output.write(`${formatDoneLine(Date.now() - startedAt)}\n`);
|
|
1383
|
+
return { approval_request: record.approval_request, response: null, message, stale: true };
|
|
1384
|
+
}
|
|
1385
|
+
output.write('Accepted.\n');
|
|
1386
|
+
const execution = await executeApprovalRequest(record.approval_request, {
|
|
1387
|
+
model: options.model || modelForMode(options.mode || record.mode || 'fast'),
|
|
1388
|
+
output,
|
|
1389
|
+
...(options.postApproval ? { postApproval: options.postApproval } : {}),
|
|
1390
|
+
});
|
|
1391
|
+
const credits = creditsFromApprovalExecution(execution && execution.response);
|
|
1392
|
+
output.write(`${formatDoneLine(Date.now() - startedAt, credits)}\n`);
|
|
1393
|
+
if (approvalExecutionShouldRemainPending(execution && execution.response)) {
|
|
1394
|
+
const store = readApprovalStore(options);
|
|
1395
|
+
store.approvals = store.approvals.map(row => row.id === record.id ? { ...row, updated_at: new Date().toISOString() } : row);
|
|
1396
|
+
writeApprovalStore(store, options);
|
|
1397
|
+
} else {
|
|
1398
|
+
removeStoredApproval(record.id, options);
|
|
1399
|
+
}
|
|
1400
|
+
return execution;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function denyStoredApproval(ref, options = {}) {
|
|
1404
|
+
const record = removeStoredApproval(ref, options);
|
|
1405
|
+
if (!record) throw new Error(`Pending approval not found: ${ref}`);
|
|
1406
|
+
return record;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1698
1409
|
function clearRetriedText(state) {
|
|
1699
1410
|
state.pendingText = '';
|
|
1700
1411
|
state.output = '';
|
|
@@ -1712,7 +1423,7 @@ function stopProgress(state) {
|
|
|
1712
1423
|
}
|
|
1713
1424
|
|
|
1714
1425
|
function flushPendingText(state, output) {
|
|
1715
|
-
const hasMarkdownRemainder = output && (
|
|
1426
|
+
const hasMarkdownRemainder = output && output.isTTY && (
|
|
1716
1427
|
state.markdownCarry
|
|
1717
1428
|
|| state.markdownBuffer
|
|
1718
1429
|
|| (state.markdownMode && state.markdownMode !== 'normal')
|
|
@@ -1724,7 +1435,7 @@ function flushPendingText(state, output) {
|
|
|
1724
1435
|
output.write('● ');
|
|
1725
1436
|
state.needsBullet = false;
|
|
1726
1437
|
}
|
|
1727
|
-
if (state.pendingText) output.write(renderTerminalMarkdown(state.pendingText, output));
|
|
1438
|
+
if (state.pendingText) output.write(output && output.isTTY ? renderTerminalMarkdown(state.pendingText, output) : state.pendingText);
|
|
1728
1439
|
const flushedMarkdown = hasMarkdownRemainder ? flushStreamingMarkdown(state, output) : '';
|
|
1729
1440
|
state.wroteText = true;
|
|
1730
1441
|
state.wroteActivity = true;
|
|
@@ -1763,42 +1474,92 @@ function writeAuxLine(state, output, line) {
|
|
|
1763
1474
|
state.needsBullet = true;
|
|
1764
1475
|
}
|
|
1765
1476
|
|
|
1766
|
-
function
|
|
1767
|
-
return history
|
|
1477
|
+
function structuredHistory(history, limit = 8) {
|
|
1478
|
+
return (Array.isArray(history) ? history : [])
|
|
1768
1479
|
.slice(-limit)
|
|
1769
|
-
.map(turn =>
|
|
1770
|
-
|
|
1480
|
+
.map(turn => {
|
|
1481
|
+
const message = {
|
|
1482
|
+
role: turn && turn.role === 'assistant' ? 'assistant' : 'user',
|
|
1483
|
+
content: String((turn && turn.content) || '').slice(0, 1200)
|
|
1484
|
+
};
|
|
1485
|
+
if (message.role === 'assistant' && turn && turn.task_preview) {
|
|
1486
|
+
message.task_preview = turn.task_preview;
|
|
1487
|
+
}
|
|
1488
|
+
return message;
|
|
1489
|
+
})
|
|
1490
|
+
.filter(turn => turn.content.trim());
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function latestPendingTaskPreview(history = []) {
|
|
1494
|
+
for (const turn of [...(Array.isArray(history) ? history : [])].reverse()) {
|
|
1495
|
+
if (!turn || turn.role !== 'assistant' || !turn.task_preview) continue;
|
|
1496
|
+
const status = String(turn.task_preview.status || '');
|
|
1497
|
+
if (['needs_input', 'approval_required'].includes(status)) return turn.task_preview;
|
|
1498
|
+
return null;
|
|
1499
|
+
}
|
|
1500
|
+
return null;
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
function latestPendingApprovalRequest(history = []) {
|
|
1504
|
+
for (const turn of [...(Array.isArray(history) ? history : [])].reverse()) {
|
|
1505
|
+
if (!turn || turn.role !== 'assistant') continue;
|
|
1506
|
+
if (turn.approval_request) return approvalRequestWithStableId(turn.approval_request);
|
|
1507
|
+
return null;
|
|
1508
|
+
}
|
|
1509
|
+
return null;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
function messageApprovesPendingApproval(message) {
|
|
1513
|
+
const normalized = String(message || '').trim().toLowerCase().replace(/[.!?]+$/g, '');
|
|
1514
|
+
return new Set([
|
|
1515
|
+
'yes',
|
|
1516
|
+
'y',
|
|
1517
|
+
'yep',
|
|
1518
|
+
'yeah',
|
|
1519
|
+
'ok',
|
|
1520
|
+
'okay',
|
|
1521
|
+
'approve',
|
|
1522
|
+
'approved',
|
|
1523
|
+
'confirm',
|
|
1524
|
+
'create it',
|
|
1525
|
+
'send it',
|
|
1526
|
+
'post it',
|
|
1527
|
+
'do it',
|
|
1528
|
+
'go ahead',
|
|
1529
|
+
'looks good',
|
|
1530
|
+
'yes create it',
|
|
1531
|
+
'yes send it',
|
|
1532
|
+
'yes post it',
|
|
1533
|
+
]).has(normalized);
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function messageCancelsPendingApproval(message) {
|
|
1537
|
+
const normalized = String(message || '').trim().toLowerCase().replace(/[.!?]+$/g, '');
|
|
1538
|
+
return new Set(['no', 'nope', 'cancel', 'cancel it', 'stop', 'never mind', 'nevermind', 'do not', "don't"]).has(normalized);
|
|
1771
1539
|
}
|
|
1772
1540
|
|
|
1773
1541
|
function buildMessage(message, history = []) {
|
|
1774
|
-
|
|
1775
|
-
if (!history.length) return trimmed;
|
|
1776
|
-
return [
|
|
1777
|
-
'Continue this terminal coding-agent conversation in the same workspace.',
|
|
1778
|
-
'Use local tools when the user asks about files, code, tests, or edits.',
|
|
1779
|
-
'',
|
|
1780
|
-
'Recent conversation:',
|
|
1781
|
-
compactHistory(history),
|
|
1782
|
-
'',
|
|
1783
|
-
'# Current user message',
|
|
1784
|
-
trimmed,
|
|
1785
|
-
].join('\n');
|
|
1542
|
+
return String(message || '').trim();
|
|
1786
1543
|
}
|
|
1787
1544
|
|
|
1788
1545
|
function buildPayload(message, options = {}) {
|
|
1789
1546
|
const mode = normalizeMode(options.mode);
|
|
1790
1547
|
const route = options.business ? 'local' : resolveRoute(message, options);
|
|
1791
1548
|
const local = route !== 'cloud';
|
|
1792
|
-
const bypass = resolveBypassPermissions(options);
|
|
1793
1549
|
const verifyCommand = String(options.verify || '').trim();
|
|
1550
|
+
const previousMessages = structuredHistory(options.history || []);
|
|
1794
1551
|
const payload = {
|
|
1795
1552
|
message: buildMessage(message, options.history || []),
|
|
1796
1553
|
model: modelForMode(mode),
|
|
1797
|
-
max_turns:
|
|
1798
|
-
verify_command: verifyCommand || 'true'
|
|
1799
|
-
member_slug: MEMBER_SLUG,
|
|
1800
|
-
bypass_permissions: bypass,
|
|
1554
|
+
max_turns: local ? (mode === 'fast' ? 8 : 14) : 1,
|
|
1555
|
+
verify_command: verifyCommand || 'true'
|
|
1801
1556
|
};
|
|
1557
|
+
if (previousMessages.length) {
|
|
1558
|
+
payload.previous_messages = previousMessages;
|
|
1559
|
+
}
|
|
1560
|
+
if (options.conversationId) {
|
|
1561
|
+
payload.conversation_id = String(options.conversationId);
|
|
1562
|
+
}
|
|
1802
1563
|
if (options.business) {
|
|
1803
1564
|
// Business cloud workspace: the model loop stays on the backend and every
|
|
1804
1565
|
// file/bash tool call relays here, executing on the business EC2 via the
|
|
@@ -1814,20 +1575,22 @@ function buildPayload(message, options = {}) {
|
|
|
1814
1575
|
if (!local && options.connectionUserId) {
|
|
1815
1576
|
payload.connection_user_id = options.connectionUserId;
|
|
1816
1577
|
}
|
|
1817
|
-
if (!local && bypass && connectorWriteIntent(message)) {
|
|
1818
|
-
payload.allow_external_actions = true;
|
|
1819
|
-
payload.cleanup_external_actions = true;
|
|
1820
|
-
payload.max_turns = mode === 'fast' ? 2 : 4;
|
|
1821
|
-
}
|
|
1822
1578
|
return payload;
|
|
1823
1579
|
}
|
|
1824
1580
|
|
|
1825
1581
|
function buildCodeFastPayload(message, options = {}) {
|
|
1582
|
+
const previousMessages = structuredHistory(options.history || []);
|
|
1826
1583
|
const payload = {
|
|
1827
1584
|
message: buildMessage(message, options.history || []),
|
|
1828
1585
|
model: CODE_FAST.model,
|
|
1829
1586
|
timeout_seconds: Number(options.timeoutSeconds || 180)
|
|
1830
1587
|
};
|
|
1588
|
+
if (previousMessages.length) {
|
|
1589
|
+
payload.previous_messages = previousMessages;
|
|
1590
|
+
}
|
|
1591
|
+
if (options.conversationId) {
|
|
1592
|
+
payload.conversation_id = String(options.conversationId);
|
|
1593
|
+
}
|
|
1831
1594
|
if (isCodeFastLocal(options)) {
|
|
1832
1595
|
payload.workspace_path = options.cwd || process.cwd();
|
|
1833
1596
|
}
|
|
@@ -1880,7 +1643,6 @@ function handleEvent(event, state, output) {
|
|
|
1880
1643
|
writeAuxLine(state, output, formatToolResultLine(summarizeToolResult(result, output), output));
|
|
1881
1644
|
state.lastAux = 'result';
|
|
1882
1645
|
}
|
|
1883
|
-
if (state.progress && state.progress.setVerb) state.progress.setVerb('Thinking');
|
|
1884
1646
|
return;
|
|
1885
1647
|
}
|
|
1886
1648
|
|
|
@@ -1911,13 +1673,23 @@ function handleEvent(event, state, output) {
|
|
|
1911
1673
|
clearRetriedText(state);
|
|
1912
1674
|
return;
|
|
1913
1675
|
}
|
|
1914
|
-
if (state.progress && state.progress.setVerb && /think|reason|plan/i.test(event.message)) {
|
|
1915
|
-
state.progress.setVerb('Thinking');
|
|
1916
|
-
}
|
|
1917
1676
|
flushPendingText(state, output);
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1677
|
+
writeAuxLine(state, output, paint(`· ${formatStatusMessage(event.message)}`, [ANSI.muted], output));
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
if (event.type === 'receipt' && event.receipt) {
|
|
1682
|
+
const approvalLine = formatApprovalReceipt(event.receipt);
|
|
1683
|
+
const previewRows = formatTaskPreviewRows(event.receipt, output);
|
|
1684
|
+
if (approvalLine) {
|
|
1685
|
+
flushPendingText(state, output);
|
|
1686
|
+
for (const row of previewRows) writeAuxLine(state, output, row);
|
|
1687
|
+
writeAuxLine(state, output, formatAuxRow('wait', approvalLine, output));
|
|
1688
|
+
state.lastAux = 'approval';
|
|
1689
|
+
} else if (previewRows.length) {
|
|
1690
|
+
flushPendingText(state, output);
|
|
1691
|
+
for (const row of previewRows) writeAuxLine(state, output, row);
|
|
1692
|
+
state.lastAux = 'preview';
|
|
1921
1693
|
}
|
|
1922
1694
|
return;
|
|
1923
1695
|
}
|
|
@@ -1946,7 +1718,7 @@ async function postTurn(message, options = {}) {
|
|
|
1946
1718
|
const connectionContext = options.connectionContext || (shouldSendConnectionContext
|
|
1947
1719
|
? await buildConnectionContext({ token, localWorkspace: local })
|
|
1948
1720
|
: null);
|
|
1949
|
-
const connectionUserId = !local && isLoopbackBackend(
|
|
1721
|
+
const connectionUserId = !local && isLoopbackBackend() ? authUserId() : '';
|
|
1950
1722
|
const payload = buildPayload(message, { ...options, route, connectionContext, connectionUserId });
|
|
1951
1723
|
const postData = JSON.stringify(payload);
|
|
1952
1724
|
const output = options.output || process.stdout;
|
|
@@ -1954,7 +1726,7 @@ async function postTurn(message, options = {}) {
|
|
|
1954
1726
|
// SSE traffic in between, so the socket-idle timeout needs more headroom.
|
|
1955
1727
|
const baseTimeoutMs = payload.model === 'atris:max' ? 300000 : payload.model === 'atris:pro' ? 180000 : 60000;
|
|
1956
1728
|
const timeoutMs = options.business ? Math.max(baseTimeoutMs, 180000) : baseTimeoutMs;
|
|
1957
|
-
const turnUrl = new URL(backendUrl(
|
|
1729
|
+
const turnUrl = new URL(backendUrl());
|
|
1958
1730
|
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
1959
1731
|
const state = {
|
|
1960
1732
|
events: [],
|
|
@@ -1972,19 +1744,15 @@ async function postTurn(message, options = {}) {
|
|
|
1972
1744
|
markdownMode: 'normal',
|
|
1973
1745
|
markdownBuffer: '',
|
|
1974
1746
|
markdownCarry: '',
|
|
1975
|
-
atLineStart: true,
|
|
1976
1747
|
relay: options.business ? {
|
|
1977
1748
|
chain: Promise.resolve(),
|
|
1978
1749
|
execute: options.business.executor,
|
|
1979
|
-
post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl(
|
|
1750
|
+
post: (callId, result) => options.business.postToolResult(callId, result, backendBaseUrl())
|
|
1980
1751
|
} : null
|
|
1981
1752
|
};
|
|
1982
1753
|
|
|
1983
1754
|
return new Promise((resolve, reject) => {
|
|
1984
1755
|
let settled = false;
|
|
1985
|
-
let interrupted = false;
|
|
1986
|
-
let req = null;
|
|
1987
|
-
let removeInterrupt = null;
|
|
1988
1756
|
const startedAt = Date.now();
|
|
1989
1757
|
state.progress = createProgressReporter(output, options);
|
|
1990
1758
|
state.progress.start();
|
|
@@ -1992,8 +1760,6 @@ async function postTurn(message, options = {}) {
|
|
|
1992
1760
|
const finish = (error, value) => {
|
|
1993
1761
|
if (settled) return;
|
|
1994
1762
|
settled = true;
|
|
1995
|
-
if (removeInterrupt) removeInterrupt();
|
|
1996
|
-
removeInterrupt = null;
|
|
1997
1763
|
if (state.progress) state.progress.stop();
|
|
1998
1764
|
state.progress = null;
|
|
1999
1765
|
state.durationMs = Date.now() - startedAt;
|
|
@@ -2001,15 +1767,7 @@ async function postTurn(message, options = {}) {
|
|
|
2001
1767
|
else resolve(value);
|
|
2002
1768
|
};
|
|
2003
1769
|
|
|
2004
|
-
|
|
2005
|
-
if (settled) return;
|
|
2006
|
-
interrupted = true;
|
|
2007
|
-
flushPendingText(state, output);
|
|
2008
|
-
if (req) req.destroy();
|
|
2009
|
-
finish(null, { ...state, interrupted: true });
|
|
2010
|
-
}, options);
|
|
2011
|
-
|
|
2012
|
-
req = transport.request({
|
|
1770
|
+
const req = transport.request({
|
|
2013
1771
|
hostname: turnUrl.hostname,
|
|
2014
1772
|
port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
|
|
2015
1773
|
path: `${turnUrl.pathname}${turnUrl.search}`,
|
|
@@ -2074,12 +1832,8 @@ async function postTurn(message, options = {}) {
|
|
|
2074
1832
|
});
|
|
2075
1833
|
});
|
|
2076
1834
|
|
|
2077
|
-
req.on('error',
|
|
2078
|
-
if (interrupted) return;
|
|
2079
|
-
finish(error);
|
|
2080
|
-
});
|
|
1835
|
+
req.on('error', finish);
|
|
2081
1836
|
req.setTimeout(timeoutMs, () => {
|
|
2082
|
-
if (settled) return;
|
|
2083
1837
|
finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
|
|
2084
1838
|
req.destroy();
|
|
2085
1839
|
});
|
|
@@ -2099,7 +1853,7 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
2099
1853
|
const output = options.output || process.stdout;
|
|
2100
1854
|
const token = authToken();
|
|
2101
1855
|
const timeoutMs = Math.max(10000, Math.min(600000, Number(payload.timeout_seconds || 180) * 1000));
|
|
2102
|
-
const turnUrl = new URL(codeFastUrl(
|
|
1856
|
+
const turnUrl = new URL(codeFastUrl());
|
|
2103
1857
|
const transport = turnUrl.protocol === 'https:' ? https : http;
|
|
2104
1858
|
const state = {
|
|
2105
1859
|
events: [],
|
|
@@ -2116,15 +1870,11 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
2116
1870
|
lastAux: '',
|
|
2117
1871
|
markdownMode: 'normal',
|
|
2118
1872
|
markdownBuffer: '',
|
|
2119
|
-
markdownCarry: ''
|
|
2120
|
-
atLineStart: true,
|
|
1873
|
+
markdownCarry: ''
|
|
2121
1874
|
};
|
|
2122
1875
|
|
|
2123
1876
|
return new Promise((resolve, reject) => {
|
|
2124
1877
|
let settled = false;
|
|
2125
|
-
let interrupted = false;
|
|
2126
|
-
let req = null;
|
|
2127
|
-
let removeInterrupt = null;
|
|
2128
1878
|
const startedAt = Date.now();
|
|
2129
1879
|
state.progress = createProgressReporter(output, options);
|
|
2130
1880
|
state.progress.start();
|
|
@@ -2132,8 +1882,6 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
2132
1882
|
const finish = (error, value) => {
|
|
2133
1883
|
if (settled) return;
|
|
2134
1884
|
settled = true;
|
|
2135
|
-
if (removeInterrupt) removeInterrupt();
|
|
2136
|
-
removeInterrupt = null;
|
|
2137
1885
|
if (state.progress) state.progress.stop();
|
|
2138
1886
|
state.progress = null;
|
|
2139
1887
|
state.durationMs = Date.now() - startedAt;
|
|
@@ -2141,15 +1889,7 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
2141
1889
|
else resolve(value);
|
|
2142
1890
|
};
|
|
2143
1891
|
|
|
2144
|
-
|
|
2145
|
-
if (settled) return;
|
|
2146
|
-
interrupted = true;
|
|
2147
|
-
flushPendingText(state, output);
|
|
2148
|
-
if (req) req.destroy();
|
|
2149
|
-
finish(null, { ...state, interrupted: true });
|
|
2150
|
-
}, options);
|
|
2151
|
-
|
|
2152
|
-
req = transport.request({
|
|
1892
|
+
const req = transport.request({
|
|
2153
1893
|
hostname: turnUrl.hostname,
|
|
2154
1894
|
port: turnUrl.port || (turnUrl.protocol === 'https:' ? 443 : 80),
|
|
2155
1895
|
path: `${turnUrl.pathname}${turnUrl.search}`,
|
|
@@ -2201,12 +1941,8 @@ async function postCodeFastTurn(message, options = {}) {
|
|
|
2201
1941
|
});
|
|
2202
1942
|
});
|
|
2203
1943
|
|
|
2204
|
-
req.on('error',
|
|
2205
|
-
if (interrupted) return;
|
|
2206
|
-
finish(error);
|
|
2207
|
-
});
|
|
1944
|
+
req.on('error', finish);
|
|
2208
1945
|
req.setTimeout(timeoutMs, () => {
|
|
2209
|
-
if (settled) return;
|
|
2210
1946
|
finish(new Error(`Request timeout after ${timeoutMs / 1000}s`));
|
|
2211
1947
|
req.destroy();
|
|
2212
1948
|
});
|
|
@@ -2221,136 +1957,36 @@ function turnFunctionForMode(mode) {
|
|
|
2221
1957
|
|
|
2222
1958
|
async function chat(options = {}) {
|
|
2223
1959
|
let mode = normalizeMode(options.mode);
|
|
2224
|
-
let bypassPermissions = resolveBypassPermissions(options);
|
|
2225
1960
|
const cwd = options.cwd || process.cwd();
|
|
2226
1961
|
const input = options.input || process.stdin;
|
|
2227
1962
|
const baseOutput = options.output || process.stdout;
|
|
2228
1963
|
const logger = createRunLogger({ cwd, mode, kind: 'play', output: baseOutput });
|
|
2229
1964
|
const output = logger ? logger.output : baseOutput;
|
|
2230
1965
|
const history = [];
|
|
2231
|
-
|
|
2232
|
-
let lastAchievedGoal = null;
|
|
2233
|
-
|
|
2234
|
-
const goalPaintOptions = {
|
|
2235
|
-
paint: (text, codes) => paint(text, codes, output),
|
|
2236
|
-
bold: ANSI.bold,
|
|
2237
|
-
magenta: ANSI.magenta,
|
|
2238
|
-
muted: ANSI.muted,
|
|
2239
|
-
accent: ANSI.accent,
|
|
2240
|
-
ok: ANSI.ok,
|
|
2241
|
-
};
|
|
1966
|
+
const conversationId = options.conversationId || `ax-${process.pid}-${Date.now().toString(36)}`;
|
|
2242
1967
|
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
};
|
|
2246
|
-
|
|
2247
|
-
writeHeader();
|
|
2248
|
-
|
|
2249
|
-
const executeChatTurn = async (message, turnOptions = {}) => {
|
|
2250
|
-
let result;
|
|
2251
|
-
try {
|
|
2252
|
-
const turnFn = typeof options.turnFunction === 'function'
|
|
2253
|
-
? options.turnFunction
|
|
2254
|
-
: turnFunctionForMode(mode);
|
|
2255
|
-
result = await turnFn(message, {
|
|
2256
|
-
mode,
|
|
2257
|
-
cwd,
|
|
2258
|
-
history,
|
|
2259
|
-
output,
|
|
2260
|
-
route: options.route,
|
|
2261
|
-
business: options.business,
|
|
2262
|
-
verify: options.verify,
|
|
2263
|
-
bypassPermissions,
|
|
2264
|
-
registerAbort: options.registerAbort,
|
|
2265
|
-
goalEval: turnOptions.goalEval === true,
|
|
2266
|
-
});
|
|
2267
|
-
} finally {
|
|
2268
|
-
if (typeof options.registerAbort === 'function') options.registerAbort(null);
|
|
2269
|
-
}
|
|
1968
|
+
output.write(`${formatHeader({ mode, cwd, chat: true }, output)}\n\n`);
|
|
1969
|
+
if (logger) output.write(`${formatAuxRow('log', formatPathSubject(logger.path, output), output)}\n\n`);
|
|
2270
1970
|
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
history.push({ role: 'user', content: message });
|
|
2278
|
-
history.push({ role: 'assistant', content: result.output });
|
|
2279
|
-
}
|
|
2280
|
-
return result;
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2284
|
-
if (!turnOptions.goalEval) {
|
|
2285
|
-
output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n\n`);
|
|
1971
|
+
const runtimePreflight = { ready: false };
|
|
1972
|
+
const ensureRuntimeReady = async () => {
|
|
1973
|
+
if (runtimePreflight.ready) return true;
|
|
1974
|
+
if (options.skipRuntimePreflight || options.turnFunction || options.business || normalizeMode(mode) === 'code-fast') {
|
|
1975
|
+
runtimePreflight.ready = true;
|
|
1976
|
+
return true;
|
|
2286
1977
|
}
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
writeHeader();
|
|
2295
|
-
|
|
2296
|
-
while (activeGoal.active) {
|
|
2297
|
-
const directive = buildGoalDirective(activeGoal, { continue: activeGoal.turns > 0 });
|
|
2298
|
-
if (activeGoal.turns > 0) {
|
|
2299
|
-
output.write(`${formatGoalContinue(activeGoal, goalPaintOptions)}\n\n`);
|
|
2300
|
-
}
|
|
2301
|
-
output.write('\n');
|
|
2302
|
-
|
|
2303
|
-
const result = await executeChatTurn(directive, { goalLoop: true });
|
|
2304
|
-
activeGoal.turns += 1;
|
|
2305
|
-
accumulateGoalUsage(activeGoal, result, creditsFromState);
|
|
2306
|
-
|
|
2307
|
-
if (result.interrupted) {
|
|
2308
|
-
output.write(`${paint('· Goal turn interrupted — /goal for status · /goal clear to stop', [ANSI.muted], output)}\n\n`);
|
|
2309
|
-
return;
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
const evalResult = await evaluateGoalTurn(activeGoal, {
|
|
2313
|
-
history,
|
|
2314
|
-
lastOutput: result.output,
|
|
2315
|
-
turnOptions: {
|
|
2316
|
-
mode,
|
|
2317
|
-
cwd,
|
|
2318
|
-
bypassPermissions,
|
|
2319
|
-
route: options.route,
|
|
2320
|
-
business: options.business,
|
|
2321
|
-
verify: options.verify,
|
|
2322
|
-
registerAbort: options.registerAbort,
|
|
2323
|
-
},
|
|
2324
|
-
}, { postTurn, evaluateGoal: options.evaluateGoal });
|
|
2325
|
-
|
|
2326
|
-
activeGoal.evalTurns += 1;
|
|
2327
|
-
activeGoal.lastReason = evalResult.reason || activeGoal.lastReason;
|
|
2328
|
-
|
|
2329
|
-
if (evalResult.achieved) {
|
|
2330
|
-
finishGoalAchieved(activeGoal, evalResult.reason);
|
|
2331
|
-
output.write(`${formatGoalAchieved(activeGoal, goalPaintOptions)}\n\n`);
|
|
2332
|
-
lastAchievedGoal = { ...activeGoal };
|
|
2333
|
-
goalState = null;
|
|
2334
|
-
writeHeader();
|
|
2335
|
-
return;
|
|
2336
|
-
}
|
|
2337
|
-
|
|
2338
|
-
if (goalTurnLimitReached(activeGoal)) {
|
|
2339
|
-
clearGoalState(activeGoal);
|
|
2340
|
-
output.write(`${formatGoalStopped(activeGoal, 'Turn limit reached.', goalPaintOptions)}\n\n`);
|
|
2341
|
-
goalState = null;
|
|
2342
|
-
writeHeader();
|
|
2343
|
-
return;
|
|
2344
|
-
}
|
|
2345
|
-
|
|
2346
|
-
if (goalBudgetExceeded(activeGoal)) {
|
|
2347
|
-
clearGoalState(activeGoal);
|
|
2348
|
-
output.write(`${formatGoalStopped(activeGoal, 'Token budget reached.', goalPaintOptions)}\n\n`);
|
|
2349
|
-
goalState = null;
|
|
2350
|
-
writeHeader();
|
|
2351
|
-
return;
|
|
2352
|
-
}
|
|
1978
|
+
const startedAt = Date.now();
|
|
1979
|
+
const health = options.runtimeHealth
|
|
1980
|
+
? await Promise.resolve(options.runtimeHealth({ mode }))
|
|
1981
|
+
: await buildRuntimeHealth({ timeoutMs: options.preflightTimeoutMs || 1200 });
|
|
1982
|
+
if (runtimeReadyForChat(health, mode)) {
|
|
1983
|
+
runtimePreflight.ready = true;
|
|
1984
|
+
return true;
|
|
2353
1985
|
}
|
|
1986
|
+
output.write(`${formatRuntimeHealth(health, output)}\n\n`);
|
|
1987
|
+
output.write(`${formatBackendHint()}\n`);
|
|
1988
|
+
output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
|
|
1989
|
+
return false;
|
|
2354
1990
|
};
|
|
2355
1991
|
|
|
2356
1992
|
const runLine = async (line) => {
|
|
@@ -2371,68 +2007,104 @@ async function chat(options = {}) {
|
|
|
2371
2007
|
return false;
|
|
2372
2008
|
}
|
|
2373
2009
|
|
|
2374
|
-
const permission = chatPermissionCommand(trimmed);
|
|
2375
|
-
if (permission !== null) {
|
|
2376
|
-
bypassPermissions = permission;
|
|
2377
|
-
const persistPrefs = options.persistPrefs !== false;
|
|
2378
|
-
setBypassPermissions(permission, { persist: persistPrefs });
|
|
2379
|
-
if (logger) logger.write(`${trimmed}\n`);
|
|
2380
|
-
output.write(`${formatPermissionToggleMessage(permission, { ...output, persistPrefs })}\n\n`);
|
|
2381
|
-
return false;
|
|
2382
|
-
}
|
|
2383
|
-
|
|
2384
|
-
const goalCmd = parseGoalCommand(trimmed);
|
|
2385
|
-
if (goalCmd) {
|
|
2386
|
-
if (logger) logger.write(`${trimmed}\n`);
|
|
2387
|
-
if (goalCmd.action === 'clear') {
|
|
2388
|
-
if (goalState) clearGoalState(goalState);
|
|
2389
|
-
goalState = null;
|
|
2390
|
-
output.write(`${paint('· Goal cleared', [ANSI.ok], output)}\n\n`);
|
|
2391
|
-
writeHeader();
|
|
2392
|
-
return false;
|
|
2393
|
-
}
|
|
2394
|
-
if (goalCmd.action === 'status') {
|
|
2395
|
-
output.write(`${formatGoalStatus(goalState || lastAchievedGoal, goalPaintOptions)}\n\n`);
|
|
2396
|
-
return false;
|
|
2397
|
-
}
|
|
2398
|
-
if (goalCmd.action === 'set') {
|
|
2399
|
-
if (goalState && goalState.active) clearGoalState(goalState);
|
|
2400
|
-
goalState = createGoalState(goalCmd.condition, goalCmd);
|
|
2401
|
-
lastAchievedGoal = null;
|
|
2402
|
-
await runGoalLoop(goalState);
|
|
2403
|
-
return false;
|
|
2404
|
-
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
2010
|
if (trimmed.startsWith('/')) {
|
|
2408
2011
|
output.write(`${chatMenu(output)}\n\n`);
|
|
2409
2012
|
return false;
|
|
2410
2013
|
}
|
|
2411
2014
|
|
|
2412
|
-
if (
|
|
2413
|
-
|
|
2414
|
-
|
|
2015
|
+
if (logger) logger.write(`${formatPrompt(mode)}${trimmed}\n`);
|
|
2016
|
+
output.write('\n');
|
|
2017
|
+
const pendingApprovalRequest = latestPendingApprovalRequest(history);
|
|
2018
|
+
if (pendingApprovalRequest && normalizeMode(mode) !== 'code-fast' && messageCancelsPendingApproval(trimmed)) {
|
|
2415
2019
|
const startedAt = Date.now();
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
youtubeCommand: options.youtubeCommand,
|
|
2419
|
-
ensureValidCredentials: options.ensureValidCredentials,
|
|
2420
|
-
apiRequestJson: options.apiRequestJson,
|
|
2421
|
-
output: (line = '') => {
|
|
2422
|
-
const text = String(line);
|
|
2423
|
-
youtubeOutput.push(text);
|
|
2424
|
-
output.write(`${text}\n`);
|
|
2425
|
-
},
|
|
2426
|
-
});
|
|
2020
|
+
if (!options.turnFunction) removeStoredApproval(pendingApprovalRequest.approval_request_id || pendingApprovalRequest.approval_id, { storePath: options.approvalStorePath });
|
|
2021
|
+
output.write('Cancelled.\n');
|
|
2427
2022
|
output.write(`${formatDoneLine(Date.now() - startedAt)}\n\n`);
|
|
2428
2023
|
history.push({ role: 'user', content: trimmed });
|
|
2429
|
-
history.push({ role: 'assistant', content:
|
|
2024
|
+
history.push({ role: 'assistant', content: 'Cancelled.' });
|
|
2025
|
+
return false;
|
|
2026
|
+
}
|
|
2027
|
+
if (pendingApprovalRequest && normalizeMode(mode) !== 'code-fast' && messageApprovesPendingApproval(trimmed)) {
|
|
2028
|
+
if (!(await ensureRuntimeReady())) return false;
|
|
2029
|
+
const startedAt = Date.now();
|
|
2030
|
+
const approvalState = {
|
|
2031
|
+
events: [],
|
|
2032
|
+
errors: [],
|
|
2033
|
+
output: 'Accepted.',
|
|
2034
|
+
pendingText: '',
|
|
2035
|
+
wroteText: true,
|
|
2036
|
+
wroteActivity: true,
|
|
2037
|
+
lastChar: '\n',
|
|
2038
|
+
progress: null,
|
|
2039
|
+
inAuxBlock: false,
|
|
2040
|
+
needsBullet: true,
|
|
2041
|
+
};
|
|
2042
|
+
output.write('Accepted.\n');
|
|
2043
|
+
const approvalExecution = await executeApprovalRequest(pendingApprovalRequest, {
|
|
2044
|
+
model: modelForMode(mode),
|
|
2045
|
+
output,
|
|
2046
|
+
state: approvalState,
|
|
2047
|
+
...(options.postApproval ? { postApproval: options.postApproval } : {}),
|
|
2048
|
+
});
|
|
2049
|
+
const credits = creditsFromApprovalExecution(approvalExecution && approvalExecution.response);
|
|
2050
|
+
output.write(`${formatDoneLine(Date.now() - startedAt, credits)}\n\n`);
|
|
2051
|
+
if (!options.turnFunction) {
|
|
2052
|
+
const approvalId = pendingApprovalRequest.approval_request_id || pendingApprovalRequest.approval_id;
|
|
2053
|
+
if (approvalExecutionShouldRemainPending(approvalExecution && approvalExecution.response)) {
|
|
2054
|
+
persistPendingApproval({}, pendingApprovalRequest, { cwd, mode, storePath: options.approvalStorePath });
|
|
2055
|
+
} else {
|
|
2056
|
+
removeStoredApproval(approvalId, { storePath: options.approvalStorePath });
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
history.push({ role: 'user', content: trimmed });
|
|
2060
|
+
history.push({
|
|
2061
|
+
role: 'assistant',
|
|
2062
|
+
content: ['Accepted.', approvalExecution && approvalExecution.message ? approvalExecution.message : ''].filter(Boolean).join('\n'),
|
|
2063
|
+
...(
|
|
2064
|
+
approvalExecutionShouldRemainPending(approvalExecution && approvalExecution.response)
|
|
2065
|
+
? { approval_request: pendingApprovalRequest }
|
|
2066
|
+
: {}
|
|
2067
|
+
),
|
|
2068
|
+
});
|
|
2430
2069
|
return false;
|
|
2431
2070
|
}
|
|
2432
2071
|
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
await
|
|
2072
|
+
const pendingTaskPreview = latestPendingTaskPreview(history);
|
|
2073
|
+
const route = pendingTaskPreview ? 'cloud' : options.route;
|
|
2074
|
+
if (!(await ensureRuntimeReady())) return false;
|
|
2075
|
+
const turnFunction = options.turnFunction || turnFunctionForMode(mode);
|
|
2076
|
+
const result = await turnFunction(trimmed, { mode, cwd, history, output, route, business: options.business, verify: options.verify, conversationId });
|
|
2077
|
+
if (result.output && !result.output.endsWith('\n')) output.write('\n');
|
|
2078
|
+
const approvalExecution = normalizeMode(mode) === 'code-fast'
|
|
2079
|
+
? null
|
|
2080
|
+
: await executeApprovalFromState(result, {
|
|
2081
|
+
model: modelForMode(mode),
|
|
2082
|
+
output,
|
|
2083
|
+
...(options.postApproval ? { postApproval: options.postApproval } : {}),
|
|
2084
|
+
});
|
|
2085
|
+
output.write(`${formatDoneLine(result.durationMs, creditsFromState(result))}\n\n`);
|
|
2086
|
+
const receipt = receiptFromState(result);
|
|
2087
|
+
const approvalRequest = approvalExecution
|
|
2088
|
+
? (
|
|
2089
|
+
approvalExecutionShouldRemainPending(approvalExecution.response)
|
|
2090
|
+
? approvalExecution.approval_request
|
|
2091
|
+
: null
|
|
2092
|
+
)
|
|
2093
|
+
: approvalRequestFromReceipt(receipt);
|
|
2094
|
+
if (approvalRequest && !options.turnFunction) {
|
|
2095
|
+
persistPendingApproval(receipt, approvalRequest, { cwd, mode, storePath: options.approvalStorePath });
|
|
2096
|
+
}
|
|
2097
|
+
const assistantContent = [
|
|
2098
|
+
result.output || '',
|
|
2099
|
+
approvalExecution && approvalExecution.message ? approvalExecution.message : '',
|
|
2100
|
+
].filter(Boolean).join('\n');
|
|
2101
|
+
history.push({ role: 'user', content: trimmed });
|
|
2102
|
+
history.push({
|
|
2103
|
+
role: 'assistant',
|
|
2104
|
+
content: assistantContent,
|
|
2105
|
+
...(receipt && receipt.task_preview ? { task_preview: receipt.task_preview } : {}),
|
|
2106
|
+
...(approvalRequest ? { approval_request: approvalRequest } : {}),
|
|
2107
|
+
});
|
|
2436
2108
|
return false;
|
|
2437
2109
|
};
|
|
2438
2110
|
|
|
@@ -2445,445 +2117,29 @@ async function chat(options = {}) {
|
|
|
2445
2117
|
return;
|
|
2446
2118
|
}
|
|
2447
2119
|
|
|
2448
|
-
const rl = readline.createInterface({
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
crlfDelay: Infinity,
|
|
2453
|
-
});
|
|
2454
|
-
readline.emitKeypressEvents(input, rl);
|
|
2455
|
-
// Bracketed paste: terminals wrap pasted text in \x1b[200~ … \x1b[201~ so a
|
|
2456
|
-
// multi-line paste lands as ONE message instead of submitting on every
|
|
2457
|
-
// newline. readline + the multiline hook already understand the markers; we
|
|
2458
|
-
// just have to turn the mode on (and off again on every exit path).
|
|
2459
|
-
const setBracketedPaste = (on) => {
|
|
2460
|
-
if (baseOutput && baseOutput.isTTY && typeof baseOutput.write === 'function') {
|
|
2461
|
-
baseOutput.write(on ? '\x1b[?2004h' : '\x1b[?2004l');
|
|
2462
|
-
}
|
|
2463
|
-
};
|
|
2464
|
-
const disableBracketedPasteOnExit = () => setBracketedPaste(false);
|
|
2465
|
-
setBracketedPaste(true);
|
|
2466
|
-
process.once('exit', disableBracketedPasteOnExit);
|
|
2467
|
-
// readline can't emit a 'line' string that holds a literal newline, so the
|
|
2468
|
-
// multiline hook hands us the true buffer here and ask() prefers it.
|
|
2469
|
-
let pendingMultilineLine = null;
|
|
2470
|
-
const detachMultilineInput = attachMultilineChatInput(rl, {
|
|
2471
|
-
insertBreak: insertMultilineBreak,
|
|
2472
|
-
onSubmit: (line) => { pendingMultilineLine = line; },
|
|
2473
|
-
});
|
|
2474
|
-
const boxLayoutOptions = () => inputBoxLayoutOptions(baseOutput, { bypassPermissions });
|
|
2475
|
-
let exiting = false;
|
|
2476
|
-
let awaitingInput = false;
|
|
2477
|
-
// Repaint the box bottom rule after every readline render so the frame stays
|
|
2478
|
-
// closed while typing (empty, wrapped, and multiline input alike).
|
|
2479
|
-
const baseRefreshLine = typeof rl._refreshLine === 'function' ? rl._refreshLine.bind(rl) : null;
|
|
2480
|
-
if (baseRefreshLine) {
|
|
2481
|
-
rl._refreshLine = () => {
|
|
2482
|
-
baseRefreshLine();
|
|
2483
|
-
if (awaitingInput) repaintInputBoxBottom(rl, baseOutput, boxLayoutOptions());
|
|
2484
|
-
};
|
|
2485
|
-
}
|
|
2486
|
-
let turnAbort = null;
|
|
2487
|
-
options.registerAbort = (fn) => {
|
|
2488
|
-
turnAbort = typeof fn === 'function' ? fn : null;
|
|
2489
|
-
};
|
|
2490
|
-
const onChatKeypress = (_str, key) => {
|
|
2491
|
-
if (!awaitingInput) return;
|
|
2492
|
-
if (!isPermissionToggleKey(key)) return;
|
|
2493
|
-
bypassPermissions = !bypassPermissions;
|
|
2494
|
-
const persistPrefs = options.persistPrefs !== false;
|
|
2495
|
-
setBypassPermissions(bypassPermissions, { persist: persistPrefs });
|
|
2496
|
-
if (logger) logger.write(`[shift+tab permissions ${bypassPermissions ? 'bypass' : 'safe'}]\n`);
|
|
2497
|
-
// The mode label lives on the status line below the box now, so repaint
|
|
2498
|
-
// that (it also restores the cursor) instead of redrawing the plain top.
|
|
2499
|
-
repaintInputBoxBottom(rl, baseOutput, boxLayoutOptions());
|
|
2500
|
-
};
|
|
2501
|
-
input.prependListener('keypress', onChatKeypress);
|
|
2502
|
-
rl.on('SIGINT', () => {
|
|
2503
|
-
if (turnAbort) {
|
|
2504
|
-
turnAbort();
|
|
2505
|
-
return;
|
|
2506
|
-
}
|
|
2507
|
-
exiting = true;
|
|
2508
|
-
output.write('\n');
|
|
2509
|
-
rl.close();
|
|
2510
|
-
});
|
|
2511
|
-
const ask = () => new Promise((resolve, reject) => {
|
|
2512
|
-
function onClose() {
|
|
2513
|
-
reject(new Error('CHAT_CLOSED'));
|
|
2514
|
-
}
|
|
2515
|
-
rl.once('close', onClose);
|
|
2516
|
-
awaitingInput = true;
|
|
2517
|
-
const layout = boxLayoutOptions();
|
|
2518
|
-
output.write(`\n${formatInputBoxTop(layout)}\n`);
|
|
2519
|
-
pendingMultilineLine = null;
|
|
2520
|
-
rl.question(formatChatInputPrompt(mode, { ...layout, goal: goalState }), (answer) => {
|
|
2521
|
-
awaitingInput = false;
|
|
2522
|
-
rl.removeListener('close', onClose);
|
|
2523
|
-
closeInputBox(output, layout);
|
|
2524
|
-
const captured = pendingMultilineLine;
|
|
2525
|
-
pendingMultilineLine = null;
|
|
2526
|
-
resolve(stripMultilineCsiText(captured != null ? captured : answer));
|
|
2527
|
-
});
|
|
2528
|
-
});
|
|
2529
|
-
while (!exiting) {
|
|
2530
|
-
let line = '';
|
|
2531
|
-
try {
|
|
2532
|
-
line = await ask();
|
|
2533
|
-
} catch (error) {
|
|
2534
|
-
if (error && error.message === 'CHAT_CLOSED') break;
|
|
2535
|
-
throw error;
|
|
2536
|
-
}
|
|
2120
|
+
const rl = readline.createInterface({ input, output: baseOutput, completer: chatCompleter });
|
|
2121
|
+
const ask = () => new Promise(resolve => rl.question(formatPrompt(mode, baseOutput), resolve));
|
|
2122
|
+
while (true) {
|
|
2123
|
+
const line = await ask();
|
|
2537
2124
|
if (await runLine(line)) {
|
|
2538
2125
|
rl.close();
|
|
2539
2126
|
break;
|
|
2540
2127
|
}
|
|
2541
2128
|
}
|
|
2542
|
-
input.removeListener('keypress', onChatKeypress);
|
|
2543
|
-
detachMultilineInput();
|
|
2544
|
-
if (baseRefreshLine) rl._refreshLine = baseRefreshLine;
|
|
2545
|
-
setBracketedPaste(false);
|
|
2546
|
-
process.removeListener('exit', disableBracketedPasteOnExit);
|
|
2547
2129
|
if (logger) logger.close(0);
|
|
2548
2130
|
}
|
|
2549
2131
|
|
|
2550
|
-
function
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
let newest = null;
|
|
2554
|
-
for (const name of fs.readdirSync(runsDir)) {
|
|
2555
|
-
if (!/^ax-.*\.log$/.test(name)) continue;
|
|
2556
|
-
const full = path.join(runsDir, name);
|
|
2557
|
-
let stat = null;
|
|
2558
|
-
try {
|
|
2559
|
-
stat = fs.statSync(full);
|
|
2560
|
-
} catch {
|
|
2561
|
-
continue;
|
|
2562
|
-
}
|
|
2563
|
-
if (!stat.isFile() || stat.mtimeMs < sinceMs) continue;
|
|
2564
|
-
if (!newest || stat.mtimeMs > newest.mtimeMs) newest = { path: full, mtimeMs: stat.mtimeMs };
|
|
2565
|
-
}
|
|
2566
|
-
return newest ? newest.path : null;
|
|
2567
|
-
}
|
|
2568
|
-
|
|
2569
|
-
function makeChecklistItem(id, label, passed, evidence = '') {
|
|
2570
|
-
return { id, label, passed: passed === true, evidence: compactText(evidence, 180) };
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
function compactText(text, max = 180) {
|
|
2574
|
-
const one = String(text || '').replace(/\s+/g, ' ').trim();
|
|
2575
|
-
if (!one) return '';
|
|
2576
|
-
return one.length <= max ? one : `${one.slice(0, max - 3)}...`;
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
|
-
function buildDogfoodChecklist({ cases, outputText, turnCalls, youtubeCalls, logPath }) {
|
|
2580
|
-
const turnCases = cases.filter(item => item.kind === 'turn');
|
|
2581
|
-
const paired = turnCases.map((item, index) => ({ case: item, call: turnCalls[index] || null }));
|
|
2582
|
-
const smallTalk = paired.filter(item => /small_talk|short_noise|greeting/.test(item.case.coverage));
|
|
2583
|
-
const workspace = paired.filter(item => /workspace_|github_workspace_mutation|max_cloud/.test(item.case.coverage));
|
|
2584
|
-
const cloud = paired.filter(item => item.case.expectRoute === 'cloud');
|
|
2585
|
-
const bypassCall = paired.find(item => item.case.coverage === 'connector_write_bypass')?.call;
|
|
2586
|
-
const safeCalls = paired.filter(item => /connector_write_(safe|resafe)/.test(item.case.coverage)).map(item => item.call);
|
|
2587
|
-
const maxCall = paired.find(item => item.case.coverage === 'max_cloud')?.call;
|
|
2588
|
-
const firstHeader = outputText.split(/\r?\n/).slice(0, 4).map(stripAnsi);
|
|
2589
|
-
const longestHeader = firstHeader.reduce((max, line) => Math.max(max, line.length), 0);
|
|
2590
|
-
const modelPattern = /atris:|composer-2-5|gpt-|kimi|fable|fireworks|openrouter/i;
|
|
2591
|
-
const errorPattern = /Cannot access 'business'|Blocked: direct network access|ReferenceError|TypeError|UnhandledPromiseRejection|ECONNREFUSED|HTTP 5\d\d/i;
|
|
2592
|
-
|
|
2593
|
-
return [
|
|
2594
|
-
makeChecklistItem('loops_25', 'Ran the requested loop count', cases.length === 25, `${cases.length}/25 loops`),
|
|
2595
|
-
makeChecklistItem('header_compact', 'Chat header fits a small terminal', longestHeader <= 96 && /shift\+enter/.test(outputText), `longest header line ${longestHeader}`),
|
|
2596
|
-
makeChecklistItem('menu_discoverable', 'Slash menu exposes key controls', /\/goal/.test(outputText) && /\/fast/.test(outputText) && /\/safe/.test(outputText), 'menu printed by /help and unknown slash'),
|
|
2597
|
-
makeChecklistItem('small_talk_cloud', 'Greetings/noise avoid workspace listings', smallTalk.length >= 4 && smallTalk.every(item => item.call?.route === 'cloud' && !item.call?.workspace_path), smallTalk.map(item => `${item.case.input}:${item.call?.route}`).join(', ')),
|
|
2598
|
-
makeChecklistItem('workspace_cloud_default', 'Workspace/code asks stay hosted by default', workspace.length >= 5 && workspace.every(item => item.call?.route === 'cloud' && !item.call?.workspace_path), workspace.map(item => `${item.case.coverage}:${item.call?.route}`).join(', ')),
|
|
2599
|
-
makeChecklistItem('connectors_cloud', 'Connector reads/writes stay cloud by default', cloud.length >= 8 && cloud.every(item => item.call?.route === 'cloud'), cloud.map(item => `${item.case.coverage}:${item.call?.route}`).join(', ')),
|
|
2600
|
-
makeChecklistItem('tier_switches', 'Tier commands affect following turns', maxCall?.mode === 'max', `max turn mode ${maxCall?.mode || 'missing'}`),
|
|
2601
|
-
makeChecklistItem('permission_gates', 'Safe/bypass gates are visible in payloads', bypassCall?.allow_external_actions === true && safeCalls.every(call => call && call.allow_external_actions !== true), `bypass=${bypassCall?.allow_external_actions === true}; safe=${safeCalls.length}`),
|
|
2602
|
-
makeChecklistItem('youtube_shortcut', 'YouTube URLs route to local processor path', youtubeCalls.length === 1 && !turnCalls.some(call => extractYoutubeUrl(call.message)), `${youtubeCalls.length} youtube call(s)`),
|
|
2603
|
-
makeChecklistItem('no_model_ids', 'UI hides backend model ids', !modelPattern.test(outputText), 'no raw model ids in transcript'),
|
|
2604
|
-
makeChecklistItem('no_error_patterns', 'Transcript has no known crash/block patterns', !errorPattern.test(outputText), 'business TDZ/network/crash patterns absent'),
|
|
2605
|
-
makeChecklistItem('log_written', 'Dogfood leaves an inspectable ax run log', Boolean(logPath && fs.existsSync(logPath)), logPath || 'missing'),
|
|
2606
|
-
];
|
|
2607
|
-
}
|
|
2608
|
-
|
|
2609
|
-
async function runChatDogfood(options = {}) {
|
|
2610
|
-
const cwd = options.cwd || process.cwd();
|
|
2611
|
-
const loops = Math.max(1, Number(options.loops) || 25);
|
|
2612
|
-
const cases = buildChatDogfoodCases(loops);
|
|
2613
|
-
const startedAt = Date.now();
|
|
2614
|
-
const chunks = [];
|
|
2615
|
-
const output = options.output || {
|
|
2616
|
-
isTTY: true,
|
|
2617
|
-
color: false,
|
|
2618
|
-
write(chunk) {
|
|
2619
|
-
chunks.push(String(chunk || ''));
|
|
2620
|
-
return true;
|
|
2621
|
-
},
|
|
2622
|
-
};
|
|
2623
|
-
const turnCalls = [];
|
|
2624
|
-
const youtubeCalls = [];
|
|
2625
|
-
const turnFunction = options.live === true ? undefined : async (message, turnOptions = {}) => {
|
|
2626
|
-
const route = resolveRoute(message, turnOptions);
|
|
2627
|
-
const payload = buildPayload(message, { ...turnOptions, route });
|
|
2628
|
-
const record = {
|
|
2629
|
-
message,
|
|
2630
|
-
mode: normalizeMode(turnOptions.mode),
|
|
2631
|
-
route,
|
|
2632
|
-
workspace_path: payload.workspace_path || null,
|
|
2633
|
-
max_turns: payload.max_turns,
|
|
2634
|
-
bypass_permissions: payload.bypass_permissions === true,
|
|
2635
|
-
allow_external_actions: payload.allow_external_actions === true,
|
|
2636
|
-
};
|
|
2637
|
-
turnCalls.push(record);
|
|
2638
|
-
const text = `dogfood ok: ${record.mode}/${record.route}`;
|
|
2639
|
-
if (turnOptions.output) turnOptions.output.write(text);
|
|
2640
|
-
return {
|
|
2641
|
-
output: text,
|
|
2642
|
-
durationMs: 1,
|
|
2643
|
-
events: [{ type: 'receipt', receipt: { billing: { billed: false }, dogfood: record } }],
|
|
2644
|
-
};
|
|
2645
|
-
};
|
|
2646
|
-
const youtubeCommand = async (argv, deps = {}) => {
|
|
2647
|
-
youtubeCalls.push(argv);
|
|
2648
|
-
if (deps.output) deps.output(`dogfood youtube: ${argv[0] || 'missing-url'}`);
|
|
2649
|
-
return 0;
|
|
2650
|
-
};
|
|
2651
|
-
const input = Readable.from(cases.map(item => `${item.input}\n`).concat('exit\n'));
|
|
2652
|
-
|
|
2653
|
-
await chat({
|
|
2654
|
-
mode: 'fast',
|
|
2655
|
-
cwd,
|
|
2656
|
-
input,
|
|
2657
|
-
output,
|
|
2658
|
-
turnFunction,
|
|
2659
|
-
youtubeCommand: options.youtubeCommand || youtubeCommand,
|
|
2660
|
-
persistPrefs: false,
|
|
2661
|
-
bypassPermissions: false,
|
|
2662
|
-
});
|
|
2663
|
-
|
|
2664
|
-
const outputText = typeof output.text === 'function' ? output.text() : chunks.join('');
|
|
2665
|
-
const logPath = newestAxLog(cwd, startedAt - 1000);
|
|
2666
|
-
const checklist = buildDogfoodChecklist({ cases, outputText, turnCalls, youtubeCalls, logPath });
|
|
2667
|
-
const failures = checklist.filter(item => !item.passed);
|
|
2668
|
-
return {
|
|
2669
|
-
schema: 'atris.ax_chat_dogfood.v1',
|
|
2670
|
-
mode: 'fast',
|
|
2671
|
-
loops_requested: loops,
|
|
2672
|
-
loops_run: cases.length,
|
|
2673
|
-
dry_run: options.live !== true,
|
|
2674
|
-
cwd,
|
|
2675
|
-
log_path: logPath,
|
|
2676
|
-
turn_calls: turnCalls,
|
|
2677
|
-
youtube_calls: youtubeCalls,
|
|
2678
|
-
checklist,
|
|
2679
|
-
failures,
|
|
2680
|
-
output: outputText,
|
|
2681
|
-
};
|
|
2682
|
-
}
|
|
2683
|
-
|
|
2684
|
-
function formatChatDogfoodReport(report) {
|
|
2685
|
-
const passed = report.checklist.filter(item => item.passed).length;
|
|
2686
|
-
const lines = [
|
|
2687
|
-
'Ax fast chat dogfood',
|
|
2688
|
-
`loops: ${report.loops_run}/${report.loops_requested}`,
|
|
2689
|
-
`checklist: ${passed}/${report.checklist.length} passed`,
|
|
2690
|
-
`log: ${report.log_path || 'missing'}`,
|
|
2691
|
-
'',
|
|
2692
|
-
];
|
|
2693
|
-
for (const item of report.checklist) {
|
|
2694
|
-
lines.push(`${item.passed ? 'ok' : 'fail'} ${item.id} — ${item.evidence || item.label}`);
|
|
2695
|
-
}
|
|
2696
|
-
if (report.failures.length) {
|
|
2697
|
-
lines.push('');
|
|
2698
|
-
lines.push(`failures: ${report.failures.map(item => item.id).join(', ')}`);
|
|
2699
|
-
}
|
|
2700
|
-
return lines.join('\n');
|
|
2701
|
-
}
|
|
2702
|
-
|
|
2703
|
-
function safeJsonFile(filePath) {
|
|
2704
|
-
try {
|
|
2705
|
-
if (!fs.existsSync(filePath)) return null;
|
|
2706
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
2707
|
-
} catch {
|
|
2708
|
-
return null;
|
|
2709
|
-
}
|
|
2710
|
-
}
|
|
2711
|
-
|
|
2712
|
-
function readJsonlRows(filePath) {
|
|
2713
|
-
try {
|
|
2714
|
-
if (!fs.existsSync(filePath)) return [];
|
|
2715
|
-
return fs.readFileSync(filePath, 'utf8')
|
|
2716
|
-
.split(/\r?\n/)
|
|
2717
|
-
.filter(Boolean)
|
|
2718
|
-
.map(line => {
|
|
2719
|
-
try {
|
|
2720
|
-
return JSON.parse(line);
|
|
2721
|
-
} catch {
|
|
2722
|
-
return null;
|
|
2723
|
-
}
|
|
2724
|
-
})
|
|
2725
|
-
.filter(Boolean);
|
|
2726
|
-
} catch {
|
|
2727
|
-
return [];
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
|
|
2731
|
-
function parseDogfoodVerifierOutput(stdout = '') {
|
|
2732
|
-
const text = String(stdout || '');
|
|
2733
|
-
const loops = text.match(/loops:\s*(\d+)\/(\d+)/i);
|
|
2734
|
-
const checklist = text.match(/checklist:\s*(\d+)\/(\d+)\s+passed/i);
|
|
2735
|
-
const log = text.match(/^log:\s*(.+)$/im);
|
|
2736
|
-
return {
|
|
2737
|
-
loops_passed: loops ? Number(loops[1]) : 0,
|
|
2738
|
-
loops_total: loops ? Number(loops[2]) : 0,
|
|
2739
|
-
checklist_passed: checklist ? Number(checklist[1]) : 0,
|
|
2740
|
-
checklist_total: checklist ? Number(checklist[2]) : 0,
|
|
2741
|
-
log_path: log ? log[1].trim() : null,
|
|
2742
|
-
};
|
|
2743
|
-
}
|
|
2744
|
-
|
|
2745
|
-
function latestDogfoodMission(cwd = process.cwd()) {
|
|
2746
|
-
const rows = readJsonlRows(path.join(cwd, '.atris', 'state', 'missions.jsonl'));
|
|
2747
|
-
const byId = new Map();
|
|
2748
|
-
for (const row of rows) {
|
|
2749
|
-
if (!row || row.schema !== 'atris.mission.v1') continue;
|
|
2750
|
-
if (!/--dogfood-chat/.test(String(row.verifier || ''))) continue;
|
|
2751
|
-
byId.set(row.id, row);
|
|
2752
|
-
}
|
|
2753
|
-
const missions = Array.from(byId.values());
|
|
2754
|
-
missions.sort((a, b) => new Date(b.updated_at || b.created_at || 0) - new Date(a.updated_at || a.created_at || 0));
|
|
2755
|
-
return missions[0] || null;
|
|
2756
|
-
}
|
|
2757
|
-
|
|
2758
|
-
function collectDogfoodTicks(cwd, mission) {
|
|
2759
|
-
const runsDir = path.join(cwd, 'atris', 'runs');
|
|
2760
|
-
const out = new Map();
|
|
2761
|
-
if (!mission || !mission.id || !fs.existsSync(runsDir)) return [];
|
|
2762
|
-
const prefix = `mission-${mission.id}-`;
|
|
2763
|
-
for (const name of fs.readdirSync(runsDir)) {
|
|
2764
|
-
if (!name.startsWith(prefix) || !name.endsWith('.json')) continue;
|
|
2765
|
-
const full = path.join(runsDir, name);
|
|
2766
|
-
const payload = safeJsonFile(full);
|
|
2767
|
-
if (!payload) continue;
|
|
2768
|
-
let mtimeMs = 0;
|
|
2769
|
-
try {
|
|
2770
|
-
mtimeMs = fs.statSync(full).mtimeMs;
|
|
2771
|
-
} catch {}
|
|
2772
|
-
const body = payload.result || payload;
|
|
2773
|
-
const stdout = body.verifier_result?.stdout || payload.verifier_result?.stdout || payload.mission?.verifier_result?.stdout || '';
|
|
2774
|
-
const ticks = [];
|
|
2775
|
-
if (payload.tick) ticks.push(payload.tick);
|
|
2776
|
-
if (body.tick) ticks.push(body.tick);
|
|
2777
|
-
if (Array.isArray(payload.ticks)) ticks.push(...payload.ticks);
|
|
2778
|
-
if (Array.isArray(body.ticks)) ticks.push(...body.ticks);
|
|
2779
|
-
for (const tick of ticks) {
|
|
2780
|
-
const index = Number(tick && tick.tick_index);
|
|
2781
|
-
if (!Number.isFinite(index) || index < 1) continue;
|
|
2782
|
-
const previous = out.get(index);
|
|
2783
|
-
if (previous && previous.mtimeMs > mtimeMs) continue;
|
|
2784
|
-
const summary = parseDogfoodVerifierOutput(stdout);
|
|
2785
|
-
out.set(index, {
|
|
2786
|
-
index,
|
|
2787
|
-
verifier_passed: tick.verifier_passed === true || body.verifier_result?.passed === true || payload.verifier_result?.passed === true,
|
|
2788
|
-
receipt_path: path.relative(cwd, full),
|
|
2789
|
-
mtimeMs,
|
|
2790
|
-
...summary,
|
|
2791
|
-
});
|
|
2792
|
-
}
|
|
2793
|
-
}
|
|
2794
|
-
return Array.from(out.values()).sort((a, b) => a.index - b.index);
|
|
2795
|
-
}
|
|
2796
|
-
|
|
2797
|
-
function dogfoodCrontabInstalled(crontabText = null) {
|
|
2798
|
-
const text = crontabText !== null ? String(crontabText || '') : (() => {
|
|
2799
|
-
try {
|
|
2800
|
-
const res = spawnSync('crontab', ['-l'], { encoding: 'utf8', timeout: 8000 });
|
|
2801
|
-
return res.status === 0 ? String(res.stdout || '') : '';
|
|
2802
|
-
} catch {
|
|
2803
|
-
return '';
|
|
2804
|
-
}
|
|
2805
|
-
})();
|
|
2806
|
-
return /ATRIS_AX_FAST_CHAT_DOGFOOD/.test(text);
|
|
2807
|
-
}
|
|
2808
|
-
|
|
2809
|
-
function countDogfoodCronRuns(cwd = process.cwd(), cronLogText = null) {
|
|
2810
|
-
const text = cronLogText !== null
|
|
2811
|
-
? String(cronLogText || '')
|
|
2812
|
-
: String(safeReadText(path.join(cwd, 'atris', 'runs', 'ax-fast-chat-overnight-cron.log')) || '');
|
|
2813
|
-
return (text.match(/^=== ax fast chat dogfood tick /gm) || []).length;
|
|
2814
|
-
}
|
|
2815
|
-
|
|
2816
|
-
function safeReadText(filePath) {
|
|
2817
|
-
try {
|
|
2818
|
-
if (!fs.existsSync(filePath)) return '';
|
|
2819
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
2820
|
-
} catch {
|
|
2821
|
-
return '';
|
|
2822
|
-
}
|
|
2132
|
+
function printBackendHint() {
|
|
2133
|
+
console.log('');
|
|
2134
|
+
console.log(formatBackendHint());
|
|
2823
2135
|
}
|
|
2824
2136
|
|
|
2825
|
-
function
|
|
2826
|
-
|
|
2827
|
-
const targetLoops = Math.max(1, Number(options.targetLoops) || 25);
|
|
2828
|
-
const mission = options.mission || latestDogfoodMission(cwd);
|
|
2829
|
-
const ticks = collectDogfoodTicks(cwd, mission);
|
|
2830
|
-
const cleanTicks = ticks.filter(tick => tick.verifier_passed && tick.loops_passed > 0);
|
|
2831
|
-
const cleanLoops = cleanTicks.reduce((sum, tick) => sum + tick.loops_passed, 0);
|
|
2832
|
-
const latestClean = cleanTicks[cleanTicks.length - 1] || null;
|
|
2833
|
-
const latestTick = ticks[ticks.length - 1] || null;
|
|
2834
|
-
const checklistPassed = latestClean && latestClean.checklist_total > 0 && latestClean.checklist_passed === latestClean.checklist_total;
|
|
2835
|
-
const cronInstalled = dogfoodCrontabInstalled(options.crontabText ?? null);
|
|
2836
|
-
const cronRuns = countDogfoodCronRuns(cwd, options.cronLogText ?? null);
|
|
2837
|
-
return {
|
|
2838
|
-
schema: 'atris.ax_chat_dogfood_status.v1',
|
|
2839
|
-
cwd,
|
|
2840
|
-
mission_id: mission ? mission.id : null,
|
|
2841
|
-
mission_status: mission ? mission.status : null,
|
|
2842
|
-
mission_cadence: mission ? mission.cadence : null,
|
|
2843
|
-
last_tick_index: mission ? Number(mission.last_tick_index || 0) : 0,
|
|
2844
|
-
last_tick_at: mission ? mission.last_tick_at || null : null,
|
|
2845
|
-
target_loops: targetLoops,
|
|
2846
|
-
clean_chat_loops: cleanLoops,
|
|
2847
|
-
remaining_loops: Math.max(0, targetLoops - cleanLoops),
|
|
2848
|
-
target_met: cleanLoops >= targetLoops,
|
|
2849
|
-
clean_tick_count: cleanTicks.length,
|
|
2850
|
-
latest_receipt_path: latestClean ? latestClean.receipt_path : latestTick ? latestTick.receipt_path : null,
|
|
2851
|
-
latest_log_path: latestClean ? latestClean.log_path : null,
|
|
2852
|
-
latest_checklist_passed: checklistPassed === true,
|
|
2853
|
-
cron_installed: cronInstalled,
|
|
2854
|
-
cron_runs: cronRuns,
|
|
2855
|
-
overnight_proven: cronRuns > 0 && cleanLoops >= targetLoops,
|
|
2856
|
-
};
|
|
2137
|
+
function backendStartCommand() {
|
|
2138
|
+
return `cd /Users/keshavrao/arena/atrisos-backend/backend && ATRIS2_ALLOW_LOCAL_WORKSPACE=1 ENVIRONMENT=development ENV=development ../venv/bin/uvicorn main:app --host ${BACKEND.host} --port ${BACKEND.port}`;
|
|
2857
2139
|
}
|
|
2858
2140
|
|
|
2859
|
-
function
|
|
2860
|
-
|
|
2861
|
-
'Ax fast chat overnight',
|
|
2862
|
-
`mission: ${status.mission_id || 'missing'}`,
|
|
2863
|
-
`status: ${status.mission_status || 'missing'} · cadence ${status.mission_cadence || 'n/a'}`,
|
|
2864
|
-
`loops: ${status.clean_chat_loops}/${status.target_loops} clean (${status.remaining_loops} remaining)`,
|
|
2865
|
-
`ticks: ${status.clean_tick_count} clean · last ${status.last_tick_index || 0} at ${status.last_tick_at || 'never'}`,
|
|
2866
|
-
`checklist: ${status.latest_checklist_passed ? 'passed' : 'missing/failing'}`,
|
|
2867
|
-
`cron: ${status.cron_installed ? 'installed' : 'missing'} · runs ${status.cron_runs}`,
|
|
2868
|
-
`receipt: ${status.latest_receipt_path || 'missing'}`,
|
|
2869
|
-
`log: ${status.latest_log_path || 'missing'}`,
|
|
2870
|
-
];
|
|
2871
|
-
lines.push(status.overnight_proven ? 'result: overnight 25-loop proof is complete' : 'result: still collecting overnight proof');
|
|
2872
|
-
return lines.join('\n');
|
|
2873
|
-
}
|
|
2874
|
-
|
|
2875
|
-
function printBackendHint(options = {}) {
|
|
2876
|
-
console.log('');
|
|
2877
|
-
console.log('Hosted lane:');
|
|
2878
|
-
console.log('ax --cloud "hello"');
|
|
2879
|
-
console.log('');
|
|
2880
|
-
console.log('Cloud API:');
|
|
2881
|
-
console.log(`ATRIS_API_BASE=${BACKEND.publicBase}`);
|
|
2882
|
-
if (options.route === 'local' || options.forceLocal) {
|
|
2883
|
-
console.log('');
|
|
2884
|
-
console.log('Local developer lane:');
|
|
2885
|
-
console.log(`Set AX_BACKEND_URL=http://${BACKEND.host}:${BACKEND.port} after starting your local Atris2 service.`);
|
|
2886
|
-
}
|
|
2141
|
+
function formatBackendHint() {
|
|
2142
|
+
return `Start backend:\n${backendStartCommand()}`;
|
|
2887
2143
|
}
|
|
2888
2144
|
|
|
2889
2145
|
function bufferedOutput() {
|
|
@@ -2942,6 +2198,535 @@ async function runBenchmarkCase(label, fn, results, output) {
|
|
|
2942
2198
|
}
|
|
2943
2199
|
}
|
|
2944
2200
|
|
|
2201
|
+
async function runSelfTestCase(label, fn, results, output) {
|
|
2202
|
+
output.write(`- ${label} ... `);
|
|
2203
|
+
try {
|
|
2204
|
+
await fn();
|
|
2205
|
+
output.write('ok\n');
|
|
2206
|
+
results.push({ label, ok: true });
|
|
2207
|
+
} catch (error) {
|
|
2208
|
+
output.write(`fail - ${error.message}\n`);
|
|
2209
|
+
results.push({ label, ok: false, error: error.message });
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
function assertSelfTest(condition, message) {
|
|
2214
|
+
if (!condition) throw new Error(message);
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
async function runSelfTest(options = {}) {
|
|
2218
|
+
const output = options.output || process.stdout;
|
|
2219
|
+
const results = [];
|
|
2220
|
+
output.write('AX self-test\n\n');
|
|
2221
|
+
|
|
2222
|
+
await runSelfTestCase('doctor redaction', async () => {
|
|
2223
|
+
const health = await buildRuntimeHealth({
|
|
2224
|
+
healthRes: {
|
|
2225
|
+
ok: true,
|
|
2226
|
+
status: 200,
|
|
2227
|
+
data: {
|
|
2228
|
+
ready: true,
|
|
2229
|
+
models: [{
|
|
2230
|
+
id: 'atris:fast',
|
|
2231
|
+
route: 'fireworks',
|
|
2232
|
+
chat_model: 'fireworks:glm-5.2',
|
|
2233
|
+
tool_model: 'fireworks:glm-5.2',
|
|
2234
|
+
ready: true,
|
|
2235
|
+
}],
|
|
2236
|
+
},
|
|
2237
|
+
},
|
|
2238
|
+
});
|
|
2239
|
+
const text = formatRuntimeHealth(health, { color: false });
|
|
2240
|
+
assertSelfTest(/backend\s+ready/.test(text), 'backend readiness missing');
|
|
2241
|
+
assertSelfTest(/fast\s+ready/.test(text), 'fast readiness missing');
|
|
2242
|
+
assertSelfTest(!/atris:|fireworks|glm|secret|token/i.test(text), 'doctor output leaked provider/debug text');
|
|
2243
|
+
}, results, output);
|
|
2244
|
+
|
|
2245
|
+
await runSelfTestCase('chat backend offline preflight', async () => {
|
|
2246
|
+
const previousAuto = process.env.AX_AUTO_LOG;
|
|
2247
|
+
process.env.AX_AUTO_LOG = '0';
|
|
2248
|
+
const chunks = [];
|
|
2249
|
+
const chatOutput = {
|
|
2250
|
+
isTTY: false,
|
|
2251
|
+
write(chunk) {
|
|
2252
|
+
chunks.push(String(chunk || ''));
|
|
2253
|
+
return true;
|
|
2254
|
+
},
|
|
2255
|
+
};
|
|
2256
|
+
let healthCalls = 0;
|
|
2257
|
+
try {
|
|
2258
|
+
await chat({
|
|
2259
|
+
mode: 'fast',
|
|
2260
|
+
cwd: os.tmpdir(),
|
|
2261
|
+
input: Readable.from(['hi\n', 'exit\n']),
|
|
2262
|
+
output: chatOutput,
|
|
2263
|
+
runtimeHealth: async () => {
|
|
2264
|
+
healthCalls += 1;
|
|
2265
|
+
return {
|
|
2266
|
+
schema: 'ax.runtime_health.v1',
|
|
2267
|
+
backend: { ready: false, reachable: false, status: 0, error: 'ECONNREFUSED secret-token' },
|
|
2268
|
+
fast: { ready: false, route_ready: false },
|
|
2269
|
+
permissions: { ready: false },
|
|
2270
|
+
};
|
|
2271
|
+
},
|
|
2272
|
+
});
|
|
2273
|
+
} finally {
|
|
2274
|
+
if (previousAuto === undefined) delete process.env.AX_AUTO_LOG;
|
|
2275
|
+
else process.env.AX_AUTO_LOG = previousAuto;
|
|
2276
|
+
}
|
|
2277
|
+
const text = chunks.join('');
|
|
2278
|
+
assertSelfTest(healthCalls === 1, 'offline preflight repeated unexpectedly');
|
|
2279
|
+
assertSelfTest(/backend\s+offline/.test(text), 'offline backend status missing');
|
|
2280
|
+
assertSelfTest(/Start backend:/.test(text) && /uvicorn main:app/.test(text), 'start-backend command missing');
|
|
2281
|
+
assertSelfTest(!/secret-token/.test(text), 'offline preflight leaked error detail');
|
|
2282
|
+
}, results, output);
|
|
2283
|
+
|
|
2284
|
+
await runSelfTestCase('approval id', async () => {
|
|
2285
|
+
const receipt = {
|
|
2286
|
+
tool_events: [{
|
|
2287
|
+
approval_request: {
|
|
2288
|
+
connector: 'slack',
|
|
2289
|
+
executor_action_type: 'slack_post_message',
|
|
2290
|
+
status: 'approval_required',
|
|
2291
|
+
payload: { channel: '#general', text: 'ship note' },
|
|
2292
|
+
},
|
|
2293
|
+
}],
|
|
2294
|
+
};
|
|
2295
|
+
const first = approvalRequestFromReceipt(receipt);
|
|
2296
|
+
const second = approvalRequestFromReceipt(receipt);
|
|
2297
|
+
assertSelfTest(first && /^apreq_ax_[a-f0-9]{20}$/.test(first.approval_request_id), 'stable approval id missing');
|
|
2298
|
+
assertSelfTest(first.approval_request_id === second.approval_request_id, 'approval id was not stable');
|
|
2299
|
+
}, results, output);
|
|
2300
|
+
|
|
2301
|
+
await runSelfTestCase('calendar approval rail', async () => {
|
|
2302
|
+
const payload = buildPayload('add calendar event today at 2pm', {
|
|
2303
|
+
mode: 'fast',
|
|
2304
|
+
route: 'cloud',
|
|
2305
|
+
connectionContext: {
|
|
2306
|
+
schema: 'atris.connection_capabilities.v1',
|
|
2307
|
+
source: 'ax',
|
|
2308
|
+
connections: [{
|
|
2309
|
+
id: 'google-calendar',
|
|
2310
|
+
connected: true,
|
|
2311
|
+
actions: ['list_events', 'create_event'],
|
|
2312
|
+
authority: { list_events: 'read_only', create_event: 'approval_required' },
|
|
2313
|
+
}],
|
|
2314
|
+
},
|
|
2315
|
+
});
|
|
2316
|
+
assertSelfTest(payload.workspace_path === undefined, 'calendar write should stay on cloud approval route');
|
|
2317
|
+
assertSelfTest(payload.allow_external_actions === undefined, 'calendar write bypassed approval rail');
|
|
2318
|
+
assertSelfTest(payload.cleanup_external_actions === undefined, 'calendar write requested cleanup bypass');
|
|
2319
|
+
}, results, output);
|
|
2320
|
+
|
|
2321
|
+
await runSelfTestCase('calendar approval execution', async () => {
|
|
2322
|
+
const approvalRequest = {
|
|
2323
|
+
connector: 'google_calendar',
|
|
2324
|
+
action: 'create_event',
|
|
2325
|
+
executor_action_type: 'google_calendar_create_event',
|
|
2326
|
+
status: 'approval_required',
|
|
2327
|
+
payload: {
|
|
2328
|
+
summary: 'markoo',
|
|
2329
|
+
start: { dateTime: '2026-06-28T14:00:00-07:00' },
|
|
2330
|
+
end: { dateTime: '2026-06-28T15:00:00-07:00' },
|
|
2331
|
+
},
|
|
2332
|
+
};
|
|
2333
|
+
const previewOnly = {
|
|
2334
|
+
tool_events: [{ approval_request: approvalRequest }],
|
|
2335
|
+
};
|
|
2336
|
+
assertSelfTest(approvalExecutionRequestFromReceipt(previewOnly) === null, 'preview-only calendar receipt would execute');
|
|
2337
|
+
|
|
2338
|
+
const accepted = {
|
|
2339
|
+
task_accept_receipt: {
|
|
2340
|
+
accepted: true,
|
|
2341
|
+
task: 'calendar.create_event',
|
|
2342
|
+
execution_status: 'pending_approval_execution',
|
|
2343
|
+
execution_endpoint: APPROVAL_EXECUTE_PATH,
|
|
2344
|
+
},
|
|
2345
|
+
tool_events: [{ approval_request: approvalRequest }],
|
|
2346
|
+
};
|
|
2347
|
+
const executable = approvalExecutionRequestFromReceipt(accepted);
|
|
2348
|
+
assertSelfTest(executable && /^apreq_ax_[a-f0-9]{20}$/.test(executable.approval_request_id), 'accepted calendar approval lacks stable id');
|
|
2349
|
+
assertSelfTest(executable.executor_action_type === 'google_calendar_create_event', 'wrong calendar executor action');
|
|
2350
|
+
assertSelfTest(executable.payload && executable.payload.summary === 'markoo', 'calendar approval payload changed');
|
|
2351
|
+
}, results, output);
|
|
2352
|
+
|
|
2353
|
+
await runSelfTestCase('calendar chat approval loop', async () => {
|
|
2354
|
+
const chunks = [];
|
|
2355
|
+
const chatOutput = {
|
|
2356
|
+
isTTY: false,
|
|
2357
|
+
write(chunk) {
|
|
2358
|
+
chunks.push(String(chunk || ''));
|
|
2359
|
+
return true;
|
|
2360
|
+
},
|
|
2361
|
+
};
|
|
2362
|
+
const approvalRequest = {
|
|
2363
|
+
connector: 'google_calendar',
|
|
2364
|
+
action: 'create_event',
|
|
2365
|
+
executor_action_type: 'google_calendar_create_event',
|
|
2366
|
+
status: 'approval_required',
|
|
2367
|
+
payload: {
|
|
2368
|
+
summary: 'markoo',
|
|
2369
|
+
start: { dateTime: '2026-06-28T14:00:00-07:00' },
|
|
2370
|
+
end: { dateTime: '2026-06-28T15:00:00-07:00' },
|
|
2371
|
+
},
|
|
2372
|
+
};
|
|
2373
|
+
const firstFinal = "I'll add it today at 2:00 PM.\nWhat should I call it?";
|
|
2374
|
+
const firstTaskPreview = {
|
|
2375
|
+
task: 'calendar.create_event',
|
|
2376
|
+
owner_member: 'harness-engineer',
|
|
2377
|
+
status: 'needs_input',
|
|
2378
|
+
missing: ['title'],
|
|
2379
|
+
slots: {
|
|
2380
|
+
date: '2026-06-28',
|
|
2381
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2382
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2383
|
+
},
|
|
2384
|
+
};
|
|
2385
|
+
const secondFinal = 'Ready to create: markoo today at 2:00 PM.\nCreate it?';
|
|
2386
|
+
const secondTaskPreview = {
|
|
2387
|
+
task: 'calendar.create_event',
|
|
2388
|
+
owner_member: 'harness-engineer',
|
|
2389
|
+
status: 'approval_required',
|
|
2390
|
+
missing: [],
|
|
2391
|
+
slots: {
|
|
2392
|
+
title: 'markoo',
|
|
2393
|
+
date: '2026-06-28',
|
|
2394
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2395
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2396
|
+
},
|
|
2397
|
+
};
|
|
2398
|
+
const turns = [];
|
|
2399
|
+
const approvals = [];
|
|
2400
|
+
await chat({
|
|
2401
|
+
mode: 'fast',
|
|
2402
|
+
route: 'cloud',
|
|
2403
|
+
cwd: os.tmpdir(),
|
|
2404
|
+
conversationId: 'ax-self-test-calendar-chat',
|
|
2405
|
+
input: Readable.from(['add calendar event today at 2pm\n', 'markoo\n', 'yes\n', 'exit\n']),
|
|
2406
|
+
output: chatOutput,
|
|
2407
|
+
turnFunction: async (message, turnOptions) => {
|
|
2408
|
+
turns.push({ message, route: turnOptions.route, historyLength: turnOptions.history.length });
|
|
2409
|
+
const receipt = turns.length === 1
|
|
2410
|
+
? {
|
|
2411
|
+
final: firstFinal,
|
|
2412
|
+
task_preview: firstTaskPreview,
|
|
2413
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'blocked_missing_details' }],
|
|
2414
|
+
}
|
|
2415
|
+
: {
|
|
2416
|
+
final: secondFinal,
|
|
2417
|
+
task_preview: secondTaskPreview,
|
|
2418
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'approval_required', approval_request: approvalRequest }],
|
|
2419
|
+
};
|
|
2420
|
+
turnOptions.output.write(receipt.final);
|
|
2421
|
+
return { output: receipt.final, durationMs: 1, events: [{ type: 'receipt', receipt }] };
|
|
2422
|
+
},
|
|
2423
|
+
postApproval: async (request) => {
|
|
2424
|
+
approvals.push(request);
|
|
2425
|
+
return {
|
|
2426
|
+
ok: true,
|
|
2427
|
+
status: 200,
|
|
2428
|
+
data: {
|
|
2429
|
+
status: 'executed',
|
|
2430
|
+
action_type: 'google_calendar_create_event',
|
|
2431
|
+
execution: { status: 'executed', action_type: 'google_calendar_create_event', executed: true },
|
|
2432
|
+
},
|
|
2433
|
+
};
|
|
2434
|
+
},
|
|
2435
|
+
});
|
|
2436
|
+
const text = chunks.join('');
|
|
2437
|
+
assertSelfTest(turns.length === 2, 'approval yes triggered another model/backend turn');
|
|
2438
|
+
assertSelfTest(turns[1].route === 'cloud', 'slot-fill follow-up did not stay on cloud approval route');
|
|
2439
|
+
assertSelfTest(approvals.length === 1, 'approval execution was not called exactly once');
|
|
2440
|
+
assertSelfTest(approvals[0].executor_action_type === 'google_calendar_create_event', 'chat loop executed wrong approval action');
|
|
2441
|
+
const previewRows = formatTaskPreviewRows({
|
|
2442
|
+
task_preview: secondTaskPreview,
|
|
2443
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'approval_required', approval_request: approvalRequest }],
|
|
2444
|
+
}, { color: false }).join('\n');
|
|
2445
|
+
assertSelfTest(/owner\s+harness-engineer/.test(previewRows), 'task preview owner row missing');
|
|
2446
|
+
assertSelfTest(/plan\s+create calendar event "markoo" Jun 28, 2026 at 2:00 PM/.test(previewRows), 'task preview plan row missing');
|
|
2447
|
+
assertSelfTest(/check\s+approval required/.test(previewRows), 'task preview check row missing');
|
|
2448
|
+
assertSelfTest(/Accepted\./.test(text) && /Created calendar event\./.test(text), 'chat loop did not render accepted execution result');
|
|
2449
|
+
}, results, output);
|
|
2450
|
+
|
|
2451
|
+
await runSelfTestCase('calendar blocked approval retry', async () => {
|
|
2452
|
+
const chunks = [];
|
|
2453
|
+
const chatOutput = {
|
|
2454
|
+
isTTY: false,
|
|
2455
|
+
write(chunk) {
|
|
2456
|
+
chunks.push(String(chunk || ''));
|
|
2457
|
+
return true;
|
|
2458
|
+
},
|
|
2459
|
+
};
|
|
2460
|
+
const approvalRequest = {
|
|
2461
|
+
connector: 'google_calendar',
|
|
2462
|
+
action: 'create_event',
|
|
2463
|
+
executor_action_type: 'google_calendar_create_event',
|
|
2464
|
+
status: 'approval_required',
|
|
2465
|
+
payload: {
|
|
2466
|
+
summary: 'markoo',
|
|
2467
|
+
start: { dateTime: '2026-06-28T14:00:00-07:00' },
|
|
2468
|
+
end: { dateTime: '2026-06-28T15:00:00-07:00' },
|
|
2469
|
+
},
|
|
2470
|
+
};
|
|
2471
|
+
const turns = [];
|
|
2472
|
+
const approvals = [];
|
|
2473
|
+
await chat({
|
|
2474
|
+
mode: 'fast',
|
|
2475
|
+
route: 'cloud',
|
|
2476
|
+
cwd: os.tmpdir(),
|
|
2477
|
+
conversationId: 'ax-self-test-calendar-blocked-retry',
|
|
2478
|
+
input: Readable.from(['add calendar event today at 2pm\n', 'markoo\n', 'yes\n', 'yes\n', 'exit\n']),
|
|
2479
|
+
output: chatOutput,
|
|
2480
|
+
turnFunction: async (message, turnOptions) => {
|
|
2481
|
+
turns.push({ message, route: turnOptions.route, historyLength: turnOptions.history.length });
|
|
2482
|
+
const receipt = turns.length === 1
|
|
2483
|
+
? {
|
|
2484
|
+
final: "I'll add it today at 2:00 PM.\nWhat should I call it?",
|
|
2485
|
+
task_preview: {
|
|
2486
|
+
task: 'calendar.create_event',
|
|
2487
|
+
status: 'needs_input',
|
|
2488
|
+
missing: ['title'],
|
|
2489
|
+
slots: {
|
|
2490
|
+
date: '2026-06-28',
|
|
2491
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2492
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2493
|
+
},
|
|
2494
|
+
},
|
|
2495
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'blocked_missing_details' }],
|
|
2496
|
+
}
|
|
2497
|
+
: {
|
|
2498
|
+
final: 'Ready to create: markoo today at 2:00 PM.\nCreate it?',
|
|
2499
|
+
task_preview: {
|
|
2500
|
+
task: 'calendar.create_event',
|
|
2501
|
+
status: 'approval_required',
|
|
2502
|
+
missing: [],
|
|
2503
|
+
slots: {
|
|
2504
|
+
title: 'markoo',
|
|
2505
|
+
date: '2026-06-28',
|
|
2506
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2507
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2508
|
+
},
|
|
2509
|
+
},
|
|
2510
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'approval_required', approval_request: approvalRequest }],
|
|
2511
|
+
};
|
|
2512
|
+
turnOptions.output.write(receipt.final);
|
|
2513
|
+
return { output: receipt.final, durationMs: 1, events: [{ type: 'receipt', receipt }] };
|
|
2514
|
+
},
|
|
2515
|
+
postApproval: async (request) => {
|
|
2516
|
+
approvals.push(request);
|
|
2517
|
+
if (approvals.length === 1) {
|
|
2518
|
+
return {
|
|
2519
|
+
ok: false,
|
|
2520
|
+
status: 402,
|
|
2521
|
+
data: { detail: { error: 'paid_cloud_computer_required', action_type: 'google_calendar_create_event' } },
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
return {
|
|
2525
|
+
ok: true,
|
|
2526
|
+
status: 200,
|
|
2527
|
+
data: {
|
|
2528
|
+
status: 'executed',
|
|
2529
|
+
action_type: 'google_calendar_create_event',
|
|
2530
|
+
execution: { status: 'executed', action_type: 'google_calendar_create_event', executed: true },
|
|
2531
|
+
},
|
|
2532
|
+
};
|
|
2533
|
+
},
|
|
2534
|
+
});
|
|
2535
|
+
const text = chunks.join('');
|
|
2536
|
+
assertSelfTest(turns.length === 2, 'blocked approval retry triggered another model/backend turn');
|
|
2537
|
+
assertSelfTest(approvals.length === 2, 'blocked approval was not retryable');
|
|
2538
|
+
assertSelfTest(approvals[0].approval_request_id === approvals[1].approval_request_id, 'retry changed approval id');
|
|
2539
|
+
assertSelfTest(/Approval execution needs a paid cloud computer\./.test(text), 'blocked approval message missing');
|
|
2540
|
+
assertSelfTest(/Created calendar event\./.test(text), 'retry success message missing');
|
|
2541
|
+
}, results, output);
|
|
2542
|
+
|
|
2543
|
+
await runSelfTestCase('calendar chat denial loop', async () => {
|
|
2544
|
+
const chunks = [];
|
|
2545
|
+
const chatOutput = {
|
|
2546
|
+
isTTY: false,
|
|
2547
|
+
write(chunk) {
|
|
2548
|
+
chunks.push(String(chunk || ''));
|
|
2549
|
+
return true;
|
|
2550
|
+
},
|
|
2551
|
+
};
|
|
2552
|
+
const approvalRequest = {
|
|
2553
|
+
connector: 'google_calendar',
|
|
2554
|
+
action: 'create_event',
|
|
2555
|
+
executor_action_type: 'google_calendar_create_event',
|
|
2556
|
+
status: 'approval_required',
|
|
2557
|
+
payload: {
|
|
2558
|
+
summary: 'markoo',
|
|
2559
|
+
start: { dateTime: '2026-06-28T14:00:00-07:00' },
|
|
2560
|
+
end: { dateTime: '2026-06-28T15:00:00-07:00' },
|
|
2561
|
+
},
|
|
2562
|
+
};
|
|
2563
|
+
const turns = [];
|
|
2564
|
+
const approvals = [];
|
|
2565
|
+
await chat({
|
|
2566
|
+
mode: 'fast',
|
|
2567
|
+
route: 'cloud',
|
|
2568
|
+
cwd: os.tmpdir(),
|
|
2569
|
+
conversationId: 'ax-self-test-calendar-denial',
|
|
2570
|
+
input: Readable.from(['add calendar event today at 2pm\n', 'markoo\n', 'no\n', 'exit\n']),
|
|
2571
|
+
output: chatOutput,
|
|
2572
|
+
turnFunction: async (message, turnOptions) => {
|
|
2573
|
+
turns.push({ message, route: turnOptions.route, historyLength: turnOptions.history.length });
|
|
2574
|
+
const receipt = turns.length === 1
|
|
2575
|
+
? {
|
|
2576
|
+
final: "I'll add it today at 2:00 PM.\nWhat should I call it?",
|
|
2577
|
+
task_preview: {
|
|
2578
|
+
task: 'calendar.create_event',
|
|
2579
|
+
status: 'needs_input',
|
|
2580
|
+
missing: ['title'],
|
|
2581
|
+
slots: {
|
|
2582
|
+
date: '2026-06-28',
|
|
2583
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2584
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2585
|
+
},
|
|
2586
|
+
},
|
|
2587
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'blocked_missing_details' }],
|
|
2588
|
+
}
|
|
2589
|
+
: {
|
|
2590
|
+
final: 'Ready to create: markoo today at 2:00 PM.\nCreate it?',
|
|
2591
|
+
task_preview: {
|
|
2592
|
+
task: 'calendar.create_event',
|
|
2593
|
+
status: 'approval_required',
|
|
2594
|
+
missing: [],
|
|
2595
|
+
slots: {
|
|
2596
|
+
title: 'markoo',
|
|
2597
|
+
date: '2026-06-28',
|
|
2598
|
+
start: '2026-06-28T14:00:00-07:00',
|
|
2599
|
+
end: '2026-06-28T15:00:00-07:00',
|
|
2600
|
+
},
|
|
2601
|
+
},
|
|
2602
|
+
tool_events: [{ tool: 'google_calendar', action: 'create_event', status: 'approval_required', approval_request: approvalRequest }],
|
|
2603
|
+
};
|
|
2604
|
+
turnOptions.output.write(receipt.final);
|
|
2605
|
+
return { output: receipt.final, durationMs: 1, events: [{ type: 'receipt', receipt }] };
|
|
2606
|
+
},
|
|
2607
|
+
postApproval: async (request) => {
|
|
2608
|
+
approvals.push(request);
|
|
2609
|
+
return { ok: true, status: 200, data: { status: 'executed' } };
|
|
2610
|
+
},
|
|
2611
|
+
});
|
|
2612
|
+
const text = chunks.join('');
|
|
2613
|
+
assertSelfTest(turns.length === 2, 'denial triggered another model/backend turn');
|
|
2614
|
+
assertSelfTest(approvals.length === 0, 'denial executed an approval action');
|
|
2615
|
+
assertSelfTest(/Cancelled\./.test(text), 'denial did not render cancellation');
|
|
2616
|
+
}, results, output);
|
|
2617
|
+
|
|
2618
|
+
await runSelfTestCase('approval queue privacy', async () => {
|
|
2619
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ax-self-test-approvals-'));
|
|
2620
|
+
const storePath = path.join(dir, 'approvals.json');
|
|
2621
|
+
try {
|
|
2622
|
+
const receipt = {
|
|
2623
|
+
task_preview: {
|
|
2624
|
+
task: 'slack.post_message',
|
|
2625
|
+
owner_member: 'comms',
|
|
2626
|
+
status: 'approval_required',
|
|
2627
|
+
missing: [],
|
|
2628
|
+
},
|
|
2629
|
+
tool_events: [{
|
|
2630
|
+
tool: 'slack',
|
|
2631
|
+
approval_request: {
|
|
2632
|
+
connector: 'slack',
|
|
2633
|
+
action: 'post_message',
|
|
2634
|
+
executor_action_type: 'slack_post_message',
|
|
2635
|
+
status: 'approval_required',
|
|
2636
|
+
payload: { channel: '#general', text: 'secret payload text' },
|
|
2637
|
+
},
|
|
2638
|
+
}],
|
|
2639
|
+
};
|
|
2640
|
+
const request = approvalRequestFromReceipt(receipt);
|
|
2641
|
+
const record = persistPendingApproval(receipt, request, { storePath, cwd: dir, mode: 'fast' });
|
|
2642
|
+
const mode = fs.statSync(storePath).mode & 0o777;
|
|
2643
|
+
const listed = formatStoredApprovals(readApprovalStore({ storePath }), { color: false });
|
|
2644
|
+
assertSelfTest(record && record.id, 'approval queue record missing');
|
|
2645
|
+
assertSelfTest(mode === 0o600, `approval queue mode was ${mode.toString(8)}`);
|
|
2646
|
+
assertSelfTest(/owner\s+comms/.test(listed), 'approval queue owner missing');
|
|
2647
|
+
assertSelfTest(!/secret payload text/.test(listed), 'approval queue list leaked payload');
|
|
2648
|
+
denyStoredApproval(record.id, { storePath });
|
|
2649
|
+
assertSelfTest(readApprovalStore({ storePath }).approvals.length === 0, 'approval denial did not remove queue record');
|
|
2650
|
+
|
|
2651
|
+
const oldCreatedAt = new Date(Date.now() - (25 * 60 * 60 * 1000)).toISOString();
|
|
2652
|
+
const stale = persistPendingApproval(receipt, request, { storePath, cwd: dir, mode: 'fast', createdAt: oldCreatedAt });
|
|
2653
|
+
let executed = false;
|
|
2654
|
+
const staleOutput = {
|
|
2655
|
+
isTTY: false,
|
|
2656
|
+
chunks: [],
|
|
2657
|
+
write(chunk) {
|
|
2658
|
+
this.chunks.push(String(chunk || ''));
|
|
2659
|
+
return true;
|
|
2660
|
+
},
|
|
2661
|
+
};
|
|
2662
|
+
const result = await approveStoredApproval(stale.id, {
|
|
2663
|
+
storePath,
|
|
2664
|
+
output: staleOutput,
|
|
2665
|
+
postApproval: async () => {
|
|
2666
|
+
executed = true;
|
|
2667
|
+
return { ok: true, status: 200, data: {} };
|
|
2668
|
+
},
|
|
2669
|
+
});
|
|
2670
|
+
assertSelfTest(result && result.stale === true, 'stale approval was not marked stale');
|
|
2671
|
+
assertSelfTest(executed === false, 'stale approval executed');
|
|
2672
|
+
assertSelfTest(readApprovalStore({ storePath }).approvals.length === 0, 'stale approval was not removed');
|
|
2673
|
+
assertSelfTest(/Approval expired/.test(staleOutput.chunks.join('')), 'stale approval message missing');
|
|
2674
|
+
} finally {
|
|
2675
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
2676
|
+
}
|
|
2677
|
+
}, results, output);
|
|
2678
|
+
|
|
2679
|
+
await runSelfTestCase('approval output privacy', async () => {
|
|
2680
|
+
const text = formatApprovalExecutionResult({
|
|
2681
|
+
ok: false,
|
|
2682
|
+
status: 402,
|
|
2683
|
+
data: { detail: { error: 'paid_cloud_computer_required', message: 'secret payload text' } },
|
|
2684
|
+
});
|
|
2685
|
+
assertSelfTest(text === 'Approval execution needs a paid cloud computer.', 'unexpected approval failure text');
|
|
2686
|
+
assertSelfTest(!/secret payload/i.test(text), 'approval failure leaked private detail');
|
|
2687
|
+
}, results, output);
|
|
2688
|
+
|
|
2689
|
+
await runSelfTestCase('run log privacy', async () => {
|
|
2690
|
+
const previousFull = process.env.AX_LOG_FULL;
|
|
2691
|
+
const previousAuto = process.env.AX_AUTO_LOG;
|
|
2692
|
+
delete process.env.AX_LOG_FULL;
|
|
2693
|
+
delete process.env.AX_AUTO_LOG;
|
|
2694
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'ax-self-test-log-'));
|
|
2695
|
+
fs.mkdirSync(path.join(cwd, 'atris'), { recursive: true });
|
|
2696
|
+
try {
|
|
2697
|
+
const logger = createRunLogger({
|
|
2698
|
+
cwd,
|
|
2699
|
+
mode: 'fast',
|
|
2700
|
+
kind: 'self-test',
|
|
2701
|
+
output: { isTTY: true, write() { return true; } },
|
|
2702
|
+
});
|
|
2703
|
+
assertSelfTest(logger, 'logger was not created');
|
|
2704
|
+
logger.output.write('private input secret\n');
|
|
2705
|
+
logger.write('private output secret\n');
|
|
2706
|
+
logger.close(0);
|
|
2707
|
+
const logText = fs.readFileSync(logger.path, 'utf8');
|
|
2708
|
+
assertSelfTest(/transcript: redacted/.test(logText), 'redacted transcript marker missing');
|
|
2709
|
+
assertSelfTest(!/private input secret|private output secret/.test(logText), 'run log leaked private text');
|
|
2710
|
+
} finally {
|
|
2711
|
+
if (previousFull === undefined) delete process.env.AX_LOG_FULL;
|
|
2712
|
+
else process.env.AX_LOG_FULL = previousFull;
|
|
2713
|
+
if (previousAuto === undefined) delete process.env.AX_AUTO_LOG;
|
|
2714
|
+
else process.env.AX_AUTO_LOG = previousAuto;
|
|
2715
|
+
fs.rmSync(cwd, { recursive: true, force: true });
|
|
2716
|
+
}
|
|
2717
|
+
}, results, output);
|
|
2718
|
+
|
|
2719
|
+
const failed = results.filter(result => !result.ok);
|
|
2720
|
+
output.write('\n');
|
|
2721
|
+
output.write(failed.length ? `Self-test failed: ${failed.length}/${results.length} failed\n` : `Self-test passed: ${results.length}/${results.length}\n`);
|
|
2722
|
+
if (failed.length) {
|
|
2723
|
+
const error = new Error(failed.map(result => `${result.label}: ${result.error}`).join('; '));
|
|
2724
|
+
error.results = results;
|
|
2725
|
+
throw error;
|
|
2726
|
+
}
|
|
2727
|
+
return results;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2945
2730
|
async function runBenchmark(options = {}) {
|
|
2946
2731
|
const normalized = normalizeMode(options.mode);
|
|
2947
2732
|
const mode = normalized === 'code-fast' ? 'fast' : normalized;
|
|
@@ -3063,35 +2848,12 @@ async function runBenchmark(options = {}) {
|
|
|
3063
2848
|
}
|
|
3064
2849
|
|
|
3065
2850
|
async function main() {
|
|
3066
|
-
|
|
3067
|
-
if (isAxSpawnCommand(args[0])) {
|
|
3068
|
-
try {
|
|
3069
|
-
runAxSpawnCommand(args, { root: process.cwd() });
|
|
3070
|
-
} catch (error) {
|
|
3071
|
-
console.error(`x ${error.message}`);
|
|
3072
|
-
process.exit(1);
|
|
3073
|
-
}
|
|
3074
|
-
return;
|
|
3075
|
-
}
|
|
3076
|
-
if (args[0] === 'youtube') {
|
|
3077
|
-
try {
|
|
3078
|
-
await runAxYoutubeCommand(args, { output: (line = '') => console.log(line) });
|
|
3079
|
-
} catch (error) {
|
|
3080
|
-
console.error(`x ${error.message}`);
|
|
3081
|
-
process.exit(1);
|
|
3082
|
-
}
|
|
3083
|
-
return;
|
|
3084
|
-
}
|
|
2851
|
+
const args = process.argv.slice(2);
|
|
3085
2852
|
if (args.includes('--help') || args.includes('-h')) {
|
|
3086
2853
|
console.log(formatUsage());
|
|
3087
2854
|
return;
|
|
3088
2855
|
}
|
|
3089
2856
|
|
|
3090
|
-
const bypassPermissions = parsePermissionFlags(args);
|
|
3091
|
-
args = stripPermissionFlags(args);
|
|
3092
|
-
const chatArgs = normalizeChatCommandArgs(args);
|
|
3093
|
-
args = chatArgs.args;
|
|
3094
|
-
|
|
3095
2857
|
// --business <slug>: chat against that business's cloud workspace. The flag
|
|
3096
2858
|
// takes a value, so splice the pair out before building the prompt.
|
|
3097
2859
|
let businessSlug = null;
|
|
@@ -3118,63 +2880,42 @@ async function main() {
|
|
|
3118
2880
|
args.splice(verifyIdx, 2);
|
|
3119
2881
|
}
|
|
3120
2882
|
|
|
3121
|
-
|
|
3122
|
-
const loopsIdx = args.indexOf('--loops');
|
|
3123
|
-
if (loopsIdx !== -1) {
|
|
3124
|
-
dogfoodLoops = Number(args[loopsIdx + 1]);
|
|
3125
|
-
if (!Number.isInteger(dogfoodLoops) || dogfoodLoops < 1 || dogfoodLoops > 100) {
|
|
3126
|
-
console.error('Usage: ax --dogfood-chat --loops <1-100>');
|
|
3127
|
-
process.exit(1);
|
|
3128
|
-
}
|
|
3129
|
-
args.splice(loopsIdx, 2);
|
|
3130
|
-
}
|
|
3131
|
-
|
|
3132
|
-
const mode = args.includes('--code-fast') || args.includes('--code') ? 'code-fast'
|
|
3133
|
-
: args.includes('--max') ? 'max'
|
|
3134
|
-
: args.includes('--pro') ? 'pro'
|
|
3135
|
-
: 'fast';
|
|
2883
|
+
const mode = args.includes('--code-fast') || args.includes('--code') ? 'code-fast' : args.includes('--max') ? 'max' : args.includes('--fast') ? 'fast' : 'pro';
|
|
3136
2884
|
const doctor = args.includes('--doctor');
|
|
2885
|
+
const selfTest = args.includes('--self-test');
|
|
3137
2886
|
const benchmark = args.includes('--benchmark');
|
|
3138
|
-
const
|
|
3139
|
-
const dogfoodStatus = args.includes('--dogfood-status');
|
|
3140
|
-
const dogfoodJson = dogfoodChat && args.includes('--json');
|
|
3141
|
-
const dogfoodLive = dogfoodChat && args.includes('--live');
|
|
2887
|
+
const approvals = args.includes('--approvals');
|
|
3142
2888
|
const forceCloud = args.includes('--cloud');
|
|
3143
2889
|
const forceLocal = args.includes('--local');
|
|
3144
|
-
const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
|
|
3145
|
-
const prompt = args
|
|
3146
|
-
.filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--benchmark', '--dogfood-chat', '--dogfood-status', '--json', '--live', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
3147
|
-
.join(' ')
|
|
3148
|
-
.trim();
|
|
3149
|
-
|
|
3150
|
-
if (dogfoodStatus) {
|
|
3151
|
-
const status = buildChatDogfoodStatus({ cwd: process.cwd(), targetLoops: dogfoodLoops });
|
|
3152
|
-
console.log(args.includes('--json') ? JSON.stringify(status, null, 2) : formatChatDogfoodStatusReport(status));
|
|
3153
|
-
return;
|
|
3154
|
-
}
|
|
3155
2890
|
|
|
3156
|
-
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
console.error(`x ${error.message}`);
|
|
2891
|
+
let approveId = '';
|
|
2892
|
+
const approveIdx = args.indexOf('--approve');
|
|
2893
|
+
if (approveIdx !== -1) {
|
|
2894
|
+
approveId = String(args[approveIdx + 1] || '').trim();
|
|
2895
|
+
if (!approveId) {
|
|
2896
|
+
console.error('Usage: ax --approve <approval-id>');
|
|
3163
2897
|
process.exit(1);
|
|
3164
2898
|
}
|
|
3165
|
-
|
|
2899
|
+
args.splice(approveIdx, 2);
|
|
3166
2900
|
}
|
|
3167
2901
|
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
2902
|
+
let denyId = '';
|
|
2903
|
+
const denyIdx = args.indexOf('--deny');
|
|
2904
|
+
if (denyIdx !== -1) {
|
|
2905
|
+
denyId = String(args[denyIdx + 1] || '').trim();
|
|
2906
|
+
if (!denyId) {
|
|
2907
|
+
console.error('Usage: ax --deny <approval-id>');
|
|
3173
2908
|
process.exit(1);
|
|
3174
2909
|
}
|
|
3175
|
-
|
|
2910
|
+
args.splice(denyIdx, 2);
|
|
3176
2911
|
}
|
|
3177
2912
|
|
|
2913
|
+
const route = forceCloud ? 'cloud' : forceLocal ? 'local' : 'auto';
|
|
2914
|
+
const prompt = args
|
|
2915
|
+
.filter(arg => !['--max', '--fast', '--pro', '--code-fast', '--code', '--chat', '--doctor', '--approvals', '--self-test', '--benchmark', '--local', '--cloud', '--help', '-h'].includes(arg))
|
|
2916
|
+
.join(' ')
|
|
2917
|
+
.trim();
|
|
2918
|
+
|
|
3178
2919
|
let business = null;
|
|
3179
2920
|
if (businessSlug) {
|
|
3180
2921
|
if (normalizeMode(mode) === 'code-fast') {
|
|
@@ -3206,46 +2947,61 @@ async function main() {
|
|
|
3206
2947
|
console.log(`cloud workspace: ${biz.businessName || businessSlug}`);
|
|
3207
2948
|
}
|
|
3208
2949
|
|
|
3209
|
-
const runOptions = {
|
|
3210
|
-
mode,
|
|
3211
|
-
cwd: process.cwd(),
|
|
3212
|
-
route: route === 'auto' ? undefined : route,
|
|
3213
|
-
business,
|
|
3214
|
-
verify,
|
|
3215
|
-
bypassPermissions,
|
|
3216
|
-
};
|
|
3217
|
-
|
|
3218
2950
|
try {
|
|
2951
|
+
if (selfTest) {
|
|
2952
|
+
await runSelfTest({ output: process.stdout });
|
|
2953
|
+
return;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
3219
2956
|
if (benchmark) {
|
|
3220
2957
|
await runBenchmark({ mode, cwd: process.cwd(), output: process.stdout });
|
|
3221
2958
|
return;
|
|
3222
2959
|
}
|
|
3223
2960
|
|
|
3224
2961
|
if (doctor) {
|
|
3225
|
-
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false
|
|
2962
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
|
|
3226
2963
|
console.log('');
|
|
3227
|
-
console.log(formatRunProfile(buildRunProfile(
|
|
2964
|
+
console.log(formatRunProfile(buildRunProfile({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route }), process.stdout));
|
|
2965
|
+
console.log('');
|
|
2966
|
+
console.log(formatRuntimeHealth(await buildRuntimeHealth(), process.stdout));
|
|
3228
2967
|
return;
|
|
3229
2968
|
}
|
|
3230
2969
|
|
|
3231
|
-
if (
|
|
3232
|
-
|
|
2970
|
+
if (approvals) {
|
|
2971
|
+
console.log(formatStoredApprovals(readApprovalStore(), process.stdout));
|
|
3233
2972
|
return;
|
|
3234
2973
|
}
|
|
3235
2974
|
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
2975
|
+
if (approveId) {
|
|
2976
|
+
await approveStoredApproval(approveId, { mode, output: process.stdout });
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
if (denyId) {
|
|
2981
|
+
const record = denyStoredApproval(denyId);
|
|
2982
|
+
console.log(`Cancelled pending approval ${record.id}.`);
|
|
2983
|
+
return;
|
|
3243
2984
|
}
|
|
2985
|
+
|
|
2986
|
+
if (!prompt || args.includes('--chat')) {
|
|
2987
|
+
await chat({ mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
console.log(formatHeader({ mode, cwd: process.cwd(), chat: false }, process.stdout));
|
|
2992
|
+
console.log('');
|
|
2993
|
+
const result = await turnFunctionForMode(mode)(prompt, { mode, cwd: process.cwd(), route: route === 'auto' ? undefined : route, business, verify });
|
|
2994
|
+
const receipt = receiptFromState(result);
|
|
2995
|
+
const approvalRequest = normalizeMode(mode) === 'code-fast' ? null : approvalRequestFromReceipt(receipt);
|
|
2996
|
+
const approvalRecord = approvalRequest
|
|
2997
|
+
? persistPendingApproval(receipt, approvalRequest, { cwd: process.cwd(), mode })
|
|
2998
|
+
: null;
|
|
2999
|
+
if (approvalRecord) console.log(formatAuxRow('approve', formatApprovalCommand(approvalRecord.id), process.stdout));
|
|
3244
3000
|
console.log('');
|
|
3245
3001
|
console.log(formatDoneLine(result.durationMs, creditsFromState(result)));
|
|
3246
3002
|
} catch (error) {
|
|
3247
3003
|
console.error(`x ${error.message}`);
|
|
3248
|
-
printBackendHint(
|
|
3004
|
+
printBackendHint();
|
|
3249
3005
|
process.exit(1);
|
|
3250
3006
|
}
|
|
3251
3007
|
}
|
|
@@ -3257,87 +3013,67 @@ if (require.main === module) {
|
|
|
3257
3013
|
module.exports = {
|
|
3258
3014
|
authToken,
|
|
3259
3015
|
authUserId,
|
|
3016
|
+
approvalExecutionRequestFromReceipt,
|
|
3017
|
+
approvalRequestFromReceipt,
|
|
3018
|
+
approvalStorePath,
|
|
3019
|
+
approveStoredApproval,
|
|
3260
3020
|
backendBaseUrl,
|
|
3021
|
+
backendStartCommand,
|
|
3261
3022
|
backendUrl,
|
|
3262
|
-
buildAxYoutubeArgs,
|
|
3263
3023
|
buildCodeFastPayload,
|
|
3264
|
-
buildChatDogfoodCases,
|
|
3265
|
-
buildChatDogfoodStatus,
|
|
3266
|
-
buildMessage,
|
|
3267
3024
|
buildPayload,
|
|
3025
|
+
buildRuntimeHealth,
|
|
3268
3026
|
cachedIntegrationStatus,
|
|
3269
3027
|
buildConnectionContext,
|
|
3270
3028
|
buildRunProfile,
|
|
3271
3029
|
chat,
|
|
3272
3030
|
chatCompleter,
|
|
3273
|
-
chatGoalCommand: parseGoalCommand,
|
|
3274
3031
|
chatMenu,
|
|
3275
|
-
chatPermissionCommand,
|
|
3276
3032
|
chatTierCommand,
|
|
3277
|
-
loadAxPrefs,
|
|
3278
|
-
MEMBER_SLUG,
|
|
3279
|
-
parsePermissionFlags,
|
|
3280
|
-
permissionsLabel,
|
|
3281
|
-
printBackendHint,
|
|
3282
|
-
resolveBypassPermissions,
|
|
3283
|
-
runAxSpawnCommand,
|
|
3284
|
-
setBypassPermissions,
|
|
3285
3033
|
codeFastWorkspaceNotice,
|
|
3286
3034
|
creditsFromState,
|
|
3287
3035
|
codeFastBaseUrl,
|
|
3288
3036
|
codeFastUrl,
|
|
3289
3037
|
createRunLogger,
|
|
3290
3038
|
createProgressReporter,
|
|
3039
|
+
creditsFromApprovalExecution,
|
|
3040
|
+
denyStoredApproval,
|
|
3041
|
+
executeApprovalRequest,
|
|
3042
|
+
executeApprovalFromState,
|
|
3291
3043
|
formatDoneLine,
|
|
3292
|
-
formatChatDogfoodReport,
|
|
3293
|
-
formatChatDogfoodStatusReport,
|
|
3294
3044
|
formatDuration,
|
|
3045
|
+
formatApprovalExecutionResult,
|
|
3046
|
+
formatApprovalReceipt,
|
|
3047
|
+
formatBackendHint,
|
|
3295
3048
|
formatHeader,
|
|
3296
3049
|
formatPathSubject,
|
|
3297
|
-
buildInputBoxBottomPlain,
|
|
3298
|
-
buildInputBoxStatusPlain,
|
|
3299
|
-
buildInputBoxTopPlain,
|
|
3300
|
-
closeInputBox,
|
|
3301
|
-
repaintInputBoxBottom,
|
|
3302
|
-
formatChatInputInnerPrefix,
|
|
3303
|
-
formatChatInputPrompt,
|
|
3304
|
-
formatInputBoxBottom,
|
|
3305
|
-
formatInputBoxInputRow,
|
|
3306
|
-
formatInputBoxStatus,
|
|
3307
|
-
formatInputBoxTop,
|
|
3308
|
-
formatPermissionModeBrief,
|
|
3309
|
-
formatPermissionToggleMessage,
|
|
3310
3050
|
formatPrompt,
|
|
3311
|
-
insertMultilineBreak,
|
|
3312
|
-
inputBoxLayoutOptions,
|
|
3313
|
-
inputBoxPlainWidth,
|
|
3314
|
-
isMultilineInsertKey,
|
|
3315
|
-
isPermissionToggleKey,
|
|
3316
|
-
permissionAccentCodes,
|
|
3317
3051
|
formatRunProfile,
|
|
3052
|
+
formatRuntimeHealth,
|
|
3318
3053
|
formatStatusMessage,
|
|
3054
|
+
formatStoredApprovals,
|
|
3319
3055
|
formatSystemInit,
|
|
3056
|
+
formatTaskPreviewRows,
|
|
3320
3057
|
formatUsage,
|
|
3321
3058
|
formatWorkingLine,
|
|
3322
3059
|
handleEvent,
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3060
|
+
latestPendingApprovalRequest,
|
|
3061
|
+
messageApprovesPendingApproval,
|
|
3062
|
+
messageCancelsPendingApproval,
|
|
3326
3063
|
modelForMode,
|
|
3327
|
-
normalizeChatCommandArgs,
|
|
3328
3064
|
parseSseBlock,
|
|
3065
|
+
persistPendingApproval,
|
|
3066
|
+
postApprovalExecution,
|
|
3329
3067
|
postCodeFastTurn,
|
|
3330
3068
|
postTurn,
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
formatMarkdownLink,
|
|
3069
|
+
readApprovalStore,
|
|
3070
|
+
removeStoredApproval,
|
|
3334
3071
|
renderStreamingMarkdown,
|
|
3335
|
-
renderShimmerText,
|
|
3336
3072
|
renderTerminalMarkdown,
|
|
3337
3073
|
resolveRoute,
|
|
3338
3074
|
runBenchmark,
|
|
3339
|
-
|
|
3340
|
-
|
|
3075
|
+
runSelfTest,
|
|
3076
|
+
runtimeReadyForChat,
|
|
3341
3077
|
summarizeToolInput,
|
|
3342
3078
|
summarizeToolResult,
|
|
3343
3079
|
tierLabel
|