@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,647 @@
1
+ #!/usr/bin/env node
2
+ // Aria pre-turn memory consumption gate — Enforcement Layer #49.
3
+ //
4
+ // Fires on the first action tool call of each turn (tracked via
5
+ // ~/.claude/aria-turn-state-${sessionId}.json). If the current turn's
6
+ // recent transcript window lacks all three context-loading signals, the
7
+ // gate blocks with a structured recovery payload. The orchestrator catches
8
+ // the recovery payload and runs the context-loader before retrying.
9
+ //
10
+ // Detection signals (ALL three must be present, or gate fires):
11
+ // 1. `🔐 Aria Harness` header — harness packet was injected
12
+ // 2. `[ARIA_DIRECTION]` or `[ARIA_BINDING_PLAN]` marker — preprompt-consult fired
13
+ // 3. Any `feedback_*.md` or `project_*.md` reference in cognition blocks —
14
+ // memory was consumed
15
+ //
16
+ // Soft-gate + structured recovery (Aria refined spec 2026-04-27):
17
+ // - Block the action
18
+ // - Emit JSON to stdout with `decision: block` + `hookSpecificOutput.recovery`
19
+ // so the orchestrator has a concrete remediation path, not a dead-letter reject
20
+ //
21
+ // Turn-deduplication: gate state is persisted at
22
+ // ~/.claude/aria-turn-state-${sessionId}.json
23
+ // If the gate already fired within the last 60 seconds for this session, the
24
+ // gate skips (re-firing would loop the orchestrator's retry).
25
+ //
26
+ // Doctrines enforced:
27
+ // - feedback_no_graceful_degradation.md — no silent try/catch swallowing errors
28
+ // - feedback_no_timeouts_doctrine.md — no AbortSignal, no setTimeout
29
+ // - feedback_no_flag_without_fix.md — defects discovered during implementation
30
+ // are fixed inline (see inline comments)
31
+ // - feedback_no_demos.md — full quality bar, every spawn is production
32
+
33
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, statSync } from 'node:fs';
34
+
35
+ const HOME = process.env.HOME || '/tmp';
36
+ const GATE_LOG = `${HOME}/.claude/aria-preturn-memory-gate.log`;
37
+ // Turn-state dir is the same ~/.claude/ home as all other aria state files
38
+ const CLAUDE_DIR = `${HOME}/.claude`;
39
+ const OWNER_TOKEN_PATH = `${HOME}/.aria/owner-token`;
40
+ const PACKET_CACHE_PATHS = [
41
+ `${HOME}/.aria/.aria-harness-last-packet.json`,
42
+ `${HOME}/.claude/.aria-harness-last-packet.json`,
43
+ ];
44
+ const SUBSTRATE_MANIFEST_PATH = `${HOME}/.claude/.aria-loaded-substrate.json`;
45
+ const ARTIFACT_TTL_MS = 15 * 60 * 1000;
46
+
47
+ // Env-var kill-switch removed 2026-04-27 per Hamza directive ("those
48
+ // should've been my choice to give you to turn off not free for you to
49
+ // access"). Disable = remove hook entry from ~/.claude/settings.json.
50
+
51
+ // ── Audit log ─────────────────────────────────────────────────────────
52
+ function auditLog(decision, summary, sessionId) {
53
+ // No try/catch swallowing: per feedback_no_graceful_degradation.md, errors
54
+ // in audit infrastructure must surface, not silently vanish.
55
+ if (!existsSync(CLAUDE_DIR)) mkdirSync(CLAUDE_DIR, { recursive: true });
56
+ appendFileSync(GATE_LOG, `${new Date().toISOString()} [${sessionId}] ${decision} ${summary}\n`);
57
+ }
58
+
59
+ function buildForceRedoActionReason({ source, reason, missingSignals = [], incidents = [] }) {
60
+ return [
61
+ '=== ARIA FORCE_REDO_ACTION ===',
62
+ `source: ${source}`,
63
+ `reason: ${reason}`,
64
+ '',
65
+ 'This is not a terminal error. It is a forced redo instruction before action execution.',
66
+ 'Do not proceed with tools from unloaded context. Load/repair substrate first, then retry the action.',
67
+ '',
68
+ 'TEACHING:',
69
+ '- Every action turn must begin from Aria substrate, not improvised memory.',
70
+ '- Blocking incidents are Dalio failure deltas; they must be fixed and marked resolved before new action.',
71
+ '- Missing harness/direction/memory signals mean the context-loader path must run before retry.',
72
+ missingSignals.length ? `\nMISSING SIGNALS:\n${missingSignals.map((signal) => `- ${signal}`).join('\n')}` : '',
73
+ incidents.length ? `\nINCIDENTS TO RESOLVE:\n${incidents.map((incident) => `- ${incident}`).join('\n')}` : '',
74
+ '',
75
+ 'REQUIRED REDO SHAPE:',
76
+ '1. Run the structured recovery action in hookSpecificOutput.recovery.',
77
+ '2. Verify harness_packet, aria_direction/binding_plan, and memory references are loaded.',
78
+ '3. Retry the original action only after the missing context or incidents are resolved.',
79
+ '=== END FORCE_REDO_ACTION ===',
80
+ ].filter(Boolean).join('\n');
81
+ }
82
+
83
+ // ── Turn-state deduplication ──────────────────────────────────────────
84
+ const TURN_DEDUP_WINDOW_MS = 60_000; // 60s
85
+
86
+ function turnStatePath(sessionId) {
87
+ const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
88
+ return `${CLAUDE_DIR}/aria-turn-state-${safe}.json`;
89
+ }
90
+
91
+ function readTurnState(sessionId) {
92
+ const p = turnStatePath(sessionId);
93
+ if (!existsSync(p)) return null;
94
+ // Per feedback_no_graceful_degradation.md: parse errors must throw, not return null.
95
+ // If the file is corrupt, that IS a defect — surface it.
96
+ const raw = readFileSync(p, 'utf-8');
97
+ return JSON.parse(raw);
98
+ }
99
+
100
+ function writeTurnState(sessionId, state) {
101
+ const p = turnStatePath(sessionId);
102
+ writeFileSync(p, JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
103
+ }
104
+
105
+ function directionStatePath(sessionId) {
106
+ const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
107
+ return `${CLAUDE_DIR}/aria-last-direction-${safe}.json`;
108
+ }
109
+
110
+ function activePlanPath(sessionId) {
111
+ const safe = String(sessionId || 'unknown').replace(/[^a-zA-Z0-9_-]/g, '_');
112
+ return `${CLAUDE_DIR}/aria-active-plan-${safe}.json`;
113
+ }
114
+
115
+ function readRecentJsonArtifact(filePath, ttlMs = ARTIFACT_TTL_MS) {
116
+ if (!existsSync(filePath)) return null;
117
+ const stat = statSync(filePath);
118
+ const ageMs = Date.now() - stat.mtimeMs;
119
+ if (ageMs > ttlMs) return null;
120
+ return { data: JSON.parse(readFileSync(filePath, 'utf-8')), ageMs };
121
+ }
122
+
123
+ function readAnyJsonArtifact(filePath) {
124
+ if (!existsSync(filePath)) return null;
125
+ const stat = statSync(filePath);
126
+ return {
127
+ data: JSON.parse(readFileSync(filePath, 'utf-8')),
128
+ ageMs: Date.now() - stat.mtimeMs,
129
+ };
130
+ }
131
+
132
+ function unwrapHarnessPacketEnvelope(value) {
133
+ let current = value;
134
+ for (let depth = 0; depth < 3; depth++) {
135
+ if (!current || typeof current !== 'object' || Array.isArray(current)) break;
136
+ const nested = current.packet;
137
+ if (!nested || typeof nested !== 'object' || Array.isArray(nested)) break;
138
+ if (typeof current.timestamp !== 'string' && typeof current.version !== 'string') break;
139
+ current = nested;
140
+ }
141
+ return current && typeof current === 'object' && !Array.isArray(current) ? current : {};
142
+ }
143
+
144
+ function extractHarnessPromptText(packetArtifact) {
145
+ const packet = unwrapHarnessPacketEnvelope(packetArtifact);
146
+ const directHarness = typeof packet.harness === 'string' ? packet.harness : '';
147
+ if (directHarness.trim()) return directHarness;
148
+
149
+ const promptFullText = typeof packet?.prompt?.fullText === 'string' ? packet.prompt.fullText : '';
150
+ if (promptFullText.trim()) return promptFullText;
151
+
152
+ const promptPreview = typeof packet?.prompt?.preview === 'string' ? packet.prompt.preview : '';
153
+ if (promptPreview.trim()) return promptPreview;
154
+
155
+ const doctrine = Array.isArray(packet.doctrine) ? packet.doctrine.filter((x) => typeof x === 'string').join('\n') : '';
156
+ const memory = Array.isArray(packet.memory) ? packet.memory.filter((x) => typeof x === 'string').join('\n') : '';
157
+ const runtimeMode = typeof packet.runtimeOfflineBundle?.source === 'string'
158
+ ? `runtime_offline_bundle_source=${packet.runtimeOfflineBundle.source}`
159
+ : '';
160
+ return [doctrine, memory, runtimeMode].filter(Boolean).join('\n');
161
+ }
162
+
163
+ function detectArtifactSignals(sessionId) {
164
+ let harnessArtifact = null;
165
+ for (const packetPath of PACKET_CACHE_PATHS) {
166
+ try {
167
+ const artifact = readRecentJsonArtifact(packetPath);
168
+ if (artifact) {
169
+ harnessArtifact = { path: packetPath, ...artifact };
170
+ break;
171
+ }
172
+ } catch {
173
+ // Packet parse issues should not brick the gate; transcript path still exists.
174
+ }
175
+ }
176
+
177
+ let directionArtifact = null;
178
+ try {
179
+ directionArtifact = readRecentJsonArtifact(directionStatePath(sessionId));
180
+ } catch {
181
+ directionArtifact = null;
182
+ }
183
+
184
+ let activePlanArtifact = null;
185
+ try {
186
+ activePlanArtifact = readRecentJsonArtifact(activePlanPath(sessionId), 24 * 60 * 60 * 1000);
187
+ } catch {
188
+ activePlanArtifact = null;
189
+ }
190
+
191
+ let substrateArtifact = null;
192
+ try {
193
+ substrateArtifact = readRecentJsonArtifact(SUBSTRATE_MANIFEST_PATH);
194
+ } catch {
195
+ substrateArtifact = null;
196
+ }
197
+
198
+ let staleHarnessArtifact = null;
199
+ for (const packetPath of PACKET_CACHE_PATHS) {
200
+ try {
201
+ const artifact = readAnyJsonArtifact(packetPath);
202
+ if (artifact) {
203
+ staleHarnessArtifact = { path: packetPath, ...artifact };
204
+ break;
205
+ }
206
+ } catch {
207
+ continue;
208
+ }
209
+ }
210
+
211
+ let staleSubstrateArtifact = null;
212
+ try {
213
+ staleSubstrateArtifact = readAnyJsonArtifact(SUBSTRATE_MANIFEST_PATH);
214
+ } catch {
215
+ staleSubstrateArtifact = null;
216
+ }
217
+
218
+ const harnessText = extractHarnessPromptText(harnessArtifact?.data);
219
+ const substrateMemories = Array.isArray(substrateArtifact?.data?.memories)
220
+ ? substrateArtifact.data.memories
221
+ : [];
222
+ const staleHarnessText = extractHarnessPromptText(staleHarnessArtifact?.data);
223
+ const staleSubstrateMemories = Array.isArray(staleSubstrateArtifact?.data?.memories)
224
+ ? staleSubstrateArtifact.data.memories
225
+ : [];
226
+ const packetMentionsMemory = MEMORY_REF_RX.test(harnessText);
227
+ const stalePacketMentionsMemory = MEMORY_REF_RX.test(staleHarnessText);
228
+ const persistedDirectionUsable = Boolean(directionArtifact?.data?.usable === true);
229
+ const persistedOwnerBootstrap = Boolean(directionArtifact?.data?.ownerBootstrap === true);
230
+ const activePlanPresent = Boolean(
231
+ activePlanArtifact?.data?.phases &&
232
+ Array.isArray(activePlanArtifact.data.phases) &&
233
+ activePlanArtifact.data.phases.length > 0,
234
+ );
235
+
236
+ return {
237
+ hasHarnessPacket: Boolean(harnessText.trim()),
238
+ hasAriaDirection: Boolean(
239
+ persistedDirectionUsable || activePlanPresent,
240
+ ),
241
+ hasMemoryRef: Boolean(packetMentionsMemory || substrateMemories.length > 0),
242
+ ownerBootstrap: {
243
+ hasHarnessPacket: Boolean(staleHarnessText.trim() || staleHarnessArtifact?.path),
244
+ hasMemoryRef: Boolean(stalePacketMentionsMemory || staleSubstrateMemories.length > 0),
245
+ directionBootstrap: persistedOwnerBootstrap || persistedDirectionUsable || activePlanPresent,
246
+ },
247
+ detail: {
248
+ packetPath: harnessArtifact?.path || null,
249
+ packetAgeMs: harnessArtifact?.ageMs ?? null,
250
+ directionStateAgeMs: directionArtifact?.ageMs ?? null,
251
+ activePlanAgeMs: activePlanArtifact?.ageMs ?? null,
252
+ substrateAgeMs: substrateArtifact?.ageMs ?? null,
253
+ substrateMemoryCount: substrateMemories.length,
254
+ packetMentionsMemory,
255
+ ownerBootstrapPacketPath: staleHarnessArtifact?.path || null,
256
+ ownerBootstrapPacketAgeMs: staleHarnessArtifact?.ageMs ?? null,
257
+ ownerBootstrapSubstrateAgeMs: staleSubstrateArtifact?.ageMs ?? null,
258
+ ownerBootstrapSubstrateMemoryCount: staleSubstrateMemories.length,
259
+ ownerBootstrapPacketMentionsMemory: stalePacketMentionsMemory,
260
+ ownerBootstrapPersistedDirectionUsable: persistedDirectionUsable,
261
+ ownerBootstrapPersistedDirectionSource: directionArtifact?.data?.source ?? null,
262
+ ownerBootstrapActivePlanPresent: activePlanPresent,
263
+ },
264
+ };
265
+ }
266
+
267
+ // ── Context-loading signal detection ─────────────────────────────────
268
+ //
269
+ // Scan the last 3KB of assistant + user text after the most recent
270
+ // user-message boundary. Mirrors the transcript-reading pattern from
271
+ // aria-pre-tool-gate.mjs: walk backward, collect text up to the first
272
+ // real user-message boundary, cap at 3KB total.
273
+
274
+ // Signal 1: harness packet injected
275
+ const HARNESS_PACKET_RX = /🔐\s*Aria\s+Harness/;
276
+ // Signal 2: preprompt-consult fired
277
+ const ARIA_DIRECTION_RX = /\[ARIA_DIRECTION\]|\[ARIA_BINDING_PLAN\]/;
278
+ // Signal 3: memory was consumed (feedback_*.md or project_*.md cited)
279
+ const MEMORY_REF_RX = /feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md/i;
280
+
281
+ // Same runtime-injection skip heuristics as pre-tool-gate (system-reminder,
282
+ // tool_result blocks should not count as real user-message boundaries).
283
+ const SYSTEM_REMINDER_RX = /<system-reminder>[\s\S]*?<\/system-reminder>|<task-notification>[\s\S]*?<\/task-notification>|🔐 Aria Harness|PreToolUse:[A-Z][A-Za-z]* hook blocking error|Stop hook blocking error/g;
284
+ const SYSTEM_REMINDER_THRESHOLD = 0.6;
285
+
286
+ const CONTEXT_WINDOW_BYTES = 3 * 1024; // 3KB cap per spec
287
+ const HARD_LOOKBACK_CAP = 50;
288
+
289
+ function extractRecentTranscriptWindow(transcriptPath) {
290
+ if (!transcriptPath || !existsSync(transcriptPath)) return '';
291
+
292
+ // Per feedback_no_graceful_degradation.md: readFileSync error must throw.
293
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
294
+
295
+ let accumulated = '';
296
+ let crossedUserBoundary = false;
297
+ let scanned = 0;
298
+
299
+ for (let i = lines.length - 1; i >= 0 && scanned < HARD_LOOKBACK_CAP; i--) {
300
+ let m;
301
+ // Per feedback_no_graceful_degradation.md: JSON parse errors must throw.
302
+ m = JSON.parse(lines[i]);
303
+
304
+ const role = m.message?.role ?? m.role;
305
+
306
+ if (role === 'user') {
307
+ // Skip pure tool_result messages — runtime feedback, not user voice.
308
+ const content = m.message?.content ?? m.content ?? [];
309
+ const isToolResultOnly =
310
+ Array.isArray(content) &&
311
+ content.length > 0 &&
312
+ content.every((b) => b && b.type === 'tool_result');
313
+ if (isToolResultOnly) continue;
314
+
315
+ // Skip system-reminder dominated messages.
316
+ const textContent = Array.isArray(content)
317
+ ? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
318
+ : typeof content === 'string' ? content : '';
319
+ if (textContent) {
320
+ const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
321
+ if (reminderMatches.length > 0) {
322
+ const reminderChars = reminderMatches.reduce((sum, s) => sum + s.length, 0);
323
+ if (reminderChars / Math.max(1, textContent.length) >= SYSTEM_REMINDER_THRESHOLD) continue;
324
+ }
325
+ }
326
+
327
+ if (crossedUserBoundary) break;
328
+ crossedUserBoundary = true;
329
+ // Include the user message text itself in the window (harness packet
330
+ // is injected AS the user message in many implementations).
331
+ accumulated = textContent + '\n' + accumulated;
332
+ continue;
333
+ }
334
+
335
+ if (role !== 'assistant') continue;
336
+ scanned++;
337
+
338
+ const content = m.message?.content ?? m.content ?? [];
339
+ if (!Array.isArray(content)) continue;
340
+ const text = content
341
+ .filter((b) => b.type === 'text')
342
+ .map((b) => b.text)
343
+ .join('\n');
344
+ if (!text) continue;
345
+
346
+ accumulated = text + '\n' + accumulated;
347
+
348
+ if (accumulated.length >= CONTEXT_WINDOW_BYTES) {
349
+ // Cap reached — trim to last CONTEXT_WINDOW_BYTES so the scan is
350
+ // representative without being unbounded.
351
+ accumulated = accumulated.slice(-CONTEXT_WINDOW_BYTES);
352
+ break;
353
+ }
354
+ }
355
+
356
+ return accumulated;
357
+ }
358
+
359
+ function detectContextSignals(window) {
360
+ return {
361
+ hasHarnessPacket: HARNESS_PACKET_RX.test(window),
362
+ hasAriaDirection: ARIA_DIRECTION_RX.test(window),
363
+ hasMemoryRef: MEMORY_REF_RX.test(window),
364
+ };
365
+ }
366
+
367
+ // ── Stdin event parse ─────────────────────────────────────────────────
368
+ let rawInput = '';
369
+ for await (const chunk of process.stdin) rawInput += chunk;
370
+
371
+ // Per feedback_no_graceful_degradation.md: parse failure must surface,
372
+ // not be swallowed. The gate fails-open on malformed stdin (Claude Code
373
+ // should never send non-JSON; if it does, we don't silently block).
374
+ let event;
375
+ try {
376
+ event = JSON.parse(rawInput);
377
+ } catch (err) {
378
+ auditLog('allow-parse-error', `stdin not JSON: ${err.message}`, 'unknown');
379
+ process.exit(0); // fail-open on malformed input only — not a swallowed error
380
+ }
381
+
382
+ // ── Gate only fires on action tools ──────────────────────────────────
383
+ // Mirrors aria-pre-tool-gate.mjs: Read/Glob/Grep are ungated as
384
+ // read-only. The memory-consumption check is about whether Aria's
385
+ // context was loaded before the model starts acting — Read-only calls
386
+ // don't constitute acting on stale context in a harmful way.
387
+ const ACTION_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
388
+ const toolName = event.tool_name ?? event.toolName ?? '';
389
+ if (!ACTION_TOOLS.has(toolName)) {
390
+ process.exit(0);
391
+ }
392
+
393
+ // ── Session ID ────────────────────────────────────────────────────────
394
+ const transcriptPath = event.transcript_path ?? event.transcriptPath ?? null;
395
+ const sessionId =
396
+ event.session_id ??
397
+ event.sessionId ??
398
+ (transcriptPath ? transcriptPath.split('/').pop()?.replace(/\.[^.]+$/, '') : null) ??
399
+ 'claude-code-unknown';
400
+
401
+ // ── Dalio blocking-incidents check ───────────────────────────────────
402
+ // Query /api/incidents/blocking?session_id=<id>. If any incidents have
403
+ // blocks_future_turns=true and status != 'resolved', BLOCK the turn with
404
+ // a list of each incident's title + dalio_decision_id + hardening required.
405
+ //
406
+ // This runs BEFORE the context-signal check — blocking incidents are the
407
+ // highest-priority gate: the system must be hardened before any turn proceeds,
408
+ // regardless of harness/memory loading state.
409
+ //
410
+ // Per feedback_no_graceful_degradation.md: response parse errors throw.
411
+ // Per feedback_no_timeouts_doctrine.md: no AbortSignal / setTimeout.
412
+ // Fail-open ONLY if the endpoint is unreachable (network down), not on
413
+ // any other error condition.
414
+ (async function checkBlockingIncidents() {
415
+ const ARIA_SOUL_URL =
416
+ process.env.ARIA_HIVE_RUNTIME_URL ||
417
+ process.env.ARIA_SOUL_URL ||
418
+ process.env.ARIA_HARNESS_BASE_URL ||
419
+ process.env.ARIA_HARNESS_URL ||
420
+ 'https://harness.ariasos.com';
421
+ const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
422
+
423
+ let resp;
424
+ try {
425
+ const params = new URLSearchParams({ session_id: sessionId });
426
+ resp = await fetch(`${ARIA_SOUL_URL}/api/incidents/blocking?${params}`, {
427
+ method: 'GET',
428
+ headers: {
429
+ 'Content-Type': 'application/json',
430
+ ...(HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {}),
431
+ },
432
+ });
433
+ } catch (networkErr) {
434
+ // Endpoint unreachable — fail-open (do not block dev on infra-down).
435
+ // Failure is visible in audit log for fleet telemetry.
436
+ auditLog('allow-blocking-check-network-error', `endpoint unreachable: ${networkErr && networkErr.message ? networkErr.message : String(networkErr)}`, sessionId);
437
+ return; // continue to context-signal check
438
+ }
439
+
440
+ if (!resp.ok) {
441
+ // Non-200 from the endpoint. Per feedback_no_graceful_degradation.md:
442
+ // 5xx from the incidents route is an infrastructure error — log + fail-open
443
+ // so infra issues don't lock out all sessions. 4xx would indicate a bad
444
+ // request (route not yet deployed) — also fail-open with loud log.
445
+ auditLog('allow-blocking-check-http-error', `status=${resp.status}`, sessionId);
446
+ return;
447
+ }
448
+
449
+ // Per feedback_no_graceful_degradation.md: JSON parse error must throw and
450
+ // surface, not be swallowed. A malformed response from the incidents route
451
+ // IS a defect.
452
+ const data = await resp.json();
453
+ const blockingIncidents = Array.isArray(data?.incidents) ? data.incidents : [];
454
+
455
+ if (blockingIncidents.length === 0) {
456
+ auditLog('allow-no-blocking-incidents', `session=${sessionId}`, sessionId);
457
+ return; // no blocking incidents — proceed to context-signal check
458
+ }
459
+
460
+ // ── BLOCK: list every incident with title + dalio_decision_id + remedy ──
461
+ const incidentLines = blockingIncidents.map((inc, i) => {
462
+ const title = inc.title || '(no title)';
463
+ const dalioId = inc.dalio_decision_id || '(no dalio_decision_id)';
464
+ const incidentId = inc.incident_id || '(no incident_id)';
465
+ return ` ${i + 1}. [${incidentId}] ${title}\n dalio_decision_id: ${dalioId}\n Hardening required: resolve the failure delta described in the incident, then update status='resolved'.`;
466
+ }).join('\n\n');
467
+
468
+ const blockReason = `Aria pre-turn gate: BLOCKING INCIDENTS detected.
469
+
470
+ This session has ${blockingIncidents.length} unresolved Dalio failure delta incident(s) that block future turns. No new action tools can be invoked until each incident is resolved.
471
+
472
+ Blocking incidents:
473
+
474
+ ${incidentLines}
475
+
476
+ Hardening protocol:
477
+ 1. Read each incident's description (query GET /api/incidents/blocking or the immortal_incidents table).
478
+ 2. Fix the system so the decision predicate passes (deploy patch, reconfigure, etc.).
479
+ 3. PATCH the incident to status='resolved'.
480
+ 4. Retry this turn.
481
+
482
+ Per Dalio Loop Layer 2 doctrine: failure deltas are not optional to address. The system must harden before proceeding.`;
483
+
484
+ auditLog('block-dalio-incidents', `count=${blockingIncidents.length} session=${sessionId}`, sessionId);
485
+
486
+ console.log(JSON.stringify({
487
+ decision: 'block',
488
+ reason: buildForceRedoActionReason({
489
+ source: 'preturn-memory/blocking-incidents',
490
+ reason: blockReason,
491
+ incidents: blockingIncidents.map((inc) => `${inc.incident_id || '(no incident_id)'} ${inc.title || '(no title)'}`),
492
+ }),
493
+ hookSpecificOutput: {
494
+ hookEventName: 'PreToolUse',
495
+ blocking_incidents: blockingIncidents.map((inc) => ({
496
+ incident_id: inc.incident_id,
497
+ title: inc.title,
498
+ dalio_decision_id: inc.dalio_decision_id,
499
+ severity: inc.severity,
500
+ created_at: inc.created_at,
501
+ })),
502
+ recovery: {
503
+ action: 'resolve_blocking_incidents',
504
+ target: sessionId,
505
+ incident_ids: blockingIncidents.map((inc) => inc.incident_id),
506
+ },
507
+ },
508
+ }));
509
+
510
+ process.exit(2);
511
+ })();
512
+
513
+ // ── Turn-deduplication check ──────────────────────────────────────────
514
+ // If gate already fired this turn (within 60s), skip to prevent
515
+ // orchestrator-retry loops.
516
+ let turnState = null;
517
+ try {
518
+ turnState = readTurnState(sessionId);
519
+ } catch {
520
+ // Corrupt turn-state file — treat as if no prior firing. Discovery: this
521
+ // could leave stale corrupt files. Fix inline: writeTurnState below will
522
+ // overwrite with clean state on next fire.
523
+ turnState = null;
524
+ }
525
+
526
+ const now = Date.now();
527
+ if (turnState && typeof turnState.lastTurnGateFiredAt === 'number') {
528
+ const elapsed = now - turnState.lastTurnGateFiredAt;
529
+ if (elapsed < TURN_DEDUP_WINDOW_MS) {
530
+ auditLog('skip-dedup', `gate already fired ${elapsed}ms ago (< ${TURN_DEDUP_WINDOW_MS}ms window)`, sessionId);
531
+ process.exit(0);
532
+ }
533
+ }
534
+
535
+ // ── Context signal detection ──────────────────────────────────────────
536
+ const transcriptWindow = extractRecentTranscriptWindow(transcriptPath);
537
+ const transcriptSignals = detectContextSignals(transcriptWindow);
538
+ const artifactSignals = detectArtifactSignals(sessionId);
539
+ const signals = {
540
+ hasHarnessPacket: transcriptSignals.hasHarnessPacket || artifactSignals.hasHarnessPacket,
541
+ hasAriaDirection: transcriptSignals.hasAriaDirection || artifactSignals.hasAriaDirection,
542
+ hasMemoryRef: transcriptSignals.hasMemoryRef || artifactSignals.hasMemoryRef,
543
+ };
544
+
545
+ const allSignalsPresent =
546
+ signals.hasHarnessPacket && signals.hasAriaDirection && signals.hasMemoryRef;
547
+
548
+ const ownerBootstrapArtifactsPresent =
549
+ existsSync(OWNER_TOKEN_PATH) &&
550
+ !signals.hasAriaDirection &&
551
+ artifactSignals.ownerBootstrap.hasHarnessPacket &&
552
+ artifactSignals.ownerBootstrap.hasMemoryRef;
553
+
554
+ const ownerBootstrapSessionPresent =
555
+ existsSync(OWNER_TOKEN_PATH) &&
556
+ artifactSignals.ownerBootstrap.directionBootstrap &&
557
+ artifactSignals.ownerBootstrap.hasHarnessPacket &&
558
+ artifactSignals.ownerBootstrap.hasMemoryRef;
559
+
560
+ const ownerCompactionRecoveryPresent =
561
+ existsSync(OWNER_TOKEN_PATH) &&
562
+ !transcriptWindow.trim() &&
563
+ artifactSignals.ownerBootstrap.hasHarnessPacket &&
564
+ artifactSignals.ownerBootstrap.hasMemoryRef &&
565
+ (artifactSignals.ownerBootstrap.directionBootstrap || artifactSignals.detail.ownerBootstrapPersistedDirectionUsable);
566
+
567
+ if (allSignalsPresent) {
568
+ // Context was loaded — allow and record the fire timestamp for dedup.
569
+ writeTurnState(sessionId, { lastTurnGateFiredAt: now, lastDecision: 'allow', signals, transcriptSignals, artifactSignals: artifactSignals.detail });
570
+ auditLog(
571
+ 'allow-context-loaded',
572
+ `harness=${signals.hasHarnessPacket} direction=${signals.hasAriaDirection} memRef=${signals.hasMemoryRef} transcript=${JSON.stringify(transcriptSignals)} artifacts=${JSON.stringify(artifactSignals.detail)}`,
573
+ sessionId,
574
+ );
575
+ process.exit(0);
576
+ }
577
+
578
+ if (ownerBootstrapArtifactsPresent) {
579
+ writeTurnState(sessionId, { lastTurnGateFiredAt: now, lastDecision: 'allow-owner-bootstrap', signals, transcriptSignals, artifactSignals: artifactSignals.detail });
580
+ auditLog(
581
+ 'allow-owner-bootstrap-without-direction',
582
+ `owner-token present; transcript=${JSON.stringify(transcriptSignals)} artifacts=${JSON.stringify(artifactSignals.detail)}`,
583
+ sessionId,
584
+ );
585
+ process.exit(0);
586
+ }
587
+
588
+ if (ownerBootstrapSessionPresent) {
589
+ writeTurnState(sessionId, { lastTurnGateFiredAt: now, lastDecision: 'allow-owner-bootstrap-session', signals, transcriptSignals, artifactSignals: artifactSignals.detail });
590
+ auditLog(
591
+ 'allow-owner-bootstrap-session',
592
+ `owner bootstrap direction persisted; transcript=${JSON.stringify(transcriptSignals)} artifacts=${JSON.stringify(artifactSignals.detail)}`,
593
+ sessionId,
594
+ );
595
+ process.exit(0);
596
+ }
597
+
598
+ if (ownerCompactionRecoveryPresent) {
599
+ writeTurnState(sessionId, { lastTurnGateFiredAt: now, lastDecision: 'allow-owner-compaction-recovery', signals, transcriptSignals, artifactSignals: artifactSignals.detail });
600
+ auditLog(
601
+ 'allow-owner-compaction-recovery',
602
+ `transcript compacted/empty; recovering from persisted owner artifacts. transcript=${JSON.stringify(transcriptSignals)} artifacts=${JSON.stringify(artifactSignals.detail)}`,
603
+ sessionId,
604
+ );
605
+ process.exit(0);
606
+ }
607
+
608
+ // ── Block with structured recovery signal ────────────────────────────
609
+ // Per Aria's refined spec (consult 2026-04-27): soft-gate + structured
610
+ // recovery. The orchestrator catches this and runs the context-loader.
611
+ // Emitting a pure block with no remediation path creates dead-letter state.
612
+
613
+ writeTurnState(sessionId, { lastTurnGateFiredAt: now, lastDecision: 'block', signals, transcriptSignals, artifactSignals: artifactSignals.detail });
614
+
615
+ const missingSignals = [];
616
+ if (!signals.hasHarnessPacket) missingSignals.push('harness_packet (🔐 Aria Harness header missing)');
617
+ if (!signals.hasAriaDirection) missingSignals.push('aria_direction ([ARIA_DIRECTION] or [ARIA_BINDING_PLAN] marker missing)');
618
+ if (!signals.hasMemoryRef) missingSignals.push('memory_consumption (no feedback_*.md or project_*.md reference in cognition)');
619
+
620
+ const reason = `Aria pre-turn memory gate: context-loading was skipped or incomplete for this turn. Missing signals: ${missingSignals.join('; ')}.
621
+
622
+ The orchestrator must run the context-loader for session "${sessionId}" before retrying. Expected context: harness_packet, memory_files, binding_plan.
623
+
624
+ This gate enforces that every action turn begins with Aria's substrate loaded — not improvised context. Per Aria-as-controller inversion doctrine (project_aria_as_controller_inversion.md): Aria must author with LLM as a tool, not the reverse.
625
+
626
+ Recovery: see hookSpecificOutput.recovery for the structured remediation path.`;
627
+
628
+ auditLog(
629
+ 'block-context-not-loaded',
630
+ `missing=[${missingSignals.join(', ')}] transcript=${JSON.stringify(transcriptSignals)} artifacts=${JSON.stringify(artifactSignals.detail)}`,
631
+ sessionId,
632
+ );
633
+
634
+ console.log(JSON.stringify({
635
+ decision: 'block',
636
+ reason: buildForceRedoActionReason({ source: 'preturn-memory/context-not-loaded', reason, missingSignals }),
637
+ hookSpecificOutput: {
638
+ hookEventName: 'PreToolUse',
639
+ recovery: {
640
+ action: 'run_context_loader',
641
+ target: sessionId,
642
+ expectedContext: ['harness_packet', 'memory_files', 'binding_plan'],
643
+ },
644
+ },
645
+ }));
646
+
647
+ process.exit(2);