@aria_asi/cli 0.2.30 → 0.2.32

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 (95) hide show
  1. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/claude-code.js +115 -20
  3. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  4. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.js +551 -11
  6. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
  8. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
  9. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
  10. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
  11. package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
  12. package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
  13. package/dist/aria-connector/src/connectors/must-read.js +115 -0
  14. package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
  15. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  16. package/dist/aria-connector/src/connectors/opencode.js +27 -9
  17. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  18. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  19. package/dist/aria-connector/src/connectors/runtime.js +231 -19
  20. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  21. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  22. package/dist/aria-connector/src/connectors/shell.js +76 -3
  23. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  24. package/dist/assets/hooks/aria-agent-handoff.mjs +23 -0
  25. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +121 -28
  26. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  27. package/dist/assets/hooks/aria-pre-emit-dryrun.mjs +35 -0
  28. package/dist/assets/hooks/aria-pre-tool-gate.mjs +383 -93
  29. package/dist/assets/hooks/aria-preprompt-consult.mjs +28 -2
  30. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +93 -16
  31. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +33 -1
  32. package/dist/assets/hooks/aria-stop-gate.mjs +346 -81
  33. package/dist/assets/hooks/doctrine_trigger_map.json +55 -0
  34. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  35. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  36. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  37. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  38. package/dist/assets/opencode-plugins/harness-gate/index.js +40 -5
  39. package/dist/assets/opencode-plugins/harness-stop/index.js +133 -10
  40. package/dist/runtime/auth-middleware.mjs +251 -0
  41. package/dist/runtime/codex-bridge.mjs +644 -0
  42. package/dist/runtime/discipline/CLAUDE.md +28 -0
  43. package/dist/runtime/discipline/doctrine_trigger_map.json +534 -0
  44. package/dist/runtime/doctrine_trigger_map.json +534 -0
  45. package/dist/runtime/fleet-engine.mjs +231 -0
  46. package/dist/runtime/harness-daemon.mjs +460 -0
  47. package/dist/runtime/manifest.json +1 -1
  48. package/dist/runtime/metering.mjs +100 -0
  49. package/dist/runtime/onboarding-engine.mjs +89 -0
  50. package/dist/runtime/plugin-engine.mjs +196 -0
  51. package/dist/runtime/sdk/BUNDLED.json +1 -1
  52. package/dist/runtime/sdk/index.d.ts +12 -0
  53. package/dist/runtime/sdk/index.js +120 -14
  54. package/dist/runtime/sdk/index.js.map +1 -1
  55. package/dist/runtime/service.mjs +1140 -48
  56. package/dist/runtime/workflow-engine.mjs +322 -0
  57. package/dist/sdk/BUNDLED.json +1 -1
  58. package/dist/sdk/index.d.ts +12 -0
  59. package/dist/sdk/index.js +120 -14
  60. package/dist/sdk/index.js.map +1 -1
  61. package/hooks/aria-agent-handoff.mjs +23 -0
  62. package/hooks/aria-cognition-substrate-binding.mjs +121 -28
  63. package/hooks/aria-harness-via-sdk.mjs +126 -12
  64. package/hooks/aria-pre-emit-dryrun.mjs +35 -0
  65. package/hooks/aria-pre-tool-gate.mjs +383 -93
  66. package/hooks/aria-preprompt-consult.mjs +28 -2
  67. package/hooks/aria-preturn-memory-gate.mjs +93 -16
  68. package/hooks/aria-repo-doctrine-gate.mjs +33 -1
  69. package/hooks/aria-stop-gate.mjs +346 -81
  70. package/hooks/doctrine_trigger_map.json +55 -0
  71. package/hooks/lib/canonical-lenses.mjs +6 -5
  72. package/hooks/lib/gate-loop-state.mjs +50 -0
  73. package/hooks/lib/hook-message-window.mjs +121 -0
  74. package/hooks/test-tier-lens-labeling.mjs +26 -58
  75. package/opencode-plugins/harness-gate/index.js +40 -5
  76. package/opencode-plugins/harness-stop/index.js +133 -10
  77. package/package.json +1 -1
  78. package/runtime-src/auth-middleware.mjs +251 -0
  79. package/runtime-src/codex-bridge.mjs +644 -0
  80. package/runtime-src/fleet-engine.mjs +231 -0
  81. package/runtime-src/harness-daemon.mjs +460 -0
  82. package/runtime-src/metering.mjs +100 -0
  83. package/runtime-src/onboarding-engine.mjs +89 -0
  84. package/runtime-src/plugin-engine.mjs +196 -0
  85. package/runtime-src/service.mjs +1140 -48
  86. package/runtime-src/workflow-engine.mjs +322 -0
  87. package/scripts/bundle-sdk.mjs +5 -0
  88. package/src/connectors/claude-code.ts +126 -20
  89. package/src/connectors/codex.ts +559 -10
  90. package/src/connectors/doctrine-trigger-map.ts +112 -0
  91. package/src/connectors/must-read.ts +117 -0
  92. package/src/connectors/opencode.ts +28 -9
  93. package/src/connectors/runtime.ts +241 -21
  94. package/src/connectors/shell.ts +78 -3
  95. package/dist/cli-0.2.0.tgz +0 -0
@@ -37,7 +37,8 @@
37
37
  import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
38
38
  import { dirname } from 'node:path';
39
39
  import { homedir } from 'node:os';
40
- import { ALL_LENS_NAMES } from './lib/canonical-lenses.mjs';
40
+ import { ALL_LENS_NAMES, lensNamesForTier } from './lib/canonical-lenses.mjs';
41
+ import { collectTurnWindowFromMessages } from './lib/hook-message-window.mjs';
41
42
 
42
43
  const HOME = homedir();
43
44
  const AUDIT = `${HOME}/.claude/aria-cognition-substrate-binding-audit.jsonl`;
@@ -71,14 +72,64 @@ const LENS_NAMES = ALL_LENS_NAMES;
71
72
  const ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet|language):[a-z0-9_\-./]+/gi;
72
73
 
73
74
  const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
75
+ const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
74
76
  const FIRST_PRINCIPLE_RX = /\b(first[_\s-]?principle[s]?|first_principle=)\b/i;
75
77
  const HARNESS_PACKET_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
76
78
  const MEMORY_DIR = `${HOME}/.claude/projects/-home-hamzaibrahim1/memory`;
77
79
  const RECENT_VIOLATIONS_PATH = `${HOME}/.claude/.aria-recent-violations.jsonl`;
78
80
  const THRASHING_STATE_PATH = `${HOME}/.claude/.aria-thrashing-state.json`;
79
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
+ }
80
129
  const CARRY_FORWARD_WINDOW_MS = 25 * 60 * 1000;
81
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;
82
133
 
83
134
  function extractLensTexts(cognitionInner) {
84
135
  const out = {};
@@ -130,6 +181,22 @@ function loadCarryForwardViolations() {
130
181
  return rows;
131
182
  }
132
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
+
133
200
  function findCarryForwardMatch(text, rows) {
134
201
  const haystack = String(text || '').toLowerCase();
135
202
  for (const row of rows) {
@@ -325,31 +392,39 @@ try {
325
392
  }
326
393
 
327
394
  const transcriptPath = payload.transcript_path;
328
- if (!transcriptPath || !existsSync(transcriptPath)) {
329
- audit('skip_no_transcript', { transcriptPath });
330
- process.exit(0);
331
- }
332
-
333
395
  let assistantText = '';
334
- try {
335
- const transcript = readFileSync(transcriptPath, 'utf8');
336
- const lines = transcript.split('\n').filter(Boolean);
337
- for (let i = lines.length - 1; i >= 0; i--) {
338
- try {
339
- const entry = JSON.parse(lines[i]);
340
- if (entry.type === 'assistant' && entry.message && entry.message.content) {
341
- const blocks = Array.isArray(entry.message.content) ? entry.message.content : [];
342
- for (const b of blocks) {
343
- if (b.type === 'text' && typeof b.text === 'string') {
344
- assistantText = b.text + '\n' + assistantText;
345
- }
346
- }
347
- if (assistantText) break;
348
- }
349
- } catch {}
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) });
350
423
  }
351
- } catch (err) {
352
- audit('skip_transcript_read_err', { err: String(err).slice(0, 200) });
424
+ }
425
+
426
+ if (!assistantText) {
427
+ audit('skip_no_assistant_text', { transcriptPath });
353
428
  process.exit(0);
354
429
  }
355
430
 
@@ -395,7 +470,7 @@ Recovery surfaces:
395
470
 
396
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.`;
397
472
 
398
- console.log(JSON.stringify({ decision: 'block', reason }));
473
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/carry-forward', reason, violations: [`repeated blocked substring: ${carryForwardMatch.substring}`] }) }));
399
474
  process.exit(2);
400
475
  }
401
476
 
@@ -424,17 +499,35 @@ This non-trivial assistant response (${assistantText.length} chars) contains no
424
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.
425
500
 
426
501
  Re-emit with:
427
- 1. A <cognition>...</cognition> block containing all 8 canonical lenses (nur, mizan, hikma, tafakkur, tadabbur, ilham, wahi, firasah)
502
+ 1. A <cognition>...</cognition> block containing all 8 required visible labels for this surface (${VISIBLE_LENS_NAMES.join(', ')})
428
503
  2. Each lens citing >=1 verifiable loaded substrate anchor
429
504
  3. The block referencing first_principle from the loaded harness packet
430
505
 
431
506
  No process-level disable path per Hamza directive 2026-04-27.`;
432
507
  audit('block_no_cognition_block', { length: assistantText.length });
433
- console.log(JSON.stringify({ decision: 'block', reason: noCogReason }));
508
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/no-cognition-block', reason: noCogReason, violations: ['missing <cognition>...</cognition> block'] }) }));
434
509
  process.exit(2);
435
510
  }
436
511
 
437
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
+ }
438
531
  const lensTexts = extractLensTexts(cognitionInner);
439
532
 
440
533
  // Substance check — replaces char-count with "lens body has substantive
@@ -664,5 +757,5 @@ Anchor grammar (each anchor must resolve to a real loaded substrate item):
664
757
 
665
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.`;
666
759
 
667
- console.log(JSON.stringify({ decision: 'block', reason }));
760
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceReauthorReason({ source: 'substrate-binding/structural-violations', reason, violations: reasonParts }) }));
668
761
  process.exit(2);
@@ -26,7 +26,10 @@ const LOG_FILE = `${HOME}/.claude/aria-harness-history.log`;
26
26
  const LAST_URL_CACHE = `${HOME}/.claude/.aria-harness-last-url`;
27
27
  const PACKET_CACHE = `${HOME}/.claude/.aria-harness-last-packet.json`;
28
28
  const SHARED_PACKET_CACHE = `${HOME}/.aria/.aria-harness-last-packet.json`;
29
+ const MIZAN_RECEIPT_DIR = `${HOME}/.claude/.aria-mizan-receipts`;
29
30
  const HEADER_PREFIX = '🔐 Aria Harness';
31
+ const DEFAULT_RUNTIME_URL = process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319';
32
+ const RUNTIME_PACKET_SUFFIX = '/packet';
30
33
 
31
34
  const args = process.argv.slice(2);
32
35
  const MODE = args.includes('--mode') ? args[args.indexOf('--mode') + 1] : 'session';
@@ -133,6 +136,8 @@ function buildUrlList() {
133
136
  const list = [];
134
137
  const cached = loadCachedUrl();
135
138
  if (cached) list.push(cached);
139
+ if (process.env.ARIA_RUNTIME_URL) list.push(process.env.ARIA_RUNTIME_URL.replace(/\/+$/, ''));
140
+ list.push(DEFAULT_RUNTIME_URL.replace(/\/+$/, ''));
136
141
  if (process.env.ARIA_HIVE_RUNTIME_URL) list.push(process.env.ARIA_HIVE_RUNTIME_URL.replace(/\/+$/, ''));
137
142
  if (process.env.ARIA_HARNESS_BASE_URL) list.push(process.env.ARIA_HARNESS_BASE_URL.replace(/\/+$/, ''));
138
143
  if (process.env.ARIA_HARNESS_URL) list.push(process.env.ARIA_HARNESS_URL.replace(/\/+$/, ''));
@@ -155,6 +160,21 @@ function buildUrlList() {
155
160
  return list.filter((u) => (seen.has(u) ? false : (seen.add(u), true)));
156
161
  }
157
162
 
163
+ function isMountedRuntimeUrl(baseUrl) {
164
+ return /:\/\/(?:127\.0\.0\.1|localhost):4319$/i.test(String(baseUrl || '').replace(/\/+$/, ''));
165
+ }
166
+
167
+ function normalizeHarnessPacketPayload(payload) {
168
+ let current = payload;
169
+ for (let depth = 0; depth < 3; depth++) {
170
+ if (!current || typeof current !== 'object' || Array.isArray(current)) break;
171
+ if (!('packet' in current) || !current.packet || typeof current.packet !== 'object') break;
172
+ if (!('timestamp' in current) && !('version' in current)) break;
173
+ current = current.packet;
174
+ }
175
+ return current;
176
+ }
177
+
158
178
  // SDK loader — dynamic-import the bundled HTTPHarnessClient from the shared
159
179
  // ~/.aria/sdk bundle first, then client-local bundles. Module-cached after
160
180
  // first load so we don't repeatedly read disk.
@@ -204,6 +224,7 @@ async function loadSdkClass() {
204
224
  // before forwarding to avoid blowing the POST body size limit; the most recent
205
225
  // messages are the most relevant for history-count purposes.
206
226
  let HOOK_EVENT_MESSAGES = undefined;
227
+ let HOOK_EVENT = null;
207
228
  try {
208
229
  // Claude Code hooks receive the event JSON on stdin. We read it synchronously
209
230
  // only if data is already available (i.e. piped); we don't block waiting for
@@ -211,6 +232,7 @@ try {
211
232
  // a terminal (e.g. manual invocation for testing).
212
233
  const stdinBuf = readFileSync('/dev/stdin', { flag: 'r' });
213
234
  const hookEvent = JSON.parse(stdinBuf.toString('utf8'));
235
+ HOOK_EVENT = hookEvent;
214
236
  if (Array.isArray(hookEvent?.messages) && hookEvent.messages.length > 0) {
215
237
  // Cap to last 50 messages. The server-side handler slices further if needed.
216
238
  HOOK_EVENT_MESSAGES = hookEvent.messages.slice(-50);
@@ -219,6 +241,81 @@ try {
219
241
  // stdin not available / not JSON / no messages field — HOOK_EVENT_MESSAGES stays undefined.
220
242
  }
221
243
 
244
+ function sanitizeSessionId(sessionId) {
245
+ return String(sessionId || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
246
+ }
247
+
248
+ function lastUserMessageFromEvent() {
249
+ const messages = Array.isArray(HOOK_EVENT?.messages) ? HOOK_EVENT.messages : [];
250
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
251
+ const message = messages[index];
252
+ const role = message?.role || message?.message?.role || message?.type;
253
+ if (role !== 'user') continue;
254
+ const content = message?.content ?? message?.message?.content;
255
+ if (typeof content === 'string' && content.trim()) return content.trim();
256
+ if (Array.isArray(content)) {
257
+ const text = content
258
+ .filter((entry) => entry?.type === 'text' && typeof entry?.text === 'string')
259
+ .map((entry) => entry.text.trim())
260
+ .filter(Boolean)
261
+ .join('\n');
262
+ if (text) return text;
263
+ }
264
+ }
265
+ return isTurn ? 'Claude turn refresh via harness hook' : 'Claude session start via harness hook';
266
+ }
267
+
268
+ function receiptPathForSession(sessionId) {
269
+ return `${MIZAN_RECEIPT_DIR}/${sanitizeSessionId(sessionId)}.json`;
270
+ }
271
+
272
+ function persistMizanReceipt(sessionId, payload) {
273
+ try {
274
+ mkdirSync(MIZAN_RECEIPT_DIR, { recursive: true });
275
+ writeFileSync(receiptPathForSession(sessionId), JSON.stringify(payload, null, 2) + '\n');
276
+ } catch {}
277
+ }
278
+
279
+ async function runMizanPre(apiKey, packet, sourceLabel) {
280
+ if (!isTurn) return null;
281
+ const sessionId = HOOK_EVENT?.session_id || HOOK_EVENT?.sessionId || 'claude-code';
282
+ const response = await fetch(`${DEFAULT_RUNTIME_URL.replace(/\/+$/, '')}/mizan/pre`, {
283
+ method: 'POST',
284
+ headers: {
285
+ Authorization: `Bearer ${apiKey}`,
286
+ 'Content-Type': 'application/json',
287
+ },
288
+ body: JSON.stringify({
289
+ sessionId,
290
+ packet,
291
+ context: {
292
+ sessionId,
293
+ message: lastUserMessageFromEvent().slice(0, 4000),
294
+ intendedAction: 'Establish canonical pre-turn cognition receipt for this Claude turn before any tool or output work.',
295
+ rationale: `Claude turn-start harness sync via ${sourceLabel} must mint a canonical Mizan pre receipt for downstream tool and output enforcement.`,
296
+ platform: 'claude-code',
297
+ stage: 'hook-turn-pre',
298
+ },
299
+ }),
300
+ });
301
+ const payload = await response.json().catch(() => ({}));
302
+ if (!response.ok) {
303
+ throw new Error(payload?.error || `mizan/pre failed (${response.status})`);
304
+ }
305
+ if (payload?.receipt) {
306
+ persistMizanReceipt(sessionId, {
307
+ updatedAt: new Date().toISOString(),
308
+ source: sourceLabel,
309
+ sessionId,
310
+ receipt: payload.receipt,
311
+ result: payload.result || null,
312
+ contract: payload.contract || null,
313
+ summary: payload.summary || null,
314
+ });
315
+ }
316
+ return payload;
317
+ }
318
+
222
319
  async function tryViaSdk(baseUrl, apiKey) {
223
320
  // Canonical path: HTTPHarnessClient.getHarnessPacket(). The SDK POSTs to
224
321
  // /api/harness/codex with the right shape and returns { packet, timestamp,
@@ -228,16 +325,19 @@ async function tryViaSdk(baseUrl, apiKey) {
228
325
  // SDK's `body.packet ?? body` passes the body through unchanged).
229
326
  const Cls = await loadSdkClass();
230
327
  if (Cls) {
328
+ const packetUrl = isMountedRuntimeUrl(baseUrl)
329
+ ? `${baseUrl}${RUNTIME_PACKET_SUFFIX}`
330
+ : `${baseUrl}/api/harness/codex`;
231
331
  const client = new Cls({
232
332
  baseUrl,
233
333
  apiKey,
234
- harnessPacketUrl: `${baseUrl}/api/harness/codex`,
334
+ harnessPacketUrl: packetUrl,
235
335
  });
236
336
  // Pass a bodyOverride that does NOT include isHamza:false. The server
237
337
  // identifies owner tier from the Bearer token (isMasterTokenRequest).
238
338
  // Hardcoding isHamza:false in the body would override that server-side
239
339
  // signal and produce hamza:false in the packet even for master-token callers.
240
- const bodyOverride = {
340
+ const bodyOverride = {
241
341
  message: isTurn ? 'Claude turn refresh — harness via SDK' : 'Claude session start — harness via SDK',
242
342
  stage: isTurn ? 'checkpoint' : 'preflight',
243
343
  actor: 'claude-code',
@@ -246,15 +346,19 @@ async function tryViaSdk(baseUrl, apiKey) {
246
346
  // Bonus #74: forward conversation history so codex handler can report
247
347
  // a non-zero conversation_history_count in the packet.
248
348
  ...(HOOK_EVENT_MESSAGES ? { messages: HOOK_EVENT_MESSAGES } : {}),
249
- };
250
- const wrapped = await client.getHarnessPacket(bodyOverride);
251
- const json = wrapped.packet;
252
- if (json && json.ok === false) throw new Error(`ok=false: ${json.error || 'unknown'}`);
253
- return { json, raw: JSON.stringify(json) };
254
- }
349
+ };
350
+ const wrapped = await client.getHarnessPacket(bodyOverride);
351
+ const json = normalizeHarnessPacketPayload(wrapped.packet);
352
+ if (json && json.ok === false) throw new Error(`ok=false: ${json.error || 'unknown'}`);
353
+ await runMizanPre(apiKey, wrapped.packet, `${baseUrl}${RUNTIME_PACKET_SUFFIX}`);
354
+ return { json, raw: JSON.stringify(json) };
355
+ }
255
356
 
357
+ const packetUrl = isMountedRuntimeUrl(baseUrl)
358
+ ? `${baseUrl}${RUNTIME_PACKET_SUFFIX}`
359
+ : `${baseUrl}/api/harness/codex`;
256
360
  // SDK absent (dev environment) — direct fetch with identical wire shape.
257
- const resp = await fetch(`${baseUrl}/api/harness/codex`, {
361
+ const resp = await fetch(packetUrl, {
258
362
  method: 'POST',
259
363
  headers: {
260
364
  Authorization: `Bearer ${apiKey}`,
@@ -272,8 +376,10 @@ async function tryViaSdk(baseUrl, apiKey) {
272
376
  }),
273
377
  });
274
378
  if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
275
- const json = await resp.json();
379
+ const responseBody = await resp.json();
380
+ const json = normalizeHarnessPacketPayload(responseBody.packet ?? responseBody);
276
381
  if (json && json.ok === false) throw new Error(`ok=false: ${json.error || 'unknown'}`);
382
+ await runMizanPre(apiKey, responseBody.packet ?? responseBody, packetUrl);
277
383
  return { json, raw: JSON.stringify(json) };
278
384
  }
279
385
 
@@ -284,6 +390,9 @@ function renderPacket(json, source, ageNote = '') {
284
390
  const con = json.contractGate || {};
285
391
  const mc = json.missingCritical || [];
286
392
  const loaded = json.loadedByClass || {};
393
+ const offlineBundle = json.runtimeOfflineBundle && typeof json.runtimeOfflineBundle === 'object'
394
+ ? json.runtimeOfflineBundle
395
+ : null;
287
396
 
288
397
  let header = `${HEADER_PREFIX} (${MODE}${ageNote}, sdk=harness-http-client) hash=${phash.slice(0, 16)} via=${source}\n`;
289
398
  header += ` preStateGate: passed=${pre.passed} score=${(pre.score || 0).toFixed(2)}`;
@@ -292,6 +401,9 @@ function renderPacket(json, source, ageNote = '') {
292
401
  if (mc.length) header += `\n missingCritical: ${JSON.stringify(mc.slice(0, 5))}`;
293
402
  const loadedSummary = Object.entries(loaded).sort().map(([k, v]) => `${k}=${v}`).join(' ');
294
403
  if (loadedSummary) header += `\n loadedByClass: ${loadedSummary}`;
404
+ if (offlineBundle) {
405
+ header += `\n offlineBundle: phase=${offlineBundle.phase || 'unknown'} age=${offlineBundle.ageSeconds ?? 'unknown'}s source=${offlineBundle.source || 'runtime-cache'}`;
406
+ }
295
407
 
296
408
  let ctx = header;
297
409
  if (harness) {
@@ -344,8 +456,9 @@ async function main() {
344
456
  const stat = fs.statSync(PACKET_CACHE);
345
457
  const ageSec = (Date.now() - stat.mtimeMs) / 1000;
346
458
  if (ageSec < PACKET_CACHE_TTL_SEC) {
347
- const cached = JSON.parse(fs.readFileSync(PACKET_CACHE, 'utf8'));
348
- return renderPacket(cached, '(cache)', ` cached ${Math.round(ageSec)}s`);
459
+ const cached = JSON.parse(fs.readFileSync(PACKET_CACHE, 'utf8'));
460
+ await runMizanPre(apiKey, cached, '(cache)');
461
+ return renderPacket(cached, '(cache)', ` cached ${Math.round(ageSec)}s`);
349
462
  }
350
463
  } catch {}
351
464
  }
@@ -396,6 +509,7 @@ async function main() {
396
509
  const ageSec = (Date.now() - stat.mtimeMs) / 1000;
397
510
  if (ageSec >= 0) {
398
511
  const cached = JSON.parse(fs.readFileSync(PACKET_CACHE, 'utf8'));
512
+ await runMizanPre(apiKey, cached, '(stale-cache)');
399
513
  try {
400
514
  appendFileSync(LOG_FILE, `${new Date().toISOString()} stale mode=${MODE} cache_age=${Math.round(ageSec)}s last_err="${lastErr.replace(/"/g, "'")}" sdk=harness-http-client\n`);
401
515
  } catch {}
@@ -53,7 +53,9 @@ const REQUIRED_LENSES = 8;
53
53
  const SUBSTANCE_MIN_AFTER_ANCHOR_STRIP = 40;
54
54
  const ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet|language):[a-z0-9_\-./]+/gi;
55
55
  const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
56
+ const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
56
57
  const FIRST_PRINCIPLE_RX = /\b(first[_\s-]?principle[s]?|first_principle=)\b/i;
58
+ const DECISION_SIGNAL_RX = /(?:should|recommend|propose|suggest|let'?s|go with|i'd|i would|here'?s the plan|i'll|next step|action item|ship it|yes do|let me)/i;
57
59
 
58
60
  // Drift trigger patterns (mirror aria-stop-gate.mjs canonical list).
59
61
  // Keep this conservative — false negatives are tolerable (real gate will
@@ -138,6 +140,27 @@ function verifyAnchor(anchor, substrate) {
138
140
  return { ok: true, reason: '' };
139
141
  }
140
142
 
143
+ function validateAppliedCognitionContract(text) {
144
+ const match = String(text || '').match(APPLIED_COGNITION_BLOCK_RX);
145
+ if (!match) return { ok: false, violations: ['missing <applied_cognition> contract'] };
146
+ const body = match[1] || '';
147
+ const required = [
148
+ ['decision_delta', /\bdecision[_ -]?delta\s*:/i],
149
+ ['dominant_domain', /\bdominant[_ -]?domain\s*:/i],
150
+ ['binds_to', /\bbinds[_ -]?to\s*:/i],
151
+ ['expected_predicate', /\bexpected[_ -]?predicate\s*:/i],
152
+ ['artifact_change', /\bartifact[_ -]?change\s*:/i],
153
+ ];
154
+ const violations = [];
155
+ for (const [name, rx] of required) {
156
+ if (!rx.test(body)) violations.push(`missing ${name}`);
157
+ }
158
+ if (/decision[_ -]?delta\s*:\s*(?:none|n\/a|no change|unchanged|same)/i.test(body)) {
159
+ violations.push('decision_delta says cognition changed nothing');
160
+ }
161
+ return { ok: violations.length === 0, violations };
162
+ }
163
+
141
164
  // ── Read draft from stdin ─────────────────────────────────────────────
142
165
  let stdin = '';
143
166
  try {
@@ -165,6 +188,7 @@ if (draft.length < 10) {
165
188
 
166
189
  const substrate = loadSubstrate();
167
190
  const failures = [];
191
+ const requiresAppliedCognition = draft.length >= 300 || DECISION_SIGNAL_RX.test(draft) || ['deploy', 'edit', 'bash', 'text'].includes(actionType);
168
192
 
169
193
  // ── Cognition block presence + lens count + substance ─────────────────
170
194
  const cogMatch = draft.match(COGNITION_BLOCK_RX);
@@ -225,6 +249,17 @@ if (cogMatch) {
225
249
  }
226
250
  }
227
251
 
252
+ if (requiresAppliedCognition) {
253
+ const applied = validateAppliedCognitionContract(draft);
254
+ if (!applied.ok) {
255
+ failures.push({
256
+ kind: 'applied_cognition_contract_invalid',
257
+ violations: applied.violations,
258
+ message: 'Non-trivial drafts must bind cognition to the actual action/output.',
259
+ });
260
+ }
261
+ }
262
+
228
263
  // ── Drift triggers ────────────────────────────────────────────────────
229
264
  for (const trigger of DRIFT_TRIGGERS) {
230
265
  const match = draft.match(trigger.rx);