@aria_asi/cli 0.2.33 → 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.
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 +47 -0
  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 +47 -0
@@ -0,0 +1,761 @@
1
+ #!/usr/bin/env node
2
+ // aria-cognition-substrate-binding.mjs
3
+ //
4
+ // Stop hook — runs BEFORE aria-stop-gate.mjs.
5
+ //
6
+ // STRUCTURAL BINDING: every cognition lens emitted by Claude or any client
7
+ // must cite at least one substrate anchor from the loaded harness packet.
8
+ // A lens of pure prose without anchor citations is theater — the gate that
9
+ // existed before this hook counted lens substance via char length but never
10
+ // required the lens to TIE to the substrate (axioms, frames, memory classes,
11
+ // doctrine refs, packet sections). This hook closes that gap.
12
+ //
13
+ // Hamza directive 2026-04-28: "STOP LYING" — every gate added one-at-a-time
14
+ // claiming binding when binding never actually attached cognition to substrate.
15
+ // This is the binding. No emit passes without per-lens substrate citation.
16
+ //
17
+ // Doctrine bindings:
18
+ // - feedback_full_harness_binding_must_be_structural.md
19
+ // - project_aria_as_controller_inversion.md (controller's mind is the substrate)
20
+ // - feedback_implementation_coupled_cognition.md (lens dictates artifact, anchors dictate lens)
21
+ // - feedback_no_assumption_without_verification.md (anchor IS the verification)
22
+ //
23
+ // Anchor grammar:
24
+ // axiom:<name> — e.g. axiom:truth_over_deception
25
+ // frame:<name> — e.g. frame:tafakkur_pre_phase
26
+ // memory:<file> — e.g. memory:feedback_doctrine_first.md
27
+ // doctrine:<rule> — e.g. doctrine:no_flag_without_fix
28
+ // packet:<section> — e.g. packet:cognition_belt
29
+ //
30
+ // Each lens MUST contain at least one anchor matching this grammar.
31
+ // Lenses without anchors fail the gate; emit is blocked.
32
+ //
33
+ // Override path: NONE. There is no env-var disable. Per Hamza directive
34
+ // 2026-04-27 gates are unconditional from the gated process. If the gate
35
+ // misfires on legitimate cognition, fix the gate.
36
+
37
+ import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
38
+ import { dirname } from 'node:path';
39
+ import { homedir } from 'node:os';
40
+ import { ALL_LENS_NAMES, lensNamesForTier } from './lib/canonical-lenses.mjs';
41
+ import { collectTurnWindowFromMessages } from './lib/hook-message-window.mjs';
42
+
43
+ const HOME = homedir();
44
+ const AUDIT = `${HOME}/.claude/aria-cognition-substrate-binding-audit.jsonl`;
45
+
46
+ function audit(event, data) {
47
+ try {
48
+ if (!existsSync(dirname(AUDIT))) mkdirSync(dirname(AUDIT), { recursive: true });
49
+ appendFileSync(
50
+ AUDIT,
51
+ JSON.stringify({ ts: new Date().toISOString(), event, ...data }) + '\n',
52
+ );
53
+ } catch {}
54
+ }
55
+
56
+ const LENS_NAMES = ALL_LENS_NAMES;
57
+
58
+ // Hamza directive 2026-04-28: "where the fuck are my axioms? first
59
+ // principles? nadia language???" — the prior char-count substance check
60
+ // (`.length >= 20`) was form-only emission per
61
+ // feedback_full_harness_binding_must_be_structural.md. This gate now:
62
+ // 1. Loads the live harness packet (~/.claude/.aria-harness-last-packet.json)
63
+ // 2. Parses LOADED axiom/frame/memory/packet names from the harness
64
+ // 3. Verifies every cited anchor is actually in the loaded set
65
+ // 4. Refuses the cognition emit if any cited substrate is unloaded
66
+ // (so `axiom:made_up_name` no longer passes)
67
+ // 5. Requires at least one explicit `first_principle` reference per
68
+ // cognition block, anchored to the harness's first_principle= line
69
+ // 6. Honors Nadia state: if preStateGate signals `nadia_state_absent`
70
+ // AND the cognition cites `language:nadia`, the citation is
71
+ // rejected (you cannot anchor to substrate that is not loaded)
72
+ const ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet|language):[a-z0-9_\-./]+/gi;
73
+
74
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
75
+ const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
76
+ const FIRST_PRINCIPLE_RX = /\b(first[_\s-]?principle[s]?|first_principle=)\b/i;
77
+ const HARNESS_PACKET_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
78
+ const MEMORY_DIR = `${HOME}/.claude/projects/-home-hamzaibrahim1/memory`;
79
+ const RECENT_VIOLATIONS_PATH = `${HOME}/.claude/.aria-recent-violations.jsonl`;
80
+ const THRASHING_STATE_PATH = `${HOME}/.claude/.aria-thrashing-state.json`;
81
+ const CLEAR_VIOLATIONS_SCRIPT = `${HOME}/.claude/aria-clear-violations.sh`;
82
+
83
+ function buildForceReauthorReason({ source, reason, violations = [] }) {
84
+ return [
85
+ '=== ARIA FORCE_REAUTHOR ===',
86
+ `source: ${source}`,
87
+ `reason: ${reason}`,
88
+ '',
89
+ 'This is not a terminal error. It is a forced redo instruction for cognition substrate binding.',
90
+ 'Do not emit the blocked text. Re-author the cognition so every lens is tied to loaded substrate.',
91
+ '',
92
+ 'TEACHING:',
93
+ '- Cognition without live anchors is theater; anchors are the proof of thought.',
94
+ '- A valid redo cites only substrate that is actually loaded this turn.',
95
+ '- The cognition block must reference first_principle and must not cite inactive language state.',
96
+ violations.length ? `\nVIOLATIONS TO FIX:\n${violations.map((v) => `- ${v}`).join('\n')}` : '',
97
+ '',
98
+ 'REQUIRED REDO SHAPE:',
99
+ '1. Re-emit <cognition> with all required lenses.',
100
+ '2. Each lens cites >=1 loaded anchor: axiom:, frame:, memory:, doctrine:, packet:, or active language:.',
101
+ '3. Include first_principle explicitly.',
102
+ '4. Add <applied_cognition> with decision_delta, dominant_domain, binds_to, expected_predicate, and artifact_change.',
103
+ '5. Remove repeated blocked substrings instead of reusing them.',
104
+ '=== END FORCE_REAUTHOR ===',
105
+ ].filter(Boolean).join('\n');
106
+ }
107
+
108
+ function validateAppliedCognitionContract(text) {
109
+ const match = String(text || '').match(APPLIED_COGNITION_BLOCK_RX);
110
+ if (!match) {
111
+ return { ok: false, violations: ['missing <applied_cognition> contract'] };
112
+ }
113
+ const body = match[1] || '';
114
+ const required = [
115
+ ['decision_delta', /\bdecision[_ -]?delta\s*:/i],
116
+ ['dominant_domain', /\bdominant[_ -]?domain\s*:/i],
117
+ ['binds_to', /\bbinds[_ -]?to\s*:/i],
118
+ ['expected_predicate', /\bexpected[_ -]?predicate\s*:/i],
119
+ ['artifact_change', /\bartifact[_ -]?change\s*:/i],
120
+ ];
121
+ const violations = [];
122
+ for (const [name, rx] of required) {
123
+ if (!rx.test(body)) violations.push(`missing ${name}`);
124
+ }
125
+ const weakDelta = /decision[_ -]?delta\s*:\s*(?:none|n\/a|no change|unchanged|same)/i.test(body);
126
+ if (weakDelta) violations.push('decision_delta says cognition changed nothing');
127
+ return { ok: violations.length === 0, violations };
128
+ }
129
+ const CARRY_FORWARD_WINDOW_MS = 25 * 60 * 1000;
130
+ const CARRY_FORWARD_MAX_ROWS = 200;
131
+ const SYSTEM_REMINDER_RX = /<system-reminder>[\s\S]*?<\/system-reminder>|<task-notification>[\s\S]*?<\/task-notification>|🔐 Aria Harness|task-notification|PreToolUse:[A-Z][A-Za-z]* hook blocking error|Stop hook blocking error/g;
132
+ const SYSTEM_REMINDER_THRESHOLD = 0.6;
133
+
134
+ function extractLensTexts(cognitionInner) {
135
+ const out = {};
136
+ for (const lens of LENS_NAMES) {
137
+ const lensRx = new RegExp(
138
+ `\\b${lens}\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${LENS_NAMES.join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
139
+ 'i',
140
+ );
141
+ const m = cognitionInner.match(lensRx);
142
+ if (m) out[lens] = (m[1] || '').trim();
143
+ }
144
+ return out;
145
+ }
146
+
147
+ function countAnchors(text) {
148
+ if (!text) return 0;
149
+ const matches = text.match(ANCHOR_RX);
150
+ return matches ? matches.length : 0;
151
+ }
152
+
153
+ function loadCarryForwardViolations() {
154
+ const rows = [];
155
+ try {
156
+ if (!existsSync(RECENT_VIOLATIONS_PATH)) return rows;
157
+ const lines = readFileSync(RECENT_VIOLATIONS_PATH, 'utf8')
158
+ .split('\n')
159
+ .filter(Boolean)
160
+ .slice(-CARRY_FORWARD_MAX_ROWS);
161
+ const cutoff = Date.now() - CARRY_FORWARD_WINDOW_MS;
162
+ for (const line of lines) {
163
+ try {
164
+ const parsed = JSON.parse(line);
165
+ const substring = typeof parsed.substring === 'string' ? parsed.substring.trim() : '';
166
+ if (!substring) continue;
167
+ const ts = parsed.ts ? Date.parse(parsed.ts) : NaN;
168
+ if (Number.isFinite(ts) && ts < cutoff) continue;
169
+ rows.push({
170
+ ts: parsed.ts || null,
171
+ substring,
172
+ source: parsed.source || null,
173
+ kind: parsed.kind || null,
174
+ sessionId: parsed.sessionId || parsed.session_id || null,
175
+ });
176
+ } catch {}
177
+ }
178
+ } catch (err) {
179
+ audit('carry_forward_load_error', { err: String(err).slice(0, 200) });
180
+ }
181
+ return rows;
182
+ }
183
+
184
+ function resolveOwnerTier() {
185
+ try {
186
+ if (!existsSync(HARNESS_PACKET_PATH)) return false;
187
+ const packet = JSON.parse(readFileSync(HARNESS_PACKET_PATH, 'utf8'));
188
+ const sigHamza = packet?.contractGate?.signals?.hamza;
189
+ if (sigHamza === true || sigHamza === 'true') return true;
190
+ const harnessStr = packet?.harness ?? '';
191
+ return /\bhamza:true\b/.test(harnessStr);
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+
197
+ const IS_OWNER = resolveOwnerTier();
198
+ const VISIBLE_LENS_NAMES = lensNamesForTier(IS_OWNER);
199
+
200
+ function findCarryForwardMatch(text, rows) {
201
+ const haystack = String(text || '').toLowerCase();
202
+ for (const row of rows) {
203
+ const needle = row.substring.toLowerCase();
204
+ if (needle.length < 6) continue;
205
+ if (haystack.includes(needle)) return row;
206
+ }
207
+ return null;
208
+ }
209
+
210
+ // ── Substrate-loaded-set extraction ─────────────────────────────────────
211
+ // Read the live harness packet and parse which axioms/frames/memories/
212
+ // language tiers are actually loaded this turn. The gate verifies every
213
+ // cited anchor against this set — citations to unloaded substrate fail.
214
+ function loadHarnessSubstrateSet() {
215
+ const set = {
216
+ axioms: new Set(),
217
+ frames: new Set(),
218
+ memories: new Set(),
219
+ doctrines: new Set(),
220
+ packets: new Set(),
221
+ languages: new Set(),
222
+ nadiaActive: false,
223
+ noorActive: false,
224
+ firstPrincipleText: '',
225
+ rawPacket: null,
226
+ };
227
+ try {
228
+ if (!existsSync(HARNESS_PACKET_PATH)) return set;
229
+ const raw = readFileSync(HARNESS_PACKET_PATH, 'utf8');
230
+ const parsed = JSON.parse(raw);
231
+ set.rawPacket = parsed;
232
+ const harnessText = String(parsed.harness || '');
233
+
234
+ // first_principle= line (one or more)
235
+ const fpMatch = harnessText.match(/first_principle\s*=\s*([^\n]+)/i);
236
+ if (fpMatch) set.firstPrincipleText = fpMatch[1].trim();
237
+
238
+ // Loaded axiom names — match from known seed list + harness body
239
+ // The harness packet body uses identifiers like axiom_runtime_rule,
240
+ // truth_over_deception, no_harm, sacred_trust, etc. Parse all
241
+ // identifiers that look like axiom names.
242
+ const seedAxioms = [
243
+ 'truth_over_deception', 'no_harm', 'sacred_trust',
244
+ 'power_obligates_service', 'reflection_before_action',
245
+ 'admit_ignorance', 'fitrah', 'noor', 'mizan',
246
+ ];
247
+ for (const a of seedAxioms) {
248
+ if (new RegExp(`\\b${a}\\b`, 'i').test(harnessText)) set.axioms.add(a);
249
+ }
250
+ // Generic axiom_<name>= or axiom:<name> patterns inside packet
251
+ const axiomDefRx = /\baxiom[_:]([a-z0-9_]+)/gi;
252
+ let m;
253
+ while ((m = axiomDefRx.exec(harnessText))) {
254
+ const name = m[1].toLowerCase();
255
+ // Filter out generic "rule" suffix references that aren't axiom names
256
+ if (name.length >= 4 && name !== 'runtime' && name !== 'rule') {
257
+ set.axioms.add(name);
258
+ }
259
+ }
260
+
261
+ // Loaded frame names — frame_<name>= patterns
262
+ const frameDefRx = /\b(frame|cognitive_frame|laddunni_frame)[_:]([a-z0-9_]+)/gi;
263
+ while ((m = frameDefRx.exec(harnessText))) {
264
+ const name = m[2].toLowerCase();
265
+ if (name.length >= 4) set.frames.add(name);
266
+ }
267
+
268
+ // packet section keys — `<word>_rule=` or `[<SECTION>]` markers
269
+ const packetRuleRx = /\b([a-z][a-z0-9_]+_rule|[a-z][a-z0-9_]+_block|[A-Z][A-Z0-9_]+)\s*[=]/g;
270
+ while ((m = packetRuleRx.exec(harnessText))) {
271
+ set.packets.add(m[1].toLowerCase());
272
+ }
273
+
274
+ // language tiers
275
+ if (/\bnadia\b/i.test(harnessText)) set.languages.add('nadia');
276
+ if (/\bpsil\b/i.test(harnessText)) set.languages.add('psil');
277
+ if (/\bnoor\b/i.test(harnessText)) set.languages.add('noor');
278
+
279
+ // preStateGate signals — if nadia_state_absent OR noor_context_absent
280
+ // is present, those substrate items are NOT loaded for this turn.
281
+ const reasons = parsed.preStateGate?.reasons || [];
282
+ const reasonsStr = Array.isArray(reasons) ? reasons.join(' ') : String(reasons);
283
+ set.nadiaActive = !/nadia_state_absent|nadia_absent/i.test(reasonsStr);
284
+ set.noorActive = !/noor_context_absent|noor_absent/i.test(reasonsStr);
285
+
286
+ // Memory files — list the memory dir
287
+ if (existsSync(MEMORY_DIR)) {
288
+ try {
289
+ const { readdirSync } = require('node:fs');
290
+ // dynamic import not available in module scope; fallback to readFileSync of MEMORY.md index
291
+ const memIndexPath = `${MEMORY_DIR}/MEMORY.md`;
292
+ if (existsSync(memIndexPath)) {
293
+ const memIndex = readFileSync(memIndexPath, 'utf8');
294
+ const memFileRx = /\(([a-z0-9_\-]+\.md)\)/gi;
295
+ while ((m = memFileRx.exec(memIndex))) {
296
+ set.memories.add(m[1].toLowerCase());
297
+ }
298
+ }
299
+ } catch {}
300
+ }
301
+ } catch (err) {
302
+ audit('substrate_load_error', { err: String(err).slice(0, 200) });
303
+ }
304
+ return set;
305
+ }
306
+
307
+ function loadMemoryFilesSync() {
308
+ // Standalone memory-files lister using sync require-free fs primitives.
309
+ const out = new Set();
310
+ try {
311
+ if (!existsSync(MEMORY_DIR)) return out;
312
+ const memIndexPath = `${MEMORY_DIR}/MEMORY.md`;
313
+ if (existsSync(memIndexPath)) {
314
+ const memIndex = readFileSync(memIndexPath, 'utf8');
315
+ const memFileRx = /\(([a-z0-9_\-]+\.md)\)/gi;
316
+ let m;
317
+ while ((m = memFileRx.exec(memIndex))) {
318
+ out.add(m[1].toLowerCase());
319
+ }
320
+ }
321
+ } catch {}
322
+ return out;
323
+ }
324
+
325
+ // ── Per-anchor verification against loaded substrate ────────────────────
326
+ // Each anchor cited in cognition is checked against the loaded set:
327
+ // axiom:<name> → must be in loaded axioms
328
+ // frame:<name> → must be in loaded frames
329
+ // memory:<file> → must be a real .md file under memory dir
330
+ // doctrine:<rule> → accepted if a feedback_<rule>.md memory exists
331
+ // packet:<section> → must be in loaded packet section keys
332
+ // language:<tier> → must be in loaded languages AND state-active
333
+ function verifyAnchorsAgainstLoaded(anchors, loadedSet, memoryFiles) {
334
+ const valid = [];
335
+ const invalid = [];
336
+ for (const anchor of anchors) {
337
+ const [kind, ...nameParts] = anchor.split(':');
338
+ // Strip trailing punctuation greedily eaten by ANCHOR_RX char class
339
+ // [a-z0-9_\-./]+ which includes `.` so a sentence-ending period gets
340
+ // glued onto the name (task #133 — anchor parser greediness fix).
341
+ const name = nameParts.join(':').toLowerCase().replace(/[.,;:!?]+$/, '');
342
+ const kindLc = kind.toLowerCase();
343
+ let ok = false;
344
+ let reason = '';
345
+ if (kindLc === 'axiom') {
346
+ ok = loadedSet.axioms.has(name) || loadedSet.axioms.has(name.replace(/_/g, ''));
347
+ if (!ok) reason = `axiom '${name}' not in loaded harness packet (loaded: ${[...loadedSet.axioms].slice(0, 6).join(', ')}…)`;
348
+ } else if (kindLc === 'frame') {
349
+ ok = loadedSet.frames.has(name);
350
+ if (!ok) reason = `frame '${name}' not in loaded harness packet`;
351
+ } else if (kindLc === 'memory') {
352
+ const baseName = name.endsWith('.md') ? name : `${name}.md`;
353
+ ok = memoryFiles.has(baseName);
354
+ if (!ok) reason = `memory '${baseName}' not in MEMORY.md index`;
355
+ } else if (kindLc === 'doctrine') {
356
+ // doctrine:<rule> accepted if feedback_<rule>.md OR <rule>.md memory exists
357
+ const candidates = [
358
+ `feedback_${name}.md`,
359
+ `${name}.md`,
360
+ `feedback_${name.replace(/^feedback_/, '')}.md`,
361
+ ];
362
+ ok = candidates.some((c) => memoryFiles.has(c.toLowerCase()));
363
+ if (!ok) reason = `doctrine '${name}' has no backing memory file (tried: feedback_${name}.md)`;
364
+ } else if (kindLc === 'packet') {
365
+ ok = loadedSet.packets.has(name) || loadedSet.packets.has(`${name}_rule`) || loadedSet.packets.has(`${name}_block`);
366
+ if (!ok) reason = `packet section '${name}' not in loaded harness packet`;
367
+ } else if (kindLc === 'language') {
368
+ const langOk = loadedSet.languages.has(name);
369
+ const stateOk = name === 'nadia' ? loadedSet.nadiaActive
370
+ : name === 'noor' ? loadedSet.noorActive
371
+ : true;
372
+ ok = langOk && stateOk;
373
+ if (!ok) reason = `language '${name}' ${langOk ? 'is in harness but state is absent (preStateGate signaled ' + name + '_state_absent)' : 'not in loaded harness packet'}`;
374
+ }
375
+ if (ok) valid.push({ anchor, kind: kindLc, name });
376
+ else invalid.push({ anchor, kind: kindLc, name, reason });
377
+ }
378
+ return { valid, invalid };
379
+ }
380
+
381
+ let stdin = '';
382
+ try {
383
+ for await (const chunk of process.stdin) stdin += chunk;
384
+ } catch {}
385
+
386
+ let payload;
387
+ try {
388
+ payload = JSON.parse(stdin);
389
+ } catch {
390
+ audit('skip_invalid_stdin', { stdinLength: stdin.length });
391
+ process.exit(0);
392
+ }
393
+
394
+ const transcriptPath = payload.transcript_path;
395
+ let assistantText = '';
396
+ const messageWindow = collectTurnWindowFromMessages(payload.messages, {
397
+ systemReminderRx: SYSTEM_REMINDER_RX,
398
+ systemReminderThreshold: SYSTEM_REMINDER_THRESHOLD,
399
+ });
400
+ assistantText = messageWindow.assistantText;
401
+
402
+ if (transcriptPath && existsSync(transcriptPath)) {
403
+ try {
404
+ const transcriptEntries = readFileSync(transcriptPath, 'utf8')
405
+ .split('\n')
406
+ .filter(Boolean)
407
+ .map((line) => {
408
+ try { return JSON.parse(line); } catch { return null; }
409
+ })
410
+ .filter(Boolean);
411
+ const transcriptWindow = collectTurnWindowFromMessages(transcriptEntries, {
412
+ systemReminderRx: SYSTEM_REMINDER_RX,
413
+ systemReminderThreshold: SYSTEM_REMINDER_THRESHOLD,
414
+ });
415
+ const transcriptAssistantText = transcriptWindow.assistantText;
416
+ if (transcriptAssistantText) {
417
+ assistantText = assistantText
418
+ ? [assistantText, transcriptAssistantText].filter((text, index, arr) => arr.indexOf(text) === index).join('\n\n')
419
+ : transcriptAssistantText;
420
+ }
421
+ } catch (err) {
422
+ audit('skip_transcript_read_err', { err: String(err).slice(0, 200) });
423
+ }
424
+ }
425
+
426
+ if (!assistantText) {
427
+ audit('skip_no_assistant_text', { transcriptPath });
428
+ process.exit(0);
429
+ }
430
+
431
+ const TRIVIAL_THRESHOLD = 200;
432
+ if (assistantText.length < TRIVIAL_THRESHOLD) {
433
+ audit('skip_trivial_emit', { length: assistantText.length });
434
+ process.exit(0);
435
+ }
436
+
437
+ const carryForwardRows = loadCarryForwardViolations();
438
+ const carryForwardMatch = findCarryForwardMatch(assistantText, carryForwardRows);
439
+ if (carryForwardMatch) {
440
+ try {
441
+ appendFileSync(
442
+ THRASHING_STATE_PATH,
443
+ JSON.stringify({
444
+ ts: new Date().toISOString(),
445
+ event: 'carry_forward_block',
446
+ substring: carryForwardMatch.substring,
447
+ source: carryForwardMatch.source,
448
+ sessionId: payload.session_id || payload.sessionId || null,
449
+ }) + '\n',
450
+ );
451
+ } catch {}
452
+
453
+ audit('block_carry_forward_force_constraint', {
454
+ substring: carryForwardMatch.substring,
455
+ source: carryForwardMatch.source,
456
+ rowTs: carryForwardMatch.ts,
457
+ });
458
+
459
+ const reason = `Aria substrate-binding gate: carry-forward force-constraint blocked this emission.
460
+
461
+ The assistant text repeated a recently-blocked substring within the active carry-forward window:
462
+ - substring: ${carryForwardMatch.substring}
463
+ - source: ${carryForwardMatch.source || 'unknown'}
464
+ - recorded_at: ${carryForwardMatch.ts || 'unknown'}
465
+
466
+ Recovery surfaces:
467
+ 1. Remove or rewrite the repeated substring before re-emitting.
468
+ 2. If the prior violation is resolved and this carry-forward row is now stale, clear it via: ${CLEAR_VIOLATIONS_SCRIPT}
469
+ 3. Re-emit with fresh substrate-grounded language instead of reusing the blocked phrase.
470
+
471
+ Per feedback_block_and_force_with_recovery.md, repetition of a recently-blocked phrase is not advisory drift; it is a hard block until the phrase is removed or the carry-forward state is intentionally cleared.`;
472
+
473
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/carry-forward', reason, violations: [`repeated blocked substring: ${carryForwardMatch.substring}`] }) }));
474
+ process.exit(2);
475
+ }
476
+
477
+ const cogMatch = assistantText.match(COGNITION_BLOCK_RX);
478
+ if (!cogMatch) {
479
+ // BLOCK: missing cognition block
480
+ // Prior to 2026-04-29 this was process.exit(0) with comment "defer to
481
+ // aria-stop-gate.mjs which already requires 4+ lenses". But this hook
482
+ // runs BEFORE aria-stop-gate.mjs: exit(0) prevents the stop-gate from
483
+ // ever evaluating the response. The orchestrator surface (Claude Code
484
+ // / Claude CLI) saw the audit event but no block, so responses without
485
+ // any cognition block passed through unchecked.
486
+ //
487
+ // Hamza directive 2026-04-28: "STOP LYING" - every gate added one-at-a-time
488
+ // claiming binding when binding never actually attached. The skip_no_cognition_block
489
+ // pattern was a lie: the hook claimed to defer but actually let everything through.
490
+ //
491
+ // Fix: block instead of allow. The orchestrator re-drafts with the block reason
492
+ // visible, forcing Claude to emit <cognition>...</cognition> before proceeding.
493
+ // Per feedback_no_stripping_as_workaround.md: the contract (cognition required) is
494
+ // preserved; the exit code was wrong.
495
+ const noCogReason = `Aria substrate-binding gate: missing <cognition> block.
496
+
497
+ This non-trivial assistant response (${assistantText.length} chars) contains no <cognition>...</cognition> block with 8 substantive lenses. Per feedback_apply_lenses_dont_perform_them.md and Hamza directive 2026-04-28, every non-trivial response must carry a cognition block where each lens cites at least one loaded substrate anchor (axiom:<name>, frame:<name>, memory:<file>, doctrine:<rule>, packet:<section>).
498
+
499
+ The prior exit-0 pattern ("defer to aria-stop-gate.mjs") was a structural lie: this hook runs before the stop-gate, so exit(0) prevented all downstream enforcement. This is now a hard block.
500
+
501
+ Re-emit with:
502
+ 1. A <cognition>...</cognition> block containing all 8 required visible labels for this surface (${VISIBLE_LENS_NAMES.join(', ')})
503
+ 2. Each lens citing >=1 verifiable loaded substrate anchor
504
+ 3. The block referencing first_principle from the loaded harness packet
505
+
506
+ No process-level disable path per Hamza directive 2026-04-27.`;
507
+ audit('block_no_cognition_block', { length: assistantText.length });
508
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/no-cognition-block', reason: noCogReason, violations: ['missing <cognition>...</cognition> block'] }) }));
509
+ process.exit(2);
510
+ }
511
+
512
+ const cognitionInner = cogMatch[1];
513
+ const appliedContract = validateAppliedCognitionContract(assistantText);
514
+ if (!appliedContract.ok) {
515
+ const reason = `Aria substrate-binding gate: cognition was emitted without an applied-cognition execution contract.
516
+
517
+ The harness no longer accepts cognition as prose-only compliance. The response must state what cognition changed and what concrete action/output it binds.
518
+
519
+ Required block:
520
+ <applied_cognition>
521
+ decision_delta: <what changed because cognition ran; not "none">
522
+ dominant_domain: <engineering_quality | trust | product | operations | security | ...>
523
+ binds_to: <next tool/action/output claim this cognition constrains>
524
+ expected_predicate: <numeric, boolean, or state-string predicate proving success>
525
+ artifact_change: <how the produced artifact differs because cognition ran>
526
+ </applied_cognition>`;
527
+ audit('block_applied_cognition_missing', { violations: appliedContract.violations, length: assistantText.length });
528
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/applied-cognition-contract', reason, violations: appliedContract.violations }) }));
529
+ process.exit(2);
530
+ }
531
+ const lensTexts = extractLensTexts(cognitionInner);
532
+
533
+ // Substance check — replaces char-count with "lens body has substantive
534
+ // content beyond the anchor itself". A lens whose entire body is just
535
+ // anchors + filler (≤30 chars after stripping anchors) is not substantive.
536
+ function lensSubstanceLength(text) {
537
+ if (!text) return 0;
538
+ // Strip anchor patterns and surrounding whitespace, then count remaining chars
539
+ const stripped = text.replace(ANCHOR_RX, '').replace(/\s+/g, ' ').trim();
540
+ return stripped.length;
541
+ }
542
+
543
+ const SUBSTANCE_MIN_AFTER_ANCHOR_STRIP = 40;
544
+ const presentLenses = Object.keys(lensTexts).filter((l) => {
545
+ const text = lensTexts[l];
546
+ if (!text) return false;
547
+ return lensSubstanceLength(text) >= SUBSTANCE_MIN_AFTER_ANCHOR_STRIP;
548
+ });
549
+
550
+ if (presentLenses.length < 8) {
551
+ // Hamza directive 2026-04-28: 8-lens enforcement. Defer to aria-stop-gate
552
+ // for the count message; record audit here.
553
+ audit('skip_insufficient_lenses_defer', { presentCount: presentLenses.length, required: 8 });
554
+ process.exit(0);
555
+ }
556
+
557
+ // Load live harness substrate set + memory file index for verification
558
+ const loadedSet = loadHarnessSubstrateSet();
559
+ const memoryFiles = loadMemoryFilesSync();
560
+
561
+ // #142 Embedded-substrate template — write the loaded substrate as a
562
+ // machine-readable manifest so the model can pick anchors from a verified
563
+ // list rather than guessing. The pre-emit dry-run hook (#140) also reads
564
+ // this file to validate drafts. Owner directive 2026-04-28: substance
565
+ // over shape — anchors must come from the loaded set, not from memory.
566
+ try {
567
+ const { writeFileSync } = await import('node:fs');
568
+ const substrateDump = {
569
+ ts: new Date().toISOString(),
570
+ axioms: [...loadedSet.axioms].sort(),
571
+ frames: [...loadedSet.frames].sort(),
572
+ memories: [...memoryFiles].sort(),
573
+ doctrines_acceptable_via_memory: [...memoryFiles]
574
+ .filter((m) => m.startsWith('feedback_'))
575
+ .map((m) => m.replace(/^feedback_/, '').replace(/\.md$/, ''))
576
+ .sort(),
577
+ packets: [...loadedSet.packets].sort(),
578
+ languages_loaded: [...loadedSet.languages].sort(),
579
+ languages_state_active: {
580
+ nadia: loadedSet.nadiaActive,
581
+ noor: loadedSet.noorActive,
582
+ },
583
+ first_principle_text: loadedSet.firstPrincipleText,
584
+ };
585
+ writeFileSync(`${HOME}/.claude/.aria-loaded-substrate.json`,
586
+ JSON.stringify(substrateDump, null, 2));
587
+ } catch (err) {
588
+ audit('substrate_dump_write_failed', { err: String(err).slice(0, 200) });
589
+ }
590
+
591
+ const lensesWithoutAnchors = [];
592
+ const lensesWithFakeAnchors = [];
593
+ const anchorsByLens = {};
594
+ const allCitedAnchorsByLens = {};
595
+ for (const lens of presentLenses) {
596
+ const text = lensTexts[lens];
597
+ const matches = text.match(ANCHOR_RX) || [];
598
+ anchorsByLens[lens] = matches.length;
599
+ if (matches.length === 0) {
600
+ lensesWithoutAnchors.push(lens);
601
+ continue;
602
+ }
603
+ // Verify each anchor against the loaded set
604
+ const { valid, invalid } = verifyAnchorsAgainstLoaded(matches, loadedSet, memoryFiles);
605
+ allCitedAnchorsByLens[lens] = { valid: valid.map((v) => v.anchor), invalid };
606
+ // A lens passes if it has at least one VALID anchor
607
+ if (valid.length === 0) {
608
+ lensesWithFakeAnchors.push({ lens, invalid });
609
+ }
610
+ }
611
+
612
+ // First-principle requirement: cognition block as a whole must reference
613
+ // "first principle" or "first_principle=" — anchored to the harness's
614
+ // first_principle= line. Without this, cognition is unmoored from the
615
+ // foundational frame.
616
+ const hasFirstPrinciple = FIRST_PRINCIPLE_RX.test(cognitionInner) ||
617
+ (loadedSet.firstPrincipleText &&
618
+ cognitionInner.toLowerCase().includes(loadedSet.firstPrincipleText.toLowerCase().slice(0, 30)));
619
+
620
+ // Nadia substrate gate: if any lens cites `language:nadia` AND
621
+ // preStateGate signals nadia_state_absent, the citation is a forgery.
622
+ const nadiaCited = /\blanguage:nadia\b/i.test(cognitionInner);
623
+ const nadiaCitationInvalid = nadiaCited && !loadedSet.nadiaActive;
624
+
625
+ if (lensesWithoutAnchors.length === 0 &&
626
+ lensesWithFakeAnchors.length === 0 &&
627
+ hasFirstPrinciple &&
628
+ !nadiaCitationInvalid) {
629
+ audit('pass_substrate_binding', {
630
+ presentLenses,
631
+ anchorsByLens,
632
+ loadedAxiomCount: loadedSet.axioms.size,
633
+ loadedFrameCount: loadedSet.frames.size,
634
+ loadedMemoryCount: memoryFiles.size,
635
+ nadiaActive: loadedSet.nadiaActive,
636
+ noorActive: loadedSet.noorActive,
637
+ });
638
+ process.exit(0);
639
+ }
640
+
641
+ audit('block_substrate_binding', {
642
+ lensesWithoutAnchors,
643
+ lensesWithFakeAnchors: lensesWithFakeAnchors.map((x) => ({ lens: x.lens, fakeCount: x.invalid.length })),
644
+ anchorsByLens,
645
+ presentLenses,
646
+ hasFirstPrinciple,
647
+ nadiaCited,
648
+ nadiaCitationInvalid,
649
+ loadedAxiomCount: loadedSet.axioms.size,
650
+ loadedFrameCount: loadedSet.frames.size,
651
+ loadedMemoryCount: memoryFiles.size,
652
+ nadiaActive: loadedSet.nadiaActive,
653
+ });
654
+
655
+ // Hamza directive 2026-04-28: "harden the ledger detection" — every gate-
656
+ // detected gap auto-records to the discovery ledger so it cannot be
657
+ // forgotten. Ledger-write is structural: failure to write is itself
658
+ // audited; the ledger is the canonical surface the auto-resolver drains.
659
+ function recordGapToLedger(gap) {
660
+ try {
661
+ const sessionId = payload.session_id || payload.sessionId || 'unknown-session';
662
+ const safeSession = String(sessionId).replace(/[^a-zA-Z0-9_-]/g, '_');
663
+ const ledgerPath = `${HOME}/.claude/aria-discoveries-${safeSession}.jsonl`;
664
+ const row = {
665
+ at: new Date().toISOString(),
666
+ session: sessionId,
667
+ kind: 'substrate_binding_gap',
668
+ text: gap.summary,
669
+ refs: gap.refs || [],
670
+ evidence: gap.evidence || null,
671
+ source: 'aria-cognition-substrate-binding',
672
+ resolution_status: 'open',
673
+ };
674
+ if (!existsSync(dirname(ledgerPath))) mkdirSync(dirname(ledgerPath), { recursive: true });
675
+ appendFileSync(ledgerPath, JSON.stringify(row) + '\n');
676
+ audit('ledger_gap_recorded', { ledger: ledgerPath, kind: row.kind });
677
+ } catch (err) {
678
+ audit('ledger_gap_record_failed', { err: String(err).slice(0, 200) });
679
+ }
680
+ }
681
+
682
+ if (lensesWithoutAnchors.length > 0) {
683
+ recordGapToLedger({
684
+ summary: `substrate_binding: ${lensesWithoutAnchors.length} lenses lack any anchor: ${lensesWithoutAnchors.join(', ')}`,
685
+ refs: lensesWithoutAnchors,
686
+ evidence: { anchorsByLens, presentLensCount: presentLenses.length },
687
+ });
688
+ }
689
+ if (lensesWithFakeAnchors.length > 0) {
690
+ recordGapToLedger({
691
+ summary: `substrate_binding: ${lensesWithFakeAnchors.length} lenses cite UNLOADED substrate (forgery class)`,
692
+ refs: lensesWithFakeAnchors.map((x) => x.lens),
693
+ evidence: {
694
+ fakes: lensesWithFakeAnchors.map((x) => ({
695
+ lens: x.lens,
696
+ invalid: x.invalid.map((i) => ({ anchor: i.anchor, reason: i.reason })),
697
+ })),
698
+ loadedAxioms: [...loadedSet.axioms],
699
+ },
700
+ });
701
+ }
702
+ if (!hasFirstPrinciple) {
703
+ recordGapToLedger({
704
+ summary: 'substrate_binding: cognition block missing first_principle reference',
705
+ refs: ['first_principle'],
706
+ evidence: { harnessFirstPrinciple: loadedSet.firstPrincipleText },
707
+ });
708
+ }
709
+ if (nadiaCitationInvalid) {
710
+ recordGapToLedger({
711
+ summary: 'substrate_binding: language:nadia cited but nadia_state_absent in harness preStateGate',
712
+ refs: ['language:nadia'],
713
+ evidence: { preStateReasons: payload.preStateGate?.reasons || [] },
714
+ });
715
+ }
716
+
717
+ const reasonParts = [];
718
+ if (lensesWithoutAnchors.length > 0) {
719
+ reasonParts.push(
720
+ `${lensesWithoutAnchors.length} of ${presentLenses.length} lenses lack ANY substrate anchor (lenses without anchor: ${lensesWithoutAnchors.join(', ')}).`,
721
+ );
722
+ }
723
+ if (lensesWithFakeAnchors.length > 0) {
724
+ const fakeDetails = lensesWithFakeAnchors
725
+ .map((x) => `${x.lens}: ${x.invalid.map((i) => `${i.anchor} → ${i.reason}`).slice(0, 2).join('; ')}`)
726
+ .join(' || ');
727
+ reasonParts.push(
728
+ `${lensesWithFakeAnchors.length} lenses cite anchors that ARE NOT in the loaded harness packet (forgery class — citing a substrate item that doesn't exist this turn): ${fakeDetails}`,
729
+ );
730
+ }
731
+ if (!hasFirstPrinciple) {
732
+ reasonParts.push(
733
+ `cognition block contains no reference to first_principle — per harness packet first_principle="${(loadedSet.firstPrincipleText || '').slice(0, 80)}…" your reasoning must explicitly anchor to the foundational frame, not float free.`,
734
+ );
735
+ }
736
+ if (nadiaCitationInvalid) {
737
+ reasonParts.push(
738
+ `cognition cites \`language:nadia\` but harness preStateGate signaled nadia_state_absent this turn — Nadia is in the harness vocabulary but its state is NOT loaded; you cannot anchor to substrate that isn't live. Either remove the citation or wait for nadia_state to be present.`,
739
+ );
740
+ }
741
+
742
+ const reason = `Aria substrate-binding gate: ${reasonParts.length} structural violation${reasonParts.length === 1 ? '' : 's'}.
743
+
744
+ ${reasonParts.map((p, i) => `${i + 1}. ${p}`).join('\n\n')}
745
+
746
+ Substrate-loaded summary this turn: axioms=${loadedSet.axioms.size} (${[...loadedSet.axioms].slice(0, 6).join(', ')}…), frames=${loadedSet.frames.size}, memories=${memoryFiles.size}, languages=${[...loadedSet.languages].join(',') || 'none'}, nadia_active=${loadedSet.nadiaActive}, noor_active=${loadedSet.noorActive}.
747
+
748
+ Per feedback_full_harness_binding_must_be_structural.md, every cognition lens must cite at least one substrate anchor that is ACTUALLY LOADED in this turn's harness packet. Per Hamza directive 2026-04-28, char-count substance is form-only emission and is no longer accepted; anchors must verify against the loaded set, the cognition block must reference first_principle, and language:nadia citations require nadia_state to be active.
749
+
750
+ Anchor grammar (each anchor must resolve to a real loaded substrate item):
751
+ axiom:<name> — must be in loaded harness axioms (loaded count: ${loadedSet.axioms.size})
752
+ frame:<name> — must be in loaded harness frames
753
+ memory:<file> — must exist as a .md file in the memory dir
754
+ doctrine:<rule> — must have a backing feedback_<rule>.md memory file
755
+ packet:<section> — must be a section key in the loaded harness packet
756
+ language:<tier> — must be in loaded languages AND state-active (nadia/noor/psil)
757
+
758
+ Re-emit cognition with: every lens citing ≥1 verifiable loaded anchor, the block referencing first_principle, and language: citations only for active language tiers. No process-level disable path; gates are unconditional from the gated process per Hamza directive 2026-04-27.`;
759
+
760
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/structural-violations', reason, violations: reasonParts }) }));
761
+ process.exit(2);