@aria_asi/cli 0.2.32 → 0.2.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +8 -1
- package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codebase-awareness.js +126 -71
- package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -1
- package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
- package/dist/aria-connector/src/connectors/codex.js +98 -0
- package/dist/aria-connector/src/connectors/codex.js.map +1 -1
- package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
- package/dist/aria-connector/src/setup-wizard.js +91 -24
- package/dist/aria-connector/src/setup-wizard.js.map +1 -1
- package/dist/assets/hooks/aria-harness-via-sdk.mjs +26 -8
- package/dist/assets/hooks/aria-pre-tool-gate.mjs +60 -1
- package/dist/assets/hooks/aria-stop-gate.mjs +69 -3
- package/dist/assets/hooks/doctrine_trigger_map.json +43 -0
- package/dist/assets/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/assets/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/assets/opencode-plugins/harness-context/index.js +1 -1
- package/dist/assets/opencode-plugins/harness-gate/index.js +114 -10
- package/dist/assets/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
- package/dist/assets/opencode-plugins/harness-outcome/index.js +39 -0
- package/dist/assets/opencode-plugins/harness-stop/index.js +234 -139
- package/dist/assets/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
- package/dist/assets/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
- package/dist/runtime/codex-bridge.mjs +71 -8
- package/dist/runtime/discipline/CLAUDE.md +2 -2
- package/dist/runtime/discipline/doctrine_trigger_map.json +43 -0
- package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +3 -3
- package/dist/runtime/doctrine_trigger_map.json +43 -0
- package/dist/runtime/harness-daemon.mjs +50 -2
- package/dist/runtime/hooks/aria-agent-handoff.mjs +247 -0
- package/dist/runtime/hooks/aria-agent-ledger-merge.mjs +164 -0
- package/dist/runtime/hooks/aria-architect-fallback.mjs +267 -0
- package/dist/runtime/hooks/aria-cognition-substrate-binding.mjs +761 -0
- package/dist/runtime/hooks/aria-discovery-record.mjs +101 -0
- package/dist/runtime/hooks/aria-harness-via-sdk.mjs +544 -0
- package/dist/runtime/hooks/aria-import-resolution-gate.mjs +330 -0
- package/dist/runtime/hooks/aria-outcome-record.mjs +84 -0
- package/dist/runtime/hooks/aria-pre-emit-dryrun.mjs +329 -0
- package/dist/runtime/hooks/aria-pre-text-gate.mjs +112 -0
- package/dist/runtime/hooks/aria-pre-tool-gate.mjs +2482 -0
- package/dist/runtime/hooks/aria-preprompt-consult.mjs +464 -0
- package/dist/runtime/hooks/aria-preturn-memory-gate.mjs +647 -0
- package/dist/runtime/hooks/aria-repo-doctrine-gate.mjs +429 -0
- package/dist/runtime/hooks/aria-stop-gate.mjs +1882 -0
- package/dist/runtime/hooks/aria-trigger-autolearn.mjs +229 -0
- package/dist/runtime/hooks/aria-userprompt-abandon-detect.mjs +192 -0
- package/dist/runtime/hooks/doctrine_trigger_map.json +577 -0
- package/dist/runtime/hooks/lib/canonical-lenses.mjs +65 -0
- package/dist/runtime/hooks/lib/domain-output-quality.mjs +103 -0
- package/dist/runtime/hooks/lib/gate-audit.mjs +43 -0
- package/dist/runtime/hooks/lib/gate-loop-state.mjs +50 -0
- package/dist/runtime/hooks/lib/hook-message-window.mjs +121 -0
- package/dist/runtime/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/dist/runtime/hooks/test-aria-preturn-memory-gate.mjs +245 -0
- package/dist/runtime/hooks/test-tier-lens-labeling.mjs +367 -0
- package/dist/runtime/manifest.json +2 -2
- package/dist/runtime/sdk/BUNDLED.json +2 -2
- package/dist/runtime/sdk/index.d.ts +48 -0
- package/dist/runtime/sdk/index.js +140 -1
- package/dist/runtime/sdk/index.js.map +1 -1
- package/dist/runtime/sdk/runWithGovernance.d.ts +16 -0
- package/dist/runtime/sdk/runWithGovernance.js +54 -0
- package/dist/runtime/sdk/runWithGovernance.js.map +1 -0
- package/dist/runtime/service.mjs +339 -10
- package/dist/sdk/BUNDLED.json +2 -2
- package/dist/sdk/index.d.ts +48 -0
- package/dist/sdk/index.js +140 -1
- package/dist/sdk/index.js.map +1 -1
- package/dist/sdk/runWithGovernance.d.ts +16 -0
- package/dist/sdk/runWithGovernance.js +54 -0
- package/dist/sdk/runWithGovernance.js.map +1 -0
- package/hooks/aria-harness-via-sdk.mjs +26 -8
- package/hooks/aria-pre-tool-gate.mjs +60 -1
- package/hooks/aria-stop-gate.mjs +69 -3
- package/hooks/doctrine_trigger_map.json +43 -0
- package/hooks/lib/domain-output-quality.mjs +103 -0
- package/hooks/lib/skill-autoload-gate.mjs +14 -0
- package/opencode-plugins/harness-context/index.js +1 -1
- package/opencode-plugins/harness-gate/index.js +114 -10
- package/opencode-plugins/harness-gate/lib/skill-autoload-gate.js +14 -0
- package/opencode-plugins/harness-outcome/index.js +39 -0
- package/opencode-plugins/harness-stop/index.js +234 -139
- package/opencode-plugins/harness-stop/lib/domain-output-quality.js +103 -0
- package/opencode-plugins/harness-stop/lib/skill-autoload-gate.js +14 -0
- package/package.json +12 -5
- package/runtime-src/codex-bridge.mjs +71 -8
- package/runtime-src/harness-daemon.mjs +50 -2
- package/runtime-src/service.mjs +339 -10
- package/scripts/bundle-sdk.mjs +2 -0
- package/scripts/self-test-harness-gates.mjs +79 -0
- package/src/connectors/codebase-awareness.ts +141 -77
- package/src/connectors/codex.ts +98 -0
- package/src/setup-wizard.ts +105 -25
|
@@ -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
|
+
});
|