@aria_asi/cli 0.2.0

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 (153) hide show
  1. package/bin/aria.js +168 -0
  2. package/dist/aria-connector/src/auth-commands.d.ts +28 -0
  3. package/dist/aria-connector/src/auth-commands.d.ts.map +1 -0
  4. package/dist/aria-connector/src/auth-commands.js +129 -0
  5. package/dist/aria-connector/src/auth-commands.js.map +1 -0
  6. package/dist/aria-connector/src/auth.d.ts +12 -0
  7. package/dist/aria-connector/src/auth.d.ts.map +1 -0
  8. package/dist/aria-connector/src/auth.js +31 -0
  9. package/dist/aria-connector/src/auth.js.map +1 -0
  10. package/dist/aria-connector/src/auto-mcp.d.ts +23 -0
  11. package/dist/aria-connector/src/auto-mcp.d.ts.map +1 -0
  12. package/dist/aria-connector/src/auto-mcp.js +994 -0
  13. package/dist/aria-connector/src/auto-mcp.js.map +1 -0
  14. package/dist/aria-connector/src/chat.d.ts +21 -0
  15. package/dist/aria-connector/src/chat.d.ts.map +1 -0
  16. package/dist/aria-connector/src/chat.js +332 -0
  17. package/dist/aria-connector/src/chat.js.map +1 -0
  18. package/dist/aria-connector/src/codebase-scanner.d.ts +7 -0
  19. package/dist/aria-connector/src/codebase-scanner.d.ts.map +1 -0
  20. package/dist/aria-connector/src/codebase-scanner.js +6 -0
  21. package/dist/aria-connector/src/codebase-scanner.js.map +1 -0
  22. package/dist/aria-connector/src/cognition-log.d.ts +17 -0
  23. package/dist/aria-connector/src/cognition-log.d.ts.map +1 -0
  24. package/dist/aria-connector/src/cognition-log.js +19 -0
  25. package/dist/aria-connector/src/cognition-log.js.map +1 -0
  26. package/dist/aria-connector/src/config.d.ts +41 -0
  27. package/dist/aria-connector/src/config.d.ts.map +1 -0
  28. package/dist/aria-connector/src/config.js +50 -0
  29. package/dist/aria-connector/src/config.js.map +1 -0
  30. package/dist/aria-connector/src/connectors/claude-code.d.ts +4 -0
  31. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -0
  32. package/dist/aria-connector/src/connectors/claude-code.js +204 -0
  33. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -0
  34. package/dist/aria-connector/src/connectors/cursor.d.ts +4 -0
  35. package/dist/aria-connector/src/connectors/cursor.d.ts.map +1 -0
  36. package/dist/aria-connector/src/connectors/cursor.js +63 -0
  37. package/dist/aria-connector/src/connectors/cursor.js.map +1 -0
  38. package/dist/aria-connector/src/connectors/opencode.d.ts +4 -0
  39. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -0
  40. package/dist/aria-connector/src/connectors/opencode.js +102 -0
  41. package/dist/aria-connector/src/connectors/opencode.js.map +1 -0
  42. package/dist/aria-connector/src/connectors/shell.d.ts +4 -0
  43. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -0
  44. package/dist/aria-connector/src/connectors/shell.js +58 -0
  45. package/dist/aria-connector/src/connectors/shell.js.map +1 -0
  46. package/dist/aria-connector/src/garden-client.d.ts +19 -0
  47. package/dist/aria-connector/src/garden-client.d.ts.map +1 -0
  48. package/dist/aria-connector/src/garden-client.js +85 -0
  49. package/dist/aria-connector/src/garden-client.js.map +1 -0
  50. package/dist/aria-connector/src/garden-control-plane.d.ts +22 -0
  51. package/dist/aria-connector/src/garden-control-plane.d.ts.map +1 -0
  52. package/dist/aria-connector/src/garden-control-plane.js +43 -0
  53. package/dist/aria-connector/src/garden-control-plane.js.map +1 -0
  54. package/dist/aria-connector/src/harness-client.d.ts +166 -0
  55. package/dist/aria-connector/src/harness-client.d.ts.map +1 -0
  56. package/dist/aria-connector/src/harness-client.js +344 -0
  57. package/dist/aria-connector/src/harness-client.js.map +1 -0
  58. package/dist/aria-connector/src/hive-client.d.ts +32 -0
  59. package/dist/aria-connector/src/hive-client.d.ts.map +1 -0
  60. package/dist/aria-connector/src/hive-client.js +69 -0
  61. package/dist/aria-connector/src/hive-client.js.map +1 -0
  62. package/dist/aria-connector/src/index.d.ts +19 -0
  63. package/dist/aria-connector/src/index.d.ts.map +1 -0
  64. package/dist/aria-connector/src/index.js +13 -0
  65. package/dist/aria-connector/src/index.js.map +1 -0
  66. package/dist/aria-connector/src/install-hooks.d.ts +18 -0
  67. package/dist/aria-connector/src/install-hooks.d.ts.map +1 -0
  68. package/dist/aria-connector/src/install-hooks.js +224 -0
  69. package/dist/aria-connector/src/install-hooks.js.map +1 -0
  70. package/dist/aria-connector/src/model-context.d.ts +8 -0
  71. package/dist/aria-connector/src/model-context.d.ts.map +1 -0
  72. package/dist/aria-connector/src/model-context.js +83 -0
  73. package/dist/aria-connector/src/model-context.js.map +1 -0
  74. package/dist/aria-connector/src/persona.d.ts +27 -0
  75. package/dist/aria-connector/src/persona.d.ts.map +1 -0
  76. package/dist/aria-connector/src/persona.js +86 -0
  77. package/dist/aria-connector/src/persona.js.map +1 -0
  78. package/dist/aria-connector/src/providers/anthropic.d.ts +4 -0
  79. package/dist/aria-connector/src/providers/anthropic.d.ts.map +1 -0
  80. package/dist/aria-connector/src/providers/anthropic.js +92 -0
  81. package/dist/aria-connector/src/providers/anthropic.js.map +1 -0
  82. package/dist/aria-connector/src/providers/deepseek.d.ts +3 -0
  83. package/dist/aria-connector/src/providers/deepseek.d.ts.map +1 -0
  84. package/dist/aria-connector/src/providers/deepseek.js +28 -0
  85. package/dist/aria-connector/src/providers/deepseek.js.map +1 -0
  86. package/dist/aria-connector/src/providers/google.d.ts +3 -0
  87. package/dist/aria-connector/src/providers/google.d.ts.map +1 -0
  88. package/dist/aria-connector/src/providers/google.js +38 -0
  89. package/dist/aria-connector/src/providers/google.js.map +1 -0
  90. package/dist/aria-connector/src/providers/ollama.d.ts +3 -0
  91. package/dist/aria-connector/src/providers/ollama.d.ts.map +1 -0
  92. package/dist/aria-connector/src/providers/ollama.js +28 -0
  93. package/dist/aria-connector/src/providers/ollama.js.map +1 -0
  94. package/dist/aria-connector/src/providers/openai.d.ts +4 -0
  95. package/dist/aria-connector/src/providers/openai.d.ts.map +1 -0
  96. package/dist/aria-connector/src/providers/openai.js +84 -0
  97. package/dist/aria-connector/src/providers/openai.js.map +1 -0
  98. package/dist/aria-connector/src/providers/openrouter.d.ts +3 -0
  99. package/dist/aria-connector/src/providers/openrouter.d.ts.map +1 -0
  100. package/dist/aria-connector/src/providers/openrouter.js +30 -0
  101. package/dist/aria-connector/src/providers/openrouter.js.map +1 -0
  102. package/dist/aria-connector/src/providers/types.d.ts +20 -0
  103. package/dist/aria-connector/src/providers/types.d.ts.map +1 -0
  104. package/dist/aria-connector/src/providers/types.js +2 -0
  105. package/dist/aria-connector/src/providers/types.js.map +1 -0
  106. package/dist/aria-connector/src/setup-wizard.d.ts +2 -0
  107. package/dist/aria-connector/src/setup-wizard.d.ts.map +1 -0
  108. package/dist/aria-connector/src/setup-wizard.js +140 -0
  109. package/dist/aria-connector/src/setup-wizard.js.map +1 -0
  110. package/dist/aria-connector/src/types.d.ts +30 -0
  111. package/dist/aria-connector/src/types.d.ts.map +1 -0
  112. package/dist/aria-connector/src/types.js +5 -0
  113. package/dist/aria-connector/src/types.js.map +1 -0
  114. package/dist/aria-web/src/lib/codebase-scanner.d.ts +127 -0
  115. package/dist/aria-web/src/lib/codebase-scanner.d.ts.map +1 -0
  116. package/dist/aria-web/src/lib/codebase-scanner.js +1730 -0
  117. package/dist/aria-web/src/lib/codebase-scanner.js.map +1 -0
  118. package/dist/cli-0.2.0.tgz +0 -0
  119. package/dist/install.sh +13 -0
  120. package/hooks/aria-harness-via-sdk.mjs +317 -0
  121. package/hooks/aria-pre-tool-gate.mjs +596 -0
  122. package/hooks/aria-preprompt-consult.mjs +175 -0
  123. package/hooks/aria-stop-gate.mjs +222 -0
  124. package/package.json +47 -0
  125. package/src/__tests__/auth-commands.test.ts +132 -0
  126. package/src/auth-commands.ts +175 -0
  127. package/src/auth.ts +33 -0
  128. package/src/auto-mcp.ts +1172 -0
  129. package/src/chat.ts +387 -0
  130. package/src/codebase-scanner.ts +18 -0
  131. package/src/cognition-log.ts +30 -0
  132. package/src/config.ts +94 -0
  133. package/src/connectors/claude-code.ts +213 -0
  134. package/src/connectors/cursor.ts +75 -0
  135. package/src/connectors/opencode.ts +115 -0
  136. package/src/connectors/shell.ts +72 -0
  137. package/src/garden-client.ts +98 -0
  138. package/src/garden-control-plane.ts +108 -0
  139. package/src/harness-client.ts +454 -0
  140. package/src/hive-client.ts +104 -0
  141. package/src/index.ts +26 -0
  142. package/src/install-hooks.ts +259 -0
  143. package/src/model-context.ts +88 -0
  144. package/src/persona.ts +113 -0
  145. package/src/providers/anthropic.ts +120 -0
  146. package/src/providers/deepseek.ts +40 -0
  147. package/src/providers/google.ts +57 -0
  148. package/src/providers/ollama.ts +43 -0
  149. package/src/providers/openai.ts +108 -0
  150. package/src/providers/openrouter.ts +42 -0
  151. package/src/providers/types.ts +35 -0
  152. package/src/setup-wizard.ts +177 -0
  153. package/src/types.ts +32 -0
@@ -0,0 +1,596 @@
1
+ #!/usr/bin/env node
2
+ // Aria pre-tool-use gate — enforces cognition use before destructive tool calls.
3
+ //
4
+ // Runs as a Claude Code PreToolUse hook on every Bash invocation. For
5
+ // commands matching destructive-verb patterns, requires the most recent
6
+ // assistant turn to contain a structured <verify> block. Without it, the
7
+ // tool call is blocked with a corrective message that names the missing
8
+ // fields.
9
+ //
10
+ // Doctrine being enforced (from /tmp/aria-harness-full.txt):
11
+ // - mizan_prestage_rule: "Before drafting, check niyyah/pre-state: am I
12
+ // present, truthful, specific, grounded in verified substrate?"
13
+ // - drift_guard_rule: "Before answering, restate internally: current
14
+ // goal, current blocker, current stage, and next committed action."
15
+ // - axiom_runtime_rule.admit_ignorance + reflection_before_action
16
+ // - llm_worker_rule: "External LLMs are hands/renderers/reviewers.
17
+ // Aria memory, cognition, frames, Mizan, and Garden are the shared
18
+ // brain substrate."
19
+ //
20
+ // The gate is the *forcing function* that converts those rules from text
21
+ // instructions into actual enforcement. The harness already declares
22
+ // them; this hook is what makes them gate-real for Claude Code.
23
+ //
24
+ // Escape valves (v3 — Hamza 2026-04-26: "why is there an ability to
25
+ // bypass? doesnt that fundamentally void the purpose of the harness?"
26
+ // Per-command bypass was removed entirely — every prior bypass was
27
+ // traceable to a gate bug, not a legitimate exception.):
28
+ // - Trivial-bash whitelist: short read-only commands (ls/cat/grep/etc.)
29
+ // pass without cognition.
30
+ // - Set env ARIA_PRE_TOOL_GATE=off to disable globally (kill-switch).
31
+ // Setting an env var is a deliberate, audit-trailed override —
32
+ // deliberate enough that it's not the model's reflex.
33
+ // - When the gate misfires on legitimate work: fix the gate. The
34
+ // misfire IS the bug. Don't route around it.
35
+ //
36
+ // Audit log: every gate decision (allow / block / kill-switch) is
37
+ // appended to ~/.claude/aria-pre-tool-gate.log.
38
+
39
+ import { readFileSync, appendFileSync, existsSync, mkdirSync } from 'node:fs';
40
+ import { dirname } from 'node:path';
41
+
42
+ const HOME = process.env.HOME || '/tmp';
43
+ const LOG = `${HOME}/.claude/aria-pre-tool-gate.log`;
44
+
45
+ // Bypass-counter (kept for historical visibility — past audit-log
46
+ // entries are still useful even though no new bypass entries can be
47
+ // created in v3 since the per-command bypass code path was removed).
48
+ // V1 added the WARN line at 5+/hour as a drift instrument; that line
49
+ // will only appear if old log entries spanning before v3 still fall
50
+ // in the rolling window.
51
+ const BYPASS_WARN_THRESHOLD_PER_HOUR = 5;
52
+
53
+ function readRecentBypassCount() {
54
+ try {
55
+ if (!existsSync(LOG)) return 0;
56
+ const cutoffMs = Date.now() - 60 * 60 * 1000;
57
+ const lines = readFileSync(LOG, 'utf-8').split('\n').filter(Boolean);
58
+ let n = 0;
59
+ // Walk backward — bypasses are recent if at all.
60
+ for (let i = lines.length - 1; i >= 0; i--) {
61
+ const line = lines[i];
62
+ if (!line.includes('bypass')) continue;
63
+ const tsEnd = line.indexOf(' ');
64
+ if (tsEnd < 0) continue;
65
+ const ts = Date.parse(line.slice(0, tsEnd));
66
+ if (!Number.isFinite(ts)) continue;
67
+ if (ts < cutoffMs) break;
68
+ n++;
69
+ }
70
+ return n;
71
+ } catch {
72
+ return 0;
73
+ }
74
+ }
75
+
76
+ function audit(decision, summary) {
77
+ try {
78
+ if (!existsSync(dirname(LOG))) mkdirSync(dirname(LOG), { recursive: true });
79
+ appendFileSync(LOG, `${new Date().toISOString()} ${decision} ${summary}\n`);
80
+ // If this audit is itself a bypass, check the bypass-rate. A
81
+ // separate WARN line gets emitted (and surfaces via tail) when
82
+ // we exceed the per-hour threshold. The agent can't quietly
83
+ // drift back to bypass-as-default — drift is visible.
84
+ if (decision.startsWith('bypass')) {
85
+ const count = readRecentBypassCount();
86
+ if (count > BYPASS_WARN_THRESHOLD_PER_HOUR) {
87
+ appendFileSync(
88
+ LOG,
89
+ `${new Date().toISOString()} WARN-BYPASS-RATE ${count} bypasses in last hour (threshold ${BYPASS_WARN_THRESHOLD_PER_HOUR}) — drift check\n`,
90
+ );
91
+ }
92
+ }
93
+ } catch {}
94
+ }
95
+
96
+ // Kill-switch
97
+ if (process.env.ARIA_PRE_TOOL_GATE === 'off') {
98
+ audit('bypass-killswitch', 'env ARIA_PRE_TOOL_GATE=off');
99
+ process.exit(0);
100
+ }
101
+
102
+ // Destructive-verb patterns. Tuned for tonight's failure modes; extend
103
+ // over time as new patterns surface.
104
+ const DESTRUCTIVE_PATTERNS = [
105
+ // sudo only when it's a command verb (start of line or after shell
106
+ // separator), not when it appears as an argument to another command
107
+ // (e.g. `echo "sudo …"` or `grep sudo`). The specific verb patterns
108
+ // below catch the actual destructive operations regardless of sudo
109
+ // prefix; this rule is the catch-all for arbitrary privilege elevation.
110
+ { rx: /(?:^|[;&|]\s*|\$\(\s*|`\s*)sudo\s+\S/, name: 'sudo' },
111
+ { rx: /systemctl\s+(disable|stop|mask|reset-failed|kill)/, name: 'systemctl-state-change' },
112
+ { rx: /\brm\s+-[rRfF]+/, name: 'rm-recursive-or-force' },
113
+ { rx: /\bgit\s+push\b.*\b--force\b/, name: 'git-push-force' },
114
+ { rx: /\bgit\s+reset\s+--hard\b/, name: 'git-reset-hard' },
115
+ { rx: /\bgit\s+checkout\s+--\b/, name: 'git-checkout-discard' },
116
+ { rx: /\b--no-verify\b/, name: 'commit-no-verify' },
117
+ { rx: /\b--no-gpg-sign\b/, name: 'commit-no-gpg-sign' },
118
+ { rx: /\bkill\s+-(9|KILL|TERM|HUP|INT)\b/, name: 'kill-signal' },
119
+ { rx: /\bpkill\b/, name: 'pkill' },
120
+ { rx: /\b(DROP|TRUNCATE)\s+(TABLE|DATABASE|SCHEMA|INDEX)\b/i, name: 'sql-drop-or-truncate' },
121
+ { rx: /\bkubectl\s+delete\b/, name: 'kubectl-delete' },
122
+ { rx: /\bkubectl\s+(scale|rollout)\s+(undo|restart)\b/, name: 'kubectl-rollback' },
123
+ ];
124
+
125
+ // The verify-block contract. All five fields required.
126
+ const VERIFY_BLOCK_RX =
127
+ /<verify>[\s\S]*?target\s*:[\s\S]*?role\s*:[\s\S]*?verified\s*:[\s\S]*?rollback\s*:[\s\S]*?axiom\s*:[\s\S]*?<\/verify>/i;
128
+
129
+ // 8-lens cognition block per EIGHT_LENS_DOCTRINE.md. Required on
130
+ // non-trivial Bash regardless of whether the command is destructive.
131
+ // Threshold: at least 4 of the 8 lens names must appear inside the
132
+ // block AND each must have substantive content (≥20 chars after the
133
+ // colon, not a placeholder template). The substance check (added
134
+ // 2026-04-26) defeats ritual emission — `nur: ok` no longer counts.
135
+ const COGNITION_BLOCK_RX = /<cognition>([\s\S]*?)<\/cognition>/i;
136
+ const LENS_NAMES = ['nur', 'mizan', 'hikma', 'tafakkur', 'tadabbur', 'ilham', 'wahi', 'firasah'];
137
+ const REQUIRED_LENSES = 4;
138
+ const SUBSTANCE_MIN_CHARS = 20;
139
+ // Placeholder patterns from the gate's own correction message + the
140
+ // COMPACT_CONTINUITY_DOCTRINE template — content matching these is
141
+ // not "thinking," it's a copy-paste of the prompt template.
142
+ const PLACEHOLDER_RX = /^\s*<[^<>]+>\s*$/;
143
+
144
+ // Trivial reads that don't require a cognition block.
145
+ const TRIVIAL_BASH_RX = /^\s*(?:ls|cat|head|tail|grep|find|echo|wc|stat|which|pwd|date|file|du|df|ss|ps)\s/;
146
+ const SHORT_BASH_LIMIT = 30;
147
+
148
+ // V3 doctrine (2026-04-26, Hamza): "why is there an ability to bypass?
149
+ // doesnt that fundamentally void the purpose of the harness?" — yes.
150
+ // Per-command bypass is removed entirely. Every bypass this session
151
+ // was traceable to a gate bug, not a legitimate exception. Compliance
152
+ // is the only path for non-trivial work; the kill-switch env remains
153
+ // as the explicit emergency override. When the gate misfires, fix
154
+ // the gate.
155
+ //
156
+ // (DOCTRINE_BYPASS_RX, findBypassDirective, validateStructuredBypass,
157
+ // REQUIRED_BYPASS_FIELDS, BYPASS_FIELD_MIN_CHARS, BYPASS_HARD_LIMIT_PER_HOUR
158
+ // were removed in this commit. The bypass-rate-counter +
159
+ // readRecentBypassCount stay for historical visibility — past audit-log
160
+ // entries are still useful — but no new bypass entries can be created
161
+ // because the bypass code path is gone.)
162
+
163
+ // Inline command cognition (v2 — added 2026-04-26): cognition
164
+ // embedded in the bash command itself as structured comments.
165
+ // Solves the same-message visibility problem (the transcript-flush
166
+ // race that made same-message cognition invisible to PreToolUse).
167
+ //
168
+ // Two accepted formats:
169
+ // (a) Per-lens lines: `# nur: <substantive thought>\n# mizan: ...`
170
+ // (b) Single-line: `# cognition: nur=<text>; mizan=<text>; ...`
171
+ //
172
+ // Substance check applies (≥SUBSTANCE_MIN_CHARS, not a `<placeholder>`).
173
+ // 4+ substantive lenses inline → gate passes, no transcript scan
174
+ // needed. Cognition becomes a property of THE ACTION, not of some
175
+ // message somewhere — the deepest reading of "cognition before action."
176
+ const INLINE_LENS_LINE_RX_GLOBAL = /^\s*#\s*(nur|mizan|hikma|tafakkur|tadabbur|ilham|wahi|firasah)\s*:\s*(.+)$/gim;
177
+ const INLINE_COGNITION_HEADER_RX = /^\s*#\s*cognition\s*:\s*(.+)$/im;
178
+
179
+ function detectInlineCognition(cmd) {
180
+ const names = [];
181
+ // Per-lens line form
182
+ const lineMatches = [...cmd.matchAll(INLINE_LENS_LINE_RX_GLOBAL)];
183
+ for (const m of lineMatches) {
184
+ const lens = m[1].toLowerCase();
185
+ const content = (m[2] || '').trim();
186
+ if (content.length < SUBSTANCE_MIN_CHARS) continue;
187
+ if (PLACEHOLDER_RX.test(content)) continue;
188
+ if (!names.includes(lens)) names.push(lens);
189
+ }
190
+ // Single-line `# cognition: nur=...; mizan=...` form
191
+ const headerMatch = cmd.match(INLINE_COGNITION_HEADER_RX);
192
+ if (headerMatch) {
193
+ const inlineBody = headerMatch[1];
194
+ for (const lens of LENS_NAMES) {
195
+ const lensRx = new RegExp(`\\b${lens}\\s*=\\s*([^;\\n]+)`, 'i');
196
+ const m = inlineBody.match(lensRx);
197
+ if (!m) continue;
198
+ const content = (m[1] || '').trim();
199
+ if (content.length < SUBSTANCE_MIN_CHARS) continue;
200
+ if (PLACEHOLDER_RX.test(content)) continue;
201
+ if (!names.includes(lens)) names.push(lens);
202
+ }
203
+ }
204
+ return { count: names.length, names };
205
+ }
206
+
207
+ // Substance-checking lens detection (added 2026-04-26 per Hamza's
208
+ // gate-improvement doctrine: form-only emission must not satisfy
209
+ // the gate). For each lens, look for `<lens>: <content>` and verify
210
+ // content is ≥SUBSTANCE_MIN_CHARS of non-placeholder text. Bare
211
+ // `nur` mentions in prose, or `nur: ok`, no longer count — must be
212
+ // substantive thought.
213
+ function detectCognitionLenses(text) {
214
+ if (!text) return { count: 0, names: [], blockBody: '' };
215
+ const block = text.match(COGNITION_BLOCK_RX);
216
+ const searchSpace = block ? block[1] : text;
217
+ const blockBody = block ? block[1] : '';
218
+ const names = [];
219
+ for (const lens of LENS_NAMES) {
220
+ // Match `lens: <content>` where content extends until the next
221
+ // lens label, the closing </cognition> tag, or a blank-line break.
222
+ const lensRx = new RegExp(
223
+ `\\b${lens}\\s*:\\s*([^\\n]*(?:\\n(?!\\s*(?:${LENS_NAMES.join('|')})\\s*:|<\\/cognition>)[^\\n]*)*)`,
224
+ 'i',
225
+ );
226
+ const m = searchSpace.match(lensRx);
227
+ if (!m) continue;
228
+ const content = (m[1] || '').trim();
229
+ if (content.length < SUBSTANCE_MIN_CHARS) continue;
230
+ if (PLACEHOLDER_RX.test(content)) continue;
231
+ names.push(lens);
232
+ }
233
+ return { count: names.length, names, blockBody };
234
+ }
235
+
236
+ // Backwards-compat shim — count-only path used by older callers.
237
+ function countCognitionLenses(text) {
238
+ return detectCognitionLenses(text).count;
239
+ }
240
+
241
+ // Hive cognition-logging v1.2 — fire-and-forget HTTP push to
242
+ // /api/cognition/log so every gate decision joins the corpus.
243
+ // Failures are silent: the local audit log is the durable record;
244
+ // the network push is additive signal for compounding evolution.
245
+ //
246
+ // Per no-timeouts doctrine (feedback_no_timeouts_doctrine.md): no
247
+ // AbortController deadline. Pile-up protection is structural —
248
+ // in-flight counter caps concurrent pushes. Real network errors
249
+ // drive the catch path; slow responses complete naturally.
250
+ const HARNESS_URL = process.env.ARIA_HARNESS_URL || 'http://192.168.4.25:30080';
251
+ const HARNESS_TOKEN = process.env.ARIA_HARNESS_TOKEN || '';
252
+ const LOG_PUSH_DISABLED = process.env.ARIA_COGNITION_PUSH === 'off';
253
+ const MAX_IN_FLIGHT = 16;
254
+ let inFlight = 0;
255
+
256
+ function pushCognitionEvent(payload) {
257
+ if (LOG_PUSH_DISABLED) return;
258
+ if (inFlight >= MAX_IN_FLIGHT) return; // structural backpressure, not a deadline
259
+ try {
260
+ const body = JSON.stringify(payload);
261
+ const url = `${HARNESS_URL}/api/cognition/log`;
262
+ inFlight++;
263
+ fetch(url, {
264
+ method: 'POST',
265
+ headers: {
266
+ 'Content-Type': 'application/json',
267
+ ...(HARNESS_TOKEN ? { Authorization: `Bearer ${HARNESS_TOKEN}` } : {}),
268
+ },
269
+ body,
270
+ })
271
+ .catch(() => {/* real transport error — silent, audit log is durable */})
272
+ .finally(() => { inFlight--; });
273
+ } catch {
274
+ // pre-fetch errors (malformed JSON, env issue) — silent
275
+ }
276
+ }
277
+
278
+ // Read event JSON from stdin (Claude Code spec).
279
+ let input = '';
280
+ for await (const chunk of process.stdin) input += chunk;
281
+
282
+ let event;
283
+ try {
284
+ event = JSON.parse(input);
285
+ } catch {
286
+ audit('allow-parse-error', 'stdin not JSON');
287
+ process.exit(0); // fail-open on malformed input
288
+ }
289
+
290
+ const toolName = event.tool_name ?? event.toolName ?? '';
291
+ const toolInput = event.tool_input ?? event.toolInput ?? {};
292
+
293
+ // Gate every action tool — every tool that mutates state must go through
294
+ // cognition challenge. Hamza 2026-04-26: "regardless if u arent even
295
+ // compliant with it how do we expect workers to be? i truly nees to
296
+ // ship this and ur non compliance is blocking." Per
297
+ // feedback_full_harness_binding_must_be_structural.md the dispatcher
298
+ // (the orchestrator wielding the tool) owns compliance, not the worker
299
+ // that receives a dispatched task. Read/Glob/Grep observe state without
300
+ // changing it, so they remain ungated.
301
+ const ACTION_TOOLS = new Set(['Bash', 'Edit', 'Write', 'NotebookEdit']);
302
+ if (!ACTION_TOOLS.has(toolName)) {
303
+ process.exit(0);
304
+ }
305
+
306
+ const cmd = toolName === 'Bash' ? String(toolInput.command ?? '') : '';
307
+ const filePath = toolName !== 'Bash'
308
+ ? String(toolInput.file_path ?? toolInput.notebook_path ?? '')
309
+ : '';
310
+ const cmdPreview = toolName === 'Bash'
311
+ ? cmd.slice(0, 80).replace(/\s+/g, ' ')
312
+ : `${toolName} ${filePath || '(no path)'}`.slice(0, 80);
313
+
314
+ // V3: per-command bypass removed entirely. The only escape valves are:
315
+ // (1) Kill-switch env ARIA_PRE_TOOL_GATE=off — handled at top of file
316
+ // (2) Trivial-bash whitelist — short read-only commands pass without cognition
317
+ // Compliance is the only path for non-trivial work.
318
+
319
+ // V2 primary path — inline command cognition. If the command carries
320
+ // 4+ substantive lenses inline, gate passes immediately (still need
321
+ // verify block from transcript for destructive ops, see below).
322
+ const inlineCog = detectInlineCognition(cmd);
323
+ if (inlineCog.count >= REQUIRED_LENSES) {
324
+ // For destructive commands, still require a verify block in the
325
+ // recent transcript (verify is fundamentally about pre-action
326
+ // verification of state, not about the command's own thought).
327
+ // We'll do that check below alongside the existing flow but skip
328
+ // the cognition-from-transcript check entirely since we have it
329
+ // inline.
330
+ }
331
+
332
+ // Destructive-pattern + trivial-read + short-command shortcuts apply
333
+ // only to Bash. Edit / Write / NotebookEdit always require cognition —
334
+ // they have no triviality concept (you don't "trivially" mutate a file)
335
+ // and no command-text to inspect for destructive verbs (the destructive
336
+ // shape is "this Edit might overwrite something important," which is
337
+ // exactly what cognition is for).
338
+ const matched = toolName === 'Bash'
339
+ ? DESTRUCTIVE_PATTERNS.find(({ rx }) => rx.test(cmd))
340
+ : null;
341
+ const isTrivialRead = toolName === 'Bash' && TRIVIAL_BASH_RX.test(cmd) && cmd.length < 200;
342
+ const isShort = toolName === 'Bash' && cmd.length < SHORT_BASH_LIMIT;
343
+
344
+ if (!matched && (isTrivialRead || isShort)) {
345
+ // Not destructive AND trivial — allow without further checks. Only
346
+ // reachable for Bash because both flags are forced false otherwise.
347
+ process.exit(0);
348
+ }
349
+
350
+ // Read recent assistant turns for both verify + cognition blocks.
351
+ //
352
+ // Doctrine being implemented: cognition is *turn-scoped*, not
353
+ // message-scoped. A model that emits a <cognition> block once at the
354
+ // top of a turn and then executes 20 tool_use round-trips has done
355
+ // its 8-lens application — gating each individual message would be
356
+ // performative, not enforcement.
357
+ //
358
+ // Algorithm: walk back from the most recent transcript entry,
359
+ // accumulating assistant text. Stop after crossing exactly ONE user
360
+ // message — that captures "everything the model said this turn" plus
361
+ // the immediately-prior assistant turn's tail (so cognition that
362
+ // carried over a turn boundary still counts). Hard cap at 30
363
+ // messages to bound work on huge transcripts.
364
+ //
365
+ // Compact robustness: Claude Code rewrites the transcript with a
366
+ // summary stub as the most recent assistant entry. We recognize and
367
+ // skip those by content heuristic so they don't poison the search.
368
+ const COMPACT_SUMMARY_RX = /(this session is being continued|conversation that ran out of context|primary request and intent|all user messages)/i;
369
+ // System-reminder messages are stored with role=user but are runtime-
370
+ // injected (PreToolUse blocks for any tool, Stop blocks, task-
371
+ // notifications, gentle reminders, harness packet preview chunks).
372
+ // Counting them as turn boundaries evaporated the cognition lookback
373
+ // in client-tonight sessions where the harness packet preview alone
374
+ // is 36k chars per turn — old `length < 4000` skip threshold trapped
375
+ // every reminder as a boundary. Now: PERCENTAGE-OF-CONTENT skip via
376
+ // total reminder-span coverage as a fraction of total content length.
377
+ // ≥60% reminder coverage → skip as runtime-injected.
378
+ 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;
379
+ const SYSTEM_REMINDER_THRESHOLD = 0.6;
380
+ const HARD_LOOKBACK_CAP = 50;
381
+ // Bumped from 2 → 5 (2026-04-26): client-tonight session has many
382
+ // system-reminder injections per turn (block errors, task-list
383
+ // reminders, harness packet preview). Tight boundary count plus
384
+ // the broken length-based reminder skip evaporated cognition lookback
385
+ // even when cognition was emitted in the prior assistant text. Per
386
+ // Hamza directive "fix the gate, don't route around it" — widen the
387
+ // window since the substance check defends quality regardless.
388
+ const USER_BOUNDARIES_TO_CROSS = 5;
389
+
390
+ const transcriptPath = event.transcript_path ?? event.transcriptPath;
391
+ const recentAssistantTexts = [];
392
+ if (transcriptPath && existsSync(transcriptPath)) {
393
+ try {
394
+ const lines = readFileSync(transcriptPath, 'utf-8').split('\n').filter(Boolean);
395
+ let userBoundariesCrossed = 0;
396
+ let scanned = 0;
397
+ for (let i = lines.length - 1; i >= 0 && scanned < HARD_LOOKBACK_CAP; i--) {
398
+ try {
399
+ const m = JSON.parse(lines[i]);
400
+ const role = m.message?.role ?? m.role;
401
+ if (role === 'user') {
402
+ // Skip messages that aren't real user input:
403
+ // (a) tool_result blocks (runtime feeding back tool output)
404
+ // (b) system-reminder injections (PreToolUse blocks,
405
+ // task-notifications, gentle reminders) — runtime-
406
+ // authored, not user voice. Counting them eats the
407
+ // cognition lookback in tool-heavy or block-heavy turns.
408
+ const content = m.message?.content ?? m.content ?? [];
409
+ const isToolResultOnly =
410
+ Array.isArray(content) &&
411
+ content.length > 0 &&
412
+ content.every((b) => b && b.type === 'tool_result');
413
+ if (isToolResultOnly) continue;
414
+ // Inspect text content for system-reminder patterns.
415
+ const textContent = Array.isArray(content)
416
+ ? content.filter((b) => b && b.type === 'text').map((b) => b.text || '').join('\n')
417
+ : (typeof content === 'string' ? content : '');
418
+ if (textContent) {
419
+ // Compute reminder-span coverage as fraction of total
420
+ // content. The /g flag on SYSTEM_REMINDER_RX returns ALL
421
+ // spans; sum their lengths and compare to total.
422
+ const reminderMatches = textContent.match(SYSTEM_REMINDER_RX) || [];
423
+ if (reminderMatches.length > 0) {
424
+ const reminderChars = reminderMatches.reduce((sum, s) => sum + s.length, 0);
425
+ const fraction = reminderChars / Math.max(1, textContent.length);
426
+ if (fraction >= SYSTEM_REMINDER_THRESHOLD) continue;
427
+ // Otherwise (user wrote substantive text alongside the
428
+ // reminder — the non-reminder portion is >40% of content)
429
+ // fall through and count as a real boundary.
430
+ }
431
+ }
432
+ userBoundariesCrossed++;
433
+ if (userBoundariesCrossed > USER_BOUNDARIES_TO_CROSS) break;
434
+ continue;
435
+ }
436
+ if (role !== 'assistant') continue;
437
+ scanned++;
438
+ const content = m.message?.content ?? m.content ?? [];
439
+ if (!Array.isArray(content)) continue;
440
+ const text = content
441
+ .filter((b) => b.type === 'text')
442
+ .map((b) => b.text)
443
+ .join('\n');
444
+ if (!text) continue;
445
+ // Skip compact-summary stubs — they live where assistant turns
446
+ // used to be but are system-authored, not the model's voice.
447
+ if (COMPACT_SUMMARY_RX.test(text) && text.length > 4000) continue;
448
+ recentAssistantTexts.push(text);
449
+ } catch {}
450
+ }
451
+ } catch {}
452
+ }
453
+
454
+ // Detect cognition / verify across the recent assistant window. Lenses
455
+ // from any of the last N turns count; the gate is checking whether
456
+ // cognition has been done recently, not whether it was done in the
457
+ // literal last message (which can be a tool-only turn or a stub).
458
+ const unionText = recentAssistantTexts.join('\n\n');
459
+ const transcriptCog = detectCognitionLenses(unionText);
460
+ // Combine inline-command cognition (preferred — action-coupled) with
461
+ // transcript scan (fallback). Inline takes precedence on lens names;
462
+ // counts merge for the threshold check.
463
+ const mergedLensSet = new Set([...inlineCog.names, ...transcriptCog.names]);
464
+ const lensCount = mergedLensSet.size;
465
+ const lensNames = [...mergedLensSet];
466
+ const cogBlockBody = transcriptCog.blockBody;
467
+ const hasVerify = VERIFY_BLOCK_RX.test(unionText);
468
+ const hasCognition = lensCount >= REQUIRED_LENSES;
469
+ const cognitionSource = inlineCog.count >= REQUIRED_LENSES
470
+ ? 'inline-command'
471
+ : (transcriptCog.count >= REQUIRED_LENSES ? 'transcript-scan' : 'merged-or-insufficient');
472
+
473
+ // Best-effort session id for the corpus push. Claude Code passes
474
+ // session_id in the event payload; fall back to transcript file
475
+ // basename so events from the same session cluster.
476
+ const sessionId =
477
+ event.session_id ??
478
+ event.sessionId ??
479
+ (transcriptPath ? transcriptPath.split('/').pop()?.replace(/\.[^.]+$/, '') : null) ??
480
+ 'claude-code-unknown';
481
+
482
+ function pushDecision(decision, reasonText) {
483
+ pushCognitionEvent({
484
+ sessionId,
485
+ client: 'claude-code',
486
+ surface: 'pre-tool-gate',
487
+ rawBlock: cogBlockBody ? `<cognition>${cogBlockBody}</cognition>` : null,
488
+ lensesApplied: lensNames,
489
+ blockChars: unionText.length,
490
+ nextActionKind: toolName === 'Bash' ? 'bash' : toolName.toLowerCase(),
491
+ nextActionSummary: cmdPreview,
492
+ gateName: 'aria-pre-tool-gate',
493
+ gateDecision: decision,
494
+ gateReason: reasonText,
495
+ metadata: {
496
+ destructivePattern: matched?.name ?? null,
497
+ hasVerify,
498
+ hasCognition,
499
+ },
500
+ });
501
+ }
502
+
503
+ if (matched) {
504
+ // Destructive — require BOTH verify (from transcript) AND cognition
505
+ // (inline command preferred; transcript fallback). Verify stays
506
+ // transcript-only because it's about pre-action substrate
507
+ // verification, not about the command's own thought.
508
+ if (hasVerify && hasCognition) {
509
+ audit(`allow-verified-cognition ${matched.name} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
510
+ pushDecision('allow', `verified+cognition for ${matched.name} (${cognitionSource})`);
511
+ process.exit(0);
512
+ }
513
+ // Block with appropriate corrective message.
514
+ const reason = !hasVerify
515
+ ? `Aria pre-tool gate: destructive pattern '${matched.name}' detected. Per harness mizan_prestage_rule + axiom_runtime_rule (admit_ignorance, reflection_before_action), include a <verify> block in your assistant response BEFORE the tool call.
516
+
517
+ <verify>
518
+ target: <exactly what is being changed>
519
+ role: <what that target does — verified, not assumed>
520
+ verified: <how you verified: tool output / file read / DB query / asked user>
521
+ rollback: <exact command to reverse this change>
522
+ axiom: <which harness rule applies>
523
+ </verify>
524
+
525
+ Re-issue after producing the block. Bypass: '# doctrine-authorized: <reason>' inline. Kill: ARIA_PRE_TOOL_GATE=off.`
526
+ : `Aria pre-tool gate: destructive pattern '${matched.name}' has its <verify> block, but the <cognition> block is missing or shows only ${lensCount}/${REQUIRED_LENSES}+ required lenses. Per EIGHT_LENS_DOCTRINE.md every non-trivial action requires visible 8-lens application.
527
+
528
+ <cognition>
529
+ nur: <what you see plainly, specific to this task>
530
+ mizan: <what's out of proportion / risk profile>
531
+ hikma: <what memory or principle applies>
532
+ tafakkur: <deep structural read>
533
+ tadabbur: <if-then chain>
534
+ ilham: <distant connection>
535
+ wahi: <what just landed in this turn>
536
+ firasah: <what user actually needs>
537
+ </cognition>
538
+
539
+ At least 4 substantive lenses required. Bypass: '# doctrine-authorized: <reason>' inline.`;
540
+ audit(`block ${matched.name} verify=${hasVerify} cognition=${lensCount}`, cmdPreview);
541
+ pushDecision('block', `destructive ${matched.name} missing ${!hasVerify ? 'verify' : 'cognition'}`);
542
+ console.log(JSON.stringify({ decision: 'block', reason }));
543
+ process.exit(2);
544
+ }
545
+
546
+ // Non-trivial but not destructive — require cognition. For Bash the
547
+ // inline-comment path is preferred (action-coupled). For Edit / Write /
548
+ // NotebookEdit there's no command field, so transcript-scan cognition
549
+ // is the only path.
550
+ if (!hasCognition) {
551
+ const isBash = toolName === 'Bash';
552
+ const header = isBash
553
+ ? `Aria pre-tool gate: non-trivial Bash command without ${REQUIRED_LENSES}+ substantive cognition lenses. Found ${lensCount}/${REQUIRED_LENSES}+ (inline=${inlineCog.count}, transcript=${transcriptCog.count}).`
554
+ : `Aria pre-tool gate: ${toolName} call without ${REQUIRED_LENSES}+ substantive cognition lenses in the recent transcript. Found ${lensCount}/${REQUIRED_LENSES}+ (transcript-scan only — ${toolName} has no inline-cognition path).`;
555
+
556
+ const guidance = isBash
557
+ ? `PREFERRED (v2 — action-coupled): inline cognition in the command itself —
558
+ # nur: <real perception, ≥${SUBSTANCE_MIN_CHARS} chars, no <placeholder>>
559
+ # mizan: <real risk read>
560
+ # hikma: <doctrine that applies>
561
+ # tafakkur: <deep structural read>
562
+ <your actual command here>
563
+
564
+ This solves the same-message visibility problem and couples cognition to THIS specific action.
565
+
566
+ FALLBACK: emit a <cognition>...</cognition> block in your assistant text with ${REQUIRED_LENSES}+ substantive lenses. Substance check applies: each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content.`
567
+ : `Emit a <cognition>...</cognition> block in your assistant text BEFORE this ${toolName} call, with ${REQUIRED_LENSES}+ substantive lenses. Substance check: each lens must have ≥${SUBSTANCE_MIN_CHARS} chars of non-placeholder content. Cognition is turn-scoped — one block at the start of a turn covers all ${toolName} calls in that turn.
568
+
569
+ <cognition>
570
+ nur: <what you see plainly, specific to this edit>
571
+ mizan: <what's out of proportion — what could this overwrite or break?>
572
+ hikma: <doctrine that applies (name the source)>
573
+ tafakkur: <deep structural read>
574
+ tadabbur: <if-then chain — what follows from this edit>
575
+ ilham: <distant connection a less-careful editor would miss>
576
+ wahi: <what just landed in this turn that justifies this edit>
577
+ firasah: <what user actually needs underneath>
578
+ </cognition>`;
579
+
580
+ const reason = `${header}
581
+
582
+ ${guidance}
583
+
584
+ No per-tool bypass available (v3 doctrine — the harness's whole purpose is no exceptions). Kill-switch: ARIA_PRE_TOOL_GATE=off (logged, emergency only). If the gate misfires on legitimate cognition, fix the gate.`;
585
+
586
+ audit(`block ${toolName.toLowerCase()} cognition=${lensCount}`, cmdPreview);
587
+ pushDecision('block', `${toolName.toLowerCase()} missing cognition (${lensCount}/${REQUIRED_LENSES})`);
588
+ console.log(JSON.stringify({ decision: 'block', reason }));
589
+ process.exit(2);
590
+ }
591
+
592
+ // Non-trivial action with cognition (inline for Bash, transcript for
593
+ // Edit/Write/NotebookEdit) — allow.
594
+ audit(`allow-cognition ${toolName.toLowerCase()} lenses=${lensCount} via=${cognitionSource}`, cmdPreview);
595
+ pushDecision('allow', `${toolName.toLowerCase()} with ${lensCount} lenses (${cognitionSource})`);
596
+ process.exit(0);