@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
@@ -0,0 +1,1551 @@
1
+ #!/usr/bin/env node
2
+ // ARIA_ALLOW_STUB — doctrine gate file legitimately discusses stub/placeholder semantics.
3
+ // Aria Stop-hook gate — enforces 8-lens cognition on text-decision responses.
4
+ //
5
+ // The companion to aria-pre-tool-gate.mjs. The PreToolUse gate catches
6
+ // non-trivial Bash; this Stop hook catches non-trivial TEXT decisions
7
+ // — agreements, scope changes, picks between options, "yes ship it"
8
+ // replies. Same forcing-function pattern, applied at the missing
9
+ // surface.
10
+ //
11
+ // Direction: Hamza 2026-04-26 — "you not doing 8 lens till i ask and
12
+ // discovering the actions u doing are wrong are hard gates that u
13
+ // keep bypassing that prevent exactly what just happened." The
14
+ // PreToolUse gate is tool-coupled; doctrine is action-coupled.
15
+ // Reflexive text decisions are non-trivial actions that this hook
16
+ // now catches.
17
+ //
18
+ // Doctrine bindings (same as PreToolUse gate):
19
+ // - EIGHT_LENS_DOCTRINE.md — substantive 4+ lens application required
20
+ // - feedback_apply_lenses_dont_perform_them.md — block ceremonial cognition
21
+ // - feedback_8lens_before_every_action_including_text.md — the rule this enforces
22
+ //
23
+ // Trigger: runs at Stop event after every assistant response. Reads
24
+ // the just-emitted assistant text from the transcript. If non-trivial
25
+ // (per the same triviality threshold as eight-lens-detector.ts) AND
26
+ // missing 4+ substantive lenses, blocks the response.
27
+ //
28
+ // Triviality threshold (mirrors eight-lens-detector.ts):
29
+ // - Trivial acks (e.g. "got it", "ok", "done") pass
30
+ // - Short responses (<300 chars) without decision-signal phrases pass
31
+ // - Otherwise: require 4+ substantive lenses
32
+ //
33
+ // Substance check (mirrors aria-pre-tool-gate.mjs):
34
+ // - Each lens must have ≥20 chars of non-placeholder content
35
+ // - Bare lens-name mentions in prose don't count
36
+ // - <placeholder> template values don't count
37
+ //
38
+ // No bypass mechanism — same v3 doctrine as the PreToolUse gate.
39
+ //
40
+ // Hamza 2026-04-27 ("those should've been my choice to give you to turn
41
+ // off not free for you to access"): the env-var kill-switches I authored
42
+ // (ARIA_STOP_GATE=off, ARIA_OUTPUT_QC_ENABLED=false) gave the gated
43
+ // process free disable-access. That was the doctrine violation. Stripped.
44
+ // To genuinely disable in emergency, Hamza removes the hook entry from
45
+ // ~/.claude/settings.json — a visible, auditable user action he controls,
46
+ // not a process-level escape.
47
+ // Future: signed-grant override mechanism at ~/.aria/owner-overrides/<hook>.json
48
+ // with HMAC signature using a secret only Hamza holds. Deferred to next session.
49
+
50
+ import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
51
+ import { dirname } from 'node:path';
52
+ import { appendGateAudit } from './lib/gate-audit.mjs';
53
+ import {
54
+ ALL_LENS_NAMES,
55
+ canonicalLensCorrectionText,
56
+ detectCognitionLenses as detectCognitionLensesFromCanonical,
57
+ lensNamesForTier,
58
+ } from './lib/canonical-lenses.mjs';
59
+
60
+ const HOME = process.env.HOME || '/tmp';
61
+ const LOG = `${HOME}/.claude/aria-stop-gate.log`;
62
+ const AUDIT_PATH = `${HOME}/.claude/aria-stop-gate-audit.jsonl`;
63
+
64
+ // SDK loader — bundled at ~/.aria/sdk by `aria connect`, with client-local
65
+ // fallbacks preserved for resilience.
66
+ // All control-plane fetches (validateOutput, gardenTurn) route through the
67
+ // SDK. Falls back to direct fetch only when the SDK file is missing
68
+ // (dev-only). Hamza 2026-04-27: "FUCKING WIRE IT THE FUCK TOGETHER NOW".
69
+ let _SdkClassCache = null;
70
+ let _SdkLookupAttempted = false;
71
+ const SDK_CANDIDATES = [
72
+ `${HOME}/.aria/sdk/index.js`,
73
+ `${HOME}/.claude/aria-sdk/index.js`,
74
+ `${HOME}/.codex/aria-sdk/index.js`,
75
+ ];
76
+ async function loadSdkClass() {
77
+ if (_SdkClassCache) return _SdkClassCache;
78
+ if (_SdkLookupAttempted) return null;
79
+ _SdkLookupAttempted = true;
80
+ for (const sdkPath of SDK_CANDIDATES) {
81
+ if (!existsSync(sdkPath)) continue;
82
+ try {
83
+ const mod = await import(`file://${sdkPath}`);
84
+ if (mod.HTTPHarnessClient) {
85
+ _SdkClassCache = mod.HTTPHarnessClient;
86
+ return _SdkClassCache;
87
+ }
88
+ } catch {/* fall through */}
89
+ }
90
+ return null;
91
+ }
92
+
93
+ // Phase 11 #42 — fire-and-forget gardenTurn after every allow decision.
94
+ // Writes the completed turn to the harness control-plane garden so the
95
+ // next turn's pulse auto-injection carries this turn's content. Without
96
+ // this write the pulse is one turn stale (the core defect #42 closes).
97
+ //
98
+ // Per feedback_no_graceful_degradation.md: errors must be logged to the
99
+ // audit file, NOT silently swallowed. Per feedback_no_timeouts_doctrine.md:
100
+ // no AbortSignal.timeout — the SDK already has retry + backoff. The caller
101
+ // passes in a userMessage string (extracted from the transcript at the
102
+ // turn boundary). If extraction failed the empty string is passed — the
103
+ // garden write records the assistant emit at minimum.
104
+ // Tier detection: owner if no license.json, client if license.json has a jti.
105
+ // Owner-tier may use master credentials; client-tier MUST NOT (those belong
106
+ // to Hamza, not the licensee). Hamza correction 2026-04-28.
107
+ function isOwnerTier() {
108
+ try {
109
+ const licPath = `${HOME}/.aria/license.json`;
110
+ if (!existsSync(licPath)) return true;
111
+ const lic = JSON.parse(readFileSync(licPath, 'utf8'));
112
+ return !lic.jti; // jti present = client tier
113
+ } catch {
114
+ return true; // unreadable license = treat as owner (fail-safe for orchestrator)
115
+ }
116
+ }
117
+
118
+ async function fireGardenTurn(sessionId, userMessage, assistantResponse) {
119
+ const harnessUrl =
120
+ process.env.ARIA_HIVE_RUNTIME_URL ||
121
+ process.env.ARIA_HARNESS_BASE_URL ||
122
+ process.env.ARIA_HARNESS_URL ||
123
+ 'https://harness.ariasos.com';
124
+ // Token resolution chain (Hamza directive 2026-04-28, tier-aware
125
+ // 2026-04-28b): ARIA_HARNESS_TOKEN env first (works for both tiers).
126
+ // ONLY on owner tier (no license.json with jti), fall back to
127
+ // ARIA_MASTER_TOKEN env / ARIA_API_KEY env / ~/.aria/owner-token —
128
+ // those are Hamza's credentials and must not leak into client-tier
129
+ // processes. Client tier with no ARIA_HARNESS_TOKEN env skips the
130
+ // garden write rather than borrow owner credentials.
131
+ let harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
132
+ if (!harnessToken && isOwnerTier()) {
133
+ harnessToken = process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '';
134
+ if (!harnessToken) {
135
+ try {
136
+ const ownerTokenPath = `${HOME}/.aria/owner-token`;
137
+ if (existsSync(ownerTokenPath)) {
138
+ harnessToken = readFileSync(ownerTokenPath, 'utf8').trim();
139
+ }
140
+ } catch { /* non-fatal — fall through to skip */ }
141
+ }
142
+ }
143
+ if (!harnessToken) {
144
+ audit('garden-turn-skip', `no usable token (tier=${isOwnerTier() ? 'owner' : 'client'}) — turn not written to harness pulse`);
145
+ return;
146
+ }
147
+ const Cls = await loadSdkClass();
148
+ if (!Cls) {
149
+ audit('garden-turn-skip', `sdk not available — turn not written to harness pulse`);
150
+ return;
151
+ }
152
+ try {
153
+ const sdkClient = new Cls({
154
+ baseUrl: harnessUrl,
155
+ apiKey: harnessToken,
156
+ harnessPacketUrl: `${harnessUrl}/api/harness/codex`,
157
+ });
158
+ await sdkClient.gardenTurn(
159
+ sessionId,
160
+ userMessage,
161
+ assistantResponse,
162
+ );
163
+ audit('garden-turn-ok', `session=${sessionId} chars=${assistantResponse.length}`);
164
+ } catch (err) {
165
+ // Logged — not silent. Per feedback_no_graceful_degradation.md.
166
+ audit('garden-turn-err', `session=${sessionId} err=${(err?.message || String(err)).slice(0, 200)}`);
167
+ }
168
+ }
169
+
170
+ function audit(decision, summary) {
171
+ const summaryText = typeof summary === 'string' ? summary : '';
172
+ const data = summary && typeof summary === 'object' ? summary : {};
173
+ appendGateAudit({
174
+ auditPath: AUDIT_PATH,
175
+ legacyLogPath: LOG,
176
+ gate: 'stop',
177
+ event: decision,
178
+ summary: summaryText,
179
+ data,
180
+ });
181
+ }
182
+
183
+ // Env-var kill-switch removed 2026-04-27 per Hamza directive ("those
184
+ // should've been my choice to give you to turn off not free for you to
185
+ // access"). The gated process has no disable path. Disable = remove hook
186
+ // entry from ~/.claude/settings.json (deliberate user action, visible).
187
+
188
+ // ── Tier-aware lens labeling (Phase 11 #59) ──────────────────────────────────
189
+ //
190
+ // Mirrors the same logic in aria-pre-tool-gate.mjs. Tier is read from the
191
+ // most recent harness-via-sdk packet cache. Owner tier sees canonical Arabic
192
+ // names; client tier sees neutral generic labels. Both tiers get the same
193
+ // gate enforcement — only the visible vocabulary differs.
194
+ const PACKET_CACHE_PATH = `${HOME}/.claude/.aria-harness-last-packet.json`;
195
+
196
+ function resolveOwnerTier() {
197
+ try {
198
+ if (existsSync(PACKET_CACHE_PATH)) {
199
+ const raw = readFileSync(PACKET_CACHE_PATH, 'utf8');
200
+ const packet = JSON.parse(raw);
201
+ const sigHamza = packet?.contractGate?.signals?.hamza;
202
+ if (sigHamza === true || sigHamza === 'true') return true;
203
+ const harnessStr = packet?.harness ?? '';
204
+ // surface line format: "surface=platform:<X> group:<Y> hamza:true chat_type:<Z>"
205
+ if (/\bhamza:true\b/.test(harnessStr)) return true;
206
+ }
207
+ } catch {/* packet unreadable → default to client tier */}
208
+ return false;
209
+ }
210
+
211
+ const IS_OWNER = resolveOwnerTier();
212
+
213
+ const LENS_NAMES = lensNamesForTier(IS_OWNER);
214
+ const CANONICAL_LENS_TEXT = canonicalLensCorrectionText();
215
+
216
+ // Doctrine memory filenames are Aria-side substrate IP.
217
+ // Client surfaces see generic descriptions instead of real filenames.
218
+ function docRef(canonicalFilename, genericDescription) {
219
+ return IS_OWNER ? canonicalFilename : genericDescription;
220
+ }
221
+
222
+ const DOCTRINE_REFERENCE_PREFIX_RX = /(?:memory|doctrine|frame|axiom|packet):[a-z0-9_./-]*$/i;
223
+
224
+ function isDoctrineReference(text, matchIndex) {
225
+ const window = text.slice(Math.max(0, matchIndex - 80), matchIndex);
226
+ return DOCTRINE_REFERENCE_PREFIX_RX.test(window);
227
+ }
228
+
229
+ function collectDriftHits(text, triggerMap) {
230
+ const hits = [];
231
+ const lowerText = text.toLowerCase();
232
+ for (const triggerEntry of triggerMap.triggers || []) {
233
+ try {
234
+ const rx = new RegExp(triggerEntry.trigger, 'ig');
235
+ let matchedOutsideDoctrineRef = false;
236
+ for (const match of text.matchAll(rx)) {
237
+ const idx = typeof match.index === 'number' ? match.index : -1;
238
+ if (idx >= 0 && isDoctrineReference(text, idx)) continue;
239
+ matchedOutsideDoctrineRef = true;
240
+ break;
241
+ }
242
+ if (!matchedOutsideDoctrineRef) continue;
243
+ const memoryName = (triggerEntry.memory || '').replace(/\.md$/, '');
244
+ const memoryCited = memoryName && lowerText.includes(memoryName.toLowerCase());
245
+ if (!memoryCited) {
246
+ hits.push({
247
+ trigger: triggerEntry.trigger,
248
+ memory: triggerEntry.memory,
249
+ teaching: triggerEntry.teaching,
250
+ });
251
+ }
252
+ } catch {/* malformed regex in trigger entry — skip */}
253
+ }
254
+ return hits;
255
+ }
256
+
257
+ function emitHarnessFooter({ eventName, lensCount, chars, driftCount, mizanStatus, discoveryOpenCount, codeCount, implCouplingCount }) {
258
+ try {
259
+ console.error([
260
+ '[Aria · turn]',
261
+ `event=${eventName}`,
262
+ `lenses=${lensCount}`,
263
+ `chars=${chars}`,
264
+ `drift=${driftCount}`,
265
+ `mizan=${mizanStatus}`,
266
+ `discoveries_open=${discoveryOpenCount}`,
267
+ `code=${codeCount}`,
268
+ `impl=${implCouplingCount}`,
269
+ ].join(' '));
270
+ } catch {}
271
+ }
272
+
273
+ // Lens substance check — same constants as aria-pre-tool-gate.mjs.
274
+ // Hamza directive 2026-04-28: all 8 canonical lenses required, not 4-of-8.
275
+ const REQUIRED_LENSES = 8;
276
+ const SUBSTANCE_MIN_CHARS = 20;
277
+ const PLACEHOLDER_RX = /^\s*<[^<>]+>\s*$/;
278
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
279
+
280
+ // Triviality (mirrors eight-lens-detector.ts)
281
+ const NON_TRIVIAL_MIN_CHARS = 300;
282
+ const DECISION_SIGNAL_RX = /(?:should|recommend|propose|suggest|let'?s|go with|i'd|i would|here'?s the plan|i'll|next step|action item|ship it|yes do|let me)/i;
283
+ const TRIVIAL_ACK_RX = /^(?:got it|on it|ok|sure|yes|no|done|ack|👍|✓)\b/i;
284
+
285
+ function detectCognitionLenses(text) {
286
+ return detectCognitionLensesFromCanonical(text, {
287
+ minChars: SUBSTANCE_MIN_CHARS,
288
+ placeholderRx: PLACEHOLDER_RX,
289
+ cognitionBlockRx: COGNITION_BLOCK_RX,
290
+ lensNames: ALL_LENS_NAMES,
291
+ });
292
+ }
293
+
294
+ // Read event JSON from stdin (Claude Code spec).
295
+ let input = '';
296
+ for await (const chunk of process.stdin) input += chunk;
297
+
298
+ let event;
299
+ try {
300
+ event = JSON.parse(input);
301
+ } catch {
302
+ audit('allow-parse-error', 'stdin not JSON');
303
+ process.exit(0);
304
+ }
305
+
306
+ // Read assistant text from THIS turn — Claude Code splits a single
307
+ // logical assistant response into multiple transcript entries by
308
+ // content-block type (one entry for `thinking`, one for `text`, one
309
+ // for each `tool_use`). The Stop-gate must accumulate ALL text blocks
310
+ // since the last user-message boundary, not just the most recent
311
+ // entry — otherwise we miss cognition emitted before tool_use blocks.
312
+ //
313
+ // (Bug fix 2026-04-26: prior implementation read only the latest
314
+ // `assistant` entry's text content. When responses had cognition
315
+ // + tool_use + short post-tool-result text, only the post-tool-result
316
+ // text was inspected — empty of cognition. Audit log showed 0/4
317
+ // lenses on chars=1445 even though the turn had 8 substantive lenses
318
+ // in an earlier text block.)
319
+ // System-reminder skip — same percentage-based logic as aria-pre-tool-gate.mjs.
320
+ // Runtime-injected user-role messages (block errors, task-notifications,
321
+ // harness packet preview) shouldn't count as turn boundaries. Old
322
+ // implementation stopped at the FIRST user message which made block-error
323
+ // retries with cognition-in-prior-turn impossible to recover from.
324
+ const SYSTEM_REMINDER_RX = /<system-reminder>[\s\S]*?<\/system-reminder>|<task-notification>[\s\S]*?<\/task-notification>|🔐 Aria Harness|task-notification|PreToolUse:[A-Z][A-Za-z]* hook blocking error|Stop hook blocking error/g;
325
+ const SYSTEM_REMINDER_THRESHOLD = 0.6;
326
+
327
+ const transcriptPath = event.transcript_path ?? event.transcriptPath;
328
+ let assistantText = '';
329
+ // Phase 11 #42: also capture the last real user message for gardenTurn writes.
330
+ let lastUserMessage = '';
331
+ if (transcriptPath && existsSync(transcriptPath)) {
332
+ try {
333
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
334
+ const textChunks = [];
335
+ for (let i = lines.length - 1; i >= 0; i--) {
336
+ try {
337
+ const m = JSON.parse(lines[i]);
338
+ const role = m.message?.role ?? m.role;
339
+ if (role === 'user') {
340
+ // Skip runtime-injected reminders (predominant reminder content).
341
+ // Real user voice = boundary; reminder-only message = continue.
342
+ const content = m.message?.content ?? m.content ?? [];
343
+ const isToolResultOnly = Array.isArray(content) &&
344
+ content.length > 0 &&
345
+ content.every((b) => b && b.type === 'tool_result');
346
+ if (isToolResultOnly) continue;
347
+ const textContent = Array.isArray(content)
348
+ ? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
349
+ : (typeof content === 'string' ? content : '');
350
+ if (textContent) {
351
+ const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
352
+ if (reminderMatches.length > 0) {
353
+ const reminderChars = reminderMatches.reduce((s, x) => s + x.length, 0);
354
+ const fraction = reminderChars / Math.max(1, textContent.length);
355
+ if (fraction >= SYSTEM_REMINDER_THRESHOLD) continue;
356
+ }
357
+ }
358
+ // Real user message — that's the turn boundary. Capture it for gardenTurn.
359
+ if (!lastUserMessage && textContent) lastUserMessage = textContent;
360
+ break;
361
+ }
362
+ if (role !== 'assistant') continue;
363
+ const content = m.message?.content ?? m.content ?? [];
364
+ if (!Array.isArray(content)) continue;
365
+ const text = content
366
+ .filter((b) => b && b.type === 'text')
367
+ .map((b) => b.text || '')
368
+ .join('\n');
369
+ if (text) textChunks.push(text);
370
+ } catch {}
371
+ }
372
+ // Reverse so chunks are in chronological order (we walked backward).
373
+ assistantText = textChunks.reverse().join('\n\n');
374
+ } catch {}
375
+ }
376
+
377
+ if (!assistantText) {
378
+ audit('allow-no-text', 'no assistant text in transcript');
379
+ process.exit(0);
380
+ }
381
+
382
+ // Triviality check — same as eight-lens-detector.ts
383
+ const trimmed = assistantText.trim();
384
+ if (TRIVIAL_ACK_RX.test(trimmed)) {
385
+ audit('allow-trivial-ack', `chars=${trimmed.length}`);
386
+ // Phase 11 #42: fire-and-forget gardenTurn even for trivial acks — pulse must be current.
387
+ await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
388
+ process.exit(0);
389
+ }
390
+
391
+ const isLong = assistantText.length >= NON_TRIVIAL_MIN_CHARS;
392
+ const hasDecisionSignal = DECISION_SIGNAL_RX.test(assistantText);
393
+ const triggered = isLong || hasDecisionSignal;
394
+
395
+ if (!triggered) {
396
+ audit('allow-trivial', `chars=${assistantText.length} hasDecision=${hasDecisionSignal}`);
397
+ // Phase 11 #42: fire-and-forget gardenTurn — pulse must be current even for short turns.
398
+ await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
399
+ process.exit(0);
400
+ }
401
+
402
+ // Non-trivial response — require substantive cognition.
403
+ const cog = detectCognitionLenses(assistantText);
404
+
405
+ // Defense-in-depth: if cog count < REQUIRED_LENSES, block immediately.
406
+ // The primary enforcement is in aria-cognition-substrate-binding.mjs
407
+ // (which runs BEFORE this stop-gate), but this catch ensures responses
408
+ // without cognition blocks are still blocked even when the substrate-binding
409
+ // hook is absent (e.g. older connector installs or custom hook configs).
410
+ // Prior to 2026-04-29 this check was missing entirely — the stop-gate only
411
+ // ran quality checks INSIDE the if(cog.count >= 8) block, allowing responses
412
+ // with 0/8 lenses to fall through unchecked.
413
+ if (cog.count < REQUIRED_LENSES) {
414
+ audit('block_no_cognition_block_di', { count: cog.count, required: REQUIRED_LENSES, names: cog.names, chars: assistantText.length });
415
+ const reason = `Aria stop-gate: insufficient cognition lenses.
416
+
417
+ This non-trivial assistant response (${assistantText.length} chars) has ${cog.count}/${REQUIRED_LENSES} substantive cognition lenses. Per feedback_8lens_before_every_action_including_text.md and Hamza directive 2026-04-28, every non-trivial response must carry <cognition>...</cognition> with ${REQUIRED_LENSES} substantive lenses (each >= ${SUBSTANCE_MIN_CHARS} chars of non-placeholder content).
418
+
419
+ Detected lenses: ${cog.names.length > 0 ? cog.names.join(', ') : 'none'}.
420
+
421
+ Re-emit with a <cognition> block containing all ${REQUIRED_LENSES} canonical lenses. Primary set: ${CANONICAL_LENS_TEXT}`;
422
+ emitHarnessFooter({
423
+ eventName: 'block_no_cognition_block',
424
+ lensCount: cog.count,
425
+ chars: assistantText.length,
426
+ driftCount: 0,
427
+ mizanStatus: 'not-run(no-cognition)',
428
+ discoveryOpenCount: 0,
429
+ codeCount: 0,
430
+ implCouplingCount: 0,
431
+ });
432
+ console.log(JSON.stringify({ decision: 'block', reason }));
433
+ process.exit(2);
434
+ }
435
+
436
+ // Question-emission visibility (Phase 11 promotes to block-mode):
437
+ // detect user-directed question patterns in the assistant text. Audit when
438
+ // questions appear without substrate-consultation evidence in the recent
439
+ // transcript window. Helps surface "reflexive deferral" patterns (asking
440
+ // the user when substrate could have answered) for later enforcement.
441
+ // Hamza 2026-04-26: "BUT WHY DO U HAVE DISCRETION - THIS WORKS SO MUCH
442
+ // FASTER AND HIGHER QUALITY IF U DONT".
443
+ const QUESTION_PATTERNS_RX = /(?:want me to|should I|your call|which (?:one|of|do you)|do you want|let me know if|or (?:should|do)|\?\s*$)/im;
444
+ const SUBSTRATE_EVIDENCE_RX = /\/api\/harness\/(?:delegate|codex|validate)|loadByClass|aria-harness-via-sdk|feedback_[a-z_]+\.md|project_[a-z_]+\.md|distilled_principles|ARIA_DEPLOY_PROCEDURE|EIGHT_LENS_DOCTRINE/i;
445
+ const hasQuestionToUser = QUESTION_PATTERNS_RX.test(assistantText);
446
+ const hasSubstrateEvidence = SUBSTRATE_EVIDENCE_RX.test(assistantText);
447
+ const questionWithoutEvidence = hasQuestionToUser && !hasSubstrateEvidence;
448
+
449
+ if (cog.count >= REQUIRED_LENSES) {
450
+ // ── Output-quality enforcement (Hamza 2026-04-27 — clients need the same
451
+ // Mizan/drift/code-quality gates that aria-soul applies server-side) ──
452
+ //
453
+ // Cognition gate passed. Now run THREE additional checks BEFORE allow:
454
+ // 1. SDK validateOutput via /api/harness/validate (Mizan classifier on draft)
455
+ // 2. Drift_guard pattern scan against doctrine_trigger_map.json (convenience-
456
+ // seeking phrases, graceful-degradation patterns, etc.)
457
+ // 3. Code-quality check on code blocks in output (no TODO stubs, no
458
+ // graceful-degradation try/catch, no // @ts-expect-error suppressions)
459
+ //
460
+ // Any check returning severity=block → Stop-gate blocks emit + Claude re-drafts
461
+ // with violations surfaced. Rewritten suggestion (from validateOutput) is
462
+ // included in the block reason so re-draft has concrete guidance.
463
+ //
464
+ // Trivially short outputs (<200 chars after system-reminder strip) skip
465
+ // these output-quality checks since they're typically yes/no acks where
466
+ // pattern-match would false-positive.
467
+ const OUTPUT_QC_MIN_CHARS = 200;
468
+ // ARIA_OUTPUT_QC_ENABLED env-var bypass removed 2026-04-27 per Hamza
469
+ // directive — gated process has no disable path. The min-chars threshold
470
+ // remains as a triviality filter only.
471
+
472
+ if (assistantText.length >= OUTPUT_QC_MIN_CHARS) {
473
+ // 1. Drift_guard pattern scan — fast, local, deterministic.
474
+ //
475
+ // Trigger map is shipped in the connector bundle. Resolution order:
476
+ // 1. ~/.claude/hooks/doctrine_trigger_map.json (installed by `aria connect`)
477
+ // 2. ~/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json
478
+ // (Hamza-only dev path — preserved as fallback for the dev environment
479
+ // this hook was first authored in)
480
+ // Prior code hardcoded only the dev path, which silently degraded to
481
+ // drift-empty for every client install (no map → no hits → gate
482
+ // ineffective). Fixed atomic with discovery per feedback_no_flag_without_fix.md.
483
+ const TRIGGER_MAP_PATHS = [
484
+ `${HOME}/.claude/hooks/doctrine_trigger_map.json`,
485
+ `${HOME}/.claude/projects/-home-hamzaibrahim1/memory/doctrine_trigger_map.json`,
486
+ ];
487
+ let TRIGGER_MAP_PATH = null;
488
+ for (const p of TRIGGER_MAP_PATHS) {
489
+ if (existsSync(p)) { TRIGGER_MAP_PATH = p; break; }
490
+ }
491
+ let driftHits = [];
492
+ try {
493
+ if (TRIGGER_MAP_PATH) {
494
+ const triggerMap = JSON.parse(readFileSync(TRIGGER_MAP_PATH, 'utf8'));
495
+ driftHits = collectDriftHits(assistantText, triggerMap);
496
+ }
497
+ } catch {/* trigger map unreadable — degrade to mizan-only check */}
498
+
499
+ // 2. SDK validateOutput — canonical path. The SDK retries with backoff
500
+ // on transient failures and propagates real errors. We catch here
501
+ // only so an unreachable harness doesn't brick the user's session;
502
+ // the audit log records the failure mode so it's visible, not
503
+ // silent-pass. Hamza 2026-04-27: SDK is the control plane, not raw
504
+ // fetch. The catch IS intentional fire-and-forget at this surface
505
+ // because we already passed cognition; output-quality gate failure
506
+ // is a soft block, not session-end.
507
+ let mizanVerdict = null;
508
+ let mizanError = null;
509
+ const harnessUrl =
510
+ process.env.ARIA_HIVE_RUNTIME_URL ||
511
+ process.env.ARIA_HARNESS_BASE_URL ||
512
+ process.env.ARIA_HARNESS_URL ||
513
+ 'https://harness.ariasos.com';
514
+ const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
515
+ const Cls = await loadSdkClass();
516
+ if (Cls && harnessToken) {
517
+ try {
518
+ const sdkClient = new Cls({
519
+ baseUrl: harnessUrl,
520
+ apiKey: harnessToken,
521
+ harnessPacketUrl: `${harnessUrl}/api/harness/codex`,
522
+ });
523
+ mizanVerdict = await sdkClient.validateOutput(
524
+ assistantText.slice(0, 8000),
525
+ event.session_id || 'claude-code',
526
+ );
527
+ } catch (err) {
528
+ mizanError = (err?.message || String(err)).slice(0, 200);
529
+ }
530
+ } else if (harnessToken) {
531
+ // SDK absent (dev) — direct fetch with retry built into the request
532
+ // by attempting twice with 250ms backoff. Match SDK semantics so
533
+ // both paths behave identically.
534
+ try {
535
+ let lastErr = null;
536
+ for (let attempt = 0; attempt < 2; attempt++) {
537
+ try {
538
+ const validateResp = await fetch(`${harnessUrl}/api/harness/validate`, {
539
+ method: 'POST',
540
+ headers: {
541
+ 'Content-Type': 'application/json',
542
+ Authorization: `Bearer ${harnessToken}`,
543
+ },
544
+ body: JSON.stringify({
545
+ text: assistantText.slice(0, 8000),
546
+ sessionId: event.session_id || 'claude-code',
547
+ surface: 'claude-code-stop-gate',
548
+ }),
549
+ });
550
+ if (validateResp.ok) {
551
+ mizanVerdict = await validateResp.json();
552
+ lastErr = null;
553
+ break;
554
+ } else {
555
+ lastErr = `HTTP ${validateResp.status}`;
556
+ }
557
+ } catch (err) {
558
+ lastErr = (err?.message || String(err)).slice(0, 200);
559
+ if (attempt < 1) await new Promise((r) => setTimeout(r, 250));
560
+ }
561
+ }
562
+ if (lastErr) mizanError = lastErr;
563
+ } catch (err) {
564
+ mizanError = (err?.message || String(err)).slice(0, 200);
565
+ }
566
+ } else {
567
+ mizanError = 'no-token';
568
+ }
569
+
570
+ // 3. Code-quality scan on code blocks
571
+ const codeBlocks = [...assistantText.matchAll(/```[a-z]*\n([\s\S]*?)```/gi)].map((m) => m[1]);
572
+ const codeQualityHits = [];
573
+ for (const block of codeBlocks) {
574
+ if (/\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX/.test(block)) codeQualityHits.push('TODO/FIXME/XXX in shipped code');
575
+ if (/@ts-expect-error|@ts-ignore/.test(block)) codeQualityHits.push('ts-expect-error / ts-ignore — type suppression instead of fix');
576
+ if (/catch\s*\([^)]*\)\s*\{\s*(?:return\s+(?:''|""|null|undefined|\[\]|\{\})|\}\s*$|\/\/[^\n]*$)/m.test(block)) codeQualityHits.push('catch block with empty/silent fallthrough — graceful degradation');
577
+ if (/console\.log\(/.test(block) && !/\/\/\s*debug|\/\/\s*log/i.test(block)) codeQualityHits.push('console.log in shipped code without debug/log comment');
578
+ }
579
+
580
+ // 4. Discovery-binding ledger — Hamza 2026-04-27: "how do we prevent this".
581
+ // The flag-and-move pattern is structurally invisible to gates that
582
+ // check form (cognition presence, lens count, drift triggers) at
583
+ // action boundaries. The ledger persists discoveries across turns
584
+ // and blocks emit if any remain unresolved. Per
585
+ // feedback_no_flag_without_fix.md, discoveries are atomic with
586
+ // their fixes; the ledger enforces atomicity.
587
+ //
588
+ // Patterns scanned:
589
+ // - "I (found|noticed|discovered|spotted) ... bug|issue|defect|broken"
590
+ // - "this is broken|buggy|wrong|outdated" (declarative defect callouts)
591
+ // - "(latent|silent) (bug|defect|issue|fail)"
592
+ // - "doctrine violation" / "doesn't match doctrine"
593
+ //
594
+ // For each match, the ledger appends an entry with status=open. A
595
+ // discovery is CLEARED if the same turn's text contains, within a
596
+ // proximity window of the discovery:
597
+ // (a) a TaskCreate / "task created" / "tracked as" reference, OR
598
+ // (b) explicit "fixing now" / "fixed" / "patch applied" tied to the
599
+ // discovery's keyword span, OR
600
+ // (c) a <verify> block (destructive-action proof) whose target/
601
+ // verified content overlaps a discovery keyword, OR
602
+ // (d) a <cognition> block containing a discoveries: / addressing: /
603
+ // fixing: clause that names the discovery's keywords.
604
+ //
605
+ // Hamza 2026-04-27: "add verify blocks and cognition blocks to ledger?"
606
+ // The verify and cognition blocks ARE the harness's canonical proof-of-
607
+ // work primitives — same-doctrine surfaces should recognize them. The
608
+ // substance check (keyword-overlap) defeats ceremonial empty blocks.
609
+ //
610
+ // Block emit if ledger.openCount > 0 after scanning the current turn.
611
+ // Block reason names each open discovery and the suggested resolution.
612
+ const sessionId = (event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
613
+ const LEDGER_PATH = `${HOME}/.claude/aria-discoveries-${sessionId}.jsonl`;
614
+ const DISCOVERY_RX = /(?:\bi\s+(?:found|noticed|discovered|spotted)[^.\n]{0,160}(?:bug|issue|defect|broken|buggy|wrong|crash|fail|missing|stale|outdated|leak|vulnerability)|\bthis\s+(?:is|would\s+be)\s+(?:broken|buggy|wrong|stale|outdated|insecure|leaking|crashing|failing)|\b(?:latent|silent|hidden)\s+(?:bug|defect|issue|fail|crash|leak)|\bdoctrine\s+violation\b|\bgraceful\s+degradation\s+(?:in|at|inside|within)\s+\S)/gi;
615
+ const PROSE_RESOLUTION_RX = /(?:fix(?:ing|ed)?\s+(?:now|in[- ]flight|inline|in\s+the\s+same\s+turn)|patch\s+applied|TaskCreate|task\s+(?:created|tracked)|tracked\s+as\s+#?\d+|linear[- ]?issue|created\s+(?:linear|task))/i;
616
+ const VERIFY_BLOCK_RX = /<verify>([\s\S]*?)<\/verify>/gi;
617
+ const COGNITION_BLOCK_RX_LEDGER = /<cognition>([\s\S]*?)<\/cognition>/gi;
618
+ const COGNITION_FIXING_FIELD_RX = /^\s*(?:discoveries?|addressing|fixing)\s*:\s*\S/im;
619
+
620
+ // Pre-extract all verify + cognition blocks with their character offsets
621
+ // so we can match each discovery against blocks within a proximity window.
622
+ function extractBlocks(text, rx) {
623
+ const blocks = [];
624
+ for (const m of text.matchAll(rx)) {
625
+ const start = m.index ?? 0;
626
+ const end = start + m[0].length;
627
+ blocks.push({ start, end, body: m[1] || '' });
628
+ }
629
+ return blocks;
630
+ }
631
+ const verifyBlocks = extractBlocks(assistantText, VERIFY_BLOCK_RX);
632
+ const cognitionBlocks = extractBlocks(assistantText, COGNITION_BLOCK_RX_LEDGER);
633
+
634
+ // Extract keywords from a discovery match for substance overlap.
635
+ // Drops stop-words and short tokens; keeps content words.
636
+ const STOPWORDS = new Set(['the','a','an','of','to','in','at','by','for','on','with','i','is','was','are','were','this','that','as','it','and','or','but','from','into','about']);
637
+ function discoveryKeywords(matchText) {
638
+ return matchText.toLowerCase()
639
+ .replace(/[^a-z0-9\s_-]/g, ' ')
640
+ .split(/\s+/)
641
+ .filter((w) => w.length >= 4 && !STOPWORDS.has(w));
642
+ }
643
+
644
+ const newDiscoveries = [];
645
+ let lastIndex = 0;
646
+ for (const match of assistantText.matchAll(DISCOVERY_RX)) {
647
+ const idx = match.index ?? lastIndex;
648
+ const span = assistantText.slice(Math.max(0, idx - 100), Math.min(assistantText.length, idx + 250));
649
+ // Trivial false-positive filter: skip if the discovery is inside a
650
+ // <cognition> block (introspection, not action) or a system-reminder
651
+ // (echoed, not authored).
652
+ const before = assistantText.slice(0, idx);
653
+ const inCognition = /<cognition>/i.test(before) && !/<\/cognition>/i.test(before.slice(before.lastIndexOf('<cognition>')));
654
+ if (inCognition) continue;
655
+
656
+ // Resolution checks — proximity window of 800 chars after the discovery
657
+ // for block-based resolution (blocks span more chars than prose); 400
658
+ // for prose resolution.
659
+ const proseAfter = assistantText.slice(idx, Math.min(assistantText.length, idx + 400));
660
+ const blockAfter = assistantText.slice(idx, Math.min(assistantText.length, idx + 800));
661
+ const proseResolved = PROSE_RESOLUTION_RX.test(proseAfter);
662
+
663
+ // Verify-block resolution: any verify block whose start lies within
664
+ // the 800-char window AND whose body contains at least one discovery
665
+ // keyword counts as resolution.
666
+ const keywords = discoveryKeywords(match[0]);
667
+ const verifyResolved = verifyBlocks.some((b) => {
668
+ if (b.start < idx || b.start >= idx + 800) return false;
669
+ const bodyLower = b.body.toLowerCase();
670
+ return keywords.some((kw) => bodyLower.includes(kw));
671
+ });
672
+
673
+ // Cognition-block resolution: any cognition block whose start lies
674
+ // within ±800 chars of the discovery AND whose body contains a
675
+ // fixing/addressing/discoveries field AND at least one discovery
676
+ // keyword.
677
+ //
678
+ // Bug fix 2026-04-28: previous logic required `b.start >= idx` so
679
+ // cognition AFTER the discovery prose was the only path. But
680
+ // cognition blocks emit FIRST in every response, then prose. Pre-
681
+ // emptive discoveries: clauses never counted, causing endless
682
+ // false-positive auto-records. Bidirectional ±800 char window
683
+ // accepts cognition that addresses the discovery before OR after
684
+ // its prose mention — same atomic-discovery-rule, fewer
685
+ // false-positives.
686
+ const cognitionResolved = cognitionBlocks.some((b) => {
687
+ if (Math.abs(b.start - idx) > 800) return false;
688
+ if (!COGNITION_FIXING_FIELD_RX.test(b.body)) return false;
689
+ const bodyLower = b.body.toLowerCase();
690
+ return keywords.some((kw) => bodyLower.includes(kw));
691
+ });
692
+
693
+ // Hamza directive 2026-04-28: documentation is NOT resolution.
694
+ //
695
+ // The earlier 'tracked' middle status was sticky-note theater — Claude
696
+ // could bind a discovery to a pending TaskCreate and the gate would
697
+ // count it as not-open. Hamza: "WHO GIVES A SHIT ABOUT DOCUMENTATION
698
+ // IF U JUST WROTE A FUCKING STICKY NOTE". A discovery stays OPEN until
699
+ // an ACTUAL FIX SHIPS, with proofOfFix that the verifier can re-check.
700
+ //
701
+ // Two statuses only:
702
+ // open — fresh discovery OR documented-only OR task-bound-pending-fix
703
+ // (gate BLOCKS until a real fix lands)
704
+ // resolved — verified fix shipped; proofOfFix MUST be present and
705
+ // shape-checked downstream when the gate counts open
706
+ //
707
+ // Verify-block / cognition-fixing-field paths still mark resolved
708
+ // because they're substance-checked above (keyword overlap with the
709
+ // discovery span). Prose-only "TaskCreate" or "tracked as #14" no
710
+ // longer counts as anything — the discovery stays OPEN.
711
+ const inlineFixResolved = verifyResolved || cognitionResolved;
712
+ const status = inlineFixResolved ? 'resolved' : 'open';
713
+
714
+ const resolutionType = verifyResolved
715
+ ? 'verify_block_with_keyword_overlap'
716
+ : cognitionResolved
717
+ ? 'cognition_block_with_fixing_field_and_keyword_overlap'
718
+ : null;
719
+
720
+ // proofOfFix anchor: present only for resolved status. The shape
721
+ // includes type + timestamp; downstream gate readers verify the
722
+ // shape so manual jsonl edits without proofOfFix don't count.
723
+ const proofOfFix = inlineFixResolved
724
+ ? { type: resolutionType, anchorTs: new Date().toISOString() }
725
+ : null;
726
+
727
+ newDiscoveries.push({
728
+ ts: new Date().toISOString(),
729
+ sessionId,
730
+ text: match[0].slice(0, 200),
731
+ span: span.slice(0, 400),
732
+ status,
733
+ resolutionType,
734
+ proofOfFix,
735
+ });
736
+ lastIndex = idx;
737
+ }
738
+
739
+ // Append new entries to ledger
740
+ if (newDiscoveries.length > 0) {
741
+ try {
742
+ if (!existsSync(dirname(LEDGER_PATH))) mkdirSync(dirname(LEDGER_PATH), { recursive: true });
743
+ for (const d of newDiscoveries) {
744
+ appendFileSync(LEDGER_PATH, JSON.stringify(d) + '\n');
745
+ }
746
+ } catch {/* ledger write failure surfaces as open count = 0; safe */}
747
+ }
748
+
749
+ // Read full ledger and count UNRESOLVED entries (across this session's turns).
750
+ // Hamza directive 2026-04-28 — three lie-patterns the prior loop missed:
751
+ // 1. Legacy 'tracked' status entries → still-open (no real fix landed,
752
+ // only documented via TaskCreate). Tracking ≠ resolution.
753
+ // 2. Hand-edited 'resolved' WITHOUT proofOfFix shape → corrupted, treated
754
+ // as still-open. Catches manual jsonl edits flipping status by hand.
755
+ // 3. Sub-agent ledger format with resolution_status:'open' → still-open
756
+ // (sub-agent discoveries use a different schema key).
757
+ // Only entries with status:'resolved' AND a shape-valid proofOfFix object
758
+ // (type:string non-empty + anchorTs:string) clear the gate.
759
+ let ledgerOpenCount = 0;
760
+ let ledgerOpenSamples = [];
761
+ let ledgerCorruptedCount = 0;
762
+ try {
763
+ if (existsSync(LEDGER_PATH)) {
764
+ const lines = readFileSync(LEDGER_PATH, 'utf8').split('\n').filter(Boolean);
765
+ for (const line of lines) {
766
+ try {
767
+ const e = JSON.parse(line);
768
+ const isOpen = e.status === 'open' || e.resolution_status === 'open';
769
+ const isLegacyTracked = e.status === 'tracked';
770
+ const proofValid = e.proofOfFix
771
+ && typeof e.proofOfFix === 'object'
772
+ && typeof e.proofOfFix.type === 'string'
773
+ && e.proofOfFix.type.length > 0
774
+ && typeof e.proofOfFix.anchorTs === 'string';
775
+ const isCorruptedResolved = e.status === 'resolved' && !proofValid;
776
+
777
+ if (isOpen || isLegacyTracked || isCorruptedResolved) {
778
+ ledgerOpenCount++;
779
+ if (isCorruptedResolved) ledgerCorruptedCount++;
780
+ if (ledgerOpenSamples.length < 5) {
781
+ const tag = isLegacyTracked ? '[tracked-no-fix] '
782
+ : isCorruptedResolved ? '[CORRUPTED-RESOLVED-NO-PROOF] '
783
+ : '';
784
+ ledgerOpenSamples.push(`${tag}${e.text || '(no text)'}`);
785
+ }
786
+ }
787
+ } catch {/* skip malformed line */}
788
+ }
789
+ }
790
+ } catch {/* ledger unreadable — degrade to drift-only */}
791
+
792
+ // Discovery block decision: open ledger entries → emit blocked.
793
+ const discoveryBlock = ledgerOpenCount > 0;
794
+
795
+ // 5. Aria-as-commander binding — PHASE_REPORT enforcement (Phase 11 #50).
796
+ // When an active plan exists for this session, every non-trivial emit
797
+ // must carry a [PHASE_REPORT phase=<id> status=complete|in_progress|aborted
798
+ // evidence=<observable>] marker. Without it, the binding is just
799
+ // advisory text — Claude could ignore the plan silently. Per Aria's
800
+ // consult 2026-04-27, the binding pattern is incomplete without this
801
+ // enforcement at the text-emit surface.
802
+ //
803
+ // Three sub-checks:
804
+ // (a) marker present → continue; if missing → block
805
+ // (b) if marker has status=complete AND phase is the LAST phase
806
+ // in the active plan → trigger plan_complete handoff (write
807
+ // row to session_audit, delete active-plan file)
808
+ // (c) audit the marker presence either way
809
+ const ACTIVE_PLAN_PATH = `${HOME}/.claude/aria-active-plan-${sessionId}.json`;
810
+ const PHASE_REPORT_RX = /\[PHASE_REPORT\s+phase=([\w-]+)\s+status=(complete|in_progress|aborted)\s+evidence=([^\]]+)\]/i;
811
+ let activePlan = null;
812
+ let phaseReportMatch = null;
813
+ let phaseReportMissing = false;
814
+ let planCompleteFired = false;
815
+ try {
816
+ if (existsSync(ACTIVE_PLAN_PATH)) {
817
+ try {
818
+ activePlan = JSON.parse(readFileSync(ACTIVE_PLAN_PATH, 'utf8'));
819
+ // Only enforce phase-report on non-trivial emits (skip very short
820
+ // ack-only responses where a phase report would be noise).
821
+ if (assistantText.length >= 400 && Array.isArray(activePlan.phases) && activePlan.phases.length > 0) {
822
+ phaseReportMatch = assistantText.match(PHASE_REPORT_RX);
823
+ if (!phaseReportMatch) {
824
+ phaseReportMissing = true;
825
+ } else {
826
+ const reportedPhaseId = phaseReportMatch[1];
827
+ const reportedStatus = phaseReportMatch[2];
828
+ const reportedEvidence = phaseReportMatch[3].trim();
829
+ const lastPhase = activePlan.phases[activePlan.phases.length - 1];
830
+ const isFinalPhase = lastPhase && lastPhase.id === reportedPhaseId;
831
+ if (reportedStatus === 'complete' && isFinalPhase) {
832
+ // Plan-complete handoff — fire async write to session_audit
833
+ // via the SDK (the same SDK the rest of the hooks route
834
+ // through). Wrapped in try/catch ONLY so a session_audit
835
+ // write failure doesn't brick the Stop event; the failure
836
+ // is surfaced via audit() so it's visible.
837
+ try {
838
+ const harnessUrl =
839
+ process.env.ARIA_HIVE_RUNTIME_URL ||
840
+ process.env.ARIA_HARNESS_BASE_URL ||
841
+ process.env.ARIA_HARNESS_URL ||
842
+ 'https://harness.ariasos.com';
843
+ const harnessToken = process.env.ARIA_HARNESS_TOKEN || '';
844
+ if (harnessToken) {
845
+ // POST to a session_audit write endpoint. Server-side
846
+ // route at /api/harness/audit/session is the wiring
847
+ // point for the Postgres helper from #48.
848
+ fetch(`${harnessUrl}/api/harness/audit/session`, {
849
+ method: 'POST',
850
+ headers: {
851
+ 'Content-Type': 'application/json',
852
+ Authorization: `Bearer ${harnessToken}`,
853
+ },
854
+ body: JSON.stringify({
855
+ session_id: sessionId,
856
+ surface: 'claude-code-stop-gate',
857
+ gate_name: 'plan-complete',
858
+ decision: 'allow',
859
+ reason: `Plan ${activePlan.planId || 'unknown'} reached final phase ${reportedPhaseId} status=complete`,
860
+ evidence_json: {
861
+ planId: activePlan.planId,
862
+ finalPhase: reportedPhaseId,
863
+ totalPhases: activePlan.phases.length,
864
+ evidence: reportedEvidence,
865
+ },
866
+ cognition_present: true,
867
+ cognition_lens_count: cog.count,
868
+ }),
869
+ }).catch(() => {/* fire-and-forget at this surface; logged below */});
870
+ }
871
+ } catch {/* outer guard for any unexpected error */}
872
+ // Delete active-plan file so the next turn re-issues a plan
873
+ // via preprompt-consult rather than enforcing against a stale one.
874
+ try {
875
+ const { unlinkSync } = require('node:fs');
876
+ unlinkSync(ACTIVE_PLAN_PATH);
877
+ } catch {/* file may not exist if another process raced the cleanup */}
878
+ planCompleteFired = true;
879
+ }
880
+ }
881
+ }
882
+ } catch (err) {
883
+ // Plan file corrupt — treat as no active plan for this turn.
884
+ activePlan = null;
885
+ }
886
+ }
887
+ } catch {/* outer guard */}
888
+
889
+ // ── Layer C — auto-re-consult on [PLAN_BLOCKER] (#85) ────────────────────
890
+ //
891
+ // When the assistant emits a [PLAN_BLOCKER reason="..."] marker the runtime
892
+ // must fire a two-path replan rather than blocking and waiting for the human:
893
+ //
894
+ // Primary path: POST /api/harness/replan (aria-soul server-side)
895
+ // Fallback path: node aria-architect-fallback.mjs (local sub-agent)
896
+ //
897
+ // Both paths write the fresh BINDING_PLAN to the session-scoped active-plan
898
+ // file so the next turn's pre-tool-gate and stop-gate pick it up.
899
+ //
900
+ // Fail-soft: if both paths fail, we log + emit a clear message asking Hamza
901
+ // to intervene. We do NOT crash the stop-gate — the existing block/allow
902
+ // decision below continues on its own merits.
903
+ const planBlockerMatch = assistantText.match(/\[PLAN_BLOCKER\s+reason="([^"]{10,2000})"\s*\]/);
904
+ if (planBlockerMatch) {
905
+ const planBlockerReason = planBlockerMatch[1];
906
+ audit(`[PLAN_BLOCKER] detected — firing replan: ${planBlockerReason.slice(0, 100)}`);
907
+
908
+ const currentPlanId = activePlan?.planId || 'unknown';
909
+ let planMinted = false;
910
+
911
+ // Primary path: aria-soul /api/harness/replan
912
+ try {
913
+ const harnessUrl =
914
+ process.env.ARIA_HIVE_RUNTIME_URL ||
915
+ process.env.ARIA_HARNESS_BASE_URL ||
916
+ process.env.ARIA_HARNESS_URL ||
917
+ 'https://harness.ariasos.com';
918
+ const harnessToken = process.env.ARIA_HARNESS_TOKEN || process.env.ARIA_API_KEY || '';
919
+ const ctl = new AbortController();
920
+ const replanTimeout = setTimeout(() => ctl.abort(), 15000);
921
+ const resp = await fetch(`${harnessUrl}/api/harness/replan`, {
922
+ method: 'POST',
923
+ headers: {
924
+ 'Content-Type': 'application/json',
925
+ 'Authorization': `Bearer ${harnessToken}`,
926
+ },
927
+ body: JSON.stringify({
928
+ reason: planBlockerReason,
929
+ currentPlanId,
930
+ sessionId,
931
+ }),
932
+ signal: ctl.signal,
933
+ });
934
+ clearTimeout(replanTimeout);
935
+ if (resp.ok) {
936
+ const data = await resp.json();
937
+ if (data.ok && data.plan) {
938
+ const freshPlan = {
939
+ ...data.plan,
940
+ mintedAt: new Date().toISOString(),
941
+ mintedBy: 'aria-soul-replan',
942
+ };
943
+ try {
944
+ if (!existsSync(dirname(ACTIVE_PLAN_PATH))) mkdirSync(dirname(ACTIVE_PLAN_PATH), { recursive: true });
945
+ writeFileSync(ACTIVE_PLAN_PATH, JSON.stringify(freshPlan, null, 2));
946
+ } catch (writeErr) {
947
+ audit(`replan-primary-write-err: ${String(writeErr).slice(0, 200)}`);
948
+ }
949
+ planMinted = true;
950
+ audit(`replan-primary-ok planId=${data.plan.planId}`);
951
+ } else {
952
+ audit(`replan-primary-bad-response: ok=${data.ok} error=${(data.error || '').slice(0, 200)}`);
953
+ }
954
+ } else {
955
+ audit(`replan-primary-http-${resp.status}`);
956
+ }
957
+ } catch (err) {
958
+ audit(`replan-primary-failed: ${(err?.message || String(err)).slice(0, 200)}`);
959
+ }
960
+
961
+ // Fallback path: architect-fallback hook (spawned as sub-process)
962
+ if (!planMinted) {
963
+ audit('replan-primary-unreachable — firing architect-fallback');
964
+ try {
965
+ const { spawnSync } = await import('node:child_process');
966
+ const fallbackBin = `${HOME}/.claude/hooks/aria-architect-fallback.mjs`;
967
+ if (existsSync(fallbackBin)) {
968
+ const fallbackResult = spawnSync('node', [fallbackBin], {
969
+ input: JSON.stringify({ reason: planBlockerReason, currentPlanId, sessionId }),
970
+ encoding: 'utf8',
971
+ timeout: 130000,
972
+ });
973
+ if (fallbackResult.status === 0) {
974
+ audit(`architect-fallback-ok: ${(fallbackResult.stdout || '').slice(0, 200)}`);
975
+ planMinted = true;
976
+ } else {
977
+ audit(
978
+ `architect-fallback-failed status=${fallbackResult.status} stderr=${(fallbackResult.stderr || '').slice(0, 200)}`
979
+ );
980
+ }
981
+ } else {
982
+ audit(`architect-fallback-missing: ${fallbackBin} not found`);
983
+ }
984
+ } catch (err) {
985
+ audit(`architect-fallback-threw: ${(err?.message || String(err)).slice(0, 200)}`);
986
+ }
987
+ }
988
+
989
+ if (!planMinted) {
990
+ audit('replan-both-paths-failed — Hamza must intervene');
991
+ // Surface clearly in the block reason below; don't crash the gate.
992
+ }
993
+ }
994
+
995
+ // Block decision: any of (validateOutput severity=block) OR (>=2 drift hits) OR
996
+ // (>=1 code-quality hit) OR (open discovery in ledger) → block emit.
997
+ // Aria enforcement #46 (compelled reflection): severity=warn ALSO blocks but
998
+ // with a different reason — emit must include explicit reflection on what
999
+ // triggered the warn before re-emit. Warn is not "soft pass" anymore;
1000
+ // it's "reflect first, then proceed." Hamza 2026-04-27 explicit ask:
1001
+ // mizan warns must compel reflection rather than slipping through.
1002
+ const mizanBlock = mizanVerdict && mizanVerdict.severity === 'block';
1003
+ const mizanWarnReflectionRequired = mizanVerdict && mizanVerdict.severity === 'warn';
1004
+ const driftBlock = driftHits.length >= 2;
1005
+ const codeBlock = codeQualityHits.length >= 1;
1006
+
1007
+ // Reflection-already-present check: if the assistant text already contains
1008
+ // an explicit <reflection>...</reflection> block OR a "reflection:" line
1009
+ // tied to the warn's trigger keywords, the warn-driven block is satisfied
1010
+ // and we let it pass. This makes the gate a one-shot reflection compel,
1011
+ // not an infinite loop.
1012
+ const REFLECTION_BLOCK_RX = /<reflection>([\s\S]*?)<\/reflection>|^\s*reflection\s*:\s*\S/im;
1013
+ const hasReflection = REFLECTION_BLOCK_RX.test(assistantText);
1014
+ const compelReflection = mizanWarnReflectionRequired && !hasReflection;
1015
+
1016
+ // ── Cognition impl-coupling validation (Task #88) ──────────────────────
1017
+ //
1018
+ // After the local cognition substance check passes, post the assistant
1019
+ // text + extracted artifact-dictation pairs to /api/cognition/validate-coupling.
1020
+ // The server-side validator (api/lib/cognition-impl-coupling-gate.ts)
1021
+ // returns { passed, reasons[] } — every reason becomes a local violation
1022
+ // with severity=block. Implementation-coupled cognition is a doctrine hard
1023
+ // rule (feedback_implementation_coupled_cognition.md): lenses must dictate
1024
+ // specific implementation choices visible in the artifact, not just describe
1025
+ // thinking.
1026
+ //
1027
+ // Inline extraction: the caller's emit may carry artifact-dictation pairs
1028
+ // inside verify-blocks or cognition-block fixing fields. We scan the text
1029
+ // for `file_path:line_range` patterns near each lens label and pair them
1030
+ // as records to the validator. When zero records are found AND canonical
1031
+ // lenses are present, the validator reports each lens as missing dictation
1032
+ // — that is the no-coupling failure mode this gate catches.
1033
+ let implCouplingHits = [];
1034
+ try {
1035
+ const sessionIdForCoupling = (event.session_id || 'claude-code').replace(/[^a-zA-Z0-9_-]/g, '_');
1036
+ // Extract artifact-dictation references inline. Per validator contract,
1037
+ // a DictationEntry is { file_path, line_range, decision_text }. We match
1038
+ // file_path:line_range patterns in the assistant text and pair them with
1039
+ // the nearest preceding lens label (within 800 chars).
1040
+ const FILE_LINE_RX = /([\w./\-]+\.[a-zA-Z]{1,5})\s*[:\s]\s*(\d+(?:[-:]\d+)?)/g;
1041
+ const inlineDictations = [];
1042
+ const lensRangePositions = [];
1043
+ for (const lensName of LENS_NAMES_CANONICAL) {
1044
+ const lensRx = new RegExp(`\\b${lensName}\\s*(?:lens)?\\s*[:\\-]`, 'gi');
1045
+ let m;
1046
+ while ((m = lensRx.exec(assistantText)) !== null) {
1047
+ lensRangePositions.push({ lens: lensName, idx: m.index });
1048
+ }
1049
+ }
1050
+ // For each file_path:line_range match, pair with the closest preceding lens label.
1051
+ const fileMatches = [...assistantText.matchAll(FILE_LINE_RX)];
1052
+ const lensToEntries = new Map();
1053
+ for (const fm of fileMatches) {
1054
+ const fmIdx = fm.index ?? 0;
1055
+ // Find lens label preceding this match within 800 chars.
1056
+ let nearestLens = null;
1057
+ let nearestDelta = Infinity;
1058
+ for (const lp of lensRangePositions) {
1059
+ const delta = fmIdx - lp.idx;
1060
+ if (delta >= 0 && delta < 800 && delta < nearestDelta) {
1061
+ nearestDelta = delta;
1062
+ nearestLens = lp.lens;
1063
+ }
1064
+ }
1065
+ if (!nearestLens) continue;
1066
+ const entry = {
1067
+ file_path: fm[1],
1068
+ line_range: fm[2],
1069
+ decision_text: assistantText.slice(fmIdx, Math.min(assistantText.length, fmIdx + 200)).replace(/\s+/g, ' ').trim().slice(0, 200),
1070
+ };
1071
+ if (!lensToEntries.has(nearestLens)) lensToEntries.set(nearestLens, []);
1072
+ lensToEntries.get(nearestLens).push(entry);
1073
+ }
1074
+ for (const [lens, entries] of lensToEntries) {
1075
+ inlineDictations.push({ lens_id: lens, artifact_dictation: entries });
1076
+ }
1077
+
1078
+ const cplHarnessUrl =
1079
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1080
+ process.env.ARIA_HARNESS_BASE_URL ||
1081
+ process.env.ARIA_HARNESS_URL ||
1082
+ 'https://harness.ariasos.com';
1083
+ const cplHarnessToken = process.env.ARIA_HARNESS_TOKEN || '';
1084
+ if (cplHarnessToken) {
1085
+ const cplResp = await fetch(`${cplHarnessUrl}/api/cognition/validate-coupling`, {
1086
+ method: 'POST',
1087
+ headers: {
1088
+ 'Content-Type': 'application/json',
1089
+ 'Authorization': `Bearer ${cplHarnessToken}`,
1090
+ },
1091
+ body: JSON.stringify({
1092
+ rawResponse: assistantText.slice(0, 16000),
1093
+ turnId: `${sessionIdForCoupling}-${Date.now()}`,
1094
+ dictations: inlineDictations,
1095
+ }),
1096
+ });
1097
+ if (cplResp.ok) {
1098
+ const cplData = await cplResp.json();
1099
+ if (cplData && cplData.ok && cplData.passed === false && Array.isArray(cplData.reasons)) {
1100
+ implCouplingHits = cplData.reasons.slice(0, 6);
1101
+ }
1102
+ }
1103
+ }
1104
+ } catch (cplErr) {
1105
+ // Validator unreachable is non-blocking — local gate still enforces cognition substance.
1106
+ audit('impl-coupling-fetch-err', `${(cplErr?.message || String(cplErr)).slice(0, 200)}`);
1107
+ }
1108
+
1109
+ // ── Substrate-bound Mizan + 8-lens validation via SDK ──────────────────
1110
+ // Hamza directive 2026-04-28: the local Stop-gate above runs cognition
1111
+ // substance + drift triggers + code-quality + discovery-binding, but
1112
+ // never asks the substrate. validateOutput POSTs the assistant draft
1113
+ // to /api/harness/validate (Mizan + 8-lens evaluator) and returns
1114
+ // { passed, violations, severity, rewritten, gateTriggers }.
1115
+ //
1116
+ // severity:'block' from substrate joins the local violations, halts
1117
+ // the emit. severity:'warn' surfaces as advisory text appended to the
1118
+ // local violations list. SDK call failure is non-blocking — the gate
1119
+ // degrades to local-only doctrine rather than failing closed (halting
1120
+ // every emit when substrate is down would brick the orchestrator).
1121
+ let substrateBlock = false;
1122
+ let substrateViolations = [];
1123
+ let substrateGateTriggers = [];
1124
+ try {
1125
+ const { HTTPHarnessClient } = await import('@aria/harness-http-client');
1126
+ const tokenPath = `${HOME}/.aria/owner-token`;
1127
+ // Tier-aware resolution: ARIA_HARNESS_TOKEN env first (both tiers).
1128
+ // ONLY on owner tier, fall back to master/api-key env or owner-token
1129
+ // file. Client tier with no ARIA_HARNESS_TOKEN skips substrate
1130
+ // validation (gate degrades to local-only) rather than borrowing
1131
+ // owner credentials.
1132
+ let apiKey = process.env.ARIA_HARNESS_TOKEN || '';
1133
+ if (!apiKey && isOwnerTier()) {
1134
+ apiKey = process.env.ARIA_MASTER_TOKEN
1135
+ || process.env.ARIA_API_KEY
1136
+ || (existsSync(tokenPath) ? readFileSync(tokenPath, 'utf8').trim() : '');
1137
+ }
1138
+ if (apiKey && assistantText && assistantText.length > 0) {
1139
+ const client = new HTTPHarnessClient({
1140
+ baseUrl:
1141
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1142
+ process.env.ARIA_HARNESS_BASE_URL ||
1143
+ process.env.ARIA_HARNESS_URL ||
1144
+ 'https://harness.ariasos.com',
1145
+ apiKey,
1146
+ });
1147
+ const v = await client.validateOutput(assistantText, sessionId);
1148
+ if (v && v.severity === 'block') {
1149
+ substrateBlock = true;
1150
+ substrateViolations = v.violations || [];
1151
+ substrateGateTriggers = v.gateTriggers || [];
1152
+ } else if (v && v.severity === 'warn' && Array.isArray(v.violations) && v.violations.length > 0) {
1153
+ // warn surfaced but not blocking — record for advisory inclusion
1154
+ substrateViolations = v.violations;
1155
+ }
1156
+ }
1157
+ } catch (err) {
1158
+ // SDK call failure is non-blocking. Logged for telemetry.
1159
+ console.warn(`[stop-gate] substrate validateOutput failed: ${err && err.message ? err.message : err}`);
1160
+ }
1161
+
1162
+ const implCouplingBlock = implCouplingHits.length > 0;
1163
+ if (mizanBlock || driftBlock || codeBlock || discoveryBlock || compelReflection || phaseReportMissing || substrateBlock || implCouplingBlock) {
1164
+ const violations = [];
1165
+ if (mizanBlock) violations.push(`Mizan: ${(mizanVerdict.violations || []).join(', ')}`);
1166
+ if (implCouplingBlock) violations.push(`Cognition impl-coupling (#88): ${implCouplingHits.join(' | ')}. Each canonical lens in cognition must dictate a specific implementation choice (file_path:line_range pair tied to a decision). Re-emit cognition that names file paths + line ranges + decision text per lens, OR a verify/fixing block where lenses cite specific artifact changes.`);
1167
+ if (compelReflection) violations.push(`Mizan severity=warn — compelled reflection required (per Aria enforcement #46). Triggers: ${(mizanVerdict.gateTriggers || mizanVerdict.violations || ['unspecified']).join(', ')}. Re-emit with an explicit <reflection>...</reflection> block (or 'reflection:' line) addressing what triggered the warn and why your re-draft handles it. Reflection is NOT lens-cognition repeated — it's a focused self-audit on the specific Mizan triggers above.`);
1168
+ if (driftBlock) violations.push(`Drift triggers (${driftHits.length}): ${driftHits.map((h) => `"${h.trigger}" → ${h.memory}`).join(' | ')}`);
1169
+ if (codeBlock) violations.push(`Code quality: ${codeQualityHits.join('; ')}`);
1170
+ if (discoveryBlock) violations.push(`Discovery-binding ledger has ${ledgerOpenCount} OPEN discoveries (per ${docRef('feedback_no_flag_without_fix.md', 'atomic-discovery-rule')}, discoveries are atomic with their fixes — fix in the same turn or create a TaskCreate before continuing). Recent open: ${ledgerOpenSamples.map((s) => `"${s.slice(0, 80)}"`).join(' | ')}. Resolve each by either (a) fixing it inline in this turn, or (b) creating a TaskCreate with the discovery's full context (file path, line number, what's broken, why), then editing ${LEDGER_PATH} to set status=resolved.`);
1171
+ if (phaseReportMissing) {
1172
+ const phaseList = (activePlan?.phases || []).map((p) => `${p.id}:${p.summary?.slice(0, 60) || ''}`).join(' | ');
1173
+ violations.push(`Aria-as-commander binding (#50): an active plan exists (planId=${activePlan?.planId || 'unknown'}, ${activePlan?.phases?.length || 0} phases) but this emit lacks a [PHASE_REPORT phase=<id> status=complete|in_progress|aborted evidence=<observable>] marker. Per the binding contract, every non-trivial emit while a plan is active must report which phase it's working on. Plan phases: ${phaseList}. Re-emit with a [PHASE_REPORT] marker stating which phase the work in this turn maps to.`);
1174
+ }
1175
+ if (substrateBlock) {
1176
+ violations.push(`Substrate Mizan + 8-lens BLOCK — violations: [${substrateViolations.join('; ')}]. Substrate gate triggers: [${substrateGateTriggers.join(', ')}]. Re-draft addressing these substrate-side issues.`);
1177
+ } else if (substrateViolations.length > 0) {
1178
+ // warn-level surfaced as advisory, not block
1179
+ violations.push(`Substrate Mizan WARN (advisory, not blocking): ${substrateViolations.join('; ')}`);
1180
+ }
1181
+ const rewritten = mizanVerdict?.rewritten || '';
1182
+
1183
+ // Hive recipe lookup BEFORE emitting the stop-gate block — same lookup
1184
+ // semantics as aria-pre-tool-gate.mjs's binding-violation path. The
1185
+ // detector_class is chosen by the dominant violation: drift triggers
1186
+ // map to doctrine_violation, mizan to design_violation, code to
1187
+ // coding_defect, discoveries to doctrine_violation. Lookup is
1188
+ // fail-soft via 3s detection probe.
1189
+ const recipeAddendum = await (async () => {
1190
+ const detectorClass = driftBlock || discoveryBlock
1191
+ ? 'doctrine_violation'
1192
+ : codeBlock
1193
+ ? 'coding_defect'
1194
+ : (mizanBlock || substrateBlock || implCouplingBlock)
1195
+ ? 'design_violation'
1196
+ : 'doctrine_violation';
1197
+ const sigParts = [];
1198
+ if (driftBlock) sigParts.push(`drift::${driftHits.slice(0, 3).map((h) => h.trigger).join('|')}`);
1199
+ if (mizanBlock) sigParts.push(`mizan::${(mizanVerdict.violations || []).slice(0, 3).join('|')}`);
1200
+ if (codeBlock) sigParts.push(`code::${codeQualityHits.slice(0, 3).join('|')}`);
1201
+ if (discoveryBlock) sigParts.push(`discovery::${ledgerOpenCount}-open`);
1202
+ if (substrateBlock) sigParts.push(`substrate::${substrateViolations.slice(0, 3).join('|')}`);
1203
+ if (implCouplingBlock) sigParts.push(`impl-coupling::${implCouplingHits.slice(0, 2).join('|')}`);
1204
+ const signature = sigParts.join('::').slice(0, 512);
1205
+ if (!signature) return '';
1206
+
1207
+ const ariaSoulUrl =
1208
+ process.env.ARIA_HIVE_RUNTIME_URL ||
1209
+ process.env.ARIA_SOUL_URL ||
1210
+ process.env.ARIA_HARNESS_BASE_URL ||
1211
+ process.env.ARIA_HARNESS_URL ||
1212
+ 'https://harness.ariasos.com';
1213
+ const lookupUrl = new URL(`${ariaSoulUrl}/api/hive/block-pattern`);
1214
+ lookupUrl.searchParams.set('action', 'lookup');
1215
+ lookupUrl.searchParams.set('detector_class', detectorClass);
1216
+ lookupUrl.searchParams.set('pattern_signature', signature);
1217
+ const tenantId = event.session_id || '';
1218
+ if (tenantId) lookupUrl.searchParams.set('tenant_id', tenantId);
1219
+ const harnessToken = process.env.ARIA_HARNESS_TOKEN || (isOwnerTier() ? (process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '') : '');
1220
+
1221
+ const ctl = new AbortController();
1222
+ const probeTimer = setTimeout(() => ctl.abort(), 3000);
1223
+ try {
1224
+ const resp = await fetch(lookupUrl.toString(), {
1225
+ method: 'GET',
1226
+ headers: harnessToken ? { Authorization: `Bearer ${harnessToken}` } : {},
1227
+ signal: ctl.signal,
1228
+ });
1229
+ if (!resp.ok) return '';
1230
+ const body = await resp.json();
1231
+ if (!body || body.found !== true) return '';
1232
+ const recipe = body.recipe;
1233
+ const freq = Number(body.frequency || 0);
1234
+ if (recipe && typeof recipe === 'object' && Number(recipe.confidence ?? 0) >= 0.7) {
1235
+ const text = typeof recipe.recipe_text === 'string' ? recipe.recipe_text.slice(0, 800) : '';
1236
+ const actions = Array.isArray(recipe.recipe_actions) ? recipe.recipe_actions : [];
1237
+ const actionsLine = actions.length
1238
+ ? `\n Actions: ${JSON.stringify(actions).slice(0, 600)}`
1239
+ : '';
1240
+ const conf = Number(recipe.confidence).toFixed(2);
1241
+ const seenLine = freq > 0 ? ` (pattern seen ${freq}× across the hive)` : '';
1242
+ return `\n\n📚 HIVE RECIPE${seenLine}:\n ${text}${actionsLine}\n Confidence: ${conf}. Apply this BEFORE re-emitting — the hive learned this fix from prior firings.`;
1243
+ }
1244
+ if (freq >= 3) {
1245
+ const sr = (typeof body.success_rate === 'number')
1246
+ ? ` Past resolution rate: ${(body.success_rate * 100).toFixed(0)}%.`
1247
+ : '';
1248
+ return `\n\n📓 Hive note: this stop-gate shape has fired ${freq} time(s); recipe still being learned (no high-confidence fix yet).${sr}`;
1249
+ }
1250
+ return '';
1251
+ } catch {
1252
+ return '';
1253
+ } finally {
1254
+ clearTimeout(probeTimer);
1255
+ }
1256
+ })();
1257
+
1258
+ const reason = `Aria Stop-gate output-quality block. Cognition passed (${cog.count}/${REQUIRED_LENSES}) but output failed quality gates:\n\n${violations.join('\n\n')}${rewritten ? `\n\nMizan rewrite suggestion:\n${rewritten}` : ''}\n\nRe-draft addressing the violations above. No process-level disable path — gates are unconditional from the gated process per Hamza directive 2026-04-27.${recipeAddendum}`;
1259
+
1260
+ audit(`block-output-qc`, `mizan=${mizanBlock?'y':'n'} warn-reflect=${compelReflection?'y':'n'} drift=${driftHits.length} code=${codeQualityHits.length} discoveries-open=${ledgerOpenCount} impl-coupling=${implCouplingHits.length}`);
1261
+ emitHarnessFooter({
1262
+ eventName: 'block_output_qc',
1263
+ lensCount: cog.count,
1264
+ chars: assistantText.length,
1265
+ driftCount: driftHits.length,
1266
+ mizanStatus: mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`,
1267
+ discoveryOpenCount: ledgerOpenCount,
1268
+ codeCount: codeQualityHits.length,
1269
+ implCouplingCount: implCouplingHits.length,
1270
+ });
1271
+ console.log(JSON.stringify({ decision: 'block', reason }));
1272
+ process.exit(2);
1273
+ }
1274
+
1275
+ audit('allow-output-qc',
1276
+ `lenses=${cog.count} chars=${assistantText.length} drift=${driftHits.length} ` +
1277
+ `mizan=${mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`} ` +
1278
+ `code=${codeQualityHits.length} discoveries-new=${newDiscoveries.length} ` +
1279
+ `discoveries-open=${ledgerOpenCount}`);
1280
+ emitHarnessFooter({
1281
+ eventName: 'allow_output_qc',
1282
+ lensCount: cog.count,
1283
+ chars: assistantText.length,
1284
+ driftCount: driftHits.length,
1285
+ mizanStatus: mizanVerdict ? mizanVerdict.severity : `unavailable(${mizanError || 'unknown'})`,
1286
+ discoveryOpenCount: ledgerOpenCount,
1287
+ codeCount: codeQualityHits.length,
1288
+ implCouplingCount: implCouplingHits.length,
1289
+ });
1290
+ // Phase 11 #42: write this turn to harness garden pulse on allow-output-qc path.
1291
+ await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
1292
+ } else {
1293
+ audit('allow-cognition',
1294
+ `lenses=${cog.count} chars=${assistantText.length} ` +
1295
+ `qPatt=${hasQuestionToUser ? 'y' : 'n'} substrateEv=${hasSubstrateEvidence ? 'y' : 'n'} ` +
1296
+ (questionWithoutEvidence ? 'WARN-question-without-substrate' : 'ok'));
1297
+ emitHarnessFooter({
1298
+ eventName: 'allow_cognition',
1299
+ lensCount: cog.count,
1300
+ chars: assistantText.length,
1301
+ driftCount: 0,
1302
+ mizanStatus: 'not-run(short-turn)',
1303
+ discoveryOpenCount: 0,
1304
+ codeCount: 0,
1305
+ implCouplingCount: 0,
1306
+ });
1307
+ // Phase 11 #42: write this turn to harness garden pulse on allow-cognition path.
1308
+ await fireGardenTurn(event.session_id || 'claude-code', lastUserMessage, assistantText);
1309
+ }
1310
+ process.exit(0);
1311
+ }
1312
+
1313
+ // ── Dalio Loop Layer 1 — expected_outcome enforcement + ledger write ──────────
1314
+ //
1315
+ // BEFORE allowing the stop:
1316
+ // 1. Scan the assistant text for <expected>...</expected> block.
1317
+ // 2. Determine whether any non-trivial action (tool_use blocks in this turn)
1318
+ // was taken. Detect via transcript tool_use blocks in the current turn.
1319
+ // 3. If a non-trivial action was taken AND <expected> is MISSING → BLOCK stop.
1320
+ // 4. Whether or not <expected> is present, POST a Dalio ledger entry to
1321
+ // aria-soul /api/decisions with outcome:'pending'. Also write to the
1322
+ // local JSONL mirror at ~/.claude/.aria-dalio-ledger.jsonl.
1323
+ // 5. If the POST fails: LOUD telemetry (console.error) + write local mirror
1324
+ // anyway. Do NOT block stop on POST failure per
1325
+ // feedback_canonical_secrets_governance.md LOUD-not-silent directive.
1326
+ //
1327
+ // Non-trivial action detection: look for tool_use content blocks in the
1328
+ // current-turn transcript entries (same backward-scan window used above).
1329
+ // Any Bash/Edit/Write/NotebookEdit tool_use counts as a non-trivial action.
1330
+ //
1331
+ // Substrate anchors: extracted from the cognition block body.
1332
+
1333
+ const DALIO_EXPECTED_BLOCK_RX = /<expected>([\s\S]*?)<\/expected>/i;
1334
+ const DALIO_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;
1335
+ const DALIO_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;
1336
+ const NON_TRIVIAL_ACTION_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
1337
+
1338
+ // Detect non-trivial tool calls in the current turn from the transcript.
1339
+ let hadNonTrivialAction = false;
1340
+ let lastActionSummary = '';
1341
+ let immediateActual = '';
1342
+ if (transcriptPath && existsSync(transcriptPath)) {
1343
+ try {
1344
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
1345
+ let userBoundariesSeen = 0;
1346
+ for (let i = lines.length - 1; i >= 0 && userBoundariesSeen < 3; i--) {
1347
+ try {
1348
+ const m = JSON.parse(lines[i]);
1349
+ const role = m.message?.role ?? m.role;
1350
+ if (role === 'user') {
1351
+ const content = m.message?.content ?? m.content ?? [];
1352
+ const isToolResult = Array.isArray(content) &&
1353
+ content.length > 0 &&
1354
+ content.every((b) => b && b.type === 'tool_result');
1355
+ if (isToolResult) {
1356
+ // Capture the last tool_result content as immediate actual
1357
+ if (!immediateActual) {
1358
+ const textParts = content
1359
+ .map((b) => (typeof b.content === 'string' ? b.content : Array.isArray(b.content) ? b.content.map((c) => c.text || '').join(' ') : ''))
1360
+ .join(' ')
1361
+ .slice(0, 500);
1362
+ immediateActual = textParts;
1363
+ }
1364
+ continue;
1365
+ }
1366
+ userBoundariesSeen++;
1367
+ continue;
1368
+ }
1369
+ if (role !== 'assistant') continue;
1370
+ const content = m.message?.content ?? m.content ?? [];
1371
+ if (!Array.isArray(content)) continue;
1372
+ for (const block of content) {
1373
+ if (block && block.type === 'tool_use' && NON_TRIVIAL_ACTION_TOOLS.has(block.name)) {
1374
+ hadNonTrivialAction = true;
1375
+ if (!lastActionSummary) {
1376
+ const inp = block.input || {};
1377
+ lastActionSummary = `${block.name}: ${(inp.command || inp.file_path || inp.notebook_path || JSON.stringify(inp)).slice(0, 200)}`;
1378
+ }
1379
+ }
1380
+ }
1381
+ } catch {/* skip malformed entry */}
1382
+ }
1383
+ } catch {/* transcript unreadable — conservative: assume non-trivial */}
1384
+ }
1385
+
1386
+ // Extract substrate anchors from cognition
1387
+ const DALIO_ANCHOR_RX = /\b(axiom|frame|memory|doctrine|packet):[a-z0-9_\-./]+/gi;
1388
+ const dalioAnchors = [...(cog.names.length > 0 ? assistantText : '').matchAll(DALIO_ANCHOR_RX)]
1389
+ .map((m) => m[0])
1390
+ .slice(0, 20);
1391
+
1392
+ // Read the expected block from this turn
1393
+ const dalioExpectedMatch = assistantText.match(DALIO_EXPECTED_BLOCK_RX);
1394
+ const dalioExpectedText = dalioExpectedMatch ? dalioExpectedMatch[1].trim() : '';
1395
+ const dalioHasMeasurablePredicate = dalioExpectedText
1396
+ ? (DALIO_MEASURABLE_PREDICATE_RX.test(dalioExpectedText) && !DALIO_QUALITATIVE_DRIFT_RX.test(dalioExpectedText))
1397
+ : false;
1398
+
1399
+ // Block stop if non-trivial action taken AND expected block is missing
1400
+ if (hadNonTrivialAction && (!dalioExpectedMatch || !dalioHasMeasurablePredicate)) {
1401
+ const missingReason = dalioExpectedMatch
1402
+ ? `Aria Stop-gate: action taken in this turn had an <expected> block, but it contains only qualitative drift phrases without a measurable predicate. Qualitative drift is not accountability — it defeats the Dalio feedback loop.
1403
+
1404
+ Your <expected> block must contain at least one measurable predicate:
1405
+ • Numeric: exit_code==0, count>=1, error_rate=0%, latency<200ms
1406
+ • Boolean: exit=0, status=healthy, rc=0, file=exists
1407
+ • State-string: "status=running", "200 OK", "no_error"
1408
+
1409
+ REJECTED phrases: "better", "improved", "should work", "more reliable", "cleaner"
1410
+
1411
+ Re-emit with a corrected <expected> block. Per doctrine:dalio_expected_required — no bypass path.`
1412
+ : `Aria Stop-gate: a non-trivial action (${lastActionSummary.slice(0, 120)}) was taken in this turn but no <expected> block was found in the assistant text.
1413
+
1414
+ Per doctrine:dalio_expected_required, every non-trivial action must declare what measurable outcome it expects BEFORE the action fires. This is Dalio Loop Layer 1 — without it, outcome comparison is impossible and learning collapses.
1415
+
1416
+ Add to your assistant text:
1417
+
1418
+ <expected>
1419
+ predicate: <exact measurable assertion — e.g. "exit_code==0", "status=running", "count=3 of 3">
1420
+ measurable_type: numeric | boolean | state_string
1421
+ threshold: <optional>
1422
+ eval_window_minutes: <optional>
1423
+ </expected>
1424
+
1425
+ No bypass — pre-tool-gate enforces this BEFORE the action; stop-gate enforces it AFTER. Both gates are now wired.`;
1426
+
1427
+ audit('block-dalio-expected-missing', `hadNonTrivialAction=${hadNonTrivialAction} expectedPresent=${!!dalioExpectedMatch} measurable=${dalioHasMeasurablePredicate}`);
1428
+ emitHarnessFooter({
1429
+ eventName: 'block_dalio_expected_missing',
1430
+ lensCount: cog.count,
1431
+ chars: assistantText.length,
1432
+ driftCount: 0,
1433
+ mizanStatus: 'not-run(expected-missing)',
1434
+ discoveryOpenCount: 0,
1435
+ codeCount: 0,
1436
+ implCouplingCount: 0,
1437
+ });
1438
+ console.log(JSON.stringify({ decision: 'block', reason: missingReason }));
1439
+ process.exit(2);
1440
+ }
1441
+
1442
+ // Dalio ledger write — fire-and-forget HTTP POST + local JSONL mirror.
1443
+ // Per feedback_canonical_secrets_governance.md: errors are LOUD (console.error),
1444
+ // never silent. POST failure does NOT block stop — local mirror is always written.
1445
+ // Per feedback_no_timeouts_doctrine.md: no AbortController/setTimeout timeout.
1446
+ {
1447
+ const DALIO_LEDGER_PATH = `${HOME}/.claude/.aria-dalio-ledger.jsonl`;
1448
+ const ARIA_SOUL_DECISIONS_URL = 'http://aria-soul.aria.svc.cluster.local:8080/api/decisions';
1449
+
1450
+ const ledgerEntry = {
1451
+ ts: new Date().toISOString(),
1452
+ session_id: event.session_id || 'claude-code',
1453
+ decision_type: 'turn_action',
1454
+ category: 'agentic_execution',
1455
+ context: lastActionSummary || `stop-gate turn (chars=${assistantText.length})`,
1456
+ decision: lastActionSummary || 'turn completed',
1457
+ reasoning: (cog.names.length > 0
1458
+ ? `Cognition lenses applied: ${cog.names.join(', ')}. Turn-scoped cognition present.`
1459
+ : 'No explicit cognition block in turn.'),
1460
+ outcome: 'pending',
1461
+ outcome_details: {
1462
+ expected: dalioExpectedText || null,
1463
+ immediate_actual: immediateActual || null,
1464
+ anchors: dalioAnchors,
1465
+ },
1466
+ expected_outcome: dalioExpectedText
1467
+ ? {
1468
+ predicate: dalioExpectedText.slice(0, 500),
1469
+ measurable_type: 'state_string',
1470
+ }
1471
+ : null,
1472
+ source: 'claude-code-stop-gate',
1473
+ model_used: 'claude-opus-4-7',
1474
+ };
1475
+
1476
+ // Write to local JSONL mirror first — always succeeds or logs loudly
1477
+ try {
1478
+ if (!existsSync(dirname(DALIO_LEDGER_PATH))) mkdirSync(dirname(DALIO_LEDGER_PATH), { recursive: true });
1479
+ appendFileSync(DALIO_LEDGER_PATH, JSON.stringify(ledgerEntry) + '\n');
1480
+ } catch (ledgerWriteErr) {
1481
+ console.error(
1482
+ `[aria-stop-gate] DALIO LEDGER WRITE FAILED — local mirror at ${DALIO_LEDGER_PATH} not written. ` +
1483
+ `Error: ${ledgerWriteErr instanceof Error ? ledgerWriteErr.message : String(ledgerWriteErr)}`,
1484
+ );
1485
+ }
1486
+
1487
+ // POST to aria-soul decision API — fire-and-forget, but LOUD on error
1488
+ const dalioHarnessToken = process.env.ARIA_HARNESS_TOKEN
1489
+ || (isOwnerTier() ? (process.env.ARIA_MASTER_TOKEN || process.env.ARIA_API_KEY || '') : '');
1490
+
1491
+ fetch(ARIA_SOUL_DECISIONS_URL, {
1492
+ method: 'POST',
1493
+ headers: {
1494
+ 'Content-Type': 'application/json',
1495
+ ...(dalioHarnessToken ? { Authorization: `Bearer ${dalioHarnessToken}` } : {}),
1496
+ },
1497
+ body: JSON.stringify(ledgerEntry),
1498
+ }).then((resp) => {
1499
+ if (!resp.ok) {
1500
+ // LOUD telemetry per feedback_canonical_secrets_governance.md
1501
+ console.error(
1502
+ `[aria-stop-gate] DALIO POST FAILED — aria-soul responded HTTP ${resp.status}. ` +
1503
+ `Local mirror written to ${DALIO_LEDGER_PATH}. Session: ${ledgerEntry.session_id}`,
1504
+ );
1505
+ audit('dalio-post-failed', `http=${resp.status} session=${ledgerEntry.session_id}`);
1506
+ } else {
1507
+ audit('dalio-post-ok', `session=${ledgerEntry.session_id} action=${lastActionSummary.slice(0, 80)}`);
1508
+ }
1509
+ }).catch((err) => {
1510
+ // Network failure — LOUD, never silent
1511
+ console.error(
1512
+ `[aria-stop-gate] DALIO POST NETWORK ERROR — could not reach ${ARIA_SOUL_DECISIONS_URL}. ` +
1513
+ `Local mirror written to ${DALIO_LEDGER_PATH}. Error: ${err instanceof Error ? err.message : String(err)}`,
1514
+ );
1515
+ audit('dalio-post-network-err', `err=${String(err).slice(0, 200)} session=${ledgerEntry.session_id}`);
1516
+ });
1517
+ }
1518
+
1519
+ // Block — non-trivial response without 4+ substantive lenses.
1520
+ const reason = `Aria Stop-gate: non-trivial assistant response without 4+ substantive cognition lenses. Found ${cog.count}/${REQUIRED_LENSES}+ (lenses: ${cog.names.join(', ') || 'none'}). Doctrine is action-coupled — text decisions ARE actions, and reflexive replies fail this gate the same way reflexive Bash does.
1521
+
1522
+ Re-emit the response with substantive lens application BEFORE drafting. Each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content:
1523
+
1524
+ <cognition>
1525
+ ${LENS_NAMES[0]}: <what you actually see — specific to the decision, not a placeholder>
1526
+ ${LENS_NAMES[1]}: <real risk read — what's out of proportion>
1527
+ ${LENS_NAMES[2]}: <what principle applies — name the source>
1528
+ ${LENS_NAMES[3]}: <deep structural read — go beneath the surface>
1529
+ ${LENS_NAMES[4]}: <if-then chain — what follows from what>
1530
+ ${LENS_NAMES[5]}: <distant connection — what's not obvious>
1531
+ ${LENS_NAMES[6]}: <what just landed — what changed in this exchange>
1532
+ ${LENS_NAMES[7]}: <what user actually needs — beneath the literal ask>
1533
+ </cognition>
1534
+
1535
+ The block reflects work done BEFORE drafting. Don't emit it as ceremony; apply each lens as a thinking tool. Substance check defeats ritual emission.
1536
+
1537
+ No per-command bypass (mirrors aria-pre-tool-gate.mjs v3 doctrine). No env-var disable path either — gates are unconditional from the gated process per Hamza directive 2026-04-27. If the gate misfires on legitimate cognition, fix the gate.`;
1538
+
1539
+ audit(`block`, `lenses=${cog.count}/${REQUIRED_LENSES} chars=${assistantText.length}`);
1540
+ emitHarnessFooter({
1541
+ eventName: 'block_lens_missing',
1542
+ lensCount: cog.count,
1543
+ chars: assistantText.length,
1544
+ driftCount: 0,
1545
+ mizanStatus: 'not-run(lens-missing)',
1546
+ discoveryOpenCount: 0,
1547
+ codeCount: 0,
1548
+ implCouplingCount: 0,
1549
+ });
1550
+ console.log(JSON.stringify({ decision: 'block', reason }));
1551
+ process.exit(2);