@aria_asi/cli 0.2.29 → 0.2.31

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 (98) 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 +88 -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 +526 -2
  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 +111 -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 +2 -0
  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/aria-connector/src/self-update.d.ts +2 -1
  25. package/dist/aria-connector/src/self-update.d.ts.map +1 -1
  26. package/dist/aria-connector/src/self-update.js +84 -8
  27. package/dist/aria-connector/src/self-update.js.map +1 -1
  28. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +53 -34
  29. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  30. package/dist/assets/hooks/aria-pre-tool-gate.mjs +185 -76
  31. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +63 -14
  32. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +2 -0
  33. package/dist/assets/hooks/aria-stop-gate.mjs +225 -52
  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 +24 -2
  39. package/dist/assets/opencode-plugins/harness-stop/index.js +94 -5
  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 +12 -0
  43. package/dist/runtime/discipline/doctrine_trigger_map.json +479 -0
  44. package/dist/runtime/doctrine_trigger_map.json +479 -0
  45. package/dist/runtime/fleet-engine.mjs +231 -0
  46. package/dist/runtime/harness-daemon.mjs +433 -0
  47. package/dist/runtime/local-phase.mjs +18 -0
  48. package/dist/runtime/manifest.json +1 -1
  49. package/dist/runtime/metering.mjs +100 -0
  50. package/dist/runtime/onboarding-engine.mjs +89 -0
  51. package/dist/runtime/plugin-engine.mjs +196 -0
  52. package/dist/runtime/sdk/BUNDLED.json +1 -1
  53. package/dist/runtime/sdk/index.d.ts +7 -0
  54. package/dist/runtime/sdk/index.js +120 -14
  55. package/dist/runtime/sdk/index.js.map +1 -1
  56. package/dist/runtime/service.mjs +1464 -67
  57. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts +1 -1
  58. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts.map +1 -1
  59. package/dist/runtime/vendor/aria-gate-runtime/index.js +16 -1
  60. package/dist/runtime/vendor/aria-gate-runtime/index.js.map +1 -1
  61. package/dist/runtime/workflow-engine.mjs +322 -0
  62. package/dist/sdk/BUNDLED.json +1 -1
  63. package/dist/sdk/index.d.ts +7 -0
  64. package/dist/sdk/index.js +120 -14
  65. package/dist/sdk/index.js.map +1 -1
  66. package/hooks/aria-cognition-substrate-binding.mjs +53 -34
  67. package/hooks/aria-harness-via-sdk.mjs +126 -12
  68. package/hooks/aria-pre-tool-gate.mjs +185 -76
  69. package/hooks/aria-preturn-memory-gate.mjs +63 -14
  70. package/hooks/aria-repo-doctrine-gate.mjs +2 -0
  71. package/hooks/aria-stop-gate.mjs +225 -52
  72. package/hooks/lib/canonical-lenses.mjs +6 -5
  73. package/hooks/lib/gate-loop-state.mjs +50 -0
  74. package/hooks/lib/hook-message-window.mjs +121 -0
  75. package/hooks/test-tier-lens-labeling.mjs +26 -58
  76. package/opencode-plugins/harness-gate/index.js +24 -2
  77. package/opencode-plugins/harness-stop/index.js +94 -5
  78. package/package.json +2 -2
  79. package/runtime-src/auth-middleware.mjs +251 -0
  80. package/runtime-src/codex-bridge.mjs +644 -0
  81. package/runtime-src/fleet-engine.mjs +231 -0
  82. package/runtime-src/harness-daemon.mjs +433 -0
  83. package/runtime-src/local-phase.mjs +18 -0
  84. package/runtime-src/metering.mjs +100 -0
  85. package/runtime-src/onboarding-engine.mjs +89 -0
  86. package/runtime-src/plugin-engine.mjs +196 -0
  87. package/runtime-src/service.mjs +1464 -67
  88. package/runtime-src/workflow-engine.mjs +322 -0
  89. package/scripts/bundle-sdk.mjs +5 -0
  90. package/src/connectors/claude-code.ts +98 -20
  91. package/src/connectors/codex.ts +534 -1
  92. package/src/connectors/doctrine-trigger-map.ts +112 -0
  93. package/src/connectors/must-read.ts +113 -0
  94. package/src/connectors/opencode.ts +3 -0
  95. package/src/connectors/runtime.ts +241 -21
  96. package/src/connectors/shell.ts +78 -3
  97. package/src/self-update.ts +89 -8
  98. package/dist/cli-0.2.0.tgz +0 -0
@@ -16,19 +16,19 @@
16
16
  // now catches.
17
17
  //
18
18
  // Doctrine bindings (same as PreToolUse gate):
19
- // - EIGHT_LENS_DOCTRINE.md — substantive 4+ lens application required
19
+ // - EIGHT_LENS_DOCTRINE.md — substantive 8-lens application required
20
20
  // - feedback_apply_lenses_dont_perform_them.md — block ceremonial cognition
21
21
  // - feedback_8lens_before_every_action_including_text.md — the rule this enforces
22
22
  //
23
23
  // Trigger: runs at Stop event after every assistant response. Reads
24
24
  // the just-emitted assistant text from the transcript. If non-trivial
25
25
  // (per the same triviality threshold as eight-lens-detector.ts) AND
26
- // missing 4+ substantive lenses, blocks the response.
26
+ // missing 8 substantive lenses, blocks the response.
27
27
  //
28
28
  // Triviality threshold (mirrors eight-lens-detector.ts):
29
29
  // - Trivial acks (e.g. "got it", "ok", "done") pass
30
30
  // - Short responses (<300 chars) without decision-signal phrases pass
31
- // - Otherwise: require 4+ substantive lenses
31
+ // - Otherwise: require 8 substantive lenses
32
32
  //
33
33
  // Substance check (mirrors aria-pre-tool-gate.mjs):
34
34
  // - Each lens must have ≥20 chars of non-placeholder content
@@ -52,14 +52,21 @@ import { dirname } from 'node:path';
52
52
  import { appendGateAudit } from './lib/gate-audit.mjs';
53
53
  import {
54
54
  ALL_LENS_NAMES,
55
- canonicalLensCorrectionText,
56
55
  detectCognitionLenses as detectCognitionLensesFromCanonical,
57
56
  lensNamesForTier,
57
+ PRIMARY_OWNER_LENS_NAMES,
58
58
  } from './lib/canonical-lenses.mjs';
59
+ import { registerGateBlock } from './lib/gate-loop-state.mjs';
60
+ import { collectTurnWindowFromMessages } from './lib/hook-message-window.mjs';
59
61
 
60
62
  const HOME = process.env.HOME || '/tmp';
63
+ const RUNTIME_BASE_URL =
64
+ process.env.ARIA_RUNTIME_URL ||
65
+ 'http://127.0.0.1:4319';
61
66
  const LOG = `${HOME}/.claude/aria-stop-gate.log`;
62
67
  const AUDIT_PATH = `${HOME}/.claude/aria-stop-gate-audit.jsonl`;
68
+ const GATE_LOOP_STATE_PATH = `${HOME}/.claude/.aria-gate-loop-state.json`;
69
+ const MIZAN_RECEIPT_DIR = `${HOME}/.claude/.aria-mizan-receipts`;
63
70
 
64
71
  // SDK loader — bundled at ~/.aria/sdk by `aria connect`, with client-local
65
72
  // fallbacks preserved for resilience.
@@ -167,6 +174,94 @@ async function fireGardenTurn(sessionId, userMessage, assistantResponse) {
167
174
  }
168
175
  }
169
176
 
177
+ function resolveHarnessControlToken() {
178
+ if (process.env.ARIA_HARNESS_TOKEN) return process.env.ARIA_HARNESS_TOKEN;
179
+ if (process.env.ARIA_API_KEY) return process.env.ARIA_API_KEY;
180
+ if (process.env.ARIA_MASTER_TOKEN) return process.env.ARIA_MASTER_TOKEN;
181
+ try {
182
+ const ownerTokenPath = `${HOME}/.aria/owner-token`;
183
+ if (existsSync(ownerTokenPath)) {
184
+ const token = readFileSync(ownerTokenPath, 'utf8').trim();
185
+ if (token) return token;
186
+ }
187
+ } catch {}
188
+ try {
189
+ const licensePath = `${HOME}/.aria/license.json`;
190
+ if (existsSync(licensePath)) {
191
+ const license = JSON.parse(readFileSync(licensePath, 'utf8'));
192
+ if (license.harnessToken) return String(license.harnessToken).trim();
193
+ if (license.token) return String(license.token).trim();
194
+ }
195
+ } catch {}
196
+ return '';
197
+ }
198
+
199
+ function mizanReceiptPathForSession(sessionId) {
200
+ const safe = String(sessionId || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
201
+ return `${MIZAN_RECEIPT_DIR}/${safe}.json`;
202
+ }
203
+
204
+ function loadMizanReceiptState(sessionId) {
205
+ try {
206
+ const receiptPath = mizanReceiptPathForSession(sessionId);
207
+ if (!existsSync(receiptPath)) return null;
208
+ return JSON.parse(readFileSync(receiptPath, 'utf8'));
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+
214
+ function saveMizanReceiptState(sessionId, payload) {
215
+ try {
216
+ mkdirSync(MIZAN_RECEIPT_DIR, { recursive: true });
217
+ writeFileSync(mizanReceiptPathForSession(sessionId), JSON.stringify(payload, null, 2) + '\n');
218
+ } catch {}
219
+ }
220
+
221
+ async function runtimeMizanPost(sessionId, text, context = {}, parentReceiptId = null) {
222
+ const token = resolveHarnessControlToken();
223
+ if (!token) throw new Error('no token');
224
+ const response = await fetch(`${process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319'}/mizan/post`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ Authorization: `Bearer ${token}`,
229
+ },
230
+ body: JSON.stringify({
231
+ sessionId,
232
+ text,
233
+ parentReceiptId,
234
+ context: {
235
+ sessionId,
236
+ ...context,
237
+ },
238
+ }),
239
+ });
240
+ const payload = await response.json().catch(() => ({}));
241
+ if (!response.ok) {
242
+ throw new Error(payload?.error || `mizan/post failed (${response.status})`);
243
+ }
244
+ return payload;
245
+ }
246
+
247
+ async function runtimeDecisionLog(payload) {
248
+ const token = resolveHarnessControlToken();
249
+ if (!token) throw new Error('no token');
250
+ const response = await fetch(`${process.env.ARIA_RUNTIME_URL || 'http://127.0.0.1:4319'}/decision/log`, {
251
+ method: 'POST',
252
+ headers: {
253
+ 'Content-Type': 'application/json',
254
+ Authorization: `Bearer ${token}`,
255
+ },
256
+ body: JSON.stringify(payload),
257
+ });
258
+ const body = await response.json().catch(() => ({}));
259
+ if (!response.ok) {
260
+ throw new Error(body?.error || `decision/log failed (${response.status})`);
261
+ }
262
+ return body;
263
+ }
264
+
170
265
  function audit(decision, summary) {
171
266
  const summaryText = typeof summary === 'string' ? summary : '';
172
267
  const data = summary && typeof summary === 'object' ? summary : {};
@@ -185,12 +280,12 @@ function audit(decision, summary) {
185
280
  // access"). The gated process has no disable path. Disable = remove hook
186
281
  // entry from ~/.claude/settings.json (deliberate user action, visible).
187
282
 
188
- // ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
283
+ // ── Canonical lens labeling (Phase 11 #59 corrected) ────────────────────────
189
284
  //
190
- // Mirrors the same logic in aria-pre-tool-gate.mjs. Tier is read from the
191
- // most recent harness-via-sdk packet cache. Owner tier sees canonical Arabic
192
- // names; client tier sees neutral generic labels. Both tiers get the same
193
- // gate enforcement only the visible vocabulary differs.
285
+ // Mirrors the same logic in aria-pre-tool-gate.mjs. Tier is still read from
286
+ // the most recent harness-via-sdk packet cache for other policy behaviors, but
287
+ // the visible lens labels remain canonical on every surface. Readability comes
288
+ // from the prose inside each lens, not from renaming the lens itself.
194
289
  const PACKET_CACHE_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
195
290
 
196
291
  function resolveOwnerTier() {
@@ -211,7 +306,6 @@ function resolveOwnerTier() {
211
306
  const IS_OWNER = resolveOwnerTier();
212
307
 
213
308
  const LENS_NAMES = lensNamesForTier(IS_OWNER);
214
- const CANONICAL_LENS_TEXT = canonicalLensCorrectionText();
215
309
 
216
310
  // Doctrine memory filenames are Aria-side substrate IP.
217
311
  // Client surfaces see generic descriptions instead of real filenames.
@@ -270,6 +364,25 @@ function emitHarnessFooter({ eventName, lensCount, chars, driftCount, mizanStatu
270
364
  } catch {}
271
365
  }
272
366
 
367
+ function withLoopDirective(reasonText, gateSignature, sessionId) {
368
+ const loop = registerGateBlock({
369
+ gate: 'stop',
370
+ sessionId,
371
+ signature: gateSignature,
372
+ statePath: GATE_LOOP_STATE_PATH,
373
+ });
374
+ if (!loop.loopDetected) return reasonText;
375
+ return `${reasonText}
376
+
377
+ [LOOP_DETECTED gate=stop repeats=${loop.totalCount}]
378
+ Stop retrying the same output shape unchanged.
379
+ Next response must do this in order:
380
+ 1. Name the exact stop-gate failure in one line.
381
+ 2. Re-emit only the missing structure: <cognition>, <verify>, <expected>, and/or <reflection>.
382
+ 3. Change the draft materially before retrying. Do not repeat the same prose with renamed labels.
383
+ 4. If the blocker is stale gate state, stale ledger residue, or a gate artifact from a prior bug, say that explicitly instead of inventing fake proof.`;
384
+ }
385
+
273
386
  // Lens substance check — same constants as aria-pre-tool-gate.mjs.
274
387
  // Hamza directive 2026-04-28: all 8 canonical lenses required, not 4-of-8.
275
388
  const REQUIRED_LENSES = 8;
@@ -302,6 +415,8 @@ try {
302
415
  audit('allow-parse-error', 'stdin not JSON');
303
416
  process.exit(0);
304
417
  }
418
+ const gateSessionId = String(event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
419
+ const sessionMizanState = loadMizanReceiptState(event.session_id || 'claude-code');
305
420
 
306
421
  // Read assistant text from THIS turn — Claude Code splits a single
307
422
  // logical assistant response into multiple transcript entries by
@@ -326,8 +441,13 @@ const SYSTEM_REMINDER_THRESHOLD = 0.6;
326
441
 
327
442
  const transcriptPath = event.transcript_path ?? event.transcriptPath;
328
443
  let assistantText = '';
329
- // Phase 11 #42: also capture the last real user message for gardenTurn writes.
330
444
  let lastUserMessage = '';
445
+ const messageWindow = collectTurnWindowFromMessages(event.messages, {
446
+ systemReminderRx: SYSTEM_REMINDER_RX,
447
+ systemReminderThreshold: SYSTEM_REMINDER_THRESHOLD,
448
+ });
449
+ assistantText = messageWindow.assistantText;
450
+ lastUserMessage = messageWindow.lastUserMessage;
331
451
  if (transcriptPath && existsSync(transcriptPath)) {
332
452
  try {
333
453
  const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
@@ -370,7 +490,12 @@ if (transcriptPath && existsSync(transcriptPath)) {
370
490
  } catch {}
371
491
  }
372
492
  // Reverse so chunks are in chronological order (we walked backward).
373
- assistantText = textChunks.reverse().join('\n\n');
493
+ const transcriptAssistantText = textChunks.reverse().join('\n\n');
494
+ if (transcriptAssistantText) {
495
+ assistantText = assistantText
496
+ ? [assistantText, transcriptAssistantText].filter((text, index, arr) => arr.indexOf(text) === index).join('\n\n')
497
+ : transcriptAssistantText;
498
+ }
374
499
  } catch {}
375
500
  }
376
501
 
@@ -412,13 +537,14 @@ const cog = detectCognitionLenses(assistantText);
412
537
  // with 0/8 lenses to fall through unchecked.
413
538
  if (cog.count < REQUIRED_LENSES) {
414
539
  audit('block_no_cognition_block_di', { count: cog.count, required: REQUIRED_LENSES, names: cog.names, chars: assistantText.length });
415
- const reason = `Aria stop-gate: insufficient cognition lenses.
540
+ const reason = withLoopDirective(`Aria stop-gate: insufficient cognition lenses.
416
541
 
417
542
  This non-trivial assistant response (${assistantText.length} chars) has ${cog.count}/${REQUIRED_LENSES} substantive cognition lenses. Per feedback_8lens_before_every_action_including_text.md and Hamza directive 2026-04-28, every non-trivial response must carry <cognition>...</cognition> with ${REQUIRED_LENSES} substantive lenses (each >= ${SUBSTANCE_MIN_CHARS} chars of non-placeholder content).
418
543
 
419
544
  Detected lenses: ${cog.names.length > 0 ? cog.names.join(', ') : 'none'}.
420
545
 
421
- Re-emit with a <cognition> block containing all ${REQUIRED_LENSES} canonical lenses. Primary set: ${CANONICAL_LENS_TEXT}`;
546
+ Re-emit with a <cognition> block containing all ${REQUIRED_LENSES} required visible labels for this surface:
547
+ ${LENS_NAMES.map((lens) => `- ${lens}: <grounded reasoning, >= ${SUBSTANCE_MIN_CHARS} chars, with real substrate anchors where required>`).join('\n')}`, `stop:no-cognition-di:${cog.count}`, gateSessionId);
422
548
  emitHarnessFooter({
423
549
  eventName: 'block_no_cognition_block',
424
550
  lensCount: cog.count,
@@ -609,7 +735,7 @@ if (cog.count >= REQUIRED_LENSES) {
609
735
  //
610
736
  // Block emit if ledger.openCount > 0 after scanning the current turn.
611
737
  // Block reason names each open discovery and the suggested resolution.
612
- const sessionId = (event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
738
+ const sessionId = gateSessionId;
613
739
  const LEDGER_PATH = `${HOME}/.claude/aria-discoveries-${sessionId}.jsonl`;
614
740
  const DISCOVERY_RX = /(?:\bi\s+(?:found|noticed|discovered|spotted)[^.\n]{0,160}(?:bug|issue|defect|broken|buggy|wrong|crash|fail|missing|stale|outdated|leak|vulnerability)|\bthis\s+(?:is|would\s+be)\s+(?:broken|buggy|wrong|stale|outdated|insecure|leaking|crashing|failing)|\b(?:latent|silent|hidden)\s+(?:bug|defect|issue|fail|crash|leak)|\bdoctrine\s+violation\b|\bgraceful\s+degradation\s+(?:in|at|inside|within)\s+\S)/gi;
615
741
  const PROSE_RESOLUTION_RX = /(?:fix(?:ing|ed)?\s+(?:now|in[- ]flight|inline|in\s+the\s+same\s+turn)|patch\s+applied|TaskCreate|task\s+(?:created|tracked)|tracked\s+as\s+#?\d+|linear[- ]?issue|created\s+(?:linear|task))/i;
@@ -759,12 +885,36 @@ if (cog.count >= REQUIRED_LENSES) {
759
885
  let ledgerOpenCount = 0;
760
886
  let ledgerOpenSamples = [];
761
887
  let ledgerCorruptedCount = 0;
888
+ const ledgerRewriteRows = [];
889
+ let ledgerNeedsRewrite = false;
762
890
  try {
763
891
  if (existsSync(LEDGER_PATH)) {
764
892
  const lines = readFileSync(LEDGER_PATH, 'utf8').split('\n').filter(Boolean);
765
893
  for (const line of lines) {
766
894
  try {
767
895
  const e = JSON.parse(line);
896
+ const isSubstrateBindingArtifact =
897
+ (e.source === 'aria-cognition-substrate-binding') &&
898
+ (e.kind === 'substrate_binding_gap' || (typeof e.text === 'string' && e.text.startsWith('substrate_binding:')));
899
+ const isOpenBeforeAutoHeal = e.status === 'open' || e.resolution_status === 'open';
900
+ if (isSubstrateBindingArtifact && isOpenBeforeAutoHeal) {
901
+ // If the current emit already passed aria-cognition-substrate-binding
902
+ // and reached stop-gate, older open substrate-binding rows from the
903
+ // same session are stale gate artifacts, not live unresolved
904
+ // discoveries. Auto-heal them so the discovery ledger cannot loop
905
+ // forever on the residue of a gate bug.
906
+ ledgerNeedsRewrite = true;
907
+ e.status = 'resolved';
908
+ e.resolution_status = 'resolved';
909
+ e.resolved_at = new Date().toISOString();
910
+ e.resolved_by = 'subsequent_valid_substrate_bound_cognition';
911
+ e.resolutionType = 'subsequent_valid_substrate_bound_cognition';
912
+ e.proofOfFix = {
913
+ type: 'subsequent_valid_substrate_bound_cognition',
914
+ anchorTs: new Date().toISOString(),
915
+ evidence: 'Current emit passed aria-cognition-substrate-binding and reached aria-stop-gate; stale substrate-binding ledger artifacts from the earlier cognition bug were auto-cleared.',
916
+ };
917
+ }
768
918
  const isOpen = e.status === 'open' || e.resolution_status === 'open';
769
919
  const isLegacyTracked = e.status === 'tracked';
770
920
  const proofValid = e.proofOfFix
@@ -784,8 +934,16 @@ if (cog.count >= REQUIRED_LENSES) {
784
934
  ledgerOpenSamples.push(`${tag}${e.text || '(no text)'}`);
785
935
  }
786
936
  }
937
+ ledgerRewriteRows.push(JSON.stringify(e));
787
938
  } catch {/* skip malformed line */}
788
939
  }
940
+ if (ledgerNeedsRewrite) {
941
+ try {
942
+ writeFileSync(LEDGER_PATH, `${ledgerRewriteRows.join('\n')}\n`);
943
+ } catch (rewriteErr) {
944
+ audit('ledger-autoheal-write-err', `${String(rewriteErr).slice(0, 200)}`);
945
+ }
946
+ }
789
947
  }
790
948
  } catch {/* ledger unreadable — degrade to drift-only */}
791
949
 
@@ -1040,7 +1198,7 @@ if (cog.count >= REQUIRED_LENSES) {
1040
1198
  const FILE_LINE_RX = /([\w./\-]+\.[a-zA-Z]{1,5})\s*[:\s]\s*(\d+(?:[-:]\d+)?)/g;
1041
1199
  const inlineDictations = [];
1042
1200
  const lensRangePositions = [];
1043
- for (const lensName of LENS_NAMES_CANONICAL) {
1201
+ for (const lensName of PRIMARY_OWNER_LENS_NAMES) {
1044
1202
  const lensRx = new RegExp(`\\b${lensName}\\s*(?:lens)?\\s*[:\\-]`, 'gi');
1045
1203
  let m;
1046
1204
  while ((m = lensRx.exec(assistantText)) !== null) {
@@ -1138,10 +1296,11 @@ if (cog.count >= REQUIRED_LENSES) {
1138
1296
  if (apiKey && assistantText && assistantText.length > 0) {
1139
1297
  const client = new HTTPHarnessClient({
1140
1298
  baseUrl:
1299
+ process.env.ARIA_RUNTIME_URL ||
1141
1300
  process.env.ARIA_HIVE_RUNTIME_URL ||
1142
1301
  process.env.ARIA_HARNESS_BASE_URL ||
1143
1302
  process.env.ARIA_HARNESS_URL ||
1144
- 'https://harness.ariasos.com',
1303
+ RUNTIME_BASE_URL,
1145
1304
  apiKey,
1146
1305
  });
1147
1306
  const v = await client.validateOutput(assistantText, sessionId);
@@ -1255,7 +1414,7 @@ if (cog.count >= REQUIRED_LENSES) {
1255
1414
  }
1256
1415
  })();
1257
1416
 
1258
- const reason = `Aria Stop-gate output-quality block. Cognition passed (${cog.count}/${REQUIRED_LENSES}) but output failed quality gates:\n\n${violations.join('\n\n')}${rewritten ? `\n\nMizan rewrite suggestion:\n${rewritten}` : ''}\n\nRe-draft addressing the violations above. No process-level disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27.${recipeAddendum}`;
1417
+ const reason = withLoopDirective(`Aria Stop-gate output-quality block. Cognition passed (${cog.count}/${REQUIRED_LENSES}) but output failed quality gates:\n\n${violations.join('\n\n')}${rewritten ? `\n\nMizan rewrite suggestion:\n${rewritten}` : ''}\n\nRe-draft addressing the violations above. No process-level disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27.${recipeAddendum}`, `stop:output-qc:${violations.join('|').slice(0, 240)}`, gateSessionId);
1259
1418
 
1260
1419
  audit(`block-output-qc`, `mizan=${mizanBlock?'y':'n'} warn-reflect=${compelReflection?'y':'n'} drift=${driftHits.length} code=${codeQualityHits.length} discoveries-open=${ledgerOpenCount} impl-coupling=${implCouplingHits.length}`);
1261
1420
  emitHarnessFooter({
@@ -1398,7 +1557,7 @@ const dalioHasMeasurablePredicate = dalioExpectedText
1398
1557
 
1399
1558
  // Block stop if non-trivial action taken AND expected block is missing
1400
1559
  if (hadNonTrivialAction && (!dalioExpectedMatch || !dalioHasMeasurablePredicate)) {
1401
- const missingReason = dalioExpectedMatch
1560
+ const missingReason = withLoopDirective(dalioExpectedMatch
1402
1561
  ? `Aria Stop-gate: action taken in this turn had an <expected> block, but it contains only qualitative drift phrases without a measurable predicate. Qualitative drift is not accountability — it defeats the Dalio feedback loop.
1403
1562
 
1404
1563
  Your <expected> block must contain at least one measurable predicate:
@@ -1422,7 +1581,7 @@ Add to your assistant text:
1422
1581
  eval_window_minutes: <optional>
1423
1582
  </expected>
1424
1583
 
1425
- No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces it AFTER. Both gates are now wired.`;
1584
+ No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces it AFTER. Both gates are now wired.`, `stop:dalio-expected:${hadNonTrivialAction}:${!!dalioExpectedMatch}:${dalioHasMeasurablePredicate}`, gateSessionId);
1426
1585
 
1427
1586
  audit('block-dalio-expected-missing', `hadNonTrivialAction=${hadNonTrivialAction} expectedPresent=${!!dalioExpectedMatch} measurable=${dalioHasMeasurablePredicate}`);
1428
1587
  emitHarnessFooter({
@@ -1445,7 +1604,37 @@ No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces
1445
1604
  // Per feedback_no_timeouts_doctrine.md: no AbortController/setTimeout timeout.
1446
1605
  {
1447
1606
  const DALIO_LEDGER_PATH = `${HOME}/.claude/.aria-dalio-ledger.jsonl`;
1448
- const ARIA_SOUL_DECISIONS_URL = 'http://aria-soul.aria.svc.cluster.local:8080/api/decisions';
1607
+ let postReceipt = null;
1608
+ try {
1609
+ const post = await runtimeMizanPost(
1610
+ event.session_id || 'claude-code',
1611
+ assistantText.slice(0, 8000),
1612
+ {
1613
+ message: lastUserMessage || `stop-gate turn (${assistantText.length} chars)`,
1614
+ plannedApproach: lastActionSummary || 'Finalize and validate the just-emitted Claude response.',
1615
+ platform: 'claude-code',
1616
+ stage: 'hook-stop-post',
1617
+ },
1618
+ sessionMizanState?.receipt?.receiptId || null,
1619
+ );
1620
+ postReceipt = post?.receipt || null;
1621
+ if (postReceipt) {
1622
+ saveMizanReceiptState(event.session_id || 'claude-code', {
1623
+ ...(sessionMizanState || {}),
1624
+ updatedAt: new Date().toISOString(),
1625
+ sessionId: event.session_id || 'claude-code',
1626
+ postReceipt,
1627
+ postResult: post?.result || null,
1628
+ postContract: post?.contract || null,
1629
+ postSummary: post?.summary || null,
1630
+ });
1631
+ }
1632
+ } catch (postErr) {
1633
+ console.error(
1634
+ `[aria-stop-gate] MIZAN POST FAILED — runtime receipt not recorded. Error: ${postErr instanceof Error ? postErr.message : String(postErr)}`,
1635
+ );
1636
+ audit('mizan-post-failed', `session=${event.session_id || 'claude-code'} err=${String(postErr).slice(0, 200)}`);
1637
+ }
1449
1638
 
1450
1639
  const ledgerEntry = {
1451
1640
  ts: new Date().toISOString(),
@@ -1469,7 +1658,11 @@ No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces
1469
1658
  measurable_type: 'state_string',
1470
1659
  }
1471
1660
  : null,
1472
- source: 'claude-code-stop-gate',
1661
+ metadata: {
1662
+ pre_receipt_id: sessionMizanState?.receipt?.receiptId || null,
1663
+ post_receipt_id: postReceipt?.receiptId || null,
1664
+ },
1665
+ source: 'claude-code-stop-gate-runtime',
1473
1666
  model_used: 'claude-opus-4-7',
1474
1667
  };
1475
1668
 
@@ -1484,40 +1677,20 @@ No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces
1484
1677
  );
1485
1678
  }
1486
1679
 
1487
- // POST to aria-soul decision API — fire-and-forget, but LOUD on error
1488
- const dalioHarnessToken = process.env.ARIA_HARNESS_TOKEN
1489
- || (isOwnerTier() ? (process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '') : '');
1490
-
1491
- fetch(ARIA_SOUL_DECISIONS_URL, {
1492
- method: 'POST',
1493
- headers: {
1494
- 'Content-Type': 'application/json',
1495
- ...(dalioHarnessToken ? { Authorization: `Bearer ${dalioHarnessToken}` } : {}),
1496
- },
1497
- body: JSON.stringify(ledgerEntry),
1498
- }).then((resp) => {
1499
- if (!resp.ok) {
1500
- // LOUD telemetry per feedback_canonical_secrets_governance.md
1501
- console.error(
1502
- `[aria-stop-gate] DALIO POST FAILED — aria-soul responded HTTP ${resp.status}. ` +
1503
- `Local mirror written to ${DALIO_LEDGER_PATH}. Session: ${ledgerEntry.session_id}`,
1504
- );
1505
- audit('dalio-post-failed', `http=${resp.status} session=${ledgerEntry.session_id}`);
1506
- } else {
1507
- audit('dalio-post-ok', `session=${ledgerEntry.session_id} action=${lastActionSummary.slice(0, 80)}`);
1508
- }
1509
- }).catch((err) => {
1510
- // Network failure — LOUD, never silent
1680
+ try {
1681
+ await runtimeDecisionLog(ledgerEntry);
1682
+ audit('dalio-post-ok', `session=${ledgerEntry.session_id} action=${lastActionSummary.slice(0, 80)}`);
1683
+ } catch (err) {
1511
1684
  console.error(
1512
- `[aria-stop-gate] DALIO POST NETWORK ERROR could not reach ${ARIA_SOUL_DECISIONS_URL}. ` +
1685
+ `[aria-stop-gate] DALIO POST FAILEDruntime /decision/log rejected or unreachable. ` +
1513
1686
  `Local mirror written to ${DALIO_LEDGER_PATH}. Error: ${err instanceof Error ? err.message : String(err)}`,
1514
1687
  );
1515
- audit('dalio-post-network-err', `err=${String(err).slice(0, 200)} session=${ledgerEntry.session_id}`);
1516
- });
1688
+ audit('dalio-post-runtime-err', `err=${String(err).slice(0, 200)} session=${ledgerEntry.session_id}`);
1689
+ }
1517
1690
  }
1518
1691
 
1519
- // Block — non-trivial response without 4+ substantive lenses.
1520
- const reason = `Aria Stop-gate: non-trivial assistant response without 4+ substantive cognition lenses. Found ${cog.count}/${REQUIRED_LENSES}+ (lenses: ${cog.names.join(', ') || 'none'}). Doctrine is action-coupled — text decisions ARE actions, and reflexive replies fail this gate the same way reflexive Bash does.
1692
+ // Block — non-trivial response without all required substantive lenses.
1693
+ const reason = withLoopDirective(`Aria Stop-gate: non-trivial assistant response without all required substantive cognition lenses. Found ${cog.count}/${REQUIRED_LENSES} (lenses: ${cog.names.join(', ') || 'none'}). Doctrine is action-coupled — text decisions ARE actions, and reflexive replies fail this gate the same way reflexive Bash does.
1521
1694
 
1522
1695
  Re-emit the response with substantive lens application BEFORE drafting. Each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content:
1523
1696
 
@@ -1534,7 +1707,7 @@ Re-emit the response with substantive lens application BEFORE drafting. Each len
1534
1707
 
1535
1708
  The block reflects work done BEFORE drafting. Don't emit it as ceremony; apply each lens as a thinking tool. Substance check defeats ritual emission.
1536
1709
 
1537
- No per-command bypass (mirrors aria-pre-tool-gate.mjs v3 doctrine). No env-var disable path either — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`;
1710
+ No per-command bypass (mirrors aria-pre-tool-gate.mjs v3 doctrine). No env-var disable path either — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`, `stop:lens-missing:${cog.count}`, gateSessionId);
1538
1711
 
1539
1712
  audit(`block`, `lenses=${cog.count}/${REQUIRED_LENSES} chars=${assistantText.length}`);
1540
1713
  emitHarnessFooter({
@@ -1,14 +1,15 @@
1
1
  export const LENS_NAMES_OLDER = ['nur', 'mizan', 'hikma', 'tafakkur', 'tadabbur', 'ilham', 'wahi', 'firasah'];
2
2
  export const LENS_NAMES_NEWER = ['zahir', 'batin', 'sabab', 'hikmah', 'aqibah', 'ilham', 'meta', 'fitrah'];
3
- export const LENS_NAMES_GENERIC = ['perception', 'balance', 'wisdom', 'reflection', 'foresight', 'insight', 'revelation', 'discernment'];
4
3
 
5
4
  export const PRIMARY_OWNER_LENS_NAMES = LENS_NAMES_OLDER;
6
- export const PRIMARY_CLIENT_LENS_NAMES = LENS_NAMES_GENERIC;
5
+ // Readability must not rewrite the lens semantics. Client surfaces may need
6
+ // friendlier prose inside each lens, but the visible lens labels themselves
7
+ // stay canonical so the meaning is preserved.
8
+ export const PRIMARY_CLIENT_LENS_NAMES = LENS_NAMES_OLDER;
7
9
 
8
10
  export const ALL_LENS_NAMES = [...new Set([
9
11
  ...LENS_NAMES_OLDER,
10
12
  ...LENS_NAMES_NEWER,
11
- ...LENS_NAMES_GENERIC,
12
13
  ])];
13
14
 
14
15
  export function lensNamesForTier(isOwner) {
@@ -19,7 +20,7 @@ export function canonicalLensCorrectionText() {
19
20
  return [
20
21
  `Owner canonical lenses: ${LENS_NAMES_OLDER.join(', ')}.`,
21
22
  `Owner mapped counterparts that must remain represented in phase reasoning and receipts: ${LENS_NAMES_NEWER.join(', ')}.`,
22
- `Generic client-safe alternatives: ${LENS_NAMES_GENERIC.join(', ')}.`,
23
+ `Canonical lens labels are preserved on every surface. Readability comes from the explanatory prose inside each lens, never from renaming the lens itself.`,
23
24
  ].join(' ');
24
25
  }
25
26
 
@@ -57,7 +58,7 @@ export function detectCognitionLenses(text, {
57
58
  }
58
59
 
59
60
  const matchedSet =
60
- [LENS_NAMES_OLDER, LENS_NAMES_NEWER, LENS_NAMES_GENERIC]
61
+ [LENS_NAMES_OLDER, LENS_NAMES_NEWER]
61
62
  .find((candidateSet) => candidateSet.every((name) => names.includes(name))) || null;
62
63
 
63
64
  return { count: names.length, names, matchedSet };
@@ -0,0 +1,50 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+
4
+ const MAX_RECORDS = 300;
5
+
6
+ export function registerGateBlock({
7
+ gate,
8
+ sessionId,
9
+ signature,
10
+ statePath,
11
+ threshold = 2,
12
+ windowMs = 10 * 60 * 1000,
13
+ }) {
14
+ const now = Date.now();
15
+ let rows = [];
16
+ try {
17
+ if (existsSync(statePath)) {
18
+ const raw = JSON.parse(readFileSync(statePath, 'utf8'));
19
+ if (Array.isArray(raw)) rows = raw;
20
+ }
21
+ } catch {}
22
+
23
+ const freshRows = rows.filter((row) =>
24
+ row &&
25
+ typeof row.ts === 'number' &&
26
+ typeof row.sessionId === 'string' &&
27
+ typeof row.gate === 'string' &&
28
+ typeof row.signature === 'string' &&
29
+ (now - row.ts) <= windowMs,
30
+ );
31
+
32
+ const priorMatches = freshRows.filter((row) =>
33
+ row.sessionId === sessionId &&
34
+ row.gate === gate &&
35
+ row.signature === signature,
36
+ );
37
+
38
+ freshRows.push({ ts: now, sessionId, gate, signature });
39
+
40
+ try {
41
+ mkdirSync(dirname(statePath), { recursive: true });
42
+ writeFileSync(statePath, JSON.stringify(freshRows.slice(-MAX_RECORDS), null, 2) + '\n');
43
+ } catch {}
44
+
45
+ return {
46
+ loopDetected: priorMatches.length >= threshold,
47
+ priorCount: priorMatches.length,
48
+ totalCount: priorMatches.length + 1,
49
+ };
50
+ }
@@ -0,0 +1,121 @@
1
+ function withGlobalRegex(regex) {
2
+ if (!(regex instanceof RegExp)) return null;
3
+ const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
4
+ return new RegExp(regex.source, flags);
5
+ }
6
+
7
+ export function normalizeRole(entry) {
8
+ if (entry?.message?.role) return entry.message.role;
9
+ if (entry?.role) return entry.role;
10
+ if (entry?.type === 'assistant' || entry?.type === 'user') return entry.type;
11
+ return null;
12
+ }
13
+
14
+ export function normalizeContent(entry) {
15
+ return entry?.message?.content ?? entry?.content ?? [];
16
+ }
17
+
18
+ export function extractTextFromContent(content) {
19
+ if (Array.isArray(content)) {
20
+ return content
21
+ .filter((block) => block && block.type === 'text')
22
+ .map((block) => (typeof block.text === 'string' ? block.text : ''))
23
+ .filter(Boolean)
24
+ .join('\n');
25
+ }
26
+ return typeof content === 'string' ? content : '';
27
+ }
28
+
29
+ export function isToolResultOnlyContent(content) {
30
+ return (
31
+ Array.isArray(content) &&
32
+ content.length > 0 &&
33
+ content.every((block) => block && block.type === 'tool_result')
34
+ );
35
+ }
36
+
37
+ export function isMostlySystemReminder(text, systemReminderRx, threshold = 0.6) {
38
+ if (!text || !(systemReminderRx instanceof RegExp)) return false;
39
+ const reminderRx = withGlobalRegex(systemReminderRx);
40
+ if (!reminderRx) return false;
41
+ const matches = text.match(reminderRx) || [];
42
+ if (matches.length === 0) return false;
43
+ const reminderChars = matches.reduce((sum, match) => sum + match.length, 0);
44
+ return reminderChars / Math.max(1, text.length) >= threshold;
45
+ }
46
+
47
+ export function collectRecentAssistantTextsFromMessages(messages, {
48
+ compactSummaryRx = null,
49
+ hardLookbackCap = 50,
50
+ systemReminderRx = null,
51
+ systemReminderThreshold = 0.6,
52
+ userBoundariesToCross = 1,
53
+ } = {}) {
54
+ if (!Array.isArray(messages) || messages.length === 0) return [];
55
+ const recentAssistantTexts = [];
56
+ let scanned = 0;
57
+ let userBoundariesCrossed = 0;
58
+
59
+ for (let i = messages.length - 1; i >= 0 && scanned < hardLookbackCap; i--) {
60
+ const entry = messages[i];
61
+ const role = normalizeRole(entry);
62
+ const content = normalizeContent(entry);
63
+ if (role === 'user') {
64
+ if (isToolResultOnlyContent(content)) continue;
65
+ const textContent = extractTextFromContent(content);
66
+ if (isMostlySystemReminder(textContent, systemReminderRx, systemReminderThreshold)) {
67
+ continue;
68
+ }
69
+ userBoundariesCrossed++;
70
+ if (userBoundariesCrossed > userBoundariesToCross) break;
71
+ continue;
72
+ }
73
+ if (role !== 'assistant') continue;
74
+ scanned++;
75
+ const text = extractTextFromContent(content);
76
+ if (!text) continue;
77
+ if (compactSummaryRx instanceof RegExp && compactSummaryRx.test(text) && text.length > 4000) {
78
+ continue;
79
+ }
80
+ recentAssistantTexts.push(text);
81
+ }
82
+
83
+ return recentAssistantTexts.reverse();
84
+ }
85
+
86
+ export function collectTurnWindowFromMessages(messages, {
87
+ systemReminderRx = null,
88
+ systemReminderThreshold = 0.6,
89
+ } = {}) {
90
+ if (!Array.isArray(messages) || messages.length === 0) {
91
+ return { assistantText: '', lastUserMessage: '' };
92
+ }
93
+
94
+ const textChunks = [];
95
+ let lastUserMessage = '';
96
+
97
+ for (let i = messages.length - 1; i >= 0; i--) {
98
+ const entry = messages[i];
99
+ const role = normalizeRole(entry);
100
+ const content = normalizeContent(entry);
101
+
102
+ if (role === 'user') {
103
+ if (isToolResultOnlyContent(content)) continue;
104
+ const textContent = extractTextFromContent(content);
105
+ if (isMostlySystemReminder(textContent, systemReminderRx, systemReminderThreshold)) {
106
+ continue;
107
+ }
108
+ lastUserMessage = textContent || lastUserMessage;
109
+ break;
110
+ }
111
+
112
+ if (role !== 'assistant') continue;
113
+ const text = extractTextFromContent(content);
114
+ if (text) textChunks.push(text);
115
+ }
116
+
117
+ return {
118
+ assistantText: textChunks.reverse().join('\n\n'),
119
+ lastUserMessage,
120
+ };
121
+ }