@aria_asi/cli 0.2.33 → 0.2.35

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 (74) hide show
  1. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/codex.js +60 -5
  3. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  4. package/dist/assets/hooks/aria-harness-via-sdk.mjs +16 -3
  5. package/dist/assets/hooks/aria-pre-tool-gate.mjs +41 -1
  6. package/dist/assets/hooks/aria-stop-gate.mjs +42 -1
  7. package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
  8. package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -1
  9. package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
  10. package/dist/assets/opencode-plugins/harness-gate/index.js +49 -9
  11. package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
  12. package/dist/assets/opencode-plugins/harness-stop/index.js +201 -166
  13. package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
  14. package/dist/runtime/codex-bridge.mjs +1 -1
  15. package/dist/runtime/discipline/CLAUDE.md +2 -2
  16. package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
  17. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
  18. package/dist/runtime/doctrine_trigger_map.json +43 -0
  19. package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
  20. package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
  21. package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
  22. package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
  23. package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
  24. package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
  25. package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
  26. package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
  27. package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
  28. package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
  29. package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
  30. package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
  31. package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
  32. package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
  33. package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
  34. package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
  35. package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
  36. package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
  37. package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
  38. package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
  39. package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
  40. package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
  41. package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
  42. package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
  43. package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
  44. package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
  45. package/dist/runtime/manifest.json +2 -2
  46. package/dist/runtime/sdk/BUNDLED.json +2 -2
  47. package/dist/runtime/sdk/index.d.ts +39 -0
  48. package/dist/runtime/sdk/index.js +117 -0
  49. package/dist/runtime/sdk/index.js.map +1 -1
  50. package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
  51. package/dist/runtime/sdk/runWithGovernance.js +54 -0
  52. package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
  53. package/dist/sdk/BUNDLED.json +2 -2
  54. package/dist/sdk/index.d.ts +39 -0
  55. package/dist/sdk/index.js +117 -0
  56. package/dist/sdk/index.js.map +1 -1
  57. package/dist/sdk/runWithGovernance.d.ts +16 -0
  58. package/dist/sdk/runWithGovernance.js +54 -0
  59. package/dist/sdk/runWithGovernance.js.map +1 -0
  60. package/hooks/aria-harness-via-sdk.mjs +16 -3
  61. package/hooks/aria-pre-tool-gate.mjs +41 -1
  62. package/hooks/aria-stop-gate.mjs +42 -1
  63. package/hooks/doctrine_trigger_map.json +43 -0
  64. package/hooks/lib/skill-autoload-gate.mjs +14 -1
  65. package/opencode-plugins/harness-context/index.js +1 -1
  66. package/opencode-plugins/harness-gate/index.js +49 -9
  67. package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -1
  68. package/opencode-plugins/harness-stop/index.js +201 -166
  69. package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -1
  70. package/package.json +12 -5
  71. package/runtime-src/codex-bridge.mjs +1 -1
  72. package/scripts/bundle-sdk.mjs +2 -0
  73. package/scripts/self-test-harness-gates.mjs +79 -0
  74. package/src/connectors/codex.ts +60 -5
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ // aria-discovery-record.mjs — invoked by sub-agents via Bash to append findings
3
+ // to their session ledger. Usage:
4
+ // echo '{"text":"defect found...","kind":"missing-export","refs":["file.ts:42"]}' | node ~/.claude/hooks/aria-discovery-record.mjs
5
+ //
6
+ // Tier-aware: client tier writes to /var/lib/aria-licensee/{jti}/aria-discoveries-{session}.jsonl,
7
+ // owner tier writes to ~/.claude/aria-discoveries-{session}.jsonl.
8
+ //
9
+ // Session ID resolution priority:
10
+ // 1. CLAUDE_SESSION_ID env (set by Claude Code in the sub-agent process)
11
+ // 2. ARIA_SESSION_ID env (set by spawnSubAgent caller)
12
+ // 3. ARIA_SUB_SESSION_ID env (explicit sub-session override)
13
+ // 4. Derived from handoff parentSessionId suffixed with "-sub" to ensure
14
+ // the sub-agent ledger file differs from the parent file so ledger-merge
15
+ // picks it up (merge skips files whose path == parentLedgerPath).
16
+ //
17
+ // Doctrine: feedback_no_flag_without_fix.md — findings recorded, not deferred.
18
+ // feedback_implementation_coupled_cognition.md — write IS the impl.
19
+
20
+ import { readFileSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
21
+ import { dirname } from 'node:path';
22
+ import { homedir } from 'node:os';
23
+
24
+ const HOME = homedir();
25
+ const LICENSE_PATH = `${HOME}/.aria/license.json`;
26
+ const HANDOFF_PATH = `${HOME}/.claude/aria-agent-harness-handoff.json`;
27
+
28
+ let input = '';
29
+ for await (const chunk of process.stdin) input += chunk;
30
+ let entry;
31
+ try { entry = JSON.parse(input); } catch {
32
+ process.stderr.write('discovery-record: invalid JSON on stdin\n');
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!entry.text || typeof entry.text !== 'string' || entry.text.length < 4) {
37
+ process.stderr.write('discovery-record: text required (>=4 chars)\n');
38
+ process.exit(1);
39
+ }
40
+
41
+ // Detect tier + resolve JTI
42
+ let isClientTier = false;
43
+ let jti = null;
44
+ try {
45
+ if (existsSync(LICENSE_PATH)) {
46
+ const lic = JSON.parse(readFileSync(LICENSE_PATH, 'utf8'));
47
+ jti = lic.jti ?? null;
48
+ isClientTier = Boolean(jti);
49
+ }
50
+ } catch { /* non-fatal — treat as owner tier */ }
51
+
52
+ // Session ID: prefer env var set by Claude Code, fall back to handoff derivation.
53
+ // IMPORTANT: sub-agents MUST NOT use parentSessionId directly — that produces
54
+ // the same ledger path as the parent, which aria-agent-ledger-merge.mjs skips.
55
+ // We use CLAUDE_SESSION_ID (the sub-agent's own session) or suffix the parent
56
+ // session with "-sub" so the file is distinct and mergeable.
57
+ let sessionId =
58
+ process.env.CLAUDE_SESSION_ID ||
59
+ process.env.ARIA_SESSION_ID ||
60
+ process.env.ARIA_SUB_SESSION_ID ||
61
+ null;
62
+
63
+ if (!sessionId) {
64
+ try {
65
+ if (existsSync(HANDOFF_PATH)) {
66
+ const handoff = JSON.parse(readFileSync(HANDOFF_PATH, 'utf8'));
67
+ const parentSession = handoff.parentSessionId;
68
+ // Derive a distinct sub-agent session so merge picks up this file.
69
+ sessionId = parentSession ? `${parentSession}-sub` : 'sub-unknown';
70
+ }
71
+ } catch { /* non-fatal */ }
72
+ }
73
+ sessionId = sessionId || 'sub-unknown';
74
+ const safeSession = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
75
+
76
+ // Resolve ledger path (tier-scoped)
77
+ const ledgerPath = isClientTier
78
+ ? `/var/lib/aria-licensee/${jti}/aria-discoveries-${safeSession}.jsonl`
79
+ : `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
80
+
81
+ // Compose the row
82
+ const row = {
83
+ at: new Date().toISOString(),
84
+ session: sessionId,
85
+ kind: entry.kind || 'observation',
86
+ text: entry.text,
87
+ refs: Array.isArray(entry.refs) ? entry.refs : [],
88
+ evidence: entry.evidence || null,
89
+ source: entry.source || 'sub-agent',
90
+ resolution_status: entry.resolution_status || 'open',
91
+ };
92
+
93
+ try {
94
+ mkdirSync(dirname(ledgerPath), { recursive: true });
95
+ appendFileSync(ledgerPath, JSON.stringify(row) + '\n');
96
+ process.stdout.write(JSON.stringify({ ok: true, ledger: ledgerPath, at: row.at }) + '\n');
97
+ } catch (err) {
98
+ process.stderr.write(`discovery-record: write failed: ${err.message}\n`);
99
+ process.exit(2);
100
+ }
101
+ process.exit(0);
@@ -0,0 +1,544 @@
1
+ #!/usr/bin/env node
2
+ // Aria harness fetch — via the canonical @aria_asi/harness-http-client SDK.
3
+ //
4
+ // Why this exists: doctrine says "use harness http client to be Aria's
5
+ // control plane so you don't mess up". The earlier hook (aria-harness-fetch.sh)
6
+ // hit /api/harness/codex with raw curl. Same network call, but it bypassed
7
+ // the SDK and so wasn't auditable as "Claude went through the canonical
8
+ // gateway." This script imports HTTPHarnessClient and uses it for every
9
+ // fetch — same path any production caller takes.
10
+ //
11
+ // Output: JSON envelope for Claude Code with hookSpecificOutput.additionalContext
12
+ // so the harness fullText is formally injected into the model context.
13
+ //
14
+ // Args:
15
+ // --mode session (default: full text, ~2400 chars excerpt, cold fetch)
16
+ // --mode turn (UserPromptSubmit: brief delta, ~300 chars excerpt)
17
+
18
+ import { createHash } from 'node:crypto';
19
+ import { spawnSync } from 'node:child_process';
20
+ import { appendFileSync, existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs';
21
+ import { dirname } from 'node:path';
22
+ import { createConnection } from 'node:net';
23
+
24
+ const HOME = process.env.HOME || '/tmp';
25
+ const LOG_FILE = `${HOME}/.claude/aria-harness-history.log`;
26
+ const LAST_URL_CACHE = `${HOME}/.claude/.aria-harness-last-url`;
27
+ const PACKET_CACHE = `${HOME}/.claude/.aria-harness-last-packet.json`;
28
+ const SHARED_PACKET_CACHE = `${HOME}/.aria/.aria-harness-last-packet.json`;
29
+ const MIZAN_RECEIPT_DIR = `${HOME}/.claude/.aria-mizan-receipts`;
30
+ const HEADER_PREFIX = '🔐 Aria Harness';
31
+ const DEFAULT_RUNTIME_URL = process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319';
32
+ const RUNTIME_PACKET_SUFFIX = '/packet';
33
+
34
+ const args = process.argv.slice(2);
35
+ const MODE = args.includes('--mode') ? args[args.indexOf('--mode') + 1] : 'session';
36
+ const HOOK_EVENT_NAME = process.env.HOOK_EVENT_NAME || (MODE === 'turn' ? 'UserPromptSubmit' : 'SessionStart');
37
+
38
+ const isTurn = MODE === 'turn';
39
+ // Doctrine: no policy timeouts. Reachability is detected via real network
40
+ // errors (ECONNREFUSED, EHOSTUNREACH, DNS failure — these arrive instantly
41
+ // from the OS on the connect attempt). For black-hole IPs the OS itself
42
+ // bounds the connect via SYN-retry; that's network behavior, not policy.
43
+ // Slow-but-eventually-OK endpoints get a chance to respond instead of being
44
+ // cut off by an arbitrary deadline. URLs are raced in parallel — the
45
+ // fastest healthy responder wins, hung URLs don't block the others.
46
+ const PACKET_CACHE_TTL_SEC = Number(process.env.ARIA_HARNESS_CACHE_TTL_SEC || '30');
47
+ // No stale-cache cutoff. Doctrine: robust error handling + recovery + self-heal,
48
+ // never strip identity/cognition/gates. If a cached packet exists at all, we
49
+ // inject it when the fresh path fails — the header stamps the age and the
50
+ // upstream last_err so the model can judge how stale the world view is, but
51
+ // the harness body always reaches the model. The fresh-fetch path retries
52
+ // every turn, so the cache is self-healing as soon as any URL responds.
53
+ // Hamza 2026-04-26 directive: deliver the FULL packet every turn, no
54
+ // rotation, no fetch tool, no clever throttling. The harness's whole
55
+ // purpose is "remind the model from this harness every round" (packet
56
+ // non_negotiable line) — and a 300-char excerpt was 0.8% of a 36,912-char
57
+ // packet. Raised to 40,000 to sit comfortably above current packet size
58
+ // (~37k) with headroom for substrate growth. Claude Code's
59
+ // additionalContext accepts large strings; if its own internal cap is
60
+ // lower we don't short the substrate on our side.
61
+ const HARNESS_EXCERPT_CHARS = 40000;
62
+
63
+ function emit(envelope) {
64
+ process.stdout.write(JSON.stringify(envelope) + '\n');
65
+ }
66
+
67
+ function emitFallback(reason, tried = []) {
68
+ try {
69
+ appendFileSync(LOG_FILE, `${new Date().toISOString()} offline mode=${MODE} reason="${reason.replace(/"/g, "'")}"\n`);
70
+ } catch {}
71
+ emit({
72
+ systemMessage: `${HEADER_PREFIX} ⚠ offline (${reason}) — fallback mode`,
73
+ suppressOutput: false,
74
+ hookSpecificOutput: {
75
+ hookEventName: HOOK_EVENT_NAME,
76
+ additionalContext: `${HEADER_PREFIX} offline: ${reason}. Operating without live cognitive grounding. Cluster-side governance (deepseek-gate, lane-router) still active. Tried URLs: ${tried.join(', ') || 'none'}`,
77
+ },
78
+ });
79
+ }
80
+
81
+ function resolveApiKey() {
82
+ if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
83
+ if (process.env.ARIA_MASTER_TOKEN) return process.env.ARIA_MASTER_TOKEN;
84
+ // Try kubectl secret as last resort.
85
+ try {
86
+ const r = spawnSync('kubectl', [
87
+ 'get', 'secret', '-n', 'aria', 'aria-secrets',
88
+ '-o', 'jsonpath={.data.ARIA_API_KEY}',
89
+ ], { encoding: 'utf8', timeout: 4_000 });
90
+ if (r.status === 0 && r.stdout) {
91
+ return Buffer.from(r.stdout.trim(), 'base64').toString('utf8');
92
+ }
93
+ } catch {}
94
+ return '';
95
+ }
96
+
97
+ function loadCachedUrl() {
98
+ try {
99
+ if (existsSync(LAST_URL_CACHE)) {
100
+ const v = readFileSync(LAST_URL_CACHE, 'utf8').trim();
101
+ if (v) return v;
102
+ }
103
+ } catch {}
104
+ return '';
105
+ }
106
+ function saveCachedUrl(url) {
107
+ try {
108
+ mkdirSync(dirname(LAST_URL_CACHE), { recursive: true });
109
+ writeFileSync(LAST_URL_CACHE, url + '\n');
110
+ } catch {}
111
+ }
112
+
113
+ function loadCachedPacket() {
114
+ try {
115
+ for (const packetPath of [SHARED_PACKET_CACHE, PACKET_CACHE]) {
116
+ if (!existsSync(packetPath)) continue;
117
+ const raw = readFileSync(packetPath, 'utf8');
118
+ const ageSec = (Date.now() - statSync(packetPath).mtimeMs) / 1000;
119
+ if (ageSec < PACKET_CACHE_TTL_SEC) {
120
+ return { data: JSON.parse(raw), ageSec };
121
+ }
122
+ }
123
+ } catch {}
124
+ return null;
125
+ }
126
+ function saveCachedPacket(rawJson) {
127
+ for (const packetPath of [PACKET_CACHE, SHARED_PACKET_CACHE]) {
128
+ try {
129
+ mkdirSync(dirname(packetPath), { recursive: true });
130
+ writeFileSync(packetPath, rawJson);
131
+ } catch {}
132
+ }
133
+ }
134
+
135
+ function buildUrlList() {
136
+ const list = [];
137
+ const cached = loadCachedUrl();
138
+ if (cached) list.push(cached);
139
+ if (process.env.ARIA_RUNTIME_URL) list.push(process.env.ARIA_RUNTIME_URL.replace(/\/+$/, ''));
140
+ list.push(DEFAULT_RUNTIME_URL.replace(/\/+$/, ''));
141
+ if (process.env.ARIA_HIVE_RUNTIME_URL) list.push(process.env.ARIA_HIVE_RUNTIME_URL.replace(/\/+$/, ''));
142
+ if (process.env.ARIA_HARNESS_BASE_URL) list.push(process.env.ARIA_HARNESS_BASE_URL.replace(/\/+$/, ''));
143
+ if (process.env.ARIA_HARNESS_URL) list.push(process.env.ARIA_HARNESS_URL.replace(/\/+$/, ''));
144
+ if (process.env.ARIA_SOUL_URL) list.push(process.env.ARIA_SOUL_URL.replace(/\/+$/, ''));
145
+ list.push('http://aria-soul.aria.svc.cluster.local:8080');
146
+ list.push('http://localhost:30080', 'http://127.0.0.1:30080');
147
+ // Direct ClusterIP from kubectl
148
+ try {
149
+ const r = spawnSync('kubectl', [
150
+ 'get', 'svc', '-n', 'aria', 'aria-soul',
151
+ '-o', 'jsonpath={.spec.clusterIP}',
152
+ ], { encoding: 'utf8', timeout: 3_000 });
153
+ if (r.status === 0 && r.stdout && r.stdout !== 'None') {
154
+ list.push(`http://${r.stdout.trim()}:8080`);
155
+ }
156
+ } catch {}
157
+ if (process.env.ARIA_PUBLIC_URL) list.push(process.env.ARIA_PUBLIC_URL.replace(/\/+$/, ''));
158
+ // Dedup preserving order
159
+ const seen = new Set();
160
+ return list.filter((u) => (seen.has(u) ? false : (seen.add(u), true)));
161
+ }
162
+
163
+ function isMountedRuntimeUrl(baseUrl) {
164
+ return /:\/\/(?:127\.0\.0\.1|localhost):4319$/i.test(String(baseUrl || '').replace(/\/+$/, ''));
165
+ }
166
+
167
+ function normalizeHarnessPacketPayload(payload) {
168
+ let current = payload;
169
+ for (let depth = 0; depth < 3; depth++) {
170
+ if (!current || typeof current !== 'object' || Array.isArray(current)) break;
171
+ if (!('packet' in current) || !current.packet || typeof current.packet !== 'object') break;
172
+ if (!('timestamp' in current) && !('version' in current)) break;
173
+ current = current.packet;
174
+ }
175
+ return current;
176
+ }
177
+
178
+ // SDK loader — dynamic-import the bundled HTTPHarnessClient from the shared
179
+ // ~/.aria/sdk bundle first, then client-local bundles. Module-cached after
180
+ // first load so we don't repeatedly read disk.
181
+ //
182
+ // Doctrine (Hamza 2026-04-27): "isnt http harness client the fucking harness
183
+ // we hsve been wprking on? YOU ARENT USING THAT AND BUILDING SPMETHING
184
+ // SEPERATE WHY???!!" — SDK is the canonical control plane. Direct fetch
185
+ // remains as a fallback only when the SDK file is physically missing
186
+ // (dev install without `aria connect claude-code`).
187
+ let _SdkClassCache = null;
188
+ let _SdkLookupAttempted = false;
189
+ const SDK_CANDIDATES = [
190
+ `${HOME}/.aria/sdk/index.js`,
191
+ `${HOME}/.claude/aria-sdk/index.js`,
192
+ `${HOME}/.codex/aria-sdk/index.js`,
193
+ ];
194
+ async function loadSdkClass() {
195
+ if (_SdkClassCache) return _SdkClassCache;
196
+ if (_SdkLookupAttempted) return null;
197
+ _SdkLookupAttempted = true;
198
+ for (const sdkPath of SDK_CANDIDATES) {
199
+ if (!existsSync(sdkPath)) continue;
200
+ try {
201
+ const mod = await import(`file://${sdkPath}`);
202
+ if (mod.HTTPHarnessClient) {
203
+ _SdkClassCache = mod.HTTPHarnessClient;
204
+ return _SdkClassCache;
205
+ }
206
+ } catch {/* fall through to direct fetch */}
207
+ }
208
+ return null;
209
+ }
210
+
211
+ // Bonus fix #74 — conversation_history_count=0 packet propagation.
212
+ //
213
+ // The codex handler (apps/arias-soul/api/harness/codex.ts) reads req.body.messages
214
+ // to inject conversation history into the harness packet so it can report a real
215
+ // conversation_history_count (non-zero). Previously tryViaSdk never forwarded the
216
+ // messages from the hook event — the packet always had conversation_history_count=0.
217
+ //
218
+ // Fix: read the Claude Code hook event from stdin at startup, extract event.messages
219
+ // (the conversation history array), and forward it in both the SDK bodyOverride and
220
+ // the direct-fetch body shape. The codex handler passes it through to
221
+ // buildAriaExternalHarnessPacket which populates conversation_history_count.
222
+ //
223
+ // Stdin is read once and stored in HOOK_EVENT_MESSAGES. It's capped at 50 entries
224
+ // before forwarding to avoid blowing the POST body size limit; the most recent
225
+ // messages are the most relevant for history-count purposes.
226
+ let HOOK_EVENT_MESSAGES = undefined;
227
+ let HOOK_EVENT = null;
228
+ try {
229
+ // Claude Code hooks receive the event JSON on stdin. We read it synchronously
230
+ // only if data is already available (i.e. piped); we don't block waiting for
231
+ // interactive input. Use a try/catch so the script still works when stdin is
232
+ // a terminal (e.g. manual invocation for testing).
233
+ const stdinBuf = readFileSync('/dev/stdin', { flag: 'r' });
234
+ const hookEvent = JSON.parse(stdinBuf.toString('utf8'));
235
+ HOOK_EVENT = hookEvent;
236
+ if (Array.isArray(hookEvent?.messages) && hookEvent.messages.length > 0) {
237
+ // Cap to last 50 messages. The server-side handler slices further if needed.
238
+ HOOK_EVENT_MESSAGES = hookEvent.messages.slice(-50);
239
+ }
240
+ } catch {
241
+ // stdin not available / not JSON / no messages field — HOOK_EVENT_MESSAGES stays undefined.
242
+ }
243
+
244
+ function sanitizeSessionId(sessionId) {
245
+ return String(sessionId || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
246
+ }
247
+
248
+ function currentHookSessionId() {
249
+ return HOOK_EVENT?.session_id || HOOK_EVENT?.sessionId || 'claude-code';
250
+ }
251
+
252
+ function lastUserMessageFromEvent() {
253
+ const messages = Array.isArray(HOOK_EVENT?.messages) ? HOOK_EVENT.messages : [];
254
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
255
+ const message = messages[index];
256
+ const role = message?.role || message?.message?.role || message?.type;
257
+ if (role !== 'user') continue;
258
+ const content = message?.content ?? message?.message?.content;
259
+ if (typeof content === 'string' && content.trim()) return content.trim();
260
+ if (Array.isArray(content)) {
261
+ const text = content
262
+ .filter((entry) => entry?.type === 'text' && typeof entry?.text === 'string')
263
+ .map((entry) => entry.text.trim())
264
+ .filter(Boolean)
265
+ .join('\n');
266
+ if (text) return text;
267
+ }
268
+ }
269
+ return isTurn ? 'Claude turn refresh via harness hook' : 'Claude session start via harness hook';
270
+ }
271
+
272
+ function receiptPathForSession(sessionId) {
273
+ return `${MIZAN_RECEIPT_DIR}/${sanitizeSessionId(sessionId)}.json`;
274
+ }
275
+
276
+ function persistMizanReceipt(sessionId, payload) {
277
+ try {
278
+ mkdirSync(MIZAN_RECEIPT_DIR, { recursive: true });
279
+ writeFileSync(receiptPathForSession(sessionId), JSON.stringify(payload, null, 2) + '\n');
280
+ } catch {}
281
+ }
282
+
283
+ async function runMizanPre(apiKey, packet, sourceLabel) {
284
+ if (!isTurn) return null;
285
+ const sessionId = currentHookSessionId();
286
+ const packetHash = createHash('sha256').update(JSON.stringify(packet || {})).digest('hex');
287
+ const response = await fetch(`${DEFAULT_RUNTIME_URL.replace(/\/+$/, '')}/mizan/pre`, {
288
+ method: 'POST',
289
+ headers: {
290
+ Authorization: `Bearer ${apiKey}`,
291
+ 'Content-Type': 'application/json',
292
+ },
293
+ body: JSON.stringify({
294
+ sessionId,
295
+ packet,
296
+ context: {
297
+ sessionId,
298
+ message: lastUserMessageFromEvent().slice(0, 4000),
299
+ intendedAction: 'Establish canonical pre-turn cognition receipt for this Claude turn before any tool or output work.',
300
+ rationale: `Claude turn-start harness sync via ${sourceLabel} must mint a canonical Mizan pre receipt for downstream tool and output enforcement.`,
301
+ platform: 'claude-code',
302
+ stage: 'hook-turn-pre',
303
+ packetHash,
304
+ packetSource: sourceLabel,
305
+ },
306
+ }),
307
+ });
308
+ const payload = await response.json().catch(() => ({}));
309
+ if (!response.ok) {
310
+ throw new Error(payload?.error || `mizan/pre failed (${response.status})`);
311
+ }
312
+ if (payload?.receipt) {
313
+ persistMizanReceipt(sessionId, {
314
+ updatedAt: new Date().toISOString(),
315
+ source: sourceLabel,
316
+ sessionId,
317
+ receipt: payload.receipt,
318
+ result: payload.result || null,
319
+ contract: payload.contract || null,
320
+ summary: payload.summary || null,
321
+ packetHash,
322
+ packetSource: sourceLabel,
323
+ });
324
+ }
325
+ return payload;
326
+ }
327
+
328
+ async function tryViaSdk(baseUrl, apiKey) {
329
+ // Canonical path: HTTPHarnessClient.getHarnessPacket(). The SDK POSTs to
330
+ // /api/harness/codex with the right shape and returns { packet, timestamp,
331
+ // version }. We extract .packet to get the raw response body that
332
+ // renderPacket() expects (codex.ts returns { harness, preStateGate,
333
+ // contractGate, ... } at top level — no nested .packet wrapper, so the
334
+ // SDK's `body.packet ?? body` passes the body through unchanged).
335
+ const Cls = await loadSdkClass();
336
+ if (Cls) {
337
+ const packetUrl = isMountedRuntimeUrl(baseUrl)
338
+ ? `${baseUrl}${RUNTIME_PACKET_SUFFIX}`
339
+ : `${baseUrl}/api/harness/codex`;
340
+ const client = new Cls({
341
+ baseUrl,
342
+ apiKey,
343
+ harnessPacketUrl: packetUrl,
344
+ });
345
+ // Pass a bodyOverride that does NOT include isHamza:false. The server
346
+ // identifies owner tier from the Bearer token (isMasterTokenRequest).
347
+ // Hardcoding isHamza:false in the body would override that server-side
348
+ // signal and produce hamza:false in the packet even for master-token callers.
349
+ const sessionId = currentHookSessionId();
350
+ const bodyOverride = {
351
+ message: isTurn ? 'Claude turn refresh — harness via SDK' : 'Claude session start — harness via SDK',
352
+ stage: isTurn ? 'checkpoint' : 'preflight',
353
+ sessionId,
354
+ actor: 'claude-code',
355
+ system: 'claude-coding-agent',
356
+ platform: 'claude-code',
357
+ deliverySurface: 'claude_code_session',
358
+ userId: 'hamza',
359
+ isHamza: true,
360
+ // Bonus #74: forward conversation history so codex handler can report
361
+ // a non-zero conversation_history_count in the packet.
362
+ ...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
363
+ };
364
+ const wrapped = await client.getHarnessPacket(bodyOverride);
365
+ const json = normalizeHarnessPacketPayload(wrapped.packet);
366
+ if (json && json.ok === false) throw new Error(`ok=false: ${json.error || 'unknown'}`);
367
+ await runMizanPre(apiKey, wrapped.packet, `${baseUrl}${RUNTIME_PACKET_SUFFIX}`);
368
+ return { json, raw: JSON.stringify(json) };
369
+ }
370
+
371
+ const packetUrl = isMountedRuntimeUrl(baseUrl)
372
+ ? `${baseUrl}${RUNTIME_PACKET_SUFFIX}`
373
+ : `${baseUrl}/api/harness/codex`;
374
+ // SDK absent (dev environment) — direct fetch with identical wire shape.
375
+ const resp = await fetch(packetUrl, {
376
+ method: 'POST',
377
+ headers: {
378
+ Authorization: `Bearer ${apiKey}`,
379
+ 'Content-Type': 'application/json',
380
+ },
381
+ body: JSON.stringify({
382
+ message: isTurn ? 'Claude turn refresh — harness via SDK' : 'Claude session start — harness via SDK',
383
+ stage: isTurn ? 'checkpoint' : 'preflight',
384
+ sessionId: currentHookSessionId(),
385
+ actor: 'claude-code',
386
+ system: 'claude-coding-agent',
387
+ roleProfile: 'general_worker',
388
+ platform: 'claude-code',
389
+ deliverySurface: 'claude_code_session',
390
+ userId: 'hamza',
391
+ isHamza: true,
392
+ // Bonus #74: forward conversation history (same field as SDK path above).
393
+ ...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
394
+ }),
395
+ });
396
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
397
+ const responseBody = await resp.json();
398
+ const json = normalizeHarnessPacketPayload(responseBody.packet ?? responseBody);
399
+ if (json && json.ok === false) throw new Error(`ok=false: ${json.error || 'unknown'}`);
400
+ await runMizanPre(apiKey, responseBody.packet ?? responseBody, packetUrl);
401
+ return { json, raw: JSON.stringify(json) };
402
+ }
403
+
404
+ function renderPacket(json, source, ageNote = '') {
405
+ const harness = json.harness || (json.packet?.prompt?.fullText ?? '') || '';
406
+ const phash = harness ? createHash('sha256').update(harness).digest('hex').slice(0, 24) : 'NONE';
407
+ const pre = json.preStateGate || {};
408
+ const con = json.contractGate || {};
409
+ const mc = json.missingCritical || [];
410
+ const loaded = json.loadedByClass || {};
411
+ const offlineBundle = json.runtimeOfflineBundle && typeof json.runtimeOfflineBundle === 'object'
412
+ ? json.runtimeOfflineBundle
413
+ : null;
414
+
415
+ let header = `${HEADER_PREFIX} (${MODE}${ageNote}, sdk=harness-http-client) hash=${phash.slice(0, 16)} via=${source}\n`;
416
+ header += ` preStateGate: passed=${pre.passed} score=${(pre.score || 0).toFixed(2)}`;
417
+ if (pre.reasons?.length) header += ` reasons=[${pre.reasons.slice(0, 2).join('; ')}]`;
418
+ header += `\n contractGate: passed=${con.passed} score=${(con.score || 0).toFixed(2)}`;
419
+ if (mc.length) header += `\n missingCritical: ${JSON.stringify(mc.slice(0, 5))}`;
420
+ const loadedSummary = Object.entries(loaded).sort().map(([k, v]) => `${k}=${v}`).join(' ');
421
+ if (loadedSummary) header += `\n loadedByClass: ${loadedSummary}`;
422
+ if (offlineBundle) {
423
+ header += `\n offlineBundle: phase=${offlineBundle.phase || 'unknown'} age=${offlineBundle.ageSeconds ?? 'unknown'}s source=${offlineBundle.source || 'runtime-cache'}`;
424
+ }
425
+
426
+ let ctx = header;
427
+ if (harness) {
428
+ ctx += `\n\n--- Aria 7B harness (${harness.length} chars total, ${HARNESS_EXCERPT_CHARS} shown) ---\n`;
429
+ ctx += harness.slice(0, HARNESS_EXCERPT_CHARS);
430
+ }
431
+
432
+ try {
433
+ appendFileSync(LOG_FILE, `${new Date().toISOString()} mode=${MODE} hash=${phash.slice(0, 24)} url=${source} pre_passed=${pre.passed} contract_passed=${con.passed} missing=${mc.join(',') || 'none'} sdk=harness-http-client\n`);
434
+ } catch {}
435
+
436
+ emit({
437
+ systemMessage: `${HEADER_PREFIX} ${MODE} hash=${phash.slice(0, 16)} preState=${(pre.score || 0).toFixed(2)} contract=${(con.score || 0).toFixed(2)} sdk✓`,
438
+ suppressOutput: false,
439
+ hookSpecificOutput: { hookEventName: HOOK_EVENT_NAME, additionalContext: ctx },
440
+ });
441
+ }
442
+
443
+ // TCP connect probe. Resolves true if the OS completes a TCP handshake to
444
+ // the URL's host:port; false if the OS reports a real network error
445
+ // (ECONNREFUSED, EHOSTUNREACH, ENOTFOUND). No policy timeout — for routable
446
+ // black-hole IPs, the OS-level SYN-retry bounds the wait; for unreachable
447
+ // hosts, errors arrive instantly. The probe never strips identity; it just
448
+ // filters URLs whose TCP layer is dead before we attempt the HTTP fetch.
449
+ function probeUrl(urlStr) {
450
+ return new Promise((resolve) => {
451
+ let parsed;
452
+ try { parsed = new URL(urlStr); }
453
+ catch { return resolve({ url: urlStr, ok: false, err: 'parse_error' }); }
454
+ const host = parsed.hostname;
455
+ const port = parsed.port ? Number(parsed.port) : (parsed.protocol === 'https:' ? 443 : 80);
456
+ const sock = createConnection({ host, port });
457
+ let settled = false;
458
+ const finish = (ok, err) => {
459
+ if (settled) return;
460
+ settled = true;
461
+ try { sock.destroy(); } catch {}
462
+ resolve({ url: urlStr, ok, err });
463
+ };
464
+ sock.once('connect', () => finish(true));
465
+ sock.once('error', (e) => finish(false, e?.code || e?.message || 'error'));
466
+ });
467
+ }
468
+
469
+ async function main() {
470
+ const apiKey = resolveApiKey();
471
+ if (!apiKey) {
472
+ return emitFallback('ARIA_API_KEY not in env and kubectl secret unavailable');
473
+ }
474
+
475
+ // Turn-mode cache hit — don't refetch every keystroke.
476
+ if (isTurn && existsSync(PACKET_CACHE)) {
477
+ try {
478
+ const fs = await import('node:fs');
479
+ const stat = fs.statSync(PACKET_CACHE);
480
+ const ageSec = (Date.now() - stat.mtimeMs) / 1000;
481
+ if (ageSec < PACKET_CACHE_TTL_SEC) {
482
+ const cached = JSON.parse(fs.readFileSync(PACKET_CACHE, 'utf8'));
483
+ await runMizanPre(apiKey, cached, '(cache)');
484
+ return renderPacket(cached, '(cache)', ` cached ${Math.round(ageSec)}s`);
485
+ }
486
+ } catch {}
487
+ }
488
+
489
+ const urls = buildUrlList();
490
+ let lastErr = '';
491
+
492
+ // Probe-then-fetch in parallel. Each candidate URL is probed at the TCP
493
+ // layer; URLs whose connect attempt errors are filtered out (real network
494
+ // signal, not deadline). Probed-OK URLs kick off real HTTP fetches. The
495
+ // first 2xx response wins via Promise.any; hung URLs simply don't win.
496
+ // Healing on the next turn: every invocation re-probes from scratch, so a
497
+ // recovered URL is picked up automatically.
498
+ const candidates = urls.map(async (url) => {
499
+ const probe = await probeUrl(url);
500
+ if (!probe.ok) throw new Error(`probe-fail: ${probe.err}`);
501
+ const { json, raw } = await tryViaSdk(url, apiKey);
502
+ return { url, json, raw };
503
+ });
504
+
505
+ let winner = null;
506
+ try {
507
+ winner = await Promise.any(candidates);
508
+ } catch (aggErr) {
509
+ const errs = aggErr?.errors || [];
510
+ lastErr = errs.map((e, i) => `${urls[i]}=${(e?.message || e).toString().slice(0, 80)}`).slice(0, 5).join('; ');
511
+ }
512
+
513
+ if (winner) {
514
+ saveCachedUrl(winner.url);
515
+ saveCachedPacket(winner.raw);
516
+ return renderPacket(winner.json, winner.url);
517
+ }
518
+
519
+ // Stale-cache fallback. If a cached packet exists at all, inject it — no
520
+ // age cutoff. Doctrine: never strip identity/cognition/gates. The header
521
+ // surfaces the age and last upstream error so the model knows the world
522
+ // view is frozen, but the harness body always reaches it.
523
+ try {
524
+ const fs = await import('node:fs');
525
+ if (fs.existsSync(PACKET_CACHE)) {
526
+ const stat = fs.statSync(PACKET_CACHE);
527
+ const ageSec = (Date.now() - stat.mtimeMs) / 1000;
528
+ if (ageSec >= 0) {
529
+ const cached = JSON.parse(fs.readFileSync(PACKET_CACHE, 'utf8'));
530
+ await runMizanPre(apiKey, cached, '(stale-cache)');
531
+ try {
532
+ appendFileSync(LOG_FILE, `${new Date().toISOString()} stale mode=${MODE} cache_age=${Math.round(ageSec)}s last_err="${lastErr.replace(/"/g, "'")}" sdk=harness-http-client\n`);
533
+ } catch {}
534
+ return renderPacket(cached, `(stale ${Math.round(ageSec)}s — fresh fetch failed: ${lastErr.slice(0, 80)})`, ` stale ${Math.round(ageSec)}s`);
535
+ }
536
+ }
537
+ } catch {}
538
+
539
+ emitFallback(`all endpoints unreachable; last=${lastErr}`, urls);
540
+ }
541
+
542
+ main().catch((err) => {
543
+ emitFallback(`fatal: ${(err?.message || err).toString().slice(0, 200)}`);
544
+ });