@aria_asi/cli 0.2.32 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
  2. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
  3. package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
  4. package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  6. package/dist/aria-connector/src/connectors/codex.js +51 -0
  7. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  8. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  9. package/dist/aria-connector/src/setup-wizard.js +91 -24
  10. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  11. package/dist/assets/hooks/aria-harness-via-sdk.mjs +10 -5
  12. package/dist/assets/hooks/aria-pre-tool-gate.mjs +19 -0
  13. package/dist/assets/hooks/aria-stop-gate.mjs +27 -2
  14. package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
  15. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +1 -0
  16. package/dist/assets/opencode-plugins/harness-gate/index.js +67 -3
  17. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -0
  18. package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
  19. package/dist/assets/opencode-plugins/harness-stop/index.js +61 -1
  20. package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  21. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -0
  22. package/dist/runtime/codex-bridge.mjs +71 -8
  23. package/dist/runtime/harness-daemon.mjs +50 -2
  24. package/dist/runtime/manifest.json +1 -1
  25. package/dist/runtime/sdk/BUNDLED.json +1 -1
  26. package/dist/runtime/sdk/index.d.ts +9 -0
  27. package/dist/runtime/sdk/index.js +23 -1
  28. package/dist/runtime/sdk/index.js.map +1 -1
  29. package/dist/runtime/service.mjs +339 -10
  30. package/dist/sdk/BUNDLED.json +1 -1
  31. package/dist/sdk/index.d.ts +9 -0
  32. package/dist/sdk/index.js +23 -1
  33. package/dist/sdk/index.js.map +1 -1
  34. package/hooks/aria-harness-via-sdk.mjs +10 -5
  35. package/hooks/aria-pre-tool-gate.mjs +19 -0
  36. package/hooks/aria-stop-gate.mjs +27 -2
  37. package/hooks/lib/domain-output-quality.mjs +103 -0
  38. package/hooks/lib/skill-autoload-gate.mjs +1 -0
  39. package/opencode-plugins/harness-gate/index.js +67 -3
  40. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +1 -0
  41. package/opencode-plugins/harness-outcome/index.js +39 -0
  42. package/opencode-plugins/harness-stop/index.js +61 -1
  43. package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
  44. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +1 -0
  45. package/package.json +1 -1
  46. package/runtime-src/codex-bridge.mjs +71 -8
  47. package/runtime-src/harness-daemon.mjs +50 -2
  48. package/runtime-src/service.mjs +339 -10
  49. package/src/connectors/codebase-awareness.ts +141 -77
  50. package/src/connectors/codex.ts +51 -0
  51. package/src/setup-wizard.ts +105 -25
@@ -14,7 +14,10 @@ const ENV_PATH = path.join(ARIA_DIR, 'codebase-awareness.env');
14
14
  const STATE_PATH = path.join(ARIA_DIR, 'codebase-awareness-state.json');
15
15
  const INDEX_PATH = path.join(ARIA_DIR, 'codebase-awareness-index.json');
16
16
 
17
- type AwarenessStatus = 'idle' | 'watching' | 'scanning' | 'error';
17
+ type AwarenessStatus = 'idle' | 'watching' | 'scanning' | 'degraded' | 'error';
18
+ const HEARTBEAT_INTERVAL_MS = Math.max(5_000, Number(process.env.ARIA_CODEBASE_AWARENESS_HEARTBEAT_MS || '15000'));
19
+ const REFRESH_INTERVAL_MS = Math.max(30_000, Number(process.env.ARIA_CODEBASE_AWARENESS_REFRESH_MS || '300000'));
20
+ const REPO_DISCOVERY_INTERVAL_MS = Math.max(30_000, Number(process.env.ARIA_CODEBASE_AWARENESS_RETRY_MS || '60000'));
18
21
 
19
22
  interface RepoSnapshot {
20
23
  path: string;
@@ -36,6 +39,13 @@ interface AwarenessState {
36
39
  repoSnapshots: RepoSnapshot[];
37
40
  lastError: string | null;
38
41
  updatedAt: string;
42
+ daemon?: {
43
+ pid: number;
44
+ startedAt: string;
45
+ heartbeatAt: string;
46
+ refreshIntervalMs: number;
47
+ watcherModes: Record<string, string>;
48
+ };
39
49
  }
40
50
 
41
51
  function connectorPackageRoot(): string {
@@ -82,12 +92,17 @@ function readState(): AwarenessState {
82
92
  repoSnapshots: Array.isArray(parsed.repoSnapshots) ? parsed.repoSnapshots as RepoSnapshot[] : [],
83
93
  lastError: parsed.lastError || null,
84
94
  updatedAt: parsed.updatedAt || new Date(0).toISOString(),
95
+ daemon: parsed.daemon,
85
96
  };
86
97
  } catch {
87
98
  return defaultState();
88
99
  }
89
100
  }
90
101
 
102
+ function compactError(error: unknown): string {
103
+ return error instanceof Error ? error.message : String(error);
104
+ }
105
+
91
106
  function writeState(update: Partial<AwarenessState>): AwarenessState {
92
107
  const current = readState();
93
108
  const next: AwarenessState = {
@@ -254,86 +269,135 @@ export async function startCodebaseAwarenessDaemon(options: {
254
269
  repos?: string[];
255
270
  fallbackRepoPath?: string;
256
271
  } = {}): Promise<void> {
257
- const repos = resolveRepos(undefined, options.repos, options.fallbackRepoPath);
258
- if (!repos.length) {
259
- writeState({ status: 'error', watchedRepos: [], lastError: 'No git repositories configured for codebase awareness.' });
260
- throw new Error('No git repositories configured for codebase awareness.');
261
- }
272
+ const startedAt = new Date().toISOString();
273
+ const handles = new Map<string, { stop: () => void; readonly mode: string }>();
274
+ const watcherModes: Record<string, string> = {};
275
+ let repos = resolveRepos(undefined, options.repos, options.fallbackRepoPath);
276
+
277
+ const writeDaemonState = (status: AwarenessStatus, update: Partial<AwarenessState> = {}): void => {
278
+ writeState({
279
+ status,
280
+ watchedRepos: repos,
281
+ ...update,
282
+ daemon: {
283
+ pid: process.pid,
284
+ startedAt,
285
+ heartbeatAt: new Date().toISOString(),
286
+ refreshIntervalMs: REFRESH_INTERVAL_MS,
287
+ watcherModes,
288
+ },
289
+ });
290
+ };
262
291
 
263
- await runCodebaseAwarenessScan({ repos });
264
- writeState({ status: 'watching', watchedRepos: repos, lastError: null });
292
+ const scanAll = async (reason: string): Promise<void> => {
293
+ repos = resolveRepos(undefined, options.repos, options.fallbackRepoPath);
294
+ if (!repos.length) {
295
+ writeDaemonState('degraded', { lastError: 'No git repositories configured for codebase awareness.' });
296
+ return;
297
+ }
265
298
 
266
- for (const repo of repos) {
267
- watchCodebase(
268
- repo,
269
- (image) => {
270
- const schemaText = schemaImageToText(image);
271
- const scanHash = hashSchema(schemaText);
272
- const lastScan = new Date().toISOString();
273
- const config = loadConfig();
274
- const repositories = [
275
- ...(config.repositories || []).filter((entry) => path.resolve(entry.path) !== repo),
276
- {
277
- path: repo,
278
- name: image.projectName,
279
- scanHash,
280
- lastScan,
299
+ writeDaemonState('scanning', { lastError: null });
300
+ const snapshots: RepoSnapshot[] = [];
301
+ const failures: string[] = [];
302
+ for (const repo of repos) {
303
+ try {
304
+ snapshots.push(await applyRepoScan(repo));
305
+ } catch (error) {
306
+ failures.push(`${repo}: ${compactError(error)}`);
307
+ }
308
+ }
309
+
310
+ const current = readState();
311
+ const repoSnapshots = [
312
+ ...snapshots,
313
+ ...current.repoSnapshots.filter((entry) => !snapshots.some((snapshot) => path.resolve(snapshot.path) === path.resolve(entry.path))),
314
+ ];
315
+ writeDaemonState(failures.length ? 'degraded' : 'watching', {
316
+ repoSnapshots,
317
+ lastError: failures.length ? `${reason}: ${failures.join(' | ')}` : null,
318
+ });
319
+ writeIndex({
320
+ generatedAt: new Date().toISOString(),
321
+ reason,
322
+ watchedRepos: repos,
323
+ repoSnapshots,
324
+ schemaImages: loadConfig().schemaImages || {},
325
+ });
326
+ };
327
+
328
+ const ensureWatchers = (): void => {
329
+ repos = resolveRepos(undefined, options.repos, options.fallbackRepoPath);
330
+ for (const [repo, handle] of handles.entries()) {
331
+ if (!repos.includes(repo)) {
332
+ handle.stop();
333
+ handles.delete(repo);
334
+ delete watcherModes[repo];
335
+ }
336
+ }
337
+ for (const repo of repos) {
338
+ if (handles.has(repo)) continue;
339
+ try {
340
+ const handle = watchCodebase(
341
+ repo,
342
+ (image) => {
343
+ const schemaText = schemaImageToText(image);
344
+ const scanHash = hashSchema(schemaText);
345
+ const lastScan = new Date().toISOString();
346
+ const config = loadConfig();
347
+ const repositories = [
348
+ ...(config.repositories || []).filter((entry) => path.resolve(entry.path) !== repo),
349
+ { path: repo, name: image.projectName, scanHash, lastScan },
350
+ ];
351
+ const nextConfig = {
352
+ ...config,
353
+ repositories,
354
+ schemaImages: { ...(config.schemaImages || {}), [image.projectName]: schemaText },
355
+ };
356
+ saveConfig(nextConfig);
357
+
358
+ const current = readState();
359
+ const snapshot: RepoSnapshot = {
360
+ path: repo,
361
+ name: image.projectName,
362
+ scanHash,
363
+ lastScan,
364
+ fileCount: image.fileCount,
365
+ language: image.language,
366
+ framework: image.framework || null,
367
+ packageManager: image.packageManager || null,
368
+ database: image.database || null,
369
+ orm: image.orm || null,
370
+ architecture: architectureSummary(image),
371
+ };
372
+ const repoSnapshots = [snapshot, ...current.repoSnapshots.filter((entry) => path.resolve(entry.path) !== repo)];
373
+ writeDaemonState('watching', { repoSnapshots, lastError: null });
374
+ writeIndex({ generatedAt: new Date().toISOString(), reason: 'watch-change', watchedRepos: repos, repoSnapshots, schemaImages: nextConfig.schemaImages || {} });
281
375
  },
282
- ];
283
- const nextConfig = {
284
- ...config,
285
- repositories,
286
- schemaImages: {
287
- ...(config.schemaImages || {}),
288
- [image.projectName]: schemaText,
376
+ {
377
+ debounceMs: 800,
378
+ pollIntervalMs: 4000,
379
+ onReady: (mode) => {
380
+ watcherModes[repo] = mode;
381
+ writeDaemonState('watching', { lastError: null });
382
+ },
383
+ onError: ({ phase, error }) => {
384
+ writeDaemonState('degraded', { lastError: `${repo} ${phase}: ${error.message}` });
385
+ },
289
386
  },
290
- };
291
- saveConfig(nextConfig);
292
-
293
- const current = readState();
294
- const snapshot: RepoSnapshot = {
295
- path: repo,
296
- name: image.projectName,
297
- scanHash,
298
- lastScan,
299
- fileCount: image.fileCount,
300
- language: image.language,
301
- framework: image.framework || null,
302
- packageManager: image.packageManager || null,
303
- database: image.database || null,
304
- orm: image.orm || null,
305
- architecture: architectureSummary(image),
306
- };
307
- const repoSnapshots = [
308
- snapshot,
309
- ...current.repoSnapshots.filter((entry) => path.resolve(entry.path) !== repo),
310
- ];
311
- writeState({
312
- status: 'watching',
313
- watchedRepos: repos,
314
- repoSnapshots,
315
- lastError: null,
316
- });
317
- writeIndex({
318
- generatedAt: new Date().toISOString(),
319
- watchedRepos: repos,
320
- repoSnapshots,
321
- schemaImages: nextConfig.schemaImages || {},
322
- });
323
- },
324
- {
325
- debounceMs: 800,
326
- pollIntervalMs: 4000,
327
- onError: ({ error }) => {
328
- writeState({
329
- status: 'error',
330
- watchedRepos: repos,
331
- lastError: error.message,
332
- });
333
- },
334
- },
335
- );
336
- }
387
+ );
388
+ handles.set(repo, handle);
389
+ watcherModes[repo] = handle.mode;
390
+ } catch (error) {
391
+ writeDaemonState('degraded', { lastError: `${repo} watcher init: ${compactError(error)}` });
392
+ }
393
+ }
394
+ };
395
+
396
+ await scanAll('daemon-start');
397
+ ensureWatchers();
398
+ setInterval(() => writeDaemonState(repos.length ? 'watching' : 'degraded'), HEARTBEAT_INTERVAL_MS).unref();
399
+ setInterval(() => { ensureWatchers(); void scanAll('periodic-refresh'); }, REFRESH_INTERVAL_MS).unref();
400
+ setInterval(() => { ensureWatchers(); }, REPO_DISCOVERY_INTERVAL_MS).unref();
337
401
 
338
402
  await new Promise(() => {});
339
403
  }
@@ -90,6 +90,7 @@ function tomlString(value: string): string {
90
90
 
91
91
  function buildCodexHookRuntimeClient(): string {
92
92
  return `import { readFileSync, existsSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
93
+ import { createHash, randomUUID } from 'node:crypto';
93
94
  import { homedir } from 'node:os';
94
95
  import path from 'node:path';
95
96
  import { HTTPHarnessClient } from '@aria_asi/harness-http-client';
@@ -154,6 +155,28 @@ export function inferSessionId(event) {
154
155
  return \`codex:\${threadId}:\${turnId}\`;
155
156
  }
156
157
 
158
+ export function ensureTraceId(state = {}) {
159
+ return typeof state.traceId === 'string' && state.traceId ? state.traceId : \`trace_\${randomUUID()}\`;
160
+ }
161
+
162
+ function stableEvidenceString(value) {
163
+ if (typeof value === 'string') return value;
164
+ try { return JSON.stringify(value); } catch { return String(value); }
165
+ }
166
+
167
+ export function makeEvidenceRef(kind, value, metadata = {}) {
168
+ const raw = stableEvidenceString(value);
169
+ const sha256 = createHash('sha256').update(raw).digest('hex');
170
+ return {
171
+ evidenceId: \`ev_\${sha256.slice(0, 16)}\`,
172
+ kind,
173
+ at: new Date().toISOString(),
174
+ sha256,
175
+ preview: raw.slice(0, 500),
176
+ metadata,
177
+ };
178
+ }
179
+
157
180
  export function inferUserId(event) {
158
181
  return (
159
182
  extractFirst(event, ['user_id', 'userId']) ||
@@ -309,9 +332,12 @@ import {
309
332
  getHarnessClient,
310
333
  inferSessionId,
311
334
  inferUserId,
335
+ ensureTraceId,
312
336
  extractUserText,
337
+ makeEvidenceRef,
313
338
  readEventFromStdin,
314
339
  runtimePost,
340
+ loadTurnState,
315
341
  saveTurnState,
316
342
  emitJson,
317
343
  } from './lib/runtime-client.mjs';
@@ -321,6 +347,8 @@ const client = getHarnessClient();
321
347
  const userText = extractUserText(event);
322
348
  const sessionId = inferSessionId(event);
323
349
  const userId = inferUserId(event);
350
+ const priorState = loadTurnState(sessionId);
351
+ const traceId = ensureTraceId(priorState);
324
352
 
325
353
  try {
326
354
  const packet = await client.getHarnessPacket({
@@ -328,6 +356,7 @@ try {
328
356
  platform: 'codex',
329
357
  message: userText || 'codex turn start',
330
358
  });
359
+ const packetRef = makeEvidenceRef('harness_packet', packet, { sessionId, platform: 'codex' });
331
360
  const result = await runtimePost('/mizan/pre', {
332
361
  sessionId,
333
362
  packet,
@@ -341,17 +370,21 @@ try {
341
370
  },
342
371
  context: {
343
372
  sessionId,
373
+ traceId,
344
374
  surface: 'codex-hooks',
345
375
  platform: 'codex',
346
376
  userText,
347
377
  userId,
378
+ evidenceRefs: [packetRef],
348
379
  },
349
380
  });
350
381
  saveTurnState(sessionId, {
382
+ traceId,
351
383
  userId,
352
384
  userText,
353
385
  preReceiptId: result?.receipt?.receiptId || null,
354
386
  packetTimestamp: packet?.timestamp || null,
387
+ packetRef,
355
388
  lastEvent: 'UserPromptSubmit',
356
389
  });
357
390
  process.exit(0);
@@ -373,6 +406,7 @@ import {
373
406
  summarizeTarget,
374
407
  readEventFromStdin,
375
408
  loadTurnState,
409
+ makeEvidenceRef,
376
410
  saveTurnState,
377
411
  emitJson,
378
412
  } from './lib/runtime-client.mjs';
@@ -405,6 +439,7 @@ try {
405
439
  action,
406
440
  toolName,
407
441
  target,
442
+ evidenceRef: makeEvidenceRef('tool_request', { action, toolName, target }, { sessionId }),
408
443
  });
409
444
  saveTurnState(sessionId, {
410
445
  tools,
@@ -426,6 +461,7 @@ import {
426
461
  inferSessionId,
427
462
  readEventFromStdin,
428
463
  loadTurnState,
464
+ makeEvidenceRef,
429
465
  saveTurnState,
430
466
  } from './lib/runtime-client.mjs';
431
467
 
@@ -435,11 +471,16 @@ const state = loadTurnState(sessionId);
435
471
 
436
472
  try {
437
473
  const toolResponse = JSON.stringify(event?.tool_response ?? event?.toolResponse ?? null).slice(0, 4000);
474
+ const evidenceRef = makeEvidenceRef('tool_response', event?.tool_response ?? event?.toolResponse ?? null, {
475
+ sessionId,
476
+ toolName: event?.tool_name || event?.toolName || null,
477
+ });
438
478
  const toolOutputs = Array.isArray(state?.toolOutputs) ? state.toolOutputs.slice(-24) : [];
439
479
  toolOutputs.push({
440
480
  at: new Date().toISOString(),
441
481
  toolName: event?.tool_name || event?.toolName || null,
442
482
  toolResponse,
483
+ evidenceRef,
443
484
  });
444
485
  saveTurnState(sessionId, {
445
486
  toolOutputs,
@@ -461,6 +502,7 @@ import {
461
502
  readEventFromStdin,
462
503
  runtimePost,
463
504
  loadTurnState,
505
+ makeEvidenceRef,
464
506
  clearTurnState,
465
507
  formatValidationFailure,
466
508
  emitJson,
@@ -471,6 +513,8 @@ const client = getHarnessClient();
471
513
  const sessionId = inferSessionId(event);
472
514
  const state = loadTurnState(sessionId);
473
515
  const text = extractAssistantText(event);
516
+ const outputRef = makeEvidenceRef('assistant_output', text, { sessionId, traceId: state?.traceId || null });
517
+ const toolRefs = Array.isArray(state?.toolOutputs) ? state.toolOutputs.map((entry) => entry.evidenceRef).filter(Boolean) : [];
474
518
 
475
519
  try {
476
520
  if (!text) {
@@ -505,12 +549,16 @@ try {
505
549
  layer3_pass: validation?.layer3?.pass !== false,
506
550
  surface: 'codex-hooks',
507
551
  tool_count: Array.isArray(state?.tools) ? state.tools.length : 0,
552
+ trace_id: state?.traceId || null,
553
+ output_ref: outputRef,
554
+ tool_refs: toolRefs,
508
555
  },
509
556
  context: {
510
557
  sessionId,
511
558
  surface: 'codex-hooks',
512
559
  platform: 'codex',
513
560
  userText: state?.userText || null,
561
+ traceId: state?.traceId || null,
514
562
  },
515
563
  parentReceiptId: state?.preReceiptId || null,
516
564
  });
@@ -527,6 +575,9 @@ try {
527
575
  metadata: {
528
576
  post_receipt_id: post?.receipt?.receiptId || null,
529
577
  pre_receipt_id: state?.preReceiptId || null,
578
+ trace_id: state?.traceId || null,
579
+ output_ref: outputRef,
580
+ tool_refs: toolRefs,
530
581
  tool_count: Array.isArray(state?.tools) ? state.tools.length : 0,
531
582
  validation_severity: validation?.validation?.severity || 'pass',
532
583
  layer3_pass: validation?.layer3?.pass !== false,
@@ -33,6 +33,22 @@ const HARNESS_URL =
33
33
  const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
34
34
  const ARIA_DIR = join(homedir(), '.aria');
35
35
  const LICENSE_PATH = join(ARIA_DIR, 'license.json');
36
+ const ONBOARDING_FALLBACK_URLS = [
37
+ process.env.ARIA_ONBOARDING_URL || '',
38
+ HARNESS_URL,
39
+ process.env.ARIA_SOUL_URL || '',
40
+ process.env.ARIAS_SOUL_URL || '',
41
+ process.env.ARIA_SOUL_BASE_URL || '',
42
+ 'https://api.ariasos.com',
43
+ 'https://arias-soul-6zp3gtk2ca-uc.a.run.app',
44
+ 'https://harness.ariasos.com',
45
+ ]
46
+ .flatMap((entry) => String(entry || '').split(','))
47
+ .map((entry) => entry.trim().replace(/\/+$/, ''))
48
+ .filter(Boolean)
49
+ .filter((entry, index, list) => list.indexOf(entry) === index);
50
+
51
+ let activeOnboardingBaseUrl = ONBOARDING_FALLBACK_URLS[0] || HARNESS_URL.replace(/\/+$/, '');
36
52
 
37
53
  type ConfigWrite =
38
54
  | { kind: 'persona_update'; updates: Record<string, unknown> }
@@ -57,7 +73,7 @@ interface DepthOption {
57
73
  interface ConverseResponse {
58
74
  ok: boolean;
59
75
  sessionId: string;
60
- topic: 'identity' | 'codebase' | 'persona' | 'done';
76
+ topic: 'identity' | 'codebase' | 'persona' | 'workforce' | 'harness_setup' | 'done';
61
77
  ariaPrompt: string;
62
78
  depthOptions: DepthOption[];
63
79
  freeTextAllowed: boolean;
@@ -69,17 +85,32 @@ interface ConverseResponse {
69
85
 
70
86
  export async function runSetupWizard(): Promise<void> {
71
87
  const rl = createInterface({ input: process.stdin, output: process.stdout });
88
+ let readlineClosed = false;
89
+ rl.on('close', () => { readlineClosed = true; });
72
90
  const ask = (q: string): Promise<string> =>
73
- new Promise((resolve) => rl.question(q, (a) => resolve(a)));
91
+ new Promise((resolve) => {
92
+ if (readlineClosed) {
93
+ resolve('__ARIA_ONBOARDING_EOF__');
94
+ return;
95
+ }
96
+ rl.question(q, (a) => resolve(a));
97
+ });
74
98
 
75
99
  const sessionId = `onboard_${Date.now()}_${randomBytes(4).toString('hex')}`;
76
100
  let userMessage: string | undefined;
77
101
  let action: 'start' | 'answer' = 'start';
102
+ let turnCount = 0;
78
103
 
79
104
  console.log('\n💬 Aria: starting onboarding…\n');
80
105
 
81
106
  try {
82
107
  while (true) {
108
+ turnCount += 1;
109
+ if (turnCount > 40) {
110
+ console.error('\n❌ Onboarding stopped after too many turns. Please run `aria` again to resume.');
111
+ return;
112
+ }
113
+
83
114
  const resp = await callConverse({ sessionId, action, userMessage });
84
115
  if (!resp.ok) {
85
116
  console.error(`\n❌ Onboarding error: ${resp.error || 'unknown'}`);
@@ -100,6 +131,10 @@ export async function runSetupWizard(): Promise<void> {
100
131
  }
101
132
 
102
133
  userMessage = (await ask('> ')).trim();
134
+ if (userMessage === '__ARIA_ONBOARDING_EOF__') {
135
+ console.error('\n❌ Onboarding input ended before setup completed. Run `aria` again to resume.');
136
+ return;
137
+ }
103
138
  action = 'answer';
104
139
  if (!userMessage) userMessage = resp.depthOptions[0]?.signal ?? 'next';
105
140
  }
@@ -117,18 +152,8 @@ async function callConverse(body: {
117
152
  action: 'start' | 'answer';
118
153
  userMessage?: string;
119
154
  }): Promise<ConverseResponse> {
120
- try {
121
- const res = await fetch(`${HARNESS_URL}/api/onboarding/converse`, {
122
- method: 'POST',
123
- headers: {
124
- 'Content-Type': 'application/json',
125
- ...(HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {}),
126
- },
127
- body: JSON.stringify(body),
128
- });
129
- return (await res.json()) as ConverseResponse;
130
- } catch (err) {
131
- return {
155
+ return requestOnboardingJson<ConverseResponse>('/api/onboarding/converse', body, {
156
+ fallback: {
132
157
  ok: false,
133
158
  sessionId: body.sessionId,
134
159
  topic: 'identity',
@@ -137,9 +162,69 @@ async function callConverse(body: {
137
162
  freeTextAllowed: false,
138
163
  isComplete: false,
139
164
  progressPct: 0,
140
- error: err instanceof Error ? err.message : String(err),
141
- };
165
+ },
166
+ });
167
+ }
168
+
169
+ async function requestOnboardingJson<T extends { ok?: boolean; error?: string }>(
170
+ pathname: string,
171
+ body: Record<string, unknown>,
172
+ options: { fallback: T },
173
+ ): Promise<T> {
174
+ const failures: string[] = [];
175
+ const baseUrls = [
176
+ activeOnboardingBaseUrl,
177
+ ...ONBOARDING_FALLBACK_URLS,
178
+ ].filter((entry, index, list) => entry && list.indexOf(entry) === index);
179
+
180
+ for (const baseUrl of baseUrls) {
181
+ try {
182
+ const res = await fetch(`${baseUrl}${pathname}`, {
183
+ method: 'POST',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ Accept: 'application/json',
187
+ ...(HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {}),
188
+ },
189
+ body: JSON.stringify(body),
190
+ });
191
+ const text = await res.text();
192
+ const contentType = res.headers.get('content-type') || '';
193
+ const looksJson = /^\s*[\[{]/.test(text);
194
+
195
+ if (!contentType.includes('application/json') && !looksJson) {
196
+ failures.push(`${baseUrl}${pathname} returned HTTP ${res.status} ${contentType || 'without content-type'}`);
197
+ continue;
198
+ }
199
+
200
+ let data: T;
201
+ try {
202
+ data = JSON.parse(text || '{}') as T;
203
+ } catch (err) {
204
+ failures.push(`${baseUrl}${pathname} returned invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
205
+ continue;
206
+ }
207
+
208
+ if (!res.ok) {
209
+ failures.push(`${baseUrl}${pathname} returned HTTP ${res.status}: ${data.error || 'no error message'}`);
210
+ continue;
211
+ }
212
+
213
+ activeOnboardingBaseUrl = baseUrl;
214
+ return data;
215
+ } catch (err) {
216
+ failures.push(`${baseUrl}${pathname} transport failed: ${err instanceof Error ? err.message : String(err)}`);
217
+ }
142
218
  }
219
+
220
+ return {
221
+ ...options.fallback,
222
+ ok: false,
223
+ error:
224
+ 'Onboarding API is unavailable or not returning JSON. ' +
225
+ failures.slice(0, 4).join(' | ') +
226
+ (failures.length > 4 ? ` | ${failures.length - 4} more endpoint(s) failed` : ''),
227
+ };
143
228
  }
144
229
 
145
230
  async function maybeOfferGitHubConnect(ask: (q: string) => Promise<string>): Promise<void> {
@@ -207,19 +292,14 @@ async function applyConfigWrites(writes: ConfigWrite[]): Promise<void> {
207
292
  const provider = claims.provider || '';
208
293
  const apiKey = claims.apiKey || '';
209
294
  try {
210
- const resp = await fetch(`${HARNESS_URL}/api/onboarding/self-issue`, {
211
- method: 'POST',
212
- headers: { 'Content-Type': 'application/json' },
213
- body: JSON.stringify({ email, provider, llm_key: apiKey }),
214
- });
215
- const data = await resp.json() as {
295
+ const data = await requestOnboardingJson<{
216
296
  ok?: boolean;
217
297
  license?: { token: string; jti: string; tier: string; expires_at: string };
218
298
  claims?: Record<string, unknown>;
219
299
  error?: string;
220
- };
221
- if (!resp.ok || !data.ok || !data.license) {
222
- console.warn(` ⚠ I couldn't issue your license: ${data.error || `HTTP ${resp.status}`}`);
300
+ }>('/api/onboarding/self-issue', { email, provider, llm_key: apiKey }, { fallback: { ok: false } });
301
+ if (!data.ok || !data.license) {
302
+ console.warn(` ⚠ I couldn't issue your license: ${data.error || 'license endpoint did not return a license'}`);
223
303
  continue;
224
304
  }
225
305
  if (!existsSync(ARIA_DIR)) mkdirSync(ARIA_DIR, { recursive: true, mode: 0o700 });