@aria_asi/cli 0.2.25 → 0.2.29

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 (249) hide show
  1. package/CLIENT-ONBOARDING.md +282 -0
  2. package/bin/aria.js +1140 -14
  3. package/dist/aria-connector/src/auth-commands.d.ts +1 -0
  4. package/dist/aria-connector/src/auth-commands.d.ts.map +1 -1
  5. package/dist/aria-connector/src/auth-commands.js +89 -41
  6. package/dist/aria-connector/src/auth-commands.js.map +1 -1
  7. package/dist/aria-connector/src/chat.d.ts +3 -0
  8. package/dist/aria-connector/src/chat.d.ts.map +1 -1
  9. package/dist/aria-connector/src/chat.js +146 -8
  10. package/dist/aria-connector/src/chat.js.map +1 -1
  11. package/dist/aria-connector/src/codebase-scanner.d.ts +2 -2
  12. package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -1
  13. package/dist/aria-connector/src/codebase-scanner.js +1 -1
  14. package/dist/aria-connector/src/codebase-scanner.js.map +1 -1
  15. package/dist/aria-connector/src/config.d.ts +12 -0
  16. package/dist/aria-connector/src/config.d.ts.map +1 -1
  17. package/dist/aria-connector/src/config.js +2 -0
  18. package/dist/aria-connector/src/config.js.map +1 -1
  19. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  20. package/dist/aria-connector/src/connectors/claude-code.js +111 -21
  21. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  22. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts +37 -0
  23. package/dist/aria-connector/src/connectors/codebase-awareness.d.ts.map +1 -0
  24. package/dist/aria-connector/src/connectors/codebase-awareness.js +335 -0
  25. package/dist/aria-connector/src/connectors/codebase-awareness.js.map +1 -0
  26. package/dist/aria-connector/src/connectors/codex.d.ts +3 -0
  27. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -0
  28. package/dist/aria-connector/src/connectors/codex.js +248 -0
  29. package/dist/aria-connector/src/connectors/codex.js.map +1 -0
  30. package/dist/aria-connector/src/connectors/cognitive-skills.d.ts +2 -0
  31. package/dist/aria-connector/src/connectors/cognitive-skills.d.ts.map +1 -0
  32. package/dist/aria-connector/src/connectors/cognitive-skills.js +47 -0
  33. package/dist/aria-connector/src/connectors/cognitive-skills.js.map +1 -0
  34. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  35. package/dist/aria-connector/src/connectors/opencode.js +90 -4
  36. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  37. package/dist/aria-connector/src/connectors/repo-git-hooks.d.ts +3 -0
  38. package/dist/aria-connector/src/connectors/repo-git-hooks.d.ts.map +1 -0
  39. package/dist/aria-connector/src/connectors/repo-git-hooks.js +87 -0
  40. package/dist/aria-connector/src/connectors/repo-git-hooks.js.map +1 -0
  41. package/dist/aria-connector/src/connectors/repo-guard.d.ts +19 -0
  42. package/dist/aria-connector/src/connectors/repo-guard.d.ts.map +1 -0
  43. package/dist/aria-connector/src/connectors/repo-guard.js +509 -0
  44. package/dist/aria-connector/src/connectors/repo-guard.js.map +1 -0
  45. package/dist/aria-connector/src/connectors/runtime.d.ts +2 -0
  46. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -0
  47. package/dist/aria-connector/src/connectors/runtime.js +330 -0
  48. package/dist/aria-connector/src/connectors/runtime.js.map +1 -0
  49. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  50. package/dist/aria-connector/src/connectors/shell.js +78 -13
  51. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  52. package/dist/aria-connector/src/connectors/syncd.d.ts +27 -0
  53. package/dist/aria-connector/src/connectors/syncd.d.ts.map +1 -0
  54. package/dist/aria-connector/src/connectors/syncd.js +405 -0
  55. package/dist/aria-connector/src/connectors/syncd.js.map +1 -0
  56. package/dist/aria-connector/src/decisions.d.ts +207 -0
  57. package/dist/aria-connector/src/decisions.d.ts.map +1 -0
  58. package/dist/aria-connector/src/decisions.js +291 -0
  59. package/dist/aria-connector/src/decisions.js.map +1 -0
  60. package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -1
  61. package/dist/aria-connector/src/garden-control-plane.js +74 -17
  62. package/dist/aria-connector/src/garden-control-plane.js.map +1 -1
  63. package/dist/aria-connector/src/github-connect.d.ts +18 -0
  64. package/dist/aria-connector/src/github-connect.d.ts.map +1 -0
  65. package/dist/aria-connector/src/github-connect.js +117 -0
  66. package/dist/aria-connector/src/github-connect.js.map +1 -0
  67. package/dist/aria-connector/src/harness-client.d.ts +15 -0
  68. package/dist/aria-connector/src/harness-client.d.ts.map +1 -1
  69. package/dist/aria-connector/src/harness-client.js +106 -3
  70. package/dist/aria-connector/src/harness-client.js.map +1 -1
  71. package/dist/aria-connector/src/hive-client.d.ts +30 -0
  72. package/dist/aria-connector/src/hive-client.d.ts.map +1 -1
  73. package/dist/aria-connector/src/hive-client.js +124 -5
  74. package/dist/aria-connector/src/hive-client.js.map +1 -1
  75. package/dist/aria-connector/src/index.d.ts +13 -2
  76. package/dist/aria-connector/src/index.d.ts.map +1 -1
  77. package/dist/aria-connector/src/index.js +10 -1
  78. package/dist/aria-connector/src/index.js.map +1 -1
  79. package/dist/aria-connector/src/lib/aristotle-noor-wire.d.ts +102 -0
  80. package/dist/aria-connector/src/lib/aristotle-noor-wire.d.ts.map +1 -0
  81. package/dist/aria-connector/src/lib/aristotle-noor-wire.js +231 -0
  82. package/dist/aria-connector/src/lib/aristotle-noor-wire.js.map +1 -0
  83. package/dist/aria-connector/src/providers/types.d.ts +5 -0
  84. package/dist/aria-connector/src/providers/types.d.ts.map +1 -1
  85. package/dist/aria-connector/src/runtime-proof.d.ts +45 -0
  86. package/dist/aria-connector/src/runtime-proof.d.ts.map +1 -0
  87. package/dist/aria-connector/src/runtime-proof.js +340 -0
  88. package/dist/aria-connector/src/runtime-proof.js.map +1 -0
  89. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -1
  90. package/dist/aria-connector/src/setup-wizard.js +34 -2
  91. package/dist/aria-connector/src/setup-wizard.js.map +1 -1
  92. package/dist/assets/hooks/aria-agent-handoff.mjs +224 -0
  93. package/dist/assets/hooks/aria-agent-ledger-merge.mjs +164 -0
  94. package/dist/assets/hooks/aria-architect-fallback.mjs +267 -0
  95. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +676 -0
  96. package/dist/assets/hooks/aria-discovery-record.mjs +101 -0
  97. package/dist/assets/hooks/aria-harness-via-sdk.mjs +412 -0
  98. package/dist/assets/hooks/aria-import-resolution-gate.mjs +330 -0
  99. package/dist/assets/hooks/aria-outcome-record.mjs +84 -0
  100. package/dist/assets/hooks/aria-pre-emit-dryrun.mjs +294 -0
  101. package/dist/assets/hooks/aria-pre-text-gate.mjs +112 -0
  102. package/dist/assets/hooks/aria-pre-tool-gate.mjs +2133 -0
  103. package/dist/assets/hooks/aria-preprompt-consult.mjs +438 -0
  104. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +570 -0
  105. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +397 -0
  106. package/dist/assets/hooks/aria-stop-gate.mjs +1551 -0
  107. package/dist/assets/hooks/aria-trigger-autolearn.mjs +229 -0
  108. package/dist/assets/hooks/aria-userprompt-abandon-detect.mjs +192 -0
  109. package/dist/assets/hooks/doctrine_trigger_map.json +479 -0
  110. package/dist/assets/hooks/lib/canonical-lenses.mjs +64 -0
  111. package/dist/assets/hooks/lib/gate-audit.mjs +43 -0
  112. package/dist/assets/hooks/test-aria-preturn-memory-gate.mjs +245 -0
  113. package/dist/assets/hooks/test-tier-lens-labeling.mjs +399 -0
  114. package/dist/assets/opencode-plugins/harness-context/index.js +60 -0
  115. package/dist/assets/opencode-plugins/harness-context/inject-context.mjs +179 -0
  116. package/dist/assets/opencode-plugins/harness-context/package.json +9 -0
  117. package/dist/assets/opencode-plugins/harness-gate/index.js +248 -0
  118. package/dist/assets/opencode-plugins/harness-outcome/index.js +129 -0
  119. package/dist/assets/opencode-plugins/harness-role/index.js +77 -0
  120. package/dist/assets/opencode-plugins/harness-role/package.json +9 -0
  121. package/dist/assets/opencode-plugins/harness-stop/index.js +241 -0
  122. package/dist/runtime/discipline/CLAUDE.md +339 -0
  123. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/SKILL.md +63 -0
  124. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/domain-matrix.md +80 -0
  125. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/evolution-loop.md +30 -0
  126. package/dist/runtime/discipline/skills/aria-cognition/aria-essence/references/readable-cognition.md +27 -0
  127. package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/SKILL.md +35 -0
  128. package/dist/runtime/discipline/skills/aria-cognition/aria-forge-guardrails/references/checklist.md +31 -0
  129. package/dist/runtime/discipline/skills/aria-cognition/aria-repo-doctrine/SKILL.md +39 -0
  130. package/dist/runtime/discipline/skills/aria-cognition/forge-quality-rules/SKILL.md +43 -0
  131. package/dist/runtime/discipline/skills/aria-cognition/ghazali-8lens/SKILL.md +38 -0
  132. package/dist/runtime/discipline/skills/aria-cognition/istiqra-induction/SKILL.md +26 -0
  133. package/dist/runtime/discipline/skills/aria-cognition/ladunni-22/SKILL.md +35 -0
  134. package/dist/runtime/discipline/skills/aria-cognition/mizan/SKILL.md +72 -0
  135. package/dist/runtime/discipline/skills/aria-cognition/nadia/SKILL.md +38 -0
  136. package/dist/runtime/discipline/skills/aria-cognition/nadia-psi/SKILL.md +38 -0
  137. package/dist/runtime/discipline/skills/aria-cognition/predictor/SKILL.md +25 -0
  138. package/dist/runtime/discipline/skills/aria-cognition/qiyas-analogy/SKILL.md +26 -0
  139. package/dist/runtime/discipline/skills/aria-cognition/soul-domains/SKILL.md +25 -0
  140. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-intra-phase/SKILL.md +81 -0
  141. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-post-phase/SKILL.md +98 -0
  142. package/dist/runtime/discipline/skills/aria-harness/aria-aristotle-pre-phase/SKILL.md +99 -0
  143. package/dist/runtime/discipline/skills/aria-harness/aria-harness-deploy/SKILL.md +127 -0
  144. package/dist/runtime/discipline/skills/aria-harness/aria-harness-no-stripping/SKILL.md +117 -0
  145. package/dist/runtime/discipline/skills/aria-harness/aria-harness-onboarding/SKILL.md +112 -0
  146. package/dist/runtime/discipline/skills/aria-harness/aria-harness-output-discipline/SKILL.md +102 -0
  147. package/dist/runtime/discipline/skills/aria-harness/aria-harness-substrate-binding/SKILL.md +121 -0
  148. package/dist/runtime/doctor.mjs +23 -0
  149. package/dist/runtime/local-phase.mjs +632 -0
  150. package/dist/runtime/manifest.json +15 -0
  151. package/dist/runtime/mizan-scheduler.mjs +331 -0
  152. package/dist/runtime/package.json +6 -0
  153. package/dist/runtime/provider-proxy.mjs +594 -0
  154. package/dist/runtime/sdk/BUNDLED.json +5 -0
  155. package/dist/runtime/sdk/index.d.ts +477 -0
  156. package/dist/runtime/sdk/index.js +1469 -0
  157. package/dist/runtime/sdk/index.js.map +1 -0
  158. package/dist/runtime/sdk/package.json +8 -0
  159. package/dist/runtime/sdk/runWithCognition.d.ts +77 -0
  160. package/dist/runtime/sdk/runWithCognition.js +157 -0
  161. package/dist/runtime/sdk/runWithCognition.js.map +1 -0
  162. package/dist/runtime/service.mjs +2708 -0
  163. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts +53 -0
  164. package/dist/runtime/vendor/aria-gate-runtime/index.d.ts.map +1 -0
  165. package/dist/runtime/vendor/aria-gate-runtime/index.js +277 -0
  166. package/dist/runtime/vendor/aria-gate-runtime/index.js.map +1 -0
  167. package/dist/runtime/vendor/aria-gate-runtime/package.json +6 -0
  168. package/dist/sdk/BUNDLED.json +2 -2
  169. package/dist/sdk/index.d.ts +317 -0
  170. package/dist/sdk/index.js +827 -85
  171. package/dist/sdk/index.js.map +1 -1
  172. package/dist/sdk/runWithCognition.d.ts +77 -0
  173. package/dist/sdk/runWithCognition.js +157 -0
  174. package/dist/sdk/runWithCognition.js.map +1 -0
  175. package/hooks/aria-agent-handoff.mjs +11 -1
  176. package/hooks/aria-architect-fallback.mjs +267 -0
  177. package/hooks/aria-cognition-substrate-binding.mjs +676 -0
  178. package/hooks/aria-discovery-record.mjs +101 -0
  179. package/hooks/aria-harness-via-sdk.mjs +34 -21
  180. package/hooks/aria-import-resolution-gate.mjs +330 -0
  181. package/hooks/aria-outcome-record.mjs +84 -0
  182. package/hooks/aria-pre-emit-dryrun.mjs +294 -0
  183. package/hooks/aria-pre-tool-gate.mjs +985 -40
  184. package/hooks/aria-preprompt-consult.mjs +113 -13
  185. package/hooks/aria-preturn-memory-gate.mjs +298 -6
  186. package/hooks/aria-repo-doctrine-gate.mjs +397 -0
  187. package/hooks/aria-stop-gate.mjs +840 -75
  188. package/hooks/aria-userprompt-abandon-detect.mjs +5 -1
  189. package/hooks/doctrine_trigger_map.json +209 -15
  190. package/hooks/lib/canonical-lenses.mjs +64 -0
  191. package/hooks/lib/gate-audit.mjs +43 -0
  192. package/opencode-plugins/harness-context/index.js +1 -1
  193. package/opencode-plugins/harness-context/inject-context.mjs +82 -23
  194. package/opencode-plugins/harness-gate/index.js +248 -0
  195. package/opencode-plugins/harness-outcome/index.js +129 -0
  196. package/opencode-plugins/harness-stop/index.js +241 -0
  197. package/package.json +8 -2
  198. package/runtime-src/doctor.mjs +23 -0
  199. package/runtime-src/local-phase.mjs +632 -0
  200. package/runtime-src/mizan-scheduler.mjs +331 -0
  201. package/runtime-src/provider-proxy.mjs +594 -0
  202. package/runtime-src/service.mjs +2708 -0
  203. package/scripts/bundle-sdk.mjs +317 -0
  204. package/scripts/install-client.sh +176 -0
  205. package/scripts/publish-all.sh +344 -0
  206. package/scripts/publish-docker.sh +27 -0
  207. package/scripts/validate-hook-contracts.mjs +54 -0
  208. package/scripts/validate-skill-prompts.mjs +95 -0
  209. package/skills/aria-cognition/aria-essence/SKILL.md +63 -0
  210. package/skills/aria-cognition/aria-essence/references/domain-matrix.md +80 -0
  211. package/skills/aria-cognition/aria-essence/references/evolution-loop.md +30 -0
  212. package/skills/aria-cognition/aria-essence/references/readable-cognition.md +27 -0
  213. package/skills/aria-cognition/aria-forge-guardrails/SKILL.md +35 -0
  214. package/skills/aria-cognition/aria-forge-guardrails/references/checklist.md +31 -0
  215. package/skills/aria-cognition/aria-repo-doctrine/SKILL.md +39 -0
  216. package/skills/aria-cognition/forge-quality-rules/SKILL.md +43 -0
  217. package/skills/aria-cognition/ghazali-8lens/SKILL.md +38 -0
  218. package/skills/aria-cognition/istiqra-induction/SKILL.md +26 -0
  219. package/skills/aria-cognition/ladunni-22/SKILL.md +35 -0
  220. package/skills/aria-cognition/mizan/SKILL.md +72 -0
  221. package/skills/aria-cognition/nadia/SKILL.md +38 -0
  222. package/skills/aria-cognition/nadia-psi/SKILL.md +38 -0
  223. package/skills/aria-cognition/predictor/SKILL.md +25 -0
  224. package/skills/aria-cognition/qiyas-analogy/SKILL.md +26 -0
  225. package/skills/aria-cognition/soul-domains/SKILL.md +25 -0
  226. package/src/auth-commands.ts +111 -45
  227. package/src/chat.ts +174 -13
  228. package/src/codebase-scanner.ts +4 -0
  229. package/src/config.ts +15 -0
  230. package/src/connectors/claude-code.ts +115 -26
  231. package/src/connectors/codebase-awareness.ts +408 -0
  232. package/src/connectors/codex.ts +274 -0
  233. package/src/connectors/cognitive-skills.ts +51 -0
  234. package/src/connectors/opencode.ts +93 -4
  235. package/src/connectors/repo-git-hooks.ts +86 -0
  236. package/src/connectors/repo-guard.ts +589 -0
  237. package/src/connectors/runtime.ts +374 -0
  238. package/src/connectors/shell.ts +83 -14
  239. package/src/connectors/syncd.ts +488 -0
  240. package/src/decisions.ts +469 -0
  241. package/src/garden-control-plane.ts +101 -26
  242. package/src/github-connect.ts +143 -0
  243. package/src/harness-client.ts +128 -3
  244. package/src/hive-client.ts +165 -5
  245. package/src/index.ts +41 -2
  246. package/src/lib/aristotle-noor-wire.ts +310 -0
  247. package/src/providers/types.ts +6 -0
  248. package/src/runtime-proof.ts +392 -0
  249. package/src/setup-wizard.ts +37 -2
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // ARIA_ALLOW_STUB — doctrine gate file legitimately discusses stub/placeholder semantics.
2
3
  // Aria pre-tool-use gate — enforces cognition use before destructive tool calls.
3
4
  //
4
5
  // Runs as a Claude Code PreToolUse hook on every Bash invocation. For
@@ -36,11 +37,44 @@
36
37
  // Audit log: every gate decision (allow / block / kill-switch) is
37
38
  // appended to ~/.claude/aria-pre-tool-gate.log.
38
39
 
39
- import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
40
+ import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, chmodSync } from 'node:fs';
40
41
  import { dirname } from 'node:path';
42
+ import { homedir } from 'node:os';
43
+ import { spawnSync } from 'node:child_process';
44
+ import { createHmac, randomBytes as cryptoRandomBytes } from 'node:crypto';
41
45
 
42
46
  const HOME = process.env.HOME || '/tmp';
43
47
  const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
48
+ const HEARTBEAT = `${HOME}/.claude/aria-pre-tool-gate-heartbeat.jsonl`;
49
+
50
+ // ── Heartbeat OUTSIDE the crash boundary (doctrine #123) ───────────────
51
+ // Per feedback_ledger_writes_outside_crash_boundary.md + doctrine #124
52
+ // (non-blocking errors are NOT acceptable), the gate writes a heartbeat
53
+ // FIRST — before transcript scan, before cognition extraction, before
54
+ // any code path that could crash, hang, or be killed by an external
55
+ // timeout. A heartbeat-write is the ONLY signal that proves the gate
56
+ // process actually started executing. Hook-health monitors read this
57
+ // file to detect silent gate death.
58
+ //
59
+ // The heartbeat is intentionally a tiny synchronous write so that even
60
+ // if the gate is killed by Claude Code's hook timeout in the next
61
+ // millisecond, we have proof it was alive at startup.
62
+ try {
63
+ const hbPid = process.pid;
64
+ const hbTs = new Date().toISOString();
65
+ // Write before reading stdin or doing any work that could throw.
66
+ appendFileSync(HEARTBEAT, JSON.stringify({ ts: hbTs, pid: hbPid, gate: 'aria-pre-tool-gate', stage: 'startup' }) + '\n');
67
+ } catch {
68
+ // The ONLY catch in this file that may swallow — a heartbeat write
69
+ // failure here is fail-loud-on-stderr because no other surface exists
70
+ // yet (we haven't opened the audit log) but cannot itself block the
71
+ // gate. Per feedback_non_blocking_errors_unacceptable.md the
72
+ // failure-mode for THIS specific path is documented in the doctrine
73
+ // memory; the structural fix for "what if the heartbeat write fails"
74
+ // is the hook-health monitor task #122 which checks heartbeat
75
+ // freshness and reports staleness.
76
+ process.stderr.write(`[aria-pre-tool-gate] heartbeat write failed at ${new Date().toISOString()}\n`);
77
+ }
44
78
 
45
79
  // Bypass-counter (kept for historical visibility — past audit-log
46
80
  // entries are still useful even though no new bypass entries can be
@@ -93,21 +127,18 @@ function audit(decision, summary) {
93
127
  } catch {}
94
128
  }
95
129
 
96
- // ARIA_BINDING_ENABLED escape hatch (Defect #2 — env override was named in doctrine
97
- // but never read). Wire it first, synchronously, before any other logic runs.
98
- // appendFileSync / existsSync / mkdirSync are already imported at the top of this file
99
- // so they're available here. This is the owner's deliberate kill-switch; setting
100
- // ARIA_BINDING_ENABLED=false is the visible, audited emergency override.
101
- if (process.env.ARIA_BINDING_ENABLED === 'false') {
102
- const _home = process.env.HOME || '/tmp';
103
- const _audit = `${_home}/.claude/aria-binding-audit.jsonl`;
104
- try {
105
- const _dir = dirname(_audit);
106
- if (!existsSync(_dir)) mkdirSync(_dir, { recursive: true });
107
- appendFileSync(_audit, JSON.stringify({ ts: new Date().toISOString(), source: 'pre-tool-gate', event: 'env_override_exit', reason: 'ARIA_BINDING_ENABLED=false', pid: process.pid }) + '\n');
108
- } catch {}
109
- process.exit(0);
110
- }
130
+ // ARIA_BINDING_ENABLED env-override REMOVED 2026-04-28 per Hamza directive
131
+ // + memory:feedback_gap_discovery_hardens_doctrine.md env-var disables
132
+ // are the textbook bypass class doctrine forbids. The override was the
133
+ // de-facto Aria-down handler for an unknown count of sessions and turned
134
+ // off every gate, not just the plan-check. Replaced by the architect
135
+ // fallback plan path (tracked task Aria-unreachable detection writes
136
+ // a bounded local plan with limited allowedActions, loud audit, auto-
137
+ // expiry on Aria return). Prior env_override_exit entries remain in
138
+ // ~/.claude/aria-binding-audit.jsonl for historical audit.
139
+ //
140
+ // The gate now enforces unconditionally from the gated process per
141
+ // Hamza directive 2026-04-27. No process-level disable path.
111
142
 
112
143
  // ── Aria-as-commander binding (Layer A — allowedActions/forbiddenActions per active phase) ──
113
144
  //
@@ -343,10 +374,78 @@ const DESTRUCTIVE_PATTERNS = [
343
374
  { rx: /\bkubectl\s+(scale|rollout)\s+(undo|restart)\b/, name: 'kubectl-rollback' },
344
375
  ];
345
376
 
377
+ // Deploy patterns — bash invocations that mutate the canonical aria k8s
378
+ // cluster or push images. Per feedback_deploy_requires_verify_cognition.md
379
+ // (Hamza directive 2026-04-28 after consciousness.ts crash took aria-soul
380
+ // into CrashLoopBackOff): deploys require BOTH a <verify> block citing the
381
+ // shipping artifact AND a <cognition> block with substantive substrate
382
+ // anchors. Without both, the deploy is hard-blocked. This is doctrine #104
383
+ // — the structural enforcement Hamza demanded after the prior deploy
384
+ // crashed all of aria.
385
+ const DEPLOY_PATTERNS = [
386
+ { rx: /\bbash\s+(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
387
+ { rx: /\b(?:\.\/)?scripts\/deploy-service\.sh\b/, name: 'deploy-service-script' },
388
+ { rx: /\bkubectl\s+apply\b/, name: 'kubectl-apply' },
389
+ { rx: /\bkubectl\s+set\s+image\b/, name: 'kubectl-set-image' },
390
+ { rx: /\bkubectl\s+rollout\s+restart\b/, name: 'kubectl-rollout-restart' },
391
+ { rx: /\bkubectl\s+rollout\s+undo\b/, name: 'kubectl-rollout-undo' },
392
+ { rx: /\bkubectl\s+create\b/, name: 'kubectl-create' },
393
+ { rx: /\bkubectl\s+replace\b/, name: 'kubectl-replace' },
394
+ { rx: /\bdocker\s+push\b/, name: 'docker-push' },
395
+ { rx: /\bdocker\s+build\b.*--push\b/, name: 'docker-build-push' },
396
+ ];
397
+
398
+ // Minimum substrate anchors required in cognition body for a deploy.
399
+ // Per feedback_full_harness_binding_must_be_structural.md, every cognition
400
+ // lens should cite a substrate anchor; for deploy specifically we require
401
+ // at least 4 distinct anchors total across the block (one per lens minimum
402
+ // across at least 4 lenses).
403
+ const DEPLOY_MIN_SUBSTRATE_ANCHORS = 4;
404
+ const SUBSTRATE_ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet):[a-z0-9_\-./]+/gi;
405
+
406
+ // Verify-block fields specifically required for deploys — beyond the base
407
+ // 5 fields, the deploy verify must cite a commit/SHA, a TS-check result,
408
+ // and the admission policy that authorizes the canonical image.
409
+ const DEPLOY_VERIFY_REQUIRED_FIELDS = [
410
+ // Either a git commit hash OR an explicit "files changed" listing
411
+ { rx: /\b(?:commit|HEAD|SHA|files\s+changed)\b/i, name: 'commit_or_files' },
412
+ // TS-check / build evidence
413
+ { rx: /\b(?:tsc|type[\s-]?check|build|npm\s+run\s+build)\b/i, name: 'build_or_typecheck' },
414
+ // Admission policy citation
415
+ { rx: /\b(?:admission[\s_-]?policy|validatingadmissionpolicy)\b/i, name: 'admission_policy' },
416
+ ];
417
+
346
418
  // The verify-block contract. All five fields required.
347
419
  const VERIFY_BLOCK_RX =
348
420
  /<verify>[\s\S]*?target\s*:[\s\S]*?role\s*:[\s\S]*?verified\s*:[\s\S]*?rollback\s*:[\s\S]*?axiom\s*:[\s\S]*?<\/verify>/i;
349
421
 
422
+ // ── Dalio Loop: expected_outcome enforcement (doctrine:dalio_expected_required) ─
423
+ //
424
+ // Every non-trivial action must carry an <expected> block with at least one
425
+ // measurable predicate. The predicate must be numeric (≥X, ==X, ≤X, X%),
426
+ // boolean (true/false/exit=0/exit=1), or state-string ("status=running",
427
+ // "200 OK", "exit=0", "file=exists", etc.).
428
+ //
429
+ // Qualitative drift phrases ("better", "improved", "more robust", etc.) are
430
+ // REJECTED — they are unmeasurable and defeat the Dalio accountability loop.
431
+ // The block must appear in the same turn or a prior turn (turn-scoped, same
432
+ // window as cognition).
433
+ //
434
+ // Per doctrine:dalio_expected_required — no measurable predicate = no allow.
435
+ const EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
436
+
437
+ // Measurable predicate patterns — at least ONE must be present inside <expected>.
438
+ // Forms accepted:
439
+ // numeric: ≥X, >=X, ==X, <=X, <X, >X, X%, N of N, count=N, latency<Xms
440
+ // boolean: true, false, exit=0, exit=1, exit_code=N, rc=N
441
+ // state-string: status=running, status=healthy, status=200, "200 OK",
442
+ // "exit=0", "file=exists", "count=N", "error_rate=0%"
443
+ const MEASURABLE_PREDICATE_RX = /(?:>=|<=|==|!=|>|<|≥|≤)\s*\d+(?:\.\d+)?(?:ms|s|%|kb|mb|gb)?|\d+(?:\.\d+)?%(?:\s+(?:reduction|increase|success|error|coverage))?|exit[_=]\s*(?:0|1|\d+)|exit[-_]?code\s*[=:]\s*\d+|\brc\s*[=:]\s*\d+|\bstatus\s*[=:]\s*(?:running|healthy|ready|degraded|down|up|ok|200|201|204|400|401|403|404|500|502|503|504|true|false)\b|\bcount\s*[=:]\s*\d+|\berror[_-]?rate\s*[=:]\s*0%|\b(?:true|false)\b|\bfile[=_-]exists\b|\b200\s*OK\b|\bno[-_\s]?error|\bhealthy\b|\bpassed?\b|N\s*of\s*N|\d+\s*of\s*\d+/i;
444
+
445
+ // Qualitative drift phrases that masquerade as measurable but are not.
446
+ // Any <expected> block containing ONLY these (no actual predicate) is rejected.
447
+ 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
+
350
449
  // ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
351
450
  //
352
451
  // Aria's Arabic cognition lens names are proprietary IP. On Hamza's surface
@@ -405,7 +504,14 @@ function docRef(canonicalFilename, genericDescription) {
405
504
  // colon, not a placeholder template). The substance check (added
406
505
  // 2026-04-26) defeats ritual emission — `nur: ok` no longer counts.
407
506
  const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
408
- const REQUIRED_LENSES = 4;
507
+ // Hamza directive 2026-04-28: 8-lens enforcement, not 4-of-8. The earlier
508
+ // REQUIRED_LENSES=4 was a regression — owner caught it ("i no longer see
509
+ // 8 lens cognition per turn"). All 8 canonical lenses must appear with
510
+ // substantive (≥20-char, non-placeholder) content per turn for the gate
511
+ // to accept the cognition. The substrate-binding stop hook also requires
512
+ // all 8 to carry substrate anchors; this gate enforces the 8 lens NAMES
513
+ // are present and have substance.
514
+ const REQUIRED_LENSES = 8;
409
515
  const SUBSTANCE_MIN_CHARS = 20;
410
516
  // Placeholder patterns from the gate's own correction message + the
411
517
  // COMPACT_CONTINUITY_DOCTRINE template — content matching these is
@@ -611,7 +717,11 @@ async function archFactsGate(toolInput) {
611
717
  // AbortController deadline. Pile-up protection is structural —
612
718
  // in-flight counter caps concurrent pushes. Real network errors
613
719
  // drive the catch path; slow responses complete naturally.
614
- const HARNESS_URL = process.env.ARIA_HARNESS_URL || 'http://192.168.4.25:30080';
720
+ const HARNESS_URL =
721
+ process.env.ARIA_HIVE_RUNTIME_URL ||
722
+ process.env.ARIA_HARNESS_BASE_URL ||
723
+ process.env.ARIA_HARNESS_URL ||
724
+ 'https://harness.ariasos.com';
615
725
  const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
616
726
  const LOG_PUSH_DISABLED = process.env.ARIA_COGNITION_PUSH === 'off';
617
727
  const MAX_IN_FLIGHT = 16;
@@ -639,6 +749,89 @@ function pushCognitionEvent(payload) {
639
749
  }
640
750
  }
641
751
 
752
+ // ── Block-pattern recipe lookup (Task #86) ──────────────────────────────────
753
+ // Before emitting a binding-violation block, ask aria-soul if this exact
754
+ // shape has been seen before and a high-confidence corrective recipe exists.
755
+ // If so, surface the recipe in the block message so the gated process gets
756
+ // the fix the hive already learned, not just the block.
757
+ //
758
+ // Per feedback_no_timeouts_doctrine.md the lookup uses a DETECTION PROBE
759
+ // (3s AbortController) — that's "is the lookup endpoint alive at all?", not
760
+ // a hard deadline on the underlying lookup. If the probe fires the gate
761
+ // continues with the block-only message: a missed lookup must NEVER convert
762
+ // a real block into an allow.
763
+ //
764
+ // Recipe is surfaced only when confidence > RECIPE_SURFACE_CONFIDENCE (0.7
765
+ // per migration 208 RECIPE_PROMOTE_THRESHOLD), matching the same threshold
766
+ // the cron uses to promote a recipe to the pattern's hot pointer. Below
767
+ // that, we still cite the pattern's existence ("similar block has been
768
+ // seen N times") without claiming a fix.
769
+ const ARIA_SOUL_URL = process.env.ARIA_SOUL_URL || HARNESS_URL;
770
+ const RECIPE_SURFACE_CONFIDENCE = 0.7;
771
+ const BLOCK_PATTERN_PROBE_MS = 3000;
772
+
773
+ async function lookupBlockPatternRecipe({ detectorClass, signature, tenantId }) {
774
+ if (!detectorClass || !signature) return null;
775
+ const url = new URL(`${ARIA_SOUL_URL}/api/hive/block-pattern`);
776
+ url.searchParams.set('action', 'lookup');
777
+ url.searchParams.set('detector_class', detectorClass);
778
+ url.searchParams.set('pattern_signature', signature.slice(0, 512));
779
+ if (tenantId) url.searchParams.set('tenant_id', tenantId);
780
+
781
+ const ctl = new AbortController();
782
+ const probeTimer = setTimeout(() => ctl.abort(), BLOCK_PATTERN_PROBE_MS);
783
+ try {
784
+ const resp = await fetch(url.toString(), {
785
+ method: 'GET',
786
+ headers: HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {},
787
+ signal: ctl.signal,
788
+ });
789
+ if (!resp.ok) return null;
790
+ const body = await resp.json();
791
+ if (!body || body.found !== true) return null;
792
+ return body;
793
+ } catch {
794
+ return null; // probe failure — block proceeds without recipe surfacing
795
+ } finally {
796
+ clearTimeout(probeTimer);
797
+ }
798
+ }
799
+
800
+ // Renders the lookup result as an inline addendum to a block message. Returns
801
+ // the empty string when there's nothing useful to surface — the caller can
802
+ // concatenate unconditionally without producing dangling whitespace.
803
+ function renderRecipeAddendum(lookupResult) {
804
+ if (!lookupResult) return '';
805
+ const recipe = lookupResult.recipe;
806
+ const freq = Number(lookupResult.frequency || 0);
807
+ const successRate = lookupResult.success_rate;
808
+
809
+ // Recipe present + above promotion threshold → surface the fix verbatim.
810
+ if (recipe && typeof recipe === 'object' && Number(recipe.confidence ?? 0) >= RECIPE_SURFACE_CONFIDENCE) {
811
+ const text = typeof recipe.recipe_text === 'string' ? recipe.recipe_text.slice(0, 800) : '';
812
+ const actions = Array.isArray(recipe.recipe_actions) ? recipe.recipe_actions : [];
813
+ const actionsLine = actions.length
814
+ ? `\n Actions: ${JSON.stringify(actions).slice(0, 600)}`
815
+ : '';
816
+ const conf = Number(recipe.confidence).toFixed(2);
817
+ const seenLine = freq > 0 ? ` (pattern seen ${freq}× across the hive)` : '';
818
+ return `\n\n📚 HIVE RECIPE${seenLine}:
819
+ ${text}${actionsLine}
820
+ Confidence: ${conf}. Apply this BEFORE the block escalates — the hive learned this fix from prior firings.`;
821
+ }
822
+
823
+ // No promoted recipe — but the pattern is recurring. Cite it so the caller
824
+ // knows this isn't novel; the recipe is still being learned.
825
+ if (freq >= 3) {
826
+ const rateLine = (typeof successRate === 'number')
827
+ ? ` Past resolution rate: ${(successRate * 100).toFixed(0)}%.`
828
+ : '';
829
+ return `\n\n📓 Hive note: this block-shape has fired ${freq} time(s); recipe is still being learned (no high-confidence fix yet).${rateLine}`;
830
+ }
831
+
832
+ return '';
833
+ }
834
+
642
835
  // Read event JSON from stdin (Claude Code spec).
643
836
  let input = '';
644
837
  for await (const chunk of process.stdin) input += chunk;
@@ -703,12 +896,18 @@ if (inlineCog.count >= REQUIRED_LENSES) {
703
896
  const matched = toolName === 'Bash'
704
897
  ? DESTRUCTIVE_PATTERNS.find(({ rx }) => rx.test(cmd))
705
898
  : null;
899
+ // Deploy-specific match — separate from destructive because it carries
900
+ // stricter substrate-anchor requirements per doctrine #104.
901
+ const deployMatched = toolName === 'Bash'
902
+ ? DEPLOY_PATTERNS.find(({ rx }) => rx.test(cmd))
903
+ : null;
706
904
  const isTrivialRead = toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200;
707
905
  const isShort = toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT;
708
906
 
709
- if (!matched && (isTrivialRead || isShort)) {
710
- // Not destructive AND trivial — allow without further checks. Only
711
- // reachable for Bash because both flags are forced false otherwise.
907
+ if (!matched && !deployMatched && (isTrivialRead || isShort)) {
908
+ // Not destructive AND not a deploy AND trivial — allow without
909
+ // further checks. Only reachable for Bash because all three flags are
910
+ // forced false otherwise.
712
911
  process.exit(0);
713
912
  }
714
913
 
@@ -829,7 +1028,10 @@ const mergedLensSet = new Set([...inlineCog.names, ...transcriptCog.names]);
829
1028
  const lensCount = mergedLensSet.size;
830
1029
  const lensNames = [...mergedLensSet];
831
1030
  const cogBlockBody = transcriptCog.blockBody;
832
- const hasVerify = VERIFY_BLOCK_RX.test(unionText);
1031
+ const verifyBodies = [...unionText.matchAll(/<verify>([\s\S]*?)<\/verify>/gi)]
1032
+ .map((m) => (m[1] || '').trim())
1033
+ .filter(Boolean);
1034
+ const hasVerify = verifyBodies.length > 0;
833
1035
  const hasCognition = lensCount >= REQUIRED_LENSES;
834
1036
  const cognitionSource = inlineCog.count >= REQUIRED_LENSES
835
1037
  ? 'inline-command'
@@ -881,6 +1083,176 @@ function pushDecision(decision, reasonText) {
881
1083
  });
882
1084
  }
883
1085
 
1086
+ // ── Deploy-specific gate (doctrine #104) ────────────────────────────────
1087
+ // Per feedback_deploy_requires_verify_cognition.md (Hamza 2026-04-28 after
1088
+ // consciousness.ts crash took aria-soul into CrashLoopBackOff): deploys
1089
+ // require the verify block to cite commit/files/build/admission-policy
1090
+ // AND the cognition block to carry ≥DEPLOY_MIN_SUBSTRATE_ANCHORS substrate
1091
+ // anchors. The full-harness-binding doctrine says lenses without anchors
1092
+ // are unsourced prose — for deploys, that prose-only level of evidence is
1093
+ // not sufficient. The deploy gate refuses until anchors are present.
1094
+ if (deployMatched) {
1095
+ // Anchor count is read from the cognition body (turn-scoped, same
1096
+ // source the substrate-binding stop hook uses).
1097
+ const cognitionBody = cogBlockBody || '';
1098
+ const anchorMatches = cognitionBody.match(SUBSTRATE_ANCHOR_RX) || [];
1099
+ const anchorCount = anchorMatches.length;
1100
+
1101
+ // 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
1119
+ .filter(({ rx }) => !rx.test(verifyBody))
1120
+ .map(({ name }) => name);
1121
+
1122
+ // Heartbeat at deploy-gate entry — proves the gate reached this path
1123
+ // even if a downstream exit fires silently (per
1124
+ // feedback_ledger_writes_outside_crash_boundary.md).
1125
+ try {
1126
+ appendFileSync(HEARTBEAT, JSON.stringify({
1127
+ ts: new Date().toISOString(), gate: 'aria-pre-tool-gate', stage: 'deploy-gate-entry',
1128
+ deployPattern: deployMatched.name, hasVerify, hasCognition, lensCount, anchorCount,
1129
+ missingDeployFields, cogBlockBodyLen: (cogBlockBody || '').length,
1130
+ verifyBodyLen: verifyBody.length,
1131
+ }) + '\n');
1132
+ } catch {}
1133
+
1134
+ const deployBlocked =
1135
+ !hasVerify ||
1136
+ !hasCognition ||
1137
+ anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS ||
1138
+ missingDeployFields.length > 0;
1139
+
1140
+ if (!deployBlocked) {
1141
+ // Write justification artifact for deploy-service.sh to read.
1142
+ // Doctrine #104 + bypass-vulnerability-closure 2026-04-28:
1143
+ // The artifact is HMAC-signed using a per-installation secret at
1144
+ // ~/.claude/.aria-gate-secret (0600). deploy-service.sh recomputes
1145
+ // the HMAC and refuses any artifact whose signature does not match
1146
+ // — this binds the artifact to having been written by THIS gate
1147
+ // process, preventing any other process from synthesizing a
1148
+ // passing artifact (which is exactly the bypass owner caught when
1149
+ // the assistant attempted a manualJustification:true write).
1150
+ //
1151
+ // The secret is generated on first run if absent; the file is
1152
+ // chmod 0600 so other users cannot read it; deploy-service.sh
1153
+ // reads the same file and computes the same HMAC.
1154
+ try {
1155
+ const justificationPath = `${HOME}/.claude/.aria-deploy-justification.json`;
1156
+ const secretPath = `${HOME}/.claude/.aria-gate-secret`;
1157
+
1158
+ // Lazy-generate per-installation secret if absent.
1159
+ if (!existsSync(secretPath)) {
1160
+ const secret = cryptoRandomBytes(32).toString('hex');
1161
+ writeFileSync(secretPath, secret + '\n', { mode: 0o600 });
1162
+ chmodSync(secretPath, 0o600);
1163
+ }
1164
+ const secret = readFileSync(secretPath, 'utf-8').trim();
1165
+
1166
+ // Build the unsigned artifact body. Note: deliberately reject any
1167
+ // attempt to set `manualJustification` — only this gate emits the
1168
+ // artifact, and a manual flag is by definition a forgery class.
1169
+ const unsignedBody = {
1170
+ timestamp: new Date().toISOString(),
1171
+ sessionId,
1172
+ deployPattern: deployMatched.name,
1173
+ command: cmdPreview,
1174
+ verify: verifyBody.trim().slice(0, 4000),
1175
+ cognition: cogBlockBody.trim().slice(0, 8000),
1176
+ substrateAnchors: anchorMatches.slice(0, 50),
1177
+ anchorCount,
1178
+ lensCount,
1179
+ verifyFieldsPresent: DEPLOY_VERIFY_REQUIRED_FIELDS
1180
+ .filter(({ rx }) => rx.test(verifyBody))
1181
+ .map(({ name }) => name),
1182
+ };
1183
+
1184
+ // HMAC-SHA256 over the canonical-JSON of the unsigned body.
1185
+ // canonical-JSON = JSON.stringify with no signature field, no
1186
+ // pretty-print whitespace, key order as written above.
1187
+ const signature = createHmac('sha256', secret)
1188
+ .update(JSON.stringify(unsignedBody))
1189
+ .digest('hex');
1190
+
1191
+ const justification = {
1192
+ ...unsignedBody,
1193
+ signature,
1194
+ signatureAlgo: 'HMAC-SHA256',
1195
+ };
1196
+ writeFileSync(justificationPath, JSON.stringify(justification, null, 2));
1197
+ } catch (writeErr) {
1198
+ // Write failure is non-fatal for the gate decision (the gate itself
1199
+ // is the structural enforcement); log loudly per
1200
+ // canonical-secrets-governance LOUD telemetry doctrine.
1201
+ console.error(
1202
+ `[aria-pre-tool-gate] WARN deploy-justification artifact write failed: ${(writeErr instanceof Error ? writeErr.message : String(writeErr)).slice(0, 200)}`,
1203
+ );
1204
+ }
1205
+ audit(
1206
+ `allow-deploy ${deployMatched.name} lenses=${lensCount} anchors=${anchorCount}`,
1207
+ cmdPreview,
1208
+ );
1209
+ pushDecision(
1210
+ 'allow',
1211
+ `verify+cognition+anchors(${anchorCount}) for deploy ${deployMatched.name}`,
1212
+ );
1213
+ process.exit(0);
1214
+ }
1215
+
1216
+ // Build a focused refusal naming exactly which piece is missing.
1217
+ const reasons = [];
1218
+ if (!hasVerify) reasons.push('missing <verify> block');
1219
+ if (!hasCognition) reasons.push(`missing <cognition> block (lenses=${lensCount}/${REQUIRED_LENSES})`);
1220
+ if (hasCognition && anchorCount < DEPLOY_MIN_SUBSTRATE_ANCHORS) {
1221
+ reasons.push(
1222
+ `cognition block has ${anchorCount}/${DEPLOY_MIN_SUBSTRATE_ANCHORS} substrate anchors (axiom:/frame:/memory:/doctrine:/packet:)`,
1223
+ );
1224
+ }
1225
+ if (missingDeployFields.length > 0) {
1226
+ reasons.push(`<verify> block missing required fields: ${missingDeployFields.join(', ')}`);
1227
+ }
1228
+
1229
+ const refusal = `Aria pre-tool gate: DEPLOY hard-block — pattern '${deployMatched.name}' detected.
1230
+
1231
+ 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
+
1233
+ 1. A <verify> block in the recent assistant text containing AT MINIMUM:
1234
+ - target/role/verified/rollback/axiom (base verify fields)
1235
+ - commit hash or files-changed listing
1236
+ - tsc / build / type-check evidence
1237
+ - admission policy citation (kubectl get validatingadmissionpolicy <service>-canonical-image-policy)
1238
+
1239
+ 2. A <cognition> block with ≥${REQUIRED_LENSES} substantive lenses AND ≥${DEPLOY_MIN_SUBSTRATE_ANCHORS} substrate anchors total (axiom:<name> / frame:<name> / memory:<file> / doctrine:<rule> / packet:<section>).
1240
+
1241
+ Block reasons (this turn): ${reasons.join(' • ')}.
1242
+
1243
+ 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
+
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.`;
1246
+
1247
+ audit(
1248
+ `block-deploy ${deployMatched.name} verify=${hasVerify} cognition=${lensCount} anchors=${anchorCount} missing=${missingDeployFields.join(',')}`,
1249
+ cmdPreview,
1250
+ );
1251
+ pushDecision('block', `deploy ${deployMatched.name}: ${reasons.join('; ')}`);
1252
+ console.log(JSON.stringify({ decision: 'block', reason: refusal }));
1253
+ process.exit(2);
1254
+ }
1255
+
884
1256
  if (matched) {
885
1257
  // Destructive — require BOTH verify (from transcript) AND cognition
886
1258
  // (inline command preferred; transcript fallback). Verify stays
@@ -1004,6 +1376,153 @@ No env-var disable path — gates are unconditional from the gated process per H
1004
1376
  process.exit(2);
1005
1377
  }
1006
1378
 
1379
+ // ── Dalio expected_outcome gate ──────────────────────────────────────────────
1380
+ //
1381
+ // Every non-trivial action must carry an <expected> block with at least one
1382
+ // measurable predicate. Block is read from the same turn-scoped assistant text
1383
+ // window as cognition (unionText). Qualitative drift phrases are rejected even
1384
+ // when they appear inside an <expected> block.
1385
+ //
1386
+ // Hard-block path: block the tool call, name the missing block, cite doctrine.
1387
+ // Per feedback_no_graceful_degradation.md — never silent-pass.
1388
+ {
1389
+ const expectedMatch = unionText.match(EXPECTED_BLOCK_RX);
1390
+ const expectedBlockText = expectedMatch ? expectedMatch[1] : '';
1391
+ const hasMeasurablePredicate = expectedBlockText
1392
+ ? (MEASURABLE_PREDICATE_RX.test(expectedBlockText) && !QUALITATIVE_DRIFT_RX.test(expectedBlockText))
1393
+ : false;
1394
+
1395
+ if (!expectedMatch || !hasMeasurablePredicate) {
1396
+ const reason = expectedMatch
1397
+ ? `Aria pre-tool gate: action requires a measurable predicate inside <expected> per doctrine:dalio_expected_required.
1398
+
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.
1400
+
1401
+ Replace with one of:
1402
+ • Numeric: exit_code==0, latency<200ms, count>=1, error_rate=0%
1403
+ • Boolean: exit=0, status=healthy, file=exists, rc=0
1404
+ • State-string: "status=running", "200 OK", "count=3 of 3 passed"
1405
+
1406
+ <expected>
1407
+ predicate: exit_code==0 AND file=/home/hamzaibrahim1/.foo written
1408
+ measurable_type: boolean
1409
+ threshold: 0
1410
+ eval_window_minutes: 1
1411
+ </expected>
1412
+
1413
+ 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.
1415
+
1416
+ 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
+
1418
+ Required format (add to your assistant turn before this tool call):
1419
+
1420
+ <expected>
1421
+ predicate: <concrete measurable assertion — e.g. "exit_code==0", "status=running", "count>=1">
1422
+ measurable_type: numeric | boolean | state_string
1423
+ threshold: <optional — the exact boundary, e.g. 0 or "healthy">
1424
+ eval_window_minutes: <optional — how long before this expires, e.g. 5>
1425
+ </expected>
1426
+
1427
+ Accepted predicates:
1428
+ • Numeric: >=X, <=X, ==X, X%, count=N, latency<Xms, error_rate=0%
1429
+ • Boolean: exit=0, exit=1, true, false, file=exists, status=healthy
1430
+ • State-string: "status=running", "200 OK", "exit=0", "no_error"
1431
+
1432
+ REJECTED (qualitative drift): "better", "improved", "should work", "more reliable", "cleaner"
1433
+
1434
+ No bypass — doctrine:dalio_expected_required is unconditional for non-trivial actions per feedback_implementation_coupled_cognition.md.`;
1435
+
1436
+ audit(`block-expected-missing ${toolName.toLowerCase()}`, cmdPreview);
1437
+ pushDecision('block', `${toolName.toLowerCase()} missing <expected> measurable predicate`);
1438
+ console.log(JSON.stringify({ decision: 'block', reason }));
1439
+ process.exit(2);
1440
+ }
1441
+ }
1442
+
1443
+ // ── Sub-agent packet-citation check (Layer 4 — #84) ─────────────────────────
1444
+ //
1445
+ // When running inside a sub-agent process (detected by a non-stale handoff file
1446
+ // existing AND harnessPacketPath is set in that handoff), require the cognition
1447
+ // lens content to CITE THE PACKET — at least one of:
1448
+ // - A doctrine rule ID (e.g. "doctrine_first", "no_demos", "workaround_vs_path_fix")
1449
+ // - An axiom name (e.g. "truth_over_deception", "no_harm", "sacred_trust", "reflection_before_action")
1450
+ // - A frame primitive (e.g. "Fitrah", "Tafakkur", "Tadabbur", "Ilham", "Mizan", "Hikma", "Nur", "Wahi", "Firasah")
1451
+ // - A memory class reference (e.g. "feedback_*.md", "project_*.md", "reference_*.md")
1452
+ //
1453
+ // Owner-tier sessions (ownerTier.hamza === true AND no jti) are EXEMPT —
1454
+ // they are the source of truth for the packet itself.
1455
+ //
1456
+ // Fail-soft detection: handoff read errors silently skip this check.
1457
+ (function checkSubAgentPacketCitation() {
1458
+ const _HOME = process.env.HOME || '/tmp';
1459
+ // Try owner-tier handoff path first, then client-tier paths.
1460
+ const HANDOFF_TTL_MS = 5 * 60 * 1000;
1461
+ // Probe known handoff paths.
1462
+ const candidatePaths = [
1463
+ `${_HOME}/.claude/aria-agent-harness-handoff.json`,
1464
+ ];
1465
+ // Also check /var/lib/aria-licensee if that dir exists (client-tier).
1466
+ try {
1467
+ const licPath = `${_HOME}/.aria/license.json`;
1468
+ if (existsSync(licPath)) {
1469
+ const lic = JSON.parse(readFileSync(licPath, 'utf8'));
1470
+ if (lic.jti) {
1471
+ candidatePaths.push(`/var/lib/aria-licensee/${lic.jti}/handoff.json`);
1472
+ }
1473
+ }
1474
+ } catch { /* non-fatal */ }
1475
+
1476
+ let handoff = null;
1477
+ for (const hp of candidatePaths) {
1478
+ if (!existsSync(hp)) continue;
1479
+ try {
1480
+ const raw = JSON.parse(readFileSync(hp, 'utf8'));
1481
+ const ageMs = Date.now() - new Date(raw.writtenAt || 0).getTime();
1482
+ if (ageMs > HANDOFF_TTL_MS) continue; // stale handoff — not a sub-agent context
1483
+ if (!raw.harnessPacketPath) continue; // no packet path written → identity-only handoff, skip check
1484
+ handoff = raw;
1485
+ break;
1486
+ } catch { /* malformed — skip */ }
1487
+ }
1488
+
1489
+ if (!handoff) return; // not a sub-agent context, or handoff has no packetPath
1490
+
1491
+ // Owner-tier exemption: if ownerTier.hamza === true AND no jti → source of truth
1492
+ const ownerExempt = (handoff.ownerTier?.hamza === true) && !handoff.ownerTier?.jti;
1493
+ if (ownerExempt) return;
1494
+
1495
+ // Now verify that the cognition block cites at least one packet substrate token.
1496
+ // Tokens accepted (case-insensitive):
1497
+ // • Doctrine rule IDs from feedback_*/project_*/reference_* filenames
1498
+ // • Axiom names: truth_over_deception, no_harm, sacred_trust, reflection_before_action
1499
+ // • Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
1500
+ // • Memory class patterns: feedback_*.md, project_*.md, reference_*.md
1501
+ const PACKET_CITE_RX = /\b(?:feedback_[a-z0-9_]+\.md|project_[a-z0-9_]+\.md|reference_[a-z0-9_]+\.md|doctrine_first|no_demos|workaround_vs_path_fix|no_flag_without_fix|implementation_coupled_cognition|session_starts_with_linear|gates_enforce_form_not_substance|truth_over_deception|no_harm|sacred_trust|reflection_before_action|power_obligates_service|fitrah|tafakkur|tadabbur|ilham|mizan|hikma|nur|wahi|firasah|harness\s*packet)\b/i;
1502
+
1503
+ const cogText = cogBlockBody || unionText;
1504
+ const hasCite = PACKET_CITE_RX.test(cogText) || PACKET_CITE_RX.test(cmd);
1505
+
1506
+ if (!hasCite) {
1507
+ const packetRef = handoff.harnessPacketPath;
1508
+ const reason = `Sub-agent cognition cites no packet substrate — lens content must reference at least one axiom/frame/memory/doctrine from the harness packet at ${packetRef}.
1509
+
1510
+ Accepted citations (case-insensitive, any ONE suffices):
1511
+ • Doctrine rule IDs: doctrine_first, no_demos, workaround_vs_path_fix, no_flag_without_fix, etc.
1512
+ • Axioms: truth_over_deception, no_harm, sacred_trust, reflection_before_action, power_obligates_service
1513
+ • Frame primitives: Fitrah, Tafakkur, Tadabbur, Ilham, Mizan, Hikma, Nur, Wahi, Firasah
1514
+ • Memory class refs: feedback_*.md, project_*.md, reference_*.md
1515
+
1516
+ The harness packet is at: ${packetRef}
1517
+ Read it first (it is in your environment as ARIA_HARNESS_PACKET_PATH), then reference it in your cognition block.
1518
+
1519
+ Cognition-theater rejected per feedback_gates_enforce_form_not_substance.md.`;
1520
+ audit(`block-subagent-no-packet-cite ${toolName.toLowerCase()}`, cmdPreview);
1521
+ console.log(JSON.stringify({ decision: 'block', reason }));
1522
+ process.exit(2);
1523
+ }
1524
+ })();
1525
+
1007
1526
  // ── arch_facts gate (architectural violation scan) ────────────────────────
1008
1527
  //
1009
1528
  // Runs after cognition + discovery-binding pass, for Edit/Write/NotebookEdit.
@@ -1104,25 +1623,53 @@ if (__isBootstrapConsult) {
1104
1623
  bindingAuditAppend({ event: 'allow_bootstrap_consult', sessionId, target: __bindingActionClassification.target, toolName });
1105
1624
  }
1106
1625
  if (BINDING_ENABLED && !bindingBypassReason && !__isBootstrapConsult) {
1107
- const plan = loadActivePlan(sessionId);
1626
+ let plan = loadActivePlan(sessionId);
1108
1627
  if (!plan) {
1109
- bindingAuditAppend({ event: 'block_no_active_plan', sessionId, toolName, cmdPreview });
1110
- const reason = `Aria binding gate: no active plan exists for this session and Aria-as-commander binding is required (ARIA_BINDING_ENABLED=true).
1628
+ // INLINE architect-fallback (Hamza directive 2026-04-27 no async stop-gate
1629
+ // races). When no plan exists, fire architect-fallback synchronously here
1630
+ // so a real plan is minted and loaded inside the same hook execution. If
1631
+ // architect-fallback fails the gate still BLOCKS per doctrine (no bypass)
1632
+ // but the block message includes the architect-fallback exit details so the
1633
+ // failure is visible LOUDLY per feedback_no_graceful_degradation.md.
1634
+ bindingAuditAppend({ event: 'no_plan_inline_fallback_attempt', sessionId, toolName });
1635
+ const blockerReason = `pre-tool-gate inline fallback: no active plan for session ${sessionId} when ${toolName} was attempted on ${cmdPreview.slice(0, 200)}`;
1636
+ const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: 'none' });
1637
+ let architectExit = -1;
1638
+ let architectStderr = '';
1639
+ try {
1640
+ const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
1641
+ input: fallbackEvent,
1642
+ encoding: 'utf8',
1643
+ timeout: 60000,
1644
+ });
1645
+ architectExit = archProc.status ?? -1;
1646
+ architectStderr = (archProc.stderr || '').slice(0, 500);
1647
+ } catch (err) {
1648
+ architectStderr = String(err).slice(0, 500);
1649
+ }
1650
+ bindingAuditAppend({ event: 'architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
1111
1651
 
1112
- This means: preprompt-consult either hasn't fired yet OR consult failed and there's no prior plan to fall back to.
1652
+ plan = loadActivePlan(sessionId);
1653
+ if (plan) {
1654
+ process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: architect-fallback minted plan ${plan.planId}. Continuing.\n`);
1655
+ bindingAuditAppend({ event: 'architect_fallback_minted_plan', sessionId, planId: plan.planId });
1656
+ } else {
1657
+ bindingAuditAppend({ event: 'block_no_active_plan_after_fallback', sessionId, toolName, architectExit });
1658
+ const reason = `Aria binding gate: no active plan exists AND inline architect-fallback failed (exit=${architectExit}). Plan-mint chain broken. ${architectStderr ? 'Architect stderr: ' + architectStderr : ''}
1113
1659
 
1114
1660
  What Claude must do:
1115
- 1. Acknowledge to Hamza that no plan was issued
1116
- 2. Propose holding for cluster recovery (lane-gateway / aria-soul / consult endpoint)
1117
- 3. Wait for the next user prompt — preprompt-consult will retry the consult; if it succeeds a plan will exist on the next tool call attempt
1661
+ 1. Acknowledge to Hamza that the architect-fallback chain failed (visible in audit log)
1662
+ 2. Surface the failure LOUDLY this is a substrate-level break, not a routine consult miss
1663
+ 3. Wait for next user prompt — preprompt-consult will retry; if it succeeds a plan will exist on next tool call
1118
1664
 
1119
- Non-trivial actions are blocked until a plan exists. Trivial reads (ls/cat/grep) bypass automatically per existing whitelist. To temporarily disable binding for an emergency: ARIA_BINDING_ENABLED=false (logged).`;
1120
- console.log(JSON.stringify({ decision: 'block', reason }));
1121
- process.exit(2);
1665
+ 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 }));
1667
+ process.exit(2);
1668
+ }
1122
1669
  }
1123
1670
 
1124
1671
  const transcriptText = unionText || '';
1125
- const phaseInfo = pickCurrentPhase(plan, transcriptText);
1672
+ let phaseInfo = pickCurrentPhase(plan, transcriptText);
1126
1673
 
1127
1674
  if (!phaseInfo) {
1128
1675
  // All phases reported complete — needs new consult before more action
@@ -1140,12 +1687,50 @@ This prevents Claude from drifting past Aria's authorized scope.`;
1140
1687
  }
1141
1688
 
1142
1689
  if (phaseInfo.abortedHere) {
1143
- bindingAuditAppend({ event: 'block_phase_aborted', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, toolName });
1144
- const reason = `Aria binding gate: phase ${phaseInfo.phase.id} of plan ${plan.planId} was reported aborted in this transcript. Plan progression is halted.
1690
+ // INLINE architect-fallback (Hamza directive 2026-04-27 same recovery as
1691
+ // no-plan branch above). When phase is aborted, fire architect-fallback
1692
+ // synchronously here to mint a fresh plan instead of deadlocking. If
1693
+ // architect-fallback fails the gate still BLOCKS per doctrine but the
1694
+ // failure is LOUD per feedback_no_graceful_degradation.md.
1695
+ bindingAuditAppend({ event: 'phase_aborted_inline_fallback_attempt', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, toolName });
1696
+ const blockerReason = `pre-tool-gate inline fallback: phase ${phaseInfo.phase.id} of plan ${plan.planId} aborted, ${toolName} attempted on ${cmdPreview.slice(0, 200)}`;
1697
+ const fallbackEvent = JSON.stringify({ reason: blockerReason, sessionId, toolName, currentPlanId: plan.planId });
1698
+ let architectExit = -1;
1699
+ let architectStderr = '';
1700
+ try {
1701
+ const archProc = spawnSync(process.execPath, [`${HOME}/.claude/hooks/aria-architect-fallback.mjs`], {
1702
+ input: fallbackEvent,
1703
+ encoding: 'utf8',
1704
+ timeout: 60000,
1705
+ });
1706
+ architectExit = archProc.status ?? -1;
1707
+ architectStderr = (archProc.stderr || '').slice(0, 500);
1708
+ } catch (err) {
1709
+ architectStderr = String(err).slice(0, 500);
1710
+ }
1711
+ bindingAuditAppend({ event: 'aborted_phase_architect_fallback_result', sessionId, exit: architectExit, stderr: architectStderr.slice(0, 300) });
1712
+
1713
+ const freshPlan = loadActivePlan(sessionId);
1714
+ if (freshPlan && freshPlan.planId !== plan.planId) {
1715
+ process.stderr.write(`\n✓ PRE-TOOL-GATE FALLBACK: aborted phase recovered, fresh plan ${freshPlan.planId} minted. Continuing.\n`);
1716
+ bindingAuditAppend({ event: 'aborted_phase_recovered', sessionId, oldPlanId: plan.planId, newPlanId: freshPlan.planId });
1717
+ plan = freshPlan;
1718
+ const freshPhaseInfo = pickCurrentPhase(plan, transcriptText);
1719
+ if (!freshPhaseInfo || freshPhaseInfo.abortedHere) {
1720
+ bindingAuditAppend({ event: 'block_aborted_phase_post_fallback_still_bad', sessionId, planId: plan.planId });
1721
+ 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 }));
1723
+ process.exit(2);
1724
+ }
1725
+ phaseInfo = freshPhaseInfo;
1726
+ } else {
1727
+ bindingAuditAppend({ event: 'block_phase_aborted_fallback_failed', sessionId, planId: plan.planId, phaseId: phaseInfo.phase.id, architectExit });
1728
+ 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 : ''}
1145
1729
 
1146
1730
  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.`;
1147
- console.log(JSON.stringify({ decision: 'block', reason }));
1148
- process.exit(2);
1731
+ console.log(JSON.stringify({ decision: 'block', reason }));
1732
+ process.exit(2);
1733
+ }
1149
1734
  }
1150
1735
 
1151
1736
  const { action, target } = classifyToolForBinding(toolName, cmd, filePath);
@@ -1155,13 +1740,22 @@ What Claude must do: emit [PLAN_BLOCKER reason="<concrete observation>" suggeste
1155
1740
  const forbidden = (phase.forbiddenActions || []).find((p) => actionMatchesPattern(action, p, target));
1156
1741
  if (forbidden) {
1157
1742
  bindingAuditAppend({ event: 'block_forbidden_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target, matchedRule: forbidden });
1743
+ // Hive recipe lookup BEFORE emitting the block — if the same shape has
1744
+ // fired before, surface the recipe inline. Lookup is fail-soft: a probe
1745
+ // failure leaves the block message unchanged.
1746
+ const lookup = await lookupBlockPatternRecipe({
1747
+ detectorClass: 'doctrine_violation',
1748
+ signature: `binding-forbidden::action=${action}::matches=${forbidden}`,
1749
+ tenantId: sessionId,
1750
+ });
1751
+ const recipeAddendum = renderRecipeAddendum(lookup);
1158
1752
  const reason = `Aria binding gate: action "${action}" on target "${target}" matches forbidden pattern "${forbidden}" for current phase ${phase.id} ("${phase.summary}") of plan ${plan.planId}.
1159
1753
 
1160
1754
  Phase summary: ${phase.summary}
1161
1755
  Forbidden actions for this phase: ${(phase.forbiddenActions || []).join(', ') || '(none)'}
1162
1756
  Allowed actions for this phase: ${(phase.allowedActions || []).join(', ') || '(none)'}
1163
1757
 
1164
- Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan.`;
1758
+ Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] requesting Aria amend the plan.${recipeAddendum}`;
1165
1759
  console.log(JSON.stringify({ decision: 'block', reason }));
1166
1760
  process.exit(2);
1167
1761
  }
@@ -1169,12 +1763,18 @@ Claude must either: (a) reframe the action to fit allowedActions, OR (b) emit [P
1169
1763
  const allowed = (phase.allowedActions || []).find((p) => actionMatchesPattern(action, p, target));
1170
1764
  if (!allowed) {
1171
1765
  bindingAuditAppend({ event: 'block_action_not_in_allowed_list', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
1766
+ const lookup = await lookupBlockPatternRecipe({
1767
+ detectorClass: 'doctrine_violation',
1768
+ signature: `binding-not-allowed::action=${action}::phase=${phase.id}`,
1769
+ tenantId: sessionId,
1770
+ });
1771
+ const recipeAddendum = renderRecipeAddendum(lookup);
1172
1772
  const reason = `Aria binding gate: action "${action}" on target "${target}" is NOT in allowedActions for current phase ${phase.id} of plan ${plan.planId}.
1173
1773
 
1174
1774
  Phase summary: ${phase.summary}
1175
1775
  Allowed actions: ${(phase.allowedActions || []).join(', ') || '(none — phase is observation-only)'}
1176
1776
 
1177
- Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment.`;
1777
+ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_BLOCKER reason="..."] for plan amendment.${recipeAddendum}`;
1178
1778
  console.log(JSON.stringify({ decision: 'block', reason }));
1179
1779
  process.exit(2);
1180
1780
  }
@@ -1182,7 +1782,352 @@ Claude must either: (a) reframe action to fit allowedActions, OR (b) emit [PLAN_
1182
1782
  bindingAuditAppend({ event: 'allow_phase_action', sessionId, planId: plan.planId, phaseId: phase.id, action, target });
1183
1783
  }
1184
1784
 
1185
- // Non-trivial action with cognition AND (binding-allowed OR binding-disabled) — allow.
1785
+ // ── Outcome Ledger regression flag (fail-soft) ───────────────────────────────
1786
+ //
1787
+ // Before allowing, query aria_outcome_ledger for recent regressions on the same
1788
+ // action shape. This is NOT a block — it's a soft-warning surfaced to stderr so
1789
+ // the agent can see the risk and decide whether the root cause has shipped.
1790
+ //
1791
+ // action_kind: 'edit' for Edit/Write/NotebookEdit, 'bash' for Bash
1792
+ // action_target: file path for file tools; first word of bash command otherwise
1793
+ //
1794
+ // Fail-open: network errors, timeouts, or non-200 responses are silently ignored.
1795
+ // The gate does not hardcode timeouts (no-timeouts doctrine); the 3s AbortController
1796
+ // is a DETECTION PROBE — if the endpoint is alive it will respond quickly; if it
1797
+ // is down the catch path fail-opens without blocking the developer.
1798
+ (async function checkOutcomeLedger() {
1799
+ const _harnessUrl =
1800
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1801
+ process.env.ARIA_HARNESS_BASE_URL ||
1802
+ process.env.ARIA_HARNESS_URL ||
1803
+ 'https://harness.ariasos.com';
1804
+ const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
1805
+
1806
+ // Derive action_kind and action_target from current tool call
1807
+ let _actionKind = 'bash';
1808
+ let _actionTarget = '';
1809
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
1810
+ _actionKind = 'edit';
1811
+ _actionTarget = filePath;
1812
+ } else if (toolName === 'Bash') {
1813
+ _actionKind = 'bash';
1814
+ // First word of the command (the verb) as the shape key
1815
+ _actionTarget = cmd.trim().split(/\s+/)[0] || '';
1816
+ // If the command touches a specific file path, prefer that as target
1817
+ // (captures edit-like bash operations: sed, awk, tee, etc.)
1818
+ const _fileArgMatch = cmd.match(/\b([\w./~-]+\.[a-z]{2,6})\b/i);
1819
+ if (_fileArgMatch) _actionTarget = _fileArgMatch[1];
1820
+ }
1821
+
1822
+ if (!_actionTarget) return; // nothing useful to query
1823
+
1824
+ try {
1825
+ const _ctl = new AbortController();
1826
+ const _probeTimer = setTimeout(() => _ctl.abort(), 3000);
1827
+ let _resp;
1828
+ try {
1829
+ const _params = new URLSearchParams({ action_kind: _actionKind, action_target: _actionTarget });
1830
+ _resp = await fetch(`${_harnessUrl}/api/harness/outcome-recent-regressions?${_params}`, {
1831
+ method: 'GET',
1832
+ headers: {
1833
+ 'Content-Type': 'application/json',
1834
+ ...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
1835
+ },
1836
+ signal: _ctl.signal,
1837
+ });
1838
+ } finally {
1839
+ clearTimeout(_probeTimer);
1840
+ }
1841
+
1842
+ if (!_resp || !_resp.ok) return; // endpoint unreachable or error — fail-open
1843
+ const _data = await _resp.json();
1844
+ const _regressions = Array.isArray(_data?.regressions) ? _data.regressions : [];
1845
+
1846
+ if (_regressions.length > 0) {
1847
+ // Soft-warning to stderr — visible in Claude Code's hook output but does NOT block
1848
+ const _lines = _regressions.map(
1849
+ (r) => ` - ${r.action_target} at ${new Date(r.created_at).toISOString()}: ${r.regression_signal || '(no signal text)'}`,
1850
+ ).join('\n');
1851
+ process.stderr.write(
1852
+ `⚠️ Aria Outcome Ledger: this action shape regressed ${_regressions.length} time(s) in the last 60 min:\n` +
1853
+ `${_lines}\n` +
1854
+ `Re-emitting with this risk visible. Proceed only if root cause has shipped since.\n`,
1855
+ );
1856
+ audit(`warn-ledger-regression ${_actionKind} target=${_actionTarget} count=${_regressions.length}`, cmdPreview);
1857
+ }
1858
+ } catch {
1859
+ // Network error, abort, parse failure — fail-open, no warning
1860
+ }
1861
+ })();
1862
+
1863
+ // ── Substrate-bound contract gate via SDK.checkAction ──────────────────────
1864
+ // Hamza directive 2026-04-28: local cognition substance + binding-plan gates
1865
+ // pass, but the substrate has its own contract gate that knows about deploy
1866
+ // state, soul-charge, hospital admission policy, etc. checkAction returns
1867
+ // { allowed, reason, requiredGates }. allowed:false halts the tool with the
1868
+ // substrate's reason. SDK call failure is non-blocking (fail-open) — halting
1869
+ // every tool when substrate is down would brick the orchestrator, but the
1870
+ // failure is logged for telemetry.
1871
+ try {
1872
+ const { HTTPHarnessClient } = await import('@aria/harness-http-client');
1873
+ const { readFileSync: __fsRead, existsSync: __fsExists } = await import('node:fs');
1874
+ const { homedir: __homedir } = await import('node:os');
1875
+ const __home = __homedir();
1876
+ const __tokenPath = `${__home}/.aria/owner-token`;
1877
+ const __licensePath = `${__home}/.aria/license.json`;
1878
+ // Tier detection: client if license.json has a jti, else owner.
1879
+ // Hamza correction 2026-04-28b: master/owner-token credentials belong
1880
+ // to Hamza only — never resolve them on client-tier processes.
1881
+ let __isOwner = true;
1882
+ try {
1883
+ if (__fsExists(__licensePath)) {
1884
+ const __lic = JSON.parse(__fsRead(__licensePath, 'utf8'));
1885
+ __isOwner = !__lic.jti;
1886
+ }
1887
+ } catch { __isOwner = true; }
1888
+ // Resolution: ARIA_HARNESS_TOKEN env first (both tiers). ONLY on owner
1889
+ // tier, fall back to master/api-key env or owner-token file.
1890
+ let __apiKey = process.env.ARIA_HARNESS_TOKEN || '';
1891
+ if (!__apiKey && __isOwner) {
1892
+ __apiKey = process.env.ARIA_MASTER_TOKEN
1893
+ || process.env.ARIA_API_KEY
1894
+ || (__fsExists(__tokenPath) ? __fsRead(__tokenPath, 'utf8').trim() : '');
1895
+ }
1896
+ // Map Claude tool names → checkAction action enum (substrate signature is
1897
+ // 'deploy'|'build'|'write'|'delete'). Most state-mutating tools map to
1898
+ // 'write'; explicit deploy/destructive cases would map elsewhere if the
1899
+ // substrate widens the enum later.
1900
+ const __toolToAction = { Bash: 'write', Edit: 'write', Write: 'write', NotebookEdit: 'write' };
1901
+ const __action = __toolToAction[toolName];
1902
+ if (__apiKey && __action) {
1903
+ const __client = new HTTPHarnessClient({
1904
+ baseUrl:
1905
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1906
+ process.env.ARIA_HARNESS_BASE_URL ||
1907
+ process.env.ARIA_HARNESS_URL ||
1908
+ 'https://harness.ariasos.com',
1909
+ apiKey: __apiKey,
1910
+ });
1911
+ const __target = JSON.stringify(toolInput || {}).slice(0, 500);
1912
+ const __check = await __client.checkAction(__action, __target);
1913
+ if (__check && __check.allowed === false) {
1914
+ const __reason = `Aria substrate checkAction DENIED for ${toolName}/${__action}: ${__check.reason || 'no reason given'}. Required gates: [${(__check.requiredGates || []).join(', ')}].
1915
+
1916
+ 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
+ audit(`block-substrate-checkAction ${toolName.toLowerCase()} action=${__action} reason="${(__check.reason || '').slice(0, 80)}"`, cmdPreview);
1918
+ pushDecision('block', `substrate checkAction denied: ${(__check.reason || 'unspecified').slice(0, 100)}`);
1919
+ console.log(JSON.stringify({ decision: 'block', reason: __reason }));
1920
+ process.exit(2);
1921
+ }
1922
+ }
1923
+ } catch (err) {
1924
+ // SDK call failure is non-blocking — gate degrades to local-only doctrine.
1925
+ // The failure is recorded so a fleet probe can detect substrate-side
1926
+ // contract gate outages.
1927
+ console.warn(`[pre-tool-gate] substrate checkAction failed: ${err && err.message ? err.message : err}`);
1928
+ }
1929
+
1930
+ // ── Hive session-lock check ───────────────────────────────────────────────────
1931
+ // For Edit/Write/NotebookEdit: check file_path against hive_session_locks.
1932
+ // For Bash with file-mutating verbs (rm, mv, sed -i, awk, tee, cp, chmod,
1933
+ // truncate, install): extract the file argument and check it.
1934
+ //
1935
+ // If an active lock exists from a DIFFERENT session, BLOCK with coordination
1936
+ // instructions. Fail-open only on network error (endpoint unreachable).
1937
+ //
1938
+ // Per feedback_no_graceful_degradation.md: parse errors and non-network
1939
+ // failures surface in the block reason — they are not silently ignored.
1940
+ // Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
1941
+ (async function checkHiveSessionLock() {
1942
+ // Determine the file path to check
1943
+ let _lockCheckPath = '';
1944
+
1945
+ if (toolName === 'Edit' || toolName === 'Write' || toolName === 'NotebookEdit') {
1946
+ _lockCheckPath = filePath;
1947
+ } else if (toolName === 'Bash') {
1948
+ // Destructive bash verbs that mutate files — extract the file argument
1949
+ const _mutatingVerbRx = /^(rm|mv|cp|sed\s+-i|awk\s+.*-i|tee|truncate|install|chmod|chown)\b/;
1950
+ if (_mutatingVerbRx.test(cmd.trim())) {
1951
+ // Extract the first file-path argument: look for something with /
1952
+ const _pathMatch = cmd.match(/\s+((?:\/|~\/|\.\.?\/|[\w.-]+\/)[^\s'"]+)/);
1953
+ if (_pathMatch) _lockCheckPath = _pathMatch[1];
1954
+ }
1955
+ }
1956
+
1957
+ if (!_lockCheckPath) return; // no file target — skip lock check
1958
+
1959
+ const _soulUrl =
1960
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1961
+ process.env.ARIA_SOUL_URL ||
1962
+ process.env.ARIA_HARNESS_BASE_URL ||
1963
+ process.env.ARIA_HARNESS_URL ||
1964
+ 'https://harness.ariasos.com';
1965
+ const _harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
1966
+
1967
+ let _lockResp;
1968
+ try {
1969
+ const _params = new URLSearchParams({ file_path: _lockCheckPath });
1970
+ _lockResp = await fetch(`${_soulUrl}/api/hive/session-lock?${_params}`, {
1971
+ method: 'GET',
1972
+ headers: {
1973
+ 'Content-Type': 'application/json',
1974
+ ...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
1975
+ },
1976
+ });
1977
+ } catch (_netErr) {
1978
+ // Endpoint unreachable — fail-open. Lock check is a coordination layer,
1979
+ // not a safety hard-stop. Infra-down must not block all development.
1980
+ audit(`allow-lock-check-network-error path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
1981
+ return;
1982
+ }
1983
+
1984
+ if (!_lockResp.ok) {
1985
+ // Non-200 — route may not be deployed yet. Fail-open with audit record.
1986
+ audit(`allow-lock-check-http-error status=${_lockResp.status} path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
1987
+ return;
1988
+ }
1989
+
1990
+ // Per feedback_no_graceful_degradation.md: JSON parse error is surfaced,
1991
+ // not swallowed. A malformed response IS a defect that must be visible.
1992
+ const _lockData = await _lockResp.json();
1993
+ const _activeLocks = Array.isArray(_lockData?.locks) ? _lockData.locks : [];
1994
+
1995
+ // Filter to locks from a DIFFERENT session
1996
+ const _conflictingLocks = _activeLocks.filter(
1997
+ (l) => l.session_id && String(l.session_id) !== String(sessionId),
1998
+ );
1999
+
2000
+ if (_conflictingLocks.length === 0) {
2001
+ audit(`allow-lock-clear path=${_lockCheckPath.slice(0, 80)}`, cmdPreview);
2002
+ return; // no conflict — proceed
2003
+ }
2004
+
2005
+ // ── Auto-post coordination message to each conflicting session ───────────
2006
+ // Per hive-session-coordination doctrine (memory:feedback_hive_session_coordination.md):
2007
+ // when a lock conflict is detected, the gate AUTOMATICALLY posts a
2008
+ // lock_conflict_request session-message to each conflicting session so they see
2009
+ // the inbound coordination request in their next turn's HIVE_SESSION_INBOX block.
2010
+ // The model never has to manually post — the gate is the structural binding.
2011
+ //
2012
+ // Per feedback_no_timeouts_doctrine.md: no AbortSignal or setTimeout.
2013
+ // Per feedback_no_graceful_degradation.md: message-post failures are logged
2014
+ // loudly to stderr; they do NOT silently degrade — the block still fires regardless.
2015
+ const _autoMessageIds = [];
2016
+ for (const _conflict of _conflictingLocks) {
2017
+ try {
2018
+ const _msgId = `lock-conflict-${sessionId}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
2019
+ const _requestedAt = new Date().toISOString();
2020
+ const _msgBody = {
2021
+ coordination_protocol: 'aria-hive-lock-conflict/v2',
2022
+ coordination_key: `file:${_lockCheckPath}`,
2023
+ file_path: _lockCheckPath,
2024
+ requesting_session_id: sessionId,
2025
+ requesting_client_id: 'aria-connector',
2026
+ requesting_surface: 'pre-tool-gate',
2027
+ intent_summary: `Session ${sessionId} attempted to edit ${_lockCheckPath} via ${toolName} but found your active lock. Requesting coordination — please release when done.`,
2028
+ requested_at: _requestedAt,
2029
+ tool_name: toolName,
2030
+ command_preview: cmdPreview,
2031
+ file_claims: [{ path: _lockCheckPath, intent: 'edit' }],
2032
+ target_lock: {
2033
+ lock_id: _conflict.lock_id ?? null,
2034
+ session_id: _conflict.session_id ?? null,
2035
+ client_id: _conflict.client_id ?? null,
2036
+ surface: _conflict.surface ?? null,
2037
+ locked_at: _conflict.locked_at ?? null,
2038
+ expires_at: _conflict.expires_at ?? null,
2039
+ },
2040
+ };
2041
+ const _msgResp = await fetch(`${_soulUrl}/api/hive/session-message`, {
2042
+ method: 'POST',
2043
+ headers: {
2044
+ 'Content-Type': 'application/json',
2045
+ ...(_harnessToken ? { Authorization: `Bearer ${_harnessToken}` } : {}),
2046
+ },
2047
+ body: JSON.stringify({
2048
+ message_id: _msgId,
2049
+ from_session_id: sessionId,
2050
+ to_session_id: _conflict.session_id,
2051
+ topic: 'lock_conflict_request',
2052
+ body: _msgBody,
2053
+ }),
2054
+ });
2055
+ if (_msgResp.ok) {
2056
+ _autoMessageIds.push({ session_id: _conflict.session_id, message_id: _msgId });
2057
+ audit(`auto-msg-sent lock-conflict to=${_conflict.session_id} msg=${_msgId} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
2058
+ } else {
2059
+ const _errText = await _msgResp.text().catch(() => 'unreadable');
2060
+ process.stderr.write(
2061
+ `[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} failed: HTTP ${_msgResp.status} ${_errText.slice(0, 200)}\n`,
2062
+ );
2063
+ audit(`auto-msg-failed lock-conflict to=${_conflict.session_id} status=${_msgResp.status} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
2064
+ }
2065
+ } catch (_msgErr) {
2066
+ // Network error posting auto-message — log loudly per no-graceful-degradation doctrine.
2067
+ // Block still fires; auto-message is additive signal, not a safety gate.
2068
+ process.stderr.write(
2069
+ `[aria-pre-tool-gate] WARN auto-message to session ${_conflict.session_id} threw: ${_msgErr instanceof Error ? _msgErr.message : String(_msgErr)}\n`,
2070
+ );
2071
+ audit(`auto-msg-error lock-conflict to=${_conflict.session_id} path=${_lockCheckPath.slice(0, 60)}`, cmdPreview);
2072
+ }
2073
+ }
2074
+
2075
+ const _conflictDetails = _conflictingLocks.map((l) => {
2076
+ const _ago = l.locked_at ? `since ${l.locked_at}` : '';
2077
+ const _expires = l.expires_at ? `, expires ${l.expires_at}` : '';
2078
+ const _who = l.client_id ? ` (client: ${l.client_id})` : '';
2079
+ const _surface = l.surface ? ` [${l.surface}]` : '';
2080
+ const _sentMsg = _autoMessageIds.find((m) => m.session_id === l.session_id);
2081
+ const _msgNote = _sentMsg
2082
+ ? ` [auto-coordination message posted: ${_sentMsg.message_id}]`
2083
+ : ' [auto-message failed — coordinate manually]';
2084
+ return ` - Session ${l.session_id}${_who}${_surface} holds lock on ${l.file_path} ${_ago}${_expires}${_msgNote}`;
2085
+ }).join('\n');
2086
+
2087
+ const _autoMsgSummary = _autoMessageIds.length > 0
2088
+ ? `\nAuto-coordination: gate posted lock_conflict_request message(s) to ${_autoMessageIds.map((m) => `session ${m.session_id} (msg: ${m.message_id})`).join(', ')}. They will see this inbound on their next turn via [HIVE_SESSION_INBOX].`
2089
+ : '\nAuto-coordination message could not be delivered (see stderr). Coordinate manually via POST /api/hive/session-message.';
2090
+
2091
+ const _lockBlockReason = `Hive session-lock conflict: another session holds an active lock on this file.
2092
+
2093
+ File: ${_lockCheckPath}
2094
+
2095
+ Conflicting locks:
2096
+ ${_conflictDetails}
2097
+ ${_autoMsgSummary}
2098
+
2099
+ Resolution:
2100
+ 1. A lock_conflict_request message was automatically posted to the lock-holding session. Wait for them to see it.
2101
+ 2. They release via: aria hive lock release --lock-id <ID> OR DELETE /api/hive/session-lock.
2102
+ 3. No automatic timeout-based release — explicit release only (memory:feedback_no_timeouts_doctrine.md).
2103
+ 4. Retry this action — the gate allows when no conflicting lock exists.
2104
+
2105
+ Per memory:feedback_hive_session_coordination.md: blind-edit on the same file from concurrent sessions
2106
+ causes merge conflicts and state divergence. Explicit coordination is the only safe path.`;
2107
+
2108
+ audit(`block-hive-lock-conflict path=${_lockCheckPath.slice(0, 80)} conflicting_sessions=${_conflictingLocks.map((l) => l.session_id).join(',')} auto_msgs=${_autoMessageIds.length}`, cmdPreview);
2109
+ pushDecision('block', `hive session-lock conflict on ${_lockCheckPath.slice(0, 80)}`);
2110
+ console.log(JSON.stringify({
2111
+ decision: 'block',
2112
+ reason: _lockBlockReason,
2113
+ hookSpecificOutput: {
2114
+ hookEventName: 'PreToolUse',
2115
+ conflicting_locks: _conflictingLocks,
2116
+ auto_coordination_messages: _autoMessageIds,
2117
+ recovery: {
2118
+ action: 'wait_for_lock_release_then_retry',
2119
+ file_path: _lockCheckPath,
2120
+ conflicting_session_ids: _conflictingLocks.map((l) => l.session_id),
2121
+ auto_message_posted: _autoMessageIds.length > 0,
2122
+ send_message_endpoint: '/api/hive/session-message',
2123
+ release_lock_endpoint: '/api/hive/session-lock',
2124
+ },
2125
+ },
2126
+ }));
2127
+ process.exit(2);
2128
+ })();
2129
+
2130
+ // Non-trivial action with cognition AND (binding-allowed OR binding-disabled) AND substrate-cleared — allow.
1186
2131
  audit(`allow-cognition ${toolName.toLowerCase()} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
1187
2132
  pushDecision('allow', `${toolName.toLowerCase()} with ${lensCount} lenses (${cognitionSource})`);
1188
2133
  process.exit(0);