@aria_asi/cli 0.2.30 → 0.2.32

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 (95) hide show
  1. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/claude-code.js +115 -20
  3. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  4. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.js +551 -11
  6. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
  8. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
  9. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
  10. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
  11. package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
  12. package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
  13. package/dist/aria-connector/src/connectors/must-read.js +115 -0
  14. package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
  15. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  16. package/dist/aria-connector/src/connectors/opencode.js +27 -9
  17. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  18. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  19. package/dist/aria-connector/src/connectors/runtime.js +231 -19
  20. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  21. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  22. package/dist/aria-connector/src/connectors/shell.js +76 -3
  23. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  24. package/dist/assets/hooks/aria-agent-handoff.mjs +23 -0
  25. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +121 -28
  26. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  27. package/dist/assets/hooks/aria-pre-emit-dryrun.mjs +35 -0
  28. package/dist/assets/hooks/aria-pre-tool-gate.mjs +383 -93
  29. package/dist/assets/hooks/aria-preprompt-consult.mjs +28 -2
  30. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +93 -16
  31. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +33 -1
  32. package/dist/assets/hooks/aria-stop-gate.mjs +346 -81
  33. package/dist/assets/hooks/doctrine_trigger_map.json +55 -0
  34. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  35. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  36. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  37. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  38. package/dist/assets/opencode-plugins/harness-gate/index.js +40 -5
  39. package/dist/assets/opencode-plugins/harness-stop/index.js +133 -10
  40. package/dist/runtime/auth-middleware.mjs +251 -0
  41. package/dist/runtime/codex-bridge.mjs +644 -0
  42. package/dist/runtime/discipline/CLAUDE.md +28 -0
  43. package/dist/runtime/discipline/doctrine_trigger_map.json +534 -0
  44. package/dist/runtime/doctrine_trigger_map.json +534 -0
  45. package/dist/runtime/fleet-engine.mjs +231 -0
  46. package/dist/runtime/harness-daemon.mjs +460 -0
  47. package/dist/runtime/manifest.json +1 -1
  48. package/dist/runtime/metering.mjs +100 -0
  49. package/dist/runtime/onboarding-engine.mjs +89 -0
  50. package/dist/runtime/plugin-engine.mjs +196 -0
  51. package/dist/runtime/sdk/BUNDLED.json +1 -1
  52. package/dist/runtime/sdk/index.d.ts +12 -0
  53. package/dist/runtime/sdk/index.js +120 -14
  54. package/dist/runtime/sdk/index.js.map +1 -1
  55. package/dist/runtime/service.mjs +1140 -48
  56. package/dist/runtime/workflow-engine.mjs +322 -0
  57. package/dist/sdk/BUNDLED.json +1 -1
  58. package/dist/sdk/index.d.ts +12 -0
  59. package/dist/sdk/index.js +120 -14
  60. package/dist/sdk/index.js.map +1 -1
  61. package/hooks/aria-agent-handoff.mjs +23 -0
  62. package/hooks/aria-cognition-substrate-binding.mjs +121 -28
  63. package/hooks/aria-harness-via-sdk.mjs +126 -12
  64. package/hooks/aria-pre-emit-dryrun.mjs +35 -0
  65. package/hooks/aria-pre-tool-gate.mjs +383 -93
  66. package/hooks/aria-preprompt-consult.mjs +28 -2
  67. package/hooks/aria-preturn-memory-gate.mjs +93 -16
  68. package/hooks/aria-repo-doctrine-gate.mjs +33 -1
  69. package/hooks/aria-stop-gate.mjs +346 -81
  70. package/hooks/doctrine_trigger_map.json +55 -0
  71. package/hooks/lib/canonical-lenses.mjs +6 -5
  72. package/hooks/lib/gate-loop-state.mjs +50 -0
  73. package/hooks/lib/hook-message-window.mjs +121 -0
  74. package/hooks/test-tier-lens-labeling.mjs +26 -58
  75. package/opencode-plugins/harness-gate/index.js +40 -5
  76. package/opencode-plugins/harness-stop/index.js +133 -10
  77. package/package.json +1 -1
  78. package/runtime-src/auth-middleware.mjs +251 -0
  79. package/runtime-src/codex-bridge.mjs +644 -0
  80. package/runtime-src/fleet-engine.mjs +231 -0
  81. package/runtime-src/harness-daemon.mjs +460 -0
  82. package/runtime-src/metering.mjs +100 -0
  83. package/runtime-src/onboarding-engine.mjs +89 -0
  84. package/runtime-src/plugin-engine.mjs +196 -0
  85. package/runtime-src/service.mjs +1140 -48
  86. package/runtime-src/workflow-engine.mjs +322 -0
  87. package/scripts/bundle-sdk.mjs +5 -0
  88. package/src/connectors/claude-code.ts +126 -20
  89. package/src/connectors/codex.ts +559 -10
  90. package/src/connectors/doctrine-trigger-map.ts +112 -0
  91. package/src/connectors/must-read.ts +117 -0
  92. package/src/connectors/opencode.ts +28 -9
  93. package/src/connectors/runtime.ts +241 -21
  94. package/src/connectors/shell.ts +78 -3
  95. package/dist/cli-0.2.0.tgz +0 -0
@@ -3,7 +3,7 @@
3
3
  import { createServer } from 'node:http';
4
4
  import { createRequire } from 'node:module';
5
5
  import { createHash, createCipheriv, createDecipheriv, randomBytes, randomUUID, scryptSync } from 'node:crypto';
6
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { dirname, join } from 'node:path';
9
9
 
@@ -14,6 +14,11 @@ import {
14
14
  extractAnthropicUserMessage,
15
15
  extractOpenAIUserMessage,
16
16
  } from './provider-proxy.mjs';
17
+ import { recordTokenUsage, brandModel, getUsageSummary, getBillingSummary, getSubscriptionTier } from './metering.mjs';
18
+ import { deployFleet, loadFleet, saveFleet, getFleetStatus, buildAgentSystemPrompt } from './fleet-engine.mjs';
19
+ import { listPlugins, installPlugin, configurePlugin, dispatchHook } from './plugin-engine.mjs';
20
+ import { listWorkflowTemplates, configureWorkflow, getWorkflowStatus, startWorkflow, approveWorkflowStep } from './workflow-engine.mjs';
21
+ import { hqAuthMiddleware, generateApiKey, loginUser, registerUser, revokeSession, listAllTenants } from './auth-middleware.mjs';
17
22
  import {
18
23
  ARISTOTLE_28_MODULES,
19
24
  NOOR_COGNITIVE_SUITE,
@@ -37,8 +42,13 @@ const { runFullChain } = require('./vendor/aria-gate-runtime/index.js');
37
42
  const DEFAULT_HOST = process.env.ARIA_RUNTIME_HOST || '127.0.0.1';
38
43
  const DEFAULT_PORT = Number(process.env.ARIA_RUNTIME_PORT || 4319);
39
44
  const DEFAULT_HARNESS_URL =
45
+ process.env.ARIA_HARNESS_DAEMON_URL ||
46
+ process.env.ARIA_HIVE_RUNTIME_URL ||
40
47
  process.env.ARIA_HARNESS_BASE_URL ||
41
48
  process.env.ARIA_HARNESS_URL ||
49
+ process.env.ARIA_SOUL_URL ||
50
+ process.env.ARIAS_SOUL_URL ||
51
+ process.env.ARIA_SOUL_BASE_URL ||
42
52
  'https://harness.ariasos.com';
43
53
  const DEFAULT_RUNTIME_URL = process.env.ARIA_RUNTIME_URL || `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
44
54
  const DEFAULT_QDRANT_URL = process.env.ARIA_QDRANT_URL || 'http://127.0.0.1:6333';
@@ -48,16 +58,23 @@ const DEFAULT_FORGE_SERVICE_URL =
48
58
  process.env.FORGE_SERVICE_URL ||
49
59
  `${DEFAULT_HARNESS_URL.replace(/\/$/, '')}/api/forge/psi`;
50
60
  const DEFAULT_HEARTBEAT_GRACE_SECONDS = Number(process.env.ARIA_RUNTIME_HEARTBEAT_GRACE_SECONDS || 900);
61
+ const DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS = Number(process.env.ARIA_RUNTIME_OFFLINE_BUNDLE_SOFT_TTL_SECONDS || 86400);
62
+ const DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS = Number(process.env.ARIA_RUNTIME_OFFLINE_BUNDLE_HARD_TTL_SECONDS || 259200);
51
63
  const DEFAULT_JOB_CLAIM_SECONDS = Number(process.env.ARIA_RUNTIME_JOB_CLAIM_SECONDS || 120);
52
64
  const __dirname = dirname(fileURLToPath(import.meta.url));
53
65
  const STATE_DIR = join(__dirname, 'state');
54
66
  const LEASE_PATH = join(STATE_DIR, 'lease.enc');
67
+ const OFFLINE_BUNDLE_PATH = join(STATE_DIR, 'offline-policy-bundle.enc');
55
68
  const RUNTIME_META_PATH = join(STATE_DIR, 'runtime-meta.json');
56
69
  const AUTONOMY_STATE_PATH = join(STATE_DIR, 'autonomy.json');
57
70
  const COGNITION_STATE_PATH = join(STATE_DIR, 'cognition-state.enc');
58
71
  const REVOCATION_LOCK_PATH = join(STATE_DIR, 'revoked.json');
59
72
  const CONFIG_PATH = join(process.env.HOME || '', '.aria', 'config.json');
60
73
  const CODEBASE_AWARENESS_STATE_PATH = join(process.env.HOME || '', '.aria', 'codebase-awareness-state.json');
74
+ const LEGACY_PACKET_CACHE_CANDIDATES = [
75
+ join(process.env.HOME || '', '.aria', '.aria-harness-last-packet.json'),
76
+ join(process.env.HOME || '', '.claude', '.aria-harness-last-packet.json'),
77
+ ];
61
78
  const leaseCache = new Map();
62
79
  const TELEMETRY_LIMIT = 250;
63
80
  const DECISION_LIMIT = 250;
@@ -88,10 +105,14 @@ const READABLE_LENS_SLOTS = [
88
105
  const DOCTRINE_TRIGGER_MAP_CANDIDATES = [
89
106
  join(__dirname, 'discipline', 'doctrine_trigger_map.json'),
90
107
  join(__dirname, 'doctrine_trigger_map.json'),
108
+ join(process.env.HOME || '', '.aria', 'runtime', 'discipline', 'doctrine_trigger_map.json'),
109
+ join(process.env.HOME || '', '.aria', 'runtime', 'doctrine_trigger_map.json'),
91
110
  join(process.env.HOME || '', '.claude', 'hooks', 'doctrine_trigger_map.json'),
92
111
  join(process.env.HOME || '', '.claude', 'projects', '-home-hamzaibrahim1', 'memory', 'doctrine_trigger_map.json'),
93
112
  join(process.env.HOME || '', '.codex', 'doctrine_trigger_map.json'),
113
+ join(process.env.HOME || '', '.opencode', 'doctrine_trigger_map.json'),
94
114
  ];
115
+ const DOCTRINE_TRIGGER_SYNC_INTERVAL_MS = Number(process.env.ARIA_DOCTRINE_TRIGGER_SYNC_INTERVAL_MS || 5000);
95
116
  const TOOL_DEPLOY_PATTERNS = [
96
117
  /\b(?:\.\/)?scripts\/deploy-/i,
97
118
  /\bkubectl\s+apply\b/i,
@@ -115,12 +136,75 @@ const TOOL_DESTRUCTIVE_PATTERNS = [
115
136
  /\b(?:DROP|TRUNCATE)\s+(?:TABLE|DATABASE|SCHEMA|INDEX)\b/i,
116
137
  /\bkubectl\s+delete\b/i,
117
138
  ];
139
+ let doctrineTriggerMapCache = { sourcePath: null, map: { triggers: [] } };
140
+ let doctrineTriggerMapSyncedAt = 0;
118
141
 
119
142
  function json(res, status, payload) {
120
143
  res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
121
144
  res.end(JSON.stringify(payload, null, 2));
122
145
  }
123
146
 
147
+ function buildWebSocketAccept(key) {
148
+ return createHash('sha1')
149
+ .update(`${String(key || '').trim()}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
150
+ .digest('base64');
151
+ }
152
+
153
+ function sendWebSocketCloseFrame(socket, code = 1008, reason = '') {
154
+ const reasonBuffer = Buffer.from(String(reason || ''), 'utf8');
155
+ const payload = Buffer.allocUnsafe(2 + reasonBuffer.length);
156
+ payload.writeUInt16BE(code, 0);
157
+ reasonBuffer.copy(payload, 2);
158
+
159
+ const header =
160
+ payload.length < 126
161
+ ? Buffer.from([0x88, payload.length])
162
+ : Buffer.from([0x88, 126, (payload.length >> 8) & 0xff, payload.length & 0xff]);
163
+
164
+ socket.write(Buffer.concat([header, payload]));
165
+ }
166
+
167
+ function handleWebSocketUpgrade(req, socket) {
168
+ const url = new URL(req.url || '/', DEFAULT_RUNTIME_URL);
169
+ if (url.pathname !== '/v1/responses' && url.pathname !== '/responses') {
170
+ socket.write('HTTP/1.1 404 Not Found\r\nConnection: close\r\n\r\n');
171
+ socket.destroy();
172
+ return;
173
+ }
174
+
175
+ const upgrade = String(req.headers.upgrade || '').toLowerCase();
176
+ const connection = String(req.headers.connection || '').toLowerCase();
177
+ const websocketKey = req.headers['sec-websocket-key'];
178
+ if (upgrade !== 'websocket' || !connection.includes('upgrade') || !websocketKey) {
179
+ socket.write('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
180
+ socket.destroy();
181
+ return;
182
+ }
183
+
184
+ const accept = buildWebSocketAccept(websocketKey);
185
+ socket.write(
186
+ [
187
+ 'HTTP/1.1 101 Switching Protocols',
188
+ 'Upgrade: websocket',
189
+ 'Connection: Upgrade',
190
+ `Sec-WebSocket-Accept: ${accept}`,
191
+ '\r\n',
192
+ ].join('\r\n'),
193
+ );
194
+
195
+ // Codex can fall back to the HTTPS Responses path after a real websocket
196
+ // handshake closes. Until Aria owns the full Responses websocket protocol,
197
+ // keep this seam explicit instead of returning a misleading 404.
198
+ setTimeout(() => {
199
+ try {
200
+ sendWebSocketCloseFrame(socket, 1008, 'aria-runtime-http-fallback');
201
+ } catch {}
202
+ try {
203
+ socket.end();
204
+ } catch {}
205
+ }, 25).unref?.();
206
+ }
207
+
124
208
  function isNonTrivialAssistantTurn(text, toolIntents = []) {
125
209
  const body = String(text || '').trim();
126
210
  if (toolIntents.length > 0) return true;
@@ -198,6 +282,17 @@ function inferToolAction(toolName, args) {
198
282
  return 'tool';
199
283
  }
200
284
 
285
+ function normalizeHarnessPacketPayload(payload) {
286
+ let current = payload;
287
+ for (let depth = 0; depth < 3; depth++) {
288
+ if (!current || typeof current !== 'object' || Array.isArray(current)) break;
289
+ if (!('packet' in current) || !current.packet || typeof current.packet !== 'object') break;
290
+ if (!('timestamp' in current) && !('version' in current)) break;
291
+ current = current.packet;
292
+ }
293
+ return current;
294
+ }
295
+
201
296
  function summarizeToolTarget(toolName, args) {
202
297
  if (typeof args?.command === 'string' && args.command.trim()) return args.command.trim();
203
298
  if (typeof args?.file_path === 'string' && args.file_path.trim()) return args.file_path.trim();
@@ -244,11 +339,49 @@ function extractProviderToolIntents(providerStyle, providerMeta) {
244
339
  }
245
340
 
246
341
  function loadDoctrineTriggerMap() {
342
+ const now = Date.now();
343
+ if (now - doctrineTriggerMapSyncedAt < DOCTRINE_TRIGGER_SYNC_INTERVAL_MS) {
344
+ return doctrineTriggerMapCache.map;
345
+ }
346
+
347
+ const validCandidates = [];
247
348
  for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
248
349
  const map = readJsonFile(candidate, null);
249
- if (map && Array.isArray(map.triggers)) return map;
350
+ if (!map || !Array.isArray(map.triggers)) continue;
351
+ try {
352
+ validCandidates.push({
353
+ path: candidate,
354
+ mtimeMs: statSync(candidate).mtimeMs,
355
+ map,
356
+ });
357
+ } catch {}
358
+ }
359
+
360
+ if (validCandidates.length === 0) {
361
+ doctrineTriggerMapCache = { sourcePath: null, map: { triggers: [] } };
362
+ doctrineTriggerMapSyncedAt = now;
363
+ return doctrineTriggerMapCache.map;
364
+ }
365
+
366
+ validCandidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
367
+ const latest = validCandidates[0];
368
+ const normalized = JSON.stringify(latest.map, null, 2) + '\n';
369
+ for (const candidate of DOCTRINE_TRIGGER_MAP_CANDIDATES) {
370
+ try {
371
+ const dirPath = dirname(candidate);
372
+ if (!existsSync(dirPath)) {
373
+ mkdirSync(dirPath, { recursive: true, mode: 0o755 });
374
+ }
375
+ const current = existsSync(candidate) ? readFileSync(candidate, 'utf8') : null;
376
+ if (current !== normalized) {
377
+ writeFileSync(candidate, normalized, { mode: 0o644 });
378
+ }
379
+ } catch {}
250
380
  }
251
- return { triggers: [] };
381
+
382
+ doctrineTriggerMapCache = { sourcePath: latest.path, map: latest.map };
383
+ doctrineTriggerMapSyncedAt = now;
384
+ return latest.map;
252
385
  }
253
386
 
254
387
  function collectDoctrineTriggerHits(text) {
@@ -341,6 +474,26 @@ function synthesizeOwnerLease(apiKey, reason) {
341
474
  };
342
475
  }
343
476
 
477
+ function buildOfflineBundleFallbackPacket(bundle, error) {
478
+ const normalized = normalizeHarnessPacketPayload(bundle?.packet);
479
+ const packet = normalized && typeof normalized === 'object'
480
+ ? JSON.parse(JSON.stringify(normalized))
481
+ : null;
482
+ if (!packet) return null;
483
+ packet.runtimeOfflineBundle = {
484
+ phase: error?.softExpired ? 'degraded' : 'fresh',
485
+ cachedAt: bundle.cachedAt || null,
486
+ lastUpstreamOkAt: bundle.lastUpstreamOkAt || null,
487
+ softExpiresAt: error?.softExpiresAt || null,
488
+ hardExpiresAt: error?.hardExpiresAt || null,
489
+ ageSeconds: error?.ageSeconds ?? null,
490
+ doctrineBundleHash: bundle.doctrineBundleHash || null,
491
+ source: bundle.source || null,
492
+ lastUpstreamError: bundle.lastUpstreamError || null,
493
+ };
494
+ return packet;
495
+ }
496
+
344
497
  function deriveEncryptionKey(secret) {
345
498
  return scryptSync(secret, 'aria-mounted-runtime', 32);
346
499
  }
@@ -390,10 +543,130 @@ function saveEncryptedLease(lease, secret) {
390
543
  writeFileSync(LEASE_PATH, encryptJson(lease, secret), { mode: 0o600 });
391
544
  }
392
545
 
546
+ function loadEncryptedOfflineBundle(secret) {
547
+ try {
548
+ if (!existsSync(OFFLINE_BUNDLE_PATH)) return null;
549
+ return decryptJson(readFileSync(OFFLINE_BUNDLE_PATH, 'utf8'), secret);
550
+ } catch {
551
+ return null;
552
+ }
553
+ }
554
+
555
+ function saveEncryptedOfflineBundle(bundle, secret) {
556
+ mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
557
+ writeFileSync(OFFLINE_BUNDLE_PATH, encryptJson(bundle, secret), { mode: 0o600 });
558
+ }
559
+
560
+ function computeOfflineBundleStatus(bundle, now = Date.now()) {
561
+ if (!bundle || typeof bundle !== 'object') {
562
+ return {
563
+ present: false,
564
+ phase: 'missing',
565
+ usable: false,
566
+ softExpired: true,
567
+ hardExpired: true,
568
+ ageSeconds: null,
569
+ softTtlSeconds: DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS,
570
+ hardTtlSeconds: DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS,
571
+ softExpiresAt: null,
572
+ hardExpiresAt: null,
573
+ };
574
+ }
575
+
576
+ const cachedAtMs = Date.parse(bundle.cachedAt || bundle.lastUpdatedAt || bundle.lastUpstreamOkAt || '');
577
+ const softExpiresAt = Number(bundle.softExpiresAt || 0);
578
+ const hardExpiresAt = Number(bundle.hardExpiresAt || 0);
579
+ const ageSeconds = Number.isFinite(cachedAtMs) ? Math.max(0, Math.round((now - cachedAtMs) / 1000)) : null;
580
+ const softExpired = !softExpiresAt || softExpiresAt <= now;
581
+ const hardExpired = !hardExpiresAt || hardExpiresAt <= now;
582
+ const phase = hardExpired ? 'expired' : (softExpired ? 'degraded' : 'fresh');
583
+ return {
584
+ present: true,
585
+ phase,
586
+ usable: !hardExpired,
587
+ softExpired,
588
+ hardExpired,
589
+ ageSeconds,
590
+ softTtlSeconds: Number(bundle.softTtlSeconds || DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS),
591
+ hardTtlSeconds: Number(bundle.hardTtlSeconds || DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS),
592
+ softExpiresAt: softExpiresAt ? new Date(softExpiresAt).toISOString() : null,
593
+ hardExpiresAt: hardExpiresAt ? new Date(hardExpiresAt).toISOString() : null,
594
+ cachedAt: bundle.cachedAt || null,
595
+ lastUpstreamOkAt: bundle.lastUpstreamOkAt || null,
596
+ source: bundle.source || null,
597
+ doctrineBundleHash: bundle.doctrineBundleHash || null,
598
+ lastUpstreamError: bundle.lastUpstreamError || null,
599
+ };
600
+ }
601
+
602
+ function persistOfflineBundle(secret, payload) {
603
+ const existing = loadEncryptedOfflineBundle(secret);
604
+ const now = Date.now();
605
+ const base = existing && typeof existing === 'object' ? existing : {};
606
+ const cachedAt = typeof base.cachedAt === 'string' && base.cachedAt ? base.cachedAt : new Date(now).toISOString();
607
+ const softTtlSeconds = Math.max(60, Number(payload.softTtlSeconds || base.softTtlSeconds || DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS));
608
+ const hardTtlSeconds = Math.max(softTtlSeconds, Number(payload.hardTtlSeconds || base.hardTtlSeconds || DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS));
609
+ const refreshedAt = new Date(now).toISOString();
610
+ const bundle = {
611
+ ...base,
612
+ ...payload,
613
+ cachedAt,
614
+ refreshedAt,
615
+ lastUpdatedAt: refreshedAt,
616
+ lastUpstreamOkAt: payload.lastUpstreamOkAt || refreshedAt,
617
+ softTtlSeconds,
618
+ hardTtlSeconds,
619
+ softExpiresAt: now + softTtlSeconds * 1000,
620
+ hardExpiresAt: now + hardTtlSeconds * 1000,
621
+ };
622
+ saveEncryptedOfflineBundle(bundle, secret);
623
+ return bundle;
624
+ }
625
+
626
+ function readLegacyPacketCache() {
627
+ const candidates = [];
628
+ for (const candidate of LEGACY_PACKET_CACHE_CANDIDATES) {
629
+ try {
630
+ if (!existsSync(candidate)) continue;
631
+ const raw = JSON.parse(readFileSync(candidate, 'utf8'));
632
+ const packet = normalizeHarnessPacketPayload(raw.packet ?? raw);
633
+ const harness = typeof packet?.harness === 'string' ? packet.harness : '';
634
+ if (!harness.trim()) continue;
635
+ candidates.push({
636
+ path: candidate,
637
+ packet,
638
+ mtimeMs: statSync(candidate).mtimeMs,
639
+ });
640
+ } catch {}
641
+ }
642
+ candidates.sort((a, b) => b.mtimeMs - a.mtimeMs);
643
+ return candidates[0] || null;
644
+ }
645
+
646
+ function ensureOfflineBundleSeeded(secret, lease = null) {
647
+ const existing = loadEncryptedOfflineBundle(secret);
648
+ const normalizedExisting = normalizeHarnessPacketPayload(existing?.packet);
649
+ if (normalizedExisting && typeof normalizedExisting.harness === 'string' && normalizedExisting.harness.trim()) {
650
+ return existing;
651
+ }
652
+ const legacy = readLegacyPacketCache();
653
+ if (!legacy) return existing;
654
+ return persistOfflineBundle(secret, {
655
+ ...(existing && typeof existing === 'object' ? existing : {}),
656
+ keyHash: hashKey(secret),
657
+ packet: legacy.packet,
658
+ lease: lease || existing?.lease || loadEncryptedLease(secret),
659
+ source: `legacy-cache:${legacy.path}`,
660
+ doctrineBundleHash: lease?.claims?.doctrine_bundle_hash || existing?.doctrineBundleHash || null,
661
+ lastUpstreamError: existing?.lastUpstreamError || null,
662
+ });
663
+ }
664
+
393
665
  function defaultCognitionState() {
394
666
  return {
395
667
  telemetry: [],
396
668
  decisions: [],
669
+ pendingDecisions: [],
397
670
  evolutionPrinciples: [],
398
671
  aegisPatterns: [],
399
672
  heartbeats: [],
@@ -586,6 +859,7 @@ function summarizeCognitionState(state) {
586
859
  return {
587
860
  telemetryTurns: Array.isArray(state.telemetry) ? state.telemetry.length : 0,
588
861
  decisions: Array.isArray(state.decisions) ? state.decisions.length : 0,
862
+ pendingDecisionUploads: Array.isArray(state.pendingDecisions) ? state.pendingDecisions.length : 0,
589
863
  evolutionPrinciples: Array.isArray(state.evolutionPrinciples) ? state.evolutionPrinciples.length : 0,
590
864
  aegisPatterns: Array.isArray(state.aegisPatterns) ? state.aegisPatterns.length : 0,
591
865
  heartbeats: Array.isArray(state.heartbeats) ? state.heartbeats.length : 0,
@@ -827,6 +1101,15 @@ async function heartbeatUpstream(req, body, client, apiKey) {
827
1101
  },
828
1102
  };
829
1103
  clearRevocationLock();
1104
+ const existingBundle = loadEncryptedOfflineBundle(apiKey);
1105
+ persistOfflineBundle(apiKey, {
1106
+ keyHash: lease.keyHash,
1107
+ lease,
1108
+ packet: existingBundle?.packet || null,
1109
+ source: existingBundle?.source || client.baseUrl || DEFAULT_HARNESS_URL,
1110
+ doctrineBundleHash: lease.claims?.doctrine_bundle_hash || existingBundle?.doctrineBundleHash || null,
1111
+ lastUpstreamError: null,
1112
+ });
830
1113
  } catch (error) {
831
1114
  if (!ownerBypassAllowed) {
832
1115
  throw error;
@@ -883,14 +1166,45 @@ async function ensureLease(req, body, client) {
883
1166
 
884
1167
  const keyHash = hashKey(apiKey);
885
1168
  const cached = leaseCache.get(keyHash) || loadEncryptedLease(apiKey);
1169
+ ensureOfflineBundleSeeded(apiKey, cached);
886
1170
  if (cached && cached.hardStopAt > Date.now()) {
887
1171
  leaseCache.set(keyHash, cached);
888
1172
  if (cached.nextRequiredAt > Date.now()) {
889
1173
  return cached;
890
1174
  }
891
1175
  }
892
-
893
- return heartbeatUpstream(req, body, client, apiKey);
1176
+ try {
1177
+ return await heartbeatUpstream(req, body, client, apiKey);
1178
+ } catch (error) {
1179
+ const bundle = ensureOfflineBundleSeeded(apiKey, cached) || loadEncryptedOfflineBundle(apiKey);
1180
+ const bundleStatus = computeOfflineBundleStatus(bundle);
1181
+ if (bundle?.keyHash === keyHash && bundleStatus.usable) {
1182
+ const fallbackLease = cached || bundle.lease || null;
1183
+ if (fallbackLease) {
1184
+ const lease = {
1185
+ ...fallbackLease,
1186
+ offlineBundle: {
1187
+ phase: bundleStatus.phase,
1188
+ ageSeconds: bundleStatus.ageSeconds,
1189
+ softExpiresAt: bundleStatus.softExpiresAt,
1190
+ hardExpiresAt: bundleStatus.hardExpiresAt,
1191
+ doctrineBundleHash: bundleStatus.doctrineBundleHash,
1192
+ source: bundleStatus.source,
1193
+ lastUpstreamError: error instanceof Error ? error.message : String(error),
1194
+ },
1195
+ };
1196
+ leaseCache.set(keyHash, lease);
1197
+ persistOfflineBundle(apiKey, {
1198
+ ...bundle,
1199
+ lease,
1200
+ lastUpstreamError: error instanceof Error ? error.message : String(error),
1201
+ lastUpstreamOkAt: bundle.lastUpstreamOkAt || bundle.cachedAt || new Date().toISOString(),
1202
+ });
1203
+ return lease;
1204
+ }
1205
+ }
1206
+ throw error;
1207
+ }
894
1208
  }
895
1209
 
896
1210
  function deriveSessionId(req, body, prefix = 'runtime') {
@@ -919,6 +1233,47 @@ function findVerifiedState(text) {
919
1233
  return /(?:verified|confirmed|observed|tested|health[- ]check|response code|exit code|pod image|digest)/i.test(String(text || ''));
920
1234
  }
921
1235
 
1236
+ const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
1237
+
1238
+ function validateAppliedCognitionContract(text) {
1239
+ const match = String(text || '').match(APPLIED_COGNITION_BLOCK_RX);
1240
+ if (!match) return { ok: false, violations: ['missing <applied_cognition> contract'] };
1241
+ const body = match[1] || '';
1242
+ const required = [
1243
+ ['decision_delta', /\bdecision[_ -]?delta\s*:/i],
1244
+ ['dominant_domain', /\bdominant[_ -]?domain\s*:/i],
1245
+ ['binds_to', /\bbinds[_ -]?to\s*:/i],
1246
+ ['expected_predicate', /\bexpected[_ -]?predicate\s*:/i],
1247
+ ['artifact_change', /\bartifact[_ -]?change\s*:/i],
1248
+ ];
1249
+ const violations = [];
1250
+ for (const [name, rx] of required) {
1251
+ if (!rx.test(body)) violations.push(`missing ${name}`);
1252
+ }
1253
+ if (/decision[_ -]?delta\s*:\s*(?:none|n\/a|no change|unchanged|same)/i.test(body)) {
1254
+ violations.push('decision_delta says cognition changed nothing');
1255
+ }
1256
+ return { ok: violations.length === 0, violations, contract: body.trim() };
1257
+ }
1258
+
1259
+ function mergeAppliedCognitionValidation(validation, applied) {
1260
+ if (applied.ok) {
1261
+ return {
1262
+ ...validation,
1263
+ appliedCognition: { ok: true, contract: applied.contract },
1264
+ gateTriggers: [...new Set([...(validation.gateTriggers || []), 'applied-cognition-contract'])],
1265
+ };
1266
+ }
1267
+ return {
1268
+ ...validation,
1269
+ passed: false,
1270
+ severity: 'block',
1271
+ violations: [...(validation.violations || []), ...applied.violations.map((v) => `applied_cognition: ${v}`)],
1272
+ gateTriggers: [...new Set([...(validation.gateTriggers || []), 'applied-cognition-contract-missing'])],
1273
+ appliedCognition: { ok: false, violations: applied.violations },
1274
+ };
1275
+ }
1276
+
922
1277
  function toTelemetryEvent(payload, source = 'aria-mounted-runtime') {
923
1278
  return {
924
1279
  event_type: payload.event_type || 'runtime.cognition.turn',
@@ -949,6 +1304,7 @@ async function pushTelemetryUpstream(client, apiKey, payload) {
949
1304
  }
950
1305
 
951
1306
  async function pushDecisionUpstream(client, apiKey, payload) {
1307
+ const normalizedPayload = normalizeDecisionPayload(payload);
952
1308
  const url = `${client.baseUrl || DEFAULT_HARNESS_URL}/api/decisions/log`;
953
1309
  const response = await fetch(url, {
954
1310
  method: 'POST',
@@ -956,7 +1312,7 @@ async function pushDecisionUpstream(client, apiKey, payload) {
956
1312
  Authorization: `Bearer ${apiKey}`,
957
1313
  'Content-Type': 'application/json',
958
1314
  },
959
- body: JSON.stringify(payload),
1315
+ body: JSON.stringify(normalizedPayload),
960
1316
  });
961
1317
  if (!response.ok) {
962
1318
  const body = await response.text().catch(() => response.statusText);
@@ -965,6 +1321,88 @@ async function pushDecisionUpstream(client, apiKey, payload) {
965
1321
  return response.json().catch(() => ({ logged: true }));
966
1322
  }
967
1323
 
1324
+ function normalizeDecisionPayload(payload) {
1325
+ const sessionId = payload?.session_id || payload?.sessionId || null;
1326
+ const decisionType = payload?.decision_type || payload?.decisionType || 'operational';
1327
+ const category = payload?.category || payload?.decision_category || payload?.decisionCategory || payload?.surface || 'runtime';
1328
+ const decision = payload?.decision || payload?.summary || payload?.outcome || decisionType;
1329
+ const reasoning =
1330
+ payload?.reasoning ||
1331
+ payload?.summary ||
1332
+ (payload?.outcome ? `Outcome: ${payload.outcome}` : 'Runtime decision log');
1333
+ const context =
1334
+ payload?.context ||
1335
+ (sessionId ? `Session ${sessionId}` : 'Runtime decision log');
1336
+ const outcomeRaw = String(payload?.outcome || '').toLowerCase();
1337
+ const outcome =
1338
+ outcomeRaw === 'validated' ? 'success'
1339
+ : outcomeRaw === 'error' ? 'failure'
1340
+ : ['success', 'failure', 'neutral', 'pending'].includes(outcomeRaw) ? outcomeRaw
1341
+ : undefined;
1342
+
1343
+ return {
1344
+ ...payload,
1345
+ session_id: sessionId,
1346
+ decision_type: decisionType,
1347
+ category,
1348
+ context,
1349
+ decision,
1350
+ reasoning,
1351
+ ...(outcome ? { outcome } : {}),
1352
+ };
1353
+ }
1354
+
1355
+ function delay(ms) {
1356
+ return new Promise((resolve) => setTimeout(resolve, ms));
1357
+ }
1358
+
1359
+ async function pushDecisionUpstreamWithRetry(client, apiKey, payload, attempts = 3) {
1360
+ let lastError = null;
1361
+ for (let attempt = 0; attempt < attempts; attempt++) {
1362
+ try {
1363
+ return await pushDecisionUpstream(client, apiKey, payload);
1364
+ } catch (error) {
1365
+ lastError = error;
1366
+ if (attempt === attempts - 1) break;
1367
+ await delay(250 * (2 ** attempt));
1368
+ }
1369
+ }
1370
+ throw lastError || new Error('decision upstream failed');
1371
+ }
1372
+
1373
+ async function flushPendingDecisionUploads(client, apiKey) {
1374
+ const state = loadEncryptedCognitionState(apiKey);
1375
+ const pending = Array.isArray(state.pendingDecisions) ? state.pendingDecisions : [];
1376
+ if (!pending.length) {
1377
+ return { flushed: 0, retained: 0, lastError: null };
1378
+ }
1379
+
1380
+ const remaining = [];
1381
+ let flushed = 0;
1382
+ let lastError = null;
1383
+ for (const entry of pending) {
1384
+ try {
1385
+ await pushDecisionUpstreamWithRetry(client, apiKey, entry.payload);
1386
+ flushed++;
1387
+ } catch (error) {
1388
+ lastError = error instanceof Error ? error.message : String(error);
1389
+ remaining.push({
1390
+ ...entry,
1391
+ attempts: Number(entry.attempts || 0) + 1,
1392
+ lastError,
1393
+ lastTriedAt: new Date().toISOString(),
1394
+ });
1395
+ }
1396
+ }
1397
+
1398
+ mutateCognitionState(apiKey, (current) => ({
1399
+ ...current,
1400
+ pendingDecisions: remaining,
1401
+ }));
1402
+
1403
+ return { flushed, retained: remaining.length, lastError };
1404
+ }
1405
+
968
1406
  function buildMinimalInjection(packet, task, aegisLearnings = null) {
969
1407
  return {
970
1408
  harness: packet,
@@ -1022,9 +1460,35 @@ function buildOwnerBypassPacket(message, reason = 'owner-local-bypass') {
1022
1460
 
1023
1461
  async function loadRuntimePacket(req, body, client, packetRequest, message) {
1024
1462
  if (body.packet) return body.packet;
1463
+ const apiKey = resolveApiKey(req, body);
1464
+ ensureOfflineBundleSeeded(apiKey, leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey));
1025
1465
  try {
1026
- return await client.getHarnessPacket(packetRequest || {});
1466
+ const wrapped = await client.getHarnessPacket(packetRequest || {});
1467
+ const packet = normalizeHarnessPacketPayload(wrapped?.packet || wrapped);
1468
+ const lease = leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey);
1469
+ persistOfflineBundle(apiKey, {
1470
+ keyHash: hashKey(apiKey),
1471
+ packet,
1472
+ lease,
1473
+ source: client.baseUrl || DEFAULT_HARNESS_URL,
1474
+ doctrineBundleHash: lease?.claims?.doctrine_bundle_hash || null,
1475
+ lastUpstreamError: null,
1476
+ });
1477
+ return packet;
1027
1478
  } catch (error) {
1479
+ const bundle = ensureOfflineBundleSeeded(apiKey, leaseCache.get(hashKey(apiKey)) || loadEncryptedLease(apiKey)) || loadEncryptedOfflineBundle(apiKey);
1480
+ const bundleStatus = computeOfflineBundleStatus(bundle);
1481
+ if (bundle?.keyHash === hashKey(apiKey) && bundleStatus.usable) {
1482
+ const fallbackPacket = buildOfflineBundleFallbackPacket(bundle, bundleStatus);
1483
+ if (fallbackPacket) {
1484
+ persistOfflineBundle(apiKey, {
1485
+ ...bundle,
1486
+ lastUpstreamError: error instanceof Error ? error.message : String(error),
1487
+ lastUpstreamOkAt: bundle.lastUpstreamOkAt || bundle.cachedAt || new Date().toISOString(),
1488
+ });
1489
+ return fallbackPacket;
1490
+ }
1491
+ }
1028
1492
  if (!isOwnerBypassRequest(req, body)) {
1029
1493
  throw error;
1030
1494
  }
@@ -1266,6 +1730,174 @@ function anthropicResponseEnvelope(text, providerMeta, extra = {}, debug = false
1266
1730
  };
1267
1731
  }
1268
1732
 
1733
+ function flattenResponsesTextContent(content) {
1734
+ if (typeof content === 'string') return content;
1735
+ if (!Array.isArray(content)) {
1736
+ if (typeof content?.text === 'string') return content.text;
1737
+ if (typeof content?.content === 'string') return content.content;
1738
+ return '';
1739
+ }
1740
+ return content
1741
+ .map((part) => {
1742
+ if (typeof part === 'string') return part;
1743
+ if (typeof part?.text === 'string') return part.text;
1744
+ if (typeof part?.content === 'string') return part.content;
1745
+ if ((part?.type === 'input_text' || part?.type === 'output_text' || part?.type === 'text') && typeof part?.text === 'string') {
1746
+ return part.text;
1747
+ }
1748
+ return '';
1749
+ })
1750
+ .filter(Boolean)
1751
+ .join('\n');
1752
+ }
1753
+
1754
+ function normalizeResponsesTool(tool) {
1755
+ if (!tool || typeof tool !== 'object') return null;
1756
+ if (tool.type === 'function' && tool.function && typeof tool.function === 'object') {
1757
+ return tool;
1758
+ }
1759
+ if (tool.type === 'function' || typeof tool.name === 'string') {
1760
+ return {
1761
+ type: 'function',
1762
+ function: {
1763
+ name: tool.name || tool.function?.name || 'tool',
1764
+ description: tool.description || tool.function?.description || '',
1765
+ parameters: tool.parameters || tool.function?.parameters || { type: 'object', properties: {} },
1766
+ },
1767
+ };
1768
+ }
1769
+ return tool;
1770
+ }
1771
+
1772
+ function responsesInputToMessages(input) {
1773
+ if (typeof input === 'string' && input.trim()) {
1774
+ return [{ role: 'user', content: input.trim() }];
1775
+ }
1776
+ if (!Array.isArray(input)) return [];
1777
+
1778
+ const messages = [];
1779
+ for (const item of input) {
1780
+ if (!item || typeof item !== 'object') continue;
1781
+ if (item.type === 'message') {
1782
+ const role = typeof item.role === 'string' ? item.role : 'user';
1783
+ const text = flattenResponsesTextContent(item.content);
1784
+ if (text) messages.push({ role, content: text });
1785
+ continue;
1786
+ }
1787
+ if (item.type === 'input_text' || item.type === 'output_text' || item.type === 'text') {
1788
+ const role = item.role === 'assistant' ? 'assistant' : 'user';
1789
+ const text = flattenResponsesTextContent(item);
1790
+ if (text) messages.push({ role, content: text });
1791
+ continue;
1792
+ }
1793
+ if (item.type === 'function_call_output') {
1794
+ const outputText =
1795
+ typeof item.output === 'string'
1796
+ ? item.output
1797
+ : JSON.stringify(item.output || {});
1798
+ if (outputText) {
1799
+ messages.push({
1800
+ role: 'tool',
1801
+ tool_call_id: item.call_id || item.id || null,
1802
+ content: outputText,
1803
+ });
1804
+ }
1805
+ continue;
1806
+ }
1807
+ if (item.type === 'function_call') {
1808
+ const args =
1809
+ typeof item.arguments === 'string'
1810
+ ? item.arguments
1811
+ : JSON.stringify(item.arguments || item.input || {});
1812
+ messages.push({
1813
+ role: 'assistant',
1814
+ content: `${item.name || 'function_call'} ${args}`.trim(),
1815
+ });
1816
+ }
1817
+ }
1818
+ return messages;
1819
+ }
1820
+
1821
+ function responsesRequestToOpenAIBody(body = {}) {
1822
+ const messages = responsesInputToMessages(body.input);
1823
+ const instructions = typeof body.instructions === 'string' && body.instructions.trim()
1824
+ ? [{ role: 'system', content: body.instructions.trim() }]
1825
+ : [];
1826
+
1827
+ return {
1828
+ ...body,
1829
+ client: body.client || 'codex',
1830
+ surface: body.surface || 'codex',
1831
+ messages: [...instructions, ...messages],
1832
+ tools: Array.isArray(body.tools) ? body.tools.map(normalizeResponsesTool).filter(Boolean) : undefined,
1833
+ tool_choice: body.tool_choice,
1834
+ max_completion_tokens: body.max_output_tokens || body.max_completion_tokens,
1835
+ metadata: {
1836
+ ...(body.metadata && typeof body.metadata === 'object' ? body.metadata : {}),
1837
+ response_api: true,
1838
+ client: body.client || 'codex',
1839
+ surface: body.surface || 'codex',
1840
+ },
1841
+ };
1842
+ }
1843
+
1844
+ function openAiCompletionToResponsesEnvelope(body, completion) {
1845
+ const message = completion?.choices?.[0]?.message || {};
1846
+ const text = typeof message.content === 'string' ? message.content : '';
1847
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
1848
+ const output = [];
1849
+
1850
+ if (text) {
1851
+ output.push({
1852
+ id: `msg_${randomUUID().replace(/-/g, '')}`,
1853
+ type: 'message',
1854
+ role: 'assistant',
1855
+ status: 'completed',
1856
+ content: [
1857
+ {
1858
+ type: 'output_text',
1859
+ text,
1860
+ },
1861
+ ],
1862
+ });
1863
+ }
1864
+
1865
+ for (const toolCall of toolCalls) {
1866
+ const callId = toolCall?.id || `call_${randomUUID().replace(/-/g, '')}`;
1867
+ const functionInfo = toolCall?.function || {};
1868
+ output.push({
1869
+ id: callId,
1870
+ type: 'function_call',
1871
+ status: 'completed',
1872
+ call_id: callId,
1873
+ name: functionInfo.name || toolCall?.name || 'tool',
1874
+ arguments:
1875
+ typeof functionInfo.arguments === 'string'
1876
+ ? functionInfo.arguments
1877
+ : JSON.stringify(functionInfo.arguments || toolCall?.arguments || {}),
1878
+ });
1879
+ }
1880
+
1881
+ return {
1882
+ id: `resp_${randomUUID().replace(/-/g, '')}`,
1883
+ object: 'response',
1884
+ created_at: new Date().toISOString(),
1885
+ status: 'completed',
1886
+ model: completion?.model || body?.model || 'aria-runtime',
1887
+ output,
1888
+ output_text: text,
1889
+ usage: completion?.usage
1890
+ ? {
1891
+ input_tokens: completion.usage.prompt_tokens || 0,
1892
+ output_tokens: completion.usage.completion_tokens || 0,
1893
+ total_tokens: completion.usage.total_tokens || ((completion.usage.prompt_tokens || 0) + (completion.usage.completion_tokens || 0)),
1894
+ }
1895
+ : undefined,
1896
+ aria: completion?.aria || null,
1897
+ metadata: body?.metadata || null,
1898
+ };
1899
+ }
1900
+
1269
1901
  function toReadableSignal(name) {
1270
1902
  const map = {
1271
1903
  fitrah_gate: 'truth boundary',
@@ -1644,6 +2276,10 @@ function computeForgeContractIssues(manifest, forgeResult) {
1644
2276
  }
1645
2277
 
1646
2278
  async function persistTurnArtifacts(req, body, client, apiKey, turn) {
2279
+ persistMizanBundle(apiKey, turn.preBundle, 'runtime/v1-pre');
2280
+ persistMizanBundle(apiKey, turn.midBundle, 'runtime/v1-mid');
2281
+ persistMizanBundle(apiKey, turn.postBundle, 'runtime/v1-post');
2282
+
1647
2283
  const evolutionPrinciples = extractEvolutionPrinciples(turn.packet, [turn.preResult, turn.midResult, turn.postResult]);
1648
2284
  const aegisPatterns = [
1649
2285
  ...extractAegisPatterns(turn.midResult || {}),
@@ -1783,39 +2419,93 @@ async function persistTurnArtifacts(req, body, client, apiKey, turn) {
1783
2419
  await pushTelemetryUpstream(client, apiKey, telemetryPayload);
1784
2420
  } catch {}
1785
2421
 
1786
- if (findVerifiedState(turn.finalText) || /recommend|should|propose|choose/i.test(turn.finalText)) {
2422
+ const isDecisionTurn =
2423
+ (turn.turnClass?.intensity && turn.turnClass.intensity !== 'light') ||
2424
+ isNonTrivialAssistantTurn(turn.userMessage || '', []) ||
2425
+ isNonTrivialAssistantTurn(turn.finalText || '', []);
2426
+ if (isDecisionTurn) {
2427
+ const surface =
2428
+ body?.surface ||
2429
+ body?.platform ||
2430
+ body?.client ||
2431
+ body?.metadata?.surface ||
2432
+ body?.metadata?.client ||
2433
+ 'aria-mounted-runtime';
2434
+ const reasoning = [
2435
+ ...(turn.preResult?.notes || []),
2436
+ ...(turn.midResult?.notes || []),
2437
+ ...(turn.postResult?.notes || []),
2438
+ ...(Array.isArray(turn.validation?.violations) ? turn.validation.violations : []),
2439
+ ...(Array.isArray(turn.layer3?.failures)
2440
+ ? turn.layer3.failures.map((failure) => failure?.detail).filter(Boolean)
2441
+ : []),
2442
+ ]
2443
+ .join(' | ')
2444
+ .slice(0, 4000) || 'Aria runtime cognition turn';
1787
2445
  const decisionPayload = {
1788
2446
  decision_type: body?.metadata?.decision_type || 'runtime-turn',
1789
2447
  category: body?.metadata?.decision_category || 'cognition-control-plane',
1790
- context: turn.userMessage.slice(0, 2000) || 'mounted runtime turn',
1791
- decision: turn.finalText.slice(0, 2000),
1792
- reasoning: [
1793
- ...(turn.preResult?.notes || []),
1794
- ...(turn.midResult?.notes || []),
1795
- ...(turn.postResult?.notes || []),
1796
- ].join(' | ').slice(0, 4000) || 'Aria runtime cognition turn',
2448
+ context:
2449
+ (turn.userMessage && turn.userMessage.slice(0, 2000)) ||
2450
+ `${surface} runtime turn (${turn.providerMeta.provider || 'provider'})`,
2451
+ decision: turn.success ? 'turn completed' : 'turn blocked by runtime gate',
2452
+ reasoning,
1797
2453
  outcome: 'pending',
1798
- expected_outcome: {
1799
- predicate: 'response survives runtime validation and compounds into central telemetry',
2454
+ outcome_details: {
2455
+ expected: body?.metadata?.expected_outcome || null,
2456
+ immediate_actual: {
2457
+ success: turn.success,
2458
+ validation_severity: turn.validation?.severity || null,
2459
+ layer3_pass: turn.layer3?.pass ?? null,
2460
+ doctrine_hits: Array.isArray(turn.layer3?.doctrine?.hits)
2461
+ ? turn.layer3.doctrine.hits.length
2462
+ : 0,
2463
+ },
2464
+ anchors: [
2465
+ turn.preReceipt?.receiptId ? `pre_receipt:${turn.preReceipt.receiptId}` : null,
2466
+ turn.midReceipt?.receiptId ? `mid_receipt:${turn.midReceipt.receiptId}` : null,
2467
+ turn.postReceipt?.receiptId ? `post_receipt:${turn.postReceipt.receiptId}` : null,
2468
+ ].filter(Boolean),
2469
+ },
2470
+ expected_outcome: body?.metadata?.expected_outcome || {
2471
+ predicate: 'turn survives runtime validation and compounds into central telemetry with canonical receipts',
1800
2472
  measurable_type: 'boolean',
1801
2473
  threshold: true,
1802
2474
  },
1803
- source: 'aria-mounted-runtime',
2475
+ metadata: {
2476
+ session_id: turn.sessionId,
2477
+ surface,
2478
+ provider: turn.providerMeta.provider || null,
2479
+ finish_reason: turn.providerMeta.finishReason || null,
2480
+ pre_receipt_id: turn.preReceipt?.receiptId || null,
2481
+ mid_receipt_id: turn.midReceipt?.receiptId || null,
2482
+ post_receipt_id: turn.postReceipt?.receiptId || null,
2483
+ validation_severity: turn.validation?.severity || null,
2484
+ validation_passed: turn.validation?.passed ?? null,
2485
+ layer3_pass: turn.layer3?.pass ?? null,
2486
+ packet_bypassed: turn.packetBypassed === true,
2487
+ },
2488
+ source: body?.metadata?.decision_source || `${surface}-runtime`,
1804
2489
  model_used: turn.providerMeta.model,
1805
2490
  code_links: body?.metadata?.code_links || null,
1806
2491
  };
2492
+ let decisionResult = null;
2493
+ let decisionError = null;
1807
2494
  try {
1808
- const decisionResult = await pushDecisionUpstream(client, apiKey, decisionPayload);
1809
- mutateCognitionState(apiKey, (state) => ({
1810
- ...state,
1811
- decisions: appendBounded(state.decisions, {
1812
- at: new Date().toISOString(),
1813
- sessionId: turn.sessionId,
1814
- decision: decisionPayload,
1815
- result: decisionResult,
1816
- }, DECISION_LIMIT),
1817
- }));
1818
- } catch {}
2495
+ decisionResult = await pushDecisionUpstream(client, apiKey, decisionPayload);
2496
+ } catch (error) {
2497
+ decisionError = error instanceof Error ? error.message : String(error);
2498
+ }
2499
+ mutateCognitionState(apiKey, (state) => ({
2500
+ ...state,
2501
+ decisions: appendBounded(state.decisions, {
2502
+ at: new Date().toISOString(),
2503
+ sessionId: turn.sessionId,
2504
+ decision: decisionPayload,
2505
+ result: decisionResult,
2506
+ error: decisionError,
2507
+ }, DECISION_LIMIT),
2508
+ }));
1819
2509
  }
1820
2510
 
1821
2511
  if (body.ariaGarden !== false && turn.userMessage && turn.finalText) {
@@ -1841,6 +2531,20 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1841
2531
  ? await callProviderForAnthropic(body, turn.ariaSystemPrompt)
1842
2532
  : await callProviderForOpenAI(body, turn.ariaSystemPrompt);
1843
2533
 
2534
+ try {
2535
+ recordTokenUsage({
2536
+ tenantId: body?.metadata?.jti || body?.jti || 'owner-local',
2537
+ agentId: body?.metadata?.agentId || body?.metadata?.roleProfile || null,
2538
+ agentName: body?.metadata?.agentName || null,
2539
+ department: body?.metadata?.department || null,
2540
+ sessionId: turn.sessionId,
2541
+ provider: providerMeta.provider,
2542
+ model: providerMeta.model,
2543
+ usage: providerMeta.usage,
2544
+ requestType: body?.metadata?.requestType || 'chat',
2545
+ });
2546
+ } catch {}
2547
+
1844
2548
  let candidateText = providerMeta.text || '';
1845
2549
  const toolIntents = extractProviderToolIntents(providerStyle, providerMeta);
1846
2550
  const requiresReadableCognition = isNonTrivialAssistantTurn(candidateText, toolIntents);
@@ -1856,7 +2560,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1856
2560
  try {
1857
2561
  validation = await client.validateOutput(candidateText, turn.sessionId);
1858
2562
  } catch (error) {
1859
- if (!turn.packetBypassed) throw error;
2563
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
1860
2564
  validation = {
1861
2565
  passed: true,
1862
2566
  severity: 'warn',
@@ -1871,7 +2575,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1871
2575
  try {
1872
2576
  validation = await client.validateOutput(candidateText, turn.sessionId);
1873
2577
  } catch (error) {
1874
- if (!turn.packetBypassed) throw error;
2578
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
1875
2579
  validation = {
1876
2580
  passed: true,
1877
2581
  severity: 'warn',
@@ -1911,7 +2615,7 @@ async function handleProviderProxy(req, body, client, providerStyle) {
1911
2615
  toolGateBlockers.push(`${intent.toolName}: ${runtimeCheck.reason || 'runtime action gate blocked this tool request'}`);
1912
2616
  }
1913
2617
  } catch (error) {
1914
- if (!turn.packetBypassed) throw error;
2618
+ if (!turn.packetBypassed && !isOwnerBypassRequest(req, body, apiKey)) throw error;
1915
2619
  toolGateBlockers.push(`${intent.toolName}: runtime action gate unavailable during owner-local-bypass`);
1916
2620
  }
1917
2621
  }
@@ -2260,6 +2964,8 @@ function packetToSubstrateSet(packet) {
2260
2964
  function runtimeManifest() {
2261
2965
  const runtimeMeta = ensureRuntimeMeta();
2262
2966
  const state = sweepAutonomyState(loadAutonomyState());
2967
+ const ownerToken = readOwnerToken();
2968
+ const offlineBundleStatus = ownerToken ? computeOfflineBundleStatus(loadEncryptedOfflineBundle(ownerToken)) : computeOfflineBundleStatus(null);
2263
2969
  return {
2264
2970
  ok: true,
2265
2971
  runtime: 'aria-mounted-runtime',
@@ -2306,6 +3012,8 @@ function runtimeManifest() {
2306
3012
  'POST /forge/synthesize',
2307
3013
  'POST /codebase/state',
2308
3014
  'POST /v1/chat/completions',
3015
+ 'POST /v1/responses',
3016
+ 'POST /responses',
2309
3017
  'POST /v1/messages',
2310
3018
  ],
2311
3019
  mount: {
@@ -2323,9 +3031,13 @@ function runtimeManifest() {
2323
3031
  },
2324
3032
  security: {
2325
3033
  encrypted_local_lease: LEASE_PATH,
3034
+ encrypted_offline_bundle: OFFLINE_BUNDLE_PATH,
2326
3035
  encrypted_cognition_state: COGNITION_STATE_PATH,
2327
3036
  revocation_lock: REVOCATION_LOCK_PATH,
2328
3037
  upstream_heartbeat: '/api/license/heartbeat',
3038
+ offline_bundle_soft_ttl_seconds: DEFAULT_OFFLINE_BUNDLE_SOFT_TTL_SECONDS,
3039
+ offline_bundle_hard_ttl_seconds: DEFAULT_OFFLINE_BUNDLE_HARD_TTL_SECONDS,
3040
+ offline_bundle_status: offlineBundleStatus,
2329
3041
  },
2330
3042
  memory: {
2331
3043
  qdrant_url: DEFAULT_QDRANT_URL,
@@ -2354,12 +3066,27 @@ async function runLayer3(req, body, client) {
2354
3066
  substrate: packetToSubstrateSet(packet),
2355
3067
  requireCognitionBlock: body.requireCognitionBlock ?? false,
2356
3068
  });
3069
+ const doctrineHits = collectDoctrineTriggerHits(body.text);
3070
+ const doctrineFailures = doctrineHits.map((hit) => ({
3071
+ severity: String(hit.severity || 'block').toLowerCase() === 'block' ? 'block' : 'warn',
3072
+ kind: 'drift_trigger',
3073
+ detail: `${hit.trigger} (${hit.memory || 'doctrine_trigger_map.json'}): ${hit.message || hit.teaching || 'Doctrine trigger matched.'}`,
3074
+ }));
3075
+ const allFailures = [...result.failures, ...doctrineFailures];
3076
+ const hardFailures = allFailures.filter((failure) => failure.severity === 'block');
2357
3077
 
2358
3078
  return {
2359
- pass: result.pass,
2360
- summary: result.summary,
2361
- failures: result.failures,
3079
+ pass: hardFailures.length === 0,
3080
+ summary:
3081
+ hardFailures.length === 0
3082
+ ? `full_chain: pass (${allFailures.length} warns)`
3083
+ : `full_chain: ${hardFailures.length} hard failures across ${allFailures.length} total`,
3084
+ failures: allFailures,
2362
3085
  packetTimestamp: packet.timestamp || null,
3086
+ doctrine: {
3087
+ sourcePath: doctrineTriggerMapCache.sourcePath,
3088
+ hits: doctrineHits,
3089
+ },
2363
3090
  };
2364
3091
  }
2365
3092
 
@@ -2602,7 +3329,10 @@ async function handleRoute(req, res) {
2602
3329
 
2603
3330
  let client;
2604
3331
  try {
2605
- client = createClient(req, body);
3332
+ // HQ routes use their own auth middleware — skip the global API key gate
3333
+ if (!url.pathname.startsWith('/hq/')) {
3334
+ client = createClient(req, body);
3335
+ }
2606
3336
  } catch (error) {
2607
3337
  return json(res, 401, { ok: false, error: error.message });
2608
3338
  }
@@ -2698,6 +3428,12 @@ async function handleRoute(req, res) {
2698
3428
  return json(res, 200, response);
2699
3429
  }
2700
3430
 
3431
+ if (url.pathname === '/v1/responses' || url.pathname === '/responses') {
3432
+ const responseBody = responsesRequestToOpenAIBody(body);
3433
+ const completion = await handleProviderProxy(req, responseBody, client, 'openai');
3434
+ return json(res, 200, openAiCompletionToResponsesEnvelope(body, completion));
3435
+ }
3436
+
2701
3437
  if (url.pathname === '/v1/messages') {
2702
3438
  const response = await handleProviderProxy(req, body, client, 'anthropic');
2703
3439
  return json(res, 200, response);
@@ -2728,16 +3464,55 @@ async function handleRoute(req, res) {
2728
3464
 
2729
3465
  if (url.pathname === '/decision/log') {
2730
3466
  const apiKey = resolveApiKey(req, body);
2731
- const result = await pushDecisionUpstream(client, apiKey, body);
2732
- mutateCognitionState(apiKey, (state) => ({
2733
- ...state,
2734
- decisions: appendBounded(state.decisions, {
2735
- at: new Date().toISOString(),
2736
- decision: body,
3467
+ const normalizedBody = normalizeDecisionPayload(body);
3468
+ const flush = await flushPendingDecisionUploads(client, apiKey);
3469
+ try {
3470
+ const result = await pushDecisionUpstreamWithRetry(client, apiKey, normalizedBody);
3471
+ mutateCognitionState(apiKey, (state) => ({
3472
+ ...state,
3473
+ decisions: appendBounded(state.decisions, {
3474
+ at: new Date().toISOString(),
3475
+ decision: normalizedBody,
3476
+ result,
3477
+ flushedPending: flush.flushed,
3478
+ }, DECISION_LIMIT),
3479
+ }));
3480
+ return json(res, 200, {
3481
+ ok: true,
2737
3482
  result,
2738
- }, DECISION_LIMIT),
2739
- }));
2740
- return json(res, 200, { ok: true, result });
3483
+ flushedPending: flush.flushed,
3484
+ retainedPending: flush.retained,
3485
+ });
3486
+ } catch (error) {
3487
+ const upstreamError = error instanceof Error ? error.message : String(error);
3488
+ mutateCognitionState(apiKey, (state) => ({
3489
+ ...state,
3490
+ decisions: appendBounded(state.decisions, {
3491
+ at: new Date().toISOString(),
3492
+ decision: normalizedBody,
3493
+ result: {
3494
+ logged: false,
3495
+ queuedUpstream: true,
3496
+ upstreamError,
3497
+ },
3498
+ flushedPending: flush.flushed,
3499
+ }, DECISION_LIMIT),
3500
+ pendingDecisions: appendBounded(state.pendingDecisions, {
3501
+ at: new Date().toISOString(),
3502
+ payload: normalizedBody,
3503
+ attempts: 0,
3504
+ lastError: upstreamError,
3505
+ queuedBy: 'decision/log',
3506
+ }, DECISION_LIMIT),
3507
+ }));
3508
+ return json(res, 200, {
3509
+ ok: true,
3510
+ queuedUpstream: true,
3511
+ upstreamError,
3512
+ flushedPending: flush.flushed,
3513
+ retainedPending: flush.retained + 1,
3514
+ });
3515
+ }
2741
3516
  }
2742
3517
 
2743
3518
  if (url.pathname === '/aegis/patterns') {
@@ -2799,12 +3574,12 @@ async function handleRoute(req, res) {
2799
3574
  }
2800
3575
  }
2801
3576
 
2802
- if (url.pathname === '/packet') {
3577
+ if (url.pathname === '/packet' || url.pathname === '/api/harness/codex') {
2803
3578
  const packet = await loadRuntimePacket(req, body, client, body.packetRequest || body, body.message || '');
2804
3579
  return json(res, 200, { ok: true, packet });
2805
3580
  }
2806
3581
 
2807
- if (url.pathname === '/consult') {
3582
+ if (url.pathname === '/consult' || url.pathname === '/api/harness/delegate') {
2808
3583
  const result = await client.consult(body);
2809
3584
  return json(res, 200, { ok: true, ...result });
2810
3585
  }
@@ -2824,7 +3599,7 @@ async function handleRoute(req, res) {
2824
3599
  return json(res, 200, { ok: true, ...result });
2825
3600
  }
2826
3601
 
2827
- if (url.pathname === '/validate-output') {
3602
+ if (url.pathname === '/validate-output' || url.pathname === '/api/harness/validate') {
2828
3603
  if (typeof body.text !== 'string' || typeof body.sessionId !== 'string') {
2829
3604
  throw new Error('validate-output requires text and sessionId');
2830
3605
  }
@@ -2843,6 +3618,7 @@ async function handleRoute(req, res) {
2843
3618
  gateTriggers: ['owner-local-bypass'],
2844
3619
  };
2845
3620
  }
3621
+ validation = mergeAppliedCognitionValidation(validation, validateAppliedCognitionContract(body.text));
2846
3622
  const response = { ok: true, validation };
2847
3623
 
2848
3624
  if (body.runLayer3 !== false) {
@@ -3003,6 +3779,313 @@ async function handleRoute(req, res) {
3003
3779
  return json(res, 200, { ok: true, ...result });
3004
3780
  }
3005
3781
 
3782
+ // ── /hq/* routes: Agentic HQ API ──────────────────────────────
3783
+
3784
+ if (url.pathname.startsWith('/hq/')) {
3785
+ const auth = hqAuthMiddleware(url.pathname, req);
3786
+ if (!auth.authorized) {
3787
+ return json(res, 401, { ok: false, error: auth.error || 'Unauthorized' });
3788
+ }
3789
+ if (auth.tenantId && !body.tenantId) body.tenantId = auth.tenantId;
3790
+ req.hqAuth = auth;
3791
+ }
3792
+
3793
+ // ── /hq/auth/* routes (public, handled before auth gate takes effect) ──
3794
+ if (url.pathname === '/hq/auth/login') {
3795
+ const { username, password } = body;
3796
+ if (!username || !password) return json(res, 400, { ok: false, error: 'username and password required' });
3797
+ const result = loginUser(username, password);
3798
+ if (!result.ok) return json(res, 401, result);
3799
+ return json(res, 200, { ok: true, token: result.session.token, role: result.session.role, tenantId: result.session.tenantId, email: result.session.email });
3800
+ }
3801
+
3802
+ if (url.pathname === '/hq/auth/register') {
3803
+ const { email, password, tenantId } = body;
3804
+ if (!email || !password || !tenantId) return json(res, 400, { ok: false, error: 'email, password, and tenantId required' });
3805
+ const result = registerUser(email, password, tenantId);
3806
+ if (!result.ok) return json(res, 409, result);
3807
+ const loginResult = loginUser(email, password);
3808
+ return json(res, 200, { ok: true, token: loginResult.session.token, role: loginResult.session.role, tenantId: loginResult.session.tenantId, email: loginResult.session.email });
3809
+ }
3810
+
3811
+ if (url.pathname === '/hq/auth/session') {
3812
+ const authHeader = req.headers['authorization'] || '';
3813
+ const token = authHeader.replace(/^Bearer\s+/i, '').trim();
3814
+ if (!token) return json(res, 401, { ok: false, error: 'Token required' });
3815
+ const { validateSession } = await import('./auth-middleware.mjs');
3816
+ const session = validateSession(token);
3817
+ if (!session.valid) return json(res, 401, { ok: false, error: 'Invalid or expired session' });
3818
+ return json(res, 200, { ok: true, role: session.role, tenantId: session.tenantId, email: session.email });
3819
+ }
3820
+
3821
+ if (url.pathname === '/hq/auth/logout') {
3822
+ const authHeader = req.headers['authorization'] || '';
3823
+ const token = authHeader.replace(/^Bearer\s+/i, '').trim();
3824
+ if (token) revokeSession(token);
3825
+ return json(res, 200, { ok: true });
3826
+ }
3827
+
3828
+ // ── /hq/owner/* routes (owner-only) ──
3829
+ if (url.pathname === '/hq/owner/tenants') {
3830
+ const auth = req.hqAuth;
3831
+ if (!auth || auth.role !== 'owner') return json(res, 403, { ok: false, error: 'Owner access required' });
3832
+ const tenants = listAllTenants();
3833
+ const enriched = tenants.map(t => {
3834
+ const fleet = loadFleet(t.tenantId);
3835
+ return { ...t, fleet: fleet ? { deployed: true, activeAgents: fleet.agents?.length || 0, industry: fleet.config?.industry } : { deployed: false } };
3836
+ });
3837
+ return json(res, 200, { ok: true, tenants: enriched });
3838
+ }
3839
+
3840
+ if (url.pathname === '/hq/owner/tenant/dashboard') {
3841
+ const auth = req.hqAuth;
3842
+ if (!auth || auth.role !== 'owner') return json(res, 403, { ok: false, error: 'Owner access required' });
3843
+ const { tenantId: targetTenant } = body;
3844
+ if (!targetTenant) return json(res, 400, { ok: false, error: 'targetTenant required' });
3845
+ const fleet = loadFleet(targetTenant);
3846
+ if (!fleet) return json(res, 404, { ok: false, error: 'No fleet for that tenant' });
3847
+ return json(res, 200, { ok: true, ...getFleetStatus(targetTenant) });
3848
+ }
3849
+
3850
+ if (url.pathname === '/hq/metering/usage') {
3851
+ const { tenantId, from, to } = body;
3852
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
3853
+ return json(res, 200, { ok: true, ...getUsageSummary(tenantId, { from, to }) });
3854
+ }
3855
+
3856
+ if (url.pathname === '/hq/metering/billing') {
3857
+ const { tenantId, tier } = body;
3858
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
3859
+ return json(res, 200, { ok: true, ...getBillingSummary(tenantId, tier || 'starter') });
3860
+ }
3861
+
3862
+ if (url.pathname === '/hq/onboarding/chat') {
3863
+ const { tenantId, message, state: clientState } = body;
3864
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
3865
+ const { createOnboardingSession, loadSession, saveSession, processResponse, getStepPrompt, advanceStep } = await import('./onboarding-engine.mjs');
3866
+ let session = loadSession(tenantId) || createOnboardingSession(tenantId);
3867
+ if (clientState) Object.assign(session.data, clientState);
3868
+ session.history.push({ role: 'user', content: message || '', step: session.step, timestamp: new Date().toISOString() });
3869
+
3870
+ let ariaText = '';
3871
+
3872
+ // Password collection: skip LLM, take user message directly
3873
+ if (session.step === 'credentials' && session.data.email && !session.data.password) {
3874
+ session.data.password = message;
3875
+ session.step = advanceStep(session.step);
3876
+ ariaText = `Great, I have your email as ${session.data.email}. Now please type a password for your dashboard login. Make it at least 8 characters.`;
3877
+ } else {
3878
+ const stepPrompt = getStepPrompt(session.step);
3879
+ try {
3880
+ const providerResult = await callProviderForOpenAI({
3881
+ ...body,
3882
+ messages: [
3883
+ { role: 'system', content: stepPrompt },
3884
+ { role: 'user', content: message || 'Hello' },
3885
+ ],
3886
+ });
3887
+ ariaText = providerResult.text || '';
3888
+ } catch (err) {
3889
+ ariaText = `I'd love to chat more about setting up your AI workforce! Unfortunately, I'm having trouble connecting right now. Could you try again? (Error: ${err.message})`;
3890
+ }
3891
+ session = processResponse(session, ariaText);
3892
+ // After email is extracted, append password prompt
3893
+ if (session.step === 'credentials' && session.data.email && !session.data.password) {
3894
+ ariaText += '\n\nNow please type a password for your dashboard login (at least 8 characters). Your next message will be saved as your password.';
3895
+ }
3896
+ }
3897
+ saveSession(session);
3898
+
3899
+ if (session.step === 'complete' && session.data.confirmed) {
3900
+ const { buildFleetConfig } = await import('./onboarding-engine.mjs');
3901
+ const fleetConfig = buildFleetConfig(session);
3902
+ const enqueueJob = (jobDef) => {
3903
+ const autonomyState = loadAutonomyState();
3904
+ const job = { jobId: randomUUID(), kind: jobDef.kind, surface: 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
3905
+ autonomyState.jobs.push(job);
3906
+ saveAutonomyState(autonomyState);
3907
+ return job;
3908
+ };
3909
+ const { fleet, workerRegistrations, initialJobs } = deployFleet(tenantId, fleetConfig, enqueueJob);
3910
+ const autonomyState = loadAutonomyState();
3911
+ for (const reg of workerRegistrations) { ensureWorker(autonomyState, reg.workerId, reg); }
3912
+ for (const jobDef of initialJobs) {
3913
+ const job = { jobId: randomUUID(), kind: jobDef.kind, surface: 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
3914
+ autonomyState.jobs.push(job);
3915
+ }
3916
+ saveAutonomyState(autonomyState);
3917
+ const apiKey = generateApiKey(tenantId);
3918
+ let authToken = null;
3919
+ if (session.data.email && session.data.password) {
3920
+ const regResult = registerUser(session.data.email, session.data.password, tenantId);
3921
+ if (regResult.ok) {
3922
+ const loginResult = loginUser(session.data.email, session.data.password);
3923
+ authToken = loginResult.ok ? loginResult.session.token : null;
3924
+ }
3925
+ }
3926
+ return json(res, 200, { ok: true, step: 'complete', data: session.data, ariaMessage: ariaText, fleet: { deployed: true, agents: fleet.agents.length }, apiKey, authToken });
3927
+ }
3928
+
3929
+ return json(res, 200, { ok: true, step: session.step, data: session.data, ariaMessage: ariaText });
3930
+ }
3931
+
3932
+ if (url.pathname === '/hq/onboarding/status') {
3933
+ const { tenantId } = body;
3934
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
3935
+ const { loadSession } = await import('./onboarding-engine.mjs');
3936
+ const session = loadSession(tenantId);
3937
+ if (!session) return json(res, 200, { ok: true, step: 'not-started', data: {} });
3938
+ return json(res, 200, { ok: true, step: session.step, data: session.data, updatedAt: session.updatedAt });
3939
+ }
3940
+
3941
+ if (url.pathname === '/hq/fleet/deploy') {
3942
+ const { tenantId, config } = body;
3943
+ if (!tenantId || !config) return json(res, 400, { ok: false, error: 'tenantId and config required' });
3944
+ const enqueueJob = (jobDef) => {
3945
+ const autonomyState = loadAutonomyState();
3946
+ const job = { jobId: randomUUID(), kind: jobDef.kind, surface: jobDef.surface || 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
3947
+ autonomyState.jobs.push(job);
3948
+ saveAutonomyState(autonomyState);
3949
+ return job;
3950
+ };
3951
+ const { fleet, workerRegistrations, initialJobs } = deployFleet(tenantId, config, enqueueJob);
3952
+ const autonomyState = loadAutonomyState();
3953
+ for (const reg of workerRegistrations) {
3954
+ ensureWorker(autonomyState, reg.workerId, reg);
3955
+ }
3956
+ for (const jobDef of initialJobs) {
3957
+ const job = { jobId: randomUUID(), kind: jobDef.kind, surface: jobDef.surface || 'fleet', sessionId: null, payload: jobDef.payload || {}, metadata: jobDef.metadata || {}, priority: jobDef.priority || 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
3958
+ autonomyState.jobs.push(job);
3959
+ }
3960
+ saveAutonomyState(autonomyState);
3961
+ return json(res, 200, { ok: true, fleet, workersRegistered: workerRegistrations.length, jobsEnqueued: initialJobs.length });
3962
+ }
3963
+
3964
+ if (url.pathname === '/hq/fleet/status') {
3965
+ const { tenantId } = body;
3966
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
3967
+ return json(res, 200, getFleetStatus(tenantId));
3968
+ }
3969
+
3970
+ if (url.pathname === '/hq/fleet/agent/chat') {
3971
+ const { tenantId, agentId, message: agentMsg } = body;
3972
+ if (!tenantId || !agentId || !agentMsg) return json(res, 400, { ok: false, error: 'tenantId, agentId, and message required' });
3973
+ const fleet = loadFleet(tenantId);
3974
+ if (!fleet) return json(res, 404, { ok: false, error: 'No fleet deployed' });
3975
+ const agent = (fleet.agents || []).find(a => a.id === agentId || a.templateId === agentId);
3976
+ if (!agent) return json(res, 404, { ok: false, error: `Agent ${agentId} not found` });
3977
+ const systemPrompt = buildAgentSystemPrompt(agent, fleet.config);
3978
+ let reply = '';
3979
+ try {
3980
+ const providerResult = await callProviderForOpenAI({
3981
+ ...body,
3982
+ messages: [
3983
+ { role: 'system', content: systemPrompt },
3984
+ { role: 'user', content: agentMsg },
3985
+ ],
3986
+ metadata: { ...body.metadata, agentId: agent.id, agentName: agent.name, department: agent.department, requestType: 'fleet-chat' },
3987
+ });
3988
+ reply = providerResult.text || '';
3989
+ } catch (err) {
3990
+ reply = `I'm having trouble connecting right now. Please try again. (Error: ${err.message})`;
3991
+ }
3992
+ try {
3993
+ if (providerResult.usage) {
3994
+ recordTokenUsage({
3995
+ tenantId: body.tenantId || 'owner-local',
3996
+ agentId: agent.id,
3997
+ agentName: agent.name,
3998
+ department: agent.department,
3999
+ sessionId: null,
4000
+ provider: 'deepseek',
4001
+ model: body.model || 'deepseek-v4-flash',
4002
+ usage: providerResult.usage,
4003
+ requestType: 'fleet-chat',
4004
+ });
4005
+ }
4006
+ } catch {}
4007
+ agent.lastActivity = new Date().toISOString();
4008
+ agent.stats.messagesSent = (agent.stats.messagesSent || 0) + 1;
4009
+ saveFleet(fleet);
4010
+ return json(res, 200, { ok: true, agentId, agentName: agent.name, department: agent.department, reply, status: agent.status });
4011
+ }
4012
+
4013
+ if (url.pathname === '/hq/fleet/agent/action') {
4014
+ const { tenantId, agentId, action, payload: actionPayload } = body;
4015
+ if (!tenantId || !agentId || !action) return json(res, 400, { ok: false, error: 'tenantId, agentId, and action required' });
4016
+ const fleet = loadFleet(tenantId);
4017
+ if (!fleet) return json(res, 404, { ok: false, error: 'No fleet deployed' });
4018
+ const agent = (fleet.agents || []).find(a => a.id === agentId || a.templateId === agentId);
4019
+ if (!agent) return json(res, 404, { ok: false, error: `Agent ${agentId} not found` });
4020
+ const autonomyState = loadAutonomyState();
4021
+ const job = { jobId: randomUUID(), kind: `fleet:${agent.id}:${action}`, surface: 'fleet', sessionId: null, payload: actionPayload || {}, metadata: { tenantId, agentId: agent.id, agentName: agent.name, department: agent.department, action, requestType: 'fleet-action' }, priority: 100, status: 'queued', attempts: 0, maxAttempts: 3, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), workerId: null, claimedAt: null, claimExpiresAt: null, progress: [], garden: null };
4022
+ autonomyState.jobs.push(job);
4023
+ saveAutonomyState(autonomyState);
4024
+ agent.lastActivity = new Date().toISOString();
4025
+ saveFleet(fleet);
4026
+ return json(res, 200, { ok: true, agentId, action, jobId: job.jobId, result: 'enqueued' });
4027
+ }
4028
+
4029
+ if (url.pathname === '/hq/plugins/list') {
4030
+ const { tenantId } = body;
4031
+ return json(res, 200, { ok: true, plugins: listPlugins(tenantId || 'owner-local') });
4032
+ }
4033
+
4034
+ if (url.pathname === '/hq/plugins/install') {
4035
+ const { tenantId, pluginId, config: pluginConfig } = body;
4036
+ if (!tenantId || !pluginId) return json(res, 400, { ok: false, error: 'tenantId and pluginId required' });
4037
+ const result = installPlugin(tenantId, pluginId, pluginConfig);
4038
+ if (!result.ok) return json(res, 400, result);
4039
+ return json(res, 200, result);
4040
+ }
4041
+
4042
+ if (url.pathname === '/hq/plugins/configure') {
4043
+ const { tenantId, pluginId, config: pluginConfig } = body;
4044
+ if (!tenantId || !pluginId) return json(res, 400, { ok: false, error: 'tenantId and pluginId required' });
4045
+ const result = configurePlugin(tenantId, pluginId, pluginConfig);
4046
+ if (!result.ok) return json(res, result.error.startsWith('No plugins') ? 404 : 400, result);
4047
+ return json(res, 200, result);
4048
+ }
4049
+
4050
+ if (url.pathname === '/hq/workflows/list') {
4051
+ return json(res, 200, { ok: true, workflows: listWorkflowTemplates() });
4052
+ }
4053
+
4054
+ if (url.pathname === '/hq/workflows/configure') {
4055
+ const { tenantId, workflowId, config: workflowConfig } = body;
4056
+ if (!tenantId || !workflowId) return json(res, 400, { ok: false, error: 'tenantId and workflowId required' });
4057
+ const result = configureWorkflow(tenantId, workflowId, workflowConfig);
4058
+ if (!result.ok) return json(res, 400, result);
4059
+ return json(res, 200, result);
4060
+ }
4061
+
4062
+ if (url.pathname === '/hq/workflows/start') {
4063
+ const { tenantId, workflowId, payload } = body;
4064
+ if (!tenantId || !workflowId) return json(res, 400, { ok: false, error: 'tenantId and workflowId required' });
4065
+ const result = startWorkflow(tenantId, workflowId, payload);
4066
+ if (!result.ok) return json(res, 400, result);
4067
+ return json(res, 200, result);
4068
+ }
4069
+
4070
+ if (url.pathname === '/hq/workflows/approve') {
4071
+ const { tenantId, instanceId, approved } = body;
4072
+ if (!tenantId || !instanceId) return json(res, 400, { ok: false, error: 'tenantId and instanceId required' });
4073
+ const result = approveWorkflowStep(tenantId, instanceId, approved !== false);
4074
+ if (!result.ok) return json(res, 400, result);
4075
+ return json(res, 200, result);
4076
+ }
4077
+
4078
+ if (url.pathname === '/hq/workflows/status') {
4079
+ const { tenantId } = body;
4080
+ if (!tenantId) return json(res, 400, { ok: false, error: 'tenantId required' });
4081
+ return json(res, 200, { ok: true, ...getWorkflowStatus(tenantId) });
4082
+ }
4083
+
4084
+ if (url.pathname === '/hq/subscription/tier') {
4085
+ const { tier } = body;
4086
+ return json(res, 200, { ok: true, tier: getSubscriptionTier(tier) });
4087
+ }
4088
+
3006
4089
  return json(res, 404, { ok: false, error: `No route for POST ${url.pathname}` });
3007
4090
  } catch (error) {
3008
4091
  return json(res, 500, {
@@ -3028,6 +4111,15 @@ export async function startRuntimeServer(options = {}) {
3028
4111
  });
3029
4112
  });
3030
4113
  });
4114
+ server.on('upgrade', (req, socket) => {
4115
+ try {
4116
+ handleWebSocketUpgrade(req, socket);
4117
+ } catch {
4118
+ try {
4119
+ socket.destroy();
4120
+ } catch {}
4121
+ }
4122
+ });
3031
4123
 
3032
4124
  await new Promise((resolve, reject) => {
3033
4125
  server.once('error', reject);