@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
@@ -42,10 +42,21 @@ import { dirname } from 'node:path';
42
42
  import { homedir } from 'node:os';
43
43
  import { spawnSync } from 'node:child_process';
44
44
  import { createHmac, randomBytes as cryptoRandomBytes } from 'node:crypto';
45
+ import { lensNamesForTier } from './lib/canonical-lenses.mjs';
46
+ import { registerGateBlock } from './lib/gate-loop-state.mjs';
47
+ import {
48
+ collectRecentAssistantTextsFromMessages,
49
+ extractTextFromContent,
50
+ isMostlySystemReminder,
51
+ isToolResultOnlyContent,
52
+ normalizeContent,
53
+ normalizeRole,
54
+ } from './lib/hook-message-window.mjs';
45
55
 
46
56
  const HOME = process.env.HOME || '/tmp';
47
57
  const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
48
58
  const HEARTBEAT = `${HOME}/.claude/aria-pre-tool-gate-heartbeat.jsonl`;
59
+ const GATE_LOOP_STATE_PATH = `${HOME}/.claude/.aria-gate-loop-state.json`;
49
60
 
50
61
  // ── Heartbeat OUTSIDE the crash boundary (doctrine #123) ───────────────
51
62
  // Per feedback_ledger_writes_outside_crash_boundary.md + doctrine #124
@@ -321,12 +332,29 @@ function actionMatchesPattern(action, pattern, target) {
321
332
  }
322
333
  }
323
334
 
335
+ function isOperationalMaintenanceCommand(command) {
336
+ const cmd = String(command || '').trim().replace(/\s+/g, ' ');
337
+ if (!cmd || /[;&|`$()]/.test(cmd)) return false;
338
+
339
+ // A rollout restart requeues existing pod templates; it does not change images,
340
+ // manifests, or cluster policy. Keep this narrow so deploy/apply/delete still gate.
341
+ const kubectlGlobalFlag = String.raw`(?:--(?:namespace|context|kubeconfig|as|as-group|cluster|user|server|request-timeout|field-manager)(?:=|\s+)\S+|-[A-Za-z](?:\s+\S+)?)`;
342
+ const kubectlTailFlag = String.raw`(?:--(?:namespace|context|request-timeout|field-manager|selector)(?:=|\s+)\S+|-[A-Za-z](?:\s+\S+)?)`;
343
+ const workloadTarget = String.raw`(?:deploy(?:ment)?|statefulset|sts|daemonset|ds)(?:\/[^\s]+|\s+[^\s-][^\s]*)`;
344
+ const rolloutRestartRx = new RegExp(
345
+ String.raw`^kubectl(?:\s+${kubectlGlobalFlag})*\s+rollout\s+restart\s+${workloadTarget}(?:\s+${kubectlTailFlag})*$`,
346
+ 'i',
347
+ );
348
+ return rolloutRestartRx.test(cmd);
349
+ }
350
+
324
351
  function classifyToolForBinding(toolName, command, filePath) {
325
352
  // Map Claude Code tool + payload to a binding-vocabulary action.
326
353
  if (toolName === 'Read' || toolName === 'Glob' || toolName === 'Grep') return { action: 'read', target: filePath || '' };
327
354
  if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') return { action: 'edit', target: filePath || '' };
328
355
  if (toolName === 'Bash') {
329
356
  const cmd = String(command || '');
357
+ if (isOperationalMaintenanceCommand(cmd)) return { action: 'kubectl_maintenance', target: cmd.slice(0, 80) };
330
358
  if (/^kubectl\s+(get|describe|logs|exec\s+\S+\s+--\s+printenv|top|version|api-resources)\b/.test(cmd)) return { action: 'kubectl_get', target: cmd.slice(0, 80) };
331
359
  if (/^kubectl\s+(apply|delete|set|patch|rollout|scale|drain|cordon)\b/.test(cmd)) return { action: 'kubectl_apply', target: cmd.slice(0, 80) };
332
360
  if (/curl\s+.*\/api\/harness\/(delegate|codex|army|plan)/.test(cmd)) return { action: 'consult', target: 'harness' };
@@ -446,12 +474,11 @@ const MEASURABLE_PREDICATE_RX = /(?:>=|<=|==|!=|>|<|≥|≤)\s*\d+(?:\.\d+)?(?:m
446
474
  // Any <expected> block containing ONLY these (no actual predicate) is rejected.
447
475
  const QUALITATIVE_DRIFT_RX = /\b(?:better(?:er)?|improved?(?:ment)?|more\s+robust|should\s+(?:work|pass|succeed|run|fix)|more\s+reliable|cleaner|less\s+error[-_\s]?prone|nicer|smoother|faster[-\s]?loading|higher[-\s]?quality|more\s+stable|looks\s+(?:good|better|right))\b/i;
448
476
 
449
- // ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
477
+ // ── Canonical lens labeling (Phase 11 #59 corrected) ────────────────────────
450
478
  //
451
- // Aria's Arabic cognition lens names are proprietary IP. On Hamza's surface
452
- // (isHamza=true / surface line contains hamza:true) the canonical names are shown.
453
- // On any client surface the gate uses neutral generic labels so the IP
454
- // vocabulary never appears in client-facing block-reason text.
479
+ // The lens names themselves carry meaning and must remain canonical on every
480
+ // surface. Readability comes from the explanatory prose inside each lens, not
481
+ // by swapping the lens label for a flattened stand-in.
455
482
  //
456
483
  // Tier is read from the most recent harness-via-sdk packet cache at
457
484
  // ~/.claude/.aria-harness-last-packet.json. Two detection paths:
@@ -484,12 +511,9 @@ function resolveOwnerTier() {
484
511
 
485
512
  const IS_OWNER = resolveOwnerTier();
486
513
 
487
- // Canonical (owner) lens namesAria's Arabic cognition vocabulary.
488
- const LENS_NAMES_CANONICAL = ['nur', 'mizan', 'hikma', 'tafakkur', 'tadabbur', 'ilham', 'wahi', 'firasah'];
489
- // Generic (client) lens labels — neutral, IP-neutral equivalents.
490
- const LENS_NAMES_GENERIC = ['perception', 'balance', 'wisdom', 'reflection', 'foresight', 'insight', 'revelation', 'discernment'];
491
- // Active label set for this execution — determined by tier.
492
- const LENS_NAMES = IS_OWNER ? LENS_NAMES_CANONICAL : LENS_NAMES_GENERIC;
514
+ // Active visible label set for this execution canonical labels on every
515
+ // surface. Tier only affects other policy surfaces, not lens semantics.
516
+ const LENS_NAMES = lensNamesForTier(IS_OWNER);
493
517
 
494
518
  // Doctrine memory filenames are Aria-side substrate IP. On client surfaces
495
519
  // replace them with generic descriptions in block-reason text.
@@ -504,6 +528,7 @@ function docRef(canonicalFilename, genericDescription) {
504
528
  // colon, not a placeholder template). The substance check (added
505
529
  // 2026-04-26) defeats ritual emission — `nur: ok` no longer counts.
506
530
  const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
531
+ const APPLIED_COGNITION_BLOCK_RX = /<applied_cognition>([\s\S]*?)<\/applied_cognition>/i;
507
532
  // Hamza directive 2026-04-28: 8-lens enforcement, not 4-of-8. The earlier
508
533
  // REQUIRED_LENSES=4 was a regression — owner caught it ("i no longer see
509
534
  // 8 lens cognition per turn"). All 8 canonical lenses must appear with
@@ -547,18 +572,28 @@ const SHORT_BASH_LIMIT = 30;
547
572
  // (b) Single-line: `# cognition: <label>=<text>; <label>=<text>; ...`
548
573
  //
549
574
  // Substance check applies (≥SUBSTANCE_MIN_CHARS, not a `<placeholder>`).
550
- // 4+ substantive lenses inline → gate passes, no transcript scan
575
+ // All required substantive lenses inline → gate passes, no transcript scan
551
576
  // needed. Cognition becomes a property of THE ACTION, not of some
552
577
  // message somewhere — the deepest reading of "cognition before action."
553
578
  //
554
- // Inline regex is built from the active LENS_NAMES so both canonical
555
- // and generic label sets are accepted in their respective tiers.
579
+ // Inline regex is built from the active canonical LENS_NAMES. We do not
580
+ // accept flattened generic aliases because they change the lens meaning.
556
581
  const _INLINE_LENS_PATTERN = LENS_NAMES.join('|');
557
582
  const INLINE_LENS_LINE_RX_GLOBAL = new RegExp(
558
583
  `^\\s*#\\s*(${_INLINE_LENS_PATTERN})\\s*:\\s*(.+)$`,
559
584
  'gim',
560
585
  );
561
586
  const INLINE_COGNITION_HEADER_RX = /^\s*#\s*cognition\s*:\s*(.+)$/im;
587
+ const INLINE_EXPECTED_LINE_RX = /^\s*#\s*expected\s*:\s*(.+)$/gim;
588
+ const INLINE_VERIFY_LINE_RX = /^\s*#\s*verify\s*:\s*(.+)$/gim;
589
+
590
+ const VERIFY_REQUIRED_FIELDS = [
591
+ { rx: /\btarget\s*:/i, name: 'target' },
592
+ { rx: /\brole\s*:/i, name: 'role' },
593
+ { rx: /\bverified\s*:/i, name: 'verified' },
594
+ { rx: /\brollback\s*:/i, name: 'rollback' },
595
+ { rx: /\baxiom\s*:/i, name: 'axiom' },
596
+ ];
562
597
 
563
598
  function detectInlineCognition(cmd) {
564
599
  const names = [];
@@ -593,6 +628,152 @@ function detectInlineCognition(cmd) {
593
628
  return { count: names.length, names, hasSubstrateCite };
594
629
  }
595
630
 
631
+ function validateAppliedCognitionContract(text, cmdText = '') {
632
+ const bodyText = `${String(text || '')}\n${String(cmdText || '')}`;
633
+ const block = bodyText.match(APPLIED_COGNITION_BLOCK_RX);
634
+ const inlineBody = String(cmdText || '')
635
+ .split('\n')
636
+ .filter((line) => /^\s*#\s*(?:decision[_ -]?delta|dominant[_ -]?domain|binds[_ -]?to|expected[_ -]?predicate|artifact[_ -]?change)\s*:/i.test(line))
637
+ .join('\n');
638
+ const contractBody = block ? block[1] : inlineBody;
639
+ if (!contractBody) return { ok: false, violations: ['missing <applied_cognition> contract or inline applied-cognition comments'] };
640
+ const required = [
641
+ ['decision_delta', /\bdecision[_ -]?delta\s*:/i],
642
+ ['dominant_domain', /\bdominant[_ -]?domain\s*:/i],
643
+ ['binds_to', /\bbinds[_ -]?to\s*:/i],
644
+ ['expected_predicate', /\bexpected[_ -]?predicate\s*:/i],
645
+ ['artifact_change', /\bartifact[_ -]?change\s*:/i],
646
+ ];
647
+ const violations = [];
648
+ for (const [name, rx] of required) {
649
+ if (!rx.test(contractBody)) violations.push(`missing ${name}`);
650
+ }
651
+ if (/decision[_ -]?delta\s*:\s*(?:none|n\/a|no change|unchanged|same)/i.test(contractBody)) {
652
+ violations.push('decision_delta says cognition changed nothing');
653
+ }
654
+ return { ok: violations.length === 0, violations, body: contractBody.trim() };
655
+ }
656
+
657
+ function normalizeInlineDirectiveBody(rawBody) {
658
+ if (!rawBody) return '';
659
+ const segments = String(rawBody)
660
+ .split(/;|\s+(?=[a-z_][a-z0-9_-]*\s*[=:])/i)
661
+ .map((segment) => segment.trim())
662
+ .filter(Boolean);
663
+ const lines = [];
664
+ for (const segment of segments) {
665
+ const match = segment.match(/^([a-z_][a-z0-9_-]*)\s*[=:]\s*(.+)$/i);
666
+ if (match) {
667
+ lines.push(`${match[1]}: ${match[2].trim()}`);
668
+ continue;
669
+ }
670
+ lines.push(segment);
671
+ }
672
+ return lines.join('\n').trim();
673
+ }
674
+
675
+ function extractInlineDirectiveBody(cmd, directiveRx) {
676
+ if (!cmd || !(directiveRx instanceof RegExp)) return '';
677
+ directiveRx.lastIndex = 0;
678
+ const lines = [];
679
+ let match;
680
+ while ((match = directiveRx.exec(cmd)) !== null) {
681
+ const normalized = normalizeInlineDirectiveBody(match[1]);
682
+ if (normalized) lines.push(normalized);
683
+ }
684
+ return lines.join('\n').trim();
685
+ }
686
+
687
+ function scoreVerifyFields(body, fields) {
688
+ if (!body) return 0;
689
+ return fields.reduce((count, { rx }) => count + (rx.test(body) ? 1 : 0), 0);
690
+ }
691
+
692
+ function extractCurrentTurnAssistantText(event, toolInput) {
693
+ const candidates = [
694
+ event.assistant_text,
695
+ event.assistantText,
696
+ event.draft,
697
+ event.text,
698
+ event.current_text,
699
+ event.currentText,
700
+ event.message,
701
+ event.prompt,
702
+ toolInput?.assistant_text,
703
+ toolInput?.assistantText,
704
+ toolInput?.text,
705
+ ];
706
+ const parts = [];
707
+ const seen = new Set();
708
+ for (const candidate of candidates) {
709
+ if (typeof candidate !== 'string') continue;
710
+ const text = candidate.trim();
711
+ if (!text || seen.has(text)) continue;
712
+ seen.add(text);
713
+ parts.push(text);
714
+ }
715
+ return parts.join('\n\n').trim();
716
+ }
717
+
718
+ function extractTextForUserCorrection(content) {
719
+ if (Array.isArray(content)) {
720
+ return content.filter((block) => block && block.type === 'text').map((block) => block.text || '').filter(Boolean).join('\n');
721
+ }
722
+ return typeof content === 'string' ? content : '';
723
+ }
724
+
725
+ function isRuntimeInjectedUserText(text) {
726
+ return /^\s*\[tool_result\b/i.test(text) ||
727
+ /(?:PreToolUse:[A-Za-z]+ hook blocking error|Stop hook feedback:|Aria pre-tool gate:|Aria stop-gate:|<system-reminder>|<task-notification>|task-notification)/i.test(text);
728
+ }
729
+
730
+ function collectRecentRealUserText(event, transcriptPath, limit = 6) {
731
+ const texts = [];
732
+ const seen = new Set();
733
+ const pushText = (text) => {
734
+ const normalized = String(text || '').trim();
735
+ if (!normalized || seen.has(normalized) || isRuntimeInjectedUserText(normalized)) return;
736
+ seen.add(normalized);
737
+ texts.push(normalized);
738
+ };
739
+ const messages = Array.isArray(event?.messages) ? event.messages : [];
740
+ for (let i = messages.length - 1; i >= 0 && texts.length < limit; i--) {
741
+ const role = messages[i]?.message?.role ?? messages[i]?.role ?? messages[i]?.type;
742
+ if (role !== 'user') continue;
743
+ const content = messages[i]?.message?.content ?? messages[i]?.content ?? [];
744
+ if (Array.isArray(content) && content.length > 0 && content.every((block) => block && block.type === 'tool_result')) continue;
745
+ pushText(extractTextForUserCorrection(content));
746
+ }
747
+ if (transcriptPath && existsSync(transcriptPath) && texts.length < limit) {
748
+ try {
749
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
750
+ for (let i = lines.length - 1; i >= 0 && texts.length < limit; i--) {
751
+ let entry;
752
+ try { entry = JSON.parse(lines[i]); } catch { continue; }
753
+ const role = entry?.message?.role ?? entry?.role ?? entry?.type;
754
+ if (role !== 'user') continue;
755
+ const content = entry?.message?.content ?? entry?.content ?? [];
756
+ if (Array.isArray(content) && content.length > 0 && content.every((block) => block && block.type === 'tool_result')) continue;
757
+ pushText(extractTextForUserCorrection(content));
758
+ }
759
+ } catch {}
760
+ }
761
+ return texts.join('\n\n');
762
+ }
763
+
764
+ function userCorrectionBlocksCommand(userText, command) {
765
+ const text = String(userText || '');
766
+ const cmd = String(command || '');
767
+ if (!text || !cmd) return null;
768
+ const k8sMutation = /\bkubectl\s+(?:apply|delete|set\s+image|patch|replace|create|scale|rollout\s+(?:restart|undo))\b|scripts\/deploy-|\bdocker\s+push\b/i.test(cmd);
769
+ const explicitStop = /\b(?:stop|do\s+not|don't|quit|cease)\b[\s\S]{0,160}\b(?:deploy|redeploy|restart|rollout|kubectl|command|pods?)\b/i.test(text);
770
+ if (explicitStop && k8sMutation) return 'recent user explicitly told the agent to stop deploy/restart command attempts';
771
+ const macTargetInCommand = /\bmlx-mac\b|\bdeployment\/mlx-mac|\bdeploy(?:ment)?\s+mlx-mac/i.test(cmd);
772
+ const macContradiction = /\b(?:mac\s+lanes?|mac\s+pods?|mlx-mac)\b[\s\S]{0,140}\b(?:not\s+pods?|no\s+such\s+thing|non[-\s]?existent|do(?:es)?\s+not\s+exist|don't\s+exist)\b|\b(?:no\s+such\s+thing|non[-\s]?existent|do(?:es)?\s+not\s+exist|don't\s+exist)\b[\s\S]{0,140}\b(?:mac\s+lanes?|mac\s+pods?|mlx-mac|pods?)\b/i.test(text);
773
+ if (macTargetInCommand && macContradiction) return 'recent user contradicted the mlx-mac Kubernetes-pod/deployment assumption';
774
+ return null;
775
+ }
776
+
596
777
  // Substance-checking lens detection (added 2026-04-26 per Hamza's
597
778
  // gate-improvement doctrine: form-only emission must not satisfy
598
779
  // the gate). For each lens, look for `<lens>: <content>` and verify
@@ -722,6 +903,9 @@ const HARNESS_URL =
722
903
  process.env.ARIA_HARNESS_BASE_URL ||
723
904
  process.env.ARIA_HARNESS_URL ||
724
905
  'https://harness.ariasos.com';
906
+ const RUNTIME_BASE_URL =
907
+ process.env.ARIA_RUNTIME_URL ||
908
+ 'http://127.0.0.1:4319';
725
909
  const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
726
910
  const LOG_PUSH_DISABLED = process.env.ARIA_COGNITION_PUSH === 'off';
727
911
  const MAX_IN_FLIGHT = 16;
@@ -846,6 +1030,7 @@ try {
846
1030
 
847
1031
  const toolName = event.tool_name ?? event.toolName ?? '';
848
1032
  const toolInput = event.tool_input ?? event.toolInput ?? {};
1033
+ const transcriptPath = event.transcript_path ?? event.transcriptPath;
849
1034
 
850
1035
  // Gate every action tool — every tool that mutates state must go through
851
1036
  // cognition challenge. Hamza 2026-04-26: "regardless if u arent even
@@ -868,6 +1053,26 @@ const cmdPreview = toolName === 'Bash'
868
1053
  ? cmd.slice(0, 80).replace(/\s+/g, ' ')
869
1054
  : `${toolName} ${filePath || '(no path)'}`.slice(0, 80);
870
1055
 
1056
+ if (toolName === 'Bash') {
1057
+ const recentUserText = collectRecentRealUserText(event, transcriptPath);
1058
+ const correctionBlockReason = userCorrectionBlocksCommand(recentUserText, cmd);
1059
+ if (correctionBlockReason) {
1060
+ const reason = `Aria pre-tool gate: USER-CORRECTION hard-block.
1061
+
1062
+ ${correctionBlockReason}.
1063
+
1064
+ The next assistant response must stop the command loop, quote the user's correction in one sentence, and re-evaluate the target from substrate before any further mutation. Do not retry this Bash command shape.`;
1065
+ audit('block-user-correction-override', cmdPreview);
1066
+ emitBlock(reason, { source: 'pre-tool/user-correction' });
1067
+ process.exit(2);
1068
+ }
1069
+ }
1070
+
1071
+ if (toolName === 'Bash' && isOperationalMaintenanceCommand(cmd)) {
1072
+ audit('allow-operational-maintenance kubectl-rollout-restart', cmdPreview);
1073
+ process.exit(0);
1074
+ }
1075
+
871
1076
  // V3: per-command bypass removed entirely. The only escape valves are:
872
1077
  // (1) Trivial-bash whitelist — short read-only commands pass without cognition
873
1078
  // (2) ~/.claude/settings.json hook removal — visible user action Hamza controls
@@ -951,8 +1156,27 @@ const HARD_LOOKBACK_CAP = 50;
951
1156
  // window since the substance check defends quality regardless.
952
1157
  const USER_BOUNDARIES_TO_CROSS = 5;
953
1158
 
954
- const transcriptPath = event.transcript_path ?? event.transcriptPath;
955
1159
  const recentAssistantTexts = [];
1160
+ const recentAssistantTextSet = new Set();
1161
+ function appendAssistantTexts(texts) {
1162
+ for (const text of texts) {
1163
+ if (!text || recentAssistantTextSet.has(text)) continue;
1164
+ recentAssistantTexts.push(text);
1165
+ recentAssistantTextSet.add(text);
1166
+ }
1167
+ }
1168
+
1169
+ const eventMessages = Array.isArray(event.messages) ? event.messages : [];
1170
+ const eventAssistantTexts = collectRecentAssistantTextsFromMessages(eventMessages, {
1171
+ compactSummaryRx: COMPACT_SUMMARY_RX,
1172
+ hardLookbackCap: HARD_LOOKBACK_CAP,
1173
+ systemReminderRx: SYSTEM_REMINDER_RX,
1174
+ systemReminderThreshold: SYSTEM_REMINDER_THRESHOLD,
1175
+ userBoundariesToCross: USER_BOUNDARIES_TO_CROSS,
1176
+ });
1177
+ appendAssistantTexts(eventAssistantTexts);
1178
+
1179
+ const transcriptAssistantTexts = [];
956
1180
  if (transcriptPath && existsSync(transcriptPath)) {
957
1181
  try {
958
1182
  const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
@@ -961,7 +1185,8 @@ if (transcriptPath && existsSync(transcriptPath)) {
961
1185
  for (let i = lines.length - 1; i >= 0 && scanned < HARD_LOOKBACK_CAP; i--) {
962
1186
  try {
963
1187
  const m = JSON.parse(lines[i]);
964
- const role = m.message?.role ?? m.role;
1188
+ const role = normalizeRole(m);
1189
+ const content = normalizeContent(m);
965
1190
  if (role === 'user') {
966
1191
  // Skip messages that aren't real user input:
967
1192
  // (a) tool_result blocks (runtime feeding back tool output)
@@ -969,57 +1194,36 @@ if (transcriptPath && existsSync(transcriptPath)) {
969
1194
  // task-notifications, gentle reminders) — runtime-
970
1195
  // authored, not user voice. Counting them eats the
971
1196
  // cognition lookback in tool-heavy or block-heavy turns.
972
- const content = m.message?.content ?? m.content ?? [];
973
- const isToolResultOnly =
974
- Array.isArray(content) &&
975
- content.length > 0 &&
976
- content.every((b) => b && b.type === 'tool_result');
977
- if (isToolResultOnly) continue;
1197
+ if (isToolResultOnlyContent(content)) continue;
978
1198
  // Inspect text content for system-reminder patterns.
979
- const textContent = Array.isArray(content)
980
- ? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
981
- : (typeof content === 'string' ? content : '');
982
- if (textContent) {
983
- // Compute reminder-span coverage as fraction of total
984
- // content. The /g flag on SYSTEM_REMINDER_RX returns ALL
985
- // spans; sum their lengths and compare to total.
986
- const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
987
- if (reminderMatches.length > 0) {
988
- const reminderChars = reminderMatches.reduce((sum, s) => sum + s.length, 0);
989
- const fraction = reminderChars / Math.max(1, textContent.length);
990
- if (fraction >= SYSTEM_REMINDER_THRESHOLD) continue;
991
- // Otherwise (user wrote substantive text alongside the
992
- // reminder — the non-reminder portion is >40% of content)
993
- // fall through and count as a real boundary.
994
- }
995
- }
1199
+ const textContent = extractTextFromContent(content);
1200
+ if (isMostlySystemReminder(textContent, SYSTEM_REMINDER_RX, SYSTEM_REMINDER_THRESHOLD)) continue;
996
1201
  userBoundariesCrossed++;
997
1202
  if (userBoundariesCrossed > USER_BOUNDARIES_TO_CROSS) break;
998
1203
  continue;
999
1204
  }
1000
1205
  if (role !== 'assistant') continue;
1001
1206
  scanned++;
1002
- const content = m.message?.content ?? m.content ?? [];
1003
- if (!Array.isArray(content)) continue;
1004
- const text = content
1005
- .filter((b) => b.type === 'text')
1006
- .map((b) => b.text)
1007
- .join('\n');
1207
+ const text = extractTextFromContent(content);
1008
1208
  if (!text) continue;
1009
1209
  // Skip compact-summary stubs — they live where assistant turns
1010
1210
  // used to be but are system-authored, not the model's voice.
1011
1211
  if (COMPACT_SUMMARY_RX.test(text) && text.length > 4000) continue;
1012
- recentAssistantTexts.push(text);
1212
+ transcriptAssistantTexts.push(text);
1013
1213
  } catch {}
1014
1214
  }
1015
1215
  } catch {}
1016
1216
  }
1217
+ appendAssistantTexts(transcriptAssistantTexts);
1218
+ const currentTurnAssistantText = extractCurrentTurnAssistantText(event, toolInput);
1219
+ if (currentTurnAssistantText) appendAssistantTexts([currentTurnAssistantText]);
1017
1220
 
1018
1221
  // Detect cognition / verify across the recent assistant window. Lenses
1019
1222
  // from any of the last N turns count; the gate is checking whether
1020
1223
  // cognition has been done recently, not whether it was done in the
1021
1224
  // literal last message (which can be a tool-only turn or a stub).
1022
1225
  const unionText = recentAssistantTexts.join('\n\n');
1226
+ const eventCog = detectCognitionLenses(eventAssistantTexts.join('\n\n'));
1023
1227
  const transcriptCog = detectCognitionLenses(unionText);
1024
1228
  // Combine inline-command cognition (preferred — action-coupled) with
1025
1229
  // transcript scan (fallback). Inline takes precedence on lens names;
@@ -1028,14 +1232,36 @@ const mergedLensSet = new Set([...inlineCog.names, ...transcriptCog.names]);
1028
1232
  const lensCount = mergedLensSet.size;
1029
1233
  const lensNames = [...mergedLensSet];
1030
1234
  const cogBlockBody = transcriptCog.blockBody;
1235
+ const inlineVerifyBody = extractInlineDirectiveBody(cmd, INLINE_VERIFY_LINE_RX);
1031
1236
  const verifyBodies = [...unionText.matchAll(/<verify>([\s\S]*?)<\/verify>/gi)]
1032
1237
  .map((m) => (m[1] || '').trim())
1033
1238
  .filter(Boolean);
1034
- const hasVerify = verifyBodies.length > 0;
1239
+ if (inlineVerifyBody) verifyBodies.push(inlineVerifyBody);
1240
+ const bestVerifyBody = (() => {
1241
+ if (verifyBodies.length === 0) return '';
1242
+ let bestBody = verifyBodies[0];
1243
+ let bestScore = -1;
1244
+ for (const body of verifyBodies) {
1245
+ const score = scoreVerifyFields(body, VERIFY_REQUIRED_FIELDS);
1246
+ if (score > bestScore) {
1247
+ bestScore = score;
1248
+ bestBody = body;
1249
+ }
1250
+ }
1251
+ return bestBody;
1252
+ })();
1253
+ const hasVerify = scoreVerifyFields(bestVerifyBody, VERIFY_REQUIRED_FIELDS) === VERIFY_REQUIRED_FIELDS.length;
1035
1254
  const hasCognition = lensCount >= REQUIRED_LENSES;
1255
+ const appliedContract = validateAppliedCognitionContract(unionText, cmd);
1036
1256
  const cognitionSource = inlineCog.count >= REQUIRED_LENSES
1037
1257
  ? 'inline-command'
1038
- : (transcriptCog.count >= REQUIRED_LENSES ? 'transcript-scan' : 'merged-or-insufficient');
1258
+ : eventCog.count >= REQUIRED_LENSES
1259
+ ? 'event-messages'
1260
+ : transcriptCog.count >= REQUIRED_LENSES
1261
+ ? 'recent-assistant-window'
1262
+ : lensCount >= REQUIRED_LENSES
1263
+ ? 'merged-recent-window'
1264
+ : 'merged-or-insufficient';
1039
1265
  // Substrate-citation visibility (Phase 11 will promote to block-mode once
1040
1266
  // telemetry shows healthy substrate-cite rate). For now: log the metric so
1041
1267
  // we can audit substrate-grounded vs ceremonial cognition over time.
@@ -1079,10 +1305,86 @@ function pushDecision(decision, reasonText) {
1079
1305
  hasVerify,
1080
1306
  hasCognition,
1081
1307
  hasSubstrateCite,
1308
+ appliedCognitionContractOk: appliedContract.ok,
1082
1309
  },
1083
1310
  });
1084
1311
  }
1085
1312
 
1313
+ function withLoopDirective(reasonText, gateSignature) {
1314
+ const loop = registerGateBlock({
1315
+ gate: 'pre-tool',
1316
+ sessionId,
1317
+ signature: gateSignature,
1318
+ statePath: GATE_LOOP_STATE_PATH,
1319
+ });
1320
+ if (!loop.loopDetected) return reasonText;
1321
+ return `${reasonText}
1322
+
1323
+ [LOOP_DETECTED gate=pre-tool repeats=${loop.totalCount}]
1324
+ Stop retrying the same action shape unchanged.
1325
+ Next response must do this in order:
1326
+ 1. Name the exact gate failure in one line.
1327
+ 2. Re-emit the missing pre-gate structure only: <cognition>, <verify>, and/or <expected> as required.
1328
+ 3. Change the action plan before retrying the tool. Do not issue the same tool call in the same message as the diagnosis.
1329
+ 4. If the blocker is stale gate state or ledger residue, say that explicitly instead of inventing fake proof.`;
1330
+ }
1331
+
1332
+ function buildForceRedoActionReason(reasonText, { source = 'pre-tool-gate', tool = toolName || 'unknown', lensCount = 0, requiredLenses = REQUIRED_LENSES } = {}) {
1333
+ if (String(reasonText || '').includes('ARIA FORCE_REDO_ACTION')) return reasonText;
1334
+ return [
1335
+ '=== ARIA FORCE_REDO_ACTION ===',
1336
+ `source: ${source}`,
1337
+ `tool: ${tool}`,
1338
+ '',
1339
+ 'This is not a terminal error. It is a forced redo instruction before tool execution.',
1340
+ 'Do not retry the same tool call unchanged. Re-author the action with cognition, proof, and expected outcome first.',
1341
+ '',
1342
+ 'TEACHING:',
1343
+ '- Pre-tool gates exist to make the model think before acting, not to merely throw after a bad action shape.',
1344
+ '- The redo must name the failed mechanism, cite the current substrate, and state why the next action is safe.',
1345
+ '- Do not bypass by disabling tools, changing endpoints from memory, shortening runtime, or asking Hamza to solve saved-memory ambiguity.',
1346
+ '',
1347
+ 'ORIGINAL GATE REASON:',
1348
+ String(reasonText || '').trim(),
1349
+ '',
1350
+ 'REQUIRED REDO SHAPE:',
1351
+ '1. Emit/attach <cognition> with the required lenses before retrying the tool.',
1352
+ '2. For Bash, prepend inline cognition comments to the command:',
1353
+ ' # nur: <observed state and substrate, >=20 chars>',
1354
+ ' # mizan: <risk/tradeoff and why this action is balanced, >=20 chars>',
1355
+ ' # hikma: <doctrine applied and what it forbids, >=20 chars>',
1356
+ ' # tafakkur: <second-order effects and expected result, >=20 chars>',
1357
+ `3. Current cognition count: ${lensCount}/${requiredLenses}.`,
1358
+ '4. If deploy/shared-infra: include <verify> with target, verified evidence, rollback, and axiom.',
1359
+ '5. If non-trivial action: include <expected> with numeric/boolean/state-string predicate.',
1360
+ '=== END FORCE_REDO_ACTION ===',
1361
+ ].join('\n');
1362
+ }
1363
+
1364
+ function emitBlock(reasonText, meta = {}) {
1365
+ console.log(JSON.stringify({ decision: 'block', reason: buildForceRedoActionReason(reasonText, meta) }));
1366
+ }
1367
+
1368
+ if (hasCognition && !appliedContract.ok) {
1369
+ const reason = `Aria pre-tool gate: cognition exists but is not applied to the action.
1370
+
1371
+ The harness no longer accepts cognition as decorative preface. Before a mutating tool call, cognition must produce an execution contract that changes or constrains the action.
1372
+
1373
+ Violations:
1374
+ ${appliedContract.violations.map((v) => `- ${v}`).join('\n')}
1375
+
1376
+ Required contract, either in the assistant turn as <applied_cognition>...</applied_cognition> or inline Bash comments:
1377
+ decision_delta: <what changed because cognition ran; not "none">
1378
+ dominant_domain: <engineering_quality | trust | operations | security | product | ...>
1379
+ binds_to: <this exact tool call/action>
1380
+ expected_predicate: <numeric, boolean, or state-string predicate proving success>
1381
+ artifact_change: <how the artifact/action is different because cognition ran>`;
1382
+ audit('block-applied-cognition-contract', cmdPreview);
1383
+ pushDecision('block', `applied cognition contract missing: ${appliedContract.violations.join(', ')}`);
1384
+ emitBlock(reason, { source: 'pre-tool/applied-cognition-contract', tool: toolName, lensCount, requiredLenses: REQUIRED_LENSES });
1385
+ process.exit(2);
1386
+ }
1387
+
1086
1388
  // ── Deploy-specific gate (doctrine #104) ────────────────────────────────
1087
1389
  // Per feedback_deploy_requires_verify_cognition.md (Hamza 2026-04-28 after
1088
1390
  // consciousness.ts crash took aria-soul into CrashLoopBackOff): deploys
@@ -1099,23 +1401,8 @@ if (deployMatched) {
1099
1401
  const anchorCount = anchorMatches.length;
1100
1402
 
1101
1403
  // Required verify-block fields specific to deploy.
1102
- const verifyBody = (() => {
1103
- if (verifyBodies.length === 0) return '';
1104
- let bestBody = verifyBodies[0];
1105
- let bestScore = -1;
1106
- for (const body of verifyBodies) {
1107
- const score = DEPLOY_VERIFY_REQUIRED_FIELDS.reduce(
1108
- (count, { rx }) => count + (rx.test(body) ? 1 : 0),
1109
- 0,
1110
- );
1111
- if (score > bestScore) {
1112
- bestScore = score;
1113
- bestBody = body;
1114
- }
1115
- }
1116
- return bestBody;
1117
- })();
1118
- const missingDeployFields = DEPLOY_VERIFY_REQUIRED_FIELDS
1404
+ const verifyBody = bestVerifyBody;
1405
+ const missingDeployFields = [...VERIFY_REQUIRED_FIELDS, ...DEPLOY_VERIFY_REQUIRED_FIELDS]
1119
1406
  .filter(({ rx }) => !rx.test(verifyBody))
1120
1407
  .map(({ name }) => name);
1121
1408
 
@@ -1226,7 +1513,7 @@ if (deployMatched) {
1226
1513
  reasons.push(`<verify> block missing required fields: ${missingDeployFields.join(', ')}`);
1227
1514
  }
1228
1515
 
1229
- const refusal = `Aria pre-tool gate: DEPLOY hard-block — pattern '${deployMatched.name}' detected.
1516
+ const refusal = withLoopDirective(`Aria pre-tool gate: DEPLOY hard-block — pattern '${deployMatched.name}' detected.
1230
1517
 
1231
1518
  Per feedback_deploy_requires_verify_cognition.md (Hamza directive 2026-04-28 after consciousness.ts crash took aria-soul into CrashLoopBackOff), every deploy command requires:
1232
1519
 
@@ -1242,14 +1529,14 @@ Block reasons (this turn): ${reasons.join(' • ')}.
1242
1529
 
1243
1530
  The 2026-04-28 deploy of an empty consciousness.ts crashed aria-soul because no verify-block step caught the missing export at substrate-citation time. This gate is the structural enforcement that prevents the same gap.
1244
1531
 
1245
- Re-emit verify+cognition with the missing pieces, then retry the deploy command. There is no env-var override path; doctrine #104 forbids it.`;
1532
+ Re-emit verify+cognition with the missing pieces, then retry the deploy command. There is no env-var override path; doctrine #104 forbids it.`, `deploy:${deployMatched.name}:${missingDeployFields.join(',')}:${anchorCount}:${lensCount}`);
1246
1533
 
1247
1534
  audit(
1248
1535
  `block-deploy ${deployMatched.name} verify=${hasVerify} cognition=${lensCount} anchors=${anchorCount} missing=${missingDeployFields.join(',')}`,
1249
1536
  cmdPreview,
1250
1537
  );
1251
1538
  pushDecision('block', `deploy ${deployMatched.name}: ${reasons.join('; ')}`);
1252
- console.log(JSON.stringify({ decision: 'block', reason: refusal }));
1539
+ emitBlock(refusal, { source: 'pre-tool/deploy', tool: toolName });
1253
1540
  process.exit(2);
1254
1541
  }
1255
1542
 
@@ -1264,7 +1551,7 @@ if (matched) {
1264
1551
  process.exit(0);
1265
1552
  }
1266
1553
  // Block with appropriate corrective message.
1267
- const reason = !hasVerify
1554
+ const reason = withLoopDirective((!hasVerify
1268
1555
  ? `Aria pre-tool gate: destructive pattern '${matched.name}' detected. ${IS_OWNER ? 'Per harness mizan_prestage_rule + axiom_runtime_rule (admit_ignorance, reflection_before_action), include' : 'Pre-action verification required — include'} a <verify> block in your assistant response BEFORE the tool call.
1269
1556
 
1270
1557
  <verify>
@@ -1289,10 +1576,11 @@ Re-issue after producing the block. Bypass: '# doctrine-authorized: <reason>' in
1289
1576
  ${LENS_NAMES[7]}: <what user actually needs>
1290
1577
  </cognition>
1291
1578
 
1292
- At least 4 substantive lenses required.`;
1579
+ At least 4 substantive lenses required.`),
1580
+ `destructive:${matched.name}:${hasVerify ? 'need-cognition' : 'need-verify'}:${toolName}:${filePath || cmdPreview}`);
1293
1581
  audit(`block ${matched.name} verify=${hasVerify} cognition=${lensCount}`, cmdPreview);
1294
1582
  pushDecision('block', `destructive ${matched.name} missing ${!hasVerify ? 'verify' : 'cognition'}`);
1295
- console.log(JSON.stringify({ decision: 'block', reason }));
1583
+ emitBlock(reason, { source: 'pre-tool/destructive' });
1296
1584
  process.exit(2);
1297
1585
  }
1298
1586
 
@@ -1304,7 +1592,7 @@ if (!hasCognition) {
1304
1592
  const isBash = toolName === 'Bash';
1305
1593
  const header = isBash
1306
1594
  ? `Aria pre-tool gate: non-trivial Bash command without ${REQUIRED_LENSES}+ substantive cognition lenses. Found ${lensCount}/${REQUIRED_LENSES}+ (inline=${inlineCog.count}, transcript=${transcriptCog.count}).`
1307
- : `Aria pre-tool gate: ${toolName} call without ${REQUIRED_LENSES}+ substantive cognition lenses in the recent transcript. Found ${lensCount}/${REQUIRED_LENSES}+ (transcript-scan only — ${toolName} has no inline-cognition path).`;
1595
+ : `Aria pre-tool gate: ${toolName} call without ${REQUIRED_LENSES}+ substantive cognition lenses in the recent assistant window. Found ${lensCount}/${REQUIRED_LENSES}+ (source=${cognitionSource} — ${toolName} has no inline-cognition path).`;
1308
1596
 
1309
1597
  const guidance = isBash
1310
1598
  ? `REQUIRED — cognition for every action, no exceptions. Two equivalent forms; either satisfies the gate (both equally REQUIRED, neither preferred over the other):
@@ -1333,15 +1621,15 @@ Both forms count toward the ${REQUIRED_LENSES}+ requirement; gate counts inline
1333
1621
  ${LENS_NAMES[7]}: <what user actually needs underneath>
1334
1622
  </cognition>`;
1335
1623
 
1336
- const reason = `${header}
1624
+ const reason = withLoopDirective(`${header}
1337
1625
 
1338
1626
  ${guidance}
1339
1627
 
1340
- No per-tool bypass available (v3 doctrine — the harness's whole purpose is no exceptions). No env-var disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`;
1628
+ No per-tool bypass available (v3 doctrine — the harness's whole purpose is no exceptions). No env-var disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`, `cognition:${toolName}:${filePath || cmdPreview}:${cognitionSource}`);
1341
1629
 
1342
1630
  audit(`block ${toolName.toLowerCase()} cognition=${lensCount}`, cmdPreview);
1343
1631
  pushDecision('block', `${toolName.toLowerCase()} missing cognition (${lensCount}/${REQUIRED_LENSES})`);
1344
- console.log(JSON.stringify({ decision: 'block', reason }));
1632
+ emitBlock(reason, { source: 'pre-tool/missing-cognition' });
1345
1633
  process.exit(2);
1346
1634
  }
1347
1635
 
@@ -1372,7 +1660,7 @@ No env-var disable path — gates are unconditional from the gated process per H
1372
1660
 
1373
1661
  audit(`block-discovery-unresolved ${toolName.toLowerCase()}`, cmdPreview);
1374
1662
  pushDecision('block', `${toolName.toLowerCase()} cognition has unresolved discovery`);
1375
- console.log(JSON.stringify({ decision: 'block', reason }));
1663
+ emitBlock(reason, { source: 'pre-tool/discovery-binding' });
1376
1664
  process.exit(2);
1377
1665
  }
1378
1666
 
@@ -1387,16 +1675,17 @@ No env-var disable path — gates are unconditional from the gated process per H
1387
1675
  // Per feedback_no_graceful_degradation.md — never silent-pass.
1388
1676
  {
1389
1677
  const expectedMatch = unionText.match(EXPECTED_BLOCK_RX);
1390
- const expectedBlockText = expectedMatch ? expectedMatch[1] : '';
1678
+ const inlineExpectedBody = extractInlineDirectiveBody(cmd, INLINE_EXPECTED_LINE_RX);
1679
+ const expectedBlockText = expectedMatch ? expectedMatch[1] : inlineExpectedBody;
1391
1680
  const hasMeasurablePredicate = expectedBlockText
1392
1681
  ? (MEASURABLE_PREDICATE_RX.test(expectedBlockText) && !QUALITATIVE_DRIFT_RX.test(expectedBlockText))
1393
1682
  : false;
1394
1683
 
1395
- if (!expectedMatch || !hasMeasurablePredicate) {
1396
- const reason = expectedMatch
1684
+ if (!expectedBlockText || !hasMeasurablePredicate) {
1685
+ const reason = expectedBlockText
1397
1686
  ? `Aria pre-tool gate: action requires a measurable predicate inside <expected> per doctrine:dalio_expected_required.
1398
1687
 
1399
- Your <expected> block was found but contains only qualitative drift phrases (e.g. "better", "improved", "should work", "more reliable") without a concrete measurable predicate. These are unmeasurable and defeat the Dalio accountability loop.
1688
+ Your expected-outcome structure was found but contains only qualitative drift phrases (e.g. "better", "improved", "should work", "more reliable") without a concrete measurable predicate. These are unmeasurable and defeat the Dalio accountability loop.
1400
1689
 
1401
1690
  Replace with one of:
1402
1691
  • Numeric: exit_code==0, latency<200ms, count>=1, error_rate=0%
@@ -1411,7 +1700,7 @@ Replace with one of:
1411
1700
  </expected>
1412
1701
 
1413
1702
  No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions.`
1414
- : `Aria pre-tool gate: action requires an <expected> block with measurable predicate per doctrine:dalio_expected_required.
1703
+ : `Aria pre-tool gate: action requires an <expected> block or inline '# expected:' directive with measurable predicate per doctrine:dalio_expected_required.
1415
1704
 
1416
1705
  Every non-trivial action must state WHAT MEASURABLE STATE the action is expected to produce, so the stop-gate can compare predicted vs actual outcome and write a Dalio ledger entry.
1417
1706
 
@@ -1435,7 +1724,7 @@ No bypass — doctrine:dalio_expected_required is unconditional for non-trivial
1435
1724
 
1436
1725
  audit(`block-expected-missing ${toolName.toLowerCase()}`, cmdPreview);
1437
1726
  pushDecision('block', `${toolName.toLowerCase()} missing <expected> measurable predicate`);
1438
- console.log(JSON.stringify({ decision: 'block', reason }));
1727
+ emitBlock(reason, { source: 'pre-tool/discovery-binding' });
1439
1728
  process.exit(2);
1440
1729
  }
1441
1730
  }
@@ -1518,7 +1807,7 @@ Read it first (it is in your environment as ARIA_HARNESS_PACKET_PATH), then refe
1518
1807
 
1519
1808
  Cognition-theater rejected per feedback_gates_enforce_form_not_substance.md.`;
1520
1809
  audit(`block-subagent-no-packet-cite ${toolName.toLowerCase()}`, cmdPreview);
1521
- console.log(JSON.stringify({ decision: 'block', reason }));
1810
+ emitBlock(reason, { source: 'pre-tool/architecture-facts' });
1522
1811
  process.exit(2);
1523
1812
  }
1524
1813
  })();
@@ -1561,7 +1850,7 @@ Rule references:
1561
1850
  R10 no_kubectl_apply_for_image_drift — kubectl apply on aria-soul-stateful (project_forge_psi_oom_cascade.md)
1562
1851
  R11 no_console_log_secrets — console.log with TOKEN/PASSWORD/SECRET/API_KEY/JWT/BEARER`;
1563
1852
  audit(`block-arch-facts ${toolName.toLowerCase()} path=${filePath}`, `violations=${archResult.violations?.length ?? 0}`);
1564
- console.log(JSON.stringify({ decision: 'block', reason: archReason }));
1853
+ emitBlock(archReason, { source: 'pre-tool/architecture-facts' });
1565
1854
  process.exit(2);
1566
1855
  }
1567
1856
  }
@@ -1663,7 +1952,7 @@ What Claude must do:
1663
1952
  3. Wait for next user prompt — preprompt-consult will retry; if it succeeds a plan will exist on next tool call
1664
1953
 
1665
1954
  Non-trivial actions are blocked until a plan exists. Trivial reads (ls/cat/grep) bypass automatically per existing whitelist. To temporarily disable binding for emergency: ARIA_BINDING_ENABLED=false (logged).`;
1666
- console.log(JSON.stringify({ decision: 'block', reason }));
1955
+ emitBlock(reason, { source: 'pre-tool/substrate-binding' });
1667
1956
  process.exit(2);
1668
1957
  }
1669
1958
  }
@@ -1682,7 +1971,7 @@ What Claude must do:
1682
1971
  3. Don't continue executing actions beyond the issued plan boundary
1683
1972
 
1684
1973
  This prevents Claude from drifting past Aria's authorized scope.`;
1685
- console.log(JSON.stringify({ decision: 'block', reason }));
1974
+ emitBlock(reason, { source: 'pre-tool/substrate-binding' });
1686
1975
  process.exit(2);
1687
1976
  }
1688
1977
 
@@ -1719,7 +2008,7 @@ This prevents Claude from drifting past Aria's authorized scope.`;
1719
2008
  if (!freshPhaseInfo || freshPhaseInfo.abortedHere) {
1720
2009
  bindingAuditAppend({ event: 'block_aborted_phase_post_fallback_still_bad', sessionId, planId: plan.planId });
1721
2010
  const reason = `Aria binding gate: aborted phase recovery minted plan ${plan.planId} but new plan also has no usable phase. Manual intervention required.`;
1722
- console.log(JSON.stringify({ decision: 'block', reason }));
2011
+ emitBlock(reason, { source: 'pre-tool/dalio-expected' });
1723
2012
  process.exit(2);
1724
2013
  }
1725
2014
  phaseInfo = freshPhaseInfo;
@@ -1728,7 +2017,7 @@ This prevents Claude from drifting past Aria's authorized scope.`;
1728
2017
  const reason = `Aria binding gate: phase ${phaseInfo.phase.id} of plan ${plan.planId} was reported aborted AND inline architect-fallback failed (exit=${architectExit}). Plan progression halted. ${architectStderr ? 'Architect stderr: ' + architectStderr : ''}
1729
2018
 
1730
2019
  What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggestedAmendment="<if any>"] for Hamza/Aria to issue a corrected plan. Do not continue executing the aborted plan.`;
1731
- console.log(JSON.stringify({ decision: 'block', reason }));
2020
+ emitBlock(reason, { source: 'pre-tool/dalio-expected' });
1732
2021
  process.exit(2);
1733
2022
  }
1734
2023
  }
@@ -1756,7 +2045,7 @@ Forbidden actions for this phase: ${(phase.forbiddenActions || []).join(', ') ||
1756
2045
  Allowed actions for this phase: ${(phase.allowedActions || []).join(', ') || '(none)'}
1757
2046
 
1758
2047
  Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan.${recipeAddendum}`;
1759
- console.log(JSON.stringify({ decision: 'block', reason }));
2048
+ emitBlock(reason, { source: 'pre-tool/output-claim' });
1760
2049
  process.exit(2);
1761
2050
  }
1762
2051
 
@@ -1775,7 +2064,7 @@ Phase summary: ${phase.summary}
1775
2064
  Allowed actions: ${(phase.allowedActions || []).join(', ') || '(none — phase is observation-only)'}
1776
2065
 
1777
2066
  Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment.${recipeAddendum}`;
1778
- console.log(JSON.stringify({ decision: 'block', reason }));
2067
+ emitBlock(reason, { source: 'pre-tool/output-claim' });
1779
2068
  process.exit(2);
1780
2069
  }
1781
2070
 
@@ -1902,10 +2191,11 @@ try {
1902
2191
  if (__apiKey && __action) {
1903
2192
  const __client = new HTTPHarnessClient({
1904
2193
  baseUrl:
2194
+ process.env.ARIA_RUNTIME_URL ||
1905
2195
  process.env.ARIA_HIVE_RUNTIME_URL ||
1906
2196
  process.env.ARIA_HARNESS_BASE_URL ||
1907
2197
  process.env.ARIA_HARNESS_URL ||
1908
- 'https://harness.ariasos.com',
2198
+ RUNTIME_BASE_URL,
1909
2199
  apiKey: __apiKey,
1910
2200
  });
1911
2201
  const __target = JSON.stringify(toolInput || {}).slice(0, 500);
@@ -1916,7 +2206,7 @@ try {
1916
2206
  The substrate's contract gate refused this action. Local doctrine gates passed (cognition lenses, binding plan, drift) but the substrate sees a downstream contract that disallows this tool call. Address the substrate's reason above before retrying.`;
1917
2207
  audit(`block-substrate-checkAction ${toolName.toLowerCase()} action=${__action} reason="${(__check.reason || '').slice(0, 80)}"`, cmdPreview);
1918
2208
  pushDecision('block', `substrate checkAction denied: ${(__check.reason || 'unspecified').slice(0, 100)}`);
1919
- console.log(JSON.stringify({ decision: 'block', reason: __reason }));
2209
+ emitBlock(__reason, { source: 'pre-tool/final' });
1920
2210
  process.exit(2);
1921
2211
  }
1922
2212
  }
@@ -2109,7 +2399,7 @@ causes merge conflicts and state divergence. Explicit coordination is the only s
2109
2399
  pushDecision('block', `hive session-lock conflict on ${_lockCheckPath.slice(0, 80)}`);
2110
2400
  console.log(JSON.stringify({
2111
2401
  decision: 'block',
2112
- reason: _lockBlockReason,
2402
+ reason: buildForceRedoActionReason(_lockBlockReason, { source: 'pre-tool/hive-lock-conflict', tool: toolName || 'unknown' }),
2113
2403
  hookSpecificOutput: {
2114
2404
  hookEventName: 'PreToolUse',
2115
2405
  conflicting_locks: _conflictingLocks,