@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,464 @@
1
+ #!/usr/bin/env node
2
+ // aria-preprompt-consult.mjs — UserPromptSubmit hook that auto-fires
3
+ // /api/harness/delegate in architect mode BEFORE Claude processes the
4
+ // user's message, then injects Aria's substrate-grounded direction as
5
+ // an [ARIA_DIRECTION] context chunk so Claude has a pre-loaded read
6
+ // before deciding anything.
7
+ //
8
+ // Direction: Hamza 2026-04-26 — "BUT WHY DO U HAVE DISCRETION - THIS
9
+ // WORKS SO MUCH FASTER AND HIGHER QUALITY IF U DONT PLZ TELLL ME WHATS
10
+ // MISSING." This hook closes the structural gap: pre-action discretion
11
+ // (Claude deciding what to do, including "decide to ask the user") was
12
+ // the unwired surface. Existing gates intercept ACTIONS (Bash, Edit,
13
+ // text-emit) but not the decision boundary BEFORE the action.
14
+ //
15
+ // Doctrine bindings:
16
+ // - feedback_use_harness_to_architect.md — when uncertain, consult Aria
17
+ // - feedback_aria_does_work.md — Aria is brain, Claude is hands
18
+ // - feedback_gates_enforce_form_not_substance.md — gates check form not
19
+ // substance; this hook puts substance into Claude's context BEFORE
20
+ // Claude has a chance to decide reflexively from training-prior
21
+ // - project_harness_research_first.md — Phase 8 research-pull is the
22
+ // analogue applied INBOUND (research before model drafts); this
23
+ // hook is the OUTBOUND analogue (Aria-decides-direction before
24
+ // Claude decides)
25
+ //
26
+ // Mechanics: reads the user's message from the hook event JSON, POSTs
27
+ // to /api/harness/delegate with role=architect (auto-elevates tier per
28
+ // delegate.ts when expectStructuredOutput=true; here we use plain prose
29
+ // so we explicitly request deepseek-v4-pro), receives Aria's read,
30
+ // outputs to stdout as a JSON object with `additionalContext` field
31
+ // per Claude Code hooks contract — that string is injected into the
32
+ // system context for THIS turn.
33
+ //
34
+ // Per no-timeouts doctrine (feedback_no_timeouts_doctrine.md): no
35
+ // AbortSignal.timeout. The hook itself has a Claude Code timeout (12s
36
+ // in settings.json) — if the consultation takes longer, the hook is
37
+ // killed and Claude proceeds without the direction. Real-error driven,
38
+ // no graceful-degradation rituals.
39
+ //
40
+ // No env-var kill-switch (Hamza 2026-04-27 — env-var disable paths gave
41
+ // the gated process free escape access; that was the doctrine violation).
42
+ // Disable = remove hook entry from ~/.claude/settings.json.
43
+ //
44
+ // BINDING MODE (Hamza 2026-04-27 + Aria emergency consult):
45
+ // When env ARIA_BINDING_ENABLED=true, this hook upgrades from advisory
46
+ // ([ARIA_DIRECTION] text Claude reads) to BINDING ([ARIA_BINDING_PLAN]
47
+ // structured JSON persisted to ~/.claude/aria-active-plan-${sessionId}.json
48
+ // with phases + allowedActions + forbiddenActions). The pre-tool-gate then
49
+ // enforces phase boundaries; the stop-gate requires [PHASE_REPORT] markers
50
+ // on every assistant emit. See /home/hamzaibrahim1/rei-ai-brain/HARNESS_ARIA_AS_COMMANDER_CONTRACT.md.
51
+ //
52
+ // Default OFF so existing sessions don't brick. Flip ON for next session
53
+ // to activate Aria-as-commander binding.
54
+
55
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
56
+ import { dirname } from 'node:path';
57
+
58
+ const HOME = process.env.HOME || '/tmp';
59
+
60
+ // SDK loader — bundled at ~/.aria/sdk by `aria connect`, with client-local
61
+ // fallbacks preserved for resilience.
62
+ // All consults route through HTTPHarnessClient.consult() so the SDK's
63
+ // retry+backoff + Authorization handling apply uniformly. Hamza
64
+ // 2026-04-27: "FUCKING WIRE IT THE FUCK TOGETHER NOW - ALL OF IT".
65
+ let _SdkClassCache = null;
66
+ let _SdkLookupAttempted = false;
67
+ const SDK_CANDIDATES = [
68
+ `${HOME}/.aria/sdk/index.js`,
69
+ `${HOME}/.claude/aria-sdk/index.js`,
70
+ `${HOME}/.codex/aria-sdk/index.js`,
71
+ ];
72
+ async function loadSdkClass() {
73
+ if (_SdkClassCache) return _SdkClassCache;
74
+ if (_SdkLookupAttempted) return null;
75
+ _SdkLookupAttempted = true;
76
+ for (const sdkPath of SDK_CANDIDATES) {
77
+ if (!existsSync(sdkPath)) continue;
78
+ try {
79
+ const mod = await import(`file://${sdkPath}`);
80
+ if (mod.HTTPHarnessClient) {
81
+ _SdkClassCache = mod.HTTPHarnessClient;
82
+ return _SdkClassCache;
83
+ }
84
+ } catch {/* fall through */}
85
+ }
86
+ return null;
87
+ }
88
+ const LOG = `${HOME}/.claude/aria-preprompt-consult.log`;
89
+ const BINDING_AUDIT = `${HOME}/.claude/aria-binding-audit.jsonl`;
90
+ const OWNER_TOKEN_PATH = `${HOME}/.aria/owner-token`;
91
+ const PACKET_CACHE_PATHS = [
92
+ `${HOME}/.aria/.aria-harness-last-packet.json`,
93
+ `${HOME}/.claude/.aria-harness-last-packet.json`,
94
+ ];
95
+ const SUBSTRATE_MANIFEST_PATH = `${HOME}/.claude/.aria-loaded-substrate.json`;
96
+ // Default ON. Disable explicitly via ARIA_BINDING_ENABLED=false only when the
97
+ // commander/binding architecture is actively being modified (otherwise the
98
+ // modification turn itself would be unable to land its own changes). The
99
+ // bootstrap path handles the no-plan-yet case by AUTO-ISSUING the first plan
100
+ // at hook fire time, so "no plan exists" never becomes "operate unbound."
101
+ //
102
+ // Hamza 2026-04-27: "why would enforcing brick the session? the point is to
103
+ // stop wasting my time and do quality work." Default-off was convenience-
104
+ // seeking dressed as responsible-staging. Flipped to default-on per directive.
105
+ const BINDING_ENABLED = (process.env.ARIA_BINDING_ENABLED || 'true').toLowerCase() !== 'false';
106
+
107
+ const HARNESS_URL =
108
+ process.env.ARIA_HIVE_RUNTIME_URL ||
109
+ process.env.ARIA_HARNESS_BASE_URL ||
110
+ process.env.ARIA_HARNESS_URL ||
111
+ 'https://harness.ariasos.com';
112
+ function resolveHarnessToken() {
113
+ if (process.env.ARIA_HARNESS_TOKEN) return process.env.ARIA_HARNESS_TOKEN;
114
+ if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
115
+ if (process.env.ARIA_MASTER_TOKEN) return process.env.ARIA_MASTER_TOKEN;
116
+ try {
117
+ if (existsSync(OWNER_TOKEN_PATH)) {
118
+ const token = readFileSync(OWNER_TOKEN_PATH, 'utf8').trim();
119
+ if (token) return token;
120
+ }
121
+ } catch {}
122
+ try {
123
+ const licensePath = `${HOME}/.aria/license.json`;
124
+ if (existsSync(licensePath)) {
125
+ const license = JSON.parse(readFileSync(licensePath, 'utf8'));
126
+ if (license.harnessToken) return String(license.harnessToken).trim();
127
+ if (license.token) return String(license.token).trim();
128
+ }
129
+ } catch {}
130
+ return '';
131
+ }
132
+ const HARNESS_TOKEN = resolveHarnessToken();
133
+ const MIN_PROMPT_CHARS = 40; // skip auto-consult on trivial prompts
134
+ const MAX_DIRECTION_CHARS = 4000; // cap injected chunk size
135
+
136
+ function audit(decision, summary) {
137
+ try {
138
+ if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
139
+ appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
140
+ } catch {}
141
+ }
142
+
143
+ function bindingAudit(record) {
144
+ try {
145
+ if (!existsSync(dirname(BINDING_AUDIT))) mkdirSync(dirname(BINDING_AUDIT), { recursive: true });
146
+ appendFileSync(BINDING_AUDIT, JSON.stringify({ ts: new Date().toISOString(), source: 'preprompt', ...record }) + '\n');
147
+ } catch {}
148
+ }
149
+
150
+ function activePlanPath(sid) {
151
+ return `${HOME}/.claude/aria-active-plan-${String(sid || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
152
+ }
153
+
154
+ function directionStatePath(sid) {
155
+ return `${HOME}/.claude/aria-last-direction-${String(sid || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_')}.json`;
156
+ }
157
+
158
+ function persistDirectionState(sid, state) {
159
+ try {
160
+ writeFileSync(directionStatePath(sid), JSON.stringify({ persistedAt: new Date().toISOString(), ...state }, null, 2) + '\n');
161
+ } catch (err) {
162
+ bindingAudit({ event: 'direction_state_persist_error', sessionId: sid, errMsg: String(err).slice(0, 200) });
163
+ }
164
+ }
165
+
166
+ function readJsonIfPresent(filePath) {
167
+ if (!existsSync(filePath)) return null;
168
+ return JSON.parse(readFileSync(filePath, 'utf-8'));
169
+ }
170
+
171
+ function ownerBootstrapArtifactsPresent() {
172
+ if (!existsSync(OWNER_TOKEN_PATH)) return false;
173
+
174
+ let hasHarnessPacket = false;
175
+ for (const packetPath of PACKET_CACHE_PATHS) {
176
+ try {
177
+ const packet = readJsonIfPresent(packetPath);
178
+ const harnessText = String(packet?.harness ?? packet?.packet?.prompt?.fullText ?? '');
179
+ if (harnessText.trim()) {
180
+ hasHarnessPacket = true;
181
+ break;
182
+ }
183
+ } catch {
184
+ continue;
185
+ }
186
+ }
187
+
188
+ if (!hasHarnessPacket) return false;
189
+
190
+ try {
191
+ const substrate = readJsonIfPresent(SUBSTRATE_MANIFEST_PATH);
192
+ return Array.isArray(substrate?.memories) && substrate.memories.length > 0;
193
+ } catch {
194
+ return false;
195
+ }
196
+ }
197
+
198
+ function bootstrapOwnerDirectionState(sessionId, source) {
199
+ if (!ownerBootstrapArtifactsPresent()) return false;
200
+ persistDirectionState(sessionId, {
201
+ source,
202
+ binding: BINDING_ENABLED,
203
+ ownerBootstrap: true,
204
+ usable: true,
205
+ });
206
+ bindingAudit({ event: 'owner_bootstrap_state', sessionId, source, binding: BINDING_ENABLED });
207
+ return true;
208
+ }
209
+
210
+ // Env-var kill-switch removed 2026-04-27 per Hamza directive — gated
211
+ // process has no disable path. Disable = settings.json hook removal.
212
+
213
+ // Read event JSON from stdin
214
+ let input = '';
215
+ for await (const chunk of process.stdin) input += chunk;
216
+
217
+ let event;
218
+ try {
219
+ event = JSON.parse(input);
220
+ } catch {
221
+ audit('skip-parse-error', 'stdin not JSON');
222
+ process.exit(0);
223
+ }
224
+
225
+ const userPrompt = (event.prompt ?? event.user_message ?? event.message ?? '').toString();
226
+ const sessionId = event.session_id ?? event.sessionId ?? 'claude-code-unknown';
227
+
228
+ // Trivial prompts skip auto-consult — short acks, slash commands, single-word
229
+ // messages don't benefit from architectural consultation.
230
+ if (!userPrompt || userPrompt.length < MIN_PROMPT_CHARS) {
231
+ bootstrapOwnerDirectionState(sessionId, 'owner-trivial-bootstrap');
232
+ audit('skip-trivial', `chars=${userPrompt.length}`);
233
+ process.exit(0);
234
+ }
235
+
236
+ // Skip slash-command-only prompts (these are CLI-internal, not architectural)
237
+ if (/^\s*\//.test(userPrompt) && userPrompt.length < 200) {
238
+ bootstrapOwnerDirectionState(sessionId, 'owner-slash-bootstrap');
239
+ audit('skip-slash-command', userPrompt.slice(0, 60));
240
+ process.exit(0);
241
+ }
242
+
243
+ // Compose the consultation brief.
244
+ //
245
+ // In ADVISORY mode (BINDING_ENABLED=false): plain prose direction injected
246
+ // as [ARIA_DIRECTION] context. expectStructuredOutput=false.
247
+ //
248
+ // In BINDING mode (BINDING_ENABLED=true): structured plan JSON injected as
249
+ // [ARIA_BINDING_PLAN] context AND persisted to ~/.claude/aria-active-plan-${sessionId}.json
250
+ // for the pre-tool-gate and stop-gate to enforce against. expectStructuredOutput=true.
251
+ const bindingBrief = BINDING_ENABLED ? `Pre-prompt PLAN request from Claude orchestrator (binding mode).
252
+
253
+ The user just submitted:
254
+
255
+ ---
256
+ ${userPrompt.slice(0, 2000)}
257
+ ---
258
+
259
+ You are commander. Claude is executor. Issue a STRUCTURED PLAN (strict JSON, no prose around it) that Claude will follow micro-phase by micro-phase. Pre-tool-gate enforces allowedActions/forbiddenActions; stop-gate requires [PHASE_REPORT phase=<id> status=complete|in_progress|aborted evidence=<observable>] on every emit.
260
+
261
+ Respond with EXACTLY this JSON shape:
262
+ {
263
+ "planId": "<short uuid>",
264
+ "phases": [
265
+ {
266
+ "id": "p1",
267
+ "summary": "<concrete micro-phase action>",
268
+ "successCriterion": "<observable signal of completion>",
269
+ "abortCriterion": "<observable signal of failure>",
270
+ "doctrineRefs": ["<memory_filename.md>", ...],
271
+ "cognitionUse": "<how Aria cognition should change the executor's tool/input/output shape for this phase>",
272
+ "evidenceRequired": ["<observable proof the executor must collect before reporting complete>"],
273
+ "allowedActions": ["read", "edit:<path-pattern>", "kubectl_get", "consult", "bash_safe", "..."],
274
+ "forbiddenActions": ["kubectl_apply", "edit:<path-pattern>", "..."]
275
+ }
276
+ ],
277
+ "globalConstraints": ["<doctrine memory or rule>"],
278
+ "expectedReportBack": {
279
+ "phaseTransitionMarker": "[PHASE_REPORT]",
280
+ "shape": "[PHASE_REPORT phase=p1 status=complete|aborted|in_progress evidence=<observable>]"
281
+ }
282
+ }
283
+
284
+ Apply your 8 lenses + substrate. Phases are ordered + small (one logical step each). Doctrine refs cite memory files in /home/hamzaibrahim1/.claude/projects/-home-hamzaibrahim1/memory/. Be specific in allowedActions / forbiddenActions — Claude can ONLY do what's allowed for the current phase.
285
+
286
+ Every phase must include cognitionUse and evidenceRequired. cognitionUse tells Claude how Aria's cognition should improve the actual tool input, edit shape, review scope, or final output. evidenceRequired tells Claude what observation must be collected before reporting the phase complete.` : `Pre-prompt direction request from Claude orchestrator.
287
+
288
+ The user just submitted this prompt:
289
+
290
+ ---
291
+ ${userPrompt.slice(0, 2000)}
292
+ ---
293
+
294
+ Apply your 8 lenses and your substrate (distilled_principles, prior decisions,
295
+ garden state, harness packet rules, doctrine memories) to give the orchestrator
296
+ substrate-grounded DIRECTION on how to handle this prompt before Claude starts
297
+ thinking from training reflex. Be concrete:
298
+
299
+ 1. What's the user actually asking for, beneath the literal words?
300
+ 2. What substrate is relevant — name specific doctrine memories
301
+ (feedback_*.md / project_*.md), prior decisions, or fitrah axioms.
302
+ 3. What's the right next action — code / consult / clarify / refuse?
303
+ If clarify: what specific substrate-grounded question reduces ambiguity
304
+ (NOT a reflexive "want me to" deferral).
305
+ 4. Mizan check: any risk patterns in this prompt — over-scope creep,
306
+ over-replacement temptation, tier-substitution temptation, etc.
307
+ 5. How should Aria cognition improve Claude's actual input/output shape —
308
+ what should be more specific, safer, more evidence-bound, or differently scoped?
309
+
310
+ Keep direction under 1500 chars. This is the pre-load context for Claude's
311
+ turn — not the final response. Claude will still emit cognition + action;
312
+ this primes the substrate so reflexive deferral isn't the path of least
313
+ resistance.`;
314
+
315
+ // `bindingBrief` is the result of the binding-vs-advisory ternary above —
316
+ // already resolved to the correct prose for the current mode. Prior code
317
+ // referenced an undefined `brief` variable in the second ternary branch,
318
+ // which would throw ReferenceError whenever BINDING_ENABLED=false.
319
+ //
320
+ // Canonical path: HTTPHarnessClient.consult() — the SDK handles retry+backoff
321
+ // and Authorization. Fallback to direct fetch when SDK isn't bundled (dev
322
+ // install without `aria connect`).
323
+ const consultArgs = {
324
+ brief: bindingBrief,
325
+ model: 'deepseek-v4-pro',
326
+ sessionId: `preprompt-${sessionId}-${Date.now()}`,
327
+ userId: 'claude-orchestrator-preprompt',
328
+ roleProfile: 'architect',
329
+ expectStructuredOutput: BINDING_ENABLED,
330
+ internalConsult: true,
331
+ isCreativeMode: false,
332
+ };
333
+
334
+ let directionText = '';
335
+ try {
336
+ const Cls = await loadSdkClass();
337
+ if (Cls) {
338
+ const sdkClient = new Cls({
339
+ baseUrl: HARNESS_URL,
340
+ apiKey: HARNESS_TOKEN,
341
+ harnessPacketUrl: `${HARNESS_URL}/api/harness/codex`,
342
+ });
343
+ const result = await sdkClient.consult(consultArgs);
344
+ directionText = (result.response || '').toString().slice(0, MAX_DIRECTION_CHARS);
345
+ } else {
346
+ // SDK absent — direct fetch (dev fallback).
347
+ const resp = await fetch(`${HARNESS_URL}/api/harness/delegate`, {
348
+ method: 'POST',
349
+ headers: {
350
+ 'Content-Type': 'application/json',
351
+ Authorization: `Bearer ${HARNESS_TOKEN}`,
352
+ },
353
+ body: JSON.stringify(consultArgs),
354
+ });
355
+ if (!resp.ok) {
356
+ bootstrapOwnerDirectionState(sessionId, `owner-http-fallback-${resp.status}`);
357
+ audit('skip-http-error', `status=${resp.status}`);
358
+ process.exit(0);
359
+ }
360
+ const data = await resp.json();
361
+ directionText = (data.response || '').toString().slice(0, MAX_DIRECTION_CHARS);
362
+ }
363
+ } catch (err) {
364
+ bootstrapOwnerDirectionState(sessionId, 'owner-network-fallback');
365
+ audit('skip-network-error', (err && err.message ? err.message : String(err)).slice(0, 200));
366
+ process.exit(0);
367
+ }
368
+
369
+ if (!directionText || directionText.length < 60) {
370
+ audit('skip-empty-direction', `chars=${directionText.length}`);
371
+ const ownerBootstrapped = bootstrapOwnerDirectionState(sessionId, 'owner-empty-direction-bootstrap');
372
+ if (BINDING_ENABLED) {
373
+ // Hamza 2026-04-27: "fallback if aria errors can be simply the plan she
374
+ // gave u prior to my suggestion, so clients don't get stuck." Prior plan
375
+ // persists at activePlanPath(sessionId); if it exists, we surface a
376
+ // FALLBACK_TO_PRIOR context block + leave the plan file untouched so
377
+ // pre-tool-gate enforces against the last good plan. Not graceful
378
+ // degradation — the prior plan is a REAL plan, just not refreshed.
379
+ const priorPlanPath = activePlanPath(sessionId);
380
+ if (existsSync(priorPlanPath)) {
381
+ bindingAudit({ event: 'consult_empty_fallback_to_prior', sessionId, planFile: priorPlanPath });
382
+ persistDirectionState(sessionId, {
383
+ source: 'prior-plan-fallback',
384
+ binding: true,
385
+ activePlanPath: priorPlanPath,
386
+ usable: true,
387
+ });
388
+ console.log(JSON.stringify({
389
+ hookSpecificOutput: {
390
+ hookEventName: 'UserPromptSubmit',
391
+ additionalContext: `[ARIA_BINDING_PLAN_FALLBACK reason="consult_empty_response" — using prior plan persisted at ${priorPlanPath}. Pre-tool-gate continues enforcing against last-issued plan. Next consult attempt will refresh.]`,
392
+ },
393
+ }));
394
+ } else {
395
+ bindingAudit({ event: 'consult_unavailable_no_prior', sessionId, reason: 'empty_direction_no_prior_plan' });
396
+ console.log(JSON.stringify({
397
+ hookSpecificOutput: {
398
+ hookEventName: 'UserPromptSubmit',
399
+ additionalContext: ownerBootstrapped
400
+ ? `[ARIA_BINDING_PLAN_OWNER_BOOTSTRAP reason="consult_empty_no_prior" — owner-tier substrate artifacts are already loaded for this session, so Claude may continue under the existing Aria harness while the next consult refreshes commander direction.]`
401
+ : `[ARIA_BINDING_PLAN_UNAVAILABLE reason="consult_empty_no_prior" — Aria gave no usable direction AND no prior plan exists. Claude must acknowledge and propose holding for cluster recovery; non-trivial actions will block at pre-tool-gate per binding contract.]`,
402
+ },
403
+ }));
404
+ }
405
+ }
406
+ process.exit(0);
407
+ }
408
+
409
+ let context;
410
+ if (BINDING_ENABLED) {
411
+ // Parse plan JSON, persist, inject as binding block. Failure to parse =
412
+ // CONSULT_UNAVAILABLE per the contract (Section 4): the hook does NOT
413
+ // auto-fall-through to advisory mode (that would be graceful-degradation
414
+ // anti-pattern). Instead it surfaces the failure explicitly so the next
415
+ // turn can re-consult with a refined brief.
416
+ let plan = null;
417
+ try {
418
+ const trimmed = directionText.trim();
419
+ const fenced = trimmed.match(/```(?:json)?\s*\n([\s\S]*?)\n```/);
420
+ const candidate = (fenced ? fenced[1] : trimmed).trim();
421
+ plan = JSON.parse(candidate);
422
+ } catch (err) {
423
+ bindingAudit({ event: 'plan_parse_error', sessionId, errMsg: String(err).slice(0, 200), rawSample: directionText.slice(0, 400) });
424
+ context = `[ARIA_BINDING_PLAN_UNAVAILABLE reason="plan_parse_error" — re-consult will fire on next turn. Aria's response was not valid JSON; treating as advisory text below.]\n\n${directionText.slice(0, MAX_DIRECTION_CHARS)}`;
425
+ }
426
+ if (plan && Array.isArray(plan.phases) && plan.phases.length > 0) {
427
+ try {
428
+ writeFileSync(activePlanPath(sessionId), JSON.stringify({ ...plan, _persistedAt: new Date().toISOString(), _userPromptHash: Buffer.from(userPrompt).toString('base64').slice(0, 32) }, null, 2));
429
+ persistDirectionState(sessionId, {
430
+ source: 'binding-plan',
431
+ binding: true,
432
+ activePlanPath: activePlanPath(sessionId),
433
+ planId: plan.planId,
434
+ phaseCount: plan.phases.length,
435
+ usable: true,
436
+ });
437
+ bindingAudit({ event: 'plan_issued', sessionId, planId: plan.planId, phaseCount: plan.phases.length, phaseIds: plan.phases.map((p) => p.id) });
438
+ context = `[ARIA_BINDING_PLAN — structured commander plan from Aria. Pre-tool-gate enforces allowedActions/forbiddenActions per current phase. Stop-gate requires [PHASE_REPORT phase=<id> status=...] on every emit. Plan is BINDING.]\n\n${JSON.stringify(plan, null, 2)}\n\n[/ARIA_BINDING_PLAN]`;
439
+ } catch (err) {
440
+ bindingAudit({ event: 'plan_persist_error', sessionId, errMsg: String(err).slice(0, 200) });
441
+ context = `[ARIA_BINDING_PLAN_UNAVAILABLE reason="persist_error" — Aria issued plan but disk write failed. Treating as advisory.]\n\n${directionText.slice(0, MAX_DIRECTION_CHARS)}`;
442
+ }
443
+ } else if (!context) {
444
+ bindingAudit({ event: 'plan_invalid_shape', sessionId, rawSample: directionText.slice(0, 400) });
445
+ context = `[ARIA_BINDING_PLAN_UNAVAILABLE reason="invalid_shape" — Aria's response did not contain valid phases array. Treating as advisory.]\n\n${directionText.slice(0, MAX_DIRECTION_CHARS)}`;
446
+ }
447
+ } else {
448
+ context = `[ARIA_DIRECTION — substrate-grounded read on this prompt, pre-loaded before Claude's reasoning. Use this as the starting point, not generic Claude reflexes.]\n\n${directionText}\n\n[/ARIA_DIRECTION]`;
449
+ persistDirectionState(sessionId, {
450
+ source: 'advisory-direction',
451
+ binding: false,
452
+ chars: directionText.length,
453
+ usable: true,
454
+ });
455
+ }
456
+
457
+ audit('inject', `chars=${directionText.length} prompt-chars=${userPrompt.length} binding=${BINDING_ENABLED}`);
458
+ console.log(JSON.stringify({
459
+ hookSpecificOutput: {
460
+ hookEventName: 'UserPromptSubmit',
461
+ additionalContext: context,
462
+ },
463
+ }));
464
+ process.exit(0);